caixa_theme/style.rs
1//! The small semantic-style enum every caixa tool agrees on.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(
6 Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, gen_platform::IsVariant,
7)]
8pub enum Semantic {
9 /// Language keywords — `defcaixa`, `defteia`, `let`, `lambda`, etc.
10 Keyword,
11 /// Non-keyword symbols — identifiers, function names, variant names.
12 Symbol,
13 /// `:keyword-positioned` atoms.
14 KeywordArg,
15 /// `"string literals"`.
16 String,
17 /// `42`, `3.14`.
18 Number,
19 /// `#t`, `#f`, `nil`.
20 Literal,
21 /// `; comments`.
22 Comment,
23 /// Primary accent — useful for highlights, carets, focused tokens.
24 Accent,
25 /// Dim text — metadata, line numbers, help text.
26 Muted,
27
28 // Diagnostic severities.
29 Error,
30 Warning,
31 Info,
32 Hint,
33
34 // Diff decorations — used by formatter preview and lint output.
35 Added,
36 Removed,
37 Unchanged,
38}
39
40impl Semantic {
41 /// Every variant of [`Semantic`] in declaration order.
42 ///
43 /// The single canonical arm-list every substrate consumer that has
44 /// to walk the closed 15-arm semantic-style partition (the two
45 /// theme overlays' exhaustive-match resolver functions
46 /// `blackmatter_dark_color` / `blackmatter_light_color` in
47 /// [`crate::blackmatter`], the future LSP-side per-Semantic
48 /// `SemanticTokenType` dispatch at
49 /// `caixa-lsp/src/main.rs`, a future
50 /// `feira lint --list-styles` operator-facing enumeration verb)
51 /// reads for. Peer of the sibling closed-set fieldless typed
52 /// enums' `ALL` slices already carried by
53 /// [`caixa_core::CaixaKind`] /
54 /// [`caixa_core::supervisor::RestartStrategy`] /
55 /// [`caixa_core::supervisor::RestartPolicy`] /
56 /// [`caixa_core::aplicacao::PlacementStrategy`] /
57 /// [`caixa_core::upgrade::UpgradeInstruction`] /
58 /// `caixa_lint::diagnostic::Severity` /
59 /// `caixa_lint::diagnostic::FixSafety` /
60 /// `caixa_arch::InvariantKind` / `caixa_arch::ArchVerdict` /
61 /// `caixa_provedor::FerriteRuntime` closed-set typed-enum
62 /// discriminator axes.
63 pub const ALL: &'static [Self] = &[
64 Self::Keyword,
65 Self::Symbol,
66 Self::KeywordArg,
67 Self::String,
68 Self::Number,
69 Self::Literal,
70 Self::Comment,
71 Self::Accent,
72 Self::Muted,
73 Self::Error,
74 Self::Warning,
75 Self::Info,
76 Self::Hint,
77 Self::Added,
78 Self::Removed,
79 Self::Unchanged,
80 ];
81
82 /// Canonical kebab-case discriminator scalar for this variant — the
83 /// single substrate-primitive `&'static str` projection every
84 /// downstream consumer of the closed 16-arm [`Semantic`] partition
85 /// (a future LSP-side per-Semantic `SemanticTokenType` name-mapping
86 /// dispatch at `caixa-lsp/src/main.rs`, a future
87 /// `feira lint --list-styles` operator-facing enumeration verb, a
88 /// future `caixa.nvim` per-Semantic highlight-group name resolver
89 /// that reaches for a stable kebab identifier per arm, a future
90 /// `blackmatter-shell` per-arm classname the terminal emitter
91 /// composes into a `data-semantic="<kebab>"` attribute) reaches
92 /// through. Kebab-case matches the peer
93 /// [`gen_platform::IsVariant`]-derived kebab discriminant convention
94 /// the sibling closed-set typed enums
95 /// ([`caixa_core::CaixaKind::as_str`],
96 /// [`caixa_core::supervisor::RestartStrategy::as_str`],
97 /// [`caixa_core::supervisor::RestartPolicy::as_str`],
98 /// [`caixa_core::aplicacao::PlacementStrategy::as_str`],
99 /// [`caixa_core::upgrade::UpgradeInstruction::as_str`],
100 /// `caixa_lint::diagnostic::Severity::as_str`,
101 /// `caixa_lint::diagnostic::FixSafety::as_str`,
102 /// `caixa_arch::InvariantKind::as_str`,
103 /// `caixa_arch::ArchVerdict::as_str`,
104 /// `caixa_provedor::FerriteRuntime::variant_slug`) already emit
105 /// on their canonical `&'static str` projection axis.
106 ///
107 /// The 16 arms return the kebab-case forms of their `PascalCase`
108 /// variant names (`Keyword` → `"keyword"`, `KeywordArg` →
109 /// `"keyword-arg"`, `Unchanged` → `"unchanged"`, etc.), matching
110 /// the peer closed-set typed-enum canonical byte-string conventions.
111 ///
112 /// Peer of the [`std::fmt::Display`] and [`AsRef<str>`] impls on
113 /// this enum, which both route through this accessor so
114 /// `format!("{s}")`, `s.as_str()`, and
115 /// `<Semantic as AsRef<str>>::as_ref(&s)` resolve to the same
116 /// per-arm byte-string.
117 #[must_use]
118 pub const fn as_str(self) -> &'static str {
119 match self {
120 Self::Keyword => "keyword",
121 Self::Symbol => "symbol",
122 Self::KeywordArg => "keyword-arg",
123 Self::String => "string",
124 Self::Number => "number",
125 Self::Literal => "literal",
126 Self::Comment => "comment",
127 Self::Accent => "accent",
128 Self::Muted => "muted",
129 Self::Error => "error",
130 Self::Warning => "warning",
131 Self::Info => "info",
132 Self::Hint => "hint",
133 Self::Added => "added",
134 Self::Removed => "removed",
135 Self::Unchanged => "unchanged",
136 }
137 }
138
139 /// Reverse projection on the [`Semantic`] closed 16-arm enum's
140 /// canonical kebab-tag axis — parses a `"keyword"` / `"symbol"` /
141 /// `"keyword-arg"` / `"string"` / `"number"` / `"literal"` /
142 /// `"comment"` / `"accent"` / `"muted"` / `"error"` / `"warning"` /
143 /// `"info"` / `"hint"` / `"added"` / `"removed"` / `"unchanged"` wire
144 /// byte-string back to the typed enum, or returns `None` when `s`
145 /// lies outside the 16-arm accept-set [`Self::as_str`] emits. The
146 /// single `&str → Self` projection every future re-entry point on
147 /// the caixa-theme semantic-style axis dispatches through (a future
148 /// `feira lint --list-styles` operator-facing enumeration verb
149 /// hydrating a per-arm row from a stored kebab identifier back to
150 /// the typed enum before rendering, a future `caixa-lsp`-side
151 /// per-`SemanticTokenType` re-parse binding a prior
152 /// [`Self::as_str`] output back to the typed enum for
153 /// per-Semantic-highlight dispatch, a future `caixa.nvim` per-
154 /// Semantic highlight-group resolver re-loading a stored kebab
155 /// identifier back to the typed enum, a future `blackmatter-shell`
156 /// per-arm classname reverse-lookup binding a
157 /// `data-semantic="<kebab>"` DOM attribute back to the typed enum
158 /// for per-arm style dispatch, a `tracing::field::Value::Str`-arm
159 /// structured-log re-loader binding a prior emission's
160 /// [`Self::as_str`] output back to the typed enum for cross-run
161 /// per-Semantic-paint-histogram diff) would have had to re-inline a
162 /// 16-arm `match s` cascade that expressed no compile-time link back
163 /// to the substrate primitive.
164 ///
165 /// Same closed-set-reverse-projection discipline the sibling
166 /// [`caixa_core::CaixaKind::from_wire`] (2aa6d23),
167 /// [`caixa_core::CaixaDialeto::from_wire`] (d0e65ea),
168 /// [`caixa_core::supervisor::RestartStrategy::from_wire`] (4eec29c),
169 /// [`caixa_core::supervisor::RestartPolicy::from_wire`] (dd32ccf),
170 /// [`caixa_core::aplicacao::PlacementStrategy::from_wire`] (18c7342),
171 /// [`caixa_core::dep::DepList::from_wire`] (45ee563),
172 /// [`caixa_core::render::PathShapeViolation::from_wire`] (aebd9c6),
173 /// `caixa_arch::invariants::InvariantKind::from_wire` (b9e4e61),
174 /// `caixa_arch::report::ArchVerdict::from_wire` (6afe564),
175 /// `caixa_lint::diagnostic::Severity::from_wire` (5afff0e), and
176 /// `caixa_lint::diagnostic::FixSafety::from_wire` (bd505a1) typed
177 /// enums carry on the peer wire-side `str → Self` axes — extends
178 /// the substrate-wide `(as_str, from_wire)` round-trip family onto
179 /// the caixa-theme closed-set fieldless typed-enum axis (the first
180 /// closed-set fieldless typed enum on `caixa-theme` to converge on
181 /// the reverse-projection discipline), matching the same two-way
182 /// `str ↔ Self` round-trip every sibling closed-set enum already
183 /// carries. Method-named `from_wire` (not `from_str`) to match the
184 /// peer shapes verbatim and side-step a
185 /// `clippy::should_implement_trait` lint that a plain `from_str`
186 /// name would otherwise trigger without paired
187 /// [`std::str::FromStr`] impl scaffolding this axis does not carry
188 /// today. Returns `Option<Self>` (rather than `Result<Self, _>`) to
189 /// match the peer shapes: the caller picks the diagnostic form
190 /// appropriate for its use site (a `feira lint --list-styles` CLI
191 /// arg-parse renders its own per-verb error message; a future
192 /// admission-webhook rejection body wraps the `None` outcome with
193 /// the accepted-set enumeration `Semantic::ALL.iter().map(…)` for
194 /// operator diagnostics).
195 ///
196 /// Pinned load-bearing at the substrate-primitive level by
197 /// [`tests::semantic_from_wire_accepts_every_as_str_output`]
198 /// (round-trip witness against the peer [`Self::as_str`] axis) and
199 /// [`tests::semantic_from_wire_rejects_unknown_byte_strings`]
200 /// (rejection witness against silent accept-set widening).
201 #[must_use]
202 pub fn from_wire(s: &str) -> Option<Self> {
203 match s {
204 "keyword" => Some(Self::Keyword),
205 "symbol" => Some(Self::Symbol),
206 "keyword-arg" => Some(Self::KeywordArg),
207 "string" => Some(Self::String),
208 "number" => Some(Self::Number),
209 "literal" => Some(Self::Literal),
210 "comment" => Some(Self::Comment),
211 "accent" => Some(Self::Accent),
212 "muted" => Some(Self::Muted),
213 "error" => Some(Self::Error),
214 "warning" => Some(Self::Warning),
215 "info" => Some(Self::Info),
216 "hint" => Some(Self::Hint),
217 "added" => Some(Self::Added),
218 "removed" => Some(Self::Removed),
219 "unchanged" => Some(Self::Unchanged),
220 _ => None,
221 }
222 }
223}
224
225/// [`std::fmt::Display`] routed through [`Semantic::as_str`], so the
226/// pretty-printed byte-string every consumer that formats the semantic
227/// style as user-facing text lands on (a future `feira lint --list-styles`
228/// operator-facing enumeration verb's per-arm line, a future
229/// `caixa-lsp` diagnostic-source line naming the offending semantic,
230/// a future `caixa.nvim` per-Semantic highlight-group name emitter,
231/// a `tracing::field::display(&sem)` structured-log recorder on the
232/// paint-side emit path) reaches for the same lifted kebab-case
233/// per-arm byte-string [`Semantic::as_str`] returns.
234///
235/// Peer of the sibling [`std::fmt::Display`] impls on the closed-set
236/// typed enums the substrate carries — [`caixa_core::CaixaKind`],
237/// [`caixa_core::supervisor::RestartStrategy`],
238/// [`caixa_core::supervisor::RestartPolicy`],
239/// [`caixa_core::aplicacao::PlacementStrategy`],
240/// [`caixa_core::upgrade::UpgradeInstruction`],
241/// `caixa_lint::diagnostic::Severity`,
242/// `caixa_lint::diagnostic::FixSafety`,
243/// `caixa_arch::InvariantKind`, `caixa_arch::ArchVerdict`,
244/// `caixa_provedor::FerriteRuntime` — extended to the second-to-last
245/// un-lifted `caixa-theme` closed-set fieldless typed enum on the
246/// substrate-wide `(as_str, AsRef<str>, Display)` canonical-projection
247/// triple ratchet the prior lifts converged onto.
248impl std::fmt::Display for Semantic {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 f.write_str(self.as_str())
251 }
252}
253
254/// Substrate-canonical [`AsRef<str>`] projection on the caixa-theme
255/// [`Semantic`] closed-set fieldless typed enum — routes through the
256/// same [`Semantic::as_str`] `pub const fn` scalar accessor the paired
257/// [`std::fmt::Display`] impl already reaches for.
258///
259/// Peer of the sibling [`AsRef<str>`] impls the substrate carries on
260/// [`caixa_core::CaixaKind`], [`caixa_core::CaixaVersion`],
261/// [`caixa_core::CaixaDialeto`], [`caixa_core::dep::DepList`],
262/// [`caixa_core::supervisor::RestartStrategy`],
263/// [`caixa_core::supervisor::RestartPolicy`],
264/// [`caixa_core::aplicacao::PlacementStrategy`],
265/// [`caixa_core::aplicacao::RateLimitUnit`],
266/// `caixa_lint::diagnostic::Severity`,
267/// `caixa_arch::InvariantKind`, `caixa_arch::ArchVerdict`,
268/// `caixa_provedor::FerriteRuntime` — extends the substrate-wide
269/// `(as_str, AsRef<str>, Display)` canonical-projection triple onto
270/// the caixa-theme closed-set typed-enum axis, so a future consumer
271/// bound through the trait-idiomatic `.as_ref()` (a
272/// `HashMap::get::<str>(sem.as_ref())` per-Semantic style-lookup, a
273/// future `caixa-lsp` `SemanticTokenType::new(sem.as_ref())`
274/// registration site, any `impl AsRef<str>`-bound generic function)
275/// reaches the same kebab byte-string [`Semantic::as_str`] returns
276/// rather than an open-coded `.as_str()` projection at every
277/// wire-up.
278impl AsRef<str> for Semantic {
279 fn as_ref(&self) -> &str {
280 self.as_str()
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn semantic_all_enumerates_every_variant_in_declaration_order() {
290 // Fail-before-pass-after pin on the [`Semantic::ALL`] slice:
291 // the slice must list every one of the 15 variants in
292 // declaration order (Keyword → Symbol → KeywordArg → String →
293 // Number → Literal → Comment → Accent → Muted → Error →
294 // Warning → Info → Hint → Added → Removed → Unchanged). Peer
295 // of the sibling ALL slices on the closed-set typed-enum
296 // discriminator axes ([`caixa_core::CaixaKind::ALL`],
297 // [`caixa_core::supervisor::RestartStrategy::ALL`],
298 // [`caixa_core::supervisor::RestartPolicy::ALL`],
299 // [`caixa_core::aplicacao::PlacementStrategy::ALL`],
300 // [`caixa_core::upgrade::UpgradeInstruction::ALL`]). A future
301 // arm addition (a `Namespace` tier between `Symbol` and
302 // `KeywordArg` for the M4 tatara-lisp module system's
303 // qualified-name semantic-token dispatch, a `Deleted` tier
304 // for a hard-delete-mark distinct from `Removed` the future
305 // 3-way diff surface grows) that lands the arm on the enum
306 // but forgets to extend `ALL` must trip this pin rather than
307 // surface as a downstream consumer's silently-partial
308 // iteration.
309 assert_eq!(
310 Semantic::ALL,
311 &[
312 Semantic::Keyword,
313 Semantic::Symbol,
314 Semantic::KeywordArg,
315 Semantic::String,
316 Semantic::Number,
317 Semantic::Literal,
318 Semantic::Comment,
319 Semantic::Accent,
320 Semantic::Muted,
321 Semantic::Error,
322 Semantic::Warning,
323 Semantic::Info,
324 Semantic::Hint,
325 Semantic::Added,
326 Semantic::Removed,
327 Semantic::Unchanged,
328 ],
329 );
330 // Also pin the per-arm `IsVariant`-derived partition: every
331 // arm in `ALL` must satisfy exactly one of the 15 generated
332 // arm-discriminator predicates.
333 for variant in Semantic::ALL {
334 let row = [
335 variant.is_keyword(),
336 variant.is_symbol(),
337 variant.is_keyword_arg(),
338 variant.is_string(),
339 variant.is_number(),
340 variant.is_literal(),
341 variant.is_comment(),
342 variant.is_accent(),
343 variant.is_muted(),
344 variant.is_error(),
345 variant.is_warning(),
346 variant.is_info(),
347 variant.is_hint(),
348 variant.is_added(),
349 variant.is_removed(),
350 variant.is_unchanged(),
351 ];
352 let hits = row.iter().filter(|b| **b).count();
353 assert_eq!(
354 hits, 1,
355 "Semantic::{variant:?} must satisfy exactly one of the \
356 15 is_* arm-discriminator predicates; got {row:?}",
357 );
358 }
359 }
360
361 #[test]
362 fn semantic_is_variant_predicates_partition_the_arm_set() {
363 // Fail-before-pass-after pin on the [`gen_platform::IsVariant`]
364 // derive: for each of the 15 variants, exactly one of the
365 // generated is_* predicates returns `true` and the other 14
366 // return `false`. Pre-derive the closed 15-arm partition
367 // lived only inside the two theme overlays' 15-arm match
368 // resolvers; a future rebrand (a `#[is_variant(name = "…")]`
369 // drift, a manual hand-rolled `impl` that shadows the
370 // derive-generated method, an arm rename) trips this pin at
371 // caixa-theme build time rather than surfacing far from the
372 // derive declaration. Peer of the sibling
373 // [`caixa_core::CaixaKind`] `IsVariant` partition pin.
374 // A copy-paste flip that reroutes one arm through the wrong
375 // predicate lane trips at the identity-diagonal assertion,
376 // since each variant's row is generated live from `ALL`'s
377 // declaration order rather than transcribed by hand.
378 for (idx, variant) in Semantic::ALL.iter().enumerate() {
379 let observed: [bool; 16] = [
380 variant.is_keyword(),
381 variant.is_symbol(),
382 variant.is_keyword_arg(),
383 variant.is_string(),
384 variant.is_number(),
385 variant.is_literal(),
386 variant.is_comment(),
387 variant.is_accent(),
388 variant.is_muted(),
389 variant.is_error(),
390 variant.is_warning(),
391 variant.is_info(),
392 variant.is_hint(),
393 variant.is_added(),
394 variant.is_removed(),
395 variant.is_unchanged(),
396 ];
397 let mut expected = [false; 16];
398 expected[idx] = true;
399 assert_eq!(
400 observed, expected,
401 "Semantic::{variant:?} at ALL[{idx}] is_* predicates \
402 must fire only on their own arm lane (identity \
403 diagonal); got {observed:?}",
404 );
405 }
406 }
407
408 #[test]
409 fn semantic_as_str_returns_canonical_kebab_case_per_arm() {
410 // Fail-before-pass-after per-arm byte-string pin on
411 // [`Semantic::as_str`] — the substrate-canonical `&'static str`
412 // projection every downstream consumer of the closed 16-arm
413 // partition reaches through. A future arm rename (a `Symbol` →
414 // `Identifier` rebrand tracking a hypothetical LSP-side
415 // `SemanticTokenType` reshuffle, an `Accent` → `Highlight`
416 // rebrand tracking a `blackmatter-shell` classname rework) that
417 // touches the enum arm but forgets to update the paired
418 // `as_str` arm — or vice versa — trips this pin at caixa-theme
419 // build time rather than surfacing as a downstream
420 // `feira lint --list-styles` operator-facing enumeration verb's
421 // silently-renamed row far from the two-declaration site.
422 //
423 // Kebab-case matches the peer [`gen_platform::IsVariant`]-
424 // derived kebab discriminant convention the sibling closed-set
425 // typed enums already emit on their canonical byte-string
426 // projection axis.
427 assert_eq!(Semantic::Keyword.as_str(), "keyword");
428 assert_eq!(Semantic::Symbol.as_str(), "symbol");
429 assert_eq!(Semantic::KeywordArg.as_str(), "keyword-arg");
430 assert_eq!(Semantic::String.as_str(), "string");
431 assert_eq!(Semantic::Number.as_str(), "number");
432 assert_eq!(Semantic::Literal.as_str(), "literal");
433 assert_eq!(Semantic::Comment.as_str(), "comment");
434 assert_eq!(Semantic::Accent.as_str(), "accent");
435 assert_eq!(Semantic::Muted.as_str(), "muted");
436 assert_eq!(Semantic::Error.as_str(), "error");
437 assert_eq!(Semantic::Warning.as_str(), "warning");
438 assert_eq!(Semantic::Info.as_str(), "info");
439 assert_eq!(Semantic::Hint.as_str(), "hint");
440 assert_eq!(Semantic::Added.as_str(), "added");
441 assert_eq!(Semantic::Removed.as_str(), "removed");
442 assert_eq!(Semantic::Unchanged.as_str(), "unchanged");
443 }
444
445 #[test]
446 fn semantic_as_str_projections_are_all_distinct_across_arms() {
447 // Fail-before-pass-after pin on the injectivity of
448 // [`Semantic::as_str`]'s projection — no two arms may share
449 // their canonical kebab byte-string, since a future consumer
450 // that keys a per-arm dispatch table off the projection (a
451 // `HashMap::<&str, _>::from_iter(Semantic::ALL.iter().map(|s|
452 // (s.as_str(), …)))` style-lookup, a future `caixa-lsp`
453 // `SemanticTokenType::new(sem.as_ref())` registration table
454 // keyed by kebab identifier, a future `feira lint --list-styles`
455 // one-row-per-arm enumeration table) would silently collapse
456 // the two colliding arms onto one entry, dropping the second
457 // insertion. A future arm addition (a `Namespace` tier between
458 // `Symbol` and `KeywordArg` for the M4 tatara-lisp module
459 // system's qualified-name semantic-token dispatch, a `Deleted`
460 // tier for a hard-delete-mark distinct from `Removed` a future
461 // 3-way diff surface grows) that lands the arm on the enum and
462 // reuses a peer arm's kebab identifier (a copy-paste-derived
463 // `"removed"` on the new `Deleted` arm) trips this pin rather
464 // than surfacing far from the arm addition site.
465 let mut projections: Vec<&'static str> = Semantic::ALL.iter().map(|s| s.as_str()).collect();
466 let before = projections.len();
467 projections.sort_unstable();
468 projections.dedup();
469 assert_eq!(
470 projections.len(),
471 before,
472 "Semantic::as_str must be injective across ALL — collisions: \
473 {projections:?}",
474 );
475 }
476
477 #[test]
478 fn semantic_display_and_as_ref_str_route_through_as_str_accessor() {
479 // Fail-before-pass-after three-path convergence pin on the
480 // substrate-wide `(as_str, AsRef<str>, Display)` canonical-
481 // projection triple for the caixa-theme [`Semantic`] closed-set
482 // fieldless typed enum. For every arm in [`Semantic::ALL`], the
483 // paired [`std::fmt::Display`] impl + [`AsRef<str>`] impl + the
484 // substrate-canonical [`Semantic::as_str`] `pub const fn`
485 // scalar accessor must resolve to the same `&'static str`
486 // per arm. Peer of the sibling three-path-convergence pins the
487 // substrate carries on the closed-set typed enums the prior
488 // lifts converged onto
489 // (`restart_strategy_display_routes_through_as_str_helper` /
490 // `restart_strategy_as_ref_str_routes_through_as_str_accessor`
491 // on [`caixa_core::supervisor::RestartStrategy`],
492 // `ferrite_runtime_display_and_as_ref_str_route_through_variant_slug_accessor`
493 // on `caixa_provedor::FerriteRuntime`, and the analogous pins
494 // on `caixa_lint::Severity` / `caixa_lint::FixSafety` /
495 // `caixa_arch::InvariantKind` / `caixa_arch::ArchVerdict`).
496 //
497 // A future accidental split (a hand-rolled `impl fmt::Display`
498 // that shadows this route through a divergent per-arm match, an
499 // `impl AsRef<str>` that returns the compiler-derived `Debug`
500 // string via `format!("{:?}", self)` — allocating and diverging
501 // on every arm — or a `#[serde(rename_all = "…")]` attribute
502 // drift that quietly forks the projection) trips this pin at
503 // caixa-theme build time rather than surfacing as a downstream
504 // consumer's silently-forked per-Semantic dispatch far from the
505 // trait-impl declaration site.
506 for &sem in Semantic::ALL {
507 let via_as_str: &str = sem.as_str();
508 let via_display: String = format!("{sem}");
509 let via_as_ref: &str = <Semantic as AsRef<str>>::as_ref(&sem);
510 assert_eq!(
511 via_display, via_as_str,
512 "Semantic::{sem:?} — Display routes off `as_str`; got \
513 Display={via_display:?} vs as_str={via_as_str:?}",
514 );
515 assert_eq!(
516 via_as_ref, via_as_str,
517 "Semantic::{sem:?} — AsRef<str> routes off `as_str`; got \
518 AsRef={via_as_ref:?} vs as_str={via_as_str:?}",
519 );
520 }
521 }
522
523 #[test]
524 fn semantic_as_str_is_usable_in_const_context() {
525 // The [`Semantic::as_str`] accessor is declared `pub const fn`,
526 // matching the peer closed-set typed enums' canonical
527 // `&'static str` projection accessors
528 // ([`caixa_core::CaixaKind::as_str`],
529 // [`caixa_core::supervisor::RestartStrategy::as_str`],
530 // [`caixa_core::aplicacao::PlacementStrategy::as_str`],
531 // `caixa_provedor::FerriteRuntime::variant_slug`). Pin the
532 // same posture with a `const {}` assertion block so a future
533 // accidental downgrade to non-`const` (an added runtime helper
534 // reachable only from a non-`const` context) trips at
535 // caixa-theme build time rather than surfacing as a downstream
536 // `const`-context regression far from the accessor
537 // declaration.
538 const KEYWORD: &str = Semantic::Keyword.as_str();
539 const ERROR: &str = Semantic::Error.as_str();
540 const UNCHANGED: &str = Semantic::Unchanged.as_str();
541 const { assert!(KEYWORD.as_bytes()[0] == b'k') };
542 const { assert!(ERROR.as_bytes()[0] == b'e') };
543 const { assert!(UNCHANGED.as_bytes()[0] == b'u') };
544 }
545
546 #[test]
547 fn semantic_from_wire_accepts_every_as_str_output() {
548 // Fail-before-pass-after per-arm accept pin on the newly lifted
549 // [`Semantic::from_wire`] reverse projection: every arm in
550 // [`Semantic::ALL`] must parse back through `from_wire` when fed
551 // its own [`Semantic::as_str`] output, landing on
552 // `Some(same_variant)`. A regression that hand-rolled either
553 // side's per-arm match without threading through the shared
554 // 16-string closed set would silently disagree on any future
555 // arm rename (a `Symbol` → `Identifier` rebrand tracking a
556 // hypothetical LSP-side `SemanticTokenType` reshuffle, an
557 // `Accent` → `Highlight` rebrand tracking a `blackmatter-shell`
558 // classname rework) or new arm the theme grows (a `Namespace`
559 // tier between `Symbol` and `KeywordArg` for the M4 tatara-lisp
560 // module system's qualified-name semantic-token dispatch, a
561 // `Deleted` tier for a hard-delete-mark distinct from `Removed`
562 // the future 3-way diff surface grows) and this pin flags it at
563 // caixa-theme build time rather than at a downstream
564 // `feira lint --list-styles` operator-facing enumeration verb's
565 // silent tag misclassification.
566 //
567 // Peer of the sibling
568 // `caixa_lint::diagnostic::tests::severity_from_wire_accepts_every_as_str_output`
569 // (5afff0e) /
570 // `caixa_lint::diagnostic::tests::fix_safety_from_wire_accepts_every_as_str_output`
571 // (bd505a1) /
572 // `caixa_arch::report::tests::arch_verdict_from_wire_accepts_every_as_str_output`
573 // (6afe564) /
574 // `caixa_arch::invariants::tests::invariant_kind_from_wire_accepts_every_as_str_output`
575 // (b9e4e61) round-trip pins on the peer caixa-lint / caixa-arch
576 // closed-set-enum reverse-projection axes, and of the sibling
577 // `caixa_core::kind::tests::caixa_kind_wire_round_trips_through_from_wire`
578 // (2aa6d23) /
579 // `caixa_core::dialeto::tests::caixa_dialeto_from_wire_accepts_every_as_str_output`
580 // (d0e65ea) /
581 // `caixa_core::aplicacao::tests::placement_strategy_from_wire_accepts_every_lifted_constant`
582 // (18c7342) /
583 // `caixa_core::dep::tests::dep_list_round_trips_through_as_str_and_from_wire`
584 // (45ee563) /
585 // `caixa_core::render::tests::path_shape_violation_from_wire_accepts_every_as_str_output`
586 // (aebd9c6) round-trip pins on the sibling caixa-core closed-
587 // set typed-enum reverse-projection axes.
588 for &variant in Semantic::ALL {
589 let wire = variant.as_str();
590 let parsed = Semantic::from_wire(wire).unwrap_or_else(|| {
591 panic!(
592 "Semantic::from_wire({wire:?}) must accept every \
593 Semantic::as_str output — got None for the wire \
594 byte-string of {variant:?}"
595 )
596 });
597 assert_eq!(
598 parsed, variant,
599 "Semantic::from_wire(Semantic::{variant:?}.as_str()) \
600 must return Semantic::{variant:?} — the (as_str, \
601 from_wire) pair must form a total round-trip on the \
602 closed 16-arm Semantic arm-set",
603 );
604 }
605 }
606
607 #[test]
608 fn semantic_from_wire_rejects_unknown_byte_strings() {
609 // Rejection pin on the [`Semantic::from_wire`] parser's accept-
610 // set: any string outside the 16-arm [`Semantic::as_str`] output
611 // set must return `None`. A future accidental widening of the
612 // accept-set (a case-insensitive match that accepts `"KEYWORD"`
613 // / `"Keyword"`, a silent acceptance of the pre-lift PascalCase
614 // Debug-derived shapes `"Keyword"` / `"KeywordArg"` /
615 // `"Unchanged"` on the wire axis, a snake_case drift accepting
616 // `"keyword_arg"` beside the canonical kebab-case
617 // `"keyword-arg"`, a Levenshtein-forgiving arm-lookup that
618 // admits `"kewyord"` typos, a silent absorption of a hypothetical
619 // future `Namespace` / `Deleted` arm before it lands on the enum
620 // and its paired [`Semantic::as_str`] emitter arm) would
621 // silently drift the parser's accept-set from the emitter's — a
622 // downstream style-report re-loader that bound a prior report's
623 // [`Self::as_str`] output back to the typed enum through this
624 // parser would then bind a malformed byte-string to a plausibly-
625 // wrong typed arm the caller does not route through any
626 // fallback, silently misclassifying the reloaded row.
627 //
628 // Peer of the sibling
629 // `caixa_lint::diagnostic::tests::severity_from_wire_rejects_unknown_byte_strings`
630 // (5afff0e) /
631 // `caixa_lint::diagnostic::tests::fix_safety_from_wire_rejects_unknown_byte_strings`
632 // (bd505a1) /
633 // `caixa_arch::report::tests::arch_verdict_from_wire_rejects_unknown_byte_strings`
634 // (6afe564) /
635 // `caixa_arch::invariants::tests::invariant_kind_from_wire_rejects_unknown_byte_strings`
636 // (b9e4e61) rejection pins on the peer caixa-lint / caixa-arch
637 // axes, and of the sibling
638 // `caixa_kind_from_wire_rejects_unknown_byte_strings` (2aa6d23),
639 // `caixa_dialeto_from_wire_rejects_unknown_byte_strings`
640 // (d0e65ea),
641 // `placement_strategy_from_wire_rejects_unknown_byte_strings`
642 // (18c7342),
643 // `dep_list_from_wire_returns_none_on_unknown_wire_scalar`
644 // (45ee563), and
645 // `path_shape_violation_from_wire_rejects_unknown_byte_strings`
646 // (aebd9c6) rejection pins on the sibling caixa-core axes.
647 //
648 // The rejection set also covers overlapping-byte-string tags
649 // from peer axes: caixa-lint `Severity::as_str` outputs
650 // `"error"`/`"warning"`/`"info"`/`"hint"` and caixa-arch
651 // `InvariantKind::as_str` outputs `"safety"`/`"compliance"`
652 // share zero canonical byte-strings with the widened 16-arm
653 // Semantic set here — a widened parser that admitted the peer's
654 // arm on the sibling axis would still not admit an arm foreign
655 // to the caixa-theme semantic-style discriminator's own accept-
656 // set. Yet four peer-axis strings DO overlap with the
657 // caixa-theme set here (`Severity`'s
658 // `"error"`/`"warning"`/`"info"`/`"hint"` map identically onto
659 // the caixa-theme diagnostic-severity sub-region
660 // `Semantic::Error`/`Warning`/`Info`/`Hint`) — a widened parser
661 // that admitted them under a different arm would collapse the
662 // two axes and silently mislabel; the pin excludes those four
663 // from the rejection set precisely because they must accept.
664 for bad in [
665 "",
666 " ",
667 "Keyword",
668 "KEYWORD",
669 "Symbol",
670 "SYMBOL",
671 "KeywordArg",
672 "keyword_arg",
673 "keywordarg",
674 "String",
675 "STRING",
676 "Number",
677 "Literal",
678 "Comment",
679 "Accent",
680 "Muted",
681 "Error",
682 "ERROR",
683 "Warning",
684 "WARNING",
685 "Info",
686 "INFO",
687 "Hint",
688 "HINT",
689 "Added",
690 "ADDED",
691 "Removed",
692 "REMOVED",
693 "Unchanged",
694 "UNCHANGED",
695 "kewyord",
696 "sym",
697 "kw",
698 "str",
699 "num",
700 "lit",
701 "cmt",
702 "safe",
703 "unsafe",
704 "safety",
705 "compliance",
706 "proven",
707 "rejected",
708 "namespace",
709 "deleted",
710 "highlight",
711 "identifier",
712 "keyword ",
713 " keyword",
714 "keyword\n",
715 "keyword\t",
716 "keyword-arg ",
717 " keyword-arg",
718 "added ",
719 " added",
720 "unchanged ",
721 " unchanged",
722 ] {
723 assert!(
724 Semantic::from_wire(bad).is_none(),
725 "Semantic::from_wire({bad:?}) must return None — the \
726 parser's accept-set is exactly the 16 Semantic::as_str \
727 outputs; a widening would silently split the parser's \
728 accept-set from the emitter's arm-set",
729 );
730 }
731 }
732
733 #[test]
734 fn semantic_is_variant_predicates_are_const_fn() {
735 // The [`gen_platform::IsVariant`] derive emits `const fn`
736 // predicates on the peer [`caixa_core::CaixaKind`] /
737 // [`caixa_core::upgrade::UpgradeInstruction`] /
738 // [`caixa_core::supervisor::RestartStrategy`] /
739 // [`caixa_core::supervisor::RestartPolicy`] closed-set typed
740 // enums — pin the same posture on [`Semantic`] so a future
741 // accidental downgrade to non-`const` (an added runtime helper
742 // reachable only from a non-`const` context, a manual hand-
743 // rolled `impl` that shadows the derive-generated method)
744 // trips at caixa-theme build time rather than surfacing as a
745 // downstream `const`-context regression far from the derive
746 // declaration.
747 const { assert!(Semantic::Keyword.is_keyword()) };
748 const { assert!(Semantic::Error.is_error()) };
749 const { assert!(Semantic::Added.is_added()) };
750 const { assert!(Semantic::Unchanged.is_unchanged()) };
751 }
752}