1use alloc::collections::BTreeMap;
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use core::fmt;
12
13use crate::entity::EntityKind;
14
15fn to_snake_case(s: &str) -> String {
23 let mut out = String::with_capacity(s.len());
24 let mut prev_sep = true; for ch in s.chars() {
26 if ch == ' ' || ch == '-' || ch == '_' {
27 if !prev_sep && !out.is_empty() {
28 out.push('_');
29 prev_sep = true;
30 }
31 } else {
32 out.push(ch.to_ascii_lowercase());
33 prev_sep = false;
34 }
35 }
36 if out.ends_with('_') {
37 out.pop();
38 }
39 out
40}
41
42#[derive(Clone, Debug)]
45pub struct EntityTypeDef {
46 pub kind: EntityKind,
48 pub type_name: &'static str,
50 pub aliases: &'static [&'static str],
53}
54
55static BUILTIN_DEFS: &[EntityTypeDef] = &[
57 EntityTypeDef {
59 kind: EntityKind::Document,
60 type_name: "paper",
61 aliases: &["preprint", "article"],
62 },
63 EntityTypeDef {
64 kind: EntityKind::Document,
65 type_name: "report",
66 aliases: &[],
67 },
68 EntityTypeDef {
69 kind: EntityKind::Document,
70 type_name: "blog_post",
71 aliases: &["blog"],
72 },
73 EntityTypeDef {
74 kind: EntityKind::Document,
75 type_name: "book",
76 aliases: &[],
77 },
78 EntityTypeDef {
79 kind: EntityKind::Document,
80 type_name: "specification",
81 aliases: &["spec"],
82 },
83 EntityTypeDef {
84 kind: EntityKind::Document,
85 type_name: "documentation",
86 aliases: &["docs"],
87 },
88 EntityTypeDef {
89 kind: EntityKind::Document,
90 type_name: "thesis",
91 aliases: &[],
92 },
93 EntityTypeDef {
95 kind: EntityKind::Concept,
96 type_name: "algorithm",
97 aliases: &["algo"],
98 },
99 EntityTypeDef {
101 kind: EntityKind::Concept,
102 type_name: "theorem",
103 aliases: &["lemma", "proposition", "corollary"],
104 },
105 EntityTypeDef {
106 kind: EntityKind::Concept,
107 type_name: "definition",
108 aliases: &["def"],
109 },
110 EntityTypeDef {
111 kind: EntityKind::Concept,
112 type_name: "structure",
113 aliases: &["inductive", "struct", "class"],
114 },
115 EntityTypeDef {
116 kind: EntityKind::Concept,
117 type_name: "instance",
118 aliases: &[],
119 },
120 EntityTypeDef {
121 kind: EntityKind::Concept,
122 type_name: "axiom",
123 aliases: &[],
124 },
125 EntityTypeDef {
126 kind: EntityKind::Concept,
127 type_name: "goal",
128 aliases: &["proof_goal"],
129 },
130 EntityTypeDef {
131 kind: EntityKind::Concept,
132 type_name: "technique",
133 aliases: &[],
134 },
135 EntityTypeDef {
136 kind: EntityKind::Concept,
137 type_name: "architecture",
138 aliases: &["arch"],
139 },
140 EntityTypeDef {
141 kind: EntityKind::Concept,
142 type_name: "model_family",
145 aliases: &["model"],
146 },
147 EntityTypeDef {
148 kind: EntityKind::Concept,
149 type_name: "theory",
150 aliases: &[],
151 },
152 EntityTypeDef {
153 kind: EntityKind::Concept,
154 type_name: "research_gap",
155 aliases: &["gap"],
156 },
157 EntityTypeDef {
158 kind: EntityKind::Concept,
159 type_name: "design_pattern",
160 aliases: &["pattern"],
161 },
162 EntityTypeDef {
163 kind: EntityKind::Concept,
164 type_name: "mathematical_operation",
165 aliases: &["math_op"],
166 },
167 EntityTypeDef {
168 kind: EntityKind::Concept,
169 type_name: "metric",
170 aliases: &[],
171 },
172 EntityTypeDef {
173 kind: EntityKind::Concept,
174 type_name: "objective",
175 aliases: &["loss"],
176 },
177 EntityTypeDef {
182 kind: EntityKind::Concept,
183 type_name: "module",
184 aliases: &["mod", "namespace"],
185 },
186 EntityTypeDef {
187 kind: EntityKind::Concept,
188 type_name: "function",
189 aliases: &["fn", "func", "method"],
190 },
191 EntityTypeDef {
192 kind: EntityKind::Concept,
193 type_name: "datatype",
194 aliases: &["enum", "record", "type_alias"],
195 },
196 EntityTypeDef {
197 kind: EntityKind::Concept,
198 type_name: "interface",
199 aliases: &["trait", "protocol"],
200 },
201 EntityTypeDef {
204 kind: EntityKind::Dataset,
205 type_name: "benchmark",
206 aliases: &[],
207 },
208 EntityTypeDef {
209 kind: EntityKind::Dataset,
210 type_name: "corpus",
211 aliases: &[],
212 },
213 EntityTypeDef {
214 kind: EntityKind::Dataset,
215 type_name: "training_set",
216 aliases: &["train_set"],
217 },
218 EntityTypeDef {
219 kind: EntityKind::Dataset,
220 type_name: "evaluation_set",
221 aliases: &["eval_set"],
222 },
223 EntityTypeDef {
224 kind: EntityKind::Dataset,
225 type_name: "test_set",
226 aliases: &[],
227 },
228 EntityTypeDef {
229 kind: EntityKind::Dataset,
230 type_name: "synthetic_dataset",
231 aliases: &["synthetic"],
232 },
233 EntityTypeDef {
238 kind: EntityKind::Project,
239 type_name: "library",
240 aliases: &["lib"],
241 },
242 EntityTypeDef {
243 kind: EntityKind::Project,
244 type_name: "framework",
245 aliases: &[],
246 },
247 EntityTypeDef {
248 kind: EntityKind::Project,
249 type_name: "tool",
250 aliases: &[],
251 },
252 EntityTypeDef {
253 kind: EntityKind::Project,
254 type_name: "application",
255 aliases: &["app"],
256 },
257 EntityTypeDef {
258 kind: EntityKind::Project,
259 type_name: "repository",
260 aliases: &["repo"],
261 },
262 EntityTypeDef {
264 kind: EntityKind::Org,
265 type_name: "academic_institution",
266 aliases: &["university", "uni"],
267 },
268 EntityTypeDef {
269 kind: EntityKind::Org,
270 type_name: "company",
271 aliases: &[],
272 },
273 EntityTypeDef {
274 kind: EntityKind::Org,
275 type_name: "research_lab",
276 aliases: &["lab"],
277 },
278 EntityTypeDef {
279 kind: EntityKind::Org,
280 type_name: "nonprofit",
281 aliases: &[],
282 },
283 EntityTypeDef {
284 kind: EntityKind::Org,
285 type_name: "government_agency",
286 aliases: &["gov_agency"],
287 },
288 EntityTypeDef {
289 kind: EntityKind::Org,
290 type_name: "consortium",
291 aliases: &[],
292 },
293 EntityTypeDef {
294 kind: EntityKind::Org,
295 type_name: "standards_body",
296 aliases: &[],
297 },
298 EntityTypeDef {
300 kind: EntityKind::Artifact,
301 type_name: "checkpoint",
302 aliases: &["ckpt"],
303 },
304 EntityTypeDef {
305 kind: EntityKind::Artifact,
306 type_name: "snapshot",
307 aliases: &[],
308 },
309 EntityTypeDef {
310 kind: EntityKind::Artifact,
311 type_name: "export",
312 aliases: &[],
313 },
314 EntityTypeDef {
315 kind: EntityKind::Artifact,
316 type_name: "embedding_index",
317 aliases: &["embed_index"],
318 },
319 EntityTypeDef {
320 kind: EntityKind::Artifact,
321 type_name: "state_bundle",
322 aliases: &[],
323 },
324 EntityTypeDef {
325 kind: EntityKind::Artifact,
326 type_name: "profile",
327 aliases: &[],
328 },
329 EntityTypeDef {
333 kind: EntityKind::Service,
334 type_name: "inference_engine",
335 aliases: &[],
336 },
337 EntityTypeDef {
338 kind: EntityKind::Service,
339 type_name: "retrieval_engine",
340 aliases: &[],
341 },
342 EntityTypeDef {
343 kind: EntityKind::Service,
344 type_name: "embedding_engine",
345 aliases: &[],
346 },
347 EntityTypeDef {
348 kind: EntityKind::Service,
349 type_name: "api",
350 aliases: &["endpoint"],
351 },
352 EntityTypeDef {
353 kind: EntityKind::Service,
354 type_name: "database",
355 aliases: &["db"],
356 },
357 EntityTypeDef {
358 kind: EntityKind::Service,
359 type_name: "search_engine",
360 aliases: &[],
361 },
362 EntityTypeDef {
363 kind: EntityKind::Service,
364 type_name: "mcp_server",
365 aliases: &["mcp"],
366 },
367 ];
369
370#[derive(Clone, Debug, PartialEq, Eq)]
372pub struct ResolvedEntityType {
373 pub kind: EntityKind,
375 pub entity_type: Option<String>,
378}
379
380#[derive(Clone, Debug, PartialEq, Eq)]
385pub enum EntityTypeError {
386 WrongKind {
388 raw_type: String,
389 actual_kind: &'static str,
390 expected_kind: &'static str,
391 valid: String,
392 },
393 UnknownType {
395 raw_type: String,
396 kind: &'static str,
397 valid: String,
398 },
399}
400
401impl fmt::Display for EntityTypeError {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Self::WrongKind {
405 raw_type,
406 actual_kind,
407 expected_kind,
408 valid,
409 } => write!(
410 f,
411 "entity_type {raw_type:?} belongs to {actual_kind:?}, not {expected_kind:?}; \
412 valid types for {expected_kind:?}: {valid}",
413 ),
414 Self::UnknownType {
415 raw_type,
416 kind,
417 valid,
418 } => write!(
419 f,
420 "unknown entity_type {raw_type:?} for {kind:?}; valid: {valid}"
421 ),
422 }
423 }
424}
425
426#[cfg(feature = "std")]
427impl std::error::Error for EntityTypeError {}
428
429#[derive(Clone)]
431pub struct EntityTypeRegistry {
432 lookup: BTreeMap<String, usize>,
435 defs: Vec<EntityTypeDef>,
436}
437
438impl EntityTypeRegistry {
439 pub fn new(defs: impl IntoIterator<Item = EntityTypeDef>) -> Self {
441 let defs: Vec<EntityTypeDef> = defs.into_iter().collect();
442 let mut lookup: BTreeMap<String, usize> = BTreeMap::new();
443 for (idx, def) in defs.iter().enumerate() {
444 let canonical_key = format!("{}:{}", def.kind.name(), def.type_name);
445 lookup.insert(canonical_key, idx);
446 let bare_key = def.type_name.to_string();
447 lookup.entry(bare_key).or_insert(idx);
449 for alias in def.aliases {
450 let kind_alias_key = format!("{}:{}", def.kind.name(), alias);
451 lookup.insert(kind_alias_key, idx);
452 lookup.entry(alias.to_string()).or_insert(idx);
453 }
454 }
455 Self { lookup, defs }
456 }
457
458 pub fn builtin() -> Self {
460 Self::new(BUILTIN_DEFS.iter().cloned())
461 }
462
463 pub fn with_extra(extra: impl IntoIterator<Item = EntityTypeDef>) -> Self {
466 let defs: Vec<EntityTypeDef> = BUILTIN_DEFS.iter().cloned().chain(extra).collect();
467 Self::new(defs)
468 }
469
470 pub fn register(&mut self, def: EntityTypeDef) {
472 let idx = self.defs.len();
473 let canonical_key = format!("{}:{}", def.kind.name(), def.type_name);
474 self.lookup.insert(canonical_key, idx);
475 self.lookup.entry(def.type_name.to_string()).or_insert(idx);
476 for alias in def.aliases {
477 let kind_alias_key = format!("{}:{}", def.kind.name(), alias);
478 self.lookup.insert(kind_alias_key, idx);
479 self.lookup.entry(alias.to_string()).or_insert(idx);
480 }
481 self.defs.push(def);
482 }
483
484 pub fn resolve(
486 &self,
487 kind: EntityKind,
488 entity_type: Option<&str>,
489 ) -> Result<ResolvedEntityType, EntityTypeError> {
490 let Some(raw_type) = entity_type else {
491 return Ok(ResolvedEntityType {
492 kind,
493 entity_type: None,
494 });
495 };
496
497 let normalised = to_snake_case(raw_type.trim());
498
499 let kind_key = format!("{}:{}", kind.name(), normalised);
501 if let Some(&idx) = self.lookup.get(&kind_key) {
502 let def = &self.defs[idx];
503 return Ok(ResolvedEntityType {
504 kind,
505 entity_type: Some(def.type_name.to_string()),
506 });
507 }
508
509 if let Some(&idx) = self.lookup.get(&normalised) {
511 let def = &self.defs[idx];
512 if def.kind == kind {
513 return Ok(ResolvedEntityType {
514 kind,
515 entity_type: Some(def.type_name.to_string()),
516 });
517 }
518 return Err(EntityTypeError::WrongKind {
520 raw_type: raw_type.to_string(),
521 actual_kind: def.kind.name(),
522 expected_kind: kind.name(),
523 valid: self.valid_types_for(kind),
524 });
525 }
526
527 Err(EntityTypeError::UnknownType {
529 raw_type: raw_type.to_string(),
530 kind: kind.name(),
531 valid: self.valid_types_for(kind),
532 })
533 }
534
535 pub fn valid_types_for(&self, kind: EntityKind) -> String {
537 let mut names: Vec<&str> = self
538 .defs
539 .iter()
540 .filter(|d| d.kind == kind)
541 .map(|d| d.type_name)
542 .collect();
543 names.sort_unstable();
544 if names.is_empty() {
545 "(none registered)".to_string()
546 } else {
547 names.join(" | ")
548 }
549 }
550}
551
552#[cfg(feature = "std")]
555static GLOBAL_REGISTRY: std::sync::OnceLock<EntityTypeRegistry> = std::sync::OnceLock::new();
556
557#[cfg(feature = "std")]
558impl EntityTypeRegistry {
559 pub fn global() -> &'static EntityTypeRegistry {
561 GLOBAL_REGISTRY.get_or_init(EntityTypeRegistry::builtin)
562 }
563}
564
565#[cfg(test)]
568mod tests {
569 use super::*;
570 use crate::entity::EntityKind;
571
572 fn reg() -> EntityTypeRegistry {
573 EntityTypeRegistry::builtin()
574 }
575
576 #[test]
579 fn resolve_paper_infers_document() {
580 let r = reg();
581 let res = r
582 .resolve(EntityKind::Document, Some("paper"))
583 .expect("paper is a valid Document subtype");
584 assert_eq!(res.kind, EntityKind::Document);
585 assert_eq!(res.entity_type.as_deref(), Some("paper"));
586 }
587
588 #[test]
589 fn resolve_none_entity_type_always_ok() {
590 let r = reg();
591 for kind in EntityKind::ALL {
592 let res = r.resolve(kind, None).expect("None entity_type always ok");
593 assert_eq!(res.entity_type, None);
594 }
595 }
596
597 #[test]
598 fn resolve_algo_alias_to_algorithm() {
599 let r = reg();
600 let res = r
601 .resolve(EntityKind::Concept, Some("algo"))
602 .expect("algo is a valid alias for algorithm");
603 assert_eq!(res.kind, EntityKind::Concept);
604 assert_eq!(res.entity_type.as_deref(), Some("algorithm"));
605 }
606
607 #[test]
608 fn resolve_spec_alias_to_specification() {
609 let r = reg();
610 let res = r
611 .resolve(EntityKind::Document, Some("spec"))
612 .expect("spec is alias for specification");
613 assert_eq!(res.entity_type.as_deref(), Some("specification"));
614 }
615
616 #[test]
619 fn reject_brain_profile_for_concept() {
620 let r = reg();
621 let err = r
622 .resolve(EntityKind::Concept, Some("brain_profile"))
623 .expect_err("brain_profile is not a Concept subtype");
624 let msg = format!("{err}");
625 assert!(
626 msg.contains("brain_profile"),
627 "error must mention the rejected type; got: {msg}"
628 );
629 assert!(
630 msg.contains("concept"),
631 "error must mention the target kind; got: {msg}"
632 );
633 }
634
635 #[test]
636 fn reject_unknown_subtype_with_valid_list() {
637 let r = reg();
638 let err = r
639 .resolve(EntityKind::Document, Some("mystery_type"))
640 .expect_err("mystery_type must be rejected");
641 let msg = format!("{err}");
642 assert!(
643 msg.contains("mystery_type"),
644 "error must echo the rejected value; got: {msg}"
645 );
646 assert!(
648 msg.contains("paper"),
649 "error must list valid Document subtypes; got: {msg}"
650 );
651 }
652
653 #[test]
654 fn reject_wrong_kind_subtype_mentions_correct_kind() {
655 let r = reg();
657 let err = r
658 .resolve(EntityKind::Concept, Some("paper"))
659 .expect_err("paper is a Document subtype, not Concept");
660 let msg = format!("{err}");
661 assert!(
662 msg.contains("document") || msg.contains("Document"),
663 "error must name the correct kind; got: {msg}"
664 );
665 }
666
667 #[test]
670 fn register_brain_profile_for_concept() {
671 let mut r = EntityTypeRegistry::builtin();
672 r.register(EntityTypeDef {
673 kind: EntityKind::Concept,
674 type_name: "brain_profile",
675 aliases: &[],
676 });
677 let res = r
678 .resolve(EntityKind::Concept, Some("brain_profile"))
679 .expect("brain_profile registered for Concept");
680 assert_eq!(res.entity_type.as_deref(), Some("brain_profile"));
681 }
682
683 #[test]
684 fn with_extra_adds_subtypes() {
685 let r = EntityTypeRegistry::with_extra([EntityTypeDef {
687 kind: EntityKind::Service,
688 type_name: "grpc_service",
689 aliases: &["grpc"],
690 }]);
691 let res = r
692 .resolve(EntityKind::Service, Some("grpc"))
693 .expect("grpc alias for grpc_service");
694 assert_eq!(res.entity_type.as_deref(), Some("grpc_service"));
695 }
696
697 #[test]
698 fn service_api_is_builtin() {
699 let r = reg();
700 let res = r
701 .resolve(EntityKind::Service, Some("api"))
702 .expect("api is a built-in Service subtype");
703 assert_eq!(res.entity_type.as_deref(), Some("api"));
704 let res2 = r
705 .resolve(EntityKind::Service, Some("endpoint"))
706 .expect("endpoint is an alias for api");
707 assert_eq!(res2.entity_type.as_deref(), Some("api"));
708 }
709
710 #[test]
711 fn service_mcp_server_is_builtin() {
712 let r = reg();
713 let res = r
714 .resolve(EntityKind::Service, Some("mcp_server"))
715 .expect("mcp_server is a built-in Service subtype");
716 assert_eq!(res.entity_type.as_deref(), Some("mcp_server"));
717 let res2 = r
718 .resolve(EntityKind::Service, Some("mcp"))
719 .expect("mcp is an alias for mcp_server");
720 assert_eq!(res2.entity_type.as_deref(), Some("mcp_server"));
721 }
722
723 #[test]
724 fn project_has_no_service_subtype() {
725 let r = reg();
726 r.resolve(EntityKind::Project, Some("service"))
727 .expect_err("service must not be a Project subtype");
728 r.resolve(EntityKind::Project, Some("svc"))
729 .expect_err("svc must not be a Project subtype");
730 }
731
732 #[test]
733 fn benchmark_is_dataset_not_concept() {
734 let r = reg();
735 let res = r
736 .resolve(EntityKind::Dataset, Some("benchmark"))
737 .expect("benchmark is a valid Dataset subtype");
738 assert_eq!(res.entity_type.as_deref(), Some("benchmark"));
739 r.resolve(EntityKind::Concept, Some("benchmark"))
740 .expect_err("benchmark must not be a Concept subtype");
741 }
742
743 #[test]
744 fn model_alias_resolves_to_model_family() {
745 let r = reg();
746 let res = r
747 .resolve(EntityKind::Concept, Some("model"))
748 .expect("model is an alias for model_family");
749 assert_eq!(res.entity_type.as_deref(), Some("model_family"));
750 let res2 = r
751 .resolve(EntityKind::Concept, Some("model_family"))
752 .expect("model_family is the canonical name");
753 assert_eq!(res2.entity_type.as_deref(), Some("model_family"));
754 }
755
756 #[test]
759 fn resolve_is_case_insensitive() {
760 let r = reg();
761 let res = r
762 .resolve(EntityKind::Concept, Some("Algorithm"))
763 .expect("Algorithm (mixed case) must resolve");
764 assert_eq!(res.entity_type.as_deref(), Some("algorithm"));
765 }
766
767 #[test]
770 fn valid_types_for_person_is_none_registered() {
771 let r = reg();
772 let s = r.valid_types_for(EntityKind::Person);
773 assert_eq!(
774 s, "(none registered)",
775 "Person has no built-in subtypes; got: {s}"
776 );
777 }
778
779 #[test]
780 fn valid_types_for_concept_includes_algorithm() {
781 let r = reg();
782 let s = r.valid_types_for(EntityKind::Concept);
783 assert!(
784 s.contains("algorithm"),
785 "Concept valid types must include algorithm; got: {s}"
786 );
787 }
788
789 #[test]
790 fn resolve_inductive_alias_to_structure() {
791 let r = reg();
792 let res = r
793 .resolve(EntityKind::Concept, Some("inductive"))
794 .expect("inductive is a valid alias for structure");
795 assert_eq!(res.kind, EntityKind::Concept);
796 assert_eq!(res.entity_type.as_deref(), Some("structure"));
797 }
798
799 #[test]
800 fn resolve_goal_subtype() {
801 let r = reg();
802 let res = r
803 .resolve(EntityKind::Concept, Some("goal"))
804 .expect("goal is a valid Concept subtype");
805 assert_eq!(res.kind, EntityKind::Concept);
806 assert_eq!(res.entity_type.as_deref(), Some("goal"));
807 }
808
809 #[cfg(feature = "std")]
812 #[test]
813 fn global_registry_is_accessible() {
814 let r = EntityTypeRegistry::global();
815 let res = r
816 .resolve(EntityKind::Document, Some("paper"))
817 .expect("global registry must resolve paper");
818 assert_eq!(res.entity_type.as_deref(), Some("paper"));
819 }
820
821 #[test]
824 fn to_snake_case_converts_hyphen_to_underscore() {
825 assert_eq!(to_snake_case("proof-goal"), "proof_goal");
826 }
827
828 #[test]
829 fn to_snake_case_converts_space_to_underscore() {
830 assert_eq!(to_snake_case("Proof Goal"), "proof_goal");
831 }
832
833 #[test]
834 fn to_snake_case_collapses_mixed_separators() {
835 assert_eq!(to_snake_case("proof - goal"), "proof_goal");
836 assert_eq!(to_snake_case("proof__goal"), "proof_goal");
837 }
838
839 #[test]
840 fn to_snake_case_strips_leading_trailing_separators() {
841 assert_eq!(to_snake_case("-theorem-"), "theorem");
842 assert_eq!(to_snake_case(" theorem "), "theorem");
843 }
844
845 #[test]
846 fn resolve_proof_goal_hyphen_normalises_to_goal() {
847 let r = reg();
848 let res = r
849 .resolve(EntityKind::Concept, Some("proof-goal"))
850 .expect("proof-goal must resolve via snake_case normalisation + alias");
851 assert_eq!(res.entity_type.as_deref(), Some("goal"));
852 }
853
854 #[test]
855 fn resolve_proof_goal_space_normalises_to_goal() {
856 let r = reg();
857 let res = r
858 .resolve(EntityKind::Concept, Some("Proof Goal"))
859 .expect("Proof Goal must resolve via snake_case normalisation + alias");
860 assert_eq!(res.entity_type.as_deref(), Some("goal"));
861 }
862
863 #[test]
864 fn resolve_blog_hyphen_normalises_to_blog_post_non_formal() {
865 let r = reg();
866 let res = r
867 .resolve(EntityKind::Document, Some("blog-post"))
868 .expect("blog-post must normalise to blog_post");
869 assert_eq!(res.entity_type.as_deref(), Some("blog_post"));
870 }
871
872 #[test]
875 fn entity_type_registry_accepts_code_tokens_and_aliases() {
876 let r = reg();
877 for (raw, canonical) in [
878 ("module", "module"),
879 ("mod", "module"),
880 ("namespace", "module"),
881 ("function", "function"),
882 ("fn", "function"),
883 ("func", "function"),
884 ("method", "function"),
885 ("datatype", "datatype"),
886 ("enum", "datatype"),
887 ("record", "datatype"),
888 ("type_alias", "datatype"),
889 ("interface", "interface"),
890 ("trait", "interface"),
891 ("protocol", "interface"),
892 ] {
893 let res = r
894 .resolve(EntityKind::Concept, Some(raw))
895 .unwrap_or_else(|e| panic!("{raw:?} must resolve for Concept: {e}"));
896 assert_eq!(
897 res.entity_type.as_deref(),
898 Some(canonical),
899 "{raw:?} must resolve to {canonical:?}"
900 );
901 }
902 }
903
904 #[test]
905 fn entity_type_registry_does_not_claim_struct_or_class_for_code() {
906 let r = reg();
907 let struct_res = r
908 .resolve(EntityKind::Concept, Some("struct"))
909 .expect("struct remains a valid Concept subtype (owned by formal structure)");
910 assert_eq!(
911 struct_res.entity_type.as_deref(),
912 Some("structure"),
913 "struct must still resolve to formal-math structure, not datatype"
914 );
915 let class_res = r
916 .resolve(EntityKind::Concept, Some("class"))
917 .expect("class remains a valid Concept subtype (owned by formal structure)");
918 assert_eq!(
919 class_res.entity_type.as_deref(),
920 Some("structure"),
921 "class must still resolve to formal-math structure, not datatype"
922 );
923 }
924}