Skip to main content

agentics_domain/models/
names.rs

1//! Validated human-authored names shared by API, database, and CLI DTOs.
2
3use std::borrow::Cow;
4use std::fmt;
5
6use nutype::nutype;
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8
9/// User-facing validation message for challenge names.
10pub const CHALLENGE_NAME_ERROR_MESSAGE: &str = "challenge_name must be 3-63 lowercase ASCII letters, digits, or single hyphens, and must start and end with a letter or digit";
11
12/// Validation failure for [`ChallengeName`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ChallengeNameError;
15
16impl fmt::Display for ChallengeNameError {
17    /// Handles fmt for this module.
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        f.write_str(CHALLENGE_NAME_ERROR_MESSAGE)
20    }
21}
22
23impl std::error::Error for ChallengeNameError {}
24
25/// User-facing validation message for target names.
26pub const TARGET_NAME_ERROR_MESSAGE: &str = "target must be non-empty and contain only ASCII letters, digits, underscores, hyphens, or dots";
27
28/// Validation failure for [`TargetName`].
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TargetNameError;
31
32impl fmt::Display for TargetNameError {
33    /// Handles fmt for this module.
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str(TARGET_NAME_ERROR_MESSAGE)
36    }
37}
38
39impl std::error::Error for TargetNameError {}
40
41/// User-facing validation message for metric names.
42pub const METRIC_NAME_ERROR_MESSAGE: &str = "metric_name must be non-empty and contain only ASCII letters, digits, underscores, hyphens, or dots";
43
44/// Validation failure for [`MetricName`].
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct MetricNameError;
47
48impl fmt::Display for MetricNameError {
49    /// Handles fmt for this module.
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(METRIC_NAME_ERROR_MESSAGE)
52    }
53}
54
55impl std::error::Error for MetricNameError {}
56
57/// User-facing validation message for private asset names.
58pub const ASSET_NAME_ERROR_MESSAGE: &str = "asset_name must be non-empty and contain only ASCII letters, digits, underscores, hyphens, or dots";
59
60/// Validation failure for [`AssetName`].
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct AssetNameError;
63
64impl fmt::Display for AssetNameError {
65    /// Handles fmt for this module.
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str(ASSET_NAME_ERROR_MESSAGE)
68    }
69}
70
71impl std::error::Error for AssetNameError {}
72
73/// User-facing validation message for challenge run names.
74pub const RUN_NAME_ERROR_MESSAGE: &str = "run_name must be non-empty, must not be `.` or `..`, and must contain only ASCII letters, digits, underscores, hyphens, or dots";
75
76/// Validation failure for [`RunName`].
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct RunNameError;
79
80impl fmt::Display for RunNameError {
81    /// Handles fmt for this module.
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.write_str(RUN_NAME_ERROR_MESSAGE)
84    }
85}
86
87impl std::error::Error for RunNameError {}
88
89/// User-facing validation message for resource profile names.
90pub const RESOURCE_PROFILE_NAME_ERROR_MESSAGE: &str = "resource_profile.name must be non-empty and contain only ASCII letters, digits, underscores, hyphens, or dots";
91
92/// User-facing validation message for challenge keywords.
93pub const CHALLENGE_KEYWORD_ERROR_MESSAGE: &str = "challenge keyword must be non-empty after trimming, at most 30 UTF-8 bytes, and must not contain control characters";
94
95/// User-facing validation message for Moltbook Submolt names.
96pub const MOLTBOOK_SUBMOLT_NAME_ERROR_MESSAGE: &str = "moltbook submolt name must be 2-30 lowercase ASCII letters, digits, or single hyphens, and must start and end with a letter or digit";
97
98/// Validation failure for [`ResourceProfileName`].
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct ResourceProfileNameError;
101
102impl fmt::Display for ResourceProfileNameError {
103    /// Handles fmt for this module.
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        f.write_str(RESOURCE_PROFILE_NAME_ERROR_MESSAGE)
106    }
107}
108
109impl std::error::Error for ResourceProfileNameError {}
110
111/// Validation failure for [`ChallengeKeyword`].
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct ChallengeKeywordError;
114
115impl fmt::Display for ChallengeKeywordError {
116    /// Format the user-facing keyword validation error.
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str(CHALLENGE_KEYWORD_ERROR_MESSAGE)
119    }
120}
121
122impl std::error::Error for ChallengeKeywordError {}
123
124/// Validation failure for [`MoltbookSubmoltName`].
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub struct MoltbookSubmoltNameError;
127
128impl fmt::Display for MoltbookSubmoltNameError {
129    /// Format the user-facing Moltbook Submolt name validation error.
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.write_str(MOLTBOOK_SUBMOLT_NAME_ERROR_MESSAGE)
132    }
133}
134
135impl std::error::Error for MoltbookSubmoltNameError {}
136
137#[nutype(
138    sanitize(trim, lowercase),
139    validate(with = validate_challenge_name, error = ChallengeNameError),
140    derive(
141        Debug,
142        Clone,
143        PartialEq,
144        Eq,
145        PartialOrd,
146        Ord,
147        Hash,
148        AsRef,
149        Deref,
150        Display,
151        Serialize,
152        Deserialize,
153        FromStr,
154        TryFrom,
155    ),
156)]
157/// Carries challenge name data across this module boundary.
158pub struct ChallengeName(String);
159
160impl ChallengeName {
161    /// Borrow the canonical challenge name string.
162    pub fn as_str(&self) -> &str {
163        self.as_ref()
164    }
165}
166
167#[nutype(
168    validate(with = validate_target_name, error = TargetNameError),
169    derive(
170        Debug,
171        Clone,
172        PartialEq,
173        Eq,
174        PartialOrd,
175        Ord,
176        Hash,
177        AsRef,
178        Deref,
179        Display,
180        Serialize,
181        Deserialize,
182        FromStr,
183        TryFrom,
184    ),
185)]
186/// Carries target name data across this module boundary.
187pub struct TargetName(String);
188
189impl TargetName {
190    /// Borrow the canonical target name string.
191    pub fn as_str(&self) -> &str {
192        self.as_ref()
193    }
194}
195
196#[nutype(
197    validate(with = validate_asset_name, error = AssetNameError),
198    derive(
199        Debug,
200        Clone,
201        PartialEq,
202        Eq,
203        PartialOrd,
204        Ord,
205        Hash,
206        AsRef,
207        Deref,
208        Display,
209        Serialize,
210        Deserialize,
211        FromStr,
212        TryFrom,
213    ),
214)]
215/// Carries asset name data across this module boundary.
216pub struct AssetName(String);
217
218impl AssetName {
219    /// Borrow the canonical private asset name string.
220    pub fn as_str(&self) -> &str {
221        self.as_ref()
222    }
223}
224
225#[nutype(
226    validate(with = validate_run_name, error = RunNameError),
227    derive(
228        Debug,
229        Clone,
230        PartialEq,
231        Eq,
232        PartialOrd,
233        Ord,
234        Hash,
235        AsRef,
236        Deref,
237        Display,
238        Serialize,
239        Deserialize,
240        FromStr,
241        TryFrom,
242    ),
243)]
244/// Carries run name data across this module boundary.
245pub struct RunName(String);
246
247impl RunName {
248    /// Borrow the canonical evaluator run name string.
249    pub fn as_str(&self) -> &str {
250        self.as_ref()
251    }
252}
253
254#[nutype(
255    validate(
256        with = validate_resource_profile_name,
257        error = ResourceProfileNameError
258    ),
259    derive(
260        Debug,
261        Clone,
262        PartialEq,
263        Eq,
264        PartialOrd,
265        Ord,
266        Hash,
267        AsRef,
268        Deref,
269        Display,
270        Serialize,
271        Deserialize,
272        FromStr,
273        TryFrom,
274    ),
275)]
276/// Carries resource profile name data across this module boundary.
277pub struct ResourceProfileName(String);
278
279impl ResourceProfileName {
280    /// Borrow the canonical resource profile name string.
281    pub fn as_str(&self) -> &str {
282        self.as_ref()
283    }
284}
285
286#[nutype(
287    sanitize(trim),
288    validate(with = validate_challenge_keyword, error = ChallengeKeywordError),
289    derive(
290        Debug,
291        Clone,
292        PartialEq,
293        Eq,
294        PartialOrd,
295        Ord,
296        Hash,
297        AsRef,
298        Deref,
299        Display,
300        Serialize,
301        Deserialize,
302        FromStr,
303        TryFrom,
304    ),
305)]
306/// Carries one public challenge keyword used for catalog filtering.
307pub struct ChallengeKeyword(String);
308
309impl ChallengeKeyword {
310    /// Borrow the canonical challenge keyword string.
311    pub fn as_str(&self) -> &str {
312        self.as_ref()
313    }
314}
315
316#[nutype(
317    sanitize(trim, lowercase),
318    validate(
319        with = validate_moltbook_submolt_name,
320        error = MoltbookSubmoltNameError
321    ),
322    derive(
323        Debug,
324        Clone,
325        PartialEq,
326        Eq,
327        PartialOrd,
328        Ord,
329        Hash,
330        AsRef,
331        Deref,
332        Display,
333        Serialize,
334        Deserialize,
335        FromStr,
336        TryFrom,
337    ),
338)]
339/// Carries the canonical Moltbook Submolt name used by platform metadata.
340pub struct MoltbookSubmoltName(String);
341
342impl MoltbookSubmoltName {
343    /// Borrow the canonical Moltbook Submolt name string.
344    pub fn as_str(&self) -> &str {
345        self.as_ref()
346    }
347}
348
349#[nutype(
350    sanitize(trim),
351    validate(with = validate_metric_name, error = MetricNameError),
352    derive(
353        Debug,
354        Clone,
355        PartialEq,
356        Eq,
357        PartialOrd,
358        Ord,
359        Hash,
360        AsRef,
361        Deref,
362        Display,
363        Serialize,
364        Deserialize,
365        FromStr,
366        TryFrom,
367    ),
368)]
369/// Carries metric name data across this module boundary.
370pub struct MetricName(String);
371
372impl MetricName {
373    /// Borrow the canonical metric name string.
374    pub fn as_str(&self) -> &str {
375        self.as_ref()
376    }
377
378    /// Built-in compatibility metric used by legacy evaluators.
379    #[allow(
380        clippy::panic,
381        reason = "the built-in `score` metric name is a hard-coded valid literal"
382    )]
383    /// Handles score for this module.
384    pub fn score() -> Self {
385        match Self::try_new("score".to_string()) {
386            Ok(metric_name) => metric_name,
387            Err(_) => panic!("built-in metric name `score` must be valid"),
388        }
389    }
390}
391
392impl JsonSchema for ChallengeName {
393    /// Handles inline schema for this module.
394    fn inline_schema() -> bool {
395        true
396    }
397
398    /// Handles schema name for this module.
399    fn schema_name() -> Cow<'static, str> {
400        "ChallengeName".into()
401    }
402
403    /// Handles json schema for this module.
404    fn json_schema(_: &mut SchemaGenerator) -> Schema {
405        json_schema!({
406            "type": "string",
407            "minLength": 3,
408            "maxLength": 63,
409            "pattern": "^[a-z0-9](?:[a-z0-9]|-(?!-)){1,61}[a-z0-9]$"
410        })
411    }
412}
413
414macro_rules! impl_token_json_schema {
415    ($type_name:ident, $schema_name:literal) => {
416        impl JsonSchema for $type_name {
417            /// Handles inline schema for this module.
418            fn inline_schema() -> bool {
419                true
420            }
421
422            /// Handles schema name for this module.
423            fn schema_name() -> Cow<'static, str> {
424                $schema_name.into()
425            }
426
427            /// Handles json schema for this module.
428            fn json_schema(_: &mut SchemaGenerator) -> Schema {
429                json_schema!({
430                    "type": "string",
431                    "minLength": 1,
432                    "pattern": "^[A-Za-z0-9_.-]+$"
433                })
434            }
435        }
436    };
437}
438
439impl_token_json_schema!(TargetName, "TargetName");
440impl_token_json_schema!(MetricName, "MetricName");
441impl_token_json_schema!(AssetName, "AssetName");
442impl_token_json_schema!(ResourceProfileName, "ResourceProfileName");
443
444impl JsonSchema for MoltbookSubmoltName {
445    /// Keep Moltbook Submolt schemas inline at every field use site.
446    fn inline_schema() -> bool {
447        true
448    }
449
450    /// Return the schema name used by generated frontend contracts.
451    fn schema_name() -> Cow<'static, str> {
452        "MoltbookSubmoltName".into()
453    }
454
455    /// Emit the JSON Schema shape for canonical Moltbook Submolt names.
456    fn json_schema(_: &mut SchemaGenerator) -> Schema {
457        json_schema!({
458            "type": "string",
459            "minLength": 2,
460            "maxLength": 30,
461            "pattern": "^[a-z0-9](?:[a-z0-9]|-(?!-)){0,28}[a-z0-9]$"
462        })
463    }
464}
465
466impl JsonSchema for RunName {
467    /// Handles inline schema for this module.
468    fn inline_schema() -> bool {
469        true
470    }
471
472    /// Handles schema name for this module.
473    fn schema_name() -> Cow<'static, str> {
474        "RunName".into()
475    }
476
477    /// Handles json schema for this module.
478    fn json_schema(_: &mut SchemaGenerator) -> Schema {
479        json_schema!({
480            "type": "string",
481            "minLength": 1,
482            "not": {
483                "enum": [".", ".."]
484            },
485            "pattern": "^[A-Za-z0-9_.-]+$"
486        })
487    }
488}
489
490impl JsonSchema for ChallengeKeyword {
491    /// Keep keyword schemas inline at every field use site.
492    fn inline_schema() -> bool {
493        true
494    }
495
496    /// Return the schema name used by generated frontend contracts.
497    fn schema_name() -> Cow<'static, str> {
498        "ChallengeKeyword".into()
499    }
500
501    /// Emit the JSON Schema shape for public challenge keywords.
502    fn json_schema(_: &mut SchemaGenerator) -> Schema {
503        json_schema!({
504            "type": "string",
505            "minLength": 1,
506            "maxLength": 30,
507            "description": "Public challenge keyword. Runtime validation enforces a 30 UTF-8 byte maximum and rejects control characters."
508        })
509    }
510}
511
512/// Check whether a challenge name is valid in the public repository namespace.
513pub fn is_valid_challenge_name(value: &str) -> bool {
514    let bytes = value.as_bytes();
515    if !(3..=63).contains(&bytes.len()) {
516        return false;
517    }
518    let (Some(first), Some(last)) = (bytes.first(), bytes.last()) else {
519        return false;
520    };
521    if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
522        return false;
523    }
524    if value.contains("--") {
525        return false;
526    }
527    bytes
528        .iter()
529        .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
530}
531
532/// Returns whether name token syntax is present.
533fn has_name_token_syntax(value: &str) -> bool {
534    !value.is_empty()
535        && value
536            .bytes()
537            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.'))
538}
539
540/// Validates challenge name invariants for this contract.
541fn validate_challenge_name(value: &str) -> Result<(), ChallengeNameError> {
542    if is_valid_challenge_name(value) {
543        Ok(())
544    } else {
545        Err(ChallengeNameError)
546    }
547}
548
549/// Validates target name invariants for this contract.
550fn validate_target_name(value: &str) -> Result<(), TargetNameError> {
551    if has_name_token_syntax(value) {
552        Ok(())
553    } else {
554        Err(TargetNameError)
555    }
556}
557
558/// Validates metric name invariants for this contract.
559fn validate_metric_name(value: &str) -> Result<(), MetricNameError> {
560    if has_name_token_syntax(value) {
561        Ok(())
562    } else {
563        Err(MetricNameError)
564    }
565}
566
567/// Validates asset name invariants for this contract.
568fn validate_asset_name(value: &str) -> Result<(), AssetNameError> {
569    if has_name_token_syntax(value) {
570        Ok(())
571    } else {
572        Err(AssetNameError)
573    }
574}
575
576/// Validates run name invariants for this contract.
577fn validate_run_name(value: &str) -> Result<(), RunNameError> {
578    if has_name_token_syntax(value) && !matches!(value, "." | "..") {
579        Ok(())
580    } else {
581        Err(RunNameError)
582    }
583}
584
585/// Validates resource profile name invariants for this contract.
586fn validate_resource_profile_name(value: &str) -> Result<(), ResourceProfileNameError> {
587    if has_name_token_syntax(value) {
588        Ok(())
589    } else {
590        Err(ResourceProfileNameError)
591    }
592}
593
594/// Validates one public challenge keyword.
595fn validate_challenge_keyword(value: &str) -> Result<(), ChallengeKeywordError> {
596    if value.is_empty() || value.len() > 30 || value.chars().any(char::is_control) {
597        Err(ChallengeKeywordError)
598    } else {
599        Ok(())
600    }
601}
602
603/// Validates Moltbook Submolt names used by platform community metadata.
604fn validate_moltbook_submolt_name(value: &str) -> Result<(), MoltbookSubmoltNameError> {
605    let bytes = value.as_bytes();
606    if !(2..=30).contains(&bytes.len()) {
607        return Err(MoltbookSubmoltNameError);
608    }
609    let (Some(first), Some(last)) = (bytes.first(), bytes.last()) else {
610        return Err(MoltbookSubmoltNameError);
611    };
612    if !first.is_ascii_alphanumeric() || !last.is_ascii_alphanumeric() {
613        return Err(MoltbookSubmoltNameError);
614    }
615    if value.contains("--")
616        || !bytes
617            .iter()
618            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
619    {
620        return Err(MoltbookSubmoltNameError);
621    }
622    Ok(())
623}
624
625#[cfg(test)]
626mod tests {
627    use super::{
628        AssetName, ChallengeKeyword, ChallengeName, MetricName, MoltbookSubmoltName,
629        ResourceProfileName, RunName, TargetName, is_valid_challenge_name,
630    };
631
632    /// Verifies that validates challenge names.
633    #[test]
634    fn validates_challenge_names() {
635        assert!(is_valid_challenge_name("sample-sum"));
636        assert!(ChallengeName::try_new("matrix-multiplication").is_ok());
637        let canonical = ChallengeName::try_new(" Matrix-Multiplication ")
638            .expect("challenge names should be lowercased and trimmed");
639        assert_eq!(canonical.as_str(), "matrix-multiplication");
640        assert!(ChallengeName::try_new("Bad_ID").is_err());
641        assert!(ChallengeName::try_new("-bad").is_err());
642        assert!(ChallengeName::try_new("bad-").is_err());
643        assert!(ChallengeName::try_new("bad--id").is_err());
644        assert!(ChallengeName::try_new("ab").is_err());
645        assert!(ChallengeName::try_new("matrix mult").is_err());
646    }
647
648    /// Verifies that validates token names.
649    #[test]
650    fn validates_token_names() {
651        for value in ["linux-arm64-cpu", "score.v1", "cuda_12"] {
652            assert!(TargetName::try_new(value).is_ok());
653            assert!(MetricName::try_new(value).is_ok());
654            assert!(AssetName::try_new(value).is_ok());
655            assert!(RunName::try_new(value).is_ok());
656            assert!(ResourceProfileName::try_new(value).is_ok());
657        }
658        for value in ["", "linux arm64", "linux/arm64", "bad\ntarget"] {
659            assert!(TargetName::try_new(value).is_err());
660            assert!(MetricName::try_new(value).is_err());
661            assert!(AssetName::try_new(value).is_err());
662            assert!(RunName::try_new(value).is_err());
663            assert!(ResourceProfileName::try_new(value).is_err());
664        }
665        for value in [".", ".."] {
666            assert!(TargetName::try_new(value).is_ok());
667            assert!(MetricName::try_new(value).is_ok());
668            assert!(AssetName::try_new(value).is_ok());
669            assert!(RunName::try_new(value).is_err());
670            assert!(ResourceProfileName::try_new(value).is_ok());
671        }
672        let metric = MetricName::try_new(" runtime_ms ").expect("metric names trim edge spaces");
673        assert_eq!(metric.as_str(), "runtime_ms");
674        assert!(MetricName::try_new("runtime ms").is_err());
675    }
676
677    /// Verifies that serde rejects invalid names.
678    #[test]
679    fn serde_rejects_invalid_names() {
680        let challenge: ChallengeName =
681            serde_json::from_str("\"sample-sum\"").expect("valid challenge name should parse");
682        assert_eq!(challenge.as_str(), "sample-sum");
683        let challenge: ChallengeName =
684            serde_json::from_str("\" Sample-Sum \"").expect("challenge name should canonicalize");
685        assert_eq!(challenge.as_str(), "sample-sum");
686        assert!(serde_json::from_str::<ChallengeName>("\"sample sum\"").is_err());
687
688        let target: TargetName =
689            serde_json::from_str("\"linux-arm64-cpu\"").expect("valid target should parse");
690        assert_eq!(target.as_str(), "linux-arm64-cpu");
691        assert!(serde_json::from_str::<TargetName>("\"linux arm64\"").is_err());
692
693        let metric: MetricName =
694            serde_json::from_str("\"runtime_ms\"").expect("valid metric name should parse");
695        assert_eq!(metric.as_str(), "runtime_ms");
696        let metric: MetricName =
697            serde_json::from_str("\" runtime_ms \"").expect("metric name should trim");
698        assert_eq!(metric.as_str(), "runtime_ms");
699        assert!(serde_json::from_str::<MetricName>("\"runtime ms\"").is_err());
700    }
701
702    /// Verifies that challenge keywords allow short Unicode phrases.
703    #[test]
704    fn validates_challenge_keywords() {
705        let keyword = ChallengeKeyword::try_new(" protein folding ")
706            .expect("keyword phrases should trim edge whitespace");
707        assert_eq!(keyword.as_str(), "protein folding");
708        assert!(ChallengeKeyword::try_new("AI".to_string()).is_ok());
709        assert!(ChallengeKeyword::try_new("图搜索".to_string()).is_ok());
710        assert!(ChallengeKeyword::try_new("".to_string()).is_err());
711        assert!(ChallengeKeyword::try_new("bad\nkeyword".to_string()).is_err());
712        assert!(ChallengeKeyword::try_new("abcdefghijklmnopqrstuvwxyz12345".to_string()).is_err());
713        assert!(ChallengeKeyword::try_new("多字节多字节多字节多字节".to_string()).is_err());
714    }
715
716    /// Verifies Moltbook Submolt name canonicalization and validation.
717    #[test]
718    fn validates_moltbook_submolt_names() {
719        let name = MoltbookSubmoltName::try_new(" Agentics-Platform ".to_string())
720            .expect("submolt name should canonicalize");
721        assert_eq!(name.as_str(), "agentics-platform");
722        assert!(MoltbookSubmoltName::try_new("ai".to_string()).is_ok());
723        assert!(MoltbookSubmoltName::try_new("a".to_string()).is_err());
724        assert!(MoltbookSubmoltName::try_new("-agentics".to_string()).is_err());
725        assert!(MoltbookSubmoltName::try_new("agentics-".to_string()).is_err());
726        assert!(MoltbookSubmoltName::try_new("agentics--platform".to_string()).is_err());
727        assert!(MoltbookSubmoltName::try_new("agentics_platform".to_string()).is_err());
728    }
729}