caixa_core/layout.rs
1//! Layout invariants — the Rust-enforced package structure.
2//!
3//! This is the caixa analog of Cargo's implicit `src/lib.rs` vs `src/main.rs`
4//! rule: the Rust type system dictates the package shape, and the invariant
5//! checker runs before any build step. [`StandardLayout`] encodes the
6//! canonical layout:
7//!
8//! - `caixa.lisp` — always required
9//! - `lib/<nome>.lisp` — required when `:kind Biblioteca` and
10//! `:bibliotecas` is empty
11//! - each `:bibliotecas` — must resolve on disk
12//! - each `:exe` — must resolve on disk, under `exe/`
13//! - each `:servicos` — must resolve on disk, under `servicos/`
14//!
15//! Filesystem I/O is injected through [`StandardLayout::with_path_exists`]
16//! so tests can run without touching disk.
17
18use std::path::{Path, PathBuf};
19use std::sync::Arc;
20
21use thiserror::Error;
22
23use crate::{Caixa, CaixaKind};
24
25/// Contract — a caixa layout checker.
26pub trait LayoutInvariants {
27 /// Verify every declared path resolves + kind-specific invariants hold.
28 fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError>;
29}
30
31type ExistsFn = Arc<dyn Fn(&Path) -> bool + Send + Sync>;
32
33/// The default layout contract.
34#[derive(Default, Clone)]
35pub struct StandardLayout {
36 path_exists: Option<ExistsFn>,
37}
38
39impl StandardLayout {
40 #[must_use]
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 /// Override how file existence is tested. Useful for in-memory tests.
46 #[must_use]
47 pub fn with_path_exists<F>(mut self, f: F) -> Self
48 where
49 F: Fn(&Path) -> bool + Send + Sync + 'static,
50 {
51 self.path_exists = Some(Arc::new(f));
52 self
53 }
54
55 fn exists(&self, p: &Path) -> bool {
56 self.path_exists
57 .as_ref()
58 .map_or_else(|| p.exists(), |f| f(p))
59 }
60
61 /// Probe an authored path's resolved on-disk location under `root`
62 /// and, on absence, return the paired [`LayoutError::MissingEntry`]
63 /// naming the missing entry at that resolved location with the
64 /// caller-provided canonical `kind` label (from the
65 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] const family).
66 /// Returns the resolved [`PathBuf`] on hit so callers that need to
67 /// run a follow-up per-path gate (the `:exe` /
68 /// `:servicos` sandbox-directory-containment check the peer
69 /// wire-up sites carry) reach it without re-computing
70 /// `root.join(path)`.
71 ///
72 /// Folds the five self-similar
73 /// `let full = root.join(p); if !self.exists(&full) { return
74 /// Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
75 /// blocks at [`Self::verify`] (`:bibliotecas` iteration, `:exe`
76 /// iteration, `:servicos` iteration, `:behavior` on-disk callback-
77 /// path iteration, `:upgrade-from` per-instruction script-path
78 /// iteration) onto one substrate primitive on [`StandardLayout`].
79 /// Every future consumer that wants to probe a declared path
80 /// against an out-of-band filesystem oracle (a per-slot admission
81 /// webhook, a `feira validate --paths` per-caixa admission verb,
82 /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
83 /// admission-webhook floor, a per-cluster overlay resolver
84 /// re-probing a declared path against a cluster-local filesystem
85 /// snapshot) reaches the two-step `root.join` + `exists` + wrap
86 /// dispatch through one call rather than re-inlining the three-
87 /// line block in lockstep with the five wire-up sites.
88 fn probe_declared_entry<P: AsRef<Path>>(
89 &self,
90 path: P,
91 root: &Path,
92 kind: &'static str,
93 ) -> Result<PathBuf, LayoutError> {
94 let full = root.join(path.as_ref());
95 if !self.exists(&full) {
96 return Err(LayoutError::missing_entry(kind, full));
97 }
98 Ok(full)
99 }
100
101 /// Probe an authored path's resolved on-disk location under `root`
102 /// and, on presence, verify the resolved [`PathBuf`] lives inside
103 /// the paired `sandbox_dir` sub-tree; on absence return the paired
104 /// [`LayoutError::MissingEntry`] (via [`Self::probe_declared_entry`])
105 /// and on sandbox escape return `outside_ctor(full)` naming the
106 /// offending resolved path.
107 ///
108 /// Folds the two self-similar `let full = self.probe_declared_entry(
109 /// p, root, <kind>)?; if !full.starts_with(&<slot>_dir) { return
110 /// Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
111 /// [`Self::verify`] (`:exe` iteration, `:servicos` iteration) onto
112 /// one substrate primitive on [`StandardLayout`]. The two wire-ups
113 /// used the identical two-arm cascade around the peer
114 /// [`Self::probe_declared_entry`] hit-arm hand-off, differing only
115 /// in the three names bound at each site — the `kind` label, the
116 /// resolved `sandbox_dir`, and the paired outside-dir
117 /// tuple-variant constructor (`LayoutError::ExeOutsideDir` /
118 /// `LayoutError::ServicoOutsideDir`) — exactly the shape the PRIME
119 /// DIRECTIVE names as a bug.
120 ///
121 /// Diagnostic order preserved: [`LayoutError::MissingEntry`] fires
122 /// before `outside_ctor` on the same iteration (the pre-lift order
123 /// this primitive's sibling [`Self::probe_declared_entry`]
124 /// docstring documents). Every future consumer that wants to probe
125 /// a declared path against an out-of-band filesystem oracle *and*
126 /// gate the resolved path on a sandbox sub-tree — a per-slot
127 /// admission webhook probing a declared `:exe` / `:servicos` entry
128 /// against a mounted cluster-local filesystem snapshot, a
129 /// per-cluster overlay resolver rejecting a sandbox-escape patch,
130 /// the deferred `caixa.pleme.io/v1alpha1/Caixa` CR materializer's
131 /// admission-webhook floor — reaches the two-arm probe + sandbox
132 /// dispatch through one call rather than re-inlining the three-
133 /// line block in lockstep with the two `verify` wire-up sites.
134 fn probe_sandboxed_declared_entry<P: AsRef<Path>>(
135 &self,
136 path: P,
137 root: &Path,
138 kind: &'static str,
139 sandbox_dir: &Path,
140 outside_ctor: fn(PathBuf) -> LayoutError,
141 ) -> Result<PathBuf, LayoutError> {
142 let full = self.probe_declared_entry(path, root, kind)?;
143 if !full.starts_with(sandbox_dir) {
144 return Err(outside_ctor(full));
145 }
146 Ok(full)
147 }
148
149 /// Probe every path yielded by `paths` against the injected oracle
150 /// under `root`; on the first miss return the paired
151 /// [`LayoutError::MissingEntry`] naming the offending entry with the
152 /// caller-provided canonical `kind` label (via the sibling
153 /// [`Self::probe_declared_entry`] primitive), and on empty / all-hit
154 /// arms return `Ok(())`.
155 ///
156 /// Folds the three self-similar per-slot existence-probe *loop*
157 /// blocks at [`Self::verify`] onto one substrate primitive on
158 /// [`StandardLayout`]:
159 ///
160 /// - `:bibliotecas` iteration — `for p in caixa.bibliotecas() { self.
161 /// probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA)?; }`,
162 /// - `:behavior` on-disk callback-path iteration — `for p in
163 /// b.declared_paths() { self.probe_declared_entry(p, root,
164 /// LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK)?; }`,
165 /// - `:upgrade-from` per-instruction script-path iteration — the
166 /// `for entry in caixa.upgrade_from() { for instr in entry.
167 /// instructions() { if let Some(p) = instr.declared_path() { self.
168 /// probe_declared_entry(p, root, LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT)?;
169 /// } } }` nested cascade, flattened at the wire-up site through
170 /// `.iter().flat_map(_::instructions).filter_map(_::declared_path)`.
171 ///
172 /// Three consumers, three identical `for … { self.probe_declared_entry
173 /// (…, kind)?; }` loop shapes, one substrate primitive on
174 /// [`StandardLayout`] closing the duplication the PRIME DIRECTIVE
175 /// names as a bug — sibling to the peer per-arm-probe
176 /// [`Self::probe_declared_entry`] (fda1e35) and the two-arm-sandboxed
177 /// [`Self::probe_sandboxed_declared_entry`] (4940d55) primitives on
178 /// the same layout-pipeline existence-probe axis. Diagnostic order
179 /// preserved: iteration proceeds in the caller-supplied iterator
180 /// order and short-circuits on the first miss, byte-equal to the
181 /// pre-lift `for … { … ? }` loop's first-error return semantics.
182 /// Every future consumer that wants to sweep a per-slot declared-
183 /// path list against an out-of-band filesystem oracle (a per-slot
184 /// admission webhook probing an entire `:bibliotecas` / `:behavior`
185 /// / `:upgrade-from` slot as a unit against a mounted cluster-local
186 /// filesystem snapshot, a `feira validate --exists` per-caixa
187 /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
188 /// materializer's admission-webhook floor, a per-cluster overlay
189 /// resolver re-probing a slot-of-paths after a patch) reaches the
190 /// per-slot batch through one call rather than re-inlining the
191 /// three-line loop body in lockstep with the three `verify` wire-up
192 /// sites.
193 fn probe_declared_entries<I, P>(
194 &self,
195 paths: I,
196 root: &Path,
197 kind: &'static str,
198 ) -> Result<(), LayoutError>
199 where
200 I: IntoIterator<Item = P>,
201 P: AsRef<Path>,
202 {
203 for p in paths {
204 self.probe_declared_entry(p, root, kind)?;
205 }
206 Ok(())
207 }
208
209 /// Probe every path yielded by `paths` against the injected oracle
210 /// under `root` and, on presence, verify the resolved path lives
211 /// inside `sandbox_dir` (via the sibling
212 /// [`Self::probe_sandboxed_declared_entry`] primitive); on the first
213 /// miss return the paired [`LayoutError::MissingEntry`] naming the
214 /// offending entry with the caller-provided canonical `kind` label,
215 /// on the first sandbox-escape return `outside_ctor(full)` naming
216 /// the offending resolved path, and on empty / all-hit arms return
217 /// `Ok(())`.
218 ///
219 /// Folds the two self-similar per-slot sandboxed-existence-probe
220 /// *loop* blocks at [`Self::verify`] onto one substrate primitive on
221 /// [`StandardLayout`]:
222 ///
223 /// - `:exe` iteration — `let exe_dir = root.join(LAYOUT_DIR_EXE); for
224 /// p in caixa.exe() { self.probe_sandboxed_declared_entry(p, root,
225 /// LAYOUT_MISSING_ENTRY_KIND_EXE, &exe_dir, LayoutError::ExeOutsideDir)?; }`,
226 /// - `:servicos` iteration — `let servicos_dir = root.join(
227 /// LAYOUT_DIR_SERVICOS); for p in caixa.servicos() {
228 /// self.probe_sandboxed_declared_entry(p, root,
229 /// LAYOUT_MISSING_ENTRY_KIND_SERVICO, &servicos_dir,
230 /// LayoutError::ServicoOutsideDir)?; }`.
231 ///
232 /// Two consumers, two identical `for … { self.probe_sandboxed_declared_entry
233 /// (…, kind, &sandbox_dir, outside_ctor)?; }` loop shapes, one
234 /// substrate primitive on [`StandardLayout`] closing the duplication
235 /// the PRIME DIRECTIVE names as a bug — sibling to the peer per-slot
236 /// batch [`Self::probe_declared_entries`] (d1ccb0b) primitive on the
237 /// non-sandboxed axis and the per-arm [`Self::probe_declared_entry`]
238 /// (fda1e35) / [`Self::probe_sandboxed_declared_entry`] (4940d55)
239 /// primitives on the per-path axis. The four together close the
240 /// layout-pipeline existence-probe algebra: every wire-up on the
241 /// (per-arm | per-slot batch) × (bare | sandboxed) product now
242 /// routes through one substrate primitive rather than N open-coded
243 /// blocks.
244 ///
245 /// Diagnostic order preserved: iteration proceeds in the caller-
246 /// supplied iterator order and short-circuits on the first miss or
247 /// sandbox-escape, byte-equal to the pre-lift `for … { … ? }` loop's
248 /// first-error return semantics. Within each iteration
249 /// [`LayoutError::MissingEntry`] fires before `outside_ctor` (the
250 /// order the peer [`Self::probe_sandboxed_declared_entry`] primitive
251 /// docstring documents). Every future consumer that wants to sweep a
252 /// per-slot declared-path list against an out-of-band filesystem
253 /// oracle *and* gate each resolved path on a sandbox sub-tree (a
254 /// per-slot admission webhook probing an entire `:exe` / `:servicos`
255 /// slot as a unit against a mounted cluster-local filesystem
256 /// snapshot, a `feira validate --exists --sandbox` per-caixa
257 /// admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
258 /// materializer's admission-webhook floor, a per-cluster overlay
259 /// resolver re-probing a slot-of-sandboxed-paths after a patch)
260 /// reaches the per-slot batch through one call rather than
261 /// re-inlining the three-line loop body in lockstep with the two
262 /// `verify` wire-up sites.
263 fn probe_sandboxed_declared_entries<I, P>(
264 &self,
265 paths: I,
266 root: &Path,
267 kind: &'static str,
268 sandbox_dir: &Path,
269 outside_ctor: fn(PathBuf) -> LayoutError,
270 ) -> Result<(), LayoutError>
271 where
272 I: IntoIterator<Item = P>,
273 P: AsRef<Path>,
274 {
275 for p in paths {
276 self.probe_sandboxed_declared_entry(p, root, kind, sandbox_dir, outside_ctor)?;
277 }
278 Ok(())
279 }
280}
281
282impl std::fmt::Debug for StandardLayout {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 f.debug_struct("StandardLayout")
285 .field("custom_exists", &self.path_exists.is_some())
286 .finish()
287 }
288}
289
290impl LayoutInvariants for StandardLayout {
291 fn verify(&self, caixa: &Caixa, root: &Path) -> Result<(), LayoutError> {
292 let manifest = root.join("caixa.lisp");
293 if !self.exists(&manifest) {
294 return Err(LayoutError::MissingManifest(manifest));
295 }
296
297 // Caixa-identity value-shape gates on the two universal axes
298 // (`:nome`, `:versao`) every substrate-side artifact's
299 // `metadata.name` / version derivation flows through. The
300 // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] doc-
301 // comments name the canonical authoring footguns verbatim —
302 // `:nome` "MyApp" / "my_app" / "team.app" / "-app" / "café"
303 // (DNS-1123 violations the K8s apiserver refuses at admission
304 // time on every derived `metadata.name`: `lareira-<nome>`,
305 // programs.yaml entry, CiliumNetworkPolicy / HTTPRoute names,
306 // `LABEL_APLICACAO` value); `:versao` "0.1" / "v0.1.0" / "latest"
307 // / "^0.1" / "0.1.0.0" (SemVer-2 violations Helm / OCI tag /
308 // `feira publish` git tag / lacre `concrete_versao` /
309 // `:upgrade-from :from` peer matching each refuse downstream).
310 // Until this wire-up landed both validators existed as `pub fn`
311 // on [`Caixa`] (with full per-arm test coverage in
312 // `manifest::tests`) but no production code path called them —
313 // `feira build` (the canonical author-time gate) silently
314 // accepted a malformed `:nome` / `:versao` and the failure
315 // surfaced at `helm install` / `kubectl apply` / `feira publish`
316 // time on the *first* downstream consumer to strict-parse the
317 // value, far from the source `caixa.lisp` and without any field
318 // naming the offending Caixa identity axis. The gate runs
319 // *after* [`LayoutError::MissingManifest`] (no caixa to check
320 // when the manifest is missing) and *before* every kind-coherence
321 // gate (each of which carries `caixa.nome` verbatim in its
322 // diagnostic — running them first on a structurally-invalid
323 // identity would surface a "this kind has slot X" diagnostic
324 // against an unrecoverable name). Cross-axis precedence is
325 // `:nome` → `:versao` — the canonical declaration order on
326 // [`Caixa`] and the same author-grep ordering the
327 // [`ManifestError`] family uses. Same per-axis `*Violation
328 // { caixa, issue }` envelope every peer per-axis wrap exposes
329 // ([`LayoutError::CodePathViolation`] b868442,
330 // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
331 // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
332 // / [`LayoutError::AplicacaoViolation`]).
333 caixa.run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)?;
334 // `:nome`-side joint-length budget on the canonical
335 // `lareira-<nome>` chart-name shape — the second arm on the
336 // shared `:nome` axis after the bare-DNS-1123 gate above. Runs
337 // through the same [`LayoutError::NomeViolation`] envelope so
338 // every per-axis diagnostic on `:nome` carries one wrap shape,
339 // peer with the [`Caixa::validate_nome`] → `NomeInvalid`
340 // routing already at this site. The chart-name budget is the
341 // second-axis ceiling [`Caixa::validate_nome`] cannot see — a
342 // 56-byte DNS-1123-valid `:nome` passes the bare-`:nome` shape
343 // but produces a 64-byte `lareira-<nome>` chart name the
344 // apiserver / `helm lint` rejects at admission, far from the
345 // source `caixa.lisp` and naming none of the joint-length
346 // overflow's three carriers (DNS-1123 cap, prefix, `:nome`
347 // length). Closing it at this wire-up turns the
348 // [`lareira_chart_name`] doc-comment's explicit M4-admission
349 // deferral (caixa-core/src/render.rs:3198) into a build-time
350 // structural property of every emitted artifact.
351 caixa.run_layout_gate(
352 Caixa::validate_nome_chart_name_budget,
353 LayoutError::nome_violation,
354 )?;
355 caixa.run_layout_gate(Caixa::validate_versao, LayoutError::versao_violation)?;
356
357 // `:deps` / `:deps-dev` per-entry shape gate. The third Caixa-
358 // level orphan validator on the universal authoring surface (peer
359 // of [`Caixa::validate_nome`] / [`Caixa::validate_versao`] wired
360 // immediately above): [`Caixa::validate_deps`] walks every
361 // [`Dep::validate`] arm — empty / non-DNS-1123 `:nome`, empty /
362 // unparseable `:versao` requirement, malformed `:fonte` repo /
363 // pin / `:caminho`, malformed `:caracteristicas` Cargo-feature
364 // name (de68c0c) — and then closes the per-list set-not-multiset
365 // duplicate-`:nome` invariant on each of `:deps` and `:deps-dev`
366 // (359fba5). Until this wire-up landed `validate_deps` existed as
367 // `pub fn` on [`Caixa`] with full per-arm unit coverage in
368 // `manifest::tests` + `dep::tests` (validate_deps_rejects_*,
369 // 53 dep-axis tests) but no production code path called it —
370 // `feira build` (the canonical author-time gate;
371 // `caixa-feira/src/cmd/build.rs:29` routes through
372 // `StandardLayout::verify`) silently accepted a malformed `:deps`
373 // entry and the failure surfaced at the *first* downstream
374 // consumer to strict-parse it: at lacre-resolve time as a
375 // `semver::Error` not naming the offending dep (`:versao` per-
376 // entry); at `git clone` time as a fetch failure quoting the
377 // shell-escape `repo` (`:fonte :repo`); at the resolver's
378 // `HashMap<:nome>` collapse as a silent "second-wins" overwrite
379 // (within-list `:nome` duplicate); at `cargo metadata` time as a
380 // feature-name rejection on the *target* caixa rather than the
381 // dep entry referencing it (`:caracteristicas`); at `helm
382 // install` / `kubectl apply` time as an apiserver `metadata.name`
383 // rejection on the rendered `lareira-<nome>` chart's per-dep
384 // derivation (DNS-1123-violating `:deps :nome`) — each far from
385 // the source `caixa.lisp`, none naming the offending `:deps` /
386 // `:deps-dev` axis. Runs *after* the Caixa-identity gates (the
387 // diagnostic carries `caixa.nome().to_string()` verbatim, which the
388 // peer [`Caixa::validate_nome`] gate above has just guaranteed is
389 // a valid DNS-1123 label) and *before* every kind-coherence gate
390 // (the dep surface is universal — every kind has `:deps` /
391 // `:deps-dev` — so its shape diagnostic is more fundamental than
392 // the kind-coherence partitions on `:bibliotecas` / `:exe` /
393 // `:servicos` / `:membros` / `:children` / M2 slots that follow).
394 // Same per-axis `*Violation { caixa, issue }` envelope every peer
395 // per-axis wrap exposes ([`LayoutError::NomeViolation`] /
396 // [`LayoutError::VersaoViolation`] (1f74a5f),
397 // [`LayoutError::CodePathViolation`] (b868442),
398 // [`LayoutError::LimitsViolation`] / [`LayoutError::BehaviorViolation`]
399 // / [`LayoutError::UpgradeViolation`] / [`LayoutError::SupervisorViolation`]
400 // / [`LayoutError::AplicacaoViolation`]). Threads [`DepError`]
401 // Display through verbatim — every per-arm reason already names
402 // the offending dep's `:nome` (e.g. `":deps entry "caixa-teia"
403 // :versao "^bad" is not a valid semver requirement: …"`), so the
404 // wrap envelope's `issue` carries a self-locating "which dep,
405 // which axis, why" without re-shaping the per-arm parser-side
406 // reason. With this wire-up the canonical author-time gate
407 // refuses every ill-formed `:deps` / `:deps-dev` value-shape by
408 // construction — closing the second-to-last orphan-validator gap
409 // on the typed Caixa surface (`validate_restart_window` is the
410 // remaining orphan, Supervisor-axis specific and wired into the
411 // Supervisor branch below alongside `view.validate()`).
412 // Compound per-Caixa entry gate on the dep-graph axis: the
413 // layout pipeline's two-dispatch `:deps` / `:deps-dev` cascade
414 // — the per-entry + within-list duplicate-`:nome` gate (the
415 // [`crate::Dep::validate`] + [`crate::render::insert_first_seen`]
416 // cascade `Caixa::validate_deps` opened on, 359fba5) and the
417 // cross-slot self-edge gate
418 // ([`crate::dep::validate_no_self_dep`], ad4abf1) — folded
419 // onto the [`crate::Caixa::validate_deps`] substrate primitive.
420 // The two arms run in the same canonical order at the primitive
421 // (per-entry + cross-entry duplicate → cross-slot self-edge) so
422 // the fold is byte-for-byte equivalent to the pre-fold
423 // two-block cascade this call site formerly carried, pinned by
424 // the paired
425 // `validate_deps_folds_{per_entry,self_edge}_arm_matches_gate`
426 // equivalence pins and the
427 // `validate_deps_per_entry_arm_fires_before_self_edge_arm`
428 // ordering pin in the [`crate::Caixa::validate_deps`] pin
429 // family (`manifest.rs`).
430 //
431 // Same lift discipline the peer per-slot compound gates
432 // ([`crate::AplicacaoSpec::validate_contratos`] and its
433 // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
434 // [`crate::MeshPolicy::validate`],
435 // [`crate::SupervisorSpec::validate_children`],
436 // [`crate::Caixa::validate_upgrade_from`] d6801df) each carry —
437 // one named substrate-primitive gate per typed slot folds every
438 // structural + cross-slot axis on that slot onto one call, so
439 // every future consumer that wants to re-check the dep-graph
440 // after a per-entry patch (the deferred
441 // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
442 // webhook, a future `feira validate --deps` per-caixa admission
443 // verb, a per-`:deps` overlay resolver) reaches the two-arm
444 // compound gate through one dispatch rather than re-inlining
445 // the two-dispatch cascade in lockstep with this wire-up.
446 caixa.run_layout_gate(Caixa::validate_deps, LayoutError::deps_violation)?;
447
448 // `:etiquetas` per-entry empty + cross-entry duplicate gate. The
449 // fourth universal-axis Caixa-level value-shape gate (peer of
450 // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
451 // [`Caixa::validate_deps`] wired immediately above and
452 // [`Caixa::validate_code_paths`] wired below the kind-coherence
453 // gates) on the typed Caixa surface. `:etiquetas` is the
454 // registry-search-tag axis every kind carries (universal
455 // `Vec<String>` slot on [`Caixa`]) and lands verbatim as the
456 // Helm chart `Chart.yaml` `keywords:` array on every Servico
457 // (`caixa-helm/src/lib.rs:236` folds it through a
458 // [`std::collections::BTreeSet`]). Until this wire-up landed
459 // `:etiquetas` had no shape gate at any layer — an empty entry
460 // (`(:etiquetas (""))` — the canonical paste-from-blank-doc
461 // footgun) silently rendered as `keywords: [""]` in `Chart.yaml`,
462 // and duplicate entries (`(:etiquetas ("demo" "demo"))` — the
463 // copy-paste-the-wrong-tag footgun) were silently dedup'd by
464 // the renderer's `BTreeSet` collect — a "second wins / one
465 // silently disappears" shape divergent from every peer typed-
466 // graph set gate (`:membros :caixa`, `:placement :clusters`,
467 // `:entrada :paths`, `:contratos`, `:deps :nome`,
468 // `:upgrade-from :from`, the per-instruction-class singularity
469 // gates on `:upgrade-from :instructions`). Runs *after* the
470 // peer universal `:nome` / `:versao` / `:deps` gates (declaration
471 // order on [`Caixa`] is `:nome` → `:versao` → `:edicao` →
472 // `:descricao` → `:repositorio` → `:licenca` → `:autores` →
473 // `:etiquetas` → `:deps` → `:deps-dev`, but the gate order
474 // follows the same identity-axis-first cascade the peer gates
475 // establish: `:nome` → `:versao` are the load-bearing identity
476 // axes that flow into every diagnostic's caixa prefix, and
477 // `:deps` is the universal dep surface that dominates every
478 // kind-coherence gate; `:etiquetas` runs after this trio so the
479 // diagnostic carries an already-validated `:nome` and the
480 // peer universal axes' narrower diagnostics surface first when
481 // multiple axes are malformed) and *before* the kind-coherence
482 // gates ([`Self::MeshSlotsOnNonAplicacao`] /
483 // [`Self::SupervisorSlotsOnNonSupervisor`] /
484 // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
485 // — `:etiquetas` is universal so its shape diagnostic is more
486 // fundamental than the kind-coherence partitions on kind-
487 // exclusive slot sets.
488 //
489 // Same per-axis `*Violation { caixa, issue }` envelope every peer
490 // per-axis wrap exposes ([`Self::NomeViolation`] /
491 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
492 // aa77d0f, [`Self::CodePathViolation`] b868442,
493 // [`Self::RestartWindowViolation`] 10e321a). Threads
494 // [`ManifestError::EtiquetaEmpty`] / [`ManifestError::EtiquetaDuplicate`]
495 // Display through verbatim — each per-arm reason already names
496 // the offending tag (for the duplicate arm) or the structural
497 // "empty entry" defect (for the empty arm), so the wrap
498 // envelope's `issue` carries a self-locating "which axis, which
499 // entry, why" without re-shaping the per-arm reason.
500 caixa.run_layout_gate(Caixa::validate_etiquetas, LayoutError::etiquetas_violation)?;
501
502 // `:autores` per-entry empty + cross-entry duplicate gate. The
503 // fifth universal-axis Caixa-level value-shape gate (peer of
504 // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
505 // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] wired
506 // immediately above and [`Caixa::validate_code_paths`] wired
507 // below the kind-coherence gates) on the typed Caixa surface.
508 // `:autores` is the maintainer-axis every kind carries
509 // (universal `Vec<String>` slot on [`Caixa`]) and lands verbatim
510 // as the Helm chart `Chart.yaml` `maintainers:` array on every
511 // Servico (`caixa-helm/src/lib.rs:251` maps each entry to a
512 // `Maintainer { name, email: None }` without dedup). Until this
513 // wire-up landed `:autores` had no shape gate at any layer — an
514 // empty entry (`(:autores (""))` — the canonical paste-from-
515 // blank-doc footgun) silently rendered as
516 // `maintainers: [{name: "", email: null}]` in `Chart.yaml`, and
517 // duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
518 // the copy-paste-the-wrong-author footgun) stacked verbatim in
519 // the chart. Unlike the peer `:etiquetas` axis (where the
520 // renderer's `BTreeSet` collect silently dedups the `keywords:`
521 // array at chart render — a "second wins / one silently
522 // disappears" shape), `maintainers:` has *no* renderer-side
523 // dedup, so duplicate `:autores` entries render as two identical
524 // maintainer records by construction — a strictly worse footgun
525 // than the peer `:etiquetas` shape. Runs *after* the peer
526 // universal `:nome` / `:versao` / `:deps` / `:etiquetas` gates
527 // (the gate order follows the canonical identity-axis-first
528 // cascade the peer gates establish; `:autores` and `:etiquetas`
529 // are the two Vec-shaped universal metadata axes — they sit
530 // adjacent in the cascade after the load-bearing identity +
531 // dep trio) and *before* the kind-coherence gates
532 // ([`Self::MeshSlotsOnNonAplicacao`] /
533 // [`Self::SupervisorSlotsOnNonSupervisor`] /
534 // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
535 // — `:autores` is universal so its shape diagnostic is more
536 // fundamental than the kind-coherence partitions on kind-
537 // exclusive slot sets.
538 //
539 // Same per-axis `*Violation { caixa, issue }` envelope every peer
540 // per-axis wrap exposes ([`Self::NomeViolation`] /
541 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
542 // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
543 // [`Self::CodePathViolation`] b868442,
544 // [`Self::RestartWindowViolation`] 10e321a). Threads
545 // [`ManifestError::AutorEmpty`] / [`ManifestError::AutorDuplicate`]
546 // Display through verbatim — each per-arm reason already names
547 // the offending author (for the duplicate arm) or the structural
548 // "empty entry" defect (for the empty arm), so the wrap
549 // envelope's `issue` carries a self-locating "which axis, which
550 // entry, why" without re-shaping the per-arm reason.
551 caixa.run_layout_gate(Caixa::validate_autores, LayoutError::autores_violation)?;
552
553 // `:repositorio` git-repo-URL shape gate. The sixth
554 // universal-axis Caixa-level value-shape gate (peer of
555 // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
556 // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
557 // [`Caixa::validate_autores`] wired immediately above and
558 // [`Caixa::validate_code_paths`] wired below the kind-coherence
559 // gates) on the typed Caixa surface. `:repositorio` is the
560 // universal git-shaped homepage axis every kind carries
561 // (universal `Option<String>` slot on [`Caixa`]) and routes
562 // through two load-bearing substrate consumers:
563 // [`caixa-helm`] folds it verbatim into the rendered
564 // `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
565 // (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
566 // the chart `README.md` `repo = …` interpolation
567 // (`caixa-helm/src/lib.rs:359`); [`caixa-flux`] folds it
568 // verbatim into the standalone `ClusterBundleOpts::for_caixa`
569 // `git_url:` field (`caixa-flux/src/lib.rs:293`), which
570 // becomes the FluxCD `GitRepository.spec.url` the cluster's
571 // source-controller polls — the load-bearing deploy-time axis.
572 // Both consumers use `Option::unwrap_or_else(|| <fallback>)`
573 // to substitute a placeholder when the slot is absent (`None`
574 // → the fallback fires); a `Some("")` *skips the fallback*
575 // and silently passes the empty string through to
576 // `Chart.yaml home: ""` / `GitRepository url: ""`. Until this
577 // wire-up landed `:repositorio` had no shape gate at any
578 // layer — empty (`(:repositorio "")` — the canonical
579 // paste-from-blank-doc footgun) and malformed (whitespace,
580 // control char / CRLF, leading `-` CLI-arg-injection,
581 // missing `:` separator) values silently landed in the
582 // rendered artifacts and broke at `helm template` / FluxCD
583 // reconcile time far from the source `caixa.lisp`.
584 //
585 // Runs *after* the peer universal `:nome` / `:versao` /
586 // `:deps` / `:etiquetas` / `:autores` gates (the gate order
587 // follows the canonical identity-axis-first cascade the peer
588 // gates establish; `:repositorio` is the universal git-URL
589 // axis — it sits adjacent to `:autores` in the cascade after
590 // the load-bearing identity + dep trio + the two Vec-shaped
591 // universal metadata axes) and *before* the kind-coherence
592 // gates ([`Self::MeshSlotsOnNonAplicacao`] /
593 // [`Self::SupervisorSlotsOnNonSupervisor`] /
594 // [`Self::ServicoSlotsOnNonServico`] / [`Self::ForeignCodeSlot`])
595 // — `:repositorio` is universal so its shape diagnostic is
596 // more fundamental than the kind-coherence partitions on
597 // kind-exclusive slot sets.
598 //
599 // Same per-axis `*Violation { caixa, issue }` envelope every
600 // peer per-axis wrap exposes ([`Self::NomeViolation`] /
601 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
602 // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
603 // [`Self::AutoresViolation`] 86c769b, [`Self::CodePathViolation`]
604 // b868442, [`Self::RestartWindowViolation`] 10e321a). Threads
605 // [`ManifestError::RepositorioEmpty`] /
606 // [`ManifestError::RepositorioInvalid`] Display through
607 // verbatim — each per-arm reason already names the offending
608 // `:repositorio` value (for the invalid arm) or the
609 // structural "empty entry" defect (for the empty arm), so
610 // the wrap envelope's `issue` carries a self-locating "which
611 // axis, which value, why" without re-shaping the per-arm
612 // reason. With this gate the two `git URL`-shaped surfaces on
613 // the typed Caixa (`:repositorio` here, `:deps :fonte :repo`
614 // peer routed through the same shared
615 // [`crate::render::is_git_repo_url`] predicate via
616 // [`crate::DepSource::validate`]) are now structurally
617 // equivalent — every value past validate is
618 // guaranteed-acceptable by the shared predicate's constraint
619 // union, by construction.
620 caixa.run_layout_gate(
621 Caixa::validate_repositorio,
622 LayoutError::repositorio_violation,
623 )?;
624
625 // `:descricao` non-empty shape gate. The seventh universal-
626 // axis Caixa-level value-shape gate (peer of
627 // [`Caixa::validate_nome`] / [`Caixa::validate_versao`] /
628 // [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
629 // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
630 // wired immediately above and [`Caixa::validate_code_paths`]
631 // wired below the kind-coherence gates) on the typed Caixa
632 // surface. `:descricao` is the universal free-form-prose
633 // summary axis every kind carries (universal `Option<String>`
634 // slot on [`Caixa`]) and routes through two load-bearing
635 // [`caixa-helm`] consumers: `build_chart_yaml` folds it
636 // verbatim into the rendered `lareira-<nome>` Helm chart's
637 // `Chart.yaml` `description:` field
638 // (`caixa-helm/src/lib.rs:232-235`), and `build_readme` folds
639 // it verbatim into the chart `README.md` header
640 // (`caixa-helm/src/lib.rs:333-336`). Both consumers use
641 // `Option::unwrap_or_else(|| <fallback>)` to substitute a
642 // `caixa.nome`-derived placeholder when the slot is absent
643 // (`None` → the fallback fires); a `Some("")` *skips the
644 // fallback* and silently passes the empty string through to
645 // `Chart.yaml description: ""` / a blank `README.md` header
646 // — exact same footgun shape as the peer `:repositorio`
647 // surface above. Until this wire-up landed `:descricao` had
648 // no shape gate at any layer — the empty
649 // (`(:descricao "")` — the canonical paste-from-blank-doc
650 // footgun) silently landed in the rendered artifacts and
651 // broke at `helm lint` time (`WARNING [chart.metadata.description]:
652 // description is required` on `apiVersion: v2` charts) far
653 // from the source `caixa.lisp`.
654 //
655 // Runs *after* the peer universal `:nome` / `:versao` /
656 // `:deps` / `:etiquetas` / `:autores` / `:repositorio` gates
657 // (the gate order follows the canonical identity-axis-first
658 // cascade the peer gates establish; `:descricao` is the
659 // universal free-form-prose axis — it sits adjacent to
660 // `:repositorio` in the cascade after the load-bearing
661 // identity + dep trio + the two Vec-shaped universal
662 // metadata axes + the universal git-URL axis) and *before*
663 // the kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`]
664 // / [`Self::SupervisorSlotsOnNonSupervisor`] /
665 // [`Self::ServicoSlotsOnNonServico`] /
666 // [`Self::ForeignCodeSlot`]) — `:descricao` is universal so
667 // its shape diagnostic is more fundamental than the kind-
668 // coherence partitions on kind-exclusive slot sets.
669 //
670 // Same per-axis `*Violation { caixa, issue }` envelope every
671 // peer per-axis wrap exposes ([`Self::NomeViolation`] /
672 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
673 // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
674 // [`Self::AutoresViolation`] 86c769b,
675 // [`Self::RepositorioViolation`] 577b0a9,
676 // [`Self::CodePathViolation`] b868442,
677 // [`Self::RestartWindowViolation`] 10e321a). Threads
678 // [`ManifestError::DescricaoEmpty`] Display through verbatim
679 // — the per-arm reason already names the offending
680 // `:descricao` slot + cites the renderer-side footgun, so
681 // the wrap envelope's `issue` carries a self-locating
682 // "which axis, why" without re-shaping the per-arm reason.
683 caixa.run_layout_gate(Caixa::validate_descricao, LayoutError::descricao_violation)?;
684
685 // `:licenca` non-empty shape gate. The eighth universal-axis
686 // Caixa-level value-shape gate (peer of [`Caixa::validate_nome`]
687 // / [`Caixa::validate_versao`] / [`Caixa::validate_deps`] /
688 // [`Caixa::validate_etiquetas`] / [`Caixa::validate_autores`] /
689 // [`Caixa::validate_repositorio`] / [`Caixa::validate_descricao`]
690 // wired immediately above and [`Caixa::validate_code_paths`]
691 // wired below the kind-coherence gates) on the typed Caixa
692 // surface. `:licenca` is the universal SPDX-shaped license-
693 // expression axis every kind carries (universal `Option<String>`
694 // slot on [`Caixa`]) and routes through one load-bearing
695 // [`caixa-helm`] consumer: `build_readme` folds it verbatim into
696 // the rendered `lareira-<nome>` Helm chart's `README.md` `##
697 // License` section (`caixa-helm/src/lib.rs:361`) via
698 // `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
699 // consumer's fallback only fires when the slot is absent (`None`
700 // → the `MIT` fallback fires); a `Some("")` *skips the
701 // fallback* and silently passes the empty string through to a
702 // chart `README.md` whose `License` section renders as a bare
703 // trailing period — exact same footgun shape as the peer
704 // `:repositorio` (577b0a9) and `:descricao` (4e6db38) surfaces
705 // above. Until this wire-up landed `:licenca` had no shape
706 // gate at any layer — the empty (`(:licenca "")` — the
707 // canonical paste-from-blank-doc footgun) silently landed in
708 // the rendered chart `README.md` far from the source
709 // `caixa.lisp`.
710 //
711 // Runs *after* the peer universal `:nome` / `:versao` /
712 // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
713 // `:descricao` gates (the gate order follows the canonical
714 // identity-axis-first cascade the peer gates establish;
715 // `:licenca` sits adjacent to `:descricao` in the cascade
716 // after the load-bearing identity + dep trio + the two
717 // Vec-shaped universal metadata axes + the universal
718 // git-URL + free-form-prose axes) and *before* the kind-
719 // coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
720 // [`Self::SupervisorSlotsOnNonSupervisor`] /
721 // [`Self::ServicoSlotsOnNonServico`] /
722 // [`Self::ForeignCodeSlot`]) — `:licenca` is universal so
723 // its shape diagnostic is more fundamental than the kind-
724 // coherence partitions on kind-exclusive slot sets.
725 //
726 // Same per-axis `*Violation { caixa, issue }` envelope every
727 // peer per-axis wrap exposes ([`Self::NomeViolation`] /
728 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
729 // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
730 // [`Self::AutoresViolation`] 86c769b,
731 // [`Self::RepositorioViolation`] 577b0a9,
732 // [`Self::DescricaoViolation`] 4e6db38,
733 // [`Self::CodePathViolation`] b868442,
734 // [`Self::RestartWindowViolation`] 10e321a). Threads
735 // [`ManifestError::LicencaEmpty`] Display through verbatim
736 // — the per-arm reason already names the offending
737 // `:licenca` slot + cites the renderer-side footgun, so
738 // the wrap envelope's `issue` carries a self-locating
739 // "which axis, why" without re-shaping the per-arm reason.
740 caixa.run_layout_gate(Caixa::validate_licenca, LayoutError::licenca_violation)?;
741
742 // `:edicao` non-empty shape gate. The ninth (and last
743 // un-gated) universal-axis Caixa-level value-shape gate
744 // (peer of [`Caixa::validate_nome`] / [`Caixa::validate_versao`]
745 // / [`Caixa::validate_deps`] / [`Caixa::validate_etiquetas`] /
746 // [`Caixa::validate_autores`] / [`Caixa::validate_repositorio`]
747 // / [`Caixa::validate_descricao`] / [`Caixa::validate_licenca`]
748 // wired immediately above and [`Caixa::validate_code_paths`]
749 // wired below the kind-coherence gates) on the typed Caixa
750 // surface. `:edicao` is the universal language-edition axis
751 // every kind carries (universal `Option<String>` slot on
752 // [`Caixa`]) that selects the tatara-lisp macro surface +
753 // compatibility flags the substrate applies when building
754 // the caixa. The canonical [`Caixa::template`] scaffold every
755 // `feira init` emits carries `:edicao "2026"` verbatim
756 // (`caixa-core/src/manifest.rs:1193`) and every renderer-side
757 // fixture carries `edicao: Some("2026".into())` by
758 // construction (`caixa-helm/src/lib.rs:375`,
759 // `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
760 // `caixa-core/src/render.rs:2510`). Until this wire-up landed
761 // `:edicao` had no shape gate at any layer — the empty
762 // (`(:edicao "")` — the canonical paste-from-blank-doc
763 // footgun) silently landed as a bare `(:edicao "")` line
764 // in the rendered caixa.lisp and a future renderer-side
765 // consumer that folds the value through
766 // `Option::unwrap_or_else` would skip its fallback (which
767 // only fires on `None`) and pass the empty edition through
768 // to the substrate's build-time edition selector far from
769 // the source `caixa.lisp` — exact same
770 // `Some("")`-skips-`unwrap_or_else` footgun shape as the
771 // peer `:repositorio` (577b0a9), `:descricao` (4e6db38),
772 // and `:licenca` (3d1e535) surfaces above.
773 //
774 // Runs *after* the peer universal `:nome` / `:versao` /
775 // `:deps` / `:etiquetas` / `:autores` / `:repositorio` /
776 // `:descricao` / `:licenca` gates (the gate order follows
777 // the canonical identity-axis-first cascade the peer gates
778 // establish; `:edicao` sits at the tail of the cascade
779 // after the load-bearing identity + dep trio + the two
780 // Vec-shaped universal metadata axes + the three universal
781 // `Option<String>` chart-metadata axes) and *before* the
782 // kind-coherence gates ([`Self::MeshSlotsOnNonAplicacao`] /
783 // [`Self::SupervisorSlotsOnNonSupervisor`] /
784 // [`Self::ServicoSlotsOnNonServico`] /
785 // [`Self::ForeignCodeSlot`]) — `:edicao` is universal so
786 // its shape diagnostic is more fundamental than the kind-
787 // coherence partitions on kind-exclusive slot sets.
788 //
789 // Same per-axis `*Violation { caixa, issue }` envelope every
790 // peer per-axis wrap exposes ([`Self::NomeViolation`] /
791 // [`Self::VersaoViolation`] 1f74a5f, [`Self::DepsViolation`]
792 // aa77d0f, [`Self::EtiquetasViolation`] 360a499,
793 // [`Self::AutoresViolation`] 86c769b,
794 // [`Self::RepositorioViolation`] 577b0a9,
795 // [`Self::DescricaoViolation`] 4e6db38,
796 // [`Self::LicencaViolation`] 3d1e535,
797 // [`Self::CodePathViolation`] b868442,
798 // [`Self::RestartWindowViolation`] 10e321a). Threads
799 // [`ManifestError::EdicaoEmpty`] Display through verbatim
800 // — the per-arm reason already names the offending
801 // `:edicao` slot + cites the renderer-side footgun, so
802 // the wrap envelope's `issue` carries a self-locating
803 // "which axis, why" without re-shaping the per-arm reason.
804 // With this gate every universal-axis `Option<String>`
805 // surface on the typed Caixa (`:repositorio` 577b0a9,
806 // `:descricao` 4e6db38, `:licenca` 3d1e535, `:edicao` here)
807 // now carries the same structural empty-arm gate by
808 // construction.
809 caixa.run_layout_gate(Caixa::validate_edicao, LayoutError::edicao_violation)?;
810
811 // Kind ↔ code-surface coherence on the three no-code kinds
812 // (Supervisor / Aplicacao / Acao) — folded onto one substrate
813 // primitive at [`crate::Caixa::validate_no_code_kind_coherence`].
814 // Pre-lift each of the three arms lived as a self-similar
815 // `if caixa.kind().is_<no-code-kind>() && has_code { return
816 // Err(LayoutError::<kind>_owns_code(caixa)); }` block at this
817 // call site — three consumers, three identical shapes, one
818 // substrate primitive on [`Caixa`] closing the duplication the
819 // PRIME DIRECTIVE names as a bug. Mirror of the sibling
820 // [`crate::Caixa::validate_kind_slot_coherence`] fold f0d286e
821 // on the author-time typed-slot coherence axis: this wire-up
822 // closes the same three-arm cascade on the reciprocal
823 // code-surface axis, so the layout pipeline now routes both
824 // "no-code kind declares typed slots" and "no-code kind
825 // declares code" author-time footgun families through one
826 // substrate primitive per axis rather than six open-coded
827 // blocks. Each of the three inner ctors
828 // ([`crate::LayoutError::supervisor_owns_code`] /
829 // [`crate::LayoutError::aplicacao_owns_code`] /
830 // [`crate::LayoutError::acao_owns_code`]) was already lifted
831 // onto the substrate by the peer [`layout_nome_only_ctors!`]
832 // macro, so the primitive routes through the same
833 // `Self::<variant>(caixa.nome().to_string())` tuple-literal
834 // wrap per arm as the pre-lift open-coded blocks. Runs
835 // BEFORE the path-existence loops below so a no-code kind
836 // that declares code surfaces the self-locating OwnsCode
837 // diagnostic naming the offending kind rather than a
838 // downstream `MissingEntry` / `ExeOutsideDir` /
839 // `ServicoOutsideDir` against the resolved path far from the
840 // source `caixa.lisp`.
841 caixa.validate_no_code_kind_coherence()?;
842
843 // Kind ↔ typed-slot coherence on the M3 mesh / supervisor-tree /
844 // M2 Servico-runtime slot families — folded onto one substrate
845 // primitive at [`crate::Caixa::validate_kind_slot_coherence`].
846 // Pre-lift each of the three arms lived as a self-similar
847 // five-line `if !caixa.kind().is_<owner>() { let slots =
848 // caixa.declared_<family>_slots(); if !slots.is_empty() { return
849 // Err(LayoutError::<family>_on_non_<owner>(caixa, slots)); } }`
850 // block at this call site — three consumers, three identical
851 // shapes, one substrate primitive on [`Caixa`] closing the
852 // duplication the PRIME DIRECTIVE names as a bug. The primitive
853 // preserves the pre-fold canonical diagnostic order — mesh →
854 // supervisor → servico — pinned by the load-bearing
855 // `validate_kind_slot_coherence_mesh_arm_fires_before_supervisor_arm`
856 // / `_supervisor_arm_fires_before_servico_arm` ordering pins at
857 // caixa-core/src/manifest.rs, so this wire-up is byte-for-byte
858 // equivalent to the pre-fold three-block cascade on every fixture
859 // exercising any of the three arms. Peer with the per-slot
860 // compound entry gates the substrate already carries
861 // ([`crate::Caixa::validate_deps`] b5dd55e,
862 // [`crate::Caixa::validate_limits`] baa4688,
863 // [`crate::Caixa::validate_behavior`] 0d2877a,
864 // [`crate::Caixa::validate_upgrade_from`] d6801df,
865 // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
866 // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
867 // [`crate::Caixa::validate_acao_shape`] 5d6df54) — the
868 // author-time gate axis on the per-slot algebra now shares one
869 // substrate primitive per compound gate, and this lift closes
870 // the symmetric axis on the cross-family kind ↔ slot coherence
871 // algebra so the layout pipeline routes the three self-similar
872 // gates through one substrate primitive rather than three
873 // open-coded blocks. The sibling
874 // [`LayoutError::ForeignCodeSlot`] (code-surface family) and
875 // [`LayoutError::CiOnNonAcao`] (`:ci` axis) gates stay
876 // open-coded downstream: the former bakes the kind-check into
877 // its declared_*_slots helper by design (so it carries no outer
878 // `if !kind().is_<owner>()` guard), the latter carries a
879 // distinct `{ caixa, kind }` wrap shape (no `slots` field —
880 // `:ci` is a single `Option`, not a `Vec`-of-named-slots) whose
881 // reshape onto the uniform `{ caixa, kind, slots }` envelope a
882 // future symmetry lift can join here.
883 caixa.validate_kind_slot_coherence()?;
884
885 // Kind ↔ `:ci` coherence (mirror of the three arms above on
886 // the M3 mesh / supervisor-tree / M2 Servico-runtime slot
887 // families, CANTEIRO §7.1-C, folded onto one substrate
888 // primitive at [`crate::Caixa::validate_ci_kind_coherence`]):
889 // `:ci` carries a typed CI run — a
890 // [`canteiro_types::CiRun`] — that only the caixa-actions
891 // renderer decomposes + validates, and only for a `:kind
892 // Acao`. On any *other* kind a declared `:ci` is the
893 // manifest field's documented "ignored otherwise": it
894 // silently passes verify and then vanishes (never
895 // decomposed, never rendered), far from the source
896 // `caixa.lisp`. Pre-lift the arm lived as a self-similar
897 // `if caixa.ci().is_some() && !caixa.kind().is_acao() { …
898 // return Err(LayoutError::CiOnNonAcao { … }); }` block at
899 // this call site — one consumer today but every future
900 // consumer that wanted to gate this coherence axis as a
901 // unit was structurally forced to re-inline the two-
902 // condition guard in lockstep with this wire-up (the
903 // duplication the PRIME DIRECTIVE names as a bug).
904 // Post-fold the arm reads through one call, and the
905 // [`crate::LayoutError::CiOnNonAcao`] envelope (with its
906 // distinct `{ caixa, kind }` wrap shape — no `slots`
907 // field, because `:ci` is a single `Option` not a `Vec`-
908 // of-named-slots) surfaces byte-for-byte equivalent to the
909 // pre-fold open-coded block, pinned by the paired
910 // `validate_ci_kind_coherence_folds_arm_matches_gate`
911 // per-arm equivalence pin and the
912 // `validate_ci_kind_coherence_accepts_acao_on_every_ci_shape`
913 // /
914 // `validate_ci_kind_coherence_accepts_absent_ci_on_every_kind`
915 // identity-element pins in the
916 // [`crate::Caixa::validate_ci_kind_coherence`] pin family
917 // (`manifest.rs`). Peer to the sibling three-arm
918 // [`crate::Caixa::validate_kind_slot_coherence`] fold
919 // f0d286e: that primitive carries the M3 / supervisor-tree
920 // / M2 axes under a uniform `{ caixa, kind, slots }`
921 // envelope, this primitive carries the `:ci` axis under
922 // its distinct `{ caixa, kind }` envelope, and every
923 // author-time kind ↔ slot coherence diagnostic at the
924 // layout altitude now routes through one substrate
925 // primitive per envelope shape rather than an open-coded
926 // block.
927 caixa.validate_ci_kind_coherence()?;
928
929 // Kind ↔ slot coherence on the fourth and final axis — the
930 // code-surface slot set (the trio M2/Supervisor/Aplicacao gates
931 // above close on the M2 runtime, supervisor-tree, and M3 mesh
932 // axes; this gate closes the symmetric "kind owns this code
933 // shape" relation on `:exe` + `:servicos`). `:exe` is the nix-
934 // built executable surface owned only by Binario; `:servicos`
935 // is the wasm-component daemon surface owned only by Servico.
936 // The caixa-helm / caixa-flux / caixa-flake renderers gate on
937 // `require_kind(_, <owning-kind>)` and only emit the slot for
938 // its owning kind — so on any *other* code-running kind a
939 // declared `:exe` / `:servicos` is the manifest field's
940 // documented "ignored otherwise" (see the field docs on
941 // `Caixa::exe` + `Caixa::servicos`): the path is validated by
942 // the per-kind path-existence loops below, but the value is
943 // never rendered into a build target or programs.yaml entry —
944 // it silently passes `feira build` and then vanishes, far from
945 // the source caixa.lisp.
946 //
947 // Reject it here — beside the M2/Supervisor/Aplicacao slot
948 // gates, after the `SupervisorOwnsCode` / `AplicacaoOwnsCode`
949 // OwnCode gates which dominate on those two no-code kinds (a
950 // Supervisor / Aplicacao with any of `:bibliotecas` / `:exe` /
951 // `:servicos` surfaces the OwnCode diagnostic first), and
952 // before the path-existence loops which would otherwise spend
953 // a less-helpful `MissingEntry` diagnostic on the foreign
954 // slot's path. `declared_foreign_code_slots` is the single
955 // typed source of the foreign-code-slot set + its canonical
956 // diagnostic order (`:exe` → `:servicos`).
957 //
958 // Mirrors the 9d37f98 / 510c00a / 760a430 kind ↔ slot
959 // coherence trio's "declared-but-inert" footgun closure on the
960 // M2 / supervisor-tree / M3 axes, now extended onto the code-
961 // surface axis — every code-running kind's exclusive code
962 // surface is structurally fenced from every other code-running
963 // kind. `:bibliotecas` is deliberately excluded from the foreign
964 // set on Binario / Servico (a `lib/` helper bundled into the
965 // nix flake's build or the wasm-component's source tree is a
966 // legitimate cross-kind authoring shape); on Biblioteca it is
967 // the native slot, and on Supervisor / Aplicacao the OwnCode
968 // gates above already close it.
969 //
970 // Folded onto [`crate::Caixa::validate_foreign_code_kind_coherence`]
971 // — the fourth and last kind ↔ slot coherence primitive on the
972 // typed [`Caixa`] surface, peer of
973 // [`crate::Caixa::validate_kind_slot_coherence`] (f0d286e,
974 // the cross-family M3/supervisor/M2 fold on the sibling
975 // `{ caixa, kind, slots }` envelope),
976 // [`crate::Caixa::validate_no_code_kind_coherence`] (3bbf6a2,
977 // the reciprocal no-code-kind fold on `SupervisorOwnsCode` /
978 // `AplicacaoOwnsCode` / `AcaoOwnsCode`), and
979 // [`crate::Caixa::validate_ci_kind_coherence`] (9b55beb, the
980 // `:ci` axis fold on the `{ caixa, kind }` envelope). With
981 // this lift every kind-coherence axis at the layout altitude
982 // routes through one substrate primitive per axis rather than
983 // an open-coded block, closing the last open-coded gap the
984 // sibling [`crate::Caixa::validate_kind_slot_coherence`]
985 // doc-comment's "ForeignCodeSlot … stays open-coded downstream"
986 // note flagged.
987 caixa.validate_foreign_code_kind_coherence()?;
988
989 // Per-entry path-shape gate on the three Caixa-level code-surface
990 // path lists (`:bibliotecas`, `:exe`, `:servicos`): each entry must
991 // be non-empty, relative, and free of `..` components — the same
992 // [`crate::render::is_sandboxed_relative_path`] discipline the
993 // peer `:behavior :on-*` (b0c8389) and
994 // `:upgrade-from :state-change :script` (26da2c7) axes already
995 // route through. Runs *after* the kind-coherence gates above (so
996 // a `:exe` on a Servico surfaces ForeignCodeSlot rather than a
997 // per-entry shape diagnostic, and a Supervisor/Aplicacao with any
998 // code surface surfaces OwnCode first) and *before* the existence
999 // loops below (so an empty / absolute / parent-escaping entry
1000 // surfaces its self-locating per-slot diagnostic rather than a
1001 // downstream `MissingEntry` / `ExeOutsideDir` /
1002 // `ServicoOutsideDir` against the resolved sandbox-escape path).
1003 caixa.run_layout_gate(Caixa::validate_code_paths, LayoutError::code_path_violation)?;
1004
1005 if caixa.kind().requires_lib() && caixa.bibliotecas().is_empty() {
1006 let expected = root
1007 .join(crate::render::LAYOUT_DIR_LIB)
1008 .join(format!("{}.lisp", caixa.nome()));
1009 if !self.exists(&expected) {
1010 return Err(LayoutError::MissingLib {
1011 caixa: caixa.nome().to_string(),
1012 expected,
1013 });
1014 }
1015 }
1016
1017 // Required-slot gate on the three [`CaixaKind`] arms whose
1018 // sole payload is a canonical typed slot — folded onto one
1019 // substrate primitive at
1020 // [`crate::Caixa::validate_required_kind_slot`]. Pre-lift each
1021 // of the three arms lived as a self-similar
1022 // `if caixa.kind().requires_<slot>() && caixa.<slot>().is_<empty>() {
1023 // return Err(LayoutError::<kind>_without_<slot>(caixa)); }` block
1024 // at this call site — three consumers, three identical shapes,
1025 // one substrate primitive on [`Caixa`] closing the duplication
1026 // the PRIME DIRECTIVE names as a bug. Diagnostic order at the
1027 // primitive matches the pre-fold canonical sequence — `:exe` →
1028 // `:servicos` → `:ci` — the same three-arm sweep the peer
1029 // [`CaixaKind`] discriminator carries at its `requires_*`
1030 // accessors; the three arms are mutually exclusive by
1031 // construction (`:kind` is a single-valued discriminator so at
1032 // most one arm can fire per caixa) so no cross-arm ordering
1033 // pin is meaningful.
1034 //
1035 // The paired `Biblioteca`-arm required-slot check
1036 // ([`LayoutError::MissingLib`], immediately above) stays
1037 // open-coded at this altitude by design: it needs the
1038 // [`LayoutInvariants::exists`] filesystem oracle to check the
1039 // default `lib/<nome>.lisp` fallback path, which the pure
1040 // per-`Caixa` typed-shape surface the fold rides on has no
1041 // reference to. Same posture the peer
1042 // [`crate::Caixa::validate_no_code_kind_coherence`] fold
1043 // (3bbf6a2) takes on the on-disk existence loops.
1044 //
1045 // Peer with the per-slot and per-kind compound entry gates
1046 // every substrate primitive on the M2/M3 typed-slot family
1047 // already carries ([`crate::Caixa::validate_deps`] b5dd55e,
1048 // [`crate::Caixa::validate_limits`] baa4688,
1049 // [`crate::Caixa::validate_behavior`] 0d2877a,
1050 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1051 // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1052 // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
1053 // [`crate::Caixa::validate_acao_shape`] 5d6df54,
1054 // [`crate::Caixa::validate_kind_slot_coherence`] f0d286e,
1055 // [`crate::Caixa::validate_no_code_kind_coherence`] 3bbf6a2,
1056 // [`crate::Caixa::validate_ci_kind_coherence`] 9b55beb): the
1057 // layout pipeline routes the three self-similar required-slot
1058 // gates through one substrate primitive rather than three
1059 // open-coded blocks, and every future required-slot arm (a
1060 // per-Aplicacao required-`:membros` gate, a per-Supervisor
1061 // required-`:children` gate — both already carried at
1062 // [`CaixaKind::requires_membros`] / [`CaixaKind::requires_children`]
1063 // without a paired layout-side wire-up) folds onto this
1064 // compound gate as one arm addition rather than a fourth
1065 // open-coded block at the wire-up site.
1066 caixa.validate_required_kind_slot()?;
1067
1068 // On-disk existence probe on the three Caixa-level code-surface
1069 // path lists (`:bibliotecas`, `:exe`, `:servicos`): every declared
1070 // entry must resolve on disk. The `:bibliotecas` sweep folds onto
1071 // the [`Self::probe_declared_entries`] per-slot batch primitive
1072 // so the iteration + per-iteration `probe_declared_entry`
1073 // dispatch reads through one call rather than a three-line
1074 // `for … { self.probe_declared_entry(…)?; }` loop kept in
1075 // lockstep with the peer `:behavior` / `:upgrade-from` batch
1076 // wire-ups below. The `:exe` / `:servicos` peers fold onto the
1077 // sibling [`Self::probe_sandboxed_declared_entries`] per-slot
1078 // batch primitive on the sandboxed axis so the iteration +
1079 // per-iteration `probe_sandboxed_declared_entry` dispatch reads
1080 // through one call rather than a three-line `for … { self.
1081 // probe_sandboxed_declared_entry(…)?; }` loop kept in lockstep
1082 // with each other and with the peer `:bibliotecas` bare-batch
1083 // wire-up above. All three preserve the pre-lift diagnostic
1084 // order: `MissingEntry` fires before `ExeOutsideDir` /
1085 // `ServicoOutsideDir` on the same iteration, and iteration
1086 // proceeds in the caller-supplied iterator order (short-
1087 // circuiting on the first miss).
1088 self.probe_declared_entries(
1089 caixa.bibliotecas(),
1090 root,
1091 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1092 )?;
1093
1094 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
1095 self.probe_sandboxed_declared_entries(
1096 caixa.exe(),
1097 root,
1098 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
1099 &exe_dir,
1100 LayoutError::ExeOutsideDir,
1101 )?;
1102
1103 let servicos_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
1104 self.probe_sandboxed_declared_entries(
1105 caixa.servicos(),
1106 root,
1107 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
1108 &servicos_dir,
1109 LayoutError::ServicoOutsideDir,
1110 )?;
1111
1112 // ── M2 typed-substrate invariants ────────────────────────────────
1113
1114 // Compound per-Caixa entry gate on the M2 `:limits` slot: the
1115 // layout pipeline's `if let Some(l) = caixa.limits() { l.validate() }`
1116 // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1117 // dispatch pattern — the four-axis cascade on the present-slot
1118 // arm ([`crate::LimitsSpec::validate`]'s `:memory` wasm32
1119 // zero-floor / below-page / above-cap / non-page-multiple;
1120 // `:fuel` zero-floor / cap; `:wall-clock` zero-floor / cap;
1121 // `:cpu` zero-floor / cap) folded onto the
1122 // [`crate::Caixa::validate_limits`] substrate primitive. The
1123 // absent-slot arm (`limits: None`, the canonical "no bound
1124 // declared — engine-default applies" author shape) is the
1125 // fold's identity element and passes trivially through the
1126 // primitive, byte-equal to the pre-lift `if let Some(l) = …`
1127 // guard this call site formerly carried. Pinned by the paired
1128 // `validate_limits_folds_arm_matches_gate` equivalence pin and
1129 // the `validate_limits_accepts_none` / `_accepts_clean_fixture`
1130 // positive-control pins in the [`crate::Caixa::validate_limits`]
1131 // pin family (`manifest.rs`).
1132 //
1133 // Same lift discipline the peer per-Caixa compound gates
1134 // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1135 // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1136 // named substrate-primitive gate per typed slot folds every
1137 // structural axis on that slot (plus the `Option::None`
1138 // identity element for the `Option`-shaped slots) onto one
1139 // call, so every future consumer that wants to re-check
1140 // `:limits` after a per-`{:memory, :fuel, :wall-clock, :cpu}`
1141 // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1142 // materializer's admission webhook, a future `feira validate
1143 // --limits` per-caixa admission verb, a per-`:limits` overlay
1144 // resolver) reaches the four-axis cascade through one dispatch
1145 // rather than re-inlining the `if let Some(l) = …` unwrap-and-
1146 // dispatch pattern in lockstep with this wire-up.
1147 caixa.run_layout_gate(Caixa::validate_limits, LayoutError::limits_violation)?;
1148
1149 // Compound per-Caixa entry gate on the M2 `:behavior` slot's
1150 // pure value-shape surface: the layout pipeline's
1151 // `if let Some(b) = caixa.behavior() { b.validate() }`
1152 // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1153 // dispatch pattern — the six-slot value-shape cascade on the
1154 // present-slot arm ([`crate::BehaviorSpec::validate`]'s per-
1155 // `:on-init` / `:on-call` / `:on-cast` / `:on-info` /
1156 // `:on-state-change` / `:on-terminate` non-empty / relative /
1157 // no-`..`-parent-escape / terminating-`.lisp`-extension
1158 // arm-set routed through the shared
1159 // [`crate::render::require_sandboxed_lisp_path`] helper) —
1160 // folded onto the [`crate::Caixa::validate_behavior`] substrate
1161 // primitive. The absent-slot arm (`behavior: None`, the
1162 // canonical "no callback declared — the runtime falls back to
1163 // the wasm-engine's default per arm" author shape) is the
1164 // fold's identity element and passes trivially through the
1165 // primitive, byte-equal to the pre-lift `if let Some(b) = …`
1166 // guard this call site formerly carried. Pinned by the paired
1167 // `validate_behavior_folds_arm_matches_gate` equivalence pin
1168 // and the `validate_behavior_accepts_none` /
1169 // `_accepts_clean_fixture` positive-control pins in the
1170 // [`crate::Caixa::validate_behavior`] pin family
1171 // (`manifest.rs`).
1172 //
1173 // The value-shape gate runs BEFORE the on-disk callback-path
1174 // existence walk below so a malformed `:behavior` slot
1175 // surfaces its self-locating per-slot diagnostic (naming the
1176 // offending `:on-*` slot) rather than the less-helpful
1177 // "missing behavior-callback" the existence probe would raise
1178 // against the resolved sandbox-escape path.
1179 //
1180 // Same lift discipline the peer per-Caixa compound gates
1181 // ([`crate::Caixa::validate_limits`] baa4688,
1182 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1183 // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1184 // named substrate-primitive gate per typed slot folds every
1185 // structural axis on that slot (plus the `Option::None`
1186 // identity element for the `Option`-shaped slots) onto one
1187 // call, so every future consumer that wants to re-check
1188 // `:behavior` after a per-`{:on-init, …, :on-terminate}`
1189 // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1190 // materializer's admission webhook, a future `feira validate
1191 // --behavior` per-caixa admission verb, a per-`:behavior`
1192 // overlay resolver) reaches the six-slot cascade through one
1193 // dispatch rather than re-inlining the `if let Some(b) = …`
1194 // unwrap-and-dispatch pattern in lockstep with this wire-up.
1195 // The paired on-disk existence walk stays open-coded at this
1196 // altitude because it needs the [`LayoutInvariants::exists`]
1197 // filesystem oracle the pure typed-shape surface has no
1198 // reference to — mirror of the peer M2 `:upgrade-from` per-
1199 // instruction script-path existence probe that stayed at this
1200 // altitude after the [`crate::Caixa::validate_upgrade_from`]
1201 // lift for the same reason.
1202 caixa.run_layout_gate(Caixa::validate_behavior, LayoutError::behavior_violation)?;
1203 if let Some(b) = caixa.behavior() {
1204 self.probe_declared_entries(
1205 b.declared_paths(),
1206 root,
1207 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
1208 )?;
1209 }
1210
1211 // Compound per-Caixa entry gate on `:upgrade-from`: the layout
1212 // pipeline's three-dispatch M2 `:upgrade-from` cascade — the
1213 // per-entry shape + cross-entry duplicate-`:from` gate
1214 // ([`crate::upgrade::validate_upgrade_from`]), the cross-slot
1215 // `:from < :versao` SemVer-2 precedence gate
1216 // ([`crate::upgrade::validate_upgrade_from_against_versao`]), and
1217 // the cross-slot `:state-change` ↔ `:on-state-change` composition
1218 // gate ([`crate::upgrade::validate_upgrade_from_against_behavior`])
1219 // — folded onto the [`crate::Caixa::validate_upgrade_from`]
1220 // substrate primitive. The three dispatches run in the same
1221 // canonical order at the primitive (per-entry → versao → behavior)
1222 // so the fold is byte-for-byte equivalent to the pre-fold
1223 // three-block cascade this call site formerly carried, pinned by
1224 // the paired
1225 // `validate_upgrade_from_folds_{per_entry,versao,behavior}_arm_matches_gate`
1226 // equivalence pins and the
1227 // `validate_upgrade_from_{per_entry_arm_fires_before_versao_arm,
1228 // versao_arm_fires_before_behavior_arm}` ordering pins in the
1229 // [`crate::Caixa::validate_upgrade_from`] pin family
1230 // (`manifest.rs`).
1231 //
1232 // Runs BEFORE the existing per-instruction script-path existence
1233 // pass below so a malformed typed slot surfaces its own
1234 // self-locating diagnostic rather than the less-helpful "missing
1235 // upgrade-script" (which doesn't fire for non-script axes at all).
1236 // Same lift discipline the peer per-slot compound gates
1237 // ([`crate::AplicacaoSpec::validate_contratos`] and its
1238 // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
1239 // [`crate::MeshPolicy::validate`],
1240 // [`crate::SupervisorSpec::validate_children`]) each carry — one
1241 // named substrate-primitive gate per typed slot folds every
1242 // structural axis on that slot onto one call, so every future
1243 // consumer that wants to re-check `:upgrade-from` after a
1244 // per-entry patch (the deferred `caixa.pleme.io/v1alpha1/Caixa`
1245 // CR materializer's admission webhook, a future `feira validate
1246 // --upgrade` per-caixa admission verb, a per-`:upgrade-from`
1247 // overlay resolver) reaches the three-arm compound gate through
1248 // one dispatch rather than re-inlining the three-dispatch
1249 // cascade in lockstep with this wire-up.
1250 //
1251 // The per-instruction on-disk existence-probe walk below stays
1252 // open-coded at the layout wire-up site — that arm needs the
1253 // filesystem oracle on the [`LayoutInvariants`] trait, not on
1254 // the pure per-Caixa typed-shape surface the compound gate
1255 // folds. Same posture [`crate::Caixa::validate_code_paths`] takes
1256 // on the sibling code-path axes: the typed-shape gate fires on
1257 // the per-Caixa surface, the on-disk existence check fires on
1258 // the [`StandardLayout`] surface.
1259 caixa.run_layout_gate(Caixa::validate_upgrade_from, LayoutError::upgrade_violation)?;
1260 self.probe_declared_entries(
1261 caixa
1262 .upgrade_from()
1263 .iter()
1264 .flat_map(crate::UpgradeFromEntry::instructions)
1265 .filter_map(crate::upgrade::UpgradeInstruction::declared_path),
1266 root,
1267 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
1268 )?;
1269
1270 // Supervisor invariants (typed shape — children, restart strategy).
1271 // The "supervisor doesn't own code" check is at the top of verify()
1272 // so it fires before the existence-check loops.
1273 if caixa.kind().is_supervisor() {
1274 // Raw `:restart-window` parse gate on the flat
1275 // `Caixa::restart_window: Option<String>` axis — the last
1276 // orphan-validator on the typed Caixa surface flagged by the
1277 // [`Self::validate_deps`] wire-up's closing comment (the
1278 // "Supervisor-axis specific" remainder) and the
1279 // [`Caixa::supervisor_view`] doc-comment's "the future
1280 // layout-side wire-up" pin. Until this gate landed
1281 // [`Caixa::validate_restart_window`] existed as `pub fn` on
1282 // [`Caixa`] with full per-arm unit coverage in `manifest::tests`
1283 // (`validate_restart_window_rejects_*` — fractional seconds,
1284 // decimal-shaped integer, half-unit minute, leading sign,
1285 // unknown unit, garbage, empty-after-trim; eight rejection
1286 // arms total), but no production code path called it —
1287 // `feira build` (the canonical author-time gate; routes
1288 // through [`StandardLayout::verify`]) silently accepted a
1289 // malformed `:restart-window` and [`Caixa::supervisor_view`]
1290 // soft-swallowed the parse failure as `restart_window: None`
1291 // (i.e. the canonical "omit the slot to express no reset"
1292 // sentinel), turning every malformed window into a never-reset
1293 // supervisor far from the source `caixa.lisp`, with no field
1294 // naming the offending `:restart-window`. The Erlang/OTP
1295 // `MaxIntensity / Period` invariant the typed [`SupervisorSpec`]
1296 // gate (`view.validate()` immediately below) enforces on the
1297 // `Option<Duration>` value never reached the gate at all on
1298 // these inputs: the parse error was already laundered to
1299 // `None`, and `None` is the canonical "never reset" shape
1300 // that always validates cleanly. Lifting the parse gate to
1301 // the layout-pipeline wire-up closes the laundering — every
1302 // value past this gate either parses through the shared
1303 // `crate::supervisor::duration_codec::parse` (and therefore
1304 // round-trips canonically) or fires the new
1305 // [`Self::RestartWindowViolation`] envelope at the source.
1306 //
1307 // Runs *inside* the `kind == Supervisor` branch (rather than
1308 // alongside the peer flat-Caixa gates `validate_nome` /
1309 // `validate_versao` / `validate_deps` / `validate_code_paths`
1310 // above the kind dispatch) because `:restart-window` is in
1311 // the Supervisor slot set per [`Caixa::declared_supervisor_slots`]
1312 // — every non-Supervisor caixa with `:restart-window` set
1313 // already errors upstream via the
1314 // [`Self::SupervisorSlotsOnNonSupervisor`] kind-coherence gate
1315 // (line 243-252), so reaching this gate on a non-Supervisor
1316 // kind would be a no-op (the field is `None` by construction).
1317 // Runs *before* `view.validate()` so the parse-side diagnostic
1318 // surfaces first on the raw-string axis — a `:restart-window
1319 // "1.5s"` lands on the more self-locating
1320 // `RestartWindowViolation` (which names the offending raw
1321 // string verbatim) rather than the laundered-to-`None`
1322 // soft-pass that the typed view would silently let through.
1323 //
1324 // Same per-axis `*Violation { caixa, issue }` envelope every
1325 // peer flat-Caixa wrap exposes ([`Self::NomeViolation`] /
1326 // [`Self::VersaoViolation`] 1f74a5f,
1327 // [`Self::DepsViolation`] aa77d0f, [`Self::CodePathViolation`]
1328 // b868442). Threads [`ManifestError::RestartWindowMalformed`]
1329 // Display through verbatim — the per-arm reason already names
1330 // the offending raw value (e.g. `":restart-window \"1.5s\" is
1331 // not a canonical duration: …"`), so the wrap's `issue`
1332 // carries a self-locating "which axis, which value, why"
1333 // without re-shaping the parser-side reason.
1334 caixa.run_layout_gate(
1335 Caixa::validate_restart_window,
1336 LayoutError::restart_window_violation,
1337 )?;
1338 // Compound per-Caixa entry gate on the Supervisor-kind
1339 // supervision-tree slot family: the layout pipeline's paired
1340 // `let view = caixa.supervisor_view().expect(...);
1341 // view.validate() … validate_no_self_supervision(...) …`
1342 // cascade — the typed-shape cascade
1343 // ([`crate::SupervisorSpec::validate`]'s per-slot gates on
1344 // `:estrategia` ↔ `:children` invariants, `:max-restarts` /
1345 // `:restart-window` bounds, per-child DNS-1123 `:caixa`
1346 // names, semver-valid `:versao` constraints, the
1347 // set-not-multiset duplicate-child gate) and the cross-slot
1348 // self-edge gate
1349 // ([`crate::supervisor::validate_no_self_supervision`], the
1350 // `:children :caixa` ≠ `:nome` invariant the typed view
1351 // cannot enforce on its own because it carries the children
1352 // but not the parent `:nome`) — folded onto the
1353 // [`crate::Caixa::validate_supervisor_shape`] substrate
1354 // primitive. The two arms run in the same canonical order at
1355 // the primitive (typed-shape cascade → cross-slot self-edge)
1356 // so the fold is byte-for-byte equivalent to the pre-fold
1357 // two-block cascade this call site formerly carried, pinned
1358 // by the paired
1359 // `validate_supervisor_shape_folds_{view,self_supervision}_arm_matches_gate`
1360 // equivalence pins and the
1361 // `validate_supervisor_shape_view_arm_fires_before_self_supervision_arm`
1362 // ordering pin in the
1363 // [`crate::Caixa::validate_supervisor_shape`] pin family
1364 // (`manifest.rs`).
1365 //
1366 // Same lift discipline the peer per-Caixa compound gates
1367 // ([`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1368 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1369 // [`crate::Caixa::validate_deps`] b5dd55e,
1370 // [`crate::Caixa::validate_limits`] baa4688,
1371 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1372 // one named substrate-primitive gate folds every structural +
1373 // cross-slot axis on that slot family onto one call, so every
1374 // future consumer that wants to re-check the Supervisor shape
1375 // after a per-slot patch (the wasm-operator's hierarchical
1376 // reconciliation scheduler, the M4
1377 // `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's
1378 // admission webhook, a future `feira validate --supervisor`
1379 // per-caixa admission verb, a per-Supervisor overlay
1380 // resolver) reaches the two-arm compound gate through one
1381 // dispatch rather than re-inlining the two-dispatch cascade
1382 // in lockstep with this wire-up. Peer with the
1383 // [`crate::render::require_supervisor_view`] compound entry
1384 // gate every per-Supervisor renderer would route through
1385 // (which already folds the same `spec.validate()` +
1386 // `validate_no_self_supervision` two-arm cascade behind its
1387 // `require_kind` + `validate_restart_window` prelude): the
1388 // two consumers of the Supervisor-shape cascade now share
1389 // one substrate primitive on each side of the
1390 // author-time-vs-renderer split, rather than two open-coded
1391 // cascades kept in lockstep.
1392 //
1393 // `:restart-window` stays wired above through its own
1394 // per-axis `LayoutError::RestartWindowViolation` envelope
1395 // (the raw-string parse gate on the flat
1396 // `Caixa::restart_window: Option<String>` axis, distinct
1397 // from the typed view's `Duration`-shape gate) — the fold
1398 // covers the two arms that share the
1399 // `LayoutError::SupervisorViolation` envelope; the
1400 // parse-gate arm keeps its self-locating envelope so a
1401 // malformed `:restart-window` surfaces the raw-value
1402 // diagnostic rather than the laundered-to-`None` soft-pass.
1403 caixa.run_layout_gate(
1404 Caixa::validate_supervisor_shape,
1405 LayoutError::supervisor_violation,
1406 )?;
1407 }
1408
1409 // Aplicacao invariants — typed graph composition. Like
1410 // Supervisor, an Aplicacao runs no code itself.
1411 //
1412 // Compound per-Caixa entry gate on the Aplicacao-kind mesh-slot
1413 // family: the layout pipeline's paired `let view =
1414 // caixa.aplicacao_view().expect(...); view.validate() …
1415 // validate_no_self_membership(...) …` cascade — the typed-shape
1416 // cascade ([`crate::AplicacaoSpec::validate`]'s per-slot gates
1417 // on `:membros`, `:contratos`, `:entrada`, `:placement`,
1418 // `:politicas`, in that declared order) and the cross-slot
1419 // self-edge gate ([`crate::aplicacao::validate_no_self_membership`],
1420 // the `:membros :caixa` ≠ `:nome` invariant the typed view
1421 // cannot enforce on its own because it carries the membros but
1422 // not the parent `:nome`) — folded onto the
1423 // [`crate::Caixa::validate_aplicacao_shape`] substrate primitive.
1424 // The two arms run in the same canonical order at the primitive
1425 // (typed-shape cascade → cross-slot self-edge) so the fold is
1426 // byte-for-byte equivalent to the pre-fold two-block cascade
1427 // this call site formerly carried, pinned by the paired
1428 // `validate_aplicacao_shape_folds_{view,self_membership}_arm_matches_gate`
1429 // equivalence pins and the
1430 // `validate_aplicacao_shape_view_arm_fires_before_self_membership_arm`
1431 // ordering pin in the [`crate::Caixa::validate_aplicacao_shape`]
1432 // pin family (`manifest.rs`).
1433 //
1434 // Same lift discipline the peer per-slot compound gates
1435 // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1436 // [`crate::Caixa::validate_deps`] b5dd55e,
1437 // [`crate::Caixa::validate_limits`] baa4688,
1438 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry — one
1439 // named substrate-primitive gate folds every structural +
1440 // cross-slot axis on that slot family onto one call, so every
1441 // future consumer that wants to re-check the Aplicacao shape
1442 // after a per-slot patch (the deferred
1443 // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
1444 // webhook, a future `feira validate --aplicacao` per-caixa
1445 // admission verb, a per-Aplicacao overlay resolver) reaches the
1446 // two-arm compound gate through one dispatch rather than
1447 // re-inlining the two-dispatch cascade in lockstep with this
1448 // wire-up. Peer with the [`crate::render::require_aplicacao_view`]
1449 // compound entry gate every per-Aplicacao renderer routes
1450 // through (3aefefb): the two consumers of the Aplicacao-shape
1451 // cascade now share one substrate primitive on each side of the
1452 // author-time-vs-renderer split, rather than two open-coded
1453 // cascades kept in lockstep.
1454 //
1455 // The outer `if caixa.kind().is_aplicacao()` guard stays because
1456 // [`crate::Caixa::validate_aplicacao_shape`] is the fold's
1457 // identity element on non-Aplicacao kinds (returns `Ok(())`
1458 // without touching the mesh slots — same posture as
1459 // [`crate::Caixa::validate_limits`] / [`Self`]::
1460 // [`crate::Caixa::validate_behavior`] on their `Option`-shaped
1461 // slots); the guard is a redundant but zero-cost fast-path that
1462 // preserves the peer supervisor branch's `if
1463 // caixa.kind().is_supervisor()` parallel structure at this
1464 // altitude.
1465 if caixa.kind().is_aplicacao() {
1466 caixa.run_layout_gate(
1467 Caixa::validate_aplicacao_shape,
1468 LayoutError::aplicacao_violation,
1469 )?;
1470 }
1471
1472 // Acao invariants — typed CI-run decompose. Like Supervisor and
1473 // Aplicacao, an Acao runs no code itself; unlike them, its sole
1474 // payload is a [`canteiro_types::CiRun`] whose declared-node
1475 // shape can carry three structural violations
1476 // ([`canteiro_types::DecomposeError`]: `DuplicateNode`,
1477 // `UnknownDep`, `Cycle`) the layout pipeline formerly deferred
1478 // to [`caixa_actions::validate`] — layout only checked `:ci`
1479 // *presence* via [`Self::MissingCi`] above, so a `:kind Acao`
1480 // carrying a structurally illegal `:ci` (a duplicate node
1481 // name, a dependency on an undeclared node, a dependency
1482 // cycle) passed `feira build` cleanly and surfaced the
1483 // diagnostic only when [`caixa_actions::validate`] later
1484 // refused it — far from the source `caixa.lisp` on the
1485 // author-time gate side.
1486 //
1487 // Compound per-Caixa entry gate on the Acao-kind `:ci` slot
1488 // family: the [`crate::render::decompose_ci`] typed decompose
1489 // gate — the sibling axis owned by the substrate-canonical
1490 // [`crate::render::require_acao_view`] compound helper every
1491 // per-`Acao` renderer routes through — folded onto the
1492 // [`crate::Caixa::validate_acao_shape`] substrate primitive.
1493 // The single-arm fold is byte-for-byte equivalent to the
1494 // pre-fold `decompose_ci(caixa, ci).map(|_| ())?` call this
1495 // wire-up sees, pinned by the paired
1496 // `validate_acao_shape_folds_decompose_arm_matches_gate`
1497 // equivalence pin and the
1498 // `validate_acao_shape_accepts_non_acao_kind` /
1499 // `validate_acao_shape_accepts_absent_ci_slot` identity-
1500 // element pins in the
1501 // [`crate::Caixa::validate_acao_shape`] pin family
1502 // (`manifest.rs`).
1503 //
1504 // Same lift discipline the peer per-kind compound gates
1505 // ([`crate::Caixa::validate_supervisor_shape`] 4c70105,
1506 // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1507 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1508 // [`crate::Caixa::validate_deps`] b5dd55e,
1509 // [`crate::Caixa::validate_limits`] baa4688,
1510 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1511 // one named substrate-primitive gate folds every structural
1512 // axis on that kind onto one call, so every future consumer
1513 // that wants to re-check the Acao shape after a per-node
1514 // patch (a per-`Acao` CR materializer's admission webhook, a
1515 // future `feira validate --acao` per-caixa admission verb, a
1516 // per-`Acao` overlay resolver) reaches the compound gate
1517 // through one dispatch rather than re-inlining the decompose
1518 // cascade in lockstep with this wire-up. Peer with the
1519 // [`crate::render::require_acao_view`] compound entry gate
1520 // every per-`Acao` renderer routes through: the two consumers
1521 // of the Acao-shape cascade now share one substrate primitive
1522 // on each side of the author-time-vs-renderer split, rather
1523 // than two open-coded cascades kept in lockstep. Closes the
1524 // last per-kind asymmetry — with this wire-up the four typed
1525 // named-caixa kinds (`Servico` / `Aplicacao` / `Supervisor` /
1526 // `Acao`) each route through one compound per-Caixa shape
1527 // gate at the layout altitude.
1528 //
1529 // Runs *after* the [`Self::MissingCi`] presence gate above
1530 // (so a `:kind Acao` caixa with `ci = None` surfaces the
1531 // presence diagnostic first — the fold's identity-element arm
1532 // passes cleanly on absent `:ci`) and *after* the sibling
1533 // Supervisor / Aplicacao shape gates so the per-kind
1534 // diagnostic ordering at the layout altitude reads as the
1535 // canonical `Supervisor → Aplicacao → Acao` sweep.
1536 //
1537 // The outer `if caixa.kind().is_acao()` guard stays because
1538 // [`crate::Caixa::validate_acao_shape`] is the fold's
1539 // identity element on non-Acao kinds (returns `Ok(())`
1540 // without touching the `:ci` slot — same posture as
1541 // [`crate::Caixa::validate_supervisor_shape`] /
1542 // [`crate::Caixa::validate_aplicacao_shape`] on the sibling
1543 // typed-view-carrying arms); the guard is a redundant but
1544 // zero-cost fast-path that preserves the peer supervisor /
1545 // aplicacao branches' `if caixa.kind().is_<kind>()` parallel
1546 // structure at this altitude.
1547 if caixa.kind().is_acao() {
1548 caixa.run_layout_gate(Caixa::validate_acao_shape, LayoutError::acao_violation)?;
1549 }
1550
1551 Ok(())
1552 }
1553}
1554
1555#[derive(Debug, Error, PartialEq, Eq)]
1556pub enum LayoutError {
1557 #[error("manifest missing: {}", .0.display())]
1558 MissingManifest(PathBuf),
1559 #[error("caixa '{caixa}' is a Biblioteca but has no lib entry — expected {}", expected.display())]
1560 MissingLib { caixa: String, expected: PathBuf },
1561 #[error("caixa '{0}' is a Binario but has no :exe entries")]
1562 BinarioWithoutExe(String),
1563 #[error("caixa '{0}' is a Servico but has no :servicos entries")]
1564 ServicoWithoutServicos(String),
1565 #[error("declared {kind} entry missing: {}", path.display())]
1566 MissingEntry { kind: &'static str, path: PathBuf },
1567 #[error("exe entry outside exe/ directory: {}", .0.display())]
1568 ExeOutsideDir(PathBuf),
1569 #[error("servico entry outside servicos/ directory: {}", .0.display())]
1570 ServicoOutsideDir(PathBuf),
1571 #[error("caixa '{caixa}' has invalid :nome: {issue}")]
1572 NomeViolation { caixa: String, issue: String },
1573 #[error("caixa '{caixa}' has invalid :versao: {issue}")]
1574 VersaoViolation { caixa: String, issue: String },
1575 #[error("caixa '{caixa}' has invalid :deps / :deps-dev entry: {issue}")]
1576 DepsViolation { caixa: String, issue: String },
1577 #[error("caixa '{caixa}' has invalid :etiquetas entry: {issue}")]
1578 EtiquetasViolation { caixa: String, issue: String },
1579 #[error("caixa '{caixa}' has invalid :autores entry: {issue}")]
1580 AutoresViolation { caixa: String, issue: String },
1581 #[error("caixa '{caixa}' has invalid :repositorio: {issue}")]
1582 RepositorioViolation { caixa: String, issue: String },
1583 #[error("caixa '{caixa}' has invalid :descricao: {issue}")]
1584 DescricaoViolation { caixa: String, issue: String },
1585 #[error("caixa '{caixa}' has invalid :licenca: {issue}")]
1586 LicencaViolation { caixa: String, issue: String },
1587 #[error("caixa '{caixa}' has invalid :edicao: {issue}")]
1588 EdicaoViolation { caixa: String, issue: String },
1589 #[error("caixa '{caixa}' has invalid code-path entry: {issue}")]
1590 CodePathViolation { caixa: String, issue: String },
1591 #[error("caixa '{caixa}' has invalid :limits: {issue}")]
1592 LimitsViolation { caixa: String, issue: String },
1593 #[error("caixa '{caixa}' has invalid :behavior callback: {issue}")]
1594 BehaviorViolation { caixa: String, issue: String },
1595 #[error("caixa '{caixa}' has invalid :upgrade-from entry: {issue}")]
1596 UpgradeViolation { caixa: String, issue: String },
1597 #[error("supervisor caixa '{caixa}' violates typed shape: {issue}")]
1598 SupervisorViolation { caixa: String, issue: String },
1599 #[error("supervisor caixa '{caixa}' has invalid :restart-window: {issue}")]
1600 RestartWindowViolation { caixa: String, issue: String },
1601 #[error(
1602 "supervisor caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — supervisors don't run code, they orchestrate other caixas"
1603 )]
1604 SupervisorOwnsCode(String),
1605 #[error("aplicacao caixa '{caixa}' violates typed shape: {issue}")]
1606 AplicacaoViolation { caixa: String, issue: String },
1607 #[error("acao caixa '{caixa}' violates typed shape: {issue}")]
1608 AcaoViolation { caixa: String, issue: String },
1609 #[error(
1610 "aplicacao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — aplicacaos compose Servicos, they don't run code themselves"
1611 )]
1612 AplicacaoOwnsCode(String),
1613 #[error(
1614 "acao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — acaos carry a typed CI run (:ci), they don't run code themselves"
1615 )]
1616 AcaoOwnsCode(String),
1617 #[error(
1618 "caixa '{caixa}' is :kind {kind:?} but declares Aplicacao-only mesh slot(s): {slots} — \
1619 :membros / :contratos / :politicas / :placement / :entrada compose a :kind Aplicacao's \
1620 typed graph (MESH-COMPOSITION §III.1) and are silently ignored on every other kind \
1621 (never validated, never rendered); move them to a :kind Aplicacao caixa or remove them"
1622 )]
1623 MeshSlotsOnNonAplicacao {
1624 caixa: String,
1625 kind: CaixaKind,
1626 slots: String,
1627 },
1628 #[error(
1629 "caixa '{caixa}' is :kind {kind:?} but declares Supervisor-only slot(s): {slots} — \
1630 :estrategia / :max-restarts / :restart-window / :children compose a :kind Supervisor's \
1631 typed OTP supervisor (INSPIRATIONS §II.2) and are silently ignored on every other kind \
1632 (never validated, never reconciled); move them to a :kind Supervisor caixa or remove them"
1633 )]
1634 SupervisorSlotsOnNonSupervisor {
1635 caixa: String,
1636 kind: CaixaKind,
1637 slots: String,
1638 },
1639 #[error(
1640 "caixa '{caixa}' is :kind {kind:?} but declares Servico-only slot(s): {slots} — \
1641 :limits / :behavior / :upgrade-from configure the runtime of a long-running :kind Servico \
1642 wasm component (INSPIRATIONS §III.1 / §II.3 / §II.4) and are silently ignored on every \
1643 other kind (never rendered into a chart or programs.yaml entry); move them to a :kind \
1644 Servico caixa or remove them"
1645 )]
1646 ServicoSlotsOnNonServico {
1647 caixa: String,
1648 kind: CaixaKind,
1649 slots: String,
1650 },
1651 #[error(
1652 "caixa '{caixa}' is :kind {kind:?} but declares foreign code-surface slot(s): {slots} — \
1653 :exe is the nix-built executable surface owned only by :kind Binario, :servicos is the \
1654 wasm-component + ComputeUnit daemon surface owned only by :kind Servico; \
1655 caixa-helm / caixa-flux / caixa-flake gate emission on `require_kind(_, <owning-kind>)`, \
1656 so a declared :exe / :servicos on the wrong code-running kind is silently ignored — the \
1657 path is validated by the layout's path-existence loops but never rendered into a build \
1658 target or programs.yaml entry. Move the slot to its owning kind, change :kind to match \
1659 (Binario for :exe, Servico for :servicos), or drop the slot entirely"
1660 )]
1661 ForeignCodeSlot {
1662 caixa: String,
1663 kind: CaixaKind,
1664 slots: String,
1665 },
1666 #[error("caixa '{0}' is an Acao but has no :ci slot")]
1667 MissingCi(String),
1668 #[error(
1669 "caixa '{caixa}' is :kind {kind:?} but declares the Acao-only :ci slot — \
1670 :ci carries a typed CI run (canteiro_types::CiRun, CANTEIRO §7.1-C) that only the \
1671 caixa-actions renderer validates for :kind Acao, and is silently ignored on every \
1672 other kind (never decomposed, never rendered); move it to a :kind Acao caixa or \
1673 remove it"
1674 )]
1675 CiOnNonAcao { caixa: String, kind: CaixaKind },
1676}
1677
1678// Fold the layout-pipeline per-Caixa violation wrap onto one substrate
1679// primitive per typed slot. Every `LayoutError::*Violation { caixa, issue }`
1680// variant follows the same uniform shape — `caixa = layout-Caixa's :nome`,
1681// `issue = the gate's per-arm Display` — and every wire-up in
1682// [`StandardLayout::verify`] used to open-code the identical five-line
1683// `.map_err(|err| LayoutError::XxxViolation { caixa: caixa.nome().to_string(),
1684// issue: err.to_string() })` block, once per typed slot. Sixteen distinct
1685// wrap-variants × eighteen wire-up sites is exactly the duplication the
1686// PRIME DIRECTIVE names as a bug: every future consumer that wants to add a
1687// new per-slot gate (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1688// materializer's admission webhook, a future per-slot `feira validate`
1689// verb, a per-slot overlay resolver) had to re-inline the five-line block
1690// in lockstep with the pre-existing wire-ups.
1691//
1692// The macro below generates one static constructor per variant of shape
1693// `fn <slot>_violation(caixa: &Caixa, err: impl Display) -> LayoutError`,
1694// so every wire-up site collapses onto one dispatch:
1695// `caixa.validate_<slot>().map_err(|err| LayoutError::<slot>_violation(caixa,
1696// err))?;`. The uniform two-slot construction (`caixa: caixa.nome()
1697// .to_string()`, `issue: err.to_string()`) is spelled once — inside the
1698// macro — rather than at every wire-up site. Every constructor is
1699// `#[must_use]` so a caller who mistakenly discards the wrapped error
1700// (rather than routing it through `?`) trips a compile warning at the
1701// wire-up site.
1702//
1703// Peer with the per-slot compound entry gates every substrate primitive
1704// on the M2/M3 typed-slot family already carries
1705// ([`crate::Caixa::validate_deps`] b5dd55e, [`crate::Caixa::validate_limits`]
1706// baa4688, [`crate::Caixa::validate_behavior`] 0d2877a,
1707// [`crate::Caixa::validate_upgrade_from`] d6801df,
1708// [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1709// [`crate::AplicacaoSpec::validate_contratos`],
1710// [`crate::MeshPolicy::validate`],
1711// [`crate::SupervisorSpec::validate_children`]): the author-time gates
1712// fold onto one substrate primitive per typed slot; here the layout-side
1713// error-wrap folds onto one substrate primitive per typed variant, so the
1714// two sides of the layout pipeline's per-slot cascade (the gate, the
1715// wrap) each route through one call rather than N open-coded block
1716// repetitions.
1717macro_rules! layout_violation_ctors {
1718 ($($ctor:ident => $variant:ident),* $(,)?) => {
1719 impl LayoutError {
1720 $(
1721 #[doc = concat!(
1722 "Construct a [`LayoutError::",
1723 stringify!($variant),
1724 "`] wrapping `err` under `caixa.nome()`. Folds the ",
1725 "uniform `{ caixa: caixa.nome().to_string(), issue: ",
1726 "err.to_string() }` two-slot construction onto one ",
1727 "substrate primitive so every ",
1728 "[`StandardLayout::verify`] wire-up on this variant ",
1729 "reads through one dispatch rather than the pre-lift ",
1730 "five-line open-coded block."
1731 )]
1732 #[must_use]
1733 pub fn $ctor<E: std::fmt::Display>(caixa: &crate::Caixa, err: E) -> Self {
1734 Self::$variant {
1735 caixa: caixa.nome().to_string(),
1736 issue: err.to_string(),
1737 }
1738 }
1739 )*
1740 }
1741 };
1742}
1743
1744layout_violation_ctors! {
1745 nome_violation => NomeViolation,
1746 versao_violation => VersaoViolation,
1747 deps_violation => DepsViolation,
1748 etiquetas_violation => EtiquetasViolation,
1749 autores_violation => AutoresViolation,
1750 repositorio_violation => RepositorioViolation,
1751 descricao_violation => DescricaoViolation,
1752 licenca_violation => LicencaViolation,
1753 edicao_violation => EdicaoViolation,
1754 code_path_violation => CodePathViolation,
1755 limits_violation => LimitsViolation,
1756 behavior_violation => BehaviorViolation,
1757 upgrade_violation => UpgradeViolation,
1758 restart_window_violation => RestartWindowViolation,
1759 supervisor_violation => SupervisorViolation,
1760 aplicacao_violation => AplicacaoViolation,
1761 acao_violation => AcaoViolation,
1762}
1763
1764// Fold the four `LayoutError::*SlotsOn*` / `LayoutError::ForeignCodeSlot`
1765// kind-coherence wrap sites onto one substrate primitive per typed variant —
1766// the sibling of [`layout_violation_ctors!`] above on the second uniform
1767// error-envelope shape `LayoutError` carries: `{ caixa: caixa.nome(),
1768// kind: caixa.kind(), slots: <declared_*_slots()>.join(" ") }`. Every
1769// wire-up in [`StandardLayout::verify`] on this shape (four sites: the
1770// M3-mesh gate on non-Aplicacao, the supervisor-tree gate on
1771// non-Supervisor, the M2-runtime gate on non-Servico, the code-surface
1772// `ForeignCodeSlot` gate) used to open-code the identical four-field
1773// `.{ caixa: caixa.nome().to_string(), kind: caixa.kind(), slots:
1774// <declared_*_slots>.join(" ") }` block — the exact "same block re-inlined
1775// at every consumer" shape the PRIME DIRECTIVE names as a bug on the same
1776// altitude the peer [`layout_violation_ctors!`] macro just closed on the
1777// `{ caixa, issue }` sibling shape.
1778//
1779// The macro below generates one static constructor per variant of shape
1780// `fn <slot>_on_non_<owner>(caixa: &Caixa, slots: Vec<&'static str>) ->
1781// LayoutError`, so every wire-up site collapses onto one dispatch:
1782// `return Err(LayoutError::<slot>_on_non_<owner>(caixa, <declared_slots>));`.
1783// The uniform four-field construction is spelled once — inside the macro —
1784// rather than at every wire-up site. Every constructor is `#[must_use]` so
1785// a caller who mistakenly discards the constructed error (rather than
1786// routing it through `return Err(…)`) trips a compile warning at the
1787// wire-up site.
1788//
1789// Peer with the `_violation` constructor family above on the same
1790// `LayoutError` — the two together now fold every uniform-shape
1791// `LayoutError` variant carried by [`StandardLayout::verify`] onto one
1792// substrate primitive per typed variant, so the layout-side error-wrap
1793// surface reads through one dispatch per variant rather than N open-coded
1794// blocks. Every future consumer that wants to construct one of these
1795// variants outside the layout pipeline (a per-slot admission webhook, a
1796// `feira validate --kind X` verb, an overlay resolver rejecting a
1797// kind-foreign patch) reaches its variant through one call, matching the
1798// `_violation` family's substrate-primitive discipline.
1799macro_rules! layout_slot_kind_ctors {
1800 ($($ctor:ident => $variant:ident),* $(,)?) => {
1801 impl LayoutError {
1802 $(
1803 #[doc = concat!(
1804 "Construct a [`LayoutError::",
1805 stringify!($variant),
1806 "`] naming the offending slot list under `caixa.nome()` ",
1807 "at `caixa.kind()`. Folds the uniform `{ caixa: caixa.",
1808 "nome().to_string(), kind: caixa.kind(), slots: slots.",
1809 "join(\" \") }` four-field construction onto one substrate ",
1810 "primitive so every [`StandardLayout::verify`] wire-up on ",
1811 "this variant reads through one dispatch rather than the ",
1812 "pre-lift open-coded block."
1813 )]
1814 #[must_use]
1815 pub fn $ctor(caixa: &crate::Caixa, slots: Vec<&'static str>) -> Self {
1816 Self::$variant {
1817 caixa: caixa.nome().to_string(),
1818 kind: caixa.kind(),
1819 slots: slots.join(" "),
1820 }
1821 }
1822 )*
1823 }
1824 };
1825}
1826
1827layout_slot_kind_ctors! {
1828 mesh_slots_on_non_aplicacao => MeshSlotsOnNonAplicacao,
1829 supervisor_slots_on_non_supervisor => SupervisorSlotsOnNonSupervisor,
1830 servico_slots_on_non_servico => ServicoSlotsOnNonServico,
1831 foreign_code_slot => ForeignCodeSlot,
1832}
1833
1834// Fold the five [`LayoutError::MissingEntry`] wire-up sites at
1835// [`StandardLayout::verify`] onto one substrate primitive on `LayoutError` —
1836// the third and last uniform-shape envelope on `LayoutError` after the
1837// `{ caixa, issue }` family the [`layout_violation_ctors!`] macro closed
1838// (131ca0d) and the `{ caixa, kind, slots }` family the peer
1839// [`layout_slot_kind_ctors!`] macro closed (0419438). Each of the five
1840// wire-up sites on `MissingEntry` (`:bibliotecas` iteration line 823,
1841// `:exe` iteration line 834, `:servicos` iteration line 848, `:behavior`
1842// on-disk callback-path iteration line 957, `:upgrade-from` per-
1843// instruction script-path iteration line 1021) opened the same four-line
1844// `LayoutError::MissingEntry { kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_<slot>,
1845// path: full }` struct-literal block — the exact "same block re-inlined
1846// at every consumer" shape the PRIME DIRECTIVE names as a bug, on the
1847// same altitude the peer `_violation` / `_slots_on_non_*` families each
1848// closed on the sibling `LayoutError` envelopes.
1849//
1850// One `#[must_use]` inherent constructor on `LayoutError` collapses the
1851// five sites onto one dispatch:
1852// `return Err(LayoutError::missing_entry(<kind-label>, full));`, byte-
1853// equal to the pre-lift struct-literal block. A macro is not warranted
1854// on the one-variant envelope shape `{ kind: &'static str, path: PathBuf }`
1855// (unlike the sibling 16-variant `_violation` / 4-variant
1856// `_slots_on_non_*` shapes), but the same substrate-primitive discipline
1857// applies: every future consumer that wants to construct a `MissingEntry`
1858// outside the layout pipeline (a per-slot admission webhook probing a
1859// declared path against an out-of-band filesystem oracle, a `feira
1860// validate --lib` / `--exe` / `--servico` / `--behavior` / `--upgrade`
1861// per-caixa admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa`
1862// CR materializer's admission-webhook floor, a per-cluster overlay
1863// resolver rejecting a missing entry against a cluster-local filesystem
1864// snapshot) reaches the variant through one call rather than re-inlining
1865// the four-line struct-literal block in lockstep with the five
1866// layout-pipeline wire-up sites.
1867impl LayoutError {
1868 /// Construct a [`LayoutError::MissingEntry`] naming the missing
1869 /// declared entry at `path` under the canonical `kind` label from
1870 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
1871 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
1872 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] /
1873 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`] /
1874 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`].
1875 /// Folds the uniform `{ kind, path }` two-slot construction onto one
1876 /// substrate primitive so every [`StandardLayout::verify`] wire-up
1877 /// on this variant reads through one dispatch rather than the
1878 /// pre-lift open-coded struct-literal block.
1879 #[must_use]
1880 pub fn missing_entry(kind: &'static str, path: PathBuf) -> Self {
1881 Self::MissingEntry { kind, path }
1882 }
1883}
1884
1885// Fold the six `LayoutError::<Variant>(caixa.nome().to_string())` nome-
1886// only tuple-variant wire-up sites at [`StandardLayout::verify`] onto one
1887// substrate primitive per typed variant — the fourth uniform-shape
1888// envelope on `LayoutError` after the `{ caixa, issue }` family the
1889// [`layout_violation_ctors!`] macro closed (131ca0d), the
1890// `{ caixa, kind, slots }` family the peer [`layout_slot_kind_ctors!`]
1891// macro closed (0419438), and the `{ kind, path }`
1892// [`LayoutError::missing_entry`] one-variant ctor (1b09f9d). Each of the
1893// six wire-up sites on this shape (`SupervisorOwnsCode` /
1894// `AplicacaoOwnsCode` / `AcaoOwnsCode` at the no-code kind-coherence gate;
1895// `BinarioWithoutExe` / `ServicoWithoutServicos` / `MissingCi` at the
1896// required-slot gate) opened the identical one-line
1897// `LayoutError::<Variant>(caixa.nome().to_string())` tuple-literal — the
1898// exact "same block re-inlined at every consumer" shape the PRIME
1899// DIRECTIVE names as a bug, on the same altitude the peer `_violation` /
1900// `_slots_on_non_*` / `missing_entry` families each closed on the
1901// sibling `LayoutError` envelopes.
1902//
1903// The macro below generates one static constructor per variant of shape
1904// `fn <slot>(caixa: &Caixa) -> LayoutError`, so every wire-up site
1905// collapses onto one dispatch:
1906// `return Err(LayoutError::<slot>(caixa));`, byte-equal to the pre-lift
1907// tuple-literal. The uniform one-field construction (`caixa: caixa.nome()
1908// .to_string()`) is spelled once — inside the macro — rather than at
1909// every wire-up site. Every constructor is `#[must_use]` so a caller who
1910// mistakenly discards the constructed error (rather than routing it
1911// through `return Err(…)`) trips a compile warning at the wire-up site.
1912//
1913// Peer with the three prior `LayoutError`-envelope constructor families
1914// — the four together now fold every uniform-shape `LayoutError` variant
1915// carried by [`StandardLayout::verify`] onto one substrate primitive per
1916// typed variant, so every layout-side error-wrap on `LayoutError` reads
1917// through one dispatch per variant rather than N open-coded blocks.
1918// Every future consumer that wants to construct one of these variants
1919// outside the layout pipeline (a per-slot admission webhook probing an
1920// Acao's `:ci` slot, a `feira validate --kind <X>` verb refusing a
1921// no-code kind that declares `:bibliotecas` / `:exe` / `:servicos`, an
1922// overlay resolver rejecting a required-slot omission against a
1923// cluster-local snapshot) reaches its variant through one call, matching
1924// the `_violation` / `_slots_on_non_*` / `missing_entry` families'
1925// substrate-primitive discipline.
1926macro_rules! layout_nome_only_ctors {
1927 ($($ctor:ident => $variant:ident),* $(,)?) => {
1928 impl LayoutError {
1929 $(
1930 #[doc = concat!(
1931 "Construct a [`LayoutError::",
1932 stringify!($variant),
1933 "`] naming the offending `caixa.nome()`. Folds the ",
1934 "uniform `Self::",
1935 stringify!($variant),
1936 "(caixa.nome().to_string())` one-field tuple-",
1937 "literal onto one substrate primitive so every ",
1938 "[`StandardLayout::verify`] wire-up on this variant ",
1939 "reads through one dispatch rather than the pre-lift ",
1940 "open-coded block."
1941 )]
1942 #[must_use]
1943 pub fn $ctor(caixa: &crate::Caixa) -> Self {
1944 Self::$variant(caixa.nome().to_string())
1945 }
1946 )*
1947 }
1948 };
1949}
1950
1951layout_nome_only_ctors! {
1952 binario_without_exe => BinarioWithoutExe,
1953 servico_without_servicos => ServicoWithoutServicos,
1954 missing_ci => MissingCi,
1955 supervisor_owns_code => SupervisorOwnsCode,
1956 aplicacao_owns_code => AplicacaoOwnsCode,
1957 acao_owns_code => AcaoOwnsCode,
1958}
1959
1960#[cfg(test)]
1961mod tests {
1962 use super::*;
1963 use crate::{Caixa, CaixaKind};
1964 use std::path::PathBuf;
1965
1966 fn caixa(kind: CaixaKind) -> Caixa {
1967 Caixa {
1968 nome: "demo".into(),
1969 versao: "0.1.0".into(),
1970 kind,
1971 edicao: None,
1972 descricao: None,
1973 repositorio: None,
1974 licenca: None,
1975 autores: vec![],
1976 etiquetas: vec![],
1977 deps: vec![],
1978 deps_dev: vec![],
1979 exe: vec![],
1980 bibliotecas: vec![],
1981 servicos: vec![],
1982 // M2 typed-substrate slots default to absent.
1983 limits: None,
1984 behavior: None,
1985 upgrade_from: vec![],
1986 estrategia: None,
1987 max_restarts: None,
1988 restart_window: None,
1989 children: vec![],
1990 // M3 Aplicacao slots default to absent.
1991 membros: vec![],
1992 contratos: vec![],
1993 politicas: None,
1994 placement: None,
1995 entrada: None,
1996 ci: None,
1997 }
1998 }
1999
2000 #[test]
2001 fn missing_manifest_errors() {
2002 let layout = StandardLayout::new().with_path_exists(|_| false);
2003 let err = layout
2004 .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
2005 .unwrap_err();
2006 assert!(matches!(err, LayoutError::MissingManifest(_)));
2007 }
2008
2009 // ── LayoutError::*_violation constructor family ──────────────────────
2010 //
2011 // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
2012 // definition) generates one static constructor per `*Violation { caixa,
2013 // issue }` variant that folds the uniform `{ caixa: caixa.nome()
2014 // .to_string(), issue: err.to_string() }` two-slot construction onto
2015 // one substrate primitive. The per-variant equivalence pins below
2016 // (fail-before-pass-after by construction — a byte-mismatched macro
2017 // arm would trip its equivalence pin first) lock each generated
2018 // constructor to its struct-literal peer under `PartialEq`, so every
2019 // wire-up in [`StandardLayout::verify`] on that variant produces a
2020 // byte-equal `LayoutError` to the pre-lift open-coded block. The
2021 // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
2022 // half is pinned; the fixture error fires under a fixed `&str` so the
2023 // `issue: <literal>` half is pinned; the two together pin every field
2024 // of every generated variant.
2025
2026 fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
2027 (caixa(CaixaKind::Biblioteca), "sample issue text")
2028 }
2029
2030 // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
2031 // `actual`/`expected` are only ever read, never moved into anything —
2032 // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
2033 // every call site) favors the owned form for a test-only assertion
2034 // helper called from 17 wire-up pins. The lint targets the general
2035 // API-shape case where callers still have downstream uses for the
2036 // moved value; the assertion helper terminates on the equality check.
2037 #[allow(clippy::needless_pass_by_value)]
2038 fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
2039 assert_eq!(
2040 actual, expected,
2041 "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2042 );
2043 }
2044
2045 #[test]
2046 fn nome_violation_ctor_matches_struct_literal_wrap() {
2047 let (c, issue) = layout_violation_ctor_fixture();
2048 assert_violation_ctor_matches(
2049 LayoutError::nome_violation(&c, issue),
2050 LayoutError::NomeViolation {
2051 caixa: c.nome().to_string(),
2052 issue: issue.to_string(),
2053 },
2054 );
2055 }
2056
2057 #[test]
2058 fn versao_violation_ctor_matches_struct_literal_wrap() {
2059 let (c, issue) = layout_violation_ctor_fixture();
2060 assert_violation_ctor_matches(
2061 LayoutError::versao_violation(&c, issue),
2062 LayoutError::VersaoViolation {
2063 caixa: c.nome().to_string(),
2064 issue: issue.to_string(),
2065 },
2066 );
2067 }
2068
2069 #[test]
2070 fn deps_violation_ctor_matches_struct_literal_wrap() {
2071 let (c, issue) = layout_violation_ctor_fixture();
2072 assert_violation_ctor_matches(
2073 LayoutError::deps_violation(&c, issue),
2074 LayoutError::DepsViolation {
2075 caixa: c.nome().to_string(),
2076 issue: issue.to_string(),
2077 },
2078 );
2079 }
2080
2081 #[test]
2082 fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
2083 let (c, issue) = layout_violation_ctor_fixture();
2084 assert_violation_ctor_matches(
2085 LayoutError::etiquetas_violation(&c, issue),
2086 LayoutError::EtiquetasViolation {
2087 caixa: c.nome().to_string(),
2088 issue: issue.to_string(),
2089 },
2090 );
2091 }
2092
2093 #[test]
2094 fn autores_violation_ctor_matches_struct_literal_wrap() {
2095 let (c, issue) = layout_violation_ctor_fixture();
2096 assert_violation_ctor_matches(
2097 LayoutError::autores_violation(&c, issue),
2098 LayoutError::AutoresViolation {
2099 caixa: c.nome().to_string(),
2100 issue: issue.to_string(),
2101 },
2102 );
2103 }
2104
2105 #[test]
2106 fn repositorio_violation_ctor_matches_struct_literal_wrap() {
2107 let (c, issue) = layout_violation_ctor_fixture();
2108 assert_violation_ctor_matches(
2109 LayoutError::repositorio_violation(&c, issue),
2110 LayoutError::RepositorioViolation {
2111 caixa: c.nome().to_string(),
2112 issue: issue.to_string(),
2113 },
2114 );
2115 }
2116
2117 #[test]
2118 fn descricao_violation_ctor_matches_struct_literal_wrap() {
2119 let (c, issue) = layout_violation_ctor_fixture();
2120 assert_violation_ctor_matches(
2121 LayoutError::descricao_violation(&c, issue),
2122 LayoutError::DescricaoViolation {
2123 caixa: c.nome().to_string(),
2124 issue: issue.to_string(),
2125 },
2126 );
2127 }
2128
2129 #[test]
2130 fn licenca_violation_ctor_matches_struct_literal_wrap() {
2131 let (c, issue) = layout_violation_ctor_fixture();
2132 assert_violation_ctor_matches(
2133 LayoutError::licenca_violation(&c, issue),
2134 LayoutError::LicencaViolation {
2135 caixa: c.nome().to_string(),
2136 issue: issue.to_string(),
2137 },
2138 );
2139 }
2140
2141 #[test]
2142 fn edicao_violation_ctor_matches_struct_literal_wrap() {
2143 let (c, issue) = layout_violation_ctor_fixture();
2144 assert_violation_ctor_matches(
2145 LayoutError::edicao_violation(&c, issue),
2146 LayoutError::EdicaoViolation {
2147 caixa: c.nome().to_string(),
2148 issue: issue.to_string(),
2149 },
2150 );
2151 }
2152
2153 #[test]
2154 fn code_path_violation_ctor_matches_struct_literal_wrap() {
2155 let (c, issue) = layout_violation_ctor_fixture();
2156 assert_violation_ctor_matches(
2157 LayoutError::code_path_violation(&c, issue),
2158 LayoutError::CodePathViolation {
2159 caixa: c.nome().to_string(),
2160 issue: issue.to_string(),
2161 },
2162 );
2163 }
2164
2165 #[test]
2166 fn limits_violation_ctor_matches_struct_literal_wrap() {
2167 let (c, issue) = layout_violation_ctor_fixture();
2168 assert_violation_ctor_matches(
2169 LayoutError::limits_violation(&c, issue),
2170 LayoutError::LimitsViolation {
2171 caixa: c.nome().to_string(),
2172 issue: issue.to_string(),
2173 },
2174 );
2175 }
2176
2177 #[test]
2178 fn behavior_violation_ctor_matches_struct_literal_wrap() {
2179 let (c, issue) = layout_violation_ctor_fixture();
2180 assert_violation_ctor_matches(
2181 LayoutError::behavior_violation(&c, issue),
2182 LayoutError::BehaviorViolation {
2183 caixa: c.nome().to_string(),
2184 issue: issue.to_string(),
2185 },
2186 );
2187 }
2188
2189 #[test]
2190 fn upgrade_violation_ctor_matches_struct_literal_wrap() {
2191 let (c, issue) = layout_violation_ctor_fixture();
2192 assert_violation_ctor_matches(
2193 LayoutError::upgrade_violation(&c, issue),
2194 LayoutError::UpgradeViolation {
2195 caixa: c.nome().to_string(),
2196 issue: issue.to_string(),
2197 },
2198 );
2199 }
2200
2201 #[test]
2202 fn restart_window_violation_ctor_matches_struct_literal_wrap() {
2203 let (c, issue) = layout_violation_ctor_fixture();
2204 assert_violation_ctor_matches(
2205 LayoutError::restart_window_violation(&c, issue),
2206 LayoutError::RestartWindowViolation {
2207 caixa: c.nome().to_string(),
2208 issue: issue.to_string(),
2209 },
2210 );
2211 }
2212
2213 #[test]
2214 fn supervisor_violation_ctor_matches_struct_literal_wrap() {
2215 let (c, issue) = layout_violation_ctor_fixture();
2216 assert_violation_ctor_matches(
2217 LayoutError::supervisor_violation(&c, issue),
2218 LayoutError::SupervisorViolation {
2219 caixa: c.nome().to_string(),
2220 issue: issue.to_string(),
2221 },
2222 );
2223 }
2224
2225 #[test]
2226 fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
2227 let (c, issue) = layout_violation_ctor_fixture();
2228 assert_violation_ctor_matches(
2229 LayoutError::aplicacao_violation(&c, issue),
2230 LayoutError::AplicacaoViolation {
2231 caixa: c.nome().to_string(),
2232 issue: issue.to_string(),
2233 },
2234 );
2235 }
2236
2237 #[test]
2238 fn acao_violation_ctor_matches_struct_literal_wrap() {
2239 // Sibling of [`aplicacao_violation_ctor_matches_struct_literal_wrap`]
2240 // / [`supervisor_violation_ctor_matches_struct_literal_wrap`] on
2241 // the third per-kind compound-shape wrap envelope on
2242 // `LayoutError`. Pins the macro-generated `acao_violation`
2243 // constructor to its struct-literal peer under `PartialEq`, so
2244 // every wire-up in [`StandardLayout::verify`] on the
2245 // [`LayoutError::AcaoViolation`] variant produces a byte-equal
2246 // `LayoutError` to the pre-lift open-coded block. Closes the
2247 // pin family the peer per-kind shape wraps already carry.
2248 let (c, issue) = layout_violation_ctor_fixture();
2249 assert_violation_ctor_matches(
2250 LayoutError::acao_violation(&c, issue),
2251 LayoutError::AcaoViolation {
2252 caixa: c.nome().to_string(),
2253 issue: issue.to_string(),
2254 },
2255 );
2256 }
2257
2258 #[test]
2259 fn violation_ctor_routes_issue_through_display_impl() {
2260 // Pin the fold's `issue = err.to_string()` half against any type
2261 // implementing `Display` — a per-arm error type from a foreign
2262 // module (here, `std::io::Error`) threads through byte-equal to
2263 // the struct-literal `.to_string()` construction, so the fold
2264 // does not silently collapse onto `&str`-only inputs.
2265 let c = caixa(CaixaKind::Biblioteca);
2266 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
2267 let expected_issue = io_err.to_string();
2268 let actual = LayoutError::deps_violation(&c, &io_err);
2269 assert_eq!(
2270 actual,
2271 LayoutError::DepsViolation {
2272 caixa: c.nome().to_string(),
2273 issue: expected_issue,
2274 },
2275 );
2276 }
2277
2278 #[test]
2279 fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
2280 // Pin the fold's `caixa = caixa.nome().to_string()` half against
2281 // a non-default `:nome` — the accessor threads the caller's
2282 // `:nome` verbatim into the wrap envelope, so the fold does not
2283 // silently collapse onto the default `"demo"` fixture nome.
2284 let mut c = caixa(CaixaKind::Biblioteca);
2285 c.nome = "alt-nome".into();
2286 let actual = LayoutError::behavior_violation(&c, "sample issue");
2287 assert_eq!(
2288 actual,
2289 LayoutError::BehaviorViolation {
2290 caixa: "alt-nome".to_string(),
2291 issue: "sample issue".to_string(),
2292 },
2293 );
2294 }
2295
2296 // ── Caixa::run_layout_gate — per-slot gate + LayoutError wrap fold ───
2297 //
2298 // The [`Caixa::run_layout_gate`] substrate primitive folds the 18
2299 // self-similar `caixa.validate_<slot>().map_err(|err| LayoutError::
2300 // <slot>_violation(caixa, err))?;` wire-up sites at
2301 // [`StandardLayout::verify`] onto one dispatch. The pins below
2302 // (fail-before-pass-after by construction — a silent regression that
2303 // de-folded either arm would trip its own pin first) lock the two
2304 // arms of the fold under `PartialEq`:
2305 //
2306 // - The `Ok(())` identity-element arm passes through verbatim (no
2307 // wrap runs, no `caixa` closure capture).
2308 // - The `Err(E)` arm routes through the caller-supplied `wrap`
2309 // ctor with `self` bound as the caixa slot, byte-equal to the
2310 // pre-lift `.map_err(|err| CTOR(caixa, err))` closure.
2311 //
2312 // The third pin (per-arm equivalence) runs the primitive with the
2313 // canonical `Caixa::validate_nome` validator and the paired
2314 // `LayoutError::nome_violation` ctor on a fixture whose `:nome`
2315 // fails `validate_nome`'s DNS-1123 gate ("Bad_Nome" — the uppercase
2316 // + underscore double footgun) and asserts the primitive's
2317 // `Result<(), LayoutError>` matches the open-coded pre-lift cascade
2318 // on the same fixture. A silent regression that de-folded the wrap
2319 // (dropped the `self` binding, threaded a stale caixa nome, swapped
2320 // the wrap ctor) would surface here as a mismatch between the two
2321 // dispatches.
2322
2323 #[test]
2324 fn run_layout_gate_ok_arm_passes_through() {
2325 // Positive control on the identity-element arm: a gate returning
2326 // `Ok(())` short-circuits before the wrap runs, so the caller
2327 // receives `Ok(())` verbatim regardless of what the paired ctor
2328 // would have produced. Pins the fold's `Result::map_err`
2329 // short-circuit semantics — a regression that unconditionally
2330 // wrapped (e.g. always called the ctor) would trip here.
2331 let c = caixa(CaixaKind::Biblioteca);
2332 let result: Result<(), LayoutError> = c.run_layout_gate(
2333 |_c: &Caixa| Ok::<(), &'static str>(()),
2334 |_c: &Caixa, _err: &'static str| {
2335 panic!("wrap must not run on the Ok(()) arm of the fold")
2336 },
2337 );
2338 assert!(
2339 result.is_ok(),
2340 "run_layout_gate must pass Ok(()) through verbatim, got {result:?}"
2341 );
2342 }
2343
2344 #[test]
2345 fn run_layout_gate_err_arm_wraps_via_ctor() {
2346 // Positive control on the err arm: a gate returning `Err(E)`
2347 // threads `E` into the caller-supplied `wrap` ctor with `self`
2348 // bound as the caixa argument. Uses a synthetic `&'static str`
2349 // error and the canonical `LayoutError::deps_violation` ctor so
2350 // the pin covers the fold's two-callable dispatch shape without
2351 // depending on any per-slot validator's specific arm sweep.
2352 let c = caixa(CaixaKind::Biblioteca);
2353 let sample_reason = "sample gate reason";
2354 let result = c.run_layout_gate(
2355 |_c: &Caixa| Err::<(), &'static str>(sample_reason),
2356 LayoutError::deps_violation,
2357 );
2358 let err = result.expect_err("Err arm must reach the caller");
2359 assert_eq!(
2360 err,
2361 LayoutError::DepsViolation {
2362 caixa: c.nome().to_string(),
2363 issue: sample_reason.to_string(),
2364 },
2365 "run_layout_gate must wrap the gate's Err via the caller-supplied \
2366 ctor with `self` bound as the caixa slot"
2367 );
2368 }
2369
2370 #[test]
2371 fn run_layout_gate_folds_arm_matches_gate() {
2372 // Fail-before-pass-after per-arm equivalence pin on the err arm:
2373 // a fixture whose `:nome` fails `validate_nome`'s DNS-1123 gate
2374 // (`"Bad_Nome"` — uppercase + underscore double footgun the
2375 // [`crate::ManifestError::NomeInvalid`] arm rejects) surfaces
2376 // the same `LayoutError::NomeViolation` byte-equal through both
2377 // the primitive `Caixa::run_layout_gate(Caixa::validate_nome,
2378 // LayoutError::nome_violation)` and the open-coded pre-lift
2379 // cascade `Caixa::validate_nome().map_err(|err|
2380 // LayoutError::nome_violation(&c, err))` on the same Caixa
2381 // fixture. Pins the fold — a silent regression that de-folded
2382 // either arm (dropped the `self` binding, threaded a stale
2383 // caixa nome, swapped the wrap ctor) would surface here as a
2384 // mismatch between the two dispatches.
2385 let mut c = caixa(CaixaKind::Biblioteca);
2386 c.nome = "Bad_Nome".into();
2387 let via_primitive = c
2388 .run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)
2389 .expect_err("Bad_Nome must fail validate_nome");
2390 let via_open_coded = c
2391 .validate_nome()
2392 .map_err(|err| LayoutError::nome_violation(&c, err))
2393 .expect_err("Bad_Nome must fail validate_nome");
2394 assert_eq!(
2395 via_primitive, via_open_coded,
2396 "Caixa::run_layout_gate must surface the err-arm diagnostic \
2397 byte-equal to the open-coded `.map_err(|err| CTOR(caixa, err))` \
2398 cascade on the same Caixa fixture"
2399 );
2400 assert!(
2401 matches!(via_primitive, LayoutError::NomeViolation { .. }),
2402 "expected NomeViolation on Bad_Nome, got {via_primitive:?}"
2403 );
2404 }
2405
2406 // ── LayoutError kind-coherence constructor family ────────────────────
2407 //
2408 // The [`layout_slot_kind_ctors!`] macro (sibling of
2409 // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
2410 // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
2411 // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
2412 // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
2413 // onto one substrate primitive. The per-variant equivalence pins below
2414 // (fail-before-pass-after by construction — a byte-mismatched macro arm
2415 // would trip its equivalence pin first) lock each generated constructor
2416 // to its struct-literal peer under `PartialEq`, so every wire-up in
2417 // [`StandardLayout::verify`] on that variant produces a byte-equal
2418 // `LayoutError` to the pre-lift open-coded block. The three cross-axis
2419 // pins that follow (non-default `:nome`, non-default kind, non-trivial
2420 // slots list) route each of the three constructor input axes through
2421 // its declared accessor / arg, so the fold does not silently collapse
2422 // onto a fixture default on any axis.
2423
2424 fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
2425 (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
2426 }
2427
2428 // Same rationale as `assert_violation_ctor_matches` above: the helper
2429 // terminates on the equality check, so the owned-arg lint's general
2430 // API-shape target does not apply.
2431 #[allow(clippy::needless_pass_by_value)]
2432 fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
2433 assert_eq!(
2434 actual, expected,
2435 "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2436 );
2437 }
2438
2439 #[test]
2440 fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
2441 let (c, slots) = layout_slot_kind_ctor_fixture();
2442 assert_slot_kind_ctor_matches(
2443 LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
2444 LayoutError::MeshSlotsOnNonAplicacao {
2445 caixa: c.nome().to_string(),
2446 kind: c.kind(),
2447 slots: slots.join(" "),
2448 },
2449 );
2450 }
2451
2452 #[test]
2453 fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
2454 let (c, slots) = layout_slot_kind_ctor_fixture();
2455 assert_slot_kind_ctor_matches(
2456 LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
2457 LayoutError::SupervisorSlotsOnNonSupervisor {
2458 caixa: c.nome().to_string(),
2459 kind: c.kind(),
2460 slots: slots.join(" "),
2461 },
2462 );
2463 }
2464
2465 #[test]
2466 fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
2467 let (c, slots) = layout_slot_kind_ctor_fixture();
2468 assert_slot_kind_ctor_matches(
2469 LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
2470 LayoutError::ServicoSlotsOnNonServico {
2471 caixa: c.nome().to_string(),
2472 kind: c.kind(),
2473 slots: slots.join(" "),
2474 },
2475 );
2476 }
2477
2478 #[test]
2479 fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
2480 let (c, slots) = layout_slot_kind_ctor_fixture();
2481 assert_slot_kind_ctor_matches(
2482 LayoutError::foreign_code_slot(&c, slots.clone()),
2483 LayoutError::ForeignCodeSlot {
2484 caixa: c.nome().to_string(),
2485 kind: c.kind(),
2486 slots: slots.join(" "),
2487 },
2488 );
2489 }
2490
2491 #[test]
2492 fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
2493 // Pin the fold's `caixa = caixa.nome().to_string()` half against a
2494 // non-default `:nome` — the accessor threads the caller's `:nome`
2495 // verbatim into the wrap envelope, so the fold does not silently
2496 // collapse onto the default `"demo"` fixture nome. Peer of the
2497 // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
2498 // pin on the `{ caixa, issue }` envelope; extended here onto the
2499 // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
2500 // constructor families guarantee the `:nome`-derived-caixa slot
2501 // routes through [`Caixa::nome`] rather than a hard-coded string.
2502 let mut c = caixa(CaixaKind::Biblioteca);
2503 c.nome = "alt-nome".into();
2504 let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
2505 assert_eq!(
2506 actual,
2507 LayoutError::MeshSlotsOnNonAplicacao {
2508 caixa: "alt-nome".to_string(),
2509 kind: CaixaKind::Biblioteca,
2510 slots: ":membros".to_string(),
2511 },
2512 );
2513 }
2514
2515 #[test]
2516 fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
2517 // Pin the fold's `kind = caixa.kind()` half against a non-default
2518 // kind — the accessor threads the caller's `:kind` verbatim into
2519 // the wrap envelope, so the fold does not silently collapse onto
2520 // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
2521 // / non-Servico kind the corresponding gate can fire on so the
2522 // pin covers the kind-derivation axis on every downstream variant.
2523 for kind in [
2524 CaixaKind::Biblioteca,
2525 CaixaKind::Binario,
2526 CaixaKind::Servico,
2527 CaixaKind::Supervisor,
2528 CaixaKind::Aplicacao,
2529 CaixaKind::Acao,
2530 ] {
2531 let c = caixa(kind);
2532 let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
2533 assert_eq!(
2534 actual,
2535 LayoutError::ForeignCodeSlot {
2536 caixa: c.nome().to_string(),
2537 kind,
2538 slots: ":exe".to_string(),
2539 },
2540 "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
2541 );
2542 }
2543 }
2544
2545 #[test]
2546 fn slot_kind_ctor_routes_slots_through_join_separator() {
2547 // Pin the fold's `slots = slots.join(" ")` half against a
2548 // multi-entry slots list — the join threads exactly one ASCII
2549 // space between entries, in caller-supplied order, so the fold
2550 // does not silently collapse onto a fixed separator (`", "`, `";
2551 // "`, `"\n"`), a sorted order, or a single-entry pass-through.
2552 // Uses the M2 servico-slot vocabulary since these are what the
2553 // corresponding `servico_slots_on_non_servico` gate reports.
2554 let c = caixa(CaixaKind::Biblioteca);
2555 let actual = LayoutError::servico_slots_on_non_servico(
2556 &c,
2557 vec![":limits", ":behavior", ":upgrade-from"],
2558 );
2559 assert_eq!(
2560 actual,
2561 LayoutError::ServicoSlotsOnNonServico {
2562 caixa: c.nome().to_string(),
2563 kind: c.kind(),
2564 slots: ":limits :behavior :upgrade-from".to_string(),
2565 },
2566 );
2567 }
2568
2569 // ── LayoutError::missing_entry substrate-primitive constructor ───────
2570 //
2571 // The [`LayoutError::missing_entry`] constructor beside the enum
2572 // definition folds the `{ kind: &'static str, path: PathBuf }`
2573 // uniform-shape envelope onto one substrate primitive — the third
2574 // and last uniform-shape envelope on `LayoutError` after the
2575 // `{ caixa, issue }` family the [`layout_violation_ctors!`] macro
2576 // closed (131ca0d) and the `{ caixa, kind, slots }` family the peer
2577 // [`layout_slot_kind_ctors!`] macro closed (0419438). The pins below
2578 // (fail-before-pass-after by construction — a byte-mismatched
2579 // constructor arm would trip its equivalence pin first) lock the
2580 // constructor to its struct-literal peer under `PartialEq`, so every
2581 // wire-up in [`StandardLayout::verify`] on this variant produces a
2582 // byte-equal `LayoutError` to the pre-lift open-coded block. The two
2583 // cross-axis pins that follow (canonical-kind-label sweep, non-
2584 // default path) route each of the two constructor input axes through
2585 // its arg verbatim, so the fold does not silently collapse onto a
2586 // fixture default on either axis.
2587
2588 #[test]
2589 fn missing_entry_ctor_matches_struct_literal_wrap() {
2590 // Per-envelope equivalence pin — the `missing_entry` constructor
2591 // produces a `LayoutError::MissingEntry` byte-equal under
2592 // `PartialEq` to the open-coded four-line struct-literal wrap on
2593 // the same `(kind, path)` fixture. Peer of the sibling
2594 // `<slot>_violation_ctor_matches_struct_literal_wrap` /
2595 // `<slot>_slots_on_non_<owner>_ctor_matches_struct_literal_wrap`
2596 // pins on the two prior uniform-shape envelopes on the same
2597 // `LayoutError`.
2598 let path = PathBuf::from("/tmp/x/lib/demo.lisp");
2599 assert_eq!(
2600 LayoutError::missing_entry(
2601 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2602 path.clone(),
2603 ),
2604 LayoutError::MissingEntry {
2605 kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2606 path,
2607 },
2608 );
2609 }
2610
2611 #[test]
2612 fn missing_entry_ctor_routes_kind_through_arg_verbatim() {
2613 // Pin the fold's `kind: &'static str` arg through every canonical
2614 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2615 // wire-up sites in [`StandardLayout::verify`] pass — so the fold
2616 // does not silently collapse onto one hard-coded label. Sweep
2617 // matches the arm set the peer
2618 // `layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`
2619 // pin (below) covers on the const-label declarations.
2620 let path = PathBuf::from("/tmp/x/entry");
2621 for kind in [
2622 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2623 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2624 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2625 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2626 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2627 ] {
2628 assert_eq!(
2629 LayoutError::missing_entry(kind, path.clone()),
2630 LayoutError::MissingEntry {
2631 kind,
2632 path: path.clone(),
2633 },
2634 "missing_entry ctor must thread `kind` verbatim on every canonical label",
2635 );
2636 }
2637 }
2638
2639 #[test]
2640 fn missing_entry_ctor_routes_path_through_arg_verbatim() {
2641 // Pin the fold's `path: PathBuf` arg against a non-default,
2642 // multi-component `PathBuf` — the ctor threads the caller's
2643 // `PathBuf` verbatim into the wrap envelope, so the fold does
2644 // not silently collapse onto a fixed component prefix, a
2645 // canonicalized form, or a single-component pass-through.
2646 let path = PathBuf::from("/alt/root")
2647 .join("servicos")
2648 .join("hello-rio.computeunit.yaml");
2649 assert_eq!(
2650 LayoutError::missing_entry(
2651 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2652 path.clone(),
2653 ),
2654 LayoutError::MissingEntry {
2655 kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2656 path,
2657 },
2658 );
2659 }
2660
2661 // ── StandardLayout::probe_declared_entry substrate primitive ─────────
2662 //
2663 // The [`StandardLayout::probe_declared_entry`] method
2664 // (`layout.rs`) folds the five self-similar
2665 // `let full = root.join(p); if !self.exists(&full) { return
2666 // Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
2667 // blocks at [`StandardLayout::verify`] (`:bibliotecas` iteration,
2668 // `:exe` iteration, `:servicos` iteration, `:behavior` on-disk
2669 // callback-path iteration, `:upgrade-from` per-instruction script-
2670 // path iteration) onto one substrate primitive. The pins below
2671 // (fail-before-pass-after by construction — a byte-mismatched
2672 // primitive body would trip its equivalence pin first) lock the
2673 // fold to its pre-lift open-coded shape under `PartialEq`, so every
2674 // wire-up in [`StandardLayout::verify`] on this primitive produces
2675 // a byte-equal `LayoutError` on miss and a byte-equal `PathBuf` on
2676 // hit.
2677
2678 #[test]
2679 fn probe_declared_entry_folds_miss_returns_missing_entry() {
2680 // Per-primitive equivalence pin on the miss arm — a
2681 // [`StandardLayout`] whose oracle returns `false` for every
2682 // path yields a `MissingEntry` byte-equal under `PartialEq` to
2683 // the open-coded `LayoutError::missing_entry(<kind>,
2684 // root.join(path))` wrap the pre-lift block carried at each of
2685 // the five wire-up sites. Peer of the sibling
2686 // `missing_entry_ctor_matches_struct_literal_wrap` pin on the
2687 // constructor's own byte-equal shape.
2688 let layout = StandardLayout::new().with_path_exists(|_| false);
2689 let root = PathBuf::from("/tmp/x");
2690 let path = Path::new("lib/demo.lisp");
2691 let err = layout
2692 .probe_declared_entry(
2693 path,
2694 &root,
2695 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2696 )
2697 .unwrap_err();
2698 assert_eq!(
2699 err,
2700 LayoutError::missing_entry(
2701 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2702 root.join(path),
2703 ),
2704 "probe_declared_entry miss arm must produce byte-equal LayoutError to \
2705 open-coded `missing_entry(<kind>, root.join(path))` wrap",
2706 );
2707 }
2708
2709 #[test]
2710 fn probe_declared_entry_folds_hit_returns_resolved_full() {
2711 // Per-primitive equivalence pin on the hit arm — a
2712 // [`StandardLayout`] whose oracle returns `true` for the probed
2713 // resolved path yields `Ok(root.join(path))` byte-equal under
2714 // `PartialEq`. The pre-lift `:exe` / `:servicos` wire-up sites
2715 // needed the resolved `full` for the follow-up sandbox-directory-
2716 // containment check; the fold preserves that hand-off through
2717 // the primitive's `Ok(PathBuf)` return arm rather than
2718 // re-computing `root.join(path)` at the follow-up gate.
2719 let root = PathBuf::from("/tmp/x");
2720 let path = Path::new("exe/tool.lisp");
2721 let full = root.join(path);
2722 let full_probe = full.clone();
2723 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2724 let resolved = layout
2725 .probe_declared_entry(path, &root, crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE)
2726 .expect("probe_declared_entry must return Ok(root.join(path)) when the oracle hits");
2727 assert_eq!(
2728 resolved, full,
2729 "probe_declared_entry hit arm must return the resolved `root.join(path)` \
2730 byte-equal so the `:exe` / `:servicos` sandbox-containment follow-up \
2731 reads it verbatim without re-computing",
2732 );
2733 }
2734
2735 #[test]
2736 fn probe_declared_entry_threads_kind_through_arg_verbatim() {
2737 // Cross-axis pin — sweep every canonical
2738 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2739 // wire-up sites in [`StandardLayout::verify`] pass. Each miss
2740 // must return a `MissingEntry` whose `kind:` field byte-equals
2741 // the caller-provided arg, so the fold does not silently
2742 // collapse onto one hard-coded label. Sweep matches the arm
2743 // set the peer
2744 // `missing_entry_ctor_routes_kind_through_arg_verbatim` pin
2745 // covers on the constructor arg.
2746 let layout = StandardLayout::new().with_path_exists(|_| false);
2747 let root = PathBuf::from("/tmp/x");
2748 let path = Path::new("some/entry");
2749 for kind in [
2750 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2751 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2752 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2753 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2754 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2755 ] {
2756 let err = layout.probe_declared_entry(path, &root, kind).unwrap_err();
2757 assert_eq!(
2758 err,
2759 LayoutError::missing_entry(kind, root.join(path)),
2760 "probe_declared_entry must thread `kind` verbatim on every canonical label",
2761 );
2762 }
2763 }
2764
2765 #[test]
2766 fn probe_declared_entry_threads_path_through_arg_verbatim() {
2767 // Cross-axis pin — the primitive must compose the `path` arg
2768 // through `root.join` verbatim on the miss arm's `MissingEntry
2769 // { path: … }` field, so the fold does not silently collapse
2770 // onto a fixed component prefix, a canonicalized form, or a
2771 // hand-authored `root.join(<literal>)`. Sweep two multi-
2772 // component `Path` fixtures (one under `servicos/`, one under
2773 // `lib/`) so a byte-drifted composition on either axis would
2774 // trip.
2775 let layout = StandardLayout::new().with_path_exists(|_| false);
2776 let root = PathBuf::from("/alt/root");
2777 for path in [
2778 Path::new("servicos/hello-rio.computeunit.yaml"),
2779 Path::new("lib/demo.lisp"),
2780 ] {
2781 let err = layout
2782 .probe_declared_entry(
2783 path,
2784 &root,
2785 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2786 )
2787 .unwrap_err();
2788 assert_eq!(
2789 err,
2790 LayoutError::missing_entry(
2791 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2792 root.join(path),
2793 ),
2794 "probe_declared_entry must compose `path` through `root.join` verbatim",
2795 );
2796 }
2797 }
2798
2799 #[test]
2800 fn probe_declared_entry_routes_through_configurable_exists_oracle() {
2801 // Cross-axis pin — the primitive must consult the injected
2802 // [`StandardLayout::with_path_exists`] oracle, not the ambient
2803 // `Path::exists` filesystem probe. Configure a per-path
2804 // discriminator that returns `true` only for one canonical
2805 // resolved path and assert both arms:
2806 // - the "hit" path resolves to `Ok(full)` byte-equal
2807 // - every other path resolves to `MissingEntry` on the same
2808 // [`StandardLayout`] instance
2809 // The pin traps a regression that reroutes the primitive off
2810 // the injected oracle onto the ambient `Path::exists` (which
2811 // would silently return `false` for every path in `/tmp/x/…`
2812 // and mask the miss-arm hand-off on the hit fixture, or
2813 // silently return `true` for a real system path and mask the
2814 // hit-arm hand-off on the miss fixture).
2815 let root = PathBuf::from("/tmp/x");
2816 let hit_path = Path::new("servicos/keep.computeunit.yaml");
2817 let miss_path = Path::new("servicos/drop.computeunit.yaml");
2818 let hit_full = root.join(hit_path);
2819 let hit_full_probe = hit_full.clone();
2820 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_full_probe);
2821
2822 let resolved = layout
2823 .probe_declared_entry(
2824 hit_path,
2825 &root,
2826 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2827 )
2828 .expect("probe_declared_entry must consult the injected oracle on the hit arm");
2829 assert_eq!(
2830 resolved, hit_full,
2831 "probe_declared_entry hit arm must return the oracle-approved resolved path",
2832 );
2833
2834 let err = layout
2835 .probe_declared_entry(
2836 miss_path,
2837 &root,
2838 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2839 )
2840 .unwrap_err();
2841 assert_eq!(
2842 err,
2843 LayoutError::missing_entry(
2844 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2845 root.join(miss_path),
2846 ),
2847 "probe_declared_entry miss arm must fire when the injected oracle rejects the path",
2848 );
2849 }
2850
2851 // ── StandardLayout::probe_sandboxed_declared_entry substrate primitive ─
2852 //
2853 // The [`StandardLayout::probe_sandboxed_declared_entry`] method
2854 // (`layout.rs`) folds the two self-similar `let full = self.
2855 // probe_declared_entry(p, root, <kind>)?; if !full.starts_with(&<slot>_dir)
2856 // { return Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
2857 // [`StandardLayout::verify`] (`:exe` iteration, `:servicos` iteration)
2858 // onto one substrate primitive. The pins below (fail-before-pass-after
2859 // by construction — a byte-mismatched primitive body would trip its
2860 // equivalence pin first) lock the fold to its pre-lift open-coded
2861 // shape under `PartialEq`, so every wire-up in
2862 // [`StandardLayout::verify`] on this primitive produces a byte-equal
2863 // `LayoutError` on miss / sandbox-escape and a byte-equal `PathBuf`
2864 // on hit.
2865
2866 #[test]
2867 fn probe_sandboxed_declared_entry_folds_miss_returns_missing_entry() {
2868 // Per-primitive equivalence pin on the miss arm — a
2869 // [`StandardLayout`] whose oracle returns `false` for every
2870 // path yields a `MissingEntry` byte-equal under `PartialEq` to
2871 // the open-coded `LayoutError::missing_entry(<kind>,
2872 // root.join(path))` wrap the sibling
2873 // [`StandardLayout::probe_declared_entry`] primitive routes
2874 // through. Diagnostic-order pin: `MissingEntry` outranks the
2875 // `outside_ctor` sandbox-escape arm on the same iteration, so
2876 // an entry that is both absent *and* outside the sandbox fires
2877 // the `MissingEntry` diagnostic (the pre-lift order the two
2878 // wire-up sites carried).
2879 let layout = StandardLayout::new().with_path_exists(|_| false);
2880 let root = PathBuf::from("/tmp/x");
2881 let path = Path::new("lib/tool");
2882 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2883 let err = layout
2884 .probe_sandboxed_declared_entry(
2885 path,
2886 &root,
2887 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2888 &exe_dir,
2889 LayoutError::ExeOutsideDir,
2890 )
2891 .unwrap_err();
2892 assert_eq!(
2893 err,
2894 LayoutError::missing_entry(
2895 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2896 root.join(path),
2897 ),
2898 "probe_sandboxed_declared_entry miss arm must produce byte-equal \
2899 LayoutError to the sibling probe_declared_entry primitive's miss wrap, \
2900 preserving the pre-lift MissingEntry-before-<Slot>OutsideDir diagnostic order",
2901 );
2902 }
2903
2904 #[test]
2905 fn probe_sandboxed_declared_entry_folds_sandbox_escape_via_outside_ctor() {
2906 // Per-primitive equivalence pin on the sandbox-escape arm — a
2907 // [`StandardLayout`] whose oracle admits the probed resolved
2908 // path (so the miss arm passes) but whose resolved path lies
2909 // outside the caller-provided `sandbox_dir` yields the paired
2910 // `outside_ctor(full)` byte-equal under `PartialEq`. The
2911 // primitive threads the resolved `full` through the caller-
2912 // supplied `fn(PathBuf) -> LayoutError` constructor rather
2913 // than a hard-coded variant, so the fold does not silently
2914 // collapse onto one of the two `:exe` / `:servicos` outside-
2915 // dir variants.
2916 let root = PathBuf::from("/tmp/x");
2917 let path = Path::new("lib/tool");
2918 let full = root.join(path);
2919 let full_probe = full.clone();
2920 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2921 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2922 let err = layout
2923 .probe_sandboxed_declared_entry(
2924 path,
2925 &root,
2926 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2927 &exe_dir,
2928 LayoutError::ExeOutsideDir,
2929 )
2930 .unwrap_err();
2931 assert_eq!(
2932 err,
2933 LayoutError::ExeOutsideDir(full),
2934 "probe_sandboxed_declared_entry sandbox-escape arm must route the \
2935 resolved `full` through the caller-supplied outside_ctor byte-equal to \
2936 the pre-lift `LayoutError::ExeOutsideDir(full)` tuple-literal",
2937 );
2938 }
2939
2940 #[test]
2941 fn probe_sandboxed_declared_entry_folds_hit_returns_resolved_full() {
2942 // Per-primitive equivalence pin on the hit arm — a
2943 // [`StandardLayout`] whose oracle admits the probed path *and*
2944 // whose resolved path lives inside `sandbox_dir` yields
2945 // `Ok(root.join(path))` byte-equal under `PartialEq`. The
2946 // pre-lift wire-ups discarded the resolved `Ok(PathBuf)` since
2947 // no follow-up per-path gate consumes it after the sandbox
2948 // check; the fold preserves the same hit-arm hand-off through
2949 // the primitive's `Ok(PathBuf)` return so a future consumer
2950 // that wants to run a per-path successor gate reaches the
2951 // resolved path without re-computing `root.join(path)`.
2952 let root = PathBuf::from("/tmp/x");
2953 let path = Path::new("exe/tool");
2954 let full = root.join(path);
2955 let full_probe = full.clone();
2956 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2957 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
2958 let resolved = layout
2959 .probe_sandboxed_declared_entry(
2960 path,
2961 &root,
2962 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2963 &exe_dir,
2964 LayoutError::ExeOutsideDir,
2965 )
2966 .expect(
2967 "probe_sandboxed_declared_entry must return Ok(root.join(path)) \
2968 when the oracle admits the path and it lives inside sandbox_dir",
2969 );
2970 assert_eq!(
2971 resolved, full,
2972 "probe_sandboxed_declared_entry hit arm must return the resolved \
2973 `root.join(path)` byte-equal so a future per-path successor gate \
2974 reaches it without re-computing",
2975 );
2976 }
2977
2978 #[test]
2979 fn probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim() {
2980 // Cross-axis pin — the primitive must thread the caller-
2981 // supplied `outside_ctor` verbatim into the sandbox-escape
2982 // arm's `LayoutError` return, so the fold does not silently
2983 // collapse onto one hard-coded variant. Sweep both
2984 // [`LayoutError`] tuple-variants the two wire-up sites in
2985 // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
2986 // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
2987 // ctor-routing on either axis would trip.
2988 let root = PathBuf::from("/tmp/x");
2989 let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
2990 for (kind, sandbox_component, ctor, expected_variant) in [
2991 (
2992 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2993 crate::render::LAYOUT_DIR_EXE,
2994 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
2995 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
2996 ),
2997 (
2998 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2999 crate::render::LAYOUT_DIR_SERVICOS,
3000 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3001 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3002 ),
3003 ] {
3004 let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3005 let full = root.join(&path);
3006 let full_probe = full.clone();
3007 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3008 let sandbox_dir = root.join(sandbox_component);
3009 let err = layout
3010 .probe_sandboxed_declared_entry(&path, &root, kind, &sandbox_dir, ctor)
3011 .unwrap_err();
3012 assert_eq!(
3013 err,
3014 expected_variant(full),
3015 "probe_sandboxed_declared_entry must thread `outside_ctor` verbatim \
3016 on every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3017 );
3018 }
3019 }
3020
3021 #[test]
3022 fn probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry() {
3023 // Cross-primitive pin — the sandboxed probe must route its
3024 // miss arm through the sibling [`StandardLayout::
3025 // probe_declared_entry`] primitive rather than re-inlining the
3026 // `root.join` + `exists` + `missing_entry` cascade, so a
3027 // future edit to the miss-arm shape on either primitive lands
3028 // in exactly one place. Byte-parity assertion: on a fixture
3029 // that misses the oracle, the sandboxed primitive's `Err`
3030 // arm must equal the peer [`StandardLayout::
3031 // probe_declared_entry`] primitive's `Err` arm on the same
3032 // fixture — otherwise the fold has drifted from the substrate
3033 // primitive.
3034 let layout = StandardLayout::new().with_path_exists(|_| false);
3035 let root = PathBuf::from("/tmp/x");
3036 let path = Path::new("servicos/hello-rio.computeunit.yaml");
3037 let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3038 let via_sandboxed = layout
3039 .probe_sandboxed_declared_entry(
3040 path,
3041 &root,
3042 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3043 &sandbox_dir,
3044 LayoutError::ServicoOutsideDir,
3045 )
3046 .unwrap_err();
3047 let via_probe = layout
3048 .probe_declared_entry(
3049 path,
3050 &root,
3051 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3052 )
3053 .unwrap_err();
3054 assert_eq!(
3055 via_sandboxed, via_probe,
3056 "probe_sandboxed_declared_entry's miss arm must equal the sibling \
3057 probe_declared_entry primitive's miss arm byte-equal — pins that the \
3058 sandboxed primitive routes through the substrate primitive rather than \
3059 re-inlining the `root.join` + `exists` + `missing_entry` cascade",
3060 );
3061 }
3062
3063 // ── StandardLayout::probe_declared_entries substrate primitive ───────
3064 //
3065 // The [`StandardLayout::probe_declared_entries`] method (`layout.rs`)
3066 // folds the three self-similar per-slot existence-probe *loop* blocks
3067 // at [`StandardLayout::verify`] (`:bibliotecas` iteration,
3068 // `:behavior` on-disk callback-path iteration, `:upgrade-from` per-
3069 // instruction script-path iteration) onto one substrate primitive.
3070 // The pins below (fail-before-pass-after by construction — a byte-
3071 // mismatched primitive body would trip its equivalence pin first)
3072 // lock the fold to its pre-lift open-coded shape under `PartialEq`,
3073 // so every wire-up in [`StandardLayout::verify`] on this primitive
3074 // produces a byte-equal `LayoutError` on miss (via the sibling
3075 // [`StandardLayout::probe_declared_entry`] miss arm) and a byte-
3076 // equal `Ok(())` on the empty / all-hit arms (the fold's identity
3077 // element on an empty slot list; the pre-lift `for … { … }` loop's
3078 // vacuous pass-through).
3079 //
3080 // Sibling of the peer per-arm-probe [`StandardLayout::probe_declared_entry`]
3081 // (fda1e35) and two-arm-sandboxed [`StandardLayout::
3082 // probe_sandboxed_declared_entry`] (4940d55) primitive test blocks
3083 // — same substrate-primitive discipline extended onto the per-slot
3084 // batch axis these two per-path primitives compose under.
3085
3086 #[test]
3087 fn probe_declared_entries_folds_empty_iterator_returns_ok() {
3088 // Per-primitive identity-element pin on the empty-iterator arm —
3089 // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3090 // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3091 // pass-through. Peer of the peer-primitive `Option::None →
3092 // Ok(())` identity elements the sibling per-Caixa compound
3093 // gates ([`crate::Caixa::validate_limits`] baa4688,
3094 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on
3095 // the sibling per-slot compound-gate axis.
3096 let layout = StandardLayout::new().with_path_exists(|_| false);
3097 let root = PathBuf::from("/tmp/x");
3098 let empty: [&Path; 0] = [];
3099 layout
3100 .probe_declared_entries(
3101 empty,
3102 &root,
3103 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3104 )
3105 .expect(
3106 "probe_declared_entries must return Ok(()) on an empty iterator \
3107 — the fold's identity element on an empty per-slot batch",
3108 );
3109 }
3110
3111 #[test]
3112 fn probe_declared_entries_folds_all_hits_returns_ok() {
3113 // Per-primitive equivalence pin on the all-hit arm — a
3114 // [`StandardLayout`] whose oracle admits every path in the batch
3115 // yields `Ok(())` byte-equal to the pre-lift `for p in … {
3116 // self.probe_declared_entry(p, root, kind)?; }` loop's full-
3117 // consumption pass-through.
3118 let root = PathBuf::from("/tmp/x");
3119 let a = root.join("lib/a.lisp");
3120 let b = root.join("lib/b.lisp");
3121 let a_probe = a.clone();
3122 let b_probe = b.clone();
3123 let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3124 layout
3125 .probe_declared_entries(
3126 [Path::new("lib/a.lisp"), Path::new("lib/b.lisp")],
3127 &root,
3128 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3129 )
3130 .expect(
3131 "probe_declared_entries must return Ok(()) when every path in the \
3132 batch is admitted by the oracle",
3133 );
3134 }
3135
3136 #[test]
3137 fn probe_declared_entries_folds_first_miss_short_circuits_via_probe_declared_entry() {
3138 // Per-primitive equivalence pin on the first-miss arm — a
3139 // [`StandardLayout`] whose oracle admits the first path and
3140 // rejects the second yields a `MissingEntry` byte-equal under
3141 // `PartialEq` to the sibling [`StandardLayout::
3142 // probe_declared_entry`] primitive's miss wrap on the *second*
3143 // path (the pre-lift `for … { … ? }` loop's first-error return
3144 // semantics), *not* on the first (admitted) path.
3145 let root = PathBuf::from("/tmp/x");
3146 let hit = root.join("lib/a.lisp");
3147 let hit_probe = hit.clone();
3148 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3149 let miss = Path::new("lib/b.lisp");
3150 let err = layout
3151 .probe_declared_entries(
3152 [Path::new("lib/a.lisp"), miss],
3153 &root,
3154 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3155 )
3156 .unwrap_err();
3157 assert_eq!(
3158 err,
3159 LayoutError::missing_entry(
3160 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3161 root.join(miss),
3162 ),
3163 "probe_declared_entries must short-circuit on the first missing entry \
3164 with a `MissingEntry` byte-equal to the sibling probe_declared_entry \
3165 primitive's miss wrap on that entry — the pre-lift `for … {{ … ? }}` \
3166 loop's first-error return semantics",
3167 );
3168 }
3169
3170 #[test]
3171 fn probe_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3172 // Diagnostic-order pin — the primitive must return on the *first*
3173 // miss in iterator order rather than probing every path and
3174 // returning the last miss (which would silently drop the pre-
3175 // lift `for … { … ? }` loop's first-error contract). Fixture:
3176 // the oracle rejects both paths, so a byte-equal `MissingEntry`
3177 // on the *first* path in the iterator distinguishes the two
3178 // return-order shapes.
3179 let layout = StandardLayout::new().with_path_exists(|_| false);
3180 let root = PathBuf::from("/tmp/x");
3181 let first = Path::new("lib/first.lisp");
3182 let second = Path::new("lib/second.lisp");
3183 let err = layout
3184 .probe_declared_entries(
3185 [first, second],
3186 &root,
3187 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3188 )
3189 .unwrap_err();
3190 assert_eq!(
3191 err,
3192 LayoutError::missing_entry(
3193 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3194 root.join(first),
3195 ),
3196 "probe_declared_entries must return on the *first* miss in iterator order \
3197 — a `MissingEntry` on the second path would silently drop the pre-lift \
3198 `for … {{ … ? }}` loop's first-error contract",
3199 );
3200 }
3201
3202 #[test]
3203 fn probe_declared_entries_threads_kind_through_arg_verbatim() {
3204 // Cross-axis pin — sweep every canonical
3205 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the three
3206 // batch wire-up sites in [`StandardLayout::verify`] pass through
3207 // this primitive (`:bibliotecas`, `:behavior` callback,
3208 // `:upgrade-from` script). Each miss must return a `MissingEntry`
3209 // whose `kind:` field byte-equals the caller-provided arg, so
3210 // the fold does not silently collapse onto one hard-coded label.
3211 // Sibling of the peer `probe_declared_entry_threads_kind_through_arg_verbatim`
3212 // pin's five-label sweep on the per-arm primitive.
3213 let layout = StandardLayout::new().with_path_exists(|_| false);
3214 let root = PathBuf::from("/tmp/x");
3215 let path = Path::new("some/entry");
3216 for kind in [
3217 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3218 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
3219 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
3220 ] {
3221 let err = layout
3222 .probe_declared_entries([path], &root, kind)
3223 .unwrap_err();
3224 assert_eq!(
3225 err,
3226 LayoutError::missing_entry(kind, root.join(path)),
3227 "probe_declared_entries must thread `kind` verbatim on every canonical \
3228 batch label",
3229 );
3230 }
3231 }
3232
3233 #[test]
3234 fn probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3235 // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3236 // admit every concrete iterator element type the three
3237 // [`StandardLayout::verify`] wire-up sites pass:
3238 // - `&String` (from `caixa.bibliotecas(): &[String]`),
3239 // - `&Path` (from `b.declared_paths(): impl Iterator<Item = &Path>`),
3240 // - `&PathBuf` (from the flattened `.filter_map(_::declared_path)`
3241 // on `:upgrade-from` instructions, whose `declared_path`
3242 // returns `Option<&PathBuf>`).
3243 //
3244 // Byte-parity assertion: on a shared `/tmp/x/lib/demo.lisp`
3245 // fixture the miss-arm return must be byte-equal across all
3246 // three element-type flavors, so a future re-shape of the
3247 // bound (a narrower `P: Into<PathBuf>` collapse, a stricter
3248 // `&Path`-only signature) would surface here rather than at
3249 // the caller wire-up site.
3250 let layout = StandardLayout::new().with_path_exists(|_| false);
3251 let root = PathBuf::from("/tmp/x");
3252 let literal = "lib/demo.lisp";
3253 let expected = LayoutError::missing_entry(
3254 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3255 root.join(Path::new(literal)),
3256 );
3257
3258 let via_string_slice: Vec<String> = vec![literal.to_string()];
3259 let via_string_err = layout
3260 .probe_declared_entries(
3261 via_string_slice.iter(),
3262 &root,
3263 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3264 )
3265 .unwrap_err();
3266 assert_eq!(
3267 via_string_err, expected,
3268 "probe_declared_entries must accept `&String` items (the `caixa.bibliotecas() \
3269 : &[String]` wire-up shape)",
3270 );
3271
3272 let via_path_slice: [&Path; 1] = [Path::new(literal)];
3273 let via_path_err = layout
3274 .probe_declared_entries(
3275 via_path_slice,
3276 &root,
3277 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3278 )
3279 .unwrap_err();
3280 assert_eq!(
3281 via_path_err, expected,
3282 "probe_declared_entries must accept `&Path` items (the `b.declared_paths() \
3283 : impl Iterator<Item = &Path>` wire-up shape)",
3284 );
3285
3286 let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3287 let via_pathbuf_err = layout
3288 .probe_declared_entries(
3289 via_pathbuf_slice.iter(),
3290 &root,
3291 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3292 )
3293 .unwrap_err();
3294 assert_eq!(
3295 via_pathbuf_err, expected,
3296 "probe_declared_entries must accept `&PathBuf` items (the `.filter_map( \
3297 _::declared_path)` on `:upgrade-from` instructions wire-up shape)",
3298 );
3299 }
3300
3301 #[test]
3302 fn probe_declared_entries_routes_miss_arm_through_probe_declared_entry() {
3303 // Cross-primitive pin — the batch primitive must route its miss
3304 // arm through the sibling [`StandardLayout::probe_declared_entry`]
3305 // primitive rather than re-inlining the `root.join` + `exists`
3306 // + `missing_entry` cascade, so a future edit to the miss-arm
3307 // shape on either primitive lands in exactly one place. Byte-
3308 // parity assertion: on a fixture that misses the oracle, the
3309 // batch primitive's `Err` arm must equal the peer per-arm
3310 // [`StandardLayout::probe_declared_entry`] primitive's `Err` arm
3311 // on the same fixture — otherwise the batch fold has drifted
3312 // from the substrate primitive. Same discipline the peer
3313 // `probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry`
3314 // pin establishes on the sibling sandboxed-primitive axis.
3315 let layout = StandardLayout::new().with_path_exists(|_| false);
3316 let root = PathBuf::from("/tmp/x");
3317 let path = Path::new("lib/demo.lisp");
3318 let via_batch = layout
3319 .probe_declared_entries(
3320 [path],
3321 &root,
3322 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3323 )
3324 .unwrap_err();
3325 let via_probe = layout
3326 .probe_declared_entry(
3327 path,
3328 &root,
3329 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3330 )
3331 .unwrap_err();
3332 assert_eq!(
3333 via_batch, via_probe,
3334 "probe_declared_entries's miss arm must equal the sibling probe_declared_entry \
3335 primitive's miss arm byte-equal — pins that the batch primitive routes \
3336 through the substrate primitive rather than re-inlining the `root.join` + \
3337 `exists` + `missing_entry` cascade",
3338 );
3339 }
3340
3341 // ── StandardLayout::probe_sandboxed_declared_entries substrate primitive ─
3342 //
3343 // The [`StandardLayout::probe_sandboxed_declared_entries`] method
3344 // (`layout.rs`) folds the two self-similar per-slot sandboxed-
3345 // existence-probe *loop* blocks at [`StandardLayout::verify`]
3346 // (`:exe` iteration, `:servicos` iteration) onto one substrate
3347 // primitive. The pins below (fail-before-pass-after by construction
3348 // — a byte-mismatched primitive body would trip its equivalence pin
3349 // first) lock the fold to its pre-lift open-coded shape under
3350 // `PartialEq`, so every wire-up in [`StandardLayout::verify`] on
3351 // this primitive produces a byte-equal `LayoutError` on miss / on
3352 // sandbox-escape (via the sibling
3353 // [`StandardLayout::probe_sandboxed_declared_entry`] arms) and a
3354 // byte-equal `Ok(())` on the empty / all-hit arms (the fold's
3355 // identity element on an empty slot list; the pre-lift `for … {
3356 // … }` loop's vacuous pass-through).
3357 //
3358 // Sibling of the peer per-slot batch bare-probe
3359 // [`StandardLayout::probe_declared_entries`] (d1ccb0b), per-arm
3360 // bare-probe [`StandardLayout::probe_declared_entry`] (fda1e35), and
3361 // per-arm sandboxed-probe
3362 // [`StandardLayout::probe_sandboxed_declared_entry`] (4940d55)
3363 // primitive test blocks — same substrate-primitive discipline
3364 // extended onto the fourth and last quadrant of the
3365 // (per-arm | per-slot batch) × (bare | sandboxed) existence-probe
3366 // algebra.
3367
3368 #[test]
3369 fn probe_sandboxed_declared_entries_folds_empty_iterator_returns_ok() {
3370 // Per-primitive identity-element pin on the empty-iterator arm —
3371 // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3372 // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3373 // pass-through. Peer of the sibling
3374 // `probe_declared_entries_folds_empty_iterator_returns_ok` pin
3375 // on the bare-batch axis and the `Option::None → Ok(())`
3376 // identity elements the per-Caixa compound gates
3377 // ([`crate::Caixa::validate_limits`] baa4688,
3378 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on the
3379 // per-slot compound-gate axis.
3380 let layout = StandardLayout::new().with_path_exists(|_| false);
3381 let root = PathBuf::from("/tmp/x");
3382 let empty: [&Path; 0] = [];
3383 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3384 layout
3385 .probe_sandboxed_declared_entries(
3386 empty,
3387 &root,
3388 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3389 &exe_dir,
3390 LayoutError::ExeOutsideDir,
3391 )
3392 .expect(
3393 "probe_sandboxed_declared_entries must return Ok(()) on an empty iterator \
3394 — the fold's identity element on an empty per-slot sandboxed batch",
3395 );
3396 }
3397
3398 #[test]
3399 fn probe_sandboxed_declared_entries_folds_all_hits_returns_ok() {
3400 // Per-primitive equivalence pin on the all-hit arm — a
3401 // [`StandardLayout`] whose oracle admits every path in the batch
3402 // *and* whose resolved paths live inside `sandbox_dir` yields
3403 // `Ok(())` byte-equal to the pre-lift `for p in … {
3404 // self.probe_sandboxed_declared_entry(p, root, kind,
3405 // &sandbox_dir, outside_ctor)?; }` loop's full-consumption
3406 // pass-through.
3407 let root = PathBuf::from("/tmp/x");
3408 let a = root.join("exe/a");
3409 let b = root.join("exe/b");
3410 let a_probe = a.clone();
3411 let b_probe = b.clone();
3412 let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3413 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3414 layout
3415 .probe_sandboxed_declared_entries(
3416 [Path::new("exe/a"), Path::new("exe/b")],
3417 &root,
3418 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3419 &exe_dir,
3420 LayoutError::ExeOutsideDir,
3421 )
3422 .expect(
3423 "probe_sandboxed_declared_entries must return Ok(()) when every path in \
3424 the batch is admitted by the oracle and lives inside sandbox_dir",
3425 );
3426 }
3427
3428 #[test]
3429 fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_via_probe_sandboxed_declared_entry()
3430 {
3431 // Per-primitive equivalence pin on the first-miss arm — a
3432 // [`StandardLayout`] whose oracle admits the first path (inside
3433 // sandbox_dir) and rejects the second yields a `MissingEntry`
3434 // byte-equal under `PartialEq` to the sibling
3435 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3436 // miss wrap on the *second* path (the pre-lift `for … { … ? }`
3437 // loop's first-error return semantics), *not* on the first
3438 // (admitted) path.
3439 let root = PathBuf::from("/tmp/x");
3440 let hit = root.join("exe/a");
3441 let hit_probe = hit.clone();
3442 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3443 let miss = Path::new("exe/b");
3444 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3445 let err = layout
3446 .probe_sandboxed_declared_entries(
3447 [Path::new("exe/a"), miss],
3448 &root,
3449 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3450 &exe_dir,
3451 LayoutError::ExeOutsideDir,
3452 )
3453 .unwrap_err();
3454 assert_eq!(
3455 err,
3456 LayoutError::missing_entry(
3457 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3458 root.join(miss),
3459 ),
3460 "probe_sandboxed_declared_entries must short-circuit on the first missing \
3461 entry with a `MissingEntry` byte-equal to the sibling \
3462 probe_sandboxed_declared_entry primitive's miss wrap on that entry — the \
3463 pre-lift `for … {{ … ? }}` loop's first-error return semantics",
3464 );
3465 }
3466
3467 #[test]
3468 fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3469 // Diagnostic-order pin — the primitive must return on the *first*
3470 // miss in iterator order rather than probing every path and
3471 // returning the last miss (which would silently drop the pre-
3472 // lift `for … { … ? }` loop's first-error contract). Fixture:
3473 // the oracle rejects both paths, so a byte-equal `MissingEntry`
3474 // on the *first* path in the iterator distinguishes the two
3475 // return-order shapes. Sibling of the peer
3476 // `probe_declared_entries_folds_first_miss_short_circuits_before_later_paths`
3477 // pin on the bare-batch axis.
3478 let layout = StandardLayout::new().with_path_exists(|_| false);
3479 let root = PathBuf::from("/tmp/x");
3480 let first = Path::new("exe/first");
3481 let second = Path::new("exe/second");
3482 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3483 let err = layout
3484 .probe_sandboxed_declared_entries(
3485 [first, second],
3486 &root,
3487 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3488 &exe_dir,
3489 LayoutError::ExeOutsideDir,
3490 )
3491 .unwrap_err();
3492 assert_eq!(
3493 err,
3494 LayoutError::missing_entry(
3495 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3496 root.join(first),
3497 ),
3498 "probe_sandboxed_declared_entries must return on the *first* miss in iterator \
3499 order — a `MissingEntry` on the second path would silently drop the pre-lift \
3500 `for … {{ … ? }}` loop's first-error contract",
3501 );
3502 }
3503
3504 #[test]
3505 fn probe_sandboxed_declared_entries_folds_sandbox_escape_via_outside_ctor() {
3506 // Per-primitive equivalence pin on the sandbox-escape arm — a
3507 // [`StandardLayout`] whose oracle admits the probed resolved
3508 // path (so the miss arm passes) but whose resolved path lies
3509 // outside the caller-provided `sandbox_dir` yields the paired
3510 // `outside_ctor(full)` byte-equal under `PartialEq`. The
3511 // primitive threads the resolved `full` through the caller-
3512 // supplied `fn(PathBuf) -> LayoutError` constructor rather than
3513 // a hard-coded variant, so the fold does not silently collapse
3514 // onto one of the two `:exe` / `:servicos` outside-dir variants.
3515 let root = PathBuf::from("/tmp/x");
3516 let path = Path::new("lib/tool");
3517 let full = root.join(path);
3518 let full_probe = full.clone();
3519 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3520 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3521 let err = layout
3522 .probe_sandboxed_declared_entries(
3523 [path],
3524 &root,
3525 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3526 &exe_dir,
3527 LayoutError::ExeOutsideDir,
3528 )
3529 .unwrap_err();
3530 assert_eq!(
3531 err,
3532 LayoutError::ExeOutsideDir(full),
3533 "probe_sandboxed_declared_entries sandbox-escape arm must route the resolved \
3534 `full` through the caller-supplied outside_ctor byte-equal to the pre-lift \
3535 `LayoutError::ExeOutsideDir(full)` tuple-literal",
3536 );
3537 }
3538
3539 #[test]
3540 fn probe_sandboxed_declared_entries_threads_outside_ctor_through_arg_verbatim() {
3541 // Cross-axis pin — the primitive must thread the caller-supplied
3542 // `outside_ctor` verbatim into the sandbox-escape arm's
3543 // `LayoutError` return, so the fold does not silently collapse
3544 // onto one hard-coded variant. Sweep both [`LayoutError`]
3545 // tuple-variants the two wire-up sites in
3546 // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3547 // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3548 // ctor-routing on either axis would trip. Sibling of the peer
3549 // `probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim`
3550 // pin on the per-arm sandboxed-primitive axis.
3551 let root = PathBuf::from("/tmp/x");
3552 let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3553 for (kind, sandbox_component, ctor) in [
3554 (
3555 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3556 crate::render::LAYOUT_DIR_EXE,
3557 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3558 ),
3559 (
3560 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3561 crate::render::LAYOUT_DIR_SERVICOS,
3562 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3563 ),
3564 ] {
3565 let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3566 let full = root.join(&path);
3567 let full_probe = full.clone();
3568 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3569 let sandbox_dir = root.join(sandbox_component);
3570 let err = layout
3571 .probe_sandboxed_declared_entries([&path], &root, kind, &sandbox_dir, ctor)
3572 .unwrap_err();
3573 assert_eq!(
3574 err,
3575 ctor(full),
3576 "probe_sandboxed_declared_entries must thread `outside_ctor` verbatim on \
3577 every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3578 );
3579 }
3580 }
3581
3582 #[test]
3583 fn probe_sandboxed_declared_entries_threads_kind_through_arg_verbatim() {
3584 // Cross-axis pin — sweep every canonical
3585 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the two
3586 // sandboxed-batch wire-up sites in [`StandardLayout::verify`]
3587 // pass through this primitive (`:exe`, `:servicos`). Each miss
3588 // must return a `MissingEntry` whose `kind:` field byte-equals
3589 // the caller-provided arg, so the fold does not silently
3590 // collapse onto one hard-coded label. Sibling of the peer
3591 // `probe_declared_entries_threads_kind_through_arg_verbatim`
3592 // pin's label sweep on the bare-batch axis.
3593 let layout = StandardLayout::new().with_path_exists(|_| false);
3594 let root = PathBuf::from("/tmp/x");
3595 let path = Path::new("some/entry");
3596 for (kind, sandbox_component, ctor) in [
3597 (
3598 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3599 crate::render::LAYOUT_DIR_EXE,
3600 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3601 ),
3602 (
3603 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3604 crate::render::LAYOUT_DIR_SERVICOS,
3605 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3606 ),
3607 ] {
3608 let sandbox_dir = root.join(sandbox_component);
3609 let err = layout
3610 .probe_sandboxed_declared_entries([path], &root, kind, &sandbox_dir, ctor)
3611 .unwrap_err();
3612 assert_eq!(
3613 err,
3614 LayoutError::missing_entry(kind, root.join(path)),
3615 "probe_sandboxed_declared_entries must thread `kind` verbatim on every \
3616 canonical sandboxed-batch label",
3617 );
3618 }
3619 }
3620
3621 #[test]
3622 fn probe_sandboxed_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3623 // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3624 // admit every concrete iterator element type the two
3625 // [`StandardLayout::verify`] wire-up sites pass:
3626 // - `&String` (from `caixa.exe(): &[String]`),
3627 // - `&String` (from `caixa.servicos(): &[String]`).
3628 //
3629 // Byte-parity assertion: on a shared `/tmp/x/lib/demo`
3630 // fixture the miss-arm return must be byte-equal across
3631 // `&String` and the ergonomic `&Path` / `&PathBuf` element-type
3632 // flavors, so a future re-shape of the bound (a narrower `P:
3633 // Into<PathBuf>` collapse, a stricter `&Path`-only signature)
3634 // would surface here rather than at the caller wire-up site.
3635 // Sibling of the peer
3636 // `probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass`
3637 // pin on the bare-batch axis.
3638 let layout = StandardLayout::new().with_path_exists(|_| false);
3639 let root = PathBuf::from("/tmp/x");
3640 let literal = "exe/demo";
3641 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3642 let expected = LayoutError::missing_entry(
3643 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3644 root.join(Path::new(literal)),
3645 );
3646
3647 let via_string_slice: Vec<String> = vec![literal.to_string()];
3648 let via_string_err = layout
3649 .probe_sandboxed_declared_entries(
3650 via_string_slice.iter(),
3651 &root,
3652 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3653 &exe_dir,
3654 LayoutError::ExeOutsideDir,
3655 )
3656 .unwrap_err();
3657 assert_eq!(
3658 via_string_err, expected,
3659 "probe_sandboxed_declared_entries must accept `&String` items (the `caixa.exe() \
3660 : &[String]` / `caixa.servicos(): &[String]` wire-up shape)",
3661 );
3662
3663 let via_path_slice: [&Path; 1] = [Path::new(literal)];
3664 let via_path_err = layout
3665 .probe_sandboxed_declared_entries(
3666 via_path_slice,
3667 &root,
3668 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3669 &exe_dir,
3670 LayoutError::ExeOutsideDir,
3671 )
3672 .unwrap_err();
3673 assert_eq!(
3674 via_path_err, expected,
3675 "probe_sandboxed_declared_entries must accept `&Path` items",
3676 );
3677
3678 let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3679 let via_pathbuf_err = layout
3680 .probe_sandboxed_declared_entries(
3681 via_pathbuf_slice.iter(),
3682 &root,
3683 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3684 &exe_dir,
3685 LayoutError::ExeOutsideDir,
3686 )
3687 .unwrap_err();
3688 assert_eq!(
3689 via_pathbuf_err, expected,
3690 "probe_sandboxed_declared_entries must accept `&PathBuf` items",
3691 );
3692 }
3693
3694 #[test]
3695 fn probe_sandboxed_declared_entries_routes_miss_arm_through_probe_sandboxed_declared_entry() {
3696 // Cross-primitive pin — the batch primitive must route its miss
3697 // arm through the sibling
3698 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive
3699 // rather than re-inlining the `probe_declared_entry` +
3700 // `starts_with` cascade, so a future edit to the miss-arm shape
3701 // on either primitive lands in exactly one place. Byte-parity
3702 // assertion: on a fixture that misses the oracle, the batch
3703 // primitive's `Err` arm must equal the peer per-arm
3704 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3705 // `Err` arm on the same fixture — otherwise the batch fold has
3706 // drifted from the substrate primitive. Same discipline the peer
3707 // `probe_declared_entries_routes_miss_arm_through_probe_declared_entry`
3708 // pin establishes on the sibling bare-batch axis.
3709 let layout = StandardLayout::new().with_path_exists(|_| false);
3710 let root = PathBuf::from("/tmp/x");
3711 let path = Path::new("servicos/hello-rio.computeunit.yaml");
3712 let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3713 let via_batch = layout
3714 .probe_sandboxed_declared_entries(
3715 [path],
3716 &root,
3717 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3718 &sandbox_dir,
3719 LayoutError::ServicoOutsideDir,
3720 )
3721 .unwrap_err();
3722 let via_probe = layout
3723 .probe_sandboxed_declared_entry(
3724 path,
3725 &root,
3726 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3727 &sandbox_dir,
3728 LayoutError::ServicoOutsideDir,
3729 )
3730 .unwrap_err();
3731 assert_eq!(
3732 via_batch, via_probe,
3733 "probe_sandboxed_declared_entries's miss arm must equal the sibling \
3734 probe_sandboxed_declared_entry primitive's miss arm byte-equal — pins that \
3735 the batch primitive routes through the substrate primitive rather than \
3736 re-inlining the `probe_declared_entry` + `starts_with` cascade",
3737 );
3738 }
3739
3740 #[test]
3741 fn probe_sandboxed_declared_entries_routes_sandbox_escape_arm_through_probe_sandboxed_declared_entry()
3742 {
3743 // Cross-primitive pin — the batch primitive's sandbox-escape arm
3744 // must equal the sibling
3745 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3746 // sandbox-escape arm on the same fixture. Byte-parity assertion:
3747 // on a fixture where the oracle admits the probed path but the
3748 // resolved path lies outside sandbox_dir, both primitives must
3749 // return the same `outside_ctor(full)` byte-equal under
3750 // `PartialEq` — otherwise the batch fold has drifted from the
3751 // per-arm primitive on the second-arm dispatch.
3752 let root = PathBuf::from("/tmp/x");
3753 let path = Path::new("lib/escape");
3754 let full = root.join(path);
3755 let full_probe = full.clone();
3756 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3757 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3758 let via_batch = layout
3759 .probe_sandboxed_declared_entries(
3760 [path],
3761 &root,
3762 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3763 &exe_dir,
3764 LayoutError::ExeOutsideDir,
3765 )
3766 .unwrap_err();
3767 let via_probe = layout
3768 .probe_sandboxed_declared_entry(
3769 path,
3770 &root,
3771 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3772 &exe_dir,
3773 LayoutError::ExeOutsideDir,
3774 )
3775 .unwrap_err();
3776 assert_eq!(
3777 via_batch, via_probe,
3778 "probe_sandboxed_declared_entries's sandbox-escape arm must equal the sibling \
3779 probe_sandboxed_declared_entry primitive's sandbox-escape arm byte-equal — \
3780 pins that the batch primitive routes through the substrate primitive on both \
3781 the miss and sandbox-escape arms rather than re-inlining either cascade",
3782 );
3783 }
3784
3785 // ── LayoutError::<nome-only> constructor family ──────────────────────
3786 //
3787 // The [`layout_nome_only_ctors!`] macro (below the `LayoutError` enum
3788 // definition) generates one static constructor per `<Variant>(String)`
3789 // tuple-variant that folds the uniform `Self::<Variant>(caixa.nome()
3790 // .to_string())` one-field construction onto one substrate primitive.
3791 // The per-variant equivalence pins below (fail-before-pass-after by
3792 // construction — a byte-mismatched macro arm would trip its
3793 // equivalence pin first) lock each generated constructor to its
3794 // tuple-literal peer under `PartialEq`, so every wire-up in
3795 // [`StandardLayout::verify`] on that variant produces a byte-equal
3796 // `LayoutError` to the pre-lift open-coded tuple-literal. The
3797 // cross-axis pin that follows (non-default `:nome`) routes the sole
3798 // constructor input axis through its declared accessor, so the fold
3799 // does not silently collapse onto the fixture default `:nome`.
3800
3801 fn layout_nome_only_ctor_fixture() -> Caixa {
3802 caixa(CaixaKind::Biblioteca)
3803 }
3804
3805 // Same rationale as `assert_violation_ctor_matches` / `assert_slot_
3806 // kind_ctor_matches` above: the helper terminates on the equality
3807 // check, so the owned-arg lint's general API-shape target does not
3808 // apply.
3809 #[allow(clippy::needless_pass_by_value)]
3810 fn assert_nome_only_ctor_matches(actual: LayoutError, expected: LayoutError) {
3811 assert_eq!(
3812 actual, expected,
3813 "generated constructor must produce byte-equal LayoutError to open-coded tuple-literal wrap",
3814 );
3815 }
3816
3817 #[test]
3818 fn binario_without_exe_ctor_matches_tuple_literal_wrap() {
3819 let c = layout_nome_only_ctor_fixture();
3820 assert_nome_only_ctor_matches(
3821 LayoutError::binario_without_exe(&c),
3822 LayoutError::BinarioWithoutExe(c.nome().to_string()),
3823 );
3824 }
3825
3826 #[test]
3827 fn servico_without_servicos_ctor_matches_tuple_literal_wrap() {
3828 let c = layout_nome_only_ctor_fixture();
3829 assert_nome_only_ctor_matches(
3830 LayoutError::servico_without_servicos(&c),
3831 LayoutError::ServicoWithoutServicos(c.nome().to_string()),
3832 );
3833 }
3834
3835 #[test]
3836 fn missing_ci_ctor_matches_tuple_literal_wrap() {
3837 let c = layout_nome_only_ctor_fixture();
3838 assert_nome_only_ctor_matches(
3839 LayoutError::missing_ci(&c),
3840 LayoutError::MissingCi(c.nome().to_string()),
3841 );
3842 }
3843
3844 #[test]
3845 fn supervisor_owns_code_ctor_matches_tuple_literal_wrap() {
3846 let c = layout_nome_only_ctor_fixture();
3847 assert_nome_only_ctor_matches(
3848 LayoutError::supervisor_owns_code(&c),
3849 LayoutError::SupervisorOwnsCode(c.nome().to_string()),
3850 );
3851 }
3852
3853 #[test]
3854 fn aplicacao_owns_code_ctor_matches_tuple_literal_wrap() {
3855 let c = layout_nome_only_ctor_fixture();
3856 assert_nome_only_ctor_matches(
3857 LayoutError::aplicacao_owns_code(&c),
3858 LayoutError::AplicacaoOwnsCode(c.nome().to_string()),
3859 );
3860 }
3861
3862 #[test]
3863 fn acao_owns_code_ctor_matches_tuple_literal_wrap() {
3864 let c = layout_nome_only_ctor_fixture();
3865 assert_nome_only_ctor_matches(
3866 LayoutError::acao_owns_code(&c),
3867 LayoutError::AcaoOwnsCode(c.nome().to_string()),
3868 );
3869 }
3870
3871 #[test]
3872 fn nome_only_ctor_routes_caixa_through_nome_accessor() {
3873 // Pin the fold's `caixa.nome().to_string()` sole-field construction
3874 // against a non-default `:nome` — the accessor threads the caller's
3875 // `:nome` verbatim into the tuple-variant, so the fold does not
3876 // silently collapse onto the default `"demo"` fixture nome. Peer of
3877 // the sibling `violation_ctor_routes_caixa_prefix_through_nome_
3878 // accessor` / `slot_kind_ctor_routes_caixa_prefix_through_nome_
3879 // accessor` pins on the `{ caixa, issue }` / `{ caixa, kind,
3880 // slots }` envelopes; extended here onto the sixth
3881 // `<Variant>(String)` envelope so every LayoutError-shape ctor
3882 // family guarantees the `:nome`-derived-caixa slot routes through
3883 // [`Caixa::nome`] rather than a hard-coded string. Sweeps the six
3884 // ctors in the [`layout_nome_only_ctors!`] macro so the pin covers
3885 // every generated arm.
3886 let mut c = caixa(CaixaKind::Biblioteca);
3887 c.nome = "alt-nome".into();
3888 assert_eq!(
3889 LayoutError::binario_without_exe(&c),
3890 LayoutError::BinarioWithoutExe("alt-nome".to_string()),
3891 );
3892 assert_eq!(
3893 LayoutError::servico_without_servicos(&c),
3894 LayoutError::ServicoWithoutServicos("alt-nome".to_string()),
3895 );
3896 assert_eq!(
3897 LayoutError::missing_ci(&c),
3898 LayoutError::MissingCi("alt-nome".to_string()),
3899 );
3900 assert_eq!(
3901 LayoutError::supervisor_owns_code(&c),
3902 LayoutError::SupervisorOwnsCode("alt-nome".to_string()),
3903 );
3904 assert_eq!(
3905 LayoutError::aplicacao_owns_code(&c),
3906 LayoutError::AplicacaoOwnsCode("alt-nome".to_string()),
3907 );
3908 assert_eq!(
3909 LayoutError::acao_owns_code(&c),
3910 LayoutError::AcaoOwnsCode("alt-nome".to_string()),
3911 );
3912 }
3913
3914 #[test]
3915 fn biblioteca_needs_default_lib_path() {
3916 let root = PathBuf::from("/tmp/x");
3917 let expect_manifest = root.join("caixa.lisp");
3918 let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
3919 let err = layout
3920 .verify(&caixa(CaixaKind::Biblioteca), &root)
3921 .unwrap_err();
3922 assert!(matches!(err, LayoutError::MissingLib { .. }));
3923 }
3924
3925 #[test]
3926 fn biblioteca_passes_when_default_lib_exists() {
3927 let root = PathBuf::from("/tmp/x");
3928 let manifest = root.join("caixa.lisp");
3929 let default_lib = root.join("lib").join("demo.lisp");
3930 let layout =
3931 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3932 layout
3933 .verify(&caixa(CaixaKind::Biblioteca), &root)
3934 .expect("should pass");
3935 }
3936
3937 #[test]
3938 fn binario_without_exe_errors() {
3939 let root = PathBuf::from("/tmp/x");
3940 let manifest = root.join("caixa.lisp");
3941 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
3942 let err = layout
3943 .verify(&caixa(CaixaKind::Binario), &root)
3944 .unwrap_err();
3945 assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
3946 }
3947
3948 #[test]
3949 fn exe_outside_dir_errors() {
3950 // A relative entry that lives under the caixa root but *not*
3951 // under `exe/` — the canonical case the `starts_with(exe_dir)`
3952 // fence catches. The prior parent-escape shape this test used
3953 // (`"../sibling/tool"`) is now caught at validate time by
3954 // [`Caixa::validate_code_paths`] with the narrower
3955 // [`crate::ManifestError::CodePathParentEscape`] diagnostic
3956 // (see the layout-level integration pin
3957 // `code_path_violation_on_parent_escape_fires_before_existence_check`),
3958 // so this fence pin uses a non-`..` non-absolute shape outside
3959 // `exe/` to preserve coverage of the ExeOutsideDir surface.
3960 let root = PathBuf::from("/tmp/x");
3961 let manifest = root.join("caixa.lisp");
3962 let outside = root.join("lib/tool");
3963 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
3964 let mut c = caixa(CaixaKind::Binario);
3965 c.exe = vec!["lib/tool".into()];
3966 let err = layout.verify(&c, &root).unwrap_err();
3967 assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
3968 }
3969
3970 // ── code-path shape gate (lifted to layout-level verify) ─────────────
3971
3972 #[test]
3973 fn code_path_violation_on_empty_bibliotecas_entry() {
3974 let root = PathBuf::from("/tmp/x");
3975 let manifest = root.join("caixa.lisp");
3976 let default_lib = root.join("lib").join("demo.lisp");
3977 let layout =
3978 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
3979 let mut c = caixa(CaixaKind::Biblioteca);
3980 c.bibliotecas = vec![String::new()];
3981 let err = layout.verify(&c, &root).unwrap_err();
3982 // The wire-up wraps `ManifestError` Display into the
3983 // CodePathViolation envelope (peer of LimitsViolation /
3984 // BehaviorViolation / UpgradeViolation), so the issue string
3985 // names the offending slot at the source.
3986 let LayoutError::CodePathViolation { caixa, issue } = err else {
3987 panic!("expected LayoutError::CodePathViolation, got {err:?}");
3988 };
3989 assert_eq!(caixa, "demo");
3990 assert!(
3991 issue.contains(":bibliotecas"),
3992 "issue must name the offending slot: {issue}",
3993 );
3994 }
3995
3996 #[test]
3997 fn code_path_violation_on_absolute_servicos_entry() {
3998 let root = PathBuf::from("/tmp/x");
3999 let manifest = root.join("caixa.lisp");
4000 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4001 let mut c = caixa(CaixaKind::Servico);
4002 c.servicos = vec!["/etc/servicos/escape.yaml".into()];
4003 let err = layout.verify(&c, &root).unwrap_err();
4004 let LayoutError::CodePathViolation { caixa, issue } = err else {
4005 panic!("expected LayoutError::CodePathViolation, got {err:?}");
4006 };
4007 assert_eq!(caixa, "demo");
4008 assert!(
4009 issue.contains(":servicos"),
4010 "issue must name the offending slot: {issue}",
4011 );
4012 assert!(
4013 issue.contains("/etc/servicos/escape.yaml"),
4014 "issue must quote the offending path: {issue}",
4015 );
4016 }
4017
4018 #[test]
4019 fn code_path_violation_on_parent_escape_fires_before_existence_check() {
4020 // The new gate runs BEFORE the existence loops, so a
4021 // parent-escaping `:exe` entry surfaces CodePathViolation
4022 // (naming `:exe` at the source) rather than the downstream
4023 // ExeOutsideDir / MissingEntry against the resolved sandbox-
4024 // escape path. Even if the resolved escape target exists
4025 // on disk (which we simulate here by claiming it does), the
4026 // shape diagnostic wins.
4027 let root = PathBuf::from("/tmp/x");
4028 let manifest = root.join("caixa.lisp");
4029 let resolved_escape = root.join("exe/../../escape.lisp");
4030 let layout =
4031 StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
4032 let mut c = caixa(CaixaKind::Binario);
4033 c.exe = vec!["exe/../../escape.lisp".into()];
4034 let err = layout.verify(&c, &root).unwrap_err();
4035 let LayoutError::CodePathViolation { caixa, issue } = err else {
4036 panic!("expected LayoutError::CodePathViolation, got {err:?}");
4037 };
4038 assert_eq!(caixa, "demo");
4039 assert!(
4040 issue.contains(":exe"),
4041 "issue must name the offending slot: {issue}",
4042 );
4043 }
4044
4045 // ── etiquetas universal-axis gate wired into verify ─────────────────
4046 //
4047 // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
4048 // the fourth universal-axis Caixa-level value-shape gate (peer of
4049 // `validate_nome` / `validate_versao` / `validate_deps` /
4050 // `validate_code_paths`), wired before the kind-coherence gates so
4051 // a structurally-invalid `:etiquetas` entry on any kind surfaces
4052 // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
4053 // the source rather than silently rendering as `keywords: [""]`
4054 // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
4055 // collect) or silently dedup'ing at chart render (every kind).
4056 // Until this wire-up landed `:etiquetas` had no shape gate at any
4057 // layer — the registry-search-tag axis was the largest universal
4058 // authoring surface on the typed Caixa surface with no validate
4059 // discipline.
4060 //
4061 // Same per-axis `*Violation { caixa, issue }` envelope every peer
4062 // per-axis wrap exposes; the wire-up runs after `validate_deps`
4063 // (universal axis ordering: `:nome` → `:versao` → `:deps` →
4064 // `:etiquetas`) and before every kind-coherence gate
4065 // (`:etiquetas` is universal so its shape diagnostic is more
4066 // fundamental than the partition-on-kind diagnostics).
4067
4068 #[test]
4069 fn etiquetas_violation_on_empty_entry() {
4070 // Canonical paste-from-blank-doc footgun on every kind. The
4071 // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
4072 // Display through verbatim, so the issue string names the
4073 // offending `:etiquetas` axis at the source — the author can
4074 // grep their caixa.lisp for `:etiquetas` and fix the empty
4075 // entry in one edit. Mirrors the peer
4076 // `code_path_violation_on_empty_bibliotecas_entry` shape
4077 // (b868442) on the `:bibliotecas` axis.
4078 let root = PathBuf::from("/tmp/x");
4079 let manifest = root.join("caixa.lisp");
4080 let default_lib = root.join("lib").join("demo.lisp");
4081 let layout =
4082 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4083 let mut c = caixa(CaixaKind::Biblioteca);
4084 c.etiquetas = vec![String::new()];
4085 let err = layout.verify(&c, &root).unwrap_err();
4086 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4087 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4088 };
4089 assert_eq!(caixa, "demo");
4090 assert!(
4091 issue.contains(":etiquetas"),
4092 "issue must name the offending slot: {issue}",
4093 );
4094 }
4095
4096 #[test]
4097 fn etiquetas_violation_on_duplicate_entry() {
4098 // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
4099 // up the duplicate was silently dedup'd by caixa-helm's
4100 // `BTreeSet` collect at chart render — a "second wins / one
4101 // silently disappears" shape. The wrap envelope names the
4102 // offending tag verbatim through the inner
4103 // [`ManifestError::EtiquetaDuplicate`]'s Display.
4104 let root = PathBuf::from("/tmp/x");
4105 let manifest = root.join("caixa.lisp");
4106 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4107 let mut c = caixa(CaixaKind::Servico);
4108 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4109 c.etiquetas = vec!["demo".into(), "demo".into()];
4110 // The servicos path doesn't exist in this fixture, but the
4111 // `:etiquetas` gate fires before the existence loop (universal
4112 // axis dominates kind-specific existence checks). Wire is
4113 // intact iff the wrap envelope surfaces first.
4114 let err = layout.verify(&c, &root).unwrap_err();
4115 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4116 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4117 };
4118 assert_eq!(caixa, "demo");
4119 assert!(
4120 issue.contains("demo"),
4121 "issue must quote the offending tag: {issue}",
4122 );
4123 }
4124
4125 #[test]
4126 fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
4127 // Cross-axis precedence pin: a Biblioteca with malformed
4128 // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
4129 // the universal `:etiquetas` diagnostic first, not the
4130 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4131 // `:etiquetas` is universal (every kind owns the slot), so its
4132 // shape diagnostic is more fundamental than the partition-on-
4133 // kind diagnostic. Mirrors the peer
4134 // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
4135 // the universal `:deps` axis vs the same kind-coherence gates.
4136 let root = PathBuf::from("/tmp/x");
4137 let manifest = root.join("caixa.lisp");
4138 let default_lib = root.join("lib").join("demo.lisp");
4139 let layout =
4140 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4141 let mut c = caixa(CaixaKind::Biblioteca);
4142 c.etiquetas = vec![String::new()];
4143 c.membros = vec![crate::aplicacao::Membro {
4144 caixa: "x".into(),
4145 versao: "^0.1".into(),
4146 }];
4147 let err = layout.verify(&c, &root).unwrap_err();
4148 assert!(
4149 matches!(err, LayoutError::EtiquetasViolation { .. }),
4150 "got {err:?}",
4151 );
4152 }
4153
4154 #[test]
4155 fn etiquetas_violation_fires_after_deps_violation() {
4156 // Cross-axis precedence pin (inside the universal-axis trio):
4157 // a caixa with both a malformed `:deps` entry *and* a malformed
4158 // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
4159 // is the third universal axis in declaration order
4160 // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
4161 // in `verify`. Mirrors the peer
4162 // `nome_violation_fires_before_versao_violation` shape on the
4163 // identity-axis pair.
4164 let root = PathBuf::from("/tmp/x");
4165 let manifest = root.join("caixa.lisp");
4166 let default_lib = root.join("lib").join("demo.lisp");
4167 let layout =
4168 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4169 let mut c = caixa(CaixaKind::Biblioteca);
4170 c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
4171 c.etiquetas = vec![String::new()];
4172 let err = layout.verify(&c, &root).unwrap_err();
4173 assert!(
4174 matches!(err, LayoutError::DepsViolation { .. }),
4175 "got {err:?}",
4176 );
4177 }
4178
4179 #[test]
4180 fn etiquetas_violation_accepts_canonical_template() {
4181 // Positive control sanity pin: the canonical `Caixa::template`
4182 // shape (`:etiquetas ()` — empty list) passes the gate
4183 // trivially. Mirrors the peer
4184 // `validate_code_paths_accepts_canonical_template` pin.
4185 let root = PathBuf::from("/tmp/x");
4186 let manifest = root.join("caixa.lisp");
4187 let default_lib = root.join("lib").join("demo.lisp");
4188 let layout =
4189 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4190 let c = caixa(CaixaKind::Biblioteca);
4191 layout.verify(&c, &root).expect("template must pass");
4192 }
4193
4194 #[test]
4195 fn etiquetas_violation_on_non_chart_keyword_shape() {
4196 // Canonical CSV-list-separator-confusion footgun: the author
4197 // confused the CSV-style separator with the `:etiquetas` list
4198 // grammar. The shape gate fires past the empty + duplicate
4199 // arms via [`Caixa::validate_etiquetas`]'s new
4200 // `is_chart_keyword_shape` cascade, and the layout envelope
4201 // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
4202 // verbatim — the issue string names both the offending slot
4203 // and the offending value (debug-escaped). Peer with the
4204 // `autores_violation_on_non_chart_maintainer_shape` pin on
4205 // the sibling universal-axis `Vec<String>` surface — the
4206 // second layout pin on the Vec<String> per-entry shape
4207 // cascade.
4208 let root = PathBuf::from("/tmp/x");
4209 let manifest = root.join("caixa.lisp");
4210 let default_lib = root.join("lib").join("demo.lisp");
4211 let layout =
4212 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4213 let mut c = caixa(CaixaKind::Biblioteca);
4214 c.etiquetas = vec!["mesh,http,grpc".into()];
4215 let err = layout.verify(&c, &root).unwrap_err();
4216 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4217 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4218 };
4219 assert_eq!(caixa, "demo");
4220 assert!(
4221 issue.contains(":etiquetas"),
4222 "issue must name the offending slot: {issue}",
4223 );
4224 assert!(
4225 issue.contains("mesh,http,grpc"),
4226 "issue must quote the offending value: {issue}",
4227 );
4228 }
4229
4230 // ── autores universal-axis gate wired into verify ───────────────────
4231 //
4232 // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
4233 // the fifth universal-axis Caixa-level value-shape gate (peer of
4234 // `validate_nome` / `validate_versao` / `validate_deps` /
4235 // `validate_etiquetas` / `validate_code_paths`), wired immediately
4236 // after `validate_etiquetas` so the two Vec-shaped universal
4237 // metadata axes sit adjacent in the cascade. Until this wire-up
4238 // landed `:autores` had no shape gate at any layer — the
4239 // maintainer-axis was the second largest universal authoring
4240 // surface on the typed Caixa surface with no validate discipline,
4241 // and unlike `:etiquetas` (caixa-helm dedups the rendered
4242 // `keywords:` array via `BTreeSet` collect at chart render),
4243 // `maintainers:` has *no* renderer-side dedup, so duplicate
4244 // `:autores` entries render verbatim as two identical
4245 // `Maintainer { name, email: None }` records — a strictly worse
4246 // footgun than the peer `:etiquetas` shape.
4247
4248 #[test]
4249 fn autores_violation_on_empty_entry() {
4250 // Canonical paste-from-blank-doc footgun on every kind. The
4251 // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
4252 // through verbatim, so the issue string names the offending
4253 // `:autores` axis at the source — the author can grep their
4254 // caixa.lisp for `:autores` and fix the empty entry in one
4255 // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
4256 // shape (360a499) on the `:etiquetas` axis.
4257 let root = PathBuf::from("/tmp/x");
4258 let manifest = root.join("caixa.lisp");
4259 let default_lib = root.join("lib").join("demo.lisp");
4260 let layout =
4261 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4262 let mut c = caixa(CaixaKind::Biblioteca);
4263 c.autores = vec![String::new()];
4264 let err = layout.verify(&c, &root).unwrap_err();
4265 let LayoutError::AutoresViolation { caixa, issue } = err else {
4266 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4267 };
4268 assert_eq!(caixa, "demo");
4269 assert!(
4270 issue.contains(":autores"),
4271 "issue must name the offending slot: {issue}",
4272 );
4273 }
4274
4275 #[test]
4276 fn autores_violation_on_duplicate_entry() {
4277 // Canonical copy-paste-the-wrong-author footgun. Unlike the
4278 // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
4279 // `BTreeSet` collect at chart render), `:autores` duplicates
4280 // stack verbatim in the rendered `maintainers:` — the gate
4281 // closes the footgun at validate time before any renderer
4282 // sees it. The wrap envelope names the offending author
4283 // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
4284 // Display.
4285 let root = PathBuf::from("/tmp/x");
4286 let manifest = root.join("caixa.lisp");
4287 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4288 let mut c = caixa(CaixaKind::Servico);
4289 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4290 c.autores = vec!["pleme-io".into(), "pleme-io".into()];
4291 // The servicos path doesn't exist in this fixture, but the
4292 // `:autores` gate fires before the existence loop (universal
4293 // axis dominates kind-specific existence checks). Wire is
4294 // intact iff the wrap envelope surfaces first.
4295 let err = layout.verify(&c, &root).unwrap_err();
4296 let LayoutError::AutoresViolation { caixa, issue } = err else {
4297 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4298 };
4299 assert_eq!(caixa, "demo");
4300 assert!(
4301 issue.contains("pleme-io"),
4302 "issue must quote the offending author: {issue}",
4303 );
4304 }
4305
4306 #[test]
4307 fn autores_violation_fires_before_kind_coherence_mesh_slot() {
4308 // Cross-axis precedence pin: a Biblioteca with malformed
4309 // `:autores` *and* declared mesh slots (`:membros`) surfaces
4310 // the universal `:autores` diagnostic first, not the
4311 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4312 // `:autores` is universal (every kind owns the slot), so its
4313 // shape diagnostic is more fundamental than the partition-on-
4314 // kind diagnostic. Mirrors the peer
4315 // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
4316 // pin (360a499) on the `:etiquetas` axis vs the same kind-
4317 // coherence gates.
4318 let root = PathBuf::from("/tmp/x");
4319 let manifest = root.join("caixa.lisp");
4320 let default_lib = root.join("lib").join("demo.lisp");
4321 let layout =
4322 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4323 let mut c = caixa(CaixaKind::Biblioteca);
4324 c.autores = vec![String::new()];
4325 c.membros = vec![crate::aplicacao::Membro {
4326 caixa: "x".into(),
4327 versao: "^0.1".into(),
4328 }];
4329 let err = layout.verify(&c, &root).unwrap_err();
4330 assert!(
4331 matches!(err, LayoutError::AutoresViolation { .. }),
4332 "got {err:?}",
4333 );
4334 }
4335
4336 #[test]
4337 fn autores_violation_fires_after_etiquetas_violation() {
4338 // Cross-axis precedence pin (inside the Vec-shaped universal
4339 // metadata pair): a caixa with both a malformed `:etiquetas`
4340 // entry *and* a malformed `:autores` entry surfaces
4341 // `EtiquetasViolation` first — `:etiquetas` is the fourth
4342 // universal axis in the cascade and runs before `:autores`,
4343 // peer with the canonical identity-axis-first cascade the
4344 // peer gates establish. Mirrors the peer
4345 // `etiquetas_violation_fires_after_deps_violation` precedence
4346 // pin (360a499) on the dep-axis-before-tag-axis pair.
4347 let root = PathBuf::from("/tmp/x");
4348 let manifest = root.join("caixa.lisp");
4349 let default_lib = root.join("lib").join("demo.lisp");
4350 let layout =
4351 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4352 let mut c = caixa(CaixaKind::Biblioteca);
4353 c.etiquetas = vec![String::new()];
4354 c.autores = vec![String::new()];
4355 let err = layout.verify(&c, &root).unwrap_err();
4356 assert!(
4357 matches!(err, LayoutError::EtiquetasViolation { .. }),
4358 "got {err:?}",
4359 );
4360 }
4361
4362 #[test]
4363 fn autores_violation_accepts_canonical_template() {
4364 // Positive control sanity pin: the canonical `Caixa::template`
4365 // shape (`:autores ()` — empty list) passes the gate trivially.
4366 // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
4367 // pin (360a499).
4368 let root = PathBuf::from("/tmp/x");
4369 let manifest = root.join("caixa.lisp");
4370 let default_lib = root.join("lib").join("demo.lisp");
4371 let layout =
4372 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4373 let c = caixa(CaixaKind::Biblioteca);
4374 layout.verify(&c, &root).expect("template must pass");
4375 }
4376
4377 #[test]
4378 fn autores_violation_on_non_chart_maintainer_shape() {
4379 // Canonical paste-from-multiline-doc footgun: the author
4380 // pasted a multi-line block of author records into one
4381 // `:autores` entry instead of splitting into one entry per
4382 // author. The shape gate fires past the empty + duplicate arms
4383 // via [`Caixa::validate_autores`]'s new
4384 // `is_chart_maintainer_name_shape` cascade, and the layout
4385 // envelope wraps [`ManifestError::AutorInvalid`]'s Display
4386 // through verbatim — the issue string names both the offending
4387 // slot and the offending value (debug-escaped). Peer with the
4388 // `descricao_violation_on_non_chart_shape` pin on the sibling
4389 // universal-axis `Option<String>` surface and the
4390 // `licenca_violation_on_non_spdx_shape` /
4391 // `edicao_violation_on_non_year_shape` peers — and the first
4392 // layout pin on the Vec<String> per-entry shape cascade.
4393 let root = PathBuf::from("/tmp/x");
4394 let manifest = root.join("caixa.lisp");
4395 let default_lib = root.join("lib").join("demo.lisp");
4396 let layout =
4397 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4398 let mut c = caixa(CaixaKind::Biblioteca);
4399 c.autores = vec!["alice\nbob".into()];
4400 let err = layout.verify(&c, &root).unwrap_err();
4401 let LayoutError::AutoresViolation { caixa, issue } = err else {
4402 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4403 };
4404 assert_eq!(caixa, "demo");
4405 assert!(
4406 issue.contains(":autores"),
4407 "issue must name the offending slot: {issue}",
4408 );
4409 assert!(
4410 issue.contains("alice\\nbob"),
4411 "issue must quote the offending value (debug-escaped): {issue}",
4412 );
4413 }
4414
4415 // ── repositorio universal-axis gate wired into verify ────────────────
4416 //
4417 // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
4418 // the sixth universal-axis Caixa-level value-shape gate (peer of
4419 // `validate_nome` / `validate_versao` / `validate_deps` /
4420 // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
4421 // wired immediately after `validate_autores` so the universal
4422 // git-URL axis sits adjacent to the two Vec-shaped universal
4423 // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
4424 // this wire-up landed `:repositorio` had no shape gate at any
4425 // layer — the universal git-shaped homepage axis was the third
4426 // largest universal authoring surface on the typed Caixa with no
4427 // validate discipline, routing the same string through two
4428 // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
4429 // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
4430 // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
4431 // a `Some("")` silently passed every fallback and rendered as an
4432 // empty URL in both consumers, breaking at `helm template` /
4433 // FluxCD reconcile time far from the source `caixa.lisp`. The
4434 // gate closes the divergence and makes the two `git URL`-shaped
4435 // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
4436 // :repo` peer routed through the same shared
4437 // `crate::render::is_git_repo_url` predicate) structurally
4438 // equivalent by construction.
4439
4440 #[test]
4441 fn repositorio_violation_on_empty_some() {
4442 // Canonical paste-from-blank-doc footgun on every kind. The
4443 // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
4444 // Display through verbatim, so the issue string names the
4445 // offending `:repositorio` axis at the source — the author
4446 // can grep their caixa.lisp for `:repositorio ""` and fix the
4447 // empty value in one edit. Mirrors the peer
4448 // `autores_violation_on_empty_entry` shape (86c769b) on the
4449 // `:autores` axis.
4450 let root = PathBuf::from("/tmp/x");
4451 let manifest = root.join("caixa.lisp");
4452 let default_lib = root.join("lib").join("demo.lisp");
4453 let layout =
4454 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4455 let mut c = caixa(CaixaKind::Biblioteca);
4456 c.repositorio = Some(String::new());
4457 let err = layout.verify(&c, &root).unwrap_err();
4458 let LayoutError::RepositorioViolation { caixa, issue } = err else {
4459 panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4460 };
4461 assert_eq!(caixa, "demo");
4462 assert!(
4463 issue.contains(":repositorio"),
4464 "issue must name the offending slot: {issue}",
4465 );
4466 }
4467
4468 #[test]
4469 fn repositorio_violation_on_malformed_shape() {
4470 // Canonical CLI-argument-injection footgun: a leading `-`
4471 // value (`-upload-pack=evil`) escapes the `git clone <repo>`
4472 // subprocess argument boundary at clone time. The shared
4473 // `is_git_repo_url` predicate — the same parser the peer
4474 // `:deps :fonte :repo` axis routes through via
4475 // `DepSource::validate` — refuses every leading-`-` shape at
4476 // validate time. The wrap envelope names the offending value
4477 // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
4478 // Display.
4479 let root = PathBuf::from("/tmp/x");
4480 let manifest = root.join("caixa.lisp");
4481 let default_lib = root.join("lib").join("demo.lisp");
4482 let layout =
4483 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4484 let mut c = caixa(CaixaKind::Biblioteca);
4485 c.repositorio = Some("-upload-pack=evil".into());
4486 let err = layout.verify(&c, &root).unwrap_err();
4487 let LayoutError::RepositorioViolation { caixa, issue } = err else {
4488 panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4489 };
4490 assert_eq!(caixa, "demo");
4491 assert!(
4492 issue.contains("-upload-pack=evil"),
4493 "issue must quote the offending value: {issue}",
4494 );
4495 }
4496
4497 #[test]
4498 fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
4499 // Cross-axis precedence pin: a Biblioteca with malformed
4500 // `:repositorio` *and* declared mesh slots (`:membros`)
4501 // surfaces the universal `:repositorio` diagnostic first, not
4502 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4503 // `:repositorio` is universal (every kind owns the slot), so
4504 // its shape diagnostic is more fundamental than the
4505 // partition-on-kind diagnostic. Mirrors the peer
4506 // `autores_violation_fires_before_kind_coherence_mesh_slot`
4507 // pin (86c769b) on the `:autores` axis vs the same
4508 // kind-coherence gates.
4509 let root = PathBuf::from("/tmp/x");
4510 let manifest = root.join("caixa.lisp");
4511 let default_lib = root.join("lib").join("demo.lisp");
4512 let layout =
4513 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4514 let mut c = caixa(CaixaKind::Biblioteca);
4515 c.repositorio = Some(String::new());
4516 c.membros = vec![crate::aplicacao::Membro {
4517 caixa: "x".into(),
4518 versao: "^0.1".into(),
4519 }];
4520 let err = layout.verify(&c, &root).unwrap_err();
4521 assert!(
4522 matches!(err, LayoutError::RepositorioViolation { .. }),
4523 "got {err:?}",
4524 );
4525 }
4526
4527 #[test]
4528 fn repositorio_violation_fires_after_autores_violation() {
4529 // Cross-axis precedence pin (inside the universal metadata
4530 // trio): a caixa with both a malformed `:autores` entry *and*
4531 // a malformed `:repositorio` value surfaces `AutoresViolation`
4532 // first — `:autores` is the fifth universal axis in the
4533 // cascade and runs before `:repositorio`, peer with the
4534 // canonical identity-axis-first cascade the peer gates
4535 // establish. Mirrors the peer
4536 // `autores_violation_fires_after_etiquetas_violation`
4537 // precedence pin (86c769b) on the tag-axis-before-author-axis
4538 // pair.
4539 let root = PathBuf::from("/tmp/x");
4540 let manifest = root.join("caixa.lisp");
4541 let default_lib = root.join("lib").join("demo.lisp");
4542 let layout =
4543 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4544 let mut c = caixa(CaixaKind::Biblioteca);
4545 c.autores = vec![String::new()];
4546 c.repositorio = Some(String::new());
4547 let err = layout.verify(&c, &root).unwrap_err();
4548 assert!(
4549 matches!(err, LayoutError::AutoresViolation { .. }),
4550 "got {err:?}",
4551 );
4552 }
4553
4554 #[test]
4555 fn repositorio_violation_accepts_canonical_template() {
4556 // Positive control sanity pin: the canonical `Caixa::template`
4557 // shape (omits `:repositorio` entirely → `None` on the typed
4558 // surface) passes the gate trivially — the gate is a no-op
4559 // when the author didn't author a value. Mirrors the peer
4560 // `autores_violation_accepts_canonical_template` pin (86c769b).
4561 let root = PathBuf::from("/tmp/x");
4562 let manifest = root.join("caixa.lisp");
4563 let default_lib = root.join("lib").join("demo.lisp");
4564 let layout =
4565 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4566 let c = caixa(CaixaKind::Biblioteca);
4567 layout.verify(&c, &root).expect("template must pass");
4568 }
4569
4570 #[test]
4571 fn repositorio_violation_accepts_canonical_github_shorthand() {
4572 // Positive control pin on the canonical pleme-io `:repositorio`
4573 // shape: the `github:org/repo` shorthand the README quickstart
4574 // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
4575 // all use passes the gate end-to-end. Closes the structural
4576 // equivalence between this surface and the peer `:deps :fonte
4577 // :repo` axis — both consume `crate::render::is_git_repo_url`
4578 // and both must agree on the same accepted shape set.
4579 let root = PathBuf::from("/tmp/x");
4580 let manifest = root.join("caixa.lisp");
4581 let default_lib = root.join("lib").join("demo.lisp");
4582 let layout =
4583 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4584 let mut c = caixa(CaixaKind::Biblioteca);
4585 c.repositorio = Some("github:pleme-io/hello-rio".into());
4586 layout.verify(&c, &root).expect("canonical shape must pass");
4587 }
4588
4589 // ── descricao universal-axis gate wired into verify ──────────────────
4590 //
4591 // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
4592 // the seventh universal-axis Caixa-level value-shape gate (peer of
4593 // `validate_nome` / `validate_versao` / `validate_deps` /
4594 // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
4595 // `validate_code_paths`), wired immediately after `validate_repositorio`
4596 // so the universal free-form-prose axis sits adjacent to the
4597 // universal git-URL axis in the cascade. Until this wire-up landed
4598 // `:descricao` had no shape gate at any layer — the empty
4599 // `Some("")` silently passed both `caixa-helm` consumers'
4600 // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
4601 // `None`) and rendered as `Chart.yaml description: ""` plus a
4602 // blank `README.md` header, breaking at `helm lint` time
4603 // (`WARNING [chart.metadata.description]: description is required`
4604 // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
4605 // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
4606 // peer `:repositorio` gate (577b0a9) closed, on the universal
4607 // free-form-prose summary axis.
4608
4609 #[test]
4610 fn descricao_violation_on_empty_some() {
4611 // Canonical paste-from-blank-doc footgun on every kind. The
4612 // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
4613 // Display through verbatim, so the issue string names the
4614 // offending `:descricao` axis at the source — the author can
4615 // grep their caixa.lisp for `:descricao ""` and fix the empty
4616 // value in one edit. Mirrors the peer
4617 // `repositorio_violation_on_empty_some` shape (577b0a9) on
4618 // the sibling `Option<String>` `:repositorio` axis.
4619 let root = PathBuf::from("/tmp/x");
4620 let manifest = root.join("caixa.lisp");
4621 let default_lib = root.join("lib").join("demo.lisp");
4622 let layout =
4623 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4624 let mut c = caixa(CaixaKind::Biblioteca);
4625 c.descricao = Some(String::new());
4626 let err = layout.verify(&c, &root).unwrap_err();
4627 let LayoutError::DescricaoViolation { caixa, issue } = err else {
4628 panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4629 };
4630 assert_eq!(caixa, "demo");
4631 assert!(
4632 issue.contains(":descricao"),
4633 "issue must name the offending slot: {issue}",
4634 );
4635 }
4636
4637 #[test]
4638 fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
4639 // Cross-axis precedence pin: a Biblioteca with empty
4640 // `:descricao` *and* declared mesh slots (`:membros`)
4641 // surfaces the universal `:descricao` diagnostic first, not
4642 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4643 // `:descricao` is universal (every kind owns the slot), so
4644 // its shape diagnostic is more fundamental than the
4645 // partition-on-kind diagnostic. Mirrors the peer
4646 // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
4647 // pin (577b0a9) on the `:repositorio` axis vs the same
4648 // kind-coherence gates.
4649 let root = PathBuf::from("/tmp/x");
4650 let manifest = root.join("caixa.lisp");
4651 let default_lib = root.join("lib").join("demo.lisp");
4652 let layout =
4653 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4654 let mut c = caixa(CaixaKind::Biblioteca);
4655 c.descricao = Some(String::new());
4656 c.membros = vec![crate::aplicacao::Membro {
4657 caixa: "x".into(),
4658 versao: "^0.1".into(),
4659 }];
4660 let err = layout.verify(&c, &root).unwrap_err();
4661 assert!(
4662 matches!(err, LayoutError::DescricaoViolation { .. }),
4663 "got {err:?}",
4664 );
4665 }
4666
4667 #[test]
4668 fn descricao_violation_fires_after_repositorio_violation() {
4669 // Cross-axis precedence pin (inside the universal metadata
4670 // cascade): a caixa with both a malformed `:repositorio` *and*
4671 // an empty `:descricao` surfaces `RepositorioViolation`
4672 // first — `:repositorio` is the sixth universal axis in the
4673 // cascade and runs before `:descricao`, peer with the
4674 // canonical identity-axis-first cascade the peer gates
4675 // establish. Mirrors the peer
4676 // `repositorio_violation_fires_after_autores_violation`
4677 // precedence pin (577b0a9) on the autores-axis-before-
4678 // repositorio-axis pair.
4679 let root = PathBuf::from("/tmp/x");
4680 let manifest = root.join("caixa.lisp");
4681 let default_lib = root.join("lib").join("demo.lisp");
4682 let layout =
4683 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4684 let mut c = caixa(CaixaKind::Biblioteca);
4685 c.repositorio = Some(String::new());
4686 c.descricao = Some(String::new());
4687 let err = layout.verify(&c, &root).unwrap_err();
4688 assert!(
4689 matches!(err, LayoutError::RepositorioViolation { .. }),
4690 "got {err:?}",
4691 );
4692 }
4693
4694 #[test]
4695 fn descricao_violation_accepts_none() {
4696 // Positive control sanity pin: a caixa that omits
4697 // `:descricao` entirely (the canonical `Caixa::template` shape
4698 // carries `Some("FIXME — describe this caixa")`, but the
4699 // layout-test fixture defaults to `None`) passes the gate
4700 // trivially — the gate is a no-op when the author didn't
4701 // author a value. Mirrors the peer
4702 // `repositorio_violation_accepts_canonical_template` pin
4703 // (577b0a9).
4704 let root = PathBuf::from("/tmp/x");
4705 let manifest = root.join("caixa.lisp");
4706 let default_lib = root.join("lib").join("demo.lisp");
4707 let layout =
4708 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4709 let c = caixa(CaixaKind::Biblioteca);
4710 layout.verify(&c, &root).expect("None must pass");
4711 }
4712
4713 #[test]
4714 fn descricao_violation_accepts_canonical_summary() {
4715 // Positive control pin on the canonical pleme-io `:descricao`
4716 // shape: a short free-form prose summary the `caixa-helm` /
4717 // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
4718 // gate end-to-end.
4719 let root = PathBuf::from("/tmp/x");
4720 let manifest = root.join("caixa.lisp");
4721 let default_lib = root.join("lib").join("demo.lisp");
4722 let layout =
4723 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4724 let mut c = caixa(CaixaKind::Biblioteca);
4725 c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
4726 layout
4727 .verify(&c, &root)
4728 .expect("canonical summary must pass");
4729 }
4730
4731 #[test]
4732 fn descricao_violation_on_non_chart_shape() {
4733 // Shape-predicate wire-up pin: a malformed `:descricao` value
4734 // that's a non-empty `Some(s)` but carries a paste-from-
4735 // multiline-doc embedded newline surfaces the
4736 // `DescricaoViolation` envelope via the manifest-layer
4737 // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
4738 // `descricao_violation_on_empty_some` shape on the empty arm
4739 // of the same axis and the peer
4740 // `licenca_violation_on_non_spdx_shape` shape on the sibling
4741 // `:licenca` axis. Until this gate landed a value like
4742 // `"Checkout\nflow."` (an embedded newline) or `"Checkout
4743 // flow. "` (a trailing whitespace) silently passed
4744 // `StandardLayout::verify` and landed in the rendered
4745 // Chart.yaml `description:` field as a YAML-illegal
4746 // multi-line scalar or a silently-trimmed whitespace
4747 // round-trip far from the source caixa.lisp.
4748 let root = PathBuf::from("/tmp/x");
4749 let manifest = root.join("caixa.lisp");
4750 let default_lib = root.join("lib").join("demo.lisp");
4751 let layout =
4752 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4753 let mut c = caixa(CaixaKind::Biblioteca);
4754 c.descricao = Some("Checkout\nflow.".into());
4755 let err = layout.verify(&c, &root).unwrap_err();
4756 let LayoutError::DescricaoViolation { caixa, issue } = err else {
4757 panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4758 };
4759 assert_eq!(caixa, "demo");
4760 assert!(
4761 issue.contains(":descricao"),
4762 "issue must name the offending slot: {issue}",
4763 );
4764 // The wrapped `ManifestError::DescricaoInvalid` Display uses
4765 // `{descricao:?}` (Debug) so the embedded newline surfaces
4766 // debug-escaped as `\n` in the issue string.
4767 assert!(
4768 issue.contains("Checkout\\nflow."),
4769 "issue must quote the offending value (debug-escaped): {issue}",
4770 );
4771 }
4772
4773 // ── :licenca empty-Some shape wired into verify (universal axis) ──
4774 //
4775 // Until this wire-up landed `Caixa::validate_licenca` did not
4776 // exist — the universal SPDX-shaped license-expression axis had
4777 // no shape gate at any layer, so an empty `Some("")` silently
4778 // passed `Caixa::from_lisp` and `StandardLayout::verify` and
4779 // landed as a bare trailing period in the rendered
4780 // `lareira-<nome>` chart's `README.md` `## License` section via
4781 // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
4782 // "MIT".into())` (which only fires on `None`) at
4783 // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
4784 // skips-`unwrap_or_else` footgun the peer `:repositorio`
4785 // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
4786 // universal license-expression axis.
4787
4788 #[test]
4789 fn licenca_violation_on_empty_some() {
4790 // Canonical paste-from-blank-doc footgun on every kind. The
4791 // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
4792 // Display through verbatim, so the issue string names the
4793 // offending `:licenca` axis at the source — the author can
4794 // grep their caixa.lisp for `:licenca ""` and fix the empty
4795 // value in one edit. Mirrors the peer
4796 // `descricao_violation_on_empty_some` shape (4e6db38) on
4797 // the sibling `Option<String>` `:licenca` axis.
4798 let root = PathBuf::from("/tmp/x");
4799 let manifest = root.join("caixa.lisp");
4800 let default_lib = root.join("lib").join("demo.lisp");
4801 let layout =
4802 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4803 let mut c = caixa(CaixaKind::Biblioteca);
4804 c.licenca = Some(String::new());
4805 let err = layout.verify(&c, &root).unwrap_err();
4806 let LayoutError::LicencaViolation { caixa, issue } = err else {
4807 panic!("expected LayoutError::LicencaViolation, got {err:?}");
4808 };
4809 assert_eq!(caixa, "demo");
4810 assert!(
4811 issue.contains(":licenca"),
4812 "issue must name the offending slot: {issue}",
4813 );
4814 }
4815
4816 #[test]
4817 fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
4818 // Cross-axis precedence pin: a Biblioteca with empty
4819 // `:licenca` *and* declared mesh slots (`:membros`)
4820 // surfaces the universal `:licenca` diagnostic first, not
4821 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4822 // `:licenca` is universal (every kind owns the slot), so
4823 // its shape diagnostic is more fundamental than the
4824 // partition-on-kind diagnostic. Mirrors the peer
4825 // `descricao_violation_fires_before_kind_coherence_mesh_slot`
4826 // pin (4e6db38) on the `:descricao` axis vs the same
4827 // kind-coherence gates.
4828 let root = PathBuf::from("/tmp/x");
4829 let manifest = root.join("caixa.lisp");
4830 let default_lib = root.join("lib").join("demo.lisp");
4831 let layout =
4832 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4833 let mut c = caixa(CaixaKind::Biblioteca);
4834 c.licenca = Some(String::new());
4835 c.membros = vec![crate::aplicacao::Membro {
4836 caixa: "x".into(),
4837 versao: "^0.1".into(),
4838 }];
4839 let err = layout.verify(&c, &root).unwrap_err();
4840 assert!(
4841 matches!(err, LayoutError::LicencaViolation { .. }),
4842 "got {err:?}",
4843 );
4844 }
4845
4846 #[test]
4847 fn licenca_violation_fires_after_descricao_violation() {
4848 // Cross-axis precedence pin (inside the universal metadata
4849 // cascade): a caixa with both an empty `:descricao` *and*
4850 // an empty `:licenca` surfaces `DescricaoViolation`
4851 // first — `:descricao` is the seventh universal axis in the
4852 // cascade and runs before `:licenca`, peer with the
4853 // canonical identity-axis-first cascade the peer gates
4854 // establish. Mirrors the peer
4855 // `descricao_violation_fires_after_repositorio_violation`
4856 // precedence pin (4e6db38) on the repositorio-axis-before-
4857 // descricao-axis pair.
4858 let root = PathBuf::from("/tmp/x");
4859 let manifest = root.join("caixa.lisp");
4860 let default_lib = root.join("lib").join("demo.lisp");
4861 let layout =
4862 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4863 let mut c = caixa(CaixaKind::Biblioteca);
4864 c.descricao = Some(String::new());
4865 c.licenca = Some(String::new());
4866 let err = layout.verify(&c, &root).unwrap_err();
4867 assert!(
4868 matches!(err, LayoutError::DescricaoViolation { .. }),
4869 "got {err:?}",
4870 );
4871 }
4872
4873 #[test]
4874 fn licenca_violation_accepts_none() {
4875 // Positive control sanity pin: a caixa that omits `:licenca`
4876 // entirely (the layout-test fixture defaults to `None`)
4877 // passes the gate trivially — the gate is a no-op when the
4878 // author didn't author a value. Mirrors the peer
4879 // `descricao_violation_accepts_none` pin (4e6db38).
4880 let root = PathBuf::from("/tmp/x");
4881 let manifest = root.join("caixa.lisp");
4882 let default_lib = root.join("lib").join("demo.lisp");
4883 let layout =
4884 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4885 let c = caixa(CaixaKind::Biblioteca);
4886 layout.verify(&c, &root).expect("None must pass");
4887 }
4888
4889 #[test]
4890 fn licenca_violation_accepts_canonical_expression() {
4891 // Positive control pin on the canonical pleme-io `:licenca`
4892 // shape: a non-empty SPDX expression the `caixa-helm` /
4893 // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
4894 // passes the gate end-to-end.
4895 let root = PathBuf::from("/tmp/x");
4896 let manifest = root.join("caixa.lisp");
4897 let default_lib = root.join("lib").join("demo.lisp");
4898 let layout =
4899 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4900 let mut c = caixa(CaixaKind::Biblioteca);
4901 c.licenca = Some("Apache-2.0 OR MIT".into());
4902 layout
4903 .verify(&c, &root)
4904 .expect("canonical SPDX expression must pass");
4905 }
4906
4907 #[test]
4908 fn licenca_violation_on_non_spdx_shape() {
4909 // Shape-predicate wire-up pin: a malformed `:licenca` value
4910 // that's a non-empty `Some(s)` but falls outside the SPDX
4911 // expression alphabet floor surfaces the `LicencaViolation`
4912 // envelope via the manifest-layer `ManifestError::LicencaInvalid`
4913 // arm. Mirrors the peer `licenca_violation_on_empty_some`
4914 // shape on the empty arm of the same axis and the peer
4915 // `edicao_violation_on_non_year_shape` shape on the sibling
4916 // `:edicao` axis. Until this gate landed a value like
4917 // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
4918 // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
4919 // colloquial idiom) silently passed `StandardLayout::verify`
4920 // and landed in the rendered chart `README.md` `## License`
4921 // section + a future SPDX-aware Chart.yaml `license:`
4922 // emitter would refuse the value at `helm lint` time far
4923 // from the source caixa.lisp.
4924 let root = PathBuf::from("/tmp/x");
4925 let manifest = root.join("caixa.lisp");
4926 let default_lib = root.join("lib").join("demo.lisp");
4927 let layout =
4928 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4929 let mut c = caixa(CaixaKind::Biblioteca);
4930 c.licenca = Some("Apache_2.0".into());
4931 let err = layout.verify(&c, &root).unwrap_err();
4932 let LayoutError::LicencaViolation { caixa, issue } = err else {
4933 panic!("expected LayoutError::LicencaViolation, got {err:?}");
4934 };
4935 assert_eq!(caixa, "demo");
4936 assert!(
4937 issue.contains(":licenca"),
4938 "issue must name the offending slot: {issue}",
4939 );
4940 assert!(
4941 issue.contains("Apache_2.0"),
4942 "issue must quote the offending value: {issue}",
4943 );
4944 }
4945
4946 // ── :edicao empty-Some shape wired into verify (universal axis) ──
4947 //
4948 // Until this wire-up landed `Caixa::validate_edicao` did not
4949 // exist — the universal language-edition axis had no shape gate
4950 // at any layer, so an empty `Some("")` silently passed
4951 // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
4952 // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
4953 // a future renderer-side consumer's `Option::unwrap_or_else`
4954 // (which only fires on `None`) to skip its fallback. Closes the
4955 // same `Some("")`-skips-`unwrap_or_else` footgun the peer
4956 // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
4957 // `:licenca` (3d1e535) gates closed, on the universal language-
4958 // edition axis — the last un-gated universal-axis
4959 // `Option<String>` Caixa-level value-shape surface.
4960
4961 #[test]
4962 fn edicao_violation_on_empty_some() {
4963 // Canonical paste-from-blank-doc footgun on every kind. The
4964 // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
4965 // Display through verbatim, so the issue string names the
4966 // offending `:edicao` axis at the source — the author can
4967 // grep their caixa.lisp for `:edicao ""` and fix the empty
4968 // value in one edit. Mirrors the peer
4969 // `licenca_violation_on_empty_some` shape (3d1e535) on the
4970 // sibling `Option<String>` `:edicao` axis.
4971 let root = PathBuf::from("/tmp/x");
4972 let manifest = root.join("caixa.lisp");
4973 let default_lib = root.join("lib").join("demo.lisp");
4974 let layout =
4975 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4976 let mut c = caixa(CaixaKind::Biblioteca);
4977 c.edicao = Some(String::new());
4978 let err = layout.verify(&c, &root).unwrap_err();
4979 let LayoutError::EdicaoViolation { caixa, issue } = err else {
4980 panic!("expected LayoutError::EdicaoViolation, got {err:?}");
4981 };
4982 assert_eq!(caixa, "demo");
4983 assert!(
4984 issue.contains(":edicao"),
4985 "issue must name the offending slot: {issue}",
4986 );
4987 }
4988
4989 #[test]
4990 fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
4991 // Cross-axis precedence pin: a Biblioteca with empty
4992 // `:edicao` *and* declared mesh slots (`:membros`) surfaces
4993 // the universal `:edicao` diagnostic first, not the
4994 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4995 // `:edicao` is universal (every kind owns the slot), so
4996 // its shape diagnostic is more fundamental than the
4997 // partition-on-kind diagnostic. Mirrors the peer
4998 // `licenca_violation_fires_before_kind_coherence_mesh_slot`
4999 // pin (3d1e535) on the `:licenca` axis vs the same
5000 // kind-coherence gates.
5001 let root = PathBuf::from("/tmp/x");
5002 let manifest = root.join("caixa.lisp");
5003 let default_lib = root.join("lib").join("demo.lisp");
5004 let layout =
5005 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5006 let mut c = caixa(CaixaKind::Biblioteca);
5007 c.edicao = Some(String::new());
5008 c.membros = vec![crate::aplicacao::Membro {
5009 caixa: "x".into(),
5010 versao: "^0.1".into(),
5011 }];
5012 let err = layout.verify(&c, &root).unwrap_err();
5013 assert!(
5014 matches!(err, LayoutError::EdicaoViolation { .. }),
5015 "got {err:?}",
5016 );
5017 }
5018
5019 #[test]
5020 fn edicao_violation_fires_after_licenca_violation() {
5021 // Cross-axis precedence pin (inside the universal metadata
5022 // cascade): a caixa with both an empty `:licenca` *and* an
5023 // empty `:edicao` surfaces `LicencaViolation` first —
5024 // `:licenca` is the eighth universal axis in the cascade
5025 // and runs before `:edicao`, peer with the canonical
5026 // identity-axis-first cascade the peer gates establish.
5027 // Mirrors the peer
5028 // `licenca_violation_fires_after_descricao_violation`
5029 // precedence pin (3d1e535) on the descricao-axis-before-
5030 // licenca-axis pair.
5031 let root = PathBuf::from("/tmp/x");
5032 let manifest = root.join("caixa.lisp");
5033 let default_lib = root.join("lib").join("demo.lisp");
5034 let layout =
5035 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5036 let mut c = caixa(CaixaKind::Biblioteca);
5037 c.licenca = Some(String::new());
5038 c.edicao = Some(String::new());
5039 let err = layout.verify(&c, &root).unwrap_err();
5040 assert!(
5041 matches!(err, LayoutError::LicencaViolation { .. }),
5042 "got {err:?}",
5043 );
5044 }
5045
5046 #[test]
5047 fn edicao_violation_accepts_none() {
5048 // Positive control sanity pin: a caixa that omits `:edicao`
5049 // entirely (the layout-test fixture defaults to `None`)
5050 // passes the gate trivially — the gate is a no-op when the
5051 // author didn't author a value. Mirrors the peer
5052 // `licenca_violation_accepts_none` pin (3d1e535).
5053 let root = PathBuf::from("/tmp/x");
5054 let manifest = root.join("caixa.lisp");
5055 let default_lib = root.join("lib").join("demo.lisp");
5056 let layout =
5057 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5058 let c = caixa(CaixaKind::Biblioteca);
5059 layout.verify(&c, &root).expect("None must pass");
5060 }
5061
5062 #[test]
5063 fn edicao_violation_accepts_canonical_value() {
5064 // Positive control pin on the canonical pleme-io `:edicao`
5065 // shape: the `"2026"` edition every `caixa-helm` /
5066 // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
5067 // fixture carries by construction passes the gate end-to-end.
5068 let root = PathBuf::from("/tmp/x");
5069 let manifest = root.join("caixa.lisp");
5070 let default_lib = root.join("lib").join("demo.lisp");
5071 let layout =
5072 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5073 let mut c = caixa(CaixaKind::Biblioteca);
5074 c.edicao = Some("2026".into());
5075 layout
5076 .verify(&c, &root)
5077 .expect("canonical edition must pass");
5078 }
5079
5080 #[test]
5081 fn edicao_violation_on_non_year_shape() {
5082 // Shape-predicate wire-up pin: a malformed `:edicao` value
5083 // that's a non-empty `Some(s)` but not a 4-digit ASCII
5084 // decimal year surfaces the `EdicaoViolation` envelope via
5085 // the manifest-layer `ManifestError::EdicaoInvalid` arm.
5086 // Mirrors the peer `edicao_violation_on_empty_some` shape
5087 // on the empty arm of the same axis. Until this gate landed
5088 // a value like `"v2026"` (a familiar git-tag idiom that
5089 // doesn't apply to the year-shaped edition axis) silently
5090 // passed `StandardLayout::verify` and broke at the
5091 // substrate's build-time edition selector far from the
5092 // source caixa.lisp.
5093 let root = PathBuf::from("/tmp/x");
5094 let manifest = root.join("caixa.lisp");
5095 let default_lib = root.join("lib").join("demo.lisp");
5096 let layout =
5097 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5098 let mut c = caixa(CaixaKind::Biblioteca);
5099 c.edicao = Some("v2026".into());
5100 let err = layout.verify(&c, &root).unwrap_err();
5101 let LayoutError::EdicaoViolation { caixa, issue } = err else {
5102 panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5103 };
5104 assert_eq!(caixa, "demo");
5105 assert!(
5106 issue.contains(":edicao"),
5107 "issue must name the offending slot: {issue}",
5108 );
5109 assert!(
5110 issue.contains("v2026"),
5111 "issue must quote the offending value: {issue}",
5112 );
5113 }
5114
5115 // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
5116 //
5117 // Until this wire-up landed `Caixa::validate_nome` and
5118 // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
5119 // per-arm unit coverage in `manifest::tests`, but no production
5120 // path called them — `feira build` silently accepted malformed
5121 // `:nome` / `:versao` and the failure surfaced at `helm install` /
5122 // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
5123 // :from` matching time, far from the source `caixa.lisp`. The
5124 // following pins fence the layout-pipeline wire-up: every layout
5125 // verify on a structurally-invalid Caixa identity axis surfaces
5126 // the per-axis `*Violation { caixa, issue }` envelope before any
5127 // kind-coherence, code-path, or downstream gate sees it.
5128
5129 #[test]
5130 fn nome_violation_on_uppercase() {
5131 let root = PathBuf::from("/tmp/x");
5132 let manifest = root.join("caixa.lisp");
5133 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5134 let mut c = caixa(CaixaKind::Biblioteca);
5135 c.nome = "MyApp".into();
5136 let err = layout.verify(&c, &root).unwrap_err();
5137 let LayoutError::NomeViolation { caixa, issue } = err else {
5138 panic!("expected LayoutError::NomeViolation, got {err:?}");
5139 };
5140 assert_eq!(caixa, "MyApp");
5141 assert!(
5142 issue.contains("MyApp"),
5143 "issue must quote the offending nome: {issue}",
5144 );
5145 }
5146
5147 #[test]
5148 fn nome_violation_on_underscore() {
5149 let root = PathBuf::from("/tmp/x");
5150 let manifest = root.join("caixa.lisp");
5151 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5152 let mut c = caixa(CaixaKind::Biblioteca);
5153 c.nome = "my_app".into();
5154 let err = layout.verify(&c, &root).unwrap_err();
5155 assert!(
5156 matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
5157 "got {err:?}",
5158 );
5159 }
5160
5161 #[test]
5162 fn nome_violation_on_empty() {
5163 // Empty `:nome` surfaces NomeViolation wrapping the narrower
5164 // `ManifestError::NomeEmpty` arm — the empty-first cascade the
5165 // peer per-axis name gates already use.
5166 let root = PathBuf::from("/tmp/x");
5167 let manifest = root.join("caixa.lisp");
5168 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5169 let mut c = caixa(CaixaKind::Biblioteca);
5170 c.nome = String::new();
5171 let err = layout.verify(&c, &root).unwrap_err();
5172 let LayoutError::NomeViolation { caixa, issue } = err else {
5173 panic!("expected LayoutError::NomeViolation, got {err:?}");
5174 };
5175 assert!(caixa.is_empty());
5176 assert!(
5177 issue.contains(":nome is empty"),
5178 "issue must surface the empty-arm diagnostic: {issue}",
5179 );
5180 }
5181
5182 #[test]
5183 fn versao_violation_on_missing_patch() {
5184 // `"0.1"` — the canonical "I shortened it" footgun. Helm /
5185 // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
5186 // through `semver::Version::parse`, which refuses a two-part
5187 // shape.
5188 let root = PathBuf::from("/tmp/x");
5189 let manifest = root.join("caixa.lisp");
5190 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5191 let mut c = caixa(CaixaKind::Biblioteca);
5192 c.versao = "0.1".into();
5193 let err = layout.verify(&c, &root).unwrap_err();
5194 let LayoutError::VersaoViolation { caixa, issue } = err else {
5195 panic!("expected LayoutError::VersaoViolation, got {err:?}");
5196 };
5197 assert_eq!(caixa, "demo");
5198 assert!(
5199 issue.contains("0.1"),
5200 "issue must quote the offending versao: {issue}",
5201 );
5202 }
5203
5204 #[test]
5205 fn versao_violation_on_git_tag_shape() {
5206 // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
5207 let root = PathBuf::from("/tmp/x");
5208 let manifest = root.join("caixa.lisp");
5209 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5210 let mut c = caixa(CaixaKind::Biblioteca);
5211 c.versao = "v0.1.0".into();
5212 let err = layout.verify(&c, &root).unwrap_err();
5213 assert!(
5214 matches!(err, LayoutError::VersaoViolation { ref issue, .. }
5215 if issue.contains("v0.1.0")),
5216 "got {err:?}",
5217 );
5218 }
5219
5220 #[test]
5221 fn versao_violation_on_empty() {
5222 let root = PathBuf::from("/tmp/x");
5223 let manifest = root.join("caixa.lisp");
5224 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5225 let mut c = caixa(CaixaKind::Biblioteca);
5226 c.versao = String::new();
5227 let err = layout.verify(&c, &root).unwrap_err();
5228 let LayoutError::VersaoViolation { caixa, issue } = err else {
5229 panic!("expected LayoutError::VersaoViolation, got {err:?}");
5230 };
5231 assert_eq!(caixa, "demo");
5232 assert!(
5233 issue.contains(":versao is empty"),
5234 "issue must surface the empty-arm diagnostic: {issue}",
5235 );
5236 }
5237
5238 #[test]
5239 fn nome_violation_fires_before_versao_violation() {
5240 // Precedence pin: when both `:nome` and `:versao` are malformed,
5241 // `:nome` surfaces first — the canonical declaration-order
5242 // precedence the `ManifestError` family establishes, the same
5243 // grep-order the author follows when fixing in `caixa.lisp`.
5244 let root = PathBuf::from("/tmp/x");
5245 let manifest = root.join("caixa.lisp");
5246 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5247 let mut c = caixa(CaixaKind::Biblioteca);
5248 c.nome = "MyApp".into();
5249 c.versao = "0.1".into();
5250 let err = layout.verify(&c, &root).unwrap_err();
5251 assert!(
5252 matches!(err, LayoutError::NomeViolation { .. }),
5253 "got {err:?} — nome must fire before versao",
5254 );
5255 }
5256
5257 #[test]
5258 fn nome_violation_fires_before_kind_coherence() {
5259 // Precedence pin: a Biblioteca caixa with a malformed `:nome`
5260 // AND a declared mesh slot surfaces NomeViolation, not
5261 // MeshSlotsOnNonAplicacao — the identity-axis gate is more
5262 // fundamental than the kind-coherence gate (which carries
5263 // `caixa.nome` verbatim in its diagnostic, and so depends on the
5264 // name being structurally valid to render a useful message).
5265 let root = PathBuf::from("/tmp/x");
5266 let manifest = root.join("caixa.lisp");
5267 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5268 let mut c = caixa(CaixaKind::Biblioteca);
5269 c.nome = "MyApp".into();
5270 c.membros = vec![crate::aplicacao::Membro {
5271 caixa: "x".into(),
5272 versao: "^0.1".into(),
5273 }];
5274 let err = layout.verify(&c, &root).unwrap_err();
5275 assert!(
5276 matches!(err, LayoutError::NomeViolation { .. }),
5277 "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
5278 );
5279 }
5280
5281 #[test]
5282 fn nome_violation_fires_before_owncode() {
5283 // Precedence pin: a Supervisor with a malformed `:nome` AND
5284 // declared `:bibliotecas` surfaces NomeViolation, not
5285 // SupervisorOwnsCode — same rationale as above.
5286 let root = PathBuf::from("/tmp/x");
5287 let manifest = root.join("caixa.lisp");
5288 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5289 let mut c = caixa(CaixaKind::Supervisor);
5290 c.nome = "MyApp".into();
5291 c.bibliotecas = vec!["lib/x.lisp".into()];
5292 let err = layout.verify(&c, &root).unwrap_err();
5293 assert!(
5294 matches!(err, LayoutError::NomeViolation { .. }),
5295 "got {err:?} — nome must fire before SupervisorOwnsCode",
5296 );
5297 }
5298
5299 #[test]
5300 fn versao_violation_fires_before_kind_coherence() {
5301 // Precedence pin: a Biblioteca with a valid `:nome` but a
5302 // malformed `:versao` AND a declared servico slot surfaces
5303 // VersaoViolation before ServicoSlotsOnNonServico.
5304 let root = PathBuf::from("/tmp/x");
5305 let manifest = root.join("caixa.lisp");
5306 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5307 let mut c = caixa(CaixaKind::Biblioteca);
5308 c.versao = "v0.1.0".into();
5309 c.limits = Some(crate::LimitsSpec {
5310 memory: Some(64 * 1024 * 1024),
5311 ..Default::default()
5312 });
5313 let err = layout.verify(&c, &root).unwrap_err();
5314 assert!(
5315 matches!(err, LayoutError::VersaoViolation { .. }),
5316 "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
5317 );
5318 }
5319
5320 #[test]
5321 fn nome_violation_fires_before_missing_lib() {
5322 // Precedence pin: a Biblioteca with a malformed `:nome` and no
5323 // lib entry surfaces NomeViolation, not MissingLib — the
5324 // identity-axis gate is more fundamental than the layout's
5325 // `lib/<nome>.lisp` default-path check (which derives the
5326 // expected path from `:nome` itself, so would surface a
5327 // misleading "expected lib/MyApp.lisp" diagnostic against an
5328 // unrecoverable name).
5329 let root = PathBuf::from("/tmp/x");
5330 let manifest = root.join("caixa.lisp");
5331 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5332 let mut c = caixa(CaixaKind::Biblioteca);
5333 c.nome = "MyApp".into();
5334 let err = layout.verify(&c, &root).unwrap_err();
5335 assert!(
5336 matches!(err, LayoutError::NomeViolation { .. }),
5337 "got {err:?} — nome must fire before MissingLib",
5338 );
5339 }
5340
5341 #[test]
5342 fn nome_versao_violations_fire_after_missing_manifest() {
5343 // Precedence pin: `MissingManifest` still dominates — there's
5344 // no caixa to identity-check when the manifest is missing.
5345 let root = PathBuf::from("/tmp/x");
5346 let layout = StandardLayout::new().with_path_exists(|_| false);
5347 let mut c = caixa(CaixaKind::Biblioteca);
5348 c.nome = "MyApp".into();
5349 c.versao = "0.1".into();
5350 let err = layout.verify(&c, &root).unwrap_err();
5351 assert!(
5352 matches!(err, LayoutError::MissingManifest(_)),
5353 "got {err:?} — MissingManifest must dominate identity gates",
5354 );
5355 }
5356
5357 #[test]
5358 fn valid_nome_versao_passes_to_downstream_gates() {
5359 // Sanity pin: the canonical "demo" / "0.1.0" identity passes
5360 // both axes; downstream gates (MissingLib here) take over.
5361 let root = PathBuf::from("/tmp/x");
5362 let manifest = root.join("caixa.lisp");
5363 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5364 let err = layout
5365 .verify(&caixa(CaixaKind::Biblioteca), &root)
5366 .unwrap_err();
5367 assert!(
5368 matches!(err, LayoutError::MissingLib { .. }),
5369 "got {err:?} — valid identity must pass to MissingLib",
5370 );
5371 }
5372
5373 // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
5374 //
5375 // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
5376 // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
5377 // `dep::tests` but no production path called it — `feira build`
5378 // silently accepted a malformed `:deps` / `:deps-dev` entry and the
5379 // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
5380 // / `helm install` time on the *first* downstream consumer to
5381 // strict-parse the value, far from the source `caixa.lisp` and
5382 // without any field naming the offending `:deps` axis. The following
5383 // pins fence the layout-pipeline wire-up: every layout verify on a
5384 // structurally-invalid `:deps` value-shape surfaces the per-axis
5385 // `DepsViolation { caixa, issue }` envelope (peer of
5386 // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
5387 // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
5388 // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
5389 // coherence, code-path, or downstream gate sees it.
5390
5391 #[test]
5392 fn deps_violation_on_empty_dep_nome() {
5393 // Empty `:nome` on a `:deps` entry surfaces the narrower
5394 // `DepError::NomeEmpty` arm through the wrap envelope.
5395 use crate::Dep;
5396 let root = PathBuf::from("/tmp/x");
5397 let manifest = root.join("caixa.lisp");
5398 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5399 let mut c = caixa(CaixaKind::Biblioteca);
5400 c.deps = vec![Dep::simple("", "^0.1")];
5401 let err = layout.verify(&c, &root).unwrap_err();
5402 let LayoutError::DepsViolation { caixa, issue } = err else {
5403 panic!("expected LayoutError::DepsViolation, got {err:?}");
5404 };
5405 assert_eq!(caixa, "demo");
5406 assert!(
5407 issue.contains(":deps") && issue.contains(":nome"),
5408 "issue must name the offending slot + axis: {issue}",
5409 );
5410 }
5411
5412 #[test]
5413 fn deps_violation_on_uppercase_dep_nome() {
5414 // Uppercase `:nome` on a `:deps` entry surfaces
5415 // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
5416 use crate::Dep;
5417 let root = PathBuf::from("/tmp/x");
5418 let manifest = root.join("caixa.lisp");
5419 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5420 let mut c = caixa(CaixaKind::Biblioteca);
5421 c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
5422 let err = layout.verify(&c, &root).unwrap_err();
5423 let LayoutError::DepsViolation { caixa, issue } = err else {
5424 panic!("expected LayoutError::DepsViolation, got {err:?}");
5425 };
5426 assert_eq!(caixa, "demo");
5427 assert!(
5428 issue.contains("Caixa-Teia"),
5429 "issue must quote the offending dep nome verbatim: {issue}",
5430 );
5431 }
5432
5433 #[test]
5434 fn deps_violation_on_unparseable_dep_versao() {
5435 // Unparseable `:versao` requirement on a `:deps` entry surfaces
5436 // `DepError::VersaoInvalid` through the wrap — the canonical
5437 // "the semver::Error reached the resolver, far from the source"
5438 // footgun closed at author time.
5439 use crate::Dep;
5440 let root = PathBuf::from("/tmp/x");
5441 let manifest = root.join("caixa.lisp");
5442 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5443 let mut c = caixa(CaixaKind::Biblioteca);
5444 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5445 let err = layout.verify(&c, &root).unwrap_err();
5446 let LayoutError::DepsViolation { caixa, issue } = err else {
5447 panic!("expected LayoutError::DepsViolation, got {err:?}");
5448 };
5449 assert_eq!(caixa, "demo");
5450 assert!(
5451 issue.contains("caixa-teia") && issue.contains("not-a-req"),
5452 "issue must quote the dep nome + offending versao: {issue}",
5453 );
5454 }
5455
5456 #[test]
5457 fn deps_violation_on_duplicate_nome_in_deps() {
5458 // Within-list `:deps :nome` duplicate surfaces
5459 // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
5460 use crate::Dep;
5461 let root = PathBuf::from("/tmp/x");
5462 let manifest = root.join("caixa.lisp");
5463 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5464 let mut c = caixa(CaixaKind::Biblioteca);
5465 c.deps = vec![
5466 Dep::simple("caixa-teia", "^0.1"),
5467 Dep::simple("caixa-teia", "^0.2"),
5468 ];
5469 let err = layout.verify(&c, &root).unwrap_err();
5470 let LayoutError::DepsViolation { caixa, issue } = err else {
5471 panic!("expected LayoutError::DepsViolation, got {err:?}");
5472 };
5473 assert_eq!(caixa, "demo");
5474 assert!(
5475 issue.contains("caixa-teia") && issue.contains(":deps"),
5476 "issue must quote the duplicated nome + list: {issue}",
5477 );
5478 }
5479
5480 #[test]
5481 fn deps_violation_on_duplicate_nome_in_deps_dev() {
5482 // Within-list `:deps-dev :nome` duplicate surfaces the same
5483 // diagnostic on the dev-only axis — neither list is a
5484 // second-class citizen of the typed surface.
5485 use crate::Dep;
5486 let root = PathBuf::from("/tmp/x");
5487 let manifest = root.join("caixa.lisp");
5488 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5489 let mut c = caixa(CaixaKind::Biblioteca);
5490 c.deps_dev = vec![
5491 Dep::simple("caixa-teia", "^0.1"),
5492 Dep::simple("caixa-teia", "^0.2"),
5493 ];
5494 let err = layout.verify(&c, &root).unwrap_err();
5495 let LayoutError::DepsViolation { caixa, issue } = err else {
5496 panic!("expected LayoutError::DepsViolation, got {err:?}");
5497 };
5498 assert_eq!(caixa, "demo");
5499 assert!(
5500 issue.contains(":deps-dev"),
5501 "issue must name the offending list: {issue}",
5502 );
5503 }
5504
5505 #[test]
5506 fn deps_violation_in_deps_fires_before_deps_dev() {
5507 // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
5508 // malformed entry, the `:deps` walk fires first — the canonical
5509 // declaration-order precedence `Caixa::validate_deps` establishes
5510 // (the same author-grep ordering the typed-graph peers use on
5511 // every other Vec-shaped surface).
5512 use crate::Dep;
5513 let root = PathBuf::from("/tmp/x");
5514 let manifest = root.join("caixa.lisp");
5515 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5516 let mut c = caixa(CaixaKind::Biblioteca);
5517 c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
5518 c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
5519 let err = layout.verify(&c, &root).unwrap_err();
5520 let LayoutError::DepsViolation { caixa: _, issue } = err else {
5521 panic!("expected LayoutError::DepsViolation, got {err:?}");
5522 };
5523 assert!(
5524 issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
5525 "issue must name the :deps offender, not :deps-dev: {issue}",
5526 );
5527 }
5528
5529 #[test]
5530 fn deps_violation_fires_after_versao_violation() {
5531 // Precedence pin: when both the top-level `:versao` and a `:deps`
5532 // entry are malformed, the Caixa-identity gate fires first — the
5533 // canonical declaration order on `Caixa` (`:nome` → `:versao` →
5534 // ... → `:deps`) and the same identity-axis-dominates-content-
5535 // axis discipline the peer `validate_nome` / `validate_versao`
5536 // wire-up established (1f74a5f). A malformed `:versao` would
5537 // otherwise quote `caixa.nome` against a downstream-shaped
5538 // diagnostic.
5539 use crate::Dep;
5540 let root = PathBuf::from("/tmp/x");
5541 let manifest = root.join("caixa.lisp");
5542 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5543 let mut c = caixa(CaixaKind::Biblioteca);
5544 c.versao = "v0.1.0".into();
5545 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5546 let err = layout.verify(&c, &root).unwrap_err();
5547 assert!(
5548 matches!(err, LayoutError::VersaoViolation { .. }),
5549 "got {err:?} — versao must fire before DepsViolation",
5550 );
5551 }
5552
5553 #[test]
5554 fn deps_violation_fires_before_kind_coherence() {
5555 // Precedence pin: a Supervisor with a malformed `:deps` entry
5556 // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
5557 // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
5558 // dep surface is universal across all kinds and its shape gate
5559 // is more fundamental than the kind-coherence partitions on
5560 // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
5561 // dep typo without first being told to move their `:bibliotecas`
5562 // off a Supervisor.
5563 use crate::Dep;
5564 let root = PathBuf::from("/tmp/x");
5565 let manifest = root.join("caixa.lisp");
5566 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5567 let mut c = caixa(CaixaKind::Supervisor);
5568 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5569 c.bibliotecas = vec!["lib/x.lisp".into()];
5570 let err = layout.verify(&c, &root).unwrap_err();
5571 assert!(
5572 matches!(err, LayoutError::DepsViolation { .. }),
5573 "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
5574 );
5575 }
5576
5577 #[test]
5578 fn deps_violation_fires_after_missing_manifest() {
5579 // Precedence pin: `MissingManifest` still dominates — there's no
5580 // caixa to deps-check when the manifest is missing.
5581 use crate::Dep;
5582 let root = PathBuf::from("/tmp/x");
5583 let layout = StandardLayout::new().with_path_exists(|_| false);
5584 let mut c = caixa(CaixaKind::Biblioteca);
5585 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5586 let err = layout.verify(&c, &root).unwrap_err();
5587 assert!(
5588 matches!(err, LayoutError::MissingManifest(_)),
5589 "got {err:?} — MissingManifest must dominate the deps gate",
5590 );
5591 }
5592
5593 #[test]
5594 fn deps_violation_on_self_dep_in_deps() {
5595 // Cross-slot self-edge: a caixa whose `:deps` lists its own
5596 // `:nome` is rejected at the layout wire-up, the diagnostic
5597 // surfaces through the `DepsViolation` envelope with both the
5598 // offending list tag (`":deps"`) and the parent's `:nome`
5599 // verbatim. Until this wire-up landed the self-dep silently
5600 // passed `feira build` and the resolver's lacre-pipeline
5601 // closure walk either rejected mid-traversal (infinite
5602 // recursion detected far from the source caixa.lisp) or, on
5603 // the unbounded path, recursed until it exhausted its stack.
5604 // Mirrors the supervision-tree
5605 // [`supervisor_violation_on_self_supervision`] and the
5606 // Aplicacao-membership self-edge wire-up tests on the peer
5607 // typed-name-graph axes.
5608 use crate::Dep;
5609 let root = PathBuf::from("/tmp/x");
5610 let manifest = root.join("caixa.lisp");
5611 let default_lib = root.join("lib").join("demo.lisp");
5612 let layout =
5613 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5614 let mut c = caixa(CaixaKind::Biblioteca);
5615 c.deps = vec![Dep::simple("demo", "^0.1")];
5616 let err = layout.verify(&c, &root).unwrap_err();
5617 let LayoutError::DepsViolation { caixa, issue } = err else {
5618 panic!("expected LayoutError::DepsViolation, got {err:?}");
5619 };
5620 assert_eq!(caixa, "demo");
5621 assert!(
5622 issue.contains(":deps") && issue.contains("demo"),
5623 "issue must name the offending list + parent :nome: {issue}",
5624 );
5625 }
5626
5627 #[test]
5628 fn deps_violation_on_self_dep_in_deps_dev() {
5629 // Same cross-slot self-edge gate on the `:deps-dev` axis —
5630 // neither dep list is a second-class citizen of the typed
5631 // surface. The diagnostic names `:deps-dev` so the author can
5632 // grep their caixa.lisp for the offending block directly.
5633 use crate::Dep;
5634 let root = PathBuf::from("/tmp/x");
5635 let manifest = root.join("caixa.lisp");
5636 let default_lib = root.join("lib").join("demo.lisp");
5637 let layout =
5638 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5639 let mut c = caixa(CaixaKind::Biblioteca);
5640 c.deps_dev = vec![Dep::simple("demo", "^0.1")];
5641 let err = layout.verify(&c, &root).unwrap_err();
5642 let LayoutError::DepsViolation { caixa, issue } = err else {
5643 panic!("expected LayoutError::DepsViolation, got {err:?}");
5644 };
5645 assert_eq!(caixa, "demo");
5646 assert!(
5647 issue.contains(":deps-dev"),
5648 "issue must name the offending list: {issue}",
5649 );
5650 }
5651
5652 #[test]
5653 fn self_dep_fires_after_per_entry_dep_shape() {
5654 // Precedence pin: the per-entry shape gates of
5655 // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
5656 // fire first on a self-dep entry whose `:nome` is malformed.
5657 // Same ordering posture every peer cross-slot gate uses
5658 // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
5659 // `validate_no_self_membership` after `AplicacaoSpec::validate`).
5660 // A malformed self-dep `:nome` surfaces the narrower
5661 // per-entry diagnostic (which already names the parser-side
5662 // reason) before the self-edge gate sees the entry.
5663 use crate::Dep;
5664 let root = PathBuf::from("/tmp/x");
5665 let manifest = root.join("caixa.lisp");
5666 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5667 let mut c = caixa(CaixaKind::Biblioteca);
5668 // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
5669 // (DNS-1123 invalid). The per-entry shape gate fires on the
5670 // upper-case nome, masking the self-edge gate (and that's the
5671 // canonical precedence — fix the dep shape first, then the
5672 // structural self-edge becomes the next live diagnostic).
5673 c.deps = vec![Dep::simple("DEMO", "^0.1")];
5674 let err = layout.verify(&c, &root).unwrap_err();
5675 let LayoutError::DepsViolation { caixa: _, issue } = err else {
5676 panic!("expected LayoutError::DepsViolation, got {err:?}");
5677 };
5678 assert!(
5679 issue.contains("DNS-1123"),
5680 "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
5681 );
5682 }
5683
5684 #[test]
5685 fn valid_deps_pass_to_downstream_gates() {
5686 // Positive control pin: the canonical authoring shape (one
5687 // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
5688 // one `:deps-dev` entry on a distinct nome) passes the dep gate;
5689 // downstream gates (MissingLib here) take over. Drift here =
5690 // a future tighten that rejects any canonical shape surfaces as
5691 // a regression at this layout-level pin.
5692 use crate::Dep;
5693 let root = PathBuf::from("/tmp/x");
5694 let manifest = root.join("caixa.lisp");
5695 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5696 let mut c = caixa(CaixaKind::Biblioteca);
5697 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5698 c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
5699 let err = layout.verify(&c, &root).unwrap_err();
5700 assert!(
5701 matches!(err, LayoutError::MissingLib { .. }),
5702 "got {err:?} — valid deps must pass to MissingLib",
5703 );
5704 }
5705
5706 #[test]
5707 fn code_path_gate_runs_after_foreign_code_slot_gate() {
5708 // Precedence pin: a Servico that declares `:exe` (foreign code
5709 // surface) surfaces ForeignCodeSlot, *not* a per-entry path
5710 // shape diagnostic, even when the `:exe` entry is itself
5711 // malformed. The kind-coherence gate is the load-bearing
5712 // diagnostic at this site — once the slot is moved off the
5713 // wrong kind, the per-entry shape gate becomes the next live
5714 // diagnostic.
5715 let root = PathBuf::from("/tmp/x");
5716 let manifest = root.join("caixa.lisp");
5717 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5718 let mut c = caixa(CaixaKind::Servico);
5719 c.servicos = vec!["servicos/ok.yaml".into()];
5720 c.exe = vec!["/etc/foreign".into()];
5721 let err = layout.verify(&c, &root).unwrap_err();
5722 assert!(
5723 matches!(err, LayoutError::ForeignCodeSlot { .. }),
5724 "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
5725 );
5726 }
5727
5728 // ── M2 typed-substrate invariants ────────────────────────────────────
5729
5730 #[test]
5731 fn behavior_callback_path_must_exist() {
5732 use crate::BehaviorSpec;
5733 use std::path::PathBuf;
5734 let root = PathBuf::from("/tmp/x");
5735 let manifest = root.join("caixa.lisp");
5736 let mut c = caixa(CaixaKind::Servico);
5737 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5738 let svc = root.join("servicos/demo.computeunit.yaml");
5739 c.behavior = Some(BehaviorSpec {
5740 on_init: Some(PathBuf::from("lib/init.lisp")),
5741 ..Default::default()
5742 });
5743 let manifest_clone = manifest.clone();
5744 let svc_clone = svc.clone();
5745 let layout =
5746 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
5747 let err = layout.verify(&c, &root).unwrap_err();
5748 assert!(matches!(
5749 err,
5750 LayoutError::MissingEntry { kind, .. }
5751 if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
5752 ));
5753
5754 // Now declare the path exists — passes.
5755 let init = root.join("lib/init.lisp");
5756 let layout =
5757 StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5758 layout.verify(&c, &root).unwrap();
5759 }
5760
5761 #[test]
5762 fn behavior_absolute_callback_is_violation_not_missing() {
5763 // An absolute path silently subverts `root.join(p)` (Path::join
5764 // replaces the base when the right side is absolute). Before
5765 // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
5766 // surface as a confusing "missing behavior-callback /etc/passwd"
5767 // — or, worse, pass when /etc/passwd happens to exist. Now it's
5768 // a value-shape error naming the slot.
5769 use crate::BehaviorSpec;
5770 let root = PathBuf::from("/tmp/x");
5771 let manifest = root.join("caixa.lisp");
5772 let svc = root.join("servicos/demo.computeunit.yaml");
5773 let mut c = caixa(CaixaKind::Servico);
5774 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5775 c.behavior = Some(BehaviorSpec {
5776 on_init: Some(PathBuf::from("/etc/passwd")),
5777 ..Default::default()
5778 });
5779 // Path exists check would *succeed* on /etc/passwd (proving the
5780 // sandbox bypass) — value-shape pass must fire first.
5781 let layout = StandardLayout::new()
5782 .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
5783 let err = layout.verify(&c, &root).unwrap_err();
5784 assert!(
5785 matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
5786 "got {err:?}",
5787 );
5788 }
5789
5790 #[test]
5791 fn behavior_empty_callback_is_violation() {
5792 use crate::BehaviorSpec;
5793 let root = PathBuf::from("/tmp/x");
5794 let manifest = root.join("caixa.lisp");
5795 let svc = root.join("servicos/demo.computeunit.yaml");
5796 let mut c = caixa(CaixaKind::Servico);
5797 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5798 c.behavior = Some(BehaviorSpec {
5799 on_call: Some(PathBuf::new()),
5800 ..Default::default()
5801 });
5802 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5803 let err = layout.verify(&c, &root).unwrap_err();
5804 assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
5805 }
5806
5807 #[test]
5808 fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
5809 // Wiring pin: the cross-entry duplicate-`:from` gate in
5810 // `validate_upgrade_from` lands on the same
5811 // `LayoutError::UpgradeViolation` axis the per-entry
5812 // `UpgradeFromEntry::validate` already does (26da2c7), so a
5813 // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
5814 // `feira build` time naming the offending caixa rather than
5815 // silently passing into the wasm-operator's non-deterministic
5816 // dispatch. Mirrors `behavior_empty_callback_is_violation` on
5817 // the peer M2 typed slot.
5818 use crate::{UpgradeFromEntry, UpgradeInstruction};
5819 let root = PathBuf::from("/tmp/x");
5820 let manifest = root.join("caixa.lisp");
5821 let svc = root.join("servicos/demo.computeunit.yaml");
5822 let mut c = caixa(CaixaKind::Servico);
5823 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5824 c.upgrade_from = vec![
5825 UpgradeFromEntry {
5826 from: "0.1.0".into(),
5827 instructions: vec![UpgradeInstruction::Restart],
5828 },
5829 UpgradeFromEntry {
5830 from: "0.1.0".into(),
5831 instructions: vec![UpgradeInstruction::Restart],
5832 },
5833 ];
5834 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5835 let err = layout.verify(&c, &root).unwrap_err();
5836 let LayoutError::UpgradeViolation { caixa, issue } = err else {
5837 panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
5838 };
5839 assert_eq!(caixa, "demo");
5840 assert!(
5841 issue.contains("0.1.0"),
5842 "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
5843 );
5844 }
5845
5846 #[test]
5847 fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
5848 // Wiring pin: the cross-slot precedence gate in
5849 // `validate_upgrade_from_against_versao` lands on the same
5850 // `LayoutError::UpgradeViolation` axis the per-entry and
5851 // cross-entry gates already do (26da2c7, 7c6aef2), so a
5852 // caixa.lisp whose `:upgrade-from :from` is greater than the
5853 // caixa's own `:versao` surfaces at `feira build` time
5854 // naming the offending caixa rather than silently passing
5855 // into the wasm-operator's `:from`-match dispatch where the
5856 // entry would sit dormant forever. Mirrors
5857 // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
5858 // the peer cross-entry gate.
5859 use crate::{UpgradeFromEntry, UpgradeInstruction};
5860 let root = PathBuf::from("/tmp/x");
5861 let manifest = root.join("caixa.lisp");
5862 let svc = root.join("servicos/demo.computeunit.yaml");
5863 let mut c = caixa(CaixaKind::Servico);
5864 c.versao = "0.1.5".into();
5865 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5866 c.upgrade_from = vec![UpgradeFromEntry {
5867 from: "0.2.0".into(),
5868 instructions: vec![UpgradeInstruction::Restart],
5869 }];
5870 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5871 let err = layout.verify(&c, &root).unwrap_err();
5872 let LayoutError::UpgradeViolation { caixa, issue } = err else {
5873 panic!(
5874 "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
5875 );
5876 };
5877 assert_eq!(caixa, "demo");
5878 assert!(
5879 issue.contains("0.2.0") && issue.contains("0.1.5"),
5880 "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
5881 );
5882 }
5883
5884 #[test]
5885 fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
5886 // Self-upgrade no-op arm: `:from "0.1.0"` while
5887 // `:versao "0.1.0"` declares "upgrade from myself to
5888 // myself", which the operator's dispatch either skips
5889 // silently or trivially "succeeds" with no observable
5890 // transition. Surfaces at validate time naming both values
5891 // so the author can fix in one edit.
5892 use crate::{UpgradeFromEntry, UpgradeInstruction};
5893 let root = PathBuf::from("/tmp/x");
5894 let manifest = root.join("caixa.lisp");
5895 let svc = root.join("servicos/demo.computeunit.yaml");
5896 let mut c = caixa(CaixaKind::Servico);
5897 c.versao = "0.1.0".into();
5898 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5899 c.upgrade_from = vec![UpgradeFromEntry {
5900 from: "0.1.0".into(),
5901 instructions: vec![UpgradeInstruction::Restart],
5902 }];
5903 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5904 let err = layout.verify(&c, &root).unwrap_err();
5905 let LayoutError::UpgradeViolation { caixa, issue } = err else {
5906 panic!(
5907 "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
5908 {err:?}"
5909 );
5910 };
5911 assert_eq!(caixa, "demo");
5912 assert!(
5913 issue.contains("0.1.0"),
5914 "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
5915 );
5916 }
5917
5918 #[test]
5919 fn upgrade_from_strict_upgrade_passes_layout() {
5920 // Positive control for the precedence gate at the
5921 // LayoutInvariants level: a valid `:from < :versao` chain
5922 // (`0.1.0 → 0.2.0`) must not regress into a false-positive
5923 // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
5924 // positive-control arm.
5925 use crate::{UpgradeFromEntry, UpgradeInstruction};
5926 let root = PathBuf::from("/tmp/x");
5927 let manifest = root.join("caixa.lisp");
5928 let svc = root.join("servicos/demo.computeunit.yaml");
5929 let mut c = caixa(CaixaKind::Servico);
5930 c.versao = "0.2.0".into();
5931 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5932 c.upgrade_from = vec![UpgradeFromEntry {
5933 from: "0.1.0".into(),
5934 instructions: vec![UpgradeInstruction::Restart],
5935 }];
5936 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
5937 layout.verify(&c, &root).unwrap();
5938 }
5939
5940 #[test]
5941 fn upgrade_script_path_must_exist() {
5942 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
5943 use std::path::PathBuf;
5944 let root = PathBuf::from("/tmp/x");
5945 let manifest = root.join("caixa.lisp");
5946 let svc = root.join("servicos/demo.computeunit.yaml");
5947 let on_state_change = root.join("lib/migrations.lisp");
5948 let mut c = caixa(CaixaKind::Servico);
5949 // `:versao` past the entry's `:from` so the cross-slot
5950 // precedence gate (`FromNotBeforeVersao`) lets this case
5951 // through to the path-existence pass under test.
5952 c.versao = "0.2.0".into();
5953 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5954 // `:on-state-change` declared so the cross-slot composition
5955 // gate (`validate_upgrade_from_against_behavior`) lets the
5956 // `:state-change` entry through to the path-existence pass
5957 // under test. Without the callback the missing-callback gate
5958 // would surface first and the path-existence pass wouldn't be
5959 // exercised.
5960 c.behavior = Some(BehaviorSpec {
5961 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
5962 ..Default::default()
5963 });
5964 // A `:load-module` precedes the `:state-change` so the entry
5965 // satisfies the within-entry state-change-ordering gate
5966 // (`StateChangeWithoutPriorLoad`) and the path-existence pass
5967 // under test is the gate actually exercised. `:load-module`
5968 // carries no on-disk path, so it adds no existence requirement.
5969 c.upgrade_from = vec![UpgradeFromEntry {
5970 from: "0.1.0".into(),
5971 instructions: vec![
5972 UpgradeInstruction::LoadModule {
5973 module: "demo".into(),
5974 },
5975 UpgradeInstruction::StateChange {
5976 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5977 },
5978 ],
5979 }];
5980 let manifest_clone = manifest.clone();
5981 let svc_clone = svc.clone();
5982 let on_state_change_clone = on_state_change.clone();
5983 let layout = StandardLayout::new().with_path_exists(move |p| {
5984 p == manifest_clone || p == svc_clone || p == on_state_change_clone
5985 });
5986 let err = layout.verify(&c, &root).unwrap_err();
5987 assert!(matches!(
5988 err,
5989 LayoutError::MissingEntry { kind, .. }
5990 if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
5991 ));
5992 }
5993
5994 #[test]
5995 fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
5996 // Byte-identity pin: the two per-M2-slot leaf-kind labels the
5997 // [`LayoutError::MissingEntry`] `kind: &'static str`
5998 // discriminator surfaces under (the M2 `:behavior` per-callback
5999 // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
6000 // per-`:state-change` script-path on-disk-leaf axis) route
6001 // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6002 // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6003 // consts, so a future rebrand that reaches the const but not
6004 // the production emit / test probe (or vice versa) surfaces
6005 // here at build time rather than at runtime as a downstream
6006 // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6007 // diagnostic mismatch far from the rename's commit. Mirror of
6008 // the peer
6009 // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
6010 // (f50c875) and
6011 // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
6012 // (56120ef) byte-identity pins on the sibling M3 `:contratos`
6013 // per-entry endpoint-label + M2 `:upgrade-from :instructions`
6014 // per-variant kind-tag axes.
6015 assert_eq!(
6016 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6017 "behavior-callback"
6018 );
6019 assert_eq!(
6020 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6021 "upgrade-script"
6022 );
6023 }
6024
6025 #[test]
6026 fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
6027 // Byte-identity pin: the three per-M0-code-slot leaf-kind
6028 // labels the [`LayoutError::MissingEntry`] `kind: &'static
6029 // str` discriminator surfaces under (the `:bibliotecas`
6030 // per-entry axis, the `:exe` per-entry axis, the `:servicos`
6031 // per-entry axis) route through the lifted
6032 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6033 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6034 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
6035 // so a future rebrand that reaches the const but not the
6036 // production emit (or vice versa) surfaces here at build time
6037 // rather than at runtime as a downstream
6038 // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6039 // diagnostic mismatch far from the rename's commit. Mirror of
6040 // the peer M2-tier pin
6041 // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
6042 // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
6043 // per-slot leaf-kind axes.
6044 assert_eq!(
6045 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6046 "biblioteca"
6047 );
6048 assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
6049 assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
6050 }
6051
6052 #[test]
6053 fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
6054 // Cross-axis byte-identity pin: the two `:kind`-namesake M0
6055 // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6056 // = `"biblioteca"`,
6057 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
6058 // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
6059 // [`crate::CaixaKind::Servico`]'s
6060 // [`crate::CaixaKind::as_str`] outputs verbatim — the
6061 // substrate's canonical human-readable-kind axis and the
6062 // layout diagnostic's per-slot leaf-kind axis share one
6063 // vocabulary for these two arms by design (both label the
6064 // caixa's code-producing shape by its Portuguese-native
6065 // idiom), so drift between the two lands as a build-time
6066 // pattern-arm miss here rather than as a runtime diagnostic
6067 // that reads inconsistently across `feira build`'s
6068 // per-invocation output.
6069 //
6070 // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
6071 // = `"exe"`) is deliberately *distinct* from
6072 // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
6073 // output (`"binario"`) — the `:exe` code slot names the
6074 // per-directory leaf-kind at the `exe/` subtree, whereas
6075 // [`crate::CaixaKind::Binario`] names the caixa's own runtime
6076 // kind. Two axes, two labels — the inequality assertion here
6077 // pins the split so a future accidental collapse of the two
6078 // onto one scalar (a rebrand that reroutes either axis to
6079 // match the other) trips at build time.
6080 assert_eq!(
6081 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6082 CaixaKind::Biblioteca.as_str()
6083 );
6084 assert_eq!(
6085 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6086 CaixaKind::Servico.as_str()
6087 );
6088 assert_ne!(
6089 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6090 CaixaKind::Binario.as_str(),
6091 "`exe` leaf-kind label names the per-directory code-slot \
6092 axis; `binario` names the caixa-kind axis — the two must \
6093 not silently collapse onto one scalar"
6094 );
6095 }
6096
6097 #[test]
6098 fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
6099 // Distinctness pin: the five [`LayoutError::MissingEntry`]
6100 // `kind: &'static str` accept-set members
6101 // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6102 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6103 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
6104 // M0 code-slot arms plus
6105 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6106 // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6107 // on the M2 slot arms) must be pairwise distinct — an
6108 // accidental copy-paste flip that reroutes one label's byte-
6109 // string to also match another silently collapses two
6110 // per-slot diagnostics onto one, so an operator running
6111 // `feira build` reads `kind: "biblioteca"` for what should
6112 // have surfaced as a `:behavior :on-init` script-not-found
6113 // diagnostic (or vice versa). This pin catches any such
6114 // flip at build time. Mirror of the peer
6115 // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
6116 // / peer distinctness pins on other closed-set typed axes.
6117 let entries: &[(&str, &str)] = &[
6118 (
6119 "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
6120 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6121 ),
6122 (
6123 "LAYOUT_MISSING_ENTRY_KIND_EXE",
6124 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6125 ),
6126 (
6127 "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
6128 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6129 ),
6130 (
6131 "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
6132 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6133 ),
6134 (
6135 "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
6136 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6137 ),
6138 ];
6139 for (i, (name_a, value_a)) in entries.iter().enumerate() {
6140 for (name_b, value_b) in entries.iter().skip(i + 1) {
6141 assert_ne!(
6142 value_a, value_b,
6143 "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
6144 pairwise-distinct byte-strings — {name_a} and \
6145 {name_b} both resolve to {value_a:?}"
6146 );
6147 }
6148 }
6149 }
6150
6151 #[test]
6152 fn layout_dir_consts_pin_canonical_directory_names() {
6153 // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
6154 // consts naming the CSE-invariant per-[`CaixaKind`]
6155 // on-disk-directory-name axes the substrate's layout invariants
6156 // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
6157 // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
6158 // A future rebrand of any of the three on-disk directory landing
6159 // conventions must reach this pin — the const-edit lands on one
6160 // arm, the assertion here re-pins the new byte-string, and every
6161 // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
6162 // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
6163 // sandbox reconstruction, the future
6164 // `feira app deploy`-cluster scaffolder) picks up the new
6165 // directory name at build time. Mirror of the peer
6166 // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
6167 // (fe2a898) on the sibling
6168 // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
6169 // axis this on-disk-directory axis composes with.
6170 assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
6171 assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
6172 assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
6173 }
6174
6175 #[test]
6176 fn layout_dir_consts_are_pairwise_distinct() {
6177 // Distinctness pin: the three per-[`CaixaKind`]
6178 // on-disk-directory-name arms must resolve to pairwise-distinct
6179 // byte-strings — a future accidental copy-paste flip that
6180 // reroutes any one of the three onto another's value silently
6181 // collapses two per-kind on-disk sandboxes onto one, so
6182 // [`crate::LayoutInvariants::verify`] would gate a
6183 // [`CaixaKind::Binario`] caixa's `:exe` entries against the
6184 // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
6185 // entries against `lib/` and pass every entry `feira build`
6186 // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
6187 // Mirror of the peer
6188 // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
6189 // (fe2a898) on the sibling leaf-kind label accept-set.
6190 let entries: &[(&str, &str)] = &[
6191 ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
6192 ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
6193 ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
6194 ];
6195 for (i, (name_a, value_a)) in entries.iter().enumerate() {
6196 for (name_b, value_b) in entries.iter().skip(i + 1) {
6197 assert_ne!(
6198 value_a, value_b,
6199 "LAYOUT_DIR_* consts must be pairwise-distinct \
6200 byte-strings — {name_a} and {name_b} both resolve \
6201 to {value_a:?}"
6202 );
6203 }
6204 }
6205 }
6206
6207 #[test]
6208 fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
6209 // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
6210 // (the on-disk-directory-name arm) equals
6211 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
6212 // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
6213 // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
6214 // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
6215 // discriminator name the same three-byte sub-tree (`exe/`), a
6216 // coincidence [`crate::LayoutInvariants::verify`] itself relies
6217 // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
6218 // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
6219 // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
6220 // for every non-resolving entry. Making the coincidence
6221 // load-bearing means a future rebrand touching either axis
6222 // without the other (a per-consumer disambiguation collapsing
6223 // the leaf-kind label onto `"binary"` while the directory stays
6224 // `"exe"`, or vice versa) trips at caixa-core build time rather
6225 // than surfacing at runtime as a mismatched
6226 // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
6227 // reads one label while the `path:` sits under a differently-named
6228 // sub-tree.
6229 assert_eq!(
6230 crate::render::LAYOUT_DIR_EXE,
6231 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6232 "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
6233 both name the M0 `:kind Binario` sub-tree by the same \
6234 three-byte scalar"
6235 );
6236 }
6237
6238 #[test]
6239 fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
6240 // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
6241 // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
6242 // is *deliberately* distinct from
6243 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6244 // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
6245 // label) — the substrate splits the on-disk convention terse
6246 // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
6247 // matching Cargo's `src/lib.rs` abbreviation of the `library`
6248 // crate-type discriminator. A future accidental collapse of the
6249 // two axes onto one scalar (a rebrand aligning either arm with
6250 // the other for schema-clarity, an English-uniformity pass that
6251 // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
6252 // diagnostic label to `"lib"`) would silently reroute either
6253 // consumer onto the other's byte-string. This pin catches the
6254 // collapse at build time. Peer of the sibling
6255 // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
6256 // (fe2a898) that pins the analogous *equality* between the M0
6257 // `:kind Biblioteca` diagnostic-label arm and
6258 // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
6259 // output — the two pins jointly encode the "which of the three
6260 // Biblioteca-related scalars are load-bearing-equal, which are
6261 // load-bearing-distinct" invariant across the substrate's
6262 // per-kind vocabulary.
6263 assert_ne!(
6264 crate::render::LAYOUT_DIR_LIB,
6265 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6266 "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
6267 (`\"biblioteca\"`) name two distinct axes — the on-disk \
6268 directory convention (Cargo-style abbreviated) and the \
6269 layout-diagnostic leaf-kind label (full-form Portuguese) — \
6270 and must not silently collapse onto one scalar"
6271 );
6272 }
6273
6274 #[test]
6275 fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
6276 // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
6277 // (`"servicos"`, the Portuguese-*plural* on-disk directory
6278 // name) is *deliberately* distinct from
6279 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
6280 // (`"servico"`, the singular leaf-kind label) — the on-disk
6281 // sub-tree houses one-or-more ComputeUnit YAML descriptors per
6282 // caixa (hence the plural), the diagnostic label names the
6283 // caixa's own kind (singular). A future accidental collapse
6284 // onto one scalar (a per-consumer disambiguation aligning the
6285 // two, a hypothetical English-uniformity pass renaming
6286 // `"servicos"` → `"services"` while retaining `"servico"` on
6287 // the diagnostic arm — or vice versa) would silently reroute
6288 // either consumer onto the other's byte-string. Peer of the
6289 // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
6290 // pin on the M0 `:kind Biblioteca` split axis; two of the three
6291 // per-kind on-disk / leaf-kind splits carry a distinctness
6292 // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
6293 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
6294 // equality pin under
6295 // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
6296 assert_ne!(
6297 crate::render::LAYOUT_DIR_SERVICOS,
6298 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6299 "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
6300 and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
6301 leaf-kind label) name two distinct axes and must not silently \
6302 collapse onto one scalar"
6303 );
6304 }
6305
6306 #[test]
6307 fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
6308 // Production-through-const pin: [`LayoutInvariants::verify`]
6309 // routes its three per-kind sandbox-root joins
6310 // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
6311 // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
6312 // for the [`LayoutError::ExeOutsideDir`] gate,
6313 // `root.join(LAYOUT_DIR_SERVICOS)` for the
6314 // [`LayoutError::ServicoOutsideDir`] gate) through the three
6315 // lifted consts, not through inline `"lib"` / `"exe"` /
6316 // `"servicos"` `&str` literals. This test drives the
6317 // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
6318 // caixa whose declared `:exe` entry deliberately escapes
6319 // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
6320 // if the production emit reads the wrong const (or reverts to
6321 // an inline literal that drifts from the const) the diagnostic
6322 // arm surfaces the wrong variant, catching the drift at build
6323 // time rather than as a per-invocation runtime mismatch.
6324 //
6325 // Mirror of the peer production-through-const pin
6326 // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
6327 // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
6328 use std::path::PathBuf;
6329 let root = PathBuf::from("/tmp/x");
6330 let manifest = root.join("caixa.lisp");
6331 let bin_entry_outside = root.join("bin/tool");
6332 let mut c = caixa(CaixaKind::Binario);
6333 c.exe = vec!["bin/tool".into()];
6334 let manifest_clone = manifest.clone();
6335 let outside_clone = bin_entry_outside.clone();
6336 let layout = StandardLayout::new()
6337 .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
6338 let err = layout.verify(&c, &root).unwrap_err();
6339 match err {
6340 LayoutError::ExeOutsideDir(path) => {
6341 assert_eq!(
6342 path, bin_entry_outside,
6343 "ExeOutsideDir must carry the resolved `:exe` entry that \
6344 escapes `root.join(LAYOUT_DIR_EXE)`"
6345 );
6346 // Byte-identity check: the escape must be against the
6347 // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
6348 // literal — a future const-edit that drifts from `"exe"`
6349 // reroutes `exe_dir` off the sandbox `bin/tool` escapes
6350 // from, and this pattern-arm miss re-surfaces here.
6351 assert!(
6352 !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
6353 "resolved `:exe` entry {path:?} must escape the \
6354 `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
6355 emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
6356 );
6357 }
6358 other => panic!("expected ExeOutsideDir, got {other:?}"),
6359 }
6360 }
6361
6362 #[test]
6363 fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
6364 // Wiring pin for the cross-slot composition gate
6365 // (`validate_upgrade_from_against_behavior`): a caixa whose
6366 // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
6367 // instruction but does not declare `:behavior :on-state-change`
6368 // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
6369 // naming the offending caixa + the entry's `:from` + the
6370 // offending script — not at hot-upgrade dispatch when the
6371 // operator reaches for the missing callback. Mirrors
6372 // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
6373 // peer `:from` ↔ `:versao` cross-slot precedence gate.
6374 use crate::{UpgradeFromEntry, UpgradeInstruction};
6375 use std::path::PathBuf;
6376 let root = PathBuf::from("/tmp/x");
6377 let manifest = root.join("caixa.lisp");
6378 let svc = root.join("servicos/demo.computeunit.yaml");
6379 let mut c = caixa(CaixaKind::Servico);
6380 c.versao = "0.2.0".into();
6381 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6382 // `:behavior` is None (the canonical "I added the upgrade path
6383 // but never declared :behavior" footgun the gate closes); a
6384 // peer arm covers the BehaviorSpec-Some-but-on-state-change-
6385 // None shape in `upgrade::tests::behavior_gate_rejects_state_
6386 // change_when_on_state_change_is_none`.
6387 c.upgrade_from = vec![UpgradeFromEntry {
6388 from: "0.1.0".into(),
6389 instructions: vec![
6390 UpgradeInstruction::LoadModule {
6391 module: "demo".into(),
6392 },
6393 UpgradeInstruction::StateChange {
6394 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6395 },
6396 ],
6397 }];
6398 let manifest_clone = manifest.clone();
6399 let svc_clone = svc.clone();
6400 let layout =
6401 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6402 let err = layout.verify(&c, &root).unwrap_err();
6403 match err {
6404 LayoutError::UpgradeViolation { caixa, issue } => {
6405 assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
6406 assert!(
6407 issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
6408 "diagnostic must name the missing callback slot for self-locating fix, \
6409 got {issue:?}"
6410 );
6411 assert!(
6412 issue.contains("0.1.0"),
6413 "diagnostic must name the offending entry's :from, got {issue:?}"
6414 );
6415 assert!(
6416 issue.contains("v01-to-v02.lisp"),
6417 "diagnostic must name the offending :script for self-locating fix, \
6418 got {issue:?}"
6419 );
6420 }
6421 other => panic!("expected UpgradeViolation, got {other:?}"),
6422 }
6423 }
6424
6425 #[test]
6426 fn supervisor_must_have_children() {
6427 use crate::RestartStrategy;
6428 let root = PathBuf::from("/tmp/x");
6429 let manifest = root.join("caixa.lisp");
6430 let manifest_clone = manifest.clone();
6431 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6432 let mut c = caixa(CaixaKind::Supervisor);
6433 c.estrategia = Some(RestartStrategy::OneForOne);
6434 c.max_restarts = Some(5);
6435 // No children → should fail
6436 let err = layout.verify(&c, &root).unwrap_err();
6437 assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
6438 }
6439
6440 #[test]
6441 fn supervisor_self_referential_child_is_violation() {
6442 // A Supervisor whose `:children` names its own `:nome` is a
6443 // one-node supervision cycle. The cross-slot gate fires at
6444 // verify time, surfacing as a SupervisorViolation that names the
6445 // offending supervisor — not at the cluster apply far from
6446 // source. The `caixa()` helper's `:nome` is "demo".
6447 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6448 let root = PathBuf::from("/tmp/x");
6449 let manifest = root.join("caixa.lisp");
6450 let manifest_clone = manifest.clone();
6451 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6452 let mut c = caixa(CaixaKind::Supervisor);
6453 c.estrategia = Some(RestartStrategy::OneForOne);
6454 c.max_restarts = Some(5);
6455 c.children = vec![
6456 ChildSpec {
6457 caixa: "worker".into(),
6458 versao: "^0.1".into(),
6459 restart: RestartPolicy::Permanent,
6460 },
6461 ChildSpec {
6462 caixa: "demo".into(),
6463 versao: "^0.1".into(),
6464 restart: RestartPolicy::Permanent,
6465 },
6466 ];
6467 let err = layout.verify(&c, &root).unwrap_err();
6468 let LayoutError::SupervisorViolation { caixa, issue } = err else {
6469 panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6470 };
6471 assert_eq!(caixa, "demo");
6472 assert!(
6473 issue.contains("demo") && issue.contains("itself"),
6474 "issue must name the self-supervising caixa, got {issue:?}"
6475 );
6476 }
6477
6478 #[test]
6479 fn supervisor_distinct_children_pass_self_supervision_gate() {
6480 // Positive control: a Supervisor whose children are all distinct
6481 // from its own `:nome` verifies cleanly.
6482 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6483 let root = PathBuf::from("/tmp/x");
6484 let manifest = root.join("caixa.lisp");
6485 let manifest_clone = manifest.clone();
6486 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6487 let mut c = caixa(CaixaKind::Supervisor);
6488 c.estrategia = Some(RestartStrategy::OneForOne);
6489 c.max_restarts = Some(5);
6490 c.children = vec![ChildSpec {
6491 caixa: "worker".into(),
6492 versao: "^0.1".into(),
6493 restart: RestartPolicy::Permanent,
6494 }];
6495 layout.verify(&c, &root).unwrap();
6496 }
6497
6498 #[test]
6499 fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
6500 // Composition pin: every cross-slot self-edge gate fired from
6501 // `LayoutInvariants::verify` — the supervision-tree arm's
6502 // `crate::supervisor::validate_no_self_supervision` call, the
6503 // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
6504 // call, and the dep-graph arm's
6505 // `crate::dep::validate_no_self_dep` call — must key its
6506 // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
6507 // the raw `&caixa.nome` `&String`-borrow of the underlying
6508 // field.
6509 //
6510 // Structurally: a rename of the storage field or a hypothetical
6511 // accessor rebrand (a per-cluster alias table pinned through a
6512 // future `:placement`-scoped slot, the M4 CR materializer's
6513 // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
6514 // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
6515 // through the accessor by construction; a raw-borrow bypass
6516 // would silently disagree with every peer consumer that already
6517 // routes through `caixa.nome()` (the caixa-mesh 980c059,
6518 // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6519 // caixa-feira ef83332 raw-borrow converges), reintroducing the
6520 // drift surface the sibling converges closed. Each arm fires
6521 // its per-kind `LayoutError` variant (`SupervisorViolation` /
6522 // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
6523 // carries the offending parent name verbatim through
6524 // `caixa.nome().clone()`; asserting the field equals
6525 // `caixa.nome()` on the mutated fixture pins the accessor-
6526 // routed parent-nome projection at every call site — a future
6527 // silent detour that had the gate observe a stale / aliased
6528 // name at the arg boundary would surface here as a
6529 // `caixa != "demo"` inequality.
6530 //
6531 // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
6532 // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
6533 // renderer crates; ef83332 on the CLI) — extends the "one typed
6534 // dispatch per `:nome` consumer" discipline onto the substrate's
6535 // own [`LayoutInvariants::verify`] cross-slot self-edge gate
6536 // wire-up on all three typed-name-graph kinds.
6537 use crate::{
6538 ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
6539 };
6540 let root = PathBuf::from("/tmp/x");
6541 let manifest = root.join("caixa.lisp");
6542 let manifest_clone = manifest.clone();
6543 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6544
6545 // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
6546 // and the accessor's return `caixa.nome()` must equal the
6547 // parent-nome that the self-supervision gate observes.
6548 let mut sup = caixa(CaixaKind::Supervisor);
6549 sup.estrategia = Some(RestartStrategy::OneForOne);
6550 sup.max_restarts = Some(5);
6551 sup.children = vec![ChildSpec {
6552 caixa: "demo".into(),
6553 versao: "^0.1".into(),
6554 restart: RestartPolicy::Permanent,
6555 }];
6556 let parent_nome_via_accessor = sup.nome();
6557 assert_eq!(
6558 parent_nome_via_accessor, "demo",
6559 "the caixa() fixture helper's `:nome` must be \"demo\" — \
6560 the accessor's return is the pin's ground truth for the \
6561 cross-slot gate's parent-nome arg",
6562 );
6563 let err = layout.verify(&sup, &root).unwrap_err();
6564 let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
6565 panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6566 };
6567 assert_eq!(
6568 c_nome, parent_nome_via_accessor,
6569 "the SupervisorViolation's `caixa` field must equal \
6570 `sup.nome()` — the cross-slot self-supervision gate's \
6571 `parent_nome` arg must route through the lifted \
6572 [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6573 `&String`-borrow of the underlying field",
6574 );
6575
6576 // Aplicacao arm — same discipline on the peer typed-name-graph
6577 // kind. Constructed alongside the supervisor arm so any future
6578 // accessor drift lands on both arms in the same pin.
6579 let mut app = caixa(CaixaKind::Aplicacao);
6580 app.placement = Some(Placement {
6581 estrategia: PlacementStrategy::Replicated,
6582 clusters: vec!["rio".into()],
6583 affinity: None,
6584 shard_key: None,
6585 });
6586 app.membros = vec![Membro {
6587 caixa: "demo".into(),
6588 versao: "^0.1".into(),
6589 }];
6590 let parent_nome_via_accessor = app.nome();
6591 assert_eq!(
6592 parent_nome_via_accessor, "demo",
6593 "the caixa() fixture helper's `:nome` must be \"demo\" on \
6594 the Aplicacao arm too — same accessor-ground-truth as the \
6595 sibling supervisor arm above",
6596 );
6597 let err = layout.verify(&app, &root).unwrap_err();
6598 let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
6599 panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
6600 };
6601 assert_eq!(
6602 c_nome, parent_nome_via_accessor,
6603 "the AplicacaoViolation's `caixa` field must equal \
6604 `app.nome()` — the cross-slot self-membership gate's \
6605 `parent_nome` arg must route through the lifted \
6606 [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6607 `&String`-borrow of the underlying field",
6608 );
6609
6610 // Dep-graph arm — third typed-name-graph kind on the
6611 // `parent_nome` arg boundary. Same discipline as the peer
6612 // supervision-tree and Aplicacao-membership arms above.
6613 // Constructed alongside so any future accessor drift lands on
6614 // all three arms in the same pin. Needs a distinct layout
6615 // fixture from the supervisor / aplicacao arms above because
6616 // the `Biblioteca` kind's code-path existence gate demands the
6617 // canonical `lib/<nome>.lisp` path also `exists`, so the shim
6618 // covers both `caixa.lisp` and `lib/demo.lisp`.
6619 let default_lib = root.join("lib").join("demo.lisp");
6620 let manifest_dep = manifest.clone();
6621 let default_lib_clone = default_lib.clone();
6622 let layout_dep = StandardLayout::new()
6623 .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
6624 let mut lib = caixa(CaixaKind::Biblioteca);
6625 lib.deps = vec![Dep::simple("demo", "^0.1")];
6626 let parent_nome_via_accessor = lib.nome();
6627 assert_eq!(
6628 parent_nome_via_accessor, "demo",
6629 "the caixa() fixture helper's `:nome` must be \"demo\" on \
6630 the Biblioteca arm too — same accessor-ground-truth as the \
6631 sibling supervisor + Aplicacao arms above",
6632 );
6633 let err = layout_dep.verify(&lib, &root).unwrap_err();
6634 let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
6635 panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
6636 };
6637 assert_eq!(
6638 c_nome, parent_nome_via_accessor,
6639 "the DepsViolation's `caixa` field must equal \
6640 `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
6641 arg must route through the lifted [`Caixa::nome`] accessor, \
6642 not the raw `&caixa.nome` `&String`-borrow of the underlying \
6643 field",
6644 );
6645 }
6646
6647 #[test]
6648 fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
6649 // Composition pin: the cross-slot `:upgrade-from :from` ↔
6650 // `:versao` precedence gate fired from
6651 // `LayoutInvariants::verify` — the
6652 // `crate::upgrade::validate_upgrade_from_against_versao` call —
6653 // must key its `versao` arg off the typed [`Caixa::versao`]
6654 // accessor, not the raw `&caixa.versao` `&String`-borrow of
6655 // the underlying field.
6656 //
6657 // Same "arg-boundary reads through the lifted accessor"
6658 // discipline as the sibling
6659 // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6660 // pin above on the `:nome`-arg axis of the three typed-name-
6661 // graph self-edge gates — extended here onto the `:versao`-arg
6662 // axis of the substrate's remaining `LayoutInvariants::verify`
6663 // cross-slot arg-carrying call site. Structurally byte-equal
6664 // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
6665 // so both paths coerce to the same `&str`); the pin catches a
6666 // future silent detour (an accessor rebrand that no longer
6667 // shipped the raw slot verbatim — a per-`:edicao` overlay,
6668 // a promotion of `:versao` to a `CaixaVersion` newtype with a
6669 // canonicalizing accessor, an M4 CR-materializer-side pinning
6670 // through a resolver-annotated `:versao-resolved` slot) that
6671 // would silently split the substrate's own precedence gate
6672 // from every peer consumer already routing `:versao` reads
6673 // through the lifted accessor.
6674 //
6675 // The gate fires an `UpgradeViolation { caixa, issue }` when a
6676 // `:upgrade-from` entry's `:from` is not strictly less than
6677 // the top-level `:versao` — the `issue` string names both the
6678 // offending prior version and the current version verbatim,
6679 // so asserting the substring `caixa.versao()` appears in the
6680 // fired diagnostic pins the accessor-routed current-versao
6681 // projection at the arg boundary. A raw-borrow bypass would
6682 // still surface the same bytes today, but the presence of
6683 // this pin makes any future divergence between the accessor's
6684 // return and the raw slot's contents a build-time failure at
6685 // this call site.
6686 use crate::{UpgradeFromEntry, UpgradeInstruction};
6687 let root = PathBuf::from("/tmp/x");
6688 let manifest = root.join("caixa.lisp");
6689 let servico_path = root.join("servicos").join("demo.computeunit.yaml");
6690 let manifest_clone = manifest.clone();
6691 let servico_clone = servico_path.clone();
6692 let layout = StandardLayout::new()
6693 .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
6694 let mut svc = caixa(CaixaKind::Servico);
6695 svc.versao = "0.1.0".into();
6696 svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6697 // `:from` >= current `:versao` — trips the precedence gate the
6698 // `validate_upgrade_from_against_versao` cross-slot call
6699 // enforces. `:load-module` carries no on-disk path so the
6700 // path-existence gate downstream stays inert; the precedence
6701 // gate is what fires. No `:on-state-change` needed because the
6702 // instruction list carries no `:state-change` entry, so the
6703 // sibling `validate_upgrade_from_against_behavior` gate is
6704 // inert too.
6705 svc.upgrade_from = vec![UpgradeFromEntry {
6706 from: "0.2.0".into(),
6707 instructions: vec![UpgradeInstruction::LoadModule {
6708 module: "demo".into(),
6709 }],
6710 }];
6711 let current_versao_via_accessor = svc.versao().to_string();
6712 assert_eq!(
6713 current_versao_via_accessor, "0.1.0",
6714 "the mutated fixture's `:versao` must be observable through \
6715 the accessor before layout verification fires — a drift on \
6716 `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
6717 inequality",
6718 );
6719 let err = layout.verify(&svc, &root).unwrap_err();
6720 let LayoutError::UpgradeViolation {
6721 caixa: c_nome,
6722 issue,
6723 } = err
6724 else {
6725 panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
6726 };
6727 assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
6728 assert!(
6729 issue.contains(¤t_versao_via_accessor),
6730 "the UpgradeViolation's `issue` must quote the current \
6731 `:versao` byte-string verbatim — the cross-slot precedence \
6732 gate's `versao` arg must route through the lifted \
6733 [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
6734 `&String`-borrow of the underlying field. issue: {issue}",
6735 );
6736 }
6737
6738 #[test]
6739 fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
6740 // Wrap-envelope drift-detection pin: every per-axis
6741 // `LayoutError::*Violation { caixa, issue }` envelope fired
6742 // from `LayoutInvariants::verify` must key its offending-caixa
6743 // field off the typed [`Caixa::nome`] accessor's
6744 // `.to_string()` extension, not the raw
6745 // `caixa.nome.clone()` `String::clone()` of the underlying
6746 // field. Structurally byte-equal today (each accessor is
6747 // `pub fn nome(&self) -> &str { &self.nome }`, so
6748 // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
6749 // the same bytes); the pin catches a future silent detour
6750 // (an accessor rebrand that no longer shipped the raw slot
6751 // verbatim — a per-cluster alias table pinned through a
6752 // future `:placement`-scoped slot, the M4 CR materializer's
6753 // per-CR namespace-qualified rewrite, a `:nome-suffix`
6754 // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
6755 // that would silently split the substrate's own layout
6756 // invariant verifier's diagnostic surface from every peer
6757 // caixa-crate consumer that already routes `:nome` reads
6758 // through the lifted accessor (the caixa-mesh 980c059,
6759 // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6760 // caixa-feira ef83332 raw-borrow converges).
6761 //
6762 // Exercises a representative variant on each of the three
6763 // wrap-envelope arm shapes the substrate's per-axis fan-out
6764 // carries: (1) `LayoutError::NomeViolation` (the leading arm
6765 // in the `verify` order — the `:nome` axis's DNS-1123 shape
6766 // gate fires immediately after the manifest-existence gate),
6767 // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
6768 // the kind-coherence family — different envelope shape than
6769 // the struct-variant `*Violation { caixa, issue }` family
6770 // but the same converge target on the `caixa.nome().to_string()`
6771 // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
6772 // sibling tuple-variant on the same kind-coherence family).
6773 // Together they cover the two `LayoutError` envelope shapes
6774 // (struct-variant + tuple-variant) the layout invariants file
6775 // emits on `:nome`-carrying arms.
6776 //
6777 // Peer of the sibling
6778 // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6779 // pin above — extends the "wrap-envelope `caixa:` field
6780 // reads through the lifted accessor" discipline from the
6781 // cross-slot self-edge gates' `parent_nome` arg boundary
6782 // onto the per-axis `LayoutError::*Violation` envelope's
6783 // `caixa:` field boundary.
6784
6785 let root = PathBuf::from("/tmp/x");
6786 let manifest = root.join("caixa.lisp");
6787 let manifest_clone = manifest.clone();
6788 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6789
6790 // (1) `NomeViolation` on the struct-variant envelope: force a
6791 // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
6792 // rejects) and assert the fired envelope's `caixa:` field
6793 // byte-equals `c.nome().to_string()`.
6794 let mut c = caixa(CaixaKind::Biblioteca);
6795 c.nome = "BAD_NAME".into();
6796 c.bibliotecas = vec!["lib/demo.lisp".into()];
6797 let expected_nome_via_accessor = c.nome().to_string();
6798 assert_eq!(
6799 expected_nome_via_accessor, "BAD_NAME",
6800 "the mutated fixture's `:nome` must be observable through \
6801 the accessor before layout verification fires — a drift \
6802 on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
6803 inequality",
6804 );
6805 let err = layout.verify(&c, &root).unwrap_err();
6806 let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
6807 panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
6808 };
6809 assert_eq!(
6810 c_nome, expected_nome_via_accessor,
6811 "the NomeViolation's `caixa` field must equal \
6812 `c.nome().to_string()` — the wrap envelope's per-axis \
6813 projection must route through the lifted [`Caixa::nome`] \
6814 accessor's `.to_string()` extension, not the raw \
6815 `caixa.nome.clone()` `String::clone()` of the underlying \
6816 field",
6817 );
6818
6819 // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
6820 // Binario-kind caixa with an empty `:exe` list fires the
6821 // kind-coherence gate whose payload is a bare `String`, so the
6822 // pattern is `LayoutError::BinarioWithoutExe(String)` rather
6823 // than the struct-variant `{ caixa, issue }` family. The
6824 // converge target is the same — `caixa.nome().to_string()` — but
6825 // the envelope shape is different, so the pin exercises both.
6826 let mut c = caixa(CaixaKind::Binario);
6827 // `:exe` empty is the trigger — the fixture helper defaults
6828 // it to `vec![]`, so no mutation is needed.
6829 c.nome = "binario-demo".into();
6830 let expected_nome_via_accessor = c.nome().to_string();
6831 assert_eq!(
6832 expected_nome_via_accessor, "binario-demo",
6833 "the mutated fixture's `:nome` must be observable through \
6834 the accessor before layout verification fires",
6835 );
6836 let err = layout.verify(&c, &root).unwrap_err();
6837 let LayoutError::BinarioWithoutExe(c_nome) = err else {
6838 panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
6839 };
6840 assert_eq!(
6841 c_nome, expected_nome_via_accessor,
6842 "the BinarioWithoutExe's payload must equal \
6843 `c.nome().to_string()` — the tuple-variant envelope's \
6844 per-axis projection must route through the lifted \
6845 [`Caixa::nome`] accessor's `.to_string()` extension, not \
6846 the raw `caixa.nome.clone()` `String::clone()` of the \
6847 underlying field",
6848 );
6849
6850 // (3) `ServicoWithoutServicos` on the sibling tuple-variant
6851 // envelope: same discipline on the peer kind-coherence
6852 // partition arm. Constructed alongside the Binario arm so any
6853 // future accessor drift lands on both arms in the same pin.
6854 let mut c = caixa(CaixaKind::Servico);
6855 // `:servicos` empty is the trigger — the fixture helper
6856 // defaults it to `vec![]`, so no mutation is needed.
6857 c.nome = "servico-demo".into();
6858 let expected_nome_via_accessor = c.nome().to_string();
6859 assert_eq!(
6860 expected_nome_via_accessor, "servico-demo",
6861 "the mutated fixture's `:nome` must be observable through \
6862 the accessor before layout verification fires",
6863 );
6864 let err = layout.verify(&c, &root).unwrap_err();
6865 let LayoutError::ServicoWithoutServicos(c_nome) = err else {
6866 panic!(
6867 "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
6868 got {err:?}"
6869 );
6870 };
6871 assert_eq!(
6872 c_nome, expected_nome_via_accessor,
6873 "the ServicoWithoutServicos's payload must equal \
6874 `c.nome().to_string()` — same converge discipline as the \
6875 sibling `BinarioWithoutExe` tuple-variant arm above",
6876 );
6877 }
6878
6879 #[test]
6880 fn supervisor_must_not_have_bibliotecas() {
6881 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6882 let root = PathBuf::from("/tmp/x");
6883 let manifest = root.join("caixa.lisp");
6884 let manifest_clone = manifest.clone();
6885 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6886 let mut c = caixa(CaixaKind::Supervisor);
6887 c.estrategia = Some(RestartStrategy::OneForOne);
6888 c.max_restarts = Some(5);
6889 c.bibliotecas = vec!["lib/code.lisp".into()];
6890 c.children = vec![ChildSpec {
6891 caixa: "worker".into(),
6892 versao: "^0.1".into(),
6893 restart: RestartPolicy::Permanent,
6894 }];
6895 let err = layout.verify(&c, &root).unwrap_err();
6896 assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
6897 }
6898
6899 // ── Caixa::validate_restart_window wired into Supervisor verify ─────
6900 //
6901 // Until this wire-up landed `Caixa::validate_restart_window` lived as
6902 // `pub fn` on `Caixa` with full per-arm unit coverage in
6903 // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
6904 // decimal-shaped integer, half-unit minute, leading sign, unknown
6905 // unit, garbage, empty-after-trim) but no production path called it;
6906 // `feira build` silently accepted malformed `:restart-window` and
6907 // `Caixa::supervisor_view` soft-swallowed the parse failure as
6908 // `restart_window: None` (the canonical "no reset" sentinel), turning
6909 // every authoring footgun into a never-reset supervisor far from the
6910 // source caixa.lisp. The following pins fence the layout-pipeline
6911 // wire-up: every layout verify on a structurally-invalid `:restart-
6912 // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
6913 // issue }` envelope before the typed `SupervisorSpec::validate` gate
6914 // sees the laundered `None`.
6915
6916 fn supervisor_with_window(window: Option<&str>) -> Caixa {
6917 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6918 let mut c = caixa(CaixaKind::Supervisor);
6919 c.estrategia = Some(RestartStrategy::OneForOne);
6920 c.max_restarts = Some(5);
6921 c.restart_window = window.map(str::to_string);
6922 c.children = vec![ChildSpec {
6923 caixa: "worker".into(),
6924 versao: "^0.1".into(),
6925 restart: RestartPolicy::Permanent,
6926 }];
6927 c
6928 }
6929
6930 #[test]
6931 fn restart_window_violation_on_fractional_seconds() {
6932 // `"1.5s"` is the canonical fractional-seconds drift footgun the
6933 // shared integer-magnitude codec (1c55a2a) rejects: round-trips
6934 // through `render` as `"1500ms"` on first serialize, breaking
6935 // THEORY.md §V.2.7 render-determinism. Before this wire-up
6936 // `supervisor_view` soft-swallowed the parse error as
6937 // `restart_window: None`, masking the drift as a never-reset
6938 // supervisor.
6939 let root = PathBuf::from("/tmp/x");
6940 let manifest = root.join("caixa.lisp");
6941 let manifest_clone = manifest.clone();
6942 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6943 let c = supervisor_with_window(Some("1.5s"));
6944 let err = layout.verify(&c, &root).unwrap_err();
6945 let LayoutError::RestartWindowViolation { caixa, issue } = err else {
6946 panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
6947 };
6948 assert_eq!(caixa, "demo");
6949 assert!(
6950 issue.contains("1.5s"),
6951 "issue must quote the offending raw value: {issue}",
6952 );
6953 }
6954
6955 #[test]
6956 fn restart_window_violation_on_decimal_shaped_integer() {
6957 // `"1.0s"` — decimal-shaped integer the codec also rejects (a
6958 // canonical authoring form is `"1s"`). Sibling of the fractional
6959 // case; same codec arm.
6960 let root = PathBuf::from("/tmp/x");
6961 let manifest = root.join("caixa.lisp");
6962 let manifest_clone = manifest.clone();
6963 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6964 let c = supervisor_with_window(Some("1.0s"));
6965 let err = layout.verify(&c, &root).unwrap_err();
6966 assert!(
6967 matches!(
6968 err,
6969 LayoutError::RestartWindowViolation { ref caixa, ref issue }
6970 if caixa == "demo" && issue.contains("1.0s")
6971 ),
6972 "got {err:?}",
6973 );
6974 }
6975
6976 #[test]
6977 fn restart_window_violation_on_leading_sign() {
6978 // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
6979 // Canonical form is `"30s"`. Pin both signs separately because
6980 // a future relaxation might accept one but not the other.
6981 for raw in ["+30s", "-30s"] {
6982 let root = PathBuf::from("/tmp/x");
6983 let manifest = root.join("caixa.lisp");
6984 let manifest_clone = manifest.clone();
6985 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6986 let c = supervisor_with_window(Some(raw));
6987 let err = layout.verify(&c, &root).unwrap_err();
6988 assert!(
6989 matches!(
6990 err,
6991 LayoutError::RestartWindowViolation { ref caixa, ref issue }
6992 if caixa == "demo" && issue.contains(raw)
6993 ),
6994 "leading-sign {raw:?} got {err:?}",
6995 );
6996 }
6997 }
6998
6999 #[test]
7000 fn restart_window_violation_on_unknown_unit() {
7001 // `"30x"` — unknown duration unit. The codec admits only
7002 // `ms`/`s`/`m`/`h`.
7003 let root = PathBuf::from("/tmp/x");
7004 let manifest = root.join("caixa.lisp");
7005 let manifest_clone = manifest.clone();
7006 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7007 let c = supervisor_with_window(Some("30x"));
7008 let err = layout.verify(&c, &root).unwrap_err();
7009 assert!(
7010 matches!(
7011 err,
7012 LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
7013 ),
7014 "got {err:?}",
7015 );
7016 }
7017
7018 #[test]
7019 fn restart_window_violation_on_garbage() {
7020 // `"abc"` — pure garbage. The codec's parse fails before the
7021 // unit dispatch; the wrap envelope still surfaces the
7022 // self-locating diagnostic at the source.
7023 let root = PathBuf::from("/tmp/x");
7024 let manifest = root.join("caixa.lisp");
7025 let manifest_clone = manifest.clone();
7026 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7027 let c = supervisor_with_window(Some("abc"));
7028 let err = layout.verify(&c, &root).unwrap_err();
7029 assert!(
7030 matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7031 "got {err:?}",
7032 );
7033 }
7034
7035 #[test]
7036 fn restart_window_violation_on_empty_string() {
7037 // `""` — empty after trim. The shared codec's digit-only gate
7038 // refuses an empty magnitude. Distinguished here from the
7039 // `None` ("omit the slot") canonical authoring shape: an empty
7040 // string is an authored-but-empty slot, never the author's
7041 // intent.
7042 let root = PathBuf::from("/tmp/x");
7043 let manifest = root.join("caixa.lisp");
7044 let manifest_clone = manifest.clone();
7045 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7046 let c = supervisor_with_window(Some(""));
7047 let err = layout.verify(&c, &root).unwrap_err();
7048 assert!(
7049 matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7050 "got {err:?}",
7051 );
7052 }
7053
7054 #[test]
7055 fn verify_accepts_supervisor_without_restart_window() {
7056 // `None` is the canonical "omit the slot to express no reset"
7057 // shape — never reaches the codec, validates cleanly.
7058 let root = PathBuf::from("/tmp/x");
7059 let manifest = root.join("caixa.lisp");
7060 let manifest_clone = manifest.clone();
7061 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7062 let c = supervisor_with_window(None);
7063 layout.verify(&c, &root).unwrap();
7064 }
7065
7066 #[test]
7067 fn verify_accepts_supervisor_with_canonical_restart_window() {
7068 // Every canonical form the shared codec round-trips losslessly
7069 // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
7070 // `"1h"`. Pin every form so a future tightening of the codec's
7071 // accepted set surfaces here as a test failure.
7072 for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
7073 let root = PathBuf::from("/tmp/x");
7074 let manifest = root.join("caixa.lisp");
7075 let manifest_clone = manifest.clone();
7076 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7077 let c = supervisor_with_window(Some(form));
7078 layout
7079 .verify(&c, &root)
7080 .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
7081 }
7082 }
7083
7084 #[test]
7085 fn restart_window_violation_fires_before_supervisor_view_validate() {
7086 // Diagnostic-precedence pin: a Supervisor with a malformed
7087 // `:restart-window` AND a typed-shape defect on the typed view
7088 // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
7089 // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
7090 // diagnostic first — the narrower self-locating gate wins. Until
7091 // this wire-up landed `supervisor_view` would silently launder
7092 // the malformed `:restart-window` to `None` and then the typed
7093 // view's `ZeroMaxRestarts` gate would surface, masking the
7094 // raw-string footgun.
7095 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7096 let root = PathBuf::from("/tmp/x");
7097 let manifest = root.join("caixa.lisp");
7098 let manifest_clone = manifest.clone();
7099 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7100 let mut c = caixa(CaixaKind::Supervisor);
7101 c.estrategia = Some(RestartStrategy::OneForOne);
7102 c.max_restarts = Some(0);
7103 c.restart_window = Some("1.5s".into());
7104 c.children = vec![ChildSpec {
7105 caixa: "worker".into(),
7106 versao: "^0.1".into(),
7107 restart: RestartPolicy::Permanent,
7108 }];
7109 let err = layout.verify(&c, &root).unwrap_err();
7110 assert!(
7111 matches!(err, LayoutError::RestartWindowViolation { .. }),
7112 "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
7113 );
7114 }
7115
7116 #[test]
7117 fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
7118 // Order pin: a non-Supervisor caixa with a malformed
7119 // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
7120 // (the kind-coherence gate at the top of verify) before the
7121 // raw-string parse gate inside the Supervisor branch — because
7122 // `:restart-window` is foreign to non-Supervisor kinds, the
7123 // kind-coherence diagnostic is the load-bearing one. Mirrors
7124 // the existing `nome_violation_on_*` ordering tests that fence
7125 // the precedence between universal and kind-specific gates.
7126 let root = PathBuf::from("/tmp/x");
7127 let manifest = root.join("caixa.lisp");
7128 let default_lib = root.join("lib").join("demo.lisp");
7129 let layout =
7130 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
7131 let mut c = caixa(CaixaKind::Biblioteca);
7132 c.restart_window = Some("1.5s".into());
7133 let err = layout.verify(&c, &root).unwrap_err();
7134 assert!(
7135 matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
7136 "got {err:?} — kind-coherence must fire before RestartWindowViolation",
7137 );
7138 }
7139
7140 #[test]
7141 fn restart_window_violation_diagnostic_carries_offending_value() {
7142 // Diagnostic-shape pin: the wrap envelope's `issue` carries the
7143 // codec's parser-shaped reason verbatim (which names the
7144 // offending raw value), so the author can grep their caixa.lisp
7145 // for `:restart-window "<value>"` and fix in one edit. Mirrors
7146 // `nome_violation_*_carries_offending_*` shape pins.
7147 let root = PathBuf::from("/tmp/x");
7148 let manifest = root.join("caixa.lisp");
7149 let manifest_clone = manifest.clone();
7150 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7151 let c = supervisor_with_window(Some("0.5m"));
7152 let err = layout.verify(&c, &root).unwrap_err();
7153 let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7154 panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7155 };
7156 assert_eq!(caixa, "demo");
7157 assert!(
7158 issue.contains("0.5m"),
7159 "issue must quote the offending raw value verbatim: {issue}",
7160 );
7161 assert!(
7162 !issue.is_empty(),
7163 "issue must carry the codec's parser-shaped reason",
7164 );
7165 }
7166
7167 // ── Aplicacao layout tests ──────────────────────────────────────────
7168
7169 #[test]
7170 fn aplicacao_must_have_membros() {
7171 use crate::{Membro, Placement, PlacementStrategy};
7172 let root = PathBuf::from("/tmp/x");
7173 let manifest = root.join("caixa.lisp");
7174 let manifest_clone = manifest.clone();
7175 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7176 let mut c = caixa(CaixaKind::Aplicacao);
7177 c.placement = Some(Placement {
7178 estrategia: PlacementStrategy::Replicated,
7179 clusters: vec!["rio".into()],
7180 affinity: None,
7181 shard_key: None,
7182 });
7183 // No membros → fails
7184 let err = layout.verify(&c, &root).unwrap_err();
7185 assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
7186
7187 // With membros → passes
7188 c.membros = vec![Membro {
7189 caixa: "service-a".into(),
7190 versao: "^0.1".into(),
7191 }];
7192 layout.verify(&c, &root).unwrap();
7193 }
7194
7195 #[test]
7196 fn aplicacao_self_referential_membro_is_violation() {
7197 // An Aplicacao whose `:membros` names its own `:nome` is a
7198 // one-node lacre-closure recursion. The cross-slot gate fires
7199 // at verify time, surfacing as an AplicacaoViolation that
7200 // names the offending aplicacao — not at lacre-resolve time
7201 // far from source. The `caixa()` helper's `:nome` is "demo".
7202 // Peer of `supervisor_self_referential_child_is_violation`
7203 // on the supervision-tree axis.
7204 use crate::{Membro, Placement, PlacementStrategy};
7205 let root = PathBuf::from("/tmp/x");
7206 let manifest = root.join("caixa.lisp");
7207 let manifest_clone = manifest.clone();
7208 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7209 let mut c = caixa(CaixaKind::Aplicacao);
7210 c.placement = Some(Placement {
7211 estrategia: PlacementStrategy::Replicated,
7212 clusters: vec!["rio".into()],
7213 affinity: None,
7214 shard_key: None,
7215 });
7216 c.membros = vec![
7217 Membro {
7218 caixa: "service-a".into(),
7219 versao: "^0.1".into(),
7220 },
7221 Membro {
7222 caixa: "demo".into(),
7223 versao: "^0.1".into(),
7224 },
7225 ];
7226 let err = layout.verify(&c, &root).unwrap_err();
7227 let LayoutError::AplicacaoViolation { caixa, issue } = err else {
7228 panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
7229 };
7230 assert_eq!(caixa, "demo");
7231 assert!(
7232 issue.contains("demo") && issue.contains("lists itself"),
7233 "issue must name the self-membering aplicacao, got {issue:?}"
7234 );
7235 }
7236
7237 #[test]
7238 fn aplicacao_distinct_membros_pass_self_membership_gate() {
7239 // Positive control: an Aplicacao whose membros are all distinct
7240 // from its own `:nome` verifies cleanly.
7241 use crate::{Membro, Placement, PlacementStrategy};
7242 let root = PathBuf::from("/tmp/x");
7243 let manifest = root.join("caixa.lisp");
7244 let manifest_clone = manifest.clone();
7245 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7246 let mut c = caixa(CaixaKind::Aplicacao);
7247 c.placement = Some(Placement {
7248 estrategia: PlacementStrategy::Replicated,
7249 clusters: vec!["rio".into()],
7250 affinity: None,
7251 shard_key: None,
7252 });
7253 c.membros = vec![
7254 Membro {
7255 caixa: "service-a".into(),
7256 versao: "^0.1".into(),
7257 },
7258 Membro {
7259 caixa: "service-b".into(),
7260 versao: "^0.1".into(),
7261 },
7262 ];
7263 layout.verify(&c, &root).unwrap();
7264 }
7265
7266 #[test]
7267 fn aplicacao_self_membership_fires_after_view_validate() {
7268 // Diagnostic-precedence pin: a self-referential membro alongside
7269 // a duplicate-:caixa shape surfaces the more-fundamental
7270 // `MembroDuplicate` (from `view.validate()`) first; only when the
7271 // per-membros shape diagnostics pass does the cross-slot
7272 // self-membership gate fire. Mirrors the ordering pin
7273 // `supervisor_self_referential_child_is_violation` carries on
7274 // the peer supervision-tree axis (`view.validate()` runs first,
7275 // then the cross-slot gate). Without this ordering a future
7276 // refactor that swaps the two calls would silently mask the
7277 // narrower per-membro defect.
7278 use crate::{Membro, Placement, PlacementStrategy};
7279 let root = PathBuf::from("/tmp/x");
7280 let manifest = root.join("caixa.lisp");
7281 let manifest_clone = manifest.clone();
7282 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7283 let mut c = caixa(CaixaKind::Aplicacao);
7284 c.placement = Some(Placement {
7285 estrategia: PlacementStrategy::Replicated,
7286 clusters: vec!["rio".into()],
7287 affinity: None,
7288 shard_key: None,
7289 });
7290 c.membros = vec![
7291 Membro {
7292 caixa: "service-a".into(),
7293 versao: "^0.1".into(),
7294 },
7295 Membro {
7296 caixa: "service-a".into(),
7297 versao: "^0.2".into(),
7298 },
7299 Membro {
7300 caixa: "demo".into(),
7301 versao: "^0.1".into(),
7302 },
7303 ];
7304 let err = layout.verify(&c, &root).unwrap_err();
7305 let LayoutError::AplicacaoViolation { issue, .. } = err else {
7306 panic!("expected AplicacaoViolation, got {err:?}");
7307 };
7308 // The per-membros duplicate diagnostic (from view.validate())
7309 // surfaces ahead of the cross-slot self-membership gate, so the
7310 // `service-a` duplicate is named — not the `demo` self-reference.
7311 assert!(
7312 issue.contains("service-a") && issue.contains("more than once"),
7313 "duplicate-:caixa diagnostic must surface before self-membership gate, \
7314 got {issue:?}"
7315 );
7316 }
7317
7318 #[test]
7319 fn mesh_slots_on_servico_rejected() {
7320 // The canonical real-world footgun: an author adds :entrada to a
7321 // :kind Servico expecting it to expose ingress. aplicacao_view
7322 // returns None for Servico, so the slot is the manifest's
7323 // "ignored otherwise" — never validated, never rendered. The
7324 // kind-coherence gate rejects it at build time (before the
7325 // :servicos existence loop), naming the offending slot + kind.
7326 use crate::Entrada;
7327 let root = PathBuf::from("/tmp/x");
7328 let manifest = root.join("caixa.lisp");
7329 let manifest_clone = manifest.clone();
7330 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7331 let mut c = caixa(CaixaKind::Servico);
7332 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7333 c.entrada = Some(Entrada {
7334 host: "demo.example.com".into(),
7335 para: "demo".into(),
7336 paths: vec![],
7337 port: 8080,
7338 });
7339 let err = layout.verify(&c, &root).unwrap_err();
7340 match err {
7341 LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
7342 assert_eq!(caixa, "demo");
7343 assert_eq!(kind, CaixaKind::Servico);
7344 assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
7345 }
7346 other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7347 }
7348 }
7349
7350 #[test]
7351 fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
7352 // All five mesh slots declared on a Biblioteca → the diagnostic
7353 // enumerates them in canonical declaration order, deterministic
7354 // across runs. The gate fires on declared-ness only (the values
7355 // need not be a *valid* AplicacaoSpec — aplicacao_view is never
7356 // called for a non-Aplicacao kind).
7357 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
7358 let root = PathBuf::from("/tmp/x");
7359 let manifest = root.join("caixa.lisp");
7360 let manifest_clone = manifest.clone();
7361 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7362 let mut c = caixa(CaixaKind::Biblioteca);
7363 c.membros = vec![Membro {
7364 caixa: "a".into(),
7365 versao: "^0.1".into(),
7366 }];
7367 c.contratos = vec![WitContract {
7368 de: "a".into(),
7369 para: "a".into(),
7370 wit: "wasi:http/proxy".into(),
7371 endpoint: Some("/x".into()),
7372 subject: None,
7373 slot: None,
7374 }];
7375 c.politicas = Some(MeshPolicy::default());
7376 c.placement = Some(Placement {
7377 estrategia: PlacementStrategy::Replicated,
7378 clusters: vec!["rio".into()],
7379 affinity: None,
7380 shard_key: None,
7381 });
7382 c.entrada = Some(Entrada {
7383 host: "x.example.com".into(),
7384 para: "a".into(),
7385 paths: vec![],
7386 port: 8080,
7387 });
7388 let err = layout.verify(&c, &root).unwrap_err();
7389 match err {
7390 LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
7391 assert_eq!(
7392 slots,
7393 format!(
7394 "{} {} {} {} {}",
7395 crate::render::M3_AUTHOR_KEY_MEMBROS,
7396 crate::render::M3_AUTHOR_KEY_CONTRATOS,
7397 crate::render::M3_AUTHOR_KEY_POLITICAS,
7398 crate::render::M3_AUTHOR_KEY_PLACEMENT,
7399 crate::render::M3_AUTHOR_KEY_ENTRADA,
7400 )
7401 );
7402 }
7403 other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7404 }
7405 }
7406
7407 #[test]
7408 fn servico_without_mesh_slots_still_verifies() {
7409 // Pass-after control: a well-formed Servico carrying no mesh
7410 // slots must remain accepted — the gate keys off declared-ness,
7411 // so it must not over-fire on the common case.
7412 let root = PathBuf::from("/tmp/x");
7413 let servico = root.join("servicos/demo.computeunit.yaml");
7414 let manifest = root.join("caixa.lisp");
7415 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7416 let mut c = caixa(CaixaKind::Servico);
7417 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7418 layout.verify(&c, &root).unwrap();
7419 }
7420
7421 #[test]
7422 fn supervisor_slots_on_servico_rejected() {
7423 // Mirror of `mesh_slots_on_servico_rejected` on the
7424 // supervisor-tree slot set: an author adds `:children` to a
7425 // `:kind Servico` expecting it to spawn workers. supervisor_view
7426 // returns None for Servico, so the slot is the manifest's
7427 // "ignored otherwise" — never validated, never reconciled. The
7428 // kind-coherence gate rejects it at build time (before the
7429 // :servicos existence loop), naming the offending slot + kind.
7430 use crate::{ChildSpec, RestartPolicy};
7431 let root = PathBuf::from("/tmp/x");
7432 let manifest = root.join("caixa.lisp");
7433 let manifest_clone = manifest.clone();
7434 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7435 let mut c = caixa(CaixaKind::Servico);
7436 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7437 c.children = vec![ChildSpec {
7438 caixa: "worker".into(),
7439 versao: "^0.1".into(),
7440 restart: RestartPolicy::Permanent,
7441 }];
7442 let err = layout.verify(&c, &root).unwrap_err();
7443 match err {
7444 LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7445 assert_eq!(caixa, "demo");
7446 assert_eq!(kind, CaixaKind::Servico);
7447 assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
7448 }
7449 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7450 }
7451 }
7452
7453 #[test]
7454 fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
7455 // All four supervisor slots declared on a Biblioteca → the
7456 // diagnostic enumerates them in canonical declaration order
7457 // (`:estrategia` → `:max-restarts` → `:restart-window` →
7458 // `:children`), deterministic across runs. The gate fires on
7459 // declared-ness only (the values need not be a *valid*
7460 // SupervisorSpec — supervisor_view is never called for a
7461 // non-Supervisor kind). Mirror of
7462 // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
7463 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7464 let root = PathBuf::from("/tmp/x");
7465 let manifest = root.join("caixa.lisp");
7466 let manifest_clone = manifest.clone();
7467 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7468 let mut c = caixa(CaixaKind::Biblioteca);
7469 c.estrategia = Some(RestartStrategy::OneForOne);
7470 c.max_restarts = Some(5);
7471 c.restart_window = Some("60s".into());
7472 c.children = vec![ChildSpec {
7473 caixa: "worker".into(),
7474 versao: "^0.1".into(),
7475 restart: RestartPolicy::Permanent,
7476 }];
7477 let err = layout.verify(&c, &root).unwrap_err();
7478 match err {
7479 LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
7480 assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
7481 }
7482 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7483 }
7484 }
7485
7486 #[test]
7487 fn aplicacao_with_supervisor_slots_rejected() {
7488 // Cross-kind pin: an Aplicacao (the other no-code orchestrator
7489 // kind) that declares a supervisor slot is rejected by the
7490 // supervisor-slot gate, just as a Supervisor declaring a mesh
7491 // slot is rejected by the mesh-slot gate — the two kind ↔ slot
7492 // coherence gates are symmetric and mutually exclusive. The
7493 // gate fires before the Aplicacao typed-graph validation, so
7494 // the diagnostic names the foreign supervisor slot rather than
7495 // a downstream AplicacaoViolation.
7496 use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
7497 let root = PathBuf::from("/tmp/x");
7498 let manifest = root.join("caixa.lisp");
7499 let manifest_clone = manifest.clone();
7500 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7501 let mut c = caixa(CaixaKind::Aplicacao);
7502 c.membros = vec![Membro {
7503 caixa: "service-a".into(),
7504 versao: "^0.1".into(),
7505 }];
7506 c.placement = Some(Placement {
7507 estrategia: PlacementStrategy::Replicated,
7508 clusters: vec!["rio".into()],
7509 affinity: None,
7510 shard_key: None,
7511 });
7512 c.estrategia = Some(RestartStrategy::OneForAll);
7513 let err = layout.verify(&c, &root).unwrap_err();
7514 match err {
7515 LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7516 assert_eq!(caixa, "demo");
7517 assert_eq!(kind, CaixaKind::Aplicacao);
7518 assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
7519 }
7520 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7521 }
7522 }
7523
7524 #[test]
7525 fn servico_without_supervisor_slots_still_verifies() {
7526 // Pass-after control: a well-formed Servico carrying no
7527 // supervisor slots must remain accepted — the gate keys off
7528 // declared-ness, so it must not over-fire on the common case.
7529 let root = PathBuf::from("/tmp/x");
7530 let servico = root.join("servicos/demo.computeunit.yaml");
7531 let manifest = root.join("caixa.lisp");
7532 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7533 let mut c = caixa(CaixaKind::Servico);
7534 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7535 layout.verify(&c, &root).unwrap();
7536 }
7537
7538 #[test]
7539 fn servico_slots_on_biblioteca_rejected() {
7540 // Mirror of `mesh_slots_on_servico_rejected` /
7541 // `supervisor_slots_on_servico_rejected` on the M2
7542 // Servico-runtime slot set: an author adds `:limits` to a
7543 // `:kind Biblioteca` expecting per-process sandboxing. The
7544 // caixa-helm / caixa-flux renderers gate on `require_kind(_,
7545 // Servico)`, so the slot is the manifest's "ignored otherwise" —
7546 // never rendered into any artifact. The kind-coherence gate
7547 // rejects it at build time (before the M2 validate blocks),
7548 // naming the offending slot + kind.
7549 use crate::LimitsSpec;
7550 let root = PathBuf::from("/tmp/x");
7551 let manifest = root.join("caixa.lisp");
7552 let lib = root.join("lib").join("demo.lisp");
7553 let manifest_clone = manifest.clone();
7554 let layout =
7555 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
7556 let mut c = caixa(CaixaKind::Biblioteca);
7557 c.limits = Some(LimitsSpec {
7558 fuel: Some(1_000_000),
7559 ..Default::default()
7560 });
7561 let err = layout.verify(&c, &root).unwrap_err();
7562 match err {
7563 LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7564 assert_eq!(caixa, "demo");
7565 assert_eq!(kind, CaixaKind::Biblioteca);
7566 assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
7567 }
7568 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7569 }
7570 }
7571
7572 #[test]
7573 fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
7574 // All three M2 slots declared on a Biblioteca → the diagnostic
7575 // enumerates them in canonical declaration order (`:limits` →
7576 // `:behavior` → `:upgrade-from`), deterministic across runs. The
7577 // gate fires on declared-ness only (the values need not pass the
7578 // M2 validate blocks — those run only after the kind-coherence
7579 // gate, and never for a non-Servico declared-slot caixa). Mirror
7580 // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
7581 use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7582 let root = PathBuf::from("/tmp/x");
7583 let manifest = root.join("caixa.lisp");
7584 let manifest_clone = manifest.clone();
7585 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7586 let mut c = caixa(CaixaKind::Biblioteca);
7587 c.limits = Some(LimitsSpec {
7588 fuel: Some(1_000_000),
7589 ..Default::default()
7590 });
7591 c.behavior = Some(BehaviorSpec {
7592 on_init: Some(PathBuf::from("lib/init.lisp")),
7593 ..Default::default()
7594 });
7595 c.upgrade_from = vec![UpgradeFromEntry {
7596 from: "0.1.0".into(),
7597 instructions: vec![UpgradeInstruction::Restart],
7598 }];
7599 let err = layout.verify(&c, &root).unwrap_err();
7600 match err {
7601 LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
7602 assert_eq!(
7603 slots,
7604 format!(
7605 "{} {} {}",
7606 crate::render::M2_AUTHOR_KEY_LIMITS,
7607 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
7608 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
7609 )
7610 );
7611 }
7612 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7613 }
7614 }
7615
7616 #[test]
7617 fn aplicacao_with_servico_slots_rejected() {
7618 // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
7619 // an Aplicacao that declares an M2 Servico-runtime slot is
7620 // rejected by the Servico-slot gate, just as a Supervisor
7621 // declaring a mesh slot is rejected by the mesh-slot gate — the
7622 // three kind ↔ slot coherence gates are symmetric and mutually
7623 // exclusive. The gate fires before the Aplicacao typed-graph
7624 // validation, so the diagnostic names the foreign M2 slot rather
7625 // than a downstream AplicacaoViolation about missing :membros.
7626 use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
7627 let root = PathBuf::from("/tmp/x");
7628 let manifest = root.join("caixa.lisp");
7629 let manifest_clone = manifest.clone();
7630 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7631 let mut c = caixa(CaixaKind::Aplicacao);
7632 c.membros = vec![Membro {
7633 caixa: "service-a".into(),
7634 versao: "^0.1".into(),
7635 }];
7636 c.placement = Some(Placement {
7637 estrategia: PlacementStrategy::Replicated,
7638 clusters: vec!["rio".into()],
7639 affinity: None,
7640 shard_key: None,
7641 });
7642 c.upgrade_from = vec![UpgradeFromEntry {
7643 from: "0.1.0".into(),
7644 instructions: vec![UpgradeInstruction::Restart],
7645 }];
7646 let err = layout.verify(&c, &root).unwrap_err();
7647 match err {
7648 LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7649 assert_eq!(caixa, "demo");
7650 assert_eq!(kind, CaixaKind::Aplicacao);
7651 assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
7652 }
7653 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7654 }
7655 }
7656
7657 #[test]
7658 fn servico_with_servico_slots_still_verifies() {
7659 // Pass-after control: a well-formed Servico carrying all three M2
7660 // slots must remain accepted — the gate is guarded by `kind !=
7661 // Servico`, so it must not over-fire on the kind these slots
7662 // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
7663 // still_verifies` on the legitimate-declaration axis.
7664 use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7665 let root = PathBuf::from("/tmp/x");
7666 let manifest = root.join("caixa.lisp");
7667 let svc = root.join("servicos/demo.computeunit.yaml");
7668 let init = root.join("lib/init.lisp");
7669 let layout =
7670 StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
7671 let mut c = caixa(CaixaKind::Servico);
7672 c.versao = "0.2.0".into();
7673 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7674 c.limits = Some(LimitsSpec {
7675 fuel: Some(1_000_000),
7676 ..Default::default()
7677 });
7678 c.behavior = Some(BehaviorSpec {
7679 on_init: Some(PathBuf::from("lib/init.lisp")),
7680 ..Default::default()
7681 });
7682 c.upgrade_from = vec![UpgradeFromEntry {
7683 from: "0.1.0".into(),
7684 instructions: vec![UpgradeInstruction::Restart],
7685 }];
7686 layout.verify(&c, &root).unwrap();
7687 }
7688
7689 #[test]
7690 fn aplicacao_must_not_have_bibliotecas() {
7691 use crate::{Membro, Placement, PlacementStrategy};
7692 let root = PathBuf::from("/tmp/x");
7693 let manifest = root.join("caixa.lisp");
7694 let manifest_clone = manifest.clone();
7695 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7696 let mut c = caixa(CaixaKind::Aplicacao);
7697 c.bibliotecas = vec!["lib/code.lisp".into()];
7698 c.membros = vec![Membro {
7699 caixa: "x".into(),
7700 versao: "^0.1".into(),
7701 }];
7702 c.placement = Some(Placement {
7703 estrategia: PlacementStrategy::Replicated,
7704 clusters: vec!["rio".into()],
7705 affinity: None,
7706 shard_key: None,
7707 });
7708 let err = layout.verify(&c, &root).unwrap_err();
7709 assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
7710 }
7711
7712 #[test]
7713 fn acao_must_not_have_bibliotecas() {
7714 // Mirror of `supervisor_must_not_have_bibliotecas` /
7715 // `aplicacao_must_not_have_bibliotecas` on the third no-code
7716 // kind. `has_code` fires before the `:ci`-presence gates below
7717 // it, so this must surface `AcaoOwnsCode` even though the
7718 // caixa also lacks a `:ci` slot (which would otherwise surface
7719 // as `MissingCi`) — the more-fundamental "this kind runs no
7720 // code at all" diagnostic wins.
7721 let root = PathBuf::from("/tmp/x");
7722 let manifest = root.join("caixa.lisp");
7723 let manifest_clone = manifest.clone();
7724 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7725 let mut c = caixa(CaixaKind::Acao);
7726 c.bibliotecas = vec!["lib/code.lisp".into()];
7727 let err = layout.verify(&c, &root).unwrap_err();
7728 assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
7729 }
7730
7731 #[test]
7732 fn acao_without_ci_errors() {
7733 // Mirror of `binario_without_exe_errors` on the fifth required-
7734 // slot axis.
7735 let root = PathBuf::from("/tmp/x");
7736 let manifest = root.join("caixa.lisp");
7737 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7738 let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
7739 assert!(matches!(err, LayoutError::MissingCi(_)));
7740 }
7741
7742 #[test]
7743 fn acao_with_ci_passes() {
7744 let root = PathBuf::from("/tmp/x");
7745 let manifest = root.join("caixa.lisp");
7746 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7747 let mut c = caixa(CaixaKind::Acao);
7748 c.ci = Some(canteiro_types::CiRun {
7749 workspace: "pleme-io".into(),
7750 repo: "caixa".into(),
7751 nodes: vec![],
7752 });
7753 layout
7754 .verify(&c, &root)
7755 .expect("an Acao caixa with a declared :ci slot passes layout verify");
7756 }
7757
7758 #[test]
7759 fn acao_with_cyclic_ci_rejected_at_layout() {
7760 // Layout-side wire-up pin on the compound
7761 // [`crate::Caixa::validate_acao_shape`] gate: a `:kind Acao`
7762 // caixa declaring a structurally illegal `:ci` (here — a
7763 // minimal two-node cycle `a → b → a`, one of the three
7764 // `canteiro_types::DecomposeError` arms
7765 // [`crate::decompose_ci`] refuses) surfaces
7766 // [`LayoutError::AcaoViolation`] at `feira build` time rather
7767 // than passing the layout gate silently and deferring the
7768 // diagnostic to [`caixa_actions::validate`] at renderer time.
7769 //
7770 // Pre-lift the layout pipeline only checked `:ci` *presence*
7771 // via [`LayoutError::MissingCi`]; the decompose gate lived
7772 // only wired open-coded at
7773 // [`caixa_actions::validate`] via the substrate-canonical
7774 // [`crate::require_acao_view`] compound helper. This wire-up
7775 // pin locks the new layout-side compound-shape gate in place
7776 // — a future regression that dropped the `if
7777 // caixa.kind().is_acao() { validate_acao_shape() }` block or
7778 // relaxed the diagnostic surface trips here at caixa-core
7779 // build time. Sibling in shape to the peer
7780 // [`aplicacao_must_not_have_bibliotecas`] /
7781 // [`supervisor_must_not_have_bibliotecas`] /
7782 // [`acao_must_not_have_bibliotecas`] layout wire-up pins on
7783 // the sibling per-kind shape gates.
7784 let root = PathBuf::from("/tmp/x");
7785 let manifest = root.join("caixa.lisp");
7786 let manifest_clone = manifest.clone();
7787 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7788 let mut c = caixa(CaixaKind::Acao);
7789 c.ci = Some(canteiro_types::CiRun {
7790 workspace: "pleme-io".into(),
7791 repo: "caixa".into(),
7792 nodes: vec![
7793 canteiro_types::CiNode::new(
7794 "a",
7795 canteiro_types::EnvClass::None,
7796 canteiro_types::ActionRef {
7797 name: "a".into(),
7798 command: "true".into(),
7799 args: vec![],
7800 },
7801 vec!["b".into()],
7802 ),
7803 canteiro_types::CiNode::new(
7804 "b",
7805 canteiro_types::EnvClass::None,
7806 canteiro_types::ActionRef {
7807 name: "b".into(),
7808 command: "true".into(),
7809 args: vec![],
7810 },
7811 vec!["a".into()],
7812 ),
7813 ],
7814 });
7815 let err = layout.verify(&c, &root).unwrap_err();
7816 match err {
7817 LayoutError::AcaoViolation { caixa, issue } => {
7818 assert_eq!(caixa, "demo");
7819 assert!(
7820 issue.contains("decompose"),
7821 "AcaoViolation issue must name the decompose axis (got: {issue:?})",
7822 );
7823 assert!(
7824 issue.contains("demo"),
7825 "AcaoViolation issue must name the offending caixa nome via the folded \
7826 CiDecomposeFailure Display (got: {issue:?})",
7827 );
7828 }
7829 other => panic!("expected AcaoViolation on a cyclic :ci, got {other:?}"),
7830 }
7831 }
7832
7833 #[test]
7834 fn ci_on_non_acao_errors() {
7835 // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
7836 // on the Acao-only `:ci` axis — declaring `:ci` on any other
7837 // kind is the same "silently ignored" footgun the sibling
7838 // mesh-/supervisor-/servico-slot gates already close.
7839 let root = PathBuf::from("/tmp/x");
7840 let manifest = root.join("caixa.lisp");
7841 let manifest_clone = manifest.clone();
7842 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7843 let mut c = caixa(CaixaKind::Biblioteca);
7844 c.ci = Some(canteiro_types::CiRun {
7845 workspace: "pleme-io".into(),
7846 repo: "caixa".into(),
7847 nodes: vec![],
7848 });
7849 let err = layout.verify(&c, &root).unwrap_err();
7850 match err {
7851 LayoutError::CiOnNonAcao { caixa, kind } => {
7852 assert_eq!(caixa, "demo");
7853 assert_eq!(kind, CaixaKind::Biblioteca);
7854 }
7855 other => panic!("expected CiOnNonAcao, got {other:?}"),
7856 }
7857 }
7858
7859 // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
7860
7861 #[test]
7862 fn biblioteca_with_exe_rejected() {
7863 // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
7864 // `:exe` is the "I added a CLI to my library" footgun — the nix
7865 // flake renderer for Binario gates on `require_kind(_, Binario)`,
7866 // so on a Biblioteca the `:exe` path is silently dropped past
7867 // the layout's path-existence check (no executable target is
7868 // ever generated). The diagnostic names the offending kind +
7869 // slot verbatim so the author can grep their caixa.lisp for
7870 // `:exe` and fix in one edit (drop the slot or change
7871 // `:kind Biblioteca` → `:kind Binario`).
7872 let root = PathBuf::from("/tmp/x");
7873 let manifest = root.join("caixa.lisp");
7874 let lib = root.join("lib").join("demo.lisp");
7875 let exe_path = root.join("exe").join("tool");
7876 let layout = StandardLayout::new()
7877 .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
7878 let mut c = caixa(CaixaKind::Biblioteca);
7879 c.exe = vec!["exe/tool".into()];
7880 let err = layout.verify(&c, &root).unwrap_err();
7881 match err {
7882 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7883 assert_eq!(caixa, "demo");
7884 assert_eq!(kind, CaixaKind::Biblioteca);
7885 assert_eq!(slots, ":exe");
7886 }
7887 other => panic!("expected ForeignCodeSlot, got {other:?}"),
7888 }
7889 }
7890
7891 #[test]
7892 fn biblioteca_with_servicos_rejected() {
7893 // Symmetric to `biblioteca_with_exe_rejected` on the
7894 // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
7895 // computeunit silently passed validate and the daemon's
7896 // ComputeUnit / lareira chart never materialized (caixa-helm /
7897 // caixa-flux gate emission on `require_kind(_, Servico)`).
7898 let root = PathBuf::from("/tmp/x");
7899 let manifest = root.join("caixa.lisp");
7900 let lib = root.join("lib").join("demo.lisp");
7901 let svc = root.join("servicos").join("demo.computeunit.yaml");
7902 let layout =
7903 StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
7904 let mut c = caixa(CaixaKind::Biblioteca);
7905 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7906 let err = layout.verify(&c, &root).unwrap_err();
7907 match err {
7908 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7909 assert_eq!(caixa, "demo");
7910 assert_eq!(kind, CaixaKind::Biblioteca);
7911 assert_eq!(slots, ":servicos");
7912 }
7913 other => panic!("expected ForeignCodeSlot, got {other:?}"),
7914 }
7915 }
7916
7917 #[test]
7918 fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
7919 // Both foreign code slots declared on a Biblioteca → the
7920 // diagnostic enumerates them in canonical declaration order
7921 // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
7922 // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
7923 // pins on the peer kind ↔ slot algebra axes; drift in the
7924 // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
7925 // here.
7926 let root = PathBuf::from("/tmp/x");
7927 let manifest = root.join("caixa.lisp");
7928 let manifest_clone = manifest.clone();
7929 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7930 let mut c = caixa(CaixaKind::Biblioteca);
7931 c.exe = vec!["exe/tool".into()];
7932 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7933 let err = layout.verify(&c, &root).unwrap_err();
7934 match err {
7935 LayoutError::ForeignCodeSlot { slots, .. } => {
7936 assert_eq!(slots, ":exe :servicos");
7937 }
7938 other => panic!("expected ForeignCodeSlot, got {other:?}"),
7939 }
7940 }
7941
7942 #[test]
7943 fn binario_with_servicos_rejected() {
7944 // The peer footgun on the Binario kind: declaring a Servico
7945 // computeunit on a `:kind Binario` caixa. The caixa-helm /
7946 // caixa-flux renderers gate on `require_kind(_, Servico)`, so
7947 // the `:servicos` slot vanishes past the layout's path-
7948 // existence check — no ComputeUnit, no Helm chart. `:exe` stays
7949 // valid (Binario's native code surface), so the kind-coherence
7950 // diagnostic targets only `:servicos`.
7951 let root = PathBuf::from("/tmp/x");
7952 let manifest = root.join("caixa.lisp");
7953 let exe_path = root.join("exe").join("tool");
7954 let svc = root.join("servicos").join("demo.computeunit.yaml");
7955 let layout = StandardLayout::new()
7956 .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
7957 let mut c = caixa(CaixaKind::Binario);
7958 c.exe = vec!["exe/tool".into()];
7959 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7960 let err = layout.verify(&c, &root).unwrap_err();
7961 match err {
7962 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7963 assert_eq!(caixa, "demo");
7964 assert_eq!(kind, CaixaKind::Binario);
7965 assert_eq!(slots, ":servicos");
7966 }
7967 other => panic!("expected ForeignCodeSlot, got {other:?}"),
7968 }
7969 }
7970
7971 #[test]
7972 fn servico_with_exe_rejected() {
7973 // Symmetric to `binario_with_servicos_rejected` on the other
7974 // code-running peer: a `:kind Servico` declaring an `:exe` is
7975 // the "I added a host-side CLI to my wasm component" footgun —
7976 // the nix flake's Binario target gates on `require_kind(_,
7977 // Binario)`, so the `:exe` path vanishes past the layout's
7978 // path-existence check.
7979 let root = PathBuf::from("/tmp/x");
7980 let manifest = root.join("caixa.lisp");
7981 let svc = root.join("servicos").join("demo.computeunit.yaml");
7982 let exe_path = root.join("exe").join("tool");
7983 let layout = StandardLayout::new()
7984 .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
7985 let mut c = caixa(CaixaKind::Servico);
7986 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7987 c.exe = vec!["exe/tool".into()];
7988 let err = layout.verify(&c, &root).unwrap_err();
7989 match err {
7990 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
7991 assert_eq!(caixa, "demo");
7992 assert_eq!(kind, CaixaKind::Servico);
7993 assert_eq!(slots, ":exe");
7994 }
7995 other => panic!("expected ForeignCodeSlot, got {other:?}"),
7996 }
7997 }
7998
7999 #[test]
8000 fn binario_without_servicos_still_verifies() {
8001 // Pass-after control: a well-formed Binario carrying only its
8002 // native `:exe` surface must remain accepted — the gate keys off
8003 // declared-ness of the *foreign* slots, so it must not over-fire
8004 // on the legitimate same-kind case. Mirror of
8005 // `servico_with_servico_slots_still_verifies` on the peer axis.
8006 let root = PathBuf::from("/tmp/x");
8007 let manifest = root.join("caixa.lisp");
8008 let exe_path = root.join("exe").join("tool");
8009 let layout =
8010 StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
8011 let mut c = caixa(CaixaKind::Binario);
8012 c.exe = vec!["exe/tool".into()];
8013 layout.verify(&c, &root).unwrap();
8014 }
8015
8016 #[test]
8017 fn biblioteca_with_only_bibliotecas_still_verifies() {
8018 // Pass-after control: a well-formed Biblioteca carrying only
8019 // its native `:bibliotecas` surface (or the default
8020 // `lib/<nome>.lisp`) must remain accepted. The gate keys off
8021 // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
8022 // is deliberately excluded from the foreign-set on every
8023 // code-running kind (`declared_foreign_code_slots` doc), so a
8024 // Biblioteca with the canonical lib surface alone passes.
8025 let root = PathBuf::from("/tmp/x");
8026 let manifest = root.join("caixa.lisp");
8027 let lib = root.join("lib").join("demo.lisp");
8028 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
8029 layout
8030 .verify(&caixa(CaixaKind::Biblioteca), &root)
8031 .expect("Biblioteca with default lib must verify");
8032 }
8033
8034 #[test]
8035 fn binario_with_bibliotecas_helper_still_verifies() {
8036 // Pass-after control on the deliberate `:bibliotecas`-as-helper
8037 // shape: a `:kind Binario` may legitimately bundle a `lib/`
8038 // helper its nix flake build consumes (the same shape a
8039 // `:kind Servico` may bundle for its wasm-component source).
8040 // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
8041 // either code-running kind; pinned here so a future tightening
8042 // that adds `:bibliotecas` to the foreign set on Binario /
8043 // Servico surfaces as a test failure rather than as a silent
8044 // over-reach.
8045 let root = PathBuf::from("/tmp/x");
8046 let manifest = root.join("caixa.lisp");
8047 let exe_path = root.join("exe").join("tool");
8048 let lib = root.join("lib").join("helper.lisp");
8049 let layout = StandardLayout::new()
8050 .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
8051 let mut c = caixa(CaixaKind::Binario);
8052 c.exe = vec!["exe/tool".into()];
8053 c.bibliotecas = vec!["lib/helper.lisp".into()];
8054 layout.verify(&c, &root).unwrap();
8055 }
8056
8057 #[test]
8058 fn supervisor_with_exe_still_surfaces_owns_code() {
8059 // Diagnostic-precedence pin: a `:kind Supervisor` declaring
8060 // `:exe` is *both* "Supervisor with code" and "foreign code
8061 // slot". The more-fundamental `SupervisorOwnsCode` must win
8062 // (Supervisor doesn't run code at all — the foreign-slot
8063 // diagnostic would mislead the author toward changing `:kind`
8064 // when the underlying defect is that supervisors orchestrate
8065 // children, not code). Guards the call order in `verify`
8066 // against silent reordering.
8067 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8068 let root = PathBuf::from("/tmp/x");
8069 let manifest = root.join("caixa.lisp");
8070 let manifest_clone = manifest.clone();
8071 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8072 let mut c = caixa(CaixaKind::Supervisor);
8073 c.estrategia = Some(RestartStrategy::OneForOne);
8074 c.max_restarts = Some(5);
8075 c.exe = vec!["exe/tool".into()];
8076 c.children = vec![ChildSpec {
8077 caixa: "worker".into(),
8078 versao: "^0.1".into(),
8079 restart: RestartPolicy::Permanent,
8080 }];
8081 let err = layout.verify(&c, &root).unwrap_err();
8082 assert!(
8083 matches!(err, LayoutError::SupervisorOwnsCode(_)),
8084 "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
8085 no-code-at-all diagnostic), got {err:?}"
8086 );
8087 }
8088
8089 #[test]
8090 fn declared_foreign_code_slots_returns_canonical_order() {
8091 // Unit-level pin for the lifted method: the canonical iteration
8092 // order is `:exe` → `:servicos`, independent of which subset is
8093 // populated. Empty input + each single-slot subset + the full
8094 // pair are all checked so a future axis added to the method
8095 // (a hypothetical fifth code-surface slot) is one extension
8096 // point + one assertion update here, not a coordinated rewrite
8097 // across the layout-test sites that reach for the canonical
8098 // order.
8099 let mut c = caixa(CaixaKind::Biblioteca);
8100 assert!(c.declared_foreign_code_slots().is_empty());
8101 c.exe = vec!["exe/tool".into()];
8102 assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
8103 c.exe.clear();
8104 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8105 assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
8106 c.exe = vec!["exe/tool".into()];
8107 assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
8108 }
8109
8110 #[test]
8111 fn aplicacao_with_unknown_contrato_member_fails() {
8112 use crate::{Membro, Placement, PlacementStrategy, WitContract};
8113 let root = PathBuf::from("/tmp/x");
8114 let manifest = root.join("caixa.lisp");
8115 let manifest_clone = manifest.clone();
8116 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8117 let mut c = caixa(CaixaKind::Aplicacao);
8118 c.membros = vec![Membro {
8119 caixa: "service-a".into(),
8120 versao: "^0.1".into(),
8121 }];
8122 c.contratos = vec![WitContract {
8123 de: "service-a".into(),
8124 para: "phantom".into(),
8125 wit: "wasi:http/proxy".into(),
8126 endpoint: Some("/x".into()),
8127 subject: None,
8128 slot: None,
8129 }];
8130 c.placement = Some(Placement {
8131 estrategia: PlacementStrategy::Replicated,
8132 clusters: vec!["rio".into()],
8133 affinity: None,
8134 shard_key: None,
8135 });
8136 let err = layout.verify(&c, &root).unwrap_err();
8137 assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
8138 }
8139
8140 #[test]
8141 fn limits_zero_axis_surfaces_as_layout_violation() {
8142 use crate::LimitsSpec;
8143 let root = PathBuf::from("/tmp/x");
8144 let manifest = root.join("caixa.lisp");
8145 let svc = root.join("servicos/demo.computeunit.yaml");
8146 let mut c = caixa(CaixaKind::Servico);
8147 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8148 c.limits = Some(LimitsSpec {
8149 fuel: Some(0),
8150 ..Default::default()
8151 });
8152 let manifest_clone = manifest.clone();
8153 let svc_clone = svc.clone();
8154 let layout =
8155 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8156 let err = layout.verify(&c, &root).unwrap_err();
8157 let LayoutError::LimitsViolation { caixa, issue } = err else {
8158 panic!("expected LimitsViolation, got {err:?}");
8159 };
8160 assert_eq!(caixa, "demo");
8161 assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
8162 }
8163
8164 #[test]
8165 fn limits_well_formed_passes_layout() {
8166 use crate::LimitsSpec;
8167 use std::time::Duration;
8168 let root = PathBuf::from("/tmp/x");
8169 let manifest = root.join("caixa.lisp");
8170 let svc = root.join("servicos/demo.computeunit.yaml");
8171 let mut c = caixa(CaixaKind::Servico);
8172 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8173 c.limits = Some(LimitsSpec {
8174 memory: Some(64 * 1024 * 1024),
8175 fuel: Some(1_000_000),
8176 wall_clock: Some(Duration::from_secs(30)),
8177 cpu: Some(500),
8178 });
8179 let manifest_clone = manifest.clone();
8180 let svc_clone = svc.clone();
8181 let layout =
8182 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8183 layout.verify(&c, &root).unwrap();
8184 }
8185
8186 #[test]
8187 fn supervisor_with_valid_children_passes() {
8188 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8189 let root = PathBuf::from("/tmp/x");
8190 let manifest = root.join("caixa.lisp");
8191 let manifest_clone = manifest.clone();
8192 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8193 let mut c = caixa(CaixaKind::Supervisor);
8194 c.estrategia = Some(RestartStrategy::OneForOne);
8195 c.max_restarts = Some(5);
8196 c.children = vec![
8197 ChildSpec {
8198 caixa: "worker".into(),
8199 versao: "^0.1".into(),
8200 restart: RestartPolicy::Permanent,
8201 },
8202 ChildSpec {
8203 caixa: "cache".into(),
8204 versao: "^0.1".into(),
8205 restart: RestartPolicy::Transient,
8206 },
8207 ];
8208 layout.verify(&c, &root).unwrap();
8209 }
8210
8211 // ── :upgrade-from entry validation pipes through layout ─────────────
8212
8213 #[test]
8214 fn upgrade_invalid_module_surfaces_as_layout_violation() {
8215 // End-to-end pin that
8216 // [`crate::UpgradeFromEntry::validate`] runs *inside*
8217 // `LayoutInvariants::verify` and surfaces value-shape
8218 // violations through the new `UpgradeViolation` arm
8219 // (parallel to `BehaviorViolation`, `LimitsViolation`,
8220 // `SupervisorViolation`, `AplicacaoViolation`). Until this
8221 // wiring landed the entry validator was unreachable from any
8222 // build-pipeline caller — an `:upgrade-from
8223 // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
8224 // module name the K8s apiserver would reject on the per-
8225 // ComputeUnit `metadata.name` axis) silently passed
8226 // `feira lint` / `feira build` and surfaced only at wasm-engine
8227 // hot-upgrade time as a per-backend "module not found" /
8228 // `code:load_module/1` `badarg` runtime error, far from the
8229 // source caixa.lisp. Pinning the wiring here so a future
8230 // refactor that drops the `entry.validate()` call surfaces as
8231 // a build-pipeline regression at this test, not as a runtime
8232 // surprise per consumer.
8233 use crate::{UpgradeFromEntry, UpgradeInstruction};
8234 use std::path::PathBuf;
8235 let root = PathBuf::from("/tmp/x");
8236 let manifest = root.join("caixa.lisp");
8237 let svc = root.join("servicos/demo.computeunit.yaml");
8238 let mut c = caixa(CaixaKind::Servico);
8239 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8240 c.upgrade_from = vec![UpgradeFromEntry {
8241 from: "0.1.0".into(),
8242 instructions: vec![UpgradeInstruction::LoadModule {
8243 module: "Hello".into(), // uppercase — not DNS-1123
8244 }],
8245 }];
8246 let manifest_clone = manifest.clone();
8247 let svc_clone = svc.clone();
8248 let layout =
8249 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8250 let err = layout.verify(&c, &root).unwrap_err();
8251 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8252 panic!("expected UpgradeViolation, got {err:?}");
8253 };
8254 assert_eq!(caixa, "demo");
8255 assert!(
8256 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8257 "issue must name the lisp-form of the offending instruction: {issue}"
8258 );
8259 assert!(
8260 issue.contains("Hello"),
8261 "issue must name the offending :module verbatim: {issue}"
8262 );
8263 }
8264
8265 #[test]
8266 fn upgrade_empty_module_surfaces_as_layout_violation() {
8267 // Companion to the DNS-1123 footgun above on the narrower
8268 // empty arm. Every Module-bearing variant's empty value
8269 // reaches the layout pipeline through the kind-tagged
8270 // `ModuleEmpty` diagnostic naming its lisp-form.
8271 use crate::{UpgradeFromEntry, UpgradeInstruction};
8272 use std::path::PathBuf;
8273 let root = PathBuf::from("/tmp/x");
8274 let manifest = root.join("caixa.lisp");
8275 let svc = root.join("servicos/demo.computeunit.yaml");
8276 let mut c = caixa(CaixaKind::Servico);
8277 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8278 c.upgrade_from = vec![UpgradeFromEntry {
8279 from: "0.1.0".into(),
8280 instructions: vec![UpgradeInstruction::SoftPurge {
8281 module: String::new(),
8282 }],
8283 }];
8284 let manifest_clone = manifest.clone();
8285 let svc_clone = svc.clone();
8286 let layout =
8287 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8288 let err = layout.verify(&c, &root).unwrap_err();
8289 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8290 panic!("expected UpgradeViolation, got {err:?}");
8291 };
8292 assert_eq!(caixa, "demo");
8293 assert!(
8294 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
8295 "issue must name the lisp-form of the empty instruction: {issue}"
8296 );
8297 }
8298
8299 #[test]
8300 fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
8301 // Pins that the b0c8389 script value-shape gates
8302 // (AbsoluteScript / ParentEscapeScript) — previously
8303 // unreachable from any build-pipeline caller — now fire
8304 // through the same `UpgradeViolation` arm before the path-
8305 // existence pass would otherwise emit the less-helpful
8306 // "missing upgrade-script" (or, worse, *succeed* against
8307 // /etc/passwd, proving the sandbox bypass — same defect
8308 // the b0c8389 BehaviorSpec wiring closed on the peer M2
8309 // slot).
8310 use crate::{UpgradeFromEntry, UpgradeInstruction};
8311 use std::path::PathBuf;
8312 let root = PathBuf::from("/tmp/x");
8313 let manifest = root.join("caixa.lisp");
8314 let svc = root.join("servicos/demo.computeunit.yaml");
8315 let etc_passwd = PathBuf::from("/etc/passwd");
8316 let mut c = caixa(CaixaKind::Servico);
8317 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8318 c.upgrade_from = vec![UpgradeFromEntry {
8319 from: "0.1.0".into(),
8320 instructions: vec![UpgradeInstruction::StateChange {
8321 script: PathBuf::from("/etc/passwd"),
8322 }],
8323 }];
8324 let manifest_clone = manifest.clone();
8325 let svc_clone = svc.clone();
8326 let etc_passwd_clone = etc_passwd.clone();
8327 // Critically: /etc/passwd "exists" in our mock — without the
8328 // value-shape pre-check, the existence loop would *succeed*
8329 // and the path-traversal exit from the project sandbox would
8330 // pass `feira build` silently.
8331 let layout = StandardLayout::new().with_path_exists(move |p| {
8332 p == manifest_clone || p == svc_clone || p == etc_passwd_clone
8333 });
8334 let err = layout.verify(&c, &root).unwrap_err();
8335 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8336 panic!("expected UpgradeViolation, got {err:?}");
8337 };
8338 assert_eq!(caixa, "demo");
8339 assert!(
8340 issue.contains("absolute") || issue.contains("Absolute"),
8341 "issue must name the violation kind (absolute): {issue}"
8342 );
8343 }
8344
8345 #[test]
8346 fn upgrade_well_formed_passes_layout() {
8347 // Positive control — every documented authoring shape
8348 // (`:load-module`, `:state-change` with a relative path,
8349 // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
8350 // gate. The typed sequence (`:load-module` → `:state-change`
8351 // → `:soft-purge` → `:purge`) lives in one entry; the sole
8352 // `:restart` fallback lives in a *separate* entry on a
8353 // different `:from` (the within-entry restart-exclusivity
8354 // gate added in this commit rejects mixing the fallback with
8355 // the typed sequence — per the UpgradeInstruction::Restart
8356 // doc, `:restart` is terminal and any other instructions in
8357 // the same entry are dead code). Drift here = a future
8358 // tighten that rejects any canonical shape surfaces as a
8359 // regression at this layout-level pin, not piecemeal across
8360 // per-renderer call sites.
8361 //
8362 // `:soft-purge` and `:purge` target *distinct* old-version
8363 // modules (`hello-rio-old` and `hello-rio-oldest`) so the
8364 // within-entry cleanup-singularity gate
8365 // (`UpgradeError::DuplicateCleanup`) passes — that gate
8366 // rejects more than one cleanup per module per entry (one
8367 // semantic per old version; mixing drain + discard on one
8368 // module is the soft-then-hard fallback footgun the author
8369 // shouldn't write because the operator handles cleanup
8370 // failure escalation itself). The two distinct names cover
8371 // the legitimate "drain a recent old, hard-discard an
8372 // older-still" shape — both authoring forms remain load-
8373 // bearing in this positive-control enumeration.
8374 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
8375 use std::path::PathBuf;
8376 let root = PathBuf::from("/tmp/x");
8377 let manifest = root.join("caixa.lisp");
8378 let svc = root.join("servicos/demo.computeunit.yaml");
8379 let migration = root.join("lib/migrations/v01-to-v02.lisp");
8380 let on_state_change = root.join("lib/migrations.lisp");
8381 let mut c = caixa(CaixaKind::Servico);
8382 // `:versao` past both entries' `:from` so the cross-slot
8383 // precedence gate (`FromNotBeforeVersao`) lets this canonical
8384 // authoring shape through to the positive-control assertion.
8385 c.versao = "0.2.0".into();
8386 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8387 // `:on-state-change` declared alongside the `(:state-change …)`
8388 // instruction below — the cross-slot composition gate
8389 // (`validate_upgrade_from_against_behavior`) rejects a
8390 // `:state-change` without the callback, so the canonical
8391 // authoring shape this positive control pins now includes the
8392 // runtime delivery hook (the `gen_server:code_change/3` analog
8393 // that the per-version script is invoked through during hot
8394 // upgrade per the upgrade.rs module doc "Composes with"
8395 // promise).
8396 c.behavior = Some(BehaviorSpec {
8397 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
8398 ..Default::default()
8399 });
8400 c.upgrade_from = vec![
8401 UpgradeFromEntry {
8402 from: "0.1.0".into(),
8403 instructions: vec![
8404 UpgradeInstruction::LoadModule {
8405 module: "hello-rio".into(),
8406 },
8407 UpgradeInstruction::StateChange {
8408 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
8409 },
8410 UpgradeInstruction::SoftPurge {
8411 module: "hello-rio-old".into(),
8412 },
8413 UpgradeInstruction::Purge {
8414 module: "hello-rio-oldest".into(),
8415 },
8416 ],
8417 },
8418 UpgradeFromEntry {
8419 from: "0.0.9".into(),
8420 instructions: vec![UpgradeInstruction::Restart],
8421 },
8422 ];
8423 let manifest_clone = manifest.clone();
8424 let svc_clone = svc.clone();
8425 let migration_clone = migration.clone();
8426 let on_state_change_clone = on_state_change.clone();
8427 let layout = StandardLayout::new().with_path_exists(move |p| {
8428 p == manifest_clone
8429 || p == svc_clone
8430 || p == migration_clone
8431 || p == on_state_change_clone
8432 });
8433 layout.verify(&c, &root).unwrap();
8434 }
8435
8436 #[test]
8437 fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
8438 // Wiring pin: the within-entry `(:restart)`-exclusivity gate
8439 // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
8440 // the same `LayoutError::UpgradeViolation` axis the per-entry
8441 // shape gate (26da2c7), the cross-entry duplicate-`:from`
8442 // gate (7c6aef2), and the cross-slot `:from < :versao`
8443 // precedence gate (de7ab1a) already do. A caixa.lisp whose
8444 // `:upgrade-from` entry mixes `(:restart)` with a typed
8445 // instruction surfaces at `feira build` time naming the
8446 // offending caixa + the entry's `:from` rather than silently
8447 // passing into the wasm-operator with semantically dead code
8448 // in the operator's dispatch table. Mirrors
8449 // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
8450 // the peer cross-entry gate.
8451 use crate::{UpgradeFromEntry, UpgradeInstruction};
8452 let root = PathBuf::from("/tmp/x");
8453 let manifest = root.join("caixa.lisp");
8454 let svc = root.join("servicos/demo.computeunit.yaml");
8455 let mut c = caixa(CaixaKind::Servico);
8456 c.versao = "0.2.0".into();
8457 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8458 c.upgrade_from = vec![UpgradeFromEntry {
8459 from: "0.1.0".into(),
8460 instructions: vec![
8461 UpgradeInstruction::LoadModule {
8462 module: "hello-rio".into(),
8463 },
8464 UpgradeInstruction::Restart,
8465 ],
8466 }];
8467 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8468 let err = layout.verify(&c, &root).unwrap_err();
8469 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8470 panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
8471 };
8472 assert_eq!(caixa, "demo");
8473 assert!(
8474 issue.contains("0.1.0"),
8475 "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8476 {issue:?}"
8477 );
8478 assert!(
8479 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8480 "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8481 );
8482 assert!(
8483 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8484 "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
8485 verbatim, got {issue:?}"
8486 );
8487 }
8488
8489 #[test]
8490 fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
8491 // Companion arm: the duplicate-`(:restart)` mode of
8492 // `RestartNotExclusive` (no typed peers, just multiple
8493 // `Restart` variants) surfaces through the same wiring as the
8494 // mixed-with-typed mode above. The diagnostic still names the
8495 // offending entry's `:from` verbatim even when `other_kinds`
8496 // is empty.
8497 use crate::{UpgradeFromEntry, UpgradeInstruction};
8498 let root = PathBuf::from("/tmp/x");
8499 let manifest = root.join("caixa.lisp");
8500 let svc = root.join("servicos/demo.computeunit.yaml");
8501 let mut c = caixa(CaixaKind::Servico);
8502 c.versao = "0.2.0".into();
8503 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8504 c.upgrade_from = vec![UpgradeFromEntry {
8505 from: "0.1.0".into(),
8506 instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
8507 }];
8508 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8509 let err = layout.verify(&c, &root).unwrap_err();
8510 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8511 panic!(
8512 "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
8513 {err:?}"
8514 );
8515 };
8516 assert_eq!(caixa, "demo");
8517 assert!(
8518 issue.contains("0.1.0"),
8519 "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8520 {issue:?}"
8521 );
8522 assert!(
8523 issue.contains("(:restart)")
8524 || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8525 "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8526 );
8527 }
8528
8529 #[test]
8530 fn upgrade_from_invalid_surfaces_as_layout_violation() {
8531 // The `:from` semver gate (`UpgradeError::FromInvalid`)
8532 // was likewise unreachable before this wiring landed — a
8533 // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
8534 // the semver slot) silently passed `feira build` and
8535 // surfaced only when the operator's hot-upgrade decision
8536 // engine tried to match against the version key it couldn't
8537 // parse. Now wired through `UpgradeViolation` with the
8538 // peer-shaped `{ from, reason }` payload — the
8539 // parser-shaped `reason` flows through `Display` so the
8540 // wrapped issue string carries both the offending value
8541 // *and* the SemVer-2 parser's wording (peer with the
8542 // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
8543 // sibling SemVer-2 axes).
8544 use crate::{UpgradeFromEntry, UpgradeInstruction};
8545 use std::path::PathBuf;
8546 let root = PathBuf::from("/tmp/x");
8547 let manifest = root.join("caixa.lisp");
8548 let svc = root.join("servicos/demo.computeunit.yaml");
8549 let mut c = caixa(CaixaKind::Servico);
8550 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8551 c.upgrade_from = vec![UpgradeFromEntry {
8552 from: "v0.1.0".into(), // git-tag-shape, not semver
8553 instructions: vec![UpgradeInstruction::Restart],
8554 }];
8555 let manifest_clone = manifest.clone();
8556 let svc_clone = svc.clone();
8557 let layout =
8558 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8559 let err = layout.verify(&c, &root).unwrap_err();
8560 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8561 panic!("expected UpgradeViolation, got {err:?}");
8562 };
8563 assert_eq!(caixa, "demo");
8564 assert!(
8565 issue.contains("v0.1.0"),
8566 "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
8567 );
8568 assert!(
8569 issue.contains(":from"),
8570 "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
8571 );
8572 // Pin the parser-shaped reason flow-through: the renamed
8573 // `FromInvalid { from, reason }` carries the SemVer-2 parser's
8574 // wording verbatim, and the [`UpgradeError`] Display routes it
8575 // into the wrapped `issue` string so the layout envelope
8576 // surfaces both the offending value *and* the parser's
8577 // diagnosis. Mirrors the peer flow-through on
8578 // `ManifestError::VersaoInvalid` (top-level `:versao`) and
8579 // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
8580 assert!(
8581 issue.contains("SemVer-2"),
8582 "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
8583 );
8584 }
8585
8586 #[test]
8587 fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
8588 // Fail-before-pass-after pin on the two-part converge landed
8589 // at layout.rs:844-847:
8590 // (a) `caixa.kind().is_biblioteca()` →
8591 // `caixa.kind().requires_lib()` — routes the biblioteca
8592 // required-slot gate onto the same `requires_*()`
8593 // predicate family the three sibling required-slot
8594 // gates (`requires_exe()` at :856, `requires_servicos()`
8595 // at :860, `requires_ci()` at :874) already key off.
8596 // All four gates in the block now share one convention;
8597 // a future kind that gains its own required-slot gate
8598 // (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
8599 // may grow) reaches for the same predicate family and
8600 // inherits the accessor discipline for free.
8601 // (b) raw `caixa.nome` → `caixa.nome()` — routes the
8602 // `expected` path composition through the typed
8603 // [`crate::Caixa::nome`] accessor, closing the last
8604 // unlifted raw `caixa.nome` production field-access
8605 // site in `caixa-core/src/layout.rs` (every peer
8606 // diagnostic in the file already routes through
8607 // `caixa.nome().to_string()`).
8608 //
8609 // The behavioral pin: for a Biblioteca kind with no fallback
8610 // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
8611 // path composes through `Caixa::nome()`; for every other
8612 // kind, MissingLib does NOT fire (the gate short-circuits on
8613 // kinds where `requires_lib()` returns false), even when the
8614 // fallback file is likewise absent. A future regression that
8615 // reroutes the gate off `requires_lib()` (e.g. onto
8616 // `is_biblioteca()` again, or onto a hand-authored
8617 // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
8618 // *happens* to agree byte-for-byte on today's arm-set trips
8619 // this test the moment a future kind's `requires_lib()`
8620 // returns true for a non-`Biblioteca` arm (or the sibling
8621 // required-slot gates diverge from the same convention).
8622 let root = PathBuf::from("/tmp/x");
8623 let manifest = root.join("caixa.lisp");
8624 let manifest_only = manifest.clone();
8625 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8626
8627 // Biblioteca kind + no lib fallback → MissingLib fires with
8628 // the expected path composed through `Caixa::nome()`.
8629 let bib = caixa(CaixaKind::Biblioteca);
8630 assert!(
8631 bib.kind().requires_lib(),
8632 "requires_lib() must return true for Biblioteca — the four-required-\
8633 slot-gate family's routing depends on this arm's assignment"
8634 );
8635 let err = layout.verify(&bib, &root).unwrap_err();
8636 let LayoutError::MissingLib {
8637 caixa: cname,
8638 expected,
8639 } = err
8640 else {
8641 panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
8642 };
8643 assert_eq!(
8644 cname,
8645 bib.nome(),
8646 "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
8647 );
8648 assert_eq!(
8649 expected,
8650 root.join(crate::render::LAYOUT_DIR_LIB)
8651 .join(format!("{}.lisp", bib.nome())),
8652 "MissingLib `expected:` path must compose through Caixa::nome() \
8653 verbatim — a raw-field-access regression would silently drift \
8654 the composed path on any future `:nome` axis extension \
8655 (namespace-qualified rewrite, per-cluster alias overlay)"
8656 );
8657
8658 // Non-Biblioteca kinds → the MissingLib gate short-circuits.
8659 // Different kinds fail on their own required-slot gate
8660 // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
8661 // on downstream M2/M3 invariants; none of them may surface as
8662 // MissingLib, because `requires_lib()` returns false for each.
8663 for kind in [
8664 CaixaKind::Binario,
8665 CaixaKind::Servico,
8666 CaixaKind::Supervisor,
8667 CaixaKind::Aplicacao,
8668 CaixaKind::Acao,
8669 ] {
8670 assert!(
8671 !kind.requires_lib(),
8672 "requires_lib() must return false for {kind:?} — the \
8673 four-required-slot-gate family's arm assignment pins \
8674 exactly one kind (Biblioteca) as the arm that requires \
8675 a `lib/` entry"
8676 );
8677 let c = caixa(kind);
8678 let result = layout.verify(&c, &root);
8679 assert!(
8680 !matches!(result, Err(LayoutError::MissingLib { .. })),
8681 "MissingLib gate at layout.rs:844 must short-circuit for \
8682 kinds where requires_lib() returns false; unexpectedly \
8683 fired for {kind:?}: {result:?}"
8684 );
8685 }
8686 }
8687}