Skip to main content

caixa_core/
dialeto.rs

1//! `defcaixa` is spoken by two unrelated declarations. This module makes that
2//! a **typed fact** instead of an anonymous parse failure.
3//!
4//! # The finding
5//!
6//! Measured 2026-07-31 over the pleme-io org checkout (270 `caixa.lisp` /
7//! `*.caixa.lisp` files found with `rg --no-ignore`; a bare `rg` from the org
8//! root returns 0, which is how this stayed invisible), the corpus splits into
9//! two schemas that share zero required slots:
10//!
11//! * [`CaixaDialeto::Pacote`] — this crate's [`crate::Caixa`]. `:nome
12//!   :versao :kind :deps :bibliotecas :exe :servicos` + the supervisor/mesh
13//!   slots. It declares a **tatara-lisp package**: the thing `feira` resolves,
14//!   builds, links and publishes.
15//! * [`CaixaDialeto::Molde`] — `:name :kind :ecosystem :package {…} :workflows
16//!   […] :ci-config {…} :files […]`. It declares a **repo's generated
17//!   surface**: which foreign ecosystem (rust / go / python / …), that
18//!   ecosystem's own package metadata, the CI shims to emit, and byte-captured
19//!   file bodies. Read by `pleme-doc-gen`, never by `feira`.
20//!
21//! `:package`, `:ecosystem`, `:supports` and `:profile` have no counterpart in
22//! [`crate::Caixa`] at all — the theory doc's own D4 note records the same
23//! thing: those manifests "are authored against a schema that does not exist in
24//! Rust". They are not two spellings of one declaration. They are two domains
25//! that collided on one word, because *caixa* names a box and both are boxes.
26//!
27//! # Why this is not a bug report about broken files
28//!
29//! The Molde-dialect files are not malformed. They are correct inputs to their
30//! own consumer, and nothing in the shipped `feira` reads them, so nothing is
31//! failing today. The hazard is **latent and certain**: any new declarative
32//! surface written against "a `.caixa.lisp` is a [`crate::Caixa`]" meets a
33//! corpus where that is false for the large majority of files, and gets a flat
34//! unknown-keyword rejection that reads as "this manifest is broken" rather
35//! than "this manifest is not yours".
36//!
37//! # What this module does about it
38//!
39//! [`classify`] is total: every `(defcaixa …)` form lands in exactly one
40//! [`CaixaDialeto`], including [`CaixaDialeto::Desconhecido`] for one that
41//! matches neither. [`crate::Caixa::from_lisp`] runs it first, so a foreign
42//! dialect is [`crate::ManifestError::DialetoEstrangeiro`] — an error that
43//! names the dialect it found and the consumer that speaks it — rather than an
44//! unknown-kwarg error indistinguishable from a typo.
45//!
46//! Tier-honest: this is **parse-time rejection with a named cause**, not
47//! unrepresentability. A caller that ignores the `Err` still gets nothing
48//! useful; what it can no longer do is mistake "wrong dialect" for "bad file".
49
50use tatara_lisp::{Atom, Sexp};
51
52/// Which `(defcaixa …)` declaration a source speaks.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub enum CaixaDialeto {
55    /// This crate's [`crate::Caixa`] — a tatara-lisp package manifest.
56    /// Keyword-argument form headed by `:nome`.
57    Pacote,
58    /// `pleme-doc-gen`'s repo-surface declaration, keyword-argument form
59    /// headed by `:name` (plus `:ecosystem` / `:package`).
60    Molde,
61    /// The same declaration as [`Self::Molde`], written with the package name
62    /// as a bare positional symbol — `(defcaixa todoku-go :kind :Biblioteca
63    /// :ecosystem :go …)`. `pleme-doc-gen`'s parser reads the first token
64    /// after the head as the name, so this is one arity of one declaration,
65    /// not a third schema.
66    MoldePosicional,
67    /// A `(defcaixa …)` form matching neither. Kept as a variant rather than
68    /// an error so [`classify`] is total and a census can COUNT the residue —
69    /// a classifier that threw here would report "0 unknown" by construction.
70    Desconhecido,
71}
72
73impl CaixaDialeto {
74    /// Exhaustive iteration surface for every consumer that walks the
75    /// closed four-arm [`CaixaDialeto`] discriminator set — the
76    /// [`feira dialeto`](../../caixa_feira/cmd/dialeto/index.html)
77    /// census counter's per-arm accept-set, a future
78    /// `feira dialeto --list-dialects` CLI listing of the accepted
79    /// classifications, a future M4 `mesh.pleme.io/v1alpha1/Manifesto`
80    /// CR materializer's admission-webhook rejection body naming the
81    /// accepted-dialect set, any future census-report shape probe that
82    /// sweeps every arm to compute per-arm coverage. A future arm
83    /// addition (a fifth dialect the [`crate::dialeto`] module doc's
84    /// "third dialect" hazard actualises — the module explicitly frames
85    /// its purpose as "what stops a third dialect appearing", and this
86    /// slice is the substrate-side answer: the arm-set is one edit and
87    /// every consumer picks up the new entry by construction) extends
88    /// this slice as one edit and every downstream consumer picks up
89    /// the new entry through the shared iteration; the compiler-checked
90    /// exhaustiveness on the sibling method `match` arms
91    /// ([`Self::palavra_canonica`] / [`Self::consumidor`] /
92    /// [`Self::descricao`] / [`std::fmt::Display`]) is the build-time
93    /// guarantee that no arm forgets to grow.
94    ///
95    /// Peer of the sibling closed-set typed enums'
96    /// [`crate::CaixaKind::ALL`] (6b1f4fb) /
97    /// [`crate::aplicacao::PlacementStrategy::ALL`] (18c7342) /
98    /// [`crate::aplicacao::RateLimitUnit::ALL`] (6bce03d) /
99    /// [`crate::dep::DepList::ALL`] (45ee563) /
100    /// [`crate::supervisor::RestartStrategy::ALL`] (4eec29c) /
101    /// [`crate::supervisor::RestartPolicy::ALL`] (dd32ccf)
102    /// exhaustive-iteration surfaces — the seventh closed-set typed
103    /// enum on the caixa surface to converge onto the same
104    /// one-canonical-arm-list-per-enum discipline, and the first
105    /// dialect-classification axis (as distinct from an OTP-shape M2
106    /// slot or an M3 mesh slot) to reach it. Order matches variant
107    /// declaration order verbatim (`Pacote` → `Molde` →
108    /// `MoldePosicional` → `Desconhecido`) so the slice is the
109    /// canonical ordering every listing / rendering consumer defers to.
110    pub const ALL: &'static [Self] = &[
111        Self::Pacote,
112        Self::Molde,
113        Self::MoldePosicional,
114        Self::Desconhecido,
115    ];
116
117    /// Substrate-canonical `PascalCase` variant-name byte-string every consumer
118    /// that formats the dialect as census-facing text lands on. Returns the
119    /// per-arm `PascalCase` name of the variant (`"Pacote"` / `"Molde"` /
120    /// `"MoldePosicional"` / `"Desconhecido"`) — the one canonical
121    /// byte-string the paired [`std::fmt::Display`] impl routes through so
122    /// every downstream consumer (the `feira dialeto` census counter output
123    /// line, a future `feira dialeto --list-dialects` CLI enumeration, a
124    /// future M4 `mesh.pleme.io/v1alpha1/Manifesto` CR materializer's
125    /// admission-webhook rejection body naming the accepted-dialect set)
126    /// reaches for the same substrate primitive rather than the pre-lift
127    /// hand-rolled four-arm literal-string match every [`std::fmt::Display`]
128    /// call previously routed through in place.
129    ///
130    /// Peer of the sibling closed-set typed enums'
131    /// [`crate::CaixaKind::as_str`] / [`crate::supervisor::RestartStrategy::as_str`]
132    /// / [`crate::supervisor::RestartPolicy::as_str`] /
133    /// [`crate::aplicacao::PlacementStrategy::as_str`] /
134    /// [`crate::dep::DepList::as_str`] projections on the sibling closed-set
135    /// typed-enum discriminator axes — the seventh (and last unlifted)
136    /// closed-set fieldless typed enum on the caixa surface to converge
137    /// onto the same one-canonical-byte-string-per-arm-through-`as_str`
138    /// discipline the six siblings already carry. Unlike [`crate::CaixaKind`]
139    /// (which carries two axes: `as_str` returning lowercase Portuguese
140    /// diagnostic form vs `wire_name` returning `PascalCase` tatara-lisp
141    /// author-surface bytes), [`CaixaDialeto`] is an internal
142    /// classification with no wire surface — the `PascalCase` variant name
143    /// is the census-facing form every consumer reads, so `as_str`
144    /// suffices without a paired `wire_name` axis.
145    #[must_use]
146    pub const fn as_str(self) -> &'static str {
147        match self {
148            Self::Pacote => "Pacote",
149            Self::Molde => "Molde",
150            Self::MoldePosicional => "MoldePosicional",
151            Self::Desconhecido => "Desconhecido",
152        }
153    }
154
155    /// The keyword an author should write for this dialect, once the
156    /// migration named in [`Self::consumidor`] completes.
157    #[must_use]
158    pub const fn palavra_canonica(self) -> &'static str {
159        match self {
160            Self::Pacote => "defcaixa",
161            Self::Molde | Self::MoldePosicional => "defmolde",
162            Self::Desconhecido => "?",
163        }
164    }
165
166    /// Who reads this dialect.
167    #[must_use]
168    pub const fn consumidor(self) -> &'static str {
169        match self {
170            Self::Pacote => "caixa-core / feira",
171            Self::Molde | Self::MoldePosicional => "pleme-doc-gen",
172            Self::Desconhecido => "nobody known",
173        }
174    }
175
176    /// A one-line description for a census row or an error message.
177    #[must_use]
178    pub const fn descricao(self) -> &'static str {
179        match self {
180            Self::Pacote => "tatara-lisp package manifest (:nome :versao :kind :deps …)",
181            Self::Molde => "repo-surface declaration (:name :ecosystem :package {…} …)",
182            Self::MoldePosicional => {
183                "repo-surface declaration, positional name (defcaixa <nome> :kind …)"
184            }
185            Self::Desconhecido => "unrecognised — matches no known defcaixa schema",
186        }
187    }
188
189    /// True when this arm belongs to the `defmolde` declaration family —
190    /// the two-arity closure of [`Self::Molde`] and [`Self::MoldePosicional`]
191    /// under the shared `defmolde` head keyword the sibling
192    /// [`Self::palavra_canonica`] projection already collapses onto
193    /// `"defmolde"` for both arms (and the sibling [`Self::consumidor`]
194    /// projection collapses onto `"pleme-doc-gen"` for the same two arms).
195    /// False on [`Self::Pacote`] (the sibling `defcaixa` tatara-lisp
196    /// package manifest, [`Self::palavra_canonica`] `→ "defcaixa"`) and
197    /// on [`Self::Desconhecido`] (the residue that names no known
198    /// declaration, [`Self::palavra_canonica`] `→ "?"`).
199    ///
200    /// The [`Self::Molde`] / [`Self::MoldePosicional`] split is one
201    /// declaration written two ways ([`Self::MoldePosicional`]'s
202    /// variant-declaration docstring at [`Self::MoldePosicional`] frames
203    /// it exactly: "the same declaration as [`Self::Molde`], written with
204    /// the package name as a bare positional symbol … this is one arity
205    /// of one declaration, not a third schema"). Every downstream gate
206    /// that keys off "does this dialect belong to the `defmolde` family"
207    /// (as distinct from the four-arm-per-arm census-counter axis the
208    /// sibling `feira dialeto` verb already fans on separately at
209    /// `caixa-feira/src/cmd/dialeto.rs:110-127`) previously hand-rolled
210    /// the two-arm collapse inline as `matches!(d, CaixaDialeto::Molde |
211    /// CaixaDialeto::MoldePosicional)` — a compile-time-anonymous
212    /// two-arm literal set with no link back to the [`CaixaDialeto`]
213    /// variant declaration nor to the sibling
214    /// [`Self::palavra_canonica`] / [`Self::consumidor`] projections
215    /// that already carry the same two-arm collapse under the shared
216    /// `defmolde` / `pleme-doc-gen` axis. The `feira dialeto` verb's
217    /// [`caixa-feira/src/cmd/dialeto.rs`] carried the same
218    /// `matches!` twice — once in the `--strict-palavra` gate that
219    /// refuses a repo-surface declaration still written as
220    /// `(defcaixa …)`, once in the wrong-declaration-under-`caixa.lisp`
221    /// gate that refuses a repo-surface declaration under the filename
222    /// `feira` loads as a package manifest — with no compile-time link
223    /// between the two hand-rolled arm sets. A future arm addition (the
224    /// module doc's "third dialect" hazard actualises as a fifth arm
225    /// [`CaixaDialeto`] that belongs to the `defmolde` declaration
226    /// family — a third arity variant, an alias-declaration family
227    /// pleme-doc-gen sharpens as its schema evolves) would silently
228    /// split the two hand-rolled `matches!` arm-sets from each other
229    /// and from the paired [`Self::palavra_canonica`] projection: one
230    /// call site picks up the new arm, one does not, and the disagreement
231    /// surfaces far from the arm-addition commit as a `feira dialeto`
232    /// consumer reporting a repo-surface declaration under one gate but
233    /// not the other. Routing every "belongs to the `defmolde` family"
234    /// predicate through this one substrate primitive closes the axis:
235    /// a future arm addition lands one match arm here (a compile-time
236    /// exhaustiveness error otherwise), not a coordinated per-`matches!`
237    /// rewrite across every caller.
238    ///
239    /// Peer of the sibling [`crate::CaixaKind::requires_lib`] (0421c22)
240    /// per-arm-set predicate on the [`crate::CaixaKind`] closed-set
241    /// discriminator's "kind requires a `lib/` surface" axis — extends
242    /// the same "one canonical typed predicate per per-arm-set gate,
243    /// one dispatch on the substrate primitive" discipline onto the
244    /// [`CaixaDialeto`] closed-set discriminator's "belongs to the
245    /// `defmolde` declaration family" axis. The dialect-classification
246    /// axis's second per-arm-set predicate (first being the implicit
247    /// palavra_canonica-through-consumidor-through-descricao arm-set
248    /// collapse already carried on the sibling projections) — the first
249    /// explicitly-typed per-arm-set predicate on the axis, matching the
250    /// discipline the sibling M2 [`crate::CaixaKind`] closed-set
251    /// discriminator already carries with `requires_lib`.
252    ///
253    /// Three consumers now route through this one typed dispatch: the
254    /// [`caixa-feira`](../../caixa_feira/cmd/dialeto/index.html) verb's
255    /// `--strict-palavra` gate (refusing a repo-surface declaration
256    /// still written as `(defcaixa …)`), the same verb's wrong-
257    /// declaration-under-`caixa.lisp` gate (refusing a repo-surface
258    /// declaration under the filename `feira` loads as a package
259    /// manifest), and [`crate::Caixa::from_lisp`]'s foreign-dialect
260    /// gate (raising [`crate::ManifestError::DialetoEstrangeiro`] before
261    /// the derive's `parse_kwargs_strict` walk on any `defmolde`-family
262    /// classification — the pre-lift hand-rolled three-arm
263    /// `match { Pacote => {}, Desconhecido => {}, foreign => Err(…) }`
264    /// literal whose `foreign =>` wildcard silently absorbed anything
265    /// non-Pacote-non-Desconhecido, now the third external consumer of
266    /// the `defmolde`-family partition).
267    #[must_use]
268    pub const fn is_molde_family(self) -> bool {
269        matches!(self, Self::Molde | Self::MoldePosicional)
270    }
271}
272
273/// [`std::fmt::Display`] routed through [`CaixaDialeto::as_str`], so the
274/// pretty-printed byte-string every consumer that formats the dialect as
275/// user-facing / census text lands on (the `feira dialeto` per-manifest
276/// `--list` row, the `feira dialeto` census summary line's per-arm
277/// counters, a future M4 admission-webhook's rejection body naming the
278/// accepted-dialect set) reaches for the same `PascalCase` per-arm
279/// byte-string the [`CaixaDialeto::as_str`] helper returns.
280///
281/// Prior to this lift the [`std::fmt::Display`] impl hand-rolled its own
282/// four-arm literal-string match — the one hand-rolled per-arm dispatch
283/// on the closed [`CaixaDialeto`] discriminator that had NO substrate
284/// primitive accessor to defer to (the sibling [`CaixaDialeto::palavra_canonica`] /
285/// [`CaixaDialeto::consumidor`] / [`CaixaDialeto::descricao`] projections
286/// carry distinct byte-shapes per axis, so none of them could serve as
287/// the Display source). A future variant addition (a fifth dialect the
288/// module doc's "third dialect" hazard actualises) would land one arm at
289/// the enum and per-arm returns at the paired accessors, but a hand-rolled
290/// [`std::fmt::Display`] match would silently drop the new arm to compile-
291/// fail-at-the-match-arm-site rather than through the shared substrate
292/// primitive. Routing [`std::fmt::Display`] through [`CaixaDialeto::as_str`]
293/// closes the last unlifted per-arm `PascalCase`-name projection on the
294/// caixa surface — the seventh (and last unlifted) closed-set fieldless
295/// typed enum on the caixa surface to converge onto the same
296/// `Display`-through-`as_str` discipline the six siblings
297/// ([`crate::CaixaKind`] / [`crate::supervisor::RestartStrategy`] /
298/// [`crate::supervisor::RestartPolicy`] /
299/// [`crate::aplicacao::PlacementStrategy`] / [`crate::aplicacao::RateLimitUnit`]
300/// / [`crate::dep::DepList`]) already carry.
301impl std::fmt::Display for CaixaDialeto {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.write_str(self.as_str())
304    }
305}
306
307/// A source that is not a `(defcaixa …)` / `(defmolde …)` form at all.
308#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
309pub enum DialetoError {
310    #[error("source has no top-level form")]
311    Vazio,
312    #[error("top-level form is not a list — a manifest is `(defcaixa …)`")]
313    NaoEhLista,
314    #[error(
315        "top-level form is headed by `{encontrado}`, not `defcaixa` or `defmolde` \
316         (a manifest's first form must be the declaration itself)"
317    )]
318    CabecaErrada { encontrado: String },
319    #[error("manifest does not parse as tatara-lisp: {0}")]
320    Leitura(String),
321}
322
323/// Classify a manifest source without committing to either schema.
324///
325/// Deliberately reads only the head symbol and the set of top-level keywords —
326/// enough to route, never enough to half-parse. A classifier that started
327/// validating would grow into a third parser, which is the shape of the problem
328/// it exists to name.
329///
330/// # Errors
331/// [`DialetoError`] when the source is not a manifest declaration at all.
332pub fn classify(src: &str) -> Result<CaixaDialeto, DialetoError> {
333    let forms = tatara_lisp::read(src).map_err(|e| DialetoError::Leitura(e.to_string()))?;
334    let first = forms.first().ok_or(DialetoError::Vazio)?;
335    classify_form(first)
336}
337
338/// [`classify`] over an already-read form.
339///
340/// # Errors
341/// [`DialetoError`] when the form is not a manifest declaration.
342pub fn classify_form(form: &Sexp) -> Result<CaixaDialeto, DialetoError> {
343    let list = form.as_list().ok_or(DialetoError::NaoEhLista)?;
344    let head = list
345        .first()
346        .and_then(Sexp::as_symbol)
347        .ok_or(DialetoError::NaoEhLista)?;
348
349    match head {
350        // `defmolde` is unambiguous by construction — it exists precisely so a
351        // consumer never has to infer which declaration it holds. Both arities
352        // are the same declaration; the positional one keeps its own variant
353        // only so a census can report the split.
354        "defmolde" => {
355            return Ok(if starts_with_positional_name(&list[1..]) {
356                CaixaDialeto::MoldePosicional
357            } else {
358                CaixaDialeto::Molde
359            });
360        }
361        "defcaixa" => {}
362        other => {
363            return Err(DialetoError::CabecaErrada {
364                encontrado: other.to_string(),
365            });
366        }
367    }
368
369    let args = &list[1..];
370
371    // `(defcaixa <symbol> :kind … :ecosystem …)`. Only the Molde dialect has a
372    // positional arity; `Caixa` is keyword-only, so a leading bare symbol
373    // settles it without looking further.
374    if starts_with_positional_name(args) {
375        return Ok(CaixaDialeto::MoldePosicional);
376    }
377
378    let keys = top_level_keywords(args);
379    let has = |k: &str| keys.iter().any(|s| s == k);
380
381    // Order matters, and it is not arbitrary: `:nome` and `:name` are the two
382    // required head slots and no file in the measured corpus carries both.
383    // Checking them FIRST means the decision rests on the one slot each schema
384    // makes mandatory, rather than on optional evidence like `:ecosystem`.
385    if has("nome") {
386        return Ok(CaixaDialeto::Pacote);
387    }
388    if has("name") || has("ecosystem") || has("package") {
389        return Ok(CaixaDialeto::Molde);
390    }
391    Ok(CaixaDialeto::Desconhecido)
392}
393
394/// True when the first argument is a bare symbol rather than a keyword — the
395/// positional-name arity.
396fn starts_with_positional_name(args: &[Sexp]) -> bool {
397    matches!(args.first(), Some(Sexp::Atom(Atom::Symbol(_))))
398}
399
400/// The top-level keyword names (without the leading `:`) of a kwarg list.
401///
402/// Steps in pairs so a keyword appearing as a VALUE — `:kind :Biblioteca`, or a
403/// nested `(:nome "dep" :versao "^0.1")` inside `:deps` — is never counted as a
404/// top-level slot. A naive scan for `:nome` anywhere in the source classifies
405/// every Molde manifest with a `:deps` list as a Pacote.
406fn top_level_keywords(args: &[Sexp]) -> Vec<String> {
407    let mut out = Vec::new();
408    let mut i = 0;
409    while i < args.len() {
410        if let Sexp::Atom(Atom::Keyword(k)) = &args[i] {
411            out.push(k.clone());
412            i += 2;
413        } else {
414            i += 1;
415        }
416    }
417    out
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    const PACOTE: &str = r#"
425      (defcaixa
426        :nome   "checkout"
427        :versao "0.1.0"
428        :kind   Servico
429        :deps   ((:nome "caixa-teia" :versao "^0.1")))
430    "#;
431
432    const MOLDE: &str = r#"
433      (defcaixa
434        :name "base64"
435        :kind :Biblioteca
436        :ecosystem :rust-single-crate
437        :package {:name "base64" :version "0.22.1"}
438        :workflows [:auto-release])
439    "#;
440
441    const MOLDE_POSICIONAL: &str = r#"
442      (defcaixa todoku-go
443        :kind :Biblioteca
444        :ecosystem :go
445        :package {:name "todoku-go" :version "0.3.0"})
446    "#;
447
448    #[test]
449    fn the_package_dialect_is_recognised() {
450        assert_eq!(classify(PACOTE), Ok(CaixaDialeto::Pacote));
451    }
452
453    #[test]
454    fn the_repo_surface_dialect_is_recognised() {
455        assert_eq!(classify(MOLDE), Ok(CaixaDialeto::Molde));
456    }
457
458    #[test]
459    fn the_positional_arity_is_recognised() {
460        assert_eq!(
461            classify(MOLDE_POSICIONAL),
462            Ok(CaixaDialeto::MoldePosicional)
463        );
464    }
465
466    #[test]
467    fn defmolde_classifies_without_inference() {
468        // The whole point of the new keyword: no schema sniffing required.
469        let src = r#"(defmolde :name "x" :kind :Biblioteca :ecosystem :go)"#;
470        assert_eq!(classify(src), Ok(CaixaDialeto::Molde));
471        let pos = r"(defmolde todoku-go :kind :Biblioteca :ecosystem :go)";
472        assert_eq!(classify(pos), Ok(CaixaDialeto::MoldePosicional));
473    }
474
475    #[test]
476    fn a_nested_nome_does_not_make_a_repo_surface_look_like_a_package() {
477        // The exact failure a substring scan produces: `:deps ((:nome …))`
478        // contains `:nome`, but not as a top-level slot.
479        let src = r#"
480          (defcaixa
481            :name "x"
482            :ecosystem :rust-single-crate
483            :deps ((:nome "inner" :versao "^0.1")))
484        "#;
485        assert_eq!(classify(src), Ok(CaixaDialeto::Molde));
486    }
487
488    #[test]
489    fn a_keyword_in_value_position_is_not_a_slot() {
490        // `:kind :Biblioteca` — the value is itself a keyword. Stepping one at
491        // a time would read `:Biblioteca` as a top-level slot.
492        let src = r#"(defcaixa :kind :Biblioteca :name "x")"#;
493        assert_eq!(classify(src), Ok(CaixaDialeto::Molde));
494    }
495
496    #[test]
497    fn an_unrecognised_defcaixa_is_reported_not_guessed() {
498        let src = r#"(defcaixa :licenca "MIT")"#;
499        assert_eq!(classify(src), Ok(CaixaDialeto::Desconhecido));
500    }
501
502    #[test]
503    fn a_form_that_is_not_a_manifest_is_an_error_not_a_dialect() {
504        assert_eq!(
505            classify("(defflake :nome \"x\")"),
506            Err(DialetoError::CabecaErrada {
507                encontrado: "defflake".into()
508            })
509        );
510        assert_eq!(classify(""), Err(DialetoError::Vazio));
511    }
512
513    #[test]
514    fn every_dialect_names_its_consumer_and_its_canonical_keyword() {
515        // Guards the routing table itself: a new variant added without an arm
516        // here is a compile error in the match, and a variant that claims
517        // `defcaixa` while being read by pleme-doc-gen would re-open the
518        // collision this module closes. Sweeps [`CaixaDialeto::ALL`] rather
519        // than the pre-lift open-coded four-arm literal list — a future arm
520        // addition extends the slice as one edit and this pin picks it up
521        // by construction.
522        for &d in CaixaDialeto::ALL {
523            assert!(!d.descricao().is_empty(), "{d}");
524            assert!(!d.consumidor().is_empty(), "{d}");
525        }
526        assert_eq!(CaixaDialeto::Pacote.palavra_canonica(), "defcaixa");
527        assert_eq!(CaixaDialeto::Molde.palavra_canonica(), "defmolde");
528        assert_ne!(
529            CaixaDialeto::Pacote.palavra_canonica(),
530            CaixaDialeto::Molde.palavra_canonica(),
531            "the two dialects must not share a canonical keyword — that IS the defect"
532        );
533    }
534
535    #[test]
536    fn caixa_dialeto_all_enumerates_every_variant_exactly_once() {
537        // Three-legged exhaustiveness pin, peer of the sibling
538        // `caixa_kind_all_enumerates_every_variant_exactly_once`
539        // (caixa-core/src/kind.rs) /
540        // `restart_strategy_all_enumerates_every_variant_exactly_once`
541        // (caixa-core/src/supervisor.rs) shape.
542        //
543        // 1. arm-count invariant: `ALL.len()` matches the declared arm
544        //    count (four — a fifth arm added without extending `ALL`
545        //    fails this pin at caixa-core test time);
546        // 2. pairwise-distinctness invariant: every variant appears at
547        //    most once in the slice (a duplicate arm would silently
548        //    double-count in the census consumer, so the pin rejects
549        //    duplicates outright);
550        // 3. coverage invariant: every literal `CaixaDialeto::X` is in
551        //    the slice (the compiler-checked exhaustiveness on the peer
552        //    per-arm `match self` in the accessors keeps the enum arm
553        //    set and the `ALL` slice mutually aligned).
554        assert_eq!(
555            CaixaDialeto::ALL.len(),
556            4,
557            "ALL must list every arm exactly once; a fifth arm added \
558             without extending ALL fails this pin — extend ALL alongside \
559             the new variant"
560        );
561
562        let mut seen: Vec<CaixaDialeto> = Vec::new();
563        for &d in CaixaDialeto::ALL {
564            assert!(
565                !seen.contains(&d),
566                "ALL contains a duplicate arm: {d}. Every variant appears \
567                 exactly once — a duplicate would double-count in every \
568                 iteration consumer"
569            );
570            seen.push(d);
571        }
572
573        // Coverage: exhaustively assert every literal variant is somewhere
574        // in the slice. Written as an exhaustive `match` so a future arm
575        // addition fails to compile here (missing match arm) until the
576        // corresponding `assert` is added — the compiler enforces the pin's
577        // completeness rather than a hand-maintained variant list.
578        for variant in [
579            CaixaDialeto::Pacote,
580            CaixaDialeto::Molde,
581            CaixaDialeto::MoldePosicional,
582            CaixaDialeto::Desconhecido,
583        ] {
584            let coverage_probe = match variant {
585                CaixaDialeto::Pacote
586                | CaixaDialeto::Molde
587                | CaixaDialeto::MoldePosicional
588                | CaixaDialeto::Desconhecido => variant,
589            };
590            assert!(
591                CaixaDialeto::ALL.contains(&coverage_probe),
592                "ALL is missing variant {coverage_probe} — extend the slice"
593            );
594        }
595    }
596
597    #[test]
598    fn caixa_dialeto_all_is_const_and_matches_iteration_count() {
599        // Pins the const-ness of the slice at const-fold time. A future
600        // change that promoted `ALL` to a non-const initializer (a lazy-
601        // static, a runtime-computed Vec) would fail to compile here —
602        // the pin locks in the compile-time-known iteration surface
603        // every consumer builds against. Peer of the sibling
604        // `caixa_kind_all_is_const_and_matches_iteration_count` (kind.rs)
605        // / `restart_strategy_all_is_const_and_matches_iteration_count`
606        // (supervisor.rs) shape.
607        const ALL: &[CaixaDialeto] = CaixaDialeto::ALL;
608        assert_eq!(ALL.len(), CaixaDialeto::ALL.len());
609        // Sweep the iterator without collapsing to `.len()` so a future
610        // change to `ALL`'s carrier that decouples `.len()` from the
611        // iteration count (a lazy-computed shape, an alias `impl Iterator`
612        // return, a wrapper newtype) still passes here iff the two agree
613        // arm-for-arm; the `#[allow]` opts this local pin out of the
614        // clippy `iter_count` collapse that would defeat the intent.
615        #[allow(clippy::iter_count)]
616        let iterated = ALL.iter().count();
617        assert_eq!(iterated, CaixaDialeto::ALL.len());
618    }
619
620    #[test]
621    fn caixa_dialeto_all_covers_every_variant_by_display_probe() {
622        // Fanning `Display` over the slice sweeps the paired accessors
623        // ([`CaixaDialeto::palavra_canonica`] / [`CaixaDialeto::consumidor`]
624        // / [`CaixaDialeto::descricao`]) at every arm — every returned
625        // byte-string is non-empty (the accessors' contract). A future
626        // arm added without extending its per-arm `match self` return
627        // would compile-fail at the accessor call inside the loop;
628        // together with the `ALL.len() == 4` pin above, this locks the
629        // accessor arm-set and the `ALL` slice mutually.
630        for &d in CaixaDialeto::ALL {
631            let display_form = d.to_string();
632            assert!(
633                !display_form.is_empty(),
634                "Display must render a non-empty byte-string for every \
635                 arm; empty: {d:?}"
636            );
637            // Consumidor / descricao / palavra-canonica must each surface
638            // a non-empty scalar; every downstream diagnostic consumer
639            // reaches through these accessors.
640            assert!(!d.palavra_canonica().is_empty(), "{d}");
641            assert!(!d.consumidor().is_empty(), "{d}");
642            assert!(!d.descricao().is_empty(), "{d}");
643        }
644    }
645
646    #[test]
647    fn caixa_dialeto_as_str_returns_pascal_case_variant_name() {
648        // Fail-before-pass-after per-arm shape pin: the four
649        // [`CaixaDialeto::as_str`] arms must return the canonical
650        // `PascalCase` byte-string that names the variant. Pre-lift this
651        // byte-string existed only inside the hand-rolled Display impl's
652        // four-arm literal-string match — every consumer that wanted the
653        // `PascalCase` name reached through `format!("{d}")`'s allocation
654        // path. Pinning the four arms explicitly here refuses a future
655        // regression that ever reroutes an arm to a distinct spelling
656        // (`"pacote"` lowercase, `"MoldePositional"` English rebrand,
657        // `"Unknown"` for `Desconhecido`) — the census output and the
658        // typed accessor would silently disagree until a downstream
659        // consumer surfaced the drift at census time. Peer of the sibling
660        // [`crate::supervisor::tests::restart_strategy_variants_serialize_to_lifted_scalar_values`]
661        // / `placement_strategy_variants_serialize_to_lifted_scalar_values`
662        // / `caixa_kind_as_str_returns_lifted_peer_const` shape on the
663        // sibling closed-set typed-enum discriminator axes — the seventh
664        // (and last unlifted) closed-set typed enum on the caixa surface
665        // to converge onto the same per-arm-shape-pin discipline.
666        for (variant, expected) in [
667            (CaixaDialeto::Pacote, "Pacote"),
668            (CaixaDialeto::Molde, "Molde"),
669            (CaixaDialeto::MoldePosicional, "MoldePosicional"),
670            (CaixaDialeto::Desconhecido, "Desconhecido"),
671        ] {
672            assert_eq!(
673                variant.as_str(),
674                expected,
675                "CaixaDialeto::{variant:?}.as_str() must return the \
676                 canonical `PascalCase` variant-name byte-string; drift here \
677                 splits the census-facing text from the substrate \
678                 primitive every downstream consumer will read"
679            );
680        }
681    }
682
683    #[test]
684    fn caixa_dialeto_display_routes_through_as_str_helper() {
685        // Fail-before-pass-after convergence pin: for every arm in
686        // [`CaixaDialeto::ALL`], the [`std::fmt::Display`] rendered form
687        // must byte-equal [`CaixaDialeto::as_str`]'s return value. Pre-
688        // lift these two paths were structurally independent — the
689        // Display impl hand-rolled its own four-arm literal-string
690        // match with no compile-time link back to any substrate accessor
691        // — so a future variant rename could land at `Display` without
692        // touching a paired accessor (or vice versa), silently splitting
693        // the two paths on the renamed arm. Pinning the byte-equality
694        // here makes any such split a caixa-core build-time failure at
695        // this test rather than surfacing far from the rename commit as
696        // a downstream census consumer emitting one spelling while the
697        // typed accessor returned another. Peer of the sibling
698        // [`crate::kind::tests::caixa_kind_display_routes_through_as_str_helper`]
699        // (which pins the same convergence on the [`crate::CaixaKind`]
700        // closed-set axis) — extends the discipline onto the seventh
701        // (and last unlifted) closed-set fieldless typed enum on the
702        // caixa surface.
703        for &variant in CaixaDialeto::ALL {
704            assert_eq!(
705                variant.to_string(),
706                variant.as_str(),
707                "CaixaDialeto::{variant:?} Display must route through \
708                 CaixaDialeto::as_str (single source of truth: the \
709                 lifted per-arm `PascalCase` variant-name byte-string)"
710            );
711        }
712    }
713
714    #[test]
715    fn caixa_dialeto_is_molde_family_returns_true_on_molde_and_positional_arms() {
716        // Fail-before-pass-after per-arm shape pin on the two `defmolde`
717        // declaration-family arms: [`CaixaDialeto::is_molde_family`] must
718        // return `true` for [`CaixaDialeto::Molde`] and
719        // [`CaixaDialeto::MoldePosicional`] — the two-arity closure of
720        // one declaration ([`CaixaDialeto::MoldePosicional`]'s docstring:
721        // "same declaration as [`Self::Molde`], written with the package
722        // name as a bare positional symbol … one arity of one
723        // declaration, not a third schema"). A future accidental flip that
724        // reversed a per-arm arm's return without touching the paired
725        // false-arm pin would silently open the substrate primitive to
726        // false-positive on either arm — the `feira dialeto` verb's
727        // `--strict-palavra` gate would then silently accept
728        // repo-surface declarations under `(defcaixa …)` on one arm and
729        // reject them on the other. Pinning the two true arms explicitly
730        // here refuses that split at caixa-core build time.
731        assert!(
732            CaixaDialeto::Molde.is_molde_family(),
733            "CaixaDialeto::Molde.is_molde_family() must return true — \
734             Molde is the primary `defmolde` arm"
735        );
736        assert!(
737            CaixaDialeto::MoldePosicional.is_molde_family(),
738            "CaixaDialeto::MoldePosicional.is_molde_family() must return \
739             true — MoldePosicional is the positional-arity form of the \
740             same `defmolde` declaration Molde carries"
741        );
742    }
743
744    #[test]
745    fn caixa_dialeto_is_molde_family_returns_false_on_pacote_and_desconhecido_arms() {
746        // Fail-before-pass-after per-arm shape pin on the two non-`defmolde`
747        // arms: [`CaixaDialeto::is_molde_family`] must return `false` for
748        // [`CaixaDialeto::Pacote`] (the sibling `defcaixa` tatara-lisp
749        // package manifest, `palavra_canonica → "defcaixa"`) and for
750        // [`CaixaDialeto::Desconhecido`] (the residue that names no
751        // known declaration, `palavra_canonica → "?"`). Pinning the two
752        // false arms explicitly here refuses a future accidental flip
753        // that let the predicate widen to include either arm — the
754        // `feira dialeto` verb's `--strict-palavra` gate would then
755        // spuriously refuse every `(defcaixa …)` package manifest as if
756        // it were a repo-surface declaration.
757        assert!(
758            !CaixaDialeto::Pacote.is_molde_family(),
759            "CaixaDialeto::Pacote.is_molde_family() must return false — \
760             Pacote is the `defcaixa` tatara-lisp package manifest, not \
761             the `defmolde` repo-surface declaration"
762        );
763        assert!(
764            !CaixaDialeto::Desconhecido.is_molde_family(),
765            "CaixaDialeto::Desconhecido.is_molde_family() must return \
766             false — the residue arm names no known declaration; it is \
767             not silently promoted into the `defmolde` family"
768        );
769    }
770
771    #[test]
772    fn caixa_dialeto_is_molde_family_agrees_with_palavra_canonica_defmolde_projection() {
773        // Load-bearing pin: for every arm in [`CaixaDialeto::ALL`], the
774        // typed [`CaixaDialeto::is_molde_family`] predicate must agree
775        // byte-for-byte with the paired [`CaixaDialeto::palavra_canonica`]
776        // projection's `== "defmolde"` classifier — i.e. the two paths
777        // partition the four-arm discriminator set into the same
778        // `{Molde, MoldePosicional}` and `{Pacote, Desconhecido}` halves.
779        // Pre-lift the sibling [`CaixaDialeto::palavra_canonica`] projection
780        // (which returns `"defmolde"` for `Molde | MoldePosicional`,
781        // `"defcaixa"` for `Pacote`, `"?"` for `Desconhecido`) was the
782        // only substrate-side surface carrying the two-arm collapse; the
783        // hand-rolled `matches!(d, CaixaDialeto::Molde |
784        // CaixaDialeto::MoldePosicional)` sites in the `feira dialeto`
785        // verb expressed no compile-time link back to it. A future arm
786        // addition — the module doc's "third dialect" hazard actualises
787        // as a fifth arm belonging to the `defmolde` family — would land
788        // one match arm at [`Self::palavra_canonica`]'s `defmolde` return
789        // (extending the sibling projection) but silently split the
790        // hand-rolled two-arm `matches!` predicate sites if the new arm's
791        // `is_molde_family` return were forgotten. Pinning byte-equality
792        // between the two paths here makes any such split a caixa-core
793        // build-time failure at this test rather than surfacing far from
794        // the arm-addition commit as a downstream `--strict-palavra` /
795        // `caixa.lisp`-holds-wrong-declaration gate silently ignoring the
796        // new arm.
797        for &d in CaixaDialeto::ALL {
798            let via_palavra_canonica = d.palavra_canonica() == "defmolde";
799            let via_is_molde_family = d.is_molde_family();
800            assert_eq!(
801                via_is_molde_family, via_palavra_canonica,
802                "CaixaDialeto::{d:?}.is_molde_family() ({via_is_molde_family}) \
803                 must agree with CaixaDialeto::{d:?}.palavra_canonica() == \
804                 \"defmolde\" ({via_palavra_canonica}) — a split between the \
805                 typed predicate and the sibling keyword projection would let \
806                 a future arm addition land at one path and drift at the other, \
807                 which is exactly the drift this pin refuses"
808            );
809        }
810    }
811
812    #[test]
813    fn caixa_dialeto_is_molde_family_is_const_fn() {
814        // Const-context pin: [`CaixaDialeto::is_molde_family`] must remain
815        // `const fn` (its match is a fieldless-arm literal-pattern
816        // discriminator, so no non-const operation exists on the resolution
817        // path). Downstream consumers reaching for the predicate from a
818        // `const` context (a future substrate-wide const-fold-driven audit
819        // table that materializes per-arm gate-membership at build time,
820        // a per-arm CR-admission-webhook gate registration in a `const`
821        // context) rely on the const-ness. A future accidental downgrade
822        // to non-`const` (an added runtime helper reachable only from a
823        // non-`const` context) trips at caixa-core build time rather than
824        // surfacing as a downstream `const`-context regression far from
825        // the predicate declaration. Peer of the sibling
826        // [`caixa_dialeto_as_str_is_const_fn`] pin on the paired
827        // [`CaixaDialeto::as_str`] byte-string axis.
828        const ARMS: [(CaixaDialeto, bool); 4] = [
829            (CaixaDialeto::Pacote, CaixaDialeto::Pacote.is_molde_family()),
830            (CaixaDialeto::Molde, CaixaDialeto::Molde.is_molde_family()),
831            (
832                CaixaDialeto::MoldePosicional,
833                CaixaDialeto::MoldePosicional.is_molde_family(),
834            ),
835            (
836                CaixaDialeto::Desconhecido,
837                CaixaDialeto::Desconhecido.is_molde_family(),
838            ),
839        ];
840        // Materialize the const-fold-evaluated table into a runtime slice
841        // assertion — carries the same `bool = const fn call` shape a raw
842        // `assert!(const_bool)` would, without tripping the
843        // `assertions_on_constants` clippy lint that a per-arm
844        // `assert!(CONST)` on a `const bool` triggers when the arm-count
845        // is enumerated flat rather than compared as a whole-table shape.
846        assert_eq!(
847            ARMS,
848            [
849                (CaixaDialeto::Pacote, false),
850                (CaixaDialeto::Molde, true),
851                (CaixaDialeto::MoldePosicional, true),
852                (CaixaDialeto::Desconhecido, false),
853            ],
854            "CaixaDialeto::is_molde_family() must evaluate in const context \
855             for every arm and land on the {{false, true, true, false}} \
856             partition — a future accidental downgrade to non-`const` \
857             would trip the const-context array-initializer here"
858        );
859    }
860
861    #[test]
862    fn caixa_dialeto_as_str_is_const_fn() {
863        // Const-context pin: [`CaixaDialeto::as_str`] must remain
864        // `const fn` (its match arms return `pub const` byte-strings, so
865        // no non-const operation exists on the resolution path).
866        // Downstream consumers reaching for the accessor from a `const`
867        // context (a future substrate-wide const-fold-driven audit table
868        // that materializes every dialect's census label at build time,
869        // a per-arm CR-admission-webhook message registration in a
870        // `const` gate) rely on the const-ness. A future accidental
871        // downgrade to non-`const` (an added runtime helper reachable
872        // only from a non-`const` context, a manual hand-rolled `impl`
873        // that shadows this method) trips at caixa-core build time
874        // rather than surfacing as a downstream `const`-context
875        // regression far from the accessor declaration. Peer of the
876        // sibling [`crate::kind::tests::caixa_kind_wire_name_is_const_fn`]
877        // pin on the paired [`crate::CaixaKind`] byte-string axis.
878        const PACOTE: &str = CaixaDialeto::Pacote.as_str();
879        const MOLDE: &str = CaixaDialeto::Molde.as_str();
880        const MOLDE_POSICIONAL: &str = CaixaDialeto::MoldePosicional.as_str();
881        const DESCONHECIDO: &str = CaixaDialeto::Desconhecido.as_str();
882        assert_eq!(PACOTE, "Pacote");
883        assert_eq!(MOLDE, "Molde");
884        assert_eq!(MOLDE_POSICIONAL, "MoldePosicional");
885        assert_eq!(DESCONHECIDO, "Desconhecido");
886    }
887}