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::missing_manifest(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::missing_lib(caixa, expected));
1011 }
1012 }
1013
1014 // Required-slot gate on the three [`CaixaKind`] arms whose
1015 // sole payload is a canonical typed slot — folded onto one
1016 // substrate primitive at
1017 // [`crate::Caixa::validate_required_kind_slot`]. Pre-lift each
1018 // of the three arms lived as a self-similar
1019 // `if caixa.kind().requires_<slot>() && caixa.<slot>().is_<empty>() {
1020 // return Err(LayoutError::<kind>_without_<slot>(caixa)); }` block
1021 // at this call site — three consumers, three identical shapes,
1022 // one substrate primitive on [`Caixa`] closing the duplication
1023 // the PRIME DIRECTIVE names as a bug. Diagnostic order at the
1024 // primitive matches the pre-fold canonical sequence — `:exe` →
1025 // `:servicos` → `:ci` — the same three-arm sweep the peer
1026 // [`CaixaKind`] discriminator carries at its `requires_*`
1027 // accessors; the three arms are mutually exclusive by
1028 // construction (`:kind` is a single-valued discriminator so at
1029 // most one arm can fire per caixa) so no cross-arm ordering
1030 // pin is meaningful.
1031 //
1032 // The paired `Biblioteca`-arm required-slot check
1033 // ([`LayoutError::MissingLib`], immediately above) stays
1034 // open-coded at this altitude by design: it needs the
1035 // [`LayoutInvariants::exists`] filesystem oracle to check the
1036 // default `lib/<nome>.lisp` fallback path, which the pure
1037 // per-`Caixa` typed-shape surface the fold rides on has no
1038 // reference to. Same posture the peer
1039 // [`crate::Caixa::validate_no_code_kind_coherence`] fold
1040 // (3bbf6a2) takes on the on-disk existence loops.
1041 //
1042 // Peer with the per-slot and per-kind compound entry gates
1043 // every substrate primitive on the M2/M3 typed-slot family
1044 // already carries ([`crate::Caixa::validate_deps`] b5dd55e,
1045 // [`crate::Caixa::validate_limits`] baa4688,
1046 // [`crate::Caixa::validate_behavior`] 0d2877a,
1047 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1048 // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1049 // [`crate::Caixa::validate_supervisor_shape`] 4c70105,
1050 // [`crate::Caixa::validate_acao_shape`] 5d6df54,
1051 // [`crate::Caixa::validate_kind_slot_coherence`] f0d286e,
1052 // [`crate::Caixa::validate_no_code_kind_coherence`] 3bbf6a2,
1053 // [`crate::Caixa::validate_ci_kind_coherence`] 9b55beb): the
1054 // layout pipeline routes the three self-similar required-slot
1055 // gates through one substrate primitive rather than three
1056 // open-coded blocks, and every future required-slot arm (a
1057 // per-Aplicacao required-`:membros` gate, a per-Supervisor
1058 // required-`:children` gate — both already carried at
1059 // [`CaixaKind::requires_membros`] / [`CaixaKind::requires_children`]
1060 // without a paired layout-side wire-up) folds onto this
1061 // compound gate as one arm addition rather than a fourth
1062 // open-coded block at the wire-up site.
1063 caixa.validate_required_kind_slot()?;
1064
1065 // On-disk existence probe on the three Caixa-level code-surface
1066 // path lists (`:bibliotecas`, `:exe`, `:servicos`): every declared
1067 // entry must resolve on disk. The `:bibliotecas` sweep folds onto
1068 // the [`Self::probe_declared_entries`] per-slot batch primitive
1069 // so the iteration + per-iteration `probe_declared_entry`
1070 // dispatch reads through one call rather than a three-line
1071 // `for … { self.probe_declared_entry(…)?; }` loop kept in
1072 // lockstep with the peer `:behavior` / `:upgrade-from` batch
1073 // wire-ups below. The `:exe` / `:servicos` peers fold onto the
1074 // sibling [`Self::probe_sandboxed_declared_entries`] per-slot
1075 // batch primitive on the sandboxed axis so the iteration +
1076 // per-iteration `probe_sandboxed_declared_entry` dispatch reads
1077 // through one call rather than a three-line `for … { self.
1078 // probe_sandboxed_declared_entry(…)?; }` loop kept in lockstep
1079 // with each other and with the peer `:bibliotecas` bare-batch
1080 // wire-up above. All three preserve the pre-lift diagnostic
1081 // order: `MissingEntry` fires before `ExeOutsideDir` /
1082 // `ServicoOutsideDir` on the same iteration, and iteration
1083 // proceeds in the caller-supplied iterator order (short-
1084 // circuiting on the first miss).
1085 self.probe_declared_entries(
1086 caixa.bibliotecas(),
1087 root,
1088 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1089 )?;
1090
1091 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
1092 self.probe_sandboxed_declared_entries(
1093 caixa.exe(),
1094 root,
1095 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
1096 &exe_dir,
1097 LayoutError::ExeOutsideDir,
1098 )?;
1099
1100 let servicos_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
1101 self.probe_sandboxed_declared_entries(
1102 caixa.servicos(),
1103 root,
1104 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
1105 &servicos_dir,
1106 LayoutError::ServicoOutsideDir,
1107 )?;
1108
1109 // ── M2 typed-substrate invariants ────────────────────────────────
1110
1111 // Compound per-Caixa entry gate on the M2 `:limits` slot: the
1112 // layout pipeline's `if let Some(l) = caixa.limits() { l.validate() }`
1113 // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1114 // dispatch pattern — the four-axis cascade on the present-slot
1115 // arm ([`crate::LimitsSpec::validate`]'s `:memory` wasm32
1116 // zero-floor / below-page / above-cap / non-page-multiple;
1117 // `:fuel` zero-floor / cap; `:wall-clock` zero-floor / cap;
1118 // `:cpu` zero-floor / cap) folded onto the
1119 // [`crate::Caixa::validate_limits`] substrate primitive. The
1120 // absent-slot arm (`limits: None`, the canonical "no bound
1121 // declared — engine-default applies" author shape) is the
1122 // fold's identity element and passes trivially through the
1123 // primitive, byte-equal to the pre-lift `if let Some(l) = …`
1124 // guard this call site formerly carried. Pinned by the paired
1125 // `validate_limits_folds_arm_matches_gate` equivalence pin and
1126 // the `validate_limits_accepts_none` / `_accepts_clean_fixture`
1127 // positive-control pins in the [`crate::Caixa::validate_limits`]
1128 // pin family (`manifest.rs`).
1129 //
1130 // Same lift discipline the peer per-Caixa compound gates
1131 // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1132 // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1133 // named substrate-primitive gate per typed slot folds every
1134 // structural axis on that slot (plus the `Option::None`
1135 // identity element for the `Option`-shaped slots) onto one
1136 // call, so every future consumer that wants to re-check
1137 // `:limits` after a per-`{:memory, :fuel, :wall-clock, :cpu}`
1138 // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1139 // materializer's admission webhook, a future `feira validate
1140 // --limits` per-caixa admission verb, a per-`:limits` overlay
1141 // resolver) reaches the four-axis cascade through one dispatch
1142 // rather than re-inlining the `if let Some(l) = …` unwrap-and-
1143 // dispatch pattern in lockstep with this wire-up.
1144 caixa.run_layout_gate(Caixa::validate_limits, LayoutError::limits_violation)?;
1145
1146 // Compound per-Caixa entry gate on the M2 `:behavior` slot's
1147 // pure value-shape surface: the layout pipeline's
1148 // `if let Some(b) = caixa.behavior() { b.validate() }`
1149 // `Option::None → Ok(()) | Some(_) → dispatch` unwrap-and-
1150 // dispatch pattern — the six-slot value-shape cascade on the
1151 // present-slot arm ([`crate::BehaviorSpec::validate`]'s per-
1152 // `:on-init` / `:on-call` / `:on-cast` / `:on-info` /
1153 // `:on-state-change` / `:on-terminate` non-empty / relative /
1154 // no-`..`-parent-escape / terminating-`.lisp`-extension
1155 // arm-set routed through the shared
1156 // [`crate::render::require_sandboxed_lisp_path`] helper) —
1157 // folded onto the [`crate::Caixa::validate_behavior`] substrate
1158 // primitive. The absent-slot arm (`behavior: None`, the
1159 // canonical "no callback declared — the runtime falls back to
1160 // the wasm-engine's default per arm" author shape) is the
1161 // fold's identity element and passes trivially through the
1162 // primitive, byte-equal to the pre-lift `if let Some(b) = …`
1163 // guard this call site formerly carried. Pinned by the paired
1164 // `validate_behavior_folds_arm_matches_gate` equivalence pin
1165 // and the `validate_behavior_accepts_none` /
1166 // `_accepts_clean_fixture` positive-control pins in the
1167 // [`crate::Caixa::validate_behavior`] pin family
1168 // (`manifest.rs`).
1169 //
1170 // The value-shape gate runs BEFORE the on-disk callback-path
1171 // existence walk below so a malformed `:behavior` slot
1172 // surfaces its self-locating per-slot diagnostic (naming the
1173 // offending `:on-*` slot) rather than the less-helpful
1174 // "missing behavior-callback" the existence probe would raise
1175 // against the resolved sandbox-escape path.
1176 //
1177 // Same lift discipline the peer per-Caixa compound gates
1178 // ([`crate::Caixa::validate_limits`] baa4688,
1179 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1180 // [`crate::Caixa::validate_deps`] b5dd55e) each carry — one
1181 // named substrate-primitive gate per typed slot folds every
1182 // structural axis on that slot (plus the `Option::None`
1183 // identity element for the `Option`-shaped slots) onto one
1184 // call, so every future consumer that wants to re-check
1185 // `:behavior` after a per-`{:on-init, …, :on-terminate}`
1186 // patch (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1187 // materializer's admission webhook, a future `feira validate
1188 // --behavior` per-caixa admission verb, a per-`:behavior`
1189 // overlay resolver) reaches the six-slot cascade through one
1190 // dispatch rather than re-inlining the `if let Some(b) = …`
1191 // unwrap-and-dispatch pattern in lockstep with this wire-up.
1192 // The paired on-disk existence walk stays open-coded at this
1193 // altitude because it needs the [`LayoutInvariants::exists`]
1194 // filesystem oracle the pure typed-shape surface has no
1195 // reference to — mirror of the peer M2 `:upgrade-from` per-
1196 // instruction script-path existence probe that stayed at this
1197 // altitude after the [`crate::Caixa::validate_upgrade_from`]
1198 // lift for the same reason.
1199 caixa.run_layout_gate(Caixa::validate_behavior, LayoutError::behavior_violation)?;
1200 if let Some(b) = caixa.behavior() {
1201 self.probe_declared_entries(
1202 b.declared_paths(),
1203 root,
1204 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
1205 )?;
1206 }
1207
1208 // Compound per-Caixa entry gate on `:upgrade-from`: the layout
1209 // pipeline's three-dispatch M2 `:upgrade-from` cascade — the
1210 // per-entry shape + cross-entry duplicate-`:from` gate
1211 // ([`crate::upgrade::validate_upgrade_from`]), the cross-slot
1212 // `:from < :versao` SemVer-2 precedence gate
1213 // ([`crate::upgrade::validate_upgrade_from_against_versao`]), and
1214 // the cross-slot `:state-change` ↔ `:on-state-change` composition
1215 // gate ([`crate::upgrade::validate_upgrade_from_against_behavior`])
1216 // — folded onto the [`crate::Caixa::validate_upgrade_from`]
1217 // substrate primitive. The three dispatches run in the same
1218 // canonical order at the primitive (per-entry → versao → behavior)
1219 // so the fold is byte-for-byte equivalent to the pre-fold
1220 // three-block cascade this call site formerly carried, pinned by
1221 // the paired
1222 // `validate_upgrade_from_folds_{per_entry,versao,behavior}_arm_matches_gate`
1223 // equivalence pins and the
1224 // `validate_upgrade_from_{per_entry_arm_fires_before_versao_arm,
1225 // versao_arm_fires_before_behavior_arm}` ordering pins in the
1226 // [`crate::Caixa::validate_upgrade_from`] pin family
1227 // (`manifest.rs`).
1228 //
1229 // Runs BEFORE the existing per-instruction script-path existence
1230 // pass below so a malformed typed slot surfaces its own
1231 // self-locating diagnostic rather than the less-helpful "missing
1232 // upgrade-script" (which doesn't fire for non-script axes at all).
1233 // Same lift discipline the peer per-slot compound gates
1234 // ([`crate::AplicacaoSpec::validate_contratos`] and its
1235 // `:membros` / `:entrada` / `:placement` / `:politicas` peers,
1236 // [`crate::MeshPolicy::validate`],
1237 // [`crate::SupervisorSpec::validate_children`]) each carry — one
1238 // named substrate-primitive gate per typed slot folds every
1239 // structural axis on that slot onto one call, so every future
1240 // consumer that wants to re-check `:upgrade-from` after a
1241 // per-entry patch (the deferred `caixa.pleme.io/v1alpha1/Caixa`
1242 // CR materializer's admission webhook, a future `feira validate
1243 // --upgrade` per-caixa admission verb, a per-`:upgrade-from`
1244 // overlay resolver) reaches the three-arm compound gate through
1245 // one dispatch rather than re-inlining the three-dispatch
1246 // cascade in lockstep with this wire-up.
1247 //
1248 // The per-instruction on-disk existence-probe walk below stays
1249 // open-coded at the layout wire-up site — that arm needs the
1250 // filesystem oracle on the [`LayoutInvariants`] trait, not on
1251 // the pure per-Caixa typed-shape surface the compound gate
1252 // folds. Same posture [`crate::Caixa::validate_code_paths`] takes
1253 // on the sibling code-path axes: the typed-shape gate fires on
1254 // the per-Caixa surface, the on-disk existence check fires on
1255 // the [`StandardLayout`] surface.
1256 caixa.run_layout_gate(Caixa::validate_upgrade_from, LayoutError::upgrade_violation)?;
1257 self.probe_declared_entries(
1258 caixa
1259 .upgrade_from()
1260 .iter()
1261 .flat_map(crate::UpgradeFromEntry::instructions)
1262 .filter_map(crate::upgrade::UpgradeInstruction::declared_path),
1263 root,
1264 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
1265 )?;
1266
1267 // Supervisor invariants (typed shape — children, restart strategy).
1268 // The "supervisor doesn't own code" check is at the top of verify()
1269 // so it fires before the existence-check loops.
1270 if caixa.kind().is_supervisor() {
1271 // Raw `:restart-window` parse gate on the flat
1272 // `Caixa::restart_window: Option<String>` axis — the last
1273 // orphan-validator on the typed Caixa surface flagged by the
1274 // [`Self::validate_deps`] wire-up's closing comment (the
1275 // "Supervisor-axis specific" remainder) and the
1276 // [`Caixa::supervisor_view`] doc-comment's "the future
1277 // layout-side wire-up" pin. Until this gate landed
1278 // [`Caixa::validate_restart_window`] existed as `pub fn` on
1279 // [`Caixa`] with full per-arm unit coverage in `manifest::tests`
1280 // (`validate_restart_window_rejects_*` — fractional seconds,
1281 // decimal-shaped integer, half-unit minute, leading sign,
1282 // unknown unit, garbage, empty-after-trim; eight rejection
1283 // arms total), but no production code path called it —
1284 // `feira build` (the canonical author-time gate; routes
1285 // through [`StandardLayout::verify`]) silently accepted a
1286 // malformed `:restart-window` and [`Caixa::supervisor_view`]
1287 // soft-swallowed the parse failure as `restart_window: None`
1288 // (i.e. the canonical "omit the slot to express no reset"
1289 // sentinel), turning every malformed window into a never-reset
1290 // supervisor far from the source `caixa.lisp`, with no field
1291 // naming the offending `:restart-window`. The Erlang/OTP
1292 // `MaxIntensity / Period` invariant the typed [`SupervisorSpec`]
1293 // gate (`view.validate()` immediately below) enforces on the
1294 // `Option<Duration>` value never reached the gate at all on
1295 // these inputs: the parse error was already laundered to
1296 // `None`, and `None` is the canonical "never reset" shape
1297 // that always validates cleanly. Lifting the parse gate to
1298 // the layout-pipeline wire-up closes the laundering — every
1299 // value past this gate either parses through the shared
1300 // `crate::supervisor::duration_codec::parse` (and therefore
1301 // round-trips canonically) or fires the new
1302 // [`Self::RestartWindowViolation`] envelope at the source.
1303 //
1304 // Runs *inside* the `kind == Supervisor` branch (rather than
1305 // alongside the peer flat-Caixa gates `validate_nome` /
1306 // `validate_versao` / `validate_deps` / `validate_code_paths`
1307 // above the kind dispatch) because `:restart-window` is in
1308 // the Supervisor slot set per [`Caixa::declared_supervisor_slots`]
1309 // — every non-Supervisor caixa with `:restart-window` set
1310 // already errors upstream via the
1311 // [`Self::SupervisorSlotsOnNonSupervisor`] kind-coherence gate
1312 // (line 243-252), so reaching this gate on a non-Supervisor
1313 // kind would be a no-op (the field is `None` by construction).
1314 // Runs *before* `view.validate()` so the parse-side diagnostic
1315 // surfaces first on the raw-string axis — a `:restart-window
1316 // "1.5s"` lands on the more self-locating
1317 // `RestartWindowViolation` (which names the offending raw
1318 // string verbatim) rather than the laundered-to-`None`
1319 // soft-pass that the typed view would silently let through.
1320 //
1321 // Same per-axis `*Violation { caixa, issue }` envelope every
1322 // peer flat-Caixa wrap exposes ([`Self::NomeViolation`] /
1323 // [`Self::VersaoViolation`] 1f74a5f,
1324 // [`Self::DepsViolation`] aa77d0f, [`Self::CodePathViolation`]
1325 // b868442). Threads [`ManifestError::RestartWindowMalformed`]
1326 // Display through verbatim — the per-arm reason already names
1327 // the offending raw value (e.g. `":restart-window \"1.5s\" is
1328 // not a canonical duration: …"`), so the wrap's `issue`
1329 // carries a self-locating "which axis, which value, why"
1330 // without re-shaping the parser-side reason.
1331 caixa.run_layout_gate(
1332 Caixa::validate_restart_window,
1333 LayoutError::restart_window_violation,
1334 )?;
1335 // Compound per-Caixa entry gate on the Supervisor-kind
1336 // supervision-tree slot family: the layout pipeline's paired
1337 // `let view = caixa.supervisor_view().expect(...);
1338 // view.validate() … validate_no_self_supervision(...) …`
1339 // cascade — the typed-shape cascade
1340 // ([`crate::SupervisorSpec::validate`]'s per-slot gates on
1341 // `:estrategia` ↔ `:children` invariants, `:max-restarts` /
1342 // `:restart-window` bounds, per-child DNS-1123 `:caixa`
1343 // names, semver-valid `:versao` constraints, the
1344 // set-not-multiset duplicate-child gate) and the cross-slot
1345 // self-edge gate
1346 // ([`crate::supervisor::validate_no_self_supervision`], the
1347 // `:children :caixa` ≠ `:nome` invariant the typed view
1348 // cannot enforce on its own because it carries the children
1349 // but not the parent `:nome`) — folded onto the
1350 // [`crate::Caixa::validate_supervisor_shape`] substrate
1351 // primitive. The two arms run in the same canonical order at
1352 // the primitive (typed-shape cascade → cross-slot self-edge)
1353 // so the fold is byte-for-byte equivalent to the pre-fold
1354 // two-block cascade this call site formerly carried, pinned
1355 // by the paired
1356 // `validate_supervisor_shape_folds_{view,self_supervision}_arm_matches_gate`
1357 // equivalence pins and the
1358 // `validate_supervisor_shape_view_arm_fires_before_self_supervision_arm`
1359 // ordering pin in the
1360 // [`crate::Caixa::validate_supervisor_shape`] pin family
1361 // (`manifest.rs`).
1362 //
1363 // Same lift discipline the peer per-Caixa compound gates
1364 // ([`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1365 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1366 // [`crate::Caixa::validate_deps`] b5dd55e,
1367 // [`crate::Caixa::validate_limits`] baa4688,
1368 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1369 // one named substrate-primitive gate folds every structural +
1370 // cross-slot axis on that slot family onto one call, so every
1371 // future consumer that wants to re-check the Supervisor shape
1372 // after a per-slot patch (the wasm-operator's hierarchical
1373 // reconciliation scheduler, the M4
1374 // `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's
1375 // admission webhook, a future `feira validate --supervisor`
1376 // per-caixa admission verb, a per-Supervisor overlay
1377 // resolver) reaches the two-arm compound gate through one
1378 // dispatch rather than re-inlining the two-dispatch cascade
1379 // in lockstep with this wire-up. Peer with the
1380 // [`crate::render::require_supervisor_view`] compound entry
1381 // gate every per-Supervisor renderer would route through
1382 // (which already folds the same `spec.validate()` +
1383 // `validate_no_self_supervision` two-arm cascade behind its
1384 // `require_kind` + `validate_restart_window` prelude): the
1385 // two consumers of the Supervisor-shape cascade now share
1386 // one substrate primitive on each side of the
1387 // author-time-vs-renderer split, rather than two open-coded
1388 // cascades kept in lockstep.
1389 //
1390 // `:restart-window` stays wired above through its own
1391 // per-axis `LayoutError::RestartWindowViolation` envelope
1392 // (the raw-string parse gate on the flat
1393 // `Caixa::restart_window: Option<String>` axis, distinct
1394 // from the typed view's `Duration`-shape gate) — the fold
1395 // covers the two arms that share the
1396 // `LayoutError::SupervisorViolation` envelope; the
1397 // parse-gate arm keeps its self-locating envelope so a
1398 // malformed `:restart-window` surfaces the raw-value
1399 // diagnostic rather than the laundered-to-`None` soft-pass.
1400 caixa.run_layout_gate(
1401 Caixa::validate_supervisor_shape,
1402 LayoutError::supervisor_violation,
1403 )?;
1404 }
1405
1406 // Aplicacao invariants — typed graph composition. Like
1407 // Supervisor, an Aplicacao runs no code itself.
1408 //
1409 // Compound per-Caixa entry gate on the Aplicacao-kind mesh-slot
1410 // family: the layout pipeline's paired `let view =
1411 // caixa.aplicacao_view().expect(...); view.validate() …
1412 // validate_no_self_membership(...) …` cascade — the typed-shape
1413 // cascade ([`crate::AplicacaoSpec::validate`]'s per-slot gates
1414 // on `:membros`, `:contratos`, `:entrada`, `:placement`,
1415 // `:politicas`, in that declared order) and the cross-slot
1416 // self-edge gate ([`crate::aplicacao::validate_no_self_membership`],
1417 // the `:membros :caixa` ≠ `:nome` invariant the typed view
1418 // cannot enforce on its own because it carries the membros but
1419 // not the parent `:nome`) — folded onto the
1420 // [`crate::Caixa::validate_aplicacao_shape`] substrate primitive.
1421 // The two arms run in the same canonical order at the primitive
1422 // (typed-shape cascade → cross-slot self-edge) so the fold is
1423 // byte-for-byte equivalent to the pre-fold two-block cascade
1424 // this call site formerly carried, pinned by the paired
1425 // `validate_aplicacao_shape_folds_{view,self_membership}_arm_matches_gate`
1426 // equivalence pins and the
1427 // `validate_aplicacao_shape_view_arm_fires_before_self_membership_arm`
1428 // ordering pin in the [`crate::Caixa::validate_aplicacao_shape`]
1429 // pin family (`manifest.rs`).
1430 //
1431 // Same lift discipline the peer per-slot compound gates
1432 // ([`crate::Caixa::validate_upgrade_from`] d6801df,
1433 // [`crate::Caixa::validate_deps`] b5dd55e,
1434 // [`crate::Caixa::validate_limits`] baa4688,
1435 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry — one
1436 // named substrate-primitive gate folds every structural +
1437 // cross-slot axis on that slot family onto one call, so every
1438 // future consumer that wants to re-check the Aplicacao shape
1439 // after a per-slot patch (the deferred
1440 // `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission
1441 // webhook, a future `feira validate --aplicacao` per-caixa
1442 // admission verb, a per-Aplicacao overlay resolver) reaches the
1443 // two-arm compound gate through one dispatch rather than
1444 // re-inlining the two-dispatch cascade in lockstep with this
1445 // wire-up. Peer with the [`crate::render::require_aplicacao_view`]
1446 // compound entry gate every per-Aplicacao renderer routes
1447 // through (3aefefb): the two consumers of the Aplicacao-shape
1448 // cascade now share one substrate primitive on each side of the
1449 // author-time-vs-renderer split, rather than two open-coded
1450 // cascades kept in lockstep.
1451 //
1452 // The outer `if caixa.kind().is_aplicacao()` guard stays because
1453 // [`crate::Caixa::validate_aplicacao_shape`] is the fold's
1454 // identity element on non-Aplicacao kinds (returns `Ok(())`
1455 // without touching the mesh slots — same posture as
1456 // [`crate::Caixa::validate_limits`] / [`Self`]::
1457 // [`crate::Caixa::validate_behavior`] on their `Option`-shaped
1458 // slots); the guard is a redundant but zero-cost fast-path that
1459 // preserves the peer supervisor branch's `if
1460 // caixa.kind().is_supervisor()` parallel structure at this
1461 // altitude.
1462 if caixa.kind().is_aplicacao() {
1463 caixa.run_layout_gate(
1464 Caixa::validate_aplicacao_shape,
1465 LayoutError::aplicacao_violation,
1466 )?;
1467 }
1468
1469 // Acao invariants — typed CI-run decompose. Like Supervisor and
1470 // Aplicacao, an Acao runs no code itself; unlike them, its sole
1471 // payload is a [`canteiro_types::CiRun`] whose declared-node
1472 // shape can carry three structural violations
1473 // ([`canteiro_types::DecomposeError`]: `DuplicateNode`,
1474 // `UnknownDep`, `Cycle`) the layout pipeline formerly deferred
1475 // to [`caixa_actions::validate`] — layout only checked `:ci`
1476 // *presence* via [`Self::MissingCi`] above, so a `:kind Acao`
1477 // carrying a structurally illegal `:ci` (a duplicate node
1478 // name, a dependency on an undeclared node, a dependency
1479 // cycle) passed `feira build` cleanly and surfaced the
1480 // diagnostic only when [`caixa_actions::validate`] later
1481 // refused it — far from the source `caixa.lisp` on the
1482 // author-time gate side.
1483 //
1484 // Compound per-Caixa entry gate on the Acao-kind `:ci` slot
1485 // family: the [`crate::render::decompose_ci`] typed decompose
1486 // gate — the sibling axis owned by the substrate-canonical
1487 // [`crate::render::require_acao_view`] compound helper every
1488 // per-`Acao` renderer routes through — folded onto the
1489 // [`crate::Caixa::validate_acao_shape`] substrate primitive.
1490 // The single-arm fold is byte-for-byte equivalent to the
1491 // pre-fold `decompose_ci(caixa, ci).map(|_| ())?` call this
1492 // wire-up sees, pinned by the paired
1493 // `validate_acao_shape_folds_decompose_arm_matches_gate`
1494 // equivalence pin and the
1495 // `validate_acao_shape_accepts_non_acao_kind` /
1496 // `validate_acao_shape_accepts_absent_ci_slot` identity-
1497 // element pins in the
1498 // [`crate::Caixa::validate_acao_shape`] pin family
1499 // (`manifest.rs`).
1500 //
1501 // Same lift discipline the peer per-kind compound gates
1502 // ([`crate::Caixa::validate_supervisor_shape`] 4c70105,
1503 // [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1504 // [`crate::Caixa::validate_upgrade_from`] d6801df,
1505 // [`crate::Caixa::validate_deps`] b5dd55e,
1506 // [`crate::Caixa::validate_limits`] baa4688,
1507 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry —
1508 // one named substrate-primitive gate folds every structural
1509 // axis on that kind onto one call, so every future consumer
1510 // that wants to re-check the Acao shape after a per-node
1511 // patch (a per-`Acao` CR materializer's admission webhook, a
1512 // future `feira validate --acao` per-caixa admission verb, a
1513 // per-`Acao` overlay resolver) reaches the compound gate
1514 // through one dispatch rather than re-inlining the decompose
1515 // cascade in lockstep with this wire-up. Peer with the
1516 // [`crate::render::require_acao_view`] compound entry gate
1517 // every per-`Acao` renderer routes through: the two consumers
1518 // of the Acao-shape cascade now share one substrate primitive
1519 // on each side of the author-time-vs-renderer split, rather
1520 // than two open-coded cascades kept in lockstep. Closes the
1521 // last per-kind asymmetry — with this wire-up the four typed
1522 // named-caixa kinds (`Servico` / `Aplicacao` / `Supervisor` /
1523 // `Acao`) each route through one compound per-Caixa shape
1524 // gate at the layout altitude.
1525 //
1526 // Runs *after* the [`Self::MissingCi`] presence gate above
1527 // (so a `:kind Acao` caixa with `ci = None` surfaces the
1528 // presence diagnostic first — the fold's identity-element arm
1529 // passes cleanly on absent `:ci`) and *after* the sibling
1530 // Supervisor / Aplicacao shape gates so the per-kind
1531 // diagnostic ordering at the layout altitude reads as the
1532 // canonical `Supervisor → Aplicacao → Acao` sweep.
1533 //
1534 // The outer `if caixa.kind().is_acao()` guard stays because
1535 // [`crate::Caixa::validate_acao_shape`] is the fold's
1536 // identity element on non-Acao kinds (returns `Ok(())`
1537 // without touching the `:ci` slot — same posture as
1538 // [`crate::Caixa::validate_supervisor_shape`] /
1539 // [`crate::Caixa::validate_aplicacao_shape`] on the sibling
1540 // typed-view-carrying arms); the guard is a redundant but
1541 // zero-cost fast-path that preserves the peer supervisor /
1542 // aplicacao branches' `if caixa.kind().is_<kind>()` parallel
1543 // structure at this altitude.
1544 if caixa.kind().is_acao() {
1545 caixa.run_layout_gate(Caixa::validate_acao_shape, LayoutError::acao_violation)?;
1546 }
1547
1548 Ok(())
1549 }
1550}
1551
1552#[derive(Debug, Error, PartialEq, Eq)]
1553pub enum LayoutError {
1554 #[error("manifest missing: {}", .0.display())]
1555 MissingManifest(PathBuf),
1556 #[error("caixa '{caixa}' is a Biblioteca but has no lib entry — expected {}", expected.display())]
1557 MissingLib { caixa: String, expected: PathBuf },
1558 #[error("caixa '{0}' is a Binario but has no :exe entries")]
1559 BinarioWithoutExe(String),
1560 #[error("caixa '{0}' is a Servico but has no :servicos entries")]
1561 ServicoWithoutServicos(String),
1562 #[error("declared {kind} entry missing: {}", path.display())]
1563 MissingEntry { kind: &'static str, path: PathBuf },
1564 #[error("exe entry outside exe/ directory: {}", .0.display())]
1565 ExeOutsideDir(PathBuf),
1566 #[error("servico entry outside servicos/ directory: {}", .0.display())]
1567 ServicoOutsideDir(PathBuf),
1568 #[error("caixa '{caixa}' has invalid :nome: {issue}")]
1569 NomeViolation { caixa: String, issue: String },
1570 #[error("caixa '{caixa}' has invalid :versao: {issue}")]
1571 VersaoViolation { caixa: String, issue: String },
1572 #[error("caixa '{caixa}' has invalid :deps / :deps-dev entry: {issue}")]
1573 DepsViolation { caixa: String, issue: String },
1574 #[error("caixa '{caixa}' has invalid :etiquetas entry: {issue}")]
1575 EtiquetasViolation { caixa: String, issue: String },
1576 #[error("caixa '{caixa}' has invalid :autores entry: {issue}")]
1577 AutoresViolation { caixa: String, issue: String },
1578 #[error("caixa '{caixa}' has invalid :repositorio: {issue}")]
1579 RepositorioViolation { caixa: String, issue: String },
1580 #[error("caixa '{caixa}' has invalid :descricao: {issue}")]
1581 DescricaoViolation { caixa: String, issue: String },
1582 #[error("caixa '{caixa}' has invalid :licenca: {issue}")]
1583 LicencaViolation { caixa: String, issue: String },
1584 #[error("caixa '{caixa}' has invalid :edicao: {issue}")]
1585 EdicaoViolation { caixa: String, issue: String },
1586 #[error("caixa '{caixa}' has invalid code-path entry: {issue}")]
1587 CodePathViolation { caixa: String, issue: String },
1588 #[error("caixa '{caixa}' has invalid :limits: {issue}")]
1589 LimitsViolation { caixa: String, issue: String },
1590 #[error("caixa '{caixa}' has invalid :behavior callback: {issue}")]
1591 BehaviorViolation { caixa: String, issue: String },
1592 #[error("caixa '{caixa}' has invalid :upgrade-from entry: {issue}")]
1593 UpgradeViolation { caixa: String, issue: String },
1594 #[error("supervisor caixa '{caixa}' violates typed shape: {issue}")]
1595 SupervisorViolation { caixa: String, issue: String },
1596 #[error("supervisor caixa '{caixa}' has invalid :restart-window: {issue}")]
1597 RestartWindowViolation { caixa: String, issue: String },
1598 #[error(
1599 "supervisor caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — supervisors don't run code, they orchestrate other caixas"
1600 )]
1601 SupervisorOwnsCode(String),
1602 #[error("aplicacao caixa '{caixa}' violates typed shape: {issue}")]
1603 AplicacaoViolation { caixa: String, issue: String },
1604 #[error("acao caixa '{caixa}' violates typed shape: {issue}")]
1605 AcaoViolation { caixa: String, issue: String },
1606 #[error(
1607 "aplicacao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — aplicacaos compose Servicos, they don't run code themselves"
1608 )]
1609 AplicacaoOwnsCode(String),
1610 #[error(
1611 "acao caixa '{0}' must not declare :bibliotecas, :exe, or :servicos — acaos carry a typed CI run (:ci), they don't run code themselves"
1612 )]
1613 AcaoOwnsCode(String),
1614 #[error(
1615 "caixa '{caixa}' is :kind {kind:?} but declares Aplicacao-only mesh slot(s): {slots} — \
1616 :membros / :contratos / :politicas / :placement / :entrada compose a :kind Aplicacao's \
1617 typed graph (MESH-COMPOSITION §III.1) and are silently ignored on every other kind \
1618 (never validated, never rendered); move them to a :kind Aplicacao caixa or remove them"
1619 )]
1620 MeshSlotsOnNonAplicacao {
1621 caixa: String,
1622 kind: CaixaKind,
1623 slots: String,
1624 },
1625 #[error(
1626 "caixa '{caixa}' is :kind {kind:?} but declares Supervisor-only slot(s): {slots} — \
1627 :estrategia / :max-restarts / :restart-window / :children compose a :kind Supervisor's \
1628 typed OTP supervisor (INSPIRATIONS §II.2) and are silently ignored on every other kind \
1629 (never validated, never reconciled); move them to a :kind Supervisor caixa or remove them"
1630 )]
1631 SupervisorSlotsOnNonSupervisor {
1632 caixa: String,
1633 kind: CaixaKind,
1634 slots: String,
1635 },
1636 #[error(
1637 "caixa '{caixa}' is :kind {kind:?} but declares Servico-only slot(s): {slots} — \
1638 :limits / :behavior / :upgrade-from configure the runtime of a long-running :kind Servico \
1639 wasm component (INSPIRATIONS §III.1 / §II.3 / §II.4) and are silently ignored on every \
1640 other kind (never rendered into a chart or programs.yaml entry); move them to a :kind \
1641 Servico caixa or remove them"
1642 )]
1643 ServicoSlotsOnNonServico {
1644 caixa: String,
1645 kind: CaixaKind,
1646 slots: String,
1647 },
1648 #[error(
1649 "caixa '{caixa}' is :kind {kind:?} but declares foreign code-surface slot(s): {slots} — \
1650 :exe is the nix-built executable surface owned only by :kind Binario, :servicos is the \
1651 wasm-component + ComputeUnit daemon surface owned only by :kind Servico; \
1652 caixa-helm / caixa-flux / caixa-flake gate emission on `require_kind(_, <owning-kind>)`, \
1653 so a declared :exe / :servicos on the wrong code-running kind is silently ignored — the \
1654 path is validated by the layout's path-existence loops but never rendered into a build \
1655 target or programs.yaml entry. Move the slot to its owning kind, change :kind to match \
1656 (Binario for :exe, Servico for :servicos), or drop the slot entirely"
1657 )]
1658 ForeignCodeSlot {
1659 caixa: String,
1660 kind: CaixaKind,
1661 slots: String,
1662 },
1663 #[error("caixa '{0}' is an Acao but has no :ci slot")]
1664 MissingCi(String),
1665 #[error(
1666 "caixa '{caixa}' is :kind {kind:?} but declares the Acao-only :ci slot — \
1667 :ci carries a typed CI run (canteiro_types::CiRun, CANTEIRO §7.1-C) that only the \
1668 caixa-actions renderer validates for :kind Acao, and is silently ignored on every \
1669 other kind (never decomposed, never rendered); move it to a :kind Acao caixa or \
1670 remove it"
1671 )]
1672 CiOnNonAcao { caixa: String, kind: CaixaKind },
1673}
1674
1675// Fold the layout-pipeline per-Caixa violation wrap onto one substrate
1676// primitive per typed slot. Every `LayoutError::*Violation { caixa, issue }`
1677// variant follows the same uniform shape — `caixa = layout-Caixa's :nome`,
1678// `issue = the gate's per-arm Display` — and every wire-up in
1679// [`StandardLayout::verify`] used to open-code the identical five-line
1680// `.map_err(|err| LayoutError::XxxViolation { caixa: caixa.nome().to_string(),
1681// issue: err.to_string() })` block, once per typed slot. Sixteen distinct
1682// wrap-variants × eighteen wire-up sites is exactly the duplication the
1683// PRIME DIRECTIVE names as a bug: every future consumer that wants to add a
1684// new per-slot gate (the deferred `caixa.pleme.io/v1alpha1/Caixa` CR
1685// materializer's admission webhook, a future per-slot `feira validate`
1686// verb, a per-slot overlay resolver) had to re-inline the five-line block
1687// in lockstep with the pre-existing wire-ups.
1688//
1689// The macro below generates one static constructor per variant of shape
1690// `fn <slot>_violation(caixa: &Caixa, err: impl Display) -> LayoutError`,
1691// so every wire-up site collapses onto one dispatch:
1692// `caixa.validate_<slot>().map_err(|err| LayoutError::<slot>_violation(caixa,
1693// err))?;`. The uniform two-slot construction (`caixa: caixa.nome()
1694// .to_string()`, `issue: err.to_string()`) is spelled once — inside the
1695// macro — rather than at every wire-up site. Every constructor is
1696// `#[must_use]` so a caller who mistakenly discards the wrapped error
1697// (rather than routing it through `?`) trips a compile warning at the
1698// wire-up site.
1699//
1700// Peer with the per-slot compound entry gates every substrate primitive
1701// on the M2/M3 typed-slot family already carries
1702// ([`crate::Caixa::validate_deps`] b5dd55e, [`crate::Caixa::validate_limits`]
1703// baa4688, [`crate::Caixa::validate_behavior`] 0d2877a,
1704// [`crate::Caixa::validate_upgrade_from`] d6801df,
1705// [`crate::Caixa::validate_aplicacao_shape`] 949a7a0,
1706// [`crate::AplicacaoSpec::validate_contratos`],
1707// [`crate::MeshPolicy::validate`],
1708// [`crate::SupervisorSpec::validate_children`]): the author-time gates
1709// fold onto one substrate primitive per typed slot; here the layout-side
1710// error-wrap folds onto one substrate primitive per typed variant, so the
1711// two sides of the layout pipeline's per-slot cascade (the gate, the
1712// wrap) each route through one call rather than N open-coded block
1713// repetitions.
1714macro_rules! layout_violation_ctors {
1715 ($($ctor:ident => $variant:ident),* $(,)?) => {
1716 impl LayoutError {
1717 $(
1718 #[doc = concat!(
1719 "Construct a [`LayoutError::",
1720 stringify!($variant),
1721 "`] wrapping `err` under `caixa.nome()`. Folds the ",
1722 "uniform `{ caixa: caixa.nome().to_string(), issue: ",
1723 "err.to_string() }` two-slot construction onto one ",
1724 "substrate primitive so every ",
1725 "[`StandardLayout::verify`] wire-up on this variant ",
1726 "reads through one dispatch rather than the pre-lift ",
1727 "five-line open-coded block."
1728 )]
1729 #[must_use]
1730 pub fn $ctor<E: std::fmt::Display>(caixa: &crate::Caixa, err: E) -> Self {
1731 Self::$variant {
1732 caixa: caixa.nome().to_string(),
1733 issue: err.to_string(),
1734 }
1735 }
1736 )*
1737 }
1738 };
1739}
1740
1741layout_violation_ctors! {
1742 nome_violation => NomeViolation,
1743 versao_violation => VersaoViolation,
1744 deps_violation => DepsViolation,
1745 etiquetas_violation => EtiquetasViolation,
1746 autores_violation => AutoresViolation,
1747 repositorio_violation => RepositorioViolation,
1748 descricao_violation => DescricaoViolation,
1749 licenca_violation => LicencaViolation,
1750 edicao_violation => EdicaoViolation,
1751 code_path_violation => CodePathViolation,
1752 limits_violation => LimitsViolation,
1753 behavior_violation => BehaviorViolation,
1754 upgrade_violation => UpgradeViolation,
1755 restart_window_violation => RestartWindowViolation,
1756 supervisor_violation => SupervisorViolation,
1757 aplicacao_violation => AplicacaoViolation,
1758 acao_violation => AcaoViolation,
1759}
1760
1761// Fold the four `LayoutError::*SlotsOn*` / `LayoutError::ForeignCodeSlot`
1762// kind-coherence wrap sites onto one substrate primitive per typed variant —
1763// the sibling of [`layout_violation_ctors!`] above on the second uniform
1764// error-envelope shape `LayoutError` carries: `{ caixa: caixa.nome(),
1765// kind: caixa.kind(), slots: <declared_*_slots()>.join(" ") }`. Every
1766// wire-up in [`StandardLayout::verify`] on this shape (four sites: the
1767// M3-mesh gate on non-Aplicacao, the supervisor-tree gate on
1768// non-Supervisor, the M2-runtime gate on non-Servico, the code-surface
1769// `ForeignCodeSlot` gate) used to open-code the identical four-field
1770// `.{ caixa: caixa.nome().to_string(), kind: caixa.kind(), slots:
1771// <declared_*_slots>.join(" ") }` block — the exact "same block re-inlined
1772// at every consumer" shape the PRIME DIRECTIVE names as a bug on the same
1773// altitude the peer [`layout_violation_ctors!`] macro just closed on the
1774// `{ caixa, issue }` sibling shape.
1775//
1776// The macro below generates one static constructor per variant of shape
1777// `fn <slot>_on_non_<owner>(caixa: &Caixa, slots: Vec<&'static str>) ->
1778// LayoutError`, so every wire-up site collapses onto one dispatch:
1779// `return Err(LayoutError::<slot>_on_non_<owner>(caixa, <declared_slots>));`.
1780// The uniform four-field construction is spelled once — inside the macro —
1781// rather than at every wire-up site. Every constructor is `#[must_use]` so
1782// a caller who mistakenly discards the constructed error (rather than
1783// routing it through `return Err(…)`) trips a compile warning at the
1784// wire-up site.
1785//
1786// Peer with the `_violation` constructor family above on the same
1787// `LayoutError` — the two together now fold every uniform-shape
1788// `LayoutError` variant carried by [`StandardLayout::verify`] onto one
1789// substrate primitive per typed variant, so the layout-side error-wrap
1790// surface reads through one dispatch per variant rather than N open-coded
1791// blocks. Every future consumer that wants to construct one of these
1792// variants outside the layout pipeline (a per-slot admission webhook, a
1793// `feira validate --kind X` verb, an overlay resolver rejecting a
1794// kind-foreign patch) reaches its variant through one call, matching the
1795// `_violation` family's substrate-primitive discipline.
1796macro_rules! layout_slot_kind_ctors {
1797 ($($ctor:ident => $variant:ident),* $(,)?) => {
1798 impl LayoutError {
1799 $(
1800 #[doc = concat!(
1801 "Construct a [`LayoutError::",
1802 stringify!($variant),
1803 "`] naming the offending slot list under `caixa.nome()` ",
1804 "at `caixa.kind()`. Folds the uniform `{ caixa: caixa.",
1805 "nome().to_string(), kind: caixa.kind(), slots: slots.",
1806 "join(\" \") }` four-field construction onto one substrate ",
1807 "primitive so every [`StandardLayout::verify`] wire-up on ",
1808 "this variant reads through one dispatch rather than the ",
1809 "pre-lift open-coded block."
1810 )]
1811 #[must_use]
1812 pub fn $ctor(caixa: &crate::Caixa, slots: Vec<&'static str>) -> Self {
1813 Self::$variant {
1814 caixa: caixa.nome().to_string(),
1815 kind: caixa.kind(),
1816 slots: slots.join(" "),
1817 }
1818 }
1819 )*
1820 }
1821 };
1822}
1823
1824layout_slot_kind_ctors! {
1825 mesh_slots_on_non_aplicacao => MeshSlotsOnNonAplicacao,
1826 supervisor_slots_on_non_supervisor => SupervisorSlotsOnNonSupervisor,
1827 servico_slots_on_non_servico => ServicoSlotsOnNonServico,
1828 foreign_code_slot => ForeignCodeSlot,
1829}
1830
1831// Fold the five [`LayoutError::MissingEntry`] wire-up sites at
1832// [`StandardLayout::verify`] onto one substrate primitive on `LayoutError` —
1833// the third and last uniform-shape envelope on `LayoutError` after the
1834// `{ caixa, issue }` family the [`layout_violation_ctors!`] macro closed
1835// (131ca0d) and the `{ caixa, kind, slots }` family the peer
1836// [`layout_slot_kind_ctors!`] macro closed (0419438). Each of the five
1837// wire-up sites on `MissingEntry` (`:bibliotecas` iteration line 823,
1838// `:exe` iteration line 834, `:servicos` iteration line 848, `:behavior`
1839// on-disk callback-path iteration line 957, `:upgrade-from` per-
1840// instruction script-path iteration line 1021) opened the same four-line
1841// `LayoutError::MissingEntry { kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_<slot>,
1842// path: full }` struct-literal block — the exact "same block re-inlined
1843// at every consumer" shape the PRIME DIRECTIVE names as a bug, on the
1844// same altitude the peer `_violation` / `_slots_on_non_*` families each
1845// closed on the sibling `LayoutError` envelopes.
1846//
1847// One `#[must_use]` inherent constructor on `LayoutError` collapses the
1848// five sites onto one dispatch:
1849// `return Err(LayoutError::missing_entry(<kind-label>, full));`, byte-
1850// equal to the pre-lift struct-literal block. A macro is not warranted
1851// on the one-variant envelope shape `{ kind: &'static str, path: PathBuf }`
1852// (unlike the sibling 16-variant `_violation` / 4-variant
1853// `_slots_on_non_*` shapes), but the same substrate-primitive discipline
1854// applies: every future consumer that wants to construct a `MissingEntry`
1855// outside the layout pipeline (a per-slot admission webhook probing a
1856// declared path against an out-of-band filesystem oracle, a `feira
1857// validate --lib` / `--exe` / `--servico` / `--behavior` / `--upgrade`
1858// per-caixa admission verb, the deferred `caixa.pleme.io/v1alpha1/Caixa`
1859// CR materializer's admission-webhook floor, a per-cluster overlay
1860// resolver rejecting a missing entry against a cluster-local filesystem
1861// snapshot) reaches the variant through one call rather than re-inlining
1862// the four-line struct-literal block in lockstep with the five
1863// layout-pipeline wire-up sites.
1864impl LayoutError {
1865 /// Construct a [`LayoutError::MissingManifest`] naming the resolved
1866 /// `caixa.lisp` path the layout gate probed for at
1867 /// [`StandardLayout::verify`]'s top-of-body manifest-existence
1868 /// check.
1869 ///
1870 /// Folds the uniform `Self::MissingManifest(path)` one-slot
1871 /// tuple-literal onto one substrate primitive so the sole
1872 /// [`StandardLayout::verify`] wire-up on this variant reads through
1873 /// one dispatch rather than the pre-lift open-coded tuple-literal
1874 /// block. Peer of the sibling [`Self::missing_entry`] `{ kind, path }`
1875 /// ctor on the same envelope's declared-entry-existence axis, and
1876 /// mirror-symmetric sibling of the sibling [`Self::missing_lib`]
1877 /// `{ caixa, expected }` ctor on the enclosing envelope's per-caixa
1878 /// required-fallback-path axis: the top-level manifest-existence
1879 /// gate now routes its emit-site through the same substrate-
1880 /// primitive discipline every sibling variant on the same
1881 /// [`LayoutError`] envelope carries, closing the last un-lifted
1882 /// `<Variant>(PathBuf)` tuple-newtype variant on the envelope
1883 /// carried through a direct open-coded construction (the peer
1884 /// [`Self::ExeOutsideDir`] / [`Self::ServicoOutsideDir`]
1885 /// `<Variant>(PathBuf)` variants stay on their pre-lift
1886 /// tuple-variant-constructor coercion inside
1887 /// [`StandardLayout::probe_sandboxed_declared_entry`]'s
1888 /// `outside_ctor: fn(PathBuf) -> LayoutError` function-pointer
1889 /// parameter — the substrate primitive on that axis is the
1890 /// sandbox-probe helper itself, so the paired variant ctors are
1891 /// already substrate-canonical at their wire-up site by
1892 /// construction).
1893 ///
1894 /// The `PathBuf` parameter takes ownership of the resolved
1895 /// `root.join("caixa.lisp")` path from the wire-up site's
1896 /// [`Path::join`] product — byte-equal to the pre-lift
1897 /// `Self::MissingManifest(manifest)` tuple-literal on the same
1898 /// owned `PathBuf` fixture. `#[must_use]` fires a compile warning
1899 /// at any wire-up that mistakenly discards the constructed error
1900 /// rather than routing it through `return Err(…)`.
1901 ///
1902 /// Every future consumer that wants to construct this variant
1903 /// outside [`StandardLayout::verify`] — a deferred
1904 /// `caixa.pleme.io/v1alpha1/Caixa` CR materializer's admission-
1905 /// webhook re-check probing the resolved manifest path against a
1906 /// mounted cluster-local filesystem snapshot, a future
1907 /// `feira validate --manifest-exists` per-caixa admission verb
1908 /// re-running the manifest-existence gate on demand, a per-cluster
1909 /// overlay resolver rejecting a `:placement`-scoped manifest
1910 /// omission against a cluster-local snapshot — now reaches the
1911 /// variant through one call rather than re-inlining the two-line
1912 /// tuple-literal in lockstep with the in-crate wire-up site.
1913 #[must_use]
1914 pub const fn missing_manifest(path: PathBuf) -> Self {
1915 Self::MissingManifest(path)
1916 }
1917
1918 /// Construct a [`LayoutError::MissingEntry`] naming the missing
1919 /// declared entry at `path` under the canonical `kind` label from
1920 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
1921 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
1922 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] /
1923 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`] /
1924 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`].
1925 /// Folds the uniform `{ kind, path }` two-slot construction onto one
1926 /// substrate primitive so every [`StandardLayout::verify`] wire-up
1927 /// on this variant reads through one dispatch rather than the
1928 /// pre-lift open-coded struct-literal block.
1929 ///
1930 /// `pub const fn` — matches the sibling [`Self::missing_manifest`]
1931 /// `pub const fn` shape on the same [`LayoutError`] envelope's
1932 /// path-carrying tuple-newtype axis. The body is a pure two-field
1933 /// struct-literal (`Self::MissingEntry { kind, path }` — no
1934 /// `.to_string()` / `.into()` / accessor projection on either arg,
1935 /// unlike the sibling [`Self::missing_lib`] / [`Self::ci_on_non_acao`]
1936 /// ctors that route through `caixa.nome().to_string()`), so the
1937 /// const-eval surface widens with no body change. Every downstream
1938 /// consumer that wants to fold a per-arm layout diagnostic into a
1939 /// `const` position (a `const MISSING_BIBLIOTECA: LayoutError =
1940 /// LayoutError::missing_entry(LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
1941 /// PathBuf::new());` fixture the future M4 `caixa.pleme.io/v1alpha1/Caixa`
1942 /// CR materializer's admission-webhook per-kind rejection-body
1943 /// lookup table reaches for, a compile-time per-arm diagnostic
1944 /// registry the LSP hover renderer materializes per typed-slot
1945 /// fixture, a `const`-context probe the future
1946 /// `feira validate --entry-exists` per-caixa verb consults) now
1947 /// reads through one const dispatch on the substrate primitive
1948 /// rather than being forced onto the runtime code path.
1949 #[must_use]
1950 pub const fn missing_entry(kind: &'static str, path: PathBuf) -> Self {
1951 Self::MissingEntry { kind, path }
1952 }
1953
1954 /// Construct a [`LayoutError::MissingLib`] naming the offending
1955 /// `caixa.nome()` and the resolved fallback `expected` path.
1956 ///
1957 /// Folds the uniform `Self::MissingLib { caixa: caixa.nome()
1958 /// .to_string(), expected }` two-slot struct-literal onto one
1959 /// substrate primitive so every [`StandardLayout::verify`] wire-up
1960 /// on this variant reads through one dispatch rather than the
1961 /// pre-lift open-coded struct-literal block, projecting the caixa
1962 /// slot through the paired [`crate::Caixa::nome`] accessor — the
1963 /// same discipline the sibling [`Self::missing_entry`]
1964 /// `{ kind, path }` ctor and the [`layout_nome_only_ctors!`]
1965 /// `{ caixa }`-only tuple-variant ctor family already take on the
1966 /// same envelope.
1967 #[must_use]
1968 pub fn missing_lib(caixa: &crate::Caixa, expected: PathBuf) -> Self {
1969 Self::MissingLib {
1970 caixa: caixa.nome().to_string(),
1971 expected,
1972 }
1973 }
1974
1975 /// Construct a [`LayoutError::CiOnNonAcao`] naming the offending
1976 /// `caixa.nome()` and its declared `:kind`.
1977 ///
1978 /// Folds the uniform `Self::CiOnNonAcao { caixa: caixa.nome()
1979 /// .to_string(), kind: caixa.kind() }` two-slot struct-literal
1980 /// onto one substrate primitive so every wire-up on this variant
1981 /// (today the sole [`crate::Caixa::validate_ci_kind_coherence`]
1982 /// call site at `caixa-core/src/manifest.rs:5586` — the deferred
1983 /// `caixa.pleme.io/v1alpha1/Caixa` CR admission webhook and a
1984 /// future `feira validate --ci-kind` per-caixa verb sit at the
1985 /// same axis) reads through one dispatch rather than the pre-lift
1986 /// open-coded struct-literal block. Both slots project through
1987 /// the paired [`crate::Caixa::nome`] / [`crate::Caixa::kind`]
1988 /// accessors — the same discipline every sibling ctor on this
1989 /// envelope ([`Self::missing_lib`] on `{ caixa, expected }`, the
1990 /// [`layout_nome_only_ctors!`] family on `{ caixa }`-only
1991 /// tuple-variants, the [`layout_slot_kind_ctors!`] family on
1992 /// `{ caixa, kind, slots }`) already takes.
1993 #[must_use]
1994 pub fn ci_on_non_acao(caixa: &crate::Caixa) -> Self {
1995 Self::CiOnNonAcao {
1996 caixa: caixa.nome().to_string(),
1997 kind: caixa.kind(),
1998 }
1999 }
2000}
2001
2002// Fold the six `LayoutError::<Variant>(caixa.nome().to_string())` nome-
2003// only tuple-variant wire-up sites at [`StandardLayout::verify`] onto one
2004// substrate primitive per typed variant — the fourth uniform-shape
2005// envelope on `LayoutError` after the `{ caixa, issue }` family the
2006// [`layout_violation_ctors!`] macro closed (131ca0d), the
2007// `{ caixa, kind, slots }` family the peer [`layout_slot_kind_ctors!`]
2008// macro closed (0419438), and the `{ kind, path }`
2009// [`LayoutError::missing_entry`] one-variant ctor (1b09f9d). Each of the
2010// six wire-up sites on this shape (`SupervisorOwnsCode` /
2011// `AplicacaoOwnsCode` / `AcaoOwnsCode` at the no-code kind-coherence gate;
2012// `BinarioWithoutExe` / `ServicoWithoutServicos` / `MissingCi` at the
2013// required-slot gate) opened the identical one-line
2014// `LayoutError::<Variant>(caixa.nome().to_string())` tuple-literal — the
2015// exact "same block re-inlined at every consumer" shape the PRIME
2016// DIRECTIVE names as a bug, on the same altitude the peer `_violation` /
2017// `_slots_on_non_*` / `missing_entry` families each closed on the
2018// sibling `LayoutError` envelopes.
2019//
2020// The macro below generates one static constructor per variant of shape
2021// `fn <slot>(caixa: &Caixa) -> LayoutError`, so every wire-up site
2022// collapses onto one dispatch:
2023// `return Err(LayoutError::<slot>(caixa));`, byte-equal to the pre-lift
2024// tuple-literal. The uniform one-field construction (`caixa: caixa.nome()
2025// .to_string()`) is spelled once — inside the macro — rather than at
2026// every wire-up site. Every constructor is `#[must_use]` so a caller who
2027// mistakenly discards the constructed error (rather than routing it
2028// through `return Err(…)`) trips a compile warning at the wire-up site.
2029//
2030// Peer with the three prior `LayoutError`-envelope constructor families
2031// — the four together now fold every uniform-shape `LayoutError` variant
2032// carried by [`StandardLayout::verify`] onto one substrate primitive per
2033// typed variant, so every layout-side error-wrap on `LayoutError` reads
2034// through one dispatch per variant rather than N open-coded blocks.
2035// Every future consumer that wants to construct one of these variants
2036// outside the layout pipeline (a per-slot admission webhook probing an
2037// Acao's `:ci` slot, a `feira validate --kind <X>` verb refusing a
2038// no-code kind that declares `:bibliotecas` / `:exe` / `:servicos`, an
2039// overlay resolver rejecting a required-slot omission against a
2040// cluster-local snapshot) reaches its variant through one call, matching
2041// the `_violation` / `_slots_on_non_*` / `missing_entry` families'
2042// substrate-primitive discipline.
2043macro_rules! layout_nome_only_ctors {
2044 ($($ctor:ident => $variant:ident),* $(,)?) => {
2045 impl LayoutError {
2046 $(
2047 #[doc = concat!(
2048 "Construct a [`LayoutError::",
2049 stringify!($variant),
2050 "`] naming the offending `caixa.nome()`. Folds the ",
2051 "uniform `Self::",
2052 stringify!($variant),
2053 "(caixa.nome().to_string())` one-field tuple-",
2054 "literal onto one substrate primitive so every ",
2055 "[`StandardLayout::verify`] wire-up on this variant ",
2056 "reads through one dispatch rather than the pre-lift ",
2057 "open-coded block."
2058 )]
2059 #[must_use]
2060 pub fn $ctor(caixa: &crate::Caixa) -> Self {
2061 Self::$variant(caixa.nome().to_string())
2062 }
2063 )*
2064 }
2065 };
2066}
2067
2068layout_nome_only_ctors! {
2069 binario_without_exe => BinarioWithoutExe,
2070 servico_without_servicos => ServicoWithoutServicos,
2071 missing_ci => MissingCi,
2072 supervisor_owns_code => SupervisorOwnsCode,
2073 aplicacao_owns_code => AplicacaoOwnsCode,
2074 acao_owns_code => AcaoOwnsCode,
2075}
2076
2077#[cfg(test)]
2078mod tests {
2079 use super::*;
2080 use crate::{Caixa, CaixaKind};
2081 use std::path::PathBuf;
2082
2083 fn caixa(kind: CaixaKind) -> Caixa {
2084 Caixa {
2085 nome: "demo".into(),
2086 versao: "0.1.0".into(),
2087 kind,
2088 edicao: None,
2089 descricao: None,
2090 repositorio: None,
2091 licenca: None,
2092 autores: vec![],
2093 etiquetas: vec![],
2094 deps: vec![],
2095 deps_dev: vec![],
2096 exe: vec![],
2097 bibliotecas: vec![],
2098 servicos: vec![],
2099 // M2 typed-substrate slots default to absent.
2100 limits: None,
2101 behavior: None,
2102 upgrade_from: vec![],
2103 estrategia: None,
2104 max_restarts: None,
2105 restart_window: None,
2106 children: vec![],
2107 // M3 Aplicacao slots default to absent.
2108 membros: vec![],
2109 contratos: vec![],
2110 politicas: None,
2111 placement: None,
2112 entrada: None,
2113 ci: None,
2114 }
2115 }
2116
2117 #[test]
2118 fn missing_manifest_errors() {
2119 let layout = StandardLayout::new().with_path_exists(|_| false);
2120 let err = layout
2121 .verify(&caixa(CaixaKind::Biblioteca), Path::new("/tmp/x"))
2122 .unwrap_err();
2123 assert!(matches!(err, LayoutError::MissingManifest(_)));
2124 }
2125
2126 #[test]
2127 fn missing_manifest_ctor_matches_tuple_literal_wrap() {
2128 // Equivalence pin locking [`LayoutError::missing_manifest`] to
2129 // its tuple-literal peer under PartialEq. The pre-lift wire-up
2130 // at [`StandardLayout::verify`]'s top-of-body manifest-existence
2131 // gate read `LayoutError::MissingManifest(manifest)` — a two-
2132 // line open-coded tuple-literal against the resolved
2133 // `root.join("caixa.lisp")` `PathBuf`. The post-lift dispatch
2134 // reads `LayoutError::missing_manifest(manifest)`. Both must
2135 // produce byte-equal variants — a silent divergence (a
2136 // `.canonicalize()` slip on the ctor body, a stray `.clone()`
2137 // that duplicates the payload's underlying buffer, an
2138 // accidental `.into()` that reshapes the `PathBuf` bytes on
2139 // one side and not the other) surfaces here rather than at a
2140 // downstream diagnostic-shape drift far from the ctor
2141 // definition.
2142 //
2143 // Peer of the sibling
2144 // [`missing_entry_ctor_matches_struct_literal_wrap`] /
2145 // [`missing_lib_ctor_matches_struct_literal_wrap`] /
2146 // [`ci_on_non_acao_ctor_matches_struct_literal_wrap`] pins on
2147 // the same [`LayoutError`] envelope — closes the last
2148 // un-pinned inherent-ctor equivalence-against-open-coded-
2149 // literal pin on the envelope.
2150 let path = PathBuf::from("/tmp/x/caixa.lisp");
2151 assert_eq!(
2152 LayoutError::missing_manifest(path.clone()),
2153 LayoutError::MissingManifest(path),
2154 "missing_manifest ctor must byte-equal the pre-lift tuple-literal",
2155 );
2156 }
2157
2158 #[test]
2159 fn verify_missing_manifest_gate_routes_through_missing_manifest_ctor() {
2160 // Fail-before-pass-after routing pin: the pre-lift
2161 // [`StandardLayout::verify`] wire-up hand-rolled
2162 // `LayoutError::MissingManifest(manifest)` at its top-of-body
2163 // manifest-existence arm; the post-lift dispatch routes the
2164 // same arm through
2165 // [`LayoutError::missing_manifest`]. This pin sweeps a fixture
2166 // whose oracle refuses every path (`with_path_exists(|_| false)`
2167 // — the same shape [`missing_manifest_errors`] uses to prove
2168 // the gate fires at all) and asserts that the emitted
2169 // [`LayoutError`] equals the ctor-built error verbatim under
2170 // [`PartialEq`]. A future de-lift of this wire-up (a
2171 // hand-rolled `Self::MissingManifest(_)` reintroduced at the
2172 // gate, an intercept that unwraps the ctor's payload before
2173 // re-emitting) trips this pin at layout.rs build time rather
2174 // than surfacing as a drift-detection failure at a downstream
2175 // diagnostic-shape consumer far from the wire-up site.
2176 //
2177 // Same discipline the peer
2178 // [`ci_on_non_acao_ctor_matches_struct_literal_wrap`] /
2179 // [`missing_entry_ctor_routes_kind_through_arg_verbatim`] /
2180 // [`missing_lib_ctor_projects_nome_through_accessor`] pins on
2181 // the sibling [`LayoutError`] envelope's per-variant substrate-
2182 // primitive ctor family already carry — extended here onto
2183 // the last un-pinned wire-up-routing gate on the envelope.
2184 let root = Path::new("/tmp/routing-pin");
2185 let layout = StandardLayout::new().with_path_exists(|_| false);
2186 let err = layout
2187 .verify(&caixa(CaixaKind::Biblioteca), root)
2188 .unwrap_err();
2189 assert_eq!(
2190 err,
2191 LayoutError::missing_manifest(root.join("caixa.lisp")),
2192 "StandardLayout::verify's manifest-existence gate must route through \
2193 LayoutError::missing_manifest with the resolved `root.join(\"caixa.lisp\")` \
2194 payload verbatim",
2195 );
2196 }
2197
2198 // ── LayoutError::*_violation constructor family ──────────────────────
2199 //
2200 // The [`layout_violation_ctors!`] macro (below the `LayoutError` enum
2201 // definition) generates one static constructor per `*Violation { caixa,
2202 // issue }` variant that folds the uniform `{ caixa: caixa.nome()
2203 // .to_string(), issue: err.to_string() }` two-slot construction onto
2204 // one substrate primitive. The per-variant equivalence pins below
2205 // (fail-before-pass-after by construction — a byte-mismatched macro
2206 // arm would trip its equivalence pin first) lock each generated
2207 // constructor to its struct-literal peer under `PartialEq`, so every
2208 // wire-up in [`StandardLayout::verify`] on that variant produces a
2209 // byte-equal `LayoutError` to the pre-lift open-coded block. The
2210 // fixture caixa fires under `caixa("demo")` so the `caixa: "demo"`
2211 // half is pinned; the fixture error fires under a fixed `&str` so the
2212 // `issue: <literal>` half is pinned; the two together pin every field
2213 // of every generated variant.
2214
2215 fn layout_violation_ctor_fixture() -> (Caixa, &'static str) {
2216 (caixa(CaixaKind::Biblioteca), "sample issue text")
2217 }
2218
2219 // `assert_eq!` uses `PartialEq::eq(&self, &other)` under the hood, so
2220 // `actual`/`expected` are only ever read, never moved into anything —
2221 // the ergonomic tradeoff (owned + move-in vs. reference + &-borrow at
2222 // every call site) favors the owned form for a test-only assertion
2223 // helper called from 17 wire-up pins. The lint targets the general
2224 // API-shape case where callers still have downstream uses for the
2225 // moved value; the assertion helper terminates on the equality check.
2226 #[allow(clippy::needless_pass_by_value)]
2227 fn assert_violation_ctor_matches(actual: LayoutError, expected: LayoutError) {
2228 assert_eq!(
2229 actual, expected,
2230 "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2231 );
2232 }
2233
2234 #[test]
2235 fn nome_violation_ctor_matches_struct_literal_wrap() {
2236 let (c, issue) = layout_violation_ctor_fixture();
2237 assert_violation_ctor_matches(
2238 LayoutError::nome_violation(&c, issue),
2239 LayoutError::NomeViolation {
2240 caixa: c.nome().to_string(),
2241 issue: issue.to_string(),
2242 },
2243 );
2244 }
2245
2246 #[test]
2247 fn versao_violation_ctor_matches_struct_literal_wrap() {
2248 let (c, issue) = layout_violation_ctor_fixture();
2249 assert_violation_ctor_matches(
2250 LayoutError::versao_violation(&c, issue),
2251 LayoutError::VersaoViolation {
2252 caixa: c.nome().to_string(),
2253 issue: issue.to_string(),
2254 },
2255 );
2256 }
2257
2258 #[test]
2259 fn deps_violation_ctor_matches_struct_literal_wrap() {
2260 let (c, issue) = layout_violation_ctor_fixture();
2261 assert_violation_ctor_matches(
2262 LayoutError::deps_violation(&c, issue),
2263 LayoutError::DepsViolation {
2264 caixa: c.nome().to_string(),
2265 issue: issue.to_string(),
2266 },
2267 );
2268 }
2269
2270 #[test]
2271 fn etiquetas_violation_ctor_matches_struct_literal_wrap() {
2272 let (c, issue) = layout_violation_ctor_fixture();
2273 assert_violation_ctor_matches(
2274 LayoutError::etiquetas_violation(&c, issue),
2275 LayoutError::EtiquetasViolation {
2276 caixa: c.nome().to_string(),
2277 issue: issue.to_string(),
2278 },
2279 );
2280 }
2281
2282 #[test]
2283 fn autores_violation_ctor_matches_struct_literal_wrap() {
2284 let (c, issue) = layout_violation_ctor_fixture();
2285 assert_violation_ctor_matches(
2286 LayoutError::autores_violation(&c, issue),
2287 LayoutError::AutoresViolation {
2288 caixa: c.nome().to_string(),
2289 issue: issue.to_string(),
2290 },
2291 );
2292 }
2293
2294 #[test]
2295 fn repositorio_violation_ctor_matches_struct_literal_wrap() {
2296 let (c, issue) = layout_violation_ctor_fixture();
2297 assert_violation_ctor_matches(
2298 LayoutError::repositorio_violation(&c, issue),
2299 LayoutError::RepositorioViolation {
2300 caixa: c.nome().to_string(),
2301 issue: issue.to_string(),
2302 },
2303 );
2304 }
2305
2306 #[test]
2307 fn descricao_violation_ctor_matches_struct_literal_wrap() {
2308 let (c, issue) = layout_violation_ctor_fixture();
2309 assert_violation_ctor_matches(
2310 LayoutError::descricao_violation(&c, issue),
2311 LayoutError::DescricaoViolation {
2312 caixa: c.nome().to_string(),
2313 issue: issue.to_string(),
2314 },
2315 );
2316 }
2317
2318 #[test]
2319 fn licenca_violation_ctor_matches_struct_literal_wrap() {
2320 let (c, issue) = layout_violation_ctor_fixture();
2321 assert_violation_ctor_matches(
2322 LayoutError::licenca_violation(&c, issue),
2323 LayoutError::LicencaViolation {
2324 caixa: c.nome().to_string(),
2325 issue: issue.to_string(),
2326 },
2327 );
2328 }
2329
2330 #[test]
2331 fn edicao_violation_ctor_matches_struct_literal_wrap() {
2332 let (c, issue) = layout_violation_ctor_fixture();
2333 assert_violation_ctor_matches(
2334 LayoutError::edicao_violation(&c, issue),
2335 LayoutError::EdicaoViolation {
2336 caixa: c.nome().to_string(),
2337 issue: issue.to_string(),
2338 },
2339 );
2340 }
2341
2342 #[test]
2343 fn code_path_violation_ctor_matches_struct_literal_wrap() {
2344 let (c, issue) = layout_violation_ctor_fixture();
2345 assert_violation_ctor_matches(
2346 LayoutError::code_path_violation(&c, issue),
2347 LayoutError::CodePathViolation {
2348 caixa: c.nome().to_string(),
2349 issue: issue.to_string(),
2350 },
2351 );
2352 }
2353
2354 #[test]
2355 fn limits_violation_ctor_matches_struct_literal_wrap() {
2356 let (c, issue) = layout_violation_ctor_fixture();
2357 assert_violation_ctor_matches(
2358 LayoutError::limits_violation(&c, issue),
2359 LayoutError::LimitsViolation {
2360 caixa: c.nome().to_string(),
2361 issue: issue.to_string(),
2362 },
2363 );
2364 }
2365
2366 #[test]
2367 fn behavior_violation_ctor_matches_struct_literal_wrap() {
2368 let (c, issue) = layout_violation_ctor_fixture();
2369 assert_violation_ctor_matches(
2370 LayoutError::behavior_violation(&c, issue),
2371 LayoutError::BehaviorViolation {
2372 caixa: c.nome().to_string(),
2373 issue: issue.to_string(),
2374 },
2375 );
2376 }
2377
2378 #[test]
2379 fn upgrade_violation_ctor_matches_struct_literal_wrap() {
2380 let (c, issue) = layout_violation_ctor_fixture();
2381 assert_violation_ctor_matches(
2382 LayoutError::upgrade_violation(&c, issue),
2383 LayoutError::UpgradeViolation {
2384 caixa: c.nome().to_string(),
2385 issue: issue.to_string(),
2386 },
2387 );
2388 }
2389
2390 #[test]
2391 fn restart_window_violation_ctor_matches_struct_literal_wrap() {
2392 let (c, issue) = layout_violation_ctor_fixture();
2393 assert_violation_ctor_matches(
2394 LayoutError::restart_window_violation(&c, issue),
2395 LayoutError::RestartWindowViolation {
2396 caixa: c.nome().to_string(),
2397 issue: issue.to_string(),
2398 },
2399 );
2400 }
2401
2402 #[test]
2403 fn supervisor_violation_ctor_matches_struct_literal_wrap() {
2404 let (c, issue) = layout_violation_ctor_fixture();
2405 assert_violation_ctor_matches(
2406 LayoutError::supervisor_violation(&c, issue),
2407 LayoutError::SupervisorViolation {
2408 caixa: c.nome().to_string(),
2409 issue: issue.to_string(),
2410 },
2411 );
2412 }
2413
2414 #[test]
2415 fn aplicacao_violation_ctor_matches_struct_literal_wrap() {
2416 let (c, issue) = layout_violation_ctor_fixture();
2417 assert_violation_ctor_matches(
2418 LayoutError::aplicacao_violation(&c, issue),
2419 LayoutError::AplicacaoViolation {
2420 caixa: c.nome().to_string(),
2421 issue: issue.to_string(),
2422 },
2423 );
2424 }
2425
2426 #[test]
2427 fn acao_violation_ctor_matches_struct_literal_wrap() {
2428 // Sibling of [`aplicacao_violation_ctor_matches_struct_literal_wrap`]
2429 // / [`supervisor_violation_ctor_matches_struct_literal_wrap`] on
2430 // the third per-kind compound-shape wrap envelope on
2431 // `LayoutError`. Pins the macro-generated `acao_violation`
2432 // constructor to its struct-literal peer under `PartialEq`, so
2433 // every wire-up in [`StandardLayout::verify`] on the
2434 // [`LayoutError::AcaoViolation`] variant produces a byte-equal
2435 // `LayoutError` to the pre-lift open-coded block. Closes the
2436 // pin family the peer per-kind shape wraps already carry.
2437 let (c, issue) = layout_violation_ctor_fixture();
2438 assert_violation_ctor_matches(
2439 LayoutError::acao_violation(&c, issue),
2440 LayoutError::AcaoViolation {
2441 caixa: c.nome().to_string(),
2442 issue: issue.to_string(),
2443 },
2444 );
2445 }
2446
2447 #[test]
2448 fn violation_ctor_routes_issue_through_display_impl() {
2449 // Pin the fold's `issue = err.to_string()` half against any type
2450 // implementing `Display` — a per-arm error type from a foreign
2451 // module (here, `std::io::Error`) threads through byte-equal to
2452 // the struct-literal `.to_string()` construction, so the fold
2453 // does not silently collapse onto `&str`-only inputs.
2454 let c = caixa(CaixaKind::Biblioteca);
2455 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "sample io display source");
2456 let expected_issue = io_err.to_string();
2457 let actual = LayoutError::deps_violation(&c, &io_err);
2458 assert_eq!(
2459 actual,
2460 LayoutError::DepsViolation {
2461 caixa: c.nome().to_string(),
2462 issue: expected_issue,
2463 },
2464 );
2465 }
2466
2467 #[test]
2468 fn violation_ctor_routes_caixa_prefix_through_nome_accessor() {
2469 // Pin the fold's `caixa = caixa.nome().to_string()` half against
2470 // a non-default `:nome` — the accessor threads the caller's
2471 // `:nome` verbatim into the wrap envelope, so the fold does not
2472 // silently collapse onto the default `"demo"` fixture nome.
2473 let mut c = caixa(CaixaKind::Biblioteca);
2474 c.nome = "alt-nome".into();
2475 let actual = LayoutError::behavior_violation(&c, "sample issue");
2476 assert_eq!(
2477 actual,
2478 LayoutError::BehaviorViolation {
2479 caixa: "alt-nome".to_string(),
2480 issue: "sample issue".to_string(),
2481 },
2482 );
2483 }
2484
2485 // ── Caixa::run_layout_gate — per-slot gate + LayoutError wrap fold ───
2486 //
2487 // The [`Caixa::run_layout_gate`] substrate primitive folds the 18
2488 // self-similar `caixa.validate_<slot>().map_err(|err| LayoutError::
2489 // <slot>_violation(caixa, err))?;` wire-up sites at
2490 // [`StandardLayout::verify`] onto one dispatch. The pins below
2491 // (fail-before-pass-after by construction — a silent regression that
2492 // de-folded either arm would trip its own pin first) lock the two
2493 // arms of the fold under `PartialEq`:
2494 //
2495 // - The `Ok(())` identity-element arm passes through verbatim (no
2496 // wrap runs, no `caixa` closure capture).
2497 // - The `Err(E)` arm routes through the caller-supplied `wrap`
2498 // ctor with `self` bound as the caixa slot, byte-equal to the
2499 // pre-lift `.map_err(|err| CTOR(caixa, err))` closure.
2500 //
2501 // The third pin (per-arm equivalence) runs the primitive with the
2502 // canonical `Caixa::validate_nome` validator and the paired
2503 // `LayoutError::nome_violation` ctor on a fixture whose `:nome`
2504 // fails `validate_nome`'s DNS-1123 gate ("Bad_Nome" — the uppercase
2505 // + underscore double footgun) and asserts the primitive's
2506 // `Result<(), LayoutError>` matches the open-coded pre-lift cascade
2507 // on the same fixture. A silent regression that de-folded the wrap
2508 // (dropped the `self` binding, threaded a stale caixa nome, swapped
2509 // the wrap ctor) would surface here as a mismatch between the two
2510 // dispatches.
2511
2512 #[test]
2513 fn run_layout_gate_ok_arm_passes_through() {
2514 // Positive control on the identity-element arm: a gate returning
2515 // `Ok(())` short-circuits before the wrap runs, so the caller
2516 // receives `Ok(())` verbatim regardless of what the paired ctor
2517 // would have produced. Pins the fold's `Result::map_err`
2518 // short-circuit semantics — a regression that unconditionally
2519 // wrapped (e.g. always called the ctor) would trip here.
2520 let c = caixa(CaixaKind::Biblioteca);
2521 let result: Result<(), LayoutError> = c.run_layout_gate(
2522 |_c: &Caixa| Ok::<(), &'static str>(()),
2523 |_c: &Caixa, _err: &'static str| {
2524 panic!("wrap must not run on the Ok(()) arm of the fold")
2525 },
2526 );
2527 assert!(
2528 result.is_ok(),
2529 "run_layout_gate must pass Ok(()) through verbatim, got {result:?}"
2530 );
2531 }
2532
2533 #[test]
2534 fn run_layout_gate_err_arm_wraps_via_ctor() {
2535 // Positive control on the err arm: a gate returning `Err(E)`
2536 // threads `E` into the caller-supplied `wrap` ctor with `self`
2537 // bound as the caixa argument. Uses a synthetic `&'static str`
2538 // error and the canonical `LayoutError::deps_violation` ctor so
2539 // the pin covers the fold's two-callable dispatch shape without
2540 // depending on any per-slot validator's specific arm sweep.
2541 let c = caixa(CaixaKind::Biblioteca);
2542 let sample_reason = "sample gate reason";
2543 let result = c.run_layout_gate(
2544 |_c: &Caixa| Err::<(), &'static str>(sample_reason),
2545 LayoutError::deps_violation,
2546 );
2547 let err = result.expect_err("Err arm must reach the caller");
2548 assert_eq!(
2549 err,
2550 LayoutError::DepsViolation {
2551 caixa: c.nome().to_string(),
2552 issue: sample_reason.to_string(),
2553 },
2554 "run_layout_gate must wrap the gate's Err via the caller-supplied \
2555 ctor with `self` bound as the caixa slot"
2556 );
2557 }
2558
2559 #[test]
2560 fn run_layout_gate_folds_arm_matches_gate() {
2561 // Fail-before-pass-after per-arm equivalence pin on the err arm:
2562 // a fixture whose `:nome` fails `validate_nome`'s DNS-1123 gate
2563 // (`"Bad_Nome"` — uppercase + underscore double footgun the
2564 // [`crate::ManifestError::NomeInvalid`] arm rejects) surfaces
2565 // the same `LayoutError::NomeViolation` byte-equal through both
2566 // the primitive `Caixa::run_layout_gate(Caixa::validate_nome,
2567 // LayoutError::nome_violation)` and the open-coded pre-lift
2568 // cascade `Caixa::validate_nome().map_err(|err|
2569 // LayoutError::nome_violation(&c, err))` on the same Caixa
2570 // fixture. Pins the fold — a silent regression that de-folded
2571 // either arm (dropped the `self` binding, threaded a stale
2572 // caixa nome, swapped the wrap ctor) would surface here as a
2573 // mismatch between the two dispatches.
2574 let mut c = caixa(CaixaKind::Biblioteca);
2575 c.nome = "Bad_Nome".into();
2576 let via_primitive = c
2577 .run_layout_gate(Caixa::validate_nome, LayoutError::nome_violation)
2578 .expect_err("Bad_Nome must fail validate_nome");
2579 let via_open_coded = c
2580 .validate_nome()
2581 .map_err(|err| LayoutError::nome_violation(&c, err))
2582 .expect_err("Bad_Nome must fail validate_nome");
2583 assert_eq!(
2584 via_primitive, via_open_coded,
2585 "Caixa::run_layout_gate must surface the err-arm diagnostic \
2586 byte-equal to the open-coded `.map_err(|err| CTOR(caixa, err))` \
2587 cascade on the same Caixa fixture"
2588 );
2589 assert!(
2590 matches!(via_primitive, LayoutError::NomeViolation { .. }),
2591 "expected NomeViolation on Bad_Nome, got {via_primitive:?}"
2592 );
2593 }
2594
2595 // ── LayoutError kind-coherence constructor family ────────────────────
2596 //
2597 // The [`layout_slot_kind_ctors!`] macro (sibling of
2598 // [`layout_violation_ctors!`] beside the `LayoutError` enum definition)
2599 // generates one static constructor per `*SlotsOn*` / `ForeignCodeSlot`
2600 // variant that folds the uniform `{ caixa: caixa.nome().to_string(),
2601 // kind: caixa.kind(), slots: slots.join(" ") }` four-field construction
2602 // onto one substrate primitive. The per-variant equivalence pins below
2603 // (fail-before-pass-after by construction — a byte-mismatched macro arm
2604 // would trip its equivalence pin first) lock each generated constructor
2605 // to its struct-literal peer under `PartialEq`, so every wire-up in
2606 // [`StandardLayout::verify`] on that variant produces a byte-equal
2607 // `LayoutError` to the pre-lift open-coded block. The three cross-axis
2608 // pins that follow (non-default `:nome`, non-default kind, non-trivial
2609 // slots list) route each of the three constructor input axes through
2610 // its declared accessor / arg, so the fold does not silently collapse
2611 // onto a fixture default on any axis.
2612
2613 fn layout_slot_kind_ctor_fixture() -> (Caixa, Vec<&'static str>) {
2614 (caixa(CaixaKind::Biblioteca), vec![":membros", ":contratos"])
2615 }
2616
2617 // Same rationale as `assert_violation_ctor_matches` above: the helper
2618 // terminates on the equality check, so the owned-arg lint's general
2619 // API-shape target does not apply.
2620 #[allow(clippy::needless_pass_by_value)]
2621 fn assert_slot_kind_ctor_matches(actual: LayoutError, expected: LayoutError) {
2622 assert_eq!(
2623 actual, expected,
2624 "generated constructor must produce byte-equal LayoutError to open-coded struct-literal wrap",
2625 );
2626 }
2627
2628 #[test]
2629 fn mesh_slots_on_non_aplicacao_ctor_matches_struct_literal_wrap() {
2630 let (c, slots) = layout_slot_kind_ctor_fixture();
2631 assert_slot_kind_ctor_matches(
2632 LayoutError::mesh_slots_on_non_aplicacao(&c, slots.clone()),
2633 LayoutError::MeshSlotsOnNonAplicacao {
2634 caixa: c.nome().to_string(),
2635 kind: c.kind(),
2636 slots: slots.join(" "),
2637 },
2638 );
2639 }
2640
2641 #[test]
2642 fn supervisor_slots_on_non_supervisor_ctor_matches_struct_literal_wrap() {
2643 let (c, slots) = layout_slot_kind_ctor_fixture();
2644 assert_slot_kind_ctor_matches(
2645 LayoutError::supervisor_slots_on_non_supervisor(&c, slots.clone()),
2646 LayoutError::SupervisorSlotsOnNonSupervisor {
2647 caixa: c.nome().to_string(),
2648 kind: c.kind(),
2649 slots: slots.join(" "),
2650 },
2651 );
2652 }
2653
2654 #[test]
2655 fn servico_slots_on_non_servico_ctor_matches_struct_literal_wrap() {
2656 let (c, slots) = layout_slot_kind_ctor_fixture();
2657 assert_slot_kind_ctor_matches(
2658 LayoutError::servico_slots_on_non_servico(&c, slots.clone()),
2659 LayoutError::ServicoSlotsOnNonServico {
2660 caixa: c.nome().to_string(),
2661 kind: c.kind(),
2662 slots: slots.join(" "),
2663 },
2664 );
2665 }
2666
2667 #[test]
2668 fn foreign_code_slot_ctor_matches_struct_literal_wrap() {
2669 let (c, slots) = layout_slot_kind_ctor_fixture();
2670 assert_slot_kind_ctor_matches(
2671 LayoutError::foreign_code_slot(&c, slots.clone()),
2672 LayoutError::ForeignCodeSlot {
2673 caixa: c.nome().to_string(),
2674 kind: c.kind(),
2675 slots: slots.join(" "),
2676 },
2677 );
2678 }
2679
2680 #[test]
2681 fn slot_kind_ctor_routes_caixa_prefix_through_nome_accessor() {
2682 // Pin the fold's `caixa = caixa.nome().to_string()` half against a
2683 // non-default `:nome` — the accessor threads the caller's `:nome`
2684 // verbatim into the wrap envelope, so the fold does not silently
2685 // collapse onto the default `"demo"` fixture nome. Peer of the
2686 // sibling `violation_ctor_routes_caixa_prefix_through_nome_accessor`
2687 // pin on the `{ caixa, issue }` envelope; extended here onto the
2688 // `{ caixa, kind, slots }` envelope so both `LayoutError`-shape
2689 // constructor families guarantee the `:nome`-derived-caixa slot
2690 // routes through [`Caixa::nome`] rather than a hard-coded string.
2691 let mut c = caixa(CaixaKind::Biblioteca);
2692 c.nome = "alt-nome".into();
2693 let actual = LayoutError::mesh_slots_on_non_aplicacao(&c, vec![":membros"]);
2694 assert_eq!(
2695 actual,
2696 LayoutError::MeshSlotsOnNonAplicacao {
2697 caixa: "alt-nome".to_string(),
2698 kind: CaixaKind::Biblioteca,
2699 slots: ":membros".to_string(),
2700 },
2701 );
2702 }
2703
2704 #[test]
2705 fn slot_kind_ctor_routes_kind_through_caixa_kind_accessor() {
2706 // Pin the fold's `kind = caixa.kind()` half against a non-default
2707 // kind — the accessor threads the caller's `:kind` verbatim into
2708 // the wrap envelope, so the fold does not silently collapse onto
2709 // one hard-coded kind. Sweeps every non-Aplicacao / non-Supervisor
2710 // / non-Servico kind the corresponding gate can fire on so the
2711 // pin covers the kind-derivation axis on every downstream variant.
2712 for kind in [
2713 CaixaKind::Biblioteca,
2714 CaixaKind::Binario,
2715 CaixaKind::Servico,
2716 CaixaKind::Supervisor,
2717 CaixaKind::Aplicacao,
2718 CaixaKind::Acao,
2719 ] {
2720 let c = caixa(kind);
2721 let actual = LayoutError::foreign_code_slot(&c, vec![":exe"]);
2722 assert_eq!(
2723 actual,
2724 LayoutError::ForeignCodeSlot {
2725 caixa: c.nome().to_string(),
2726 kind,
2727 slots: ":exe".to_string(),
2728 },
2729 "foreign_code_slot ctor must thread `caixa.kind()` verbatim on every kind",
2730 );
2731 }
2732 }
2733
2734 #[test]
2735 fn slot_kind_ctor_routes_slots_through_join_separator() {
2736 // Pin the fold's `slots = slots.join(" ")` half against a
2737 // multi-entry slots list — the join threads exactly one ASCII
2738 // space between entries, in caller-supplied order, so the fold
2739 // does not silently collapse onto a fixed separator (`", "`, `";
2740 // "`, `"\n"`), a sorted order, or a single-entry pass-through.
2741 // Uses the M2 servico-slot vocabulary since these are what the
2742 // corresponding `servico_slots_on_non_servico` gate reports.
2743 let c = caixa(CaixaKind::Biblioteca);
2744 let actual = LayoutError::servico_slots_on_non_servico(
2745 &c,
2746 vec![":limits", ":behavior", ":upgrade-from"],
2747 );
2748 assert_eq!(
2749 actual,
2750 LayoutError::ServicoSlotsOnNonServico {
2751 caixa: c.nome().to_string(),
2752 kind: c.kind(),
2753 slots: ":limits :behavior :upgrade-from".to_string(),
2754 },
2755 );
2756 }
2757
2758 // ── LayoutError::missing_entry substrate-primitive constructor ───────
2759 //
2760 // The [`LayoutError::missing_entry`] constructor beside the enum
2761 // definition folds the `{ kind: &'static str, path: PathBuf }`
2762 // uniform-shape envelope onto one substrate primitive — the third
2763 // and last uniform-shape envelope on `LayoutError` after the
2764 // `{ caixa, issue }` family the [`layout_violation_ctors!`] macro
2765 // closed (131ca0d) and the `{ caixa, kind, slots }` family the peer
2766 // [`layout_slot_kind_ctors!`] macro closed (0419438). The pins below
2767 // (fail-before-pass-after by construction — a byte-mismatched
2768 // constructor arm would trip its equivalence pin first) lock the
2769 // constructor to its struct-literal peer under `PartialEq`, so every
2770 // wire-up in [`StandardLayout::verify`] on this variant produces a
2771 // byte-equal `LayoutError` to the pre-lift open-coded block. The two
2772 // cross-axis pins that follow (canonical-kind-label sweep, non-
2773 // default path) route each of the two constructor input axes through
2774 // its arg verbatim, so the fold does not silently collapse onto a
2775 // fixture default on either axis.
2776
2777 #[test]
2778 fn missing_entry_ctor_matches_struct_literal_wrap() {
2779 // Per-envelope equivalence pin — the `missing_entry` constructor
2780 // produces a `LayoutError::MissingEntry` byte-equal under
2781 // `PartialEq` to the open-coded four-line struct-literal wrap on
2782 // the same `(kind, path)` fixture. Peer of the sibling
2783 // `<slot>_violation_ctor_matches_struct_literal_wrap` /
2784 // `<slot>_slots_on_non_<owner>_ctor_matches_struct_literal_wrap`
2785 // pins on the two prior uniform-shape envelopes on the same
2786 // `LayoutError`.
2787 let path = PathBuf::from("/tmp/x/lib/demo.lisp");
2788 assert_eq!(
2789 LayoutError::missing_entry(
2790 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2791 path.clone(),
2792 ),
2793 LayoutError::MissingEntry {
2794 kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2795 path,
2796 },
2797 );
2798 }
2799
2800 #[test]
2801 fn missing_entry_ctor_routes_kind_through_arg_verbatim() {
2802 // Pin the fold's `kind: &'static str` arg through every canonical
2803 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2804 // wire-up sites in [`StandardLayout::verify`] pass — so the fold
2805 // does not silently collapse onto one hard-coded label. Sweep
2806 // matches the arm set the peer
2807 // `layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`
2808 // pin (below) covers on the const-label declarations.
2809 let path = PathBuf::from("/tmp/x/entry");
2810 for kind in [
2811 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2812 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2813 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2814 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2815 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2816 ] {
2817 assert_eq!(
2818 LayoutError::missing_entry(kind, path.clone()),
2819 LayoutError::MissingEntry {
2820 kind,
2821 path: path.clone(),
2822 },
2823 "missing_entry ctor must thread `kind` verbatim on every canonical label",
2824 );
2825 }
2826 }
2827
2828 #[test]
2829 fn missing_entry_ctor_routes_path_through_arg_verbatim() {
2830 // Pin the fold's `path: PathBuf` arg against a non-default,
2831 // multi-component `PathBuf` — the ctor threads the caller's
2832 // `PathBuf` verbatim into the wrap envelope, so the fold does
2833 // not silently collapse onto a fixed component prefix, a
2834 // canonicalized form, or a single-component pass-through.
2835 let path = PathBuf::from("/alt/root")
2836 .join("servicos")
2837 .join("hello-rio.computeunit.yaml");
2838 assert_eq!(
2839 LayoutError::missing_entry(
2840 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2841 path.clone(),
2842 ),
2843 LayoutError::MissingEntry {
2844 kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2845 path,
2846 },
2847 );
2848 }
2849
2850 #[test]
2851 fn missing_entry_ctor_is_const_fn_usable_in_const_position() {
2852 // Fail-before-pass-after pin on [`LayoutError::missing_entry`]'s
2853 // `pub const fn` shape — a future accidental downgrade to a
2854 // non-`const` `pub fn` (a body reversion that reaches for a
2855 // `.to_string()` / `.into()` / accessor projection on either
2856 // arg, matching the sibling [`LayoutError::missing_lib`] /
2857 // [`LayoutError::ci_on_non_acao`] non-const-body shape) trips
2858 // here at caixa-core build time with "cannot call non-const fn
2859 // `LayoutError::missing_entry` in constants" rather than
2860 // surfacing as a downstream `const`-context regression at a
2861 // future M4 CR materializer's admission-webhook per-kind
2862 // rejection-body lookup table, a compile-time per-arm
2863 // diagnostic registry the LSP hover renderer materializes, or
2864 // a `const`-context probe the future `feira validate
2865 // --entry-exists` per-caixa verb consults.
2866 //
2867 // Peer of the sibling
2868 // [`crate::behavior::tests::behavior_spec_is_empty_is_const_fn_usable_in_const_position`]
2869 // (ba644e7 — `pub const fn` widening of the M2 `:behavior`
2870 // emptiness predicate) and the sibling per-envelope pins on
2871 // the [`crate::supervisor::RestartStrategy::as_str`] /
2872 // [`crate::supervisor::RestartPolicy::as_str`] /
2873 // [`crate::aplicacao::PlacementStrategy::as_str`] const-fn
2874 // widenings — extends the same "one substrate primitive per
2875 // pure-struct-literal ctor stays `pub const fn` so every
2876 // downstream const-context consumer routes through one const
2877 // dispatch" discipline onto the last un-pinned pure-struct-
2878 // literal ctor on the [`LayoutError`] envelope's path-carrying
2879 // axis (the sibling [`Self::missing_manifest`] already carries
2880 // this posture; [`Self::missing_lib`] / [`Self::ci_on_non_acao`]
2881 // stay non-const by design because their bodies project through
2882 // the paired [`crate::Caixa::nome`] `String → String` accessor
2883 // which is not `const`-stable).
2884 const MISSING_BIBLIOTECA: LayoutError = LayoutError::missing_entry(
2885 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2886 PathBuf::new(),
2887 );
2888 assert_eq!(
2889 MISSING_BIBLIOTECA,
2890 LayoutError::MissingEntry {
2891 kind: crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2892 path: PathBuf::new(),
2893 },
2894 "missing_entry const-position dispatch must byte-equal the runtime-position \
2895 struct-literal wrap on the same (kind, path) fixture",
2896 );
2897 }
2898
2899 // ── StandardLayout::probe_declared_entry substrate primitive ─────────
2900 //
2901 // The [`StandardLayout::probe_declared_entry`] method
2902 // (`layout.rs`) folds the five self-similar
2903 // `let full = root.join(p); if !self.exists(&full) { return
2904 // Err(LayoutError::missing_entry(<kind>, full)); }` existence-probe
2905 // blocks at [`StandardLayout::verify`] (`:bibliotecas` iteration,
2906 // `:exe` iteration, `:servicos` iteration, `:behavior` on-disk
2907 // callback-path iteration, `:upgrade-from` per-instruction script-
2908 // path iteration) onto one substrate primitive. The pins below
2909 // (fail-before-pass-after by construction — a byte-mismatched
2910 // primitive body would trip its equivalence pin first) lock the
2911 // fold to its pre-lift open-coded shape under `PartialEq`, so every
2912 // wire-up in [`StandardLayout::verify`] on this primitive produces
2913 // a byte-equal `LayoutError` on miss and a byte-equal `PathBuf` on
2914 // hit.
2915
2916 #[test]
2917 fn probe_declared_entry_folds_miss_returns_missing_entry() {
2918 // Per-primitive equivalence pin on the miss arm — a
2919 // [`StandardLayout`] whose oracle returns `false` for every
2920 // path yields a `MissingEntry` byte-equal under `PartialEq` to
2921 // the open-coded `LayoutError::missing_entry(<kind>,
2922 // root.join(path))` wrap the pre-lift block carried at each of
2923 // the five wire-up sites. Peer of the sibling
2924 // `missing_entry_ctor_matches_struct_literal_wrap` pin on the
2925 // constructor's own byte-equal shape.
2926 let layout = StandardLayout::new().with_path_exists(|_| false);
2927 let root = PathBuf::from("/tmp/x");
2928 let path = Path::new("lib/demo.lisp");
2929 let err = layout
2930 .probe_declared_entry(
2931 path,
2932 &root,
2933 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2934 )
2935 .unwrap_err();
2936 assert_eq!(
2937 err,
2938 LayoutError::missing_entry(
2939 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2940 root.join(path),
2941 ),
2942 "probe_declared_entry miss arm must produce byte-equal LayoutError to \
2943 open-coded `missing_entry(<kind>, root.join(path))` wrap",
2944 );
2945 }
2946
2947 #[test]
2948 fn probe_declared_entry_folds_hit_returns_resolved_full() {
2949 // Per-primitive equivalence pin on the hit arm — a
2950 // [`StandardLayout`] whose oracle returns `true` for the probed
2951 // resolved path yields `Ok(root.join(path))` byte-equal under
2952 // `PartialEq`. The pre-lift `:exe` / `:servicos` wire-up sites
2953 // needed the resolved `full` for the follow-up sandbox-directory-
2954 // containment check; the fold preserves that hand-off through
2955 // the primitive's `Ok(PathBuf)` return arm rather than
2956 // re-computing `root.join(path)` at the follow-up gate.
2957 let root = PathBuf::from("/tmp/x");
2958 let path = Path::new("exe/tool.lisp");
2959 let full = root.join(path);
2960 let full_probe = full.clone();
2961 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
2962 let resolved = layout
2963 .probe_declared_entry(path, &root, crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE)
2964 .expect("probe_declared_entry must return Ok(root.join(path)) when the oracle hits");
2965 assert_eq!(
2966 resolved, full,
2967 "probe_declared_entry hit arm must return the resolved `root.join(path)` \
2968 byte-equal so the `:exe` / `:servicos` sandbox-containment follow-up \
2969 reads it verbatim without re-computing",
2970 );
2971 }
2972
2973 #[test]
2974 fn probe_declared_entry_threads_kind_through_arg_verbatim() {
2975 // Cross-axis pin — sweep every canonical
2976 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the five
2977 // wire-up sites in [`StandardLayout::verify`] pass. Each miss
2978 // must return a `MissingEntry` whose `kind:` field byte-equals
2979 // the caller-provided arg, so the fold does not silently
2980 // collapse onto one hard-coded label. Sweep matches the arm
2981 // set the peer
2982 // `missing_entry_ctor_routes_kind_through_arg_verbatim` pin
2983 // covers on the constructor arg.
2984 let layout = StandardLayout::new().with_path_exists(|_| false);
2985 let root = PathBuf::from("/tmp/x");
2986 let path = Path::new("some/entry");
2987 for kind in [
2988 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
2989 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
2990 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
2991 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
2992 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
2993 ] {
2994 let err = layout.probe_declared_entry(path, &root, kind).unwrap_err();
2995 assert_eq!(
2996 err,
2997 LayoutError::missing_entry(kind, root.join(path)),
2998 "probe_declared_entry must thread `kind` verbatim on every canonical label",
2999 );
3000 }
3001 }
3002
3003 #[test]
3004 fn probe_declared_entry_threads_path_through_arg_verbatim() {
3005 // Cross-axis pin — the primitive must compose the `path` arg
3006 // through `root.join` verbatim on the miss arm's `MissingEntry
3007 // { path: … }` field, so the fold does not silently collapse
3008 // onto a fixed component prefix, a canonicalized form, or a
3009 // hand-authored `root.join(<literal>)`. Sweep two multi-
3010 // component `Path` fixtures (one under `servicos/`, one under
3011 // `lib/`) so a byte-drifted composition on either axis would
3012 // trip.
3013 let layout = StandardLayout::new().with_path_exists(|_| false);
3014 let root = PathBuf::from("/alt/root");
3015 for path in [
3016 Path::new("servicos/hello-rio.computeunit.yaml"),
3017 Path::new("lib/demo.lisp"),
3018 ] {
3019 let err = layout
3020 .probe_declared_entry(
3021 path,
3022 &root,
3023 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3024 )
3025 .unwrap_err();
3026 assert_eq!(
3027 err,
3028 LayoutError::missing_entry(
3029 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3030 root.join(path),
3031 ),
3032 "probe_declared_entry must compose `path` through `root.join` verbatim",
3033 );
3034 }
3035 }
3036
3037 #[test]
3038 fn probe_declared_entry_routes_through_configurable_exists_oracle() {
3039 // Cross-axis pin — the primitive must consult the injected
3040 // [`StandardLayout::with_path_exists`] oracle, not the ambient
3041 // `Path::exists` filesystem probe. Configure a per-path
3042 // discriminator that returns `true` only for one canonical
3043 // resolved path and assert both arms:
3044 // - the "hit" path resolves to `Ok(full)` byte-equal
3045 // - every other path resolves to `MissingEntry` on the same
3046 // [`StandardLayout`] instance
3047 // The pin traps a regression that reroutes the primitive off
3048 // the injected oracle onto the ambient `Path::exists` (which
3049 // would silently return `false` for every path in `/tmp/x/…`
3050 // and mask the miss-arm hand-off on the hit fixture, or
3051 // silently return `true` for a real system path and mask the
3052 // hit-arm hand-off on the miss fixture).
3053 let root = PathBuf::from("/tmp/x");
3054 let hit_path = Path::new("servicos/keep.computeunit.yaml");
3055 let miss_path = Path::new("servicos/drop.computeunit.yaml");
3056 let hit_full = root.join(hit_path);
3057 let hit_full_probe = hit_full.clone();
3058 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_full_probe);
3059
3060 let resolved = layout
3061 .probe_declared_entry(
3062 hit_path,
3063 &root,
3064 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3065 )
3066 .expect("probe_declared_entry must consult the injected oracle on the hit arm");
3067 assert_eq!(
3068 resolved, hit_full,
3069 "probe_declared_entry hit arm must return the oracle-approved resolved path",
3070 );
3071
3072 let err = layout
3073 .probe_declared_entry(
3074 miss_path,
3075 &root,
3076 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3077 )
3078 .unwrap_err();
3079 assert_eq!(
3080 err,
3081 LayoutError::missing_entry(
3082 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3083 root.join(miss_path),
3084 ),
3085 "probe_declared_entry miss arm must fire when the injected oracle rejects the path",
3086 );
3087 }
3088
3089 // ── StandardLayout::probe_sandboxed_declared_entry substrate primitive ─
3090 //
3091 // The [`StandardLayout::probe_sandboxed_declared_entry`] method
3092 // (`layout.rs`) folds the two self-similar `let full = self.
3093 // probe_declared_entry(p, root, <kind>)?; if !full.starts_with(&<slot>_dir)
3094 // { return Err(LayoutError::<Slot>OutsideDir(full)); }` blocks at
3095 // [`StandardLayout::verify`] (`:exe` iteration, `:servicos` iteration)
3096 // onto one substrate primitive. The pins below (fail-before-pass-after
3097 // by construction — a byte-mismatched primitive body would trip its
3098 // equivalence pin first) lock the fold to its pre-lift open-coded
3099 // shape under `PartialEq`, so every wire-up in
3100 // [`StandardLayout::verify`] on this primitive produces a byte-equal
3101 // `LayoutError` on miss / sandbox-escape and a byte-equal `PathBuf`
3102 // on hit.
3103
3104 #[test]
3105 fn probe_sandboxed_declared_entry_folds_miss_returns_missing_entry() {
3106 // Per-primitive equivalence pin on the miss arm — a
3107 // [`StandardLayout`] whose oracle returns `false` for every
3108 // path yields a `MissingEntry` byte-equal under `PartialEq` to
3109 // the open-coded `LayoutError::missing_entry(<kind>,
3110 // root.join(path))` wrap the sibling
3111 // [`StandardLayout::probe_declared_entry`] primitive routes
3112 // through. Diagnostic-order pin: `MissingEntry` outranks the
3113 // `outside_ctor` sandbox-escape arm on the same iteration, so
3114 // an entry that is both absent *and* outside the sandbox fires
3115 // the `MissingEntry` diagnostic (the pre-lift order the two
3116 // wire-up sites carried).
3117 let layout = StandardLayout::new().with_path_exists(|_| false);
3118 let root = PathBuf::from("/tmp/x");
3119 let path = Path::new("lib/tool");
3120 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3121 let err = layout
3122 .probe_sandboxed_declared_entry(
3123 path,
3124 &root,
3125 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3126 &exe_dir,
3127 LayoutError::ExeOutsideDir,
3128 )
3129 .unwrap_err();
3130 assert_eq!(
3131 err,
3132 LayoutError::missing_entry(
3133 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3134 root.join(path),
3135 ),
3136 "probe_sandboxed_declared_entry miss arm must produce byte-equal \
3137 LayoutError to the sibling probe_declared_entry primitive's miss wrap, \
3138 preserving the pre-lift MissingEntry-before-<Slot>OutsideDir diagnostic order",
3139 );
3140 }
3141
3142 #[test]
3143 fn probe_sandboxed_declared_entry_folds_sandbox_escape_via_outside_ctor() {
3144 // Per-primitive equivalence pin on the sandbox-escape arm — a
3145 // [`StandardLayout`] whose oracle admits the probed resolved
3146 // path (so the miss arm passes) but whose resolved path lies
3147 // outside the caller-provided `sandbox_dir` yields the paired
3148 // `outside_ctor(full)` byte-equal under `PartialEq`. The
3149 // primitive threads the resolved `full` through the caller-
3150 // supplied `fn(PathBuf) -> LayoutError` constructor rather
3151 // than a hard-coded variant, so the fold does not silently
3152 // collapse onto one of the two `:exe` / `:servicos` outside-
3153 // dir variants.
3154 let root = PathBuf::from("/tmp/x");
3155 let path = Path::new("lib/tool");
3156 let full = root.join(path);
3157 let full_probe = full.clone();
3158 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3159 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3160 let err = layout
3161 .probe_sandboxed_declared_entry(
3162 path,
3163 &root,
3164 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3165 &exe_dir,
3166 LayoutError::ExeOutsideDir,
3167 )
3168 .unwrap_err();
3169 assert_eq!(
3170 err,
3171 LayoutError::ExeOutsideDir(full),
3172 "probe_sandboxed_declared_entry sandbox-escape arm must route the \
3173 resolved `full` through the caller-supplied outside_ctor byte-equal to \
3174 the pre-lift `LayoutError::ExeOutsideDir(full)` tuple-literal",
3175 );
3176 }
3177
3178 #[test]
3179 fn probe_sandboxed_declared_entry_folds_hit_returns_resolved_full() {
3180 // Per-primitive equivalence pin on the hit arm — a
3181 // [`StandardLayout`] whose oracle admits the probed path *and*
3182 // whose resolved path lives inside `sandbox_dir` yields
3183 // `Ok(root.join(path))` byte-equal under `PartialEq`. The
3184 // pre-lift wire-ups discarded the resolved `Ok(PathBuf)` since
3185 // no follow-up per-path gate consumes it after the sandbox
3186 // check; the fold preserves the same hit-arm hand-off through
3187 // the primitive's `Ok(PathBuf)` return so a future consumer
3188 // that wants to run a per-path successor gate reaches the
3189 // resolved path without re-computing `root.join(path)`.
3190 let root = PathBuf::from("/tmp/x");
3191 let path = Path::new("exe/tool");
3192 let full = root.join(path);
3193 let full_probe = full.clone();
3194 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3195 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3196 let resolved = layout
3197 .probe_sandboxed_declared_entry(
3198 path,
3199 &root,
3200 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3201 &exe_dir,
3202 LayoutError::ExeOutsideDir,
3203 )
3204 .expect(
3205 "probe_sandboxed_declared_entry must return Ok(root.join(path)) \
3206 when the oracle admits the path and it lives inside sandbox_dir",
3207 );
3208 assert_eq!(
3209 resolved, full,
3210 "probe_sandboxed_declared_entry hit arm must return the resolved \
3211 `root.join(path)` byte-equal so a future per-path successor gate \
3212 reaches it without re-computing",
3213 );
3214 }
3215
3216 #[test]
3217 fn probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim() {
3218 // Cross-axis pin — the primitive must thread the caller-
3219 // supplied `outside_ctor` verbatim into the sandbox-escape
3220 // arm's `LayoutError` return, so the fold does not silently
3221 // collapse onto one hard-coded variant. Sweep both
3222 // [`LayoutError`] tuple-variants the two wire-up sites in
3223 // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3224 // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3225 // ctor-routing on either axis would trip.
3226 let root = PathBuf::from("/tmp/x");
3227 let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3228 for (kind, sandbox_component, ctor, expected_variant) in [
3229 (
3230 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3231 crate::render::LAYOUT_DIR_EXE,
3232 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3233 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3234 ),
3235 (
3236 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3237 crate::render::LAYOUT_DIR_SERVICOS,
3238 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3239 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3240 ),
3241 ] {
3242 let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3243 let full = root.join(&path);
3244 let full_probe = full.clone();
3245 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3246 let sandbox_dir = root.join(sandbox_component);
3247 let err = layout
3248 .probe_sandboxed_declared_entry(&path, &root, kind, &sandbox_dir, ctor)
3249 .unwrap_err();
3250 assert_eq!(
3251 err,
3252 expected_variant(full),
3253 "probe_sandboxed_declared_entry must thread `outside_ctor` verbatim \
3254 on every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3255 );
3256 }
3257 }
3258
3259 #[test]
3260 fn probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry() {
3261 // Cross-primitive pin — the sandboxed probe must route its
3262 // miss arm through the sibling [`StandardLayout::
3263 // probe_declared_entry`] primitive rather than re-inlining the
3264 // `root.join` + `exists` + `missing_entry` cascade, so a
3265 // future edit to the miss-arm shape on either primitive lands
3266 // in exactly one place. Byte-parity assertion: on a fixture
3267 // that misses the oracle, the sandboxed primitive's `Err`
3268 // arm must equal the peer [`StandardLayout::
3269 // probe_declared_entry`] primitive's `Err` arm on the same
3270 // fixture — otherwise the fold has drifted from the substrate
3271 // primitive.
3272 let layout = StandardLayout::new().with_path_exists(|_| false);
3273 let root = PathBuf::from("/tmp/x");
3274 let path = Path::new("servicos/hello-rio.computeunit.yaml");
3275 let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3276 let via_sandboxed = layout
3277 .probe_sandboxed_declared_entry(
3278 path,
3279 &root,
3280 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3281 &sandbox_dir,
3282 LayoutError::ServicoOutsideDir,
3283 )
3284 .unwrap_err();
3285 let via_probe = layout
3286 .probe_declared_entry(
3287 path,
3288 &root,
3289 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3290 )
3291 .unwrap_err();
3292 assert_eq!(
3293 via_sandboxed, via_probe,
3294 "probe_sandboxed_declared_entry's miss arm must equal the sibling \
3295 probe_declared_entry primitive's miss arm byte-equal — pins that the \
3296 sandboxed primitive routes through the substrate primitive rather than \
3297 re-inlining the `root.join` + `exists` + `missing_entry` cascade",
3298 );
3299 }
3300
3301 // ── StandardLayout::probe_declared_entries substrate primitive ───────
3302 //
3303 // The [`StandardLayout::probe_declared_entries`] method (`layout.rs`)
3304 // folds the three self-similar per-slot existence-probe *loop* blocks
3305 // at [`StandardLayout::verify`] (`:bibliotecas` iteration,
3306 // `:behavior` on-disk callback-path iteration, `:upgrade-from` per-
3307 // instruction script-path iteration) onto one substrate primitive.
3308 // The pins below (fail-before-pass-after by construction — a byte-
3309 // mismatched primitive body would trip its equivalence pin first)
3310 // lock the fold to its pre-lift open-coded shape under `PartialEq`,
3311 // so every wire-up in [`StandardLayout::verify`] on this primitive
3312 // produces a byte-equal `LayoutError` on miss (via the sibling
3313 // [`StandardLayout::probe_declared_entry`] miss arm) and a byte-
3314 // equal `Ok(())` on the empty / all-hit arms (the fold's identity
3315 // element on an empty slot list; the pre-lift `for … { … }` loop's
3316 // vacuous pass-through).
3317 //
3318 // Sibling of the peer per-arm-probe [`StandardLayout::probe_declared_entry`]
3319 // (fda1e35) and two-arm-sandboxed [`StandardLayout::
3320 // probe_sandboxed_declared_entry`] (4940d55) primitive test blocks
3321 // — same substrate-primitive discipline extended onto the per-slot
3322 // batch axis these two per-path primitives compose under.
3323
3324 #[test]
3325 fn probe_declared_entries_folds_empty_iterator_returns_ok() {
3326 // Per-primitive identity-element pin on the empty-iterator arm —
3327 // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3328 // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3329 // pass-through. Peer of the peer-primitive `Option::None →
3330 // Ok(())` identity elements the sibling per-Caixa compound
3331 // gates ([`crate::Caixa::validate_limits`] baa4688,
3332 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on
3333 // the sibling per-slot compound-gate axis.
3334 let layout = StandardLayout::new().with_path_exists(|_| false);
3335 let root = PathBuf::from("/tmp/x");
3336 let empty: [&Path; 0] = [];
3337 layout
3338 .probe_declared_entries(
3339 empty,
3340 &root,
3341 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3342 )
3343 .expect(
3344 "probe_declared_entries must return Ok(()) on an empty iterator \
3345 — the fold's identity element on an empty per-slot batch",
3346 );
3347 }
3348
3349 #[test]
3350 fn probe_declared_entries_folds_all_hits_returns_ok() {
3351 // Per-primitive equivalence pin on the all-hit arm — a
3352 // [`StandardLayout`] whose oracle admits every path in the batch
3353 // yields `Ok(())` byte-equal to the pre-lift `for p in … {
3354 // self.probe_declared_entry(p, root, kind)?; }` loop's full-
3355 // consumption pass-through.
3356 let root = PathBuf::from("/tmp/x");
3357 let a = root.join("lib/a.lisp");
3358 let b = root.join("lib/b.lisp");
3359 let a_probe = a.clone();
3360 let b_probe = b.clone();
3361 let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3362 layout
3363 .probe_declared_entries(
3364 [Path::new("lib/a.lisp"), Path::new("lib/b.lisp")],
3365 &root,
3366 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3367 )
3368 .expect(
3369 "probe_declared_entries must return Ok(()) when every path in the \
3370 batch is admitted by the oracle",
3371 );
3372 }
3373
3374 #[test]
3375 fn probe_declared_entries_folds_first_miss_short_circuits_via_probe_declared_entry() {
3376 // Per-primitive equivalence pin on the first-miss arm — a
3377 // [`StandardLayout`] whose oracle admits the first path and
3378 // rejects the second yields a `MissingEntry` byte-equal under
3379 // `PartialEq` to the sibling [`StandardLayout::
3380 // probe_declared_entry`] primitive's miss wrap on the *second*
3381 // path (the pre-lift `for … { … ? }` loop's first-error return
3382 // semantics), *not* on the first (admitted) path.
3383 let root = PathBuf::from("/tmp/x");
3384 let hit = root.join("lib/a.lisp");
3385 let hit_probe = hit.clone();
3386 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3387 let miss = Path::new("lib/b.lisp");
3388 let err = layout
3389 .probe_declared_entries(
3390 [Path::new("lib/a.lisp"), miss],
3391 &root,
3392 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3393 )
3394 .unwrap_err();
3395 assert_eq!(
3396 err,
3397 LayoutError::missing_entry(
3398 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3399 root.join(miss),
3400 ),
3401 "probe_declared_entries must short-circuit on the first missing entry \
3402 with a `MissingEntry` byte-equal to the sibling probe_declared_entry \
3403 primitive's miss wrap on that entry — the pre-lift `for … {{ … ? }}` \
3404 loop's first-error return semantics",
3405 );
3406 }
3407
3408 #[test]
3409 fn probe_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3410 // Diagnostic-order pin — the primitive must return on the *first*
3411 // miss in iterator order rather than probing every path and
3412 // returning the last miss (which would silently drop the pre-
3413 // lift `for … { … ? }` loop's first-error contract). Fixture:
3414 // the oracle rejects both paths, so a byte-equal `MissingEntry`
3415 // on the *first* path in the iterator distinguishes the two
3416 // return-order shapes.
3417 let layout = StandardLayout::new().with_path_exists(|_| false);
3418 let root = PathBuf::from("/tmp/x");
3419 let first = Path::new("lib/first.lisp");
3420 let second = Path::new("lib/second.lisp");
3421 let err = layout
3422 .probe_declared_entries(
3423 [first, second],
3424 &root,
3425 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3426 )
3427 .unwrap_err();
3428 assert_eq!(
3429 err,
3430 LayoutError::missing_entry(
3431 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3432 root.join(first),
3433 ),
3434 "probe_declared_entries must return on the *first* miss in iterator order \
3435 — a `MissingEntry` on the second path would silently drop the pre-lift \
3436 `for … {{ … ? }}` loop's first-error contract",
3437 );
3438 }
3439
3440 #[test]
3441 fn probe_declared_entries_threads_kind_through_arg_verbatim() {
3442 // Cross-axis pin — sweep every canonical
3443 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the three
3444 // batch wire-up sites in [`StandardLayout::verify`] pass through
3445 // this primitive (`:bibliotecas`, `:behavior` callback,
3446 // `:upgrade-from` script). Each miss must return a `MissingEntry`
3447 // whose `kind:` field byte-equals the caller-provided arg, so
3448 // the fold does not silently collapse onto one hard-coded label.
3449 // Sibling of the peer `probe_declared_entry_threads_kind_through_arg_verbatim`
3450 // pin's five-label sweep on the per-arm primitive.
3451 let layout = StandardLayout::new().with_path_exists(|_| false);
3452 let root = PathBuf::from("/tmp/x");
3453 let path = Path::new("some/entry");
3454 for kind in [
3455 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3456 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
3457 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
3458 ] {
3459 let err = layout
3460 .probe_declared_entries([path], &root, kind)
3461 .unwrap_err();
3462 assert_eq!(
3463 err,
3464 LayoutError::missing_entry(kind, root.join(path)),
3465 "probe_declared_entries must thread `kind` verbatim on every canonical \
3466 batch label",
3467 );
3468 }
3469 }
3470
3471 #[test]
3472 fn probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3473 // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3474 // admit every concrete iterator element type the three
3475 // [`StandardLayout::verify`] wire-up sites pass:
3476 // - `&String` (from `caixa.bibliotecas(): &[String]`),
3477 // - `&Path` (from `b.declared_paths(): impl Iterator<Item = &Path>`),
3478 // - `&PathBuf` (from the flattened `.filter_map(_::declared_path)`
3479 // on `:upgrade-from` instructions, whose `declared_path`
3480 // returns `Option<&PathBuf>`).
3481 //
3482 // Byte-parity assertion: on a shared `/tmp/x/lib/demo.lisp`
3483 // fixture the miss-arm return must be byte-equal across all
3484 // three element-type flavors, so a future re-shape of the
3485 // bound (a narrower `P: Into<PathBuf>` collapse, a stricter
3486 // `&Path`-only signature) would surface here rather than at
3487 // the caller wire-up site.
3488 let layout = StandardLayout::new().with_path_exists(|_| false);
3489 let root = PathBuf::from("/tmp/x");
3490 let literal = "lib/demo.lisp";
3491 let expected = LayoutError::missing_entry(
3492 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3493 root.join(Path::new(literal)),
3494 );
3495
3496 let via_string_slice: Vec<String> = vec![literal.to_string()];
3497 let via_string_err = layout
3498 .probe_declared_entries(
3499 via_string_slice.iter(),
3500 &root,
3501 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3502 )
3503 .unwrap_err();
3504 assert_eq!(
3505 via_string_err, expected,
3506 "probe_declared_entries must accept `&String` items (the `caixa.bibliotecas() \
3507 : &[String]` wire-up shape)",
3508 );
3509
3510 let via_path_slice: [&Path; 1] = [Path::new(literal)];
3511 let via_path_err = layout
3512 .probe_declared_entries(
3513 via_path_slice,
3514 &root,
3515 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3516 )
3517 .unwrap_err();
3518 assert_eq!(
3519 via_path_err, expected,
3520 "probe_declared_entries must accept `&Path` items (the `b.declared_paths() \
3521 : impl Iterator<Item = &Path>` wire-up shape)",
3522 );
3523
3524 let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3525 let via_pathbuf_err = layout
3526 .probe_declared_entries(
3527 via_pathbuf_slice.iter(),
3528 &root,
3529 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3530 )
3531 .unwrap_err();
3532 assert_eq!(
3533 via_pathbuf_err, expected,
3534 "probe_declared_entries must accept `&PathBuf` items (the `.filter_map( \
3535 _::declared_path)` on `:upgrade-from` instructions wire-up shape)",
3536 );
3537 }
3538
3539 #[test]
3540 fn probe_declared_entries_routes_miss_arm_through_probe_declared_entry() {
3541 // Cross-primitive pin — the batch primitive must route its miss
3542 // arm through the sibling [`StandardLayout::probe_declared_entry`]
3543 // primitive rather than re-inlining the `root.join` + `exists`
3544 // + `missing_entry` cascade, so a future edit to the miss-arm
3545 // shape on either primitive lands in exactly one place. Byte-
3546 // parity assertion: on a fixture that misses the oracle, the
3547 // batch primitive's `Err` arm must equal the peer per-arm
3548 // [`StandardLayout::probe_declared_entry`] primitive's `Err` arm
3549 // on the same fixture — otherwise the batch fold has drifted
3550 // from the substrate primitive. Same discipline the peer
3551 // `probe_sandboxed_declared_entry_routes_miss_arm_through_probe_declared_entry`
3552 // pin establishes on the sibling sandboxed-primitive axis.
3553 let layout = StandardLayout::new().with_path_exists(|_| false);
3554 let root = PathBuf::from("/tmp/x");
3555 let path = Path::new("lib/demo.lisp");
3556 let via_batch = layout
3557 .probe_declared_entries(
3558 [path],
3559 &root,
3560 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3561 )
3562 .unwrap_err();
3563 let via_probe = layout
3564 .probe_declared_entry(
3565 path,
3566 &root,
3567 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
3568 )
3569 .unwrap_err();
3570 assert_eq!(
3571 via_batch, via_probe,
3572 "probe_declared_entries's miss arm must equal the sibling probe_declared_entry \
3573 primitive's miss arm byte-equal — pins that the batch primitive routes \
3574 through the substrate primitive rather than re-inlining the `root.join` + \
3575 `exists` + `missing_entry` cascade",
3576 );
3577 }
3578
3579 // ── StandardLayout::probe_sandboxed_declared_entries substrate primitive ─
3580 //
3581 // The [`StandardLayout::probe_sandboxed_declared_entries`] method
3582 // (`layout.rs`) folds the two self-similar per-slot sandboxed-
3583 // existence-probe *loop* blocks at [`StandardLayout::verify`]
3584 // (`:exe` iteration, `:servicos` iteration) onto one substrate
3585 // primitive. The pins below (fail-before-pass-after by construction
3586 // — a byte-mismatched primitive body would trip its equivalence pin
3587 // first) lock the fold to its pre-lift open-coded shape under
3588 // `PartialEq`, so every wire-up in [`StandardLayout::verify`] on
3589 // this primitive produces a byte-equal `LayoutError` on miss / on
3590 // sandbox-escape (via the sibling
3591 // [`StandardLayout::probe_sandboxed_declared_entry`] arms) and a
3592 // byte-equal `Ok(())` on the empty / all-hit arms (the fold's
3593 // identity element on an empty slot list; the pre-lift `for … {
3594 // … }` loop's vacuous pass-through).
3595 //
3596 // Sibling of the peer per-slot batch bare-probe
3597 // [`StandardLayout::probe_declared_entries`] (d1ccb0b), per-arm
3598 // bare-probe [`StandardLayout::probe_declared_entry`] (fda1e35), and
3599 // per-arm sandboxed-probe
3600 // [`StandardLayout::probe_sandboxed_declared_entry`] (4940d55)
3601 // primitive test blocks — same substrate-primitive discipline
3602 // extended onto the fourth and last quadrant of the
3603 // (per-arm | per-slot batch) × (bare | sandboxed) existence-probe
3604 // algebra.
3605
3606 #[test]
3607 fn probe_sandboxed_declared_entries_folds_empty_iterator_returns_ok() {
3608 // Per-primitive identity-element pin on the empty-iterator arm —
3609 // the fold's `Ok(())` return on an empty `IntoIterator` is byte-
3610 // equal to the pre-lift `for _ in <empty> { … }` loop's vacuous
3611 // pass-through. Peer of the sibling
3612 // `probe_declared_entries_folds_empty_iterator_returns_ok` pin
3613 // on the bare-batch axis and the `Option::None → Ok(())`
3614 // identity elements the per-Caixa compound gates
3615 // ([`crate::Caixa::validate_limits`] baa4688,
3616 // [`crate::Caixa::validate_behavior`] 0d2877a) each carry on the
3617 // per-slot compound-gate axis.
3618 let layout = StandardLayout::new().with_path_exists(|_| false);
3619 let root = PathBuf::from("/tmp/x");
3620 let empty: [&Path; 0] = [];
3621 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3622 layout
3623 .probe_sandboxed_declared_entries(
3624 empty,
3625 &root,
3626 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3627 &exe_dir,
3628 LayoutError::ExeOutsideDir,
3629 )
3630 .expect(
3631 "probe_sandboxed_declared_entries must return Ok(()) on an empty iterator \
3632 — the fold's identity element on an empty per-slot sandboxed batch",
3633 );
3634 }
3635
3636 #[test]
3637 fn probe_sandboxed_declared_entries_folds_all_hits_returns_ok() {
3638 // Per-primitive equivalence pin on the all-hit arm — a
3639 // [`StandardLayout`] whose oracle admits every path in the batch
3640 // *and* whose resolved paths live inside `sandbox_dir` yields
3641 // `Ok(())` byte-equal to the pre-lift `for p in … {
3642 // self.probe_sandboxed_declared_entry(p, root, kind,
3643 // &sandbox_dir, outside_ctor)?; }` loop's full-consumption
3644 // pass-through.
3645 let root = PathBuf::from("/tmp/x");
3646 let a = root.join("exe/a");
3647 let b = root.join("exe/b");
3648 let a_probe = a.clone();
3649 let b_probe = b.clone();
3650 let layout = StandardLayout::new().with_path_exists(move |p| p == a_probe || p == b_probe);
3651 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3652 layout
3653 .probe_sandboxed_declared_entries(
3654 [Path::new("exe/a"), Path::new("exe/b")],
3655 &root,
3656 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3657 &exe_dir,
3658 LayoutError::ExeOutsideDir,
3659 )
3660 .expect(
3661 "probe_sandboxed_declared_entries must return Ok(()) when every path in \
3662 the batch is admitted by the oracle and lives inside sandbox_dir",
3663 );
3664 }
3665
3666 #[test]
3667 fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_via_probe_sandboxed_declared_entry()
3668 {
3669 // Per-primitive equivalence pin on the first-miss arm — a
3670 // [`StandardLayout`] whose oracle admits the first path (inside
3671 // sandbox_dir) and rejects the second yields a `MissingEntry`
3672 // byte-equal under `PartialEq` to the sibling
3673 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3674 // miss wrap on the *second* path (the pre-lift `for … { … ? }`
3675 // loop's first-error return semantics), *not* on the first
3676 // (admitted) path.
3677 let root = PathBuf::from("/tmp/x");
3678 let hit = root.join("exe/a");
3679 let hit_probe = hit.clone();
3680 let layout = StandardLayout::new().with_path_exists(move |p| p == hit_probe);
3681 let miss = Path::new("exe/b");
3682 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3683 let err = layout
3684 .probe_sandboxed_declared_entries(
3685 [Path::new("exe/a"), miss],
3686 &root,
3687 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3688 &exe_dir,
3689 LayoutError::ExeOutsideDir,
3690 )
3691 .unwrap_err();
3692 assert_eq!(
3693 err,
3694 LayoutError::missing_entry(
3695 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3696 root.join(miss),
3697 ),
3698 "probe_sandboxed_declared_entries must short-circuit on the first missing \
3699 entry with a `MissingEntry` byte-equal to the sibling \
3700 probe_sandboxed_declared_entry primitive's miss wrap on that entry — the \
3701 pre-lift `for … {{ … ? }}` loop's first-error return semantics",
3702 );
3703 }
3704
3705 #[test]
3706 fn probe_sandboxed_declared_entries_folds_first_miss_short_circuits_before_later_paths() {
3707 // Diagnostic-order pin — the primitive must return on the *first*
3708 // miss in iterator order rather than probing every path and
3709 // returning the last miss (which would silently drop the pre-
3710 // lift `for … { … ? }` loop's first-error contract). Fixture:
3711 // the oracle rejects both paths, so a byte-equal `MissingEntry`
3712 // on the *first* path in the iterator distinguishes the two
3713 // return-order shapes. Sibling of the peer
3714 // `probe_declared_entries_folds_first_miss_short_circuits_before_later_paths`
3715 // pin on the bare-batch axis.
3716 let layout = StandardLayout::new().with_path_exists(|_| false);
3717 let root = PathBuf::from("/tmp/x");
3718 let first = Path::new("exe/first");
3719 let second = Path::new("exe/second");
3720 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3721 let err = layout
3722 .probe_sandboxed_declared_entries(
3723 [first, second],
3724 &root,
3725 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3726 &exe_dir,
3727 LayoutError::ExeOutsideDir,
3728 )
3729 .unwrap_err();
3730 assert_eq!(
3731 err,
3732 LayoutError::missing_entry(
3733 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3734 root.join(first),
3735 ),
3736 "probe_sandboxed_declared_entries must return on the *first* miss in iterator \
3737 order — a `MissingEntry` on the second path would silently drop the pre-lift \
3738 `for … {{ … ? }}` loop's first-error contract",
3739 );
3740 }
3741
3742 #[test]
3743 fn probe_sandboxed_declared_entries_folds_sandbox_escape_via_outside_ctor() {
3744 // Per-primitive equivalence pin on the sandbox-escape arm — a
3745 // [`StandardLayout`] whose oracle admits the probed resolved
3746 // path (so the miss arm passes) but whose resolved path lies
3747 // outside the caller-provided `sandbox_dir` yields the paired
3748 // `outside_ctor(full)` byte-equal under `PartialEq`. The
3749 // primitive threads the resolved `full` through the caller-
3750 // supplied `fn(PathBuf) -> LayoutError` constructor rather than
3751 // a hard-coded variant, so the fold does not silently collapse
3752 // onto one of the two `:exe` / `:servicos` outside-dir variants.
3753 let root = PathBuf::from("/tmp/x");
3754 let path = Path::new("lib/tool");
3755 let full = root.join(path);
3756 let full_probe = full.clone();
3757 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3758 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3759 let err = layout
3760 .probe_sandboxed_declared_entries(
3761 [path],
3762 &root,
3763 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3764 &exe_dir,
3765 LayoutError::ExeOutsideDir,
3766 )
3767 .unwrap_err();
3768 assert_eq!(
3769 err,
3770 LayoutError::ExeOutsideDir(full),
3771 "probe_sandboxed_declared_entries sandbox-escape arm must route the resolved \
3772 `full` through the caller-supplied outside_ctor byte-equal to the pre-lift \
3773 `LayoutError::ExeOutsideDir(full)` tuple-literal",
3774 );
3775 }
3776
3777 #[test]
3778 fn probe_sandboxed_declared_entries_threads_outside_ctor_through_arg_verbatim() {
3779 // Cross-axis pin — the primitive must thread the caller-supplied
3780 // `outside_ctor` verbatim into the sandbox-escape arm's
3781 // `LayoutError` return, so the fold does not silently collapse
3782 // onto one hard-coded variant. Sweep both [`LayoutError`]
3783 // tuple-variants the two wire-up sites in
3784 // [`StandardLayout::verify`] pass — [`LayoutError::ExeOutsideDir`]
3785 // and [`LayoutError::ServicoOutsideDir`] — so a byte-drifted
3786 // ctor-routing on either axis would trip. Sibling of the peer
3787 // `probe_sandboxed_declared_entry_threads_outside_ctor_through_arg_verbatim`
3788 // pin on the per-arm sandboxed-primitive axis.
3789 let root = PathBuf::from("/tmp/x");
3790 let outside_dir = root.join(crate::render::LAYOUT_DIR_LIB);
3791 for (kind, sandbox_component, ctor) in [
3792 (
3793 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3794 crate::render::LAYOUT_DIR_EXE,
3795 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3796 ),
3797 (
3798 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3799 crate::render::LAYOUT_DIR_SERVICOS,
3800 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3801 ),
3802 ] {
3803 let path = outside_dir.strip_prefix(&root).unwrap().join("tool");
3804 let full = root.join(&path);
3805 let full_probe = full.clone();
3806 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3807 let sandbox_dir = root.join(sandbox_component);
3808 let err = layout
3809 .probe_sandboxed_declared_entries([&path], &root, kind, &sandbox_dir, ctor)
3810 .unwrap_err();
3811 assert_eq!(
3812 err,
3813 ctor(full),
3814 "probe_sandboxed_declared_entries must thread `outside_ctor` verbatim on \
3815 every canonical `<Slot>OutsideDir` variant the two wire-up sites pass",
3816 );
3817 }
3818 }
3819
3820 #[test]
3821 fn probe_sandboxed_declared_entries_threads_kind_through_arg_verbatim() {
3822 // Cross-axis pin — sweep every canonical
3823 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_*`] label the two
3824 // sandboxed-batch wire-up sites in [`StandardLayout::verify`]
3825 // pass through this primitive (`:exe`, `:servicos`). Each miss
3826 // must return a `MissingEntry` whose `kind:` field byte-equals
3827 // the caller-provided arg, so the fold does not silently
3828 // collapse onto one hard-coded label. Sibling of the peer
3829 // `probe_declared_entries_threads_kind_through_arg_verbatim`
3830 // pin's label sweep on the bare-batch axis.
3831 let layout = StandardLayout::new().with_path_exists(|_| false);
3832 let root = PathBuf::from("/tmp/x");
3833 let path = Path::new("some/entry");
3834 for (kind, sandbox_component, ctor) in [
3835 (
3836 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3837 crate::render::LAYOUT_DIR_EXE,
3838 LayoutError::ExeOutsideDir as fn(PathBuf) -> LayoutError,
3839 ),
3840 (
3841 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3842 crate::render::LAYOUT_DIR_SERVICOS,
3843 LayoutError::ServicoOutsideDir as fn(PathBuf) -> LayoutError,
3844 ),
3845 ] {
3846 let sandbox_dir = root.join(sandbox_component);
3847 let err = layout
3848 .probe_sandboxed_declared_entries([path], &root, kind, &sandbox_dir, ctor)
3849 .unwrap_err();
3850 assert_eq!(
3851 err,
3852 LayoutError::missing_entry(kind, root.join(path)),
3853 "probe_sandboxed_declared_entries must thread `kind` verbatim on every \
3854 canonical sandboxed-batch label",
3855 );
3856 }
3857 }
3858
3859 #[test]
3860 fn probe_sandboxed_declared_entries_accepts_asref_path_shape_wire_up_sites_pass() {
3861 // Cross-axis pin — the primitive's `P: AsRef<Path>` bound must
3862 // admit every concrete iterator element type the two
3863 // [`StandardLayout::verify`] wire-up sites pass:
3864 // - `&String` (from `caixa.exe(): &[String]`),
3865 // - `&String` (from `caixa.servicos(): &[String]`).
3866 //
3867 // Byte-parity assertion: on a shared `/tmp/x/lib/demo`
3868 // fixture the miss-arm return must be byte-equal across
3869 // `&String` and the ergonomic `&Path` / `&PathBuf` element-type
3870 // flavors, so a future re-shape of the bound (a narrower `P:
3871 // Into<PathBuf>` collapse, a stricter `&Path`-only signature)
3872 // would surface here rather than at the caller wire-up site.
3873 // Sibling of the peer
3874 // `probe_declared_entries_accepts_asref_path_shape_wire_up_sites_pass`
3875 // pin on the bare-batch axis.
3876 let layout = StandardLayout::new().with_path_exists(|_| false);
3877 let root = PathBuf::from("/tmp/x");
3878 let literal = "exe/demo";
3879 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3880 let expected = LayoutError::missing_entry(
3881 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3882 root.join(Path::new(literal)),
3883 );
3884
3885 let via_string_slice: Vec<String> = vec![literal.to_string()];
3886 let via_string_err = layout
3887 .probe_sandboxed_declared_entries(
3888 via_string_slice.iter(),
3889 &root,
3890 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3891 &exe_dir,
3892 LayoutError::ExeOutsideDir,
3893 )
3894 .unwrap_err();
3895 assert_eq!(
3896 via_string_err, expected,
3897 "probe_sandboxed_declared_entries must accept `&String` items (the `caixa.exe() \
3898 : &[String]` / `caixa.servicos(): &[String]` wire-up shape)",
3899 );
3900
3901 let via_path_slice: [&Path; 1] = [Path::new(literal)];
3902 let via_path_err = layout
3903 .probe_sandboxed_declared_entries(
3904 via_path_slice,
3905 &root,
3906 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3907 &exe_dir,
3908 LayoutError::ExeOutsideDir,
3909 )
3910 .unwrap_err();
3911 assert_eq!(
3912 via_path_err, expected,
3913 "probe_sandboxed_declared_entries must accept `&Path` items",
3914 );
3915
3916 let via_pathbuf_slice: Vec<PathBuf> = vec![PathBuf::from(literal)];
3917 let via_pathbuf_err = layout
3918 .probe_sandboxed_declared_entries(
3919 via_pathbuf_slice.iter(),
3920 &root,
3921 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
3922 &exe_dir,
3923 LayoutError::ExeOutsideDir,
3924 )
3925 .unwrap_err();
3926 assert_eq!(
3927 via_pathbuf_err, expected,
3928 "probe_sandboxed_declared_entries must accept `&PathBuf` items",
3929 );
3930 }
3931
3932 #[test]
3933 fn probe_sandboxed_declared_entries_routes_miss_arm_through_probe_sandboxed_declared_entry() {
3934 // Cross-primitive pin — the batch primitive must route its miss
3935 // arm through the sibling
3936 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive
3937 // rather than re-inlining the `probe_declared_entry` +
3938 // `starts_with` cascade, so a future edit to the miss-arm shape
3939 // on either primitive lands in exactly one place. Byte-parity
3940 // assertion: on a fixture that misses the oracle, the batch
3941 // primitive's `Err` arm must equal the peer per-arm
3942 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3943 // `Err` arm on the same fixture — otherwise the batch fold has
3944 // drifted from the substrate primitive. Same discipline the peer
3945 // `probe_declared_entries_routes_miss_arm_through_probe_declared_entry`
3946 // pin establishes on the sibling bare-batch axis.
3947 let layout = StandardLayout::new().with_path_exists(|_| false);
3948 let root = PathBuf::from("/tmp/x");
3949 let path = Path::new("servicos/hello-rio.computeunit.yaml");
3950 let sandbox_dir = root.join(crate::render::LAYOUT_DIR_SERVICOS);
3951 let via_batch = layout
3952 .probe_sandboxed_declared_entries(
3953 [path],
3954 &root,
3955 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3956 &sandbox_dir,
3957 LayoutError::ServicoOutsideDir,
3958 )
3959 .unwrap_err();
3960 let via_probe = layout
3961 .probe_sandboxed_declared_entry(
3962 path,
3963 &root,
3964 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
3965 &sandbox_dir,
3966 LayoutError::ServicoOutsideDir,
3967 )
3968 .unwrap_err();
3969 assert_eq!(
3970 via_batch, via_probe,
3971 "probe_sandboxed_declared_entries's miss arm must equal the sibling \
3972 probe_sandboxed_declared_entry primitive's miss arm byte-equal — pins that \
3973 the batch primitive routes through the substrate primitive rather than \
3974 re-inlining the `probe_declared_entry` + `starts_with` cascade",
3975 );
3976 }
3977
3978 #[test]
3979 fn probe_sandboxed_declared_entries_routes_sandbox_escape_arm_through_probe_sandboxed_declared_entry()
3980 {
3981 // Cross-primitive pin — the batch primitive's sandbox-escape arm
3982 // must equal the sibling
3983 // [`StandardLayout::probe_sandboxed_declared_entry`] primitive's
3984 // sandbox-escape arm on the same fixture. Byte-parity assertion:
3985 // on a fixture where the oracle admits the probed path but the
3986 // resolved path lies outside sandbox_dir, both primitives must
3987 // return the same `outside_ctor(full)` byte-equal under
3988 // `PartialEq` — otherwise the batch fold has drifted from the
3989 // per-arm primitive on the second-arm dispatch.
3990 let root = PathBuf::from("/tmp/x");
3991 let path = Path::new("lib/escape");
3992 let full = root.join(path);
3993 let full_probe = full.clone();
3994 let layout = StandardLayout::new().with_path_exists(move |p| p == full_probe);
3995 let exe_dir = root.join(crate::render::LAYOUT_DIR_EXE);
3996 let via_batch = layout
3997 .probe_sandboxed_declared_entries(
3998 [path],
3999 &root,
4000 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4001 &exe_dir,
4002 LayoutError::ExeOutsideDir,
4003 )
4004 .unwrap_err();
4005 let via_probe = layout
4006 .probe_sandboxed_declared_entry(
4007 path,
4008 &root,
4009 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
4010 &exe_dir,
4011 LayoutError::ExeOutsideDir,
4012 )
4013 .unwrap_err();
4014 assert_eq!(
4015 via_batch, via_probe,
4016 "probe_sandboxed_declared_entries's sandbox-escape arm must equal the sibling \
4017 probe_sandboxed_declared_entry primitive's sandbox-escape arm byte-equal — \
4018 pins that the batch primitive routes through the substrate primitive on both \
4019 the miss and sandbox-escape arms rather than re-inlining either cascade",
4020 );
4021 }
4022
4023 // ── LayoutError::<nome-only> constructor family ──────────────────────
4024 //
4025 // The [`layout_nome_only_ctors!`] macro (below the `LayoutError` enum
4026 // definition) generates one static constructor per `<Variant>(String)`
4027 // tuple-variant that folds the uniform `Self::<Variant>(caixa.nome()
4028 // .to_string())` one-field construction onto one substrate primitive.
4029 // The per-variant equivalence pins below (fail-before-pass-after by
4030 // construction — a byte-mismatched macro arm would trip its
4031 // equivalence pin first) lock each generated constructor to its
4032 // tuple-literal peer under `PartialEq`, so every wire-up in
4033 // [`StandardLayout::verify`] on that variant produces a byte-equal
4034 // `LayoutError` to the pre-lift open-coded tuple-literal. The
4035 // cross-axis pin that follows (non-default `:nome`) routes the sole
4036 // constructor input axis through its declared accessor, so the fold
4037 // does not silently collapse onto the fixture default `:nome`.
4038
4039 fn layout_nome_only_ctor_fixture() -> Caixa {
4040 caixa(CaixaKind::Biblioteca)
4041 }
4042
4043 // Same rationale as `assert_violation_ctor_matches` / `assert_slot_
4044 // kind_ctor_matches` above: the helper terminates on the equality
4045 // check, so the owned-arg lint's general API-shape target does not
4046 // apply.
4047 #[allow(clippy::needless_pass_by_value)]
4048 fn assert_nome_only_ctor_matches(actual: LayoutError, expected: LayoutError) {
4049 assert_eq!(
4050 actual, expected,
4051 "generated constructor must produce byte-equal LayoutError to open-coded tuple-literal wrap",
4052 );
4053 }
4054
4055 #[test]
4056 fn binario_without_exe_ctor_matches_tuple_literal_wrap() {
4057 let c = layout_nome_only_ctor_fixture();
4058 assert_nome_only_ctor_matches(
4059 LayoutError::binario_without_exe(&c),
4060 LayoutError::BinarioWithoutExe(c.nome().to_string()),
4061 );
4062 }
4063
4064 #[test]
4065 fn servico_without_servicos_ctor_matches_tuple_literal_wrap() {
4066 let c = layout_nome_only_ctor_fixture();
4067 assert_nome_only_ctor_matches(
4068 LayoutError::servico_without_servicos(&c),
4069 LayoutError::ServicoWithoutServicos(c.nome().to_string()),
4070 );
4071 }
4072
4073 #[test]
4074 fn missing_ci_ctor_matches_tuple_literal_wrap() {
4075 let c = layout_nome_only_ctor_fixture();
4076 assert_nome_only_ctor_matches(
4077 LayoutError::missing_ci(&c),
4078 LayoutError::MissingCi(c.nome().to_string()),
4079 );
4080 }
4081
4082 #[test]
4083 fn supervisor_owns_code_ctor_matches_tuple_literal_wrap() {
4084 let c = layout_nome_only_ctor_fixture();
4085 assert_nome_only_ctor_matches(
4086 LayoutError::supervisor_owns_code(&c),
4087 LayoutError::SupervisorOwnsCode(c.nome().to_string()),
4088 );
4089 }
4090
4091 #[test]
4092 fn aplicacao_owns_code_ctor_matches_tuple_literal_wrap() {
4093 let c = layout_nome_only_ctor_fixture();
4094 assert_nome_only_ctor_matches(
4095 LayoutError::aplicacao_owns_code(&c),
4096 LayoutError::AplicacaoOwnsCode(c.nome().to_string()),
4097 );
4098 }
4099
4100 #[test]
4101 fn acao_owns_code_ctor_matches_tuple_literal_wrap() {
4102 let c = layout_nome_only_ctor_fixture();
4103 assert_nome_only_ctor_matches(
4104 LayoutError::acao_owns_code(&c),
4105 LayoutError::AcaoOwnsCode(c.nome().to_string()),
4106 );
4107 }
4108
4109 #[test]
4110 fn nome_only_ctor_routes_caixa_through_nome_accessor() {
4111 // Pin the fold's `caixa.nome().to_string()` sole-field construction
4112 // against a non-default `:nome` — the accessor threads the caller's
4113 // `:nome` verbatim into the tuple-variant, so the fold does not
4114 // silently collapse onto the default `"demo"` fixture nome. Peer of
4115 // the sibling `violation_ctor_routes_caixa_prefix_through_nome_
4116 // accessor` / `slot_kind_ctor_routes_caixa_prefix_through_nome_
4117 // accessor` pins on the `{ caixa, issue }` / `{ caixa, kind,
4118 // slots }` envelopes; extended here onto the sixth
4119 // `<Variant>(String)` envelope so every LayoutError-shape ctor
4120 // family guarantees the `:nome`-derived-caixa slot routes through
4121 // [`Caixa::nome`] rather than a hard-coded string. Sweeps the six
4122 // ctors in the [`layout_nome_only_ctors!`] macro so the pin covers
4123 // every generated arm.
4124 let mut c = caixa(CaixaKind::Biblioteca);
4125 c.nome = "alt-nome".into();
4126 assert_eq!(
4127 LayoutError::binario_without_exe(&c),
4128 LayoutError::BinarioWithoutExe("alt-nome".to_string()),
4129 );
4130 assert_eq!(
4131 LayoutError::servico_without_servicos(&c),
4132 LayoutError::ServicoWithoutServicos("alt-nome".to_string()),
4133 );
4134 assert_eq!(
4135 LayoutError::missing_ci(&c),
4136 LayoutError::MissingCi("alt-nome".to_string()),
4137 );
4138 assert_eq!(
4139 LayoutError::supervisor_owns_code(&c),
4140 LayoutError::SupervisorOwnsCode("alt-nome".to_string()),
4141 );
4142 assert_eq!(
4143 LayoutError::aplicacao_owns_code(&c),
4144 LayoutError::AplicacaoOwnsCode("alt-nome".to_string()),
4145 );
4146 assert_eq!(
4147 LayoutError::acao_owns_code(&c),
4148 LayoutError::AcaoOwnsCode("alt-nome".to_string()),
4149 );
4150 }
4151
4152 #[test]
4153 fn biblioteca_needs_default_lib_path() {
4154 let root = PathBuf::from("/tmp/x");
4155 let expect_manifest = root.join("caixa.lisp");
4156 let layout = StandardLayout::new().with_path_exists(move |p| p == expect_manifest);
4157 let err = layout
4158 .verify(&caixa(CaixaKind::Biblioteca), &root)
4159 .unwrap_err();
4160 assert!(matches!(err, LayoutError::MissingLib { .. }));
4161 }
4162
4163 #[test]
4164 fn biblioteca_passes_when_default_lib_exists() {
4165 let root = PathBuf::from("/tmp/x");
4166 let manifest = root.join("caixa.lisp");
4167 let default_lib = root.join("lib").join("demo.lisp");
4168 let layout =
4169 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4170 layout
4171 .verify(&caixa(CaixaKind::Biblioteca), &root)
4172 .expect("should pass");
4173 }
4174
4175 #[test]
4176 fn binario_without_exe_errors() {
4177 let root = PathBuf::from("/tmp/x");
4178 let manifest = root.join("caixa.lisp");
4179 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4180 let err = layout
4181 .verify(&caixa(CaixaKind::Binario), &root)
4182 .unwrap_err();
4183 assert!(matches!(err, LayoutError::BinarioWithoutExe(_)));
4184 }
4185
4186 #[test]
4187 fn exe_outside_dir_errors() {
4188 // A relative entry that lives under the caixa root but *not*
4189 // under `exe/` — the canonical case the `starts_with(exe_dir)`
4190 // fence catches. The prior parent-escape shape this test used
4191 // (`"../sibling/tool"`) is now caught at validate time by
4192 // [`Caixa::validate_code_paths`] with the narrower
4193 // [`crate::ManifestError::CodePathParentEscape`] diagnostic
4194 // (see the layout-level integration pin
4195 // `code_path_violation_on_parent_escape_fires_before_existence_check`),
4196 // so this fence pin uses a non-`..` non-absolute shape outside
4197 // `exe/` to preserve coverage of the ExeOutsideDir surface.
4198 let root = PathBuf::from("/tmp/x");
4199 let manifest = root.join("caixa.lisp");
4200 let outside = root.join("lib/tool");
4201 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == outside);
4202 let mut c = caixa(CaixaKind::Binario);
4203 c.exe = vec!["lib/tool".into()];
4204 let err = layout.verify(&c, &root).unwrap_err();
4205 assert!(matches!(err, LayoutError::ExeOutsideDir(_)));
4206 }
4207
4208 // ── code-path shape gate (lifted to layout-level verify) ─────────────
4209
4210 #[test]
4211 fn code_path_violation_on_empty_bibliotecas_entry() {
4212 let root = PathBuf::from("/tmp/x");
4213 let manifest = root.join("caixa.lisp");
4214 let default_lib = root.join("lib").join("demo.lisp");
4215 let layout =
4216 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4217 let mut c = caixa(CaixaKind::Biblioteca);
4218 c.bibliotecas = vec![String::new()];
4219 let err = layout.verify(&c, &root).unwrap_err();
4220 // The wire-up wraps `ManifestError` Display into the
4221 // CodePathViolation envelope (peer of LimitsViolation /
4222 // BehaviorViolation / UpgradeViolation), so the issue string
4223 // names the offending slot at the source.
4224 let LayoutError::CodePathViolation { caixa, issue } = err else {
4225 panic!("expected LayoutError::CodePathViolation, got {err:?}");
4226 };
4227 assert_eq!(caixa, "demo");
4228 assert!(
4229 issue.contains(":bibliotecas"),
4230 "issue must name the offending slot: {issue}",
4231 );
4232 }
4233
4234 #[test]
4235 fn code_path_violation_on_absolute_servicos_entry() {
4236 let root = PathBuf::from("/tmp/x");
4237 let manifest = root.join("caixa.lisp");
4238 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4239 let mut c = caixa(CaixaKind::Servico);
4240 c.servicos = vec!["/etc/servicos/escape.yaml".into()];
4241 let err = layout.verify(&c, &root).unwrap_err();
4242 let LayoutError::CodePathViolation { caixa, issue } = err else {
4243 panic!("expected LayoutError::CodePathViolation, got {err:?}");
4244 };
4245 assert_eq!(caixa, "demo");
4246 assert!(
4247 issue.contains(":servicos"),
4248 "issue must name the offending slot: {issue}",
4249 );
4250 assert!(
4251 issue.contains("/etc/servicos/escape.yaml"),
4252 "issue must quote the offending path: {issue}",
4253 );
4254 }
4255
4256 #[test]
4257 fn code_path_violation_on_parent_escape_fires_before_existence_check() {
4258 // The new gate runs BEFORE the existence loops, so a
4259 // parent-escaping `:exe` entry surfaces CodePathViolation
4260 // (naming `:exe` at the source) rather than the downstream
4261 // ExeOutsideDir / MissingEntry against the resolved sandbox-
4262 // escape path. Even if the resolved escape target exists
4263 // on disk (which we simulate here by claiming it does), the
4264 // shape diagnostic wins.
4265 let root = PathBuf::from("/tmp/x");
4266 let manifest = root.join("caixa.lisp");
4267 let resolved_escape = root.join("exe/../../escape.lisp");
4268 let layout =
4269 StandardLayout::new().with_path_exists(move |p| p == manifest || p == resolved_escape);
4270 let mut c = caixa(CaixaKind::Binario);
4271 c.exe = vec!["exe/../../escape.lisp".into()];
4272 let err = layout.verify(&c, &root).unwrap_err();
4273 let LayoutError::CodePathViolation { caixa, issue } = err else {
4274 panic!("expected LayoutError::CodePathViolation, got {err:?}");
4275 };
4276 assert_eq!(caixa, "demo");
4277 assert!(
4278 issue.contains(":exe"),
4279 "issue must name the offending slot: {issue}",
4280 );
4281 }
4282
4283 // ── etiquetas universal-axis gate wired into verify ─────────────────
4284 //
4285 // Pins the layout-pipeline wire-up of [`Caixa::validate_etiquetas`]:
4286 // the fourth universal-axis Caixa-level value-shape gate (peer of
4287 // `validate_nome` / `validate_versao` / `validate_deps` /
4288 // `validate_code_paths`), wired before the kind-coherence gates so
4289 // a structurally-invalid `:etiquetas` entry on any kind surfaces
4290 // the per-axis `EtiquetasViolation { caixa, issue }` envelope at
4291 // the source rather than silently rendering as `keywords: [""]`
4292 // in `Chart.yaml` (Servico kind, via caixa-helm's `BTreeSet`
4293 // collect) or silently dedup'ing at chart render (every kind).
4294 // Until this wire-up landed `:etiquetas` had no shape gate at any
4295 // layer — the registry-search-tag axis was the largest universal
4296 // authoring surface on the typed Caixa surface with no validate
4297 // discipline.
4298 //
4299 // Same per-axis `*Violation { caixa, issue }` envelope every peer
4300 // per-axis wrap exposes; the wire-up runs after `validate_deps`
4301 // (universal axis ordering: `:nome` → `:versao` → `:deps` →
4302 // `:etiquetas`) and before every kind-coherence gate
4303 // (`:etiquetas` is universal so its shape diagnostic is more
4304 // fundamental than the partition-on-kind diagnostics).
4305
4306 #[test]
4307 fn etiquetas_violation_on_empty_entry() {
4308 // Canonical paste-from-blank-doc footgun on every kind. The
4309 // wrap envelope wraps [`ManifestError::EtiquetaEmpty`]'s
4310 // Display through verbatim, so the issue string names the
4311 // offending `:etiquetas` axis at the source — the author can
4312 // grep their caixa.lisp for `:etiquetas` and fix the empty
4313 // entry in one edit. Mirrors the peer
4314 // `code_path_violation_on_empty_bibliotecas_entry` shape
4315 // (b868442) on the `:bibliotecas` axis.
4316 let root = PathBuf::from("/tmp/x");
4317 let manifest = root.join("caixa.lisp");
4318 let default_lib = root.join("lib").join("demo.lisp");
4319 let layout =
4320 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4321 let mut c = caixa(CaixaKind::Biblioteca);
4322 c.etiquetas = vec![String::new()];
4323 let err = layout.verify(&c, &root).unwrap_err();
4324 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4325 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4326 };
4327 assert_eq!(caixa, "demo");
4328 assert!(
4329 issue.contains(":etiquetas"),
4330 "issue must name the offending slot: {issue}",
4331 );
4332 }
4333
4334 #[test]
4335 fn etiquetas_violation_on_duplicate_entry() {
4336 // Canonical copy-paste-the-wrong-tag footgun. Without the wire-
4337 // up the duplicate was silently dedup'd by caixa-helm's
4338 // `BTreeSet` collect at chart render — a "second wins / one
4339 // silently disappears" shape. The wrap envelope names the
4340 // offending tag verbatim through the inner
4341 // [`ManifestError::EtiquetaDuplicate`]'s Display.
4342 let root = PathBuf::from("/tmp/x");
4343 let manifest = root.join("caixa.lisp");
4344 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4345 let mut c = caixa(CaixaKind::Servico);
4346 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4347 c.etiquetas = vec!["demo".into(), "demo".into()];
4348 // The servicos path doesn't exist in this fixture, but the
4349 // `:etiquetas` gate fires before the existence loop (universal
4350 // axis dominates kind-specific existence checks). Wire is
4351 // intact iff the wrap envelope surfaces first.
4352 let err = layout.verify(&c, &root).unwrap_err();
4353 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4354 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4355 };
4356 assert_eq!(caixa, "demo");
4357 assert!(
4358 issue.contains("demo"),
4359 "issue must quote the offending tag: {issue}",
4360 );
4361 }
4362
4363 #[test]
4364 fn etiquetas_violation_fires_before_kind_coherence_mesh_slot() {
4365 // Cross-axis precedence pin: a Biblioteca with malformed
4366 // `:etiquetas` *and* declared mesh slots (`:membros`) surfaces
4367 // the universal `:etiquetas` diagnostic first, not the
4368 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4369 // `:etiquetas` is universal (every kind owns the slot), so its
4370 // shape diagnostic is more fundamental than the partition-on-
4371 // kind diagnostic. Mirrors the peer
4372 // `deps_violation_fires_before_*` precedence pins (aa77d0f) on
4373 // the universal `:deps` axis vs the same kind-coherence gates.
4374 let root = PathBuf::from("/tmp/x");
4375 let manifest = root.join("caixa.lisp");
4376 let default_lib = root.join("lib").join("demo.lisp");
4377 let layout =
4378 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4379 let mut c = caixa(CaixaKind::Biblioteca);
4380 c.etiquetas = vec![String::new()];
4381 c.membros = vec![crate::aplicacao::Membro {
4382 caixa: "x".into(),
4383 versao: "^0.1".into(),
4384 }];
4385 let err = layout.verify(&c, &root).unwrap_err();
4386 assert!(
4387 matches!(err, LayoutError::EtiquetasViolation { .. }),
4388 "got {err:?}",
4389 );
4390 }
4391
4392 #[test]
4393 fn etiquetas_violation_fires_after_deps_violation() {
4394 // Cross-axis precedence pin (inside the universal-axis trio):
4395 // a caixa with both a malformed `:deps` entry *and* a malformed
4396 // `:etiquetas` entry surfaces `DepsViolation` first — `:deps`
4397 // is the third universal axis in declaration order
4398 // (`:nome` → `:versao` → `:deps` → `:etiquetas`) and runs first
4399 // in `verify`. Mirrors the peer
4400 // `nome_violation_fires_before_versao_violation` shape on the
4401 // identity-axis pair.
4402 let root = PathBuf::from("/tmp/x");
4403 let manifest = root.join("caixa.lisp");
4404 let default_lib = root.join("lib").join("demo.lisp");
4405 let layout =
4406 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4407 let mut c = caixa(CaixaKind::Biblioteca);
4408 c.deps = vec![crate::Dep::simple("Caixa-Teia", "^0.1")]; // uppercase :nome
4409 c.etiquetas = vec![String::new()];
4410 let err = layout.verify(&c, &root).unwrap_err();
4411 assert!(
4412 matches!(err, LayoutError::DepsViolation { .. }),
4413 "got {err:?}",
4414 );
4415 }
4416
4417 #[test]
4418 fn etiquetas_violation_accepts_canonical_template() {
4419 // Positive control sanity pin: the canonical `Caixa::template`
4420 // shape (`:etiquetas ()` — empty list) passes the gate
4421 // trivially. Mirrors the peer
4422 // `validate_code_paths_accepts_canonical_template` pin.
4423 let root = PathBuf::from("/tmp/x");
4424 let manifest = root.join("caixa.lisp");
4425 let default_lib = root.join("lib").join("demo.lisp");
4426 let layout =
4427 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4428 let c = caixa(CaixaKind::Biblioteca);
4429 layout.verify(&c, &root).expect("template must pass");
4430 }
4431
4432 #[test]
4433 fn etiquetas_violation_on_non_chart_keyword_shape() {
4434 // Canonical CSV-list-separator-confusion footgun: the author
4435 // confused the CSV-style separator with the `:etiquetas` list
4436 // grammar. The shape gate fires past the empty + duplicate
4437 // arms via [`Caixa::validate_etiquetas`]'s new
4438 // `is_chart_keyword_shape` cascade, and the layout envelope
4439 // wraps [`ManifestError::EtiquetaInvalid`]'s Display through
4440 // verbatim — the issue string names both the offending slot
4441 // and the offending value (debug-escaped). Peer with the
4442 // `autores_violation_on_non_chart_maintainer_shape` pin on
4443 // the sibling universal-axis `Vec<String>` surface — the
4444 // second layout pin on the Vec<String> per-entry shape
4445 // cascade.
4446 let root = PathBuf::from("/tmp/x");
4447 let manifest = root.join("caixa.lisp");
4448 let default_lib = root.join("lib").join("demo.lisp");
4449 let layout =
4450 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4451 let mut c = caixa(CaixaKind::Biblioteca);
4452 c.etiquetas = vec!["mesh,http,grpc".into()];
4453 let err = layout.verify(&c, &root).unwrap_err();
4454 let LayoutError::EtiquetasViolation { caixa, issue } = err else {
4455 panic!("expected LayoutError::EtiquetasViolation, got {err:?}");
4456 };
4457 assert_eq!(caixa, "demo");
4458 assert!(
4459 issue.contains(":etiquetas"),
4460 "issue must name the offending slot: {issue}",
4461 );
4462 assert!(
4463 issue.contains("mesh,http,grpc"),
4464 "issue must quote the offending value: {issue}",
4465 );
4466 }
4467
4468 // ── autores universal-axis gate wired into verify ───────────────────
4469 //
4470 // Pins the layout-pipeline wire-up of [`Caixa::validate_autores`]:
4471 // the fifth universal-axis Caixa-level value-shape gate (peer of
4472 // `validate_nome` / `validate_versao` / `validate_deps` /
4473 // `validate_etiquetas` / `validate_code_paths`), wired immediately
4474 // after `validate_etiquetas` so the two Vec-shaped universal
4475 // metadata axes sit adjacent in the cascade. Until this wire-up
4476 // landed `:autores` had no shape gate at any layer — the
4477 // maintainer-axis was the second largest universal authoring
4478 // surface on the typed Caixa surface with no validate discipline,
4479 // and unlike `:etiquetas` (caixa-helm dedups the rendered
4480 // `keywords:` array via `BTreeSet` collect at chart render),
4481 // `maintainers:` has *no* renderer-side dedup, so duplicate
4482 // `:autores` entries render verbatim as two identical
4483 // `Maintainer { name, email: None }` records — a strictly worse
4484 // footgun than the peer `:etiquetas` shape.
4485
4486 #[test]
4487 fn autores_violation_on_empty_entry() {
4488 // Canonical paste-from-blank-doc footgun on every kind. The
4489 // wrap envelope wraps [`ManifestError::AutorEmpty`]'s Display
4490 // through verbatim, so the issue string names the offending
4491 // `:autores` axis at the source — the author can grep their
4492 // caixa.lisp for `:autores` and fix the empty entry in one
4493 // edit. Mirrors the peer `etiquetas_violation_on_empty_entry`
4494 // shape (360a499) on the `:etiquetas` axis.
4495 let root = PathBuf::from("/tmp/x");
4496 let manifest = root.join("caixa.lisp");
4497 let default_lib = root.join("lib").join("demo.lisp");
4498 let layout =
4499 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4500 let mut c = caixa(CaixaKind::Biblioteca);
4501 c.autores = vec![String::new()];
4502 let err = layout.verify(&c, &root).unwrap_err();
4503 let LayoutError::AutoresViolation { caixa, issue } = err else {
4504 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4505 };
4506 assert_eq!(caixa, "demo");
4507 assert!(
4508 issue.contains(":autores"),
4509 "issue must name the offending slot: {issue}",
4510 );
4511 }
4512
4513 #[test]
4514 fn autores_violation_on_duplicate_entry() {
4515 // Canonical copy-paste-the-wrong-author footgun. Unlike the
4516 // peer `:etiquetas` axis (silently dedup'd by caixa-helm's
4517 // `BTreeSet` collect at chart render), `:autores` duplicates
4518 // stack verbatim in the rendered `maintainers:` — the gate
4519 // closes the footgun at validate time before any renderer
4520 // sees it. The wrap envelope names the offending author
4521 // verbatim through the inner [`ManifestError::AutorDuplicate`]'s
4522 // Display.
4523 let root = PathBuf::from("/tmp/x");
4524 let manifest = root.join("caixa.lisp");
4525 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
4526 let mut c = caixa(CaixaKind::Servico);
4527 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
4528 c.autores = vec!["pleme-io".into(), "pleme-io".into()];
4529 // The servicos path doesn't exist in this fixture, but the
4530 // `:autores` gate fires before the existence loop (universal
4531 // axis dominates kind-specific existence checks). Wire is
4532 // intact iff the wrap envelope surfaces first.
4533 let err = layout.verify(&c, &root).unwrap_err();
4534 let LayoutError::AutoresViolation { caixa, issue } = err else {
4535 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4536 };
4537 assert_eq!(caixa, "demo");
4538 assert!(
4539 issue.contains("pleme-io"),
4540 "issue must quote the offending author: {issue}",
4541 );
4542 }
4543
4544 #[test]
4545 fn autores_violation_fires_before_kind_coherence_mesh_slot() {
4546 // Cross-axis precedence pin: a Biblioteca with malformed
4547 // `:autores` *and* declared mesh slots (`:membros`) surfaces
4548 // the universal `:autores` diagnostic first, not the
4549 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4550 // `:autores` is universal (every kind owns the slot), so its
4551 // shape diagnostic is more fundamental than the partition-on-
4552 // kind diagnostic. Mirrors the peer
4553 // `etiquetas_violation_fires_before_kind_coherence_mesh_slot`
4554 // pin (360a499) on the `:etiquetas` axis vs the same kind-
4555 // coherence gates.
4556 let root = PathBuf::from("/tmp/x");
4557 let manifest = root.join("caixa.lisp");
4558 let default_lib = root.join("lib").join("demo.lisp");
4559 let layout =
4560 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4561 let mut c = caixa(CaixaKind::Biblioteca);
4562 c.autores = vec![String::new()];
4563 c.membros = vec![crate::aplicacao::Membro {
4564 caixa: "x".into(),
4565 versao: "^0.1".into(),
4566 }];
4567 let err = layout.verify(&c, &root).unwrap_err();
4568 assert!(
4569 matches!(err, LayoutError::AutoresViolation { .. }),
4570 "got {err:?}",
4571 );
4572 }
4573
4574 #[test]
4575 fn autores_violation_fires_after_etiquetas_violation() {
4576 // Cross-axis precedence pin (inside the Vec-shaped universal
4577 // metadata pair): a caixa with both a malformed `:etiquetas`
4578 // entry *and* a malformed `:autores` entry surfaces
4579 // `EtiquetasViolation` first — `:etiquetas` is the fourth
4580 // universal axis in the cascade and runs before `:autores`,
4581 // peer with the canonical identity-axis-first cascade the
4582 // peer gates establish. Mirrors the peer
4583 // `etiquetas_violation_fires_after_deps_violation` precedence
4584 // pin (360a499) on the dep-axis-before-tag-axis pair.
4585 let root = PathBuf::from("/tmp/x");
4586 let manifest = root.join("caixa.lisp");
4587 let default_lib = root.join("lib").join("demo.lisp");
4588 let layout =
4589 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4590 let mut c = caixa(CaixaKind::Biblioteca);
4591 c.etiquetas = vec![String::new()];
4592 c.autores = vec![String::new()];
4593 let err = layout.verify(&c, &root).unwrap_err();
4594 assert!(
4595 matches!(err, LayoutError::EtiquetasViolation { .. }),
4596 "got {err:?}",
4597 );
4598 }
4599
4600 #[test]
4601 fn autores_violation_accepts_canonical_template() {
4602 // Positive control sanity pin: the canonical `Caixa::template`
4603 // shape (`:autores ()` — empty list) passes the gate trivially.
4604 // Mirrors the peer `etiquetas_violation_accepts_canonical_template`
4605 // pin (360a499).
4606 let root = PathBuf::from("/tmp/x");
4607 let manifest = root.join("caixa.lisp");
4608 let default_lib = root.join("lib").join("demo.lisp");
4609 let layout =
4610 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4611 let c = caixa(CaixaKind::Biblioteca);
4612 layout.verify(&c, &root).expect("template must pass");
4613 }
4614
4615 #[test]
4616 fn autores_violation_on_non_chart_maintainer_shape() {
4617 // Canonical paste-from-multiline-doc footgun: the author
4618 // pasted a multi-line block of author records into one
4619 // `:autores` entry instead of splitting into one entry per
4620 // author. The shape gate fires past the empty + duplicate arms
4621 // via [`Caixa::validate_autores`]'s new
4622 // `is_chart_maintainer_name_shape` cascade, and the layout
4623 // envelope wraps [`ManifestError::AutorInvalid`]'s Display
4624 // through verbatim — the issue string names both the offending
4625 // slot and the offending value (debug-escaped). Peer with the
4626 // `descricao_violation_on_non_chart_shape` pin on the sibling
4627 // universal-axis `Option<String>` surface and the
4628 // `licenca_violation_on_non_spdx_shape` /
4629 // `edicao_violation_on_non_year_shape` peers — and the first
4630 // layout pin on the Vec<String> per-entry shape cascade.
4631 let root = PathBuf::from("/tmp/x");
4632 let manifest = root.join("caixa.lisp");
4633 let default_lib = root.join("lib").join("demo.lisp");
4634 let layout =
4635 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4636 let mut c = caixa(CaixaKind::Biblioteca);
4637 c.autores = vec!["alice\nbob".into()];
4638 let err = layout.verify(&c, &root).unwrap_err();
4639 let LayoutError::AutoresViolation { caixa, issue } = err else {
4640 panic!("expected LayoutError::AutoresViolation, got {err:?}");
4641 };
4642 assert_eq!(caixa, "demo");
4643 assert!(
4644 issue.contains(":autores"),
4645 "issue must name the offending slot: {issue}",
4646 );
4647 assert!(
4648 issue.contains("alice\\nbob"),
4649 "issue must quote the offending value (debug-escaped): {issue}",
4650 );
4651 }
4652
4653 // ── repositorio universal-axis gate wired into verify ────────────────
4654 //
4655 // Pins the layout-pipeline wire-up of [`Caixa::validate_repositorio`]:
4656 // the sixth universal-axis Caixa-level value-shape gate (peer of
4657 // `validate_nome` / `validate_versao` / `validate_deps` /
4658 // `validate_etiquetas` / `validate_autores` / `validate_code_paths`),
4659 // wired immediately after `validate_autores` so the universal
4660 // git-URL axis sits adjacent to the two Vec-shaped universal
4661 // metadata axes (`:etiquetas`, `:autores`) in the cascade. Until
4662 // this wire-up landed `:repositorio` had no shape gate at any
4663 // layer — the universal git-shaped homepage axis was the third
4664 // largest universal authoring surface on the typed Caixa with no
4665 // validate discipline, routing the same string through two
4666 // load-bearing substrate consumers (`caixa-helm`'s `Chart.yaml
4667 // home:` field and `caixa-flux`'s FluxCD `GitRepository.spec.url`)
4668 // via `Option::unwrap_or_else` fallbacks that only fire on `None` —
4669 // a `Some("")` silently passed every fallback and rendered as an
4670 // empty URL in both consumers, breaking at `helm template` /
4671 // FluxCD reconcile time far from the source `caixa.lisp`. The
4672 // gate closes the divergence and makes the two `git URL`-shaped
4673 // surfaces on the typed Caixa (`:repositorio` here, `:deps :fonte
4674 // :repo` peer routed through the same shared
4675 // `crate::render::is_git_repo_url` predicate) structurally
4676 // equivalent by construction.
4677
4678 #[test]
4679 fn repositorio_violation_on_empty_some() {
4680 // Canonical paste-from-blank-doc footgun on every kind. The
4681 // wrap envelope wraps [`ManifestError::RepositorioEmpty`]'s
4682 // Display through verbatim, so the issue string names the
4683 // offending `:repositorio` axis at the source — the author
4684 // can grep their caixa.lisp for `:repositorio ""` and fix the
4685 // empty value in one edit. Mirrors the peer
4686 // `autores_violation_on_empty_entry` shape (86c769b) on the
4687 // `:autores` axis.
4688 let root = PathBuf::from("/tmp/x");
4689 let manifest = root.join("caixa.lisp");
4690 let default_lib = root.join("lib").join("demo.lisp");
4691 let layout =
4692 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4693 let mut c = caixa(CaixaKind::Biblioteca);
4694 c.repositorio = Some(String::new());
4695 let err = layout.verify(&c, &root).unwrap_err();
4696 let LayoutError::RepositorioViolation { caixa, issue } = err else {
4697 panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4698 };
4699 assert_eq!(caixa, "demo");
4700 assert!(
4701 issue.contains(":repositorio"),
4702 "issue must name the offending slot: {issue}",
4703 );
4704 }
4705
4706 #[test]
4707 fn repositorio_violation_on_malformed_shape() {
4708 // Canonical CLI-argument-injection footgun: a leading `-`
4709 // value (`-upload-pack=evil`) escapes the `git clone <repo>`
4710 // subprocess argument boundary at clone time. The shared
4711 // `is_git_repo_url` predicate — the same parser the peer
4712 // `:deps :fonte :repo` axis routes through via
4713 // `DepSource::validate` — refuses every leading-`-` shape at
4714 // validate time. The wrap envelope names the offending value
4715 // verbatim through the inner [`ManifestError::RepositorioInvalid`]'s
4716 // Display.
4717 let root = PathBuf::from("/tmp/x");
4718 let manifest = root.join("caixa.lisp");
4719 let default_lib = root.join("lib").join("demo.lisp");
4720 let layout =
4721 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4722 let mut c = caixa(CaixaKind::Biblioteca);
4723 c.repositorio = Some("-upload-pack=evil".into());
4724 let err = layout.verify(&c, &root).unwrap_err();
4725 let LayoutError::RepositorioViolation { caixa, issue } = err else {
4726 panic!("expected LayoutError::RepositorioViolation, got {err:?}");
4727 };
4728 assert_eq!(caixa, "demo");
4729 assert!(
4730 issue.contains("-upload-pack=evil"),
4731 "issue must quote the offending value: {issue}",
4732 );
4733 }
4734
4735 #[test]
4736 fn repositorio_violation_fires_before_kind_coherence_mesh_slot() {
4737 // Cross-axis precedence pin: a Biblioteca with malformed
4738 // `:repositorio` *and* declared mesh slots (`:membros`)
4739 // surfaces the universal `:repositorio` diagnostic first, not
4740 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4741 // `:repositorio` is universal (every kind owns the slot), so
4742 // its shape diagnostic is more fundamental than the
4743 // partition-on-kind diagnostic. Mirrors the peer
4744 // `autores_violation_fires_before_kind_coherence_mesh_slot`
4745 // pin (86c769b) on the `:autores` axis vs the same
4746 // kind-coherence gates.
4747 let root = PathBuf::from("/tmp/x");
4748 let manifest = root.join("caixa.lisp");
4749 let default_lib = root.join("lib").join("demo.lisp");
4750 let layout =
4751 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4752 let mut c = caixa(CaixaKind::Biblioteca);
4753 c.repositorio = Some(String::new());
4754 c.membros = vec![crate::aplicacao::Membro {
4755 caixa: "x".into(),
4756 versao: "^0.1".into(),
4757 }];
4758 let err = layout.verify(&c, &root).unwrap_err();
4759 assert!(
4760 matches!(err, LayoutError::RepositorioViolation { .. }),
4761 "got {err:?}",
4762 );
4763 }
4764
4765 #[test]
4766 fn repositorio_violation_fires_after_autores_violation() {
4767 // Cross-axis precedence pin (inside the universal metadata
4768 // trio): a caixa with both a malformed `:autores` entry *and*
4769 // a malformed `:repositorio` value surfaces `AutoresViolation`
4770 // first — `:autores` is the fifth universal axis in the
4771 // cascade and runs before `:repositorio`, peer with the
4772 // canonical identity-axis-first cascade the peer gates
4773 // establish. Mirrors the peer
4774 // `autores_violation_fires_after_etiquetas_violation`
4775 // precedence pin (86c769b) on the tag-axis-before-author-axis
4776 // pair.
4777 let root = PathBuf::from("/tmp/x");
4778 let manifest = root.join("caixa.lisp");
4779 let default_lib = root.join("lib").join("demo.lisp");
4780 let layout =
4781 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4782 let mut c = caixa(CaixaKind::Biblioteca);
4783 c.autores = vec![String::new()];
4784 c.repositorio = Some(String::new());
4785 let err = layout.verify(&c, &root).unwrap_err();
4786 assert!(
4787 matches!(err, LayoutError::AutoresViolation { .. }),
4788 "got {err:?}",
4789 );
4790 }
4791
4792 #[test]
4793 fn repositorio_violation_accepts_canonical_template() {
4794 // Positive control sanity pin: the canonical `Caixa::template`
4795 // shape (omits `:repositorio` entirely → `None` on the typed
4796 // surface) passes the gate trivially — the gate is a no-op
4797 // when the author didn't author a value. Mirrors the peer
4798 // `autores_violation_accepts_canonical_template` pin (86c769b).
4799 let root = PathBuf::from("/tmp/x");
4800 let manifest = root.join("caixa.lisp");
4801 let default_lib = root.join("lib").join("demo.lisp");
4802 let layout =
4803 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4804 let c = caixa(CaixaKind::Biblioteca);
4805 layout.verify(&c, &root).expect("template must pass");
4806 }
4807
4808 #[test]
4809 fn repositorio_violation_accepts_canonical_github_shorthand() {
4810 // Positive control pin on the canonical pleme-io `:repositorio`
4811 // shape: the `github:org/repo` shorthand the README quickstart
4812 // and the `caixa-helm` / `caixa-mesh` / `caixa-flux` fixtures
4813 // all use passes the gate end-to-end. Closes the structural
4814 // equivalence between this surface and the peer `:deps :fonte
4815 // :repo` axis — both consume `crate::render::is_git_repo_url`
4816 // and both must agree on the same accepted shape set.
4817 let root = PathBuf::from("/tmp/x");
4818 let manifest = root.join("caixa.lisp");
4819 let default_lib = root.join("lib").join("demo.lisp");
4820 let layout =
4821 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4822 let mut c = caixa(CaixaKind::Biblioteca);
4823 c.repositorio = Some("github:pleme-io/hello-rio".into());
4824 layout.verify(&c, &root).expect("canonical shape must pass");
4825 }
4826
4827 // ── descricao universal-axis gate wired into verify ──────────────────
4828 //
4829 // Pins the layout-pipeline wire-up of [`Caixa::validate_descricao`]:
4830 // the seventh universal-axis Caixa-level value-shape gate (peer of
4831 // `validate_nome` / `validate_versao` / `validate_deps` /
4832 // `validate_etiquetas` / `validate_autores` / `validate_repositorio` /
4833 // `validate_code_paths`), wired immediately after `validate_repositorio`
4834 // so the universal free-form-prose axis sits adjacent to the
4835 // universal git-URL axis in the cascade. Until this wire-up landed
4836 // `:descricao` had no shape gate at any layer — the empty
4837 // `Some("")` silently passed both `caixa-helm` consumers'
4838 // `Option::unwrap_or_else(|| <fallback>)` (which only fire on
4839 // `None`) and rendered as `Chart.yaml description: ""` plus a
4840 // blank `README.md` header, breaking at `helm lint` time
4841 // (`WARNING [chart.metadata.description]: description is required`
4842 // on `apiVersion: v2` charts) far from the source `caixa.lisp`.
4843 // Closes the same `Some("")` skips-`unwrap_or_else` footgun the
4844 // peer `:repositorio` gate (577b0a9) closed, on the universal
4845 // free-form-prose summary axis.
4846
4847 #[test]
4848 fn descricao_violation_on_empty_some() {
4849 // Canonical paste-from-blank-doc footgun on every kind. The
4850 // wrap envelope wraps [`ManifestError::DescricaoEmpty`]'s
4851 // Display through verbatim, so the issue string names the
4852 // offending `:descricao` axis at the source — the author can
4853 // grep their caixa.lisp for `:descricao ""` and fix the empty
4854 // value in one edit. Mirrors the peer
4855 // `repositorio_violation_on_empty_some` shape (577b0a9) on
4856 // the sibling `Option<String>` `:repositorio` axis.
4857 let root = PathBuf::from("/tmp/x");
4858 let manifest = root.join("caixa.lisp");
4859 let default_lib = root.join("lib").join("demo.lisp");
4860 let layout =
4861 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4862 let mut c = caixa(CaixaKind::Biblioteca);
4863 c.descricao = Some(String::new());
4864 let err = layout.verify(&c, &root).unwrap_err();
4865 let LayoutError::DescricaoViolation { caixa, issue } = err else {
4866 panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4867 };
4868 assert_eq!(caixa, "demo");
4869 assert!(
4870 issue.contains(":descricao"),
4871 "issue must name the offending slot: {issue}",
4872 );
4873 }
4874
4875 #[test]
4876 fn descricao_violation_fires_before_kind_coherence_mesh_slot() {
4877 // Cross-axis precedence pin: a Biblioteca with empty
4878 // `:descricao` *and* declared mesh slots (`:membros`)
4879 // surfaces the universal `:descricao` diagnostic first, not
4880 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
4881 // `:descricao` is universal (every kind owns the slot), so
4882 // its shape diagnostic is more fundamental than the
4883 // partition-on-kind diagnostic. Mirrors the peer
4884 // `repositorio_violation_fires_before_kind_coherence_mesh_slot`
4885 // pin (577b0a9) on the `:repositorio` axis vs the same
4886 // kind-coherence gates.
4887 let root = PathBuf::from("/tmp/x");
4888 let manifest = root.join("caixa.lisp");
4889 let default_lib = root.join("lib").join("demo.lisp");
4890 let layout =
4891 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4892 let mut c = caixa(CaixaKind::Biblioteca);
4893 c.descricao = Some(String::new());
4894 c.membros = vec![crate::aplicacao::Membro {
4895 caixa: "x".into(),
4896 versao: "^0.1".into(),
4897 }];
4898 let err = layout.verify(&c, &root).unwrap_err();
4899 assert!(
4900 matches!(err, LayoutError::DescricaoViolation { .. }),
4901 "got {err:?}",
4902 );
4903 }
4904
4905 #[test]
4906 fn descricao_violation_fires_after_repositorio_violation() {
4907 // Cross-axis precedence pin (inside the universal metadata
4908 // cascade): a caixa with both a malformed `:repositorio` *and*
4909 // an empty `:descricao` surfaces `RepositorioViolation`
4910 // first — `:repositorio` is the sixth universal axis in the
4911 // cascade and runs before `:descricao`, peer with the
4912 // canonical identity-axis-first cascade the peer gates
4913 // establish. Mirrors the peer
4914 // `repositorio_violation_fires_after_autores_violation`
4915 // precedence pin (577b0a9) on the autores-axis-before-
4916 // repositorio-axis pair.
4917 let root = PathBuf::from("/tmp/x");
4918 let manifest = root.join("caixa.lisp");
4919 let default_lib = root.join("lib").join("demo.lisp");
4920 let layout =
4921 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4922 let mut c = caixa(CaixaKind::Biblioteca);
4923 c.repositorio = Some(String::new());
4924 c.descricao = Some(String::new());
4925 let err = layout.verify(&c, &root).unwrap_err();
4926 assert!(
4927 matches!(err, LayoutError::RepositorioViolation { .. }),
4928 "got {err:?}",
4929 );
4930 }
4931
4932 #[test]
4933 fn descricao_violation_accepts_none() {
4934 // Positive control sanity pin: a caixa that omits
4935 // `:descricao` entirely (the canonical `Caixa::template` shape
4936 // carries `Some("FIXME — describe this caixa")`, but the
4937 // layout-test fixture defaults to `None`) passes the gate
4938 // trivially — the gate is a no-op when the author didn't
4939 // author a value. Mirrors the peer
4940 // `repositorio_violation_accepts_canonical_template` pin
4941 // (577b0a9).
4942 let root = PathBuf::from("/tmp/x");
4943 let manifest = root.join("caixa.lisp");
4944 let default_lib = root.join("lib").join("demo.lisp");
4945 let layout =
4946 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4947 let c = caixa(CaixaKind::Biblioteca);
4948 layout.verify(&c, &root).expect("None must pass");
4949 }
4950
4951 #[test]
4952 fn descricao_violation_accepts_canonical_summary() {
4953 // Positive control pin on the canonical pleme-io `:descricao`
4954 // shape: a short free-form prose summary the `caixa-helm` /
4955 // `caixa-flux` / `caixa-mesh` fixtures all carry passes the
4956 // gate end-to-end.
4957 let root = PathBuf::from("/tmp/x");
4958 let manifest = root.join("caixa.lisp");
4959 let default_lib = root.join("lib").join("demo.lisp");
4960 let layout =
4961 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4962 let mut c = caixa(CaixaKind::Biblioteca);
4963 c.descricao = Some("Canonical Rust→wasm32-wasip2 caixa Servico.".into());
4964 layout
4965 .verify(&c, &root)
4966 .expect("canonical summary must pass");
4967 }
4968
4969 #[test]
4970 fn descricao_violation_on_non_chart_shape() {
4971 // Shape-predicate wire-up pin: a malformed `:descricao` value
4972 // that's a non-empty `Some(s)` but carries a paste-from-
4973 // multiline-doc embedded newline surfaces the
4974 // `DescricaoViolation` envelope via the manifest-layer
4975 // `ManifestError::DescricaoInvalid` arm. Mirrors the peer
4976 // `descricao_violation_on_empty_some` shape on the empty arm
4977 // of the same axis and the peer
4978 // `licenca_violation_on_non_spdx_shape` shape on the sibling
4979 // `:licenca` axis. Until this gate landed a value like
4980 // `"Checkout\nflow."` (an embedded newline) or `"Checkout
4981 // flow. "` (a trailing whitespace) silently passed
4982 // `StandardLayout::verify` and landed in the rendered
4983 // Chart.yaml `description:` field as a YAML-illegal
4984 // multi-line scalar or a silently-trimmed whitespace
4985 // round-trip far from the source caixa.lisp.
4986 let root = PathBuf::from("/tmp/x");
4987 let manifest = root.join("caixa.lisp");
4988 let default_lib = root.join("lib").join("demo.lisp");
4989 let layout =
4990 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
4991 let mut c = caixa(CaixaKind::Biblioteca);
4992 c.descricao = Some("Checkout\nflow.".into());
4993 let err = layout.verify(&c, &root).unwrap_err();
4994 let LayoutError::DescricaoViolation { caixa, issue } = err else {
4995 panic!("expected LayoutError::DescricaoViolation, got {err:?}");
4996 };
4997 assert_eq!(caixa, "demo");
4998 assert!(
4999 issue.contains(":descricao"),
5000 "issue must name the offending slot: {issue}",
5001 );
5002 // The wrapped `ManifestError::DescricaoInvalid` Display uses
5003 // `{descricao:?}` (Debug) so the embedded newline surfaces
5004 // debug-escaped as `\n` in the issue string.
5005 assert!(
5006 issue.contains("Checkout\\nflow."),
5007 "issue must quote the offending value (debug-escaped): {issue}",
5008 );
5009 }
5010
5011 // ── :licenca empty-Some shape wired into verify (universal axis) ──
5012 //
5013 // Until this wire-up landed `Caixa::validate_licenca` did not
5014 // exist — the universal SPDX-shaped license-expression axis had
5015 // no shape gate at any layer, so an empty `Some("")` silently
5016 // passed `Caixa::from_lisp` and `StandardLayout::verify` and
5017 // landed as a bare trailing period in the rendered
5018 // `lareira-<nome>` chart's `README.md` `## License` section via
5019 // the `caixa-helm` consumer's `caixa.licenca.clone().unwrap_or_else(||
5020 // "MIT".into())` (which only fires on `None`) at
5021 // `caixa-helm/src/lib.rs:361`. Closes the same `Some("")`
5022 // skips-`unwrap_or_else` footgun the peer `:repositorio`
5023 // (577b0a9) and `:descricao` (4e6db38) gates closed, on the
5024 // universal license-expression axis.
5025
5026 #[test]
5027 fn licenca_violation_on_empty_some() {
5028 // Canonical paste-from-blank-doc footgun on every kind. The
5029 // wrap envelope wraps [`ManifestError::LicencaEmpty`]'s
5030 // Display through verbatim, so the issue string names the
5031 // offending `:licenca` axis at the source — the author can
5032 // grep their caixa.lisp for `:licenca ""` and fix the empty
5033 // value in one edit. Mirrors the peer
5034 // `descricao_violation_on_empty_some` shape (4e6db38) on
5035 // the sibling `Option<String>` `:licenca` axis.
5036 let root = PathBuf::from("/tmp/x");
5037 let manifest = root.join("caixa.lisp");
5038 let default_lib = root.join("lib").join("demo.lisp");
5039 let layout =
5040 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5041 let mut c = caixa(CaixaKind::Biblioteca);
5042 c.licenca = Some(String::new());
5043 let err = layout.verify(&c, &root).unwrap_err();
5044 let LayoutError::LicencaViolation { caixa, issue } = err else {
5045 panic!("expected LayoutError::LicencaViolation, got {err:?}");
5046 };
5047 assert_eq!(caixa, "demo");
5048 assert!(
5049 issue.contains(":licenca"),
5050 "issue must name the offending slot: {issue}",
5051 );
5052 }
5053
5054 #[test]
5055 fn licenca_violation_fires_before_kind_coherence_mesh_slot() {
5056 // Cross-axis precedence pin: a Biblioteca with empty
5057 // `:licenca` *and* declared mesh slots (`:membros`)
5058 // surfaces the universal `:licenca` diagnostic first, not
5059 // the kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
5060 // `:licenca` is universal (every kind owns the slot), so
5061 // its shape diagnostic is more fundamental than the
5062 // partition-on-kind diagnostic. Mirrors the peer
5063 // `descricao_violation_fires_before_kind_coherence_mesh_slot`
5064 // pin (4e6db38) on the `:descricao` axis vs the same
5065 // kind-coherence gates.
5066 let root = PathBuf::from("/tmp/x");
5067 let manifest = root.join("caixa.lisp");
5068 let default_lib = root.join("lib").join("demo.lisp");
5069 let layout =
5070 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5071 let mut c = caixa(CaixaKind::Biblioteca);
5072 c.licenca = Some(String::new());
5073 c.membros = vec![crate::aplicacao::Membro {
5074 caixa: "x".into(),
5075 versao: "^0.1".into(),
5076 }];
5077 let err = layout.verify(&c, &root).unwrap_err();
5078 assert!(
5079 matches!(err, LayoutError::LicencaViolation { .. }),
5080 "got {err:?}",
5081 );
5082 }
5083
5084 #[test]
5085 fn licenca_violation_fires_after_descricao_violation() {
5086 // Cross-axis precedence pin (inside the universal metadata
5087 // cascade): a caixa with both an empty `:descricao` *and*
5088 // an empty `:licenca` surfaces `DescricaoViolation`
5089 // first — `:descricao` is the seventh universal axis in the
5090 // cascade and runs before `:licenca`, peer with the
5091 // canonical identity-axis-first cascade the peer gates
5092 // establish. Mirrors the peer
5093 // `descricao_violation_fires_after_repositorio_violation`
5094 // precedence pin (4e6db38) on the repositorio-axis-before-
5095 // descricao-axis pair.
5096 let root = PathBuf::from("/tmp/x");
5097 let manifest = root.join("caixa.lisp");
5098 let default_lib = root.join("lib").join("demo.lisp");
5099 let layout =
5100 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5101 let mut c = caixa(CaixaKind::Biblioteca);
5102 c.descricao = Some(String::new());
5103 c.licenca = Some(String::new());
5104 let err = layout.verify(&c, &root).unwrap_err();
5105 assert!(
5106 matches!(err, LayoutError::DescricaoViolation { .. }),
5107 "got {err:?}",
5108 );
5109 }
5110
5111 #[test]
5112 fn licenca_violation_accepts_none() {
5113 // Positive control sanity pin: a caixa that omits `:licenca`
5114 // entirely (the layout-test fixture defaults to `None`)
5115 // passes the gate trivially — the gate is a no-op when the
5116 // author didn't author a value. Mirrors the peer
5117 // `descricao_violation_accepts_none` pin (4e6db38).
5118 let root = PathBuf::from("/tmp/x");
5119 let manifest = root.join("caixa.lisp");
5120 let default_lib = root.join("lib").join("demo.lisp");
5121 let layout =
5122 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5123 let c = caixa(CaixaKind::Biblioteca);
5124 layout.verify(&c, &root).expect("None must pass");
5125 }
5126
5127 #[test]
5128 fn licenca_violation_accepts_canonical_expression() {
5129 // Positive control pin on the canonical pleme-io `:licenca`
5130 // shape: a non-empty SPDX expression the `caixa-helm` /
5131 // `caixa-flux` / `caixa-mesh` fixtures all carry (`"MIT"`)
5132 // passes the gate end-to-end.
5133 let root = PathBuf::from("/tmp/x");
5134 let manifest = root.join("caixa.lisp");
5135 let default_lib = root.join("lib").join("demo.lisp");
5136 let layout =
5137 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5138 let mut c = caixa(CaixaKind::Biblioteca);
5139 c.licenca = Some("Apache-2.0 OR MIT".into());
5140 layout
5141 .verify(&c, &root)
5142 .expect("canonical SPDX expression must pass");
5143 }
5144
5145 #[test]
5146 fn licenca_violation_on_non_spdx_shape() {
5147 // Shape-predicate wire-up pin: a malformed `:licenca` value
5148 // that's a non-empty `Some(s)` but falls outside the SPDX
5149 // expression alphabet floor surfaces the `LicencaViolation`
5150 // envelope via the manifest-layer `ManifestError::LicencaInvalid`
5151 // arm. Mirrors the peer `licenca_violation_on_empty_some`
5152 // shape on the empty arm of the same axis and the peer
5153 // `edicao_violation_on_non_year_shape` shape on the sibling
5154 // `:edicao` axis. Until this gate landed a value like
5155 // `"Apache_2.0"` (an underscore-instead-of-hyphen typo) or
5156 // `"MIT, Apache-2.0"` (a comma-instead-of-`OR`-keyword
5157 // colloquial idiom) silently passed `StandardLayout::verify`
5158 // and landed in the rendered chart `README.md` `## License`
5159 // section + a future SPDX-aware Chart.yaml `license:`
5160 // emitter would refuse the value at `helm lint` time far
5161 // from the source caixa.lisp.
5162 let root = PathBuf::from("/tmp/x");
5163 let manifest = root.join("caixa.lisp");
5164 let default_lib = root.join("lib").join("demo.lisp");
5165 let layout =
5166 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5167 let mut c = caixa(CaixaKind::Biblioteca);
5168 c.licenca = Some("Apache_2.0".into());
5169 let err = layout.verify(&c, &root).unwrap_err();
5170 let LayoutError::LicencaViolation { caixa, issue } = err else {
5171 panic!("expected LayoutError::LicencaViolation, got {err:?}");
5172 };
5173 assert_eq!(caixa, "demo");
5174 assert!(
5175 issue.contains(":licenca"),
5176 "issue must name the offending slot: {issue}",
5177 );
5178 assert!(
5179 issue.contains("Apache_2.0"),
5180 "issue must quote the offending value: {issue}",
5181 );
5182 }
5183
5184 // ── :edicao empty-Some shape wired into verify (universal axis) ──
5185 //
5186 // Until this wire-up landed `Caixa::validate_edicao` did not
5187 // exist — the universal language-edition axis had no shape gate
5188 // at any layer, so an empty `Some("")` silently passed
5189 // `Caixa::from_lisp` and `StandardLayout::verify` and landed as a
5190 // bare `(:edicao "")` line in the rendered caixa.lisp, ready for
5191 // a future renderer-side consumer's `Option::unwrap_or_else`
5192 // (which only fires on `None`) to skip its fallback. Closes the
5193 // same `Some("")`-skips-`unwrap_or_else` footgun the peer
5194 // `:repositorio` (577b0a9), `:descricao` (4e6db38), and
5195 // `:licenca` (3d1e535) gates closed, on the universal language-
5196 // edition axis — the last un-gated universal-axis
5197 // `Option<String>` Caixa-level value-shape surface.
5198
5199 #[test]
5200 fn edicao_violation_on_empty_some() {
5201 // Canonical paste-from-blank-doc footgun on every kind. The
5202 // wrap envelope wraps [`ManifestError::EdicaoEmpty`]'s
5203 // Display through verbatim, so the issue string names the
5204 // offending `:edicao` axis at the source — the author can
5205 // grep their caixa.lisp for `:edicao ""` and fix the empty
5206 // value in one edit. Mirrors the peer
5207 // `licenca_violation_on_empty_some` shape (3d1e535) on the
5208 // sibling `Option<String>` `:edicao` axis.
5209 let root = PathBuf::from("/tmp/x");
5210 let manifest = root.join("caixa.lisp");
5211 let default_lib = root.join("lib").join("demo.lisp");
5212 let layout =
5213 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5214 let mut c = caixa(CaixaKind::Biblioteca);
5215 c.edicao = Some(String::new());
5216 let err = layout.verify(&c, &root).unwrap_err();
5217 let LayoutError::EdicaoViolation { caixa, issue } = err else {
5218 panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5219 };
5220 assert_eq!(caixa, "demo");
5221 assert!(
5222 issue.contains(":edicao"),
5223 "issue must name the offending slot: {issue}",
5224 );
5225 }
5226
5227 #[test]
5228 fn edicao_violation_fires_before_kind_coherence_mesh_slot() {
5229 // Cross-axis precedence pin: a Biblioteca with empty
5230 // `:edicao` *and* declared mesh slots (`:membros`) surfaces
5231 // the universal `:edicao` diagnostic first, not the
5232 // kind-coherence `MeshSlotsOnNonAplicacao` diagnostic.
5233 // `:edicao` is universal (every kind owns the slot), so
5234 // its shape diagnostic is more fundamental than the
5235 // partition-on-kind diagnostic. Mirrors the peer
5236 // `licenca_violation_fires_before_kind_coherence_mesh_slot`
5237 // pin (3d1e535) on the `:licenca` axis vs the same
5238 // kind-coherence gates.
5239 let root = PathBuf::from("/tmp/x");
5240 let manifest = root.join("caixa.lisp");
5241 let default_lib = root.join("lib").join("demo.lisp");
5242 let layout =
5243 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5244 let mut c = caixa(CaixaKind::Biblioteca);
5245 c.edicao = Some(String::new());
5246 c.membros = vec![crate::aplicacao::Membro {
5247 caixa: "x".into(),
5248 versao: "^0.1".into(),
5249 }];
5250 let err = layout.verify(&c, &root).unwrap_err();
5251 assert!(
5252 matches!(err, LayoutError::EdicaoViolation { .. }),
5253 "got {err:?}",
5254 );
5255 }
5256
5257 #[test]
5258 fn edicao_violation_fires_after_licenca_violation() {
5259 // Cross-axis precedence pin (inside the universal metadata
5260 // cascade): a caixa with both an empty `:licenca` *and* an
5261 // empty `:edicao` surfaces `LicencaViolation` first —
5262 // `:licenca` is the eighth universal axis in the cascade
5263 // and runs before `:edicao`, peer with the canonical
5264 // identity-axis-first cascade the peer gates establish.
5265 // Mirrors the peer
5266 // `licenca_violation_fires_after_descricao_violation`
5267 // precedence pin (3d1e535) on the descricao-axis-before-
5268 // licenca-axis pair.
5269 let root = PathBuf::from("/tmp/x");
5270 let manifest = root.join("caixa.lisp");
5271 let default_lib = root.join("lib").join("demo.lisp");
5272 let layout =
5273 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5274 let mut c = caixa(CaixaKind::Biblioteca);
5275 c.licenca = Some(String::new());
5276 c.edicao = Some(String::new());
5277 let err = layout.verify(&c, &root).unwrap_err();
5278 assert!(
5279 matches!(err, LayoutError::LicencaViolation { .. }),
5280 "got {err:?}",
5281 );
5282 }
5283
5284 #[test]
5285 fn edicao_violation_accepts_none() {
5286 // Positive control sanity pin: a caixa that omits `:edicao`
5287 // entirely (the layout-test fixture defaults to `None`)
5288 // passes the gate trivially — the gate is a no-op when the
5289 // author didn't author a value. Mirrors the peer
5290 // `licenca_violation_accepts_none` pin (3d1e535).
5291 let root = PathBuf::from("/tmp/x");
5292 let manifest = root.join("caixa.lisp");
5293 let default_lib = root.join("lib").join("demo.lisp");
5294 let layout =
5295 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5296 let c = caixa(CaixaKind::Biblioteca);
5297 layout.verify(&c, &root).expect("None must pass");
5298 }
5299
5300 #[test]
5301 fn edicao_violation_accepts_canonical_value() {
5302 // Positive control pin on the canonical pleme-io `:edicao`
5303 // shape: the `"2026"` edition every `caixa-helm` /
5304 // `caixa-flux` / `caixa-mesh` / `caixa-core/src/render.rs`
5305 // fixture carries by construction passes the gate end-to-end.
5306 let root = PathBuf::from("/tmp/x");
5307 let manifest = root.join("caixa.lisp");
5308 let default_lib = root.join("lib").join("demo.lisp");
5309 let layout =
5310 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5311 let mut c = caixa(CaixaKind::Biblioteca);
5312 c.edicao = Some("2026".into());
5313 layout
5314 .verify(&c, &root)
5315 .expect("canonical edition must pass");
5316 }
5317
5318 #[test]
5319 fn edicao_violation_on_non_year_shape() {
5320 // Shape-predicate wire-up pin: a malformed `:edicao` value
5321 // that's a non-empty `Some(s)` but not a 4-digit ASCII
5322 // decimal year surfaces the `EdicaoViolation` envelope via
5323 // the manifest-layer `ManifestError::EdicaoInvalid` arm.
5324 // Mirrors the peer `edicao_violation_on_empty_some` shape
5325 // on the empty arm of the same axis. Until this gate landed
5326 // a value like `"v2026"` (a familiar git-tag idiom that
5327 // doesn't apply to the year-shaped edition axis) silently
5328 // passed `StandardLayout::verify` and broke at the
5329 // substrate's build-time edition selector far from the
5330 // source caixa.lisp.
5331 let root = PathBuf::from("/tmp/x");
5332 let manifest = root.join("caixa.lisp");
5333 let default_lib = root.join("lib").join("demo.lisp");
5334 let layout =
5335 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5336 let mut c = caixa(CaixaKind::Biblioteca);
5337 c.edicao = Some("v2026".into());
5338 let err = layout.verify(&c, &root).unwrap_err();
5339 let LayoutError::EdicaoViolation { caixa, issue } = err else {
5340 panic!("expected LayoutError::EdicaoViolation, got {err:?}");
5341 };
5342 assert_eq!(caixa, "demo");
5343 assert!(
5344 issue.contains(":edicao"),
5345 "issue must name the offending slot: {issue}",
5346 );
5347 assert!(
5348 issue.contains("v2026"),
5349 "issue must quote the offending value: {issue}",
5350 );
5351 }
5352
5353 // ── Caixa-identity gates (`:nome`, `:versao`) wired into verify ────
5354 //
5355 // Until this wire-up landed `Caixa::validate_nome` and
5356 // `Caixa::validate_versao` lived as `pub fn` on `Caixa` with full
5357 // per-arm unit coverage in `manifest::tests`, but no production
5358 // path called them — `feira build` silently accepted malformed
5359 // `:nome` / `:versao` and the failure surfaced at `helm install` /
5360 // `kubectl apply` / `feira publish` / lacre-resolve / `:upgrade-from
5361 // :from` matching time, far from the source `caixa.lisp`. The
5362 // following pins fence the layout-pipeline wire-up: every layout
5363 // verify on a structurally-invalid Caixa identity axis surfaces
5364 // the per-axis `*Violation { caixa, issue }` envelope before any
5365 // kind-coherence, code-path, or downstream gate sees it.
5366
5367 #[test]
5368 fn nome_violation_on_uppercase() {
5369 let root = PathBuf::from("/tmp/x");
5370 let manifest = root.join("caixa.lisp");
5371 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5372 let mut c = caixa(CaixaKind::Biblioteca);
5373 c.nome = "MyApp".into();
5374 let err = layout.verify(&c, &root).unwrap_err();
5375 let LayoutError::NomeViolation { caixa, issue } = err else {
5376 panic!("expected LayoutError::NomeViolation, got {err:?}");
5377 };
5378 assert_eq!(caixa, "MyApp");
5379 assert!(
5380 issue.contains("MyApp"),
5381 "issue must quote the offending nome: {issue}",
5382 );
5383 }
5384
5385 #[test]
5386 fn nome_violation_on_underscore() {
5387 let root = PathBuf::from("/tmp/x");
5388 let manifest = root.join("caixa.lisp");
5389 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5390 let mut c = caixa(CaixaKind::Biblioteca);
5391 c.nome = "my_app".into();
5392 let err = layout.verify(&c, &root).unwrap_err();
5393 assert!(
5394 matches!(err, LayoutError::NomeViolation { ref caixa, .. } if caixa == "my_app"),
5395 "got {err:?}",
5396 );
5397 }
5398
5399 #[test]
5400 fn nome_violation_on_empty() {
5401 // Empty `:nome` surfaces NomeViolation wrapping the narrower
5402 // `ManifestError::NomeEmpty` arm — the empty-first cascade the
5403 // peer per-axis name gates already use.
5404 let root = PathBuf::from("/tmp/x");
5405 let manifest = root.join("caixa.lisp");
5406 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5407 let mut c = caixa(CaixaKind::Biblioteca);
5408 c.nome = String::new();
5409 let err = layout.verify(&c, &root).unwrap_err();
5410 let LayoutError::NomeViolation { caixa, issue } = err else {
5411 panic!("expected LayoutError::NomeViolation, got {err:?}");
5412 };
5413 assert!(caixa.is_empty());
5414 assert!(
5415 issue.contains(":nome is empty"),
5416 "issue must surface the empty-arm diagnostic: {issue}",
5417 );
5418 }
5419
5420 #[test]
5421 fn versao_violation_on_missing_patch() {
5422 // `"0.1"` — the canonical "I shortened it" footgun. Helm /
5423 // OCI / lacre-resolve / `:upgrade-from :from` all strict-parse
5424 // through `semver::Version::parse`, which refuses a two-part
5425 // shape.
5426 let root = PathBuf::from("/tmp/x");
5427 let manifest = root.join("caixa.lisp");
5428 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5429 let mut c = caixa(CaixaKind::Biblioteca);
5430 c.versao = "0.1".into();
5431 let err = layout.verify(&c, &root).unwrap_err();
5432 let LayoutError::VersaoViolation { caixa, issue } = err else {
5433 panic!("expected LayoutError::VersaoViolation, got {err:?}");
5434 };
5435 assert_eq!(caixa, "demo");
5436 assert!(
5437 issue.contains("0.1"),
5438 "issue must quote the offending versao: {issue}",
5439 );
5440 }
5441
5442 #[test]
5443 fn versao_violation_on_git_tag_shape() {
5444 // `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo.
5445 let root = PathBuf::from("/tmp/x");
5446 let manifest = root.join("caixa.lisp");
5447 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5448 let mut c = caixa(CaixaKind::Biblioteca);
5449 c.versao = "v0.1.0".into();
5450 let err = layout.verify(&c, &root).unwrap_err();
5451 assert!(
5452 matches!(err, LayoutError::VersaoViolation { ref issue, .. }
5453 if issue.contains("v0.1.0")),
5454 "got {err:?}",
5455 );
5456 }
5457
5458 #[test]
5459 fn versao_violation_on_empty() {
5460 let root = PathBuf::from("/tmp/x");
5461 let manifest = root.join("caixa.lisp");
5462 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5463 let mut c = caixa(CaixaKind::Biblioteca);
5464 c.versao = String::new();
5465 let err = layout.verify(&c, &root).unwrap_err();
5466 let LayoutError::VersaoViolation { caixa, issue } = err else {
5467 panic!("expected LayoutError::VersaoViolation, got {err:?}");
5468 };
5469 assert_eq!(caixa, "demo");
5470 assert!(
5471 issue.contains(":versao is empty"),
5472 "issue must surface the empty-arm diagnostic: {issue}",
5473 );
5474 }
5475
5476 #[test]
5477 fn nome_violation_fires_before_versao_violation() {
5478 // Precedence pin: when both `:nome` and `:versao` are malformed,
5479 // `:nome` surfaces first — the canonical declaration-order
5480 // precedence the `ManifestError` family establishes, the same
5481 // grep-order the author follows when fixing in `caixa.lisp`.
5482 let root = PathBuf::from("/tmp/x");
5483 let manifest = root.join("caixa.lisp");
5484 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5485 let mut c = caixa(CaixaKind::Biblioteca);
5486 c.nome = "MyApp".into();
5487 c.versao = "0.1".into();
5488 let err = layout.verify(&c, &root).unwrap_err();
5489 assert!(
5490 matches!(err, LayoutError::NomeViolation { .. }),
5491 "got {err:?} — nome must fire before versao",
5492 );
5493 }
5494
5495 #[test]
5496 fn nome_violation_fires_before_kind_coherence() {
5497 // Precedence pin: a Biblioteca caixa with a malformed `:nome`
5498 // AND a declared mesh slot surfaces NomeViolation, not
5499 // MeshSlotsOnNonAplicacao — the identity-axis gate is more
5500 // fundamental than the kind-coherence gate (which carries
5501 // `caixa.nome` verbatim in its diagnostic, and so depends on the
5502 // name being structurally valid to render a useful message).
5503 let root = PathBuf::from("/tmp/x");
5504 let manifest = root.join("caixa.lisp");
5505 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5506 let mut c = caixa(CaixaKind::Biblioteca);
5507 c.nome = "MyApp".into();
5508 c.membros = vec![crate::aplicacao::Membro {
5509 caixa: "x".into(),
5510 versao: "^0.1".into(),
5511 }];
5512 let err = layout.verify(&c, &root).unwrap_err();
5513 assert!(
5514 matches!(err, LayoutError::NomeViolation { .. }),
5515 "got {err:?} — nome must fire before MeshSlotsOnNonAplicacao",
5516 );
5517 }
5518
5519 #[test]
5520 fn nome_violation_fires_before_owncode() {
5521 // Precedence pin: a Supervisor with a malformed `:nome` AND
5522 // declared `:bibliotecas` surfaces NomeViolation, not
5523 // SupervisorOwnsCode — same rationale as above.
5524 let root = PathBuf::from("/tmp/x");
5525 let manifest = root.join("caixa.lisp");
5526 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5527 let mut c = caixa(CaixaKind::Supervisor);
5528 c.nome = "MyApp".into();
5529 c.bibliotecas = vec!["lib/x.lisp".into()];
5530 let err = layout.verify(&c, &root).unwrap_err();
5531 assert!(
5532 matches!(err, LayoutError::NomeViolation { .. }),
5533 "got {err:?} — nome must fire before SupervisorOwnsCode",
5534 );
5535 }
5536
5537 #[test]
5538 fn versao_violation_fires_before_kind_coherence() {
5539 // Precedence pin: a Biblioteca with a valid `:nome` but a
5540 // malformed `:versao` AND a declared servico slot surfaces
5541 // VersaoViolation before ServicoSlotsOnNonServico.
5542 let root = PathBuf::from("/tmp/x");
5543 let manifest = root.join("caixa.lisp");
5544 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5545 let mut c = caixa(CaixaKind::Biblioteca);
5546 c.versao = "v0.1.0".into();
5547 c.limits = Some(crate::LimitsSpec {
5548 memory: Some(64 * 1024 * 1024),
5549 ..Default::default()
5550 });
5551 let err = layout.verify(&c, &root).unwrap_err();
5552 assert!(
5553 matches!(err, LayoutError::VersaoViolation { .. }),
5554 "got {err:?} — versao must fire before ServicoSlotsOnNonServico",
5555 );
5556 }
5557
5558 #[test]
5559 fn nome_violation_fires_before_missing_lib() {
5560 // Precedence pin: a Biblioteca with a malformed `:nome` and no
5561 // lib entry surfaces NomeViolation, not MissingLib — the
5562 // identity-axis gate is more fundamental than the layout's
5563 // `lib/<nome>.lisp` default-path check (which derives the
5564 // expected path from `:nome` itself, so would surface a
5565 // misleading "expected lib/MyApp.lisp" diagnostic against an
5566 // unrecoverable name).
5567 let root = PathBuf::from("/tmp/x");
5568 let manifest = root.join("caixa.lisp");
5569 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5570 let mut c = caixa(CaixaKind::Biblioteca);
5571 c.nome = "MyApp".into();
5572 let err = layout.verify(&c, &root).unwrap_err();
5573 assert!(
5574 matches!(err, LayoutError::NomeViolation { .. }),
5575 "got {err:?} — nome must fire before MissingLib",
5576 );
5577 }
5578
5579 #[test]
5580 fn nome_versao_violations_fire_after_missing_manifest() {
5581 // Precedence pin: `MissingManifest` still dominates — there's
5582 // no caixa to identity-check when the manifest is missing.
5583 let root = PathBuf::from("/tmp/x");
5584 let layout = StandardLayout::new().with_path_exists(|_| false);
5585 let mut c = caixa(CaixaKind::Biblioteca);
5586 c.nome = "MyApp".into();
5587 c.versao = "0.1".into();
5588 let err = layout.verify(&c, &root).unwrap_err();
5589 assert!(
5590 matches!(err, LayoutError::MissingManifest(_)),
5591 "got {err:?} — MissingManifest must dominate identity gates",
5592 );
5593 }
5594
5595 #[test]
5596 fn valid_nome_versao_passes_to_downstream_gates() {
5597 // Sanity pin: the canonical "demo" / "0.1.0" identity passes
5598 // both axes; downstream gates (MissingLib here) take over.
5599 let root = PathBuf::from("/tmp/x");
5600 let manifest = root.join("caixa.lisp");
5601 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5602 let err = layout
5603 .verify(&caixa(CaixaKind::Biblioteca), &root)
5604 .unwrap_err();
5605 assert!(
5606 matches!(err, LayoutError::MissingLib { .. }),
5607 "got {err:?} — valid identity must pass to MissingLib",
5608 );
5609 }
5610
5611 // ── :deps / :deps-dev shape gate (lifted to layout-level verify) ─────
5612 //
5613 // Until this wire-up landed `Caixa::validate_deps` lived as `pub fn`
5614 // on `Caixa` with full per-arm unit coverage in `manifest::tests` +
5615 // `dep::tests` but no production path called it — `feira build`
5616 // silently accepted a malformed `:deps` / `:deps-dev` entry and the
5617 // failure surfaced at lacre-resolve / `git clone` / `cargo metadata`
5618 // / `helm install` time on the *first* downstream consumer to
5619 // strict-parse the value, far from the source `caixa.lisp` and
5620 // without any field naming the offending `:deps` axis. The following
5621 // pins fence the layout-pipeline wire-up: every layout verify on a
5622 // structurally-invalid `:deps` value-shape surfaces the per-axis
5623 // `DepsViolation { caixa, issue }` envelope (peer of
5624 // `NomeViolation` / `VersaoViolation` / `CodePathViolation` /
5625 // `LimitsViolation` / `BehaviorViolation` / `UpgradeViolation` /
5626 // `SupervisorViolation` / `AplicacaoViolation`) before any kind-
5627 // coherence, code-path, or downstream gate sees it.
5628
5629 #[test]
5630 fn deps_violation_on_empty_dep_nome() {
5631 // Empty `:nome` on a `:deps` entry surfaces the narrower
5632 // `DepError::NomeEmpty` arm through the wrap envelope.
5633 use crate::Dep;
5634 let root = PathBuf::from("/tmp/x");
5635 let manifest = root.join("caixa.lisp");
5636 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5637 let mut c = caixa(CaixaKind::Biblioteca);
5638 c.deps = vec![Dep::simple("", "^0.1")];
5639 let err = layout.verify(&c, &root).unwrap_err();
5640 let LayoutError::DepsViolation { caixa, issue } = err else {
5641 panic!("expected LayoutError::DepsViolation, got {err:?}");
5642 };
5643 assert_eq!(caixa, "demo");
5644 assert!(
5645 issue.contains(":deps") && issue.contains(":nome"),
5646 "issue must name the offending slot + axis: {issue}",
5647 );
5648 }
5649
5650 #[test]
5651 fn deps_violation_on_uppercase_dep_nome() {
5652 // Uppercase `:nome` on a `:deps` entry surfaces
5653 // `DepError::NomeInvalid` (DNS-1123 violation) through the wrap.
5654 use crate::Dep;
5655 let root = PathBuf::from("/tmp/x");
5656 let manifest = root.join("caixa.lisp");
5657 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5658 let mut c = caixa(CaixaKind::Biblioteca);
5659 c.deps = vec![Dep::simple("Caixa-Teia", "^0.1")];
5660 let err = layout.verify(&c, &root).unwrap_err();
5661 let LayoutError::DepsViolation { caixa, issue } = err else {
5662 panic!("expected LayoutError::DepsViolation, got {err:?}");
5663 };
5664 assert_eq!(caixa, "demo");
5665 assert!(
5666 issue.contains("Caixa-Teia"),
5667 "issue must quote the offending dep nome verbatim: {issue}",
5668 );
5669 }
5670
5671 #[test]
5672 fn deps_violation_on_unparseable_dep_versao() {
5673 // Unparseable `:versao` requirement on a `:deps` entry surfaces
5674 // `DepError::VersaoInvalid` through the wrap — the canonical
5675 // "the semver::Error reached the resolver, far from the source"
5676 // footgun closed at author time.
5677 use crate::Dep;
5678 let root = PathBuf::from("/tmp/x");
5679 let manifest = root.join("caixa.lisp");
5680 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5681 let mut c = caixa(CaixaKind::Biblioteca);
5682 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
5683 let err = layout.verify(&c, &root).unwrap_err();
5684 let LayoutError::DepsViolation { caixa, issue } = err else {
5685 panic!("expected LayoutError::DepsViolation, got {err:?}");
5686 };
5687 assert_eq!(caixa, "demo");
5688 assert!(
5689 issue.contains("caixa-teia") && issue.contains("not-a-req"),
5690 "issue must quote the dep nome + offending versao: {issue}",
5691 );
5692 }
5693
5694 #[test]
5695 fn deps_violation_on_duplicate_nome_in_deps() {
5696 // Within-list `:deps :nome` duplicate surfaces
5697 // `DepError::DuplicateNome { list: ":deps" }` through the wrap.
5698 use crate::Dep;
5699 let root = PathBuf::from("/tmp/x");
5700 let manifest = root.join("caixa.lisp");
5701 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5702 let mut c = caixa(CaixaKind::Biblioteca);
5703 c.deps = vec![
5704 Dep::simple("caixa-teia", "^0.1"),
5705 Dep::simple("caixa-teia", "^0.2"),
5706 ];
5707 let err = layout.verify(&c, &root).unwrap_err();
5708 let LayoutError::DepsViolation { caixa, issue } = err else {
5709 panic!("expected LayoutError::DepsViolation, got {err:?}");
5710 };
5711 assert_eq!(caixa, "demo");
5712 assert!(
5713 issue.contains("caixa-teia") && issue.contains(":deps"),
5714 "issue must quote the duplicated nome + list: {issue}",
5715 );
5716 }
5717
5718 #[test]
5719 fn deps_violation_on_duplicate_nome_in_deps_dev() {
5720 // Within-list `:deps-dev :nome` duplicate surfaces the same
5721 // diagnostic on the dev-only axis — neither list is a
5722 // second-class citizen of the typed surface.
5723 use crate::Dep;
5724 let root = PathBuf::from("/tmp/x");
5725 let manifest = root.join("caixa.lisp");
5726 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5727 let mut c = caixa(CaixaKind::Biblioteca);
5728 c.deps_dev = vec![
5729 Dep::simple("caixa-teia", "^0.1"),
5730 Dep::simple("caixa-teia", "^0.2"),
5731 ];
5732 let err = layout.verify(&c, &root).unwrap_err();
5733 let LayoutError::DepsViolation { caixa, issue } = err else {
5734 panic!("expected LayoutError::DepsViolation, got {err:?}");
5735 };
5736 assert_eq!(caixa, "demo");
5737 assert!(
5738 issue.contains(":deps-dev"),
5739 "issue must name the offending list: {issue}",
5740 );
5741 }
5742
5743 #[test]
5744 fn deps_violation_in_deps_fires_before_deps_dev() {
5745 // Precedence pin: when *both* `:deps` and `:deps-dev` carry a
5746 // malformed entry, the `:deps` walk fires first — the canonical
5747 // declaration-order precedence `Caixa::validate_deps` establishes
5748 // (the same author-grep ordering the typed-graph peers use on
5749 // every other Vec-shaped surface).
5750 use crate::Dep;
5751 let root = PathBuf::from("/tmp/x");
5752 let manifest = root.join("caixa.lisp");
5753 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5754 let mut c = caixa(CaixaKind::Biblioteca);
5755 c.deps = vec![Dep::simple("Bad-In-Deps", "^0.1")];
5756 c.deps_dev = vec![Dep::simple("Bad-In-Deps-Dev", "^0.1")];
5757 let err = layout.verify(&c, &root).unwrap_err();
5758 let LayoutError::DepsViolation { caixa: _, issue } = err else {
5759 panic!("expected LayoutError::DepsViolation, got {err:?}");
5760 };
5761 assert!(
5762 issue.contains("Bad-In-Deps") && !issue.contains("Bad-In-Deps-Dev"),
5763 "issue must name the :deps offender, not :deps-dev: {issue}",
5764 );
5765 }
5766
5767 #[test]
5768 fn deps_violation_fires_after_versao_violation() {
5769 // Precedence pin: when both the top-level `:versao` and a `:deps`
5770 // entry are malformed, the Caixa-identity gate fires first — the
5771 // canonical declaration order on `Caixa` (`:nome` → `:versao` →
5772 // ... → `:deps`) and the same identity-axis-dominates-content-
5773 // axis discipline the peer `validate_nome` / `validate_versao`
5774 // wire-up established (1f74a5f). A malformed `:versao` would
5775 // otherwise quote `caixa.nome` against a downstream-shaped
5776 // diagnostic.
5777 use crate::Dep;
5778 let root = PathBuf::from("/tmp/x");
5779 let manifest = root.join("caixa.lisp");
5780 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5781 let mut c = caixa(CaixaKind::Biblioteca);
5782 c.versao = "v0.1.0".into();
5783 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5784 let err = layout.verify(&c, &root).unwrap_err();
5785 assert!(
5786 matches!(err, LayoutError::VersaoViolation { .. }),
5787 "got {err:?} — versao must fire before DepsViolation",
5788 );
5789 }
5790
5791 #[test]
5792 fn deps_violation_fires_before_kind_coherence() {
5793 // Precedence pin: a Supervisor with a malformed `:deps` entry
5794 // AND declared `:bibliotecas` (the canonical SupervisorOwnsCode
5795 // shape) surfaces DepsViolation, not SupervisorOwnsCode — the
5796 // dep surface is universal across all kinds and its shape gate
5797 // is more fundamental than the kind-coherence partitions on
5798 // `:bibliotecas` / `:exe` / `:servicos`. The author can fix the
5799 // dep typo without first being told to move their `:bibliotecas`
5800 // off a Supervisor.
5801 use crate::Dep;
5802 let root = PathBuf::from("/tmp/x");
5803 let manifest = root.join("caixa.lisp");
5804 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5805 let mut c = caixa(CaixaKind::Supervisor);
5806 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5807 c.bibliotecas = vec!["lib/x.lisp".into()];
5808 let err = layout.verify(&c, &root).unwrap_err();
5809 assert!(
5810 matches!(err, LayoutError::DepsViolation { .. }),
5811 "got {err:?} — DepsViolation must fire before SupervisorOwnsCode",
5812 );
5813 }
5814
5815 #[test]
5816 fn deps_violation_fires_after_missing_manifest() {
5817 // Precedence pin: `MissingManifest` still dominates — there's no
5818 // caixa to deps-check when the manifest is missing.
5819 use crate::Dep;
5820 let root = PathBuf::from("/tmp/x");
5821 let layout = StandardLayout::new().with_path_exists(|_| false);
5822 let mut c = caixa(CaixaKind::Biblioteca);
5823 c.deps = vec![Dep::simple("Bad-Dep", "^0.1")];
5824 let err = layout.verify(&c, &root).unwrap_err();
5825 assert!(
5826 matches!(err, LayoutError::MissingManifest(_)),
5827 "got {err:?} — MissingManifest must dominate the deps gate",
5828 );
5829 }
5830
5831 #[test]
5832 fn deps_violation_on_self_dep_in_deps() {
5833 // Cross-slot self-edge: a caixa whose `:deps` lists its own
5834 // `:nome` is rejected at the layout wire-up, the diagnostic
5835 // surfaces through the `DepsViolation` envelope with both the
5836 // offending list tag (`":deps"`) and the parent's `:nome`
5837 // verbatim. Until this wire-up landed the self-dep silently
5838 // passed `feira build` and the resolver's lacre-pipeline
5839 // closure walk either rejected mid-traversal (infinite
5840 // recursion detected far from the source caixa.lisp) or, on
5841 // the unbounded path, recursed until it exhausted its stack.
5842 // Mirrors the supervision-tree
5843 // [`supervisor_violation_on_self_supervision`] and the
5844 // Aplicacao-membership self-edge wire-up tests on the peer
5845 // typed-name-graph axes.
5846 use crate::Dep;
5847 let root = PathBuf::from("/tmp/x");
5848 let manifest = root.join("caixa.lisp");
5849 let default_lib = root.join("lib").join("demo.lisp");
5850 let layout =
5851 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5852 let mut c = caixa(CaixaKind::Biblioteca);
5853 c.deps = vec![Dep::simple("demo", "^0.1")];
5854 let err = layout.verify(&c, &root).unwrap_err();
5855 let LayoutError::DepsViolation { caixa, issue } = err else {
5856 panic!("expected LayoutError::DepsViolation, got {err:?}");
5857 };
5858 assert_eq!(caixa, "demo");
5859 assert!(
5860 issue.contains(":deps") && issue.contains("demo"),
5861 "issue must name the offending list + parent :nome: {issue}",
5862 );
5863 }
5864
5865 #[test]
5866 fn deps_violation_on_self_dep_in_deps_dev() {
5867 // Same cross-slot self-edge gate on the `:deps-dev` axis —
5868 // neither dep list is a second-class citizen of the typed
5869 // surface. The diagnostic names `:deps-dev` so the author can
5870 // grep their caixa.lisp for the offending block directly.
5871 use crate::Dep;
5872 let root = PathBuf::from("/tmp/x");
5873 let manifest = root.join("caixa.lisp");
5874 let default_lib = root.join("lib").join("demo.lisp");
5875 let layout =
5876 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
5877 let mut c = caixa(CaixaKind::Biblioteca);
5878 c.deps_dev = vec![Dep::simple("demo", "^0.1")];
5879 let err = layout.verify(&c, &root).unwrap_err();
5880 let LayoutError::DepsViolation { caixa, issue } = err else {
5881 panic!("expected LayoutError::DepsViolation, got {err:?}");
5882 };
5883 assert_eq!(caixa, "demo");
5884 assert!(
5885 issue.contains(":deps-dev"),
5886 "issue must name the offending list: {issue}",
5887 );
5888 }
5889
5890 #[test]
5891 fn self_dep_fires_after_per_entry_dep_shape() {
5892 // Precedence pin: the per-entry shape gates of
5893 // [`Caixa::validate_deps`] (DNS-1123 / SemVer / fonte / etc.)
5894 // fire first on a self-dep entry whose `:nome` is malformed.
5895 // Same ordering posture every peer cross-slot gate uses
5896 // (`validate_no_self_supervision` after `SupervisorSpec::validate`,
5897 // `validate_no_self_membership` after `AplicacaoSpec::validate`).
5898 // A malformed self-dep `:nome` surfaces the narrower
5899 // per-entry diagnostic (which already names the parser-side
5900 // reason) before the self-edge gate sees the entry.
5901 use crate::Dep;
5902 let root = PathBuf::from("/tmp/x");
5903 let manifest = root.join("caixa.lisp");
5904 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5905 let mut c = caixa(CaixaKind::Biblioteca);
5906 // The parent is "demo" (DNS-1123 valid); the dep is "DEMO"
5907 // (DNS-1123 invalid). The per-entry shape gate fires on the
5908 // upper-case nome, masking the self-edge gate (and that's the
5909 // canonical precedence — fix the dep shape first, then the
5910 // structural self-edge becomes the next live diagnostic).
5911 c.deps = vec![Dep::simple("DEMO", "^0.1")];
5912 let err = layout.verify(&c, &root).unwrap_err();
5913 let LayoutError::DepsViolation { caixa: _, issue } = err else {
5914 panic!("expected LayoutError::DepsViolation, got {err:?}");
5915 };
5916 assert!(
5917 issue.contains("DNS-1123"),
5918 "issue must be the per-entry shape diagnostic, not the self-edge gate: {issue}",
5919 );
5920 }
5921
5922 #[test]
5923 fn valid_deps_pass_to_downstream_gates() {
5924 // Positive control pin: the canonical authoring shape (one
5925 // `:deps` entry naming a DNS-1123 nome + Cargo-shaped requirement,
5926 // one `:deps-dev` entry on a distinct nome) passes the dep gate;
5927 // downstream gates (MissingLib here) take over. Drift here =
5928 // a future tighten that rejects any canonical shape surfaces as
5929 // a regression at this layout-level pin.
5930 use crate::Dep;
5931 let root = PathBuf::from("/tmp/x");
5932 let manifest = root.join("caixa.lisp");
5933 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5934 let mut c = caixa(CaixaKind::Biblioteca);
5935 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
5936 c.deps_dev = vec![Dep::simple("caixa-lint", "^0.2")];
5937 let err = layout.verify(&c, &root).unwrap_err();
5938 assert!(
5939 matches!(err, LayoutError::MissingLib { .. }),
5940 "got {err:?} — valid deps must pass to MissingLib",
5941 );
5942 }
5943
5944 #[test]
5945 fn code_path_gate_runs_after_foreign_code_slot_gate() {
5946 // Precedence pin: a Servico that declares `:exe` (foreign code
5947 // surface) surfaces ForeignCodeSlot, *not* a per-entry path
5948 // shape diagnostic, even when the `:exe` entry is itself
5949 // malformed. The kind-coherence gate is the load-bearing
5950 // diagnostic at this site — once the slot is moved off the
5951 // wrong kind, the per-entry shape gate becomes the next live
5952 // diagnostic.
5953 let root = PathBuf::from("/tmp/x");
5954 let manifest = root.join("caixa.lisp");
5955 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
5956 let mut c = caixa(CaixaKind::Servico);
5957 c.servicos = vec!["servicos/ok.yaml".into()];
5958 c.exe = vec!["/etc/foreign".into()];
5959 let err = layout.verify(&c, &root).unwrap_err();
5960 assert!(
5961 matches!(err, LayoutError::ForeignCodeSlot { .. }),
5962 "expected ForeignCodeSlot (kind-coherence wins over per-entry shape), got {err:?}",
5963 );
5964 }
5965
5966 // ── M2 typed-substrate invariants ────────────────────────────────────
5967
5968 #[test]
5969 fn behavior_callback_path_must_exist() {
5970 use crate::BehaviorSpec;
5971 use std::path::PathBuf;
5972 let root = PathBuf::from("/tmp/x");
5973 let manifest = root.join("caixa.lisp");
5974 let mut c = caixa(CaixaKind::Servico);
5975 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
5976 let svc = root.join("servicos/demo.computeunit.yaml");
5977 c.behavior = Some(BehaviorSpec {
5978 on_init: Some(PathBuf::from("lib/init.lisp")),
5979 ..Default::default()
5980 });
5981 let manifest_clone = manifest.clone();
5982 let svc_clone = svc.clone();
5983 let layout =
5984 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
5985 let err = layout.verify(&c, &root).unwrap_err();
5986 assert!(matches!(
5987 err,
5988 LayoutError::MissingEntry { kind, .. }
5989 if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK
5990 ));
5991
5992 // Now declare the path exists — passes.
5993 let init = root.join("lib/init.lisp");
5994 let layout =
5995 StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
5996 layout.verify(&c, &root).unwrap();
5997 }
5998
5999 #[test]
6000 fn behavior_absolute_callback_is_violation_not_missing() {
6001 // An absolute path silently subverts `root.join(p)` (Path::join
6002 // replaces the base when the right side is absolute). Before
6003 // BehaviorSpec::validate ran, an `:on-init "/etc/passwd"` would
6004 // surface as a confusing "missing behavior-callback /etc/passwd"
6005 // — or, worse, pass when /etc/passwd happens to exist. Now it's
6006 // a value-shape error naming the slot.
6007 use crate::BehaviorSpec;
6008 let root = PathBuf::from("/tmp/x");
6009 let manifest = root.join("caixa.lisp");
6010 let svc = root.join("servicos/demo.computeunit.yaml");
6011 let mut c = caixa(CaixaKind::Servico);
6012 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6013 c.behavior = Some(BehaviorSpec {
6014 on_init: Some(PathBuf::from("/etc/passwd")),
6015 ..Default::default()
6016 });
6017 // Path exists check would *succeed* on /etc/passwd (proving the
6018 // sandbox bypass) — value-shape pass must fire first.
6019 let layout = StandardLayout::new()
6020 .with_path_exists(move |p| p == manifest || p == svc || p == Path::new("/etc/passwd"));
6021 let err = layout.verify(&c, &root).unwrap_err();
6022 assert!(
6023 matches!(err, LayoutError::BehaviorViolation { ref caixa, .. } if caixa == "demo"),
6024 "got {err:?}",
6025 );
6026 }
6027
6028 #[test]
6029 fn behavior_empty_callback_is_violation() {
6030 use crate::BehaviorSpec;
6031 let root = PathBuf::from("/tmp/x");
6032 let manifest = root.join("caixa.lisp");
6033 let svc = root.join("servicos/demo.computeunit.yaml");
6034 let mut c = caixa(CaixaKind::Servico);
6035 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6036 c.behavior = Some(BehaviorSpec {
6037 on_call: Some(PathBuf::new()),
6038 ..Default::default()
6039 });
6040 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6041 let err = layout.verify(&c, &root).unwrap_err();
6042 assert!(matches!(err, LayoutError::BehaviorViolation { .. }));
6043 }
6044
6045 #[test]
6046 fn upgrade_from_duplicate_surfaces_as_upgrade_violation() {
6047 // Wiring pin: the cross-entry duplicate-`:from` gate in
6048 // `validate_upgrade_from` lands on the same
6049 // `LayoutError::UpgradeViolation` axis the per-entry
6050 // `UpgradeFromEntry::validate` already does (26da2c7), so a
6051 // caixa.lisp with two `(:from "0.1.0" …)` blocks surfaces at
6052 // `feira build` time naming the offending caixa rather than
6053 // silently passing into the wasm-operator's non-deterministic
6054 // dispatch. Mirrors `behavior_empty_callback_is_violation` on
6055 // the peer M2 typed slot.
6056 use crate::{UpgradeFromEntry, UpgradeInstruction};
6057 let root = PathBuf::from("/tmp/x");
6058 let manifest = root.join("caixa.lisp");
6059 let svc = root.join("servicos/demo.computeunit.yaml");
6060 let mut c = caixa(CaixaKind::Servico);
6061 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6062 c.upgrade_from = vec![
6063 UpgradeFromEntry {
6064 from: "0.1.0".into(),
6065 instructions: vec![UpgradeInstruction::Restart],
6066 },
6067 UpgradeFromEntry {
6068 from: "0.1.0".into(),
6069 instructions: vec![UpgradeInstruction::Restart],
6070 },
6071 ];
6072 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6073 let err = layout.verify(&c, &root).unwrap_err();
6074 let LayoutError::UpgradeViolation { caixa, issue } = err else {
6075 panic!("expected LayoutError::UpgradeViolation for duplicate `:from`, got {err:?}");
6076 };
6077 assert_eq!(caixa, "demo");
6078 assert!(
6079 issue.contains("0.1.0"),
6080 "UpgradeViolation issue must name the offending `:from` verbatim, got {issue:?}"
6081 );
6082 }
6083
6084 #[test]
6085 fn upgrade_from_downgrade_surfaces_as_upgrade_violation() {
6086 // Wiring pin: the cross-slot precedence gate in
6087 // `validate_upgrade_from_against_versao` lands on the same
6088 // `LayoutError::UpgradeViolation` axis the per-entry and
6089 // cross-entry gates already do (26da2c7, 7c6aef2), so a
6090 // caixa.lisp whose `:upgrade-from :from` is greater than the
6091 // caixa's own `:versao` surfaces at `feira build` time
6092 // naming the offending caixa rather than silently passing
6093 // into the wasm-operator's `:from`-match dispatch where the
6094 // entry would sit dormant forever. Mirrors
6095 // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
6096 // the peer cross-entry gate.
6097 use crate::{UpgradeFromEntry, UpgradeInstruction};
6098 let root = PathBuf::from("/tmp/x");
6099 let manifest = root.join("caixa.lisp");
6100 let svc = root.join("servicos/demo.computeunit.yaml");
6101 let mut c = caixa(CaixaKind::Servico);
6102 c.versao = "0.1.5".into();
6103 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6104 c.upgrade_from = vec![UpgradeFromEntry {
6105 from: "0.2.0".into(),
6106 instructions: vec![UpgradeInstruction::Restart],
6107 }];
6108 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6109 let err = layout.verify(&c, &root).unwrap_err();
6110 let LayoutError::UpgradeViolation { caixa, issue } = err else {
6111 panic!(
6112 "expected LayoutError::UpgradeViolation for downgrade-shaped `:from`, got {err:?}"
6113 );
6114 };
6115 assert_eq!(caixa, "demo");
6116 assert!(
6117 issue.contains("0.2.0") && issue.contains("0.1.5"),
6118 "UpgradeViolation issue must name both `:from` and `:versao` verbatim, got {issue:?}"
6119 );
6120 }
6121
6122 #[test]
6123 fn upgrade_from_equal_to_versao_surfaces_as_upgrade_violation() {
6124 // Self-upgrade no-op arm: `:from "0.1.0"` while
6125 // `:versao "0.1.0"` declares "upgrade from myself to
6126 // myself", which the operator's dispatch either skips
6127 // silently or trivially "succeeds" with no observable
6128 // transition. Surfaces at validate time naming both values
6129 // so the author can fix in one edit.
6130 use crate::{UpgradeFromEntry, UpgradeInstruction};
6131 let root = PathBuf::from("/tmp/x");
6132 let manifest = root.join("caixa.lisp");
6133 let svc = root.join("servicos/demo.computeunit.yaml");
6134 let mut c = caixa(CaixaKind::Servico);
6135 c.versao = "0.1.0".into();
6136 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6137 c.upgrade_from = vec![UpgradeFromEntry {
6138 from: "0.1.0".into(),
6139 instructions: vec![UpgradeInstruction::Restart],
6140 }];
6141 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6142 let err = layout.verify(&c, &root).unwrap_err();
6143 let LayoutError::UpgradeViolation { caixa, issue } = err else {
6144 panic!(
6145 "expected LayoutError::UpgradeViolation for self-upgrade `:from == :versao`, got \
6146 {err:?}"
6147 );
6148 };
6149 assert_eq!(caixa, "demo");
6150 assert!(
6151 issue.contains("0.1.0"),
6152 "UpgradeViolation issue must name the equal `:from`/`:versao` verbatim, got {issue:?}"
6153 );
6154 }
6155
6156 #[test]
6157 fn upgrade_from_strict_upgrade_passes_layout() {
6158 // Positive control for the precedence gate at the
6159 // LayoutInvariants level: a valid `:from < :versao` chain
6160 // (`0.1.0 → 0.2.0`) must not regress into a false-positive
6161 // `UpgradeViolation`. Mirrors `behavior_callback_path_must_exist`'s
6162 // positive-control arm.
6163 use crate::{UpgradeFromEntry, UpgradeInstruction};
6164 let root = PathBuf::from("/tmp/x");
6165 let manifest = root.join("caixa.lisp");
6166 let svc = root.join("servicos/demo.computeunit.yaml");
6167 let mut c = caixa(CaixaKind::Servico);
6168 c.versao = "0.2.0".into();
6169 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6170 c.upgrade_from = vec![UpgradeFromEntry {
6171 from: "0.1.0".into(),
6172 instructions: vec![UpgradeInstruction::Restart],
6173 }];
6174 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
6175 layout.verify(&c, &root).unwrap();
6176 }
6177
6178 #[test]
6179 fn upgrade_script_path_must_exist() {
6180 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6181 use std::path::PathBuf;
6182 let root = PathBuf::from("/tmp/x");
6183 let manifest = root.join("caixa.lisp");
6184 let svc = root.join("servicos/demo.computeunit.yaml");
6185 let on_state_change = root.join("lib/migrations.lisp");
6186 let mut c = caixa(CaixaKind::Servico);
6187 // `:versao` past the entry's `:from` so the cross-slot
6188 // precedence gate (`FromNotBeforeVersao`) lets this case
6189 // through to the path-existence pass under test.
6190 c.versao = "0.2.0".into();
6191 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6192 // `:on-state-change` declared so the cross-slot composition
6193 // gate (`validate_upgrade_from_against_behavior`) lets the
6194 // `:state-change` entry through to the path-existence pass
6195 // under test. Without the callback the missing-callback gate
6196 // would surface first and the path-existence pass wouldn't be
6197 // exercised.
6198 c.behavior = Some(BehaviorSpec {
6199 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
6200 ..Default::default()
6201 });
6202 // A `:load-module` precedes the `:state-change` so the entry
6203 // satisfies the within-entry state-change-ordering gate
6204 // (`StateChangeWithoutPriorLoad`) and the path-existence pass
6205 // under test is the gate actually exercised. `:load-module`
6206 // carries no on-disk path, so it adds no existence requirement.
6207 c.upgrade_from = vec![UpgradeFromEntry {
6208 from: "0.1.0".into(),
6209 instructions: vec![
6210 UpgradeInstruction::LoadModule {
6211 module: "demo".into(),
6212 },
6213 UpgradeInstruction::StateChange {
6214 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6215 },
6216 ],
6217 }];
6218 let manifest_clone = manifest.clone();
6219 let svc_clone = svc.clone();
6220 let on_state_change_clone = on_state_change.clone();
6221 let layout = StandardLayout::new().with_path_exists(move |p| {
6222 p == manifest_clone || p == svc_clone || p == on_state_change_clone
6223 });
6224 let err = layout.verify(&c, &root).unwrap_err();
6225 assert!(matches!(
6226 err,
6227 LayoutError::MissingEntry { kind, .. }
6228 if kind == crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT
6229 ));
6230 }
6231
6232 #[test]
6233 fn layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels() {
6234 // Byte-identity pin: the two per-M2-slot leaf-kind labels the
6235 // [`LayoutError::MissingEntry`] `kind: &'static str`
6236 // discriminator surfaces under (the M2 `:behavior` per-callback
6237 // on-disk-leaf axis, the M2 `:upgrade-from :instructions`
6238 // per-`:state-change` script-path on-disk-leaf axis) route
6239 // through the lifted [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6240 // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6241 // consts, so a future rebrand that reaches the const but not
6242 // the production emit / test probe (or vice versa) surfaces
6243 // here at build time rather than at runtime as a downstream
6244 // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6245 // diagnostic mismatch far from the rename's commit. Mirror of
6246 // the peer
6247 // [`crate::aplicacao::tests::contrato_author_key_consts_pin_canonical_kebab_case_labels`]
6248 // (f50c875) and
6249 // [`crate::upgrade::tests::upgrade_instruction_kind_consts_pin_canonical_kebab_case_tags`]
6250 // (56120ef) byte-identity pins on the sibling M3 `:contratos`
6251 // per-entry endpoint-label + M2 `:upgrade-from :instructions`
6252 // per-variant kind-tag axes.
6253 assert_eq!(
6254 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6255 "behavior-callback"
6256 );
6257 assert_eq!(
6258 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6259 "upgrade-script"
6260 );
6261 }
6262
6263 #[test]
6264 fn layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels() {
6265 // Byte-identity pin: the three per-M0-code-slot leaf-kind
6266 // labels the [`LayoutError::MissingEntry`] `kind: &'static
6267 // str` discriminator surfaces under (the `:bibliotecas`
6268 // per-entry axis, the `:exe` per-entry axis, the `:servicos`
6269 // per-entry axis) route through the lifted
6270 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6271 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6272 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] consts,
6273 // so a future rebrand that reaches the const but not the
6274 // production emit (or vice versa) surfaces here at build time
6275 // rather than at runtime as a downstream
6276 // [`LayoutError::MissingEntry`] `kind: <stale-label>`
6277 // diagnostic mismatch far from the rename's commit. Mirror of
6278 // the peer M2-tier pin
6279 // [`layout_missing_entry_kind_m2_consts_pin_canonical_kebab_case_labels`]
6280 // (95c9c4c) on the sibling `:behavior` / `:upgrade-from`
6281 // per-slot leaf-kind axes.
6282 assert_eq!(
6283 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6284 "biblioteca"
6285 );
6286 assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE, "exe");
6287 assert_eq!(crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO, "servico");
6288 }
6289
6290 #[test]
6291 fn layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str() {
6292 // Cross-axis byte-identity pin: the two `:kind`-namesake M0
6293 // leaf-kind labels ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6294 // = `"biblioteca"`,
6295 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] =
6296 // `"servico"`) must equal [`crate::CaixaKind::Biblioteca`] /
6297 // [`crate::CaixaKind::Servico`]'s
6298 // [`crate::CaixaKind::as_str`] outputs verbatim — the
6299 // substrate's canonical human-readable-kind axis and the
6300 // layout diagnostic's per-slot leaf-kind axis share one
6301 // vocabulary for these two arms by design (both label the
6302 // caixa's code-producing shape by its Portuguese-native
6303 // idiom), so drift between the two lands as a build-time
6304 // pattern-arm miss here rather than as a runtime diagnostic
6305 // that reads inconsistently across `feira build`'s
6306 // per-invocation output.
6307 //
6308 // The third M0 arm ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]
6309 // = `"exe"`) is deliberately *distinct* from
6310 // [`crate::CaixaKind::Binario`]'s [`crate::CaixaKind::as_str`]
6311 // output (`"binario"`) — the `:exe` code slot names the
6312 // per-directory leaf-kind at the `exe/` subtree, whereas
6313 // [`crate::CaixaKind::Binario`] names the caixa's own runtime
6314 // kind. Two axes, two labels — the inequality assertion here
6315 // pins the split so a future accidental collapse of the two
6316 // onto one scalar (a rebrand that reroutes either axis to
6317 // match the other) trips at build time.
6318 assert_eq!(
6319 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6320 CaixaKind::Biblioteca.as_str()
6321 );
6322 assert_eq!(
6323 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6324 CaixaKind::Servico.as_str()
6325 );
6326 assert_ne!(
6327 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6328 CaixaKind::Binario.as_str(),
6329 "`exe` leaf-kind label names the per-directory code-slot \
6330 axis; `binario` names the caixa-kind axis — the two must \
6331 not silently collapse onto one scalar"
6332 );
6333 }
6334
6335 #[test]
6336 fn layout_missing_entry_kind_consts_are_pairwise_distinct() {
6337 // Distinctness pin: the five [`LayoutError::MissingEntry`]
6338 // `kind: &'static str` accept-set members
6339 // ([`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`] /
6340 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] /
6341 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`] on the
6342 // M0 code-slot arms plus
6343 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]
6344 // / [`crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT`]
6345 // on the M2 slot arms) must be pairwise distinct — an
6346 // accidental copy-paste flip that reroutes one label's byte-
6347 // string to also match another silently collapses two
6348 // per-slot diagnostics onto one, so an operator running
6349 // `feira build` reads `kind: "biblioteca"` for what should
6350 // have surfaced as a `:behavior :on-init` script-not-found
6351 // diagnostic (or vice versa). This pin catches any such
6352 // flip at build time. Mirror of the peer
6353 // [`crate::render::tests::m2_limits_key_consts_are_pairwise_distinct`]
6354 // / peer distinctness pins on other closed-set typed axes.
6355 let entries: &[(&str, &str)] = &[
6356 (
6357 "LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA",
6358 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6359 ),
6360 (
6361 "LAYOUT_MISSING_ENTRY_KIND_EXE",
6362 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6363 ),
6364 (
6365 "LAYOUT_MISSING_ENTRY_KIND_SERVICO",
6366 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6367 ),
6368 (
6369 "LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK",
6370 crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK,
6371 ),
6372 (
6373 "LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT",
6374 crate::render::LAYOUT_MISSING_ENTRY_KIND_UPGRADE_SCRIPT,
6375 ),
6376 ];
6377 for (i, (name_a, value_a)) in entries.iter().enumerate() {
6378 for (name_b, value_b) in entries.iter().skip(i + 1) {
6379 assert_ne!(
6380 value_a, value_b,
6381 "LAYOUT_MISSING_ENTRY_KIND_* consts must be \
6382 pairwise-distinct byte-strings — {name_a} and \
6383 {name_b} both resolve to {value_a:?}"
6384 );
6385 }
6386 }
6387 }
6388
6389 #[test]
6390 fn layout_dir_consts_pin_canonical_directory_names() {
6391 // Scalar-value pin for the three [`crate::render::LAYOUT_DIR_*`]
6392 // consts naming the CSE-invariant per-[`CaixaKind`]
6393 // on-disk-directory-name axes the substrate's layout invariants
6394 // pin (`lib/` for [`CaixaKind::Biblioteca`], `exe/` for
6395 // [`CaixaKind::Binario`], `servicos/` for [`CaixaKind::Servico`]).
6396 // A future rebrand of any of the three on-disk directory landing
6397 // conventions must reach this pin — the const-edit lands on one
6398 // arm, the assertion here re-pins the new byte-string, and every
6399 // downstream consumer (the caixa-feira `init` / `fmt` / `lint` /
6400 // `tofu` scaffolders, the [`crate::LayoutInvariants::verify`]
6401 // sandbox reconstruction, the future
6402 // `feira app deploy`-cluster scaffolder) picks up the new
6403 // directory name at build time. Mirror of the peer
6404 // [`layout_missing_entry_kind_m0_consts_pin_canonical_code_slot_labels`]
6405 // (fe2a898) on the sibling
6406 // [`crate::LayoutError::MissingEntry`] `kind:` discriminator
6407 // axis this on-disk-directory axis composes with.
6408 assert_eq!(crate::render::LAYOUT_DIR_LIB, "lib");
6409 assert_eq!(crate::render::LAYOUT_DIR_EXE, "exe");
6410 assert_eq!(crate::render::LAYOUT_DIR_SERVICOS, "servicos");
6411 }
6412
6413 #[test]
6414 fn layout_dir_consts_are_pairwise_distinct() {
6415 // Distinctness pin: the three per-[`CaixaKind`]
6416 // on-disk-directory-name arms must resolve to pairwise-distinct
6417 // byte-strings — a future accidental copy-paste flip that
6418 // reroutes any one of the three onto another's value silently
6419 // collapses two per-kind on-disk sandboxes onto one, so
6420 // [`crate::LayoutInvariants::verify`] would gate a
6421 // [`CaixaKind::Binario`] caixa's `:exe` entries against the
6422 // wrong sub-tree (or a `:kind Servico` caixa's `:servicos`
6423 // entries against `lib/` and pass every entry `feira build`
6424 // should have rejected as [`crate::LayoutError::ServicoOutsideDir`]).
6425 // Mirror of the peer
6426 // [`layout_missing_entry_kind_consts_are_pairwise_distinct`]
6427 // (fe2a898) on the sibling leaf-kind label accept-set.
6428 let entries: &[(&str, &str)] = &[
6429 ("LAYOUT_DIR_LIB", crate::render::LAYOUT_DIR_LIB),
6430 ("LAYOUT_DIR_EXE", crate::render::LAYOUT_DIR_EXE),
6431 ("LAYOUT_DIR_SERVICOS", crate::render::LAYOUT_DIR_SERVICOS),
6432 ];
6433 for (i, (name_a, value_a)) in entries.iter().enumerate() {
6434 for (name_b, value_b) in entries.iter().skip(i + 1) {
6435 assert_ne!(
6436 value_a, value_b,
6437 "LAYOUT_DIR_* consts must be pairwise-distinct \
6438 byte-strings — {name_a} and {name_b} both resolve \
6439 to {value_a:?}"
6440 );
6441 }
6442 }
6443 }
6444
6445 #[test]
6446 fn layout_dir_exe_matches_layout_missing_entry_kind_exe() {
6447 // Cross-axis byte-identity pin: [`crate::render::LAYOUT_DIR_EXE`]
6448 // (the on-disk-directory-name arm) equals
6449 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`] (the
6450 // [`LayoutError::MissingEntry`] `kind:` leaf-kind categorization
6451 // arm) verbatim — the M0 `:kind Binario` on-disk-directory axis
6452 // and the [`crate::LayoutError::MissingEntry`] `kind:` leaf-kind
6453 // discriminator name the same three-byte sub-tree (`exe/`), a
6454 // coincidence [`crate::LayoutInvariants::verify`] itself relies
6455 // on: it joins `root` with [`crate::render::LAYOUT_DIR_EXE`] to
6456 // reconstruct `exe_dir` and emits [`crate::LayoutError::MissingEntry
6457 // { kind: LAYOUT_MISSING_ENTRY_KIND_EXE, path: <under exe_dir> }`]
6458 // for every non-resolving entry. Making the coincidence
6459 // load-bearing means a future rebrand touching either axis
6460 // without the other (a per-consumer disambiguation collapsing
6461 // the leaf-kind label onto `"binary"` while the directory stays
6462 // `"exe"`, or vice versa) trips at caixa-core build time rather
6463 // than surfacing at runtime as a mismatched
6464 // [`crate::LayoutInvariants::verify`] diagnostic whose `kind:`
6465 // reads one label while the `path:` sits under a differently-named
6466 // sub-tree.
6467 assert_eq!(
6468 crate::render::LAYOUT_DIR_EXE,
6469 crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE,
6470 "LAYOUT_DIR_EXE must equal LAYOUT_MISSING_ENTRY_KIND_EXE — \
6471 both name the M0 `:kind Binario` sub-tree by the same \
6472 three-byte scalar"
6473 );
6474 }
6475
6476 #[test]
6477 fn layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib() {
6478 // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_LIB`]
6479 // (`"lib"`, the Cargo-style abbreviated on-disk directory name)
6480 // is *deliberately* distinct from
6481 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA`]
6482 // (`"biblioteca"`, the full-form Portuguese-native leaf-kind
6483 // label) — the substrate splits the on-disk convention terse
6484 // (`lib/`) from the diagnostic vocabulary full (`biblioteca`),
6485 // matching Cargo's `src/lib.rs` abbreviation of the `library`
6486 // crate-type discriminator. A future accidental collapse of the
6487 // two axes onto one scalar (a rebrand aligning either arm with
6488 // the other for schema-clarity, an English-uniformity pass that
6489 // renames `LAYOUT_DIR_LIB` to `LAYOUT_DIR_BIBLIOTECA` or the
6490 // diagnostic label to `"lib"`) would silently reroute either
6491 // consumer onto the other's byte-string. This pin catches the
6492 // collapse at build time. Peer of the sibling
6493 // [`layout_missing_entry_kind_m0_consts_align_with_caixa_kind_as_str`]
6494 // (fe2a898) that pins the analogous *equality* between the M0
6495 // `:kind Biblioteca` diagnostic-label arm and
6496 // [`crate::CaixaKind::Biblioteca`]'s [`crate::CaixaKind::as_str`]
6497 // output — the two pins jointly encode the "which of the three
6498 // Biblioteca-related scalars are load-bearing-equal, which are
6499 // load-bearing-distinct" invariant across the substrate's
6500 // per-kind vocabulary.
6501 assert_ne!(
6502 crate::render::LAYOUT_DIR_LIB,
6503 crate::render::LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA,
6504 "LAYOUT_DIR_LIB (`\"lib\"`) and LAYOUT_MISSING_ENTRY_KIND_BIBLIOTECA \
6505 (`\"biblioteca\"`) name two distinct axes — the on-disk \
6506 directory convention (Cargo-style abbreviated) and the \
6507 layout-diagnostic leaf-kind label (full-form Portuguese) — \
6508 and must not silently collapse onto one scalar"
6509 );
6510 }
6511
6512 #[test]
6513 fn layout_dir_servicos_is_distinct_from_layout_missing_entry_kind_servico() {
6514 // Cross-axis distinctness pin: [`crate::render::LAYOUT_DIR_SERVICOS`]
6515 // (`"servicos"`, the Portuguese-*plural* on-disk directory
6516 // name) is *deliberately* distinct from
6517 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO`]
6518 // (`"servico"`, the singular leaf-kind label) — the on-disk
6519 // sub-tree houses one-or-more ComputeUnit YAML descriptors per
6520 // caixa (hence the plural), the diagnostic label names the
6521 // caixa's own kind (singular). A future accidental collapse
6522 // onto one scalar (a per-consumer disambiguation aligning the
6523 // two, a hypothetical English-uniformity pass renaming
6524 // `"servicos"` → `"services"` while retaining `"servico"` on
6525 // the diagnostic arm — or vice versa) would silently reroute
6526 // either consumer onto the other's byte-string. Peer of the
6527 // sibling [`layout_dir_bib_is_distinct_from_layout_missing_entry_kind_bib`]
6528 // pin on the M0 `:kind Biblioteca` split axis; two of the three
6529 // per-kind on-disk / leaf-kind splits carry a distinctness
6530 // pin here, the third ([`crate::render::LAYOUT_DIR_EXE`] vs
6531 // [`crate::render::LAYOUT_MISSING_ENTRY_KIND_EXE`]) carries an
6532 // equality pin under
6533 // [`layout_dir_exe_matches_layout_missing_entry_kind_exe`].
6534 assert_ne!(
6535 crate::render::LAYOUT_DIR_SERVICOS,
6536 crate::render::LAYOUT_MISSING_ENTRY_KIND_SERVICO,
6537 "LAYOUT_DIR_SERVICOS (`\"servicos\"`, plural on-disk sub-tree) \
6538 and LAYOUT_MISSING_ENTRY_KIND_SERVICO (`\"servico\"`, singular \
6539 leaf-kind label) name two distinct axes and must not silently \
6540 collapse onto one scalar"
6541 );
6542 }
6543
6544 #[test]
6545 fn layout_invariants_reconstruct_sandbox_roots_through_lifted_layout_dir_consts() {
6546 // Production-through-const pin: [`LayoutInvariants::verify`]
6547 // routes its three per-kind sandbox-root joins
6548 // (`root.join(LAYOUT_DIR_LIB)` for the `:kind Biblioteca`
6549 // default `lib/<nome>.lisp` reconstruction, `root.join(LAYOUT_DIR_EXE)`
6550 // for the [`LayoutError::ExeOutsideDir`] gate,
6551 // `root.join(LAYOUT_DIR_SERVICOS)` for the
6552 // [`LayoutError::ServicoOutsideDir`] gate) through the three
6553 // lifted consts, not through inline `"lib"` / `"exe"` /
6554 // `"servicos"` `&str` literals. This test drives the
6555 // [`LayoutError::ExeOutsideDir`] arm through a `:kind Binario`
6556 // caixa whose declared `:exe` entry deliberately escapes
6557 // `root.join(LAYOUT_DIR_EXE)` (a sibling `bin/tool` path) —
6558 // if the production emit reads the wrong const (or reverts to
6559 // an inline literal that drifts from the const) the diagnostic
6560 // arm surfaces the wrong variant, catching the drift at build
6561 // time rather than as a per-invocation runtime mismatch.
6562 //
6563 // Mirror of the peer production-through-const pin
6564 // [`crate::dep::tests::validate_no_self_dep_deps_field_routes_through_dep_author_key`]
6565 // (4da6fba) on the sibling M0 `:deps` `list:` diagnostic axis.
6566 use std::path::PathBuf;
6567 let root = PathBuf::from("/tmp/x");
6568 let manifest = root.join("caixa.lisp");
6569 let bin_entry_outside = root.join("bin/tool");
6570 let mut c = caixa(CaixaKind::Binario);
6571 c.exe = vec!["bin/tool".into()];
6572 let manifest_clone = manifest.clone();
6573 let outside_clone = bin_entry_outside.clone();
6574 let layout = StandardLayout::new()
6575 .with_path_exists(move |p| p == manifest_clone || p == outside_clone);
6576 let err = layout.verify(&c, &root).unwrap_err();
6577 match err {
6578 LayoutError::ExeOutsideDir(path) => {
6579 assert_eq!(
6580 path, bin_entry_outside,
6581 "ExeOutsideDir must carry the resolved `:exe` entry that \
6582 escapes `root.join(LAYOUT_DIR_EXE)`"
6583 );
6584 // Byte-identity check: the escape must be against the
6585 // lifted `LAYOUT_DIR_EXE` sub-tree, not a stale inline
6586 // literal — a future const-edit that drifts from `"exe"`
6587 // reroutes `exe_dir` off the sandbox `bin/tool` escapes
6588 // from, and this pattern-arm miss re-surfaces here.
6589 assert!(
6590 !path.starts_with(root.join(crate::render::LAYOUT_DIR_EXE)),
6591 "resolved `:exe` entry {path:?} must escape the \
6592 `root.join(LAYOUT_DIR_EXE)` sub-tree the production \
6593 emit uses to gate the [`LayoutError::ExeOutsideDir`] arm"
6594 );
6595 }
6596 other => panic!("expected ExeOutsideDir, got {other:?}"),
6597 }
6598 }
6599
6600 #[test]
6601 fn upgrade_state_change_without_behavior_callback_surfaces_as_upgrade_violation() {
6602 // Wiring pin for the cross-slot composition gate
6603 // (`validate_upgrade_from_against_behavior`): a caixa whose
6604 // `:upgrade-from` declares a `(:state-change "lib/m.lisp")`
6605 // instruction but does not declare `:behavior :on-state-change`
6606 // surfaces at `feira build` time as a `LayoutError::UpgradeViolation`
6607 // naming the offending caixa + the entry's `:from` + the
6608 // offending script — not at hot-upgrade dispatch when the
6609 // operator reaches for the missing callback. Mirrors
6610 // `upgrade_from_downgrade_surfaces_as_upgrade_violation` on the
6611 // peer `:from` ↔ `:versao` cross-slot precedence gate.
6612 use crate::{UpgradeFromEntry, UpgradeInstruction};
6613 use std::path::PathBuf;
6614 let root = PathBuf::from("/tmp/x");
6615 let manifest = root.join("caixa.lisp");
6616 let svc = root.join("servicos/demo.computeunit.yaml");
6617 let mut c = caixa(CaixaKind::Servico);
6618 c.versao = "0.2.0".into();
6619 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6620 // `:behavior` is None (the canonical "I added the upgrade path
6621 // but never declared :behavior" footgun the gate closes); a
6622 // peer arm covers the BehaviorSpec-Some-but-on-state-change-
6623 // None shape in `upgrade::tests::behavior_gate_rejects_state_
6624 // change_when_on_state_change_is_none`.
6625 c.upgrade_from = vec![UpgradeFromEntry {
6626 from: "0.1.0".into(),
6627 instructions: vec![
6628 UpgradeInstruction::LoadModule {
6629 module: "demo".into(),
6630 },
6631 UpgradeInstruction::StateChange {
6632 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
6633 },
6634 ],
6635 }];
6636 let manifest_clone = manifest.clone();
6637 let svc_clone = svc.clone();
6638 let layout =
6639 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
6640 let err = layout.verify(&c, &root).unwrap_err();
6641 match err {
6642 LayoutError::UpgradeViolation { caixa, issue } => {
6643 assert_eq!(caixa, "demo", "diagnostic must name the offending caixa");
6644 assert!(
6645 issue.contains(crate::render::M2_BEHAVIOR_AUTHOR_KEY_ON_STATE_CHANGE),
6646 "diagnostic must name the missing callback slot for self-locating fix, \
6647 got {issue:?}"
6648 );
6649 assert!(
6650 issue.contains("0.1.0"),
6651 "diagnostic must name the offending entry's :from, got {issue:?}"
6652 );
6653 assert!(
6654 issue.contains("v01-to-v02.lisp"),
6655 "diagnostic must name the offending :script for self-locating fix, \
6656 got {issue:?}"
6657 );
6658 }
6659 other => panic!("expected UpgradeViolation, got {other:?}"),
6660 }
6661 }
6662
6663 #[test]
6664 fn supervisor_must_have_children() {
6665 use crate::RestartStrategy;
6666 let root = PathBuf::from("/tmp/x");
6667 let manifest = root.join("caixa.lisp");
6668 let manifest_clone = manifest.clone();
6669 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6670 let mut c = caixa(CaixaKind::Supervisor);
6671 c.estrategia = Some(RestartStrategy::OneForOne);
6672 c.max_restarts = Some(5);
6673 // No children → should fail
6674 let err = layout.verify(&c, &root).unwrap_err();
6675 assert!(matches!(err, LayoutError::SupervisorViolation { .. }));
6676 }
6677
6678 #[test]
6679 fn supervisor_self_referential_child_is_violation() {
6680 // A Supervisor whose `:children` names its own `:nome` is a
6681 // one-node supervision cycle. The cross-slot gate fires at
6682 // verify time, surfacing as a SupervisorViolation that names the
6683 // offending supervisor — not at the cluster apply far from
6684 // source. The `caixa()` helper's `:nome` is "demo".
6685 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6686 let root = PathBuf::from("/tmp/x");
6687 let manifest = root.join("caixa.lisp");
6688 let manifest_clone = manifest.clone();
6689 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6690 let mut c = caixa(CaixaKind::Supervisor);
6691 c.estrategia = Some(RestartStrategy::OneForOne);
6692 c.max_restarts = Some(5);
6693 c.children = vec![
6694 ChildSpec {
6695 caixa: "worker".into(),
6696 versao: "^0.1".into(),
6697 restart: RestartPolicy::Permanent,
6698 },
6699 ChildSpec {
6700 caixa: "demo".into(),
6701 versao: "^0.1".into(),
6702 restart: RestartPolicy::Permanent,
6703 },
6704 ];
6705 let err = layout.verify(&c, &root).unwrap_err();
6706 let LayoutError::SupervisorViolation { caixa, issue } = err else {
6707 panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6708 };
6709 assert_eq!(caixa, "demo");
6710 assert!(
6711 issue.contains("demo") && issue.contains("itself"),
6712 "issue must name the self-supervising caixa, got {issue:?}"
6713 );
6714 }
6715
6716 #[test]
6717 fn supervisor_distinct_children_pass_self_supervision_gate() {
6718 // Positive control: a Supervisor whose children are all distinct
6719 // from its own `:nome` verifies cleanly.
6720 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6721 let root = PathBuf::from("/tmp/x");
6722 let manifest = root.join("caixa.lisp");
6723 let manifest_clone = manifest.clone();
6724 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6725 let mut c = caixa(CaixaKind::Supervisor);
6726 c.estrategia = Some(RestartStrategy::OneForOne);
6727 c.max_restarts = Some(5);
6728 c.children = vec![ChildSpec {
6729 caixa: "worker".into(),
6730 versao: "^0.1".into(),
6731 restart: RestartPolicy::Permanent,
6732 }];
6733 layout.verify(&c, &root).unwrap();
6734 }
6735
6736 #[test]
6737 fn cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor() {
6738 // Composition pin: every cross-slot self-edge gate fired from
6739 // `LayoutInvariants::verify` — the supervision-tree arm's
6740 // `crate::supervisor::validate_no_self_supervision` call, the
6741 // Aplicacao arm's `crate::aplicacao::validate_no_self_membership`
6742 // call, and the dep-graph arm's
6743 // `crate::dep::validate_no_self_dep` call — must key its
6744 // `parent_nome` arg off the typed [`Caixa::nome`] accessor, not
6745 // the raw `&caixa.nome` `&String`-borrow of the underlying
6746 // field.
6747 //
6748 // Structurally: a rename of the storage field or a hypothetical
6749 // accessor rebrand (a per-cluster alias table pinned through a
6750 // future `:placement`-scoped slot, the M4 CR materializer's
6751 // per-CR namespace-qualified rewrite, a `:nome-suffix` overlay
6752 // the MESH-COMPOSITION §III.2 roadmap acknowledges) would land
6753 // through the accessor by construction; a raw-borrow bypass
6754 // would silently disagree with every peer consumer that already
6755 // routes through `caixa.nome()` (the caixa-mesh 980c059,
6756 // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6757 // caixa-feira ef83332 raw-borrow converges), reintroducing the
6758 // drift surface the sibling converges closed. Each arm fires
6759 // its per-kind `LayoutError` variant (`SupervisorViolation` /
6760 // `AplicacaoViolation` / `DepsViolation`) whose `caixa` field
6761 // carries the offending parent name verbatim through
6762 // `caixa.nome().clone()`; asserting the field equals
6763 // `caixa.nome()` on the mutated fixture pins the accessor-
6764 // routed parent-nome projection at every call site — a future
6765 // silent detour that had the gate observe a stale / aliased
6766 // name at the arg boundary would surface here as a
6767 // `caixa != "demo"` inequality.
6768 //
6769 // Peer of the sibling per-caixa-crate `nome`-arg raw-borrow
6770 // convergence pin discipline (54bf2f3 / 22461ef / 162e2e2 on the
6771 // renderer crates; ef83332 on the CLI) — extends the "one typed
6772 // dispatch per `:nome` consumer" discipline onto the substrate's
6773 // own [`LayoutInvariants::verify`] cross-slot self-edge gate
6774 // wire-up on all three typed-name-graph kinds.
6775 use crate::{
6776 ChildSpec, Dep, Membro, Placement, PlacementStrategy, RestartPolicy, RestartStrategy,
6777 };
6778 let root = PathBuf::from("/tmp/x");
6779 let manifest = root.join("caixa.lisp");
6780 let manifest_clone = manifest.clone();
6781 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
6782
6783 // Supervisor arm — the `caixa()` helper's `:nome` is "demo",
6784 // and the accessor's return `caixa.nome()` must equal the
6785 // parent-nome that the self-supervision gate observes.
6786 let mut sup = caixa(CaixaKind::Supervisor);
6787 sup.estrategia = Some(RestartStrategy::OneForOne);
6788 sup.max_restarts = Some(5);
6789 sup.children = vec![ChildSpec {
6790 caixa: "demo".into(),
6791 versao: "^0.1".into(),
6792 restart: RestartPolicy::Permanent,
6793 }];
6794 let parent_nome_via_accessor = sup.nome();
6795 assert_eq!(
6796 parent_nome_via_accessor, "demo",
6797 "the caixa() fixture helper's `:nome` must be \"demo\" — \
6798 the accessor's return is the pin's ground truth for the \
6799 cross-slot gate's parent-nome arg",
6800 );
6801 let err = layout.verify(&sup, &root).unwrap_err();
6802 let LayoutError::SupervisorViolation { caixa: c_nome, .. } = err else {
6803 panic!("expected SupervisorViolation for self-referential child, got {err:?}");
6804 };
6805 assert_eq!(
6806 c_nome, parent_nome_via_accessor,
6807 "the SupervisorViolation's `caixa` field must equal \
6808 `sup.nome()` — the cross-slot self-supervision gate's \
6809 `parent_nome` arg must route through the lifted \
6810 [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6811 `&String`-borrow of the underlying field",
6812 );
6813
6814 // Aplicacao arm — same discipline on the peer typed-name-graph
6815 // kind. Constructed alongside the supervisor arm so any future
6816 // accessor drift lands on both arms in the same pin.
6817 let mut app = caixa(CaixaKind::Aplicacao);
6818 app.placement = Some(Placement {
6819 estrategia: PlacementStrategy::Replicated,
6820 clusters: vec!["rio".into()],
6821 affinity: None,
6822 shard_key: None,
6823 });
6824 app.membros = vec![Membro {
6825 caixa: "demo".into(),
6826 versao: "^0.1".into(),
6827 }];
6828 let parent_nome_via_accessor = app.nome();
6829 assert_eq!(
6830 parent_nome_via_accessor, "demo",
6831 "the caixa() fixture helper's `:nome` must be \"demo\" on \
6832 the Aplicacao arm too — same accessor-ground-truth as the \
6833 sibling supervisor arm above",
6834 );
6835 let err = layout.verify(&app, &root).unwrap_err();
6836 let LayoutError::AplicacaoViolation { caixa: c_nome, .. } = err else {
6837 panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
6838 };
6839 assert_eq!(
6840 c_nome, parent_nome_via_accessor,
6841 "the AplicacaoViolation's `caixa` field must equal \
6842 `app.nome()` — the cross-slot self-membership gate's \
6843 `parent_nome` arg must route through the lifted \
6844 [`Caixa::nome`] accessor, not the raw `&caixa.nome` \
6845 `&String`-borrow of the underlying field",
6846 );
6847
6848 // Dep-graph arm — third typed-name-graph kind on the
6849 // `parent_nome` arg boundary. Same discipline as the peer
6850 // supervision-tree and Aplicacao-membership arms above.
6851 // Constructed alongside so any future accessor drift lands on
6852 // all three arms in the same pin. Needs a distinct layout
6853 // fixture from the supervisor / aplicacao arms above because
6854 // the `Biblioteca` kind's code-path existence gate demands the
6855 // canonical `lib/<nome>.lisp` path also `exists`, so the shim
6856 // covers both `caixa.lisp` and `lib/demo.lisp`.
6857 let default_lib = root.join("lib").join("demo.lisp");
6858 let manifest_dep = manifest.clone();
6859 let default_lib_clone = default_lib.clone();
6860 let layout_dep = StandardLayout::new()
6861 .with_path_exists(move |p| p == manifest_dep || p == default_lib_clone);
6862 let mut lib = caixa(CaixaKind::Biblioteca);
6863 lib.deps = vec![Dep::simple("demo", "^0.1")];
6864 let parent_nome_via_accessor = lib.nome();
6865 assert_eq!(
6866 parent_nome_via_accessor, "demo",
6867 "the caixa() fixture helper's `:nome` must be \"demo\" on \
6868 the Biblioteca arm too — same accessor-ground-truth as the \
6869 sibling supervisor + Aplicacao arms above",
6870 );
6871 let err = layout_dep.verify(&lib, &root).unwrap_err();
6872 let LayoutError::DepsViolation { caixa: c_nome, .. } = err else {
6873 panic!("expected DepsViolation for self-referential :deps entry, got {err:?}");
6874 };
6875 assert_eq!(
6876 c_nome, parent_nome_via_accessor,
6877 "the DepsViolation's `caixa` field must equal \
6878 `lib.nome()` — the cross-slot self-dep gate's `parent_nome` \
6879 arg must route through the lifted [`Caixa::nome`] accessor, \
6880 not the raw `&caixa.nome` `&String`-borrow of the underlying \
6881 field",
6882 );
6883 }
6884
6885 #[test]
6886 fn upgrade_against_versao_gate_routes_current_versao_through_lifted_accessor() {
6887 // Composition pin: the cross-slot `:upgrade-from :from` ↔
6888 // `:versao` precedence gate fired from
6889 // `LayoutInvariants::verify` — the
6890 // `crate::upgrade::validate_upgrade_from_against_versao` call —
6891 // must key its `versao` arg off the typed [`Caixa::versao`]
6892 // accessor, not the raw `&caixa.versao` `&String`-borrow of
6893 // the underlying field.
6894 //
6895 // Same "arg-boundary reads through the lifted accessor"
6896 // discipline as the sibling
6897 // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
6898 // pin above on the `:nome`-arg axis of the three typed-name-
6899 // graph self-edge gates — extended here onto the `:versao`-arg
6900 // axis of the substrate's remaining `LayoutInvariants::verify`
6901 // cross-slot arg-carrying call site. Structurally byte-equal
6902 // today (the accessor is `pub fn versao(&self) -> &str { &self.versao }`,
6903 // so both paths coerce to the same `&str`); the pin catches a
6904 // future silent detour (an accessor rebrand that no longer
6905 // shipped the raw slot verbatim — a per-`:edicao` overlay,
6906 // a promotion of `:versao` to a `CaixaVersion` newtype with a
6907 // canonicalizing accessor, an M4 CR-materializer-side pinning
6908 // through a resolver-annotated `:versao-resolved` slot) that
6909 // would silently split the substrate's own precedence gate
6910 // from every peer consumer already routing `:versao` reads
6911 // through the lifted accessor.
6912 //
6913 // The gate fires an `UpgradeViolation { caixa, issue }` when a
6914 // `:upgrade-from` entry's `:from` is not strictly less than
6915 // the top-level `:versao` — the `issue` string names both the
6916 // offending prior version and the current version verbatim,
6917 // so asserting the substring `caixa.versao()` appears in the
6918 // fired diagnostic pins the accessor-routed current-versao
6919 // projection at the arg boundary. A raw-borrow bypass would
6920 // still surface the same bytes today, but the presence of
6921 // this pin makes any future divergence between the accessor's
6922 // return and the raw slot's contents a build-time failure at
6923 // this call site.
6924 use crate::{UpgradeFromEntry, UpgradeInstruction};
6925 let root = PathBuf::from("/tmp/x");
6926 let manifest = root.join("caixa.lisp");
6927 let servico_path = root.join("servicos").join("demo.computeunit.yaml");
6928 let manifest_clone = manifest.clone();
6929 let servico_clone = servico_path.clone();
6930 let layout = StandardLayout::new()
6931 .with_path_exists(move |p| p == manifest_clone || p == servico_clone);
6932 let mut svc = caixa(CaixaKind::Servico);
6933 svc.versao = "0.1.0".into();
6934 svc.servicos = vec!["servicos/demo.computeunit.yaml".into()];
6935 // `:from` >= current `:versao` — trips the precedence gate the
6936 // `validate_upgrade_from_against_versao` cross-slot call
6937 // enforces. `:load-module` carries no on-disk path so the
6938 // path-existence gate downstream stays inert; the precedence
6939 // gate is what fires. No `:on-state-change` needed because the
6940 // instruction list carries no `:state-change` entry, so the
6941 // sibling `validate_upgrade_from_against_behavior` gate is
6942 // inert too.
6943 svc.upgrade_from = vec![UpgradeFromEntry {
6944 from: "0.2.0".into(),
6945 instructions: vec![UpgradeInstruction::LoadModule {
6946 module: "demo".into(),
6947 }],
6948 }];
6949 let current_versao_via_accessor = svc.versao().to_string();
6950 assert_eq!(
6951 current_versao_via_accessor, "0.1.0",
6952 "the mutated fixture's `:versao` must be observable through \
6953 the accessor before layout verification fires — a drift on \
6954 `Caixa::versao` would surface here as a `!= \"0.1.0\"` \
6955 inequality",
6956 );
6957 let err = layout.verify(&svc, &root).unwrap_err();
6958 let LayoutError::UpgradeViolation {
6959 caixa: c_nome,
6960 issue,
6961 } = err
6962 else {
6963 panic!("expected UpgradeViolation for :from >= :versao, got {err:?}");
6964 };
6965 assert_eq!(c_nome, svc.nome(), "wrap envelope names the caixa");
6966 assert!(
6967 issue.contains(¤t_versao_via_accessor),
6968 "the UpgradeViolation's `issue` must quote the current \
6969 `:versao` byte-string verbatim — the cross-slot precedence \
6970 gate's `versao` arg must route through the lifted \
6971 [`Caixa::versao`] accessor, not the raw `&caixa.versao` \
6972 `&String`-borrow of the underlying field. issue: {issue}",
6973 );
6974 }
6975
6976 #[test]
6977 fn layout_violation_envelopes_carry_caixa_nome_through_lifted_accessor() {
6978 // Wrap-envelope drift-detection pin: every per-axis
6979 // `LayoutError::*Violation { caixa, issue }` envelope fired
6980 // from `LayoutInvariants::verify` must key its offending-caixa
6981 // field off the typed [`Caixa::nome`] accessor's
6982 // `.to_string()` extension, not the raw
6983 // `caixa.nome.clone()` `String::clone()` of the underlying
6984 // field. Structurally byte-equal today (each accessor is
6985 // `pub fn nome(&self) -> &str { &self.nome }`, so
6986 // `caixa.nome().to_string()` and `caixa.nome.clone()` produce
6987 // the same bytes); the pin catches a future silent detour
6988 // (an accessor rebrand that no longer shipped the raw slot
6989 // verbatim — a per-cluster alias table pinned through a
6990 // future `:placement`-scoped slot, the M4 CR materializer's
6991 // per-CR namespace-qualified rewrite, a `:nome-suffix`
6992 // overlay the MESH-COMPOSITION §III.2 roadmap acknowledges)
6993 // that would silently split the substrate's own layout
6994 // invariant verifier's diagnostic surface from every peer
6995 // caixa-crate consumer that already routes `:nome` reads
6996 // through the lifted accessor (the caixa-mesh 980c059,
6997 // caixa-helm 22461ef, caixa-flux 162e2e2, caixa-crd 61d3429,
6998 // caixa-feira ef83332 raw-borrow converges).
6999 //
7000 // Exercises a representative variant on each of the three
7001 // wrap-envelope arm shapes the substrate's per-axis fan-out
7002 // carries: (1) `LayoutError::NomeViolation` (the leading arm
7003 // in the `verify` order — the `:nome` axis's DNS-1123 shape
7004 // gate fires immediately after the manifest-existence gate),
7005 // (2) `LayoutError::BinarioWithoutExe` (a tuple-variant on
7006 // the kind-coherence family — different envelope shape than
7007 // the struct-variant `*Violation { caixa, issue }` family
7008 // but the same converge target on the `caixa.nome().to_string()`
7009 // arg), and (3) `LayoutError::ServicoWithoutServicos` (the
7010 // sibling tuple-variant on the same kind-coherence family).
7011 // Together they cover the two `LayoutError` envelope shapes
7012 // (struct-variant + tuple-variant) the layout invariants file
7013 // emits on `:nome`-carrying arms.
7014 //
7015 // Peer of the sibling
7016 // [`cross_slot_self_edge_gates_route_parent_nome_through_lifted_accessor`]
7017 // pin above — extends the "wrap-envelope `caixa:` field
7018 // reads through the lifted accessor" discipline from the
7019 // cross-slot self-edge gates' `parent_nome` arg boundary
7020 // onto the per-axis `LayoutError::*Violation` envelope's
7021 // `caixa:` field boundary.
7022
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
7028 // (1) `NomeViolation` on the struct-variant envelope: force a
7029 // DNS-1123-invalid `:nome` (uppercase byte — `is_dns_1123_label`
7030 // rejects) and assert the fired envelope's `caixa:` field
7031 // byte-equals `c.nome().to_string()`.
7032 let mut c = caixa(CaixaKind::Biblioteca);
7033 c.nome = "BAD_NAME".into();
7034 c.bibliotecas = vec!["lib/demo.lisp".into()];
7035 let expected_nome_via_accessor = c.nome().to_string();
7036 assert_eq!(
7037 expected_nome_via_accessor, "BAD_NAME",
7038 "the mutated fixture's `:nome` must be observable through \
7039 the accessor before layout verification fires — a drift \
7040 on `Caixa::nome` would surface here as a `!= \"BAD_NAME\"` \
7041 inequality",
7042 );
7043 let err = layout.verify(&c, &root).unwrap_err();
7044 let LayoutError::NomeViolation { caixa: c_nome, .. } = err else {
7045 panic!("expected NomeViolation for DNS-1123-invalid :nome, got {err:?}");
7046 };
7047 assert_eq!(
7048 c_nome, expected_nome_via_accessor,
7049 "the NomeViolation's `caixa` field must equal \
7050 `c.nome().to_string()` — the wrap envelope's per-axis \
7051 projection must route through the lifted [`Caixa::nome`] \
7052 accessor's `.to_string()` extension, not the raw \
7053 `caixa.nome.clone()` `String::clone()` of the underlying \
7054 field",
7055 );
7056
7057 // (2) `BinarioWithoutExe` on the tuple-variant envelope: a
7058 // Binario-kind caixa with an empty `:exe` list fires the
7059 // kind-coherence gate whose payload is a bare `String`, so the
7060 // pattern is `LayoutError::BinarioWithoutExe(String)` rather
7061 // than the struct-variant `{ caixa, issue }` family. The
7062 // converge target is the same — `caixa.nome().to_string()` — but
7063 // the envelope shape is different, so the pin exercises both.
7064 let mut c = caixa(CaixaKind::Binario);
7065 // `:exe` empty is the trigger — the fixture helper defaults
7066 // it to `vec![]`, so no mutation is needed.
7067 c.nome = "binario-demo".into();
7068 let expected_nome_via_accessor = c.nome().to_string();
7069 assert_eq!(
7070 expected_nome_via_accessor, "binario-demo",
7071 "the mutated fixture's `:nome` must be observable through \
7072 the accessor before layout verification fires",
7073 );
7074 let err = layout.verify(&c, &root).unwrap_err();
7075 let LayoutError::BinarioWithoutExe(c_nome) = err else {
7076 panic!("expected BinarioWithoutExe for empty :exe list on Binario kind, got {err:?}");
7077 };
7078 assert_eq!(
7079 c_nome, expected_nome_via_accessor,
7080 "the BinarioWithoutExe's payload must equal \
7081 `c.nome().to_string()` — the tuple-variant envelope's \
7082 per-axis projection must route through the lifted \
7083 [`Caixa::nome`] accessor's `.to_string()` extension, not \
7084 the raw `caixa.nome.clone()` `String::clone()` of the \
7085 underlying field",
7086 );
7087
7088 // (3) `ServicoWithoutServicos` on the sibling tuple-variant
7089 // envelope: same discipline on the peer kind-coherence
7090 // partition arm. Constructed alongside the Binario arm so any
7091 // future accessor drift lands on both arms in the same pin.
7092 let mut c = caixa(CaixaKind::Servico);
7093 // `:servicos` empty is the trigger — the fixture helper
7094 // defaults it to `vec![]`, so no mutation is needed.
7095 c.nome = "servico-demo".into();
7096 let expected_nome_via_accessor = c.nome().to_string();
7097 assert_eq!(
7098 expected_nome_via_accessor, "servico-demo",
7099 "the mutated fixture's `:nome` must be observable through \
7100 the accessor before layout verification fires",
7101 );
7102 let err = layout.verify(&c, &root).unwrap_err();
7103 let LayoutError::ServicoWithoutServicos(c_nome) = err else {
7104 panic!(
7105 "expected ServicoWithoutServicos for empty :servicos list on Servico kind, \
7106 got {err:?}"
7107 );
7108 };
7109 assert_eq!(
7110 c_nome, expected_nome_via_accessor,
7111 "the ServicoWithoutServicos's payload must equal \
7112 `c.nome().to_string()` — same converge discipline as the \
7113 sibling `BinarioWithoutExe` tuple-variant arm above",
7114 );
7115 }
7116
7117 #[test]
7118 fn supervisor_must_not_have_bibliotecas() {
7119 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7120 let root = PathBuf::from("/tmp/x");
7121 let manifest = root.join("caixa.lisp");
7122 let manifest_clone = manifest.clone();
7123 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7124 let mut c = caixa(CaixaKind::Supervisor);
7125 c.estrategia = Some(RestartStrategy::OneForOne);
7126 c.max_restarts = Some(5);
7127 c.bibliotecas = vec!["lib/code.lisp".into()];
7128 c.children = vec![ChildSpec {
7129 caixa: "worker".into(),
7130 versao: "^0.1".into(),
7131 restart: RestartPolicy::Permanent,
7132 }];
7133 let err = layout.verify(&c, &root).unwrap_err();
7134 assert!(matches!(err, LayoutError::SupervisorOwnsCode(_)));
7135 }
7136
7137 // ── Caixa::validate_restart_window wired into Supervisor verify ─────
7138 //
7139 // Until this wire-up landed `Caixa::validate_restart_window` lived as
7140 // `pub fn` on `Caixa` with full per-arm unit coverage in
7141 // `manifest::tests` (`validate_restart_window_rejects_*` — fractional,
7142 // decimal-shaped integer, half-unit minute, leading sign, unknown
7143 // unit, garbage, empty-after-trim) but no production path called it;
7144 // `feira build` silently accepted malformed `:restart-window` and
7145 // `Caixa::supervisor_view` soft-swallowed the parse failure as
7146 // `restart_window: None` (the canonical "no reset" sentinel), turning
7147 // every authoring footgun into a never-reset supervisor far from the
7148 // source caixa.lisp. The following pins fence the layout-pipeline
7149 // wire-up: every layout verify on a structurally-invalid `:restart-
7150 // window` axis surfaces the per-axis `RestartWindowViolation { caixa,
7151 // issue }` envelope before the typed `SupervisorSpec::validate` gate
7152 // sees the laundered `None`.
7153
7154 fn supervisor_with_window(window: Option<&str>) -> Caixa {
7155 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7156 let mut c = caixa(CaixaKind::Supervisor);
7157 c.estrategia = Some(RestartStrategy::OneForOne);
7158 c.max_restarts = Some(5);
7159 c.restart_window = window.map(str::to_string);
7160 c.children = vec![ChildSpec {
7161 caixa: "worker".into(),
7162 versao: "^0.1".into(),
7163 restart: RestartPolicy::Permanent,
7164 }];
7165 c
7166 }
7167
7168 #[test]
7169 fn restart_window_violation_on_fractional_seconds() {
7170 // `"1.5s"` is the canonical fractional-seconds drift footgun the
7171 // shared integer-magnitude codec (1c55a2a) rejects: round-trips
7172 // through `render` as `"1500ms"` on first serialize, breaking
7173 // THEORY.md §V.2.7 render-determinism. Before this wire-up
7174 // `supervisor_view` soft-swallowed the parse error as
7175 // `restart_window: None`, masking the drift as a never-reset
7176 // supervisor.
7177 let root = PathBuf::from("/tmp/x");
7178 let manifest = root.join("caixa.lisp");
7179 let manifest_clone = manifest.clone();
7180 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7181 let c = supervisor_with_window(Some("1.5s"));
7182 let err = layout.verify(&c, &root).unwrap_err();
7183 let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7184 panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7185 };
7186 assert_eq!(caixa, "demo");
7187 assert!(
7188 issue.contains("1.5s"),
7189 "issue must quote the offending raw value: {issue}",
7190 );
7191 }
7192
7193 #[test]
7194 fn restart_window_violation_on_decimal_shaped_integer() {
7195 // `"1.0s"` — decimal-shaped integer the codec also rejects (a
7196 // canonical authoring form is `"1s"`). Sibling of the fractional
7197 // case; same codec arm.
7198 let root = PathBuf::from("/tmp/x");
7199 let manifest = root.join("caixa.lisp");
7200 let manifest_clone = manifest.clone();
7201 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7202 let c = supervisor_with_window(Some("1.0s"));
7203 let err = layout.verify(&c, &root).unwrap_err();
7204 assert!(
7205 matches!(
7206 err,
7207 LayoutError::RestartWindowViolation { ref caixa, ref issue }
7208 if caixa == "demo" && issue.contains("1.0s")
7209 ),
7210 "got {err:?}",
7211 );
7212 }
7213
7214 #[test]
7215 fn restart_window_violation_on_leading_sign() {
7216 // `"+30s"` / `"-30s"` — leading-sign drift the codec rejects.
7217 // Canonical form is `"30s"`. Pin both signs separately because
7218 // a future relaxation might accept one but not the other.
7219 for raw in ["+30s", "-30s"] {
7220 let root = PathBuf::from("/tmp/x");
7221 let manifest = root.join("caixa.lisp");
7222 let manifest_clone = manifest.clone();
7223 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7224 let c = supervisor_with_window(Some(raw));
7225 let err = layout.verify(&c, &root).unwrap_err();
7226 assert!(
7227 matches!(
7228 err,
7229 LayoutError::RestartWindowViolation { ref caixa, ref issue }
7230 if caixa == "demo" && issue.contains(raw)
7231 ),
7232 "leading-sign {raw:?} got {err:?}",
7233 );
7234 }
7235 }
7236
7237 #[test]
7238 fn restart_window_violation_on_unknown_unit() {
7239 // `"30x"` — unknown duration unit. The codec admits only
7240 // `ms`/`s`/`m`/`h`.
7241 let root = PathBuf::from("/tmp/x");
7242 let manifest = root.join("caixa.lisp");
7243 let manifest_clone = manifest.clone();
7244 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7245 let c = supervisor_with_window(Some("30x"));
7246 let err = layout.verify(&c, &root).unwrap_err();
7247 assert!(
7248 matches!(
7249 err,
7250 LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"
7251 ),
7252 "got {err:?}",
7253 );
7254 }
7255
7256 #[test]
7257 fn restart_window_violation_on_garbage() {
7258 // `"abc"` — pure garbage. The codec's parse fails before the
7259 // unit dispatch; the wrap envelope still surfaces the
7260 // self-locating diagnostic at the source.
7261 let root = PathBuf::from("/tmp/x");
7262 let manifest = root.join("caixa.lisp");
7263 let manifest_clone = manifest.clone();
7264 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7265 let c = supervisor_with_window(Some("abc"));
7266 let err = layout.verify(&c, &root).unwrap_err();
7267 assert!(
7268 matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7269 "got {err:?}",
7270 );
7271 }
7272
7273 #[test]
7274 fn restart_window_violation_on_empty_string() {
7275 // `""` — empty after trim. The shared codec's digit-only gate
7276 // refuses an empty magnitude. Distinguished here from the
7277 // `None` ("omit the slot") canonical authoring shape: an empty
7278 // string is an authored-but-empty slot, never the author's
7279 // intent.
7280 let root = PathBuf::from("/tmp/x");
7281 let manifest = root.join("caixa.lisp");
7282 let manifest_clone = manifest.clone();
7283 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7284 let c = supervisor_with_window(Some(""));
7285 let err = layout.verify(&c, &root).unwrap_err();
7286 assert!(
7287 matches!(err, LayoutError::RestartWindowViolation { ref caixa, .. } if caixa == "demo"),
7288 "got {err:?}",
7289 );
7290 }
7291
7292 #[test]
7293 fn verify_accepts_supervisor_without_restart_window() {
7294 // `None` is the canonical "omit the slot to express no reset"
7295 // shape — never reaches the codec, validates cleanly.
7296 let root = PathBuf::from("/tmp/x");
7297 let manifest = root.join("caixa.lisp");
7298 let manifest_clone = manifest.clone();
7299 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7300 let c = supervisor_with_window(None);
7301 layout.verify(&c, &root).unwrap();
7302 }
7303
7304 #[test]
7305 fn verify_accepts_supervisor_with_canonical_restart_window() {
7306 // Every canonical form the shared codec round-trips losslessly
7307 // must pass — `"500ms"`, `"30s"`, `"60s"`, `"1m"`, `"2m"`,
7308 // `"1h"`. Pin every form so a future tightening of the codec's
7309 // accepted set surfaces here as a test failure.
7310 for form in ["500ms", "30s", "60s", "1m", "2m", "1h"] {
7311 let root = PathBuf::from("/tmp/x");
7312 let manifest = root.join("caixa.lisp");
7313 let manifest_clone = manifest.clone();
7314 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7315 let c = supervisor_with_window(Some(form));
7316 layout
7317 .verify(&c, &root)
7318 .unwrap_or_else(|e| panic!("canonical {form:?} must validate, got {e:?}"));
7319 }
7320 }
7321
7322 #[test]
7323 fn restart_window_violation_fires_before_supervisor_view_validate() {
7324 // Diagnostic-precedence pin: a Supervisor with a malformed
7325 // `:restart-window` AND a typed-shape defect on the typed view
7326 // (zero `:max-restarts`, which `SupervisorSpec::validate`'s
7327 // `ZeroMaxRestarts` arm rejects) surfaces the raw-string
7328 // diagnostic first — the narrower self-locating gate wins. Until
7329 // this wire-up landed `supervisor_view` would silently launder
7330 // the malformed `:restart-window` to `None` and then the typed
7331 // view's `ZeroMaxRestarts` gate would surface, masking the
7332 // raw-string footgun.
7333 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7334 let root = PathBuf::from("/tmp/x");
7335 let manifest = root.join("caixa.lisp");
7336 let manifest_clone = manifest.clone();
7337 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7338 let mut c = caixa(CaixaKind::Supervisor);
7339 c.estrategia = Some(RestartStrategy::OneForOne);
7340 c.max_restarts = Some(0);
7341 c.restart_window = Some("1.5s".into());
7342 c.children = vec![ChildSpec {
7343 caixa: "worker".into(),
7344 versao: "^0.1".into(),
7345 restart: RestartPolicy::Permanent,
7346 }];
7347 let err = layout.verify(&c, &root).unwrap_err();
7348 assert!(
7349 matches!(err, LayoutError::RestartWindowViolation { .. }),
7350 "got {err:?} — RestartWindowViolation must fire before SupervisorViolation",
7351 );
7352 }
7353
7354 #[test]
7355 fn supervisor_slots_on_non_supervisor_fires_before_restart_window_violation() {
7356 // Order pin: a non-Supervisor caixa with a malformed
7357 // `:restart-window` surfaces `SupervisorSlotsOnNonSupervisor`
7358 // (the kind-coherence gate at the top of verify) before the
7359 // raw-string parse gate inside the Supervisor branch — because
7360 // `:restart-window` is foreign to non-Supervisor kinds, the
7361 // kind-coherence diagnostic is the load-bearing one. Mirrors
7362 // the existing `nome_violation_on_*` ordering tests that fence
7363 // the precedence between universal and kind-specific gates.
7364 let root = PathBuf::from("/tmp/x");
7365 let manifest = root.join("caixa.lisp");
7366 let default_lib = root.join("lib").join("demo.lisp");
7367 let layout =
7368 StandardLayout::new().with_path_exists(move |p| p == manifest || p == default_lib);
7369 let mut c = caixa(CaixaKind::Biblioteca);
7370 c.restart_window = Some("1.5s".into());
7371 let err = layout.verify(&c, &root).unwrap_err();
7372 assert!(
7373 matches!(err, LayoutError::SupervisorSlotsOnNonSupervisor { .. }),
7374 "got {err:?} — kind-coherence must fire before RestartWindowViolation",
7375 );
7376 }
7377
7378 #[test]
7379 fn restart_window_violation_diagnostic_carries_offending_value() {
7380 // Diagnostic-shape pin: the wrap envelope's `issue` carries the
7381 // codec's parser-shaped reason verbatim (which names the
7382 // offending raw value), so the author can grep their caixa.lisp
7383 // for `:restart-window "<value>"` and fix in one edit. Mirrors
7384 // `nome_violation_*_carries_offending_*` shape pins.
7385 let root = PathBuf::from("/tmp/x");
7386 let manifest = root.join("caixa.lisp");
7387 let manifest_clone = manifest.clone();
7388 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7389 let c = supervisor_with_window(Some("0.5m"));
7390 let err = layout.verify(&c, &root).unwrap_err();
7391 let LayoutError::RestartWindowViolation { caixa, issue } = err else {
7392 panic!("expected LayoutError::RestartWindowViolation, got {err:?}");
7393 };
7394 assert_eq!(caixa, "demo");
7395 assert!(
7396 issue.contains("0.5m"),
7397 "issue must quote the offending raw value verbatim: {issue}",
7398 );
7399 assert!(
7400 !issue.is_empty(),
7401 "issue must carry the codec's parser-shaped reason",
7402 );
7403 }
7404
7405 // ── Aplicacao layout tests ──────────────────────────────────────────
7406
7407 #[test]
7408 fn aplicacao_must_have_membros() {
7409 use crate::{Membro, Placement, PlacementStrategy};
7410 let root = PathBuf::from("/tmp/x");
7411 let manifest = root.join("caixa.lisp");
7412 let manifest_clone = manifest.clone();
7413 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7414 let mut c = caixa(CaixaKind::Aplicacao);
7415 c.placement = Some(Placement {
7416 estrategia: PlacementStrategy::Replicated,
7417 clusters: vec!["rio".into()],
7418 affinity: None,
7419 shard_key: None,
7420 });
7421 // No membros → fails
7422 let err = layout.verify(&c, &root).unwrap_err();
7423 assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
7424
7425 // With membros → passes
7426 c.membros = vec![Membro {
7427 caixa: "service-a".into(),
7428 versao: "^0.1".into(),
7429 }];
7430 layout.verify(&c, &root).unwrap();
7431 }
7432
7433 #[test]
7434 fn aplicacao_self_referential_membro_is_violation() {
7435 // An Aplicacao whose `:membros` names its own `:nome` is a
7436 // one-node lacre-closure recursion. The cross-slot gate fires
7437 // at verify time, surfacing as an AplicacaoViolation that
7438 // names the offending aplicacao — not at lacre-resolve time
7439 // far from source. The `caixa()` helper's `:nome` is "demo".
7440 // Peer of `supervisor_self_referential_child_is_violation`
7441 // on the supervision-tree axis.
7442 use crate::{Membro, Placement, PlacementStrategy};
7443 let root = PathBuf::from("/tmp/x");
7444 let manifest = root.join("caixa.lisp");
7445 let manifest_clone = manifest.clone();
7446 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7447 let mut c = caixa(CaixaKind::Aplicacao);
7448 c.placement = Some(Placement {
7449 estrategia: PlacementStrategy::Replicated,
7450 clusters: vec!["rio".into()],
7451 affinity: None,
7452 shard_key: None,
7453 });
7454 c.membros = vec![
7455 Membro {
7456 caixa: "service-a".into(),
7457 versao: "^0.1".into(),
7458 },
7459 Membro {
7460 caixa: "demo".into(),
7461 versao: "^0.1".into(),
7462 },
7463 ];
7464 let err = layout.verify(&c, &root).unwrap_err();
7465 let LayoutError::AplicacaoViolation { caixa, issue } = err else {
7466 panic!("expected AplicacaoViolation for self-referential membro, got {err:?}");
7467 };
7468 assert_eq!(caixa, "demo");
7469 assert!(
7470 issue.contains("demo") && issue.contains("lists itself"),
7471 "issue must name the self-membering aplicacao, got {issue:?}"
7472 );
7473 }
7474
7475 #[test]
7476 fn aplicacao_distinct_membros_pass_self_membership_gate() {
7477 // Positive control: an Aplicacao whose membros are all distinct
7478 // from its own `:nome` verifies cleanly.
7479 use crate::{Membro, Placement, PlacementStrategy};
7480 let root = PathBuf::from("/tmp/x");
7481 let manifest = root.join("caixa.lisp");
7482 let manifest_clone = manifest.clone();
7483 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7484 let mut c = caixa(CaixaKind::Aplicacao);
7485 c.placement = Some(Placement {
7486 estrategia: PlacementStrategy::Replicated,
7487 clusters: vec!["rio".into()],
7488 affinity: None,
7489 shard_key: None,
7490 });
7491 c.membros = vec![
7492 Membro {
7493 caixa: "service-a".into(),
7494 versao: "^0.1".into(),
7495 },
7496 Membro {
7497 caixa: "service-b".into(),
7498 versao: "^0.1".into(),
7499 },
7500 ];
7501 layout.verify(&c, &root).unwrap();
7502 }
7503
7504 #[test]
7505 fn aplicacao_self_membership_fires_after_view_validate() {
7506 // Diagnostic-precedence pin: a self-referential membro alongside
7507 // a duplicate-:caixa shape surfaces the more-fundamental
7508 // `MembroDuplicate` (from `view.validate()`) first; only when the
7509 // per-membros shape diagnostics pass does the cross-slot
7510 // self-membership gate fire. Mirrors the ordering pin
7511 // `supervisor_self_referential_child_is_violation` carries on
7512 // the peer supervision-tree axis (`view.validate()` runs first,
7513 // then the cross-slot gate). Without this ordering a future
7514 // refactor that swaps the two calls would silently mask the
7515 // narrower per-membro defect.
7516 use crate::{Membro, Placement, PlacementStrategy};
7517 let root = PathBuf::from("/tmp/x");
7518 let manifest = root.join("caixa.lisp");
7519 let manifest_clone = manifest.clone();
7520 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7521 let mut c = caixa(CaixaKind::Aplicacao);
7522 c.placement = Some(Placement {
7523 estrategia: PlacementStrategy::Replicated,
7524 clusters: vec!["rio".into()],
7525 affinity: None,
7526 shard_key: None,
7527 });
7528 c.membros = vec![
7529 Membro {
7530 caixa: "service-a".into(),
7531 versao: "^0.1".into(),
7532 },
7533 Membro {
7534 caixa: "service-a".into(),
7535 versao: "^0.2".into(),
7536 },
7537 Membro {
7538 caixa: "demo".into(),
7539 versao: "^0.1".into(),
7540 },
7541 ];
7542 let err = layout.verify(&c, &root).unwrap_err();
7543 let LayoutError::AplicacaoViolation { issue, .. } = err else {
7544 panic!("expected AplicacaoViolation, got {err:?}");
7545 };
7546 // The per-membros duplicate diagnostic (from view.validate())
7547 // surfaces ahead of the cross-slot self-membership gate, so the
7548 // `service-a` duplicate is named — not the `demo` self-reference.
7549 assert!(
7550 issue.contains("service-a") && issue.contains("more than once"),
7551 "duplicate-:caixa diagnostic must surface before self-membership gate, \
7552 got {issue:?}"
7553 );
7554 }
7555
7556 #[test]
7557 fn mesh_slots_on_servico_rejected() {
7558 // The canonical real-world footgun: an author adds :entrada to a
7559 // :kind Servico expecting it to expose ingress. aplicacao_view
7560 // returns None for Servico, so the slot is the manifest's
7561 // "ignored otherwise" — never validated, never rendered. The
7562 // kind-coherence gate rejects it at build time (before the
7563 // :servicos existence loop), naming the offending slot + kind.
7564 use crate::Entrada;
7565 let root = PathBuf::from("/tmp/x");
7566 let manifest = root.join("caixa.lisp");
7567 let manifest_clone = manifest.clone();
7568 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7569 let mut c = caixa(CaixaKind::Servico);
7570 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7571 c.entrada = Some(Entrada {
7572 host: "demo.example.com".into(),
7573 para: "demo".into(),
7574 paths: vec![],
7575 port: 8080,
7576 });
7577 let err = layout.verify(&c, &root).unwrap_err();
7578 match err {
7579 LayoutError::MeshSlotsOnNonAplicacao { caixa, kind, slots } => {
7580 assert_eq!(caixa, "demo");
7581 assert_eq!(kind, CaixaKind::Servico);
7582 assert_eq!(slots, crate::render::M3_AUTHOR_KEY_ENTRADA);
7583 }
7584 other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7585 }
7586 }
7587
7588 #[test]
7589 fn mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order() {
7590 // All five mesh slots declared on a Biblioteca → the diagnostic
7591 // enumerates them in canonical declaration order, deterministic
7592 // across runs. The gate fires on declared-ness only (the values
7593 // need not be a *valid* AplicacaoSpec — aplicacao_view is never
7594 // called for a non-Aplicacao kind).
7595 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
7596 let root = PathBuf::from("/tmp/x");
7597 let manifest = root.join("caixa.lisp");
7598 let manifest_clone = manifest.clone();
7599 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7600 let mut c = caixa(CaixaKind::Biblioteca);
7601 c.membros = vec![Membro {
7602 caixa: "a".into(),
7603 versao: "^0.1".into(),
7604 }];
7605 c.contratos = vec![WitContract {
7606 de: "a".into(),
7607 para: "a".into(),
7608 wit: "wasi:http/proxy".into(),
7609 endpoint: Some("/x".into()),
7610 subject: None,
7611 slot: None,
7612 }];
7613 c.politicas = Some(MeshPolicy::default());
7614 c.placement = Some(Placement {
7615 estrategia: PlacementStrategy::Replicated,
7616 clusters: vec!["rio".into()],
7617 affinity: None,
7618 shard_key: None,
7619 });
7620 c.entrada = Some(Entrada {
7621 host: "x.example.com".into(),
7622 para: "a".into(),
7623 paths: vec![],
7624 port: 8080,
7625 });
7626 let err = layout.verify(&c, &root).unwrap_err();
7627 match err {
7628 LayoutError::MeshSlotsOnNonAplicacao { slots, .. } => {
7629 assert_eq!(
7630 slots,
7631 format!(
7632 "{} {} {} {} {}",
7633 crate::render::M3_AUTHOR_KEY_MEMBROS,
7634 crate::render::M3_AUTHOR_KEY_CONTRATOS,
7635 crate::render::M3_AUTHOR_KEY_POLITICAS,
7636 crate::render::M3_AUTHOR_KEY_PLACEMENT,
7637 crate::render::M3_AUTHOR_KEY_ENTRADA,
7638 )
7639 );
7640 }
7641 other => panic!("expected MeshSlotsOnNonAplicacao, got {other:?}"),
7642 }
7643 }
7644
7645 #[test]
7646 fn servico_without_mesh_slots_still_verifies() {
7647 // Pass-after control: a well-formed Servico carrying no mesh
7648 // slots must remain accepted — the gate keys off declared-ness,
7649 // so it must not over-fire on the common case.
7650 let root = PathBuf::from("/tmp/x");
7651 let servico = root.join("servicos/demo.computeunit.yaml");
7652 let manifest = root.join("caixa.lisp");
7653 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7654 let mut c = caixa(CaixaKind::Servico);
7655 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7656 layout.verify(&c, &root).unwrap();
7657 }
7658
7659 #[test]
7660 fn supervisor_slots_on_servico_rejected() {
7661 // Mirror of `mesh_slots_on_servico_rejected` on the
7662 // supervisor-tree slot set: an author adds `:children` to a
7663 // `:kind Servico` expecting it to spawn workers. supervisor_view
7664 // returns None for Servico, so the slot is the manifest's
7665 // "ignored otherwise" — never validated, never reconciled. The
7666 // kind-coherence gate rejects it at build time (before the
7667 // :servicos existence loop), naming the offending slot + kind.
7668 use crate::{ChildSpec, RestartPolicy};
7669 let root = PathBuf::from("/tmp/x");
7670 let manifest = root.join("caixa.lisp");
7671 let manifest_clone = manifest.clone();
7672 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7673 let mut c = caixa(CaixaKind::Servico);
7674 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7675 c.children = vec![ChildSpec {
7676 caixa: "worker".into(),
7677 versao: "^0.1".into(),
7678 restart: RestartPolicy::Permanent,
7679 }];
7680 let err = layout.verify(&c, &root).unwrap_err();
7681 match err {
7682 LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7683 assert_eq!(caixa, "demo");
7684 assert_eq!(kind, CaixaKind::Servico);
7685 assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
7686 }
7687 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7688 }
7689 }
7690
7691 #[test]
7692 fn supervisor_slots_on_non_supervisor_lists_slots_in_canonical_order() {
7693 // All four supervisor slots declared on a Biblioteca → the
7694 // diagnostic enumerates them in canonical declaration order
7695 // (`:estrategia` → `:max-restarts` → `:restart-window` →
7696 // `:children`), deterministic across runs. The gate fires on
7697 // declared-ness only (the values need not be a *valid*
7698 // SupervisorSpec — supervisor_view is never called for a
7699 // non-Supervisor kind). Mirror of
7700 // `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`.
7701 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
7702 let root = PathBuf::from("/tmp/x");
7703 let manifest = root.join("caixa.lisp");
7704 let manifest_clone = manifest.clone();
7705 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7706 let mut c = caixa(CaixaKind::Biblioteca);
7707 c.estrategia = Some(RestartStrategy::OneForOne);
7708 c.max_restarts = Some(5);
7709 c.restart_window = Some("60s".into());
7710 c.children = vec![ChildSpec {
7711 caixa: "worker".into(),
7712 versao: "^0.1".into(),
7713 restart: RestartPolicy::Permanent,
7714 }];
7715 let err = layout.verify(&c, &root).unwrap_err();
7716 match err {
7717 LayoutError::SupervisorSlotsOnNonSupervisor { slots, .. } => {
7718 assert_eq!(slots, ":estrategia :max-restarts :restart-window :children");
7719 }
7720 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7721 }
7722 }
7723
7724 #[test]
7725 fn aplicacao_with_supervisor_slots_rejected() {
7726 // Cross-kind pin: an Aplicacao (the other no-code orchestrator
7727 // kind) that declares a supervisor slot is rejected by the
7728 // supervisor-slot gate, just as a Supervisor declaring a mesh
7729 // slot is rejected by the mesh-slot gate — the two kind ↔ slot
7730 // coherence gates are symmetric and mutually exclusive. The
7731 // gate fires before the Aplicacao typed-graph validation, so
7732 // the diagnostic names the foreign supervisor slot rather than
7733 // a downstream AplicacaoViolation.
7734 use crate::{Membro, Placement, PlacementStrategy, RestartStrategy};
7735 let root = PathBuf::from("/tmp/x");
7736 let manifest = root.join("caixa.lisp");
7737 let manifest_clone = manifest.clone();
7738 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7739 let mut c = caixa(CaixaKind::Aplicacao);
7740 c.membros = vec![Membro {
7741 caixa: "service-a".into(),
7742 versao: "^0.1".into(),
7743 }];
7744 c.placement = Some(Placement {
7745 estrategia: PlacementStrategy::Replicated,
7746 clusters: vec!["rio".into()],
7747 affinity: None,
7748 shard_key: None,
7749 });
7750 c.estrategia = Some(RestartStrategy::OneForAll);
7751 let err = layout.verify(&c, &root).unwrap_err();
7752 match err {
7753 LayoutError::SupervisorSlotsOnNonSupervisor { caixa, kind, slots } => {
7754 assert_eq!(caixa, "demo");
7755 assert_eq!(kind, CaixaKind::Aplicacao);
7756 assert_eq!(slots, crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
7757 }
7758 other => panic!("expected SupervisorSlotsOnNonSupervisor, got {other:?}"),
7759 }
7760 }
7761
7762 #[test]
7763 fn servico_without_supervisor_slots_still_verifies() {
7764 // Pass-after control: a well-formed Servico carrying no
7765 // supervisor slots must remain accepted — the gate keys off
7766 // declared-ness, so it must not over-fire on the common case.
7767 let root = PathBuf::from("/tmp/x");
7768 let servico = root.join("servicos/demo.computeunit.yaml");
7769 let manifest = root.join("caixa.lisp");
7770 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == servico);
7771 let mut c = caixa(CaixaKind::Servico);
7772 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7773 layout.verify(&c, &root).unwrap();
7774 }
7775
7776 #[test]
7777 fn servico_slots_on_biblioteca_rejected() {
7778 // Mirror of `mesh_slots_on_servico_rejected` /
7779 // `supervisor_slots_on_servico_rejected` on the M2
7780 // Servico-runtime slot set: an author adds `:limits` to a
7781 // `:kind Biblioteca` expecting per-process sandboxing. The
7782 // caixa-helm / caixa-flux renderers gate on `require_kind(_,
7783 // Servico)`, so the slot is the manifest's "ignored otherwise" —
7784 // never rendered into any artifact. The kind-coherence gate
7785 // rejects it at build time (before the M2 validate blocks),
7786 // naming the offending slot + kind.
7787 use crate::LimitsSpec;
7788 let root = PathBuf::from("/tmp/x");
7789 let manifest = root.join("caixa.lisp");
7790 let lib = root.join("lib").join("demo.lisp");
7791 let manifest_clone = manifest.clone();
7792 let layout =
7793 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == lib);
7794 let mut c = caixa(CaixaKind::Biblioteca);
7795 c.limits = Some(LimitsSpec {
7796 fuel: Some(1_000_000),
7797 ..Default::default()
7798 });
7799 let err = layout.verify(&c, &root).unwrap_err();
7800 match err {
7801 LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7802 assert_eq!(caixa, "demo");
7803 assert_eq!(kind, CaixaKind::Biblioteca);
7804 assert_eq!(slots, crate::render::M2_AUTHOR_KEY_LIMITS);
7805 }
7806 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7807 }
7808 }
7809
7810 #[test]
7811 fn servico_slots_on_non_servico_lists_slots_in_canonical_order() {
7812 // All three M2 slots declared on a Biblioteca → the diagnostic
7813 // enumerates them in canonical declaration order (`:limits` →
7814 // `:behavior` → `:upgrade-from`), deterministic across runs. The
7815 // gate fires on declared-ness only (the values need not pass the
7816 // M2 validate blocks — those run only after the kind-coherence
7817 // gate, and never for a non-Servico declared-slot caixa). Mirror
7818 // of the mesh/supervisor `*_lists_slots_in_canonical_order` pins.
7819 use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7820 let root = PathBuf::from("/tmp/x");
7821 let manifest = root.join("caixa.lisp");
7822 let manifest_clone = manifest.clone();
7823 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7824 let mut c = caixa(CaixaKind::Biblioteca);
7825 c.limits = Some(LimitsSpec {
7826 fuel: Some(1_000_000),
7827 ..Default::default()
7828 });
7829 c.behavior = Some(BehaviorSpec {
7830 on_init: Some(PathBuf::from("lib/init.lisp")),
7831 ..Default::default()
7832 });
7833 c.upgrade_from = vec![UpgradeFromEntry {
7834 from: "0.1.0".into(),
7835 instructions: vec![UpgradeInstruction::Restart],
7836 }];
7837 let err = layout.verify(&c, &root).unwrap_err();
7838 match err {
7839 LayoutError::ServicoSlotsOnNonServico { slots, .. } => {
7840 assert_eq!(
7841 slots,
7842 format!(
7843 "{} {} {}",
7844 crate::render::M2_AUTHOR_KEY_LIMITS,
7845 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
7846 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
7847 )
7848 );
7849 }
7850 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7851 }
7852 }
7853
7854 #[test]
7855 fn aplicacao_with_servico_slots_rejected() {
7856 // Cross-kind pin (mirror of `aplicacao_with_supervisor_slots_rejected`):
7857 // an Aplicacao that declares an M2 Servico-runtime slot is
7858 // rejected by the Servico-slot gate, just as a Supervisor
7859 // declaring a mesh slot is rejected by the mesh-slot gate — the
7860 // three kind ↔ slot coherence gates are symmetric and mutually
7861 // exclusive. The gate fires before the Aplicacao typed-graph
7862 // validation, so the diagnostic names the foreign M2 slot rather
7863 // than a downstream AplicacaoViolation about missing :membros.
7864 use crate::{Membro, Placement, PlacementStrategy, UpgradeFromEntry, UpgradeInstruction};
7865 let root = PathBuf::from("/tmp/x");
7866 let manifest = root.join("caixa.lisp");
7867 let manifest_clone = manifest.clone();
7868 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7869 let mut c = caixa(CaixaKind::Aplicacao);
7870 c.membros = vec![Membro {
7871 caixa: "service-a".into(),
7872 versao: "^0.1".into(),
7873 }];
7874 c.placement = Some(Placement {
7875 estrategia: PlacementStrategy::Replicated,
7876 clusters: vec!["rio".into()],
7877 affinity: None,
7878 shard_key: None,
7879 });
7880 c.upgrade_from = vec![UpgradeFromEntry {
7881 from: "0.1.0".into(),
7882 instructions: vec![UpgradeInstruction::Restart],
7883 }];
7884 let err = layout.verify(&c, &root).unwrap_err();
7885 match err {
7886 LayoutError::ServicoSlotsOnNonServico { caixa, kind, slots } => {
7887 assert_eq!(caixa, "demo");
7888 assert_eq!(kind, CaixaKind::Aplicacao);
7889 assert_eq!(slots, crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
7890 }
7891 other => panic!("expected ServicoSlotsOnNonServico, got {other:?}"),
7892 }
7893 }
7894
7895 #[test]
7896 fn servico_with_servico_slots_still_verifies() {
7897 // Pass-after control: a well-formed Servico carrying all three M2
7898 // slots must remain accepted — the gate is guarded by `kind !=
7899 // Servico`, so it must not over-fire on the kind these slots
7900 // exist for. Mirror of `servico_without_{mesh,supervisor}_slots_
7901 // still_verifies` on the legitimate-declaration axis.
7902 use crate::{BehaviorSpec, LimitsSpec, UpgradeFromEntry, UpgradeInstruction};
7903 let root = PathBuf::from("/tmp/x");
7904 let manifest = root.join("caixa.lisp");
7905 let svc = root.join("servicos/demo.computeunit.yaml");
7906 let init = root.join("lib/init.lisp");
7907 let layout =
7908 StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc || p == init);
7909 let mut c = caixa(CaixaKind::Servico);
7910 c.versao = "0.2.0".into();
7911 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
7912 c.limits = Some(LimitsSpec {
7913 fuel: Some(1_000_000),
7914 ..Default::default()
7915 });
7916 c.behavior = Some(BehaviorSpec {
7917 on_init: Some(PathBuf::from("lib/init.lisp")),
7918 ..Default::default()
7919 });
7920 c.upgrade_from = vec![UpgradeFromEntry {
7921 from: "0.1.0".into(),
7922 instructions: vec![UpgradeInstruction::Restart],
7923 }];
7924 layout.verify(&c, &root).unwrap();
7925 }
7926
7927 #[test]
7928 fn aplicacao_must_not_have_bibliotecas() {
7929 use crate::{Membro, Placement, PlacementStrategy};
7930 let root = PathBuf::from("/tmp/x");
7931 let manifest = root.join("caixa.lisp");
7932 let manifest_clone = manifest.clone();
7933 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7934 let mut c = caixa(CaixaKind::Aplicacao);
7935 c.bibliotecas = vec!["lib/code.lisp".into()];
7936 c.membros = vec![Membro {
7937 caixa: "x".into(),
7938 versao: "^0.1".into(),
7939 }];
7940 c.placement = Some(Placement {
7941 estrategia: PlacementStrategy::Replicated,
7942 clusters: vec!["rio".into()],
7943 affinity: None,
7944 shard_key: None,
7945 });
7946 let err = layout.verify(&c, &root).unwrap_err();
7947 assert!(matches!(err, LayoutError::AplicacaoOwnsCode(_)));
7948 }
7949
7950 #[test]
7951 fn acao_must_not_have_bibliotecas() {
7952 // Mirror of `supervisor_must_not_have_bibliotecas` /
7953 // `aplicacao_must_not_have_bibliotecas` on the third no-code
7954 // kind. `has_code` fires before the `:ci`-presence gates below
7955 // it, so this must surface `AcaoOwnsCode` even though the
7956 // caixa also lacks a `:ci` slot (which would otherwise surface
7957 // as `MissingCi`) — the more-fundamental "this kind runs no
7958 // code at all" diagnostic wins.
7959 let root = PathBuf::from("/tmp/x");
7960 let manifest = root.join("caixa.lisp");
7961 let manifest_clone = manifest.clone();
7962 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
7963 let mut c = caixa(CaixaKind::Acao);
7964 c.bibliotecas = vec!["lib/code.lisp".into()];
7965 let err = layout.verify(&c, &root).unwrap_err();
7966 assert!(matches!(err, LayoutError::AcaoOwnsCode(_)));
7967 }
7968
7969 #[test]
7970 fn acao_without_ci_errors() {
7971 // Mirror of `binario_without_exe_errors` on the fifth required-
7972 // slot axis.
7973 let root = PathBuf::from("/tmp/x");
7974 let manifest = root.join("caixa.lisp");
7975 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7976 let err = layout.verify(&caixa(CaixaKind::Acao), &root).unwrap_err();
7977 assert!(matches!(err, LayoutError::MissingCi(_)));
7978 }
7979
7980 #[test]
7981 fn acao_with_ci_passes() {
7982 let root = PathBuf::from("/tmp/x");
7983 let manifest = root.join("caixa.lisp");
7984 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest);
7985 let mut c = caixa(CaixaKind::Acao);
7986 c.ci = Some(canteiro_types::CiRun {
7987 workspace: "pleme-io".into(),
7988 repo: "caixa".into(),
7989 nodes: vec![],
7990 });
7991 layout
7992 .verify(&c, &root)
7993 .expect("an Acao caixa with a declared :ci slot passes layout verify");
7994 }
7995
7996 #[test]
7997 fn acao_with_cyclic_ci_rejected_at_layout() {
7998 // Layout-side wire-up pin on the compound
7999 // [`crate::Caixa::validate_acao_shape`] gate: a `:kind Acao`
8000 // caixa declaring a structurally illegal `:ci` (here — a
8001 // minimal two-node cycle `a → b → a`, one of the three
8002 // `canteiro_types::DecomposeError` arms
8003 // [`crate::decompose_ci`] refuses) surfaces
8004 // [`LayoutError::AcaoViolation`] at `feira build` time rather
8005 // than passing the layout gate silently and deferring the
8006 // diagnostic to [`caixa_actions::validate`] at renderer time.
8007 //
8008 // Pre-lift the layout pipeline only checked `:ci` *presence*
8009 // via [`LayoutError::MissingCi`]; the decompose gate lived
8010 // only wired open-coded at
8011 // [`caixa_actions::validate`] via the substrate-canonical
8012 // [`crate::require_acao_view`] compound helper. This wire-up
8013 // pin locks the new layout-side compound-shape gate in place
8014 // — a future regression that dropped the `if
8015 // caixa.kind().is_acao() { validate_acao_shape() }` block or
8016 // relaxed the diagnostic surface trips here at caixa-core
8017 // build time. Sibling in shape to the peer
8018 // [`aplicacao_must_not_have_bibliotecas`] /
8019 // [`supervisor_must_not_have_bibliotecas`] /
8020 // [`acao_must_not_have_bibliotecas`] layout wire-up pins on
8021 // the sibling per-kind shape gates.
8022 let root = PathBuf::from("/tmp/x");
8023 let manifest = root.join("caixa.lisp");
8024 let manifest_clone = manifest.clone();
8025 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8026 let mut c = caixa(CaixaKind::Acao);
8027 c.ci = Some(canteiro_types::CiRun {
8028 workspace: "pleme-io".into(),
8029 repo: "caixa".into(),
8030 nodes: vec![
8031 canteiro_types::CiNode::new(
8032 "a",
8033 canteiro_types::EnvClass::None,
8034 canteiro_types::ActionRef {
8035 name: "a".into(),
8036 command: "true".into(),
8037 args: vec![],
8038 },
8039 vec!["b".into()],
8040 ),
8041 canteiro_types::CiNode::new(
8042 "b",
8043 canteiro_types::EnvClass::None,
8044 canteiro_types::ActionRef {
8045 name: "b".into(),
8046 command: "true".into(),
8047 args: vec![],
8048 },
8049 vec!["a".into()],
8050 ),
8051 ],
8052 });
8053 let err = layout.verify(&c, &root).unwrap_err();
8054 match err {
8055 LayoutError::AcaoViolation { caixa, issue } => {
8056 assert_eq!(caixa, "demo");
8057 assert!(
8058 issue.contains("decompose"),
8059 "AcaoViolation issue must name the decompose axis (got: {issue:?})",
8060 );
8061 assert!(
8062 issue.contains("demo"),
8063 "AcaoViolation issue must name the offending caixa nome via the folded \
8064 CiDecomposeFailure Display (got: {issue:?})",
8065 );
8066 }
8067 other => panic!("expected AcaoViolation on a cyclic :ci, got {other:?}"),
8068 }
8069 }
8070
8071 #[test]
8072 fn ci_on_non_acao_errors() {
8073 // Mirror of `mesh_slots_on_non_aplicacao_lists_slots_in_canonical_order`
8074 // on the Acao-only `:ci` axis — declaring `:ci` on any other
8075 // kind is the same "silently ignored" footgun the sibling
8076 // mesh-/supervisor-/servico-slot gates already close.
8077 let root = PathBuf::from("/tmp/x");
8078 let manifest = root.join("caixa.lisp");
8079 let manifest_clone = manifest.clone();
8080 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8081 let mut c = caixa(CaixaKind::Biblioteca);
8082 c.ci = Some(canteiro_types::CiRun {
8083 workspace: "pleme-io".into(),
8084 repo: "caixa".into(),
8085 nodes: vec![],
8086 });
8087 let err = layout.verify(&c, &root).unwrap_err();
8088 match err {
8089 LayoutError::CiOnNonAcao { caixa, kind } => {
8090 assert_eq!(caixa, "demo");
8091 assert_eq!(kind, CaixaKind::Biblioteca);
8092 }
8093 other => panic!("expected CiOnNonAcao, got {other:?}"),
8094 }
8095 }
8096
8097 // ── ForeignCodeSlot — kind ↔ code-surface coherence ────────────────
8098
8099 #[test]
8100 fn biblioteca_with_exe_rejected() {
8101 // Fail-before-pass-after pin: a `:kind Biblioteca` declaring
8102 // `:exe` is the "I added a CLI to my library" footgun — the nix
8103 // flake renderer for Binario gates on `require_kind(_, Binario)`,
8104 // so on a Biblioteca the `:exe` path is silently dropped past
8105 // the layout's path-existence check (no executable target is
8106 // ever generated). The diagnostic names the offending kind +
8107 // slot verbatim so the author can grep their caixa.lisp for
8108 // `:exe` and fix in one edit (drop the slot or change
8109 // `:kind Biblioteca` → `:kind Binario`).
8110 let root = PathBuf::from("/tmp/x");
8111 let manifest = root.join("caixa.lisp");
8112 let lib = root.join("lib").join("demo.lisp");
8113 let exe_path = root.join("exe").join("tool");
8114 let layout = StandardLayout::new()
8115 .with_path_exists(move |p| p == manifest || p == lib || p == exe_path);
8116 let mut c = caixa(CaixaKind::Biblioteca);
8117 c.exe = vec!["exe/tool".into()];
8118 let err = layout.verify(&c, &root).unwrap_err();
8119 match err {
8120 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8121 assert_eq!(caixa, "demo");
8122 assert_eq!(kind, CaixaKind::Biblioteca);
8123 assert_eq!(slots, ":exe");
8124 }
8125 other => panic!("expected ForeignCodeSlot, got {other:?}"),
8126 }
8127 }
8128
8129 #[test]
8130 fn biblioteca_with_servicos_rejected() {
8131 // Symmetric to `biblioteca_with_exe_rejected` on the
8132 // `:servicos` axis: a `:kind Biblioteca` declaring a Servico
8133 // computeunit silently passed validate and the daemon's
8134 // ComputeUnit / lareira chart never materialized (caixa-helm /
8135 // caixa-flux gate emission on `require_kind(_, Servico)`).
8136 let root = PathBuf::from("/tmp/x");
8137 let manifest = root.join("caixa.lisp");
8138 let lib = root.join("lib").join("demo.lisp");
8139 let svc = root.join("servicos").join("demo.computeunit.yaml");
8140 let layout =
8141 StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib || p == svc);
8142 let mut c = caixa(CaixaKind::Biblioteca);
8143 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8144 let err = layout.verify(&c, &root).unwrap_err();
8145 match err {
8146 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8147 assert_eq!(caixa, "demo");
8148 assert_eq!(kind, CaixaKind::Biblioteca);
8149 assert_eq!(slots, ":servicos");
8150 }
8151 other => panic!("expected ForeignCodeSlot, got {other:?}"),
8152 }
8153 }
8154
8155 #[test]
8156 fn biblioteca_with_exe_and_servicos_lists_slots_in_canonical_order() {
8157 // Both foreign code slots declared on a Biblioteca → the
8158 // diagnostic enumerates them in canonical declaration order
8159 // (`:exe` → `:servicos`), deterministic across runs. Mirrors the
8160 // mesh/supervisor/servico-slot `*_lists_slots_in_canonical_order`
8161 // pins on the peer kind ↔ slot algebra axes; drift in the
8162 // [`Caixa::declared_foreign_code_slots`] iteration order surfaces
8163 // here.
8164 let root = PathBuf::from("/tmp/x");
8165 let manifest = root.join("caixa.lisp");
8166 let manifest_clone = manifest.clone();
8167 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8168 let mut c = caixa(CaixaKind::Biblioteca);
8169 c.exe = vec!["exe/tool".into()];
8170 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8171 let err = layout.verify(&c, &root).unwrap_err();
8172 match err {
8173 LayoutError::ForeignCodeSlot { slots, .. } => {
8174 assert_eq!(slots, ":exe :servicos");
8175 }
8176 other => panic!("expected ForeignCodeSlot, got {other:?}"),
8177 }
8178 }
8179
8180 #[test]
8181 fn binario_with_servicos_rejected() {
8182 // The peer footgun on the Binario kind: declaring a Servico
8183 // computeunit on a `:kind Binario` caixa. The caixa-helm /
8184 // caixa-flux renderers gate on `require_kind(_, Servico)`, so
8185 // the `:servicos` slot vanishes past the layout's path-
8186 // existence check — no ComputeUnit, no Helm chart. `:exe` stays
8187 // valid (Binario's native code surface), so the kind-coherence
8188 // diagnostic targets only `:servicos`.
8189 let root = PathBuf::from("/tmp/x");
8190 let manifest = root.join("caixa.lisp");
8191 let exe_path = root.join("exe").join("tool");
8192 let svc = root.join("servicos").join("demo.computeunit.yaml");
8193 let layout = StandardLayout::new()
8194 .with_path_exists(move |p| p == manifest || p == exe_path || p == svc);
8195 let mut c = caixa(CaixaKind::Binario);
8196 c.exe = vec!["exe/tool".into()];
8197 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8198 let err = layout.verify(&c, &root).unwrap_err();
8199 match err {
8200 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8201 assert_eq!(caixa, "demo");
8202 assert_eq!(kind, CaixaKind::Binario);
8203 assert_eq!(slots, ":servicos");
8204 }
8205 other => panic!("expected ForeignCodeSlot, got {other:?}"),
8206 }
8207 }
8208
8209 #[test]
8210 fn servico_with_exe_rejected() {
8211 // Symmetric to `binario_with_servicos_rejected` on the other
8212 // code-running peer: a `:kind Servico` declaring an `:exe` is
8213 // the "I added a host-side CLI to my wasm component" footgun —
8214 // the nix flake's Binario target gates on `require_kind(_,
8215 // Binario)`, so the `:exe` path vanishes past the layout's
8216 // path-existence check.
8217 let root = PathBuf::from("/tmp/x");
8218 let manifest = root.join("caixa.lisp");
8219 let svc = root.join("servicos").join("demo.computeunit.yaml");
8220 let exe_path = root.join("exe").join("tool");
8221 let layout = StandardLayout::new()
8222 .with_path_exists(move |p| p == manifest || p == svc || p == exe_path);
8223 let mut c = caixa(CaixaKind::Servico);
8224 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8225 c.exe = vec!["exe/tool".into()];
8226 let err = layout.verify(&c, &root).unwrap_err();
8227 match err {
8228 LayoutError::ForeignCodeSlot { caixa, kind, slots } => {
8229 assert_eq!(caixa, "demo");
8230 assert_eq!(kind, CaixaKind::Servico);
8231 assert_eq!(slots, ":exe");
8232 }
8233 other => panic!("expected ForeignCodeSlot, got {other:?}"),
8234 }
8235 }
8236
8237 #[test]
8238 fn binario_without_servicos_still_verifies() {
8239 // Pass-after control: a well-formed Binario carrying only its
8240 // native `:exe` surface must remain accepted — the gate keys off
8241 // declared-ness of the *foreign* slots, so it must not over-fire
8242 // on the legitimate same-kind case. Mirror of
8243 // `servico_with_servico_slots_still_verifies` on the peer axis.
8244 let root = PathBuf::from("/tmp/x");
8245 let manifest = root.join("caixa.lisp");
8246 let exe_path = root.join("exe").join("tool");
8247 let layout =
8248 StandardLayout::new().with_path_exists(move |p| p == manifest || p == exe_path);
8249 let mut c = caixa(CaixaKind::Binario);
8250 c.exe = vec!["exe/tool".into()];
8251 layout.verify(&c, &root).unwrap();
8252 }
8253
8254 #[test]
8255 fn biblioteca_with_only_bibliotecas_still_verifies() {
8256 // Pass-after control: a well-formed Biblioteca carrying only
8257 // its native `:bibliotecas` surface (or the default
8258 // `lib/<nome>.lisp`) must remain accepted. The gate keys off
8259 // declared-ness of `:exe` + `:servicos` only — `:bibliotecas`
8260 // is deliberately excluded from the foreign-set on every
8261 // code-running kind (`declared_foreign_code_slots` doc), so a
8262 // Biblioteca with the canonical lib surface alone passes.
8263 let root = PathBuf::from("/tmp/x");
8264 let manifest = root.join("caixa.lisp");
8265 let lib = root.join("lib").join("demo.lisp");
8266 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == lib);
8267 layout
8268 .verify(&caixa(CaixaKind::Biblioteca), &root)
8269 .expect("Biblioteca with default lib must verify");
8270 }
8271
8272 #[test]
8273 fn binario_with_bibliotecas_helper_still_verifies() {
8274 // Pass-after control on the deliberate `:bibliotecas`-as-helper
8275 // shape: a `:kind Binario` may legitimately bundle a `lib/`
8276 // helper its nix flake build consumes (the same shape a
8277 // `:kind Servico` may bundle for its wasm-component source).
8278 // The foreign-code-slot gate must NOT fire on `:bibliotecas` for
8279 // either code-running kind; pinned here so a future tightening
8280 // that adds `:bibliotecas` to the foreign set on Binario /
8281 // Servico surfaces as a test failure rather than as a silent
8282 // over-reach.
8283 let root = PathBuf::from("/tmp/x");
8284 let manifest = root.join("caixa.lisp");
8285 let exe_path = root.join("exe").join("tool");
8286 let lib = root.join("lib").join("helper.lisp");
8287 let layout = StandardLayout::new()
8288 .with_path_exists(move |p| p == manifest || p == exe_path || p == lib);
8289 let mut c = caixa(CaixaKind::Binario);
8290 c.exe = vec!["exe/tool".into()];
8291 c.bibliotecas = vec!["lib/helper.lisp".into()];
8292 layout.verify(&c, &root).unwrap();
8293 }
8294
8295 #[test]
8296 fn supervisor_with_exe_still_surfaces_owns_code() {
8297 // Diagnostic-precedence pin: a `:kind Supervisor` declaring
8298 // `:exe` is *both* "Supervisor with code" and "foreign code
8299 // slot". The more-fundamental `SupervisorOwnsCode` must win
8300 // (Supervisor doesn't run code at all — the foreign-slot
8301 // diagnostic would mislead the author toward changing `:kind`
8302 // when the underlying defect is that supervisors orchestrate
8303 // children, not code). Guards the call order in `verify`
8304 // against silent reordering.
8305 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8306 let root = PathBuf::from("/tmp/x");
8307 let manifest = root.join("caixa.lisp");
8308 let manifest_clone = manifest.clone();
8309 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8310 let mut c = caixa(CaixaKind::Supervisor);
8311 c.estrategia = Some(RestartStrategy::OneForOne);
8312 c.max_restarts = Some(5);
8313 c.exe = vec!["exe/tool".into()];
8314 c.children = vec![ChildSpec {
8315 caixa: "worker".into(),
8316 versao: "^0.1".into(),
8317 restart: RestartPolicy::Permanent,
8318 }];
8319 let err = layout.verify(&c, &root).unwrap_err();
8320 assert!(
8321 matches!(err, LayoutError::SupervisorOwnsCode(_)),
8322 "Supervisor-with-:exe must surface as SupervisorOwnsCode (the more-fundamental \
8323 no-code-at-all diagnostic), got {err:?}"
8324 );
8325 }
8326
8327 #[test]
8328 fn declared_foreign_code_slots_returns_canonical_order() {
8329 // Unit-level pin for the lifted method: the canonical iteration
8330 // order is `:exe` → `:servicos`, independent of which subset is
8331 // populated. Empty input + each single-slot subset + the full
8332 // pair are all checked so a future axis added to the method
8333 // (a hypothetical fifth code-surface slot) is one extension
8334 // point + one assertion update here, not a coordinated rewrite
8335 // across the layout-test sites that reach for the canonical
8336 // order.
8337 let mut c = caixa(CaixaKind::Biblioteca);
8338 assert!(c.declared_foreign_code_slots().is_empty());
8339 c.exe = vec!["exe/tool".into()];
8340 assert_eq!(c.declared_foreign_code_slots(), vec![":exe"]);
8341 c.exe.clear();
8342 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8343 assert_eq!(c.declared_foreign_code_slots(), vec![":servicos"]);
8344 c.exe = vec!["exe/tool".into()];
8345 assert_eq!(c.declared_foreign_code_slots(), vec![":exe", ":servicos"]);
8346 }
8347
8348 #[test]
8349 fn aplicacao_with_unknown_contrato_member_fails() {
8350 use crate::{Membro, Placement, PlacementStrategy, WitContract};
8351 let root = PathBuf::from("/tmp/x");
8352 let manifest = root.join("caixa.lisp");
8353 let manifest_clone = manifest.clone();
8354 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8355 let mut c = caixa(CaixaKind::Aplicacao);
8356 c.membros = vec![Membro {
8357 caixa: "service-a".into(),
8358 versao: "^0.1".into(),
8359 }];
8360 c.contratos = vec![WitContract {
8361 de: "service-a".into(),
8362 para: "phantom".into(),
8363 wit: "wasi:http/proxy".into(),
8364 endpoint: Some("/x".into()),
8365 subject: None,
8366 slot: None,
8367 }];
8368 c.placement = Some(Placement {
8369 estrategia: PlacementStrategy::Replicated,
8370 clusters: vec!["rio".into()],
8371 affinity: None,
8372 shard_key: None,
8373 });
8374 let err = layout.verify(&c, &root).unwrap_err();
8375 assert!(matches!(err, LayoutError::AplicacaoViolation { .. }));
8376 }
8377
8378 #[test]
8379 fn limits_zero_axis_surfaces_as_layout_violation() {
8380 use crate::LimitsSpec;
8381 let root = PathBuf::from("/tmp/x");
8382 let manifest = root.join("caixa.lisp");
8383 let svc = root.join("servicos/demo.computeunit.yaml");
8384 let mut c = caixa(CaixaKind::Servico);
8385 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8386 c.limits = Some(LimitsSpec {
8387 fuel: Some(0),
8388 ..Default::default()
8389 });
8390 let manifest_clone = manifest.clone();
8391 let svc_clone = svc.clone();
8392 let layout =
8393 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8394 let err = layout.verify(&c, &root).unwrap_err();
8395 let LayoutError::LimitsViolation { caixa, issue } = err else {
8396 panic!("expected LimitsViolation, got {err:?}");
8397 };
8398 assert_eq!(caixa, "demo");
8399 assert!(issue.contains(":fuel"), "issue must name the axis: {issue}");
8400 }
8401
8402 #[test]
8403 fn limits_well_formed_passes_layout() {
8404 use crate::LimitsSpec;
8405 use std::time::Duration;
8406 let root = PathBuf::from("/tmp/x");
8407 let manifest = root.join("caixa.lisp");
8408 let svc = root.join("servicos/demo.computeunit.yaml");
8409 let mut c = caixa(CaixaKind::Servico);
8410 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8411 c.limits = Some(LimitsSpec {
8412 memory: Some(64 * 1024 * 1024),
8413 fuel: Some(1_000_000),
8414 wall_clock: Some(Duration::from_secs(30)),
8415 cpu: Some(500),
8416 });
8417 let manifest_clone = manifest.clone();
8418 let svc_clone = svc.clone();
8419 let layout =
8420 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8421 layout.verify(&c, &root).unwrap();
8422 }
8423
8424 #[test]
8425 fn supervisor_with_valid_children_passes() {
8426 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
8427 let root = PathBuf::from("/tmp/x");
8428 let manifest = root.join("caixa.lisp");
8429 let manifest_clone = manifest.clone();
8430 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_clone);
8431 let mut c = caixa(CaixaKind::Supervisor);
8432 c.estrategia = Some(RestartStrategy::OneForOne);
8433 c.max_restarts = Some(5);
8434 c.children = vec![
8435 ChildSpec {
8436 caixa: "worker".into(),
8437 versao: "^0.1".into(),
8438 restart: RestartPolicy::Permanent,
8439 },
8440 ChildSpec {
8441 caixa: "cache".into(),
8442 versao: "^0.1".into(),
8443 restart: RestartPolicy::Transient,
8444 },
8445 ];
8446 layout.verify(&c, &root).unwrap();
8447 }
8448
8449 // ── :upgrade-from entry validation pipes through layout ─────────────
8450
8451 #[test]
8452 fn upgrade_invalid_module_surfaces_as_layout_violation() {
8453 // End-to-end pin that
8454 // [`crate::UpgradeFromEntry::validate`] runs *inside*
8455 // `LayoutInvariants::verify` and surfaces value-shape
8456 // violations through the new `UpgradeViolation` arm
8457 // (parallel to `BehaviorViolation`, `LimitsViolation`,
8458 // `SupervisorViolation`, `AplicacaoViolation`). Until this
8459 // wiring landed the entry validator was unreachable from any
8460 // build-pipeline caller — an `:upgrade-from
8461 // ((:from "0.1.0" :instructions ((:load-module "Hello")))` (uppercase
8462 // module name the K8s apiserver would reject on the per-
8463 // ComputeUnit `metadata.name` axis) silently passed
8464 // `feira lint` / `feira build` and surfaced only at wasm-engine
8465 // hot-upgrade time as a per-backend "module not found" /
8466 // `code:load_module/1` `badarg` runtime error, far from the
8467 // source caixa.lisp. Pinning the wiring here so a future
8468 // refactor that drops the `entry.validate()` call surfaces as
8469 // a build-pipeline regression at this test, not as a runtime
8470 // surprise per consumer.
8471 use crate::{UpgradeFromEntry, UpgradeInstruction};
8472 use std::path::PathBuf;
8473 let root = PathBuf::from("/tmp/x");
8474 let manifest = root.join("caixa.lisp");
8475 let svc = root.join("servicos/demo.computeunit.yaml");
8476 let mut c = caixa(CaixaKind::Servico);
8477 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8478 c.upgrade_from = vec![UpgradeFromEntry {
8479 from: "0.1.0".into(),
8480 instructions: vec![UpgradeInstruction::LoadModule {
8481 module: "Hello".into(), // uppercase — not DNS-1123
8482 }],
8483 }];
8484 let manifest_clone = manifest.clone();
8485 let svc_clone = svc.clone();
8486 let layout =
8487 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8488 let err = layout.verify(&c, &root).unwrap_err();
8489 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8490 panic!("expected UpgradeViolation, got {err:?}");
8491 };
8492 assert_eq!(caixa, "demo");
8493 assert!(
8494 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8495 "issue must name the lisp-form of the offending instruction: {issue}"
8496 );
8497 assert!(
8498 issue.contains("Hello"),
8499 "issue must name the offending :module verbatim: {issue}"
8500 );
8501 }
8502
8503 #[test]
8504 fn upgrade_empty_module_surfaces_as_layout_violation() {
8505 // Companion to the DNS-1123 footgun above on the narrower
8506 // empty arm. Every Module-bearing variant's empty value
8507 // reaches the layout pipeline through the kind-tagged
8508 // `ModuleEmpty` diagnostic naming its lisp-form.
8509 use crate::{UpgradeFromEntry, UpgradeInstruction};
8510 use std::path::PathBuf;
8511 let root = PathBuf::from("/tmp/x");
8512 let manifest = root.join("caixa.lisp");
8513 let svc = root.join("servicos/demo.computeunit.yaml");
8514 let mut c = caixa(CaixaKind::Servico);
8515 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8516 c.upgrade_from = vec![UpgradeFromEntry {
8517 from: "0.1.0".into(),
8518 instructions: vec![UpgradeInstruction::SoftPurge {
8519 module: String::new(),
8520 }],
8521 }];
8522 let manifest_clone = manifest.clone();
8523 let svc_clone = svc.clone();
8524 let layout =
8525 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8526 let err = layout.verify(&c, &root).unwrap_err();
8527 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8528 panic!("expected UpgradeViolation, got {err:?}");
8529 };
8530 assert_eq!(caixa, "demo");
8531 assert!(
8532 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_SOFT_PURGE),
8533 "issue must name the lisp-form of the empty instruction: {issue}"
8534 );
8535 }
8536
8537 #[test]
8538 fn upgrade_invalid_state_change_script_surfaces_as_layout_violation() {
8539 // Pins that the b0c8389 script value-shape gates
8540 // (AbsoluteScript / ParentEscapeScript) — previously
8541 // unreachable from any build-pipeline caller — now fire
8542 // through the same `UpgradeViolation` arm before the path-
8543 // existence pass would otherwise emit the less-helpful
8544 // "missing upgrade-script" (or, worse, *succeed* against
8545 // /etc/passwd, proving the sandbox bypass — same defect
8546 // the b0c8389 BehaviorSpec wiring closed on the peer M2
8547 // slot).
8548 use crate::{UpgradeFromEntry, UpgradeInstruction};
8549 use std::path::PathBuf;
8550 let root = PathBuf::from("/tmp/x");
8551 let manifest = root.join("caixa.lisp");
8552 let svc = root.join("servicos/demo.computeunit.yaml");
8553 let etc_passwd = PathBuf::from("/etc/passwd");
8554 let mut c = caixa(CaixaKind::Servico);
8555 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8556 c.upgrade_from = vec![UpgradeFromEntry {
8557 from: "0.1.0".into(),
8558 instructions: vec![UpgradeInstruction::StateChange {
8559 script: PathBuf::from("/etc/passwd"),
8560 }],
8561 }];
8562 let manifest_clone = manifest.clone();
8563 let svc_clone = svc.clone();
8564 let etc_passwd_clone = etc_passwd.clone();
8565 // Critically: /etc/passwd "exists" in our mock — without the
8566 // value-shape pre-check, the existence loop would *succeed*
8567 // and the path-traversal exit from the project sandbox would
8568 // pass `feira build` silently.
8569 let layout = StandardLayout::new().with_path_exists(move |p| {
8570 p == manifest_clone || p == svc_clone || p == etc_passwd_clone
8571 });
8572 let err = layout.verify(&c, &root).unwrap_err();
8573 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8574 panic!("expected UpgradeViolation, got {err:?}");
8575 };
8576 assert_eq!(caixa, "demo");
8577 assert!(
8578 issue.contains("absolute") || issue.contains("Absolute"),
8579 "issue must name the violation kind (absolute): {issue}"
8580 );
8581 }
8582
8583 #[test]
8584 fn upgrade_well_formed_passes_layout() {
8585 // Positive control — every documented authoring shape
8586 // (`:load-module`, `:state-change` with a relative path,
8587 // `:soft-purge`, `:purge`, sole `:restart`) passes the wired
8588 // gate. The typed sequence (`:load-module` → `:state-change`
8589 // → `:soft-purge` → `:purge`) lives in one entry; the sole
8590 // `:restart` fallback lives in a *separate* entry on a
8591 // different `:from` (the within-entry restart-exclusivity
8592 // gate added in this commit rejects mixing the fallback with
8593 // the typed sequence — per the UpgradeInstruction::Restart
8594 // doc, `:restart` is terminal and any other instructions in
8595 // the same entry are dead code). Drift here = a future
8596 // tighten that rejects any canonical shape surfaces as a
8597 // regression at this layout-level pin, not piecemeal across
8598 // per-renderer call sites.
8599 //
8600 // `:soft-purge` and `:purge` target *distinct* old-version
8601 // modules (`hello-rio-old` and `hello-rio-oldest`) so the
8602 // within-entry cleanup-singularity gate
8603 // (`UpgradeError::DuplicateCleanup`) passes — that gate
8604 // rejects more than one cleanup per module per entry (one
8605 // semantic per old version; mixing drain + discard on one
8606 // module is the soft-then-hard fallback footgun the author
8607 // shouldn't write because the operator handles cleanup
8608 // failure escalation itself). The two distinct names cover
8609 // the legitimate "drain a recent old, hard-discard an
8610 // older-still" shape — both authoring forms remain load-
8611 // bearing in this positive-control enumeration.
8612 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
8613 use std::path::PathBuf;
8614 let root = PathBuf::from("/tmp/x");
8615 let manifest = root.join("caixa.lisp");
8616 let svc = root.join("servicos/demo.computeunit.yaml");
8617 let migration = root.join("lib/migrations/v01-to-v02.lisp");
8618 let on_state_change = root.join("lib/migrations.lisp");
8619 let mut c = caixa(CaixaKind::Servico);
8620 // `:versao` past both entries' `:from` so the cross-slot
8621 // precedence gate (`FromNotBeforeVersao`) lets this canonical
8622 // authoring shape through to the positive-control assertion.
8623 c.versao = "0.2.0".into();
8624 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8625 // `:on-state-change` declared alongside the `(:state-change …)`
8626 // instruction below — the cross-slot composition gate
8627 // (`validate_upgrade_from_against_behavior`) rejects a
8628 // `:state-change` without the callback, so the canonical
8629 // authoring shape this positive control pins now includes the
8630 // runtime delivery hook (the `gen_server:code_change/3` analog
8631 // that the per-version script is invoked through during hot
8632 // upgrade per the upgrade.rs module doc "Composes with"
8633 // promise).
8634 c.behavior = Some(BehaviorSpec {
8635 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
8636 ..Default::default()
8637 });
8638 c.upgrade_from = vec![
8639 UpgradeFromEntry {
8640 from: "0.1.0".into(),
8641 instructions: vec![
8642 UpgradeInstruction::LoadModule {
8643 module: "hello-rio".into(),
8644 },
8645 UpgradeInstruction::StateChange {
8646 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
8647 },
8648 UpgradeInstruction::SoftPurge {
8649 module: "hello-rio-old".into(),
8650 },
8651 UpgradeInstruction::Purge {
8652 module: "hello-rio-oldest".into(),
8653 },
8654 ],
8655 },
8656 UpgradeFromEntry {
8657 from: "0.0.9".into(),
8658 instructions: vec![UpgradeInstruction::Restart],
8659 },
8660 ];
8661 let manifest_clone = manifest.clone();
8662 let svc_clone = svc.clone();
8663 let migration_clone = migration.clone();
8664 let on_state_change_clone = on_state_change.clone();
8665 let layout = StandardLayout::new().with_path_exists(move |p| {
8666 p == manifest_clone
8667 || p == svc_clone
8668 || p == migration_clone
8669 || p == on_state_change_clone
8670 });
8671 layout.verify(&c, &root).unwrap();
8672 }
8673
8674 #[test]
8675 fn upgrade_from_restart_mixed_surfaces_as_upgrade_violation() {
8676 // Wiring pin: the within-entry `(:restart)`-exclusivity gate
8677 // (`UpgradeFromEntry::validate_restart_exclusive`) lands on
8678 // the same `LayoutError::UpgradeViolation` axis the per-entry
8679 // shape gate (26da2c7), the cross-entry duplicate-`:from`
8680 // gate (7c6aef2), and the cross-slot `:from < :versao`
8681 // precedence gate (de7ab1a) already do. A caixa.lisp whose
8682 // `:upgrade-from` entry mixes `(:restart)` with a typed
8683 // instruction surfaces at `feira build` time naming the
8684 // offending caixa + the entry's `:from` rather than silently
8685 // passing into the wasm-operator with semantically dead code
8686 // in the operator's dispatch table. Mirrors
8687 // `upgrade_from_duplicate_surfaces_as_upgrade_violation` on
8688 // the peer cross-entry gate.
8689 use crate::{UpgradeFromEntry, UpgradeInstruction};
8690 let root = PathBuf::from("/tmp/x");
8691 let manifest = root.join("caixa.lisp");
8692 let svc = root.join("servicos/demo.computeunit.yaml");
8693 let mut c = caixa(CaixaKind::Servico);
8694 c.versao = "0.2.0".into();
8695 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8696 c.upgrade_from = vec![UpgradeFromEntry {
8697 from: "0.1.0".into(),
8698 instructions: vec![
8699 UpgradeInstruction::LoadModule {
8700 module: "hello-rio".into(),
8701 },
8702 UpgradeInstruction::Restart,
8703 ],
8704 }];
8705 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8706 let err = layout.verify(&c, &root).unwrap_err();
8707 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8708 panic!("expected LayoutError::UpgradeViolation for restart-mixed entry, got {err:?}");
8709 };
8710 assert_eq!(caixa, "demo");
8711 assert!(
8712 issue.contains("0.1.0"),
8713 "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8714 {issue:?}"
8715 );
8716 assert!(
8717 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8718 "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8719 );
8720 assert!(
8721 issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_LOAD_MODULE),
8722 "UpgradeViolation issue must name the non-:restart peer instruction's lisp-form \
8723 verbatim, got {issue:?}"
8724 );
8725 }
8726
8727 #[test]
8728 fn upgrade_from_restart_duplicated_surfaces_as_upgrade_violation() {
8729 // Companion arm: the duplicate-`(:restart)` mode of
8730 // `RestartNotExclusive` (no typed peers, just multiple
8731 // `Restart` variants) surfaces through the same wiring as the
8732 // mixed-with-typed mode above. The diagnostic still names the
8733 // offending entry's `:from` verbatim even when `other_kinds`
8734 // is empty.
8735 use crate::{UpgradeFromEntry, UpgradeInstruction};
8736 let root = PathBuf::from("/tmp/x");
8737 let manifest = root.join("caixa.lisp");
8738 let svc = root.join("servicos/demo.computeunit.yaml");
8739 let mut c = caixa(CaixaKind::Servico);
8740 c.versao = "0.2.0".into();
8741 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8742 c.upgrade_from = vec![UpgradeFromEntry {
8743 from: "0.1.0".into(),
8744 instructions: vec![UpgradeInstruction::Restart, UpgradeInstruction::Restart],
8745 }];
8746 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest || p == svc);
8747 let err = layout.verify(&c, &root).unwrap_err();
8748 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8749 panic!(
8750 "expected LayoutError::UpgradeViolation for duplicate-restart entry, got \
8751 {err:?}"
8752 );
8753 };
8754 assert_eq!(caixa, "demo");
8755 assert!(
8756 issue.contains("0.1.0"),
8757 "UpgradeViolation issue must name the offending entry's `:from` verbatim, got \
8758 {issue:?}"
8759 );
8760 assert!(
8761 issue.contains("(:restart)")
8762 || issue.contains(crate::render::M2_UPGRADE_INSTRUCTION_KIND_RESTART),
8763 "UpgradeViolation issue must name the `:restart` axis verbatim, got {issue:?}"
8764 );
8765 }
8766
8767 #[test]
8768 fn upgrade_from_invalid_surfaces_as_layout_violation() {
8769 // The `:from` semver gate (`UpgradeError::FromInvalid`)
8770 // was likewise unreachable before this wiring landed — a
8771 // typo-shaped `:from "v0.1.0"` (git-tag-shape leaking into
8772 // the semver slot) silently passed `feira build` and
8773 // surfaced only when the operator's hot-upgrade decision
8774 // engine tried to match against the version key it couldn't
8775 // parse. Now wired through `UpgradeViolation` with the
8776 // peer-shaped `{ from, reason }` payload — the
8777 // parser-shaped `reason` flows through `Display` so the
8778 // wrapped issue string carries both the offending value
8779 // *and* the SemVer-2 parser's wording (peer with the
8780 // `VersaoInvalid` / `MembroVersaoInvalid` envelopes on the
8781 // sibling SemVer-2 axes).
8782 use crate::{UpgradeFromEntry, UpgradeInstruction};
8783 use std::path::PathBuf;
8784 let root = PathBuf::from("/tmp/x");
8785 let manifest = root.join("caixa.lisp");
8786 let svc = root.join("servicos/demo.computeunit.yaml");
8787 let mut c = caixa(CaixaKind::Servico);
8788 c.servicos = vec!["servicos/demo.computeunit.yaml".into()];
8789 c.upgrade_from = vec![UpgradeFromEntry {
8790 from: "v0.1.0".into(), // git-tag-shape, not semver
8791 instructions: vec![UpgradeInstruction::Restart],
8792 }];
8793 let manifest_clone = manifest.clone();
8794 let svc_clone = svc.clone();
8795 let layout =
8796 StandardLayout::new().with_path_exists(move |p| p == manifest_clone || p == svc_clone);
8797 let err = layout.verify(&c, &root).unwrap_err();
8798 let LayoutError::UpgradeViolation { caixa, issue } = err else {
8799 panic!("expected UpgradeViolation, got {err:?}");
8800 };
8801 assert_eq!(caixa, "demo");
8802 assert!(
8803 issue.contains("v0.1.0"),
8804 "UpgradeViolation issue must name the offending :from value verbatim, got {issue:?}"
8805 );
8806 assert!(
8807 issue.contains(":from"),
8808 "UpgradeViolation issue must name the :from slot verbatim, got {issue:?}"
8809 );
8810 // Pin the parser-shaped reason flow-through: the renamed
8811 // `FromInvalid { from, reason }` carries the SemVer-2 parser's
8812 // wording verbatim, and the [`UpgradeError`] Display routes it
8813 // into the wrapped `issue` string so the layout envelope
8814 // surfaces both the offending value *and* the parser's
8815 // diagnosis. Mirrors the peer flow-through on
8816 // `ManifestError::VersaoInvalid` (top-level `:versao`) and
8817 // `AplicacaoError::MembroVersaoInvalid` (`:membros :versao`).
8818 assert!(
8819 issue.contains("SemVer-2"),
8820 "UpgradeViolation issue must carry the parser-shaped reason (\"SemVer-2\"), got {issue:?}"
8821 );
8822 }
8823
8824 #[test]
8825 fn missing_lib_gate_routes_through_kind_requires_lib_and_caixa_nome() {
8826 // Fail-before-pass-after pin on the two-part converge landed
8827 // at layout.rs:844-847:
8828 // (a) `caixa.kind().is_biblioteca()` →
8829 // `caixa.kind().requires_lib()` — routes the biblioteca
8830 // required-slot gate onto the same `requires_*()`
8831 // predicate family the three sibling required-slot
8832 // gates (`requires_exe()` at :856, `requires_servicos()`
8833 // at :860, `requires_ci()` at :874) already key off.
8834 // All four gates in the block now share one convention;
8835 // a future kind that gains its own required-slot gate
8836 // (an M4/M5 typed arm the CAIXA-SDLC §I six-kind roster
8837 // may grow) reaches for the same predicate family and
8838 // inherits the accessor discipline for free.
8839 // (b) raw `caixa.nome` → `caixa.nome()` — routes the
8840 // `expected` path composition through the typed
8841 // [`crate::Caixa::nome`] accessor, closing the last
8842 // unlifted raw `caixa.nome` production field-access
8843 // site in `caixa-core/src/layout.rs` (every peer
8844 // diagnostic in the file already routes through
8845 // `caixa.nome().to_string()`).
8846 //
8847 // The behavioral pin: for a Biblioteca kind with no fallback
8848 // `lib/<nome>.lisp` file, MissingLib fires and its `expected`
8849 // path composes through `Caixa::nome()`; for every other
8850 // kind, MissingLib does NOT fire (the gate short-circuits on
8851 // kinds where `requires_lib()` returns false), even when the
8852 // fallback file is likewise absent. A future regression that
8853 // reroutes the gate off `requires_lib()` (e.g. onto
8854 // `is_biblioteca()` again, or onto a hand-authored
8855 // `matches!(caixa.kind(), CaixaKind::Biblioteca)`) that
8856 // *happens* to agree byte-for-byte on today's arm-set trips
8857 // this test the moment a future kind's `requires_lib()`
8858 // returns true for a non-`Biblioteca` arm (or the sibling
8859 // required-slot gates diverge from the same convention).
8860 let root = PathBuf::from("/tmp/x");
8861 let manifest = root.join("caixa.lisp");
8862 let manifest_only = manifest.clone();
8863 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
8864
8865 // Biblioteca kind + no lib fallback → MissingLib fires with
8866 // the expected path composed through `Caixa::nome()`.
8867 let bib = caixa(CaixaKind::Biblioteca);
8868 assert!(
8869 bib.kind().requires_lib(),
8870 "requires_lib() must return true for Biblioteca — the four-required-\
8871 slot-gate family's routing depends on this arm's assignment"
8872 );
8873 let err = layout.verify(&bib, &root).unwrap_err();
8874 let LayoutError::MissingLib {
8875 caixa: cname,
8876 expected,
8877 } = err
8878 else {
8879 panic!("expected MissingLib for Biblioteca kind with no lib fallback, got {err:?}");
8880 };
8881 assert_eq!(
8882 cname,
8883 bib.nome(),
8884 "MissingLib `caixa:` carrier must byte-equal Caixa::nome()"
8885 );
8886 assert_eq!(
8887 expected,
8888 root.join(crate::render::LAYOUT_DIR_LIB)
8889 .join(format!("{}.lisp", bib.nome())),
8890 "MissingLib `expected:` path must compose through Caixa::nome() \
8891 verbatim — a raw-field-access regression would silently drift \
8892 the composed path on any future `:nome` axis extension \
8893 (namespace-qualified rewrite, per-cluster alias overlay)"
8894 );
8895
8896 // Non-Biblioteca kinds → the MissingLib gate short-circuits.
8897 // Different kinds fail on their own required-slot gate
8898 // (BinarioWithoutExe, ServicoWithoutServicos, MissingCi) or
8899 // on downstream M2/M3 invariants; none of them may surface as
8900 // MissingLib, because `requires_lib()` returns false for each.
8901 for kind in [
8902 CaixaKind::Binario,
8903 CaixaKind::Servico,
8904 CaixaKind::Supervisor,
8905 CaixaKind::Aplicacao,
8906 CaixaKind::Acao,
8907 ] {
8908 assert!(
8909 !kind.requires_lib(),
8910 "requires_lib() must return false for {kind:?} — the \
8911 four-required-slot-gate family's arm assignment pins \
8912 exactly one kind (Biblioteca) as the arm that requires \
8913 a `lib/` entry"
8914 );
8915 let c = caixa(kind);
8916 let result = layout.verify(&c, &root);
8917 assert!(
8918 !matches!(result, Err(LayoutError::MissingLib { .. })),
8919 "MissingLib gate at layout.rs:844 must short-circuit for \
8920 kinds where requires_lib() returns false; unexpectedly \
8921 fired for {kind:?}: {result:?}"
8922 );
8923 }
8924 }
8925
8926 #[test]
8927 fn missing_lib_ctor_matches_struct_literal_wrap() {
8928 // Equivalence pin locking [`LayoutError::missing_lib`] to its
8929 // struct-literal peer under PartialEq. The pre-lift wire-up at
8930 // layout.rs:1010 read `LayoutError::MissingLib { caixa:
8931 // caixa.nome().to_string(), expected }`; the post-lift
8932 // dispatch reads `LayoutError::missing_lib(caixa, expected)`.
8933 // Both must produce byte-equal variants — a silent divergence
8934 // (a `to_lowercase()`, a `trim()`, a lost `.to_string()` copy,
8935 // an accidental `.clone()` of the wrong side of the `expected`
8936 // path) surfaces here rather than at a downstream diagnostic-
8937 // shape drift.
8938 let bib = caixa(CaixaKind::Biblioteca);
8939 let expected = PathBuf::from("/tmp/x")
8940 .join(crate::render::LAYOUT_DIR_LIB)
8941 .join(format!("{}.lisp", bib.nome()));
8942 let struct_lit = LayoutError::MissingLib {
8943 caixa: bib.nome().to_string(),
8944 expected: expected.clone(),
8945 };
8946 let ctor = LayoutError::missing_lib(&bib, expected);
8947 assert_eq!(
8948 struct_lit, ctor,
8949 "missing_lib ctor must byte-equal the pre-lift struct-literal"
8950 );
8951 }
8952
8953 #[test]
8954 fn missing_lib_ctor_projects_nome_through_accessor() {
8955 // Accessor-fidelity pin: any future `:nome` axis extension
8956 // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
8957 // alias overlay, case-normalization pass) that lands on
8958 // [`crate::Caixa::nome`] must reach the `caixa:` carrier
8959 // through this projection rather than a raw field access.
8960 // The neighbour required-slot ctor family
8961 // ([`layout_nome_only_ctors!`]) projects the same way; this
8962 // pin locks `missing_lib` onto the same discipline so the
8963 // whole `LayoutError` family stays coherent under any future
8964 // `:nome` rewrite.
8965 //
8966 // Deliberately uses a byte-distinctive nome ("named-lib") so
8967 // a regression that hard-codes a fixture literal at the ctor
8968 // body (rather than projecting through the accessor) drops
8969 // the bytes and trips the assertion.
8970 let mut bib = caixa(CaixaKind::Biblioteca);
8971 bib.nome = "named-lib".into();
8972 let expected = PathBuf::from("/srv")
8973 .join(crate::render::LAYOUT_DIR_LIB)
8974 .join(format!("{}.lisp", bib.nome()));
8975 let err = LayoutError::missing_lib(&bib, expected.clone());
8976 let LayoutError::MissingLib {
8977 caixa: cname,
8978 expected: got,
8979 } = err
8980 else {
8981 panic!("missing_lib ctor must construct the MissingLib variant, got a foreign arm");
8982 };
8983 assert_eq!(
8984 cname,
8985 bib.nome(),
8986 "missing_lib `caixa:` carrier must project through Caixa::nome()"
8987 );
8988 assert_eq!(
8989 got, expected,
8990 "missing_lib `expected:` path must pass through verbatim"
8991 );
8992 }
8993
8994 #[test]
8995 fn missing_lib_verify_wire_up_routes_through_ctor() {
8996 // Behavioural pin: [`StandardLayout::verify`] must reach the
8997 // `MissingLib` variant through the newly lifted ctor rather
8998 // than a residual struct-literal block. The end-to-end
8999 // observable — a Biblioteca with no lib fallback — must
9000 // surface a `MissingLib` whose `caixa:` and `expected:`
9001 // carriers are byte-equal to what the ctor would produce
9002 // when called directly.
9003 let root = PathBuf::from("/opt/pkg");
9004 let manifest = root.join("caixa.lisp");
9005 let manifest_only = manifest.clone();
9006 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
9007 let bib = caixa(CaixaKind::Biblioteca);
9008 let expected = root
9009 .join(crate::render::LAYOUT_DIR_LIB)
9010 .join(format!("{}.lisp", bib.nome()));
9011
9012 let observed = layout.verify(&bib, &root).unwrap_err();
9013 let synthesized = LayoutError::missing_lib(&bib, expected);
9014 assert_eq!(
9015 observed, synthesized,
9016 "StandardLayout::verify must reach MissingLib through the missing_lib ctor \
9017 — a residual struct-literal block would silently diverge on any future \
9018 accessor projection change"
9019 );
9020 }
9021
9022 #[test]
9023 fn ci_on_non_acao_ctor_matches_struct_literal_wrap() {
9024 // Equivalence pin locking [`LayoutError::ci_on_non_acao`] to
9025 // its struct-literal peer under `PartialEq`. The pre-lift
9026 // wire-up at `caixa-core/src/manifest.rs:5586` read
9027 // `LayoutError::CiOnNonAcao { caixa: self.nome().to_string(),
9028 // kind: self.kind() }`; the post-lift dispatch reads
9029 // `LayoutError::ci_on_non_acao(self)`. Both must produce
9030 // byte-equal variants — a silent divergence (a `to_lowercase()`,
9031 // a lost `.to_string()` copy, a swapped kind-copy) surfaces
9032 // here rather than at a downstream diagnostic-shape drift.
9033 let bib = caixa(CaixaKind::Biblioteca);
9034 let struct_lit = LayoutError::CiOnNonAcao {
9035 caixa: bib.nome().to_string(),
9036 kind: bib.kind(),
9037 };
9038 let ctor = LayoutError::ci_on_non_acao(&bib);
9039 assert_eq!(
9040 struct_lit, ctor,
9041 "ci_on_non_acao ctor must byte-equal the pre-lift struct-literal"
9042 );
9043 }
9044
9045 #[test]
9046 fn ci_on_non_acao_ctor_projects_nome_and_kind_through_accessors() {
9047 // Accessor-fidelity pin: any future `:nome` axis extension
9048 // (namespace-qualified rewrite `pleme-io/<nome>`, per-cluster
9049 // alias overlay, case-normalization pass) that lands on
9050 // [`crate::Caixa::nome`] and any future `:kind` re-projection
9051 // (an overlay pass returning a different `CaixaKind` copy)
9052 // that lands on [`crate::Caixa::kind`] must reach the
9053 // `caixa:` / `kind:` carriers through these projections
9054 // rather than raw field accesses. The neighbour
9055 // [`LayoutError::missing_lib`] ctor (b4d5a49) projects nome
9056 // the same way; this pin locks the `:ci` axis onto the same
9057 // discipline so the whole `LayoutError` family stays coherent
9058 // under any future rewrite.
9059 //
9060 // Deliberately uses a byte-distinctive nome ("ci-on-binario")
9061 // and a non-Acao kind (`Binario`) so a regression that
9062 // hard-codes a fixture literal at the ctor body (rather than
9063 // projecting through the accessors) drops the bytes or the
9064 // kind and trips the assertion.
9065 let mut bin = caixa(CaixaKind::Binario);
9066 bin.nome = "ci-on-binario".into();
9067 let err = LayoutError::ci_on_non_acao(&bin);
9068 let LayoutError::CiOnNonAcao {
9069 caixa: cname,
9070 kind: cknd,
9071 } = err
9072 else {
9073 panic!("ci_on_non_acao ctor must construct the CiOnNonAcao variant, got a foreign arm");
9074 };
9075 assert_eq!(
9076 cname,
9077 bin.nome(),
9078 "ci_on_non_acao `caixa:` carrier must project through Caixa::nome()"
9079 );
9080 assert_eq!(
9081 cknd,
9082 bin.kind(),
9083 "ci_on_non_acao `kind:` carrier must project through Caixa::kind()"
9084 );
9085 }
9086
9087 #[test]
9088 fn ci_on_non_acao_verify_wire_up_routes_through_ctor() {
9089 // Behavioural pin: [`StandardLayout::verify`] must reach the
9090 // `CiOnNonAcao` variant through the newly lifted ctor rather
9091 // than a residual struct-literal block. The end-to-end
9092 // observable — a non-Acao caixa with `:ci` declared — must
9093 // surface a `CiOnNonAcao` whose `caixa:` and `kind:` carriers
9094 // are byte-equal to what the ctor would produce when called
9095 // directly.
9096 let root = PathBuf::from("/tmp/x");
9097 let manifest = root.join("caixa.lisp");
9098 let manifest_only = manifest.clone();
9099 let layout = StandardLayout::new().with_path_exists(move |p| p == manifest_only);
9100 let mut bib = caixa(CaixaKind::Biblioteca);
9101 bib.ci = Some(canteiro_types::CiRun {
9102 workspace: "pleme-io".into(),
9103 repo: "caixa".into(),
9104 nodes: vec![],
9105 });
9106
9107 let observed = layout.verify(&bib, &root).unwrap_err();
9108 let synthesized = LayoutError::ci_on_non_acao(&bib);
9109 assert_eq!(
9110 observed, synthesized,
9111 "Caixa::validate_ci_kind_coherence must reach CiOnNonAcao through the \
9112 ci_on_non_acao ctor — a residual struct-literal block would silently \
9113 diverge on any future accessor projection change"
9114 );
9115 }
9116}