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 /// `pending-fallible-register`: upstream `tatara_lisp::domain::register`
341 /// became `-> Result<(), KeywordCollision>` on 2026-07-31, so a second type
342 /// claiming `defcaixa` in one process is refused and named instead of
343 /// silently displacing this one. This workspace pins
344 /// `tatara-lisp = "0.3.3"`, which predates that, so the result cannot be
345 /// checked here yet. Propagate it — `pub fn register() -> Result<(),
346 /// tatara_lisp::KeywordCollision>` — in the same commit that bumps the pin.
347 pub fn register() {
348 tatara_lisp::domain::register::<Self>();
349 }
350
351 /// Substrate-canonical per-`Caixa` `:licenca` SPDX-expression scalar
352 /// accessor every consumer of the top-level manifest's license axis
353 /// keys off — returns the author-declared `:licenca` byte-string
354 /// verbatim as an `Option<&str>`, borrowed from the typed slot's own
355 /// `Option<String>` storage. `None` when the slot is absent (the
356 /// canonical "omit to defer to the caixa-helm renderer's `MIT`
357 /// fallback" shape [`Self::validate_licenca`] documents at
358 /// caixa-core/src/manifest.rs:1560; the peer [`caixa-helm`]
359 /// `build_readme` fold at caixa-helm/src/lib.rs:962 reads this
360 /// predicate too, so an authored-but-unset `:licenca` round-trips to
361 /// a rendered `lareira-<nome>` chart's `README.md` `## License`
362 /// section structurally identical to one that omits the slot).
363 ///
364 /// The `:licenca` slot carries the universal-axis SPDX-expression
365 /// license identifier every kind of caixa emits under (CAIXA-SDLC
366 /// §I — the author-facing surface every `defcaixa` form supplies) —
367 /// the typed slot's `Option<String>` accept-set (empty-string
368 /// rejected through [`ManifestError::LicencaEmpty`], SPDX-alphabet-
369 /// invalid rejected through [`ManifestError::LicencaInvalid`]) maps
370 /// onto the `lareira-<nome>` Helm chart's `README.md` `## License`
371 /// section (caixa-helm/src/lib.rs:962) and (through future
372 /// tightening documented at [`Self::validate_licenca`]) the
373 /// Chart.yaml `annotations["artifacthub.io/license"]` axis every
374 /// registry-facing chart carries. Every downstream consumer that
375 /// reads the license byte-string keys off this scalar (the
376 /// [`Self::validate_licenca`] empty-arm + SPDX-shape gate that
377 /// routes through `self.licenca.as_deref()`, the caixa-helm
378 /// `build_readme` `unwrap_or_else(|| "MIT".into())` fold that keys
379 /// the fallback off the `Option::is_none()` arm, every future
380 /// per-`Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
381 /// acknowledges).
382 ///
383 /// Prior to this lift the `.licenca` field was accessed inline at
384 /// two production sites — [`Self::validate_licenca`]'s
385 /// `self.licenca.as_deref()` empty-and-shape gate binding and the
386 /// caixa-helm `build_readme` `caixa.licenca.clone().unwrap_or_else(||
387 /// "MIT".into())` `README.md` `## License` fold — two open-coded
388 /// field-accesses that expressed no compile-time link back to the
389 /// typed slot. A future extension of the `:licenca` axis to a
390 /// richer author surface — a per-`:licenca` structured SPDX
391 /// expression parser + license-id allowlist (the future tightening
392 /// [`Self::validate_licenca`]'s docstring acknowledges), a
393 /// per-cluster license-default overlay the M4 CR materializer
394 /// resolves per-CR (the "cluster policy pins `Apache-2.0` for every
395 /// unlisted caixa" arm), a promotion of the plain
396 /// `Option<String>` byte-string to a richer `SpdxExpression` enum
397 /// once the SPDX-expression parser lands — would have had to be
398 /// threaded through both open-coded copies in lockstep or the
399 /// validate gate and the caixa-helm emit path would silently
400 /// disagree on which license a given [`Caixa`] resolves to (an
401 /// author's `:licenca "MIT OR Apache-2.0"` would satisfy validate
402 /// while the emit path silently rendered a stale `MIT` fallback,
403 /// or vice versa). Lifting the resolution to a typed method on the
404 /// substrate primitive means every downstream consumer of the
405 /// caixa's per-`Caixa` license surface reaches for exactly one
406 /// typed dispatch — the resolver's accept-set migrates as a unit
407 /// on any future axis addition.
408 ///
409 /// First `Option<&str>`-return top-level [`Caixa`] scalar accessor —
410 /// opens the "outer [`Caixa`] `Option<&str>` scalar" projection
411 /// pattern the sibling per-`Caixa` `:descricao` / `:repositorio` /
412 /// `:edicao` future lifts fold on. Same "one typed dispatch on the
413 /// substrate primitive, thin projections at each consumer"
414 /// discipline the peer per-`:placement` [`crate::aplicacao::Placement::shard_key`]
415 /// (7cd2a28) / [`crate::aplicacao::Placement::affinity`] (74ec2d3)
416 /// / per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
417 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
418 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
419 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
420 /// typed-slot atom axes, extended here to the outer top-level
421 /// `Caixa` universal-axis surface. Named `licenca()` to match the
422 /// storage field's name; the accessor's identity maps onto the
423 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
424 /// carries.
425 #[must_use]
426 pub fn licenca(&self) -> Option<&str> {
427 self.licenca.as_deref()
428 }
429
430 /// Substrate-canonical per-`Caixa` `:repositorio` git-repo-URL scalar
431 /// accessor every consumer of the top-level manifest's homepage /
432 /// source-of-truth axis keys off — returns the author-declared
433 /// `:repositorio` byte-string verbatim as an `Option<&str>`, borrowed
434 /// from the typed slot's own `Option<String>` storage. `None` when
435 /// the slot is absent (the canonical "omit to defer to the renderer's
436 /// per-target placeholder" shape — [`caixa-helm`]'s `ChartYaml.home`
437 /// carries the `Option<String>` through verbatim so an author-omitted
438 /// `:repositorio` renders a `Chart.yaml` without a `home:` field
439 /// (`skip_serializing_if = "Option::is_none"`), while [`caixa-flux`]'s
440 /// `ClusterBundleOpts::for_caixa` folds the omitted slot through a
441 /// `format!("https://github.com/{DEFAULT_PLEME_GIT_ORG}/{nome}")`
442 /// fallback derived from `caixa.nome`).
443 ///
444 /// The `:repositorio` slot carries the universal-axis git-repo-URL
445 /// homepage identifier every kind of caixa emits under (CAIXA-SDLC
446 /// §I — the author-facing surface every `defcaixa` form supplies) —
447 /// the typed slot's `Option<String>` accept-set (empty-string
448 /// rejected through [`ManifestError::RepositorioEmpty`], git-repo-URL-
449 /// shape-invalid rejected through [`ManifestError::RepositorioInvalid`]
450 /// past the shared [`crate::render::is_git_repo_url`] predicate the
451 /// peer per-`:deps :fonte :repo` axis also routes through) maps onto
452 /// four load-bearing downstream consumers:
453 ///
454 /// - [`Self::validate_repositorio`]'s empty-arm + shape-predicate
455 /// gate binding at caixa-core/src/manifest.rs:1456 — the
456 /// universal-axis identity gate wired at caixa-build time.
457 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.home` fold at
458 /// caixa-helm/src/lib.rs:840 — the rendered `lareira-<nome>`
459 /// Helm chart's `Chart.yaml` `home:` field, which every registry
460 /// that ingests the chart (ArtifactHub, chartmuseum,
461 /// `helm search repo`) surfaces as the chart's canonical source-
462 /// of-truth link.
463 /// - [`caixa-helm`]'s `build_readme` `## Source` fold at
464 /// caixa-helm/src/lib.rs:957 — the rendered `lareira-<nome>`
465 /// chart's `README.md` header link back to the source repo,
466 /// which every author who inspects the rendered chart bundle
467 /// lands at.
468 /// - [`caixa-flux`]'s `ClusterBundleOpts::for_caixa`
469 /// `GitRepository.spec.url` fold at caixa-flux/src/lib.rs:2006 —
470 /// the rendered `GitRepository` CR's `spec.url` field, which
471 /// FluxCD's `source-controller` polls to reconcile the caixa's
472 /// manifest bundle from git.
473 ///
474 /// Prior to this lift the `.repositorio` field was accessed inline
475 /// at four production sites — [`Self::validate_repositorio`]'s
476 /// `self.repositorio.as_deref()` empty-and-shape gate binding, the
477 /// caixa-helm `build_chart_yaml` `caixa.repositorio.clone()`
478 /// `Chart.yaml` `home:` field fold, the caixa-helm `build_readme`
479 /// `caixa.repositorio.clone().unwrap_or_else(|| caixa.nome.clone())`
480 /// `README.md` `## Source` fold, and the caixa-flux
481 /// `ClusterBundleOpts::for_caixa`
482 /// `caixa.repositorio.clone().unwrap_or_else(|| format!(...))`
483 /// `GitRepository.spec.url` fold — four open-coded field-accesses
484 /// that expressed no compile-time link back to the typed slot. A
485 /// future extension of the `:repositorio` axis to a richer author
486 /// surface — a per-`:repositorio` structured
487 /// [`crate::render::GitRepoUrl`]-shaped scheme+host+path parse
488 /// (the future tightening [`Self::validate_repositorio`]'s
489 /// docstring anticipates alongside the peer per-`:deps :fonte
490 /// :repo` axis), a per-cluster repo-mirror overlay the M4 CR
491 /// materializer resolves per-CR (the "cluster policy rewrites
492 /// `github:pleme-io/...` to `git.internal/mirror/pleme-io/...`"
493 /// arm the private-registry story acknowledges), a promotion of
494 /// the plain `Option<String>` byte-string to a richer
495 /// `RepoUrl` enum discriminated on scheme — would have had to be
496 /// threaded through all four open-coded copies in lockstep or the
497 /// validate gate and the three emit paths would silently disagree
498 /// on which URL a given [`Caixa`] resolves to (an author's
499 /// `:repositorio "github:pleme-io/checkout"` would satisfy validate
500 /// while one of the emit paths silently rendered a stale URL, or
501 /// vice versa). Lifting the resolution to a typed method on the
502 /// substrate primitive means every downstream consumer of the
503 /// caixa's per-`Caixa` repo-URL surface reaches for exactly one
504 /// typed dispatch — the resolver's accept-set migrates as a unit on
505 /// any future axis addition.
506 ///
507 /// Second outer top-level [`Caixa`] `Option<&str>`-return scalar
508 /// accessor — sibling of [`Self::licenca`] (6d5bc28), the accessor
509 /// that opened the "outer [`Caixa`] `Option<&str>` scalar"
510 /// projection pattern this lift folds on. Same "one typed dispatch
511 /// on the substrate primitive, thin projections at each consumer"
512 /// discipline the peer per-`:placement`
513 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
514 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
515 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
516 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
517 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
518 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
519 /// typed-slot atom axes, extended here to the second outer top-level
520 /// `Caixa` universal-axis surface. Named `repositorio()` to match
521 /// the storage field's name; the accessor's identity maps onto the
522 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
523 /// carries.
524 #[must_use]
525 pub fn repositorio(&self) -> Option<&str> {
526 self.repositorio.as_deref()
527 }
528
529 /// Substrate-canonical per-`Caixa` `:descricao` free-form-prose
530 /// chart-description scalar accessor every consumer of the top-level
531 /// manifest's Chart.yaml `description:` axis keys off — returns the
532 /// author-declared `:descricao` byte-string verbatim as an
533 /// `Option<&str>`, borrowed from the typed slot's own
534 /// `Option<String>` storage. `None` when the slot is absent (the
535 /// canonical "omit to defer to the per-renderer `caixa.nome`-derived
536 /// fallback" shape — [`caixa-helm`]'s `build_chart_yaml` folds the
537 /// omitted slot through a `format!("Generated chart for caixa Servico
538 /// {}", caixa.nome)` fallback, [`caixa-helm`]'s `build_readme` folds
539 /// it through a `format!("caixa Servico {}", caixa.nome)` fallback,
540 /// and [`caixa-feira`]'s `render_flake` folds it through a
541 /// `format!("caixa {}", c.nome)` `flake.nix` `description = ""`
542 /// fallback — each derived from `caixa.nome` on the null-carrier arm).
543 ///
544 /// The `:descricao` slot carries the universal-axis free-form-prose
545 /// chart-description identifier every kind of caixa emits under
546 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa` form
547 /// supplies) — the typed slot's `Option<String>` accept-set
548 /// (empty-string rejected through [`ManifestError::DescricaoEmpty`],
549 /// chart-description-shape-invalid rejected through
550 /// [`ManifestError::DescricaoInvalid`] past the shared
551 /// [`crate::render::is_chart_description_shape`] predicate the peer
552 /// per-`Caixa` `:descricao` axis also routes through) maps onto four
553 /// load-bearing downstream consumers:
554 ///
555 /// - [`Self::validate_descricao`]'s empty-arm + shape-predicate
556 /// gate binding — the universal-axis identity gate wired at
557 /// caixa-build time.
558 /// - [`caixa-helm`]'s `build_chart_yaml` `ChartYaml.description`
559 /// `Chart.yaml` field fold — the rendered `lareira-<nome>` Helm
560 /// chart's `Chart.yaml` `description:` field, which
561 /// `apiVersion: v2` charts require non-empty (`helm lint` fires
562 /// `WARNING [chart.metadata.description]: description is required`
563 /// when absent) and which every registry that ingests the chart
564 /// (ArtifactHub, chartmuseum, `helm search repo`) surfaces as the
565 /// chart's canonical one-line prose descriptor.
566 /// - [`caixa-helm`]'s `build_readme` chart-`README.md` header fold
567 /// — the rendered `lareira-<nome>` chart's `README.md` prose
568 /// header directly beneath the `# <chart-name>` title, which
569 /// every author who inspects the rendered chart bundle lands at.
570 /// - [`caixa-feira`]'s `render_flake` `flake.nix` `description = ""`
571 /// top-level fold — the emitted `flake.nix`'s `description`
572 /// field, which every Nix consumer (`nix flake show`,
573 /// `nix flake metadata`, downstream flake-registry ingestors)
574 /// surfaces as the flake's canonical descriptor.
575 ///
576 /// Prior to this lift the `.descricao` field was accessed inline at
577 /// four production sites — [`Self::validate_descricao`]'s
578 /// `self.descricao.as_deref()` empty-and-shape gate binding, the
579 /// caixa-helm `build_chart_yaml`
580 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
581 /// `Chart.yaml` `description:` fold, the caixa-helm `build_readme`
582 /// `caixa.descricao.clone().unwrap_or_else(|| format!(...))`
583 /// `README.md` header fold, and the caixa-feira `render_flake`
584 /// `c.descricao.clone().unwrap_or_else(|| format!(...))` `flake.nix`
585 /// `description = ""` fold — four open-coded field-accesses that
586 /// expressed no compile-time link back to the typed slot. A future
587 /// extension of the `:descricao` axis to a richer author surface —
588 /// a per-`:descricao` locale-tagged multi-language descriptor map
589 /// (the "one caixa, N language-tagged prose descriptions" arm
590 /// author-tooling internationalization anticipates), a
591 /// per-registry-target length-and-shape overlay the M4 CR
592 /// materializer resolves per-CR (the "ArtifactHub caps description
593 /// at 512 bytes but the internal registry caps at 256" arm), a
594 /// promotion of the plain `Option<String>` byte-string to a richer
595 /// `ChartDescription` newtype guaranteeing the
596 /// `is_chart_description_shape` predicate at the type level — would
597 /// have had to be threaded through all four open-coded copies in
598 /// lockstep or the validate gate and the three emit paths would
599 /// silently disagree on which prose string a given [`Caixa`]
600 /// resolves to (an author's
601 /// `:descricao "Checkout flow orchestration."` would satisfy
602 /// validate while one of the emit paths silently rendered a stale
603 /// `caixa.nome`-derived fallback, or vice versa). Lifting the
604 /// resolution to a typed method on the substrate primitive means
605 /// every downstream consumer of the caixa's per-`Caixa`
606 /// chart-description surface reaches for exactly one typed dispatch
607 /// — the resolver's accept-set migrates as a unit on any future
608 /// axis addition.
609 ///
610 /// Third outer top-level [`Caixa`] `Option<&str>`-return scalar
611 /// accessor — sibling of [`Self::licenca`] (6d5bc28) and
612 /// [`Self::repositorio`] (cc7332d), the accessors that opened the
613 /// "outer [`Caixa`] `Option<&str>` scalar" projection pattern this
614 /// lift folds on. Same "one typed dispatch on the substrate
615 /// primitive, thin projections at each consumer" discipline the
616 /// peer per-`:placement`
617 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
618 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
619 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
620 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
621 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
622 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
623 /// typed-slot atom axes, extended here to the third outer top-level
624 /// `Caixa` universal-axis surface. Named `descricao()` to match the
625 /// storage field's name; the accessor's identity maps onto the
626 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
627 /// carries. The one remaining universal `Option<String>` slot
628 /// (`:edicao`) folds on this pattern next.
629 #[must_use]
630 pub fn descricao(&self) -> Option<&str> {
631 self.descricao.as_deref()
632 }
633
634 /// Substrate-canonical per-`Caixa` `:edicao` language-edition scalar
635 /// accessor every consumer of the top-level manifest's tatara-lisp
636 /// edition-selector axis keys off — returns the author-declared
637 /// `:edicao` byte-string verbatim as an `Option<&str>`, borrowed from
638 /// the typed slot's own `Option<String>` storage. `None` when the
639 /// slot is absent (the canonical "omit the slot to defer to the
640 /// substrate's default edition" shape every existing
641 /// [`caixa-resolver`] integration test fixture carries via
642 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`;
643 /// the peer [`Self::validate_edicao`] gate is a no-op on the omitted
644 /// arm by construction, so an author-omitted `:edicao` round-trips
645 /// to a build without triggering the year-shape predicate).
646 ///
647 /// The `:edicao` slot carries the universal-axis 4-digit-ASCII-
648 /// decimal-year language-edition identifier every kind of caixa
649 /// emits under (CAIXA-SDLC §I — the author-facing surface every
650 /// `defcaixa` form supplies) — the typed slot's `Option<String>`
651 /// accept-set (empty-string rejected through
652 /// [`ManifestError::EdicaoEmpty`], year-shape-invalid rejected
653 /// through [`ManifestError::EdicaoInvalid`] past the 4-digit-ASCII-
654 /// decimal-year predicate [`Self::validate_edicao`] enforces) maps
655 /// onto one load-bearing downstream consumer today
656 /// ([`Self::validate_edicao`]'s empty-arm + year-shape-predicate
657 /// gate binding at caixa-core/src/manifest.rs:1959) plus every
658 /// future edition-aware substrate consumer the CAIXA-SDLC §I
659 /// roadmap anticipates (the tatara-lisp compiler's macro-surface
660 /// selector every edition-aware build step keys off, the future
661 /// per-edition compatibility-flag overlay the M4 CR materializer
662 /// resolves per-CR, the peer [`Caixa::template`] canonical
663 /// `:edicao "2026"` scaffold every `feira init` emits verbatim,
664 /// and the renderer-side fixtures at `caixa-helm/src/lib.rs:978` /
665 /// `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208` that
666 /// carry `edicao: Some("2026".into())` by construction).
667 ///
668 /// Prior to this lift the `.edicao` field was accessed inline at
669 /// one production site — [`Self::validate_edicao`]'s
670 /// `self.edicao.as_deref()` empty-and-shape gate binding — one
671 /// open-coded field-access that expressed no compile-time link
672 /// back to the typed slot. A future extension of the `:edicao`
673 /// axis to a richer author surface — a per-`:edicao` known-
674 /// edition allowlist (the future tightening
675 /// [`Self::validate_edicao`]'s docstring acknowledges past the
676 /// structural year-shape floor, rejecting year-shaped values that
677 /// don't name a tatara-lisp edition the substrate actually
678 /// understands — `"1999"` is year-shaped but no `1999` edition
679 /// exists), a per-edition compatibility-flag overlay the M4 CR
680 /// materializer resolves per-CR (the "edition `"2026"` enables
681 /// macro-surface features the sibling `"2018"` gates behind a
682 /// feature flag" arm the edition-selector story anticipates), a
683 /// promotion of the plain `Option<String>` byte-string to a
684 /// richer `CaixaEdition` enum discriminated on year once a sibling
685 /// edition to `"2026"` lands — would have had to be threaded
686 /// through the open-coded copy in lockstep with every future
687 /// edition-aware consumer, or the validate gate and the future
688 /// edition-aware consumer path would silently disagree on which
689 /// edition a given [`Caixa`] resolves to (an author's
690 /// `:edicao "2026"` would satisfy validate while a future
691 /// edition-aware consumer silently defaulted to a stale edition,
692 /// or vice versa). Lifting the resolution to a typed method on
693 /// the substrate primitive means every downstream consumer of the
694 /// caixa's per-`Caixa` edition surface reaches for exactly one
695 /// typed dispatch — the resolver's accept-set migrates as a unit
696 /// on any future axis addition.
697 ///
698 /// Fourth and final outer top-level [`Caixa`] `Option<&str>`-return
699 /// scalar accessor — sibling of [`Self::licenca`] (6d5bc28),
700 /// [`Self::repositorio`] (cc7332d), and [`Self::descricao`]
701 /// (3f16e2f), the accessors that opened the "outer [`Caixa`]
702 /// `Option<&str>` scalar" projection pattern this lift folds on.
703 /// Same "one typed dispatch on the substrate primitive, thin
704 /// projections at each consumer" discipline the peer per-`:placement`
705 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
706 /// [`crate::aplicacao::Placement::affinity`] (74ec2d3) /
707 /// per-`:contratos` [`crate::aplicacao::WitContract::endpoint`]
708 /// (7020470) / [`crate::aplicacao::WitContract::subject`] (90de675)
709 /// / [`crate::aplicacao::WitContract::slot`] (ed22b66)
710 /// `Option<&str>`-return accessors carry on the sibling M2 / M3
711 /// typed-slot atom axes, extended here to close the outer top-level
712 /// `Caixa` universal-axis surface's last unlifted `Option<String>`
713 /// slot. Named `edicao()` to match the storage field's name; the
714 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
715 /// vocabulary the slot's docstring already carries.
716 #[must_use]
717 pub fn edicao(&self) -> Option<&str> {
718 self.edicao.as_deref()
719 }
720
721 /// Substrate-canonical per-`Caixa` `:nome` universal-axis DNS-1123-
722 /// label caixa-identity scalar accessor every consumer of the top-
723 /// level manifest's identity axis keys off — returns the author-
724 /// declared `:nome` byte-string verbatim as an `&str`, borrowed from
725 /// the typed slot's own `String` storage. Non-optional (`:nome` is
726 /// a required-axis scalar every `defcaixa` form must supply; the
727 /// [`Self::from_lisp`] derive rejects an omitted / non-string
728 /// `:nome` at parse time, so a `Caixa` past parse definitionally
729 /// carries a non-`None` `:nome`).
730 ///
731 /// The `:nome` slot carries the universal-axis DNS-1123-label
732 /// caixa-identity every kind of caixa emits under (CAIXA-SDLC §I —
733 /// the primary identity axis every `defcaixa` form supplies
734 /// alongside `:versao` / `:kind`; the substrate-wide identity every
735 /// other typed surface that names a caixa reaches through — `:deps`
736 /// entries, `:membros` entries, `:children` entries, the
737 /// `lareira-<nome>` Helm chart name every per-Servico renderer
738 /// derives, the `pleme-program-<nome>` label every per-Aplicacao
739 /// renderer emits) — the typed slot's `String` accept-set (empty
740 /// rejected through [`ManifestError::NomeEmpty`], DNS-1123-shape-
741 /// invalid rejected through [`ManifestError::NomeInvalid`] past
742 /// the shared [`crate::render::require_valid_dns_1123_label`] gate
743 /// the peer name axes each land on, joint-length-with-`lareira-`-
744 /// prefix rejected through
745 /// [`ManifestError::NomeChartNameBudgetExceeded`] past
746 /// [`crate::render::is_lareira_chart_name_shape`]) maps onto every
747 /// load-bearing downstream consumer the substrate carries — the
748 /// two universal-axis validate gates at caixa-build time
749 /// ([`Self::validate_nome`] + [`Self::validate_nome_chart_name_budget`]),
750 /// [`crate::lareira_chart_name`]'s `lareira-<nome>` Helm chart-name
751 /// derivation every per-Servico renderer keys off, the caixa-helm
752 /// `Chart.yaml`'s `name:` axis, caixa-flux's `programs.yaml` entry
753 /// `name:` axis, caixa-mesh's Cilium `CiliumNetworkPolicy` /
754 /// `HTTPRoute` per-Aplicacao name axes at
755 /// caixa-mesh/src/lib.rs:{2650, 2797, 2919, 2925},
756 /// [`crate::pleme_program_selector`] /
757 /// [`crate::pleme_program_in_aplicacao_selector`] label-selector
758 /// derivations, and every future substrate renderer that emits an
759 /// artifact keyed by the caixa's identity.
760 ///
761 /// Prior to this lift the `.nome` field was accessed inline at a
762 /// dozen production sites across `caixa-core` (the two universal-
763 /// axis validate gates + [`Dep::validate`]-adjacent duplicate
764 /// tracking), `caixa-helm` (the `lareira_chart_name` fold, the
765 /// `ChartYaml.name` / `ChartYaml.description` / `Chart.yaml`
766 /// `keywords` fallback), `caixa-flux` (the `programs.yaml`
767 /// entry `name:` fold, the `flux_kustomization_source_subtree`
768 /// per-cluster subpath derivation), and `caixa-mesh` (the
769 /// `pleme_program_in_aplicacao_selector` label-selector fold, the
770 /// `cilium_network_policy_name` / `gateway_api_http_route_name`
771 /// per-CR name derivations, the `LABEL_APLICACAO` labels-map
772 /// insert) — a dozen open-coded field-accesses that expressed no
773 /// compile-time link back to the typed slot. A future extension of
774 /// the `:nome` axis to a richer author surface — a per-`:nome`
775 /// structured `CaixaIdentity` newtype that carries the joint-
776 /// length-with-prefix invariant [`Self::validate_nome_chart_name_budget`]
777 /// enforces at the type level (rather than as a validate-time
778 /// gate), a per-registry `:nome` namespacing overlay the M4 CR
779 /// materializer resolves per-CR (the "`pleme-io/checkout` vs
780 /// `partner-org/checkout` collision" arm the multi-tenant-registry
781 /// story acknowledges), a promotion of the plain `String` byte-
782 /// string to a richer `CaixaNome` newtype discriminated on
783 /// namespace prefix — would have had to be threaded through every
784 /// open-coded copy in lockstep or the two validate gates and the
785 /// dozen emit paths would silently disagree on which identity a
786 /// given [`Caixa`] resolves to (an author's `:nome "checkout"`
787 /// would satisfy validate while one of the emit paths silently
788 /// rendered a drifted other identity, or vice versa). Lifting the
789 /// resolution to a typed method on the substrate primitive means
790 /// every downstream consumer of the caixa's per-`Caixa` identity
791 /// surface reaches for exactly one typed dispatch — the resolver's
792 /// accept-set migrates as a unit on any future axis addition.
793 ///
794 /// First outer top-level [`Caixa`] `&str`-return required-scalar
795 /// accessor — opens the "outer [`Caixa`] `&str` required-scalar"
796 /// projection pattern the sibling per-`Caixa` `:versao` future lift
797 /// folds on. Sibling in shape to the peer per-`:membros`
798 /// [`crate::aplicacao::Membro::nome`] (4a32abf) / per-`:contratos`
799 /// [`crate::aplicacao::WitContract::source`] /
800 /// [`crate::aplicacao::WitContract::destination`] (7f0fd43),
801 /// [`crate::aplicacao::WitContract::world_ref`] (0804823),
802 /// [`crate::aplicacao::Membro::versao_requirement`] (a40b0e3),
803 /// [`crate::aplicacao::Entrada::destination`] (6db982c),
804 /// [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062),
805 /// per-sub-struct required-axis accessors carry on the sibling M3
806 /// mesh-slot-atom scalar-value axes, extended here to open the
807 /// outer top-level [`Caixa`] `&str`-return required-scalar surface.
808 /// Named `nome()` to match the storage field's name; the accessor's
809 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
810 /// slot's docstring already carries.
811 #[must_use]
812 pub fn nome(&self) -> &str {
813 &self.nome
814 }
815
816 /// Substrate-canonical per-`Caixa` `:versao` universal-axis SemVer-2
817 /// pinned-version scalar accessor every consumer of the top-level
818 /// manifest's version axis keys off — returns the author-declared
819 /// `:versao` byte-string verbatim as an `&str`, borrowed from the
820 /// typed slot's own `String` storage. Non-optional (`:versao` is a
821 /// required-axis scalar every `defcaixa` form must supply alongside
822 /// `:nome` / `:kind`; the [`Self::from_lisp`] derive rejects an
823 /// omitted / non-string `:versao` at parse time, so a `Caixa` past
824 /// parse definitionally carries a non-`None` `:versao`).
825 ///
826 /// The `:versao` slot carries the universal-axis SemVer-2
827 /// concrete-version body every kind of caixa emits under
828 /// (CAIXA-SDLC §I — the required-scalar every `defcaixa` form
829 /// supplies alongside `:nome` / `:kind`; the substrate-wide
830 /// pinned-version every downstream artifact-emitting consumer
831 /// composes under — the `lareira-<nome>` Helm chart's `Chart.yaml`
832 /// `version:` + `appVersion:` axes, the `feira publish` Zig-style
833 /// `v<versao>` git tag the [`crate::DEFAULT_PUBLISH_TAG_PREFIX`]
834 /// prefix composes on top of, the programs.yaml entry's `versao:`
835 /// value the `lareira-fleet-programs` aggregator carries onto each
836 /// rendered `ComputeUnit`, the OCI image's `:v<versao>` / `:latest`
837 /// tags every substrate-side `skopeo push` writes, the lacre
838 /// closure's pinned `concrete_versao`, and the `:upgrade-from :from`
839 /// prior-version references peers in the exact same SemVer-2 shape).
840 /// The typed slot's `String` accept-set (empty rejected through
841 /// [`ManifestError::VersaoEmpty`], SemVer-2-shape-invalid rejected
842 /// through [`ManifestError::VersaoInvalid`] past
843 /// [`semver::Version::parse`]) maps onto every load-bearing
844 /// downstream consumer the substrate carries — the [`Self::validate_versao`]
845 /// universal-axis validate gate at caixa-build time, the
846 /// [`crate::CaixaVersion::parse`] typed-wrapper resolver,
847 /// [`caixa-helm`]'s `Chart.yaml` `version:` / `appVersion:` fold,
848 /// [`caixa-flux`]'s `programs.yaml` entry `versao:` fold + the
849 /// `cluster_bundle` `GitRepository` `ref: { tag: v<versao> }`
850 /// derivation, [`caixa-mesh`]'s per-Aplicacao `programs.yaml` fan-
851 /// out entry `versao:` fold, [`caixa-feira`]'s `feira publish` git-
852 /// tag derivation (`format!("{prefix}{versao}")`), and every future
853 /// substrate renderer that emits an artifact keyed by the caixa's
854 /// pinned version.
855 ///
856 /// Prior to this lift the `.versao` field was accessed inline at a
857 /// dozen production sites across `caixa-core` (the universal-axis
858 /// [`Self::validate_versao`] gate + [`Dep::validate`]-adjacent
859 /// version-shape gates), `caixa-helm` (the `ChartYaml.version` /
860 /// `ChartYaml.app_version` folds), `caixa-flux` (the `programs.yaml`
861 /// entry `versao:` fold, the `cluster_bundle` `GitRepository` `ref:
862 /// { tag: v<versao> }` derivation), `caixa-mesh` (the per-Aplicacao
863 /// `programs.yaml` fan-out entry `versao:` fold), and `caixa-feira`
864 /// (the `feira publish` git-tag derivation + the `feira app graph` /
865 /// `feira app deploy` diagnostic renderers) — a dozen open-coded
866 /// field-accesses that expressed no compile-time link back to the
867 /// typed slot. A future extension of the `:versao` axis to a richer
868 /// author surface — a per-`:versao` structured `CaixaVersion` at the
869 /// storage layer (the substrate already carries a `CaixaVersion`
870 /// newtype at [`crate::version::CaixaVersion`], deferred until the
871 /// serde-transparent-newtype-through-DeriveTataraDomain path lands),
872 /// a per-registry `:versao` immutability overlay the M4 CR
873 /// materializer enforces per-CR, a promotion of the plain `String`
874 /// byte-string to a richer `PinnedVersao` newtype discriminated on
875 /// SemVer-2 pre-release / build-metadata presence — would have had
876 /// to be threaded through every open-coded copy in lockstep or the
877 /// validate gate and the dozen emit paths would silently disagree
878 /// on which version a given [`Caixa`] resolves to (an author's
879 /// `:versao "0.1.0"` would satisfy validate while one of the emit
880 /// paths silently rendered a drifted other version, or vice versa).
881 /// Lifting the resolution to a typed method on the substrate
882 /// primitive means every downstream consumer of the caixa's
883 /// per-`Caixa` pinned-version surface reaches for exactly one typed
884 /// dispatch — the resolver's accept-set migrates as a unit on any
885 /// future axis addition.
886 ///
887 /// Second outer top-level [`Caixa`] `&str`-return required-scalar
888 /// accessor — folds on the "outer [`Caixa`] `&str` required-scalar"
889 /// projection pattern the sibling per-`Caixa` [`Self::nome`]
890 /// (e6b7d97) opened. Sibling in shape to the peer per-`:membros`
891 /// [`crate::aplicacao::Membro::versao_requirement`] (4127bb6) /
892 /// per-`:children` [`crate::supervisor::ChildSpec::versao_requirement`]
893 /// (2c053c8) / per-`:upgrade-from` [`crate::UpgradeFromEntry::prior_versao`]
894 /// (75d27a8) per-sub-struct `:versao`-shaped `&str`-return accessors
895 /// on the sibling per-typed-slot version-carrier axes, extended here
896 /// to close the second outer top-level [`Caixa`] required-`&str`-
897 /// carrying axis so the two universal-axis identity-carrying
898 /// scalars every `defcaixa` form supplies (`:nome` + `:versao`)
899 /// share the same "one typed dispatch per axis" discipline. Named
900 /// `versao()` to match the storage field's name; the accessor's
901 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
902 /// slot's docstring already carries.
903 #[must_use]
904 pub fn versao(&self) -> &str {
905 &self.versao
906 }
907
908 /// Substrate-canonical per-`Caixa` `:kind` universal-axis
909 /// closed-set-enum discriminant accessor every consumer of the top-
910 /// level manifest's kind axis keys off — returns the author-declared
911 /// `:kind` variant verbatim as a [`CaixaKind`], `Copy`-projected
912 /// from the typed slot's own [`CaixaKind`] storage. Non-optional
913 /// (`:kind` is a required-axis discriminant every `defcaixa` form
914 /// must supply alongside `:nome` / `:versao`; the [`Self::from_lisp`]
915 /// derive rejects an omitted / non-symbol `:kind` at parse time, so
916 /// a `Caixa` past parse definitionally carries a valid [`CaixaKind`]
917 /// variant).
918 ///
919 /// The `:kind` slot carries the universal-axis closed-set typed-
920 /// discriminant every substrate-side dispatch keys off (CAIXA-SDLC
921 /// §I — the primary shape gate every renderer / verifier /
922 /// operator branches on; the five variants `Biblioteca` /
923 /// `Binario` / `Servico` / `Supervisor` / `Aplicacao` partition
924 /// the caixa surface into disjoint runtime contracts) — the typed
925 /// slot's [`CaixaKind`] accept-set (parse-time-rejected non-symbol
926 /// values through the derive-macro's symbol-arm gate, exhaustively
927 /// matched at every downstream dispatch site) maps onto every
928 /// load-bearing downstream consumer the substrate carries:
929 ///
930 /// - [`crate::render::require_kind`]'s per-renderer entry-gate
931 /// predicate — the canonical two-line
932 /// `require_kind(caixa, Servico)?` prelude every per-Servico
933 /// renderer (`caixa-helm`, `caixa-flux`, the future `caixa-otel`
934 /// / per-Servico OCI packager / M4 `wasm.pleme.io/v1alpha1/
935 /// ComputeUnit` CR materializer) runs at its entry-point,
936 /// alongside the [`crate::render::KindMismatch`] error carrier's
937 /// `actual:` field the diagnostic surfaces to name the offending
938 /// caixa's variant.
939 /// - [`Self::aplicacao_view`]'s + [`Self::supervisor_view`]'s
940 /// per-view kind-gate binding — the two `Option<TypedSpec>`
941 /// `_view` composers that fold the flat mesh-slot / supervisor-
942 /// slot columns into their typed sub-spec only when the kind
943 /// matches (returns `None` otherwise); the future per-Servico
944 /// M2-view composer (`servico_view`) will follow the same shape.
945 /// - [`Self::declared_foreign_code_slots`]'s per-slot kind-
946 /// coherence gate — the `!self.kind.requires_exe()` /
947 /// `!self.kind.requires_servicos()` predicates that fence
948 /// each code-surface slot from the wrong owning kind.
949 /// - [`crate::LayoutInvariants::verify`]'s kind ↔ code-surface
950 /// coherence gates — the six `caixa.kind == CaixaKind::X` /
951 /// `caixa.kind != CaixaKind::X` predicates and the four kind-
952 /// coherence error carriers (`SupervisorOwnsCode` /
953 /// `AplicacaoOwnsCode` / `MeshSlotsOnNonAplicacao` /
954 /// `SupervisorSlotsOnNonSupervisor` / `ServicoSlotsOnNonServico`
955 /// / `ForeignCodeSlot`) which each name the offending caixa's
956 /// variant in their `kind:` field.
957 ///
958 /// Prior to this lift the `.kind` field was accessed inline at
959 /// twenty-plus production sites across `caixa-core` (the
960 /// [`crate::render::require_kind`] entry-gate predicate + the
961 /// [`crate::render::KindMismatch`] `actual:` field, the two `_view`
962 /// composers, the `declared_foreign_code_slots` per-slot kind-
963 /// coherence gate, and the six [`crate::LayoutInvariants::verify`]
964 /// kind ↔ code-surface predicates + four error carriers) — a score
965 /// of open-coded field-accesses that expressed no compile-time link
966 /// back to the typed slot. A future extension of the `:kind` axis
967 /// to a richer author surface — a per-`:kind` sub-variant discriminant
968 /// (e.g. `Servico(ServicoRuntime)` splitting the current single
969 /// variant across the wasm-component / legacy-container / native-
970 /// binary runtime axes the M5 roadmap acknowledges), a per-cluster
971 /// kind-overlay the M4 CR materializer resolves per-CR (the
972 /// "cluster policy demotes `Aplicacao` to `Servico` on a single-
973 /// tenant cluster" arm), a promotion of the plain [`CaixaKind`]
974 /// enum to a richer `KindWithRuntime` discriminated on the
975 /// component-model world axis — would have had to be threaded
976 /// through every open-coded copy in lockstep or the entry gate,
977 /// the view composers, and the layout invariants would silently
978 /// disagree on which kind a given [`Caixa`] resolves to. Lifting
979 /// the resolution to a typed method on the substrate primitive
980 /// means every downstream consumer of the caixa's per-`Caixa`
981 /// kind surface reaches for exactly one typed dispatch — the
982 /// resolver's accept-set migrates as a unit on any future axis
983 /// addition.
984 ///
985 /// First outer top-level [`Caixa`] `Copy`-return required-enum-
986 /// discriminant accessor — opens the "outer [`Caixa`] `Copy`-return
987 /// required-discriminant" projection pattern. Sibling in shape to
988 /// the peer per-`:supervisor` [`crate::supervisor::SupervisorSpec::estrategia`]
989 /// (eafb619), per-`:placement` [`crate::aplicacao::Placement::estrategia`]
990 /// (921fe1b), and per-`:children` [`crate::supervisor::ChildSpec::restart`]
991 /// (dfb4a81) `Copy`-return closed-set-enum discriminant accessors
992 /// on the sibling nested-spec typed-slot discriminator axes,
993 /// extended here to the outer top-level [`Caixa`] universal-axis
994 /// surface. Named `kind()` to match the storage field's name;
995 /// the accessor's identity maps onto the canonical CAIXA-SDLC §I
996 /// vocabulary the slot's docstring already carries.
997 #[must_use]
998 pub fn kind(&self) -> CaixaKind {
999 self.kind
1000 }
1001
1002 /// Substrate-canonical per-`Caixa` `:autores` universal-axis
1003 /// maintainer-name-list slice-accessor every consumer of the top-
1004 /// level manifest's maintainer axis keys off — returns the author-
1005 /// declared `:autores` list verbatim as a `&[String]` slice-view over
1006 /// the same backing buffer the raw `self.autores.as_slice()` field
1007 /// access borrows from. Empty-list-carrying (`:autores` is a default-
1008 /// empty axis every `defcaixa` form supplies with an empty `()` when
1009 /// unset; the [`Self::from_lisp`] derive folds an omitted `:autores`
1010 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1011 /// parse definitionally carries a `Vec<String>` slot — possibly
1012 /// empty — and the returned `&[String]` degenerates to an empty
1013 /// slice on that arm without any silent `None` collapse).
1014 ///
1015 /// The `:autores` slot carries the universal-axis maintainer-name
1016 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1017 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1018 /// `:versao` / `:kind`; the substrate-wide contact-carrying axis
1019 /// every downstream registry-facing artifact emits under) — the
1020 /// typed slot's `Vec<String>` accept-set (empty-per-entry rejected
1021 /// through [`ManifestError::AutorEmpty`], non-chart-maintainer-shape
1022 /// rejected through [`ManifestError::AutorInvalid`], cross-entry
1023 /// duplicate rejected through [`ManifestError::AutorDuplicate`]) maps
1024 /// onto every load-bearing downstream consumer the substrate carries
1025 /// — the [`Self::validate_autores`] universal-axis empty-per-entry +
1026 /// shape + duplicate gate at caixa-core/src/manifest.rs, the
1027 /// caixa-helm `build_chart_yaml` `maintainers:` fold at
1028 /// caixa-helm/src/lib.rs that walks each entry into a `Maintainer {
1029 /// name, email: None }` record, every future per-`Caixa` registry-
1030 /// facing renderer the CAIXA-SDLC §I roadmap acknowledges (the
1031 /// future `artifacthub.io/maintainers` `Chart.yaml` annotation the
1032 /// caixa-helm docstring alludes to at [`Self::validate_licenca`],
1033 /// the future per-cluster author-notification overlay the M4 CR
1034 /// materializer resolves per-CR).
1035 ///
1036 /// Prior to this lift the `.autores` field was accessed inline at
1037 /// two production sites — [`Self::validate_autores`]'s `for autor
1038 /// in &self.autores` walk that gates every entry through
1039 /// [`ManifestError::AutorEmpty`] / `AutorInvalid` / `AutorDuplicate`,
1040 /// and the caixa-helm `build_chart_yaml` `caixa.autores.iter().map(|a|
1041 /// Maintainer { name: a.clone(), email: None }).collect()` fold that
1042 /// materializes every entry into a `Chart.yaml` `maintainers:` row —
1043 /// two open-coded field-accesses that expressed no compile-time link
1044 /// back to the typed slot. A future extension of the `:autores` axis
1045 /// to a richer author surface — a per-`:autores` structured
1046 /// `Maintainer { name, email, url }` at the storage layer once the
1047 /// substrate absorbs `artifacthub.io/maintainers`' name+email+url
1048 /// tuple, a per-registry `:autores` allowlist the M4 CR materializer
1049 /// enforces per-CR (the "cluster policy demands every author declare
1050 /// an on-file `mailto:` contact" arm), a promotion of the plain
1051 /// `Vec<String>` byte-string list to a richer
1052 /// `Vec<ChartMaintainer>` newtype discriminated on the RFC-5322
1053 /// `<name> [<email>]` grammar the `is_chart_maintainer_name_shape`
1054 /// predicate already resolves through — would have had to be
1055 /// threaded through both open-coded copies in lockstep or the
1056 /// validate gate and the caixa-helm emit path would silently
1057 /// disagree on which authors a given [`Caixa`] resolves to (an
1058 /// author's `:autores ("alice" "bob")` would satisfy validate while
1059 /// the caixa-helm emit path silently rendered a drifted other
1060 /// maintainer list, or vice versa). Lifting the resolution to a
1061 /// typed method on the substrate primitive means every downstream
1062 /// consumer of the caixa's per-`Caixa` maintainer surface reaches
1063 /// for exactly one typed dispatch — the resolver's accept-set
1064 /// migrates as a unit on any future axis addition.
1065 ///
1066 /// First outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1067 /// opens the "outer [`Caixa`] `&[T]` slice" projection pattern the
1068 /// sibling per-`Caixa` `:etiquetas` / `:deps` / `:deps-dev` / `:exe`
1069 /// / `:bibliotecas` / `:servicos` / `:upgrade-from` / `:children`
1070 /// future lifts fold on. Sibling in shape to the peer per-`:supervisor`
1071 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce), per-`:placement`
1072 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7), per-`:membros`
1073 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36), per-`:contratos`
1074 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1075 /// per-`:upgrade-from :instructions` [`crate::upgrade::UpgradeFromEntry::instructions`]
1076 /// (0137e5a) `&[T]`-return slice accessors on the sibling per-M2 /
1077 /// per-M3 typed-slot list axes, extended here to the outer top-level
1078 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1079 /// `&Vec<String>`) because every downstream consumer of the author
1080 /// list treats it as a read-only sequence — the slice-view is the
1081 /// narrowest borrow that supports every present + roadmapped consumer
1082 /// (`.iter()`, `.len()`, `.is_empty()`) without leaking the backing
1083 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
1084 /// reaches for (the storage-side `Vec` remains reachable through the
1085 /// `pub autores` field for the mutation-carrying serde round-trip and
1086 /// per-test fixture-mutation paths). Named `autores()` to match the
1087 /// storage field's name; the accessor's identity maps onto the
1088 /// canonical CAIXA-SDLC §I vocabulary the slot's docstring already
1089 /// carries.
1090 #[must_use]
1091 pub fn autores(&self) -> &[String] {
1092 self.autores.as_slice()
1093 }
1094
1095 /// Substrate-canonical per-`Caixa` `:etiquetas` universal-axis
1096 /// registry-search-tag-list slice-accessor every consumer of the
1097 /// top-level manifest's topical-tag axis keys off — returns the
1098 /// author-declared `:etiquetas` list verbatim as a `&[String]`
1099 /// slice-view over the same backing buffer the raw
1100 /// `self.etiquetas.as_slice()` field access borrows from. Empty-
1101 /// list-carrying (`:etiquetas` is a default-empty axis every
1102 /// `defcaixa` form supplies with an empty `()` when unset; the
1103 /// [`Self::from_lisp`] derive folds an omitted `:etiquetas` through
1104 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1105 /// definitionally carries a `Vec<String>` slot — possibly empty —
1106 /// and the returned `&[String]` degenerates to an empty slice on
1107 /// that arm without any silent `None` collapse).
1108 ///
1109 /// The `:etiquetas` slot carries the universal-axis topical-tag
1110 /// list every kind of caixa emits under (CAIXA-SDLC §I — the
1111 /// author-facing surface every `defcaixa` form supplies alongside
1112 /// `:nome` / `:versao` / `:kind`; the substrate-wide registry-
1113 /// search-facing axis every downstream registry-facing artifact
1114 /// emits under) — the typed slot's `Vec<String>` accept-set
1115 /// (empty-per-entry rejected through [`ManifestError::EtiquetaEmpty`],
1116 /// non-chart-keyword-shape rejected through
1117 /// [`ManifestError::EtiquetaInvalid`], cross-entry duplicate
1118 /// rejected through [`ManifestError::EtiquetaDuplicate`]) maps onto
1119 /// every load-bearing downstream consumer the substrate carries —
1120 /// the [`Self::validate_etiquetas`] universal-axis empty-per-entry
1121 /// + shape + duplicate gate at caixa-core/src/manifest.rs, the
1122 /// caixa-helm `build_chart_yaml` `keywords:` fold at
1123 /// caixa-helm/src/lib.rs that walks each entry into the rendered
1124 /// `Chart.yaml` `keywords:` array (chained with the
1125 /// [`crate::LAREIRA_CHART_KEYWORDS`] substrate-wide floor set and
1126 /// dedup'd through a `BTreeSet` at emit time), every future per-
1127 /// `Caixa` registry-facing renderer the CAIXA-SDLC §I roadmap
1128 /// acknowledges (the future `artifacthub.io/keywords` `Chart.yaml`
1129 /// annotation, the future per-cluster tag-notification overlay the
1130 /// M4 CR materializer resolves per-CR).
1131 ///
1132 /// Prior to this lift the `.etiquetas` field was accessed inline at
1133 /// two production sites — [`Self::validate_etiquetas`]'s `for
1134 /// etiqueta in &self.etiquetas` walk that gates every entry through
1135 /// [`ManifestError::EtiquetaEmpty`] / `EtiquetaInvalid` /
1136 /// `EtiquetaDuplicate`, and the caixa-helm `build_chart_yaml`
1137 /// `caixa.etiquetas.iter().cloned().chain(...)` fold that
1138 /// materializes every entry into a `Chart.yaml` `keywords:` row —
1139 /// two open-coded field-accesses that expressed no compile-time
1140 /// link back to the typed slot. A future extension of the
1141 /// `:etiquetas` axis to a richer tag surface — a per-`:etiquetas`
1142 /// structured `ChartKeyword { name, uri, category }` at the storage
1143 /// layer once the substrate absorbs `artifacthub.io/keywords`
1144 /// richer tag tuple, a per-registry `:etiquetas` allowlist the M4
1145 /// CR materializer enforces per-CR (the "cluster policy demands
1146 /// every tag come from a substrate-approved taxonomy" arm), a
1147 /// promotion of the plain `Vec<String>` byte-string list to a
1148 /// richer `Vec<ChartKeyword>` newtype discriminated on the DNS-
1149 /// 1123-label-shaped grammar the `is_chart_keyword_shape` predicate
1150 /// already resolves through — would have had to be threaded through
1151 /// both open-coded copies in lockstep or the validate gate and the
1152 /// caixa-helm emit path would silently disagree on which tags a
1153 /// given [`Caixa`] resolves to (an author's `:etiquetas ("demo"
1154 /// "aplicacao")` would satisfy validate while the caixa-helm emit
1155 /// path silently rendered a drifted other keyword list, or vice
1156 /// versa). Lifting the resolution to a typed method on the
1157 /// substrate primitive means every downstream consumer of the
1158 /// caixa's per-`Caixa` topical-tag surface reaches for exactly one
1159 /// typed dispatch — the resolver's accept-set migrates as a unit
1160 /// on any future axis addition.
1161 ///
1162 /// Second outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1163 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1164 /// [`Self::autores`] (b5d813f) opened, sibling in shape and
1165 /// idiom. The remaining unlifted outer-`Caixa` slice-carrying axes
1166 /// (`:deps` / `:deps-dev` / `:exe` / `:bibliotecas` / `:servicos`
1167 /// / `:upgrade-from` / `:children` / `:membros` / `:contratos`)
1168 /// fold onto the same pattern in future lifts. Sibling in shape to
1169 /// the peer per-`:supervisor`
1170 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1171 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1172 /// (a6e18d7), per-`:membros`
1173 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1174 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1175 /// (0dcc926), and per-`:upgrade-from :instructions`
1176 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1177 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1178 /// typed-slot list axes, extended here to the outer top-level
1179 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1180 /// `&Vec<String>`) because every downstream consumer of the tag
1181 /// list treats it as a read-only sequence — the slice-view is the
1182 /// narrowest borrow that supports every present + roadmapped
1183 /// consumer (`.iter()`, `.len()`, `.is_empty()`) without leaking
1184 /// the backing `Vec`'s grow/push/reserve surface no consumer of
1185 /// the typed view reaches for (the storage-side `Vec` remains
1186 /// reachable through the `pub etiquetas` field for the mutation-
1187 /// carrying serde round-trip and per-test fixture-mutation paths).
1188 /// Named `etiquetas()` to match the storage field's name; the
1189 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1190 /// vocabulary the slot's docstring already carries.
1191 #[must_use]
1192 pub fn etiquetas(&self) -> &[String] {
1193 self.etiquetas.as_slice()
1194 }
1195
1196 /// Substrate-canonical per-`Caixa` `:bibliotecas` universal-axis
1197 /// library-source-path-list slice-accessor every consumer of the
1198 /// top-level manifest's Biblioteca-source axis keys off — returns
1199 /// the author-declared `:bibliotecas` list verbatim as a
1200 /// `&[String]` slice-view over the same backing buffer the raw
1201 /// `self.bibliotecas.as_slice()` field access borrows from. Empty-
1202 /// list-carrying (`:bibliotecas` is a default-empty axis every
1203 /// `defcaixa` form supplies with an empty `()` when unset; the
1204 /// [`Self::from_lisp`] derive folds an omitted `:bibliotecas`
1205 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
1206 /// parse definitionally carries a `Vec<String>` slot — possibly
1207 /// empty — and the returned `&[String]` degenerates to an empty
1208 /// slice on that arm without any silent `None` collapse).
1209 ///
1210 /// The `:bibliotecas` slot carries the universal-axis lisp-library
1211 /// entry-path list every `:kind Biblioteca` caixa emits under
1212 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1213 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1214 /// substrate-wide library-carrier axis every downstream
1215 /// authoring-facing consumer keys off) — the typed slot's
1216 /// `Vec<String>` accept-set (empty-per-entry rejected through
1217 /// [`ManifestError::CodePathEmpty { slot: ":bibliotecas" }`],
1218 /// non-sandboxed-relative-shape rejected through
1219 /// [`ManifestError::CodePathShape`], non-`.lisp`-extension rejected
1220 /// through [`ManifestError::CodePathNonLispExtension`], cross-entry
1221 /// duplicate rejected through [`ManifestError::CodePathDuplicate`])
1222 /// maps onto every load-bearing downstream consumer the substrate
1223 /// carries — the [`crate::LayoutInvariants`] Biblioteca-arm
1224 /// empty-check + per-entry file-exists loop at
1225 /// caixa-core/src/layout.rs that gates each entry through
1226 /// [`crate::LayoutError::MissingLib`] / `MissingEntry`, the
1227 /// [`Self::validate_code_paths`] per-slot shape gate at
1228 /// caixa-core/src/manifest.rs that walks each entry through the
1229 /// sandbox-relative / `.lisp`-extension / cross-entry duplicate
1230 /// gates, the `feira build` per-entry `tatara_lisp::read` parse
1231 /// walk at caixa-feira/src/cmd/build.rs that phase-1-checks each
1232 /// declared library file for lexical / structural errors before
1233 /// downstream `importar` resolution, every future per-`Caixa`
1234 /// library-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1235 /// (the future `tatara-lispc` compilation entry the docstring at
1236 /// caixa-feira/src/cmd/build.rs alludes to, the future per-cluster
1237 /// bytecode-caching overlay the M4 CR materializer resolves per-CR,
1238 /// the future `caixa-lsp` per-library semantic-token stream the
1239 /// caixa-lsp docstring roadmaps).
1240 ///
1241 /// Prior to this lift the `.bibliotecas` field was accessed inline
1242 /// at three production sites — [`crate::LayoutInvariants`]'s
1243 /// `caixa.bibliotecas.is_empty()` `MissingLib`-arm gate + `for p
1244 /// in &caixa.bibliotecas` `MissingEntry` walk that gates each
1245 /// declared library path through the on-disk-existence check,
1246 /// the compound-code-path `has_code = !caixa.bibliotecas.is_empty()
1247 /// || !caixa.exe.is_empty() || !caixa.servicos.is_empty()` OR-fold
1248 /// on the [`crate::LayoutError::SupervisorOwnsCode`] /
1249 /// `AplicacaoOwnsCode` kind-coherence gate, and the `feira build`
1250 /// per-entry `for entry in &caixa.bibliotecas` + `caixa.bibliotecas.
1251 /// len()` phase-1 tatara-lispc-precursor parse walk — three open-
1252 /// coded field-accesses that expressed no compile-time link back
1253 /// to the typed slot. A future extension of the `:bibliotecas`
1254 /// axis to a richer library surface — a per-`:bibliotecas`
1255 /// structured `BibliotecaEntry { path, edition, exports }` at the
1256 /// storage layer once the substrate absorbs the per-library
1257 /// language-edition + explicit-exports tuple the tatara-lisp
1258 /// module-system roadmap acknowledges, a per-registry
1259 /// `:bibliotecas` allowlist the M4 CR materializer enforces
1260 /// per-CR (the "cluster policy demands every biblioteca declare
1261 /// its own :edicao" arm), a promotion of the plain `Vec<String>`
1262 /// byte-string list to a richer `Vec<LibraryPath>` newtype
1263 /// discriminated on the `lib/<nome>.lisp`-shape grammar the
1264 /// [`crate::render::is_sandboxed_relative_path`] +
1265 /// [`crate::render::is_lisp_extension`] predicates already resolve
1266 /// through — would have had to be threaded through all three
1267 /// open-coded copies in lockstep or the layout gate, the shape
1268 /// validator, and the `feira build` phase-1 parse walk would
1269 /// silently disagree on which library paths a given [`Caixa`]
1270 /// resolves to (an author's `:bibliotecas ("lib/foo.lisp"
1271 /// "lib/bar.lisp")` would satisfy layout while `feira build`
1272 /// silently parsed a drifted other list, or vice versa). Lifting
1273 /// the resolution to a typed method on the substrate primitive
1274 /// means every downstream consumer of the caixa's per-`Caixa`
1275 /// library-source surface reaches for exactly one typed dispatch
1276 /// — the resolver's accept-set migrates as a unit on any future
1277 /// axis addition.
1278 ///
1279 /// Third outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1280 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1281 /// [`Self::autores`] (b5d813f) opened and [`Self::etiquetas`]
1282 /// (78c7d3c) folded on, sibling in shape and idiom. The remaining
1283 /// unlifted outer-`Caixa` slice-carrying axes (`:deps` /
1284 /// `:deps-dev` / `:exe` / `:servicos` / `:upgrade-from` /
1285 /// `:children` / `:membros` / `:contratos`) fold onto the same
1286 /// pattern in future lifts. Sibling in shape to the peer
1287 /// per-`:supervisor`
1288 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1289 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1290 /// (a6e18d7), per-`:membros`
1291 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1292 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1293 /// (0dcc926), and per-`:upgrade-from :instructions`
1294 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1295 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1296 /// typed-slot list axes, extended here to the outer top-level
1297 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1298 /// `&Vec<String>`) because every downstream consumer of the
1299 /// library-source list treats it as a read-only sequence — the
1300 /// slice-view is the narrowest borrow that supports every
1301 /// present + roadmapped consumer (`.iter()`, `.len()`,
1302 /// `.is_empty()`) without leaking the backing `Vec`'s
1303 /// grow/push/reserve surface no consumer of the typed view
1304 /// reaches for (the storage-side `Vec` remains reachable through
1305 /// the `pub bibliotecas` field for the mutation-carrying serde
1306 /// round-trip and per-test fixture-mutation paths). Named
1307 /// `bibliotecas()` to match the storage field's name; the
1308 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1309 /// vocabulary the slot's docstring already carries.
1310 #[must_use]
1311 pub fn bibliotecas(&self) -> &[String] {
1312 self.bibliotecas.as_slice()
1313 }
1314
1315 /// Substrate-canonical per-`Caixa` `:exe` universal-axis
1316 /// nix-built-executable-entry-path-list slice-accessor every consumer
1317 /// of the top-level manifest's Binario-executable axis keys off —
1318 /// returns the author-declared `:exe` list verbatim as a `&[String]`
1319 /// slice-view over the same backing buffer the raw
1320 /// `self.exe.as_slice()` field access borrows from. Empty-list-
1321 /// carrying (`:exe` is a default-empty axis every `defcaixa` form
1322 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1323 /// derive folds an omitted `:exe` through `#[serde(default)]` to
1324 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1325 /// `Vec<String>` slot — possibly empty — and the returned `&[String]`
1326 /// degenerates to an empty slice on that arm without any silent
1327 /// `None` collapse).
1328 ///
1329 /// The `:exe` slot carries the universal-axis nix-built executable
1330 /// entry-path list every `:kind Binario` caixa emits under
1331 /// (CAIXA-SDLC §I — the author-facing surface every `defcaixa`
1332 /// form supplies alongside `:nome` / `:versao` / `:kind`; the
1333 /// substrate-wide `exe/`-directory-fenced entry-carrier axis every
1334 /// downstream flake-build-facing consumer keys off) — the typed
1335 /// slot's `Vec<String>` accept-set (empty-per-entry rejected
1336 /// through [`ManifestError::CodePathEmpty { slot: ":exe" }`],
1337 /// non-sandboxed-relative-shape rejected through
1338 /// [`ManifestError::CodePathShape`], cross-entry duplicate rejected
1339 /// through [`ManifestError::CodePathDuplicate`], out-of-`exe/`-
1340 /// directory paths rejected past the layout's
1341 /// [`crate::LayoutError::ExeOutsideDir`] `starts_with` fence) maps
1342 /// onto every load-bearing downstream consumer the substrate carries
1343 /// — the [`crate::LayoutInvariants`] Binario-arm empty-check +
1344 /// per-entry file-exists + `exe/`-directory-fence loop at
1345 /// caixa-core/src/layout.rs that gates each entry through
1346 /// [`crate::LayoutError::BinarioWithoutExe`] / `MissingEntry` /
1347 /// `ExeOutsideDir`, the compound `has_code` OR-fold on the
1348 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1349 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1350 /// that fences code-surface slots off from the two no-code kinds,
1351 /// [`Self::declared_foreign_code_slots`]'s `!self.exe.is_empty()`
1352 /// arm on the [`crate::LayoutError::ForeignCodeSlot`] gate that
1353 /// fences the `:exe` code surface off from every non-Binario code-
1354 /// running kind, [`Self::validate_code_paths`]'s per-slot shape gate
1355 /// that walks each entry through the sandbox-relative / cross-entry
1356 /// duplicate gates, every future per-`Caixa` executable-facing
1357 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1358 /// `caixa-flake` per-Binario `packages.<system>.<nome>` derivation
1359 /// entry the caixa-flake docstring roadmaps, the future per-cluster
1360 /// `nix-store` overlay the M4 CR materializer resolves per-CR, the
1361 /// future `feira nix` per-executable Binario-target emit path).
1362 ///
1363 /// Prior to this lift the `.exe` field was accessed inline at three
1364 /// production sites — the compound-code-path `has_code =
1365 /// !caixa.bibliotecas().is_empty() || !caixa.exe.is_empty() ||
1366 /// !caixa.servicos.is_empty()` OR-fold on the
1367 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1368 /// `AplicacaoOwnsCode` kind-coherence gate, the Binario-arm
1369 /// `caixa.exe.is_empty()` [`crate::LayoutError::BinarioWithoutExe`]
1370 /// gate, the per-entry `for p in &caixa.exe`
1371 /// `MissingEntry`/`ExeOutsideDir` walk, and the
1372 /// [`Self::declared_foreign_code_slots`]'s
1373 /// `!self.exe.is_empty()` arm on the `ForeignCodeSlot` gate — four
1374 /// open-coded field-accesses that expressed no compile-time link
1375 /// back to the typed slot. A future extension of the `:exe` axis
1376 /// to a richer executable surface — a per-`:exe` structured
1377 /// `BinarioEntry { path, wrapper, capabilities }` at the storage
1378 /// layer once the substrate absorbs the per-executable
1379 /// nix-wrapper + linux-capabilities tuple the CAIXA-SDLC §I
1380 /// executable roadmap acknowledges, a per-registry `:exe` allowlist
1381 /// the M4 CR materializer enforces per-CR (the "cluster policy
1382 /// demands every Binario declare an explicit `:wrapper`" arm), a
1383 /// promotion of the plain `Vec<String>` byte-string list to a
1384 /// richer `Vec<ExecutablePath>` newtype discriminated on the
1385 /// `exe/<nome>`-shape grammar the layout's `starts_with(exe_dir)`
1386 /// fence already resolves through — would have had to be threaded
1387 /// through all four open-coded copies in lockstep or the layout
1388 /// gate, the shape validator, and the `feira nix` emit path would
1389 /// silently disagree on which executable paths a given [`Caixa`]
1390 /// resolves to (an author's `:exe ("exe/cli" "exe/serve")` would
1391 /// satisfy layout while `feira nix` silently packaged a drifted
1392 /// other list, or vice versa). Lifting the resolution to a typed
1393 /// method on the substrate primitive means every downstream
1394 /// consumer of the caixa's per-`Caixa` executable-source surface
1395 /// reaches for exactly one typed dispatch — the resolver's accept-
1396 /// set migrates as a unit on any future axis addition.
1397 ///
1398 /// Fourth outer top-level [`Caixa`] `&[T]`-return slice-accessor —
1399 /// folds on the "outer [`Caixa`] `&[T]` slice" projection pattern
1400 /// [`Self::autores`] (b5d813f) opened, [`Self::etiquetas`]
1401 /// (78c7d3c) folded on, and [`Self::bibliotecas`] (8a36c23) closed
1402 /// the universal-axis text-tag family of. Opens the outer-`Caixa`
1403 /// foreign-code-slot `&[T]` sub-family the sibling `:servicos`
1404 /// future lift closes onto (per the trio of code-surface list slots
1405 /// the [`Self::validate_code_paths`] per-slot dispatch tuple
1406 /// already carries — `:bibliotecas` + `:exe` + `:servicos`, of which
1407 /// `:bibliotecas` landed at 8a36c23 and `:servicos` remains as the
1408 /// last unlifted code-surface slot). Sibling in shape to the peer
1409 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1410 /// (bc92bce), per-`:placement`
1411 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1412 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1413 /// (6c77e36), per-`:contratos`
1414 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1415 /// per-`:upgrade-from :instructions`
1416 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1417 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1418 /// typed-slot list axes, extended here to the outer top-level
1419 /// [`Caixa`] universal-axis surface. Returns `&[String]` (not
1420 /// `&Vec<String>`) because every downstream consumer of the
1421 /// executable-source list treats it as a read-only sequence — the
1422 /// slice-view is the narrowest borrow that supports every
1423 /// present + roadmapped consumer (`.iter()`, `.len()`,
1424 /// `.is_empty()`) without leaking the backing `Vec`'s
1425 /// grow/push/reserve surface no consumer of the typed view
1426 /// reaches for (the storage-side `Vec` remains reachable through
1427 /// the `pub exe` field for the mutation-carrying serde
1428 /// round-trip and per-test fixture-mutation paths). Named `exe()`
1429 /// to match the storage field's name; the accessor's identity
1430 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1431 /// docstring already carries.
1432 #[must_use]
1433 pub fn exe(&self) -> &[String] {
1434 self.exe.as_slice()
1435 }
1436
1437 /// Substrate-canonical per-`Caixa` `:servicos` universal-axis
1438 /// ComputeUnit-CR-YAML-entry-path-list slice-accessor every consumer
1439 /// of the top-level manifest's Servico-component axis keys off —
1440 /// returns the author-declared `:servicos` list verbatim as a
1441 /// `&[String]` slice-view over the same backing buffer the raw
1442 /// `self.servicos.as_slice()` field access borrows from. Empty-list-
1443 /// carrying (`:servicos` is a default-empty axis every `defcaixa`
1444 /// form supplies with an empty `()` when unset; the
1445 /// [`Self::from_lisp`] derive folds an omitted `:servicos` through
1446 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1447 /// definitionally carries a `Vec<String>` slot — possibly empty —
1448 /// and the returned `&[String]` degenerates to an empty slice on
1449 /// that arm without any silent `None` collapse).
1450 ///
1451 /// The `:servicos` slot carries the universal-axis
1452 /// `.computeunit.yaml` ComputeUnit-CR entry-path list every
1453 /// `:kind Servico` caixa emits under (CAIXA-SDLC §I — the
1454 /// author-facing surface every `defcaixa` form supplies alongside
1455 /// `:nome` / `:versao` / `:kind`; the substrate-wide
1456 /// `servicos/`-directory-fenced entry-carrier axis every downstream
1457 /// Servico-facing renderer keys off) — the typed slot's
1458 /// `Vec<String>` accept-set (empty-per-entry rejected through
1459 /// [`ManifestError::CodePathEmpty { slot: ":servicos" }`],
1460 /// non-sandboxed-relative-shape rejected through
1461 /// [`ManifestError::CodePathShape`], non-`.computeunit.yaml`
1462 /// extension rejected through
1463 /// [`ManifestError::CodePathNonComputeUnitYamlExtension`], cross-
1464 /// entry duplicate rejected through
1465 /// [`ManifestError::CodePathDuplicate`], `len != 1` rejected by the
1466 /// V0 [`crate::ServicoCountMismatch`] gate on the per-Servico
1467 /// renderer entry-points, out-of-`servicos/`-directory paths
1468 /// rejected past the layout's [`crate::LayoutError::ServicoOutsideDir`]
1469 /// `starts_with` fence) maps onto every load-bearing downstream
1470 /// consumer the substrate carries — the [`crate::LayoutInvariants`]
1471 /// Servico-arm empty-check + per-entry file-exists + `servicos/`-
1472 /// directory-fence loop at caixa-core/src/layout.rs that gates each
1473 /// entry through [`crate::LayoutError::ServicoWithoutServicos`] /
1474 /// `MissingEntry` / `ServicoOutsideDir`, the compound `has_code`
1475 /// OR-fold on the [`crate::LayoutError::SupervisorOwnsCode`] /
1476 /// [`crate::LayoutError::AplicacaoOwnsCode`] kind-coherence gate
1477 /// that fences code-surface slots off from the two no-code kinds,
1478 /// [`Self::declared_foreign_code_slots`]'s
1479 /// `!self.servicos.is_empty()` arm on the
1480 /// [`crate::LayoutError::ForeignCodeSlot`] gate that fences the
1481 /// `:servicos` code surface off from every non-Servico code-running
1482 /// kind, [`Self::validate_code_paths`]'s per-slot shape gate that
1483 /// walks each entry through the sandbox-relative / `.computeunit.
1484 /// yaml`-extension / cross-entry duplicate gates, the
1485 /// [`crate::require_single_servico`] V0 singularity gate every
1486 /// per-Servico renderer entry-point runs through
1487 /// [`crate::require_v0_servico_shape`], the `feira chart` /
1488 /// `feira deploy` per-verb `first_servico_path` walk at
1489 /// caixa-feira/src/cmd/chart.rs that resolves the singleton
1490 /// ComputeUnit-CR file, every future per-`Caixa` Servico-facing
1491 /// renderer the CAIXA-SDLC §I roadmap acknowledges (the future
1492 /// per-Servico OCI packager, the future M4
1493 /// `wasm.pleme.io/v1alpha1/ComputeUnit` CR materializer, the future
1494 /// per-Servico OTel collector-config emit).
1495 ///
1496 /// Prior to this lift the `.servicos` field was accessed inline at
1497 /// five production sites — the compound-code-path `has_code =
1498 /// !caixa.bibliotecas().is_empty() || !caixa.exe().is_empty() ||
1499 /// !caixa.servicos.is_empty()` OR-fold on the
1500 /// [`crate::LayoutError::SupervisorOwnsCode`] /
1501 /// `AplicacaoOwnsCode` kind-coherence gate, the Servico-arm
1502 /// `caixa.servicos.is_empty()`
1503 /// [`crate::LayoutError::ServicoWithoutServicos`] gate, the
1504 /// per-entry `for p in &caixa.servicos`
1505 /// `MissingEntry`/`ServicoOutsideDir` walk, the
1506 /// [`Self::declared_foreign_code_slots`]'s
1507 /// `!self.servicos.is_empty()` arm on the `ForeignCodeSlot` gate,
1508 /// and the [`crate::require_single_servico`] V0 count gate's
1509 /// `caixa.servicos.len() == 1` / `caixa.servicos.len()` count
1510 /// projection (both the accept-arm predicate and the
1511 /// diagnostic-carrying `ServicoCountMismatch { count }`
1512 /// projection) — five open-coded field-accesses across three
1513 /// crates that expressed no compile-time link back to the typed
1514 /// slot. A future extension of the `:servicos` axis to a richer
1515 /// component surface — a per-`:servicos` structured
1516 /// `ServicoEntry { path, world, capabilities }` at the storage
1517 /// layer once the substrate absorbs the per-component WIT-world +
1518 /// capability-set tuple the CAIXA-SDLC §I Servico roadmap
1519 /// acknowledges, a per-registry `:servicos` allowlist the M4 CR
1520 /// materializer enforces per-CR (the "cluster policy demands every
1521 /// Servico declare an explicit `:world`" arm), a promotion of the
1522 /// plain `Vec<String>` byte-string list to a richer
1523 /// `Vec<ComputeUnitPath>` newtype discriminated on the
1524 /// `servicos/<nome>.computeunit.yaml`-shape grammar the layout's
1525 /// `starts_with(servicos_dir)` fence and the
1526 /// [`crate::render::is_computeunit_yaml_extension`] predicate
1527 /// already resolve through, a promotion of the V0 singleton
1528 /// contract to a multi-component `Vec<ComputeUnitPath>` past the M5
1529 /// component-model multi-world boundary — would have had to be
1530 /// threaded through all five open-coded copies in lockstep or the
1531 /// layout gate, the shape validator, the V0 count gate, and the
1532 /// `feira chart` / `feira deploy` entry-point walks would silently
1533 /// disagree on which ComputeUnit-CR paths a given [`Caixa`]
1534 /// resolves to (an author's `:servicos ("servicos/foo.computeunit.
1535 /// yaml")` would satisfy layout while `feira chart` silently
1536 /// packaged a drifted other list, or vice versa). Lifting the
1537 /// resolution to a typed method on the substrate primitive means
1538 /// every downstream consumer of the caixa's per-`Caixa`
1539 /// ComputeUnit-CR-source surface reaches for exactly one typed
1540 /// dispatch — the resolver's accept-set migrates as a unit on any
1541 /// future axis addition.
1542 ///
1543 /// Fifth and final outer top-level [`Caixa`] `&[T]`-return slice-
1544 /// accessor — folds on the "outer [`Caixa`] `&[T]` slice"
1545 /// projection pattern [`Self::autores`] (b5d813f) opened,
1546 /// [`Self::etiquetas`] (78c7d3c) folded on, [`Self::bibliotecas`]
1547 /// (8a36c23) closed the universal-axis text-tag family of, and
1548 /// [`Self::exe`] (65d9527) opened the foreign-code-slot sub-family
1549 /// of. Closes the outer-`Caixa` foreign-code-slot `&[T]` sub-family
1550 /// — with `:bibliotecas`, `:exe`, and `:servicos` now each carrying
1551 /// a substrate-canonical slice accessor, the trio of code-surface
1552 /// list slots the [`Self::validate_code_paths`] per-slot dispatch
1553 /// tuple carries is complete on the typed dispatch surface (the
1554 /// internal `[(":bibliotecas", &self.bibliotecas, ..), (":exe",
1555 /// &self.exe, ..), (":servicos", &self.servicos, ..)]` per-slot
1556 /// dispatch tuple's homogeneous `&Vec<String>`-typed shape blocks a
1557 /// per-element accessor swap in isolation — a future companion lift
1558 /// promotes the tuple's element type to `&[String]` and threads the
1559 /// triple of typed dispatches through as a unit). Sibling in shape
1560 /// to the peer per-`:supervisor`
1561 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1562 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1563 /// (a6e18d7), per-`:membros`
1564 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1565 /// 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 /// ComputeUnit-CR-source list treats it as a read-only sequence —
1574 /// the slice-view is the narrowest borrow that supports every
1575 /// present + roadmapped consumer (`.iter()`, `.len()`,
1576 /// `.is_empty()`, `.first()`) without leaking the backing `Vec`'s
1577 /// grow/push/reserve surface no consumer of the typed view reaches
1578 /// for (the storage-side `Vec` remains reachable through the
1579 /// `pub servicos` field for the mutation-carrying serde round-trip
1580 /// and per-test fixture-mutation paths, and for the
1581 /// [`Self::validate_code_paths`] per-slot dispatch tuple whose
1582 /// homogeneous-element-type shape carries the raw field access
1583 /// until the trio-closure lift promotes the tuple as a unit).
1584 /// Named `servicos()` to match the storage field's name; the
1585 /// accessor's identity maps onto the canonical CAIXA-SDLC §I
1586 /// vocabulary the slot's docstring already carries.
1587 #[must_use]
1588 pub fn servicos(&self) -> &[String] {
1589 self.servicos.as_slice()
1590 }
1591
1592 /// Substrate-canonical per-`Caixa` `:deps` universal-axis
1593 /// runtime-dependency-declaration-list slice-accessor every consumer
1594 /// of the top-level manifest's runtime-dep-graph axis keys off —
1595 /// returns the author-declared `:deps` list verbatim as a `&[Dep]`
1596 /// slice-view over the same backing buffer the raw
1597 /// `self.deps.as_slice()` field access borrows from. Empty-list-
1598 /// carrying (`:deps` is a default-empty axis every `defcaixa` form
1599 /// supplies with an empty `()` when unset; the [`Self::from_lisp`]
1600 /// derive folds an omitted `:deps` through `#[serde(default)]` to
1601 /// `Vec::new()`, so a `Caixa` past parse definitionally carries a
1602 /// `Vec<Dep>` slot — possibly empty — and the returned `&[Dep]`
1603 /// degenerates to an empty slice on that arm without any silent
1604 /// `None` collapse).
1605 ///
1606 /// The `:deps` slot carries the universal-axis runtime dependency
1607 /// list every kind of caixa emits under (CAIXA-SDLC §I — the author-
1608 /// facing surface every `defcaixa` form supplies alongside `:nome` /
1609 /// `:versao` / `:kind`; the substrate-wide runtime-closure-input axis
1610 /// every downstream resolver-facing artifact emits under) — the
1611 /// typed slot's `Vec<Dep>` accept-set (empty-`:nome` rejected through
1612 /// [`DepError::NomeEmpty`], non-DNS-1123-label `:nome` rejected
1613 /// through [`DepError::NomeInvalid`], malformed `:versao` rejected
1614 /// through [`DepError::VersaoInvalid`], empty `:fonte.repo` rejected
1615 /// through [`DepError::FonteRepoEmpty`], within-list duplicate `:nome`
1616 /// rejected through [`DepError::DuplicateNome { list: ":deps" }`])
1617 /// maps onto every load-bearing downstream consumer the substrate
1618 /// carries — the [`Self::validate_deps`] per-entry
1619 /// [`Dep::validate`] + within-list dedup walk at
1620 /// caixa-core/src/manifest.rs, the [`crate::dep::validate_no_self_dep`]
1621 /// cross-list self-reference gate at caixa-core/src/layout.rs that
1622 /// checks each entry against the caixa's own `:nome`, the
1623 /// caixa-resolver `for dep in &root.deps` closure walk at
1624 /// caixa-resolver/src/resolve.rs that seeds every git-clone target
1625 /// through the resolver's [`crate::Dep`]-keyed pipeline, the
1626 /// caixa-crd `caixa.deps.iter().map(dep_into_ref).collect()` fold at
1627 /// caixa-crd/src/conversion.rs that materializes each entry into the
1628 /// K8s `Caixa` CR's `spec.deps` field, every future per-`Caixa`
1629 /// resolver-facing renderer the CAIXA-SDLC §I roadmap acknowledges
1630 /// (the future per-cluster runtime-closure-audit overlay the M4 CR
1631 /// materializer resolves per-CR, the future `lacre.lisp` BLAKE3-
1632 /// closure emit walk the caixa-resolver docstring roadmaps).
1633 ///
1634 /// First outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1635 /// opens the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1636 /// sibling `:deps-dev` future lift closes on. Peer of the closed
1637 /// outer-`Caixa` foreign-code-slot `&[String]` sub-family
1638 /// ([`Self::bibliotecas`] 8a36c23, [`Self::exe`] 65d9527,
1639 /// [`Self::servicos`] 611f78b) and the outer-`Caixa` universal-axis
1640 /// text-tag family ([`Self::autores`] b5d813f, [`Self::etiquetas`]
1641 /// 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice" projection
1642 /// pattern onto a novel element-type axis (`Dep` composite vs the
1643 /// prior sibling family's `String` scalar). Sibling in shape to the
1644 /// peer per-`:supervisor`
1645 /// [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
1646 /// per-`:placement` [`crate::aplicacao::Placement::clusters`]
1647 /// (a6e18d7), per-`:membros`
1648 /// [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
1649 /// per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
1650 /// (0dcc926), and per-`:upgrade-from :instructions`
1651 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1652 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1653 /// typed-slot list axes, extended here to the outer top-level
1654 /// [`Caixa`] universal-axis dep-graph surface. Returns `&[Dep]`
1655 /// (not `&Vec<Dep>`) because every downstream consumer of the
1656 /// runtime-dep list treats it as a read-only sequence — the slice-
1657 /// view is the narrowest borrow that supports every present +
1658 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1659 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1660 /// of the typed view reaches for (the storage-side `Vec` remains
1661 /// reachable through the `pub deps` field for the mutation-carrying
1662 /// serde round-trip and per-test fixture-mutation paths). Named
1663 /// `deps()` to match the storage field's name; the accessor's
1664 /// identity maps onto the canonical CAIXA-SDLC §I vocabulary the
1665 /// slot's docstring already carries.
1666 #[must_use]
1667 pub fn deps(&self) -> &[Dep] {
1668 self.deps.as_slice()
1669 }
1670
1671 /// Substrate-canonical per-`Caixa` `:deps-dev` universal-axis
1672 /// development-only-dependency-declaration-list slice-accessor every
1673 /// consumer of the top-level manifest's dev-dep-graph axis keys off —
1674 /// returns the author-declared `:deps-dev` list verbatim as a `&[Dep]`
1675 /// slice-view over the same backing buffer the raw
1676 /// `self.deps_dev.as_slice()` field access borrows from. Empty-list-
1677 /// carrying (`:deps-dev` is a default-empty axis every `defcaixa`
1678 /// form supplies with an empty `()` when unset; the
1679 /// [`Self::from_lisp`] derive folds an omitted `:deps-dev` through
1680 /// `#[serde(default)]` to `Vec::new()`, so a `Caixa` past parse
1681 /// definitionally carries a `Vec<Dep>` slot — possibly empty — and
1682 /// the returned `&[Dep]` degenerates to an empty slice on that arm
1683 /// without any silent `None` collapse).
1684 ///
1685 /// The `:deps-dev` slot carries the universal-axis dev-only
1686 /// dependency list every kind of caixa emits under (CAIXA-SDLC §I —
1687 /// the author-facing sibling of `:deps` that every `defcaixa` form
1688 /// supplies to declare tests / lint / bench closures the runtime
1689 /// `:deps` axis does not carry; the substrate-wide dev-closure-input
1690 /// axis every downstream test-facing artifact emits under, matching
1691 /// Cargo's `[dev-dependencies]` table's dev-time-only visibility
1692 /// contract) — the typed slot's `Vec<Dep>` accept-set (empty-`:nome`
1693 /// rejected through [`DepError::NomeEmpty`], non-DNS-1123-label
1694 /// `:nome` rejected through [`DepError::NomeInvalid`], malformed
1695 /// `:versao` rejected through [`DepError::VersaoInvalid`], empty
1696 /// `:fonte.repo` rejected through [`DepError::FonteRepoEmpty`],
1697 /// within-list duplicate `:nome` rejected through
1698 /// [`DepError::DuplicateNome { list: ":deps-dev" }`]) maps onto every
1699 /// load-bearing downstream consumer the substrate carries — the
1700 /// [`Self::validate_deps`] per-entry [`Dep::validate`] + within-list
1701 /// dedup walk at caixa-core/src/manifest.rs, the
1702 /// [`crate::dep::validate_no_self_dep`] cross-list self-reference
1703 /// gate at caixa-core/src/layout.rs that checks each entry against
1704 /// the caixa's own `:nome`, the caixa-resolver
1705 /// `for dep in &root.deps_dev` closure walk at
1706 /// caixa-resolver/src/resolve.rs that seeds every dev-only git-clone
1707 /// target through the resolver's [`crate::Dep`]-keyed pipeline, and
1708 /// every future per-`Caixa` resolver-facing renderer the CAIXA-SDLC
1709 /// §I roadmap acknowledges (the future per-cluster dev-closure-audit
1710 /// overlay the M4 CR materializer resolves per-CR, the future
1711 /// `lacre.lisp` BLAKE3-closure emit walk the caixa-resolver docstring
1712 /// roadmaps).
1713 ///
1714 /// Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor —
1715 /// closes the outer-`Caixa` dependency-slot `&[Dep]` sub-family the
1716 /// sibling [`Self::deps`] (ad34b4e) opened on. The two accessors
1717 /// jointly close the two-list dep-graph surface every downstream
1718 /// resolver-facing consumer keys off (runtime `:deps` +
1719 /// dev-only `:deps-dev`, the canonical Cargo-shaped dependency-table
1720 /// pair the [`Self::validate_deps`] gate already walks in canonical
1721 /// order). Peer of the closed outer-`Caixa` foreign-code-slot
1722 /// `&[String]` sub-family ([`Self::bibliotecas`] 8a36c23,
1723 /// [`Self::exe`] 65d9527, [`Self::servicos`] 611f78b) and the outer-
1724 /// `Caixa` universal-axis text-tag family ([`Self::autores`]
1725 /// b5d813f, [`Self::etiquetas`] 78c7d3c) — folds the "outer
1726 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
1727 /// dev-dep composite-element axis (`Dep` composite, matching the
1728 /// [`Self::deps`] element type). Sibling in shape to the peer
1729 /// per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
1730 /// (bc92bce), per-`:placement`
1731 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7),
1732 /// per-`:membros` [`crate::aplicacao::AplicacaoSpec::membros`]
1733 /// (6c77e36), per-`:contratos`
1734 /// [`crate::aplicacao::AplicacaoSpec::contratos`] (0dcc926), and
1735 /// per-`:upgrade-from :instructions`
1736 /// [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
1737 /// `&[T]`-return slice accessors on the sibling per-M2 / per-M3
1738 /// typed-slot list axes, folded here to the outer top-level
1739 /// [`Caixa`] universal-axis dev-dep-graph surface. Returns `&[Dep]`
1740 /// (not `&Vec<Dep>`) because every downstream consumer of the
1741 /// dev-dep list treats it as a read-only sequence — the slice-view
1742 /// is the narrowest borrow that supports every present +
1743 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`) without
1744 /// leaking the backing `Vec`'s grow/push/reserve surface no consumer
1745 /// of the typed view reaches for (the storage-side `Vec` remains
1746 /// reachable through the `pub deps_dev` field for the mutation-
1747 /// carrying serde round-trip and per-test fixture-mutation paths).
1748 /// Named `deps_dev()` to match the storage field's `snake_case` name;
1749 /// the kebab-case author-surface tag `:deps-dev` is the same axis
1750 /// after tatara-lisp's kebab↔snake fold and the accessor's identity
1751 /// maps onto the canonical CAIXA-SDLC §I vocabulary the slot's
1752 /// docstring already carries.
1753 #[must_use]
1754 pub fn deps_dev(&self) -> &[Dep] {
1755 self.deps_dev.as_slice()
1756 }
1757
1758 /// Substrate-canonical per-[`Caixa`] typed-dispatch read accessor
1759 /// every consumer that walks one of the two dep-list axes keyed on a
1760 /// [`crate::dep::DepList`] discriminant reaches for — routes the
1761 /// `(list: DepList) -> &[Dep]` projection through one typed method on
1762 /// the substrate primitive rather than the prior open-coded
1763 /// `match list { Prod => caixa.deps(), Dev => caixa.deps_dev() }`
1764 /// inline dispatch every per-axis walker would otherwise carry.
1765 /// Returns the author-declared per-list `Vec<Dep>` verbatim as a
1766 /// `&[Dep]` slice-view over the same backing buffer the sibling
1767 /// [`Self::deps`] (`Prod`) / [`Self::deps_dev`] (`Dev`) per-slot
1768 /// accessors borrow from, preserving the empty-list-carrying invariant
1769 /// each per-slot accessor already establishes (`:deps` / `:deps-dev`
1770 /// are default-empty axes every `defcaixa` form supplies with an empty
1771 /// `()` when unset; the [`Self::from_lisp`] derive folds an omitted
1772 /// list through `#[serde(default)]` to `Vec::new()`, so both arms
1773 /// definitionally carry a `Vec<Dep>` slot — possibly empty — and the
1774 /// returned `&[Dep]` degenerates to an empty slice on either arm
1775 /// without any silent `None` collapse).
1776 ///
1777 /// The [`crate::dep::DepList`] closed-set typed enum is the
1778 /// substrate's canonical discriminator for the "runtime-closure
1779 /// `:deps` vs dev-only-closure `:deps-dev`" axis every dep-list
1780 /// consumer dispatches on — the compiler-checked exhaustiveness on
1781 /// the enum's `match` arms is the build-time guarantee that no future
1782 /// per-list read-site regresses to a bare-`bool`-flag inline dispatch
1783 /// that a future third dep-list axis (a `:deps-build` build-only
1784 /// closure once the substrate grows cross-artifact heterogeneous
1785 /// dep-graphs, per CAIXA-SDLC §I) would silently split at every
1786 /// consumer. Prior to this the read side carried two per-slot
1787 /// accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`]) and no
1788 /// typed dispatch that a per-axis walker could parametrise on, so
1789 /// every per-list walker (the [`Self::validate_deps`] per-list
1790 /// [`crate::render::insert_first_seen`] dedup walk, a future
1791 /// `feira app graph` per-list dep summary, a future M4 per-cluster
1792 /// dev-closure-audit overlay the CR materializer resolves per-CR)
1793 /// open-coded the same two-block "run over `:deps`, then run over
1794 /// `:deps-dev`" pattern — a silent duplication that a future third
1795 /// dep-list axis would have had to grow a third block at every site.
1796 ///
1797 /// Peer of the sibling [`Self::push_dep`] typed-mutation dispatch
1798 /// (359fba5) — closes the two-side dispatch symmetry on the outer
1799 /// [`Caixa`] two-list dep-graph surface: `push_dep` on the mutation
1800 /// side, `deps_of` on the read side, both keyed on the same
1801 /// [`crate::dep::DepList`] discriminator. Same "one typed dispatch on
1802 /// the substrate primitive, thin projections at each consumer"
1803 /// discipline the sibling per-slot read accessors ([`Self::nome`]
1804 /// e6b7d97, [`Self::versao`], [`Self::kind`]) carry — extended onto
1805 /// the outer-[`Caixa`] typed-dispatch read surface.
1806 #[must_use]
1807 pub fn deps_of(&self, list: crate::dep::DepList) -> &[Dep] {
1808 match list {
1809 crate::dep::DepList::Prod => self.deps(),
1810 crate::dep::DepList::Dev => self.deps_dev(),
1811 }
1812 }
1813
1814 /// Substrate-canonical per-[`Caixa`] typed-mutation dispatch every
1815 /// consumer that appends to one of the two dep-list axes keys off
1816 /// — routes the `(list: DepList, dep: Dep)` tuple through one typed
1817 /// method on the substrate primitive rather than the prior
1818 /// `feira add`-side open-coded `if self.dev { &mut caixa.deps_dev }
1819 /// else { &mut caixa.deps }` inline dispatch + open-coded
1820 /// `.iter().any(|d| d.nome == …)` dup-check cascade. Refuses the
1821 /// mutation with the canonical typed [`DepError::DuplicateNome`] on
1822 /// a within-list name collision — the same `list: &'static str`
1823 /// diagnostic shape [`Self::validate_deps`]'s per-list
1824 /// [`crate::render::insert_first_seen`] walk raises on the peer
1825 /// parse-time within-list dedup axis, so a future author reading a
1826 /// `feira add` refusal and a `feira build` refusal reaches for the
1827 /// same corrective surface without switching diagnostic idioms.
1828 ///
1829 /// The two-arm [`crate::dep::DepList`] enum is the substrate's
1830 /// closed-set typed carrier for the "runtime-closure `:deps` vs
1831 /// dev-only-closure `:deps-dev`" axis every dep-list consumer
1832 /// dispatches on — the compiler-checked exhaustiveness on the
1833 /// enum's `match` arms is the build-time guarantee that no future
1834 /// per-list mutation-site regresses to a bare-`bool`-flag
1835 /// (`is_dev: bool`) inline dispatch that a future third
1836 /// dep-list axis (a `:deps-build` build-only closure once the
1837 /// substrate grows cross-artifact heterogeneous dep-graphs, per
1838 /// CAIXA-SDLC §I) would silently split at every consumer.
1839 ///
1840 /// Same "one typed dispatch on the substrate primitive, thin
1841 /// projections at each consumer" discipline the sibling per-slot
1842 /// read accessors ([`Self::deps`] ad34b4e, [`Self::deps_dev`],
1843 /// [`Self::nome`] e6b7d97, [`Self::versao`], [`Self::kind`])
1844 /// carry — extended onto the outer-[`Caixa`] typed-mutation surface,
1845 /// the substrate's first typed-mutation dispatch on the top-level
1846 /// manifest. The prior `feira add` open-coded `&mut caixa.deps` /
1847 /// `&mut caixa.deps_dev` inline field-access + `bail!` string-
1848 /// diagnostic path routed no through-line back to the typed slot,
1849 /// so a future extension of either dep-list axis to a richer author
1850 /// surface (a per-cluster override the operator pins through a
1851 /// future `:placement`-scoped dep-list slot the CAIXA-SDLC §I
1852 /// roadmap acknowledges, an M4
1853 /// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-CR
1854 /// admission-webhook that normalized the list at admission time)
1855 /// would have had to be threaded through the `feira add` mutation
1856 /// site in lockstep with every read consumer or one path would
1857 /// silently disagree with the other on which list a given dep lands
1858 /// in. Lifting the resolution rule to a typed method on the
1859 /// substrate primitive means every downstream dep-list-mutating
1860 /// consumer of the top-level manifest reaches for exactly one typed
1861 /// dispatch — the resolver's accept-set migrates as a unit on any
1862 /// future axis addition.
1863 ///
1864 /// # Errors
1865 ///
1866 /// Returns [`DepError::DuplicateNome`] with `list = list.as_str()`
1867 /// when another entry in the same list already carries the same
1868 /// `:nome` — the mutation is refused and the caller can surface the
1869 /// typed diagnostic to the author (the `feira add` verb routes the
1870 /// error through `anyhow::Error::from`, which preserves the
1871 /// canonical `#[error(...)]`-templated diagnostic body).
1872 pub fn push_dep(&mut self, list: crate::dep::DepList, dep: Dep) -> Result<(), DepError> {
1873 let target = match list {
1874 crate::dep::DepList::Prod => &mut self.deps,
1875 crate::dep::DepList::Dev => &mut self.deps_dev,
1876 };
1877 if target.iter().any(|d| d.nome() == dep.nome()) {
1878 return Err(DepError::DuplicateNome {
1879 nome: dep.nome().to_string(),
1880 list: list.as_str(),
1881 });
1882 }
1883 target.push(dep);
1884 Ok(())
1885 }
1886
1887 /// Substrate-canonical per-`Caixa` `:limits` M2 typed-slot outer-
1888 /// composite Lunatic-per-process wasm32-sandboxing-composite optional-
1889 /// composite-reference accessor every consumer of the top-level
1890 /// manifest's per-Servico [`LimitsSpec`] outer-composite reader keys
1891 /// off — returns the author-declared `:limits` typed composite
1892 /// verbatim as an `Option<&LimitsSpec>` reference over the same
1893 /// backing storage the raw `self.limits.as_ref()` field access
1894 /// borrows from, with `None` naming the "no `:limits` block
1895 /// authored — every per-axis Lunatic-sandbox cap defers to the
1896 /// wasm-engine-default arm named on the per-axis
1897 /// [`LimitsSpec::memory`] / [`LimitsSpec::fuel`] /
1898 /// [`LimitsSpec::wall_clock`] / [`LimitsSpec::cpu`] scalar-accessor
1899 /// docstrings" partition every downstream Servico-M2-overlay
1900 /// emitter treats as "emit nothing" and the sibling
1901 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate
1902 /// treats as "skip the per-axis
1903 /// [`crate::LimitsError::MemoryZero`] / `MemoryBelowWasm32Page` /
1904 /// `FuelZero` / `WallClockZero` / `CpuZero` refusal cascade".
1905 ///
1906 /// The outer `:limits` slot carries the M2 Servico-runtime typed
1907 /// composite — the load-bearing container of every Lunatic-shaped
1908 /// per-process wasm32-sandbox cap axis every long-running wasm
1909 /// component's runtime dispatches on (INSPIRATIONS §III.1 —
1910 /// Lunatic per-process linear-memory / fuel / wall-clock /
1911 /// millicore cap primitives translated onto pleme-io's typed
1912 /// `:limits :memory` / `:limits :fuel` / `:limits :wall-clock` /
1913 /// `:limits :cpu` sub-slot axes; CAIXA-SDLC §II — the typed-M2
1914 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
1915 /// chart both fan on). Every per-`:limits` axis threads through a
1916 /// lifted per-slot accessor on the [`LimitsSpec`] type: the
1917 /// [`LimitsSpec::memory`] wasm32 linear-memory byte-cap scalar
1918 /// accessor, the [`LimitsSpec::fuel`] wasmtime fuel-cap scalar
1919 /// accessor, the [`LimitsSpec::wall_clock`] per-call wall-clock
1920 /// deadline scalar accessor, and the [`LimitsSpec::cpu`]
1921 /// K8s-millicore soft-CPU-share scalar accessor. Every downstream
1922 /// consumer that reaches for a limits axis first passes through
1923 /// this outer accessor onto the composite and then dispatches
1924 /// onto the per-axis accessor — the two-level dispatch means
1925 /// every per-`:limits` reader now routes through a typed dispatch
1926 /// on the substrate primitive at both altitudes.
1927 ///
1928 /// Prior to this lift the `.limits` `Option<LimitsSpec>` composite
1929 /// was accessed inline at three production sites — the
1930 /// [`crate::StandardLayout::verify`] per-`:limits` shape gate's
1931 /// `if let Some(l) = &caixa.limits { … }` traversal head
1932 /// (caixa-core/src/layout.rs:882, which drives the per-axis
1933 /// refusal cascade on the composite: the `LimitsError::MemoryZero`
1934 /// / `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Max` /
1935 /// `FuelZero` / `FuelExceedsMax` / `WallClockZero` /
1936 /// `WallClockExceedsMax` / `CpuZero` / `CpuExceedsMax` refusals
1937 /// [`LimitsSpec::validate`] fans onto), the
1938 /// [`crate::render::servico_m2_overlay`] per-Servico M2 overlay
1939 /// emitter's `if let Some(limits) = &caixa.limits { … }` traversal
1940 /// head (caixa-core/src/render.rs:18504, which drives the
1941 /// `M2_KEY_LIMITS`-keyed `limits.is_empty()`-gated `serde_yaml`
1942 /// projection every `caixa-helm` / `caixa-flux` Servico values-
1943 /// block emitter fans on), and the
1944 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
1945 /// set enumerator's `self.limits.is_some()` presence probe
1946 /// (caixa-core/src/manifest.rs:1788, which drives the
1947 /// `M2_AUTHOR_KEY_LIMITS` kebab-case author-label push every
1948 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
1949 /// gate reads) — three open-coded outer-field accesses that
1950 /// expressed no compile-time link back to the typed slot at the
1951 /// [`Caixa`] altitude. A future extension of the `:limits` outer
1952 /// axis to a richer author surface (a multi-`:limits` list the M4
1953 /// CR materializer resolves per-CR at admission time so a Servico
1954 /// can expose a compute-heavy + IO-heavy limits pair, a per-
1955 /// cluster `:limits-overrides` slot the operator pins so a
1956 /// cluster-specific policy can tighten a caixa-declared cap
1957 /// without re-authoring the `caixa.lisp`, a promotion of the
1958 /// plain `Option<LimitsSpec>` to a richer
1959 /// `{static, dynamic}` partition once the wasm-engine's runtime-
1960 /// resolved dynamic-cap surface lands) would have had to be
1961 /// threaded through all three open-coded copies in lockstep or
1962 /// one consumer would silently disagree with the peers on which
1963 /// limits composite a given Caixa resolves to — the layout gate's
1964 /// per-axis bracket-dispatch seed reading the raw slot while the
1965 /// peer `servico_m2_overlay` emitter read an operator-resolved
1966 /// slot would silently split the build-time sandbox-shape gate
1967 /// from the runtime `ComputeUnit` CR emission gate, a three-
1968 /// consumer split at the layout gate, the M2 overlay emitter, and
1969 /// the declared-slot enumerator far from the source `caixa.lisp`
1970 /// with no field naming the limits-drift root cause. Lifting the
1971 /// resolution rule to a typed method on the substrate primitive
1972 /// means every downstream consumer of the caixa's per-`Caixa`
1973 /// Lunatic-sandboxing outer-composite surface reaches for exactly
1974 /// one typed dispatch — the resolver's accept-set migrates as a
1975 /// unit on any future axis addition.
1976 ///
1977 /// First outer top-level [`Caixa`] `Option<&Composite>`-return
1978 /// composite-reference accessor — opens the outer-`Caixa`
1979 /// `Option<&Composite>` composite-reference projection pattern the
1980 /// sibling per-`Caixa` `:behavior` [`crate::BehaviorSpec`] /
1981 /// `:politicas` [`crate::aplicacao::MeshPolicy`] / `:placement`
1982 /// [`crate::aplicacao::Placement`] / `:entrada`
1983 /// [`crate::aplicacao::Entrada`] future outer-composite lifts
1984 /// fold on. Peer of the M3 mesh-slot outer-composite family the
1985 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
1986 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
1987 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
1988 /// accessors already close on the outer [`crate::AplicacaoSpec`]
1989 /// altitude — extends that "one typed dispatch on the substrate
1990 /// primitive, thin projections at each consumer" discipline onto
1991 /// the outer top-level [`Caixa`] altitude, opening the M2 Servico-
1992 /// runtime slot family's outer-composite axis. Returns
1993 /// `Option<&LimitsSpec>` (not the owning composite by copy or
1994 /// clone) because every downstream consumer of the limits
1995 /// composite treats it as a read-only per-axis dispatch source —
1996 /// the reference-view is the narrowest borrow that supports every
1997 /// present + roadmapped consumer (per-axis accessor dispatch,
1998 /// `.is_empty()`-gated overlay projection, presence-probe early
1999 /// return on the "author-omitted `:limits` ⇒ engine-default
2000 /// applies" partition) without cloning the composite through
2001 /// every consumer's fast path. The `Option` half of the return-
2002 /// type preserves the load-bearing "author-omitted `:limits` ⇒
2003 /// engine-default applies" partition (not a default composite the
2004 /// downstream must reject on emptiness) — the accessor projects
2005 /// the raw `Option<LimitsSpec>` slot's presence bit through the
2006 /// reference-return unchanged. Named `limits()` to match the
2007 /// storage field's name verbatim and the tatara-lisp author-
2008 /// surface term (`:limits`) the field's own docstring already
2009 /// carries.
2010 #[must_use]
2011 pub fn limits(&self) -> Option<&LimitsSpec> {
2012 self.limits.as_ref()
2013 }
2014
2015 /// Substrate-canonical per-`Caixa` `:behavior` M2 typed-slot outer-
2016 /// composite OTP-`gen_server`-shaped callback-table optional-
2017 /// composite-reference accessor every consumer of the top-level
2018 /// manifest's per-Servico [`BehaviorSpec`] outer-composite reader
2019 /// keys off — returns the author-declared `:behavior` typed
2020 /// composite verbatim as an `Option<&BehaviorSpec>` reference over
2021 /// the same backing storage the raw `self.behavior.as_ref()` field
2022 /// access borrows from, with `None` naming the "no `:behavior`
2023 /// block authored — every per-callback OTP-shaped hook defers to
2024 /// the wasm-engine's runtime default arm named on the per-axis
2025 /// [`BehaviorSpec::on_init`] / [`BehaviorSpec::on_call`] /
2026 /// [`BehaviorSpec::on_cast`] / [`BehaviorSpec::on_info`] /
2027 /// [`BehaviorSpec::on_state_change`] /
2028 /// [`BehaviorSpec::on_terminate`] scalar-accessor docstrings"
2029 /// partition every downstream Servico-M2-overlay emitter treats as
2030 /// "emit nothing" and the sibling [`crate::StandardLayout::verify`]
2031 /// per-`:behavior` shape gate treats as "skip the per-arm
2032 /// [`crate::behavior::BehaviorError`] refusal cascade + the
2033 /// per-callback on-disk `MissingEntry` existence check".
2034 ///
2035 /// The outer `:behavior` slot carries the M2 Servico-runtime typed
2036 /// composite — the load-bearing container of every OTP-shaped
2037 /// per-Servico lifecycle-callback path axis every long-running wasm
2038 /// component's runtime dispatches on (INSPIRATIONS §II.3 — Erlang/
2039 /// OTP `gen_server:init/1` / `handle_call/3` / `handle_cast/2` /
2040 /// `handle_info/2` / `code_change/3` / `terminate/2` primitives
2041 /// translated onto pleme-io's typed `:behavior :on-init` /
2042 /// `:on-call` / `:on-cast` / `:on-info` / `:on-state-change` /
2043 /// `:on-terminate` sub-slot axes; CAIXA-SDLC §II — the typed-M2
2044 /// slot algebra the wasm-engine + `pleme-computeunit` Helm-library
2045 /// chart both fan on). Every per-`:behavior` axis threads through a
2046 /// lifted per-callback accessor on the [`BehaviorSpec`] type
2047 /// (9b4ecde / d66c702 / 156ddbe / 99616ac / 4846cef / 701add7).
2048 /// Every downstream consumer that reaches for a behavior axis
2049 /// first passes through this outer accessor onto the composite
2050 /// and then dispatches onto the per-callback accessor — the
2051 /// two-level dispatch means every per-`:behavior` reader now
2052 /// routes through a typed dispatch on the substrate primitive at
2053 /// both altitudes.
2054 ///
2055 /// Composes cross-slot with the M2 `:upgrade-from` gate: the
2056 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2057 /// cross-slot composition gate at [`crate::StandardLayout::verify`]
2058 /// keys the "per-version `:state-change` instruction must have a
2059 /// `:on-state-change` callback" precondition off this accessor's
2060 /// composite (the callback-side counterpart to the
2061 /// `:upgrade-from :instructions :state-change :script` refusal at
2062 /// the appup-side). Threading that gate's traversal input through
2063 /// this accessor closes the cross-slot invariant on the substrate
2064 /// primitive, not on the raw field.
2065 ///
2066 /// Prior to this lift the `.behavior` `Option<BehaviorSpec>`
2067 /// composite was accessed inline at four production sites — the
2068 /// [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
2069 /// `if let Some(b) = &caixa.behavior { … }` traversal head
2070 /// (caixa-core/src/layout.rs:896, which drives the per-arm
2071 /// `BehaviorError` refusal cascade + the per-callback on-disk
2072 /// [`crate::LayoutError::MissingEntry`] existence check under
2073 /// [`crate::render::LAYOUT_MISSING_ENTRY_KIND_BEHAVIOR_CALLBACK`]),
2074 /// the [`crate::upgrade::validate_upgrade_from_against_behavior`]
2075 /// cross-slot composition gate's `caixa.behavior.as_ref()`
2076 /// traversal-input feed (caixa-core/src/layout.rs:1008, which
2077 /// drives the `:state-change` ↔ `:on-state-change` precondition
2078 /// refusal), the [`crate::render::servico_m2_overlay`] per-Servico
2079 /// M2 overlay emitter's `if let Some(behavior) = &caixa.behavior
2080 /// { … }` traversal head (caixa-core/src/render.rs:18513, which
2081 /// drives the `M2_KEY_BEHAVIOR`-keyed `behavior.is_empty()`-gated
2082 /// `serde_yaml` projection every `caixa-helm` / `caixa-flux`
2083 /// Servico values-block emitter fans on), and the
2084 /// [`Self::declared_servico_slots`] per-Servico M2 declared-slot-
2085 /// set enumerator's `self.behavior.is_some()` presence probe
2086 /// (caixa-core/src/manifest.rs:1919, which drives the
2087 /// `M2_AUTHOR_KEY_BEHAVIOR` kebab-case author-label push every
2088 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2089 /// gate reads) — four open-coded outer-field accesses that
2090 /// expressed no compile-time link back to the typed slot at the
2091 /// [`Caixa`] altitude. A future extension of the `:behavior`
2092 /// outer axis to a richer author surface (a per-callback overlay
2093 /// resolver the operator materializes at admission time so a
2094 /// cluster-specific policy can inject a per-callback tracing
2095 /// interceptor without re-authoring the `caixa.lisp`, a promotion
2096 /// of the plain `Option<BehaviorSpec>` to a richer `{static,
2097 /// dynamic}` partition once a runtime-resolved behavior-swap
2098 /// surface lands, the M4 per-callback middleware chain the
2099 /// caixa-operator's per-Servico admission webhook keys off) would
2100 /// have had to be threaded through all four open-coded copies in
2101 /// lockstep or one consumer would silently disagree with the
2102 /// peers on which behavior composite a given Caixa resolves to —
2103 /// the layout gate's per-callback existence-check seed reading
2104 /// the raw slot while the peer `servico_m2_overlay` emitter read
2105 /// an operator-resolved slot would silently split the build-time
2106 /// callback-shape gate from the runtime `ComputeUnit` CR emission
2107 /// gate from the cross-slot `:state-change` composition gate from
2108 /// the M2 declared-slot enumerator, a four-consumer split far
2109 /// from the source `caixa.lisp` with no field naming the
2110 /// behavior-drift root cause. Lifting the resolution rule to a
2111 /// typed method on the substrate primitive means every downstream
2112 /// consumer of the caixa's per-`Caixa` OTP-callback-table outer-
2113 /// composite surface reaches for exactly one typed dispatch — the
2114 /// resolver's accept-set migrates as a unit on any future axis
2115 /// addition.
2116 ///
2117 /// Second outer top-level [`Caixa`] `Option<&Composite>`-return
2118 /// composite-reference accessor — sibling to the opening
2119 /// [`Self::limits`] (b2bd9d7) accessor on the outer-`Caixa`
2120 /// `Option<&Composite>` composite-reference sub-family, extends
2121 /// the "one typed dispatch on the substrate primitive, thin
2122 /// projections at each consumer" discipline onto the second of
2123 /// the three M2 Servico-runtime slots. The remaining
2124 /// `Option<&Composite>` axes at the outer top-level [`Caixa`]
2125 /// altitude — the M3 mesh-slot family (`:politicas`,
2126 /// `:placement`, `:entrada` — already closed on the inner
2127 /// [`crate::AplicacaoSpec`] altitude via 534dc21 / 9abb8f0 /
2128 /// d32111c) — remain the future sibling lifts on the outer
2129 /// top-level projection. Returns `Option<&BehaviorSpec>` (not
2130 /// the owning composite by copy or clone) because every
2131 /// downstream consumer of the behavior composite treats it as a
2132 /// read-only per-callback dispatch source — the reference-view is
2133 /// the narrowest borrow that supports every present + roadmapped
2134 /// consumer (per-callback accessor dispatch, `.is_empty()`-gated
2135 /// overlay projection, presence-probe early return on the
2136 /// "author-omitted `:behavior` ⇒ runtime-default applies"
2137 /// partition, cross-slot `:state-change` composition input)
2138 /// without cloning the composite through every consumer's fast
2139 /// path. The `Option` half of the return-type preserves the
2140 /// load-bearing "author-omitted `:behavior` ⇒ runtime-default
2141 /// applies" partition (not a default composite the downstream
2142 /// must reject on emptiness) — the accessor projects the raw
2143 /// `Option<BehaviorSpec>` slot's presence bit through the
2144 /// reference-return unchanged. Named `behavior()` to match the
2145 /// storage field's name verbatim and the tatara-lisp author-
2146 /// surface term (`:behavior`) the field's own docstring already
2147 /// carries.
2148 #[must_use]
2149 pub fn behavior(&self) -> Option<&crate::BehaviorSpec> {
2150 self.behavior.as_ref()
2151 }
2152
2153 /// Substrate-canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
2154 /// composite MESH-COMPOSITION-shaped mesh-policy optional-composite-
2155 /// reference accessor every consumer of the top-level manifest's
2156 /// per-Aplicacao [`crate::aplicacao::MeshPolicy`] outer-composite
2157 /// reader keys off — returns the author-declared `:politicas` typed
2158 /// composite verbatim as an `Option<&MeshPolicy>` reference over the
2159 /// same backing storage the raw `self.politicas.as_ref()` field
2160 /// access borrows from, with `None` naming the "no `:politicas`
2161 /// block authored — every per-axis mesh-policy scalar defers to the
2162 /// cluster-default arm named on the per-axis
2163 /// [`crate::aplicacao::MeshPolicy::timeout`] /
2164 /// [`crate::aplicacao::MeshPolicy::retries`] /
2165 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] /
2166 /// [`crate::aplicacao::MeshPolicy::mtls_required`] /
2167 /// [`crate::aplicacao::MeshPolicy::rate_limit`] scalar-accessor
2168 /// docstrings" partition every downstream caixa-mesh /
2169 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2170 /// "emit no per-`:politicas` overlay" and the sibling
2171 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2172 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2173 /// arm.
2174 ///
2175 /// The outer `:politicas` slot carries the M3 mesh-slot per-
2176 /// Aplicacao typed composite — the load-bearing container of every
2177 /// mesh-level policy axis every Cilium NetworkPolicy / Gateway API
2178 /// v1.x HTTPRoute / future M4 per-edge policy overlay emitter fans
2179 /// on (MESH-COMPOSITION §III.2 — the Aplicacao's typed mesh-policy
2180 /// composite; §V — the "no infinite blocking" per-call deadline +
2181 /// "sandboxing-by-default" mTLS-enforcement CSE invariants; §III.3
2182 /// — the typed inter-Servico contrato-edge overlay the per-`(:de,
2183 /// :para)` mesh renderer keys off). Every per-`:politicas` axis
2184 /// threads through a lifted per-slot accessor on the
2185 /// [`crate::aplicacao::MeshPolicy`] type: the
2186 /// [`crate::aplicacao::MeshPolicy::mtls_required`] (c0110f1) Cilium
2187 /// mTLS-enforcement toggle, the
2188 /// [`crate::aplicacao::MeshPolicy::retries`] (bdfb399) transient-
2189 /// failure retry budget, the [`crate::aplicacao::MeshPolicy::timeout`]
2190 /// (7073d0f) Gateway-API per-call deadline, the
2191 /// [`crate::aplicacao::MeshPolicy::circuit_breaker`] (b0e741a)
2192 /// Envoy-outlier-detection composite. Every downstream consumer
2193 /// that reaches for a mesh-policy axis first passes through this
2194 /// outer accessor onto the composite and then dispatches onto the
2195 /// per-axis accessor — the two-level dispatch means every per-
2196 /// `:politicas` reader now routes through a typed dispatch on the
2197 /// substrate primitive at both altitudes.
2198 ///
2199 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2200 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2201 /// author-omitted arm onto the [`crate::aplicacao::MeshPolicy::default`]
2202 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::politicas`]
2203 /// (534dc21) `&MeshPolicy`-return accessor observes a typed
2204 /// composite whether or not the author declared the outer slot.
2205 /// The outer accessor preserves the "author-omitted vs authored-
2206 /// empty" partition the inner accessor's `is_empty()`-gated
2207 /// renderer overlay collapses — routing the presence bit through
2208 /// this accessor keeps the [`Self::declared_mesh_slots`] M3 kind-
2209 /// coherence enumerator's `M3_AUTHOR_KEY_POLITICAS` push separate
2210 /// from the inner `MeshPolicy::is_empty()`-gated overlay elision.
2211 ///
2212 /// Prior to this lift the `.politicas` `Option<MeshPolicy>`
2213 /// composite was accessed inline at two production sites — the
2214 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2215 /// `self.politicas.clone().unwrap_or_default()` traversal head
2216 /// (caixa-core/src/manifest.rs:1899, which drives the fold onto
2217 /// the [`crate::aplicacao::MeshPolicy::default`] cluster-default
2218 /// arm the inner [`crate::AplicacaoSpec::politicas`] accessor
2219 /// then observes), and the [`Self::declared_mesh_slots`] M3
2220 /// declared-slot-set enumerator's `self.politicas.is_some()`
2221 /// presence probe (caixa-core/src/manifest.rs:1961, which drives
2222 /// the `M3_AUTHOR_KEY_POLITICAS` kebab-case author-label push
2223 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2224 /// coherence gate reads) — two open-coded outer-field accesses
2225 /// that expressed no compile-time link back to the typed slot at
2226 /// the [`Caixa`] altitude. A future extension of the `:politicas`
2227 /// outer axis to a richer author surface (a per-cluster
2228 /// `:politicas-overrides` slot the operator materializes at
2229 /// admission time so a cluster-specific policy can tighten the
2230 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2231 /// promotion of the plain `Option<MeshPolicy>` to a richer
2232 /// `{static, dynamic}` partition once the M4 per-edge
2233 /// contrato-scoped policy-override surface lands, the M5 traffic-
2234 /// shaping composition the caixa-operator's per-Aplicacao mesh
2235 /// admission webhook keys off) would have had to be threaded
2236 /// through both open-coded copies in lockstep or the Aplicacao-
2237 /// composition seed's default-fold arm would silently disagree
2238 /// with the M3 declared-slot enumerator on which policy composite
2239 /// a given Caixa resolves to — the seed reading an operator-
2240 /// resolved slot while the enumerator's presence probe read the
2241 /// raw slot would silently split the build-time mesh-artifact
2242 /// emission gate from the M3 declared-slot enumerator's kind-
2243 /// coherence gate, a two-consumer split far from the source
2244 /// `caixa.lisp` with no field naming the policy-drift root cause.
2245 /// Lifting the resolution rule to a typed method on the substrate
2246 /// primitive means every downstream consumer of the caixa's per-
2247 /// `Caixa` MESH-COMPOSITION mesh-policy outer-composite surface
2248 /// reaches for exactly one typed dispatch — the resolver's
2249 /// accept-set migrates as a unit on any future axis addition.
2250 ///
2251 /// Third outer top-level [`Caixa`] `Option<&Composite>`-return
2252 /// composite-reference accessor — sibling to the opening
2253 /// [`Self::limits`] (b2bd9d7) and [`Self::behavior`] (35d8b52)
2254 /// accessors on the outer-`Caixa` `Option<&Composite>` composite-
2255 /// reference sub-family, extends the "one typed dispatch on the
2256 /// substrate primitive, thin projections at each consumer"
2257 /// discipline onto the first of the three M3 mesh-slot axes.
2258 /// Peer of the closed inner mesh-slot outer-composite family the
2259 /// sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
2260 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2261 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2262 /// accessor pins already close on the inner [`crate::AplicacaoSpec`]
2263 /// altitude — opens the outer top-level [`Caixa`] altitude's M3
2264 /// mesh-slot arm of the composite-reference family the remaining
2265 /// two axes (`:placement`, `:entrada`) fold onto in future
2266 /// sibling lifts. Returns `Option<&MeshPolicy>` (not the owning
2267 /// composite by copy or clone) because every downstream consumer
2268 /// of the mesh-policy composite treats it as a read-only per-axis
2269 /// dispatch source — the reference-view is the narrowest borrow
2270 /// that supports every present + roadmapped consumer (per-axis
2271 /// accessor dispatch, `.is_empty()`-gated overlay projection,
2272 /// presence-probe early return on the "author-omitted `:politicas`
2273 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2274 /// seed's default-fold arm) without cloning the composite through
2275 /// every consumer's fast path. The `Option` half of the return-
2276 /// type preserves the load-bearing "author-omitted `:politicas` ⇒
2277 /// cluster-default applies" partition (not a default composite
2278 /// the downstream must reject on emptiness) — the accessor
2279 /// projects the raw `Option<MeshPolicy>` slot's presence bit
2280 /// through the reference-return unchanged. Named `politicas()` to
2281 /// match the storage field's name verbatim and the tatara-lisp
2282 /// author-surface term (`:politicas`) the field's own docstring
2283 /// already carries.
2284 #[must_use]
2285 pub fn politicas(&self) -> Option<&crate::aplicacao::MeshPolicy> {
2286 self.politicas.as_ref()
2287 }
2288
2289 /// Substrate-canonical per-`Caixa` `:placement` M3 mesh-slot outer-
2290 /// composite MESH-COMPOSITION-shaped distribution optional-composite-
2291 /// reference accessor every consumer of the top-level manifest's
2292 /// per-Aplicacao [`crate::aplicacao::Placement`] outer-composite
2293 /// reader keys off — returns the author-declared `:placement` typed
2294 /// composite verbatim as an `Option<&Placement>` reference over the
2295 /// same backing storage the raw `self.placement.as_ref()` field
2296 /// access borrows from, with `None` naming the "no `:placement`
2297 /// block authored — every per-axis placement scalar defers to the
2298 /// cluster-default arm named on the per-axis
2299 /// [`crate::aplicacao::Placement::estrategia`] /
2300 /// [`crate::aplicacao::Placement::clusters`] /
2301 /// [`crate::aplicacao::Placement::affinity`] /
2302 /// [`crate::aplicacao::Placement::shard_key`] scalar-accessor
2303 /// docstrings" partition every downstream caixa-mesh /
2304 /// caixa-flux / caixa-helm Aplicacao-artifact emitter treats as
2305 /// "emit no per-`:placement` overlay" and the sibling
2306 /// [`Self::aplicacao_view`] Aplicacao-composition seed folds through
2307 /// the [`crate::aplicacao::Placement::default`] cluster-default arm.
2308 ///
2309 /// The outer `:placement` slot carries the M3 mesh-slot per-
2310 /// Aplicacao typed distribution composite — the load-bearing
2311 /// container of every where-does-this-Aplicacao-run axis every
2312 /// caixa-mesh programs.yaml per-cluster distribution overlay /
2313 /// caixa-flux per-Aplicacao GitRepository/HelmRelease fan-out /
2314 /// future M4 per-Aplicacao Akka-style cluster-sharding entity-id
2315 /// resolver emitter fans on (MESH-COMPOSITION §II.4 — the
2316 /// Aplicacao's typed distribution composite; §V CSE invariants —
2317 /// "distribution is a first-class typed composite, not a runtime
2318 /// scheduler hint" the per-axis scalars enforce; §III.3 — the
2319 /// typed inter-Servico contrato-edge overlay the per-cluster
2320 /// mesh renderer keys off). Every per-`:placement` axis threads
2321 /// through a lifted per-slot accessor on the
2322 /// [`crate::aplicacao::Placement`] type: the
2323 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2324 /// MESH-COMPOSITION distribution-strategy scalar, the
2325 /// [`crate::aplicacao::Placement::clusters`] (a6e18d7) per-cluster
2326 /// distribution-target slice, the [`crate::aplicacao::Placement::affinity`]
2327 /// M3-Adaptive-compression-hint optional-scalar, and the
2328 /// [`crate::aplicacao::Placement::shard_key`] (7cd2a28) Akka-cluster-
2329 /// sharding extractor-expression optional-scalar. Every downstream
2330 /// consumer that reaches for a placement axis first passes through
2331 /// this outer accessor onto the composite and then dispatches onto
2332 /// the per-axis accessor — the two-level dispatch means every per-
2333 /// `:placement` reader now routes through a typed dispatch on the
2334 /// substrate primitive at both altitudes.
2335 ///
2336 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2337 /// seed: the Aplicacao-view builder folds the outer `Option`'s
2338 /// author-omitted arm onto the [`crate::aplicacao::Placement::default`]
2339 /// cluster-default, so the peer inner [`crate::AplicacaoSpec::placement`]
2340 /// (9abb8f0) `&Placement`-return accessor observes a typed composite
2341 /// whether or not the author declared the outer slot. The outer
2342 /// accessor preserves the "author-omitted vs authored-empty" partition
2343 /// the inner accessor collapses at the cluster-default fold —
2344 /// routing the presence bit through this accessor keeps the
2345 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2346 /// `M3_AUTHOR_KEY_PLACEMENT` push separate from the inner
2347 /// [`crate::AplicacaoSpec::validate_placement`]-gated overlay
2348 /// dispatch.
2349 ///
2350 /// Prior to this lift the `.placement` `Option<Placement>`
2351 /// composite was accessed inline at two production sites — the
2352 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2353 /// `self.placement.clone().unwrap_or_default()` traversal head
2354 /// (caixa-core/src/manifest.rs:2036, which drives the fold onto
2355 /// the [`crate::aplicacao::Placement::default`] cluster-default
2356 /// arm the inner [`crate::AplicacaoSpec::placement`] accessor
2357 /// then observes), and the [`Self::declared_mesh_slots`] M3
2358 /// declared-slot-set enumerator's `self.placement.is_some()`
2359 /// presence probe (caixa-core/src/manifest.rs:2100, which drives
2360 /// the `M3_AUTHOR_KEY_PLACEMENT` kebab-case author-label push
2361 /// every [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
2362 /// coherence gate reads) — two open-coded outer-field accesses
2363 /// that expressed no compile-time link back to the typed slot at
2364 /// the [`Caixa`] altitude. A future extension of the `:placement`
2365 /// outer axis to a richer author surface (a per-cluster
2366 /// `:placement-overrides` slot the operator materializes at
2367 /// admission time so a cluster-specific placement can tighten the
2368 /// caixa-declared bound without re-authoring the `caixa.lisp`, a
2369 /// per-tenant placement-alias table the M4
2370 /// `mesh.pleme.io/v1alpha1/Aplicacao` CR materializer resolves
2371 /// per-CR at admission time, a promotion of the plain
2372 /// `Option<Placement>` to a richer `{static, dynamic}` partition
2373 /// once Orleans-style virtual-actor dynamic placement comes into
2374 /// typed scope) would have had to be threaded through both open-
2375 /// coded copies in lockstep or the Aplicacao-composition seed's
2376 /// default-fold arm would silently disagree with the M3 declared-
2377 /// slot enumerator on which distribution composite a given Caixa
2378 /// resolves to — the seed reading an operator-resolved slot while
2379 /// the enumerator's presence probe read the raw slot would
2380 /// silently split the build-time distribution-artifact emission
2381 /// gate from the M3 declared-slot enumerator's kind-coherence
2382 /// gate, a two-consumer split far from the source `caixa.lisp`
2383 /// with no field naming the distribution-drift root cause.
2384 /// Lifting the resolution rule to a typed method on the substrate
2385 /// primitive means every downstream consumer of the caixa's per-
2386 /// `Caixa` MESH-COMPOSITION distribution outer-composite surface
2387 /// reaches for exactly one typed dispatch — the resolver's
2388 /// accept-set migrates as a unit on any future axis addition.
2389 ///
2390 /// Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
2391 /// composite-reference accessor — sibling to the opening
2392 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) M2-
2393 /// Servico-runtime pair and the peer [`Self::politicas`] (5d23d29)
2394 /// M3-mesh-slot arm on the outer-`Caixa` `Option<&Composite>`
2395 /// composite-reference sub-family, folds on the "one typed
2396 /// dispatch on the substrate primitive, thin projections at each
2397 /// consumer" discipline extended onto the second of the three M3
2398 /// mesh-slot axes. Peer of the closed inner mesh-slot outer-
2399 /// composite family the sibling
2400 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2401 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2402 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2403 /// accessor pins already close on the inner
2404 /// [`crate::AplicacaoSpec`] altitude — folds on the outer top-
2405 /// level [`Caixa`] altitude's M3 mesh-slot arm the sibling
2406 /// [`Self::politicas`] opened, extending the discipline onto the
2407 /// second of the three M3 mesh-slot axes. The remaining M3
2408 /// mesh-slot axis (`:entrada`) folds onto this accessor's
2409 /// discipline in the final sibling lift, closing the outer top-
2410 /// level [`Caixa`] `Option<&Composite>` M3 mesh-slot sub-family.
2411 /// Returns `Option<&Placement>` (not the owning composite by copy
2412 /// or clone) because every downstream consumer of the placement
2413 /// composite treats it as a read-only per-axis dispatch source —
2414 /// the reference-view is the narrowest borrow that supports every
2415 /// present + roadmapped consumer (per-axis accessor dispatch,
2416 /// serde composite-serialization on the programs.yaml overlay,
2417 /// presence-probe early return on the "author-omitted `:placement`
2418 /// ⇒ cluster-default applies" partition, `Aplicacao`-composition
2419 /// seed's default-fold arm) without cloning the composite through
2420 /// every consumer's fast path. The `Option` half of the return-
2421 /// type preserves the load-bearing "author-omitted `:placement` ⇒
2422 /// cluster-default applies" partition (not a default composite
2423 /// the downstream must reject on emptiness) — the accessor
2424 /// projects the raw `Option<Placement>` slot's presence bit
2425 /// through the reference-return unchanged. Named `placement()` to
2426 /// match the storage field's name verbatim and the tatara-lisp
2427 /// author-surface term (`:placement`) the field's own docstring
2428 /// already carries.
2429 #[must_use]
2430 pub fn placement(&self) -> Option<&crate::aplicacao::Placement> {
2431 self.placement.as_ref()
2432 }
2433
2434 /// Substrate-canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
2435 /// composite MESH-COMPOSITION-shaped external-gateway optional-
2436 /// composite-reference accessor every consumer of the top-level
2437 /// manifest's per-Aplicacao [`crate::aplicacao::Entrada`] outer-
2438 /// composite reader keys off — returns the author-declared
2439 /// `:entrada` typed composite verbatim as an `Option<&Entrada>`
2440 /// reference over the same backing storage the raw
2441 /// `self.entrada.as_ref()` field access borrows from, with `None`
2442 /// naming the "no `:entrada` block authored — this Aplicacao is
2443 /// cluster-internal, no `Gateway`/`HTTPRoute` fan-out emitted"
2444 /// partition every downstream caixa-mesh Gateway-API artifact
2445 /// emitter treats as "emit no gateway-listener + no `HTTPRoute`
2446 /// backend for this Aplicacao" and the sibling
2447 /// [`Self::aplicacao_view`] Aplicacao-composition seed forwards
2448 /// verbatim (unlike the peer `:politicas` / `:placement` arms,
2449 /// `:entrada` has no cluster-default fold — an omitted `:entrada`
2450 /// stays `None` on the projected [`crate::AplicacaoSpec`] and the
2451 /// peer inner [`crate::AplicacaoSpec::entrada`] accessor observes
2452 /// the same `Option<&Entrada>` presence bit unchanged).
2453 ///
2454 /// The outer `:entrada` slot carries the M3 mesh-slot per-
2455 /// Aplicacao typed external-gateway composite — the load-bearing
2456 /// container of every how-does-the-outside-world-reach-this-
2457 /// Aplicacao axis every caixa-mesh `Gateway`/`HTTPRoute` fan-out
2458 /// emitter fans on (MESH-COMPOSITION §II.5 — the Aplicacao's typed
2459 /// external-entry composite; §V CSE invariants — "the external
2460 /// gateway is a first-class typed composite, not a per-Servico
2461 /// ingress annotation" the per-axis scalars enforce; §III.4 — the
2462 /// typed hostname + backend-Servico pair the per-cluster Gateway-
2463 /// API renderer keys off). Every per-`:entrada` axis threads
2464 /// through a lifted per-slot accessor on the
2465 /// [`crate::aplicacao::Entrada`] type: the
2466 /// [`crate::aplicacao::Entrada::host`] Gateway-API `Listener.hostname`
2467 /// scalar, the [`crate::aplicacao::Entrada::para`] backend-Servico
2468 /// caixa-name scalar, the [`crate::aplicacao::Entrada::paths`]
2469 /// per-rule `HTTPPathMatch` list, the [`crate::aplicacao::Entrada::port`]
2470 /// backend `trigger.service.port` scalar, and the
2471 /// [`crate::aplicacao::Entrada::resolved_paths`] URL-path fallback
2472 /// resolver every HTTPRoute-aware renderer consumes. Every
2473 /// downstream consumer that reaches for an entry axis first passes
2474 /// through this outer accessor onto the composite and then
2475 /// dispatches onto the per-axis accessor — the two-level dispatch
2476 /// means every per-`:entrada` reader now routes through a typed
2477 /// dispatch on the substrate primitive at both altitudes.
2478 ///
2479 /// Composes through [`Self::aplicacao_view`]'s Aplicacao-composition
2480 /// seed: the Aplicacao-view builder forwards the outer `Option`
2481 /// arm verbatim (no default fold — `:entrada` is inherently
2482 /// optional; a cluster-internal Aplicacao has no external gateway
2483 /// at all, not "an external gateway that defaults to nothing"), so
2484 /// the peer inner [`crate::AplicacaoSpec::entrada`] (d32111c)
2485 /// `Option<&Entrada>`-return accessor observes the same presence
2486 /// bit whether or not the author declared the outer slot. Routing
2487 /// the presence bit through this accessor keeps the
2488 /// [`Self::declared_mesh_slots`] M3 kind-coherence enumerator's
2489 /// `M3_AUTHOR_KEY_ENTRADA` push separate from the inner
2490 /// [`crate::AplicacaoSpec::validate_entrada`]-gated
2491 /// hostname/backend/path emission dispatch.
2492 ///
2493 /// Prior to this lift the `.entrada` `Option<Entrada>` composite
2494 /// was accessed inline at two production sites — the
2495 /// [`Self::aplicacao_view`] Aplicacao-composition seed's
2496 /// `self.entrada.clone()` traversal head (caixa-core/src/manifest.rs:2182,
2497 /// which drives the forward onto the peer inner
2498 /// [`crate::AplicacaoSpec::entrada`] accessor the caixa-mesh
2499 /// Gateway-API fan-out then observes), and the
2500 /// [`Self::declared_mesh_slots`] M3 declared-slot-set enumerator's
2501 /// `self.entrada.is_some()` presence probe (caixa-core/src/manifest.rs:2248,
2502 /// which drives the `M3_AUTHOR_KEY_ENTRADA` kebab-case author-
2503 /// label push every [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
2504 /// kind-coherence gate reads) — two open-coded outer-field
2505 /// accesses that expressed no compile-time link back to the typed
2506 /// slot at the [`Caixa`] altitude. A future extension of the
2507 /// `:entrada` outer axis to a richer author surface (a per-cluster
2508 /// `:entrada-overrides` slot the operator materializes at admission
2509 /// time so a cluster-specific hostname can pin the caixa-declared
2510 /// bound without re-authoring the `caixa.lisp`, a per-tenant
2511 /// gateway-alias table the M4 `mesh.pleme.io/v1alpha1/Aplicacao`
2512 /// CR materializer resolves per-CR at admission time, a promotion
2513 /// of the plain `Option<Entrada>` to a richer
2514 /// `{public, private, internal}` partition once Cilium-identity-
2515 /// scoped internal gateways come into typed scope) would have had
2516 /// to be threaded through both open-coded copies in lockstep or the
2517 /// Aplicacao-composition seed's forward arm would silently
2518 /// disagree with the M3 declared-slot enumerator on which external-
2519 /// gateway composite a given Caixa resolves to — the seed reading
2520 /// an operator-resolved slot while the enumerator's presence probe
2521 /// read the raw slot would silently split the build-time gateway-
2522 /// artifact emission gate from the M3 declared-slot enumerator's
2523 /// kind-coherence gate, a two-consumer split far from the source
2524 /// `caixa.lisp` with no field naming the entry-drift root cause.
2525 /// Lifting the resolution rule to a typed method on the substrate
2526 /// primitive means every downstream consumer of the caixa's per-
2527 /// `Caixa` MESH-COMPOSITION external-gateway outer-composite
2528 /// surface reaches for exactly one typed dispatch — the resolver's
2529 /// accept-set migrates as a unit on any future axis addition.
2530 ///
2531 /// Fifth and final outer top-level [`Caixa`] `Option<&Composite>`-
2532 /// return composite-reference accessor — closes the outer-`Caixa`
2533 /// `Option<&Composite>` composite-reference sub-family opened by
2534 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) on the
2535 /// M2 Servico-runtime arm and extended onto the M3 mesh-slot arm
2536 /// by [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074),
2537 /// folds on the "one typed dispatch on the substrate primitive,
2538 /// thin projections at each consumer" discipline extended onto the
2539 /// third and final M3 mesh-slot axis. Peer of the closed inner
2540 /// mesh-slot outer-composite family the sibling
2541 /// [`crate::AplicacaoSpec::politicas`] (534dc21) /
2542 /// [`crate::AplicacaoSpec::placement`] (9abb8f0) /
2543 /// [`crate::AplicacaoSpec::entrada`] (d32111c) composite-reference
2544 /// accessor pins already close on the inner
2545 /// [`crate::AplicacaoSpec`] altitude — this lift closes the mirror
2546 /// sub-family on the outer top-level [`Caixa`] altitude, so both
2547 /// altitudes of the outer-composite reference-return discipline
2548 /// (per-`Caixa` outer-slot presence + per-`AplicacaoSpec` inner-
2549 /// slot presence) now carry the full five-arm accept-set behind a
2550 /// typed dispatch on the substrate primitive. Returns
2551 /// `Option<&Entrada>` (not the owning composite by copy or clone)
2552 /// because every downstream consumer of the entrada composite
2553 /// treats it as a read-only per-axis dispatch source — the
2554 /// reference-view is the narrowest borrow that supports every
2555 /// present + roadmapped consumer (per-axis accessor dispatch,
2556 /// serde composite-serialization on the programs.yaml overlay,
2557 /// presence-probe early return on the "author-omitted `:entrada`
2558 /// ⇒ cluster-internal Aplicacao" partition, `Aplicacao`-composition
2559 /// seed's forward arm) without cloning the composite through every
2560 /// consumer's fast path. The `Option` half of the return-type
2561 /// preserves the load-bearing "author-omitted `:entrada` ⇒
2562 /// cluster-internal Aplicacao" partition (not a default composite
2563 /// the downstream must reject on emptiness — a cluster-internal
2564 /// Aplicacao has no external gateway at all, not "a default gateway
2565 /// that emits nothing"); the accessor projects the raw
2566 /// `Option<Entrada>` slot's presence bit through the reference-
2567 /// return unchanged. Named `entrada()` to match the storage field's
2568 /// name verbatim and the tatara-lisp author-surface term
2569 /// (`:entrada`) the field's own docstring already carries.
2570 #[must_use]
2571 pub fn entrada(&self) -> Option<&crate::aplicacao::Entrada> {
2572 self.entrada.as_ref()
2573 }
2574
2575 /// Substrate-canonical per-`Caixa` `:ci` slot accessor — returns the
2576 /// author-declared typed CI run (`canteiro_types::CiRun`) verbatim as
2577 /// an `Option<&CiRun>`, borrowed from the typed slot's own
2578 /// `Option<CiRun>` storage. `None` when the slot is absent (every
2579 /// non-`Acao` kind, and an `Acao` caixa that hasn't declared `:ci`
2580 /// yet — the latter is caught by [`crate::LayoutError::MissingCi`],
2581 /// not silently accepted).
2582 ///
2583 /// Named `ci()` to match the storage field's name and the
2584 /// tatara-lisp author surface (`:ci`); mirrors the sibling
2585 /// `Option<&Composite>` accessors on this same `Caixa` altitude
2586 /// ([`Self::limits`], [`Self::behavior`], [`Self::politicas`],
2587 /// [`Self::placement`], [`Self::entrada`]) — one typed dispatch on
2588 /// the substrate primitive rather than an open-coded `self.ci.as_ref()`
2589 /// at every consumer.
2590 #[must_use]
2591 pub fn ci(&self) -> Option<&canteiro_types::CiRun> {
2592 self.ci.as_ref()
2593 }
2594
2595 /// Substrate-canonical per-`Caixa` `:estrategia` M2 supervisor-tree-
2596 /// slot flat-spread OTP-shaped sibling-restart-strategy discriminant
2597 /// accessor every consumer of the top-level manifest's per-Supervisor
2598 /// restart-strategy axis keys off — returns the author-declared
2599 /// `:estrategia` variant verbatim as an `Option<RestartStrategy>`,
2600 /// `Copy`-projected from the typed slot's own
2601 /// `Option<crate::supervisor::RestartStrategy>` storage. Optional
2602 /// (`:estrategia` is a flat-spread supervisor-only slot every
2603 /// non-`Supervisor`-kind `defcaixa` carries as `None` by
2604 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2605 /// still omit to defer to [`RestartStrategy::default`] —
2606 /// [`RestartStrategy::OneForOne`] — through the [`Self::supervisor_view`]
2607 /// `unwrap_or_default()` fold; a returned `None` degenerates to the
2608 /// [`SupervisorSpec::default`]-inherited strategy without any silent
2609 /// promotion to a fresh explicit variant at the accessor boundary).
2610 ///
2611 /// The `:estrategia` slot carries the M2 typed OTP-shaped sibling-
2612 /// restart-strategy discriminant every substrate-side per-Supervisor
2613 /// dispatch fans on (INSPIRATIONS §II.2 — OTP `supervisor:strategy`
2614 /// closed-set `one_for_one | one_for_all | rest_for_one |
2615 /// simple_one_for_one` algebra translated onto pleme-io's typed
2616 /// [`RestartStrategy`] enum; CAIXA-SDLC §II — the M2 supervisor-tree
2617 /// slot algebra the operator's hierarchical reconciliation scheduler
2618 /// fans on). The slot is *flat-spread* on the outer top-level `Caixa`
2619 /// (per the field-shape docstring at caixa-core/src/manifest.rs — "The
2620 /// supervisor slots are flat on Caixa (vs nested under a
2621 /// `SupervisorSpec` sub-form) to keep tatara-lisp authoring at one
2622 /// level of nesting"), so the accessor's altitude is the outer
2623 /// [`Caixa`] surface rather than the composed [`SupervisorSpec`]
2624 /// altitude the sibling [`crate::supervisor::SupervisorSpec::estrategia`]
2625 /// (eafb619) accessor keys off. The two typed axes — the outer
2626 /// author-surface `Option<RestartStrategy>` on the [`Caixa`] altitude
2627 /// (author-omitted arm carried as `None`) and the inner post-
2628 /// composition `RestartStrategy` on the [`SupervisorSpec`] altitude
2629 /// (`Option` collapsed through the [`Self::supervisor_view`]
2630 /// `unwrap_or_default()` fold) — now share one accessor discipline for
2631 /// the shared substrate concept "the author-declared OTP-shaped
2632 /// sibling-restart-strategy variant that partitions the downstream
2633 /// per-Supervisor renderer's per-arm fan-out"; the outer-altitude
2634 /// `None` arm is the pre-composition presence bit every declared-slot
2635 /// enumerator ([`Self::declared_supervisor_slots`]) reads, and the
2636 /// inner-altitude non-`Option` `RestartStrategy` is the post-
2637 /// composition partition-dispatch input every strategy-arm consumer
2638 /// ([`SupervisorSpec::validate`], the future wasm-operator's per-
2639 /// Supervisor sibling-restart branch, the future M4
2640 /// `mesh.pleme.io/v1alpha1/Supervisor` CR materializer's admission
2641 /// webhook) fans on.
2642 ///
2643 /// Prior to this lift the `.estrategia` field was accessed inline at
2644 /// two production sites in `caixa-core/src/manifest.rs` — the
2645 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`
2646 /// presence-probe arm at `if self.estrategia.is_some()` (which drives
2647 /// the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2648 /// coherence gate's per-slot label push) and the [`Self::supervisor_view`]
2649 /// `SupervisorSpec` construction site at `estrategia:
2650 /// self.estrategia.unwrap_or_default()` (which composes the flat-
2651 /// spread outer author-surface `Option<RestartStrategy>` onto the
2652 /// inner post-composition [`SupervisorSpec`] `RestartStrategy` field
2653 /// the [`SupervisorSpec::estrategia`] accessor keys off) — two open-
2654 /// coded field-accesses that expressed no compile-time link back to
2655 /// the typed slot. A future extension of the outer `:estrategia` axis
2656 /// to a richer author surface (a per-cluster strategy override the
2657 /// operator pins through a future `:estrategia-overrides` overlay the
2658 /// MESH-COMPOSITION §III.2 supervision-canary roadmap acknowledges,
2659 /// a per-tenant strategy-alias table the M4 CR materializer resolves
2660 /// per-CR, a per-Supervisor dynamic strategy derivation the future
2661 /// adaptive-supervision engine computes from child-failure-history
2662 /// topology, a per-child-cohort strategy split the future
2663 /// `RestForCohort` extension the INSPIRATIONS.md §II.2 Erlang/OTP
2664 /// absorption roadmap acknowledges, a promotion of the plain
2665 /// `Option<RestartStrategy>` to a richer
2666 /// `AuthorDeclaredStrategy { declared, overlay }` newtype once the
2667 /// operator-resolved overlay lands) would have had to be threaded
2668 /// through both open-coded copies in lockstep or the enumerator's
2669 /// presence probe and the composition site's `unwrap_or_default()`
2670 /// fold would silently disagree on which strategy a given [`Caixa`]
2671 /// resolves to (an author's `:estrategia OneForAll` would satisfy
2672 /// the enumerator's presence probe while the composition site
2673 /// silently rendered a stale `OneForOne`, or vice versa). Lifting
2674 /// the resolution rule to a typed method on the substrate primitive
2675 /// means every downstream consumer of the caixa's per-`Caixa` outer-
2676 /// altitude sibling-restart-strategy surface reaches for exactly one
2677 /// typed dispatch — the resolver's accept-set migrates as a unit on
2678 /// any future axis addition.
2679 ///
2680 /// First outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2681 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2682 /// axes — opens the outer-`Caixa` `Option<Copy>` flat-spread
2683 /// projection pattern the sibling per-`Caixa` `:max-restarts`
2684 /// `Option<u32>` and (through the future duration-newtype landing)
2685 /// `:restart-window` `Option<Duration>` future outer-scalar lifts
2686 /// fold on. Peer of the inner-altitude [`crate::supervisor::SupervisorSpec::estrategia`]
2687 /// (eafb619) `Copy`-return sibling-restart-strategy scalar accessor on
2688 /// the post-composition [`SupervisorSpec`] altitude — same "one
2689 /// typed dispatch on the substrate primitive, thin projections at
2690 /// each consumer" discipline extended onto the pre-composition outer
2691 /// author-surface [`Caixa`] altitude for the same OTP-shaped
2692 /// sibling-restart-strategy axis. Peer of the closed outer-`Caixa`
2693 /// `Option<&Composite>` composite-reference family the sibling
2694 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
2695 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
2696 /// [`Self::entrada`] (e4128e4) accessor pins already carry on the
2697 /// outer `Option<&Composite>` altitude — extends the outer-`Caixa`
2698 /// typed-slot accessor discipline onto the flat-spread M2 supervisor-
2699 /// tree `Option<Copy>`-discriminant sub-family the sibling M3
2700 /// [`crate::aplicacao::Placement::estrategia`] (921fe1b)
2701 /// `PlacementStrategy` `Copy`-composite-enum scalar accessor already
2702 /// pins on the inner-altitude per-`:placement` composite. Named
2703 /// `estrategia()` to match the storage field's name and the
2704 /// per-[`SupervisorSpec`] peer [`crate::supervisor::SupervisorSpec::estrategia`]
2705 /// / per-[`crate::aplicacao::Placement`] peer
2706 /// [`crate::aplicacao::Placement::estrategia`] method-name discipline
2707 /// verbatim; the accessor's identity name maps onto the canonical
2708 /// OTP-shape supervision vocabulary the [`RestartStrategy`] enum's
2709 /// docstring already carries.
2710 #[must_use]
2711 pub fn estrategia(&self) -> Option<crate::supervisor::RestartStrategy> {
2712 self.estrategia
2713 }
2714
2715 /// Substrate-canonical per-`Caixa` `:max-restarts` M2 supervisor-tree-
2716 /// slot flat-spread OTP-`MaxIntensity`-shaped restart-budget-count
2717 /// scalar accessor every consumer of the top-level manifest's per-
2718 /// Supervisor `:max-restarts` restart-budget-count axis keys off —
2719 /// returns the author-declared `:max-restarts` typed `Option<u32>`
2720 /// verbatim, `Copy`-projected from the typed slot's own `Option<u32>`
2721 /// storage (`u32` is `Copy`, so `Option<u32>` is `Copy` and the
2722 /// accessor returns by value; no borrow of `&self` past the call).
2723 /// Optional (`:max-restarts` is a flat-spread supervisor-only slot
2724 /// every non-`Supervisor`-kind `defcaixa` carries as `None` by
2725 /// `#[serde(default)]`, and every `Supervisor`-kind `defcaixa` may
2726 /// still omit to defer to the [`Self::supervisor_view`]
2727 /// `unwrap_or(5)` fold's OTP-canonical `{intensity, 5, 60}` default).
2728 ///
2729 /// The `:max-restarts` slot carries the M2 typed Erlang/OTP-shaped
2730 /// `MaxIntensity` restart-budget count that pairs with the sibling
2731 /// `:restart-window` `Period` to form the `MaxIntensity / Period`
2732 /// restart-intensity ratio the supervisor trips its own escalation on
2733 /// (INSPIRATIONS §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}`
2734 /// worker-supervisor default; RUNTIME-PATTERNS §II.2; CAIXA-SDLC §II
2735 /// — the M2 supervisor-tree slot algebra the operator's hierarchical
2736 /// reconciliation scheduler fans on). The slot is *flat-spread* on
2737 /// the outer top-level `Caixa` (per the field-shape docstring at
2738 /// caixa-core/src/manifest.rs — "The supervisor slots are flat on
2739 /// Caixa (vs nested under a `SupervisorSpec` sub-form)"), so the
2740 /// accessor's altitude is the outer [`Caixa`] surface rather than the
2741 /// composed [`SupervisorSpec`] altitude the sibling
2742 /// [`crate::supervisor::SupervisorSpec::max_restarts`] accessor keys
2743 /// off. The two typed axes — the outer author-surface `Option<u32>`
2744 /// on the [`Caixa`] altitude (author-omitted arm carried as `None`)
2745 /// and the inner post-composition `u32` on the [`SupervisorSpec`]
2746 /// altitude (`Option` collapsed through the [`Self::supervisor_view`]
2747 /// `unwrap_or(5)` fold) — now share one accessor discipline for the
2748 /// shared substrate concept "the author-declared OTP-shaped
2749 /// restart-budget count every downstream per-Supervisor consumer's
2750 /// restart-intensity budget-vs-count comparator fans on".
2751 ///
2752 /// Prior to this lift the `.max_restarts` field was accessed inline
2753 /// at two production sites in `caixa-core/src/manifest.rs` — the
2754 /// [`Self::declared_supervisor_slots`] `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`
2755 /// presence-probe arm at `if self.max_restarts.is_some()` (which
2756 /// drives the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
2757 /// kind-coherence gate's per-slot label push) and the
2758 /// [`Self::supervisor_view`] `SupervisorSpec` construction site at
2759 /// `max_restarts: self.max_restarts.unwrap_or(5)` (which composes the
2760 /// flat-spread outer author-surface `Option<u32>` onto the inner
2761 /// post-composition [`SupervisorSpec`] `u32` field the
2762 /// [`SupervisorSpec::max_restarts`] accessor keys off) — two open-
2763 /// coded field-accesses that expressed no compile-time link back to
2764 /// the typed slot. A future extension of the outer `:max-restarts`
2765 /// axis to a richer author surface (a per-cluster restart-budget
2766 /// override the operator pins through a future `:max-restarts-overrides`
2767 /// overlay the MESH-COMPOSITION §III.2 supervision-canary roadmap
2768 /// acknowledges, a per-tenant restart-budget-alias table the M4 CR
2769 /// materializer resolves per-CR, a per-Supervisor dynamic restart-
2770 /// budget derivation the future adaptive-supervision engine computes
2771 /// from child-failure-history topology, a promotion of the plain
2772 /// `Option<u32>` count to a richer `{MaxR, MaxT}` per-child-cohort
2773 /// restart-budget-partition once the INSPIRATIONS §II.2 Erlang/OTP
2774 /// per-child-cohort roadmap lands) would have had to be threaded
2775 /// through both open-coded copies in lockstep or the enumerator's
2776 /// presence probe and the composition site's `unwrap_or(5)` fold
2777 /// would silently disagree on which restart-budget a given [`Caixa`]
2778 /// resolves to (an author's `:max-restarts 10` would satisfy the
2779 /// enumerator's presence probe while the composition site silently
2780 /// composed the OTP-canonical `5`, or vice versa). Lifting the
2781 /// resolution rule to a typed method on the substrate primitive means
2782 /// every downstream consumer of the caixa's per-`Caixa` outer-altitude
2783 /// restart-budget-count surface reaches for exactly one typed dispatch
2784 /// — the resolver's accept-set migrates as a unit on any future axis
2785 /// addition.
2786 ///
2787 /// Second outer top-level [`Caixa`] `Option<Copy>`-return supervisor-
2788 /// tree-slot flat-spread accessor for M2 supervisor-slot Copy-carry
2789 /// axes — folds on the outer-`Caixa` `Option<Copy>` flat-spread
2790 /// projection pattern the sibling per-`Caixa`
2791 /// [`Self::estrategia`] (ed04d3c) accessor opened, extends the
2792 /// sub-family onto the sibling `Option<u32>` restart-budget-count arm.
2793 /// Peer of the inner-altitude
2794 /// [`crate::supervisor::SupervisorSpec::max_restarts`] `u32` accessor
2795 /// on the post-composition [`SupervisorSpec`] altitude — same "one
2796 /// typed dispatch on the substrate primitive, thin projections at
2797 /// each consumer" discipline extended onto the pre-composition outer
2798 /// author-surface [`Caixa`] altitude for the same OTP-`MaxIntensity`-
2799 /// shaped restart-budget-count axis. Named `max_restarts()` to match
2800 /// the storage field's name and the per-[`SupervisorSpec`] peer
2801 /// [`crate::supervisor::SupervisorSpec::max_restarts`] method-name
2802 /// discipline verbatim; the accessor's identity maps onto the
2803 /// canonical OTP-shape supervision vocabulary the `:max-restarts`
2804 /// field's docstring already carries.
2805 #[must_use]
2806 pub const fn max_restarts(&self) -> Option<u32> {
2807 self.max_restarts
2808 }
2809
2810 /// Substrate-canonical per-`Caixa` `:restart-window` M2 supervisor-
2811 /// tree-slot flat-spread OTP-`Period`-shaped restart-intensity-
2812 /// denominator raw-duration-string scalar accessor every consumer of
2813 /// the top-level manifest's per-Supervisor `:restart-window` sliding-
2814 /// window axis keys off — returns the author-declared `:restart-window`
2815 /// typed `Option<String>` verbatim as an `Option<&str>`, borrowed
2816 /// from the typed slot's own `Option<String>` storage. `None` when
2817 /// the slot is absent (the canonical "never reset — every restart
2818 /// across the supervisor's lifetime counts against the sibling
2819 /// `:max-restarts` budget" sentinel every non-`Supervisor`-kind
2820 /// `defcaixa` carries by `#[serde(default)]` and every
2821 /// `Supervisor`-kind `defcaixa` may still omit to defer to the
2822 /// [`Self::supervisor_view`] `restart_window: None` composition
2823 /// through the [`crate::supervisor::duration_codec::parse`] soft-
2824 /// swallow `.and_then(|s| … .ok())` fold).
2825 ///
2826 /// The `:restart-window` slot carries the raw M2 typed Erlang/OTP-
2827 /// shaped `Period` sliding-observation-interval duration string that
2828 /// pairs with the sibling `:max-restarts` `MaxIntensity` restart-
2829 /// budget count to form the `MaxIntensity / Period` restart-intensity
2830 /// ratio the supervisor trips its own escalation on (INSPIRATIONS
2831 /// §II.2 — Erlang/OTP `supervisor` `{intensity, 5, 60}` worker-
2832 /// supervisor default; RUNTIME-PATTERNS §II.2). The outer-`Caixa`
2833 /// slot stores the raw duration string (`"60s"`, `"5m"`, `"500ms"`)
2834 /// authored under `:restart-window` — the typed [`SupervisorSpec`]
2835 /// holds an `Option<Duration>` routed through the shared
2836 /// [`crate::supervisor::duration_codec`] via `with = "duration_codec"`
2837 /// — so the outer altitude's accessor returns `Option<&str>` (raw
2838 /// authoring surface) while the inner altitude's
2839 /// [`crate::supervisor::SupervisorSpec::restart_window`] returns
2840 /// `Option<Duration>` (parsed typed surface). The parse-refusal arm
2841 /// is closed by the sibling [`Self::validate_restart_window`] gate
2842 /// that surfaces [`ManifestError::RestartWindowMalformed`] naming
2843 /// the offending value; the view-construction path
2844 /// [`Self::supervisor_view`] soft-swallows the same parse error to
2845 /// `None` to keep the view best-effort.
2846 ///
2847 /// Prior to this lift the `.restart_window` field was accessed inline
2848 /// at three production sites in `caixa-core/src/manifest.rs` — the
2849 /// [`Self::declared_supervisor_slots`]
2850 /// `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` presence-probe arm at
2851 /// `if self.restart_window.is_some()` (which drives the
2852 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
2853 /// coherence gate's per-slot label push), the
2854 /// [`Self::validate_restart_window`] `let Some(s) =
2855 /// self.restart_window.as_deref()` empty-and-shape gate binding
2856 /// (which folds the raw string through the shared
2857 /// [`crate::supervisor::duration_codec::parse`] to surface
2858 /// [`ManifestError::RestartWindowMalformed`] naming the offending
2859 /// value), and the [`Self::supervisor_view`] `self.restart_window
2860 /// .as_deref().and_then(…)` view-construction fold (which composes
2861 /// the flat-spread outer author-surface `Option<String>` onto the
2862 /// inner post-composition [`SupervisorSpec`] `Option<Duration>`
2863 /// field the [`SupervisorSpec::restart_window`] accessor keys off) —
2864 /// three open-coded field-accesses that expressed no compile-time
2865 /// link back to the typed slot. A future extension of the outer
2866 /// `:restart-window` axis to a richer author surface (a per-cluster
2867 /// window override, a per-tenant window-alias table, a per-Supervisor
2868 /// dynamic window derivation the future adaptive-supervision engine
2869 /// computes from child-failure-history topology, a promotion of the
2870 /// plain `Option<String>` raw duration to a typed `Option<Duration>`
2871 /// once the future author-surface parser lands at the [`Caixa`]
2872 /// altitude and the raw-string form is retired) would have had to be
2873 /// threaded through every open-coded copy in lockstep or the three
2874 /// consumers would silently disagree on which raw string a given
2875 /// [`Caixa`] resolves to. Lifting the resolution rule to a typed
2876 /// method on the substrate primitive means every downstream consumer
2877 /// of the caixa's per-`Caixa` outer-altitude restart-window raw-
2878 /// string surface reaches for exactly one typed dispatch — the
2879 /// resolver's accept-set migrates as a unit on any future axis
2880 /// addition.
2881 ///
2882 /// Third outer top-level [`Caixa`] supervisor-tree-slot flat-spread
2883 /// accessor — folds on the outer-`Caixa` M2 supervisor-tree flat-
2884 /// spread projection pattern the sibling per-`Caixa`
2885 /// [`Self::estrategia`] (ed04d3c) `Option<Copy>` and
2886 /// [`Self::max_restarts`] `Option<Copy>` accessors opened, extends
2887 /// the sub-family onto the sibling `Option<&str>` raw-duration-
2888 /// string arm (the outer altitude's raw-string form; the inner
2889 /// altitude's parsed [`Duration`] form is the peer
2890 /// [`crate::supervisor::SupervisorSpec::restart_window`] accessor).
2891 /// Peer of the sibling per-`Caixa` `Option<&str>`-return scalar
2892 /// accessors ([`Self::licenca`] / [`Self::repositorio`] /
2893 /// [`Self::descricao`] / [`Self::edicao`]) on the universal-axis
2894 /// outer scalar-projection family the outer-`Caixa` `Option<&str>`
2895 /// sub-family already carries — same "one typed dispatch on the
2896 /// substrate primitive, thin projections at each consumer"
2897 /// discipline extended onto the M2 supervisor-tree flat-spread
2898 /// `Option<&str>` raw-duration-string arm. Named `restart_window()`
2899 /// to match the storage field's name and the per-[`SupervisorSpec`]
2900 /// peer [`crate::supervisor::SupervisorSpec::restart_window`]
2901 /// method-name discipline verbatim; the accessor's identity maps
2902 /// onto the canonical OTP-shape supervision vocabulary the
2903 /// `:restart-window` field's docstring already carries.
2904 #[must_use]
2905 pub fn restart_window(&self) -> Option<&str> {
2906 self.restart_window.as_deref()
2907 }
2908
2909 /// Substrate-canonical per-`Caixa` `:upgrade-from` M2 typed-slot
2910 /// outer-composite OTP-appup-shaped per-prior-version migration-
2911 /// entry-list slice accessor every consumer of the top-level
2912 /// manifest's per-Servico hot-upgrade-block `&[UpgradeFromEntry]`
2913 /// slice-view keys off — returns the author-declared `:upgrade-from`
2914 /// typed `Vec<UpgradeFromEntry>` verbatim as a
2915 /// `&[UpgradeFromEntry]` slice-view over the same backing buffer
2916 /// the raw `self.upgrade_from.as_slice()` field access borrows
2917 /// from. Empty-slice-carrying (the "no hot-upgrade path declared"
2918 /// arm every `defcaixa` without an `:upgrade-from` block carries;
2919 /// the [`Self::from_lisp`] derive folds an omitted `:upgrade-from`
2920 /// through `#[serde(default)]` to `Vec::new()`, so a `Caixa` past
2921 /// parse definitionally carries a `Vec<UpgradeFromEntry>` slot —
2922 /// possibly empty — and the returned `&[UpgradeFromEntry]`
2923 /// degenerates to an empty slice on that arm without any silent
2924 /// `None` collapse).
2925 ///
2926 /// The outer `:upgrade-from` slot carries the M2 typed OTP-appup
2927 /// migration block — the load-bearing container of every per-
2928 /// prior-`:versao` migration-instruction list the wasm-operator
2929 /// dispatches on at hot-upgrade time (INSPIRATIONS §II.4 — OTP
2930 /// `.appup` per-prior-version `LoadModule | StateChange |
2931 /// SoftPurge | Purge | Restart` instruction algebra translated
2932 /// onto pleme-io's typed `:upgrade-from :from` + `:instructions`
2933 /// entry list; CAIXA-SDLC §II — the typed-M2 slot algebra the
2934 /// operator's hot-upgrade dispatch fans on). Every per-entry axis
2935 /// threads through a lifted per-entry accessor on the
2936 /// [`UpgradeFromEntry`] type: the
2937 /// [`UpgradeFromEntry::prior_versao`] SemVer-shaped previous-
2938 /// version scalar accessor and the
2939 /// [`UpgradeFromEntry::instructions`] `&[UpgradeInstruction]`-
2940 /// return per-entry instruction-list accessor (0137e5a). Every
2941 /// downstream consumer of the hot-upgrade path first passes
2942 /// through this outer accessor onto the slice and then dispatches
2943 /// per-entry through the inner accessors — the two-level dispatch
2944 /// means every per-`:upgrade-from` reader now routes through a
2945 /// typed dispatch on the substrate primitive at both altitudes.
2946 ///
2947 /// Prior to this lift the `.upgrade_from` `Vec<UpgradeFromEntry>`
2948 /// slot was accessed inline at production sites across three
2949 /// files — the [`Self::declared_servico_slots`] M2 declared-slot
2950 /// enumerator's `self.upgrade_from.is_empty()` presence probe
2951 /// (caixa-core/src/manifest.rs, which drives the
2952 /// `M2_AUTHOR_KEY_UPGRADE_FROM` kebab-case author-label push every
2953 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-coherence
2954 /// gate reads), the [`crate::StandardLayout::verify`] per-
2955 /// `:upgrade-from` three-stage validation pass (caixa-core/src/
2956 /// layout.rs, which fans onto the
2957 /// [`crate::upgrade::validate_upgrade_from`] per-entry shape +
2958 /// cross-entry duplicate gate, the
2959 /// [`crate::upgrade::validate_upgrade_from_against_versao`]
2960 /// SemVer-precedence cross-slot gate, the
2961 /// [`crate::upgrade::validate_upgrade_from_against_behavior`]
2962 /// `:state-change` ↔ `:on-state-change` cross-slot composition
2963 /// gate, and the per-instruction script-path existence-probe walk
2964 /// that reads each entry's [`UpgradeFromEntry::instructions`] to
2965 /// resolve every declared migration script against the layout
2966 /// root), and the [`crate::render::servico_m2_overlay`] per-
2967 /// Servico M2 overlay emitter's `!caixa.upgrade_from.is_empty()`
2968 /// presence gate + `serde_yaml::to_value(&caixa.upgrade_from)`
2969 /// projection (caixa-core/src/render.rs, which drives the
2970 /// `M2_KEY_UPGRADE_FROM`-keyed `serde_yaml` projection every
2971 /// `caixa-helm` / `caixa-flux` Servico values-block emitter fans
2972 /// on and lands as the ComputeUnit CR's `spec.upgradeFrom` field).
2973 /// A future extension of the outer `:upgrade-from` axis (a per-
2974 /// cluster `:upgrade-overrides` overlay the wasm-engine operator
2975 /// resolves at admission time so a cluster-specific migration
2976 /// policy can tighten a caixa-declared step without re-authoring
2977 /// the `caixa.lisp`, promotion of the plain
2978 /// `Vec<UpgradeFromEntry>` to a richer `{static, dynamic}`
2979 /// partition once runtime-resolved hot-upgrade instructions land,
2980 /// per-entry priority annotation once multi-strategy fan-out
2981 /// lands) would have had to be threaded through all six open-
2982 /// coded copies in lockstep or one consumer would silently
2983 /// disagree with the peers on which upgrade slice a given Caixa
2984 /// resolves to — a six-consumer split at the enumerator, the
2985 /// three-stage validate pass, the script-path probe walk, and the
2986 /// M2 overlay emitter, far from the source `caixa.lisp` with no
2987 /// field naming the upgrade-drift root cause. Lifting the
2988 /// resolution rule to a typed method on the substrate primitive
2989 /// means every downstream consumer of the caixa's per-`Caixa`
2990 /// OTP-appup outer-slice surface reaches for exactly one typed
2991 /// dispatch — the resolver's accept-set migrates as a unit on any
2992 /// future axis addition.
2993 ///
2994 /// First outer top-level [`Caixa`] `&[Composite]`-return slice
2995 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the
2996 /// outer-`Caixa` `&[Composite]` composite-slice projection
2997 /// pattern the sibling `:children`
2998 /// [`crate::supervisor::ChildSpec`] / `:membros`
2999 /// [`crate::aplicacao::Membro`] / `:contratos`
3000 /// [`crate::aplicacao::WitContract`] future outer-composite-slice
3001 /// lifts fold on. Peer of the closed outer-`Caixa` scalar
3002 /// `Option<&Composite>` composite-reference family the sibling
3003 /// [`Self::limits`] (b2bd9d7) / [`Self::behavior`] (35d8b52) /
3004 /// [`Self::politicas`] (5d23d29) / [`Self::placement`] (4fb8074) /
3005 /// [`Self::entrada`] (e4128e4) accessors closed on the outer
3006 /// `Option<&Composite>` altitude, extended here to the outer-
3007 /// `Caixa` `&[Composite]` vec-carry altitude. Peer at the inner
3008 /// altitude of [`crate::upgrade::UpgradeFromEntry::instructions`]
3009 /// (0137e5a) — same "one typed dispatch on the substrate
3010 /// primitive, thin projections at each consumer" discipline
3011 /// folded onto the outer top-level [`Caixa`] altitude, opening the
3012 /// M2 vec-carry slot family's outer-composite-slice axis. Sibling
3013 /// in shape to the peer outer-`Caixa` `&[Dep]`-return
3014 /// [`Self::deps`] (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and
3015 /// `&[String]`-return [`Self::autores`] (b5d813f) /
3016 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`]
3017 /// (8a36c23) / [`Self::exe`] (65d9527) / [`Self::servicos`]
3018 /// (611f78b) slice-accessors on the sibling outer-`Caixa` scalar-
3019 /// element vec-carry axes — folds the "outer [`Caixa`] `&[T]`
3020 /// slice" projection pattern onto the sibling M2 typed-composite-
3021 /// element axis (`UpgradeFromEntry` composite, matching the
3022 /// per-inner [`UpgradeFromEntry::instructions`] element type at a
3023 /// different altitude).
3024 ///
3025 /// Returns `&[UpgradeFromEntry]` (not `&Vec<UpgradeFromEntry>`)
3026 /// because every downstream consumer of the hot-upgrade list
3027 /// treats it as a read-only sequence — the slice-view is the
3028 /// narrowest borrow that supports every present + roadmapped
3029 /// consumer (`.iter()`, `.len()`, `.is_empty()`, `serde` slice-
3030 /// serialization through
3031 /// `serde_yaml::to_value(&[UpgradeFromEntry])`) without leaking
3032 /// the backing `Vec`'s grow/push/reserve surface no consumer of
3033 /// the typed view reaches for (the storage-side `Vec` remains
3034 /// reachable through the `pub upgrade_from` field for the
3035 /// mutation-carrying serde round-trip and per-test fixture-
3036 /// mutation paths). Named `upgrade_from()` to match the storage
3037 /// field's `snake_case` name; the kebab-case author-surface tag
3038 /// `:upgrade-from` is the same axis after tatara-lisp's
3039 /// kebab↔snake fold and the accessor's identity maps onto the
3040 /// canonical CAIXA-SDLC §II vocabulary the slot's docstring
3041 /// already carries.
3042 #[must_use]
3043 pub fn upgrade_from(&self) -> &[UpgradeFromEntry] {
3044 self.upgrade_from.as_slice()
3045 }
3046
3047 /// Substrate-canonical per-`Caixa` `:children` M2 supervisor-tree-
3048 /// slot outer-composite OTP-shaped per-supervisor static-child-list
3049 /// slice accessor every consumer of the top-level manifest's per-
3050 /// Supervisor `&[ChildSpec]` slice-view keys off — returns the
3051 /// author-declared `:children` typed `Vec<crate::supervisor::ChildSpec>`
3052 /// verbatim as a `&[crate::supervisor::ChildSpec]` slice-view over
3053 /// the same backing buffer the raw `self.children.as_slice()` field
3054 /// access borrows from. Empty-slice-carrying (the "no static children
3055 /// declared" arm every non-`Supervisor`-kind `defcaixa` carries by
3056 /// #[serde(default)] and every `SimpleOneForOne` supervisor carries
3057 /// by [`crate::supervisor::SupervisorError::SimpleOneForOneWithStaticChildren`]
3058 /// gate; the returned `&[ChildSpec]` degenerates to an empty slice
3059 /// on those arms without any silent `None` collapse).
3060 ///
3061 /// The outer `:children` slot carries the M2 typed OTP-supervisor
3062 /// static-child list — the load-bearing container of every per-
3063 /// child `{caixa, versao, restart}` triple the wasm-operator's
3064 /// hierarchical reconciler dispatches on at supervisor-tree
3065 /// materialization time (INSPIRATIONS §II.2 — OTP `supervisor:init/1`
3066 /// static-child list translated onto pleme-io's typed
3067 /// [`crate::supervisor::ChildSpec`] entry list; CAIXA-SDLC §II —
3068 /// the typed-M2 slot algebra the operator's per-supervisor fan-out
3069 /// dispatch fans on). Every per-child axis threads through a lifted
3070 /// per-entry accessor on the [`crate::supervisor::ChildSpec`] type:
3071 /// the [`crate::supervisor::ChildSpec::nome`] DNS-1123-label
3072 /// child-caixa-identity scalar accessor, the peer versao SemVer-2
3073 /// version-requirement scalar accessor, and the
3074 /// [`crate::supervisor::ChildSpec::restart`] `Copy`-composite-enum
3075 /// per-child post-exit restart-decision-policy discriminant
3076 /// accessor (dfb4a81). Every downstream consumer of the supervisor-
3077 /// tree path first passes through this outer accessor onto the
3078 /// slice and then dispatches per-child through the inner accessors
3079 /// — the two-level dispatch means every per-`:children` reader now
3080 /// routes through a typed dispatch on the substrate primitive at
3081 /// both altitudes.
3082 ///
3083 /// Prior to this lift the `.children` `Vec<ChildSpec>` slot was
3084 /// accessed inline at three production sites across two files —
3085 /// the [`Self::declared_supervisor_slots`] supervisor-tree
3086 /// declared-slot enumerator's `!self.children.is_empty()` presence
3087 /// probe (caixa-core/src/manifest.rs, which drives the
3088 /// `SUPERVISOR_AUTHOR_KEY_CHILDREN` kebab-case author-label push
3089 /// every [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
3090 /// kind-coherence gate reads), the [`Self::supervisor_view`]
3091 /// per-supervisor typed-view composer's `self.children.clone()`
3092 /// per-child fold-in path (caixa-core/src/manifest.rs, which
3093 /// materializes the typed [`crate::supervisor::SupervisorSpec`]
3094 /// view every [`crate::StandardLayout::verify`] Supervisor-arm gate
3095 /// dispatches on), and the [`crate::StandardLayout::verify`] per-
3096 /// `:children :caixa` self-parent refusal probe's
3097 /// `&caixa.children`-borrowed
3098 /// [`crate::supervisor::validate_no_self_supervision`] input
3099 /// (caixa-core/src/layout.rs, which pins the "no child names the
3100 /// supervisor's own `:nome`" cross-slot coherence gate). A future
3101 /// extension of the outer `:children` axis (a per-cluster
3102 /// `:children-overrides` overlay the wasm-engine operator resolves
3103 /// at admission time so a cluster-specific child-set can tighten
3104 /// a caixa-declared list without re-authoring the `caixa.lisp`,
3105 /// promotion of the plain `Vec<ChildSpec>` to a richer
3106 /// `{static, dynamic}` partition once Erlang/OTP's
3107 /// `simple_one_for_one`-shaped dynamic-child slot lands as a typed
3108 /// axis, per-child priority annotation once multi-strategy fan-out
3109 /// lands) would have had to be threaded through all three open-
3110 /// coded copies in lockstep or one consumer would silently
3111 /// disagree with the peers on which child slice a given Caixa
3112 /// resolves to — the enumerator's presence probe reading the raw
3113 /// slot while the peer view-composer's fold-in path read an
3114 /// operator-resolved slot would silently split the paired
3115 /// declared-slot enumerator and typed-view composition, and the
3116 /// [`crate::supervisor::validate_no_self_supervision`] self-parent
3117 /// refusal probe reading a third borrow would silently drift the
3118 /// cross-slot coherence gate's traversal input from the two peers,
3119 /// a three-consumer split at the enumerator, the view composer,
3120 /// and the self-parent gate far from the source `caixa.lisp` with
3121 /// no field naming the child-set-drift root cause. Lifting the
3122 /// resolution rule to a typed method on the substrate primitive
3123 /// means every downstream consumer of the caixa's per-`Caixa`
3124 /// OTP-supervisor outer-slice surface reaches for exactly one
3125 /// typed dispatch — the resolver's accept-set migrates as a unit
3126 /// on any future axis addition.
3127 ///
3128 /// Second outer top-level [`Caixa`] `&[Composite]`-return slice
3129 /// accessor for M2 / M3 typed-slot vec-carry axes — folds on the
3130 /// outer-`Caixa` `&[Composite]` composite-slice sub-family the
3131 /// sibling [`Self::upgrade_from`] (2a1f907) accessor opened, peer
3132 /// at the outer altitude of the closed inner-`SupervisorSpec`
3133 /// [`crate::SupervisorSpec::children`] (bc92bce) accessor on the
3134 /// same OTP-supervisor static-child-list axis — same "byte-equal,
3135 /// borrow-shared" outer-accessor discipline extended onto the
3136 /// second outer-`Caixa` `&[Composite]` vec-carry axis. Sibling in
3137 /// shape to the peer outer-`Caixa` `&[Dep]`-return [`Self::deps`]
3138 /// (ad34b4e) / [`Self::deps_dev`] (f7fd81e) and `&[String]`-return
3139 /// [`Self::autores`] (b5d813f) / [`Self::etiquetas`] (78c7d3c) /
3140 /// [`Self::bibliotecas`] (8a36c23) / [`Self::exe`] (65d9527) /
3141 /// [`Self::servicos`] (611f78b) slice-accessors on the sibling
3142 /// outer-`Caixa` scalar-element vec-carry axes — folds the "outer
3143 /// [`Caixa`] `&[T]` slice" projection pattern onto the sibling
3144 /// M2 typed-composite-element axis
3145 /// ([`crate::supervisor::ChildSpec`] composite, matching the
3146 /// per-inner [`crate::SupervisorSpec::children`] element type at a
3147 /// different altitude).
3148 ///
3149 /// Returns `&[crate::supervisor::ChildSpec]` (not
3150 /// `&Vec<ChildSpec>`) because every downstream consumer of the
3151 /// child list treats it as a read-only sequence — the slice-view
3152 /// is the narrowest borrow that supports every present +
3153 /// roadmapped consumer (`.iter()`, `.len()`, `.is_empty()`, the
3154 /// [`crate::supervisor::validate_no_self_supervision`] `&[ChildSpec]`
3155 /// input, `serde` slice-serialization) without leaking the backing
3156 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3157 /// reaches for (the storage-side `Vec` remains reachable through
3158 /// the `pub children` field for the mutation-carrying serde round-
3159 /// trip and per-test fixture-mutation paths, including the
3160 /// [`Self::supervisor_view`] fold-in path that clones the slot
3161 /// into the typed view). Named `children()` to match the storage
3162 /// field's name verbatim and the tatara-lisp author-surface term
3163 /// (`:children`) the field's own docstring already carries; the
3164 /// accessor's identity maps onto the canonical OTP supervision
3165 /// vocabulary the [`Caixa::children`] field's docstring already
3166 /// reaches for ("Static children of a supervisor").
3167 #[must_use]
3168 pub fn children(&self) -> &[crate::supervisor::ChildSpec] {
3169 self.children.as_slice()
3170 }
3171
3172 /// Substrate-canonical per-`Caixa` `:membros` M3 mesh-slot outer-
3173 /// composite MESH-COMPOSITION-shaped per-Aplicacao member-list slice
3174 /// accessor every consumer of the top-level manifest's per-Aplicacao
3175 /// `&[crate::aplicacao::Membro]` slice-view keys off — returns the
3176 /// author-declared `:membros` typed `Vec<crate::aplicacao::Membro>`
3177 /// verbatim as a `&[crate::aplicacao::Membro]` slice-view over the
3178 /// same backing buffer the raw `self.membros.as_slice()` field access
3179 /// borrows from. Empty-slice-carrying (the "no members declared" arm
3180 /// every non-`Aplicacao`-kind `defcaixa` carries by `#[serde(default)]`
3181 /// and every partially-authored Aplicacao carries before the
3182 /// [`crate::AplicacaoError::MembrosEmpty`] gate fires; the returned
3183 /// `&[Membro]` degenerates to an empty slice on those arms without any
3184 /// silent `None` collapse).
3185 ///
3186 /// The outer `:membros` slot carries the M3 typed MESH-COMPOSITION
3187 /// per-Aplicacao member list — the load-bearing container of every
3188 /// per-member `{caixa, versao}` pair the caixa-mesh renderer's
3189 /// per-Aplicacao program-emission dispatch fans on at mesh-artifact
3190 /// materialization time (MESH-COMPOSITION §III.1 — the typed graph's
3191 /// vertex set the `:contratos` `:de`/`:para` edges resolve against and
3192 /// the `:entrada :para` external-gateway destination validates
3193 /// against; CAIXA-SDLC §II — the typed-M3 slot algebra the operator's
3194 /// per-Aplicacao fan-out dispatch fans on). Every per-member axis
3195 /// threads through a lifted per-entry accessor on the
3196 /// [`crate::aplicacao::Membro`] type: the
3197 /// [`crate::aplicacao::Membro::nome`] DNS-1123-label member-caixa-
3198 /// identity scalar accessor (4a32abf) and the peer
3199 /// [`crate::aplicacao::Membro::versao_requirement`] SemVer-2
3200 /// version-requirement scalar accessor (a40b0e3). Every downstream
3201 /// consumer of the mesh-graph path first passes through this outer
3202 /// accessor onto the slice and then dispatches per-member through
3203 /// the inner accessors — the two-level dispatch means every per-
3204 /// `:membros` reader now routes through a typed dispatch on the
3205 /// substrate primitive at both altitudes.
3206 ///
3207 /// Prior to this lift the `.membros` `Vec<Membro>` slot was accessed
3208 /// inline at three production sites across two files — the
3209 /// [`Self::declared_mesh_slots`] mesh-slot declared-slot
3210 /// enumerator's `!self.membros.is_empty()` presence probe
3211 /// (caixa-core/src/manifest.rs, which drives the
3212 /// `M3_AUTHOR_KEY_MEMBROS` kebab-case author-label push every
3213 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3214 /// gate reads), the [`Self::aplicacao_view`] per-Aplicacao typed-view
3215 /// composer's `self.membros.clone()` per-member fold-in path
3216 /// (caixa-core/src/manifest.rs, which materializes the typed
3217 /// [`crate::aplicacao::AplicacaoSpec`] view every
3218 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate dispatches
3219 /// on), and the [`crate::StandardLayout::verify`] per-`:membros
3220 /// :caixa` self-membership refusal probe's `&caixa.membros`-borrowed
3221 /// [`crate::aplicacao::validate_no_self_membership`] input
3222 /// (caixa-core/src/layout.rs, which pins the "no member names the
3223 /// Aplicacao's own `:nome`" cross-slot coherence gate). A future
3224 /// extension of the outer `:membros` axis (a per-cluster
3225 /// `:membros-overrides` overlay the wasm-engine operator resolves at
3226 /// admission time so a cluster-specific member-set can tighten a
3227 /// caixa-declared list without re-authoring the `caixa.lisp`,
3228 /// promotion of the plain `Vec<Membro>` to a richer
3229 /// `{static, dynamic}` partition once runtime-resolved Aplicacao
3230 /// members land as a typed axis, per-member priority annotation once
3231 /// multi-strategy fan-out lands) would have had to be threaded
3232 /// through all three open-coded copies in lockstep or one consumer
3233 /// would silently disagree with the peers on which member slice a
3234 /// given Caixa resolves to — the enumerator's presence probe reading
3235 /// the raw slot while the peer view-composer's fold-in path read an
3236 /// operator-resolved slot would silently split the paired
3237 /// declared-slot enumerator and typed-view composition, and the
3238 /// [`crate::aplicacao::validate_no_self_membership`] self-membership
3239 /// refusal probe reading a third borrow would silently drift the
3240 /// cross-slot coherence gate's traversal input from the two peers, a
3241 /// three-consumer split at the enumerator, the view composer, and
3242 /// the self-membership gate far from the source `caixa.lisp` with no
3243 /// field naming the member-set-drift root cause. Lifting the
3244 /// resolution rule to a typed method on the substrate primitive
3245 /// means every downstream consumer of the caixa's per-`Caixa`
3246 /// MESH-COMPOSITION outer-slice surface reaches for exactly one
3247 /// typed dispatch — the resolver's accept-set migrates as a unit on
3248 /// any future axis addition.
3249 ///
3250 /// Third outer top-level [`Caixa`] `&[Composite]`-return slice
3251 /// accessor for M2 / M3 typed-slot vec-carry axes — opens the outer-
3252 /// `Caixa` M3 mesh-slot arm of the `&[Composite]` composite-slice
3253 /// sub-family the sibling M2 [`Self::upgrade_from`] (2a1f907) /
3254 /// [`Self::children`] (c17b51e) accessors opened for the M2 vec-carry
3255 /// altitude. Peer at the outer altitude of the closed inner-
3256 /// [`crate::AplicacaoSpec::membros`] (6c77e36) accessor on the same
3257 /// MESH-COMPOSITION per-Aplicacao member-list axis — the two
3258 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3259 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3260 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3261 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3262 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3263 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3264 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3265 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3266 /// pattern onto the sibling M3 typed-composite-element axis
3267 /// ([`crate::aplicacao::Membro`] composite, matching the per-inner
3268 /// [`crate::AplicacaoSpec::membros`] element type at a different
3269 /// altitude).
3270 ///
3271 /// Returns `&[crate::aplicacao::Membro]` (not `&Vec<Membro>`)
3272 /// because every downstream consumer of the member list treats it
3273 /// as a read-only sequence — the slice-view is the narrowest borrow
3274 /// that supports every present + roadmapped consumer (`.iter()`,
3275 /// `.len()`, `.is_empty()`, the
3276 /// [`crate::aplicacao::validate_no_self_membership`] `&[Membro]`
3277 /// input, `serde` slice-serialization) without leaking the backing
3278 /// `Vec`'s grow/push/reserve surface no consumer of the typed view
3279 /// reaches for (the storage-side `Vec` remains reachable through the
3280 /// `pub membros` field for the mutation-carrying serde round-trip
3281 /// and per-test fixture-mutation paths, including the
3282 /// [`Self::aplicacao_view`] fold-in path that clones the slot into
3283 /// the typed view). Named `membros()` to match the storage field's
3284 /// name verbatim and the tatara-lisp author-surface term
3285 /// (`:membros`) the field's own docstring already carries; the
3286 /// accessor's identity maps onto the canonical MESH-COMPOSITION
3287 /// vocabulary the [`Caixa::membros`] field's docstring already
3288 /// reaches for ("Member Servicos that make up this Aplicacao").
3289 #[must_use]
3290 pub fn membros(&self) -> &[crate::aplicacao::Membro] {
3291 self.membros.as_slice()
3292 }
3293
3294 /// Substrate-canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
3295 /// composite MESH-COMPOSITION-shaped per-Aplicacao WIT-typed
3296 /// inter-Servico contract-list slice accessor every consumer of the
3297 /// top-level manifest's per-Aplicacao `&[crate::aplicacao::WitContract]`
3298 /// slice-view keys off — returns the author-declared `:contratos`
3299 /// typed `Vec<crate::aplicacao::WitContract>` verbatim as a
3300 /// `&[crate::aplicacao::WitContract]` slice-view over the same
3301 /// backing buffer the raw `self.contratos.as_slice()` field access
3302 /// borrows from. Empty-slice-carrying (the "no contracts declared"
3303 /// arm every non-`Aplicacao`-kind `defcaixa` carries by
3304 /// `#[serde(default)]` and every leaf Aplicacao carrying only a
3305 /// single member with no inter-Servico edge carries; the returned
3306 /// `&[WitContract]` degenerates to an empty slice on those arms
3307 /// without any silent `None` collapse).
3308 ///
3309 /// The outer `:contratos` slot carries the M3 typed MESH-COMPOSITION
3310 /// per-Aplicacao WIT-typed inter-Servico edge list — the load-bearing
3311 /// container of every per-edge `{de, para, wit, endpoint | subject |
3312 /// slot}` quadruple the caixa-mesh renderer's per-Aplicacao
3313 /// `CiliumNetworkPolicy` fan-out (one L7 policy per edge —
3314 /// MESH-COMPOSITION §III.2 point 2) and per-`(:de, :para)`
3315 /// adjacency-list seed dispatch on at mesh-artifact materialization
3316 /// time (MESH-COMPOSITION §III.1 — the typed graph's edge set the
3317 /// `:membros` vertex set resolves against, closed by the
3318 /// [`crate::AplicacaoError::ContractoUnknownMember`] / cycle-refusal
3319 /// gates in §III.3; CAIXA-SDLC §II — the typed-M3 slot algebra the
3320 /// operator's per-Aplicacao fan-out dispatch fans on). Every
3321 /// per-edge axis threads through a lifted per-entry accessor on the
3322 /// [`crate::aplicacao::WitContract`] type: the peer `de` / `para`
3323 /// DNS-1123-label member-caixa-name endpoint scalar accessors, the
3324 /// [`crate::aplicacao::WitContract::endpoint`] (7020470) HTTP-shape
3325 /// / [`crate::aplicacao::WitContract::subject`] (90de675)
3326 /// NATS-pub-sub-shape / [`crate::aplicacao::WitContract::slot`]
3327 /// (ed22b66) `wasi:keyvalue/store`-shape payload-carrier accessors,
3328 /// and the WIT-world discriminant. Every downstream consumer of the
3329 /// mesh-graph edge path first passes through this outer accessor
3330 /// onto the slice and then dispatches per-contract through the
3331 /// inner accessors — the two-level dispatch means every
3332 /// per-`:contratos` reader now routes through a typed dispatch on
3333 /// the substrate primitive at both altitudes.
3334 ///
3335 /// Prior to this lift the `.contratos` `Vec<WitContract>` slot was
3336 /// accessed inline at two production sites in
3337 /// caixa-core/src/manifest.rs — the [`Self::declared_mesh_slots`]
3338 /// mesh-slot declared-slot enumerator's
3339 /// `!self.contratos.is_empty()` presence probe (which drives the
3340 /// `M3_AUTHOR_KEY_CONTRATOS` kebab-case author-label push every
3341 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-coherence
3342 /// gate reads) and the [`Self::aplicacao_view`] per-Aplicacao
3343 /// typed-view composer's `self.contratos.clone()` per-contract
3344 /// fold-in path (which materializes the typed
3345 /// [`crate::aplicacao::AplicacaoSpec`] view every
3346 /// [`crate::StandardLayout::verify`] Aplicacao-arm gate and every
3347 /// downstream `caixa-mesh` renderer dispatches on). A future
3348 /// extension of the outer `:contratos` axis (a per-cluster
3349 /// `:contratos-overrides` overlay the wasm-engine operator resolves
3350 /// at admission time so a cluster-specific edge-set can tighten a
3351 /// caixa-declared list without re-authoring the `caixa.lisp`,
3352 /// promotion of the plain `Vec<WitContract>` to a richer
3353 /// `{static, dynamic}` partition once runtime-resolved contract
3354 /// edges land, per-edge policy annotation once the M4 per-edge
3355 /// policy overlay axis lands) would have had to be threaded through
3356 /// both open-coded copies in lockstep or one consumer would
3357 /// silently disagree with the peer on which edge slice a given
3358 /// Caixa resolves to — the enumerator's presence probe reading the
3359 /// raw slot while the peer view-composer's fold-in path read an
3360 /// operator-resolved slot would silently split the paired
3361 /// declared-slot enumerator and typed-view composition, a
3362 /// two-consumer split at the enumerator and the view composer far
3363 /// from the source `caixa.lisp` with no field naming the edge-set-
3364 /// drift root cause. Lifting the resolution rule to a typed method
3365 /// on the substrate primitive means every downstream consumer of
3366 /// the caixa's per-`Caixa` MESH-COMPOSITION outer-slice surface
3367 /// reaches for exactly one typed dispatch — the resolver's
3368 /// accept-set migrates as a unit on any future axis addition.
3369 ///
3370 /// Fourth and final outer top-level [`Caixa`] `&[Composite]`-return
3371 /// slice accessor for M2 / M3 typed-slot vec-carry axes — closes
3372 /// the outer-`Caixa` `&[Composite]` composite-slice sub-family the
3373 /// sibling M2 [`Self::upgrade_from`] (2a1f907) / [`Self::children`]
3374 /// (c17b51e) accessors opened and the M3 [`Self::membros`]
3375 /// (0f26987) accessor folded on, and closes the outer-`Caixa` M3
3376 /// mesh-slot arm of the composite-slice sub-family the sibling
3377 /// [`Self::membros`] accessor opened for the M3 vec-carry altitude.
3378 /// Peer at the outer altitude of the closed inner-
3379 /// [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
3380 /// same MESH-COMPOSITION per-Aplicacao contract-list axis — the two
3381 /// altitudes now share the same "byte-equal, borrow-shared" outer-
3382 /// accessor discipline. Sibling in shape to the peer outer-`Caixa`
3383 /// `&[Dep]`-return [`Self::deps`] (ad34b4e) / [`Self::deps_dev`]
3384 /// (f7fd81e) and `&[String]`-return [`Self::autores`] (b5d813f) /
3385 /// [`Self::etiquetas`] (78c7d3c) / [`Self::bibliotecas`] (8a36c23) /
3386 /// [`Self::exe`] (65d9527) / [`Self::servicos`] (611f78b) slice-
3387 /// accessors on the sibling outer-`Caixa` scalar-element vec-carry
3388 /// axes — folds the "outer [`Caixa`] `&[T]` slice" projection
3389 /// pattern onto the sibling M3 typed-composite-element axis
3390 /// ([`crate::aplicacao::WitContract`] composite, matching the
3391 /// per-inner [`crate::AplicacaoSpec::contratos`] element type at a
3392 /// different altitude).
3393 ///
3394 /// Returns `&[crate::aplicacao::WitContract]` (not
3395 /// `&Vec<WitContract>`) because every downstream consumer of the
3396 /// contract list treats it as a read-only sequence — the slice-view
3397 /// is the narrowest borrow that supports every present + roadmapped
3398 /// consumer (`.iter()`, `.len()`, `.is_empty()`, per-edge WIT-world
3399 /// discriminant dispatch, `serde` slice-serialization) without
3400 /// leaking the backing `Vec`'s grow/push/reserve surface no
3401 /// consumer of the typed view reaches for (the storage-side `Vec`
3402 /// remains reachable through the `pub contratos` field for the
3403 /// mutation-carrying serde round-trip and per-test fixture-mutation
3404 /// paths, including the [`Self::aplicacao_view`] fold-in path that
3405 /// clones the slot into the typed view). Named `contratos()` to
3406 /// match the storage field's name verbatim and the tatara-lisp
3407 /// author-surface term (`:contratos`) the field's own docstring
3408 /// already carries; the accessor's identity maps onto the canonical
3409 /// MESH-COMPOSITION vocabulary the [`Caixa::contratos`] field's
3410 /// docstring already reaches for ("WIT-typed inter-Servico
3411 /// contracts").
3412 #[must_use]
3413 pub fn contratos(&self) -> &[crate::aplicacao::WitContract] {
3414 self.contratos.as_slice()
3415 }
3416
3417 /// Compose the Aplicacao-related flat slots into a single typed
3418 /// [`crate::aplicacao::AplicacaoSpec`] for validation +
3419 /// downstream renderer consumption. Returns `None` when the
3420 /// caixa isn't a `:kind Aplicacao`.
3421 #[must_use]
3422 pub fn aplicacao_view(&self) -> Option<crate::aplicacao::AplicacaoSpec> {
3423 if !self.kind().is_aplicacao() {
3424 return None;
3425 }
3426 Some(crate::aplicacao::AplicacaoSpec {
3427 membros: self.membros().to_vec(),
3428 contratos: self.contratos().to_vec(),
3429 politicas: self.politicas().cloned().unwrap_or_default(),
3430 placement: self.placement().cloned().unwrap_or_default(),
3431 entrada: self.entrada().cloned(),
3432 })
3433 }
3434
3435 /// The kebab-case `:slot` tags of every M3 mesh slot this caixa
3436 /// *declares* a value on, in canonical declaration order
3437 /// (`:membros` → `:contratos` → `:politicas` → `:placement` →
3438 /// `:entrada`). A slot counts as declared when its backing field
3439 /// carries a value — a non-empty `Vec`, or a `Some(...)`.
3440 ///
3441 /// The M3 mesh slots compose the typed graph of a `:kind Aplicacao`
3442 /// (MESH-COMPOSITION §III.1). [`Self::aplicacao_view`] only folds
3443 /// them into a validatable [`crate::aplicacao::AplicacaoSpec`] when
3444 /// the kind matches (returns `None` otherwise), and the caixa-mesh /
3445 /// caixa-flux / caixa-helm renderers only emit them for an
3446 /// Aplicacao. On any *other* kind a declared mesh slot is the
3447 /// manifest field's documented "ignored otherwise" (see the
3448 /// `:membros` … `:entrada` field docs): it silently passes
3449 /// [`Caixa::from_lisp`] and then vanishes — never validated, never
3450 /// rendered — far from the source caixa.lisp.
3451 /// [`crate::StandardLayout::verify`] consults this to reject that
3452 /// silent-drop at caixa-build time
3453 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]), mirroring the
3454 /// `SupervisorOwnsCode` / `AplicacaoOwnsCode` kind-coherence gates:
3455 /// a slot foreign to the kind is a build error, not a silent drop.
3456 ///
3457 /// Lifted as a typed method (rather than an inline disjunction at
3458 /// the verify call site) so the mesh-slot set lives in one place —
3459 /// a future M4 axis added to the Aplicacao surface (per-edge policy
3460 /// overlay, distributed-app takeover config) is one push here, and
3461 /// every consumer reaching for "which mesh slots are set" (the
3462 /// verify gate, a future `feira lint` kind-coherence advisory)
3463 /// inherits the canonical order without rolling its own.
3464 ///
3465 /// Each per-arm kebab-case label is routed through the peer
3466 /// [`crate::M3_AUTHOR_KEY_MEMBROS`] /
3467 /// [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
3468 /// [`crate::M3_AUTHOR_KEY_POLITICAS`] /
3469 /// [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
3470 /// [`crate::M3_AUTHOR_KEY_ENTRADA`] consts declared next to the
3471 /// [`crate::M3_KEY_PLACEMENT`] renderer-side wire-key peer, so both
3472 /// halves of every M3 top-level mesh slot's dual axis (author-facing
3473 /// kebab-case label + renderer-side artifact key) route through one
3474 /// canonical declaration per arm — same discipline the peer
3475 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`]
3476 /// / [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot consts
3477 /// (f49c8b0) establish on the sibling per-Servico M2 top-level slot
3478 /// axis, extended here to close the M3 mesh-slot author-facing-label
3479 /// axis so both altitudes of the typed-slot algebra
3480 /// (per-Servico M2 + per-Aplicacao M3) share the same
3481 /// "one canonical byte-string per arm, next to the axis" discipline.
3482 #[must_use]
3483 pub fn declared_mesh_slots(&self) -> Vec<&'static str> {
3484 let mut slots = Vec::new();
3485 if !self.membros().is_empty() {
3486 slots.push(crate::render::M3_AUTHOR_KEY_MEMBROS);
3487 }
3488 if !self.contratos().is_empty() {
3489 slots.push(crate::render::M3_AUTHOR_KEY_CONTRATOS);
3490 }
3491 if self.politicas().is_some() {
3492 slots.push(crate::render::M3_AUTHOR_KEY_POLITICAS);
3493 }
3494 if self.placement().is_some() {
3495 slots.push(crate::render::M3_AUTHOR_KEY_PLACEMENT);
3496 }
3497 if self.entrada().is_some() {
3498 slots.push(crate::render::M3_AUTHOR_KEY_ENTRADA);
3499 }
3500 slots
3501 }
3502
3503 /// The kebab-case `:slot` tags of every supervisor-tree slot this
3504 /// caixa *declares* a value on, in canonical declaration order
3505 /// (`:estrategia` → `:max-restarts` → `:restart-window` →
3506 /// `:children`). A slot counts as declared when its backing field
3507 /// carries a value — a `Some(...)`, or a non-empty `Vec`.
3508 ///
3509 /// The supervisor-tree slots compose the typed OTP supervisor of a
3510 /// `:kind Supervisor` (INSPIRATIONS §II.2; the `:estrategia` +
3511 /// `:children` field docs above). [`Self::supervisor_view`] only
3512 /// folds them into a validatable [`SupervisorSpec`] when the kind
3513 /// matches (returns `None` otherwise), and the wasm-operator's
3514 /// hierarchical reconciler only consumes them for a Supervisor. On
3515 /// any *other* kind a declared supervisor slot is the manifest
3516 /// field's documented "ignored otherwise" (see the `:estrategia` …
3517 /// `:children` field docs): it silently passes [`Caixa::from_lisp`]
3518 /// and then vanishes — never validated, never reconciled — far from
3519 /// the source caixa.lisp. [`crate::StandardLayout::verify`] consults
3520 /// this to reject that silent-drop at caixa-build time
3521 /// ([`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]), the
3522 /// exact mirror of the [`Self::declared_mesh_slots`] /
3523 /// [`crate::LayoutError::MeshSlotsOnNonAplicacao`] gate on the
3524 /// Aplicacao-only slot set: a slot foreign to the kind is a build
3525 /// error, not a silent drop.
3526 #[must_use]
3527 pub fn declared_supervisor_slots(&self) -> Vec<&'static str> {
3528 let mut slots = Vec::new();
3529 if self.estrategia().is_some() {
3530 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA);
3531 }
3532 if self.max_restarts().is_some() {
3533 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS);
3534 }
3535 if self.restart_window().is_some() {
3536 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW);
3537 }
3538 if !self.children().is_empty() {
3539 slots.push(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN);
3540 }
3541 slots
3542 }
3543
3544 /// The kebab-case `:slot` tags of every M2 Servico-runtime slot this
3545 /// caixa *declares* a value on, in canonical declaration order
3546 /// (`:limits` → `:behavior` → `:upgrade-from`). A slot counts as
3547 /// declared when its backing field carries a value — a `Some(...)`,
3548 /// or a non-empty `Vec`.
3549 ///
3550 /// The M2 slots configure the runtime of a long-running wasm
3551 /// component, i.e. a `:kind Servico`: `:limits` is Lunatic
3552 /// per-process sandboxing (INSPIRATIONS §III.1), `:behavior` is the
3553 /// OTP `gen_server` callback set (§II.3), `:upgrade-from` is the OTP
3554 /// appup hot-code-reload table (§II.4). The caixa-helm / caixa-flux
3555 /// renderers gate on [`crate::require_kind`]`(_, Servico)` and only
3556 /// emit these slots for a Servico; on any *other* kind a declared M2
3557 /// slot is the manifest field's documented "ignored otherwise": its
3558 /// well-formedness is checked by [`crate::StandardLayout::verify`]
3559 /// but the value is never rendered into a chart / programs.yaml entry
3560 /// — it silently passes [`Caixa::from_lisp`] + `feira build` and then
3561 /// vanishes, far from the source caixa.lisp.
3562 /// [`crate::StandardLayout::verify`] consults this to reject that
3563 /// silent-drop at caixa-build time
3564 /// ([`crate::LayoutError::ServicoSlotsOnNonServico`]), the exact
3565 /// mirror of the [`Self::declared_mesh_slots`] /
3566 /// [`Self::declared_supervisor_slots`] gates on the peer
3567 /// kind-exclusive slot sets: a slot foreign to the kind is a build
3568 /// error, not a silent drop.
3569 ///
3570 /// Each per-arm kebab-case label is routed through the peer
3571 /// [`crate::M2_AUTHOR_KEY_LIMITS`] / [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
3572 /// [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts declared next to the
3573 /// [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
3574 /// [`crate::M2_KEY_UPGRADE_FROM`] renderer-side wire-key peers, so
3575 /// both halves of the M2 top-level slot's dual axis (author-facing
3576 /// kebab-case label + renderer-side camelCase overlay-container wire
3577 /// key) route through one canonical declaration per arm — same
3578 /// discipline the peer [`crate::M2_BEHAVIOR_AUTHOR_KEY_ON_*`] sub-slot
3579 /// author-label consts (889dc18) establish on the sibling
3580 /// per-callback axis inside the `:behavior` overlay block.
3581 #[must_use]
3582 pub fn declared_servico_slots(&self) -> Vec<&'static str> {
3583 let mut slots = Vec::new();
3584 if self.limits().is_some() {
3585 slots.push(crate::render::M2_AUTHOR_KEY_LIMITS);
3586 }
3587 if self.behavior().is_some() {
3588 slots.push(crate::render::M2_AUTHOR_KEY_BEHAVIOR);
3589 }
3590 if !self.upgrade_from().is_empty() {
3591 slots.push(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM);
3592 }
3593 slots
3594 }
3595
3596 /// The kebab-case `:slot` tags of every code-surface slot this caixa
3597 /// declares a value on that its [`CaixaKind`] doesn't natively own,
3598 /// in canonical declaration order (`:exe` → `:servicos`). A
3599 /// code-surface slot is owned by exactly one kind: `:exe` by
3600 /// [`CaixaKind::Binario`] (the nix-built executable surface), and
3601 /// `:servicos` by [`CaixaKind::Servico`] (the wasm component +
3602 /// `ComputeUnit` daemon surface).
3603 ///
3604 /// Each is silently ignored when declared on the wrong kind: the
3605 /// caixa-helm / caixa-flux / caixa-flake renderers gate on
3606 /// [`crate::require_kind`]`(_, <owning-kind>)`, so on any *other*
3607 /// code-running kind a declared `:exe` / `:servicos` is the manifest
3608 /// field's documented "ignored otherwise" — its path is checked for
3609 /// existence by the layout's `bibliotecas`/`exe`/`servicos` loops
3610 /// (which run after [`Caixa::from_lisp`]), but the value is never
3611 /// rendered into a build target or programs.yaml entry. It silently
3612 /// passes [`Caixa::from_lisp`] + `feira build`, far from the source
3613 /// caixa.lisp, with no field naming which slot is foreign.
3614 ///
3615 /// [`crate::StandardLayout::verify`] consults this to reject that
3616 /// silent-drop at caixa-build time
3617 /// ([`crate::LayoutError::ForeignCodeSlot`]), beside the M2
3618 /// servico-runtime, supervisor-tree, and M3 mesh kind-coherence
3619 /// gates ([`Self::declared_servico_slots`] /
3620 /// [`Self::declared_supervisor_slots`] /
3621 /// [`Self::declared_mesh_slots`]): the fourth kind ↔ slot algebra
3622 /// axis to be closed on the typed surface. The Supervisor /
3623 /// Aplicacao "no code at all" cases ([`crate::LayoutError::SupervisorOwnsCode`]
3624 /// / [`crate::LayoutError::AplicacaoOwnsCode`]) keep their dedicated
3625 /// diagnostics — they fire ahead of this gate on the same `verify`
3626 /// pass, so for Supervisor / Aplicacao the `OwnCode` arm always wins
3627 /// and this method is moot. For Biblioteca / Binario / Servico, this
3628 /// gate fires when a code-running kind declares another code-running
3629 /// kind's exclusive code surface.
3630 ///
3631 /// `:bibliotecas` is deliberately excluded — a Binario or Servico
3632 /// may legitimately ship a `lib/` helper that the underlying
3633 /// substrate (the nix flake for Binario, the wasm component build
3634 /// for Servico) bundles into its build, so the slot's
3635 /// declared-on-wrong-kind cardinality isn't a structural error on
3636 /// either code-running kind. A Biblioteca declaring `:bibliotecas`
3637 /// is the native case (the slot's owning kind). Supervisor /
3638 /// Aplicacao declaring `:bibliotecas` is gated upstream by
3639 /// [`crate::LayoutError::SupervisorOwnsCode`] /
3640 /// [`crate::LayoutError::AplicacaoOwnsCode`].
3641 ///
3642 /// Lifted as a typed method (rather than an inline disjunction at
3643 /// the verify call site) so the foreign-code-slot set lives in one
3644 /// place — a future kind that gains its own code-surface slot is
3645 /// one push here, and every consumer reaching for "which code
3646 /// surfaces are foreign to this kind" (the verify gate, a future
3647 /// `feira lint` kind-coherence advisory, the future `app-operator`'s
3648 /// per-caixa build-target classifier) inherits the canonical order
3649 /// without rolling its own.
3650 #[must_use]
3651 pub fn declared_foreign_code_slots(&self) -> Vec<&'static str> {
3652 let mut slots = Vec::new();
3653 if !self.exe().is_empty() && !self.kind().requires_exe() {
3654 slots.push(":exe");
3655 }
3656 if !self.servicos().is_empty() && !self.kind().requires_servicos() {
3657 slots.push(":servicos");
3658 }
3659 slots
3660 }
3661
3662 /// Validate every entry of `:deps` and `:deps-dev` through
3663 /// [`Dep::validate`] — closing the parity loop with the per-axis
3664 /// `:versao` gates already wired into the typed-graph
3665 /// ([`crate::AplicacaoSpec::validate_membros`] for `:membros`,
3666 /// 9888b13) and typed supervisor tree
3667 /// ([`crate::SupervisorSpec::validate`] for `:children`, b38ff3a).
3668 ///
3669 /// Until this gate landed `:deps :versao` and `:deps-dev :versao`
3670 /// were the only `:versao` axes still untyped past
3671 /// [`Caixa::from_lisp`]: the derive macro stored the requirement
3672 /// as a String without parsing it, so a malformed-but-non-empty
3673 /// requirement (`"^bad-version"`, `"^^0.1"`, `"v0.1"`, `"not-a-req"`)
3674 /// silently passed parse and the `semver::Error` surfaced at
3675 /// lacre-resolve time, far from the source caixa.lisp, with no
3676 /// field naming which `:deps` entry carried the typo. Lifting the
3677 /// gate here makes the four `:versao` typed surfaces (`:deps`,
3678 /// `:deps-dev`, `:membros`, `:children`) structurally equivalent —
3679 /// every requirement string past `validate_deps` is round-trippable
3680 /// through [`crate::parse_requirement`] without re-checking at the
3681 /// resolver layer.
3682 ///
3683 /// Both lists run through the same per-entry validator so a typo
3684 /// in `:deps-dev` surfaces with the same diagnostic as one in
3685 /// `:deps` — neither axis is a second-class citizen of the typed
3686 /// surface.
3687 ///
3688 /// Within each list, [`DepError::DuplicateNome`] closes the
3689 /// set-not-multiset discipline on the `:nome` axis: two entries
3690 /// naming the same caixa carry two `:versao` / `:fonte` / feature
3691 /// triples that the caixa-resolver's lacre pipeline collapses to one
3692 /// via its `HashMap`-keyed-by-`:nome` consumption — the second entry
3693 /// silently overwrites the first at `concrete_versao`-resolve time
3694 /// (the same "second wins / one silently overwrites the other"
3695 /// shape the peer typed-graph duplicate gates already close on every
3696 /// other Vec-shaped authoring surface that keys by name). The
3697 /// duplicate check fires per-list and runs *after* each per-entry
3698 /// [`Dep::validate`] call so a malformed-and-duplicated entry
3699 /// surfaces its narrower per-entry diagnostic
3700 /// ([`DepError::NomeInvalid`], [`DepError::VersaoInvalid`],
3701 /// [`DepError::FonteRepoEmpty`], …) before the cross-entry duplicate
3702 /// diagnostic — the canonical "per-entry shape before cross-entry
3703 /// uniqueness" precedence the peer `:children :caixa`
3704 /// ([`crate::SupervisorSpec::validate`]), `:membros :caixa`
3705 /// ([`crate::AplicacaoSpec::validate_membros`]), `:contratos`
3706 /// ([`crate::AplicacaoSpec::validate`]), `:placement :clusters`
3707 /// ([`crate::AplicacaoSpec::validate_placement`]),
3708 /// `:entrada :paths` ([`crate::AplicacaoSpec::validate`]),
3709 /// `:upgrade-from :from` ([`crate::upgrade::validate_upgrade_from`]),
3710 /// and the within-`:upgrade-from`-entry per-instruction-class
3711 /// singularity gates ([`crate::UpgradeError::DuplicateLoadModule`],
3712 /// [`crate::UpgradeError::DuplicateStateChange`],
3713 /// [`crate::UpgradeError::DuplicateCleanup`]) all establish.
3714 ///
3715 /// Cross-list (`:deps` ↔ `:deps-dev`) coincidence is *not* gated
3716 /// here: Cargo's `[dependencies]` + `[dev-dependencies]` accept the
3717 /// same name in both tables (the dev table's pin overrides the
3718 /// runtime table's pin in test/dev contexts), and caixa's surface
3719 /// mirrors that convention until a deliberate choice retires the
3720 /// override pattern. Only within-list duplicates are structurally
3721 /// incoherent — those are what this gate closes.
3722 pub fn validate_deps(&self) -> Result<(), DepError> {
3723 for &list in crate::dep::DepList::ALL {
3724 let mut seen = std::collections::HashSet::new();
3725 for dep in self.deps_of(list) {
3726 dep.validate()?;
3727 crate::render::insert_first_seen(&mut seen, dep.nome(), || {
3728 DepError::DuplicateNome {
3729 nome: dep.nome().to_string(),
3730 list: list.as_str(),
3731 }
3732 })?;
3733 }
3734 }
3735 Ok(())
3736 }
3737
3738 /// Reject `:nome` values the K8s apiserver would refuse at admission
3739 /// time. The top-level Caixa identity flows directly into every
3740 /// substrate-side artifact's `metadata.name` axis: the
3741 /// `lareira-<nome>` Helm chart name ([`caixa-helm::lib::chart_name`]),
3742 /// the programs.yaml `name:` entry the `lareira-fleet-programs`
3743 /// aggregator keys ComputeUnit derivation off
3744 /// ([`caixa-flux::lib::programs_yaml_entry`]), the
3745 /// `LABEL_APLICACAO` label value carried on every Aplicacao-owned
3746 /// pod and the per-`:contratos` CiliumNetworkPolicy `metadata.name`
3747 /// (`<aplicacao>-<de>-to-<para>`) and the per-`:entrada`
3748 /// `<aplicacao>-<para>` HTTPRoute `metadata.name`
3749 /// ([`caixa-mesh::lib::cilium_network_policies`],
3750 /// [`caixa-mesh::lib::gateway_routes`]), and the default
3751 /// `lib/<nome>.lisp` / `exe/<nome>` layout paths
3752 /// ([`crate::StandardLayout::verify`]). Each K8s apiserver-side
3753 /// schema enforces the DNS-1123 label rule on admission; a
3754 /// structurally invalid `:nome` (`"MyApp"` — the canonical
3755 /// "I copied the display name verbatim" footgun, `"my_app"` — the
3756 /// Python-/Postgres-leak, `"team.app"` — `:nome` is a single label
3757 /// not a subdomain, `"-app"` / `"app-"` — DNS-1123 boundary
3758 /// violations, `"my app"` — the paste-from-doc footgun, `"café"` —
3759 /// IDN must be pre-encoded as Punycode, the 64-byte UUID-shaped
3760 /// over-cap slug) silently passed [`Caixa::from_lisp`] and the
3761 /// failure surfaced at `kubectl apply` time as a `metadata.name:
3762 /// Invalid value` rejection on whichever derived artifact admitted
3763 /// first, far from the source `caixa.lisp` and without any field
3764 /// naming the offending `:nome`.
3765 ///
3766 /// Thin wrapper around [`crate::render::is_dns_1123_label`] (the
3767 /// substrate-side predicate the per-axis name gates already share:
3768 /// `:membros :caixa` 3f9d7a0, `:placement :clusters` 6cbb900,
3769 /// `:children :caixa` 31bfa43) that maps the shared parser-shaped
3770 /// reason into the [`ManifestError::NomeInvalid`] variant, so the
3771 /// diagnostic is self-locating (the offending `:nome` is named
3772 /// verbatim) and the author can grep their `caixa.lisp` for
3773 /// `:nome "<value>"` and fix it in one edit. Same diagnostic shape
3774 /// every per-axis sibling gate already exposes
3775 /// ([`crate::AplicacaoError::MembroCaixaInvalid`],
3776 /// [`crate::AplicacaoError::PlacementClusterInvalid`],
3777 /// [`crate::SupervisorError::ChildCaixaInvalid`]).
3778 ///
3779 /// Empty `:nome` (which [`Caixa::from_lisp`] does not reject — the
3780 /// derive macro stores the raw String) is gated by the narrower
3781 /// [`ManifestError::NomeEmpty`] arm before the predicate is
3782 /// consulted, mirroring the empty-first cascade every per-axis
3783 /// name gate already uses (e.g. `MembroCaixaEmpty` before
3784 /// `MembroCaixaInvalid`, `EmptyChildName` before `ChildCaixaInvalid`).
3785 pub fn validate_nome(&self) -> Result<(), ManifestError> {
3786 // Routes through the shared
3787 // [`crate::render::require_valid_dns_1123_label`] gate the peer
3788 // name axes each land on so drift between the eight axes'
3789 // accepted DNS-1123-label sets is structurally impossible.
3790 let nome = self.nome();
3791 crate::render::require_valid_dns_1123_label(
3792 nome,
3793 || ManifestError::NomeEmpty,
3794 |reason| ManifestError::NomeInvalid {
3795 nome: nome.to_string(),
3796 reason,
3797 },
3798 )
3799 }
3800
3801 /// Reject `:nome` values whose joint length with the canonical
3802 /// [`crate::LAREIRA_CHART_NAME_PREFIX`] (`"lareira-"`) overflows
3803 /// the K8s DNS-1123 label cap [`crate::DNS_1123_LABEL_MAX_LEN`]
3804 /// (63 bytes). Every per-Servico / per-Aplicacao renderer the
3805 /// substrate carries materializes the caixa's `:nome` through the
3806 /// canonical [`crate::lareira_chart_name`] helper (f7320d7) into a
3807 /// `lareira-<nome>` artifact that lands as a K8s `metadata.name` /
3808 /// Helm chart name / `HelmRelease` `release_name`: `caixa-helm`'s
3809 /// `ChartDir.name` + `Chart.yaml::name`
3810 /// (caixa-helm/src/lib.rs:207), `caixa-flux`'s `cluster_bundle`
3811 /// `HelmRelease` `chart:` slot (caixa-flux/src/lib.rs:329),
3812 /// `caixa-tatara`'s `process_for_aplicacao` `release_name` +
3813 /// `oci://<registry>/lareira-<nome>` chart ref
3814 /// (caixa-tatara/src/lib.rs:124,178). Helm's own `Chart.yaml::name`
3815 /// admission rule strict-parses against DNS-1123-label, the Helm
3816 /// operator's tracking-secret name is derived from `release_name`
3817 /// and is itself DNS-1123-label-bounded, and the rendered chart's
3818 /// K8s object `metadata.name` axes embed the chart name as a
3819 /// prefix — every one fails admission on a > 63-byte chart name.
3820 ///
3821 /// The per-axis [`Self::validate_nome`] gate (6c992f8) already
3822 /// caps `:nome` itself at 63 bytes via [`is_dns_1123_label`], so a
3823 /// `:nome` of 56–63 bytes silently passed validate (the inner
3824 /// DNS-1123 check accepts the bare `:nome`) but produced a
3825 /// `lareira-<nome>` of 64–71 bytes that the apiserver / `helm lint`
3826 /// rejected at admission — far from the source `caixa.lisp`, with
3827 /// no field naming the overflow root cause. The
3828 /// [`lareira_chart_name`] helper's own doc comment
3829 /// (caixa-core/src/render.rs:3198) explicitly deferred the fix:
3830 /// "the M4 admission webhook will pin the joint-length invariant
3831 /// when it lands". This gate lands the invariant at the
3832 /// manifest-validate layer rather than waiting for the apiserver
3833 /// — the same fail-at-the-source posture every peer per-axis
3834 /// value-shape gate (DNS-1123 on `:nome`, SemVer-2 on `:versao`,
3835 /// SPDX-expression-shape on `:licenca`, 4-digit decimal year on
3836 /// `:edicao`, etc.) takes.
3837 ///
3838 /// Thin wrapper around
3839 /// [`crate::render::is_lareira_chart_name_shape`] (the
3840 /// substrate-side predicate that composes [`lareira_chart_name`] +
3841 /// [`is_dns_1123_label`] via the lifted
3842 /// [`crate::LAREIRA_CHART_NAME_NOME_MAX_LEN`] budget); maps the
3843 /// shared parser-shaped reason into the
3844 /// [`ManifestError::NomeChartNameBudgetExceeded`] variant so the
3845 /// diagnostic is self-locating (the offending `:nome` is named
3846 /// verbatim alongside the rendered chart name and the budget) and
3847 /// the author can shorten in one edit. The gate runs across every
3848 /// `:kind` — `:nome` is the substrate-wide identity axis any
3849 /// future renderer the substrate adds can derive a
3850 /// `lareira-<nome>` artifact from, and uniform enforcement closes
3851 /// the drift footgun where a future kind grows a chart-emitting
3852 /// render path while the validate cascade doesn't catch it.
3853 ///
3854 /// Runs *after* [`Self::validate_nome`] so the narrower
3855 /// `NomeEmpty` / `NomeInvalid` shape diagnostics fire first — a
3856 /// structurally-malformed `:nome` (empty, uppercase, underscore,
3857 /// dot, leading/trailing hyphen, Unicode, > 63 bytes) surfaces its
3858 /// specific shape error rather than the chart-name-budget error,
3859 /// preserving the legitimate "well-shaped `:nome` that happens to
3860 /// overflow the joint cap" arm for this gate.
3861 pub fn validate_nome_chart_name_budget(&self) -> Result<(), ManifestError> {
3862 let nome = self.nome();
3863 crate::render::is_lareira_chart_name_shape(nome).map_err(|reason| {
3864 ManifestError::NomeChartNameBudgetExceeded {
3865 nome: nome.to_string(),
3866 reason,
3867 }
3868 })
3869 }
3870
3871 /// Reject `:versao` values that don't parse as [`semver::Version`].
3872 /// The top-level Caixa version flows directly into every
3873 /// substrate-side artifact that carries a "this is which version of
3874 /// the caixa" axis: the `lareira-<nome>` Helm chart's `Chart.yaml`
3875 /// `version:` + `appVersion:` axes ([`caixa-helm::lib`] —
3876 /// SemVer-2-strict at `helm template` / `helm install` time per
3877 /// https://helm.sh/docs/topics/charts/#charts-and-versioning), the
3878 /// `feira publish` Zig-style `v<versao>` git tag
3879 /// ([`caixa-flux::lib::programs_yaml_entry`] / the
3880 /// `caixa-publish.yml` reusable workflow), the programs.yaml entry's
3881 /// `versao:` value the `lareira-fleet-programs` aggregator carries
3882 /// onto each rendered ComputeUnit, the OCI image's `:v<versao>` /
3883 /// `:latest` tags the substrate's `wasi-service-flake` builds with
3884 /// `skopeo push`, the lacre closure's pinned versions
3885 /// ([`caixa-resolver`] keys `concrete_versao`), and the
3886 /// `:upgrade-from :from` references peers in this exact `versao`
3887 /// shape (`semver::Version`, not `VersionReq`). Each consumer
3888 /// expects a strict three-part `MAJOR.MINOR.PATCH` (optionally
3889 /// `-prerelease` and/or `+build`); a structurally invalid `:versao`
3890 /// (`"0.1"` — missing patch, the canonical "I shortened it" footgun;
3891 /// `"v0.1.0"` — the git-tag-shape-leaking-into-versao typo;
3892 /// `"latest"` / `"main"` — the "I confused it with a docker tag"
3893 /// footgun; `"^0.1"` / `"~0.1.2"` — the requirement-shape leaking
3894 /// into the version field a peer `:deps :versao` accepts;
3895 /// `"0.1.0.0"` — the four-part Java/Microsoft convention DNS
3896 /// SemVer-2 forbids) silently passed [`Caixa::from_lisp`] (the
3897 /// derive macro stores the raw String) and the failure surfaced at
3898 /// the *first* downstream consumer that strict-parses it: at
3899 /// `helm install` time as a chart-version rejection, at
3900 /// `feira publish` time as a malformed git tag, at lacre-resolve
3901 /// time as a `semver::Error` not naming the offending caixa, at
3902 /// `feira upgrade --to <versao>` time as an unresolvable
3903 /// `:upgrade-from :from` match — far from the source `caixa.lisp`
3904 /// and without any field naming the offending `:versao`.
3905 ///
3906 /// Thin wrapper around [`semver::Version::parse`] — the same parser
3907 /// [`crate::CaixaVersion::parse`] (the typed `:versao` accessor)
3908 /// and [`crate::UpgradeFromEntry::validate`] (the peer
3909 /// `:upgrade-from :from` axis, 26da2c7) consume. Maps the
3910 /// `semver::Error` reason into the [`ManifestError::VersaoInvalid`]
3911 /// variant, carrying the offending `:versao` verbatim + a
3912 /// parser-shaped reason naming the specific violation, so the
3913 /// diagnostic is self-locating (the author can grep their
3914 /// `caixa.lisp` for `:versao "<value>"` and fix it in one edit).
3915 /// Same diagnostic shape as [`ManifestError::NomeInvalid`]
3916 /// (6c992f8) and [`crate::UpgradeError::FromInvalid`]
3917 /// (b0c8389) on the peer axes. With this gate, the typed `:versao`
3918 /// surfaces — top-level `:versao`, `:upgrade-from :from` — are
3919 /// now structurally equivalent (every value past validate is
3920 /// round-trippable through [`semver::Version::parse`] without
3921 /// re-checking at the renderer, resolver, or operator hot-upgrade
3922 /// layer), peer with the four `:versao` requirement axes (`:deps`,
3923 /// `:deps-dev`, `:membros`, `:children`) the prior commits
3924 /// (2420c44, 9888b13, b38ff3a) wired through `parse_requirement`.
3925 ///
3926 /// Empty `:versao` (which [`Caixa::from_lisp`] does not reject —
3927 /// the derive macro stores the raw String) is gated by the
3928 /// narrower [`ManifestError::VersaoEmpty`] arm before the parser is
3929 /// consulted, mirroring the empty-first cascade every per-axis
3930 /// version gate already uses (e.g. `MembroVersaoEmpty` before
3931 /// `MembroVersaoInvalid`, `EmptyChildVersion` before
3932 /// `ChildVersaoInvalid`, `NomeEmpty` before `NomeInvalid`).
3933 pub fn validate_versao(&self) -> Result<(), ManifestError> {
3934 let versao = self.versao();
3935 if versao.is_empty() {
3936 return Err(ManifestError::VersaoEmpty);
3937 }
3938 semver::Version::parse(versao).map_err(|e| ManifestError::VersaoInvalid {
3939 versao: versao.to_string(),
3940 reason: e.to_string(),
3941 })?;
3942 Ok(())
3943 }
3944
3945 /// Reject `:restart-window` values the shared
3946 /// [`crate::supervisor::duration_codec::parse`] refuses. The flat
3947 /// `restart_window: Option<String>` slot on [`Caixa`] is stored
3948 /// raw by the derive macro (the typed [`SupervisorSpec`] holds an
3949 /// `Option<Duration>` routed through the shared codec via `with =
3950 /// "duration_codec"`); the inline `Caixa → SupervisorSpec`
3951 /// view-construction path ([`Self::supervisor_view`]) folds the
3952 /// raw string through the same shared codec and soft-swallows the
3953 /// parse error as `None` to keep the view best-effort. Without
3954 /// this gate a malformed `:restart-window` (`"1.5s"` — the
3955 /// fractional-seconds drift class; `"1.0s"` — the decimal-shaped
3956 /// integer drift; `"0.5m"` — the unit-fraction drift; `"+30s"` /
3957 /// `"-30s"` — the leading-sign drift; `"30x"` — the unknown-unit
3958 /// footgun; `"abc"` — pure garbage; `""` — the empty-after-trim
3959 /// edge case) silently produced a `SupervisorSpec` with
3960 /// `restart_window: None`, indistinguishable from the canonical
3961 /// "omit the slot to express no reset" authoring shape — Erlang/OTP's
3962 /// `MaxIntensity / Period` invariant turns into a never-reset
3963 /// supervisor far from the source `caixa.lisp`, with no field
3964 /// naming the offending `:restart-window`. Lifting the gate to a
3965 /// Caixa-level validator mirrors the trajectory of the peer
3966 /// per-axis identity gates ([`Self::validate_nome`] 6c992f8,
3967 /// [`Self::validate_versao`] 1fdaa02, [`Self::validate_deps`]
3968 /// a7f0d8c) and the ABSORPTION-ROADMAP.md M2.2 test pin
3969 /// (line 196: "reject invalid `:restart-window` (non-duration)").
3970 ///
3971 /// Thin wrapper around [`crate::supervisor::duration_codec::parse`]
3972 /// (the shared codec backing `:supervisor :restart-window` as
3973 /// serde-routed on [`SupervisorSpec`], `:politicas :timeout`, and
3974 /// `:politicas :circuit-breaker :window` — all three covered by
3975 /// the integer-magnitude gate 1c55a2a). Maps the codec's parse
3976 /// error verbatim into the [`ManifestError::RestartWindowMalformed`]
3977 /// variant, carrying the offending raw string + a parser-shaped
3978 /// reason naming the canonical authoring form, so the diagnostic
3979 /// is self-locating (the author can grep their `caixa.lisp` for
3980 /// `:restart-window "<value>"` and fix it in one edit) and
3981 /// uniform with every other manifest-level validate diagnostic.
3982 /// With this gate the four `:restart-window`-shaped surfaces (the
3983 /// flat raw string on [`Caixa`], the typed `Option<Duration>` on
3984 /// [`SupervisorSpec`], the two `MeshPolicy` peer durations) are
3985 /// now structurally equivalent — every value past the codec is in
3986 /// one accepted set, by construction.
3987 ///
3988 /// `None` (the canonical "omit the slot to express no reset"
3989 /// shape) is accepted trivially — the gate is a no-op when the
3990 /// author didn't author a window. The empty string is rejected by
3991 /// the shared codec (its digit-only gate refuses an empty
3992 /// magnitude), surfacing the same `RestartWindowMalformed`
3993 /// diagnostic as every other rejected non-canonical shape.
3994 pub fn validate_restart_window(&self) -> Result<(), ManifestError> {
3995 let Some(s) = self.restart_window() else {
3996 return Ok(());
3997 };
3998 crate::supervisor::duration_codec::parse(s)
3999 .map(|_| ())
4000 .map_err(|reason| ManifestError::RestartWindowMalformed {
4001 restart_window: s.to_string(),
4002 reason,
4003 })
4004 }
4005
4006 /// Reject per-entry values on the three Caixa-level code-surface
4007 /// path lists (`:bibliotecas`, `:exe`, `:servicos`) that the
4008 /// layout checker's `root.join(p)` sandbox would silently subvert.
4009 /// Same three structural footguns the peer
4010 /// [`BehaviorSpec::validate`] (b0c8389) and
4011 /// [`crate::UpgradeInstruction::validate`] `StateChange` arm
4012 /// (26da2c7) already close on the M2 `:behavior :on-*` and
4013 /// `:upgrade-from :state-change :script` axes, here lifted onto
4014 /// the three top-level code-path axes through the shared
4015 /// [`is_sandboxed_relative_path`] predicate:
4016 ///
4017 /// - empty entry (`(:bibliotecas (""))` / `(:exe (""))` /
4018 /// `(:servicos (""))`): `PathBuf::new()` round-trips through
4019 /// [`Path::join`] as the base itself — `root.join("")` ==
4020 /// `root`, so the existence check (`self.exists(&root)`)
4021 /// trivially passes (the project root exists), and the layout
4022 /// silently treats the project root as a biblioteca / exe /
4023 /// servico entry. The `:bibliotecas` loop then hands the root
4024 /// to `tatara_lisp::read` at `feira build` time as if the root
4025 /// directory itself were a Lisp source file — a parse error
4026 /// far from the source `caixa.lisp` with no field naming the
4027 /// offending entry.
4028 /// - absolute path (`(:bibliotecas ("/etc/passwd"))`):
4029 /// [`Path::join`] *replaces* the base when the right-hand side
4030 /// is absolute, so `root.join("/etc/passwd")` resolves to
4031 /// `"/etc/passwd"` and escapes the project sandbox entirely.
4032 /// The existence check then silently consults whatever the
4033 /// escaped path resolves to — for `:bibliotecas`, the layout
4034 /// has no `starts_with`-fence (only `:exe` is fenced under
4035 /// `exe/` and `:servicos` under `servicos/`), so an absolute
4036 /// `:bibliotecas` entry that happens to resolve on disk
4037 /// silently passes. For `:exe` / `:servicos` the fence catches
4038 /// the absolute case downstream as `ExeOutsideDir` /
4039 /// `ServicoOutsideDir` (or `MissingEntry` if the absolute path
4040 /// doesn't exist), but with a downstream-shaped diagnostic
4041 /// that names the resolved escape path rather than the
4042 /// authoring footgun at the source.
4043 /// - parent-escape (`(:bibliotecas ("../sibling/x.lisp"))` /
4044 /// `(:exe ("exe/../../escape.lisp"))`): a [`PathBuf`] with any
4045 /// [`std::path::Component::ParentDir`] anywhere round-trips
4046 /// through [`Path::join`] as a traversal above the caixa root.
4047 /// The `:exe` / `:servicos` `starts_with(<dir>)` fence is
4048 /// *component-aware* (not canonical-path-aware), so
4049 /// `root.join("exe/../../escape.lisp")` `starts_with(exe_dir)`
4050 /// is **true** even though the canonical resolution
4051 /// `{parent of root}/escape.lisp` lives outside the caixa root
4052 /// — the fence silently lets the parent-escape through, and
4053 /// the existence check passes if that escape-target happens
4054 /// to exist. Caught regardless of where the `..` sits
4055 /// (leading, mid-path, trailing) so the gate matches the peer
4056 /// predicate's full coverage.
4057 ///
4058 /// Same `Empty` → `Absolute` → `ParentEscape` arm-ordering every peer
4059 /// `is_sandboxed_relative_path` consumer follows (b0c8389 / 26da2c7);
4060 /// same per-slot diagnostic shape every peer per-axis path-gate
4061 /// exposes (`*Empty { slot }` / `*Absolute { slot, path }` /
4062 /// `*ParentEscape { slot, path }`). Cross-slot precedence is
4063 /// `:bibliotecas` → `:exe` → `:servicos` — the same declaration
4064 /// order [`Caixa::declared_foreign_code_slots`] uses for its
4065 /// canonical foreign-code-slot diagnostic, so a manifest with
4066 /// multiple malformed slots surfaces the lexicographically-earliest
4067 /// slot's diagnostic deterministically.
4068 ///
4069 /// Lifted to the typed surface as a Caixa-level validator (peer
4070 /// of [`Self::validate_nome`] / [`Self::validate_versao`] /
4071 /// [`Self::validate_deps`] / [`Self::validate_restart_window`])
4072 /// and wired into [`crate::StandardLayout::verify`] before the
4073 /// existence-check loops so the diagnostic names the offending
4074 /// slot at the source caixa.lisp rather than reporting a
4075 /// downstream `MissingEntry` / `ExeOutsideDir` /
4076 /// `ServicoOutsideDir` against the resolved sandbox-escape path.
4077 /// The fourth typed code-path surface — every author-supplied
4078 /// path on the manifest — is now structurally accept-shaped
4079 /// past validate, peer with `:behavior :on-*` and
4080 /// `:upgrade-from :state-change :script`.
4081 pub fn validate_code_paths(&self) -> Result<(), ManifestError> {
4082 /// Per-slot file-type contract for the three Caixa-level
4083 /// code-path surfaces (`:bibliotecas`, `:exe`, `:servicos`).
4084 /// Each variant names the predicate the per-entry file-type
4085 /// gate consults; [`Self::None`] opts the slot out of any
4086 /// file-type contract. Lifted as a typed local enum so the
4087 /// per-slot dispatch is exhaustive at the `match` — adding a
4088 /// future axis to the typed-substrate `:` slot set (the
4089 /// future `:assets` resource axis the M5 roadmap names, the
4090 /// future `:nix-flake` derivation axis the caixa-flake
4091 /// emitter consults) lands as one variant + one `match` arm,
4092 /// not a coordinated rewrite of every per-slot bool flag.
4093 ///
4094 /// Peer of the typed-substrate per-slot variant disciplines
4095 /// already established on this surface
4096 /// ([`crate::supervisor::RestartStrategy`] +
4097 /// [`crate::supervisor::RestartPolicy`] on the OTP-shape
4098 /// supervision-tree axis,
4099 /// [`crate::aplicacao::PlacementStrategy`] on the §III.1
4100 /// placement axis, [`crate::aplicacao::WitTarget`] on the
4101 /// `:contratos` payload-target axis): the typed `enum` is
4102 /// the substrate's single source of truth for the per-axis
4103 /// dispatch, and every consumer (the per-arm body here, the
4104 /// future feira-lint per-slot diagnostic renderer, the M4
4105 /// per-axis admission webhook) reaches for the same typed
4106 /// surface rather than re-deriving the partition from inline
4107 /// flag combinations.
4108 enum CodePathFileType {
4109 /// `:exe` — nix-build derivation output, no terminating-
4110 /// extension contract (the canonical `"exe/<name>"`
4111 /// fixtures the layout's `ExeOutsideDir` error message
4112 /// documents carry no extension by convention).
4113 None,
4114 /// `:bibliotecas` — tatara-lisp source files the
4115 /// `feira build` loop reads through `tatara_lisp::read`
4116 /// at parse time. Routes to [`is_lisp_extension`].
4117 LispSource,
4118 /// `:servicos` — ComputeUnit-CR YAML files the
4119 /// caixa-helm / caixa-flux renderers consume through
4120 /// `serde_yaml::from_str`. Routes to
4121 /// [`is_computeunit_yaml_extension`].
4122 ComputeUnitYaml,
4123 }
4124
4125 // The per-slot [`CodePathFileType`] selects which axes carry the
4126 // lifted file-type predicate. `:bibliotecas` is the tatara-lisp
4127 // source axis (the `feira build` loop at
4128 // `caixa-feira/src/cmd/build.rs:33` reads each entry through
4129 // `tatara_lisp::read` at parse time) — the lifted
4130 // [`is_lisp_extension`] predicate gates the `.lisp` extension.
4131 // `:exe` is the nix-built executable surface (per the canonical
4132 // `"exe/<name>"`-shaped fixtures the layout's `ExeOutsideDir`
4133 // error message documents and every in-tree
4134 // `caixa_with_code_paths` positive control uses) — its file-type
4135 // contract is "nix-build derivation output", not a typed source
4136 // file, so [`CodePathFileType::None`] opts the slot out of any
4137 // file-type gate. `:servicos` is the `.computeunit.yaml`
4138 // ComputeUnit-CR axis (the peer caixa-helm / caixa-flux
4139 // renderers consume each entry through `serde_yaml::from_str` as
4140 // a typed `ComputeUnit` CR) — the lifted
4141 // [`is_computeunit_yaml_extension`] predicate gates the compound
4142 // `.computeunit.yaml` suffix. All three axes are surfaced through
4143 // the same iteration so the sandbox-shape + duplicate gates
4144 // apply uniformly; the typed file-type dispatch fires per-slot
4145 // exactly where the downstream consumer's accepted set demands
4146 // it. The third file-type variant ([`ComputeUnitYaml`]) is the
4147 // compounding lift on the peer 64772a9 `:bibliotecas`
4148 // `.lisp`-gate trajectory — the second of the three code-path
4149 // axes to land on a typed compound-suffix gate, with the same
4150 // self-locating per-slot diagnostic shape every peer per-axis
4151 // file-type lift uses (`*NonLispExtension { slot, path }` /
4152 // `*NonComputeUnitYamlExtension { slot, path }`).
4153 for (slot, list, file_type) in [
4154 (
4155 ":bibliotecas",
4156 &self.bibliotecas,
4157 CodePathFileType::LispSource,
4158 ),
4159 (":exe", &self.exe, CodePathFileType::None),
4160 (
4161 ":servicos",
4162 &self.servicos,
4163 CodePathFileType::ComputeUnitYaml,
4164 ),
4165 ] {
4166 // Per-slot set-not-multiset gate on the typed code-path axis.
4167 // Every peer Vec-shaped author-supplied list past validate is
4168 // a set, not a multiset: `:membros :caixa`
4169 // ([`crate::AplicacaoError::MembroDuplicate`]), `:placement
4170 // :clusters` ([`crate::AplicacaoError::PlacementClusterDuplicate`]),
4171 // `:entrada :paths` ([`crate::AplicacaoError::EntradaPathDuplicate`]),
4172 // `:contratos` ([`crate::AplicacaoError::ContratoDuplicate`]),
4173 // `:children :caixa` ([`crate::SupervisorError::DuplicateChild`]),
4174 // `:deps` / `:deps-dev` `:nome` ([`crate::DepError::DuplicateNome`]
4175 // per 359fba5), `:upgrade-from :from` ([`crate::UpgradeError::DuplicateFrom`]),
4176 // `:etiquetas` ([`ManifestError::EtiquetaDuplicate`] per 360a499),
4177 // `:autores` ([`ManifestError::AutorDuplicate`] per 86c769b) —
4178 // the three code-path lists are the last Vec-shaped author-
4179 // supplied slots on the typed Caixa surface still admitting a
4180 // duplicate entry silently. Scope is per-list (`:bibliotecas`
4181 // duplicates are flagged within `:bibliotecas`, not across
4182 // `:bibliotecas` ↔ `:exe`) — the same per-list scope `:deps`
4183 // ↔ `:deps-dev` use (a `:nome` present in both lists is a
4184 // legitimate dev-vs-runtime shape on the dep axis, fenced
4185 // separately by [`crate::dep::validate_no_self_dep`]). On the
4186 // code-path axis a cross-slot collision is structurally
4187 // impossible by the layout's `starts_with(<exe|servicos>_dir)`
4188 // fence — `:exe` and `:servicos` entries are confined to their
4189 // own directory trees, so the only way a string could appear
4190 // on two code-path lists is the (rare, structurally invalid)
4191 // case where `:bibliotecas` carries an `"exe/<x>"` or
4192 // `"servicos/<x>.yaml"`-shaped path.
4193 //
4194 // Without the gate three authoring footguns silently passed:
4195 //
4196 // - `:bibliotecas ("lib/foo.lisp" "lib/foo.lisp")` — the
4197 // canonical copy-paste-the-wrong-file footgun. `feira
4198 // build` (`caixa-feira/src/cmd/build.rs:33`) walks the
4199 // list and re-parses the same file twice, wasting work
4200 // and silently masking the author's intent to declare a
4201 // *second* biblioteca.
4202 // - `:exe ("exe/cli" "exe/cli")` — the same footgun on the
4203 // Binario surface. The future `caixa-flake` `nix flake`
4204 // emitter that materializes each `:exe` entry as a flake
4205 // `packages.<exe-name>` derivation would collide on the
4206 // duplicate package name and surface a flake-eval error
4207 // far from the source `caixa.lisp`.
4208 // - `:servicos ("servicos/x.computeunit.yaml"
4209 // "servicos/x.computeunit.yaml")` — the same footgun on
4210 // the Servico surface. The peer `caixa-helm` / `caixa-flux`
4211 // renderers already refuse `:servicos.len() != 1` with
4212 // the narrower [`UnsupportedServicoCount`] diagnostic, but
4213 // that diagnostic surfaces "too many servicos" without
4214 // naming "duplicate entry" — the typed self-locating
4215 // "which entry is the duplicate" framing only lands at
4216 // this gate.
4217 //
4218 // Same `seen.insert(entry.as_str())` shape every peer per-list
4219 // duplicate gate uses (`:etiquetas` 360a499, `:autores`
4220 // 86c769b, `:deps` 359fba5) and the same "structural shape
4221 // checks fire before the duplicate check on the same entry"
4222 // ordering (a `(:bibliotecas ("" "lib/x.lisp" "lib/x.lisp"))`
4223 // shape surfaces the narrower [`Self::CodePathEmpty`] for the
4224 // empty entry first, not the duplicate on the later pair).
4225 let mut seen = std::collections::HashSet::new();
4226 for entry in list {
4227 let path = Path::new(entry);
4228 match is_sandboxed_relative_path(path) {
4229 Ok(()) => {}
4230 Err(PathShapeViolation::Empty) => {
4231 return Err(ManifestError::CodePathEmpty { slot });
4232 }
4233 Err(PathShapeViolation::Absolute) => {
4234 return Err(ManifestError::CodePathAbsolute {
4235 slot,
4236 path: path.to_path_buf(),
4237 });
4238 }
4239 Err(PathShapeViolation::ParentEscape) => {
4240 return Err(ManifestError::CodePathParentEscape {
4241 slot,
4242 path: path.to_path_buf(),
4243 });
4244 }
4245 }
4246 // The per-slot file-type gate dispatched through the
4247 // typed [`CodePathFileType`] selector above. Each variant
4248 // routes to the lifted predicate the downstream consumer
4249 // demands:
4250 //
4251 // - [`LispSource`] → [`is_lisp_extension`] for
4252 // `:bibliotecas` (the `feira build` loop's
4253 // `tatara_lisp::read` consumer);
4254 // - [`ComputeUnitYaml`] → [`is_computeunit_yaml_extension`]
4255 // for `:servicos` (the caixa-helm / caixa-flux
4256 // `serde_yaml::from_str` consumer's `ComputeUnit` CR
4257 // accepted set);
4258 // - [`None`] for `:exe` — the nix-build derivation-
4259 // output axis has no terminating-extension contract.
4260 //
4261 // Fires after the sandbox-shape arms so a path that is
4262 // *both* sandbox-escaping and wrong-extension surfaces
4263 // the more fundamental sandbox-shape diagnostic first
4264 // (mirrors the peer `EmptyPath` → `AbsolutePath` →
4265 // `ParentEscape` → `NonLispExtension` arm-ordering on
4266 // `:behavior :on-*` c97815a, and `EmptyScript` →
4267 // `AbsoluteScript` → `ParentEscapeScript` →
4268 // `NonLispExtensionScript` on
4269 // `:upgrade-from :state-change :script` 33cc830), and
4270 // before the duplicate gate so the narrower per-entry
4271 // file-type shape dominates the cross-entry uniqueness
4272 // diagnostic (a
4273 // `("servicos/x.yaml" "servicos/x.yaml")` shape on
4274 // `:servicos` surfaces
4275 // `CodePathNonComputeUnitYamlExtension` on the first
4276 // entry rather than `CodePathDuplicate` on the pair —
4277 // peer with the 64772a9 `:bibliotecas`
4278 // `("lib/x.txt" "lib/x.txt")` ordering).
4279 match file_type {
4280 CodePathFileType::None => {}
4281 CodePathFileType::LispSource => {
4282 if !is_lisp_extension(path) {
4283 return Err(ManifestError::CodePathNonLispExtension {
4284 slot,
4285 path: path.to_path_buf(),
4286 });
4287 }
4288 }
4289 CodePathFileType::ComputeUnitYaml => {
4290 if !is_computeunit_yaml_extension(path) {
4291 return Err(ManifestError::CodePathNonComputeUnitYamlExtension {
4292 slot,
4293 path: path.to_path_buf(),
4294 });
4295 }
4296 }
4297 }
4298 crate::render::insert_first_seen(&mut seen, entry.as_str(), || {
4299 ManifestError::CodePathDuplicate {
4300 slot,
4301 path: path.to_path_buf(),
4302 }
4303 })?;
4304 }
4305 }
4306 Ok(())
4307 }
4308
4309 /// Reject `:etiquetas` lists with an empty entry or with two entries
4310 /// agreeing on the same string. `:etiquetas` is the universal
4311 /// registry-search-tag axis on [`Caixa`] (every kind carries the
4312 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4313 /// `Chart.yaml` `keywords:` array on every Servico (caixa-helm's
4314 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:236` folds it through
4315 /// a [`std::collections::BTreeSet`] alongside the four substrate-
4316 /// fixed tags `lareira` / `wasm` / `tatara-lisp` / `caixa-servico`).
4317 /// Two authoring footguns silently passed validate without this gate:
4318 ///
4319 /// - Empty entry (`(:etiquetas (""))` — the canonical paste-from-
4320 /// blank-doc footgun) rendered as `keywords: ["", "caixa-servico",
4321 /// "lareira", "tatara-lisp", "wasm"]` in `Chart.yaml`. Helm's
4322 /// `chart.metadata.keywords` admits the value without a strict
4323 /// parser-side gate, but the empty keyword has no operational
4324 /// meaning — it indexes nothing in the future caixa-registry
4325 /// search axis and clutters the rendered chart with a no-op tag.
4326 /// - Duplicate entries (`(:etiquetas ("demo" "demo"))` — the
4327 /// copy-paste-the-wrong-tag footgun) silently passed validate
4328 /// and were silently dedup'd by caixa-helm's `BTreeSet` collect
4329 /// at chart render — a "second wins / one silently disappears"
4330 /// shape divergent from every peer typed-graph set gate
4331 /// ([`crate::AplicacaoError::MembroDuplicate`] on `:membros`,
4332 /// [`crate::AplicacaoError::PlacementClusterDuplicate`] on
4333 /// `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4334 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4335 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4336 /// `:deps` / `:deps-dev` per 359fba5, [`crate::UpgradeError::DuplicateFrom`]
4337 /// on `:upgrade-from`, the per-instruction-class singularity
4338 /// gates [`crate::UpgradeError::DuplicateLoadModule`] /
4339 /// [`crate::UpgradeError::DuplicateStateChange`] /
4340 /// [`crate::UpgradeError::DuplicateCleanup`]). The typed-graph
4341 /// discipline is uniform: every Vec-shaped author-supplied list
4342 /// past validate is set-not-multiset, by construction.
4343 ///
4344 /// Past the empty arm the gate enforces the chart-keyword shape
4345 /// predicate via [`crate::render::is_chart_keyword_shape`]: Cargo's
4346 /// crates.io `[package] keywords` grammar — 1..=20 bytes, starts
4347 /// with an ASCII letter, ASCII alphanumeric / `_` / `-`
4348 /// continuation. Closes the canonical paste-from-doc footguns the
4349 /// bare empty + duplicate arms left open: paste-from-aligned-doc
4350 /// whitespace (`" mesh"`, `"mesh "`), paste-from-multiline-doc
4351 /// newline (`"mesh\nhttp"` — the author pasted a multi-tag block
4352 /// into one entry instead of splitting), paste-from-Windows-CRLF-doc
4353 /// carriage return, CSV-list-separator confusion (`"mesh,http,grpc"`
4354 /// — the author meant three separate list entries), path-separator
4355 /// confusion (`"caixa/servico"`), namespace-suffix (`"http.1"`),
4356 /// leading-digit (`"1foo"`), kebab-leak (`"-foo"`), snake-leak
4357 /// (`"_foo"`), non-ASCII (`"café"`), and paste-from-binary-blob
4358 /// control bytes that would silently land as malformed search tags
4359 /// in the rendered Chart.yaml `keywords:` array and break the
4360 /// Artifact Hub keyword index lookup far from the source caixa.lisp.
4361 /// Mirrors the [`Self::validate_autores`] shape-predicate cascade
4362 /// established on the sibling universal-axis `Vec<String>` surface
4363 /// — the second universal-axis Vec<String> surface to land the
4364 /// empty-first-then-shape-then-duplicate per-entry cascade.
4365 ///
4366 /// Same empty-first cascade discipline every peer per-axis gate
4367 /// uses: the per-entry empty arm fires before the per-entry shape
4368 /// arm fires before the cross-entry duplicate arm, so an
4369 /// `("" "mesh" "mesh")` authoring shape surfaces the narrower
4370 /// [`ManifestError::EtiquetaEmpty`] (the structural "this entry
4371 /// has no value" defect) before either the shape or the duplicate
4372 /// diagnostic. Walks the list in declaration order so the
4373 /// first-collision diagnostic surfaces the lexicographically-
4374 /// earliest offending position, peer with every other duplicate
4375 /// gate on this surface.
4376 ///
4377 /// Universal-axis (every kind carries `:etiquetas`), so wired at the
4378 /// caixa-build gate alongside the peer universal gates
4379 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4380 /// [`Self::validate_deps`] / [`Self::validate_code_paths`] — before
4381 /// the kind-coherence gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`]
4382 /// / [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4383 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4384 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4385 /// slot sets. The future caixa-registry search axis can reach for
4386 /// `caixa.etiquetas` knowing every entry is a non-empty distinct
4387 /// chart-keyword-shaped string without re-deriving the precondition.
4388 pub fn validate_etiquetas(&self) -> Result<(), ManifestError> {
4389 let mut seen = std::collections::HashSet::new();
4390 for etiqueta in self.etiquetas() {
4391 if etiqueta.is_empty() {
4392 return Err(ManifestError::EtiquetaEmpty);
4393 }
4394 crate::render::is_chart_keyword_shape(etiqueta).map_err(|reason| {
4395 ManifestError::EtiquetaInvalid {
4396 etiqueta: etiqueta.clone(),
4397 reason,
4398 }
4399 })?;
4400 crate::render::insert_first_seen(&mut seen, etiqueta.as_str(), || {
4401 ManifestError::EtiquetaDuplicate {
4402 etiqueta: etiqueta.clone(),
4403 }
4404 })?;
4405 }
4406 Ok(())
4407 }
4408
4409 /// Reject `:autores` lists with an empty entry or with two entries
4410 /// agreeing on the same string. `:autores` is the universal
4411 /// maintainer-axis on [`Caixa`] (every kind carries the
4412 /// `Vec<String>` slot) and lands verbatim as the Helm chart
4413 /// `Chart.yaml` `maintainers:` array on every Servico (caixa-helm's
4414 /// `build_chart_yaml` at `caixa-helm/src/lib.rs:251` maps each entry
4415 /// to a `Maintainer { name, email: None }` without dedup). Two
4416 /// authoring footguns silently passed validate without this gate:
4417 ///
4418 /// - Empty entry (`(:autores (""))` — the canonical paste-from-
4419 /// blank-doc footgun) rendered as
4420 /// `maintainers: [{name: "", email: null}]` in `Chart.yaml`. The
4421 /// empty maintainer name has no operational meaning — it
4422 /// identifies no one in the substrate's authorship index and
4423 /// clutters the rendered chart with a no-op maintainer.
4424 /// - Duplicate entries (`(:autores ("pleme-io" "pleme-io"))` —
4425 /// the copy-paste-the-wrong-author footgun) silently passed
4426 /// validate and rendered as two identical maintainer entries.
4427 /// Unlike the [`Self::validate_etiquetas`] peer (caixa-helm's
4428 /// `BTreeSet`-collect on `:etiquetas` silently dedups the
4429 /// rendered `keywords:` array at chart-render time), the
4430 /// `maintainers:` rendering has *no* dedup — duplicate `:autores`
4431 /// entries stack verbatim in the chart, divergent from every
4432 /// peer typed-graph set gate ([`crate::AplicacaoError::MembroDuplicate`]
4433 /// on `:membros`, [`crate::AplicacaoError::PlacementClusterDuplicate`]
4434 /// on `:placement :clusters`, [`crate::AplicacaoError::EntradaPathDuplicate`]
4435 /// on `:entrada :paths`, [`crate::AplicacaoError::ContratoDuplicate`]
4436 /// on `:contratos`, [`crate::DepError::DuplicateNome`] on
4437 /// `:deps` / `:deps-dev`, [`crate::UpgradeError::DuplicateFrom`]
4438 /// on `:upgrade-from`, [`ManifestError::EtiquetaDuplicate`] on
4439 /// `:etiquetas`).
4440 ///
4441 /// Past the empty arm the gate enforces the chart-maintainer-name
4442 /// shape predicate via [`crate::render::is_chart_maintainer_name_shape`]:
4443 /// the structural single-line printable-UTF-8 floor every realistic
4444 /// Helm chart maintainer name carries — 1..=128 bytes, no leading
4445 /// or trailing whitespace, no ASCII control characters anywhere,
4446 /// Unicode bytes accepted. Closes the canonical paste-from-doc
4447 /// footguns the bare empty + duplicate arms left open:
4448 /// paste-from-aligned-doc whitespace (`" pleme-io"`, `"pleme-io "`),
4449 /// paste-from-multiline-doc newline (`"alice\nbob"` — the author
4450 /// pasted a multi-line block of author records into one `:autores`
4451 /// entry instead of splitting into one entry per author),
4452 /// paste-from-Windows-CRLF-doc carriage return, tab-from-aligned-doc,
4453 /// and the paste-from-binary-blob control bytes that would silently
4454 /// land as YAML-illegal byte sequences in the rendered Chart.yaml
4455 /// `maintainers:` array. Mirrors the shape-predicate cascade
4456 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4457 /// [`Self::validate_edicao`] / [`Self::validate_repositorio`]
4458 /// establish past their own empty arms on the sibling universal-axis
4459 /// `Option<String>` surfaces — the first universal-axis Vec<String>
4460 /// surface to land the empty-first-then-shape-then-duplicate per-entry
4461 /// cascade.
4462 ///
4463 /// Same empty-first cascade discipline every peer per-axis gate
4464 /// uses: the per-entry empty arm fires before the per-entry shape
4465 /// arm before the cross-entry duplicate arm. Walks the list in
4466 /// declaration order so the first-collision diagnostic surfaces the
4467 /// lexicographically-earliest offending position, peer with every
4468 /// other duplicate gate on this surface.
4469 ///
4470 /// Universal-axis (every kind carries `:autores`), so wired at the
4471 /// caixa-build gate alongside the peer universal gates
4472 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4473 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4474 /// [`Self::validate_code_paths`] — before the kind-coherence gates
4475 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4476 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4477 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4478 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-specific
4479 /// slot sets.
4480 pub fn validate_autores(&self) -> Result<(), ManifestError> {
4481 let mut seen = std::collections::HashSet::new();
4482 for autor in self.autores() {
4483 if autor.is_empty() {
4484 return Err(ManifestError::AutorEmpty);
4485 }
4486 crate::render::is_chart_maintainer_name_shape(autor).map_err(|reason| {
4487 ManifestError::AutorInvalid {
4488 autor: autor.clone(),
4489 reason,
4490 }
4491 })?;
4492 crate::render::insert_first_seen(&mut seen, autor.as_str(), || {
4493 ManifestError::AutorDuplicate {
4494 autor: autor.clone(),
4495 }
4496 })?;
4497 }
4498 Ok(())
4499 }
4500
4501 /// Reject `:repositorio` values whose shape the shared
4502 /// [`crate::render::is_git_repo_url`] predicate refuses. The flat
4503 /// `repositorio: Option<String>` slot on [`Caixa`] is the
4504 /// universal git-shaped homepage axis every kind carries — the
4505 /// substrate routes the same string through two load-bearing
4506 /// consumers:
4507 ///
4508 /// - [`caixa-helm`] folds it verbatim into the rendered
4509 /// `lareira-<nome>` Helm chart's `Chart.yaml` `home:` field
4510 /// (`build_chart_yaml` at `caixa-helm/src/lib.rs:268`) and into
4511 /// the chart `README.md` `repo = …` interpolation
4512 /// (`caixa-helm/src/lib.rs:359`).
4513 /// - [`caixa-flux`] folds it verbatim into the standalone
4514 /// `ClusterBundleOpts::for_caixa` `git_url:` field
4515 /// (`caixa-flux/src/lib.rs:293`), which becomes the `FluxCD`
4516 /// `GitRepository.spec.url` the cluster's source-controller
4517 /// polls — the load-bearing deploy-time axis.
4518 ///
4519 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4520 /// substitute a placeholder when the slot is absent (`None` → the
4521 /// fallback fires); a `Some("")` *skips the fallback* and silently
4522 /// passes the empty string through to `Chart.yaml home: ""` /
4523 /// `GitRepository url: ""` — Helm's chart lint and `FluxCD`'s source
4524 /// controller both reject the empty URL far from the source
4525 /// `caixa.lisp`, with no field naming the offending `:repositorio`.
4526 /// Similarly a malformed `:repositorio` (whitespace, control char,
4527 /// missing `:` separator, leading `-`) silently lands in the
4528 /// rendered artifacts and breaks at `git clone` / `helm template`
4529 /// / `flux reconcile` time.
4530 ///
4531 /// Thin wrapper around [`crate::render::is_git_repo_url`] — the
4532 /// same shared predicate the peer [`crate::DepSource::validate`]
4533 /// routes the `:fonte (:tipo git :repo …)` axis through. With this
4534 /// gate the two `git URL`-shaped surfaces on the typed Caixa
4535 /// (`:repositorio` here, `:deps :fonte :repo` peer) are
4536 /// structurally equivalent: every value past validate is
4537 /// guaranteed-acceptable by the predicate's union of constraints
4538 /// (non-empty, length-bounded, no leading `-`, no whitespace, no
4539 /// control chars, ASCII only, no leading `:`, contains a `:`
4540 /// separator). The predicate accepts every documented authoring
4541 /// shape — `github:org/repo` shorthand, `https://host/path`,
4542 /// `ssh://[user@]host/path`, `git://host/path`, `git@host:path`
4543 /// scp-style SSH, `file:///path` — and refuses the canonical
4544 /// paste-from-blank-doc / paste-from-multiline-doc / CLI-arg-
4545 /// injection footguns at validate time. Maps the predicate's
4546 /// `String` reason verbatim into the
4547 /// [`ManifestError::RepositorioInvalid`] variant, carrying the
4548 /// offending value + parser-shaped reason so the diagnostic is
4549 /// self-locating (the author can grep their `caixa.lisp` for
4550 /// `:repositorio "<value>"` and fix it in one edit).
4551 ///
4552 /// `None` (the canonical "omit the slot to express no published
4553 /// homepage" shape) is accepted trivially — the gate is a no-op
4554 /// when the author didn't declare a value. `Some("")` is gated by
4555 /// the narrower [`ManifestError::RepositorioEmpty`] arm before the
4556 /// shape predicate is consulted, mirroring the empty-first cascade
4557 /// every peer per-axis identity gate uses
4558 /// ([`ManifestError::NomeEmpty`] → [`ManifestError::NomeInvalid`],
4559 /// [`ManifestError::VersaoEmpty`] → [`ManifestError::VersaoInvalid`],
4560 /// [`crate::DepError::FonteRepoEmpty`] →
4561 /// [`crate::DepError::FonteRepoInvalid`]).
4562 ///
4563 /// Universal-axis (every kind carries `:repositorio`), so wired at
4564 /// the caixa-build gate alongside the peer universal gates
4565 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4566 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4567 /// [`Self::validate_autores`] / [`Self::validate_code_paths`] —
4568 /// before the kind-coherence gates
4569 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4570 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4571 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4572 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4573 /// specific slot sets.
4574 pub fn validate_repositorio(&self) -> Result<(), ManifestError> {
4575 let Some(s) = self.repositorio() else {
4576 return Ok(());
4577 };
4578 if s.is_empty() {
4579 return Err(ManifestError::RepositorioEmpty);
4580 }
4581 is_git_repo_url(s).map_err(|reason| ManifestError::RepositorioInvalid {
4582 repositorio: s.to_string(),
4583 reason,
4584 })
4585 }
4586
4587 /// Reject `:descricao` values that are the empty string. The flat
4588 /// `descricao: Option<String>` slot on [`Caixa`] is the universal
4589 /// free-form-prose homepage axis every kind carries — the
4590 /// substrate routes the same string through two load-bearing
4591 /// consumers in the [`caixa-helm`] renderer:
4592 ///
4593 /// - `build_chart_yaml` folds it verbatim into the rendered
4594 /// `lareira-<nome>` Helm chart's `Chart.yaml` `description:`
4595 /// field (`caixa-helm/src/lib.rs:232-235`).
4596 /// - `build_readme` folds it verbatim into the rendered chart
4597 /// `README.md` header (`caixa-helm/src/lib.rs:333-336`).
4598 ///
4599 /// Both consumers use `Option::unwrap_or_else(|| <fallback>)` to
4600 /// substitute a `caixa.nome`-derived placeholder when the slot is
4601 /// absent (`None` → the fallback fires); a `Some("")` *skips the
4602 /// fallback* and silently passes the empty string through to
4603 /// `Chart.yaml description: ""` / a blank chart `README.md`
4604 /// header. Helm's chart spec requires a non-empty `description:`
4605 /// field on `apiVersion: v2` charts (`helm lint` surfaces it as
4606 /// `WARNING [chart.metadata.description]: description is required`),
4607 /// so the empty `Some("")` silently lands in the rendered
4608 /// artifacts and breaks at `helm lint` / `helm install` time far
4609 /// from the source `caixa.lisp`, with no field naming the
4610 /// offending `:descricao`.
4611 ///
4612 /// `None` (the canonical "omit the slot to defer to the renderer's
4613 /// `caixa.nome`-derived fallback" shape) is accepted trivially —
4614 /// the gate is a no-op when the author didn't declare a value.
4615 /// `Some("")` is gated by the narrower
4616 /// [`ManifestError::DescricaoEmpty`] arm, mirroring the empty-arm
4617 /// shape every peer per-axis empty gate uses
4618 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4619 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4620 /// [`ManifestError::RepositorioEmpty`]).
4621 ///
4622 /// Universal-axis (every kind carries `:descricao`), so wired at
4623 /// the 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_autores`] / [`Self::validate_repositorio`] /
4627 /// [`Self::validate_code_paths`] — before the kind-coherence
4628 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4629 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4630 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4631 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4632 /// specific slot sets.
4633 ///
4634 /// Past the empty arm the gate enforces the chart-description
4635 /// shape predicate via [`crate::render::is_chart_description_shape`]:
4636 /// the structural single-line UTF-8 floor every realistic chart
4637 /// description in the wild matches — 1..=512 bytes, no leading
4638 /// or trailing whitespace, no ASCII control characters anywhere
4639 /// (`0x00..=0x1F` plus `0x7F` DEL — banning tab, newline,
4640 /// carriage return, and every other control byte), Unicode
4641 /// continuation bytes accepted (the canonical fixtures carry
4642 /// `→` and `—`). Closes the canonical paste-from-doc footguns
4643 /// the bare empty-arm gate left open: paste-from-aligned-doc
4644 /// leading / trailing whitespace (`" Checkout flow."`,
4645 /// `"Checkout flow. "`), paste-from-multiline-doc newline
4646 /// (`"Checkout\nflow."`), paste-from-Windows-CRLF-doc CR
4647 /// (`"Checkout\rflow."`), tab-from-aligned-doc
4648 /// (`"Checkout\tflow."`), and paste-from-binary-blob NUL / BEL /
4649 /// ESC / DEL bytes. Mirrors the shape-predicate cascade
4650 /// [`Self::validate_repositorio`] / [`Self::validate_licenca`] /
4651 /// [`Self::validate_edicao`] establish past their own empty arms
4652 /// on the sibling universal-axis `Option<String>` Caixa-level
4653 /// value-shape surfaces.
4654 ///
4655 /// The empty-first cascade discipline mirrors every peer per-axis
4656 /// identity gate: [`ManifestError::DescricaoEmpty`] runs before
4657 /// [`ManifestError::DescricaoInvalid`], so the narrower empty
4658 /// diagnostic surfaces on `Some("")` rather than the broader
4659 /// shape-predicate diagnostic — peer with how
4660 /// [`ManifestError::LicencaEmpty`] runs before
4661 /// [`ManifestError::LicencaInvalid`],
4662 /// [`ManifestError::EdicaoEmpty`] runs before
4663 /// [`ManifestError::EdicaoInvalid`],
4664 /// [`ManifestError::RepositorioEmpty`] runs before
4665 /// [`ManifestError::RepositorioInvalid`].
4666 pub fn validate_descricao(&self) -> Result<(), ManifestError> {
4667 let Some(s) = self.descricao() else {
4668 return Ok(());
4669 };
4670 if s.is_empty() {
4671 return Err(ManifestError::DescricaoEmpty);
4672 }
4673 crate::render::is_chart_description_shape(s).map_err(|reason| {
4674 ManifestError::DescricaoInvalid {
4675 descricao: s.to_string(),
4676 reason,
4677 }
4678 })?;
4679 Ok(())
4680 }
4681
4682 /// Reject `:licenca` values that are the empty string. The flat
4683 /// `licenca: Option<String>` slot on [`Caixa`] is the universal
4684 /// SPDX-shaped license-expression axis every kind carries — the
4685 /// substrate routes the same string through the [`caixa-helm`]
4686 /// renderer's `build_readme` which folds it verbatim into the
4687 /// rendered `lareira-<nome>` Helm chart's `README.md` `## License`
4688 /// section (`caixa-helm/src/lib.rs:361`) via
4689 /// `caixa.licenca.clone().unwrap_or_else(|| "MIT".into())`. The
4690 /// fallback only fires on `None`; a `Some("")` *skips the
4691 /// fallback* and silently passes the empty string through to a
4692 /// chart `README.md` whose `License` section renders as the bare
4693 /// trailing period (`.\n`) — peer footgun with the
4694 /// `Some("")`-skips-`unwrap_or_else` shape the
4695 /// [`Self::validate_descricao`] and [`Self::validate_repositorio`]
4696 /// gates close on the sibling free-form-prose and git-URL axes.
4697 ///
4698 /// `None` (the canonical "omit the slot to defer to the
4699 /// renderer's `MIT` fallback" shape every existing fixture
4700 /// carries) is accepted trivially — the gate is a no-op when the
4701 /// author didn't declare a value. `Some("")` is gated by the
4702 /// narrower [`ManifestError::LicencaEmpty`] arm, mirroring the
4703 /// empty-arm shape every peer per-axis empty gate uses
4704 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4705 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4706 /// [`ManifestError::RepositorioEmpty`],
4707 /// [`ManifestError::DescricaoEmpty`]).
4708 ///
4709 /// Universal-axis (every kind carries `:licenca`), so wired at
4710 /// the caixa-build gate alongside the peer universal gates
4711 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4712 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4713 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4714 /// [`Self::validate_descricao`] / [`Self::validate_code_paths`]
4715 /// — before the kind-coherence gates
4716 /// ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4717 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4718 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4719 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4720 /// specific slot sets.
4721 ///
4722 /// Past the empty arm the gate enforces the SPDX-expression shape
4723 /// predicate via [`crate::render::is_spdx_expression_shape`]: the
4724 /// structural alphabet floor every realistic SPDX expression in
4725 /// the wild uses — ASCII alphanumeric plus `.`, `-`, `+`, `(`,
4726 /// `)`, `:` (the `DocumentRef-…:LicenseRef-…` separator), and a
4727 /// single ASCII space (token separator). Closes the canonical
4728 /// paste-from-doc footguns the bare empty-arm gate left open:
4729 /// paste-from-doc whitespace (`"MIT "`, `" MIT"`), paste-from-
4730 /// multiline-doc CRLF (`"MIT\n"`), tab-from-aligned-doc
4731 /// (`"MIT\tOR Apache-2.0"`), non-ASCII smart-quote paste,
4732 /// underscore-instead-of-hyphen typo (`"Apache_2.0"`),
4733 /// comma-instead-of-`OR`-keyword colloquial idiom (`"MIT,
4734 /// Apache-2.0"`), slash-dual-license colloquial idiom (`"MIT/
4735 /// Apache-2.0"`), and semicolon-list-separator confusion
4736 /// (`"MIT; Apache-2.0"`). Mirrors the shape-predicate cascade
4737 /// [`Self::validate_repositorio`] / [`Self::validate_edicao`]
4738 /// establish past their own empty arms.
4739 ///
4740 /// The empty-first cascade discipline mirrors every peer per-axis
4741 /// identity gate: [`ManifestError::LicencaEmpty`] runs before
4742 /// [`ManifestError::LicencaInvalid`], so the narrower empty
4743 /// diagnostic surfaces on `Some("")` rather than the broader
4744 /// shape-predicate diagnostic — peer with how
4745 /// [`ManifestError::EdicaoEmpty`] runs before
4746 /// [`ManifestError::EdicaoInvalid`],
4747 /// [`ManifestError::RepositorioEmpty`] runs before
4748 /// [`ManifestError::RepositorioInvalid`].
4749 ///
4750 /// A future tightening on this axis can extend the alphabet
4751 /// floor into a full SPDX expression parser + license-id
4752 /// allowlist (rejecting alphabet-valid values that don't name a
4753 /// real SPDX license identifier — e.g., `"NotAReal"` is
4754 /// alphabet-valid but no `NotAReal` license-id exists). That
4755 /// parser only becomes meaningful past a real SPDX-spec
4756 /// dependency; this gate establishes the structural floor by
4757 /// refusing every non-SPDX-alphabet value at validate time.
4758 pub fn validate_licenca(&self) -> Result<(), ManifestError> {
4759 let Some(s) = self.licenca() else {
4760 return Ok(());
4761 };
4762 if s.is_empty() {
4763 return Err(ManifestError::LicencaEmpty);
4764 }
4765 crate::render::is_spdx_expression_shape(s).map_err(|reason| {
4766 ManifestError::LicencaInvalid {
4767 licenca: s.to_string(),
4768 reason,
4769 }
4770 })?;
4771 Ok(())
4772 }
4773
4774 /// Reject `:edicao` values that are the empty string. The flat
4775 /// `edicao: Option<String>` slot on [`Caixa`] is the universal
4776 /// language-edition axis every kind carries — it determines the
4777 /// tatara-lisp macro surface + compatibility flags the substrate
4778 /// applies when building a caixa, and lands verbatim in the
4779 /// `Caixa::template` author-time scaffold (the canonical
4780 /// `:edicao "2026"` line every `feira init` emits via
4781 /// [`Caixa::template`] at `caixa-core/src/manifest.rs:1193`) and
4782 /// in every renderer-side fixture (`caixa-helm/src/lib.rs:375`,
4783 /// `caixa-flux/src/lib.rs:445`, `caixa-mesh/src/lib.rs:629`,
4784 /// `caixa-core/src/render.rs:2510`) via
4785 /// `edicao: Some("2026".into())`.
4786 ///
4787 /// `None` (the canonical "omit the slot to defer to the
4788 /// substrate's default edition" shape every existing
4789 /// [`caixa-resolver`] integration test fixture carries via
4790 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4791 /// is accepted trivially — the gate is a no-op when the author
4792 /// didn't declare a value. `Some("")` is gated by the narrower
4793 /// [`ManifestError::EdicaoEmpty`] arm, mirroring the empty-arm
4794 /// shape every peer per-axis empty gate uses
4795 /// ([`ManifestError::NomeEmpty`], [`ManifestError::VersaoEmpty`],
4796 /// [`ManifestError::EtiquetaEmpty`], [`ManifestError::AutorEmpty`],
4797 /// [`ManifestError::RepositorioEmpty`],
4798 /// [`ManifestError::DescricaoEmpty`], [`ManifestError::LicencaEmpty`]).
4799 ///
4800 /// Universal-axis (every kind carries `:edicao`), so wired at
4801 /// the caixa-build gate alongside the peer universal gates
4802 /// [`Self::validate_nome`] / [`Self::validate_versao`] /
4803 /// [`Self::validate_deps`] / [`Self::validate_etiquetas`] /
4804 /// [`Self::validate_autores`] / [`Self::validate_repositorio`] /
4805 /// [`Self::validate_descricao`] / [`Self::validate_licenca`] /
4806 /// [`Self::validate_code_paths`] — before the kind-coherence
4807 /// gates ([`crate::LayoutError::MeshSlotsOnNonAplicacao`] /
4808 /// [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] /
4809 /// [`crate::LayoutError::ServicoSlotsOnNonServico`] /
4810 /// [`crate::LayoutError::ForeignCodeSlot`]) which fence kind-
4811 /// specific slot sets.
4812 ///
4813 /// Past the empty arm the gate enforces the canonical year-shape
4814 /// predicate: every documented tatara-lisp edition is a 4-digit
4815 /// ASCII decimal year (`"2026"` is the only edition currently
4816 /// minted; future-introduced siblings will follow the same
4817 /// shape, peer with Cargo's `[package] edition` grammar which
4818 /// every value Cargo has ever accepted matches — `"2015"`,
4819 /// `"2018"`, `"2021"`, `"2024"`). Any value that's not exactly
4820 /// 4 ASCII decimal bytes is rejected with the narrower
4821 /// [`ManifestError::EdicaoInvalid`] arm, mirroring the
4822 /// shape-predicate cascade [`Self::validate_repositorio`]
4823 /// establishes past its own empty arm
4824 /// ([`ManifestError::RepositorioEmpty`] →
4825 /// [`ManifestError::RepositorioInvalid`]). Closes the canonical
4826 /// paste-from-doc footguns the bare empty-arm gate left open:
4827 ///
4828 /// - leading / trailing whitespace from a paste-from-doc
4829 /// (`"2026 "`, `" 2026"`)
4830 /// - control characters / CRLF from a paste-from-multiline-doc
4831 /// (`"2026\n"`)
4832 /// - non-ASCII look-alikes from a fullwidth keyboard
4833 /// (`"2026"`) which would silently land as a non-ASCII
4834 /// string in the rendered caixa.lisp
4835 /// - free-form non-year values (`"x"`, `"latest"`,
4836 /// `"nightly"`) that have no operational meaning on the
4837 /// substrate's build-time edition selector
4838 /// - leading non-digit prefixes (`"v2026"`, `"e2026"`,
4839 /// `"r2026"`) — common version-tag idioms that don't apply
4840 /// to the year-shaped edition axis
4841 /// - decimal-shaped values (`"2026.1"`, `"2026.0"`) — every
4842 /// edition is a year, not a fractional version
4843 /// - wrong-length numeric values (`"26"`, `"202"`, `"20260"`,
4844 /// `"00026"`) that don't name a year
4845 ///
4846 /// `None` (the canonical "omit the slot to defer to the
4847 /// substrate's default edition" shape every existing
4848 /// [`caixa-resolver`] integration test fixture carries via
4849 /// `edicao: None` — see `caixa-resolver/tests/git_integration.rs`)
4850 /// is accepted trivially — the gate is a no-op when the author
4851 /// didn't declare a value. The empty-first cascade discipline
4852 /// mirrors every peer per-axis identity gate:
4853 /// [`ManifestError::EdicaoEmpty`] runs before
4854 /// [`ManifestError::EdicaoInvalid`], so the narrower empty
4855 /// diagnostic surfaces on `Some("")` rather than the broader
4856 /// shape-predicate diagnostic — peer with how
4857 /// [`ManifestError::NomeEmpty`] runs before
4858 /// [`ManifestError::NomeInvalid`],
4859 /// [`ManifestError::VersaoEmpty`] runs before
4860 /// [`ManifestError::VersaoInvalid`],
4861 /// [`ManifestError::RepositorioEmpty`] runs before
4862 /// [`ManifestError::RepositorioInvalid`].
4863 ///
4864 /// A future tightening on this axis can extend the shape
4865 /// predicate into a known-edition allowlist (rejecting
4866 /// year-shaped values that don't name a tatara-lisp edition
4867 /// the substrate actually understands — e.g., `"1999"` is
4868 /// year-shaped but no `1999` edition exists). That allowlist
4869 /// only becomes meaningful past the introduction of a sibling
4870 /// edition to `"2026"`; this gate establishes the structural
4871 /// floor by refusing every non-year-shaped value at validate
4872 /// time.
4873 pub fn validate_edicao(&self) -> Result<(), ManifestError> {
4874 let Some(s) = self.edicao() else {
4875 return Ok(());
4876 };
4877 if s.is_empty() {
4878 return Err(ManifestError::EdicaoEmpty);
4879 }
4880 if s.len() != 4 || !s.bytes().all(|b| b.is_ascii_digit()) {
4881 return Err(ManifestError::EdicaoInvalid {
4882 edicao: s.to_string(),
4883 reason: "must be a 4-digit ASCII decimal year (canonical \"2026\")".to_string(),
4884 });
4885 }
4886 Ok(())
4887 }
4888
4889 /// Compose the supervisor-related flat slots into a single
4890 /// [`SupervisorSpec`] for validation. Returns `None` when the
4891 /// caixa isn't a `:kind Supervisor`.
4892 ///
4893 /// The flat representation in [`Caixa`] keeps tatara-lisp authoring
4894 /// simple (one form, no nested `:supervisor (…)` block); this view
4895 /// is the "typed shape" the operator + supervisor reconciler
4896 /// consume.
4897 #[must_use]
4898 pub fn supervisor_view(&self) -> Option<SupervisorSpec> {
4899 if !self.kind().is_supervisor() {
4900 return None;
4901 }
4902 // Fold through the shared `supervisor::duration_codec::parse`
4903 // — the same parser the serde-routed `with = "duration_codec"`
4904 // on `SupervisorSpec::restart_window`, the `:politicas
4905 // :timeout` codec, and the `:politicas :circuit-breaker
4906 // :window` codec all consume. The prior inline f64-shaped
4907 // duplicate (`parse_window_inline`) admitted every magnitude
4908 // the integer-magnitude gate (1c55a2a) rejects on the three
4909 // serde-routed siblings — `"1.5s"`, `"1.0s"`, `"0.5m"`,
4910 // `"+30s"`, `"-30s"` — and silently dropped malformed input as
4911 // `None` (i.e. "no reset"), divergent from the shared codec's
4912 // integer-magnitude discipline by construction. The fold
4913 // closes the divergence: every value the typed
4914 // `SupervisorSpec` carries past `supervisor_view` is in the
4915 // shared codec's accepted set. The `.ok()` here preserves the
4916 // existing soft-swallow shape on this view-construction path;
4917 // the new [`Caixa::validate_restart_window`] (sibling of
4918 // [`Self::validate_nome`] / [`Self::validate_versao`]) names
4919 // the offending raw string at build time so authoring tools
4920 // (`feira lint`, the future layout-side wire-up) surface a
4921 // self-locating diagnostic instead of a silently dropped
4922 // window.
4923 let restart_window = self
4924 .restart_window()
4925 .and_then(|s| crate::supervisor::duration_codec::parse(s).ok());
4926 Some(SupervisorSpec {
4927 estrategia: self.estrategia().unwrap_or_default(),
4928 max_restarts: self.max_restarts().unwrap_or(5),
4929 restart_window,
4930 children: self.children().to_vec(),
4931 })
4932 }
4933
4934 /// A minimal starter manifest emitted by `feira init`.
4935 #[must_use]
4936 pub fn template(nome: &str) -> String {
4937 format!(
4938 "(defcaixa\n \
4939 :nome {nome:?}\n \
4940 :versao \"0.1.0\"\n \
4941 :kind Biblioteca\n \
4942 :edicao \"2026\"\n \
4943 :descricao \"FIXME — describe this caixa\"\n \
4944 :autores ()\n \
4945 :etiquetas ()\n \
4946 :deps ()\n \
4947 :deps-dev ()\n \
4948 :bibliotecas (\"lib/{nome}.lisp\"))\n"
4949 )
4950 }
4951
4952 /// Serialize to a canonical `caixa.lisp` source — suitable for writing
4953 /// back after mutation (e.g. `feira add`).
4954 ///
4955 /// Goes through serde JSON → canonical Sexp → per-field pretty print.
4956 /// The derive-macro `compile_from_sexp` path is the inverse, so any
4957 /// `Caixa` round-trips through `to_lisp` + `from_lisp`.
4958 #[must_use]
4959 pub fn to_lisp(&self) -> String {
4960 let json = serde_json::to_value(self).expect("Caixa serialize");
4961 let sexp = tatara_lisp::domain::json_to_sexp(&json);
4962 let tatara_lisp::Sexp::List(items) = sexp else {
4963 return format!("(defcaixa {sexp})\n");
4964 };
4965 let mut out = String::from("(defcaixa");
4966 let mut i = 0;
4967 while i + 1 < items.len() {
4968 out.push_str("\n ");
4969 out.push_str(&items[i].to_string());
4970 out.push(' ');
4971 out.push_str(&items[i + 1].to_string());
4972 i += 2;
4973 }
4974 out.push_str(")\n");
4975 out
4976 }
4977}
4978
4979/// Errors raised by top-level [`Caixa`] validators that don't fit
4980/// the per-axis [`DepError`] / [`crate::AplicacaoError`] /
4981/// [`crate::SupervisorError`] / [`crate::LayoutError`] families —
4982/// the Caixa's own identity axes (`:nome`, `:versao`) that flow
4983/// through every substrate-side artifact's `metadata.name` /
4984/// version derivation.
4985///
4986/// A future top-level sum (the M4 `CaixaError` the [`DepError`]
4987/// doc-comment anticipates) can hold one of each per-axis error
4988/// family without reshaping individual diagnostics; this enum is
4989/// the first such per-Caixa-identity family.
4990#[derive(Debug, Error, PartialEq, Eq)]
4991pub enum ManifestError {
4992 #[error(
4993 ":nome is empty (every caixa must name itself; the value flows \
4994 into every K8s artifact's `metadata.name` derivation and into \
4995 the default `lib/<nome>.lisp` / `exe/<nome>` layout paths)"
4996 )]
4997 NomeEmpty,
4998 #[error(
4999 ":nome {nome:?} is not a valid DNS-1123 label: {reason} (the K8s \
5000 apiserver enforces this rule on every `metadata.name` the \
5001 caixa's substrate-side renderers derive from `:nome` — the \
5002 `lareira-<nome>` Helm chart name, the programs.yaml entry \
5003 name, the `LABEL_APLICACAO` label value, the `<aplicacao>-<de>-to-<para>` \
5004 CiliumNetworkPolicy name, the `<aplicacao>-<para>` HTTPRoute \
5005 name; use a lowercase alphanumeric + hyphen identifier like \
5006 `\"checkout\"` or `\"cart-v2\"`)"
5007 )]
5008 NomeInvalid { nome: String, reason: String },
5009 #[error(
5010 ":nome {nome:?} overflows the joint-length budget on the canonical \
5011 `lareira-<nome>` chart-name shape: {reason} (every per-Servico / \
5012 per-Aplicacao renderer the substrate carries — `caixa-helm`'s \
5013 `Chart.yaml::name`, `caixa-flux`'s `cluster_bundle` HelmRelease \
5014 `chart:` slot, `caixa-tatara`'s `release_name` + \
5015 `oci://<registry>/lareira-<nome>` chart ref — derives the same \
5016 joint name through the canonical `lareira_chart_name` helper, and \
5017 Helm's `Chart.yaml::name` admission rule + the K8s apiserver's \
5018 DNS-1123 label cap on every chart-name-derived `metadata.name` \
5019 reject any joint name exceeding 63 bytes; the narrower \
5020 `:nome` shape (`NomeInvalid`) gates the bare-`:nome` budget, this \
5021 arm gates the chart-name budget downstream renderers inherit)"
5022 )]
5023 NomeChartNameBudgetExceeded { nome: String, reason: String },
5024 #[error(
5025 ":versao is empty (every caixa must pin its own version; the value flows \
5026 into the `lareira-<nome>` Helm chart's `Chart.yaml` version + appVersion, \
5027 the `feira publish` `v<versao>` git tag, the OCI image's `:v<versao>` / \
5028 `:latest` tags, the lacre closure's `concrete_versao`, and the \
5029 `:upgrade-from :from` peers — use a SemVer-2 literal like `\"0.1.0\"`)"
5030 )]
5031 VersaoEmpty,
5032 #[error(
5033 ":versao {versao:?} is not a valid SemVer-2 version: {reason} (the substrate \
5034 consumes this string as `semver::Version` — three-part `MAJOR.MINOR.PATCH` \
5035 with optional `-prerelease` and `+build` — across every artifact derived \
5036 from `:versao`: the `lareira-<nome>` Helm chart's `Chart.yaml` version + \
5037 appVersion (Helm SemVer-2-strict), the `feira publish` `v<versao>` git tag, \
5038 the OCI image's `:v<versao>` tag, the lacre closure's `concrete_versao`, \
5039 and the `:upgrade-from :from` peers that match against this exact shape; \
5040 use a literal like `\"0.1.0\"`, `\"0.2.0-rc.1\"`, or `\"1.0.0+build.42\"` — \
5041 not a git-tag-shape like `\"v0.1.0\"`, a docker-tag-shape like `\"latest\"`, \
5042 a requirement-shape like `\"^0.1\"`, or a four-part `\"0.1.0.0\"`)"
5043 )]
5044 VersaoInvalid { versao: String, reason: String },
5045 #[error(
5046 ":restart-window {restart_window:?} is not a valid duration: {reason} (the \
5047 substrate consumes this string through the shared \
5048 `supervisor::duration_codec` — the same parser routed via `with = \
5049 \"duration_codec\"` onto the typed `SupervisorSpec::restart_window`, \
5050 `:politicas :timeout`, and `:politicas :circuit-breaker :window` slots; \
5051 the canonical authoring form is `<integer><unit>` where the unit is one \
5052 of `ms` / `s` / `m` / `h` and the magnitude has no decimal point and no \
5053 leading `+` / `-` sign — e.g. `\"60s\"`, `\"5m\"`, `\"1h\"`, `\"500ms\"`. \
5054 Without this gate a malformed `:restart-window` silently produced a \
5055 supervisor with `restart_window: None` (\"never reset\"), turning OTP's \
5056 `MaxIntensity / Period` invariant into a never-reset supervisor far from \
5057 the source `caixa.lisp`; the gate moves the diagnostic to the manifest \
5058 layer with the offending value named verbatim. Omit the slot entirely to \
5059 express \"no reset\"; carry a positive integer duration to express the \
5060 sliding window)"
5061 )]
5062 RestartWindowMalformed {
5063 restart_window: String,
5064 reason: String,
5065 },
5066 #[error(
5067 "{slot} entry is an empty path string — every {slot} entry must name \
5068 a file relative to the caixa root; omit the entry to omit the file \
5069 (the layout checker's `root.join(\"\")` resolves to the caixa root \
5070 itself, so an empty entry silently aliases the project root as a \
5071 declared {slot} file, then fails downstream at parse / existence \
5072 time with a diagnostic that names the root rather than the offending \
5073 entry)"
5074 )]
5075 CodePathEmpty { slot: &'static str },
5076 #[error(
5077 "{slot} entry {} is an absolute path — entries must be relative to \
5078 the caixa root, since `Path::join` replaces the base with an absolute \
5079 right-hand side and `root.join(\"/abs/...\")` resolves to \"/abs/...\" \
5080 outside the caixa root sandbox; rewrite the entry as a relative path \
5081 under the caixa root (e.g. `\"lib/<name>.lisp\"`, `\"exe/<name>\"`, \
5082 `\"servicos/<name>.computeunit.yaml\"`)",
5083 path.display()
5084 )]
5085 CodePathAbsolute { slot: &'static str, path: PathBuf },
5086 #[error(
5087 "{slot} entry {} contains a `..` component — entries must not traverse \
5088 above the caixa root (the layout's `starts_with(<dir>)` fence on \
5089 `:exe` / `:servicos` is component-aware, not canonical-path-aware, \
5090 so a mid-path `..` silently traverses the sandbox; `:bibliotecas` \
5091 has no such fence, so a leading `..` escapes unconditionally if the \
5092 resolved target happens to exist)",
5093 path.display()
5094 )]
5095 CodePathParentEscape { slot: &'static str, path: PathBuf },
5096 #[error(
5097 "{slot} entry {} does not terminate in the `.lisp` extension — every \
5098 `:bibliotecas` entry is a tatara-lisp source file the `feira build` \
5099 loop reads through `tatara_lisp::read` at parse time, so any other \
5100 extension (`.rs`, `.txt`, `.lisp.bak`) or no-extension shape is \
5101 structurally a parser error far from the source caixa.lisp, with \
5102 no field naming the offending `:bibliotecas` entry. Pin a relative \
5103 path under the caixa root whose terminating extension is \
5104 lowercase-`.lisp` (e.g. `\"lib/<name>.lisp\"`, \
5105 `\"lib/handlers.lisp\"`) — the same file-type contract the peer \
5106 `:behavior :on-*` (c97815a) and `:upgrade-from :state-change :script` \
5107 (33cc830) axes already carry through the same lifted \
5108 `is_lisp_extension` predicate",
5109 path.display()
5110 )]
5111 CodePathNonLispExtension { slot: &'static str, path: PathBuf },
5112 #[error(
5113 "{slot} entry {} does not terminate in the `.computeunit.yaml` \
5114 compound suffix — every `:servicos` entry is a typed `ComputeUnit` \
5115 CR YAML file the peer caixa-helm / caixa-flux renderers consume \
5116 through `serde_yaml::from_str` at chart / FluxCD bundle render \
5117 time, so any other extension (`.yaml`, `.yml`, `.json`, the \
5118 off-by-one-segment `.computeunit-yaml`, the editor-backup \
5119 `.computeunit.yaml.bak`) or no-extension shape is structurally a \
5120 YAML-parser error / `ComputeUnit` schema-mismatch far from the \
5121 source caixa.lisp, with no field naming the offending `:servicos` \
5122 entry. Pin a relative path under the caixa root whose terminating \
5123 compound suffix is lowercase-`.computeunit.yaml` (e.g. \
5124 `\"servicos/<name>.computeunit.yaml\"`, \
5125 `\"servicos/hello-rio.computeunit.yaml\"`) — the same file-type \
5126 contract the sibling `:bibliotecas` axis (64772a9) already carries \
5127 on the tatara-lisp-source axis through the peer lifted \
5128 `is_lisp_extension` predicate, here on the compound-suffix axis \
5129 `Path::extension` can't express on its own through the lifted \
5130 `is_computeunit_yaml_extension` predicate",
5131 path.display()
5132 )]
5133 CodePathNonComputeUnitYamlExtension { slot: &'static str, path: PathBuf },
5134 #[error(
5135 "{slot} entry {} appears more than once (the code-path list is \
5136 a set, not a multiset; every peer Vec-shaped author-supplied \
5137 list past validate is set-not-multiset — `:membros :caixa`, \
5138 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5139 `:children :caixa`, `:deps` / `:deps-dev` `:nome`, \
5140 `:upgrade-from :from`, `:etiquetas`, `:autores` — and the three \
5141 code-path lists are the last Vec-shaped author-supplied slots on \
5142 the typed Caixa surface still admitting a duplicate entry. \
5143 `:bibliotecas` duplicates re-parse the same file at \
5144 `feira build` time and silently mask the author's intent to \
5145 declare a *second* biblioteca; `:exe` duplicates collide on the \
5146 flake `packages.<name>` derivation key at the future \
5147 `caixa-flake` materializer; `:servicos` duplicates surface as the \
5148 narrower [`caixa-helm`] / [`caixa-flux`] `UnsupportedServicoCount` \
5149 rejection far from the source `caixa.lisp`. Drop the duplicate \
5150 or rename it to the actual second file intended)",
5151 path.display()
5152 )]
5153 CodePathDuplicate { slot: &'static str, path: PathBuf },
5154 #[error(
5155 ":etiquetas entry is empty (every tag must carry a non-empty \
5156 registry-search identifier; the empty entry has no operational \
5157 meaning — it indexes nothing in the future caixa-registry search \
5158 axis and clutters the rendered Helm `Chart.yaml` `keywords:` array \
5159 with a no-op tag; omit the entry to express \"no tag on this \
5160 position\")"
5161 )]
5162 EtiquetaEmpty,
5163 #[error(
5164 ":etiquetas entry {etiqueta:?} appears more than once (the \
5165 registry-search tag set is a set, not a multiset; duplicate \
5166 entries are silently dedup'd by caixa-helm's `BTreeSet` collect \
5167 at chart render — a \"second wins / one silently disappears\" \
5168 shape divergent from every peer typed-graph set gate \
5169 (`:membros :caixa`, `:placement :clusters`, `:entrada :paths`, \
5170 `:contratos`, `:deps :nome`, `:upgrade-from :from`); drop the \
5171 duplicate or rename it to the actual tag intended)"
5172 )]
5173 EtiquetaDuplicate { etiqueta: String },
5174 #[error(
5175 ":etiquetas entry {etiqueta:?} is not a valid chart-keyword shape: \
5176 {reason} (the substrate consumes this string through the shared \
5177 `crate::render::is_chart_keyword_shape` predicate — the same \
5178 Cargo crates.io `[package] keywords` grammar entry shape: 1..=20 \
5179 bytes, starts with an ASCII letter, ASCII alphanumeric / `_` / `-` \
5180 continuation. The canonical authoring shapes are short kebab-case \
5181 identifiers like `\"mesh\"`, `\"wasm\"`, `\"tatara-lisp\"`, \
5182 `\"hello-world\"`, `\"caixa-servico\"`, `\"infrastructure\"`. \
5183 Without this gate a malformed `:etiquetas` entry (paste-from-doc \
5184 leading / trailing whitespace `\" mesh\"` / `\"mesh \"`; \
5185 paste-from-multiline-doc newline `\"mesh\\nhttp\"`; \
5186 paste-from-Windows-CRLF-doc CR; CSV-list-separator confusion \
5187 `\"mesh,http,grpc\"` — the author meant to author three separate \
5188 list entries; path-separator confusion `\"caixa/servico\"`; \
5189 namespace-suffix `\"http.1\"`; leading-digit `\"1foo\"`; \
5190 kebab-leak `\"-foo\"`; snake-leak `\"_foo\"`; non-ASCII \
5191 `\"café\"` — every legitimate search tag is strict ASCII; \
5192 paste-from-binary-blob NUL / BEL / ESC / DEL byte) silently \
5193 passed `from_lisp` + `validate_etiquetas` + \
5194 `StandardLayout::verify` and landed in the rendered \
5195 `lareira-<nome>` Helm chart's `Chart.yaml keywords:` array as a \
5196 malformed search tag — Artifact Hub's keyword index + the future \
5197 caixa-registry's keyword index would either silently drop the \
5198 tag or fail to index it far from the source caixa.lisp; the gate \
5199 moves the diagnostic to the manifest layer with the offending \
5200 value named verbatim)"
5201 )]
5202 EtiquetaInvalid { etiqueta: String, reason: String },
5203 #[error(
5204 ":autores entry is empty (every maintainer must carry a non-empty \
5205 identifier; the empty entry has no operational meaning — it \
5206 identifies no one in the substrate's authorship index and renders \
5207 as `maintainers: [{{name: \"\", email: null}}]` in the Helm chart's \
5208 `Chart.yaml`, a no-op maintainer the substrate cannot route to; \
5209 omit the entry to express \"no maintainer on this position\")"
5210 )]
5211 AutorEmpty,
5212 #[error(
5213 ":autores entry {autor:?} appears more than once (the maintainer \
5214 set is a set, not a multiset; unlike `:etiquetas`, caixa-helm's \
5215 `maintainers:` rendering does *no* dedup — duplicate entries \
5216 stack verbatim in `Chart.yaml` as two identical \
5217 `Maintainer {{ name, email: None }}` records, divergent from every \
5218 peer typed-graph set gate (`:etiquetas`, `:membros :caixa`, \
5219 `:placement :clusters`, `:entrada :paths`, `:contratos`, \
5220 `:deps :nome`, `:upgrade-from :from`); drop the duplicate or \
5221 rename it to the actual author intended)"
5222 )]
5223 AutorDuplicate { autor: String },
5224 #[error(
5225 ":autores entry {autor:?} is not a valid chart-maintainer-name shape: \
5226 {reason} (the substrate consumes this string through the shared \
5227 `crate::render::is_chart_maintainer_name_shape` predicate — the same \
5228 single-line-UTF-8 floor every realistic chart maintainer name carries: \
5229 1..=128 bytes, no leading or trailing whitespace, no ASCII control \
5230 characters anywhere, Unicode bytes accepted. The canonical authoring \
5231 shapes are short single-line identifiers like `\"pleme-io\"`, \
5232 `\"Pleme Contributors\"`, `\"alice <alice@example.com>\"`, \
5233 `\"François Dupont\"`. Without this gate a malformed `:autores` entry \
5234 (paste-from-aligned-doc leading whitespace `\" pleme-io\"` / trailing \
5235 whitespace `\"pleme-io \"`; paste-from-multiline-doc newline \
5236 `\"alice\\nbob\"` — the author pasted a multi-line block of author \
5237 records into one entry instead of splitting into one entry per author; \
5238 paste-from-Windows-CRLF-doc carriage return `\"alice\\rbob\"`; \
5239 tab-from-aligned-doc `\"Pleme\\tContributors\"`; paste-from-binary-blob \
5240 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5241 `validate_autores` + `StandardLayout::verify` and landed in the \
5242 rendered `lareira-<nome>` Helm chart's `Chart.yaml maintainers:` array \
5243 as a YAML-illegal multi-line scalar or a silently-trimmed whitespace \
5244 round-trip — every chart-aware UI (`helm list`, `helm search`, \
5245 Artifact Hub maintainer index) would render the maintainer name in a \
5246 single-line column far from the source caixa.lisp; the gate moves the \
5247 diagnostic to the manifest layer with the offending value named \
5248 verbatim)"
5249 )]
5250 AutorInvalid { autor: String, reason: String },
5251 #[error(
5252 ":repositorio is the empty string (every published caixa names its \
5253 git source via a non-empty `:repositorio` locator — the value \
5254 flows verbatim into the rendered `lareira-<nome>` Helm chart's \
5255 `Chart.yaml` `home:` field via `caixa-helm` and into the FluxCD \
5256 `GitRepository.spec.url` via `caixa-flux`'s \
5257 `ClusterBundleOpts::for_caixa`; both consumers' \
5258 `Option::unwrap_or_else` fallbacks only fire when the slot is \
5259 `None`, so an empty `Some(\"\")` silently lands as `home: \"\"` / \
5260 `url: \"\"` in the rendered artifacts and breaks at `helm \
5261 template` / FluxCD source-controller reconcile time far from the \
5262 source caixa.lisp; omit the slot entirely to defer to the \
5263 renderer's `https://github.com/pleme-io/<nome>` / \
5264 `caixa.nome`-derived fallback, or carry a canonical authoring \
5265 shape like `\"github:org/repo\"`, `\"https://host/path\"`, \
5266 `\"ssh://[user@]host/path\"`, `\"git@host:path\"`, or \
5267 `\"file:///path\"`)"
5268 )]
5269 RepositorioEmpty,
5270 #[error(
5271 ":repositorio {repositorio:?} is not a valid git repo URL: {reason} \
5272 (the substrate consumes this string through the shared \
5273 `crate::render::is_git_repo_url` predicate — the same parser the \
5274 peer `:deps :fonte (:tipo git :repo …)` axis routes its `:repo` \
5275 value through via `DepSource::validate`; the canonical authoring \
5276 shapes are `\"github:org/repo\"` shorthand, `\"https://host/path\"` \
5277 / `\"ssh://[user@]host/path\"` / `\"git://host/path\"` / \
5278 `\"file:///path\"` URL schemes, or the `\"git@host:path\"` \
5279 scp-style SSH form. Without this gate a malformed `:repositorio` \
5280 (whitespace from a paste-from-doc; control characters / CRLF \
5281 from a paste-from-multiline-doc; a leading `-` from a \
5282 CLI-argument-injection footgun; a missing `:` separator from a \
5283 bare `org/repo` shape git treats as a relative filesystem path) \
5284 silently landed in the rendered `Chart.yaml home:` and the \
5285 FluxCD `GitRepository.spec.url` and broke at `git clone` / \
5286 FluxCD reconcile time far from the source caixa.lisp; the gate \
5287 moves the diagnostic to the manifest layer with the offending \
5288 value named verbatim)"
5289 )]
5290 RepositorioInvalid { repositorio: String, reason: String },
5291 #[error(
5292 ":descricao is the empty string (every published caixa names \
5293 its purpose via a non-empty `:descricao` summary — the value \
5294 flows verbatim into the rendered `lareira-<nome>` Helm \
5295 chart's `Chart.yaml` `description:` field via `caixa-helm`'s \
5296 `build_chart_yaml` and into the chart `README.md` header via \
5297 `build_readme`; both consumers' `Option::unwrap_or_else` \
5298 `caixa.nome`-derived fallbacks only fire when the slot is \
5299 `None`, so an empty `Some(\"\")` silently lands as \
5300 `description: \"\"` / a blank `README.md` header in the \
5301 rendered artifacts and breaks at `helm lint` time \
5302 (`WARNING [chart.metadata.description]: description is \
5303 required` on `apiVersion: v2` charts) far from the source \
5304 caixa.lisp; omit the slot entirely to defer to the \
5305 renderer's `\"Generated chart for caixa Servico <nome>\"` / \
5306 `\"caixa Servico <nome>\"` fallbacks, or carry a non-empty \
5307 summary like `\"Canonical Rust→wasm32-wasip2 caixa \
5308 Servico.\"`)"
5309 )]
5310 DescricaoEmpty,
5311 #[error(
5312 ":descricao {descricao:?} is not a valid chart-description shape: \
5313 {reason} (the substrate consumes this string through the shared \
5314 `crate::render::is_chart_description_shape` predicate — the same \
5315 single-line-UTF-8 floor every realistic chart description carries: \
5316 1..=512 bytes, no leading or trailing whitespace, no ASCII control \
5317 characters anywhere, Unicode prose bytes accepted. The canonical \
5318 authoring shapes are short single-line summaries like `\"Canonical \
5319 Rust→wasm32-wasip2 caixa Servico.\"`, `\"Checkout flow.\"`, \
5320 `\"AWS provider caixa for tatara-lisp\"`. Without this gate a \
5321 malformed `:descricao` (paste-from-aligned-doc leading whitespace \
5322 `\" Checkout flow.\"` / trailing whitespace `\"Checkout flow. \"`; \
5323 paste-from-multiline-doc newline `\"Checkout\\nflow.\"`; \
5324 paste-from-Windows-CRLF-doc carriage return `\"Checkout\\rflow.\"`; \
5325 tab-from-aligned-doc `\"Checkout\\tflow.\"`; paste-from-binary-blob \
5326 NUL / BEL / ESC / DEL byte) silently passed `from_lisp` + \
5327 `validate_descricao` + `StandardLayout::verify` and landed in the \
5328 rendered `lareira-<nome>` Helm chart's `Chart.yaml description:` \
5329 field + `README.md` header paragraph as a YAML-illegal multi-line \
5330 scalar or a silently-trimmed whitespace round-trip — every \
5331 chart-aware UI (`helm list`, `helm search`, Artifact Hub) would \
5332 render the description in a single-line column far from the source \
5333 caixa.lisp; the gate moves the diagnostic to the manifest layer \
5334 with the offending value named verbatim)"
5335 )]
5336 DescricaoInvalid { descricao: String, reason: String },
5337 #[error(
5338 ":licenca is the empty string (every published caixa names \
5339 its license via a non-empty `:licenca` SPDX expression — the \
5340 value flows verbatim into the rendered `lareira-<nome>` Helm \
5341 chart's `README.md` `## License` section via `caixa-helm`'s \
5342 `build_readme` at `caixa-helm/src/lib.rs:361`; the consumer's \
5343 `Option::unwrap_or_else(|| \"MIT\".into())` `MIT` fallback \
5344 only fires when the slot is `None`, so an empty `Some(\"\")` \
5345 silently lands as a bare trailing period in the rendered \
5346 chart `README.md` `License` section far from the source \
5347 caixa.lisp; omit the slot entirely to defer to the \
5348 renderer's `MIT` fallback, or carry a canonical SPDX \
5349 expression like `\"MIT\"`, `\"Apache-2.0\"`, \
5350 `\"Apache-2.0 OR MIT\"`)"
5351 )]
5352 LicencaEmpty,
5353 #[error(
5354 ":licenca {licenca:?} is not a valid SPDX expression shape: {reason} \
5355 (the substrate consumes this string through the shared \
5356 `crate::render::is_spdx_expression_shape` predicate — the same \
5357 alphabet-floor parser every peer per-axis value-shape gate routes \
5358 its value through; the canonical authoring shapes are single \
5359 license identifiers like `\"MIT\"`, `\"Apache-2.0\"`, `\"BSD-3-Clause\"`, \
5360 compound expressions like `\"Apache-2.0 OR MIT\"`, \
5361 `\"MIT AND BSD-3-Clause\"`, `\"(MIT OR Apache-2.0) AND ISC\"`, \
5362 license-with-exception forms like `\"Apache-2.0 WITH LLVM-exception\"`, \
5363 `+`-suffix variants like `\"GPL-2.0+\"`, and user-defined references \
5364 like `\"LicenseRef-MyLicense\"` / \
5365 `\"DocumentRef-doc:LicenseRef-MyLicense\"`. Without this gate a \
5366 malformed `:licenca` (paste-from-doc whitespace `\"MIT \"` / \
5367 `\" MIT\"`; paste-from-multiline-doc CRLF `\"MIT\\n\"`; \
5368 tab-from-aligned-doc `\"MIT\\tOR Apache-2.0\"`; non-ASCII byte from \
5369 a smart-quote paste; underscore-instead-of-hyphen typo \
5370 `\"Apache_2.0\"`; comma-instead-of-`OR`-keyword colloquial idiom \
5371 `\"MIT, Apache-2.0\"`; slash-dual-license colloquial idiom \
5372 `\"MIT/Apache-2.0\"`; semicolon-list-separator confusion \
5373 `\"MIT; Apache-2.0\"`) silently landed in the rendered chart \
5374 `README.md` `## License` section + a future SPDX-aware \
5375 `Chart.yaml license:` emitter would refuse the value at \
5376 `helm lint` time far from the source caixa.lisp; the gate moves \
5377 the diagnostic to the manifest layer with the offending value \
5378 named verbatim)"
5379 )]
5380 LicencaInvalid { licenca: String, reason: String },
5381 #[error(
5382 ":edicao is the empty string (every published caixa names \
5383 its language edition via a non-empty `:edicao` value — the \
5384 edition determines the tatara-lisp macro surface + \
5385 compatibility flags the substrate applies when building \
5386 the caixa; the canonical `Caixa::template` scaffold every \
5387 `feira init` emits carries `:edicao \"2026\"` verbatim and \
5388 every renderer-side fixture (`caixa-helm`, `caixa-flux`, \
5389 `caixa-mesh`) carries `edicao: Some(\"2026\".into())` by \
5390 construction, so an empty `Some(\"\")` silently lands as a \
5391 bare `(:edicao \"\")` line in the rendered `caixa.lisp` and \
5392 a future renderer-side consumer that folds it through \
5393 `Option::unwrap_or_else` will skip the fallback and pass the \
5394 empty edition through to the substrate's build-time edition \
5395 selector far from the source caixa.lisp; omit the slot \
5396 entirely to defer to the substrate's default edition, or \
5397 carry a canonical edition like `\"2026\"`)"
5398 )]
5399 EdicaoEmpty,
5400 #[error(
5401 ":edicao {edicao:?} is not a valid edition: {reason} (every \
5402 documented tatara-lisp edition is a 4-digit ASCII decimal \
5403 year — `\"2026\"` is the only edition currently minted; \
5404 future-introduced siblings will follow the same shape, peer \
5405 with Cargo's `[package] edition` grammar which every value \
5406 Cargo has ever accepted matches: `\"2015\"`, `\"2018\"`, \
5407 `\"2021\"`, `\"2024\"`. Without this gate the canonical \
5408 paste-from-doc footguns silently passed: a trailing space \
5409 (`\"2026 \"`) from a paste-from-doc, a CRLF (`\"2026\\n\"`) \
5410 from a paste-from-multiline-doc, a fullwidth-keyboard \
5411 look-alike (`\"2026\"`), a free-form non-year value \
5412 (`\"x\"`, `\"latest\"`, `\"nightly\"`), a leading non-digit \
5413 version-tag prefix (`\"v2026\"`, `\"e2026\"`), a \
5414 decimal-shaped pseudo-version (`\"2026.1\"`), or a \
5415 wrong-length numeric value (`\"26\"`, `\"202\"`, \
5416 `\"20260\"`) all landed as `(:edicao \"<garbage>\")` in the \
5417 rendered caixa.lisp and broke at the substrate's \
5418 build-time edition selector far from the source caixa.lisp; \
5419 omit the slot entirely to defer to the substrate's default \
5420 edition, or carry a canonical 4-digit ASCII decimal year \
5421 like `\"2026\"`)"
5422 )]
5423 EdicaoInvalid { edicao: String, reason: String },
5424}
5425
5426#[cfg(test)]
5427mod tests {
5428 use super::*;
5429
5430 #[test]
5431 fn template_round_trips() {
5432 let src = Caixa::template("demo");
5433 let c = Caixa::from_lisp(&src).expect("template must parse");
5434 assert_eq!(c.nome, "demo");
5435 assert_eq!(c.versao, "0.1.0");
5436 assert_eq!(c.kind, CaixaKind::Biblioteca);
5437 assert_eq!(c.bibliotecas, vec!["lib/demo.lisp".to_string()]);
5438 assert!(c.deps.is_empty());
5439 assert!(c.deps_dev.is_empty());
5440 }
5441
5442 #[test]
5443 fn register_populates_registry() {
5444 Caixa::register();
5445 let kws = tatara_lisp::domain::registered_keywords();
5446 assert!(kws.contains(&"defcaixa"));
5447 }
5448
5449 #[test]
5450 fn to_lisp_round_trips() {
5451 let src = Caixa::template("demo");
5452 let c1 = Caixa::from_lisp(&src).unwrap();
5453 let emitted = c1.to_lisp();
5454 let c2 = Caixa::from_lisp(&emitted).expect("emitted lisp parses back");
5455 assert_eq!(c1, c2);
5456 }
5457
5458 // ── DialetoEstrangeiro carries a single typed axis ────────────────────
5459 //
5460 // The compounding pin: the variant stores only the typed
5461 // [`crate::dialeto::CaixaDialeto`], and every user-facing byte-string
5462 // (canonical keyword, description, consumer) routes through the enum's
5463 // own accessors at Display time. Prior to that closure the variant
5464 // carried each accessor's return value as a stored `&'static str`
5465 // snapshot alongside `dialeto`; a caller could construct the variant
5466 // with a snapshot that drifted from what `dialeto`'s accessors would
5467 // return, and every downstream user-facing projection would silently
5468 // disagree with the classification. Storing only the axis makes the
5469 // drift structurally impossible.
5470
5471 #[test]
5472 fn dialeto_estrangeiro_variant_carries_only_the_typed_dialeto_axis() {
5473 // Single-field construction is the whole compounding shape — a
5474 // future re-introduction of a snapshot field (a `palavra_canonica:
5475 // &'static str`, a stored `descricao:`, a stored `consumidor:`)
5476 // would re-open the drift surface and this construction would fail
5477 // to compile with "missing field" until every snapshot was seeded
5478 // at the call site again. The compile-time guarantee is the
5479 // invariant; the assertion below only witnesses that the
5480 // construction is well-formed after the closure.
5481 let err = LeituraError::DialetoEstrangeiro {
5482 dialeto: crate::dialeto::CaixaDialeto::Molde,
5483 };
5484 assert!(matches!(
5485 err,
5486 LeituraError::DialetoEstrangeiro {
5487 dialeto: crate::dialeto::CaixaDialeto::Molde,
5488 }
5489 ));
5490 }
5491
5492 #[test]
5493 fn dialeto_estrangeiro_display_routes_through_typed_dialeto_accessors() {
5494 // For every foreign-dialect classification the variant surfaces —
5495 // [`crate::dialeto::CaixaDialeto::Molde`] and
5496 // [`crate::dialeto::CaixaDialeto::MoldePosicional`], the two
5497 // variants [`Caixa::from_lisp`] raises this error for — the
5498 // rendered [`std::fmt::Display`] byte-string must interpolate each
5499 // typed accessor's return verbatim. A future re-introduction of a
5500 // stored `&'static str` snapshot alongside `dialeto` that Display
5501 // read instead of the accessor would fail this pin as soon as the
5502 // two disagreed; a future accessor rebrand (a per-dialect
5503 // consumer rename, a canonical-keyword shift once the substrate
5504 // migration named in [`crate::dialeto`] completes) reaches every
5505 // consumer through one typed dispatch and this pin verifies the
5506 // display path is one of them.
5507 for d in [
5508 crate::dialeto::CaixaDialeto::Molde,
5509 crate::dialeto::CaixaDialeto::MoldePosicional,
5510 ] {
5511 let rendered = LeituraError::DialetoEstrangeiro { dialeto: d }.to_string();
5512 assert!(
5513 rendered.contains(d.palavra_canonica()),
5514 "Display must interpolate `dialeto.palavra_canonica()` \
5515 verbatim — a stored snapshot would silently drift from \
5516 the typed accessor. dialect: {d}, rendered: {rendered:?}"
5517 );
5518 assert!(
5519 rendered.contains(d.descricao()),
5520 "Display must interpolate `dialeto.descricao()` verbatim. \
5521 dialect: {d}, rendered: {rendered:?}"
5522 );
5523 assert!(
5524 rendered.contains(d.consumidor()),
5525 "Display must interpolate `dialeto.consumidor()` verbatim. \
5526 dialect: {d}, rendered: {rendered:?}"
5527 );
5528 }
5529 }
5530
5531 #[test]
5532 fn from_lisp_rejects_molde_dialect_via_typed_variant() {
5533 // The end-to-end pin the compounding closure defends: a
5534 // Molde-dialect source lands as [`LeituraError::DialetoEstrangeiro`]
5535 // carrying [`crate::dialeto::CaixaDialeto::Molde`], and the
5536 // rendered Display byte-string names the Molde accessors'
5537 // returns verbatim. Any future path that constructed the variant
5538 // with a mismatched snapshot (a stored `palavra_canonica:
5539 // "defcaixa"` on a `Molde` classification) would land Display
5540 // pointing at `defcaixa` while the typed axis said `Molde` — the
5541 // exact drift the closure removes.
5542 let src = r#"
5543 (defcaixa
5544 :name "x"
5545 :kind :Biblioteca
5546 :ecosystem :rust-single-crate
5547 :package {:name "x" :version "0.1.0"})
5548 "#;
5549 let err = Caixa::from_lisp(src).expect_err("Molde dialect must not parse as Pacote");
5550 match err {
5551 LeituraError::DialetoEstrangeiro { dialeto } => {
5552 assert_eq!(dialeto, crate::dialeto::CaixaDialeto::Molde);
5553 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5554 assert!(rendered.contains(dialeto.palavra_canonica()));
5555 assert!(rendered.contains(dialeto.consumidor()));
5556 assert!(rendered.contains(dialeto.descricao()));
5557 }
5558 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5559 }
5560 }
5561
5562 #[test]
5563 fn from_lisp_rejects_molde_posicional_dialect_via_typed_variant() {
5564 // Coverage pin for the [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5565 // arm of the [`Caixa::from_lisp`] foreign-dialect gate — the
5566 // positional-arity `defmolde` form written under a `(defcaixa …)`
5567 // head (`(defcaixa todoku-go :kind :Biblioteca :ecosystem :go
5568 // …)`). Pre-lift this arm rode the same `foreign =>` wildcard
5569 // the [`crate::dialeto::CaixaDialeto::Molde`] sibling arm rode,
5570 // so no test exercised the positional-arity path through
5571 // `Caixa::from_lisp` specifically; the sibling
5572 // [`from_lisp_rejects_molde_dialect_via_typed_variant`] only
5573 // covered [`crate::dialeto::CaixaDialeto::Molde`]. Post-lift the
5574 // two arms route through the lifted
5575 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5576 // typed predicate — the same predicate the pre-lift `foreign =>`
5577 // wildcard resolved to today — and this pin makes the
5578 // positional-arity arm's byte-shape at the gate explicit rather
5579 // than implied by wildcard-absorption. A future regression that
5580 // silently reordered [`crate::dialeto::CaixaDialeto::is_molde_family`]'s
5581 // arm-set (dropped [`crate::dialeto::CaixaDialeto::MoldePosicional`]
5582 // from the two-arity closure) would fail this pin at caixa-core
5583 // test time rather than surfacing far from the change as a
5584 // `caixa.lisp` carrying a `(defcaixa todoku-go :ecosystem :go
5585 // …)` silently parsing past the derive.
5586 let src = r#"
5587 (defcaixa todoku-go
5588 :kind :Biblioteca
5589 :ecosystem :go
5590 :package {:name "todoku-go" :version "0.3.0"})
5591 "#;
5592 let err =
5593 Caixa::from_lisp(src).expect_err("MoldePosicional dialect must not parse as Pacote");
5594 match err {
5595 LeituraError::DialetoEstrangeiro { dialeto } => {
5596 assert_eq!(
5597 dialeto,
5598 crate::dialeto::CaixaDialeto::MoldePosicional,
5599 "DialetoEstrangeiro must carry the MoldePosicional \
5600 variant verbatim — the positional-arity `defmolde` \
5601 form under a `(defcaixa …)` head is the \
5602 `MoldePosicional` arm's canonical byte-shape"
5603 );
5604 let rendered = LeituraError::DialetoEstrangeiro { dialeto }.to_string();
5605 assert!(
5606 rendered.contains(dialeto.palavra_canonica()),
5607 "Display must interpolate `dialeto.palavra_canonica()` \
5608 verbatim on the MoldePosicional arm; rendered: \
5609 {rendered:?}"
5610 );
5611 assert!(
5612 rendered.contains(dialeto.consumidor()),
5613 "Display must interpolate `dialeto.consumidor()` \
5614 verbatim on the MoldePosicional arm; rendered: \
5615 {rendered:?}"
5616 );
5617 assert!(
5618 rendered.contains(dialeto.descricao()),
5619 "Display must interpolate `dialeto.descricao()` \
5620 verbatim on the MoldePosicional arm; rendered: \
5621 {rendered:?}"
5622 );
5623 }
5624 other => panic!("expected DialetoEstrangeiro, got {other:?}"),
5625 }
5626 }
5627
5628 #[test]
5629 fn from_lisp_dialect_gate_dispatches_through_caixa_dialeto_is_molde_family_predicate() {
5630 // Load-bearing byte-parity pin: for every arm in
5631 // [`crate::dialeto::CaixaDialeto::ALL`], the
5632 // [`Caixa::from_lisp`] foreign-dialect gate's DialetoEstrangeiro
5633 // partition must agree with the lifted
5634 // [`crate::dialeto::CaixaDialeto::is_molde_family`] (e9d2315)
5635 // typed predicate — i.e. from_lisp raises
5636 // [`LeituraError::DialetoEstrangeiro`] carrying `d` iff
5637 // `d.is_molde_family()` returns `true`, and does NOT raise
5638 // [`LeituraError::DialetoEstrangeiro`] on any arm where the
5639 // predicate returns `false` (the arm's source falls through to
5640 // the derive — parses cleanly on
5641 // [`crate::dialeto::CaixaDialeto::Pacote`], surfaces a
5642 // [`LeituraError::Leitura`] on
5643 // [`crate::dialeto::CaixaDialeto::Desconhecido`]).
5644 //
5645 // Pre-lift the gate hand-rolled a three-arm match
5646 // (`Pacote => {}`, `Desconhecido => {}`, `foreign => Err(…)`)
5647 // whose `foreign =>` wildcard expressed no compile-time link
5648 // back to the substrate primitive's arm-family; a future fifth
5649 // dialect the [`crate::dialeto`] module doc's "third dialect"
5650 // hazard actualises would fall silently onto the wildcard
5651 // regardless of whether it belonged to the `defmolde` family or
5652 // to a distinct `defcaixa`-family. Post-lift the partition
5653 // resolves through
5654 // [`crate::dialeto::CaixaDialeto::is_molde_family`]'s single
5655 // typed dispatch, and this pin refuses any future regression
5656 // that silently split the from_lisp partition from the typed
5657 // predicate — the two paths now migrate as one on any future
5658 // arm addition.
5659 //
5660 // Sibling in shape to the peer
5661 // [`crate::dialeto::tests::caixa_dialeto_is_molde_family_agrees_with_palavra_canonica_defmolde_projection`]
5662 // (e9d2315) that pins the same byte-parity between
5663 // [`crate::dialeto::CaixaDialeto::is_molde_family`] and the
5664 // sibling [`crate::dialeto::CaixaDialeto::palavra_canonica`]
5665 // `== "defmolde"` classifier — extends the discipline from the
5666 // two paths within the [`crate::dialeto`] primitive onto the
5667 // third external consumer of the `defmolde`-family partition
5668 // (the [`Caixa::from_lisp`] gate that raises
5669 // [`LeituraError::DialetoEstrangeiro`]).
5670 let fixtures: &[(crate::dialeto::CaixaDialeto, &str)] = &[
5671 (
5672 crate::dialeto::CaixaDialeto::Pacote,
5673 r#"
5674 (defcaixa
5675 :nome "checkout"
5676 :versao "0.1.0"
5677 :kind Biblioteca
5678 :edicao "2026"
5679 :descricao "canonical Pacote source"
5680 :autores ()
5681 :etiquetas ()
5682 :deps ()
5683 :deps-dev ()
5684 :bibliotecas ("lib/checkout.lisp"))
5685 "#,
5686 ),
5687 (
5688 crate::dialeto::CaixaDialeto::Molde,
5689 r#"
5690 (defcaixa
5691 :name "base64"
5692 :kind :Biblioteca
5693 :ecosystem :rust-single-crate
5694 :package {:name "base64" :version "0.22.1"}
5695 :workflows [:auto-release])
5696 "#,
5697 ),
5698 (
5699 crate::dialeto::CaixaDialeto::MoldePosicional,
5700 r#"
5701 (defcaixa todoku-go
5702 :kind :Biblioteca
5703 :ecosystem :go
5704 :package {:name "todoku-go" :version "0.3.0"})
5705 "#,
5706 ),
5707 (
5708 crate::dialeto::CaixaDialeto::Desconhecido,
5709 r#"(defcaixa :licenca "MIT")"#,
5710 ),
5711 ];
5712
5713 // Coverage: every arm in [`crate::dialeto::CaixaDialeto::ALL`]
5714 // must appear in the fixture table so the pin's arm-set stays
5715 // synchronised with the enum's arm-set. Fails at test time if a
5716 // future fifth arm added to [`crate::dialeto::CaixaDialeto`]
5717 // (with a corresponding `is_molde_family` return) forgot to
5718 // extend this fixture table with a canonical source for the new
5719 // arm — the pin cannot cover an arm it has no source for.
5720 for &expected in crate::dialeto::CaixaDialeto::ALL {
5721 assert!(
5722 fixtures.iter().any(|(d, _)| *d == expected),
5723 "fixture table must carry a canonical source for every \
5724 CaixaDialeto arm; missing: {expected:?}"
5725 );
5726 }
5727
5728 for &(expected_dialect, src) in fixtures {
5729 let classified = crate::dialeto::classify(src.trim()).unwrap_or_else(|err| {
5730 panic!(
5731 "fixture source for {expected_dialect:?} must classify \
5732 cleanly, got err: {err:?}"
5733 )
5734 });
5735 assert_eq!(
5736 classified, expected_dialect,
5737 "fixture source for {expected_dialect:?} must classify as \
5738 {expected_dialect:?} (drift here defeats the byte-parity \
5739 pin below — a source labelled for one arm but classifying \
5740 as another would silently satisfy or violate the pin for \
5741 the wrong reason)"
5742 );
5743
5744 let outcome = Caixa::from_lisp(src);
5745 match (expected_dialect.is_molde_family(), &outcome) {
5746 (true, Err(LeituraError::DialetoEstrangeiro { dialeto })) => {
5747 assert_eq!(
5748 *dialeto, expected_dialect,
5749 "DialetoEstrangeiro must carry the same typed arm \
5750 the classifier returned — a drift here would let \
5751 from_lisp raise the error while pointing at the \
5752 wrong dialect (e.g. rejecting a \
5753 MoldePosicional source as Molde). arm: \
5754 {expected_dialect:?}"
5755 );
5756 }
5757 (true, other) => panic!(
5758 "arm {expected_dialect:?} has is_molde_family() = true \
5759 so from_lisp must raise DialetoEstrangeiro carrying \
5760 {expected_dialect:?}; got: {other:?}"
5761 ),
5762 (false, Err(LeituraError::DialetoEstrangeiro { dialeto })) => panic!(
5763 "arm {expected_dialect:?} has is_molde_family() = false \
5764 so from_lisp must NOT raise DialetoEstrangeiro; got \
5765 one carrying: {dialeto:?}. This means the typed \
5766 predicate and the from_lisp partition disagree on \
5767 this arm — exactly the drift this pin refuses."
5768 ),
5769 (false, _) => {
5770 // A non-molde arm's source falls through to the
5771 // derive: Pacote sources parse to Ok(_); Desconhecido
5772 // sources surface as LeituraError::Leitura from the
5773 // derive's own unknown-keyword rejection. Either
5774 // shape is acceptable here — the pin's promise is
5775 // narrower: "no DialetoEstrangeiro on
5776 // is_molde_family() == false".
5777 }
5778 }
5779 }
5780 }
5781
5782 // ── M2 typed-substrate slot tests (limits, behavior, upgrade-from, supervisor) ──
5783
5784 #[test]
5785 fn limits_round_trip_via_json() {
5786 use crate::LimitsSpec;
5787 use std::time::Duration;
5788 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5789 c.limits = Some(LimitsSpec {
5790 memory: Some(64 * 1024 * 1024),
5791 fuel: Some(1_000_000),
5792 wall_clock: Some(Duration::from_secs(30)),
5793 cpu: Some(500),
5794 });
5795 let json = serde_json::to_string(&c).unwrap();
5796 assert!(json.contains("\"limits\""));
5797 assert!(json.contains("\"64MiB\""));
5798 assert!(json.contains("\"30s\""));
5799 assert!(json.contains("\"500m\""));
5800 let back: Caixa = serde_json::from_str(&json).unwrap();
5801 assert_eq!(c.limits, back.limits);
5802 }
5803
5804 #[test]
5805 fn behavior_round_trip_via_json() {
5806 use crate::BehaviorSpec;
5807 use std::path::PathBuf;
5808 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5809 c.behavior = Some(BehaviorSpec {
5810 on_init: Some(PathBuf::from("lib/init.lisp")),
5811 on_call: Some(PathBuf::from("lib/handlers.lisp")),
5812 ..Default::default()
5813 });
5814 let json = serde_json::to_string(&c).unwrap();
5815 let back: Caixa = serde_json::from_str(&json).unwrap();
5816 assert_eq!(c.behavior, back.behavior);
5817 }
5818
5819 #[test]
5820 fn upgrade_from_round_trip_via_json() {
5821 use crate::{UpgradeFromEntry, UpgradeInstruction};
5822 use std::path::PathBuf;
5823 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5824 c.upgrade_from = vec![UpgradeFromEntry {
5825 from: "0.1.0".into(),
5826 instructions: vec![
5827 UpgradeInstruction::LoadModule {
5828 module: "demo".into(),
5829 },
5830 UpgradeInstruction::StateChange {
5831 script: PathBuf::from("lib/migrations/v01-to-v02.lisp"),
5832 },
5833 UpgradeInstruction::SoftPurge {
5834 module: "demo-old".into(),
5835 },
5836 ],
5837 }];
5838 let json = serde_json::to_string(&c).unwrap();
5839 let back: Caixa = serde_json::from_str(&json).unwrap();
5840 assert_eq!(c.upgrade_from, back.upgrade_from);
5841 }
5842
5843 #[test]
5844 fn supervisor_view_returns_typed_shape() {
5845 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
5846 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
5847 c.kind = CaixaKind::Supervisor;
5848 c.bibliotecas.clear();
5849 c.estrategia = Some(RestartStrategy::OneForOne);
5850 c.max_restarts = Some(5);
5851 c.restart_window = Some("60s".into());
5852 c.children = vec![ChildSpec {
5853 caixa: "worker".into(),
5854 versao: "^0.1".into(),
5855 restart: RestartPolicy::Permanent,
5856 }];
5857 let view = c.supervisor_view().expect("Supervisor kind has a view");
5858 assert_eq!(view.estrategia, RestartStrategy::OneForOne);
5859 assert_eq!(view.max_restarts, 5);
5860 assert_eq!(
5861 view.restart_window,
5862 Some(std::time::Duration::from_secs(60))
5863 );
5864 assert_eq!(view.children.len(), 1);
5865 view.validate().unwrap();
5866 }
5867
5868 #[test]
5869 fn supervisor_view_none_for_non_supervisor_kinds() {
5870 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5871 assert!(c.supervisor_view().is_none());
5872 }
5873
5874 #[test]
5875 fn declared_mesh_slots_empty_for_bare_caixa() {
5876 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5877 assert!(c.declared_mesh_slots().is_empty());
5878 }
5879
5880 #[test]
5881 fn declared_mesh_slots_reports_only_set_slots_in_canonical_order() {
5882 use crate::{Entrada, Membro};
5883 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5884 // Set a non-adjacent pair (:membros + :entrada) to pin that the
5885 // canonical declaration order is preserved regardless of which
5886 // subset is populated.
5887 c.membros = vec![Membro {
5888 caixa: "a".into(),
5889 versao: "^0.1".into(),
5890 }];
5891 c.entrada = Some(Entrada {
5892 host: "x.example.com".into(),
5893 para: "a".into(),
5894 paths: vec![],
5895 port: 8080,
5896 });
5897 assert_eq!(
5898 c.declared_mesh_slots(),
5899 vec![
5900 crate::render::M3_AUTHOR_KEY_MEMBROS,
5901 crate::render::M3_AUTHOR_KEY_ENTRADA,
5902 ]
5903 );
5904 }
5905
5906 #[test]
5907 fn m3_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
5908 // Scalar-value pin: the five author-facing kebab-case labels the
5909 // `(defcaixa … :<slot> (…))` surface admits on the M3 top-level
5910 // mesh slot axis, one arm per typed slot. Mirrors the peer
5911 // scalar-value pin the sibling
5912 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
5913 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
5914 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] M2 top-level slot consts
5915 // carry (f49c8b0), so both altitudes of the typed-slot algebra
5916 // (per-Servico M2 + per-Aplicacao M3) share the same
5917 // "one canonical byte-string per arm" discipline. A future
5918 // rebrand (`:membros` → `:members`, `:contratos` → `:contracts`,
5919 // `:politicas` → `:policies`, `:placement` → `:distribution`,
5920 // `:entrada` → `:ingress`) lands as an edit to exactly one const,
5921 // and every consumer that reaches for the label picks it up at
5922 // build time rather than at runtime as a downstream mismatch.
5923 assert_eq!(crate::render::M3_AUTHOR_KEY_MEMBROS, ":membros");
5924 assert_eq!(crate::render::M3_AUTHOR_KEY_CONTRATOS, ":contratos");
5925 assert_eq!(crate::render::M3_AUTHOR_KEY_POLITICAS, ":politicas");
5926 assert_eq!(crate::render::M3_AUTHOR_KEY_PLACEMENT, ":placement");
5927 assert_eq!(crate::render::M3_AUTHOR_KEY_ENTRADA, ":entrada");
5928 }
5929
5930 #[test]
5931 fn declared_mesh_slots_route_through_lifted_m3_author_key_consts() {
5932 // Production-through-const pin: the five per-arm labels the
5933 // [`Caixa::declared_mesh_slots`] tagger pushes onto its return
5934 // `Vec` route through the lifted
5935 // [`crate::M3_AUTHOR_KEY_MEMBROS`] /
5936 // [`crate::M3_AUTHOR_KEY_CONTRATOS`] /
5937 // [`crate::M3_AUTHOR_KEY_POLITICAS`] /
5938 // [`crate::M3_AUTHOR_KEY_PLACEMENT`] /
5939 // [`crate::M3_AUTHOR_KEY_ENTRADA`] consts, in canonical
5940 // declaration order. A future re-order or drift at the tagger
5941 // (a rename that reaches the tagger but not the const, or vice
5942 // versa) surfaces here at build time rather than at runtime as
5943 // a [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
5944 // `slots: <stale-kebab-case>` diagnostic far from the rename's
5945 // commit. Mirror of the peer
5946 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
5947 // pin (f49c8b0) on the sibling per-Servico M2 top-level slot
5948 // axis.
5949 use crate::{Entrada, Membro, MeshPolicy, Placement, PlacementStrategy, WitContract};
5950 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5951 c.membros = vec![Membro {
5952 caixa: "a".into(),
5953 versao: "^0.1".into(),
5954 }];
5955 c.contratos = vec![WitContract {
5956 de: "a".into(),
5957 para: "a".into(),
5958 wit: "wasi:http/proxy".into(),
5959 endpoint: Some("/x".into()),
5960 subject: None,
5961 slot: None,
5962 }];
5963 c.politicas = Some(MeshPolicy::default());
5964 c.placement = Some(Placement {
5965 estrategia: PlacementStrategy::Replicated,
5966 clusters: vec!["rio".into()],
5967 affinity: None,
5968 shard_key: None,
5969 });
5970 c.entrada = Some(Entrada {
5971 host: "x.example.com".into(),
5972 para: "a".into(),
5973 paths: vec![],
5974 port: 8080,
5975 });
5976 assert_eq!(
5977 c.declared_mesh_slots(),
5978 vec![
5979 crate::render::M3_AUTHOR_KEY_MEMBROS,
5980 crate::render::M3_AUTHOR_KEY_CONTRATOS,
5981 crate::render::M3_AUTHOR_KEY_POLITICAS,
5982 crate::render::M3_AUTHOR_KEY_PLACEMENT,
5983 crate::render::M3_AUTHOR_KEY_ENTRADA,
5984 ]
5985 );
5986 }
5987
5988 #[test]
5989 fn declared_supervisor_slots_empty_for_bare_caixa() {
5990 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5991 assert!(c.declared_supervisor_slots().is_empty());
5992 }
5993
5994 #[test]
5995 fn declared_supervisor_slots_reports_only_set_slots_in_canonical_order() {
5996 use crate::RestartStrategy;
5997 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
5998 // Set a non-adjacent pair (:estrategia + :restart-window) to pin
5999 // that the canonical declaration order is preserved regardless
6000 // of which subset is populated.
6001 c.estrategia = Some(RestartStrategy::OneForOne);
6002 c.restart_window = Some("60s".into());
6003 assert_eq!(
6004 c.declared_supervisor_slots(),
6005 vec![
6006 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6007 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6008 ]
6009 );
6010 }
6011
6012 #[test]
6013 fn supervisor_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6014 // Scalar-value pin: the four author-facing kebab-case labels the
6015 // `(defcaixa … :<slot> (…))` surface admits on the Supervisor
6016 // supervision-tree slot axis, one arm per typed slot. Mirrors the
6017 // peer scalar-value pins the sibling
6018 // [`crate::render::M2_AUTHOR_KEY_LIMITS`] /
6019 // [`crate::render::M2_AUTHOR_KEY_BEHAVIOR`] /
6020 // [`crate::render::M2_AUTHOR_KEY_UPGRADE_FROM`] top-level M2 slot
6021 // consts and [`crate::render::M3_AUTHOR_KEY_MEMBROS`] etc.
6022 // top-level M3 slot consts carry, so all three kind-scoped
6023 // typed-slot-family author-facing-label axes route through one
6024 // canonical per-arm declaration. A future rebrand
6025 // (`:estrategia` → `:strategy` for English uniformity,
6026 // `:max-restarts` → `:max-intensity` matching Erlang/OTP's
6027 // `MaxIntensity` name, `:restart-window` → `:period` matching
6028 // OTP's `Period` name, `:children` → `:workers` matching Elixir
6029 // idiom) lands as an edit to exactly one const, and every
6030 // consumer that reaches for the label picks it up at build time
6031 // rather than at runtime as a downstream mismatch.
6032 assert_eq!(
6033 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6034 ":estrategia"
6035 );
6036 assert_eq!(
6037 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6038 ":max-restarts"
6039 );
6040 assert_eq!(
6041 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6042 ":restart-window"
6043 );
6044 assert_eq!(crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN, ":children");
6045 }
6046
6047 #[test]
6048 fn declared_supervisor_slots_route_through_lifted_supervisor_author_key_consts() {
6049 // Production-through-const pin: the four per-arm labels the
6050 // [`Caixa::declared_supervisor_slots`] tagger pushes onto its
6051 // return `Vec` route through the lifted
6052 // [`crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA`] /
6053 // [`crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS`] /
6054 // [`crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW`] /
6055 // [`crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN`] consts, in
6056 // canonical declaration order. A future re-order or drift at the
6057 // tagger (a rename that reaches the tagger but not the const, or
6058 // vice versa) surfaces here at build time rather than at runtime
6059 // as a [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
6060 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6061 // commit. Mirror of the peer
6062 // [`declared_servico_slots_route_through_lifted_m2_author_key_consts`]
6063 // (f49c8b0) and
6064 // [`declared_mesh_slots_route_through_lifted_m3_author_key_consts`]
6065 // (882f498) pins on the sibling M2 / M3 top-level slot axes.
6066 use crate::{ChildSpec, RestartPolicy, RestartStrategy};
6067 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6068 c.estrategia = Some(RestartStrategy::OneForOne);
6069 c.max_restarts = Some(5);
6070 c.restart_window = Some("60s".into());
6071 c.children = vec![ChildSpec {
6072 caixa: "worker".into(),
6073 versao: "^0.1".into(),
6074 restart: RestartPolicy::Permanent,
6075 }];
6076 assert_eq!(
6077 c.declared_supervisor_slots(),
6078 vec![
6079 crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA,
6080 crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS,
6081 crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW,
6082 crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN,
6083 ]
6084 );
6085 }
6086
6087 #[test]
6088 fn declared_servico_slots_empty_for_bare_caixa() {
6089 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6090 assert!(c.declared_servico_slots().is_empty());
6091 }
6092
6093 #[test]
6094 fn declared_servico_slots_reports_only_set_slots_in_canonical_order() {
6095 use crate::{UpgradeFromEntry, UpgradeInstruction};
6096 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6097 // Set a non-adjacent pair (:limits + :upgrade-from) to pin that
6098 // the canonical declaration order is preserved regardless of
6099 // which subset is populated.
6100 c.limits = Some(crate::LimitsSpec {
6101 fuel: Some(1_000_000),
6102 ..Default::default()
6103 });
6104 c.upgrade_from = vec![UpgradeFromEntry {
6105 from: "0.1.0".into(),
6106 instructions: vec![UpgradeInstruction::Restart],
6107 }];
6108 assert_eq!(
6109 c.declared_servico_slots(),
6110 vec![
6111 crate::render::M2_AUTHOR_KEY_LIMITS,
6112 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6113 ]
6114 );
6115 }
6116
6117 #[test]
6118 fn m2_top_level_author_key_consts_pin_canonical_kebab_case_labels() {
6119 // Scalar-value pin: the three author-facing kebab-case labels
6120 // the `(defcaixa … :<slot> (…))` surface admits on the M2
6121 // top-level slot axis, one arm per typed slot. Mirrors the peer
6122 // scalar-value pin the sibling renderer-side
6123 // [`crate::M2_KEY_LIMITS`] / [`crate::M2_KEY_BEHAVIOR`] /
6124 // [`crate::M2_KEY_UPGRADE_FROM`] camelCase overlay-container
6125 // consts carry, so both halves of the M2 top-level slot dual
6126 // axis (author-facing kebab-case label + renderer-side
6127 // camelCase overlay-container wire key) route through one
6128 // canonical per-arm declaration. A future rebrand
6129 // (`:limits` → `:sandbox` matching Lunatic per-process
6130 // terminology INSPIRATIONS §III.1, `:behavior` → `:gen-server`
6131 // matching Erlang's verbatim name, `:upgrade-from` → `:appup`
6132 // matching Erlang's verbatim appup name) lands as an edit to
6133 // exactly one const, and every consumer that reaches for the
6134 // label picks it up at build time rather than at runtime as a
6135 // downstream mismatch.
6136 assert_eq!(crate::render::M2_AUTHOR_KEY_LIMITS, ":limits");
6137 assert_eq!(crate::render::M2_AUTHOR_KEY_BEHAVIOR, ":behavior");
6138 assert_eq!(crate::render::M2_AUTHOR_KEY_UPGRADE_FROM, ":upgrade-from");
6139 }
6140
6141 #[test]
6142 fn declared_servico_slots_route_through_lifted_m2_author_key_consts() {
6143 // Production-through-const pin: the three per-arm labels the
6144 // [`Caixa::declared_servico_slots`] tagger pushes onto its
6145 // return `Vec` route through the lifted
6146 // [`crate::M2_AUTHOR_KEY_LIMITS`] /
6147 // [`crate::M2_AUTHOR_KEY_BEHAVIOR`] /
6148 // [`crate::M2_AUTHOR_KEY_UPGRADE_FROM`] consts, in canonical
6149 // declaration order. A future re-order or drift at the tagger
6150 // (a rename that reaches the tagger but not the const, or vice
6151 // versa) surfaces here at build time rather than at runtime as
6152 // a [`crate::LayoutError::ServicoSlotsOnNonServico`]
6153 // `slots: <stale-kebab-case>` diagnostic far from the rename's
6154 // commit. Mirror of the peer
6155 // [`crate::behavior::BehaviorSpec::declared_slots`] production
6156 // tagger pin (889dc18) on the sibling per-callback axis.
6157 use crate::{BehaviorSpec, UpgradeFromEntry, UpgradeInstruction};
6158 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6159 c.limits = Some(crate::LimitsSpec {
6160 fuel: Some(1_000_000),
6161 ..Default::default()
6162 });
6163 c.behavior = Some(BehaviorSpec {
6164 on_init: Some(PathBuf::from("lib/init.lisp")),
6165 ..Default::default()
6166 });
6167 c.upgrade_from = vec![UpgradeFromEntry {
6168 from: "0.1.0".into(),
6169 instructions: vec![UpgradeInstruction::Restart],
6170 }];
6171 assert_eq!(
6172 c.declared_servico_slots(),
6173 vec![
6174 crate::render::M2_AUTHOR_KEY_LIMITS,
6175 crate::render::M2_AUTHOR_KEY_BEHAVIOR,
6176 crate::render::M2_AUTHOR_KEY_UPGRADE_FROM,
6177 ]
6178 );
6179 }
6180
6181 #[test]
6182 fn existing_manifests_unaffected_by_new_optional_slots() {
6183 // Regression test: a caixa.lisp authored before M2 typed slots
6184 // should still parse + serialize cleanly. The bare `defcaixa`
6185 // emitted by `Caixa::template` has none of the new fields.
6186 let src = Caixa::template("legacy");
6187 let c = Caixa::from_lisp(&src).unwrap();
6188 assert!(c.limits.is_none());
6189 assert!(c.behavior.is_none());
6190 assert!(c.upgrade_from.is_empty());
6191 assert!(c.estrategia.is_none());
6192 assert!(c.children.is_empty());
6193
6194 // And to_lisp emits a manifest with the new slots in the
6195 // empty/default state — round-trippable.
6196 let emitted = c.to_lisp();
6197 let back = Caixa::from_lisp(&emitted).unwrap();
6198 assert_eq!(c, back);
6199 }
6200
6201 #[test]
6202 fn validate_deps_accepts_canonical_caixa() {
6203 // Positive control: the bare template — zero deps, zero
6204 // deps_dev — passes the gate trivially. A future axis added to
6205 // `Dep::validate` mustn't regress an empty-deps caixa to a
6206 // build error.
6207 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6208 c.validate_deps().unwrap();
6209 }
6210
6211 #[test]
6212 fn validate_deps_rejects_invalid_versao_in_deps() {
6213 // Fail-before-pass-after pin: a malformed `:deps :versao`
6214 // surfaces at validate_deps() time, not at lacre-resolve time.
6215 // Mirrors `rejects_invalid_membro_versao_requirement` and
6216 // `validate_rejects_invalid_child_versao_requirement` on the
6217 // other two `:versao` axes.
6218 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6219 c.deps = vec![Dep::simple("caixa-teia", "^bad-version")];
6220 let err = c.validate_deps().unwrap_err();
6221 assert!(
6222 matches!(
6223 err,
6224 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6225 if nome == "caixa-teia" && versao == "^bad-version"
6226 ),
6227 "got {err:?}"
6228 );
6229 }
6230
6231 #[test]
6232 fn validate_deps_rejects_invalid_versao_in_deps_dev() {
6233 // Parity pin: `:deps-dev` must run through the same per-entry
6234 // validator as `:deps` — a typo in either axis surfaces the
6235 // same diagnostic. Without this leg, `:deps-dev` would be a
6236 // second-class citizen of the typed surface and an author
6237 // could land a build that passes validate_deps but fails at
6238 // `feira lock`-time when the dev-dep is resolved for a test
6239 // build.
6240 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6241 c.deps_dev = vec![Dep::simple("tatara-check", "^^0.1")];
6242 let err = c.validate_deps().unwrap_err();
6243 assert!(
6244 matches!(
6245 err,
6246 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6247 if nome == "tatara-check" && versao == "^^0.1"
6248 ),
6249 "got {err:?}"
6250 );
6251 }
6252
6253 #[test]
6254 fn validate_deps_runs_deps_before_deps_dev() {
6255 // Order pin: when both lists carry typos, the `:deps`
6256 // diagnostic surfaces first. The author's mental model is
6257 // "runtime deps are load-bearing; dev deps are scaffolding";
6258 // surfacing the runtime axis first matches that hierarchy.
6259 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6260 c.deps = vec![Dep::simple("runtime-dep", "^bad-runtime")];
6261 c.deps_dev = vec![Dep::simple("dev-dep", "^bad-dev")];
6262 let err = c.validate_deps().unwrap_err();
6263 assert!(
6264 matches!(
6265 err,
6266 crate::dep::DepError::VersaoInvalid { ref nome, .. }
6267 if nome == "runtime-dep"
6268 ),
6269 "expected `:deps` typo to surface first, got {err:?}"
6270 );
6271 }
6272
6273 #[test]
6274 fn validate_deps_accepts_canonical_versao_forms_in_both_lists() {
6275 // Positive control sweep across both lists. Pin every
6276 // canonical Cargo-shaped form so a future tightening of the
6277 // accepted set surfaces here as a test failure (parity with
6278 // `accepts_canonical_membro_versao_forms` and
6279 // `validate_accepts_canonical_child_versao_forms`).
6280 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6281 c.deps = vec![
6282 Dep::simple("caret", "^0.1"),
6283 Dep::simple("tilde", "~0.1.2"),
6284 Dep::simple("exact", "0.1.0"),
6285 Dep::simple("wildcard", "*"),
6286 Dep::simple("multi-range", ">=0.1, <2"),
6287 ];
6288 c.deps_dev = vec![
6289 Dep::simple("dev-caret", "^0.1"),
6290 Dep::simple("dev-wildcard", "*"),
6291 ];
6292 c.validate_deps().unwrap();
6293 }
6294
6295 #[test]
6296 fn validate_deps_diagnostic_carries_offending_dep() {
6297 // Diagnostic-shape pin: the error names the offending entry's
6298 // `:nome` + `:versao` verbatim and carries a non-empty
6299 // `reason` from `semver::VersionReq::parse`, so a `feira lint`
6300 // run can render the diagnostic without re-parsing.
6301 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6302 c.deps = vec![Dep::simple("caixa-teia", "not-a-req")];
6303 let err = c.validate_deps().unwrap_err();
6304 let crate::dep::DepError::VersaoInvalid {
6305 nome,
6306 versao,
6307 reason,
6308 } = err
6309 else {
6310 panic!("expected VersaoInvalid, got other variant");
6311 };
6312 assert_eq!(nome, "caixa-teia");
6313 assert_eq!(versao, "not-a-req");
6314 assert!(
6315 !reason.is_empty(),
6316 "VersaoInvalid `reason` must carry the parser's wording verbatim"
6317 );
6318 }
6319
6320 #[test]
6321 fn validate_deps_rejects_ambiguous_fonte_in_deps_dev() {
6322 // Cross-axis pin: `validate_deps` walks both :deps and
6323 // :deps-dev through `Dep::validate`, and the new fonte gate
6324 // (`:tag` + `:branch` both set — the canonical "pin drift"
6325 // footgun) must surface from the :deps-dev arm with the
6326 // offending entry's :nome named. Pin the :deps-dev arm
6327 // explicitly so a future shortcut that only walks :deps
6328 // surfaces here as a regression.
6329 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6330 c.deps_dev = vec![Dep {
6331 nome: "dev-only".into(),
6332 versao: "^0.1".into(),
6333 fonte: Some(crate::DepSource::Git {
6334 repo: "github:p/x".into(),
6335 tag: Some("v1".into()),
6336 rev: None,
6337 branch: Some("main".into()),
6338 }),
6339 opcional: false,
6340 caracteristicas: vec![],
6341 }];
6342 let err = c.validate_deps().unwrap_err();
6343 let crate::dep::DepError::FontePinAmbiguous { nome, pins } = err else {
6344 panic!("expected FontePinAmbiguous from :deps-dev walk");
6345 };
6346 assert_eq!(nome, "dev-only");
6347 assert!(pins.contains(":tag") && pins.contains(":branch"));
6348 }
6349
6350 #[test]
6351 fn validate_deps_rejects_empty_repo_in_deps() {
6352 // Parity pin on the :deps arm: an empty :repo on the runtime
6353 // deps list surfaces the same FonteRepoEmpty diagnostic the
6354 // dep.rs per-entry tests pin, naming the offending entry.
6355 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6356 c.deps = vec![Dep {
6357 nome: "runtime".into(),
6358 versao: "^0.1".into(),
6359 fonte: Some(crate::DepSource::Git {
6360 repo: String::new(),
6361 tag: Some("v1".into()),
6362 rev: None,
6363 branch: None,
6364 }),
6365 opcional: false,
6366 caracteristicas: vec![],
6367 }];
6368 let err = c.validate_deps().unwrap_err();
6369 assert!(
6370 matches!(
6371 err,
6372 crate::dep::DepError::FonteRepoEmpty { ref nome }
6373 if nome == "runtime"
6374 ),
6375 "got {err:?}"
6376 );
6377 }
6378
6379 // ── validate_deps: within-list :nome set-not-multiset gate ─────────
6380
6381 #[test]
6382 fn validate_deps_rejects_duplicate_nome_in_deps() {
6383 // Fail-before-pass-after pin: two `:deps` entries naming the same
6384 // caixa carry two `:versao` / `:fonte` / feature triples that the
6385 // caixa-resolver's lacre pipeline collapses (the second silently
6386 // overwrites the first at `concrete_versao`-resolve time). The
6387 // gate surfaces the duplicate at validate-time, naming the
6388 // offending caixa + the list, before the resolver-side silent
6389 // drop. Mirrors the peer typed-graph duplicate gates
6390 // (`DuplicateChildCaixa`, `MembroDuplicate`, `DuplicateFrom`, …).
6391 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6392 c.deps = vec![
6393 Dep::simple("caixa-teia", "^0.1"),
6394 Dep::simple("caixa-teia", "^0.2"),
6395 ];
6396 let err = c.validate_deps().unwrap_err();
6397 assert!(
6398 matches!(
6399 err,
6400 crate::dep::DepError::DuplicateNome { ref nome, list }
6401 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6402 ),
6403 "got {err:?}"
6404 );
6405 }
6406
6407 #[test]
6408 fn validate_deps_rejects_duplicate_nome_in_deps_dev() {
6409 // Parity pin: `:deps-dev` runs through the same per-list
6410 // duplicate check as `:deps` — neither axis is a second-class
6411 // citizen of the set-not-multiset discipline.
6412 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6413 c.deps_dev = vec![
6414 Dep::simple("tatara-check", "*"),
6415 Dep::simple("tatara-check", "^0.1"),
6416 ];
6417 let err = c.validate_deps().unwrap_err();
6418 assert!(
6419 matches!(
6420 err,
6421 crate::dep::DepError::DuplicateNome { ref nome, list }
6422 if nome == "tatara-check" && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
6423 ),
6424 "got {err:?}"
6425 );
6426 }
6427
6428 #[test]
6429 fn validate_deps_accepts_cross_list_same_nome() {
6430 // The Cargo `[dependencies]` + `[dev-dependencies]` override
6431 // convention is preserved: a name appearing in *both* lists is
6432 // valid (the dev-pin overrides at test/dev time). Only
6433 // within-list duplicates are structurally incoherent — pin the
6434 // permissive cross-list semantics so a future shortcut that
6435 // collapses the two seen-sets into one surfaces here as a test
6436 // failure.
6437 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6438 c.deps = vec![Dep::simple("caixa-teia", "^0.1")];
6439 c.deps_dev = vec![Dep::simple("caixa-teia", "^0.2")];
6440 c.validate_deps().unwrap();
6441 }
6442
6443 #[test]
6444 fn validate_deps_accepts_distinct_nome_in_both_lists() {
6445 // Positive control: distinct names within each list pass — the
6446 // gate's identity element on the canonical authoring shape.
6447 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6448 c.deps = vec![
6449 Dep::simple("caixa-teia", "^0.1"),
6450 Dep::simple("pleme-mesh", "*"),
6451 ];
6452 c.deps_dev = vec![
6453 Dep::simple("tatara-check", "*"),
6454 Dep::simple("dev-shim", "^0.1"),
6455 ];
6456 c.validate_deps().unwrap();
6457 }
6458
6459 #[test]
6460 fn validate_deps_per_entry_validate_fires_before_duplicate_in_deps() {
6461 // Diagnostic-precedence pin: a malformed `:versao` on the
6462 // duplicating entry surfaces its narrower `VersaoInvalid`
6463 // diagnostic first, before the cross-entry duplicate gate fires
6464 // — the canonical "per-entry shape before cross-entry uniqueness"
6465 // precedence every peer set-not-multiset gate establishes
6466 // (`*_invalid_fires_before_duplicate_check` pins on
6467 // `SupervisorSpec::validate`, `AplicacaoSpec::validate_membros`,
6468 // `validate_upgrade_from`).
6469 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6470 c.deps = vec![
6471 Dep::simple("caixa-teia", "^0.1"),
6472 Dep::simple("caixa-teia", "^bad-version"),
6473 ];
6474 let err = c.validate_deps().unwrap_err();
6475 assert!(
6476 matches!(
6477 err,
6478 crate::dep::DepError::VersaoInvalid { ref nome, ref versao, .. }
6479 if nome == "caixa-teia" && versao == "^bad-version"
6480 ),
6481 "expected VersaoInvalid to surface before DuplicateNome, got {err:?}"
6482 );
6483 }
6484
6485 #[test]
6486 fn validate_deps_duplicate_diagnostic_names_first_collision() {
6487 // First-collision determinism pin: with three entries naming the
6488 // same caixa, the first colliding pair surfaces — not the last.
6489 // Mirrors the peer first-collision posture on every
6490 // duplicate-target gate
6491 // (`validate_upgrade_from_duplicate_diagnostic_names_second_collision`
6492 // — the second entry is the first collision; this gate uses the
6493 // same shape: the second entry's `:nome` lands in the diagnostic
6494 // because `seen.insert(first.nome)` already populated the set).
6495 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6496 c.deps = vec![
6497 Dep::simple("caixa-teia", "^0.1"),
6498 Dep::simple("caixa-teia", "^0.2"),
6499 Dep::simple("caixa-teia", "^0.3"),
6500 ];
6501 let err = c.validate_deps().unwrap_err();
6502 // The diagnostic carries the offending caixa name; the
6503 // implementation surfaces on the *second* entry (the first
6504 // collision), so the test pins the `:nome` value.
6505 assert!(
6506 matches!(
6507 err,
6508 crate::dep::DepError::DuplicateNome { ref nome, list }
6509 if nome == "caixa-teia" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6510 ),
6511 "got {err:?}"
6512 );
6513 }
6514
6515 #[test]
6516 fn validate_deps_duplicate_in_deps_fires_before_duplicate_in_deps_dev() {
6517 // Cross-list precedence pin: when both lists carry duplicates,
6518 // the `:deps` diagnostic surfaces first — same author-mental-
6519 // model ordering the `validate_deps_runs_deps_before_deps_dev`
6520 // pin establishes for malformed `:versao` (runtime axis before
6521 // dev axis).
6522 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6523 c.deps = vec![
6524 Dep::simple("runtime-dep", "^0.1"),
6525 Dep::simple("runtime-dep", "^0.2"),
6526 ];
6527 c.deps_dev = vec![Dep::simple("dev-dep", "*"), Dep::simple("dev-dep", "^0.1")];
6528 let err = c.validate_deps().unwrap_err();
6529 assert!(
6530 matches!(
6531 err,
6532 crate::dep::DepError::DuplicateNome { ref nome, list }
6533 if nome == "runtime-dep" && list == crate::render::DEP_AUTHOR_KEY_DEPS
6534 ),
6535 "expected :deps duplicate to surface before :deps-dev duplicate, got {err:?}"
6536 );
6537 }
6538
6539 #[test]
6540 fn validate_deps_empty_lists_pass_duplicate_gate() {
6541 // Empty-set identity pin: the bare template (zero deps, zero
6542 // deps_dev) passes the duplicate gate as the gate's identity
6543 // element. A future tighten that conflates "empty" with
6544 // "missing" would regress this baseline.
6545 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6546 c.validate_deps().unwrap();
6547 }
6548
6549 #[test]
6550 fn validate_deps_duplicate_diagnostic_carries_list_tag() {
6551 // Diagnostic-shape pin: the `list:` field tags which list the
6552 // duplicate landed in (`:deps` vs `:deps-dev`) verbatim, so a
6553 // `feira lint` run can route the author to the right block in
6554 // their caixa.lisp without re-deriving the list from context.
6555 // Same self-locating shape every peer per-axis diagnostic
6556 // already exposes.
6557 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6558 c.deps_dev = vec![
6559 Dep::simple("dev-thing", "*"),
6560 Dep::simple("dev-thing", "^0.1"),
6561 ];
6562 let err = c.validate_deps().unwrap_err();
6563 let crate::dep::DepError::DuplicateNome { nome, list } = err else {
6564 panic!("expected DuplicateNome from :deps-dev walk");
6565 };
6566 assert_eq!(nome, "dev-thing");
6567 assert_eq!(list, crate::render::DEP_AUTHOR_KEY_DEPS_DEV);
6568 }
6569
6570 // ── validate_deps: per-entry :caracteristicas set-discipline gate ──
6571
6572 #[test]
6573 fn validate_deps_surfaces_caracteristicas_duplicate_in_deps_list() {
6574 // Thread-through pin on `:deps`: the per-entry
6575 // `Dep::validate_caracteristicas` gate fires inside
6576 // `Caixa::validate_deps`'s linear walk, so a malformed feature
6577 // list on any `:deps` entry surfaces as a `DepError` from
6578 // `validate_deps` — the same reachability shape every per-entry
6579 // `Dep::validate` arm threads through. Without this pin a future
6580 // shortcut that skips the per-entry `Dep::validate` call on the
6581 // cross-entry-uniqueness path would mask the within-entry
6582 // `:caracteristicas` gates.
6583 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6584 c.deps = vec![Dep {
6585 nome: "caixa-teia".into(),
6586 versao: "^0.1".into(),
6587 fonte: None,
6588 opcional: false,
6589 caracteristicas: vec!["http".into(), "http".into()],
6590 }];
6591 let err = c.validate_deps().unwrap_err();
6592 let crate::dep::DepError::CaracteristicaDuplicate {
6593 nome,
6594 caracteristica,
6595 } = err
6596 else {
6597 panic!("expected CaracteristicaDuplicate from :deps walk, got {err:?}");
6598 };
6599 assert_eq!(nome, "caixa-teia");
6600 assert_eq!(caracteristica, "http");
6601 }
6602
6603 #[test]
6604 fn validate_deps_surfaces_caracteristicas_empty_in_deps_dev_list() {
6605 // Peer thread-through pin on `:deps-dev`: same reachability as
6606 // the `:deps` arm above, on the dev-only authoring axis. Pins
6607 // that the `validate_deps` walk visits both lists' per-entry
6608 // gates uniformly. The empty-feature arm carries here so both
6609 // new `:caracteristicas` arms are surfaced via at least one
6610 // `validate_deps` thread-through.
6611 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6612 c.deps_dev = vec![Dep {
6613 nome: "caixa-teia".into(),
6614 versao: "^0.1".into(),
6615 fonte: None,
6616 opcional: false,
6617 caracteristicas: vec![String::new()],
6618 }];
6619 let err = c.validate_deps().unwrap_err();
6620 let crate::dep::DepError::CaracteristicaEmpty { nome } = err else {
6621 panic!("expected CaracteristicaEmpty from :deps-dev walk, got {err:?}");
6622 };
6623 assert_eq!(nome, "caixa-teia");
6624 }
6625
6626 #[test]
6627 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_list() {
6628 // Thread-through pin on `:deps`: the per-entry
6629 // `Dep::validate_caracteristicas` value-shape gate (lifted via
6630 // `crate::render::is_cargo_feature_name`) fires inside
6631 // `Caixa::validate_deps`'s linear walk on the `:deps` list, so
6632 // a structurally invalid feature name on any `:deps` entry
6633 // surfaces as `DepError::CaracteristicaInvalid` from
6634 // `validate_deps` — the same reachability shape every per-entry
6635 // `Dep::validate` arm threads through. Without this pin a
6636 // future shortcut that skips the per-entry `Dep::validate` call
6637 // on the cross-entry-uniqueness path would mask the within-
6638 // entry `:caracteristicas` value-shape gate.
6639 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6640 c.deps = vec![Dep {
6641 nome: "caixa-teia".into(),
6642 versao: "^0.1".into(),
6643 fonte: None,
6644 opcional: false,
6645 caracteristicas: vec!["+http".into()],
6646 }];
6647 let err = c.validate_deps().unwrap_err();
6648 let crate::dep::DepError::CaracteristicaInvalid {
6649 nome,
6650 caracteristica,
6651 ..
6652 } = err
6653 else {
6654 panic!("expected CaracteristicaInvalid from :deps walk, got {err:?}");
6655 };
6656 assert_eq!(nome, "caixa-teia");
6657 assert_eq!(caracteristica, "+http");
6658 }
6659
6660 #[test]
6661 fn validate_deps_surfaces_caracteristicas_invalid_in_deps_dev_list() {
6662 // Peer thread-through pin on `:deps-dev`: same reachability as
6663 // the `:deps` arm above, on the dev-only authoring axis. The
6664 // `http/json` shape carries here so the segment-separator
6665 // diagnostic (the canonical Cargo `dep/feat` namespaced-dep
6666 // confusion footgun) is surfaced via the cross-entry walk too —
6667 // pinning that the `:deps-dev` list visits the same per-entry
6668 // value-shape gate as the `:deps` list.
6669 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6670 c.deps_dev = vec![Dep {
6671 nome: "caixa-teia".into(),
6672 versao: "^0.1".into(),
6673 fonte: None,
6674 opcional: false,
6675 caracteristicas: vec!["http/json".into()],
6676 }];
6677 let err = c.validate_deps().unwrap_err();
6678 let crate::dep::DepError::CaracteristicaInvalid {
6679 nome,
6680 caracteristica,
6681 ..
6682 } = err
6683 else {
6684 panic!("expected CaracteristicaInvalid from :deps-dev walk, got {err:?}");
6685 };
6686 assert_eq!(nome, "caixa-teia");
6687 assert_eq!(caracteristica, "http/json");
6688 }
6689
6690 #[test]
6691 fn to_lisp_preserves_deps() {
6692 let src = r#"
6693(defcaixa
6694 :nome "x"
6695 :versao "0.1.0"
6696 :kind Biblioteca
6697 :deps ((:nome "a" :versao "^0.1")
6698 (:nome "b" :versao "*" :fonte (:tipo git :repo "github:o/b" :tag "v1"))))
6699"#;
6700 let c1 = Caixa::from_lisp(src).unwrap();
6701 let emitted = c1.to_lisp();
6702 let c2 = Caixa::from_lisp(&emitted).expect("round trip");
6703 assert_eq!(c1.deps, c2.deps);
6704 }
6705
6706 // ── Caixa::validate_nome — top-level :nome value-shape gate ─────────
6707
6708 fn caixa_with_nome(nome: &str) -> Caixa {
6709 let mut c = Caixa::from_lisp(&Caixa::template("placeholder")).unwrap();
6710 c.nome = nome.to_string();
6711 c
6712 }
6713
6714 #[test]
6715 fn validate_nome_accepts_canonical_template() {
6716 // Positive control: the bare `feira init`-style template's
6717 // `:nome` ("demo") is a canonical DNS-1123 label; the gate must
6718 // not regress this baseline shape. A future tightening of the
6719 // accepted set surfaces here as a test failure first.
6720 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6721 c.validate_nome().unwrap();
6722 }
6723
6724 #[test]
6725 fn validate_nome_accepts_canonical_forms() {
6726 // Positive-set sweep: each realistic caixa-name shape the K8s
6727 // apiserver accepts as a `metadata.name` label must pass —
6728 // single-word, hyphen-joined, version-suffixed, single-char,
6729 // two-char, digit-start (DNS-1123 allows this; the stricter
6730 // DNS-1035 Service-name rule doesn't), version-suffix-bearing.
6731 // Mirrors `accepts_canonical_membro_caixa_forms` (3f9d7a0) on
6732 // the peer member-name axis.
6733 for nome in [
6734 "checkout",
6735 "cart-v2",
6736 "a",
6737 "db",
6738 "3rd-party-shim",
6739 "payment-retry",
6740 "0",
6741 ] {
6742 caixa_with_nome(nome)
6743 .validate_nome()
6744 .unwrap_or_else(|e| panic!("canonical :nome {nome:?} must validate, got {e:?}"));
6745 }
6746 }
6747
6748 #[test]
6749 fn validate_nome_rejects_empty() {
6750 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
6751 // an empty `:nome` (the derive macro stores the raw String);
6752 // the gate's empty arm names the offending axis with a narrower
6753 // diagnostic than the `NomeInvalid` parse arm would emit.
6754 let c = caixa_with_nome("");
6755 let err = c.validate_nome().unwrap_err();
6756 assert_eq!(err, ManifestError::NomeEmpty);
6757 }
6758
6759 #[test]
6760 fn validate_nome_rejects_uppercase() {
6761 // The canonical "I copied the TitleCase display name verbatim"
6762 // footgun. The K8s apiserver rejects `metadata.name: MyApp` at
6763 // admission on every derived artifact (Helm chart, ComputeUnit,
6764 // CNP, HTTPRoute, label values); the gate moves the diagnostic
6765 // to the source `caixa.lisp` and the reason suggests the
6766 // lowercased fix verbatim.
6767 let c = caixa_with_nome("MyApp");
6768 let err = c.validate_nome().unwrap_err();
6769 let ManifestError::NomeInvalid { nome, reason } = err else {
6770 panic!("expected NomeInvalid for uppercase :nome");
6771 };
6772 assert_eq!(nome, "MyApp");
6773 assert!(
6774 reason.contains("uppercase") && reason.contains("myapp"),
6775 "diagnostic must name the violation + the lowercased fix, got {reason:?}"
6776 );
6777 }
6778
6779 #[test]
6780 fn validate_nome_rejects_underscore() {
6781 // The Python-/Postgres-style `snake_case` leak. DNS-1123 forbids
6782 // `_`; the apiserver rejects on admission across every derived
6783 // artifact. Same fixture pinned for `:membros :caixa` (3f9d7a0)
6784 // and `:children :caixa` (31bfa43).
6785 let c = caixa_with_nome("my_app");
6786 let err = c.validate_nome().unwrap_err();
6787 assert!(
6788 matches!(
6789 err,
6790 ManifestError::NomeInvalid { ref nome, ref reason }
6791 if nome == "my_app" && reason.contains('_')
6792 ),
6793 "got {err:?}"
6794 );
6795 }
6796
6797 #[test]
6798 fn validate_nome_rejects_dot() {
6799 // A `:nome` is a single DNS-1123 label, not a subdomain. The
6800 // "I want to namespace with `.`" footgun the gate redirects to
6801 // `-` via the shared predicate's reason wording.
6802 let c = caixa_with_nome("team.app");
6803 let err = c.validate_nome().unwrap_err();
6804 assert!(
6805 matches!(
6806 err,
6807 ManifestError::NomeInvalid { ref nome, ref reason }
6808 if nome == "team.app" && reason.contains('.')
6809 ),
6810 "got {err:?}"
6811 );
6812 }
6813
6814 #[test]
6815 fn validate_nome_rejects_leading_hyphen() {
6816 // DNS-1123 boundary rule: the label must start with an ASCII
6817 // alphanumeric. Pin the leading-`-` arm explicitly.
6818 let c = caixa_with_nome("-app");
6819 let err = c.validate_nome().unwrap_err();
6820 assert!(
6821 matches!(
6822 err,
6823 ManifestError::NomeInvalid { ref nome, .. } if nome == "-app"
6824 ),
6825 "got {err:?}"
6826 );
6827 }
6828
6829 #[test]
6830 fn validate_nome_rejects_trailing_hyphen() {
6831 // Symmetric arm of the boundary rule, pinned separately so a
6832 // future relaxation that only checks the leading position
6833 // surfaces here. Mirrors `rejects_membro_caixa_with_trailing_hyphen`
6834 // and `_with_trailing_hyphen` on the supervisor / aplicacao
6835 // axes.
6836 let c = caixa_with_nome("app-");
6837 let err = c.validate_nome().unwrap_err();
6838 assert!(
6839 matches!(
6840 err,
6841 ManifestError::NomeInvalid { ref nome, .. } if nome == "app-"
6842 ),
6843 "got {err:?}"
6844 );
6845 }
6846
6847 #[test]
6848 fn validate_nome_rejects_unicode() {
6849 // IDN must be pre-encoded as Punycode (`xn--…`); raw Unicode
6850 // bytes are rejected by the K8s apiserver on every name axis.
6851 let c = caixa_with_nome("café");
6852 let err = c.validate_nome().unwrap_err();
6853 assert!(
6854 matches!(
6855 err,
6856 ManifestError::NomeInvalid { ref nome, .. } if nome == "café"
6857 ),
6858 "got {err:?}"
6859 );
6860 }
6861
6862 #[test]
6863 fn validate_nome_rejects_whitespace() {
6864 // The paste-from-sketch / paste-from-spec footgun. Internal
6865 // whitespace is rejected by every K8s name axis.
6866 let c = caixa_with_nome("my app");
6867 let err = c.validate_nome().unwrap_err();
6868 assert!(
6869 matches!(
6870 err,
6871 ManifestError::NomeInvalid { ref nome, .. } if nome == "my app"
6872 ),
6873 "got {err:?}"
6874 );
6875 }
6876
6877 #[test]
6878 fn validate_nome_rejects_too_long() {
6879 // 64-byte boundary pin: the K8s apiserver rejects any
6880 // `metadata.name` over 63 bytes at admission; the diagnostic
6881 // names both the 63-byte cap and the actual length so the
6882 // author can shorten in one edit. Mirrors `_too_long` on the
6883 // peer member-/cluster-/child-name axes.
6884 let over = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN + 1);
6885 let c = caixa_with_nome(&over);
6886 let err = c.validate_nome().unwrap_err();
6887 let ManifestError::NomeInvalid { nome, reason } = err else {
6888 panic!("expected NomeInvalid for over-cap :nome");
6889 };
6890 assert_eq!(nome.len(), crate::DNS_1123_LABEL_MAX_LEN + 1);
6891 assert!(
6892 reason.contains("63") && reason.contains("64"),
6893 "diagnostic must name the cap + actual length, got {reason:?}"
6894 );
6895 }
6896
6897 #[test]
6898 fn nome_max_length_validates() {
6899 // The 63-byte cap exactly — the boundary-accepting case pinned
6900 // alongside `validate_nome_rejects_too_long` so a future cap
6901 // shift surfaces both arms simultaneously. Mirrors
6902 // `membro_caixa_max_length_validates`,
6903 // `placement_cluster_max_length_validates`,
6904 // `child_caixa_max_length_validates`.
6905 let at_cap = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
6906 caixa_with_nome(&at_cap).validate_nome().unwrap();
6907 }
6908
6909 #[test]
6910 fn nome_empty_takes_precedence_over_invalid() {
6911 // Order pin: the empty arm fires before the predicate is
6912 // consulted. Empty < invalid in self-locating-ness — the
6913 // narrower `NomeEmpty` diagnostic doesn't carry a useless
6914 // `nome: ""` reference into the parser-shaped reason. Mirrors
6915 // `membro_caixa_empty_takes_precedence_over_invalid` on the
6916 // peer axis (3f9d7a0).
6917 let c = caixa_with_nome("");
6918 assert_eq!(c.validate_nome().unwrap_err(), ManifestError::NomeEmpty);
6919 }
6920
6921 #[test]
6922 fn nome_invalid_diagnostic_carries_offending_nome() {
6923 // Diagnostic-shape pin: the error names the offending `:nome`
6924 // verbatim with a non-empty parser-shaped reason, so a `feira
6925 // lint` run can render the diagnostic without re-parsing.
6926 // Mirrors `membro_caixa_invalid_diagnostic_carries_offending_caixa`.
6927 let c = caixa_with_nome("MyApp");
6928 let err = c.validate_nome().unwrap_err();
6929 let ManifestError::NomeInvalid { nome, reason } = err else {
6930 panic!("expected NomeInvalid variant");
6931 };
6932 assert_eq!(nome, "MyApp");
6933 assert!(
6934 !reason.is_empty(),
6935 "NomeInvalid `reason` must carry the predicate's wording verbatim"
6936 );
6937 }
6938
6939 // ── Caixa::validate_nome_chart_name_budget — joint-length on `:nome` ──
6940 //
6941 // The bare-`:nome` axis [`Caixa::validate_nome`] caps at 63 bytes
6942 // via DNS-1123; this second-axis gate caps the joint
6943 // `lareira-<nome>` chart name at the same 63-byte ceiling. The
6944 // canonical [`crate::lareira_chart_name`] helper's doc comment
6945 // (f7320d7, caixa-core/src/render.rs:3198) explicitly deferred:
6946 // "the M4 admission webhook will pin the joint-length invariant
6947 // when it lands". These tests pin it at the manifest-validate
6948 // layer instead, fail-before-pass-after on the 56-byte boundary.
6949
6950 #[test]
6951 fn validate_nome_chart_name_budget_accepts_canonical_template() {
6952 // Positive control: the bare `feira init`-style template's
6953 // `:nome` ("demo") sits far below the cap; the gate must not
6954 // regress this baseline. Same shape every peer
6955 // value-shape-gate baseline pin uses.
6956 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
6957 c.validate_nome_chart_name_budget().unwrap();
6958 }
6959
6960 #[test]
6961 fn validate_nome_chart_name_budget_accepts_canonical_fixtures() {
6962 // Positive-set sweep across the canonical author surface every
6963 // in-tree fixture uses (`hello-rio`, `cart`, `checkout`,
6964 // `worker`, the `checkout-aplicacao` example members, the
6965 // `akeyless-attest` caixa-tatara fixture). Every value sits
6966 // far below the 55-byte per-`:nome` budget. Same shape every
6967 // peer per-axis baseline pin uses.
6968 for nome in [
6969 "hello-rio",
6970 "cart",
6971 "checkout",
6972 "worker",
6973 "akeyless-attest",
6974 "demo",
6975 "a",
6976 ] {
6977 caixa_with_nome(nome)
6978 .validate_nome_chart_name_budget()
6979 .unwrap_or_else(|e| {
6980 panic!("canonical :nome {nome:?} must pass chart-name budget, got {e:?}")
6981 });
6982 }
6983 }
6984
6985 #[test]
6986 fn validate_nome_chart_name_budget_accepts_nome_at_cap() {
6987 // Boundary-accepting case at the 55-byte per-`:nome` budget —
6988 // the joint chart name is exactly 63 bytes, the DNS-1123 label
6989 // cap. Pinned alongside the rejecting-arm test so a future cap
6990 // shift surfaces both arms simultaneously. Mirrors
6991 // `nome_max_length_validates` on the peer bare-`:nome` axis.
6992 let at_cap = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN);
6993 caixa_with_nome(&at_cap)
6994 .validate_nome_chart_name_budget()
6995 .unwrap();
6996 }
6997
6998 #[test]
6999 fn validate_nome_chart_name_budget_rejects_nome_one_over_cap() {
7000 // Fail-before-pass-after pin on the 56-byte boundary: the
7001 // smallest `:nome` length that overflows the joint chart-name
7002 // cap. The inner [`is_dns_1123_label`] gate
7003 // (`Caixa::validate_nome`) accepts it (56 ≤ 63), so prior to
7004 // this gate it silently passed the manifest-validate cascade
7005 // and surfaced as a `helm lint` / apiserver rejection on the
7006 // rendered chart name far from the source `caixa.lisp`, with
7007 // no field naming the overflow. With this gate the diagnostic
7008 // names the offending `:nome` verbatim alongside the rendered
7009 // chart name and the budget, so the author can shorten in one
7010 // edit. Mirrors `validate_nome_rejects_too_long` on the peer
7011 // bare-`:nome` axis.
7012 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7013 let c = caixa_with_nome(&over);
7014 let err = c.validate_nome_chart_name_budget().unwrap_err();
7015 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7016 panic!("expected NomeChartNameBudgetExceeded for over-budget :nome");
7017 };
7018 assert_eq!(nome.len(), crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7019 assert_eq!(nome, over);
7020 assert!(
7021 reason.contains("63") && reason.contains("64") && reason.contains("55"),
7022 "diagnostic must name the DNS-1123 cap (63), the actual chart-name length (64), \
7023 and the per-`:nome` budget (55), got {reason:?}"
7024 );
7025 }
7026
7027 #[test]
7028 fn validate_nome_chart_name_budget_rejects_nome_at_bare_dns_cap() {
7029 // The 63-byte `:nome` boundary — passes the bare-`:nome`
7030 // [`is_dns_1123_label`] cap exactly, but produces a 71-byte
7031 // joint chart name that overflows the DNS-1123 label cap
7032 // structurally. The most stringent fail-before-pass-after
7033 // surface: every `:nome` in the 56..=63-byte range passed the
7034 // prior cascade and broke at admission.
7035 let bare_max = "a".repeat(crate::DNS_1123_LABEL_MAX_LEN);
7036 let c = caixa_with_nome(&bare_max);
7037 // The bare-`:nome` gate accepts the 63-byte length.
7038 c.validate_nome().unwrap();
7039 // The new joint-length gate rejects it.
7040 let err = c.validate_nome_chart_name_budget().unwrap_err();
7041 assert!(
7042 matches!(
7043 err,
7044 ManifestError::NomeChartNameBudgetExceeded { ref nome, .. }
7045 if nome.len() == crate::DNS_1123_LABEL_MAX_LEN
7046 ),
7047 "got {err:?}"
7048 );
7049 }
7050
7051 #[test]
7052 fn validate_nome_chart_name_budget_diagnostic_carries_offending_chart_name() {
7053 // Diagnostic-shape pin: the rendered `lareira-<nome>` chart
7054 // name appears verbatim in the diagnostic so the author sees
7055 // exactly the string the apiserver / `helm lint` would have
7056 // rejected — no re-derivation required to grep the source.
7057 // Peer with `nome_invalid_diagnostic_carries_offending_nome`
7058 // on the bare-`:nome` axis.
7059 let over = "x".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 5);
7060 let c = caixa_with_nome(&over);
7061 let err = c.validate_nome_chart_name_budget().unwrap_err();
7062 let ManifestError::NomeChartNameBudgetExceeded { nome, reason } = err else {
7063 panic!("expected NomeChartNameBudgetExceeded variant");
7064 };
7065 assert_eq!(nome, over);
7066 let expected_chart = crate::lareira_chart_name(&over);
7067 assert!(
7068 reason.contains(&expected_chart),
7069 "diagnostic must carry the rendered chart name {expected_chart:?} verbatim, \
7070 got {reason:?}"
7071 );
7072 assert!(
7073 reason.contains("lareira-"),
7074 "diagnostic must name the canonical chart-name prefix verbatim, got {reason:?}"
7075 );
7076 }
7077
7078 #[test]
7079 fn validate_nome_chart_name_budget_runs_after_nome_shape_via_layout_verify() {
7080 // Order pin on the layout cascade: the narrower
7081 // `NomeInvalid` (bare-DNS-1123 shape) fires before the
7082 // joint-length budget. A structurally-malformed `:nome` (here:
7083 // uppercase) surfaces its specific shape error rather than
7084 // the chart-name-budget error, even when the joint length
7085 // would also overflow — the narrower diagnostic is more
7086 // self-locating. Mirrors the cascade-precedence pins peer
7087 // gates already use (e.g. `EntradaParaEmpty` before
7088 // `EntradaParaInvalid`).
7089 let over = "A".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7090 let c = caixa_with_nome(&over);
7091 // The bare-shape gate fires first.
7092 let err = c.validate_nome().unwrap_err();
7093 assert!(
7094 matches!(err, ManifestError::NomeInvalid { .. }),
7095 "bare-shape gate must fire before chart-name-budget gate; got {err:?}"
7096 );
7097 // And the layout verify cascade surfaces that diagnostic, not
7098 // the budget arm. Inject a path-exists oracle so the cascade
7099 // gets past the manifest-presence check and into the
7100 // value-shape gates.
7101 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7102 let err = crate::LayoutInvariants::verify(
7103 &layout,
7104 &c,
7105 std::path::Path::new("/tmp/caixa-test-fake-root"),
7106 )
7107 .unwrap_err();
7108 let issue = err.to_string();
7109 assert!(
7110 issue.contains("DNS-1123") || issue.contains("uppercase"),
7111 "layout cascade must surface the bare-DNS-1123 diagnostic on a \
7112 structurally-malformed :nome, not the chart-name-budget diagnostic; got {issue:?}"
7113 );
7114 }
7115
7116 #[test]
7117 fn layout_verify_routes_chart_name_budget_through_nome_violation() {
7118 // Cross-axis envelope pin: the layout cascade wraps both
7119 // bare-`:nome` and joint-length-`:nome` failures through the
7120 // same [`LayoutError::NomeViolation`] envelope, since both
7121 // arms are on the `:nome` axis. The user's diagnostic stays
7122 // self-locating ("which axis"), and a future consumer that
7123 // dispatches on the layout-error variant (e.g. a `feira lint`
7124 // exit-code mapping) sees a single per-axis envelope. The
7125 // wrapped `issue:` carries the full inner diagnostic.
7126 let over = "a".repeat(crate::LAREIRA_CHART_NAME_NOME_MAX_LEN + 1);
7127 let c = caixa_with_nome(&over);
7128 // The bare-shape gate accepts.
7129 c.validate_nome().unwrap();
7130 let layout = crate::StandardLayout::new().with_path_exists(|_| true);
7131 let err = crate::LayoutInvariants::verify(
7132 &layout,
7133 &c,
7134 std::path::Path::new("/tmp/caixa-test-fake-root"),
7135 )
7136 .unwrap_err();
7137 let crate::LayoutError::NomeViolation { caixa, issue } = err else {
7138 panic!("expected LayoutError::NomeViolation, got {err:?}");
7139 };
7140 assert_eq!(caixa, over);
7141 assert!(
7142 issue.contains("lareira-") && issue.contains("63") && issue.contains("55"),
7143 "wrapped issue must carry the joint-length diagnostic verbatim, got {issue:?}"
7144 );
7145 }
7146
7147 // ── Caixa::validate_versao — top-level :versao value-shape gate ─────
7148
7149 fn caixa_with_versao(versao: &str) -> Caixa {
7150 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7151 c.versao = versao.to_string();
7152 c
7153 }
7154
7155 #[test]
7156 fn validate_versao_accepts_canonical_template() {
7157 // Positive control: the bare `feira init`-style template's
7158 // `:versao` ("0.1.0") is a canonical SemVer-2 literal; the gate
7159 // must not regress this baseline shape. A future tightening of
7160 // the accepted set surfaces here as a test failure first.
7161 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7162 c.validate_versao().unwrap();
7163 }
7164
7165 #[test]
7166 fn validate_versao_accepts_canonical_forms() {
7167 // Positive-set sweep: each realistic SemVer-2 shape the
7168 // substrate's downstream consumers accept must pass — bare
7169 // MAJOR.MINOR.PATCH, pre-release tags (`-rc.1`, `-alpha.0`),
7170 // build metadata (`+build.42`), the combined form, and the
7171 // `0.0.0` boundary case. Mirrors `accepts_canonical_forms` on
7172 // the peer `:nome` axis (6c992f8).
7173 for versao in [
7174 "0.1.0",
7175 "0.0.0",
7176 "1.0.0",
7177 "0.2.0-rc.1",
7178 "1.0.0-alpha.0",
7179 "1.0.0+build.42",
7180 "1.0.0-rc.1+build.42",
7181 "10.20.30",
7182 ] {
7183 caixa_with_versao(versao)
7184 .validate_versao()
7185 .unwrap_or_else(|e| {
7186 panic!("canonical :versao {versao:?} must validate, got {e:?}")
7187 });
7188 }
7189 }
7190
7191 #[test]
7192 fn validate_versao_rejects_empty() {
7193 // Fail-before-pass-after pin: `Caixa::from_lisp` does not refuse
7194 // an empty `:versao` (the derive macro stores the raw String);
7195 // the gate's empty arm names the offending axis with a narrower
7196 // diagnostic than the `VersaoInvalid` parse arm would emit.
7197 // Mirrors `validate_nome_rejects_empty` (6c992f8).
7198 let c = caixa_with_versao("");
7199 let err = c.validate_versao().unwrap_err();
7200 assert_eq!(err, ManifestError::VersaoEmpty);
7201 }
7202
7203 #[test]
7204 fn validate_versao_rejects_git_tag_shape() {
7205 // The canonical "I copied the git tag verbatim" footgun —
7206 // `feira publish` *emits* `v<versao>` git tags, so a leaked
7207 // `v0.1.0` in `:versao` would render as `vv0.1.0` and silently
7208 // shift every downstream consumer's version axis. `semver`
7209 // rejects the leading `v` at parse time; the gate moves the
7210 // diagnostic to the source `caixa.lisp`.
7211 let c = caixa_with_versao("v0.1.0");
7212 let err = c.validate_versao().unwrap_err();
7213 let ManifestError::VersaoInvalid { versao, reason } = err else {
7214 panic!("expected VersaoInvalid for git-tag-shape :versao");
7215 };
7216 assert_eq!(versao, "v0.1.0");
7217 assert!(
7218 !reason.is_empty(),
7219 "VersaoInvalid `reason` must carry the parser's wording, got {reason:?}"
7220 );
7221 }
7222
7223 #[test]
7224 fn validate_versao_rejects_missing_patch() {
7225 // The canonical "I shortened it" footgun — SemVer-2 requires
7226 // three parts. Cargo's `version =` field accepts the shortened
7227 // form as a requirement, conflating the two leaks across the
7228 // typed `:deps :versao` vs top-level `:versao` axes; the gate
7229 // pins the top-level axis to the strict three-part shape.
7230 let c = caixa_with_versao("0.1");
7231 let err = c.validate_versao().unwrap_err();
7232 assert!(
7233 matches!(
7234 err,
7235 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1"
7236 ),
7237 "got {err:?}"
7238 );
7239 }
7240
7241 #[test]
7242 fn validate_versao_rejects_requirement_shape() {
7243 // The canonical "I leaked a requirement into a version" footgun —
7244 // the typed `:deps :versao` / `:membros :versao` axes accept
7245 // `^0.1` (a `VersionReq`); the top-level `:versao` requires a
7246 // concrete `Version`. Without this gate the two typed surfaces
7247 // would silently overlap, and a top-level `^0.1` would surface
7248 // at `helm install` time as a Chart.yaml version rejection far
7249 // from the source `caixa.lisp`.
7250 let c = caixa_with_versao("^0.1");
7251 let err = c.validate_versao().unwrap_err();
7252 assert!(
7253 matches!(
7254 err,
7255 ManifestError::VersaoInvalid { ref versao, .. } if versao == "^0.1"
7256 ),
7257 "got {err:?}"
7258 );
7259 }
7260
7261 #[test]
7262 fn validate_versao_rejects_docker_tag_shape() {
7263 // The "I confused it with a docker tag" footgun — `latest`,
7264 // `main`, `stable` parse as identifiers, not SemVer-2 versions.
7265 // SemVer rejects at parse time; the gate moves the diagnostic
7266 // to the source `caixa.lisp`.
7267 for bad in ["latest", "main", "stable"] {
7268 let c = caixa_with_versao(bad);
7269 let err = c.validate_versao().unwrap_err();
7270 assert!(
7271 matches!(
7272 err,
7273 ManifestError::VersaoInvalid { ref versao, .. } if versao == bad
7274 ),
7275 "got {err:?} for {bad:?}"
7276 );
7277 }
7278 }
7279
7280 #[test]
7281 fn validate_versao_rejects_four_part_form() {
7282 // The Java/Microsoft "MAJOR.MINOR.PATCH.BUILD" convention
7283 // SemVer-2 forbids. A leak from a non-SemVer ecosystem; the
7284 // semver crate rejects the extra `.0` at parse time.
7285 let c = caixa_with_versao("0.1.0.0");
7286 let err = c.validate_versao().unwrap_err();
7287 assert!(
7288 matches!(
7289 err,
7290 ManifestError::VersaoInvalid { ref versao, .. } if versao == "0.1.0.0"
7291 ),
7292 "got {err:?}"
7293 );
7294 }
7295
7296 #[test]
7297 fn versao_empty_takes_precedence_over_invalid() {
7298 // Order pin: the empty arm fires before the parser is consulted.
7299 // Empty < invalid in self-locating-ness — the narrower
7300 // `VersaoEmpty` diagnostic doesn't carry a useless `versao: ""`
7301 // reference into the parser-shaped reason. Mirrors
7302 // `nome_empty_takes_precedence_over_invalid` (6c992f8) on the
7303 // peer axis.
7304 let c = caixa_with_versao("");
7305 assert_eq!(c.validate_versao().unwrap_err(), ManifestError::VersaoEmpty);
7306 }
7307
7308 #[test]
7309 fn versao_invalid_diagnostic_carries_offending_versao() {
7310 // Diagnostic-shape pin: the error names the offending `:versao`
7311 // verbatim with a non-empty parser-shaped reason, so a `feira
7312 // lint` run can render the diagnostic without re-parsing.
7313 // Mirrors `nome_invalid_diagnostic_carries_offending_nome`.
7314 let c = caixa_with_versao("v0.1.0");
7315 let err = c.validate_versao().unwrap_err();
7316 let ManifestError::VersaoInvalid { versao, reason } = err else {
7317 panic!("expected VersaoInvalid variant");
7318 };
7319 assert_eq!(versao, "v0.1.0");
7320 assert!(
7321 !reason.is_empty(),
7322 "VersaoInvalid `reason` must carry the parser's wording verbatim"
7323 );
7324 }
7325
7326 #[test]
7327 fn validate_versao_accepts_what_upgrade_from_from_accepts() {
7328 // Parity pin: every shape `UpgradeFromEntry::validate` accepts
7329 // for `:upgrade-from :from` must also pass `validate_versao` —
7330 // the two `:versao`-typed surfaces (top-level `:versao`,
7331 // `:upgrade-from :from`) consume the *same* `semver::Version`
7332 // parser, so they must agree on the accepted set. Without this
7333 // pin, a future tightening of one axis could silently diverge
7334 // from the other. Mirrors the `:versao` requirement-axis
7335 // parity (`:deps`/`:deps-dev`/`:membros`/`:children`) the prior
7336 // commits established.
7337 for versao in ["0.1.0", "0.2.0-rc.1", "1.0.0+build.42"] {
7338 // From the canonical UpgradeFromEntry round-trip fixture
7339 // (`upgrade::tests::round_trip_load_module` peers).
7340 let entry = crate::UpgradeFromEntry {
7341 from: versao.to_string(),
7342 instructions: Vec::new(),
7343 };
7344 entry
7345 .validate()
7346 .unwrap_or_else(|e| panic!(":from {versao:?} must validate, got {e:?}"));
7347 caixa_with_versao(versao)
7348 .validate_versao()
7349 .unwrap_or_else(|e| {
7350 panic!(":versao {versao:?} must validate, got {e:?} — peer axis diverges")
7351 });
7352 }
7353 }
7354
7355 // ── Caixa::validate_restart_window — supervisor restart-window
7356 // folds through the shared `supervisor::duration_codec` ────────
7357
7358 fn caixa_with_restart_window(window: Option<&str>) -> Caixa {
7359 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
7360 c.kind = CaixaKind::Supervisor;
7361 c.restart_window = window.map(str::to_string);
7362 c
7363 }
7364
7365 #[test]
7366 fn validate_restart_window_accepts_none() {
7367 // The canonical "omit the slot to express no reset" shape — a
7368 // `None` raw string is the absence of the typed
7369 // `:restart-window` slot, which is exactly the SupervisorSpec
7370 // "never reset" semantics. The gate must be a no-op here; a
7371 // future tightening that rejected `None` would force every
7372 // supervisor caixa to authoring-time pin a window even when
7373 // the OTP semantics call for none.
7374 caixa_with_restart_window(None)
7375 .validate_restart_window()
7376 .unwrap();
7377 }
7378
7379 #[test]
7380 fn validate_restart_window_accepts_canonical_forms() {
7381 // Positive-set sweep across the canonical authoring units the
7382 // shared `supervisor::duration_codec::parse` accepts —
7383 // matches the codec-side `parse_accepts_integer_canonical_units`
7384 // pin in supervisor::tests so a future codec-side tightening
7385 // surfaces simultaneously on both axes.
7386 for window in ["60s", "5m", "1h", "500ms", "30", "0s"] {
7387 caixa_with_restart_window(Some(window))
7388 .validate_restart_window()
7389 .unwrap_or_else(|e| {
7390 panic!("canonical :restart-window {window:?} must validate, got {e:?}")
7391 });
7392 }
7393 }
7394
7395 #[test]
7396 fn validate_restart_window_rejects_fractional_seconds() {
7397 // Fail-before-pass-after pin: the `"1.5s"` drift class (parses
7398 // as f64 to 1.5 → renders back as `"1500ms"` on first
7399 // serialize). Prior to the fold + this gate, the inline
7400 // `parse_window_inline` accepted f64 magnitudes and silently
7401 // produced a `Duration::from_secs_f64(1.5)`, divergent from
7402 // the shared codec's integer-magnitude discipline on the
7403 // serde-routed siblings. The gate now surfaces a self-locating
7404 // diagnostic at the manifest layer.
7405 let err = caixa_with_restart_window(Some("1.5s"))
7406 .validate_restart_window()
7407 .unwrap_err();
7408 let ManifestError::RestartWindowMalformed {
7409 restart_window,
7410 reason,
7411 } = err
7412 else {
7413 panic!("expected RestartWindowMalformed for fractional seconds");
7414 };
7415 assert_eq!(restart_window, "1.5s");
7416 assert!(
7417 reason.contains("\"1.5\"") && reason.contains("not a non-negative integer"),
7418 "diagnostic must carry shared-codec wording, got {reason:?}"
7419 );
7420 }
7421
7422 #[test]
7423 fn validate_restart_window_rejects_decimal_shaped_integer() {
7424 // The `"1.0s"` class — numerically `1s` exactly, but the
7425 // canonical form is `"1s"` not `"1.0s"`. Decimal-shape leak
7426 // gets the same canonical-form diagnostic.
7427 let err = caixa_with_restart_window(Some("1.0s"))
7428 .validate_restart_window()
7429 .unwrap_err();
7430 assert!(
7431 matches!(
7432 err,
7433 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7434 if restart_window == "1.0s"
7435 ),
7436 "got {err:?}"
7437 );
7438 }
7439
7440 #[test]
7441 fn validate_restart_window_rejects_half_unit_minute() {
7442 // `"0.5m"` is the unit-fraction footgun — author writes a
7443 // human-readable half-minute, the prior inline parser silently
7444 // produced `Duration::from_secs_f64(30.0)` and serde
7445 // re-emitted as `"30s"`, rewriting author intent. The gate
7446 // closes the loop at the manifest layer.
7447 let err = caixa_with_restart_window(Some("0.5m"))
7448 .validate_restart_window()
7449 .unwrap_err();
7450 let ManifestError::RestartWindowMalformed {
7451 restart_window,
7452 reason,
7453 } = err
7454 else {
7455 panic!("expected RestartWindowMalformed");
7456 };
7457 assert_eq!(restart_window, "0.5m");
7458 assert!(
7459 reason.contains("\"30s\""),
7460 "diagnostic must point at the canonical-form remediation, got {reason:?}"
7461 );
7462 }
7463
7464 #[test]
7465 fn validate_restart_window_rejects_leading_sign() {
7466 // `"+30s"` and `"-30s"` both round-tripped through f64 cleanly
7467 // on the prior parser (`+30` parses as `30.0`; `-30` parsed
7468 // and was caught by the `num < 0.0` arm which silently
7469 // returned `None`, dropping the author-supplied window). The
7470 // shared codec's digit-only gate rejects both with a unified
7471 // canonical-form diagnostic; the manifest-layer wrapper names
7472 // the offending value.
7473 for bad in ["+30s", "-30s"] {
7474 let err = caixa_with_restart_window(Some(bad))
7475 .validate_restart_window()
7476 .unwrap_err();
7477 assert!(
7478 matches!(
7479 err,
7480 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7481 if restart_window == bad
7482 ),
7483 "got {err:?} for {bad:?}"
7484 );
7485 }
7486 }
7487
7488 #[test]
7489 fn validate_restart_window_rejects_unknown_unit() {
7490 // `"30x"` — the typo / wrong-unit footgun. The shared codec's
7491 // unit dispatch surfaces an `unknown duration unit` reason;
7492 // the manifest-layer wrapper names the offending value.
7493 let err = caixa_with_restart_window(Some("30x"))
7494 .validate_restart_window()
7495 .unwrap_err();
7496 let ManifestError::RestartWindowMalformed {
7497 restart_window,
7498 reason,
7499 } = err
7500 else {
7501 panic!("expected RestartWindowMalformed for unknown unit");
7502 };
7503 assert_eq!(restart_window, "30x");
7504 assert!(
7505 reason.contains("unknown duration unit"),
7506 "diagnostic must carry shared-codec unit-rejection wording, got {reason:?}"
7507 );
7508 }
7509
7510 #[test]
7511 fn validate_restart_window_rejects_garbage() {
7512 // Pure non-numeric magnitude (`"abc"`) falls through to the
7513 // shared codec's narrower `"bad duration magnitude"` arm. Same
7514 // diagnostic shape as the codec-side
7515 // `parse_garbage_still_falls_through_to_bad_magnitude` pin.
7516 let err = caixa_with_restart_window(Some("abc"))
7517 .validate_restart_window()
7518 .unwrap_err();
7519 let ManifestError::RestartWindowMalformed {
7520 restart_window,
7521 reason,
7522 } = err
7523 else {
7524 panic!("expected RestartWindowMalformed for garbage");
7525 };
7526 assert_eq!(restart_window, "abc");
7527 assert!(
7528 reason.contains("bad duration magnitude"),
7529 "diagnostic must carry shared-codec garbage-rejection wording, got {reason:?}"
7530 );
7531 }
7532
7533 #[test]
7534 fn validate_restart_window_rejects_empty_string() {
7535 // The empty-after-trim edge case — distinct from the `None`
7536 // canonical "omit the slot" shape. The shared codec's
7537 // digit-only gate refuses an empty magnitude; the manifest
7538 // layer names the offending `""` so the author can grep for
7539 // the literal empty value in their `caixa.lisp` and either
7540 // remove the slot (the canonical "no reset" shape) or pin a
7541 // positive duration.
7542 let err = caixa_with_restart_window(Some(""))
7543 .validate_restart_window()
7544 .unwrap_err();
7545 assert!(
7546 matches!(
7547 err,
7548 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7549 if restart_window.is_empty()
7550 ),
7551 "got {err:?}"
7552 );
7553 }
7554
7555 #[test]
7556 fn validate_restart_window_diagnostic_carries_offending_value() {
7557 // Diagnostic-shape pin (peer with
7558 // `nome_invalid_diagnostic_carries_offending_nome` /
7559 // `versao_invalid_diagnostic_carries_offending_versao`): the
7560 // error names the offending raw `:restart-window` verbatim
7561 // with a non-empty shared-codec-shaped reason, so a `feira
7562 // lint` run can render the diagnostic without re-parsing.
7563 let err = caixa_with_restart_window(Some("1.5s"))
7564 .validate_restart_window()
7565 .unwrap_err();
7566 let ManifestError::RestartWindowMalformed {
7567 restart_window,
7568 reason,
7569 } = err
7570 else {
7571 panic!("expected RestartWindowMalformed variant");
7572 };
7573 assert_eq!(restart_window, "1.5s");
7574 assert!(
7575 !reason.is_empty(),
7576 "RestartWindowMalformed `reason` must carry the codec's wording verbatim"
7577 );
7578 }
7579
7580 #[test]
7581 fn supervisor_view_folds_through_shared_codec_on_canonical_form() {
7582 // Behavioral parity pin after the fold (`parse_window_inline`
7583 // deletion): the canonical `"60s"` still produces
7584 // `Duration::from_secs(60)` on the typed view — the fold is
7585 // semantically equivalent to the prior inline parser on the
7586 // accepted set. Mirrors the pre-fold `supervisor_view_returns_typed_shape`
7587 // pin, narrowed to the parser-side contract.
7588 let c = caixa_with_restart_window(Some("60s"));
7589 let view = c.supervisor_view().expect("Supervisor kind has a view");
7590 assert_eq!(
7591 view.restart_window,
7592 Some(std::time::Duration::from_secs(60))
7593 );
7594 }
7595
7596 #[test]
7597 fn supervisor_view_soft_swallows_what_validate_rejects() {
7598 // Parity pin between the view-construction path and the
7599 // manifest-level validator: the same `"1.5s"` that surfaces
7600 // `RestartWindowMalformed` at `validate_restart_window` time
7601 // becomes `restart_window: None` on the typed view (the fold
7602 // preserves the existing best-effort shape of `supervisor_view`).
7603 // The contract is: a layout-verifier / `feira lint` flow that
7604 // cares about the malformed-window axis MUST consult
7605 // `validate_restart_window` — relying solely on the view's
7606 // `None` swallows the diagnostic silently. This pin makes the
7607 // expectation a typed invariant.
7608 let c = caixa_with_restart_window(Some("1.5s"));
7609 let view = c.supervisor_view().expect("Supervisor kind has a view");
7610 assert_eq!(
7611 view.restart_window, None,
7612 "view-construction path soft-swallows the parse error to None"
7613 );
7614 // And the manifest-level validator does NOT soft-swallow:
7615 assert!(
7616 matches!(
7617 c.validate_restart_window().unwrap_err(),
7618 ManifestError::RestartWindowMalformed { ref restart_window, .. }
7619 if restart_window == "1.5s"
7620 ),
7621 "validator must surface the offending value",
7622 );
7623 }
7624
7625 // ── validate_code_paths — per-entry shape on :bibliotecas / :exe / :servicos ──
7626
7627 fn caixa_with_code_paths(bibliotecas: Vec<&str>, exe: Vec<&str>, servicos: Vec<&str>) -> Caixa {
7628 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7629 c.bibliotecas = bibliotecas.into_iter().map(String::from).collect();
7630 c.exe = exe.into_iter().map(String::from).collect();
7631 c.servicos = servicos.into_iter().map(String::from).collect();
7632 c
7633 }
7634
7635 #[test]
7636 fn validate_code_paths_accepts_canonical_template() {
7637 // The bare `Caixa::template` shape is the gate's identity element
7638 // on the canonical authoring shape — `:bibliotecas
7639 // ("lib/demo.lisp")` + empty `:exe` + empty `:servicos`. Pins
7640 // that the gate is non-disruptive against every existing caixa.
7641 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
7642 c.validate_code_paths().unwrap();
7643 }
7644
7645 #[test]
7646 fn validate_code_paths_accepts_explicit_relative_paths_on_every_slot() {
7647 // Positive control sweep: a canonical-shaped path on every slot
7648 // passes. Mirrors the peer
7649 // `behavior::validate_every_slot_relative_is_ok` pin.
7650 let c = caixa_with_code_paths(
7651 vec!["lib/demo.lisp", "lib/helpers.lisp"],
7652 vec!["exe/demo", "exe/tool"],
7653 vec!["servicos/demo.computeunit.yaml"],
7654 );
7655 c.validate_code_paths().unwrap();
7656 }
7657
7658 #[test]
7659 fn validate_code_paths_accepts_all_empty_lists() {
7660 // The empty-list identity element: every Caixa with no declared
7661 // code paths trivially passes (Supervisor / Aplicacao kinds rely
7662 // on this — the OwnCode gate already rejected them before the
7663 // path-shape gate runs in the layout, but the validator itself
7664 // must accept the empty shape).
7665 let c = caixa_with_code_paths(vec![], vec![], vec![]);
7666 c.validate_code_paths().unwrap();
7667 }
7668
7669 #[test]
7670 fn validate_code_paths_rejects_empty_bibliotecas_entry() {
7671 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
7672 let err = c.validate_code_paths().unwrap_err();
7673 assert!(
7674 matches!(
7675 err,
7676 ManifestError::CodePathEmpty {
7677 slot: ":bibliotecas"
7678 }
7679 ),
7680 "got {err:?}",
7681 );
7682 }
7683
7684 #[test]
7685 fn validate_code_paths_rejects_empty_exe_entry() {
7686 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
7687 let err = c.validate_code_paths().unwrap_err();
7688 assert!(
7689 matches!(err, ManifestError::CodePathEmpty { slot: ":exe" }),
7690 "got {err:?}",
7691 );
7692 }
7693
7694 #[test]
7695 fn validate_code_paths_rejects_empty_servicos_entry() {
7696 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
7697 let err = c.validate_code_paths().unwrap_err();
7698 assert!(
7699 matches!(err, ManifestError::CodePathEmpty { slot: ":servicos" }),
7700 "got {err:?}",
7701 );
7702 }
7703
7704 #[test]
7705 fn validate_code_paths_rejects_absolute_bibliotecas_entry() {
7706 // `:bibliotecas` has no `starts_with(<dir>)` fence downstream,
7707 // so an absolute path that resolves on disk silently passes the
7708 // layout's existence check — the canonical sandbox-escape on
7709 // the biblioteca axis.
7710 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7711 let err = c.validate_code_paths().unwrap_err();
7712 let ManifestError::CodePathAbsolute { slot, path } = err else {
7713 panic!("expected CodePathAbsolute, got {err:?}");
7714 };
7715 assert_eq!(slot, ":bibliotecas");
7716 assert_eq!(path, PathBuf::from("/etc/passwd"));
7717 }
7718
7719 #[test]
7720 fn validate_code_paths_rejects_absolute_exe_entry() {
7721 let c = caixa_with_code_paths(vec![], vec!["/usr/bin/env"], vec![]);
7722 let err = c.validate_code_paths().unwrap_err();
7723 let ManifestError::CodePathAbsolute { slot, path } = err else {
7724 panic!("expected CodePathAbsolute, got {err:?}");
7725 };
7726 assert_eq!(slot, ":exe");
7727 assert_eq!(path, PathBuf::from("/usr/bin/env"));
7728 }
7729
7730 #[test]
7731 fn validate_code_paths_rejects_absolute_servicos_entry() {
7732 let c = caixa_with_code_paths(vec![], vec![], vec!["/var/servicos/x.yaml"]);
7733 let err = c.validate_code_paths().unwrap_err();
7734 let ManifestError::CodePathAbsolute { slot, path } = err else {
7735 panic!("expected CodePathAbsolute, got {err:?}");
7736 };
7737 assert_eq!(slot, ":servicos");
7738 assert_eq!(path, PathBuf::from("/var/servicos/x.yaml"));
7739 }
7740
7741 #[test]
7742 fn validate_code_paths_rejects_parent_escape_bibliotecas_leading() {
7743 // Canonical "I want a lib from a sibling caixa" footgun on the
7744 // biblioteca axis. `:bibliotecas` has no `starts_with` fence
7745 // downstream, so a leading `..` traverses to the parent of the
7746 // caixa root with no diagnostic at layout time if the resolved
7747 // target exists.
7748 let c = caixa_with_code_paths(vec!["../sibling/x.lisp"], vec![], vec![]);
7749 let err = c.validate_code_paths().unwrap_err();
7750 let ManifestError::CodePathParentEscape { slot, path } = err else {
7751 panic!("expected CodePathParentEscape, got {err:?}");
7752 };
7753 assert_eq!(slot, ":bibliotecas");
7754 assert_eq!(path, PathBuf::from("../sibling/x.lisp"));
7755 }
7756
7757 #[test]
7758 fn validate_code_paths_rejects_parent_escape_exe_mid_path() {
7759 // Mid-path `..` defeats the layout's component-aware
7760 // `starts_with(exe_dir)` fence — `root.join("exe/../../escape")`
7761 // `starts_with(<root>/exe)` is true, but the canonical resolution
7762 // lives outside the caixa root. Caught regardless of where the
7763 // `..` sits — mirrors the peer
7764 // `behavior::validate_rejects_parent_escape_mid_path` pin.
7765 let c = caixa_with_code_paths(vec![], vec!["exe/../../escape"], vec![]);
7766 let err = c.validate_code_paths().unwrap_err();
7767 let ManifestError::CodePathParentEscape { slot, path } = err else {
7768 panic!("expected CodePathParentEscape, got {err:?}");
7769 };
7770 assert_eq!(slot, ":exe");
7771 assert_eq!(path, PathBuf::from("exe/../../escape"));
7772 }
7773
7774 #[test]
7775 fn validate_code_paths_rejects_parent_escape_servicos_trailing() {
7776 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/foo/../../escape.yaml"]);
7777 let err = c.validate_code_paths().unwrap_err();
7778 let ManifestError::CodePathParentEscape { slot, path } = err else {
7779 panic!("expected CodePathParentEscape, got {err:?}");
7780 };
7781 assert_eq!(slot, ":servicos");
7782 assert_eq!(path, PathBuf::from("servicos/foo/../../escape.yaml"));
7783 }
7784
7785 #[test]
7786 fn validate_code_paths_cross_slot_precedence_bibliotecas_before_exe_before_servicos() {
7787 // Cross-slot precedence pin: `:bibliotecas` → `:exe` →
7788 // `:servicos`. A manifest with malformed entries on all three
7789 // surfaces surfaces the `:bibliotecas` defect first, mirroring
7790 // the canonical declaration order
7791 // `Caixa::declared_foreign_code_slots` already establishes for
7792 // the foreign-code-slot diagnostic.
7793 let c = caixa_with_code_paths(vec![""], vec![""], vec![""]);
7794 let err = c.validate_code_paths().unwrap_err();
7795 assert!(
7796 matches!(
7797 err,
7798 ManifestError::CodePathEmpty {
7799 slot: ":bibliotecas"
7800 }
7801 ),
7802 "got {err:?}",
7803 );
7804 }
7805
7806 #[test]
7807 fn validate_code_paths_within_slot_precedence_empty_before_absolute_before_parent_escape() {
7808 // Within-slot precedence pin: empty → absolute → parent-escape,
7809 // matching the [`PathShapeViolation`] arm-ordering every peer
7810 // `is_sandboxed_relative_path` caller follows (b0c8389
7811 // BehaviorSpec, 26da2c7 UpgradeInstruction::StateChange). A
7812 // `:bibliotecas` list whose first entry is empty *and* whose
7813 // later entries are absolute/parent-escape surfaces the empty
7814 // arm first, on the lexicographically-earliest offending entry.
7815 let c = caixa_with_code_paths(vec!["", "/etc/passwd", "../escape.lisp"], vec![], vec![]);
7816 let err = c.validate_code_paths().unwrap_err();
7817 assert!(
7818 matches!(
7819 err,
7820 ManifestError::CodePathEmpty {
7821 slot: ":bibliotecas"
7822 }
7823 ),
7824 "got {err:?}",
7825 );
7826 }
7827
7828 #[test]
7829 fn validate_code_paths_first_offender_per_slot_wins() {
7830 // Within a single slot, the first declaration-order offender
7831 // surfaces — pins that the gate is left-to-right deterministic
7832 // (peer of every `*_first_collision_*` pin on duplicate gates).
7833 let c = caixa_with_code_paths(
7834 vec!["lib/ok.lisp", "/etc/escape", "../also-escape"],
7835 vec![],
7836 vec![],
7837 );
7838 let err = c.validate_code_paths().unwrap_err();
7839 let ManifestError::CodePathAbsolute { slot, path } = err else {
7840 panic!("expected CodePathAbsolute, got {err:?}");
7841 };
7842 assert_eq!(slot, ":bibliotecas");
7843 assert_eq!(path, PathBuf::from("/etc/escape"));
7844 }
7845
7846 #[test]
7847 fn validate_code_paths_diagnostic_carries_offending_slot_and_path() {
7848 // Diagnostic-shape pin (peer with
7849 // `nome_invalid_diagnostic_carries_offending_nome` /
7850 // `versao_invalid_diagnostic_carries_offending_versao`): the
7851 // error's Display surfaces both the offending `:slot` tag and
7852 // the offending path verbatim, so a `feira lint` run can render
7853 // the diagnostic without re-parsing.
7854 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
7855 let rendered = c.validate_code_paths().unwrap_err().to_string();
7856 assert!(
7857 rendered.contains(":bibliotecas"),
7858 "diagnostic must name the offending slot: {rendered}",
7859 );
7860 assert!(
7861 rendered.contains("/etc/passwd"),
7862 "diagnostic must quote the offending path: {rendered}",
7863 );
7864 }
7865
7866 #[test]
7867 fn validate_code_paths_rejects_duplicate_bibliotecas_entry() {
7868 // Canonical copy-paste-the-wrong-file footgun on the biblioteca
7869 // axis. Without the gate `feira build` re-parses the same lib
7870 // twice, wasting work and silently masking the author's intent
7871 // to declare a *second* biblioteca.
7872 let c = caixa_with_code_paths(vec!["lib/demo.lisp", "lib/demo.lisp"], vec![], vec![]);
7873 let err = c.validate_code_paths().unwrap_err();
7874 let ManifestError::CodePathDuplicate { slot, path } = err else {
7875 panic!("expected CodePathDuplicate, got {err:?}");
7876 };
7877 assert_eq!(slot, ":bibliotecas");
7878 assert_eq!(path, PathBuf::from("lib/demo.lisp"));
7879 }
7880
7881 #[test]
7882 fn validate_code_paths_rejects_duplicate_exe_entry() {
7883 // Same footgun on the Binario surface. The future `caixa-flake`
7884 // emitter that materializes each `:exe` entry as a flake
7885 // `packages.<name>` derivation would collide on the duplicate
7886 // package key — surfaced here at the typed-validate layer with a
7887 // self-locating diagnostic instead.
7888 let c = caixa_with_code_paths(vec![], vec!["exe/cli", "exe/cli"], vec![]);
7889 let err = c.validate_code_paths().unwrap_err();
7890 let ManifestError::CodePathDuplicate { slot, path } = err else {
7891 panic!("expected CodePathDuplicate, got {err:?}");
7892 };
7893 assert_eq!(slot, ":exe");
7894 assert_eq!(path, PathBuf::from("exe/cli"));
7895 }
7896
7897 #[test]
7898 fn validate_code_paths_rejects_duplicate_servicos_entry() {
7899 // Same footgun on the Servico surface. The peer caixa-helm /
7900 // caixa-flux renderers refuse `:servicos.len() != 1` with the
7901 // narrower `UnsupportedServicoCount` diagnostic, but that
7902 // diagnostic surfaces "too many servicos" without naming
7903 // "duplicate entry" — the typed self-locating framing only lands
7904 // at this gate.
7905 let c = caixa_with_code_paths(
7906 vec![],
7907 vec![],
7908 vec![
7909 "servicos/demo.computeunit.yaml",
7910 "servicos/demo.computeunit.yaml",
7911 ],
7912 );
7913 let err = c.validate_code_paths().unwrap_err();
7914 let ManifestError::CodePathDuplicate { slot, path } = err else {
7915 panic!("expected CodePathDuplicate, got {err:?}");
7916 };
7917 assert_eq!(slot, ":servicos");
7918 assert_eq!(path, PathBuf::from("servicos/demo.computeunit.yaml"));
7919 }
7920
7921 #[test]
7922 fn validate_code_paths_accepts_same_path_across_slots() {
7923 // Per-list scope pin: a `:bibliotecas` entry that happens to
7924 // collide with an `:exe` or `:servicos` entry as a *string* is
7925 // not a duplicate by this gate (each list gets its own HashSet),
7926 // mirroring the peer `:deps` ↔ `:deps-dev` per-list scope
7927 // (a `:nome` present in both lists is a legitimate dev-vs-runtime
7928 // shape on the dep axis). The structural `starts_with(<exe |
7929 // servicos>_dir)` fence at layout time prevents the realistic
7930 // cross-slot collision case from existing on disk, but the gate's
7931 // per-list scope is correct independent of that downstream fence.
7932 let c = caixa_with_code_paths(
7933 vec!["lib/x.lisp"],
7934 vec!["exe/x"],
7935 vec!["servicos/x.computeunit.yaml"],
7936 );
7937 c.validate_code_paths().unwrap();
7938 }
7939
7940 #[test]
7941 fn validate_code_paths_duplicate_fires_after_structural_checks_on_same_slot() {
7942 // Within-slot ordering pin: structural defects (empty / absolute
7943 // / parent-escape) fire before the duplicate gate on the same
7944 // slot. A `:bibliotecas ("" "lib/x.lisp" "lib/x.lisp")` shape
7945 // surfaces the narrower `CodePathEmpty` for the empty entry
7946 // first, not the duplicate on the later pair — same arm-ordering
7947 // every peer per-list duplicate gate uses (`:etiquetas` 360a499,
7948 // `:autores` 86c769b, `:deps` 359fba5).
7949 let c = caixa_with_code_paths(vec!["", "lib/x.lisp", "lib/x.lisp"], vec![], vec![]);
7950 let err = c.validate_code_paths().unwrap_err();
7951 assert!(
7952 matches!(
7953 err,
7954 ManifestError::CodePathEmpty {
7955 slot: ":bibliotecas"
7956 }
7957 ),
7958 "got {err:?}",
7959 );
7960 }
7961
7962 #[test]
7963 fn validate_code_paths_duplicate_in_bibliotecas_fires_before_duplicate_in_exe() {
7964 // Cross-slot ordering pin on the duplicate arm: `:bibliotecas`
7965 // duplicates surface before `:exe` duplicates, matching the
7966 // canonical `:bibliotecas` → `:exe` → `:servicos` declaration
7967 // order every peer per-slot diagnostic on this surface follows.
7968 let c = caixa_with_code_paths(
7969 vec!["lib/x.lisp", "lib/x.lisp"],
7970 vec!["exe/y", "exe/y"],
7971 vec![],
7972 );
7973 let err = c.validate_code_paths().unwrap_err();
7974 let ManifestError::CodePathDuplicate { slot, path } = err else {
7975 panic!("expected CodePathDuplicate, got {err:?}");
7976 };
7977 assert_eq!(slot, ":bibliotecas");
7978 assert_eq!(path, PathBuf::from("lib/x.lisp"));
7979 }
7980
7981 #[test]
7982 fn validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path() {
7983 // Diagnostic-shape pin (peer with
7984 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
7985 // on the structural arm): the duplicate-arm Display surfaces both
7986 // the offending `:slot` tag and the offending path verbatim, so a
7987 // `feira lint` run can render the diagnostic without re-parsing.
7988 let c = caixa_with_code_paths(
7989 vec![],
7990 vec![],
7991 vec![
7992 "servicos/demo.computeunit.yaml",
7993 "servicos/demo.computeunit.yaml",
7994 ],
7995 );
7996 let rendered = c.validate_code_paths().unwrap_err().to_string();
7997 assert!(
7998 rendered.contains(":servicos"),
7999 "diagnostic must name the offending slot: {rendered}",
8000 );
8001 assert!(
8002 rendered.contains("servicos/demo.computeunit.yaml"),
8003 "diagnostic must quote the offending path: {rendered}",
8004 );
8005 }
8006
8007 // ── validate_code_paths — `.lisp` extension gate on :bibliotecas ──
8008 //
8009 // The lifted [`crate::render::is_lisp_extension`] predicate (33cc830)
8010 // now gates `:bibliotecas` entries on the tatara-lisp-source file-type
8011 // contract. The `feira build` loop (`caixa-feira/src/cmd/build.rs:33`)
8012 // reads every declared `:bibliotecas` entry through `tatara_lisp::read`
8013 // at parse time — the same downstream consumer the peer `:behavior
8014 // :on-*` (c97815a, [`crate::BehaviorError::NonLispExtension`]) and
8015 // `:upgrade-from :state-change :script` (33cc830,
8016 // [`crate::UpgradeError::NonLispExtensionScript`]) axes route through.
8017 // `:exe` and `:servicos` are deliberately excluded — `:exe` is the
8018 // nix-built executable surface (`"exe/<name>"` shape per the canonical
8019 // [`crate::LayoutError::ExeOutsideDir`] error message and every
8020 // in-tree `caixa_with_code_paths` positive control), and `:servicos`
8021 // is the `.computeunit.yaml` ComputeUnit-CR axis.
8022
8023 #[test]
8024 fn validate_code_paths_rejects_no_extension_bibliotecas_entry() {
8025 // Canonical "I dragged the wrong file from the workspace tree"
8026 // footgun on the biblioteca axis. Without the gate `feira build`
8027 // hands the extensionless path to `tatara_lisp::read` and fails
8028 // with a parser-shaped diagnostic far from the source caixa.lisp,
8029 // with no field naming the offending `:bibliotecas` entry.
8030 for relpath in ["lib/demo", "demo", "lib/handlers/inner"] {
8031 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8032 let err = c.validate_code_paths().unwrap_err();
8033 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8034 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8035 };
8036 assert_eq!(slot, ":bibliotecas");
8037 assert_eq!(path, PathBuf::from(relpath));
8038 }
8039 }
8040
8041 #[test]
8042 fn validate_code_paths_rejects_wrong_extension_bibliotecas_entry() {
8043 // Wrong-extension sweep across common authoring footguns. Same
8044 // sweep posture as the peer
8045 // `behavior::validate_rejects_wrong_extension` (c97815a) and
8046 // `upgrade::tests::state_change_rejects_wrong_extension_script`
8047 // (33cc830) cases.
8048 for relpath in [
8049 "lib/demo.rs",
8050 "lib/demo.txt",
8051 "lib/demo.md",
8052 "lib/demo.json",
8053 "lib/demo.yaml",
8054 "lib/demo.toml",
8055 "lib/demo.lisp.bak",
8056 "lib/demo.lispx",
8057 "lib/demo.lis",
8058 ] {
8059 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8060 let err = c.validate_code_paths().unwrap_err();
8061 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8062 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8063 };
8064 assert_eq!(slot, ":bibliotecas");
8065 assert_eq!(path, PathBuf::from(relpath));
8066 }
8067 }
8068
8069 #[test]
8070 fn validate_code_paths_rejects_case_folded_extension_bibliotecas_entry() {
8071 // Case-sensitivity sweep — pins the strict lowercase `.lisp`
8072 // contract. An uppercase `.LISP` shape that the layout's existence
8073 // check would (case-insensitively, on case-insensitive volumes)
8074 // match the on-disk file still mismatches the canonical form the
8075 // codec emits, breaking the THEORY.md §V.2.7 render-determinism
8076 // contract. Mirrors the peer
8077 // `behavior::validate_rejects_case_folded_extension` (c97815a) and
8078 // `upgrade::tests::state_change_rejects_case_folded_extension_script`
8079 // (33cc830) sweeps.
8080 for relpath in [
8081 "lib/demo.LISP",
8082 "lib/demo.Lisp",
8083 "lib/demo.LiSp",
8084 "lib/demo.lISP",
8085 ] {
8086 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8087 let err = c.validate_code_paths().unwrap_err();
8088 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8089 panic!("expected CodePathNonLispExtension for {relpath:?}, got {err:?}");
8090 };
8091 assert_eq!(slot, ":bibliotecas");
8092 assert_eq!(path, PathBuf::from(relpath));
8093 }
8094 }
8095
8096 #[test]
8097 fn validate_code_paths_accepts_canonical_lisp_shapes() {
8098 // Positive-control sweep through every canonical authoring shape
8099 // every in-tree fixture and the `Caixa::template` scaffold use.
8100 // Mirrors the peer `behavior::validate_accepts_canonical_lisp_paths`
8101 // (c97815a) and the lifted predicate's own
8102 // `is_lisp_extension_accepts_canonical_shapes` sweep in render.rs
8103 // (33cc830).
8104 for relpath in [
8105 "lib/demo.lisp",
8106 "lib/handlers.lisp",
8107 "lib/migrations/v01-to-v02.lisp",
8108 "demo.lisp",
8109 "a.lisp",
8110 "./lib/demo.lisp",
8111 "lib/./handlers.lisp",
8112 "lib/migrations/v.0.1.lisp",
8113 ] {
8114 let c = caixa_with_code_paths(vec![relpath], vec![], vec![]);
8115 c.validate_code_paths()
8116 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8117 }
8118 }
8119
8120 #[test]
8121 fn validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos() {
8122 // The file-type gate is per-slot — only `:bibliotecas` carries the
8123 // tatara-lisp-source contract. An extensionless `:exe` entry
8124 // (`exe/demo`) and a `.computeunit.yaml` `:servicos` entry are the
8125 // canonical shapes every in-tree fixture uses, and must continue
8126 // to pass validate. Pins that a future tightening that broadens
8127 // the `.lisp` gate to either axis surfaces as a test failure
8128 // rather than as a silent breaking change to existing valid
8129 // manifests.
8130 let c = caixa_with_code_paths(
8131 vec![],
8132 vec!["exe/demo", "exe/tool"],
8133 vec!["servicos/demo.computeunit.yaml"],
8134 );
8135 c.validate_code_paths().unwrap();
8136 }
8137
8138 #[test]
8139 fn validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension() {
8140 // Cross-arm precedence pin: a `:bibliotecas` entry that is *both*
8141 // sandbox-escaping and non-`.lisp` surfaces the more fundamental
8142 // sandbox-shape diagnostic first (the `.lisp` remediation would
8143 // be misleading when the offending path can never resolve under
8144 // the caixa root anyway). Mirrors the peer
8145 // `EmptyPath` → `AbsolutePath` → `ParentEscape` → `NonLispExtension`
8146 // ordering on `:behavior :on-*` (c97815a) and `EmptyScript` →
8147 // `AbsoluteScript` → `ParentEscapeScript` → `NonLispExtensionScript`
8148 // on `:upgrade-from :state-change :script` (33cc830).
8149 //
8150 // Empty wins (the strictly-smaller-scope structural arm).
8151 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
8152 assert!(
8153 matches!(
8154 c.validate_code_paths().unwrap_err(),
8155 ManifestError::CodePathEmpty {
8156 slot: ":bibliotecas"
8157 }
8158 ),
8159 "empty must win over non-lisp-extension",
8160 );
8161 // Absolute wins (the path can't resolve under the caixa root).
8162 let c = caixa_with_code_paths(vec!["/etc/passwd"], vec![], vec![]);
8163 let err = c.validate_code_paths().unwrap_err();
8164 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8165 panic!("absolute must win over non-lisp-extension, got {err:?}");
8166 };
8167 assert_eq!(slot, ":bibliotecas");
8168 // ParentEscape wins (the path escapes the caixa root).
8169 let c = caixa_with_code_paths(vec!["../sibling/x.txt"], vec![], vec![]);
8170 let err = c.validate_code_paths().unwrap_err();
8171 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8172 panic!("parent-escape must win over non-lisp-extension, got {err:?}");
8173 };
8174 assert_eq!(slot, ":bibliotecas");
8175 }
8176
8177 #[test]
8178 fn validate_code_paths_non_lisp_extension_precedes_duplicate() {
8179 // Within-slot precedence pin: the per-entry file-type shape gate
8180 // fires before the cross-entry duplicate gate, so the narrower
8181 // structural defect dominates the uniqueness diagnostic. A
8182 // `("lib/x.txt" "lib/x.txt")` shape surfaces
8183 // `CodePathNonLispExtension` on the first entry rather than
8184 // `CodePathDuplicate` on the pair — same posture every per-entry
8185 // shape-gate-precedes-duplicate cascade follows on this surface
8186 // (the empty / absolute / parent-escape arms already precede the
8187 // duplicate arm; the lifted file-type arm joins that set).
8188 let c = caixa_with_code_paths(vec!["lib/x.txt", "lib/x.txt"], vec![], vec![]);
8189 let err = c.validate_code_paths().unwrap_err();
8190 let ManifestError::CodePathNonLispExtension { slot, path } = err else {
8191 panic!("expected CodePathNonLispExtension, got {err:?}");
8192 };
8193 assert_eq!(slot, ":bibliotecas");
8194 assert_eq!(path, PathBuf::from("lib/x.txt"));
8195 }
8196
8197 #[test]
8198 fn validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path() {
8199 // Diagnostic-shape pin (peer with
8200 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`
8201 // on the sandbox-shape arms and
8202 // `validate_code_paths_duplicate_diagnostic_carries_offending_slot_and_path`
8203 // on the duplicate arm): the file-type-arm Display surfaces both
8204 // the offending `:slot` tag, the offending path verbatim, and the
8205 // expected `.lisp` extension named in the remediation text, so a
8206 // `feira lint` run can render the diagnostic without re-parsing.
8207 let c = caixa_with_code_paths(vec!["lib/demo.rs"], vec![], vec![]);
8208 let rendered = c.validate_code_paths().unwrap_err().to_string();
8209 assert!(
8210 rendered.contains(":bibliotecas"),
8211 "diagnostic must name the offending slot: {rendered}",
8212 );
8213 assert!(
8214 rendered.contains("lib/demo.rs"),
8215 "diagnostic must quote the offending path: {rendered}",
8216 );
8217 assert!(
8218 rendered.contains(".lisp"),
8219 "diagnostic must name the expected extension: {rendered}",
8220 );
8221 }
8222
8223 // ── validate_code_paths — `.computeunit.yaml` compound-suffix gate on :servicos ──
8224 //
8225 // The lifted [`crate::render::is_computeunit_yaml_extension`] predicate
8226 // now gates `:servicos` entries on the ComputeUnit-CR YAML file-type
8227 // contract. The peer caixa-helm / caixa-flux renderers consume each
8228 // `:servicos` entry through `serde_yaml::from_str` as a typed
8229 // `ComputeUnit` CR — same downstream-consumer-shape lift as the peer
8230 // `:bibliotecas` `.lisp` gate (64772a9), here on the compound-suffix
8231 // axis `Path::extension` can't express on its own.
8232
8233 #[test]
8234 fn validate_code_paths_rejects_no_extension_servicos_entry() {
8235 // Canonical "I dragged the wrong file from the workspace tree"
8236 // footgun on the Servico axis. Without the gate the peer
8237 // caixa-helm / caixa-flux renderers hand the extensionless path
8238 // to `serde_yaml::from_str` and fail with a parser-shaped
8239 // diagnostic far from the source caixa.lisp, with no field
8240 // naming the offending `:servicos` entry.
8241 for relpath in ["servicos/demo", "demo", "servicos/sub/nested"] {
8242 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8243 let err = c.validate_code_paths().unwrap_err();
8244 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8245 panic!(
8246 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8247 got {err:?}"
8248 );
8249 };
8250 assert_eq!(slot, ":servicos");
8251 assert_eq!(path, PathBuf::from(relpath));
8252 }
8253 }
8254
8255 #[test]
8256 fn validate_code_paths_rejects_wrong_extension_servicos_entry() {
8257 // Wrong-extension sweep across common authoring footguns on the
8258 // Servico axis. Bare `.yaml` is the canonical "I forgot the
8259 // `.computeunit` segment" typo; the off-by-one-segment shapes
8260 // (`.computeunit-yaml` / `.computeunit_yaml`) silently pass the
8261 // bare `Path::extension` view but mismatch the typed compound
8262 // suffix the renderers' `serde_yaml::from_str` consumer demands.
8263 // Same sweep-posture as the peer
8264 // `validate_code_paths_rejects_wrong_extension_bibliotecas_entry`
8265 // (64772a9) on the sibling tatara-lisp-source axis.
8266 for relpath in [
8267 "servicos/demo.yaml",
8268 "servicos/demo.yml",
8269 "servicos/demo.json",
8270 "servicos/demo.toml",
8271 "servicos/demo.txt",
8272 "servicos/demo.computeunit.yaml.bak",
8273 "servicos/demo.computeunit.yam",
8274 "servicos/demo.computeunit",
8275 "servicos/demo-computeunit.yaml",
8276 "servicos/demo_computeunit.yaml",
8277 ] {
8278 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8279 let err = c.validate_code_paths().unwrap_err();
8280 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8281 panic!(
8282 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8283 got {err:?}"
8284 );
8285 };
8286 assert_eq!(slot, ":servicos");
8287 assert_eq!(path, PathBuf::from(relpath));
8288 }
8289 }
8290
8291 #[test]
8292 fn validate_code_paths_rejects_case_folded_extension_servicos_entry() {
8293 // Case-sensitivity sweep — pins the strict lowercase
8294 // `.computeunit.yaml` contract. A case-folded shape that the
8295 // layout's existence check would (case-insensitively, on
8296 // case-insensitive volumes) match the on-disk file still
8297 // mismatches the canonical form the codec emits, breaking the
8298 // THEORY.md §V.2.7 render-determinism contract. Mirrors the peer
8299 // `validate_code_paths_rejects_case_folded_extension_bibliotecas_entry`
8300 // (64772a9) sweep on the sibling tatara-lisp-source axis.
8301 for relpath in [
8302 "servicos/demo.ComputeUnit.yaml",
8303 "servicos/demo.COMPUTEUNIT.yaml",
8304 "servicos/demo.computeunit.YAML",
8305 "servicos/demo.computeunit.Yaml",
8306 "servicos/demo.COMPUTEUNIT.YAML",
8307 ] {
8308 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8309 let err = c.validate_code_paths().unwrap_err();
8310 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8311 panic!(
8312 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8313 got {err:?}"
8314 );
8315 };
8316 assert_eq!(slot, ":servicos");
8317 assert_eq!(path, PathBuf::from(relpath));
8318 }
8319 }
8320
8321 #[test]
8322 fn validate_code_paths_rejects_empty_stem_servicos_entry() {
8323 // Degenerate hidden-file shape: a file name exactly equal to the
8324 // suffix (`.computeunit.yaml` — no stem preceding the suffix) is
8325 // the structural "Servico declared with no identity" footgun.
8326 // The substrate identifies each ComputeUnit by the file-stem
8327 // segment that precedes `.computeunit.yaml` (the rendered
8328 // `lareira-<stem>` Helm chart, the per-Servico `metadata.name`,
8329 // the M3 `:contratos` membership lookup), so an empty stem
8330 // leaves the Servico unidentifiable. Pinned at the typed-axis
8331 // level so a future regression that drops the `name.len() >
8332 // SUFFIX.len()` bound at the predicate surfaces here, not
8333 // piecemeal as a `lareira-` chart-name collision at render time.
8334 for relpath in ["servicos/.computeunit.yaml"] {
8335 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8336 let err = c.validate_code_paths().unwrap_err();
8337 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8338 panic!(
8339 "expected CodePathNonComputeUnitYamlExtension for {relpath:?}, \
8340 got {err:?}"
8341 );
8342 };
8343 assert_eq!(slot, ":servicos");
8344 assert_eq!(path, PathBuf::from(relpath));
8345 }
8346 }
8347
8348 #[test]
8349 fn validate_code_paths_accepts_canonical_computeunit_yaml_shapes() {
8350 // Positive-control sweep through every canonical authoring shape
8351 // every in-tree fixture and the `Caixa::template` scaffold use.
8352 // Mirrors the peer
8353 // `validate_code_paths_accepts_canonical_lisp_shapes` (64772a9)
8354 // and the lifted predicate's own
8355 // `computeunit_yaml_extension_accepts_canonical_shapes` sweep in
8356 // render.rs.
8357 for relpath in [
8358 "servicos/demo.computeunit.yaml",
8359 "servicos/hello-rio.computeunit.yaml",
8360 "servicos/my-service.computeunit.yaml",
8361 "servicos/a.computeunit.yaml",
8362 "./servicos/demo.computeunit.yaml",
8363 "servicos/./demo.computeunit.yaml",
8364 "servicos/sub/nested.computeunit.yaml",
8365 "servicos/v0.1.computeunit.yaml",
8366 ] {
8367 let c = caixa_with_code_paths(vec![], vec![], vec![relpath]);
8368 c.validate_code_paths()
8369 .unwrap_or_else(|e| panic!("canonical shape {relpath:?} must pass, got {e:?}"));
8370 }
8371 }
8372
8373 #[test]
8374 fn validate_code_paths_non_computeunit_yaml_extension_does_not_fire_on_bibliotecas_or_exe() {
8375 // The file-type gate is per-slot — only `:servicos` carries the
8376 // ComputeUnit-CR YAML contract. A canonical `.lisp` `:bibliotecas`
8377 // entry and an extensionless `:exe` entry are the canonical
8378 // shapes every in-tree fixture uses, and must continue to pass
8379 // validate. Peer of
8380 // `validate_code_paths_non_lisp_extension_does_not_fire_on_exe_or_servicos`
8381 // (64772a9) — together pin that the typed
8382 // [`CodePathFileType`] dispatch is exhaustively per-slot, with no
8383 // cross-axis leakage in either direction.
8384 let c = caixa_with_code_paths(
8385 vec!["lib/demo.lisp"],
8386 vec!["exe/demo", "exe/tool"],
8387 vec!["servicos/demo.computeunit.yaml"],
8388 );
8389 c.validate_code_paths().unwrap();
8390 }
8391
8392 #[test]
8393 fn validate_code_paths_sandbox_shape_arms_precede_non_computeunit_yaml_extension() {
8394 // Cross-arm precedence pin: a `:servicos` entry that is *both*
8395 // sandbox-escaping and wrong-extension surfaces the more
8396 // fundamental sandbox-shape diagnostic first (the
8397 // `.computeunit.yaml` remediation would be misleading when the
8398 // offending path can never resolve under the caixa root
8399 // anyway). Mirrors the peer
8400 // `validate_code_paths_sandbox_shape_arms_precede_non_lisp_extension`
8401 // (64772a9) ordering on the sibling `:bibliotecas` axis and the
8402 // peer `EmptyPath` → `AbsolutePath` → `ParentEscape` →
8403 // `NonComputeUnitYamlExtension` arm-ordering the dispatch
8404 // table establishes.
8405 //
8406 // Empty wins (the strictly-smaller-scope structural arm).
8407 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
8408 assert!(
8409 matches!(
8410 c.validate_code_paths().unwrap_err(),
8411 ManifestError::CodePathEmpty { slot: ":servicos" }
8412 ),
8413 "empty must win over non-computeunit-yaml-extension",
8414 );
8415 // Absolute wins (the path can't resolve under the caixa root).
8416 let c = caixa_with_code_paths(vec![], vec![], vec!["/etc/foo.yaml"]);
8417 let err = c.validate_code_paths().unwrap_err();
8418 let ManifestError::CodePathAbsolute { slot, .. } = err else {
8419 panic!("absolute must win over non-computeunit-yaml-extension, got {err:?}");
8420 };
8421 assert_eq!(slot, ":servicos");
8422 // ParentEscape wins (the path escapes the caixa root).
8423 let c = caixa_with_code_paths(vec![], vec![], vec!["../sibling/x.yaml"]);
8424 let err = c.validate_code_paths().unwrap_err();
8425 let ManifestError::CodePathParentEscape { slot, .. } = err else {
8426 panic!("parent-escape must win over non-computeunit-yaml-extension, got {err:?}");
8427 };
8428 assert_eq!(slot, ":servicos");
8429 }
8430
8431 #[test]
8432 fn validate_code_paths_non_computeunit_yaml_extension_precedes_duplicate() {
8433 // Within-slot precedence pin: the per-entry file-type shape gate
8434 // fires before the cross-entry duplicate gate, so the narrower
8435 // structural defect dominates the uniqueness diagnostic. A
8436 // `("servicos/x.yaml" "servicos/x.yaml")` shape surfaces
8437 // `CodePathNonComputeUnitYamlExtension` on the first entry
8438 // rather than `CodePathDuplicate` on the pair — same posture
8439 // every per-entry shape-gate-precedes-duplicate cascade follows
8440 // on this surface, peer of the 64772a9 `:bibliotecas`
8441 // `("lib/x.txt" "lib/x.txt")` ordering.
8442 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/x.yaml", "servicos/x.yaml"]);
8443 let err = c.validate_code_paths().unwrap_err();
8444 let ManifestError::CodePathNonComputeUnitYamlExtension { slot, path } = err else {
8445 panic!("expected CodePathNonComputeUnitYamlExtension, got {err:?}");
8446 };
8447 assert_eq!(slot, ":servicos");
8448 assert_eq!(path, PathBuf::from("servicos/x.yaml"));
8449 }
8450
8451 #[test]
8452 fn validate_code_paths_non_computeunit_yaml_extension_diagnostic_carries_offending_slot_and_path()
8453 {
8454 // Diagnostic-shape pin (peer with
8455 // `validate_code_paths_non_lisp_extension_diagnostic_carries_offending_slot_and_path`
8456 // on the sibling tatara-lisp-source axis): the file-type-arm
8457 // Display surfaces both the offending `:slot` tag, the
8458 // offending path verbatim, and the expected
8459 // `.computeunit.yaml` compound suffix named in the remediation
8460 // text, so a `feira lint` run can render the diagnostic without
8461 // re-parsing.
8462 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.yaml"]);
8463 let rendered = c.validate_code_paths().unwrap_err().to_string();
8464 assert!(
8465 rendered.contains(":servicos"),
8466 "diagnostic must name the offending slot: {rendered}",
8467 );
8468 assert!(
8469 rendered.contains("servicos/demo.yaml"),
8470 "diagnostic must quote the offending path: {rendered}",
8471 );
8472 assert!(
8473 rendered.contains(".computeunit.yaml"),
8474 "diagnostic must name the expected compound suffix: {rendered}",
8475 );
8476 }
8477
8478 // ── validate_etiquetas — universal-axis registry-search-tag shape ──
8479
8480 fn caixa_with_etiquetas(etiquetas: Vec<&str>) -> Caixa {
8481 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8482 c.etiquetas = etiquetas.into_iter().map(String::from).collect();
8483 c
8484 }
8485
8486 #[test]
8487 fn validate_etiquetas_accepts_empty_list() {
8488 // The empty-list identity: every caixa with no declared tags
8489 // trivially passes — `Caixa::template` emits `:etiquetas ()`,
8490 // so the gate is non-disruptive against every existing manifest.
8491 let c = caixa_with_etiquetas(vec![]);
8492 c.validate_etiquetas().unwrap();
8493 }
8494
8495 #[test]
8496 fn validate_etiquetas_accepts_canonical_forms() {
8497 // Positive control sweep: a canonical-shaped non-empty distinct
8498 // tag list passes, mirroring the example checkout-aplicacao
8499 // (`:etiquetas ("example" "aplicacao" "mesh" "ecommerce" "demo")`)
8500 // and the hello-rio fixture (`("hello-world" "wasm" "rust")`).
8501 let c = caixa_with_etiquetas(vec!["example", "aplicacao", "mesh", "ecommerce", "demo"]);
8502 c.validate_etiquetas().unwrap();
8503 }
8504
8505 #[test]
8506 fn validate_etiquetas_rejects_empty_entry() {
8507 // Canonical paste-from-blank-doc footgun. Without the gate the
8508 // empty entry rendered as `keywords: [""]` in `Chart.yaml`, a
8509 // no-op tag indexing nothing in the future caixa-registry.
8510 let c = caixa_with_etiquetas(vec![""]);
8511 let err = c.validate_etiquetas().unwrap_err();
8512 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8513 }
8514
8515 #[test]
8516 fn validate_etiquetas_rejects_duplicate_entry() {
8517 // Canonical copy-paste-the-wrong-tag footgun. Without the gate
8518 // the duplicate was silently dedup'd by caixa-helm's BTreeSet
8519 // collect at chart render — a "second wins / one silently
8520 // disappears" shape divergent from every peer typed-graph set
8521 // gate. The duplicate-arm names the offending tag verbatim.
8522 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8523 let err = c.validate_etiquetas().unwrap_err();
8524 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8525 panic!("expected EtiquetaDuplicate, got {err:?}");
8526 };
8527 assert_eq!(etiqueta, "demo");
8528 }
8529
8530 #[test]
8531 fn validate_etiquetas_empty_takes_precedence_over_duplicate() {
8532 // Empty-first cascade pin: `("" "demo" "demo")` surfaces
8533 // `EtiquetaEmpty` not `EtiquetaDuplicate` — the narrower
8534 // structural "this entry has no value" defect dominates the
8535 // cross-entry uniqueness diagnostic. Mirrors the peer
8536 // empty-before-duplicate cascades on `:caracteristicas`
8537 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8538 // fc3b4d5) and `:membros :caixa` (`MembroCaixaEmpty` before
8539 // `MembroDuplicate`).
8540 let c = caixa_with_etiquetas(vec!["", "demo", "demo"]);
8541 let err = c.validate_etiquetas().unwrap_err();
8542 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8543 }
8544
8545 #[test]
8546 fn validate_etiquetas_duplicate_reports_first_collision() {
8547 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8548 // duplicate (the lexicographically-earliest offending position
8549 // — the second `"a"` at index 2 collides with the first `"a"`
8550 // at index 0), not the later `"b"` collision at index 3,
8551 // peer with every other first-collision diagnostic posture on
8552 // this surface (`validate_load_singularity_reports_first_collision`,
8553 // `validate_cleanup_singularity_reports_first_collision`).
8554 let c = caixa_with_etiquetas(vec!["a", "b", "a", "b"]);
8555 let err = c.validate_etiquetas().unwrap_err();
8556 let ManifestError::EtiquetaDuplicate { etiqueta } = err else {
8557 panic!("expected EtiquetaDuplicate, got {err:?}");
8558 };
8559 assert_eq!(etiqueta, "a");
8560 }
8561
8562 #[test]
8563 fn validate_etiquetas_case_sensitive() {
8564 // Case-sensitivity pin: `("Foo" "foo")` is two distinct entries,
8565 // mirroring the peer `:membros :caixa` / `:children :caixa`
8566 // exact-string-match discipline. The shape gate this routine
8567 // landed (`is_chart_keyword_shape`, Cargo crates.io keyword
8568 // grammar) accepts mixed case — crates.io's keyword rule is
8569 // "case-insensitive" at the index layer but admits mixed case
8570 // at the entry layer (the canonical Helm chart `keywords:`
8571 // shape is lowercase by convention, but the grammar admits
8572 // uppercase). Case-sensitivity at the duplicate-set layer
8573 // remains structural — two distinct strings are two distinct
8574 // entries.
8575 let c = caixa_with_etiquetas(vec!["Foo", "foo"]);
8576 c.validate_etiquetas().unwrap();
8577 }
8578
8579 #[test]
8580 fn validate_etiquetas_diagnostic_carries_offending_tag() {
8581 // Diagnostic-shape pin (peer with
8582 // `validate_code_paths_diagnostic_carries_offending_slot_and_path`):
8583 // the error's Display surfaces the offending tag verbatim, so a
8584 // `feira lint` run can render the diagnostic without re-parsing
8585 // and the author can grep their caixa.lisp for the offending
8586 // value.
8587 let c = caixa_with_etiquetas(vec!["demo", "demo"]);
8588 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8589 assert!(
8590 rendered.contains(":etiquetas"),
8591 "diagnostic must name the offending slot: {rendered}",
8592 );
8593 assert!(
8594 rendered.contains("demo"),
8595 "diagnostic must quote the offending tag: {rendered}",
8596 );
8597 }
8598
8599 #[test]
8600 fn validate_etiquetas_rejects_leading_whitespace_entry() {
8601 // Canonical paste-from-aligned-doc footgun. Without the shape
8602 // gate `" mesh"` silently passed validate and landed as a
8603 // YAML plain-style scalar with leading whitespace in the
8604 // rendered Chart.yaml `keywords:` array — every YAML 1.2
8605 // dumper trims leading whitespace from plain-style scalars,
8606 // so the authored space round-tripped inconsistently back
8607 // through `caixa.lisp`. Mirrors the peer
8608 // `validate_autores_rejects_leading_whitespace_entry`.
8609 let c = caixa_with_etiquetas(vec![" mesh"]);
8610 let err = c.validate_etiquetas().unwrap_err();
8611 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8612 panic!("expected EtiquetaInvalid, got {err:?}");
8613 };
8614 assert_eq!(etiqueta, " mesh");
8615 assert!(reason.contains("whitespace"), "got: {reason}");
8616 }
8617
8618 #[test]
8619 fn validate_etiquetas_rejects_embedded_newline_entry() {
8620 // Canonical paste-from-multiline-doc footgun — the author
8621 // pasted a multi-tag block into one `:etiquetas` entry
8622 // instead of splitting into one entry per tag. Without the
8623 // shape gate `"mesh\nhttp"` silently passed validate and
8624 // landed as a YAML-illegal multi-line scalar in the rendered
8625 // Chart.yaml `keywords:` array.
8626 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8627 let err = c.validate_etiquetas().unwrap_err();
8628 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8629 panic!("expected EtiquetaInvalid, got {err:?}");
8630 };
8631 assert_eq!(etiqueta, "mesh\nhttp");
8632 assert!(reason.contains("newline"), "got: {reason}");
8633 }
8634
8635 #[test]
8636 fn validate_etiquetas_rejects_embedded_comma_entry() {
8637 // Canonical CSV-list-separator-confusion footgun: the author
8638 // confused the CSV-style separator convention with the
8639 // `:etiquetas` list grammar. Without the shape gate
8640 // `"mesh,http,grpc"` silently passed validate and landed as a
8641 // single malformed search tag in the rendered Chart.yaml
8642 // `keywords:` array — Artifact Hub's keyword index would
8643 // either silently drop the tag or index it as
8644 // `mesh,http,grpc` instead of three separate tags.
8645 let c = caixa_with_etiquetas(vec!["mesh,http,grpc"]);
8646 let err = c.validate_etiquetas().unwrap_err();
8647 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8648 panic!("expected EtiquetaInvalid, got {err:?}");
8649 };
8650 assert_eq!(etiqueta, "mesh,http,grpc");
8651 assert!(reason.contains('`'), "got: {reason}");
8652 assert!(reason.contains(','), "got: {reason}");
8653 }
8654
8655 #[test]
8656 fn validate_etiquetas_rejects_embedded_slash_entry() {
8657 // Canonical path-separator-confusion footgun: the author
8658 // confused namespace-path notation with the keyword grammar.
8659 let c = caixa_with_etiquetas(vec!["caixa/servico"]);
8660 let err = c.validate_etiquetas().unwrap_err();
8661 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8662 panic!("expected EtiquetaInvalid, got {err:?}");
8663 };
8664 assert_eq!(etiqueta, "caixa/servico");
8665 assert!(reason.contains('/'), "got: {reason}");
8666 }
8667
8668 #[test]
8669 fn validate_etiquetas_rejects_leading_digit_entry() {
8670 // Canonical paste-from-numbered-list footgun: the author
8671 // copied `1. mesh` from a numbered doc and the `1` leaked
8672 // into the tag.
8673 let c = caixa_with_etiquetas(vec!["1mesh"]);
8674 let err = c.validate_etiquetas().unwrap_err();
8675 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8676 panic!("expected EtiquetaInvalid, got {err:?}");
8677 };
8678 assert_eq!(etiqueta, "1mesh");
8679 assert!(reason.contains("digit"), "got: {reason}");
8680 }
8681
8682 #[test]
8683 fn validate_etiquetas_rejects_leading_hyphen_entry() {
8684 // Canonical kebab-leak footgun.
8685 let c = caixa_with_etiquetas(vec!["-foo"]);
8686 let err = c.validate_etiquetas().unwrap_err();
8687 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8688 panic!("expected EtiquetaInvalid, got {err:?}");
8689 };
8690 assert_eq!(etiqueta, "-foo");
8691 assert!(reason.contains('-'), "got: {reason}");
8692 }
8693
8694 #[test]
8695 fn validate_etiquetas_rejects_non_ascii_entry() {
8696 // Canonical paste-from-Unicode-doc footgun. Every legitimate
8697 // search tag is strict ASCII; raw non-ASCII silently
8698 // round-trips inconsistently across NFC/NFD normalization on
8699 // APFS / case-folding filesystems and breaks the Artifact Hub
8700 // keyword search index lookup.
8701 let c = caixa_with_etiquetas(vec!["café"]);
8702 let err = c.validate_etiquetas().unwrap_err();
8703 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8704 panic!("expected EtiquetaInvalid, got {err:?}");
8705 };
8706 assert_eq!(etiqueta, "café");
8707 assert!(reason.contains("non-ASCII"), "got: {reason}");
8708 }
8709
8710 #[test]
8711 fn validate_etiquetas_rejects_period_entry() {
8712 // Canonical namespace-confusion / version-suffix footgun
8713 // (`"http.1"` / `"v1.0"`): Cargo's crates.io keyword grammar
8714 // excludes `.` from the continuation set even though the
8715 // sibling `:caracteristicas` axis (Cargo's feature-name
8716 // grammar) admits it. Tighter than the sibling axis, peer
8717 // with Cargo's own crates.io keyword shape.
8718 let c = caixa_with_etiquetas(vec!["http.1"]);
8719 let err = c.validate_etiquetas().unwrap_err();
8720 let ManifestError::EtiquetaInvalid { etiqueta, reason } = err else {
8721 panic!("expected EtiquetaInvalid, got {err:?}");
8722 };
8723 assert_eq!(etiqueta, "http.1");
8724 assert!(reason.contains('.'), "got: {reason}");
8725 }
8726
8727 #[test]
8728 fn validate_etiquetas_empty_takes_precedence_over_shape() {
8729 // Per-entry empty-first cascade pin: an entry that is both
8730 // empty *and* shape-invalid surfaces `EtiquetaEmpty` (the
8731 // narrower "this entry has no value" structural defect
8732 // dominates the broader shape-predicate diagnostic). The
8733 // empty arm fires before the shape predicate is consulted,
8734 // mirroring the peer `validate_autores_empty_takes_precedence_over_shape`
8735 // cascade established on the sibling universal-axis Vec<String>
8736 // surface.
8737 let c = caixa_with_etiquetas(vec![""]);
8738 let err = c.validate_etiquetas().unwrap_err();
8739 assert!(matches!(err, ManifestError::EtiquetaEmpty), "got {err:?}",);
8740 }
8741
8742 #[test]
8743 fn validate_etiquetas_shape_takes_precedence_over_duplicate() {
8744 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
8745 // entry that is malformed surfaces `EtiquetaInvalid` even when
8746 // a later entry would have collided on duplicate. The
8747 // per-entry shape arm fires inside the same loop iteration as
8748 // the empty arm, before the seen-set insert at end-of-iteration
8749 // — structural per-entry defects dominate the cross-entry
8750 // uniqueness diagnostic. Mirrors the peer
8751 // `validate_autores_shape_takes_precedence_over_duplicate`.
8752 let c = caixa_with_etiquetas(vec!["mesh\nhttp", "mesh\nhttp"]);
8753 let err = c.validate_etiquetas().unwrap_err();
8754 assert!(
8755 matches!(err, ManifestError::EtiquetaInvalid { .. }),
8756 "got {err:?}",
8757 );
8758 }
8759
8760 #[test]
8761 fn validate_etiquetas_invalid_diagnostic_names_offending_slot_and_value() {
8762 // Diagnostic-shape pin on the new shape arm (peer with
8763 // `validate_autores_invalid_diagnostic_names_offending_slot_and_value`):
8764 // the rendered Display surfaces both the offending slot name
8765 // and the offending value verbatim, so a `feira lint` run
8766 // points the author at the exact `:etiquetas` entry to fix.
8767 let c = caixa_with_etiquetas(vec!["mesh\nhttp"]);
8768 let rendered = c.validate_etiquetas().unwrap_err().to_string();
8769 assert!(
8770 rendered.contains(":etiquetas"),
8771 "diagnostic must name the offending slot: {rendered}",
8772 );
8773 assert!(
8774 rendered.contains("mesh\\nhttp"),
8775 "diagnostic must quote the offending value (debug-escaped): {rendered}",
8776 );
8777 }
8778
8779 #[test]
8780 fn validate_etiquetas_rejects_at_21_byte_boundary() {
8781 // The 20-byte cap pin — boundary-exceeding case rejected,
8782 // boundary-accepting case passes. Mirrors the peer
8783 // `chart_keyword_shape_rejects_at_21_byte_boundary` substrate-
8784 // side pin, surfaced at the per-axis caller so the cap
8785 // propagates through validate end-to-end. Constructed as a
8786 // single all-`a` token so only the cap arm fires.
8787 let max_ok = "a".repeat(20);
8788 let c = caixa_with_etiquetas(vec![max_ok.as_str()]);
8789 c.validate_etiquetas().unwrap();
8790 let too_long = "a".repeat(21);
8791 let c = caixa_with_etiquetas(vec![too_long.as_str()]);
8792 let err = c.validate_etiquetas().unwrap_err();
8793 let ManifestError::EtiquetaInvalid { reason, .. } = err else {
8794 panic!("expected EtiquetaInvalid, got {err:?}");
8795 };
8796 assert!(reason.contains("20"), "got: {reason}");
8797 assert!(reason.contains("21"), "got: {reason}");
8798 }
8799
8800 #[test]
8801 fn validate_etiquetas_accepts_canonical_shaped_forms() {
8802 // Positive control sweep: every canonical-shaped tag from the
8803 // hello-rio / checkout-aplicacao / pangea-tatara-akeyless
8804 // example fixtures plus the substrate-fixed tags caixa-helm
8805 // unions in at chart render. Drift between this list and the
8806 // substrate-side `chart_keyword_shape_accepts_canonical_forms`
8807 // sweep surfaces here — one source of truth for the rule.
8808 let c = caixa_with_etiquetas(vec![
8809 "example",
8810 "aplicacao",
8811 "mesh",
8812 "ecommerce",
8813 "demo",
8814 "infrastructure",
8815 "aws",
8816 "akeyless",
8817 "pangea-native",
8818 "hello-world",
8819 "wasm",
8820 "rust",
8821 "tatara-lisp",
8822 "caixa-servico",
8823 "lareira",
8824 ]);
8825 c.validate_etiquetas().unwrap();
8826 }
8827
8828 // ── validate_autores — universal-axis maintainer shape ────────────
8829
8830 fn caixa_with_autores(autores: Vec<&str>) -> Caixa {
8831 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
8832 c.autores = autores.into_iter().map(String::from).collect();
8833 c
8834 }
8835
8836 #[test]
8837 fn validate_autores_accepts_empty_list() {
8838 // The empty-list identity: `Caixa::template` emits `:autores ()`,
8839 // so the gate is non-disruptive against every existing manifest.
8840 let c = caixa_with_autores(vec![]);
8841 c.validate_autores().unwrap();
8842 }
8843
8844 #[test]
8845 fn validate_autores_accepts_canonical_forms() {
8846 // Positive control sweep: every canonical-shaped non-empty
8847 // distinct maintainer list passes — the hello-rio / checkout-
8848 // aplicacao fixtures' `:autores ("pleme-io")` shape, plus the
8849 // multi-author shape downstream packaging surfaces emit.
8850 let c = caixa_with_autores(vec!["pleme-io"]);
8851 c.validate_autores().unwrap();
8852 let c = caixa_with_autores(vec!["alice <alice@example.com>", "bob <bob@example.com>"]);
8853 c.validate_autores().unwrap();
8854 }
8855
8856 #[test]
8857 fn validate_autores_rejects_empty_entry() {
8858 // Canonical paste-from-blank-doc footgun. Without the gate the
8859 // empty entry rendered as `maintainers: [{name: "", email: null}]`
8860 // in `Chart.yaml`, a no-op maintainer the substrate cannot route
8861 // to.
8862 let c = caixa_with_autores(vec![""]);
8863 let err = c.validate_autores().unwrap_err();
8864 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8865 }
8866
8867 #[test]
8868 fn validate_autores_rejects_duplicate_entry() {
8869 // Canonical copy-paste-the-wrong-author footgun. Unlike the
8870 // `:etiquetas` peer (caixa-helm's `BTreeSet` collect silently
8871 // dedups the rendered `keywords:` array), the `maintainers:`
8872 // rendering has *no* dedup — duplicates stack verbatim. The
8873 // duplicate-arm names the offending author verbatim.
8874 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8875 let err = c.validate_autores().unwrap_err();
8876 let ManifestError::AutorDuplicate { autor } = err else {
8877 panic!("expected AutorDuplicate, got {err:?}");
8878 };
8879 assert_eq!(autor, "pleme-io");
8880 }
8881
8882 #[test]
8883 fn validate_autores_empty_takes_precedence_over_duplicate() {
8884 // Empty-first cascade pin: `("" "pleme-io" "pleme-io")` surfaces
8885 // `AutorEmpty` not `AutorDuplicate` — the narrower structural
8886 // "this entry has no value" defect dominates the cross-entry
8887 // uniqueness diagnostic. Mirrors the peer empty-before-duplicate
8888 // cascades on `:etiquetas` (`EtiquetaEmpty` before
8889 // `EtiquetaDuplicate`, 360a499), `:caracteristicas`
8890 // (`CaracteristicaEmpty` before `CaracteristicaDuplicate`,
8891 // fc3b4d5), and `:membros :caixa` (`MembroCaixaEmpty` before
8892 // `MembroDuplicate`).
8893 let c = caixa_with_autores(vec!["", "pleme-io", "pleme-io"]);
8894 let err = c.validate_autores().unwrap_err();
8895 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
8896 }
8897
8898 #[test]
8899 fn validate_autores_duplicate_reports_first_collision() {
8900 // First-collision pin: `("a" "b" "a" "b")` surfaces the `"a"`
8901 // duplicate (the lexicographically-earliest offending position
8902 // — the second `"a"` at index 2 collides with the first `"a"`
8903 // at index 0), not the later `"b"` collision at index 3,
8904 // peer with every other first-collision diagnostic posture on
8905 // this surface.
8906 let c = caixa_with_autores(vec!["a", "b", "a", "b"]);
8907 let err = c.validate_autores().unwrap_err();
8908 let ManifestError::AutorDuplicate { autor } = err else {
8909 panic!("expected AutorDuplicate, got {err:?}");
8910 };
8911 assert_eq!(autor, "a");
8912 }
8913
8914 #[test]
8915 fn validate_autores_case_sensitive() {
8916 // Case-sensitivity pin: `("Pleme-io" "pleme-io")` is two distinct
8917 // entries, mirroring the peer `:etiquetas` / `:membros :caixa`
8918 // / `:children :caixa` exact-string-match discipline.
8919 let c = caixa_with_autores(vec!["Pleme-io", "pleme-io"]);
8920 c.validate_autores().unwrap();
8921 }
8922
8923 #[test]
8924 fn validate_autores_diagnostic_carries_offending_author() {
8925 // Diagnostic-shape pin (peer with
8926 // `validate_etiquetas_diagnostic_carries_offending_tag`): the
8927 // error's Display surfaces the offending author verbatim, so a
8928 // `feira lint` run can render the diagnostic without re-parsing
8929 // and the author can grep their caixa.lisp for the offending
8930 // value.
8931 let c = caixa_with_autores(vec!["pleme-io", "pleme-io"]);
8932 let rendered = c.validate_autores().unwrap_err().to_string();
8933 assert!(
8934 rendered.contains(":autores"),
8935 "diagnostic must name the offending slot: {rendered}",
8936 );
8937 assert!(
8938 rendered.contains("pleme-io"),
8939 "diagnostic must quote the offending author: {rendered}",
8940 );
8941 }
8942
8943 #[test]
8944 fn validate_autores_rejects_leading_whitespace_entry() {
8945 // Canonical paste-from-aligned-doc footgun. Without the shape
8946 // gate `" pleme-io"` silently passed validate and landed as a
8947 // YAML plain-style scalar with leading whitespace in the
8948 // rendered Chart.yaml `maintainers:` array — every YAML 1.2
8949 // dumper trims leading whitespace from plain-style scalars, so
8950 // the authored space round-tripped inconsistently back through
8951 // `caixa.lisp`. Mirrors the peer
8952 // `validate_descricao_rejects_leading_whitespace`.
8953 let c = caixa_with_autores(vec![" pleme-io"]);
8954 let err = c.validate_autores().unwrap_err();
8955 let ManifestError::AutorInvalid { autor, reason } = err else {
8956 panic!("expected AutorInvalid, got {err:?}");
8957 };
8958 assert_eq!(autor, " pleme-io");
8959 assert!(reason.contains("whitespace"), "got: {reason}");
8960 }
8961
8962 #[test]
8963 fn validate_autores_rejects_trailing_whitespace_entry() {
8964 // Canonical paste-from-doc footgun.
8965 let c = caixa_with_autores(vec!["pleme-io "]);
8966 let err = c.validate_autores().unwrap_err();
8967 let ManifestError::AutorInvalid { autor, reason } = err else {
8968 panic!("expected AutorInvalid, got {err:?}");
8969 };
8970 assert_eq!(autor, "pleme-io ");
8971 assert!(reason.contains("whitespace"), "got: {reason}");
8972 }
8973
8974 #[test]
8975 fn validate_autores_rejects_embedded_newline_entry() {
8976 // Canonical paste-from-multiline-doc footgun — the author
8977 // pasted a multi-line block of author records into one
8978 // `:autores` entry instead of splitting into one entry per
8979 // author. Without the shape gate `"alice\nbob"` silently
8980 // passed validate and landed as a YAML-illegal multi-line
8981 // scalar in the rendered Chart.yaml `maintainers:` array.
8982 let c = caixa_with_autores(vec!["alice\nbob"]);
8983 let err = c.validate_autores().unwrap_err();
8984 let ManifestError::AutorInvalid { autor, reason } = err else {
8985 panic!("expected AutorInvalid, got {err:?}");
8986 };
8987 assert_eq!(autor, "alice\nbob");
8988 assert!(reason.contains("newline"), "got: {reason}");
8989 }
8990
8991 #[test]
8992 fn validate_autores_rejects_embedded_carriage_return_entry() {
8993 // Canonical paste-from-Windows-CRLF-doc footgun.
8994 let c = caixa_with_autores(vec!["alice\rbob"]);
8995 let err = c.validate_autores().unwrap_err();
8996 let ManifestError::AutorInvalid { autor, reason } = err else {
8997 panic!("expected AutorInvalid, got {err:?}");
8998 };
8999 assert_eq!(autor, "alice\rbob");
9000 assert!(reason.contains("carriage return"), "got: {reason}");
9001 }
9002
9003 #[test]
9004 fn validate_autores_rejects_embedded_tab_entry() {
9005 // Canonical tab-from-aligned-doc footgun.
9006 let c = caixa_with_autores(vec!["Pleme\tContributors"]);
9007 let err = c.validate_autores().unwrap_err();
9008 let ManifestError::AutorInvalid { autor, reason } = err else {
9009 panic!("expected AutorInvalid, got {err:?}");
9010 };
9011 assert_eq!(autor, "Pleme\tContributors");
9012 assert!(reason.contains("tab"), "got: {reason}");
9013 }
9014
9015 #[test]
9016 fn validate_autores_rejects_embedded_control_bytes_entry() {
9017 // Paste-from-binary-blob footguns: NUL, BEL, ESC, DEL all
9018 // surface the same control-byte arm.
9019 for entry in [
9020 "alice\x00bob",
9021 "alice\x07bob",
9022 "alice\x1bbob",
9023 "alice\x7fbob",
9024 ] {
9025 let c = caixa_with_autores(vec![entry]);
9026 let err = c.validate_autores().unwrap_err();
9027 let ManifestError::AutorInvalid { autor, reason } = err else {
9028 panic!("expected AutorInvalid for {entry:?}, got {err:?}");
9029 };
9030 assert_eq!(autor, entry);
9031 assert!(
9032 reason.contains("control character"),
9033 "{entry:?} reason: {reason}",
9034 );
9035 }
9036 }
9037
9038 #[test]
9039 fn validate_autores_accepts_unicode_entry() {
9040 // Unicode positive control: realistic maintainer names carry
9041 // Unicode (`François`, `日本語`, `naïve`). The predicate must
9042 // round-trip Unicode losslessly, peer with the
9043 // `chart_maintainer_name_shape_accepts_unicode` substrate-side
9044 // sweep.
9045 let c = caixa_with_autores(vec![
9046 "François Dupont",
9047 "日本語の名前",
9048 "naïve <naive@example.com>",
9049 ]);
9050 c.validate_autores().unwrap();
9051 }
9052
9053 #[test]
9054 fn validate_autores_empty_takes_precedence_over_shape() {
9055 // Per-entry empty-first cascade pin: an entry that is both
9056 // empty *and* shape-invalid surfaces `AutorEmpty` (the narrower
9057 // "this entry has no value" structural defect dominates the
9058 // broader shape-predicate diagnostic). The empty arm fires
9059 // before the shape predicate is consulted, mirroring the peer
9060 // `validate_repositorio_empty_takes_precedence_over_shape`
9061 // cascade on the universal `Option<String>` siblings — and now
9062 // established on the Vec<String> per-entry surface.
9063 let c = caixa_with_autores(vec![""]);
9064 let err = c.validate_autores().unwrap_err();
9065 assert!(matches!(err, ManifestError::AutorEmpty), "got {err:?}",);
9066 }
9067
9068 #[test]
9069 fn validate_autores_shape_takes_precedence_over_duplicate() {
9070 // Per-entry shape-before-cross-entry-duplicate cascade pin: an
9071 // entry that is malformed surfaces `AutorInvalid` even when a
9072 // later entry would have collided on duplicate. The per-entry
9073 // shape arm fires inside the same loop iteration as the empty
9074 // arm, before the seen-set insert at end-of-iteration —
9075 // structural per-entry defects dominate the cross-entry
9076 // uniqueness diagnostic.
9077 let c = caixa_with_autores(vec!["alice\nbob", "alice\nbob"]);
9078 let err = c.validate_autores().unwrap_err();
9079 assert!(
9080 matches!(err, ManifestError::AutorInvalid { .. }),
9081 "got {err:?}",
9082 );
9083 }
9084
9085 #[test]
9086 fn validate_autores_invalid_diagnostic_names_offending_slot_and_value() {
9087 // Diagnostic-shape pin on the new shape arm (peer with
9088 // `validate_descricao_invalid_diagnostic_carries_offending_value`):
9089 // the rendered Display surfaces both the offending slot name
9090 // and the offending value verbatim, so a `feira lint` run
9091 // points the author at the exact `:autores` entry to fix.
9092 let c = caixa_with_autores(vec!["alice\nbob"]);
9093 let rendered = c.validate_autores().unwrap_err().to_string();
9094 assert!(
9095 rendered.contains(":autores"),
9096 "diagnostic must name the offending slot: {rendered}",
9097 );
9098 assert!(
9099 rendered.contains("alice\\nbob"),
9100 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9101 );
9102 }
9103
9104 #[test]
9105 fn validate_autores_rejects_at_129_byte_boundary() {
9106 // The 128-byte cap pin — boundary-exceeding case rejected,
9107 // boundary-accepting case passes. Mirrors the peer
9108 // `chart_maintainer_name_shape_rejects_at_129_byte_boundary`
9109 // substrate-side pin, surfaced at the per-axis caller so the
9110 // cap propagates through validate end-to-end. Constructed as
9111 // a single all-`a` token so only the cap arm fires.
9112 let max_ok = "a".repeat(128);
9113 let c = caixa_with_autores(vec![max_ok.as_str()]);
9114 c.validate_autores().unwrap();
9115 let too_long = "a".repeat(129);
9116 let c = caixa_with_autores(vec![too_long.as_str()]);
9117 let err = c.validate_autores().unwrap_err();
9118 let ManifestError::AutorInvalid { reason, .. } = err else {
9119 panic!("expected AutorInvalid, got {err:?}");
9120 };
9121 assert!(reason.contains("128"), "got: {reason}");
9122 assert!(reason.contains("129"), "got: {reason}");
9123 }
9124
9125 // ── validate_repositorio — universal-axis git-repo-URL shape ──────
9126
9127 fn caixa_with_repositorio(repositorio: Option<&str>) -> Caixa {
9128 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9129 c.repositorio = repositorio.map(String::from);
9130 c
9131 }
9132
9133 #[test]
9134 fn validate_repositorio_accepts_none() {
9135 // The omit-the-slot identity: `:repositorio` is optional. The
9136 // gate is a no-op when the author didn't declare a value —
9137 // every caixa without a `:repositorio` line trivially passes,
9138 // and the substrate-side renderers fall back to their
9139 // documented placeholder (`caixa-helm`'s `home: None`,
9140 // `caixa-flux`'s `https://github.com/pleme-io/<nome>` derived
9141 // URL). Mirrors the peer `validate_restart_window_accepts_none`
9142 // posture on the other `Option<String>` Caixa slot.
9143 let c = caixa_with_repositorio(None);
9144 c.validate_repositorio().unwrap();
9145 }
9146
9147 #[test]
9148 fn validate_repositorio_accepts_canonical_forms() {
9149 // Positive control sweep across every documented `:repositorio`
9150 // authoring shape — the same union the shared
9151 // `crate::render::is_git_repo_url` predicate accepts and the
9152 // peer `:deps :fonte :repo` axis already routes through.
9153 // Covers the `github:` shorthand (the canonical pleme-io
9154 // convention used in the `:repositorio` field of every
9155 // manifest fixture across `caixa-helm` / `caixa-mesh` and the
9156 // `examples/`), the `https://…` URL the README quickstart uses,
9157 // the `ssh://`, `git://`, `git@host:path` scp-style SSH, and
9158 // `file://` URL schemes the shared predicate documents.
9159 for repo in [
9160 "github:pleme-io/hello-rio",
9161 "github:pleme-io/checkout",
9162 "https://github.com/pleme-io/hello-rio",
9163 "ssh://git@github.com/pleme-io/hello-rio.git",
9164 "git://github.com/pleme-io/hello-rio.git",
9165 "git@github.com:pleme-io/hello-rio.git",
9166 "file:///srv/pleme/hello-rio",
9167 ] {
9168 let c = caixa_with_repositorio(Some(repo));
9169 c.validate_repositorio()
9170 .unwrap_or_else(|err| panic!("canonical {repo:?} must pass: {err:?}"));
9171 }
9172 }
9173
9174 #[test]
9175 fn validate_repositorio_rejects_empty_some() {
9176 // Canonical paste-from-blank-doc footgun. The narrower
9177 // [`ManifestError::RepositorioEmpty`] arm fires before the
9178 // shape predicate is consulted, mirroring the empty-first
9179 // cascade every peer per-axis identity gate uses
9180 // (`NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9181 // `FonteRepoEmpty` → `FonteRepoInvalid`). Without this gate
9182 // the empty `Some("")` silently passed the renderer's
9183 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9184 // on `None`) and landed as `home: ""` in `Chart.yaml` /
9185 // `url: ""` in the FluxCD `GitRepository`.
9186 let c = caixa_with_repositorio(Some(""));
9187 let err = c.validate_repositorio().unwrap_err();
9188 assert!(
9189 matches!(err, ManifestError::RepositorioEmpty),
9190 "got {err:?}",
9191 );
9192 }
9193
9194 #[test]
9195 fn validate_repositorio_rejects_whitespace() {
9196 // Paste-from-doc whitespace footgun. The shared
9197 // `is_git_repo_url` predicate refuses any whitespace byte; a
9198 // trailing space in a `:repositorio` value silently broke
9199 // `git clone '<value> '` at clone time. The diagnostic names
9200 // the offending value verbatim.
9201 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio "));
9202 let err = c.validate_repositorio().unwrap_err();
9203 let ManifestError::RepositorioInvalid { repositorio, .. } = err else {
9204 panic!("expected RepositorioInvalid, got {err:?}");
9205 };
9206 assert_eq!(repositorio, "github:pleme-io/hello-rio ");
9207 }
9208
9209 #[test]
9210 fn validate_repositorio_rejects_control_char() {
9211 // Paste-from-multiline-doc CRLF footgun — control characters
9212 // at the URL boundary are a class of subprocess-arg injection
9213 // and break git's URL parser at every porcelain entry point.
9214 let c = caixa_with_repositorio(Some("https://example.com/repo\n"));
9215 let err = c.validate_repositorio().unwrap_err();
9216 assert!(
9217 matches!(err, ManifestError::RepositorioInvalid { .. }),
9218 "got {err:?}",
9219 );
9220 }
9221
9222 #[test]
9223 fn validate_repositorio_rejects_leading_dash() {
9224 // Canonical CLI-argument-injection footgun: `git clone <repo>`
9225 // interprets a leading `-` as a CLI flag, so a
9226 // `-upload-pack=…` value escapes the subprocess argument
9227 // boundary. The shared predicate refuses every leading-`-`
9228 // shape at validate time.
9229 let c = caixa_with_repositorio(Some("-upload-pack=evil"));
9230 let err = c.validate_repositorio().unwrap_err();
9231 assert!(
9232 matches!(err, ManifestError::RepositorioInvalid { .. }),
9233 "got {err:?}",
9234 );
9235 }
9236
9237 #[test]
9238 fn validate_repositorio_rejects_missing_colon_separator() {
9239 // The bare `org/repo` ambiguity footgun — `git clone` reads
9240 // a no-`:` form as a relative filesystem path rather than the
9241 // GitHub-shorthand expansion the author probably intended.
9242 // The shared predicate refuses every shape without a `:`
9243 // separator.
9244 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9245 let err = c.validate_repositorio().unwrap_err();
9246 assert!(
9247 matches!(err, ManifestError::RepositorioInvalid { .. }),
9248 "got {err:?}",
9249 );
9250 }
9251
9252 #[test]
9253 fn validate_repositorio_rejects_fragment_anchor() {
9254 // Paste-from-browser-address-bar footgun on the
9255 // `:repositorio` axis — an author copies a GitHub permalink
9256 // to a README section / line-permalink and forgets to trim
9257 // the `#fragment` tail. The shared `is_git_repo_url`
9258 // predicate refuses the byte at the URL-grammar layer
9259 // (libcurl strips the fragment before opening the
9260 // transport, so the byte rides verbatim into the rendered
9261 // `Chart.yaml` `home:` and FluxCD `GitRepository` `url:`
9262 // fields but is silently dropped on the wire — two
9263 // manifest variants whose values differ only in their
9264 // fragment anchor lock to two distinct rendered artifacts
9265 // for the byte-identical clone, defeating the THEORY.md
9266 // §V.2 render-determinism contract on the `:repositorio`
9267 // axis the peer `:fonte :repo` axis already closes).
9268 let c = caixa_with_repositorio(Some("https://github.com/pleme-io/hello-rio#readme"));
9269 let err = c.validate_repositorio().unwrap_err();
9270 let ManifestError::RepositorioInvalid {
9271 repositorio,
9272 reason,
9273 } = err
9274 else {
9275 panic!("expected RepositorioInvalid, got {err:?}");
9276 };
9277 assert_eq!(repositorio, "https://github.com/pleme-io/hello-rio#readme");
9278 assert!(
9279 reason.contains("must not contain `#`"),
9280 "reason must surface the fragment-`#` arm, got {reason:?}"
9281 );
9282 }
9283
9284 #[test]
9285 fn validate_repositorio_rejects_query_string() {
9286 // Paste-from-browser-address-bar footgun on the
9287 // `:repositorio` axis (peer with the a68f818 fragment-`#`
9288 // arm on the same axis). An author copies a GitHub tab
9289 // deep-link out of the address bar and forgets to trim
9290 // the `?tab=…` query tail. The shared `is_git_repo_url`
9291 // predicate refuses the byte at the URL-grammar layer
9292 // (GitHub / GitLab / Bitbucket silently ignore the
9293 // `?query` tail and serve the same repo regardless, so
9294 // the byte rides verbatim into the rendered `Chart.yaml`
9295 // `home:` and FluxCD `GitRepository` `url:` fields but
9296 // is silently masked at the wire — two manifest variants
9297 // whose values differ only in their query tail lock to
9298 // two distinct rendered artifacts for the byte-identical
9299 // clone, defeating the THEORY.md §V.2 render-determinism
9300 // contract on the `:repositorio` axis the peer `:fonte
9301 // :repo` axis already closes).
9302 let c = caixa_with_repositorio(Some(
9303 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file",
9304 ));
9305 let err = c.validate_repositorio().unwrap_err();
9306 let ManifestError::RepositorioInvalid {
9307 repositorio,
9308 reason,
9309 } = err
9310 else {
9311 panic!("expected RepositorioInvalid, got {err:?}");
9312 };
9313 assert_eq!(
9314 repositorio,
9315 "https://github.com/pleme-io/hello-rio?tab=readme-ov-file"
9316 );
9317 assert!(
9318 reason.contains("must not contain `?`"),
9319 "reason must surface the query-`?` arm, got {reason:?}"
9320 );
9321 }
9322
9323 #[test]
9324 fn validate_repositorio_rejects_embedded_backslash() {
9325 // Windows-file-path-confusion footgun on the `:repositorio`
9326 // axis (peer with the prior fragment-`#` / query-`?` arms on
9327 // the same axis, and peer with the new dep-level `:fonte :repo`
9328 // backslash arm on the URL-grammar trajectory). An author
9329 // pastes a Windows Explorer address-bar `file:///C:\Users\me\
9330 // hello-rio` into the `:repositorio` slot, expecting the
9331 // `lareira-<nome>` chart's `home:` field and the FluxCD
9332 // `GitRepository` `url:` field to render the canonical local
9333 // file-URI. The shared `is_git_repo_url` predicate refuses
9334 // the byte at the URL-grammar layer (libcurl silently
9335 // translates `\` → `/` on some platforms and refuses it on
9336 // others, so the byte rides verbatim into the rendered
9337 // artifacts but is silently rewritten or rejected at the wire
9338 // — two manifest variants whose values differ only in
9339 // backslash-vs-forward-slash lock to two distinct rendered
9340 // artifacts for the byte-identical clone, defeating the
9341 // THEORY.md §V.2 render-determinism contract on the
9342 // `:repositorio` axis the peer `:fonte :repo` axis already
9343 // closes).
9344 let c = caixa_with_repositorio(Some("file:///C:\\Users\\me\\hello-rio"));
9345 let err = c.validate_repositorio().unwrap_err();
9346 let ManifestError::RepositorioInvalid {
9347 repositorio,
9348 reason,
9349 } = err
9350 else {
9351 panic!("expected RepositorioInvalid, got {err:?}");
9352 };
9353 assert_eq!(repositorio, "file:///C:\\Users\\me\\hello-rio");
9354 assert!(
9355 reason.contains("must not contain `\\`"),
9356 "reason must surface the backslash-`\\` arm, got {reason:?}"
9357 );
9358 }
9359
9360 #[test]
9361 fn validate_repositorio_rejects_uri_template_placeholder() {
9362 // URI Template (RFC 6570) placeholder footgun on the
9363 // `:repositorio` axis (peer with the prior fragment-`#` /
9364 // query-`?` / backslash-`\` arms on the same axis, and peer
9365 // with the new dep-level `:fonte :repo` `{` / `}` arm on the
9366 // URL-grammar trajectory). An author pastes a quick-start
9367 // README snippet / OpenAPI `servers:` URL / Helm chart
9368 // `home:` template carrying unresolved `{org}` / `{repo}`
9369 // placeholders into the `:repositorio` slot, expecting the
9370 // substrate to resolve the placeholder downstream. The
9371 // shared `is_git_repo_url` predicate refuses the byte at the
9372 // URL-grammar layer (libcurl percent-encodes `{` / `}` to
9373 // `%7B` / `%7D` on the wire, so the byte round-trips
9374 // inconsistently between the rendered `Chart.yaml home:` /
9375 // FluxCD `GitRepository url:` and the resolver's `git clone`
9376 // invocation, defeating the THEORY.md §V.2 render-
9377 // determinism contract on the `:repositorio` axis the peer
9378 // `:fonte :repo` axis already closes; every git porcelain
9379 // entry-point additionally fetches a nonexistent literal-
9380 // `{placeholder}`-named path far from the source caixa.lisp).
9381 let c = caixa_with_repositorio(Some("https://github.com/{org}/hello-rio"));
9382 let err = c.validate_repositorio().unwrap_err();
9383 let ManifestError::RepositorioInvalid {
9384 repositorio,
9385 reason,
9386 } = err
9387 else {
9388 panic!("expected RepositorioInvalid, got {err:?}");
9389 };
9390 assert_eq!(repositorio, "https://github.com/{org}/hello-rio");
9391 assert!(
9392 reason.contains("must not contain `{`"),
9393 "reason must surface the open-brace `{{` arm, got {reason:?}"
9394 );
9395 assert!(
9396 reason.contains("URI Template") || reason.contains("RFC 6570"),
9397 "reason must name the RFC 6570 URI Template grammar, got {reason:?}"
9398 );
9399 }
9400
9401 #[test]
9402 fn validate_repositorio_empty_takes_precedence_over_shape() {
9403 // Empty-first cascade pin: the empty `Some("")` surfaces the
9404 // narrower `RepositorioEmpty` not the shape-predicate-wrapped
9405 // `RepositorioInvalid`, mirroring the peer
9406 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid`,
9407 // `FonteRepoEmpty` → `FonteRepoInvalid` cascades. The shared
9408 // `is_git_repo_url` predicate also rejects the empty input
9409 // (defensively, with its own `"must not be empty"` reason),
9410 // but the manifest-layer empty arm runs first to surface the
9411 // narrower diagnostic verbatim.
9412 let c = caixa_with_repositorio(Some(""));
9413 let err = c.validate_repositorio().unwrap_err();
9414 assert!(
9415 matches!(err, ManifestError::RepositorioEmpty),
9416 "got {err:?}",
9417 );
9418 }
9419
9420 #[test]
9421 fn validate_repositorio_diagnostic_carries_offending_value() {
9422 // Diagnostic-shape pin (peer with
9423 // `validate_autores_diagnostic_carries_offending_author`): the
9424 // error's Display surfaces the offending value + slot name
9425 // verbatim, so a `feira lint` run can render the diagnostic
9426 // without re-parsing and the author can grep their caixa.lisp
9427 // for the offending `:repositorio` value.
9428 let c = caixa_with_repositorio(Some("pleme-io/hello-rio"));
9429 let rendered = c.validate_repositorio().unwrap_err().to_string();
9430 assert!(
9431 rendered.contains(":repositorio"),
9432 "diagnostic must name the offending slot: {rendered}",
9433 );
9434 assert!(
9435 rendered.contains("pleme-io/hello-rio"),
9436 "diagnostic must quote the offending value: {rendered}",
9437 );
9438 }
9439
9440 // ── validate_descricao — universal-axis Chart.yaml description shape ──
9441
9442 fn caixa_with_descricao(descricao: Option<&str>) -> Caixa {
9443 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9444 c.descricao = descricao.map(String::from);
9445 c
9446 }
9447
9448 #[test]
9449 fn validate_descricao_accepts_none() {
9450 // The omit-the-slot identity: `:descricao` is optional. The
9451 // gate is a no-op when the author didn't declare a value —
9452 // every caixa without a `:descricao` line trivially passes,
9453 // and the substrate-side renderers fall back to their
9454 // documented `caixa.nome`-derived placeholder. Mirrors the
9455 // peer `validate_repositorio_accepts_none` posture on the
9456 // sibling `Option<String>` Caixa slot.
9457 let c = caixa_with_descricao(None);
9458 c.validate_descricao().unwrap();
9459 }
9460
9461 #[test]
9462 fn validate_descricao_accepts_canonical_summary() {
9463 // Positive control: the canonical pleme-io descricao shape —
9464 // a short free-form prose summary — passes the gate. Covers
9465 // the fixture shapes the `caixa-helm` / `caixa-flux` /
9466 // `caixa-mesh` test fixtures use (`"Canonical Rust→wasm32-
9467 // wasip2 caixa Servico."`, `"Checkout flow."`).
9468 for desc in [
9469 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9470 "Checkout flow.",
9471 "AWS provider caixa for tatara-lisp",
9472 "FIXME — describe this caixa",
9473 "x",
9474 ] {
9475 let c = caixa_with_descricao(Some(desc));
9476 c.validate_descricao()
9477 .unwrap_or_else(|err| panic!("canonical {desc:?} must pass: {err:?}"));
9478 }
9479 }
9480
9481 #[test]
9482 fn validate_descricao_rejects_empty_some() {
9483 // Canonical paste-from-blank-doc footgun. Without this gate
9484 // the empty `Some("")` silently passed the renderer's
9485 // `Option::unwrap_or_else(|| <fallback>)` (which only fires
9486 // on `None`) and landed as `description: ""` in `Chart.yaml`
9487 // and a blank `README.md` header. Mirrors the peer
9488 // [`ManifestError::RepositorioEmpty`] empty-arm on the
9489 // sibling `Option<String>` Caixa slot.
9490 let c = caixa_with_descricao(Some(""));
9491 let err = c.validate_descricao().unwrap_err();
9492 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9493 }
9494
9495 #[test]
9496 fn validate_descricao_rejects_leading_whitespace() {
9497 // Paste-from-aligned-doc footgun: a leading ASCII space the
9498 // bare empty-arm gate accepted, the shape predicate now
9499 // refuses. The diagnostic carries the offending value
9500 // verbatim (with the leading space preserved) so the author
9501 // can grep their caixa.lisp for the exact `:descricao` line
9502 // and fix the round-trip-inconsistent leading whitespace.
9503 // Mirrors the peer
9504 // `validate_licenca_rejects_leading_whitespace` arm on the
9505 // sibling `:licenca` axis.
9506 let c = caixa_with_descricao(Some(" Checkout flow."));
9507 let err = c.validate_descricao().unwrap_err();
9508 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9509 panic!("expected DescricaoInvalid, got {err:?}");
9510 };
9511 assert_eq!(descricao, " Checkout flow.");
9512 assert!(reason.contains("whitespace"), "got: {reason:?}");
9513 }
9514
9515 #[test]
9516 fn validate_descricao_rejects_trailing_whitespace() {
9517 // Paste-from-doc footgun: a trailing ASCII space the bare
9518 // empty-arm gate accepted, the shape predicate now refuses.
9519 let c = caixa_with_descricao(Some("Checkout flow. "));
9520 let err = c.validate_descricao().unwrap_err();
9521 let ManifestError::DescricaoInvalid { descricao, reason } = err else {
9522 panic!("expected DescricaoInvalid, got {err:?}");
9523 };
9524 assert_eq!(descricao, "Checkout flow. ");
9525 assert!(reason.contains("whitespace"), "got: {reason:?}");
9526 }
9527
9528 #[test]
9529 fn validate_descricao_rejects_embedded_newline() {
9530 // Paste-from-multiline-doc footgun: an embedded LF the bare
9531 // empty-arm gate accepted, the shape predicate now refuses.
9532 // Without this gate the embedded newline silently landed in
9533 // the rendered Chart.yaml as a multi-line YAML block scalar,
9534 // and every chart-aware UI (`helm list`, `helm search`,
9535 // Artifact Hub) renders the description in a single-line
9536 // column so the embedded newline is silently dropped at
9537 // every downstream consumer.
9538 let c = caixa_with_descricao(Some("Checkout\nflow."));
9539 let err = c.validate_descricao().unwrap_err();
9540 assert!(
9541 matches!(err, ManifestError::DescricaoInvalid { .. }),
9542 "got {err:?}",
9543 );
9544 assert!(err.to_string().contains("newline"), "got {err}");
9545 }
9546
9547 #[test]
9548 fn validate_descricao_rejects_embedded_carriage_return() {
9549 // Paste-from-Windows-CRLF-doc footgun.
9550 let c = caixa_with_descricao(Some("Checkout\rflow."));
9551 let err = c.validate_descricao().unwrap_err();
9552 assert!(
9553 matches!(err, ManifestError::DescricaoInvalid { .. }),
9554 "got {err:?}",
9555 );
9556 assert!(err.to_string().contains("carriage return"), "got {err}");
9557 }
9558
9559 #[test]
9560 fn validate_descricao_rejects_embedded_tab() {
9561 // Tab-from-aligned-doc footgun.
9562 let c = caixa_with_descricao(Some("Checkout\tflow."));
9563 let err = c.validate_descricao().unwrap_err();
9564 assert!(
9565 matches!(err, ManifestError::DescricaoInvalid { .. }),
9566 "got {err:?}",
9567 );
9568 assert!(err.to_string().contains("tab"), "got {err}");
9569 }
9570
9571 #[test]
9572 fn validate_descricao_rejects_embedded_control_bytes() {
9573 // Paste-from-binary-blob footgun: every other control byte
9574 // (NUL, BEL, ESC, DEL) is refused at validate time. Mirrors
9575 // the peer SPDX-expression control-byte arm.
9576 for s in [
9577 "Checkout\x00flow.",
9578 "Checkout\x07flow.",
9579 "Checkout\x1bflow.",
9580 "Checkout\x7fflow.",
9581 ] {
9582 let c = caixa_with_descricao(Some(s));
9583 let err = c.validate_descricao().unwrap_err();
9584 assert!(
9585 matches!(err, ManifestError::DescricaoInvalid { .. }),
9586 "{s:?} got {err:?}",
9587 );
9588 assert!(
9589 err.to_string().contains("control character"),
9590 "{s:?} got {err}",
9591 );
9592 }
9593 }
9594
9595 #[test]
9596 fn validate_descricao_accepts_unicode_prose() {
9597 // Positive control: Unicode prose is accepted — the
9598 // canonical fixtures carry `→` (U+2192) and `—` (U+2014),
9599 // and `Caixa::template`'s `"FIXME — describe this caixa"`
9600 // scaffold every `feira init` emits must continue to pass.
9601 for s in [
9602 "Canonical Rust→wasm32-wasip2 caixa Servico.",
9603 "FIXME — describe this caixa",
9604 "Caixa pour le projet tâche",
9605 "日本語の説明",
9606 ] {
9607 let c = caixa_with_descricao(Some(s));
9608 c.validate_descricao()
9609 .unwrap_or_else(|err| panic!("Unicode {s:?} must pass: {err:?}"));
9610 }
9611 }
9612
9613 #[test]
9614 fn validate_descricao_empty_takes_precedence_over_shape() {
9615 // Cascade pin: a `Some("")` surfaces the narrower
9616 // `DescricaoEmpty` arm, not the broader `DescricaoInvalid`
9617 // shape-predicate arm. Mirrors the peer
9618 // `validate_licenca_empty_takes_precedence_over_shape` pin
9619 // on the sibling `:licenca` axis.
9620 let c = caixa_with_descricao(Some(""));
9621 let err = c.validate_descricao().unwrap_err();
9622 assert!(matches!(err, ManifestError::DescricaoEmpty), "got {err:?}",);
9623 }
9624
9625 #[test]
9626 fn validate_descricao_invalid_diagnostic_carries_offending_value_and_slot() {
9627 // Diagnostic-shape pin: the error's Display surfaces both
9628 // the `:descricao` slot name and the offending value
9629 // verbatim, so a `feira lint` run can render the diagnostic
9630 // without re-parsing and the author can grep their caixa.lisp
9631 // for the offending `:descricao` line. Mirrors the peer
9632 // `validate_licenca_invalid_diagnostic_carries_offending_value_and_slot`
9633 // pin (ee2e888) on the sibling `:licenca` axis.
9634 // The `{descricao:?}` Debug format escapes embedded control
9635 // bytes; the quoted offending value surfaces as
9636 // `"Checkout\nflow."` (literal backslash-n) in the rendered
9637 // diagnostic. The author can grep their caixa.lisp for the
9638 // literal `Checkout` summary prefix.
9639 let c = caixa_with_descricao(Some("Checkout\nflow."));
9640 let rendered = c.validate_descricao().unwrap_err().to_string();
9641 assert!(
9642 rendered.contains(":descricao"),
9643 "diagnostic must name the offending slot: {rendered}",
9644 );
9645 assert!(
9646 rendered.contains("Checkout\\nflow."),
9647 "diagnostic must quote the offending value (debug-escaped): {rendered}",
9648 );
9649 }
9650
9651 #[test]
9652 fn validate_descricao_template_passes() {
9653 // Round-trip pin: the bare `Caixa::template` shape carries
9654 // `:descricao "FIXME — describe this caixa"` (a non-empty
9655 // sentinel), so the template-derived Caixa passes the gate by
9656 // construction. A future template-shape change that omits or
9657 // empties `:descricao` would surface here as a regression.
9658 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9659 c.validate_descricao().unwrap();
9660 }
9661
9662 #[test]
9663 fn validate_descricao_diagnostic_names_offending_slot() {
9664 // Diagnostic-shape pin (peer with
9665 // `validate_repositorio_diagnostic_carries_offending_value`):
9666 // the error's Display surfaces the `:descricao` slot name
9667 // verbatim, so a `feira lint` run can render the diagnostic
9668 // without re-parsing and the author can grep their caixa.lisp
9669 // for the offending `:descricao` line.
9670 let c = caixa_with_descricao(Some(""));
9671 let rendered = c.validate_descricao().unwrap_err().to_string();
9672 assert!(
9673 rendered.contains(":descricao"),
9674 "diagnostic must name the offending slot: {rendered}",
9675 );
9676 }
9677
9678 // ── validate_licenca — universal-axis chart README license shape ──
9679
9680 fn caixa_with_licenca(licenca: Option<&str>) -> Caixa {
9681 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9682 c.licenca = licenca.map(String::from);
9683 c
9684 }
9685
9686 #[test]
9687 fn validate_licenca_accepts_none() {
9688 // The omit-the-slot identity: `:licenca` is optional. The
9689 // gate is a no-op when the author didn't declare a value —
9690 // every caixa without a `:licenca` line trivially passes,
9691 // and the substrate-side `caixa-helm` renderer falls back to
9692 // the documented `"MIT"` placeholder. Mirrors the peer
9693 // `validate_descricao_accepts_none` posture on the sibling
9694 // `Option<String>` Caixa slot.
9695 let c = caixa_with_licenca(None);
9696 c.validate_licenca().unwrap();
9697 }
9698
9699 #[test]
9700 fn validate_licenca_accepts_canonical_expressions() {
9701 // Positive control: every canonical SPDX expression shape
9702 // pleme-io carries in its existing fixtures + the canonical
9703 // SPDX dual-license / with-exception / `+`-suffix / grouped /
9704 // user-defined-reference shapes all pass the gate. Covers
9705 // the single-license, `OR`-compound, `AND`-compound,
9706 // `WITH`-exception, parenthesis-grouped, `+`-suffix, and
9707 // `LicenseRef-` / `DocumentRef-:LicenseRef-` shapes — every
9708 // production the SPDX 2.1 expression grammar admits that
9709 // sits within the alphabet floor the
9710 // `is_spdx_expression_shape` predicate enforces.
9711 for lic in [
9712 "MIT",
9713 "Apache-2.0",
9714 "Apache-2.0 OR MIT",
9715 "Apache-2.0 AND MIT",
9716 "BSD-3-Clause",
9717 "MPL-2.0",
9718 "GPL-3.0-or-later",
9719 "GPL-2.0+",
9720 "Apache-2.0 WITH LLVM-exception",
9721 "(MIT OR Apache-2.0) AND BSD-3-Clause",
9722 "(MIT OR Apache-2.0) AND BSD-3-Clause AND ISC",
9723 "LicenseRef-MyLicense",
9724 "DocumentRef-spdx-tool:LicenseRef-MIT-Style",
9725 "x",
9726 ] {
9727 let c = caixa_with_licenca(Some(lic));
9728 c.validate_licenca()
9729 .unwrap_or_else(|err| panic!("canonical {lic:?} must pass: {err:?}"));
9730 }
9731 }
9732
9733 #[test]
9734 fn validate_licenca_rejects_trailing_whitespace() {
9735 // Paste-from-doc whitespace footgun. A trailing space in the
9736 // `:licenca` value would silently break a downstream SPDX
9737 // parser that splits on exact `AND` / `OR` / `WITH` keyword
9738 // boundaries. The shape predicate refuses every trailing
9739 // whitespace byte by construction. Peer with
9740 // `validate_repositorio_rejects_whitespace` and
9741 // `validate_edicao_rejects_trailing_whitespace`.
9742 let c = caixa_with_licenca(Some("MIT "));
9743 let err = c.validate_licenca().unwrap_err();
9744 let ManifestError::LicencaInvalid { licenca, .. } = err else {
9745 panic!("expected LicencaInvalid, got {err:?}");
9746 };
9747 assert_eq!(licenca, "MIT ");
9748 }
9749
9750 #[test]
9751 fn validate_licenca_rejects_leading_whitespace() {
9752 // Symmetric paste-from-doc whitespace footgun on the leading
9753 // boundary — the gate refuses every shape that starts with a
9754 // space byte by construction. Peer with
9755 // `validate_edicao_rejects_leading_whitespace`.
9756 let c = caixa_with_licenca(Some(" MIT"));
9757 let err = c.validate_licenca().unwrap_err();
9758 assert!(
9759 matches!(err, ManifestError::LicencaInvalid { .. }),
9760 "got {err:?}",
9761 );
9762 }
9763
9764 #[test]
9765 fn validate_licenca_rejects_control_char() {
9766 // Paste-from-multiline-doc CRLF footgun — control characters
9767 // at the value boundary land as a malformed line in the
9768 // rendered chart `README.md` `## License` section. Peer with
9769 // `validate_repositorio_rejects_control_char` and
9770 // `validate_edicao_rejects_control_char`.
9771 for lic in ["MIT\n", "MIT\r\n", "MIT\rApache-2.0"] {
9772 let c = caixa_with_licenca(Some(lic));
9773 let err = c.validate_licenca().unwrap_err();
9774 assert!(
9775 matches!(err, ManifestError::LicencaInvalid { .. }),
9776 "expected LicencaInvalid on {lic:?}, got {err:?}",
9777 );
9778 }
9779 }
9780
9781 #[test]
9782 fn validate_licenca_rejects_tab() {
9783 // Tab-from-aligned-doc footgun — SPDX expressions use a
9784 // single ASCII space between tokens; a tab breaks every
9785 // downstream SPDX parser that splits on exact `" "`
9786 // boundaries.
9787 let c = caixa_with_licenca(Some("MIT\tOR Apache-2.0"));
9788 let err = c.validate_licenca().unwrap_err();
9789 assert!(
9790 matches!(err, ManifestError::LicencaInvalid { .. }),
9791 "got {err:?}",
9792 );
9793 }
9794
9795 #[test]
9796 fn validate_licenca_rejects_non_ascii() {
9797 // Smart-quote / non-ASCII paste footgun — SPDX identifiers
9798 // are ASCII per the `idstring = 1*(ALPHA / DIGIT / "-" /
9799 // ".")` production. The shape predicate refuses every
9800 // non-ASCII byte by construction; peer with
9801 // `validate_edicao_rejects_non_ascii_lookalike`.
9802 for lic in ["MIT\u{a0}OR Apache-2.0", "MIT\u{2013}1.0", "Café-1.0"] {
9803 let c = caixa_with_licenca(Some(lic));
9804 let err = c.validate_licenca().unwrap_err();
9805 assert!(
9806 matches!(err, ManifestError::LicencaInvalid { .. }),
9807 "expected LicencaInvalid on {lic:?}, got {err:?}",
9808 );
9809 }
9810 }
9811
9812 #[test]
9813 fn validate_licenca_rejects_underscore() {
9814 // Underscore-instead-of-hyphen typo footgun — `Apache_2.0` /
9815 // `MIT_Style` / `BSD_3_Clause` are familiar shapes from
9816 // snake-case identifier conventions that don't apply to the
9817 // SPDX `idstring` grammar (which admits only `ALPHA / DIGIT /
9818 // "-" / "."`). The shape predicate refuses every underscore
9819 // byte by construction.
9820 for lic in ["Apache_2.0", "MIT_Style", "BSD_3_Clause"] {
9821 let c = caixa_with_licenca(Some(lic));
9822 let err = c.validate_licenca().unwrap_err();
9823 assert!(
9824 matches!(err, ManifestError::LicencaInvalid { .. }),
9825 "expected LicencaInvalid on {lic:?}, got {err:?}",
9826 );
9827 }
9828 }
9829
9830 #[test]
9831 fn validate_licenca_rejects_comma_separator() {
9832 // Comma-instead-of-`OR`-keyword colloquial idiom footgun —
9833 // SPDX expressions compose multiple licenses via `AND` / `OR`
9834 // keywords, not the comma separator. The shape predicate
9835 // refuses every comma byte by construction.
9836 for lic in ["MIT, Apache-2.0", "MIT,Apache-2.0"] {
9837 let c = caixa_with_licenca(Some(lic));
9838 let err = c.validate_licenca().unwrap_err();
9839 assert!(
9840 matches!(err, ManifestError::LicencaInvalid { .. }),
9841 "expected LicencaInvalid on {lic:?}, got {err:?}",
9842 );
9843 }
9844 }
9845
9846 #[test]
9847 fn validate_licenca_rejects_slash_dual_license() {
9848 // Slash-dual-license colloquial idiom footgun — the
9849 // `MIT/Apache-2.0` shape is common in Cargo's pre-SPDX
9850 // `package.license` field but non-SPDX; the SPDX equivalent
9851 // is `MIT OR Apache-2.0`. The shape predicate refuses every
9852 // forward-slash byte by construction.
9853 for lic in ["MIT/Apache-2.0", "MIT/BSD-3-Clause"] {
9854 let c = caixa_with_licenca(Some(lic));
9855 let err = c.validate_licenca().unwrap_err();
9856 assert!(
9857 matches!(err, ManifestError::LicencaInvalid { .. }),
9858 "expected LicencaInvalid on {lic:?}, got {err:?}",
9859 );
9860 }
9861 }
9862
9863 #[test]
9864 fn validate_licenca_rejects_semicolon_separator() {
9865 // Semicolon-list-separator confusion footgun — adjacent to
9866 // the comma-separator idiom, every list-separator-belongs-
9867 // to-list-grammar confusion lands here.
9868 let c = caixa_with_licenca(Some("MIT; Apache-2.0"));
9869 let err = c.validate_licenca().unwrap_err();
9870 assert!(
9871 matches!(err, ManifestError::LicencaInvalid { .. }),
9872 "got {err:?}",
9873 );
9874 }
9875
9876 #[test]
9877 fn validate_licenca_empty_takes_precedence_over_shape() {
9878 // Empty-first cascade pin: the empty `Some("")` surfaces the
9879 // narrower `LicencaEmpty` not the shape-predicate-wrapped
9880 // `LicencaInvalid`, mirroring the peer
9881 // `validate_edicao_empty_takes_precedence_over_shape` and
9882 // `validate_repositorio_empty_takes_precedence_over_shape`
9883 // (`RepositorioEmpty` → `RepositorioInvalid`), `NomeEmpty` →
9884 // `NomeInvalid`, `VersaoEmpty` → `VersaoInvalid` cascades.
9885 // The shape predicate also refuses the empty input
9886 // (defensively — `"must not be empty"`), but the manifest-
9887 // layer empty arm runs first to surface the narrower
9888 // diagnostic verbatim.
9889 let c = caixa_with_licenca(Some(""));
9890 let err = c.validate_licenca().unwrap_err();
9891 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9892 }
9893
9894 #[test]
9895 fn validate_licenca_invalid_diagnostic_carries_offending_value() {
9896 // Diagnostic-shape pin on the shape-predicate arm (peer with
9897 // `validate_edicao_invalid_diagnostic_carries_offending_value`
9898 // and `validate_repositorio_diagnostic_carries_offending_value`):
9899 // the error's Display surfaces the offending value + slot
9900 // name verbatim, so a `feira lint` run can render the
9901 // diagnostic without re-parsing and the author can grep
9902 // their caixa.lisp for the offending `:licenca` value.
9903 let c = caixa_with_licenca(Some("Apache_2.0"));
9904 let rendered = c.validate_licenca().unwrap_err().to_string();
9905 assert!(
9906 rendered.contains(":licenca"),
9907 "diagnostic must name the offending slot: {rendered}",
9908 );
9909 assert!(
9910 rendered.contains("Apache_2.0"),
9911 "diagnostic must quote the offending value: {rendered}",
9912 );
9913 }
9914
9915 #[test]
9916 fn validate_licenca_rejects_empty_some() {
9917 // Canonical paste-from-blank-doc footgun. Without this gate
9918 // the empty `Some("")` silently passed the renderer's
9919 // `Option::unwrap_or_else(|| "MIT".into())` (which only
9920 // fires on `None`) and landed as a bare trailing period in
9921 // the rendered chart `README.md` `## License` section.
9922 // Mirrors the peer [`ManifestError::DescricaoEmpty`] empty-
9923 // arm on the sibling `Option<String>` Caixa slot.
9924 let c = caixa_with_licenca(Some(""));
9925 let err = c.validate_licenca().unwrap_err();
9926 assert!(matches!(err, ManifestError::LicencaEmpty), "got {err:?}",);
9927 }
9928
9929 #[test]
9930 fn validate_licenca_template_passes() {
9931 // Round-trip pin: the bare `Caixa::template` shape (whether
9932 // it carries `:licenca` or omits it) passes the gate by
9933 // construction. A future template-shape change that
9934 // introduced `(:licenca "")` would surface here as a
9935 // regression. Mirrors the peer
9936 // `validate_descricao_template_passes` pin.
9937 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
9938 c.validate_licenca().unwrap();
9939 }
9940
9941 #[test]
9942 fn validate_licenca_diagnostic_names_offending_slot() {
9943 // Diagnostic-shape pin (peer with
9944 // `validate_descricao_diagnostic_names_offending_slot`):
9945 // the error's Display surfaces the `:licenca` slot name
9946 // verbatim, so a `feira lint` run can render the diagnostic
9947 // without re-parsing and the author can grep their caixa.lisp
9948 // for the offending `:licenca` line.
9949 let c = caixa_with_licenca(Some(""));
9950 let rendered = c.validate_licenca().unwrap_err().to_string();
9951 assert!(
9952 rendered.contains(":licenca"),
9953 "diagnostic must name the offending slot: {rendered}",
9954 );
9955 }
9956
9957 // ── Caixa::licenca — outer top-level Option<&str> scalar accessor ──
9958
9959 #[test]
9960 fn licenca_returns_licenca_byte_string_verbatim_across_permutations() {
9961 // The canonical per-`Caixa` `:licenca` SPDX-expression scalar
9962 // pin: [`Caixa::licenca`] must return the `:licenca` typed
9963 // byte-string verbatim as an `Option<&str>`, byte-equal to the
9964 // raw `self.licenca.as_deref()` access across every
9965 // representative value in the accept-set — `None` (the "omit
9966 // the slot to defer to the caixa-helm renderer's `MIT`
9967 // fallback" arm every existing fixture without a `:licenca`
9968 // line carries), `Some("")` (a past-the-guard sentinel that
9969 // pins the accessor doesn't perform a silent
9970 // `Some("") → None` collapse on the empty arm — validate
9971 // rejects `Some("")` through `LicencaEmpty` but the accessor
9972 // must ship the raw slot verbatim so a validate-time gate
9973 // regression surfaces at the caixa-helm emit boundary rather
9974 // than being silently absorbed into the fallback), `Some("MIT")`
9975 // (the canonical single-license shape every `feira init`
9976 // template scaffolds), `Some("Apache-2.0 OR MIT")` (the
9977 // canonical `OR`-compound shape the peer
9978 // `validate_licenca_accepts_canonical_expressions` positive
9979 // sweep exercises), `Some("(MIT OR Apache-2.0) AND
9980 // BSD-3-Clause")` (the canonical parenthesis-grouped shape),
9981 // `Some("MIT ")` / `Some(" MIT")` / `Some("MIT\n")` /
9982 // `Some("Apache_2.0")` / `Some("MIT,Apache-2.0")` (past-the-
9983 // guard sentinels — validate rejects each through
9984 // `LicencaInvalid` but the accessor must ship the raw slot
9985 // verbatim).
9986 //
9987 // First outer top-level [`Caixa`] `Option<&str>`-return scalar
9988 // accessor pin on the substrate primitive — opens the "outer
9989 // [`Caixa`] `Option<&str>` scalar" projection pattern the
9990 // sibling per-`Caixa` `:descricao` / `:repositorio` / `:edicao`
9991 // future lifts fold on. Sibling in shape to the peer per-`:placement`
9992 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
9993 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
9994 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
9995 // axes, extended onto the outer top-level [`Caixa`] universal-
9996 // axis surface. Pins against a future silent detour that
9997 // returned an owned `Option<String>` (which would type-check
9998 // but silently allocate on every accessor call, breaking the
9999 // zero-cost projection every peer sibling accessor carries), a
10000 // `Some("") → None` collapse (which would silently absorb the
10001 // `LicencaEmpty` refusal case at the accessor boundary and the
10002 // caixa-helm emit path would silently fall back to `"MIT"` on
10003 // a struct-literal `Caixa { licenca: Some(""), .. }`), or a
10004 // `None → Some("MIT")` collapse (which would silently reify
10005 // the caixa-helm renderer's `"MIT"` fallback at the accessor
10006 // boundary and every downstream consumer keying off the
10007 // `Option::is_none()` discriminator would lose the "author
10008 // omitted the slot" signal).
10009 for licenca in [
10010 None,
10011 Some(""),
10012 Some("MIT"),
10013 Some("Apache-2.0 OR MIT"),
10014 Some("(MIT OR Apache-2.0) AND BSD-3-Clause"),
10015 Some("MIT "),
10016 Some(" MIT"),
10017 Some("MIT\n"),
10018 Some("Apache_2.0"),
10019 Some("MIT,Apache-2.0"),
10020 ] {
10021 let c = caixa_with_licenca(licenca);
10022 assert_eq!(
10023 c.licenca(),
10024 licenca,
10025 "Caixa::licenca must return :licenca verbatim (got {:?}, \
10026 expected {licenca:?})",
10027 c.licenca(),
10028 );
10029 assert_eq!(
10030 c.licenca(),
10031 c.licenca.as_deref(),
10032 "Caixa::licenca must byte-equal the raw \
10033 `self.licenca.as_deref()` field access across every \
10034 value in the Option<&str> accept-set",
10035 );
10036 }
10037 }
10038
10039 #[test]
10040 fn validate_licenca_empty_arm_routes_through_accessor() {
10041 // Composition pin: [`Caixa::validate_licenca`]'s empty-arm gate
10042 // must key off [`Caixa::licenca`], not the raw
10043 // `self.licenca.as_deref()` field access. Structurally: a
10044 // `Caixa { licenca: Some(""), .. }` must surface the
10045 // `LicencaEmpty` refusal exactly, and a
10046 // `Caixa { licenca: Some("MIT"), .. }` (the canonical
10047 // single-license form) must pass validate. The pair jointly
10048 // pins the accessor + validate-gate composition: any future
10049 // silent detour that had the accessor return `None` on the
10050 // empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10051 // silently absorb the `LicencaEmpty` refusal at the accessor
10052 // boundary and the validate gate would accept a struct-literal
10053 // `Caixa { licenca: Some(""), .. }` — the composition pin
10054 // catches that at caixa-core build time.
10055 //
10056 // Peer of the per-`:politicas :circuit-breaker`
10057 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
10058 // accessor-composition pin
10059 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
10060 // on the sibling per-M3-mesh-slot required-`u32` axis — same
10061 // "the validate / shape-gate predicate must route through the
10062 // substrate-primitive typed dispatch" discipline extended onto
10063 // the outer top-level [`Caixa`] universal-axis
10064 // `Option<&str>`-composition surface.
10065 let c = caixa_with_licenca(Some(""));
10066 assert!(
10067 matches!(c.validate_licenca(), Err(ManifestError::LicencaEmpty)),
10068 "validate_licenca must reject licenca == Some(\"\") with \
10069 LicencaEmpty — the accessor and the validate gate must \
10070 route through the same substrate-primitive typed dispatch \
10071 on the :licenca empty arm",
10072 );
10073 let c = caixa_with_licenca(Some("MIT"));
10074 assert!(
10075 c.validate_licenca().is_ok(),
10076 "validate_licenca must accept licenca == Some(\"MIT\") \
10077 (the canonical single-license SPDX shape)",
10078 );
10079 }
10080
10081 #[test]
10082 fn licenca_projects_option_str_by_borrow() {
10083 // The by-borrow pin: [`Caixa::licenca`] returns
10084 // `Option<&str>` by borrow — the `&str` borrows the underlying
10085 // `String` storage of the `Option<String>` slot and the
10086 // accessor must not allocate a fresh `String` on every call.
10087 // Peer of the per-`:placement`
10088 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10089 // borrow pin on the peer per-M3-mesh-slot
10090 // `Option<&str>`-return axis, extended onto the outer top-
10091 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10092 // accessor's returned `&str` must borrow from `&self` (the
10093 // returned reference's lifetime is tied to `&self`), and
10094 // calling the accessor twice on the same [`Caixa`] must yield
10095 // the same `Option<&str>` verbatim (idempotent, no side
10096 // effects on `&self`).
10097 //
10098 // Pins against a future silent detour that returned an owned
10099 // `Option<String>` (which would type-check but silently
10100 // allocate on every call, breaking the zero-cost projection
10101 // every peer sibling accessor carries), or a one-arm-only
10102 // accessor that returned a saturating value on some sentinel
10103 // input (breaking the pass-through invariant the sibling
10104 // required-scalar accessors carry).
10105 for licenca in [None, Some(""), Some("MIT"), Some("Apache-2.0 OR MIT")] {
10106 let c = caixa_with_licenca(licenca);
10107 let first = c.licenca();
10108 let second = c.licenca();
10109 assert_eq!(
10110 first, second,
10111 "Caixa::licenca must be idempotent — two successive \
10112 calls on the same &self must return the same \
10113 Option<&str>",
10114 );
10115 assert_eq!(
10116 first, licenca,
10117 "Caixa::licenca must return :licenca verbatim by \
10118 borrow — got {first:?}, expected {licenca:?}",
10119 );
10120 }
10121 }
10122
10123 // ── Caixa::repositorio — outer top-level Option<&str> scalar accessor ──
10124
10125 #[test]
10126 fn repositorio_returns_repositorio_byte_string_verbatim_across_permutations() {
10127 // The canonical per-`Caixa` `:repositorio` git-repo-URL scalar
10128 // pin: [`Caixa::repositorio`] must return the `:repositorio`
10129 // typed byte-string verbatim as an `Option<&str>`, byte-equal
10130 // to the raw `self.repositorio.as_deref()` access across every
10131 // representative value in the accept-set — `None` (the "omit
10132 // the slot to defer to the per-renderer placeholder" arm every
10133 // existing fixture without a `:repositorio` line carries),
10134 // `Some("")` (a past-the-guard sentinel that pins the accessor
10135 // doesn't perform a silent `Some("") → None` collapse on the
10136 // empty arm — validate rejects `Some("")` through
10137 // `RepositorioEmpty` but the accessor must ship the raw slot
10138 // verbatim so a validate-time gate regression surfaces at the
10139 // caixa-helm / caixa-flux emit boundary rather than being
10140 // silently absorbed into the per-renderer fallback),
10141 // `Some("github:pleme-io/hello-rio")` (the canonical `github:`
10142 // shorthand every existing manifest fixture across
10143 // `caixa-helm` / `caixa-mesh` and the `examples/` uses),
10144 // `Some("https://github.com/pleme-io/checkout")` (the canonical
10145 // `https://` URL the README quickstart uses),
10146 // `Some("ssh://git@github.com/pleme-io/checkout.git")` /
10147 // `Some("git://github.com/pleme-io/checkout.git")` /
10148 // `Some("git@github.com:pleme-io/checkout.git")` /
10149 // `Some("file:///opt/mirrors/pleme-io/checkout")` (every non-
10150 // github scheme the shared `is_git_repo_url` predicate
10151 // documents), and five past-the-guard sentinels for the
10152 // `RepositorioInvalid` refusal cases (`Some("pleme-io/checkout")`
10153 // missing-colon, `Some("-upload-pack=evil")` leading-dash, /
10154 // `Some("github:pleme-io/checkout?ref=main")` query-string, /
10155 // `Some("github:pleme-io/checkout#main")` fragment-anchor, /
10156 // `Some("github:pleme-io/{tpl}")` URI-template-placeholder — the
10157 // sentinels pin the accessor doesn't silently absorb the
10158 // refusal cases into a fallback).
10159 //
10160 // Second outer top-level [`Caixa`] `Option<&str>`-return scalar
10161 // accessor pin on the substrate primitive — sibling of the peer
10162 // [`Caixa::licenca`] (6d5bc28) pin
10163 // (`licenca_returns_licenca_byte_string_verbatim_across_permutations`)
10164 // that opened the "outer [`Caixa`] `Option<&str>` scalar"
10165 // projection pin pattern this pin folds on. Sibling in shape to
10166 // the peer per-`:placement`
10167 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10168 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10169 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10170 // axes, extended onto the outer top-level [`Caixa`] universal-
10171 // axis surface. Pins against a future silent detour that
10172 // returned an owned `Option<String>` (which would type-check
10173 // but silently allocate on every accessor call, breaking the
10174 // zero-cost projection every peer sibling accessor carries), a
10175 // `Some("") → None` collapse (which would silently absorb the
10176 // `RepositorioEmpty` refusal case at the accessor boundary and
10177 // the caixa-helm `Chart.yaml` `home:` fold would silently
10178 // render a `home: null` / omitted field on a struct-literal
10179 // `Caixa { repositorio: Some(""), .. }`), or a
10180 // `None → Some(<default>)` collapse (which would silently reify
10181 // the per-renderer fallback at the accessor boundary and every
10182 // downstream consumer keying off the `Option::is_none()`
10183 // discriminator would lose the "author omitted the slot"
10184 // signal).
10185 for repositorio in [
10186 None,
10187 Some(""),
10188 Some("github:pleme-io/hello-rio"),
10189 Some("https://github.com/pleme-io/checkout"),
10190 Some("ssh://git@github.com/pleme-io/checkout.git"),
10191 Some("git://github.com/pleme-io/checkout.git"),
10192 Some("git@github.com:pleme-io/checkout.git"),
10193 Some("file:///opt/mirrors/pleme-io/checkout"),
10194 Some("pleme-io/checkout"),
10195 Some("-upload-pack=evil"),
10196 Some("github:pleme-io/checkout?ref=main"),
10197 Some("github:pleme-io/checkout#main"),
10198 Some("github:pleme-io/{tpl}"),
10199 ] {
10200 let c = caixa_with_repositorio(repositorio);
10201 assert_eq!(
10202 c.repositorio(),
10203 repositorio,
10204 "Caixa::repositorio must return :repositorio verbatim \
10205 (got {:?}, expected {repositorio:?})",
10206 c.repositorio(),
10207 );
10208 assert_eq!(
10209 c.repositorio(),
10210 c.repositorio.as_deref(),
10211 "Caixa::repositorio must byte-equal the raw \
10212 `self.repositorio.as_deref()` field access across every \
10213 value in the Option<&str> accept-set",
10214 );
10215 }
10216 }
10217
10218 #[test]
10219 fn validate_repositorio_empty_arm_routes_through_accessor() {
10220 // Composition pin: [`Caixa::validate_repositorio`]'s empty-arm
10221 // gate must key off [`Caixa::repositorio`], not the raw
10222 // `self.repositorio.as_deref()` field access. Structurally: a
10223 // `Caixa { repositorio: Some(""), .. }` must surface the
10224 // `RepositorioEmpty` refusal exactly, and a
10225 // `Caixa { repositorio: Some("github:pleme-io/hello-rio"), .. }`
10226 // (the canonical `github:` shorthand form) must pass validate.
10227 // The pair jointly pins the accessor + validate-gate
10228 // composition: any future silent detour that had the accessor
10229 // return `None` on the empty arm (a `.filter(|s| !s.is_empty())`
10230 // collapse) would silently absorb the `RepositorioEmpty` refusal
10231 // at the accessor boundary and the validate gate would accept a
10232 // struct-literal `Caixa { repositorio: Some(""), .. }` — the
10233 // composition pin catches that at caixa-core build time.
10234 //
10235 // Peer of the [`Caixa::licenca`] (6d5bc28)
10236 // `validate_licenca_empty_arm_routes_through_accessor`
10237 // composition pin on the sibling outer top-level [`Caixa`]
10238 // `Option<&str>` universal-axis surface — same "the validate /
10239 // shape-gate predicate must route through the substrate-
10240 // primitive typed dispatch" discipline extended onto the second
10241 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10242 // composition surface.
10243 let c = caixa_with_repositorio(Some(""));
10244 assert!(
10245 matches!(
10246 c.validate_repositorio(),
10247 Err(ManifestError::RepositorioEmpty),
10248 ),
10249 "validate_repositorio must reject repositorio == Some(\"\") \
10250 with RepositorioEmpty — the accessor and the validate gate \
10251 must route through the same substrate-primitive typed \
10252 dispatch on the :repositorio empty arm",
10253 );
10254 let c = caixa_with_repositorio(Some("github:pleme-io/hello-rio"));
10255 assert!(
10256 c.validate_repositorio().is_ok(),
10257 "validate_repositorio must accept repositorio == \
10258 Some(\"github:pleme-io/hello-rio\") (the canonical \
10259 `github:` shorthand git-repo-URL shape)",
10260 );
10261 }
10262
10263 #[test]
10264 fn repositorio_projects_option_str_by_borrow() {
10265 // The by-borrow pin: [`Caixa::repositorio`] returns
10266 // `Option<&str>` by borrow — the `&str` borrows the underlying
10267 // `String` storage of the `Option<String>` slot and the
10268 // accessor must not allocate a fresh `String` on every call.
10269 // Peer of the per-`:placement`
10270 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) and the
10271 // [`Caixa::licenca`] (6d5bc28) by-borrow pins on the peer
10272 // `Option<&str>`-return axes, extended onto the second outer
10273 // top-level [`Caixa`] universal-axis `Option<&str>` shape —
10274 // the accessor's returned `&str` must borrow from `&self` (the
10275 // returned reference's lifetime is tied to `&self`), and
10276 // calling the accessor twice on the same [`Caixa`] must yield
10277 // the same `Option<&str>` verbatim (idempotent, no side effects
10278 // on `&self`).
10279 //
10280 // Pins against a future silent detour that returned an owned
10281 // `Option<String>` (which would type-check but silently
10282 // allocate on every call, breaking the zero-cost projection
10283 // every peer sibling accessor carries), or a one-arm-only
10284 // accessor that returned a saturating value on some sentinel
10285 // input (breaking the pass-through invariant the sibling
10286 // required-scalar accessors carry).
10287 for repositorio in [
10288 None,
10289 Some(""),
10290 Some("github:pleme-io/hello-rio"),
10291 Some("https://github.com/pleme-io/checkout"),
10292 ] {
10293 let c = caixa_with_repositorio(repositorio);
10294 let first = c.repositorio();
10295 let second = c.repositorio();
10296 assert_eq!(
10297 first, second,
10298 "Caixa::repositorio must be idempotent — two successive \
10299 calls on the same &self must return the same \
10300 Option<&str>",
10301 );
10302 assert_eq!(
10303 first, repositorio,
10304 "Caixa::repositorio must return :repositorio verbatim by \
10305 borrow — got {first:?}, expected {repositorio:?}",
10306 );
10307 }
10308 }
10309
10310 // ── Caixa::descricao — outer top-level Option<&str> scalar accessor ──
10311
10312 #[test]
10313 fn descricao_returns_descricao_byte_string_verbatim_across_permutations() {
10314 // The canonical per-`Caixa` `:descricao` free-form-prose scalar
10315 // pin: [`Caixa::descricao`] must return the `:descricao` typed
10316 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10317 // raw `self.descricao.as_deref()` access across every
10318 // representative value in the accept-set — `None` (the "omit
10319 // the slot to defer to the per-renderer `caixa.nome`-derived
10320 // fallback" arm every existing fixture without a `:descricao`
10321 // line carries), `Some("")` (a past-the-guard sentinel that
10322 // pins the accessor doesn't perform a silent `Some("") → None`
10323 // collapse on the empty arm — validate rejects `Some("")`
10324 // through `DescricaoEmpty` but the accessor must ship the raw
10325 // slot verbatim so a validate-time gate regression surfaces at
10326 // the caixa-helm / caixa-feira emit boundary rather than being
10327 // silently absorbed into the per-renderer `caixa.nome`-derived
10328 // fallback), `Some("Checkout flow.")` (the canonical one-line
10329 // prose descriptor the peer
10330 // `validate_descricao_accepts_canonical_value` positive sweep
10331 // exercises), `Some("Canonical Rust→wasm32-wasip2 caixa
10332 // Servico.")` (the multi-byte Unicode continuation-byte shape
10333 // the `hello-rio` fixture carries), `Some("→ — · ✓")` (a
10334 // multi-glyph Unicode shape the peer
10335 // `is_chart_description_shape` predicate accepts), and five
10336 // past-the-guard sentinels for the `DescricaoInvalid` refusal
10337 // cases (`Some(" Checkout flow.")` leading-whitespace,
10338 // `Some("Checkout flow. ")` trailing-whitespace,
10339 // `Some("Checkout\nflow.")` embedded-LF,
10340 // `Some("Checkout\tflow.")` embedded-TAB, and
10341 // `Some("Checkout\x00flow.")` embedded-NUL — the sentinels pin
10342 // the accessor doesn't silently absorb the refusal cases into
10343 // a fallback).
10344 //
10345 // Third outer top-level [`Caixa`] `Option<&str>`-return scalar
10346 // accessor pin on the substrate primitive — sibling of the peer
10347 // [`Caixa::licenca`] (6d5bc28) and [`Caixa::repositorio`]
10348 // (cc7332d) pins that opened the "outer [`Caixa`]
10349 // `Option<&str>` scalar" projection pin pattern this pin folds
10350 // on. Sibling in shape to the peer per-`:placement`
10351 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10352 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10353 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10354 // axes, extended onto the outer top-level [`Caixa`] universal-
10355 // axis surface. Pins against a future silent detour that
10356 // returned an owned `Option<String>` (which would type-check
10357 // but silently allocate on every accessor call, breaking the
10358 // zero-cost projection every peer sibling accessor carries), a
10359 // `Some("") → None` collapse (which would silently absorb the
10360 // `DescricaoEmpty` refusal case at the accessor boundary and
10361 // the caixa-helm `Chart.yaml` `description:` fold would
10362 // silently render a `caixa.nome`-derived fallback on a
10363 // struct-literal `Caixa { descricao: Some(""), .. }`), or a
10364 // `None → Some(<default>)` collapse (which would silently
10365 // reify the per-renderer `caixa.nome`-derived fallback at the
10366 // accessor boundary and every downstream consumer keying off
10367 // the `Option::is_none()` discriminator would lose the "author
10368 // omitted the slot" signal).
10369 for descricao in [
10370 None,
10371 Some(""),
10372 Some("Checkout flow."),
10373 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10374 Some("→ — · ✓"),
10375 Some(" Checkout flow."),
10376 Some("Checkout flow. "),
10377 Some("Checkout\nflow."),
10378 Some("Checkout\tflow."),
10379 Some("Checkout\x00flow."),
10380 ] {
10381 let c = caixa_with_descricao(descricao);
10382 assert_eq!(
10383 c.descricao(),
10384 descricao,
10385 "Caixa::descricao must return :descricao verbatim (got \
10386 {:?}, expected {descricao:?})",
10387 c.descricao(),
10388 );
10389 assert_eq!(
10390 c.descricao(),
10391 c.descricao.as_deref(),
10392 "Caixa::descricao must byte-equal the raw \
10393 `self.descricao.as_deref()` field access across every \
10394 value in the Option<&str> accept-set",
10395 );
10396 }
10397 }
10398
10399 #[test]
10400 fn validate_descricao_empty_arm_routes_through_accessor() {
10401 // Composition pin: [`Caixa::validate_descricao`]'s empty-arm
10402 // gate must key off [`Caixa::descricao`], not the raw
10403 // `self.descricao.as_deref()` field access. Structurally: a
10404 // `Caixa { descricao: Some(""), .. }` must surface the
10405 // `DescricaoEmpty` refusal exactly, and a
10406 // `Caixa { descricao: Some("Checkout flow."), .. }` (the
10407 // canonical one-line-prose form) must pass validate. The pair
10408 // jointly pins the accessor + validate-gate composition: any
10409 // future silent detour that had the accessor return `None` on
10410 // the empty arm (a `.filter(|s| !s.is_empty())` collapse) would
10411 // silently absorb the `DescricaoEmpty` refusal at the accessor
10412 // boundary and the validate gate would accept a struct-literal
10413 // `Caixa { descricao: Some(""), .. }` — the composition pin
10414 // catches that at caixa-core build time.
10415 //
10416 // Peer of the [`Caixa::licenca`] (6d5bc28)
10417 // `validate_licenca_empty_arm_routes_through_accessor` and
10418 // [`Caixa::repositorio`] (cc7332d)
10419 // `validate_repositorio_empty_arm_routes_through_accessor`
10420 // composition pins on the sibling outer top-level [`Caixa`]
10421 // `Option<&str>` universal-axis surface — same "the validate /
10422 // shape-gate predicate must route through the substrate-
10423 // primitive typed dispatch" discipline extended onto the third
10424 // outer top-level [`Caixa`] universal-axis `Option<&str>`-
10425 // composition surface.
10426 let c = caixa_with_descricao(Some(""));
10427 assert!(
10428 matches!(c.validate_descricao(), Err(ManifestError::DescricaoEmpty),),
10429 "validate_descricao must reject descricao == Some(\"\") \
10430 with DescricaoEmpty — the accessor and the validate gate \
10431 must route through the same substrate-primitive typed \
10432 dispatch on the :descricao empty arm",
10433 );
10434 let c = caixa_with_descricao(Some("Checkout flow."));
10435 assert!(
10436 c.validate_descricao().is_ok(),
10437 "validate_descricao must accept descricao == \
10438 Some(\"Checkout flow.\") (the canonical one-line-prose \
10439 chart-description shape)",
10440 );
10441 }
10442
10443 #[test]
10444 fn descricao_projects_option_str_by_borrow() {
10445 // The by-borrow pin: [`Caixa::descricao`] returns
10446 // `Option<&str>` by borrow — the `&str` borrows the underlying
10447 // `String` storage of the `Option<String>` slot and the
10448 // accessor must not allocate a fresh `String` on every call.
10449 // Peer of the [`Caixa::licenca`] (6d5bc28) and
10450 // [`Caixa::repositorio`] (cc7332d) by-borrow pins on the peer
10451 // outer top-level [`Caixa`] `Option<&str>`-return axes, and of
10452 // the per-`:placement`
10453 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10454 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10455 // return axis, extended onto the third outer top-level
10456 // [`Caixa`] universal-axis `Option<&str>` shape — the
10457 // accessor's returned `&str` must borrow from `&self` (the
10458 // returned reference's lifetime is tied to `&self`), and
10459 // calling the accessor twice on the same [`Caixa`] must yield
10460 // the same `Option<&str>` verbatim (idempotent, no side
10461 // effects on `&self`).
10462 //
10463 // Pins against a future silent detour that returned an owned
10464 // `Option<String>` (which would type-check but silently
10465 // allocate on every call, breaking the zero-cost projection
10466 // every peer sibling accessor carries), or a one-arm-only
10467 // accessor that returned a saturating value on some sentinel
10468 // input (breaking the pass-through invariant the sibling
10469 // required-scalar accessors carry).
10470 for descricao in [
10471 None,
10472 Some(""),
10473 Some("Checkout flow."),
10474 Some("Canonical Rust→wasm32-wasip2 caixa Servico."),
10475 ] {
10476 let c = caixa_with_descricao(descricao);
10477 let first = c.descricao();
10478 let second = c.descricao();
10479 assert_eq!(
10480 first, second,
10481 "Caixa::descricao must be idempotent — two successive \
10482 calls on the same &self must return the same \
10483 Option<&str>",
10484 );
10485 assert_eq!(
10486 first, descricao,
10487 "Caixa::descricao must return :descricao verbatim by \
10488 borrow — got {first:?}, expected {descricao:?}",
10489 );
10490 }
10491 }
10492
10493 // ── validate_edicao — universal-axis language-edition shape ──
10494
10495 fn caixa_with_edicao(edicao: Option<&str>) -> Caixa {
10496 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10497 c.edicao = edicao.map(String::from);
10498 c
10499 }
10500
10501 #[test]
10502 fn validate_edicao_accepts_none() {
10503 // The omit-the-slot identity: `:edicao` is optional. The
10504 // gate is a no-op when the author didn't declare a value —
10505 // every caixa without an `:edicao` line trivially passes,
10506 // and the substrate-side build pipeline falls back to the
10507 // documented default edition. Mirrors the peer
10508 // `validate_licenca_accepts_none` posture on the sibling
10509 // `Option<String>` Caixa slot.
10510 let c = caixa_with_edicao(None);
10511 c.validate_edicao().unwrap();
10512 }
10513
10514 #[test]
10515 fn validate_edicao_accepts_canonical_value() {
10516 // Positive control: the canonical `"2026"` edition every
10517 // existing renderer-side fixture (`caixa-helm`, `caixa-flux`,
10518 // `caixa-mesh`) carries by construction passes the gate.
10519 // Future-introduced sibling editions (`"2027"`, `"2030"`,
10520 // `"2049"`) that match the same 4-digit ASCII decimal year
10521 // shape must also trivially pass — the structural shape
10522 // predicate accepts every well-formed year regardless of
10523 // whether the substrate yet understands the specific value
10524 // (a future known-edition allowlist tightens that).
10525 for ed in ["2026", "2027", "2030", "2049"] {
10526 let c = caixa_with_edicao(Some(ed));
10527 c.validate_edicao()
10528 .unwrap_or_else(|err| panic!("canonical {ed:?} must pass: {err:?}"));
10529 }
10530 }
10531
10532 #[test]
10533 fn validate_edicao_rejects_empty_some() {
10534 // Canonical paste-from-blank-doc footgun. Without this gate
10535 // the empty `Some("")` silently lands as `(:edicao "")` in
10536 // the rendered caixa.lisp and a future renderer-side
10537 // consumer's `Option::unwrap_or_else` (which only fires on
10538 // `None`) skips its fallback. Mirrors the peer
10539 // [`ManifestError::LicencaEmpty`] empty-arm on the sibling
10540 // `Option<String>` Caixa slot.
10541 let c = caixa_with_edicao(Some(""));
10542 let err = c.validate_edicao().unwrap_err();
10543 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10544 }
10545
10546 #[test]
10547 fn validate_edicao_rejects_free_form_non_year() {
10548 // Free-form non-year footgun: the bare `"x"` / `"latest"` /
10549 // `"nightly"` shapes carry no operational meaning on the
10550 // substrate's build-time edition selector. Until this gate
10551 // landed the bare empty-arm check let every such value
10552 // through and broke far from the source caixa.lisp. Peer
10553 // with the shape-predicate cascade
10554 // `validate_repositorio_rejects_missing_colon_separator`
10555 // establishes past its own empty arm.
10556 for ed in ["x", "latest", "nightly", "stable"] {
10557 let c = caixa_with_edicao(Some(ed));
10558 let err = c.validate_edicao().unwrap_err();
10559 assert!(
10560 matches!(err, ManifestError::EdicaoInvalid { .. }),
10561 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10562 );
10563 }
10564 }
10565
10566 #[test]
10567 fn validate_edicao_rejects_trailing_whitespace() {
10568 // Paste-from-doc whitespace footgun. A trailing space in
10569 // the `:edicao` value would silently break the substrate's
10570 // build-time edition match-table lookup at the rendered
10571 // artifact's edition-selector consumer. The shape predicate
10572 // refuses every whitespace byte by construction (any byte
10573 // outside `0-9` fails `is_ascii_digit`). Peer with
10574 // `validate_repositorio_rejects_whitespace`.
10575 let c = caixa_with_edicao(Some("2026 "));
10576 let err = c.validate_edicao().unwrap_err();
10577 let ManifestError::EdicaoInvalid { edicao, .. } = err else {
10578 panic!("expected EdicaoInvalid, got {err:?}");
10579 };
10580 assert_eq!(edicao, "2026 ");
10581 }
10582
10583 #[test]
10584 fn validate_edicao_rejects_leading_whitespace() {
10585 // Symmetric paste-from-doc whitespace footgun on the leading
10586 // boundary — the gate refuses every shape with a non-digit
10587 // byte by construction.
10588 let c = caixa_with_edicao(Some(" 2026"));
10589 let err = c.validate_edicao().unwrap_err();
10590 assert!(
10591 matches!(err, ManifestError::EdicaoInvalid { .. }),
10592 "got {err:?}",
10593 );
10594 }
10595
10596 #[test]
10597 fn validate_edicao_rejects_control_char() {
10598 // Paste-from-multiline-doc CRLF footgun — control characters
10599 // at the value boundary break the substrate's build-time
10600 // edition-selector parser. Peer with
10601 // `validate_repositorio_rejects_control_char`.
10602 let c = caixa_with_edicao(Some("2026\n"));
10603 let err = c.validate_edicao().unwrap_err();
10604 assert!(
10605 matches!(err, ManifestError::EdicaoInvalid { .. }),
10606 "got {err:?}",
10607 );
10608 }
10609
10610 #[test]
10611 fn validate_edicao_rejects_non_ascii_lookalike() {
10612 // Fullwidth-keyboard look-alike footgun — `"2026"` is
10613 // the U+FF12 U+FF10 U+FF12 U+FF16 sequence (CJK fullwidth
10614 // digits), 4 codepoints but 12 UTF-8 bytes; the substrate's
10615 // edition selector wants an ASCII year, and the gate
10616 // refuses every non-ASCII shape by construction (length in
10617 // bytes is 12 ≠ 4, *and* every byte falls outside
10618 // `is_ascii_digit`'s `0-9` range).
10619 let c = caixa_with_edicao(Some("2026"));
10620 let err = c.validate_edicao().unwrap_err();
10621 assert!(
10622 matches!(err, ManifestError::EdicaoInvalid { .. }),
10623 "got {err:?}",
10624 );
10625 }
10626
10627 #[test]
10628 fn validate_edicao_rejects_version_tag_prefix() {
10629 // Common version-tag idiom footgun — `"v2026"` / `"e2026"`
10630 // / `"r2026"` are familiar shapes from git-tag / Rust
10631 // edition / release-tag conventions that don't apply to
10632 // the year-shaped edition axis. The shape predicate refuses
10633 // every leading non-digit prefix.
10634 for ed in ["v2026", "e2026", "r2026"] {
10635 let c = caixa_with_edicao(Some(ed));
10636 let err = c.validate_edicao().unwrap_err();
10637 assert!(
10638 matches!(err, ManifestError::EdicaoInvalid { .. }),
10639 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10640 );
10641 }
10642 }
10643
10644 #[test]
10645 fn validate_edicao_rejects_decimal_shape() {
10646 // Decimal-shaped pseudo-version footgun — `"2026.1"` /
10647 // `"2026.0"` are familiar shapes from semver / float
10648 // conventions that don't apply to the year-shaped edition
10649 // axis. The shape predicate refuses every non-digit byte
10650 // (`.` falls outside `is_ascii_digit`).
10651 for ed in ["2026.1", "2026.0", "2026.0.1"] {
10652 let c = caixa_with_edicao(Some(ed));
10653 let err = c.validate_edicao().unwrap_err();
10654 assert!(
10655 matches!(err, ManifestError::EdicaoInvalid { .. }),
10656 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10657 );
10658 }
10659 }
10660
10661 #[test]
10662 fn validate_edicao_rejects_wrong_length_numeric() {
10663 // Wrong-length numeric footgun — `"26"` (truncated) /
10664 // `"202"` (truncated) / `"20260"` (extra digit) / `"00026"`
10665 // (zero-padded too wide) all parse as integers but don't
10666 // name a 4-digit year. The shape predicate refuses every
10667 // value whose length isn't exactly 4 bytes.
10668 for ed in ["26", "202", "20260", "00026", "9"] {
10669 let c = caixa_with_edicao(Some(ed));
10670 let err = c.validate_edicao().unwrap_err();
10671 assert!(
10672 matches!(err, ManifestError::EdicaoInvalid { .. }),
10673 "expected EdicaoInvalid on {ed:?}, got {err:?}",
10674 );
10675 }
10676 }
10677
10678 #[test]
10679 fn validate_edicao_empty_takes_precedence_over_shape() {
10680 // Empty-first cascade pin: the empty `Some("")` surfaces
10681 // the narrower `EdicaoEmpty` not the shape-predicate-
10682 // wrapped `EdicaoInvalid`, mirroring the peer
10683 // `validate_repositorio_empty_takes_precedence_over_shape`
10684 // (`RepositorioEmpty` → `RepositorioInvalid`),
10685 // `NomeEmpty` → `NomeInvalid`, `VersaoEmpty` →
10686 // `VersaoInvalid`, `FonteRepoEmpty` → `FonteRepoInvalid`
10687 // cascades. The shape predicate also refuses the empty
10688 // input (defensively — `s.len() != 4`), but the
10689 // manifest-layer empty arm runs first to surface the
10690 // narrower diagnostic verbatim.
10691 let c = caixa_with_edicao(Some(""));
10692 let err = c.validate_edicao().unwrap_err();
10693 assert!(matches!(err, ManifestError::EdicaoEmpty), "got {err:?}",);
10694 }
10695
10696 #[test]
10697 fn validate_edicao_template_passes() {
10698 // Round-trip pin: the bare `Caixa::template` shape (which
10699 // carries `:edicao "2026"` verbatim) passes the gate by
10700 // construction. A future template-shape change that
10701 // introduced `(:edicao "")` or a non-year value would
10702 // surface here as a regression. Mirrors the peer
10703 // `validate_licenca_template_passes` pin.
10704 let c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
10705 c.validate_edicao().unwrap();
10706 }
10707
10708 #[test]
10709 fn validate_edicao_diagnostic_names_offending_slot() {
10710 // Diagnostic-shape pin (peer with
10711 // `validate_licenca_diagnostic_names_offending_slot`): the
10712 // error's Display surfaces the `:edicao` slot name verbatim,
10713 // so a `feira lint` run can render the diagnostic without
10714 // re-parsing and the author can grep their caixa.lisp for
10715 // the offending `:edicao` line.
10716 let c = caixa_with_edicao(Some(""));
10717 let rendered = c.validate_edicao().unwrap_err().to_string();
10718 assert!(
10719 rendered.contains(":edicao"),
10720 "diagnostic must name the offending slot: {rendered}",
10721 );
10722 }
10723
10724 #[test]
10725 fn validate_edicao_invalid_diagnostic_carries_offending_value() {
10726 // Diagnostic-shape pin on the shape-predicate arm (peer
10727 // with `validate_repositorio_diagnostic_carries_offending_value`):
10728 // the error's Display surfaces the offending value + slot
10729 // name verbatim, so a `feira lint` run can render the
10730 // diagnostic without re-parsing and the author can grep
10731 // their caixa.lisp for the offending `:edicao` value.
10732 let c = caixa_with_edicao(Some("v2026"));
10733 let rendered = c.validate_edicao().unwrap_err().to_string();
10734 assert!(
10735 rendered.contains(":edicao"),
10736 "diagnostic must name the offending slot: {rendered}",
10737 );
10738 assert!(
10739 rendered.contains("v2026"),
10740 "diagnostic must quote the offending value: {rendered}",
10741 );
10742 }
10743
10744 // ── Caixa::edicao — outer top-level Option<&str> scalar accessor ──
10745
10746 #[test]
10747 fn edicao_returns_edicao_byte_string_verbatim_across_permutations() {
10748 // The canonical per-`Caixa` `:edicao` language-edition scalar
10749 // pin: [`Caixa::edicao`] must return the `:edicao` typed
10750 // byte-string verbatim as an `Option<&str>`, byte-equal to the
10751 // raw `self.edicao.as_deref()` access across every representative
10752 // value in the accept-set — `None` (the "omit the slot to defer
10753 // to the substrate's default edition" arm every existing
10754 // [`caixa-resolver`] fixture without an `:edicao` line carries),
10755 // `Some("")` (a past-the-guard sentinel that pins the accessor
10756 // doesn't perform a silent `Some("") → None` collapse on the
10757 // empty arm — validate rejects `Some("")` through `EdicaoEmpty`
10758 // but the accessor must ship the raw slot verbatim so a
10759 // validate-time gate regression surfaces at any future edition-
10760 // aware consumer's boundary rather than being silently absorbed
10761 // into the substrate's default edition), `Some("2026")` (the
10762 // canonical 4-digit-ASCII-decimal-year shape every `feira init`
10763 // template scaffolds via [`Caixa::template`] and every
10764 // renderer-side fixture at `caixa-helm/src/lib.rs:978` /
10765 // `caixa-flux/src/lib.rs:2319` / `caixa-mesh/src/lib.rs:3208`
10766 // carries by construction), `Some("2018")` / `Some("2021")` /
10767 // `Some("2024")` (canonical 4-digit-ASCII-decimal-year shapes
10768 // peer with Cargo's `[package] edition` grammar every future-
10769 // introduced sibling to `"2026"` will follow), and eight
10770 // past-the-guard sentinels for the `EdicaoInvalid` refusal cases
10771 // (`Some("2026 ")` trailing-whitespace, `Some(" 2026")` leading-
10772 // whitespace, `Some("2026\n")` embedded-LF, `Some("2026")`
10773 // fullwidth-non-ASCII-lookalike, `Some("v2026")` version-tag-
10774 // prefix, `Some("2026.1")` decimal-shape, `Some("26")` wrong-
10775 // length-numeric, `Some("latest")` free-form-non-year — the
10776 // sentinels pin the accessor doesn't silently absorb the
10777 // refusal cases into a substrate-default-edition fallback).
10778 //
10779 // Fourth and final outer top-level [`Caixa`] `Option<&str>`-
10780 // return scalar accessor pin on the substrate primitive —
10781 // sibling of the peer [`Caixa::licenca`] (6d5bc28),
10782 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10783 // (3f16e2f) pins that opened the "outer [`Caixa`]
10784 // `Option<&str>` scalar" projection pin pattern this pin folds
10785 // on. Sibling in shape to the peer per-`:placement`
10786 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) /
10787 // [`crate::aplicacao::Placement::affinity`] (74ec2d3) accessor
10788 // pins on the sibling per-M3-mesh-slot `Option<&str>`-return
10789 // axes, extended onto the outer top-level [`Caixa`] universal-
10790 // axis surface's last unlifted `Option<String>` slot. Pins
10791 // against a future silent detour that returned an owned
10792 // `Option<String>` (which would type-check but silently
10793 // allocate on every accessor call, breaking the zero-cost
10794 // projection every peer sibling accessor carries), a
10795 // `Some("") → None` collapse (which would silently absorb the
10796 // `EdicaoEmpty` refusal case at the accessor boundary and any
10797 // future edition-aware consumer would silently fall back to
10798 // the substrate's default edition on a struct-literal
10799 // `Caixa { edicao: Some(""), .. }`), or a
10800 // `None → Some("2026")` collapse (which would silently reify
10801 // the substrate's default edition at the accessor boundary
10802 // and every downstream consumer keying off the
10803 // `Option::is_none()` discriminator would lose the "author
10804 // omitted the slot" signal).
10805 for edicao in [
10806 None,
10807 Some(""),
10808 Some("2026"),
10809 Some("2018"),
10810 Some("2021"),
10811 Some("2024"),
10812 Some("2026 "),
10813 Some(" 2026"),
10814 Some("2026\n"),
10815 Some("2026"),
10816 Some("v2026"),
10817 Some("2026.1"),
10818 Some("26"),
10819 Some("latest"),
10820 ] {
10821 let c = caixa_with_edicao(edicao);
10822 assert_eq!(
10823 c.edicao(),
10824 edicao,
10825 "Caixa::edicao must return :edicao verbatim (got {:?}, \
10826 expected {edicao:?})",
10827 c.edicao(),
10828 );
10829 assert_eq!(
10830 c.edicao(),
10831 c.edicao.as_deref(),
10832 "Caixa::edicao must byte-equal the raw \
10833 `self.edicao.as_deref()` field access across every \
10834 value in the Option<&str> accept-set",
10835 );
10836 }
10837 }
10838
10839 #[test]
10840 fn validate_edicao_empty_arm_routes_through_accessor() {
10841 // Composition pin: [`Caixa::validate_edicao`]'s empty-arm gate
10842 // must key off [`Caixa::edicao`], not the raw
10843 // `self.edicao.as_deref()` field access. Structurally: a
10844 // `Caixa { edicao: Some(""), .. }` must surface the
10845 // `EdicaoEmpty` refusal exactly, and a
10846 // `Caixa { edicao: Some("2026"), .. }` (the canonical
10847 // 4-digit-ASCII-decimal-year form) must pass validate. The
10848 // pair jointly pins the accessor + validate-gate composition:
10849 // any future silent detour that had the accessor return `None`
10850 // on the empty arm (a `.filter(|s| !s.is_empty())` collapse)
10851 // would silently absorb the `EdicaoEmpty` refusal at the
10852 // accessor boundary and the validate gate would accept a
10853 // struct-literal `Caixa { edicao: Some(""), .. }` — the
10854 // composition pin catches that at caixa-core build time.
10855 //
10856 // Peer of the [`Caixa::licenca`] (6d5bc28)
10857 // `validate_licenca_empty_arm_routes_through_accessor`,
10858 // [`Caixa::repositorio`] (cc7332d)
10859 // `validate_repositorio_empty_arm_routes_through_accessor`,
10860 // and [`Caixa::descricao`] (3f16e2f)
10861 // `validate_descricao_empty_arm_routes_through_accessor`
10862 // composition pins on the sibling outer top-level [`Caixa`]
10863 // `Option<&str>` universal-axis surface — same "the validate /
10864 // shape-gate predicate must route through the substrate-
10865 // primitive typed dispatch" discipline extended onto the
10866 // fourth and final outer top-level [`Caixa`] universal-axis
10867 // `Option<&str>`-composition surface, closing the accessor-
10868 // composition family.
10869 let c = caixa_with_edicao(Some(""));
10870 assert!(
10871 matches!(c.validate_edicao(), Err(ManifestError::EdicaoEmpty)),
10872 "validate_edicao must reject edicao == Some(\"\") with \
10873 EdicaoEmpty — the accessor and the validate gate must \
10874 route through the same substrate-primitive typed dispatch \
10875 on the :edicao empty arm",
10876 );
10877 let c = caixa_with_edicao(Some("2026"));
10878 assert!(
10879 c.validate_edicao().is_ok(),
10880 "validate_edicao must accept edicao == Some(\"2026\") \
10881 (the canonical 4-digit-ASCII-decimal-year shape)",
10882 );
10883 }
10884
10885 #[test]
10886 fn edicao_projects_option_str_by_borrow() {
10887 // The by-borrow pin: [`Caixa::edicao`] returns
10888 // `Option<&str>` by borrow — the `&str` borrows the underlying
10889 // `String` storage of the `Option<String>` slot and the
10890 // accessor must not allocate a fresh `String` on every call.
10891 // Peer of the [`Caixa::licenca`] (6d5bc28),
10892 // [`Caixa::repositorio`] (cc7332d), and [`Caixa::descricao`]
10893 // (3f16e2f) by-borrow pins on the peer outer top-level
10894 // [`Caixa`] `Option<&str>`-return axes, and of the
10895 // per-`:placement`
10896 // [`crate::aplicacao::Placement::shard_key`] (7cd2a28) by-
10897 // borrow pin on the peer per-M3-mesh-slot `Option<&str>`-
10898 // return axis, extended onto the fourth and final outer top-
10899 // level [`Caixa`] universal-axis `Option<&str>` shape — the
10900 // accessor's returned `&str` must borrow from `&self` (the
10901 // returned reference's lifetime is tied to `&self`), and
10902 // calling the accessor twice on the same [`Caixa`] must yield
10903 // the same `Option<&str>` verbatim (idempotent, no side
10904 // effects on `&self`).
10905 //
10906 // Pins against a future silent detour that returned an owned
10907 // `Option<String>` (which would type-check but silently
10908 // allocate on every call, breaking the zero-cost projection
10909 // every peer sibling accessor carries), or a one-arm-only
10910 // accessor that returned a saturating value on some sentinel
10911 // input (breaking the pass-through invariant the sibling
10912 // required-scalar accessors carry).
10913 for edicao in [None, Some(""), Some("2026"), Some("2018")] {
10914 let c = caixa_with_edicao(edicao);
10915 let first = c.edicao();
10916 let second = c.edicao();
10917 assert_eq!(
10918 first, second,
10919 "Caixa::edicao must be idempotent — two successive \
10920 calls on the same &self must return the same \
10921 Option<&str>",
10922 );
10923 assert_eq!(
10924 first, edicao,
10925 "Caixa::edicao must return :edicao verbatim by \
10926 borrow — got {first:?}, expected {edicao:?}",
10927 );
10928 }
10929 }
10930
10931 #[test]
10932 fn nome_returns_nome_byte_string_verbatim_across_permutations() {
10933 // The canonical per-`Caixa` `:nome` universal-axis DNS-1123-
10934 // label caixa-identity scalar pin: [`Caixa::nome`] must return
10935 // the `:nome` typed `String` verbatim as `&str`, byte-equal to
10936 // the raw field access across every representative value in
10937 // the accept-set — the canonical `"demo"` template baseline
10938 // (the same `feira init`-scaffolded default the sibling
10939 // `validate_nome_accepts_canonical_template` positive-control
10940 // gate pins), plus every sibling per-typed-slot atom accessor's
10941 // canonical positive-arm byte-string (`"catalog"` per
10942 // [`crate::aplicacao::Membro::nome`], `"cart"` per the peer
10943 // per-`:contratos` `:de`, `"hello-rio"` per the canonical
10944 // `caixa-helm`/`caixa-flux` cross-crate integration-test
10945 // fixture, `"checkout"` per the M3 mesh-slot Aplicacao
10946 // canonical example), plus every past-the-guard sentinel for
10947 // the `NomeEmpty` / `NomeInvalid` / `NomeChartNameBudgetExceeded`
10948 // refusal cases (`""`, `"Bad_Name"`, `"a"` × 56 — 56 bytes fits
10949 // the bare DNS-1123 63-byte cap but overflows the joint
10950 // `lareira-<nome>` chart-name budget the sibling
10951 // [`Caixa::validate_nome_chart_name_budget`] gate closes on).
10952 //
10953 // The past-the-guard sentinels pin the accessor doesn't
10954 // silently absorb the refusal cases into a template-derived
10955 // fallback (a future `.nome().is_empty().then(|| "demo")`
10956 // collapse would silently absorb the `NomeEmpty` refusal at
10957 // the accessor boundary and the validate gate would accept a
10958 // struct-literal `Caixa { nome: "".into(), .. }` — the pin
10959 // catches that at caixa-core build time).
10960 //
10961 // First outer top-level [`Caixa`] `&str`-return required-
10962 // scalar accessor pin — opens the "outer [`Caixa`] `&str`
10963 // required-scalar" projection pattern the sibling per-`Caixa`
10964 // `:versao` future lift folds on. Sibling in shape to the peer
10965 // per-`:membros` [`crate::aplicacao::Membro::nome`] (4a32abf)
10966 // required-`String`-carry accessor pin on the sibling per-
10967 // sub-struct required-axis, extended onto the outer top-level
10968 // [`Caixa`] universal-axis required-`String`-carry axis.
10969 for nome in [
10970 "demo",
10971 "catalog",
10972 "cart",
10973 "hello-rio",
10974 "checkout",
10975 "",
10976 "Bad_Name",
10977 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
10978 ] {
10979 let c = caixa_with_nome(nome);
10980 assert_eq!(
10981 c.nome(),
10982 nome,
10983 "Caixa::nome must return :nome verbatim (got {}, \
10984 expected {nome})",
10985 c.nome(),
10986 );
10987 assert_eq!(
10988 c.nome(),
10989 c.nome.as_str(),
10990 "Caixa::nome must byte-equal the raw .nome field \
10991 access across every value in the String accept-set",
10992 );
10993 }
10994 }
10995
10996 #[test]
10997 fn validate_nome_empty_arm_routes_through_accessor() {
10998 // Composition pin: [`Caixa::validate_nome`]'s empty-arm must
10999 // key off [`Caixa::nome`], not the raw `.nome` field access.
11000 // Structurally: a `Caixa { nome: "".into(), .. }` must surface
11001 // the `NomeEmpty` refusal exactly, and the canonical `"demo"`
11002 // template baseline (the peer positive-arm the sibling
11003 // `validate_nome_accepts_canonical_template` gate carves out)
11004 // must pass validate. The pair jointly pins the accessor +
11005 // validate-gate composition: any future silent detour that
11006 // had the accessor return a fresh `"demo"` on the empty arm
11007 // (a `.nome().is_empty().then(|| "demo")` fallback collapse)
11008 // would silently absorb the `NomeEmpty` refusal at the
11009 // accessor boundary and the validate gate would accept a
11010 // struct-literal `Caixa { nome: "".into(), .. }` — the
11011 // composition pin catches that at caixa-core build time.
11012 //
11013 // Peer of the sibling per-`Caixa`
11014 // `validate_licenca_empty_arm_routes_through_accessor` (6d5bc28)
11015 // / `validate_repositorio_empty_arm_routes_through_accessor`
11016 // (cc7332d) / `validate_descricao_empty_arm_routes_through_accessor`
11017 // (3f16e2f) / `validate_edicao_empty_arm_routes_through_accessor`
11018 // (2641cbd) composition pins on the sibling outer top-level
11019 // [`Caixa`] `Option<&str>` axes — same "the validate /
11020 // shape-gate predicate must route through the substrate-
11021 // primitive typed dispatch" discipline extended onto the peer
11022 // outer top-level [`Caixa`] required-`&str` composition axis.
11023 let c = caixa_with_nome("");
11024 assert!(
11025 matches!(c.validate_nome(), Err(ManifestError::NomeEmpty)),
11026 "validate_nome must reject nome == \"\" with NomeEmpty — \
11027 the accessor and the validate gate must route through the \
11028 same substrate-primitive typed dispatch on the :nome \
11029 empty-arm",
11030 );
11031 let c = caixa_with_nome("demo");
11032 assert!(
11033 c.validate_nome().is_ok(),
11034 "validate_nome must accept nome == \"demo\" (the canonical \
11035 DNS-1123-label template baseline)",
11036 );
11037 }
11038
11039 #[test]
11040 fn nome_projects_str_by_borrow() {
11041 // The by-borrow pin: [`Caixa::nome`] returns `&str` by borrow
11042 // — the `&str` borrows the underlying `String` storage of the
11043 // required `nome` slot and the accessor must not allocate a
11044 // fresh `String` on every call. Peer of the [`Caixa::licenca`]
11045 // (6d5bc28) / [`Caixa::repositorio`] (cc7332d) /
11046 // [`Caixa::descricao`] (3f16e2f) / [`Caixa::edicao`] (2641cbd)
11047 // by-borrow pins on the peer outer top-level [`Caixa`]
11048 // `Option<&str>`-return axes, extended onto the first outer
11049 // top-level [`Caixa`] required-`&str`-return axis — the
11050 // accessor's returned `&str` must borrow from `&self` (the
11051 // returned reference's lifetime is tied to `&self`), and
11052 // calling the accessor twice on the same [`Caixa`] must yield
11053 // the same `&str` verbatim (idempotent, no side effects on
11054 // `&self`).
11055 //
11056 // Pins against a future silent detour that returned an owned
11057 // `String` (which would type-check but silently allocate on
11058 // every call, breaking the zero-cost projection every peer
11059 // sibling accessor carries), an accidental
11060 // `.nome.to_lowercase()` detour that returned a fresh
11061 // allocation through an already-DNS-1123-lowercase-only
11062 // string (breaking a future `const fn` regression), or a
11063 // one-arm-only accessor that returned a canonicalized value
11064 // on some sentinel input (breaking the pass-through invariant
11065 // the sibling required-scalar accessors carry).
11066 for nome in ["demo", "catalog", "hello-rio", "checkout"] {
11067 let c = caixa_with_nome(nome);
11068 let first = c.nome();
11069 let second = c.nome();
11070 assert_eq!(
11071 first, second,
11072 "Caixa::nome must be idempotent — two successive calls \
11073 on the same &self must return the same &str",
11074 );
11075 assert_eq!(
11076 first, nome,
11077 "Caixa::nome must return :nome verbatim by borrow — \
11078 got {first}, expected {nome}",
11079 );
11080 }
11081 }
11082
11083 #[test]
11084 fn versao_returns_versao_byte_string_verbatim_across_permutations() {
11085 // The canonical per-`Caixa` `:versao` universal-axis SemVer-2
11086 // pinned-version scalar pin: [`Caixa::versao`] must return the
11087 // `:versao` typed `String` verbatim as `&str`, byte-equal to the
11088 // raw `.versao` field access across every representative value
11089 // in the accept-set — the canonical `"0.1.0"` template baseline
11090 // (the same `feira init`-scaffolded default the sibling
11091 // `validate_versao_accepts_canonical_template` positive-control
11092 // gate pins), plus every canonical SemVer-2 shape the sibling
11093 // `validate_versao_accepts_canonical_forms` positive-arm sweep
11094 // covers (`"0.0.0"`, `"1.0.0"`, `"0.2.0-rc.1"`,
11095 // `"1.0.0-alpha.0"`, `"1.0.0+build.42"`, `"1.0.0-rc.1+build.42"`,
11096 // `"10.20.30"`), plus every past-the-guard sentinel for the
11097 // `VersaoEmpty` / `VersaoInvalid` refusal cases (`""` the empty
11098 // arm, `"v0.1.0"` the git-tag-shape-leak footgun, `"0.1"` the
11099 // missing-patch footgun, `"^0.1"` the requirement-shape-leak
11100 // footgun, `"0.1.0.0"` the four-part-Java-convention footgun,
11101 // `"latest"` the docker-tag-shape footgun — the sentinels pin
11102 // the accessor doesn't silently absorb the refusal cases into a
11103 // template-derived fallback like `"0.1.0"`).
11104 //
11105 // The past-the-guard sentinels pin the accessor doesn't silently
11106 // absorb the refusal cases into a template-derived fallback (a
11107 // future `.versao().is_empty().then(|| "0.1.0")` collapse would
11108 // silently absorb the `VersaoEmpty` refusal at the accessor
11109 // boundary and the validate gate would accept a struct-literal
11110 // `Caixa { versao: "".into(), .. }` — the pin catches that at
11111 // caixa-core build time).
11112 //
11113 // Second outer top-level [`Caixa`] `&str`-return required-scalar
11114 // accessor pin — folds on the "outer [`Caixa`] `&str` required-
11115 // scalar" projection pattern the sibling per-`Caixa`
11116 // [`Caixa::nome`] (e6b7d97) opened. Sibling in shape to the peer
11117 // per-`:membros` [`crate::aplicacao::Membro::versao_requirement`]
11118 // (4127bb6) / per-`:children`
11119 // [`crate::supervisor::ChildSpec::versao_requirement`] (2c053c8)
11120 // / per-`:upgrade-from`
11121 // [`crate::UpgradeFromEntry::prior_versao`] (75d27a8) per-sub-
11122 // struct `:versao`-shaped `&str`-return accessor pins on the
11123 // sibling per-typed-slot version-carrier axes, extended onto the
11124 // second outer top-level [`Caixa`] universal-axis required-
11125 // `String`-carry axis so the two universal-axis identity-
11126 // carrying scalars every `defcaixa` form supplies (`:nome` +
11127 // `:versao`) share the same "one typed dispatch per axis" pin
11128 // discipline.
11129 for versao in [
11130 "0.1.0",
11131 "0.0.0",
11132 "1.0.0",
11133 "0.2.0-rc.1",
11134 "1.0.0-alpha.0",
11135 "1.0.0+build.42",
11136 "1.0.0-rc.1+build.42",
11137 "10.20.30",
11138 "",
11139 "v0.1.0",
11140 "0.1",
11141 "^0.1",
11142 "0.1.0.0",
11143 "latest",
11144 ] {
11145 let c = caixa_with_versao(versao);
11146 assert_eq!(
11147 c.versao(),
11148 versao,
11149 "Caixa::versao must return :versao verbatim (got {}, \
11150 expected {versao})",
11151 c.versao(),
11152 );
11153 assert_eq!(
11154 c.versao(),
11155 c.versao.as_str(),
11156 "Caixa::versao must byte-equal the raw .versao field \
11157 access across every value in the String accept-set",
11158 );
11159 }
11160 }
11161
11162 #[test]
11163 fn validate_versao_empty_arm_routes_through_accessor() {
11164 // Composition pin: [`Caixa::validate_versao`]'s empty-arm gate
11165 // must key off [`Caixa::versao`], not the raw `.versao` field
11166 // access. Structurally: a `Caixa { versao: "".into(), .. }` must
11167 // surface the `VersaoEmpty` refusal exactly, and the canonical
11168 // `"0.1.0"` template baseline (the peer positive-arm the sibling
11169 // `validate_versao_accepts_canonical_template` gate carves out)
11170 // must pass validate. The pair jointly pins the accessor +
11171 // validate-gate composition: any future silent detour that had
11172 // the accessor return a fresh `"0.1.0"` on the empty arm
11173 // (a `.versao().is_empty().then(|| "0.1.0")` fallback collapse)
11174 // would silently absorb the `VersaoEmpty` refusal at the
11175 // accessor boundary and the validate gate would accept a
11176 // struct-literal `Caixa { versao: "".into(), .. }` — the
11177 // composition pin catches that at caixa-core build time.
11178 //
11179 // Peer of the sibling per-`Caixa`
11180 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97)
11181 // composition pin on the sibling outer top-level [`Caixa`]
11182 // required-`&str` universal-axis surface — same "the validate /
11183 // shape-gate predicate must route through the substrate-
11184 // primitive typed dispatch" discipline extended onto the peer
11185 // outer top-level [`Caixa`] required-`&str` universal-axis
11186 // pinned-version composition axis, closing the second
11187 // coordinate of the "one canonical typed dispatch per per-Caixa
11188 // required-`&str` universal-axis" discipline.
11189 let c = caixa_with_versao("");
11190 assert!(
11191 matches!(c.validate_versao(), Err(ManifestError::VersaoEmpty)),
11192 "validate_versao must reject versao == \"\" with VersaoEmpty — \
11193 the accessor and the validate gate must route through the \
11194 same substrate-primitive typed dispatch on the :versao \
11195 empty-arm",
11196 );
11197 let c = caixa_with_versao("0.1.0");
11198 assert!(
11199 c.validate_versao().is_ok(),
11200 "validate_versao must accept versao == \"0.1.0\" (the \
11201 canonical SemVer-2 template baseline)",
11202 );
11203 }
11204
11205 #[test]
11206 fn versao_projects_str_by_borrow() {
11207 // The by-borrow pin: [`Caixa::versao`] returns `&str` by borrow
11208 // — the `&str` borrows the underlying `String` storage of the
11209 // required `versao` slot and the accessor must not allocate a
11210 // fresh `String` on every call. Peer of the [`Caixa::nome`]
11211 // (e6b7d97) by-borrow pin on the sibling outer top-level
11212 // [`Caixa`] required-`&str`-return axis, extended onto the
11213 // second outer top-level [`Caixa`] required-`&str`-return
11214 // universal-axis pinned-version surface — the accessor's
11215 // returned `&str` must borrow from `&self` (the returned
11216 // reference's lifetime is tied to `&self`), and calling the
11217 // accessor twice on the same [`Caixa`] must yield the same
11218 // `&str` verbatim (idempotent, no side effects on `&self`).
11219 //
11220 // Pins against a future silent detour that returned an owned
11221 // `String` (which would type-check but silently allocate on
11222 // every call, breaking the zero-cost projection every peer
11223 // sibling accessor carries), an accidental
11224 // `semver::Version::parse(&self.versao).unwrap().to_string()`
11225 // detour that returned a canonicalized fresh allocation through
11226 // an already-canonical byte-string (breaking a future `const fn`
11227 // regression and silently absorbing the `VersaoInvalid` refusal
11228 // at the accessor boundary), or a one-arm-only accessor that
11229 // returned a canonicalized value on some sentinel input
11230 // (breaking the pass-through invariant the sibling required-
11231 // scalar accessors carry).
11232 for versao in ["0.1.0", "1.0.0", "0.2.0-rc.1", "1.0.0+build.42"] {
11233 let c = caixa_with_versao(versao);
11234 let first = c.versao();
11235 let second = c.versao();
11236 assert_eq!(
11237 first, second,
11238 "Caixa::versao must be idempotent — two successive \
11239 calls on the same &self must return the same &str",
11240 );
11241 assert_eq!(
11242 first, versao,
11243 "Caixa::versao must return :versao verbatim by borrow \
11244 — got {first}, expected {versao}",
11245 );
11246 }
11247 }
11248
11249 fn caixa_with_kind(kind: CaixaKind) -> Caixa {
11250 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
11251 c.kind = kind;
11252 c
11253 }
11254
11255 #[test]
11256 fn kind_returns_kind_variant_verbatim_across_permutations() {
11257 // The canonical per-`Caixa` `:kind` universal-axis closed-set-
11258 // enum discriminant pin: [`Caixa::kind`] must return the `:kind`
11259 // typed [`CaixaKind`] variant verbatim by `Copy`, byte-equal to
11260 // the raw `.kind` field access across every variant in the
11261 // closed accept-set (`Biblioteca` — the library kind that
11262 // exports lisp forms; `Binario` — the nix-built executable kind
11263 // under `exe/`; `Servico` — the wasm-component daemon kind
11264 // under `servicos/`; `Supervisor` — the OTP-shaped hierarchical
11265 // reconciliation kind; `Aplicacao` — the M3 typed-mesh
11266 // composition kind).
11267 //
11268 // Pins against a future silent detour that re-derived the kind
11269 // from a peer axis (an accidental fallback to
11270 // `if !servicos.is_empty() { Servico } else if
11271 // !membros.is_empty() { Aplicacao } else { Biblioteca }`
11272 // collapse that read the code-surface / mesh-slot columns into
11273 // the kind discriminator), a variant remap the operator
11274 // authors on one consumer without the other, or a stale-derive
11275 // detour that substituted [`CaixaKind::Biblioteca`] as the
11276 // default when the field held any other variant (which would
11277 // silently collapse the distinction between "author explicitly
11278 // declared `:kind Servico`" and "author declared any other
11279 // kind" every downstream renderer-dispatch site depends on).
11280 //
11281 // First outer top-level [`Caixa`] `Copy`-return required-enum-
11282 // discriminant accessor pin — opens the "outer [`Caixa`]
11283 // `Copy`-return required-discriminant" projection pattern.
11284 // Sibling in shape to the peer per-`:supervisor`
11285 // [`crate::supervisor::SupervisorSpec::estrategia`] (eafb619),
11286 // per-`:placement` [`crate::aplicacao::Placement::estrategia`]
11287 // (921fe1b), and per-`:children`
11288 // [`crate::supervisor::ChildSpec::restart`] (dfb4a81)
11289 // `Copy`-return closed-set-enum discriminant accessor pins on
11290 // the sibling nested-spec typed-slot discriminator axes,
11291 // extended here to the outer top-level [`Caixa`] universal-
11292 // axis surface.
11293 for kind in [
11294 CaixaKind::Biblioteca,
11295 CaixaKind::Binario,
11296 CaixaKind::Servico,
11297 CaixaKind::Supervisor,
11298 CaixaKind::Aplicacao,
11299 ] {
11300 let c = caixa_with_kind(kind);
11301 assert_eq!(
11302 c.kind(),
11303 kind,
11304 "Caixa::kind must return :kind verbatim (got {:?}, \
11305 expected {kind:?})",
11306 c.kind(),
11307 );
11308 assert_eq!(
11309 c.kind(),
11310 c.kind,
11311 "Caixa::kind accessor and .kind field access must \
11312 byte-equal — the accessor is the substrate-primitive \
11313 typed dispatch every downstream kind-gate consumer \
11314 must route through",
11315 );
11316 }
11317 }
11318
11319 #[test]
11320 fn require_kind_reads_through_lifted_kind_accessor() {
11321 // Two-consumer coherence pin: the [`crate::render::require_kind`]
11322 // entry-gate predicate (the canonical two-line
11323 // `require_kind(caixa, Servico)?` prelude every per-Servico /
11324 // per-Aplicacao renderer runs at its entry-point) and the
11325 // sibling [`crate::render::KindMismatch`] error carrier's
11326 // `actual:` field (which names the offending caixa's variant
11327 // in the diagnostic) must both key off the lifted accessor, so
11328 // any future rebrand on the typed slot's reader shape lands at
11329 // exactly one place. Pins the two-site coherence by exercising
11330 // every off-diagonal `(actual, expected)` pair across the
11331 // closed accept-set — the `KindMismatch { actual, expected }`
11332 // surfaced on the mismatch arm must byte-equal the pair the
11333 // accessor returns for each side.
11334 //
11335 // Peer of the sibling per-`:placement`
11336 // `validate_placement_reads_through_lifted_estrategia_accessor`
11337 // (921fe1b) two-arm consumer-coherence pin on the M3 mesh-slot
11338 // `Copy`-return discriminant axis — same "the entry-gate
11339 // predicate and the error carrier's `actual:` field must route
11340 // through the substrate-primitive typed dispatch" discipline
11341 // extended onto the outer top-level [`Caixa`] universal-axis
11342 // discriminant surface.
11343 for expected in [
11344 CaixaKind::Biblioteca,
11345 CaixaKind::Binario,
11346 CaixaKind::Servico,
11347 CaixaKind::Supervisor,
11348 CaixaKind::Aplicacao,
11349 ] {
11350 for actual in [
11351 CaixaKind::Biblioteca,
11352 CaixaKind::Binario,
11353 CaixaKind::Servico,
11354 CaixaKind::Supervisor,
11355 CaixaKind::Aplicacao,
11356 ] {
11357 let c = caixa_with_kind(actual);
11358 let result = crate::render::require_kind(&c, expected);
11359 if expected == actual {
11360 assert!(
11361 result.is_ok(),
11362 "require_kind must accept when actual == expected \
11363 (actual={actual:?}, expected={expected:?})",
11364 );
11365 } else {
11366 let err = result.expect_err("require_kind must reject when actual != expected");
11367 assert_eq!(
11368 err.actual,
11369 c.kind(),
11370 "KindMismatch.actual must byte-equal Caixa::kind() \
11371 — the error carrier's `actual:` field reads \
11372 through the lifted accessor",
11373 );
11374 assert_eq!(
11375 err.expected, expected,
11376 "KindMismatch.expected must byte-equal the \
11377 expected variant passed to require_kind",
11378 );
11379 }
11380 }
11381 }
11382 }
11383
11384 #[test]
11385 fn aplicacao_view_kind_gate_routes_through_accessor() {
11386 // Composition pin: [`Caixa::aplicacao_view`]'s kind-gate arm
11387 // must key off [`Caixa::kind`], not the raw `.kind` field
11388 // access. Structurally: a `Caixa { kind: X, .. }` for any
11389 // non-`Aplicacao` variant must fold to `None` on the
11390 // `aplicacao_view` composer (the "kind mismatch → no typed
11391 // view" contract every downstream Aplicacao consumer keys off
11392 // via `?`), and a `Caixa { kind: Aplicacao, .. }` must fold to
11393 // `Some(_)`. The pair jointly pins the accessor + view-gate
11394 // composition: any future silent detour that had the accessor
11395 // return a fresh [`CaixaKind::Aplicacao`] on some sentinel
11396 // input would silently absorb the kind-mismatch case at the
11397 // accessor boundary and every per-Aplicacao renderer would
11398 // silently render a non-Aplicacao caixa's mesh slots — the
11399 // composition pin catches that at caixa-core build time.
11400 //
11401 // Peer of the sibling per-`Caixa`
11402 // `validate_nome_empty_arm_routes_through_accessor` (e6b7d97) /
11403 // `validate_versao_empty_arm_routes_through_accessor` (20c0539)
11404 // composition pins on the sibling outer top-level [`Caixa`]
11405 // required-`&str` universal-axis surfaces — same "the
11406 // composer / validate gate must route through the substrate-
11407 // primitive typed dispatch" discipline extended onto the
11408 // outer top-level [`Caixa`] `Copy`-return required-
11409 // discriminant composition axis.
11410 for kind in [
11411 CaixaKind::Biblioteca,
11412 CaixaKind::Binario,
11413 CaixaKind::Servico,
11414 CaixaKind::Supervisor,
11415 ] {
11416 let c = caixa_with_kind(kind);
11417 assert!(
11418 c.aplicacao_view().is_none(),
11419 "aplicacao_view must return None on non-Aplicacao \
11420 kind {kind:?} — the composer's kind-gate must route \
11421 through Caixa::kind()",
11422 );
11423 }
11424 let c = caixa_with_kind(CaixaKind::Aplicacao);
11425 assert!(
11426 c.aplicacao_view().is_some(),
11427 "aplicacao_view must return Some on kind Aplicacao — \
11428 the composer's kind-gate must accept the matching arm \
11429 through Caixa::kind()",
11430 );
11431 }
11432
11433 #[test]
11434 fn supervisor_view_kind_gate_routes_through_accessor() {
11435 // Composition pin (mirror of the sibling
11436 // `aplicacao_view_kind_gate_routes_through_accessor` on the
11437 // second `_view` composer): [`Caixa::supervisor_view`]'s kind-
11438 // gate arm must key off [`Caixa::kind`], not the raw `.kind`
11439 // field access. A `Caixa { kind: X, .. }` for any non-
11440 // `Supervisor` variant must fold to `None` on the
11441 // `supervisor_view` composer, and a `Caixa { kind:
11442 // Supervisor, .. }` must fold to `Some(_)`. Same peer
11443 // composition pin discipline on the second `_view` composer
11444 // axis.
11445 for kind in [
11446 CaixaKind::Biblioteca,
11447 CaixaKind::Binario,
11448 CaixaKind::Servico,
11449 CaixaKind::Aplicacao,
11450 ] {
11451 let c = caixa_with_kind(kind);
11452 assert!(
11453 c.supervisor_view().is_none(),
11454 "supervisor_view must return None on non-Supervisor \
11455 kind {kind:?} — the composer's kind-gate must route \
11456 through Caixa::kind()",
11457 );
11458 }
11459 let mut c = caixa_with_kind(CaixaKind::Supervisor);
11460 // A Supervisor caixa needs a strategy + at least one child to
11461 // fold to a Some(_) that also validates; the composer itself
11462 // requires only the kind arm, so bare kind flip is enough to
11463 // pin the `Some(_)` return, but we populate the minimum
11464 // supervisor shape so a future strengthening of the composer
11465 // to reject an empty spec doesn't false-positive this pin.
11466 c.estrategia = Some(crate::supervisor::RestartStrategy::OneForOne);
11467 c.children = vec![crate::supervisor::ChildSpec {
11468 caixa: "child".into(),
11469 versao: "^0.1".into(),
11470 restart: crate::supervisor::RestartPolicy::Permanent,
11471 }];
11472 assert!(
11473 c.supervisor_view().is_some(),
11474 "supervisor_view must return Some on kind Supervisor — \
11475 the composer's kind-gate must accept the matching arm \
11476 through Caixa::kind()",
11477 );
11478 }
11479
11480 #[test]
11481 fn kind_projects_by_copy() {
11482 // The by-`Copy` pin: [`Caixa::kind`] returns a fresh
11483 // [`CaixaKind`] by `Copy` — the accessor must not borrow from
11484 // `&self` (the returned value is owned, `Copy`-projected from
11485 // the underlying [`CaixaKind`] storage; two calls on the same
11486 // [`Caixa`] must yield byte-equal values). Peer of the peer
11487 // per-`:placement` `Placement::estrategia` / per-`:supervisor`
11488 // `SupervisorSpec::estrategia` / per-`:children`
11489 // `ChildSpec::restart` `Copy`-return discriminant accessor
11490 // pins on the sibling nested-spec typed-slot discriminator
11491 // axes, extended onto the first outer top-level [`Caixa`]
11492 // required-`Copy`-return axis — pins against a future silent
11493 // detour that returned `&CaixaKind` (which would type-check
11494 // but silently constrain every consumer's callsite to a
11495 // borrow-shaped dispatch, breaking the zero-cost `Copy`
11496 // projection every peer sibling accessor carries).
11497 for kind in [
11498 CaixaKind::Biblioteca,
11499 CaixaKind::Binario,
11500 CaixaKind::Servico,
11501 CaixaKind::Supervisor,
11502 CaixaKind::Aplicacao,
11503 ] {
11504 let c = caixa_with_kind(kind);
11505 let first: CaixaKind = c.kind();
11506 let second: CaixaKind = c.kind();
11507 assert_eq!(
11508 first, second,
11509 "Caixa::kind must be idempotent — two successive \
11510 calls on the same &self must return the same \
11511 CaixaKind variant",
11512 );
11513 assert_eq!(
11514 first, kind,
11515 "Caixa::kind must return :kind verbatim by Copy — \
11516 got {first:?}, expected {kind:?}",
11517 );
11518 }
11519 }
11520
11521 // ── Caixa::autores — outer top-level &[T] slice accessor ──────────
11522
11523 #[test]
11524 fn autores_returns_autores_slice_verbatim_across_permutations() {
11525 // The canonical per-`Caixa` `:autores` universal-axis maintainer-
11526 // name-list slice pin: [`Caixa::autores`] must return the
11527 // `:autores` typed [`Vec<String>`] list verbatim as a
11528 // `&[String]`, byte-equal to the raw `self.autores.as_slice()`
11529 // access across every representative value in the accept-set —
11530 // `[]` (the "no maintainers declared" arm every existing
11531 // fixture without an `:autores` line carries), `[""]` (a past-
11532 // the-guard sentinel that pins the accessor doesn't perform a
11533 // silent `[""] → []` collapse on the empty-entry arm — validate
11534 // rejects `[""]` through `AutorEmpty` but the accessor must
11535 // ship the raw slot verbatim so a validate-time gate regression
11536 // surfaces at the caixa-helm emit boundary rather than being
11537 // silently absorbed into a maintainer-drop), `["pleme-io"]` (the
11538 // canonical single-maintainer form every `feira init` template
11539 // scaffolds), `["alice", "bob"]` (a canonical multi-maintainer
11540 // form), `["alice <alice@example.com>", "bob <bob@example.com>"]`
11541 // (the canonical RFC-5322 `<name> <email>` form the
11542 // `is_chart_maintainer_name_shape` predicate accepts), and
11543 // `["pleme-io", "pleme-io"]` (a past-the-guard duplicate
11544 // sentinel — validate rejects through `AutorDuplicate` but the
11545 // accessor must ship the raw slot verbatim).
11546 //
11547 // First outer top-level [`Caixa`] `&[T]`-return slice accessor
11548 // pin on the substrate primitive — opens the "outer [`Caixa`]
11549 // `&[T]` slice" projection pattern the sibling per-`Caixa`
11550 // `:etiquetas` / `:deps` / `:deps-dev` / `:exe` / `:bibliotecas`
11551 // / `:servicos` / `:upgrade-from` / `:children` future lifts
11552 // fold on. Sibling in shape to the peer per-`:supervisor`
11553 // [`crate::supervisor::SupervisorSpec::children`] (bc92bce),
11554 // per-`:placement` [`crate::aplicacao::Placement::clusters`]
11555 // (a6e18d7), per-`:membros`
11556 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36),
11557 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11558 // (0dcc926), and per-`:upgrade-from :instructions`
11559 // [`crate::upgrade::UpgradeFromEntry::instructions`] (0137e5a)
11560 // `&[T]`-return slice accessor pins on the sibling per-M2 /
11561 // per-M3 typed-slot list axes, extended onto the outer top-
11562 // level [`Caixa`] universal-axis surface. Pins against a future
11563 // silent detour that returned an owned `Vec<String>` (which
11564 // would type-check but silently clone on every accessor call,
11565 // breaking the zero-cost projection every peer sibling slice
11566 // accessor carries), a `[""] → []` collapse (which would
11567 // silently absorb the `AutorEmpty` refusal case at the accessor
11568 // boundary), or a `["a", "a"] → ["a"]` dedup collapse (which
11569 // would silently absorb the `AutorDuplicate` refusal case at
11570 // the accessor boundary and the caixa-helm `maintainers:` fold
11571 // would silently render a dedupped list on a struct-literal
11572 // `Caixa { autores: vec!["a".into(), "a".into()], .. }`).
11573 for autores in [
11574 vec![],
11575 vec![""],
11576 vec!["pleme-io"],
11577 vec!["alice", "bob"],
11578 vec!["alice <alice@example.com>", "bob <bob@example.com>"],
11579 vec!["pleme-io", "pleme-io"],
11580 ] {
11581 let c = caixa_with_autores(autores.clone());
11582 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11583 assert_eq!(
11584 c.autores(),
11585 expected.as_slice(),
11586 "Caixa::autores must return :autores verbatim (got {:?}, \
11587 expected {expected:?})",
11588 c.autores(),
11589 );
11590 assert_eq!(
11591 c.autores(),
11592 c.autores.as_slice(),
11593 "Caixa::autores must byte-equal the raw \
11594 `self.autores.as_slice()` field access across every \
11595 value in the Vec<String> accept-set",
11596 );
11597 }
11598 }
11599
11600 #[test]
11601 fn validate_autores_empty_entry_arm_routes_through_accessor() {
11602 // Composition pin: [`Caixa::validate_autores`]'s per-entry
11603 // empty-arm gate must key off [`Caixa::autores`], not the raw
11604 // `&self.autores` field-borrow walk. Structurally: a
11605 // `Caixa { autores: vec!["".into()], .. }` must surface the
11606 // `AutorEmpty` refusal exactly, and a
11607 // `Caixa { autores: vec!["pleme-io".into()], .. }` (the
11608 // canonical single-maintainer form) must pass validate. The
11609 // pair jointly pins the accessor + validate-gate composition:
11610 // any future silent detour that had the accessor return an
11611 // empty slice on the `[""]` arm (a
11612 // `.iter().filter(|s| !s.is_empty()).collect()` collapse)
11613 // would silently absorb the `AutorEmpty` refusal at the
11614 // accessor boundary and the validate gate would accept a
11615 // struct-literal `Caixa { autores: vec!["".into()], .. }` —
11616 // the composition pin catches that at caixa-core build time.
11617 //
11618 // Peer of the per-`Caixa` [`Caixa::validate_licenca`] (6d5bc28)
11619 // accessor-composition pin
11620 // (`validate_licenca_empty_arm_routes_through_accessor`) on the
11621 // sibling `Option<&str>`-composition axis and the
11622 // per-`:politicas :circuit-breaker`
11623 // [`crate::aplicacao::CircuitBreaker::max_failures`] (3a74062)
11624 // accessor-composition pin
11625 // (`validate_politicas_max_failures_zero_floor_arm_routes_through_accessor`)
11626 // on the sibling required-`u32`-composition axis — same "the
11627 // validate / shape-gate predicate must route through the
11628 // substrate-primitive typed dispatch" discipline extended onto
11629 // the outer top-level [`Caixa`] universal-axis `&[T]`-
11630 // composition surface.
11631 let c = caixa_with_autores(vec![""]);
11632 assert!(
11633 matches!(c.validate_autores(), Err(ManifestError::AutorEmpty)),
11634 "validate_autores must reject autores == vec![\"\"] with \
11635 AutorEmpty — the accessor and the validate gate must \
11636 route through the same substrate-primitive typed dispatch \
11637 on the :autores per-entry empty arm",
11638 );
11639 let c = caixa_with_autores(vec!["pleme-io"]);
11640 assert!(
11641 c.validate_autores().is_ok(),
11642 "validate_autores must accept autores == vec![\"pleme-io\"] \
11643 (the canonical single-maintainer shape every `feira init` \
11644 template scaffolds)",
11645 );
11646 }
11647
11648 #[test]
11649 fn autores_projects_slice_by_borrow() {
11650 // The by-borrow pin: [`Caixa::autores`] returns `&[String]` by
11651 // borrow — the returned slice borrows the underlying
11652 // `Vec<String>` storage of the `:autores` slot and the
11653 // accessor must not clone the backing `Vec` on every call.
11654 // Peer of the per-`:membros`
11655 // [`crate::aplicacao::AplicacaoSpec::membros`] (6c77e36) /
11656 // per-`:contratos` [`crate::aplicacao::AplicacaoSpec::contratos`]
11657 // (0dcc926) / per-`:placement`
11658 // [`crate::aplicacao::Placement::clusters`] (a6e18d7) /
11659 // per-`:supervisor` [`crate::supervisor::SupervisorSpec::children`]
11660 // (bc92bce) by-borrow pins on the sibling per-M2 / per-M3
11661 // typed-slot `&[T]`-return axes, extended onto the outer top-
11662 // level [`Caixa`] universal-axis `&[String]` shape — the
11663 // accessor's returned slice must borrow from `&self` (the
11664 // returned reference's lifetime is tied to `&self`), and
11665 // calling the accessor twice on the same [`Caixa`] must yield
11666 // slices that are pointer-equal (the underlying byte-buffer is
11667 // the storage `Vec`'s allocation, not a fresh copy) as well as
11668 // value-equal (idempotent, no side effects on `&self`).
11669 //
11670 // Pins against a future silent detour that returned an owned
11671 // `Vec<String>` (which would type-check but silently clone on
11672 // every call, breaking the zero-cost projection every peer
11673 // sibling slice accessor carries), a `&Vec<String>` return
11674 // (which would leak the backing `Vec`'s grow/push/reserve
11675 // surface no downstream consumer reaches for), or a one-arm-
11676 // only accessor that returned a saturating value on some
11677 // sentinel input (breaking the pass-through invariant the
11678 // sibling slice accessors carry).
11679 for autores in [
11680 vec![],
11681 vec!["pleme-io"],
11682 vec!["alice", "bob"],
11683 vec!["pleme-io", "pleme-io"],
11684 ] {
11685 let c = caixa_with_autores(autores.clone());
11686 let expected: Vec<String> = autores.iter().map(|s| (*s).to_string()).collect();
11687 let first = c.autores();
11688 let second = c.autores();
11689 assert_eq!(
11690 first, second,
11691 "Caixa::autores must be idempotent — two successive \
11692 calls on the same &self must return the same \
11693 &[String]",
11694 );
11695 assert_eq!(
11696 first.as_ptr(),
11697 second.as_ptr(),
11698 "Caixa::autores must borrow the underlying Vec<String> \
11699 storage — two successive calls must return slices \
11700 with the same backing pointer (a fresh Vec<String> \
11701 clone would change the pointer on every call)",
11702 );
11703 assert_eq!(
11704 first,
11705 expected.as_slice(),
11706 "Caixa::autores must return :autores verbatim by \
11707 borrow — got {first:?}, expected {expected:?}",
11708 );
11709 }
11710 }
11711
11712 // ── Caixa::etiquetas — outer top-level &[T] slice accessor ────────
11713
11714 #[test]
11715 fn etiquetas_returns_etiquetas_slice_verbatim_across_permutations() {
11716 // The canonical per-`Caixa` `:etiquetas` universal-axis
11717 // registry-search-tag-list slice pin: [`Caixa::etiquetas`] must
11718 // return the `:etiquetas` typed [`Vec<String>`] list verbatim
11719 // as a `&[String]`, byte-equal to the raw
11720 // `self.etiquetas.as_slice()` access across every representative
11721 // value in the accept-set — `[]` (the "no tags declared" arm
11722 // every existing fixture without an `:etiquetas` line carries),
11723 // `[""]` (a past-the-guard sentinel that pins the accessor
11724 // doesn't perform a silent `[""] → []` collapse on the empty-
11725 // entry arm — validate rejects `[""]` through `EtiquetaEmpty`
11726 // but the accessor must ship the raw slot verbatim so a
11727 // validate-time gate regression surfaces at the caixa-helm emit
11728 // boundary rather than being silently absorbed into a keyword-
11729 // drop), `["demo"]` (the canonical single-tag form every
11730 // `feira init` template scaffolds), `["example", "aplicacao",
11731 // "mesh", "ecommerce", "demo"]` (the canonical multi-tag form
11732 // the checkout-aplicacao fixture emits), and `["demo", "demo"]`
11733 // (a past-the-guard duplicate sentinel — validate rejects
11734 // through `EtiquetaDuplicate` but the accessor must ship the
11735 // raw slot verbatim so the caixa-helm `BTreeSet::collect` dedup
11736 // at chart-render time isn't silently promoted into the
11737 // accessor boundary and struct-literal
11738 // `Caixa { etiquetas: vec!["demo".into(), "demo".into()], .. }`
11739 // fixtures continue to expose the duplicate at the accessor).
11740 //
11741 // Second outer top-level [`Caixa`] `&[T]`-return slice accessor
11742 // pin on the substrate primitive — folds on the "outer
11743 // [`Caixa`] `&[T]` slice" projection pattern
11744 // `autores_returns_autores_slice_verbatim_across_permutations`
11745 // (b5d813f) opened, sibling in shape and idiom. Pins against a
11746 // future silent detour that returned an owned `Vec<String>`
11747 // (which would type-check but silently clone on every accessor
11748 // call, breaking the zero-cost projection every peer sibling
11749 // slice accessor carries), a `[""] → []` collapse (which would
11750 // silently absorb the `EtiquetaEmpty` refusal case at the
11751 // accessor boundary), or a `["a", "a"] → ["a"]` dedup collapse
11752 // (which would silently absorb the `EtiquetaDuplicate` refusal
11753 // case at the accessor boundary — the caixa-helm chart-render
11754 // `BTreeSet::collect` dedup is downstream of the accessor and
11755 // must not be silently promoted into it).
11756 for etiquetas in [
11757 vec![],
11758 vec![""],
11759 vec!["demo"],
11760 vec!["example", "aplicacao", "mesh", "ecommerce", "demo"],
11761 vec!["demo", "demo"],
11762 ] {
11763 let c = caixa_with_etiquetas(etiquetas.clone());
11764 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11765 assert_eq!(
11766 c.etiquetas(),
11767 expected.as_slice(),
11768 "Caixa::etiquetas must return :etiquetas verbatim (got \
11769 {:?}, expected {expected:?})",
11770 c.etiquetas(),
11771 );
11772 assert_eq!(
11773 c.etiquetas(),
11774 c.etiquetas.as_slice(),
11775 "Caixa::etiquetas must byte-equal the raw \
11776 `self.etiquetas.as_slice()` field access across every \
11777 value in the Vec<String> accept-set",
11778 );
11779 }
11780 }
11781
11782 #[test]
11783 fn validate_etiquetas_empty_entry_arm_routes_through_accessor() {
11784 // Composition pin: [`Caixa::validate_etiquetas`]'s per-entry
11785 // empty-arm gate must key off [`Caixa::etiquetas`], not the raw
11786 // `&self.etiquetas` field-borrow walk. Structurally: a
11787 // `Caixa { etiquetas: vec!["".into()], .. }` must surface the
11788 // `EtiquetaEmpty` refusal exactly, and a
11789 // `Caixa { etiquetas: vec!["demo".into()], .. }` (the canonical
11790 // single-tag form) must pass validate. The pair jointly pins
11791 // the accessor + validate-gate composition: any future silent
11792 // detour that had the accessor return an empty slice on the
11793 // `[""]` arm (a
11794 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
11795 // silently absorb the `EtiquetaEmpty` refusal at the accessor
11796 // boundary and the validate gate would accept a struct-literal
11797 // `Caixa { etiquetas: vec!["".into()], .. }` — the composition
11798 // pin catches that at caixa-core build time.
11799 //
11800 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11801 // through_accessor` (b5d813f) accessor-composition pin on the
11802 // sibling `&[T]`-composition axis — same "the validate / shape-
11803 // gate predicate must route through the substrate-primitive
11804 // typed dispatch" discipline extended onto the sibling outer
11805 // top-level [`Caixa`] `&[T]`-composition surface.
11806 let c = caixa_with_etiquetas(vec![""]);
11807 assert!(
11808 matches!(c.validate_etiquetas(), Err(ManifestError::EtiquetaEmpty)),
11809 "validate_etiquetas must reject etiquetas == vec![\"\"] \
11810 with EtiquetaEmpty — the accessor and the validate gate \
11811 must route through the same substrate-primitive typed \
11812 dispatch on the :etiquetas per-entry empty arm",
11813 );
11814 let c = caixa_with_etiquetas(vec!["demo"]);
11815 assert!(
11816 c.validate_etiquetas().is_ok(),
11817 "validate_etiquetas must accept etiquetas == vec![\"demo\"] \
11818 (the canonical single-tag shape every `feira init` \
11819 template scaffolds)",
11820 );
11821 }
11822
11823 #[test]
11824 fn etiquetas_projects_slice_by_borrow() {
11825 // The by-borrow pin: [`Caixa::etiquetas`] returns `&[String]`
11826 // by borrow — the returned slice borrows the underlying
11827 // `Vec<String>` storage of the `:etiquetas` slot and the
11828 // accessor must not clone the backing `Vec` on every call.
11829 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
11830 // (b5d813f) by-borrow pin on the sibling outer top-level
11831 // [`Caixa`] `&[String]`-return axis — the accessor's returned
11832 // slice must borrow from `&self` (the returned reference's
11833 // lifetime is tied to `&self`), and calling the accessor twice
11834 // on the same [`Caixa`] must yield slices that are pointer-
11835 // equal (the underlying byte-buffer is the storage `Vec`'s
11836 // allocation, not a fresh copy) as well as value-equal
11837 // (idempotent, no side effects on `&self`).
11838 //
11839 // Pins against a future silent detour that returned an owned
11840 // `Vec<String>` (which would type-check but silently clone on
11841 // every call, breaking the zero-cost projection every peer
11842 // sibling slice accessor carries), a `&Vec<String>` return
11843 // (which would leak the backing `Vec`'s grow/push/reserve
11844 // surface no downstream consumer reaches for), or a one-arm-
11845 // only accessor that returned a saturating value on some
11846 // sentinel input (breaking the pass-through invariant the
11847 // sibling slice accessors carry).
11848 for etiquetas in [
11849 vec![],
11850 vec!["demo"],
11851 vec!["example", "aplicacao", "mesh"],
11852 vec!["demo", "demo"],
11853 ] {
11854 let c = caixa_with_etiquetas(etiquetas.clone());
11855 let expected: Vec<String> = etiquetas.iter().map(|s| (*s).to_string()).collect();
11856 let first = c.etiquetas();
11857 let second = c.etiquetas();
11858 assert_eq!(
11859 first, second,
11860 "Caixa::etiquetas must be idempotent — two successive \
11861 calls on the same &self must return the same \
11862 &[String]",
11863 );
11864 assert_eq!(
11865 first.as_ptr(),
11866 second.as_ptr(),
11867 "Caixa::etiquetas must borrow the underlying \
11868 Vec<String> storage — two successive calls must \
11869 return slices with the same backing pointer (a fresh \
11870 Vec<String> clone would change the pointer on every \
11871 call)",
11872 );
11873 assert_eq!(
11874 first,
11875 expected.as_slice(),
11876 "Caixa::etiquetas must return :etiquetas verbatim by \
11877 borrow — got {first:?}, expected {expected:?}",
11878 );
11879 }
11880 }
11881
11882 // ── Caixa::bibliotecas — outer top-level &[T] slice accessor ──────
11883
11884 #[test]
11885 fn bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations() {
11886 // The canonical per-`Caixa` `:bibliotecas` universal-axis
11887 // library-source-path-list slice pin: [`Caixa::bibliotecas`]
11888 // must return the `:bibliotecas` typed [`Vec<String>`] list
11889 // verbatim as a `&[String]`, byte-equal to the raw
11890 // `self.bibliotecas.as_slice()` access across every
11891 // representative value in the accept-set — `[]` (the "no
11892 // libraries declared" arm every `:kind` other than `Biblioteca`
11893 // + every `Biblioteca` relying on the canonical
11894 // `lib/<nome>.lisp` implicit-default path carries; the
11895 // layout's [`crate::LayoutInvariants`] `MissingLib` arm-gate
11896 // fires exactly on this empty-slot + `Biblioteca`-kind
11897 // combination), `[""]` (a past-the-guard sentinel that pins
11898 // the accessor doesn't perform a silent `[""] → []` collapse
11899 // on the empty-entry arm — validate rejects `[""]` through
11900 // `CodePathEmpty { slot: ":bibliotecas" }` but the accessor
11901 // must ship the raw slot verbatim so a validate-time gate
11902 // regression surfaces at the `feira build` phase-1 parse
11903 // boundary rather than being silently absorbed into a
11904 // library-drop), `["lib/demo.lisp"]` (the canonical single-
11905 // entry form `Caixa::template` scaffolds and every `feira init`
11906 // template emits), `["lib/demo.lisp", "lib/helpers.lisp"]`
11907 // (the canonical multi-library form the
11908 // `validate_code_paths_accepts_explicit_relative_paths_on_
11909 // every_slot` fixture emits), and `["lib/foo.lisp",
11910 // "lib/foo.lisp"]` (a past-the-guard duplicate sentinel —
11911 // validate rejects through `CodePathDuplicate { slot:
11912 // ":bibliotecas" }` per the per-slot set-not-multiset gate,
11913 // but the accessor must ship the raw slot verbatim so the
11914 // `feira build` `for entry in caixa.bibliotecas()` parse walk
11915 // sees the duplicate at the accessor boundary and struct-
11916 // literal `Caixa { bibliotecas: vec!["lib/foo.lisp".into(),
11917 // "lib/foo.lisp".into()], .. }` fixtures continue to expose
11918 // the duplicate at the accessor).
11919 //
11920 // Third outer top-level [`Caixa`] `&[T]`-return slice accessor
11921 // pin on the substrate primitive — folds on the "outer
11922 // [`Caixa`] `&[T]` slice" projection pattern
11923 // `autores_returns_autores_slice_verbatim_across_permutations`
11924 // (b5d813f) opened and
11925 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
11926 // (78c7d3c) folded on, sibling in shape and idiom. Pins
11927 // against a future silent detour that returned an owned
11928 // `Vec<String>` (which would type-check but silently clone on
11929 // every accessor call, breaking the zero-cost projection
11930 // every peer sibling slice accessor carries), a `[""] → []`
11931 // collapse (which would silently absorb the `CodePathEmpty`
11932 // refusal case at the accessor boundary), or a `["lib/foo.lisp",
11933 // "lib/foo.lisp"] → ["lib/foo.lisp"]` dedup collapse (which
11934 // would silently absorb the `CodePathDuplicate` refusal case
11935 // at the accessor boundary — the per-slot set-not-multiset
11936 // gate is downstream of the accessor and must not be silently
11937 // promoted into it).
11938 for bibliotecas in [
11939 vec![],
11940 vec![""],
11941 vec!["lib/demo.lisp"],
11942 vec!["lib/demo.lisp", "lib/helpers.lisp"],
11943 vec!["lib/foo.lisp", "lib/foo.lisp"],
11944 ] {
11945 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
11946 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
11947 assert_eq!(
11948 c.bibliotecas(),
11949 expected.as_slice(),
11950 "Caixa::bibliotecas must return :bibliotecas verbatim \
11951 (got {:?}, expected {expected:?})",
11952 c.bibliotecas(),
11953 );
11954 assert_eq!(
11955 c.bibliotecas(),
11956 c.bibliotecas.as_slice(),
11957 "Caixa::bibliotecas must byte-equal the raw \
11958 `self.bibliotecas.as_slice()` field access across \
11959 every value in the Vec<String> accept-set",
11960 );
11961 }
11962 }
11963
11964 #[test]
11965 fn validate_code_paths_bibliotecas_empty_arm_routes_through_accessor() {
11966 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
11967 // empty-arm gate on the `:bibliotecas` slot must key off
11968 // [`Caixa::bibliotecas`], not a divergent raw
11969 // `&self.bibliotecas` field-borrow walk. Structurally: a
11970 // `Caixa { bibliotecas: vec!["".into()], .. }` must surface
11971 // the `CodePathEmpty { slot: ":bibliotecas" }` refusal
11972 // exactly, and a `Caixa { bibliotecas: vec!["lib/demo.lisp".
11973 // into()], .. }` (the canonical single-library form
11974 // `Caixa::template` scaffolds) must pass validate. The pair
11975 // jointly pins the accessor + validate-gate composition: any
11976 // future silent detour that had the accessor return an empty
11977 // slice on the `[""]` arm (a `.iter().filter(|s|
11978 // !s.is_empty()).collect()` collapse) would silently absorb
11979 // the `CodePathEmpty` refusal at the accessor boundary and
11980 // the validate gate would accept a struct-literal
11981 // `Caixa { bibliotecas: vec!["".into()], .. }` — the
11982 // composition pin catches that at caixa-core build time.
11983 //
11984 // Peer of the per-`Caixa` `validate_autores_empty_arm_routes_
11985 // through_accessor` (b5d813f) and
11986 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
11987 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
11988 // composition axes — same "the validate / shape-gate
11989 // predicate must route through the substrate-primitive typed
11990 // dispatch" discipline extended onto the sibling outer top-
11991 // level [`Caixa`] `&[T]`-composition surface. Nominally the
11992 // in-tree `validate_code_paths` production body still keys
11993 // off the internal `[(":bibliotecas", &self.bibliotecas,
11994 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
11995 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
11996 // (the tuple's homogeneous slice-typed shape blocks a per-
11997 // element accessor swap in isolation — a future companion
11998 // lift for `:exe` and `:servicos` on the same outer-`Caixa`
11999 // `&[T]` slice-accessor axis closes that tuple onto the
12000 // triple of typed dispatches as a unit); the composition pin
12001 // catches any future accessor-side silent filter drop against
12002 // that eventual tuple-closure regardless of whether the
12003 // `:bibliotecas` slot is threaded through the accessor or the
12004 // raw field access at the tuple's construction site.
12005 let c = caixa_with_code_paths(vec![""], vec![], vec![]);
12006 assert!(
12007 matches!(
12008 c.validate_code_paths(),
12009 Err(ManifestError::CodePathEmpty {
12010 slot: ":bibliotecas"
12011 })
12012 ),
12013 "validate_code_paths must reject bibliotecas == vec![\"\"] \
12014 with CodePathEmpty {{ slot: \":bibliotecas\" }} — the \
12015 accessor and the validate gate must route through the \
12016 same substrate-primitive typed dispatch on the \
12017 :bibliotecas per-entry empty arm",
12018 );
12019 let c = caixa_with_code_paths(vec!["lib/demo.lisp"], vec![], vec![]);
12020 assert!(
12021 c.validate_code_paths().is_ok(),
12022 "validate_code_paths must accept bibliotecas == \
12023 vec![\"lib/demo.lisp\"] (the canonical single-library \
12024 shape every `feira init` template scaffolds)",
12025 );
12026 }
12027
12028 #[test]
12029 fn bibliotecas_projects_slice_by_borrow() {
12030 // The by-borrow pin: [`Caixa::bibliotecas`] returns
12031 // `&[String]` by borrow — the returned slice borrows the
12032 // underlying `Vec<String>` storage of the `:bibliotecas` slot
12033 // and the accessor must not clone the backing `Vec` on every
12034 // call. Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12035 // (b5d813f) and `etiquetas_projects_slice_by_borrow` (78c7d3c)
12036 // by-borrow pins on the sibling outer top-level [`Caixa`]
12037 // `&[String]`-return axes — the accessor's returned slice
12038 // must borrow from `&self` (the returned reference's lifetime
12039 // is tied to `&self`), and calling the accessor twice on the
12040 // same [`Caixa`] must yield slices that are pointer-equal
12041 // (the underlying byte-buffer is the storage `Vec`'s
12042 // allocation, not a fresh copy) as well as value-equal
12043 // (idempotent, no side effects on `&self`).
12044 //
12045 // Pins against a future silent detour that returned an owned
12046 // `Vec<String>` (which would type-check but silently clone on
12047 // every call, breaking the zero-cost projection every peer
12048 // sibling slice accessor carries), a `&Vec<String>` return
12049 // (which would leak the backing `Vec`'s grow/push/reserve
12050 // surface no downstream consumer reaches for), or a one-arm-
12051 // only accessor that returned a saturating value on some
12052 // sentinel input (breaking the pass-through invariant the
12053 // sibling slice accessors carry).
12054 for bibliotecas in [
12055 vec![],
12056 vec!["lib/demo.lisp"],
12057 vec!["lib/demo.lisp", "lib/helpers.lisp"],
12058 vec!["lib/foo.lisp", "lib/foo.lisp"],
12059 ] {
12060 let c = caixa_with_code_paths(bibliotecas.clone(), vec![], vec![]);
12061 let expected: Vec<String> = bibliotecas.iter().map(|s| (*s).to_string()).collect();
12062 let first = c.bibliotecas();
12063 let second = c.bibliotecas();
12064 assert_eq!(
12065 first, second,
12066 "Caixa::bibliotecas must be idempotent — two \
12067 successive calls on the same &self must return the \
12068 same &[String]",
12069 );
12070 assert_eq!(
12071 first.as_ptr(),
12072 second.as_ptr(),
12073 "Caixa::bibliotecas must borrow the underlying \
12074 Vec<String> storage — two successive calls must \
12075 return slices with the same backing pointer (a \
12076 fresh Vec<String> clone would change the pointer on \
12077 every call)",
12078 );
12079 assert_eq!(
12080 first,
12081 expected.as_slice(),
12082 "Caixa::bibliotecas must return :bibliotecas verbatim \
12083 by borrow — got {first:?}, expected {expected:?}",
12084 );
12085 }
12086 }
12087
12088 // ── Caixa::exe — outer top-level &[T] slice accessor ──────────────
12089
12090 #[test]
12091 fn exe_returns_exe_slice_verbatim_across_permutations() {
12092 // The canonical per-`Caixa` `:exe` universal-axis
12093 // nix-built-executable-entry-path-list slice pin: [`Caixa::exe`]
12094 // must return the `:exe` typed [`Vec<String>`] list verbatim as
12095 // a `&[String]`, byte-equal to the raw `self.exe.as_slice()`
12096 // access across every representative value in the accept-set —
12097 // `[]` (the "no executable declared" arm every `:kind` other
12098 // than `Binario` carries; the layout's [`crate::LayoutInvariants`]
12099 // `BinarioWithoutExe` arm-gate fires exactly on this empty-slot
12100 // + `Binario`-kind combination), `[""]` (a past-the-guard
12101 // sentinel that pins the accessor doesn't perform a silent
12102 // `[""] → []` collapse on the empty-entry arm — validate rejects
12103 // `[""]` through `CodePathEmpty { slot: ":exe" }` but the
12104 // accessor must ship the raw slot verbatim so a validate-time
12105 // gate regression surfaces at the layout / `feira nix` boundary
12106 // rather than being silently absorbed into an executable-drop),
12107 // `["exe/cli"]` (the canonical single-entry Binario form every
12108 // in-tree `caixa_with_code_paths` positive control uses),
12109 // `["exe/cli", "exe/serve"]` (the canonical multi-executable
12110 // form the `validate_code_paths_accepts_explicit_relative_paths_
12111 // on_every_slot` fixture emits), and `["exe/cli", "exe/cli"]`
12112 // (a past-the-guard duplicate sentinel — validate rejects
12113 // through `CodePathDuplicate { slot: ":exe" }` per the per-slot
12114 // set-not-multiset gate, but the accessor must ship the raw
12115 // slot verbatim so struct-literal `Caixa { exe: vec!["exe/cli".
12116 // into(), "exe/cli".into()], .. }` fixtures continue to expose
12117 // the duplicate at the accessor).
12118 //
12119 // Fourth outer top-level [`Caixa`] `&[T]`-return slice accessor
12120 // pin on the substrate primitive — folds on the "outer
12121 // [`Caixa`] `&[T]` slice" projection pattern
12122 // `autores_returns_autores_slice_verbatim_across_permutations`
12123 // (b5d813f) opened,
12124 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12125 // (78c7d3c) folded on, and
12126 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12127 // (8a36c23) closed the universal-axis text-tag family of.
12128 // Opens the outer-`Caixa` foreign-code-slot `&[T]` sub-family
12129 // the sibling `:servicos` future lift closes onto. Pins against
12130 // a future silent detour that returned an owned `Vec<String>`
12131 // (which would type-check but silently clone on every accessor
12132 // call, breaking the zero-cost projection every peer sibling
12133 // slice accessor carries), a `[""] → []` collapse (which would
12134 // silently absorb the `CodePathEmpty` refusal case at the
12135 // accessor boundary), or an `["exe/cli", "exe/cli"] →
12136 // ["exe/cli"]` dedup collapse (which would silently absorb the
12137 // `CodePathDuplicate` refusal case at the accessor boundary —
12138 // the per-slot set-not-multiset gate is downstream of the
12139 // accessor and must not be silently promoted into it).
12140 for exe in [
12141 vec![],
12142 vec![""],
12143 vec!["exe/cli"],
12144 vec!["exe/cli", "exe/serve"],
12145 vec!["exe/cli", "exe/cli"],
12146 ] {
12147 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12148 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12149 assert_eq!(
12150 c.exe(),
12151 expected.as_slice(),
12152 "Caixa::exe must return :exe verbatim (got {:?}, \
12153 expected {expected:?})",
12154 c.exe(),
12155 );
12156 assert_eq!(
12157 c.exe(),
12158 c.exe.as_slice(),
12159 "Caixa::exe must byte-equal the raw \
12160 `self.exe.as_slice()` field access across every value \
12161 in the Vec<String> accept-set",
12162 );
12163 }
12164 }
12165
12166 #[test]
12167 fn validate_code_paths_exe_empty_arm_routes_through_accessor() {
12168 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12169 // empty-arm gate on the `:exe` slot must key off
12170 // [`Caixa::exe`], not a divergent raw `&self.exe` field-borrow
12171 // walk. Structurally: a `Caixa { exe: vec!["".into()], .. }`
12172 // must surface the `CodePathEmpty { slot: ":exe" }` refusal
12173 // exactly, and a `Caixa { exe: vec!["exe/cli".into()], .. }`
12174 // (the canonical single-executable form every in-tree
12175 // `caixa_with_code_paths` positive control uses) must pass
12176 // validate. The pair jointly pins the accessor + validate-gate
12177 // composition: any future silent detour that had the accessor
12178 // return an empty slice on the `[""]` arm (a
12179 // `.iter().filter(|s| !s.is_empty()).collect()` collapse) would
12180 // silently absorb the `CodePathEmpty` refusal at the accessor
12181 // boundary and the validate gate would accept a struct-literal
12182 // `Caixa { exe: vec!["".into()], .. }` — the composition pin
12183 // catches that at caixa-core build time.
12184 //
12185 // Peer of the per-`Caixa`
12186 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12187 // (8a36c23), `validate_autores_empty_arm_routes_through_accessor`
12188 // (b5d813f), and
12189 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12190 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12191 // composition axes — same "the validate / shape-gate predicate
12192 // must route through the substrate-primitive typed dispatch"
12193 // discipline extended onto the sibling outer top-level [`Caixa`]
12194 // `&[T]`-composition surface. Nominally the in-tree
12195 // `validate_code_paths` production body still keys off the
12196 // internal `[(":bibliotecas", &self.bibliotecas,
12197 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12198 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12199 // (the tuple's homogeneous slice-typed shape blocks a per-
12200 // element accessor swap in isolation — a future companion lift
12201 // for `:servicos` on the same outer-`Caixa` `&[T]` slice-
12202 // accessor axis closes that tuple onto the triple of typed
12203 // dispatches as a unit); the composition pin catches any future
12204 // accessor-side silent filter drop against that eventual tuple-
12205 // closure regardless of whether the `:exe` slot is threaded
12206 // through the accessor or the raw field access at the tuple's
12207 // construction site.
12208 let c = caixa_with_code_paths(vec![], vec![""], vec![]);
12209 assert!(
12210 matches!(
12211 c.validate_code_paths(),
12212 Err(ManifestError::CodePathEmpty { slot: ":exe" })
12213 ),
12214 "validate_code_paths must reject exe == vec![\"\"] \
12215 with CodePathEmpty {{ slot: \":exe\" }} — the \
12216 accessor and the validate gate must route through the \
12217 same substrate-primitive typed dispatch on the \
12218 :exe per-entry empty arm",
12219 );
12220 let c = caixa_with_code_paths(vec![], vec!["exe/cli"], vec![]);
12221 assert!(
12222 c.validate_code_paths().is_ok(),
12223 "validate_code_paths must accept exe == vec![\"exe/cli\"] \
12224 (the canonical single-executable shape every in-tree \
12225 `caixa_with_code_paths` positive control uses)",
12226 );
12227 }
12228
12229 #[test]
12230 fn exe_projects_slice_by_borrow() {
12231 // The by-borrow pin: [`Caixa::exe`] returns `&[String]` by
12232 // borrow — the returned slice borrows the underlying
12233 // `Vec<String>` storage of the `:exe` slot and the accessor
12234 // must not clone the backing `Vec` on every call. Peer of the
12235 // per-`Caixa` `autores_projects_slice_by_borrow` (b5d813f),
12236 // `etiquetas_projects_slice_by_borrow` (78c7d3c), and
12237 // `bibliotecas_projects_slice_by_borrow` (8a36c23) by-borrow
12238 // pins on the sibling outer top-level [`Caixa`] `&[String]`-
12239 // return axes — the accessor's returned slice must borrow from
12240 // `&self` (the returned reference's lifetime is tied to
12241 // `&self`), and calling the accessor twice on the same
12242 // [`Caixa`] must yield slices that are pointer-equal (the
12243 // underlying byte-buffer is the storage `Vec`'s allocation,
12244 // not a fresh copy) as well as value-equal (idempotent, no
12245 // side effects on `&self`).
12246 //
12247 // Pins against a future silent detour that returned an owned
12248 // `Vec<String>` (which would type-check but silently clone on
12249 // every call, breaking the zero-cost projection every peer
12250 // sibling slice accessor carries), a `&Vec<String>` return
12251 // (which would leak the backing `Vec`'s grow/push/reserve
12252 // surface no downstream consumer reaches for), or a one-arm-
12253 // only accessor that returned a saturating value on some
12254 // sentinel input (breaking the pass-through invariant the
12255 // sibling slice accessors carry).
12256 for exe in [
12257 vec![],
12258 vec!["exe/cli"],
12259 vec!["exe/cli", "exe/serve"],
12260 vec!["exe/cli", "exe/cli"],
12261 ] {
12262 let c = caixa_with_code_paths(vec![], exe.clone(), vec![]);
12263 let expected: Vec<String> = exe.iter().map(|s| (*s).to_string()).collect();
12264 let first = c.exe();
12265 let second = c.exe();
12266 assert_eq!(
12267 first, second,
12268 "Caixa::exe must be idempotent — two successive calls \
12269 on the same &self must return the same &[String]",
12270 );
12271 assert_eq!(
12272 first.as_ptr(),
12273 second.as_ptr(),
12274 "Caixa::exe must borrow the underlying Vec<String> \
12275 storage — two successive calls must return slices \
12276 with the same backing pointer (a fresh Vec<String> \
12277 clone would change the pointer on every call)",
12278 );
12279 assert_eq!(
12280 first,
12281 expected.as_slice(),
12282 "Caixa::exe must return :exe verbatim by borrow — \
12283 got {first:?}, expected {expected:?}",
12284 );
12285 }
12286 }
12287
12288 // ── Caixa::servicos — outer top-level &[T] slice accessor ─────────
12289
12290 #[test]
12291 fn servicos_returns_servicos_slice_verbatim_across_permutations() {
12292 // The canonical per-`Caixa` `:servicos` universal-axis
12293 // ComputeUnit-CR-YAML-entry-path-list slice pin:
12294 // [`Caixa::servicos`] must return the `:servicos` typed
12295 // [`Vec<String>`] list verbatim as a `&[String]`, byte-equal to
12296 // the raw `self.servicos.as_slice()` access across every
12297 // representative value in the accept-set — `[]` (the "no
12298 // ComputeUnit-CR declared" arm every `:kind` other than
12299 // `Servico` carries; the layout's [`crate::LayoutInvariants`]
12300 // `ServicoWithoutServicos` arm-gate fires exactly on this
12301 // empty-slot + `Servico`-kind combination), `[""]` (a past-the-
12302 // guard sentinel that pins the accessor doesn't perform a
12303 // silent `[""] → []` collapse on the empty-entry arm — validate
12304 // rejects `[""]` through `CodePathEmpty { slot: ":servicos" }`
12305 // but the accessor must ship the raw slot verbatim so a
12306 // validate-time gate regression surfaces at the layout /
12307 // per-Servico renderer boundary rather than being silently
12308 // absorbed into a component-drop),
12309 // `["servicos/demo.computeunit.yaml"]` (the canonical
12310 // singleton V0-shape every in-tree `caixa_with_code_paths`
12311 // positive control uses; the same shape
12312 // [`crate::require_single_servico`] admits),
12313 // `["servicos/a.computeunit.yaml", "servicos/b.computeunit.
12314 // yaml"]` (a past-the-guard `len != 1` sentinel — the V0
12315 // singularity gate rejects through `ServicoCountMismatch
12316 // { count: 2 }` but the accessor must ship the raw slot
12317 // verbatim so struct-literal `Caixa { servicos: vec![...,
12318 // ...], .. }` fixtures continue to expose the count at the
12319 // accessor), and `["servicos/a.computeunit.yaml",
12320 // "servicos/a.computeunit.yaml"]` (a past-the-guard duplicate
12321 // sentinel — validate rejects through
12322 // `CodePathDuplicate { slot: ":servicos" }` per the per-slot
12323 // set-not-multiset gate, but the accessor must ship the raw
12324 // slot verbatim so struct-literal fixtures continue to expose
12325 // the duplicate at the accessor).
12326 //
12327 // Fifth and final outer top-level [`Caixa`] `&[T]`-return
12328 // slice accessor pin on the substrate primitive — folds on the
12329 // "outer [`Caixa`] `&[T]` slice" projection pattern
12330 // `autores_returns_autores_slice_verbatim_across_permutations`
12331 // (b5d813f) opened,
12332 // `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12333 // (78c7d3c) folded on,
12334 // `bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12335 // (8a36c23) closed the universal-axis text-tag family of, and
12336 // `exe_returns_exe_slice_verbatim_across_permutations`
12337 // (65d9527) opened the foreign-code-slot sub-family of. Closes
12338 // the outer-`Caixa` foreign-code-slot `&[T]` sub-family — the
12339 // trio of code-surface list slots (`:bibliotecas` + `:exe` +
12340 // `:servicos`) now each carries a substrate-canonical slice
12341 // accessor. Pins against a future silent detour that returned
12342 // an owned `Vec<String>` (which would type-check but silently
12343 // clone on every accessor call, breaking the zero-cost
12344 // projection every peer sibling slice accessor carries), a
12345 // `[""] → []` collapse (which would silently absorb the
12346 // `CodePathEmpty` refusal case at the accessor boundary), an
12347 // `[a, a] → [a]` dedup collapse (which would silently absorb
12348 // the `CodePathDuplicate` refusal case at the accessor
12349 // boundary — the per-slot set-not-multiset gate is downstream
12350 // of the accessor and must not be silently promoted into it),
12351 // or a `[a, b] → [a]` singleton collapse (which would silently
12352 // absorb the V0 `ServicoCountMismatch` refusal case at the
12353 // accessor boundary — the V0 singularity gate is downstream of
12354 // the accessor and must not be silently promoted into it).
12355 for servicos in [
12356 vec![],
12357 vec![""],
12358 vec!["servicos/demo.computeunit.yaml"],
12359 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12360 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12361 ] {
12362 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12363 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12364 assert_eq!(
12365 c.servicos(),
12366 expected.as_slice(),
12367 "Caixa::servicos must return :servicos verbatim (got \
12368 {:?}, expected {expected:?})",
12369 c.servicos(),
12370 );
12371 assert_eq!(
12372 c.servicos(),
12373 c.servicos.as_slice(),
12374 "Caixa::servicos must byte-equal the raw \
12375 `self.servicos.as_slice()` field access across every \
12376 value in the Vec<String> accept-set",
12377 );
12378 }
12379 }
12380
12381 #[test]
12382 fn validate_code_paths_servicos_empty_arm_routes_through_accessor() {
12383 // Composition pin: [`Caixa::validate_code_paths`]'s per-entry
12384 // empty-arm gate on the `:servicos` slot must key off
12385 // [`Caixa::servicos`], not a divergent raw `&self.servicos`
12386 // field-borrow walk. Structurally: a `Caixa { servicos:
12387 // vec!["".into()], .. }` must surface the `CodePathEmpty
12388 // { slot: ":servicos" }` refusal exactly, and a `Caixa
12389 // { servicos: vec!["servicos/demo.computeunit.yaml".into()],
12390 // .. }` (the canonical singleton V0-shape every in-tree
12391 // `caixa_with_code_paths` positive control uses) must pass
12392 // validate. The pair jointly pins the accessor + validate-gate
12393 // composition: any future silent detour that had the accessor
12394 // return an empty slice on the `[""]` arm (a `.iter().filter
12395 // (|s| !s.is_empty()).collect()` collapse) would silently
12396 // absorb the `CodePathEmpty` refusal at the accessor boundary
12397 // and the validate gate would accept a struct-literal
12398 // `Caixa { servicos: vec!["".into()], .. }` — the composition
12399 // pin catches that at caixa-core build time.
12400 //
12401 // Peer of the per-`Caixa`
12402 // `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12403 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12404 // (65d9527), `validate_autores_empty_arm_routes_through_accessor`
12405 // (b5d813f), and
12406 // `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12407 // (78c7d3c) accessor-composition pins on the sibling `&[T]`-
12408 // composition axes — same "the validate / shape-gate predicate
12409 // must route through the substrate-primitive typed dispatch"
12410 // discipline extended onto the sibling outer top-level
12411 // [`Caixa`] `&[T]`-composition surface, closing the trio of
12412 // code-surface accessor-composition pins on the same axis.
12413 // Nominally the in-tree `validate_code_paths` production body
12414 // still keys off the internal
12415 // `[(":bibliotecas", &self.bibliotecas,
12416 // CodePathFileType::LispSource), (":exe", &self.exe, ..),
12417 // (":servicos", &self.servicos, ..)]` per-slot dispatch tuple
12418 // (the tuple's homogeneous `&Vec<String>`-typed shape blocks a
12419 // per-element accessor swap in isolation — a future companion
12420 // lift promotes the tuple's element type to `&[String]` and
12421 // threads the triple of typed dispatches through as a unit);
12422 // the composition pin catches any future accessor-side silent
12423 // filter drop against that eventual tuple-closure regardless
12424 // of whether the `:servicos` slot is threaded through the
12425 // accessor or the raw field access at the tuple's construction
12426 // site.
12427 let c = caixa_with_code_paths(vec![], vec![], vec![""]);
12428 assert!(
12429 matches!(
12430 c.validate_code_paths(),
12431 Err(ManifestError::CodePathEmpty { slot: ":servicos" })
12432 ),
12433 "validate_code_paths must reject servicos == vec![\"\"] \
12434 with CodePathEmpty {{ slot: \":servicos\" }} — the \
12435 accessor and the validate gate must route through the \
12436 same substrate-primitive typed dispatch on the \
12437 :servicos per-entry empty arm",
12438 );
12439 let c = caixa_with_code_paths(vec![], vec![], vec!["servicos/demo.computeunit.yaml"]);
12440 assert!(
12441 c.validate_code_paths().is_ok(),
12442 "validate_code_paths must accept servicos == \
12443 vec![\"servicos/demo.computeunit.yaml\"] (the canonical \
12444 singleton V0-shape every in-tree `caixa_with_code_paths` \
12445 positive control uses)",
12446 );
12447 }
12448
12449 #[test]
12450 fn servicos_projects_slice_by_borrow() {
12451 // The by-borrow pin: [`Caixa::servicos`] returns `&[String]` by
12452 // borrow — the returned slice borrows the underlying
12453 // `Vec<String>` storage of the `:servicos` slot and the
12454 // accessor must not clone the backing `Vec` on every call.
12455 // Peer of the per-`Caixa` `autores_projects_slice_by_borrow`
12456 // (b5d813f), `etiquetas_projects_slice_by_borrow` (78c7d3c),
12457 // `bibliotecas_projects_slice_by_borrow` (8a36c23), and
12458 // `exe_projects_slice_by_borrow` (65d9527) by-borrow pins on
12459 // the sibling outer top-level [`Caixa`] `&[String]`-return
12460 // axes — the accessor's returned slice must borrow from
12461 // `&self` (the returned reference's lifetime is tied to
12462 // `&self`), and calling the accessor twice on the same
12463 // [`Caixa`] must yield slices that are pointer-equal (the
12464 // underlying byte-buffer is the storage `Vec`'s allocation,
12465 // not a fresh copy) as well as value-equal (idempotent, no
12466 // side effects on `&self`).
12467 //
12468 // Pins against a future silent detour that returned an owned
12469 // `Vec<String>` (which would type-check but silently clone on
12470 // every call, breaking the zero-cost projection every peer
12471 // sibling slice accessor carries), a `&Vec<String>` return
12472 // (which would leak the backing `Vec`'s grow/push/reserve
12473 // surface no downstream consumer reaches for), or a one-arm-
12474 // only accessor that returned a saturating value on some
12475 // sentinel input (breaking the pass-through invariant the
12476 // sibling slice accessors carry).
12477 for servicos in [
12478 vec![],
12479 vec!["servicos/demo.computeunit.yaml"],
12480 vec!["servicos/a.computeunit.yaml", "servicos/b.computeunit.yaml"],
12481 vec!["servicos/a.computeunit.yaml", "servicos/a.computeunit.yaml"],
12482 ] {
12483 let c = caixa_with_code_paths(vec![], vec![], servicos.clone());
12484 let expected: Vec<String> = servicos.iter().map(|s| (*s).to_string()).collect();
12485 let first = c.servicos();
12486 let second = c.servicos();
12487 assert_eq!(
12488 first, second,
12489 "Caixa::servicos must be idempotent — two successive \
12490 calls on the same &self must return the same &[String]",
12491 );
12492 assert_eq!(
12493 first.as_ptr(),
12494 second.as_ptr(),
12495 "Caixa::servicos must borrow the underlying \
12496 Vec<String> storage — two successive calls must \
12497 return slices with the same backing pointer (a fresh \
12498 Vec<String> clone would change the pointer on every \
12499 call)",
12500 );
12501 assert_eq!(
12502 first,
12503 expected.as_slice(),
12504 "Caixa::servicos must return :servicos verbatim by \
12505 borrow — got {first:?}, expected {expected:?}",
12506 );
12507 }
12508 }
12509
12510 // ── Caixa::deps — outer top-level &[Dep] slice accessor ───────────
12511
12512 fn caixa_with_deps(deps: Vec<Dep>) -> Caixa {
12513 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12514 c.deps = deps;
12515 c
12516 }
12517
12518 #[test]
12519 fn deps_returns_deps_slice_verbatim_across_permutations() {
12520 // The canonical per-`Caixa` `:deps` universal-axis runtime-
12521 // dependency-declaration-list slice pin: [`Caixa::deps`] must
12522 // return the `:deps` typed [`Vec<Dep>`] list verbatim as a
12523 // `&[Dep]`, element-equal to the raw `self.deps.as_slice()`
12524 // access across every representative value in the accept-set —
12525 // `[]` (the "no runtime deps declared" arm every existing
12526 // fixture without a `:deps` line carries; the
12527 // [`Caixa::template`] scaffold emits `:deps ()`), a canonical
12528 // single-entry list (the shape most consumer caixas carry), a
12529 // canonical two-entry list (the multi-dep runtime closure), and
12530 // two past-the-guard sentinels — a `[""]`-`:nome` entry
12531 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12532 // `NomeInvalid` but the accessor must ship the raw slot
12533 // verbatim) and a `[a, a]` duplicate (validate rejects through
12534 // `DuplicateNome { list: ":deps" }` but the accessor must ship
12535 // the raw slot verbatim so struct-literal fixtures continue to
12536 // expose the duplicate at the accessor).
12537 //
12538 // First outer top-level [`Caixa`] `&[Dep]`-return slice accessor
12539 // pin on the substrate primitive — opens the outer-`Caixa`
12540 // dependency-slot `&[Dep]` sub-family the sibling `:deps-dev`
12541 // future lift closes on. Peer of the closed outer-`Caixa`
12542 // foreign-code-slot `&[String]` sub-family
12543 // (`bibliotecas_returns_bibliotecas_slice_verbatim_across_permutations`
12544 // 8a36c23, `exe_returns_exe_slice_verbatim_across_permutations`
12545 // 65d9527, `servicos_returns_servicos_slice_verbatim_across_permutations`
12546 // 611f78b) and the outer-`Caixa` universal-axis text-tag family
12547 // (`autores_returns_autores_slice_verbatim_across_permutations`
12548 // b5d813f, `etiquetas_returns_etiquetas_slice_verbatim_across_permutations`
12549 // 78c7d3c) — extends the "outer [`Caixa`] `&[T]` slice"
12550 // projection pattern onto a novel element-type axis (`Dep`
12551 // composite vs the prior sibling family's `String` scalar).
12552 // Pins against a future silent detour that returned an owned
12553 // `Vec<Dep>` (which would type-check but silently clone on every
12554 // accessor call, breaking the zero-cost projection every peer
12555 // sibling slice accessor carries), a `[""] → []` collapse (which
12556 // would silently absorb the `NomeEmpty` refusal case at the
12557 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12558 // would silently absorb the `DuplicateNome` refusal case at the
12559 // accessor boundary).
12560 for deps in [
12561 vec![],
12562 vec![Dep::simple("", "^0.1")],
12563 vec![Dep::simple("caixa-teia", "^0.1")],
12564 vec![
12565 Dep::simple("caixa-teia", "^0.1"),
12566 Dep::simple("caixa-core", "^0.1"),
12567 ],
12568 vec![
12569 Dep::simple("caixa-teia", "^0.1"),
12570 Dep::simple("caixa-teia", "^0.2"),
12571 ],
12572 ] {
12573 let c = caixa_with_deps(deps.clone());
12574 assert_eq!(
12575 c.deps(),
12576 deps.as_slice(),
12577 "Caixa::deps must return :deps verbatim (got {:?}, \
12578 expected {deps:?})",
12579 c.deps(),
12580 );
12581 assert_eq!(
12582 c.deps(),
12583 c.deps.as_slice(),
12584 "Caixa::deps must element-equal the raw \
12585 `self.deps.as_slice()` field access across every \
12586 value in the Vec<Dep> accept-set",
12587 );
12588 }
12589 }
12590
12591 #[test]
12592 fn validate_deps_duplicate_arm_routes_through_accessor() {
12593 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps`
12594 // duplicate-`:nome` gate must key off [`Caixa::deps`], not the
12595 // raw `&self.deps` field-borrow walk. Structurally: a `Caixa
12596 // { deps: vec![Dep::simple("d", "^0.1"), Dep::simple("d",
12597 // "^0.2")], .. }` must surface the `DuplicateNome { list:
12598 // ":deps" }` refusal exactly, and a `Caixa { deps: vec![
12599 // Dep::simple("d", "^0.1")], .. }` (the canonical single-entry
12600 // form) must pass validate. The pair jointly pins the accessor +
12601 // validate-gate composition: any future silent detour that had
12602 // the accessor return a dedupped slice on the `[a, a]` arm (a
12603 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12604 // would silently absorb the `DuplicateNome` refusal at the
12605 // accessor boundary and the validate gate would accept a
12606 // struct-literal `Caixa` carrying the drift — the composition
12607 // pin catches that at caixa-core build time.
12608 //
12609 // Peer of the per-`Caixa`
12610 // `validate_autores_empty_entry_arm_routes_through_accessor`
12611 // (b5d813f), `validate_etiquetas_empty_entry_arm_routes_through_accessor`
12612 // (78c7d3c), `validate_code_paths_bibliotecas_empty_arm_routes_through_accessor`
12613 // (8a36c23), `validate_code_paths_exe_empty_arm_routes_through_accessor`
12614 // (65d9527), and `validate_code_paths_servicos_empty_arm_routes_through_accessor`
12615 // (611f78b) accessor-composition pins on the sibling `&[T]`-
12616 // composition axes — same "the validate gate must route through
12617 // the substrate-primitive typed dispatch" discipline extended
12618 // onto the sibling outer top-level [`Caixa`] `&[Dep]`-
12619 // composition surface, opening the outer-`Caixa` dependency-slot
12620 // arm of the composition-pin family.
12621 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12622 let err = c.validate_deps().unwrap_err();
12623 assert!(
12624 matches!(
12625 err,
12626 DepError::DuplicateNome { ref nome, list } if nome == "d"
12627 && list == crate::render::DEP_AUTHOR_KEY_DEPS
12628 ),
12629 "validate_deps must reject deps == \
12630 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12631 DuplicateNome {{ nome: \"d\", list: \":deps\" }} — the \
12632 accessor and the validate gate must route through the \
12633 same substrate-primitive typed dispatch on the :deps \
12634 within-list duplicate arm (got {err:?})",
12635 );
12636 let c = caixa_with_deps(vec![Dep::simple("d", "^0.1")]);
12637 assert!(
12638 c.validate_deps().is_ok(),
12639 "validate_deps must accept deps == vec![Dep(\"d\",\"^0.1\")] \
12640 (the canonical single-entry form)",
12641 );
12642 }
12643
12644 #[test]
12645 fn deps_projects_slice_by_borrow() {
12646 // The by-borrow pin: [`Caixa::deps`] returns `&[Dep]` by borrow
12647 // — the returned slice borrows the underlying `Vec<Dep>` storage
12648 // of the `:deps` slot and the accessor must not clone the
12649 // backing `Vec` on every call. Peer of the per-`Caixa`
12650 // `autores_projects_slice_by_borrow` (b5d813f),
12651 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12652 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12653 // `exe_projects_slice_by_borrow` (65d9527), and
12654 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12655 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12656 // axes — the accessor's returned slice must borrow from `&self`
12657 // (the returned reference's lifetime is tied to `&self`), and
12658 // calling the accessor twice on the same [`Caixa`] must yield
12659 // slices that are pointer-equal (the underlying byte-buffer is
12660 // the storage `Vec`'s allocation, not a fresh copy) as well as
12661 // value-equal (idempotent, no side effects on `&self`).
12662 //
12663 // Pins against a future silent detour that returned an owned
12664 // `Vec<Dep>` (which would type-check but silently clone on
12665 // every call), a `&Vec<Dep>` return (which would leak the
12666 // backing `Vec`'s grow/push/reserve surface no downstream
12667 // consumer reaches for), or a one-arm-only accessor that
12668 // returned a saturating value on some sentinel input.
12669 for deps in [
12670 vec![],
12671 vec![Dep::simple("caixa-teia", "^0.1")],
12672 vec![
12673 Dep::simple("caixa-teia", "^0.1"),
12674 Dep::simple("caixa-core", "^0.1"),
12675 ],
12676 ] {
12677 let c = caixa_with_deps(deps.clone());
12678 let first = c.deps();
12679 let second = c.deps();
12680 assert_eq!(
12681 first, second,
12682 "Caixa::deps must be idempotent — two successive calls \
12683 on the same &self must return the same &[Dep]",
12684 );
12685 assert_eq!(
12686 first.as_ptr(),
12687 second.as_ptr(),
12688 "Caixa::deps must borrow the underlying Vec<Dep> \
12689 storage — two successive calls must return slices \
12690 with the same backing pointer (a fresh Vec<Dep> clone \
12691 would change the pointer on every call)",
12692 );
12693 assert_eq!(
12694 first,
12695 deps.as_slice(),
12696 "Caixa::deps must return :deps verbatim by borrow — \
12697 got {first:?}, expected {deps:?}",
12698 );
12699 }
12700 }
12701
12702 // ── Caixa::deps_dev — outer top-level &[Dep] slice accessor ──────
12703
12704 fn caixa_with_deps_dev(deps_dev: Vec<Dep>) -> Caixa {
12705 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12706 c.deps_dev = deps_dev;
12707 c
12708 }
12709
12710 #[test]
12711 fn deps_dev_returns_deps_dev_slice_verbatim_across_permutations() {
12712 // The canonical per-`Caixa` `:deps-dev` universal-axis dev-only-
12713 // dependency-declaration-list slice pin: [`Caixa::deps_dev`]
12714 // must return the `:deps-dev` typed [`Vec<Dep>`] list verbatim as
12715 // a `&[Dep]`, element-equal to the raw `self.deps_dev.as_slice()`
12716 // access across every representative value in the accept-set —
12717 // `[]` (the "no dev deps declared" arm every existing fixture
12718 // without a `:deps-dev` line carries; the [`Caixa::template`]
12719 // scaffold emits `:deps-dev ()`), a canonical single-entry list
12720 // (the shape most consumer caixas carry — a `tatara-check` dev
12721 // pin), a canonical two-entry list (the multi-dev-dep closure),
12722 // and two past-the-guard sentinels — a `[""]`-`:nome` entry
12723 // ([`Self::validate_deps`] rejects through `NomeEmpty` /
12724 // `NomeInvalid` but the accessor must ship the raw slot
12725 // verbatim) and a `[a, a]` duplicate (validate rejects through
12726 // `DuplicateNome { list: ":deps-dev" }` but the accessor must
12727 // ship the raw slot verbatim so struct-literal fixtures continue
12728 // to expose the duplicate at the accessor).
12729 //
12730 // Second outer top-level [`Caixa`] `&[Dep]`-return slice-accessor
12731 // pin on the substrate primitive — closes the outer-`Caixa`
12732 // dependency-slot `&[Dep]` sub-family the sibling
12733 // `deps_returns_deps_slice_verbatim_across_permutations`
12734 // (ad34b4e) opened on. Folds the "outer [`Caixa`] `&[Dep]`
12735 // slice" projection pattern onto the sibling dev-dep axis —
12736 // pins against a future silent detour that returned an owned
12737 // `Vec<Dep>` (which would type-check but silently clone on every
12738 // accessor call, breaking the zero-cost projection every peer
12739 // sibling slice accessor carries), a `[""] → []` collapse (which
12740 // would silently absorb the `NomeEmpty` refusal case at the
12741 // accessor boundary), or a `[a, a] → [a]` dedup collapse (which
12742 // would silently absorb the `DuplicateNome` refusal case at the
12743 // accessor boundary).
12744 for deps_dev in [
12745 vec![],
12746 vec![Dep::simple("", "^0.1")],
12747 vec![Dep::simple("tatara-check", "^0.1")],
12748 vec![
12749 Dep::simple("tatara-check", "^0.1"),
12750 Dep::simple("caixa-lint", "^0.1"),
12751 ],
12752 vec![
12753 Dep::simple("tatara-check", "^0.1"),
12754 Dep::simple("tatara-check", "^0.2"),
12755 ],
12756 ] {
12757 let c = caixa_with_deps_dev(deps_dev.clone());
12758 assert_eq!(
12759 c.deps_dev(),
12760 deps_dev.as_slice(),
12761 "Caixa::deps_dev must return :deps-dev verbatim (got \
12762 {:?}, expected {deps_dev:?})",
12763 c.deps_dev(),
12764 );
12765 assert_eq!(
12766 c.deps_dev(),
12767 c.deps_dev.as_slice(),
12768 "Caixa::deps_dev must element-equal the raw \
12769 `self.deps_dev.as_slice()` field access across every \
12770 value in the Vec<Dep> accept-set",
12771 );
12772 }
12773 }
12774
12775 #[test]
12776 fn validate_deps_duplicate_deps_dev_arm_routes_through_accessor() {
12777 // Composition pin: [`Caixa::validate_deps`]'s within-`:deps-dev`
12778 // duplicate-`:nome` gate must key off [`Caixa::deps_dev`], not
12779 // the raw `&self.deps_dev` field-borrow walk. Structurally: a
12780 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1"),
12781 // Dep::simple("d", "^0.2")], .. }` must surface the
12782 // `DuplicateNome { list: ":deps-dev" }` refusal exactly, and a
12783 // `Caixa { deps_dev: vec![Dep::simple("d", "^0.1")], .. }` (the
12784 // canonical single-entry form) must pass validate. The pair
12785 // jointly pins the accessor + validate-gate composition: any
12786 // future silent detour that had the accessor return a dedupped
12787 // slice on the `[a, a]` arm (a
12788 // `.iter().unique_by(|d| d.nome.as_str()).collect()` collapse)
12789 // would silently absorb the `DuplicateNome` refusal at the
12790 // accessor boundary and the validate gate would accept a
12791 // struct-literal `Caixa` carrying the drift — the composition
12792 // pin catches that at caixa-core build time.
12793 //
12794 // Peer of `validate_deps_duplicate_arm_routes_through_accessor`
12795 // (ad34b4e) on the sibling `:deps` axis — same "the validate
12796 // gate must route through the substrate-primitive typed
12797 // dispatch" discipline folded onto the sibling `:deps-dev`
12798 // axis, closing the two-list dep-graph composition-pin family.
12799 // The `:deps-dev` diagnostic must carry the
12800 // `DEP_AUTHOR_KEY_DEPS_DEV` list-tag (not
12801 // `DEP_AUTHOR_KEY_DEPS`) so the emitted error names the
12802 // offending list unambiguously.
12803 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1"), Dep::simple("d", "^0.2")]);
12804 let err = c.validate_deps().unwrap_err();
12805 assert!(
12806 matches!(
12807 err,
12808 DepError::DuplicateNome { ref nome, list } if nome == "d"
12809 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
12810 ),
12811 "validate_deps must reject deps_dev == \
12812 vec![Dep(\"d\",\"^0.1\"), Dep(\"d\",\"^0.2\")] with \
12813 DuplicateNome {{ nome: \"d\", list: \":deps-dev\" }} — the \
12814 accessor and the validate gate must route through the \
12815 same substrate-primitive typed dispatch on the :deps-dev \
12816 within-list duplicate arm (got {err:?})",
12817 );
12818 let c = caixa_with_deps_dev(vec![Dep::simple("d", "^0.1")]);
12819 assert!(
12820 c.validate_deps().is_ok(),
12821 "validate_deps must accept deps_dev == \
12822 vec![Dep(\"d\",\"^0.1\")] (the canonical single-entry form)",
12823 );
12824 }
12825
12826 #[test]
12827 fn deps_dev_projects_slice_by_borrow() {
12828 // The by-borrow pin: [`Caixa::deps_dev`] returns `&[Dep]` by
12829 // borrow — the returned slice borrows the underlying `Vec<Dep>`
12830 // storage of the `:deps-dev` slot and the accessor must not
12831 // clone the backing `Vec` on every call. Peer of
12832 // `deps_projects_slice_by_borrow` (ad34b4e) on the sibling
12833 // `:deps` axis, and of the per-`Caixa`
12834 // `autores_projects_slice_by_borrow` (b5d813f),
12835 // `etiquetas_projects_slice_by_borrow` (78c7d3c),
12836 // `bibliotecas_projects_slice_by_borrow` (8a36c23),
12837 // `exe_projects_slice_by_borrow` (65d9527), and
12838 // `servicos_projects_slice_by_borrow` (611f78b) by-borrow pins
12839 // on the sibling outer top-level [`Caixa`] `&[String]`-return
12840 // axes — the accessor's returned slice must borrow from `&self`
12841 // (the returned reference's lifetime is tied to `&self`), and
12842 // calling the accessor twice on the same [`Caixa`] must yield
12843 // slices that are pointer-equal (the underlying byte-buffer is
12844 // the storage `Vec`'s allocation, not a fresh copy) as well as
12845 // value-equal (idempotent, no side effects on `&self`).
12846 //
12847 // Pins against a future silent detour that returned an owned
12848 // `Vec<Dep>` (which would type-check but silently clone on
12849 // every call), a `&Vec<Dep>` return (which would leak the
12850 // backing `Vec`'s grow/push/reserve surface no downstream
12851 // consumer reaches for), or a one-arm-only accessor that
12852 // returned a saturating value on some sentinel input.
12853 for deps_dev in [
12854 vec![],
12855 vec![Dep::simple("tatara-check", "^0.1")],
12856 vec![
12857 Dep::simple("tatara-check", "^0.1"),
12858 Dep::simple("caixa-lint", "^0.1"),
12859 ],
12860 ] {
12861 let c = caixa_with_deps_dev(deps_dev.clone());
12862 let first = c.deps_dev();
12863 let second = c.deps_dev();
12864 assert_eq!(
12865 first, second,
12866 "Caixa::deps_dev must be idempotent — two successive \
12867 calls on the same &self must return the same &[Dep]",
12868 );
12869 assert_eq!(
12870 first.as_ptr(),
12871 second.as_ptr(),
12872 "Caixa::deps_dev must borrow the underlying Vec<Dep> \
12873 storage — two successive calls must return slices \
12874 with the same backing pointer (a fresh Vec<Dep> clone \
12875 would change the pointer on every call)",
12876 );
12877 assert_eq!(
12878 first,
12879 deps_dev.as_slice(),
12880 "Caixa::deps_dev must return :deps-dev verbatim by \
12881 borrow — got {first:?}, expected {deps_dev:?}",
12882 );
12883 }
12884 }
12885
12886 // ── Caixa::limits — outer top-level Option<&LimitsSpec> composite-reference accessor ──
12887
12888 fn caixa_with_limits(limits: Option<crate::LimitsSpec>) -> Caixa {
12889 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
12890 c.limits = limits;
12891 c
12892 }
12893
12894 #[test]
12895 fn limits_returns_limits_option_ref_verbatim_across_permutations() {
12896 // The canonical per-`Caixa` `:limits` M2 typed-slot outer-
12897 // composite optional-composite-reference-shape pin:
12898 // [`Caixa::limits`] must return the `:limits` typed
12899 // `Option<LimitsSpec>` verbatim as an `Option<&LimitsSpec>`
12900 // reference over the same backing storage the raw
12901 // `self.limits.as_ref()` field access borrows from, byte-equal
12902 // across every representative fixture in the accept-set — the
12903 // author-omitted `None` shape (the "engine-default applies"
12904 // partition every downstream Servico M2 overlay emitter treats
12905 // as "emit nothing"), the empty-composite `Some(LimitsSpec {
12906 // .. default })` shape ([`LimitsSpec::is_empty`] holds — every
12907 // per-axis cap is `None`, so the peer M2 overlay emitter's
12908 // `.is_empty()`-gated projection still emits nothing but the
12909 // outer presence-bit is `Some`, so [`Caixa::declared_servico_slots`]
12910 // still pushes the `M2_AUTHOR_KEY_LIMITS` label), a single-axis
12911 // fixture (only `:memory` set — the canonical shape most
12912 // memory-heavy Servicos carry), and a fully-populated composite
12913 // (every per-axis cap set — the canonical shape a
12914 // sandboxed-by-default Servico carries).
12915 //
12916 // Pins against a future silent detour that returned a fresh-
12917 // cloned [`LimitsSpec`] copy (which would type-check via the
12918 // `Clone` impl but silently break every downstream caller that
12919 // relied on the reference sharing the composite's backing
12920 // identity), a reference to an operator-resolved overlay (the
12921 // future per-cluster `:limits-overrides` slot — its resolution
12922 // must land at exactly this accessor body, not silently divert
12923 // the raw slot away from a second consumer), a
12924 // `None` → `Some(LimitsSpec::default)` cluster-default
12925 // projection (which would collapse the load-bearing
12926 // "author-omitted `:limits` ⇒ engine-default applies" partition
12927 // the peer [`crate::render::servico_m2_overlay`] emitter and
12928 // the peer [`Caixa::declared_servico_slots`] enumerator both
12929 // read), or an axis-shuffled projection (a future detour that
12930 // swapped `memory` and `fuel` through the accessor would
12931 // silently split the paired [`crate::StandardLayout::verify`]
12932 // per-`:limits` shape gate's traversal input from the peer
12933 // `servico_m2_overlay` emitter's projection input).
12934 //
12935 // First outer top-level [`Caixa`] `Option<&Composite>`-return
12936 // composite-reference accessor pin on the substrate primitive
12937 // — opens the outer-`Caixa` `Option<&Composite>` composite-
12938 // reference projection pattern the sibling `:behavior`
12939 // [`crate::BehaviorSpec`] / `:politicas`
12940 // [`crate::aplicacao::MeshPolicy`] / `:placement`
12941 // [`crate::aplicacao::Placement`] / `:entrada`
12942 // [`crate::aplicacao::Entrada`] future outer-composite lifts
12943 // fold on. Peer of the closed M3 outer-composite family the
12944 // sibling [`crate::AplicacaoSpec::politicas`] (534dc21) /
12945 // [`crate::AplicacaoSpec::placement`] (9abb8f0) /
12946 // [`crate::AplicacaoSpec::entrada`] (d32111c) composite-
12947 // reference accessor pins already carry on the outer
12948 // [`crate::AplicacaoSpec`] altitude — extends the outer-
12949 // accessor byte-equal-projection discipline onto the outer
12950 // top-level [`Caixa`] M2 Servico-runtime slot altitude.
12951 use crate::LimitsSpec;
12952 use std::time::Duration;
12953 let fixtures: Vec<Option<LimitsSpec>> = vec![
12954 None,
12955 Some(LimitsSpec::default()),
12956 Some(LimitsSpec {
12957 memory: Some(64 * 1024 * 1024),
12958 ..Default::default()
12959 }),
12960 Some(LimitsSpec {
12961 memory: Some(64 * 1024 * 1024),
12962 fuel: Some(1_000_000),
12963 wall_clock: Some(Duration::from_secs(30)),
12964 cpu: Some(500),
12965 }),
12966 ];
12967 for limits in fixtures {
12968 let c = caixa_with_limits(limits.clone());
12969 assert_eq!(
12970 c.limits(),
12971 limits.as_ref(),
12972 "Caixa::limits must return :limits verbatim (got {:?}, \
12973 expected {:?})",
12974 c.limits(),
12975 limits.as_ref(),
12976 );
12977 match (c.limits(), c.limits.as_ref()) {
12978 (Some(a), Some(b)) => assert!(
12979 std::ptr::eq(a, b),
12980 "Caixa::limits accessor and self.limits.as_ref() \
12981 field access must borrow the same backing storage \
12982 — the accessor is the substrate-primitive typed \
12983 dispatch every downstream Servico-M2-overlay \
12984 composite consumer must route through, and a \
12985 reference-identity split would silently break \
12986 every consumer that relied on the borrow sharing \
12987 the composite's storage",
12988 ),
12989 (None, None) => {}
12990 _ => panic!(
12991 "Caixa::limits presence bit must byte-equal \
12992 self.limits.is_some() — a presence-bit drift would \
12993 silently split the paired StandardLayout::verify \
12994 per-`:limits` shape gate's traversal head from \
12995 the peer render::servico_m2_overlay M2 overlay \
12996 emitter's traversal head from the peer \
12997 Caixa::declared_servico_slots M2 declared-slot \
12998 enumerator's presence probe",
12999 ),
13000 }
13001 assert_eq!(
13002 c.limits().is_some(),
13003 c.limits.is_some(),
13004 "Caixa::limits().is_some() must byte-equal \
13005 self.limits.is_some() — a presence-bit drift would \
13006 silently split every downstream Option<&LimitsSpec> \
13007 consumer's partition on the engine-default arm",
13008 );
13009 }
13010 }
13011
13012 #[test]
13013 fn declared_servico_slots_limits_arm_routes_through_accessor() {
13014 // Composition pin: [`Caixa::declared_servico_slots`]'s
13015 // `:limits` presence-probe arm must key off [`Caixa::limits`],
13016 // not the raw `self.limits.is_some()` field-probe. Structurally:
13017 // a `Caixa { limits: Some(LimitsSpec::default()), .. }` must
13018 // still push `M2_AUTHOR_KEY_LIMITS` onto the declared-slot list
13019 // (the presence bit is `Some`, so the M2 kind-coherence gate
13020 // must surface the slot as "declared" even when every per-axis
13021 // cap is unset), and a `Caixa { limits: None, .. }` must NOT
13022 // push the label (the "author omitted the slot entirely"
13023 // partition). The pair jointly pins the accessor + declared-
13024 // slot enumerator composition: any future silent detour that
13025 // had the accessor collapse `Some(LimitsSpec::default())` to
13026 // `None` (a `.filter(|l| !l.is_empty())` projection) would
13027 // silently absorb the "declared but empty" arm at the
13028 // accessor boundary and the [`crate::LayoutError::ServicoSlotsOnNonServico`]
13029 // kind-coherence gate would silently accept a
13030 // struct-literal `Caixa` carrying the drift.
13031 //
13032 // Peer of the sibling per-`Caixa`
13033 // `validate_deps_duplicate_arm_routes_through_accessor` (ad34b4e)
13034 // and `validate_deps_duplicate_deps_dev_arm_routes_through_accessor`
13035 // (f7fd81e) accessor-composition pins on the sibling `:deps` /
13036 // `:deps-dev` outer-`&[Dep]`-composition axes — same "the
13037 // enumerator gate must route through the substrate-primitive
13038 // typed dispatch" discipline extended onto the outer top-level
13039 // [`Caixa`] `Option<&LimitsSpec>`-composition surface, opening
13040 // the outer-`Caixa` M2 Servico-runtime-slot arm of the
13041 // composition-pin family.
13042 use crate::LimitsSpec;
13043 let c = caixa_with_limits(Some(LimitsSpec::default()));
13044 let slots = c.declared_servico_slots();
13045 assert!(
13046 slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13047 "declared_servico_slots must push M2_AUTHOR_KEY_LIMITS \
13048 when `:limits` is Some (even for LimitsSpec::default()) \
13049 — the accessor and the enumerator gate must route through \
13050 the same substrate-primitive typed dispatch on the outer \
13051 :limits presence bit (got slots={slots:?})",
13052 );
13053 let c = caixa_with_limits(None);
13054 let slots = c.declared_servico_slots();
13055 assert!(
13056 !slots.contains(&crate::render::M2_AUTHOR_KEY_LIMITS),
13057 "declared_servico_slots must NOT push M2_AUTHOR_KEY_LIMITS \
13058 when `:limits` is None — the author-omitted arm must \
13059 route through the accessor's None-return unchanged (got \
13060 slots={slots:?})",
13061 );
13062 }
13063
13064 #[test]
13065 fn servico_m2_overlay_limits_arm_routes_through_accessor() {
13066 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13067 // per-`:limits` M2 overlay emit arm must key off
13068 // [`Caixa::limits`], not the raw `&caixa.limits` field-borrow.
13069 // Structurally: a `Caixa { limits: Some(LimitsSpec { memory:
13070 // Some(64 MiB), .. default }), .. }` must surface the
13071 // `M2_KEY_LIMITS` key with the per-axis
13072 // `memory: "64MiB"` sub-mapping in the overlay, a `Caixa {
13073 // limits: Some(LimitsSpec::default()), .. }` must omit the
13074 // key entirely (the `.is_empty()`-gated inner arm elides an
13075 // empty composite even when the outer presence bit is `Some`),
13076 // and a `Caixa { limits: None, .. }` must also omit the key
13077 // (the "author omitted the slot entirely" partition). The
13078 // three-fixture family jointly pins the accessor + M2 overlay
13079 // emitter composition: any future silent detour that had the
13080 // accessor return a fresh-cloned copy on the `Some` arm (a
13081 // `LimitsSpec::clone()` projection) would silently break the
13082 // reference-identity pin the peer per-axis
13083 // `serde_yaml::to_value(limits)` projection reads from.
13084 use crate::LimitsSpec;
13085 use crate::render::{M2_KEY_LIMITS, servico_m2_overlay};
13086 let c = caixa_with_limits(Some(LimitsSpec {
13087 memory: Some(64 * 1024 * 1024),
13088 ..Default::default()
13089 }));
13090 let overlay = servico_m2_overlay(&c).unwrap();
13091 assert!(
13092 overlay.contains_key(M2_KEY_LIMITS),
13093 "servico_m2_overlay must surface M2_KEY_LIMITS when \
13094 `:limits` carries a non-empty composite — the accessor \
13095 and the M2 overlay emitter must route through the same \
13096 substrate-primitive typed dispatch on the outer :limits \
13097 composite (got overlay={overlay:?})",
13098 );
13099 let c = caixa_with_limits(Some(LimitsSpec::default()));
13100 let overlay = servico_m2_overlay(&c).unwrap();
13101 assert!(
13102 !overlay.contains_key(M2_KEY_LIMITS),
13103 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13104 `:limits` is Some(LimitsSpec::default()) — the empty \
13105 composite's `.is_empty()`-gated inner arm must elide \
13106 the key regardless of the outer presence bit (got \
13107 overlay={overlay:?})",
13108 );
13109 let c = caixa_with_limits(None);
13110 let overlay = servico_m2_overlay(&c).unwrap();
13111 assert!(
13112 !overlay.contains_key(M2_KEY_LIMITS),
13113 "servico_m2_overlay must omit M2_KEY_LIMITS when \
13114 `:limits` is None — the author-omitted arm must route \
13115 through the accessor's None-return unchanged (got \
13116 overlay={overlay:?})",
13117 );
13118 }
13119
13120 #[test]
13121 fn limits_projects_option_ref_by_borrow() {
13122 // The by-borrow pin: [`Caixa::limits`] returns
13123 // `Option<&LimitsSpec>` by borrow — the returned reference
13124 // borrows the underlying `Option<LimitsSpec>` storage of the
13125 // `:limits` slot and the accessor must not clone the backing
13126 // composite on every call. Peer of the sibling
13127 // `deps_projects_slice_by_borrow` (ad34b4e) /
13128 // `deps_dev_projects_slice_by_borrow` (f7fd81e) by-borrow pins
13129 // on the outer top-level [`Caixa`] `&[Dep]`-return axes —
13130 // extended here to the outer [`Caixa`] `Option<&Composite>`-
13131 // return axis: the accessor's returned reference must borrow
13132 // from `&self` (the returned reference's lifetime is tied to
13133 // `&self`), and calling the accessor twice on the same
13134 // [`Caixa`] must yield references that are pointer-equal (the
13135 // underlying byte-buffer is the storage `LimitsSpec`'s
13136 // allocation, not a fresh copy) as well as value-equal
13137 // (idempotent, no side effects on `&self`).
13138 //
13139 // Pins against a future silent detour that returned an owned
13140 // `LimitsSpec` (which would type-check via the `Clone` impl
13141 // but silently clone on every call), a `&LimitsSpec` panic-
13142 // return on the `None` arm (which would collapse the load-
13143 // bearing `Option` presence-bit into a runtime panic), or a
13144 // one-arm-only accessor that returned a saturating composite
13145 // on some sentinel input.
13146 use crate::LimitsSpec;
13147 use std::time::Duration;
13148 for limits in [
13149 Some(LimitsSpec::default()),
13150 Some(LimitsSpec {
13151 memory: Some(64 * 1024 * 1024),
13152 fuel: Some(1_000_000),
13153 wall_clock: Some(Duration::from_secs(30)),
13154 cpu: Some(500),
13155 }),
13156 ] {
13157 let c = caixa_with_limits(limits.clone());
13158 let first = c.limits().unwrap();
13159 let second = c.limits().unwrap();
13160 assert_eq!(
13161 first, second,
13162 "Caixa::limits must be idempotent — two successive \
13163 calls on the same &self must return the same \
13164 &LimitsSpec",
13165 );
13166 assert!(
13167 std::ptr::eq(first, second),
13168 "Caixa::limits must borrow the underlying \
13169 Option<LimitsSpec> storage — two successive calls \
13170 must return references with the same backing pointer \
13171 (a fresh LimitsSpec clone would change the pointer \
13172 on every call)",
13173 );
13174 assert_eq!(
13175 Some(first),
13176 limits.as_ref(),
13177 "Caixa::limits must return :limits verbatim by borrow \
13178 — got {first:?}, expected {:?}",
13179 limits.as_ref(),
13180 );
13181 }
13182 let c = caixa_with_limits(None);
13183 assert!(
13184 c.limits().is_none(),
13185 "Caixa::limits must return None when :limits is absent — \
13186 the author-omitted arm must project through the \
13187 accessor's Option::None unchanged",
13188 );
13189 }
13190
13191 // ── Caixa::behavior — outer top-level Option<&BehaviorSpec> composite-reference accessor ──
13192
13193 fn caixa_with_behavior(behavior: Option<crate::BehaviorSpec>) -> Caixa {
13194 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13195 c.behavior = behavior;
13196 c
13197 }
13198
13199 #[test]
13200 fn behavior_returns_behavior_option_ref_verbatim_across_permutations() {
13201 // The canonical per-`Caixa` `:behavior` M2 typed-slot outer-
13202 // composite optional-composite-reference-shape pin:
13203 // [`Caixa::behavior`] must return the `:behavior` typed
13204 // `Option<BehaviorSpec>` verbatim as an `Option<&BehaviorSpec>`
13205 // reference over the same backing storage the raw
13206 // `self.behavior.as_ref()` field access borrows from, byte-equal
13207 // across every representative fixture in the accept-set — the
13208 // author-omitted `None` shape (the "runtime-default applies"
13209 // partition every downstream Servico M2 overlay emitter treats
13210 // as "emit nothing"), the empty-composite `Some(BehaviorSpec {
13211 // .. default })` shape ([`BehaviorSpec::is_empty`] holds —
13212 // every per-callback path is `None`, so the peer M2 overlay
13213 // emitter's `.is_empty()`-gated projection still emits nothing
13214 // but the outer presence-bit is `Some`, so
13215 // [`Caixa::declared_servico_slots`] still pushes the
13216 // `M2_AUTHOR_KEY_BEHAVIOR` label), a single-callback fixture
13217 // (only `:on-state-change` set — the canonical shape a caixa
13218 // that only wires the hot-upgrade migration path carries), and
13219 // a fully-populated composite (every per-callback path set —
13220 // the canonical shape a fully-instrumented gen_server-shaped
13221 // Servico carries).
13222 //
13223 // Peer of the sibling
13224 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13225 // (b2bd9d7) opening fixture-family + reference-identity +
13226 // presence-bit tetrad pin on the outer top-level [`Caixa`]
13227 // `Option<&Composite>`-return sub-family — extended here to the
13228 // second axis of that sub-family so both of the currently-lifted
13229 // M2 Servico-runtime `Option<&Composite>` slots (`:limits` /
13230 // `:behavior`) carry the same "byte-equal, borrow-shared,
13231 // presence-bit-preserved" outer-accessor discipline.
13232 //
13233 // Pins against a future silent detour that returned a fresh-
13234 // cloned [`crate::BehaviorSpec`] copy (which would type-check
13235 // via the `Clone` impl but silently break every downstream
13236 // caller that relied on the reference sharing the composite's
13237 // backing identity), a reference to an operator-resolved
13238 // overlay (a future per-cluster `:behavior-overrides` slot —
13239 // its resolution must land at exactly this accessor body, not
13240 // silently divert the raw slot away from a second consumer), a
13241 // `None` → `Some(BehaviorSpec::default)` cluster-default
13242 // projection (which would collapse the load-bearing
13243 // "author-omitted `:behavior` ⇒ runtime-default applies"
13244 // partition the peer [`crate::render::servico_m2_overlay`]
13245 // emitter, the peer [`Caixa::declared_servico_slots`]
13246 // enumerator, and the cross-slot
13247 // [`crate::upgrade::validate_upgrade_from_against_behavior`]
13248 // gate all read), or a callback-shuffled projection (a future
13249 // detour that swapped `on_init` and `on_terminate` through the
13250 // accessor would silently split the paired
13251 // [`crate::StandardLayout::verify`] per-`:behavior` shape gate's
13252 // traversal input from the peer `servico_m2_overlay` emitter's
13253 // projection input from the cross-slot `:state-change`
13254 // composition gate's traversal input).
13255 use crate::BehaviorSpec;
13256 use std::path::PathBuf;
13257 let fixtures: Vec<Option<BehaviorSpec>> = vec![
13258 None,
13259 Some(BehaviorSpec::default()),
13260 Some(BehaviorSpec {
13261 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13262 ..Default::default()
13263 }),
13264 Some(BehaviorSpec {
13265 on_init: Some(PathBuf::from("lib/init.lisp")),
13266 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13267 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13268 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13269 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13270 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13271 }),
13272 ];
13273 for behavior in fixtures {
13274 let c = caixa_with_behavior(behavior.clone());
13275 assert_eq!(
13276 c.behavior(),
13277 behavior.as_ref(),
13278 "Caixa::behavior must return :behavior verbatim (got \
13279 {:?}, expected {:?})",
13280 c.behavior(),
13281 behavior.as_ref(),
13282 );
13283 match (c.behavior(), c.behavior.as_ref()) {
13284 (Some(a), Some(b)) => assert!(
13285 std::ptr::eq(a, b),
13286 "Caixa::behavior accessor and self.behavior.as_ref() \
13287 field access must borrow the same backing storage \
13288 — the accessor is the substrate-primitive typed \
13289 dispatch every downstream Servico-M2-overlay \
13290 composite consumer must route through, and a \
13291 reference-identity split would silently break \
13292 every consumer that relied on the borrow sharing \
13293 the composite's storage",
13294 ),
13295 (None, None) => {}
13296 _ => panic!(
13297 "Caixa::behavior presence bit must byte-equal \
13298 self.behavior.is_some() — a presence-bit drift \
13299 would silently split the paired \
13300 StandardLayout::verify per-`:behavior` shape \
13301 gate's traversal head from the peer \
13302 render::servico_m2_overlay M2 overlay emitter's \
13303 traversal head from the cross-slot \
13304 validate_upgrade_from_against_behavior \
13305 composition gate's traversal head from the peer \
13306 Caixa::declared_servico_slots M2 declared-slot \
13307 enumerator's presence probe",
13308 ),
13309 }
13310 assert_eq!(
13311 c.behavior().is_some(),
13312 c.behavior.is_some(),
13313 "Caixa::behavior().is_some() must byte-equal \
13314 self.behavior.is_some() — a presence-bit drift would \
13315 silently split every downstream Option<&BehaviorSpec> \
13316 consumer's partition on the runtime-default arm",
13317 );
13318 }
13319 }
13320
13321 #[test]
13322 fn declared_servico_slots_behavior_arm_routes_through_accessor() {
13323 // Composition pin: [`Caixa::declared_servico_slots`]'s
13324 // `:behavior` presence-probe arm must key off
13325 // [`Caixa::behavior`], not the raw `self.behavior.is_some()`
13326 // field-probe. Structurally: a `Caixa { behavior:
13327 // Some(BehaviorSpec::default()), .. }` must still push
13328 // `M2_AUTHOR_KEY_BEHAVIOR` onto the declared-slot list (the
13329 // presence bit is `Some`, so the M2 kind-coherence gate must
13330 // surface the slot as "declared" even when every per-callback
13331 // path is unset), and a `Caixa { behavior: None, .. }` must
13332 // NOT push the label (the "author omitted the slot entirely"
13333 // partition). The pair jointly pins the accessor + declared-
13334 // slot enumerator composition: any future silent detour that
13335 // had the accessor collapse `Some(BehaviorSpec::default())`
13336 // to `None` (a `.filter(|b| !b.is_empty())` projection) would
13337 // silently absorb the "declared but empty" arm at the
13338 // accessor boundary and the
13339 // [`crate::LayoutError::ServicoSlotsOnNonServico`]
13340 // kind-coherence gate would silently accept a struct-literal
13341 // `Caixa` carrying the drift.
13342 //
13343 // Peer of the sibling
13344 // `declared_servico_slots_limits_arm_routes_through_accessor`
13345 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13346 // `Option<&LimitsSpec>` arm of the same
13347 // [`Caixa::declared_servico_slots`] M2 declared-slot
13348 // enumerator's traversal — same "the enumerator gate must
13349 // route through the substrate-primitive typed dispatch"
13350 // discipline extended onto the outer top-level [`Caixa`]
13351 // `Option<&BehaviorSpec>`-composition surface.
13352 use crate::BehaviorSpec;
13353 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13354 let slots = c.declared_servico_slots();
13355 assert!(
13356 slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13357 "declared_servico_slots must push M2_AUTHOR_KEY_BEHAVIOR \
13358 when `:behavior` is Some (even for BehaviorSpec::default()) \
13359 — the accessor and the enumerator gate must route through \
13360 the same substrate-primitive typed dispatch on the outer \
13361 :behavior presence bit (got slots={slots:?})",
13362 );
13363 let c = caixa_with_behavior(None);
13364 let slots = c.declared_servico_slots();
13365 assert!(
13366 !slots.contains(&crate::render::M2_AUTHOR_KEY_BEHAVIOR),
13367 "declared_servico_slots must NOT push M2_AUTHOR_KEY_BEHAVIOR \
13368 when `:behavior` is None — the author-omitted arm must \
13369 route through the accessor's None-return unchanged (got \
13370 slots={slots:?})",
13371 );
13372 }
13373
13374 #[test]
13375 fn servico_m2_overlay_behavior_arm_routes_through_accessor() {
13376 // Composition pin: [`crate::render::servico_m2_overlay`]'s
13377 // per-`:behavior` M2 overlay emit arm must key off
13378 // [`Caixa::behavior`], not the raw `&caixa.behavior`
13379 // field-borrow. Structurally: a `Caixa { behavior:
13380 // Some(BehaviorSpec { on_state_change: Some(...), .. default
13381 // }), .. }` must surface the `M2_KEY_BEHAVIOR` key with the
13382 // per-callback `onStateChange` sub-mapping in the overlay, a
13383 // `Caixa { behavior: Some(BehaviorSpec::default()), .. }`
13384 // must omit the key entirely (the `.is_empty()`-gated inner
13385 // arm elides an empty composite even when the outer presence
13386 // bit is `Some`), and a `Caixa { behavior: None, .. }` must
13387 // also omit the key (the "author omitted the slot entirely"
13388 // partition). The three-fixture family jointly pins the
13389 // accessor + M2 overlay emitter composition: any future
13390 // silent detour that had the accessor return a fresh-cloned
13391 // copy on the `Some` arm (a `BehaviorSpec::clone()`
13392 // projection) would silently break the reference-identity
13393 // pin the peer per-callback `serde_yaml::to_value(behavior)`
13394 // projection reads from.
13395 //
13396 // Peer of the sibling
13397 // `servico_m2_overlay_limits_arm_routes_through_accessor`
13398 // (b2bd9d7) composition pin on the sibling `:limits` outer-
13399 // `Option<&LimitsSpec>` arm of the same
13400 // [`crate::render::servico_m2_overlay`] M2 overlay emitter's
13401 // traversal — same "the emitter must route through the
13402 // substrate-primitive typed dispatch on the outer composite"
13403 // discipline extended onto the outer top-level [`Caixa`]
13404 // `Option<&BehaviorSpec>`-composition surface.
13405 use crate::BehaviorSpec;
13406 use crate::render::{M2_KEY_BEHAVIOR, servico_m2_overlay};
13407 use std::path::PathBuf;
13408 let c = caixa_with_behavior(Some(BehaviorSpec {
13409 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13410 ..Default::default()
13411 }));
13412 let overlay = servico_m2_overlay(&c).unwrap();
13413 assert!(
13414 overlay.contains_key(M2_KEY_BEHAVIOR),
13415 "servico_m2_overlay must surface M2_KEY_BEHAVIOR when \
13416 `:behavior` carries a non-empty composite — the accessor \
13417 and the M2 overlay emitter must route through the same \
13418 substrate-primitive typed dispatch on the outer :behavior \
13419 composite (got overlay={overlay:?})",
13420 );
13421 let c = caixa_with_behavior(Some(BehaviorSpec::default()));
13422 let overlay = servico_m2_overlay(&c).unwrap();
13423 assert!(
13424 !overlay.contains_key(M2_KEY_BEHAVIOR),
13425 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13426 `:behavior` is Some(BehaviorSpec::default()) — the empty \
13427 composite's `.is_empty()`-gated inner arm must elide the \
13428 key regardless of the outer presence bit (got \
13429 overlay={overlay:?})",
13430 );
13431 let c = caixa_with_behavior(None);
13432 let overlay = servico_m2_overlay(&c).unwrap();
13433 assert!(
13434 !overlay.contains_key(M2_KEY_BEHAVIOR),
13435 "servico_m2_overlay must omit M2_KEY_BEHAVIOR when \
13436 `:behavior` is None — the author-omitted arm must route \
13437 through the accessor's None-return unchanged (got \
13438 overlay={overlay:?})",
13439 );
13440 }
13441
13442 #[test]
13443 fn behavior_projects_option_ref_by_borrow() {
13444 // The by-borrow pin: [`Caixa::behavior`] returns
13445 // `Option<&BehaviorSpec>` by borrow — the returned reference
13446 // borrows the underlying `Option<BehaviorSpec>` storage of the
13447 // `:behavior` slot and the accessor must not clone the backing
13448 // composite on every call. Peer of the sibling
13449 // `limits_projects_option_ref_by_borrow` (b2bd9d7) by-borrow
13450 // pin on the outer top-level [`Caixa`] `Option<&Composite>`-
13451 // return sub-family — extended here to the second axis of the
13452 // same sub-family: the accessor's returned reference must
13453 // borrow from `&self` (the returned reference's lifetime is
13454 // tied to `&self`), and calling the accessor twice on the same
13455 // [`Caixa`] must yield references that are pointer-equal (the
13456 // underlying byte-buffer is the storage `BehaviorSpec`'s
13457 // allocation, not a fresh copy) as well as value-equal
13458 // (idempotent, no side effects on `&self`).
13459 //
13460 // Pins against a future silent detour that returned an owned
13461 // `BehaviorSpec` (which would type-check via the `Clone` impl
13462 // but silently clone on every call), a `&BehaviorSpec` panic-
13463 // return on the `None` arm (which would collapse the load-
13464 // bearing `Option` presence-bit into a runtime panic), or a
13465 // one-arm-only accessor that returned a saturating composite
13466 // on some sentinel input.
13467 use crate::BehaviorSpec;
13468 use std::path::PathBuf;
13469 for behavior in [
13470 Some(BehaviorSpec::default()),
13471 Some(BehaviorSpec {
13472 on_init: Some(PathBuf::from("lib/init.lisp")),
13473 on_call: Some(PathBuf::from("lib/handlers.lisp")),
13474 on_cast: Some(PathBuf::from("lib/handlers.lisp")),
13475 on_info: Some(PathBuf::from("lib/handlers.lisp")),
13476 on_state_change: Some(PathBuf::from("lib/migrations.lisp")),
13477 on_terminate: Some(PathBuf::from("lib/cleanup.lisp")),
13478 }),
13479 ] {
13480 let c = caixa_with_behavior(behavior.clone());
13481 let first = c.behavior().unwrap();
13482 let second = c.behavior().unwrap();
13483 assert_eq!(
13484 first, second,
13485 "Caixa::behavior must be idempotent — two successive \
13486 calls on the same &self must return the same \
13487 &BehaviorSpec",
13488 );
13489 assert!(
13490 std::ptr::eq(first, second),
13491 "Caixa::behavior must borrow the underlying \
13492 Option<BehaviorSpec> storage — two successive calls \
13493 must return references with the same backing pointer \
13494 (a fresh BehaviorSpec clone would change the pointer \
13495 on every call)",
13496 );
13497 assert_eq!(
13498 Some(first),
13499 behavior.as_ref(),
13500 "Caixa::behavior must return :behavior verbatim by \
13501 borrow — got {first:?}, expected {:?}",
13502 behavior.as_ref(),
13503 );
13504 }
13505 let c = caixa_with_behavior(None);
13506 assert!(
13507 c.behavior().is_none(),
13508 "Caixa::behavior must return None when :behavior is absent \
13509 — the author-omitted arm must project through the \
13510 accessor's Option::None unchanged",
13511 );
13512 }
13513
13514 // ── Caixa::politicas — outer top-level Option<&MeshPolicy> composite-reference accessor ──
13515
13516 fn caixa_aplicacao_with_politicas(politicas: Option<crate::aplicacao::MeshPolicy>) -> Caixa {
13517 use crate::aplicacao::{Membro, WitContract};
13518 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13519 c.kind = CaixaKind::Aplicacao;
13520 c.membros = vec![Membro {
13521 caixa: "a".into(),
13522 versao: "^0.1".into(),
13523 }];
13524 c.contratos = vec![WitContract {
13525 de: "a".into(),
13526 para: "a".into(),
13527 wit: "wasi:http/proxy".into(),
13528 endpoint: Some("/x".into()),
13529 subject: None,
13530 slot: None,
13531 }];
13532 c.politicas = politicas;
13533 c
13534 }
13535
13536 #[test]
13537 fn politicas_returns_politicas_option_ref_verbatim_across_permutations() {
13538 // The canonical per-`Caixa` `:politicas` M3 mesh-slot outer-
13539 // composite optional-composite-reference-shape pin:
13540 // [`Caixa::politicas`] must return the `:politicas` typed
13541 // `Option<MeshPolicy>` verbatim as an `Option<&MeshPolicy>`
13542 // reference over the same backing storage the raw
13543 // `self.politicas.as_ref()` field access borrows from,
13544 // byte-equal across every representative fixture in the
13545 // accept-set — the author-omitted `None` shape (the "cluster-
13546 // default applies" partition every downstream mesh-artifact
13547 // emitter treats as "emit no `:politicas` overlay"), the
13548 // empty-composite `Some(MeshPolicy { .. default })` shape
13549 // ([`crate::aplicacao::MeshPolicy::is_empty`] holds — every
13550 // per-axis mesh-policy scalar is `None`, so the peer inner
13551 // [`crate::AplicacaoSpec::politicas`] `.is_empty()`-gated
13552 // caixa-mesh overlay elides every per-axis emit but the outer
13553 // presence-bit is `Some`, so [`Caixa::declared_mesh_slots`]
13554 // still pushes the `M3_AUTHOR_KEY_POLITICAS` label), a
13555 // single-axis fixture (only `:timeout` set — the canonical
13556 // shape a latency-sensitive Aplicacao carries), and a
13557 // fully-populated composite (every per-axis mesh-policy
13558 // scalar set — the canonical shape a fully-governed
13559 // Aplicacao carries).
13560 //
13561 // Pins against a future silent detour that returned a fresh-
13562 // cloned [`crate::aplicacao::MeshPolicy`] copy (which would
13563 // type-check via the `Clone` impl but silently break every
13564 // downstream caller that relied on the reference sharing the
13565 // composite's backing identity), a reference to an operator-
13566 // resolved overlay (the future per-cluster
13567 // `:politicas-overrides` slot — its resolution must land at
13568 // exactly this accessor body, not silently divert the raw
13569 // slot away from the peer [`Caixa::declared_mesh_slots`]
13570 // enumerator's presence probe), a
13571 // `None` → `Some(MeshPolicy::default)` cluster-default
13572 // projection (which would collapse the load-bearing
13573 // "author-omitted `:politicas` ⇒ cluster-default applies"
13574 // partition the peer [`Caixa::declared_mesh_slots`]
13575 // enumerator and the peer [`Caixa::aplicacao_view`]
13576 // Aplicacao-composition seed both read), or an axis-shuffled
13577 // projection (a future detour that swapped `timeout` and
13578 // `retries` through the accessor would silently split the
13579 // paired [`Caixa::aplicacao_view`] seed's fold input from the
13580 // sibling M3 mesh-artifact emitter's projection input).
13581 //
13582 // Third outer top-level [`Caixa`] `Option<&Composite>`-return
13583 // composite-reference accessor pin on the substrate primitive
13584 // — peer of the sibling
13585 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13586 // (b2bd9d7) and
13587 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13588 // (35d8b52) opening tetrad pins on the outer top-level
13589 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13590 // here to the first of the three M3 mesh-slot axes so the
13591 // opening third of the outer `Option<&Composite>` sub-family
13592 // carries the same "byte-equal, borrow-shared, presence-bit-
13593 // preserved" outer-accessor discipline.
13594 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13595 use std::time::Duration;
13596 let fixtures: Vec<Option<MeshPolicy>> = vec![
13597 None,
13598 Some(MeshPolicy::default()),
13599 Some(MeshPolicy {
13600 timeout: Some(Duration::from_secs(30)),
13601 ..Default::default()
13602 }),
13603 Some(MeshPolicy {
13604 timeout: Some(Duration::from_secs(30)),
13605 retries: Some(3),
13606 circuit_breaker: Some(CircuitBreaker {
13607 max_failures: 5,
13608 window: Duration::from_secs(60),
13609 }),
13610 mtls_required: Some(true),
13611 rate_limit: Some(RateLimit {
13612 rate: 100,
13613 window: Duration::from_secs(1),
13614 }),
13615 }),
13616 ];
13617 for politicas in fixtures {
13618 let c = caixa_aplicacao_with_politicas(politicas.clone());
13619 assert_eq!(
13620 c.politicas(),
13621 politicas.as_ref(),
13622 "Caixa::politicas must return :politicas verbatim (got \
13623 {:?}, expected {:?})",
13624 c.politicas(),
13625 politicas.as_ref(),
13626 );
13627 match (c.politicas(), c.politicas.as_ref()) {
13628 (Some(a), Some(b)) => assert!(
13629 std::ptr::eq(a, b),
13630 "Caixa::politicas accessor and self.politicas.as_ref() \
13631 field access must borrow the same backing storage \
13632 — the accessor is the substrate-primitive typed \
13633 dispatch every downstream Aplicacao-mesh-overlay \
13634 composite consumer must route through, and a \
13635 reference-identity split would silently break \
13636 every consumer that relied on the borrow sharing \
13637 the composite's storage",
13638 ),
13639 (None, None) => {}
13640 _ => panic!(
13641 "Caixa::politicas presence bit must byte-equal \
13642 self.politicas.is_some() — a presence-bit drift \
13643 would silently split the paired \
13644 Caixa::aplicacao_view Aplicacao-composition seed's \
13645 traversal head from the peer \
13646 Caixa::declared_mesh_slots M3 declared-slot \
13647 enumerator's presence probe",
13648 ),
13649 }
13650 assert_eq!(
13651 c.politicas().is_some(),
13652 c.politicas.is_some(),
13653 "Caixa::politicas().is_some() must byte-equal \
13654 self.politicas.is_some() — a presence-bit drift would \
13655 silently split every downstream Option<&MeshPolicy> \
13656 consumer's partition on the cluster-default arm",
13657 );
13658 }
13659 }
13660
13661 #[test]
13662 fn declared_mesh_slots_politicas_arm_routes_through_accessor() {
13663 // Composition pin: [`Caixa::declared_mesh_slots`]'s
13664 // `:politicas` presence-probe arm must key off
13665 // [`Caixa::politicas`], not the raw `self.politicas.is_some()`
13666 // field-probe. Structurally: a `Caixa { politicas:
13667 // Some(MeshPolicy::default()), .. }` must still push
13668 // `M3_AUTHOR_KEY_POLITICAS` onto the declared-slot list (the
13669 // presence bit is `Some`, so the M3 kind-coherence gate must
13670 // surface the slot as "declared" even when every per-axis
13671 // scalar is unset), and a `Caixa { politicas: None, .. }` must
13672 // NOT push the label (the "author omitted the slot entirely"
13673 // partition). The pair jointly pins the accessor + declared-
13674 // slot enumerator composition: any future silent detour that
13675 // had the accessor collapse `Some(MeshPolicy::default())` to
13676 // `None` (a `.filter(|p| !p.is_empty())` projection) would
13677 // silently absorb the "declared but empty" arm at the
13678 // accessor boundary and the
13679 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
13680 // coherence gate would silently accept a struct-literal
13681 // `Caixa` carrying the drift.
13682 //
13683 // Peer of the sibling
13684 // `declared_servico_slots_limits_arm_routes_through_accessor`
13685 // (b2bd9d7) and
13686 // `declared_servico_slots_behavior_arm_routes_through_accessor`
13687 // (35d8b52) composition pins on the sibling `:limits` /
13688 // `:behavior` outer-`Option<&Composite>` arms of the peer
13689 // [`Caixa::declared_servico_slots`] M2 declared-slot
13690 // enumerator's traversal — same "the enumerator gate must
13691 // route through the substrate-primitive typed dispatch"
13692 // discipline extended onto the outer top-level [`Caixa`] M3
13693 // mesh-slot family so the [`Caixa::declared_mesh_slots`]
13694 // enumerator carries the same routing invariant as its M2
13695 // sibling.
13696 use crate::aplicacao::MeshPolicy;
13697 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13698 let slots = c.declared_mesh_slots();
13699 assert!(
13700 slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13701 "declared_mesh_slots must push M3_AUTHOR_KEY_POLITICAS \
13702 when `:politicas` is Some (even for MeshPolicy::default()) \
13703 — the accessor and the enumerator gate must route through \
13704 the same substrate-primitive typed dispatch on the outer \
13705 :politicas presence bit (got slots={slots:?})",
13706 );
13707 let c = caixa_aplicacao_with_politicas(None);
13708 let slots = c.declared_mesh_slots();
13709 assert!(
13710 !slots.contains(&crate::render::M3_AUTHOR_KEY_POLITICAS),
13711 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_POLITICAS \
13712 when `:politicas` is None — the author-omitted arm must \
13713 route through the accessor's None-return unchanged (got \
13714 slots={slots:?})",
13715 );
13716 }
13717
13718 #[test]
13719 fn aplicacao_view_politicas_arm_folds_through_accessor() {
13720 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:politicas`
13721 // Aplicacao-composition seed must fold through
13722 // [`Caixa::politicas`], not the raw
13723 // `self.politicas.clone().unwrap_or_default()` field-borrow.
13724 // Structurally: a `Caixa { politicas: Some(MeshPolicy {
13725 // timeout: Some(30s), .. default }), kind: Aplicacao, .. }`
13726 // must surface a projected [`crate::AplicacaoSpec`] whose
13727 // `politicas().timeout()` field byte-equals the outer
13728 // composite's `timeout` scalar (the fold must project the
13729 // authored composite verbatim), a `Caixa { politicas:
13730 // Some(MeshPolicy::default()), kind: Aplicacao, .. }` must
13731 // surface an [`crate::AplicacaoSpec`] whose `politicas()`
13732 // byte-equals [`crate::aplicacao::MeshPolicy::default`] (the
13733 // fold's empty-composite arm collapses to the same default the
13734 // author-omitted arm does), and a `Caixa { politicas: None,
13735 // kind: Aplicacao, .. }` must surface an
13736 // [`crate::AplicacaoSpec`] whose `politicas()` byte-equals
13737 // [`crate::aplicacao::MeshPolicy::default`] (the "author
13738 // omitted the slot entirely" arm folds through the
13739 // `unwrap_or_default` onto the cluster-default). The triad
13740 // jointly pins the accessor + Aplicacao-composition seed
13741 // composition: any future silent detour that had the accessor
13742 // divert the raw slot away from the seed's fold (an operator-
13743 // resolved overlay's default-fold arm silently differing from
13744 // the raw slot's default-fold arm) would silently split the
13745 // build-time mesh-artifact emission gate from the caixa-mesh
13746 // renderer's Aplicacao-view input at the composition boundary.
13747 use crate::aplicacao::MeshPolicy;
13748 use std::time::Duration;
13749 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy {
13750 timeout: Some(Duration::from_secs(30)),
13751 ..Default::default()
13752 }));
13753 let view = c.aplicacao_view().unwrap();
13754 assert_eq!(
13755 view.politicas().timeout(),
13756 Some(Duration::from_secs(30)),
13757 "Caixa::aplicacao_view must fold the authored :politicas \
13758 :timeout scalar through the accessor verbatim onto the \
13759 projected AplicacaoSpec — a future silent detour at the \
13760 seed's fold arm would surface here as a projected-scalar \
13761 drift (got {:?})",
13762 view.politicas().timeout(),
13763 );
13764 let c = caixa_aplicacao_with_politicas(Some(MeshPolicy::default()));
13765 let view = c.aplicacao_view().unwrap();
13766 assert_eq!(
13767 view.politicas(),
13768 &MeshPolicy::default(),
13769 "Caixa::aplicacao_view must fold Some(MeshPolicy::default()) \
13770 through the accessor onto MeshPolicy::default — the empty- \
13771 composite arm collapses to the same default the author- \
13772 omitted arm does (got {:?})",
13773 view.politicas(),
13774 );
13775 let c = caixa_aplicacao_with_politicas(None);
13776 let view = c.aplicacao_view().unwrap();
13777 assert_eq!(
13778 view.politicas(),
13779 &MeshPolicy::default(),
13780 "Caixa::aplicacao_view must fold None through the accessor's \
13781 unwrap_or_default onto MeshPolicy::default — the author- \
13782 omitted arm must route through the accessor's None-return \
13783 unchanged (got {:?})",
13784 view.politicas(),
13785 );
13786 }
13787
13788 #[test]
13789 fn politicas_projects_option_ref_by_borrow() {
13790 // The by-borrow pin: [`Caixa::politicas`] returns
13791 // `Option<&MeshPolicy>` by borrow — the returned reference
13792 // borrows the underlying `Option<MeshPolicy>` storage of the
13793 // `:politicas` slot and the accessor must not clone the
13794 // backing composite on every call. Peer of the sibling
13795 // `limits_projects_option_ref_by_borrow` (b2bd9d7) and
13796 // `behavior_projects_option_ref_by_borrow` (35d8b52) by-borrow
13797 // pins on the outer top-level [`Caixa`]
13798 // `Option<&Composite>`-return sub-family — extended here to
13799 // the third axis of the same sub-family: the accessor's
13800 // returned reference must borrow from `&self` (the returned
13801 // reference's lifetime is tied to `&self`), and calling the
13802 // accessor twice on the same [`Caixa`] must yield references
13803 // that are pointer-equal (the underlying byte-buffer is the
13804 // storage `MeshPolicy`'s allocation, not a fresh copy) as
13805 // well as value-equal (idempotent, no side effects on
13806 // `&self`).
13807 //
13808 // Pins against a future silent detour that returned an owned
13809 // `MeshPolicy` (which would type-check via the `Clone` impl
13810 // but silently clone on every call), a `&MeshPolicy` panic-
13811 // return on the `None` arm (which would collapse the load-
13812 // bearing `Option` presence-bit into a runtime panic), or a
13813 // one-arm-only accessor that returned a saturating composite
13814 // on some sentinel input.
13815 use crate::aplicacao::{CircuitBreaker, MeshPolicy, RateLimit};
13816 use std::time::Duration;
13817 for politicas in [
13818 Some(MeshPolicy::default()),
13819 Some(MeshPolicy {
13820 timeout: Some(Duration::from_secs(30)),
13821 retries: Some(3),
13822 circuit_breaker: Some(CircuitBreaker {
13823 max_failures: 5,
13824 window: Duration::from_secs(60),
13825 }),
13826 mtls_required: Some(true),
13827 rate_limit: Some(RateLimit {
13828 rate: 100,
13829 window: Duration::from_secs(1),
13830 }),
13831 }),
13832 ] {
13833 let c = caixa_aplicacao_with_politicas(politicas.clone());
13834 let first = c.politicas().unwrap();
13835 let second = c.politicas().unwrap();
13836 assert_eq!(
13837 first, second,
13838 "Caixa::politicas must be idempotent — two successive \
13839 calls on the same &self must return the same \
13840 &MeshPolicy",
13841 );
13842 assert!(
13843 std::ptr::eq(first, second),
13844 "Caixa::politicas must borrow the underlying \
13845 Option<MeshPolicy> storage — two successive calls \
13846 must return references with the same backing pointer \
13847 (a fresh MeshPolicy clone would change the pointer on \
13848 every call)",
13849 );
13850 assert_eq!(
13851 Some(first),
13852 politicas.as_ref(),
13853 "Caixa::politicas must return :politicas verbatim by \
13854 borrow — got {first:?}, expected {:?}",
13855 politicas.as_ref(),
13856 );
13857 }
13858 let c = caixa_aplicacao_with_politicas(None);
13859 assert!(
13860 c.politicas().is_none(),
13861 "Caixa::politicas must return None when :politicas is \
13862 absent — the author-omitted arm must project through the \
13863 accessor's Option::None unchanged",
13864 );
13865 }
13866
13867 // ── Caixa::placement — outer top-level Option<&Placement> composite-reference accessor ──
13868
13869 fn caixa_aplicacao_with_placement(placement: Option<crate::aplicacao::Placement>) -> Caixa {
13870 use crate::aplicacao::{Membro, WitContract};
13871 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
13872 c.kind = CaixaKind::Aplicacao;
13873 c.membros = vec![Membro {
13874 caixa: "a".into(),
13875 versao: "^0.1".into(),
13876 }];
13877 c.contratos = vec![WitContract {
13878 de: "a".into(),
13879 para: "a".into(),
13880 wit: "wasi:http/proxy".into(),
13881 endpoint: Some("/x".into()),
13882 subject: None,
13883 slot: None,
13884 }];
13885 c.placement = placement;
13886 c
13887 }
13888
13889 #[test]
13890 fn placement_returns_placement_option_ref_verbatim_across_permutations() {
13891 // The canonical per-`Caixa` `:placement` M3 mesh-slot outer-
13892 // composite optional-composite-reference-shape pin:
13893 // [`Caixa::placement`] must return the `:placement` typed
13894 // `Option<Placement>` verbatim as an `Option<&Placement>`
13895 // reference over the same backing storage the raw
13896 // `self.placement.as_ref()` field access borrows from,
13897 // byte-equal across every representative fixture in the
13898 // accept-set — the author-omitted `None` shape (the
13899 // "cluster-default applies" partition every downstream mesh-
13900 // artifact emitter treats as "emit no `:placement` overlay"),
13901 // the empty-composite `Some(Placement { .. default })` shape
13902 // (`estrategia: SingleNode`, empty clusters, no shard-key /
13903 // affinity — the outer presence-bit is `Some` so
13904 // [`Caixa::declared_mesh_slots`] still pushes the
13905 // `M3_AUTHOR_KEY_PLACEMENT` label), a single-axis
13906 // `Replicated`-on-two-clusters fixture (the canonical shape a
13907 // stateless HTTP Aplicacao carries), and a fully-populated
13908 // `Sharded`-with-shard-key-and-affinity fixture (the canonical
13909 // shape a stateful Akka-style cluster-sharding Aplicacao
13910 // carries).
13911 //
13912 // Pins against a future silent detour that returned a fresh-
13913 // cloned [`crate::aplicacao::Placement`] copy (which would
13914 // type-check via the `Clone` impl but silently break every
13915 // downstream caller that relied on the reference sharing the
13916 // composite's backing identity), a reference to an operator-
13917 // resolved overlay (the future per-cluster
13918 // `:placement-overrides` slot — its resolution must land at
13919 // exactly this accessor body, not silently divert the raw
13920 // slot away from the peer [`Caixa::declared_mesh_slots`]
13921 // enumerator's presence probe), a `None` →
13922 // `Some(Placement::default)` cluster-default projection (which
13923 // would collapse the load-bearing "author-omitted `:placement`
13924 // ⇒ cluster-default applies" partition the peer
13925 // [`Caixa::declared_mesh_slots`] enumerator and the peer
13926 // [`Caixa::aplicacao_view`] Aplicacao-composition seed both
13927 // read), or an axis-shuffled projection (a future detour that
13928 // swapped `clusters` and `affinity` through the accessor would
13929 // silently split the paired [`Caixa::aplicacao_view`] seed's
13930 // fold input from the sibling M3 mesh-artifact emitter's
13931 // projection input).
13932 //
13933 // Fourth outer top-level [`Caixa`] `Option<&Composite>`-return
13934 // composite-reference accessor pin on the substrate primitive
13935 // — peer of the sibling
13936 // `limits_returns_limits_option_ref_verbatim_across_permutations`
13937 // (b2bd9d7),
13938 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
13939 // (35d8b52), and
13940 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
13941 // (5d23d29) opening triad pins on the outer top-level
13942 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
13943 // here to the second of the three M3 mesh-slot axes so the
13944 // opening four-fifths of the outer `Option<&Composite>` sub-
13945 // family carries the same "byte-equal, borrow-shared,
13946 // presence-bit-preserved" outer-accessor discipline.
13947 use crate::aplicacao::{Placement, PlacementStrategy};
13948 let fixtures: Vec<Option<Placement>> = vec![
13949 None,
13950 Some(Placement::default()),
13951 Some(Placement {
13952 estrategia: PlacementStrategy::Replicated,
13953 clusters: vec!["rio".into(), "sao-paulo".into()],
13954 affinity: None,
13955 shard_key: None,
13956 }),
13957 Some(Placement {
13958 estrategia: PlacementStrategy::Sharded,
13959 clusters: vec!["rio".into(), "sao-paulo".into(), "brasilia".into()],
13960 affinity: Some("data-locality".into()),
13961 shard_key: Some("$tenantId".into()),
13962 }),
13963 ];
13964 for placement in fixtures {
13965 let c = caixa_aplicacao_with_placement(placement.clone());
13966 assert_eq!(
13967 c.placement(),
13968 placement.as_ref(),
13969 "Caixa::placement must return :placement verbatim (got \
13970 {:?}, expected {:?})",
13971 c.placement(),
13972 placement.as_ref(),
13973 );
13974 match (c.placement(), c.placement.as_ref()) {
13975 (Some(a), Some(b)) => assert!(
13976 std::ptr::eq(a, b),
13977 "Caixa::placement accessor and self.placement.as_ref() \
13978 field access must borrow the same backing storage \
13979 — the accessor is the substrate-primitive typed \
13980 dispatch every downstream Aplicacao-distribution- \
13981 overlay composite consumer must route through, and \
13982 a reference-identity split would silently break \
13983 every consumer that relied on the borrow sharing \
13984 the composite's storage",
13985 ),
13986 (None, None) => {}
13987 _ => panic!(
13988 "Caixa::placement presence bit must byte-equal \
13989 self.placement.is_some() — a presence-bit drift \
13990 would silently split the paired \
13991 Caixa::aplicacao_view Aplicacao-composition seed's \
13992 traversal head from the peer \
13993 Caixa::declared_mesh_slots M3 declared-slot \
13994 enumerator's presence probe",
13995 ),
13996 }
13997 assert_eq!(
13998 c.placement().is_some(),
13999 c.placement.is_some(),
14000 "Caixa::placement().is_some() must byte-equal \
14001 self.placement.is_some() — a presence-bit drift would \
14002 silently split every downstream Option<&Placement> \
14003 consumer's partition on the cluster-default arm",
14004 );
14005 }
14006 }
14007
14008 #[test]
14009 fn declared_mesh_slots_placement_arm_routes_through_accessor() {
14010 // Composition pin: [`Caixa::declared_mesh_slots`]'s
14011 // `:placement` presence-probe arm must key off
14012 // [`Caixa::placement`], not the raw `self.placement.is_some()`
14013 // field-probe. Structurally: a `Caixa { placement:
14014 // Some(Placement::default()), .. }` must still push
14015 // `M3_AUTHOR_KEY_PLACEMENT` onto the declared-slot list (the
14016 // presence bit is `Some`, so the M3 kind-coherence gate must
14017 // surface the slot as "declared" even when every per-axis
14018 // scalar defers to the cluster-default arm), and a `Caixa {
14019 // placement: None, .. }` must NOT push the label (the "author
14020 // omitted the slot entirely" partition). The pair jointly pins
14021 // the accessor + declared-slot enumerator composition: any
14022 // future silent detour that had the accessor collapse
14023 // `Some(Placement::default())` to `None` (a `.filter(|p|
14024 // p.clusters().is_empty().not())` projection) would silently
14025 // absorb the "declared but empty" arm at the accessor boundary
14026 // and the [`crate::LayoutError::MeshSlotsOnNonAplicacao`]
14027 // kind-coherence gate would silently accept a struct-literal
14028 // `Caixa` carrying the drift.
14029 //
14030 // Peer of the sibling
14031 // `declared_servico_slots_limits_arm_routes_through_accessor`
14032 // (b2bd9d7),
14033 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14034 // (35d8b52), and
14035 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14036 // (5d23d29) composition pins on the sibling `:limits` /
14037 // `:behavior` / `:politicas` outer-`Option<&Composite>` arms
14038 // — same "the enumerator gate must route through the
14039 // substrate-primitive typed dispatch" discipline extended onto
14040 // the second of the three M3 mesh-slot axes so the
14041 // [`Caixa::declared_mesh_slots`] enumerator carries the same
14042 // routing invariant on the `:placement` arm as the peer
14043 // `:politicas` arm.
14044 use crate::aplicacao::Placement;
14045 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14046 let slots = c.declared_mesh_slots();
14047 assert!(
14048 slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14049 "declared_mesh_slots must push M3_AUTHOR_KEY_PLACEMENT \
14050 when `:placement` is Some (even for Placement::default()) \
14051 — the accessor and the enumerator gate must route through \
14052 the same substrate-primitive typed dispatch on the outer \
14053 :placement presence bit (got slots={slots:?})",
14054 );
14055 let c = caixa_aplicacao_with_placement(None);
14056 let slots = c.declared_mesh_slots();
14057 assert!(
14058 !slots.contains(&crate::render::M3_AUTHOR_KEY_PLACEMENT),
14059 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_PLACEMENT \
14060 when `:placement` is None — the author-omitted arm must \
14061 route through the accessor's None-return unchanged (got \
14062 slots={slots:?})",
14063 );
14064 }
14065
14066 #[test]
14067 fn aplicacao_view_placement_arm_folds_through_accessor() {
14068 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:placement`
14069 // Aplicacao-composition seed must fold through
14070 // [`Caixa::placement`], not the raw
14071 // `self.placement.clone().unwrap_or_default()` field-borrow.
14072 // Structurally: a `Caixa { placement: Some(Placement {
14073 // estrategia: Replicated, clusters: ["rio"], .. default }),
14074 // kind: Aplicacao, .. }` must surface a projected
14075 // [`crate::AplicacaoSpec`] whose `placement().estrategia()` +
14076 // `placement().clusters()` byte-equal the outer composite's
14077 // authored values (the fold must project the authored
14078 // composite verbatim), a `Caixa { placement:
14079 // Some(Placement::default()), kind: Aplicacao, .. }` must
14080 // surface an [`crate::AplicacaoSpec`] whose `placement()`
14081 // byte-equals [`crate::aplicacao::Placement::default`] (the
14082 // fold's empty-composite arm collapses to the same default
14083 // the author-omitted arm does), and a `Caixa { placement:
14084 // None, kind: Aplicacao, .. }` must surface an
14085 // [`crate::AplicacaoSpec`] whose `placement()` byte-equals
14086 // [`crate::aplicacao::Placement::default`] (the "author
14087 // omitted the slot entirely" arm folds through the
14088 // `unwrap_or_default` onto the cluster-default). The triad
14089 // jointly pins the accessor + Aplicacao-composition seed
14090 // composition: any future silent detour that had the accessor
14091 // divert the raw slot away from the seed's fold (an operator-
14092 // resolved overlay's default-fold arm silently differing from
14093 // the raw slot's default-fold arm) would silently split the
14094 // build-time distribution-artifact emission gate from the
14095 // caixa-mesh renderer's Aplicacao-view input at the
14096 // composition boundary.
14097 use crate::aplicacao::{Placement, PlacementStrategy};
14098 let c = caixa_aplicacao_with_placement(Some(Placement {
14099 estrategia: PlacementStrategy::Replicated,
14100 clusters: vec!["rio".into()],
14101 affinity: None,
14102 shard_key: None,
14103 }));
14104 let view = c.aplicacao_view().unwrap();
14105 assert_eq!(
14106 view.placement().estrategia(),
14107 PlacementStrategy::Replicated,
14108 "Caixa::aplicacao_view must fold the authored :placement \
14109 :estrategia scalar through the accessor verbatim onto the \
14110 projected AplicacaoSpec — a future silent detour at the \
14111 seed's fold arm would surface here as a projected-scalar \
14112 drift (got {:?})",
14113 view.placement().estrategia(),
14114 );
14115 assert_eq!(
14116 view.placement().clusters(),
14117 &["rio"],
14118 "Caixa::aplicacao_view must fold the authored :placement \
14119 :clusters list through the accessor verbatim onto the \
14120 projected AplicacaoSpec — a future silent detour at the \
14121 seed's fold arm would surface here as a projected-list \
14122 drift (got {:?})",
14123 view.placement().clusters(),
14124 );
14125 let c = caixa_aplicacao_with_placement(Some(Placement::default()));
14126 let view = c.aplicacao_view().unwrap();
14127 assert_eq!(
14128 view.placement(),
14129 &Placement::default(),
14130 "Caixa::aplicacao_view must fold Some(Placement::default()) \
14131 through the accessor onto Placement::default — the empty- \
14132 composite arm collapses to the same default the author- \
14133 omitted arm does (got {:?})",
14134 view.placement(),
14135 );
14136 let c = caixa_aplicacao_with_placement(None);
14137 let view = c.aplicacao_view().unwrap();
14138 assert_eq!(
14139 view.placement(),
14140 &Placement::default(),
14141 "Caixa::aplicacao_view must fold None through the accessor's \
14142 unwrap_or_default onto Placement::default — the author- \
14143 omitted arm must route through the accessor's None-return \
14144 unchanged (got {:?})",
14145 view.placement(),
14146 );
14147 }
14148
14149 #[test]
14150 fn placement_projects_option_ref_by_borrow() {
14151 // The by-borrow pin: [`Caixa::placement`] returns
14152 // `Option<&Placement>` by borrow — the returned reference
14153 // borrows the underlying `Option<Placement>` storage of the
14154 // `:placement` slot and the accessor must not clone the
14155 // backing composite on every call. Peer of the sibling
14156 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14157 // `behavior_projects_option_ref_by_borrow` (35d8b52), and
14158 // `politicas_projects_option_ref_by_borrow` (5d23d29) by-borrow
14159 // pins on the outer top-level [`Caixa`]
14160 // `Option<&Composite>`-return sub-family — extended here to
14161 // the fourth axis of the same sub-family: the accessor's
14162 // returned reference must borrow from `&self` (the returned
14163 // reference's lifetime is tied to `&self`), and calling the
14164 // accessor twice on the same [`Caixa`] must yield references
14165 // that are pointer-equal (the underlying byte-buffer is the
14166 // storage `Placement`'s allocation, not a fresh copy) as well
14167 // as value-equal (idempotent, no side effects on `&self`).
14168 //
14169 // Pins against a future silent detour that returned an owned
14170 // `Placement` (which would type-check via the `Clone` impl
14171 // but silently clone on every call), a `&Placement` panic-
14172 // return on the `None` arm (which would collapse the load-
14173 // bearing `Option` presence-bit into a runtime panic), or a
14174 // one-arm-only accessor that returned a saturating composite
14175 // on some sentinel input.
14176 use crate::aplicacao::{Placement, PlacementStrategy};
14177 for placement in [
14178 Some(Placement::default()),
14179 Some(Placement {
14180 estrategia: PlacementStrategy::Sharded,
14181 clusters: vec!["rio".into(), "sao-paulo".into()],
14182 affinity: Some("data-locality".into()),
14183 shard_key: Some("$tenantId".into()),
14184 }),
14185 ] {
14186 let c = caixa_aplicacao_with_placement(placement.clone());
14187 let first = c.placement().unwrap();
14188 let second = c.placement().unwrap();
14189 assert_eq!(
14190 first, second,
14191 "Caixa::placement must be idempotent — two successive \
14192 calls on the same &self must return the same \
14193 &Placement",
14194 );
14195 assert!(
14196 std::ptr::eq(first, second),
14197 "Caixa::placement must borrow the underlying \
14198 Option<Placement> storage — two successive calls \
14199 must return references with the same backing pointer \
14200 (a fresh Placement clone would change the pointer on \
14201 every call)",
14202 );
14203 assert_eq!(
14204 Some(first),
14205 placement.as_ref(),
14206 "Caixa::placement must return :placement verbatim by \
14207 borrow — got {first:?}, expected {:?}",
14208 placement.as_ref(),
14209 );
14210 }
14211 let c = caixa_aplicacao_with_placement(None);
14212 assert!(
14213 c.placement().is_none(),
14214 "Caixa::placement must return None when :placement is \
14215 absent — the author-omitted arm must project through the \
14216 accessor's Option::None unchanged",
14217 );
14218 }
14219
14220 // ── Caixa::entrada — outer top-level Option<&Entrada> composite-reference accessor ──
14221
14222 fn caixa_aplicacao_with_entrada(entrada: Option<crate::aplicacao::Entrada>) -> Caixa {
14223 use crate::aplicacao::{Membro, WitContract};
14224 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14225 c.kind = CaixaKind::Aplicacao;
14226 c.membros = vec![Membro {
14227 caixa: "a".into(),
14228 versao: "^0.1".into(),
14229 }];
14230 c.contratos = vec![WitContract {
14231 de: "a".into(),
14232 para: "a".into(),
14233 wit: "wasi:http/proxy".into(),
14234 endpoint: Some("/x".into()),
14235 subject: None,
14236 slot: None,
14237 }];
14238 c.entrada = entrada;
14239 c
14240 }
14241
14242 #[test]
14243 fn entrada_returns_entrada_option_ref_verbatim_across_permutations() {
14244 // The canonical per-`Caixa` `:entrada` M3 mesh-slot outer-
14245 // composite optional-composite-reference-shape pin:
14246 // [`Caixa::entrada`] must return the `:entrada` typed
14247 // `Option<Entrada>` verbatim as an `Option<&Entrada>`
14248 // reference over the same backing storage the raw
14249 // `self.entrada.as_ref()` field access borrows from,
14250 // byte-equal across every representative fixture in the
14251 // accept-set — the author-omitted `None` shape (the
14252 // "cluster-internal Aplicacao" partition every downstream
14253 // Gateway-API emitter treats as "emit no listener + no
14254 // HTTPRoute"), a bare-`host`/`para` minimum-composite fixture
14255 // (empty `paths` — the resolved-paths fallback the peer
14256 // [`crate::aplicacao::Entrada::resolved_paths`] cascade folds
14257 // onto the substrate catch-all), and a fully-populated
14258 // multi-path-with-non-default-port fixture (the canonical
14259 // shape a public HTTP Aplicacao carries).
14260 //
14261 // Pins against a future silent detour that returned a fresh-
14262 // cloned [`crate::aplicacao::Entrada`] copy (which would
14263 // type-check via the `Clone` impl but silently break every
14264 // downstream caller that relied on the reference sharing the
14265 // composite's backing identity), a reference to an operator-
14266 // resolved overlay (the future per-cluster
14267 // `:entrada-overrides` slot — its resolution must land at
14268 // exactly this accessor body, not silently divert the raw
14269 // slot away from the peer [`Caixa::declared_mesh_slots`]
14270 // enumerator's presence probe), or an axis-shuffled projection
14271 // (a future detour that swapped `host` and `para` through the
14272 // accessor would silently split the paired
14273 // [`Caixa::aplicacao_view`] seed's forward input from the
14274 // sibling M3 gateway-artifact emitter's projection input).
14275 //
14276 // Fifth and final outer top-level [`Caixa`]
14277 // `Option<&Composite>`-return composite-reference accessor pin
14278 // on the substrate primitive — peer of the sibling
14279 // `limits_returns_limits_option_ref_verbatim_across_permutations`
14280 // (b2bd9d7),
14281 // `behavior_returns_behavior_option_ref_verbatim_across_permutations`
14282 // (35d8b52),
14283 // `politicas_returns_politicas_option_ref_verbatim_across_permutations`
14284 // (5d23d29), and
14285 // `placement_returns_placement_option_ref_verbatim_across_permutations`
14286 // (4fb8074) opening tetrad pins on the outer top-level
14287 // [`Caixa`] `Option<&Composite>`-return sub-family — extended
14288 // here to the third and final M3 mesh-slot axis so the closed
14289 // outer `Option<&Composite>` sub-family carries the same
14290 // "byte-equal, borrow-shared, presence-bit-preserved" outer-
14291 // accessor discipline across all five arms.
14292 use crate::aplicacao::Entrada;
14293 let fixtures: Vec<Option<Entrada>> = vec![
14294 None,
14295 Some(Entrada {
14296 host: "checkout.quero.cloud".into(),
14297 para: "gateway".into(),
14298 paths: Vec::new(),
14299 port: crate::DEFAULT_SERVICO_PORT,
14300 }),
14301 Some(Entrada {
14302 host: "api.pleme.io".into(),
14303 para: "public-api".into(),
14304 paths: vec!["/v1".into(), "/v2".into()],
14305 port: 8080,
14306 }),
14307 ];
14308 for entrada in fixtures {
14309 let c = caixa_aplicacao_with_entrada(entrada.clone());
14310 assert_eq!(
14311 c.entrada(),
14312 entrada.as_ref(),
14313 "Caixa::entrada must return :entrada verbatim (got \
14314 {:?}, expected {:?})",
14315 c.entrada(),
14316 entrada.as_ref(),
14317 );
14318 match (c.entrada(), c.entrada.as_ref()) {
14319 (Some(a), Some(b)) => assert!(
14320 std::ptr::eq(a, b),
14321 "Caixa::entrada accessor and self.entrada.as_ref() \
14322 field access must borrow the same backing storage \
14323 — the accessor is the substrate-primitive typed \
14324 dispatch every downstream Aplicacao-external- \
14325 gateway composite consumer must route through, and \
14326 a reference-identity split would silently break \
14327 every consumer that relied on the borrow sharing \
14328 the composite's storage",
14329 ),
14330 (None, None) => {}
14331 _ => panic!(
14332 "Caixa::entrada presence bit must byte-equal \
14333 self.entrada.is_some() — a presence-bit drift \
14334 would silently split the paired \
14335 Caixa::aplicacao_view Aplicacao-composition seed's \
14336 traversal head from the peer \
14337 Caixa::declared_mesh_slots M3 declared-slot \
14338 enumerator's presence probe",
14339 ),
14340 }
14341 assert_eq!(
14342 c.entrada().is_some(),
14343 c.entrada.is_some(),
14344 "Caixa::entrada().is_some() must byte-equal \
14345 self.entrada.is_some() — a presence-bit drift would \
14346 silently split every downstream Option<&Entrada> \
14347 consumer's partition on the cluster-internal arm",
14348 );
14349 }
14350 }
14351
14352 #[test]
14353 fn declared_mesh_slots_entrada_arm_routes_through_accessor() {
14354 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:entrada`
14355 // presence-probe arm must key off [`Caixa::entrada`], not the
14356 // raw `self.entrada.is_some()` field-probe. Structurally: a
14357 // `Caixa { entrada: Some(Entrada { host: "...", para: "...",
14358 // paths: [], port: DEFAULT_SERVICO_PORT }), .. }` must push
14359 // `M3_AUTHOR_KEY_ENTRADA` onto the declared-slot list (the
14360 // presence bit is `Some`, so the M3 kind-coherence gate must
14361 // surface the slot as "declared" even when every per-axis
14362 // scalar defers to the substrate catch-all / default port),
14363 // and a `Caixa { entrada: None, .. }` must NOT push the label
14364 // (the "author omitted the slot entirely" partition). The pair
14365 // jointly pins the accessor + declared-slot enumerator
14366 // composition: any future silent detour that had the accessor
14367 // collapse `Some(Entrada { paths: [], .. })` to `None` (a
14368 // `.filter(|e| !e.paths.is_empty())` projection) would silently
14369 // absorb the "declared but empty-paths" arm at the accessor
14370 // boundary and the
14371 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
14372 // coherence gate would silently accept a struct-literal
14373 // `Caixa` carrying the drift.
14374 //
14375 // Peer of the sibling
14376 // `declared_servico_slots_limits_arm_routes_through_accessor`
14377 // (b2bd9d7),
14378 // `declared_servico_slots_behavior_arm_routes_through_accessor`
14379 // (35d8b52),
14380 // `declared_mesh_slots_politicas_arm_routes_through_accessor`
14381 // (5d23d29), and
14382 // `declared_mesh_slots_placement_arm_routes_through_accessor`
14383 // (4fb8074) composition pins on the sibling `:limits` /
14384 // `:behavior` / `:politicas` / `:placement` outer-
14385 // `Option<&Composite>` arms — same "the enumerator gate must
14386 // route through the substrate-primitive typed dispatch"
14387 // discipline extended onto the third and final M3 mesh-slot
14388 // axis so the [`Caixa::declared_mesh_slots`] enumerator now
14389 // carries the routing invariant on every M3 mesh-slot arm.
14390 use crate::aplicacao::Entrada;
14391 let c = caixa_aplicacao_with_entrada(Some(Entrada {
14392 host: "checkout.quero.cloud".into(),
14393 para: "gateway".into(),
14394 paths: Vec::new(),
14395 port: crate::DEFAULT_SERVICO_PORT,
14396 }));
14397 let slots = c.declared_mesh_slots();
14398 assert!(
14399 slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14400 "declared_mesh_slots must push M3_AUTHOR_KEY_ENTRADA when \
14401 `:entrada` is Some (even for empty-paths / default-port) \
14402 — the accessor and the enumerator gate must route through \
14403 the same substrate-primitive typed dispatch on the outer \
14404 :entrada presence bit (got slots={slots:?})",
14405 );
14406 let c = caixa_aplicacao_with_entrada(None);
14407 let slots = c.declared_mesh_slots();
14408 assert!(
14409 !slots.contains(&crate::render::M3_AUTHOR_KEY_ENTRADA),
14410 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_ENTRADA \
14411 when `:entrada` is None — the author-omitted arm must \
14412 route through the accessor's None-return unchanged (got \
14413 slots={slots:?})",
14414 );
14415 }
14416
14417 #[test]
14418 fn aplicacao_view_entrada_arm_folds_through_accessor() {
14419 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:entrada`
14420 // Aplicacao-composition seed must fold through
14421 // [`Caixa::entrada`], not the raw `self.entrada.clone()` field-
14422 // borrow. Structurally: a `Caixa { entrada: Some(Entrada {
14423 // host: "api.pleme.io", para: "public-api", paths: ["/v1"],
14424 // port: 8080 }), kind: Aplicacao, .. }` must surface a projected
14425 // [`crate::AplicacaoSpec`] whose `entrada().unwrap()` byte-
14426 // equals the outer composite's authored value (the fold must
14427 // project the authored composite verbatim), and a `Caixa {
14428 // entrada: None, kind: Aplicacao, .. }` must surface an
14429 // [`crate::AplicacaoSpec`] whose `entrada()` is `None` (the
14430 // "author omitted the slot entirely" arm folds through the
14431 // accessor's `Option::cloned` onto the same `None` presence
14432 // bit — unlike the peer `:politicas` / `:placement` arms
14433 // `:entrada` has no cluster-default fold, the omitted arm
14434 // stays omitted). The pair jointly pins the accessor +
14435 // Aplicacao-composition seed composition: any future silent
14436 // detour that had the accessor divert the raw slot away from
14437 // the seed's fold (an operator-resolved overlay's forward arm
14438 // silently differing from the raw slot's forward arm) would
14439 // silently split the build-time gateway-artifact emission gate
14440 // from the caixa-mesh renderer's Aplicacao-view input at the
14441 // composition boundary.
14442 use crate::aplicacao::Entrada;
14443 let authored = Entrada {
14444 host: "api.pleme.io".into(),
14445 para: "public-api".into(),
14446 paths: vec!["/v1".into()],
14447 port: 8080,
14448 };
14449 let c = caixa_aplicacao_with_entrada(Some(authored.clone()));
14450 let view = c.aplicacao_view().unwrap();
14451 assert_eq!(
14452 view.entrada(),
14453 Some(&authored),
14454 "Caixa::aplicacao_view must fold the authored :entrada \
14455 composite through the accessor verbatim onto the \
14456 projected AplicacaoSpec — a future silent detour at the \
14457 seed's fold arm would surface here as a projected- \
14458 composite drift (got {:?})",
14459 view.entrada(),
14460 );
14461 let c = caixa_aplicacao_with_entrada(None);
14462 let view = c.aplicacao_view().unwrap();
14463 assert!(
14464 view.entrada().is_none(),
14465 "Caixa::aplicacao_view must fold None through the \
14466 accessor's Option::cloned onto None — the author- \
14467 omitted arm must route through the accessor's None-return \
14468 unchanged (got {:?})",
14469 view.entrada(),
14470 );
14471 }
14472
14473 #[test]
14474 fn entrada_projects_option_ref_by_borrow() {
14475 // The by-borrow pin: [`Caixa::entrada`] returns
14476 // `Option<&Entrada>` by borrow — the returned reference
14477 // borrows the underlying `Option<Entrada>` storage of the
14478 // `:entrada` slot and the accessor must not clone the backing
14479 // composite on every call. Peer of the sibling
14480 // `limits_projects_option_ref_by_borrow` (b2bd9d7),
14481 // `behavior_projects_option_ref_by_borrow` (35d8b52),
14482 // `politicas_projects_option_ref_by_borrow` (5d23d29), and
14483 // `placement_projects_option_ref_by_borrow` (4fb8074) by-
14484 // borrow pins on the outer top-level [`Caixa`]
14485 // `Option<&Composite>`-return sub-family — extended here to
14486 // the fifth and final axis of the same sub-family, closing
14487 // the discipline: the accessor's returned reference must
14488 // borrow from `&self` (the returned reference's lifetime is
14489 // tied to `&self`), and calling the accessor twice on the
14490 // same [`Caixa`] must yield references that are pointer-equal
14491 // (the underlying byte-buffer is the storage `Entrada`'s
14492 // allocation, not a fresh copy) as well as value-equal
14493 // (idempotent, no side effects on `&self`).
14494 //
14495 // Pins against a future silent detour that returned an owned
14496 // `Entrada` (which would type-check via the `Clone` impl but
14497 // silently clone on every call), a `&Entrada` panic-return on
14498 // the `None` arm (which would collapse the load-bearing
14499 // `Option` presence-bit into a runtime panic), or a one-arm-
14500 // only accessor that returned a saturating composite on some
14501 // sentinel input.
14502 use crate::aplicacao::Entrada;
14503 for entrada in [
14504 Some(Entrada {
14505 host: "checkout.quero.cloud".into(),
14506 para: "gateway".into(),
14507 paths: Vec::new(),
14508 port: crate::DEFAULT_SERVICO_PORT,
14509 }),
14510 Some(Entrada {
14511 host: "api.pleme.io".into(),
14512 para: "public-api".into(),
14513 paths: vec!["/v1".into(), "/v2".into()],
14514 port: 8080,
14515 }),
14516 ] {
14517 let c = caixa_aplicacao_with_entrada(entrada.clone());
14518 let first = c.entrada().unwrap();
14519 let second = c.entrada().unwrap();
14520 assert_eq!(
14521 first, second,
14522 "Caixa::entrada must be idempotent — two successive \
14523 calls on the same &self must return the same &Entrada",
14524 );
14525 assert!(
14526 std::ptr::eq(first, second),
14527 "Caixa::entrada must borrow the underlying \
14528 Option<Entrada> storage — two successive calls must \
14529 return references with the same backing pointer (a \
14530 fresh Entrada clone would change the pointer on every \
14531 call)",
14532 );
14533 assert_eq!(
14534 Some(first),
14535 entrada.as_ref(),
14536 "Caixa::entrada must return :entrada verbatim by \
14537 borrow — got {first:?}, expected {:?}",
14538 entrada.as_ref(),
14539 );
14540 }
14541 let c = caixa_aplicacao_with_entrada(None);
14542 assert!(
14543 c.entrada().is_none(),
14544 "Caixa::entrada must return None when :entrada is absent \
14545 — the author-omitted arm must project through the \
14546 accessor's Option::None unchanged",
14547 );
14548 }
14549
14550 // ── Caixa::estrategia — outer top-level Option<RestartStrategy> flat-spread supervisor-tree accessor ──
14551
14552 fn caixa_with_estrategia(estrategia: Option<crate::supervisor::RestartStrategy>) -> Caixa {
14553 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14554 c.estrategia = estrategia;
14555 c
14556 }
14557
14558 #[test]
14559 fn estrategia_returns_estrategia_option_verbatim_across_permutations() {
14560 // The canonical per-`Caixa` `:estrategia` M2 supervisor-tree-slot
14561 // flat-spread `Option<RestartStrategy>`-return `Copy`-composite-
14562 // enum-arm scalar shape pin: [`Caixa::estrategia`] must return
14563 // the `:estrategia` typed `Option<crate::supervisor::RestartStrategy>`
14564 // verbatim as an `Option<RestartStrategy>` `Copy`-projected value
14565 // over the same discriminant the raw `self.estrategia` field
14566 // access carries, byte-equal across every representative fixture
14567 // in the accept-set — the author-omitted `None` shape (the
14568 // "defer to [`RestartStrategy::default`] through the
14569 // [`Self::supervisor_view`] `unwrap_or_default()` fold" partition
14570 // every non-`Supervisor`-kind `defcaixa` carries by
14571 // `#[serde(default)]`), and each of the four closed-set variants
14572 // [`RestartStrategy::OneForOne`] / [`RestartStrategy::OneForAll`]
14573 // / [`RestartStrategy::RestForOne`] /
14574 // [`RestartStrategy::SimpleOneForOne`] the author-declared arm
14575 // partitions on.
14576 //
14577 // Pins against a future silent detour that re-derived the
14578 // strategy from a peer axis (an accidental fallback to
14579 // `if children.is_empty() { SimpleOneForOne } else { OneForOne }`
14580 // collapse that read the outer `:children` list-length axis into
14581 // the strategy discriminator at the accessor boundary), a
14582 // stale-derive detour that substituted [`RestartStrategy::default`]
14583 // when the outer `Option` held `None` (which would silently
14584 // collapse the load-bearing "author explicitly declared
14585 // `:estrategia OneForOne`" vs "author omitted the slot and
14586 // inherited the default" partition the [`Self::declared_supervisor_slots`]
14587 // presence-probe reads — the enumerator gate would still push
14588 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` on the omitted arm, silently
14589 // splitting the paired [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
14590 // kind-coherence gate's traversal head from the
14591 // [`Self::supervisor_view`] `unwrap_or_default()` fold's
14592 // composition head), a reference to an operator-resolved overlay
14593 // (the future per-cluster `:estrategia-overrides` slot — its
14594 // resolution must land at exactly this accessor body, not
14595 // silently divert the raw slot away from a second consumer), or
14596 // an axis-remap projection (a future detour that mapped
14597 // `OneForAll` through the accessor onto `OneForOne` would
14598 // silently split every downstream sibling-restart-strategy
14599 // consumer's per-arm fan-out).
14600 //
14601 // First outer top-level [`Caixa`] `Option<Copy>`-return
14602 // supervisor-tree-slot flat-spread accessor pin on the substrate
14603 // primitive — opens the outer-`Caixa` `Option<Copy>` flat-spread
14604 // projection pattern the sibling per-`Caixa` `:max-restarts` /
14605 // `:restart-window` future outer-scalar pins fold on. Peer of
14606 // the inner-altitude
14607 // `supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14608 // (eafb619) pin on the post-composition [`SupervisorSpec`]
14609 // altitude — same "the substrate-primitive accessor must byte-
14610 // equal the raw field access verbatim across every author-
14611 // declared value" discipline extended onto the pre-composition
14612 // outer author-surface [`Caixa`] altitude. Peer of the closed
14613 // outer-`Caixa` `Option<&Composite>` composite-reference family
14614 // the sibling `limits` / `behavior` / `politicas` / `placement` /
14615 // `entrada`
14616 // `..._returns_..._option_ref_verbatim_across_permutations` pins
14617 // already carry on the outer `Option<&Composite>` altitude.
14618 use crate::supervisor::RestartStrategy;
14619 let fixtures: Vec<Option<RestartStrategy>> = vec![
14620 None,
14621 Some(RestartStrategy::OneForOne),
14622 Some(RestartStrategy::OneForAll),
14623 Some(RestartStrategy::RestForOne),
14624 Some(RestartStrategy::SimpleOneForOne),
14625 ];
14626 for estrategia in fixtures {
14627 let c = caixa_with_estrategia(estrategia);
14628 assert_eq!(
14629 c.estrategia(),
14630 estrategia,
14631 "Caixa::estrategia must return :estrategia verbatim (got \
14632 {:?}, expected {:?})",
14633 c.estrategia(),
14634 estrategia,
14635 );
14636 assert_eq!(
14637 c.estrategia(),
14638 c.estrategia,
14639 "Caixa::estrategia accessor and self.estrategia field \
14640 access must byte-equal — the accessor is the substrate-\
14641 primitive typed dispatch every downstream supervisor-\
14642 tree flat-spread consumer must route through, and a \
14643 discriminant split would silently break every consumer \
14644 that relied on the accessor sharing the field's own \
14645 Option<Copy> shape",
14646 );
14647 assert_eq!(
14648 c.estrategia().is_some(),
14649 c.estrategia.is_some(),
14650 "Caixa::estrategia().is_some() must byte-equal \
14651 self.estrategia.is_some() — a presence-bit drift would \
14652 silently split the paired Caixa::declared_supervisor_slots \
14653 presence-probe arm from the Caixa::supervisor_view \
14654 unwrap_or_default() fold's composition input",
14655 );
14656 }
14657 }
14658
14659 #[test]
14660 fn declared_supervisor_slots_estrategia_arm_routes_through_accessor() {
14661 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
14662 // `:estrategia` presence-probe arm must key off
14663 // [`Caixa::estrategia`], not the raw `self.estrategia.is_some()`
14664 // field-probe. Structurally: every `Caixa { estrategia:
14665 // Some(RestartStrategy::_), .. }` variant must push
14666 // `SUPERVISOR_AUTHOR_KEY_ESTRATEGIA` onto the declared-slot list
14667 // (the presence bit is `Some` for every closed-set variant, so
14668 // the M2 supervisor-tree kind-coherence gate must surface the
14669 // slot as "declared" regardless of which variant the author
14670 // picked), and a `Caixa { estrategia: None, .. }` must NOT push
14671 // the label (the "author omitted the slot entirely, deferring
14672 // to [`RestartStrategy::default`] through the supervisor_view
14673 // fold" partition). The pair jointly pins the accessor +
14674 // declared-slot enumerator composition: any future silent detour
14675 // that had the accessor collapse `Some(RestartStrategy::default())`
14676 // to `None` (a `.filter(|e| *e != RestartStrategy::default())`
14677 // projection) would silently absorb the "declared but default-
14678 // valued" arm at the accessor boundary and the
14679 // [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`] kind-
14680 // coherence gate would silently accept a struct-literal `Caixa`
14681 // carrying the drift.
14682 //
14683 // Peer of the sibling per-`Caixa`
14684 // `declared_servico_slots_limits_arm_routes_through_accessor`
14685 // (b2bd9d7) accessor-composition pin on the sibling outer-`Caixa`
14686 // `Option<&LimitsSpec>` composition axis — same "the enumerator
14687 // gate must route through the substrate-primitive typed
14688 // dispatch" discipline extended onto the flat-spread M2
14689 // supervisor-tree `Option<RestartStrategy>`-composition surface,
14690 // opening the outer-`Caixa` supervisor-tree-slot arm of the
14691 // composition-pin family.
14692 use crate::supervisor::RestartStrategy;
14693 for estrategia in [
14694 RestartStrategy::OneForOne,
14695 RestartStrategy::OneForAll,
14696 RestartStrategy::RestForOne,
14697 RestartStrategy::SimpleOneForOne,
14698 ] {
14699 let c = caixa_with_estrategia(Some(estrategia));
14700 let slots = c.declared_supervisor_slots();
14701 assert!(
14702 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14703 "declared_supervisor_slots must push \
14704 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is \
14705 Some({estrategia:?}) — the accessor and the enumerator \
14706 gate must route through the same substrate-primitive \
14707 typed dispatch on the outer :estrategia presence bit \
14708 (got slots={slots:?})",
14709 );
14710 }
14711 let c = caixa_with_estrategia(None);
14712 let slots = c.declared_supervisor_slots();
14713 assert!(
14714 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_ESTRATEGIA),
14715 "declared_supervisor_slots must NOT push \
14716 SUPERVISOR_AUTHOR_KEY_ESTRATEGIA when `:estrategia` is None \
14717 — the author-omitted arm must route through the accessor's \
14718 None-return unchanged (got slots={slots:?})",
14719 );
14720 }
14721
14722 #[test]
14723 fn supervisor_view_estrategia_arm_routes_through_accessor() {
14724 // Composition pin: [`Caixa::supervisor_view`]'s per-`:estrategia`
14725 // [`SupervisorSpec`] construction arm must key off
14726 // [`Caixa::estrategia`]'s `unwrap_or_default()` fold, not the raw
14727 // `self.estrategia.unwrap_or_default()` field-fold. Structurally:
14728 // for every `:kind Supervisor` `Caixa` carrying an author-
14729 // declared `Some(RestartStrategy::_)` variant, the composed
14730 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal the
14731 // outer accessor's declared variant unchanged; and for a
14732 // `:kind Supervisor` `Caixa` carrying `None`, the composed
14733 // [`SupervisorSpec`]'s `.estrategia` field must byte-equal
14734 // [`RestartStrategy::default`] (the [`RestartStrategy::OneForOne`]
14735 // arm the flat-spread `unwrap_or_default()` fold projects to on
14736 // the author-omitted arm — this is the *composition* between the
14737 // outer `Option<RestartStrategy>` accessor's presence-bit
14738 // surface and the inner post-composition non-`Option`
14739 // [`SupervisorSpec::estrategia`] altitude). The pair jointly
14740 // pins the accessor + supervisor_view composition: any future
14741 // silent detour that had the accessor promote `None` to
14742 // `Some(RestartStrategy::default())` (a `.or_else(|| Some(RestartStrategy::default()))`
14743 // projection) would silently collapse the two arms into one at
14744 // the accessor boundary and the [`Self::declared_supervisor_slots`]
14745 // presence probe would silently drift from the composition site.
14746 //
14747 // Peer of the sibling M2 supervisor-slot post-composition
14748 // `validate_reads_through_lifted_estrategia_accessor` (eafb619)
14749 // pin on the [`SupervisorSpec::validate`] altitude — this pin
14750 // extends that inner-altitude accessor-routing discipline onto
14751 // the pre-composition outer author-surface [`Caixa`] altitude,
14752 // pinning the composition edge between the flat-spread outer
14753 // `Option<RestartStrategy>` and the composed [`SupervisorSpec`]
14754 // `RestartStrategy` axes.
14755 use crate::CaixaKind;
14756 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
14757 for estrategia in [
14758 RestartStrategy::OneForOne,
14759 RestartStrategy::OneForAll,
14760 RestartStrategy::RestForOne,
14761 RestartStrategy::SimpleOneForOne,
14762 ] {
14763 let mut c = caixa_with_estrategia(Some(estrategia));
14764 c.kind = CaixaKind::Supervisor;
14765 // Route the `SimpleOneForOne ↔ non-SimpleOneForOne` fixture-
14766 // shape partition through the [`gen_platform::IsVariant`]
14767 // derive-generated
14768 // [`RestartStrategy::is_simple_one_for_one`] predicate rather
14769 // than the raw `matches!(estrategia, RestartStrategy::
14770 // SimpleOneForOne)` open-coded pattern-match — same closed-
14771 // set-typed-enum arm-discriminator dispatch discipline the
14772 // sibling [`crate::upgrade::UpgradeInstruction::is_restart`]
14773 // convergence (915a934) extended onto its two paired positive
14774 // / negated `matches!` sites and the peer
14775 // [`crate::aplicacao::PlacementStrategy`] `IsVariant`-derived
14776 // predicate convergence (766ec63) extended onto the M3 mesh-
14777 // slot per-`:placement` distribution-strategy discriminator
14778 // axis. See the sibling `supervisor::tests::
14779 // round_trip_all_strategies` and
14780 // `supervisor::tests::supervisor_spec_estrategia_returns_estrategia_verbatim_across_permutations`
14781 // fixtures — the three sites (all test-only,
14782 // acknowledged in 915a934's Prior-commits footnote as the
14783 // outstanding follow-up) now consult one typed dispatch on
14784 // the substrate primitive.
14785 c.children = if estrategia.is_simple_one_for_one() {
14786 Vec::new()
14787 } else {
14788 vec![ChildSpec {
14789 caixa: "worker".into(),
14790 versao: "^0.1".into(),
14791 restart: RestartPolicy::Permanent,
14792 }]
14793 };
14794 let view = c.supervisor_view().expect(
14795 "supervisor_view must materialize a SupervisorSpec for a \
14796 :kind Supervisor Caixa carrying a Some(:estrategia) slot",
14797 );
14798 assert_eq!(
14799 view.estrategia(),
14800 c.estrategia().unwrap(),
14801 "supervisor_view must carry the outer Caixa::estrategia() \
14802 declared variant onto the composed SupervisorSpec.estrategia \
14803 field verbatim on the Some arm (got {:?}, expected {:?})",
14804 view.estrategia(),
14805 c.estrategia().unwrap(),
14806 );
14807 }
14808 // The author-omitted arm: outer `None` → composed
14809 // `RestartStrategy::default()` through the flat-spread
14810 // `unwrap_or_default()` fold.
14811 let mut c = caixa_with_estrategia(None);
14812 c.kind = CaixaKind::Supervisor;
14813 // Populate children so the sibling supervisor slots are coherent
14814 // for the [`Self::supervisor_view`] projection; the `:estrategia`
14815 // arm still defers to [`RestartStrategy::default`] on the
14816 // author-omitted arm even when the sibling slots carry values.
14817 c.children = vec![ChildSpec {
14818 caixa: "worker".into(),
14819 versao: "^0.1".into(),
14820 restart: RestartPolicy::Permanent,
14821 }];
14822 let view = c.supervisor_view().expect(
14823 "supervisor_view must materialize a SupervisorSpec for a \
14824 :kind Supervisor Caixa carrying a None `:estrategia` slot",
14825 );
14826 assert_eq!(
14827 view.estrategia(),
14828 RestartStrategy::default(),
14829 "supervisor_view must project the outer Caixa::estrategia() \
14830 None arm onto RestartStrategy::default() through the flat-\
14831 spread unwrap_or_default() fold (got {:?}, expected {:?})",
14832 view.estrategia(),
14833 RestartStrategy::default(),
14834 );
14835 assert!(
14836 c.estrategia().is_none(),
14837 "Caixa::estrategia() must remain None on the author-omitted \
14838 arm — the supervisor_view fold must not mutate the outer \
14839 flat-spread presence bit",
14840 );
14841 }
14842
14843 #[test]
14844 fn estrategia_projects_option_by_copy() {
14845 // The by-`Copy` pin: [`Caixa::estrategia`] returns
14846 // `Option<RestartStrategy>` by value (`RestartStrategy: Copy`) —
14847 // the accessor does not borrow `&self` past the call (no
14848 // lifetime on the return type), and calling the accessor twice
14849 // on the same [`Caixa`] must yield discriminant-equal values
14850 // (idempotent, no side effects on `&self`). Peer of the sibling
14851 // outer-`Caixa` `Option<&Composite>` by-borrow
14852 // `limits_projects_option_ref_by_borrow` (b2bd9d7) /
14853 // `behavior_projects_option_ref_by_borrow` (35d8b52) /
14854 // `politicas_projects_option_ref_by_borrow` (5d23d29) /
14855 // `placement_projects_option_ref_by_borrow` (4fb8074) /
14856 // `entrada_projects_option_ref_by_borrow` (e4128e4) by-borrow
14857 // pins on the outer-`Caixa` `Option<&Composite>`-return axes —
14858 // extended here to the outer-`Caixa` `Option<Copy>`-return
14859 // flat-spread axis. The `Copy` discipline replaces the pointer-
14860 // equality claim the by-borrow siblings pin (a fresh `Copy` of a
14861 // `Copy` discriminant is definitionally the same discriminant, so
14862 // the axis reduces to discriminant equality).
14863 //
14864 // Pins against a future silent detour that returned a fresh
14865 // `Option<&RestartStrategy>` (which would type-check but silently
14866 // introduce a borrow of `&self` past the call, collapsing the
14867 // load-bearing "no lifetime on the return type" `Copy` projection
14868 // the flat-spread axis's `Option<Copy>` shape carries), a stale-
14869 // read side effect that flipped the outer discriminant on
14870 // successive calls, or an axis-remap projection that returned a
14871 // different variant than the field storage.
14872 use crate::supervisor::RestartStrategy;
14873 for estrategia in [
14874 Some(RestartStrategy::OneForOne),
14875 Some(RestartStrategy::OneForAll),
14876 Some(RestartStrategy::RestForOne),
14877 Some(RestartStrategy::SimpleOneForOne),
14878 ] {
14879 let c = caixa_with_estrategia(estrategia);
14880 let first = c.estrategia();
14881 let second = c.estrategia();
14882 assert_eq!(
14883 first, second,
14884 "Caixa::estrategia must be idempotent — two successive \
14885 calls on the same &self must return the same \
14886 Option<RestartStrategy>",
14887 );
14888 assert_eq!(
14889 first, estrategia,
14890 "Caixa::estrategia must return :estrategia verbatim by \
14891 Copy — got {first:?}, expected {estrategia:?}",
14892 );
14893 }
14894 let c = caixa_with_estrategia(None);
14895 assert!(
14896 c.estrategia().is_none(),
14897 "Caixa::estrategia must return None when :estrategia is \
14898 absent — the author-omitted arm must project through the \
14899 accessor's Option::None unchanged",
14900 );
14901 }
14902
14903 // ── Caixa::max_restarts / Caixa::restart_window —
14904 // outer top-level M2 supervisor-tree-slot flat-spread accessors
14905 // (Option<u32> / Option<&str>) folding on the ed04d3c
14906 // Caixa::estrategia Option<Copy> sub-family ─────────────────────
14907
14908 fn caixa_with_max_restarts(max_restarts: Option<u32>) -> Caixa {
14909 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
14910 c.max_restarts = max_restarts;
14911 c
14912 }
14913
14914 fn caixa_supervisor_with_max_restarts_and_window(
14915 max_restarts: Option<u32>,
14916 restart_window: Option<&str>,
14917 ) -> Caixa {
14918 use crate::CaixaKind;
14919 use crate::supervisor::{ChildSpec, RestartPolicy};
14920 let mut c = Caixa::from_lisp(&Caixa::template("root")).unwrap();
14921 c.kind = CaixaKind::Supervisor;
14922 c.max_restarts = max_restarts;
14923 c.restart_window = restart_window.map(str::to_string);
14924 c.children = vec![ChildSpec {
14925 caixa: "worker".into(),
14926 versao: "^0.1".into(),
14927 restart: RestartPolicy::Permanent,
14928 }];
14929 c
14930 }
14931
14932 #[test]
14933 fn max_restarts_returns_max_restarts_option_verbatim_across_permutations() {
14934 // Value-shape pin: [`Caixa::max_restarts`] returns the
14935 // `:max-restarts` typed `Option<u32>` verbatim, `Copy`-projected
14936 // from the typed slot's own storage, byte-equal across the
14937 // author-omitted `None` arm (the "defer to the
14938 // [`Self::supervisor_view`] `unwrap_or(5)` OTP-canonical
14939 // `{intensity, 5, 60}` default" partition every
14940 // non-`Supervisor`-kind caixa carries by `#[serde(default)]`)
14941 // and each of the representative fixtures in the accept-set —
14942 // `0` (the zero-floor arm the peer
14943 // [`crate::supervisor::SupervisorSpec::validate`]
14944 // [`crate::SupervisorError::ZeroMaxRestarts`] gate refuses on
14945 // the post-composition altitude — the accessor must ship the
14946 // raw slot verbatim so struct-literal fixtures continue to
14947 // expose the zero at the accessor boundary), the OTP-canonical
14948 // `5` default (`{intensity, 5, 60}` worker-supervisor from
14949 // Learn You Some Erlang), `1000` (the
14950 // [`SUPERVISOR_MAX_RESTARTS_MAX`] cap the peer post-composition
14951 // upper-bound gate accepts on the boundary), `u32::MAX` (a
14952 // past-the-cap sentinel that the substrate-primitive accessor
14953 // must still ship verbatim). Second outer top-level
14954 // [`Caixa`] `Option<Copy>`-return supervisor-tree flat-spread
14955 // pin — folds on the sibling
14956 // `estrategia_returns_estrategia_option_verbatim_across_permutations`
14957 // (ed04d3c) pin's `Option<Copy>` shape, extending the sub-family
14958 // onto the sibling `Option<u32>` restart-budget-count arm.
14959 let fixtures: Vec<Option<u32>> = vec![None, Some(0), Some(5), Some(1000), Some(u32::MAX)];
14960 for max_restarts in fixtures {
14961 let c = caixa_with_max_restarts(max_restarts);
14962 assert_eq!(
14963 c.max_restarts(),
14964 max_restarts,
14965 "Caixa::max_restarts must return :max-restarts verbatim \
14966 (got {:?}, expected {max_restarts:?})",
14967 c.max_restarts(),
14968 );
14969 assert_eq!(
14970 c.max_restarts(),
14971 c.max_restarts,
14972 "Caixa::max_restarts accessor and self.max_restarts \
14973 field access must byte-equal — a presence-bit or count \
14974 drift would silently split the paired \
14975 Caixa::declared_supervisor_slots presence-probe arm \
14976 from the Caixa::supervisor_view unwrap_or(5) fold's \
14977 composition input",
14978 );
14979 }
14980 }
14981
14982 #[test]
14983 fn max_restarts_projects_option_by_copy() {
14984 // The by-`Copy` pin: [`Caixa::max_restarts`] returns
14985 // `Option<u32>` by value (`u32: Copy`) — the accessor does not
14986 // borrow `&self` past the call (no lifetime on the return type),
14987 // and calling the accessor twice on the same [`Caixa`] must
14988 // yield equal values (idempotent, no side effects). Peer of the
14989 // sibling `estrategia_projects_option_by_copy` (ed04d3c) pin on
14990 // the outer-`Caixa` `Option<Copy>`-return flat-spread axis.
14991 for max_restarts in [Some(0u32), Some(5), Some(1000), Some(u32::MAX), None] {
14992 let c = caixa_with_max_restarts(max_restarts);
14993 let first = c.max_restarts();
14994 let second = c.max_restarts();
14995 assert_eq!(
14996 first, second,
14997 "Caixa::max_restarts must be idempotent — two successive \
14998 calls on the same &self must return the same Option<u32>",
14999 );
15000 assert_eq!(
15001 first, max_restarts,
15002 "Caixa::max_restarts must return :max-restarts verbatim \
15003 by Copy — got {first:?}, expected {max_restarts:?}",
15004 );
15005 }
15006 }
15007
15008 #[test]
15009 fn declared_supervisor_slots_max_restarts_arm_routes_through_accessor() {
15010 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15011 // `:max-restarts` presence-probe arm must key off
15012 // [`Caixa::max_restarts`], not the raw
15013 // `self.max_restarts.is_some()` field-probe. Structurally: every
15014 // `Caixa { max_restarts: Some(_), .. }` variant must push
15015 // `SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS` onto the declared-slot
15016 // list (the presence bit is `Some` for every representative
15017 // count, so the M2 kind-coherence gate must surface the slot as
15018 // "declared"), and a `Caixa { max_restarts: None, .. }` must
15019 // NOT push the label. Peer of the sibling
15020 // `declared_supervisor_slots_estrategia_arm_routes_through_accessor`
15021 // (ed04d3c) composition pin — same routing-through-accessor
15022 // discipline extended onto the sibling flat-spread `Option<u32>`
15023 // arm.
15024 for max_restarts in [0u32, 5, 1000, u32::MAX] {
15025 let c = caixa_with_max_restarts(Some(max_restarts));
15026 let slots = c.declared_supervisor_slots();
15027 assert!(
15028 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15029 "declared_supervisor_slots must push \
15030 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` \
15031 is Some({max_restarts}) — the accessor and the \
15032 enumerator gate must route through the same \
15033 substrate-primitive typed dispatch on the outer \
15034 :max-restarts presence bit (got slots={slots:?})",
15035 );
15036 }
15037 let c = caixa_with_max_restarts(None);
15038 let slots = c.declared_supervisor_slots();
15039 assert!(
15040 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS),
15041 "declared_supervisor_slots must NOT push \
15042 SUPERVISOR_AUTHOR_KEY_MAX_RESTARTS when `:max-restarts` is \
15043 None — the author-omitted arm must route through the \
15044 accessor's None-return unchanged (got slots={slots:?})",
15045 );
15046 }
15047
15048 #[test]
15049 fn supervisor_view_max_restarts_arm_routes_through_accessor() {
15050 // Composition pin: [`Caixa::supervisor_view`]'s per-`:max-restarts`
15051 // [`SupervisorSpec`] construction arm must key off
15052 // [`Caixa::max_restarts`]'s `unwrap_or(5)` fold, not the raw
15053 // `self.max_restarts.unwrap_or(5)` field-fold. Structurally: for
15054 // every `:kind Supervisor` `Caixa` carrying an author-declared
15055 // `Some(n)`, the composed [`SupervisorSpec`]'s `.max_restarts()`
15056 // must byte-equal `n`; and for a `:kind Supervisor` `Caixa`
15057 // carrying `None`, the composed [`SupervisorSpec`]'s
15058 // `.max_restarts()` must byte-equal the OTP-canonical `5`. Peer
15059 // of the sibling
15060 // `supervisor_view_estrategia_arm_routes_through_accessor`
15061 // (ed04d3c) composition pin.
15062 for max_restarts in [1u32, 5, 1000] {
15063 let c = caixa_supervisor_with_max_restarts_and_window(Some(max_restarts), None);
15064 let view = c.supervisor_view().expect(
15065 "supervisor_view must materialize a SupervisorSpec for a \
15066 :kind Supervisor Caixa carrying a Some(:max-restarts)",
15067 );
15068 assert_eq!(
15069 view.max_restarts(),
15070 max_restarts,
15071 "supervisor_view must carry the outer \
15072 Caixa::max_restarts() Some arm onto the composed \
15073 SupervisorSpec.max_restarts field verbatim (got {}, \
15074 expected {max_restarts})",
15075 view.max_restarts(),
15076 );
15077 }
15078 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15079 let view = c.supervisor_view().expect(
15080 "supervisor_view must materialize a SupervisorSpec for a \
15081 :kind Supervisor Caixa carrying a None :max-restarts",
15082 );
15083 assert_eq!(
15084 view.max_restarts(),
15085 5,
15086 "supervisor_view must project the outer \
15087 Caixa::max_restarts() None arm onto the OTP-canonical \
15088 {{intensity, 5, 60}} default (5) through the flat-spread \
15089 unwrap_or(5) fold (got {})",
15090 view.max_restarts(),
15091 );
15092 assert!(
15093 c.max_restarts().is_none(),
15094 "Caixa::max_restarts() must remain None on the author-\
15095 omitted arm — the supervisor_view fold must not mutate \
15096 the outer flat-spread presence bit",
15097 );
15098 }
15099
15100 #[test]
15101 fn restart_window_returns_restart_window_option_verbatim_across_permutations() {
15102 // Value-shape pin: [`Caixa::restart_window`] returns the
15103 // `:restart-window` typed `Option<String>` verbatim as an
15104 // `Option<&str>`, borrowed from the typed slot's own storage,
15105 // byte-equal across the author-omitted `None` arm and each of
15106 // the representative fixtures in the accept-set — the canonical
15107 // `"60s"` from `{intensity, 5, 60}`, the sibling
15108 // canonical-magnitude forms (`"5m"` / `"1h"` / `"500ms"` / `"30"`
15109 // / `"0s"`) the shared codec's positive-set sweep pin covers,
15110 // plus a past-the-guard sentinel (`"1.5s"` — the fractional-
15111 // seconds drift the sibling [`Self::validate_restart_window`]
15112 // gate refuses; the accessor must ship the raw slot verbatim
15113 // so struct-literal fixtures continue to expose the drift at
15114 // the accessor boundary). Third outer top-level [`Caixa`]
15115 // supervisor-tree flat-spread pin — extends the sub-family onto
15116 // the sibling `Option<&str>` raw-duration-string arm.
15117 for window in [
15118 None,
15119 Some("60s"),
15120 Some("5m"),
15121 Some("1h"),
15122 Some("500ms"),
15123 Some("1.5s"),
15124 Some(""),
15125 ] {
15126 let c = caixa_with_restart_window(window);
15127 assert_eq!(
15128 c.restart_window(),
15129 window,
15130 "Caixa::restart_window must return :restart-window \
15131 verbatim as Option<&str> (got {:?}, expected {window:?})",
15132 c.restart_window(),
15133 );
15134 assert_eq!(
15135 c.restart_window(),
15136 c.restart_window.as_deref(),
15137 "Caixa::restart_window accessor and \
15138 self.restart_window.as_deref() field access must \
15139 byte-equal — a byte-level drift would silently split \
15140 the paired Caixa::declared_supervisor_slots \
15141 presence-probe arm from the \
15142 Caixa::validate_restart_window shared-codec gate and \
15143 the Caixa::supervisor_view soft-swallowing fold",
15144 );
15145 }
15146 }
15147
15148 #[test]
15149 fn restart_window_projects_slice_by_borrow() {
15150 // The by-borrow pin: [`Caixa::restart_window`] returns
15151 // `Option<&str>` by borrow — the returned string slice borrows
15152 // the underlying `Option<String>` storage of the `:restart-window`
15153 // slot and the accessor must not clone on every call. Peer of
15154 // the sibling outer top-level [`Caixa`] `Option<&str>`-return
15155 // by-borrow pins on the universal-axis scalar family
15156 // (`licenca_projects_option_ref_by_borrow` /
15157 // `descricao_projects_option_ref_by_borrow` and siblings) —
15158 // extended onto the M2 supervisor-tree flat-spread
15159 // `Option<&str>` raw-duration-string axis.
15160 for window in [None, Some("60s"), Some("5m"), Some("")] {
15161 let c = caixa_with_restart_window(window);
15162 let first = c.restart_window();
15163 let second = c.restart_window();
15164 assert_eq!(
15165 first, second,
15166 "Caixa::restart_window must be idempotent — two \
15167 successive calls on the same &self must return the \
15168 same Option<&str>",
15169 );
15170 if let (Some(a), Some(b)) = (first, second) {
15171 assert_eq!(
15172 a.as_ptr(),
15173 b.as_ptr(),
15174 "Caixa::restart_window must borrow the underlying \
15175 String storage — two successive Some-arm calls must \
15176 return slices with the same backing pointer (a fresh \
15177 String clone would change the pointer on every call)",
15178 );
15179 }
15180 assert_eq!(
15181 first, window,
15182 "Caixa::restart_window must return :restart-window \
15183 verbatim by borrow — got {first:?}, expected {window:?}",
15184 );
15185 }
15186 }
15187
15188 #[test]
15189 fn declared_supervisor_slots_restart_window_arm_routes_through_accessor() {
15190 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15191 // `:restart-window` presence-probe arm must key off
15192 // [`Caixa::restart_window`], not the raw
15193 // `self.restart_window.is_some()` field-probe. Structurally:
15194 // every `Caixa { restart_window: Some(_), .. }` must push
15195 // `SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW` onto the declared-slot
15196 // list, and a `Caixa { restart_window: None, .. }` must NOT
15197 // push the label. Peer of the sibling
15198 // `declared_supervisor_slots_max_restarts_arm_routes_through_accessor`
15199 // routing pin.
15200 for window in ["60s", "5m", "1h", "500ms", "1.5s", ""] {
15201 let c = caixa_with_restart_window(Some(window));
15202 let slots = c.declared_supervisor_slots();
15203 assert!(
15204 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15205 "declared_supervisor_slots must push \
15206 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when \
15207 `:restart-window` is Some({window:?}) — the accessor \
15208 and the enumerator gate must route through the same \
15209 substrate-primitive typed dispatch on the outer \
15210 :restart-window presence bit (got slots={slots:?})",
15211 );
15212 }
15213 let c = caixa_with_restart_window(None);
15214 let slots = c.declared_supervisor_slots();
15215 assert!(
15216 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW),
15217 "declared_supervisor_slots must NOT push \
15218 SUPERVISOR_AUTHOR_KEY_RESTART_WINDOW when `:restart-window` \
15219 is None — the author-omitted arm must route through the \
15220 accessor's None-return unchanged (got slots={slots:?})",
15221 );
15222 }
15223
15224 #[test]
15225 fn validate_restart_window_arm_routes_through_accessor() {
15226 // Composition pin: [`Caixa::validate_restart_window`]'s
15227 // shared-codec fold arm must key off [`Caixa::restart_window`],
15228 // not the raw `self.restart_window.as_deref()` field-projection.
15229 // Structurally: (1) `None` → `Ok(())` (the "omit the slot to
15230 // express no reset" canonical shape); (2) a canonical `Some`
15231 // arm (`"60s"`) → `Ok(())`; (3) a codec-rejected `Some` arm
15232 // (`"1.5s"`) → `Err(RestartWindowMalformed { restart_window,
15233 // .. })` carrying the offending raw string verbatim. The three
15234 // arms jointly pin that the validator's raw-string binding is
15235 // the accessor's return, not a peer projection — any future
15236 // silent detour that had the accessor collapse `Some("")` to
15237 // `None` would silently absorb the empty-after-trim refusal
15238 // case at the accessor boundary.
15239 caixa_with_restart_window(None)
15240 .validate_restart_window()
15241 .expect("None :restart-window must validate through the accessor");
15242 caixa_with_restart_window(Some("60s"))
15243 .validate_restart_window()
15244 .expect("canonical :restart-window \"60s\" must validate through the accessor");
15245 let err = caixa_with_restart_window(Some("1.5s"))
15246 .validate_restart_window()
15247 .expect_err("fractional-seconds :restart-window must fail through the accessor");
15248 assert!(
15249 matches!(
15250 err,
15251 ManifestError::RestartWindowMalformed { ref restart_window, .. }
15252 if restart_window == "1.5s"
15253 ),
15254 "validator must carry the offending raw string verbatim \
15255 from the accessor's borrowed &str (got {err:?})",
15256 );
15257 }
15258
15259 #[test]
15260 fn supervisor_view_restart_window_arm_routes_through_accessor() {
15261 // Composition pin: [`Caixa::supervisor_view`]'s
15262 // per-`:restart-window` [`SupervisorSpec`] construction arm
15263 // must key off [`Caixa::restart_window`]'s soft-swallowing
15264 // `.and_then(|s| duration_codec::parse(s).ok())` fold, not the
15265 // raw `self.restart_window.as_deref().and_then(…)` field-fold.
15266 // Structurally: (1) `None` → `SupervisorSpec.restart_window ==
15267 // None` (the "never reset" sentinel); (2) canonical `Some("60s")`
15268 // → `SupervisorSpec.restart_window == Some(Duration::from_secs(60))`
15269 // (the shared codec's canonical parse); (3) codec-rejected
15270 // `Some("1.5s")` → `SupervisorSpec.restart_window == None`
15271 // (the soft-swallow preserving the view's best-effort shape).
15272 let c = caixa_supervisor_with_max_restarts_and_window(None, None);
15273 let view = c.supervisor_view().expect("Supervisor kind has a view");
15274 assert_eq!(
15275 view.restart_window(),
15276 None,
15277 "supervisor_view must project outer None :restart-window \
15278 onto None on the composed SupervisorSpec (never-reset \
15279 sentinel) through the accessor's None-return unchanged",
15280 );
15281
15282 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("60s"));
15283 let view = c.supervisor_view().expect("Supervisor kind has a view");
15284 assert_eq!(
15285 view.restart_window(),
15286 Some(std::time::Duration::from_secs(60)),
15287 "supervisor_view must fold outer Some(\"60s\") through the \
15288 shared duration_codec into Duration::from_secs(60) on the \
15289 composed SupervisorSpec (accessor's Some(&str) → codec \
15290 parse → Some(Duration))",
15291 );
15292
15293 let c = caixa_supervisor_with_max_restarts_and_window(None, Some("1.5s"));
15294 let view = c.supervisor_view().expect("Supervisor kind has a view");
15295 assert_eq!(
15296 view.restart_window(),
15297 None,
15298 "supervisor_view must soft-swallow the shared-codec parse \
15299 failure to None (the view's best-effort shape the sibling \
15300 manifest-level validate_restart_window surfaces as \
15301 RestartWindowMalformed); the accessor's raw-string return \
15302 is the single input every downstream consumer keys off",
15303 );
15304 }
15305
15306 // ── Caixa::upgrade_from — outer top-level &[UpgradeFromEntry] composite-slice accessor ──
15307
15308 fn caixa_with_upgrade_from(upgrade_from: Vec<crate::upgrade::UpgradeFromEntry>) -> Caixa {
15309 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15310 c.upgrade_from = upgrade_from;
15311 c
15312 }
15313
15314 #[test]
15315 fn upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations() {
15316 // The canonical per-`Caixa` `:upgrade-from` M2 typed-slot
15317 // outer-composite `&[UpgradeFromEntry]`-return slice-shape
15318 // pin: [`Caixa::upgrade_from`] must return the `:upgrade-from`
15319 // typed `Vec<UpgradeFromEntry>` verbatim as a
15320 // `&[UpgradeFromEntry]` slice-view over the same backing
15321 // buffer the raw `self.upgrade_from.as_slice()` field access
15322 // borrows from, element-equal across every representative
15323 // fixture in the accept-set — `[]` (the "no hot-upgrade path
15324 // declared" arm every `defcaixa` without an `:upgrade-from`
15325 // block carries; `#[serde(default)]` folds an omitted slot
15326 // onto `Vec::new()`), a canonical single-entry `Restart`
15327 // fixture (the shape most Servicos carry — a single prior
15328 // version with the fallback strategy), a canonical multi-
15329 // entry list carrying every typed instruction variant
15330 // (`LoadModule` / `StateChange` / `SoftPurge` / `Purge` /
15331 // `Restart`), and a past-the-guard sentinel — a duplicate-
15332 // `:from` `[(0.1.0, Restart), (0.1.0, Restart)]` entry pair
15333 // ([`crate::upgrade::validate_upgrade_from`] rejects through
15334 // `DuplicateFrom { from: "0.1.0" }` but the accessor must
15335 // ship the raw slot verbatim so struct-literal fixtures
15336 // continue to expose the duplicate at the accessor boundary).
15337 //
15338 // Pins against a future silent detour that returned an owned
15339 // `Vec<UpgradeFromEntry>` (which would type-check but silently
15340 // clone on every accessor call, breaking the zero-cost
15341 // projection every peer sibling slice accessor carries), a
15342 // `[dup, dup] → [dup]` dedup collapse (which would silently
15343 // absorb the `DuplicateFrom` refusal case at the accessor
15344 // boundary and the [`crate::StandardLayout::verify`] cross-
15345 // entry gate would silently accept a struct-literal `Caixa`
15346 // carrying the drift), a reference to an operator-resolved
15347 // overlay (the future per-cluster `:upgrade-overrides` slot
15348 // — its resolution must land at exactly this accessor body,
15349 // not silently divert the raw slot away from a second
15350 // consumer), or an axis-shuffled projection (a future detour
15351 // that reordered entries through the accessor would silently
15352 // split the paired [`crate::StandardLayout::verify`] per-
15353 // `:upgrade-from` shape gate's traversal input from the peer
15354 // [`crate::render::servico_m2_overlay`] emitter's projection
15355 // input, since the operator's hot-upgrade dispatch matches
15356 // per-`:from` and axis reordering would silently split the
15357 // per-entry script-path existence probe's iteration order
15358 // from the M2 overlay emitter's serialized-entry order).
15359 //
15360 // First outer top-level [`Caixa`] `&[Composite]`-return
15361 // slice accessor pin on the substrate primitive for M2 / M3
15362 // typed-slot vec-carry axes — opens the outer-`Caixa`
15363 // `&[Composite]` composite-slice projection pattern the
15364 // sibling `:children` [`crate::supervisor::ChildSpec`] /
15365 // `:membros` [`crate::aplicacao::Membro`] / `:contratos`
15366 // [`crate::aplicacao::WitContract`] future outer-composite-
15367 // slice pins fold on. Peer of the closed outer-`Caixa`
15368 // scalar `Option<&Composite>` composite-reference family the
15369 // sibling `limits` / `behavior` / `politicas` / `placement`
15370 // / `entrada` `..._returns_..._option_ref_verbatim_across_
15371 // permutations` pins closed (b2bd9d7 → e4128e4) — extends
15372 // the "byte-equal, borrow-shared" outer-accessor discipline
15373 // onto the outer-`Caixa` `&[Composite]` vec-carry altitude.
15374 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15375 let fixtures: Vec<Vec<UpgradeFromEntry>> = vec![
15376 vec![],
15377 vec![UpgradeFromEntry {
15378 from: "0.0.1".into(),
15379 instructions: vec![UpgradeInstruction::Restart],
15380 }],
15381 vec![
15382 UpgradeFromEntry {
15383 from: "0.0.1".into(),
15384 instructions: vec![
15385 UpgradeInstruction::LoadModule {
15386 module: "demo".into(),
15387 },
15388 UpgradeInstruction::SoftPurge {
15389 module: "demo".into(),
15390 },
15391 ],
15392 },
15393 UpgradeFromEntry {
15394 from: "0.0.2".into(),
15395 instructions: vec![
15396 UpgradeInstruction::StateChange {
15397 script: "servicos/upgrade.lisp".into(),
15398 },
15399 UpgradeInstruction::Purge {
15400 module: "demo".into(),
15401 },
15402 UpgradeInstruction::Restart,
15403 ],
15404 },
15405 ],
15406 vec![
15407 UpgradeFromEntry {
15408 from: "0.1.0".into(),
15409 instructions: vec![UpgradeInstruction::Restart],
15410 },
15411 UpgradeFromEntry {
15412 from: "0.1.0".into(),
15413 instructions: vec![UpgradeInstruction::Restart],
15414 },
15415 ],
15416 ];
15417 for upgrade_from in fixtures {
15418 let c = caixa_with_upgrade_from(upgrade_from.clone());
15419 assert_eq!(
15420 c.upgrade_from(),
15421 upgrade_from.as_slice(),
15422 "Caixa::upgrade_from must return :upgrade-from \
15423 verbatim (got {:?}, expected {upgrade_from:?})",
15424 c.upgrade_from(),
15425 );
15426 assert_eq!(
15427 c.upgrade_from(),
15428 c.upgrade_from.as_slice(),
15429 "Caixa::upgrade_from must element-equal the raw \
15430 `self.upgrade_from.as_slice()` field access across \
15431 every value in the Vec<UpgradeFromEntry> accept-set",
15432 );
15433 assert_eq!(
15434 c.upgrade_from().is_empty(),
15435 c.upgrade_from.is_empty(),
15436 "Caixa::upgrade_from().is_empty() must byte-equal \
15437 self.upgrade_from.is_empty() — a presence-bit drift \
15438 would silently split the paired \
15439 Caixa::declared_servico_slots M2 declared-slot \
15440 enumerator's presence probe from the peer \
15441 crate::render::servico_m2_overlay M2 overlay \
15442 emitter's presence gate",
15443 );
15444 }
15445 }
15446
15447 #[test]
15448 fn declared_servico_slots_upgrade_from_arm_routes_through_accessor() {
15449 // Composition pin: [`Caixa::declared_servico_slots`]'s
15450 // `:upgrade-from` presence-probe arm must key off
15451 // [`Caixa::upgrade_from`], not the raw
15452 // `self.upgrade_from.is_empty()` field-probe. Structurally: a
15453 // `Caixa { upgrade_from: vec![UpgradeFromEntry { from: "0.0.1",
15454 // instructions: vec![Restart] }], .. }` must push
15455 // `M2_AUTHOR_KEY_UPGRADE_FROM` onto the declared-slot list
15456 // (the presence bit is non-empty, so the M2 kind-coherence
15457 // gate must surface the slot as "declared"), and a `Caixa {
15458 // upgrade_from: vec![], .. }` must NOT push the label (the
15459 // "author omitted the slot entirely" arm — the empty-slice
15460 // partition the serde-default folds onto). The pair jointly
15461 // pins the accessor + declared-slot enumerator composition:
15462 // any future silent detour that had the accessor collapse
15463 // `[Restart]` to `[]` (a `.filter(|e| !e.instructions.
15464 // is_empty())` projection) would silently absorb the
15465 // "declared but degenerate" arm at the accessor boundary and
15466 // the [`crate::LayoutError::ServicoSlotsOnNonServico`] kind-
15467 // coherence gate would silently accept a struct-literal
15468 // `Caixa` carrying the drift.
15469 //
15470 // Peer of the sibling
15471 // `declared_servico_slots_limits_arm_routes_through_accessor`
15472 // (b2bd9d7) and
15473 // `declared_servico_slots_behavior_arm_routes_through_accessor`
15474 // (35d8b52) composition pins on the sibling `:limits` /
15475 // `:behavior` outer-`Option<&Composite>` arms — same "the
15476 // enumerator gate must route through the substrate-primitive
15477 // typed dispatch" discipline extended onto the third M2
15478 // Servico-runtime slot axis, closing the enumerator's routing
15479 // invariant on every M2 arm.
15480 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15481 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15482 from: "0.0.1".into(),
15483 instructions: vec![UpgradeInstruction::Restart],
15484 }]);
15485 let slots = c.declared_servico_slots();
15486 assert!(
15487 slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15488 "declared_servico_slots must push \
15489 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15490 non-empty — the accessor and the enumerator gate must \
15491 route through the same substrate-primitive typed \
15492 dispatch on the outer :upgrade-from presence bit (got \
15493 slots={slots:?})",
15494 );
15495 let c = caixa_with_upgrade_from(vec![]);
15496 let slots = c.declared_servico_slots();
15497 assert!(
15498 !slots.contains(&crate::render::M2_AUTHOR_KEY_UPGRADE_FROM),
15499 "declared_servico_slots must NOT push \
15500 M2_AUTHOR_KEY_UPGRADE_FROM when `:upgrade-from` is \
15501 empty — the author-omitted arm must route through the \
15502 accessor's empty-slice return unchanged (got \
15503 slots={slots:?})",
15504 );
15505 }
15506
15507 #[test]
15508 fn servico_m2_overlay_upgrade_from_arm_routes_through_accessor() {
15509 // Composition pin: [`crate::render::servico_m2_overlay`]'s
15510 // per-`:upgrade-from` M2 overlay emit arm must key off
15511 // [`Caixa::upgrade_from`], not the raw
15512 // `!caixa.upgrade_from.is_empty()` presence gate + the
15513 // `serde_yaml::to_value(&caixa.upgrade_from)` projection.
15514 // Structurally: a `Caixa { upgrade_from: vec![UpgradeFromEntry
15515 // { from: "0.0.1", instructions: vec![Restart] }], .. }` must
15516 // surface the `M2_KEY_UPGRADE_FROM` key with a per-entry
15517 // sequence in the overlay (the emitter fans onto the serde
15518 // slice-serialization), and a `Caixa { upgrade_from: vec![],
15519 // .. }` must omit the key entirely (the empty-slice
15520 // partition — the `!.is_empty()` outer gate elides the key
15521 // when the author omitted the slot). The pair jointly pins
15522 // the accessor + M2 overlay emitter composition: any future
15523 // silent detour that had the accessor return a fresh-cloned
15524 // `Vec<UpgradeFromEntry>` copy would silently break the
15525 // reference-identity pin the peer per-entry
15526 // `serde_yaml::to_value(caixa.upgrade_from())` projection
15527 // reads from — the projection would clone once per accessor
15528 // call instead of borrowing the storage buffer verbatim.
15529 //
15530 // Peer of the sibling
15531 // `servico_m2_overlay_limits_arm_routes_through_accessor`
15532 // (b2bd9d7) and
15533 // `servico_m2_overlay_behavior_arm_routes_through_accessor`
15534 // (35d8b52) composition pins on the sibling `:limits` /
15535 // `:behavior` outer-`Option<&Composite>` arms — same "the
15536 // M2 overlay emitter must route through the substrate-
15537 // primitive typed dispatch" discipline extended onto the
15538 // third M2 Servico-runtime slot axis, closing the overlay
15539 // emitter's routing invariant on every M2 arm.
15540 use crate::render::{M2_KEY_UPGRADE_FROM, servico_m2_overlay};
15541 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15542 let c = caixa_with_upgrade_from(vec![UpgradeFromEntry {
15543 from: "0.0.1".into(),
15544 instructions: vec![UpgradeInstruction::Restart],
15545 }]);
15546 let overlay = servico_m2_overlay(&c).unwrap();
15547 assert!(
15548 overlay.contains_key(M2_KEY_UPGRADE_FROM),
15549 "servico_m2_overlay must surface M2_KEY_UPGRADE_FROM when \
15550 `:upgrade-from` is non-empty — the accessor and the M2 \
15551 overlay emitter must route through the same substrate- \
15552 primitive typed dispatch on the outer :upgrade-from \
15553 slice (got overlay={overlay:?})",
15554 );
15555 let c = caixa_with_upgrade_from(vec![]);
15556 let overlay = servico_m2_overlay(&c).unwrap();
15557 assert!(
15558 !overlay.contains_key(M2_KEY_UPGRADE_FROM),
15559 "servico_m2_overlay must omit M2_KEY_UPGRADE_FROM when \
15560 `:upgrade-from` is empty — the empty-slice partition \
15561 must route through the accessor's empty-slice return \
15562 unchanged (got overlay={overlay:?})",
15563 );
15564 }
15565
15566 #[test]
15567 fn upgrade_from_projects_slice_by_borrow() {
15568 // The by-borrow pin: [`Caixa::upgrade_from`] returns
15569 // `&[UpgradeFromEntry]` by borrow — the returned slice
15570 // borrows the underlying `Vec<UpgradeFromEntry>` storage of
15571 // the `:upgrade-from` slot and the accessor must not clone
15572 // the backing `Vec` on every call. Peer of the sibling
15573 // outer top-level [`Caixa`] `&[T]`-return by-borrow pins
15574 // (`autores_projects_slice_by_borrow` b5d813f,
15575 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
15576 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15577 // `exe_projects_slice_by_borrow` 65d9527,
15578 // `servicos_projects_slice_by_borrow` 611f78b,
15579 // `deps_projects_slice_by_borrow` ad34b4e,
15580 // `deps_dev_projects_slice_by_borrow` f7fd81e) on the
15581 // sibling outer top-level [`Caixa`] scalar-element `&[T]`
15582 // axes — extended here to the first outer-`Caixa`
15583 // composite-element `&[Composite]` axis: the accessor's
15584 // returned slice must borrow from `&self` (the returned
15585 // reference's lifetime is tied to `&self`), and calling the
15586 // accessor twice on the same [`Caixa`] must yield slices
15587 // that are pointer-equal (the underlying byte-buffer is the
15588 // storage `Vec`'s allocation, not a fresh copy) as well as
15589 // value-equal (idempotent, no side effects on `&self`).
15590 //
15591 // Pins against a future silent detour that returned an owned
15592 // `Vec<UpgradeFromEntry>` (which would type-check but
15593 // silently clone on every call), a `&Vec<UpgradeFromEntry>`
15594 // return (which would leak the backing `Vec`'s
15595 // grow/push/reserve surface no downstream consumer reaches
15596 // for), or a one-arm-only accessor that returned a
15597 // saturating value on some sentinel input.
15598 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
15599 for upgrade_from in [
15600 vec![],
15601 vec![UpgradeFromEntry {
15602 from: "0.0.1".into(),
15603 instructions: vec![UpgradeInstruction::Restart],
15604 }],
15605 vec![
15606 UpgradeFromEntry {
15607 from: "0.0.1".into(),
15608 instructions: vec![UpgradeInstruction::Restart],
15609 },
15610 UpgradeFromEntry {
15611 from: "0.0.2".into(),
15612 instructions: vec![UpgradeInstruction::SoftPurge {
15613 module: "demo".into(),
15614 }],
15615 },
15616 ],
15617 ] {
15618 let c = caixa_with_upgrade_from(upgrade_from.clone());
15619 let first = c.upgrade_from();
15620 let second = c.upgrade_from();
15621 assert_eq!(
15622 first, second,
15623 "Caixa::upgrade_from must be idempotent — two \
15624 successive calls on the same &self must return the \
15625 same &[UpgradeFromEntry]",
15626 );
15627 assert_eq!(
15628 first.as_ptr(),
15629 second.as_ptr(),
15630 "Caixa::upgrade_from must borrow the underlying \
15631 Vec<UpgradeFromEntry> storage — two successive calls \
15632 must return slices with the same backing pointer (a \
15633 fresh Vec<UpgradeFromEntry> clone would change the \
15634 pointer on every call)",
15635 );
15636 assert_eq!(
15637 first,
15638 upgrade_from.as_slice(),
15639 "Caixa::upgrade_from must return :upgrade-from \
15640 verbatim by borrow — got {first:?}, expected \
15641 {upgrade_from:?}",
15642 );
15643 }
15644 }
15645
15646 // ── Caixa::children — outer top-level &[ChildSpec] composite-slice accessor ──
15647
15648 fn caixa_with_children(children: Vec<crate::supervisor::ChildSpec>) -> Caixa {
15649 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15650 c.children = children;
15651 c
15652 }
15653
15654 #[test]
15655 fn children_returns_children_slice_verbatim_across_permutations() {
15656 // The canonical per-`Caixa` `:children` M2 supervisor-tree-slot
15657 // outer-composite `&[ChildSpec]`-return slice-shape pin:
15658 // [`Caixa::children`] must return the `:children` typed
15659 // `Vec<ChildSpec>` verbatim as a `&[ChildSpec]` slice-view over
15660 // the same backing buffer the raw `self.children.as_slice()`
15661 // field access borrows from, element-equal across every
15662 // representative fixture in the accept-set — `[]` (the "no
15663 // static children declared" arm every non-`Supervisor`-kind
15664 // `defcaixa` carries by `#[serde(default)]` and every
15665 // `SimpleOneForOne` supervisor carries by cross-slot refusal),
15666 // a canonical single-child `Permanent` fixture (the shape
15667 // most `OneForOne` supervisors carry — a single long-running
15668 // worker child), a canonical multi-child list carrying every
15669 // typed restart-policy variant (`Permanent` / `Transient` /
15670 // `Temporary`), and a past-the-guard sentinel — a duplicate
15671 // `:caixa` `[("w", ...), ("w", ...)]` entry pair
15672 // ([`crate::SupervisorSpec::validate`] rejects through
15673 // `DuplicateChildNome { nome: "w" }` but the accessor must
15674 // ship the raw slot verbatim so struct-literal fixtures
15675 // continue to expose the duplicate at the accessor boundary).
15676 //
15677 // Pins against a future silent detour that returned an owned
15678 // `Vec<ChildSpec>` (which would type-check but silently clone
15679 // on every accessor call, breaking the zero-cost projection
15680 // every peer sibling slice accessor carries), a `[dup, dup] →
15681 // [dup]` dedup collapse (which would silently absorb the
15682 // `DuplicateChildNome` refusal case at the accessor boundary
15683 // and the [`crate::StandardLayout::verify`] cross-child gate
15684 // would silently accept a struct-literal `Caixa` carrying the
15685 // drift), a reference to an operator-resolved overlay (the
15686 // future per-cluster `:children-overrides` slot — its
15687 // resolution must land at exactly this accessor body, not
15688 // silently divert the raw slot away from a second consumer),
15689 // or an axis-shuffled projection (a future detour that
15690 // reordered children through the accessor would silently
15691 // split the paired [`crate::StandardLayout::verify`] per-
15692 // supervisor gate's traversal input from the peer
15693 // [`Self::supervisor_view`] fold-in path's clone-order input,
15694 // since the OTP `RestForOne` restart strategy dispatches on
15695 // declared child order and axis reordering would silently
15696 // split the operator's per-cluster restart-fan-out order
15697 // from the caixa.lisp source-order).
15698 //
15699 // Second outer top-level [`Caixa`] `&[Composite]`-return slice
15700 // accessor pin on the substrate primitive for M2 / M3 typed-
15701 // slot vec-carry axes — folds on the outer-`Caixa`
15702 // `&[Composite]` composite-slice sub-family the sibling
15703 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
15704 // (2a1f907) pin opened, peer at the outer altitude of the
15705 // closed inner-`SupervisorSpec` `SupervisorSpec::children`
15706 // (bc92bce) accessor on the same OTP-supervisor static-child-
15707 // list axis.
15708 use crate::supervisor::{ChildSpec, RestartPolicy};
15709 let fixtures: Vec<Vec<ChildSpec>> = vec![
15710 vec![],
15711 vec![ChildSpec {
15712 caixa: "worker".into(),
15713 versao: "^0.1".into(),
15714 restart: RestartPolicy::Permanent,
15715 }],
15716 vec![
15717 ChildSpec {
15718 caixa: "worker-a".into(),
15719 versao: "^0.1".into(),
15720 restart: RestartPolicy::Permanent,
15721 },
15722 ChildSpec {
15723 caixa: "worker-b".into(),
15724 versao: "^0.1".into(),
15725 restart: RestartPolicy::Transient,
15726 },
15727 ChildSpec {
15728 caixa: "worker-c".into(),
15729 versao: "^0.1".into(),
15730 restart: RestartPolicy::Temporary,
15731 },
15732 ],
15733 vec![
15734 ChildSpec {
15735 caixa: "w".into(),
15736 versao: "^0.1".into(),
15737 restart: RestartPolicy::Permanent,
15738 },
15739 ChildSpec {
15740 caixa: "w".into(),
15741 versao: "^0.1".into(),
15742 restart: RestartPolicy::Permanent,
15743 },
15744 ],
15745 ];
15746 for children in fixtures {
15747 let c = caixa_with_children(children.clone());
15748 assert_eq!(
15749 c.children(),
15750 children.as_slice(),
15751 "Caixa::children must return :children verbatim \
15752 (got {:?}, expected {children:?})",
15753 c.children(),
15754 );
15755 assert_eq!(
15756 c.children(),
15757 c.children.as_slice(),
15758 "Caixa::children must element-equal the raw \
15759 `self.children.as_slice()` field access across \
15760 every value in the Vec<ChildSpec> accept-set",
15761 );
15762 assert_eq!(
15763 c.children().is_empty(),
15764 c.children.is_empty(),
15765 "Caixa::children().is_empty() must byte-equal \
15766 self.children.is_empty() — a presence-bit drift \
15767 would silently split the paired \
15768 Caixa::declared_supervisor_slots supervisor-tree \
15769 declared-slot enumerator's presence probe from the \
15770 peer Caixa::supervisor_view typed-view composer's \
15771 fold-in path",
15772 );
15773 }
15774 }
15775
15776 #[test]
15777 fn declared_supervisor_slots_children_arm_routes_through_accessor() {
15778 // Composition pin: [`Caixa::declared_supervisor_slots`]'s
15779 // `:children` presence-probe arm must key off
15780 // [`Caixa::children`], not the raw
15781 // `!self.children.is_empty()` field-probe. Structurally: a
15782 // `Caixa { children: vec![ChildSpec { caixa: "w", versao:
15783 // "^0.1", restart: Permanent }], .. }` must push
15784 // `SUPERVISOR_AUTHOR_KEY_CHILDREN` onto the declared-slot list
15785 // (the presence bit is non-empty, so the supervisor-tree
15786 // kind-coherence gate must surface the slot as "declared"),
15787 // and a `Caixa { children: vec![], .. }` must NOT push the
15788 // label (the "author omitted the slot entirely" arm — the
15789 // empty-slice partition the serde-default folds onto). The
15790 // pair jointly pins the accessor + declared-slot enumerator
15791 // composition: any future silent detour that had the accessor
15792 // collapse `[Permanent]` to `[]` (a `.filter(|c| c.nome() !=
15793 // "__reserved__")` projection) would silently absorb the
15794 // "declared but degenerate" arm at the accessor boundary and
15795 // the [`crate::LayoutError::SupervisorSlotsOnNonSupervisor`]
15796 // kind-coherence gate would silently accept a struct-literal
15797 // `Caixa` carrying the drift.
15798 //
15799 // Peer of the sibling
15800 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
15801 // (2a1f907) on the M2 `:upgrade-from` composite-slice arm —
15802 // same "the enumerator gate must route through the substrate-
15803 // primitive typed dispatch" discipline extended onto the
15804 // supervisor-tree `:children` composite-slice arm.
15805 use crate::supervisor::{ChildSpec, RestartPolicy};
15806 let c = caixa_with_children(vec![ChildSpec {
15807 caixa: "w".into(),
15808 versao: "^0.1".into(),
15809 restart: RestartPolicy::Permanent,
15810 }]);
15811 let slots = c.declared_supervisor_slots();
15812 assert!(
15813 slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15814 "declared_supervisor_slots must push \
15815 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15816 non-empty — the accessor and the enumerator gate must \
15817 route through the same substrate-primitive typed \
15818 dispatch on the outer :children presence bit (got \
15819 slots={slots:?})",
15820 );
15821 let c = caixa_with_children(vec![]);
15822 let slots = c.declared_supervisor_slots();
15823 assert!(
15824 !slots.contains(&crate::render::SUPERVISOR_AUTHOR_KEY_CHILDREN),
15825 "declared_supervisor_slots must NOT push \
15826 SUPERVISOR_AUTHOR_KEY_CHILDREN when `:children` is \
15827 empty — the author-omitted arm must route through the \
15828 accessor's empty-slice return unchanged (got \
15829 slots={slots:?})",
15830 );
15831 }
15832
15833 #[test]
15834 fn supervisor_view_children_arm_routes_through_accessor() {
15835 // Composition pin: [`Caixa::supervisor_view`]'s per-`:children`
15836 // fold-in arm must key off [`Caixa::children`], not the raw
15837 // `self.children.clone()` field-clone. Structurally: a `Caixa {
15838 // kind: Supervisor, estrategia: Some(OneForOne), children:
15839 // vec![ChildSpec { caixa: "w", .. }], .. }` must fold the
15840 // per-child list through the accessor into the typed
15841 // [`SupervisorSpec`] view's `children` field verbatim — every
15842 // entry the accessor surfaces must land in the view's
15843 // `children` slot in the same order. The pair jointly pins the
15844 // accessor + view-composer composition: any future silent
15845 // detour that had the accessor return a fresh-cloned
15846 // `Vec<ChildSpec>` copy would silently break the reference-
15847 // identity pin the peer `supervisor_view` fold-in path reads
15848 // from — the fold would clone once more per accessor call
15849 // instead of borrowing the storage buffer verbatim once.
15850 //
15851 // Peer of the sibling
15852 // `supervisor_view_kind_gate_routes_through_accessor` (35d8b52-
15853 // family) composition pin on the peer kind-gate arm — same
15854 // "the view composer must route through the substrate-
15855 // primitive typed dispatch" discipline extended onto the
15856 // per-`:children` fold-in arm, closing the supervisor-view
15857 // composer's routing invariant on the composite-slice input.
15858 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
15859 let mut c = caixa_with_children(vec![
15860 ChildSpec {
15861 caixa: "worker-a".into(),
15862 versao: "^0.1".into(),
15863 restart: RestartPolicy::Permanent,
15864 },
15865 ChildSpec {
15866 caixa: "worker-b".into(),
15867 versao: "^0.1".into(),
15868 restart: RestartPolicy::Transient,
15869 },
15870 ]);
15871 c.kind = crate::CaixaKind::Supervisor;
15872 c.estrategia = Some(RestartStrategy::OneForOne);
15873 let view = c
15874 .supervisor_view()
15875 .expect("Supervisor kind must produce a supervisor_view");
15876 assert_eq!(
15877 view.children(),
15878 c.children(),
15879 "supervisor_view must fold Caixa::children verbatim into \
15880 SupervisorSpec::children — the accessor and the view \
15881 composer must route through the same substrate-primitive \
15882 typed dispatch on the outer :children slice (got view \
15883 children={:?}, expected {:?})",
15884 view.children(),
15885 c.children(),
15886 );
15887 }
15888
15889 #[test]
15890 fn children_projects_slice_by_borrow() {
15891 // The by-borrow pin: [`Caixa::children`] returns
15892 // `&[ChildSpec]` by borrow — the returned slice borrows the
15893 // underlying `Vec<ChildSpec>` storage of the `:children` slot
15894 // and the accessor must not clone the backing `Vec` on every
15895 // call. Peer of the sibling outer top-level [`Caixa`]
15896 // `&[T]`-return by-borrow pins (`autores_projects_slice_by_borrow`
15897 // b5d813f, `etiquetas_projects_slice_by_borrow` 78c7d3c,
15898 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
15899 // `exe_projects_slice_by_borrow` 65d9527,
15900 // `servicos_projects_slice_by_borrow` 611f78b,
15901 // `deps_projects_slice_by_borrow` ad34b4e,
15902 // `deps_dev_projects_slice_by_borrow` f7fd81e,
15903 // `upgrade_from_projects_slice_by_borrow` 2a1f907) on the
15904 // sibling outer top-level [`Caixa`] scalar-element and
15905 // composite-element `&[T]` axes — folds on the outer-`Caixa`
15906 // composite-element `&[Composite]` axis: the accessor's
15907 // returned slice must borrow from `&self` (the returned
15908 // reference's lifetime is tied to `&self`), and calling the
15909 // accessor twice on the same [`Caixa`] must yield slices
15910 // that are pointer-equal (the underlying byte-buffer is the
15911 // storage `Vec`'s allocation, not a fresh copy) as well as
15912 // value-equal (idempotent, no side effects on `&self`).
15913 //
15914 // Pins against a future silent detour that returned an owned
15915 // `Vec<ChildSpec>` (which would type-check but silently clone
15916 // on every call), a `&Vec<ChildSpec>` return (which would leak
15917 // the backing `Vec`'s grow/push/reserve surface no downstream
15918 // consumer reaches for), or a one-arm-only accessor that
15919 // returned a saturating value on some sentinel input.
15920 use crate::supervisor::{ChildSpec, RestartPolicy};
15921 for children in [
15922 vec![],
15923 vec![ChildSpec {
15924 caixa: "w".into(),
15925 versao: "^0.1".into(),
15926 restart: RestartPolicy::Permanent,
15927 }],
15928 vec![
15929 ChildSpec {
15930 caixa: "worker-a".into(),
15931 versao: "^0.1".into(),
15932 restart: RestartPolicy::Permanent,
15933 },
15934 ChildSpec {
15935 caixa: "worker-b".into(),
15936 versao: "^0.1".into(),
15937 restart: RestartPolicy::Transient,
15938 },
15939 ],
15940 ] {
15941 let c = caixa_with_children(children.clone());
15942 let first = c.children();
15943 let second = c.children();
15944 assert_eq!(
15945 first, second,
15946 "Caixa::children must be idempotent — two successive \
15947 calls on the same &self must return the same \
15948 &[ChildSpec]",
15949 );
15950 assert_eq!(
15951 first.as_ptr(),
15952 second.as_ptr(),
15953 "Caixa::children must borrow the underlying \
15954 Vec<ChildSpec> storage — two successive calls must \
15955 return slices with the same backing pointer (a fresh \
15956 Vec<ChildSpec> clone would change the pointer on \
15957 every call)",
15958 );
15959 assert_eq!(
15960 first,
15961 children.as_slice(),
15962 "Caixa::children must return :children verbatim by \
15963 borrow — got {first:?}, expected {children:?}",
15964 );
15965 }
15966 }
15967
15968 // ── Caixa::membros — outer top-level &[Membro] composite-slice accessor ──
15969
15970 fn caixa_aplicacao_with_membros(membros: Vec<crate::aplicacao::Membro>) -> Caixa {
15971 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
15972 c.kind = CaixaKind::Aplicacao;
15973 c.membros = membros;
15974 c
15975 }
15976
15977 #[test]
15978 fn membros_returns_membros_slice_verbatim_across_permutations() {
15979 // The canonical per-`Caixa` `:membros` M3 mesh-slot outer-
15980 // composite `&[Membro]`-return slice-shape pin:
15981 // [`Caixa::membros`] must return the `:membros` typed
15982 // `Vec<Membro>` verbatim as a `&[Membro]` slice-view over the
15983 // same backing buffer the raw `self.membros.as_slice()` field
15984 // access borrows from, element-equal across every
15985 // representative fixture in the accept-set — `[]` (the "no
15986 // members declared" arm every non-`Aplicacao`-kind `defcaixa`
15987 // carries by `#[serde(default)]` and every partially-authored
15988 // Aplicacao carries before the
15989 // [`crate::AplicacaoError::MembrosEmpty`] gate fires), a
15990 // canonical single-member fixture (the shape a minimal
15991 // Aplicacao carries — one Servico wrapping one contained
15992 // computation), a canonical multi-member list carrying three
15993 // distinct entries (the canonical checkout-shape Aplicacao —
15994 // cart / pricing / auth — every canonical example carries), and
15995 // a past-the-guard sentinel — a duplicate `:caixa`
15996 // `[("cart", ...), ("cart", ...)]` entry pair
15997 // ([`crate::AplicacaoSpec::validate`] rejects through
15998 // `DuplicateMembro { nome: "cart" }` but the accessor must ship
15999 // the raw slot verbatim so struct-literal fixtures continue to
16000 // expose the duplicate at the accessor boundary).
16001 //
16002 // Pins against a future silent detour that returned an owned
16003 // `Vec<Membro>` (which would type-check but silently clone on
16004 // every accessor call, breaking the zero-cost projection every
16005 // peer sibling slice accessor carries), a `[dup, dup] → [dup]`
16006 // dedup collapse (which would silently absorb the
16007 // `DuplicateMembro` refusal case at the accessor boundary and
16008 // the [`crate::StandardLayout::verify`] cross-member gate would
16009 // silently accept a struct-literal `Caixa` carrying the drift),
16010 // a reference to an operator-resolved overlay (the future per-
16011 // cluster `:membros-overrides` slot — its resolution must land
16012 // at exactly this accessor body, not silently divert the raw
16013 // slot away from a second consumer), or an axis-shuffled
16014 // projection (a future detour that reordered members through
16015 // the accessor would silently split the paired
16016 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16017 // traversal input from the peer [`Self::aplicacao_view`] fold-
16018 // in path's clone-order input, since the canonical `:contratos`
16019 // `:de`/`:para` and `:entrada :para` cross-slot refusal probes
16020 // read the member set through the same slice).
16021 //
16022 // Third outer top-level [`Caixa`] `&[Composite]`-return slice
16023 // accessor pin on the substrate primitive for M2 / M3 typed-
16024 // slot vec-carry axes — opens the outer-`Caixa` M3 mesh-slot
16025 // arm of the `&[Composite]` composite-slice sub-family the
16026 // sibling M2 `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16027 // (2a1f907) and
16028 // `children_returns_children_slice_verbatim_across_permutations`
16029 // (c17b51e) pins opened, peer at the outer altitude of the
16030 // closed inner-[`crate::AplicacaoSpec::membros`] (6c77e36)
16031 // accessor on the same MESH-COMPOSITION per-Aplicacao member-
16032 // list axis.
16033 use crate::aplicacao::Membro;
16034 let fixtures: Vec<Vec<Membro>> = vec![
16035 vec![],
16036 vec![Membro {
16037 caixa: "cart".into(),
16038 versao: "^0.1".into(),
16039 }],
16040 vec![
16041 Membro {
16042 caixa: "cart".into(),
16043 versao: "^0.1".into(),
16044 },
16045 Membro {
16046 caixa: "pricing".into(),
16047 versao: "^0.2".into(),
16048 },
16049 Membro {
16050 caixa: "auth".into(),
16051 versao: "^1.0".into(),
16052 },
16053 ],
16054 vec![
16055 Membro {
16056 caixa: "cart".into(),
16057 versao: "^0.1".into(),
16058 },
16059 Membro {
16060 caixa: "cart".into(),
16061 versao: "^0.1".into(),
16062 },
16063 ],
16064 ];
16065 for membros in fixtures {
16066 let c = caixa_aplicacao_with_membros(membros.clone());
16067 assert_eq!(
16068 c.membros(),
16069 membros.as_slice(),
16070 "Caixa::membros must return :membros verbatim \
16071 (got {:?}, expected {membros:?})",
16072 c.membros(),
16073 );
16074 assert_eq!(
16075 c.membros(),
16076 c.membros.as_slice(),
16077 "Caixa::membros must element-equal the raw \
16078 `self.membros.as_slice()` field access across every \
16079 value in the Vec<Membro> accept-set",
16080 );
16081 assert_eq!(
16082 c.membros().is_empty(),
16083 c.membros.is_empty(),
16084 "Caixa::membros().is_empty() must byte-equal \
16085 self.membros.is_empty() — a presence-bit drift would \
16086 silently split the paired Caixa::declared_mesh_slots \
16087 mesh declared-slot enumerator's presence probe from \
16088 the peer Caixa::aplicacao_view typed-view composer's \
16089 fold-in path",
16090 );
16091 }
16092 }
16093
16094 #[test]
16095 fn declared_mesh_slots_membros_arm_routes_through_accessor() {
16096 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:membros`
16097 // presence-probe arm must key off [`Caixa::membros`], not the
16098 // raw `!self.membros.is_empty()` field-probe. Structurally: a
16099 // `Caixa { membros: vec![Membro { caixa: "cart", versao:
16100 // "^0.1" }], .. }` must push `M3_AUTHOR_KEY_MEMBROS` onto the
16101 // declared-slot list (the presence bit is non-empty, so the
16102 // mesh kind-coherence gate must surface the slot as
16103 // "declared"), and a `Caixa { membros: vec![], .. }` must NOT
16104 // push the label (the "author omitted the slot entirely" arm
16105 // — the empty-slice partition the serde-default folds onto).
16106 // The pair jointly pins the accessor + declared-slot
16107 // enumerator composition: any future silent detour that had
16108 // the accessor collapse `[Membro { .. }]` to `[]` (a
16109 // `.filter(|m| m.nome() != "__reserved__")` projection) would
16110 // silently absorb the "declared but degenerate" arm at the
16111 // accessor boundary and the
16112 // [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16113 // coherence gate would silently accept a struct-literal
16114 // `Caixa` carrying the drift.
16115 //
16116 // Peer of the sibling
16117 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16118 // (2a1f907) and
16119 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16120 // (c17b51e) composition pins on the M2 `:upgrade-from` /
16121 // `:children` composite-slice arms — same "the enumerator gate
16122 // must route through the substrate-primitive typed dispatch"
16123 // discipline extended onto the M3 `:membros` composite-slice
16124 // arm, opening the M3 arm of the declared-slot enumerator's
16125 // routing invariant.
16126 use crate::aplicacao::Membro;
16127 let c = caixa_aplicacao_with_membros(vec![Membro {
16128 caixa: "cart".into(),
16129 versao: "^0.1".into(),
16130 }]);
16131 let slots = c.declared_mesh_slots();
16132 assert!(
16133 slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16134 "declared_mesh_slots must push M3_AUTHOR_KEY_MEMBROS when \
16135 `:membros` is non-empty — the accessor and the enumerator \
16136 gate must route through the same substrate-primitive \
16137 typed dispatch on the outer :membros presence bit (got \
16138 slots={slots:?})",
16139 );
16140 let c = caixa_aplicacao_with_membros(vec![]);
16141 let slots = c.declared_mesh_slots();
16142 assert!(
16143 !slots.contains(&crate::render::M3_AUTHOR_KEY_MEMBROS),
16144 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_MEMBROS \
16145 when `:membros` is empty — the author-omitted arm must \
16146 route through the accessor's empty-slice return unchanged \
16147 (got slots={slots:?})",
16148 );
16149 }
16150
16151 #[test]
16152 fn aplicacao_view_membros_arm_routes_through_accessor() {
16153 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:membros`
16154 // fold-in arm must key off [`Caixa::membros`], not the raw
16155 // `self.membros.clone()` field-clone. Structurally: a `Caixa {
16156 // kind: Aplicacao, membros: vec![Membro { caixa: "cart", .. },
16157 // Membro { caixa: "pricing", .. }], .. }` must fold the per-
16158 // member list through the accessor into the typed
16159 // [`crate::AplicacaoSpec`] view's `membros` slot verbatim —
16160 // every entry the accessor surfaces must land in the view's
16161 // `membros` slot in the same order. The pair jointly pins the
16162 // accessor + view-composer composition: any future silent
16163 // detour that had the accessor return a fresh-cloned
16164 // `Vec<Membro>` copy would silently break the reference-
16165 // identity pin the peer `aplicacao_view` fold-in path reads
16166 // from — the fold would clone once more per accessor call
16167 // instead of borrowing the storage buffer verbatim once.
16168 //
16169 // Peer of the sibling
16170 // `aplicacao_view_politicas_arm_folds_through_accessor`
16171 // (5d23d29) /
16172 // `aplicacao_view_placement_arm_folds_through_accessor`
16173 // (4fb8074) /
16174 // `aplicacao_view_entrada_arm_folds_through_accessor` (e4128e4)
16175 // composition pins on the M3 `:politicas` / `:placement` /
16176 // `:entrada` outer-`Option<&Composite>` arms — extended here to
16177 // the M3 `:membros` outer-`&[Composite]` composite-slice arm,
16178 // closing the aplicacao-view composer's routing invariant on
16179 // the composite-slice input.
16180 use crate::aplicacao::Membro;
16181 let c = caixa_aplicacao_with_membros(vec![
16182 Membro {
16183 caixa: "cart".into(),
16184 versao: "^0.1".into(),
16185 },
16186 Membro {
16187 caixa: "pricing".into(),
16188 versao: "^0.2".into(),
16189 },
16190 ]);
16191 let view = c
16192 .aplicacao_view()
16193 .expect("Aplicacao kind must produce an aplicacao_view");
16194 assert_eq!(
16195 view.membros(),
16196 c.membros(),
16197 "aplicacao_view must fold Caixa::membros verbatim into \
16198 AplicacaoSpec::membros — the accessor and the view \
16199 composer must route through the same substrate-primitive \
16200 typed dispatch on the outer :membros slice (got view \
16201 membros={:?}, expected {:?})",
16202 view.membros(),
16203 c.membros(),
16204 );
16205 }
16206
16207 #[test]
16208 fn membros_projects_slice_by_borrow() {
16209 // The by-borrow pin: [`Caixa::membros`] returns `&[Membro]` by
16210 // borrow — the returned slice borrows the underlying
16211 // `Vec<Membro>` storage of the `:membros` slot and the
16212 // accessor must not clone the backing `Vec` on every call.
16213 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16214 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16215 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16216 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16217 // `exe_projects_slice_by_borrow` 65d9527,
16218 // `servicos_projects_slice_by_borrow` 611f78b,
16219 // `deps_projects_slice_by_borrow` ad34b4e,
16220 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16221 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16222 // `children_projects_slice_by_borrow` c17b51e) on the sibling
16223 // outer top-level [`Caixa`] scalar-element and composite-
16224 // element `&[T]` axes — folds on the outer-`Caixa` M3 mesh-
16225 // slot composite-element `&[Composite]` axis: the accessor's
16226 // returned slice must borrow from `&self` (the returned
16227 // reference's lifetime is tied to `&self`), and calling the
16228 // accessor twice on the same [`Caixa`] must yield slices that
16229 // are pointer-equal (the underlying byte-buffer is the storage
16230 // `Vec`'s allocation, not a fresh copy) as well as value-equal
16231 // (idempotent, no side effects on `&self`).
16232 //
16233 // Pins against a future silent detour that returned an owned
16234 // `Vec<Membro>` (which would type-check but silently clone on
16235 // every call), a `&Vec<Membro>` return (which would leak the
16236 // backing `Vec`'s grow/push/reserve surface no downstream
16237 // consumer reaches for), or a one-arm-only accessor that
16238 // returned a saturating value on some sentinel input.
16239 use crate::aplicacao::Membro;
16240 for membros in [
16241 vec![],
16242 vec![Membro {
16243 caixa: "cart".into(),
16244 versao: "^0.1".into(),
16245 }],
16246 vec![
16247 Membro {
16248 caixa: "cart".into(),
16249 versao: "^0.1".into(),
16250 },
16251 Membro {
16252 caixa: "pricing".into(),
16253 versao: "^0.2".into(),
16254 },
16255 ],
16256 ] {
16257 let c = caixa_aplicacao_with_membros(membros.clone());
16258 let first = c.membros();
16259 let second = c.membros();
16260 assert_eq!(
16261 first, second,
16262 "Caixa::membros must be idempotent — two successive \
16263 calls on the same &self must return the same &[Membro]",
16264 );
16265 assert_eq!(
16266 first.as_ptr(),
16267 second.as_ptr(),
16268 "Caixa::membros must borrow the underlying Vec<Membro> \
16269 storage — two successive calls must return slices with \
16270 the same backing pointer (a fresh Vec<Membro> clone \
16271 would change the pointer on every call)",
16272 );
16273 assert_eq!(
16274 first,
16275 membros.as_slice(),
16276 "Caixa::membros must return :membros verbatim by borrow \
16277 — got {first:?}, expected {membros:?}",
16278 );
16279 }
16280 }
16281
16282 // ── Caixa::contratos — outer top-level &[WitContract] composite-slice accessor ──
16283
16284 fn caixa_aplicacao_with_contratos(contratos: Vec<crate::aplicacao::WitContract>) -> Caixa {
16285 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16286 c.kind = CaixaKind::Aplicacao;
16287 c.contratos = contratos;
16288 c
16289 }
16290
16291 fn contrato_http_for_test(
16292 de: &str,
16293 para: &str,
16294 endpoint: &str,
16295 ) -> crate::aplicacao::WitContract {
16296 crate::aplicacao::WitContract {
16297 de: de.into(),
16298 para: para.into(),
16299 wit: "wasi:http/proxy".into(),
16300 endpoint: Some(endpoint.into()),
16301 subject: None,
16302 slot: None,
16303 }
16304 }
16305
16306 #[test]
16307 fn contratos_returns_contratos_slice_verbatim_across_permutations() {
16308 // The canonical per-`Caixa` `:contratos` M3 mesh-slot outer-
16309 // composite `&[WitContract]`-return slice-shape pin:
16310 // [`Caixa::contratos`] must return the `:contratos` typed
16311 // `Vec<WitContract>` verbatim as a `&[WitContract]` slice-view
16312 // over the same backing buffer the raw
16313 // `self.contratos.as_slice()` field access borrows from,
16314 // element-equal across every representative fixture in the
16315 // accept-set — `[]` (the "no contracts declared" arm every
16316 // non-`Aplicacao`-kind `defcaixa` carries by
16317 // `#[serde(default)]` and every leaf-Aplicacao with a single
16318 // member carries), a canonical single-edge fixture (the
16319 // minimal directed-graph shape: one HTTP-shape `(cart → catalog)`
16320 // edge), and a canonical multi-edge fixture with three distinct
16321 // edges (the checkout-shape Aplicacao's HTTP-fan pattern:
16322 // `(cart → catalog)`, `(cart → pricing)`, `(cart → auth)`).
16323 //
16324 // Pins against a future silent detour that returned an owned
16325 // `Vec<WitContract>` (which would type-check but silently clone
16326 // on every accessor call, breaking the zero-cost projection
16327 // every peer sibling slice accessor carries), an axis-shuffled
16328 // projection (a future detour that reordered edges through the
16329 // accessor would silently split the paired
16330 // [`crate::StandardLayout::verify`] per-Aplicacao gate's
16331 // traversal input from the peer [`Self::aplicacao_view`] fold-
16332 // in path's clone-order input, since every canonical
16333 // `caixa-mesh` renderer's per-`(:de, :para)` adjacency-list
16334 // seed dispatch reads the edge set through the same slice),
16335 // or a reference to an operator-resolved overlay (the future
16336 // per-cluster `:contratos-overrides` slot — its resolution
16337 // must land at exactly this accessor body, not silently divert
16338 // the raw slot away from a second consumer).
16339 //
16340 // Fourth outer top-level [`Caixa`] `&[Composite]`-return slice
16341 // accessor pin on the substrate primitive for M2 / M3 typed-
16342 // slot vec-carry axes — closes the outer-`Caixa`
16343 // `&[Composite]` composite-slice sub-family the sibling M2
16344 // `upgrade_from_returns_upgrade_from_slice_verbatim_across_permutations`
16345 // (2a1f907) and
16346 // `children_returns_children_slice_verbatim_across_permutations`
16347 // (c17b51e) pins opened and the M3
16348 // `membros_returns_membros_slice_verbatim_across_permutations`
16349 // (0f26987) pin folded on, closing the outer-`Caixa` M3 mesh-
16350 // slot arm of the composite-slice sub-family. Peer at the outer
16351 // altitude of the closed inner-
16352 // [`crate::AplicacaoSpec::contratos`] (0dcc926) accessor on the
16353 // same MESH-COMPOSITION per-Aplicacao contract-list axis.
16354 let fixtures: Vec<Vec<crate::aplicacao::WitContract>> = vec![
16355 vec![],
16356 vec![contrato_http_for_test("cart", "catalog", "/items")],
16357 vec![
16358 contrato_http_for_test("cart", "catalog", "/items"),
16359 contrato_http_for_test("cart", "pricing", "/price"),
16360 contrato_http_for_test("cart", "auth", "/whoami"),
16361 ],
16362 ];
16363 for contratos in fixtures {
16364 let c = caixa_aplicacao_with_contratos(contratos.clone());
16365 assert_eq!(
16366 c.contratos(),
16367 contratos.as_slice(),
16368 "Caixa::contratos must return :contratos verbatim \
16369 (got {:?}, expected {contratos:?})",
16370 c.contratos(),
16371 );
16372 assert_eq!(
16373 c.contratos(),
16374 c.contratos.as_slice(),
16375 "Caixa::contratos must element-equal the raw \
16376 `self.contratos.as_slice()` field access across every \
16377 value in the Vec<WitContract> accept-set",
16378 );
16379 assert_eq!(
16380 c.contratos().is_empty(),
16381 c.contratos.is_empty(),
16382 "Caixa::contratos().is_empty() must byte-equal \
16383 self.contratos.is_empty() — a presence-bit drift would \
16384 silently split the paired Caixa::declared_mesh_slots \
16385 mesh declared-slot enumerator's presence probe from \
16386 the peer Caixa::aplicacao_view typed-view composer's \
16387 fold-in path",
16388 );
16389 }
16390 }
16391
16392 #[test]
16393 fn declared_mesh_slots_contratos_arm_routes_through_accessor() {
16394 // Composition pin: [`Caixa::declared_mesh_slots`]'s `:contratos`
16395 // presence-probe arm must key off [`Caixa::contratos`], not the
16396 // raw `!self.contratos.is_empty()` field-probe. Structurally: a
16397 // `Caixa { contratos: vec![WitContract { .. }], .. }` must push
16398 // `M3_AUTHOR_KEY_CONTRATOS` onto the declared-slot list (the
16399 // presence bit is non-empty, so the mesh kind-coherence gate
16400 // must surface the slot as "declared"), and a `Caixa {
16401 // contratos: vec![], .. }` must NOT push the label (the "author
16402 // omitted the slot entirely" arm — the empty-slice partition
16403 // the serde-default folds onto). The pair jointly pins the
16404 // accessor + declared-slot enumerator composition: any future
16405 // silent detour that had the accessor collapse
16406 // `[WitContract { .. }]` to `[]` (a `.filter(|c| c.de() !=
16407 // "__reserved__")` projection) would silently absorb the
16408 // "declared but degenerate" arm at the accessor boundary and
16409 // the [`crate::LayoutError::MeshSlotsOnNonAplicacao`] kind-
16410 // coherence gate would silently accept a struct-literal
16411 // `Caixa` carrying the drift.
16412 //
16413 // Peer of the sibling
16414 // `declared_servico_slots_upgrade_from_arm_routes_through_accessor`
16415 // (2a1f907),
16416 // `declared_supervisor_slots_children_arm_routes_through_accessor`
16417 // (c17b51e), and
16418 // `declared_mesh_slots_membros_arm_routes_through_accessor`
16419 // (0f26987) composition pins on the M2 `:upgrade-from` /
16420 // `:children` / M3 `:membros` composite-slice arms — same "the
16421 // enumerator gate must route through the substrate-primitive
16422 // typed dispatch" discipline extended onto the M3 `:contratos`
16423 // composite-slice arm, closing the M3 mesh-slot arm of the
16424 // declared-slot enumerator's routing invariant on the
16425 // composite-slice inputs.
16426 let c = caixa_aplicacao_with_contratos(vec![contrato_http_for_test(
16427 "cart", "catalog", "/items",
16428 )]);
16429 let slots = c.declared_mesh_slots();
16430 assert!(
16431 slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16432 "declared_mesh_slots must push M3_AUTHOR_KEY_CONTRATOS when \
16433 `:contratos` is non-empty — the accessor and the enumerator \
16434 gate must route through the same substrate-primitive \
16435 typed dispatch on the outer :contratos presence bit (got \
16436 slots={slots:?})",
16437 );
16438 let c = caixa_aplicacao_with_contratos(vec![]);
16439 let slots = c.declared_mesh_slots();
16440 assert!(
16441 !slots.contains(&crate::render::M3_AUTHOR_KEY_CONTRATOS),
16442 "declared_mesh_slots must NOT push M3_AUTHOR_KEY_CONTRATOS \
16443 when `:contratos` is empty — the author-omitted arm must \
16444 route through the accessor's empty-slice return unchanged \
16445 (got slots={slots:?})",
16446 );
16447 }
16448
16449 #[test]
16450 fn aplicacao_view_contratos_arm_routes_through_accessor() {
16451 // Composition pin: [`Caixa::aplicacao_view`]'s per-`:contratos`
16452 // fold-in arm must key off [`Caixa::contratos`], not the raw
16453 // `self.contratos.clone()` field-clone. Structurally: a `Caixa
16454 // { kind: Aplicacao, contratos: vec![WitContract { de: "cart",
16455 // .. }, WitContract { de: "pricing", .. }], .. }` must fold the
16456 // per-edge list through the accessor into the typed
16457 // [`crate::AplicacaoSpec`] view's `contratos` slot verbatim —
16458 // every entry the accessor surfaces must land in the view's
16459 // `contratos` slot in the same order. The pair jointly pins
16460 // the accessor + view-composer composition: a future silent
16461 // detour that had the accessor shuffle or drop an edge would
16462 // silently split the paired declared-slot enumerator's
16463 // presence bit from the typed-view composer's edge-list, a
16464 // two-consumer split at the enumerator and the view composer
16465 // far from the source `caixa.lisp`.
16466 //
16467 // Peer of the sibling
16468 // `aplicacao_view_membros_arm_routes_through_accessor`
16469 // (0f26987) composition pin on the M3 `:membros` outer-
16470 // `&[Composite]` composite-slice arm, closing the aplicacao-
16471 // view composer's routing invariant on the composite-slice
16472 // inputs at the outer altitude.
16473 let c = caixa_aplicacao_with_contratos(vec![
16474 contrato_http_for_test("cart", "catalog", "/items"),
16475 contrato_http_for_test("cart", "pricing", "/price"),
16476 ]);
16477 let view = c
16478 .aplicacao_view()
16479 .expect("Aplicacao kind must produce an aplicacao_view");
16480 assert_eq!(
16481 view.contratos(),
16482 c.contratos(),
16483 "aplicacao_view must fold Caixa::contratos verbatim into \
16484 AplicacaoSpec::contratos — the accessor and the view \
16485 composer must route through the same substrate-primitive \
16486 typed dispatch on the outer :contratos slice (got view \
16487 contratos={:?}, expected {:?})",
16488 view.contratos(),
16489 c.contratos(),
16490 );
16491 }
16492
16493 #[test]
16494 fn contratos_projects_slice_by_borrow() {
16495 // The by-borrow pin: [`Caixa::contratos`] returns `&[WitContract]`
16496 // by borrow — the returned slice borrows the underlying
16497 // `Vec<WitContract>` storage of the `:contratos` slot and the
16498 // accessor must not clone the backing `Vec` on every call.
16499 // Peer of the sibling outer top-level [`Caixa`] `&[T]`-return
16500 // by-borrow pins (`autores_projects_slice_by_borrow` b5d813f,
16501 // `etiquetas_projects_slice_by_borrow` 78c7d3c,
16502 // `bibliotecas_projects_slice_by_borrow` 8a36c23,
16503 // `exe_projects_slice_by_borrow` 65d9527,
16504 // `servicos_projects_slice_by_borrow` 611f78b,
16505 // `deps_projects_slice_by_borrow` ad34b4e,
16506 // `deps_dev_projects_slice_by_borrow` f7fd81e,
16507 // `upgrade_from_projects_slice_by_borrow` 2a1f907,
16508 // `children_projects_slice_by_borrow` c17b51e,
16509 // `membros_projects_slice_by_borrow` 0f26987) on the sibling
16510 // outer top-level [`Caixa`] scalar-element and composite-
16511 // element `&[T]` axes — closes the outer-`Caixa` M3 mesh-slot
16512 // composite-element `&[Composite]` axis on the by-borrow pin:
16513 // the accessor's returned slice must borrow from `&self` (the
16514 // returned reference's lifetime is tied to `&self`), and
16515 // calling the accessor twice on the same [`Caixa`] must yield
16516 // slices that are pointer-equal (the underlying byte-buffer is
16517 // the storage `Vec`'s allocation, not a fresh copy) as well as
16518 // value-equal (idempotent, no side effects on `&self`).
16519 //
16520 // Pins against a future silent detour that returned an owned
16521 // `Vec<WitContract>` (which would type-check but silently clone
16522 // on every call), a `&Vec<WitContract>` return (which would
16523 // leak the backing `Vec`'s grow/push/reserve surface no
16524 // downstream consumer reaches for), or a one-arm-only accessor
16525 // that returned a saturating value on some sentinel input.
16526 for contratos in [
16527 vec![],
16528 vec![contrato_http_for_test("cart", "catalog", "/items")],
16529 vec![
16530 contrato_http_for_test("cart", "catalog", "/items"),
16531 contrato_http_for_test("cart", "pricing", "/price"),
16532 ],
16533 ] {
16534 let c = caixa_aplicacao_with_contratos(contratos.clone());
16535 let first = c.contratos();
16536 let second = c.contratos();
16537 assert_eq!(
16538 first, second,
16539 "Caixa::contratos must be idempotent — two successive \
16540 calls on the same &self must return the same \
16541 &[WitContract]",
16542 );
16543 assert_eq!(
16544 first.as_ptr(),
16545 second.as_ptr(),
16546 "Caixa::contratos must borrow the underlying \
16547 Vec<WitContract> storage — two successive calls must \
16548 return slices with the same backing pointer (a fresh \
16549 Vec<WitContract> clone would change the pointer on \
16550 every call)",
16551 );
16552 assert_eq!(
16553 first,
16554 contratos.as_slice(),
16555 "Caixa::contratos must return :contratos verbatim by \
16556 borrow — got {first:?}, expected {contratos:?}",
16557 );
16558 }
16559 }
16560
16561 // ── drift-detection: Caixa top-level multi-word serde-derive-to-const identity ──
16562
16563 #[test]
16564 fn caixa_multi_word_serde_keys_match_lifted_top_level_key_consts() {
16565 // Load-bearing invariant: every multi-word top-level [`Caixa`]
16566 // serde-derived JSON key routes through a lifted `&'static str`
16567 // const. The Rust field names are `snake_case`
16568 // (`deps_dev` / `upgrade_from` / `max_restarts` /
16569 // `restart_window`); [`Caixa`]'s `#[serde(rename_all =
16570 // "camelCase")]` derive attribute maps each to the camelCase
16571 // byte-string the [`Caixa::to_lisp`] round-trip's
16572 // `serde_json::to_value(self)` step lands under before
16573 // `tatara_lisp::domain::json_to_sexp` re-projects the JSON keys
16574 // to the kebab-case `:deps-dev` / `:upgrade-from` /
16575 // `:max-restarts` / `:restart-window` author surface. Serialize
16576 // a fully-populated [`Caixa`] and pin that each canonical
16577 // byte-sequence appears verbatim in the JSON — a future
16578 // accidental `rename_all = "snake_case"` / `"kebab-case"` /
16579 // verbatim-field-name flip at the derive attribute (any of
16580 // which would silently break every [`Caixa::to_lisp`]
16581 // round-trip and the future M4 operator-side manifest ingest's
16582 // `Value::get(<key>)` navigation) surfaces here as a build-time
16583 // test failure at `manifest.rs`, not as an apply-time
16584 // `.get(<stale-canonical-const>)` returning `None` far from the
16585 // derive-attr drift's commit. Same discipline the sibling
16586 // `supervisor_spec_serde_keys_match_lifted_supervisor_key_consts`
16587 // (40cc4e5), `membro_serde_keys_match_lifted_membro_key_consts`
16588 // (ce80ca0), and `upgrade_from_entry_serde_keys_match_lifted_
16589 // m2_upgrade_from_key_consts` (36ffe65) pins established on the
16590 // sibling M2 supervision-tree, M3 [`Membro`] per-entry, and M2
16591 // [`UpgradeFromEntry`] per-entry axes — extended here to the
16592 // enclosing M0 [`Caixa`] top-level axis so the last of the four
16593 // multi-word top-level [`Caixa`] serde-derived JSON keys
16594 // (`depsDev`) joins the substrate's "one canonical byte-string
16595 // per typed serialized-key axis" discipline.
16596 use crate::supervisor::{ChildSpec, RestartPolicy, RestartStrategy};
16597 use crate::upgrade::{UpgradeFromEntry, UpgradeInstruction};
16598 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16599 c.deps_dev = vec![Dep::simple("tatara-check", "^0.1")];
16600 c.upgrade_from = vec![UpgradeFromEntry {
16601 from: "0.0.1".into(),
16602 instructions: vec![UpgradeInstruction::Restart],
16603 }];
16604 c.estrategia = Some(RestartStrategy::OneForOne);
16605 c.max_restarts = Some(3);
16606 c.restart_window = Some("60s".into());
16607 c.children = vec![ChildSpec {
16608 caixa: "child".into(),
16609 versao: "^0.1".into(),
16610 restart: RestartPolicy::Permanent,
16611 }];
16612 let json = serde_json::to_string(&c).unwrap();
16613 for key in [
16614 crate::render::CAIXA_KEY_DEPS_DEV,
16615 crate::render::M2_KEY_UPGRADE_FROM,
16616 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16617 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16618 ] {
16619 let quoted = format!("\"{key}\"");
16620 assert!(
16621 json.contains("ed),
16622 "serialized Caixa must carry the lifted top-level \
16623 multi-word byte-sequence {quoted} verbatim in the JSON \
16624 emission (got: {json})",
16625 );
16626 }
16627 }
16628
16629 #[test]
16630 fn caixa_top_level_multi_word_key_consts_are_pairwise_distinct() {
16631 // Cross-axis drift-detection pin: a future collapse of the four
16632 // canonical [`Caixa`] top-level multi-word byte-strings onto the
16633 // same value (e.g. an accidental copy-paste flip of
16634 // [`crate::render::CAIXA_KEY_DEPS_DEV`] to also read
16635 // `"upgradeFrom"`) would silently reroute every downstream
16636 // `Value::get(<key>)` probe on one axis onto the sibling axis's
16637 // top-level entry and pass every propagation-probe test that
16638 // expected only the stale axis's value. Peer of the sibling
16639 // four-way distinct pin on the `SUPERVISOR_KEY_*` tetrad
16640 // (40cc4e5) and the two-way pin on `MEMBRO_KEY_*` (ce80ca0).
16641 let all = [
16642 crate::render::CAIXA_KEY_DEPS_DEV,
16643 crate::render::M2_KEY_UPGRADE_FROM,
16644 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16645 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16646 ];
16647 for (i, a) in all.iter().enumerate() {
16648 for b in all.iter().skip(i + 1) {
16649 assert_ne!(
16650 a, b,
16651 "Caixa top-level multi-word key consts must be \
16652 pairwise-distinct canonical byte-sequences — got \
16653 `{a}` == `{b}`",
16654 );
16655 }
16656 }
16657 }
16658
16659 #[test]
16660 fn caixa_top_level_multi_word_key_consts_are_lower_camel_case_shape() {
16661 // Shape-pin: every [`Caixa`] top-level multi-word key const must
16662 // be a lowerCamelCase byte-sequence (no `snake_case`
16663 // underscores, no `kebab-case` hyphens, no leading colon, no
16664 // `PascalCase` leading capital, no whitespace / dots) — the
16665 // canonical shape the `#[serde(rename_all = "camelCase")]`
16666 // derive produces on [`Caixa`]. A future flip to a
16667 // non-camelCase attribute at the derive surfaces both here
16668 // (this test fails on the stale-constant shape) and at
16669 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16670 // (that test fails on the mismatch between const and derive).
16671 // Peer with `membro_key_consts_are_lower_camel_case_shape`
16672 // (ce80ca0) and `supervisor_key_consts_are_lower_camel_case_shape`
16673 // (40cc4e5) on the sibling per-entry / supervisor-tree axes.
16674 for key in [
16675 crate::render::CAIXA_KEY_DEPS_DEV,
16676 crate::render::M2_KEY_UPGRADE_FROM,
16677 crate::render::SUPERVISOR_KEY_MAX_RESTARTS,
16678 crate::render::SUPERVISOR_KEY_RESTART_WINDOW,
16679 ] {
16680 assert!(
16681 !key.is_empty(),
16682 "Caixa top-level multi-word key const must be non-empty \
16683 (got {key:?})"
16684 );
16685 let first = key.chars().next().unwrap();
16686 assert!(
16687 first.is_ascii_lowercase(),
16688 "Caixa top-level multi-word key const must lead with an \
16689 ASCII-lowercase byte (got {key:?}, leads with {first:?})",
16690 );
16691 assert!(
16692 key.chars().all(|c| c.is_ascii_alphanumeric()),
16693 "Caixa top-level multi-word key const must be \
16694 ASCII-alphanumeric only — no `_` / `-` / `:` / `.` / \
16695 whitespace (got {key:?})",
16696 );
16697 }
16698 }
16699
16700 #[test]
16701 fn caixa_key_deps_dev_pins_canonical_camel_case_byte_string() {
16702 // Scalar-value pin: the byte-string the
16703 // [`crate::render::CAIXA_KEY_DEPS_DEV`] const resolves to,
16704 // asserted verbatim. A future rebrand (`depsDev` → `devDeps`
16705 // matching Cargo's verbatim `dev-dependencies` axis, `depsDev`
16706 // → `depsTest` matching a hypothetical per-test-target
16707 // vocabulary flip) lands as an edit to exactly one const AND
16708 // one derive attribute — the sibling
16709 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16710 // pin already ties the const to the derive attribute, so a
16711 // rebrand that touches only one side of the pair fails at
16712 // caixa-core build time. Same "scalar-value pin per const"
16713 // discipline the sibling
16714 // `m2_top_level_author_key_consts_pin_canonical_kebab_case_labels`
16715 // (f49c8b0) and `contrato_key_consts_pin_canonical_camel_case_labels`
16716 // (ca463a4) pins carry on the peer M2 / M3 top-level slot axes.
16717 assert_eq!(crate::render::CAIXA_KEY_DEPS_DEV, "depsDev");
16718 }
16719
16720 #[test]
16721 fn caixa_key_deps_pins_canonical_byte_string() {
16722 // Scalar-value pin: the byte-string the
16723 // [`crate::render::CAIXA_KEY_DEPS`] const resolves to, asserted
16724 // verbatim. Peer of `caixa_key_deps_dev_pins_canonical_camel_case_byte_string`
16725 // on the two-list dep-graph serialized-key axis — the sibling
16726 // pin covers the multi-word `deps_dev → depsDev` camelCase
16727 // arm, this pin covers the single-word `deps → deps` no-op arm
16728 // (the [`crate::Caixa::deps`] field name carries no `_`, so the
16729 // `#[serde(rename_all = "camelCase")]` derive is a no-op on this
16730 // axis and the emitted JSON key equals the source-side field
16731 // name byte-for-byte). A future [`crate::Caixa::deps`] field
16732 // rename (`deps` → `dependencies` matching Cargo's verbatim
16733 // `[dependencies]` axis, `deps` → `runtime_deps` matching a
16734 // hypothetical per-runtime-target vocabulary flip) OR an added
16735 // `#[serde(rename = "…")]` explicit override lands as an edit
16736 // to exactly one const AND one derive-attr / field name — the
16737 // sibling `caixa_deps_serde_key_matches_lifted_caixa_key_deps`
16738 // pin ties the const to the emitted JSON key, so a rebrand
16739 // that touches only one side of the pair fails at caixa-core
16740 // build time.
16741 assert_eq!(crate::render::CAIXA_KEY_DEPS, "deps");
16742 }
16743
16744 #[test]
16745 fn caixa_deps_serde_key_matches_lifted_caixa_key_deps() {
16746 // Load-bearing invariant on the single-word `deps` top-level
16747 // axis: the byte-string [`crate::render::CAIXA_KEY_DEPS`] pins
16748 // must appear verbatim in the JSON [`Caixa::to_lisp`]'s
16749 // `serde_json::to_value(self)` step emits. Serialize a
16750 // populated [`Caixa`] whose `:deps` slot carries at least one
16751 // entry (the `#[serde(default)]` attribute on the field emits
16752 // an empty `[]` even without members, but a non-empty vec
16753 // additionally covers the codec's per-`Dep`-entry emission
16754 // path) and pin that `"deps"` appears verbatim in the JSON
16755 // emission — a future accidental `rename_all = "snake_case"` /
16756 // `"kebab-case"` flip at the derive attribute (or an added
16757 // `#[serde(rename = "…")]` explicit override on the field, or
16758 // a Rust field rename) would break every [`Caixa::to_lisp`]
16759 // round-trip and the future M4 operator-side manifest ingest's
16760 // `Value::get(CAIXA_KEY_DEPS)` navigation — surfaces here as a
16761 // build-time test failure at `manifest.rs`, not as an
16762 // apply-time `.get(<stale-canonical-const>)` returning `None`
16763 // far from the drift's commit. Peer of the sibling
16764 // `caixa_multi_word_serde_keys_match_lifted_top_level_key_consts`
16765 // multi-word pin on the same M0 [`Caixa`] top-level
16766 // serialized-key axis, extended here to the single-word arm
16767 // the multi-word test's `rename_all = "camelCase"` sweep can't
16768 // reach (single-word `deps → deps` is a no-op the multi-word
16769 // pin's `\"depsDev\"` / `\"upgradeFrom\"` / `\"maxRestarts\"` /
16770 // `\"restartWindow\"` byte-scan can never observe).
16771 let mut c = Caixa::from_lisp(&Caixa::template("demo")).unwrap();
16772 c.deps = vec![Dep::simple("caixa-core", "^0.1")];
16773 let json = serde_json::to_string(&c).unwrap();
16774 let quoted = format!("\"{}\"", crate::render::CAIXA_KEY_DEPS);
16775 assert!(
16776 json.contains("ed),
16777 "serialized Caixa must carry the lifted top-level `deps` \
16778 byte-sequence {quoted} verbatim in the JSON emission (got: \
16779 {json})",
16780 );
16781 }
16782
16783 #[test]
16784 fn caixa_dep_graph_two_list_key_consts_are_pairwise_distinct() {
16785 // Cross-axis drift-detection pin on the two-list dep-graph
16786 // renderer-side wire-key axis: a future collapse of the
16787 // canonical [`crate::render::CAIXA_KEY_DEPS`] /
16788 // [`crate::render::CAIXA_KEY_DEPS_DEV`] byte-strings onto the
16789 // same value (e.g. an accidental copy-paste flip of
16790 // `CAIXA_KEY_DEPS_DEV` to also read `"deps"`) would silently
16791 // reroute every downstream `Value::get(<key>)` probe on one
16792 // axis onto the sibling axis's dep-list and pass every
16793 // propagation-probe test that expected only the stale axis's
16794 // value — a dev-only dep would land in the runtime closure at
16795 // publish time, or a runtime dep would be excluded from the
16796 // published lacre. Peer of the sibling four-way distinct pin
16797 // on the top-level multi-word tetrad
16798 // (`caixa_top_level_multi_word_key_consts_are_pairwise_distinct`)
16799 // and the two-way pin on the sibling
16800 // [`DEP_AUTHOR_KEY_DEPS`] / [`DEP_AUTHOR_KEY_DEPS_DEV`]
16801 // author-facing arm (4da6fba's test), extended here to the
16802 // renderer-side wire-key arm of the same two-list dep-graph
16803 // axis so both halves of the "one canonical byte-string per
16804 // typed axis per (author, wire)" grid carry the same
16805 // distinct-ness discipline.
16806 assert_ne!(
16807 crate::render::CAIXA_KEY_DEPS,
16808 crate::render::CAIXA_KEY_DEPS_DEV,
16809 "CAIXA_KEY_DEPS and CAIXA_KEY_DEPS_DEV must be distinct \
16810 canonical byte-sequences on the two-list dep-graph \
16811 renderer-side wire-key axis"
16812 );
16813 }
16814
16815 // ── DepList / Caixa::push_dep pin ────────────────────────────────
16816 //
16817 // The compounding pin: the two-arm closed-set typed enum
16818 // [`crate::dep::DepList`] carries the runtime-closure `:deps`
16819 // (`Prod`) vs dev-only-closure `:deps-dev` (`Dev`) dispatch every
16820 // consumer of the top-level manifest's dep-mutation surface reads
16821 // through, and the typed dispatch [`Caixa::push_dep`] on the
16822 // substrate primitive folds the "select list → check within-list
16823 // dup → push" cascade onto one method call. Prior to this landing
16824 // the two axes lived across two `&'static str` constants
16825 // (`DEP_AUTHOR_KEY_DEPS`, `DEP_AUTHOR_KEY_DEPS_DEV`) with no closed-
16826 // set type carrying the pair; the `feira add` mutation site's
16827 // inline `if self.dev { &mut caixa.deps_dev } else { &mut
16828 // caixa.deps }` dispatch expressed no compile-time link back to
16829 // the substrate primitive, and a future third dep-list axis would
16830 // have silently split at every open-coded mutation site.
16831
16832 #[test]
16833 fn dep_list_as_str_routes_through_lifted_author_key_constants() {
16834 // Every arm returns the same `&'static str` the substrate's
16835 // canonical `DEP_AUTHOR_KEY_DEPS` / `DEP_AUTHOR_KEY_DEPS_DEV`
16836 // constants carry. A future rebrand on either constant reaches
16837 // the enum through one edit; a regression to inline literals
16838 // (e.g. `Prod => ":deps"`) would silently split the diagnostic
16839 // quotes from the wire-format constants every consumer routes
16840 // through and this pin flags it at build time.
16841 assert_eq!(
16842 crate::dep::DepList::Prod.as_str(),
16843 crate::render::DEP_AUTHOR_KEY_DEPS
16844 );
16845 assert_eq!(
16846 crate::dep::DepList::Dev.as_str(),
16847 crate::render::DEP_AUTHOR_KEY_DEPS_DEV
16848 );
16849 }
16850
16851 #[test]
16852 fn dep_list_display_routes_through_as_str() {
16853 // Same as-str-through-Display convergence discipline the
16854 // sibling closed-set typed enums carry — a `format!("{list}")`
16855 // call must land byte-for-byte on the accessor's return so a
16856 // future consumer that formats the enum for a diagnostic line
16857 // reaches the same wire-format constant the wire-format
16858 // producers do.
16859 assert_eq!(
16860 format!("{}", crate::dep::DepList::Prod),
16861 crate::dep::DepList::Prod.as_str()
16862 );
16863 assert_eq!(
16864 format!("{}", crate::dep::DepList::Dev),
16865 crate::dep::DepList::Dev.as_str()
16866 );
16867 }
16868
16869 #[test]
16870 fn dep_list_all_enumerates_every_variant_once() {
16871 // Exhaustive-iteration pin — every arm appears exactly once in
16872 // `ALL`, matching the closed set the compiler enforces on the
16873 // sibling `match self` arms. A future variant addition that
16874 // extends only one method's match without extending `ALL`
16875 // would silently drop the new arm from every consumer that
16876 // iterates the slice.
16877 let variants: &[crate::dep::DepList] = crate::dep::DepList::ALL;
16878 assert!(variants.contains(&crate::dep::DepList::Prod));
16879 assert!(variants.contains(&crate::dep::DepList::Dev));
16880 assert_eq!(variants.len(), 2);
16881 }
16882
16883 #[test]
16884 fn dep_list_from_wire_returns_prod_on_deps_wire_scalar() {
16885 // Reverse projection on the two-list dep-graph axis: the
16886 // author-surface wire tag the sibling `as_str` emitter walks
16887 // for `Prod` (`:deps` via `DEP_AUTHOR_KEY_DEPS`) parses back to
16888 // `Some(DepList::Prod)`. A regression that hand-rolled the
16889 // per-arm match without routing through the lifted
16890 // `DEP_AUTHOR_KEY_DEPS` const would silently disagree on any
16891 // future wire-tag rebrand and this pin flags it at build time.
16892 assert_eq!(
16893 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS),
16894 Some(crate::dep::DepList::Prod)
16895 );
16896 }
16897
16898 #[test]
16899 fn dep_list_from_wire_returns_dev_on_deps_dev_wire_scalar() {
16900 // Peer of the `Prod`-arm pin on the dev-only axis: the
16901 // author-surface wire tag the sibling `as_str` emitter walks
16902 // for `Dev` (`:deps-dev` via `DEP_AUTHOR_KEY_DEPS_DEV`) parses
16903 // back to `Some(DepList::Dev)`. Same drift-detection posture
16904 // as the peer arm — the sibling method `match` arms are
16905 // compiler-checked exhaustive so a future variant addition
16906 // trips at build time.
16907 assert_eq!(
16908 crate::dep::DepList::from_wire(crate::render::DEP_AUTHOR_KEY_DEPS_DEV),
16909 Some(crate::dep::DepList::Dev)
16910 );
16911 }
16912
16913 #[test]
16914 fn dep_list_from_wire_returns_none_on_unknown_wire_scalar() {
16915 // Every input outside the closed-set arm-string set the
16916 // sibling `as_str` emitter walks lands on the terminal `None`
16917 // fallback — no silent-accept surface. Sweeps a set of
16918 // plausibly-adjacent scalars (unprefixed wire form, PascalCase
16919 // rebrand candidates, foreign wire tags, empty string) so a
16920 // future variant addition that widened one wire form without
16921 // extending the emitter's arm-set would trip the sibling
16922 // round-trip pin below rather than silently accepting the new
16923 // form here.
16924 for candidate in [
16925 "",
16926 "deps",
16927 "deps-dev",
16928 ":deps ",
16929 ":Deps",
16930 ":DEPS",
16931 ":build-dep",
16932 ":tool-dep",
16933 "prod",
16934 "dev",
16935 ] {
16936 assert_eq!(
16937 crate::dep::DepList::from_wire(candidate),
16938 None,
16939 "from_wire({candidate:?}) must return None; every input outside \
16940 the {{DEP_AUTHOR_KEY_DEPS, DEP_AUTHOR_KEY_DEPS_DEV}} accept-set \
16941 the sibling as_str emitter walks lands on the terminal fallback",
16942 );
16943 }
16944 }
16945
16946 #[test]
16947 fn dep_list_round_trips_through_as_str_and_from_wire() {
16948 // Load-bearing round-trip pin: every arm the `ALL` iteration
16949 // exposes survives the `as_str` → `from_wire` composition
16950 // byte-for-byte. Same discipline the sibling closed-set enums
16951 // carry — `CaixaKind` /
16952 // `RestartStrategy` / `RestartPolicy` /
16953 // `PlacementStrategy` — extended onto the two-list dep-graph
16954 // axis. A future variant addition that extends `ALL` +
16955 // `as_str` without extending `from_wire` (or vice versa)
16956 // trips at build time on this iteration because the compiler
16957 // enforces exhaustiveness on the sibling `match self` arms.
16958 for &list in crate::dep::DepList::ALL {
16959 assert_eq!(
16960 crate::dep::DepList::from_wire(list.as_str()),
16961 Some(list),
16962 "DepList::from_wire(as_str({list:?})) must round-trip to Some({list:?}) — \
16963 a silent split between the forward emitter and the reverse parser \
16964 would drift the two halves of the two-list dep-graph axis's typed dispatch",
16965 );
16966 }
16967 }
16968
16969 #[test]
16970 fn push_dep_routes_to_deps_slot_on_prod_arm() {
16971 // The `Prod` arm dispatches to the runtime-closure `:deps`
16972 // slot every downstream lacre-pipeline consumer resolves at
16973 // build time. A future arm that regressed to inline `&mut
16974 // self.deps_dev` on the `Prod` path would silently reroute
16975 // every runtime dep into the dev-only closure at publish time
16976 // — this pin refuses that regression.
16977 let src = Caixa::template("host");
16978 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
16979 let before_deps = caixa.deps().len();
16980 let before_deps_dev = caixa.deps_dev().len();
16981 let dep = Dep {
16982 nome: "caixa-teia".to_string(),
16983 versao: "^0.1".to_string(),
16984 fonte: None,
16985 opcional: false,
16986 caracteristicas: Vec::new(),
16987 };
16988 caixa
16989 .push_dep(crate::dep::DepList::Prod, dep)
16990 .expect("first push into :deps succeeds");
16991 assert_eq!(caixa.deps().len(), before_deps + 1);
16992 assert_eq!(caixa.deps_dev().len(), before_deps_dev);
16993 assert_eq!(caixa.deps().last().unwrap().nome(), "caixa-teia");
16994 }
16995
16996 #[test]
16997 fn push_dep_routes_to_deps_dev_slot_on_dev_arm() {
16998 // Peer of the sibling `Prod`-arm dispatch pin — the `Dev` arm
16999 // must dispatch to the dev-only-closure `:deps-dev` slot every
17000 // downstream test-facing artifact resolver reads. A future
17001 // regression that inverted the two arms would silently route
17002 // every dev-only dep into the runtime closure at publish time
17003 // and this pin catches it before the drift ships.
17004 let src = Caixa::template("host");
17005 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17006 let dep = Dep {
17007 nome: "tatara-check".to_string(),
17008 versao: "*".to_string(),
17009 fonte: None,
17010 opcional: false,
17011 caracteristicas: Vec::new(),
17012 };
17013 caixa
17014 .push_dep(crate::dep::DepList::Dev, dep)
17015 .expect("first push into :deps-dev succeeds");
17016 assert!(caixa.deps().is_empty());
17017 assert_eq!(caixa.deps_dev().len(), 1);
17018 assert_eq!(caixa.deps_dev().last().unwrap().nome(), "tatara-check");
17019 }
17020
17021 #[test]
17022 fn push_dep_refuses_within_list_duplicate_nome_with_typed_error() {
17023 // Within-list dup check routes through the canonical
17024 // [`DepError::DuplicateNome`] carrier — the substrate's typed
17025 // diagnostic for the same axis [`Caixa::validate_deps`]'s
17026 // parse-time [`crate::render::insert_first_seen`] walk raises
17027 // on. Prior to the lift the mutation site's inline
17028 // `bail!("dep '{}' already declared", …)` string-diagnostic
17029 // path expressed no through-line back to the typed error;
17030 // routing every dep-list refusal through one carrier means an
17031 // author reading a `feira add` refusal and a `feira build`
17032 // refusal reaches for the same corrective surface without
17033 // switching diagnostic idioms.
17034 let src = Caixa::template("host");
17035 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17036 let dep = Dep {
17037 nome: "caixa-teia".to_string(),
17038 versao: "^0.1".to_string(),
17039 fonte: None,
17040 opcional: false,
17041 caracteristicas: Vec::new(),
17042 };
17043 caixa
17044 .push_dep(crate::dep::DepList::Prod, dep.clone())
17045 .expect("first push succeeds");
17046 let dup = Dep {
17047 nome: "caixa-teia".to_string(),
17048 versao: "^0.2".to_string(),
17049 fonte: None,
17050 opcional: false,
17051 caracteristicas: Vec::new(),
17052 };
17053 let err = caixa
17054 .push_dep(crate::dep::DepList::Prod, dup)
17055 .expect_err("second push with same :nome refuses");
17056 assert_eq!(
17057 err,
17058 DepError::DuplicateNome {
17059 nome: "caixa-teia".to_string(),
17060 list: crate::render::DEP_AUTHOR_KEY_DEPS,
17061 }
17062 );
17063 // The refused mutation must not corrupt the target list —
17064 // exactly one entry lives past the refusal, matching the
17065 // canonical single-source-of-truth invariant `Caixa::deps()`
17066 // carries.
17067 assert_eq!(caixa.deps().len(), 1);
17068 }
17069
17070 #[test]
17071 fn push_dep_refuses_dup_on_dev_list_arm_names_deps_dev_key() {
17072 // Peer of the sibling `Prod`-arm dup-refusal pin — the `Dev`
17073 // arm's refusal must carry `DEP_AUTHOR_KEY_DEPS_DEV` in the
17074 // `list` payload so a future author reading the refusal grep's
17075 // for the correct `:deps-dev` block in their `caixa.lisp`,
17076 // not the sibling `:deps` block the runtime closure resolves.
17077 let src = Caixa::template("host");
17078 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17079 let dep = Dep {
17080 nome: "tatara-check".to_string(),
17081 versao: "*".to_string(),
17082 fonte: None,
17083 opcional: false,
17084 caracteristicas: Vec::new(),
17085 };
17086 caixa
17087 .push_dep(crate::dep::DepList::Dev, dep.clone())
17088 .expect("first push succeeds");
17089 let err = caixa
17090 .push_dep(crate::dep::DepList::Dev, dep)
17091 .expect_err("second push with same :nome refuses");
17092 assert!(matches!(
17093 err,
17094 DepError::DuplicateNome {
17095 ref nome,
17096 list,
17097 } if nome == "tatara-check"
17098 && list == crate::render::DEP_AUTHOR_KEY_DEPS_DEV
17099 ));
17100 }
17101
17102 #[test]
17103 fn push_dep_allows_same_nome_across_prod_and_dev_lists() {
17104 // The within-list dup check is scoped to the target arm — a
17105 // caixa may legitimately carry the same `:nome` under both
17106 // `:deps` and `:deps-dev` (though the substrate's peer
17107 // [`crate::Caixa::validate_deps`] walk still refuses the
17108 // shape at parse time; the mutation-site refusal is scoped to
17109 // the mutation-site's list to match the peer parse-time
17110 // per-list [`crate::render::insert_first_seen`] discipline).
17111 // The two arms hold independent seen-sets.
17112 let src = Caixa::template("host");
17113 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17114 let dep_prod = Dep {
17115 nome: "shared".to_string(),
17116 versao: "^0.1".to_string(),
17117 fonte: None,
17118 opcional: false,
17119 caracteristicas: Vec::new(),
17120 };
17121 let dep_dev = Dep {
17122 nome: "shared".to_string(),
17123 versao: "*".to_string(),
17124 fonte: None,
17125 opcional: false,
17126 caracteristicas: Vec::new(),
17127 };
17128 caixa
17129 .push_dep(crate::dep::DepList::Prod, dep_prod)
17130 .expect("push into :deps succeeds");
17131 caixa
17132 .push_dep(crate::dep::DepList::Dev, dep_dev)
17133 .expect("push same :nome into :deps-dev succeeds");
17134 assert_eq!(caixa.deps().len(), 1);
17135 assert_eq!(caixa.deps_dev().len(), 1);
17136 }
17137
17138 #[test]
17139 fn deps_of_prod_returns_the_deps_slot_verbatim() {
17140 // The `Prod` arm of the typed-dispatch [`Caixa::deps_of`] read
17141 // accessor must project onto the runtime-closure `:deps` slot —
17142 // element-equal and length-equal to the sibling per-slot
17143 // [`Caixa::deps`] accessor's return over every per-caixa fixture.
17144 // A future arm that regressed to `self.deps_dev()` on the `Prod`
17145 // path would silently reroute every downstream typed-dispatch
17146 // walker (the [`Caixa::validate_deps`] per-list
17147 // [`crate::render::insert_first_seen`] dedup walk, any future
17148 // per-axis-parametrised consumer) into the sibling dev-only
17149 // closure and this pin refuses that regression.
17150 let src = Caixa::template("host");
17151 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17152 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17153 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 0);
17154 let dep = Dep {
17155 nome: "caixa-teia".to_string(),
17156 versao: "^0.1".to_string(),
17157 fonte: None,
17158 opcional: false,
17159 caracteristicas: Vec::new(),
17160 };
17161 caixa
17162 .push_dep(crate::dep::DepList::Prod, dep.clone())
17163 .expect("push into :deps succeeds");
17164 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod), caixa.deps());
17165 assert_eq!(caixa.deps_of(crate::dep::DepList::Prod).len(), 1);
17166 assert_eq!(
17167 caixa.deps_of(crate::dep::DepList::Prod)[0].nome(),
17168 "caixa-teia"
17169 );
17170 }
17171
17172 #[test]
17173 fn deps_of_dev_returns_the_deps_dev_slot_verbatim() {
17174 // Peer of the sibling `Prod`-arm pin — the `Dev` arm of
17175 // [`Caixa::deps_of`] must project onto the dev-only-closure
17176 // `:deps-dev` slot, element-equal and length-equal to the
17177 // sibling per-slot [`Caixa::deps_dev`] accessor's return. A
17178 // future regression that inverted the two arms would silently
17179 // route every dev-list walker onto the runtime closure and this
17180 // pin catches it before the drift ships.
17181 let src = Caixa::template("host");
17182 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17183 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17184 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 0);
17185 let dep = Dep {
17186 nome: "tatara-check".to_string(),
17187 versao: "*".to_string(),
17188 fonte: None,
17189 opcional: false,
17190 caracteristicas: Vec::new(),
17191 };
17192 caixa
17193 .push_dep(crate::dep::DepList::Dev, dep)
17194 .expect("push into :deps-dev succeeds");
17195 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev), caixa.deps_dev());
17196 assert_eq!(caixa.deps_of(crate::dep::DepList::Dev).len(), 1);
17197 assert_eq!(
17198 caixa.deps_of(crate::dep::DepList::Dev)[0].nome(),
17199 "tatara-check"
17200 );
17201 }
17202
17203 #[test]
17204 fn deps_of_exhaustive_over_dep_list_all_covers_the_two_slots() {
17205 // Composition pin: iterating [`crate::dep::DepList::ALL`] through
17206 // [`Caixa::deps_of`] must land on the same two-slot partition the
17207 // per-slot [`Caixa::deps`] / [`Caixa::deps_dev`] accessors
17208 // expose — the canonical dispatch a future per-axis-parametrised
17209 // walker (a future `feira app graph` per-list dep summary, a
17210 // future M4 per-cluster dev-closure-audit overlay the CR
17211 // materializer resolves per-CR) reads through. Prior to the
17212 // lift the two-block iteration lived open-coded at every walker,
17213 // so a future third dep-list axis (`:deps-build`, per CAIXA-SDLC
17214 // §I) would have had to grow a third block at every consumer.
17215 // A regression that dropped the `Dev` arm from `ALL` would flip
17216 // the collected pairs to `[(":deps", &[])]` alone and this pin
17217 // refuses that shape.
17218 let src = Caixa::template("host");
17219 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17220 let prod_dep = Dep {
17221 nome: "caixa-teia".to_string(),
17222 versao: "^0.1".to_string(),
17223 fonte: None,
17224 opcional: false,
17225 caracteristicas: Vec::new(),
17226 };
17227 let dev_dep = Dep {
17228 nome: "tatara-check".to_string(),
17229 versao: "*".to_string(),
17230 fonte: None,
17231 opcional: false,
17232 caracteristicas: Vec::new(),
17233 };
17234 caixa
17235 .push_dep(crate::dep::DepList::Prod, prod_dep)
17236 .expect("push into :deps succeeds");
17237 caixa
17238 .push_dep(crate::dep::DepList::Dev, dev_dep)
17239 .expect("push into :deps-dev succeeds");
17240 let collected: Vec<(&'static str, usize, &str)> = crate::dep::DepList::ALL
17241 .iter()
17242 .map(|&list| {
17243 let slice = caixa.deps_of(list);
17244 (list.as_str(), slice.len(), slice[0].nome())
17245 })
17246 .collect();
17247 assert_eq!(
17248 collected,
17249 vec![
17250 (crate::render::DEP_AUTHOR_KEY_DEPS, 1, "caixa-teia"),
17251 (crate::render::DEP_AUTHOR_KEY_DEPS_DEV, 1, "tatara-check"),
17252 ]
17253 );
17254 }
17255
17256 #[test]
17257 fn validate_deps_iterates_through_dep_list_all_via_deps_of() {
17258 // Composition pin: the [`Caixa::validate_deps`] parse-time gate
17259 // must route its per-list [`crate::render::insert_first_seen`]
17260 // dedup walk through [`Caixa::deps_of`] + [`crate::dep::DepList::ALL`]
17261 // rather than the pre-lift open-coded two-block iteration over
17262 // `self.deps()` + `self.deps_dev()`. A regression that dropped
17263 // one arm (e.g. hand-inlining `self.deps()` alone) would silently
17264 // stop refusing within-list dups on the sibling arm; a
17265 // regression that flipped the arm-to-list-key mapping
17266 // (`Dev => DEP_AUTHOR_KEY_DEPS`) would silently mislabel the
17267 // diagnostic surface. Both drifts surface here through a paired
17268 // duplicate-name refusal per arm plus an offending-list-key
17269 // check on the emitted [`DepError::DuplicateNome`] carrier.
17270 for &list in crate::dep::DepList::ALL {
17271 let src = Caixa::template("host");
17272 let mut caixa = Caixa::from_lisp(&src).expect("template parses");
17273 let dup = Dep {
17274 nome: "twin".to_string(),
17275 versao: "^0.1".to_string(),
17276 fonte: None,
17277 opcional: false,
17278 caracteristicas: Vec::new(),
17279 };
17280 match list {
17281 crate::dep::DepList::Prod => {
17282 caixa.deps.push(dup.clone());
17283 caixa.deps.push(dup);
17284 }
17285 crate::dep::DepList::Dev => {
17286 caixa.deps_dev.push(dup.clone());
17287 caixa.deps_dev.push(dup);
17288 }
17289 }
17290 let err = caixa
17291 .validate_deps()
17292 .expect_err("within-list duplicate :nome must refuse");
17293 assert_eq!(
17294 err,
17295 DepError::DuplicateNome {
17296 nome: "twin".to_string(),
17297 list: list.as_str(),
17298 },
17299 "validate_deps on {list} arm must emit \
17300 DepError::DuplicateNome carrying the arm's own \
17301 as_str() diagnostic — the arm-to-list-key mapping \
17302 flowed through DepList::ALL + Caixa::deps_of"
17303 );
17304 }
17305 }
17306}