use super::super::*;
#[test]
fn test_strip_locale_suffix_4level_universal() {
let (base, locale) = strip_locale_suffix("representation.boolean.binary.UNIVERSAL");
assert_eq!(base, "representation.boolean.binary");
assert_eq!(locale, Some("UNIVERSAL"));
}
#[test]
fn test_strip_locale_suffix_3level_unchanged() {
let (base, locale) = strip_locale_suffix("geography.address.postal_code");
assert_eq!(base, "geography.address.postal_code");
assert_eq!(locale, None);
}
#[test]
fn test_strip_locale_suffix_short_locale() {
let (base, locale) = strip_locale_suffix("geography.location.city.EN");
assert_eq!(base, "geography.location.city");
assert_eq!(locale, Some("EN"));
}
#[test]
fn test_strip_locale_suffix_no_false_positive_on_type() {
let (base, locale) = strip_locale_suffix("datetime.date.iso");
assert_eq!(base, "datetime.date.iso");
assert_eq!(locale, None);
}
#[test]
fn test_strip_locale_suffix_no_false_positive_on_short_label() {
let (base, locale) = strip_locale_suffix("representation.EN");
assert_eq!(base, "representation.EN");
assert_eq!(locale, None);
}
#[test]
fn ac01_r25_http_status_gate_fires_on_status_codes() {
let values: Vec<String> = vec!["200", "404", "500", "301", "503"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.numeric.integer_number", 0.8, None);
if let Some((label, _)) = &result {
assert_ne!(
label, "geography.address.postal_code",
"R25 guard should prevent postal_code for HTTP status codes"
);
}
}
#[test]
fn v15_region_not_mapped_to_state() {
assert_eq!(header_hint("region"), None);
assert_eq!(header_hint("state"), Some("geography.location.region"));
assert_eq!(header_hint("province"), Some("geography.location.region"));
}
#[test]
fn r32_vocab_overrides_word() {
let values: Vec<String> = vec![
"active", "inactive", "active", "pending", "active", "inactive",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.text.word", 0.9, None);
assert!(
result.is_some(),
"R32 should fire on a small repeated vocab"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
assert!(rule.starts_with("text_vocab_override:"));
}
#[test]
fn r32_preserves_distinct_free_text() {
let values: Vec<String> = vec![
"Dichocarpum",
"adiantifolium",
"sutchuenense",
"arisanense",
"auriculatum",
]
.into_iter()
.map(String::from)
.collect();
for label in [
"representation.text.word",
"representation.text.entity_name",
"representation.text.plain_text",
] {
assert!(
value_sharpen(&values, label, 0.9, None).is_none(),
"R32 must not fire on distinct free text ({label})"
);
}
}
#[test]
fn r32_preserves_constant_and_short_columns() {
let constant: Vec<String> = vec!["s"; 6].into_iter().map(String::from).collect();
assert!(
value_sharpen(&constant, "representation.text.word", 0.9, None).is_none(),
"single distinct value is not a vocabulary"
);
let short: Vec<String> = vec!["a".into(), "b".into(), "a".into()];
assert!(
value_sharpen(&short, "representation.text.word", 0.9, None).is_none(),
"below the n>=4 floor"
);
}
#[test]
fn r32_out_of_scope_labels_untouched() {
let values: Vec<String> = vec![
"Sydney",
"Melbourne",
"Sydney",
"Sydney",
"Melbourne",
"Sydney",
]
.into_iter()
.map(String::from)
.collect();
for label in [
"geography.location.city",
"representation.text.entity_name",
"representation.text.plain_text",
] {
let result = value_sharpen(&values, label, 0.9, None);
if let Some((_, rule)) = &result {
assert!(
!rule.starts_with("text_vocab_override:"),
"R32 must not fire on {label}"
);
}
}
}
#[test]
fn test_sharpen_f2_hostname_high_slash_segments_no_docker_vote() {
let mut result = ColumnResult {
label: "technology.internet.hostname".to_string(),
confidence: 0.85,
vote_distribution: vec![("technology.internet.hostname".to_string(), 0.85)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 50,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::SEGMENT_COUNT_SLASH] = 2.0;
feature_sharpen(&mut result, &cf);
assert_eq!(
result.label, "technology.development.docker_ref",
"F2 should fire on hostname with high slash segments even without docker_ref in votes"
);
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.starts_with("feature_slash_segments"));
}
#[test]
fn test_sharpen_f2_hostname_low_slash_segments_stays() {
let mut result = ColumnResult {
label: "technology.internet.hostname".to_string(),
confidence: 0.85,
vote_distribution: vec![("technology.internet.hostname".to_string(), 0.85)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 50,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::SEGMENT_COUNT_SLASH] = 0.5;
feature_sharpen(&mut result, &cf);
assert_eq!(
result.label, "technology.internet.hostname",
"F2 should NOT fire when slash segments are below threshold"
);
assert!(!result.disambiguation_applied);
}
#[test]
fn test_sharpen_f6_extension_to_categorical_empty_votes() {
let mut result = ColumnResult {
label: "representation.file.extension".to_string(),
confidence: 0.75,
vote_distribution: vec![("representation.file.extension".to_string(), 0.75)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 50,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::LENGTH] = 2.5; cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 0.0; cf.mean[feature_idx::ALPHA_RATIO] = 0.95;
feature_sharpen(&mut result, &cf);
assert_eq!(
result.label, "representation.text.word",
"F6 should fallback to categorical with single-entry votes"
);
assert!(result.disambiguation_applied);
}
#[test]
fn test_sharpen_r11_categorical_single_vote() {
let values: Vec<String> = vec!["red", "blue", "green", "red", "blue"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.person.first_name", 0.70, None);
assert!(
result.is_some(),
"R11 should fire for low-cardinality categorical values"
);
let (label, _rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
}
#[test]
fn compose_from_sense_runs_sharpen() {
let yaml = r#"
technology.internet.url:
title: URL
designation: universal
tier: [VARCHAR, internet]
samples: ["https://x.com/a"]
validation:
type: string
pattern: '^https?://[^\s]+$'
"#;
let mut tax = Taxonomy::from_yaml(yaml).unwrap();
tax.compile_validators();
let mut cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
cc.set_taxonomy(tax);
let urls: Vec<String> = vec![
"https://bitcointalk.org/index.php?topic=1".to_string(),
"https://bitcointalk.org/index.php?topic=2".to_string(),
"https://bitcointalk.org/index.php?topic=3".to_string(),
];
let r = cc
.compose_from_sense("parent_id", &urls, "representation.text.plain_text", 1.0)
.unwrap();
assert_eq!(r.label, "technology.internet.url", "url recovery must fire");
let prose: Vec<String> = vec![
"just some prose here".to_string(),
"and more words today".to_string(),
"a third line of text".to_string(),
];
let r2 = cc
.compose_from_sense("notes", &prose, "representation.text.plain_text", 1.0)
.unwrap();
assert_eq!(r2.label, "representation.text.plain_text");
}
#[test]
fn native_and_compose_run_identical_sharpen() {
let yaml = r#"
technology.internet.url:
title: URL
designation: universal
tier: [VARCHAR, internet]
samples: ["https://x.com/a"]
validation:
type: string
pattern: '^https?://[^\s]+$'
"#;
let mut tax = Taxonomy::from_yaml(yaml).unwrap();
tax.compile_validators();
let mut cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
cc.set_taxonomy(tax);
let native = |header: &str, values: &[String], sense: &str, conf: f32| -> String {
let per_value_features: Vec<[f32; crate::features::FEATURE_DIM]> = values
.iter()
.map(|v| crate::features::extract_features(v))
.collect();
let column_features = aggregate_features(&per_value_features);
let mut result = ColumnResult {
label: sense.to_string(),
confidence: conf,
vote_distribution: vec![(sense.to_string(), conf)],
disambiguation_applied: false,
disambiguation_rule: Some("multi-branch".to_string()),
samples_used: values.len(),
detected_locale: None,
is_generic: false,
column_features: Some(column_features.clone()),
};
cc.run_sharpen(&mut result, header, values, values, &column_features);
result.label
};
let cases: Vec<(&str, Vec<String>, &str, f32)> = vec![
(
"parent_id",
vec![
"https://bitcointalk.org/index.php?topic=1".to_string(),
"https://bitcointalk.org/index.php?topic=2".to_string(),
"https://bitcointalk.org/index.php?topic=3".to_string(),
],
"representation.text.plain_text",
1.0,
),
(
"notes",
vec![
"just some prose here".to_string(),
"and more words today".to_string(),
"a third line of text".to_string(),
],
"representation.text.plain_text",
1.0,
),
(
"url",
vec![
"https://x.com/a".to_string(),
"https://x.com/b".to_string(),
"https://x.com/c".to_string(),
],
"technology.internet.url",
0.9,
),
];
for (header, values, sense, conf) in cases {
let composed = cc
.compose_from_sense(header, &values, sense, conf)
.unwrap()
.label;
let native_label = native(header, &values, sense, conf);
assert_eq!(
native_label, composed,
"native vs compose divergence for header={header:?} sense={sense}"
);
}
}
#[test]
fn test_structured_string_refinement() {
let yaml = r#"
technology.filesystem.windows_path:
title: Windows Path
designation: universal
tier: [VARCHAR, filesystem]
samples: ["C:\\x"]
validation:
type: string
pattern: '^([A-Za-z]:\\|\\\\)[^\r\n]*$'
technology.internet.message_id:
title: Message ID
designation: universal
tier: [VARCHAR, internet]
samples: ["<a@b>"]
validation:
type: string
pattern: '^<[^<>@\s]+@[^<>@\s]+>$'
technology.code.qualified_name:
title: Qualified Name
designation: universal
tier: [VARCHAR, code]
samples: ["a.b.c"]
validation:
type: string
pattern: '^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*){2,}$'
"#;
let mut tax = Taxonomy::from_yaml(yaml).unwrap();
tax.compile_validators();
let mut cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
cc.set_taxonomy(tax);
let mk = |label: &str| ColumnResult {
label: label.to_string(),
confidence: 0.3,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 3,
detected_locale: None,
is_generic: false,
column_features: None,
};
let run = |cc: &ColumnClassifier, label: &str, vals: &[&str]| {
let mut r = mk(label);
let s: Vec<String> = vals.iter().map(|v| v.to_string()).collect();
cc.structured_string_refinement(&mut r, &s);
r.label
};
assert_eq!(
run(
&cc,
"representation.text.plain_text",
&[r"C:\a\b.sys", r"D:\x\y.cs"]
),
"technology.filesystem.windows_path"
);
assert_eq!(
run(
&cc,
"representation.text.plain_text",
&["<a.b@thyme>", "<c.d@thyme>"]
),
"technology.internet.message_id"
);
assert_eq!(
run(&cc, "representation.text.word", &["com.a.B", "org.c.D"]),
"technology.code.qualified_name"
);
assert_eq!(
run(
&cc,
"technology.internet.urn",
&[r"C:\a\b.sys", r"D:\x\y.cs"]
),
"technology.filesystem.windows_path"
);
assert_eq!(
run(
&cc,
"technology.internet.hostname",
&["www.bbc.co.uk", "api.github.com"]
),
"technology.internet.hostname"
);
assert_eq!(
run(
&cc,
"representation.text.plain_text",
&["a sentence here", "more prose now"]
),
"representation.text.plain_text"
);
}
#[test]
fn low_confidence_hardcoded_hint_exits_through_the_hardcoded_arm() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let words: Vec<String> = ["alpha", "bravo", "charlie", "delta", "echo"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("password", &words, "identity.financial.currency_code", 0.2)
.unwrap();
assert_eq!(r.label, "identity.person.password");
assert_eq!(
r.disambiguation_rule.as_deref(),
Some("header_hint_hardcoded:password"),
"the hardcoded-authority arm must be what fires; a fallback below it is unreachable"
);
assert!(
(r.confidence - 0.5).abs() < 1e-6,
"hardcoded authority sets 0.5, not the fallback's 0.4; got {}",
r.confidence
);
}