caixa_core/manifest.rs
1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use tatara_lisp::DeriveTataraDomain;
5
6use thiserror::Error;
7
8use crate::{
9 CaixaKind, Dep,
10 behavior::BehaviorSpec,
11 dep::DepError,
12 limits::LimitsSpec,
13 render::{
14 PathShapeViolation, is_computeunit_yaml_extension, is_git_repo_url, is_lisp_extension,
15 is_sandboxed_relative_path,
16 },
17 supervisor::SupervisorSpec,
18 upgrade::UpgradeFromEntry,
19};
20
21/// Top-level manifest for a caixa (a tatara-lisp package).
22///
23/// Authored as `caixa.lisp`:
24///
25/// ```lisp
26/// (defcaixa
27/// :nome "pangea-tatara-aws"
28/// :versao "0.1.0"
29/// :kind Biblioteca
30/// :edicao "2026"
31/// :descricao "AWS provider caixa for tatara-lisp"
32/// :repositorio "github:pleme-io/pangea-tatara-aws"
33/// :licenca "MIT"
34/// :autores ("pleme-io")
35/// :etiquetas ("iac" "aws" "pangea")
36/// :deps ((:nome "caixa-teia" :versao "^0.1")
37/// (:nome "iac-forge-ir" :versao "^0.5"))
38/// :deps-dev ((:nome "tatara-check" :versao "*"))
39/// :bibliotecas ("lib/pangea-tatara-aws.lisp"))
40/// ```
41///
42/// Because `Caixa` derives [`tatara_lisp::domain::TataraDomain`], the manifest
43/// is parsed directly by the tatara-lisp compiler — an ill-formed manifest is
44/// a compile error, not a runtime error.
45#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone, PartialEq)]
46#[serde(rename_all = "camelCase")]
47#[tatara(keyword = "defcaixa")]
48pub struct Caixa {
49 /// Package name — the canonical string used in `:deps`, the registry, and
50 /// the default lib/exe entry names.
51 pub nome: String,
52
53 /// Package version — a semver literal like `"0.1.0"`. Parsed lazily via
54 /// [`crate::CaixaVersion::parse`].
55 pub versao: String,
56
57 /// What this caixa produces. See [`CaixaKind`].
58 pub kind: CaixaKind,
59
60 /// Language edition — determines macro surface + compatibility flags.
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub edicao: Option<String>,
63
64 /// Free-form description shown in the registry listing.
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub descricao: Option<String>,
67
68 /// Homepage or repo URL.
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub repositorio: Option<String>,
71
72 /// SPDX license expression — `"MIT"`, `"Apache-2.0 OR MIT"`, etc.
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub licenca: Option<String>,
75
76 /// Authors — free-form strings.
77 #[serde(default)]
78 pub autores: Vec<String>,
79
80 /// Topical tags used for registry search.
81 #[serde(default)]
82 pub etiquetas: Vec<String>,
83
84 /// Runtime dependencies.
85 #[serde(default)]
86 pub deps: Vec<Dep>,
87
88 /// Development-only dependencies (tests, lint, bench).
89 #[serde(default)]
90 pub deps_dev: Vec<Dep>,
91
92 /// Paths to executable entry points (relative to the package root).
93 /// Required when `:kind Binario`.
94 #[serde(default)]
95 pub exe: Vec<String>,
96
97 /// Paths to library entry points (relative to the package root).
98 /// First entry is the canonical `lib/<nome>.lisp`; when omitted under
99 /// `:kind Biblioteca`, the layout check expects `lib/<nome>.lisp`.
100 #[serde(default)]
101 pub bibliotecas: Vec<String>,
102
103 /// Paths to service manifests (relative to the package root).
104 /// Required when `:kind Servico`.
105 #[serde(default)]
106 pub servicos: Vec<String>,
107
108 // ── M2 typed-substrate extensions per theory/ABSORPTION-ROADMAP.md ──
109 //
110 // All four are optional + default to "absent"; existing caixas
111 // round-trip unchanged. Each maps onto a prior-art primitive named
112 // in theory/INSPIRATIONS.md:
113 //
114 // :limits — Lunatic per-process limits (§III.1)
115 // :behavior — OTP gen_server callbacks (§II.3)
116 // :upgrade-from — OTP appup migration (§II.4)
117 // :estrategia — OTP supervisor strategy (§II.2 + §III.2)
118 // :children — OTP supervisor children (§II.2 + §III.2)
119 //
120 // The supervisor slots are flat on Caixa (vs nested under a
121 // SupervisorSpec sub-form) to keep tatara-lisp authoring at one
122 // level of nesting; SupervisorSpec exists for validation +
123 // composition convenience (`Caixa::supervisor_view()`).
124 /// Lunatic-style per-process resource limits. None = unbounded.
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub limits: Option<LimitsSpec>,
127
128 /// OTP-shaped behavior callbacks for Servico-kind caixas.
129 /// Authored as `(:on-init "..." :on-call "..." …)`.
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub behavior: Option<BehaviorSpec>,
132
133 /// OTP appup — declarative upgrade instructions per prior version.
134 /// Empty list = no hot-upgrade path declared (caller falls back to
135 /// `:Restart` strategy).
136 #[serde(default)]
137 pub upgrade_from: Vec<UpgradeFromEntry>,
138
139 /// OTP supervisor strategy. Required when `:kind Supervisor`;
140 /// ignored otherwise.
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub estrategia: Option<crate::supervisor::RestartStrategy>,
143
144 /// Max restarts before the supervisor itself fails. Defaults via
145 /// SupervisorSpec at validation time.
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub max_restarts: Option<u32>,
148
149 /// Sliding window for `max_restarts`. Authored as a duration
150 /// string (`"60s"`, `"5m"`).
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub restart_window: Option<String>,
153
154 /// Static children of a supervisor. Required for OneForOne /
155 /// OneForAll / RestForOne; must be empty for SimpleOneForOne.
156 #[serde(default)]
157 pub children: Vec<crate::supervisor::ChildSpec>,
158
159 // ── M3 Aplicacao slots (theory/MESH-COMPOSITION.md) ─────────────────
160 //
161 // Required when :kind Aplicacao; ignored otherwise.
162 // Composed into a typed AplicacaoSpec via Caixa::aplicacao_view().
163 /// Member Servicos that make up this Aplicacao. Each is a
164 /// caixa-name + version-constraint pair. Required for Aplicacao.
165 #[serde(default)]
166 pub membros: Vec<crate::aplicacao::Membro>,
167
168 /// WIT-typed inter-Servico contracts. Each `:de` and `:para`
169 /// must reference a name in `:membros`.
170 #[serde(default)]
171 pub contratos: Vec<crate::aplicacao::WitContract>,
172
173 /// Mesh-level policies (timeout, retries, circuit-breaker, mTLS,
174 /// rate-limit). Apply to every contrato unless overridden per-edge
175 /// in M4.
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub politicas: Option<crate::aplicacao::MeshPolicy>,
178
179 /// Placement strategy across the cluster fleet
180 /// (single-node | replicated | sharded).
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub placement: Option<crate::aplicacao::Placement>,
183
184 /// External entry point — gateway / ingress shape. Optional;
185 /// only for public Aplicacaos.
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub entrada: Option<crate::aplicacao::Entrada>,
188
189 // ── Acao slot (CANTEIRO §7.1-C) ──────────────────────────────────────
190 //
191 // Required when :kind Acao; ignored otherwise (mirrors the M2/
192 // supervisor-tree/M3 slot triads above — a declared-but-foreign `:ci`
193 // is a `LayoutError::CiOnNonAcao` build error, not a silent drop).
194 /// Typed CI run — a repo's CI run as a set of typed nodes + their
195 /// dependency edges. Required for `:kind Acao`; validated (not
196 /// rendered) by the `caixa-actions` renderer via
197 /// `canteiro_types::decompose`. See `caixa-actions`' crate docs for
198 /// the M0 validate-only contract.
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub ci: Option<canteiro_types::CiRun>,
201}
202
203/// Why reading a manifest into a [`Caixa`] failed.
204///
205/// Split from [`ManifestError`] (which reports a *parsed* manifest that is
206/// semantically wrong) because the two answer different questions, and the
207/// distinction is the whole point of this type: `ManifestError` means "your
208/// caixa is wrong", `LeituraError::DialetoEstrangeiro` means "this file is not
209/// a caixa".
210#[derive(Debug, thiserror::Error)]
211pub enum LeituraError {
212 /// The source is not readable as a `(defcaixa …)` package manifest — bad
213 /// syntax, a wrong head symbol, an unknown or mistyped slot.
214 ///
215 /// `#[source]`, not `#[error(transparent)]`. Transparent delegates
216 /// `source()` past the inner error to ITS source, which drops the
217 /// `LispError` off the cause chain — and `feira`'s
218 /// `load_caixa_parse_error_preserves_underlying_lisp_error_on_chain`
219 /// pins that a caller can `downcast_ref::<tatara_lisp::LispError>()`
220 /// through an anyhow context to read the typed payload. That pin caught
221 /// this exact regression when the variant first landed transparent.
222 #[error("{0}")]
223 Leitura(
224 #[source]
225 #[from]
226 tatara_lisp::LispError,
227 ),
228
229 /// The source IS a well-formed `(defcaixa …)` form, but of a different
230 /// declaration than this crate's.
231 ///
232 /// The variant that did not exist before, and whose absence is the defect.
233 /// A `(defcaixa :name "x" :ecosystem :go …)` used to reach the derive's
234 /// `parse_kwargs_strict` and come back as an unknown-keyword rejection —
235 /// byte-identical in shape to a typo in a real manifest. Measured over the
236 /// org checkout on 2026-07-31, that shape is the MAJORITY of the corpus, so
237 /// the confusing error was also the common one.
238 ///
239 /// Carrying the dialect means a consumer can branch on "not mine" without
240 /// re-parsing, and a census can count it. Every user-facing byte-string
241 /// (canonical keyword, one-line description, consuming crate) is a
242 /// projection of [`crate::dialeto::CaixaDialeto`] — the variant stores the
243 /// typed dialect and the `#[error]` template calls
244 /// [`CaixaDialeto::palavra_canonica`] /
245 /// [`CaixaDialeto::descricao`] / [`CaixaDialeto::consumidor`] on it, so
246 /// the three axes cannot silently diverge from the classification. Prior
247 /// to this closure the variant carried each accessor's return value as a
248 /// stored `&'static str` snapshot alongside `dialeto`, and the sole
249 /// constructor at [`Caixa::from_lisp`] filled all four fields — a caller
250 /// could construct `DialetoEstrangeiro { dialeto: Molde,
251 /// palavra_canonica: "defcaixa", … }` and every downstream consumer
252 /// (Display, ad-hoc audit, future JSON serialization) would silently
253 /// disagree with `dialeto.palavra_canonica() == "defmolde"`. The typed
254 /// enum owns the projections; the variant only carries the axis.
255 #[error(
256 "this is a `{palavra}` declaration ({desc}), read by \
257 {cons} — not a caixa-core package manifest. `defcaixa` is the \
258 tatara-lisp package manifest (`:nome :versao :kind :deps …`); the two \
259 are different declarations that shared one keyword until 2026-07-31",
260 palavra = dialeto.palavra_canonica(),
261 desc = dialeto.descricao(),
262 cons = dialeto.consumidor()
263 )]
264 DialetoEstrangeiro {
265 /// Which declaration this actually is. Sole authoritative axis;
266 /// every user-facing projection routes through
267 /// [`crate::dialeto::CaixaDialeto`]'s typed accessors so the four
268 /// axes cannot silently disagree.
269 dialeto: crate::dialeto::CaixaDialeto,
270 },
271
272 /// Not a manifest declaration at all.
273 #[error(transparent)]
274 Dialeto(#[from] crate::dialeto::DialetoError),
275}
276
277impl Caixa {
278 /// Parse a `caixa.lisp` source string to a typed `Caixa`.
279 ///
280 /// Classifies the dialect **before** parsing. A `(defcaixa …)` of another
281 /// declaration is [`LeituraError::DialetoEstrangeiro`], naming what it is
282 /// and who reads it, instead of an unknown-keyword rejection that reads as
283 /// "your manifest is broken".
284 ///
285 /// The ordering is load-bearing. Handing a foreign dialect to the derive
286 /// first and interpreting the failure afterwards would mean guessing from
287 /// an error message, and the guess would be wrong for every file whose
288 /// first unknown slot happens to be one both schemas could plausibly carry.
289 pub fn from_lisp(src: &str) -> Result<Self, LeituraError> {
290 use tatara_lisp::domain::TataraDomain;
291 let forms = tatara_lisp::read(src).map_err(LeituraError::Leitura)?;
292 let first = forms.first().ok_or(crate::dialeto::DialetoError::Vazio)?;
293
294 // Route the foreign-dialect rejection gate through the lifted
295 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
296 // typed predicate rather than the pre-lift hand-rolled three-arm
297 // `match { Pacote => {}, Desconhecido => {}, foreign => Err(…) }`
298 // literal — the `defmolde` declaration-family partition (the two-
299 // arity closure of [`crate::dialeto::CaixaDialeto::Molde`] and
300 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two arms
301 // whose sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
302 // projection already collapses onto `"defmolde"` and whose sibling
303 // [`crate::dialeto::CaixaDialeto::consumidor`] projection already
304 // collapses onto `"pleme-doc-gen"`) resolves through one dispatch
305 // on the substrate primitive. `Pacote` (the tatara-lisp package
306 // manifest this derive can parse) and `Desconhecido` (deliberately
307 // falls through to the derive rather than short-circuiting: a
308 // `(defcaixa …)` matching neither schema is most likely a genuine
309 // package manifest with a typo in `:nome`, and the derive's
310 // diagnostic — which names the offending keyword and suggests the
311 // nearest slot — is far better than anything this classifier
312 // could say) both return `false` from `is_molde_family()` and fall
313 // through to the derive. Only the typed dialect flows into the
314 // error — the three user-facing projections (canonical keyword,
315 // description, consumer) are read at Display time through
316 // [`crate::dialeto::CaixaDialeto`]'s own accessors, so the
317 // variant cannot carry a snapshot that drifts from
318 // [`crate::dialeto::CaixaDialeto::palavra_canonica`] /
319 // `descricao` / `consumidor`. A future fifth dialect the
320 // [`crate::dialeto`] module doc's "third dialect" hazard
321 // actualises that belongs to the `defmolde` family lands one
322 // match arm at [`crate::dialeto::CaixaDialeto::is_molde_family`]
323 // and this gate picks up the new arm by construction — the pre-
324 // lift wildcard `foreign =>` was compile-time-anonymous and would
325 // silently absorb any hypothetical fifth `defcaixa`-family arm as
326 // foreign; routing the partition through the typed predicate
327 // closes both drift surfaces.
328 let dialeto = crate::dialeto::classify_form(first)?;
329 if dialeto.is_molde_family() {
330 return Err(LeituraError::DialetoEstrangeiro { dialeto });
331 }
332
333 Self::compile_from_sexp(first).map_err(LeituraError::Leitura)
334 }
335
336 /// Register `Caixa` with the global tatara-lisp domain registry so
337 /// `defcaixa` is dispatchable from any tatara-lisp binary that seeds
338 /// the registry (e.g. `tatara-check`).
339 ///
340 /// Returns the typed [`tatara_lisp::KeywordCollision`] on the second
341 /// (and every subsequent) call in the same process — one keyword,
342 /// one type, per process is a hard invariant of the upstream
343 /// registry, and a caller that hits it must fix its crate graph
344 /// rather than swallowing the error. Peer of the sibling per-crate
345 /// `register()` entry points at `caixa-flake/src/flake.rs`,
346 /// `caixa-fmt/src/lisp_config.rs`, `caixa-lacre/src/lock.rs`,
347 /// `caixa-lint/src/lisp_config.rs`, `caixa-resolver/src/lisp_config.rs`
348 /// — every substrate crate that owns a tatara-lisp keyword now
349 /// propagates the same typed error verbatim, so a downstream binary
350 /// that seeds the registry (`tatara-check`, the future LSP) reaches
351 /// for one shape at every call site.
352 ///
353 /// # Errors
354 ///
355 /// [`tatara_lisp::KeywordCollision`] when a peer type has already
356 /// claimed the `defcaixa` keyword in this process.
357 pub fn register() -> Result<(), tatara_lisp::KeywordCollision> {
358 tatara_lisp::domain::register::<Self>()
359 }
360
361 /// Substrate-canonical per-`Caixa` `:licenca` SPDX-expression scalar
362 /// accessor every consumer of the top-level manifest's license axis
363 /// keys off — returns the author-declared `:licenca` byte-string
364 /// verbatim as an `Option<&str>`, borrowed from the typed slot's own
365 /// `Option<String>` storage. `None` when the slot is absent (the
366 /// canonical "omit to defer to the caixa-helm renderer's `MIT`
367 /// fallback" shape [`Self::validate_licenca`] documents at
368 /// caixa-core/src/manifest.rs:1560; the peer [`caixa-helm`]
369 /// `build_readme` fold at caixa-helm/src/lib.rs:962 reads this
370 /// predicate too, so an authored-but-unset `:licenca` round-trips to
371 /// a rendered `lareira-<nome>` chart's `README.md` `## License`
372 /// section structurally identical to one that omits the slot).
373 ///
374 /// The `:licenca` slot carries the universal-axis SPDX-expression
375 /// license identifier every kind of caixa emits under (CAIXA-SDLC
376 /// §I — the author-facing surface every `defcaixa` form supplies) —
377 /// the typed slot's `Option<String>` accept-set (empty-string
378 /// rejected through [`ManifestError::LicencaEmpty`], SPDX-alphabet-
379 /// invalid rejected through [`ManifestError::LicencaInvalid`]) maps
380 /// onto the `lareira-<nome>` Helm chart's `README.md` `## License`
381 /// section (caixa-helm/src/lib.rs:962) and (through future
382 /// tightening documented at [`Self::validate_licenca`]) the
383 /// Chart.yaml `annotations["artifacthub.io/license"]` axis every
384 /// registry-facing chart carries. Every downstream consumer that
385 /// reads the license byte-string keys off this scalar (the
386 /// [`Self::validate_licenca`] empty-arm + SPDX-shape gate that
387 /// routes through `self.licenca.as_deref()`, the caixa-helm
388 /// `build_readme` `unwrap_or_else(|| "MIT".into())` fold that keys
389 /// the fallback off the `Option::is_none()` arm, every future
390 /// per-`Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
391 /// acknowledges).
392 ///
393 /// Prior to this lift the `.licenca` field was accessed inline at
394 /// two production sites — [`Self::validate_licenca`]'s
395 /// `self.licenca.as_deref()` empty-and-shape gate binding and the
396 /// caixa-helm `build_readme` `caixa.licenca.clone().unwrap_or_else(||
397 /// "MIT".into())` `README.md` `## License` fold — two open-coded
398 /// field-accesses that expressed no compile-time link back to the
399 /// typed slot. A future extension of the `:licenca` axis to a
400 /// richer author surface — a per-`:licenca` structured SPDX
401 /// expression parser + license-id allowlist (the future tightening
402 /// [`Self::validate_licenca`]'s docstring acknowledges), a
403 /// per-cluster license-default overlay the M4 CR materializer
404 /// resolves per-CR (the "cluster policy pins `Apache-2.0` for every
405 /// unlisted caixa" arm), a promotion of the plain
406 /// `Option<String>` byte-string to a richer `SpdxExpression` enum
407 /// once the SPDX-expression parser lands — would have had to be
408 /// threaded through both open-coded copies in lockstep or the
409 /// validate gate and the caixa-helm emit path would silently
410 /// disagree on which license a given [`Caixa`] resolves to (an
411 /// author's `:licenca "MIT OR Apache-2.0"` would satisfy validate
412 /// while the emit path silently rendered a stale `MIT` fallback,
413 /// or vice versa). Lifting the resolution to a typed method on the
414 /// substrate primitive means every downstream consumer of the
415 /// caixa's per-`Caixa` license surface reaches for exactly one
416 /// typed dispatch — the resolver's accept-set migrates as a unit
417 /// on any future axis addition.
418 ///
419 /// First `Option<&str>`-return top-level [`Caixa`] scalar accessor —
420 /// opens the "outer [`Caixa`] `Option<&str>` scalar" projection
421 /// pattern the sibling per-`Caixa` `:descricao` / `:repositorio` /
422 /// `:edicao` future lifts fold on. Same "one typed dispatch on the
423 /// substrate primitive, thin projections at each consumer"
424 /// discipline the peer per-`:placement` [`crate::aplicacao::Placement::shard_key`]
425 /// (7cd2a28) / [`crate::aplicacao::Placement::affinity`] (74ec2d3)
426 /// / per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
427 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
428 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
429 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
430 /// typed-slot atom axes, extended here to the outer top-level
431 /// `Caixa` universal-axis surface. Named `licenca()` to match the
432 /// storage field's name; the accessor's identity maps onto the
433 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
434 /// carries.
435 #[must_use]
436 pub fn licenca(&self) -> Option<&str> {
437 self.licenca.as_deref()
438 }
439
440 /// Substrate-canonical per-`Caixa` `:repositorio` git-repo-URL scalar
441 /// accessor every consumer of the top-level manifest's homepage /
442 /// source-of-truth axis keys off — returns the author-declared
443 /// `:repositorio` byte-string verbatim as an `Option<&str>`, borrowed
444 /// from the typed slot's own `Option<String>` storage. `None` when
445 /// the slot is absent (the canonical "omit to defer to the renderer's
446 /// per-target placeholder" shape — [`caixa-helm`]'s `ChartYaml.home`
447 /// carries the `Option<String>` through verbatim so an author-omitted
448 /// `:repositorio` renders a `Chart.yaml` without a `home:` field
449 /// (`skip_serializing_if = "Option::is_none"`), while [`caixa-flux`]'s
450 /// `ClusterBundleOpts::for_caixa` folds the omitted slot through a
451 /// `format!("https://github.com/{DEFAULT_PLEME_GIT_ORG}/{nome}")`
452 /// fallback derived from `caixa.nome`).
453 ///
454 /// The `:repositorio` slot carries the universal-axis git-repo-URL
455 /// homepage identifier every kind of caixa emits under (CAIXA-SDLC
456 /// §I — the author-facing surface every `defcaixa` form supplies) —
457 /// the typed slot's `Option<String>` accept-set (empty-string
458 /// rejected through [`ManifestError::RepositorioEmpty`], git-repo-URL-
459 /// shape-invalid rejected through [`ManifestError::RepositorioInvalid`]
460 /// past the shared [`crate::render::is_git_repo_url`] predicate the
461 /// peer per-`:deps :fonte :repo` axis also routes through) maps onto
462 /// four load-bearing downstream consumers:
463 ///
464 /// - [`Self::validate_repositorio`]'s empty-arm + shape-predicate
465 /// gate binding at caixa-core/src/manifest.rs:1456 — the
466 /// universal-axis identity gate wired at caixa-build time.
467 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.home` fold at
468 /// caixa-helm/src/lib.rs:840 — the rendered `lareira-<nome>`
469 /// Helm chart's `Chart.yaml` `home:` field, which every registry
470 /// that ingests the chart (ArtifactHub, chartmuseum,
471 /// `helm search repo`) surfaces as the chart's canonical source-
472 /// of-truth link.
473 /// - [`caixa-helm`]'s `build_readme` `## Source` fold at
474 /// caixa-helm/src/lib.rs:957 — the rendered `lareira-<nome>`
475 /// chart's `README.md` header link back to the source repo,
476 /// which every author who inspects the rendered chart bundle
477 /// lands at.
478 /// - [`caixa-flux`]'s `ClusterBundleOpts::for_caixa`
479 /// `GitRepository.spec.url` fold at caixa-flux/src/lib.rs:2006 —
480 /// the rendered `GitRepository` CR's `spec.url` field, which
481 /// FluxCD's `source-controller` polls to reconcile the caixa's
482 /// manifest bundle from git.
483 ///
484 /// Prior to this lift the `.repositorio` field was accessed inline
485 /// at four production sites — [`Self::validate_repositorio`]'s
486 /// `self.repositorio.as_deref()` empty-and-shape gate binding, the
487 /// caixa-helm `build_chart_yaml` `caixa.repositorio.clone()`
488 /// `Chart.yaml` `home:` field fold, the caixa-helm `build_readme`
489 /// `caixa.repositorio.clone().unwrap_or_else(|| caixa.nome.clone())`
490 /// `README.md` `## Source` fold, and the caixa-flux
491 /// `ClusterBundleOpts::for_caixa`
492 /// `caixa.repositorio.clone().unwrap_or_else(|| format!(...))`
493 /// `GitRepository.spec.url` fold — four open-coded field-accesses
494 /// that expressed no compile-time link back to the typed slot. A
495 /// future extension of the `:repositorio` axis to a richer author
496 /// surface — a per-`:repositorio` structured
497 /// [`crate::render::GitRepoUrl`]-shaped scheme+host+path parse
498 /// (the future tightening [`Self::validate_repositorio`]'s
499 /// docstring anticipates alongside the peer per-`:deps :fonte
500 /// :repo` axis), a per-cluster repo-mirror overlay the M4 CR
501 /// materializer resolves per-CR (the "cluster policy rewrites
502 /// `github:pleme-io/...` to `git.internal/mirror/pleme-io/...`"
503 /// arm the private-registry story acknowledges), a promotion of
504 /// the plain `Option<String>` byte-string to a richer
505 /// `RepoUrl` enum discriminated on scheme — would have had to be
506 /// threaded through all four open-coded copies in lockstep or the
507 /// validate gate and the three emit paths would silently disagree
508 /// on which URL a given [`Caixa`] resolves to (an author's
509 /// `:repositorio "github:pleme-io/checkout"` would satisfy validate
510 /// while one of the emit paths silently rendered a stale URL, or
511 /// vice versa). Lifting the resolution to a typed method on the
512 /// substrate primitive means every downstream consumer of the
513 /// caixa's per-`Caixa` repo-URL surface reaches for exactly one
514 /// typed dispatch — the resolver's accept-set migrates as a unit on
515 /// any future axis addition.
516 ///
517 /// Second outer top-level [`Caixa`] `Option<&str>`-return scalar
518 /// accessor — sibling of [`Self::licenca`] (6d5bc28), the accessor
519 /// that opened the "outer [`Caixa`] `Option<&str>` scalar"
520 /// projection pattern this lift folds on. Same "one typed dispatch
521 /// on the substrate primitive, thin projections at each consumer"
522 /// discipline the peer per-`:placement`
523 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
524 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
525 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
526 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
527 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
528 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
529 /// typed-slot atom axes, extended here to the second outer top-level
530 /// `Caixa` universal-axis surface. Named `repositorio()` to match
531 /// the storage field's name; the accessor's identity maps onto the
532 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
533 /// carries.
534 #[must_use]
535 pub fn repositorio(&self) -> Option<&str> {
536 self.repositorio.as_deref()
537 }
538
539 /// Substrate-canonical per-`Caixa` **resolved-git-repo-URL** composer —
540 /// returns the caixa's canonical git-source-of-truth URL as an owned
541 /// [`String`], author-declared `:repositorio` byte-string verbatim on
542 /// the `Some` arm and the substrate's canonical pleme-org github URL
543 /// fallback ([`crate::DEFAULT_PLEME_GIT_ORG`] and [`Self::nome`]
544 /// interpolated into `https://github.com/<org>/<nome>`) on the
545 /// `None` arm. Every substrate-side consumer that resolves
546 /// "which git URL does this caixa's source live at?" reaches for
547 /// exactly one typed dispatch on the substrate primitive — the raw
548 /// `caixa.repositorio().map(str::to_owned).unwrap_or_else(|| format!(
549 /// "https://github.com/{org}/{nome}", org = DEFAULT_PLEME_GIT_ORG,
550 /// nome = caixa.nome()))` open-coded composition every prior caller
551 /// re-derived collapses onto one canonical arm.
552 ///
553 /// Distinct from [`Self::repositorio`] (`Option<&str>`, exposes the
554 /// author-omitted / author-declared partition to the caller) — this
555 /// accessor is the **resolved** URL surface, folding the fallback in
556 /// at the substrate-primitive boundary. Every consumer that keys off
557 /// the `Option::is_none()` discriminator (a [`Chart.yaml`] `home:`
558 /// field emit that must omit the field entirely on an author-omitted
559 /// `:repositorio`, per the [`Self::repositorio`] docstring's
560 /// documented four-consumer list) reaches through the raw
561 /// [`Self::repositorio`] `Option<&str>` accessor by construction — the
562 /// resolved-URL composer sits alongside it as the second projection
563 /// on the same underlying `:repositorio` slot rather than replacing
564 /// the raw accessor.
565 ///
566 /// The fallback branch is the exact byte-image of the prior inline
567 /// [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_url` composer at
568 /// caixa-flux/src/lib.rs:2080 — pinned by the sibling caixa-flux
569 /// byte-parity test
570 /// `cluster_bundle_opts_for_caixa_git_url_routes_through_canonical_git_url_accessor`
571 /// against a future implementation of this method that reordered the
572 /// `format!` template arguments, migrated the `<org>` segment to a
573 /// different constant (the [`crate::DEFAULT_PLEME_GIT_ORG`] axis a
574 /// future substrate-side git-org migration may split off), or
575 /// silently absorbed the empty-string arm (a hypothetical
576 /// `Some("") → fallback` collapse the raw [`Self::repositorio`]
577 /// accessor's docstring explicitly rejects on the sibling raw
578 /// accessor).
579 ///
580 /// Peer of the sibling per-`&Caixa`-axis composed helpers
581 /// [`caixa-flux::cluster_bundle_for_caixa`] (06d52d7) on the sibling
582 /// substrate-side renderer surface — same "close the composed
583 /// substrate-primitive at one canonical arm on the single-`&Caixa`
584 /// dispatch, converge every prior open-coded caller onto the arm"
585 /// discipline extended onto the resolved-git-URL projection of the
586 /// per-`Caixa` `:repositorio` axis. Owns per-call [`String`]
587 /// allocation on both arms (the `Some` arm's `str::to_owned` and the
588 /// `None` arm's `format!`) — the by-value return matches every
589 /// downstream consumer's field-fill shape (the caixa-flux
590 /// `ClusterBundleOpts::git_url: String` field, every future
591 /// `Chart.yaml` `home:` fold's `Option<String>` field-fill on the
592 /// `Some` arm).
593 #[must_use]
594 pub fn canonical_git_url(&self) -> String {
595 self.repositorio().map_or_else(
596 || {
597 format!(
598 "https://github.com/{org}/{nome}",
599 org = crate::DEFAULT_PLEME_GIT_ORG,
600 nome = self.nome(),
601 )
602 },
603 str::to_owned,
604 )
605 }
606
607 /// Substrate-canonical per-`Caixa` **resolved-publish-tag** composer —
608 /// returns the caixa's canonical Zig-style git-publish-tag as an owned
609 /// [`String`], derived by concatenating
610 /// [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] with the typed
611 /// [`Self::versao`] byte-string on a single `format!` template.
612 /// Every substrate-side consumer that resolves "which git tag does this
613 /// caixa publish under?" reaches for exactly one typed dispatch on the
614 /// substrate primitive — the raw `format!("{prefix}{versao}", prefix =
615 /// caixa_core::DEFAULT_PUBLISH_TAG_PREFIX, versao = caixa.versao())`
616 /// open-coded composition every prior caller re-derived collapses onto
617 /// one canonical arm.
618 ///
619 /// Peer of the sibling [`Self::canonical_git_url`] (124f864) resolved-
620 /// git-URL composer on the paired per-`Caixa` git-remote axis — same
621 /// "close the composed substrate-primitive at one canonical arm on the
622 /// single-`&Caixa` dispatch, converge every prior open-coded caller
623 /// onto the arm" discipline extended from the resolved-URL projection
624 /// of the per-`Caixa` `:repositorio` axis onto the resolved-tag
625 /// projection of the per-`Caixa` `:versao` axis. The two accessors
626 /// jointly close the pair of scalars every `FluxCD` `GitRepository` CR
627 /// keys off (`spec.url` via [`Self::canonical_git_url`],
628 /// `spec.ref.tag` via [`Self::publish_tag`]) at the substrate primitive
629 /// — a downstream consumer that reaches through both accessors reads
630 /// the complete published-git-identity of a caixa through two typed
631 /// dispatches, not four open-coded field accesses.
632 ///
633 /// The reader-side (`caixa-flux::cluster_bundle` /
634 /// `ClusterBundleOpts::for_caixa`'s `git_ref` field, every future
635 /// per-cluster snapshot bundle emitter, the future M4
636 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer's tag-carrier
637 /// slot on the tatara `Process` intent) always resolves the tag under
638 /// the canonical [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] prefix — this
639 /// method encodes that reader-side convention. The writer-side
640 /// (`caixa-feira`'s `feira publish` `--prefix` clap flag) allows the
641 /// operator to override the prefix at publish time; the two surfaces
642 /// intentionally sit on the "canonical default + operator override"
643 /// pair the sibling [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] constant's
644 /// own docstring documents — a `feira publish --prefix release/`
645 /// override is the operator's explicit opt-out from the substrate
646 /// default, not a supported drift axis.
647 ///
648 /// The composition body is the exact byte-image of the prior inline
649 /// [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_ref` composer at
650 /// caixa-flux/src/lib.rs:2105 — pinned by the sibling caixa-flux
651 /// byte-parity test
652 /// `cluster_bundle_opts_for_caixa_git_ref_routes_through_publish_tag_accessor`
653 /// against a future implementation of this method that reordered the
654 /// `format!` template arguments, migrated the `<prefix>` segment to a
655 /// different constant (the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] axis
656 /// a future Zig-style-tag rebrand may split off — the constant's own
657 /// docstring anticipates a substrate-side move to `release/<versao>`
658 /// or bare `<versao>` shapes once a sibling forge convention adopts a
659 /// slash-namespaced or bare-scalar form), interposed a canonicalization
660 /// pass on the `:versao` axis (a SemVer-2 build-metadata strip an OCI-
661 /// tag normalizer might apply once the M4 registry-alignment slot
662 /// lands), or silently absorbed an empty `:versao` arm (which cannot
663 /// occur past the [`Self::validate_versao`] gate but which a
664 /// hypothetical bypass on the accessor path must not silently paper
665 /// over).
666 ///
667 /// Owns per-call [`String`] allocation via the single `format!`
668 /// invocation — the by-value return matches every downstream
669 /// consumer's field-fill shape (the caixa-flux `GitRefSpec::Tag(String)`
670 /// variant's owned payload, every future `intent.aplicacao.tag: String`
671 /// field-fill on the M4 CR materializer's tag-carrier slot).
672 #[must_use]
673 pub fn publish_tag(&self) -> String {
674 format!(
675 "{prefix}{versao}",
676 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
677 versao = self.versao(),
678 )
679 }
680
681 /// Substrate-canonical per-`Caixa` `:descricao` free-form-prose
682 /// chart-description scalar accessor every consumer of the top-level
683 /// manifest's Chart.yaml `description:` axis keys off — returns the
684 /// author-declared `:descricao` byte-string verbatim as an
685 /// `Option<&str>`, borrowed from the typed slot's own
686 /// `Option<String>` storage. `None` when the slot is absent (the
687 /// canonical "omit to defer to the per-renderer `caixa.nome`-derived
688 /// fallback" shape — [`caixa-helm`]'s `build_chart_yaml` folds the
689 /// omitted slot through a `format!("Generated chart for caixa Servico
690 /// {}", caixa.nome)` fallback, [`caixa-helm`]'s `build_readme` folds
691 /// it through a `format!("caixa Servico {}", caixa.nome)` fallback,
692 /// and [`caixa-feira`]'s `render_flake` folds it through a
693 /// `format!("caixa {}", c.nome)` `flake.nix` `description = ""`
694 /// fallback — each derived from `caixa.nome` on the null-carrier arm).
695 ///
696 /// The `:descricao` slot carries the universal-axis free-form-prose
697 /// chart-description identifier every kind of caixa emits under
698 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa` form
699 /// supplies) — the typed slot's `Option<String>` accept-set
700 /// (empty-string rejected through [`ManifestError::DescricaoEmpty`],
701 /// chart-description-shape-invalid rejected through
702 /// [`ManifestError::DescricaoInvalid`] past the shared
703 /// [`crate::render::is_chart_description_shape`] predicate the peer
704 /// per-`Caixa` `:descricao` axis also routes through) maps onto four
705 /// load-bearing downstream consumers:
706 ///
707 /// - [`Self::validate_descricao`]'s empty-arm + shape-predicate
708 /// gate binding — the universal-axis identity gate wired at
709 /// caixa-build time.
710 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.description`
711 /// `Chart.yaml` field fold — the rendered `lareira-<nome>` Helm
712 /// chart's `Chart.yaml` `description:` field, which
713 /// `apiVersion: v2` charts require non-empty (`helm lint` fires
714 /// `WARNING [chart.metadata.description]: description is required`
715 /// when absent) and which every registry that ingests the chart
716 /// (ArtifactHub, chartmuseum, `helm search repo`) surfaces as the
717 /// chart's canonical one-line prose descriptor.
718 /// - [`caixa-helm`]'s `build_readme` chart-`README.md` header fold
719 /// — the rendered `lareira-<nome>` chart's `README.md` prose
720 /// header directly beneath the `# <chart-name>` title, which
721 /// every author who inspects the rendered chart bundle lands at.
722 /// - [`caixa-feira`]'s `render_flake` `flake.nix` `description = ""`
723 /// top-level fold — the emitted `flake.nix`'s `description`
724 /// field, which every Nix consumer (`nix flake show`,
725 /// `nix flake metadata`, downstream flake-registry ingestors)
726 /// surfaces as the flake's canonical descriptor.
727 ///
728 /// Prior to this lift the `.descricao` field was accessed inline at
729 /// four production sites — [`Self::validate_descricao`]'s
730 /// `self.descricao.as_deref()` empty-and-shape gate binding, the
731 /// caixa-helm `build_chart_yaml`
732 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
733 /// `Chart.yaml` `description:` fold, the caixa-helm `build_readme`
734 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
735 /// `README.md` header fold, and the caixa-feira `render_flake`
736 /// `c.descricao.clone().unwrap_or_else(|| format!(...))` `flake.nix`
737 /// `description = ""` fold — four open-coded field-accesses that
738 /// expressed no compile-time link back to the typed slot. A future
739 /// extension of the `:descricao` axis to a richer author surface —
740 /// a per-`:descricao` locale-tagged multi-language descriptor map
741 /// (the "one caixa, N language-tagged prose descriptions" arm
742 /// author-tooling internationalization anticipates), a
743 /// per-registry-target length-and-shape overlay the M4 CR
744 /// materializer resolves per-CR (the "ArtifactHub caps description
745 /// at 512 bytes but the internal registry caps at 256" arm), a
746 /// promotion of the plain `Option<String>` byte-string to a richer
747 /// `ChartDescription` newtype guaranteeing the
748 /// `is_chart_description_shape` predicate at the type level — would
749 /// have had to be threaded through all four open-coded copies in
750 /// lockstep or the validate gate and the three emit paths would
751 /// silently disagree on which prose string a given [`Caixa`]
752 /// resolves to (an author's
753 /// `:descricao "Checkout flow orchestration."` would satisfy
754 /// validate while one of the emit paths silently rendered a stale
755 /// `caixa.nome`-derived fallback, or vice versa). Lifting the
756 /// resolution to a typed method on the substrate primitive means
757 /// every downstream consumer of the caixa's per-`Caixa`
758 /// chart-description surface reaches for exactly one typed dispatch
759 /// — the resolver's accept-set migrates as a unit on any future
760 /// axis addition.
761 ///
762 /// Third outer top-level [`Caixa`] `Option<&str>`-return scalar
763 /// accessor — sibling of [`Self::licenca`] (6d5bc28) and
764 /// [`Self::repositorio`] (cc7332d), the accessors that opened the
765 /// "outer [`Caixa`] `Option<&str>` scalar" projection pattern this
766 /// lift folds on. Same "one typed dispatch on the substrate
767 /// primitive, thin projections at each consumer" discipline the
768 /// peer per-`:placement`
769 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
770 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
771 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
772 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
773 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
774 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
775 /// typed-slot atom axes, extended here to the third outer top-level
776 /// `Caixa` universal-axis surface. Named `descricao()` to match the
777 /// storage field's name; the accessor's identity maps onto the
778 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
779 /// carries. The one remaining universal `Option<String>` slot
780 /// (`:edicao`) folds on this pattern next.
781 #[must_use]
782 pub fn descricao(&self) -> Option<&str> {
783 self.descricao.as_deref()
784 }
785
786 /// Substrate-canonical per-`Caixa` `:edicao` language-edition scalar
787 /// accessor every consumer of the top-level manifest's tatara-lisp
788 /// edition-selector axis keys off — returns the author-declared
789 /// `:edicao` byte-string verbatim as an `Option<&str>`, borrowed from
790 /// the typed slot's own `Option<String>` storage. `None` when the
791 /// slot is absent (the canonical "omit the slot to defer to the
792 /// substrate's default edition" shape every existing
793 /// [`caixa-resolver`] integration test fixture carries via
794 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`;
795 /// the peer [`Self::validate_edicao`] gate is a no-op on the omitted
796 /// arm by construction, so an author-omitted `:edicao` round-trips
797 /// to a build without triggering the year-shape predicate).
798 ///
799 /// The `:edicao` slot carries the universal-axis 4-digit-ASCII-
800 /// decimal-year language-edition identifier every kind of caixa
801 /// emits under (CAIXA-SDLC §I — the author-facing surface every
802 /// `defcaixa` form supplies) — the typed slot's `Option<String>`
803 /// accept-set (empty-string rejected through
804 /// [`ManifestError::EdicaoEmpty`], year-shape-invalid rejected
805 /// through [`ManifestError::EdicaoInvalid`] past the 4-digit-ASCII-
806 /// decimal-year predicate [`Self::validate_edicao`] enforces) maps
807 /// onto one load-bearing downstream consumer today
808 /// ([`Self::validate_edicao`]'s empty-arm + year-shape-predicate
809 /// gate binding at caixa-core/src/manifest.rs:1959) plus every
810 /// future edition-aware substrate consumer the CAIXA-SDLC §I
811 /// roadmap anticipates (the tatara-lisp compiler's macro-surface
812 /// selector every edition-aware build step keys off, the future
813 /// per-edition compatibility-flag overlay the M4 CR materializer
814 /// resolves per-CR, the peer [`Caixa::template`] canonical
815 /// `:edicao "2026"` scaffold every `feira init` emits verbatim,
816 /// and the renderer-side fixtures at `caixa-helm/src/lib.rs:978` /
817 /// `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208` that
818 /// carry `edicao: Some("2026".into())` by construction).
819 ///
820 /// Prior to this lift the `.edicao` field was accessed inline at
821 /// one production site — [`Self::validate_edicao`]'s
822 /// `self.edicao.as_deref()` empty-and-shape gate binding — one
823 /// open-coded field-access that expressed no compile-time link
824 /// back to the typed slot. A future extension of the `:edicao`
825 /// axis to a richer author surface — a per-`:edicao` known-
826 /// edition allowlist (the future tightening
827 /// [`Self::validate_edicao`]'s docstring acknowledges past the
828 /// structural year-shape floor, rejecting year-shaped values that
829 /// don't name a tatara-lisp edition the substrate actually
830 /// understands — `"1999"` is year-shaped but no `1999` edition
831 /// exists), a per-edition compatibility-flag overlay the M4 CR
832 /// materializer resolves per-CR (the "edition `"2026"` enables
833 /// macro-surface features the sibling `"2018"` gates behind a
834 /// feature flag" arm the edition-selector story anticipates), a
835 /// promotion of the plain `Option<String>` byte-string to a
836 /// richer `CaixaEdition` enum discriminated on year once a sibling
837 /// edition to `"2026"` lands — would have had to be threaded
838 /// through the open-coded copy in lockstep with every future
839 /// edition-aware consumer, or the validate gate and the future
840 /// edition-aware consumer path would silently disagree on which
841 /// edition a given [`Caixa`] resolves to (an author's
842 /// `:edicao "2026"` would satisfy validate while a future
843 /// edition-aware consumer silently defaulted to a stale edition,
844 /// or vice versa). Lifting the resolution to a typed method on
845 /// the substrate primitive means every downstream consumer of the
846 /// caixa's per-`Caixa` edition surface reaches for exactly one
847 /// typed dispatch — the resolver's accept-set migrates as a unit
848 /// on any future axis addition.
849 ///
850 /// Fourth and final outer top-level [`Caixa`] `Option<&str>`-return
851 /// scalar accessor — sibling of [`Self::licenca`] (6d5bc28),
852 /// [`Self::repositorio`] (cc7332d), and [`Self::descricao`]
853 /// (3f16e2f), the accessors that opened the "outer [`Caixa`]
854 /// `Option<&str>` scalar" projection pattern this lift folds on.
855 /// Same "one typed dispatch on the substrate primitive, thin
856 /// projections at each consumer" discipline the peer per-`:placement`
857 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
858 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
859 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
860 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
861 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
862 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
863 /// typed-slot atom axes, extended here to close the outer top-level
864 /// `Caixa` universal-axis surface's last unlifted `Option<String>`
865 /// slot. Named `edicao()` to match the storage field's name; the
866 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
867 /// vocabulary the slot's docstring already carries.
868 #[must_use]
869 pub fn edicao(&self) -> Option<&str> {
870 self.edicao.as_deref()
871 }
872
873 /// Substrate-canonical per-`Caixa` `:nome` universal-axis DNS-1123-
874 /// label caixa-identity scalar accessor every consumer of the top-
875 /// level manifest's identity axis keys off — returns the author-
876 /// declared `:nome` byte-string verbatim as an `&str`, borrowed from
877 /// the typed slot's own `String` storage. Non-optional (`:nome` is
878 /// a required-axis scalar every `defcaixa` form must supply; the
879 /// [`Self::from_lisp`] derive rejects an omitted / non-string
880 /// `:nome` at parse time, so a `Caixa` past parse definitionally
881 /// carries a non-`None` `:nome`).
882 ///
883 /// The `:nome` slot carries the universal-axis DNS-1123-label
884 /// caixa-identity every kind of caixa emits under (CAIXA-SDLC §I —
885 /// the primary identity axis every `defcaixa` form supplies
886 /// alongside `:versao` / `:kind`; the substrate-wide identity every
887 /// other typed surface that names a caixa reaches through — `:deps`
888 /// entries, `:membros` entries, `:children` entries, the
889 /// `lareira-<nome>` Helm chart name every per-Servico renderer
890 /// derives, the `pleme-program-<nome>` label every per-Aplicacao
891 /// renderer emits) — the typed slot's `String` accept-set (empty
892 /// rejected through [`ManifestError::NomeEmpty`], DNS-1123-shape-
893 /// invalid rejected through [`ManifestError::NomeInvalid`] past
894 /// the shared [`crate::render::require_valid_dns_1123_label`] gate
895 /// the peer name axes each land on, joint-length-with-`lareira-`-
896 /// prefix rejected through
897 /// [`ManifestError::NomeChartNameBudgetExceeded`] past
898 /// [`crate::render::is_lareira_chart_name_shape`]) maps onto every
899 /// load-bearing downstream consumer the substrate carries — the
900 /// two universal-axis validate gates at caixa-build time
901 /// ([`Self::validate_nome`] + [`Self::validate_nome_chart_name_budget`]),
902 /// [`crate::lareira_chart_name`]'s `lareira-<nome>` Helm chart-name
903 /// derivation every per-Servico renderer keys off, the caixa-helm
904 /// `Chart.yaml`'s `name:` axis, caixa-flux's `programs.yaml` entry
905 /// `name:` axis, caixa-mesh's Cilium `CiliumNetworkPolicy` /
906 /// `HTTPRoute` per-Aplicacao name axes at
907 /// caixa-mesh/src/lib.rs:{2650, 2797, 2919, 2925},
908 /// [`crate::pleme_program_selector`] /
909 /// [`crate::pleme_program_in_aplicacao_selector`] label-selector
910 /// derivations, and every future substrate renderer that emits an
911 /// artifact keyed by the caixa's identity.
912 ///
913 /// Prior to this lift the `.nome` field was accessed inline at a
914 /// dozen production sites across `caixa-core` (the two universal-
915 /// axis validate gates + [`Dep::validate`]-adjacent duplicate
916 /// tracking), `caixa-helm` (the `lareira_chart_name` fold, the
917 /// `ChartYaml.name` / `ChartYaml.description` / `Chart.yaml`
918 /// `keywords` fallback), `caixa-flux` (the `programs.yaml`
919 /// entry `name:` fold, the `flux_kustomization_source_subtree`
920 /// per-cluster subpath derivation), and `caixa-mesh` (the
921 /// `pleme_program_in_aplicacao_selector` label-selector fold, the
922 /// `cilium_network_policy_name` / `gateway_api_http_route_name`
923 /// per-CR name derivations, the `LABEL_APLICACAO` labels-map
924 /// insert) — a dozen open-coded field-accesses that expressed no
925 /// compile-time link back to the typed slot. A future extension of
926 /// the `:nome` axis to a richer author surface — a per-`:nome`
927 /// structured `CaixaIdentity` newtype that carries the joint-
928 /// length-with-prefix invariant [`Self::validate_nome_chart_name_budget`]
929 /// enforces at the type level (rather than as a validate-time
930 /// gate), a per-registry `:nome` namespacing overlay the M4 CR
931 /// materializer resolves per-CR (the "`pleme-io/checkout` vs
932 /// `partner-org/checkout` collision" arm the multi-tenant-registry
933 /// story acknowledges), a promotion of the plain `String` byte-
934 /// string to a richer `CaixaNome` newtype discriminated on
935 /// namespace prefix — would have had to be threaded through every
936 /// open-coded copy in lockstep or the two validate gates and the
937 /// dozen emit paths would silently disagree on which identity a
938 /// given [`Caixa`] resolves to (an author's `:nome "checkout"`
939 /// would satisfy validate while one of the emit paths silently
940 /// rendered a drifted other identity, or vice versa). Lifting the
941 /// resolution to a typed method on the substrate primitive means
942 /// every downstream consumer of the caixa's per-`Caixa` identity
943 /// surface reaches for exactly one typed dispatch — the resolver's
944 /// accept-set migrates as a unit on any future axis addition.
945 ///
946 /// First outer top-level [`Caixa`] `&str`-return required-scalar
947 /// accessor — opens the "outer [`Caixa`] `&str` required-scalar"
948 /// projection pattern the sibling per-`Caixa` `:versao` future lift
949 /// folds on. Sibling in shape to the peer per-`:membros`
950 /// [`crate::aplicacao::Membro::nome`] (4a32abf) / per-`:contratos`
951 /// [`crate::aplicacao::WitContract::source`] /
952 /// [`crate::aplicacao::WitContract::destination`] (7f0fd43),
953 /// [`crate::aplicacao::WitContract::world_ref`] (0804823),
954 /// [`crate::aplicacao::Membro::versao_requirement`] (a40b0e3),
955 /// [`crate::aplicacao::Entrada::destination`] (6db982c),
956 /// [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062),
957 /// per-sub-struct required-axis accessors carry on the sibling M3
958 /// mesh-slot-atom scalar-value axes, extended here to open the
959 /// outer top-level [`Caixa`] `&str`-return required-scalar surface.
960 /// Named `nome()` to match the storage field's name; the accessor's
961 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
962 /// slot's docstring already carries.
963 #[must_use]
964 pub fn nome(&self) -> &str {
965 &self.nome
966 }
967
968 /// Substrate-canonical per-`Caixa` `:versao` universal-axis SemVer-2
969 /// pinned-version scalar accessor every consumer of the top-level
970 /// manifest's version axis keys off — returns the author-declared
971 /// `:versao` byte-string verbatim as an `&str`, borrowed from the
972 /// typed slot's own `String` storage. Non-optional (`:versao` is a
973 /// required-axis scalar every `defcaixa` form must supply alongside
974 /// `:nome` / `:kind`; the [`Self::from_lisp`] derive rejects an
975 /// omitted / non-string `:versao` at parse time, so a `Caixa` past
976 /// parse definitionally carries a non-`None` `:versao`).
977 ///
978 /// The `:versao` slot carries the universal-axis SemVer-2
979 /// concrete-version body every kind of caixa emits under
980 /// (CAIXA-SDLC §I — the required-scalar every `defcaixa` form
981 /// supplies alongside `:nome` / `:kind`; the substrate-wide
982 /// pinned-version every downstream artifact-emitting consumer
983 /// composes under — the `lareira-<nome>` Helm chart's `Chart.yaml`
984 /// `version:` + `appVersion:` axes, the `feira publish` Zig-style
985 /// `v<versao>` git tag the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
986 /// prefix composes on top of, the programs.yaml entry's `versao:`
987 /// value the `lareira-fleet-programs` aggregator carries onto each
988 /// rendered `ComputeUnit`, the OCI image's `:v<versao>` / `:latest`
989 /// tags every substrate-side `skopeo push` writes, the lacre
990 /// closure's pinned `concrete_versao`, and the `:upgrade-from :from`
991 /// prior-version references peers in the exact same SemVer-2 shape).
992 /// The typed slot's `String` accept-set (empty rejected through
993 /// [`ManifestError::VersaoEmpty`], SemVer-2-shape-invalid rejected
994 /// through [`ManifestError::VersaoInvalid`] past
995 /// [`semver::Version::parse`]) maps onto every load-bearing
996 /// downstream consumer the substrate carries — the [`Self::validate_versao`]
997 /// universal-axis validate gate at caixa-build time, the
998 /// [`crate::CaixaVersion::parse`] typed-wrapper resolver,
999 /// [`caixa-helm`]'s `Chart.yaml` `version:` / `appVersion:` fold,
1000 /// [`caixa-flux`]'s `programs.yaml` entry `versao:` fold + the
1001 /// `cluster_bundle` `GitRepository` `ref: { tag: v<versao> }`
1002 /// derivation, [`caixa-mesh`]'s per-Aplicacao `programs.yaml` fan-
1003 /// out entry `versao:` fold, [`caixa-feira`]'s `feira publish` git-
1004 /// tag derivation (`format!("{prefix}{versao}")`), and every future
1005 /// substrate renderer that emits an artifact keyed by the caixa's
1006 /// pinned version.
1007 ///
1008 /// Prior to this lift the `.versao` field was accessed inline at a
1009 /// dozen production sites across `caixa-core` (the universal-axis
1010 /// [`Self::validate_versao`] gate + [`Dep::validate`]-adjacent
1011 /// version-shape gates), `caixa-helm` (the `ChartYaml.version` /
1012 /// `ChartYaml.app_version` folds), `caixa-flux` (the `programs.yaml`
1013 /// entry `versao:` fold, the `cluster_bundle` `GitRepository` `ref:
1014 /// { tag: v<versao> }` derivation), `caixa-mesh` (the per-Aplicacao
1015 /// `programs.yaml` fan-out entry `versao:` fold), and `caixa-feira`
1016 /// (the `feira publish` git-tag derivation + the `feira app graph` /
1017 /// `feira app deploy` diagnostic renderers) — a dozen open-coded
1018 /// field-accesses that expressed no compile-time link back to the
1019 /// typed slot. A future extension of the `:versao` axis to a richer
1020 /// author surface — a per-`:versao` structured `CaixaVersion` at the
1021 /// storage layer (the substrate already carries a `CaixaVersion`
1022 /// newtype at [`crate::version::CaixaVersion`], deferred until the
1023 /// serde-transparent-newtype-through-DeriveTataraDomain path lands),
1024 /// a per-registry `:versao` immutability overlay the M4 CR
1025 /// materializer enforces per-CR, a promotion of the plain `String`
1026 /// byte-string to a richer `PinnedVersao` newtype discriminated on
1027 /// SemVer-2 pre-release / build-metadata presence — would have had
1028 /// to be threaded through every open-coded copy in lockstep or the
1029 /// validate gate and the dozen emit paths would silently disagree
1030 /// on which version a given [`Caixa`] resolves to (an author's
1031 /// `:versao "0.1.0"` would satisfy validate while one of the emit
1032 /// paths silently rendered a drifted other version, or vice versa).
1033 /// Lifting the resolution to a typed method on the substrate
1034 /// primitive means every downstream consumer of the caixa's
1035 /// per-`Caixa` pinned-version surface reaches for exactly one typed
1036 /// dispatch — the resolver's accept-set migrates as a unit on any
1037 /// future axis addition.
1038 ///
1039 /// Second outer top-level [`Caixa`] `&str`-return required-scalar
1040 /// accessor — folds on the "outer [`Caixa`] `&str` required-scalar"
1041 /// projection pattern the sibling per-`Caixa` [`Self::nome`]
1042 /// (e6b7d97) opened. Sibling in shape to the peer per-`:membros`
1043 /// [`crate::aplicacao::Membro::versao_requirement`] (4127bb6) /
1044 /// per-`:children` [`crate::supervisor::ChildSpec::versao_requirement`]
1045 /// (2c053c8) / per-`:upgrade-from` [`crate::UpgradeFromEntry::prior_versao`]
1046 /// (75d27a8) per-sub-struct `:versao`-shaped `&str`-return accessors
1047 /// on the sibling per-typed-slot version-carrier axes, extended here
1048 /// to close the second outer top-level [`Caixa`] required-`&str`-
1049 /// carrying axis so the two universal-axis identity-carrying
1050 /// scalars every `defcaixa` form supplies (`:nome` + `:versao`)
1051 /// share the same "one typed dispatch per axis" discipline. Named
1052 /// `versao()` to match the storage field's name; the accessor's
1053 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1054 /// slot's docstring already carries.
1055 #[must_use]
1056 pub fn versao(&self) -> &str {
1057 &self.versao
1058 }
1059
1060 /// Substrate-canonical per-`Caixa` `:kind` universal-axis
1061 /// closed-set-enum discriminant accessor every consumer of the top-
1062 /// level manifest's kind axis keys off — returns the author-declared
1063 /// `:kind` variant verbatim as a [`CaixaKind`], `Copy`-projected
1064 /// from the typed slot's own [`CaixaKind`] storage. Non-optional
1065 /// (`:kind` is a required-axis discriminant every `defcaixa` form
1066 /// must supply alongside `:nome` / `:versao`; the [`Self::from_lisp`]
1067 /// derive rejects an omitted / non-symbol `:kind` at parse time, so
1068 /// a `Caixa` past parse definitionally carries a valid [`CaixaKind`]
1069 /// variant).
1070 ///
1071 /// The `:kind` slot carries the universal-axis closed-set typed-
1072 /// discriminant every substrate-side dispatch keys off (CAIXA-SDLC
1073 /// §I — the primary shape gate every renderer / verifier /
1074 /// operator branches on; the five variants `Biblioteca` /
1075 /// `Binario` / `Servico` / `Supervisor` / `Aplicacao` partition
1076 /// the caixa surface into disjoint runtime contracts) — the typed
1077 /// slot's [`CaixaKind`] accept-set (parse-time-rejected non-symbol
1078 /// values through the derive-macro's symbol-arm gate, exhaustively
1079 /// matched at every downstream dispatch site) maps onto every
1080 /// load-bearing downstream consumer the substrate carries:
1081 ///
1082 /// - [`crate::render::require_kind`]'s per-renderer entry-gate
1083 /// predicate — the canonical two-line
1084 /// `require_kind(caixa, Servico)?` prelude every per-Servico
1085 /// renderer (`caixa-helm`, `caixa-flux`, the future `caixa-otel`
1086 /// / per-Servico OCI packager / M4 `wasm.pleme.io/v1alpha1/
1087 /// ComputeUnit` CR materializer) runs at its entry-point,
1088 /// alongside the [`crate::render::KindMismatch`] error carrier's
1089 /// `actual:` field the diagnostic surfaces to name the offending
1090 /// caixa's variant.
1091 /// - [`Self::aplicacao_view`]'s + [`Self::supervisor_view`]'s
1092 /// per-view kind-gate binding — the two `Option<TypedSpec>`
1093 /// `_view` composers that fold the flat mesh-slot / supervisor-
1094 /// slot columns into their typed sub-spec only when the kind
1095 /// matches (returns `None` otherwise); the future per-Servico
1096 /// M2-view composer (`servico_view`) will follow the same shape.
1097 /// - [`Self::declared_foreign_code_slots`]'s per-slot kind-
1098 /// coherence gate — the `!self.kind.requires_exe()` /
1099 /// `!self.kind.requires_servicos()` predicates that fence
1100 /// each code-surface slot from the wrong owning kind.
1101 /// - [`crate::LayoutInvariants::verify`]'s kind ↔ code-surface
1102 /// coherence gates — the six `caixa.kind == CaixaKind::X` /
1103 /// `caixa.kind != CaixaKind::X` predicates and the four kind-
1104 /// coherence error carriers (`SupervisorOwnsCode` /
1105 /// `AplicacaoOwnsCode` / `MeshSlotsOnNonAplicacao` /
1106 /// `SupervisorSlotsOnNonSupervisor` / `ServicoSlotsOnNonServico`
1107 /// / `ForeignCodeSlot`) which each name the offending caixa's
1108 /// variant in their `kind:` field.
1109 ///
1110 /// Prior to this lift the `.kind` field was accessed inline at
1111 /// twenty-plus production sites across `caixa-core` (the
1112 /// [`crate::render::require_kind`] entry-gate predicate + the
1113 /// [`crate::render::KindMismatch`] `actual:` field, the two `_view`
1114 /// composers, the `declared_foreign_code_slots` per-slot kind-
1115 /// coherence gate, and the six [`crate::LayoutInvariants::verify`]
1116 /// kind ↔ code-surface predicates + four error carriers) — a score
1117 /// of open-coded field-accesses that expressed no compile-time link
1118 /// back to the typed slot. A future extension of the `:kind` axis
1119 /// to a richer author surface — a per-`:kind` sub-variant discriminant
1120 /// (e.g. `Servico(ServicoRuntime)` splitting the current single
1121 /// variant across the wasm-component / legacy-container / native-
1122 /// binary runtime axes the M5 roadmap acknowledges), a per-cluster
1123 /// kind-overlay the M4 CR materializer resolves per-CR (the
1124 /// "cluster policy demotes `Aplicacao` to `Servico` on a single-
1125 /// tenant cluster" arm), a promotion of the plain [`CaixaKind`]
1126 /// enum to a richer `KindWithRuntime` discriminated on the
1127 /// component-model world axis — would have had to be threaded
1128 /// through every open-coded copy in lockstep or the entry gate,
1129 /// the view composers, and the layout invariants would silently
1130 /// disagree on which kind a given [`Caixa`] resolves to. Lifting
1131 /// the resolution to a typed method on the substrate primitive
1132 /// means every downstream consumer of the caixa's per-`Caixa`
1133 /// kind surface reaches for exactly one typed dispatch — the
1134 /// resolver's accept-set migrates as a unit on any future axis
1135 /// addition.
1136 ///
1137 /// First outer top-level [`Caixa`] `Copy`-return required-enum-
1138 /// discriminant accessor — opens the "outer [`Caixa`] `Copy`-return
1139 /// required-discriminant" projection pattern. Sibling in shape to
1140 /// the peer per-`:supervisor` [`crate::supervisor::SupervisorSpec::estrategia`]
1141 /// (eafb619), per-`:placement` [`crate::aplicacao::Placement::estrategia`]
1142 /// (921fe1b), and per-`:children` [`crate::supervisor::ChildSpec::restart`]
1143 /// (dfb4a81) `Copy`-return closed-set-enum discriminant accessors
1144 /// on the sibling nested-spec typed-slot discriminator axes,
1145 /// extended here to the outer top-level [`Caixa`] universal-axis
1146 /// surface. Named `kind()` to match the storage field's name;
1147 /// the accessor's identity maps onto the canonical CAIXA-SDLC §I
1148 /// vocabulary the slot's docstring already carries.
1149 #[must_use]
1150 pub fn kind(&self) -> CaixaKind {
1151 self.kind
1152 }
1153
1154 /// Substrate-canonical per-`Caixa` `:autores` universal-axis
1155 /// maintainer-name-list slice-accessor every consumer of the top-
1156 /// level manifest's maintainer axis keys off — returns the author-
1157 /// declared `:autores` list verbatim as a `&[String]` slice-view over
1158 /// the same backing buffer the raw `self.autores.as_slice()` field
1159 /// access borrows from. Empty-list-carrying (`:autores` is a default-
1160 /// empty axis every `defcaixa` form supplies with an empty `()` when
1161 /// unset; the [`Self::from_lisp`] derive folds an omitted `:autores`
1162 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1163 /// parse definitionally carries a `Vec<String>` slot — possibly
1164 /// empty — and the returned `&[String]` degenerates to an empty
1165 /// slice on that arm without any silent `None` collapse).
1166 ///
1167 /// The `:autores` slot carries the universal-axis maintainer-name
1168 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1169 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1170 /// `:versao` / `:kind`; the substrate-wide contact-carrying axis
1171 /// every downstream registry-facing artifact emits under) — the
1172 /// typed slot's `Vec<String>` accept-set (empty-per-entry rejected
1173 /// through [`ManifestError::AutorEmpty`], non-chart-maintainer-shape
1174 /// rejected through [`ManifestError::AutorInvalid`], cross-entry
1175 /// duplicate rejected through [`ManifestError::AutorDuplicate`]) maps
1176 /// onto every load-bearing downstream consumer the substrate carries
1177 /// — the [`Self::validate_autores`] universal-axis empty-per-entry +
1178 /// shape + duplicate gate at caixa-core/src/manifest.rs, the
1179 /// caixa-helm `build_chart_yaml` `maintainers:` fold at
1180 /// caixa-helm/src/lib.rs that walks each entry into a `Maintainer {
1181 /// name, email: None }` record, every future per-`Caixa` registry-
1182 /// facing renderer the CAIXA-SDLC §I roadmap acknowledges (the
1183 /// future `artifacthub.io/maintainers` `Chart.yaml` annotation the
1184 /// caixa-helm docstring alludes to at [`Self::validate_licenca`],
1185 /// the future per-cluster author-notification overlay the M4 CR
1186 /// materializer resolves per-CR).
1187 ///
1188 /// Prior to this lift the `.autores` field was accessed inline at
1189 /// two production sites — [`Self::validate_autores`]'s `for autor
1190 /// in &self.autores` walk that gates every entry through
1191 /// [`ManifestError::AutorEmpty`] / `AutorInvalid` / `AutorDuplicate`,
1192 /// and the caixa-helm `build_chart_yaml` `caixa.autores.iter().map(|a|
1193 /// Maintainer { name: a.clone(), email: None }).collect()` fold that
1194 /// materializes every entry into a `Chart.yaml` `maintainers:` row —
1195 /// two open-coded field-accesses that expressed no compile-time link
1196 /// back to the typed slot. A future extension of the `:autores` axis
1197 /// to a richer author surface — a per-`:autores` structured
1198 /// `Maintainer { name, email, url }` at the storage layer once the
1199 /// substrate absorbs `artifacthub.io/maintainers`' name+email+url
1200 /// tuple, a per-registry `:autores` allowlist the M4 CR materializer
1201 /// enforces per-CR (the "cluster policy demands every author declare
1202 /// an on-file `mailto:` contact" arm), a promotion of the plain
1203 /// `Vec<String>` byte-string list to a richer
1204 /// `Vec<ChartMaintainer>` newtype discriminated on the RFC-5322
1205 /// `<name> [<email>]` grammar the `is_chart_maintainer_name_shape`
1206 /// predicate already resolves through — would have had to be
1207 /// threaded through both open-coded copies in lockstep or the
1208 /// validate gate and the caixa-helm emit path would silently
1209 /// disagree on which authors a given [`Caixa`] resolves to (an
1210 /// author's `:autores ("alice" "bob")` would satisfy validate while
1211 /// the caixa-helm emit path silently rendered a drifted other
1212 /// maintainer list, or vice versa). Lifting the resolution to a
1213 /// typed method on the substrate primitive means every downstream
1214 /// consumer of the caixa's per-`Caixa` maintainer surface reaches
1215 /// for exactly one typed dispatch — the resolver's accept-set
1216 /// migrates as a unit on any future axis addition.
1217 ///
1218 /// First outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1219 /// opens the "outer [`Caixa`] `&[T]` slice" projection pattern the
1220 /// sibling per-`Caixa` `:etiquetas` / `:deps` / `:deps-dev` / `:exe`
1221 /// / `:bibliotecas` / `:servicos` / `:upgrade-from` / `:children`
1222 /// future lifts fold on. Sibling in shape to the peer per-`:supervisor`
1223 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce), per-`:placement`
1224 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7), per-`:membros`
1225 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36), per-`:contratos`
1226 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1227 /// per-`:upgrade-from :instructions` [`crate::upgrade::UpgradeFromEntry::instructions`]
1228 /// (0137e5a) `&[T]`-return slice accessors on the sibling per-M2 /
1229 /// per-M3 typed-slot list axes, extended here to the outer top-level
1230 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1231 /// `&Vec<String>`) because every downstream consumer of the author
1232 /// list treats it as a read-only sequence — the slice-view is the
1233 /// narrowest borrow that supports every present + roadmapped consumer
1234 /// (`.iter()`, `.len()`, `.is_empty()`) without leaking the backing
1235 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
1236 /// reaches for (the storage-side `Vec` remains reachable through the
1237 /// `pub autores` field for the mutation-carrying serde round-trip and
1238 /// per-test fixture-mutation paths). Named `autores()` to match the
1239 /// storage field's name; the accessor's identity maps onto the
1240 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
1241 /// carries.
1242 #[must_use]
1243 pub fn autores(&self) -> &[String] {
1244 self.autores.as_slice()
1245 }
1246
1247 /// Substrate-canonical per-`Caixa` `:etiquetas` universal-axis
1248 /// registry-search-tag-list slice-accessor every consumer of the
1249 /// top-level manifest's topical-tag axis keys off — returns the
1250 /// author-declared `:etiquetas` list verbatim as a `&[String]`
1251 /// slice-view over the same backing buffer the raw
1252 /// `self.etiquetas.as_slice()` field access borrows from. Empty-
1253 /// list-carrying (`:etiquetas` is a default-empty axis every
1254 /// `defcaixa` form supplies with an empty `()` when unset; the
1255 /// [`Self::from_lisp`] derive folds an omitted `:etiquetas` through
1256 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1257 /// definitionally carries a `Vec<String>` slot — possibly empty —
1258 /// and the returned `&[String]` degenerates to an empty slice on
1259 /// that arm without any silent `None` collapse).
1260 ///
1261 /// The `:etiquetas` slot carries the universal-axis topical-tag
1262 /// list every kind of caixa emits under (CAIXA-SDLC §I — the
1263 /// author-facing surface every `defcaixa` form supplies alongside
1264 /// `:nome` / `:versao` / `:kind`; the substrate-wide registry-
1265 /// search-facing axis every downstream registry-facing artifact
1266 /// emits under) — the typed slot's `Vec<String>` accept-set
1267 /// (empty-per-entry rejected through [`ManifestError::EtiquetaEmpty`],
1268 /// non-chart-keyword-shape rejected through
1269 /// [`ManifestError::EtiquetaInvalid`], cross-entry duplicate
1270 /// rejected through [`ManifestError::EtiquetaDuplicate`]) maps onto
1271 /// every load-bearing downstream consumer the substrate carries —
1272 /// the [`Self::validate_etiquetas`] universal-axis empty-per-entry
1273 /// + shape + duplicate gate at caixa-core/src/manifest.rs, the
1274 /// caixa-helm `build_chart_yaml` `keywords:` fold at
1275 /// caixa-helm/src/lib.rs that walks each entry into the rendered
1276 /// `Chart.yaml` `keywords:` array (chained with the
1277 /// [`crate::LAREIRA_CHART_KEYWORDS`] substrate-wide floor set and
1278 /// dedup'd through a `BTreeSet` at emit time), every future per-
1279 /// `Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
1280 /// acknowledges (the future `artifacthub.io/keywords` `Chart.yaml`
1281 /// annotation, the future per-cluster tag-notification overlay the
1282 /// M4 CR materializer resolves per-CR).
1283 ///
1284 /// Prior to this lift the `.etiquetas` field was accessed inline at
1285 /// two production sites — [`Self::validate_etiquetas`]'s `for
1286 /// etiqueta in &self.etiquetas` walk that gates every entry through
1287 /// [`ManifestError::EtiquetaEmpty`] / `EtiquetaInvalid` /
1288 /// `EtiquetaDuplicate`, and the caixa-helm `build_chart_yaml`
1289 /// `caixa.etiquetas.iter().cloned().chain(...)` fold that
1290 /// materializes every entry into a `Chart.yaml` `keywords:` row —
1291 /// two open-coded field-accesses that expressed no compile-time
1292 /// link back to the typed slot. A future extension of the
1293 /// `:etiquetas` axis to a richer tag surface — a per-`:etiquetas`
1294 /// structured `ChartKeyword { name, uri, category }` at the storage
1295 /// layer once the substrate absorbs `artifacthub.io/keywords`
1296 /// richer tag tuple, a per-registry `:etiquetas` allowlist the M4
1297 /// CR materializer enforces per-CR (the "cluster policy demands
1298 /// every tag come from a substrate-approved taxonomy" arm), a
1299 /// promotion of the plain `Vec<String>` byte-string list to a
1300 /// richer `Vec<ChartKeyword>` newtype discriminated on the DNS-
1301 /// 1123-label-shaped grammar the `is_chart_keyword_shape` predicate
1302 /// already resolves through — would have had to be threaded through
1303 /// both open-coded copies in lockstep or the validate gate and the
1304 /// caixa-helm emit path would silently disagree on which tags a
1305 /// given [`Caixa`] resolves to (an author's `:etiquetas ("demo"
1306 /// "aplicacao")` would satisfy validate while the caixa-helm emit
1307 /// path silently rendered a drifted other keyword list, or vice
1308 /// versa). Lifting the resolution to a typed method on the
1309 /// substrate primitive means every downstream consumer of the
1310 /// caixa's per-`Caixa` topical-tag surface reaches for exactly one
1311 /// typed dispatch — the resolver's accept-set migrates as a unit
1312 /// on any future axis addition.
1313 ///
1314 /// Second outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1315 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1316 /// [`Self::autores`] (b5d813f) opened, sibling in shape and
1317 /// idiom. The remaining unlifted outer-`Caixa` slice-carrying axes
1318 /// (`:deps` / `:deps-dev` / `:exe` / `:bibliotecas` / `:servicos`
1319 /// / `:upgrade-from` / `:children` / `:membros` / `:contratos`)
1320 /// fold onto the same pattern in future lifts. Sibling in shape to
1321 /// the peer per-`:supervisor`
1322 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1323 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1324 /// (a6e18d7), per-`:membros`
1325 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1326 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1327 /// (0dcc926), and per-`:upgrade-from :instructions`
1328 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1329 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1330 /// typed-slot list axes, extended here to the outer top-level
1331 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1332 /// `&Vec<String>`) because every downstream consumer of the tag
1333 /// list treats it as a read-only sequence — the slice-view is the
1334 /// narrowest borrow that supports every present + roadmapped
1335 /// consumer (`.iter()`, `.len()`, `.is_empty()`) without leaking
1336 /// the backing `Vec`'s grow/push/reserve surface no consumer of
1337 /// the typed view reaches for (the storage-side `Vec` remains
1338 /// reachable through the `pub etiquetas` field for the mutation-
1339 /// carrying serde round-trip and per-test fixture-mutation paths).
1340 /// Named `etiquetas()` to match the storage field's name; the
1341 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1342 /// vocabulary the slot's docstring already carries.
1343 #[must_use]
1344 pub fn etiquetas(&self) -> &[String] {
1345 self.etiquetas.as_slice()
1346 }
1347
1348 /// Substrate-canonical per-`Caixa` `:bibliotecas` universal-axis
1349 /// library-source-path-list slice-accessor every consumer of the
1350 /// top-level manifest's Biblioteca-source axis keys off — returns
1351 /// the author-declared `:bibliotecas` list verbatim as a
1352 /// `&[String]` slice-view over the same backing buffer the raw
1353 /// `self.bibliotecas.as_slice()` field access borrows from. Empty-
1354 /// list-carrying (`:bibliotecas` is a default-empty axis every
1355 /// `defcaixa` form supplies with an empty `()` when unset; the
1356 /// [`Self::from_lisp`] derive folds an omitted `:bibliotecas`
1357 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1358 /// parse definitionally carries a `Vec<String>` slot — possibly
1359 /// empty — and the returned `&[String]` degenerates to an empty
1360 /// slice on that arm without any silent `None` collapse).
1361 ///
1362 /// The `:bibliotecas` slot carries the universal-axis lisp-library
1363 /// entry-path list every `:kind Biblioteca` caixa emits under
1364 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1365 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1366 /// substrate-wide library-carrier axis every downstream
1367 /// authoring-facing consumer keys off) — the typed slot's
1368 /// `Vec<String>` accept-set (empty-per-entry rejected through
1369 /// [`ManifestError::CodePathEmpty { slot: ":bibliotecas" }`],
1370 /// non-sandboxed-relative-shape rejected through
1371 /// [`ManifestError::CodePathShape`], non-`.lisp`-extension rejected
1372 /// through [`ManifestError::CodePathNonLispExtension`], cross-entry
1373 /// duplicate rejected through [`ManifestError::CodePathDuplicate`])
1374 /// maps onto every load-bearing downstream consumer the substrate
1375 /// carries — the [`crate::LayoutInvariants`] Biblioteca-arm
1376 /// empty-check + per-entry file-exists loop at
1377 /// caixa-core/src/layout.rs that gates each entry through
1378 /// [`crate::LayoutError::MissingLib`] / `MissingEntry`, the
1379 /// [`Self::validate_code_paths`] per-slot shape gate at
1380 /// caixa-core/src/manifest.rs that walks each entry through the
1381 /// sandbox-relative / `.lisp`-extension / cross-entry duplicate
1382 /// gates, the `feira build` per-entry `tatara_lisp::read` parse
1383 /// walk at caixa-feira/src/cmd/build.rs that phase-1-checks each
1384 /// declared library file for lexical / structural errors before
1385 /// downstream `importar` resolution, every future per-`Caixa`
1386 /// library-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1387 /// (the future `tatara-lispc` compilation entry the docstring at
1388 /// caixa-feira/src/cmd/build.rs alludes to, the future per-cluster
1389 /// bytecode-caching overlay the M4 CR materializer resolves per-CR,
1390 /// the future `caixa-lsp` per-library semantic-token stream the
1391 /// caixa-lsp docstring roadmaps).
1392 ///
1393 /// Prior to this lift the `.bibliotecas` field was accessed inline
1394 /// at three production sites — [`crate::LayoutInvariants`]'s
1395 /// `caixa.bibliotecas.is_empty()` `MissingLib`-arm gate + `for p
1396 /// in &caixa.bibliotecas` `MissingEntry` walk that gates each
1397 /// declared library path through the on-disk-existence check,
1398 /// the compound-code-path `has_code = !caixa.bibliotecas.is_empty()
1399 /// || !caixa.exe.is_empty() || !caixa.servicos.is_empty()` OR-fold
1400 /// on the [`crate::LayoutError::SupervisorOwnsCode`] /
1401 /// `AplicacaoOwnsCode` kind-coherence gate, and the `feira build`
1402 /// per-entry `for entry in &caixa.bibliotecas` + `caixa.bibliotecas.
1403 /// len()` phase-1 tatara-lispc-precursor parse walk — three open-
1404 /// coded field-accesses that expressed no compile-time link back
1405 /// to the typed slot. A future extension of the `:bibliotecas`
1406 /// axis to a richer library surface — a per-`:bibliotecas`
1407 /// structured `BibliotecaEntry { path, edition, exports }` at the
1408 /// storage layer once the substrate absorbs the per-library
1409 /// language-edition + explicit-exports tuple the tatara-lisp
1410 /// module-system roadmap acknowledges, a per-registry
1411 /// `:bibliotecas` allowlist the M4 CR materializer enforces
1412 /// per-CR (the "cluster policy demands every biblioteca declare
1413 /// its own :edicao" arm), a promotion of the plain `Vec<String>`
1414 /// byte-string list to a richer `Vec<LibraryPath>` newtype
1415 /// discriminated on the `lib/<nome>.lisp`-shape grammar the
1416 /// [`crate::render::is_sandboxed_relative_path`] +
1417 /// [`crate::render::is_lisp_extension`] predicates already resolve
1418 /// through — would have had to be threaded through all three
1419 /// open-coded copies in lockstep or the layout gate, the shape
1420 /// validator, and the `feira build` phase-1 parse walk would
1421 /// silently disagree on which library paths a given [`Caixa`]
1422 /// resolves to (an author's `:bibliotecas ("lib/foo.lisp"
1423 /// "lib/bar.lisp")` would satisfy layout while `feira build`
1424 /// silently parsed a drifted other list, or vice versa). Lifting
1425 /// the resolution to a typed method on the substrate primitive
1426 /// means every downstream consumer of the caixa's per-`Caixa`
1427 /// library-source surface reaches for exactly one typed dispatch
1428 /// — the resolver's accept-set migrates as a unit on any future
1429 /// axis addition.
1430 ///
1431 /// Third outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1432 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1433 /// [`Self::autores`] (b5d813f) opened and [`Self::etiquetas`]
1434 /// (78c7d3c) folded on, sibling in shape and idiom. The remaining
1435 /// unlifted outer-`Caixa` slice-carrying axes (`:deps` /
1436 /// `:deps-dev` / `:exe` / `:servicos` / `:upgrade-from` /
1437 /// `:children` / `:membros` / `:contratos`) fold onto the same
1438 /// pattern in future lifts. Sibling in shape to the peer
1439 /// per-`:supervisor`
1440 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1441 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1442 /// (a6e18d7), per-`:membros`
1443 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1444 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1445 /// (0dcc926), and per-`:upgrade-from :instructions`
1446 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1447 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1448 /// typed-slot list axes, extended here to the outer top-level
1449 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1450 /// `&Vec<String>`) because every downstream consumer of the
1451 /// library-source list treats it as a read-only sequence — the
1452 /// slice-view is the narrowest borrow that supports every
1453 /// present + roadmapped consumer (`.iter()`, `.len()`,
1454 /// `.is_empty()`) without leaking the backing `Vec`'s
1455 /// grow/push/reserve surface no consumer of the typed view
1456 /// reaches for (the storage-side `Vec` remains reachable through
1457 /// the `pub bibliotecas` field for the mutation-carrying serde
1458 /// round-trip and per-test fixture-mutation paths). Named
1459 /// `bibliotecas()` to match the storage field's name; the
1460 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1461 /// vocabulary the slot's docstring already carries.
1462 #[must_use]
1463 pub fn bibliotecas(&self) -> &[String] {
1464 self.bibliotecas.as_slice()
1465 }
1466
1467 /// Substrate-canonical per-`Caixa` `:exe` universal-axis
1468 /// nix-built-executable-entry-path-list slice-accessor every consumer
1469 /// of the top-level manifest's Binario-executable axis keys off —
1470 /// returns the author-declared `:exe` list verbatim as a `&[String]`
1471 /// slice-view over the same backing buffer the raw
1472 /// `self.exe.as_slice()` field access borrows from. Empty-list-
1473 /// carrying (`:exe` is a default-empty axis every `defcaixa` form
1474 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1475 /// derive folds an omitted `:exe` through `#[serde(default)]` to
1476 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1477 /// `Vec<String>` slot — possibly empty — and the returned `&[String]`
1478 /// degenerates to an empty slice on that arm without any silent
1479 /// `None` collapse).
1480 ///
1481 /// The `:exe` slot carries the universal-axis nix-built executable
1482 /// entry-path list every `:kind Binario` caixa emits under
1483 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1484 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1485 /// substrate-wide `exe/`-directory-fenced entry-carrier axis every
1486 /// downstream flake-build-facing consumer keys off) — the typed
1487 /// slot's `Vec<String>` accept-set (empty-per-entry rejected
1488 /// through [`ManifestError::CodePathEmpty { slot: ":exe" }`],
1489 /// non-sandboxed-relative-shape rejected through
1490 /// [`ManifestError::CodePathShape`], cross-entry duplicate rejected
1491 /// through [`ManifestError::CodePathDuplicate`], out-of-`exe/`-
1492 /// directory paths rejected past the layout's
1493 /// [`crate::LayoutError::ExeOutsideDir`] `starts_with` fence) maps
1494 /// onto every load-bearing downstream consumer the substrate carries
1495 /// — the [`crate::LayoutInvariants`] Binario-arm empty-check +
1496 /// per-entry file-exists + `exe/`-directory-fence loop at
1497 /// caixa-core/src/layout.rs that gates each entry through
1498 /// [`crate::LayoutError::BinarioWithoutExe`] / `MissingEntry` /
1499 /// `ExeOutsideDir`, the compound `has_code` OR-fold on the
1500 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1501 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1502 /// that fences code-surface slots off from the two no-code kinds,
1503 /// [`Self::declared_foreign_code_slots`]'s `!self.exe.is_empty()`
1504 /// arm on the [`crate::LayoutError::ForeignCodeSlot`] gate that
1505 /// fences the `:exe` code surface off from every non-Binario code-
1506 /// running kind, [`Self::validate_code_paths`]'s per-slot shape gate
1507 /// that walks each entry through the sandbox-relative / cross-entry
1508 /// duplicate gates, every future per-`Caixa` executable-facing
1509 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1510 /// `caixa-flake` per-Binario `packages.<system>.<nome>` derivation
1511 /// entry the caixa-flake docstring roadmaps, the future per-cluster
1512 /// `nix-store` overlay the M4 CR materializer resolves per-CR, the
1513 /// future `feira nix` per-executable Binario-target emit path).
1514 ///
1515 /// Prior to this lift the `.exe` field was accessed inline at three
1516 /// production sites — the compound-code-path `has_code =
1517 /// !caixa.bibliotecas().is_empty() || !caixa.exe.is_empty() ||
1518 /// !caixa.servicos.is_empty()` OR-fold on the
1519 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1520 /// `AplicacaoOwnsCode` kind-coherence gate, the Binario-arm
1521 /// `caixa.exe.is_empty()` [`crate::LayoutError::BinarioWithoutExe`]
1522 /// gate, the per-entry `for p in &caixa.exe`
1523 /// `MissingEntry`/`ExeOutsideDir` walk, and the
1524 /// [`Self::declared_foreign_code_slots`]'s
1525 /// `!self.exe.is_empty()` arm on the `ForeignCodeSlot` gate — four
1526 /// open-coded field-accesses that expressed no compile-time link
1527 /// back to the typed slot. A future extension of the `:exe` axis
1528 /// to a richer executable surface — a per-`:exe` structured
1529 /// `BinarioEntry { path, wrapper, capabilities }` at the storage
1530 /// layer once the substrate absorbs the per-executable
1531 /// nix-wrapper + linux-capabilities tuple the CAIXA-SDLC §I
1532 /// executable roadmap acknowledges, a per-registry `:exe` allowlist
1533 /// the M4 CR materializer enforces per-CR (the "cluster policy
1534 /// demands every Binario declare an explicit `:wrapper`" arm), a
1535 /// promotion of the plain `Vec<String>` byte-string list to a
1536 /// richer `Vec<ExecutablePath>` newtype discriminated on the
1537 /// `exe/<nome>`-shape grammar the layout's `starts_with(exe_dir)`
1538 /// fence already resolves through — would have had to be threaded
1539 /// through all four open-coded copies in lockstep or the layout
1540 /// gate, the shape validator, and the `feira nix` emit path would
1541 /// silently disagree on which executable paths a given [`Caixa`]
1542 /// resolves to (an author's `:exe ("exe/cli" "exe/serve")` would
1543 /// satisfy layout while `feira nix` silently packaged a drifted
1544 /// other list, or vice versa). Lifting the resolution to a typed
1545 /// method on the substrate primitive means every downstream
1546 /// consumer of the caixa's per-`Caixa` executable-source surface
1547 /// reaches for exactly one typed dispatch — the resolver's accept-
1548 /// set migrates as a unit on any future axis addition.
1549 ///
1550 /// Fourth outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1551 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1552 /// [`Self::autores`] (b5d813f) opened, [`Self::etiquetas`]
1553 /// (78c7d3c) folded on, and [`Self::bibliotecas`] (8a36c23) closed
1554 /// the universal-axis text-tag family of. Opens the outer-`Caixa`
1555 /// foreign-code-slot `&[T]` sub-family the sibling `:servicos`
1556 /// future lift closes onto (per the trio of code-surface list slots
1557 /// the [`Self::validate_code_paths`] per-slot dispatch tuple
1558 /// already carries — `:bibliotecas` + `:exe` + `:servicos`, of which
1559 /// `:bibliotecas` landed at 8a36c23 and `:servicos` remains as the
1560 /// last unlifted code-surface slot). Sibling in shape to the peer
1561 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1562 /// (bc92bce), per-`:placement`
1563 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1564 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1565 /// (6c77e36), per-`:contratos`
1566 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1567 /// per-`:upgrade-from :instructions`
1568 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1569 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1570 /// typed-slot list axes, extended here to the outer top-level
1571 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1572 /// `&Vec<String>`) because every downstream consumer of the
1573 /// executable-source list treats it as a read-only sequence — the
1574 /// slice-view is the narrowest borrow that supports every
1575 /// present + roadmapped consumer (`.iter()`, `.len()`,
1576 /// `.is_empty()`) without leaking the backing `Vec`'s
1577 /// grow/push/reserve surface no consumer of the typed view
1578 /// reaches for (the storage-side `Vec` remains reachable through
1579 /// the `pub exe` field for the mutation-carrying serde
1580 /// round-trip and per-test fixture-mutation paths). Named `exe()`
1581 /// to match the storage field's name; the accessor's identity
1582 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1583 /// docstring already carries.
1584 #[must_use]
1585 pub fn exe(&self) -> &[String] {
1586 self.exe.as_slice()
1587 }
1588
1589 /// Substrate-canonical per-`Caixa` `:servicos` universal-axis
1590 /// ComputeUnit-CR-YAML-entry-path-list slice-accessor every consumer
1591 /// of the top-level manifest's Servico-component axis keys off —
1592 /// returns the author-declared `:servicos` list verbatim as a
1593 /// `&[String]` slice-view over the same backing buffer the raw
1594 /// `self.servicos.as_slice()` field access borrows from. Empty-list-
1595 /// carrying (`:servicos` is a default-empty axis every `defcaixa`
1596 /// form supplies with an empty `()` when unset; the
1597 /// [`Self::from_lisp`] derive folds an omitted `:servicos` through
1598 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1599 /// definitionally carries a `Vec<String>` slot — possibly empty —
1600 /// and the returned `&[String]` degenerates to an empty slice on
1601 /// that arm without any silent `None` collapse).
1602 ///
1603 /// The `:servicos` slot carries the universal-axis
1604 /// `.computeunit.yaml` ComputeUnit-CR entry-path list every
1605 /// `:kind Servico` caixa emits under (CAIXA-SDLC §I — the
1606 /// author-facing surface every `defcaixa` form supplies alongside
1607 /// `:nome` / `:versao` / `:kind`; the substrate-wide
1608 /// `servicos/`-directory-fenced entry-carrier axis every downstream
1609 /// Servico-facing renderer keys off) — the typed slot's
1610 /// `Vec<String>` accept-set (empty-per-entry rejected through
1611 /// [`ManifestError::CodePathEmpty { slot: ":servicos" }`],
1612 /// non-sandboxed-relative-shape rejected through
1613 /// [`ManifestError::CodePathShape`], non-`.computeunit.yaml`
1614 /// extension rejected through
1615 /// [`ManifestError::CodePathNonComputeUnitYamlExtension`], cross-
1616 /// entry duplicate rejected through
1617 /// [`ManifestError::CodePathDuplicate`], `len != 1` rejected by the
1618 /// V0 [`crate::ServicoCountMismatch`] gate on the per-Servico
1619 /// renderer entry-points, out-of-`servicos/`-directory paths
1620 /// rejected past the layout's [`crate::LayoutError::ServicoOutsideDir`]
1621 /// `starts_with` fence) maps onto every load-bearing downstream
1622 /// consumer the substrate carries — the [`crate::LayoutInvariants`]
1623 /// Servico-arm empty-check + per-entry file-exists + `servicos/`-
1624 /// directory-fence loop at caixa-core/src/layout.rs that gates each
1625 /// entry through [`crate::LayoutError::ServicoWithoutServicos`] /
1626 /// `MissingEntry` / `ServicoOutsideDir`, the compound `has_code`
1627 /// OR-fold on the [`crate::LayoutError::SupervisorOwnsCode`] /
1628 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1629 /// that fences code-surface slots off from the two no-code kinds,
1630 /// [`Self::declared_foreign_code_slots`]'s
1631 /// `!self.servicos.is_empty()` arm on the
1632 /// [`crate::LayoutError::ForeignCodeSlot`] gate that fences the
1633 /// `:servicos` code surface off from every non-Servico code-running
1634 /// kind, [`Self::validate_code_paths`]'s per-slot shape gate that
1635 /// walks each entry through the sandbox-relative / `.computeunit.
1636 /// yaml`-extension / cross-entry duplicate gates, the
1637 /// [`crate::require_single_servico`] V0 singularity gate every
1638 /// per-Servico renderer entry-point runs through
1639 /// [`crate::require_v0_servico_shape`], the `feira chart` /
1640 /// `feira deploy` per-verb `first_servico_path` walk at
1641 /// caixa-feira/src/cmd/chart.rs that resolves the singleton
1642 /// ComputeUnit-CR file, every future per-`Caixa` Servico-facing
1643 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1644 /// per-Servico OCI packager, the future M4
1645 /// `wasm.pleme.io/v1alpha1/ComputeUnit` CR materializer, the future
1646 /// per-Servico OTel collector-config emit).
1647 ///
1648 /// Prior to this lift the `.servicos` field was accessed inline at
1649 /// five production sites — the compound-code-path `has_code =
1650 /// !caixa.bibliotecas().is_empty() || !caixa.exe().is_empty() ||
1651 /// !caixa.servicos.is_empty()` OR-fold on the
1652 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1653 /// `AplicacaoOwnsCode` kind-coherence gate, the Servico-arm
1654 /// `caixa.servicos.is_empty()`
1655 /// [`crate::LayoutError::ServicoWithoutServicos`] gate, the
1656 /// per-entry `for p in &caixa.servicos`
1657 /// `MissingEntry`/`ServicoOutsideDir` walk, the
1658 /// [`Self::declared_foreign_code_slots`]'s
1659 /// `!self.servicos.is_empty()` arm on the `ForeignCodeSlot` gate,
1660 /// and the [`crate::require_single_servico`] V0 count gate's
1661 /// `caixa.servicos.len() == 1` / `caixa.servicos.len()` count
1662 /// projection (both the accept-arm predicate and the
1663 /// diagnostic-carrying `ServicoCountMismatch { count }`
1664 /// projection) — five open-coded field-accesses across three
1665 /// crates that expressed no compile-time link back to the typed
1666 /// slot. A future extension of the `:servicos` axis to a richer
1667 /// component surface — a per-`:servicos` structured
1668 /// `ServicoEntry { path, world, capabilities }` at the storage
1669 /// layer once the substrate absorbs the per-component WIT-world +
1670 /// capability-set tuple the CAIXA-SDLC §I Servico roadmap
1671 /// acknowledges, a per-registry `:servicos` allowlist the M4 CR
1672 /// materializer enforces per-CR (the "cluster policy demands every
1673 /// Servico declare an explicit `:world`" arm), a promotion of the
1674 /// plain `Vec<String>` byte-string list to a richer
1675 /// `Vec<ComputeUnitPath>` newtype discriminated on the
1676 /// `servicos/<nome>.computeunit.yaml`-shape grammar the layout's
1677 /// `starts_with(servicos_dir)` fence and the
1678 /// [`crate::render::is_computeunit_yaml_extension`] predicate
1679 /// already resolve through, a promotion of the V0 singleton
1680 /// contract to a multi-component `Vec<ComputeUnitPath>` past the M5
1681 /// component-model multi-world boundary — would have had to be
1682 /// threaded through all five open-coded copies in lockstep or the
1683 /// layout gate, the shape validator, the V0 count gate, and the
1684 /// `feira chart` / `feira deploy` entry-point walks would silently
1685 /// disagree on which ComputeUnit-CR paths a given [`Caixa`]
1686 /// resolves to (an author's `:servicos ("servicos/foo.computeunit.
1687 /// yaml")` would satisfy layout while `feira chart` silently
1688 /// packaged a drifted other list, or vice versa). Lifting the
1689 /// resolution to a typed method on the substrate primitive means
1690 /// every downstream consumer of the caixa's per-`Caixa`
1691 /// ComputeUnit-CR-source surface reaches for exactly one typed
1692 /// dispatch — the resolver's accept-set migrates as a unit on any
1693 /// future axis addition.
1694 ///
1695 /// Fifth and final outer top-level [`Caixa`] `&[T]`-return slice-
1696 /// accessor — folds on the "outer [`Caixa`] `&[T]` slice"
1697 /// projection pattern [`Self::autores`] (b5d813f) opened,
1698 /// [`Self::etiquetas`] (78c7d3c) folded on, [`Self::bibliotecas`]
1699 /// (8a36c23) closed the universal-axis text-tag family of, and
1700 /// [`Self::exe`] (65d9527) opened the foreign-code-slot sub-family
1701 /// of. Closes the outer-`Caixa` foreign-code-slot `&[T]` sub-family
1702 /// — with `:bibliotecas`, `:exe`, and `:servicos` now each carrying
1703 /// a substrate-canonical slice accessor, the trio of code-surface
1704 /// list slots the [`Self::validate_code_paths`] per-slot dispatch
1705 /// tuple carries is complete on the typed dispatch surface (the
1706 /// internal `[(":bibliotecas", &self.bibliotecas, ..), (":exe",
1707 /// &self.exe, ..), (":servicos", &self.servicos, ..)]` per-slot
1708 /// dispatch tuple's homogeneous `&Vec<String>`-typed shape blocks a
1709 /// per-element accessor swap in isolation — a future companion lift
1710 /// promotes the tuple's element type to `&[String]` and threads the
1711 /// triple of typed dispatches through as a unit). Sibling in shape
1712 /// to the peer per-`:supervisor`
1713 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1714 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1715 /// (a6e18d7), per-`:membros`
1716 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1717 /// per-`:contratos`
1718 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1719 /// per-`:upgrade-from :instructions`
1720 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1721 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1722 /// typed-slot list axes, extended here to the outer top-level
1723 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1724 /// `&Vec<String>`) because every downstream consumer of the
1725 /// ComputeUnit-CR-source list treats it as a read-only sequence —
1726 /// the slice-view is the narrowest borrow that supports every
1727 /// present + roadmapped consumer (`.iter()`, `.len()`,
1728 /// `.is_empty()`, `.first()`) without leaking the backing `Vec`'s
1729 /// grow/push/reserve surface no consumer of the typed view reaches
1730 /// for (the storage-side `Vec` remains reachable through the
1731 /// `pub servicos` field for the mutation-carrying serde round-trip
1732 /// and per-test fixture-mutation paths, and for the
1733 /// [`Self::validate_code_paths`] per-slot dispatch tuple whose
1734 /// homogeneous-element-type shape carries the raw field access
1735 /// until the trio-closure lift promotes the tuple as a unit).
1736 /// Named `servicos()` to match the storage field's name; the
1737 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1738 /// vocabulary the slot's docstring already carries.
1739 #[must_use]
1740 pub fn servicos(&self) -> &[String] {
1741 self.servicos.as_slice()
1742 }
1743
1744 /// Substrate-canonical per-`Caixa` `:deps` universal-axis
1745 /// runtime-dependency-declaration-list slice-accessor every consumer
1746 /// of the top-level manifest's runtime-dep-graph axis keys off —
1747 /// returns the author-declared `:deps` list verbatim as a `&[Dep]`
1748 /// slice-view over the same backing buffer the raw
1749 /// `self.deps.as_slice()` field access borrows from. Empty-list-
1750 /// carrying (`:deps` is a default-empty axis every `defcaixa` form
1751 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1752 /// derive folds an omitted `:deps` through `#[serde(default)]` to
1753 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1754 /// `Vec<Dep>` slot — possibly empty — and the returned `&[Dep]`
1755 /// degenerates to an empty slice on that arm without any silent
1756 /// `None` collapse).
1757 ///
1758 /// The `:deps` slot carries the universal-axis runtime dependency
1759 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1760 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1761 /// `:versao` / `:kind`; the substrate-wide runtime-closure-input axis
1762 /// every downstream resolver-facing artifact emits under) — the
1763 /// typed slot's `Vec<Dep>` accept-set (empty-`:nome` rejected through
1764 /// [`DepError::NomeEmpty`], non-DNS-1123-label `:nome` rejected
1765 /// through [`DepError::NomeInvalid`], malformed `:versao` rejected
1766 /// through [`DepError::VersaoInvalid`], empty `:fonte.repo` rejected
1767 /// through [`DepError::FonteRepoEmpty`], within-list duplicate `:nome`
1768 /// rejected through [`DepError::DuplicateNome { list: ":deps" }`])
1769 /// maps onto every load-bearing downstream consumer the substrate
1770 /// carries — the [`Self::validate_deps`] per-entry
1771 /// [`Dep::validate`] + within-list dedup walk at
1772 /// caixa-core/src/manifest.rs, the [`crate::dep::validate_no_self_dep`]
1773 /// cross-list self-reference gate at caixa-core/src/layout.rs that
1774 /// checks each entry against the caixa's own `:nome`, the
1775 /// caixa-resolver `for dep in &root.deps` closure walk at
1776 /// caixa-resolver/src/resolve.rs that seeds every git-clone target
1777 /// through the resolver's [`crate::Dep`]-keyed pipeline, the
1778 /// caixa-crd `caixa.deps.iter().map(dep_into_ref).collect()` fold at
1779 /// caixa-crd/src/conversion.rs that materializes each entry into the
1780 /// K8s `Caixa` CR's `spec.deps` field, every future per-`Caixa`
1781 /// resolver-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1782 /// (the future per-cluster runtime-closure-audit overlay the M4 CR
1783 /// materializer resolves per-CR, the future `lacre.lisp` BLAKE3-
1784 /// closure emit walk the caixa-resolver docstring roadmaps).
1785 ///
1786 /// First outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1787 /// opens the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1788 /// sibling `:deps-dev` future lift closes on. Peer of the closed
1789 /// outer-`Caixa` foreign-code-slot `&[String]` sub-family
1790 /// ([`Self::bibliotecas`] 8a36c23, [`Self::exe`] 65d9527,
1791 /// [`Self::servicos`] 611f78b) and the outer-`Caixa` universal-axis
1792 /// text-tag family ([`Self::autores`] b5d813f, [`Self::etiquetas`]
1793 /// 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice" projection
1794 /// pattern onto a novel element-type axis (`Dep` composite vs the
1795 /// prior sibling family's `String` scalar). Sibling in shape to the
1796 /// peer per-`:supervisor`
1797 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1798 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1799 /// (a6e18d7), per-`:membros`
1800 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1801 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1802 /// (0dcc926), and per-`:upgrade-from :instructions`
1803 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1804 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1805 /// typed-slot list axes, extended here to the outer top-level
1806 /// [`Caixa`] universal-axis dep-graph surface. Returns `&[Dep]`
1807 /// (not `&Vec<Dep>`) because every downstream consumer of the
1808 /// runtime-dep list treats it as a read-only sequence — the slice-
1809 /// view is the narrowest borrow that supports every present +
1810 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1811 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1812 /// of the typed view reaches for (the storage-side `Vec` remains
1813 /// reachable through the `pub deps` field for the mutation-carrying
1814 /// serde round-trip and per-test fixture-mutation paths). Named
1815 /// `deps()` to match the storage field's name; the accessor's
1816 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1817 /// slot's docstring already carries.
1818 #[must_use]
1819 pub fn deps(&self) -> &[Dep] {
1820 self.deps.as_slice()
1821 }
1822
1823 /// Substrate-canonical per-`Caixa` `:deps-dev` universal-axis
1824 /// development-only-dependency-declaration-list slice-accessor every
1825 /// consumer of the top-level manifest's dev-dep-graph axis keys off —
1826 /// returns the author-declared `:deps-dev` list verbatim as a `&[Dep]`
1827 /// slice-view over the same backing buffer the raw
1828 /// `self.deps_dev.as_slice()` field access borrows from. Empty-list-
1829 /// carrying (`:deps-dev` is a default-empty axis every `defcaixa`
1830 /// form supplies with an empty `()` when unset; the
1831 /// [`Self::from_lisp`] derive folds an omitted `:deps-dev` through
1832 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1833 /// definitionally carries a `Vec<Dep>` slot — possibly empty — and
1834 /// the returned `&[Dep]` degenerates to an empty slice on that arm
1835 /// without any silent `None` collapse).
1836 ///
1837 /// The `:deps-dev` slot carries the universal-axis dev-only
1838 /// dependency list every kind of caixa emits under (CAIXA-SDLC §I —
1839 /// the author-facing sibling of `:deps` that every `defcaixa` form
1840 /// supplies to declare tests / lint / bench closures the runtime
1841 /// `:deps` axis does not carry; the substrate-wide dev-closure-input
1842 /// axis every downstream test-facing artifact emits under, matching
1843 /// Cargo's `[dev-dependencies]` table's dev-time-only visibility
1844 /// contract) — the typed slot's `Vec<Dep>` accept-set (empty-`:nome`
1845 /// rejected through [`DepError::NomeEmpty`], non-DNS-1123-label
1846 /// `:nome` rejected through [`DepError::NomeInvalid`], malformed
1847 /// `:versao` rejected through [`DepError::VersaoInvalid`], empty
1848 /// `:fonte.repo` rejected through [`DepError::FonteRepoEmpty`],
1849 /// within-list duplicate `:nome` rejected through
1850 /// [`DepError::DuplicateNome { list: ":deps-dev" }`]) maps onto every
1851 /// load-bearing downstream consumer the substrate carries — the
1852 /// [`Self::validate_deps`] per-entry [`Dep::validate`] + within-list
1853 /// dedup walk at caixa-core/src/manifest.rs, the
1854 /// [`crate::dep::validate_no_self_dep`] cross-list self-reference
1855 /// gate at caixa-core/src/layout.rs that checks each entry against
1856 /// the caixa's own `:nome`, the caixa-resolver
1857 /// `for dep in &root.deps_dev` closure walk at
1858 /// caixa-resolver/src/resolve.rs that seeds every dev-only git-clone
1859 /// target through the resolver's [`crate::Dep`]-keyed pipeline, and
1860 /// every future per-`Caixa` resolver-facing renderer the CAIXA-SDLC
1861 /// §I roadmap acknowledges (the future per-cluster dev-closure-audit
1862 /// overlay the M4 CR materializer resolves per-CR, the future
1863 /// `lacre.lisp` BLAKE3-closure emit walk the caixa-resolver docstring
1864 /// roadmaps).
1865 ///
1866 /// Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1867 /// closes the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1868 /// sibling [`Self::deps`] (ad34b4e) opened on. The two accessors
1869 /// jointly close the two-list dep-graph surface every downstream
1870 /// resolver-facing consumer keys off (runtime `:deps` +
1871 /// dev-only `:deps-dev`, the canonical Cargo-shaped dependency-table
1872 /// pair the [`Self::validate_deps`] gate already walks in canonical
1873 /// order). Peer of the closed outer-`Caixa` foreign-code-slot
1874 /// `&[String]` sub-family ([`Self::bibliotecas`] 8a36c23,
1875 /// [`Self::exe`] 65d9527, [`Self::servicos`] 611f78b) and the outer-
1876 /// `Caixa` universal-axis text-tag family ([`Self::autores`]
1877 /// b5d813f, [`Self::etiquetas`] 78c7d3c) — folds the "outer
1878 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
1879 /// dev-dep composite-element axis (`Dep` composite, matching the
1880 /// [`Self::deps`] element type). Sibling in shape to the peer
1881 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1882 /// (bc92bce), per-`:placement`
1883 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1884 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1885 /// (6c77e36), per-`:contratos`
1886 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1887 /// per-`:upgrade-from :instructions`
1888 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1889 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1890 /// typed-slot list axes, folded here to the outer top-level
1891 /// [`Caixa`] universal-axis dev-dep-graph surface. Returns `&[Dep]`
1892 /// (not `&Vec<Dep>`) because every downstream consumer of the
1893 /// dev-dep list treats it as a read-only sequence — the slice-view
1894 /// is the narrowest borrow that supports every present +
1895 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1896 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1897 /// of the typed view reaches for (the storage-side `Vec` remains
1898 /// reachable through the `pub deps_dev` field for the mutation-
1899 /// carrying serde round-trip and per-test fixture-mutation paths).
1900 /// Named `deps_dev()` to match the storage field's `snake_case` name;
1901 /// the kebab-case author-surface tag `:deps-dev` is the same axis
1902 /// after tatara-lisp's kebab↔snake fold and the accessor's identity
1903 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1904 /// docstring already carries.
1905 #[must_use]
1906 pub fn deps_dev(&self) -> &[Dep] {
1907 self.deps_dev.as_slice()
1908 }
1909
1910 /// Substrate-canonical per-[`Caixa`] typed-dispatch read accessor
1911 /// every consumer that walks one of the two dep-list axes keyed on a
1912 /// [`crate::dep::DepList`] discriminant reaches for — routes the
1913 /// `(list: DepList) -> &[Dep]` projection through one typed method on
1914 /// the substrate primitive rather than the prior open-coded
1915 /// `match list { Prod => caixa.deps(), Dev => caixa.deps_dev() }`
1916 /// inline dispatch every per-axis walker would otherwise carry.
1917 /// Returns the author-declared per-list `Vec<Dep>` verbatim as a
1918 /// `&[Dep]` slice-view over the same backing buffer the sibling
1919 /// [`Self::deps`] (`Prod`) / [`Self::deps_dev`] (`Dev`) per-slot
1920 /// accessors borrow from, preserving the empty-list-carrying invariant
1921 /// each per-slot accessor already establishes (`:deps` / `:deps-dev`
1922 /// are default-empty axes every `defcaixa` form supplies with an empty
1923 /// `()` when unset; the [`Self::from_lisp`] derive folds an omitted
1924 /// list through `#[serde(default)]` to `Vec::new()`, so both arms
1925 /// definitionally carry a `Vec<Dep>` slot — possibly empty — and the
1926 /// returned `&[Dep]` degenerates to an empty slice on either arm
1927 /// without any silent `None` collapse).
1928 ///
1929 /// The [`crate::dep::DepList`] closed-set typed enum is the
1930 /// substrate's canonical discriminator for the "runtime-closure
1931 /// `:deps` vs dev-only-closure `:deps-dev`" axis every dep-list
1932 /// consumer dispatches on — the compiler-checked exhaustiveness on
1933 /// the enum's `match` arms is the build-time guarantee that no future
1934 /// per-list read-site regresses to a bare-`bool`-flag inline dispatch
1935 /// that a future third dep-list axis (a `:deps-build` build-only
1936 /// closure once the substrate grows cross-artifact heterogeneous
1937 /// dep-graphs, per CAIXA-SDLC §I) would silently split at every
1938 /// consumer. Prior to this the read side carried two per-slot
1939 /// accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`]) and no
1940 /// typed dispatch that a per-axis walker could parametrise on, so
1941 /// every per-list walker (the [`Self::validate_deps`] per-list
1942 /// [`crate::render::insert_first_seen`] dedup walk, a future
1943 /// `feira app graph` per-list dep summary, a future M4 per-cluster
1944 /// dev-closure-audit overlay the CR materializer resolves per-CR)
1945 /// open-coded the same two-block "run over `:deps`, then run over
1946 /// `:deps-dev`" pattern — a silent duplication that a future third
1947 /// dep-list axis would have had to grow a third block at every site.
1948 ///
1949 /// Peer of the sibling [`Self::push_dep`] typed-mutation dispatch
1950 /// (359fba5) — closes the two-side dispatch symmetry on the outer
1951 /// [`Caixa`] two-list dep-graph surface: `push_dep` on the mutation
1952 /// side, `deps_of` on the read side, both keyed on the same
1953 /// [`crate::dep::DepList`] discriminator. Same "one typed dispatch on
1954 /// the substrate primitive, thin projections at each consumer"
1955 /// discipline the sibling per-slot read accessors ([`Self::nome`]
1956 /// e6b7d97, [`Self::versao`], [`Self::kind`]) carry — extended onto
1957 /// the outer-[`Caixa`] typed-dispatch read surface.
1958 #[must_use]
1959 pub fn deps_of(&self, list: crate::dep::DepList) -> &[Dep] {
1960 match list {
1961 crate::dep::DepList::Prod => self.deps(),
1962 crate::dep::DepList::Dev => self.deps_dev(),
1963 }
1964 }
1965
1966 /// Substrate-canonical per-[`Caixa`] typed-mutation dispatch every
1967 /// consumer that appends to one of the two dep-list axes keys off
1968 /// — routes the `(list: DepList, dep: Dep)` tuple through one typed
1969 /// method on the substrate primitive rather than the prior
1970 /// `feira add`-side open-coded `if self.dev { &mut caixa.deps_dev }
1971 /// else { &mut caixa.deps }` inline dispatch + open-coded
1972 /// `.iter().any(|d| d.nome == …)` dup-check cascade. Refuses the
1973 /// mutation with the canonical typed [`DepError::DuplicateNome`] on
1974 /// a within-list name collision — the same `list: &'static str`
1975 /// diagnostic shape [`Self::validate_deps`]'s per-list
1976 /// [`crate::render::insert_first_seen`] walk raises on the peer
1977 /// parse-time within-list dedup axis, so a future author reading a
1978 /// `feira add` refusal and a `feira build` refusal reaches for the
1979 /// same corrective surface without switching diagnostic idioms.
1980 ///
1981 /// The two-arm [`crate::dep::DepList`] enum is the substrate's
1982 /// closed-set typed carrier for the "runtime-closure `:deps` vs
1983 /// dev-only-closure `:deps-dev`" axis every dep-list consumer
1984 /// dispatches on — the compiler-checked exhaustiveness on the
1985 /// enum's `match` arms is the build-time guarantee that no future
1986 /// per-list mutation-site regresses to a bare-`bool`-flag
1987 /// (`is_dev: bool`) inline dispatch that a future third
1988 /// dep-list axis (a `:deps-build` build-only closure once the
1989 /// substrate grows cross-artifact heterogeneous dep-graphs, per
1990 /// CAIXA-SDLC §I) would silently split at every consumer.
1991 ///
1992 /// Same "one typed dispatch on the substrate primitive, thin
1993 /// projections at each consumer" discipline the sibling per-slot
1994 /// read accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`],
1995 /// [`Self::nome`] e6b7d97, [`Self::versao`], [`Self::kind`])
1996 /// carry — extended onto the outer-[`Caixa`] typed-mutation surface,
1997 /// the substrate's first typed-mutation dispatch on the top-level
1998 /// manifest. The prior `feira add` open-coded `&mut caixa.deps` /
1999 /// `&mut caixa.deps_dev` inline field-access + `bail!` string-
2000 /// diagnostic path routed no through-line back to the typed slot,
2001 /// so a future extension of either dep-list axis to a richer author
2002 /// surface (a per-cluster override the operator pins through a
2003 /// future `:placement`-scoped dep-list slot the CAIXA-SDLC §I
2004 /// roadmap acknowledges, an M4
2005 /// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-CR
2006 /// admission-webhook that normalized the list at admission time)
2007 /// would have had to be threaded through the `feira add` mutation
2008 /// site in lockstep with every read consumer or one path would
2009 /// silently disagree with the other on which list a given dep lands
2010 /// in. Lifting the resolution rule to a typed method on the
2011 /// substrate primitive means every downstream dep-list-mutating
2012 /// consumer of the top-level manifest reaches for exactly one typed
2013 /// dispatch — the resolver's accept-set migrates as a unit on any
2014 /// future axis addition.
2015 ///
2016 /// # Errors
2017 ///
2018 /// Returns [`DepError::DuplicateNome`] with `list = list.as_str()`
2019 /// when another entry in the same list already carries the same
2020 /// `:nome` — the mutation is refused and the caller can surface the
2021 /// typed diagnostic to the author (the `feira add` verb routes the
2022 /// error through `anyhow::Error::from`, which preserves the
2023 /// canonical `#[error(...)]`-templated diagnostic body).
2024 pub fn push_dep(&mut self, list: crate::dep::DepList, dep: Dep) -> Result<(), DepError> {
2025 let target = match list {
2026 crate::dep::DepList::Prod => &mut self.deps,
2027 crate::dep::DepList::Dev => &mut self.deps_dev,
2028 };
2029 if target.iter().any(|d| d.nome() == dep.nome()) {
2030 return Err(DepError::DuplicateNome {
2031 nome: dep.nome().to_string(),
2032 list: list.as_str(),
2033 });
2034 }
2035 target.push(dep);
2036 Ok(())
2037 }
2038
2039 /// Substrate-canonical per-`Caixa` `:limits` M2 typed-slot outer-
2040 /// composite Lunatic-per-process wasm32-sandboxing-composite optional-
2041 /// composite-reference accessor every consumer of the top-level
2042 /// manifest's per-Servico [`LimitsSpec`] outer-composite reader keys
2043 /// off — returns the author-declared `:limits` typed composite
2044 /// verbatim as an `Option<&LimitsSpec>` reference over the same
2045 /// backing storage the raw `self.limits.as_ref()` field access
2046 /// borrows from, with `None` naming the "no `:limits` block
2047 /// authored — every per-axis Lunatic-sandbox cap defers to the
2048 /// wasm-engine-default arm named on the per-axis
2049 /// [`LimitsSpec::memory`] / [`LimitsSpec::fuel`] /
2050 /// [`LimitsSpec::wall_clock`] / [`LimitsSpec::cpu`] scalar-accessor
2051 /// docstrings" partition every downstream Servico-M2-overlay
2052 /// emitter treats as "emit nothing" and the sibling
2053 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate
2054 /// treats as "skip the per-axis
2055 /// [`crate::LimitsError::MemoryZero`] / `MemoryBelowWasm32Page` /
2056 /// `FuelZero` / `WallClockZero` / `CpuZero` refusal cascade".
2057 ///
2058 /// The outer `:limits` slot carries the M2 Servico-runtime typed
2059 /// composite — the load-bearing container of every Lunatic-shaped
2060 /// per-process wasm32-sandbox cap axis every long-running wasm
2061 /// component's runtime dispatches on (INSPIRATIONS §III.1 —
2062 /// Lunatic per-process linear-memory / fuel / wall-clock /
2063 /// millicore cap primitives translated onto pleme-io's typed
2064 /// `:limits :memory` / `:limits :fuel` / `:limits :wall-clock` /
2065 /// `:limits :cpu` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2066 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2067 /// chart both fan on). Every per-`:limits` axis threads through a
2068 /// lifted per-slot accessor on the [`LimitsSpec`] type: the
2069 /// [`LimitsSpec::memory`] wasm32 linear-memory byte-cap scalar
2070 /// accessor, the [`LimitsSpec::fuel`] wasmtime fuel-cap scalar
2071 /// accessor, the [`LimitsSpec::wall_clock`] per-call wall-clock
2072 /// deadline scalar accessor, and the [`LimitsSpec::cpu`]
2073 /// K8s-millicore soft-CPU-share scalar accessor. Every downstream
2074 /// consumer that reaches for a limits axis first passes through
2075 /// this outer accessor onto the composite and then dispatches
2076 /// onto the per-axis accessor — the two-level dispatch means
2077 /// every per-`:limits` reader now routes through a typed dispatch
2078 /// on the substrate primitive at both altitudes.
2079 ///
2080 /// Prior to this lift the `.limits` `Option<LimitsSpec>` composite
2081 /// was accessed inline at three production sites — the
2082 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate's
2083 /// `if let Some(l) = &caixa.limits { … }` traversal head
2084 /// (caixa-core/src/layout.rs:882, which drives the per-axis
2085 /// refusal cascade on the composite: the `LimitsError::MemoryZero`
2086 /// / `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Max` /
2087 /// `FuelZero` / `FuelExceedsMax` / `WallClockZero` /
2088 /// `WallClockExceedsMax` / `CpuZero` / `CpuExceedsMax` refusals
2089 /// [`LimitsSpec::validate`] fans onto), the
2090 /// [`crate::render::servico_m2_overlay`] per-Servico M2 overlay
2091 /// emitter's `if let Some(limits) = &caixa.limits { … }` traversal
2092 /// head (caixa-core/src/render.rs:18504, which drives the
2093 /// `M2_KEY_LIMITS`-keyed `limits.is_empty()`-gated `serde_yaml`
2094 /// projection every `caixa-helm` / `caixa-flux` Servico values-
2095 /// block emitter fans on), and the
2096 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2097 /// set enumerator's `self.limits.is_some()` presence probe
2098 /// (caixa-core/src/manifest.rs:1788, which drives the
2099 /// `M2_AUTHOR_KEY_LIMITS` kebab-case author-label push every
2100 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2101 /// gate reads) — three open-coded outer-field accesses that
2102 /// expressed no compile-time link back to the typed slot at the
2103 /// [`Caixa`] altitude. A future extension of the `:limits` outer
2104 /// axis to a richer author surface (a multi-`:limits` list the M4
2105 /// CR materializer resolves per-CR at admission time so a Servico
2106 /// can expose a compute-heavy + IO-heavy limits pair, a per-
2107 /// cluster `:limits-overrides` slot the operator pins so a
2108 /// cluster-specific policy can tighten a caixa-declared cap
2109 /// without re-authoring the `caixa.lisp`, a promotion of the
2110 /// plain `Option<LimitsSpec>` to a richer
2111 /// `{static, dynamic}` partition once the wasm-engine's runtime-
2112 /// resolved dynamic-cap surface lands) would have had to be
2113 /// threaded through all three open-coded copies in lockstep or
2114 /// one consumer would silently disagree with the peers on which
2115 /// limits composite a given Caixa resolves to — the layout gate's
2116 /// per-axis bracket-dispatch seed reading the raw slot while the
2117 /// peer `servico_m2_overlay` emitter read an operator-resolved
2118 /// slot would silently split the build-time sandbox-shape gate
2119 /// from the runtime `ComputeUnit` CR emission gate, a three-
2120 /// consumer split at the layout gate, the M2 overlay emitter, and
2121 /// the declared-slot enumerator far from the source `caixa.lisp`
2122 /// with no field naming the limits-drift root cause. Lifting the
2123 /// resolution rule to a typed method on the substrate primitive
2124 /// means every downstream consumer of the caixa's per-`Caixa`
2125 /// Lunatic-sandboxing outer-composite surface reaches for exactly
2126 /// one typed dispatch — the resolver's accept-set migrates as a
2127 /// unit on any future axis addition.
2128 ///
2129 /// First outer top-level [`Caixa`] `Option<&Composite>`-return
2130 /// composite-reference accessor — opens the outer-`Caixa`
2131 /// `Option<&Composite>` composite-reference projection pattern the
2132 /// sibling per-`Caixa` `:behavior` [`crate::BehaviorSpec`] /
2133 /// `:politicas` [`crate::aplicacao::MeshPolicy`] / `:placement`
2134 /// [`crate::aplicacao::Placement`] / `:entrada`
2135 /// [`crate::aplicacao::Entrada`] future outer-composite lifts
2136 /// fold on. Peer of the M3 mesh-slot outer-composite family the
2137 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2138 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2139 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2140 /// accessors already close on the outer [`crate::AplicacaoSpec`]
2141 /// altitude — extends that "one typed dispatch on the substrate
2142 /// primitive, thin projections at each consumer" discipline onto
2143 /// the outer top-level [`Caixa`] altitude, opening the M2 Servico-
2144 /// runtime slot family's outer-composite axis. Returns
2145 /// `Option<&LimitsSpec>` (not the owning composite by copy or
2146 /// clone) because every downstream consumer of the limits
2147 /// composite treats it as a read-only per-axis dispatch source —
2148 /// the reference-view is the narrowest borrow that supports every
2149 /// present + roadmapped consumer (per-axis accessor dispatch,
2150 /// `.is_empty()`-gated overlay projection, presence-probe early
2151 /// return on the "author-omitted `:limits` ⇒ engine-default
2152 /// applies" partition) without cloning the composite through
2153 /// every consumer's fast path. The `Option` half of the return-
2154 /// type preserves the load-bearing "author-omitted `:limits` ⇒
2155 /// engine-default applies" partition (not a default composite the
2156 /// downstream must reject on emptiness) — the accessor projects
2157 /// the raw `Option<LimitsSpec>` slot's presence bit through the
2158 /// reference-return unchanged. Named `limits()` to match the
2159 /// storage field's name verbatim and the tatara-lisp author-
2160 /// surface term (`:limits`) the field's own docstring already
2161 /// carries.
2162 #[must_use]
2163 pub fn limits(&self) -> Option<&LimitsSpec> {
2164 self.limits.as_ref()
2165 }
2166
2167 /// Substrate-canonical per-`Caixa` `:behavior` M2 typed-slot outer-
2168 /// composite OTP-`gen_server`-shaped callback-table optional-
2169 /// composite-reference accessor every consumer of the top-level
2170 /// manifest's per-Servico [`BehaviorSpec`] outer-composite reader
2171 /// keys off — returns the author-declared `:behavior` typed
2172 /// composite verbatim as an `Option<&BehaviorSpec>` reference over
2173 /// the same backing storage the raw `self.behavior.as_ref()` field
2174 /// access borrows from, with `None` naming the "no `:behavior`
2175 /// block authored — every per-callback OTP-shaped hook defers to
2176 /// the wasm-engine's runtime default arm named on the per-axis
2177 /// [`BehaviorSpec::on_init`] / [`BehaviorSpec::on_call`] /
2178 /// [`BehaviorSpec::on_cast`] / [`BehaviorSpec::on_info`] /
2179 /// [`BehaviorSpec::on_state_change`] /
2180 /// [`BehaviorSpec::on_terminate`] scalar-accessor docstrings"
2181 /// partition every downstream Servico-M2-overlay emitter treats as
2182 /// "emit nothing" and the sibling [`crate::StandardLayout::verify`]
2183 /// per-`:behavior` shape gate treats as "skip the per-arm
2184 /// [`crate::behavior::BehaviorError`] refusal cascade + the
2185 /// per-callback on-disk `MissingEntry` existence check".
2186 ///
2187 /// The outer `:behavior` slot carries the M2 Servico-runtime typed
2188 /// composite — the load-bearing container of every OTP-shaped
2189 /// per-Servico lifecycle-callback path axis every long-running wasm
2190 /// component's runtime dispatches on (INSPIRATIONS §II.3 — Erlang/
2191 /// OTP `gen_server:init/1` / `handle_call/3` / `handle_cast/2` /
2192 /// `handle_info/2` / `code_change/3` / `terminate/2` primitives
2193 /// translated onto pleme-io's typed `:behavior :on-init` /
2194 /// `:on-call` / `:on-cast` / `:on-info` / `:on-state-change` /
2195 /// `:on-terminate` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2196 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2197 /// chart both fan on). Every per-`:behavior` axis threads through a
2198 /// lifted per-callback accessor on the [`BehaviorSpec`] type
2199 /// (9b4ecde / d66c702 / 156ddbe / 99616ac / 4846cef / 701add7).
2200 /// Every downstream consumer that reaches for a behavior axis
2201 /// first passes through this outer accessor onto the composite
2202 /// and then dispatches onto the per-callback accessor — the
2203 /// two-level dispatch means every per-`:behavior` reader now
2204 /// routes through a typed dispatch on the substrate primitive at
2205 /// both altitudes.
2206 ///
2207 /// Composes cross-slot with the M2 `:upgrade-from` gate: the
2208 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2209 /// cross-slot composition gate at [`crate::StandardLayout::verify`]
2210 /// keys the "per-version `:state-change` instruction must have a
2211 /// `:on-state-change` callback" precondition off this accessor's
2212 /// composite (the callback-side counterpart to the
2213 /// `:upgrade-from :instructions :state-change :script` refusal at
2214 /// the appup-side). Threading that gate's traversal input through
2215 /// this accessor closes the cross-slot invariant on the substrate
2216 /// primitive, not on the raw field.
2217 ///
2218 /// Prior to this lift the `.behavior` `Option<BehaviorSpec>`
2219 /// composite was accessed inline at four production sites — the
2220 /// [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
2221 /// `if let Some(b) = &caixa.behavior { … }` traversal head
2222 /// (caixa-core/src/layout.rs:896, which drives the per-arm
2223 /// `BehaviorError` refusal cascade + the per-callback on-disk
2224 /// [`crate::LayoutError::MissingEntry`] existence check under
2225 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]),
2226 /// the [`crate::upgrade::validate_upgrade_from_against_behavior`]
2227 /// cross-slot composition gate's `caixa.behavior.as_ref()`
2228 /// traversal-input feed (caixa-core/src/layout.rs:1008, which
2229 /// drives the `:state-change` ↔ `:on-state-change` precondition
2230 /// refusal), the [`crate::render::servico_m2_overlay`] per-Servico
2231 /// M2 overlay emitter's `if let Some(behavior) = &caixa.behavior
2232 /// { … }` traversal head (caixa-core/src/render.rs:18513, which
2233 /// drives the `M2_KEY_BEHAVIOR`-keyed `behavior.is_empty()`-gated
2234 /// `serde_yaml` projection every `caixa-helm` / `caixa-flux`
2235 /// Servico values-block emitter fans on), and the
2236 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2237 /// set enumerator's `self.behavior.is_some()` presence probe
2238 /// (caixa-core/src/manifest.rs:1919, which drives the
2239 /// `M2_AUTHOR_KEY_BEHAVIOR` kebab-case author-label push every
2240 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2241 /// gate reads) — four open-coded outer-field accesses that
2242 /// expressed no compile-time link back to the typed slot at the
2243 /// [`Caixa`] altitude. A future extension of the `:behavior`
2244 /// outer axis to a richer author surface (a per-callback overlay
2245 /// resolver the operator materializes at admission time so a
2246 /// cluster-specific policy can inject a per-callback tracing
2247 /// interceptor without re-authoring the `caixa.lisp`, a promotion
2248 /// of the plain `Option<BehaviorSpec>` to a richer `{static,
2249 /// dynamic}` partition once a runtime-resolved behavior-swap
2250 /// surface lands, the M4 per-callback middleware chain the
2251 /// caixa-operator's per-Servico admission webhook keys off) would
2252 /// have had to be threaded through all four open-coded copies in
2253 /// lockstep or one consumer would silently disagree with the
2254 /// peers on which behavior composite a given Caixa resolves to —
2255 /// the layout gate's per-callback existence-check seed reading
2256 /// the raw slot while the peer `servico_m2_overlay` emitter read
2257 /// an operator-resolved slot would silently split the build-time
2258 /// callback-shape gate from the runtime `ComputeUnit` CR emission
2259 /// gate from the cross-slot `:state-change` composition gate from
2260 /// the M2 declared-slot enumerator, a four-consumer split far
2261 /// from the source `caixa.lisp` with no field naming the
2262 /// behavior-drift root cause. Lifting the resolution rule to a
2263 /// typed method on the substrate primitive means every downstream
2264 /// consumer of the caixa's per-`Caixa` OTP-callback-table outer-
2265 /// composite surface reaches for exactly one typed dispatch — the
2266 /// resolver's accept-set migrates as a unit on any future axis
2267 /// addition.
2268 ///
2269 /// Second outer top-level [`Caixa`] `Option<&Composite>`-return
2270 /// composite-reference accessor — sibling to the opening
2271 /// [`Self::limits`] (b2bd9d7) accessor on the outer-`Caixa`
2272 /// `Option<&Composite>` composite-reference sub-family, extends
2273 /// the "one typed dispatch on the substrate primitive, thin
2274 /// projections at each consumer" discipline onto the second of
2275 /// the three M2 Servico-runtime slots. The remaining
2276 /// `Option<&Composite>` axes at the outer top-level [`Caixa`]
2277 /// altitude — the M3 mesh-slot family (`:politicas`,
2278 /// `:placement`, `:entrada` — already closed on the inner
2279 /// [`crate::AplicacaoSpec`] altitude via 534dc21 / 9abb8f0 /
2280 /// d32111c) — remain the future sibling lifts on the outer
2281 /// top-level projection. Returns `Option<&BehaviorSpec>` (not
2282 /// the owning composite by copy or clone) because every
2283 /// downstream consumer of the behavior composite treats it as a
2284 /// read-only per-callback dispatch source — the reference-view is
2285 /// the narrowest borrow that supports every present + roadmapped
2286 /// consumer (per-callback accessor dispatch, `.is_empty()`-gated
2287 /// overlay projection, presence-probe early return on the
2288 /// "author-omitted `:behavior` ⇒ runtime-default applies"
2289 /// partition, cross-slot `:state-change` composition input)
2290 /// without cloning the composite through every consumer's fast
2291 /// path. The `Option` half of the return-type preserves the
2292 /// load-bearing "author-omitted `:behavior` ⇒ runtime-default
2293 /// applies" partition (not a default composite the downstream
2294 /// must reject on emptiness) — the accessor projects the raw
2295 /// `Option<BehaviorSpec>` slot's presence bit through the
2296 /// reference-return unchanged. Named `behavior()` to match the
2297 /// storage field's name verbatim and the tatara-lisp author-
2298 /// surface term (`:behavior`) the field's own docstring already
2299 /// carries.
2300 #[must_use]
2301 pub fn behavior(&self) -> Option<&crate::BehaviorSpec> {
2302 self.behavior.as_ref()
2303 }
2304
2305 /// Substrate-canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
2306 /// composite MESH-COMPOSITION-shaped mesh-policy optional-composite-
2307 /// reference accessor every consumer of the top-level manifest's
2308 /// per-Aplicacao [`crate::aplicacao::MeshPolicy`] outer-composite
2309 /// reader keys off — returns the author-declared `:politicas` typed
2310 /// composite verbatim as an `Option<&MeshPolicy>` reference over the
2311 /// same backing storage the raw `self.politicas.as_ref()` field
2312 /// access borrows from, with `None` naming the "no `:politicas`
2313 /// block authored — every per-axis mesh-policy scalar defers to the
2314 /// cluster-default arm named on the per-axis
2315 /// [`crate::aplicacao::MeshPolicy::timeout`] /
2316 /// [`crate::aplicacao::MeshPolicy::retries`] /
2317 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] /
2318 /// [`crate::aplicacao::MeshPolicy::mtls_required`] /
2319 /// [`crate::aplicacao::MeshPolicy::rate_limit`] scalar-accessor
2320 /// docstrings" partition every downstream caixa-mesh /
2321 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2322 /// "emit no per-`:politicas` overlay" and the sibling
2323 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2324 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2325 /// arm.
2326 ///
2327 /// The outer `:politicas` slot carries the M3 mesh-slot per-
2328 /// Aplicacao typed composite — the load-bearing container of every
2329 /// mesh-level policy axis every Cilium NetworkPolicy / Gateway API
2330 /// v1.x HTTPRoute / future M4 per-edge policy overlay emitter fans
2331 /// on (MESH-COMPOSITION §III.2 — the Aplicacao's typed mesh-policy
2332 /// composite; §V — the "no infinite blocking" per-call deadline +
2333 /// "sandboxing-by-default" mTLS-enforcement CSE invariants; §III.3
2334 /// — the typed inter-Servico contrato-edge overlay the per-`(:de,
2335 /// :para)` mesh renderer keys off). Every per-`:politicas` axis
2336 /// threads through a lifted per-slot accessor on the
2337 /// [`crate::aplicacao::MeshPolicy`] type: the
2338 /// [`crate::aplicacao::MeshPolicy::mtls_required`] (c0110f1) Cilium
2339 /// mTLS-enforcement toggle, the
2340 /// [`crate::aplicacao::MeshPolicy::retries`] (bdfb399) transient-
2341 /// failure retry budget, the [`crate::aplicacao::MeshPolicy::timeout`]
2342 /// (7073d0f) Gateway-API per-call deadline, the
2343 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] (b0e741a)
2344 /// Envoy-outlier-detection composite. Every downstream consumer
2345 /// that reaches for a mesh-policy axis first passes through this
2346 /// outer accessor onto the composite and then dispatches onto the
2347 /// per-axis accessor — the two-level dispatch means every per-
2348 /// `:politicas` reader now routes through a typed dispatch on the
2349 /// substrate primitive at both altitudes.
2350 ///
2351 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2352 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2353 /// author-omitted arm onto the [`crate::aplicacao::MeshPolicy::default`]
2354 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::politicas`]
2355 /// (534dc21) `&MeshPolicy`-return accessor observes a typed
2356 /// composite whether or not the author declared the outer slot.
2357 /// The outer accessor preserves the "author-omitted vs authored-
2358 /// empty" partition the inner accessor's `is_empty()`-gated
2359 /// renderer overlay collapses — routing the presence bit through
2360 /// this accessor keeps the [`Self::declared_mesh_slots`] M3 kind-
2361 /// coherence enumerator's `M3_AUTHOR_KEY_POLITICAS` push separate
2362 /// from the inner `MeshPolicy::is_empty()`-gated overlay elision.
2363 ///
2364 /// Prior to this lift the `.politicas` `Option<MeshPolicy>`
2365 /// composite was accessed inline at two production sites — the
2366 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2367 /// `self.politicas.clone().unwrap_or_default()` traversal head
2368 /// (caixa-core/src/manifest.rs:1899, which drives the fold onto
2369 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2370 /// arm the inner [`crate::AplicacaoSpec::politicas`] accessor
2371 /// then observes), and the [`Self::declared_mesh_slots`] M3
2372 /// declared-slot-set enumerator's `self.politicas.is_some()`
2373 /// presence probe (caixa-core/src/manifest.rs:1961, which drives
2374 /// the `M3_AUTHOR_KEY_POLITICAS` kebab-case author-label push
2375 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2376 /// coherence gate reads) — two open-coded outer-field accesses
2377 /// that expressed no compile-time link back to the typed slot at
2378 /// the [`Caixa`] altitude. A future extension of the `:politicas`
2379 /// outer axis to a richer author surface (a per-cluster
2380 /// `:politicas-overrides` slot the operator materializes at
2381 /// admission time so a cluster-specific policy can tighten the
2382 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2383 /// promotion of the plain `Option<MeshPolicy>` to a richer
2384 /// `{static, dynamic}` partition once the M4 per-edge
2385 /// contrato-scoped policy-override surface lands, the M5 traffic-
2386 /// shaping composition the caixa-operator's per-Aplicacao mesh
2387 /// admission webhook keys off) would have had to be threaded
2388 /// through both open-coded copies in lockstep or the Aplicacao-
2389 /// composition seed's default-fold arm would silently disagree
2390 /// with the M3 declared-slot enumerator on which policy composite
2391 /// a given Caixa resolves to — the seed reading an operator-
2392 /// resolved slot while the enumerator's presence probe read the
2393 /// raw slot would silently split the build-time mesh-artifact
2394 /// emission gate from the M3 declared-slot enumerator's kind-
2395 /// coherence gate, a two-consumer split far from the source
2396 /// `caixa.lisp` with no field naming the policy-drift root cause.
2397 /// Lifting the resolution rule to a typed method on the substrate
2398 /// primitive means every downstream consumer of the caixa's per-
2399 /// `Caixa` MESH-COMPOSITION mesh-policy outer-composite surface
2400 /// reaches for exactly one typed dispatch — the resolver's
2401 /// accept-set migrates as a unit on any future axis addition.
2402 ///
2403 /// Third outer top-level [`Caixa`] `Option<&Composite>`-return
2404 /// composite-reference accessor — sibling to the opening
2405 /// [`Self::limits`] (b2bd9d7) and [`Self::behavior`] (35d8b52)
2406 /// accessors on the outer-`Caixa` `Option<&Composite>` composite-
2407 /// reference sub-family, extends the "one typed dispatch on the
2408 /// substrate primitive, thin projections at each consumer"
2409 /// discipline onto the first of the three M3 mesh-slot axes.
2410 /// Peer of the closed inner mesh-slot outer-composite family the
2411 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2412 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2413 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2414 /// accessor pins already close on the inner [`crate::AplicacaoSpec`]
2415 /// altitude — opens the outer top-level [`Caixa`] altitude's M3
2416 /// mesh-slot arm of the composite-reference family the remaining
2417 /// two axes (`:placement`, `:entrada`) fold onto in future
2418 /// sibling lifts. Returns `Option<&MeshPolicy>` (not the owning
2419 /// composite by copy or clone) because every downstream consumer
2420 /// of the mesh-policy composite treats it as a read-only per-axis
2421 /// dispatch source — the reference-view is the narrowest borrow
2422 /// that supports every present + roadmapped consumer (per-axis
2423 /// accessor dispatch, `.is_empty()`-gated overlay projection,
2424 /// presence-probe early return on the "author-omitted `:politicas`
2425 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2426 /// seed's default-fold arm) without cloning the composite through
2427 /// every consumer's fast path. The `Option` half of the return-
2428 /// type preserves the load-bearing "author-omitted `:politicas` ⇒
2429 /// cluster-default applies" partition (not a default composite
2430 /// the downstream must reject on emptiness) — the accessor
2431 /// projects the raw `Option<MeshPolicy>` slot's presence bit
2432 /// through the reference-return unchanged. Named `politicas()` to
2433 /// match the storage field's name verbatim and the tatara-lisp
2434 /// author-surface term (`:politicas`) the field's own docstring
2435 /// already carries.
2436 #[must_use]
2437 pub fn politicas(&self) -> Option<&crate::aplicacao::MeshPolicy> {
2438 self.politicas.as_ref()
2439 }
2440
2441 /// Substrate-canonical per-`Caixa` `:placement` M3 mesh-slot outer-
2442 /// composite MESH-COMPOSITION-shaped distribution optional-composite-
2443 /// reference accessor every consumer of the top-level manifest's
2444 /// per-Aplicacao [`crate::aplicacao::Placement`] outer-composite
2445 /// reader keys off — returns the author-declared `:placement` typed
2446 /// composite verbatim as an `Option<&Placement>` reference over the
2447 /// same backing storage the raw `self.placement.as_ref()` field
2448 /// access borrows from, with `None` naming the "no `:placement`
2449 /// block authored — every per-axis placement scalar defers to the
2450 /// cluster-default arm named on the per-axis
2451 /// [`crate::aplicacao::Placement::estrategia`] /
2452 /// [`crate::aplicacao::Placement::clusters`] /
2453 /// [`crate::aplicacao::Placement::affinity`] /
2454 /// [`crate::aplicacao::Placement::shard_key`] scalar-accessor
2455 /// docstrings" partition every downstream caixa-mesh /
2456 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2457 /// "emit no per-`:placement` overlay" and the sibling
2458 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2459 /// the [`crate::aplicacao::Placement::default`] cluster-default arm.
2460 ///
2461 /// The outer `:placement` slot carries the M3 mesh-slot per-
2462 /// Aplicacao typed distribution composite — the load-bearing
2463 /// container of every where-does-this-Aplicacao-run axis every
2464 /// caixa-mesh programs.yaml per-cluster distribution overlay /
2465 /// caixa-flux per-Aplicacao GitRepository/HelmRelease fan-out /
2466 /// future M4 per-Aplicacao Akka-style cluster-sharding entity-id
2467 /// resolver emitter fans on (MESH-COMPOSITION §II.4 — the
2468 /// Aplicacao's typed distribution composite; §V CSE invariants —
2469 /// "distribution is a first-class typed composite, not a runtime
2470 /// scheduler hint" the per-axis scalars enforce; §III.3 — the
2471 /// typed inter-Servico contrato-edge overlay the per-cluster
2472 /// mesh renderer keys off). Every per-`:placement` axis threads
2473 /// through a lifted per-slot accessor on the
2474 /// [`crate::aplicacao::Placement`] type: the
2475 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2476 /// MESH-COMPOSITION distribution-strategy scalar, the
2477 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7) per-cluster
2478 /// distribution-target slice, the [`crate::aplicacao::Placement::affinity`]
2479 /// M3-Adaptive-compression-hint optional-scalar, and the
2480 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) Akka-cluster-
2481 /// sharding extractor-expression optional-scalar. Every downstream
2482 /// consumer that reaches for a placement axis first passes through
2483 /// this outer accessor onto the composite and then dispatches onto
2484 /// the per-axis accessor — the two-level dispatch means every per-
2485 /// `:placement` reader now routes through a typed dispatch on the
2486 /// substrate primitive at both altitudes.
2487 ///
2488 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2489 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2490 /// author-omitted arm onto the [`crate::aplicacao::Placement::default`]
2491 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::placement`]
2492 /// (9abb8f0) `&Placement`-return accessor observes a typed composite
2493 /// whether or not the author declared the outer slot. The outer
2494 /// accessor preserves the "author-omitted vs authored-empty" partition
2495 /// the inner accessor collapses at the cluster-default fold —
2496 /// routing the presence bit through this accessor keeps the
2497 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2498 /// `M3_AUTHOR_KEY_PLACEMENT` push separate from the inner
2499 /// [`crate::AplicacaoSpec::validate_placement`]-gated overlay
2500 /// dispatch.
2501 ///
2502 /// Prior to this lift the `.placement` `Option<Placement>`
2503 /// composite was accessed inline at two production sites — the
2504 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2505 /// `self.placement.clone().unwrap_or_default()` traversal head
2506 /// (caixa-core/src/manifest.rs:2036, which drives the fold onto
2507 /// the [`crate::aplicacao::Placement::default`] cluster-default
2508 /// arm the inner [`crate::AplicacaoSpec::placement`] accessor
2509 /// then observes), and the [`Self::declared_mesh_slots`] M3
2510 /// declared-slot-set enumerator's `self.placement.is_some()`
2511 /// presence probe (caixa-core/src/manifest.rs:2100, which drives
2512 /// the `M3_AUTHOR_KEY_PLACEMENT` kebab-case author-label push
2513 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2514 /// coherence gate reads) — two open-coded outer-field accesses
2515 /// that expressed no compile-time link back to the typed slot at
2516 /// the [`Caixa`] altitude. A future extension of the `:placement`
2517 /// outer axis to a richer author surface (a per-cluster
2518 /// `:placement-overrides` slot the operator materializes at
2519 /// admission time so a cluster-specific placement can tighten the
2520 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2521 /// per-tenant placement-alias table the M4
2522 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer resolves
2523 /// per-CR at admission time, a promotion of the plain
2524 /// `Option<Placement>` to a richer `{static, dynamic}` partition
2525 /// once Orleans-style virtual-actor dynamic placement comes into
2526 /// typed scope) would have had to be threaded through both open-
2527 /// coded copies in lockstep or the Aplicacao-composition seed's
2528 /// default-fold arm would silently disagree with the M3 declared-
2529 /// slot enumerator on which distribution composite a given Caixa
2530 /// resolves to — the seed reading an operator-resolved slot while
2531 /// the enumerator's presence probe read the raw slot would
2532 /// silently split the build-time distribution-artifact emission
2533 /// gate from the M3 declared-slot enumerator's kind-coherence
2534 /// gate, a two-consumer split far from the source `caixa.lisp`
2535 /// with no field naming the distribution-drift root cause.
2536 /// Lifting the resolution rule to a typed method on the substrate
2537 /// primitive means every downstream consumer of the caixa's per-
2538 /// `Caixa` MESH-COMPOSITION distribution outer-composite surface
2539 /// reaches for exactly one typed dispatch — the resolver's
2540 /// accept-set migrates as a unit on any future axis addition.
2541 ///
2542 /// Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
2543 /// composite-reference accessor — sibling to the opening
2544 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) M2-
2545 /// Servico-runtime pair and the peer [`Self::politicas`] (5d23d29)
2546 /// M3-mesh-slot arm on the outer-`Caixa` `Option<&Composite>`
2547 /// composite-reference sub-family, folds on the "one typed
2548 /// dispatch on the substrate primitive, thin projections at each
2549 /// consumer" discipline extended onto the second of the three M3
2550 /// mesh-slot axes. Peer of the closed inner mesh-slot outer-
2551 /// composite family the sibling
2552 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2553 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2554 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2555 /// accessor pins already close on the inner
2556 /// [`crate::AplicacaoSpec`] altitude — folds on the outer top-
2557 /// level [`Caixa`] altitude's M3 mesh-slot arm the sibling
2558 /// [`Self::politicas`] opened, extending the discipline onto the
2559 /// second of the three M3 mesh-slot axes. The remaining M3
2560 /// mesh-slot axis (`:entrada`) folds onto this accessor's
2561 /// discipline in the final sibling lift, closing the outer top-
2562 /// level [`Caixa`] `Option<&Composite>` M3 mesh-slot sub-family.
2563 /// Returns `Option<&Placement>` (not the owning composite by copy
2564 /// or clone) because every downstream consumer of the placement
2565 /// composite treats it as a read-only per-axis dispatch source —
2566 /// the reference-view is the narrowest borrow that supports every
2567 /// present + roadmapped consumer (per-axis accessor dispatch,
2568 /// serde composite-serialization on the programs.yaml overlay,
2569 /// presence-probe early return on the "author-omitted `:placement`
2570 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2571 /// seed's default-fold arm) without cloning the composite through
2572 /// every consumer's fast path. The `Option` half of the return-
2573 /// type preserves the load-bearing "author-omitted `:placement` ⇒
2574 /// cluster-default applies" partition (not a default composite
2575 /// the downstream must reject on emptiness) — the accessor
2576 /// projects the raw `Option<Placement>` slot's presence bit
2577 /// through the reference-return unchanged. Named `placement()` to
2578 /// match the storage field's name verbatim and the tatara-lisp
2579 /// author-surface term (`:placement`) the field's own docstring
2580 /// already carries.
2581 #[must_use]
2582 pub fn placement(&self) -> Option<&crate::aplicacao::Placement> {
2583 self.placement.as_ref()
2584 }
2585
2586 /// Substrate-canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
2587 /// composite MESH-COMPOSITION-shaped external-gateway optional-
2588 /// composite-reference accessor every consumer of the top-level
2589 /// manifest's per-Aplicacao [`crate::aplicacao::Entrada`] outer-
2590 /// composite reader keys off — returns the author-declared
2591 /// `:entrada` typed composite verbatim as an `Option<&Entrada>`
2592 /// reference over the same backing storage the raw
2593 /// `self.entrada.as_ref()` field access borrows from, with `None`
2594 /// naming the "no `:entrada` block authored — this Aplicacao is
2595 /// cluster-internal, no `Gateway`/`HTTPRoute` fan-out emitted"
2596 /// partition every downstream caixa-mesh Gateway-API artifact
2597 /// emitter treats as "emit no gateway-listener + no `HTTPRoute`
2598 /// backend for this Aplicacao" and the sibling
2599 /// [`Self::aplicacao_view`] Aplicacao-composition seed forwards
2600 /// verbatim (unlike the peer `:politicas` / `:placement` arms,
2601 /// `:entrada` has no cluster-default fold — an omitted `:entrada`
2602 /// stays `None` on the projected [`crate::AplicacaoSpec`] and the
2603 /// peer inner [`crate::AplicacaoSpec::entrada`] accessor observes
2604 /// the same `Option<&Entrada>` presence bit unchanged).
2605 ///
2606 /// The outer `:entrada` slot carries the M3 mesh-slot per-
2607 /// Aplicacao typed external-gateway composite — the load-bearing
2608 /// container of every how-does-the-outside-world-reach-this-
2609 /// Aplicacao axis every caixa-mesh `Gateway`/`HTTPRoute` fan-out
2610 /// emitter fans on (MESH-COMPOSITION §II.5 — the Aplicacao's typed
2611 /// external-entry composite; §V CSE invariants — "the external
2612 /// gateway is a first-class typed composite, not a per-Servico
2613 /// ingress annotation" the per-axis scalars enforce; §III.4 — the
2614 /// typed hostname + backend-Servico pair the per-cluster Gateway-
2615 /// API renderer keys off). Every per-`:entrada` axis threads
2616 /// through a lifted per-slot accessor on the
2617 /// [`crate::aplicacao::Entrada`] type: the
2618 /// [`crate::aplicacao::Entrada::host`] Gateway-API `Listener.hostname`
2619 /// scalar, the [`crate::aplicacao::Entrada::para`] backend-Servico
2620 /// caixa-name scalar, the [`crate::aplicacao::Entrada::paths`]
2621 /// per-rule `HTTPPathMatch` list, the [`crate::aplicacao::Entrada::port`]
2622 /// backend `trigger.service.port` scalar, and the
2623 /// [`crate::aplicacao::Entrada::resolved_paths`] URL-path fallback
2624 /// resolver every HTTPRoute-aware renderer consumes. Every
2625 /// downstream consumer that reaches for an entry axis first passes
2626 /// through this outer accessor onto the composite and then
2627 /// dispatches onto the per-axis accessor — the two-level dispatch
2628 /// means every per-`:entrada` reader now routes through a typed
2629 /// dispatch on the substrate primitive at both altitudes.
2630 ///
2631 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2632 /// seed: the Aplicacao-view builder forwards the outer `Option`
2633 /// arm verbatim (no default fold — `:entrada` is inherently
2634 /// optional; a cluster-internal Aplicacao has no external gateway
2635 /// at all, not "an external gateway that defaults to nothing"), so
2636 /// the peer inner [`crate::AplicacaoSpec::entrada`] (d32111c)
2637 /// `Option<&Entrada>`-return accessor observes the same presence
2638 /// bit whether or not the author declared the outer slot. Routing
2639 /// the presence bit through this accessor keeps the
2640 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2641 /// `M3_AUTHOR_KEY_ENTRADA` push separate from the inner
2642 /// [`crate::AplicacaoSpec::validate_entrada`]-gated
2643 /// hostname/backend/path emission dispatch.
2644 ///
2645 /// Prior to this lift the `.entrada` `Option<Entrada>` composite
2646 /// was accessed inline at two production sites — the
2647 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2648 /// `self.entrada.clone()` traversal head (caixa-core/src/manifest.rs:2182,
2649 /// which drives the forward onto the peer inner
2650 /// [`crate::AplicacaoSpec::entrada`] accessor the caixa-mesh
2651 /// Gateway-API fan-out then observes), and the
2652 /// [`Self::declared_mesh_slots`] M3 declared-slot-set enumerator's
2653 /// `self.entrada.is_some()` presence probe (caixa-core/src/manifest.rs:2248,
2654 /// which drives the `M3_AUTHOR_KEY_ENTRADA` kebab-case author-
2655 /// label push every [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
2656 /// kind-coherence gate reads) — two open-coded outer-field
2657 /// accesses that expressed no compile-time link back to the typed
2658 /// slot at the [`Caixa`] altitude. A future extension of the
2659 /// `:entrada` outer axis to a richer author surface (a per-cluster
2660 /// `:entrada-overrides` slot the operator materializes at admission
2661 /// time so a cluster-specific hostname can pin the caixa-declared
2662 /// bound without re-authoring the `caixa.lisp`, a per-tenant
2663 /// gateway-alias table the M4 `mesh.pleme.io/v1alpha1/Aplicacao`
2664 /// CR materializer resolves per-CR at admission time, a promotion
2665 /// of the plain `Option<Entrada>` to a richer
2666 /// `{public, private, internal}` partition once Cilium-identity-
2667 /// scoped internal gateways come into typed scope) would have had
2668 /// to be threaded through both open-coded copies in lockstep or the
2669 /// Aplicacao-composition seed's forward arm would silently
2670 /// disagree with the M3 declared-slot enumerator on which external-
2671 /// gateway composite a given Caixa resolves to — the seed reading
2672 /// an operator-resolved slot while the enumerator's presence probe
2673 /// read the raw slot would silently split the build-time gateway-
2674 /// artifact emission gate from the M3 declared-slot enumerator's
2675 /// kind-coherence gate, a two-consumer split far from the source
2676 /// `caixa.lisp` with no field naming the entry-drift root cause.
2677 /// Lifting the resolution rule to a typed method on the substrate
2678 /// primitive means every downstream consumer of the caixa's per-
2679 /// `Caixa` MESH-COMPOSITION external-gateway outer-composite
2680 /// surface reaches for exactly one typed dispatch — the resolver's
2681 /// accept-set migrates as a unit on any future axis addition.
2682 ///
2683 /// Fifth and final outer top-level [`Caixa`] `Option<&Composite>`-
2684 /// return composite-reference accessor — closes the outer-`Caixa`
2685 /// `Option<&Composite>` composite-reference sub-family opened by
2686 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) on the
2687 /// M2 Servico-runtime arm and extended onto the M3 mesh-slot arm
2688 /// by [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074),
2689 /// folds on the "one typed dispatch on the substrate primitive,
2690 /// thin projections at each consumer" discipline extended onto the
2691 /// third and final M3 mesh-slot axis. Peer of the closed inner
2692 /// mesh-slot outer-composite family the sibling
2693 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2694 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2695 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2696 /// accessor pins already close on the inner
2697 /// [`crate::AplicacaoSpec`] altitude — this lift closes the mirror
2698 /// sub-family on the outer top-level [`Caixa`] altitude, so both
2699 /// altitudes of the outer-composite reference-return discipline
2700 /// (per-`Caixa` outer-slot presence + per-`AplicacaoSpec` inner-
2701 /// slot presence) now carry the full five-arm accept-set behind a
2702 /// typed dispatch on the substrate primitive. Returns
2703 /// `Option<&Entrada>` (not the owning composite by copy or clone)
2704 /// because every downstream consumer of the entrada composite
2705 /// treats it as a read-only per-axis dispatch source — the
2706 /// reference-view is the narrowest borrow that supports every
2707 /// present + roadmapped consumer (per-axis accessor dispatch,
2708 /// serde composite-serialization on the programs.yaml overlay,
2709 /// presence-probe early return on the "author-omitted `:entrada`
2710 /// ⇒ cluster-internal Aplicacao" partition, `Aplicacao`-composition
2711 /// seed's forward arm) without cloning the composite through every
2712 /// consumer's fast path. The `Option` half of the return-type
2713 /// preserves the load-bearing "author-omitted `:entrada` ⇒
2714 /// cluster-internal Aplicacao" partition (not a default composite
2715 /// the downstream must reject on emptiness — a cluster-internal
2716 /// Aplicacao has no external gateway at all, not "a default gateway
2717 /// that emits nothing"); the accessor projects the raw
2718 /// `Option<Entrada>` slot's presence bit through the reference-
2719 /// return unchanged. Named `entrada()` to match the storage field's
2720 /// name verbatim and the tatara-lisp author-surface term
2721 /// (`:entrada`) the field's own docstring already carries.
2722 #[must_use]
2723 pub fn entrada(&self) -> Option<&crate::aplicacao::Entrada> {
2724 self.entrada.as_ref()
2725 }
2726
2727 /// Substrate-canonical per-`Caixa` `:ci` slot accessor — returns the
2728 /// author-declared typed CI run (`canteiro_types::CiRun`) verbatim as
2729 /// an `Option<&CiRun>`, borrowed from the typed slot's own
2730 /// `Option<CiRun>` storage. `None` when the slot is absent (every
2731 /// non-`Acao` kind, and an `Acao` caixa that hasn't declared `:ci`
2732 /// yet — the latter is caught by [`crate::LayoutError::MissingCi`],
2733 /// not silently accepted).
2734 ///
2735 /// Named `ci()` to match the storage field's name and the
2736 /// tatara-lisp author surface (`:ci`); mirrors the sibling
2737 /// `Option<&Composite>` accessors on this same `Caixa` altitude
2738 /// ([`Self::limits`], [`Self::behavior`], [`Self::politicas`],
2739 /// [`Self::placement`], [`Self::entrada`]) — one typed dispatch on
2740 /// the substrate primitive rather than an open-coded `self.ci.as_ref()`
2741 /// at every consumer.
2742 #[must_use]
2743 pub fn ci(&self) -> Option<&canteiro_types::CiRun> {
2744 self.ci.as_ref()
2745 }
2746
2747 /// Substrate-canonical per-`Caixa` `:estrategia` M2 supervisor-tree-
2748 /// slot flat-spread OTP-shaped sibling-restart-strategy discriminant
2749 /// accessor every consumer of the top-level manifest's per-Supervisor
2750 /// restart-strategy axis keys off — returns the author-declared
2751 /// `:estrategia` variant verbatim as an `Option<RestartStrategy>`,
2752 /// `Copy`-projected from the typed slot's own
2753 /// `Option<crate::supervisor::RestartStrategy>` storage. Optional
2754 /// (`:estrategia` is a flat-spread supervisor-only slot every
2755 /// non-`Supervisor`-kind `defcaixa` carries as `None` by
2756 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2757 /// still omit to defer to [`RestartStrategy::default`] —
2758 /// [`RestartStrategy::OneForOne`] — through the [`Self::supervisor_view`]
2759 /// `unwrap_or_default()` fold; a returned `None` degenerates to the
2760 /// [`SupervisorSpec::default`]-inherited strategy without any silent
2761 /// promotion to a fresh explicit variant at the accessor boundary).
2762 ///
2763 /// The `:estrategia` slot carries the M2 typed OTP-shaped sibling-
2764 /// restart-strategy discriminant every substrate-side per-Supervisor
2765 /// dispatch fans on (INSPIRATIONS §II.2 — OTP `supervisor:strategy`
2766 /// closed-set `one_for_one | one_for_all | rest_for_one |
2767 /// simple_one_for_one` algebra translated onto pleme-io's typed
2768 /// [`RestartStrategy`] enum; CAIXA-SDLC §II — the M2 supervisor-tree
2769 /// slot algebra the operator's hierarchical reconciliation scheduler
2770 /// fans on). The slot is *flat-spread* on the outer top-level `Caixa`
2771 /// (per the field-shape docstring at caixa-core/src/manifest.rs — "The
2772 /// supervisor slots are flat on Caixa (vs nested under a
2773 /// `SupervisorSpec` sub-form) to keep tatara-lisp authoring at one
2774 /// level of nesting"), so the accessor's altitude is the outer
2775 /// [`Caixa`] surface rather than the composed [`SupervisorSpec`]
2776 /// altitude the sibling [`crate::supervisor::SupervisorSpec::estrategia`]
2777 /// (eafb619) accessor keys off. The two typed axes — the outer
2778 /// author-surface `Option<RestartStrategy>` on the [`Caixa`] altitude
2779 /// (author-omitted arm carried as `None`) and the inner post-
2780 /// composition `RestartStrategy` on the [`SupervisorSpec`] altitude
2781 /// (`Option` collapsed through the [`Self::supervisor_view`]
2782 /// `unwrap_or_default()` fold) — now share one accessor discipline for
2783 /// the shared substrate concept "the author-declared OTP-shaped
2784 /// sibling-restart-strategy variant that partitions the downstream
2785 /// per-Supervisor renderer's per-arm fan-out"; the outer-altitude
2786 /// `None` arm is the pre-composition presence bit every declared-slot
2787 /// enumerator ([`Self::declared_supervisor_slots`]) reads, and the
2788 /// inner-altitude non-`Option` `RestartStrategy` is the post-
2789 /// composition partition-dispatch input every strategy-arm consumer
2790 /// ([`SupervisorSpec::validate`], the future wasm-operator's per-
2791 /// Supervisor sibling-restart branch, the future M4
2792 /// `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's admission
2793 /// webhook) fans on.
2794 ///
2795 /// Prior to this lift the `.estrategia` field was accessed inline at
2796 /// two production sites in `caixa-core/src/manifest.rs` — the
2797 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`
2798 /// presence-probe arm at `if self.estrategia.is_some()` (which drives
2799 /// the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2800 /// coherence gate's per-slot label push) and the [`Self::supervisor_view`]
2801 /// `SupervisorSpec` construction site at `estrategia:
2802 /// self.estrategia.unwrap_or_default()` (which composes the flat-
2803 /// spread outer author-surface `Option<RestartStrategy>` onto the
2804 /// inner post-composition [`SupervisorSpec`] `RestartStrategy` field
2805 /// the [`SupervisorSpec::estrategia`] accessor keys off) — two open-
2806 /// coded field-accesses that expressed no compile-time link back to
2807 /// the typed slot. A future extension of the outer `:estrategia` axis
2808 /// to a richer author surface (a per-cluster strategy override the
2809 /// operator pins through a future `:estrategia-overrides` overlay the
2810 /// MESH-COMPOSITION §III.2 supervision-canary roadmap acknowledges,
2811 /// a per-tenant strategy-alias table the M4 CR materializer resolves
2812 /// per-CR, a per-Supervisor dynamic strategy derivation the future
2813 /// adaptive-supervision engine computes from child-failure-history
2814 /// topology, a per-child-cohort strategy split the future
2815 /// `RestForCohort` extension the INSPIRATIONS.md §II.2 Erlang/OTP
2816 /// absorption roadmap acknowledges, a promotion of the plain
2817 /// `Option<RestartStrategy>` to a richer
2818 /// `AuthorDeclaredStrategy { declared, overlay }` newtype once the
2819 /// operator-resolved overlay lands) would have had to be threaded
2820 /// through both open-coded copies in lockstep or the enumerator's
2821 /// presence probe and the composition site's `unwrap_or_default()`
2822 /// fold would silently disagree on which strategy a given [`Caixa`]
2823 /// resolves to (an author's `:estrategia OneForAll` would satisfy
2824 /// the enumerator's presence probe while the composition site
2825 /// silently rendered a stale `OneForOne`, or vice versa). Lifting
2826 /// the resolution rule to a typed method on the substrate primitive
2827 /// means every downstream consumer of the caixa's per-`Caixa` outer-
2828 /// altitude sibling-restart-strategy surface reaches for exactly one
2829 /// typed dispatch — the resolver's accept-set migrates as a unit on
2830 /// any future axis addition.
2831 ///
2832 /// First outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2833 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2834 /// axes — opens the outer-`Caixa` `Option<Copy>` flat-spread
2835 /// projection pattern the sibling per-`Caixa` `:max-restarts`
2836 /// `Option<u32>` and (through the future duration-newtype landing)
2837 /// `:restart-window` `Option<Duration>` future outer-scalar lifts
2838 /// fold on. Peer of the inner-altitude [`crate::supervisor::SupervisorSpec::estrategia`]
2839 /// (eafb619) `Copy`-return sibling-restart-strategy scalar accessor on
2840 /// the post-composition [`SupervisorSpec`] altitude — same "one
2841 /// typed dispatch on the substrate primitive, thin projections at
2842 /// each consumer" discipline extended onto the pre-composition outer
2843 /// author-surface [`Caixa`] altitude for the same OTP-shaped
2844 /// sibling-restart-strategy axis. Peer of the closed outer-`Caixa`
2845 /// `Option<&Composite>` composite-reference family the sibling
2846 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
2847 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
2848 /// [`Self::entrada`] (e4128e4) accessor pins already carry on the
2849 /// outer `Option<&Composite>` altitude — extends the outer-`Caixa`
2850 /// typed-slot accessor discipline onto the flat-spread M2 supervisor-
2851 /// tree `Option<Copy>`-discriminant sub-family the sibling M3
2852 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2853 /// `PlacementStrategy` `Copy`-composite-enum scalar accessor already
2854 /// pins on the inner-altitude per-`:placement` composite. Named
2855 /// `estrategia()` to match the storage field's name and the
2856 /// per-[`SupervisorSpec`] peer [`crate::supervisor::SupervisorSpec::estrategia`]
2857 /// / per-[`crate::aplicacao::Placement`] peer
2858 /// [`crate::aplicacao::Placement::estrategia`] method-name discipline
2859 /// verbatim; the accessor's identity name maps onto the canonical
2860 /// OTP-shape supervision vocabulary the [`RestartStrategy`] enum's
2861 /// docstring already carries.
2862 #[must_use]
2863 pub fn estrategia(&self) -> Option<crate::supervisor::RestartStrategy> {
2864 self.estrategia
2865 }
2866
2867 /// Substrate-canonical per-`Caixa` `:max-restarts` M2 supervisor-tree-
2868 /// slot flat-spread OTP-`MaxIntensity`-shaped restart-budget-count
2869 /// scalar accessor every consumer of the top-level manifest's per-
2870 /// Supervisor `:max-restarts` restart-budget-count axis keys off —
2871 /// returns the author-declared `:max-restarts` typed `Option<u32>`
2872 /// verbatim, `Copy`-projected from the typed slot's own `Option<u32>`
2873 /// storage (`u32` is `Copy`, so `Option<u32>` is `Copy` and the
2874 /// accessor returns by value; no borrow of `&self` past the call).
2875 /// Optional (`:max-restarts` is a flat-spread supervisor-only slot
2876 /// every non-`Supervisor`-kind `defcaixa` carries as `None` by
2877 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2878 /// still omit to defer to the [`Self::supervisor_view`]
2879 /// `unwrap_or(5)` fold's OTP-canonical `{intensity, 5, 60}` default).
2880 ///
2881 /// The `:max-restarts` slot carries the M2 typed Erlang/OTP-shaped
2882 /// `MaxIntensity` restart-budget count that pairs with the sibling
2883 /// `:restart-window` `Period` to form the `MaxIntensity / Period`
2884 /// restart-intensity ratio the supervisor trips its own escalation on
2885 /// (INSPIRATIONS §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}`
2886 /// worker-supervisor default; RUNTIME-PATTERNS §II.2; CAIXA-SDLC §II
2887 /// — the M2 supervisor-tree slot algebra the operator's hierarchical
2888 /// reconciliation scheduler fans on). The slot is *flat-spread* on
2889 /// the outer top-level `Caixa` (per the field-shape docstring at
2890 /// caixa-core/src/manifest.rs — "The supervisor slots are flat on
2891 /// Caixa (vs nested under a `SupervisorSpec` sub-form)"), so the
2892 /// accessor's altitude is the outer [`Caixa`] surface rather than the
2893 /// composed [`SupervisorSpec`] altitude the sibling
2894 /// [`crate::supervisor::SupervisorSpec::max_restarts`] accessor keys
2895 /// off. The two typed axes — the outer author-surface `Option<u32>`
2896 /// on the [`Caixa`] altitude (author-omitted arm carried as `None`)
2897 /// and the inner post-composition `u32` on the [`SupervisorSpec`]
2898 /// altitude (`Option` collapsed through the [`Self::supervisor_view`]
2899 /// `unwrap_or(5)` fold) — now share one accessor discipline for the
2900 /// shared substrate concept "the author-declared OTP-shaped
2901 /// restart-budget count every downstream per-Supervisor consumer's
2902 /// restart-intensity budget-vs-count comparator fans on".
2903 ///
2904 /// Prior to this lift the `.max_restarts` field was accessed inline
2905 /// at two production sites in `caixa-core/src/manifest.rs` — the
2906 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`
2907 /// presence-probe arm at `if self.max_restarts.is_some()` (which
2908 /// drives the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
2909 /// kind-coherence gate's per-slot label push) and the
2910 /// [`Self::supervisor_view`] `SupervisorSpec` construction site at
2911 /// `max_restarts: self.max_restarts.unwrap_or(5)` (which composes the
2912 /// flat-spread outer author-surface `Option<u32>` onto the inner
2913 /// post-composition [`SupervisorSpec`] `u32` field the
2914 /// [`SupervisorSpec::max_restarts`] accessor keys off) — two open-
2915 /// coded field-accesses that expressed no compile-time link back to
2916 /// the typed slot. A future extension of the outer `:max-restarts`
2917 /// axis to a richer author surface (a per-cluster restart-budget
2918 /// override the operator pins through a future `:max-restarts-overrides`
2919 /// overlay the MESH-COMPOSITION §III.2 supervision-canary roadmap
2920 /// acknowledges, a per-tenant restart-budget-alias table the M4 CR
2921 /// materializer resolves per-CR, a per-Supervisor dynamic restart-
2922 /// budget derivation the future adaptive-supervision engine computes
2923 /// from child-failure-history topology, a promotion of the plain
2924 /// `Option<u32>` count to a richer `{MaxR, MaxT}` per-child-cohort
2925 /// restart-budget-partition once the INSPIRATIONS §II.2 Erlang/OTP
2926 /// per-child-cohort roadmap lands) would have had to be threaded
2927 /// through both open-coded copies in lockstep or the enumerator's
2928 /// presence probe and the composition site's `unwrap_or(5)` fold
2929 /// would silently disagree on which restart-budget a given [`Caixa`]
2930 /// resolves to (an author's `:max-restarts 10` would satisfy the
2931 /// enumerator's presence probe while the composition site silently
2932 /// composed the OTP-canonical `5`, or vice versa). Lifting the
2933 /// resolution rule to a typed method on the substrate primitive means
2934 /// every downstream consumer of the caixa's per-`Caixa` outer-altitude
2935 /// restart-budget-count surface reaches for exactly one typed dispatch
2936 /// — the resolver's accept-set migrates as a unit on any future axis
2937 /// addition.
2938 ///
2939 /// Second outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2940 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2941 /// axes — folds on the outer-`Caixa` `Option<Copy>` flat-spread
2942 /// projection pattern the sibling per-`Caixa`
2943 /// [`Self::estrategia`] (ed04d3c) accessor opened, extends the
2944 /// sub-family onto the sibling `Option<u32>` restart-budget-count arm.
2945 /// Peer of the inner-altitude
2946 /// [`crate::supervisor::SupervisorSpec::max_restarts`] `u32` accessor
2947 /// on the post-composition [`SupervisorSpec`] altitude — same "one
2948 /// typed dispatch on the substrate primitive, thin projections at
2949 /// each consumer" discipline extended onto the pre-composition outer
2950 /// author-surface [`Caixa`] altitude for the same OTP-`MaxIntensity`-
2951 /// shaped restart-budget-count axis. Named `max_restarts()` to match
2952 /// the storage field's name and the per-[`SupervisorSpec`] peer
2953 /// [`crate::supervisor::SupervisorSpec::max_restarts`] method-name
2954 /// discipline verbatim; the accessor's identity maps onto the
2955 /// canonical OTP-shape supervision vocabulary the `:max-restarts`
2956 /// field's docstring already carries.
2957 #[must_use]
2958 pub const fn max_restarts(&self) -> Option<u32> {
2959 self.max_restarts
2960 }
2961
2962 /// Substrate-canonical per-`Caixa` `:restart-window` M2 supervisor-
2963 /// tree-slot flat-spread OTP-`Period`-shaped restart-intensity-
2964 /// denominator raw-duration-string scalar accessor every consumer of
2965 /// the top-level manifest's per-Supervisor `:restart-window` sliding-
2966 /// window axis keys off — returns the author-declared `:restart-window`
2967 /// typed `Option<String>` verbatim as an `Option<&str>`, borrowed
2968 /// from the typed slot's own `Option<String>` storage. `None` when
2969 /// the slot is absent (the canonical "never reset — every restart
2970 /// across the supervisor's lifetime counts against the sibling
2971 /// `:max-restarts` budget" sentinel every non-`Supervisor`-kind
2972 /// `defcaixa` carries by `#[serde(default)]` and every
2973 /// `Supervisor`-kind `defcaixa` may still omit to defer to the
2974 /// [`Self::supervisor_view`] `restart_window: None` composition
2975 /// through the [`crate::supervisor::duration_codec::parse`] soft-
2976 /// swallow `.and_then(|s| … .ok())` fold).
2977 ///
2978 /// The `:restart-window` slot carries the raw M2 typed Erlang/OTP-
2979 /// shaped `Period` sliding-observation-interval duration string that
2980 /// pairs with the sibling `:max-restarts` `MaxIntensity` restart-
2981 /// budget count to form the `MaxIntensity / Period` restart-intensity
2982 /// ratio the supervisor trips its own escalation on (INSPIRATIONS
2983 /// §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}` worker-
2984 /// supervisor default; RUNTIME-PATTERNS §II.2). The outer-`Caixa`
2985 /// slot stores the raw duration string (`"60s"`, `"5m"`, `"500ms"`)
2986 /// authored under `:restart-window` — the typed [`SupervisorSpec`]
2987 /// holds an `Option<Duration>` routed through the shared
2988 /// [`crate::supervisor::duration_codec`] via `with = "duration_codec"`
2989 /// — so the outer altitude's accessor returns `Option<&str>` (raw
2990 /// authoring surface) while the inner altitude's
2991 /// [`crate::supervisor::SupervisorSpec::restart_window`] returns
2992 /// `Option<Duration>` (parsed typed surface). The parse-refusal arm
2993 /// is closed by the sibling [`Self::validate_restart_window`] gate
2994 /// that surfaces [`ManifestError::RestartWindowMalformed`] naming
2995 /// the offending value; the view-construction path
2996 /// [`Self::supervisor_view`] soft-swallows the same parse error to
2997 /// `None` to keep the view best-effort.
2998 ///
2999 /// Prior to this lift the `.restart_window` field was accessed inline
3000 /// at three production sites in `caixa-core/src/manifest.rs` — the
3001 /// [`Self::declared_supervisor_slots`]
3002 /// `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` presence-probe arm at
3003 /// `if self.restart_window.is_some()` (which drives the
3004 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
3005 /// coherence gate's per-slot label push), the
3006 /// [`Self::validate_restart_window`] `let Some(s) =
3007 /// self.restart_window.as_deref()` empty-and-shape gate binding
3008 /// (which folds the raw string through the shared
3009 /// [`crate::supervisor::duration_codec::parse`] to surface
3010 /// [`ManifestError::RestartWindowMalformed`] naming the offending
3011 /// value), and the [`Self::supervisor_view`] `self.restart_window
3012 /// .as_deref().and_then(…)` view-construction fold (which composes
3013 /// the flat-spread outer author-surface `Option<String>` onto the
3014 /// inner post-composition [`SupervisorSpec`] `Option<Duration>`
3015 /// field the [`SupervisorSpec::restart_window`] accessor keys off) —
3016 /// three open-coded field-accesses that expressed no compile-time
3017 /// link back to the typed slot. A future extension of the outer
3018 /// `:restart-window` axis to a richer author surface (a per-cluster
3019 /// window override, a per-tenant window-alias table, a per-Supervisor
3020 /// dynamic window derivation the future adaptive-supervision engine
3021 /// computes from child-failure-history topology, a promotion of the
3022 /// plain `Option<String>` raw duration to a typed `Option<Duration>`
3023 /// once the future author-surface parser lands at the [`Caixa`]
3024 /// altitude and the raw-string form is retired) would have had to be
3025 /// threaded through every open-coded copy in lockstep or the three
3026 /// consumers would silently disagree on which raw string a given
3027 /// [`Caixa`] resolves to. Lifting the resolution rule to a typed
3028 /// method on the substrate primitive means every downstream consumer
3029 /// of the caixa's per-`Caixa` outer-altitude restart-window raw-
3030 /// string surface reaches for exactly one typed dispatch — the
3031 /// resolver's accept-set migrates as a unit on any future axis
3032 /// addition.
3033 ///
3034 /// Third outer top-level [`Caixa`] supervisor-tree-slot flat-spread
3035 /// accessor — folds on the outer-`Caixa` M2 supervisor-tree flat-
3036 /// spread projection pattern the sibling per-`Caixa`
3037 /// [`Self::estrategia`] (ed04d3c) `Option<Copy>` and
3038 /// [`Self::max_restarts`] `Option<Copy>` accessors opened, extends
3039 /// the sub-family onto the sibling `Option<&str>` raw-duration-
3040 /// string arm (the outer altitude's raw-string form; the inner
3041 /// altitude's parsed [`Duration`] form is the peer
3042 /// [`crate::supervisor::SupervisorSpec::restart_window`] accessor).
3043 /// Peer of the sibling per-`Caixa` `Option<&str>`-return scalar
3044 /// accessors ([`Self::licenca`] / [`Self::repositorio`] /
3045 /// [`Self::descricao`] / [`Self::edicao`]) on the universal-axis
3046 /// outer scalar-projection family the outer-`Caixa` `Option<&str>`
3047 /// sub-family already carries — same "one typed dispatch on the
3048 /// substrate primitive, thin projections at each consumer"
3049 /// discipline extended onto the M2 supervisor-tree flat-spread
3050 /// `Option<&str>` raw-duration-string arm. Named `restart_window()`
3051 /// to match the storage field's name and the per-[`SupervisorSpec`]
3052 /// peer [`crate::supervisor::SupervisorSpec::restart_window`]
3053 /// method-name discipline verbatim; the accessor's identity maps
3054 /// onto the canonical OTP-shape supervision vocabulary the
3055 /// `:restart-window` field's docstring already carries.
3056 #[must_use]
3057 pub fn restart_window(&self) -> Option<&str> {
3058 self.restart_window.as_deref()
3059 }
3060
3061 /// Substrate-canonical per-`Caixa` `:upgrade-from` M2 typed-slot
3062 /// outer-composite OTP-appup-shaped per-prior-version migration-
3063 /// entry-list slice accessor every consumer of the top-level
3064 /// manifest's per-Servico hot-upgrade-block `&[UpgradeFromEntry]`
3065 /// slice-view keys off — returns the author-declared `:upgrade-from`
3066 /// typed `Vec<UpgradeFromEntry>` verbatim as a
3067 /// `&[UpgradeFromEntry]` slice-view over the same backing buffer
3068 /// the raw `self.upgrade_from.as_slice()` field access borrows
3069 /// from. Empty-slice-carrying (the "no hot-upgrade path declared"
3070 /// arm every `defcaixa` without an `:upgrade-from` block carries;
3071 /// the [`Self::from_lisp`] derive folds an omitted `:upgrade-from`
3072 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
3073 /// parse definitionally carries a `Vec<UpgradeFromEntry>` slot —
3074 /// possibly empty — and the returned `&[UpgradeFromEntry]`
3075 /// degenerates to an empty slice on that arm without any silent
3076 /// `None` collapse).
3077 ///
3078 /// The outer `:upgrade-from` slot carries the M2 typed OTP-appup
3079 /// migration block — the load-bearing container of every per-
3080 /// prior-`:versao` migration-instruction list the wasm-operator
3081 /// dispatches on at hot-upgrade time (INSPIRATIONS §II.4 — OTP
3082 /// `.appup` per-prior-version `LoadModule | StateChange |
3083 /// SoftPurge | Purge | Restart` instruction algebra translated
3084 /// onto pleme-io's typed `:upgrade-from :from` + `:instructions`
3085 /// entry list; CAIXA-SDLC §II — the typed-M2 slot algebra the
3086 /// operator's hot-upgrade dispatch fans on). Every per-entry axis
3087 /// threads through a lifted per-entry accessor on the
3088 /// [`UpgradeFromEntry`] type: the
3089 /// [`UpgradeFromEntry::prior_versao`] SemVer-shaped previous-
3090 /// version scalar accessor and the
3091 /// [`UpgradeFromEntry::instructions`] `&[UpgradeInstruction]`-
3092 /// return per-entry instruction-list accessor (0137e5a). Every
3093 /// downstream consumer of the hot-upgrade path first passes
3094 /// through this outer accessor onto the slice and then dispatches
3095 /// per-entry through the inner accessors — the two-level dispatch
3096 /// means every per-`:upgrade-from` reader now routes through a
3097 /// typed dispatch on the substrate primitive at both altitudes.
3098 ///
3099 /// Prior to this lift the `.upgrade_from` `Vec<UpgradeFromEntry>`
3100 /// slot was accessed inline at production sites across three
3101 /// files — the [`Self::declared_servico_slots`] M2 declared-slot
3102 /// enumerator's `self.upgrade_from.is_empty()` presence probe
3103 /// (caixa-core/src/manifest.rs, which drives the
3104 /// `M2_AUTHOR_KEY_UPGRADE_FROM` kebab-case author-label push every
3105 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
3106 /// gate reads), the [`crate::StandardLayout::verify`] per-
3107 /// `:upgrade-from` three-stage validation pass (caixa-core/src/
3108 /// layout.rs, which fans onto the
3109 /// [`crate::upgrade::validate_upgrade_from`] per-entry shape +
3110 /// cross-entry duplicate gate, the
3111 /// [`crate::upgrade::validate_upgrade_from_against_versao`]
3112 /// SemVer-precedence cross-slot gate, the
3113 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
3114 /// `:state-change` ↔ `:on-state-change` cross-slot composition
3115 /// gate, and the per-instruction script-path existence-probe walk
3116 /// that reads each entry's [`UpgradeFromEntry::instructions`] to
3117 /// resolve every declared migration script against the layout
3118 /// root), and the [`crate::render::servico_m2_overlay`] per-
3119 /// Servico M2 overlay emitter's `!caixa.upgrade_from.is_empty()`
3120 /// presence gate + `serde_yaml::to_value(&caixa.upgrade_from)`
3121 /// projection (caixa-core/src/render.rs, which drives the
3122 /// `M2_KEY_UPGRADE_FROM`-keyed `serde_yaml` projection every
3123 /// `caixa-helm` / `caixa-flux` Servico values-block emitter fans
3124 /// on and lands as the ComputeUnit CR's `spec.upgradeFrom` field).
3125 /// A future extension of the outer `:upgrade-from` axis (a per-
3126 /// cluster `:upgrade-overrides` overlay the wasm-engine operator
3127 /// resolves at admission time so a cluster-specific migration
3128 /// policy can tighten a caixa-declared step without re-authoring
3129 /// the `caixa.lisp`, promotion of the plain
3130 /// `Vec<UpgradeFromEntry>` to a richer `{static, dynamic}`
3131 /// partition once runtime-resolved hot-upgrade instructions land,
3132 /// per-entry priority annotation once multi-strategy fan-out
3133 /// lands) would have had to be threaded through all six open-
3134 /// coded copies in lockstep or one consumer would silently
3135 /// disagree with the peers on which upgrade slice a given Caixa
3136 /// resolves to — a six-consumer split at the enumerator, the
3137 /// three-stage validate pass, the script-path probe walk, and the
3138 /// M2 overlay emitter, far from the source `caixa.lisp` with no
3139 /// field naming the upgrade-drift root cause. Lifting the
3140 /// resolution rule to a typed method on the substrate primitive
3141 /// means every downstream consumer of the caixa's per-`Caixa`
3142 /// OTP-appup outer-slice surface reaches for exactly one typed
3143 /// dispatch — the resolver's accept-set migrates as a unit on any
3144 /// future axis addition.
3145 ///
3146 /// First outer top-level [`Caixa`] `&[Composite]`-return slice
3147 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the
3148 /// outer-`Caixa` `&[Composite]` composite-slice projection
3149 /// pattern the sibling `:children`
3150 /// [`crate::supervisor::ChildSpec`] / `:membros`
3151 /// [`crate::aplicacao::Membro`] / `:contratos`
3152 /// [`crate::aplicacao::WitContract`] future outer-composite-slice
3153 /// lifts fold on. Peer of the closed outer-`Caixa` scalar
3154 /// `Option<&Composite>` composite-reference family the sibling
3155 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
3156 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
3157 /// [`Self::entrada`] (e4128e4) accessors closed on the outer
3158 /// `Option<&Composite>` altitude, extended here to the outer-
3159 /// `Caixa` `&[Composite]` vec-carry altitude. Peer at the inner
3160 /// altitude of [`crate::upgrade::UpgradeFromEntry::instructions`]
3161 /// (0137e5a) — same "one typed dispatch on the substrate
3162 /// primitive, thin projections at each consumer" discipline
3163 /// folded onto the outer top-level [`Caixa`] altitude, opening the
3164 /// M2 vec-carry slot family's outer-composite-slice axis. Sibling
3165 /// in shape to the peer outer-`Caixa` `&[Dep]`-return
3166 /// [`Self::deps`] (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and
3167 /// `&[String]`-return [`Self::autores`] (b5d813f) /
3168 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`]
3169 /// (8a36c23) / [`Self::exe`] (65d9527) / [`Self::servicos`]
3170 /// (611f78b) slice-accessors on the sibling outer-`Caixa` scalar-
3171 /// element vec-carry axes — folds the "outer [`Caixa`] `&[T]`
3172 /// slice" projection pattern onto the sibling M2 typed-composite-
3173 /// element axis (`UpgradeFromEntry` composite, matching the
3174 /// per-inner [`UpgradeFromEntry::instructions`] element type at a
3175 /// different altitude).
3176 ///
3177 /// Returns `&[UpgradeFromEntry]` (not `&Vec<UpgradeFromEntry>`)
3178 /// because every downstream consumer of the hot-upgrade list
3179 /// treats it as a read-only sequence — the slice-view is the
3180 /// narrowest borrow that supports every present + roadmapped
3181 /// consumer (`.iter()`, `.len()`, `.is_empty()`, `serde` slice-
3182 /// serialization through
3183 /// `serde_yaml::to_value(&[UpgradeFromEntry])`) without leaking
3184 /// the backing `Vec`'s grow/push/reserve surface no consumer of
3185 /// the typed view reaches for (the storage-side `Vec` remains
3186 /// reachable through the `pub upgrade_from` field for the
3187 /// mutation-carrying serde round-trip and per-test fixture-
3188 /// mutation paths). Named `upgrade_from()` to match the storage
3189 /// field's `snake_case` name; the kebab-case author-surface tag
3190 /// `:upgrade-from` is the same axis after tatara-lisp's
3191 /// kebab↔snake fold and the accessor's identity maps onto the
3192 /// canonical CAIXA-SDLC §II vocabulary the slot's docstring
3193 /// already carries.
3194 #[must_use]
3195 pub fn upgrade_from(&self) -> &[UpgradeFromEntry] {
3196 self.upgrade_from.as_slice()
3197 }
3198
3199 /// Substrate-canonical per-`Caixa` `:children` M2 supervisor-tree-
3200 /// slot outer-composite OTP-shaped per-supervisor static-child-list
3201 /// slice accessor every consumer of the top-level manifest's per-
3202 /// Supervisor `&[ChildSpec]` slice-view keys off — returns the
3203 /// author-declared `:children` typed `Vec<crate::supervisor::ChildSpec>`
3204 /// verbatim as a `&[crate::supervisor::ChildSpec]` slice-view over
3205 /// the same backing buffer the raw `self.children.as_slice()` field
3206 /// access borrows from. Empty-slice-carrying (the "no static children
3207 /// declared" arm every non-`Supervisor`-kind `defcaixa` carries by
3208 /// #[serde(default)] and every `SimpleOneForOne` supervisor carries
3209 /// by [`crate::supervisor::SupervisorError::SimpleOneForOneWithStaticChildren`]
3210 /// gate; the returned `&[ChildSpec]` degenerates to an empty slice
3211 /// on those arms without any silent `None` collapse).
3212 ///
3213 /// The outer `:children` slot carries the M2 typed OTP-supervisor
3214 /// static-child list — the load-bearing container of every per-
3215 /// child `{caixa, versao, restart}` triple the wasm-operator's
3216 /// hierarchical reconciler dispatches on at supervisor-tree
3217 /// materialization time (INSPIRATIONS §II.2 — OTP `supervisor:init/1`
3218 /// static-child list translated onto pleme-io's typed
3219 /// [`crate::supervisor::ChildSpec`] entry list; CAIXA-SDLC §II —
3220 /// the typed-M2 slot algebra the operator's per-supervisor fan-out
3221 /// dispatch fans on). Every per-child axis threads through a lifted
3222 /// per-entry accessor on the [`crate::supervisor::ChildSpec`] type:
3223 /// the [`crate::supervisor::ChildSpec::nome`] DNS-1123-label
3224 /// child-caixa-identity scalar accessor, the peer versao SemVer-2
3225 /// version-requirement scalar accessor, and the
3226 /// [`crate::supervisor::ChildSpec::restart`] `Copy`-composite-enum
3227 /// per-child post-exit restart-decision-policy discriminant
3228 /// accessor (dfb4a81). Every downstream consumer of the supervisor-
3229 /// tree path first passes through this outer accessor onto the
3230 /// slice and then dispatches per-child through the inner accessors
3231 /// — the two-level dispatch means every per-`:children` reader now
3232 /// routes through a typed dispatch on the substrate primitive at
3233 /// both altitudes.
3234 ///
3235 /// Prior to this lift the `.children` `Vec<ChildSpec>` slot was
3236 /// accessed inline at three production sites across two files —
3237 /// the [`Self::declared_supervisor_slots`] supervisor-tree
3238 /// declared-slot enumerator's `!self.children.is_empty()` presence
3239 /// probe (caixa-core/src/manifest.rs, which drives the
3240 /// `SUPERVISOR_AUTHOR_KEY_CHILDREN` kebab-case author-label push
3241 /// every [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
3242 /// kind-coherence gate reads), the [`Self::supervisor_view`]
3243 /// per-supervisor typed-view composer's `self.children.clone()`
3244 /// per-child fold-in path (caixa-core/src/manifest.rs, which
3245 /// materializes the typed [`crate::supervisor::SupervisorSpec`]
3246 /// view every [`crate::StandardLayout::verify`] Supervisor-arm gate
3247 /// dispatches on), and the [`crate::StandardLayout::verify`] per-
3248 /// `:children :caixa` self-parent refusal probe's
3249 /// `&caixa.children`-borrowed
3250 /// [`crate::supervisor::validate_no_self_supervision`] input
3251 /// (caixa-core/src/layout.rs, which pins the "no child names the
3252 /// supervisor's own `:nome`" cross-slot coherence gate). A future
3253 /// extension of the outer `:children` axis (a per-cluster
3254 /// `:children-overrides` overlay the wasm-engine operator resolves
3255 /// at admission time so a cluster-specific child-set can tighten
3256 /// a caixa-declared list without re-authoring the `caixa.lisp`,
3257 /// promotion of the plain `Vec<ChildSpec>` to a richer
3258 /// `{static, dynamic}` partition once Erlang/OTP's
3259 /// `simple_one_for_one`-shaped dynamic-child slot lands as a typed
3260 /// axis, per-child priority annotation once multi-strategy fan-out
3261 /// lands) would have had to be threaded through all three open-
3262 /// coded copies in lockstep or one consumer would silently
3263 /// disagree with the peers on which child slice a given Caixa
3264 /// resolves to — the enumerator's presence probe reading the raw
3265 /// slot while the peer view-composer's fold-in path read an
3266 /// operator-resolved slot would silently split the paired
3267 /// declared-slot enumerator and typed-view composition, and the
3268 /// [`crate::supervisor::validate_no_self_supervision`] self-parent
3269 /// refusal probe reading a third borrow would silently drift the
3270 /// cross-slot coherence gate's traversal input from the two peers,
3271 /// a three-consumer split at the enumerator, the view composer,
3272 /// and the self-parent gate far from the source `caixa.lisp` with
3273 /// no field naming the child-set-drift root cause. Lifting the
3274 /// resolution rule to a typed method on the substrate primitive
3275 /// means every downstream consumer of the caixa's per-`Caixa`
3276 /// OTP-supervisor outer-slice surface reaches for exactly one
3277 /// typed dispatch — the resolver's accept-set migrates as a unit
3278 /// on any future axis addition.
3279 ///
3280 /// Second outer top-level [`Caixa`] `&[Composite]`-return slice
3281 /// accessor for M2 / M3 typed-slot vec-carry axes — folds on the
3282 /// outer-`Caixa` `&[Composite]` composite-slice sub-family the
3283 /// sibling [`Self::upgrade_from`] (2a1f907) accessor opened, peer
3284 /// at the outer altitude of the closed inner-`SupervisorSpec`
3285 /// [`crate::SupervisorSpec::children`] (bc92bce) accessor on the
3286 /// same OTP-supervisor static-child-list axis — same "byte-equal,
3287 /// borrow-shared" outer-accessor discipline extended onto the
3288 /// second outer-`Caixa` `&[Composite]` vec-carry axis. Sibling in
3289 /// shape to the peer outer-`Caixa` `&[Dep]`-return [`Self::deps`]
3290 /// (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and `&[String]`-return
3291 /// [`Self::autores`] (b5d813f) / [`Self::etiquetas`] (78c7d3c) /
3292 /// [`Self::bibliotecas`] (8a36c23) / [`Self::exe`] (65d9527) /
3293 /// [`Self::servicos`] (611f78b) slice-accessors on the sibling
3294 /// outer-`Caixa` scalar-element vec-carry axes — folds the "outer
3295 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
3296 /// M2 typed-composite-element axis
3297 /// ([`crate::supervisor::ChildSpec`] composite, matching the
3298 /// per-inner [`crate::SupervisorSpec::children`] element type at a
3299 /// different altitude).
3300 ///
3301 /// Returns `&[crate::supervisor::ChildSpec]` (not
3302 /// `&Vec<ChildSpec>`) because every downstream consumer of the
3303 /// child list treats it as a read-only sequence — the slice-view
3304 /// is the narrowest borrow that supports every present +
3305 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`, the
3306 /// [`crate::supervisor::validate_no_self_supervision`] `&[ChildSpec]`
3307 /// input, `serde` slice-serialization) without leaking the backing
3308 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3309 /// reaches for (the storage-side `Vec` remains reachable through
3310 /// the `pub children` field for the mutation-carrying serde round-
3311 /// trip and per-test fixture-mutation paths, including the
3312 /// [`Self::supervisor_view`] fold-in path that clones the slot
3313 /// into the typed view). Named `children()` to match the storage
3314 /// field's name verbatim and the tatara-lisp author-surface term
3315 /// (`:children`) the field's own docstring already carries; the
3316 /// accessor's identity maps onto the canonical OTP supervision
3317 /// vocabulary the [`Caixa::children`] field's docstring already
3318 /// reaches for ("Static children of a supervisor").
3319 #[must_use]
3320 pub fn children(&self) -> &[crate::supervisor::ChildSpec] {
3321 self.children.as_slice()
3322 }
3323
3324 /// Substrate-canonical per-`Caixa` `:membros` M3 mesh-slot outer-
3325 /// composite MESH-COMPOSITION-shaped per-Aplicacao member-list slice
3326 /// accessor every consumer of the top-level manifest's per-Aplicacao
3327 /// `&[crate::aplicacao::Membro]` slice-view keys off — returns the
3328 /// author-declared `:membros` typed `Vec<crate::aplicacao::Membro>`
3329 /// verbatim as a `&[crate::aplicacao::Membro]` slice-view over the
3330 /// same backing buffer the raw `self.membros.as_slice()` field access
3331 /// borrows from. Empty-slice-carrying (the "no members declared" arm
3332 /// every non-`Aplicacao`-kind `defcaixa` carries by `#[serde(default)]`
3333 /// and every partially-authored Aplicacao carries before the
3334 /// [`crate::AplicacaoError::MembrosEmpty`] gate fires; the returned
3335 /// `&[Membro]` degenerates to an empty slice on those arms without any
3336 /// silent `None` collapse).
3337 ///
3338 /// The outer `:membros` slot carries the M3 typed MESH-COMPOSITION
3339 /// per-Aplicacao member list — the load-bearing container of every
3340 /// per-member `{caixa, versao}` pair the caixa-mesh renderer's
3341 /// per-Aplicacao program-emission dispatch fans on at mesh-artifact
3342 /// materialization time (MESH-COMPOSITION §III.1 — the typed graph's
3343 /// vertex set the `:contratos` `:de`/`:para` edges resolve against and
3344 /// the `:entrada :para` external-gateway destination validates
3345 /// against; CAIXA-SDLC §II — the typed-M3 slot algebra the operator's
3346 /// per-Aplicacao fan-out dispatch fans on). Every per-member axis
3347 /// threads through a lifted per-entry accessor on the
3348 /// [`crate::aplicacao::Membro`] type: the
3349 /// [`crate::aplicacao::Membro::nome`] DNS-1123-label member-caixa-
3350 /// identity scalar accessor (4a32abf) and the peer
3351 /// [`crate::aplicacao::Membro::versao_requirement`] SemVer-2
3352 /// version-requirement scalar accessor (a40b0e3). Every downstream
3353 /// consumer of the mesh-graph path first passes through this outer
3354 /// accessor onto the slice and then dispatches per-member through
3355 /// the inner accessors — the two-level dispatch means every per-
3356 /// `:membros` reader now routes through a typed dispatch on the
3357 /// substrate primitive at both altitudes.
3358 ///
3359 /// Prior to this lift the `.membros` `Vec<Membro>` slot was accessed
3360 /// inline at three production sites across two files — the
3361 /// [`Self::declared_mesh_slots`] mesh-slot declared-slot
3362 /// enumerator's `!self.membros.is_empty()` presence probe
3363 /// (caixa-core/src/manifest.rs, which drives the
3364 /// `M3_AUTHOR_KEY_MEMBROS` kebab-case author-label push every
3365 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3366 /// gate reads), the [`Self::aplicacao_view`] per-Aplicacao typed-view
3367 /// composer's `self.membros.clone()` per-member fold-in path
3368 /// (caixa-core/src/manifest.rs, which materializes the typed
3369 /// [`crate::aplicacao::AplicacaoSpec`] view every
3370 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate dispatches
3371 /// on), and the [`crate::StandardLayout::verify`] per-`:membros
3372 /// :caixa` self-membership refusal probe's `&caixa.membros`-borrowed
3373 /// [`crate::aplicacao::validate_no_self_membership`] input
3374 /// (caixa-core/src/layout.rs, which pins the "no member names the
3375 /// Aplicacao's own `:nome`" cross-slot coherence gate). A future
3376 /// extension of the outer `:membros` axis (a per-cluster
3377 /// `:membros-overrides` overlay the wasm-engine operator resolves at
3378 /// admission time so a cluster-specific member-set can tighten a
3379 /// caixa-declared list without re-authoring the `caixa.lisp`,
3380 /// promotion of the plain `Vec<Membro>` to a richer
3381 /// `{static, dynamic}` partition once runtime-resolved Aplicacao
3382 /// members land as a typed axis, per-member priority annotation once
3383 /// multi-strategy fan-out lands) would have had to be threaded
3384 /// through all three open-coded copies in lockstep or one consumer
3385 /// would silently disagree with the peers on which member slice a
3386 /// given Caixa resolves to — the enumerator's presence probe reading
3387 /// the raw slot while the peer view-composer's fold-in path read an
3388 /// operator-resolved slot would silently split the paired
3389 /// declared-slot enumerator and typed-view composition, and the
3390 /// [`crate::aplicacao::validate_no_self_membership`] self-membership
3391 /// refusal probe reading a third borrow would silently drift the
3392 /// cross-slot coherence gate's traversal input from the two peers, a
3393 /// three-consumer split at the enumerator, the view composer, and
3394 /// the self-membership gate far from the source `caixa.lisp` with no
3395 /// field naming the member-set-drift root cause. Lifting the
3396 /// resolution rule to a typed method on the substrate primitive
3397 /// means every downstream consumer of the caixa's per-`Caixa`
3398 /// MESH-COMPOSITION outer-slice surface reaches for exactly one
3399 /// typed dispatch — the resolver's accept-set migrates as a unit on
3400 /// any future axis addition.
3401 ///
3402 /// Third outer top-level [`Caixa`] `&[Composite]`-return slice
3403 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the outer-
3404 /// `Caixa` M3 mesh-slot arm of the `&[Composite]` composite-slice
3405 /// sub-family the sibling M2 [`Self::upgrade_from`] (2a1f907) /
3406 /// [`Self::children`] (c17b51e) accessors opened for the M2 vec-carry
3407 /// altitude. Peer at the outer altitude of the closed inner-
3408 /// [`crate::AplicacaoSpec::membros`] (6c77e36) accessor on the same
3409 /// MESH-COMPOSITION per-Aplicacao member-list axis — the two
3410 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3411 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3412 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3413 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3414 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3415 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3416 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3417 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3418 /// pattern onto the sibling M3 typed-composite-element axis
3419 /// ([`crate::aplicacao::Membro`] composite, matching the per-inner
3420 /// [`crate::AplicacaoSpec::membros`] element type at a different
3421 /// altitude).
3422 ///
3423 /// Returns `&[crate::aplicacao::Membro]` (not `&Vec<Membro>`)
3424 /// because every downstream consumer of the member list treats it
3425 /// as a read-only sequence — the slice-view is the narrowest borrow
3426 /// that supports every present + roadmapped consumer (`.iter()`,
3427 /// `.len()`, `.is_empty()`, the
3428 /// [`crate::aplicacao::validate_no_self_membership`] `&[Membro]`
3429 /// input, `serde` slice-serialization) without leaking the backing
3430 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3431 /// reaches for (the storage-side `Vec` remains reachable through the
3432 /// `pub membros` field for the mutation-carrying serde round-trip
3433 /// and per-test fixture-mutation paths, including the
3434 /// [`Self::aplicacao_view`] fold-in path that clones the slot into
3435 /// the typed view). Named `membros()` to match the storage field's
3436 /// name verbatim and the tatara-lisp author-surface term
3437 /// (`:membros`) the field's own docstring already carries; the
3438 /// accessor's identity maps onto the canonical MESH-COMPOSITION
3439 /// vocabulary the [`Caixa::membros`] field's docstring already
3440 /// reaches for ("Member Servicos that make up this Aplicacao").
3441 #[must_use]
3442 pub fn membros(&self) -> &[crate::aplicacao::Membro] {
3443 self.membros.as_slice()
3444 }
3445
3446 /// Substrate-canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
3447 /// composite MESH-COMPOSITION-shaped per-Aplicacao WIT-typed
3448 /// inter-Servico contract-list slice accessor every consumer of the
3449 /// top-level manifest's per-Aplicacao `&[crate::aplicacao::WitContract]`
3450 /// slice-view keys off — returns the author-declared `:contratos`
3451 /// typed `Vec<crate::aplicacao::WitContract>` verbatim as a
3452 /// `&[crate::aplicacao::WitContract]` slice-view over the same
3453 /// backing buffer the raw `self.contratos.as_slice()` field access
3454 /// borrows from. Empty-slice-carrying (the "no contracts declared"
3455 /// arm every non-`Aplicacao`-kind `defcaixa` carries by
3456 /// `#[serde(default)]` and every leaf Aplicacao carrying only a
3457 /// single member with no inter-Servico edge carries; the returned
3458 /// `&[WitContract]` degenerates to an empty slice on those arms
3459 /// without any silent `None` collapse).
3460 ///
3461 /// The outer `:contratos` slot carries the M3 typed MESH-COMPOSITION
3462 /// per-Aplicacao WIT-typed inter-Servico edge list — the load-bearing
3463 /// container of every per-edge `{de, para, wit, endpoint | subject |
3464 /// slot}` quadruple the caixa-mesh renderer's per-Aplicacao
3465 /// `CiliumNetworkPolicy` fan-out (one L7 policy per edge —
3466 /// MESH-COMPOSITION §III.2 point 2) and per-`(:de, :para)`
3467 /// adjacency-list seed dispatch on at mesh-artifact materialization
3468 /// time (MESH-COMPOSITION §III.1 — the typed graph's edge set the
3469 /// `:membros` vertex set resolves against, closed by the
3470 /// [`crate::AplicacaoError::ContractoUnknownMember`] / cycle-refusal
3471 /// gates in §III.3; CAIXA-SDLC §II — the typed-M3 slot algebra the
3472 /// operator's per-Aplicacao fan-out dispatch fans on). Every
3473 /// per-edge axis threads through a lifted per-entry accessor on the
3474 /// [`crate::aplicacao::WitContract`] type: the peer `de` / `para`
3475 /// DNS-1123-label member-caixa-name endpoint scalar accessors, the
3476 /// [`crate::aplicacao::WitContract::endpoint`] (7020470) HTTP-shape
3477 /// / [`crate::aplicacao::WitContract::subject`] (90de675)
3478 /// NATS-pub-sub-shape / [`crate::aplicacao::WitContract::slot`]
3479 /// (ed22b66) `wasi:keyvalue/store`-shape payload-carrier accessors,
3480 /// and the WIT-world discriminant. Every downstream consumer of the
3481 /// mesh-graph edge path first passes through this outer accessor
3482 /// onto the slice and then dispatches per-contract through the
3483 /// inner accessors — the two-level dispatch means every
3484 /// per-`:contratos` reader now routes through a typed dispatch on
3485 /// the substrate primitive at both altitudes.
3486 ///
3487 /// Prior to this lift the `.contratos` `Vec<WitContract>` slot was
3488 /// accessed inline at two production sites in
3489 /// caixa-core/src/manifest.rs — the [`Self::declared_mesh_slots`]
3490 /// mesh-slot declared-slot enumerator's
3491 /// `!self.contratos.is_empty()` presence probe (which drives the
3492 /// `M3_AUTHOR_KEY_CONTRATOS` kebab-case author-label push every
3493 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3494 /// gate reads) and the [`Self::aplicacao_view`] per-Aplicacao
3495 /// typed-view composer's `self.contratos.clone()` per-contract
3496 /// fold-in path (which materializes the typed
3497 /// [`crate::aplicacao::AplicacaoSpec`] view every
3498 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate and every
3499 /// downstream `caixa-mesh` renderer dispatches on). A future
3500 /// extension of the outer `:contratos` axis (a per-cluster
3501 /// `:contratos-overrides` overlay the wasm-engine operator resolves
3502 /// at admission time so a cluster-specific edge-set can tighten a
3503 /// caixa-declared list without re-authoring the `caixa.lisp`,
3504 /// promotion of the plain `Vec<WitContract>` to a richer
3505 /// `{static, dynamic}` partition once runtime-resolved contract
3506 /// edges land, per-edge policy annotation once the M4 per-edge
3507 /// policy overlay axis lands) would have had to be threaded through
3508 /// both open-coded copies in lockstep or one consumer would
3509 /// silently disagree with the peer on which edge slice a given
3510 /// Caixa resolves to — the enumerator's presence probe reading the
3511 /// raw slot while the peer view-composer's fold-in path read an
3512 /// operator-resolved slot would silently split the paired
3513 /// declared-slot enumerator and typed-view composition, a
3514 /// two-consumer split at the enumerator and the view composer far
3515 /// from the source `caixa.lisp` with no field naming the edge-set-
3516 /// drift root cause. Lifting the resolution rule to a typed method
3517 /// on the substrate primitive means every downstream consumer of
3518 /// the caixa's per-`Caixa` MESH-COMPOSITION outer-slice surface
3519 /// reaches for exactly one typed dispatch — the resolver's
3520 /// accept-set migrates as a unit on any future axis addition.
3521 ///
3522 /// Fourth and final outer top-level [`Caixa`] `&[Composite]`-return
3523 /// slice accessor for M2 / M3 typed-slot vec-carry axes — closes
3524 /// the outer-`Caixa` `&[Composite]` composite-slice sub-family the
3525 /// sibling M2 [`Self::upgrade_from`] (2a1f907) / [`Self::children`]
3526 /// (c17b51e) accessors opened and the M3 [`Self::membros`]
3527 /// (0f26987) accessor folded on, and closes the outer-`Caixa` M3
3528 /// mesh-slot arm of the composite-slice sub-family the sibling
3529 /// [`Self::membros`] accessor opened for the M3 vec-carry altitude.
3530 /// Peer at the outer altitude of the closed inner-
3531 /// [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
3532 /// same MESH-COMPOSITION per-Aplicacao contract-list axis — the two
3533 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3534 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3535 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3536 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3537 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3538 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3539 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3540 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3541 /// pattern onto the sibling M3 typed-composite-element axis
3542 /// ([`crate::aplicacao::WitContract`] composite, matching the
3543 /// per-inner [`crate::AplicacaoSpec::contratos`] element type at a
3544 /// different altitude).
3545 ///
3546 /// Returns `&[crate::aplicacao::WitContract]` (not
3547 /// `&Vec<WitContract>`) because every downstream consumer of the
3548 /// contract list treats it as a read-only sequence — the slice-view
3549 /// is the narrowest borrow that supports every present + roadmapped
3550 /// consumer (`.iter()`, `.len()`, `.is_empty()`, per-edge WIT-world
3551 /// discriminant dispatch, `serde` slice-serialization) without
3552 /// leaking the backing `Vec`'s grow/push/reserve surface no
3553 /// consumer of the typed view reaches for (the storage-side `Vec`
3554 /// remains reachable through the `pub contratos` field for the
3555 /// mutation-carrying serde round-trip and per-test fixture-mutation
3556 /// paths, including the [`Self::aplicacao_view`] fold-in path that
3557 /// clones the slot into the typed view). Named `contratos()` to
3558 /// match the storage field's name verbatim and the tatara-lisp
3559 /// author-surface term (`:contratos`) the field's own docstring
3560 /// already carries; the accessor's identity maps onto the canonical
3561 /// MESH-COMPOSITION vocabulary the [`Caixa::contratos`] field's
3562 /// docstring already reaches for ("WIT-typed inter-Servico
3563 /// contracts").
3564 #[must_use]
3565 pub fn contratos(&self) -> &[crate::aplicacao::WitContract] {
3566 self.contratos.as_slice()
3567 }
3568
3569 /// Compose the Aplicacao-related flat slots into a single typed
3570 /// [`crate::aplicacao::AplicacaoSpec`] for validation +
3571 /// downstream renderer consumption. Returns `None` when the
3572 /// caixa isn't a `:kind Aplicacao`.
3573 #[must_use]
3574 pub fn aplicacao_view(&self) -> Option<crate::aplicacao::AplicacaoSpec> {
3575 if !self.kind().is_aplicacao() {
3576 return None;
3577 }
3578 Some(crate::aplicacao::AplicacaoSpec {
3579 membros: self.membros().to_vec(),
3580 contratos: self.contratos().to_vec(),
3581 politicas: self.politicas().cloned().unwrap_or_default(),
3582 placement: self.placement().cloned().unwrap_or_default(),
3583 entrada: self.entrada().cloned(),
3584 })
3585 }
3586
3587 /// The kebab-case `:slot` tags of every M3 mesh slot this caixa
3588 /// *declares* a value on, in canonical declaration order
3589 /// (`:membros` → `:contratos` → `:politicas` → `:placement` →
3590 /// `:entrada`). A slot counts as declared when its backing field
3591 /// carries a value — a non-empty `Vec`, or a `Some(...)`.
3592 ///
3593 /// The M3 mesh slots compose the typed graph of a `:kind Aplicacao`
3594 /// (MESH-COMPOSITION §III.1). [`Self::aplicacao_view`] only folds
3595 /// them into a validatable [`crate::aplicacao::AplicacaoSpec`] when
3596 /// the kind matches (returns `None` otherwise), and the caixa-mesh /
3597 /// caixa-flux / caixa-helm renderers only emit them for an
3598 /// Aplicacao. On any *other* kind a declared mesh slot is the
3599 /// manifest field's documented "ignored otherwise" (see the
3600 /// `:membros` … `:entrada` field docs): it silently passes
3601 /// [`Caixa::from_lisp`] and then vanishes — never validated, never
3602 /// rendered — far from the source caixa.lisp.
3603 /// [`crate::StandardLayout::verify`] consults this to reject that
3604 /// silent-drop at caixa-build time
3605 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]), mirroring the
3606 /// `SupervisorOwnsCode` / `AplicacaoOwnsCode` kind-coherence gates:
3607 /// a slot foreign to the kind is a build error, not a silent drop.
3608 ///
3609 /// Lifted as a typed method (rather than an inline disjunction at
3610 /// the verify call site) so the mesh-slot set lives in one place —
3611 /// a future M4 axis added to the Aplicacao surface (per-edge policy
3612 /// overlay, distributed-app takeover config) is one push here, and
3613 /// every consumer reaching for "which mesh slots are set" (the
3614 /// verify gate, a future `feira lint` kind-coherence advisory)
3615 /// inherits the canonical order without rolling its own.
3616 ///
3617 /// Each per-arm kebab-case label is routed through the peer
3618 /// [`crate::M3_AUTHOR_KEY_MEMBROS`] /
3619 /// [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
3620 /// [`crate::M3_AUTHOR_KEY_POLITICAS`] /
3621 /// [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
3622 /// [`crate::M3_AUTHOR_KEY_ENTRADA`] consts declared next to the
3623 /// [`crate::M3_KEY_PLACEMENT`] renderer-side wire-key peer, so both
3624 /// halves of every M3 top-level mesh slot's dual axis (author-facing
3625 /// kebab-case label + renderer-side artifact key) route through one
3626 /// canonical declaration per arm — same discipline the peer
3627 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`]
3628 /// / [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot consts
3629 /// (f49c8b0) establish on the sibling per-Servico M2 top-level slot
3630 /// axis, extended here to close the M3 mesh-slot author-facing-label
3631 /// axis so both altitudes of the typed-slot algebra
3632 /// (per-Servico M2 + per-Aplicacao M3) share the same
3633 /// "one canonical byte-string per arm, next to the axis" discipline.
3634 #[must_use]
3635 pub fn declared_mesh_slots(&self) -> Vec<&'static str> {
3636 let mut slots = Vec::new();
3637 if !self.membros().is_empty() {
3638 slots.push(crate::render::M3_AUTHOR_KEY_MEMBROS);
3639 }
3640 if !self.contratos().is_empty() {
3641 slots.push(crate::render::M3_AUTHOR_KEY_CONTRATOS);
3642 }
3643 if self.politicas().is_some() {
3644 slots.push(crate::render::M3_AUTHOR_KEY_POLITICAS);
3645 }
3646 if self.placement().is_some() {
3647 slots.push(crate::render::M3_AUTHOR_KEY_PLACEMENT);
3648 }
3649 if self.entrada().is_some() {
3650 slots.push(crate::render::M3_AUTHOR_KEY_ENTRADA);
3651 }
3652 slots
3653 }
3654
3655 /// The kebab-case `:slot` tags of every supervisor-tree slot this
3656 /// caixa *declares* a value on, in canonical declaration order
3657 /// (`:estrategia` → `:max-restarts` → `:restart-window` →
3658 /// `:children`). A slot counts as declared when its backing field
3659 /// carries a value — a `Some(...)`, or a non-empty `Vec`.
3660 ///
3661 /// The supervisor-tree slots compose the typed OTP supervisor of a
3662 /// `:kind Supervisor` (INSPIRATIONS §II.2; the `:estrategia` +
3663 /// `:children` field docs above). [`Self::supervisor_view`] only
3664 /// folds them into a validatable [`SupervisorSpec`] when the kind
3665 /// matches (returns `None` otherwise), and the wasm-operator's
3666 /// hierarchical reconciler only consumes them for a Supervisor. On
3667 /// any *other* kind a declared supervisor slot is the manifest
3668 /// field's documented "ignored otherwise" (see the `:estrategia` …
3669 /// `:children` field docs): it silently passes [`Caixa::from_lisp`]
3670 /// and then vanishes — never validated, never reconciled — far from
3671 /// the source caixa.lisp. [`crate::StandardLayout::verify`] consults
3672 /// this to reject that silent-drop at caixa-build time
3673 /// ([`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]), the
3674 /// exact mirror of the [`Self::declared_mesh_slots`] /
3675 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] gate on the
3676 /// Aplicacao-only slot set: a slot foreign to the kind is a build
3677 /// error, not a silent drop.
3678 #[must_use]
3679 pub fn declared_supervisor_slots(&self) -> Vec<&'static str> {
3680 let mut slots = Vec::new();
3681 if self.estrategia().is_some() {
3682 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
3683 }
3684 if self.max_restarts().is_some() {
3685 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS);
3686 }
3687 if self.restart_window().is_some() {
3688 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW);
3689 }
3690 if !self.children().is_empty() {
3691 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
3692 }
3693 slots
3694 }
3695
3696 /// The kebab-case `:slot` tags of every M2 Servico-runtime slot this
3697 /// caixa *declares* a value on, in canonical declaration order
3698 /// (`:limits` → `:behavior` → `:upgrade-from`). A slot counts as
3699 /// declared when its backing field carries a value — a `Some(...)`,
3700 /// or a non-empty `Vec`.
3701 ///
3702 /// The M2 slots configure the runtime of a long-running wasm
3703 /// component, i.e. a `:kind Servico`: `:limits` is Lunatic
3704 /// per-process sandboxing (INSPIRATIONS §III.1), `:behavior` is the
3705 /// OTP `gen_server` callback set (§II.3), `:upgrade-from` is the OTP
3706 /// appup hot-code-reload table (§II.4). The caixa-helm / caixa-flux
3707 /// renderers gate on [`crate::require_kind`]`(_, Servico)` and only
3708 /// emit these slots for a Servico; on any *other* kind a declared M2
3709 /// slot is the manifest field's documented "ignored otherwise": its
3710 /// well-formedness is checked by [`crate::StandardLayout::verify`]
3711 /// but the value is never rendered into a chart / programs.yaml entry
3712 /// — it silently passes [`Caixa::from_lisp`] + `feira build` and then
3713 /// vanishes, far from the source caixa.lisp.
3714 /// [`crate::StandardLayout::verify`] consults this to reject that
3715 /// silent-drop at caixa-build time
3716 /// ([`crate::LayoutError::ServicoSlotsOnNonServico`]), the exact
3717 /// mirror of the [`Self::declared_mesh_slots`] /
3718 /// [`Self::declared_supervisor_slots`] gates on the peer
3719 /// kind-exclusive slot sets: a slot foreign to the kind is a build
3720 /// error, not a silent drop.
3721 ///
3722 /// Each per-arm kebab-case label is routed through the peer
3723 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
3724 /// [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts declared next to the
3725 /// [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
3726 /// [`crate::M2_KEY_UPGRADE_FROM`] renderer-side wire-key peers, so
3727 /// both halves of the M2 top-level slot's dual axis (author-facing
3728 /// kebab-case label + renderer-side camelCase overlay-container wire
3729 /// key) route through one canonical declaration per arm — same
3730 /// discipline the peer [`crate::M2_BEHAVIOR_AUTHOR_KEY_ON_*`] sub-slot
3731 /// author-label consts (889dc18) establish on the sibling
3732 /// per-callback axis inside the `:behavior` overlay block.
3733 #[must_use]
3734 pub fn declared_servico_slots(&self) -> Vec<&'static str> {
3735 let mut slots = Vec::new();
3736 if self.limits().is_some() {
3737 slots.push(crate::render::M2_AUTHOR_KEY_LIMITS);
3738 }
3739 if self.behavior().is_some() {
3740 slots.push(crate::render::M2_AUTHOR_KEY_BEHAVIOR);
3741 }
3742 if !self.upgrade_from().is_empty() {
3743 slots.push(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
3744 }
3745 slots
3746 }
3747
3748 /// The kebab-case `:slot` tags of every code-surface slot this caixa
3749 /// declares a value on that its [`CaixaKind`] doesn't natively own,
3750 /// in canonical declaration order (`:exe` → `:servicos`). A
3751 /// code-surface slot is owned by exactly one kind: `:exe` by
3752 /// [`CaixaKind::Binario`] (the nix-built executable surface), and
3753 /// `:servicos` by [`CaixaKind::Servico`] (the wasm component +
3754 /// `ComputeUnit` daemon surface).
3755 ///
3756 /// Each is silently ignored when declared on the wrong kind: the
3757 /// caixa-helm / caixa-flux / caixa-flake renderers gate on
3758 /// [`crate::require_kind`]`(_, <owning-kind>)`, so on any *other*
3759 /// code-running kind a declared `:exe` / `:servicos` is the manifest
3760 /// field's documented "ignored otherwise" — its path is checked for
3761 /// existence by the layout's `bibliotecas`/`exe`/`servicos` loops
3762 /// (which run after [`Caixa::from_lisp`]), but the value is never
3763 /// rendered into a build target or programs.yaml entry. It silently
3764 /// passes [`Caixa::from_lisp`] + `feira build`, far from the source
3765 /// caixa.lisp, with no field naming which slot is foreign.
3766 ///
3767 /// [`crate::StandardLayout::verify`] consults this to reject that
3768 /// silent-drop at caixa-build time
3769 /// ([`crate::LayoutError::ForeignCodeSlot`]), beside the M2
3770 /// servico-runtime, supervisor-tree, and M3 mesh kind-coherence
3771 /// gates ([`Self::declared_servico_slots`] /
3772 /// [`Self::declared_supervisor_slots`] /
3773 /// [`Self::declared_mesh_slots`]): the fourth kind ↔ slot algebra
3774 /// axis to be closed on the typed surface. The Supervisor /
3775 /// Aplicacao "no code at all" cases ([`crate::LayoutError::SupervisorOwnsCode`]
3776 /// / [`crate::LayoutError::AplicacaoOwnsCode`]) keep their dedicated
3777 /// diagnostics — they fire ahead of this gate on the same `verify`
3778 /// pass, so for Supervisor / Aplicacao the `OwnCode` arm always wins
3779 /// and this method is moot. For Biblioteca / Binario / Servico, this
3780 /// gate fires when a code-running kind declares another code-running
3781 /// kind's exclusive code surface.
3782 ///
3783 /// `:bibliotecas` is deliberately excluded — a Binario or Servico
3784 /// may legitimately ship a `lib/` helper that the underlying
3785 /// substrate (the nix flake for Binario, the wasm component build
3786 /// for Servico) bundles into its build, so the slot's
3787 /// declared-on-wrong-kind cardinality isn't a structural error on
3788 /// either code-running kind. A Biblioteca declaring `:bibliotecas`
3789 /// is the native case (the slot's owning kind). Supervisor /
3790 /// Aplicacao declaring `:bibliotecas` is gated upstream by
3791 /// [`crate::LayoutError::SupervisorOwnsCode`] /
3792 /// [`crate::LayoutError::AplicacaoOwnsCode`].
3793 ///
3794 /// Lifted as a typed method (rather than an inline disjunction at
3795 /// the verify call site) so the foreign-code-slot set lives in one
3796 /// place — a future kind that gains its own code-surface slot is
3797 /// one push here, and every consumer reaching for "which code
3798 /// surfaces are foreign to this kind" (the verify gate, a future
3799 /// `feira lint` kind-coherence advisory, the future `app-operator`'s
3800 /// per-caixa build-target classifier) inherits the canonical order
3801 /// without rolling its own.
3802 #[must_use]
3803 pub fn declared_foreign_code_slots(&self) -> Vec<&'static str> {
3804 let mut slots = Vec::new();
3805 if !self.exe().is_empty() && !self.kind().requires_exe() {
3806 slots.push(":exe");
3807 }
3808 if !self.servicos().is_empty() && !self.kind().requires_servicos() {
3809 slots.push(":servicos");
3810 }
3811 slots
3812 }
3813
3814 /// Validate every entry of `:deps` and `:deps-dev` through
3815 /// [`Dep::validate`] — closing the parity loop with the per-axis
3816 /// `:versao` gates already wired into the typed-graph
3817 /// ([`crate::AplicacaoSpec::validate_membros`] for `:membros`,
3818 /// 9888b13) and typed supervisor tree
3819 /// ([`crate::SupervisorSpec::validate`] for `:children`, b38ff3a).
3820 ///
3821 /// Until this gate landed `:deps :versao` and `:deps-dev :versao`
3822 /// were the only `:versao` axes still untyped past
3823 /// [`Caixa::from_lisp`]: the derive macro stored the requirement
3824 /// as a String without parsing it, so a malformed-but-non-empty
3825 /// requirement (`"^bad-version"`, `"^^0.1"`, `"v0.1"`, `"not-a-req"`)
3826 /// silently passed parse and the `semver::Error` surfaced at
3827 /// lacre-resolve time, far from the source caixa.lisp, with no
3828 /// field naming which `:deps` entry carried the typo. Lifting the
3829 /// gate here makes the four `:versao` typed surfaces (`:deps`,
3830 /// `:deps-dev`, `:membros`, `:children`) structurally equivalent —
3831 /// every requirement string past `validate_deps` is round-trippable
3832 /// through [`crate::parse_requirement`] without re-checking at the
3833 /// resolver layer.
3834 ///
3835 /// Both lists run through the same per-entry validator so a typo
3836 /// in `:deps-dev` surfaces with the same diagnostic as one in
3837 /// `:deps` — neither axis is a second-class citizen of the typed
3838 /// surface.
3839 ///
3840 /// Within each list, [`DepError::DuplicateNome`] closes the
3841 /// set-not-multiset discipline on the `:nome` axis: two entries
3842 /// naming the same caixa carry two `:versao` / `:fonte` / feature
3843 /// triples that the caixa-resolver's lacre pipeline collapses to one
3844 /// via its `HashMap`-keyed-by-`:nome` consumption — the second entry
3845 /// silently overwrites the first at `concrete_versao`-resolve time
3846 /// (the same "second wins / one silently overwrites the other"
3847 /// shape the peer typed-graph duplicate gates already close on every
3848 /// other Vec-shaped authoring surface that keys by name). The
3849 /// duplicate check fires per-list and runs *after* each per-entry
3850 /// [`Dep::validate`] call so a malformed-and-duplicated entry
3851 /// surfaces its narrower per-entry diagnostic
3852 /// ([`DepError::NomeInvalid`], [`DepError::VersaoInvalid`],
3853 /// [`DepError::FonteRepoEmpty`], …) before the cross-entry duplicate
3854 /// diagnostic — the canonical "per-entry shape before cross-entry
3855 /// uniqueness" precedence the peer `:children :caixa`
3856 /// ([`crate::SupervisorSpec::validate`]), `:membros :caixa`
3857 /// ([`crate::AplicacaoSpec::validate_membros`]), `:contratos`
3858 /// ([`crate::AplicacaoSpec::validate`]), `:placement :clusters`
3859 /// ([`crate::AplicacaoSpec::validate_placement`]),
3860 /// `:entrada :paths` ([`crate::AplicacaoSpec::validate`]),
3861 /// `:upgrade-from :from` ([`crate::upgrade::validate_upgrade_from`]),
3862 /// and the within-`:upgrade-from`-entry per-instruction-class
3863 /// singularity gates ([`crate::UpgradeError::DuplicateLoadModule`],
3864 /// [`crate::UpgradeError::DuplicateStateChange`],
3865 /// [`crate::UpgradeError::DuplicateCleanup`]) all establish.
3866 ///
3867 /// Cross-list (`:deps` ↔ `:deps-dev`) coincidence is *not* gated
3868 /// here: Cargo's `[dependencies]` + `[dev-dependencies]` accept the
3869 /// same name in both tables (the dev table's pin overrides the
3870 /// runtime table's pin in test/dev contexts), and caixa's surface
3871 /// mirrors that convention until a deliberate choice retires the
3872 /// override pattern. Only within-list duplicates are structurally
3873 /// incoherent — those are what this gate closes.
3874 pub fn validate_deps(&self) -> Result<(), DepError> {
3875 for &list in crate::dep::DepList::ALL {
3876 let mut seen = std::collections::HashSet::new();
3877 for dep in self.deps_of(list) {
3878 dep.validate()?;
3879 crate::render::insert_first_seen(&mut seen, dep.nome(), || {
3880 DepError::DuplicateNome {
3881 nome: dep.nome().to_string(),
3882 list: list.as_str(),
3883 }
3884 })?;
3885 }
3886 }
3887 Ok(())
3888 }
3889
3890 /// Reject `:nome` values the K8s apiserver would refuse at admission
3891 /// time. The top-level Caixa identity flows directly into every
3892 /// substrate-side artifact's `metadata.name` axis: the
3893 /// `lareira-<nome>` Helm chart name ([`caixa-helm::lib::chart_name`]),
3894 /// the programs.yaml `name:` entry the `lareira-fleet-programs`
3895 /// aggregator keys ComputeUnit derivation off
3896 /// ([`caixa-flux::lib::programs_yaml_entry`]), the
3897 /// `LABEL_APLICACAO` label value carried on every Aplicacao-owned
3898 /// pod and the per-`:contratos` CiliumNetworkPolicy `metadata.name`
3899 /// (`<aplicacao>-<de>-to-<para>`) and the per-`:entrada`
3900 /// `<aplicacao>-<para>` HTTPRoute `metadata.name`
3901 /// ([`caixa-mesh::lib::cilium_network_policies`],
3902 /// [`caixa-mesh::lib::gateway_routes`]), and the default
3903 /// `lib/<nome>.lisp` / `exe/<nome>` layout paths
3904 /// ([`crate::StandardLayout::verify`]). Each K8s apiserver-side
3905 /// schema enforces the DNS-1123 label rule on admission; a
3906 /// structurally invalid `:nome` (`"MyApp"` — the canonical
3907 /// "I copied the display name verbatim" footgun, `"my_app"` — the
3908 /// Python-/Postgres-leak, `"team.app"` — `:nome` is a single label
3909 /// not a subdomain, `"-app"` / `"app-"` — DNS-1123 boundary
3910 /// violations, `"my app"` — the paste-from-doc footgun, `"café"` —
3911 /// IDN must be pre-encoded as Punycode, the 64-byte UUID-shaped
3912 /// over-cap slug) silently passed [`Caixa::from_lisp`] and the
3913 /// failure surfaced at `kubectl apply` time as a `metadata.name:
3914 /// Invalid value` rejection on whichever derived artifact admitted
3915 /// first, far from the source `caixa.lisp` and without any field
3916 /// naming the offending `:nome`.
3917 ///
3918 /// Thin wrapper around [`crate::render::is_dns_1123_label`] (the
3919 /// substrate-side predicate the per-axis name gates already share:
3920 /// `:membros :caixa` 3f9d7a0, `:placement :clusters` 6cbb900,
3921 /// `:children :caixa` 31bfa43) that maps the shared parser-shaped
3922 /// reason into the [`ManifestError::NomeInvalid`] variant, so the
3923 /// diagnostic is self-locating (the offending `:nome` is named
3924 /// verbatim) and the author can grep their `caixa.lisp` for
3925 /// `:nome "<value>"` and fix it in one edit. Same diagnostic shape
3926 /// every per-axis sibling gate already exposes
3927 /// ([`crate::AplicacaoError::MembroCaixaInvalid`],
3928 /// [`crate::AplicacaoError::PlacementClusterInvalid`],
3929 /// [`crate::SupervisorError::ChildCaixaInvalid`]).
3930 ///
3931 /// Empty `:nome` (which [`Caixa::from_lisp`] does not reject — the
3932 /// derive macro stores the raw String) is gated by the narrower
3933 /// [`ManifestError::NomeEmpty`] arm before the predicate is
3934 /// consulted, mirroring the empty-first cascade every per-axis
3935 /// name gate already uses (e.g. `MembroCaixaEmpty` before
3936 /// `MembroCaixaInvalid`, `EmptyChildName` before `ChildCaixaInvalid`).
3937 pub fn validate_nome(&self) -> Result<(), ManifestError> {
3938 // Routes through the shared
3939 // [`crate::render::require_valid_dns_1123_label`] gate the peer
3940 // name axes each land on so drift between the eight axes'
3941 // accepted DNS-1123-label sets is structurally impossible.
3942 let nome = self.nome();
3943 crate::render::require_valid_dns_1123_label(
3944 nome,
3945 || ManifestError::NomeEmpty,
3946 |reason| ManifestError::NomeInvalid {
3947 nome: nome.to_string(),
3948 reason,
3949 },
3950 )
3951 }
3952
3953 /// Reject `:nome` values whose joint length with the canonical
3954 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] (`"lareira-"`) overflows
3955 /// the K8s DNS-1123 label cap [`crate::DNS_1123_LABEL_MAX_LEN`]
3956 /// (63 bytes). Every per-Servico / per-Aplicacao renderer the
3957 /// substrate carries materializes the caixa's `:nome` through the
3958 /// canonical [`crate::lareira_chart_name`] helper (f7320d7) into a
3959 /// `lareira-<nome>` artifact that lands as a K8s `metadata.name` /
3960 /// Helm chart name / `HelmRelease` `release_name`: `caixa-helm`'s
3961 /// `ChartDir.name` + `Chart.yaml::name`
3962 /// (caixa-helm/src/lib.rs:207), `caixa-flux`'s `cluster_bundle`
3963 /// `HelmRelease` `chart:` slot (caixa-flux/src/lib.rs:329),
3964 /// `caixa-tatara`'s `process_for_aplicacao` `release_name` +
3965 /// `oci://<registry>/lareira-<nome>` chart ref
3966 /// (caixa-tatara/src/lib.rs:124,178). Helm's own `Chart.yaml::name`
3967 /// admission rule strict-parses against DNS-1123-label, the Helm
3968 /// operator's tracking-secret name is derived from `release_name`
3969 /// and is itself DNS-1123-label-bounded, and the rendered chart's
3970 /// K8s object `metadata.name` axes embed the chart name as a
3971 /// prefix — every one fails admission on a > 63-byte chart name.
3972 ///
3973 /// The per-axis [`Self::validate_nome`] gate (6c992f8) already
3974 /// caps `:nome` itself at 63 bytes via [`is_dns_1123_label`], so a
3975 /// `:nome` of 56–63 bytes silently passed validate (the inner
3976 /// DNS-1123 check accepts the bare `:nome`) but produced a
3977 /// `lareira-<nome>` of 64–71 bytes that the apiserver / `helm lint`
3978 /// rejected at admission — far from the source `caixa.lisp`, with
3979 /// no field naming the overflow root cause. The
3980 /// [`lareira_chart_name`] helper's own doc comment
3981 /// (caixa-core/src/render.rs:3198) explicitly deferred the fix:
3982 /// "the M4 admission webhook will pin the joint-length invariant
3983 /// when it lands". This gate lands the invariant at the
3984 /// manifest-validate layer rather than waiting for the apiserver
3985 /// — the same fail-at-the-source posture every peer per-axis
3986 /// value-shape gate (DNS-1123 on `:nome`, SemVer-2 on `:versao`,
3987 /// SPDX-expression-shape on `:licenca`, 4-digit decimal year on
3988 /// `:edicao`, etc.) takes.
3989 ///
3990 /// Thin wrapper around
3991 /// [`crate::render::is_lareira_chart_name_shape`] (the
3992 /// substrate-side predicate that composes [`lareira_chart_name`] +
3993 /// [`is_dns_1123_label`] via the lifted
3994 /// [`crate::LAREIRA_CHART_NAME_NOME_MAX_LEN`] budget); maps the
3995 /// shared parser-shaped reason into the
3996 /// [`ManifestError::NomeChartNameBudgetExceeded`] variant so the
3997 /// diagnostic is self-locating (the offending `:nome` is named
3998 /// verbatim alongside the rendered chart name and the budget) and
3999 /// the author can shorten in one edit. The gate runs across every
4000 /// `:kind` — `:nome` is the substrate-wide identity axis any
4001 /// future renderer the substrate adds can derive a
4002 /// `lareira-<nome>` artifact from, and uniform enforcement closes
4003 /// the drift footgun where a future kind grows a chart-emitting
4004 /// render path while the validate cascade doesn't catch it.
4005 ///
4006 /// Runs *after* [`Self::validate_nome`] so the narrower
4007 /// `NomeEmpty` / `NomeInvalid` shape diagnostics fire first — a
4008 /// structurally-malformed `:nome` (empty, uppercase, underscore,
4009 /// dot, leading/trailing hyphen, Unicode, > 63 bytes) surfaces its
4010 /// specific shape error rather than the chart-name-budget error,
4011 /// preserving the legitimate "well-shaped `:nome` that happens to
4012 /// overflow the joint cap" arm for this gate.
4013 pub fn validate_nome_chart_name_budget(&self) -> Result<(), ManifestError> {
4014 let nome = self.nome();
4015 crate::render::is_lareira_chart_name_shape(nome).map_err(|reason| {
4016 ManifestError::NomeChartNameBudgetExceeded {
4017 nome: nome.to_string(),
4018 reason,
4019 }
4020 })
4021 }
4022
4023 /// Reject `:versao` values that don't parse as [`semver::Version`].
4024 /// The top-level Caixa version flows directly into every
4025 /// substrate-side artifact that carries a "this is which version of
4026 /// the caixa" axis: the `lareira-<nome>` Helm chart's `Chart.yaml`
4027 /// `version:` + `appVersion:` axes ([`caixa-helm::lib`] —
4028 /// SemVer-2-strict at `helm template` / `helm install` time per
4029 /// https://helm.sh/docs/topics/charts/#charts-and-versioning), the
4030 /// `feira publish` Zig-style `v<versao>` git tag
4031 /// ([`caixa-flux::lib::programs_yaml_entry`] / the
4032 /// `caixa-publish.yml` reusable workflow), the programs.yaml entry's
4033 /// `versao:` value the `lareira-fleet-programs` aggregator carries
4034 /// onto each rendered ComputeUnit, the OCI image's `:v<versao>` /
4035 /// `:latest` tags the substrate's `wasi-service-flake` builds with
4036 /// `skopeo push`, the lacre closure's pinned versions
4037 /// ([`caixa-resolver`] keys `concrete_versao`), and the
4038 /// `:upgrade-from :from` references peers in this exact `versao`
4039 /// shape (`semver::Version`, not `VersionReq`). Each consumer
4040 /// expects a strict three-part `MAJOR.MINOR.PATCH` (optionally
4041 /// `-prerelease` and/or `+build`); a structurally invalid `:versao`
4042 /// (`"0.1"` — missing patch, the canonical "I shortened it" footgun;
4043 /// `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo;
4044 /// `"latest"` / `"main"` — the "I confused it with a docker tag"
4045 /// footgun; `"^0.1"` / `"~0.1.2"` — the requirement-shape leaking
4046 /// into the version field a peer `:deps :versao` accepts;
4047 /// `"0.1.0.0"` — the four-part Java/Microsoft convention DNS
4048 /// SemVer-2 forbids) silently passed [`Caixa::from_lisp`] (the
4049 /// derive macro stores the raw String) and the failure surfaced at
4050 /// the *first* downstream consumer that strict-parses it: at
4051 /// `helm install` time as a chart-version rejection, at
4052 /// `feira publish` time as a malformed git tag, at lacre-resolve
4053 /// time as a `semver::Error` not naming the offending caixa, at
4054 /// `feira upgrade --to <versao>` time as an unresolvable
4055 /// `:upgrade-from :from` match — far from the source `caixa.lisp`
4056 /// and without any field naming the offending `:versao`.
4057 ///
4058 /// Thin wrapper around [`semver::Version::parse`] — the same parser
4059 /// [`crate::CaixaVersion::parse`] (the typed `:versao` accessor)
4060 /// and [`crate::UpgradeFromEntry::validate`] (the peer
4061 /// `:upgrade-from :from` axis, 26da2c7) consume. Maps the
4062 /// `semver::Error` reason into the [`ManifestError::VersaoInvalid`]
4063 /// variant, carrying the offending `:versao` verbatim + a
4064 /// parser-shaped reason naming the specific violation, so the
4065 /// diagnostic is self-locating (the author can grep their
4066 /// `caixa.lisp` for `:versao "<value>"` and fix it in one edit).
4067 /// Same diagnostic shape as [`ManifestError::NomeInvalid`]
4068 /// (6c992f8) and [`crate::UpgradeError::FromInvalid`]
4069 /// (b0c8389) on the peer axes. With this gate, the typed `:versao`
4070 /// surfaces — top-level `:versao`, `:upgrade-from :from` — are
4071 /// now structurally equivalent (every value past validate is
4072 /// round-trippable through [`semver::Version::parse`] without
4073 /// re-checking at the renderer, resolver, or operator hot-upgrade
4074 /// layer), peer with the four `:versao` requirement axes (`:deps`,
4075 /// `:deps-dev`, `:membros`, `:children`) the prior commits
4076 /// (2420c44, 9888b13, b38ff3a) wired through `parse_requirement`.
4077 ///
4078 /// Empty `:versao` (which [`Caixa::from_lisp`] does not reject —
4079 /// the derive macro stores the raw String) is gated by the
4080 /// narrower [`ManifestError::VersaoEmpty`] arm before the parser is
4081 /// consulted, mirroring the empty-first cascade every per-axis
4082 /// version gate already uses (e.g. `MembroVersaoEmpty` before
4083 /// `MembroVersaoInvalid`, `EmptyChildVersion` before
4084 /// `ChildVersaoInvalid`, `NomeEmpty` before `NomeInvalid`).
4085 pub fn validate_versao(&self) -> Result<(), ManifestError> {
4086 let versao = self.versao();
4087 if versao.is_empty() {
4088 return Err(ManifestError::VersaoEmpty);
4089 }
4090 semver::Version::parse(versao).map_err(|e| ManifestError::VersaoInvalid {
4091 versao: versao.to_string(),
4092 reason: e.to_string(),
4093 })?;
4094 Ok(())
4095 }
4096
4097 /// Reject `:restart-window` values the shared
4098 /// [`crate::supervisor::duration_codec::parse`] refuses. The flat
4099 /// `restart_window: Option<String>` slot on [`Caixa`] is stored
4100 /// raw by the derive macro (the typed [`SupervisorSpec`] holds an
4101 /// `Option<Duration>` routed through the shared codec via `with =
4102 /// "duration_codec"`); the inline `Caixa → SupervisorSpec`
4103 /// view-construction path ([`Self::supervisor_view`]) folds the
4104 /// raw string through the same shared codec and soft-swallows the
4105 /// parse error as `None` to keep the view best-effort. Without
4106 /// this gate a malformed `:restart-window` (`"1.5s"` — the
4107 /// fractional-seconds drift class; `"1.0s"` — the decimal-shaped
4108 /// integer drift; `"0.5m"` — the unit-fraction drift; `"+30s"` /
4109 /// `"-30s"` — the leading-sign drift; `"30x"` — the unknown-unit
4110 /// footgun; `"abc"` — pure garbage; `""` — the empty-after-trim
4111 /// edge case) silently produced a `SupervisorSpec` with
4112 /// `restart_window: None`, indistinguishable from the canonical
4113 /// "omit the slot to express no reset" authoring shape — Erlang/OTP's
4114 /// `MaxIntensity / Period` invariant turns into a never-reset
4115 /// supervisor far from the source `caixa.lisp`, with no field
4116 /// naming the offending `:restart-window`. Lifting the gate to a
4117 /// Caixa-level validator mirrors the trajectory of the peer
4118 /// per-axis identity gates ([`Self::validate_nome`] 6c992f8,
4119 /// [`Self::validate_versao`] 1fdaa02, [`Self::validate_deps`]
4120 /// a7f0d8c) and the ABSORPTION-ROADMAP.md M2.2 test pin
4121 /// (line 196: "reject invalid `:restart-window` (non-duration)").
4122 ///
4123 /// Thin wrapper around [`crate::supervisor::duration_codec::parse`]
4124 /// (the shared codec backing `:supervisor :restart-window` as
4125 /// serde-routed on [`SupervisorSpec`], `:politicas :timeout`, and
4126 /// `:politicas :circuit-breaker :window` — all three covered by
4127 /// the integer-magnitude gate 1c55a2a). Maps the codec's parse
4128 /// error verbatim into the [`ManifestError::RestartWindowMalformed`]
4129 /// variant, carrying the offending raw string + a parser-shaped
4130 /// reason naming the canonical authoring form, so the diagnostic
4131 /// is self-locating (the author can grep their `caixa.lisp` for
4132 /// `:restart-window "<value>"` and fix it in one edit) and
4133 /// uniform with every other manifest-level validate diagnostic.
4134 /// With this gate the four `:restart-window`-shaped surfaces (the
4135 /// flat raw string on [`Caixa`], the typed `Option<Duration>` on
4136 /// [`SupervisorSpec`], the two `MeshPolicy` peer durations) are
4137 /// now structurally equivalent — every value past the codec is in
4138 /// one accepted set, by construction.
4139 ///
4140 /// `None` (the canonical "omit the slot to express no reset"
4141 /// shape) is accepted trivially — the gate is a no-op when the
4142 /// author didn't author a window. The empty string is rejected by
4143 /// the shared codec (its digit-only gate refuses an empty
4144 /// magnitude), surfacing the same `RestartWindowMalformed`
4145 /// diagnostic as every other rejected non-canonical shape.
4146 pub fn validate_restart_window(&self) -> Result<(), ManifestError> {
4147 let Some(s) = self.restart_window() else {
4148 return Ok(());
4149 };
4150 crate::supervisor::duration_codec::parse(s)
4151 .map(|_| ())
4152 .map_err(|reason| ManifestError::RestartWindowMalformed {
4153 restart_window: s.to_string(),
4154 reason,
4155 })
4156 }
4157
4158 /// Reject per-entry values on the three Caixa-level code-surface
4159 /// path lists (`:bibliotecas`, `:exe`, `:servicos`) that the
4160 /// layout checker's `root.join(p)` sandbox would silently subvert.
4161 /// Same three structural footguns the peer
4162 /// [`BehaviorSpec::validate`] (b0c8389) and
4163 /// [`crate::UpgradeInstruction::validate`] `StateChange` arm
4164 /// (26da2c7) already close on the M2 `:behavior :on-*` and
4165 /// `:upgrade-from :state-change :script` axes, here lifted onto
4166 /// the three top-level code-path axes through the shared
4167 /// [`is_sandboxed_relative_path`] predicate:
4168 ///
4169 /// - empty entry (`(:bibliotecas (""))` / `(:exe (""))` /
4170 /// `(:servicos (""))`): `PathBuf::new()` round-trips through
4171 /// [`Path::join`] as the base itself — `root.join("")` ==
4172 /// `root`, so the existence check (`self.exists(&root)`)
4173 /// trivially passes (the project root exists), and the layout
4174 /// silently treats the project root as a biblioteca / exe /
4175 /// servico entry. The `:bibliotecas` loop then hands the root
4176 /// to `tatara_lisp::read` at `feira build` time as if the root
4177 /// directory itself were a Lisp source file — a parse error
4178 /// far from the source `caixa.lisp` with no field naming the
4179 /// offending entry.
4180 /// - absolute path (`(:bibliotecas ("/etc/passwd"))`):
4181 /// [`Path::join`] *replaces* the base when the right-hand side
4182 /// is absolute, so `root.join("/etc/passwd")` resolves to
4183 /// `"/etc/passwd"` and escapes the project sandbox entirely.
4184 /// The existence check then silently consults whatever the
4185 /// escaped path resolves to — for `:bibliotecas`, the layout
4186 /// has no `starts_with`-fence (only `:exe` is fenced under
4187 /// `exe/` and `:servicos` under `servicos/`), so an absolute
4188 /// `:bibliotecas` entry that happens to resolve on disk
4189 /// silently passes. For `:exe` / `:servicos` the fence catches
4190 /// the absolute case downstream as `ExeOutsideDir` /
4191 /// `ServicoOutsideDir` (or `MissingEntry` if the absolute path
4192 /// doesn't exist), but with a downstream-shaped diagnostic
4193 /// that names the resolved escape path rather than the
4194 /// authoring footgun at the source.
4195 /// - parent-escape (`(:bibliotecas ("../sibling/x.lisp"))` /
4196 /// `(:exe ("exe/../../escape.lisp"))`): a [`PathBuf`] with any
4197 /// [`std::path::Component::ParentDir`] anywhere round-trips
4198 /// through [`Path::join`] as a traversal above the caixa root.
4199 /// The `:exe` / `:servicos` `starts_with(<dir>)` fence is
4200 /// *component-aware* (not canonical-path-aware), so
4201 /// `root.join("exe/../../escape.lisp")` `starts_with(exe_dir)`
4202 /// is **true** even though the canonical resolution
4203 /// `{parent of root}/escape.lisp` lives outside the caixa root
4204 /// — the fence silently lets the parent-escape through, and
4205 /// the existence check passes if that escape-target happens
4206 /// to exist. Caught regardless of where the `..` sits
4207 /// (leading, mid-path, trailing) so the gate matches the peer
4208 /// predicate's full coverage.
4209 ///
4210 /// Same `Empty` → `Absolute` → `ParentEscape` arm-ordering every peer
4211 /// `is_sandboxed_relative_path` consumer follows (b0c8389 / 26da2c7);
4212 /// same per-slot diagnostic shape every peer per-axis path-gate
4213 /// exposes (`*Empty { slot }` / `*Absolute { slot, path }` /
4214 /// `*ParentEscape { slot, path }`). Cross-slot precedence is
4215 /// `:bibliotecas` → `:exe` → `:servicos` — the same declaration
4216 /// order [`Caixa::declared_foreign_code_slots`] uses for its
4217 /// canonical foreign-code-slot diagnostic, so a manifest with
4218 /// multiple malformed slots surfaces the lexicographically-earliest
4219 /// slot's diagnostic deterministically.
4220 ///
4221 /// Lifted to the typed surface as a Caixa-level validator (peer
4222 /// of [`Self::validate_nome`] / [`Self::validate_versao`] /
4223 /// [`Self::validate_deps`] / [`Self::validate_restart_window`])
4224 /// and wired into [`crate::StandardLayout::verify`] before the
4225 /// existence-check loops so the diagnostic names the offending
4226 /// slot at the source caixa.lisp rather than reporting a
4227 /// downstream `MissingEntry` / `ExeOutsideDir` /
4228 /// `ServicoOutsideDir` against the resolved sandbox-escape path.
4229 /// The fourth typed code-path surface — every author-supplied
4230 /// path on the manifest — is now structurally accept-shaped
4231 /// past validate, peer with `:behavior :on-*` and
4232 /// `:upgrade-from :state-change :script`.
4233 pub fn validate_code_paths(&self) -> Result<(), ManifestError> {
4234 /// Per-slot file-type contract for the three Caixa-level
4235 /// code-path surfaces (`:bibliotecas`, `:exe`, `:servicos`).
4236 /// Each variant names the predicate the per-entry file-type
4237 /// gate consults; [`Self::None`] opts the slot out of any
4238 /// file-type contract. Lifted as a typed local enum so the
4239 /// per-slot dispatch is exhaustive at the `match` — adding a
4240 /// future axis to the typed-substrate `:` slot set (the
4241 /// future `:assets` resource axis the M5 roadmap names, the
4242 /// future `:nix-flake` derivation axis the caixa-flake
4243 /// emitter consults) lands as one variant + one `match` arm,
4244 /// not a coordinated rewrite of every per-slot bool flag.
4245 ///
4246 /// Peer of the typed-substrate per-slot variant disciplines
4247 /// already established on this surface
4248 /// ([`crate::supervisor::RestartStrategy`] +
4249 /// [`crate::supervisor::RestartPolicy`] on the OTP-shape
4250 /// supervision-tree axis,
4251 /// [`crate::aplicacao::PlacementStrategy`] on the §III.1
4252 /// placement axis, [`crate::aplicacao::WitTarget`] on the
4253 /// `:contratos` payload-target axis): the typed `enum` is
4254 /// the substrate's single source of truth for the per-axis
4255 /// dispatch, and every consumer (the per-arm body here, the
4256 /// future feira-lint per-slot diagnostic renderer, the M4
4257 /// per-axis admission webhook) reaches for the same typed
4258 /// surface rather than re-deriving the partition from inline
4259 /// flag combinations.
4260 enum CodePathFileType {
4261 /// `:exe` — nix-build derivation output, no terminating-
4262 /// extension contract (the canonical `"exe/<name>"`
4263 /// fixtures the layout's `ExeOutsideDir` error message
4264 /// documents carry no extension by convention).
4265 None,
4266 /// `:bibliotecas` — tatara-lisp source files the
4267 /// `feira build` loop reads through `tatara_lisp::read`
4268 /// at parse time. Routes to [`is_lisp_extension`].
4269 LispSource,
4270 /// `:servicos` — ComputeUnit-CR YAML files the
4271 /// caixa-helm / caixa-flux renderers consume through
4272 /// `serde_yaml::from_str`. Routes to
4273 /// [`is_computeunit_yaml_extension`].
4274 ComputeUnitYaml,
4275 }
4276
4277 // The per-slot [`CodePathFileType`] selects which axes carry the
4278 // lifted file-type predicate. `:bibliotecas` is the tatara-lisp
4279 // source axis (the `feira build` loop at
4280 // `caixa-feira/src/cmd/build.rs:33` reads each entry through
4281 // `tatara_lisp::read` at parse time) — the lifted
4282 // [`is_lisp_extension`] predicate gates the `.lisp` extension.
4283 // `:exe` is the nix-built executable surface (per the canonical
4284 // `"exe/<name>"`-shaped fixtures the layout's `ExeOutsideDir`
4285 // error message documents and every in-tree
4286 // `caixa_with_code_paths` positive control uses) — its file-type
4287 // contract is "nix-build derivation output", not a typed source
4288 // file, so [`CodePathFileType::None`] opts the slot out of any
4289 // file-type gate. `:servicos` is the `.computeunit.yaml`
4290 // ComputeUnit-CR axis (the peer caixa-helm / caixa-flux
4291 // renderers consume each entry through `serde_yaml::from_str` as
4292 // a typed `ComputeUnit` CR) — the lifted
4293 // [`is_computeunit_yaml_extension`] predicate gates the compound
4294 // `.computeunit.yaml` suffix. All three axes are surfaced through
4295 // the same iteration so the sandbox-shape + duplicate gates
4296 // apply uniformly; the typed file-type dispatch fires per-slot
4297 // exactly where the downstream consumer's accepted set demands
4298 // it. The third file-type variant ([`ComputeUnitYaml`]) is the
4299 // compounding lift on the peer 64772a9 `:bibliotecas`
4300 // `.lisp`-gate trajectory — the second of the three code-path
4301 // axes to land on a typed compound-suffix gate, with the same
4302 // self-locating per-slot diagnostic shape every peer per-axis
4303 // file-type lift uses (`*NonLispExtension { slot, path }` /
4304 // `*NonComputeUnitYamlExtension { slot, path }`).
4305 for (slot, list, file_type) in [
4306 (
4307 ":bibliotecas",
4308 &self.bibliotecas,
4309 CodePathFileType::LispSource,
4310 ),
4311 (":exe", &self.exe, CodePathFileType::None),
4312 (
4313 ":servicos",
4314 &self.servicos,
4315 CodePathFileType::ComputeUnitYaml,
4316 ),
4317 ] {
4318 // Per-slot set-not-multiset gate on the typed code-path axis.
4319 // Every peer Vec-shaped author-supplied list past validate is
4320 // a set, not a multiset: `:membros :caixa`
4321 // ([`crate::AplicacaoError::MembroDuplicate`]), `:placement
4322 // :clusters` ([`crate::AplicacaoError::PlacementClusterDuplicate`]),
4323 // `:entrada :paths` ([`crate::AplicacaoError::EntradaPathDuplicate`]),
4324 // `:contratos` ([`crate::AplicacaoError::ContratoDuplicate`]),
4325 // `:children :caixa` ([`crate::SupervisorError::DuplicateChild`]),
4326 // `:deps` / `:deps-dev` `:nome` ([`crate::DepError::DuplicateNome`]
4327 // per 359fba5), `:upgrade-from :from` ([`crate::UpgradeError::DuplicateFrom`]),
4328 // `:etiquetas` ([`ManifestError::EtiquetaDuplicate`] per 360a499),
4329 // `:autores` ([`ManifestError::AutorDuplicate`] per 86c769b) —
4330 // the three code-path lists are the last Vec-shaped author-
4331 // supplied slots on the typed Caixa surface still admitting a
4332 // duplicate entry silently. Scope is per-list (`:bibliotecas`
4333 // duplicates are flagged within `:bibliotecas`, not across
4334 // `:bibliotecas` ↔ `:exe`) — the same per-list scope `:deps`
4335 // ↔ `:deps-dev` use (a `:nome` present in both lists is a
4336 // legitimate dev-vs-runtime shape on the dep axis, fenced
4337 // separately by [`crate::dep::validate_no_self_dep`]). On the
4338 // code-path axis a cross-slot collision is structurally
4339 // impossible by the layout's `starts_with(<exe|servicos>_dir)`
4340 // fence — `:exe` and `:servicos` entries are confined to their
4341 // own directory trees, so the only way a string could appear
4342 // on two code-path lists is the (rare, structurally invalid)
4343 // case where `:bibliotecas` carries an `"exe/<x>"` or
4344 // `"servicos/<x>.yaml"`-shaped path.
4345 //
4346 // Without the gate three authoring footguns silently passed:
4347 //
4348 // - `:bibliotecas ("lib/foo.lisp" "lib/foo.lisp")` — the
4349 // canonical copy-paste-the-wrong-file footgun. `feira
4350 // build` (`caixa-feira/src/cmd/build.rs:33`) walks the
4351 // list and re-parses the same file twice, wasting work
4352 // and silently masking the author's intent to declare a
4353 // *second* biblioteca.
4354 // - `:exe ("exe/cli" "exe/cli")` — the same footgun on the
4355 // Binario surface. The future `caixa-flake` `nix flake`
4356 // emitter that materializes each `:exe` entry as a flake
4357 // `packages.<exe-name>` derivation would collide on the
4358 // duplicate package name and surface a flake-eval error
4359 // far from the source `caixa.lisp`.
4360 // - `:servicos ("servicos/x.computeunit.yaml"
4361 // "servicos/x.computeunit.yaml")` — the same footgun on
4362 // the Servico surface. The peer `caixa-helm` / `caixa-flux`
4363 // renderers already refuse `:servicos.len() != 1` with
4364 // the narrower [`UnsupportedServicoCount`] diagnostic, but
4365 // that diagnostic surfaces "too many servicos" without
4366 // naming "duplicate entry" — the typed self-locating
4367 // "which entry is the duplicate" framing only lands at
4368 // this gate.
4369 //
4370 // Same `seen.insert(entry.as_str())` shape every peer per-list
4371 // duplicate gate uses (`:etiquetas` 360a499, `:autores`
4372 // 86c769b, `:deps` 359fba5) and the same "structural shape
4373 // checks fire before the duplicate check on the same entry"
4374 // ordering (a `(:bibliotecas ("" "lib/x.lisp" "lib/x.lisp"))`
4375 // shape surfaces the narrower [`Self::CodePathEmpty`] for the
4376 // empty entry first, not the duplicate on the later pair).
4377 let mut seen = std::collections::HashSet::new();
4378 for entry in list {
4379 let path = Path::new(entry);
4380 match is_sandboxed_relative_path(path) {
4381 Ok(()) => {}
4382 Err(PathShapeViolation::Empty) => {
4383 return Err(ManifestError::CodePathEmpty { slot });
4384 }
4385 Err(PathShapeViolation::Absolute) => {
4386 return Err(ManifestError::CodePathAbsolute {
4387 slot,
4388 path: path.to_path_buf(),
4389 });
4390 }
4391 Err(PathShapeViolation::ParentEscape) => {
4392 return Err(ManifestError::CodePathParentEscape {
4393 slot,
4394 path: path.to_path_buf(),
4395 });
4396 }
4397 }
4398 // The per-slot file-type gate dispatched through the
4399 // typed [`CodePathFileType`] selector above. Each variant
4400 // routes to the lifted predicate the downstream consumer
4401 // demands:
4402 //
4403 // - [`LispSource`] → [`is_lisp_extension`] for
4404 // `:bibliotecas` (the `feira build` loop's
4405 // `tatara_lisp::read` consumer);
4406 // - [`ComputeUnitYaml`] → [`is_computeunit_yaml_extension`]
4407 // for `:servicos` (the caixa-helm / caixa-flux
4408 // `serde_yaml::from_str` consumer's `ComputeUnit` CR
4409 // accepted set);
4410 // - [`None`] for `:exe` — the nix-build derivation-
4411 // output axis has no terminating-extension contract.
4412 //
4413 // Fires after the sandbox-shape arms so a path that is
4414 // *both* sandbox-escaping and wrong-extension surfaces
4415 // the more fundamental sandbox-shape diagnostic first
4416 // (mirrors the peer `EmptyPath` → `AbsolutePath` →
4417 // `ParentEscape` → `NonLispExtension` arm-ordering on
4418 // `:behavior :on-*` c97815a, and `EmptyScript` →
4419 // `AbsoluteScript` → `ParentEscapeScript` →
4420 // `NonLispExtensionScript` on
4421 // `:upgrade-from :state-change :script` 33cc830), and
4422 // before the duplicate gate so the narrower per-entry
4423 // file-type shape dominates the cross-entry uniqueness
4424 // diagnostic (a
4425 // `("servicos/x.yaml" "servicos/x.yaml")` shape on
4426 // `:servicos` surfaces
4427 // `CodePathNonComputeUnitYamlExtension` on the first
4428 // entry rather than `CodePathDuplicate` on the pair —
4429 // peer with the 64772a9 `:bibliotecas`
4430 // `("lib/x.txt" "lib/x.txt")` ordering).
4431 match file_type {
4432 CodePathFileType::None => {}
4433 CodePathFileType::LispSource => {
4434 if !is_lisp_extension(path) {
4435 return Err(ManifestError::CodePathNonLispExtension {
4436 slot,
4437 path: path.to_path_buf(),
4438 });
4439 }
4440 }
4441 CodePathFileType::ComputeUnitYaml => {
4442 if !is_computeunit_yaml_extension(path) {
4443 return Err(ManifestError::CodePathNonComputeUnitYamlExtension {
4444 slot,
4445 path: path.to_path_buf(),
4446 });
4447 }
4448 }
4449 }
4450 crate::render::insert_first_seen(&mut seen, entry.as_str(), || {
4451 ManifestError::CodePathDuplicate {
4452 slot,
4453 path: path.to_path_buf(),
4454 }
4455 })?;
4456 }
4457 }
4458 Ok(())
4459 }
4460
4461 /// Reject `:etiquetas` lists with an empty entry or with two entries
4462 /// agreeing on the same string. `:etiquetas` is the universal
4463 /// registry-search-tag axis on [`Caixa`] (every kind carries the
4464 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4465 /// `Chart.yaml` `keywords:` array on every Servico (caixa-helm's
4466 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:236` folds it through
4467 /// a [`std::collections::BTreeSet`] alongside the four substrate-
4468 /// fixed tags `lareira` / `wasm` / `tatara-lisp` / `caixa-servico`).
4469 /// Two authoring footguns silently passed validate without this gate:
4470 ///
4471 /// - Empty entry (`(:etiquetas (""))` — the canonical paste-from-
4472 /// blank-doc footgun) rendered as `keywords: ["", "caixa-servico",
4473 /// "lareira", "tatara-lisp", "wasm"]` in `Chart.yaml`. Helm's
4474 /// `chart.metadata.keywords` admits the value without a strict
4475 /// parser-side gate, but the empty keyword has no operational
4476 /// meaning — it indexes nothing in the future caixa-registry
4477 /// search axis and clutters the rendered chart with a no-op tag.
4478 /// - Duplicate entries (`(:etiquetas ("demo" "demo"))` — the
4479 /// copy-paste-the-wrong-tag footgun) silently passed validate
4480 /// and were silently dedup'd by caixa-helm's `BTreeSet` collect
4481 /// at chart render — a "second wins / one silently disappears"
4482 /// shape divergent from every peer typed-graph set gate
4483 /// ([`crate::AplicacaoError::MembroDuplicate`] on `:membros`,
4484 /// [`crate::AplicacaoError::PlacementClusterDuplicate`] on
4485 /// `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4486 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4487 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4488 /// `:deps` / `:deps-dev` per 359fba5, [`crate::UpgradeError::DuplicateFrom`]
4489 /// on `:upgrade-from`, the per-instruction-class singularity
4490 /// gates [`crate::UpgradeError::DuplicateLoadModule`] /
4491 /// [`crate::UpgradeError::DuplicateStateChange`] /
4492 /// [`crate::UpgradeError::DuplicateCleanup`]). The typed-graph
4493 /// discipline is uniform: every Vec-shaped author-supplied list
4494 /// past validate is set-not-multiset, by construction.
4495 ///
4496 /// Past the empty arm the gate enforces the chart-keyword shape
4497 /// predicate via [`crate::render::is_chart_keyword_shape`]: Cargo's
4498 /// crates.io `[package] keywords` grammar — 1..=20 bytes, starts
4499 /// with an ASCII letter, ASCII alphanumeric / `_` / `-`
4500 /// continuation. Closes the canonical paste-from-doc footguns the
4501 /// bare empty + duplicate arms left open: paste-from-aligned-doc
4502 /// whitespace (`" mesh"`, `"mesh "`), paste-from-multiline-doc
4503 /// newline (`"mesh\nhttp"` — the author pasted a multi-tag block
4504 /// into one entry instead of splitting), paste-from-Windows-CRLF-doc
4505 /// carriage return, CSV-list-separator confusion (`"mesh,http,grpc"`
4506 /// — the author meant three separate list entries), path-separator
4507 /// confusion (`"caixa/servico"`), namespace-suffix (`"http.1"`),
4508 /// leading-digit (`"1foo"`), kebab-leak (`"-foo"`), snake-leak
4509 /// (`"_foo"`), non-ASCII (`"café"`), and paste-from-binary-blob
4510 /// control bytes that would silently land as malformed search tags
4511 /// in the rendered Chart.yaml `keywords:` array and break the
4512 /// Artifact Hub keyword index lookup far from the source caixa.lisp.
4513 /// Mirrors the [`Self::validate_autores`] shape-predicate cascade
4514 /// established on the sibling universal-axis `Vec<String>` surface
4515 /// — the second universal-axis Vec<String> surface to land the
4516 /// empty-first-then-shape-then-duplicate per-entry cascade.
4517 ///
4518 /// Same empty-first cascade discipline every peer per-axis gate
4519 /// uses: the per-entry empty arm fires before the per-entry shape
4520 /// arm fires before the cross-entry duplicate arm, so an
4521 /// `("" "mesh" "mesh")` authoring shape surfaces the narrower
4522 /// [`ManifestError::EtiquetaEmpty`] (the structural "this entry
4523 /// has no value" defect) before either the shape or the duplicate
4524 /// diagnostic. Walks the list in declaration order so the
4525 /// first-collision diagnostic surfaces the lexicographically-
4526 /// earliest offending position, peer with every other duplicate
4527 /// gate on this surface.
4528 ///
4529 /// Universal-axis (every kind carries `:etiquetas`), so wired at the
4530 /// caixa-build gate alongside the peer universal gates
4531 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4532 /// [`Self::validate_deps`] / [`Self::validate_code_paths`] — before
4533 /// the kind-coherence gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]
4534 /// / [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4535 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4536 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4537 /// slot sets. The future caixa-registry search axis can reach for
4538 /// `caixa.etiquetas` knowing every entry is a non-empty distinct
4539 /// chart-keyword-shaped string without re-deriving the precondition.
4540 pub fn validate_etiquetas(&self) -> Result<(), ManifestError> {
4541 let mut seen = std::collections::HashSet::new();
4542 for etiqueta in self.etiquetas() {
4543 if etiqueta.is_empty() {
4544 return Err(ManifestError::EtiquetaEmpty);
4545 }
4546 crate::render::is_chart_keyword_shape(etiqueta).map_err(|reason| {
4547 ManifestError::EtiquetaInvalid {
4548 etiqueta: etiqueta.clone(),
4549 reason,
4550 }
4551 })?;
4552 crate::render::insert_first_seen(&mut seen, etiqueta.as_str(), || {
4553 ManifestError::EtiquetaDuplicate {
4554 etiqueta: etiqueta.clone(),
4555 }
4556 })?;
4557 }
4558 Ok(())
4559 }
4560
4561 /// Reject `:autores` lists with an empty entry or with two entries
4562 /// agreeing on the same string. `:autores` is the universal
4563 /// maintainer-axis on [`Caixa`] (every kind carries the
4564 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4565 /// `Chart.yaml` `maintainers:` array on every Servico (caixa-helm's
4566 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:251` maps each entry
4567 /// to a `Maintainer { name, email: None }` without dedup). Two
4568 /// authoring footguns silently passed validate without this gate:
4569 ///
4570 /// - Empty entry (`(:autores (""))` — the canonical paste-from-
4571 /// blank-doc footgun) rendered as
4572 /// `maintainers: [{name: "", email: null}]` in `Chart.yaml`. The
4573 /// empty maintainer name has no operational meaning — it
4574 /// identifies no one in the substrate's authorship index and
4575 /// clutters the rendered chart with a no-op maintainer.
4576 /// - Duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
4577 /// the copy-paste-the-wrong-author footgun) silently passed
4578 /// validate and rendered as two identical maintainer entries.
4579 /// Unlike the [`Self::validate_etiquetas`] peer (caixa-helm's
4580 /// `BTreeSet`-collect on `:etiquetas` silently dedups the
4581 /// rendered `keywords:` array at chart-render time), the
4582 /// `maintainers:` rendering has *no* dedup — duplicate `:autores`
4583 /// entries stack verbatim in the chart, divergent from every
4584 /// peer typed-graph set gate ([`crate::AplicacaoError::MembroDuplicate`]
4585 /// on `:membros`, [`crate::AplicacaoError::PlacementClusterDuplicate`]
4586 /// on `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4587 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4588 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4589 /// `:deps` / `:deps-dev`, [`crate::UpgradeError::DuplicateFrom`]
4590 /// on `:upgrade-from`, [`ManifestError::EtiquetaDuplicate`] on
4591 /// `:etiquetas`).
4592 ///
4593 /// Past the empty arm the gate enforces the chart-maintainer-name
4594 /// shape predicate via [`crate::render::is_chart_maintainer_name_shape`]:
4595 /// the structural single-line printable-UTF-8 floor every realistic
4596 /// Helm chart maintainer name carries — 1..=128 bytes, no leading
4597 /// or trailing whitespace, no ASCII control characters anywhere,
4598 /// Unicode bytes accepted. Closes the canonical paste-from-doc
4599 /// footguns the bare empty + duplicate arms left open:
4600 /// paste-from-aligned-doc whitespace (`" pleme-io"`, `"pleme-io "`),
4601 /// paste-from-multiline-doc newline (`"alice\nbob"` — the author
4602 /// pasted a multi-line block of author records into one `:autores`
4603 /// entry instead of splitting into one entry per author),
4604 /// paste-from-Windows-CRLF-doc carriage return, tab-from-aligned-doc,
4605 /// and the paste-from-binary-blob control bytes that would silently
4606 /// land as YAML-illegal byte sequences in the rendered Chart.yaml
4607 /// `maintainers:` array. Mirrors the shape-predicate cascade
4608 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4609 /// [`Self::validate_edicao`] / [`Self::validate_repositorio`]
4610 /// establish past their own empty arms on the sibling universal-axis
4611 /// `Option<String>` surfaces — the first universal-axis Vec<String>
4612 /// surface to land the empty-first-then-shape-then-duplicate per-entry
4613 /// cascade.
4614 ///
4615 /// Same empty-first cascade discipline every peer per-axis gate
4616 /// uses: the per-entry empty arm fires before the per-entry shape
4617 /// arm before the cross-entry duplicate arm. Walks the list in
4618 /// declaration order so the first-collision diagnostic surfaces the
4619 /// lexicographically-earliest offending position, peer with every
4620 /// other duplicate gate on this surface.
4621 ///
4622 /// Universal-axis (every kind carries `:autores`), so wired at the
4623 /// caixa-build gate alongside the peer universal gates
4624 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4625 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4626 /// [`Self::validate_code_paths`] — before the kind-coherence gates
4627 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4628 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4629 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4630 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4631 /// slot sets.
4632 pub fn validate_autores(&self) -> Result<(), ManifestError> {
4633 let mut seen = std::collections::HashSet::new();
4634 for autor in self.autores() {
4635 if autor.is_empty() {
4636 return Err(ManifestError::AutorEmpty);
4637 }
4638 crate::render::is_chart_maintainer_name_shape(autor).map_err(|reason| {
4639 ManifestError::AutorInvalid {
4640 autor: autor.clone(),
4641 reason,
4642 }
4643 })?;
4644 crate::render::insert_first_seen(&mut seen, autor.as_str(), || {
4645 ManifestError::AutorDuplicate {
4646 autor: autor.clone(),
4647 }
4648 })?;
4649 }
4650 Ok(())
4651 }
4652
4653 /// Reject `:repositorio` values whose shape the shared
4654 /// [`crate::render::is_git_repo_url`] predicate refuses. The flat
4655 /// `repositorio: Option<String>` slot on [`Caixa`] is the
4656 /// universal git-shaped homepage axis every kind carries — the
4657 /// substrate routes the same string through two load-bearing
4658 /// consumers:
4659 ///
4660 /// - [`caixa-helm`] folds it verbatim into the rendered
4661 /// `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
4662 /// (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
4663 /// the chart `README.md` `repo = …` interpolation
4664 /// (`caixa-helm/src/lib.rs:359`).
4665 /// - [`caixa-flux`] folds it verbatim into the standalone
4666 /// `ClusterBundleOpts::for_caixa` `git_url:` field
4667 /// (`caixa-flux/src/lib.rs:293`), which becomes the `FluxCD`
4668 /// `GitRepository.spec.url` the cluster's source-controller
4669 /// polls — the load-bearing deploy-time axis.
4670 ///
4671 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4672 /// substitute a placeholder when the slot is absent (`None` → the
4673 /// fallback fires); a `Some("")` *skips the fallback* and silently
4674 /// passes the empty string through to `Chart.yaml home: ""` /
4675 /// `GitRepository url: ""` — Helm's chart lint and `FluxCD`'s source
4676 /// controller both reject the empty URL far from the source
4677 /// `caixa.lisp`, with no field naming the offending `:repositorio`.
4678 /// Similarly a malformed `:repositorio` (whitespace, control char,
4679 /// missing `:` separator, leading `-`) silently lands in the
4680 /// rendered artifacts and breaks at `git clone` / `helm template`
4681 /// / `flux reconcile` time.
4682 ///
4683 /// Thin wrapper around [`crate::render::is_git_repo_url`] — the
4684 /// same shared predicate the peer [`crate::DepSource::validate`]
4685 /// routes the `:fonte (:tipo git :repo …)` axis through. With this
4686 /// gate the two `git URL`-shaped surfaces on the typed Caixa
4687 /// (`:repositorio` here, `:deps :fonte :repo` peer) are
4688 /// structurally equivalent: every value past validate is
4689 /// guaranteed-acceptable by the predicate's union of constraints
4690 /// (non-empty, length-bounded, no leading `-`, no whitespace, no
4691 /// control chars, ASCII only, no leading `:`, contains a `:`
4692 /// separator). The predicate accepts every documented authoring
4693 /// shape — `github:org/repo` shorthand, `https://host/path`,
4694 /// `ssh://[user@]host/path`, `git://host/path`, `git@host:path`
4695 /// scp-style SSH, `file:///path` — and refuses the canonical
4696 /// paste-from-blank-doc / paste-from-multiline-doc / CLI-arg-
4697 /// injection footguns at validate time. Maps the predicate's
4698 /// `String` reason verbatim into the
4699 /// [`ManifestError::RepositorioInvalid`] variant, carrying the
4700 /// offending value + parser-shaped reason so the diagnostic is
4701 /// self-locating (the author can grep their `caixa.lisp` for
4702 /// `:repositorio "<value>"` and fix it in one edit).
4703 ///
4704 /// `None` (the canonical "omit the slot to express no published
4705 /// homepage" shape) is accepted trivially — the gate is a no-op
4706 /// when the author didn't declare a value. `Some("")` is gated by
4707 /// the narrower [`ManifestError::RepositorioEmpty`] arm before the
4708 /// shape predicate is consulted, mirroring the empty-first cascade
4709 /// every peer per-axis identity gate uses
4710 /// ([`ManifestError::NomeEmpty`] → [`ManifestError::NomeInvalid`],
4711 /// [`ManifestError::VersaoEmpty`] → [`ManifestError::VersaoInvalid`],
4712 /// [`crate::DepError::FonteRepoEmpty`] →
4713 /// [`crate::DepError::FonteRepoInvalid`]).
4714 ///
4715 /// Universal-axis (every kind carries `:repositorio`), so wired at
4716 /// the caixa-build gate alongside the peer universal gates
4717 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4718 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4719 /// [`Self::validate_autores`] / [`Self::validate_code_paths`] —
4720 /// before the kind-coherence gates
4721 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4722 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4723 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4724 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4725 /// specific slot sets.
4726 pub fn validate_repositorio(&self) -> Result<(), ManifestError> {
4727 let Some(s) = self.repositorio() else {
4728 return Ok(());
4729 };
4730 if s.is_empty() {
4731 return Err(ManifestError::RepositorioEmpty);
4732 }
4733 is_git_repo_url(s).map_err(|reason| ManifestError::RepositorioInvalid {
4734 repositorio: s.to_string(),
4735 reason,
4736 })
4737 }
4738
4739 /// Reject `:descricao` values that are the empty string. The flat
4740 /// `descricao: Option<String>` slot on [`Caixa`] is the universal
4741 /// free-form-prose homepage axis every kind carries — the
4742 /// substrate routes the same string through two load-bearing
4743 /// consumers in the [`caixa-helm`] renderer:
4744 ///
4745 /// - `build_chart_yaml` folds it verbatim into the rendered
4746 /// `lareira-<nome>` Helm chart's `Chart.yaml` `description:`
4747 /// field (`caixa-helm/src/lib.rs:232-235`).
4748 /// - `build_readme` folds it verbatim into the rendered chart
4749 /// `README.md` header (`caixa-helm/src/lib.rs:333-336`).
4750 ///
4751 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4752 /// substitute a `caixa.nome`-derived placeholder when the slot is
4753 /// absent (`None` → the fallback fires); a `Some("")` *skips the
4754 /// fallback* and silently passes the empty string through to
4755 /// `Chart.yaml description: ""` / a blank chart `README.md`
4756 /// header. Helm's chart spec requires a non-empty `description:`
4757 /// field on `apiVersion: v2` charts (`helm lint` surfaces it as
4758 /// `WARNING [chart.metadata.description]: description is required`),
4759 /// so the empty `Some("")` silently lands in the rendered
4760 /// artifacts and breaks at `helm lint` / `helm install` time far
4761 /// from the source `caixa.lisp`, with no field naming the
4762 /// offending `:descricao`.
4763 ///
4764 /// `None` (the canonical "omit the slot to defer to the renderer's
4765 /// `caixa.nome`-derived fallback" shape) is accepted trivially —
4766 /// the gate is a no-op when the author didn't declare a value.
4767 /// `Some("")` is gated by the narrower
4768 /// [`ManifestError::DescricaoEmpty`] arm, mirroring the empty-arm
4769 /// shape every peer per-axis empty gate uses
4770 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4771 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4772 /// [`ManifestError::RepositorioEmpty`]).
4773 ///
4774 /// Universal-axis (every kind carries `:descricao`), so wired at
4775 /// the caixa-build gate alongside the peer universal gates
4776 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4777 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4778 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4779 /// [`Self::validate_code_paths`] — before the kind-coherence
4780 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4781 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4782 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4783 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4784 /// specific slot sets.
4785 ///
4786 /// Past the empty arm the gate enforces the chart-description
4787 /// shape predicate via [`crate::render::is_chart_description_shape`]:
4788 /// the structural single-line UTF-8 floor every realistic chart
4789 /// description in the wild matches — 1..=512 bytes, no leading
4790 /// or trailing whitespace, no ASCII control characters anywhere
4791 /// (`0x00..=0x1F` plus `0x7F` DEL — banning tab, newline,
4792 /// carriage return, and every other control byte), Unicode
4793 /// continuation bytes accepted (the canonical fixtures carry
4794 /// `→` and `—`). Closes the canonical paste-from-doc footguns
4795 /// the bare empty-arm gate left open: paste-from-aligned-doc
4796 /// leading / trailing whitespace (`" Checkout flow."`,
4797 /// `"Checkout flow. "`), paste-from-multiline-doc newline
4798 /// (`"Checkout\nflow."`), paste-from-Windows-CRLF-doc CR
4799 /// (`"Checkout\rflow."`), tab-from-aligned-doc
4800 /// (`"Checkout\tflow."`), and paste-from-binary-blob NUL / BEL /
4801 /// ESC / DEL bytes. Mirrors the shape-predicate cascade
4802 /// [`Self::validate_repositorio`] / [`Self::validate_licenca`] /
4803 /// [`Self::validate_edicao`] establish past their own empty arms
4804 /// on the sibling universal-axis `Option<String>` Caixa-level
4805 /// value-shape surfaces.
4806 ///
4807 /// The empty-first cascade discipline mirrors every peer per-axis
4808 /// identity gate: [`ManifestError::DescricaoEmpty`] runs before
4809 /// [`ManifestError::DescricaoInvalid`], so the narrower empty
4810 /// diagnostic surfaces on `Some("")` rather than the broader
4811 /// shape-predicate diagnostic — peer with how
4812 /// [`ManifestError::LicencaEmpty`] runs before
4813 /// [`ManifestError::LicencaInvalid`],
4814 /// [`ManifestError::EdicaoEmpty`] runs before
4815 /// [`ManifestError::EdicaoInvalid`],
4816 /// [`ManifestError::RepositorioEmpty`] runs before
4817 /// [`ManifestError::RepositorioInvalid`].
4818 pub fn validate_descricao(&self) -> Result<(), ManifestError> {
4819 let Some(s) = self.descricao() else {
4820 return Ok(());
4821 };
4822 if s.is_empty() {
4823 return Err(ManifestError::DescricaoEmpty);
4824 }
4825 crate::render::is_chart_description_shape(s).map_err(|reason| {
4826 ManifestError::DescricaoInvalid {
4827 descricao: s.to_string(),
4828 reason,
4829 }
4830 })?;
4831 Ok(())
4832 }
4833
4834 /// Reject `:licenca` values that are the empty string. The flat
4835 /// `licenca: Option<String>` slot on [`Caixa`] is the universal
4836 /// SPDX-shaped license-expression axis every kind carries — the
4837 /// substrate routes the same string through the [`caixa-helm`]
4838 /// renderer's `build_readme` which folds it verbatim into the
4839 /// rendered `lareira-<nome>` Helm chart's `README.md` `## License`
4840 /// section (`caixa-helm/src/lib.rs:361`) via
4841 /// `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
4842 /// fallback only fires on `None`; a `Some("")` *skips the
4843 /// fallback* and silently passes the empty string through to a
4844 /// chart `README.md` whose `License` section renders as the bare
4845 /// trailing period (`.\n`) — peer footgun with the
4846 /// `Some("")`-skips-`unwrap_or_else` shape the
4847 /// [`Self::validate_descricao`] and [`Self::validate_repositorio`]
4848 /// gates close on the sibling free-form-prose and git-URL axes.
4849 ///
4850 /// `None` (the canonical "omit the slot to defer to the
4851 /// renderer's `MIT` fallback" shape every existing fixture
4852 /// carries) is accepted trivially — the gate is a no-op when the
4853 /// author didn't declare a value. `Some("")` is gated by the
4854 /// narrower [`ManifestError::LicencaEmpty`] arm, mirroring the
4855 /// empty-arm shape every peer per-axis empty gate uses
4856 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4857 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4858 /// [`ManifestError::RepositorioEmpty`],
4859 /// [`ManifestError::DescricaoEmpty`]).
4860 ///
4861 /// Universal-axis (every kind carries `:licenca`), so wired at
4862 /// the caixa-build gate alongside the peer universal gates
4863 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4864 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4865 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4866 /// [`Self::validate_descricao`] / [`Self::validate_code_paths`]
4867 /// — before the kind-coherence gates
4868 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4869 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4870 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4871 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4872 /// specific slot sets.
4873 ///
4874 /// Past the empty arm the gate enforces the SPDX-expression shape
4875 /// predicate via [`crate::render::is_spdx_expression_shape`]: the
4876 /// structural alphabet floor every realistic SPDX expression in
4877 /// the wild uses — ASCII alphanumeric plus `.`, `-`, `+`, `(`,
4878 /// `)`, `:` (the `DocumentRef-…:LicenseRef-…` separator), and a
4879 /// single ASCII space (token separator). Closes the canonical
4880 /// paste-from-doc footguns the bare empty-arm gate left open:
4881 /// paste-from-doc whitespace (`"MIT "`, `" MIT"`), paste-from-
4882 /// multiline-doc CRLF (`"MIT\n"`), tab-from-aligned-doc
4883 /// (`"MIT\tOR Apache-2.0"`), non-ASCII smart-quote paste,
4884 /// underscore-instead-of-hyphen typo (`"Apache_2.0"`),
4885 /// comma-instead-of-`OR`-keyword colloquial idiom (`"MIT,
4886 /// Apache-2.0"`), slash-dual-license colloquial idiom (`"MIT/
4887 /// Apache-2.0"`), and semicolon-list-separator confusion
4888 /// (`"MIT; Apache-2.0"`). Mirrors the shape-predicate cascade
4889 /// [`Self::validate_repositorio`] / [`Self::validate_edicao`]
4890 /// establish past their own empty arms.
4891 ///
4892 /// The empty-first cascade discipline mirrors every peer per-axis
4893 /// identity gate: [`ManifestError::LicencaEmpty`] runs before
4894 /// [`ManifestError::LicencaInvalid`], so the narrower empty
4895 /// diagnostic surfaces on `Some("")` rather than the broader
4896 /// shape-predicate diagnostic — peer with how
4897 /// [`ManifestError::EdicaoEmpty`] runs before
4898 /// [`ManifestError::EdicaoInvalid`],
4899 /// [`ManifestError::RepositorioEmpty`] runs before
4900 /// [`ManifestError::RepositorioInvalid`].
4901 ///
4902 /// A future tightening on this axis can extend the alphabet
4903 /// floor into a full SPDX expression parser + license-id
4904 /// allowlist (rejecting alphabet-valid values that don't name a
4905 /// real SPDX license identifier — e.g., `"NotAReal"` is
4906 /// alphabet-valid but no `NotAReal` license-id exists). That
4907 /// parser only becomes meaningful past a real SPDX-spec
4908 /// dependency; this gate establishes the structural floor by
4909 /// refusing every non-SPDX-alphabet value at validate time.
4910 pub fn validate_licenca(&self) -> Result<(), ManifestError> {
4911 let Some(s) = self.licenca() else {
4912 return Ok(());
4913 };
4914 if s.is_empty() {
4915 return Err(ManifestError::LicencaEmpty);
4916 }
4917 crate::render::is_spdx_expression_shape(s).map_err(|reason| {
4918 ManifestError::LicencaInvalid {
4919 licenca: s.to_string(),
4920 reason,
4921 }
4922 })?;
4923 Ok(())
4924 }
4925
4926 /// Reject `:edicao` values that are the empty string. The flat
4927 /// `edicao: Option<String>` slot on [`Caixa`] is the universal
4928 /// language-edition axis every kind carries — it determines the
4929 /// tatara-lisp macro surface + compatibility flags the substrate
4930 /// applies when building a caixa, and lands verbatim in the
4931 /// `Caixa::template` author-time scaffold (the canonical
4932 /// `:edicao "2026"` line every `feira init` emits via
4933 /// [`Caixa::template`] at `caixa-core/src/manifest.rs:1193`) and
4934 /// in every renderer-side fixture (`caixa-helm/src/lib.rs:375`,
4935 /// `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
4936 /// `caixa-core/src/render.rs:2510`) via
4937 /// `edicao: Some("2026".into())`.
4938 ///
4939 /// `None` (the canonical "omit the slot to defer to the
4940 /// substrate's default edition" shape every existing
4941 /// [`caixa-resolver`] integration test fixture carries via
4942 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4943 /// is accepted trivially — the gate is a no-op when the author
4944 /// didn't declare a value. `Some("")` is gated by the narrower
4945 /// [`ManifestError::EdicaoEmpty`] arm, mirroring the empty-arm
4946 /// shape every peer per-axis empty gate uses
4947 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4948 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4949 /// [`ManifestError::RepositorioEmpty`],
4950 /// [`ManifestError::DescricaoEmpty`], [`ManifestError::LicencaEmpty`]).
4951 ///
4952 /// Universal-axis (every kind carries `:edicao`), so wired at
4953 /// the caixa-build gate alongside the peer universal gates
4954 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4955 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4956 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4957 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4958 /// [`Self::validate_code_paths`] — before the kind-coherence
4959 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4960 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4961 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4962 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4963 /// specific slot sets.
4964 ///
4965 /// Past the empty arm the gate enforces the canonical year-shape
4966 /// predicate: every documented tatara-lisp edition is a 4-digit
4967 /// ASCII decimal year (`"2026"` is the only edition currently
4968 /// minted; future-introduced siblings will follow the same
4969 /// shape, peer with Cargo's `[package] edition` grammar which
4970 /// every value Cargo has ever accepted matches — `"2015"`,
4971 /// `"2018"`, `"2021"`, `"2024"`). Any value that's not exactly
4972 /// 4 ASCII decimal bytes is rejected with the narrower
4973 /// [`ManifestError::EdicaoInvalid`] arm, mirroring the
4974 /// shape-predicate cascade [`Self::validate_repositorio`]
4975 /// establishes past its own empty arm
4976 /// ([`ManifestError::RepositorioEmpty`] →
4977 /// [`ManifestError::RepositorioInvalid`]). Closes the canonical
4978 /// paste-from-doc footguns the bare empty-arm gate left open:
4979 ///
4980 /// - leading / trailing whitespace from a paste-from-doc
4981 /// (`"2026 "`, `" 2026"`)
4982 /// - control characters / CRLF from a paste-from-multiline-doc
4983 /// (`"2026\n"`)
4984 /// - non-ASCII look-alikes from a fullwidth keyboard
4985 /// (`"2026"`) which would silently land as a non-ASCII
4986 /// string in the rendered caixa.lisp
4987 /// - free-form non-year values (`"x"`, `"latest"`,
4988 /// `"nightly"`) that have no operational meaning on the
4989 /// substrate's build-time edition selector
4990 /// - leading non-digit prefixes (`"v2026"`, `"e2026"`,
4991 /// `"r2026"`) — common version-tag idioms that don't apply
4992 /// to the year-shaped edition axis
4993 /// - decimal-shaped values (`"2026.1"`, `"2026.0"`) — every
4994 /// edition is a year, not a fractional version
4995 /// - wrong-length numeric values (`"26"`, `"202"`, `"20260"`,
4996 /// `"00026"`) that don't name a year
4997 ///
4998 /// `None` (the canonical "omit the slot to defer to the
4999 /// substrate's default edition" shape every existing
5000 /// [`caixa-resolver`] integration test fixture carries via
5001 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
5002 /// is accepted trivially — the gate is a no-op when the author
5003 /// didn't declare a value. The empty-first cascade discipline
5004 /// mirrors every peer per-axis identity gate:
5005 /// [`ManifestError::EdicaoEmpty`] runs before
5006 /// [`ManifestError::EdicaoInvalid`], so the narrower empty
5007 /// diagnostic surfaces on `Some("")` rather than the broader
5008 /// shape-predicate diagnostic — peer with how
5009 /// [`ManifestError::NomeEmpty`] runs before
5010 /// [`ManifestError::NomeInvalid`],
5011 /// [`ManifestError::VersaoEmpty`] runs before
5012 /// [`ManifestError::VersaoInvalid`],
5013 /// [`ManifestError::RepositorioEmpty`] runs before
5014 /// [`ManifestError::RepositorioInvalid`].
5015 ///
5016 /// A future tightening on this axis can extend the shape
5017 /// predicate into a known-edition allowlist (rejecting
5018 /// year-shaped values that don't name a tatara-lisp edition
5019 /// the substrate actually understands — e.g., `"1999"` is
5020 /// year-shaped but no `1999` edition exists). That allowlist
5021 /// only becomes meaningful past the introduction of a sibling
5022 /// edition to `"2026"`; this gate establishes the structural
5023 /// floor by refusing every non-year-shaped value at validate
5024 /// time.
5025 pub fn validate_edicao(&self) -> Result<(), ManifestError> {
5026 let Some(s) = self.edicao() else {
5027 return Ok(());
5028 };
5029 if s.is_empty() {
5030 return Err(ManifestError::EdicaoEmpty);
5031 }
5032 if s.len() != 4 || !s.bytes().all(|b| b.is_ascii_digit()) {
5033 return Err(ManifestError::EdicaoInvalid {
5034 edicao: s.to_string(),
5035 reason: "must be a 4-digit ASCII decimal year (canonical \"2026\")".to_string(),
5036 });
5037 }
5038 Ok(())
5039 }
5040
5041 /// Compose the supervisor-related flat slots into a single
5042 /// [`SupervisorSpec`] for validation. Returns `None` when the
5043 /// caixa isn't a `:kind Supervisor`.
5044 ///
5045 /// The flat representation in [`Caixa`] keeps tatara-lisp authoring
5046 /// simple (one form, no nested `:supervisor (…)` block); this view
5047 /// is the "typed shape" the operator + supervisor reconciler
5048 /// consume.
5049 #[must_use]
5050 pub fn supervisor_view(&self) -> Option<SupervisorSpec> {
5051 if !self.kind().is_supervisor() {
5052 return None;
5053 }
5054 // Fold through the shared `supervisor::duration_codec::parse`
5055 // — the same parser the serde-routed `with = "duration_codec"`
5056 // on `SupervisorSpec::restart_window`, the `:politicas
5057 // :timeout` codec, and the `:politicas :circuit-breaker
5058 // :window` codec all consume. The prior inline f64-shaped
5059 // duplicate (`parse_window_inline`) admitted every magnitude
5060 // the integer-magnitude gate (1c55a2a) rejects on the three
5061 // serde-routed siblings — `"1.5s"`, `"1.0s"`, `"0.5m"`,
5062 // `"+30s"`, `"-30s"` — and silently dropped malformed input as
5063 // `None` (i.e. "no reset"), divergent from the shared codec's
5064 // integer-magnitude discipline by construction. The fold
5065 // closes the divergence: every value the typed
5066 // `SupervisorSpec` carries past `supervisor_view` is in the
5067 // shared codec's accepted set. The `.ok()` here preserves the
5068 // existing soft-swallow shape on this view-construction path;
5069 // the new [`Caixa::validate_restart_window`] (sibling of
5070 // [`Self::validate_nome`] / [`Self::validate_versao`]) names
5071 // the offending raw string at build time so authoring tools
5072 // (`feira lint`, the future layout-side wire-up) surface a
5073 // self-locating diagnostic instead of a silently dropped
5074 // window.
5075 let restart_window = self
5076 .restart_window()
5077 .and_then(|s| crate::supervisor::duration_codec::parse(s).ok());
5078 Some(SupervisorSpec {
5079 estrategia: self.estrategia().unwrap_or_default(),
5080 max_restarts: self.max_restarts().unwrap_or(5),
5081 restart_window,
5082 children: self.children().to_vec(),
5083 })
5084 }
5085
5086 /// A minimal starter manifest emitted by `feira init`.
5087 #[must_use]
5088 pub fn template(nome: &str) -> String {
5089 format!(
5090 "(defcaixa\n \
5091 :nome {nome:?}\n \
5092 :versao \"0.1.0\"\n \
5093 :kind Biblioteca\n \
5094 :edicao \"2026\"\n \
5095 :descricao \"FIXME — describe this caixa\"\n \
5096 :autores ()\n \
5097 :etiquetas ()\n \
5098 :deps ()\n \
5099 :deps-dev ()\n \
5100 :bibliotecas (\"lib/{nome}.lisp\"))\n"
5101 )
5102 }
5103
5104 /// Serialize to a canonical `caixa.lisp` source — suitable for writing
5105 /// back after mutation (e.g. `feira add`).
5106 ///
5107 /// Goes through serde JSON → canonical Sexp → per-field pretty print.
5108 /// The derive-macro `compile_from_sexp` path is the inverse, so any
5109 /// `Caixa` round-trips through `to_lisp` + `from_lisp`.
5110 #[must_use]
5111 pub fn to_lisp(&self) -> String {
5112 let json = serde_json::to_value(self).expect("Caixa serialize");
5113 let sexp = tatara_lisp::domain::json_to_sexp(&json);
5114 let tatara_lisp::Sexp::List(items) = sexp else {
5115 return format!("(defcaixa {sexp})\n");
5116 };
5117 let mut out = String::from("(defcaixa");
5118 let mut i = 0;
5119 while i + 1 < items.len() {
5120 out.push_str("\n ");
5121 out.push_str(&items[i].to_string());
5122 out.push(' ');
5123 out.push_str(&items[i + 1].to_string());
5124 i += 2;
5125 }
5126 out.push_str(")\n");
5127 out
5128 }
5129}
5130
5131/// Errors raised by top-level [`Caixa`] validators that don't fit
5132/// the per-axis [`DepError`] / [`crate::AplicacaoError`] /
5133/// [`crate::SupervisorError`] / [`crate::LayoutError`] families —
5134/// the Caixa's own identity axes (`:nome`, `:versao`) that flow
5135/// through every substrate-side artifact's `metadata.name` /
5136/// version derivation.
5137///
5138/// A future top-level sum (the M4 `CaixaError` the [`DepError`]
5139/// doc-comment anticipates) can hold one of each per-axis error
5140/// family without reshaping individual diagnostics; this enum is
5141/// the first such per-Caixa-identity family.
5142#[derive(Debug, Error, PartialEq, Eq)]
5143pub enum ManifestError {
5144 #[error(
5145 ":nome is empty (every caixa must name itself; the value flows \
5146 into every K8s artifact's `metadata.name` derivation and into \
5147 the default `lib/<nome>.lisp` / `exe/<nome>` layout paths)"
5148 )]
5149 NomeEmpty,
5150 #[error(
5151 ":nome {nome:?} is not a valid DNS-1123 label: {reason} (the K8s \
5152 apiserver enforces this rule on every `metadata.name` the \
5153 caixa's substrate-side renderers derive from `:nome` — the \
5154 `lareira-<nome>` Helm chart name, the programs.yaml entry \
5155 name, the `LABEL_APLICACAO` label value, the `<aplicacao>-<de>-to-<para>` \
5156 CiliumNetworkPolicy name, the `<aplicacao>-<para>` HTTPRoute \
5157 name; use a lowercase alphanumeric + hyphen identifier like \
5158 `\"checkout\"` or `\"cart-v2\"`)"
5159 )]
5160 NomeInvalid { nome: String, reason: String },
5161 #[error(
5162 ":nome {nome:?} overflows the joint-length budget on the canonical \
5163 `lareira-<nome>` chart-name shape: {reason} (every per-Servico / \
5164 per-Aplicacao renderer the substrate carries — `caixa-helm`'s \
5165 `Chart.yaml::name`, `caixa-flux`'s `cluster_bundle` HelmRelease \
5166 `chart:` slot, `caixa-tatara`'s `release_name` + \
5167 `oci://<registry>/lareira-<nome>` chart ref — derives the same \
5168 joint name through the canonical `lareira_chart_name` helper, and \
5169 Helm's `Chart.yaml::name` admission rule + the K8s apiserver's \
5170 DNS-1123 label cap on every chart-name-derived `metadata.name` \
5171 reject any joint name exceeding 63 bytes; the narrower \
5172 `:nome` shape (`NomeInvalid`) gates the bare-`:nome` budget, this \
5173 arm gates the chart-name budget downstream renderers inherit)"
5174 )]
5175 NomeChartNameBudgetExceeded { nome: String, reason: String },
5176 #[error(
5177 ":versao is empty (every caixa must pin its own version; the value flows \
5178 into the `lareira-<nome>` Helm chart's `Chart.yaml` version + appVersion, \
5179 the `feira publish` `v<versao>` git tag, the OCI image's `:v<versao>` / \
5180 `:latest` tags, the lacre closure's `concrete_versao`, and the \
5181 `:upgrade-from :from` peers — use a SemVer-2 literal like `\"0.1.0\"`)"
5182 )]
5183 VersaoEmpty,
5184 #[error(
5185 ":versao {versao:?} is not a valid SemVer-2 version: {reason} (the substrate \
5186 consumes this string as `semver::Version` — three-part `MAJOR.MINOR.PATCH` \
5187 with optional `-prerelease` and `+build` — across every artifact derived \
5188 from `:versao`: the `lareira-<nome>` Helm chart's `Chart.yaml` version + \
5189 appVersion (Helm SemVer-2-strict), the `feira publish` `v<versao>` git tag, \
5190 the OCI image's `:v<versao>` tag, the lacre closure's `concrete_versao`, \
5191 and the `:upgrade-from :from` peers that match against this exact shape; \
5192 use a literal like `\"0.1.0\"`, `\"0.2.0-rc.1\"`, or `\"1.0.0+build.42\"` — \
5193 not a git-tag-shape like `\"v0.1.0\"`, a docker-tag-shape like `\"latest\"`, \
5194 a requirement-shape like `\"^0.1\"`, or a four-part `\"0.1.0.0\"`)"
5195 )]
5196 VersaoInvalid { versao: String, reason: String },
5197 #[error(
5198 ":restart-window {restart_window:?} is not a valid duration: {reason} (the \
5199 substrate consumes this string through the shared \
5200 `supervisor::duration_codec` — the same parser routed via `with = \
5201 \"duration_codec\"` onto the typed `SupervisorSpec::restart_window`, \
5202 `:politicas :timeout`, and `:politicas :circuit-breaker :window` slots; \
5203 the canonical authoring form is `<integer><unit>` where the unit is one \
5204 of `ms` / `s` / `m` / `h` and the magnitude has no decimal point and no \
5205 leading `+` / `-` sign — e.g. `\"60s\"`, `\"5m\"`, `\"1h\"`, `\"500ms\"`. \
5206 Without this gate a malformed `:restart-window` silently produced a \
5207 supervisor with `restart_window: None` (\"never reset\"), turning OTP's \
5208 `MaxIntensity / Period` invariant into a never-reset supervisor far from \
5209 the source `caixa.lisp`; the gate moves the diagnostic to the manifest \
5210 layer with the offending value named verbatim. Omit the slot entirely to \
5211 express \"no reset\"; carry a positive integer duration to express the \
5212 sliding window)"
5213 )]
5214 RestartWindowMalformed {
5215 restart_window: String,
5216 reason: String,
5217 },
5218 #[error(
5219 "{slot} entry is an empty path string — every {slot} entry must name \
5220 a file relative to the caixa root; omit the entry to omit the file \
5221 (the layout checker's `root.join(\"\")` resolves to the caixa root \
5222 itself, so an empty entry silently aliases the project root as a \
5223 declared {slot} file, then fails downstream at parse / existence \
5224 time with a diagnostic that names the root rather than the offending \
5225 entry)"
5226 )]
5227 CodePathEmpty { slot: &'static str },
5228 #[error(
5229 "{slot} entry {} is an absolute path — entries must be relative to \
5230 the caixa root, since `Path::join` replaces the base with an absolute \
5231 right-hand side and `root.join(\"/abs/...\")` resolves to \"/abs/...\" \
5232 outside the caixa root sandbox; rewrite the entry as a relative path \
5233 under the caixa root (e.g. `\"lib/<name>.lisp\"`, `\"exe/<name>\"`, \
5234 `\"servicos/<name>.computeunit.yaml\"`)",
5235 path.display()
5236 )]
5237 CodePathAbsolute { slot: &'static str, path: PathBuf },
5238 #[error(
5239 "{slot} entry {} contains a `..` component — entries must not traverse \
5240 above the caixa root (the layout's `starts_with(<dir>)` fence on \
5241 `:exe` / `:servicos` is component-aware, not canonical-path-aware, \
5242 so a mid-path `..` silently traverses the sandbox; `:bibliotecas` \
5243 has no such fence, so a leading `..` escapes unconditionally if the \
5244 resolved target happens to exist)",
5245 path.display()
5246 )]
5247 CodePathParentEscape { slot: &'static str, path: PathBuf },
5248 #[error(
5249 "{slot} entry {} does not terminate in the `.lisp` extension — every \
5250 `:bibliotecas` entry is a tatara-lisp source file the `feira build` \
5251 loop reads through `tatara_lisp::read` at parse time, so any other \
5252 extension (`.rs`, `.txt`, `.lisp.bak`) or no-extension shape is \
5253 structurally a parser error far from the source caixa.lisp, with \
5254 no field naming the offending `:bibliotecas` entry. Pin a relative \
5255 path under the caixa root whose terminating extension is \
5256 lowercase-`.lisp` (e.g. `\"lib/<name>.lisp\"`, \
5257 `\"lib/handlers.lisp\"`) — the same file-type contract the peer \
5258 `:behavior :on-*` (c97815a) and `:upgrade-from :state-change :script` \
5259 (33cc830) axes already carry through the same lifted \
5260 `is_lisp_extension` predicate",
5261 path.display()
5262 )]
5263 CodePathNonLispExtension { slot: &'static str, path: PathBuf },
5264 #[error(
5265 "{slot} entry {} does not terminate in the `.computeunit.yaml` \
5266 compound suffix — every `:servicos` entry is a typed `ComputeUnit` \
5267 CR YAML file the peer caixa-helm / caixa-flux renderers consume \
5268 through `serde_yaml::from_str` at chart / FluxCD bundle render \
5269 time, so any other extension (`.yaml`, `.yml`, `.json`, the \
5270 off-by-one-segment `.computeunit-yaml`, the editor-backup \
5271 `.computeunit.yaml.bak`) or no-extension shape is structurally a \
5272 YAML-parser error / `ComputeUnit` schema-mismatch far from the \
5273 source caixa.lisp, with no field naming the offending `:servicos` \
5274 entry. Pin a relative path under the caixa root whose terminating \
5275 compound suffix is lowercase-`.computeunit.yaml` (e.g. \
5276 `\"servicos/<name>.computeunit.yaml\"`, \
5277 `\"servicos/hello-rio.computeunit.yaml\"`) — the same file-type \
5278 contract the sibling `:bibliotecas` axis (64772a9) already carries \
5279 on the tatara-lisp-source axis through the peer lifted \
5280 `is_lisp_extension` predicate, here on the compound-suffix axis \
5281 `Path::extension` can't express on its own through the lifted \
5282 `is_computeunit_yaml_extension` predicate",
5283 path.display()
5284 )]
5285 CodePathNonComputeUnitYamlExtension { slot: &'static str, path: PathBuf },
5286 #[error(
5287 "{slot} entry {} appears more than once (the code-path list is \
5288 a set, not a multiset; every peer Vec-shaped author-supplied \
5289 list past validate is set-not-multiset — `:membros :caixa`, \
5290 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5291 `:children :caixa`, `:deps` / `:deps-dev` `:nome`, \
5292 `:upgrade-from :from`, `:etiquetas`, `:autores` — and the three \
5293 code-path lists are the last Vec-shaped author-supplied slots on \
5294 the typed Caixa surface still admitting a duplicate entry. \
5295 `:bibliotecas` duplicates re-parse the same file at \
5296 `feira build` time and silently mask the author's intent to \
5297 declare a *second* biblioteca; `:exe` duplicates collide on the \
5298 flake `packages.<name>` derivation key at the future \
5299 `caixa-flake` materializer; `:servicos` duplicates surface as the \
5300 narrower [`caixa-helm`] / [`caixa-flux`] `UnsupportedServicoCount` \
5301 rejection far from the source `caixa.lisp`. Drop the duplicate \
5302 or rename it to the actual second file intended)",
5303 path.display()
5304 )]
5305 CodePathDuplicate { slot: &'static str, path: PathBuf },
5306 #[error(
5307 ":etiquetas entry is empty (every tag must carry a non-empty \
5308 registry-search identifier; the empty entry has no operational \
5309 meaning — it indexes nothing in the future caixa-registry search \
5310 axis and clutters the rendered Helm `Chart.yaml` `keywords:` array \
5311 with a no-op tag; omit the entry to express \"no tag on this \
5312 position\")"
5313 )]
5314 EtiquetaEmpty,
5315 #[error(
5316 ":etiquetas entry {etiqueta:?} appears more than once (the \
5317 registry-search tag set is a set, not a multiset; duplicate \
5318 entries are silently dedup'd by caixa-helm's `BTreeSet` collect \
5319 at chart render — a \"second wins / one silently disappears\" \
5320 shape divergent from every peer typed-graph set gate \
5321 (`:membros :caixa`, `:placement :clusters`, `:entrada :paths`, \
5322 `:contratos`, `:deps :nome`, `:upgrade-from :from`); drop the \
5323 duplicate or rename it to the actual tag intended)"
5324 )]
5325 EtiquetaDuplicate { etiqueta: String },
5326 #[error(
5327 ":etiquetas entry {etiqueta:?} is not a valid chart-keyword shape: \
5328 {reason} (the substrate consumes this string through the shared \
5329 `crate::render::is_chart_keyword_shape` predicate — the same \
5330 Cargo crates.io `[package] keywords` grammar entry shape: 1..=20 \
5331 bytes, starts with an ASCII letter, ASCII alphanumeric / `_` / `-` \
5332 continuation. The canonical authoring shapes are short kebab-case \
5333 identifiers like `\"mesh\"`, `\"wasm\"`, `\"tatara-lisp\"`, \
5334 `\"hello-world\"`, `\"caixa-servico\"`, `\"infrastructure\"`. \
5335 Without this gate a malformed `:etiquetas` entry (paste-from-doc \
5336 leading / trailing whitespace `\" mesh\"` / `\"mesh \"`; \
5337 paste-from-multiline-doc newline `\"mesh\\nhttp\"`; \
5338 paste-from-Windows-CRLF-doc CR; CSV-list-separator confusion \
5339 `\"mesh,http,grpc\"` — the author meant to author three separate \
5340 list entries; path-separator confusion `\"caixa/servico\"`; \
5341 namespace-suffix `\"http.1\"`; leading-digit `\"1foo\"`; \
5342 kebab-leak `\"-foo\"`; snake-leak `\"_foo\"`; non-ASCII \
5343 `\"café\"` — every legitimate search tag is strict ASCII; \
5344 paste-from-binary-blob NUL / BEL / ESC / DEL byte) silently \
5345 passed `from_lisp` + `validate_etiquetas` + \
5346 `StandardLayout::verify` and landed in the rendered \
5347 `lareira-<nome>` Helm chart's `Chart.yaml keywords:` array as a \
5348 malformed search tag — Artifact Hub's keyword index + the future \
5349 caixa-registry's keyword index would either silently drop the \
5350 tag or fail to index it far from the source caixa.lisp; the gate \
5351 moves the diagnostic to the manifest layer with the offending \
5352 value named verbatim)"
5353 )]
5354 EtiquetaInvalid { etiqueta: String, reason: String },
5355 #[error(
5356 ":autores entry is empty (every maintainer must carry a non-empty \
5357 identifier; the empty entry has no operational meaning — it \
5358 identifies no one in the substrate's authorship index and renders \
5359 as `maintainers: [{{name: \"\", email: null}}]` in the Helm chart's \
5360 `Chart.yaml`, a no-op maintainer the substrate cannot route to; \
5361 omit the entry to express \"no maintainer on this position\")"
5362 )]
5363 AutorEmpty,
5364 #[error(
5365 ":autores entry {autor:?} appears more than once (the maintainer \
5366 set is a set, not a multiset; unlike `:etiquetas`, caixa-helm's \
5367 `maintainers:` rendering does *no* dedup — duplicate entries \
5368 stack verbatim in `Chart.yaml` as two identical \
5369 `Maintainer {{ name, email: None }}` records, divergent from every \
5370 peer typed-graph set gate (`:etiquetas`, `:membros :caixa`, \
5371 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5372 `:deps :nome`, `:upgrade-from :from`); drop the duplicate or \
5373 rename it to the actual author intended)"
5374 )]
5375 AutorDuplicate { autor: String },
5376 #[error(
5377 ":autores entry {autor:?} is not a valid chart-maintainer-name shape: \
5378 {reason} (the substrate consumes this string through the shared \
5379 `crate::render::is_chart_maintainer_name_shape` predicate — the same \
5380 single-line-UTF-8 floor every realistic chart maintainer name carries: \
5381 1..=128 bytes, no leading or trailing whitespace, no ASCII control \
5382 characters anywhere, Unicode bytes accepted. The canonical authoring \
5383 shapes are short single-line identifiers like `\"pleme-io\"`, \
5384 `\"Pleme Contributors\"`, `\"alice <alice@example.com>\"`, \
5385 `\"François Dupont\"`. Without this gate a malformed `:autores` entry \
5386 (paste-from-aligned-doc leading whitespace `\" pleme-io\"` / trailing \
5387 whitespace `\"pleme-io \"`; paste-from-multiline-doc newline \
5388 `\"alice\\nbob\"` — the author pasted a multi-line block of author \
5389 records into one entry instead of splitting into one entry per author; \
5390 paste-from-Windows-CRLF-doc carriage return `\"alice\\rbob\"`; \
5391 tab-from-aligned-doc `\"Pleme\\tContributors\"`; paste-from-binary-blob \
5392 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5393 `validate_autores` + `StandardLayout::verify` and landed in the \
5394 rendered `lareira-<nome>` Helm chart's `Chart.yaml maintainers:` array \
5395 as a YAML-illegal multi-line scalar or a silently-trimmed whitespace \
5396 round-trip — every chart-aware UI (`helm list`, `helm search`, \
5397 Artifact Hub maintainer index) would render the maintainer name in a \
5398 single-line column far from the source caixa.lisp; the gate moves the \
5399 diagnostic to the manifest layer with the offending value named \
5400 verbatim)"
5401 )]
5402 AutorInvalid { autor: String, reason: String },
5403 #[error(
5404 ":repositorio is the empty string (every published caixa names its \
5405 git source via a non-empty `:repositorio` locator — the value \
5406 flows verbatim into the rendered `lareira-<nome>` Helm chart's \
5407 `Chart.yaml` `home:` field via `caixa-helm` and into the FluxCD \
5408 `GitRepository.spec.url` via `caixa-flux`'s \
5409 `ClusterBundleOpts::for_caixa`; both consumers' \
5410 `Option::unwrap_or_else` fallbacks only fire when the slot is \
5411 `None`, so an empty `Some(\"\")` silently lands as `home: \"\"` / \
5412 `url: \"\"` in the rendered artifacts and breaks at `helm \
5413 template` / FluxCD source-controller reconcile time far from the \
5414 source caixa.lisp; omit the slot entirely to defer to the \
5415 renderer's `https://github.com/pleme-io/<nome>` / \
5416 `caixa.nome`-derived fallback, or carry a canonical authoring \
5417 shape like `\"github:org/repo\"`, `\"https://host/path\"`, \
5418 `\"ssh://[user@]host/path\"`, `\"git@host:path\"`, or \
5419 `\"file:///path\"`)"
5420 )]
5421 RepositorioEmpty,
5422 #[error(
5423 ":repositorio {repositorio:?} is not a valid git repo URL: {reason} \
5424 (the substrate consumes this string through the shared \
5425 `crate::render::is_git_repo_url` predicate — the same parser the \
5426 peer `:deps :fonte (:tipo git :repo …)` axis routes its `:repo` \
5427 value through via `DepSource::validate`; the canonical authoring \
5428 shapes are `\"github:org/repo\"` shorthand, `\"https://host/path\"` \
5429 / `\"ssh://[user@]host/path\"` / `\"git://host/path\"` / \
5430 `\"file:///path\"` URL schemes, or the `\"git@host:path\"` \
5431 scp-style SSH form. Without this gate a malformed `:repositorio` \
5432 (whitespace from a paste-from-doc; control characters / CRLF \
5433 from a paste-from-multiline-doc; a leading `-` from a \
5434 CLI-argument-injection footgun; a missing `:` separator from a \
5435 bare `org/repo` shape git treats as a relative filesystem path) \
5436 silently landed in the rendered `Chart.yaml home:` and the \
5437 FluxCD `GitRepository.spec.url` and broke at `git clone` / \
5438 FluxCD reconcile time far from the source caixa.lisp; the gate \
5439 moves the diagnostic to the manifest layer with the offending \
5440 value named verbatim)"
5441 )]
5442 RepositorioInvalid { repositorio: String, reason: String },
5443 #[error(
5444 ":descricao is the empty string (every published caixa names \
5445 its purpose via a non-empty `:descricao` summary — the value \
5446 flows verbatim into the rendered `lareira-<nome>` Helm \
5447 chart's `Chart.yaml` `description:` field via `caixa-helm`'s \
5448 `build_chart_yaml` and into the chart `README.md` header via \
5449 `build_readme`; both consumers' `Option::unwrap_or_else` \
5450 `caixa.nome`-derived fallbacks only fire when the slot is \
5451 `None`, so an empty `Some(\"\")` silently lands as \
5452 `description: \"\"` / a blank `README.md` header in the \
5453 rendered artifacts and breaks at `helm lint` time \
5454 (`WARNING [chart.metadata.description]: description is \
5455 required` on `apiVersion: v2` charts) far from the source \
5456 caixa.lisp; omit the slot entirely to defer to the \
5457 renderer's `\"Generated chart for caixa Servico <nome>\"` / \
5458 `\"caixa Servico <nome>\"` fallbacks, or carry a non-empty \
5459 summary like `\"Canonical Rust→wasm32-wasip2 caixa \
5460 Servico.\"`)"
5461 )]
5462 DescricaoEmpty,
5463 #[error(
5464 ":descricao {descricao:?} is not a valid chart-description shape: \
5465 {reason} (the substrate consumes this string through the shared \
5466 `crate::render::is_chart_description_shape` predicate — the same \
5467 single-line-UTF-8 floor every realistic chart description carries: \
5468 1..=512 bytes, no leading or trailing whitespace, no ASCII control \
5469 characters anywhere, Unicode prose bytes accepted. The canonical \
5470 authoring shapes are short single-line summaries like `\"Canonical \
5471 Rust→wasm32-wasip2 caixa Servico.\"`, `\"Checkout flow.\"`, \
5472 `\"AWS provider caixa for tatara-lisp\"`. Without this gate a \
5473 malformed `:descricao` (paste-from-aligned-doc leading whitespace \
5474 `\" Checkout flow.\"` / trailing whitespace `\"Checkout flow. \"`; \
5475 paste-from-multiline-doc newline `\"Checkout\\nflow.\"`; \
5476 paste-from-Windows-CRLF-doc carriage return `\"Checkout\\rflow.\"`; \
5477 tab-from-aligned-doc `\"Checkout\\tflow.\"`; paste-from-binary-blob \
5478 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5479 `validate_descricao` + `StandardLayout::verify` and landed in the \
5480 rendered `lareira-<nome>` Helm chart's `Chart.yaml description:` \
5481 field + `README.md` header paragraph as a YAML-illegal multi-line \
5482 scalar or a silently-trimmed whitespace round-trip — every \
5483 chart-aware UI (`helm list`, `helm search`, Artifact Hub) would \
5484 render the description in a single-line column far from the source \
5485 caixa.lisp; the gate moves the diagnostic to the manifest layer \
5486 with the offending value named verbatim)"
5487 )]
5488 DescricaoInvalid { descricao: String, reason: String },
5489 #[error(
5490 ":licenca is the empty string (every published caixa names \
5491 its license via a non-empty `:licenca` SPDX expression — the \
5492 value flows verbatim into the rendered `lareira-<nome>` Helm \
5493 chart's `README.md` `## License` section via `caixa-helm`'s \
5494 `build_readme` at `caixa-helm/src/lib.rs:361`; the consumer's \
5495 `Option::unwrap_or_else(|| \"MIT\".into())` `MIT` fallback \
5496 only fires when the slot is `None`, so an empty `Some(\"\")` \
5497 silently lands as a bare trailing period in the rendered \
5498 chart `README.md` `License` section far from the source \
5499 caixa.lisp; omit the slot entirely to defer to the \
5500 renderer's `MIT` fallback, or carry a canonical SPDX \
5501 expression like `\"MIT\"`, `\"Apache-2.0\"`, \
5502 `\"Apache-2.0 OR MIT\"`)"
5503 )]
5504 LicencaEmpty,
5505 #[error(
5506 ":licenca {licenca:?} is not a valid SPDX expression shape: {reason} \
5507 (the substrate consumes this string through the shared \
5508 `crate::render::is_spdx_expression_shape` predicate — the same \
5509 alphabet-floor parser every peer per-axis value-shape gate routes \
5510 its value through; the canonical authoring shapes are single \
5511 license identifiers like `\"MIT\"`, `\"Apache-2.0\"`, `\"BSD-3-Clause\"`, \
5512 compound expressions like `\"Apache-2.0 OR MIT\"`, \
5513 `\"MIT AND BSD-3-Clause\"`, `\"(MIT OR Apache-2.0) AND ISC\"`, \
5514 license-with-exception forms like `\"Apache-2.0 WITH LLVM-exception\"`, \
5515 `+`-suffix variants like `\"GPL-2.0+\"`, and user-defined references \
5516 like `\"LicenseRef-MyLicense\"` / \
5517 `\"DocumentRef-doc:LicenseRef-MyLicense\"`. Without this gate a \
5518 malformed `:licenca` (paste-from-doc whitespace `\"MIT \"` / \
5519 `\" MIT\"`; paste-from-multiline-doc CRLF `\"MIT\\n\"`; \
5520 tab-from-aligned-doc `\"MIT\\tOR Apache-2.0\"`; non-ASCII byte from \
5521 a smart-quote paste; underscore-instead-of-hyphen typo \
5522 `\"Apache_2.0\"`; comma-instead-of-`OR`-keyword colloquial idiom \
5523 `\"MIT, Apache-2.0\"`; slash-dual-license colloquial idiom \
5524 `\"MIT/Apache-2.0\"`; semicolon-list-separator confusion \
5525 `\"MIT; Apache-2.0\"`) silently landed in the rendered chart \
5526 `README.md` `## License` section + a future SPDX-aware \
5527 `Chart.yaml license:` emitter would refuse the value at \
5528 `helm lint` time far from the source caixa.lisp; the gate moves \
5529 the diagnostic to the manifest layer with the offending value \
5530 named verbatim)"
5531 )]
5532 LicencaInvalid { licenca: String, reason: String },
5533 #[error(
5534 ":edicao is the empty string (every published caixa names \
5535 its language edition via a non-empty `:edicao` value — the \
5536 edition determines the tatara-lisp macro surface + \
5537 compatibility flags the substrate applies when building \
5538 the caixa; the canonical `Caixa::template` scaffold every \
5539 `feira init` emits carries `:edicao \"2026\"` verbatim and \
5540 every renderer-side fixture (`caixa-helm`, `caixa-flux`, \
5541 `caixa-mesh`) carries `edicao: Some(\"2026\".into())` by \
5542 construction, so an empty `Some(\"\")` silently lands as a \
5543 bare `(:edicao \"\")` line in the rendered `caixa.lisp` and \
5544 a future renderer-side consumer that folds it through \
5545 `Option::unwrap_or_else` will skip the fallback and pass the \
5546 empty edition through to the substrate's build-time edition \
5547 selector far from the source caixa.lisp; omit the slot \
5548 entirely to defer to the substrate's default edition, or \
5549 carry a canonical edition like `\"2026\"`)"
5550 )]
5551 EdicaoEmpty,
5552 #[error(
5553 ":edicao {edicao:?} is not a valid edition: {reason} (every \
5554 documented tatara-lisp edition is a 4-digit ASCII decimal \
5555 year — `\"2026\"` is the only edition currently minted; \
5556 future-introduced siblings will follow the same shape, peer \
5557 with Cargo's `[package] edition` grammar which every value \
5558 Cargo has ever accepted matches: `\"2015\"`, `\"2018\"`, \
5559 `\"2021\"`, `\"2024\"`. Without this gate the canonical \
5560 paste-from-doc footguns silently passed: a trailing space \
5561 (`\"2026 \"`) from a paste-from-doc, a CRLF (`\"2026\\n\"`) \
5562 from a paste-from-multiline-doc, a fullwidth-keyboard \
5563 look-alike (`\"2026\"`), a free-form non-year value \
5564 (`\"x\"`, `\"latest\"`, `\"nightly\"`), a leading non-digit \
5565 version-tag prefix (`\"v2026\"`, `\"e2026\"`), a \
5566 decimal-shaped pseudo-version (`\"2026.1\"`), or a \
5567 wrong-length numeric value (`\"26\"`, `\"202\"`, \
5568 `\"20260\"`) all landed as `(:edicao \"<garbage>\")` in the \
5569 rendered caixa.lisp and broke at the substrate's \
5570 build-time edition selector far from the source caixa.lisp; \
5571 omit the slot entirely to defer to the substrate's default \
5572 edition, or carry a canonical 4-digit ASCII decimal year \
5573 like `\"2026\"`)"
5574 )]
5575 EdicaoInvalid { edicao: String, reason: String },
5576}
5577
5578#[cfg(test)]
5579mod tests {
5580 use super::*;
5581
5582 #[test]
5583 fn template_round_trips() {
5584 let src = Caixa::template("demo");
5585 let c = Caixa::from_lisp(&src).expect("template must parse");
5586 assert_eq!(c.nome, "demo");
5587 assert_eq!(c.versao, "0.1.0");
5588 assert_eq!(c.kind, CaixaKind::Biblioteca);
5589 assert_eq!(c.bibliotecas, vec!["lib/demo.lisp".to_string()]);
5590 assert!(c.deps.is_empty());
5591 assert!(c.deps_dev.is_empty());
5592 }
5593
5594 #[test]
5595 fn register_populates_registry() {
5596 Caixa::register().expect("first register call in this test process must succeed");
5597 let kws = tatara_lisp::domain::registered_keywords();
5598 assert!(kws.contains(&"defcaixa"));
5599 }
5600
5601 #[test]
5602 fn to_lisp_round_trips() {
5603 let src = Caixa::template("demo");
5604 let c1 = Caixa::from_lisp(&src).unwrap();
5605 let emitted = c1.to_lisp();
5606 let c2 = Caixa::from_lisp(&emitted).expect("emitted lisp parses back");
5607 assert_eq!(c1, c2);
5608 }
5609
5610 // ── DialetoEstrangeiro carries a single typed axis ────────────────────
5611 //
5612 // The compounding pin: the variant stores only the typed
5613 // [`crate::dialeto::CaixaDialeto`], and every user-facing byte-string
5614 // (canonical keyword, description, consumer) routes through the enum's
5615 // own accessors at Display time. Prior to that closure the variant
5616 // carried each accessor's return value as a stored `&'static str`
5617 // snapshot alongside `dialeto`; a caller could construct the variant
5618 // with a snapshot that drifted from what `dialeto`'s accessors would
5619 // return, and every downstream user-facing projection would silently
5620 // disagree with the classification. Storing only the axis makes the
5621 // drift structurally impossible.
5622
5623 #[test]
5624 fn dialeto_estrangeiro_variant_carries_only_the_typed_dialeto_axis() {
5625 // Single-field construction is the whole compounding shape — a
5626 // future re-introduction of a snapshot field (a `palavra_canonica:
5627 // &'static str`, a stored `descricao:`, a stored `consumidor:`)
5628 // would re-open the drift surface and this construction would fail
5629 // to compile with "missing field" until every snapshot was seeded
5630 // at the call site again. The compile-time guarantee is the
5631 // invariant; the assertion below only witnesses that the
5632 // construction is well-formed after the closure.
5633 let err = LeituraError::DialetoEstrangeiro {
5634 dialeto: crate::dialeto::CaixaDialeto::Molde,
5635 };
5636 assert!(matches!(
5637 err,
5638 LeituraError::DialetoEstrangeiro {
5639 dialeto: crate::dialeto::CaixaDialeto::Molde,
5640 }
5641 ));
5642 }
5643
5644 #[test]
5645 fn dialeto_estrangeiro_display_routes_through_typed_dialeto_accessors() {
5646 // For every foreign-dialect classification the variant surfaces —
5647 // [`crate::dialeto::CaixaDialeto::Molde`] and
5648 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two
5649 // variants [`Caixa::from_lisp`] raises this error for — the
5650 // rendered [`std::fmt::Display`] byte-string must interpolate each
5651 // typed accessor's return verbatim. A future re-introduction of a
5652 // stored `&'static str` snapshot alongside `dialeto` that Display
5653 // read instead of the accessor would fail this pin as soon as the
5654 // two disagreed; a future accessor rebrand (a per-dialect
5655 // consumer rename, a canonical-keyword shift once the substrate
5656 // migration named in [`crate::dialeto`] completes) reaches every
5657 // consumer through one typed dispatch and this pin verifies the
5658 // display path is one of them.
5659 for d in [
5660 crate::dialeto::CaixaDialeto::Molde,
5661 crate::dialeto::CaixaDialeto::MoldePosicional,
5662 ] {
5663 let rendered = LeituraError::DialetoEstrangeiro { dialeto: d }.to_string();
5664 assert!(
5665 rendered.contains(d.palavra_canonica()),
5666 "Display must interpolate `dialeto.palavra_canonica()` \
5667 verbatim — a stored snapshot would silently drift from \
5668 the typed accessor. dialect: {d}, rendered: {rendered:?}"
5669 );
5670 assert!(
5671 rendered.contains(d.descricao()),
5672 "Display must interpolate `dialeto.descricao()` verbatim. \
5673 dialect: {d}, rendered: {rendered:?}"
5674 );
5675 assert!(
5676 rendered.contains(d.consumidor()),
5677 "Display must interpolate `dialeto.consumidor()` verbatim. \
5678 dialect: {d}, rendered: {rendered:?}"
5679 );
5680 }
5681 }
5682
5683 #[test]
5684 fn from_lisp_rejects_molde_dialect_via_typed_variant() {
5685 // The end-to-end pin the compounding closure defends: a
5686 // Molde-dialect source lands as [`LeituraError::DialetoEstrangeiro`]
5687 // carrying [`crate::dialeto::CaixaDialeto::Molde`], and the
5688 // rendered Display byte-string names the Molde accessors'
5689 // returns verbatim. Any future path that constructed the variant
5690 // with a mismatched snapshot (a stored `palavra_canonica:
5691 // "defcaixa"` on a `Molde` classification) would land Display
5692 // pointing at `defcaixa` while the typed axis said `Molde` — the
5693 // exact drift the closure removes.
5694 let src = r#"
5695 (defcaixa
5696 :name "x"
5697 :kind :Biblioteca
5698 :ecosystem :rust-single-crate
5699 :package {:name "x" :version "0.1.0"})
5700 "#;
5701 let err = Caixa::from_lisp(src).expect_err("Molde dialect must not parse as Pacote");
5702 match err {
5703 LeituraError::DialetoEstrangeiro { dialeto } => {
5704 assert_eq!(dialeto, crate::dialeto::CaixaDialeto::Molde);
5705 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5706 assert!(rendered.contains(dialeto.palavra_canonica()));
5707 assert!(rendered.contains(dialeto.consumidor()));
5708 assert!(rendered.contains(dialeto.descricao()));
5709 }
5710 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5711 }
5712 }
5713
5714 #[test]
5715 fn from_lisp_rejects_molde_posicional_dialect_via_typed_variant() {
5716 // Coverage pin for the [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5717 // arm of the [`Caixa::from_lisp`] foreign-dialect gate — the
5718 // positional-arity `defmolde` form written under a `(defcaixa …)`
5719 // head (`(defcaixa todoku-go :kind :Biblioteca :ecosystem :go
5720 // …)`). Pre-lift this arm rode the same `foreign =>` wildcard
5721 // the [`crate::dialeto::CaixaDialeto::Molde`] sibling arm rode,
5722 // so no test exercised the positional-arity path through
5723 // `Caixa::from_lisp` specifically; the sibling
5724 // [`from_lisp_rejects_molde_dialect_via_typed_variant`] only
5725 // covered [`crate::dialeto::CaixaDialeto::Molde`]. Post-lift the
5726 // two arms route through the lifted
5727 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5728 // typed predicate — the same predicate the pre-lift `foreign =>`
5729 // wildcard resolved to today — and this pin makes the
5730 // positional-arity arm's byte-shape at the gate explicit rather
5731 // than implied by wildcard-absorption. A future regression that
5732 // silently reordered [`crate::dialeto::CaixaDialeto::is_molde_family`]'s
5733 // arm-set (dropped [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5734 // from the two-arity closure) would fail this pin at caixa-core
5735 // test time rather than surfacing far from the change as a
5736 // `caixa.lisp` carrying a `(defcaixa todoku-go :ecosystem :go
5737 // …)` silently parsing past the derive.
5738 let src = r#"
5739 (defcaixa todoku-go
5740 :kind :Biblioteca
5741 :ecosystem :go
5742 :package {:name "todoku-go" :version "0.3.0"})
5743 "#;
5744 let err =
5745 Caixa::from_lisp(src).expect_err("MoldePosicional dialect must not parse as Pacote");
5746 match err {
5747 LeituraError::DialetoEstrangeiro { dialeto } => {
5748 assert_eq!(
5749 dialeto,
5750 crate::dialeto::CaixaDialeto::MoldePosicional,
5751 "DialetoEstrangeiro must carry the MoldePosicional \
5752 variant verbatim — the positional-arity `defmolde` \
5753 form under a `(defcaixa …)` head is the \
5754 `MoldePosicional` arm's canonical byte-shape"
5755 );
5756 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5757 assert!(
5758 rendered.contains(dialeto.palavra_canonica()),
5759 "Display must interpolate `dialeto.palavra_canonica()` \
5760 verbatim on the MoldePosicional arm; rendered: \
5761 {rendered:?}"
5762 );
5763 assert!(
5764 rendered.contains(dialeto.consumidor()),
5765 "Display must interpolate `dialeto.consumidor()` \
5766 verbatim on the MoldePosicional arm; rendered: \
5767 {rendered:?}"
5768 );
5769 assert!(
5770 rendered.contains(dialeto.descricao()),
5771 "Display must interpolate `dialeto.descricao()` \
5772 verbatim on the MoldePosicional arm; rendered: \
5773 {rendered:?}"
5774 );
5775 }
5776 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5777 }
5778 }
5779
5780 #[test]
5781 fn from_lisp_dialect_gate_dispatches_through_caixa_dialeto_is_molde_family_predicate() {
5782 // Load-bearing byte-parity pin: for every arm in
5783 // [`crate::dialeto::CaixaDialeto::ALL`], the
5784 // [`Caixa::from_lisp`] foreign-dialect gate's DialetoEstrangeiro
5785 // partition must agree with the lifted
5786 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5787 // typed predicate — i.e. from_lisp raises
5788 // [`LeituraError::DialetoEstrangeiro`] carrying `d` iff
5789 // `d.is_molde_family()` returns `true`, and does NOT raise
5790 // [`LeituraError::DialetoEstrangeiro`] on any arm where the
5791 // predicate returns `false` (the arm's source falls through to
5792 // the derive — parses cleanly on
5793 // [`crate::dialeto::CaixaDialeto::Pacote`], surfaces a
5794 // [`LeituraError::Leitura`] on
5795 // [`crate::dialeto::CaixaDialeto::Desconhecido`]).
5796 //
5797 // Pre-lift the gate hand-rolled a three-arm match
5798 // (`Pacote => {}`, `Desconhecido => {}`, `foreign => Err(…)`)
5799 // whose `foreign =>` wildcard expressed no compile-time link
5800 // back to the substrate primitive's arm-family; a future fifth
5801 // dialect the [`crate::dialeto`] module doc's "third dialect"
5802 // hazard actualises would fall silently onto the wildcard
5803 // regardless of whether it belonged to the `defmolde` family or
5804 // to a distinct `defcaixa`-family. Post-lift the partition
5805 // resolves through
5806 // [`crate::dialeto::CaixaDialeto::is_molde_family`]'s single
5807 // typed dispatch, and this pin refuses any future regression
5808 // that silently split the from_lisp partition from the typed
5809 // predicate — the two paths now migrate as one on any future
5810 // arm addition.
5811 //
5812 // Sibling in shape to the peer
5813 // [`crate::dialeto::tests::caixa_dialeto_is_molde_family_agrees_with_palavra_canonica_defmolde_projection`]
5814 // (e9d2315) that pins the same byte-parity between
5815 // [`crate::dialeto::CaixaDialeto::is_molde_family`] and the
5816 // sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
5817 // `== "defmolde"` classifier — extends the discipline from the
5818 // two paths within the [`crate::dialeto`] primitive onto the
5819 // third external consumer of the `defmolde`-family partition
5820 // (the [`Caixa::from_lisp`] gate that raises
5821 // [`LeituraError::DialetoEstrangeiro`]).
5822 let fixtures: &[(crate::dialeto::CaixaDialeto, &str)] = &[
5823 (
5824 crate::dialeto::CaixaDialeto::Pacote,
5825 r#"
5826 (defcaixa
5827 :nome "checkout"
5828 :versao "0.1.0"
5829 :kind Biblioteca
5830 :edicao "2026"
5831 :descricao "canonical Pacote source"
5832 :autores ()
5833 :etiquetas ()
5834 :deps ()
5835 :deps-dev ()
5836 :bibliotecas ("lib/checkout.lisp"))
5837 "#,
5838 ),
5839 (
5840 crate::dialeto::CaixaDialeto::Molde,
5841 r#"
5842 (defcaixa
5843 :name "base64"
5844 :kind :Biblioteca
5845 :ecosystem :rust-single-crate
5846 :package {:name "base64" :version "0.22.1"}
5847 :workflows [:auto-release])
5848 "#,
5849 ),
5850 (
5851 crate::dialeto::CaixaDialeto::MoldePosicional,
5852 r#"
5853 (defcaixa todoku-go
5854 :kind :Biblioteca
5855 :ecosystem :go
5856 :package {:name "todoku-go" :version "0.3.0"})
5857 "#,
5858 ),
5859 (
5860 crate::dialeto::CaixaDialeto::Desconhecido,
5861 r#"(defcaixa :licenca "MIT")"#,
5862 ),
5863 ];
5864
5865 // Coverage: every arm in [`crate::dialeto::CaixaDialeto::ALL`]
5866 // must appear in the fixture table so the pin's arm-set stays
5867 // synchronised with the enum's arm-set. Fails at test time if a
5868 // future fifth arm added to [`crate::dialeto::CaixaDialeto`]
5869 // (with a corresponding `is_molde_family` return) forgot to
5870 // extend this fixture table with a canonical source for the new
5871 // arm — the pin cannot cover an arm it has no source for.
5872 for &expected in crate::dialeto::CaixaDialeto::ALL {
5873 assert!(
5874 fixtures.iter().any(|(d, _)| *d == expected),
5875 "fixture table must carry a canonical source for every \
5876 CaixaDialeto arm; missing: {expected:?}"
5877 );
5878 }
5879
5880 for &(expected_dialect, src) in fixtures {
5881 let classified = crate::dialeto::classify(src.trim()).unwrap_or_else(|err| {
5882 panic!(
5883 "fixture source for {expected_dialect:?} must classify \
5884 cleanly, got err: {err:?}"
5885 )
5886 });
5887 assert_eq!(
5888 classified, expected_dialect,
5889 "fixture source for {expected_dialect:?} must classify as \
5890 {expected_dialect:?} (drift here defeats the byte-parity \
5891 pin below — a source labelled for one arm but classifying \
5892 as another would silently satisfy or violate the pin for \
5893 the wrong reason)"
5894 );
5895
5896 let outcome = Caixa::from_lisp(src);
5897 match (expected_dialect.is_molde_family(), &outcome) {
5898 (true, Err(LeituraError::DialetoEstrangeiro { dialeto })) => {
5899 assert_eq!(
5900 *dialeto, expected_dialect,
5901 "DialetoEstrangeiro must carry the same typed arm \
5902 the classifier returned — a drift here would let \
5903 from_lisp raise the error while pointing at the \
5904 wrong dialect (e.g. rejecting a \
5905 MoldePosicional source as Molde). arm: \
5906 {expected_dialect:?}"
5907 );
5908 }
5909 (true, other) => panic!(
5910 "arm {expected_dialect:?} has is_molde_family() = true \
5911 so from_lisp must raise DialetoEstrangeiro carrying \
5912 {expected_dialect:?}; got: {other:?}"
5913 ),
5914 (false, Err(LeituraError::DialetoEstrangeiro { dialeto })) => panic!(
5915 "arm {expected_dialect:?} has is_molde_family() = false \
5916 so from_lisp must NOT raise DialetoEstrangeiro; got \
5917 one carrying: {dialeto:?}. This means the typed \
5918 predicate and the from_lisp partition disagree on \
5919 this arm — exactly the drift this pin refuses."
5920 ),
5921 (false, _) => {
5922 // A non-molde arm's source falls through to the
5923 // derive: Pacote sources parse to Ok(_); Desconhecido
5924 // sources surface as LeituraError::Leitura from the
5925 // derive's own unknown-keyword rejection. Either
5926 // shape is acceptable here — the pin's promise is
5927 // narrower: "no DialetoEstrangeiro on
5928 // is_molde_family() == false".
5929 }
5930 }
5931 }
5932 }
5933
5934 // ── M2 typed-substrate slot tests (limits, behavior, upgrade-from, supervisor) ──
5935
5936 #[test]
5937 fn limits_round_trip_via_json() {
5938 use crate::LimitsSpec;
5939 use std::time::Duration;
5940 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5941 c.limits = Some(LimitsSpec {
5942 memory: Some(64 * 1024 * 1024),
5943 fuel: Some(1_000_000),
5944 wall_clock: Some(Duration::from_secs(30)),
5945 cpu: Some(500),
5946 });
5947 let json = serde_json::to_string(&c).unwrap();
5948 assert!(json.contains("\"limits\""));
5949 assert!(json.contains("\"64MiB\""));
5950 assert!(json.contains("\"30s\""));
5951 assert!(json.contains("\"500m\""));
5952 let back: Caixa = serde_json::from_str(&json).unwrap();
5953 assert_eq!(c.limits, back.limits);
5954 }
5955
5956 #[test]
5957 fn behavior_round_trip_via_json() {
5958 use crate::BehaviorSpec;
5959 use std::path::PathBuf;
5960 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5961 c.behavior = Some(BehaviorSpec {
5962 on_init: Some(PathBuf::from("lib/init.lisp")),
5963 on_call: Some(PathBuf::from("lib/handlers.lisp")),
5964 ..Default::default()
5965 });
5966 let json = serde_json::to_string(&c).unwrap();
5967 let back: Caixa = serde_json::from_str(&json).unwrap();
5968 assert_eq!(c.behavior, back.behavior);
5969 }
5970
5971 #[test]
5972 fn upgrade_from_round_trip_via_json() {
5973 use crate::{UpgradeFromEntry, UpgradeInstruction};
5974 use std::path::PathBuf;
5975 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5976 c.upgrade_from = vec![UpgradeFromEntry {
5977 from: "0.1.0".into(),
5978 instructions: vec![
5979 UpgradeInstruction::LoadModule {
5980 module: "demo".into(),
5981 },
5982 UpgradeInstruction::StateChange {
5983 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5984 },
5985 UpgradeInstruction::SoftPurge {
5986 module: "demo-old".into(),
5987 },
5988 ],
5989 }];
5990 let json = serde_json::to_string(&c).unwrap();
5991 let back: Caixa = serde_json::from_str(&json).unwrap();
5992 assert_eq!(c.upgrade_from, back.upgrade_from);
5993 }
5994
5995 #[test]
5996 fn supervisor_view_returns_typed_shape() {
5997 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5998 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
5999 c.kind = CaixaKind::Supervisor;
6000 c.bibliotecas.clear();
6001 c.estrategia = Some(RestartStrategy::OneForOne);
6002 c.max_restarts = Some(5);
6003 c.restart_window = Some("60s".into());
6004 c.children = vec![ChildSpec {
6005 caixa: "worker".into(),
6006 versao: "^0.1".into(),
6007 restart: RestartPolicy::Permanent,
6008 }];
6009 let view = c.supervisor_view().expect("Supervisor kind has a view");
6010 assert_eq!(view.estrategia, RestartStrategy::OneForOne);
6011 assert_eq!(view.max_restarts, 5);
6012 assert_eq!(
6013 view.restart_window,
6014 Some(std::time::Duration::from_secs(60))
6015 );
6016 assert_eq!(view.children.len(), 1);
6017 view.validate().unwrap();
6018 }
6019
6020 #[test]
6021 fn supervisor_view_none_for_non_supervisor_kinds() {
6022 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6023 assert!(c.supervisor_view().is_none());
6024 }
6025
6026 #[test]
6027 fn declared_mesh_slots_empty_for_bare_caixa() {
6028 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6029 assert!(c.declared_mesh_slots().is_empty());
6030 }
6031
6032 #[test]
6033 fn declared_mesh_slots_reports_only_set_slots_in_canonical_order() {
6034 use crate::{Entrada, Membro};
6035 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6036 // Set a non-adjacent pair (:membros + :entrada) to pin that the
6037 // canonical declaration order is preserved regardless of which
6038 // subset is populated.
6039 c.membros = vec![Membro {
6040 caixa: "a".into(),
6041 versao: "^0.1".into(),
6042 }];
6043 c.entrada = Some(Entrada {
6044 host: "x.example.com".into(),
6045 para: "a".into(),
6046 paths: vec![],
6047 port: 8080,
6048 });
6049 assert_eq!(
6050 c.declared_mesh_slots(),
6051 vec![
6052 crate::render::M3_AUTHOR_KEY_MEMBROS,
6053 crate::render::M3_AUTHOR_KEY_ENTRADA,
6054 ]
6055 );
6056 }
6057
6058 #[test]
6059 fn m3_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6060 // Scalar-value pin: the five author-facing kebab-case labels the
6061 // `(defcaixa … :<slot> (…))` surface admits on the M3 top-level
6062 // mesh slot axis, one arm per typed slot. Mirrors the peer
6063 // scalar-value pin the sibling
6064 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6065 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6066 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] M2 top-level slot consts
6067 // carry (f49c8b0), so both altitudes of the typed-slot algebra
6068 // (per-Servico M2 + per-Aplicacao M3) share the same
6069 // "one canonical byte-string per arm" discipline. A future
6070 // rebrand (`:membros` → `:members`, `:contratos` → `:contracts`,
6071 // `:politicas` → `:policies`, `:placement` → `:distribution`,
6072 // `:entrada` → `:ingress`) lands as an edit to exactly one const,
6073 // and every consumer that reaches for the label picks it up at
6074 // build time rather than at runtime as a downstream mismatch.
6075 assert_eq!(crate::render::M3_AUTHOR_KEY_MEMBROS, ":membros");
6076 assert_eq!(crate::render::M3_AUTHOR_KEY_CONTRATOS, ":contratos");
6077 assert_eq!(crate::render::M3_AUTHOR_KEY_POLITICAS, ":politicas");
6078 assert_eq!(crate::render::M3_AUTHOR_KEY_PLACEMENT, ":placement");
6079 assert_eq!(crate::render::M3_AUTHOR_KEY_ENTRADA, ":entrada");
6080 }
6081
6082 #[test]
6083 fn declared_mesh_slots_route_through_lifted_m3_author_key_consts() {
6084 // Production-through-const pin: the five per-arm labels the
6085 // [`Caixa::declared_mesh_slots`] tagger pushes onto its return
6086 // `Vec` route through the lifted
6087 // [`crate::M3_AUTHOR_KEY_MEMBROS`] /
6088 // [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
6089 // [`crate::M3_AUTHOR_KEY_POLITICAS`] /
6090 // [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
6091 // [`crate::M3_AUTHOR_KEY_ENTRADA`] consts, in canonical
6092 // declaration order. A future re-order or drift at the tagger
6093 // (a rename that reaches the tagger but not the const, or vice
6094 // versa) surfaces here at build time rather than at runtime as
6095 // a [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
6096 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6097 // commit. Mirror of the peer
6098 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6099 // pin (f49c8b0) on the sibling per-Servico M2 top-level slot
6100 // axis.
6101 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
6102 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6103 c.membros = vec![Membro {
6104 caixa: "a".into(),
6105 versao: "^0.1".into(),
6106 }];
6107 c.contratos = vec![WitContract {
6108 de: "a".into(),
6109 para: "a".into(),
6110 wit: "wasi:http/proxy".into(),
6111 endpoint: Some("/x".into()),
6112 subject: None,
6113 slot: None,
6114 }];
6115 c.politicas = Some(MeshPolicy::default());
6116 c.placement = Some(Placement {
6117 estrategia: PlacementStrategy::Replicated,
6118 clusters: vec!["rio".into()],
6119 affinity: None,
6120 shard_key: None,
6121 });
6122 c.entrada = Some(Entrada {
6123 host: "x.example.com".into(),
6124 para: "a".into(),
6125 paths: vec![],
6126 port: 8080,
6127 });
6128 assert_eq!(
6129 c.declared_mesh_slots(),
6130 vec![
6131 crate::render::M3_AUTHOR_KEY_MEMBROS,
6132 crate::render::M3_AUTHOR_KEY_CONTRATOS,
6133 crate::render::M3_AUTHOR_KEY_POLITICAS,
6134 crate::render::M3_AUTHOR_KEY_PLACEMENT,
6135 crate::render::M3_AUTHOR_KEY_ENTRADA,
6136 ]
6137 );
6138 }
6139
6140 #[test]
6141 fn declared_supervisor_slots_empty_for_bare_caixa() {
6142 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6143 assert!(c.declared_supervisor_slots().is_empty());
6144 }
6145
6146 #[test]
6147 fn declared_supervisor_slots_reports_only_set_slots_in_canonical_order() {
6148 use crate::RestartStrategy;
6149 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6150 // Set a non-adjacent pair (:estrategia + :restart-window) to pin
6151 // that the canonical declaration order is preserved regardless
6152 // of which subset is populated.
6153 c.estrategia = Some(RestartStrategy::OneForOne);
6154 c.restart_window = Some("60s".into());
6155 assert_eq!(
6156 c.declared_supervisor_slots(),
6157 vec![
6158 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6159 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6160 ]
6161 );
6162 }
6163
6164 #[test]
6165 fn supervisor_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6166 // Scalar-value pin: the four author-facing kebab-case labels the
6167 // `(defcaixa … :<slot> (…))` surface admits on the Supervisor
6168 // supervision-tree slot axis, one arm per typed slot. Mirrors the
6169 // peer scalar-value pins the sibling
6170 // [`crate::render::M2_AUTHOR_KEY_LIMITS`] /
6171 // [`crate::render::M2_AUTHOR_KEY_BEHAVIOR`] /
6172 // [`crate::render::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot
6173 // consts and [`crate::render::M3_AUTHOR_KEY_MEMBROS`] etc.
6174 // top-level M3 slot consts carry, so all three kind-scoped
6175 // typed-slot-family author-facing-label axes route through one
6176 // canonical per-arm declaration. A future rebrand
6177 // (`:estrategia` → `:strategy` for English uniformity,
6178 // `:max-restarts` → `:max-intensity` matching Erlang/OTP's
6179 // `MaxIntensity` name, `:restart-window` → `:period` matching
6180 // OTP's `Period` name, `:children` → `:workers` matching Elixir
6181 // idiom) lands as an edit to exactly one const, and every
6182 // consumer that reaches for the label picks it up at build time
6183 // rather than at runtime as a downstream mismatch.
6184 assert_eq!(
6185 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6186 ":estrategia"
6187 );
6188 assert_eq!(
6189 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6190 ":max-restarts"
6191 );
6192 assert_eq!(
6193 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6194 ":restart-window"
6195 );
6196 assert_eq!(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN, ":children");
6197 }
6198
6199 #[test]
6200 fn declared_supervisor_slots_route_through_lifted_supervisor_author_key_consts() {
6201 // Production-through-const pin: the four per-arm labels the
6202 // [`Caixa::declared_supervisor_slots`] tagger pushes onto its
6203 // return `Vec` route through the lifted
6204 // [`crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`] /
6205 // [`crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`] /
6206 // [`crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW`] /
6207 // [`crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN`] consts, in
6208 // canonical declaration order. A future re-order or drift at the
6209 // tagger (a rename that reaches the tagger but not the const, or
6210 // vice versa) surfaces here at build time rather than at runtime
6211 // as a [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
6212 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6213 // commit. Mirror of the peer
6214 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6215 // (f49c8b0) and
6216 // [`declared_mesh_slots_route_through_lifted_m3_author_key_consts`]
6217 // (882f498) pins on the sibling M2 / M3 top-level slot axes.
6218 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6219 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6220 c.estrategia = Some(RestartStrategy::OneForOne);
6221 c.max_restarts = Some(5);
6222 c.restart_window = Some("60s".into());
6223 c.children = vec![ChildSpec {
6224 caixa: "worker".into(),
6225 versao: "^0.1".into(),
6226 restart: RestartPolicy::Permanent,
6227 }];
6228 assert_eq!(
6229 c.declared_supervisor_slots(),
6230 vec![
6231 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6232 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6233 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6234 crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
6235 ]
6236 );
6237 }
6238
6239 #[test]
6240 fn declared_servico_slots_empty_for_bare_caixa() {
6241 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6242 assert!(c.declared_servico_slots().is_empty());
6243 }
6244
6245 #[test]
6246 fn declared_servico_slots_reports_only_set_slots_in_canonical_order() {
6247 use crate::{UpgradeFromEntry, UpgradeInstruction};
6248 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6249 // Set a non-adjacent pair (:limits + :upgrade-from) to pin that
6250 // the canonical declaration order is preserved regardless of
6251 // which subset is populated.
6252 c.limits = Some(crate::LimitsSpec {
6253 fuel: Some(1_000_000),
6254 ..Default::default()
6255 });
6256 c.upgrade_from = vec![UpgradeFromEntry {
6257 from: "0.1.0".into(),
6258 instructions: vec![UpgradeInstruction::Restart],
6259 }];
6260 assert_eq!(
6261 c.declared_servico_slots(),
6262 vec![
6263 crate::render::M2_AUTHOR_KEY_LIMITS,
6264 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6265 ]
6266 );
6267 }
6268
6269 #[test]
6270 fn m2_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6271 // Scalar-value pin: the three author-facing kebab-case labels
6272 // the `(defcaixa … :<slot> (…))` surface admits on the M2
6273 // top-level slot axis, one arm per typed slot. Mirrors the peer
6274 // scalar-value pin the sibling renderer-side
6275 // [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
6276 // [`crate::M2_KEY_UPGRADE_FROM`] camelCase overlay-container
6277 // consts carry, so both halves of the M2 top-level slot dual
6278 // axis (author-facing kebab-case label + renderer-side
6279 // camelCase overlay-container wire key) route through one
6280 // canonical per-arm declaration. A future rebrand
6281 // (`:limits` → `:sandbox` matching Lunatic per-process
6282 // terminology INSPIRATIONS §III.1, `:behavior` → `:gen-server`
6283 // matching Erlang's verbatim name, `:upgrade-from` → `:appup`
6284 // matching Erlang's verbatim appup name) lands as an edit to
6285 // exactly one const, and every consumer that reaches for the
6286 // label picks it up at build time rather than at runtime as a
6287 // downstream mismatch.
6288 assert_eq!(crate::render::M2_AUTHOR_KEY_LIMITS, ":limits");
6289 assert_eq!(crate::render::M2_AUTHOR_KEY_BEHAVIOR, ":behavior");
6290 assert_eq!(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM, ":upgrade-from");
6291 }
6292
6293 #[test]
6294 fn declared_servico_slots_route_through_lifted_m2_author_key_consts() {
6295 // Production-through-const pin: the three per-arm labels the
6296 // [`Caixa::declared_servico_slots`] tagger pushes onto its
6297 // return `Vec` route through the lifted
6298 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6299 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6300 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts, in canonical
6301 // declaration order. A future re-order or drift at the tagger
6302 // (a rename that reaches the tagger but not the const, or vice
6303 // versa) surfaces here at build time rather than at runtime as
6304 // a [`crate::LayoutError::ServicoSlotsOnNonServico`]
6305 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6306 // commit. Mirror of the peer
6307 // [`crate::behavior::BehaviorSpec::declared_slots`] production
6308 // tagger pin (889dc18) on the sibling per-callback axis.
6309 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6310 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6311 c.limits = Some(crate::LimitsSpec {
6312 fuel: Some(1_000_000),
6313 ..Default::default()
6314 });
6315 c.behavior = Some(BehaviorSpec {
6316 on_init: Some(PathBuf::from("lib/init.lisp")),
6317 ..Default::default()
6318 });
6319 c.upgrade_from = vec![UpgradeFromEntry {
6320 from: "0.1.0".into(),
6321 instructions: vec![UpgradeInstruction::Restart],
6322 }];
6323 assert_eq!(
6324 c.declared_servico_slots(),
6325 vec![
6326 crate::render::M2_AUTHOR_KEY_LIMITS,
6327 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
6328 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6329 ]
6330 );
6331 }
6332
6333 #[test]
6334 fn existing_manifests_unaffected_by_new_optional_slots() {
6335 // Regression test: a caixa.lisp authored before M2 typed slots
6336 // should still parse + serialize cleanly. The bare `defcaixa`
6337 // emitted by `Caixa::template` has none of the new fields.
6338 let src = Caixa::template("legacy");
6339 let c = Caixa::from_lisp(&src).unwrap();
6340 assert!(c.limits.is_none());
6341 assert!(c.behavior.is_none());
6342 assert!(c.upgrade_from.is_empty());
6343 assert!(c.estrategia.is_none());
6344 assert!(c.children.is_empty());
6345
6346 // And to_lisp emits a manifest with the new slots in the
6347 // empty/default state — round-trippable.
6348 let emitted = c.to_lisp();
6349 let back = Caixa::from_lisp(&emitted).unwrap();
6350 assert_eq!(c, back);
6351 }
6352
6353 #[test]
6354 fn validate_deps_accepts_canonical_caixa() {
6355 // Positive control: the bare template — zero deps, zero
6356 // deps_dev — passes the gate trivially. A future axis added to
6357 // `Dep::validate` mustn't regress an empty-deps caixa to a
6358 // build error.
6359 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6360 c.validate_deps().unwrap();
6361 }
6362
6363 #[test]
6364 fn validate_deps_rejects_invalid_versao_in_deps() {
6365 // Fail-before-pass-after pin: a malformed `:deps :versao`
6366 // surfaces at validate_deps() time, not at lacre-resolve time.
6367 // Mirrors `rejects_invalid_membro_versao_requirement` and
6368 // `validate_rejects_invalid_child_versao_requirement` on the
6369 // other two `:versao` axes.
6370 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6371 c.deps = vec![Dep::simple("caixa-teia", "^bad-version")];
6372 let err = c.validate_deps().unwrap_err();
6373 assert!(
6374 matches!(
6375 err,
6376 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6377 if nome == "caixa-teia" && versao == "^bad-version"
6378 ),
6379 "got {err:?}"
6380 );
6381 }
6382
6383 #[test]
6384 fn validate_deps_rejects_invalid_versao_in_deps_dev() {
6385 // Parity pin: `:deps-dev` must run through the same per-entry
6386 // validator as `:deps` — a typo in either axis surfaces the
6387 // same diagnostic. Without this leg, `:deps-dev` would be a
6388 // second-class citizen of the typed surface and an author
6389 // could land a build that passes validate_deps but fails at
6390 // `feira lock`-time when the dev-dep is resolved for a test
6391 // build.
6392 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6393 c.deps_dev = vec![Dep::simple("tatara-check", "^^0.1")];
6394 let err = c.validate_deps().unwrap_err();
6395 assert!(
6396 matches!(
6397 err,
6398 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6399 if nome == "tatara-check" && versao == "^^0.1"
6400 ),
6401 "got {err:?}"
6402 );
6403 }
6404
6405 #[test]
6406 fn validate_deps_runs_deps_before_deps_dev() {
6407 // Order pin: when both lists carry typos, the `:deps`
6408 // diagnostic surfaces first. The author's mental model is
6409 // "runtime deps are load-bearing; dev deps are scaffolding";
6410 // surfacing the runtime axis first matches that hierarchy.
6411 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6412 c.deps = vec![Dep::simple("runtime-dep", "^bad-runtime")];
6413 c.deps_dev = vec![Dep::simple("dev-dep", "^bad-dev")];
6414 let err = c.validate_deps().unwrap_err();
6415 assert!(
6416 matches!(
6417 err,
6418 crate::dep::DepError::VersaoInvalid { ref nome, .. }
6419 if nome == "runtime-dep"
6420 ),
6421 "expected `:deps` typo to surface first, got {err:?}"
6422 );
6423 }
6424
6425 #[test]
6426 fn validate_deps_accepts_canonical_versao_forms_in_both_lists() {
6427 // Positive control sweep across both lists. Pin every
6428 // canonical Cargo-shaped form so a future tightening of the
6429 // accepted set surfaces here as a test failure (parity with
6430 // `accepts_canonical_membro_versao_forms` and
6431 // `validate_accepts_canonical_child_versao_forms`).
6432 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6433 c.deps = vec![
6434 Dep::simple("caret", "^0.1"),
6435 Dep::simple("tilde", "~0.1.2"),
6436 Dep::simple("exact", "0.1.0"),
6437 Dep::simple("wildcard", "*"),
6438 Dep::simple("multi-range", ">=0.1, <2"),
6439 ];
6440 c.deps_dev = vec![
6441 Dep::simple("dev-caret", "^0.1"),
6442 Dep::simple("dev-wildcard", "*"),
6443 ];
6444 c.validate_deps().unwrap();
6445 }
6446
6447 #[test]
6448 fn validate_deps_diagnostic_carries_offending_dep() {
6449 // Diagnostic-shape pin: the error names the offending entry's
6450 // `:nome` + `:versao` verbatim and carries a non-empty
6451 // `reason` from `semver::VersionReq::parse`, so a `feira lint`
6452 // run can render the diagnostic without re-parsing.
6453 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6454 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
6455 let err = c.validate_deps().unwrap_err();
6456 let crate::dep::DepError::VersaoInvalid {
6457 nome,
6458 versao,
6459 reason,
6460 } = err
6461 else {
6462 panic!("expected VersaoInvalid, got other variant");
6463 };
6464 assert_eq!(nome, "caixa-teia");
6465 assert_eq!(versao, "not-a-req");
6466 assert!(
6467 !reason.is_empty(),
6468 "VersaoInvalid `reason` must carry the parser's wording verbatim"
6469 );
6470 }
6471
6472 #[test]
6473 fn validate_deps_rejects_ambiguous_fonte_in_deps_dev() {
6474 // Cross-axis pin: `validate_deps` walks both :deps and
6475 // :deps-dev through `Dep::validate`, and the new fonte gate
6476 // (`:tag` + `:branch` both set — the canonical "pin drift"
6477 // footgun) must surface from the :deps-dev arm with the
6478 // offending entry's :nome named. Pin the :deps-dev arm
6479 // explicitly so a future shortcut that only walks :deps
6480 // surfaces here as a regression.
6481 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6482 c.deps_dev = vec![Dep {
6483 nome: "dev-only".into(),
6484 versao: "^0.1".into(),
6485 fonte: Some(crate::DepSource::Git {
6486 repo: "github:p/x".into(),
6487 tag: Some("v1".into()),
6488 rev: None,
6489 branch: Some("main".into()),
6490 }),
6491 opcional: false,
6492 caracteristicas: vec![],
6493 }];
6494 let err = c.validate_deps().unwrap_err();
6495 let crate::dep::DepError::FontePinAmbiguous { nome, pins } = err else {
6496 panic!("expected FontePinAmbiguous from :deps-dev walk");
6497 };
6498 assert_eq!(nome, "dev-only");
6499 assert!(pins.contains(":tag") && pins.contains(":branch"));
6500 }
6501
6502 #[test]
6503 fn validate_deps_rejects_empty_repo_in_deps() {
6504 // Parity pin on the :deps arm: an empty :repo on the runtime
6505 // deps list surfaces the same FonteRepoEmpty diagnostic the
6506 // dep.rs per-entry tests pin, naming the offending entry.
6507 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6508 c.deps = vec![Dep {
6509 nome: "runtime".into(),
6510 versao: "^0.1".into(),
6511 fonte: Some(crate::DepSource::Git {
6512 repo: String::new(),
6513 tag: Some("v1".into()),
6514 rev: None,
6515 branch: None,
6516 }),
6517 opcional: false,
6518 caracteristicas: vec![],
6519 }];
6520 let err = c.validate_deps().unwrap_err();
6521 assert!(
6522 matches!(
6523 err,
6524 crate::dep::DepError::FonteRepoEmpty { ref nome }
6525 if nome == "runtime"
6526 ),
6527 "got {err:?}"
6528 );
6529 }
6530
6531 // ── validate_deps: within-list :nome set-not-multiset gate ─────────
6532
6533 #[test]
6534 fn validate_deps_rejects_duplicate_nome_in_deps() {
6535 // Fail-before-pass-after pin: two `:deps` entries naming the same
6536 // caixa carry two `:versao` / `:fonte` / feature triples that the
6537 // caixa-resolver's lacre pipeline collapses (the second silently
6538 // overwrites the first at `concrete_versao`-resolve time). The
6539 // gate surfaces the duplicate at validate-time, naming the
6540 // offending caixa + the list, before the resolver-side silent
6541 // drop. Mirrors the peer typed-graph duplicate gates
6542 // (`DuplicateChildCaixa`, `MembroDuplicate`, `DuplicateFrom`, …).
6543 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6544 c.deps = vec![
6545 Dep::simple("caixa-teia", "^0.1"),
6546 Dep::simple("caixa-teia", "^0.2"),
6547 ];
6548 let err = c.validate_deps().unwrap_err();
6549 assert!(
6550 matches!(
6551 err,
6552 crate::dep::DepError::DuplicateNome { ref nome, list }
6553 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6554 ),
6555 "got {err:?}"
6556 );
6557 }
6558
6559 #[test]
6560 fn validate_deps_rejects_duplicate_nome_in_deps_dev() {
6561 // Parity pin: `:deps-dev` runs through the same per-list
6562 // duplicate check as `:deps` — neither axis is a second-class
6563 // citizen of the set-not-multiset discipline.
6564 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6565 c.deps_dev = vec![
6566 Dep::simple("tatara-check", "*"),
6567 Dep::simple("tatara-check", "^0.1"),
6568 ];
6569 let err = c.validate_deps().unwrap_err();
6570 assert!(
6571 matches!(
6572 err,
6573 crate::dep::DepError::DuplicateNome { ref nome, list }
6574 if nome == "tatara-check" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
6575 ),
6576 "got {err:?}"
6577 );
6578 }
6579
6580 #[test]
6581 fn validate_deps_accepts_cross_list_same_nome() {
6582 // The Cargo `[dependencies]` + `[dev-dependencies]` override
6583 // convention is preserved: a name appearing in *both* lists is
6584 // valid (the dev-pin overrides at test/dev time). Only
6585 // within-list duplicates are structurally incoherent — pin the
6586 // permissive cross-list semantics so a future shortcut that
6587 // collapses the two seen-sets into one surfaces here as a test
6588 // failure.
6589 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6590 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
6591 c.deps_dev = vec![Dep::simple("caixa-teia", "^0.2")];
6592 c.validate_deps().unwrap();
6593 }
6594
6595 #[test]
6596 fn validate_deps_accepts_distinct_nome_in_both_lists() {
6597 // Positive control: distinct names within each list pass — the
6598 // gate's identity element on the canonical authoring shape.
6599 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6600 c.deps = vec![
6601 Dep::simple("caixa-teia", "^0.1"),
6602 Dep::simple("pleme-mesh", "*"),
6603 ];
6604 c.deps_dev = vec![
6605 Dep::simple("tatara-check", "*"),
6606 Dep::simple("dev-shim", "^0.1"),
6607 ];
6608 c.validate_deps().unwrap();
6609 }
6610
6611 #[test]
6612 fn validate_deps_per_entry_validate_fires_before_duplicate_in_deps() {
6613 // Diagnostic-precedence pin: a malformed `:versao` on the
6614 // duplicating entry surfaces its narrower `VersaoInvalid`
6615 // diagnostic first, before the cross-entry duplicate gate fires
6616 // — the canonical "per-entry shape before cross-entry uniqueness"
6617 // precedence every peer set-not-multiset gate establishes
6618 // (`*_invalid_fires_before_duplicate_check` pins on
6619 // `SupervisorSpec::validate`, `AplicacaoSpec::validate_membros`,
6620 // `validate_upgrade_from`).
6621 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6622 c.deps = vec![
6623 Dep::simple("caixa-teia", "^0.1"),
6624 Dep::simple("caixa-teia", "^bad-version"),
6625 ];
6626 let err = c.validate_deps().unwrap_err();
6627 assert!(
6628 matches!(
6629 err,
6630 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6631 if nome == "caixa-teia" && versao == "^bad-version"
6632 ),
6633 "expected VersaoInvalid to surface before DuplicateNome, got {err:?}"
6634 );
6635 }
6636
6637 #[test]
6638 fn validate_deps_duplicate_diagnostic_names_first_collision() {
6639 // First-collision determinism pin: with three entries naming the
6640 // same caixa, the first colliding pair surfaces — not the last.
6641 // Mirrors the peer first-collision posture on every
6642 // duplicate-target gate
6643 // (`validate_upgrade_from_duplicate_diagnostic_names_second_collision`
6644 // — the second entry is the first collision; this gate uses the
6645 // same shape: the second entry's `:nome` lands in the diagnostic
6646 // because `seen.insert(first.nome)` already populated the set).
6647 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6648 c.deps = vec![
6649 Dep::simple("caixa-teia", "^0.1"),
6650 Dep::simple("caixa-teia", "^0.2"),
6651 Dep::simple("caixa-teia", "^0.3"),
6652 ];
6653 let err = c.validate_deps().unwrap_err();
6654 // The diagnostic carries the offending caixa name; the
6655 // implementation surfaces on the *second* entry (the first
6656 // collision), so the test pins the `:nome` value.
6657 assert!(
6658 matches!(
6659 err,
6660 crate::dep::DepError::DuplicateNome { ref nome, list }
6661 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6662 ),
6663 "got {err:?}"
6664 );
6665 }
6666
6667 #[test]
6668 fn validate_deps_duplicate_in_deps_fires_before_duplicate_in_deps_dev() {
6669 // Cross-list precedence pin: when both lists carry duplicates,
6670 // the `:deps` diagnostic surfaces first — same author-mental-
6671 // model ordering the `validate_deps_runs_deps_before_deps_dev`
6672 // pin establishes for malformed `:versao` (runtime axis before
6673 // dev axis).
6674 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6675 c.deps = vec![
6676 Dep::simple("runtime-dep", "^0.1"),
6677 Dep::simple("runtime-dep", "^0.2"),
6678 ];
6679 c.deps_dev = vec![Dep::simple("dev-dep", "*"), Dep::simple("dev-dep", "^0.1")];
6680 let err = c.validate_deps().unwrap_err();
6681 assert!(
6682 matches!(
6683 err,
6684 crate::dep::DepError::DuplicateNome { ref nome, list }
6685 if nome == "runtime-dep" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6686 ),
6687 "expected :deps duplicate to surface before :deps-dev duplicate, got {err:?}"
6688 );
6689 }
6690
6691 #[test]
6692 fn validate_deps_empty_lists_pass_duplicate_gate() {
6693 // Empty-set identity pin: the bare template (zero deps, zero
6694 // deps_dev) passes the duplicate gate as the gate's identity
6695 // element. A future tighten that conflates "empty" with
6696 // "missing" would regress this baseline.
6697 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6698 c.validate_deps().unwrap();
6699 }
6700
6701 #[test]
6702 fn validate_deps_duplicate_diagnostic_carries_list_tag() {
6703 // Diagnostic-shape pin: the `list:` field tags which list the
6704 // duplicate landed in (`:deps` vs `:deps-dev`) verbatim, so a
6705 // `feira lint` run can route the author to the right block in
6706 // their caixa.lisp without re-deriving the list from context.
6707 // Same self-locating shape every peer per-axis diagnostic
6708 // already exposes.
6709 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6710 c.deps_dev = vec![
6711 Dep::simple("dev-thing", "*"),
6712 Dep::simple("dev-thing", "^0.1"),
6713 ];
6714 let err = c.validate_deps().unwrap_err();
6715 let crate::dep::DepError::DuplicateNome { nome, list } = err else {
6716 panic!("expected DuplicateNome from :deps-dev walk");
6717 };
6718 assert_eq!(nome, "dev-thing");
6719 assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
6720 }
6721
6722 // ── validate_deps: per-entry :caracteristicas set-discipline gate ──
6723
6724 #[test]
6725 fn validate_deps_surfaces_caracteristicas_duplicate_in_deps_list() {
6726 // Thread-through pin on `:deps`: the per-entry
6727 // `Dep::validate_caracteristicas` gate fires inside
6728 // `Caixa::validate_deps`'s linear walk, so a malformed feature
6729 // list on any `:deps` entry surfaces as a `DepError` from
6730 // `validate_deps` — the same reachability shape every per-entry
6731 // `Dep::validate` arm threads through. Without this pin a future
6732 // shortcut that skips the per-entry `Dep::validate` call on the
6733 // cross-entry-uniqueness path would mask the within-entry
6734 // `:caracteristicas` gates.
6735 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6736 c.deps = vec![Dep {
6737 nome: "caixa-teia".into(),
6738 versao: "^0.1".into(),
6739 fonte: None,
6740 opcional: false,
6741 caracteristicas: vec!["http".into(), "http".into()],
6742 }];
6743 let err = c.validate_deps().unwrap_err();
6744 let crate::dep::DepError::CaracteristicaDuplicate {
6745 nome,
6746 caracteristica,
6747 } = err
6748 else {
6749 panic!("expected CaracteristicaDuplicate from :deps walk, got {err:?}");
6750 };
6751 assert_eq!(nome, "caixa-teia");
6752 assert_eq!(caracteristica, "http");
6753 }
6754
6755 #[test]
6756 fn validate_deps_surfaces_caracteristicas_empty_in_deps_dev_list() {
6757 // Peer thread-through pin on `:deps-dev`: same reachability as
6758 // the `:deps` arm above, on the dev-only authoring axis. Pins
6759 // that the `validate_deps` walk visits both lists' per-entry
6760 // gates uniformly. The empty-feature arm carries here so both
6761 // new `:caracteristicas` arms are surfaced via at least one
6762 // `validate_deps` thread-through.
6763 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6764 c.deps_dev = vec![Dep {
6765 nome: "caixa-teia".into(),
6766 versao: "^0.1".into(),
6767 fonte: None,
6768 opcional: false,
6769 caracteristicas: vec![String::new()],
6770 }];
6771 let err = c.validate_deps().unwrap_err();
6772 let crate::dep::DepError::CaracteristicaEmpty { nome } = err else {
6773 panic!("expected CaracteristicaEmpty from :deps-dev walk, got {err:?}");
6774 };
6775 assert_eq!(nome, "caixa-teia");
6776 }
6777
6778 #[test]
6779 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_list() {
6780 // Thread-through pin on `:deps`: the per-entry
6781 // `Dep::validate_caracteristicas` value-shape gate (lifted via
6782 // `crate::render::is_cargo_feature_name`) fires inside
6783 // `Caixa::validate_deps`'s linear walk on the `:deps` list, so
6784 // a structurally invalid feature name on any `:deps` entry
6785 // surfaces as `DepError::CaracteristicaInvalid` from
6786 // `validate_deps` — the same reachability shape every per-entry
6787 // `Dep::validate` arm threads through. Without this pin a
6788 // future shortcut that skips the per-entry `Dep::validate` call
6789 // on the cross-entry-uniqueness path would mask the within-
6790 // entry `:caracteristicas` value-shape gate.
6791 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6792 c.deps = vec![Dep {
6793 nome: "caixa-teia".into(),
6794 versao: "^0.1".into(),
6795 fonte: None,
6796 opcional: false,
6797 caracteristicas: vec!["+http".into()],
6798 }];
6799 let err = c.validate_deps().unwrap_err();
6800 let crate::dep::DepError::CaracteristicaInvalid {
6801 nome,
6802 caracteristica,
6803 ..
6804 } = err
6805 else {
6806 panic!("expected CaracteristicaInvalid from :deps walk, got {err:?}");
6807 };
6808 assert_eq!(nome, "caixa-teia");
6809 assert_eq!(caracteristica, "+http");
6810 }
6811
6812 #[test]
6813 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_dev_list() {
6814 // Peer thread-through pin on `:deps-dev`: same reachability as
6815 // the `:deps` arm above, on the dev-only authoring axis. The
6816 // `http/json` shape carries here so the segment-separator
6817 // diagnostic (the canonical Cargo `dep/feat` namespaced-dep
6818 // confusion footgun) is surfaced via the cross-entry walk too —
6819 // pinning that the `:deps-dev` list visits the same per-entry
6820 // value-shape gate as the `:deps` list.
6821 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6822 c.deps_dev = vec![Dep {
6823 nome: "caixa-teia".into(),
6824 versao: "^0.1".into(),
6825 fonte: None,
6826 opcional: false,
6827 caracteristicas: vec!["http/json".into()],
6828 }];
6829 let err = c.validate_deps().unwrap_err();
6830 let crate::dep::DepError::CaracteristicaInvalid {
6831 nome,
6832 caracteristica,
6833 ..
6834 } = err
6835 else {
6836 panic!("expected CaracteristicaInvalid from :deps-dev walk, got {err:?}");
6837 };
6838 assert_eq!(nome, "caixa-teia");
6839 assert_eq!(caracteristica, "http/json");
6840 }
6841
6842 #[test]
6843 fn to_lisp_preserves_deps() {
6844 let src = r#"
6845(defcaixa
6846 :nome "x"
6847 :versao "0.1.0"
6848 :kind Biblioteca
6849 :deps ((:nome "a" :versao "^0.1")
6850 (:nome "b" :versao "*" :fonte (:tipo git :repo "github:o/b" :tag "v1"))))
6851"#;
6852 let c1 = Caixa::from_lisp(src).unwrap();
6853 let emitted = c1.to_lisp();
6854 let c2 = Caixa::from_lisp(&emitted).expect("round trip");
6855 assert_eq!(c1.deps, c2.deps);
6856 }
6857
6858 // ── Caixa::validate_nome — top-level :nome value-shape gate ─────────
6859
6860 fn caixa_with_nome(nome: &str) -> Caixa {
6861 let mut c = Caixa::from_lisp(&Caixa::template("placeholder")).unwrap();
6862 c.nome = nome.to_string();
6863 c
6864 }
6865
6866 #[test]
6867 fn validate_nome_accepts_canonical_template() {
6868 // Positive control: the bare `feira init`-style template's
6869 // `:nome` ("demo") is a canonical DNS-1123 label; the gate must
6870 // not regress this baseline shape. A future tightening of the
6871 // accepted set surfaces here as a test failure first.
6872 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6873 c.validate_nome().unwrap();
6874 }
6875
6876 #[test]
6877 fn validate_nome_accepts_canonical_forms() {
6878 // Positive-set sweep: each realistic caixa-name shape the K8s
6879 // apiserver accepts as a `metadata.name` label must pass —
6880 // single-word, hyphen-joined, version-suffixed, single-char,
6881 // two-char, digit-start (DNS-1123 allows this; the stricter
6882 // DNS-1035 Service-name rule doesn't), version-suffix-bearing.
6883 // Mirrors `accepts_canonical_membro_caixa_forms` (3f9d7a0) on
6884 // the peer member-name axis.
6885 for nome in [
6886 "checkout",
6887 "cart-v2",
6888 "a",
6889 "db",
6890 "3rd-party-shim",
6891 "payment-retry",
6892 "0",
6893 ] {
6894 caixa_with_nome(nome)
6895 .validate_nome()
6896 .unwrap_or_else(|e| panic!("canonical :nome {nome:?} must validate, got {e:?}"));
6897 }
6898 }
6899
6900 #[test]
6901 fn validate_nome_rejects_empty() {
6902 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
6903 // an empty `:nome` (the derive macro stores the raw String);
6904 // the gate's empty arm names the offending axis with a narrower
6905 // diagnostic than the `NomeInvalid` parse arm would emit.
6906 let c = caixa_with_nome("");
6907 let err = c.validate_nome().unwrap_err();
6908 assert_eq!(err, ManifestError::NomeEmpty);
6909 }
6910
6911 #[test]
6912 fn validate_nome_rejects_uppercase() {
6913 // The canonical "I copied the TitleCase display name verbatim"
6914 // footgun. The K8s apiserver rejects `metadata.name: MyApp` at
6915 // admission on every derived artifact (Helm chart, ComputeUnit,
6916 // CNP, HTTPRoute, label values); the gate moves the diagnostic
6917 // to the source `caixa.lisp` and the reason suggests the
6918 // lowercased fix verbatim.
6919 let c = caixa_with_nome("MyApp");
6920 let err = c.validate_nome().unwrap_err();
6921 let ManifestError::NomeInvalid { nome, reason } = err else {
6922 panic!("expected NomeInvalid for uppercase :nome");
6923 };
6924 assert_eq!(nome, "MyApp");
6925 assert!(
6926 reason.contains("uppercase") && reason.contains("myapp"),
6927 "diagnostic must name the violation + the lowercased fix, got {reason:?}"
6928 );
6929 }
6930
6931 #[test]
6932 fn validate_nome_rejects_underscore() {
6933 // The Python-/Postgres-style `snake_case` leak. DNS-1123 forbids
6934 // `_`; the apiserver rejects on admission across every derived
6935 // artifact. Same fixture pinned for `:membros :caixa` (3f9d7a0)
6936 // and `:children :caixa` (31bfa43).
6937 let c = caixa_with_nome("my_app");
6938 let err = c.validate_nome().unwrap_err();
6939 assert!(
6940 matches!(
6941 err,
6942 ManifestError::NomeInvalid { ref nome, ref reason }
6943 if nome == "my_app" && reason.contains('_')
6944 ),
6945 "got {err:?}"
6946 );
6947 }
6948
6949 #[test]
6950 fn validate_nome_rejects_dot() {
6951 // A `:nome` is a single DNS-1123 label, not a subdomain. The
6952 // "I want to namespace with `.`" footgun the gate redirects to
6953 // `-` via the shared predicate's reason wording.
6954 let c = caixa_with_nome("team.app");
6955 let err = c.validate_nome().unwrap_err();
6956 assert!(
6957 matches!(
6958 err,
6959 ManifestError::NomeInvalid { ref nome, ref reason }
6960 if nome == "team.app" && reason.contains('.')
6961 ),
6962 "got {err:?}"
6963 );
6964 }
6965
6966 #[test]
6967 fn validate_nome_rejects_leading_hyphen() {
6968 // DNS-1123 boundary rule: the label must start with an ASCII
6969 // alphanumeric. Pin the leading-`-` arm explicitly.
6970 let c = caixa_with_nome("-app");
6971 let err = c.validate_nome().unwrap_err();
6972 assert!(
6973 matches!(
6974 err,
6975 ManifestError::NomeInvalid { ref nome, .. } if nome == "-app"
6976 ),
6977 "got {err:?}"
6978 );
6979 }
6980
6981 #[test]
6982 fn validate_nome_rejects_trailing_hyphen() {
6983 // Symmetric arm of the boundary rule, pinned separately so a
6984 // future relaxation that only checks the leading position
6985 // surfaces here. Mirrors `rejects_membro_caixa_with_trailing_hyphen`
6986 // and `_with_trailing_hyphen` on the supervisor / aplicacao
6987 // axes.
6988 let c = caixa_with_nome("app-");
6989 let err = c.validate_nome().unwrap_err();
6990 assert!(
6991 matches!(
6992 err,
6993 ManifestError::NomeInvalid { ref nome, .. } if nome == "app-"
6994 ),
6995 "got {err:?}"
6996 );
6997 }
6998
6999 #[test]
7000 fn validate_nome_rejects_unicode() {
7001 // IDN must be pre-encoded as Punycode (`xn--…`); raw Unicode
7002 // bytes are rejected by the K8s apiserver on every name axis.
7003 let c = caixa_with_nome("café");
7004 let err = c.validate_nome().unwrap_err();
7005 assert!(
7006 matches!(
7007 err,
7008 ManifestError::NomeInvalid { ref nome, .. } if nome == "café"
7009 ),
7010 "got {err:?}"
7011 );
7012 }
7013
7014 #[test]
7015 fn validate_nome_rejects_whitespace() {
7016 // The paste-from-sketch / paste-from-spec footgun. Internal
7017 // whitespace is rejected by every K8s name axis.
7018 let c = caixa_with_nome("my app");
7019 let err = c.validate_nome().unwrap_err();
7020 assert!(
7021 matches!(
7022 err,
7023 ManifestError::NomeInvalid { ref nome, .. } if nome == "my app"
7024 ),
7025 "got {err:?}"
7026 );
7027 }
7028
7029 #[test]
7030 fn validate_nome_rejects_too_long() {
7031 // 64-byte boundary pin: the K8s apiserver rejects any
7032 // `metadata.name` over 63 bytes at admission; the diagnostic
7033 // names both the 63-byte cap and the actual length so the
7034 // author can shorten in one edit. Mirrors `_too_long` on the
7035 // peer member-/cluster-/child-name axes.
7036 let over = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN + 1);
7037 let c = caixa_with_nome(&over);
7038 let err = c.validate_nome().unwrap_err();
7039 let ManifestError::NomeInvalid { nome, reason } = err else {
7040 panic!("expected NomeInvalid for over-cap :nome");
7041 };
7042 assert_eq!(nome.len(), crate::DNS_1123_LABEL_MAX_LEN + 1);
7043 assert!(
7044 reason.contains("63") && reason.contains("64"),
7045 "diagnostic must name the cap + actual length, got {reason:?}"
7046 );
7047 }
7048
7049 #[test]
7050 fn nome_max_length_validates() {
7051 // The 63-byte cap exactly — the boundary-accepting case pinned
7052 // alongside `validate_nome_rejects_too_long` so a future cap
7053 // shift surfaces both arms simultaneously. Mirrors
7054 // `membro_caixa_max_length_validates`,
7055 // `placement_cluster_max_length_validates`,
7056 // `child_caixa_max_length_validates`.
7057 let at_cap = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7058 caixa_with_nome(&at_cap).validate_nome().unwrap();
7059 }
7060
7061 #[test]
7062 fn nome_empty_takes_precedence_over_invalid() {
7063 // Order pin: the empty arm fires before the predicate is
7064 // consulted. Empty < invalid in self-locating-ness — the
7065 // narrower `NomeEmpty` diagnostic doesn't carry a useless
7066 // `nome: ""` reference into the parser-shaped reason. Mirrors
7067 // `membro_caixa_empty_takes_precedence_over_invalid` on the
7068 // peer axis (3f9d7a0).
7069 let c = caixa_with_nome("");
7070 assert_eq!(c.validate_nome().unwrap_err(), ManifestError::NomeEmpty);
7071 }
7072
7073 #[test]
7074 fn nome_invalid_diagnostic_carries_offending_nome() {
7075 // Diagnostic-shape pin: the error names the offending `:nome`
7076 // verbatim with a non-empty parser-shaped reason, so a `feira
7077 // lint` run can render the diagnostic without re-parsing.
7078 // Mirrors `membro_caixa_invalid_diagnostic_carries_offending_caixa`.
7079 let c = caixa_with_nome("MyApp");
7080 let err = c.validate_nome().unwrap_err();
7081 let ManifestError::NomeInvalid { nome, reason } = err else {
7082 panic!("expected NomeInvalid variant");
7083 };
7084 assert_eq!(nome, "MyApp");
7085 assert!(
7086 !reason.is_empty(),
7087 "NomeInvalid `reason` must carry the predicate's wording verbatim"
7088 );
7089 }
7090
7091 // ── Caixa::validate_nome_chart_name_budget — joint-length on `:nome` ──
7092 //
7093 // The bare-`:nome` axis [`Caixa::validate_nome`] caps at 63 bytes
7094 // via DNS-1123; this second-axis gate caps the joint
7095 // `lareira-<nome>` chart name at the same 63-byte ceiling. The
7096 // canonical [`crate::lareira_chart_name`] helper's doc comment
7097 // (f7320d7, caixa-core/src/render.rs:3198) explicitly deferred:
7098 // "the M4 admission webhook will pin the joint-length invariant
7099 // when it lands". These tests pin it at the manifest-validate
7100 // layer instead, fail-before-pass-after on the 56-byte boundary.
7101
7102 #[test]
7103 fn validate_nome_chart_name_budget_accepts_canonical_template() {
7104 // Positive control: the bare `feira init`-style template's
7105 // `:nome` ("demo") sits far below the cap; the gate must not
7106 // regress this baseline. Same shape every peer
7107 // value-shape-gate baseline pin uses.
7108 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7109 c.validate_nome_chart_name_budget().unwrap();
7110 }
7111
7112 #[test]
7113 fn validate_nome_chart_name_budget_accepts_canonical_fixtures() {
7114 // Positive-set sweep across the canonical author surface every
7115 // in-tree fixture uses (`hello-rio`, `cart`, `checkout`,
7116 // `worker`, the `checkout-aplicacao` example members, the
7117 // `akeyless-attest` caixa-tatara fixture). Every value sits
7118 // far below the 55-byte per-`:nome` budget. Same shape every
7119 // peer per-axis baseline pin uses.
7120 for nome in [
7121 "hello-rio",
7122 "cart",
7123 "checkout",
7124 "worker",
7125 "akeyless-attest",
7126 "demo",
7127 "a",
7128 ] {
7129 caixa_with_nome(nome)
7130 .validate_nome_chart_name_budget()
7131 .unwrap_or_else(|e| {
7132 panic!("canonical :nome {nome:?} must pass chart-name budget, got {e:?}")
7133 });
7134 }
7135 }
7136
7137 #[test]
7138 fn validate_nome_chart_name_budget_accepts_nome_at_cap() {
7139 // Boundary-accepting case at the 55-byte per-`:nome` budget —
7140 // the joint chart name is exactly 63 bytes, the DNS-1123 label
7141 // cap. Pinned alongside the rejecting-arm test so a future cap
7142 // shift surfaces both arms simultaneously. Mirrors
7143 // `nome_max_length_validates` on the peer bare-`:nome` axis.
7144 let at_cap = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN);
7145 caixa_with_nome(&at_cap)
7146 .validate_nome_chart_name_budget()
7147 .unwrap();
7148 }
7149
7150 #[test]
7151 fn validate_nome_chart_name_budget_rejects_nome_one_over_cap() {
7152 // Fail-before-pass-after pin on the 56-byte boundary: the
7153 // smallest `:nome` length that overflows the joint chart-name
7154 // cap. The inner [`is_dns_1123_label`] gate
7155 // (`Caixa::validate_nome`) accepts it (56 ≤ 63), so prior to
7156 // this gate it silently passed the manifest-validate cascade
7157 // and surfaced as a `helm lint` / apiserver rejection on the
7158 // rendered chart name far from the source `caixa.lisp`, with
7159 // no field naming the overflow. With this gate the diagnostic
7160 // names the offending `:nome` verbatim alongside the rendered
7161 // chart name and the budget, so the author can shorten in one
7162 // edit. Mirrors `validate_nome_rejects_too_long` on the peer
7163 // bare-`:nome` axis.
7164 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7165 let c = caixa_with_nome(&over);
7166 let err = c.validate_nome_chart_name_budget().unwrap_err();
7167 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7168 panic!("expected NomeChartNameBudgetExceeded for over-budget :nome");
7169 };
7170 assert_eq!(nome.len(), crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7171 assert_eq!(nome, over);
7172 assert!(
7173 reason.contains("63") && reason.contains("64") && reason.contains("55"),
7174 "diagnostic must name the DNS-1123 cap (63), the actual chart-name length (64), \
7175 and the per-`:nome` budget (55), got {reason:?}"
7176 );
7177 }
7178
7179 #[test]
7180 fn validate_nome_chart_name_budget_rejects_nome_at_bare_dns_cap() {
7181 // The 63-byte `:nome` boundary — passes the bare-`:nome`
7182 // [`is_dns_1123_label`] cap exactly, but produces a 71-byte
7183 // joint chart name that overflows the DNS-1123 label cap
7184 // structurally. The most stringent fail-before-pass-after
7185 // surface: every `:nome` in the 56..=63-byte range passed the
7186 // prior cascade and broke at admission.
7187 let bare_max = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7188 let c = caixa_with_nome(&bare_max);
7189 // The bare-`:nome` gate accepts the 63-byte length.
7190 c.validate_nome().unwrap();
7191 // The new joint-length gate rejects it.
7192 let err = c.validate_nome_chart_name_budget().unwrap_err();
7193 assert!(
7194 matches!(
7195 err,
7196 ManifestError::NomeChartNameBudgetExceeded { ref nome, .. }
7197 if nome.len() == crate::DNS_1123_LABEL_MAX_LEN
7198 ),
7199 "got {err:?}"
7200 );
7201 }
7202
7203 #[test]
7204 fn validate_nome_chart_name_budget_diagnostic_carries_offending_chart_name() {
7205 // Diagnostic-shape pin: the rendered `lareira-<nome>` chart
7206 // name appears verbatim in the diagnostic so the author sees
7207 // exactly the string the apiserver / `helm lint` would have
7208 // rejected — no re-derivation required to grep the source.
7209 // Peer with `nome_invalid_diagnostic_carries_offending_nome`
7210 // on the bare-`:nome` axis.
7211 let over = "x".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 5);
7212 let c = caixa_with_nome(&over);
7213 let err = c.validate_nome_chart_name_budget().unwrap_err();
7214 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7215 panic!("expected NomeChartNameBudgetExceeded variant");
7216 };
7217 assert_eq!(nome, over);
7218 let expected_chart = crate::lareira_chart_name(&over);
7219 assert!(
7220 reason.contains(&expected_chart),
7221 "diagnostic must carry the rendered chart name {expected_chart:?} verbatim, \
7222 got {reason:?}"
7223 );
7224 assert!(
7225 reason.contains("lareira-"),
7226 "diagnostic must name the canonical chart-name prefix verbatim, got {reason:?}"
7227 );
7228 }
7229
7230 #[test]
7231 fn validate_nome_chart_name_budget_runs_after_nome_shape_via_layout_verify() {
7232 // Order pin on the layout cascade: the narrower
7233 // `NomeInvalid` (bare-DNS-1123 shape) fires before the
7234 // joint-length budget. A structurally-malformed `:nome` (here:
7235 // uppercase) surfaces its specific shape error rather than
7236 // the chart-name-budget error, even when the joint length
7237 // would also overflow — the narrower diagnostic is more
7238 // self-locating. Mirrors the cascade-precedence pins peer
7239 // gates already use (e.g. `EntradaParaEmpty` before
7240 // `EntradaParaInvalid`).
7241 let over = "A".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7242 let c = caixa_with_nome(&over);
7243 // The bare-shape gate fires first.
7244 let err = c.validate_nome().unwrap_err();
7245 assert!(
7246 matches!(err, ManifestError::NomeInvalid { .. }),
7247 "bare-shape gate must fire before chart-name-budget gate; got {err:?}"
7248 );
7249 // And the layout verify cascade surfaces that diagnostic, not
7250 // the budget arm. Inject a path-exists oracle so the cascade
7251 // gets past the manifest-presence check and into the
7252 // value-shape gates.
7253 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7254 let err = crate::LayoutInvariants::verify(
7255 &layout,
7256 &c,
7257 std::path::Path::new("/tmp/caixa-test-fake-root"),
7258 )
7259 .unwrap_err();
7260 let issue = err.to_string();
7261 assert!(
7262 issue.contains("DNS-1123") || issue.contains("uppercase"),
7263 "layout cascade must surface the bare-DNS-1123 diagnostic on a \
7264 structurally-malformed :nome, not the chart-name-budget diagnostic; got {issue:?}"
7265 );
7266 }
7267
7268 #[test]
7269 fn layout_verify_routes_chart_name_budget_through_nome_violation() {
7270 // Cross-axis envelope pin: the layout cascade wraps both
7271 // bare-`:nome` and joint-length-`:nome` failures through the
7272 // same [`LayoutError::NomeViolation`] envelope, since both
7273 // arms are on the `:nome` axis. The user's diagnostic stays
7274 // self-locating ("which axis"), and a future consumer that
7275 // dispatches on the layout-error variant (e.g. a `feira lint`
7276 // exit-code mapping) sees a single per-axis envelope. The
7277 // wrapped `issue:` carries the full inner diagnostic.
7278 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7279 let c = caixa_with_nome(&over);
7280 // The bare-shape gate accepts.
7281 c.validate_nome().unwrap();
7282 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7283 let err = crate::LayoutInvariants::verify(
7284 &layout,
7285 &c,
7286 std::path::Path::new("/tmp/caixa-test-fake-root"),
7287 )
7288 .unwrap_err();
7289 let crate::LayoutError::NomeViolation { caixa, issue } = err else {
7290 panic!("expected LayoutError::NomeViolation, got {err:?}");
7291 };
7292 assert_eq!(caixa, over);
7293 assert!(
7294 issue.contains("lareira-") && issue.contains("63") && issue.contains("55"),
7295 "wrapped issue must carry the joint-length diagnostic verbatim, got {issue:?}"
7296 );
7297 }
7298
7299 // ── Caixa::validate_versao — top-level :versao value-shape gate ─────
7300
7301 fn caixa_with_versao(versao: &str) -> Caixa {
7302 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7303 c.versao = versao.to_string();
7304 c
7305 }
7306
7307 #[test]
7308 fn validate_versao_accepts_canonical_template() {
7309 // Positive control: the bare `feira init`-style template's
7310 // `:versao` ("0.1.0") is a canonical SemVer-2 literal; the gate
7311 // must not regress this baseline shape. A future tightening of
7312 // the accepted set surfaces here as a test failure first.
7313 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7314 c.validate_versao().unwrap();
7315 }
7316
7317 #[test]
7318 fn validate_versao_accepts_canonical_forms() {
7319 // Positive-set sweep: each realistic SemVer-2 shape the
7320 // substrate's downstream consumers accept must pass — bare
7321 // MAJOR.MINOR.PATCH, pre-release tags (`-rc.1`, `-alpha.0`),
7322 // build metadata (`+build.42`), the combined form, and the
7323 // `0.0.0` boundary case. Mirrors `accepts_canonical_forms` on
7324 // the peer `:nome` axis (6c992f8).
7325 for versao in [
7326 "0.1.0",
7327 "0.0.0",
7328 "1.0.0",
7329 "0.2.0-rc.1",
7330 "1.0.0-alpha.0",
7331 "1.0.0+build.42",
7332 "1.0.0-rc.1+build.42",
7333 "10.20.30",
7334 ] {
7335 caixa_with_versao(versao)
7336 .validate_versao()
7337 .unwrap_or_else(|e| {
7338 panic!("canonical :versao {versao:?} must validate, got {e:?}")
7339 });
7340 }
7341 }
7342
7343 #[test]
7344 fn validate_versao_rejects_empty() {
7345 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
7346 // an empty `:versao` (the derive macro stores the raw String);
7347 // the gate's empty arm names the offending axis with a narrower
7348 // diagnostic than the `VersaoInvalid` parse arm would emit.
7349 // Mirrors `validate_nome_rejects_empty` (6c992f8).
7350 let c = caixa_with_versao("");
7351 let err = c.validate_versao().unwrap_err();
7352 assert_eq!(err, ManifestError::VersaoEmpty);
7353 }
7354
7355 #[test]
7356 fn validate_versao_rejects_git_tag_shape() {
7357 // The canonical "I copied the git tag verbatim" footgun —
7358 // `feira publish` *emits* `v<versao>` git tags, so a leaked
7359 // `v0.1.0` in `:versao` would render as `vv0.1.0` and silently
7360 // shift every downstream consumer's version axis. `semver`
7361 // rejects the leading `v` at parse time; the gate moves the
7362 // diagnostic to the source `caixa.lisp`.
7363 let c = caixa_with_versao("v0.1.0");
7364 let err = c.validate_versao().unwrap_err();
7365 let ManifestError::VersaoInvalid { versao, reason } = err else {
7366 panic!("expected VersaoInvalid for git-tag-shape :versao");
7367 };
7368 assert_eq!(versao, "v0.1.0");
7369 assert!(
7370 !reason.is_empty(),
7371 "VersaoInvalid `reason` must carry the parser's wording, got {reason:?}"
7372 );
7373 }
7374
7375 #[test]
7376 fn validate_versao_rejects_missing_patch() {
7377 // The canonical "I shortened it" footgun — SemVer-2 requires
7378 // three parts. Cargo's `version =` field accepts the shortened
7379 // form as a requirement, conflating the two leaks across the
7380 // typed `:deps :versao` vs top-level `:versao` axes; the gate
7381 // pins the top-level axis to the strict three-part shape.
7382 let c = caixa_with_versao("0.1");
7383 let err = c.validate_versao().unwrap_err();
7384 assert!(
7385 matches!(
7386 err,
7387 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1"
7388 ),
7389 "got {err:?}"
7390 );
7391 }
7392
7393 #[test]
7394 fn validate_versao_rejects_requirement_shape() {
7395 // The canonical "I leaked a requirement into a version" footgun —
7396 // the typed `:deps :versao` / `:membros :versao` axes accept
7397 // `^0.1` (a `VersionReq`); the top-level `:versao` requires a
7398 // concrete `Version`. Without this gate the two typed surfaces
7399 // would silently overlap, and a top-level `^0.1` would surface
7400 // at `helm install` time as a Chart.yaml version rejection far
7401 // from the source `caixa.lisp`.
7402 let c = caixa_with_versao("^0.1");
7403 let err = c.validate_versao().unwrap_err();
7404 assert!(
7405 matches!(
7406 err,
7407 ManifestError::VersaoInvalid { ref versao, .. } if versao == "^0.1"
7408 ),
7409 "got {err:?}"
7410 );
7411 }
7412
7413 #[test]
7414 fn validate_versao_rejects_docker_tag_shape() {
7415 // The "I confused it with a docker tag" footgun — `latest`,
7416 // `main`, `stable` parse as identifiers, not SemVer-2 versions.
7417 // SemVer rejects at parse time; the gate moves the diagnostic
7418 // to the source `caixa.lisp`.
7419 for bad in ["latest", "main", "stable"] {
7420 let c = caixa_with_versao(bad);
7421 let err = c.validate_versao().unwrap_err();
7422 assert!(
7423 matches!(
7424 err,
7425 ManifestError::VersaoInvalid { ref versao, .. } if versao == bad
7426 ),
7427 "got {err:?} for {bad:?}"
7428 );
7429 }
7430 }
7431
7432 #[test]
7433 fn validate_versao_rejects_four_part_form() {
7434 // The Java/Microsoft "MAJOR.MINOR.PATCH.BUILD" convention
7435 // SemVer-2 forbids. A leak from a non-SemVer ecosystem; the
7436 // semver crate rejects the extra `.0` at parse time.
7437 let c = caixa_with_versao("0.1.0.0");
7438 let err = c.validate_versao().unwrap_err();
7439 assert!(
7440 matches!(
7441 err,
7442 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1.0.0"
7443 ),
7444 "got {err:?}"
7445 );
7446 }
7447
7448 #[test]
7449 fn versao_empty_takes_precedence_over_invalid() {
7450 // Order pin: the empty arm fires before the parser is consulted.
7451 // Empty < invalid in self-locating-ness — the narrower
7452 // `VersaoEmpty` diagnostic doesn't carry a useless `versao: ""`
7453 // reference into the parser-shaped reason. Mirrors
7454 // `nome_empty_takes_precedence_over_invalid` (6c992f8) on the
7455 // peer axis.
7456 let c = caixa_with_versao("");
7457 assert_eq!(c.validate_versao().unwrap_err(), ManifestError::VersaoEmpty);
7458 }
7459
7460 #[test]
7461 fn versao_invalid_diagnostic_carries_offending_versao() {
7462 // Diagnostic-shape pin: the error names the offending `:versao`
7463 // verbatim with a non-empty parser-shaped reason, so a `feira
7464 // lint` run can render the diagnostic without re-parsing.
7465 // Mirrors `nome_invalid_diagnostic_carries_offending_nome`.
7466 let c = caixa_with_versao("v0.1.0");
7467 let err = c.validate_versao().unwrap_err();
7468 let ManifestError::VersaoInvalid { versao, reason } = err else {
7469 panic!("expected VersaoInvalid variant");
7470 };
7471 assert_eq!(versao, "v0.1.0");
7472 assert!(
7473 !reason.is_empty(),
7474 "VersaoInvalid `reason` must carry the parser's wording verbatim"
7475 );
7476 }
7477
7478 #[test]
7479 fn validate_versao_accepts_what_upgrade_from_from_accepts() {
7480 // Parity pin: every shape `UpgradeFromEntry::validate` accepts
7481 // for `:upgrade-from :from` must also pass `validate_versao` —
7482 // the two `:versao`-typed surfaces (top-level `:versao`,
7483 // `:upgrade-from :from`) consume the *same* `semver::Version`
7484 // parser, so they must agree on the accepted set. Without this
7485 // pin, a future tightening of one axis could silently diverge
7486 // from the other. Mirrors the `:versao` requirement-axis
7487 // parity (`:deps`/`:deps-dev`/`:membros`/`:children`) the prior
7488 // commits established.
7489 for versao in ["0.1.0", "0.2.0-rc.1", "1.0.0+build.42"] {
7490 // From the canonical UpgradeFromEntry round-trip fixture
7491 // (`upgrade::tests::round_trip_load_module` peers).
7492 let entry = crate::UpgradeFromEntry {
7493 from: versao.to_string(),
7494 instructions: Vec::new(),
7495 };
7496 entry
7497 .validate()
7498 .unwrap_or_else(|e| panic!(":from {versao:?} must validate, got {e:?}"));
7499 caixa_with_versao(versao)
7500 .validate_versao()
7501 .unwrap_or_else(|e| {
7502 panic!(":versao {versao:?} must validate, got {e:?} — peer axis diverges")
7503 });
7504 }
7505 }
7506
7507 // ── Caixa::validate_restart_window — supervisor restart-window
7508 // folds through the shared `supervisor::duration_codec` ────────
7509
7510 fn caixa_with_restart_window(window: Option<&str>) -> Caixa {
7511 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
7512 c.kind = CaixaKind::Supervisor;
7513 c.restart_window = window.map(str::to_string);
7514 c
7515 }
7516
7517 #[test]
7518 fn validate_restart_window_accepts_none() {
7519 // The canonical "omit the slot to express no reset" shape — a
7520 // `None` raw string is the absence of the typed
7521 // `:restart-window` slot, which is exactly the SupervisorSpec
7522 // "never reset" semantics. The gate must be a no-op here; a
7523 // future tightening that rejected `None` would force every
7524 // supervisor caixa to authoring-time pin a window even when
7525 // the OTP semantics call for none.
7526 caixa_with_restart_window(None)
7527 .validate_restart_window()
7528 .unwrap();
7529 }
7530
7531 #[test]
7532 fn validate_restart_window_accepts_canonical_forms() {
7533 // Positive-set sweep across the canonical authoring units the
7534 // shared `supervisor::duration_codec::parse` accepts —
7535 // matches the codec-side `parse_accepts_integer_canonical_units`
7536 // pin in supervisor::tests so a future codec-side tightening
7537 // surfaces simultaneously on both axes.
7538 for window in ["60s", "5m", "1h", "500ms", "30", "0s"] {
7539 caixa_with_restart_window(Some(window))
7540 .validate_restart_window()
7541 .unwrap_or_else(|e| {
7542 panic!("canonical :restart-window {window:?} must validate, got {e:?}")
7543 });
7544 }
7545 }
7546
7547 #[test]
7548 fn validate_restart_window_rejects_fractional_seconds() {
7549 // Fail-before-pass-after pin: the `"1.5s"` drift class (parses
7550 // as f64 to 1.5 → renders back as `"1500ms"` on first
7551 // serialize). Prior to the fold + this gate, the inline
7552 // `parse_window_inline` accepted f64 magnitudes and silently
7553 // produced a `Duration::from_secs_f64(1.5)`, divergent from
7554 // the shared codec's integer-magnitude discipline on the
7555 // serde-routed siblings. The gate now surfaces a self-locating
7556 // diagnostic at the manifest layer.
7557 let err = caixa_with_restart_window(Some("1.5s"))
7558 .validate_restart_window()
7559 .unwrap_err();
7560 let ManifestError::RestartWindowMalformed {
7561 restart_window,
7562 reason,
7563 } = err
7564 else {
7565 panic!("expected RestartWindowMalformed for fractional seconds");
7566 };
7567 assert_eq!(restart_window, "1.5s");
7568 assert!(
7569 reason.contains("\"1.5\"") && reason.contains("not a non-negative integer"),
7570 "diagnostic must carry shared-codec wording, got {reason:?}"
7571 );
7572 }
7573
7574 #[test]
7575 fn validate_restart_window_rejects_decimal_shaped_integer() {
7576 // The `"1.0s"` class — numerically `1s` exactly, but the
7577 // canonical form is `"1s"` not `"1.0s"`. Decimal-shape leak
7578 // gets the same canonical-form diagnostic.
7579 let err = caixa_with_restart_window(Some("1.0s"))
7580 .validate_restart_window()
7581 .unwrap_err();
7582 assert!(
7583 matches!(
7584 err,
7585 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7586 if restart_window == "1.0s"
7587 ),
7588 "got {err:?}"
7589 );
7590 }
7591
7592 #[test]
7593 fn validate_restart_window_rejects_half_unit_minute() {
7594 // `"0.5m"` is the unit-fraction footgun — author writes a
7595 // human-readable half-minute, the prior inline parser silently
7596 // produced `Duration::from_secs_f64(30.0)` and serde
7597 // re-emitted as `"30s"`, rewriting author intent. The gate
7598 // closes the loop at the manifest layer.
7599 let err = caixa_with_restart_window(Some("0.5m"))
7600 .validate_restart_window()
7601 .unwrap_err();
7602 let ManifestError::RestartWindowMalformed {
7603 restart_window,
7604 reason,
7605 } = err
7606 else {
7607 panic!("expected RestartWindowMalformed");
7608 };
7609 assert_eq!(restart_window, "0.5m");
7610 assert!(
7611 reason.contains("\"30s\""),
7612 "diagnostic must point at the canonical-form remediation, got {reason:?}"
7613 );
7614 }
7615
7616 #[test]
7617 fn validate_restart_window_rejects_leading_sign() {
7618 // `"+30s"` and `"-30s"` both round-tripped through f64 cleanly
7619 // on the prior parser (`+30` parses as `30.0`; `-30` parsed
7620 // and was caught by the `num < 0.0` arm which silently
7621 // returned `None`, dropping the author-supplied window). The
7622 // shared codec's digit-only gate rejects both with a unified
7623 // canonical-form diagnostic; the manifest-layer wrapper names
7624 // the offending value.
7625 for bad in ["+30s", "-30s"] {
7626 let err = caixa_with_restart_window(Some(bad))
7627 .validate_restart_window()
7628 .unwrap_err();
7629 assert!(
7630 matches!(
7631 err,
7632 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7633 if restart_window == bad
7634 ),
7635 "got {err:?} for {bad:?}"
7636 );
7637 }
7638 }
7639
7640 #[test]
7641 fn validate_restart_window_rejects_unknown_unit() {
7642 // `"30x"` — the typo / wrong-unit footgun. The shared codec's
7643 // unit dispatch surfaces an `unknown duration unit` reason;
7644 // the manifest-layer wrapper names the offending value.
7645 let err = caixa_with_restart_window(Some("30x"))
7646 .validate_restart_window()
7647 .unwrap_err();
7648 let ManifestError::RestartWindowMalformed {
7649 restart_window,
7650 reason,
7651 } = err
7652 else {
7653 panic!("expected RestartWindowMalformed for unknown unit");
7654 };
7655 assert_eq!(restart_window, "30x");
7656 assert!(
7657 reason.contains("unknown duration unit"),
7658 "diagnostic must carry shared-codec unit-rejection wording, got {reason:?}"
7659 );
7660 }
7661
7662 #[test]
7663 fn validate_restart_window_rejects_garbage() {
7664 // Pure non-numeric magnitude (`"abc"`) falls through to the
7665 // shared codec's narrower `"bad duration magnitude"` arm. Same
7666 // diagnostic shape as the codec-side
7667 // `parse_garbage_still_falls_through_to_bad_magnitude` pin.
7668 let err = caixa_with_restart_window(Some("abc"))
7669 .validate_restart_window()
7670 .unwrap_err();
7671 let ManifestError::RestartWindowMalformed {
7672 restart_window,
7673 reason,
7674 } = err
7675 else {
7676 panic!("expected RestartWindowMalformed for garbage");
7677 };
7678 assert_eq!(restart_window, "abc");
7679 assert!(
7680 reason.contains("bad duration magnitude"),
7681 "diagnostic must carry shared-codec garbage-rejection wording, got {reason:?}"
7682 );
7683 }
7684
7685 #[test]
7686 fn validate_restart_window_rejects_empty_string() {
7687 // The empty-after-trim edge case — distinct from the `None`
7688 // canonical "omit the slot" shape. The shared codec's
7689 // digit-only gate refuses an empty magnitude; the manifest
7690 // layer names the offending `""` so the author can grep for
7691 // the literal empty value in their `caixa.lisp` and either
7692 // remove the slot (the canonical "no reset" shape) or pin a
7693 // positive duration.
7694 let err = caixa_with_restart_window(Some(""))
7695 .validate_restart_window()
7696 .unwrap_err();
7697 assert!(
7698 matches!(
7699 err,
7700 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7701 if restart_window.is_empty()
7702 ),
7703 "got {err:?}"
7704 );
7705 }
7706
7707 #[test]
7708 fn validate_restart_window_diagnostic_carries_offending_value() {
7709 // Diagnostic-shape pin (peer with
7710 // `nome_invalid_diagnostic_carries_offending_nome` /
7711 // `versao_invalid_diagnostic_carries_offending_versao`): the
7712 // error names the offending raw `:restart-window` verbatim
7713 // with a non-empty shared-codec-shaped reason, so a `feira
7714 // lint` run can render the diagnostic without re-parsing.
7715 let err = caixa_with_restart_window(Some("1.5s"))
7716 .validate_restart_window()
7717 .unwrap_err();
7718 let ManifestError::RestartWindowMalformed {
7719 restart_window,
7720 reason,
7721 } = err
7722 else {
7723 panic!("expected RestartWindowMalformed variant");
7724 };
7725 assert_eq!(restart_window, "1.5s");
7726 assert!(
7727 !reason.is_empty(),
7728 "RestartWindowMalformed `reason` must carry the codec's wording verbatim"
7729 );
7730 }
7731
7732 #[test]
7733 fn supervisor_view_folds_through_shared_codec_on_canonical_form() {
7734 // Behavioral parity pin after the fold (`parse_window_inline`
7735 // deletion): the canonical `"60s"` still produces
7736 // `Duration::from_secs(60)` on the typed view — the fold is
7737 // semantically equivalent to the prior inline parser on the
7738 // accepted set. Mirrors the pre-fold `supervisor_view_returns_typed_shape`
7739 // pin, narrowed to the parser-side contract.
7740 let c = caixa_with_restart_window(Some("60s"));
7741 let view = c.supervisor_view().expect("Supervisor kind has a view");
7742 assert_eq!(
7743 view.restart_window,
7744 Some(std::time::Duration::from_secs(60))
7745 );
7746 }
7747
7748 #[test]
7749 fn supervisor_view_soft_swallows_what_validate_rejects() {
7750 // Parity pin between the view-construction path and the
7751 // manifest-level validator: the same `"1.5s"` that surfaces
7752 // `RestartWindowMalformed` at `validate_restart_window` time
7753 // becomes `restart_window: None` on the typed view (the fold
7754 // preserves the existing best-effort shape of `supervisor_view`).
7755 // The contract is: a layout-verifier / `feira lint` flow that
7756 // cares about the malformed-window axis MUST consult
7757 // `validate_restart_window` — relying solely on the view's
7758 // `None` swallows the diagnostic silently. This pin makes the
7759 // expectation a typed invariant.
7760 let c = caixa_with_restart_window(Some("1.5s"));
7761 let view = c.supervisor_view().expect("Supervisor kind has a view");
7762 assert_eq!(
7763 view.restart_window, None,
7764 "view-construction path soft-swallows the parse error to None"
7765 );
7766 // And the manifest-level validator does NOT soft-swallow:
7767 assert!(
7768 matches!(
7769 c.validate_restart_window().unwrap_err(),
7770 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7771 if restart_window == "1.5s"
7772 ),
7773 "validator must surface the offending value",
7774 );
7775 }
7776
7777 // ── validate_code_paths — per-entry shape on :bibliotecas / :exe / :servicos ──
7778
7779 fn caixa_with_code_paths(bibliotecas: Vec<&str>, exe: Vec<&str>, servicos: Vec<&str>) -> Caixa {
7780 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7781 c.bibliotecas = bibliotecas.into_iter().map(String::from).collect();
7782 c.exe = exe.into_iter().map(String::from).collect();
7783 c.servicos = servicos.into_iter().map(String::from).collect();
7784 c
7785 }
7786
7787 #[test]
7788 fn validate_code_paths_accepts_canonical_template() {
7789 // The bare `Caixa::template` shape is the gate's identity element
7790 // on the canonical authoring shape — `:bibliotecas
7791 // ("lib/demo.lisp")` + empty `:exe` + empty `:servicos`. Pins
7792 // that the gate is non-disruptive against every existing caixa.
7793 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7794 c.validate_code_paths().unwrap();
7795 }
7796
7797 #[test]
7798 fn validate_code_paths_accepts_explicit_relative_paths_on_every_slot() {
7799 // Positive control sweep: a canonical-shaped path on every slot
7800 // passes. Mirrors the peer
7801 // `behavior::validate_every_slot_relative_is_ok` pin.
7802 let c = caixa_with_code_paths(
7803 vec!["lib/demo.lisp", "lib/helpers.lisp"],
7804 vec!["exe/demo", "exe/tool"],
7805 vec!["servicos/demo.computeunit.yaml"],
7806 );
7807 c.validate_code_paths().unwrap();
7808 }
7809
7810 #[test]
7811 fn validate_code_paths_accepts_all_empty_lists() {
7812 // The empty-list identity element: every Caixa with no declared
7813 // code paths trivially passes (Supervisor / Aplicacao kinds rely
7814 // on this — the OwnCode gate already rejected them before the
7815 // path-shape gate runs in the layout, but the validator itself
7816 // must accept the empty shape).
7817 let c = caixa_with_code_paths(vec![], vec![], vec![]);
7818 c.validate_code_paths().unwrap();
7819 }
7820
7821 #[test]
7822 fn validate_code_paths_rejects_empty_bibliotecas_entry() {
7823 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
7824 let err = c.validate_code_paths().unwrap_err();
7825 assert!(
7826 matches!(
7827 err,
7828 ManifestError::CodePathEmpty {
7829 slot: ":bibliotecas"
7830 }
7831 ),
7832 "got {err:?}",
7833 );
7834 }
7835
7836 #[test]
7837 fn validate_code_paths_rejects_empty_exe_entry() {
7838 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
7839 let err = c.validate_code_paths().unwrap_err();
7840 assert!(
7841 matches!(err, ManifestError::CodePathEmpty { slot: ":exe" }),
7842 "got {err:?}",
7843 );
7844 }
7845
7846 #[test]
7847 fn validate_code_paths_rejects_empty_servicos_entry() {
7848 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
7849 let err = c.validate_code_paths().unwrap_err();
7850 assert!(
7851 matches!(err, ManifestError::CodePathEmpty { slot: ":servicos" }),
7852 "got {err:?}",
7853 );
7854 }
7855
7856 #[test]
7857 fn validate_code_paths_rejects_absolute_bibliotecas_entry() {
7858 // `:bibliotecas` has no `starts_with(<dir>)` fence downstream,
7859 // so an absolute path that resolves on disk silently passes the
7860 // layout's existence check — the canonical sandbox-escape on
7861 // the biblioteca axis.
7862 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7863 let err = c.validate_code_paths().unwrap_err();
7864 let ManifestError::CodePathAbsolute { slot, path } = err else {
7865 panic!("expected CodePathAbsolute, got {err:?}");
7866 };
7867 assert_eq!(slot, ":bibliotecas");
7868 assert_eq!(path, PathBuf::from("/etc/passwd"));
7869 }
7870
7871 #[test]
7872 fn validate_code_paths_rejects_absolute_exe_entry() {
7873 let c = caixa_with_code_paths(vec![], vec!["/usr/bin/env"], vec![]);
7874 let err = c.validate_code_paths().unwrap_err();
7875 let ManifestError::CodePathAbsolute { slot, path } = err else {
7876 panic!("expected CodePathAbsolute, got {err:?}");
7877 };
7878 assert_eq!(slot, ":exe");
7879 assert_eq!(path, PathBuf::from("/usr/bin/env"));
7880 }
7881
7882 #[test]
7883 fn validate_code_paths_rejects_absolute_servicos_entry() {
7884 let c = caixa_with_code_paths(vec![], vec![], vec!["/var/servicos/x.yaml"]);
7885 let err = c.validate_code_paths().unwrap_err();
7886 let ManifestError::CodePathAbsolute { slot, path } = err else {
7887 panic!("expected CodePathAbsolute, got {err:?}");
7888 };
7889 assert_eq!(slot, ":servicos");
7890 assert_eq!(path, PathBuf::from("/var/servicos/x.yaml"));
7891 }
7892
7893 #[test]
7894 fn validate_code_paths_rejects_parent_escape_bibliotecas_leading() {
7895 // Canonical "I want a lib from a sibling caixa" footgun on the
7896 // biblioteca axis. `:bibliotecas` has no `starts_with` fence
7897 // downstream, so a leading `..` traverses to the parent of the
7898 // caixa root with no diagnostic at layout time if the resolved
7899 // target exists.
7900 let c = caixa_with_code_paths(vec!["../sibling/x.lisp"], vec![], vec![]);
7901 let err = c.validate_code_paths().unwrap_err();
7902 let ManifestError::CodePathParentEscape { slot, path } = err else {
7903 panic!("expected CodePathParentEscape, got {err:?}");
7904 };
7905 assert_eq!(slot, ":bibliotecas");
7906 assert_eq!(path, PathBuf::from("../sibling/x.lisp"));
7907 }
7908
7909 #[test]
7910 fn validate_code_paths_rejects_parent_escape_exe_mid_path() {
7911 // Mid-path `..` defeats the layout's component-aware
7912 // `starts_with(exe_dir)` fence — `root.join("exe/../../escape")`
7913 // `starts_with(<root>/exe)` is true, but the canonical resolution
7914 // lives outside the caixa root. Caught regardless of where the
7915 // `..` sits — mirrors the peer
7916 // `behavior::validate_rejects_parent_escape_mid_path` pin.
7917 let c = caixa_with_code_paths(vec![], vec!["exe/../../escape"], vec![]);
7918 let err = c.validate_code_paths().unwrap_err();
7919 let ManifestError::CodePathParentEscape { slot, path } = err else {
7920 panic!("expected CodePathParentEscape, got {err:?}");
7921 };
7922 assert_eq!(slot, ":exe");
7923 assert_eq!(path, PathBuf::from("exe/../../escape"));
7924 }
7925
7926 #[test]
7927 fn validate_code_paths_rejects_parent_escape_servicos_trailing() {
7928 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/foo/../../escape.yaml"]);
7929 let err = c.validate_code_paths().unwrap_err();
7930 let ManifestError::CodePathParentEscape { slot, path } = err else {
7931 panic!("expected CodePathParentEscape, got {err:?}");
7932 };
7933 assert_eq!(slot, ":servicos");
7934 assert_eq!(path, PathBuf::from("servicos/foo/../../escape.yaml"));
7935 }
7936
7937 #[test]
7938 fn validate_code_paths_cross_slot_precedence_bibliotecas_before_exe_before_servicos() {
7939 // Cross-slot precedence pin: `:bibliotecas` → `:exe` →
7940 // `:servicos`. A manifest with malformed entries on all three
7941 // surfaces surfaces the `:bibliotecas` defect first, mirroring
7942 // the canonical declaration order
7943 // `Caixa::declared_foreign_code_slots` already establishes for
7944 // the foreign-code-slot diagnostic.
7945 let c = caixa_with_code_paths(vec![""], vec![""], vec![""]);
7946 let err = c.validate_code_paths().unwrap_err();
7947 assert!(
7948 matches!(
7949 err,
7950 ManifestError::CodePathEmpty {
7951 slot: ":bibliotecas"
7952 }
7953 ),
7954 "got {err:?}",
7955 );
7956 }
7957
7958 #[test]
7959 fn validate_code_paths_within_slot_precedence_empty_before_absolute_before_parent_escape() {
7960 // Within-slot precedence pin: empty → absolute → parent-escape,
7961 // matching the [`PathShapeViolation`] arm-ordering every peer
7962 // `is_sandboxed_relative_path` caller follows (b0c8389
7963 // BehaviorSpec, 26da2c7 UpgradeInstruction::StateChange). A
7964 // `:bibliotecas` list whose first entry is empty *and* whose
7965 // later entries are absolute/parent-escape surfaces the empty
7966 // arm first, on the lexicographically-earliest offending entry.
7967 let c = caixa_with_code_paths(vec!["", "/etc/passwd", "../escape.lisp"], vec![], vec![]);
7968 let err = c.validate_code_paths().unwrap_err();
7969 assert!(
7970 matches!(
7971 err,
7972 ManifestError::CodePathEmpty {
7973 slot: ":bibliotecas"
7974 }
7975 ),
7976 "got {err:?}",
7977 );
7978 }
7979
7980 #[test]
7981 fn validate_code_paths_first_offender_per_slot_wins() {
7982 // Within a single slot, the first declaration-order offender
7983 // surfaces — pins that the gate is left-to-right deterministic
7984 // (peer of every `*_first_collision_*` pin on duplicate gates).
7985 let c = caixa_with_code_paths(
7986 vec!["lib/ok.lisp", "/etc/escape", "../also-escape"],
7987 vec![],
7988 vec![],
7989 );
7990 let err = c.validate_code_paths().unwrap_err();
7991 let ManifestError::CodePathAbsolute { slot, path } = err else {
7992 panic!("expected CodePathAbsolute, got {err:?}");
7993 };
7994 assert_eq!(slot, ":bibliotecas");
7995 assert_eq!(path, PathBuf::from("/etc/escape"));
7996 }
7997
7998 #[test]
7999 fn validate_code_paths_diagnostic_carries_offending_slot_and_path() {
8000 // Diagnostic-shape pin (peer with
8001 // `nome_invalid_diagnostic_carries_offending_nome` /
8002 // `versao_invalid_diagnostic_carries_offending_versao`): the
8003 // error's Display surfaces both the offending `:slot` tag and
8004 // the offending path verbatim, so a `feira lint` run can render
8005 // the diagnostic without re-parsing.
8006 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8007 let rendered = c.validate_code_paths().unwrap_err().to_string();
8008 assert!(
8009 rendered.contains(":bibliotecas"),
8010 "diagnostic must name the offending slot: {rendered}",
8011 );
8012 assert!(
8013 rendered.contains("/etc/passwd"),
8014 "diagnostic must quote the offending path: {rendered}",
8015 );
8016 }
8017
8018 #[test]
8019 fn validate_code_paths_rejects_duplicate_bibliotecas_entry() {
8020 // Canonical copy-paste-the-wrong-file footgun on the biblioteca
8021 // axis. Without the gate `feira build` re-parses the same lib
8022 // twice, wasting work and silently masking the author's intent
8023 // to declare a *second* biblioteca.
8024 let c = caixa_with_code_paths(vec!["lib/demo.lisp", "lib/demo.lisp"], vec![], vec![]);
8025 let err = c.validate_code_paths().unwrap_err();
8026 let ManifestError::CodePathDuplicate { slot, path } = err else {
8027 panic!("expected CodePathDuplicate, got {err:?}");
8028 };
8029 assert_eq!(slot, ":bibliotecas");
8030 assert_eq!(path, PathBuf::from("lib/demo.lisp"));
8031 }
8032
8033 #[test]
8034 fn validate_code_paths_rejects_duplicate_exe_entry() {
8035 // Same footgun on the Binario surface. The future `caixa-flake`
8036 // emitter that materializes each `:exe` entry as a flake
8037 // `packages.<name>` derivation would collide on the duplicate
8038 // package key — surfaced here at the typed-validate layer with a
8039 // self-locating diagnostic instead.
8040 let c = caixa_with_code_paths(vec![], vec!["exe/cli", "exe/cli"], vec![]);
8041 let err = c.validate_code_paths().unwrap_err();
8042 let ManifestError::CodePathDuplicate { slot, path } = err else {
8043 panic!("expected CodePathDuplicate, got {err:?}");
8044 };
8045 assert_eq!(slot, ":exe");
8046 assert_eq!(path, PathBuf::from("exe/cli"));
8047 }
8048
8049 #[test]
8050 fn validate_code_paths_rejects_duplicate_servicos_entry() {
8051 // Same footgun on the Servico surface. The peer caixa-helm /
8052 // caixa-flux renderers refuse `:servicos.len() != 1` with the
8053 // narrower `UnsupportedServicoCount` diagnostic, but that
8054 // diagnostic surfaces "too many servicos" without naming
8055 // "duplicate entry" — the typed self-locating framing only lands
8056 // at this gate.
8057 let c = caixa_with_code_paths(
8058 vec![],
8059 vec![],
8060 vec![
8061 "servicos/demo.computeunit.yaml",
8062 "servicos/demo.computeunit.yaml",
8063 ],
8064 );
8065 let err = c.validate_code_paths().unwrap_err();
8066 let ManifestError::CodePathDuplicate { slot, path } = err else {
8067 panic!("expected CodePathDuplicate, got {err:?}");
8068 };
8069 assert_eq!(slot, ":servicos");
8070 assert_eq!(path, PathBuf::from("servicos/demo.computeunit.yaml"));
8071 }
8072
8073 #[test]
8074 fn validate_code_paths_accepts_same_path_across_slots() {
8075 // Per-list scope pin: a `:bibliotecas` entry that happens to
8076 // collide with an `:exe` or `:servicos` entry as a *string* is
8077 // not a duplicate by this gate (each list gets its own HashSet),
8078 // mirroring the peer `:deps` ↔ `:deps-dev` per-list scope
8079 // (a `:nome` present in both lists is a legitimate dev-vs-runtime
8080 // shape on the dep axis). The structural `starts_with(<exe |
8081 // servicos>_dir)` fence at layout time prevents the realistic
8082 // cross-slot collision case from existing on disk, but the gate's
8083 // per-list scope is correct independent of that downstream fence.
8084 let c = caixa_with_code_paths(
8085 vec!["lib/x.lisp"],
8086 vec!["exe/x"],
8087 vec!["servicos/x.computeunit.yaml"],
8088 );
8089 c.validate_code_paths().unwrap();
8090 }
8091
8092 #[test]
8093 fn validate_code_paths_duplicate_fires_after_structural_checks_on_same_slot() {
8094 // Within-slot ordering pin: structural defects (empty / absolute
8095 // / parent-escape) fire before the duplicate gate on the same
8096 // slot. A `:bibliotecas ("" "lib/x.lisp" "lib/x.lisp")` shape
8097 // surfaces the narrower `CodePathEmpty` for the empty entry
8098 // first, not the duplicate on the later pair — same arm-ordering
8099 // every peer per-list duplicate gate uses (`:etiquetas` 360a499,
8100 // `:autores` 86c769b, `:deps` 359fba5).
8101 let c = caixa_with_code_paths(vec!["", "lib/x.lisp", "lib/x.lisp"], vec![], vec![]);
8102 let err = c.validate_code_paths().unwrap_err();
8103 assert!(
8104 matches!(
8105 err,
8106 ManifestError::CodePathEmpty {
8107 slot: ":bibliotecas"
8108 }
8109 ),
8110 "got {err:?}",
8111 );
8112 }
8113
8114 #[test]
8115 fn validate_code_paths_duplicate_in_bibliotecas_fires_before_duplicate_in_exe() {
8116 // Cross-slot ordering pin on the duplicate arm: `:bibliotecas`
8117 // duplicates surface before `:exe` duplicates, matching the
8118 // canonical `:bibliotecas` → `:exe` → `:servicos` declaration
8119 // order every peer per-slot diagnostic on this surface follows.
8120 let c = caixa_with_code_paths(
8121 vec!["lib/x.lisp", "lib/x.lisp"],
8122 vec!["exe/y", "exe/y"],
8123 vec![],
8124 );
8125 let err = c.validate_code_paths().unwrap_err();
8126 let ManifestError::CodePathDuplicate { slot, path } = err else {
8127 panic!("expected CodePathDuplicate, got {err:?}");
8128 };
8129 assert_eq!(slot, ":bibliotecas");
8130 assert_eq!(path, PathBuf::from("lib/x.lisp"));
8131 }
8132
8133 #[test]
8134 fn validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path() {
8135 // Diagnostic-shape pin (peer with
8136 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8137 // on the structural arm): the duplicate-arm Display surfaces both
8138 // the offending `:slot` tag and the offending path verbatim, so a
8139 // `feira lint` run can render the diagnostic without re-parsing.
8140 let c = caixa_with_code_paths(
8141 vec![],
8142 vec![],
8143 vec![
8144 "servicos/demo.computeunit.yaml",
8145 "servicos/demo.computeunit.yaml",
8146 ],
8147 );
8148 let rendered = c.validate_code_paths().unwrap_err().to_string();
8149 assert!(
8150 rendered.contains(":servicos"),
8151 "diagnostic must name the offending slot: {rendered}",
8152 );
8153 assert!(
8154 rendered.contains("servicos/demo.computeunit.yaml"),
8155 "diagnostic must quote the offending path: {rendered}",
8156 );
8157 }
8158
8159 // ── validate_code_paths — `.lisp` extension gate on :bibliotecas ──
8160 //
8161 // The lifted [`crate::render::is_lisp_extension`] predicate (33cc830)
8162 // now gates `:bibliotecas` entries on the tatara-lisp-source file-type
8163 // contract. The `feira build` loop (`caixa-feira/src/cmd/build.rs:33`)
8164 // reads every declared `:bibliotecas` entry through `tatara_lisp::read`
8165 // at parse time — the same downstream consumer the peer `:behavior
8166 // :on-*` (c97815a, [`crate::BehaviorError::NonLispExtension`]) and
8167 // `:upgrade-from :state-change :script` (33cc830,
8168 // [`crate::UpgradeError::NonLispExtensionScript`]) axes route through.
8169 // `:exe` and `:servicos` are deliberately excluded — `:exe` is the
8170 // nix-built executable surface (`"exe/<name>"` shape per the canonical
8171 // [`crate::LayoutError::ExeOutsideDir`] error message and every
8172 // in-tree `caixa_with_code_paths` positive control), and `:servicos`
8173 // is the `.computeunit.yaml` ComputeUnit-CR axis.
8174
8175 #[test]
8176 fn validate_code_paths_rejects_no_extension_bibliotecas_entry() {
8177 // Canonical "I dragged the wrong file from the workspace tree"
8178 // footgun on the biblioteca axis. Without the gate `feira build`
8179 // hands the extensionless path to `tatara_lisp::read` and fails
8180 // with a parser-shaped diagnostic far from the source caixa.lisp,
8181 // with no field naming the offending `:bibliotecas` entry.
8182 for relpath in ["lib/demo", "demo", "lib/handlers/inner"] {
8183 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8184 let err = c.validate_code_paths().unwrap_err();
8185 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8186 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8187 };
8188 assert_eq!(slot, ":bibliotecas");
8189 assert_eq!(path, PathBuf::from(relpath));
8190 }
8191 }
8192
8193 #[test]
8194 fn validate_code_paths_rejects_wrong_extension_bibliotecas_entry() {
8195 // Wrong-extension sweep across common authoring footguns. Same
8196 // sweep posture as the peer
8197 // `behavior::validate_rejects_wrong_extension` (c97815a) and
8198 // `upgrade::tests::state_change_rejects_wrong_extension_script`
8199 // (33cc830) cases.
8200 for relpath in [
8201 "lib/demo.rs",
8202 "lib/demo.txt",
8203 "lib/demo.md",
8204 "lib/demo.json",
8205 "lib/demo.yaml",
8206 "lib/demo.toml",
8207 "lib/demo.lisp.bak",
8208 "lib/demo.lispx",
8209 "lib/demo.lis",
8210 ] {
8211 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8212 let err = c.validate_code_paths().unwrap_err();
8213 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8214 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8215 };
8216 assert_eq!(slot, ":bibliotecas");
8217 assert_eq!(path, PathBuf::from(relpath));
8218 }
8219 }
8220
8221 #[test]
8222 fn validate_code_paths_rejects_case_folded_extension_bibliotecas_entry() {
8223 // Case-sensitivity sweep — pins the strict lowercase `.lisp`
8224 // contract. An uppercase `.LISP` shape that the layout's existence
8225 // check would (case-insensitively, on case-insensitive volumes)
8226 // match the on-disk file still mismatches the canonical form the
8227 // codec emits, breaking the THEORY.md §V.2.7 render-determinism
8228 // contract. Mirrors the peer
8229 // `behavior::validate_rejects_case_folded_extension` (c97815a) and
8230 // `upgrade::tests::state_change_rejects_case_folded_extension_script`
8231 // (33cc830) sweeps.
8232 for relpath in [
8233 "lib/demo.LISP",
8234 "lib/demo.Lisp",
8235 "lib/demo.LiSp",
8236 "lib/demo.lISP",
8237 ] {
8238 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8239 let err = c.validate_code_paths().unwrap_err();
8240 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8241 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8242 };
8243 assert_eq!(slot, ":bibliotecas");
8244 assert_eq!(path, PathBuf::from(relpath));
8245 }
8246 }
8247
8248 #[test]
8249 fn validate_code_paths_accepts_canonical_lisp_shapes() {
8250 // Positive-control sweep through every canonical authoring shape
8251 // every in-tree fixture and the `Caixa::template` scaffold use.
8252 // Mirrors the peer `behavior::validate_accepts_canonical_lisp_paths`
8253 // (c97815a) and the lifted predicate's own
8254 // `is_lisp_extension_accepts_canonical_shapes` sweep in render.rs
8255 // (33cc830).
8256 for relpath in [
8257 "lib/demo.lisp",
8258 "lib/handlers.lisp",
8259 "lib/migrations/v01-to-v02.lisp",
8260 "demo.lisp",
8261 "a.lisp",
8262 "./lib/demo.lisp",
8263 "lib/./handlers.lisp",
8264 "lib/migrations/v.0.1.lisp",
8265 ] {
8266 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8267 c.validate_code_paths()
8268 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8269 }
8270 }
8271
8272 #[test]
8273 fn validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos() {
8274 // The file-type gate is per-slot — only `:bibliotecas` carries the
8275 // tatara-lisp-source contract. An extensionless `:exe` entry
8276 // (`exe/demo`) and a `.computeunit.yaml` `:servicos` entry are the
8277 // canonical shapes every in-tree fixture uses, and must continue
8278 // to pass validate. Pins that a future tightening that broadens
8279 // the `.lisp` gate to either axis surfaces as a test failure
8280 // rather than as a silent breaking change to existing valid
8281 // manifests.
8282 let c = caixa_with_code_paths(
8283 vec![],
8284 vec!["exe/demo", "exe/tool"],
8285 vec!["servicos/demo.computeunit.yaml"],
8286 );
8287 c.validate_code_paths().unwrap();
8288 }
8289
8290 #[test]
8291 fn validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension() {
8292 // Cross-arm precedence pin: a `:bibliotecas` entry that is *both*
8293 // sandbox-escaping and non-`.lisp` surfaces the more fundamental
8294 // sandbox-shape diagnostic first (the `.lisp` remediation would
8295 // be misleading when the offending path can never resolve under
8296 // the caixa root anyway). Mirrors the peer
8297 // `EmptyPath` → `AbsolutePath` → `ParentEscape` → `NonLispExtension`
8298 // ordering on `:behavior :on-*` (c97815a) and `EmptyScript` →
8299 // `AbsoluteScript` → `ParentEscapeScript` → `NonLispExtensionScript`
8300 // on `:upgrade-from :state-change :script` (33cc830).
8301 //
8302 // Empty wins (the strictly-smaller-scope structural arm).
8303 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
8304 assert!(
8305 matches!(
8306 c.validate_code_paths().unwrap_err(),
8307 ManifestError::CodePathEmpty {
8308 slot: ":bibliotecas"
8309 }
8310 ),
8311 "empty must win over non-lisp-extension",
8312 );
8313 // Absolute wins (the path can't resolve under the caixa root).
8314 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8315 let err = c.validate_code_paths().unwrap_err();
8316 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8317 panic!("absolute must win over non-lisp-extension, got {err:?}");
8318 };
8319 assert_eq!(slot, ":bibliotecas");
8320 // ParentEscape wins (the path escapes the caixa root).
8321 let c = caixa_with_code_paths(vec!["../sibling/x.txt"], vec![], vec![]);
8322 let err = c.validate_code_paths().unwrap_err();
8323 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8324 panic!("parent-escape must win over non-lisp-extension, got {err:?}");
8325 };
8326 assert_eq!(slot, ":bibliotecas");
8327 }
8328
8329 #[test]
8330 fn validate_code_paths_non_lisp_extension_precedes_duplicate() {
8331 // Within-slot precedence pin: the per-entry file-type shape gate
8332 // fires before the cross-entry duplicate gate, so the narrower
8333 // structural defect dominates the uniqueness diagnostic. A
8334 // `("lib/x.txt" "lib/x.txt")` shape surfaces
8335 // `CodePathNonLispExtension` on the first entry rather than
8336 // `CodePathDuplicate` on the pair — same posture every per-entry
8337 // shape-gate-precedes-duplicate cascade follows on this surface
8338 // (the empty / absolute / parent-escape arms already precede the
8339 // duplicate arm; the lifted file-type arm joins that set).
8340 let c = caixa_with_code_paths(vec!["lib/x.txt", "lib/x.txt"], vec![], vec![]);
8341 let err = c.validate_code_paths().unwrap_err();
8342 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8343 panic!("expected CodePathNonLispExtension, got {err:?}");
8344 };
8345 assert_eq!(slot, ":bibliotecas");
8346 assert_eq!(path, PathBuf::from("lib/x.txt"));
8347 }
8348
8349 #[test]
8350 fn validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path() {
8351 // Diagnostic-shape pin (peer with
8352 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8353 // on the sandbox-shape arms and
8354 // `validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path`
8355 // on the duplicate arm): the file-type-arm Display surfaces both
8356 // the offending `:slot` tag, the offending path verbatim, and the
8357 // expected `.lisp` extension named in the remediation text, so a
8358 // `feira lint` run can render the diagnostic without re-parsing.
8359 let c = caixa_with_code_paths(vec!["lib/demo.rs"], vec![], vec![]);
8360 let rendered = c.validate_code_paths().unwrap_err().to_string();
8361 assert!(
8362 rendered.contains(":bibliotecas"),
8363 "diagnostic must name the offending slot: {rendered}",
8364 );
8365 assert!(
8366 rendered.contains("lib/demo.rs"),
8367 "diagnostic must quote the offending path: {rendered}",
8368 );
8369 assert!(
8370 rendered.contains(".lisp"),
8371 "diagnostic must name the expected extension: {rendered}",
8372 );
8373 }
8374
8375 // ── validate_code_paths — `.computeunit.yaml` compound-suffix gate on :servicos ──
8376 //
8377 // The lifted [`crate::render::is_computeunit_yaml_extension`] predicate
8378 // now gates `:servicos` entries on the ComputeUnit-CR YAML file-type
8379 // contract. The peer caixa-helm / caixa-flux renderers consume each
8380 // `:servicos` entry through `serde_yaml::from_str` as a typed
8381 // `ComputeUnit` CR — same downstream-consumer-shape lift as the peer
8382 // `:bibliotecas` `.lisp` gate (64772a9), here on the compound-suffix
8383 // axis `Path::extension` can't express on its own.
8384
8385 #[test]
8386 fn validate_code_paths_rejects_no_extension_servicos_entry() {
8387 // Canonical "I dragged the wrong file from the workspace tree"
8388 // footgun on the Servico axis. Without the gate the peer
8389 // caixa-helm / caixa-flux renderers hand the extensionless path
8390 // to `serde_yaml::from_str` and fail with a parser-shaped
8391 // diagnostic far from the source caixa.lisp, with no field
8392 // naming the offending `:servicos` entry.
8393 for relpath in ["servicos/demo", "demo", "servicos/sub/nested"] {
8394 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8395 let err = c.validate_code_paths().unwrap_err();
8396 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8397 panic!(
8398 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8399 got {err:?}"
8400 );
8401 };
8402 assert_eq!(slot, ":servicos");
8403 assert_eq!(path, PathBuf::from(relpath));
8404 }
8405 }
8406
8407 #[test]
8408 fn validate_code_paths_rejects_wrong_extension_servicos_entry() {
8409 // Wrong-extension sweep across common authoring footguns on the
8410 // Servico axis. Bare `.yaml` is the canonical "I forgot the
8411 // `.computeunit` segment" typo; the off-by-one-segment shapes
8412 // (`.computeunit-yaml` / `.computeunit_yaml`) silently pass the
8413 // bare `Path::extension` view but mismatch the typed compound
8414 // suffix the renderers' `serde_yaml::from_str` consumer demands.
8415 // Same sweep-posture as the peer
8416 // `validate_code_paths_rejects_wrong_extension_bibliotecas_entry`
8417 // (64772a9) on the sibling tatara-lisp-source axis.
8418 for relpath in [
8419 "servicos/demo.yaml",
8420 "servicos/demo.yml",
8421 "servicos/demo.json",
8422 "servicos/demo.toml",
8423 "servicos/demo.txt",
8424 "servicos/demo.computeunit.yaml.bak",
8425 "servicos/demo.computeunit.yam",
8426 "servicos/demo.computeunit",
8427 "servicos/demo-computeunit.yaml",
8428 "servicos/demo_computeunit.yaml",
8429 ] {
8430 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8431 let err = c.validate_code_paths().unwrap_err();
8432 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8433 panic!(
8434 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8435 got {err:?}"
8436 );
8437 };
8438 assert_eq!(slot, ":servicos");
8439 assert_eq!(path, PathBuf::from(relpath));
8440 }
8441 }
8442
8443 #[test]
8444 fn validate_code_paths_rejects_case_folded_extension_servicos_entry() {
8445 // Case-sensitivity sweep — pins the strict lowercase
8446 // `.computeunit.yaml` contract. A case-folded shape that the
8447 // layout's existence check would (case-insensitively, on
8448 // case-insensitive volumes) match the on-disk file still
8449 // mismatches the canonical form the codec emits, breaking the
8450 // THEORY.md §V.2.7 render-determinism contract. Mirrors the peer
8451 // `validate_code_paths_rejects_case_folded_extension_bibliotecas_entry`
8452 // (64772a9) sweep on the sibling tatara-lisp-source axis.
8453 for relpath in [
8454 "servicos/demo.ComputeUnit.yaml",
8455 "servicos/demo.COMPUTEUNIT.yaml",
8456 "servicos/demo.computeunit.YAML",
8457 "servicos/demo.computeunit.Yaml",
8458 "servicos/demo.COMPUTEUNIT.YAML",
8459 ] {
8460 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8461 let err = c.validate_code_paths().unwrap_err();
8462 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8463 panic!(
8464 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8465 got {err:?}"
8466 );
8467 };
8468 assert_eq!(slot, ":servicos");
8469 assert_eq!(path, PathBuf::from(relpath));
8470 }
8471 }
8472
8473 #[test]
8474 fn validate_code_paths_rejects_empty_stem_servicos_entry() {
8475 // Degenerate hidden-file shape: a file name exactly equal to the
8476 // suffix (`.computeunit.yaml` — no stem preceding the suffix) is
8477 // the structural "Servico declared with no identity" footgun.
8478 // The substrate identifies each ComputeUnit by the file-stem
8479 // segment that precedes `.computeunit.yaml` (the rendered
8480 // `lareira-<stem>` Helm chart, the per-Servico `metadata.name`,
8481 // the M3 `:contratos` membership lookup), so an empty stem
8482 // leaves the Servico unidentifiable. Pinned at the typed-axis
8483 // level so a future regression that drops the `name.len() >
8484 // SUFFIX.len()` bound at the predicate surfaces here, not
8485 // piecemeal as a `lareira-` chart-name collision at render time.
8486 for relpath in ["servicos/.computeunit.yaml"] {
8487 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8488 let err = c.validate_code_paths().unwrap_err();
8489 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8490 panic!(
8491 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8492 got {err:?}"
8493 );
8494 };
8495 assert_eq!(slot, ":servicos");
8496 assert_eq!(path, PathBuf::from(relpath));
8497 }
8498 }
8499
8500 #[test]
8501 fn validate_code_paths_accepts_canonical_computeunit_yaml_shapes() {
8502 // Positive-control sweep through every canonical authoring shape
8503 // every in-tree fixture and the `Caixa::template` scaffold use.
8504 // Mirrors the peer
8505 // `validate_code_paths_accepts_canonical_lisp_shapes` (64772a9)
8506 // and the lifted predicate's own
8507 // `computeunit_yaml_extension_accepts_canonical_shapes` sweep in
8508 // render.rs.
8509 for relpath in [
8510 "servicos/demo.computeunit.yaml",
8511 "servicos/hello-rio.computeunit.yaml",
8512 "servicos/my-service.computeunit.yaml",
8513 "servicos/a.computeunit.yaml",
8514 "./servicos/demo.computeunit.yaml",
8515 "servicos/./demo.computeunit.yaml",
8516 "servicos/sub/nested.computeunit.yaml",
8517 "servicos/v0.1.computeunit.yaml",
8518 ] {
8519 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8520 c.validate_code_paths()
8521 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8522 }
8523 }
8524
8525 #[test]
8526 fn validate_code_paths_non_computeunit_yaml_extension_does_not_fire_on_bibliotecas_or_exe() {
8527 // The file-type gate is per-slot — only `:servicos` carries the
8528 // ComputeUnit-CR YAML contract. A canonical `.lisp` `:bibliotecas`
8529 // entry and an extensionless `:exe` entry are the canonical
8530 // shapes every in-tree fixture uses, and must continue to pass
8531 // validate. Peer of
8532 // `validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos`
8533 // (64772a9) — together pin that the typed
8534 // [`CodePathFileType`] dispatch is exhaustively per-slot, with no
8535 // cross-axis leakage in either direction.
8536 let c = caixa_with_code_paths(
8537 vec!["lib/demo.lisp"],
8538 vec!["exe/demo", "exe/tool"],
8539 vec!["servicos/demo.computeunit.yaml"],
8540 );
8541 c.validate_code_paths().unwrap();
8542 }
8543
8544 #[test]
8545 fn validate_code_paths_sandbox_shape_arms_precede_non_computeunit_yaml_extension() {
8546 // Cross-arm precedence pin: a `:servicos` entry that is *both*
8547 // sandbox-escaping and wrong-extension surfaces the more
8548 // fundamental sandbox-shape diagnostic first (the
8549 // `.computeunit.yaml` remediation would be misleading when the
8550 // offending path can never resolve under the caixa root
8551 // anyway). Mirrors the peer
8552 // `validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension`
8553 // (64772a9) ordering on the sibling `:bibliotecas` axis and the
8554 // peer `EmptyPath` → `AbsolutePath` → `ParentEscape` →
8555 // `NonComputeUnitYamlExtension` arm-ordering the dispatch
8556 // table establishes.
8557 //
8558 // Empty wins (the strictly-smaller-scope structural arm).
8559 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
8560 assert!(
8561 matches!(
8562 c.validate_code_paths().unwrap_err(),
8563 ManifestError::CodePathEmpty { slot: ":servicos" }
8564 ),
8565 "empty must win over non-computeunit-yaml-extension",
8566 );
8567 // Absolute wins (the path can't resolve under the caixa root).
8568 let c = caixa_with_code_paths(vec![], vec![], vec!["/etc/foo.yaml"]);
8569 let err = c.validate_code_paths().unwrap_err();
8570 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8571 panic!("absolute must win over non-computeunit-yaml-extension, got {err:?}");
8572 };
8573 assert_eq!(slot, ":servicos");
8574 // ParentEscape wins (the path escapes the caixa root).
8575 let c = caixa_with_code_paths(vec![], vec![], vec!["../sibling/x.yaml"]);
8576 let err = c.validate_code_paths().unwrap_err();
8577 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8578 panic!("parent-escape must win over non-computeunit-yaml-extension, got {err:?}");
8579 };
8580 assert_eq!(slot, ":servicos");
8581 }
8582
8583 #[test]
8584 fn validate_code_paths_non_computeunit_yaml_extension_precedes_duplicate() {
8585 // Within-slot precedence pin: the per-entry file-type shape gate
8586 // fires before the cross-entry duplicate gate, so the narrower
8587 // structural defect dominates the uniqueness diagnostic. A
8588 // `("servicos/x.yaml" "servicos/x.yaml")` shape surfaces
8589 // `CodePathNonComputeUnitYamlExtension` on the first entry
8590 // rather than `CodePathDuplicate` on the pair — same posture
8591 // every per-entry shape-gate-precedes-duplicate cascade follows
8592 // on this surface, peer of the 64772a9 `:bibliotecas`
8593 // `("lib/x.txt" "lib/x.txt")` ordering.
8594 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/x.yaml", "servicos/x.yaml"]);
8595 let err = c.validate_code_paths().unwrap_err();
8596 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8597 panic!("expected CodePathNonComputeUnitYamlExtension, got {err:?}");
8598 };
8599 assert_eq!(slot, ":servicos");
8600 assert_eq!(path, PathBuf::from("servicos/x.yaml"));
8601 }
8602
8603 #[test]
8604 fn validate_code_paths_non_computeunit_yaml_extension_diagnostic_carries_offending_slot_and_path()
8605 {
8606 // Diagnostic-shape pin (peer with
8607 // `validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path`
8608 // on the sibling tatara-lisp-source axis): the file-type-arm
8609 // Display surfaces both the offending `:slot` tag, the
8610 // offending path verbatim, and the expected
8611 // `.computeunit.yaml` compound suffix named in the remediation
8612 // text, so a `feira lint` run can render the diagnostic without
8613 // re-parsing.
8614 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.yaml"]);
8615 let rendered = c.validate_code_paths().unwrap_err().to_string();
8616 assert!(
8617 rendered.contains(":servicos"),
8618 "diagnostic must name the offending slot: {rendered}",
8619 );
8620 assert!(
8621 rendered.contains("servicos/demo.yaml"),
8622 "diagnostic must quote the offending path: {rendered}",
8623 );
8624 assert!(
8625 rendered.contains(".computeunit.yaml"),
8626 "diagnostic must name the expected compound suffix: {rendered}",
8627 );
8628 }
8629
8630 // ── validate_etiquetas — universal-axis registry-search-tag shape ──
8631
8632 fn caixa_with_etiquetas(etiquetas: Vec<&str>) -> Caixa {
8633 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8634 c.etiquetas = etiquetas.into_iter().map(String::from).collect();
8635 c
8636 }
8637
8638 #[test]
8639 fn validate_etiquetas_accepts_empty_list() {
8640 // The empty-list identity: every caixa with no declared tags
8641 // trivially passes — `Caixa::template` emits `:etiquetas ()`,
8642 // so the gate is non-disruptive against every existing manifest.
8643 let c = caixa_with_etiquetas(vec![]);
8644 c.validate_etiquetas().unwrap();
8645 }
8646
8647 #[test]
8648 fn validate_etiquetas_accepts_canonical_forms() {
8649 // Positive control sweep: a canonical-shaped non-empty distinct
8650 // tag list passes, mirroring the example checkout-aplicacao
8651 // (`:etiquetas ("example" "aplicacao" "mesh" "ecommerce" "demo")`)
8652 // and the hello-rio fixture (`("hello-world" "wasm" "rust")`).
8653 let c = caixa_with_etiquetas(vec!["example", "aplicacao", "mesh", "ecommerce", "demo"]);
8654 c.validate_etiquetas().unwrap();
8655 }
8656
8657 #[test]
8658 fn validate_etiquetas_rejects_empty_entry() {
8659 // Canonical paste-from-blank-doc footgun. Without the gate the
8660 // empty entry rendered as `keywords: [""]` in `Chart.yaml`, a
8661 // no-op tag indexing nothing in the future caixa-registry.
8662 let c = caixa_with_etiquetas(vec![""]);
8663 let err = c.validate_etiquetas().unwrap_err();
8664 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8665 }
8666
8667 #[test]
8668 fn validate_etiquetas_rejects_duplicate_entry() {
8669 // Canonical copy-paste-the-wrong-tag footgun. Without the gate
8670 // the duplicate was silently dedup'd by caixa-helm's BTreeSet
8671 // collect at chart render — a "second wins / one silently
8672 // disappears" shape divergent from every peer typed-graph set
8673 // gate. The duplicate-arm names the offending tag verbatim.
8674 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8675 let err = c.validate_etiquetas().unwrap_err();
8676 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8677 panic!("expected EtiquetaDuplicate, got {err:?}");
8678 };
8679 assert_eq!(etiqueta, "demo");
8680 }
8681
8682 #[test]
8683 fn validate_etiquetas_empty_takes_precedence_over_duplicate() {
8684 // Empty-first cascade pin: `("" "demo" "demo")` surfaces
8685 // `EtiquetaEmpty` not `EtiquetaDuplicate` — the narrower
8686 // structural "this entry has no value" defect dominates the
8687 // cross-entry uniqueness diagnostic. Mirrors the peer
8688 // empty-before-duplicate cascades on `:caracteristicas`
8689 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8690 // fc3b4d5) and `:membros :caixa` (`MembroCaixaEmpty` before
8691 // `MembroDuplicate`).
8692 let c = caixa_with_etiquetas(vec!["", "demo", "demo"]);
8693 let err = c.validate_etiquetas().unwrap_err();
8694 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8695 }
8696
8697 #[test]
8698 fn validate_etiquetas_duplicate_reports_first_collision() {
8699 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8700 // duplicate (the lexicographically-earliest offending position
8701 // — the second `"a"` at index 2 collides with the first `"a"`
8702 // at index 0), not the later `"b"` collision at index 3,
8703 // peer with every other first-collision diagnostic posture on
8704 // this surface (`validate_load_singularity_reports_first_collision`,
8705 // `validate_cleanup_singularity_reports_first_collision`).
8706 let c = caixa_with_etiquetas(vec!["a", "b", "a", "b"]);
8707 let err = c.validate_etiquetas().unwrap_err();
8708 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8709 panic!("expected EtiquetaDuplicate, got {err:?}");
8710 };
8711 assert_eq!(etiqueta, "a");
8712 }
8713
8714 #[test]
8715 fn validate_etiquetas_case_sensitive() {
8716 // Case-sensitivity pin: `("Foo" "foo")` is two distinct entries,
8717 // mirroring the peer `:membros :caixa` / `:children :caixa`
8718 // exact-string-match discipline. The shape gate this routine
8719 // landed (`is_chart_keyword_shape`, Cargo crates.io keyword
8720 // grammar) accepts mixed case — crates.io's keyword rule is
8721 // "case-insensitive" at the index layer but admits mixed case
8722 // at the entry layer (the canonical Helm chart `keywords:`
8723 // shape is lowercase by convention, but the grammar admits
8724 // uppercase). Case-sensitivity at the duplicate-set layer
8725 // remains structural — two distinct strings are two distinct
8726 // entries.
8727 let c = caixa_with_etiquetas(vec!["Foo", "foo"]);
8728 c.validate_etiquetas().unwrap();
8729 }
8730
8731 #[test]
8732 fn validate_etiquetas_diagnostic_carries_offending_tag() {
8733 // Diagnostic-shape pin (peer with
8734 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`):
8735 // the error's Display surfaces the offending tag verbatim, so a
8736 // `feira lint` run can render the diagnostic without re-parsing
8737 // and the author can grep their caixa.lisp for the offending
8738 // value.
8739 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8740 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8741 assert!(
8742 rendered.contains(":etiquetas"),
8743 "diagnostic must name the offending slot: {rendered}",
8744 );
8745 assert!(
8746 rendered.contains("demo"),
8747 "diagnostic must quote the offending tag: {rendered}",
8748 );
8749 }
8750
8751 #[test]
8752 fn validate_etiquetas_rejects_leading_whitespace_entry() {
8753 // Canonical paste-from-aligned-doc footgun. Without the shape
8754 // gate `" mesh"` silently passed validate and landed as a
8755 // YAML plain-style scalar with leading whitespace in the
8756 // rendered Chart.yaml `keywords:` array — every YAML 1.2
8757 // dumper trims leading whitespace from plain-style scalars,
8758 // so the authored space round-tripped inconsistently back
8759 // through `caixa.lisp`. Mirrors the peer
8760 // `validate_autores_rejects_leading_whitespace_entry`.
8761 let c = caixa_with_etiquetas(vec![" mesh"]);
8762 let err = c.validate_etiquetas().unwrap_err();
8763 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8764 panic!("expected EtiquetaInvalid, got {err:?}");
8765 };
8766 assert_eq!(etiqueta, " mesh");
8767 assert!(reason.contains("whitespace"), "got: {reason}");
8768 }
8769
8770 #[test]
8771 fn validate_etiquetas_rejects_embedded_newline_entry() {
8772 // Canonical paste-from-multiline-doc footgun — the author
8773 // pasted a multi-tag block into one `:etiquetas` entry
8774 // instead of splitting into one entry per tag. Without the
8775 // shape gate `"mesh\nhttp"` silently passed validate and
8776 // landed as a YAML-illegal multi-line scalar in the rendered
8777 // Chart.yaml `keywords:` array.
8778 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8779 let err = c.validate_etiquetas().unwrap_err();
8780 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8781 panic!("expected EtiquetaInvalid, got {err:?}");
8782 };
8783 assert_eq!(etiqueta, "mesh\nhttp");
8784 assert!(reason.contains("newline"), "got: {reason}");
8785 }
8786
8787 #[test]
8788 fn validate_etiquetas_rejects_embedded_comma_entry() {
8789 // Canonical CSV-list-separator-confusion footgun: the author
8790 // confused the CSV-style separator convention with the
8791 // `:etiquetas` list grammar. Without the shape gate
8792 // `"mesh,http,grpc"` silently passed validate and landed as a
8793 // single malformed search tag in the rendered Chart.yaml
8794 // `keywords:` array — Artifact Hub's keyword index would
8795 // either silently drop the tag or index it as
8796 // `mesh,http,grpc` instead of three separate tags.
8797 let c = caixa_with_etiquetas(vec!["mesh,http,grpc"]);
8798 let err = c.validate_etiquetas().unwrap_err();
8799 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8800 panic!("expected EtiquetaInvalid, got {err:?}");
8801 };
8802 assert_eq!(etiqueta, "mesh,http,grpc");
8803 assert!(reason.contains('`'), "got: {reason}");
8804 assert!(reason.contains(','), "got: {reason}");
8805 }
8806
8807 #[test]
8808 fn validate_etiquetas_rejects_embedded_slash_entry() {
8809 // Canonical path-separator-confusion footgun: the author
8810 // confused namespace-path notation with the keyword grammar.
8811 let c = caixa_with_etiquetas(vec!["caixa/servico"]);
8812 let err = c.validate_etiquetas().unwrap_err();
8813 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8814 panic!("expected EtiquetaInvalid, got {err:?}");
8815 };
8816 assert_eq!(etiqueta, "caixa/servico");
8817 assert!(reason.contains('/'), "got: {reason}");
8818 }
8819
8820 #[test]
8821 fn validate_etiquetas_rejects_leading_digit_entry() {
8822 // Canonical paste-from-numbered-list footgun: the author
8823 // copied `1. mesh` from a numbered doc and the `1` leaked
8824 // into the tag.
8825 let c = caixa_with_etiquetas(vec!["1mesh"]);
8826 let err = c.validate_etiquetas().unwrap_err();
8827 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8828 panic!("expected EtiquetaInvalid, got {err:?}");
8829 };
8830 assert_eq!(etiqueta, "1mesh");
8831 assert!(reason.contains("digit"), "got: {reason}");
8832 }
8833
8834 #[test]
8835 fn validate_etiquetas_rejects_leading_hyphen_entry() {
8836 // Canonical kebab-leak footgun.
8837 let c = caixa_with_etiquetas(vec!["-foo"]);
8838 let err = c.validate_etiquetas().unwrap_err();
8839 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8840 panic!("expected EtiquetaInvalid, got {err:?}");
8841 };
8842 assert_eq!(etiqueta, "-foo");
8843 assert!(reason.contains('-'), "got: {reason}");
8844 }
8845
8846 #[test]
8847 fn validate_etiquetas_rejects_non_ascii_entry() {
8848 // Canonical paste-from-Unicode-doc footgun. Every legitimate
8849 // search tag is strict ASCII; raw non-ASCII silently
8850 // round-trips inconsistently across NFC/NFD normalization on
8851 // APFS / case-folding filesystems and breaks the Artifact Hub
8852 // keyword search index lookup.
8853 let c = caixa_with_etiquetas(vec!["café"]);
8854 let err = c.validate_etiquetas().unwrap_err();
8855 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8856 panic!("expected EtiquetaInvalid, got {err:?}");
8857 };
8858 assert_eq!(etiqueta, "café");
8859 assert!(reason.contains("non-ASCII"), "got: {reason}");
8860 }
8861
8862 #[test]
8863 fn validate_etiquetas_rejects_period_entry() {
8864 // Canonical namespace-confusion / version-suffix footgun
8865 // (`"http.1"` / `"v1.0"`): Cargo's crates.io keyword grammar
8866 // excludes `.` from the continuation set even though the
8867 // sibling `:caracteristicas` axis (Cargo's feature-name
8868 // grammar) admits it. Tighter than the sibling axis, peer
8869 // with Cargo's own crates.io keyword shape.
8870 let c = caixa_with_etiquetas(vec!["http.1"]);
8871 let err = c.validate_etiquetas().unwrap_err();
8872 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8873 panic!("expected EtiquetaInvalid, got {err:?}");
8874 };
8875 assert_eq!(etiqueta, "http.1");
8876 assert!(reason.contains('.'), "got: {reason}");
8877 }
8878
8879 #[test]
8880 fn validate_etiquetas_empty_takes_precedence_over_shape() {
8881 // Per-entry empty-first cascade pin: an entry that is both
8882 // empty *and* shape-invalid surfaces `EtiquetaEmpty` (the
8883 // narrower "this entry has no value" structural defect
8884 // dominates the broader shape-predicate diagnostic). The
8885 // empty arm fires before the shape predicate is consulted,
8886 // mirroring the peer `validate_autores_empty_takes_precedence_over_shape`
8887 // cascade established on the sibling universal-axis Vec<String>
8888 // surface.
8889 let c = caixa_with_etiquetas(vec![""]);
8890 let err = c.validate_etiquetas().unwrap_err();
8891 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8892 }
8893
8894 #[test]
8895 fn validate_etiquetas_shape_takes_precedence_over_duplicate() {
8896 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
8897 // entry that is malformed surfaces `EtiquetaInvalid` even when
8898 // a later entry would have collided on duplicate. The
8899 // per-entry shape arm fires inside the same loop iteration as
8900 // the empty arm, before the seen-set insert at end-of-iteration
8901 // — structural per-entry defects dominate the cross-entry
8902 // uniqueness diagnostic. Mirrors the peer
8903 // `validate_autores_shape_takes_precedence_over_duplicate`.
8904 let c = caixa_with_etiquetas(vec!["mesh\nhttp", "mesh\nhttp"]);
8905 let err = c.validate_etiquetas().unwrap_err();
8906 assert!(
8907 matches!(err, ManifestError::EtiquetaInvalid { .. }),
8908 "got {err:?}",
8909 );
8910 }
8911
8912 #[test]
8913 fn validate_etiquetas_invalid_diagnostic_names_offending_slot_and_value() {
8914 // Diagnostic-shape pin on the new shape arm (peer with
8915 // `validate_autores_invalid_diagnostic_names_offending_slot_and_value`):
8916 // the rendered Display surfaces both the offending slot name
8917 // and the offending value verbatim, so a `feira lint` run
8918 // points the author at the exact `:etiquetas` entry to fix.
8919 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8920 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8921 assert!(
8922 rendered.contains(":etiquetas"),
8923 "diagnostic must name the offending slot: {rendered}",
8924 );
8925 assert!(
8926 rendered.contains("mesh\\nhttp"),
8927 "diagnostic must quote the offending value (debug-escaped): {rendered}",
8928 );
8929 }
8930
8931 #[test]
8932 fn validate_etiquetas_rejects_at_21_byte_boundary() {
8933 // The 20-byte cap pin — boundary-exceeding case rejected,
8934 // boundary-accepting case passes. Mirrors the peer
8935 // `chart_keyword_shape_rejects_at_21_byte_boundary` substrate-
8936 // side pin, surfaced at the per-axis caller so the cap
8937 // propagates through validate end-to-end. Constructed as a
8938 // single all-`a` token so only the cap arm fires.
8939 let max_ok = "a".repeat(20);
8940 let c = caixa_with_etiquetas(vec![max_ok.as_str()]);
8941 c.validate_etiquetas().unwrap();
8942 let too_long = "a".repeat(21);
8943 let c = caixa_with_etiquetas(vec![too_long.as_str()]);
8944 let err = c.validate_etiquetas().unwrap_err();
8945 let ManifestError::EtiquetaInvalid { reason, .. } = err else {
8946 panic!("expected EtiquetaInvalid, got {err:?}");
8947 };
8948 assert!(reason.contains("20"), "got: {reason}");
8949 assert!(reason.contains("21"), "got: {reason}");
8950 }
8951
8952 #[test]
8953 fn validate_etiquetas_accepts_canonical_shaped_forms() {
8954 // Positive control sweep: every canonical-shaped tag from the
8955 // hello-rio / checkout-aplicacao / pangea-tatara-akeyless
8956 // example fixtures plus the substrate-fixed tags caixa-helm
8957 // unions in at chart render. Drift between this list and the
8958 // substrate-side `chart_keyword_shape_accepts_canonical_forms`
8959 // sweep surfaces here — one source of truth for the rule.
8960 let c = caixa_with_etiquetas(vec![
8961 "example",
8962 "aplicacao",
8963 "mesh",
8964 "ecommerce",
8965 "demo",
8966 "infrastructure",
8967 "aws",
8968 "akeyless",
8969 "pangea-native",
8970 "hello-world",
8971 "wasm",
8972 "rust",
8973 "tatara-lisp",
8974 "caixa-servico",
8975 "lareira",
8976 ]);
8977 c.validate_etiquetas().unwrap();
8978 }
8979
8980 // ── validate_autores — universal-axis maintainer shape ────────────
8981
8982 fn caixa_with_autores(autores: Vec<&str>) -> Caixa {
8983 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8984 c.autores = autores.into_iter().map(String::from).collect();
8985 c
8986 }
8987
8988 #[test]
8989 fn validate_autores_accepts_empty_list() {
8990 // The empty-list identity: `Caixa::template` emits `:autores ()`,
8991 // so the gate is non-disruptive against every existing manifest.
8992 let c = caixa_with_autores(vec![]);
8993 c.validate_autores().unwrap();
8994 }
8995
8996 #[test]
8997 fn validate_autores_accepts_canonical_forms() {
8998 // Positive control sweep: every canonical-shaped non-empty
8999 // distinct maintainer list passes — the hello-rio / checkout-
9000 // aplicacao fixtures' `:autores ("pleme-io")` shape, plus the
9001 // multi-author shape downstream packaging surfaces emit.
9002 let c = caixa_with_autores(vec!["pleme-io"]);
9003 c.validate_autores().unwrap();
9004 let c = caixa_with_autores(vec!["alice <alice@example.com>", "bob <bob@example.com>"]);
9005 c.validate_autores().unwrap();
9006 }
9007
9008 #[test]
9009 fn validate_autores_rejects_empty_entry() {
9010 // Canonical paste-from-blank-doc footgun. Without the gate the
9011 // empty entry rendered as `maintainers: [{name: "", email: null}]`
9012 // in `Chart.yaml`, a no-op maintainer the substrate cannot route
9013 // to.
9014 let c = caixa_with_autores(vec![""]);
9015 let err = c.validate_autores().unwrap_err();
9016 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9017 }
9018
9019 #[test]
9020 fn validate_autores_rejects_duplicate_entry() {
9021 // Canonical copy-paste-the-wrong-author footgun. Unlike the
9022 // `:etiquetas` peer (caixa-helm's `BTreeSet` collect silently
9023 // dedups the rendered `keywords:` array), the `maintainers:`
9024 // rendering has *no* dedup — duplicates stack verbatim. The
9025 // duplicate-arm names the offending author verbatim.
9026 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
9027 let err = c.validate_autores().unwrap_err();
9028 let ManifestError::AutorDuplicate { autor } = err else {
9029 panic!("expected AutorDuplicate, got {err:?}");
9030 };
9031 assert_eq!(autor, "pleme-io");
9032 }
9033
9034 #[test]
9035 fn validate_autores_empty_takes_precedence_over_duplicate() {
9036 // Empty-first cascade pin: `("" "pleme-io" "pleme-io")` surfaces
9037 // `AutorEmpty` not `AutorDuplicate` — the narrower structural
9038 // "this entry has no value" defect dominates the cross-entry
9039 // uniqueness diagnostic. Mirrors the peer empty-before-duplicate
9040 // cascades on `:etiquetas` (`EtiquetaEmpty` before
9041 // `EtiquetaDuplicate`, 360a499), `:caracteristicas`
9042 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
9043 // fc3b4d5), and `:membros :caixa` (`MembroCaixaEmpty` before
9044 // `MembroDuplicate`).
9045 let c = caixa_with_autores(vec!["", "pleme-io", "pleme-io"]);
9046 let err = c.validate_autores().unwrap_err();
9047 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9048 }
9049
9050 #[test]
9051 fn validate_autores_duplicate_reports_first_collision() {
9052 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
9053 // duplicate (the lexicographically-earliest offending position
9054 // — the second `"a"` at index 2 collides with the first `"a"`
9055 // at index 0), not the later `"b"` collision at index 3,
9056 // peer with every other first-collision diagnostic posture on
9057 // this surface.
9058 let c = caixa_with_autores(vec!["a", "b", "a", "b"]);
9059 let err = c.validate_autores().unwrap_err();
9060 let ManifestError::AutorDuplicate { autor } = err else {
9061 panic!("expected AutorDuplicate, got {err:?}");
9062 };
9063 assert_eq!(autor, "a");
9064 }
9065
9066 #[test]
9067 fn validate_autores_case_sensitive() {
9068 // Case-sensitivity pin: `("Pleme-io" "pleme-io")` is two distinct
9069 // entries, mirroring the peer `:etiquetas` / `:membros :caixa`
9070 // / `:children :caixa` exact-string-match discipline.
9071 let c = caixa_with_autores(vec!["Pleme-io", "pleme-io"]);
9072 c.validate_autores().unwrap();
9073 }
9074
9075 #[test]
9076 fn validate_autores_diagnostic_carries_offending_author() {
9077 // Diagnostic-shape pin (peer with
9078 // `validate_etiquetas_diagnostic_carries_offending_tag`): the
9079 // error's Display surfaces the offending author verbatim, so a
9080 // `feira lint` run can render the diagnostic without re-parsing
9081 // and the author can grep their caixa.lisp for the offending
9082 // value.
9083 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
9084 let rendered = c.validate_autores().unwrap_err().to_string();
9085 assert!(
9086 rendered.contains(":autores"),
9087 "diagnostic must name the offending slot: {rendered}",
9088 );
9089 assert!(
9090 rendered.contains("pleme-io"),
9091 "diagnostic must quote the offending author: {rendered}",
9092 );
9093 }
9094
9095 #[test]
9096 fn validate_autores_rejects_leading_whitespace_entry() {
9097 // Canonical paste-from-aligned-doc footgun. Without the shape
9098 // gate `" pleme-io"` silently passed validate and landed as a
9099 // YAML plain-style scalar with leading whitespace in the
9100 // rendered Chart.yaml `maintainers:` array — every YAML 1.2
9101 // dumper trims leading whitespace from plain-style scalars, so
9102 // the authored space round-tripped inconsistently back through
9103 // `caixa.lisp`. Mirrors the peer
9104 // `validate_descricao_rejects_leading_whitespace`.
9105 let c = caixa_with_autores(vec![" pleme-io"]);
9106 let err = c.validate_autores().unwrap_err();
9107 let ManifestError::AutorInvalid { autor, reason } = err else {
9108 panic!("expected AutorInvalid, got {err:?}");
9109 };
9110 assert_eq!(autor, " pleme-io");
9111 assert!(reason.contains("whitespace"), "got: {reason}");
9112 }
9113
9114 #[test]
9115 fn validate_autores_rejects_trailing_whitespace_entry() {
9116 // Canonical paste-from-doc footgun.
9117 let c = caixa_with_autores(vec!["pleme-io "]);
9118 let err = c.validate_autores().unwrap_err();
9119 let ManifestError::AutorInvalid { autor, reason } = err else {
9120 panic!("expected AutorInvalid, got {err:?}");
9121 };
9122 assert_eq!(autor, "pleme-io ");
9123 assert!(reason.contains("whitespace"), "got: {reason}");
9124 }
9125
9126 #[test]
9127 fn validate_autores_rejects_embedded_newline_entry() {
9128 // Canonical paste-from-multiline-doc footgun — the author
9129 // pasted a multi-line block of author records into one
9130 // `:autores` entry instead of splitting into one entry per
9131 // author. Without the shape gate `"alice\nbob"` silently
9132 // passed validate and landed as a YAML-illegal multi-line
9133 // scalar in the rendered Chart.yaml `maintainers:` array.
9134 let c = caixa_with_autores(vec!["alice\nbob"]);
9135 let err = c.validate_autores().unwrap_err();
9136 let ManifestError::AutorInvalid { autor, reason } = err else {
9137 panic!("expected AutorInvalid, got {err:?}");
9138 };
9139 assert_eq!(autor, "alice\nbob");
9140 assert!(reason.contains("newline"), "got: {reason}");
9141 }
9142
9143 #[test]
9144 fn validate_autores_rejects_embedded_carriage_return_entry() {
9145 // Canonical paste-from-Windows-CRLF-doc footgun.
9146 let c = caixa_with_autores(vec!["alice\rbob"]);
9147 let err = c.validate_autores().unwrap_err();
9148 let ManifestError::AutorInvalid { autor, reason } = err else {
9149 panic!("expected AutorInvalid, got {err:?}");
9150 };
9151 assert_eq!(autor, "alice\rbob");
9152 assert!(reason.contains("carriage return"), "got: {reason}");
9153 }
9154
9155 #[test]
9156 fn validate_autores_rejects_embedded_tab_entry() {
9157 // Canonical tab-from-aligned-doc footgun.
9158 let c = caixa_with_autores(vec!["Pleme\tContributors"]);
9159 let err = c.validate_autores().unwrap_err();
9160 let ManifestError::AutorInvalid { autor, reason } = err else {
9161 panic!("expected AutorInvalid, got {err:?}");
9162 };
9163 assert_eq!(autor, "Pleme\tContributors");
9164 assert!(reason.contains("tab"), "got: {reason}");
9165 }
9166
9167 #[test]
9168 fn validate_autores_rejects_embedded_control_bytes_entry() {
9169 // Paste-from-binary-blob footguns: NUL, BEL, ESC, DEL all
9170 // surface the same control-byte arm.
9171 for entry in [
9172 "alice\x00bob",
9173 "alice\x07bob",
9174 "alice\x1bbob",
9175 "alice\x7fbob",
9176 ] {
9177 let c = caixa_with_autores(vec![entry]);
9178 let err = c.validate_autores().unwrap_err();
9179 let ManifestError::AutorInvalid { autor, reason } = err else {
9180 panic!("expected AutorInvalid for {entry:?}, got {err:?}");
9181 };
9182 assert_eq!(autor, entry);
9183 assert!(
9184 reason.contains("control character"),
9185 "{entry:?} reason: {reason}",
9186 );
9187 }
9188 }
9189
9190 #[test]
9191 fn validate_autores_accepts_unicode_entry() {
9192 // Unicode positive control: realistic maintainer names carry
9193 // Unicode (`François`, `日本語`, `naïve`). The predicate must
9194 // round-trip Unicode losslessly, peer with the
9195 // `chart_maintainer_name_shape_accepts_unicode` substrate-side
9196 // sweep.
9197 let c = caixa_with_autores(vec![
9198 "François Dupont",
9199 "日本語の名前",
9200 "naïve <naive@example.com>",
9201 ]);
9202 c.validate_autores().unwrap();
9203 }
9204
9205 #[test]
9206 fn validate_autores_empty_takes_precedence_over_shape() {
9207 // Per-entry empty-first cascade pin: an entry that is both
9208 // empty *and* shape-invalid surfaces `AutorEmpty` (the narrower
9209 // "this entry has no value" structural defect dominates the
9210 // broader shape-predicate diagnostic). The empty arm fires
9211 // before the shape predicate is consulted, mirroring the peer
9212 // `validate_repositorio_empty_takes_precedence_over_shape`
9213 // cascade on the universal `Option<String>` siblings — and now
9214 // established on the Vec<String> per-entry surface.
9215 let c = caixa_with_autores(vec![""]);
9216 let err = c.validate_autores().unwrap_err();
9217 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9218 }
9219
9220 #[test]
9221 fn validate_autores_shape_takes_precedence_over_duplicate() {
9222 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
9223 // entry that is malformed surfaces `AutorInvalid` even when a
9224 // later entry would have collided on duplicate. The per-entry
9225 // shape arm fires inside the same loop iteration as the empty
9226 // arm, before the seen-set insert at end-of-iteration —
9227 // structural per-entry defects dominate the cross-entry
9228 // uniqueness diagnostic.
9229 let c = caixa_with_autores(vec!["alice\nbob", "alice\nbob"]);
9230 let err = c.validate_autores().unwrap_err();
9231 assert!(
9232 matches!(err, ManifestError::AutorInvalid { .. }),
9233 "got {err:?}",
9234 );
9235 }
9236
9237 #[test]
9238 fn validate_autores_invalid_diagnostic_names_offending_slot_and_value() {
9239 // Diagnostic-shape pin on the new shape arm (peer with
9240 // `validate_descricao_invalid_diagnostic_carries_offending_value`):
9241 // the rendered Display surfaces both the offending slot name
9242 // and the offending value verbatim, so a `feira lint` run
9243 // points the author at the exact `:autores` entry to fix.
9244 let c = caixa_with_autores(vec!["alice\nbob"]);
9245 let rendered = c.validate_autores().unwrap_err().to_string();
9246 assert!(
9247 rendered.contains(":autores"),
9248 "diagnostic must name the offending slot: {rendered}",
9249 );
9250 assert!(
9251 rendered.contains("alice\\nbob"),
9252 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9253 );
9254 }
9255
9256 #[test]
9257 fn validate_autores_rejects_at_129_byte_boundary() {
9258 // The 128-byte cap pin — boundary-exceeding case rejected,
9259 // boundary-accepting case passes. Mirrors the peer
9260 // `chart_maintainer_name_shape_rejects_at_129_byte_boundary`
9261 // substrate-side pin, surfaced at the per-axis caller so the
9262 // cap propagates through validate end-to-end. Constructed as
9263 // a single all-`a` token so only the cap arm fires.
9264 let max_ok = "a".repeat(128);
9265 let c = caixa_with_autores(vec![max_ok.as_str()]);
9266 c.validate_autores().unwrap();
9267 let too_long = "a".repeat(129);
9268 let c = caixa_with_autores(vec![too_long.as_str()]);
9269 let err = c.validate_autores().unwrap_err();
9270 let ManifestError::AutorInvalid { reason, .. } = err else {
9271 panic!("expected AutorInvalid, got {err:?}");
9272 };
9273 assert!(reason.contains("128"), "got: {reason}");
9274 assert!(reason.contains("129"), "got: {reason}");
9275 }
9276
9277 // ── validate_repositorio — universal-axis git-repo-URL shape ──────
9278
9279 fn caixa_with_repositorio(repositorio: Option<&str>) -> Caixa {
9280 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9281 c.repositorio = repositorio.map(String::from);
9282 c
9283 }
9284
9285 #[test]
9286 fn validate_repositorio_accepts_none() {
9287 // The omit-the-slot identity: `:repositorio` is optional. The
9288 // gate is a no-op when the author didn't declare a value —
9289 // every caixa without a `:repositorio` line trivially passes,
9290 // and the substrate-side renderers fall back to their
9291 // documented placeholder (`caixa-helm`'s `home: None`,
9292 // `caixa-flux`'s `https://github.com/pleme-io/<nome>` derived
9293 // URL). Mirrors the peer `validate_restart_window_accepts_none`
9294 // posture on the other `Option<String>` Caixa slot.
9295 let c = caixa_with_repositorio(None);
9296 c.validate_repositorio().unwrap();
9297 }
9298
9299 #[test]
9300 fn validate_repositorio_accepts_canonical_forms() {
9301 // Positive control sweep across every documented `:repositorio`
9302 // authoring shape — the same union the shared
9303 // `crate::render::is_git_repo_url` predicate accepts and the
9304 // peer `:deps :fonte :repo` axis already routes through.
9305 // Covers the `github:` shorthand (the canonical pleme-io
9306 // convention used in the `:repositorio` field of every
9307 // manifest fixture across `caixa-helm` / `caixa-mesh` and the
9308 // `examples/`), the `https://…` URL the README quickstart uses,
9309 // the `ssh://`, `git://`, `git@host:path` scp-style SSH, and
9310 // `file://` URL schemes the shared predicate documents.
9311 for repo in [
9312 "github:pleme-io/hello-rio",
9313 "github:pleme-io/checkout",
9314 "https://github.com/pleme-io/hello-rio",
9315 "ssh://git@github.com/pleme-io/hello-rio.git",
9316 "git://github.com/pleme-io/hello-rio.git",
9317 "git@github.com:pleme-io/hello-rio.git",
9318 "file:///srv/pleme/hello-rio",
9319 ] {
9320 let c = caixa_with_repositorio(Some(repo));
9321 c.validate_repositorio()
9322 .unwrap_or_else(|err| panic!("canonical {repo:?} must pass: {err:?}"));
9323 }
9324 }
9325
9326 #[test]
9327 fn validate_repositorio_rejects_empty_some() {
9328 // Canonical paste-from-blank-doc footgun. The narrower
9329 // [`ManifestError::RepositorioEmpty`] arm fires before the
9330 // shape predicate is consulted, mirroring the empty-first
9331 // cascade every peer per-axis identity gate uses
9332 // (`NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9333 // `FonteRepoEmpty` → `FonteRepoInvalid`). Without this gate
9334 // the empty `Some("")` silently passed the renderer's
9335 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9336 // on `None`) and landed as `home: ""` in `Chart.yaml` /
9337 // `url: ""` in the FluxCD `GitRepository`.
9338 let c = caixa_with_repositorio(Some(""));
9339 let err = c.validate_repositorio().unwrap_err();
9340 assert!(
9341 matches!(err, ManifestError::RepositorioEmpty),
9342 "got {err:?}",
9343 );
9344 }
9345
9346 #[test]
9347 fn validate_repositorio_rejects_whitespace() {
9348 // Paste-from-doc whitespace footgun. The shared
9349 // `is_git_repo_url` predicate refuses any whitespace byte; a
9350 // trailing space in a `:repositorio` value silently broke
9351 // `git clone '<value> '` at clone time. The diagnostic names
9352 // the offending value verbatim.
9353 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio "));
9354 let err = c.validate_repositorio().unwrap_err();
9355 let ManifestError::RepositorioInvalid { repositorio, .. } = err else {
9356 panic!("expected RepositorioInvalid, got {err:?}");
9357 };
9358 assert_eq!(repositorio, "github:pleme-io/hello-rio ");
9359 }
9360
9361 #[test]
9362 fn validate_repositorio_rejects_control_char() {
9363 // Paste-from-multiline-doc CRLF footgun — control characters
9364 // at the URL boundary are a class of subprocess-arg injection
9365 // and break git's URL parser at every porcelain entry point.
9366 let c = caixa_with_repositorio(Some("https://example.com/repo\n"));
9367 let err = c.validate_repositorio().unwrap_err();
9368 assert!(
9369 matches!(err, ManifestError::RepositorioInvalid { .. }),
9370 "got {err:?}",
9371 );
9372 }
9373
9374 #[test]
9375 fn validate_repositorio_rejects_leading_dash() {
9376 // Canonical CLI-argument-injection footgun: `git clone <repo>`
9377 // interprets a leading `-` as a CLI flag, so a
9378 // `-upload-pack=…` value escapes the subprocess argument
9379 // boundary. The shared predicate refuses every leading-`-`
9380 // shape at validate time.
9381 let c = caixa_with_repositorio(Some("-upload-pack=evil"));
9382 let err = c.validate_repositorio().unwrap_err();
9383 assert!(
9384 matches!(err, ManifestError::RepositorioInvalid { .. }),
9385 "got {err:?}",
9386 );
9387 }
9388
9389 #[test]
9390 fn validate_repositorio_rejects_missing_colon_separator() {
9391 // The bare `org/repo` ambiguity footgun — `git clone` reads
9392 // a no-`:` form as a relative filesystem path rather than the
9393 // GitHub-shorthand expansion the author probably intended.
9394 // The shared predicate refuses every shape without a `:`
9395 // separator.
9396 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9397 let err = c.validate_repositorio().unwrap_err();
9398 assert!(
9399 matches!(err, ManifestError::RepositorioInvalid { .. }),
9400 "got {err:?}",
9401 );
9402 }
9403
9404 #[test]
9405 fn validate_repositorio_rejects_fragment_anchor() {
9406 // Paste-from-browser-address-bar footgun on the
9407 // `:repositorio` axis — an author copies a GitHub permalink
9408 // to a README section / line-permalink and forgets to trim
9409 // the `#fragment` tail. The shared `is_git_repo_url`
9410 // predicate refuses the byte at the URL-grammar layer
9411 // (libcurl strips the fragment before opening the
9412 // transport, so the byte rides verbatim into the rendered
9413 // `Chart.yaml` `home:` and FluxCD `GitRepository` `url:`
9414 // fields but is silently dropped on the wire — two
9415 // manifest variants whose values differ only in their
9416 // fragment anchor lock to two distinct rendered artifacts
9417 // for the byte-identical clone, defeating the THEORY.md
9418 // §V.2 render-determinism contract on the `:repositorio`
9419 // axis the peer `:fonte :repo` axis already closes).
9420 let c = caixa_with_repositorio(Some("https://github.com/pleme-io/hello-rio#readme"));
9421 let err = c.validate_repositorio().unwrap_err();
9422 let ManifestError::RepositorioInvalid {
9423 repositorio,
9424 reason,
9425 } = err
9426 else {
9427 panic!("expected RepositorioInvalid, got {err:?}");
9428 };
9429 assert_eq!(repositorio, "https://github.com/pleme-io/hello-rio#readme");
9430 assert!(
9431 reason.contains("must not contain `#`"),
9432 "reason must surface the fragment-`#` arm, got {reason:?}"
9433 );
9434 }
9435
9436 #[test]
9437 fn validate_repositorio_rejects_query_string() {
9438 // Paste-from-browser-address-bar footgun on the
9439 // `:repositorio` axis (peer with the a68f818 fragment-`#`
9440 // arm on the same axis). An author copies a GitHub tab
9441 // deep-link out of the address bar and forgets to trim
9442 // the `?tab=…` query tail. The shared `is_git_repo_url`
9443 // predicate refuses the byte at the URL-grammar layer
9444 // (GitHub / GitLab / Bitbucket silently ignore the
9445 // `?query` tail and serve the same repo regardless, so
9446 // the byte rides verbatim into the rendered `Chart.yaml`
9447 // `home:` and FluxCD `GitRepository` `url:` fields but
9448 // is silently masked at the wire — two manifest variants
9449 // whose values differ only in their query tail lock to
9450 // two distinct rendered artifacts for the byte-identical
9451 // clone, defeating the THEORY.md §V.2 render-determinism
9452 // contract on the `:repositorio` axis the peer `:fonte
9453 // :repo` axis already closes).
9454 let c = caixa_with_repositorio(Some(
9455 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file",
9456 ));
9457 let err = c.validate_repositorio().unwrap_err();
9458 let ManifestError::RepositorioInvalid {
9459 repositorio,
9460 reason,
9461 } = err
9462 else {
9463 panic!("expected RepositorioInvalid, got {err:?}");
9464 };
9465 assert_eq!(
9466 repositorio,
9467 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file"
9468 );
9469 assert!(
9470 reason.contains("must not contain `?`"),
9471 "reason must surface the query-`?` arm, got {reason:?}"
9472 );
9473 }
9474
9475 #[test]
9476 fn validate_repositorio_rejects_embedded_backslash() {
9477 // Windows-file-path-confusion footgun on the `:repositorio`
9478 // axis (peer with the prior fragment-`#` / query-`?` arms on
9479 // the same axis, and peer with the new dep-level `:fonte :repo`
9480 // backslash arm on the URL-grammar trajectory). An author
9481 // pastes a Windows Explorer address-bar `file:///C:\Users\me\
9482 // hello-rio` into the `:repositorio` slot, expecting the
9483 // `lareira-<nome>` chart's `home:` field and the FluxCD
9484 // `GitRepository` `url:` field to render the canonical local
9485 // file-URI. The shared `is_git_repo_url` predicate refuses
9486 // the byte at the URL-grammar layer (libcurl silently
9487 // translates `\` → `/` on some platforms and refuses it on
9488 // others, so the byte rides verbatim into the rendered
9489 // artifacts but is silently rewritten or rejected at the wire
9490 // — two manifest variants whose values differ only in
9491 // backslash-vs-forward-slash lock to two distinct rendered
9492 // artifacts for the byte-identical clone, defeating the
9493 // THEORY.md §V.2 render-determinism contract on the
9494 // `:repositorio` axis the peer `:fonte :repo` axis already
9495 // closes).
9496 let c = caixa_with_repositorio(Some("file:///C:\\Users\\me\\hello-rio"));
9497 let err = c.validate_repositorio().unwrap_err();
9498 let ManifestError::RepositorioInvalid {
9499 repositorio,
9500 reason,
9501 } = err
9502 else {
9503 panic!("expected RepositorioInvalid, got {err:?}");
9504 };
9505 assert_eq!(repositorio, "file:///C:\\Users\\me\\hello-rio");
9506 assert!(
9507 reason.contains("must not contain `\\`"),
9508 "reason must surface the backslash-`\\` arm, got {reason:?}"
9509 );
9510 }
9511
9512 #[test]
9513 fn validate_repositorio_rejects_uri_template_placeholder() {
9514 // URI Template (RFC 6570) placeholder footgun on the
9515 // `:repositorio` axis (peer with the prior fragment-`#` /
9516 // query-`?` / backslash-`\` arms on the same axis, and peer
9517 // with the new dep-level `:fonte :repo` `{` / `}` arm on the
9518 // URL-grammar trajectory). An author pastes a quick-start
9519 // README snippet / OpenAPI `servers:` URL / Helm chart
9520 // `home:` template carrying unresolved `{org}` / `{repo}`
9521 // placeholders into the `:repositorio` slot, expecting the
9522 // substrate to resolve the placeholder downstream. The
9523 // shared `is_git_repo_url` predicate refuses the byte at the
9524 // URL-grammar layer (libcurl percent-encodes `{` / `}` to
9525 // `%7B` / `%7D` on the wire, so the byte round-trips
9526 // inconsistently between the rendered `Chart.yaml home:` /
9527 // FluxCD `GitRepository url:` and the resolver's `git clone`
9528 // invocation, defeating the THEORY.md §V.2 render-
9529 // determinism contract on the `:repositorio` axis the peer
9530 // `:fonte :repo` axis already closes; every git porcelain
9531 // entry-point additionally fetches a nonexistent literal-
9532 // `{placeholder}`-named path far from the source caixa.lisp).
9533 let c = caixa_with_repositorio(Some("https://github.com/{org}/hello-rio"));
9534 let err = c.validate_repositorio().unwrap_err();
9535 let ManifestError::RepositorioInvalid {
9536 repositorio,
9537 reason,
9538 } = err
9539 else {
9540 panic!("expected RepositorioInvalid, got {err:?}");
9541 };
9542 assert_eq!(repositorio, "https://github.com/{org}/hello-rio");
9543 assert!(
9544 reason.contains("must not contain `{`"),
9545 "reason must surface the open-brace `{{` arm, got {reason:?}"
9546 );
9547 assert!(
9548 reason.contains("URI Template") || reason.contains("RFC 6570"),
9549 "reason must name the RFC 6570 URI Template grammar, got {reason:?}"
9550 );
9551 }
9552
9553 #[test]
9554 fn validate_repositorio_empty_takes_precedence_over_shape() {
9555 // Empty-first cascade pin: the empty `Some("")` surfaces the
9556 // narrower `RepositorioEmpty` not the shape-predicate-wrapped
9557 // `RepositorioInvalid`, mirroring the peer
9558 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9559 // `FonteRepoEmpty` → `FonteRepoInvalid` cascades. The shared
9560 // `is_git_repo_url` predicate also rejects the empty input
9561 // (defensively, with its own `"must not be empty"` reason),
9562 // but the manifest-layer empty arm runs first to surface the
9563 // narrower diagnostic verbatim.
9564 let c = caixa_with_repositorio(Some(""));
9565 let err = c.validate_repositorio().unwrap_err();
9566 assert!(
9567 matches!(err, ManifestError::RepositorioEmpty),
9568 "got {err:?}",
9569 );
9570 }
9571
9572 #[test]
9573 fn validate_repositorio_diagnostic_carries_offending_value() {
9574 // Diagnostic-shape pin (peer with
9575 // `validate_autores_diagnostic_carries_offending_author`): the
9576 // error's Display surfaces the offending value + slot name
9577 // verbatim, so a `feira lint` run can render the diagnostic
9578 // without re-parsing and the author can grep their caixa.lisp
9579 // for the offending `:repositorio` value.
9580 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9581 let rendered = c.validate_repositorio().unwrap_err().to_string();
9582 assert!(
9583 rendered.contains(":repositorio"),
9584 "diagnostic must name the offending slot: {rendered}",
9585 );
9586 assert!(
9587 rendered.contains("pleme-io/hello-rio"),
9588 "diagnostic must quote the offending value: {rendered}",
9589 );
9590 }
9591
9592 // ── validate_descricao — universal-axis Chart.yaml description shape ──
9593
9594 fn caixa_with_descricao(descricao: Option<&str>) -> Caixa {
9595 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9596 c.descricao = descricao.map(String::from);
9597 c
9598 }
9599
9600 #[test]
9601 fn validate_descricao_accepts_none() {
9602 // The omit-the-slot identity: `:descricao` is optional. The
9603 // gate is a no-op when the author didn't declare a value —
9604 // every caixa without a `:descricao` line trivially passes,
9605 // and the substrate-side renderers fall back to their
9606 // documented `caixa.nome`-derived placeholder. Mirrors the
9607 // peer `validate_repositorio_accepts_none` posture on the
9608 // sibling `Option<String>` Caixa slot.
9609 let c = caixa_with_descricao(None);
9610 c.validate_descricao().unwrap();
9611 }
9612
9613 #[test]
9614 fn validate_descricao_accepts_canonical_summary() {
9615 // Positive control: the canonical pleme-io descricao shape —
9616 // a short free-form prose summary — passes the gate. Covers
9617 // the fixture shapes the `caixa-helm` / `caixa-flux` /
9618 // `caixa-mesh` test fixtures use (`"Canonical Rust→wasm32-
9619 // wasip2 caixa Servico."`, `"Checkout flow."`).
9620 for desc in [
9621 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9622 "Checkout flow.",
9623 "AWS provider caixa for tatara-lisp",
9624 "FIXME — describe this caixa",
9625 "x",
9626 ] {
9627 let c = caixa_with_descricao(Some(desc));
9628 c.validate_descricao()
9629 .unwrap_or_else(|err| panic!("canonical {desc:?} must pass: {err:?}"));
9630 }
9631 }
9632
9633 #[test]
9634 fn validate_descricao_rejects_empty_some() {
9635 // Canonical paste-from-blank-doc footgun. Without this gate
9636 // the empty `Some("")` silently passed the renderer's
9637 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9638 // on `None`) and landed as `description: ""` in `Chart.yaml`
9639 // and a blank `README.md` header. Mirrors the peer
9640 // [`ManifestError::RepositorioEmpty`] empty-arm on the
9641 // sibling `Option<String>` Caixa slot.
9642 let c = caixa_with_descricao(Some(""));
9643 let err = c.validate_descricao().unwrap_err();
9644 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9645 }
9646
9647 #[test]
9648 fn validate_descricao_rejects_leading_whitespace() {
9649 // Paste-from-aligned-doc footgun: a leading ASCII space the
9650 // bare empty-arm gate accepted, the shape predicate now
9651 // refuses. The diagnostic carries the offending value
9652 // verbatim (with the leading space preserved) so the author
9653 // can grep their caixa.lisp for the exact `:descricao` line
9654 // and fix the round-trip-inconsistent leading whitespace.
9655 // Mirrors the peer
9656 // `validate_licenca_rejects_leading_whitespace` arm on the
9657 // sibling `:licenca` axis.
9658 let c = caixa_with_descricao(Some(" Checkout flow."));
9659 let err = c.validate_descricao().unwrap_err();
9660 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9661 panic!("expected DescricaoInvalid, got {err:?}");
9662 };
9663 assert_eq!(descricao, " Checkout flow.");
9664 assert!(reason.contains("whitespace"), "got: {reason:?}");
9665 }
9666
9667 #[test]
9668 fn validate_descricao_rejects_trailing_whitespace() {
9669 // Paste-from-doc footgun: a trailing ASCII space the bare
9670 // empty-arm gate accepted, the shape predicate now refuses.
9671 let c = caixa_with_descricao(Some("Checkout flow. "));
9672 let err = c.validate_descricao().unwrap_err();
9673 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9674 panic!("expected DescricaoInvalid, got {err:?}");
9675 };
9676 assert_eq!(descricao, "Checkout flow. ");
9677 assert!(reason.contains("whitespace"), "got: {reason:?}");
9678 }
9679
9680 #[test]
9681 fn validate_descricao_rejects_embedded_newline() {
9682 // Paste-from-multiline-doc footgun: an embedded LF the bare
9683 // empty-arm gate accepted, the shape predicate now refuses.
9684 // Without this gate the embedded newline silently landed in
9685 // the rendered Chart.yaml as a multi-line YAML block scalar,
9686 // and every chart-aware UI (`helm list`, `helm search`,
9687 // Artifact Hub) renders the description in a single-line
9688 // column so the embedded newline is silently dropped at
9689 // every downstream consumer.
9690 let c = caixa_with_descricao(Some("Checkout\nflow."));
9691 let err = c.validate_descricao().unwrap_err();
9692 assert!(
9693 matches!(err, ManifestError::DescricaoInvalid { .. }),
9694 "got {err:?}",
9695 );
9696 assert!(err.to_string().contains("newline"), "got {err}");
9697 }
9698
9699 #[test]
9700 fn validate_descricao_rejects_embedded_carriage_return() {
9701 // Paste-from-Windows-CRLF-doc footgun.
9702 let c = caixa_with_descricao(Some("Checkout\rflow."));
9703 let err = c.validate_descricao().unwrap_err();
9704 assert!(
9705 matches!(err, ManifestError::DescricaoInvalid { .. }),
9706 "got {err:?}",
9707 );
9708 assert!(err.to_string().contains("carriage return"), "got {err}");
9709 }
9710
9711 #[test]
9712 fn validate_descricao_rejects_embedded_tab() {
9713 // Tab-from-aligned-doc footgun.
9714 let c = caixa_with_descricao(Some("Checkout\tflow."));
9715 let err = c.validate_descricao().unwrap_err();
9716 assert!(
9717 matches!(err, ManifestError::DescricaoInvalid { .. }),
9718 "got {err:?}",
9719 );
9720 assert!(err.to_string().contains("tab"), "got {err}");
9721 }
9722
9723 #[test]
9724 fn validate_descricao_rejects_embedded_control_bytes() {
9725 // Paste-from-binary-blob footgun: every other control byte
9726 // (NUL, BEL, ESC, DEL) is refused at validate time. Mirrors
9727 // the peer SPDX-expression control-byte arm.
9728 for s in [
9729 "Checkout\x00flow.",
9730 "Checkout\x07flow.",
9731 "Checkout\x1bflow.",
9732 "Checkout\x7fflow.",
9733 ] {
9734 let c = caixa_with_descricao(Some(s));
9735 let err = c.validate_descricao().unwrap_err();
9736 assert!(
9737 matches!(err, ManifestError::DescricaoInvalid { .. }),
9738 "{s:?} got {err:?}",
9739 );
9740 assert!(
9741 err.to_string().contains("control character"),
9742 "{s:?} got {err}",
9743 );
9744 }
9745 }
9746
9747 #[test]
9748 fn validate_descricao_accepts_unicode_prose() {
9749 // Positive control: Unicode prose is accepted — the
9750 // canonical fixtures carry `→` (U+2192) and `—` (U+2014),
9751 // and `Caixa::template`'s `"FIXME — describe this caixa"`
9752 // scaffold every `feira init` emits must continue to pass.
9753 for s in [
9754 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9755 "FIXME — describe this caixa",
9756 "Caixa pour le projet tâche",
9757 "日本語の説明",
9758 ] {
9759 let c = caixa_with_descricao(Some(s));
9760 c.validate_descricao()
9761 .unwrap_or_else(|err| panic!("Unicode {s:?} must pass: {err:?}"));
9762 }
9763 }
9764
9765 #[test]
9766 fn validate_descricao_empty_takes_precedence_over_shape() {
9767 // Cascade pin: a `Some("")` surfaces the narrower
9768 // `DescricaoEmpty` arm, not the broader `DescricaoInvalid`
9769 // shape-predicate arm. Mirrors the peer
9770 // `validate_licenca_empty_takes_precedence_over_shape` pin
9771 // on the sibling `:licenca` axis.
9772 let c = caixa_with_descricao(Some(""));
9773 let err = c.validate_descricao().unwrap_err();
9774 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9775 }
9776
9777 #[test]
9778 fn validate_descricao_invalid_diagnostic_carries_offending_value_and_slot() {
9779 // Diagnostic-shape pin: the error's Display surfaces both
9780 // the `:descricao` slot name and the offending value
9781 // verbatim, so a `feira lint` run can render the diagnostic
9782 // without re-parsing and the author can grep their caixa.lisp
9783 // for the offending `:descricao` line. Mirrors the peer
9784 // `validate_licenca_invalid_diagnostic_carries_offending_value_and_slot`
9785 // pin (ee2e888) on the sibling `:licenca` axis.
9786 // The `{descricao:?}` Debug format escapes embedded control
9787 // bytes; the quoted offending value surfaces as
9788 // `"Checkout\nflow."` (literal backslash-n) in the rendered
9789 // diagnostic. The author can grep their caixa.lisp for the
9790 // literal `Checkout` summary prefix.
9791 let c = caixa_with_descricao(Some("Checkout\nflow."));
9792 let rendered = c.validate_descricao().unwrap_err().to_string();
9793 assert!(
9794 rendered.contains(":descricao"),
9795 "diagnostic must name the offending slot: {rendered}",
9796 );
9797 assert!(
9798 rendered.contains("Checkout\\nflow."),
9799 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9800 );
9801 }
9802
9803 #[test]
9804 fn validate_descricao_template_passes() {
9805 // Round-trip pin: the bare `Caixa::template` shape carries
9806 // `:descricao "FIXME — describe this caixa"` (a non-empty
9807 // sentinel), so the template-derived Caixa passes the gate by
9808 // construction. A future template-shape change that omits or
9809 // empties `:descricao` would surface here as a regression.
9810 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9811 c.validate_descricao().unwrap();
9812 }
9813
9814 #[test]
9815 fn validate_descricao_diagnostic_names_offending_slot() {
9816 // Diagnostic-shape pin (peer with
9817 // `validate_repositorio_diagnostic_carries_offending_value`):
9818 // the error's Display surfaces the `:descricao` slot name
9819 // verbatim, so a `feira lint` run can render the diagnostic
9820 // without re-parsing and the author can grep their caixa.lisp
9821 // for the offending `:descricao` line.
9822 let c = caixa_with_descricao(Some(""));
9823 let rendered = c.validate_descricao().unwrap_err().to_string();
9824 assert!(
9825 rendered.contains(":descricao"),
9826 "diagnostic must name the offending slot: {rendered}",
9827 );
9828 }
9829
9830 // ── validate_licenca — universal-axis chart README license shape ──
9831
9832 fn caixa_with_licenca(licenca: Option<&str>) -> Caixa {
9833 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9834 c.licenca = licenca.map(String::from);
9835 c
9836 }
9837
9838 #[test]
9839 fn validate_licenca_accepts_none() {
9840 // The omit-the-slot identity: `:licenca` is optional. The
9841 // gate is a no-op when the author didn't declare a value —
9842 // every caixa without a `:licenca` line trivially passes,
9843 // and the substrate-side `caixa-helm` renderer falls back to
9844 // the documented `"MIT"` placeholder. Mirrors the peer
9845 // `validate_descricao_accepts_none` posture on the sibling
9846 // `Option<String>` Caixa slot.
9847 let c = caixa_with_licenca(None);
9848 c.validate_licenca().unwrap();
9849 }
9850
9851 #[test]
9852 fn validate_licenca_accepts_canonical_expressions() {
9853 // Positive control: every canonical SPDX expression shape
9854 // pleme-io carries in its existing fixtures + the canonical
9855 // SPDX dual-license / with-exception / `+`-suffix / grouped /
9856 // user-defined-reference shapes all pass the gate. Covers
9857 // the single-license, `OR`-compound, `AND`-compound,
9858 // `WITH`-exception, parenthesis-grouped, `+`-suffix, and
9859 // `LicenseRef-` / `DocumentRef-:LicenseRef-` shapes — every
9860 // production the SPDX 2.1 expression grammar admits that
9861 // sits within the alphabet floor the
9862 // `is_spdx_expression_shape` predicate enforces.
9863 for lic in [
9864 "MIT",
9865 "Apache-2.0",
9866 "Apache-2.0 OR MIT",
9867 "Apache-2.0 AND MIT",
9868 "BSD-3-Clause",
9869 "MPL-2.0",
9870 "GPL-3.0-or-later",
9871 "GPL-2.0+",
9872 "Apache-2.0 WITH LLVM-exception",
9873 "(MIT OR Apache-2.0) AND BSD-3-Clause",
9874 "(MIT OR Apache-2.0) AND BSD-3-Clause AND ISC",
9875 "LicenseRef-MyLicense",
9876 "DocumentRef-spdx-tool:LicenseRef-MIT-Style",
9877 "x",
9878 ] {
9879 let c = caixa_with_licenca(Some(lic));
9880 c.validate_licenca()
9881 .unwrap_or_else(|err| panic!("canonical {lic:?} must pass: {err:?}"));
9882 }
9883 }
9884
9885 #[test]
9886 fn validate_licenca_rejects_trailing_whitespace() {
9887 // Paste-from-doc whitespace footgun. A trailing space in the
9888 // `:licenca` value would silently break a downstream SPDX
9889 // parser that splits on exact `AND` / `OR` / `WITH` keyword
9890 // boundaries. The shape predicate refuses every trailing
9891 // whitespace byte by construction. Peer with
9892 // `validate_repositorio_rejects_whitespace` and
9893 // `validate_edicao_rejects_trailing_whitespace`.
9894 let c = caixa_with_licenca(Some("MIT "));
9895 let err = c.validate_licenca().unwrap_err();
9896 let ManifestError::LicencaInvalid { licenca, .. } = err else {
9897 panic!("expected LicencaInvalid, got {err:?}");
9898 };
9899 assert_eq!(licenca, "MIT ");
9900 }
9901
9902 #[test]
9903 fn validate_licenca_rejects_leading_whitespace() {
9904 // Symmetric paste-from-doc whitespace footgun on the leading
9905 // boundary — the gate refuses every shape that starts with a
9906 // space byte by construction. Peer with
9907 // `validate_edicao_rejects_leading_whitespace`.
9908 let c = caixa_with_licenca(Some(" MIT"));
9909 let err = c.validate_licenca().unwrap_err();
9910 assert!(
9911 matches!(err, ManifestError::LicencaInvalid { .. }),
9912 "got {err:?}",
9913 );
9914 }
9915
9916 #[test]
9917 fn validate_licenca_rejects_control_char() {
9918 // Paste-from-multiline-doc CRLF footgun — control characters
9919 // at the value boundary land as a malformed line in the
9920 // rendered chart `README.md` `## License` section. Peer with
9921 // `validate_repositorio_rejects_control_char` and
9922 // `validate_edicao_rejects_control_char`.
9923 for lic in ["MIT\n", "MIT\r\n", "MIT\rApache-2.0"] {
9924 let c = caixa_with_licenca(Some(lic));
9925 let err = c.validate_licenca().unwrap_err();
9926 assert!(
9927 matches!(err, ManifestError::LicencaInvalid { .. }),
9928 "expected LicencaInvalid on {lic:?}, got {err:?}",
9929 );
9930 }
9931 }
9932
9933 #[test]
9934 fn validate_licenca_rejects_tab() {
9935 // Tab-from-aligned-doc footgun — SPDX expressions use a
9936 // single ASCII space between tokens; a tab breaks every
9937 // downstream SPDX parser that splits on exact `" "`
9938 // boundaries.
9939 let c = caixa_with_licenca(Some("MIT\tOR Apache-2.0"));
9940 let err = c.validate_licenca().unwrap_err();
9941 assert!(
9942 matches!(err, ManifestError::LicencaInvalid { .. }),
9943 "got {err:?}",
9944 );
9945 }
9946
9947 #[test]
9948 fn validate_licenca_rejects_non_ascii() {
9949 // Smart-quote / non-ASCII paste footgun — SPDX identifiers
9950 // are ASCII per the `idstring = 1*(ALPHA / DIGIT / "-" /
9951 // ".")` production. The shape predicate refuses every
9952 // non-ASCII byte by construction; peer with
9953 // `validate_edicao_rejects_non_ascii_lookalike`.
9954 for lic in ["MIT\u{a0}OR Apache-2.0", "MIT\u{2013}1.0", "Café-1.0"] {
9955 let c = caixa_with_licenca(Some(lic));
9956 let err = c.validate_licenca().unwrap_err();
9957 assert!(
9958 matches!(err, ManifestError::LicencaInvalid { .. }),
9959 "expected LicencaInvalid on {lic:?}, got {err:?}",
9960 );
9961 }
9962 }
9963
9964 #[test]
9965 fn validate_licenca_rejects_underscore() {
9966 // Underscore-instead-of-hyphen typo footgun — `Apache_2.0` /
9967 // `MIT_Style` / `BSD_3_Clause` are familiar shapes from
9968 // snake-case identifier conventions that don't apply to the
9969 // SPDX `idstring` grammar (which admits only `ALPHA / DIGIT /
9970 // "-" / "."`). The shape predicate refuses every underscore
9971 // byte by construction.
9972 for lic in ["Apache_2.0", "MIT_Style", "BSD_3_Clause"] {
9973 let c = caixa_with_licenca(Some(lic));
9974 let err = c.validate_licenca().unwrap_err();
9975 assert!(
9976 matches!(err, ManifestError::LicencaInvalid { .. }),
9977 "expected LicencaInvalid on {lic:?}, got {err:?}",
9978 );
9979 }
9980 }
9981
9982 #[test]
9983 fn validate_licenca_rejects_comma_separator() {
9984 // Comma-instead-of-`OR`-keyword colloquial idiom footgun —
9985 // SPDX expressions compose multiple licenses via `AND` / `OR`
9986 // keywords, not the comma separator. The shape predicate
9987 // refuses every comma byte by construction.
9988 for lic in ["MIT, Apache-2.0", "MIT,Apache-2.0"] {
9989 let c = caixa_with_licenca(Some(lic));
9990 let err = c.validate_licenca().unwrap_err();
9991 assert!(
9992 matches!(err, ManifestError::LicencaInvalid { .. }),
9993 "expected LicencaInvalid on {lic:?}, got {err:?}",
9994 );
9995 }
9996 }
9997
9998 #[test]
9999 fn validate_licenca_rejects_slash_dual_license() {
10000 // Slash-dual-license colloquial idiom footgun — the
10001 // `MIT/Apache-2.0` shape is common in Cargo's pre-SPDX
10002 // `package.license` field but non-SPDX; the SPDX equivalent
10003 // is `MIT OR Apache-2.0`. The shape predicate refuses every
10004 // forward-slash byte by construction.
10005 for lic in ["MIT/Apache-2.0", "MIT/BSD-3-Clause"] {
10006 let c = caixa_with_licenca(Some(lic));
10007 let err = c.validate_licenca().unwrap_err();
10008 assert!(
10009 matches!(err, ManifestError::LicencaInvalid { .. }),
10010 "expected LicencaInvalid on {lic:?}, got {err:?}",
10011 );
10012 }
10013 }
10014
10015 #[test]
10016 fn validate_licenca_rejects_semicolon_separator() {
10017 // Semicolon-list-separator confusion footgun — adjacent to
10018 // the comma-separator idiom, every list-separator-belongs-
10019 // to-list-grammar confusion lands here.
10020 let c = caixa_with_licenca(Some("MIT; Apache-2.0"));
10021 let err = c.validate_licenca().unwrap_err();
10022 assert!(
10023 matches!(err, ManifestError::LicencaInvalid { .. }),
10024 "got {err:?}",
10025 );
10026 }
10027
10028 #[test]
10029 fn validate_licenca_empty_takes_precedence_over_shape() {
10030 // Empty-first cascade pin: the empty `Some("")` surfaces the
10031 // narrower `LicencaEmpty` not the shape-predicate-wrapped
10032 // `LicencaInvalid`, mirroring the peer
10033 // `validate_edicao_empty_takes_precedence_over_shape` and
10034 // `validate_repositorio_empty_takes_precedence_over_shape`
10035 // (`RepositorioEmpty` → `RepositorioInvalid`), `NomeEmpty` →
10036 // `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid` cascades.
10037 // The shape predicate also refuses the empty input
10038 // (defensively — `"must not be empty"`), but the manifest-
10039 // layer empty arm runs first to surface the narrower
10040 // diagnostic verbatim.
10041 let c = caixa_with_licenca(Some(""));
10042 let err = c.validate_licenca().unwrap_err();
10043 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
10044 }
10045
10046 #[test]
10047 fn validate_licenca_invalid_diagnostic_carries_offending_value() {
10048 // Diagnostic-shape pin on the shape-predicate arm (peer with
10049 // `validate_edicao_invalid_diagnostic_carries_offending_value`
10050 // and `validate_repositorio_diagnostic_carries_offending_value`):
10051 // the error's Display surfaces the offending value + slot
10052 // name verbatim, so a `feira lint` run can render the
10053 // diagnostic without re-parsing and the author can grep
10054 // their caixa.lisp for the offending `:licenca` value.
10055 let c = caixa_with_licenca(Some("Apache_2.0"));
10056 let rendered = c.validate_licenca().unwrap_err().to_string();
10057 assert!(
10058 rendered.contains(":licenca"),
10059 "diagnostic must name the offending slot: {rendered}",
10060 );
10061 assert!(
10062 rendered.contains("Apache_2.0"),
10063 "diagnostic must quote the offending value: {rendered}",
10064 );
10065 }
10066
10067 #[test]
10068 fn validate_licenca_rejects_empty_some() {
10069 // Canonical paste-from-blank-doc footgun. Without this gate
10070 // the empty `Some("")` silently passed the renderer's
10071 // `Option::unwrap_or_else(|| "MIT".into())` (which only
10072 // fires on `None`) and landed as a bare trailing period in
10073 // the rendered chart `README.md` `## License` section.
10074 // Mirrors the peer [`ManifestError::DescricaoEmpty`] empty-
10075 // arm on the sibling `Option<String>` Caixa slot.
10076 let c = caixa_with_licenca(Some(""));
10077 let err = c.validate_licenca().unwrap_err();
10078 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
10079 }
10080
10081 #[test]
10082 fn validate_licenca_template_passes() {
10083 // Round-trip pin: the bare `Caixa::template` shape (whether
10084 // it carries `:licenca` or omits it) passes the gate by
10085 // construction. A future template-shape change that
10086 // introduced `(:licenca "")` would surface here as a
10087 // regression. Mirrors the peer
10088 // `validate_descricao_template_passes` pin.
10089 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10090 c.validate_licenca().unwrap();
10091 }
10092
10093 #[test]
10094 fn validate_licenca_diagnostic_names_offending_slot() {
10095 // Diagnostic-shape pin (peer with
10096 // `validate_descricao_diagnostic_names_offending_slot`):
10097 // the error's Display surfaces the `:licenca` slot name
10098 // verbatim, so a `feira lint` run can render the diagnostic
10099 // without re-parsing and the author can grep their caixa.lisp
10100 // for the offending `:licenca` line.
10101 let c = caixa_with_licenca(Some(""));
10102 let rendered = c.validate_licenca().unwrap_err().to_string();
10103 assert!(
10104 rendered.contains(":licenca"),
10105 "diagnostic must name the offending slot: {rendered}",
10106 );
10107 }
10108
10109 // ── Caixa::licenca — outer top-level Option<&str> scalar accessor ──
10110
10111 #[test]
10112 fn licenca_returns_licenca_byte_string_verbatim_across_permutations() {
10113 // The canonical per-`Caixa` `:licenca` SPDX-expression scalar
10114 // pin: [`Caixa::licenca`] must return the `:licenca` typed
10115 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10116 // raw `self.licenca.as_deref()` access across every
10117 // representative value in the accept-set — `None` (the "omit
10118 // the slot to defer to the caixa-helm renderer's `MIT`
10119 // fallback" arm every existing fixture without a `:licenca`
10120 // line carries), `Some("")` (a past-the-guard sentinel that
10121 // pins the accessor doesn't perform a silent
10122 // `Some("") → None` collapse on the empty arm — validate
10123 // rejects `Some("")` through `LicencaEmpty` but the accessor
10124 // must ship the raw slot verbatim so a validate-time gate
10125 // regression surfaces at the caixa-helm emit boundary rather
10126 // than being silently absorbed into the fallback), `Some("MIT")`
10127 // (the canonical single-license shape every `feira init`
10128 // template scaffolds), `Some("Apache-2.0 OR MIT")` (the
10129 // canonical `OR`-compound shape the peer
10130 // `validate_licenca_accepts_canonical_expressions` positive
10131 // sweep exercises), `Some("(MIT OR Apache-2.0) AND
10132 // BSD-3-Clause")` (the canonical parenthesis-grouped shape),
10133 // `Some("MIT ")` / `Some(" MIT")` / `Some("MIT\n")` /
10134 // `Some("Apache_2.0")` / `Some("MIT,Apache-2.0")` (past-the-
10135 // guard sentinels — validate rejects each through
10136 // `LicencaInvalid` but the accessor must ship the raw slot
10137 // verbatim).
10138 //
10139 // First outer top-level [`Caixa`] `Option<&str>`-return scalar
10140 // accessor pin on the substrate primitive — opens the "outer
10141 // [`Caixa`] `Option<&str>` scalar" projection pattern the
10142 // sibling per-`Caixa` `:descricao` / `:repositorio` / `:edicao`
10143 // future lifts fold on. Sibling in shape to the peer per-`:placement`
10144 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10145 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10146 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10147 // axes, extended onto the outer top-level [`Caixa`] universal-
10148 // axis surface. Pins against a future silent detour that
10149 // returned an owned `Option<String>` (which would type-check
10150 // but silently allocate on every accessor call, breaking the
10151 // zero-cost projection every peer sibling accessor carries), a
10152 // `Some("") → None` collapse (which would silently absorb the
10153 // `LicencaEmpty` refusal case at the accessor boundary and the
10154 // caixa-helm emit path would silently fall back to `"MIT"` on
10155 // a struct-literal `Caixa { licenca: Some(""), .. }`), or a
10156 // `None → Some("MIT")` collapse (which would silently reify
10157 // the caixa-helm renderer's `"MIT"` fallback at the accessor
10158 // boundary and every downstream consumer keying off the
10159 // `Option::is_none()` discriminator would lose the "author
10160 // omitted the slot" signal).
10161 for licenca in [
10162 None,
10163 Some(""),
10164 Some("MIT"),
10165 Some("Apache-2.0 OR MIT"),
10166 Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
10167 Some("MIT "),
10168 Some(" MIT"),
10169 Some("MIT\n"),
10170 Some("Apache_2.0"),
10171 Some("MIT,Apache-2.0"),
10172 ] {
10173 let c = caixa_with_licenca(licenca);
10174 assert_eq!(
10175 c.licenca(),
10176 licenca,
10177 "Caixa::licenca must return :licenca verbatim (got {:?}, \
10178 expected {licenca:?})",
10179 c.licenca(),
10180 );
10181 assert_eq!(
10182 c.licenca(),
10183 c.licenca.as_deref(),
10184 "Caixa::licenca must byte-equal the raw \
10185 `self.licenca.as_deref()` field access across every \
10186 value in the Option<&str> accept-set",
10187 );
10188 }
10189 }
10190
10191 #[test]
10192 fn validate_licenca_empty_arm_routes_through_accessor() {
10193 // Composition pin: [`Caixa::validate_licenca`]'s empty-arm gate
10194 // must key off [`Caixa::licenca`], not the raw
10195 // `self.licenca.as_deref()` field access. Structurally: a
10196 // `Caixa { licenca: Some(""), .. }` must surface the
10197 // `LicencaEmpty` refusal exactly, and a
10198 // `Caixa { licenca: Some("MIT"), .. }` (the canonical
10199 // single-license form) must pass validate. The pair jointly
10200 // pins the accessor + validate-gate composition: any future
10201 // silent detour that had the accessor return `None` on the
10202 // empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10203 // silently absorb the `LicencaEmpty` refusal at the accessor
10204 // boundary and the validate gate would accept a struct-literal
10205 // `Caixa { licenca: Some(""), .. }` — the composition pin
10206 // catches that at caixa-core build time.
10207 //
10208 // Peer of the per-`:politicas :circuit-breaker`
10209 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
10210 // accessor-composition pin
10211 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
10212 // on the sibling per-M3-mesh-slot required-`u32` axis — same
10213 // "the validate / shape-gate predicate must route through the
10214 // substrate-primitive typed dispatch" discipline extended onto
10215 // the outer top-level [`Caixa`] universal-axis
10216 // `Option<&str>`-composition surface.
10217 let c = caixa_with_licenca(Some(""));
10218 assert!(
10219 matches!(c.validate_licenca(), Err(ManifestError::LicencaEmpty)),
10220 "validate_licenca must reject licenca == Some(\"\") with \
10221 LicencaEmpty — the accessor and the validate gate must \
10222 route through the same substrate-primitive typed dispatch \
10223 on the :licenca empty arm",
10224 );
10225 let c = caixa_with_licenca(Some("MIT"));
10226 assert!(
10227 c.validate_licenca().is_ok(),
10228 "validate_licenca must accept licenca == Some(\"MIT\") \
10229 (the canonical single-license SPDX shape)",
10230 );
10231 }
10232
10233 #[test]
10234 fn licenca_projects_option_str_by_borrow() {
10235 // The by-borrow pin: [`Caixa::licenca`] returns
10236 // `Option<&str>` by borrow — the `&str` borrows the underlying
10237 // `String` storage of the `Option<String>` slot and the
10238 // accessor must not allocate a fresh `String` on every call.
10239 // Peer of the per-`:placement`
10240 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10241 // borrow pin on the peer per-M3-mesh-slot
10242 // `Option<&str>`-return axis, extended onto the outer top-
10243 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10244 // accessor's returned `&str` must borrow from `&self` (the
10245 // returned reference's lifetime is tied to `&self`), and
10246 // calling the accessor twice on the same [`Caixa`] must yield
10247 // the same `Option<&str>` verbatim (idempotent, no side
10248 // effects on `&self`).
10249 //
10250 // Pins against a future silent detour that returned an owned
10251 // `Option<String>` (which would type-check but silently
10252 // allocate on every call, breaking the zero-cost projection
10253 // every peer sibling accessor carries), or a one-arm-only
10254 // accessor that returned a saturating value on some sentinel
10255 // input (breaking the pass-through invariant the sibling
10256 // required-scalar accessors carry).
10257 for licenca in [None, Some(""), Some("MIT"), Some("Apache-2.0 OR MIT")] {
10258 let c = caixa_with_licenca(licenca);
10259 let first = c.licenca();
10260 let second = c.licenca();
10261 assert_eq!(
10262 first, second,
10263 "Caixa::licenca must be idempotent — two successive \
10264 calls on the same &self must return the same \
10265 Option<&str>",
10266 );
10267 assert_eq!(
10268 first, licenca,
10269 "Caixa::licenca must return :licenca verbatim by \
10270 borrow — got {first:?}, expected {licenca:?}",
10271 );
10272 }
10273 }
10274
10275 // ── Caixa::repositorio — outer top-level Option<&str> scalar accessor ──
10276
10277 #[test]
10278 fn repositorio_returns_repositorio_byte_string_verbatim_across_permutations() {
10279 // The canonical per-`Caixa` `:repositorio` git-repo-URL scalar
10280 // pin: [`Caixa::repositorio`] must return the `:repositorio`
10281 // typed byte-string verbatim as an `Option<&str>`, byte-equal
10282 // to the raw `self.repositorio.as_deref()` access across every
10283 // representative value in the accept-set — `None` (the "omit
10284 // the slot to defer to the per-renderer placeholder" arm every
10285 // existing fixture without a `:repositorio` line carries),
10286 // `Some("")` (a past-the-guard sentinel that pins the accessor
10287 // doesn't perform a silent `Some("") → None` collapse on the
10288 // empty arm — validate rejects `Some("")` through
10289 // `RepositorioEmpty` but the accessor must ship the raw slot
10290 // verbatim so a validate-time gate regression surfaces at the
10291 // caixa-helm / caixa-flux emit boundary rather than being
10292 // silently absorbed into the per-renderer fallback),
10293 // `Some("github:pleme-io/hello-rio")` (the canonical `github:`
10294 // shorthand every existing manifest fixture across
10295 // `caixa-helm` / `caixa-mesh` and the `examples/` uses),
10296 // `Some("https://github.com/pleme-io/checkout")` (the canonical
10297 // `https://` URL the README quickstart uses),
10298 // `Some("ssh://git@github.com/pleme-io/checkout.git")` /
10299 // `Some("git://github.com/pleme-io/checkout.git")` /
10300 // `Some("git@github.com:pleme-io/checkout.git")` /
10301 // `Some("file:///opt/mirrors/pleme-io/checkout")` (every non-
10302 // github scheme the shared `is_git_repo_url` predicate
10303 // documents), and five past-the-guard sentinels for the
10304 // `RepositorioInvalid` refusal cases (`Some("pleme-io/checkout")`
10305 // missing-colon, `Some("-upload-pack=evil")` leading-dash, /
10306 // `Some("github:pleme-io/checkout?ref=main")` query-string, /
10307 // `Some("github:pleme-io/checkout#main")` fragment-anchor, /
10308 // `Some("github:pleme-io/{tpl}")` URI-template-placeholder — the
10309 // sentinels pin the accessor doesn't silently absorb the
10310 // refusal cases into a fallback).
10311 //
10312 // Second outer top-level [`Caixa`] `Option<&str>`-return scalar
10313 // accessor pin on the substrate primitive — sibling of the peer
10314 // [`Caixa::licenca`] (6d5bc28) pin
10315 // (`licenca_returns_licenca_byte_string_verbatim_across_permutations`)
10316 // that opened the "outer [`Caixa`] `Option<&str>` scalar"
10317 // projection pin pattern this pin folds on. Sibling in shape to
10318 // the peer per-`:placement`
10319 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10320 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10321 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10322 // axes, extended onto the outer top-level [`Caixa`] universal-
10323 // axis surface. Pins against a future silent detour that
10324 // returned an owned `Option<String>` (which would type-check
10325 // but silently allocate on every accessor call, breaking the
10326 // zero-cost projection every peer sibling accessor carries), a
10327 // `Some("") → None` collapse (which would silently absorb the
10328 // `RepositorioEmpty` refusal case at the accessor boundary and
10329 // the caixa-helm `Chart.yaml` `home:` fold would silently
10330 // render a `home: null` / omitted field on a struct-literal
10331 // `Caixa { repositorio: Some(""), .. }`), or a
10332 // `None → Some(<default>)` collapse (which would silently reify
10333 // the per-renderer fallback at the accessor boundary and every
10334 // downstream consumer keying off the `Option::is_none()`
10335 // discriminator would lose the "author omitted the slot"
10336 // signal).
10337 for repositorio in [
10338 None,
10339 Some(""),
10340 Some("github:pleme-io/hello-rio"),
10341 Some("https://github.com/pleme-io/checkout"),
10342 Some("ssh://git@github.com/pleme-io/checkout.git"),
10343 Some("git://github.com/pleme-io/checkout.git"),
10344 Some("git@github.com:pleme-io/checkout.git"),
10345 Some("file:///opt/mirrors/pleme-io/checkout"),
10346 Some("pleme-io/checkout"),
10347 Some("-upload-pack=evil"),
10348 Some("github:pleme-io/checkout?ref=main"),
10349 Some("github:pleme-io/checkout#main"),
10350 Some("github:pleme-io/{tpl}"),
10351 ] {
10352 let c = caixa_with_repositorio(repositorio);
10353 assert_eq!(
10354 c.repositorio(),
10355 repositorio,
10356 "Caixa::repositorio must return :repositorio verbatim \
10357 (got {:?}, expected {repositorio:?})",
10358 c.repositorio(),
10359 );
10360 assert_eq!(
10361 c.repositorio(),
10362 c.repositorio.as_deref(),
10363 "Caixa::repositorio must byte-equal the raw \
10364 `self.repositorio.as_deref()` field access across every \
10365 value in the Option<&str> accept-set",
10366 );
10367 }
10368 }
10369
10370 #[test]
10371 fn validate_repositorio_empty_arm_routes_through_accessor() {
10372 // Composition pin: [`Caixa::validate_repositorio`]'s empty-arm
10373 // gate must key off [`Caixa::repositorio`], not the raw
10374 // `self.repositorio.as_deref()` field access. Structurally: a
10375 // `Caixa { repositorio: Some(""), .. }` must surface the
10376 // `RepositorioEmpty` refusal exactly, and a
10377 // `Caixa { repositorio: Some("github:pleme-io/hello-rio"), .. }`
10378 // (the canonical `github:` shorthand form) must pass validate.
10379 // The pair jointly pins the accessor + validate-gate
10380 // composition: any future silent detour that had the accessor
10381 // return `None` on the empty arm (a `.filter(|s| !s.is_empty())`
10382 // collapse) would silently absorb the `RepositorioEmpty` refusal
10383 // at the accessor boundary and the validate gate would accept a
10384 // struct-literal `Caixa { repositorio: Some(""), .. }` — the
10385 // composition pin catches that at caixa-core build time.
10386 //
10387 // Peer of the [`Caixa::licenca`] (6d5bc28)
10388 // `validate_licenca_empty_arm_routes_through_accessor`
10389 // composition pin on the sibling outer top-level [`Caixa`]
10390 // `Option<&str>` universal-axis surface — same "the validate /
10391 // shape-gate predicate must route through the substrate-
10392 // primitive typed dispatch" discipline extended onto the second
10393 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10394 // composition surface.
10395 let c = caixa_with_repositorio(Some(""));
10396 assert!(
10397 matches!(
10398 c.validate_repositorio(),
10399 Err(ManifestError::RepositorioEmpty),
10400 ),
10401 "validate_repositorio must reject repositorio == Some(\"\") \
10402 with RepositorioEmpty — the accessor and the validate gate \
10403 must route through the same substrate-primitive typed \
10404 dispatch on the :repositorio empty arm",
10405 );
10406 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio"));
10407 assert!(
10408 c.validate_repositorio().is_ok(),
10409 "validate_repositorio must accept repositorio == \
10410 Some(\"github:pleme-io/hello-rio\") (the canonical \
10411 `github:` shorthand git-repo-URL shape)",
10412 );
10413 }
10414
10415 #[test]
10416 fn repositorio_projects_option_str_by_borrow() {
10417 // The by-borrow pin: [`Caixa::repositorio`] returns
10418 // `Option<&str>` by borrow — the `&str` borrows the underlying
10419 // `String` storage of the `Option<String>` slot and the
10420 // accessor must not allocate a fresh `String` on every call.
10421 // Peer of the per-`:placement`
10422 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) and the
10423 // [`Caixa::licenca`] (6d5bc28) by-borrow pins on the peer
10424 // `Option<&str>`-return axes, extended onto the second outer
10425 // top-level [`Caixa`] universal-axis `Option<&str>` shape —
10426 // the accessor's returned `&str` must borrow from `&self` (the
10427 // returned reference's lifetime is tied to `&self`), and
10428 // calling the accessor twice on the same [`Caixa`] must yield
10429 // the same `Option<&str>` verbatim (idempotent, no side effects
10430 // on `&self`).
10431 //
10432 // Pins against a future silent detour that returned an owned
10433 // `Option<String>` (which would type-check but silently
10434 // allocate on every call, breaking the zero-cost projection
10435 // every peer sibling accessor carries), or a one-arm-only
10436 // accessor that returned a saturating value on some sentinel
10437 // input (breaking the pass-through invariant the sibling
10438 // required-scalar accessors carry).
10439 for repositorio in [
10440 None,
10441 Some(""),
10442 Some("github:pleme-io/hello-rio"),
10443 Some("https://github.com/pleme-io/checkout"),
10444 ] {
10445 let c = caixa_with_repositorio(repositorio);
10446 let first = c.repositorio();
10447 let second = c.repositorio();
10448 assert_eq!(
10449 first, second,
10450 "Caixa::repositorio must be idempotent — two successive \
10451 calls on the same &self must return the same \
10452 Option<&str>",
10453 );
10454 assert_eq!(
10455 first, repositorio,
10456 "Caixa::repositorio must return :repositorio verbatim by \
10457 borrow — got {first:?}, expected {repositorio:?}",
10458 );
10459 }
10460 }
10461
10462 // ── Caixa::canonical_git_url — resolved-git-URL composer ──────────
10463
10464 #[test]
10465 fn canonical_git_url_returns_repositorio_verbatim_on_some_arm() {
10466 // Fail-before-pass-after pin: [`Caixa::canonical_git_url`] must
10467 // return the author-declared `:repositorio` byte-string verbatim
10468 // on the `Some` arm — no scheme rewrite, no trailing-slash
10469 // canonicalization, no `github:` → `https://github.com/`
10470 // desugaring. The resolved-URL composer is the projection of
10471 // the raw [`Caixa::repositorio`] `Option<&str>` accessor onto
10472 // the `String`-return arity every substrate-side field-fill
10473 // consumer keys off; on the `Some` arm the projection is
10474 // `str::to_owned` verbatim, so every accept-set value the
10475 // sibling `repositorio_returns_repositorio_byte_string_verbatim_
10476 // across_permutations` pin covers (`https://…`, `github:…`,
10477 // `ssh://…`, `git://…`, `git@…`, `file://…`, and the past-the-
10478 // guard sentinel `pleme-io/…`) must survive the accessor
10479 // byte-equal. Pins against a future silent detour that rewrote
10480 // the `github:` shorthand to the `https://github.com/` full URL
10481 // at the accessor boundary (which would silently split the
10482 // resolved-URL surface from the raw [`Caixa::repositorio`]
10483 // accessor's documented pass-through invariant), or a trailing-
10484 // slash normalization (which would silently break the
10485 // FluxCD `GitRepository` `spec.url` byte-exact match every
10486 // downstream consumer keys the source-controller reconcile off).
10487 for repositorio in [
10488 "github:pleme-io/hello-rio",
10489 "https://github.com/pleme-io/checkout",
10490 "ssh://git@github.com/pleme-io/checkout.git",
10491 "git://github.com/pleme-io/checkout.git",
10492 "git@github.com:pleme-io/checkout.git",
10493 "file:///opt/mirrors/pleme-io/checkout",
10494 ] {
10495 let c = caixa_with_repositorio(Some(repositorio));
10496 assert_eq!(
10497 c.canonical_git_url(),
10498 repositorio,
10499 "Caixa::canonical_git_url on the Some arm must return \
10500 :repositorio verbatim (got {:?}, expected {repositorio:?})",
10501 c.canonical_git_url(),
10502 );
10503 }
10504 }
10505
10506 #[test]
10507 fn canonical_git_url_falls_back_to_pleme_org_url_on_none_arm() {
10508 // Fail-before-pass-after pin: [`Caixa::canonical_git_url`] on the
10509 // `None` arm must emit the substrate's canonical pleme-org github
10510 // URL derived from `caixa.nome()` — `https://github.com/<org>/
10511 // <nome>` with `<org>` bound to [`crate::DEFAULT_PLEME_GIT_ORG`]
10512 // and `<nome>` bound to the typed [`Caixa::nome`] accessor. This
10513 // is the exact byte-image of the prior inline
10514 // [`caixa-flux::ClusterBundleOpts::for_caixa`] `git_url`
10515 // composer at caixa-flux/src/lib.rs:2080 that every prior caller
10516 // re-derived open-coded. Pins against a future silent detour
10517 // that migrated the `<org>` segment to a different constant (a
10518 // fork rebranding that split off a new
10519 // `DEFAULT_PLEME_GIT_ORG_MIRROR` const the accessor would need
10520 // to migrate onto), a scheme change (`https://` → `git://` or
10521 // `ssh://`), or a per-`Caixa` `.canonical_git_url_prefix`
10522 // override (which would break the substrate-wide single-source-
10523 // of-truth guarantee this method encodes).
10524 let c = caixa_with_repositorio(None);
10525 let expected = format!(
10526 "https://github.com/{org}/{nome}",
10527 org = crate::DEFAULT_PLEME_GIT_ORG,
10528 nome = c.nome(),
10529 );
10530 assert_eq!(
10531 c.canonical_git_url(),
10532 expected,
10533 "Caixa::canonical_git_url on the None arm must fold through \
10534 the substrate's canonical pleme-org github URL fallback \
10535 `https://github.com/<DEFAULT_PLEME_GIT_ORG>/<nome>` — got \
10536 {:?}, expected {expected:?}",
10537 c.canonical_git_url(),
10538 );
10539 }
10540
10541 #[test]
10542 fn canonical_git_url_byte_matches_manual_composition() {
10543 // Byte-parity pin: [`Caixa::canonical_git_url`] must render
10544 // byte-identically to the manual open-coded
10545 // `caixa.repositorio().map(str::to_owned).unwrap_or_else(||
10546 // format!("https://github.com/{org}/{nome}", ...))` composition
10547 // every prior substrate-side caller re-derived. Guards the
10548 // paired-site convergence just applied at caixa-flux's
10549 // [`ClusterBundleOpts::for_caixa`] `git_url` composer (which
10550 // now routes through this accessor): a future implementation of
10551 // this method that reordered the format arguments, swapped the
10552 // `<org>` constant for a different one, or interposed a
10553 // canonicalization pass on the `Some` arm surfaces here as a
10554 // caixa-core build-time test failure rather than as a downstream
10555 // FluxCD `GitRepository` reconcile mismatch far from this
10556 // method's source.
10557 for repositorio in [
10558 None,
10559 Some("github:pleme-io/hello-rio"),
10560 Some("https://github.com/pleme-io/checkout"),
10561 Some("ssh://git@github.com/pleme-io/checkout.git"),
10562 ] {
10563 let c = caixa_with_repositorio(repositorio);
10564 let manual = c.repositorio().map_or_else(
10565 || {
10566 format!(
10567 "https://github.com/{org}/{nome}",
10568 org = crate::DEFAULT_PLEME_GIT_ORG,
10569 nome = c.nome(),
10570 )
10571 },
10572 str::to_owned,
10573 );
10574 assert_eq!(
10575 c.canonical_git_url(),
10576 manual,
10577 "Caixa::canonical_git_url must byte-equal the manual \
10578 open-coded `repositorio().map(str::to_owned)\
10579 .unwrap_or_else(|| format!(...))` composition across \
10580 every representative :repositorio input — got {:?}, \
10581 expected {manual:?}",
10582 c.canonical_git_url(),
10583 );
10584 }
10585 }
10586
10587 // ── Caixa::publish_tag — resolved-publish-tag composer ───────────
10588
10589 #[test]
10590 fn publish_tag_composes_prefix_and_versao_on_all_shapes() {
10591 // Fail-before-pass-after pin: [`Caixa::publish_tag`] must compose
10592 // [`crate::DEFAULT_PUBLISH_TAG_PREFIX`] against the caixa's typed
10593 // [`Caixa::versao`] byte-string across every SemVer-2 shape the
10594 // sibling [`validate_versao_accepts_canonical_forms`] positive-set
10595 // sweep documents — bare MAJOR.MINOR.PATCH, pre-release tags
10596 // (`-rc.1`), build metadata (`+build.42`), the combined form, and
10597 // the `0.0.0` boundary case. Every accept-set value the peer
10598 // validate gate lets through must survive the resolved-tag
10599 // projection byte-equal.
10600 for versao in [
10601 "0.1.0",
10602 "0.0.0",
10603 "1.0.0",
10604 "1.2.3-rc.1",
10605 "1.2.3+build.42",
10606 "1.2.3-rc.1+build.42",
10607 ] {
10608 let c = caixa_with_versao(versao);
10609 let expected = format!(
10610 "{prefix}{versao}",
10611 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10612 );
10613 assert_eq!(
10614 c.publish_tag(),
10615 expected,
10616 "Caixa::publish_tag must compose \
10617 DEFAULT_PUBLISH_TAG_PREFIX ({prefix:?}) against \
10618 :versao ({versao:?}) verbatim — got {got:?}, \
10619 expected {expected:?}",
10620 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10621 got = c.publish_tag(),
10622 );
10623 }
10624 }
10625
10626 #[test]
10627 fn publish_tag_starts_with_default_publish_tag_prefix() {
10628 // Prefix-shape pin: every [`Caixa::publish_tag`] emission must
10629 // begin with the canonical [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
10630 // byte-string on every input, guarding a hypothetical future
10631 // implementation that migrated the prefix segment to an inline
10632 // literal (`"v"`) that would silently drift from any rebrand of
10633 // the lifted constant. Peer to the sibling caixa-flux
10634 // `cluster_bundle_default_git_tag_uses_lifted_caixa_core_prefix`
10635 // test which pins the same prefix invariant at the reader-side
10636 // `GitRefSpec::Tag` emit site.
10637 for versao in ["0.0.0", "0.1.0", "1.2.3-rc.1", "9.9.9+build.1"] {
10638 let c = caixa_with_versao(versao);
10639 let tag = c.publish_tag();
10640 assert!(
10641 tag.starts_with(crate::DEFAULT_PUBLISH_TAG_PREFIX),
10642 "Caixa::publish_tag emission {tag:?} must start with \
10643 the lifted crate::DEFAULT_PUBLISH_TAG_PREFIX \
10644 ({prefix:?})",
10645 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10646 );
10647 }
10648 }
10649
10650 #[test]
10651 fn publish_tag_byte_matches_manual_composition() {
10652 // Byte-parity pin: [`Caixa::publish_tag`] must render byte-
10653 // identically to the manual open-coded
10654 // `format!("{prefix}{versao}", prefix =
10655 // caixa_core::DEFAULT_PUBLISH_TAG_PREFIX, versao =
10656 // caixa.versao())` composition every prior substrate-side
10657 // caller re-derived. Guards the paired-site convergence just
10658 // applied at caixa-flux's [`ClusterBundleOpts::for_caixa`]
10659 // `git_ref` composer (which now routes through this accessor):
10660 // a future implementation of this method that reordered the
10661 // format arguments, swapped the `<prefix>` constant for a
10662 // different one, or interposed a canonicalization pass on the
10663 // `:versao` axis surfaces here as a caixa-core build-time test
10664 // failure rather than as a downstream FluxCD `GitRepository`
10665 // reconcile mismatch far from this method's source.
10666 for versao in [
10667 "0.1.0",
10668 "0.0.0",
10669 "1.2.3-rc.1",
10670 "1.2.3+build.42",
10671 "1.2.3-rc.1+build.42",
10672 ] {
10673 let c = caixa_with_versao(versao);
10674 let manual = format!(
10675 "{prefix}{versao}",
10676 prefix = crate::DEFAULT_PUBLISH_TAG_PREFIX,
10677 versao = c.versao(),
10678 );
10679 assert_eq!(
10680 c.publish_tag(),
10681 manual,
10682 "Caixa::publish_tag must byte-equal the manual \
10683 open-coded `format!(\"{{prefix}}{{versao}}\", ...)` \
10684 composition across every representative :versao input \
10685 — got {got:?}, expected {manual:?}",
10686 got = c.publish_tag(),
10687 );
10688 }
10689 }
10690
10691 // ── Caixa::descricao — outer top-level Option<&str> scalar accessor ──
10692
10693 #[test]
10694 fn descricao_returns_descricao_byte_string_verbatim_across_permutations() {
10695 // The canonical per-`Caixa` `:descricao` free-form-prose scalar
10696 // pin: [`Caixa::descricao`] must return the `:descricao` typed
10697 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10698 // raw `self.descricao.as_deref()` access across every
10699 // representative value in the accept-set — `None` (the "omit
10700 // the slot to defer to the per-renderer `caixa.nome`-derived
10701 // fallback" arm every existing fixture without a `:descricao`
10702 // line carries), `Some("")` (a past-the-guard sentinel that
10703 // pins the accessor doesn't perform a silent `Some("") → None`
10704 // collapse on the empty arm — validate rejects `Some("")`
10705 // through `DescricaoEmpty` but the accessor must ship the raw
10706 // slot verbatim so a validate-time gate regression surfaces at
10707 // the caixa-helm / caixa-feira emit boundary rather than being
10708 // silently absorbed into the per-renderer `caixa.nome`-derived
10709 // fallback), `Some("Checkout flow.")` (the canonical one-line
10710 // prose descriptor the peer
10711 // `validate_descricao_accepts_canonical_value` positive sweep
10712 // exercises), `Some("Canonical Rust→wasm32-wasip2 caixa
10713 // Servico.")` (the multi-byte Unicode continuation-byte shape
10714 // the `hello-rio` fixture carries), `Some("→ — · ✓")` (a
10715 // multi-glyph Unicode shape the peer
10716 // `is_chart_description_shape` predicate accepts), and five
10717 // past-the-guard sentinels for the `DescricaoInvalid` refusal
10718 // cases (`Some(" Checkout flow.")` leading-whitespace,
10719 // `Some("Checkout flow. ")` trailing-whitespace,
10720 // `Some("Checkout\nflow.")` embedded-LF,
10721 // `Some("Checkout\tflow.")` embedded-TAB, and
10722 // `Some("Checkout\x00flow.")` embedded-NUL — the sentinels pin
10723 // the accessor doesn't silently absorb the refusal cases into
10724 // a fallback).
10725 //
10726 // Third outer top-level [`Caixa`] `Option<&str>`-return scalar
10727 // accessor pin on the substrate primitive — sibling of the peer
10728 // [`Caixa::licenca`] (6d5bc28) and [`Caixa::repositorio`]
10729 // (cc7332d) pins that opened the "outer [`Caixa`]
10730 // `Option<&str>` scalar" projection pin pattern this pin folds
10731 // on. Sibling in shape to the peer per-`:placement`
10732 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10733 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10734 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10735 // axes, extended onto the outer top-level [`Caixa`] universal-
10736 // axis surface. Pins against a future silent detour that
10737 // returned an owned `Option<String>` (which would type-check
10738 // but silently allocate on every accessor call, breaking the
10739 // zero-cost projection every peer sibling accessor carries), a
10740 // `Some("") → None` collapse (which would silently absorb the
10741 // `DescricaoEmpty` refusal case at the accessor boundary and
10742 // the caixa-helm `Chart.yaml` `description:` fold would
10743 // silently render a `caixa.nome`-derived fallback on a
10744 // struct-literal `Caixa { descricao: Some(""), .. }`), or a
10745 // `None → Some(<default>)` collapse (which would silently
10746 // reify the per-renderer `caixa.nome`-derived fallback at the
10747 // accessor boundary and every downstream consumer keying off
10748 // the `Option::is_none()` discriminator would lose the "author
10749 // omitted the slot" signal).
10750 for descricao in [
10751 None,
10752 Some(""),
10753 Some("Checkout flow."),
10754 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10755 Some("→ — · ✓"),
10756 Some(" Checkout flow."),
10757 Some("Checkout flow. "),
10758 Some("Checkout\nflow."),
10759 Some("Checkout\tflow."),
10760 Some("Checkout\x00flow."),
10761 ] {
10762 let c = caixa_with_descricao(descricao);
10763 assert_eq!(
10764 c.descricao(),
10765 descricao,
10766 "Caixa::descricao must return :descricao verbatim (got \
10767 {:?}, expected {descricao:?})",
10768 c.descricao(),
10769 );
10770 assert_eq!(
10771 c.descricao(),
10772 c.descricao.as_deref(),
10773 "Caixa::descricao must byte-equal the raw \
10774 `self.descricao.as_deref()` field access across every \
10775 value in the Option<&str> accept-set",
10776 );
10777 }
10778 }
10779
10780 #[test]
10781 fn validate_descricao_empty_arm_routes_through_accessor() {
10782 // Composition pin: [`Caixa::validate_descricao`]'s empty-arm
10783 // gate must key off [`Caixa::descricao`], not the raw
10784 // `self.descricao.as_deref()` field access. Structurally: a
10785 // `Caixa { descricao: Some(""), .. }` must surface the
10786 // `DescricaoEmpty` refusal exactly, and a
10787 // `Caixa { descricao: Some("Checkout flow."), .. }` (the
10788 // canonical one-line-prose form) must pass validate. The pair
10789 // jointly pins the accessor + validate-gate composition: any
10790 // future silent detour that had the accessor return `None` on
10791 // the empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10792 // silently absorb the `DescricaoEmpty` refusal at the accessor
10793 // boundary and the validate gate would accept a struct-literal
10794 // `Caixa { descricao: Some(""), .. }` — the composition pin
10795 // catches that at caixa-core build time.
10796 //
10797 // Peer of the [`Caixa::licenca`] (6d5bc28)
10798 // `validate_licenca_empty_arm_routes_through_accessor` and
10799 // [`Caixa::repositorio`] (cc7332d)
10800 // `validate_repositorio_empty_arm_routes_through_accessor`
10801 // composition pins on the sibling outer top-level [`Caixa`]
10802 // `Option<&str>` universal-axis surface — same "the validate /
10803 // shape-gate predicate must route through the substrate-
10804 // primitive typed dispatch" discipline extended onto the third
10805 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10806 // composition surface.
10807 let c = caixa_with_descricao(Some(""));
10808 assert!(
10809 matches!(c.validate_descricao(), Err(ManifestError::DescricaoEmpty),),
10810 "validate_descricao must reject descricao == Some(\"\") \
10811 with DescricaoEmpty — the accessor and the validate gate \
10812 must route through the same substrate-primitive typed \
10813 dispatch on the :descricao empty arm",
10814 );
10815 let c = caixa_with_descricao(Some("Checkout flow."));
10816 assert!(
10817 c.validate_descricao().is_ok(),
10818 "validate_descricao must accept descricao == \
10819 Some(\"Checkout flow.\") (the canonical one-line-prose \
10820 chart-description shape)",
10821 );
10822 }
10823
10824 #[test]
10825 fn descricao_projects_option_str_by_borrow() {
10826 // The by-borrow pin: [`Caixa::descricao`] returns
10827 // `Option<&str>` by borrow — the `&str` borrows the underlying
10828 // `String` storage of the `Option<String>` slot and the
10829 // accessor must not allocate a fresh `String` on every call.
10830 // Peer of the [`Caixa::licenca`] (6d5bc28) and
10831 // [`Caixa::repositorio`] (cc7332d) by-borrow pins on the peer
10832 // outer top-level [`Caixa`] `Option<&str>`-return axes, and of
10833 // the per-`:placement`
10834 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10835 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10836 // return axis, extended onto the third outer top-level
10837 // [`Caixa`] universal-axis `Option<&str>` shape — the
10838 // accessor's returned `&str` must borrow from `&self` (the
10839 // returned reference's lifetime is tied to `&self`), and
10840 // calling the accessor twice on the same [`Caixa`] must yield
10841 // the same `Option<&str>` verbatim (idempotent, no side
10842 // effects on `&self`).
10843 //
10844 // Pins against a future silent detour that returned an owned
10845 // `Option<String>` (which would type-check but silently
10846 // allocate on every call, breaking the zero-cost projection
10847 // every peer sibling accessor carries), or a one-arm-only
10848 // accessor that returned a saturating value on some sentinel
10849 // input (breaking the pass-through invariant the sibling
10850 // required-scalar accessors carry).
10851 for descricao in [
10852 None,
10853 Some(""),
10854 Some("Checkout flow."),
10855 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10856 ] {
10857 let c = caixa_with_descricao(descricao);
10858 let first = c.descricao();
10859 let second = c.descricao();
10860 assert_eq!(
10861 first, second,
10862 "Caixa::descricao must be idempotent — two successive \
10863 calls on the same &self must return the same \
10864 Option<&str>",
10865 );
10866 assert_eq!(
10867 first, descricao,
10868 "Caixa::descricao must return :descricao verbatim by \
10869 borrow — got {first:?}, expected {descricao:?}",
10870 );
10871 }
10872 }
10873
10874 // ── validate_edicao — universal-axis language-edition shape ──
10875
10876 fn caixa_with_edicao(edicao: Option<&str>) -> Caixa {
10877 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10878 c.edicao = edicao.map(String::from);
10879 c
10880 }
10881
10882 #[test]
10883 fn validate_edicao_accepts_none() {
10884 // The omit-the-slot identity: `:edicao` is optional. The
10885 // gate is a no-op when the author didn't declare a value —
10886 // every caixa without an `:edicao` line trivially passes,
10887 // and the substrate-side build pipeline falls back to the
10888 // documented default edition. Mirrors the peer
10889 // `validate_licenca_accepts_none` posture on the sibling
10890 // `Option<String>` Caixa slot.
10891 let c = caixa_with_edicao(None);
10892 c.validate_edicao().unwrap();
10893 }
10894
10895 #[test]
10896 fn validate_edicao_accepts_canonical_value() {
10897 // Positive control: the canonical `"2026"` edition every
10898 // existing renderer-side fixture (`caixa-helm`, `caixa-flux`,
10899 // `caixa-mesh`) carries by construction passes the gate.
10900 // Future-introduced sibling editions (`"2027"`, `"2030"`,
10901 // `"2049"`) that match the same 4-digit ASCII decimal year
10902 // shape must also trivially pass — the structural shape
10903 // predicate accepts every well-formed year regardless of
10904 // whether the substrate yet understands the specific value
10905 // (a future known-edition allowlist tightens that).
10906 for ed in ["2026", "2027", "2030", "2049"] {
10907 let c = caixa_with_edicao(Some(ed));
10908 c.validate_edicao()
10909 .unwrap_or_else(|err| panic!("canonical {ed:?} must pass: {err:?}"));
10910 }
10911 }
10912
10913 #[test]
10914 fn validate_edicao_rejects_empty_some() {
10915 // Canonical paste-from-blank-doc footgun. Without this gate
10916 // the empty `Some("")` silently lands as `(:edicao "")` in
10917 // the rendered caixa.lisp and a future renderer-side
10918 // consumer's `Option::unwrap_or_else` (which only fires on
10919 // `None`) skips its fallback. Mirrors the peer
10920 // [`ManifestError::LicencaEmpty`] empty-arm on the sibling
10921 // `Option<String>` Caixa slot.
10922 let c = caixa_with_edicao(Some(""));
10923 let err = c.validate_edicao().unwrap_err();
10924 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10925 }
10926
10927 #[test]
10928 fn validate_edicao_rejects_free_form_non_year() {
10929 // Free-form non-year footgun: the bare `"x"` / `"latest"` /
10930 // `"nightly"` shapes carry no operational meaning on the
10931 // substrate's build-time edition selector. Until this gate
10932 // landed the bare empty-arm check let every such value
10933 // through and broke far from the source caixa.lisp. Peer
10934 // with the shape-predicate cascade
10935 // `validate_repositorio_rejects_missing_colon_separator`
10936 // establishes past its own empty arm.
10937 for ed in ["x", "latest", "nightly", "stable"] {
10938 let c = caixa_with_edicao(Some(ed));
10939 let err = c.validate_edicao().unwrap_err();
10940 assert!(
10941 matches!(err, ManifestError::EdicaoInvalid { .. }),
10942 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10943 );
10944 }
10945 }
10946
10947 #[test]
10948 fn validate_edicao_rejects_trailing_whitespace() {
10949 // Paste-from-doc whitespace footgun. A trailing space in
10950 // the `:edicao` value would silently break the substrate's
10951 // build-time edition match-table lookup at the rendered
10952 // artifact's edition-selector consumer. The shape predicate
10953 // refuses every whitespace byte by construction (any byte
10954 // outside `0-9` fails `is_ascii_digit`). Peer with
10955 // `validate_repositorio_rejects_whitespace`.
10956 let c = caixa_with_edicao(Some("2026 "));
10957 let err = c.validate_edicao().unwrap_err();
10958 let ManifestError::EdicaoInvalid { edicao, .. } = err else {
10959 panic!("expected EdicaoInvalid, got {err:?}");
10960 };
10961 assert_eq!(edicao, "2026 ");
10962 }
10963
10964 #[test]
10965 fn validate_edicao_rejects_leading_whitespace() {
10966 // Symmetric paste-from-doc whitespace footgun on the leading
10967 // boundary — the gate refuses every shape with a non-digit
10968 // byte by construction.
10969 let c = caixa_with_edicao(Some(" 2026"));
10970 let err = c.validate_edicao().unwrap_err();
10971 assert!(
10972 matches!(err, ManifestError::EdicaoInvalid { .. }),
10973 "got {err:?}",
10974 );
10975 }
10976
10977 #[test]
10978 fn validate_edicao_rejects_control_char() {
10979 // Paste-from-multiline-doc CRLF footgun — control characters
10980 // at the value boundary break the substrate's build-time
10981 // edition-selector parser. Peer with
10982 // `validate_repositorio_rejects_control_char`.
10983 let c = caixa_with_edicao(Some("2026\n"));
10984 let err = c.validate_edicao().unwrap_err();
10985 assert!(
10986 matches!(err, ManifestError::EdicaoInvalid { .. }),
10987 "got {err:?}",
10988 );
10989 }
10990
10991 #[test]
10992 fn validate_edicao_rejects_non_ascii_lookalike() {
10993 // Fullwidth-keyboard look-alike footgun — `"2026"` is
10994 // the U+FF12 U+FF10 U+FF12 U+FF16 sequence (CJK fullwidth
10995 // digits), 4 codepoints but 12 UTF-8 bytes; the substrate's
10996 // edition selector wants an ASCII year, and the gate
10997 // refuses every non-ASCII shape by construction (length in
10998 // bytes is 12 ≠ 4, *and* every byte falls outside
10999 // `is_ascii_digit`'s `0-9` range).
11000 let c = caixa_with_edicao(Some("2026"));
11001 let err = c.validate_edicao().unwrap_err();
11002 assert!(
11003 matches!(err, ManifestError::EdicaoInvalid { .. }),
11004 "got {err:?}",
11005 );
11006 }
11007
11008 #[test]
11009 fn validate_edicao_rejects_version_tag_prefix() {
11010 // Common version-tag idiom footgun — `"v2026"` / `"e2026"`
11011 // / `"r2026"` are familiar shapes from git-tag / Rust
11012 // edition / release-tag conventions that don't apply to
11013 // the year-shaped edition axis. The shape predicate refuses
11014 // every leading non-digit prefix.
11015 for ed in ["v2026", "e2026", "r2026"] {
11016 let c = caixa_with_edicao(Some(ed));
11017 let err = c.validate_edicao().unwrap_err();
11018 assert!(
11019 matches!(err, ManifestError::EdicaoInvalid { .. }),
11020 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11021 );
11022 }
11023 }
11024
11025 #[test]
11026 fn validate_edicao_rejects_decimal_shape() {
11027 // Decimal-shaped pseudo-version footgun — `"2026.1"` /
11028 // `"2026.0"` are familiar shapes from semver / float
11029 // conventions that don't apply to the year-shaped edition
11030 // axis. The shape predicate refuses every non-digit byte
11031 // (`.` falls outside `is_ascii_digit`).
11032 for ed in ["2026.1", "2026.0", "2026.0.1"] {
11033 let c = caixa_with_edicao(Some(ed));
11034 let err = c.validate_edicao().unwrap_err();
11035 assert!(
11036 matches!(err, ManifestError::EdicaoInvalid { .. }),
11037 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11038 );
11039 }
11040 }
11041
11042 #[test]
11043 fn validate_edicao_rejects_wrong_length_numeric() {
11044 // Wrong-length numeric footgun — `"26"` (truncated) /
11045 // `"202"` (truncated) / `"20260"` (extra digit) / `"00026"`
11046 // (zero-padded too wide) all parse as integers but don't
11047 // name a 4-digit year. The shape predicate refuses every
11048 // value whose length isn't exactly 4 bytes.
11049 for ed in ["26", "202", "20260", "00026", "9"] {
11050 let c = caixa_with_edicao(Some(ed));
11051 let err = c.validate_edicao().unwrap_err();
11052 assert!(
11053 matches!(err, ManifestError::EdicaoInvalid { .. }),
11054 "expected EdicaoInvalid on {ed:?}, got {err:?}",
11055 );
11056 }
11057 }
11058
11059 #[test]
11060 fn validate_edicao_empty_takes_precedence_over_shape() {
11061 // Empty-first cascade pin: the empty `Some("")` surfaces
11062 // the narrower `EdicaoEmpty` not the shape-predicate-
11063 // wrapped `EdicaoInvalid`, mirroring the peer
11064 // `validate_repositorio_empty_takes_precedence_over_shape`
11065 // (`RepositorioEmpty` → `RepositorioInvalid`),
11066 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` →
11067 // `VersaoInvalid`, `FonteRepoEmpty` → `FonteRepoInvalid`
11068 // cascades. The shape predicate also refuses the empty
11069 // input (defensively — `s.len() != 4`), but the
11070 // manifest-layer empty arm runs first to surface the
11071 // narrower diagnostic verbatim.
11072 let c = caixa_with_edicao(Some(""));
11073 let err = c.validate_edicao().unwrap_err();
11074 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
11075 }
11076
11077 #[test]
11078 fn validate_edicao_template_passes() {
11079 // Round-trip pin: the bare `Caixa::template` shape (which
11080 // carries `:edicao "2026"` verbatim) passes the gate by
11081 // construction. A future template-shape change that
11082 // introduced `(:edicao "")` or a non-year value would
11083 // surface here as a regression. Mirrors the peer
11084 // `validate_licenca_template_passes` pin.
11085 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11086 c.validate_edicao().unwrap();
11087 }
11088
11089 #[test]
11090 fn validate_edicao_diagnostic_names_offending_slot() {
11091 // Diagnostic-shape pin (peer with
11092 // `validate_licenca_diagnostic_names_offending_slot`): the
11093 // error's Display surfaces the `:edicao` slot name verbatim,
11094 // so a `feira lint` run can render the diagnostic without
11095 // re-parsing and the author can grep their caixa.lisp for
11096 // the offending `:edicao` line.
11097 let c = caixa_with_edicao(Some(""));
11098 let rendered = c.validate_edicao().unwrap_err().to_string();
11099 assert!(
11100 rendered.contains(":edicao"),
11101 "diagnostic must name the offending slot: {rendered}",
11102 );
11103 }
11104
11105 #[test]
11106 fn validate_edicao_invalid_diagnostic_carries_offending_value() {
11107 // Diagnostic-shape pin on the shape-predicate arm (peer
11108 // with `validate_repositorio_diagnostic_carries_offending_value`):
11109 // the error's Display surfaces the offending value + slot
11110 // name verbatim, so a `feira lint` run can render the
11111 // diagnostic without re-parsing and the author can grep
11112 // their caixa.lisp for the offending `:edicao` value.
11113 let c = caixa_with_edicao(Some("v2026"));
11114 let rendered = c.validate_edicao().unwrap_err().to_string();
11115 assert!(
11116 rendered.contains(":edicao"),
11117 "diagnostic must name the offending slot: {rendered}",
11118 );
11119 assert!(
11120 rendered.contains("v2026"),
11121 "diagnostic must quote the offending value: {rendered}",
11122 );
11123 }
11124
11125 // ── Caixa::edicao — outer top-level Option<&str> scalar accessor ──
11126
11127 #[test]
11128 fn edicao_returns_edicao_byte_string_verbatim_across_permutations() {
11129 // The canonical per-`Caixa` `:edicao` language-edition scalar
11130 // pin: [`Caixa::edicao`] must return the `:edicao` typed
11131 // byte-string verbatim as an `Option<&str>`, byte-equal to the
11132 // raw `self.edicao.as_deref()` access across every representative
11133 // value in the accept-set — `None` (the "omit the slot to defer
11134 // to the substrate's default edition" arm every existing
11135 // [`caixa-resolver`] fixture without an `:edicao` line carries),
11136 // `Some("")` (a past-the-guard sentinel that pins the accessor
11137 // doesn't perform a silent `Some("") → None` collapse on the
11138 // empty arm — validate rejects `Some("")` through `EdicaoEmpty`
11139 // but the accessor must ship the raw slot verbatim so a
11140 // validate-time gate regression surfaces at any future edition-
11141 // aware consumer's boundary rather than being silently absorbed
11142 // into the substrate's default edition), `Some("2026")` (the
11143 // canonical 4-digit-ASCII-decimal-year shape every `feira init`
11144 // template scaffolds via [`Caixa::template`] and every
11145 // renderer-side fixture at `caixa-helm/src/lib.rs:978` /
11146 // `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208`
11147 // carries by construction), `Some("2018")` / `Some("2021")` /
11148 // `Some("2024")` (canonical 4-digit-ASCII-decimal-year shapes
11149 // peer with Cargo's `[package] edition` grammar every future-
11150 // introduced sibling to `"2026"` will follow), and eight
11151 // past-the-guard sentinels for the `EdicaoInvalid` refusal cases
11152 // (`Some("2026 ")` trailing-whitespace, `Some(" 2026")` leading-
11153 // whitespace, `Some("2026\n")` embedded-LF, `Some("2026")`
11154 // fullwidth-non-ASCII-lookalike, `Some("v2026")` version-tag-
11155 // prefix, `Some("2026.1")` decimal-shape, `Some("26")` wrong-
11156 // length-numeric, `Some("latest")` free-form-non-year — the
11157 // sentinels pin the accessor doesn't silently absorb the
11158 // refusal cases into a substrate-default-edition fallback).
11159 //
11160 // Fourth and final outer top-level [`Caixa`] `Option<&str>`-
11161 // return scalar accessor pin on the substrate primitive —
11162 // sibling of the peer [`Caixa::licenca`] (6d5bc28),
11163 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
11164 // (3f16e2f) pins that opened the "outer [`Caixa`]
11165 // `Option<&str>` scalar" projection pin pattern this pin folds
11166 // on. Sibling in shape to the peer per-`:placement`
11167 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
11168 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
11169 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
11170 // axes, extended onto the outer top-level [`Caixa`] universal-
11171 // axis surface's last unlifted `Option<String>` slot. Pins
11172 // against a future silent detour that returned an owned
11173 // `Option<String>` (which would type-check but silently
11174 // allocate on every accessor call, breaking the zero-cost
11175 // projection every peer sibling accessor carries), a
11176 // `Some("") → None` collapse (which would silently absorb the
11177 // `EdicaoEmpty` refusal case at the accessor boundary and any
11178 // future edition-aware consumer would silently fall back to
11179 // the substrate's default edition on a struct-literal
11180 // `Caixa { edicao: Some(""), .. }`), or a
11181 // `None → Some("2026")` collapse (which would silently reify
11182 // the substrate's default edition at the accessor boundary
11183 // and every downstream consumer keying off the
11184 // `Option::is_none()` discriminator would lose the "author
11185 // omitted the slot" signal).
11186 for edicao in [
11187 None,
11188 Some(""),
11189 Some("2026"),
11190 Some("2018"),
11191 Some("2021"),
11192 Some("2024"),
11193 Some("2026 "),
11194 Some(" 2026"),
11195 Some("2026\n"),
11196 Some("2026"),
11197 Some("v2026"),
11198 Some("2026.1"),
11199 Some("26"),
11200 Some("latest"),
11201 ] {
11202 let c = caixa_with_edicao(edicao);
11203 assert_eq!(
11204 c.edicao(),
11205 edicao,
11206 "Caixa::edicao must return :edicao verbatim (got {:?}, \
11207 expected {edicao:?})",
11208 c.edicao(),
11209 );
11210 assert_eq!(
11211 c.edicao(),
11212 c.edicao.as_deref(),
11213 "Caixa::edicao must byte-equal the raw \
11214 `self.edicao.as_deref()` field access across every \
11215 value in the Option<&str> accept-set",
11216 );
11217 }
11218 }
11219
11220 #[test]
11221 fn validate_edicao_empty_arm_routes_through_accessor() {
11222 // Composition pin: [`Caixa::validate_edicao`]'s empty-arm gate
11223 // must key off [`Caixa::edicao`], not the raw
11224 // `self.edicao.as_deref()` field access. Structurally: a
11225 // `Caixa { edicao: Some(""), .. }` must surface the
11226 // `EdicaoEmpty` refusal exactly, and a
11227 // `Caixa { edicao: Some("2026"), .. }` (the canonical
11228 // 4-digit-ASCII-decimal-year form) must pass validate. The
11229 // pair jointly pins the accessor + validate-gate composition:
11230 // any future silent detour that had the accessor return `None`
11231 // on the empty arm (a `.filter(|s| !s.is_empty())` collapse)
11232 // would silently absorb the `EdicaoEmpty` refusal at the
11233 // accessor boundary and the validate gate would accept a
11234 // struct-literal `Caixa { edicao: Some(""), .. }` — the
11235 // composition pin catches that at caixa-core build time.
11236 //
11237 // Peer of the [`Caixa::licenca`] (6d5bc28)
11238 // `validate_licenca_empty_arm_routes_through_accessor`,
11239 // [`Caixa::repositorio`] (cc7332d)
11240 // `validate_repositorio_empty_arm_routes_through_accessor`,
11241 // and [`Caixa::descricao`] (3f16e2f)
11242 // `validate_descricao_empty_arm_routes_through_accessor`
11243 // composition pins on the sibling outer top-level [`Caixa`]
11244 // `Option<&str>` universal-axis surface — same "the validate /
11245 // shape-gate predicate must route through the substrate-
11246 // primitive typed dispatch" discipline extended onto the
11247 // fourth and final outer top-level [`Caixa`] universal-axis
11248 // `Option<&str>`-composition surface, closing the accessor-
11249 // composition family.
11250 let c = caixa_with_edicao(Some(""));
11251 assert!(
11252 matches!(c.validate_edicao(), Err(ManifestError::EdicaoEmpty)),
11253 "validate_edicao must reject edicao == Some(\"\") with \
11254 EdicaoEmpty — the accessor and the validate gate must \
11255 route through the same substrate-primitive typed dispatch \
11256 on the :edicao empty arm",
11257 );
11258 let c = caixa_with_edicao(Some("2026"));
11259 assert!(
11260 c.validate_edicao().is_ok(),
11261 "validate_edicao must accept edicao == Some(\"2026\") \
11262 (the canonical 4-digit-ASCII-decimal-year shape)",
11263 );
11264 }
11265
11266 #[test]
11267 fn edicao_projects_option_str_by_borrow() {
11268 // The by-borrow pin: [`Caixa::edicao`] returns
11269 // `Option<&str>` by borrow — the `&str` borrows the underlying
11270 // `String` storage of the `Option<String>` slot and the
11271 // accessor must not allocate a fresh `String` on every call.
11272 // Peer of the [`Caixa::licenca`] (6d5bc28),
11273 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
11274 // (3f16e2f) by-borrow pins on the peer outer top-level
11275 // [`Caixa`] `Option<&str>`-return axes, and of the
11276 // per-`:placement`
11277 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
11278 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
11279 // return axis, extended onto the fourth and final outer top-
11280 // level [`Caixa`] universal-axis `Option<&str>` shape — the
11281 // accessor's returned `&str` must borrow from `&self` (the
11282 // returned reference's lifetime is tied to `&self`), and
11283 // calling the accessor twice on the same [`Caixa`] must yield
11284 // the same `Option<&str>` verbatim (idempotent, no side
11285 // effects on `&self`).
11286 //
11287 // Pins against a future silent detour that returned an owned
11288 // `Option<String>` (which would type-check but silently
11289 // allocate on every call, breaking the zero-cost projection
11290 // every peer sibling accessor carries), or a one-arm-only
11291 // accessor that returned a saturating value on some sentinel
11292 // input (breaking the pass-through invariant the sibling
11293 // required-scalar accessors carry).
11294 for edicao in [None, Some(""), Some("2026"), Some("2018")] {
11295 let c = caixa_with_edicao(edicao);
11296 let first = c.edicao();
11297 let second = c.edicao();
11298 assert_eq!(
11299 first, second,
11300 "Caixa::edicao must be idempotent — two successive \
11301 calls on the same &self must return the same \
11302 Option<&str>",
11303 );
11304 assert_eq!(
11305 first, edicao,
11306 "Caixa::edicao must return :edicao verbatim by \
11307 borrow — got {first:?}, expected {edicao:?}",
11308 );
11309 }
11310 }
11311
11312 #[test]
11313 fn nome_returns_nome_byte_string_verbatim_across_permutations() {
11314 // The canonical per-`Caixa` `:nome` universal-axis DNS-1123-
11315 // label caixa-identity scalar pin: [`Caixa::nome`] must return
11316 // the `:nome` typed `String` verbatim as `&str`, byte-equal to
11317 // the raw field access across every representative value in
11318 // the accept-set — the canonical `"demo"` template baseline
11319 // (the same `feira init`-scaffolded default the sibling
11320 // `validate_nome_accepts_canonical_template` positive-control
11321 // gate pins), plus every sibling per-typed-slot atom accessor's
11322 // canonical positive-arm byte-string (`"catalog"` per
11323 // [`crate::aplicacao::Membro::nome`], `"cart"` per the peer
11324 // per-`:contratos` `:de`, `"hello-rio"` per the canonical
11325 // `caixa-helm`/`caixa-flux` cross-crate integration-test
11326 // fixture, `"checkout"` per the M3 mesh-slot Aplicacao
11327 // canonical example), plus every past-the-guard sentinel for
11328 // the `NomeEmpty` / `NomeInvalid` / `NomeChartNameBudgetExceeded`
11329 // refusal cases (`""`, `"Bad_Name"`, `"a"` × 56 — 56 bytes fits
11330 // the bare DNS-1123 63-byte cap but overflows the joint
11331 // `lareira-<nome>` chart-name budget the sibling
11332 // [`Caixa::validate_nome_chart_name_budget`] gate closes on).
11333 //
11334 // The past-the-guard sentinels pin the accessor doesn't
11335 // silently absorb the refusal cases into a template-derived
11336 // fallback (a future `.nome().is_empty().then(|| "demo")`
11337 // collapse would silently absorb the `NomeEmpty` refusal at
11338 // the accessor boundary and the validate gate would accept a
11339 // struct-literal `Caixa { nome: "".into(), .. }` — the pin
11340 // catches that at caixa-core build time).
11341 //
11342 // First outer top-level [`Caixa`] `&str`-return required-
11343 // scalar accessor pin — opens the "outer [`Caixa`] `&str`
11344 // required-scalar" projection pattern the sibling per-`Caixa`
11345 // `:versao` future lift folds on. Sibling in shape to the peer
11346 // per-`:membros` [`crate::aplicacao::Membro::nome`] (4a32abf)
11347 // required-`String`-carry accessor pin on the sibling per-
11348 // sub-struct required-axis, extended onto the outer top-level
11349 // [`Caixa`] universal-axis required-`String`-carry axis.
11350 for nome in [
11351 "demo",
11352 "catalog",
11353 "cart",
11354 "hello-rio",
11355 "checkout",
11356 "",
11357 "Bad_Name",
11358 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
11359 ] {
11360 let c = caixa_with_nome(nome);
11361 assert_eq!(
11362 c.nome(),
11363 nome,
11364 "Caixa::nome must return :nome verbatim (got {}, \
11365 expected {nome})",
11366 c.nome(),
11367 );
11368 assert_eq!(
11369 c.nome(),
11370 c.nome.as_str(),
11371 "Caixa::nome must byte-equal the raw .nome field \
11372 access across every value in the String accept-set",
11373 );
11374 }
11375 }
11376
11377 #[test]
11378 fn validate_nome_empty_arm_routes_through_accessor() {
11379 // Composition pin: [`Caixa::validate_nome`]'s empty-arm must
11380 // key off [`Caixa::nome`], not the raw `.nome` field access.
11381 // Structurally: a `Caixa { nome: "".into(), .. }` must surface
11382 // the `NomeEmpty` refusal exactly, and the canonical `"demo"`
11383 // template baseline (the peer positive-arm the sibling
11384 // `validate_nome_accepts_canonical_template` gate carves out)
11385 // must pass validate. The pair jointly pins the accessor +
11386 // validate-gate composition: any future silent detour that
11387 // had the accessor return a fresh `"demo"` on the empty arm
11388 // (a `.nome().is_empty().then(|| "demo")` fallback collapse)
11389 // would silently absorb the `NomeEmpty` refusal at the
11390 // accessor boundary and the validate gate would accept a
11391 // struct-literal `Caixa { nome: "".into(), .. }` — the
11392 // composition pin catches that at caixa-core build time.
11393 //
11394 // Peer of the sibling per-`Caixa`
11395 // `validate_licenca_empty_arm_routes_through_accessor` (6d5bc28)
11396 // / `validate_repositorio_empty_arm_routes_through_accessor`
11397 // (cc7332d) / `validate_descricao_empty_arm_routes_through_accessor`
11398 // (3f16e2f) / `validate_edicao_empty_arm_routes_through_accessor`
11399 // (2641cbd) composition pins on the sibling outer top-level
11400 // [`Caixa`] `Option<&str>` axes — same "the validate /
11401 // shape-gate predicate must route through the substrate-
11402 // primitive typed dispatch" discipline extended onto the peer
11403 // outer top-level [`Caixa`] required-`&str` composition axis.
11404 let c = caixa_with_nome("");
11405 assert!(
11406 matches!(c.validate_nome(), Err(ManifestError::NomeEmpty)),
11407 "validate_nome must reject nome == \"\" with NomeEmpty — \
11408 the accessor and the validate gate must route through the \
11409 same substrate-primitive typed dispatch on the :nome \
11410 empty-arm",
11411 );
11412 let c = caixa_with_nome("demo");
11413 assert!(
11414 c.validate_nome().is_ok(),
11415 "validate_nome must accept nome == \"demo\" (the canonical \
11416 DNS-1123-label template baseline)",
11417 );
11418 }
11419
11420 #[test]
11421 fn nome_projects_str_by_borrow() {
11422 // The by-borrow pin: [`Caixa::nome`] returns `&str` by borrow
11423 // — the `&str` borrows the underlying `String` storage of the
11424 // required `nome` slot and the accessor must not allocate a
11425 // fresh `String` on every call. Peer of the [`Caixa::licenca`]
11426 // (6d5bc28) / [`Caixa::repositorio`] (cc7332d) /
11427 // [`Caixa::descricao`] (3f16e2f) / [`Caixa::edicao`] (2641cbd)
11428 // by-borrow pins on the peer outer top-level [`Caixa`]
11429 // `Option<&str>`-return axes, extended onto the first outer
11430 // top-level [`Caixa`] required-`&str`-return axis — the
11431 // accessor's returned `&str` must borrow from `&self` (the
11432 // returned reference's lifetime is tied to `&self`), and
11433 // calling the accessor twice on the same [`Caixa`] must yield
11434 // the same `&str` verbatim (idempotent, no side effects on
11435 // `&self`).
11436 //
11437 // Pins against a future silent detour that returned an owned
11438 // `String` (which would type-check but silently allocate on
11439 // every call, breaking the zero-cost projection every peer
11440 // sibling accessor carries), an accidental
11441 // `.nome.to_lowercase()` detour that returned a fresh
11442 // allocation through an already-DNS-1123-lowercase-only
11443 // string (breaking a future `const fn` regression), or a
11444 // one-arm-only accessor that returned a canonicalized value
11445 // on some sentinel input (breaking the pass-through invariant
11446 // the sibling required-scalar accessors carry).
11447 for nome in ["demo", "catalog", "hello-rio", "checkout"] {
11448 let c = caixa_with_nome(nome);
11449 let first = c.nome();
11450 let second = c.nome();
11451 assert_eq!(
11452 first, second,
11453 "Caixa::nome must be idempotent — two successive calls \
11454 on the same &self must return the same &str",
11455 );
11456 assert_eq!(
11457 first, nome,
11458 "Caixa::nome must return :nome verbatim by borrow — \
11459 got {first}, expected {nome}",
11460 );
11461 }
11462 }
11463
11464 #[test]
11465 fn versao_returns_versao_byte_string_verbatim_across_permutations() {
11466 // The canonical per-`Caixa` `:versao` universal-axis SemVer-2
11467 // pinned-version scalar pin: [`Caixa::versao`] must return the
11468 // `:versao` typed `String` verbatim as `&str`, byte-equal to the
11469 // raw `.versao` field access across every representative value
11470 // in the accept-set — the canonical `"0.1.0"` template baseline
11471 // (the same `feira init`-scaffolded default the sibling
11472 // `validate_versao_accepts_canonical_template` positive-control
11473 // gate pins), plus every canonical SemVer-2 shape the sibling
11474 // `validate_versao_accepts_canonical_forms` positive-arm sweep
11475 // covers (`"0.0.0"`, `"1.0.0"`, `"0.2.0-rc.1"`,
11476 // `"1.0.0-alpha.0"`, `"1.0.0+build.42"`, `"1.0.0-rc.1+build.42"`,
11477 // `"10.20.30"`), plus every past-the-guard sentinel for the
11478 // `VersaoEmpty` / `VersaoInvalid` refusal cases (`""` the empty
11479 // arm, `"v0.1.0"` the git-tag-shape-leak footgun, `"0.1"` the
11480 // missing-patch footgun, `"^0.1"` the requirement-shape-leak
11481 // footgun, `"0.1.0.0"` the four-part-Java-convention footgun,
11482 // `"latest"` the docker-tag-shape footgun — the sentinels pin
11483 // the accessor doesn't silently absorb the refusal cases into a
11484 // template-derived fallback like `"0.1.0"`).
11485 //
11486 // The past-the-guard sentinels pin the accessor doesn't silently
11487 // absorb the refusal cases into a template-derived fallback (a
11488 // future `.versao().is_empty().then(|| "0.1.0")` collapse would
11489 // silently absorb the `VersaoEmpty` refusal at the accessor
11490 // boundary and the validate gate would accept a struct-literal
11491 // `Caixa { versao: "".into(), .. }` — the pin catches that at
11492 // caixa-core build time).
11493 //
11494 // Second outer top-level [`Caixa`] `&str`-return required-scalar
11495 // accessor pin — folds on the "outer [`Caixa`] `&str` required-
11496 // scalar" projection pattern the sibling per-`Caixa`
11497 // [`Caixa::nome`] (e6b7d97) opened. Sibling in shape to the peer
11498 // per-`:membros` [`crate::aplicacao::Membro::versao_requirement`]
11499 // (4127bb6) / per-`:children`
11500 // [`crate::supervisor::ChildSpec::versao_requirement`] (2c053c8)
11501 // / per-`:upgrade-from`
11502 // [`crate::UpgradeFromEntry::prior_versao`] (75d27a8) per-sub-
11503 // struct `:versao`-shaped `&str`-return accessor pins on the
11504 // sibling per-typed-slot version-carrier axes, extended onto the
11505 // second outer top-level [`Caixa`] universal-axis required-
11506 // `String`-carry axis so the two universal-axis identity-
11507 // carrying scalars every `defcaixa` form supplies (`:nome` +
11508 // `:versao`) share the same "one typed dispatch per axis" pin
11509 // discipline.
11510 for versao in [
11511 "0.1.0",
11512 "0.0.0",
11513 "1.0.0",
11514 "0.2.0-rc.1",
11515 "1.0.0-alpha.0",
11516 "1.0.0+build.42",
11517 "1.0.0-rc.1+build.42",
11518 "10.20.30",
11519 "",
11520 "v0.1.0",
11521 "0.1",
11522 "^0.1",
11523 "0.1.0.0",
11524 "latest",
11525 ] {
11526 let c = caixa_with_versao(versao);
11527 assert_eq!(
11528 c.versao(),
11529 versao,
11530 "Caixa::versao must return :versao verbatim (got {}, \
11531 expected {versao})",
11532 c.versao(),
11533 );
11534 assert_eq!(
11535 c.versao(),
11536 c.versao.as_str(),
11537 "Caixa::versao must byte-equal the raw .versao field \
11538 access across every value in the String accept-set",
11539 );
11540 }
11541 }
11542
11543 #[test]
11544 fn validate_versao_empty_arm_routes_through_accessor() {
11545 // Composition pin: [`Caixa::validate_versao`]'s empty-arm gate
11546 // must key off [`Caixa::versao`], not the raw `.versao` field
11547 // access. Structurally: a `Caixa { versao: "".into(), .. }` must
11548 // surface the `VersaoEmpty` refusal exactly, and the canonical
11549 // `"0.1.0"` template baseline (the peer positive-arm the sibling
11550 // `validate_versao_accepts_canonical_template` gate carves out)
11551 // must pass validate. The pair jointly pins the accessor +
11552 // validate-gate composition: any future silent detour that had
11553 // the accessor return a fresh `"0.1.0"` on the empty arm
11554 // (a `.versao().is_empty().then(|| "0.1.0")` fallback collapse)
11555 // would silently absorb the `VersaoEmpty` refusal at the
11556 // accessor boundary and the validate gate would accept a
11557 // struct-literal `Caixa { versao: "".into(), .. }` — the
11558 // composition pin catches that at caixa-core build time.
11559 //
11560 // Peer of the sibling per-`Caixa`
11561 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97)
11562 // composition pin on the sibling outer top-level [`Caixa`]
11563 // required-`&str` universal-axis surface — same "the validate /
11564 // shape-gate predicate must route through the substrate-
11565 // primitive typed dispatch" discipline extended onto the peer
11566 // outer top-level [`Caixa`] required-`&str` universal-axis
11567 // pinned-version composition axis, closing the second
11568 // coordinate of the "one canonical typed dispatch per per-Caixa
11569 // required-`&str` universal-axis" discipline.
11570 let c = caixa_with_versao("");
11571 assert!(
11572 matches!(c.validate_versao(), Err(ManifestError::VersaoEmpty)),
11573 "validate_versao must reject versao == \"\" with VersaoEmpty — \
11574 the accessor and the validate gate must route through the \
11575 same substrate-primitive typed dispatch on the :versao \
11576 empty-arm",
11577 );
11578 let c = caixa_with_versao("0.1.0");
11579 assert!(
11580 c.validate_versao().is_ok(),
11581 "validate_versao must accept versao == \"0.1.0\" (the \
11582 canonical SemVer-2 template baseline)",
11583 );
11584 }
11585
11586 #[test]
11587 fn versao_projects_str_by_borrow() {
11588 // The by-borrow pin: [`Caixa::versao`] returns `&str` by borrow
11589 // — the `&str` borrows the underlying `String` storage of the
11590 // required `versao` slot and the accessor must not allocate a
11591 // fresh `String` on every call. Peer of the [`Caixa::nome`]
11592 // (e6b7d97) by-borrow pin on the sibling outer top-level
11593 // [`Caixa`] required-`&str`-return axis, extended onto the
11594 // second outer top-level [`Caixa`] required-`&str`-return
11595 // universal-axis pinned-version surface — the accessor's
11596 // returned `&str` must borrow from `&self` (the returned
11597 // reference's lifetime is tied to `&self`), and calling the
11598 // accessor twice on the same [`Caixa`] must yield the same
11599 // `&str` verbatim (idempotent, no side effects on `&self`).
11600 //
11601 // Pins against a future silent detour that returned an owned
11602 // `String` (which would type-check but silently allocate on
11603 // every call, breaking the zero-cost projection every peer
11604 // sibling accessor carries), an accidental
11605 // `semver::Version::parse(&self.versao).unwrap().to_string()`
11606 // detour that returned a canonicalized fresh allocation through
11607 // an already-canonical byte-string (breaking a future `const fn`
11608 // regression and silently absorbing the `VersaoInvalid` refusal
11609 // at the accessor boundary), or a one-arm-only accessor that
11610 // returned a canonicalized value on some sentinel input
11611 // (breaking the pass-through invariant the sibling required-
11612 // scalar accessors carry).
11613 for versao in ["0.1.0", "1.0.0", "0.2.0-rc.1", "1.0.0+build.42"] {
11614 let c = caixa_with_versao(versao);
11615 let first = c.versao();
11616 let second = c.versao();
11617 assert_eq!(
11618 first, second,
11619 "Caixa::versao must be idempotent — two successive \
11620 calls on the same &self must return the same &str",
11621 );
11622 assert_eq!(
11623 first, versao,
11624 "Caixa::versao must return :versao verbatim by borrow \
11625 — got {first}, expected {versao}",
11626 );
11627 }
11628 }
11629
11630 fn caixa_with_kind(kind: CaixaKind) -> Caixa {
11631 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11632 c.kind = kind;
11633 c
11634 }
11635
11636 #[test]
11637 fn kind_returns_kind_variant_verbatim_across_permutations() {
11638 // The canonical per-`Caixa` `:kind` universal-axis closed-set-
11639 // enum discriminant pin: [`Caixa::kind`] must return the `:kind`
11640 // typed [`CaixaKind`] variant verbatim by `Copy`, byte-equal to
11641 // the raw `.kind` field access across every variant in the
11642 // closed accept-set (`Biblioteca` — the library kind that
11643 // exports lisp forms; `Binario` — the nix-built executable kind
11644 // under `exe/`; `Servico` — the wasm-component daemon kind
11645 // under `servicos/`; `Supervisor` — the OTP-shaped hierarchical
11646 // reconciliation kind; `Aplicacao` — the M3 typed-mesh
11647 // composition kind).
11648 //
11649 // Pins against a future silent detour that re-derived the kind
11650 // from a peer axis (an accidental fallback to
11651 // `if !servicos.is_empty() { Servico } else if
11652 // !membros.is_empty() { Aplicacao } else { Biblioteca }`
11653 // collapse that read the code-surface / mesh-slot columns into
11654 // the kind discriminator), a variant remap the operator
11655 // authors on one consumer without the other, or a stale-derive
11656 // detour that substituted [`CaixaKind::Biblioteca`] as the
11657 // default when the field held any other variant (which would
11658 // silently collapse the distinction between "author explicitly
11659 // declared `:kind Servico`" and "author declared any other
11660 // kind" every downstream renderer-dispatch site depends on).
11661 //
11662 // First outer top-level [`Caixa`] `Copy`-return required-enum-
11663 // discriminant accessor pin — opens the "outer [`Caixa`]
11664 // `Copy`-return required-discriminant" projection pattern.
11665 // Sibling in shape to the peer per-`:supervisor`
11666 // [`crate::supervisor::SupervisorSpec::estrategia`] (eafb619),
11667 // per-`:placement` [`crate::aplicacao::Placement::estrategia`]
11668 // (921fe1b), and per-`:children`
11669 // [`crate::supervisor::ChildSpec::restart`] (dfb4a81)
11670 // `Copy`-return closed-set-enum discriminant accessor pins on
11671 // the sibling nested-spec typed-slot discriminator axes,
11672 // extended here to the outer top-level [`Caixa`] universal-
11673 // axis surface.
11674 for kind in [
11675 CaixaKind::Biblioteca,
11676 CaixaKind::Binario,
11677 CaixaKind::Servico,
11678 CaixaKind::Supervisor,
11679 CaixaKind::Aplicacao,
11680 ] {
11681 let c = caixa_with_kind(kind);
11682 assert_eq!(
11683 c.kind(),
11684 kind,
11685 "Caixa::kind must return :kind verbatim (got {:?}, \
11686 expected {kind:?})",
11687 c.kind(),
11688 );
11689 assert_eq!(
11690 c.kind(),
11691 c.kind,
11692 "Caixa::kind accessor and .kind field access must \
11693 byte-equal — the accessor is the substrate-primitive \
11694 typed dispatch every downstream kind-gate consumer \
11695 must route through",
11696 );
11697 }
11698 }
11699
11700 #[test]
11701 fn require_kind_reads_through_lifted_kind_accessor() {
11702 // Two-consumer coherence pin: the [`crate::render::require_kind`]
11703 // entry-gate predicate (the canonical two-line
11704 // `require_kind(caixa, Servico)?` prelude every per-Servico /
11705 // per-Aplicacao renderer runs at its entry-point) and the
11706 // sibling [`crate::render::KindMismatch`] error carrier's
11707 // `actual:` field (which names the offending caixa's variant
11708 // in the diagnostic) must both key off the lifted accessor, so
11709 // any future rebrand on the typed slot's reader shape lands at
11710 // exactly one place. Pins the two-site coherence by exercising
11711 // every off-diagonal `(actual, expected)` pair across the
11712 // closed accept-set — the `KindMismatch { actual, expected }`
11713 // surfaced on the mismatch arm must byte-equal the pair the
11714 // accessor returns for each side.
11715 //
11716 // Peer of the sibling per-`:placement`
11717 // `validate_placement_reads_through_lifted_estrategia_accessor`
11718 // (921fe1b) two-arm consumer-coherence pin on the M3 mesh-slot
11719 // `Copy`-return discriminant axis — same "the entry-gate
11720 // predicate and the error carrier's `actual:` field must route
11721 // through the substrate-primitive typed dispatch" discipline
11722 // extended onto the outer top-level [`Caixa`] universal-axis
11723 // discriminant surface.
11724 for expected in [
11725 CaixaKind::Biblioteca,
11726 CaixaKind::Binario,
11727 CaixaKind::Servico,
11728 CaixaKind::Supervisor,
11729 CaixaKind::Aplicacao,
11730 ] {
11731 for actual in [
11732 CaixaKind::Biblioteca,
11733 CaixaKind::Binario,
11734 CaixaKind::Servico,
11735 CaixaKind::Supervisor,
11736 CaixaKind::Aplicacao,
11737 ] {
11738 let c = caixa_with_kind(actual);
11739 let result = crate::render::require_kind(&c, expected);
11740 if expected == actual {
11741 assert!(
11742 result.is_ok(),
11743 "require_kind must accept when actual == expected \
11744 (actual={actual:?}, expected={expected:?})",
11745 );
11746 } else {
11747 let err = result.expect_err("require_kind must reject when actual != expected");
11748 assert_eq!(
11749 err.actual,
11750 c.kind(),
11751 "KindMismatch.actual must byte-equal Caixa::kind() \
11752 — the error carrier's `actual:` field reads \
11753 through the lifted accessor",
11754 );
11755 assert_eq!(
11756 err.expected, expected,
11757 "KindMismatch.expected must byte-equal the \
11758 expected variant passed to require_kind",
11759 );
11760 }
11761 }
11762 }
11763 }
11764
11765 #[test]
11766 fn aplicacao_view_kind_gate_routes_through_accessor() {
11767 // Composition pin: [`Caixa::aplicacao_view`]'s kind-gate arm
11768 // must key off [`Caixa::kind`], not the raw `.kind` field
11769 // access. Structurally: a `Caixa { kind: X, .. }` for any
11770 // non-`Aplicacao` variant must fold to `None` on the
11771 // `aplicacao_view` composer (the "kind mismatch → no typed
11772 // view" contract every downstream Aplicacao consumer keys off
11773 // via `?`), and a `Caixa { kind: Aplicacao, .. }` must fold to
11774 // `Some(_)`. The pair jointly pins the accessor + view-gate
11775 // composition: any future silent detour that had the accessor
11776 // return a fresh [`CaixaKind::Aplicacao`] on some sentinel
11777 // input would silently absorb the kind-mismatch case at the
11778 // accessor boundary and every per-Aplicacao renderer would
11779 // silently render a non-Aplicacao caixa's mesh slots — the
11780 // composition pin catches that at caixa-core build time.
11781 //
11782 // Peer of the sibling per-`Caixa`
11783 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97) /
11784 // `validate_versao_empty_arm_routes_through_accessor` (20c0539)
11785 // composition pins on the sibling outer top-level [`Caixa`]
11786 // required-`&str` universal-axis surfaces — same "the
11787 // composer / validate gate must route through the substrate-
11788 // primitive typed dispatch" discipline extended onto the
11789 // outer top-level [`Caixa`] `Copy`-return required-
11790 // discriminant composition axis.
11791 for kind in [
11792 CaixaKind::Biblioteca,
11793 CaixaKind::Binario,
11794 CaixaKind::Servico,
11795 CaixaKind::Supervisor,
11796 ] {
11797 let c = caixa_with_kind(kind);
11798 assert!(
11799 c.aplicacao_view().is_none(),
11800 "aplicacao_view must return None on non-Aplicacao \
11801 kind {kind:?} — the composer's kind-gate must route \
11802 through Caixa::kind()",
11803 );
11804 }
11805 let c = caixa_with_kind(CaixaKind::Aplicacao);
11806 assert!(
11807 c.aplicacao_view().is_some(),
11808 "aplicacao_view must return Some on kind Aplicacao — \
11809 the composer's kind-gate must accept the matching arm \
11810 through Caixa::kind()",
11811 );
11812 }
11813
11814 #[test]
11815 fn supervisor_view_kind_gate_routes_through_accessor() {
11816 // Composition pin (mirror of the sibling
11817 // `aplicacao_view_kind_gate_routes_through_accessor` on the
11818 // second `_view` composer): [`Caixa::supervisor_view`]'s kind-
11819 // gate arm must key off [`Caixa::kind`], not the raw `.kind`
11820 // field access. A `Caixa { kind: X, .. }` for any non-
11821 // `Supervisor` variant must fold to `None` on the
11822 // `supervisor_view` composer, and a `Caixa { kind:
11823 // Supervisor, .. }` must fold to `Some(_)`. Same peer
11824 // composition pin discipline on the second `_view` composer
11825 // axis.
11826 for kind in [
11827 CaixaKind::Biblioteca,
11828 CaixaKind::Binario,
11829 CaixaKind::Servico,
11830 CaixaKind::Aplicacao,
11831 ] {
11832 let c = caixa_with_kind(kind);
11833 assert!(
11834 c.supervisor_view().is_none(),
11835 "supervisor_view must return None on non-Supervisor \
11836 kind {kind:?} — the composer's kind-gate must route \
11837 through Caixa::kind()",
11838 );
11839 }
11840 let mut c = caixa_with_kind(CaixaKind::Supervisor);
11841 // A Supervisor caixa needs a strategy + at least one child to
11842 // fold to a Some(_) that also validates; the composer itself
11843 // requires only the kind arm, so bare kind flip is enough to
11844 // pin the `Some(_)` return, but we populate the minimum
11845 // supervisor shape so a future strengthening of the composer
11846 // to reject an empty spec doesn't false-positive this pin.
11847 c.estrategia = Some(crate::supervisor::RestartStrategy::OneForOne);
11848 c.children = vec![crate::supervisor::ChildSpec {
11849 caixa: "child".into(),
11850 versao: "^0.1".into(),
11851 restart: crate::supervisor::RestartPolicy::Permanent,
11852 }];
11853 assert!(
11854 c.supervisor_view().is_some(),
11855 "supervisor_view must return Some on kind Supervisor — \
11856 the composer's kind-gate must accept the matching arm \
11857 through Caixa::kind()",
11858 );
11859 }
11860
11861 #[test]
11862 fn kind_projects_by_copy() {
11863 // The by-`Copy` pin: [`Caixa::kind`] returns a fresh
11864 // [`CaixaKind`] by `Copy` — the accessor must not borrow from
11865 // `&self` (the returned value is owned, `Copy`-projected from
11866 // the underlying [`CaixaKind`] storage; two calls on the same
11867 // [`Caixa`] must yield byte-equal values). Peer of the peer
11868 // per-`:placement` `Placement::estrategia` / per-`:supervisor`
11869 // `SupervisorSpec::estrategia` / per-`:children`
11870 // `ChildSpec::restart` `Copy`-return discriminant accessor
11871 // pins on the sibling nested-spec typed-slot discriminator
11872 // axes, extended onto the first outer top-level [`Caixa`]
11873 // required-`Copy`-return axis — pins against a future silent
11874 // detour that returned `&CaixaKind` (which would type-check
11875 // but silently constrain every consumer's callsite to a
11876 // borrow-shaped dispatch, breaking the zero-cost `Copy`
11877 // projection every peer sibling accessor carries).
11878 for kind in [
11879 CaixaKind::Biblioteca,
11880 CaixaKind::Binario,
11881 CaixaKind::Servico,
11882 CaixaKind::Supervisor,
11883 CaixaKind::Aplicacao,
11884 ] {
11885 let c = caixa_with_kind(kind);
11886 let first: CaixaKind = c.kind();
11887 let second: CaixaKind = c.kind();
11888 assert_eq!(
11889 first, second,
11890 "Caixa::kind must be idempotent — two successive \
11891 calls on the same &self must return the same \
11892 CaixaKind variant",
11893 );
11894 assert_eq!(
11895 first, kind,
11896 "Caixa::kind must return :kind verbatim by Copy — \
11897 got {first:?}, expected {kind:?}",
11898 );
11899 }
11900 }
11901
11902 // ── Caixa::autores — outer top-level &[T] slice accessor ──────────
11903
11904 #[test]
11905 fn autores_returns_autores_slice_verbatim_across_permutations() {
11906 // The canonical per-`Caixa` `:autores` universal-axis maintainer-
11907 // name-list slice pin: [`Caixa::autores`] must return the
11908 // `:autores` typed [`Vec<String>`] list verbatim as a
11909 // `&[String]`, byte-equal to the raw `self.autores.as_slice()`
11910 // access across every representative value in the accept-set —
11911 // `[]` (the "no maintainers declared" arm every existing
11912 // fixture without an `:autores` line carries), `[""]` (a past-
11913 // the-guard sentinel that pins the accessor doesn't perform a
11914 // silent `[""] → []` collapse on the empty-entry arm — validate
11915 // rejects `[""]` through `AutorEmpty` but the accessor must
11916 // ship the raw slot verbatim so a validate-time gate regression
11917 // surfaces at the caixa-helm emit boundary rather than being
11918 // silently absorbed into a maintainer-drop), `["pleme-io"]` (the
11919 // canonical single-maintainer form every `feira init` template
11920 // scaffolds), `["alice", "bob"]` (a canonical multi-maintainer
11921 // form), `["alice <alice@example.com>", "bob <bob@example.com>"]`
11922 // (the canonical RFC-5322 `<name> <email>` form the
11923 // `is_chart_maintainer_name_shape` predicate accepts), and
11924 // `["pleme-io", "pleme-io"]` (a past-the-guard duplicate
11925 // sentinel — validate rejects through `AutorDuplicate` but the
11926 // accessor must ship the raw slot verbatim).
11927 //
11928 // First outer top-level [`Caixa`] `&[T]`-return slice accessor
11929 // pin on the substrate primitive — opens the "outer [`Caixa`]
11930 // `&[T]` slice" projection pattern the sibling per-`Caixa`
11931 // `:etiquetas` / `:deps` / `:deps-dev` / `:exe` / `:bibliotecas`
11932 // / `:servicos` / `:upgrade-from` / `:children` future lifts
11933 // fold on. Sibling in shape to the peer per-`:supervisor`
11934 // [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
11935 // per-`:placement` [`crate::aplicacao::Placement::clusters`]
11936 // (a6e18d7), per-`:membros`
11937 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
11938 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11939 // (0dcc926), and per-`:upgrade-from :instructions`
11940 // [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
11941 // `&[T]`-return slice accessor pins on the sibling per-M2 /
11942 // per-M3 typed-slot list axes, extended onto the outer top-
11943 // level [`Caixa`] universal-axis surface. Pins against a future
11944 // silent detour that returned an owned `Vec<String>` (which
11945 // would type-check but silently clone on every accessor call,
11946 // breaking the zero-cost projection every peer sibling slice
11947 // accessor carries), a `[""] → []` collapse (which would
11948 // silently absorb the `AutorEmpty` refusal case at the accessor
11949 // boundary), or a `["a", "a"] → ["a"]` dedup collapse (which
11950 // would silently absorb the `AutorDuplicate` refusal case at
11951 // the accessor boundary and the caixa-helm `maintainers:` fold
11952 // would silently render a dedupped list on a struct-literal
11953 // `Caixa { autores: vec!["a".into(), "a".into()], .. }`).
11954 for autores in [
11955 vec![],
11956 vec![""],
11957 vec!["pleme-io"],
11958 vec!["alice", "bob"],
11959 vec!["alice <alice@example.com>", "bob <bob@example.com>"],
11960 vec!["pleme-io", "pleme-io"],
11961 ] {
11962 let c = caixa_with_autores(autores.clone());
11963 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11964 assert_eq!(
11965 c.autores(),
11966 expected.as_slice(),
11967 "Caixa::autores must return :autores verbatim (got {:?}, \
11968 expected {expected:?})",
11969 c.autores(),
11970 );
11971 assert_eq!(
11972 c.autores(),
11973 c.autores.as_slice(),
11974 "Caixa::autores must byte-equal the raw \
11975 `self.autores.as_slice()` field access across every \
11976 value in the Vec<String> accept-set",
11977 );
11978 }
11979 }
11980
11981 #[test]
11982 fn validate_autores_empty_entry_arm_routes_through_accessor() {
11983 // Composition pin: [`Caixa::validate_autores`]'s per-entry
11984 // empty-arm gate must key off [`Caixa::autores`], not the raw
11985 // `&self.autores` field-borrow walk. Structurally: a
11986 // `Caixa { autores: vec!["".into()], .. }` must surface the
11987 // `AutorEmpty` refusal exactly, and a
11988 // `Caixa { autores: vec!["pleme-io".into()], .. }` (the
11989 // canonical single-maintainer form) must pass validate. The
11990 // pair jointly pins the accessor + validate-gate composition:
11991 // any future silent detour that had the accessor return an
11992 // empty slice on the `[""]` arm (a
11993 // `.iter().filter(|s| !s.is_empty()).collect()` collapse)
11994 // would silently absorb the `AutorEmpty` refusal at the
11995 // accessor boundary and the validate gate would accept a
11996 // struct-literal `Caixa { autores: vec!["".into()], .. }` —
11997 // the composition pin catches that at caixa-core build time.
11998 //
11999 // Peer of the per-`Caixa` [`Caixa::validate_licenca`] (6d5bc28)
12000 // accessor-composition pin
12001 // (`validate_licenca_empty_arm_routes_through_accessor`) on the
12002 // sibling `Option<&str>`-composition axis and the
12003 // per-`:politicas :circuit-breaker`
12004 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
12005 // accessor-composition pin
12006 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
12007 // on the sibling required-`u32`-composition axis — same "the
12008 // validate / shape-gate predicate must route through the
12009 // substrate-primitive typed dispatch" discipline extended onto
12010 // the outer top-level [`Caixa`] universal-axis `&[T]`-
12011 // composition surface.
12012 let c = caixa_with_autores(vec![""]);
12013 assert!(
12014 matches!(c.validate_autores(), Err(ManifestError::AutorEmpty)),
12015 "validate_autores must reject autores == vec![\"\"] with \
12016 AutorEmpty — the accessor and the validate gate must \
12017 route through the same substrate-primitive typed dispatch \
12018 on the :autores per-entry empty arm",
12019 );
12020 let c = caixa_with_autores(vec!["pleme-io"]);
12021 assert!(
12022 c.validate_autores().is_ok(),
12023 "validate_autores must accept autores == vec![\"pleme-io\"] \
12024 (the canonical single-maintainer shape every `feira init` \
12025 template scaffolds)",
12026 );
12027 }
12028
12029 #[test]
12030 fn autores_projects_slice_by_borrow() {
12031 // The by-borrow pin: [`Caixa::autores`] returns `&[String]` by
12032 // borrow — the returned slice borrows the underlying
12033 // `Vec<String>` storage of the `:autores` slot and the
12034 // accessor must not clone the backing `Vec` on every call.
12035 // Peer of the per-`:membros`
12036 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36) /
12037 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
12038 // (0dcc926) / per-`:placement`
12039 // [`crate::aplicacao::Placement::clusters`] (a6e18d7) /
12040 // per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
12041 // (bc92bce) by-borrow pins on the sibling per-M2 / per-M3
12042 // typed-slot `&[T]`-return axes, extended onto the outer top-
12043 // level [`Caixa`] universal-axis `&[String]` shape — the
12044 // accessor's returned slice must borrow from `&self` (the
12045 // returned reference's lifetime is tied to `&self`), and
12046 // calling the accessor twice on the same [`Caixa`] must yield
12047 // slices that are pointer-equal (the underlying byte-buffer is
12048 // the storage `Vec`'s allocation, not a fresh copy) as well as
12049 // value-equal (idempotent, no side effects on `&self`).
12050 //
12051 // Pins against a future silent detour that returned an owned
12052 // `Vec<String>` (which would type-check but silently clone on
12053 // every call, breaking the zero-cost projection every peer
12054 // sibling slice accessor carries), a `&Vec<String>` return
12055 // (which would leak the backing `Vec`'s grow/push/reserve
12056 // surface no downstream consumer reaches for), or a one-arm-
12057 // only accessor that returned a saturating value on some
12058 // sentinel input (breaking the pass-through invariant the
12059 // sibling slice accessors carry).
12060 for autores in [
12061 vec![],
12062 vec!["pleme-io"],
12063 vec!["alice", "bob"],
12064 vec!["pleme-io", "pleme-io"],
12065 ] {
12066 let c = caixa_with_autores(autores.clone());
12067 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
12068 let first = c.autores();
12069 let second = c.autores();
12070 assert_eq!(
12071 first, second,
12072 "Caixa::autores must be idempotent — two successive \
12073 calls on the same &self must return the same \
12074 &[String]",
12075 );
12076 assert_eq!(
12077 first.as_ptr(),
12078 second.as_ptr(),
12079 "Caixa::autores must borrow the underlying Vec<String> \
12080 storage — two successive calls must return slices \
12081 with the same backing pointer (a fresh Vec<String> \
12082 clone would change the pointer on every call)",
12083 );
12084 assert_eq!(
12085 first,
12086 expected.as_slice(),
12087 "Caixa::autores must return :autores verbatim by \
12088 borrow — got {first:?}, expected {expected:?}",
12089 );
12090 }
12091 }
12092
12093 // ── Caixa::etiquetas — outer top-level &[T] slice accessor ────────
12094
12095 #[test]
12096 fn etiquetas_returns_etiquetas_slice_verbatim_across_permutations() {
12097 // The canonical per-`Caixa` `:etiquetas` universal-axis
12098 // registry-search-tag-list slice pin: [`Caixa::etiquetas`] must
12099 // return the `:etiquetas` typed [`Vec<String>`] list verbatim
12100 // as a `&[String]`, byte-equal to the raw
12101 // `self.etiquetas.as_slice()` access across every representative
12102 // value in the accept-set — `[]` (the "no tags declared" arm
12103 // every existing fixture without an `:etiquetas` line carries),
12104 // `[""]` (a past-the-guard sentinel that pins the accessor
12105 // doesn't perform a silent `[""] → []` collapse on the empty-
12106 // entry arm — validate rejects `[""]` through `EtiquetaEmpty`
12107 // but the accessor must ship the raw slot verbatim so a
12108 // validate-time gate regression surfaces at the caixa-helm emit
12109 // boundary rather than being silently absorbed into a keyword-
12110 // drop), `["demo"]` (the canonical single-tag form every
12111 // `feira init` template scaffolds), `["example", "aplicacao",
12112 // "mesh", "ecommerce", "demo"]` (the canonical multi-tag form
12113 // the checkout-aplicacao fixture emits), and `["demo", "demo"]`
12114 // (a past-the-guard duplicate sentinel — validate rejects
12115 // through `EtiquetaDuplicate` but the accessor must ship the
12116 // raw slot verbatim so the caixa-helm `BTreeSet::collect` dedup
12117 // at chart-render time isn't silently promoted into the
12118 // accessor boundary and struct-literal
12119 // `Caixa { etiquetas: vec!["demo".into(), "demo".into()], .. }`
12120 // fixtures continue to expose the duplicate at the accessor).
12121 //
12122 // Second outer top-level [`Caixa`] `&[T]`-return slice accessor
12123 // pin on the substrate primitive — folds on the "outer
12124 // [`Caixa`] `&[T]` slice" projection pattern
12125 // `autores_returns_autores_slice_verbatim_across_permutations`
12126 // (b5d813f) opened, sibling in shape and idiom. Pins against a
12127 // future silent detour that returned an owned `Vec<String>`
12128 // (which would type-check but silently clone on every accessor
12129 // call, breaking the zero-cost projection every peer sibling
12130 // slice accessor carries), a `[""] → []` collapse (which would
12131 // silently absorb the `EtiquetaEmpty` refusal case at the
12132 // accessor boundary), or a `["a", "a"] → ["a"]` dedup collapse
12133 // (which would silently absorb the `EtiquetaDuplicate` refusal
12134 // case at the accessor boundary — the caixa-helm chart-render
12135 // `BTreeSet::collect` dedup is downstream of the accessor and
12136 // must not be silently promoted into it).
12137 for etiquetas in [
12138 vec![],
12139 vec![""],
12140 vec!["demo"],
12141 vec!["example", "aplicacao", "mesh", "ecommerce", "demo"],
12142 vec!["demo", "demo"],
12143 ] {
12144 let c = caixa_with_etiquetas(etiquetas.clone());
12145 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
12146 assert_eq!(
12147 c.etiquetas(),
12148 expected.as_slice(),
12149 "Caixa::etiquetas must return :etiquetas verbatim (got \
12150 {:?}, expected {expected:?})",
12151 c.etiquetas(),
12152 );
12153 assert_eq!(
12154 c.etiquetas(),
12155 c.etiquetas.as_slice(),
12156 "Caixa::etiquetas must byte-equal the raw \
12157 `self.etiquetas.as_slice()` field access across every \
12158 value in the Vec<String> accept-set",
12159 );
12160 }
12161 }
12162
12163 #[test]
12164 fn validate_etiquetas_empty_entry_arm_routes_through_accessor() {
12165 // Composition pin: [`Caixa::validate_etiquetas`]'s per-entry
12166 // empty-arm gate must key off [`Caixa::etiquetas`], not the raw
12167 // `&self.etiquetas` field-borrow walk. Structurally: a
12168 // `Caixa { etiquetas: vec!["".into()], .. }` must surface the
12169 // `EtiquetaEmpty` refusal exactly, and a
12170 // `Caixa { etiquetas: vec!["demo".into()], .. }` (the canonical
12171 // single-tag form) must pass validate. The pair jointly pins
12172 // the accessor + validate-gate composition: any future silent
12173 // detour that had the accessor return an empty slice on the
12174 // `[""]` arm (a
12175 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12176 // silently absorb the `EtiquetaEmpty` refusal at the accessor
12177 // boundary and the validate gate would accept a struct-literal
12178 // `Caixa { etiquetas: vec!["".into()], .. }` — the composition
12179 // pin catches that at caixa-core build time.
12180 //
12181 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
12182 // through_accessor` (b5d813f) accessor-composition pin on the
12183 // sibling `&[T]`-composition axis — same "the validate / shape-
12184 // gate predicate must route through the substrate-primitive
12185 // typed dispatch" discipline extended onto the sibling outer
12186 // top-level [`Caixa`] `&[T]`-composition surface.
12187 let c = caixa_with_etiquetas(vec![""]);
12188 assert!(
12189 matches!(c.validate_etiquetas(), Err(ManifestError::EtiquetaEmpty)),
12190 "validate_etiquetas must reject etiquetas == vec![\"\"] \
12191 with EtiquetaEmpty — the accessor and the validate gate \
12192 must route through the same substrate-primitive typed \
12193 dispatch on the :etiquetas per-entry empty arm",
12194 );
12195 let c = caixa_with_etiquetas(vec!["demo"]);
12196 assert!(
12197 c.validate_etiquetas().is_ok(),
12198 "validate_etiquetas must accept etiquetas == vec![\"demo\"] \
12199 (the canonical single-tag shape every `feira init` \
12200 template scaffolds)",
12201 );
12202 }
12203
12204 #[test]
12205 fn etiquetas_projects_slice_by_borrow() {
12206 // The by-borrow pin: [`Caixa::etiquetas`] returns `&[String]`
12207 // by borrow — the returned slice borrows the underlying
12208 // `Vec<String>` storage of the `:etiquetas` slot and the
12209 // accessor must not clone the backing `Vec` on every call.
12210 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12211 // (b5d813f) by-borrow pin on the sibling outer top-level
12212 // [`Caixa`] `&[String]`-return axis — the accessor's returned
12213 // slice must borrow from `&self` (the returned reference's
12214 // lifetime is tied to `&self`), and calling the accessor twice
12215 // on the same [`Caixa`] must yield slices that are pointer-
12216 // equal (the underlying byte-buffer is the storage `Vec`'s
12217 // allocation, not a fresh copy) as well as value-equal
12218 // (idempotent, no side effects on `&self`).
12219 //
12220 // Pins against a future silent detour that returned an owned
12221 // `Vec<String>` (which would type-check but silently clone on
12222 // every call, breaking the zero-cost projection every peer
12223 // sibling slice accessor carries), a `&Vec<String>` return
12224 // (which would leak the backing `Vec`'s grow/push/reserve
12225 // surface no downstream consumer reaches for), or a one-arm-
12226 // only accessor that returned a saturating value on some
12227 // sentinel input (breaking the pass-through invariant the
12228 // sibling slice accessors carry).
12229 for etiquetas in [
12230 vec![],
12231 vec!["demo"],
12232 vec!["example", "aplicacao", "mesh"],
12233 vec!["demo", "demo"],
12234 ] {
12235 let c = caixa_with_etiquetas(etiquetas.clone());
12236 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
12237 let first = c.etiquetas();
12238 let second = c.etiquetas();
12239 assert_eq!(
12240 first, second,
12241 "Caixa::etiquetas must be idempotent — two successive \
12242 calls on the same &self must return the same \
12243 &[String]",
12244 );
12245 assert_eq!(
12246 first.as_ptr(),
12247 second.as_ptr(),
12248 "Caixa::etiquetas must borrow the underlying \
12249 Vec<String> storage — two successive calls must \
12250 return slices with the same backing pointer (a fresh \
12251 Vec<String> clone would change the pointer on every \
12252 call)",
12253 );
12254 assert_eq!(
12255 first,
12256 expected.as_slice(),
12257 "Caixa::etiquetas must return :etiquetas verbatim by \
12258 borrow — got {first:?}, expected {expected:?}",
12259 );
12260 }
12261 }
12262
12263 // ── Caixa::bibliotecas — outer top-level &[T] slice accessor ──────
12264
12265 #[test]
12266 fn bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations() {
12267 // The canonical per-`Caixa` `:bibliotecas` universal-axis
12268 // library-source-path-list slice pin: [`Caixa::bibliotecas`]
12269 // must return the `:bibliotecas` typed [`Vec<String>`] list
12270 // verbatim as a `&[String]`, byte-equal to the raw
12271 // `self.bibliotecas.as_slice()` access across every
12272 // representative value in the accept-set — `[]` (the "no
12273 // libraries declared" arm every `:kind` other than `Biblioteca`
12274 // + every `Biblioteca` relying on the canonical
12275 // `lib/<nome>.lisp` implicit-default path carries; the
12276 // layout's [`crate::LayoutInvariants`] `MissingLib` arm-gate
12277 // fires exactly on this empty-slot + `Biblioteca`-kind
12278 // combination), `[""]` (a past-the-guard sentinel that pins
12279 // the accessor doesn't perform a silent `[""] → []` collapse
12280 // on the empty-entry arm — validate rejects `[""]` through
12281 // `CodePathEmpty { slot: ":bibliotecas" }` but the accessor
12282 // must ship the raw slot verbatim so a validate-time gate
12283 // regression surfaces at the `feira build` phase-1 parse
12284 // boundary rather than being silently absorbed into a
12285 // library-drop), `["lib/demo.lisp"]` (the canonical single-
12286 // entry form `Caixa::template` scaffolds and every `feira init`
12287 // template emits), `["lib/demo.lisp", "lib/helpers.lisp"]`
12288 // (the canonical multi-library form the
12289 // `validate_code_paths_accepts_explicit_relative_paths_on_
12290 // every_slot` fixture emits), and `["lib/foo.lisp",
12291 // "lib/foo.lisp"]` (a past-the-guard duplicate sentinel —
12292 // validate rejects through `CodePathDuplicate { slot:
12293 // ":bibliotecas" }` per the per-slot set-not-multiset gate,
12294 // but the accessor must ship the raw slot verbatim so the
12295 // `feira build` `for entry in caixa.bibliotecas()` parse walk
12296 // sees the duplicate at the accessor boundary and struct-
12297 // literal `Caixa { bibliotecas: vec!["lib/foo.lisp".into(),
12298 // "lib/foo.lisp".into()], .. }` fixtures continue to expose
12299 // the duplicate at the accessor).
12300 //
12301 // Third outer top-level [`Caixa`] `&[T]`-return slice accessor
12302 // pin on the substrate primitive — folds on the "outer
12303 // [`Caixa`] `&[T]` slice" projection pattern
12304 // `autores_returns_autores_slice_verbatim_across_permutations`
12305 // (b5d813f) opened and
12306 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12307 // (78c7d3c) folded on, sibling in shape and idiom. Pins
12308 // against a future silent detour that returned an owned
12309 // `Vec<String>` (which would type-check but silently clone on
12310 // every accessor call, breaking the zero-cost projection
12311 // every peer sibling slice accessor carries), a `[""] → []`
12312 // collapse (which would silently absorb the `CodePathEmpty`
12313 // refusal case at the accessor boundary), or a `["lib/foo.lisp",
12314 // "lib/foo.lisp"] → ["lib/foo.lisp"]` dedup collapse (which
12315 // would silently absorb the `CodePathDuplicate` refusal case
12316 // at the accessor boundary — the per-slot set-not-multiset
12317 // gate is downstream of the accessor and must not be silently
12318 // promoted into it).
12319 for bibliotecas in [
12320 vec![],
12321 vec![""],
12322 vec!["lib/demo.lisp"],
12323 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12324 vec!["lib/foo.lisp", "lib/foo.lisp"],
12325 ] {
12326 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12327 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12328 assert_eq!(
12329 c.bibliotecas(),
12330 expected.as_slice(),
12331 "Caixa::bibliotecas must return :bibliotecas verbatim \
12332 (got {:?}, expected {expected:?})",
12333 c.bibliotecas(),
12334 );
12335 assert_eq!(
12336 c.bibliotecas(),
12337 c.bibliotecas.as_slice(),
12338 "Caixa::bibliotecas must byte-equal the raw \
12339 `self.bibliotecas.as_slice()` field access across \
12340 every value in the Vec<String> accept-set",
12341 );
12342 }
12343 }
12344
12345 #[test]
12346 fn validate_code_paths_bibliotecas_empty_arm_routes_through_accessor() {
12347 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12348 // empty-arm gate on the `:bibliotecas` slot must key off
12349 // [`Caixa::bibliotecas`], not a divergent raw
12350 // `&self.bibliotecas` field-borrow walk. Structurally: a
12351 // `Caixa { bibliotecas: vec!["".into()], .. }` must surface
12352 // the `CodePathEmpty { slot: ":bibliotecas" }` refusal
12353 // exactly, and a `Caixa { bibliotecas: vec!["lib/demo.lisp".
12354 // into()], .. }` (the canonical single-library form
12355 // `Caixa::template` scaffolds) must pass validate. The pair
12356 // jointly pins the accessor + validate-gate composition: any
12357 // future silent detour that had the accessor return an empty
12358 // slice on the `[""]` arm (a `.iter().filter(|s|
12359 // !s.is_empty()).collect()` collapse) would silently absorb
12360 // the `CodePathEmpty` refusal at the accessor boundary and
12361 // the validate gate would accept a struct-literal
12362 // `Caixa { bibliotecas: vec!["".into()], .. }` — the
12363 // composition pin catches that at caixa-core build time.
12364 //
12365 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
12366 // through_accessor` (b5d813f) and
12367 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12368 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12369 // composition axes — same "the validate / shape-gate
12370 // predicate must route through the substrate-primitive typed
12371 // dispatch" discipline extended onto the sibling outer top-
12372 // level [`Caixa`] `&[T]`-composition surface. Nominally the
12373 // in-tree `validate_code_paths` production body still keys
12374 // off the internal `[(":bibliotecas", &self.bibliotecas,
12375 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12376 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12377 // (the tuple's homogeneous slice-typed shape blocks a per-
12378 // element accessor swap in isolation — a future companion
12379 // lift for `:exe` and `:servicos` on the same outer-`Caixa`
12380 // `&[T]` slice-accessor axis closes that tuple onto the
12381 // triple of typed dispatches as a unit); the composition pin
12382 // catches any future accessor-side silent filter drop against
12383 // that eventual tuple-closure regardless of whether the
12384 // `:bibliotecas` slot is threaded through the accessor or the
12385 // raw field access at the tuple's construction site.
12386 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
12387 assert!(
12388 matches!(
12389 c.validate_code_paths(),
12390 Err(ManifestError::CodePathEmpty {
12391 slot: ":bibliotecas"
12392 })
12393 ),
12394 "validate_code_paths must reject bibliotecas == vec![\"\"] \
12395 with CodePathEmpty {{ slot: \":bibliotecas\" }} — the \
12396 accessor and the validate gate must route through the \
12397 same substrate-primitive typed dispatch on the \
12398 :bibliotecas per-entry empty arm",
12399 );
12400 let c = caixa_with_code_paths(vec!["lib/demo.lisp"], vec![], vec![]);
12401 assert!(
12402 c.validate_code_paths().is_ok(),
12403 "validate_code_paths must accept bibliotecas == \
12404 vec![\"lib/demo.lisp\"] (the canonical single-library \
12405 shape every `feira init` template scaffolds)",
12406 );
12407 }
12408
12409 #[test]
12410 fn bibliotecas_projects_slice_by_borrow() {
12411 // The by-borrow pin: [`Caixa::bibliotecas`] returns
12412 // `&[String]` by borrow — the returned slice borrows the
12413 // underlying `Vec<String>` storage of the `:bibliotecas` slot
12414 // and the accessor must not clone the backing `Vec` on every
12415 // call. Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12416 // (b5d813f) and `etiquetas_projects_slice_by_borrow` (78c7d3c)
12417 // by-borrow pins on the sibling outer top-level [`Caixa`]
12418 // `&[String]`-return axes — the accessor's returned slice
12419 // must borrow from `&self` (the returned reference's lifetime
12420 // is tied to `&self`), and calling the accessor twice on the
12421 // same [`Caixa`] must yield slices that are pointer-equal
12422 // (the underlying byte-buffer is the storage `Vec`'s
12423 // allocation, not a fresh copy) as well as value-equal
12424 // (idempotent, no side effects on `&self`).
12425 //
12426 // Pins against a future silent detour that returned an owned
12427 // `Vec<String>` (which would type-check but silently clone on
12428 // every call, breaking the zero-cost projection every peer
12429 // sibling slice accessor carries), a `&Vec<String>` return
12430 // (which would leak the backing `Vec`'s grow/push/reserve
12431 // surface no downstream consumer reaches for), or a one-arm-
12432 // only accessor that returned a saturating value on some
12433 // sentinel input (breaking the pass-through invariant the
12434 // sibling slice accessors carry).
12435 for bibliotecas in [
12436 vec![],
12437 vec!["lib/demo.lisp"],
12438 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12439 vec!["lib/foo.lisp", "lib/foo.lisp"],
12440 ] {
12441 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12442 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12443 let first = c.bibliotecas();
12444 let second = c.bibliotecas();
12445 assert_eq!(
12446 first, second,
12447 "Caixa::bibliotecas must be idempotent — two \
12448 successive calls on the same &self must return the \
12449 same &[String]",
12450 );
12451 assert_eq!(
12452 first.as_ptr(),
12453 second.as_ptr(),
12454 "Caixa::bibliotecas must borrow the underlying \
12455 Vec<String> storage — two successive calls must \
12456 return slices with the same backing pointer (a \
12457 fresh Vec<String> clone would change the pointer on \
12458 every call)",
12459 );
12460 assert_eq!(
12461 first,
12462 expected.as_slice(),
12463 "Caixa::bibliotecas must return :bibliotecas verbatim \
12464 by borrow — got {first:?}, expected {expected:?}",
12465 );
12466 }
12467 }
12468
12469 // ── Caixa::exe — outer top-level &[T] slice accessor ──────────────
12470
12471 #[test]
12472 fn exe_returns_exe_slice_verbatim_across_permutations() {
12473 // The canonical per-`Caixa` `:exe` universal-axis
12474 // nix-built-executable-entry-path-list slice pin: [`Caixa::exe`]
12475 // must return the `:exe` typed [`Vec<String>`] list verbatim as
12476 // a `&[String]`, byte-equal to the raw `self.exe.as_slice()`
12477 // access across every representative value in the accept-set —
12478 // `[]` (the "no executable declared" arm every `:kind` other
12479 // than `Binario` carries; the layout's [`crate::LayoutInvariants`]
12480 // `BinarioWithoutExe` arm-gate fires exactly on this empty-slot
12481 // + `Binario`-kind combination), `[""]` (a past-the-guard
12482 // sentinel that pins the accessor doesn't perform a silent
12483 // `[""] → []` collapse on the empty-entry arm — validate rejects
12484 // `[""]` through `CodePathEmpty { slot: ":exe" }` but the
12485 // accessor must ship the raw slot verbatim so a validate-time
12486 // gate regression surfaces at the layout / `feira nix` boundary
12487 // rather than being silently absorbed into an executable-drop),
12488 // `["exe/cli"]` (the canonical single-entry Binario form every
12489 // in-tree `caixa_with_code_paths` positive control uses),
12490 // `["exe/cli", "exe/serve"]` (the canonical multi-executable
12491 // form the `validate_code_paths_accepts_explicit_relative_paths_
12492 // on_every_slot` fixture emits), and `["exe/cli", "exe/cli"]`
12493 // (a past-the-guard duplicate sentinel — validate rejects
12494 // through `CodePathDuplicate { slot: ":exe" }` per the per-slot
12495 // set-not-multiset gate, but the accessor must ship the raw
12496 // slot verbatim so struct-literal `Caixa { exe: vec!["exe/cli".
12497 // into(), "exe/cli".into()], .. }` fixtures continue to expose
12498 // the duplicate at the accessor).
12499 //
12500 // Fourth outer top-level [`Caixa`] `&[T]`-return slice accessor
12501 // pin on the substrate primitive — folds on the "outer
12502 // [`Caixa`] `&[T]` slice" projection pattern
12503 // `autores_returns_autores_slice_verbatim_across_permutations`
12504 // (b5d813f) opened,
12505 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12506 // (78c7d3c) folded on, and
12507 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12508 // (8a36c23) closed the universal-axis text-tag family of.
12509 // Opens the outer-`Caixa` foreign-code-slot `&[T]` sub-family
12510 // the sibling `:servicos` future lift closes onto. Pins against
12511 // a future silent detour that returned an owned `Vec<String>`
12512 // (which would type-check but silently clone on every accessor
12513 // call, breaking the zero-cost projection every peer sibling
12514 // slice accessor carries), a `[""] → []` collapse (which would
12515 // silently absorb the `CodePathEmpty` refusal case at the
12516 // accessor boundary), or an `["exe/cli", "exe/cli"] →
12517 // ["exe/cli"]` dedup collapse (which would silently absorb the
12518 // `CodePathDuplicate` refusal case at the accessor boundary —
12519 // the per-slot set-not-multiset gate is downstream of the
12520 // accessor and must not be silently promoted into it).
12521 for exe in [
12522 vec![],
12523 vec![""],
12524 vec!["exe/cli"],
12525 vec!["exe/cli", "exe/serve"],
12526 vec!["exe/cli", "exe/cli"],
12527 ] {
12528 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12529 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12530 assert_eq!(
12531 c.exe(),
12532 expected.as_slice(),
12533 "Caixa::exe must return :exe verbatim (got {:?}, \
12534 expected {expected:?})",
12535 c.exe(),
12536 );
12537 assert_eq!(
12538 c.exe(),
12539 c.exe.as_slice(),
12540 "Caixa::exe must byte-equal the raw \
12541 `self.exe.as_slice()` field access across every value \
12542 in the Vec<String> accept-set",
12543 );
12544 }
12545 }
12546
12547 #[test]
12548 fn validate_code_paths_exe_empty_arm_routes_through_accessor() {
12549 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12550 // empty-arm gate on the `:exe` slot must key off
12551 // [`Caixa::exe`], not a divergent raw `&self.exe` field-borrow
12552 // walk. Structurally: a `Caixa { exe: vec!["".into()], .. }`
12553 // must surface the `CodePathEmpty { slot: ":exe" }` refusal
12554 // exactly, and a `Caixa { exe: vec!["exe/cli".into()], .. }`
12555 // (the canonical single-executable form every in-tree
12556 // `caixa_with_code_paths` positive control uses) must pass
12557 // validate. The pair jointly pins the accessor + validate-gate
12558 // composition: any future silent detour that had the accessor
12559 // return an empty slice on the `[""]` arm (a
12560 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12561 // silently absorb the `CodePathEmpty` refusal at the accessor
12562 // boundary and the validate gate would accept a struct-literal
12563 // `Caixa { exe: vec!["".into()], .. }` — the composition pin
12564 // catches that at caixa-core build time.
12565 //
12566 // Peer of the per-`Caixa`
12567 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12568 // (8a36c23), `validate_autores_empty_arm_routes_through_accessor`
12569 // (b5d813f), and
12570 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12571 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12572 // composition axes — same "the validate / shape-gate predicate
12573 // must route through the substrate-primitive typed dispatch"
12574 // discipline extended onto the sibling outer top-level [`Caixa`]
12575 // `&[T]`-composition surface. Nominally the in-tree
12576 // `validate_code_paths` production body still keys off the
12577 // internal `[(":bibliotecas", &self.bibliotecas,
12578 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12579 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12580 // (the tuple's homogeneous slice-typed shape blocks a per-
12581 // element accessor swap in isolation — a future companion lift
12582 // for `:servicos` on the same outer-`Caixa` `&[T]` slice-
12583 // accessor axis closes that tuple onto the triple of typed
12584 // dispatches as a unit); the composition pin catches any future
12585 // accessor-side silent filter drop against that eventual tuple-
12586 // closure regardless of whether the `:exe` slot is threaded
12587 // through the accessor or the raw field access at the tuple's
12588 // construction site.
12589 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
12590 assert!(
12591 matches!(
12592 c.validate_code_paths(),
12593 Err(ManifestError::CodePathEmpty { slot: ":exe" })
12594 ),
12595 "validate_code_paths must reject exe == vec![\"\"] \
12596 with CodePathEmpty {{ slot: \":exe\" }} — the \
12597 accessor and the validate gate must route through the \
12598 same substrate-primitive typed dispatch on the \
12599 :exe per-entry empty arm",
12600 );
12601 let c = caixa_with_code_paths(vec![], vec!["exe/cli"], vec![]);
12602 assert!(
12603 c.validate_code_paths().is_ok(),
12604 "validate_code_paths must accept exe == vec![\"exe/cli\"] \
12605 (the canonical single-executable shape every in-tree \
12606 `caixa_with_code_paths` positive control uses)",
12607 );
12608 }
12609
12610 #[test]
12611 fn exe_projects_slice_by_borrow() {
12612 // The by-borrow pin: [`Caixa::exe`] returns `&[String]` by
12613 // borrow — the returned slice borrows the underlying
12614 // `Vec<String>` storage of the `:exe` slot and the accessor
12615 // must not clone the backing `Vec` on every call. Peer of the
12616 // per-`Caixa` `autores_projects_slice_by_borrow` (b5d813f),
12617 // `etiquetas_projects_slice_by_borrow` (78c7d3c), and
12618 // `bibliotecas_projects_slice_by_borrow` (8a36c23) by-borrow
12619 // pins on the sibling outer top-level [`Caixa`] `&[String]`-
12620 // return axes — the accessor's returned slice must borrow from
12621 // `&self` (the returned reference's lifetime is tied to
12622 // `&self`), and calling the accessor twice on the same
12623 // [`Caixa`] must yield slices that are pointer-equal (the
12624 // underlying byte-buffer is the storage `Vec`'s allocation,
12625 // not a fresh copy) as well as value-equal (idempotent, no
12626 // side effects on `&self`).
12627 //
12628 // Pins against a future silent detour that returned an owned
12629 // `Vec<String>` (which would type-check but silently clone on
12630 // every call, breaking the zero-cost projection every peer
12631 // sibling slice accessor carries), a `&Vec<String>` return
12632 // (which would leak the backing `Vec`'s grow/push/reserve
12633 // surface no downstream consumer reaches for), or a one-arm-
12634 // only accessor that returned a saturating value on some
12635 // sentinel input (breaking the pass-through invariant the
12636 // sibling slice accessors carry).
12637 for exe in [
12638 vec![],
12639 vec!["exe/cli"],
12640 vec!["exe/cli", "exe/serve"],
12641 vec!["exe/cli", "exe/cli"],
12642 ] {
12643 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12644 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12645 let first = c.exe();
12646 let second = c.exe();
12647 assert_eq!(
12648 first, second,
12649 "Caixa::exe must be idempotent — two successive calls \
12650 on the same &self must return the same &[String]",
12651 );
12652 assert_eq!(
12653 first.as_ptr(),
12654 second.as_ptr(),
12655 "Caixa::exe must borrow the underlying Vec<String> \
12656 storage — two successive calls must return slices \
12657 with the same backing pointer (a fresh Vec<String> \
12658 clone would change the pointer on every call)",
12659 );
12660 assert_eq!(
12661 first,
12662 expected.as_slice(),
12663 "Caixa::exe must return :exe verbatim by borrow — \
12664 got {first:?}, expected {expected:?}",
12665 );
12666 }
12667 }
12668
12669 // ── Caixa::servicos — outer top-level &[T] slice accessor ─────────
12670
12671 #[test]
12672 fn servicos_returns_servicos_slice_verbatim_across_permutations() {
12673 // The canonical per-`Caixa` `:servicos` universal-axis
12674 // ComputeUnit-CR-YAML-entry-path-list slice pin:
12675 // [`Caixa::servicos`] must return the `:servicos` typed
12676 // [`Vec<String>`] list verbatim as a `&[String]`, byte-equal to
12677 // the raw `self.servicos.as_slice()` access across every
12678 // representative value in the accept-set — `[]` (the "no
12679 // ComputeUnit-CR declared" arm every `:kind` other than
12680 // `Servico` carries; the layout's [`crate::LayoutInvariants`]
12681 // `ServicoWithoutServicos` arm-gate fires exactly on this
12682 // empty-slot + `Servico`-kind combination), `[""]` (a past-the-
12683 // guard sentinel that pins the accessor doesn't perform a
12684 // silent `[""] → []` collapse on the empty-entry arm — validate
12685 // rejects `[""]` through `CodePathEmpty { slot: ":servicos" }`
12686 // but the accessor must ship the raw slot verbatim so a
12687 // validate-time gate regression surfaces at the layout /
12688 // per-Servico renderer boundary rather than being silently
12689 // absorbed into a component-drop),
12690 // `["servicos/demo.computeunit.yaml"]` (the canonical
12691 // singleton V0-shape every in-tree `caixa_with_code_paths`
12692 // positive control uses; the same shape
12693 // [`crate::require_single_servico`] admits),
12694 // `["servicos/a.computeunit.yaml", "servicos/b.computeunit.
12695 // yaml"]` (a past-the-guard `len != 1` sentinel — the V0
12696 // singularity gate rejects through `ServicoCountMismatch
12697 // { count: 2 }` but the accessor must ship the raw slot
12698 // verbatim so struct-literal `Caixa { servicos: vec![...,
12699 // ...], .. }` fixtures continue to expose the count at the
12700 // accessor), and `["servicos/a.computeunit.yaml",
12701 // "servicos/a.computeunit.yaml"]` (a past-the-guard duplicate
12702 // sentinel — validate rejects through
12703 // `CodePathDuplicate { slot: ":servicos" }` per the per-slot
12704 // set-not-multiset gate, but the accessor must ship the raw
12705 // slot verbatim so struct-literal fixtures continue to expose
12706 // the duplicate at the accessor).
12707 //
12708 // Fifth and final outer top-level [`Caixa`] `&[T]`-return
12709 // slice accessor pin on the substrate primitive — folds on the
12710 // "outer [`Caixa`] `&[T]` slice" projection pattern
12711 // `autores_returns_autores_slice_verbatim_across_permutations`
12712 // (b5d813f) opened,
12713 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12714 // (78c7d3c) folded on,
12715 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12716 // (8a36c23) closed the universal-axis text-tag family of, and
12717 // `exe_returns_exe_slice_verbatim_across_permutations`
12718 // (65d9527) opened the foreign-code-slot sub-family of. Closes
12719 // the outer-`Caixa` foreign-code-slot `&[T]` sub-family — the
12720 // trio of code-surface list slots (`:bibliotecas` + `:exe` +
12721 // `:servicos`) now each carries a substrate-canonical slice
12722 // accessor. Pins against a future silent detour that returned
12723 // an owned `Vec<String>` (which would type-check but silently
12724 // clone on every accessor call, breaking the zero-cost
12725 // projection every peer sibling slice accessor carries), a
12726 // `[""] → []` collapse (which would silently absorb the
12727 // `CodePathEmpty` refusal case at the accessor boundary), an
12728 // `[a, a] → [a]` dedup collapse (which would silently absorb
12729 // the `CodePathDuplicate` refusal case at the accessor
12730 // boundary — the per-slot set-not-multiset gate is downstream
12731 // of the accessor and must not be silently promoted into it),
12732 // or a `[a, b] → [a]` singleton collapse (which would silently
12733 // absorb the V0 `ServicoCountMismatch` refusal case at the
12734 // accessor boundary — the V0 singularity gate is downstream of
12735 // the accessor and must not be silently promoted into it).
12736 for servicos in [
12737 vec![],
12738 vec![""],
12739 vec!["servicos/demo.computeunit.yaml"],
12740 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12741 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12742 ] {
12743 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12744 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12745 assert_eq!(
12746 c.servicos(),
12747 expected.as_slice(),
12748 "Caixa::servicos must return :servicos verbatim (got \
12749 {:?}, expected {expected:?})",
12750 c.servicos(),
12751 );
12752 assert_eq!(
12753 c.servicos(),
12754 c.servicos.as_slice(),
12755 "Caixa::servicos must byte-equal the raw \
12756 `self.servicos.as_slice()` field access across every \
12757 value in the Vec<String> accept-set",
12758 );
12759 }
12760 }
12761
12762 #[test]
12763 fn validate_code_paths_servicos_empty_arm_routes_through_accessor() {
12764 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12765 // empty-arm gate on the `:servicos` slot must key off
12766 // [`Caixa::servicos`], not a divergent raw `&self.servicos`
12767 // field-borrow walk. Structurally: a `Caixa { servicos:
12768 // vec!["".into()], .. }` must surface the `CodePathEmpty
12769 // { slot: ":servicos" }` refusal exactly, and a `Caixa
12770 // { servicos: vec!["servicos/demo.computeunit.yaml".into()],
12771 // .. }` (the canonical singleton V0-shape every in-tree
12772 // `caixa_with_code_paths` positive control uses) must pass
12773 // validate. The pair jointly pins the accessor + validate-gate
12774 // composition: any future silent detour that had the accessor
12775 // return an empty slice on the `[""]` arm (a `.iter().filter
12776 // (|s| !s.is_empty()).collect()` collapse) would silently
12777 // absorb the `CodePathEmpty` refusal at the accessor boundary
12778 // and the validate gate would accept a struct-literal
12779 // `Caixa { servicos: vec!["".into()], .. }` — the composition
12780 // pin catches that at caixa-core build time.
12781 //
12782 // Peer of the per-`Caixa`
12783 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12784 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12785 // (65d9527), `validate_autores_empty_arm_routes_through_accessor`
12786 // (b5d813f), and
12787 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12788 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12789 // composition axes — same "the validate / shape-gate predicate
12790 // must route through the substrate-primitive typed dispatch"
12791 // discipline extended onto the sibling outer top-level
12792 // [`Caixa`] `&[T]`-composition surface, closing the trio of
12793 // code-surface accessor-composition pins on the same axis.
12794 // Nominally the in-tree `validate_code_paths` production body
12795 // still keys off the internal
12796 // `[(":bibliotecas", &self.bibliotecas,
12797 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12798 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12799 // (the tuple's homogeneous `&Vec<String>`-typed shape blocks a
12800 // per-element accessor swap in isolation — a future companion
12801 // lift promotes the tuple's element type to `&[String]` and
12802 // threads the triple of typed dispatches through as a unit);
12803 // the composition pin catches any future accessor-side silent
12804 // filter drop against that eventual tuple-closure regardless
12805 // of whether the `:servicos` slot is threaded through the
12806 // accessor or the raw field access at the tuple's construction
12807 // site.
12808 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
12809 assert!(
12810 matches!(
12811 c.validate_code_paths(),
12812 Err(ManifestError::CodePathEmpty { slot: ":servicos" })
12813 ),
12814 "validate_code_paths must reject servicos == vec![\"\"] \
12815 with CodePathEmpty {{ slot: \":servicos\" }} — the \
12816 accessor and the validate gate must route through the \
12817 same substrate-primitive typed dispatch on the \
12818 :servicos per-entry empty arm",
12819 );
12820 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.computeunit.yaml"]);
12821 assert!(
12822 c.validate_code_paths().is_ok(),
12823 "validate_code_paths must accept servicos == \
12824 vec![\"servicos/demo.computeunit.yaml\"] (the canonical \
12825 singleton V0-shape every in-tree `caixa_with_code_paths` \
12826 positive control uses)",
12827 );
12828 }
12829
12830 #[test]
12831 fn servicos_projects_slice_by_borrow() {
12832 // The by-borrow pin: [`Caixa::servicos`] returns `&[String]` by
12833 // borrow — the returned slice borrows the underlying
12834 // `Vec<String>` storage of the `:servicos` slot and the
12835 // accessor must not clone the backing `Vec` on every call.
12836 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12837 // (b5d813f), `etiquetas_projects_slice_by_borrow` (78c7d3c),
12838 // `bibliotecas_projects_slice_by_borrow` (8a36c23), and
12839 // `exe_projects_slice_by_borrow` (65d9527) by-borrow pins on
12840 // the sibling outer top-level [`Caixa`] `&[String]`-return
12841 // axes — the accessor's returned slice must borrow from
12842 // `&self` (the returned reference's lifetime is tied to
12843 // `&self`), and calling the accessor twice on the same
12844 // [`Caixa`] must yield slices that are pointer-equal (the
12845 // underlying byte-buffer is the storage `Vec`'s allocation,
12846 // not a fresh copy) as well as value-equal (idempotent, no
12847 // side effects on `&self`).
12848 //
12849 // Pins against a future silent detour that returned an owned
12850 // `Vec<String>` (which would type-check but silently clone on
12851 // every call, breaking the zero-cost projection every peer
12852 // sibling slice accessor carries), a `&Vec<String>` return
12853 // (which would leak the backing `Vec`'s grow/push/reserve
12854 // surface no downstream consumer reaches for), or a one-arm-
12855 // only accessor that returned a saturating value on some
12856 // sentinel input (breaking the pass-through invariant the
12857 // sibling slice accessors carry).
12858 for servicos in [
12859 vec![],
12860 vec!["servicos/demo.computeunit.yaml"],
12861 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12862 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12863 ] {
12864 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12865 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12866 let first = c.servicos();
12867 let second = c.servicos();
12868 assert_eq!(
12869 first, second,
12870 "Caixa::servicos must be idempotent — two successive \
12871 calls on the same &self must return the same &[String]",
12872 );
12873 assert_eq!(
12874 first.as_ptr(),
12875 second.as_ptr(),
12876 "Caixa::servicos must borrow the underlying \
12877 Vec<String> storage — two successive calls must \
12878 return slices with the same backing pointer (a fresh \
12879 Vec<String> clone would change the pointer on every \
12880 call)",
12881 );
12882 assert_eq!(
12883 first,
12884 expected.as_slice(),
12885 "Caixa::servicos must return :servicos verbatim by \
12886 borrow — got {first:?}, expected {expected:?}",
12887 );
12888 }
12889 }
12890
12891 // ── Caixa::deps — outer top-level &[Dep] slice accessor ───────────
12892
12893 fn caixa_with_deps(deps: Vec<Dep>) -> Caixa {
12894 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12895 c.deps = deps;
12896 c
12897 }
12898
12899 #[test]
12900 fn deps_returns_deps_slice_verbatim_across_permutations() {
12901 // The canonical per-`Caixa` `:deps` universal-axis runtime-
12902 // dependency-declaration-list slice pin: [`Caixa::deps`] must
12903 // return the `:deps` typed [`Vec<Dep>`] list verbatim as a
12904 // `&[Dep]`, element-equal to the raw `self.deps.as_slice()`
12905 // access across every representative value in the accept-set —
12906 // `[]` (the "no runtime deps declared" arm every existing
12907 // fixture without a `:deps` line carries; the
12908 // [`Caixa::template`] scaffold emits `:deps ()`), a canonical
12909 // single-entry list (the shape most consumer caixas carry), a
12910 // canonical two-entry list (the multi-dep runtime closure), and
12911 // two past-the-guard sentinels — a `[""]`-`:nome` entry
12912 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12913 // `NomeInvalid` but the accessor must ship the raw slot
12914 // verbatim) and a `[a, a]` duplicate (validate rejects through
12915 // `DuplicateNome { list: ":deps" }` but the accessor must ship
12916 // the raw slot verbatim so struct-literal fixtures continue to
12917 // expose the duplicate at the accessor).
12918 //
12919 // First outer top-level [`Caixa`] `&[Dep]`-return slice accessor
12920 // pin on the substrate primitive — opens the outer-`Caixa`
12921 // dependency-slot `&[Dep]` sub-family the sibling `:deps-dev`
12922 // future lift closes on. Peer of the closed outer-`Caixa`
12923 // foreign-code-slot `&[String]` sub-family
12924 // (`bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12925 // 8a36c23, `exe_returns_exe_slice_verbatim_across_permutations`
12926 // 65d9527, `servicos_returns_servicos_slice_verbatim_across_permutations`
12927 // 611f78b) and the outer-`Caixa` universal-axis text-tag family
12928 // (`autores_returns_autores_slice_verbatim_across_permutations`
12929 // b5d813f, `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12930 // 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice"
12931 // projection pattern onto a novel element-type axis (`Dep`
12932 // composite vs the prior sibling family's `String` scalar).
12933 // Pins against a future silent detour that returned an owned
12934 // `Vec<Dep>` (which would type-check but silently clone on every
12935 // accessor call, breaking the zero-cost projection every peer
12936 // sibling slice accessor carries), a `[""] → []` collapse (which
12937 // would silently absorb the `NomeEmpty` refusal case at the
12938 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12939 // would silently absorb the `DuplicateNome` refusal case at the
12940 // accessor boundary).
12941 for deps in [
12942 vec![],
12943 vec![Dep::simple("", "^0.1")],
12944 vec![Dep::simple("caixa-teia", "^0.1")],
12945 vec![
12946 Dep::simple("caixa-teia", "^0.1"),
12947 Dep::simple("caixa-core", "^0.1"),
12948 ],
12949 vec![
12950 Dep::simple("caixa-teia", "^0.1"),
12951 Dep::simple("caixa-teia", "^0.2"),
12952 ],
12953 ] {
12954 let c = caixa_with_deps(deps.clone());
12955 assert_eq!(
12956 c.deps(),
12957 deps.as_slice(),
12958 "Caixa::deps must return :deps verbatim (got {:?}, \
12959 expected {deps:?})",
12960 c.deps(),
12961 );
12962 assert_eq!(
12963 c.deps(),
12964 c.deps.as_slice(),
12965 "Caixa::deps must element-equal the raw \
12966 `self.deps.as_slice()` field access across every \
12967 value in the Vec<Dep> accept-set",
12968 );
12969 }
12970 }
12971
12972 #[test]
12973 fn validate_deps_duplicate_arm_routes_through_accessor() {
12974 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps`
12975 // duplicate-`:nome` gate must key off [`Caixa::deps`], not the
12976 // raw `&self.deps` field-borrow walk. Structurally: a `Caixa
12977 // { deps: vec![Dep::simple("d", "^0.1"), Dep::simple("d",
12978 // "^0.2")], .. }` must surface the `DuplicateNome { list:
12979 // ":deps" }` refusal exactly, and a `Caixa { deps: vec![
12980 // Dep::simple("d", "^0.1")], .. }` (the canonical single-entry
12981 // form) must pass validate. The pair jointly pins the accessor +
12982 // validate-gate composition: any future silent detour that had
12983 // the accessor return a dedupped slice on the `[a, a]` arm (a
12984 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12985 // would silently absorb the `DuplicateNome` refusal at the
12986 // accessor boundary and the validate gate would accept a
12987 // struct-literal `Caixa` carrying the drift — the composition
12988 // pin catches that at caixa-core build time.
12989 //
12990 // Peer of the per-`Caixa`
12991 // `validate_autores_empty_entry_arm_routes_through_accessor`
12992 // (b5d813f), `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12993 // (78c7d3c), `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12994 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12995 // (65d9527), and `validate_code_paths_servicos_empty_arm_routes_through_accessor`
12996 // (611f78b) accessor-composition pins on the sibling `&[T]`-
12997 // composition axes — same "the validate gate must route through
12998 // the substrate-primitive typed dispatch" discipline extended
12999 // onto the sibling outer top-level [`Caixa`] `&[Dep]`-
13000 // composition surface, opening the outer-`Caixa` dependency-slot
13001 // arm of the composition-pin family.
13002 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
13003 let err = c.validate_deps().unwrap_err();
13004 assert!(
13005 matches!(
13006 err,
13007 DepError::DuplicateNome { ref nome, list } if nome == "d"
13008 && list == crate::render::DEP_AUTHOR_KEY_DEPS
13009 ),
13010 "validate_deps must reject deps == \
13011 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
13012 DuplicateNome {{ nome: \"d\", list: \":deps\" }} — the \
13013 accessor and the validate gate must route through the \
13014 same substrate-primitive typed dispatch on the :deps \
13015 within-list duplicate arm (got {err:?})",
13016 );
13017 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1")]);
13018 assert!(
13019 c.validate_deps().is_ok(),
13020 "validate_deps must accept deps == vec![Dep(\"d\",\"^0.1\")] \
13021 (the canonical single-entry form)",
13022 );
13023 }
13024
13025 #[test]
13026 fn deps_projects_slice_by_borrow() {
13027 // The by-borrow pin: [`Caixa::deps`] returns `&[Dep]` by borrow
13028 // — the returned slice borrows the underlying `Vec<Dep>` storage
13029 // of the `:deps` slot and the accessor must not clone the
13030 // backing `Vec` on every call. Peer of the per-`Caixa`
13031 // `autores_projects_slice_by_borrow` (b5d813f),
13032 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
13033 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
13034 // `exe_projects_slice_by_borrow` (65d9527), and
13035 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
13036 // on the sibling outer top-level [`Caixa`] `&[String]`-return
13037 // axes — the accessor's returned slice must borrow from `&self`
13038 // (the returned reference's lifetime is tied to `&self`), and
13039 // calling the accessor twice on the same [`Caixa`] must yield
13040 // slices that are pointer-equal (the underlying byte-buffer is
13041 // the storage `Vec`'s allocation, not a fresh copy) as well as
13042 // value-equal (idempotent, no side effects on `&self`).
13043 //
13044 // Pins against a future silent detour that returned an owned
13045 // `Vec<Dep>` (which would type-check but silently clone on
13046 // every call), a `&Vec<Dep>` return (which would leak the
13047 // backing `Vec`'s grow/push/reserve surface no downstream
13048 // consumer reaches for), or a one-arm-only accessor that
13049 // returned a saturating value on some sentinel input.
13050 for deps in [
13051 vec![],
13052 vec![Dep::simple("caixa-teia", "^0.1")],
13053 vec![
13054 Dep::simple("caixa-teia", "^0.1"),
13055 Dep::simple("caixa-core", "^0.1"),
13056 ],
13057 ] {
13058 let c = caixa_with_deps(deps.clone());
13059 let first = c.deps();
13060 let second = c.deps();
13061 assert_eq!(
13062 first, second,
13063 "Caixa::deps must be idempotent — two successive calls \
13064 on the same &self must return the same &[Dep]",
13065 );
13066 assert_eq!(
13067 first.as_ptr(),
13068 second.as_ptr(),
13069 "Caixa::deps must borrow the underlying Vec<Dep> \
13070 storage — two successive calls must return slices \
13071 with the same backing pointer (a fresh Vec<Dep> clone \
13072 would change the pointer on every call)",
13073 );
13074 assert_eq!(
13075 first,
13076 deps.as_slice(),
13077 "Caixa::deps must return :deps verbatim by borrow — \
13078 got {first:?}, expected {deps:?}",
13079 );
13080 }
13081 }
13082
13083 // ── Caixa::deps_dev — outer top-level &[Dep] slice accessor ──────
13084
13085 fn caixa_with_deps_dev(deps_dev: Vec<Dep>) -> Caixa {
13086 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13087 c.deps_dev = deps_dev;
13088 c
13089 }
13090
13091 #[test]
13092 fn deps_dev_returns_deps_dev_slice_verbatim_across_permutations() {
13093 // The canonical per-`Caixa` `:deps-dev` universal-axis dev-only-
13094 // dependency-declaration-list slice pin: [`Caixa::deps_dev`]
13095 // must return the `:deps-dev` typed [`Vec<Dep>`] list verbatim as
13096 // a `&[Dep]`, element-equal to the raw `self.deps_dev.as_slice()`
13097 // access across every representative value in the accept-set —
13098 // `[]` (the "no dev deps declared" arm every existing fixture
13099 // without a `:deps-dev` line carries; the [`Caixa::template`]
13100 // scaffold emits `:deps-dev ()`), a canonical single-entry list
13101 // (the shape most consumer caixas carry — a `tatara-check` dev
13102 // pin), a canonical two-entry list (the multi-dev-dep closure),
13103 // and two past-the-guard sentinels — a `[""]`-`:nome` entry
13104 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
13105 // `NomeInvalid` but the accessor must ship the raw slot
13106 // verbatim) and a `[a, a]` duplicate (validate rejects through
13107 // `DuplicateNome { list: ":deps-dev" }` but the accessor must
13108 // ship the raw slot verbatim so struct-literal fixtures continue
13109 // to expose the duplicate at the accessor).
13110 //
13111 // Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor
13112 // pin on the substrate primitive — closes the outer-`Caixa`
13113 // dependency-slot `&[Dep]` sub-family the sibling
13114 // `deps_returns_deps_slice_verbatim_across_permutations`
13115 // (ad34b4e) opened on. Folds the "outer [`Caixa`] `&[Dep]`
13116 // slice" projection pattern onto the sibling dev-dep axis —
13117 // pins against a future silent detour that returned an owned
13118 // `Vec<Dep>` (which would type-check but silently clone on every
13119 // accessor call, breaking the zero-cost projection every peer
13120 // sibling slice accessor carries), a `[""] → []` collapse (which
13121 // would silently absorb the `NomeEmpty` refusal case at the
13122 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
13123 // would silently absorb the `DuplicateNome` refusal case at the
13124 // accessor boundary).
13125 for deps_dev in [
13126 vec![],
13127 vec![Dep::simple("", "^0.1")],
13128 vec![Dep::simple("tatara-check", "^0.1")],
13129 vec![
13130 Dep::simple("tatara-check", "^0.1"),
13131 Dep::simple("caixa-lint", "^0.1"),
13132 ],
13133 vec![
13134 Dep::simple("tatara-check", "^0.1"),
13135 Dep::simple("tatara-check", "^0.2"),
13136 ],
13137 ] {
13138 let c = caixa_with_deps_dev(deps_dev.clone());
13139 assert_eq!(
13140 c.deps_dev(),
13141 deps_dev.as_slice(),
13142 "Caixa::deps_dev must return :deps-dev verbatim (got \
13143 {:?}, expected {deps_dev:?})",
13144 c.deps_dev(),
13145 );
13146 assert_eq!(
13147 c.deps_dev(),
13148 c.deps_dev.as_slice(),
13149 "Caixa::deps_dev must element-equal the raw \
13150 `self.deps_dev.as_slice()` field access across every \
13151 value in the Vec<Dep> accept-set",
13152 );
13153 }
13154 }
13155
13156 #[test]
13157 fn validate_deps_duplicate_deps_dev_arm_routes_through_accessor() {
13158 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps-dev`
13159 // duplicate-`:nome` gate must key off [`Caixa::deps_dev`], not
13160 // the raw `&self.deps_dev` field-borrow walk. Structurally: a
13161 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1"),
13162 // Dep::simple("d", "^0.2")], .. }` must surface the
13163 // `DuplicateNome { list: ":deps-dev" }` refusal exactly, and a
13164 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1")], .. }` (the
13165 // canonical single-entry form) must pass validate. The pair
13166 // jointly pins the accessor + validate-gate composition: any
13167 // future silent detour that had the accessor return a dedupped
13168 // slice on the `[a, a]` arm (a
13169 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
13170 // would silently absorb the `DuplicateNome` refusal at the
13171 // accessor boundary and the validate gate would accept a
13172 // struct-literal `Caixa` carrying the drift — the composition
13173 // pin catches that at caixa-core build time.
13174 //
13175 // Peer of `validate_deps_duplicate_arm_routes_through_accessor`
13176 // (ad34b4e) on the sibling `:deps` axis — same "the validate
13177 // gate must route through the substrate-primitive typed
13178 // dispatch" discipline folded onto the sibling `:deps-dev`
13179 // axis, closing the two-list dep-graph composition-pin family.
13180 // The `:deps-dev` diagnostic must carry the
13181 // `DEP_AUTHOR_KEY_DEPS_DEV` list-tag (not
13182 // `DEP_AUTHOR_KEY_DEPS`) so the emitted error names the
13183 // offending list unambiguously.
13184 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
13185 let err = c.validate_deps().unwrap_err();
13186 assert!(
13187 matches!(
13188 err,
13189 DepError::DuplicateNome { ref nome, list } if nome == "d"
13190 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
13191 ),
13192 "validate_deps must reject deps_dev == \
13193 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
13194 DuplicateNome {{ nome: \"d\", list: \":deps-dev\" }} — the \
13195 accessor and the validate gate must route through the \
13196 same substrate-primitive typed dispatch on the :deps-dev \
13197 within-list duplicate arm (got {err:?})",
13198 );
13199 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1")]);
13200 assert!(
13201 c.validate_deps().is_ok(),
13202 "validate_deps must accept deps_dev == \
13203 vec![Dep(\"d\",\"^0.1\")] (the canonical single-entry form)",
13204 );
13205 }
13206
13207 #[test]
13208 fn deps_dev_projects_slice_by_borrow() {
13209 // The by-borrow pin: [`Caixa::deps_dev`] returns `&[Dep]` by
13210 // borrow — the returned slice borrows the underlying `Vec<Dep>`
13211 // storage of the `:deps-dev` slot and the accessor must not
13212 // clone the backing `Vec` on every call. Peer of
13213 // `deps_projects_slice_by_borrow` (ad34b4e) on the sibling
13214 // `:deps` axis, and of the per-`Caixa`
13215 // `autores_projects_slice_by_borrow` (b5d813f),
13216 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
13217 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
13218 // `exe_projects_slice_by_borrow` (65d9527), and
13219 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
13220 // on the sibling outer top-level [`Caixa`] `&[String]`-return
13221 // axes — the accessor's returned slice must borrow from `&self`
13222 // (the returned reference's lifetime is tied to `&self`), and
13223 // calling the accessor twice on the same [`Caixa`] must yield
13224 // slices that are pointer-equal (the underlying byte-buffer is
13225 // the storage `Vec`'s allocation, not a fresh copy) as well as
13226 // value-equal (idempotent, no side effects on `&self`).
13227 //
13228 // Pins against a future silent detour that returned an owned
13229 // `Vec<Dep>` (which would type-check but silently clone on
13230 // every call), a `&Vec<Dep>` return (which would leak the
13231 // backing `Vec`'s grow/push/reserve surface no downstream
13232 // consumer reaches for), or a one-arm-only accessor that
13233 // returned a saturating value on some sentinel input.
13234 for deps_dev in [
13235 vec![],
13236 vec![Dep::simple("tatara-check", "^0.1")],
13237 vec![
13238 Dep::simple("tatara-check", "^0.1"),
13239 Dep::simple("caixa-lint", "^0.1"),
13240 ],
13241 ] {
13242 let c = caixa_with_deps_dev(deps_dev.clone());
13243 let first = c.deps_dev();
13244 let second = c.deps_dev();
13245 assert_eq!(
13246 first, second,
13247 "Caixa::deps_dev must be idempotent — two successive \
13248 calls on the same &self must return the same &[Dep]",
13249 );
13250 assert_eq!(
13251 first.as_ptr(),
13252 second.as_ptr(),
13253 "Caixa::deps_dev must borrow the underlying Vec<Dep> \
13254 storage — two successive calls must return slices \
13255 with the same backing pointer (a fresh Vec<Dep> clone \
13256 would change the pointer on every call)",
13257 );
13258 assert_eq!(
13259 first,
13260 deps_dev.as_slice(),
13261 "Caixa::deps_dev must return :deps-dev verbatim by \
13262 borrow — got {first:?}, expected {deps_dev:?}",
13263 );
13264 }
13265 }
13266
13267 // ── Caixa::limits — outer top-level Option<&LimitsSpec> composite-reference accessor ──
13268
13269 fn caixa_with_limits(limits: Option<crate::LimitsSpec>) -> Caixa {
13270 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13271 c.limits = limits;
13272 c
13273 }
13274
13275 #[test]
13276 fn limits_returns_limits_option_ref_verbatim_across_permutations() {
13277 // The canonical per-`Caixa` `:limits` M2 typed-slot outer-
13278 // composite optional-composite-reference-shape pin:
13279 // [`Caixa::limits`] must return the `:limits` typed
13280 // `Option<LimitsSpec>` verbatim as an `Option<&LimitsSpec>`
13281 // reference over the same backing storage the raw
13282 // `self.limits.as_ref()` field access borrows from, byte-equal
13283 // across every representative fixture in the accept-set — the
13284 // author-omitted `None` shape (the "engine-default applies"
13285 // partition every downstream Servico M2 overlay emitter treats
13286 // as "emit nothing"), the empty-composite `Some(LimitsSpec {
13287 // .. default })` shape ([`LimitsSpec::is_empty`] holds — every
13288 // per-axis cap is `None`, so the peer M2 overlay emitter's
13289 // `.is_empty()`-gated projection still emits nothing but the
13290 // outer presence-bit is `Some`, so [`Caixa::declared_servico_slots`]
13291 // still pushes the `M2_AUTHOR_KEY_LIMITS` label), a single-axis
13292 // fixture (only `:memory` set — the canonical shape most
13293 // memory-heavy Servicos carry), and a fully-populated composite
13294 // (every per-axis cap set — the canonical shape a
13295 // sandboxed-by-default Servico carries).
13296 //
13297 // Pins against a future silent detour that returned a fresh-
13298 // cloned [`LimitsSpec`] copy (which would type-check via the
13299 // `Clone` impl but silently break every downstream caller that
13300 // relied on the reference sharing the composite's backing
13301 // identity), a reference to an operator-resolved overlay (the
13302 // future per-cluster `:limits-overrides` slot — its resolution
13303 // must land at exactly this accessor body, not silently divert
13304 // the raw slot away from a second consumer), a
13305 // `None` → `Some(LimitsSpec::default)` cluster-default
13306 // projection (which would collapse the load-bearing
13307 // "author-omitted `:limits` ⇒ engine-default applies" partition
13308 // the peer [`crate::render::servico_m2_overlay`] emitter and
13309 // the peer [`Caixa::declared_servico_slots`] enumerator both
13310 // read), or an axis-shuffled projection (a future detour that
13311 // swapped `memory` and `fuel` through the accessor would
13312 // silently split the paired [`crate::StandardLayout::verify`]
13313 // per-`:limits` shape gate's traversal input from the peer
13314 // `servico_m2_overlay` emitter's projection input).
13315 //
13316 // First outer top-level [`Caixa`] `Option<&Composite>`-return
13317 // composite-reference accessor pin on the substrate primitive
13318 // — opens the outer-`Caixa` `Option<&Composite>` composite-
13319 // reference projection pattern the sibling `:behavior`
13320 // [`crate::BehaviorSpec`] / `:politicas`
13321 // [`crate::aplicacao::MeshPolicy`] / `:placement`
13322 // [`crate::aplicacao::Placement`] / `:entrada`
13323 // [`crate::aplicacao::Entrada`] future outer-composite lifts
13324 // fold on. Peer of the closed M3 outer-composite family the
13325 // sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
13326 // [`crate::AplicacaoSpec::placement`] (9abb8f0) /
13327 // [`crate::AplicacaoSpec::entrada`] (d32111c) composite-
13328 // reference accessor pins already carry on the outer
13329 // [`crate::AplicacaoSpec`] altitude — extends the outer-
13330 // accessor byte-equal-projection discipline onto the outer
13331 // top-level [`Caixa`] M2 Servico-runtime slot altitude.
13332 use crate::LimitsSpec;
13333 use std::time::Duration;
13334 let fixtures: Vec<Option<LimitsSpec>> = vec![
13335 None,
13336 Some(LimitsSpec::default()),
13337 Some(LimitsSpec {
13338 memory: Some(64 * 1024 * 1024),
13339 ..Default::default()
13340 }),
13341 Some(LimitsSpec {
13342 memory: Some(64 * 1024 * 1024),
13343 fuel: Some(1_000_000),
13344 wall_clock: Some(Duration::from_secs(30)),
13345 cpu: Some(500),
13346 }),
13347 ];
13348 for limits in fixtures {
13349 let c = caixa_with_limits(limits.clone());
13350 assert_eq!(
13351 c.limits(),
13352 limits.as_ref(),
13353 "Caixa::limits must return :limits verbatim (got {:?}, \
13354 expected {:?})",
13355 c.limits(),
13356 limits.as_ref(),
13357 );
13358 match (c.limits(), c.limits.as_ref()) {
13359 (Some(a), Some(b)) => assert!(
13360 std::ptr::eq(a, b),
13361 "Caixa::limits accessor and self.limits.as_ref() \
13362 field access must borrow the same backing storage \
13363 — the accessor is the substrate-primitive typed \
13364 dispatch every downstream Servico-M2-overlay \
13365 composite consumer must route through, and a \
13366 reference-identity split would silently break \
13367 every consumer that relied on the borrow sharing \
13368 the composite's storage",
13369 ),
13370 (None, None) => {}
13371 _ => panic!(
13372 "Caixa::limits presence bit must byte-equal \
13373 self.limits.is_some() — a presence-bit drift would \
13374 silently split the paired StandardLayout::verify \
13375 per-`:limits` shape gate's traversal head from \
13376 the peer render::servico_m2_overlay M2 overlay \
13377 emitter's traversal head from the peer \
13378 Caixa::declared_servico_slots M2 declared-slot \
13379 enumerator's presence probe",
13380 ),
13381 }
13382 assert_eq!(
13383 c.limits().is_some(),
13384 c.limits.is_some(),
13385 "Caixa::limits().is_some() must byte-equal \
13386 self.limits.is_some() — a presence-bit drift would \
13387 silently split every downstream Option<&LimitsSpec> \
13388 consumer's partition on the engine-default arm",
13389 );
13390 }
13391 }
13392
13393 #[test]
13394 fn declared_servico_slots_limits_arm_routes_through_accessor() {
13395 // Composition pin: [`Caixa::declared_servico_slots`]'s
13396 // `:limits` presence-probe arm must key off [`Caixa::limits`],
13397 // not the raw `self.limits.is_some()` field-probe. Structurally:
13398 // a `Caixa { limits: Some(LimitsSpec::default()), .. }` must
13399 // still push `M2_AUTHOR_KEY_LIMITS` onto the declared-slot list
13400 // (the presence bit is `Some`, so the M2 kind-coherence gate
13401 // must surface the slot as "declared" even when every per-axis
13402 // cap is unset), and a `Caixa { limits: None, .. }` must NOT
13403 // push the label (the "author omitted the slot entirely"
13404 // partition). The pair jointly pins the accessor + declared-
13405 // slot enumerator composition: any future silent detour that
13406 // had the accessor collapse `Some(LimitsSpec::default())` to
13407 // `None` (a `.filter(|l| !l.is_empty())` projection) would
13408 // silently absorb the "declared but empty" arm at the
13409 // accessor boundary and the [`crate::LayoutError::ServicoSlotsOnNonServico`]
13410 // kind-coherence gate would silently accept a
13411 // struct-literal `Caixa` carrying the drift.
13412 //
13413 // Peer of the sibling per-`Caixa`
13414 // `validate_deps_duplicate_arm_routes_through_accessor` (ad34b4e)
13415 // and `validate_deps_duplicate_deps_dev_arm_routes_through_accessor`
13416 // (f7fd81e) accessor-composition pins on the sibling `:deps` /
13417 // `:deps-dev` outer-`&[Dep]`-composition axes — same "the
13418 // enumerator gate must route through the substrate-primitive
13419 // typed dispatch" discipline extended onto the outer top-level
13420 // [`Caixa`] `Option<&LimitsSpec>`-composition surface, opening
13421 // the outer-`Caixa` M2 Servico-runtime-slot arm of the
13422 // composition-pin family.
13423 use crate::LimitsSpec;
13424 let c = caixa_with_limits(Some(LimitsSpec::default()));
13425 let slots = c.declared_servico_slots();
13426 assert!(
13427 slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13428 "declared_servico_slots must push M2_AUTHOR_KEY_LIMITS \
13429 when `:limits` is Some (even for LimitsSpec::default()) \
13430 — the accessor and the enumerator gate must route through \
13431 the same substrate-primitive typed dispatch on the outer \
13432 :limits presence bit (got slots={slots:?})",
13433 );
13434 let c = caixa_with_limits(None);
13435 let slots = c.declared_servico_slots();
13436 assert!(
13437 !slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13438 "declared_servico_slots must NOT push M2_AUTHOR_KEY_LIMITS \
13439 when `:limits` is None — the author-omitted arm must \
13440 route through the accessor's None-return unchanged (got \
13441 slots={slots:?})",
13442 );
13443 }
13444
13445 #[test]
13446 fn servico_m2_overlay_limits_arm_routes_through_accessor() {
13447 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13448 // per-`:limits` M2 overlay emit arm must key off
13449 // [`Caixa::limits`], not the raw `&caixa.limits` field-borrow.
13450 // Structurally: a `Caixa { limits: Some(LimitsSpec { memory:
13451 // Some(64 MiB), .. default }), .. }` must surface the
13452 // `M2_KEY_LIMITS` key with the per-axis
13453 // `memory: "64MiB"` sub-mapping in the overlay, a `Caixa {
13454 // limits: Some(LimitsSpec::default()), .. }` must omit the
13455 // key entirely (the `.is_empty()`-gated inner arm elides an
13456 // empty composite even when the outer presence bit is `Some`),
13457 // and a `Caixa { limits: None, .. }` must also omit the key
13458 // (the "author omitted the slot entirely" partition). The
13459 // three-fixture family jointly pins the accessor + M2 overlay
13460 // emitter composition: any future silent detour that had the
13461 // accessor return a fresh-cloned copy on the `Some` arm (a
13462 // `LimitsSpec::clone()` projection) would silently break the
13463 // reference-identity pin the peer per-axis
13464 // `serde_yaml::to_value(limits)` projection reads from.
13465 use crate::LimitsSpec;
13466 use crate::render::{M2_KEY_LIMITS, servico_m2_overlay};
13467 let c = caixa_with_limits(Some(LimitsSpec {
13468 memory: Some(64 * 1024 * 1024),
13469 ..Default::default()
13470 }));
13471 let overlay = servico_m2_overlay(&c).unwrap();
13472 assert!(
13473 overlay.contains_key(M2_KEY_LIMITS),
13474 "servico_m2_overlay must surface M2_KEY_LIMITS when \
13475 `:limits` carries a non-empty composite — the accessor \
13476 and the M2 overlay emitter must route through the same \
13477 substrate-primitive typed dispatch on the outer :limits \
13478 composite (got overlay={overlay:?})",
13479 );
13480 let c = caixa_with_limits(Some(LimitsSpec::default()));
13481 let overlay = servico_m2_overlay(&c).unwrap();
13482 assert!(
13483 !overlay.contains_key(M2_KEY_LIMITS),
13484 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13485 `:limits` is Some(LimitsSpec::default()) — the empty \
13486 composite's `.is_empty()`-gated inner arm must elide \
13487 the key regardless of the outer presence bit (got \
13488 overlay={overlay:?})",
13489 );
13490 let c = caixa_with_limits(None);
13491 let overlay = servico_m2_overlay(&c).unwrap();
13492 assert!(
13493 !overlay.contains_key(M2_KEY_LIMITS),
13494 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13495 `:limits` is None — the author-omitted arm must route \
13496 through the accessor's None-return unchanged (got \
13497 overlay={overlay:?})",
13498 );
13499 }
13500
13501 #[test]
13502 fn limits_projects_option_ref_by_borrow() {
13503 // The by-borrow pin: [`Caixa::limits`] returns
13504 // `Option<&LimitsSpec>` by borrow — the returned reference
13505 // borrows the underlying `Option<LimitsSpec>` storage of the
13506 // `:limits` slot and the accessor must not clone the backing
13507 // composite on every call. Peer of the sibling
13508 // `deps_projects_slice_by_borrow` (ad34b4e) /
13509 // `deps_dev_projects_slice_by_borrow` (f7fd81e) by-borrow pins
13510 // on the outer top-level [`Caixa`] `&[Dep]`-return axes —
13511 // extended here to the outer [`Caixa`] `Option<&Composite>`-
13512 // return axis: the accessor's returned reference must borrow
13513 // from `&self` (the returned reference's lifetime is tied to
13514 // `&self`), and calling the accessor twice on the same
13515 // [`Caixa`] must yield references that are pointer-equal (the
13516 // underlying byte-buffer is the storage `LimitsSpec`'s
13517 // allocation, not a fresh copy) as well as value-equal
13518 // (idempotent, no side effects on `&self`).
13519 //
13520 // Pins against a future silent detour that returned an owned
13521 // `LimitsSpec` (which would type-check via the `Clone` impl
13522 // but silently clone on every call), a `&LimitsSpec` panic-
13523 // return on the `None` arm (which would collapse the load-
13524 // bearing `Option` presence-bit into a runtime panic), or a
13525 // one-arm-only accessor that returned a saturating composite
13526 // on some sentinel input.
13527 use crate::LimitsSpec;
13528 use std::time::Duration;
13529 for limits in [
13530 Some(LimitsSpec::default()),
13531 Some(LimitsSpec {
13532 memory: Some(64 * 1024 * 1024),
13533 fuel: Some(1_000_000),
13534 wall_clock: Some(Duration::from_secs(30)),
13535 cpu: Some(500),
13536 }),
13537 ] {
13538 let c = caixa_with_limits(limits.clone());
13539 let first = c.limits().unwrap();
13540 let second = c.limits().unwrap();
13541 assert_eq!(
13542 first, second,
13543 "Caixa::limits must be idempotent — two successive \
13544 calls on the same &self must return the same \
13545 &LimitsSpec",
13546 );
13547 assert!(
13548 std::ptr::eq(first, second),
13549 "Caixa::limits must borrow the underlying \
13550 Option<LimitsSpec> storage — two successive calls \
13551 must return references with the same backing pointer \
13552 (a fresh LimitsSpec clone would change the pointer \
13553 on every call)",
13554 );
13555 assert_eq!(
13556 Some(first),
13557 limits.as_ref(),
13558 "Caixa::limits must return :limits verbatim by borrow \
13559 — got {first:?}, expected {:?}",
13560 limits.as_ref(),
13561 );
13562 }
13563 let c = caixa_with_limits(None);
13564 assert!(
13565 c.limits().is_none(),
13566 "Caixa::limits must return None when :limits is absent — \
13567 the author-omitted arm must project through the \
13568 accessor's Option::None unchanged",
13569 );
13570 }
13571
13572 // ── Caixa::behavior — outer top-level Option<&BehaviorSpec> composite-reference accessor ──
13573
13574 fn caixa_with_behavior(behavior: Option<crate::BehaviorSpec>) -> Caixa {
13575 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13576 c.behavior = behavior;
13577 c
13578 }
13579
13580 #[test]
13581 fn behavior_returns_behavior_option_ref_verbatim_across_permutations() {
13582 // The canonical per-`Caixa` `:behavior` M2 typed-slot outer-
13583 // composite optional-composite-reference-shape pin:
13584 // [`Caixa::behavior`] must return the `:behavior` typed
13585 // `Option<BehaviorSpec>` verbatim as an `Option<&BehaviorSpec>`
13586 // reference over the same backing storage the raw
13587 // `self.behavior.as_ref()` field access borrows from, byte-equal
13588 // across every representative fixture in the accept-set — the
13589 // author-omitted `None` shape (the "runtime-default applies"
13590 // partition every downstream Servico M2 overlay emitter treats
13591 // as "emit nothing"), the empty-composite `Some(BehaviorSpec {
13592 // .. default })` shape ([`BehaviorSpec::is_empty`] holds —
13593 // every per-callback path is `None`, so the peer M2 overlay
13594 // emitter's `.is_empty()`-gated projection still emits nothing
13595 // but the outer presence-bit is `Some`, so
13596 // [`Caixa::declared_servico_slots`] still pushes the
13597 // `M2_AUTHOR_KEY_BEHAVIOR` label), a single-callback fixture
13598 // (only `:on-state-change` set — the canonical shape a caixa
13599 // that only wires the hot-upgrade migration path carries), and
13600 // a fully-populated composite (every per-callback path set —
13601 // the canonical shape a fully-instrumented gen_server-shaped
13602 // Servico carries).
13603 //
13604 // Peer of the sibling
13605 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13606 // (b2bd9d7) opening fixture-family + reference-identity +
13607 // presence-bit tetrad pin on the outer top-level [`Caixa`]
13608 // `Option<&Composite>`-return sub-family — extended here to the
13609 // second axis of that sub-family so both of the currently-lifted
13610 // M2 Servico-runtime `Option<&Composite>` slots (`:limits` /
13611 // `:behavior`) carry the same "byte-equal, borrow-shared,
13612 // presence-bit-preserved" outer-accessor discipline.
13613 //
13614 // Pins against a future silent detour that returned a fresh-
13615 // cloned [`crate::BehaviorSpec`] copy (which would type-check
13616 // via the `Clone` impl but silently break every downstream
13617 // caller that relied on the reference sharing the composite's
13618 // backing identity), a reference to an operator-resolved
13619 // overlay (a future per-cluster `:behavior-overrides` slot —
13620 // its resolution must land at exactly this accessor body, not
13621 // silently divert the raw slot away from a second consumer), a
13622 // `None` → `Some(BehaviorSpec::default)` cluster-default
13623 // projection (which would collapse the load-bearing
13624 // "author-omitted `:behavior` ⇒ runtime-default applies"
13625 // partition the peer [`crate::render::servico_m2_overlay`]
13626 // emitter, the peer [`Caixa::declared_servico_slots`]
13627 // enumerator, and the cross-slot
13628 // [`crate::upgrade::validate_upgrade_from_against_behavior`]
13629 // gate all read), or a callback-shuffled projection (a future
13630 // detour that swapped `on_init` and `on_terminate` through the
13631 // accessor would silently split the paired
13632 // [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
13633 // traversal input from the peer `servico_m2_overlay` emitter's
13634 // projection input from the cross-slot `:state-change`
13635 // composition gate's traversal input).
13636 use crate::BehaviorSpec;
13637 use std::path::PathBuf;
13638 let fixtures: Vec<Option<BehaviorSpec>> = vec![
13639 None,
13640 Some(BehaviorSpec::default()),
13641 Some(BehaviorSpec {
13642 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13643 ..Default::default()
13644 }),
13645 Some(BehaviorSpec {
13646 on_init: Some(PathBuf::from("lib/init.lisp")),
13647 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13648 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13649 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13650 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13651 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13652 }),
13653 ];
13654 for behavior in fixtures {
13655 let c = caixa_with_behavior(behavior.clone());
13656 assert_eq!(
13657 c.behavior(),
13658 behavior.as_ref(),
13659 "Caixa::behavior must return :behavior verbatim (got \
13660 {:?}, expected {:?})",
13661 c.behavior(),
13662 behavior.as_ref(),
13663 );
13664 match (c.behavior(), c.behavior.as_ref()) {
13665 (Some(a), Some(b)) => assert!(
13666 std::ptr::eq(a, b),
13667 "Caixa::behavior accessor and self.behavior.as_ref() \
13668 field access must borrow the same backing storage \
13669 — the accessor is the substrate-primitive typed \
13670 dispatch every downstream Servico-M2-overlay \
13671 composite consumer must route through, and a \
13672 reference-identity split would silently break \
13673 every consumer that relied on the borrow sharing \
13674 the composite's storage",
13675 ),
13676 (None, None) => {}
13677 _ => panic!(
13678 "Caixa::behavior presence bit must byte-equal \
13679 self.behavior.is_some() — a presence-bit drift \
13680 would silently split the paired \
13681 StandardLayout::verify per-`:behavior` shape \
13682 gate's traversal head from the peer \
13683 render::servico_m2_overlay M2 overlay emitter's \
13684 traversal head from the cross-slot \
13685 validate_upgrade_from_against_behavior \
13686 composition gate's traversal head from the peer \
13687 Caixa::declared_servico_slots M2 declared-slot \
13688 enumerator's presence probe",
13689 ),
13690 }
13691 assert_eq!(
13692 c.behavior().is_some(),
13693 c.behavior.is_some(),
13694 "Caixa::behavior().is_some() must byte-equal \
13695 self.behavior.is_some() — a presence-bit drift would \
13696 silently split every downstream Option<&BehaviorSpec> \
13697 consumer's partition on the runtime-default arm",
13698 );
13699 }
13700 }
13701
13702 #[test]
13703 fn declared_servico_slots_behavior_arm_routes_through_accessor() {
13704 // Composition pin: [`Caixa::declared_servico_slots`]'s
13705 // `:behavior` presence-probe arm must key off
13706 // [`Caixa::behavior`], not the raw `self.behavior.is_some()`
13707 // field-probe. Structurally: a `Caixa { behavior:
13708 // Some(BehaviorSpec::default()), .. }` must still push
13709 // `M2_AUTHOR_KEY_BEHAVIOR` onto the declared-slot list (the
13710 // presence bit is `Some`, so the M2 kind-coherence gate must
13711 // surface the slot as "declared" even when every per-callback
13712 // path is unset), and a `Caixa { behavior: None, .. }` must
13713 // NOT push the label (the "author omitted the slot entirely"
13714 // partition). The pair jointly pins the accessor + declared-
13715 // slot enumerator composition: any future silent detour that
13716 // had the accessor collapse `Some(BehaviorSpec::default())`
13717 // to `None` (a `.filter(|b| !b.is_empty())` projection) would
13718 // silently absorb the "declared but empty" arm at the
13719 // accessor boundary and the
13720 // [`crate::LayoutError::ServicoSlotsOnNonServico`]
13721 // kind-coherence gate would silently accept a struct-literal
13722 // `Caixa` carrying the drift.
13723 //
13724 // Peer of the sibling
13725 // `declared_servico_slots_limits_arm_routes_through_accessor`
13726 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13727 // `Option<&LimitsSpec>` arm of the same
13728 // [`Caixa::declared_servico_slots`] M2 declared-slot
13729 // enumerator's traversal — same "the enumerator gate must
13730 // route through the substrate-primitive typed dispatch"
13731 // discipline extended onto the outer top-level [`Caixa`]
13732 // `Option<&BehaviorSpec>`-composition surface.
13733 use crate::BehaviorSpec;
13734 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13735 let slots = c.declared_servico_slots();
13736 assert!(
13737 slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13738 "declared_servico_slots must push M2_AUTHOR_KEY_BEHAVIOR \
13739 when `:behavior` is Some (even for BehaviorSpec::default()) \
13740 — the accessor and the enumerator gate must route through \
13741 the same substrate-primitive typed dispatch on the outer \
13742 :behavior presence bit (got slots={slots:?})",
13743 );
13744 let c = caixa_with_behavior(None);
13745 let slots = c.declared_servico_slots();
13746 assert!(
13747 !slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13748 "declared_servico_slots must NOT push M2_AUTHOR_KEY_BEHAVIOR \
13749 when `:behavior` is None — the author-omitted arm must \
13750 route through the accessor's None-return unchanged (got \
13751 slots={slots:?})",
13752 );
13753 }
13754
13755 #[test]
13756 fn servico_m2_overlay_behavior_arm_routes_through_accessor() {
13757 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13758 // per-`:behavior` M2 overlay emit arm must key off
13759 // [`Caixa::behavior`], not the raw `&caixa.behavior`
13760 // field-borrow. Structurally: a `Caixa { behavior:
13761 // Some(BehaviorSpec { on_state_change: Some(...), .. default
13762 // }), .. }` must surface the `M2_KEY_BEHAVIOR` key with the
13763 // per-callback `onStateChange` sub-mapping in the overlay, a
13764 // `Caixa { behavior: Some(BehaviorSpec::default()), .. }`
13765 // must omit the key entirely (the `.is_empty()`-gated inner
13766 // arm elides an empty composite even when the outer presence
13767 // bit is `Some`), and a `Caixa { behavior: None, .. }` must
13768 // also omit the key (the "author omitted the slot entirely"
13769 // partition). The three-fixture family jointly pins the
13770 // accessor + M2 overlay emitter composition: any future
13771 // silent detour that had the accessor return a fresh-cloned
13772 // copy on the `Some` arm (a `BehaviorSpec::clone()`
13773 // projection) would silently break the reference-identity
13774 // pin the peer per-callback `serde_yaml::to_value(behavior)`
13775 // projection reads from.
13776 //
13777 // Peer of the sibling
13778 // `servico_m2_overlay_limits_arm_routes_through_accessor`
13779 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13780 // `Option<&LimitsSpec>` arm of the same
13781 // [`crate::render::servico_m2_overlay`] M2 overlay emitter's
13782 // traversal — same "the emitter must route through the
13783 // substrate-primitive typed dispatch on the outer composite"
13784 // discipline extended onto the outer top-level [`Caixa`]
13785 // `Option<&BehaviorSpec>`-composition surface.
13786 use crate::BehaviorSpec;
13787 use crate::render::{M2_KEY_BEHAVIOR, servico_m2_overlay};
13788 use std::path::PathBuf;
13789 let c = caixa_with_behavior(Some(BehaviorSpec {
13790 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13791 ..Default::default()
13792 }));
13793 let overlay = servico_m2_overlay(&c).unwrap();
13794 assert!(
13795 overlay.contains_key(M2_KEY_BEHAVIOR),
13796 "servico_m2_overlay must surface M2_KEY_BEHAVIOR when \
13797 `:behavior` carries a non-empty composite — the accessor \
13798 and the M2 overlay emitter must route through the same \
13799 substrate-primitive typed dispatch on the outer :behavior \
13800 composite (got overlay={overlay:?})",
13801 );
13802 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13803 let overlay = servico_m2_overlay(&c).unwrap();
13804 assert!(
13805 !overlay.contains_key(M2_KEY_BEHAVIOR),
13806 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13807 `:behavior` is Some(BehaviorSpec::default()) — the empty \
13808 composite's `.is_empty()`-gated inner arm must elide the \
13809 key regardless of the outer presence bit (got \
13810 overlay={overlay:?})",
13811 );
13812 let c = caixa_with_behavior(None);
13813 let overlay = servico_m2_overlay(&c).unwrap();
13814 assert!(
13815 !overlay.contains_key(M2_KEY_BEHAVIOR),
13816 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13817 `:behavior` is None — the author-omitted arm must route \
13818 through the accessor's None-return unchanged (got \
13819 overlay={overlay:?})",
13820 );
13821 }
13822
13823 #[test]
13824 fn behavior_projects_option_ref_by_borrow() {
13825 // The by-borrow pin: [`Caixa::behavior`] returns
13826 // `Option<&BehaviorSpec>` by borrow — the returned reference
13827 // borrows the underlying `Option<BehaviorSpec>` storage of the
13828 // `:behavior` slot and the accessor must not clone the backing
13829 // composite on every call. Peer of the sibling
13830 // `limits_projects_option_ref_by_borrow` (b2bd9d7) by-borrow
13831 // pin on the outer top-level [`Caixa`] `Option<&Composite>`-
13832 // return sub-family — extended here to the second axis of the
13833 // same sub-family: the accessor's returned reference must
13834 // borrow from `&self` (the returned reference's lifetime is
13835 // tied to `&self`), and calling the accessor twice on the same
13836 // [`Caixa`] must yield references that are pointer-equal (the
13837 // underlying byte-buffer is the storage `BehaviorSpec`'s
13838 // allocation, not a fresh copy) as well as value-equal
13839 // (idempotent, no side effects on `&self`).
13840 //
13841 // Pins against a future silent detour that returned an owned
13842 // `BehaviorSpec` (which would type-check via the `Clone` impl
13843 // but silently clone on every call), a `&BehaviorSpec` panic-
13844 // return on the `None` arm (which would collapse the load-
13845 // bearing `Option` presence-bit into a runtime panic), or a
13846 // one-arm-only accessor that returned a saturating composite
13847 // on some sentinel input.
13848 use crate::BehaviorSpec;
13849 use std::path::PathBuf;
13850 for behavior in [
13851 Some(BehaviorSpec::default()),
13852 Some(BehaviorSpec {
13853 on_init: Some(PathBuf::from("lib/init.lisp")),
13854 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13855 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13856 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13857 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13858 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13859 }),
13860 ] {
13861 let c = caixa_with_behavior(behavior.clone());
13862 let first = c.behavior().unwrap();
13863 let second = c.behavior().unwrap();
13864 assert_eq!(
13865 first, second,
13866 "Caixa::behavior must be idempotent — two successive \
13867 calls on the same &self must return the same \
13868 &BehaviorSpec",
13869 );
13870 assert!(
13871 std::ptr::eq(first, second),
13872 "Caixa::behavior must borrow the underlying \
13873 Option<BehaviorSpec> storage — two successive calls \
13874 must return references with the same backing pointer \
13875 (a fresh BehaviorSpec clone would change the pointer \
13876 on every call)",
13877 );
13878 assert_eq!(
13879 Some(first),
13880 behavior.as_ref(),
13881 "Caixa::behavior must return :behavior verbatim by \
13882 borrow — got {first:?}, expected {:?}",
13883 behavior.as_ref(),
13884 );
13885 }
13886 let c = caixa_with_behavior(None);
13887 assert!(
13888 c.behavior().is_none(),
13889 "Caixa::behavior must return None when :behavior is absent \
13890 — the author-omitted arm must project through the \
13891 accessor's Option::None unchanged",
13892 );
13893 }
13894
13895 // ── Caixa::politicas — outer top-level Option<&MeshPolicy> composite-reference accessor ──
13896
13897 fn caixa_aplicacao_with_politicas(politicas: Option<crate::aplicacao::MeshPolicy>) -> Caixa {
13898 use crate::aplicacao::{Membro, WitContract};
13899 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13900 c.kind = CaixaKind::Aplicacao;
13901 c.membros = vec![Membro {
13902 caixa: "a".into(),
13903 versao: "^0.1".into(),
13904 }];
13905 c.contratos = vec![WitContract {
13906 de: "a".into(),
13907 para: "a".into(),
13908 wit: "wasi:http/proxy".into(),
13909 endpoint: Some("/x".into()),
13910 subject: None,
13911 slot: None,
13912 }];
13913 c.politicas = politicas;
13914 c
13915 }
13916
13917 #[test]
13918 fn politicas_returns_politicas_option_ref_verbatim_across_permutations() {
13919 // The canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
13920 // composite optional-composite-reference-shape pin:
13921 // [`Caixa::politicas`] must return the `:politicas` typed
13922 // `Option<MeshPolicy>` verbatim as an `Option<&MeshPolicy>`
13923 // reference over the same backing storage the raw
13924 // `self.politicas.as_ref()` field access borrows from,
13925 // byte-equal across every representative fixture in the
13926 // accept-set — the author-omitted `None` shape (the "cluster-
13927 // default applies" partition every downstream mesh-artifact
13928 // emitter treats as "emit no `:politicas` overlay"), the
13929 // empty-composite `Some(MeshPolicy { .. default })` shape
13930 // ([`crate::aplicacao::MeshPolicy::is_empty`] holds — every
13931 // per-axis mesh-policy scalar is `None`, so the peer inner
13932 // [`crate::AplicacaoSpec::politicas`] `.is_empty()`-gated
13933 // caixa-mesh overlay elides every per-axis emit but the outer
13934 // presence-bit is `Some`, so [`Caixa::declared_mesh_slots`]
13935 // still pushes the `M3_AUTHOR_KEY_POLITICAS` label), a
13936 // single-axis fixture (only `:timeout` set — the canonical
13937 // shape a latency-sensitive Aplicacao carries), and a
13938 // fully-populated composite (every per-axis mesh-policy
13939 // scalar set — the canonical shape a fully-governed
13940 // Aplicacao carries).
13941 //
13942 // Pins against a future silent detour that returned a fresh-
13943 // cloned [`crate::aplicacao::MeshPolicy`] copy (which would
13944 // type-check via the `Clone` impl but silently break every
13945 // downstream caller that relied on the reference sharing the
13946 // composite's backing identity), a reference to an operator-
13947 // resolved overlay (the future per-cluster
13948 // `:politicas-overrides` slot — its resolution must land at
13949 // exactly this accessor body, not silently divert the raw
13950 // slot away from the peer [`Caixa::declared_mesh_slots`]
13951 // enumerator's presence probe), a
13952 // `None` → `Some(MeshPolicy::default)` cluster-default
13953 // projection (which would collapse the load-bearing
13954 // "author-omitted `:politicas` ⇒ cluster-default applies"
13955 // partition the peer [`Caixa::declared_mesh_slots`]
13956 // enumerator and the peer [`Caixa::aplicacao_view`]
13957 // Aplicacao-composition seed both read), or an axis-shuffled
13958 // projection (a future detour that swapped `timeout` and
13959 // `retries` through the accessor would silently split the
13960 // paired [`Caixa::aplicacao_view`] seed's fold input from the
13961 // sibling M3 mesh-artifact emitter's projection input).
13962 //
13963 // Third outer top-level [`Caixa`] `Option<&Composite>`-return
13964 // composite-reference accessor pin on the substrate primitive
13965 // — peer of the sibling
13966 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13967 // (b2bd9d7) and
13968 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13969 // (35d8b52) opening tetrad pins on the outer top-level
13970 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13971 // here to the first of the three M3 mesh-slot axes so the
13972 // opening third of the outer `Option<&Composite>` sub-family
13973 // carries the same "byte-equal, borrow-shared, presence-bit-
13974 // preserved" outer-accessor discipline.
13975 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13976 use std::time::Duration;
13977 let fixtures: Vec<Option<MeshPolicy>> = vec![
13978 None,
13979 Some(MeshPolicy::default()),
13980 Some(MeshPolicy {
13981 timeout: Some(Duration::from_secs(30)),
13982 ..Default::default()
13983 }),
13984 Some(MeshPolicy {
13985 timeout: Some(Duration::from_secs(30)),
13986 retries: Some(3),
13987 circuit_breaker: Some(CircuitBreaker {
13988 max_failures: 5,
13989 window: Duration::from_secs(60),
13990 }),
13991 mtls_required: Some(true),
13992 rate_limit: Some(RateLimit {
13993 rate: 100,
13994 window: Duration::from_secs(1),
13995 }),
13996 }),
13997 ];
13998 for politicas in fixtures {
13999 let c = caixa_aplicacao_with_politicas(politicas.clone());
14000 assert_eq!(
14001 c.politicas(),
14002 politicas.as_ref(),
14003 "Caixa::politicas must return :politicas verbatim (got \
14004 {:?}, expected {:?})",
14005 c.politicas(),
14006 politicas.as_ref(),
14007 );
14008 match (c.politicas(), c.politicas.as_ref()) {
14009 (Some(a), Some(b)) => assert!(
14010 std::ptr::eq(a, b),
14011 "Caixa::politicas accessor and self.politicas.as_ref() \
14012 field access must borrow the same backing storage \
14013 — the accessor is the substrate-primitive typed \
14014 dispatch every downstream Aplicacao-mesh-overlay \
14015 composite consumer must route through, and a \
14016 reference-identity split would silently break \
14017 every consumer that relied on the borrow sharing \
14018 the composite's storage",
14019 ),
14020 (None, None) => {}
14021 _ => panic!(
14022 "Caixa::politicas presence bit must byte-equal \
14023 self.politicas.is_some() — a presence-bit drift \
14024 would silently split the paired \
14025 Caixa::aplicacao_view Aplicacao-composition seed's \
14026 traversal head from the peer \
14027 Caixa::declared_mesh_slots M3 declared-slot \
14028 enumerator's presence probe",
14029 ),
14030 }
14031 assert_eq!(
14032 c.politicas().is_some(),
14033 c.politicas.is_some(),
14034 "Caixa::politicas().is_some() must byte-equal \
14035 self.politicas.is_some() — a presence-bit drift would \
14036 silently split every downstream Option<&MeshPolicy> \
14037 consumer's partition on the cluster-default arm",
14038 );
14039 }
14040 }
14041
14042 #[test]
14043 fn declared_mesh_slots_politicas_arm_routes_through_accessor() {
14044 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14045 // `:politicas` presence-probe arm must key off
14046 // [`Caixa::politicas`], not the raw `self.politicas.is_some()`
14047 // field-probe. Structurally: a `Caixa { politicas:
14048 // Some(MeshPolicy::default()), .. }` must still push
14049 // `M3_AUTHOR_KEY_POLITICAS` onto the declared-slot list (the
14050 // presence bit is `Some`, so the M3 kind-coherence gate must
14051 // surface the slot as "declared" even when every per-axis
14052 // scalar is unset), and a `Caixa { politicas: None, .. }` must
14053 // NOT push the label (the "author omitted the slot entirely"
14054 // partition). The pair jointly pins the accessor + declared-
14055 // slot enumerator composition: any future silent detour that
14056 // had the accessor collapse `Some(MeshPolicy::default())` to
14057 // `None` (a `.filter(|p| !p.is_empty())` projection) would
14058 // silently absorb the "declared but empty" arm at the
14059 // accessor boundary and the
14060 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
14061 // coherence gate would silently accept a struct-literal
14062 // `Caixa` carrying the drift.
14063 //
14064 // Peer of the sibling
14065 // `declared_servico_slots_limits_arm_routes_through_accessor`
14066 // (b2bd9d7) and
14067 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14068 // (35d8b52) composition pins on the sibling `:limits` /
14069 // `:behavior` outer-`Option<&Composite>` arms of the peer
14070 // [`Caixa::declared_servico_slots`] M2 declared-slot
14071 // enumerator's traversal — same "the enumerator gate must
14072 // route through the substrate-primitive typed dispatch"
14073 // discipline extended onto the outer top-level [`Caixa`] M3
14074 // mesh-slot family so the [`Caixa::declared_mesh_slots`]
14075 // enumerator carries the same routing invariant as its M2
14076 // sibling.
14077 use crate::aplicacao::MeshPolicy;
14078 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
14079 let slots = c.declared_mesh_slots();
14080 assert!(
14081 slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
14082 "declared_mesh_slots must push M3_AUTHOR_KEY_POLITICAS \
14083 when `:politicas` is Some (even for MeshPolicy::default()) \
14084 — the accessor and the enumerator gate must route through \
14085 the same substrate-primitive typed dispatch on the outer \
14086 :politicas presence bit (got slots={slots:?})",
14087 );
14088 let c = caixa_aplicacao_with_politicas(None);
14089 let slots = c.declared_mesh_slots();
14090 assert!(
14091 !slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
14092 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_POLITICAS \
14093 when `:politicas` is None — the author-omitted arm must \
14094 route through the accessor's None-return unchanged (got \
14095 slots={slots:?})",
14096 );
14097 }
14098
14099 #[test]
14100 fn aplicacao_view_politicas_arm_folds_through_accessor() {
14101 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:politicas`
14102 // Aplicacao-composition seed must fold through
14103 // [`Caixa::politicas`], not the raw
14104 // `self.politicas.clone().unwrap_or_default()` field-borrow.
14105 // Structurally: a `Caixa { politicas: Some(MeshPolicy {
14106 // timeout: Some(30s), .. default }), kind: Aplicacao, .. }`
14107 // must surface a projected [`crate::AplicacaoSpec`] whose
14108 // `politicas().timeout()` field byte-equals the outer
14109 // composite's `timeout` scalar (the fold must project the
14110 // authored composite verbatim), a `Caixa { politicas:
14111 // Some(MeshPolicy::default()), kind: Aplicacao, .. }` must
14112 // surface an [`crate::AplicacaoSpec`] whose `politicas()`
14113 // byte-equals [`crate::aplicacao::MeshPolicy::default`] (the
14114 // fold's empty-composite arm collapses to the same default the
14115 // author-omitted arm does), and a `Caixa { politicas: None,
14116 // kind: Aplicacao, .. }` must surface an
14117 // [`crate::AplicacaoSpec`] whose `politicas()` byte-equals
14118 // [`crate::aplicacao::MeshPolicy::default`] (the "author
14119 // omitted the slot entirely" arm folds through the
14120 // `unwrap_or_default` onto the cluster-default). The triad
14121 // jointly pins the accessor + Aplicacao-composition seed
14122 // composition: any future silent detour that had the accessor
14123 // divert the raw slot away from the seed's fold (an operator-
14124 // resolved overlay's default-fold arm silently differing from
14125 // the raw slot's default-fold arm) would silently split the
14126 // build-time mesh-artifact emission gate from the caixa-mesh
14127 // renderer's Aplicacao-view input at the composition boundary.
14128 use crate::aplicacao::MeshPolicy;
14129 use std::time::Duration;
14130 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy {
14131 timeout: Some(Duration::from_secs(30)),
14132 ..Default::default()
14133 }));
14134 let view = c.aplicacao_view().unwrap();
14135 assert_eq!(
14136 view.politicas().timeout(),
14137 Some(Duration::from_secs(30)),
14138 "Caixa::aplicacao_view must fold the authored :politicas \
14139 :timeout scalar through the accessor verbatim onto the \
14140 projected AplicacaoSpec — a future silent detour at the \
14141 seed's fold arm would surface here as a projected-scalar \
14142 drift (got {:?})",
14143 view.politicas().timeout(),
14144 );
14145 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
14146 let view = c.aplicacao_view().unwrap();
14147 assert_eq!(
14148 view.politicas(),
14149 &MeshPolicy::default(),
14150 "Caixa::aplicacao_view must fold Some(MeshPolicy::default()) \
14151 through the accessor onto MeshPolicy::default — the empty- \
14152 composite arm collapses to the same default the author- \
14153 omitted arm does (got {:?})",
14154 view.politicas(),
14155 );
14156 let c = caixa_aplicacao_with_politicas(None);
14157 let view = c.aplicacao_view().unwrap();
14158 assert_eq!(
14159 view.politicas(),
14160 &MeshPolicy::default(),
14161 "Caixa::aplicacao_view must fold None through the accessor's \
14162 unwrap_or_default onto MeshPolicy::default — the author- \
14163 omitted arm must route through the accessor's None-return \
14164 unchanged (got {:?})",
14165 view.politicas(),
14166 );
14167 }
14168
14169 #[test]
14170 fn politicas_projects_option_ref_by_borrow() {
14171 // The by-borrow pin: [`Caixa::politicas`] returns
14172 // `Option<&MeshPolicy>` by borrow — the returned reference
14173 // borrows the underlying `Option<MeshPolicy>` storage of the
14174 // `:politicas` slot and the accessor must not clone the
14175 // backing composite on every call. Peer of the sibling
14176 // `limits_projects_option_ref_by_borrow` (b2bd9d7) and
14177 // `behavior_projects_option_ref_by_borrow` (35d8b52) by-borrow
14178 // pins on the outer top-level [`Caixa`]
14179 // `Option<&Composite>`-return sub-family — extended here to
14180 // the third axis of the same sub-family: the accessor's
14181 // returned reference must borrow from `&self` (the returned
14182 // reference's lifetime is tied to `&self`), and calling the
14183 // accessor twice on the same [`Caixa`] must yield references
14184 // that are pointer-equal (the underlying byte-buffer is the
14185 // storage `MeshPolicy`'s allocation, not a fresh copy) as
14186 // well as value-equal (idempotent, no side effects on
14187 // `&self`).
14188 //
14189 // Pins against a future silent detour that returned an owned
14190 // `MeshPolicy` (which would type-check via the `Clone` impl
14191 // but silently clone on every call), a `&MeshPolicy` panic-
14192 // return on the `None` arm (which would collapse the load-
14193 // bearing `Option` presence-bit into a runtime panic), or a
14194 // one-arm-only accessor that returned a saturating composite
14195 // on some sentinel input.
14196 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
14197 use std::time::Duration;
14198 for politicas in [
14199 Some(MeshPolicy::default()),
14200 Some(MeshPolicy {
14201 timeout: Some(Duration::from_secs(30)),
14202 retries: Some(3),
14203 circuit_breaker: Some(CircuitBreaker {
14204 max_failures: 5,
14205 window: Duration::from_secs(60),
14206 }),
14207 mtls_required: Some(true),
14208 rate_limit: Some(RateLimit {
14209 rate: 100,
14210 window: Duration::from_secs(1),
14211 }),
14212 }),
14213 ] {
14214 let c = caixa_aplicacao_with_politicas(politicas.clone());
14215 let first = c.politicas().unwrap();
14216 let second = c.politicas().unwrap();
14217 assert_eq!(
14218 first, second,
14219 "Caixa::politicas must be idempotent — two successive \
14220 calls on the same &self must return the same \
14221 &MeshPolicy",
14222 );
14223 assert!(
14224 std::ptr::eq(first, second),
14225 "Caixa::politicas must borrow the underlying \
14226 Option<MeshPolicy> storage — two successive calls \
14227 must return references with the same backing pointer \
14228 (a fresh MeshPolicy clone would change the pointer on \
14229 every call)",
14230 );
14231 assert_eq!(
14232 Some(first),
14233 politicas.as_ref(),
14234 "Caixa::politicas must return :politicas verbatim by \
14235 borrow — got {first:?}, expected {:?}",
14236 politicas.as_ref(),
14237 );
14238 }
14239 let c = caixa_aplicacao_with_politicas(None);
14240 assert!(
14241 c.politicas().is_none(),
14242 "Caixa::politicas must return None when :politicas is \
14243 absent — the author-omitted arm must project through the \
14244 accessor's Option::None unchanged",
14245 );
14246 }
14247
14248 // ── Caixa::placement — outer top-level Option<&Placement> composite-reference accessor ──
14249
14250 fn caixa_aplicacao_with_placement(placement: Option<crate::aplicacao::Placement>) -> Caixa {
14251 use crate::aplicacao::{Membro, WitContract};
14252 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14253 c.kind = CaixaKind::Aplicacao;
14254 c.membros = vec![Membro {
14255 caixa: "a".into(),
14256 versao: "^0.1".into(),
14257 }];
14258 c.contratos = vec![WitContract {
14259 de: "a".into(),
14260 para: "a".into(),
14261 wit: "wasi:http/proxy".into(),
14262 endpoint: Some("/x".into()),
14263 subject: None,
14264 slot: None,
14265 }];
14266 c.placement = placement;
14267 c
14268 }
14269
14270 #[test]
14271 fn placement_returns_placement_option_ref_verbatim_across_permutations() {
14272 // The canonical per-`Caixa` `:placement` M3 mesh-slot outer-
14273 // composite optional-composite-reference-shape pin:
14274 // [`Caixa::placement`] must return the `:placement` typed
14275 // `Option<Placement>` verbatim as an `Option<&Placement>`
14276 // reference over the same backing storage the raw
14277 // `self.placement.as_ref()` field access borrows from,
14278 // byte-equal across every representative fixture in the
14279 // accept-set — the author-omitted `None` shape (the
14280 // "cluster-default applies" partition every downstream mesh-
14281 // artifact emitter treats as "emit no `:placement` overlay"),
14282 // the empty-composite `Some(Placement { .. default })` shape
14283 // (`estrategia: SingleNode`, empty clusters, no shard-key /
14284 // affinity — the outer presence-bit is `Some` so
14285 // [`Caixa::declared_mesh_slots`] still pushes the
14286 // `M3_AUTHOR_KEY_PLACEMENT` label), a single-axis
14287 // `Replicated`-on-two-clusters fixture (the canonical shape a
14288 // stateless HTTP Aplicacao carries), and a fully-populated
14289 // `Sharded`-with-shard-key-and-affinity fixture (the canonical
14290 // shape a stateful Akka-style cluster-sharding Aplicacao
14291 // carries).
14292 //
14293 // Pins against a future silent detour that returned a fresh-
14294 // cloned [`crate::aplicacao::Placement`] copy (which would
14295 // type-check via the `Clone` impl but silently break every
14296 // downstream caller that relied on the reference sharing the
14297 // composite's backing identity), a reference to an operator-
14298 // resolved overlay (the future per-cluster
14299 // `:placement-overrides` slot — its resolution must land at
14300 // exactly this accessor body, not silently divert the raw
14301 // slot away from the peer [`Caixa::declared_mesh_slots`]
14302 // enumerator's presence probe), a `None` →
14303 // `Some(Placement::default)` cluster-default projection (which
14304 // would collapse the load-bearing "author-omitted `:placement`
14305 // ⇒ cluster-default applies" partition the peer
14306 // [`Caixa::declared_mesh_slots`] enumerator and the peer
14307 // [`Caixa::aplicacao_view`] Aplicacao-composition seed both
14308 // read), or an axis-shuffled projection (a future detour that
14309 // swapped `clusters` and `affinity` through the accessor would
14310 // silently split the paired [`Caixa::aplicacao_view`] seed's
14311 // fold input from the sibling M3 mesh-artifact emitter's
14312 // projection input).
14313 //
14314 // Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
14315 // composite-reference accessor pin on the substrate primitive
14316 // — peer of the sibling
14317 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14318 // (b2bd9d7),
14319 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14320 // (35d8b52), and
14321 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
14322 // (5d23d29) opening triad pins on the outer top-level
14323 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14324 // here to the second of the three M3 mesh-slot axes so the
14325 // opening four-fifths of the outer `Option<&Composite>` sub-
14326 // family carries the same "byte-equal, borrow-shared,
14327 // presence-bit-preserved" outer-accessor discipline.
14328 use crate::aplicacao::{Placement, PlacementStrategy};
14329 let fixtures: Vec<Option<Placement>> = vec![
14330 None,
14331 Some(Placement::default()),
14332 Some(Placement {
14333 estrategia: PlacementStrategy::Replicated,
14334 clusters: vec!["rio".into(), "sao-paulo".into()],
14335 affinity: None,
14336 shard_key: None,
14337 }),
14338 Some(Placement {
14339 estrategia: PlacementStrategy::Sharded,
14340 clusters: vec!["rio".into(), "sao-paulo".into(), "brasilia".into()],
14341 affinity: Some("data-locality".into()),
14342 shard_key: Some("$tenantId".into()),
14343 }),
14344 ];
14345 for placement in fixtures {
14346 let c = caixa_aplicacao_with_placement(placement.clone());
14347 assert_eq!(
14348 c.placement(),
14349 placement.as_ref(),
14350 "Caixa::placement must return :placement verbatim (got \
14351 {:?}, expected {:?})",
14352 c.placement(),
14353 placement.as_ref(),
14354 );
14355 match (c.placement(), c.placement.as_ref()) {
14356 (Some(a), Some(b)) => assert!(
14357 std::ptr::eq(a, b),
14358 "Caixa::placement accessor and self.placement.as_ref() \
14359 field access must borrow the same backing storage \
14360 — the accessor is the substrate-primitive typed \
14361 dispatch every downstream Aplicacao-distribution- \
14362 overlay composite consumer must route through, and \
14363 a reference-identity split would silently break \
14364 every consumer that relied on the borrow sharing \
14365 the composite's storage",
14366 ),
14367 (None, None) => {}
14368 _ => panic!(
14369 "Caixa::placement presence bit must byte-equal \
14370 self.placement.is_some() — a presence-bit drift \
14371 would silently split the paired \
14372 Caixa::aplicacao_view Aplicacao-composition seed's \
14373 traversal head from the peer \
14374 Caixa::declared_mesh_slots M3 declared-slot \
14375 enumerator's presence probe",
14376 ),
14377 }
14378 assert_eq!(
14379 c.placement().is_some(),
14380 c.placement.is_some(),
14381 "Caixa::placement().is_some() must byte-equal \
14382 self.placement.is_some() — a presence-bit drift would \
14383 silently split every downstream Option<&Placement> \
14384 consumer's partition on the cluster-default arm",
14385 );
14386 }
14387 }
14388
14389 #[test]
14390 fn declared_mesh_slots_placement_arm_routes_through_accessor() {
14391 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14392 // `:placement` presence-probe arm must key off
14393 // [`Caixa::placement`], not the raw `self.placement.is_some()`
14394 // field-probe. Structurally: a `Caixa { placement:
14395 // Some(Placement::default()), .. }` must still push
14396 // `M3_AUTHOR_KEY_PLACEMENT` onto the declared-slot list (the
14397 // presence bit is `Some`, so the M3 kind-coherence gate must
14398 // surface the slot as "declared" even when every per-axis
14399 // scalar defers to the cluster-default arm), and a `Caixa {
14400 // placement: None, .. }` must NOT push the label (the "author
14401 // omitted the slot entirely" partition). The pair jointly pins
14402 // the accessor + declared-slot enumerator composition: any
14403 // future silent detour that had the accessor collapse
14404 // `Some(Placement::default())` to `None` (a `.filter(|p|
14405 // p.clusters().is_empty().not())` projection) would silently
14406 // absorb the "declared but empty" arm at the accessor boundary
14407 // and the [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
14408 // kind-coherence gate would silently accept a struct-literal
14409 // `Caixa` carrying the drift.
14410 //
14411 // Peer of the sibling
14412 // `declared_servico_slots_limits_arm_routes_through_accessor`
14413 // (b2bd9d7),
14414 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14415 // (35d8b52), and
14416 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14417 // (5d23d29) composition pins on the sibling `:limits` /
14418 // `:behavior` / `:politicas` outer-`Option<&Composite>` arms
14419 // — same "the enumerator gate must route through the
14420 // substrate-primitive typed dispatch" discipline extended onto
14421 // the second of the three M3 mesh-slot axes so the
14422 // [`Caixa::declared_mesh_slots`] enumerator carries the same
14423 // routing invariant on the `:placement` arm as the peer
14424 // `:politicas` arm.
14425 use crate::aplicacao::Placement;
14426 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14427 let slots = c.declared_mesh_slots();
14428 assert!(
14429 slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14430 "declared_mesh_slots must push M3_AUTHOR_KEY_PLACEMENT \
14431 when `:placement` is Some (even for Placement::default()) \
14432 — the accessor and the enumerator gate must route through \
14433 the same substrate-primitive typed dispatch on the outer \
14434 :placement presence bit (got slots={slots:?})",
14435 );
14436 let c = caixa_aplicacao_with_placement(None);
14437 let slots = c.declared_mesh_slots();
14438 assert!(
14439 !slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14440 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_PLACEMENT \
14441 when `:placement` is None — the author-omitted arm must \
14442 route through the accessor's None-return unchanged (got \
14443 slots={slots:?})",
14444 );
14445 }
14446
14447 #[test]
14448 fn aplicacao_view_placement_arm_folds_through_accessor() {
14449 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:placement`
14450 // Aplicacao-composition seed must fold through
14451 // [`Caixa::placement`], not the raw
14452 // `self.placement.clone().unwrap_or_default()` field-borrow.
14453 // Structurally: a `Caixa { placement: Some(Placement {
14454 // estrategia: Replicated, clusters: ["rio"], .. default }),
14455 // kind: Aplicacao, .. }` must surface a projected
14456 // [`crate::AplicacaoSpec`] whose `placement().estrategia()` +
14457 // `placement().clusters()` byte-equal the outer composite's
14458 // authored values (the fold must project the authored
14459 // composite verbatim), a `Caixa { placement:
14460 // Some(Placement::default()), kind: Aplicacao, .. }` must
14461 // surface an [`crate::AplicacaoSpec`] whose `placement()`
14462 // byte-equals [`crate::aplicacao::Placement::default`] (the
14463 // fold's empty-composite arm collapses to the same default
14464 // the author-omitted arm does), and a `Caixa { placement:
14465 // None, kind: Aplicacao, .. }` must surface an
14466 // [`crate::AplicacaoSpec`] whose `placement()` byte-equals
14467 // [`crate::aplicacao::Placement::default`] (the "author
14468 // omitted the slot entirely" arm folds through the
14469 // `unwrap_or_default` onto the cluster-default). The triad
14470 // jointly pins the accessor + Aplicacao-composition seed
14471 // composition: any future silent detour that had the accessor
14472 // divert the raw slot away from the seed's fold (an operator-
14473 // resolved overlay's default-fold arm silently differing from
14474 // the raw slot's default-fold arm) would silently split the
14475 // build-time distribution-artifact emission gate from the
14476 // caixa-mesh renderer's Aplicacao-view input at the
14477 // composition boundary.
14478 use crate::aplicacao::{Placement, PlacementStrategy};
14479 let c = caixa_aplicacao_with_placement(Some(Placement {
14480 estrategia: PlacementStrategy::Replicated,
14481 clusters: vec!["rio".into()],
14482 affinity: None,
14483 shard_key: None,
14484 }));
14485 let view = c.aplicacao_view().unwrap();
14486 assert_eq!(
14487 view.placement().estrategia(),
14488 PlacementStrategy::Replicated,
14489 "Caixa::aplicacao_view must fold the authored :placement \
14490 :estrategia scalar through the accessor verbatim onto the \
14491 projected AplicacaoSpec — a future silent detour at the \
14492 seed's fold arm would surface here as a projected-scalar \
14493 drift (got {:?})",
14494 view.placement().estrategia(),
14495 );
14496 assert_eq!(
14497 view.placement().clusters(),
14498 &["rio"],
14499 "Caixa::aplicacao_view must fold the authored :placement \
14500 :clusters list through the accessor verbatim onto the \
14501 projected AplicacaoSpec — a future silent detour at the \
14502 seed's fold arm would surface here as a projected-list \
14503 drift (got {:?})",
14504 view.placement().clusters(),
14505 );
14506 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14507 let view = c.aplicacao_view().unwrap();
14508 assert_eq!(
14509 view.placement(),
14510 &Placement::default(),
14511 "Caixa::aplicacao_view must fold Some(Placement::default()) \
14512 through the accessor onto Placement::default — the empty- \
14513 composite arm collapses to the same default the author- \
14514 omitted arm does (got {:?})",
14515 view.placement(),
14516 );
14517 let c = caixa_aplicacao_with_placement(None);
14518 let view = c.aplicacao_view().unwrap();
14519 assert_eq!(
14520 view.placement(),
14521 &Placement::default(),
14522 "Caixa::aplicacao_view must fold None through the accessor's \
14523 unwrap_or_default onto Placement::default — the author- \
14524 omitted arm must route through the accessor's None-return \
14525 unchanged (got {:?})",
14526 view.placement(),
14527 );
14528 }
14529
14530 #[test]
14531 fn placement_projects_option_ref_by_borrow() {
14532 // The by-borrow pin: [`Caixa::placement`] returns
14533 // `Option<&Placement>` by borrow — the returned reference
14534 // borrows the underlying `Option<Placement>` storage of the
14535 // `:placement` slot and the accessor must not clone the
14536 // backing composite on every call. Peer of the sibling
14537 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14538 // `behavior_projects_option_ref_by_borrow` (35d8b52), and
14539 // `politicas_projects_option_ref_by_borrow` (5d23d29) by-borrow
14540 // pins on the outer top-level [`Caixa`]
14541 // `Option<&Composite>`-return sub-family — extended here to
14542 // the fourth axis of the same sub-family: the accessor's
14543 // returned reference must borrow from `&self` (the returned
14544 // reference's lifetime is tied to `&self`), and calling the
14545 // accessor twice on the same [`Caixa`] must yield references
14546 // that are pointer-equal (the underlying byte-buffer is the
14547 // storage `Placement`'s allocation, not a fresh copy) as well
14548 // as value-equal (idempotent, no side effects on `&self`).
14549 //
14550 // Pins against a future silent detour that returned an owned
14551 // `Placement` (which would type-check via the `Clone` impl
14552 // but silently clone on every call), a `&Placement` panic-
14553 // return on the `None` arm (which would collapse the load-
14554 // bearing `Option` presence-bit into a runtime panic), or a
14555 // one-arm-only accessor that returned a saturating composite
14556 // on some sentinel input.
14557 use crate::aplicacao::{Placement, PlacementStrategy};
14558 for placement in [
14559 Some(Placement::default()),
14560 Some(Placement {
14561 estrategia: PlacementStrategy::Sharded,
14562 clusters: vec!["rio".into(), "sao-paulo".into()],
14563 affinity: Some("data-locality".into()),
14564 shard_key: Some("$tenantId".into()),
14565 }),
14566 ] {
14567 let c = caixa_aplicacao_with_placement(placement.clone());
14568 let first = c.placement().unwrap();
14569 let second = c.placement().unwrap();
14570 assert_eq!(
14571 first, second,
14572 "Caixa::placement must be idempotent — two successive \
14573 calls on the same &self must return the same \
14574 &Placement",
14575 );
14576 assert!(
14577 std::ptr::eq(first, second),
14578 "Caixa::placement must borrow the underlying \
14579 Option<Placement> storage — two successive calls \
14580 must return references with the same backing pointer \
14581 (a fresh Placement clone would change the pointer on \
14582 every call)",
14583 );
14584 assert_eq!(
14585 Some(first),
14586 placement.as_ref(),
14587 "Caixa::placement must return :placement verbatim by \
14588 borrow — got {first:?}, expected {:?}",
14589 placement.as_ref(),
14590 );
14591 }
14592 let c = caixa_aplicacao_with_placement(None);
14593 assert!(
14594 c.placement().is_none(),
14595 "Caixa::placement must return None when :placement is \
14596 absent — the author-omitted arm must project through the \
14597 accessor's Option::None unchanged",
14598 );
14599 }
14600
14601 // ── Caixa::entrada — outer top-level Option<&Entrada> composite-reference accessor ──
14602
14603 fn caixa_aplicacao_with_entrada(entrada: Option<crate::aplicacao::Entrada>) -> Caixa {
14604 use crate::aplicacao::{Membro, WitContract};
14605 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14606 c.kind = CaixaKind::Aplicacao;
14607 c.membros = vec![Membro {
14608 caixa: "a".into(),
14609 versao: "^0.1".into(),
14610 }];
14611 c.contratos = vec![WitContract {
14612 de: "a".into(),
14613 para: "a".into(),
14614 wit: "wasi:http/proxy".into(),
14615 endpoint: Some("/x".into()),
14616 subject: None,
14617 slot: None,
14618 }];
14619 c.entrada = entrada;
14620 c
14621 }
14622
14623 #[test]
14624 fn entrada_returns_entrada_option_ref_verbatim_across_permutations() {
14625 // The canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
14626 // composite optional-composite-reference-shape pin:
14627 // [`Caixa::entrada`] must return the `:entrada` typed
14628 // `Option<Entrada>` verbatim as an `Option<&Entrada>`
14629 // reference over the same backing storage the raw
14630 // `self.entrada.as_ref()` field access borrows from,
14631 // byte-equal across every representative fixture in the
14632 // accept-set — the author-omitted `None` shape (the
14633 // "cluster-internal Aplicacao" partition every downstream
14634 // Gateway-API emitter treats as "emit no listener + no
14635 // HTTPRoute"), a bare-`host`/`para` minimum-composite fixture
14636 // (empty `paths` — the resolved-paths fallback the peer
14637 // [`crate::aplicacao::Entrada::resolved_paths`] cascade folds
14638 // onto the substrate catch-all), and a fully-populated
14639 // multi-path-with-non-default-port fixture (the canonical
14640 // shape a public HTTP Aplicacao carries).
14641 //
14642 // Pins against a future silent detour that returned a fresh-
14643 // cloned [`crate::aplicacao::Entrada`] copy (which would
14644 // type-check via the `Clone` impl but silently break every
14645 // downstream caller that relied on the reference sharing the
14646 // composite's backing identity), a reference to an operator-
14647 // resolved overlay (the future per-cluster
14648 // `:entrada-overrides` slot — its resolution must land at
14649 // exactly this accessor body, not silently divert the raw
14650 // slot away from the peer [`Caixa::declared_mesh_slots`]
14651 // enumerator's presence probe), or an axis-shuffled projection
14652 // (a future detour that swapped `host` and `para` through the
14653 // accessor would silently split the paired
14654 // [`Caixa::aplicacao_view`] seed's forward input from the
14655 // sibling M3 gateway-artifact emitter's projection input).
14656 //
14657 // Fifth and final outer top-level [`Caixa`]
14658 // `Option<&Composite>`-return composite-reference accessor pin
14659 // on the substrate primitive — peer of the sibling
14660 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14661 // (b2bd9d7),
14662 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14663 // (35d8b52),
14664 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
14665 // (5d23d29), and
14666 // `placement_returns_placement_option_ref_verbatim_across_permutations`
14667 // (4fb8074) opening tetrad pins on the outer top-level
14668 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14669 // here to the third and final M3 mesh-slot axis so the closed
14670 // outer `Option<&Composite>` sub-family carries the same
14671 // "byte-equal, borrow-shared, presence-bit-preserved" outer-
14672 // accessor discipline across all five arms.
14673 use crate::aplicacao::Entrada;
14674 let fixtures: Vec<Option<Entrada>> = vec![
14675 None,
14676 Some(Entrada {
14677 host: "checkout.quero.cloud".into(),
14678 para: "gateway".into(),
14679 paths: Vec::new(),
14680 port: crate::DEFAULT_SERVICO_PORT,
14681 }),
14682 Some(Entrada {
14683 host: "api.pleme.io".into(),
14684 para: "public-api".into(),
14685 paths: vec!["/v1".into(), "/v2".into()],
14686 port: 8080,
14687 }),
14688 ];
14689 for entrada in fixtures {
14690 let c = caixa_aplicacao_with_entrada(entrada.clone());
14691 assert_eq!(
14692 c.entrada(),
14693 entrada.as_ref(),
14694 "Caixa::entrada must return :entrada verbatim (got \
14695 {:?}, expected {:?})",
14696 c.entrada(),
14697 entrada.as_ref(),
14698 );
14699 match (c.entrada(), c.entrada.as_ref()) {
14700 (Some(a), Some(b)) => assert!(
14701 std::ptr::eq(a, b),
14702 "Caixa::entrada accessor and self.entrada.as_ref() \
14703 field access must borrow the same backing storage \
14704 — the accessor is the substrate-primitive typed \
14705 dispatch every downstream Aplicacao-external- \
14706 gateway composite consumer must route through, and \
14707 a reference-identity split would silently break \
14708 every consumer that relied on the borrow sharing \
14709 the composite's storage",
14710 ),
14711 (None, None) => {}
14712 _ => panic!(
14713 "Caixa::entrada presence bit must byte-equal \
14714 self.entrada.is_some() — a presence-bit drift \
14715 would silently split the paired \
14716 Caixa::aplicacao_view Aplicacao-composition seed's \
14717 traversal head from the peer \
14718 Caixa::declared_mesh_slots M3 declared-slot \
14719 enumerator's presence probe",
14720 ),
14721 }
14722 assert_eq!(
14723 c.entrada().is_some(),
14724 c.entrada.is_some(),
14725 "Caixa::entrada().is_some() must byte-equal \
14726 self.entrada.is_some() — a presence-bit drift would \
14727 silently split every downstream Option<&Entrada> \
14728 consumer's partition on the cluster-internal arm",
14729 );
14730 }
14731 }
14732
14733 #[test]
14734 fn declared_mesh_slots_entrada_arm_routes_through_accessor() {
14735 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:entrada`
14736 // presence-probe arm must key off [`Caixa::entrada`], not the
14737 // raw `self.entrada.is_some()` field-probe. Structurally: a
14738 // `Caixa { entrada: Some(Entrada { host: "...", para: "...",
14739 // paths: [], port: DEFAULT_SERVICO_PORT }), .. }` must push
14740 // `M3_AUTHOR_KEY_ENTRADA` onto the declared-slot list (the
14741 // presence bit is `Some`, so the M3 kind-coherence gate must
14742 // surface the slot as "declared" even when every per-axis
14743 // scalar defers to the substrate catch-all / default port),
14744 // and a `Caixa { entrada: None, .. }` must NOT push the label
14745 // (the "author omitted the slot entirely" partition). The pair
14746 // jointly pins the accessor + declared-slot enumerator
14747 // composition: any future silent detour that had the accessor
14748 // collapse `Some(Entrada { paths: [], .. })` to `None` (a
14749 // `.filter(|e| !e.paths.is_empty())` projection) would silently
14750 // absorb the "declared but empty-paths" arm at the accessor
14751 // boundary and the
14752 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
14753 // coherence gate would silently accept a struct-literal
14754 // `Caixa` carrying the drift.
14755 //
14756 // Peer of the sibling
14757 // `declared_servico_slots_limits_arm_routes_through_accessor`
14758 // (b2bd9d7),
14759 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14760 // (35d8b52),
14761 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14762 // (5d23d29), and
14763 // `declared_mesh_slots_placement_arm_routes_through_accessor`
14764 // (4fb8074) composition pins on the sibling `:limits` /
14765 // `:behavior` / `:politicas` / `:placement` outer-
14766 // `Option<&Composite>` arms — same "the enumerator gate must
14767 // route through the substrate-primitive typed dispatch"
14768 // discipline extended onto the third and final M3 mesh-slot
14769 // axis so the [`Caixa::declared_mesh_slots`] enumerator now
14770 // carries the routing invariant on every M3 mesh-slot arm.
14771 use crate::aplicacao::Entrada;
14772 let c = caixa_aplicacao_with_entrada(Some(Entrada {
14773 host: "checkout.quero.cloud".into(),
14774 para: "gateway".into(),
14775 paths: Vec::new(),
14776 port: crate::DEFAULT_SERVICO_PORT,
14777 }));
14778 let slots = c.declared_mesh_slots();
14779 assert!(
14780 slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14781 "declared_mesh_slots must push M3_AUTHOR_KEY_ENTRADA when \
14782 `:entrada` is Some (even for empty-paths / default-port) \
14783 — the accessor and the enumerator gate must route through \
14784 the same substrate-primitive typed dispatch on the outer \
14785 :entrada presence bit (got slots={slots:?})",
14786 );
14787 let c = caixa_aplicacao_with_entrada(None);
14788 let slots = c.declared_mesh_slots();
14789 assert!(
14790 !slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14791 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_ENTRADA \
14792 when `:entrada` is None — the author-omitted arm must \
14793 route through the accessor's None-return unchanged (got \
14794 slots={slots:?})",
14795 );
14796 }
14797
14798 #[test]
14799 fn aplicacao_view_entrada_arm_folds_through_accessor() {
14800 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:entrada`
14801 // Aplicacao-composition seed must fold through
14802 // [`Caixa::entrada`], not the raw `self.entrada.clone()` field-
14803 // borrow. Structurally: a `Caixa { entrada: Some(Entrada {
14804 // host: "api.pleme.io", para: "public-api", paths: ["/v1"],
14805 // port: 8080 }), kind: Aplicacao, .. }` must surface a projected
14806 // [`crate::AplicacaoSpec`] whose `entrada().unwrap()` byte-
14807 // equals the outer composite's authored value (the fold must
14808 // project the authored composite verbatim), and a `Caixa {
14809 // entrada: None, kind: Aplicacao, .. }` must surface an
14810 // [`crate::AplicacaoSpec`] whose `entrada()` is `None` (the
14811 // "author omitted the slot entirely" arm folds through the
14812 // accessor's `Option::cloned` onto the same `None` presence
14813 // bit — unlike the peer `:politicas` / `:placement` arms
14814 // `:entrada` has no cluster-default fold, the omitted arm
14815 // stays omitted). The pair jointly pins the accessor +
14816 // Aplicacao-composition seed composition: any future silent
14817 // detour that had the accessor divert the raw slot away from
14818 // the seed's fold (an operator-resolved overlay's forward arm
14819 // silently differing from the raw slot's forward arm) would
14820 // silently split the build-time gateway-artifact emission gate
14821 // from the caixa-mesh renderer's Aplicacao-view input at the
14822 // composition boundary.
14823 use crate::aplicacao::Entrada;
14824 let authored = Entrada {
14825 host: "api.pleme.io".into(),
14826 para: "public-api".into(),
14827 paths: vec!["/v1".into()],
14828 port: 8080,
14829 };
14830 let c = caixa_aplicacao_with_entrada(Some(authored.clone()));
14831 let view = c.aplicacao_view().unwrap();
14832 assert_eq!(
14833 view.entrada(),
14834 Some(&authored),
14835 "Caixa::aplicacao_view must fold the authored :entrada \
14836 composite through the accessor verbatim onto the \
14837 projected AplicacaoSpec — a future silent detour at the \
14838 seed's fold arm would surface here as a projected- \
14839 composite drift (got {:?})",
14840 view.entrada(),
14841 );
14842 let c = caixa_aplicacao_with_entrada(None);
14843 let view = c.aplicacao_view().unwrap();
14844 assert!(
14845 view.entrada().is_none(),
14846 "Caixa::aplicacao_view must fold None through the \
14847 accessor's Option::cloned onto None — the author- \
14848 omitted arm must route through the accessor's None-return \
14849 unchanged (got {:?})",
14850 view.entrada(),
14851 );
14852 }
14853
14854 #[test]
14855 fn entrada_projects_option_ref_by_borrow() {
14856 // The by-borrow pin: [`Caixa::entrada`] returns
14857 // `Option<&Entrada>` by borrow — the returned reference
14858 // borrows the underlying `Option<Entrada>` storage of the
14859 // `:entrada` slot and the accessor must not clone the backing
14860 // composite on every call. Peer of the sibling
14861 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14862 // `behavior_projects_option_ref_by_borrow` (35d8b52),
14863 // `politicas_projects_option_ref_by_borrow` (5d23d29), and
14864 // `placement_projects_option_ref_by_borrow` (4fb8074) by-
14865 // borrow pins on the outer top-level [`Caixa`]
14866 // `Option<&Composite>`-return sub-family — extended here to
14867 // the fifth and final axis of the same sub-family, closing
14868 // the discipline: the accessor's returned reference must
14869 // borrow from `&self` (the returned reference's lifetime is
14870 // tied to `&self`), and calling the accessor twice on the
14871 // same [`Caixa`] must yield references that are pointer-equal
14872 // (the underlying byte-buffer is the storage `Entrada`'s
14873 // allocation, not a fresh copy) as well as value-equal
14874 // (idempotent, no side effects on `&self`).
14875 //
14876 // Pins against a future silent detour that returned an owned
14877 // `Entrada` (which would type-check via the `Clone` impl but
14878 // silently clone on every call), a `&Entrada` panic-return on
14879 // the `None` arm (which would collapse the load-bearing
14880 // `Option` presence-bit into a runtime panic), or a one-arm-
14881 // only accessor that returned a saturating composite on some
14882 // sentinel input.
14883 use crate::aplicacao::Entrada;
14884 for entrada in [
14885 Some(Entrada {
14886 host: "checkout.quero.cloud".into(),
14887 para: "gateway".into(),
14888 paths: Vec::new(),
14889 port: crate::DEFAULT_SERVICO_PORT,
14890 }),
14891 Some(Entrada {
14892 host: "api.pleme.io".into(),
14893 para: "public-api".into(),
14894 paths: vec!["/v1".into(), "/v2".into()],
14895 port: 8080,
14896 }),
14897 ] {
14898 let c = caixa_aplicacao_with_entrada(entrada.clone());
14899 let first = c.entrada().unwrap();
14900 let second = c.entrada().unwrap();
14901 assert_eq!(
14902 first, second,
14903 "Caixa::entrada must be idempotent — two successive \
14904 calls on the same &self must return the same &Entrada",
14905 );
14906 assert!(
14907 std::ptr::eq(first, second),
14908 "Caixa::entrada must borrow the underlying \
14909 Option<Entrada> storage — two successive calls must \
14910 return references with the same backing pointer (a \
14911 fresh Entrada clone would change the pointer on every \
14912 call)",
14913 );
14914 assert_eq!(
14915 Some(first),
14916 entrada.as_ref(),
14917 "Caixa::entrada must return :entrada verbatim by \
14918 borrow — got {first:?}, expected {:?}",
14919 entrada.as_ref(),
14920 );
14921 }
14922 let c = caixa_aplicacao_with_entrada(None);
14923 assert!(
14924 c.entrada().is_none(),
14925 "Caixa::entrada must return None when :entrada is absent \
14926 — the author-omitted arm must project through the \
14927 accessor's Option::None unchanged",
14928 );
14929 }
14930
14931 // ── Caixa::estrategia — outer top-level Option<RestartStrategy> flat-spread supervisor-tree accessor ──
14932
14933 fn caixa_with_estrategia(estrategia: Option<crate::supervisor::RestartStrategy>) -> Caixa {
14934 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14935 c.estrategia = estrategia;
14936 c
14937 }
14938
14939 #[test]
14940 fn estrategia_returns_estrategia_option_verbatim_across_permutations() {
14941 // The canonical per-`Caixa` `:estrategia` M2 supervisor-tree-slot
14942 // flat-spread `Option<RestartStrategy>`-return `Copy`-composite-
14943 // enum-arm scalar shape pin: [`Caixa::estrategia`] must return
14944 // the `:estrategia` typed `Option<crate::supervisor::RestartStrategy>`
14945 // verbatim as an `Option<RestartStrategy>` `Copy`-projected value
14946 // over the same discriminant the raw `self.estrategia` field
14947 // access carries, byte-equal across every representative fixture
14948 // in the accept-set — the author-omitted `None` shape (the
14949 // "defer to [`RestartStrategy::default`] through the
14950 // [`Self::supervisor_view`] `unwrap_or_default()` fold" partition
14951 // every non-`Supervisor`-kind `defcaixa` carries by
14952 // `#[serde(default)]`), and each of the four closed-set variants
14953 // [`RestartStrategy::OneForOne`] / [`RestartStrategy::OneForAll`]
14954 // / [`RestartStrategy::RestForOne`] /
14955 // [`RestartStrategy::SimpleOneForOne`] the author-declared arm
14956 // partitions on.
14957 //
14958 // Pins against a future silent detour that re-derived the
14959 // strategy from a peer axis (an accidental fallback to
14960 // `if children.is_empty() { SimpleOneForOne } else { OneForOne }`
14961 // collapse that read the outer `:children` list-length axis into
14962 // the strategy discriminator at the accessor boundary), a
14963 // stale-derive detour that substituted [`RestartStrategy::default`]
14964 // when the outer `Option` held `None` (which would silently
14965 // collapse the load-bearing "author explicitly declared
14966 // `:estrategia OneForOne`" vs "author omitted the slot and
14967 // inherited the default" partition the [`Self::declared_supervisor_slots`]
14968 // presence-probe reads — the enumerator gate would still push
14969 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` on the omitted arm, silently
14970 // splitting the paired [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
14971 // kind-coherence gate's traversal head from the
14972 // [`Self::supervisor_view`] `unwrap_or_default()` fold's
14973 // composition head), a reference to an operator-resolved overlay
14974 // (the future per-cluster `:estrategia-overrides` slot — its
14975 // resolution must land at exactly this accessor body, not
14976 // silently divert the raw slot away from a second consumer), or
14977 // an axis-remap projection (a future detour that mapped
14978 // `OneForAll` through the accessor onto `OneForOne` would
14979 // silently split every downstream sibling-restart-strategy
14980 // consumer's per-arm fan-out).
14981 //
14982 // First outer top-level [`Caixa`] `Option<Copy>`-return
14983 // supervisor-tree-slot flat-spread accessor pin on the substrate
14984 // primitive — opens the outer-`Caixa` `Option<Copy>` flat-spread
14985 // projection pattern the sibling per-`Caixa` `:max-restarts` /
14986 // `:restart-window` future outer-scalar pins fold on. Peer of
14987 // the inner-altitude
14988 // `supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14989 // (eafb619) pin on the post-composition [`SupervisorSpec`]
14990 // altitude — same "the substrate-primitive accessor must byte-
14991 // equal the raw field access verbatim across every author-
14992 // declared value" discipline extended onto the pre-composition
14993 // outer author-surface [`Caixa`] altitude. Peer of the closed
14994 // outer-`Caixa` `Option<&Composite>` composite-reference family
14995 // the sibling `limits` / `behavior` / `politicas` / `placement` /
14996 // `entrada`
14997 // `..._returns_..._option_ref_verbatim_across_permutations` pins
14998 // already carry on the outer `Option<&Composite>` altitude.
14999 use crate::supervisor::RestartStrategy;
15000 let fixtures: Vec<Option<RestartStrategy>> = vec![
15001 None,
15002 Some(RestartStrategy::OneForOne),
15003 Some(RestartStrategy::OneForAll),
15004 Some(RestartStrategy::RestForOne),
15005 Some(RestartStrategy::SimpleOneForOne),
15006 ];
15007 for estrategia in fixtures {
15008 let c = caixa_with_estrategia(estrategia);
15009 assert_eq!(
15010 c.estrategia(),
15011 estrategia,
15012 "Caixa::estrategia must return :estrategia verbatim (got \
15013 {:?}, expected {:?})",
15014 c.estrategia(),
15015 estrategia,
15016 );
15017 assert_eq!(
15018 c.estrategia(),
15019 c.estrategia,
15020 "Caixa::estrategia accessor and self.estrategia field \
15021 access must byte-equal — the accessor is the substrate-\
15022 primitive typed dispatch every downstream supervisor-\
15023 tree flat-spread consumer must route through, and a \
15024 discriminant split would silently break every consumer \
15025 that relied on the accessor sharing the field's own \
15026 Option<Copy> shape",
15027 );
15028 assert_eq!(
15029 c.estrategia().is_some(),
15030 c.estrategia.is_some(),
15031 "Caixa::estrategia().is_some() must byte-equal \
15032 self.estrategia.is_some() — a presence-bit drift would \
15033 silently split the paired Caixa::declared_supervisor_slots \
15034 presence-probe arm from the Caixa::supervisor_view \
15035 unwrap_or_default() fold's composition input",
15036 );
15037 }
15038 }
15039
15040 #[test]
15041 fn declared_supervisor_slots_estrategia_arm_routes_through_accessor() {
15042 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15043 // `:estrategia` presence-probe arm must key off
15044 // [`Caixa::estrategia`], not the raw `self.estrategia.is_some()`
15045 // field-probe. Structurally: every `Caixa { estrategia:
15046 // Some(RestartStrategy::_), .. }` variant must push
15047 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` onto the declared-slot list
15048 // (the presence bit is `Some` for every closed-set variant, so
15049 // the M2 supervisor-tree kind-coherence gate must surface the
15050 // slot as "declared" regardless of which variant the author
15051 // picked), and a `Caixa { estrategia: None, .. }` must NOT push
15052 // the label (the "author omitted the slot entirely, deferring
15053 // to [`RestartStrategy::default`] through the supervisor_view
15054 // fold" partition). The pair jointly pins the accessor +
15055 // declared-slot enumerator composition: any future silent detour
15056 // that had the accessor collapse `Some(RestartStrategy::default())`
15057 // to `None` (a `.filter(|e| *e != RestartStrategy::default())`
15058 // projection) would silently absorb the "declared but default-
15059 // valued" arm at the accessor boundary and the
15060 // [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
15061 // coherence gate would silently accept a struct-literal `Caixa`
15062 // carrying the drift.
15063 //
15064 // Peer of the sibling per-`Caixa`
15065 // `declared_servico_slots_limits_arm_routes_through_accessor`
15066 // (b2bd9d7) accessor-composition pin on the sibling outer-`Caixa`
15067 // `Option<&LimitsSpec>` composition axis — same "the enumerator
15068 // gate must route through the substrate-primitive typed
15069 // dispatch" discipline extended onto the flat-spread M2
15070 // supervisor-tree `Option<RestartStrategy>`-composition surface,
15071 // opening the outer-`Caixa` supervisor-tree-slot arm of the
15072 // composition-pin family.
15073 use crate::supervisor::RestartStrategy;
15074 for estrategia in [
15075 RestartStrategy::OneForOne,
15076 RestartStrategy::OneForAll,
15077 RestartStrategy::RestForOne,
15078 RestartStrategy::SimpleOneForOne,
15079 ] {
15080 let c = caixa_with_estrategia(Some(estrategia));
15081 let slots = c.declared_supervisor_slots();
15082 assert!(
15083 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
15084 "declared_supervisor_slots must push \
15085 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is \
15086 Some({estrategia:?}) — the accessor and the enumerator \
15087 gate must route through the same substrate-primitive \
15088 typed dispatch on the outer :estrategia presence bit \
15089 (got slots={slots:?})",
15090 );
15091 }
15092 let c = caixa_with_estrategia(None);
15093 let slots = c.declared_supervisor_slots();
15094 assert!(
15095 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
15096 "declared_supervisor_slots must NOT push \
15097 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is None \
15098 — the author-omitted arm must route through the accessor's \
15099 None-return unchanged (got slots={slots:?})",
15100 );
15101 }
15102
15103 #[test]
15104 fn supervisor_view_estrategia_arm_routes_through_accessor() {
15105 // Composition pin: [`Caixa::supervisor_view`]'s per-`:estrategia`
15106 // [`SupervisorSpec`] construction arm must key off
15107 // [`Caixa::estrategia`]'s `unwrap_or_default()` fold, not the raw
15108 // `self.estrategia.unwrap_or_default()` field-fold. Structurally:
15109 // for every `:kind Supervisor` `Caixa` carrying an author-
15110 // declared `Some(RestartStrategy::_)` variant, the composed
15111 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal the
15112 // outer accessor's declared variant unchanged; and for a
15113 // `:kind Supervisor` `Caixa` carrying `None`, the composed
15114 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal
15115 // [`RestartStrategy::default`] (the [`RestartStrategy::OneForOne`]
15116 // arm the flat-spread `unwrap_or_default()` fold projects to on
15117 // the author-omitted arm — this is the *composition* between the
15118 // outer `Option<RestartStrategy>` accessor's presence-bit
15119 // surface and the inner post-composition non-`Option`
15120 // [`SupervisorSpec::estrategia`] altitude). The pair jointly
15121 // pins the accessor + supervisor_view composition: any future
15122 // silent detour that had the accessor promote `None` to
15123 // `Some(RestartStrategy::default())` (a `.or_else(|| Some(RestartStrategy::default()))`
15124 // projection) would silently collapse the two arms into one at
15125 // the accessor boundary and the [`Self::declared_supervisor_slots`]
15126 // presence probe would silently drift from the composition site.
15127 //
15128 // Peer of the sibling M2 supervisor-slot post-composition
15129 // `validate_reads_through_lifted_estrategia_accessor` (eafb619)
15130 // pin on the [`SupervisorSpec::validate`] altitude — this pin
15131 // extends that inner-altitude accessor-routing discipline onto
15132 // the pre-composition outer author-surface [`Caixa`] altitude,
15133 // pinning the composition edge between the flat-spread outer
15134 // `Option<RestartStrategy>` and the composed [`SupervisorSpec`]
15135 // `RestartStrategy` axes.
15136 use crate::CaixaKind;
15137 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
15138 for estrategia in [
15139 RestartStrategy::OneForOne,
15140 RestartStrategy::OneForAll,
15141 RestartStrategy::RestForOne,
15142 RestartStrategy::SimpleOneForOne,
15143 ] {
15144 let mut c = caixa_with_estrategia(Some(estrategia));
15145 c.kind = CaixaKind::Supervisor;
15146 // Route the `SimpleOneForOne ↔ non-SimpleOneForOne` fixture-
15147 // shape partition through the [`gen_platform::IsVariant`]
15148 // derive-generated
15149 // [`RestartStrategy::is_simple_one_for_one`] predicate rather
15150 // than the raw `matches!(estrategia, RestartStrategy::
15151 // SimpleOneForOne)` open-coded pattern-match — same closed-
15152 // set-typed-enum arm-discriminator dispatch discipline the
15153 // sibling [`crate::upgrade::UpgradeInstruction::is_restart`]
15154 // convergence (915a934) extended onto its two paired positive
15155 // / negated `matches!` sites and the peer
15156 // [`crate::aplicacao::PlacementStrategy`] `IsVariant`-derived
15157 // predicate convergence (766ec63) extended onto the M3 mesh-
15158 // slot per-`:placement` distribution-strategy discriminator
15159 // axis. See the sibling `supervisor::tests::
15160 // round_trip_all_strategies` and
15161 // `supervisor::tests::supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
15162 // fixtures — the three sites (all test-only,
15163 // acknowledged in 915a934's Prior-commits footnote as the
15164 // outstanding follow-up) now consult one typed dispatch on
15165 // the substrate primitive.
15166 c.children = if estrategia.is_simple_one_for_one() {
15167 Vec::new()
15168 } else {
15169 vec![ChildSpec {
15170 caixa: "worker".into(),
15171 versao: "^0.1".into(),
15172 restart: RestartPolicy::Permanent,
15173 }]
15174 };
15175 let view = c.supervisor_view().expect(
15176 "supervisor_view must materialize a SupervisorSpec for a \
15177 :kind Supervisor Caixa carrying a Some(:estrategia) slot",
15178 );
15179 assert_eq!(
15180 view.estrategia(),
15181 c.estrategia().unwrap(),
15182 "supervisor_view must carry the outer Caixa::estrategia() \
15183 declared variant onto the composed SupervisorSpec.estrategia \
15184 field verbatim on the Some arm (got {:?}, expected {:?})",
15185 view.estrategia(),
15186 c.estrategia().unwrap(),
15187 );
15188 }
15189 // The author-omitted arm: outer `None` → composed
15190 // `RestartStrategy::default()` through the flat-spread
15191 // `unwrap_or_default()` fold.
15192 let mut c = caixa_with_estrategia(None);
15193 c.kind = CaixaKind::Supervisor;
15194 // Populate children so the sibling supervisor slots are coherent
15195 // for the [`Self::supervisor_view`] projection; the `:estrategia`
15196 // arm still defers to [`RestartStrategy::default`] on the
15197 // author-omitted arm even when the sibling slots carry values.
15198 c.children = vec![ChildSpec {
15199 caixa: "worker".into(),
15200 versao: "^0.1".into(),
15201 restart: RestartPolicy::Permanent,
15202 }];
15203 let view = c.supervisor_view().expect(
15204 "supervisor_view must materialize a SupervisorSpec for a \
15205 :kind Supervisor Caixa carrying a None `:estrategia` slot",
15206 );
15207 assert_eq!(
15208 view.estrategia(),
15209 RestartStrategy::default(),
15210 "supervisor_view must project the outer Caixa::estrategia() \
15211 None arm onto RestartStrategy::default() through the flat-\
15212 spread unwrap_or_default() fold (got {:?}, expected {:?})",
15213 view.estrategia(),
15214 RestartStrategy::default(),
15215 );
15216 assert!(
15217 c.estrategia().is_none(),
15218 "Caixa::estrategia() must remain None on the author-omitted \
15219 arm — the supervisor_view fold must not mutate the outer \
15220 flat-spread presence bit",
15221 );
15222 }
15223
15224 #[test]
15225 fn estrategia_projects_option_by_copy() {
15226 // The by-`Copy` pin: [`Caixa::estrategia`] returns
15227 // `Option<RestartStrategy>` by value (`RestartStrategy: Copy`) —
15228 // the accessor does not borrow `&self` past the call (no
15229 // lifetime on the return type), and calling the accessor twice
15230 // on the same [`Caixa`] must yield discriminant-equal values
15231 // (idempotent, no side effects on `&self`). Peer of the sibling
15232 // outer-`Caixa` `Option<&Composite>` by-borrow
15233 // `limits_projects_option_ref_by_borrow` (b2bd9d7) /
15234 // `behavior_projects_option_ref_by_borrow` (35d8b52) /
15235 // `politicas_projects_option_ref_by_borrow` (5d23d29) /
15236 // `placement_projects_option_ref_by_borrow` (4fb8074) /
15237 // `entrada_projects_option_ref_by_borrow` (e4128e4) by-borrow
15238 // pins on the outer-`Caixa` `Option<&Composite>`-return axes —
15239 // extended here to the outer-`Caixa` `Option<Copy>`-return
15240 // flat-spread axis. The `Copy` discipline replaces the pointer-
15241 // equality claim the by-borrow siblings pin (a fresh `Copy` of a
15242 // `Copy` discriminant is definitionally the same discriminant, so
15243 // the axis reduces to discriminant equality).
15244 //
15245 // Pins against a future silent detour that returned a fresh
15246 // `Option<&RestartStrategy>` (which would type-check but silently
15247 // introduce a borrow of `&self` past the call, collapsing the
15248 // load-bearing "no lifetime on the return type" `Copy` projection
15249 // the flat-spread axis's `Option<Copy>` shape carries), a stale-
15250 // read side effect that flipped the outer discriminant on
15251 // successive calls, or an axis-remap projection that returned a
15252 // different variant than the field storage.
15253 use crate::supervisor::RestartStrategy;
15254 for estrategia in [
15255 Some(RestartStrategy::OneForOne),
15256 Some(RestartStrategy::OneForAll),
15257 Some(RestartStrategy::RestForOne),
15258 Some(RestartStrategy::SimpleOneForOne),
15259 ] {
15260 let c = caixa_with_estrategia(estrategia);
15261 let first = c.estrategia();
15262 let second = c.estrategia();
15263 assert_eq!(
15264 first, second,
15265 "Caixa::estrategia must be idempotent — two successive \
15266 calls on the same &self must return the same \
15267 Option<RestartStrategy>",
15268 );
15269 assert_eq!(
15270 first, estrategia,
15271 "Caixa::estrategia must return :estrategia verbatim by \
15272 Copy — got {first:?}, expected {estrategia:?}",
15273 );
15274 }
15275 let c = caixa_with_estrategia(None);
15276 assert!(
15277 c.estrategia().is_none(),
15278 "Caixa::estrategia must return None when :estrategia is \
15279 absent — the author-omitted arm must project through the \
15280 accessor's Option::None unchanged",
15281 );
15282 }
15283
15284 // ── Caixa::max_restarts / Caixa::restart_window —
15285 // outer top-level M2 supervisor-tree-slot flat-spread accessors
15286 // (Option<u32> / Option<&str>) folding on the ed04d3c
15287 // Caixa::estrategia Option<Copy> sub-family ─────────────────────
15288
15289 fn caixa_with_max_restarts(max_restarts: Option<u32>) -> Caixa {
15290 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15291 c.max_restarts = max_restarts;
15292 c
15293 }
15294
15295 fn caixa_supervisor_with_max_restarts_and_window(
15296 max_restarts: Option<u32>,
15297 restart_window: Option<&str>,
15298 ) -> Caixa {
15299 use crate::CaixaKind;
15300 use crate::supervisor::{ChildSpec, RestartPolicy};
15301 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
15302 c.kind = CaixaKind::Supervisor;
15303 c.max_restarts = max_restarts;
15304 c.restart_window = restart_window.map(str::to_string);
15305 c.children = vec![ChildSpec {
15306 caixa: "worker".into(),
15307 versao: "^0.1".into(),
15308 restart: RestartPolicy::Permanent,
15309 }];
15310 c
15311 }
15312
15313 #[test]
15314 fn max_restarts_returns_max_restarts_option_verbatim_across_permutations() {
15315 // Value-shape pin: [`Caixa::max_restarts`] returns the
15316 // `:max-restarts` typed `Option<u32>` verbatim, `Copy`-projected
15317 // from the typed slot's own storage, byte-equal across the
15318 // author-omitted `None` arm (the "defer to the
15319 // [`Self::supervisor_view`] `unwrap_or(5)` OTP-canonical
15320 // `{intensity, 5, 60}` default" partition every
15321 // non-`Supervisor`-kind caixa carries by `#[serde(default)]`)
15322 // and each of the representative fixtures in the accept-set —
15323 // `0` (the zero-floor arm the peer
15324 // [`crate::supervisor::SupervisorSpec::validate`]
15325 // [`crate::SupervisorError::ZeroMaxRestarts`] gate refuses on
15326 // the post-composition altitude — the accessor must ship the
15327 // raw slot verbatim so struct-literal fixtures continue to
15328 // expose the zero at the accessor boundary), the OTP-canonical
15329 // `5` default (`{intensity, 5, 60}` worker-supervisor from
15330 // Learn You Some Erlang), `1000` (the
15331 // [`SUPERVISOR_MAX_RESTARTS_MAX`] cap the peer post-composition
15332 // upper-bound gate accepts on the boundary), `u32::MAX` (a
15333 // past-the-cap sentinel that the substrate-primitive accessor
15334 // must still ship verbatim). Second outer top-level
15335 // [`Caixa`] `Option<Copy>`-return supervisor-tree flat-spread
15336 // pin — folds on the sibling
15337 // `estrategia_returns_estrategia_option_verbatim_across_permutations`
15338 // (ed04d3c) pin's `Option<Copy>` shape, extending the sub-family
15339 // onto the sibling `Option<u32>` restart-budget-count arm.
15340 let fixtures: Vec<Option<u32>> = vec![None, Some(0), Some(5), Some(1000), Some(u32::MAX)];
15341 for max_restarts in fixtures {
15342 let c = caixa_with_max_restarts(max_restarts);
15343 assert_eq!(
15344 c.max_restarts(),
15345 max_restarts,
15346 "Caixa::max_restarts must return :max-restarts verbatim \
15347 (got {:?}, expected {max_restarts:?})",
15348 c.max_restarts(),
15349 );
15350 assert_eq!(
15351 c.max_restarts(),
15352 c.max_restarts,
15353 "Caixa::max_restarts accessor and self.max_restarts \
15354 field access must byte-equal — a presence-bit or count \
15355 drift would silently split the paired \
15356 Caixa::declared_supervisor_slots presence-probe arm \
15357 from the Caixa::supervisor_view unwrap_or(5) fold's \
15358 composition input",
15359 );
15360 }
15361 }
15362
15363 #[test]
15364 fn max_restarts_projects_option_by_copy() {
15365 // The by-`Copy` pin: [`Caixa::max_restarts`] returns
15366 // `Option<u32>` by value (`u32: Copy`) — the accessor does not
15367 // borrow `&self` past the call (no lifetime on the return type),
15368 // and calling the accessor twice on the same [`Caixa`] must
15369 // yield equal values (idempotent, no side effects). Peer of the
15370 // sibling `estrategia_projects_option_by_copy` (ed04d3c) pin on
15371 // the outer-`Caixa` `Option<Copy>`-return flat-spread axis.
15372 for max_restarts in [Some(0u32), Some(5), Some(1000), Some(u32::MAX), None] {
15373 let c = caixa_with_max_restarts(max_restarts);
15374 let first = c.max_restarts();
15375 let second = c.max_restarts();
15376 assert_eq!(
15377 first, second,
15378 "Caixa::max_restarts must be idempotent — two successive \
15379 calls on the same &self must return the same Option<u32>",
15380 );
15381 assert_eq!(
15382 first, max_restarts,
15383 "Caixa::max_restarts must return :max-restarts verbatim \
15384 by Copy — got {first:?}, expected {max_restarts:?}",
15385 );
15386 }
15387 }
15388
15389 #[test]
15390 fn declared_supervisor_slots_max_restarts_arm_routes_through_accessor() {
15391 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15392 // `:max-restarts` presence-probe arm must key off
15393 // [`Caixa::max_restarts`], not the raw
15394 // `self.max_restarts.is_some()` field-probe. Structurally: every
15395 // `Caixa { max_restarts: Some(_), .. }` variant must push
15396 // `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS` onto the declared-slot
15397 // list (the presence bit is `Some` for every representative
15398 // count, so the M2 kind-coherence gate must surface the slot as
15399 // "declared"), and a `Caixa { max_restarts: None, .. }` must
15400 // NOT push the label. Peer of the sibling
15401 // `declared_supervisor_slots_estrategia_arm_routes_through_accessor`
15402 // (ed04d3c) composition pin — same routing-through-accessor
15403 // discipline extended onto the sibling flat-spread `Option<u32>`
15404 // arm.
15405 for max_restarts in [0u32, 5, 1000, u32::MAX] {
15406 let c = caixa_with_max_restarts(Some(max_restarts));
15407 let slots = c.declared_supervisor_slots();
15408 assert!(
15409 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15410 "declared_supervisor_slots must push \
15411 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` \
15412 is Some({max_restarts}) — the accessor and the \
15413 enumerator gate must route through the same \
15414 substrate-primitive typed dispatch on the outer \
15415 :max-restarts presence bit (got slots={slots:?})",
15416 );
15417 }
15418 let c = caixa_with_max_restarts(None);
15419 let slots = c.declared_supervisor_slots();
15420 assert!(
15421 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15422 "declared_supervisor_slots must NOT push \
15423 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` is \
15424 None — the author-omitted arm must route through the \
15425 accessor's None-return unchanged (got slots={slots:?})",
15426 );
15427 }
15428
15429 #[test]
15430 fn supervisor_view_max_restarts_arm_routes_through_accessor() {
15431 // Composition pin: [`Caixa::supervisor_view`]'s per-`:max-restarts`
15432 // [`SupervisorSpec`] construction arm must key off
15433 // [`Caixa::max_restarts`]'s `unwrap_or(5)` fold, not the raw
15434 // `self.max_restarts.unwrap_or(5)` field-fold. Structurally: for
15435 // every `:kind Supervisor` `Caixa` carrying an author-declared
15436 // `Some(n)`, the composed [`SupervisorSpec`]'s `.max_restarts()`
15437 // must byte-equal `n`; and for a `:kind Supervisor` `Caixa`
15438 // carrying `None`, the composed [`SupervisorSpec`]'s
15439 // `.max_restarts()` must byte-equal the OTP-canonical `5`. Peer
15440 // of the sibling
15441 // `supervisor_view_estrategia_arm_routes_through_accessor`
15442 // (ed04d3c) composition pin.
15443 for max_restarts in [1u32, 5, 1000] {
15444 let c = caixa_supervisor_with_max_restarts_and_window(Some(max_restarts), None);
15445 let view = c.supervisor_view().expect(
15446 "supervisor_view must materialize a SupervisorSpec for a \
15447 :kind Supervisor Caixa carrying a Some(:max-restarts)",
15448 );
15449 assert_eq!(
15450 view.max_restarts(),
15451 max_restarts,
15452 "supervisor_view must carry the outer \
15453 Caixa::max_restarts() Some arm onto the composed \
15454 SupervisorSpec.max_restarts field verbatim (got {}, \
15455 expected {max_restarts})",
15456 view.max_restarts(),
15457 );
15458 }
15459 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15460 let view = c.supervisor_view().expect(
15461 "supervisor_view must materialize a SupervisorSpec for a \
15462 :kind Supervisor Caixa carrying a None :max-restarts",
15463 );
15464 assert_eq!(
15465 view.max_restarts(),
15466 5,
15467 "supervisor_view must project the outer \
15468 Caixa::max_restarts() None arm onto the OTP-canonical \
15469 {{intensity, 5, 60}} default (5) through the flat-spread \
15470 unwrap_or(5) fold (got {})",
15471 view.max_restarts(),
15472 );
15473 assert!(
15474 c.max_restarts().is_none(),
15475 "Caixa::max_restarts() must remain None on the author-\
15476 omitted arm — the supervisor_view fold must not mutate \
15477 the outer flat-spread presence bit",
15478 );
15479 }
15480
15481 #[test]
15482 fn restart_window_returns_restart_window_option_verbatim_across_permutations() {
15483 // Value-shape pin: [`Caixa::restart_window`] returns the
15484 // `:restart-window` typed `Option<String>` verbatim as an
15485 // `Option<&str>`, borrowed from the typed slot's own storage,
15486 // byte-equal across the author-omitted `None` arm and each of
15487 // the representative fixtures in the accept-set — the canonical
15488 // `"60s"` from `{intensity, 5, 60}`, the sibling
15489 // canonical-magnitude forms (`"5m"` / `"1h"` / `"500ms"` / `"30"`
15490 // / `"0s"`) the shared codec's positive-set sweep pin covers,
15491 // plus a past-the-guard sentinel (`"1.5s"` — the fractional-
15492 // seconds drift the sibling [`Self::validate_restart_window`]
15493 // gate refuses; the accessor must ship the raw slot verbatim
15494 // so struct-literal fixtures continue to expose the drift at
15495 // the accessor boundary). Third outer top-level [`Caixa`]
15496 // supervisor-tree flat-spread pin — extends the sub-family onto
15497 // the sibling `Option<&str>` raw-duration-string arm.
15498 for window in [
15499 None,
15500 Some("60s"),
15501 Some("5m"),
15502 Some("1h"),
15503 Some("500ms"),
15504 Some("1.5s"),
15505 Some(""),
15506 ] {
15507 let c = caixa_with_restart_window(window);
15508 assert_eq!(
15509 c.restart_window(),
15510 window,
15511 "Caixa::restart_window must return :restart-window \
15512 verbatim as Option<&str> (got {:?}, expected {window:?})",
15513 c.restart_window(),
15514 );
15515 assert_eq!(
15516 c.restart_window(),
15517 c.restart_window.as_deref(),
15518 "Caixa::restart_window accessor and \
15519 self.restart_window.as_deref() field access must \
15520 byte-equal — a byte-level drift would silently split \
15521 the paired Caixa::declared_supervisor_slots \
15522 presence-probe arm from the \
15523 Caixa::validate_restart_window shared-codec gate and \
15524 the Caixa::supervisor_view soft-swallowing fold",
15525 );
15526 }
15527 }
15528
15529 #[test]
15530 fn restart_window_projects_slice_by_borrow() {
15531 // The by-borrow pin: [`Caixa::restart_window`] returns
15532 // `Option<&str>` by borrow — the returned string slice borrows
15533 // the underlying `Option<String>` storage of the `:restart-window`
15534 // slot and the accessor must not clone on every call. Peer of
15535 // the sibling outer top-level [`Caixa`] `Option<&str>`-return
15536 // by-borrow pins on the universal-axis scalar family
15537 // (`licenca_projects_option_ref_by_borrow` /
15538 // `descricao_projects_option_ref_by_borrow` and siblings) —
15539 // extended onto the M2 supervisor-tree flat-spread
15540 // `Option<&str>` raw-duration-string axis.
15541 for window in [None, Some("60s"), Some("5m"), Some("")] {
15542 let c = caixa_with_restart_window(window);
15543 let first = c.restart_window();
15544 let second = c.restart_window();
15545 assert_eq!(
15546 first, second,
15547 "Caixa::restart_window must be idempotent — two \
15548 successive calls on the same &self must return the \
15549 same Option<&str>",
15550 );
15551 if let (Some(a), Some(b)) = (first, second) {
15552 assert_eq!(
15553 a.as_ptr(),
15554 b.as_ptr(),
15555 "Caixa::restart_window must borrow the underlying \
15556 String storage — two successive Some-arm calls must \
15557 return slices with the same backing pointer (a fresh \
15558 String clone would change the pointer on every call)",
15559 );
15560 }
15561 assert_eq!(
15562 first, window,
15563 "Caixa::restart_window must return :restart-window \
15564 verbatim by borrow — got {first:?}, expected {window:?}",
15565 );
15566 }
15567 }
15568
15569 #[test]
15570 fn declared_supervisor_slots_restart_window_arm_routes_through_accessor() {
15571 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15572 // `:restart-window` presence-probe arm must key off
15573 // [`Caixa::restart_window`], not the raw
15574 // `self.restart_window.is_some()` field-probe. Structurally:
15575 // every `Caixa { restart_window: Some(_), .. }` must push
15576 // `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` onto the declared-slot
15577 // list, and a `Caixa { restart_window: None, .. }` must NOT
15578 // push the label. Peer of the sibling
15579 // `declared_supervisor_slots_max_restarts_arm_routes_through_accessor`
15580 // routing pin.
15581 for window in ["60s", "5m", "1h", "500ms", "1.5s", ""] {
15582 let c = caixa_with_restart_window(Some(window));
15583 let slots = c.declared_supervisor_slots();
15584 assert!(
15585 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15586 "declared_supervisor_slots must push \
15587 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when \
15588 `:restart-window` is Some({window:?}) — the accessor \
15589 and the enumerator gate must route through the same \
15590 substrate-primitive typed dispatch on the outer \
15591 :restart-window presence bit (got slots={slots:?})",
15592 );
15593 }
15594 let c = caixa_with_restart_window(None);
15595 let slots = c.declared_supervisor_slots();
15596 assert!(
15597 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15598 "declared_supervisor_slots must NOT push \
15599 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when `:restart-window` \
15600 is None — the author-omitted arm must route through the \
15601 accessor's None-return unchanged (got slots={slots:?})",
15602 );
15603 }
15604
15605 #[test]
15606 fn validate_restart_window_arm_routes_through_accessor() {
15607 // Composition pin: [`Caixa::validate_restart_window`]'s
15608 // shared-codec fold arm must key off [`Caixa::restart_window`],
15609 // not the raw `self.restart_window.as_deref()` field-projection.
15610 // Structurally: (1) `None` → `Ok(())` (the "omit the slot to
15611 // express no reset" canonical shape); (2) a canonical `Some`
15612 // arm (`"60s"`) → `Ok(())`; (3) a codec-rejected `Some` arm
15613 // (`"1.5s"`) → `Err(RestartWindowMalformed { restart_window,
15614 // .. })` carrying the offending raw string verbatim. The three
15615 // arms jointly pin that the validator's raw-string binding is
15616 // the accessor's return, not a peer projection — any future
15617 // silent detour that had the accessor collapse `Some("")` to
15618 // `None` would silently absorb the empty-after-trim refusal
15619 // case at the accessor boundary.
15620 caixa_with_restart_window(None)
15621 .validate_restart_window()
15622 .expect("None :restart-window must validate through the accessor");
15623 caixa_with_restart_window(Some("60s"))
15624 .validate_restart_window()
15625 .expect("canonical :restart-window \"60s\" must validate through the accessor");
15626 let err = caixa_with_restart_window(Some("1.5s"))
15627 .validate_restart_window()
15628 .expect_err("fractional-seconds :restart-window must fail through the accessor");
15629 assert!(
15630 matches!(
15631 err,
15632 ManifestError::RestartWindowMalformed { ref restart_window, .. }
15633 if restart_window == "1.5s"
15634 ),
15635 "validator must carry the offending raw string verbatim \
15636 from the accessor's borrowed &str (got {err:?})",
15637 );
15638 }
15639
15640 #[test]
15641 fn supervisor_view_restart_window_arm_routes_through_accessor() {
15642 // Composition pin: [`Caixa::supervisor_view`]'s
15643 // per-`:restart-window` [`SupervisorSpec`] construction arm
15644 // must key off [`Caixa::restart_window`]'s soft-swallowing
15645 // `.and_then(|s| duration_codec::parse(s).ok())` fold, not the
15646 // raw `self.restart_window.as_deref().and_then(…)` field-fold.
15647 // Structurally: (1) `None` → `SupervisorSpec.restart_window ==
15648 // None` (the "never reset" sentinel); (2) canonical `Some("60s")`
15649 // → `SupervisorSpec.restart_window == Some(Duration::from_secs(60))`
15650 // (the shared codec's canonical parse); (3) codec-rejected
15651 // `Some("1.5s")` → `SupervisorSpec.restart_window == None`
15652 // (the soft-swallow preserving the view's best-effort shape).
15653 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15654 let view = c.supervisor_view().expect("Supervisor kind has a view");
15655 assert_eq!(
15656 view.restart_window(),
15657 None,
15658 "supervisor_view must project outer None :restart-window \
15659 onto None on the composed SupervisorSpec (never-reset \
15660 sentinel) through the accessor's None-return unchanged",
15661 );
15662
15663 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("60s"));
15664 let view = c.supervisor_view().expect("Supervisor kind has a view");
15665 assert_eq!(
15666 view.restart_window(),
15667 Some(std::time::Duration::from_secs(60)),
15668 "supervisor_view must fold outer Some(\"60s\") through the \
15669 shared duration_codec into Duration::from_secs(60) on the \
15670 composed SupervisorSpec (accessor's Some(&str) → codec \
15671 parse → Some(Duration))",
15672 );
15673
15674 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("1.5s"));
15675 let view = c.supervisor_view().expect("Supervisor kind has a view");
15676 assert_eq!(
15677 view.restart_window(),
15678 None,
15679 "supervisor_view must soft-swallow the shared-codec parse \
15680 failure to None (the view's best-effort shape the sibling \
15681 manifest-level validate_restart_window surfaces as \
15682 RestartWindowMalformed); the accessor's raw-string return \
15683 is the single input every downstream consumer keys off",
15684 );
15685 }
15686
15687 // ── Caixa::upgrade_from — outer top-level &[UpgradeFromEntry] composite-slice accessor ──
15688
15689 fn caixa_with_upgrade_from(upgrade_from: Vec<crate::upgrade::UpgradeFromEntry>) -> Caixa {
15690 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15691 c.upgrade_from = upgrade_from;
15692 c
15693 }
15694
15695 #[test]
15696 fn upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations() {
15697 // The canonical per-`Caixa` `:upgrade-from` M2 typed-slot
15698 // outer-composite `&[UpgradeFromEntry]`-return slice-shape
15699 // pin: [`Caixa::upgrade_from`] must return the `:upgrade-from`
15700 // typed `Vec<UpgradeFromEntry>` verbatim as a
15701 // `&[UpgradeFromEntry]` slice-view over the same backing
15702 // buffer the raw `self.upgrade_from.as_slice()` field access
15703 // borrows from, element-equal across every representative
15704 // fixture in the accept-set — `[]` (the "no hot-upgrade path
15705 // declared" arm every `defcaixa` without an `:upgrade-from`
15706 // block carries; `#[serde(default)]` folds an omitted slot
15707 // onto `Vec::new()`), a canonical single-entry `Restart`
15708 // fixture (the shape most Servicos carry — a single prior
15709 // version with the fallback strategy), a canonical multi-
15710 // entry list carrying every typed instruction variant
15711 // (`LoadModule` / `StateChange` / `SoftPurge` / `Purge` /
15712 // `Restart`), and a past-the-guard sentinel — a duplicate-
15713 // `:from` `[(0.1.0, Restart), (0.1.0, Restart)]` entry pair
15714 // ([`crate::upgrade::validate_upgrade_from`] rejects through
15715 // `DuplicateFrom { from: "0.1.0" }` but the accessor must
15716 // ship the raw slot verbatim so struct-literal fixtures
15717 // continue to expose the duplicate at the accessor boundary).
15718 //
15719 // Pins against a future silent detour that returned an owned
15720 // `Vec<UpgradeFromEntry>` (which would type-check but silently
15721 // clone on every accessor call, breaking the zero-cost
15722 // projection every peer sibling slice accessor carries), a
15723 // `[dup, dup] → [dup]` dedup collapse (which would silently
15724 // absorb the `DuplicateFrom` refusal case at the accessor
15725 // boundary and the [`crate::StandardLayout::verify`] cross-
15726 // entry gate would silently accept a struct-literal `Caixa`
15727 // carrying the drift), a reference to an operator-resolved
15728 // overlay (the future per-cluster `:upgrade-overrides` slot
15729 // — its resolution must land at exactly this accessor body,
15730 // not silently divert the raw slot away from a second
15731 // consumer), or an axis-shuffled projection (a future detour
15732 // that reordered entries through the accessor would silently
15733 // split the paired [`crate::StandardLayout::verify`] per-
15734 // `:upgrade-from` shape gate's traversal input from the peer
15735 // [`crate::render::servico_m2_overlay`] emitter's projection
15736 // input, since the operator's hot-upgrade dispatch matches
15737 // per-`:from` and axis reordering would silently split the
15738 // per-entry script-path existence probe's iteration order
15739 // from the M2 overlay emitter's serialized-entry order).
15740 //
15741 // First outer top-level [`Caixa`] `&[Composite]`-return
15742 // slice accessor pin on the substrate primitive for M2 / M3
15743 // typed-slot vec-carry axes — opens the outer-`Caixa`
15744 // `&[Composite]` composite-slice projection pattern the
15745 // sibling `:children` [`crate::supervisor::ChildSpec`] /
15746 // `:membros` [`crate::aplicacao::Membro`] / `:contratos`
15747 // [`crate::aplicacao::WitContract`] future outer-composite-
15748 // slice pins fold on. Peer of the closed outer-`Caixa`
15749 // scalar `Option<&Composite>` composite-reference family the
15750 // sibling `limits` / `behavior` / `politicas` / `placement`
15751 // / `entrada` `..._returns_..._option_ref_verbatim_across_
15752 // permutations` pins closed (b2bd9d7 → e4128e4) — extends
15753 // the "byte-equal, borrow-shared" outer-accessor discipline
15754 // onto the outer-`Caixa` `&[Composite]` vec-carry altitude.
15755 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15756 let fixtures: Vec<Vec<UpgradeFromEntry>> = vec![
15757 vec![],
15758 vec![UpgradeFromEntry {
15759 from: "0.0.1".into(),
15760 instructions: vec![UpgradeInstruction::Restart],
15761 }],
15762 vec![
15763 UpgradeFromEntry {
15764 from: "0.0.1".into(),
15765 instructions: vec![
15766 UpgradeInstruction::LoadModule {
15767 module: "demo".into(),
15768 },
15769 UpgradeInstruction::SoftPurge {
15770 module: "demo".into(),
15771 },
15772 ],
15773 },
15774 UpgradeFromEntry {
15775 from: "0.0.2".into(),
15776 instructions: vec![
15777 UpgradeInstruction::StateChange {
15778 script: "servicos/upgrade.lisp".into(),
15779 },
15780 UpgradeInstruction::Purge {
15781 module: "demo".into(),
15782 },
15783 UpgradeInstruction::Restart,
15784 ],
15785 },
15786 ],
15787 vec![
15788 UpgradeFromEntry {
15789 from: "0.1.0".into(),
15790 instructions: vec![UpgradeInstruction::Restart],
15791 },
15792 UpgradeFromEntry {
15793 from: "0.1.0".into(),
15794 instructions: vec![UpgradeInstruction::Restart],
15795 },
15796 ],
15797 ];
15798 for upgrade_from in fixtures {
15799 let c = caixa_with_upgrade_from(upgrade_from.clone());
15800 assert_eq!(
15801 c.upgrade_from(),
15802 upgrade_from.as_slice(),
15803 "Caixa::upgrade_from must return :upgrade-from \
15804 verbatim (got {:?}, expected {upgrade_from:?})",
15805 c.upgrade_from(),
15806 );
15807 assert_eq!(
15808 c.upgrade_from(),
15809 c.upgrade_from.as_slice(),
15810 "Caixa::upgrade_from must element-equal the raw \
15811 `self.upgrade_from.as_slice()` field access across \
15812 every value in the Vec<UpgradeFromEntry> accept-set",
15813 );
15814 assert_eq!(
15815 c.upgrade_from().is_empty(),
15816 c.upgrade_from.is_empty(),
15817 "Caixa::upgrade_from().is_empty() must byte-equal \
15818 self.upgrade_from.is_empty() — a presence-bit drift \
15819 would silently split the paired \
15820 Caixa::declared_servico_slots M2 declared-slot \
15821 enumerator's presence probe from the peer \
15822 crate::render::servico_m2_overlay M2 overlay \
15823 emitter's presence gate",
15824 );
15825 }
15826 }
15827
15828 #[test]
15829 fn declared_servico_slots_upgrade_from_arm_routes_through_accessor() {
15830 // Composition pin: [`Caixa::declared_servico_slots`]'s
15831 // `:upgrade-from` presence-probe arm must key off
15832 // [`Caixa::upgrade_from`], not the raw
15833 // `self.upgrade_from.is_empty()` field-probe. Structurally: a
15834 // `Caixa { upgrade_from: vec![UpgradeFromEntry { from: "0.0.1",
15835 // instructions: vec![Restart] }], .. }` must push
15836 // `M2_AUTHOR_KEY_UPGRADE_FROM` onto the declared-slot list
15837 // (the presence bit is non-empty, so the M2 kind-coherence
15838 // gate must surface the slot as "declared"), and a `Caixa {
15839 // upgrade_from: vec![], .. }` must NOT push the label (the
15840 // "author omitted the slot entirely" arm — the empty-slice
15841 // partition the serde-default folds onto). The pair jointly
15842 // pins the accessor + declared-slot enumerator composition:
15843 // any future silent detour that had the accessor collapse
15844 // `[Restart]` to `[]` (a `.filter(|e| !e.instructions.
15845 // is_empty())` projection) would silently absorb the
15846 // "declared but degenerate" arm at the accessor boundary and
15847 // the [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-
15848 // coherence gate would silently accept a struct-literal
15849 // `Caixa` carrying the drift.
15850 //
15851 // Peer of the sibling
15852 // `declared_servico_slots_limits_arm_routes_through_accessor`
15853 // (b2bd9d7) and
15854 // `declared_servico_slots_behavior_arm_routes_through_accessor`
15855 // (35d8b52) composition pins on the sibling `:limits` /
15856 // `:behavior` outer-`Option<&Composite>` arms — same "the
15857 // enumerator gate must route through the substrate-primitive
15858 // typed dispatch" discipline extended onto the third M2
15859 // Servico-runtime slot axis, closing the enumerator's routing
15860 // invariant on every M2 arm.
15861 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15862 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15863 from: "0.0.1".into(),
15864 instructions: vec![UpgradeInstruction::Restart],
15865 }]);
15866 let slots = c.declared_servico_slots();
15867 assert!(
15868 slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15869 "declared_servico_slots must push \
15870 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15871 non-empty — the accessor and the enumerator gate must \
15872 route through the same substrate-primitive typed \
15873 dispatch on the outer :upgrade-from presence bit (got \
15874 slots={slots:?})",
15875 );
15876 let c = caixa_with_upgrade_from(vec![]);
15877 let slots = c.declared_servico_slots();
15878 assert!(
15879 !slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15880 "declared_servico_slots must NOT push \
15881 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15882 empty — the author-omitted arm must route through the \
15883 accessor's empty-slice return unchanged (got \
15884 slots={slots:?})",
15885 );
15886 }
15887
15888 #[test]
15889 fn servico_m2_overlay_upgrade_from_arm_routes_through_accessor() {
15890 // Composition pin: [`crate::render::servico_m2_overlay`]'s
15891 // per-`:upgrade-from` M2 overlay emit arm must key off
15892 // [`Caixa::upgrade_from`], not the raw
15893 // `!caixa.upgrade_from.is_empty()` presence gate + the
15894 // `serde_yaml::to_value(&caixa.upgrade_from)` projection.
15895 // Structurally: a `Caixa { upgrade_from: vec![UpgradeFromEntry
15896 // { from: "0.0.1", instructions: vec![Restart] }], .. }` must
15897 // surface the `M2_KEY_UPGRADE_FROM` key with a per-entry
15898 // sequence in the overlay (the emitter fans onto the serde
15899 // slice-serialization), and a `Caixa { upgrade_from: vec![],
15900 // .. }` must omit the key entirely (the empty-slice
15901 // partition — the `!.is_empty()` outer gate elides the key
15902 // when the author omitted the slot). The pair jointly pins
15903 // the accessor + M2 overlay emitter composition: any future
15904 // silent detour that had the accessor return a fresh-cloned
15905 // `Vec<UpgradeFromEntry>` copy would silently break the
15906 // reference-identity pin the peer per-entry
15907 // `serde_yaml::to_value(caixa.upgrade_from())` projection
15908 // reads from — the projection would clone once per accessor
15909 // call instead of borrowing the storage buffer verbatim.
15910 //
15911 // Peer of the sibling
15912 // `servico_m2_overlay_limits_arm_routes_through_accessor`
15913 // (b2bd9d7) and
15914 // `servico_m2_overlay_behavior_arm_routes_through_accessor`
15915 // (35d8b52) composition pins on the sibling `:limits` /
15916 // `:behavior` outer-`Option<&Composite>` arms — same "the
15917 // M2 overlay emitter must route through the substrate-
15918 // primitive typed dispatch" discipline extended onto the
15919 // third M2 Servico-runtime slot axis, closing the overlay
15920 // emitter's routing invariant on every M2 arm.
15921 use crate::render::{M2_KEY_UPGRADE_FROM, servico_m2_overlay};
15922 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15923 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15924 from: "0.0.1".into(),
15925 instructions: vec![UpgradeInstruction::Restart],
15926 }]);
15927 let overlay = servico_m2_overlay(&c).unwrap();
15928 assert!(
15929 overlay.contains_key(M2_KEY_UPGRADE_FROM),
15930 "servico_m2_overlay must surface M2_KEY_UPGRADE_FROM when \
15931 `:upgrade-from` is non-empty — the accessor and the M2 \
15932 overlay emitter must route through the same substrate- \
15933 primitive typed dispatch on the outer :upgrade-from \
15934 slice (got overlay={overlay:?})",
15935 );
15936 let c = caixa_with_upgrade_from(vec![]);
15937 let overlay = servico_m2_overlay(&c).unwrap();
15938 assert!(
15939 !overlay.contains_key(M2_KEY_UPGRADE_FROM),
15940 "servico_m2_overlay must omit M2_KEY_UPGRADE_FROM when \
15941 `:upgrade-from` is empty — the empty-slice partition \
15942 must route through the accessor's empty-slice return \
15943 unchanged (got overlay={overlay:?})",
15944 );
15945 }
15946
15947 #[test]
15948 fn upgrade_from_projects_slice_by_borrow() {
15949 // The by-borrow pin: [`Caixa::upgrade_from`] returns
15950 // `&[UpgradeFromEntry]` by borrow — the returned slice
15951 // borrows the underlying `Vec<UpgradeFromEntry>` storage of
15952 // the `:upgrade-from` slot and the accessor must not clone
15953 // the backing `Vec` on every call. Peer of the sibling
15954 // outer top-level [`Caixa`] `&[T]`-return by-borrow pins
15955 // (`autores_projects_slice_by_borrow` b5d813f,
15956 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
15957 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15958 // `exe_projects_slice_by_borrow` 65d9527,
15959 // `servicos_projects_slice_by_borrow` 611f78b,
15960 // `deps_projects_slice_by_borrow` ad34b4e,
15961 // `deps_dev_projects_slice_by_borrow` f7fd81e) on the
15962 // sibling outer top-level [`Caixa`] scalar-element `&[T]`
15963 // axes — extended here to the first outer-`Caixa`
15964 // composite-element `&[Composite]` axis: the accessor's
15965 // returned slice must borrow from `&self` (the returned
15966 // reference's lifetime is tied to `&self`), and calling the
15967 // accessor twice on the same [`Caixa`] must yield slices
15968 // that are pointer-equal (the underlying byte-buffer is the
15969 // storage `Vec`'s allocation, not a fresh copy) as well as
15970 // value-equal (idempotent, no side effects on `&self`).
15971 //
15972 // Pins against a future silent detour that returned an owned
15973 // `Vec<UpgradeFromEntry>` (which would type-check but
15974 // silently clone on every call), a `&Vec<UpgradeFromEntry>`
15975 // return (which would leak the backing `Vec`'s
15976 // grow/push/reserve surface no downstream consumer reaches
15977 // for), or a one-arm-only accessor that returned a
15978 // saturating value on some sentinel input.
15979 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15980 for upgrade_from in [
15981 vec![],
15982 vec![UpgradeFromEntry {
15983 from: "0.0.1".into(),
15984 instructions: vec![UpgradeInstruction::Restart],
15985 }],
15986 vec![
15987 UpgradeFromEntry {
15988 from: "0.0.1".into(),
15989 instructions: vec![UpgradeInstruction::Restart],
15990 },
15991 UpgradeFromEntry {
15992 from: "0.0.2".into(),
15993 instructions: vec![UpgradeInstruction::SoftPurge {
15994 module: "demo".into(),
15995 }],
15996 },
15997 ],
15998 ] {
15999 let c = caixa_with_upgrade_from(upgrade_from.clone());
16000 let first = c.upgrade_from();
16001 let second = c.upgrade_from();
16002 assert_eq!(
16003 first, second,
16004 "Caixa::upgrade_from must be idempotent — two \
16005 successive calls on the same &self must return the \
16006 same &[UpgradeFromEntry]",
16007 );
16008 assert_eq!(
16009 first.as_ptr(),
16010 second.as_ptr(),
16011 "Caixa::upgrade_from must borrow the underlying \
16012 Vec<UpgradeFromEntry> storage — two successive calls \
16013 must return slices with the same backing pointer (a \
16014 fresh Vec<UpgradeFromEntry> clone would change the \
16015 pointer on every call)",
16016 );
16017 assert_eq!(
16018 first,
16019 upgrade_from.as_slice(),
16020 "Caixa::upgrade_from must return :upgrade-from \
16021 verbatim by borrow — got {first:?}, expected \
16022 {upgrade_from:?}",
16023 );
16024 }
16025 }
16026
16027 // ── Caixa::children — outer top-level &[ChildSpec] composite-slice accessor ──
16028
16029 fn caixa_with_children(children: Vec<crate::supervisor::ChildSpec>) -> Caixa {
16030 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16031 c.children = children;
16032 c
16033 }
16034
16035 #[test]
16036 fn children_returns_children_slice_verbatim_across_permutations() {
16037 // The canonical per-`Caixa` `:children` M2 supervisor-tree-slot
16038 // outer-composite `&[ChildSpec]`-return slice-shape pin:
16039 // [`Caixa::children`] must return the `:children` typed
16040 // `Vec<ChildSpec>` verbatim as a `&[ChildSpec]` slice-view over
16041 // the same backing buffer the raw `self.children.as_slice()`
16042 // field access borrows from, element-equal across every
16043 // representative fixture in the accept-set — `[]` (the "no
16044 // static children declared" arm every non-`Supervisor`-kind
16045 // `defcaixa` carries by `#[serde(default)]` and every
16046 // `SimpleOneForOne` supervisor carries by cross-slot refusal),
16047 // a canonical single-child `Permanent` fixture (the shape
16048 // most `OneForOne` supervisors carry — a single long-running
16049 // worker child), a canonical multi-child list carrying every
16050 // typed restart-policy variant (`Permanent` / `Transient` /
16051 // `Temporary`), and a past-the-guard sentinel — a duplicate
16052 // `:caixa` `[("w", ...), ("w", ...)]` entry pair
16053 // ([`crate::SupervisorSpec::validate`] rejects through
16054 // `DuplicateChildNome { nome: "w" }` but the accessor must
16055 // ship the raw slot verbatim so struct-literal fixtures
16056 // continue to expose the duplicate at the accessor boundary).
16057 //
16058 // Pins against a future silent detour that returned an owned
16059 // `Vec<ChildSpec>` (which would type-check but silently clone
16060 // on every accessor call, breaking the zero-cost projection
16061 // every peer sibling slice accessor carries), a `[dup, dup] →
16062 // [dup]` dedup collapse (which would silently absorb the
16063 // `DuplicateChildNome` refusal case at the accessor boundary
16064 // and the [`crate::StandardLayout::verify`] cross-child gate
16065 // would silently accept a struct-literal `Caixa` carrying the
16066 // drift), a reference to an operator-resolved overlay (the
16067 // future per-cluster `:children-overrides` slot — its
16068 // resolution must land at exactly this accessor body, not
16069 // silently divert the raw slot away from a second consumer),
16070 // or an axis-shuffled projection (a future detour that
16071 // reordered children through the accessor would silently
16072 // split the paired [`crate::StandardLayout::verify`] per-
16073 // supervisor gate's traversal input from the peer
16074 // [`Self::supervisor_view`] fold-in path's clone-order input,
16075 // since the OTP `RestForOne` restart strategy dispatches on
16076 // declared child order and axis reordering would silently
16077 // split the operator's per-cluster restart-fan-out order
16078 // from the caixa.lisp source-order).
16079 //
16080 // Second outer top-level [`Caixa`] `&[Composite]`-return slice
16081 // accessor pin on the substrate primitive for M2 / M3 typed-
16082 // slot vec-carry axes — folds on the outer-`Caixa`
16083 // `&[Composite]` composite-slice sub-family the sibling
16084 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16085 // (2a1f907) pin opened, peer at the outer altitude of the
16086 // closed inner-`SupervisorSpec` `SupervisorSpec::children`
16087 // (bc92bce) accessor on the same OTP-supervisor static-child-
16088 // list axis.
16089 use crate::supervisor::{ChildSpec, RestartPolicy};
16090 let fixtures: Vec<Vec<ChildSpec>> = vec![
16091 vec![],
16092 vec![ChildSpec {
16093 caixa: "worker".into(),
16094 versao: "^0.1".into(),
16095 restart: RestartPolicy::Permanent,
16096 }],
16097 vec![
16098 ChildSpec {
16099 caixa: "worker-a".into(),
16100 versao: "^0.1".into(),
16101 restart: RestartPolicy::Permanent,
16102 },
16103 ChildSpec {
16104 caixa: "worker-b".into(),
16105 versao: "^0.1".into(),
16106 restart: RestartPolicy::Transient,
16107 },
16108 ChildSpec {
16109 caixa: "worker-c".into(),
16110 versao: "^0.1".into(),
16111 restart: RestartPolicy::Temporary,
16112 },
16113 ],
16114 vec![
16115 ChildSpec {
16116 caixa: "w".into(),
16117 versao: "^0.1".into(),
16118 restart: RestartPolicy::Permanent,
16119 },
16120 ChildSpec {
16121 caixa: "w".into(),
16122 versao: "^0.1".into(),
16123 restart: RestartPolicy::Permanent,
16124 },
16125 ],
16126 ];
16127 for children in fixtures {
16128 let c = caixa_with_children(children.clone());
16129 assert_eq!(
16130 c.children(),
16131 children.as_slice(),
16132 "Caixa::children must return :children verbatim \
16133 (got {:?}, expected {children:?})",
16134 c.children(),
16135 );
16136 assert_eq!(
16137 c.children(),
16138 c.children.as_slice(),
16139 "Caixa::children must element-equal the raw \
16140 `self.children.as_slice()` field access across \
16141 every value in the Vec<ChildSpec> accept-set",
16142 );
16143 assert_eq!(
16144 c.children().is_empty(),
16145 c.children.is_empty(),
16146 "Caixa::children().is_empty() must byte-equal \
16147 self.children.is_empty() — a presence-bit drift \
16148 would silently split the paired \
16149 Caixa::declared_supervisor_slots supervisor-tree \
16150 declared-slot enumerator's presence probe from the \
16151 peer Caixa::supervisor_view typed-view composer's \
16152 fold-in path",
16153 );
16154 }
16155 }
16156
16157 #[test]
16158 fn declared_supervisor_slots_children_arm_routes_through_accessor() {
16159 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
16160 // `:children` presence-probe arm must key off
16161 // [`Caixa::children`], not the raw
16162 // `!self.children.is_empty()` field-probe. Structurally: a
16163 // `Caixa { children: vec![ChildSpec { caixa: "w", versao:
16164 // "^0.1", restart: Permanent }], .. }` must push
16165 // `SUPERVISOR_AUTHOR_KEY_CHILDREN` onto the declared-slot list
16166 // (the presence bit is non-empty, so the supervisor-tree
16167 // kind-coherence gate must surface the slot as "declared"),
16168 // and a `Caixa { children: vec![], .. }` must NOT push the
16169 // label (the "author omitted the slot entirely" arm — the
16170 // empty-slice partition the serde-default folds onto). The
16171 // pair jointly pins the accessor + declared-slot enumerator
16172 // composition: any future silent detour that had the accessor
16173 // collapse `[Permanent]` to `[]` (a `.filter(|c| c.nome() !=
16174 // "__reserved__")` projection) would silently absorb the
16175 // "declared but degenerate" arm at the accessor boundary and
16176 // the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
16177 // kind-coherence gate would silently accept a struct-literal
16178 // `Caixa` carrying the drift.
16179 //
16180 // Peer of the sibling
16181 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16182 // (2a1f907) on the M2 `:upgrade-from` composite-slice arm —
16183 // same "the enumerator gate must route through the substrate-
16184 // primitive typed dispatch" discipline extended onto the
16185 // supervisor-tree `:children` composite-slice arm.
16186 use crate::supervisor::{ChildSpec, RestartPolicy};
16187 let c = caixa_with_children(vec![ChildSpec {
16188 caixa: "w".into(),
16189 versao: "^0.1".into(),
16190 restart: RestartPolicy::Permanent,
16191 }]);
16192 let slots = c.declared_supervisor_slots();
16193 assert!(
16194 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
16195 "declared_supervisor_slots must push \
16196 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
16197 non-empty — the accessor and the enumerator gate must \
16198 route through the same substrate-primitive typed \
16199 dispatch on the outer :children presence bit (got \
16200 slots={slots:?})",
16201 );
16202 let c = caixa_with_children(vec![]);
16203 let slots = c.declared_supervisor_slots();
16204 assert!(
16205 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
16206 "declared_supervisor_slots must NOT push \
16207 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
16208 empty — the author-omitted arm must route through the \
16209 accessor's empty-slice return unchanged (got \
16210 slots={slots:?})",
16211 );
16212 }
16213
16214 #[test]
16215 fn supervisor_view_children_arm_routes_through_accessor() {
16216 // Composition pin: [`Caixa::supervisor_view`]'s per-`:children`
16217 // fold-in arm must key off [`Caixa::children`], not the raw
16218 // `self.children.clone()` field-clone. Structurally: a `Caixa {
16219 // kind: Supervisor, estrategia: Some(OneForOne), children:
16220 // vec![ChildSpec { caixa: "w", .. }], .. }` must fold the
16221 // per-child list through the accessor into the typed
16222 // [`SupervisorSpec`] view's `children` field verbatim — every
16223 // entry the accessor surfaces must land in the view's
16224 // `children` slot in the same order. The pair jointly pins the
16225 // accessor + view-composer composition: any future silent
16226 // detour that had the accessor return a fresh-cloned
16227 // `Vec<ChildSpec>` copy would silently break the reference-
16228 // identity pin the peer `supervisor_view` fold-in path reads
16229 // from — the fold would clone once more per accessor call
16230 // instead of borrowing the storage buffer verbatim once.
16231 //
16232 // Peer of the sibling
16233 // `supervisor_view_kind_gate_routes_through_accessor` (35d8b52-
16234 // family) composition pin on the peer kind-gate arm — same
16235 // "the view composer must route through the substrate-
16236 // primitive typed dispatch" discipline extended onto the
16237 // per-`:children` fold-in arm, closing the supervisor-view
16238 // composer's routing invariant on the composite-slice input.
16239 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16240 let mut c = caixa_with_children(vec![
16241 ChildSpec {
16242 caixa: "worker-a".into(),
16243 versao: "^0.1".into(),
16244 restart: RestartPolicy::Permanent,
16245 },
16246 ChildSpec {
16247 caixa: "worker-b".into(),
16248 versao: "^0.1".into(),
16249 restart: RestartPolicy::Transient,
16250 },
16251 ]);
16252 c.kind = crate::CaixaKind::Supervisor;
16253 c.estrategia = Some(RestartStrategy::OneForOne);
16254 let view = c
16255 .supervisor_view()
16256 .expect("Supervisor kind must produce a supervisor_view");
16257 assert_eq!(
16258 view.children(),
16259 c.children(),
16260 "supervisor_view must fold Caixa::children verbatim into \
16261 SupervisorSpec::children — the accessor and the view \
16262 composer must route through the same substrate-primitive \
16263 typed dispatch on the outer :children slice (got view \
16264 children={:?}, expected {:?})",
16265 view.children(),
16266 c.children(),
16267 );
16268 }
16269
16270 #[test]
16271 fn children_projects_slice_by_borrow() {
16272 // The by-borrow pin: [`Caixa::children`] returns
16273 // `&[ChildSpec]` by borrow — the returned slice borrows the
16274 // underlying `Vec<ChildSpec>` storage of the `:children` slot
16275 // and the accessor must not clone the backing `Vec` on every
16276 // call. Peer of the sibling outer top-level [`Caixa`]
16277 // `&[T]`-return by-borrow pins (`autores_projects_slice_by_borrow`
16278 // b5d813f, `etiquetas_projects_slice_by_borrow` 78c7d3c,
16279 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16280 // `exe_projects_slice_by_borrow` 65d9527,
16281 // `servicos_projects_slice_by_borrow` 611f78b,
16282 // `deps_projects_slice_by_borrow` ad34b4e,
16283 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16284 // `upgrade_from_projects_slice_by_borrow` 2a1f907) on the
16285 // sibling outer top-level [`Caixa`] scalar-element and
16286 // composite-element `&[T]` axes — folds on the outer-`Caixa`
16287 // composite-element `&[Composite]` axis: the accessor's
16288 // returned slice must borrow from `&self` (the returned
16289 // reference's lifetime is tied to `&self`), and calling the
16290 // accessor twice on the same [`Caixa`] must yield slices
16291 // that are pointer-equal (the underlying byte-buffer is the
16292 // storage `Vec`'s allocation, not a fresh copy) as well as
16293 // value-equal (idempotent, no side effects on `&self`).
16294 //
16295 // Pins against a future silent detour that returned an owned
16296 // `Vec<ChildSpec>` (which would type-check but silently clone
16297 // on every call), a `&Vec<ChildSpec>` return (which would leak
16298 // the backing `Vec`'s grow/push/reserve surface no downstream
16299 // consumer reaches for), or a one-arm-only accessor that
16300 // returned a saturating value on some sentinel input.
16301 use crate::supervisor::{ChildSpec, RestartPolicy};
16302 for children in [
16303 vec![],
16304 vec![ChildSpec {
16305 caixa: "w".into(),
16306 versao: "^0.1".into(),
16307 restart: RestartPolicy::Permanent,
16308 }],
16309 vec![
16310 ChildSpec {
16311 caixa: "worker-a".into(),
16312 versao: "^0.1".into(),
16313 restart: RestartPolicy::Permanent,
16314 },
16315 ChildSpec {
16316 caixa: "worker-b".into(),
16317 versao: "^0.1".into(),
16318 restart: RestartPolicy::Transient,
16319 },
16320 ],
16321 ] {
16322 let c = caixa_with_children(children.clone());
16323 let first = c.children();
16324 let second = c.children();
16325 assert_eq!(
16326 first, second,
16327 "Caixa::children must be idempotent — two successive \
16328 calls on the same &self must return the same \
16329 &[ChildSpec]",
16330 );
16331 assert_eq!(
16332 first.as_ptr(),
16333 second.as_ptr(),
16334 "Caixa::children must borrow the underlying \
16335 Vec<ChildSpec> storage — two successive calls must \
16336 return slices with the same backing pointer (a fresh \
16337 Vec<ChildSpec> clone would change the pointer on \
16338 every call)",
16339 );
16340 assert_eq!(
16341 first,
16342 children.as_slice(),
16343 "Caixa::children must return :children verbatim by \
16344 borrow — got {first:?}, expected {children:?}",
16345 );
16346 }
16347 }
16348
16349 // ── Caixa::membros — outer top-level &[Membro] composite-slice accessor ──
16350
16351 fn caixa_aplicacao_with_membros(membros: Vec<crate::aplicacao::Membro>) -> Caixa {
16352 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16353 c.kind = CaixaKind::Aplicacao;
16354 c.membros = membros;
16355 c
16356 }
16357
16358 #[test]
16359 fn membros_returns_membros_slice_verbatim_across_permutations() {
16360 // The canonical per-`Caixa` `:membros` M3 mesh-slot outer-
16361 // composite `&[Membro]`-return slice-shape pin:
16362 // [`Caixa::membros`] must return the `:membros` typed
16363 // `Vec<Membro>` verbatim as a `&[Membro]` slice-view over the
16364 // same backing buffer the raw `self.membros.as_slice()` field
16365 // access borrows from, element-equal across every
16366 // representative fixture in the accept-set — `[]` (the "no
16367 // members declared" arm every non-`Aplicacao`-kind `defcaixa`
16368 // carries by `#[serde(default)]` and every partially-authored
16369 // Aplicacao carries before the
16370 // [`crate::AplicacaoError::MembrosEmpty`] gate fires), a
16371 // canonical single-member fixture (the shape a minimal
16372 // Aplicacao carries — one Servico wrapping one contained
16373 // computation), a canonical multi-member list carrying three
16374 // distinct entries (the canonical checkout-shape Aplicacao —
16375 // cart / pricing / auth — every canonical example carries), and
16376 // a past-the-guard sentinel — a duplicate `:caixa`
16377 // `[("cart", ...), ("cart", ...)]` entry pair
16378 // ([`crate::AplicacaoSpec::validate`] rejects through
16379 // `DuplicateMembro { nome: "cart" }` but the accessor must ship
16380 // the raw slot verbatim so struct-literal fixtures continue to
16381 // expose the duplicate at the accessor boundary).
16382 //
16383 // Pins against a future silent detour that returned an owned
16384 // `Vec<Membro>` (which would type-check but silently clone on
16385 // every accessor call, breaking the zero-cost projection every
16386 // peer sibling slice accessor carries), a `[dup, dup] → [dup]`
16387 // dedup collapse (which would silently absorb the
16388 // `DuplicateMembro` refusal case at the accessor boundary and
16389 // the [`crate::StandardLayout::verify`] cross-member gate would
16390 // silently accept a struct-literal `Caixa` carrying the drift),
16391 // a reference to an operator-resolved overlay (the future per-
16392 // cluster `:membros-overrides` slot — its resolution must land
16393 // at exactly this accessor body, not silently divert the raw
16394 // slot away from a second consumer), or an axis-shuffled
16395 // projection (a future detour that reordered members through
16396 // the accessor would silently split the paired
16397 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16398 // traversal input from the peer [`Self::aplicacao_view`] fold-
16399 // in path's clone-order input, since the canonical `:contratos`
16400 // `:de`/`:para` and `:entrada :para` cross-slot refusal probes
16401 // read the member set through the same slice).
16402 //
16403 // Third outer top-level [`Caixa`] `&[Composite]`-return slice
16404 // accessor pin on the substrate primitive for M2 / M3 typed-
16405 // slot vec-carry axes — opens the outer-`Caixa` M3 mesh-slot
16406 // arm of the `&[Composite]` composite-slice sub-family the
16407 // sibling M2 `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16408 // (2a1f907) and
16409 // `children_returns_children_slice_verbatim_across_permutations`
16410 // (c17b51e) pins opened, peer at the outer altitude of the
16411 // closed inner-[`crate::AplicacaoSpec::membros`] (6c77e36)
16412 // accessor on the same MESH-COMPOSITION per-Aplicacao member-
16413 // list axis.
16414 use crate::aplicacao::Membro;
16415 let fixtures: Vec<Vec<Membro>> = vec![
16416 vec![],
16417 vec![Membro {
16418 caixa: "cart".into(),
16419 versao: "^0.1".into(),
16420 }],
16421 vec![
16422 Membro {
16423 caixa: "cart".into(),
16424 versao: "^0.1".into(),
16425 },
16426 Membro {
16427 caixa: "pricing".into(),
16428 versao: "^0.2".into(),
16429 },
16430 Membro {
16431 caixa: "auth".into(),
16432 versao: "^1.0".into(),
16433 },
16434 ],
16435 vec![
16436 Membro {
16437 caixa: "cart".into(),
16438 versao: "^0.1".into(),
16439 },
16440 Membro {
16441 caixa: "cart".into(),
16442 versao: "^0.1".into(),
16443 },
16444 ],
16445 ];
16446 for membros in fixtures {
16447 let c = caixa_aplicacao_with_membros(membros.clone());
16448 assert_eq!(
16449 c.membros(),
16450 membros.as_slice(),
16451 "Caixa::membros must return :membros verbatim \
16452 (got {:?}, expected {membros:?})",
16453 c.membros(),
16454 );
16455 assert_eq!(
16456 c.membros(),
16457 c.membros.as_slice(),
16458 "Caixa::membros must element-equal the raw \
16459 `self.membros.as_slice()` field access across every \
16460 value in the Vec<Membro> accept-set",
16461 );
16462 assert_eq!(
16463 c.membros().is_empty(),
16464 c.membros.is_empty(),
16465 "Caixa::membros().is_empty() must byte-equal \
16466 self.membros.is_empty() — a presence-bit drift would \
16467 silently split the paired Caixa::declared_mesh_slots \
16468 mesh declared-slot enumerator's presence probe from \
16469 the peer Caixa::aplicacao_view typed-view composer's \
16470 fold-in path",
16471 );
16472 }
16473 }
16474
16475 #[test]
16476 fn declared_mesh_slots_membros_arm_routes_through_accessor() {
16477 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:membros`
16478 // presence-probe arm must key off [`Caixa::membros`], not the
16479 // raw `!self.membros.is_empty()` field-probe. Structurally: a
16480 // `Caixa { membros: vec![Membro { caixa: "cart", versao:
16481 // "^0.1" }], .. }` must push `M3_AUTHOR_KEY_MEMBROS` onto the
16482 // declared-slot list (the presence bit is non-empty, so the
16483 // mesh kind-coherence gate must surface the slot as
16484 // "declared"), and a `Caixa { membros: vec![], .. }` must NOT
16485 // push the label (the "author omitted the slot entirely" arm
16486 // — the empty-slice partition the serde-default folds onto).
16487 // The pair jointly pins the accessor + declared-slot
16488 // enumerator composition: any future silent detour that had
16489 // the accessor collapse `[Membro { .. }]` to `[]` (a
16490 // `.filter(|m| m.nome() != "__reserved__")` projection) would
16491 // silently absorb the "declared but degenerate" arm at the
16492 // accessor boundary and the
16493 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16494 // coherence gate would silently accept a struct-literal
16495 // `Caixa` carrying the drift.
16496 //
16497 // Peer of the sibling
16498 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16499 // (2a1f907) and
16500 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16501 // (c17b51e) composition pins on the M2 `:upgrade-from` /
16502 // `:children` composite-slice arms — same "the enumerator gate
16503 // must route through the substrate-primitive typed dispatch"
16504 // discipline extended onto the M3 `:membros` composite-slice
16505 // arm, opening the M3 arm of the declared-slot enumerator's
16506 // routing invariant.
16507 use crate::aplicacao::Membro;
16508 let c = caixa_aplicacao_with_membros(vec![Membro {
16509 caixa: "cart".into(),
16510 versao: "^0.1".into(),
16511 }]);
16512 let slots = c.declared_mesh_slots();
16513 assert!(
16514 slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16515 "declared_mesh_slots must push M3_AUTHOR_KEY_MEMBROS when \
16516 `:membros` is non-empty — the accessor and the enumerator \
16517 gate must route through the same substrate-primitive \
16518 typed dispatch on the outer :membros presence bit (got \
16519 slots={slots:?})",
16520 );
16521 let c = caixa_aplicacao_with_membros(vec![]);
16522 let slots = c.declared_mesh_slots();
16523 assert!(
16524 !slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16525 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_MEMBROS \
16526 when `:membros` is empty — the author-omitted arm must \
16527 route through the accessor's empty-slice return unchanged \
16528 (got slots={slots:?})",
16529 );
16530 }
16531
16532 #[test]
16533 fn aplicacao_view_membros_arm_routes_through_accessor() {
16534 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:membros`
16535 // fold-in arm must key off [`Caixa::membros`], not the raw
16536 // `self.membros.clone()` field-clone. Structurally: a `Caixa {
16537 // kind: Aplicacao, membros: vec![Membro { caixa: "cart", .. },
16538 // Membro { caixa: "pricing", .. }], .. }` must fold the per-
16539 // member list through the accessor into the typed
16540 // [`crate::AplicacaoSpec`] view's `membros` slot verbatim —
16541 // every entry the accessor surfaces must land in the view's
16542 // `membros` slot in the same order. The pair jointly pins the
16543 // accessor + view-composer composition: any future silent
16544 // detour that had the accessor return a fresh-cloned
16545 // `Vec<Membro>` copy would silently break the reference-
16546 // identity pin the peer `aplicacao_view` fold-in path reads
16547 // from — the fold would clone once more per accessor call
16548 // instead of borrowing the storage buffer verbatim once.
16549 //
16550 // Peer of the sibling
16551 // `aplicacao_view_politicas_arm_folds_through_accessor`
16552 // (5d23d29) /
16553 // `aplicacao_view_placement_arm_folds_through_accessor`
16554 // (4fb8074) /
16555 // `aplicacao_view_entrada_arm_folds_through_accessor` (e4128e4)
16556 // composition pins on the M3 `:politicas` / `:placement` /
16557 // `:entrada` outer-`Option<&Composite>` arms — extended here to
16558 // the M3 `:membros` outer-`&[Composite]` composite-slice arm,
16559 // closing the aplicacao-view composer's routing invariant on
16560 // the composite-slice input.
16561 use crate::aplicacao::Membro;
16562 let c = caixa_aplicacao_with_membros(vec![
16563 Membro {
16564 caixa: "cart".into(),
16565 versao: "^0.1".into(),
16566 },
16567 Membro {
16568 caixa: "pricing".into(),
16569 versao: "^0.2".into(),
16570 },
16571 ]);
16572 let view = c
16573 .aplicacao_view()
16574 .expect("Aplicacao kind must produce an aplicacao_view");
16575 assert_eq!(
16576 view.membros(),
16577 c.membros(),
16578 "aplicacao_view must fold Caixa::membros verbatim into \
16579 AplicacaoSpec::membros — the accessor and the view \
16580 composer must route through the same substrate-primitive \
16581 typed dispatch on the outer :membros slice (got view \
16582 membros={:?}, expected {:?})",
16583 view.membros(),
16584 c.membros(),
16585 );
16586 }
16587
16588 #[test]
16589 fn membros_projects_slice_by_borrow() {
16590 // The by-borrow pin: [`Caixa::membros`] returns `&[Membro]` by
16591 // borrow — the returned slice borrows the underlying
16592 // `Vec<Membro>` storage of the `:membros` slot and the
16593 // accessor must not clone the backing `Vec` on every call.
16594 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16595 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16596 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16597 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16598 // `exe_projects_slice_by_borrow` 65d9527,
16599 // `servicos_projects_slice_by_borrow` 611f78b,
16600 // `deps_projects_slice_by_borrow` ad34b4e,
16601 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16602 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16603 // `children_projects_slice_by_borrow` c17b51e) on the sibling
16604 // outer top-level [`Caixa`] scalar-element and composite-
16605 // element `&[T]` axes — folds on the outer-`Caixa` M3 mesh-
16606 // slot composite-element `&[Composite]` axis: the accessor's
16607 // returned slice must borrow from `&self` (the returned
16608 // reference's lifetime is tied to `&self`), and calling the
16609 // accessor twice on the same [`Caixa`] must yield slices that
16610 // are pointer-equal (the underlying byte-buffer is the storage
16611 // `Vec`'s allocation, not a fresh copy) as well as value-equal
16612 // (idempotent, no side effects on `&self`).
16613 //
16614 // Pins against a future silent detour that returned an owned
16615 // `Vec<Membro>` (which would type-check but silently clone on
16616 // every call), a `&Vec<Membro>` return (which would leak the
16617 // backing `Vec`'s grow/push/reserve surface no downstream
16618 // consumer reaches for), or a one-arm-only accessor that
16619 // returned a saturating value on some sentinel input.
16620 use crate::aplicacao::Membro;
16621 for membros in [
16622 vec![],
16623 vec![Membro {
16624 caixa: "cart".into(),
16625 versao: "^0.1".into(),
16626 }],
16627 vec![
16628 Membro {
16629 caixa: "cart".into(),
16630 versao: "^0.1".into(),
16631 },
16632 Membro {
16633 caixa: "pricing".into(),
16634 versao: "^0.2".into(),
16635 },
16636 ],
16637 ] {
16638 let c = caixa_aplicacao_with_membros(membros.clone());
16639 let first = c.membros();
16640 let second = c.membros();
16641 assert_eq!(
16642 first, second,
16643 "Caixa::membros must be idempotent — two successive \
16644 calls on the same &self must return the same &[Membro]",
16645 );
16646 assert_eq!(
16647 first.as_ptr(),
16648 second.as_ptr(),
16649 "Caixa::membros must borrow the underlying Vec<Membro> \
16650 storage — two successive calls must return slices with \
16651 the same backing pointer (a fresh Vec<Membro> clone \
16652 would change the pointer on every call)",
16653 );
16654 assert_eq!(
16655 first,
16656 membros.as_slice(),
16657 "Caixa::membros must return :membros verbatim by borrow \
16658 — got {first:?}, expected {membros:?}",
16659 );
16660 }
16661 }
16662
16663 // ── Caixa::contratos — outer top-level &[WitContract] composite-slice accessor ──
16664
16665 fn caixa_aplicacao_with_contratos(contratos: Vec<crate::aplicacao::WitContract>) -> Caixa {
16666 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16667 c.kind = CaixaKind::Aplicacao;
16668 c.contratos = contratos;
16669 c
16670 }
16671
16672 fn contrato_http_for_test(
16673 de: &str,
16674 para: &str,
16675 endpoint: &str,
16676 ) -> crate::aplicacao::WitContract {
16677 crate::aplicacao::WitContract {
16678 de: de.into(),
16679 para: para.into(),
16680 wit: "wasi:http/proxy".into(),
16681 endpoint: Some(endpoint.into()),
16682 subject: None,
16683 slot: None,
16684 }
16685 }
16686
16687 #[test]
16688 fn contratos_returns_contratos_slice_verbatim_across_permutations() {
16689 // The canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
16690 // composite `&[WitContract]`-return slice-shape pin:
16691 // [`Caixa::contratos`] must return the `:contratos` typed
16692 // `Vec<WitContract>` verbatim as a `&[WitContract]` slice-view
16693 // over the same backing buffer the raw
16694 // `self.contratos.as_slice()` field access borrows from,
16695 // element-equal across every representative fixture in the
16696 // accept-set — `[]` (the "no contracts declared" arm every
16697 // non-`Aplicacao`-kind `defcaixa` carries by
16698 // `#[serde(default)]` and every leaf-Aplicacao with a single
16699 // member carries), a canonical single-edge fixture (the
16700 // minimal directed-graph shape: one HTTP-shape `(cart → catalog)`
16701 // edge), and a canonical multi-edge fixture with three distinct
16702 // edges (the checkout-shape Aplicacao's HTTP-fan pattern:
16703 // `(cart → catalog)`, `(cart → pricing)`, `(cart → auth)`).
16704 //
16705 // Pins against a future silent detour that returned an owned
16706 // `Vec<WitContract>` (which would type-check but silently clone
16707 // on every accessor call, breaking the zero-cost projection
16708 // every peer sibling slice accessor carries), an axis-shuffled
16709 // projection (a future detour that reordered edges through the
16710 // accessor would silently split the paired
16711 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16712 // traversal input from the peer [`Self::aplicacao_view`] fold-
16713 // in path's clone-order input, since every canonical
16714 // `caixa-mesh` renderer's per-`(:de, :para)` adjacency-list
16715 // seed dispatch reads the edge set through the same slice),
16716 // or a reference to an operator-resolved overlay (the future
16717 // per-cluster `:contratos-overrides` slot — its resolution
16718 // must land at exactly this accessor body, not silently divert
16719 // the raw slot away from a second consumer).
16720 //
16721 // Fourth outer top-level [`Caixa`] `&[Composite]`-return slice
16722 // accessor pin on the substrate primitive for M2 / M3 typed-
16723 // slot vec-carry axes — closes the outer-`Caixa`
16724 // `&[Composite]` composite-slice sub-family the sibling M2
16725 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16726 // (2a1f907) and
16727 // `children_returns_children_slice_verbatim_across_permutations`
16728 // (c17b51e) pins opened and the M3
16729 // `membros_returns_membros_slice_verbatim_across_permutations`
16730 // (0f26987) pin folded on, closing the outer-`Caixa` M3 mesh-
16731 // slot arm of the composite-slice sub-family. Peer at the outer
16732 // altitude of the closed inner-
16733 // [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
16734 // same MESH-COMPOSITION per-Aplicacao contract-list axis.
16735 let fixtures: Vec<Vec<crate::aplicacao::WitContract>> = vec![
16736 vec![],
16737 vec![contrato_http_for_test("cart", "catalog", "/items")],
16738 vec![
16739 contrato_http_for_test("cart", "catalog", "/items"),
16740 contrato_http_for_test("cart", "pricing", "/price"),
16741 contrato_http_for_test("cart", "auth", "/whoami"),
16742 ],
16743 ];
16744 for contratos in fixtures {
16745 let c = caixa_aplicacao_with_contratos(contratos.clone());
16746 assert_eq!(
16747 c.contratos(),
16748 contratos.as_slice(),
16749 "Caixa::contratos must return :contratos verbatim \
16750 (got {:?}, expected {contratos:?})",
16751 c.contratos(),
16752 );
16753 assert_eq!(
16754 c.contratos(),
16755 c.contratos.as_slice(),
16756 "Caixa::contratos must element-equal the raw \
16757 `self.contratos.as_slice()` field access across every \
16758 value in the Vec<WitContract> accept-set",
16759 );
16760 assert_eq!(
16761 c.contratos().is_empty(),
16762 c.contratos.is_empty(),
16763 "Caixa::contratos().is_empty() must byte-equal \
16764 self.contratos.is_empty() — a presence-bit drift would \
16765 silently split the paired Caixa::declared_mesh_slots \
16766 mesh declared-slot enumerator's presence probe from \
16767 the peer Caixa::aplicacao_view typed-view composer's \
16768 fold-in path",
16769 );
16770 }
16771 }
16772
16773 #[test]
16774 fn declared_mesh_slots_contratos_arm_routes_through_accessor() {
16775 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:contratos`
16776 // presence-probe arm must key off [`Caixa::contratos`], not the
16777 // raw `!self.contratos.is_empty()` field-probe. Structurally: a
16778 // `Caixa { contratos: vec![WitContract { .. }], .. }` must push
16779 // `M3_AUTHOR_KEY_CONTRATOS` onto the declared-slot list (the
16780 // presence bit is non-empty, so the mesh kind-coherence gate
16781 // must surface the slot as "declared"), and a `Caixa {
16782 // contratos: vec![], .. }` must NOT push the label (the "author
16783 // omitted the slot entirely" arm — the empty-slice partition
16784 // the serde-default folds onto). The pair jointly pins the
16785 // accessor + declared-slot enumerator composition: any future
16786 // silent detour that had the accessor collapse
16787 // `[WitContract { .. }]` to `[]` (a `.filter(|c| c.de() !=
16788 // "__reserved__")` projection) would silently absorb the
16789 // "declared but degenerate" arm at the accessor boundary and
16790 // the [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16791 // coherence gate would silently accept a struct-literal
16792 // `Caixa` carrying the drift.
16793 //
16794 // Peer of the sibling
16795 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16796 // (2a1f907),
16797 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16798 // (c17b51e), and
16799 // `declared_mesh_slots_membros_arm_routes_through_accessor`
16800 // (0f26987) composition pins on the M2 `:upgrade-from` /
16801 // `:children` / M3 `:membros` composite-slice arms — same "the
16802 // enumerator gate must route through the substrate-primitive
16803 // typed dispatch" discipline extended onto the M3 `:contratos`
16804 // composite-slice arm, closing the M3 mesh-slot arm of the
16805 // declared-slot enumerator's routing invariant on the
16806 // composite-slice inputs.
16807 let c = caixa_aplicacao_with_contratos(vec![contrato_http_for_test(
16808 "cart", "catalog", "/items",
16809 )]);
16810 let slots = c.declared_mesh_slots();
16811 assert!(
16812 slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16813 "declared_mesh_slots must push M3_AUTHOR_KEY_CONTRATOS when \
16814 `:contratos` is non-empty — the accessor and the enumerator \
16815 gate must route through the same substrate-primitive \
16816 typed dispatch on the outer :contratos presence bit (got \
16817 slots={slots:?})",
16818 );
16819 let c = caixa_aplicacao_with_contratos(vec![]);
16820 let slots = c.declared_mesh_slots();
16821 assert!(
16822 !slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16823 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_CONTRATOS \
16824 when `:contratos` is empty — the author-omitted arm must \
16825 route through the accessor's empty-slice return unchanged \
16826 (got slots={slots:?})",
16827 );
16828 }
16829
16830 #[test]
16831 fn aplicacao_view_contratos_arm_routes_through_accessor() {
16832 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:contratos`
16833 // fold-in arm must key off [`Caixa::contratos`], not the raw
16834 // `self.contratos.clone()` field-clone. Structurally: a `Caixa
16835 // { kind: Aplicacao, contratos: vec![WitContract { de: "cart",
16836 // .. }, WitContract { de: "pricing", .. }], .. }` must fold the
16837 // per-edge list through the accessor into the typed
16838 // [`crate::AplicacaoSpec`] view's `contratos` slot verbatim —
16839 // every entry the accessor surfaces must land in the view's
16840 // `contratos` slot in the same order. The pair jointly pins
16841 // the accessor + view-composer composition: a future silent
16842 // detour that had the accessor shuffle or drop an edge would
16843 // silently split the paired declared-slot enumerator's
16844 // presence bit from the typed-view composer's edge-list, a
16845 // two-consumer split at the enumerator and the view composer
16846 // far from the source `caixa.lisp`.
16847 //
16848 // Peer of the sibling
16849 // `aplicacao_view_membros_arm_routes_through_accessor`
16850 // (0f26987) composition pin on the M3 `:membros` outer-
16851 // `&[Composite]` composite-slice arm, closing the aplicacao-
16852 // view composer's routing invariant on the composite-slice
16853 // inputs at the outer altitude.
16854 let c = caixa_aplicacao_with_contratos(vec![
16855 contrato_http_for_test("cart", "catalog", "/items"),
16856 contrato_http_for_test("cart", "pricing", "/price"),
16857 ]);
16858 let view = c
16859 .aplicacao_view()
16860 .expect("Aplicacao kind must produce an aplicacao_view");
16861 assert_eq!(
16862 view.contratos(),
16863 c.contratos(),
16864 "aplicacao_view must fold Caixa::contratos verbatim into \
16865 AplicacaoSpec::contratos — the accessor and the view \
16866 composer must route through the same substrate-primitive \
16867 typed dispatch on the outer :contratos slice (got view \
16868 contratos={:?}, expected {:?})",
16869 view.contratos(),
16870 c.contratos(),
16871 );
16872 }
16873
16874 #[test]
16875 fn contratos_projects_slice_by_borrow() {
16876 // The by-borrow pin: [`Caixa::contratos`] returns `&[WitContract]`
16877 // by borrow — the returned slice borrows the underlying
16878 // `Vec<WitContract>` storage of the `:contratos` slot and the
16879 // accessor must not clone the backing `Vec` on every call.
16880 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16881 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16882 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16883 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16884 // `exe_projects_slice_by_borrow` 65d9527,
16885 // `servicos_projects_slice_by_borrow` 611f78b,
16886 // `deps_projects_slice_by_borrow` ad34b4e,
16887 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16888 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16889 // `children_projects_slice_by_borrow` c17b51e,
16890 // `membros_projects_slice_by_borrow` 0f26987) on the sibling
16891 // outer top-level [`Caixa`] scalar-element and composite-
16892 // element `&[T]` axes — closes the outer-`Caixa` M3 mesh-slot
16893 // composite-element `&[Composite]` axis on the by-borrow pin:
16894 // the accessor's returned slice must borrow from `&self` (the
16895 // returned reference's lifetime is tied to `&self`), and
16896 // calling the accessor twice on the same [`Caixa`] must yield
16897 // slices that are pointer-equal (the underlying byte-buffer is
16898 // the storage `Vec`'s allocation, not a fresh copy) as well as
16899 // value-equal (idempotent, no side effects on `&self`).
16900 //
16901 // Pins against a future silent detour that returned an owned
16902 // `Vec<WitContract>` (which would type-check but silently clone
16903 // on every call), a `&Vec<WitContract>` return (which would
16904 // leak the backing `Vec`'s grow/push/reserve surface no
16905 // downstream consumer reaches for), or a one-arm-only accessor
16906 // that returned a saturating value on some sentinel input.
16907 for contratos in [
16908 vec![],
16909 vec![contrato_http_for_test("cart", "catalog", "/items")],
16910 vec![
16911 contrato_http_for_test("cart", "catalog", "/items"),
16912 contrato_http_for_test("cart", "pricing", "/price"),
16913 ],
16914 ] {
16915 let c = caixa_aplicacao_with_contratos(contratos.clone());
16916 let first = c.contratos();
16917 let second = c.contratos();
16918 assert_eq!(
16919 first, second,
16920 "Caixa::contratos must be idempotent — two successive \
16921 calls on the same &self must return the same \
16922 &[WitContract]",
16923 );
16924 assert_eq!(
16925 first.as_ptr(),
16926 second.as_ptr(),
16927 "Caixa::contratos must borrow the underlying \
16928 Vec<WitContract> storage — two successive calls must \
16929 return slices with the same backing pointer (a fresh \
16930 Vec<WitContract> clone would change the pointer on \
16931 every call)",
16932 );
16933 assert_eq!(
16934 first,
16935 contratos.as_slice(),
16936 "Caixa::contratos must return :contratos verbatim by \
16937 borrow — got {first:?}, expected {contratos:?}",
16938 );
16939 }
16940 }
16941
16942 // ── drift-detection: Caixa top-level multi-word serde-derive-to-const identity ──
16943
16944 #[test]
16945 fn caixa_multi_word_serde_keys_match_lifted_top_level_key_consts() {
16946 // Load-bearing invariant: every multi-word top-level [`Caixa`]
16947 // serde-derived JSON key routes through a lifted `&'static str`
16948 // const. The Rust field names are `snake_case`
16949 // (`deps_dev` / `upgrade_from` / `max_restarts` /
16950 // `restart_window`); [`Caixa`]'s `#[serde(rename_all =
16951 // "camelCase")]` derive attribute maps each to the camelCase
16952 // byte-string the [`Caixa::to_lisp`] round-trip's
16953 // `serde_json::to_value(self)` step lands under before
16954 // `tatara_lisp::domain::json_to_sexp` re-projects the JSON keys
16955 // to the kebab-case `:deps-dev` / `:upgrade-from` /
16956 // `:max-restarts` / `:restart-window` author surface. Serialize
16957 // a fully-populated [`Caixa`] and pin that each canonical
16958 // byte-sequence appears verbatim in the JSON — a future
16959 // accidental `rename_all = "snake_case"` / `"kebab-case"` /
16960 // verbatim-field-name flip at the derive attribute (any of
16961 // which would silently break every [`Caixa::to_lisp`]
16962 // round-trip and the future M4 operator-side manifest ingest's
16963 // `Value::get(<key>)` navigation) surfaces here as a build-time
16964 // test failure at `manifest.rs`, not as an apply-time
16965 // `.get(<stale-canonical-const>)` returning `None` far from the
16966 // derive-attr drift's commit. Same discipline the sibling
16967 // `supervisor_spec_serde_keys_match_lifted_supervisor_key_consts`
16968 // (40cc4e5), `membro_serde_keys_match_lifted_membro_key_consts`
16969 // (ce80ca0), and `upgrade_from_entry_serde_keys_match_lifted_
16970 // m2_upgrade_from_key_consts` (36ffe65) pins established on the
16971 // sibling M2 supervision-tree, M3 [`Membro`] per-entry, and M2
16972 // [`UpgradeFromEntry`] per-entry axes — extended here to the
16973 // enclosing M0 [`Caixa`] top-level axis so the last of the four
16974 // multi-word top-level [`Caixa`] serde-derived JSON keys
16975 // (`depsDev`) joins the substrate's "one canonical byte-string
16976 // per typed serialized-key axis" discipline.
16977 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16978 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16979 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16980 c.deps_dev = vec![Dep::simple("tatara-check", "^0.1")];
16981 c.upgrade_from = vec![UpgradeFromEntry {
16982 from: "0.0.1".into(),
16983 instructions: vec![UpgradeInstruction::Restart],
16984 }];
16985 c.estrategia = Some(RestartStrategy::OneForOne);
16986 c.max_restarts = Some(3);
16987 c.restart_window = Some("60s".into());
16988 c.children = vec![ChildSpec {
16989 caixa: "child".into(),
16990 versao: "^0.1".into(),
16991 restart: RestartPolicy::Permanent,
16992 }];
16993 let json = serde_json::to_string(&c).unwrap();
16994 for key in [
16995 crate::render::CAIXA_KEY_DEPS_DEV,
16996 crate::render::M2_KEY_UPGRADE_FROM,
16997 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16998 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16999 ] {
17000 let quoted = format!("\"{key}\"");
17001 assert!(
17002 json.contains("ed),
17003 "serialized Caixa must carry the lifted top-level \
17004 multi-word byte-sequence {quoted} verbatim in the JSON \
17005 emission (got: {json})",
17006 );
17007 }
17008 }
17009
17010 #[test]
17011 fn caixa_top_level_multi_word_key_consts_are_pairwise_distinct() {
17012 // Cross-axis drift-detection pin: a future collapse of the four
17013 // canonical [`Caixa`] top-level multi-word byte-strings onto the
17014 // same value (e.g. an accidental copy-paste flip of
17015 // [`crate::render::CAIXA_KEY_DEPS_DEV`] to also read
17016 // `"upgradeFrom"`) would silently reroute every downstream
17017 // `Value::get(<key>)` probe on one axis onto the sibling axis's
17018 // top-level entry and pass every propagation-probe test that
17019 // expected only the stale axis's value. Peer of the sibling
17020 // four-way distinct pin on the `SUPERVISOR_KEY_*` tetrad
17021 // (40cc4e5) and the two-way pin on `MEMBRO_KEY_*` (ce80ca0).
17022 let all = [
17023 crate::render::CAIXA_KEY_DEPS_DEV,
17024 crate::render::M2_KEY_UPGRADE_FROM,
17025 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
17026 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
17027 ];
17028 for (i, a) in all.iter().enumerate() {
17029 for b in all.iter().skip(i + 1) {
17030 assert_ne!(
17031 a, b,
17032 "Caixa top-level multi-word key consts must be \
17033 pairwise-distinct canonical byte-sequences — got \
17034 `{a}` == `{b}`",
17035 );
17036 }
17037 }
17038 }
17039
17040 #[test]
17041 fn caixa_top_level_multi_word_key_consts_are_lower_camel_case_shape() {
17042 // Shape-pin: every [`Caixa`] top-level multi-word key const must
17043 // be a lowerCamelCase byte-sequence (no `snake_case`
17044 // underscores, no `kebab-case` hyphens, no leading colon, no
17045 // `PascalCase` leading capital, no whitespace / dots) — the
17046 // canonical shape the `#[serde(rename_all = "camelCase")]`
17047 // derive produces on [`Caixa`]. A future flip to a
17048 // non-camelCase attribute at the derive surfaces both here
17049 // (this test fails on the stale-constant shape) and at
17050 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17051 // (that test fails on the mismatch between const and derive).
17052 // Peer with `membro_key_consts_are_lower_camel_case_shape`
17053 // (ce80ca0) and `supervisor_key_consts_are_lower_camel_case_shape`
17054 // (40cc4e5) on the sibling per-entry / supervisor-tree axes.
17055 for key in [
17056 crate::render::CAIXA_KEY_DEPS_DEV,
17057 crate::render::M2_KEY_UPGRADE_FROM,
17058 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
17059 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
17060 ] {
17061 assert!(
17062 !key.is_empty(),
17063 "Caixa top-level multi-word key const must be non-empty \
17064 (got {key:?})"
17065 );
17066 let first = key.chars().next().unwrap();
17067 assert!(
17068 first.is_ascii_lowercase(),
17069 "Caixa top-level multi-word key const must lead with an \
17070 ASCII-lowercase byte (got {key:?}, leads with {first:?})",
17071 );
17072 assert!(
17073 key.chars().all(|c| c.is_ascii_alphanumeric()),
17074 "Caixa top-level multi-word key const must be \
17075 ASCII-alphanumeric only — no `_` / `-` / `:` / `.` / \
17076 whitespace (got {key:?})",
17077 );
17078 }
17079 }
17080
17081 #[test]
17082 fn caixa_key_deps_dev_pins_canonical_camel_case_byte_string() {
17083 // Scalar-value pin: the byte-string the
17084 // [`crate::render::CAIXA_KEY_DEPS_DEV`] const resolves to,
17085 // asserted verbatim. A future rebrand (`depsDev` → `devDeps`
17086 // matching Cargo's verbatim `dev-dependencies` axis, `depsDev`
17087 // → `depsTest` matching a hypothetical per-test-target
17088 // vocabulary flip) lands as an edit to exactly one const AND
17089 // one derive attribute — the sibling
17090 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17091 // pin already ties the const to the derive attribute, so a
17092 // rebrand that touches only one side of the pair fails at
17093 // caixa-core build time. Same "scalar-value pin per const"
17094 // discipline the sibling
17095 // `m2_top_level_author_key_consts_pin_canonical_kebab_case_labels`
17096 // (f49c8b0) and `contrato_key_consts_pin_canonical_camel_case_labels`
17097 // (ca463a4) pins carry on the peer M2 / M3 top-level slot axes.
17098 assert_eq!(crate::render::CAIXA_KEY_DEPS_DEV, "depsDev");
17099 }
17100
17101 #[test]
17102 fn caixa_key_deps_pins_canonical_byte_string() {
17103 // Scalar-value pin: the byte-string the
17104 // [`crate::render::CAIXA_KEY_DEPS`] const resolves to, asserted
17105 // verbatim. Peer of `caixa_key_deps_dev_pins_canonical_camel_case_byte_string`
17106 // on the two-list dep-graph serialized-key axis — the sibling
17107 // pin covers the multi-word `deps_dev → depsDev` camelCase
17108 // arm, this pin covers the single-word `deps → deps` no-op arm
17109 // (the [`crate::Caixa::deps`] field name carries no `_`, so the
17110 // `#[serde(rename_all = "camelCase")]` derive is a no-op on this
17111 // axis and the emitted JSON key equals the source-side field
17112 // name byte-for-byte). A future [`crate::Caixa::deps`] field
17113 // rename (`deps` → `dependencies` matching Cargo's verbatim
17114 // `[dependencies]` axis, `deps` → `runtime_deps` matching a
17115 // hypothetical per-runtime-target vocabulary flip) OR an added
17116 // `#[serde(rename = "…")]` explicit override lands as an edit
17117 // to exactly one const AND one derive-attr / field name — the
17118 // sibling `caixa_deps_serde_key_matches_lifted_caixa_key_deps`
17119 // pin ties the const to the emitted JSON key, so a rebrand
17120 // that touches only one side of the pair fails at caixa-core
17121 // build time.
17122 assert_eq!(crate::render::CAIXA_KEY_DEPS, "deps");
17123 }
17124
17125 #[test]
17126 fn caixa_deps_serde_key_matches_lifted_caixa_key_deps() {
17127 // Load-bearing invariant on the single-word `deps` top-level
17128 // axis: the byte-string [`crate::render::CAIXA_KEY_DEPS`] pins
17129 // must appear verbatim in the JSON [`Caixa::to_lisp`]'s
17130 // `serde_json::to_value(self)` step emits. Serialize a
17131 // populated [`Caixa`] whose `:deps` slot carries at least one
17132 // entry (the `#[serde(default)]` attribute on the field emits
17133 // an empty `[]` even without members, but a non-empty vec
17134 // additionally covers the codec's per-`Dep`-entry emission
17135 // path) and pin that `"deps"` appears verbatim in the JSON
17136 // emission — a future accidental `rename_all = "snake_case"` /
17137 // `"kebab-case"` flip at the derive attribute (or an added
17138 // `#[serde(rename = "…")]` explicit override on the field, or
17139 // a Rust field rename) would break every [`Caixa::to_lisp`]
17140 // round-trip and the future M4 operator-side manifest ingest's
17141 // `Value::get(CAIXA_KEY_DEPS)` navigation — surfaces here as a
17142 // build-time test failure at `manifest.rs`, not as an
17143 // apply-time `.get(<stale-canonical-const>)` returning `None`
17144 // far from the drift's commit. Peer of the sibling
17145 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
17146 // multi-word pin on the same M0 [`Caixa`] top-level
17147 // serialized-key axis, extended here to the single-word arm
17148 // the multi-word test's `rename_all = "camelCase"` sweep can't
17149 // reach (single-word `deps → deps` is a no-op the multi-word
17150 // pin's `\"depsDev\"` / `\"upgradeFrom\"` / `\"maxRestarts\"` /
17151 // `\"restartWindow\"` byte-scan can never observe).
17152 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
17153 c.deps = vec![Dep::simple("caixa-core", "^0.1")];
17154 let json = serde_json::to_string(&c).unwrap();
17155 let quoted = format!("\"{}\"", crate::render::CAIXA_KEY_DEPS);
17156 assert!(
17157 json.contains("ed),
17158 "serialized Caixa must carry the lifted top-level `deps` \
17159 byte-sequence {quoted} verbatim in the JSON emission (got: \
17160 {json})",
17161 );
17162 }
17163
17164 #[test]
17165 fn caixa_dep_graph_two_list_key_consts_are_pairwise_distinct() {
17166 // Cross-axis drift-detection pin on the two-list dep-graph
17167 // renderer-side wire-key axis: a future collapse of the
17168 // canonical [`crate::render::CAIXA_KEY_DEPS`] /
17169 // [`crate::render::CAIXA_KEY_DEPS_DEV`] byte-strings onto the
17170 // same value (e.g. an accidental copy-paste flip of
17171 // `CAIXA_KEY_DEPS_DEV` to also read `"deps"`) would silently
17172 // reroute every downstream `Value::get(<key>)` probe on one
17173 // axis onto the sibling axis's dep-list and pass every
17174 // propagation-probe test that expected only the stale axis's
17175 // value — a dev-only dep would land in the runtime closure at
17176 // publish time, or a runtime dep would be excluded from the
17177 // published lacre. Peer of the sibling four-way distinct pin
17178 // on the top-level multi-word tetrad
17179 // (`caixa_top_level_multi_word_key_consts_are_pairwise_distinct`)
17180 // and the two-way pin on the sibling
17181 // [`DEP_AUTHOR_KEY_DEPS`] / [`DEP_AUTHOR_KEY_DEPS_DEV`]
17182 // author-facing arm (4da6fba's test), extended here to the
17183 // renderer-side wire-key arm of the same two-list dep-graph
17184 // axis so both halves of the "one canonical byte-string per
17185 // typed axis per (author, wire)" grid carry the same
17186 // distinct-ness discipline.
17187 assert_ne!(
17188 crate::render::CAIXA_KEY_DEPS,
17189 crate::render::CAIXA_KEY_DEPS_DEV,
17190 "CAIXA_KEY_DEPS and CAIXA_KEY_DEPS_DEV must be distinct \
17191 canonical byte-sequences on the two-list dep-graph \
17192 renderer-side wire-key axis"
17193 );
17194 }
17195
17196 // ── DepList / Caixa::push_dep pin ────────────────────────────────
17197 //
17198 // The compounding pin: the two-arm closed-set typed enum
17199 // [`crate::dep::DepList`] carries the runtime-closure `:deps`
17200 // (`Prod`) vs dev-only-closure `:deps-dev` (`Dev`) dispatch every
17201 // consumer of the top-level manifest's dep-mutation surface reads
17202 // through, and the typed dispatch [`Caixa::push_dep`] on the
17203 // substrate primitive folds the "select list → check within-list
17204 // dup → push" cascade onto one method call. Prior to this landing
17205 // the two axes lived across two `&'static str` constants
17206 // (`DEP_AUTHOR_KEY_DEPS`, `DEP_AUTHOR_KEY_DEPS_DEV`) with no closed-
17207 // set type carrying the pair; the `feira add` mutation site's
17208 // inline `if self.dev { &mut caixa.deps_dev } else { &mut
17209 // caixa.deps }` dispatch expressed no compile-time link back to
17210 // the substrate primitive, and a future third dep-list axis would
17211 // have silently split at every open-coded mutation site.
17212
17213 #[test]
17214 fn dep_list_as_str_routes_through_lifted_author_key_constants() {
17215 // Every arm returns the same `&'static str` the substrate's
17216 // canonical `DEP_AUTHOR_KEY_DEPS` / `DEP_AUTHOR_KEY_DEPS_DEV`
17217 // constants carry. A future rebrand on either constant reaches
17218 // the enum through one edit; a regression to inline literals
17219 // (e.g. `Prod => ":deps"`) would silently split the diagnostic
17220 // quotes from the wire-format constants every consumer routes
17221 // through and this pin flags it at build time.
17222 assert_eq!(
17223 crate::dep::DepList::Prod.as_str(),
17224 crate::render::DEP_AUTHOR_KEY_DEPS
17225 );
17226 assert_eq!(
17227 crate::dep::DepList::Dev.as_str(),
17228 crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17229 );
17230 }
17231
17232 #[test]
17233 fn dep_list_display_routes_through_as_str() {
17234 // Same as-str-through-Display convergence discipline the
17235 // sibling closed-set typed enums carry — a `format!("{list}")`
17236 // call must land byte-for-byte on the accessor's return so a
17237 // future consumer that formats the enum for a diagnostic line
17238 // reaches the same wire-format constant the wire-format
17239 // producers do.
17240 assert_eq!(
17241 format!("{}", crate::dep::DepList::Prod),
17242 crate::dep::DepList::Prod.as_str()
17243 );
17244 assert_eq!(
17245 format!("{}", crate::dep::DepList::Dev),
17246 crate::dep::DepList::Dev.as_str()
17247 );
17248 }
17249
17250 #[test]
17251 fn dep_list_all_enumerates_every_variant_once() {
17252 // Exhaustive-iteration pin — every arm appears exactly once in
17253 // `ALL`, matching the closed set the compiler enforces on the
17254 // sibling `match self` arms. A future variant addition that
17255 // extends only one method's match without extending `ALL`
17256 // would silently drop the new arm from every consumer that
17257 // iterates the slice.
17258 let variants: &[crate::dep::DepList] = crate::dep::DepList::ALL;
17259 assert!(variants.contains(&crate::dep::DepList::Prod));
17260 assert!(variants.contains(&crate::dep::DepList::Dev));
17261 assert_eq!(variants.len(), 2);
17262 }
17263
17264 #[test]
17265 fn dep_list_from_wire_returns_prod_on_deps_wire_scalar() {
17266 // Reverse projection on the two-list dep-graph axis: the
17267 // author-surface wire tag the sibling `as_str` emitter walks
17268 // for `Prod` (`:deps` via `DEP_AUTHOR_KEY_DEPS`) parses back to
17269 // `Some(DepList::Prod)`. A regression that hand-rolled the
17270 // per-arm match without routing through the lifted
17271 // `DEP_AUTHOR_KEY_DEPS` const would silently disagree on any
17272 // future wire-tag rebrand and this pin flags it at build time.
17273 assert_eq!(
17274 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS),
17275 Some(crate::dep::DepList::Prod)
17276 );
17277 }
17278
17279 #[test]
17280 fn dep_list_from_wire_returns_dev_on_deps_dev_wire_scalar() {
17281 // Peer of the `Prod`-arm pin on the dev-only axis: the
17282 // author-surface wire tag the sibling `as_str` emitter walks
17283 // for `Dev` (`:deps-dev` via `DEP_AUTHOR_KEY_DEPS_DEV`) parses
17284 // back to `Some(DepList::Dev)`. Same drift-detection posture
17285 // as the peer arm — the sibling method `match` arms are
17286 // compiler-checked exhaustive so a future variant addition
17287 // trips at build time.
17288 assert_eq!(
17289 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
17290 Some(crate::dep::DepList::Dev)
17291 );
17292 }
17293
17294 #[test]
17295 fn dep_list_from_wire_returns_none_on_unknown_wire_scalar() {
17296 // Every input outside the closed-set arm-string set the
17297 // sibling `as_str` emitter walks lands on the terminal `None`
17298 // fallback — no silent-accept surface. Sweeps a set of
17299 // plausibly-adjacent scalars (unprefixed wire form, PascalCase
17300 // rebrand candidates, foreign wire tags, empty string) so a
17301 // future variant addition that widened one wire form without
17302 // extending the emitter's arm-set would trip the sibling
17303 // round-trip pin below rather than silently accepting the new
17304 // form here.
17305 for candidate in [
17306 "",
17307 "deps",
17308 "deps-dev",
17309 ":deps ",
17310 ":Deps",
17311 ":DEPS",
17312 ":build-dep",
17313 ":tool-dep",
17314 "prod",
17315 "dev",
17316 ] {
17317 assert_eq!(
17318 crate::dep::DepList::from_wire(candidate),
17319 None,
17320 "from_wire({candidate:?}) must return None; every input outside \
17321 the {{DEP_AUTHOR_KEY_DEPS, DEP_AUTHOR_KEY_DEPS_DEV}} accept-set \
17322 the sibling as_str emitter walks lands on the terminal fallback",
17323 );
17324 }
17325 }
17326
17327 #[test]
17328 fn dep_list_round_trips_through_as_str_and_from_wire() {
17329 // Load-bearing round-trip pin: every arm the `ALL` iteration
17330 // exposes survives the `as_str` → `from_wire` composition
17331 // byte-for-byte. Same discipline the sibling closed-set enums
17332 // carry — `CaixaKind` /
17333 // `RestartStrategy` / `RestartPolicy` /
17334 // `PlacementStrategy` — extended onto the two-list dep-graph
17335 // axis. A future variant addition that extends `ALL` +
17336 // `as_str` without extending `from_wire` (or vice versa)
17337 // trips at build time on this iteration because the compiler
17338 // enforces exhaustiveness on the sibling `match self` arms.
17339 for &list in crate::dep::DepList::ALL {
17340 assert_eq!(
17341 crate::dep::DepList::from_wire(list.as_str()),
17342 Some(list),
17343 "DepList::from_wire(as_str({list:?})) must round-trip to Some({list:?}) — \
17344 a silent split between the forward emitter and the reverse parser \
17345 would drift the two halves of the two-list dep-graph axis's typed dispatch",
17346 );
17347 }
17348 }
17349
17350 #[test]
17351 fn push_dep_routes_to_deps_slot_on_prod_arm() {
17352 // The `Prod` arm dispatches to the runtime-closure `:deps`
17353 // slot every downstream lacre-pipeline consumer resolves at
17354 // build time. A future arm that regressed to inline `&mut
17355 // self.deps_dev` on the `Prod` path would silently reroute
17356 // every runtime dep into the dev-only closure at publish time
17357 // — this pin refuses that regression.
17358 let src = Caixa::template("host");
17359 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17360 let before_deps = caixa.deps().len();
17361 let before_deps_dev = caixa.deps_dev().len();
17362 let dep = Dep {
17363 nome: "caixa-teia".to_string(),
17364 versao: "^0.1".to_string(),
17365 fonte: None,
17366 opcional: false,
17367 caracteristicas: Vec::new(),
17368 };
17369 caixa
17370 .push_dep(crate::dep::DepList::Prod, dep)
17371 .expect("first push into :deps succeeds");
17372 assert_eq!(caixa.deps().len(), before_deps + 1);
17373 assert_eq!(caixa.deps_dev().len(), before_deps_dev);
17374 assert_eq!(caixa.deps().last().unwrap().nome(), "caixa-teia");
17375 }
17376
17377 #[test]
17378 fn push_dep_routes_to_deps_dev_slot_on_dev_arm() {
17379 // Peer of the sibling `Prod`-arm dispatch pin — the `Dev` arm
17380 // must dispatch to the dev-only-closure `:deps-dev` slot every
17381 // downstream test-facing artifact resolver reads. A future
17382 // regression that inverted the two arms would silently route
17383 // every dev-only dep into the runtime closure at publish time
17384 // and this pin catches it before the drift ships.
17385 let src = Caixa::template("host");
17386 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17387 let dep = Dep {
17388 nome: "tatara-check".to_string(),
17389 versao: "*".to_string(),
17390 fonte: None,
17391 opcional: false,
17392 caracteristicas: Vec::new(),
17393 };
17394 caixa
17395 .push_dep(crate::dep::DepList::Dev, dep)
17396 .expect("first push into :deps-dev succeeds");
17397 assert!(caixa.deps().is_empty());
17398 assert_eq!(caixa.deps_dev().len(), 1);
17399 assert_eq!(caixa.deps_dev().last().unwrap().nome(), "tatara-check");
17400 }
17401
17402 #[test]
17403 fn push_dep_refuses_within_list_duplicate_nome_with_typed_error() {
17404 // Within-list dup check routes through the canonical
17405 // [`DepError::DuplicateNome`] carrier — the substrate's typed
17406 // diagnostic for the same axis [`Caixa::validate_deps`]'s
17407 // parse-time [`crate::render::insert_first_seen`] walk raises
17408 // on. Prior to the lift the mutation site's inline
17409 // `bail!("dep '{}' already declared", …)` string-diagnostic
17410 // path expressed no through-line back to the typed error;
17411 // routing every dep-list refusal through one carrier means an
17412 // author reading a `feira add` refusal and a `feira build`
17413 // refusal reaches for the same corrective surface without
17414 // switching diagnostic idioms.
17415 let src = Caixa::template("host");
17416 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17417 let dep = Dep {
17418 nome: "caixa-teia".to_string(),
17419 versao: "^0.1".to_string(),
17420 fonte: None,
17421 opcional: false,
17422 caracteristicas: Vec::new(),
17423 };
17424 caixa
17425 .push_dep(crate::dep::DepList::Prod, dep.clone())
17426 .expect("first push succeeds");
17427 let dup = Dep {
17428 nome: "caixa-teia".to_string(),
17429 versao: "^0.2".to_string(),
17430 fonte: None,
17431 opcional: false,
17432 caracteristicas: Vec::new(),
17433 };
17434 let err = caixa
17435 .push_dep(crate::dep::DepList::Prod, dup)
17436 .expect_err("second push with same :nome refuses");
17437 assert_eq!(
17438 err,
17439 DepError::DuplicateNome {
17440 nome: "caixa-teia".to_string(),
17441 list: crate::render::DEP_AUTHOR_KEY_DEPS,
17442 }
17443 );
17444 // The refused mutation must not corrupt the target list —
17445 // exactly one entry lives past the refusal, matching the
17446 // canonical single-source-of-truth invariant `Caixa::deps()`
17447 // carries.
17448 assert_eq!(caixa.deps().len(), 1);
17449 }
17450
17451 #[test]
17452 fn push_dep_refuses_dup_on_dev_list_arm_names_deps_dev_key() {
17453 // Peer of the sibling `Prod`-arm dup-refusal pin — the `Dev`
17454 // arm's refusal must carry `DEP_AUTHOR_KEY_DEPS_DEV` in the
17455 // `list` payload so a future author reading the refusal grep's
17456 // for the correct `:deps-dev` block in their `caixa.lisp`,
17457 // not the sibling `:deps` block the runtime closure resolves.
17458 let src = Caixa::template("host");
17459 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17460 let dep = Dep {
17461 nome: "tatara-check".to_string(),
17462 versao: "*".to_string(),
17463 fonte: None,
17464 opcional: false,
17465 caracteristicas: Vec::new(),
17466 };
17467 caixa
17468 .push_dep(crate::dep::DepList::Dev, dep.clone())
17469 .expect("first push succeeds");
17470 let err = caixa
17471 .push_dep(crate::dep::DepList::Dev, dep)
17472 .expect_err("second push with same :nome refuses");
17473 assert!(matches!(
17474 err,
17475 DepError::DuplicateNome {
17476 ref nome,
17477 list,
17478 } if nome == "tatara-check"
17479 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17480 ));
17481 }
17482
17483 #[test]
17484 fn push_dep_allows_same_nome_across_prod_and_dev_lists() {
17485 // The within-list dup check is scoped to the target arm — a
17486 // caixa may legitimately carry the same `:nome` under both
17487 // `:deps` and `:deps-dev` (though the substrate's peer
17488 // [`crate::Caixa::validate_deps`] walk still refuses the
17489 // shape at parse time; the mutation-site refusal is scoped to
17490 // the mutation-site's list to match the peer parse-time
17491 // per-list [`crate::render::insert_first_seen`] discipline).
17492 // The two arms hold independent seen-sets.
17493 let src = Caixa::template("host");
17494 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17495 let dep_prod = Dep {
17496 nome: "shared".to_string(),
17497 versao: "^0.1".to_string(),
17498 fonte: None,
17499 opcional: false,
17500 caracteristicas: Vec::new(),
17501 };
17502 let dep_dev = Dep {
17503 nome: "shared".to_string(),
17504 versao: "*".to_string(),
17505 fonte: None,
17506 opcional: false,
17507 caracteristicas: Vec::new(),
17508 };
17509 caixa
17510 .push_dep(crate::dep::DepList::Prod, dep_prod)
17511 .expect("push into :deps succeeds");
17512 caixa
17513 .push_dep(crate::dep::DepList::Dev, dep_dev)
17514 .expect("push same :nome into :deps-dev succeeds");
17515 assert_eq!(caixa.deps().len(), 1);
17516 assert_eq!(caixa.deps_dev().len(), 1);
17517 }
17518
17519 #[test]
17520 fn deps_of_prod_returns_the_deps_slot_verbatim() {
17521 // The `Prod` arm of the typed-dispatch [`Caixa::deps_of`] read
17522 // accessor must project onto the runtime-closure `:deps` slot —
17523 // element-equal and length-equal to the sibling per-slot
17524 // [`Caixa::deps`] accessor's return over every per-caixa fixture.
17525 // A future arm that regressed to `self.deps_dev()` on the `Prod`
17526 // path would silently reroute every downstream typed-dispatch
17527 // walker (the [`Caixa::validate_deps`] per-list
17528 // [`crate::render::insert_first_seen`] dedup walk, any future
17529 // per-axis-parametrised consumer) into the sibling dev-only
17530 // closure and this pin refuses that regression.
17531 let src = Caixa::template("host");
17532 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17533 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17534 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 0);
17535 let dep = Dep {
17536 nome: "caixa-teia".to_string(),
17537 versao: "^0.1".to_string(),
17538 fonte: None,
17539 opcional: false,
17540 caracteristicas: Vec::new(),
17541 };
17542 caixa
17543 .push_dep(crate::dep::DepList::Prod, dep.clone())
17544 .expect("push into :deps succeeds");
17545 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17546 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 1);
17547 assert_eq!(
17548 caixa.deps_of(crate::dep::DepList::Prod)[0].nome(),
17549 "caixa-teia"
17550 );
17551 }
17552
17553 #[test]
17554 fn deps_of_dev_returns_the_deps_dev_slot_verbatim() {
17555 // Peer of the sibling `Prod`-arm pin — the `Dev` arm of
17556 // [`Caixa::deps_of`] must project onto the dev-only-closure
17557 // `:deps-dev` slot, element-equal and length-equal to the
17558 // sibling per-slot [`Caixa::deps_dev`] accessor's return. A
17559 // future regression that inverted the two arms would silently
17560 // route every dev-list walker onto the runtime closure and this
17561 // pin catches it before the drift ships.
17562 let src = Caixa::template("host");
17563 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17564 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17565 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 0);
17566 let dep = Dep {
17567 nome: "tatara-check".to_string(),
17568 versao: "*".to_string(),
17569 fonte: None,
17570 opcional: false,
17571 caracteristicas: Vec::new(),
17572 };
17573 caixa
17574 .push_dep(crate::dep::DepList::Dev, dep)
17575 .expect("push into :deps-dev succeeds");
17576 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17577 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 1);
17578 assert_eq!(
17579 caixa.deps_of(crate::dep::DepList::Dev)[0].nome(),
17580 "tatara-check"
17581 );
17582 }
17583
17584 #[test]
17585 fn deps_of_exhaustive_over_dep_list_all_covers_the_two_slots() {
17586 // Composition pin: iterating [`crate::dep::DepList::ALL`] through
17587 // [`Caixa::deps_of`] must land on the same two-slot partition the
17588 // per-slot [`Caixa::deps`] / [`Caixa::deps_dev`] accessors
17589 // expose — the canonical dispatch a future per-axis-parametrised
17590 // walker (a future `feira app graph` per-list dep summary, a
17591 // future M4 per-cluster dev-closure-audit overlay the CR
17592 // materializer resolves per-CR) reads through. Prior to the
17593 // lift the two-block iteration lived open-coded at every walker,
17594 // so a future third dep-list axis (`:deps-build`, per CAIXA-SDLC
17595 // §I) would have had to grow a third block at every consumer.
17596 // A regression that dropped the `Dev` arm from `ALL` would flip
17597 // the collected pairs to `[(":deps", &[])]` alone and this pin
17598 // refuses that shape.
17599 let src = Caixa::template("host");
17600 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17601 let prod_dep = Dep {
17602 nome: "caixa-teia".to_string(),
17603 versao: "^0.1".to_string(),
17604 fonte: None,
17605 opcional: false,
17606 caracteristicas: Vec::new(),
17607 };
17608 let dev_dep = Dep {
17609 nome: "tatara-check".to_string(),
17610 versao: "*".to_string(),
17611 fonte: None,
17612 opcional: false,
17613 caracteristicas: Vec::new(),
17614 };
17615 caixa
17616 .push_dep(crate::dep::DepList::Prod, prod_dep)
17617 .expect("push into :deps succeeds");
17618 caixa
17619 .push_dep(crate::dep::DepList::Dev, dev_dep)
17620 .expect("push into :deps-dev succeeds");
17621 let collected: Vec<(&'static str, usize, &str)> = crate::dep::DepList::ALL
17622 .iter()
17623 .map(|&list| {
17624 let slice = caixa.deps_of(list);
17625 (list.as_str(), slice.len(), slice[0].nome())
17626 })
17627 .collect();
17628 assert_eq!(
17629 collected,
17630 vec![
17631 (crate::render::DEP_AUTHOR_KEY_DEPS, 1, "caixa-teia"),
17632 (crate::render::DEP_AUTHOR_KEY_DEPS_DEV, 1, "tatara-check"),
17633 ]
17634 );
17635 }
17636
17637 #[test]
17638 fn validate_deps_iterates_through_dep_list_all_via_deps_of() {
17639 // Composition pin: the [`Caixa::validate_deps`] parse-time gate
17640 // must route its per-list [`crate::render::insert_first_seen`]
17641 // dedup walk through [`Caixa::deps_of`] + [`crate::dep::DepList::ALL`]
17642 // rather than the pre-lift open-coded two-block iteration over
17643 // `self.deps()` + `self.deps_dev()`. A regression that dropped
17644 // one arm (e.g. hand-inlining `self.deps()` alone) would silently
17645 // stop refusing within-list dups on the sibling arm; a
17646 // regression that flipped the arm-to-list-key mapping
17647 // (`Dev => DEP_AUTHOR_KEY_DEPS`) would silently mislabel the
17648 // diagnostic surface. Both drifts surface here through a paired
17649 // duplicate-name refusal per arm plus an offending-list-key
17650 // check on the emitted [`DepError::DuplicateNome`] carrier.
17651 for &list in crate::dep::DepList::ALL {
17652 let src = Caixa::template("host");
17653 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17654 let dup = Dep {
17655 nome: "twin".to_string(),
17656 versao: "^0.1".to_string(),
17657 fonte: None,
17658 opcional: false,
17659 caracteristicas: Vec::new(),
17660 };
17661 match list {
17662 crate::dep::DepList::Prod => {
17663 caixa.deps.push(dup.clone());
17664 caixa.deps.push(dup);
17665 }
17666 crate::dep::DepList::Dev => {
17667 caixa.deps_dev.push(dup.clone());
17668 caixa.deps_dev.push(dup);
17669 }
17670 }
17671 let err = caixa
17672 .validate_deps()
17673 .expect_err("within-list duplicate :nome must refuse");
17674 assert_eq!(
17675 err,
17676 DepError::DuplicateNome {
17677 nome: "twin".to_string(),
17678 list: list.as_str(),
17679 },
17680 "validate_deps on {list} arm must emit \
17681 DepError::DuplicateNome carrying the arm's own \
17682 as_str() diagnostic — the arm-to-list-key mapping \
17683 flowed through DepList::ALL + Caixa::deps_of"
17684 );
17685 }
17686 }
17687}