use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use crate::entity::EntityKind;
fn to_snake_case(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_sep = true; for ch in s.chars() {
if ch == ' ' || ch == '-' || ch == '_' {
if !prev_sep && !out.is_empty() {
out.push('_');
prev_sep = true;
}
} else {
out.push(ch.to_ascii_lowercase());
prev_sep = false;
}
}
if out.ends_with('_') {
out.pop();
}
out
}
#[derive(Clone, Debug)]
pub struct EntityTypeDef {
pub kind: EntityKind,
pub type_name: &'static str,
pub aliases: &'static [&'static str],
}
static BUILTIN_DEFS: &[EntityTypeDef] = &[
EntityTypeDef {
kind: EntityKind::Document,
type_name: "paper",
aliases: &["preprint", "article"],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "report",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "blog_post",
aliases: &["blog"],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "book",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "specification",
aliases: &["spec"],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "documentation",
aliases: &["docs"],
},
EntityTypeDef {
kind: EntityKind::Document,
type_name: "thesis",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "algorithm",
aliases: &["algo"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "theorem",
aliases: &["lemma", "proposition", "corollary"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "definition",
aliases: &["def"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "structure",
aliases: &["inductive", "struct", "class"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "instance",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "axiom",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "goal",
aliases: &["proof_goal"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "technique",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "architecture",
aliases: &["arch"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "model_family",
aliases: &["model"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "theory",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "research_gap",
aliases: &["gap"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "design_pattern",
aliases: &["pattern"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "mathematical_operation",
aliases: &["math_op"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "metric",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "objective",
aliases: &["loss"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "module",
aliases: &["mod", "namespace"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "function",
aliases: &["fn", "func", "method"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "datatype",
aliases: &["enum", "record", "type_alias"],
},
EntityTypeDef {
kind: EntityKind::Concept,
type_name: "interface",
aliases: &["trait", "protocol"],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "benchmark",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "corpus",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "training_set",
aliases: &["train_set"],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "evaluation_set",
aliases: &["eval_set"],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "test_set",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Dataset,
type_name: "synthetic_dataset",
aliases: &["synthetic"],
},
EntityTypeDef {
kind: EntityKind::Project,
type_name: "library",
aliases: &["lib"],
},
EntityTypeDef {
kind: EntityKind::Project,
type_name: "framework",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Project,
type_name: "tool",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Project,
type_name: "application",
aliases: &["app"],
},
EntityTypeDef {
kind: EntityKind::Project,
type_name: "repository",
aliases: &["repo"],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "academic_institution",
aliases: &["university", "uni"],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "company",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "research_lab",
aliases: &["lab"],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "nonprofit",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "government_agency",
aliases: &["gov_agency"],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "consortium",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Org,
type_name: "standards_body",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "checkpoint",
aliases: &["ckpt"],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "snapshot",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "export",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "embedding_index",
aliases: &["embed_index"],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "state_bundle",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Artifact,
type_name: "profile",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "inference_engine",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "retrieval_engine",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "embedding_engine",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "api",
aliases: &["endpoint"],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "database",
aliases: &["db"],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "search_engine",
aliases: &[],
},
EntityTypeDef {
kind: EntityKind::Service,
type_name: "mcp_server",
aliases: &["mcp"],
},
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolvedEntityType {
pub kind: EntityKind,
pub entity_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EntityTypeError {
WrongKind {
raw_type: String,
actual_kind: &'static str,
expected_kind: &'static str,
valid: String,
},
UnknownType {
raw_type: String,
kind: &'static str,
valid: String,
},
}
impl fmt::Display for EntityTypeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WrongKind {
raw_type,
actual_kind,
expected_kind,
valid,
} => write!(
f,
"entity_type {raw_type:?} belongs to {actual_kind:?}, not {expected_kind:?}; \
valid types for {expected_kind:?}: {valid}",
),
Self::UnknownType {
raw_type,
kind,
valid,
} => write!(
f,
"unknown entity_type {raw_type:?} for {kind:?}; valid: {valid}"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for EntityTypeError {}
#[derive(Clone)]
pub struct EntityTypeRegistry {
lookup: BTreeMap<String, usize>,
defs: Vec<EntityTypeDef>,
}
impl EntityTypeRegistry {
pub fn new(defs: impl IntoIterator<Item = EntityTypeDef>) -> Self {
let defs: Vec<EntityTypeDef> = defs.into_iter().collect();
let mut lookup: BTreeMap<String, usize> = BTreeMap::new();
for (idx, def) in defs.iter().enumerate() {
let canonical_type = to_snake_case(def.type_name);
let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
lookup.insert(canonical_key, idx);
lookup.entry(canonical_type).or_insert(idx);
for alias in def.aliases {
let normalised_alias = to_snake_case(alias);
let kind_alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
lookup.insert(kind_alias_key, idx);
lookup.entry(normalised_alias).or_insert(idx);
}
}
Self { lookup, defs }
}
pub fn builtin() -> Self {
Self::new(BUILTIN_DEFS.iter().cloned())
}
pub fn with_extra(extra: impl IntoIterator<Item = EntityTypeDef>) -> Self {
let defs: Vec<EntityTypeDef> = BUILTIN_DEFS.iter().cloned().chain(extra).collect();
Self::new(defs)
}
pub fn check_extra_collisions<'a>(
extras: impl IntoIterator<Item = (&'a str, &'a EntityTypeDef)>,
) -> Result<(), String> {
let mut all: Vec<(&'a str, &'a EntityTypeDef)> =
BUILTIN_DEFS.iter().map(|def| ("builtin", def)).collect();
all.extend(extras);
let mut seen: BTreeMap<String, (&'a str, String)> = BTreeMap::new();
for (owner, def) in all {
let canonical_type = to_snake_case(def.type_name);
let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
if let Some((first_owner, _)) = seen.get(&canonical_key) {
if *first_owner != owner {
return Err(format!(
"duplicate entity_type {canonical_key:?}: claimed by both \
{first_owner:?} and {owner:?}"
));
}
} else {
seen.insert(canonical_key, (owner, canonical_type.clone()));
}
for alias in def.aliases {
let normalised_alias = to_snake_case(alias);
let alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
if let Some((first_owner, first_type)) = seen.get(&alias_key) {
if *first_owner != owner || *first_type != canonical_type {
return Err(format!(
"entity_type alias {alias_key:?}: claimed by both {first_owner:?} \
(canonical {first_type:?}) and {owner:?} (canonical {canonical_type:?})",
));
}
} else {
seen.insert(alias_key, (owner, canonical_type.clone()));
}
}
}
Ok(())
}
pub fn register(&mut self, def: EntityTypeDef) {
let idx = self.defs.len();
let canonical_type = to_snake_case(def.type_name);
let canonical_key = format!("{}:{}", def.kind.name(), canonical_type);
self.lookup.insert(canonical_key, idx);
self.lookup.entry(canonical_type).or_insert(idx);
for alias in def.aliases {
let normalised_alias = to_snake_case(alias);
let kind_alias_key = format!("{}:{}", def.kind.name(), normalised_alias);
self.lookup.insert(kind_alias_key, idx);
self.lookup.entry(normalised_alias).or_insert(idx);
}
self.defs.push(def);
}
pub fn resolve(
&self,
kind: EntityKind,
entity_type: Option<&str>,
) -> Result<ResolvedEntityType, EntityTypeError> {
let Some(raw_type) = entity_type else {
return Ok(ResolvedEntityType {
kind,
entity_type: None,
});
};
let normalised = to_snake_case(raw_type.trim());
let kind_key = format!("{}:{}", kind.name(), normalised);
if let Some(&idx) = self.lookup.get(&kind_key) {
let def = &self.defs[idx];
return Ok(ResolvedEntityType {
kind,
entity_type: Some(def.type_name.to_string()),
});
}
if let Some(&idx) = self.lookup.get(&normalised) {
let def = &self.defs[idx];
if def.kind == kind {
return Ok(ResolvedEntityType {
kind,
entity_type: Some(def.type_name.to_string()),
});
}
return Err(EntityTypeError::WrongKind {
raw_type: raw_type.to_string(),
actual_kind: def.kind.name(),
expected_kind: kind.name(),
valid: self.valid_types_for(kind),
});
}
Err(EntityTypeError::UnknownType {
raw_type: raw_type.to_string(),
kind: kind.name(),
valid: self.valid_types_for(kind),
})
}
pub fn valid_types_for(&self, kind: EntityKind) -> String {
let mut names: Vec<&str> = self
.defs
.iter()
.filter(|d| d.kind == kind)
.map(|d| d.type_name)
.collect();
names.sort_unstable();
if names.is_empty() {
"(none registered)".to_string()
} else {
names.join(" | ")
}
}
}
#[cfg(feature = "std")]
static GLOBAL_REGISTRY: std::sync::OnceLock<EntityTypeRegistry> = std::sync::OnceLock::new();
#[cfg(feature = "std")]
impl EntityTypeRegistry {
pub fn global() -> &'static EntityTypeRegistry {
GLOBAL_REGISTRY.get_or_init(EntityTypeRegistry::builtin)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::entity::EntityKind;
fn reg() -> EntityTypeRegistry {
EntityTypeRegistry::builtin()
}
#[test]
fn resolve_paper_infers_document() {
let r = reg();
let res = r
.resolve(EntityKind::Document, Some("paper"))
.expect("paper is a valid Document subtype");
assert_eq!(res.kind, EntityKind::Document);
assert_eq!(res.entity_type.as_deref(), Some("paper"));
}
#[test]
fn resolve_none_entity_type_always_ok() {
let r = reg();
for kind in EntityKind::ALL {
let res = r.resolve(kind, None).expect("None entity_type always ok");
assert_eq!(res.entity_type, None);
}
}
#[test]
fn resolve_algo_alias_to_algorithm() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("algo"))
.expect("algo is a valid alias for algorithm");
assert_eq!(res.kind, EntityKind::Concept);
assert_eq!(res.entity_type.as_deref(), Some("algorithm"));
}
#[test]
fn resolve_spec_alias_to_specification() {
let r = reg();
let res = r
.resolve(EntityKind::Document, Some("spec"))
.expect("spec is alias for specification");
assert_eq!(res.entity_type.as_deref(), Some("specification"));
}
#[test]
fn reject_brain_profile_for_concept() {
let r = reg();
let err = r
.resolve(EntityKind::Concept, Some("brain_profile"))
.expect_err("brain_profile is not a Concept subtype");
let msg = format!("{err}");
assert!(
msg.contains("brain_profile"),
"error must mention the rejected type; got: {msg}"
);
assert!(
msg.contains("concept"),
"error must mention the target kind; got: {msg}"
);
}
#[test]
fn reject_unknown_subtype_with_valid_list() {
let r = reg();
let err = r
.resolve(EntityKind::Document, Some("mystery_type"))
.expect_err("mystery_type must be rejected");
let msg = format!("{err}");
assert!(
msg.contains("mystery_type"),
"error must echo the rejected value; got: {msg}"
);
assert!(
msg.contains("paper"),
"error must list valid Document subtypes; got: {msg}"
);
}
#[test]
fn reject_wrong_kind_subtype_mentions_correct_kind() {
let r = reg();
let err = r
.resolve(EntityKind::Concept, Some("paper"))
.expect_err("paper is a Document subtype, not Concept");
let msg = format!("{err}");
assert!(
msg.contains("document") || msg.contains("Document"),
"error must name the correct kind; got: {msg}"
);
}
#[test]
fn register_brain_profile_for_concept() {
let mut r = EntityTypeRegistry::builtin();
r.register(EntityTypeDef {
kind: EntityKind::Concept,
type_name: "brain_profile",
aliases: &[],
});
let res = r
.resolve(EntityKind::Concept, Some("brain_profile"))
.expect("brain_profile registered for Concept");
assert_eq!(res.entity_type.as_deref(), Some("brain_profile"));
}
#[test]
fn with_extra_adds_subtypes() {
let r = EntityTypeRegistry::with_extra([EntityTypeDef {
kind: EntityKind::Service,
type_name: "grpc_service",
aliases: &["grpc"],
}]);
let res = r
.resolve(EntityKind::Service, Some("grpc"))
.expect("grpc alias for grpc_service");
assert_eq!(res.entity_type.as_deref(), Some("grpc_service"));
}
#[test]
fn service_api_is_builtin() {
let r = reg();
let res = r
.resolve(EntityKind::Service, Some("api"))
.expect("api is a built-in Service subtype");
assert_eq!(res.entity_type.as_deref(), Some("api"));
let res2 = r
.resolve(EntityKind::Service, Some("endpoint"))
.expect("endpoint is an alias for api");
assert_eq!(res2.entity_type.as_deref(), Some("api"));
}
#[test]
fn service_mcp_server_is_builtin() {
let r = reg();
let res = r
.resolve(EntityKind::Service, Some("mcp_server"))
.expect("mcp_server is a built-in Service subtype");
assert_eq!(res.entity_type.as_deref(), Some("mcp_server"));
let res2 = r
.resolve(EntityKind::Service, Some("mcp"))
.expect("mcp is an alias for mcp_server");
assert_eq!(res2.entity_type.as_deref(), Some("mcp_server"));
}
#[test]
fn project_has_no_service_subtype() {
let r = reg();
r.resolve(EntityKind::Project, Some("service"))
.expect_err("service must not be a Project subtype");
r.resolve(EntityKind::Project, Some("svc"))
.expect_err("svc must not be a Project subtype");
}
#[test]
fn benchmark_is_dataset_not_concept() {
let r = reg();
let res = r
.resolve(EntityKind::Dataset, Some("benchmark"))
.expect("benchmark is a valid Dataset subtype");
assert_eq!(res.entity_type.as_deref(), Some("benchmark"));
r.resolve(EntityKind::Concept, Some("benchmark"))
.expect_err("benchmark must not be a Concept subtype");
}
#[test]
fn model_alias_resolves_to_model_family() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("model"))
.expect("model is an alias for model_family");
assert_eq!(res.entity_type.as_deref(), Some("model_family"));
let res2 = r
.resolve(EntityKind::Concept, Some("model_family"))
.expect("model_family is the canonical name");
assert_eq!(res2.entity_type.as_deref(), Some("model_family"));
}
#[test]
fn resolve_is_case_insensitive() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("Algorithm"))
.expect("Algorithm (mixed case) must resolve");
assert_eq!(res.entity_type.as_deref(), Some("algorithm"));
}
#[test]
fn valid_types_for_person_is_none_registered() {
let r = reg();
let s = r.valid_types_for(EntityKind::Person);
assert_eq!(
s, "(none registered)",
"Person has no built-in subtypes; got: {s}"
);
}
#[test]
fn valid_types_for_concept_includes_algorithm() {
let r = reg();
let s = r.valid_types_for(EntityKind::Concept);
assert!(
s.contains("algorithm"),
"Concept valid types must include algorithm; got: {s}"
);
}
#[test]
fn resolve_inductive_alias_to_structure() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("inductive"))
.expect("inductive is a valid alias for structure");
assert_eq!(res.kind, EntityKind::Concept);
assert_eq!(res.entity_type.as_deref(), Some("structure"));
}
#[test]
fn resolve_goal_subtype() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("goal"))
.expect("goal is a valid Concept subtype");
assert_eq!(res.kind, EntityKind::Concept);
assert_eq!(res.entity_type.as_deref(), Some("goal"));
}
#[cfg(feature = "std")]
#[test]
fn global_registry_is_accessible() {
let r = EntityTypeRegistry::global();
let res = r
.resolve(EntityKind::Document, Some("paper"))
.expect("global registry must resolve paper");
assert_eq!(res.entity_type.as_deref(), Some("paper"));
}
#[test]
fn to_snake_case_converts_hyphen_to_underscore() {
assert_eq!(to_snake_case("proof-goal"), "proof_goal");
}
#[test]
fn to_snake_case_converts_space_to_underscore() {
assert_eq!(to_snake_case("Proof Goal"), "proof_goal");
}
#[test]
fn to_snake_case_collapses_mixed_separators() {
assert_eq!(to_snake_case("proof - goal"), "proof_goal");
assert_eq!(to_snake_case("proof__goal"), "proof_goal");
}
#[test]
fn to_snake_case_strips_leading_trailing_separators() {
assert_eq!(to_snake_case("-theorem-"), "theorem");
assert_eq!(to_snake_case(" theorem "), "theorem");
}
#[test]
fn resolve_proof_goal_hyphen_normalises_to_goal() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("proof-goal"))
.expect("proof-goal must resolve via snake_case normalisation + alias");
assert_eq!(res.entity_type.as_deref(), Some("goal"));
}
#[test]
fn resolve_proof_goal_space_normalises_to_goal() {
let r = reg();
let res = r
.resolve(EntityKind::Concept, Some("Proof Goal"))
.expect("Proof Goal must resolve via snake_case normalisation + alias");
assert_eq!(res.entity_type.as_deref(), Some("goal"));
}
#[test]
fn resolve_blog_hyphen_normalises_to_blog_post_non_formal() {
let r = reg();
let res = r
.resolve(EntityKind::Document, Some("blog-post"))
.expect("blog-post must normalise to blog_post");
assert_eq!(res.entity_type.as_deref(), Some("blog_post"));
}
#[test]
fn entity_type_registry_accepts_code_tokens_and_aliases() {
let r = reg();
for (raw, canonical) in [
("module", "module"),
("mod", "module"),
("namespace", "module"),
("function", "function"),
("fn", "function"),
("func", "function"),
("method", "function"),
("datatype", "datatype"),
("enum", "datatype"),
("record", "datatype"),
("type_alias", "datatype"),
("interface", "interface"),
("trait", "interface"),
("protocol", "interface"),
] {
let res = r
.resolve(EntityKind::Concept, Some(raw))
.unwrap_or_else(|e| panic!("{raw:?} must resolve for Concept: {e}"));
assert_eq!(
res.entity_type.as_deref(),
Some(canonical),
"{raw:?} must resolve to {canonical:?}"
);
}
}
#[test]
fn entity_type_registry_does_not_claim_struct_or_class_for_code() {
let r = reg();
let struct_res = r
.resolve(EntityKind::Concept, Some("struct"))
.expect("struct remains a valid Concept subtype (owned by formal structure)");
assert_eq!(
struct_res.entity_type.as_deref(),
Some("structure"),
"struct must still resolve to formal-math structure, not datatype"
);
let class_res = r
.resolve(EntityKind::Concept, Some("class"))
.expect("class remains a valid Concept subtype (owned by formal structure)");
assert_eq!(
class_res.entity_type.as_deref(),
Some("structure"),
"class must still resolve to formal-math structure, not datatype"
);
}
#[test]
fn check_extra_collisions_detects_normalized_alias_vs_alias() {
let def_a = EntityTypeDef {
kind: EntityKind::Service,
type_name: "widget_service",
aliases: &["Widget-Alias"],
};
let def_b = EntityTypeDef {
kind: EntityKind::Service,
type_name: "gadget_service",
aliases: &["widget_alias"],
};
let err =
EntityTypeRegistry::check_extra_collisions([("pack_a", &def_a), ("pack_b", &def_b)])
.expect_err(
"aliases that only differ in case/hyphenation but normalise to the same \
kind-qualified key must collide",
);
assert!(err.contains("pack_a"), "error must name pack_a: {err}");
assert!(err.contains("pack_b"), "error must name pack_b: {err}");
}
#[test]
fn check_extra_collisions_detects_normalized_alias_vs_canonical() {
let def_a = EntityTypeDef {
kind: EntityKind::Service,
type_name: "primary_service",
aliases: &[],
};
let def_b = EntityTypeDef {
kind: EntityKind::Service,
type_name: "secondary_service",
aliases: &["Primary-Service"],
};
let err =
EntityTypeRegistry::check_extra_collisions([("pack_a", &def_a), ("pack_b", &def_b)])
.expect_err(
"an alias that normalises onto another owner's canonical key must collide, \
even when the raw spellings differ",
);
assert!(err.contains("pack_a"), "error must name pack_a: {err}");
assert!(err.contains("pack_b"), "error must name pack_b: {err}");
}
#[test]
fn check_extra_collisions_detects_normalized_builtin_vs_pack_collision() {
let def = EntityTypeDef {
kind: EntityKind::Document,
type_name: "Paper",
aliases: &[],
};
let err = EntityTypeRegistry::check_extra_collisions([("pack_a", &def)]).expect_err(
"a pack canonical name that normalises onto a builtin canonical key must collide",
);
assert!(err.contains("builtin"), "error must name builtin: {err}");
assert!(err.contains("pack_a"), "error must name pack_a: {err}");
}
}