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/// Trait-idiomatic reverse projection on the [`Semantic`] closed 16-arm
285/// caixa-theme semantic-style axis — routes byte-for-byte through the
286/// paired substrate-primitive [`Semantic::from_wire`] `Option<Self>`
287/// accessor so every future consumer that binds a canonical semantic
288/// tag through the standard-library `.try_into()` / [`TryFrom`] axis
289/// (a future `feira lint --semantic=<kebab>` CLI arg-parse that
290/// composes into `let sem: Semantic = s.try_into()?`, a future
291/// `caixa-lsp` per-token re-parse hydrating a prior
292/// [`Semantic::as_str`] output back to the typed enum for
293/// `SemanticTokenType` dispatch, a future `caixa.nvim` per-highlight-
294/// group re-loader binding a stored kebab byte-string back through the
295/// typed enum, a generic `<T: TryFrom<&str>>`-bound theme-overlay
296/// re-loader over any of the substrate's closed-set typed enums)
297/// reaches the same 16-arm accept-set the sibling
298/// [`Semantic::from_wire`] resolver parses through and the sibling
299/// [`Semantic::as_str`] emits, rather than an open-coded per-arm
300/// `match s { "keyword" => …, "symbol" => …, … _ => … }` cascade whose
301/// arm-set has no compile-time link back to the substrate primitive.
302///
303/// Complements the pre-existing forward-projection triple
304/// ([`std::fmt::Display`], [`AsRef<str>`], [`Semantic::as_str`]) with
305/// the paired trait-idiomatic reverse-projection axis: Rust-side
306/// newtype/typed-enum convention pairs [`AsRef<str>`] with either
307/// [`std::str::FromStr`] or [`TryFrom<&str>`] on the same primitive so
308/// a caller who can project *out to* a `&str` can also project *in
309/// from* one. The [`TryFrom<&str>`] axis is deliberately chosen over
310/// [`std::str::FromStr`] to sidestep the `clippy::should_implement_trait`
311/// lint the sibling method-named [`Semantic::from_wire`] would trigger
312/// under a `FromStr` impl (the same design tradeoff the peer
313/// [`caixa_core::CaixaKind`] (3c83606),
314/// [`caixa_core::CaixaDialeto`] (bf33136),
315/// [`caixa_core::aplicacao::PlacementStrategy`] (6fd00cd),
316/// [`caixa_core::supervisor::RestartStrategy`] (5b828ed),
317/// [`caixa_core::supervisor::RestartPolicy`] (6fdd0d9),
318/// [`caixa_core::aplicacao::WitShape`] (5472902),
319/// [`caixa_core::aplicacao::RateLimitUnit`] (bf78400),
320/// [`caixa_core::render::PathShapeViolation`] (e67e48a),
321/// `caixa_arch::invariants::InvariantKind` (e21a857),
322/// `caixa_arch::report::ArchVerdict` (0a4cc45),
323/// `caixa_lint::diagnostic::Severity` (a7bf74c), and
324/// `caixa_lint::diagnostic::FixSafety` (df86c94) blocks note) — this
325/// impl closes the trait-idiomatic reverse axis without disturbing the
326/// method-named `from_wire` shape every peer closed-set typed enum
327/// already carries.
328///
329/// `type Error = ()` matches the sibling [`Semantic::from_wire`]'s
330/// `Option<Self>` return-shape's deliberate deferral of error typing:
331/// the caller picks the diagnostic form appropriate for its use site
332/// (a future `feira lint --semantic` CLI arg-parse composes its own
333/// per-verb "unknown semantic-style tag: <arg> — accepted: {…}"
334/// message enumerating [`Semantic::ALL`], a future admission-webhook
335/// rejection body wraps the `Err(())` outcome with the accepted-set
336/// enumeration for operator diagnostics, a `Result::map_err` at the
337/// call site lifts the axis-error to a per-verb error type). Same
338/// shape the peer sibling reverse-projection axes carry. The return-
339/// shape uses fully-qualified `<Self as TryFrom<&str>>::Error` because
340/// the associated-type name would otherwise collide with the
341/// [`Self::Error`] variant on the same primitive (the identical
342/// ambiguous-associated-item defect the sibling
343/// `impl TryFrom<&str> for Severity` (a7bf74c) already threads around
344/// under `#[deny(future_incompatible)]`).
345///
346/// The paired [`TryFrom<&str>`] impl reaches the same 16-arm accept-
347/// set the [`Semantic::from_wire`] resolver dispatches through, so any
348/// future arm addition (a `Namespace` tier between [`Self::Symbol`]
349/// and [`Self::KeywordArg`] for the M4 tatara-lisp module system's
350/// qualified-name semantic-token dispatch, a `Deleted` tier for a
351/// hard-delete-mark distinct from [`Self::Removed`] the future 3-way
352/// diff surface grows — both trajectory items the sibling
353/// [`Semantic::ALL`] doc block already names) grows the trait-
354/// idiomatic axis by construction: one caixa-theme edit on
355/// [`Semantic::from_wire`] extends both the method-named reverse
356/// projection every existing consumer keys off and the trait-
357/// idiomatic reverse projection this impl exposes, without a
358/// coordinated rewrite across every future `TryFrom<&str>`-bound
359/// consumer's arm-set.
360///
361/// Extends the substrate-wide closed-set-enum trait-idiomatic
362/// reverse-projection family ([`caixa_core::CaixaKind`] via 3c83606,
363/// [`caixa_core::CaixaDialeto`] via bf33136,
364/// [`caixa_core::aplicacao::PlacementStrategy`] via 6fd00cd,
365/// [`caixa_core::supervisor::RestartStrategy`] via 5b828ed,
366/// [`caixa_core::supervisor::RestartPolicy`] via 6fdd0d9,
367/// [`caixa_core::aplicacao::WitShape`] via 5472902,
368/// [`caixa_core::aplicacao::RateLimitUnit`] via bf78400,
369/// [`caixa_core::render::PathShapeViolation`] via e67e48a,
370/// `caixa_arch::invariants::InvariantKind` via e21a857,
371/// `caixa_arch::report::ArchVerdict` via 0a4cc45,
372/// `caixa_lint::diagnostic::Severity` via a7bf74c, and
373/// `caixa_lint::diagnostic::FixSafety` via df86c94) onto the first
374/// closed-set fieldless typed enum on the caixa-theme surface — the
375/// semantic-style 16-arm accept-set every per-Semantic paint dispatch,
376/// every future `caixa-lsp` per-SemanticTokenType wire-up, and every
377/// future `caixa.nvim` per-highlight-group re-loader keys off. The
378/// thirteenth peer on the substrate surface, and the first inside
379/// `caixa-theme`; with `caixa_provedor::FerriteRuntime` remaining as
380/// the only outside-caixa-core closed-set fieldless typed enum whose
381/// trait-idiomatic axis is still open.
382///
383/// Pinned load-bearing by
384/// [`tests::semantic_try_from_str_routes_through_from_wire_accessor`]
385/// (byte-parity pin against [`Semantic::from_wire`] across the 16-arm
386/// accept-set) and
387/// [`tests::semantic_try_from_str_rejects_unknown_byte_strings`]
388/// (rejection witness against silent accept-set widening).
389impl TryFrom<&str> for Semantic {
390 type Error = ();
391
392 fn try_from(s: &str) -> Result<Self, <Self as TryFrom<&str>>::Error> {
393 Self::from_wire(s).ok_or(())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn semantic_all_enumerates_every_variant_in_declaration_order() {
403 // Fail-before-pass-after pin on the [`Semantic::ALL`] slice:
404 // the slice must list every one of the 15 variants in
405 // declaration order (Keyword → Symbol → KeywordArg → String →
406 // Number → Literal → Comment → Accent → Muted → Error →
407 // Warning → Info → Hint → Added → Removed → Unchanged). Peer
408 // of the sibling ALL slices on the closed-set typed-enum
409 // discriminator axes ([`caixa_core::CaixaKind::ALL`],
410 // [`caixa_core::supervisor::RestartStrategy::ALL`],
411 // [`caixa_core::supervisor::RestartPolicy::ALL`],
412 // [`caixa_core::aplicacao::PlacementStrategy::ALL`],
413 // [`caixa_core::upgrade::UpgradeInstruction::ALL`]). A future
414 // arm addition (a `Namespace` tier between `Symbol` and
415 // `KeywordArg` for the M4 tatara-lisp module system's
416 // qualified-name semantic-token dispatch, a `Deleted` tier
417 // for a hard-delete-mark distinct from `Removed` the future
418 // 3-way diff surface grows) that lands the arm on the enum
419 // but forgets to extend `ALL` must trip this pin rather than
420 // surface as a downstream consumer's silently-partial
421 // iteration.
422 assert_eq!(
423 Semantic::ALL,
424 &[
425 Semantic::Keyword,
426 Semantic::Symbol,
427 Semantic::KeywordArg,
428 Semantic::String,
429 Semantic::Number,
430 Semantic::Literal,
431 Semantic::Comment,
432 Semantic::Accent,
433 Semantic::Muted,
434 Semantic::Error,
435 Semantic::Warning,
436 Semantic::Info,
437 Semantic::Hint,
438 Semantic::Added,
439 Semantic::Removed,
440 Semantic::Unchanged,
441 ],
442 );
443 // Also pin the per-arm `IsVariant`-derived partition: every
444 // arm in `ALL` must satisfy exactly one of the 15 generated
445 // arm-discriminator predicates.
446 for variant in Semantic::ALL {
447 let row = [
448 variant.is_keyword(),
449 variant.is_symbol(),
450 variant.is_keyword_arg(),
451 variant.is_string(),
452 variant.is_number(),
453 variant.is_literal(),
454 variant.is_comment(),
455 variant.is_accent(),
456 variant.is_muted(),
457 variant.is_error(),
458 variant.is_warning(),
459 variant.is_info(),
460 variant.is_hint(),
461 variant.is_added(),
462 variant.is_removed(),
463 variant.is_unchanged(),
464 ];
465 let hits = row.iter().filter(|b| **b).count();
466 assert_eq!(
467 hits, 1,
468 "Semantic::{variant:?} must satisfy exactly one of the \
469 15 is_* arm-discriminator predicates; got {row:?}",
470 );
471 }
472 }
473
474 #[test]
475 fn semantic_is_variant_predicates_partition_the_arm_set() {
476 // Fail-before-pass-after pin on the [`gen_platform::IsVariant`]
477 // derive: for each of the 15 variants, exactly one of the
478 // generated is_* predicates returns `true` and the other 14
479 // return `false`. Pre-derive the closed 15-arm partition
480 // lived only inside the two theme overlays' 15-arm match
481 // resolvers; a future rebrand (a `#[is_variant(name = "…")]`
482 // drift, a manual hand-rolled `impl` that shadows the
483 // derive-generated method, an arm rename) trips this pin at
484 // caixa-theme build time rather than surfacing far from the
485 // derive declaration. Peer of the sibling
486 // [`caixa_core::CaixaKind`] `IsVariant` partition pin.
487 // A copy-paste flip that reroutes one arm through the wrong
488 // predicate lane trips at the identity-diagonal assertion,
489 // since each variant's row is generated live from `ALL`'s
490 // declaration order rather than transcribed by hand.
491 for (idx, variant) in Semantic::ALL.iter().enumerate() {
492 let observed: [bool; 16] = [
493 variant.is_keyword(),
494 variant.is_symbol(),
495 variant.is_keyword_arg(),
496 variant.is_string(),
497 variant.is_number(),
498 variant.is_literal(),
499 variant.is_comment(),
500 variant.is_accent(),
501 variant.is_muted(),
502 variant.is_error(),
503 variant.is_warning(),
504 variant.is_info(),
505 variant.is_hint(),
506 variant.is_added(),
507 variant.is_removed(),
508 variant.is_unchanged(),
509 ];
510 let mut expected = [false; 16];
511 expected[idx] = true;
512 assert_eq!(
513 observed, expected,
514 "Semantic::{variant:?} at ALL[{idx}] is_* predicates \
515 must fire only on their own arm lane (identity \
516 diagonal); got {observed:?}",
517 );
518 }
519 }
520
521 #[test]
522 fn semantic_as_str_returns_canonical_kebab_case_per_arm() {
523 // Fail-before-pass-after per-arm byte-string pin on
524 // [`Semantic::as_str`] — the substrate-canonical `&'static str`
525 // projection every downstream consumer of the closed 16-arm
526 // partition reaches through. A future arm rename (a `Symbol` →
527 // `Identifier` rebrand tracking a hypothetical LSP-side
528 // `SemanticTokenType` reshuffle, an `Accent` → `Highlight`
529 // rebrand tracking a `blackmatter-shell` classname rework) that
530 // touches the enum arm but forgets to update the paired
531 // `as_str` arm — or vice versa — trips this pin at caixa-theme
532 // build time rather than surfacing as a downstream
533 // `feira lint --list-styles` operator-facing enumeration verb's
534 // silently-renamed row far from the two-declaration site.
535 //
536 // Kebab-case matches the peer [`gen_platform::IsVariant`]-
537 // derived kebab discriminant convention the sibling closed-set
538 // typed enums already emit on their canonical byte-string
539 // projection axis.
540 assert_eq!(Semantic::Keyword.as_str(), "keyword");
541 assert_eq!(Semantic::Symbol.as_str(), "symbol");
542 assert_eq!(Semantic::KeywordArg.as_str(), "keyword-arg");
543 assert_eq!(Semantic::String.as_str(), "string");
544 assert_eq!(Semantic::Number.as_str(), "number");
545 assert_eq!(Semantic::Literal.as_str(), "literal");
546 assert_eq!(Semantic::Comment.as_str(), "comment");
547 assert_eq!(Semantic::Accent.as_str(), "accent");
548 assert_eq!(Semantic::Muted.as_str(), "muted");
549 assert_eq!(Semantic::Error.as_str(), "error");
550 assert_eq!(Semantic::Warning.as_str(), "warning");
551 assert_eq!(Semantic::Info.as_str(), "info");
552 assert_eq!(Semantic::Hint.as_str(), "hint");
553 assert_eq!(Semantic::Added.as_str(), "added");
554 assert_eq!(Semantic::Removed.as_str(), "removed");
555 assert_eq!(Semantic::Unchanged.as_str(), "unchanged");
556 }
557
558 #[test]
559 fn semantic_as_str_projections_are_all_distinct_across_arms() {
560 // Fail-before-pass-after pin on the injectivity of
561 // [`Semantic::as_str`]'s projection — no two arms may share
562 // their canonical kebab byte-string, since a future consumer
563 // that keys a per-arm dispatch table off the projection (a
564 // `HashMap::<&str, _>::from_iter(Semantic::ALL.iter().map(|s|
565 // (s.as_str(), …)))` style-lookup, a future `caixa-lsp`
566 // `SemanticTokenType::new(sem.as_ref())` registration table
567 // keyed by kebab identifier, a future `feira lint --list-styles`
568 // one-row-per-arm enumeration table) would silently collapse
569 // the two colliding arms onto one entry, dropping the second
570 // insertion. A future arm addition (a `Namespace` tier between
571 // `Symbol` and `KeywordArg` for the M4 tatara-lisp module
572 // system's qualified-name semantic-token dispatch, a `Deleted`
573 // tier for a hard-delete-mark distinct from `Removed` a future
574 // 3-way diff surface grows) that lands the arm on the enum and
575 // reuses a peer arm's kebab identifier (a copy-paste-derived
576 // `"removed"` on the new `Deleted` arm) trips this pin rather
577 // than surfacing far from the arm addition site.
578 let mut projections: Vec<&'static str> = Semantic::ALL.iter().map(|s| s.as_str()).collect();
579 let before = projections.len();
580 projections.sort_unstable();
581 projections.dedup();
582 assert_eq!(
583 projections.len(),
584 before,
585 "Semantic::as_str must be injective across ALL — collisions: \
586 {projections:?}",
587 );
588 }
589
590 #[test]
591 fn semantic_display_and_as_ref_str_route_through_as_str_accessor() {
592 // Fail-before-pass-after three-path convergence pin on the
593 // substrate-wide `(as_str, AsRef<str>, Display)` canonical-
594 // projection triple for the caixa-theme [`Semantic`] closed-set
595 // fieldless typed enum. For every arm in [`Semantic::ALL`], the
596 // paired [`std::fmt::Display`] impl + [`AsRef<str>`] impl + the
597 // substrate-canonical [`Semantic::as_str`] `pub const fn`
598 // scalar accessor must resolve to the same `&'static str`
599 // per arm. Peer of the sibling three-path-convergence pins the
600 // substrate carries on the closed-set typed enums the prior
601 // lifts converged onto
602 // (`restart_strategy_display_routes_through_as_str_helper` /
603 // `restart_strategy_as_ref_str_routes_through_as_str_accessor`
604 // on [`caixa_core::supervisor::RestartStrategy`],
605 // `ferrite_runtime_display_and_as_ref_str_route_through_variant_slug_accessor`
606 // on `caixa_provedor::FerriteRuntime`, and the analogous pins
607 // on `caixa_lint::Severity` / `caixa_lint::FixSafety` /
608 // `caixa_arch::InvariantKind` / `caixa_arch::ArchVerdict`).
609 //
610 // A future accidental split (a hand-rolled `impl fmt::Display`
611 // that shadows this route through a divergent per-arm match, an
612 // `impl AsRef<str>` that returns the compiler-derived `Debug`
613 // string via `format!("{:?}", self)` — allocating and diverging
614 // on every arm — or a `#[serde(rename_all = "…")]` attribute
615 // drift that quietly forks the projection) trips this pin at
616 // caixa-theme build time rather than surfacing as a downstream
617 // consumer's silently-forked per-Semantic dispatch far from the
618 // trait-impl declaration site.
619 for &sem in Semantic::ALL {
620 let via_as_str: &str = sem.as_str();
621 let via_display: String = format!("{sem}");
622 let via_as_ref: &str = <Semantic as AsRef<str>>::as_ref(&sem);
623 assert_eq!(
624 via_display, via_as_str,
625 "Semantic::{sem:?} — Display routes off `as_str`; got \
626 Display={via_display:?} vs as_str={via_as_str:?}",
627 );
628 assert_eq!(
629 via_as_ref, via_as_str,
630 "Semantic::{sem:?} — AsRef<str> routes off `as_str`; got \
631 AsRef={via_as_ref:?} vs as_str={via_as_str:?}",
632 );
633 }
634 }
635
636 #[test]
637 fn semantic_as_str_is_usable_in_const_context() {
638 // The [`Semantic::as_str`] accessor is declared `pub const fn`,
639 // matching the peer closed-set typed enums' canonical
640 // `&'static str` projection accessors
641 // ([`caixa_core::CaixaKind::as_str`],
642 // [`caixa_core::supervisor::RestartStrategy::as_str`],
643 // [`caixa_core::aplicacao::PlacementStrategy::as_str`],
644 // `caixa_provedor::FerriteRuntime::variant_slug`). Pin the
645 // same posture with a `const {}` assertion block so a future
646 // accidental downgrade to non-`const` (an added runtime helper
647 // reachable only from a non-`const` context) trips at
648 // caixa-theme build time rather than surfacing as a downstream
649 // `const`-context regression far from the accessor
650 // declaration.
651 const KEYWORD: &str = Semantic::Keyword.as_str();
652 const ERROR: &str = Semantic::Error.as_str();
653 const UNCHANGED: &str = Semantic::Unchanged.as_str();
654 const { assert!(KEYWORD.as_bytes()[0] == b'k') };
655 const { assert!(ERROR.as_bytes()[0] == b'e') };
656 const { assert!(UNCHANGED.as_bytes()[0] == b'u') };
657 }
658
659 #[test]
660 fn semantic_from_wire_accepts_every_as_str_output() {
661 // Fail-before-pass-after per-arm accept pin on the newly lifted
662 // [`Semantic::from_wire`] reverse projection: every arm in
663 // [`Semantic::ALL`] must parse back through `from_wire` when fed
664 // its own [`Semantic::as_str`] output, landing on
665 // `Some(same_variant)`. A regression that hand-rolled either
666 // side's per-arm match without threading through the shared
667 // 16-string closed set would silently disagree on any future
668 // arm rename (a `Symbol` → `Identifier` rebrand tracking a
669 // hypothetical LSP-side `SemanticTokenType` reshuffle, an
670 // `Accent` → `Highlight` rebrand tracking a `blackmatter-shell`
671 // classname rework) or new arm the theme grows (a `Namespace`
672 // tier between `Symbol` and `KeywordArg` for the M4 tatara-lisp
673 // module system's qualified-name semantic-token dispatch, a
674 // `Deleted` tier for a hard-delete-mark distinct from `Removed`
675 // the future 3-way diff surface grows) and this pin flags it at
676 // caixa-theme build time rather than at a downstream
677 // `feira lint --list-styles` operator-facing enumeration verb's
678 // silent tag misclassification.
679 //
680 // Peer of the sibling
681 // `caixa_lint::diagnostic::tests::severity_from_wire_accepts_every_as_str_output`
682 // (5afff0e) /
683 // `caixa_lint::diagnostic::tests::fix_safety_from_wire_accepts_every_as_str_output`
684 // (bd505a1) /
685 // `caixa_arch::report::tests::arch_verdict_from_wire_accepts_every_as_str_output`
686 // (6afe564) /
687 // `caixa_arch::invariants::tests::invariant_kind_from_wire_accepts_every_as_str_output`
688 // (b9e4e61) round-trip pins on the peer caixa-lint / caixa-arch
689 // closed-set-enum reverse-projection axes, and of the sibling
690 // `caixa_core::kind::tests::caixa_kind_wire_round_trips_through_from_wire`
691 // (2aa6d23) /
692 // `caixa_core::dialeto::tests::caixa_dialeto_from_wire_accepts_every_as_str_output`
693 // (d0e65ea) /
694 // `caixa_core::aplicacao::tests::placement_strategy_from_wire_accepts_every_lifted_constant`
695 // (18c7342) /
696 // `caixa_core::dep::tests::dep_list_round_trips_through_as_str_and_from_wire`
697 // (45ee563) /
698 // `caixa_core::render::tests::path_shape_violation_from_wire_accepts_every_as_str_output`
699 // (aebd9c6) round-trip pins on the sibling caixa-core closed-
700 // set typed-enum reverse-projection axes.
701 for &variant in Semantic::ALL {
702 let wire = variant.as_str();
703 let parsed = Semantic::from_wire(wire).unwrap_or_else(|| {
704 panic!(
705 "Semantic::from_wire({wire:?}) must accept every \
706 Semantic::as_str output — got None for the wire \
707 byte-string of {variant:?}"
708 )
709 });
710 assert_eq!(
711 parsed, variant,
712 "Semantic::from_wire(Semantic::{variant:?}.as_str()) \
713 must return Semantic::{variant:?} — the (as_str, \
714 from_wire) pair must form a total round-trip on the \
715 closed 16-arm Semantic arm-set",
716 );
717 }
718 }
719
720 #[test]
721 fn semantic_from_wire_rejects_unknown_byte_strings() {
722 // Rejection pin on the [`Semantic::from_wire`] parser's accept-
723 // set: any string outside the 16-arm [`Semantic::as_str`] output
724 // set must return `None`. A future accidental widening of the
725 // accept-set (a case-insensitive match that accepts `"KEYWORD"`
726 // / `"Keyword"`, a silent acceptance of the pre-lift PascalCase
727 // Debug-derived shapes `"Keyword"` / `"KeywordArg"` /
728 // `"Unchanged"` on the wire axis, a snake_case drift accepting
729 // `"keyword_arg"` beside the canonical kebab-case
730 // `"keyword-arg"`, a Levenshtein-forgiving arm-lookup that
731 // admits `"kewyord"` typos, a silent absorption of a hypothetical
732 // future `Namespace` / `Deleted` arm before it lands on the enum
733 // and its paired [`Semantic::as_str`] emitter arm) would
734 // silently drift the parser's accept-set from the emitter's — a
735 // downstream style-report re-loader that bound a prior report's
736 // [`Self::as_str`] output back to the typed enum through this
737 // parser would then bind a malformed byte-string to a plausibly-
738 // wrong typed arm the caller does not route through any
739 // fallback, silently misclassifying the reloaded row.
740 //
741 // Peer of the sibling
742 // `caixa_lint::diagnostic::tests::severity_from_wire_rejects_unknown_byte_strings`
743 // (5afff0e) /
744 // `caixa_lint::diagnostic::tests::fix_safety_from_wire_rejects_unknown_byte_strings`
745 // (bd505a1) /
746 // `caixa_arch::report::tests::arch_verdict_from_wire_rejects_unknown_byte_strings`
747 // (6afe564) /
748 // `caixa_arch::invariants::tests::invariant_kind_from_wire_rejects_unknown_byte_strings`
749 // (b9e4e61) rejection pins on the peer caixa-lint / caixa-arch
750 // axes, and of the sibling
751 // `caixa_kind_from_wire_rejects_unknown_byte_strings` (2aa6d23),
752 // `caixa_dialeto_from_wire_rejects_unknown_byte_strings`
753 // (d0e65ea),
754 // `placement_strategy_from_wire_rejects_unknown_byte_strings`
755 // (18c7342),
756 // `dep_list_from_wire_returns_none_on_unknown_wire_scalar`
757 // (45ee563), and
758 // `path_shape_violation_from_wire_rejects_unknown_byte_strings`
759 // (aebd9c6) rejection pins on the sibling caixa-core axes.
760 //
761 // The rejection set also covers overlapping-byte-string tags
762 // from peer axes: caixa-lint `Severity::as_str` outputs
763 // `"error"`/`"warning"`/`"info"`/`"hint"` and caixa-arch
764 // `InvariantKind::as_str` outputs `"safety"`/`"compliance"`
765 // share zero canonical byte-strings with the widened 16-arm
766 // Semantic set here — a widened parser that admitted the peer's
767 // arm on the sibling axis would still not admit an arm foreign
768 // to the caixa-theme semantic-style discriminator's own accept-
769 // set. Yet four peer-axis strings DO overlap with the
770 // caixa-theme set here (`Severity`'s
771 // `"error"`/`"warning"`/`"info"`/`"hint"` map identically onto
772 // the caixa-theme diagnostic-severity sub-region
773 // `Semantic::Error`/`Warning`/`Info`/`Hint`) — a widened parser
774 // that admitted them under a different arm would collapse the
775 // two axes and silently mislabel; the pin excludes those four
776 // from the rejection set precisely because they must accept.
777 for bad in [
778 "",
779 " ",
780 "Keyword",
781 "KEYWORD",
782 "Symbol",
783 "SYMBOL",
784 "KeywordArg",
785 "keyword_arg",
786 "keywordarg",
787 "String",
788 "STRING",
789 "Number",
790 "Literal",
791 "Comment",
792 "Accent",
793 "Muted",
794 "Error",
795 "ERROR",
796 "Warning",
797 "WARNING",
798 "Info",
799 "INFO",
800 "Hint",
801 "HINT",
802 "Added",
803 "ADDED",
804 "Removed",
805 "REMOVED",
806 "Unchanged",
807 "UNCHANGED",
808 "kewyord",
809 "sym",
810 "kw",
811 "str",
812 "num",
813 "lit",
814 "cmt",
815 "safe",
816 "unsafe",
817 "safety",
818 "compliance",
819 "proven",
820 "rejected",
821 "namespace",
822 "deleted",
823 "highlight",
824 "identifier",
825 "keyword ",
826 " keyword",
827 "keyword\n",
828 "keyword\t",
829 "keyword-arg ",
830 " keyword-arg",
831 "added ",
832 " added",
833 "unchanged ",
834 " unchanged",
835 ] {
836 assert!(
837 Semantic::from_wire(bad).is_none(),
838 "Semantic::from_wire({bad:?}) must return None — the \
839 parser's accept-set is exactly the 16 Semantic::as_str \
840 outputs; a widening would silently split the parser's \
841 accept-set from the emitter's arm-set",
842 );
843 }
844 }
845
846 #[test]
847 fn semantic_is_variant_predicates_are_const_fn() {
848 // The [`gen_platform::IsVariant`] derive emits `const fn`
849 // predicates on the peer [`caixa_core::CaixaKind`] /
850 // [`caixa_core::upgrade::UpgradeInstruction`] /
851 // [`caixa_core::supervisor::RestartStrategy`] /
852 // [`caixa_core::supervisor::RestartPolicy`] closed-set typed
853 // enums — pin the same posture on [`Semantic`] so a future
854 // accidental downgrade to non-`const` (an added runtime helper
855 // reachable only from a non-`const` context, a manual hand-
856 // rolled `impl` that shadows the derive-generated method)
857 // trips at caixa-theme build time rather than surfacing as a
858 // downstream `const`-context regression far from the derive
859 // declaration.
860 const { assert!(Semantic::Keyword.is_keyword()) };
861 const { assert!(Semantic::Error.is_error()) };
862 const { assert!(Semantic::Added.is_added()) };
863 const { assert!(Semantic::Unchanged.is_unchanged()) };
864 }
865
866 #[test]
867 fn semantic_try_from_str_routes_through_from_wire_accessor() {
868 // Fail-before-pass-after byte-parity pin on the newly lifted
869 // `impl TryFrom<&str> for Semantic` — asserts the standard-
870 // library trait impl and the substrate-primitive
871 // [`super::Semantic::from_wire`] `Option<Self>` accessor
872 // resolve to the same 16-arm accept-set across every arm the
873 // exhaustive [`super::Semantic::ALL`] slice enumerates. Any
874 // future silent detour that routes the trait impl through a
875 // divergent projection (a per-arm inline `match s { "keyword"
876 // => Ok(Self::Keyword), … }` re-inlining that opens a
877 // compile-time link to the un-lifted arm-literal, a silent
878 // case-fold that admits `"Keyword"` / `"KEYWORD"` and would
879 // collide the canonical-lowercase accept-set the emitter
880 // dispatches on) trips at caixa-theme test time under
881 // `assert_eq!` rather than at a downstream
882 // `impl TryFrom<&str>`-bound consumer's silent split. Sweeps
883 // every one of the 16 arms [`super::Semantic::ALL`] carries so
884 // no arm's projection is covered only by the sibling method-
885 // named `from_wire` path.
886 //
887 // Peer of the sibling
888 // [`caixa_core::kind::tests::caixa_kind_try_from_str_routes_through_from_wire_accessor`]
889 // (3c83606),
890 // [`caixa_core::dialeto::tests::caixa_dialeto_try_from_str_routes_through_from_wire_accessor`]
891 // (bf33136),
892 // `placement_strategy_try_from_str_routes_through_from_wire_accessor`
893 // (6fd00cd),
894 // `rate_limit_unit_try_from_str_routes_through_from_suffix_accessor`
895 // (bf78400),
896 // `path_shape_violation_try_from_str_routes_through_from_wire_accessor`
897 // (e67e48a),
898 // `caixa_arch::invariants::tests::invariant_kind_try_from_str_routes_through_from_wire_accessor`
899 // (e21a857),
900 // `caixa_arch::report::tests::arch_verdict_try_from_str_routes_through_from_wire_accessor`
901 // (0a4cc45),
902 // `caixa_lint::diagnostic::tests::severity_try_from_str_routes_through_from_wire_accessor`
903 // (a7bf74c), and
904 // `caixa_lint::diagnostic::tests::fix_safety_try_from_str_routes_through_from_wire_accessor`
905 // (df86c94) — extends the trait-idiomatic reverse-projection
906 // axis onto the first closed-set fieldless typed enum on the
907 // caixa-theme surface (the semantic-style axis).
908 for &variant in Semantic::ALL {
909 let wire = variant.as_str();
910 assert_eq!(
911 <Semantic as TryFrom<&str>>::try_from(wire),
912 Ok(variant),
913 "TryFrom<&str> impl on Semantic must round-trip \
914 Semantic::{variant:?}.as_str() = {wire:?} back to \
915 Ok(Semantic::{variant:?}) — divergence from \
916 Semantic::from_wire signals a silent detour off the \
917 substrate-primitive accessor",
918 );
919 assert_eq!(
920 <Semantic as TryFrom<&str>>::try_from(wire).ok(),
921 Semantic::from_wire(wire),
922 "TryFrom<&str> ok()-projection on {wire:?} must \
923 byte-equal Semantic::from_wire on the same input",
924 );
925 }
926 }
927
928 #[test]
929 fn semantic_try_from_str_rejects_unknown_byte_strings() {
930 // Rejection witness on the `impl TryFrom<&str> for Semantic` —
931 // sweeps a candidate set of byte-strings outside the 16-arm
932 // canonical-lowercase kebab wire accept-set the sibling
933 // [`super::Semantic::as_str`] emits and asserts every one
934 // lands on `Err(())`, so a future accidental widening of the
935 // trait impl's accept-set (a stray additional
936 // `_ if s.eq_ignore_ascii_case("keyword") => Ok(…)` case-fold
937 // path, a silent acceptance of the pre-lift PascalCase Debug-
938 // derived shapes `"Keyword"` / `"Symbol"` / `"KeywordArg"` on
939 // the wire axis, a Levenshtein-forgiving arm-lookup that
940 // admits `"kwd"` / `"sym"` / `"kw"` typos — the exact form a
941 // `format!("{:?}", …).to_lowercase()` round-trip on the paired
942 // [`std::fmt::Debug`] derive would otherwise land on) trips at
943 // caixa-theme test time. The candidate set includes the empty
944 // string, whitespace-only padding, uppercase / PascalCase
945 // rebrand candidates, Levenshtein-neighbor typos, sibling
946 // closed-set-enum canonical tags not shared with this axis
947 // (peer `caixa_lint::diagnostic::FixSafety::as_str` two-arm
948 // `"safe"` / `"unsafe"`, peer `caixa_arch::InvariantKind::as_str`
949 // three-arm `"safety"` / `"compliance"`, peer
950 // `caixa_arch::ArchVerdict::as_str` two-arm `"proven"` /
951 // `"rejected"`), the trajectory-item candidates
952 // (`"namespace"`, `"deleted"`) the sibling [`Semantic::ALL`]
953 // doc block already names, whitespace-padded canonical tags,
954 // and CamelCase spellings of the multi-word `KeywordArg`
955 // variant (`"KeywordArg"`, `"keywordarg"`, `"keyword_arg"`,
956 // `"keyword.arg"`) that would silently admit
957 // if the accept-set widened to a case-fold or separator-
958 // normalization rule.
959 //
960 // Peer of the sibling
961 // `caixa_kind_try_from_str_rejects_unknown_byte_strings`
962 // (3c83606),
963 // `caixa_dialeto_try_from_str_rejects_unknown_byte_strings`
964 // (bf33136),
965 // `rate_limit_unit_try_from_str_rejects_unknown_byte_strings`
966 // (bf78400),
967 // `path_shape_violation_try_from_str_rejects_unknown_byte_strings`
968 // (e67e48a),
969 // `invariant_kind_try_from_str_rejects_unknown_byte_strings`
970 // (e21a857),
971 // `arch_verdict_try_from_str_rejects_unknown_byte_strings`
972 // (0a4cc45),
973 // `severity_try_from_str_rejects_unknown_byte_strings`
974 // (a7bf74c), and
975 // `fix_safety_try_from_str_rejects_unknown_byte_strings`
976 // (df86c94) rejection pins on the sibling closed-set typed-
977 // enum trait-idiomatic reverse-projection axes.
978 for bad in [
979 "",
980 " ",
981 "Keyword",
982 "KEYWORD",
983 "Symbol",
984 "KeywordArg",
985 "keywordarg",
986 "keyword_arg",
987 "keyword.arg",
988 "String",
989 "Number",
990 "Literal",
991 "Comment",
992 "Accent",
993 "Muted",
994 "Error",
995 "ERROR",
996 "Warning",
997 "Info",
998 "Hint",
999 "Added",
1000 "Removed",
1001 "Unchanged",
1002 "kwd",
1003 "sym",
1004 "kw",
1005 "str",
1006 "num",
1007 "lit",
1008 "cmt",
1009 "safe",
1010 "unsafe",
1011 "safety",
1012 "compliance",
1013 "proven",
1014 "rejected",
1015 "namespace",
1016 "deleted",
1017 "highlight",
1018 "identifier",
1019 "keyword ",
1020 " keyword",
1021 "keyword\n",
1022 "keyword\t",
1023 "keyword-arg ",
1024 " keyword-arg",
1025 "added ",
1026 " added",
1027 "unchanged ",
1028 " unchanged",
1029 ] {
1030 assert_eq!(
1031 <Semantic as TryFrom<&str>>::try_from(bad),
1032 Err(()),
1033 "TryFrom<&str> for Semantic({bad:?}) must return \
1034 Err(()) — the trait impl's accept-set is exactly the \
1035 16 Semantic::as_str outputs; a widening would \
1036 silently split the trait impl's accept-set from the \
1037 emitter's arm-set",
1038 );
1039 }
1040 }
1041
1042 #[test]
1043 fn semantic_try_from_str_and_from_wire_partition_the_accept_set() {
1044 // Cross-axis partition pin: the trait-idiomatic
1045 // [`TryFrom<&str>`] and the method-named
1046 // [`super::Semantic::from_wire`] projections must return
1047 // equivalent decisions on every input — the trait impl's
1048 // `.ok()` project-out from `Result<Self, ()>` and the method's
1049 // `Option<Self>` return must byte-equal each other on both
1050 // accepts and rejects. A future silent bifurcation (the trait
1051 // impl gaining a case-fold path the method does not carry, the
1052 // method gaining a synonym alias the trait impl does not
1053 // honor) trips at caixa-theme test time under a single pin
1054 // rather than at a downstream generic-bound consumer that
1055 // dispatches through one axis while a peer dispatches through
1056 // the other. Sweeps both the 16-arm accept-set (via
1057 // [`super::Semantic::ALL`] threaded through
1058 // [`super::Semantic::as_str`]) and a canonical rejection
1059 // sample so both halves of the partition are covered. Peer of
1060 // the sibling
1061 // `severity_try_from_str_and_from_wire_partition_the_accept_set`
1062 // (a7bf74c) and
1063 // `fix_safety_try_from_str_and_from_wire_partition_the_accept_set`
1064 // (df86c94) partition pins.
1065 for &variant in Semantic::ALL {
1066 let wire = variant.as_str();
1067 assert_eq!(
1068 <Semantic as TryFrom<&str>>::try_from(wire).ok(),
1069 Semantic::from_wire(wire),
1070 "TryFrom<&str>::ok() and from_wire must agree on \
1071 Semantic::{variant:?}.as_str() = {wire:?}",
1072 );
1073 }
1074 for bad in [
1075 "",
1076 "Keyword",
1077 "unknown",
1078 "safety",
1079 "safe",
1080 "proven",
1081 "namespace",
1082 "keywordarg",
1083 ] {
1084 assert_eq!(
1085 <Semantic as TryFrom<&str>>::try_from(bad).ok(),
1086 Semantic::from_wire(bad),
1087 "TryFrom<&str>::ok() and from_wire must agree on the \
1088 rejection outcome for {bad:?}",
1089 );
1090 }
1091 }
1092}