use super::*;
#[test]
#[cfg(not(feature = "rhh-instrumentation"))]
fn rhh_ac02_default_build_email_match_table_unchanged() {
assert_eq!(header_hint("email"), Some("identity.person.email"));
}
#[test]
#[cfg(not(feature = "rhh-instrumentation"))]
fn rhh_ac02_default_build_phone_substring_unchanged() {
assert_eq!(header_hint("phone"), Some("identity.person.phone_number"));
}
#[test]
#[cfg(not(feature = "rhh-instrumentation"))]
fn rhh_ac02_default_build_zip_geography_unchanged() {
assert_eq!(header_hint("zip"), Some("geography.address.postal_code"));
}
#[test]
#[cfg(not(feature = "rhh-instrumentation"))]
fn rhh_ac02_default_build_env_var_ignored() {
unsafe {
std::env::set_var("RHH_DISABLE_HINTS", "substring_matcher_identity");
}
let result = header_hint("phone");
unsafe {
std::env::remove_var("RHH_DISABLE_HINTS");
}
assert_eq!(result, Some("identity.person.phone_number"));
}
#[test]
#[cfg(feature = "rhh-instrumentation")]
fn rhh_ac02_on_feature_disable_scenarios() {
let prior = std::env::var("RHH_DISABLE_HINTS").ok();
let restore = |prior: &Option<String>| match prior {
Some(v) => unsafe { std::env::set_var("RHH_DISABLE_HINTS", v) },
None => unsafe { std::env::remove_var("RHH_DISABLE_HINTS") },
};
unsafe {
std::env::set_var("RHH_DISABLE_HINTS", "substring_matcher_identity");
}
assert_eq!(
header_hint("phone"),
None,
"disabling substring_matcher_identity should silence phone hint"
);
assert_eq!(
header_hint("ipv6"),
Some("technology.internet.ip_v6"),
"disabling identity must not affect technology"
);
unsafe {
std::env::set_var(
"RHH_DISABLE_HINTS",
"header_hint_table,substring_matcher_identity",
);
}
assert_eq!(
header_hint("email"),
None,
"match_table+identity disable should silence email"
);
unsafe {
std::env::set_var("RHH_DISABLE_HINTS", "");
}
assert_eq!(
header_hint("phone"),
Some("identity.person.phone_number"),
"empty disable list must restore baseline"
);
unsafe {
std::env::set_var("RHH_DISABLE_HINTS", " substring_matcher_identity ,, ,");
}
assert_eq!(
header_hint("phone"),
None,
"whitespace and empty tokens must parse cleanly"
);
restore(&prior);
}
#[test]
fn test_slash_date_eu_detected() {
let values: Vec<String> = vec![
"15/01/2024",
"28/06/2023",
"03/11/2022",
"31/12/2019",
"12/05/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, Some("datetime.date.dmy_slash".to_string()));
}
#[test]
fn test_slash_date_mdy_detected() {
let values: Vec<String> = vec![
"01/15/2024",
"06/28/2023",
"11/03/2022",
"12/31/2019",
"05/12/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, Some("datetime.date.mdy_slash".to_string()));
}
#[test]
fn test_slash_date_ambiguous() {
let values: Vec<String> = vec![
"01/02/2024",
"03/04/2023",
"05/06/2022",
"07/08/2021",
"09/10/2020",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_slash_dates(&values);
assert_eq!(result, None);
}
#[test]
fn test_short_date_dmy_detected() {
let values: Vec<String> = vec!["15-01-24", "28-06-23", "31-12-19"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_short_dates(&values);
assert_eq!(result, Some("datetime.date.short_dmy".to_string()));
}
#[test]
fn test_short_date_mdy_detected() {
let values: Vec<String> = vec!["01-15-24", "06-28-23", "12-31-19"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_short_dates(&values);
assert_eq!(result, Some("datetime.date.short_mdy".to_string()));
}
#[test]
fn test_coordinates_longitude_detected() {
let values: Vec<String> = vec!["-74.0060", "151.2093", "-0.1278", "139.6917", "2.3522"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_coordinates(&values);
assert_eq!(result, Some("geography.coordinate.longitude".to_string()));
}
#[test]
fn test_coordinates_latitude_detected() {
let values: Vec<String> = vec!["40.7128", "-33.8688", "51.5074", "35.6762", "-22.9068"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_coordinates(&values);
assert_eq!(result, Some("geography.coordinate.latitude".to_string()));
}
#[test]
fn test_numeric_sequential_detection() {
let values: Vec<String> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.identifier.increment".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = [
("representation.identifier.increment".to_string(), 8),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "representation.identifier.increment");
}
#[test]
fn test_numeric_postal_code_detection() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001", "98101",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "geography.address.postal_code");
}
#[test]
fn test_year_detection() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "2016",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.integer_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("representation.numeric.integer_number".to_string(), 5),
("geography.address.postal_code".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
assert_eq!(rule, "numeric_year_detection");
}
#[test]
fn test_year_detection_historical() {
let values: Vec<String> = vec!["1945", "1918", "1969", "1989", "2001"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.5,
all_scores: vec![],
})
.collect();
let votes = [
("representation.numeric.decimal_number".to_string(), 3),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_year_not_triggered_for_5digit_postal() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001", "98101",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "geography.address.postal_code");
}
#[test]
fn test_sequential_years_still_detected_as_year() {
let values: Vec<String> = vec![
"2001", "2002", "2003", "2004", "2005", "2006", "2007", "2008", "2009", "2010",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.identifier.increment".to_string(),
confidence: 0.7,
all_scores: vec![],
})
.collect();
let votes = [
("representation.identifier.increment".to_string(), 7),
("representation.numeric.integer_number".to_string(), 3),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_sequential_non_year_still_increment() {
let values: Vec<String> = vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.identifier.increment".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = [
("representation.identifier.increment".to_string(), 8),
("representation.numeric.integer_number".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "representation.identifier.increment");
}
#[test]
fn test_year_with_outlier_not_postal_code() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "1776",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "geography.address.postal_code".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("geography.address.postal_code".to_string(), 5),
("representation.numeric.decimal_number".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_year_with_many_outliers_not_year() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "1500", "1600", "1700", "1800", "2022", "2023", "2024",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.integer_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("representation.numeric.integer_number".to_string(), 5),
("geography.address.postal_code".to_string(), 3),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
if let Some((label, _)) = result {
assert_ne!(label, "datetime.component.year");
}
}
#[test]
fn test_year_with_non4digit_outlier() {
let values: Vec<String> = vec![
"2020", "2019", "2021", "2018", "2023", "2015", "2022", "2017", "2024", "NA",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.6,
all_scores: vec![],
})
.collect();
let votes = [
("representation.numeric.decimal_number".to_string(), 8),
("datetime.component.year".to_string(), 2),
];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
assert_eq!(rule, "numeric_year_detection");
}
#[test]
fn test_year_with_decimal_format() {
let values: Vec<String> = vec![
"2020", "2019", "2021.0", "2018", "2023", "2015", "2022", "2017.0", "2024", "2016",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.7,
all_scores: vec![],
})
.collect();
let votes = [("representation.numeric.decimal_number".to_string(), 10)];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_not_year_when_too_few_4digit() {
let values: Vec<String> = vec![
"2020", "2019", "NA", "N/A", "", "2015", "2022", "null", "2024", "missing",
]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.5,
all_scores: vec![],
})
.collect();
let votes = [("representation.numeric.decimal_number".to_string(), 10)];
let top_labels: Vec<&str> = votes.iter().map(|(l, _)| l.as_str()).collect();
let result = disambiguate_numeric(&values, &results, &top_labels);
if let Some((label, _)) = result {
assert_ne!(label, "datetime.component.year");
}
}
#[test]
fn test_empty_column() {
let result = ColumnResult {
label: "unknown".to_string(),
confidence: 0.0,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 0,
detected_locale: None,
is_generic: false,
column_features: None,
};
assert_eq!(result.label, "unknown");
assert_eq!(result.samples_used, 0);
assert_eq!(result.detected_locale, None);
}
#[test]
fn test_strip_locale_suffix_4level_country() {
let (base, locale) = strip_locale_suffix("geography.address.postal_code.EN_US");
assert_eq!(base, "geography.address.postal_code");
assert_eq!(locale, Some("EN_US"));
}
#[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 test_gender_detection_mixed_case() {
let values: Vec<String> = vec![
"male", "female", "Male", "Female", "male", "female", "male", "Female", "male", "Male",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, Some("identity.person.gender".to_string()));
}
#[test]
fn test_gender_detection_single_char_is_gender_code() {
let values: Vec<String> = vec!["M", "F", "M", "F", "M", "F", "M", "F", "M", "F"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, Some("identity.person.gender_code".to_string()));
}
#[test]
fn test_gender_detection_single_char_mixed_case_with_x() {
let values: Vec<String> = vec!["m", "f", "X", "M", "f", "x", "F"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, Some("identity.person.gender_code".to_string()));
}
#[test]
fn test_gender_detection_word_and_code_mix_stays_gender() {
let values: Vec<String> = vec!["male", "M", "female", "F", "male"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, Some("identity.person.gender".to_string()));
}
#[test]
fn test_gender_detection_numeric_codes_do_not_fire() {
let values: Vec<String> = vec!["0", "1", "2", "1", "0", "2"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, None);
}
#[test]
fn test_gender_detection_with_nonbinary() {
let values: Vec<String> = vec![
"Male",
"Female",
"Male",
"Non-binary",
"Female",
"Male",
"Female",
"Male",
"Non-binary",
"Female",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(
result,
Some("identity.person.gender".to_string()),
"Non-binary should be recognized as a valid gender value"
);
}
#[test]
fn test_gender_detection_with_other_inclusive() {
let values: Vec<String> = vec![
"Male",
"Female",
"Other",
"Male",
"Female",
"Prefer not to say",
"Male",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, Some("identity.person.gender".to_string()));
}
#[test]
fn test_gender_detection_fails_for_non_gender() {
let values: Vec<String> = vec!["red", "blue", "green", "red", "blue"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_gender(&values);
assert_eq!(result, None);
}
#[test]
fn test_ipv4_detection_standard_ips() {
let values: Vec<String> = vec![
"192.168.1.1",
"10.0.0.1",
"172.16.0.1",
"8.8.8.8",
"1.2.3.4",
"10.0.0.255",
"192.168.0.100",
"255.255.255.0",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_ipv4(&values);
assert_eq!(
result,
Some("technology.internet.ip_v4".to_string()),
"Standard IPv4 addresses should be detected"
);
}
#[test]
fn test_ipv4_detection_rejects_version_numbers() {
let values: Vec<String> = vec![
"1.0.0", "2.1.3", "3.14.159", "0.2.53", "6.27.84", "4.24.59", "7.23.74",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_ipv4(&values);
assert_eq!(
result, None,
"Version numbers should NOT match IPv4 pattern"
);
}
#[test]
fn test_ipv4_detection_rejects_decimals() {
let values: Vec<String> = vec!["151.3", "165.0", "161.2", "169.1", "181.7"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_ipv4(&values);
assert_eq!(
result, None,
"Decimal numbers should NOT match IPv4 pattern"
);
}
#[test]
fn test_ipv4_detection_mixed_with_some_invalid() {
let values: Vec<String> = vec![
"10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4", "10.0.0.5", "10.0.0.6", "10.0.0.7",
"10.0.0.8", "N/A", "unknown",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_ipv4(&values);
assert_eq!(
result,
Some("technology.internet.ip_v4".to_string()),
"80% valid IPs should trigger detection"
);
}
#[test]
fn test_boolean_override_integer_spread() {
let values: Vec<String> = vec!["0", "1", "2", "3", "0", "1", "4", "0", "5", "8"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec![
"representation.logical.boolean",
"representation.numeric.integer_number",
];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert_eq!(rule, "boolean_override_integer_spread");
}
#[test]
fn test_boolean_override_preserves_real_boolean() {
let values: Vec<String> = vec!["0", "1", "0", "1", "1", "0", "0", "1", "0", "1"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.logical.boolean"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_none());
}
#[test]
fn test_boolean_override_single_char_categorical() {
let values: Vec<String> = vec!["S", "C", "Q", "S", "S", "C", "Q", "S", "S", "C"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.logical.boolean", "representation.text.word"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
assert_eq!(rule, "boolean_override_single_char_categorical");
}
#[test]
fn test_boolean_override_preserves_true_false_chars() {
let values: Vec<String> = vec!["T", "F", "T", "F", "T", "F", "T", "F"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.logical.boolean"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_none());
}
#[test]
fn test_header_hint_email() {
assert_eq!(header_hint("Email"), Some("identity.person.email"));
assert_eq!(header_hint("email_address"), Some("identity.person.email"));
assert_eq!(header_hint("E-Mail"), Some("identity.person.email"));
assert_eq!(header_hint("user_email"), Some("identity.person.email"));
}
#[test]
fn test_header_hint_phone() {
assert_eq!(header_hint("phone"), Some("identity.person.phone_number"));
assert_eq!(
header_hint("Phone Number"),
Some("identity.person.phone_number")
);
assert_eq!(
header_hint("telephone"),
Some("identity.person.phone_number")
);
assert_eq!(header_hint("mobile"), Some("identity.person.phone_number"));
}
#[test]
fn test_header_hint_postal() {
assert_eq!(header_hint("zip"), Some("geography.address.postal_code"));
assert_eq!(
header_hint("zip_code"),
Some("geography.address.postal_code")
);
assert_eq!(
header_hint("Postal Code"),
Some("geography.address.postal_code")
);
assert_eq!(
header_hint("postcode"),
Some("geography.address.postal_code")
);
}
#[test]
fn test_header_hint_names() {
assert_eq!(header_hint("Name"), None);
assert_eq!(header_hint("full_name"), Some("identity.person.full_name"));
assert_eq!(
header_hint("first_name"),
Some("identity.person.first_name")
);
assert_eq!(header_hint("last_name"), Some("identity.person.last_name"));
assert_eq!(header_hint("surname"), Some("identity.person.last_name"));
assert_eq!(header_hint("display name"), None);
assert_eq!(header_hint("user name"), None);
}
#[test]
fn test_header_hint_geo() {
assert_eq!(
header_hint("latitude"),
Some("geography.coordinate.latitude")
);
assert_eq!(header_hint("lat"), Some("geography.coordinate.latitude"));
assert_eq!(
header_hint("longitude"),
Some("geography.coordinate.longitude")
);
assert_eq!(header_hint("lng"), Some("geography.coordinate.longitude"));
assert_eq!(header_hint("country"), Some("geography.location.country"));
assert_eq!(header_hint("city"), Some("geography.location.city"));
}
#[test]
fn test_header_hint_identity() {
assert_eq!(header_hint("gender"), Some("identity.person.gender"));
assert_eq!(header_hint("Sex"), Some("identity.person.gender"));
assert_eq!(
header_hint("age"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("Age"),
Some("representation.numeric.integer_number")
);
}
#[test]
fn test_header_hint_tech() {
assert_eq!(header_hint("url"), Some("technology.internet.url"));
assert_eq!(header_hint("URL"), Some("technology.internet.url"));
assert_eq!(header_hint("website"), Some("technology.internet.url"));
assert_eq!(header_hint("ip_address"), Some("technology.internet.ip_v4"));
assert_eq!(header_hint("uuid"), Some("technology.identifier.uuid"));
}
#[test]
fn test_header_hint_date() {
assert_eq!(header_hint("date"), Some("datetime.timestamp.iso_8601"));
assert_eq!(
header_hint("created_date"),
Some("datetime.timestamp.iso_8601")
);
assert_eq!(header_hint("year"), Some("datetime.component.year"));
assert_eq!(header_hint("birth_date"), Some("datetime.date.iso"));
assert_eq!(header_hint("dob"), Some("datetime.date.iso"));
assert_eq!(
header_hint("rfc_2822_timestamp"),
Some("datetime.timestamp.rfc_2822")
);
assert_eq!(header_hint("rfc2822"), Some("datetime.timestamp.rfc_2822"));
assert_eq!(header_hint("rfc_3339"), Some("datetime.timestamp.rfc_3339"));
assert_eq!(
header_hint("sql_timestamp"),
Some("datetime.timestamp.sql_standard")
);
assert_eq!(
header_hint("unix_epoch"),
Some("datetime.epoch.unix_seconds")
);
assert_eq!(
header_hint("epoch_time"),
Some("datetime.epoch.unix_seconds")
);
assert_eq!(
header_hint("unix_ms"),
Some("datetime.epoch.unix_milliseconds")
);
assert_eq!(
header_hint("created_epoch"),
Some("datetime.epoch.unix_seconds")
);
assert_eq!(
header_hint("timestamp_unix"),
Some("datetime.epoch.unix_seconds")
);
}
#[test]
fn test_header_hint_categorical() {
assert_eq!(header_hint("language"), Some("representation.text.word"));
assert_eq!(header_hint("sport"), Some("representation.text.word"));
assert_eq!(header_hint("species"), Some("representation.text.word"));
assert_eq!(header_hint("exchange"), Some("representation.text.word"));
}
#[test]
fn test_header_hint_measurements() {
assert_eq!(
header_hint("altitude"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("elevation"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("pages"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("heart_rate"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("attendance"),
Some("representation.numeric.integer_number")
);
assert_eq!(header_hint("response_time_ms"), None);
assert_eq!(
header_hint("payload_size_bytes"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("duration_minutes"),
Some("representation.numeric.integer_number")
);
}
#[test]
fn test_header_hint_numeric() {
assert_eq!(header_hint("price"), Some("finance.currency.amount"));
assert_eq!(header_hint("amount"), Some("finance.currency.amount"));
assert_eq!(header_hint("salary"), Some("finance.currency.amount"));
assert_eq!(header_hint("revenue"), Some("finance.currency.amount"));
assert_eq!(header_hint("income"), Some("finance.currency.amount"));
assert_eq!(header_hint("expense"), Some("finance.currency.amount"));
assert_eq!(header_hint("fare"), Some("finance.currency.amount"));
assert_eq!(header_hint("fee"), Some("finance.currency.amount"));
assert_eq!(
header_hint("count"),
Some("representation.numeric.integer_number")
);
assert_eq!(header_hint("id"), None);
}
#[test]
fn test_header_hint_no_match() {
assert_eq!(header_hint("foo"), None);
assert_eq!(header_hint("xyz"), None);
assert_eq!(header_hint("data"), None);
assert_eq!(header_hint("column1"), None);
}
#[test]
fn coord_veto_corroborates_real_coordinate_headers() {
for h in [
"lat",
"latitude",
"lon",
"lng",
"long",
"longitude",
"lat_dd",
"y_lat",
"decimalLatitude",
"gps_lat",
"coord_x",
"wgs84_lat",
"LATITUDE",
"Longitude",
] {
assert!(header_corroborates_coordinate(h), "should corroborate: {h}");
}
}
#[test]
fn coord_veto_does_not_corroborate_false_friends_or_quantities() {
for h in [
"mag",
"mean",
"std",
"rms",
"error",
"depth",
"score",
"rate",
"probability",
"latency",
"translate",
"correlation",
"plate",
"population",
"magnitude",
"temperature",
"elevation_error",
] {
assert!(
!header_corroborates_coordinate(h),
"should NOT corroborate: {h}"
);
}
}
#[test]
fn coord_veto_value_gate_requires_numeric() {
assert!(values_look_like_generic_decimals(&[
"0.12".into(),
"-3.4".into(),
"51.5".into(),
"2.0".into(),
"9.9".into()
]));
assert!(!values_look_like_generic_decimals(&[
"alpha".into(),
"beta".into(),
"gamma".into()
]));
assert!(!values_look_like_generic_decimals(&[]));
}
#[test]
fn coord_veto_is_a_default_on_header_hint() {
assert!(!rhh::is_disabled("header_hint_coord_veto"));
}
#[test]
fn state_code_header_corroboration() {
for h in [
"State",
"Provider State",
"DL State",
"ADDRESS STATE",
"province",
] {
assert!(header_corroborates_state(h), "should corroborate: {h}");
}
for h in [
"statement",
"status",
"real estate",
"estate",
"country",
"city",
] {
assert!(!header_corroborates_state(h), "should NOT corroborate: {h}");
}
}
#[test]
fn region_header_corroboration() {
for h in [
"Region",
"County",
"district",
"borough",
"work_location_borough",
"province",
] {
assert!(header_corroborates_region(h), "should corroborate: {h}");
}
for h in ["city", "town", "name", "regional_office", "districting"] {
assert!(
!header_corroborates_region(h),
"should NOT corroborate: {h}"
);
}
}
#[test]
fn state_code_value_vocab_gate() {
assert!(values_look_like_state_codes(&[
"GA".into(),
"FL".into(),
"NM".into(),
"TX".into(),
]));
assert!(values_look_like_state_codes(&[
"ca".into(),
"ny".into(),
"wa".into(),
]));
assert!(values_look_like_state_codes(&[
"NSW".into(),
"VIC".into(),
"QLD".into(),
]));
assert!(!values_look_like_state_codes(&[
"California".into(),
"Florida".into(),
"Texas".into(),
]));
assert!(!values_look_like_state_codes(&[
"GB".into(),
"FR".into(),
"DE".into(),
"JP".into(),
"CN".into(),
]));
assert!(!values_look_like_state_codes(&["TX".into(), "FL".into()]));
}
#[test]
fn state_code_promote_is_a_default_on_header_hint() {
assert!(!rhh::is_disabled("header_hint_state_code_promote"));
}
#[test]
fn amount_bare_number_gate() {
let (bare, dec) = values_look_like_bare_numbers(&[
"84000000".into(),
"-14638000".into(),
"795000000".into(),
"0".into(),
]);
assert!(bare && !dec);
let (bare, dec) = values_look_like_bare_numbers(&[
"-270269883.0".into(),
"396458184.0".into(),
"2701751944.0".into(),
]);
assert!(bare && dec);
let (bare, _) =
values_look_like_bare_numbers(&["£45.17".into(), "£23.88".into(), "£35.02".into()]);
assert!(!bare);
let (bare, _) = values_look_like_bare_numbers(&[
"EUR 4 459 807".into(),
"EUR 4 626 565".into(),
"EUR 4 652 581".into(),
]);
assert!(!bare);
let (bare, _) = values_look_like_bare_numbers(&["100".into(), "200".into()]);
assert!(!bare);
}
#[test]
fn amount_bare_number_veto_is_default_on() {
assert!(!rhh::is_disabled("amount_bare_number_veto"));
}
#[test]
fn url_bare_number_veto_is_default_on() {
assert!(!rhh::is_disabled("url_bare_number_veto"));
}
#[test]
fn country_code_corroboration_is_default_on() {
assert!(!rhh::is_disabled("country_code_corroboration"));
}
#[test]
fn binary_vocab_veto_is_default_on() {
assert!(!rhh::is_disabled("binary_vocab_veto"));
}
#[test]
fn increment_substance_veto_is_default_on() {
assert!(!rhh::is_disabled("increment_substance_veto"));
}
#[test]
fn values_form_increment_accepts_genuine_auto_increment() {
let vals: Vec<String> = (1..=200).map(|i| i.to_string()).collect();
assert_eq!(values_form_increment(&vals), Some(true));
let gapped: Vec<String> = (1..=200)
.filter(|i| i % 10 != 0)
.map(|i| i.to_string())
.collect();
assert_eq!(values_form_increment(&gapped), Some(true));
}
#[test]
fn values_form_increment_rejects_evenly_spaced_non_increment() {
let spaced: Vec<String> = (1..=50).map(|i| (i * 100).to_string()).collect();
assert_eq!(values_form_increment(&spaced), Some(false));
let measures: Vec<String> = ["3", "7", "3", "42", "7", "108", "3", "9", "256", "42"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(values_form_increment(&measures), Some(false));
}
#[test]
fn values_form_increment_abstains_on_too_few_or_nonnumeric() {
assert_eq!(
values_form_increment(&["1".into(), "2".into(), "3".into()]),
None
);
let words: Vec<String> = ["a", "b", "c", "d", "e", "f"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(values_form_increment(&words), None);
}
#[test]
fn checksum_substance_guard_is_default_on() {
assert!(!rhh::is_disabled("checksum_substance_guard"));
}
#[test]
fn isbn_checksum_distinguishes_genuine_from_lookalikes() {
use finetype_core::checksum::isbn;
for v in [
"0306406152",
"043942089X",
"9780306406157",
"978-3-16-148410-0",
] {
assert!(isbn(v), "should be valid ISBN: {v}");
}
for v in ["5150000128", "6965100000", "7586000000", "1041000000"] {
assert!(!isbn(v), "should NOT be valid ISBN: {v}");
}
assert!(!isbn("-1617000000"));
}
#[test]
fn url_bare_number_gate() {
let (bare, dec) =
values_look_like_bare_numbers(&["0".into(), "1".into(), "0".into(), "-1".into()]);
assert!(bare && !dec);
let (bare, _) = values_look_like_bare_numbers(&[
"https://example.com/a".into(),
"http://foo.org".into(),
"https://bar.net/x".into(),
]);
assert!(!bare);
}
#[test]
fn postal_veto_corroborates_real_postal_headers() {
for h in [
"zip",
"ZIP",
"zip_code",
"zipcode",
"business_zip",
"billing zip",
"postal_code",
"PostalCode",
"postcode",
"post_code",
"PLZ",
"cep",
"pincode",
"pin_code_area_pincode",
] {
assert!(header_corroborates_postal(h), "should corroborate: {h}");
}
}
#[test]
fn postal_veto_does_not_corroborate_false_friends_or_quantities() {
for h in [
"zipper",
"unzip",
"gzip_size",
"averageDailyVolume10Day",
"fullTimeEmployees",
"totalCurrentLiabilities",
"conversation_no",
"voteId",
"d_week_seq",
"destination_x",
"sId",
"1001",
"code",
"postage_paid",
] {
assert!(
!header_corroborates_postal(h),
"should NOT corroborate: {h}"
);
}
}
#[test]
fn postal_veto_value_gate_requires_bare_integers_without_leading_zeros() {
assert!(values_look_like_generic_integers(&[
"1191".into(),
"1497".into(),
"2200".into(),
"5322".into()
]));
assert!(!values_look_like_generic_integers(&[
"01219".into(),
"1191".into(),
"1497".into()
]));
assert!(!values_look_like_generic_integers(&[
"12.5".into(),
"9.1".into(),
"3.3".into()
]));
assert!(!values_look_like_generic_integers(&[
"12345-6789".into(),
"98765-4321".into()
]));
assert!(!values_look_like_generic_integers(&[]));
}
#[test]
fn postal_veto_is_a_default_on_header_hint() {
assert!(!rhh::is_disabled("header_hint_postal_veto"));
}
#[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 ac01_r25_http_status_gate_preserves_real_postal_codes() {
let values: Vec<String> = vec![
"10001", "90210", "33139", "60601", "02134", "94102", "30308",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.numeric.integer_number", 0.8, None);
assert!(result.is_some(), "R12 should fire on 5-digit postal codes");
let (label, _) = result.unwrap();
assert_eq!(label, "geography.address.postal_code");
}
#[test]
fn ac01_r25_http_status_gate_preserves_3digit_postal() {
let values: Vec<String> = vec!["601", "900", "750", "602", "850", "700", "801"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.numeric.integer_number", 0.8, None);
if let Some((label, _)) = &result {
assert_eq!(
label, "geography.address.postal_code",
"3-digit values outside 100-599 should still be postal_code"
);
}
}
#[test]
fn ac03_r27_year_vs_compact_ym_fires_on_years() {
let values: Vec<String> = vec!["2022", "2021", "2023", "2020", "2019"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.compact_ym", 0.8, None);
assert!(result.is_some(), "R27 should fire on 4-digit years");
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
assert!(rule.starts_with("year_compact_ym_gate:"));
}
#[test]
fn ac03_r27_year_vs_compact_ym_preserves_real_compact_ym() {
let values: Vec<String> = vec!["202201", "202312", "202406"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.compact_ym", 0.8, None);
if let Some((_, rule)) = &result {
assert!(
!rule.starts_with("year_compact_ym_gate:"),
"R27 should not fire on 6-digit compact_ym values"
);
}
}
#[test]
fn ac03_r27_year_range_rejects_non_year_4digit() {
let values: Vec<String> = vec!["1234", "5678", "9012", "3456"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.compact_ym", 0.8, None);
if let Some((_, rule)) = &result {
assert!(
!rule.starts_with("year_compact_ym_gate:"),
"R27 should not fire on non-year 4-digit values"
);
}
}
#[test]
fn v15_email_display_guard() {
assert_eq!(header_hint("email_display"), None);
assert_eq!(header_hint("email"), Some("identity.person.email"));
assert_eq!(header_hint("customer_email"), Some("identity.person.email"));
}
#[test]
fn v15_phone_e164_guard() {
assert_eq!(header_hint("phone_e164"), None);
assert_eq!(header_hint("phone"), Some("identity.person.phone_number"));
assert_eq!(
header_hint("phone_number"),
Some("identity.person.phone_number")
);
}
#[test]
fn v15_ip_port_guard() {
assert_eq!(header_hint("ip_v4_with_port"), None);
assert_eq!(header_hint("ip_address"), Some("technology.internet.ip_v4"));
assert_eq!(header_hint("server_ip"), Some("technology.internet.ip_v4"));
}
#[test]
fn v15_upc_maps_to_upc_not_ean() {
assert_eq!(header_hint("upc"), Some("identity.commerce.upc"));
assert_eq!(header_hint("ean"), Some("identity.commerce.ean"));
assert_eq!(header_hint("barcode"), Some("identity.commerce.ean"));
}
#[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 state_hint_uses_live_region_leaf() {
assert_ne!(header_hint("state"), Some("geography.location.state"));
assert_ne!(header_hint("province"), Some("geography.location.state"));
}
#[test]
fn v15_subcountry_not_mapped_to_state() {
assert_eq!(header_hint("subcountry"), None);
}
#[test]
fn r31_version_overrides_dmy_short_dot_on_invalid_dates() {
let values: Vec<String> = vec!["0.2.53", "6.27.84", "4.24.59", "7.23.74", "5.14.89"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.dmy_short_dot", 0.99, None);
assert!(result.is_some(), "R31 should fire on version-like values");
let (label, rule) = result.unwrap();
assert_eq!(label, "technology.development.version");
assert!(rule.starts_with("version_dmy_short_dot_gate:"));
}
#[test]
fn r31_preserves_real_dmy_short_dot() {
let values: Vec<String> = vec!["15.06.23", "01.12.22", "28.03.24", "10.01.21", "05.09.20"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.dmy_short_dot", 0.99, None);
if let Some((_, rule)) = &result {
assert!(
!rule.starts_with("version_dmy_short_dot_gate:"),
"R31 should not fire on valid DMY dates"
);
}
}
#[test]
fn r31_borderline_valid_dates_stay_dmy() {
let values: Vec<String> = vec!["10.6.2", "9.1.11", "3.1.16", "8.4.8", "5.3.7"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "datetime.date.dmy_short_dot", 0.99, None);
if let Some((_, rule)) = &result {
assert!(
!rule.starts_with("version_dmy_short_dot_gate:"),
"R31 should not fire when most values are valid dates"
);
}
}
#[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_header_hint_coverage() {
let test_headers = vec![
"email",
"phone",
"zip",
"postal",
"name",
"full_name",
"first_name",
"last_name",
"latitude",
"longitude",
"country",
"city",
"state",
"gender",
"url",
"ip",
"uuid",
"port",
"date",
"year",
"password",
"price",
"amount",
"count",
"address",
"street",
];
let matches: Vec<&str> = test_headers
.iter()
.filter(|h| header_hint(h).is_some())
.copied()
.collect();
assert!(
matches.len() >= 20,
"Expected at least 20 matches, got {}: {:?}",
matches.len(),
matches
);
}
#[test]
fn test_boolean_override_with_current_model_label() {
let values: Vec<String> = vec!["0", "1", "2", "3", "0", "1", "4", "0", "5", "8"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec![
"technology.development.boolean",
"representation.numeric.integer_number",
];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(
result.is_some(),
"Boolean override must trigger for technology.development.boolean"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert_eq!(rule, "boolean_override_integer_spread");
}
#[test]
fn test_boolean_override_preserves_real_boolean_current_label() {
let values: Vec<String> = vec!["0", "1", "0", "1", "1", "0", "0", "1", "0", "1"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["technology.development.boolean"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(
result.is_none(),
"Real boolean {{0,1}} must not be overridden"
);
}
#[test]
fn class_rank_grade_headers_no_longer_hint_ordinal() {
assert_eq!(header_hint("Pclass"), None);
assert_eq!(header_hint("class"), None);
assert_eq!(header_hint("grade"), None);
assert_eq!(header_hint("rank"), None);
assert_eq!(header_hint("tier"), None);
}
#[test]
fn test_header_hint_count_columns() {
assert_eq!(
header_hint("SibSp"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("Parch"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("siblings"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("children"),
Some("representation.numeric.integer_number")
);
assert_eq!(
header_hint("qty"),
Some("representation.numeric.integer_number")
);
}
#[test]
fn test_header_hint_survival_columns() {
assert_eq!(
header_hint("Survived"),
Some("representation.boolean.binary")
);
assert_eq!(header_hint("alive"), Some("representation.boolean.binary"));
assert_eq!(header_hint("active"), Some("representation.boolean.binary"));
}
#[test]
fn test_header_hint_ticket_cabin() {
assert_eq!(
header_hint("Ticket"),
Some("representation.identifier.alphanumeric_id")
);
assert_eq!(
header_hint("Cabin"),
Some("representation.identifier.alphanumeric_id")
);
assert_eq!(
header_hint("seat"),
Some("representation.identifier.alphanumeric_id")
);
}
#[test]
fn test_header_hint_embarked() {
assert_eq!(header_hint("Embarked"), Some("representation.text.word"));
assert_eq!(header_hint("terminal"), Some("representation.text.word"));
}
#[test]
fn test_header_hint_fare() {
assert_eq!(header_hint("Fare"), Some("finance.currency.amount"));
assert_eq!(header_hint("fee"), Some("finance.currency.amount"));
}
#[test]
fn compound_class_headers_no_longer_hint_ordinal() {
assert_eq!(header_hint("passenger_class"), None);
assert_eq!(header_hint("skill_grade"), None);
}
#[test]
fn test_si_number_override_plain_decimals() {
let values: Vec<String> = vec!["5.1", "3.5", "1.4", "7.9", "0.2", "4.6"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_si_number(&values);
assert_eq!(
result,
Some((
"representation.numeric.decimal_number".to_string(),
"si_number_override_no_suffix".to_string()
))
);
}
#[test]
fn test_si_number_override_real_si_values() {
let values: Vec<String> = vec!["5.1K", "3.5M", "1.4B"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_si_number(&values);
assert_eq!(result, None);
}
#[test]
fn test_si_number_override_mixed_values() {
let values: Vec<String> = vec!["5.1", "3.5K", "1.4"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_si_number(&values);
assert_eq!(result, None);
}
#[test]
fn test_si_number_override_negative_decimals() {
let values: Vec<String> = vec!["-450.12", "732.57", "-1.003", "98.6"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_si_number(&values);
assert_eq!(
result,
Some((
"representation.numeric.decimal_number".to_string(),
"si_number_override_no_suffix".to_string()
))
);
}
#[test]
fn test_si_number_override_empty_values() {
let values: Vec<String> = vec!["", " ", ""].into_iter().map(String::from).collect();
let result = disambiguate_si_number(&values);
assert_eq!(
result,
Some((
"representation.numeric.decimal_number".to_string(),
"si_number_override_no_suffix".to_string()
))
);
}
#[test]
fn test_day_of_week_full_names() {
let values: Vec<String> = vec![
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
"Monday",
"Friday",
"Wednesday",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, Some("datetime.component.day_of_week".to_string()));
}
#[test]
fn test_day_of_week_abbreviated() {
let values: Vec<String> = vec![
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Fri", "Wed",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, Some("datetime.component.day_of_week".to_string()));
}
#[test]
fn test_day_of_week_two_letter() {
let values: Vec<String> = vec!["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, Some("datetime.component.day_of_week".to_string()));
}
#[test]
fn test_day_of_week_not_triggered_for_names() {
let values: Vec<String> = vec!["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, None);
}
#[test]
fn test_day_of_week_too_few_values() {
let values: Vec<String> = vec!["Monday", "Tuesday"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, None);
}
#[test]
fn test_day_of_week_below_threshold() {
let values: Vec<String> = vec![
"Monday", "Apple", "Banana", "Cherry", "Date", "Fig", "Grape", "Tuesday", "Kiwi", "Lemon",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_day_of_week(&values);
assert_eq!(result, None);
}
#[test]
fn test_month_name_full_names() {
let values: Vec<String> = vec![
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_month_name(&values);
assert_eq!(result, Some("datetime.component.month_name".to_string()));
}
#[test]
fn test_month_name_abbreviated() {
let values: Vec<String> = vec![
"Jan", "Feb", "Mar", "Apr", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_month_name(&values);
assert_eq!(result, Some("datetime.component.month_name".to_string()));
}
#[test]
fn test_month_name_mixed_case() {
let values: Vec<String> = vec![
"january", "FEBRUARY", "March", "april", "MAY", "June", "july", "AUGUST",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_month_name(&values);
assert_eq!(result, Some("datetime.component.month_name".to_string()));
}
#[test]
fn test_month_name_not_triggered_for_names() {
let values: Vec<String> = vec![
"Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen", "Ivan", "Jack",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_month_name(&values);
assert_eq!(result, None);
}
#[test]
fn test_month_name_too_few_values() {
let values: Vec<String> = vec!["January", "February"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_month_name(&values);
assert_eq!(result, None);
}
#[test]
fn test_boolean_subtype_terms() {
let values: Vec<String> = vec![
"True", "False", "True", "True", "False", "True", "False", "False", "True", "False",
]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.terms"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.boolean.terms");
assert_eq!(rule, "boolean_subtype_terms");
}
#[test]
fn test_boolean_subtype_binary() {
let values: Vec<String> = vec!["0", "1", "0", "1", "1", "0", "0", "1", "0", "1"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.boolean.binary");
assert_eq!(rule, "boolean_subtype_binary");
}
#[test]
fn test_boolean_subtype_initials() {
let values: Vec<String> = vec!["T", "F", "T", "F", "T", "T", "F", "F", "T", "F"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.initials"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_some());
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.boolean.initials");
assert_eq!(rule, "boolean_subtype_initials");
}
#[test]
fn test_boolean_subtype_yes_no() {
let values: Vec<String> = vec!["yes", "no", "yes", "yes", "no", "no", "yes", "no"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.terms"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "representation.boolean.terms");
}
#[test]
fn test_boolean_subtype_override_categorical() {
let values: Vec<String> = vec![
"True", "False", "True", "True", "False", "True", "False", "False",
]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.discrete.categorical"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(
result.is_some(),
"Should override categorical for True/False column"
);
let (label, _) = result.unwrap();
assert_eq!(label, "representation.boolean.terms");
}
#[test]
fn test_boolean_subtype_not_triggered_for_mixed() {
let values: Vec<String> = vec!["yes", "no", "maybe", "yes", "unknown", "no"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.text.word"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_none());
}
#[test]
fn test_boolean_subtype_too_few_values() {
let values: Vec<String> = vec!["True", "False"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.terms"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_none());
}
#[test]
fn test_boolean_subtype_skewed_integers_not_binary() {
let values: Vec<String> = vec![
"0", "1", "0", "1", "0", "0", "1", "0", "0", "1", "2", "0", "3", "0", "1", "0", "0", "1",
"4", "0",
]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec![
"representation.numeric.integer_number",
"representation.boolean.binary",
];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(
result.is_none(),
"Skewed integer column (SibSp-like) should not be classified as boolean"
);
}
#[test]
fn test_boolean_subtype_pure_binary_still_works() {
let values: Vec<String> = vec!["0", "1", "1", "0", "0", "1", "0", "1", "1", "0"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(result.is_some());
let (label, _) = result.unwrap();
assert_eq!(label, "representation.boolean.binary");
}
#[test]
fn test_boolean_override_all_zeros() {
let values: Vec<String> = vec!["0", "0", "0", "0", "0", "0", "0", "0"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_some(), "All-zero column should be overridden");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert_eq!(rule, "boolean_override_single_value");
}
#[test]
fn test_boolean_override_all_ones() {
let values: Vec<String> = vec!["1", "1", "1", "1", "1"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_some(), "All-ones column should be overridden");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert_eq!(rule, "boolean_override_single_value");
}
#[test]
fn test_boolean_override_skewed_95pct() {
let mut values: Vec<String> = vec!["0"; 19].into_iter().map(String::from).collect();
values.push("1".to_string());
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(
result.is_none(),
"95% skew should still allow binary (boundary)"
);
}
#[test]
fn test_boolean_override_skewed_above_95pct() {
let mut values: Vec<String> = vec!["0"; 48].into_iter().map(String::from).collect();
for _ in 0..2 {
values.push("1".to_string());
}
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(
result.is_some(),
"96% skew should override binary to integer"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert_eq!(rule, "boolean_override_skewed");
}
#[test]
fn test_boolean_override_balanced_binary_preserved() {
let values: Vec<String> = vec!["0", "1", "0", "1", "0", "1", "0", "1"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_override(&values, &top_labels);
assert!(result.is_none(), "Balanced binary should not be overridden");
}
#[test]
fn test_boolean_subtype_all_zeros_rejected() {
let values: Vec<String> = vec!["0", "0", "0", "0", "0"]
.into_iter()
.map(String::from)
.collect();
let top_labels = vec!["representation.boolean.binary"];
let result = disambiguate_boolean_subtype(&values, &top_labels);
assert!(
result.is_none(),
"All-zero through subtype path should return None"
);
}
#[test]
fn test_epoch_seconds_in_range() {
let values: Vec<String> = vec![
"1577836800", "1609459200", "1640995200", "1672531200", "1704067200", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert_eq!(result, Some("datetime.epoch.unix_seconds".to_string()));
}
#[test]
fn test_epoch_seconds_pre_2000_not_detected() {
let values: Vec<String> = vec![
"631152000", "662688000", "694224000", "725846400", "757382400", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert!(
result.is_none(),
"Pre-2000 timestamps should not be detected"
);
}
#[test]
fn test_epoch_seconds_post_2050_not_detected() {
let values: Vec<String> = vec![
"2524608001", "2556144000", "2587680000", "2619216000", "2650752000", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert!(
result.is_none(),
"Post-2050 timestamps should not be detected"
);
}
#[test]
fn test_epoch_milliseconds_detected() {
let values: Vec<String> = vec![
"1577836800000", "1609459200000", "1640995200000", "1672531200000", "1704067200000", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert_eq!(result, Some("datetime.epoch.unix_milliseconds".to_string()));
}
#[test]
fn test_epoch_seconds_threshold_boundary() {
let values: Vec<String> = vec![
"1577836800", "1609459200", "1640995200", "1672531200", "12345", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert_eq!(
result,
Some("datetime.epoch.unix_seconds".to_string()),
"80% in-range should still detect"
);
}
#[test]
fn test_epoch_seconds_below_threshold() {
let values: Vec<String> = vec![
"1577836800", "1609459200", "12345", "67890", "99999", ]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert!(result.is_none(), "40% in-range should not detect");
}
#[test]
fn test_epoch_seconds_small_integers_not_detected() {
let values: Vec<String> = vec!["100", "200", "50", "75", "150"]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert!(result.is_none(), "Small integers should not be epoch");
}
#[test]
fn test_epoch_seconds_floats_with_zero_fract() {
let values: Vec<String> = vec![
"1577836800.0",
"1609459200.0",
"1640995200.0",
"1672531200.0",
"1704067200.0",
]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert_eq!(
result,
Some("datetime.epoch.unix_seconds".to_string()),
"Float-stored epoch values should be detected"
);
}
#[test]
fn test_epoch_seconds_too_few_values() {
let values: Vec<String> = vec!["1577836800", "1609459200"]
.into_iter()
.map(String::from)
.collect();
let result = detect_epoch_seconds(&values);
assert!(result.is_none(), "Too few values should not detect");
}
#[test]
fn test_classify_column_with_semantic_hint() {
use crate::semantic::SemanticHintClassifier;
let model_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("models")
.join("model2vec");
if !model_dir.join("model.safetensors").exists() {
eprintln!("Skipping semantic column integration test: models/model2vec not found");
return;
}
let semantic = SemanticHintClassifier::load(&model_dir).unwrap();
let base_classifier =
crate::inference::MockClassifier::new("representation.numeric.decimal_number");
let column_classifier = ColumnClassifier::with_semantic_hint(
Box::new(base_classifier),
ColumnConfig::default(),
semantic,
);
let values: Vec<String> = vec!["72.5", "85.0", "63.2", "90.1"]
.into_iter()
.map(String::from)
.collect();
let result = column_classifier
.classify_column_with_header(&values, "weight_kg")
.unwrap();
assert_eq!(
result.label, "identity.person.weight",
"Semantic hint for 'weight_kg' should override generic decimal_number"
);
let result2 = column_classifier
.classify_column_with_header(&values, "col1")
.unwrap();
assert_eq!(
result2.label, "representation.numeric.decimal_number",
"Generic 'col1' should not trigger semantic override"
);
}
#[test]
fn test_attractor_confidence_demotion() {
let values: Vec<String> = vec![
"1500", "2300", "45000", "800", "99", "12", "5600", "340", "78", "4100",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let result = disambiguate_attractor_demotion(&values, &votes, 10, None);
assert!(
result.is_some(),
"Should demote postal_code at 0.6 confidence"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert!(rule.starts_with("attractor_demotion_confidence:"));
}
#[test]
fn test_attractor_cardinality_demotion() {
let values: Vec<String> = vec![
"Soccer", "Baseball", "Tennis", "Hockey", "Soccer", "Baseball", "Tennis", "Hockey",
"Soccer", "Baseball",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("identity.person.first_name".to_string(), 9),
("representation.text.word".to_string(), 1),
];
let result = disambiguate_attractor_demotion(&values, &votes, 10, None);
assert!(
result.is_some(),
"Should demote first_name with 4 unique values"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
assert!(rule.starts_with("attractor_demotion_cardinality:"));
}
#[test]
fn test_attractor_cardinality_single_value() {
let values: Vec<String> = vec![
"airport", "airport", "airport", "airport", "airport", "airport", "airport", "airport",
"airport", "airport",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("identity.person.first_name".to_string(), 10)];
let result = disambiguate_attractor_demotion(&values, &votes, 10, None);
assert!(
result.is_some(),
"Should demote first_name with 1 unique value"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
assert!(rule.starts_with("attractor_demotion_cardinality:"));
}
#[test]
fn test_attractor_validation_confirmed_skips_signal2() {
let values: Vec<String> = vec!["EGLL", "KJFK", "LFPG", "EDDF", "RJTT", "VHHH"]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.transportation.icao_code".to_string(), 6),
("representation.identifier.alphanumeric_id".to_string(), 4),
];
let yaml = r#"
geography.transportation.icao_code:
title: "ICAO Code"
validation:
type: string
pattern: "^[A-Z]{4}$"
minLength: 4
maxLength: 4
tier: [VARCHAR, geography, transportation]
release_priority: 5
samples: ["EGLL"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = disambiguate_attractor_demotion(&values, &votes, 10, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote ICAO codes when validation confirms them"
);
}
#[test]
fn test_schema_fail_demotion_measurement_unit_to_categorical() {
let values: Vec<String> = vec!["us", "ci", "nc", "us", "nn", "ci", "us", "nc"]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
representation.scientific.measurement_unit:
title: "Measurement Unit"
validation:
type: string
enum: [meter, kilogram, second, m, kg, s]
tier: [VARCHAR, scientific]
release_priority: 3
samples: ["meter"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"representation.scientific.measurement_unit",
0.9,
Some(&taxonomy),
);
let (label, rule) = result.expect("network codes should demote off measurement_unit");
assert_eq!(label, "representation.text.word");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_geohash_to_alphanumeric_id() {
let values: Vec<String> = (0..30).map(|i| format!("ci40{:06}", i)).collect();
let yaml = r#"
geography.coordinate.geohash:
title: "Geohash"
validation:
type: string
pattern: "^[0-9b-hjkmnp-z]{6,12}$"
tier: [VARCHAR, geography, coordinate]
release_priority: 5
samples: ["u4pruyd"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"geography.coordinate.geohash",
0.9,
Some(&taxonomy),
);
let (label, rule) = result.expect("event ids should demote off geohash");
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_partial_fail_kept() {
let mut values: Vec<String> = (0..25).map(|i| format!("bcd{:03}", i)).collect();
values.extend((0..5).map(|i| format!("icd{:03}", i))); let yaml = r#"
geography.coordinate.geohash:
title: "Geohash"
validation:
type: string
pattern: "^[0-9b-hjkmnp-z]{6,12}$"
tier: [VARCHAR, geography, coordinate]
release_priority: 5
samples: ["u4pruyd"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"geography.coordinate.geohash",
0.9,
Some(&taxonomy),
);
assert!(
result.is_none(),
"partial-fail column must be kept, not demoted (>50% bar spares genuine columns)"
);
}
#[test]
fn test_schema_fail_demotion_keeps_real_geohash() {
let values: Vec<String> = (0..30).map(|i| format!("bcd{:03}", i)).collect();
let yaml = r#"
geography.coordinate.geohash:
title: "Geohash"
validation:
type: string
pattern: "^[0-9b-hjkmnp-z]{6,12}$"
tier: [VARCHAR, geography, coordinate]
release_priority: 5
samples: ["u4pruyd"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"geography.coordinate.geohash",
0.9,
Some(&taxonomy),
);
assert!(
result.is_none(),
"valid geohash values must not be demoted off geohash"
);
}
#[test]
fn test_schema_fail_demotion_keeps_real_units() {
let values: Vec<String> = vec!["meter", "kg", "second", "m", "kg", "meter"]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
representation.scientific.measurement_unit:
title: "Measurement Unit"
validation:
type: string
enum: [meter, kilogram, second, m, kg, s]
tier: [VARCHAR, scientific]
release_priority: 3
samples: ["meter"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"representation.scientific.measurement_unit",
0.9,
Some(&taxonomy),
);
assert!(
result.is_none(),
"real SI-unit values must not be demoted off measurement_unit"
);
}
#[test]
fn test_schema_fail_demotion_utc_integers_to_categorical() {
let values: Vec<String> = vec!["5", "100", "23", "5", "100", "7", "23", "5"]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
datetime.offset.utc:
title: "UTC Offset"
validation:
type: string
pattern: "^UTC [+-]\\d{2}:\\d{2}$"
tier: [VARCHAR, datetime, offset]
release_priority: 4
samples: ["UTC +05:00"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "datetime.offset.utc", 0.9, Some(&taxonomy));
let (label, rule) = result.expect("integers should demote off datetime.offset.utc");
assert_eq!(label, "representation.text.word");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_keeps_real_utc() {
let values: Vec<String> = vec![
"UTC +05:00",
"UTC -08:00",
"UTC +00:00",
"UTC +05:30",
"UTC -03:00",
"UTC +09:00",
]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
datetime.offset.utc:
title: "UTC Offset"
validation:
type: string
pattern: "^UTC [+-]\\d{2}:\\d{2}$"
tier: [VARCHAR, datetime, offset]
release_priority: 4
samples: ["UTC +05:00"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "datetime.offset.utc", 0.9, Some(&taxonomy));
assert!(
result.is_none(),
"real UTC-offset values must not be demoted off datetime.offset.utc"
);
}
#[test]
fn test_schema_fail_demotion_url_codes_to_alphanumeric_id() {
let values: Vec<String> = (0..30).map(|i| format!("ABC{:05}", i)).collect();
let yaml = r#"
technology.internet.url:
title: "URL"
validation:
type: string
pattern: "^(?:https?|ftp|file)://[^\\s]+$"
tier: [VARCHAR, technology, internet]
release_priority: 4
samples: ["https://example.com"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "technology.internet.url", 0.9, Some(&taxonomy));
let (label, rule) = result.expect("bare codes should demote off technology.internet.url");
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_keeps_real_url() {
let values: Vec<String> = vec![
"https://example.com",
"http://foo.org/path",
"https://a.b.c/x?y=1",
"ftp://files.example.com",
"https://news.site/article",
"http://localhost:8080",
]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
technology.internet.url:
title: "URL"
validation:
type: string
pattern: "^(?:https?|ftp|file)://[^\\s]+$"
tier: [VARCHAR, technology, internet]
release_priority: 4
samples: ["https://example.com"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "technology.internet.url", 0.9, Some(&taxonomy));
assert!(
result.is_none(),
"real URL values must not be demoted off technology.internet.url"
);
}
#[test]
fn test_schema_fail_demotion_prose_off_wkt() {
let values: Vec<String> = (0..30)
.map(|i| format!("Provides consulting and advisory services to sector {}", i))
.collect();
let yaml = r#"
geography.format.wkt:
title: "Well-Known Text Geometry"
validation:
type: string
pattern: "^(POINT|LINESTRING|POLYGON|MULTI(POINT|LINESTRING|POLYGON)|GEOMETRYCOLLECTION)\\s*(Z|M|ZM)?\\s*(\\(|EMPTY)"
tier: [VARCHAR, format]
release_priority: 3
samples: ["POINT (30 10)"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "geography.format.wkt", 0.9, Some(&taxonomy));
let (label, rule) = result.expect("prose should demote off geography.format.wkt");
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_keeps_real_wkt() {
let values: Vec<String> = vec![
"POINT (30 10)",
"LINESTRING (30 10, 10 30, 40 40)",
"POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))",
"MULTIPOINT ((10 40), (40 30))",
"POINT Z (1 2 3)",
"GEOMETRYCOLLECTION EMPTY",
]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
geography.format.wkt:
title: "Well-Known Text Geometry"
validation:
type: string
pattern: "^(POINT|LINESTRING|POLYGON|MULTI(POINT|LINESTRING|POLYGON)|GEOMETRYCOLLECTION)\\s*(Z|M|ZM)?\\s*(\\(|EMPTY)"
tier: [VARCHAR, format]
release_priority: 3
samples: ["POINT (30 10)"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "geography.format.wkt", 0.9, Some(&taxonomy));
assert!(
result.is_none(),
"real WKT values must not be demoted off geography.format.wkt"
);
}
#[test]
fn test_schema_fail_demotion_junk_off_user_agent() {
let values: Vec<String> = (0..30)
.map(|i| format!("app-build-{:04}.release", i))
.collect();
let yaml = r#"
technology.internet.user_agent:
title: "User Agent String"
validation:
type: string
pattern: "^(Mozilla/|curl/|python-requests/|Wget/|Go-http-client/|axios/|PostmanRuntime/|kube-probe/|Java/|okhttp/|Apache-HttpClient/|libcurl/|node-fetch/|Dalvik/|CFNetwork/|Lynx/|Links |Scrapy/|Googlebot/|Bingbot/|Slackbot|Twitterbot/|facebookexternalhit/|LinkedInBot/|Prometheus/|Datadog/|Ruby/|Dart/|grpc-|HTTPie/|bot|spider|crawl)"
minLength: 10
maxLength: 500
tier: [VARCHAR, internet]
release_priority: 3
samples: ["curl/7.64.1"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"technology.internet.user_agent",
0.9,
Some(&taxonomy),
);
let (label, rule) = result.expect("bare build ids should demote off user_agent");
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_keeps_real_user_agent() {
let values: Vec<String> = vec![
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)",
"curl/7.64.1",
"python-requests/2.28.1",
"Googlebot/2.1 (+http://www.google.com/bot.html)",
"okhttp/4.9.3 android-client",
]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
technology.internet.user_agent:
title: "User Agent String"
validation:
type: string
pattern: "^(Mozilla/|curl/|python-requests/|Wget/|Go-http-client/|axios/|PostmanRuntime/|kube-probe/|Java/|okhttp/|Apache-HttpClient/|libcurl/|node-fetch/|Dalvik/|CFNetwork/|Lynx/|Links |Scrapy/|Googlebot/|Bingbot/|Slackbot|Twitterbot/|facebookexternalhit/|LinkedInBot/|Prometheus/|Datadog/|Ruby/|Dart/|grpc-|HTTPie/|bot|spider|crawl)"
minLength: 10
maxLength: 500
tier: [VARCHAR, internet]
release_priority: 3
samples: ["curl/7.64.1"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(
&values,
"technology.internet.user_agent",
0.9,
Some(&taxonomy),
);
assert!(
result.is_none(),
"real user-agent values must not be demoted off technology.internet.user_agent"
);
}
#[test]
fn test_attractor_no_demotion_high_confidence() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001", "98101",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 9),
("representation.numeric.integer_number".to_string(), 1),
];
let yaml = r#"
geography.address.postal_code:
title: "Postal Code"
validation:
type: string
pattern: "^[0-9]{3,10}$"
minLength: 3
maxLength: 10
tier: [VARCHAR, geography, address]
release_priority: 5
samples: ["10001"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = disambiguate_attractor_demotion(&values, &votes, 10, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote real postal codes at 0.9 confidence"
);
}
#[test]
fn test_select_fallback_numeric() {
let values: Vec<String> = vec!["100", "200", "300"]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 8),
("representation.numeric.integer_number".to_string(), 2),
];
let result = select_fallback(&votes, true, false, false, &values);
assert_eq!(result, "representation.numeric.integer_number");
}
#[test]
fn test_select_fallback_text() {
let values: Vec<String> = vec!["Soccer", "Tennis"]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("identity.person.first_name".to_string(), 8),
("identity.person.username".to_string(), 2),
];
let result = select_fallback(&votes, false, true, false, &values);
assert_eq!(result, "representation.text.word");
}
#[test]
fn test_select_fallback_from_votes() {
let values: Vec<String> = vec!["100", "200"].into_iter().map(String::from).collect();
let votes = vec![
("geography.address.postal_code".to_string(), 6),
("representation.numeric.decimal_number".to_string(), 3),
("representation.numeric.integer_number".to_string(), 1),
];
let result = select_fallback(&votes, true, false, false, &values);
assert_eq!(
result, "representation.numeric.decimal_number",
"Should use representation.* type from votes when available"
);
}
#[test]
fn test_attractor_demotion_locale_validation_demotes_salary() {
let values: Vec<String> = vec![
"102000", "245000", "112000", "350000", "178000", "195000", "267000", "188000", "103000",
"272000",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 9),
("representation.numeric.integer_number".to_string(), 1),
];
let yaml = r#"
geography.address.postal_code:
title: "Postal Code"
validation:
type: string
minLength: 3
maxLength: 10
maximum: 99999
validation_by_locale:
EN_US:
type: string
pattern: "^(\\d{5})(?:[ \\-](\\d{4}))?$"
minLength: 5
maxLength: 10
EN_GB:
type: string
pattern: "^[A-Z]{1,2}\\d[A-Z\\d]?\\s?\\d[A-Z]{2}$"
minLength: 5
maxLength: 8
DE:
type: string
pattern: "^\\d{5}$"
minLength: 5
maxLength: 5
tier: [VARCHAR, address]
release_priority: 4
samples: ["10001"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 10, Some(&taxonomy));
assert!(
result.is_some(),
"Should demote salary values despite matching universal validation"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert!(
rule.starts_with("attractor_demotion_validation:"),
"Should demote via validation signal, got: {}",
rule
);
}
#[test]
fn sample_contradicts_label_blocks_year_hint_on_decimals() {
let yaml = r#"
datetime.component.year:
title: "Year"
validation:
type: string
pattern: "^(19|20)\\d{2}$"
minLength: 4
maxLength: 4
tier: [VARCHAR, component]
release_priority: 4
samples: ["2021"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
let decimals: Vec<String> = vec!["3.2205129", "1.0", "2.0", "1.5", "0.0"]
.into_iter()
.map(String::from)
.collect();
assert!(
sample_contradicts_label(&taxonomy, "datetime.component.year", &decimals),
"decimals must contradict the year hint"
);
let years: Vec<String> = vec!["2021", "2020", "2019", "2022", "2018"]
.into_iter()
.map(String::from)
.collect();
assert!(
!sample_contradicts_label(&taxonomy, "datetime.component.year", &years),
"real years must corroborate the year hint"
);
assert!(
!sample_contradicts_label(&taxonomy, "representation.text.word", &decimals),
"validator-less leaf must not report a contradiction"
);
let two: Vec<String> = vec!["3.14", "2.71"].into_iter().map(String::from).collect();
assert!(!sample_contradicts_label(
&taxonomy,
"datetime.component.year",
&two
));
}
#[test]
fn header_hint_value_corroboration_is_default_on() {
assert!(!rhh::is_disabled("header_hint_value_corroboration"));
}
#[test]
fn header_corroborates_timezone_token_aware() {
assert!(header_corroborates_timezone("exchangeTimezoneShortName"));
assert!(header_corroborates_timezone("TZ"));
assert!(header_corroborates_timezone("time_zone"));
assert!(header_corroborates_timezone("tzname"));
assert!(!header_corroborates_timezone("status"));
assert!(!header_corroborates_timezone("estimate_code"));
assert!(!header_corroborates_timezone("country"));
}
#[test]
fn values_are_clearly_non_url_separates_ids_from_urls() {
let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
assert!(values_are_clearly_non_url(&v(&[
"msg32812262",
"msg32929450",
"msg11"
])));
assert!(values_are_clearly_non_url(&v(&["Yes", "Yes", "Yes", "No"])));
assert!(!values_are_clearly_non_url(&v(&[
"http://a.com/x",
"https://b.io/y",
"http://c.org/z"
])));
assert!(!values_are_clearly_non_url(&v(&[
"//cdn.a.io/x.js",
"//cdn.b.io/y.css",
"//c.io/z.js"
])));
assert!(!values_are_clearly_non_url(&v(&[
"/partner/x.asp?id=1",
"/partner/y.asp?id=2",
"/partner/z.asp?id=3"
])));
assert!(!values_are_clearly_non_url(&v(&["msg32812262"])));
assert!(values_are_clearly_non_url(&v(&[
"msg32812262",
"msg32929450",
"msg11"
])));
}
#[test]
fn test_attractor_accepts_real_us_postal_codes() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001", "98101",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 9),
("representation.numeric.integer_number".to_string(), 1),
];
let yaml = r#"
geography.address.postal_code:
title: "Postal Code"
validation:
type: string
minLength: 3
maxLength: 10
maximum: 99999
validation_by_locale:
EN_US:
type: string
pattern: "^(\\d{5})(?:[ \\-](\\d{4}))?$"
minLength: 5
maxLength: 10
EN_GB:
type: string
pattern: "^[A-Z]{1,2}\\d[A-Z\\d]?\\s?\\d[A-Z]{2}$"
minLength: 5
maxLength: 8
DE:
type: string
pattern: "^\\d{5}$"
minLength: 5
maxLength: 5
tier: [VARCHAR, address]
release_priority: 4
samples: ["10001"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 10, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote real US ZIP codes (locale-confirmed by EN_US)"
);
}
#[test]
fn test_attractor_accepts_real_uk_postcodes() {
let values: Vec<String> = vec![
"EC1A 1BB", "W1C 1AX", "M2 5BQ", "SW1A 1AA", "B1 1BB", "LS1 1BA",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("geography.address.postal_code".to_string(), 6)];
let yaml = r#"
geography.address.postal_code:
title: "Postal Code"
validation:
type: string
minLength: 3
maxLength: 10
maximum: 99999
validation_by_locale:
EN_US:
type: string
pattern: "^(\\d{5})(?:[ \\-](\\d{4}))?$"
minLength: 5
maxLength: 10
EN_GB:
type: string
pattern: "^[A-Z]{1,2}\\d[A-Z\\d]?\\s?\\d[A-Z]{2}$"
minLength: 5
maxLength: 8
tier: [VARCHAR, address]
release_priority: 4
samples: ["10001"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 6, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote real UK postcodes (locale-confirmed by EN_GB)"
);
}
#[test]
fn test_attractor_locale_low_confidence_accepted() {
let values: Vec<String> = vec![
"10001", "90210", "30301", "60601", "02101", "75001", "33101", "94102", "20001", "98101",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("geography.address.postal_code".to_string(), 6),
("representation.numeric.integer_number".to_string(), 4),
];
let yaml = r#"
geography.address.postal_code:
title: "Postal Code"
validation:
type: string
minLength: 3
maxLength: 10
maximum: 99999
validation_by_locale:
EN_US:
type: string
pattern: "^(\\d{5})(?:[ \\-](\\d{4}))?$"
minLength: 5
maxLength: 10
tier: [VARCHAR, address]
release_priority: 4
samples: ["10001"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 10, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote real US ZIPs at low confidence when locale confirms them"
);
}
#[test]
fn test_attractor_locale_confirmed_skips_cardinality() {
let values: Vec<String> = vec![
"(805) 638-3078",
"(650) 440-2450",
"(805) 638-3078",
"(805) 638-3078",
"(650) 440-2450",
"(650) 440-2450",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("identity.person.phone_number".to_string(), 6)];
let yaml = r#"
identity.person.phone_number:
title: "Phone Number"
validation:
type: string
minLength: 7
maxLength: 20
pattern: "^[+]?[0-9\\s()\\-\\.]+$"
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-\\.]*)?\\(?\\d{3}\\)?[\\s\\-\\.]*\\d{3}[\\s\\-\\.]*\\d{4}$"
minLength: 10
maxLength: 18
tier: [VARCHAR, person]
release_priority: 4
samples: ["+1 (555) 123-4567"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 6, Some(&taxonomy));
assert!(
result.is_none(),
"Should NOT demote phone numbers when locale-confirmed, even with 2 unique values"
);
}
#[test]
fn test_attractor_universal_only_does_not_confirm_locale_type() {
let values: Vec<String> = vec![
"123-456", "789-012", "345-678", "123-456", "789-012", "345-678",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("identity.person.phone_number".to_string(), 4),
("representation.discrete.categorical".to_string(), 2),
];
let yaml = r#"
identity.person.phone_number:
title: "Phone Number"
validation:
type: string
minLength: 7
maxLength: 20
pattern: "^[+]?[0-9\\s()\\-\\.]+$"
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-\\.]*)?\\(?\\d{3}\\)?[\\s\\-\\.]*\\d{3}[\\s\\-\\.]*\\d{4}$"
minLength: 10
maxLength: 18
tier: [VARCHAR, person]
release_priority: 4
samples: ["+1 (555) 123-4567"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_validators();
taxonomy.compile_locale_validators();
let result = disambiguate_attractor_demotion(&values, &votes, 6, Some(&taxonomy));
assert!(
result.is_some(),
"Should demote phone_number at low confidence when only universal validates (no locale)"
);
let (_, rule) = result.unwrap();
assert!(
rule.starts_with("attractor_demotion_confidence:"),
"Should demote via confidence signal, got: {}",
rule
);
}
#[test]
fn test_attractor_first_name_cardinality_unchanged() {
let values: Vec<String> = vec![
"John", "Jane", "Bob", "John", "Jane", "Bob", "John", "Jane", "Bob", "John",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![
("identity.person.first_name".to_string(), 9),
("representation.text.word".to_string(), 1),
];
let result = disambiguate_attractor_demotion(&values, &votes, 10, None);
assert!(
result.is_some(),
"first_name with 3 unique values should still be demoted (no locale validators)"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
assert!(
rule.starts_with("attractor_demotion_cardinality:"),
"Should demote via cardinality signal, got: {}",
rule
);
}
#[test]
fn test_duration_override_standard_durations() {
let values: Vec<String> = vec![
"PT20M", "PT30M", "PT10M", "PT15M", "PT1H", "PT45M", "PT5M", "PT60M",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(result.is_some(), "Should detect ISO 8601 durations");
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.duration.iso_8601");
assert_eq!(rule, "duration_override_sedol");
}
#[test]
fn test_duration_override_complex_durations() {
let values: Vec<String> = vec!["P1DT12H", "P2Y3M", "PT1H30M", "P30D", "P1Y", "PT2H15M30S"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(result.is_some(), "Should detect complex ISO 8601 durations");
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.duration.iso_8601");
}
#[test]
fn test_duration_override_malformed_sotab_durations() {
let values: Vec<String> = vec![
"PD1TH0M0", "PD3TH0M0", "PT30M", "PT20M", "PD1TH0M0", "PT10M",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(
result.is_some(),
"Should detect non-standard duration variants"
);
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.duration.iso_8601");
}
#[test]
fn test_duration_override_not_triggered_for_sedol() {
let values: Vec<String> = vec![
"B0YBKJ7", "B1YW440", "B39J2S1", "B0JNMQ2", "BWFGQN3", "B082RF1",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(
result.is_none(),
"Should NOT trigger for real SEDOL codes (no duration pattern)"
);
}
#[test]
fn test_duration_override_not_triggered_below_threshold() {
let values: Vec<String> = vec![
"ABC1234", "DEF5678", "GHI9012", "PT20M", "JKL3456", "MNO7890", "PQR1234", "STU5678",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(
result.is_none(),
"Should NOT trigger when <50% of values are durations"
);
}
#[test]
fn test_duration_override_week_durations() {
let values: Vec<String> = vec!["P1W", "P2W", "P3W", "P4W", "P1W", "P2W"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_duration_override(&values);
assert!(result.is_some(), "Should detect week-based durations");
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.duration.iso_8601");
}
#[test]
fn test_utc_offset_override_standard_offsets() {
let values: Vec<String> = vec![
"+05:30", "-08:00", "-05:00", "+05:30", "+01:00", "+10:00", "+10:00", "+09:00", "+01:00",
"+00:00",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_utc_offset_override(&values);
assert!(result.is_some(), "Should detect UTC offset values");
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.offset.utc");
assert_eq!(rule, "utc_offset_override_time");
}
#[test]
fn test_utc_offset_override_all_negative() {
let values: Vec<String> = vec!["-05:00", "-06:00", "-07:00", "-08:00", "-04:00", "-03:00"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_utc_offset_override(&values);
assert!(result.is_some(), "Should detect negative UTC offsets");
let (label, _) = result.unwrap();
assert_eq!(label, "datetime.offset.utc");
}
#[test]
fn test_utc_offset_override_not_triggered_for_times() {
let values: Vec<String> = vec!["14:30", "08:00", "12:15", "09:45", "16:00", "23:59"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_utc_offset_override(&values);
assert!(
result.is_none(),
"Should NOT trigger for actual time values (no +/- prefix)"
);
}
#[test]
fn test_utc_offset_override_not_triggered_below_threshold() {
let values: Vec<String> = vec![
"14:30", "08:00", "+05:30", "12:15", "09:45", "16:00", "23:59", "-08:00", "11:00", "07:30",
]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_utc_offset_override(&values);
assert!(
result.is_none(),
"Should NOT trigger when <80% of values are UTC offsets"
);
}
#[test]
fn test_utc_offset_override_too_few_values() {
let values: Vec<String> = vec!["+05:30", "-08:00"]
.into_iter()
.map(String::from)
.collect();
let result = disambiguate_utc_offset_override(&values);
assert!(
result.is_none(),
"Should NOT trigger with fewer than 3 values"
);
}
#[test]
fn test_text_length_demotion_long_text_as_address() {
let values: Vec<String> = vec![
"Contact information of the hotel Record of Zelenograd: phone, location map, address on the map. Full amenities and services list available.",
"The layout of the room includes two bedrooms and a spacious lounge, but each room has its own plasma TV for entertainment and relaxation purposes.",
"Services provided by the hotel Record (Zelenograd): Credit cards (Visa, MasterCard, World), free Wi-Fi, gym, spa, pool, conference rooms available.",
"STANDARD WITH THE KITCHEN number one category. The Record Hotel has 1 Standard Room with Kitchen area for extended stays and business travelers.",
"Preheat oven to 350 degrees. Grease a small baking dish or small cast iron skillet and fill with peaches. Sprinkle cinnamon over peaches and set aside.",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("geography.address.full_address".to_string(), 5)];
let result = disambiguate_text_length_demotion(&values, &votes);
assert!(result.is_some(), "Should demote long text overcall");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.plain_text");
assert_eq!(rule, "text_length_demotion_full_address");
}
#[test]
fn test_text_length_demotion_real_address_not_demoted() {
let values: Vec<String> = vec![
"123 Main St, Springfield, IL",
"456 Oak Ave, Portland, OR",
"789 Pine Rd, Austin, TX",
"101 Elm Blvd, Denver, CO",
"202 Maple Dr, Seattle, WA",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("geography.address.full_address".to_string(), 5)];
let result = disambiguate_text_length_demotion(&values, &votes);
assert!(
result.is_none(),
"Should NOT demote real addresses (median ~28 chars)"
);
}
#[test]
fn test_text_length_demotion_ignores_non_address() {
let values: Vec<String> = vec![
"This is a very long text that exceeds one hundred characters and should demonstrate that length alone does not trigger demotion for other types.",
"Another very long text value that is clearly longer than the threshold of one hundred characters but is not predicted as full_address by the model.",
"Yet another long text to ensure the median is above the threshold value for the test to be meaningful in demonstrating the rule only applies to addresses.",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("identity.person.full_name".to_string(), 3)];
let result = disambiguate_text_length_demotion(&values, &votes);
assert!(
result.is_none(),
"Should NOT fire for non-full_address predictions"
);
}
#[test]
fn test_text_length_demotion_long_prose_entity_name() {
let values: Vec<String> = vec![
"(Reuters) - Citigroup Inc said on Friday it recorded an additional charge related to its previously announced restructuring and severance programme.",
"(Bloomberg) - The central bank held rates steady on Thursday, signalling caution amid mixed signals on inflation and a cooling labour market this quarter.",
"LONDON - Shares in the mining group fell sharply after it warned that full-year production would land below guidance owing to disruption at two key sites.",
"NEW YORK - The technology company unveiled a sweeping reorganisation on Tuesday, consolidating its hardware and services divisions under a single leader.",
"PARIS - The luxury conglomerate reported quarterly sales that beat analyst expectations, lifted by resilient demand across its leather goods business.",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("representation.text.entity_name".to_string(), 5)];
let result = disambiguate_text_length_demotion(&values, &votes);
assert!(result.is_some(), "Should demote long entity_name prose");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.text.plain_text");
assert!(rule.starts_with("text_length_demotion_long_prose:"));
}
#[test]
fn test_text_length_demotion_short_entity_not_demoted() {
let values: Vec<String> = vec!["Apple Inc", "Microsoft", "Sony", "Nestle", "Adobe"]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("representation.text.entity_name".to_string(), 5)];
assert!(disambiguate_text_length_demotion(&values, &votes).is_none());
}
#[test]
fn test_full_address_whitespace_guard_demotes_nospace_id() {
let values: Vec<String> = vec![
"ACTIVATION_FWD8731790411514196767<CUDNN_ACTIVATION_CLIPPED_RELU>",
"CONV_FWD_1234<CUDNN_CONVOLUTION>",
"ADD_TENSOR_99<CUDNN_OP>",
"POOL_MAX_42<CUDNN_POOLING>",
"BATCHNORM_FWD_7<CUDNN_BN>",
]
.into_iter()
.map(String::from)
.collect();
let (label, rule) = disambiguate_full_address_whitespace_guard(&values).unwrap();
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("full_address_whitespace_guard"));
}
#[test]
fn test_full_address_whitespace_guard_keeps_real_addresses() {
let values: Vec<String> = vec![
"123 Main St Springfield IL",
"Hauptstrasse 12 8001 Zurich",
"45 Rue de la Paix Paris",
"789 Pine Rd Austin TX",
]
.into_iter()
.map(String::from)
.collect();
assert!(disambiguate_full_address_whitespace_guard(&values).is_none());
}
#[test]
fn test_text_length_demotion_borderline_not_demoted() {
let values: Vec<String> = vec![
"123 Main Street, Apartment 4B, Springfield, Illinois 62704, United States of America — Near the park",
"456 Oak Avenue, Suite 200, Portland, Oregon 97201, United States of America — Downtown district",
"789 Pine Road, Building C, Unit 12, Austin, Texas 78701, United States of America — East campus",
]
.into_iter()
.map(String::from)
.collect();
let votes = vec![("geography.address.full_address".to_string(), 3)];
let result = disambiguate_text_length_demotion(&values, &votes);
assert!(
result.is_none(),
"Should NOT demote borderline addresses (median ~95 chars)"
);
}
#[test]
fn test_is_generic_attractor_demoted_always_generic() {
let rule = Some("attractor_demotion_validation:something".to_string());
assert!(
is_generic_prediction("identity.person.email", &rule, None),
"Attractor-demoted predictions should always be generic"
);
}
#[test]
fn test_is_generic_boolean_always_generic() {
assert!(
is_generic_prediction("representation.boolean.binary", &None, None),
"Boolean types should always be generic"
);
assert!(
is_generic_prediction("representation.boolean.terms", &None, None),
"Boolean types should always be generic"
);
}
#[test]
fn test_is_generic_broad_words_with_taxonomy() {
let yaml = r#"
identity.person.gender:
title: Gender
designation: broad_words
tier: [VARCHAR, identity, person]
release_priority: 1
samples: ["Male"]
identity.person.email:
title: Email
designation: universal
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["test@example.com"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
assert!(
is_generic_prediction("identity.person.gender", &None, Some(&taxonomy)),
"broad_words types should be generic when taxonomy is available"
);
assert!(
!is_generic_prediction("identity.person.email", &None, Some(&taxonomy)),
"universal types should NOT be generic (unless in hardcoded list)"
);
}
#[test]
fn test_is_generic_broad_characters_with_taxonomy() {
let yaml = r#"
identity.person.password:
title: Password
designation: broad_characters
tier: [VARCHAR, identity, person]
release_priority: 1
samples: ["p@ssw0rd"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
assert!(
is_generic_prediction("identity.person.password", &None, Some(&taxonomy)),
"broad_characters types should be generic when taxonomy is available"
);
}
#[test]
fn test_is_generic_broad_numbers_with_taxonomy() {
let yaml = r#"
representation.identifier.increment:
title: Increment
designation: broad_numbers
tier: [BIGINT, identifier]
release_priority: 1
samples: ["42"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
assert!(
is_generic_prediction(
"representation.identifier.increment",
&None,
Some(&taxonomy)
),
"broad_numbers types should be generic when taxonomy is available"
);
}
#[test]
fn test_is_generic_fallback_without_taxonomy() {
assert!(
is_generic_prediction("representation.text.word", &None, None),
"Hardcoded generic label should be generic without taxonomy"
);
assert!(
is_generic_prediction("identity.person.phone_number", &None, None),
"Hardcoded generic label should be generic without taxonomy"
);
assert!(
!is_generic_prediction("identity.person.email", &None, None),
"Non-hardcoded label should NOT be generic without taxonomy"
);
}
#[test]
fn test_is_generic_locale_specific_not_generic() {
let yaml = r#"
geography.address.postal_code:
title: Postal Code
designation: locale_specific
tier: [VARCHAR, geography, address]
release_priority: 5
samples: ["90210"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
assert!(
!is_generic_prediction("geography.address.postal_code", &None, Some(&taxonomy)),
"locale_specific types not in hardcoded list should NOT be generic"
);
}
#[test]
fn test_is_generic_hardcoded_overrides_taxonomy() {
let yaml = r#"
identity.person.phone_number:
title: Phone Number
designation: locale_specific
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["+1 (202) 555-0100"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
assert!(
is_generic_prediction("identity.person.phone_number", &None, Some(&taxonomy)),
"phone_number is in hardcoded list — stays generic regardless of designation"
);
}
#[test]
fn test_detect_locale_us_phone_numbers() {
let yaml = r#"
identity.person.phone_number:
title: Phone Number
designation: locale_specific
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["+1 (202) 555-0100"]
validation:
type: string
pattern: "^[+]?[0-9\\s()\\-\\.]+$"
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-./]*)?\\(?\\d{3}\\)?[\\s\\-./]*\\d{3}[\\s\\-./]*\\d{4}$"
minLength: 10
maxLength: 30
EN_GB:
type: string
pattern: "^(\\+?44[\\s\\-./]*(\\(0\\))?)?0?\\d{2,5}([\\s\\-./]*\\d{1,8}){1,3}$"
minLength: 10
maxLength: 30
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec![
"+1 (202) 555-0100",
"+1 (415) 555-0199",
"(312) 555-0142",
"1-800-555-0123",
"(617) 555-0187",
]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "identity.person.phone_number", &taxonomy);
assert_eq!(
locale,
Some("EN_US".to_string()),
"US phone numbers should detect EN_US"
);
}
#[test]
fn test_detect_locale_uk_phone_numbers() {
let yaml = r#"
identity.person.phone_number:
title: Phone Number
designation: locale_specific
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["+44 20 7946 0958"]
validation:
type: string
pattern: "^[+]?[0-9\\s()\\-\\.]+$"
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-./]*)?\\(?\\d{3}\\)?[\\s\\-./]*\\d{3}[\\s\\-./]*\\d{4}$"
minLength: 10
maxLength: 30
EN_GB:
type: string
pattern: "^(\\+?44[\\s\\-./]*(\\(0\\))?)?0?\\d{2,5}([\\s\\-./]*\\d{1,8}){1,3}$"
minLength: 10
maxLength: 30
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec![
"+44 20 7946 0958",
"020 7946 0123",
"+44 121 496 0987",
"0161 496 0654",
"+44 131 496 0321",
]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "identity.person.phone_number", &taxonomy);
assert_eq!(
locale,
Some("EN_GB".to_string()),
"UK phone numbers should detect EN_GB"
);
}
#[test]
fn test_detect_locale_no_validators() {
let yaml = r#"
identity.person.email:
title: Email
designation: universal
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["test@example.com"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let values: Vec<String> = vec!["test@example.com", "user@domain.org"]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "identity.person.email", &taxonomy);
assert_eq!(
locale, None,
"Types without locale validators should return None"
);
}
#[test]
fn test_detect_locale_no_match_above_threshold() {
let yaml = r#"
identity.person.phone_number:
title: Phone Number
designation: locale_specific
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["+1 (202) 555-0100"]
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-./]*)?\\(?\\d{3}\\)?[\\s\\-./]*\\d{3}[\\s\\-./]*\\d{4}$"
minLength: 10
maxLength: 30
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["abc", "hello world", "12345", "not-a-phone"]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "identity.person.phone_number", &taxonomy);
assert_eq!(
locale, None,
"Non-matching values should not detect any locale"
);
}
#[test]
fn test_detect_locale_empty_values() {
let yaml = r#"
identity.person.phone_number:
title: Phone Number
designation: locale_specific
tier: [VARCHAR, identity, person]
release_priority: 5
samples: ["+1 (202) 555-0100"]
validation_by_locale:
EN_US:
type: string
pattern: "^(\\+?1[\\s\\-./]*)?\\(?\\d{3}\\)?[\\s\\-./]*\\d{3}[\\s\\-./]*\\d{4}$"
minLength: 10
maxLength: 30
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["", "", ""].into_iter().map(String::from).collect();
let locale = detect_locale_from_validation(&values, "identity.person.phone_number", &taxonomy);
assert_eq!(
locale, None,
"All-empty values should not detect any locale"
);
}
#[test]
fn test_detect_locale_calling_code_uk() {
let yaml = r#"
geography.contact.calling_code:
title: International Calling Code
designation: locale_specific
tier: [VARCHAR, contact]
release_priority: 3
samples: ["+1"]
validation:
type: string
pattern: "^\\+?[0-9]{1,4}$"
validation_by_locale:
EN_US:
type: string
pattern: "^\\+?1$"
EN_GB:
type: string
pattern: "^\\+?44$"
DE:
type: string
pattern: "^\\+?49$"
FR:
type: string
pattern: "^\\+?33$"
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["+44", "44", "+44", "+44", "44"]
.into_iter()
.map(String::from)
.collect();
let locale =
detect_locale_from_validation(&values, "geography.contact.calling_code", &taxonomy);
assert_eq!(
locale,
Some("EN_GB".to_string()),
"+44 calling codes should detect EN_GB"
);
}
#[test]
fn test_detect_locale_calling_code_de() {
let yaml = r#"
geography.contact.calling_code:
title: International Calling Code
designation: locale_specific
tier: [VARCHAR, contact]
release_priority: 3
samples: ["+1"]
validation:
type: string
pattern: "^\\+?[0-9]{1,4}$"
validation_by_locale:
EN_US:
type: string
pattern: "^\\+?1$"
EN_GB:
type: string
pattern: "^\\+?44$"
DE:
type: string
pattern: "^\\+?49$"
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["+49", "49", "+49"]
.into_iter()
.map(String::from)
.collect();
let locale =
detect_locale_from_validation(&values, "geography.contact.calling_code", &taxonomy);
assert_eq!(
locale,
Some("DE".to_string()),
"+49 calling codes should detect DE"
);
}
#[test]
fn test_detect_locale_month_name_french() {
let yaml = r#"
datetime.component.month_name:
title: Full Month Name
designation: locale_specific
tier: [VARCHAR, component]
release_priority: 1
samples: ["January"]
validation:
type: string
enum: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
validation_by_locale:
EN:
type: string
enum: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
FR:
type: string
enum: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"]
DE:
type: string
enum: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["janvier", "mars", "juin", "décembre", "août"]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "datetime.component.month_name", &taxonomy);
assert_eq!(
locale,
Some("FR".to_string()),
"French month names should detect FR"
);
}
#[test]
fn test_detect_locale_month_name_german() {
let yaml = r#"
datetime.component.month_name:
title: Full Month Name
designation: locale_specific
tier: [VARCHAR, component]
release_priority: 1
samples: ["January"]
validation:
type: string
enum: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
validation_by_locale:
EN:
type: string
enum: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
FR:
type: string
enum: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"]
DE:
type: string
enum: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["März", "Oktober", "Dezember", "Januar"]
.into_iter()
.map(String::from)
.collect();
let locale = detect_locale_from_validation(&values, "datetime.component.month_name", &taxonomy);
assert_eq!(
locale,
Some("DE".to_string()),
"German month names should detect DE"
);
}
#[test]
fn test_detect_locale_day_of_week_spanish() {
let yaml = r#"
datetime.component.day_of_week:
title: Day of Week Name
designation: locale_specific
tier: [VARCHAR, component]
release_priority: 1
samples: ["Monday"]
validation:
type: string
enum: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
validation_by_locale:
EN:
type: string
enum: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
ES:
type: string
enum: ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"]
IT:
type: string
enum: ["lunedì", "martedì", "mercoledì", "giovedì", "venerdì", "sabato", "domenica"]
"#;
let mut taxonomy = Taxonomy::from_yaml(yaml).unwrap();
taxonomy.compile_locale_validators();
let values: Vec<String> = vec!["lunes", "miércoles", "viernes", "domingo"]
.into_iter()
.map(String::from)
.collect();
let locale =
detect_locale_from_validation(&values, "datetime.component.day_of_week", &taxonomy);
assert_eq!(
locale,
Some("ES".to_string()),
"Spanish day names should detect ES"
);
}
#[test]
fn test_is_generic_numeric_postal_code_detection() {
let rule = Some("numeric_postal_code_detection".to_string());
assert!(
is_generic_prediction("geography.address.postal_code", &rule, None),
"numeric_postal_code_detection should be treated as generic to yield to header hints"
);
}
#[test]
fn test_geography_protection_person_name_hints() {
assert!(
PERSON_NAME_HINTS.contains(&"identity.person.full_name"),
"PERSON_NAME_HINTS should include full_name"
);
assert!(
PERSON_NAME_HINTS.contains(&"identity.person.last_name"),
"PERSON_NAME_HINTS should include last_name"
);
assert!(
PERSON_NAME_HINTS.contains(&"identity.person.first_name"),
"PERSON_NAME_HINTS should include first_name"
);
assert!(
!PERSON_NAME_HINTS.contains(&"identity.person.email"),
"email should NOT be in PERSON_NAME_HINTS"
);
assert!(
!PERSON_NAME_HINTS.contains(&"identity.person.phone_number"),
"phone_number should NOT be in PERSON_NAME_HINTS"
);
}
#[test]
fn test_location_types_module_level() {
assert!(
LOCATION_TYPES.contains(&"geography.location.city"),
"LOCATION_TYPES should include city"
);
assert!(
LOCATION_TYPES.contains(&"geography.location.country"),
"LOCATION_TYPES should include country"
);
assert!(
LOCATION_TYPES.contains(&"geography.location.region"),
"LOCATION_TYPES should include region"
);
assert!(
LOCATION_TYPES.contains(&"geography.location.state"),
"LOCATION_TYPES should include state"
);
assert!(
LOCATION_TYPES.contains(&"geography.location.continent"),
"LOCATION_TYPES should include continent"
);
}
#[test]
fn test_rule19_percentage_no_sign_demotes_to_decimal() {
let values: Vec<String> = vec!["5.1", "3.5", "1.4", "0.2", "4.9"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.percentage".to_string(),
confidence: 0.8,
all_scores: vec![],
})
.collect();
let votes = vec![
("representation.numeric.percentage".to_string(), 4),
("representation.numeric.decimal_number".to_string(), 1),
];
let result = disambiguate(&values, &results, &votes, 5, None);
assert!(
result.is_some(),
"Rule 19 should fire for percentage without '%'"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.decimal_number");
assert_eq!(rule, "percentage_no_sign");
}
#[test]
fn test_rule19_percentage_with_sign_keeps_percentage() {
let values: Vec<String> = vec!["35.36%", "12.5%", "98.1%", "0.5%", "100%"]
.into_iter()
.map(String::from)
.collect();
let results: Vec<ClassificationResult> = values
.iter()
.map(|_| ClassificationResult {
label: "representation.numeric.percentage".to_string(),
confidence: 0.9,
all_scores: vec![],
})
.collect();
let votes = vec![("representation.numeric.percentage".to_string(), 5)];
let result = disambiguate(&values, &results, &votes, 5, None);
assert!(
result.is_none()
|| result
.as_ref()
.map(|(_, r)| r != "percentage_no_sign")
.unwrap_or(true),
"Rule 19 should NOT fire when values contain '%'"
);
}
#[test]
fn test_header_hint_timezone() {
assert_eq!(
header_hint("timezone"),
Some("datetime.offset.iana"),
"timezone should hint to iana"
);
assert_eq!(
header_hint("tz"),
Some("datetime.offset.iana"),
"tz should hint to iana"
);
assert_eq!(
header_hint("time_zone"),
Some("datetime.offset.iana"),
"time_zone (with underscore) should hint to iana"
);
}
#[test]
fn test_header_hint_publisher() {
assert_eq!(
header_hint("publisher"),
Some("representation.text.entity_name"),
"publisher should hint to entity_name"
);
}
#[test]
fn test_header_hint_measurement_keywords() {
assert_eq!(
header_hint("pressure_atm"),
Some("representation.numeric.decimal_number"),
"pressure_atm should hint to decimal_number"
);
assert_eq!(
header_hint("temperature"),
Some("representation.numeric.decimal_number"),
"temperature should hint to decimal_number"
);
assert_eq!(
header_hint("voltage"),
Some("representation.numeric.decimal_number"),
"voltage should hint to decimal_number"
);
}
#[test]
fn test_header_hint_priority_hardcoded_first() {
assert_eq!(
header_hint("job_title"),
Some("representation.text.word"),
"job_title should hint to categorical (hardcoded)"
);
assert_eq!(
header_hint("occupation"),
Some("representation.text.word"),
"occupation should hint to categorical (hardcoded)"
);
}
#[test]
fn test_geo_override_same_domain() {
assert!(
LOCATION_TYPES.contains(&"geography.location.city"),
"city must be a location type"
);
assert!(
LOCATION_TYPES.contains(&"geography.location.country"),
"country must be a location type"
);
}
#[test]
fn ac01_bitcoin_address_not_captured_by_address_hint() {
assert_eq!(
header_hint("bitcoin_address"),
None,
"bitcoin_address must not match the address keyword"
);
assert_eq!(
header_hint("btc_address"),
None,
"btc_address must not match the address keyword"
);
assert_eq!(
header_hint("crypto_address"),
None,
"crypto_address must not match the address keyword"
);
assert_eq!(
header_hint("wallet_address"),
None,
"wallet_address must not match the address keyword"
);
assert_eq!(
header_hint("street_address"),
Some("geography.address.street_address"),
"street_address should still return street_address"
);
assert_eq!(
header_hint("full_address"),
Some("geography.address.full_address"),
"full_address should still return full_address"
);
assert_eq!(
header_hint("home_address"),
Some("geography.address.full_address"),
"home_address should still return full_address"
);
}
#[test]
fn ac02_ipv6_header_returns_ip_v6() {
assert_eq!(
header_hint("ip_v6"),
Some("technology.internet.ip_v6"),
"ip_v6 should return ip_v6"
);
assert_eq!(
header_hint("server_ipv6"),
Some("technology.internet.ip_v6"),
"server_ipv6 should return ip_v6"
);
assert_eq!(
header_hint("ipv6_address"),
Some("technology.internet.ip_v6"),
"ipv6_address should return ip_v6"
);
assert_eq!(
header_hint("source_ip"),
Some("technology.internet.ip_v4"),
"source_ip should still return ip_v4"
);
assert_eq!(
header_hint("ip_address"),
Some("technology.internet.ip_v4"),
"ip_address should still return ip_v4"
);
}
#[test]
fn ac04_icao_header_hint() {
assert_eq!(
header_hint("icao"),
Some("geography.transportation.icao_code"),
"icao should hint to icao_code"
);
assert_eq!(
header_hint("ICAO"),
Some("geography.transportation.icao_code"),
"ICAO (uppercase) should hint to icao_code"
);
assert_eq!(
header_hint("icao_code"),
Some("geography.transportation.icao_code"),
"icao_code should hint to icao_code"
);
}
#[test]
fn debug_icao_sharpen_override() {
let mock = crate::inference::MockClassifier::new("geography.transportation.unlocode");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("geography.transportation.unlocode", 0.919);
let sample: Vec<String> = vec!["EGLL".into(), "KJFK".into(), "LFPG".into()];
let hint = header_hint("icao");
assert_eq!(hint, Some("geography.transportation.icao_code"));
cc.apply_header_sharpen(&mut result, "icao", &sample);
eprintln!("result.label = {}", result.label);
eprintln!("result.rule = {:?}", result.disambiguation_rule);
assert_eq!(
result.label, "geography.transportation.icao_code",
"icao hint must override unlocode@0.919 via same-category path"
);
}
#[test]
fn debug_ipv6_sharpen_override() {
let mock = crate::inference::MockClassifier::new("technology.internet.ip_v4");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("technology.internet.ip_v4", 0.70);
let sample: Vec<String> = vec!["2001:db8::1".into(); 5];
let hint = header_hint("ip_v6");
assert_eq!(hint, Some("technology.internet.ip_v6"));
cc.apply_header_sharpen(&mut result, "ip_v6", &sample);
eprintln!("result.label = {}", result.label);
eprintln!("result.rule = {:?}", result.disambiguation_rule);
assert_eq!(
result.label, "technology.internet.ip_v6",
"ip_v6 hint must override ip_v4@0.70 via same-category path"
);
}
#[test]
fn ac04_author_header_hint() {
assert_eq!(
header_hint("author"),
Some("identity.person.full_name"),
"author should hint to full_name"
);
assert_eq!(
header_hint("authors"),
Some("identity.person.full_name"),
"authors should hint to full_name"
);
assert_eq!(
header_hint("author_name"),
Some("identity.person.full_name"),
"author_name should hint to full_name"
);
}
fn make_result(label: &str, confidence: f32) -> ColumnResult {
ColumnResult {
label: label.to_string(),
confidence,
vote_distribution: vec![(label.to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 10,
detected_locale: None,
is_generic: false,
column_features: None,
}
}
#[test]
fn ac03a_same_category_hardcoded_override_unconditional() {
let mock = crate::inference::MockClassifier::new("identity.person.ssn");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("identity.person.ssn", 1.00);
let sample: Vec<String> = vec!["555-0100".into(); 5];
cc.apply_header_sharpen(&mut result, "phone", &sample);
assert_eq!(
result.label, "identity.person.phone_number",
"phone hint must override ssn@1.00 (same category, hardcoded)"
);
assert!(
result.disambiguation_applied,
"disambiguation_applied should be true"
);
assert_eq!(
result.disambiguation_rule.as_deref(),
Some("header_hint_same_category:phone"),
"rule should be header_hint_same_category"
);
}
#[test]
fn ac03b_same_domain_hardcoded_override_below_095() {
let mock = crate::inference::MockClassifier::new("datetime.date.compact_ym");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("datetime.date.compact_ym", 0.83);
let sample: Vec<String> = vec!["2024".into(); 5];
cc.apply_header_sharpen(&mut result, "year", &sample);
assert_eq!(
result.label, "datetime.component.year",
"year hint must override compact_ym@0.83 (same domain, < 0.90)"
);
assert!(result.disambiguation_applied);
}
#[test]
fn ac03b_same_domain_hardcoded_override_url_over_docker_ref() {
let mock = crate::inference::MockClassifier::new("technology.development.docker_ref");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("technology.development.docker_ref", 0.60);
let sample: Vec<String> = vec!["https://example.com/track/123".into(); 5];
cc.apply_header_sharpen(&mut result, "tracking_url", &sample);
assert_eq!(
result.label, "technology.internet.url",
"url hint must override docker_ref@0.60 (same domain, < 0.90)"
);
assert!(result.disambiguation_applied);
}
#[test]
fn ac03b_same_domain_hardcoded_does_not_override_at_095() {
let mock = crate::inference::MockClassifier::new("identity.government.ssn");
let cc = ColumnClassifier::with_defaults(Box::new(mock));
let mut result = make_result("identity.government.ssn", 0.96);
let sample: Vec<String> = vec!["123-45-6789".into(); 5];
cc.apply_header_sharpen(&mut result, "phone", &sample);
assert_eq!(
result.label, "identity.government.ssn",
"hardcoded hint must NOT override at confidence >= 0.95"
);
assert!(
!result.disambiguation_applied,
"disambiguation should not be applied at >= 0.95"
);
}
#[test]
fn test_aggregate_features_empty() {
let cf = aggregate_features(&[]);
assert_eq!(cf.mean, [0.0f32; FEATURE_DIM]);
assert_eq!(cf.variance, [0.0f32; FEATURE_DIM]);
}
#[test]
fn test_aggregate_features_single_value() {
let features = extract_features("abc123");
let cf = aggregate_features(&[features]);
assert_eq!(cf.mean, features);
for v in cf.variance {
assert!(
v.abs() < 1e-6,
"variance should be ~0 for single value, got {}",
v
);
}
assert_eq!(cf.min, features);
assert_eq!(cf.max, features);
}
#[test]
fn test_aggregate_features_variance() {
let f1 = extract_features("abc"); let f2 = extract_features("abcdef"); let cf = aggregate_features(&[f1, f2]);
let length_idx = feature_idx::LENGTH;
assert!((cf.mean[length_idx] - 4.5).abs() < 0.01, "mean length");
assert!(
(cf.variance[length_idx] - 2.25).abs() < 0.01,
"variance length"
);
assert!((cf.min[length_idx] - 3.0).abs() < 0.01, "min length");
assert!((cf.max[length_idx] - 6.0).abs() < 0.01, "max length");
}
#[test]
fn test_uniform_length_hex_zero_variance() {
let shas = [
"a".repeat(40),
"b1c2d3e4f5a6b7c8d9e0a1b2c3d4e5f6a7b8c9d0".to_string(),
"1234567890abcdef1234567890abcdef12345678".to_string(),
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(),
];
let per_value: Vec<[f32; FEATURE_DIM]> = shas.iter().map(|s| extract_features(s)).collect();
let cf = aggregate_features(&per_value);
assert!(
cf.variance[feature_idx::LENGTH] < 0.01,
"uniform hex length variance should be ~0, got {}",
cf.variance[feature_idx::LENGTH]
);
assert!(
cf.mean[feature_idx::IS_HEX_STRING] >= 0.95,
"uniform hex strings should all be hex, got {}",
cf.mean[feature_idx::IS_HEX_STRING]
);
}
#[test]
fn test_hash_mixed_length_nonzero_variance() {
let hashes = [
"d41d8cd98f00b204e9800998ecf8427e".to_string(), "da39a3ee5e6b4b0d3255bfef95601890afd80709".to_string(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(), ];
let per_value: Vec<[f32; FEATURE_DIM]> = hashes.iter().map(|s| extract_features(s)).collect();
let cf = aggregate_features(&per_value);
assert!(
cf.variance[feature_idx::LENGTH] > 100.0,
"mixed hash length variance should be high, got {}",
cf.variance[feature_idx::LENGTH]
);
assert!(
cf.mean[feature_idx::IS_HEX_STRING] >= 0.95,
"all hashes should be hex, got {}",
cf.mean[feature_idx::IS_HEX_STRING]
);
}
#[test]
fn test_rule_f5_numeric_code_demoted_without_leading_zeros() {
let mut result = ColumnResult {
label: "representation.identifier.numeric_code".to_string(),
confidence: 1.0,
vote_distribution: vec![("representation.identifier.numeric_code".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::HAS_LEADING_ZERO] = 0.0;
let votes = vec![("representation.identifier.numeric_code".to_string(), 100)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(result.label, "representation.numeric.integer_number");
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.starts_with("feature_no_leading_zero"));
}
#[test]
fn test_rule_f5_numeric_code_kept_with_leading_zeros() {
let mut result = ColumnResult {
label: "representation.identifier.numeric_code".to_string(),
confidence: 1.0,
vote_distribution: vec![("representation.identifier.numeric_code".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::HAS_LEADING_ZERO] = 0.5;
let votes = vec![("representation.identifier.numeric_code".to_string(), 100)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(
result.label, "representation.identifier.numeric_code",
"should NOT demote when leading zeros are present"
);
assert!(!result.disambiguation_applied);
}
#[test]
fn test_rule_f5_numeric_code_with_decimals_becomes_decimal_number() {
let mut result = ColumnResult {
label: "representation.identifier.numeric_code".to_string(),
confidence: 1.0,
vote_distribution: vec![("representation.identifier.numeric_code".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::HAS_LEADING_ZERO] = 0.0; cf.mean[feature_idx::IS_FLOAT] = 1.0;
let votes = vec![("representation.identifier.numeric_code".to_string(), 100)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(
result.label, "representation.numeric.decimal_number",
"numeric_code with decimal points should become decimal_number"
);
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.starts_with("feature_decimal_over_numeric_code"));
}
#[test]
fn test_rule_f5b_decimal_demoted_to_integer_when_whole() {
let mut result = ColumnResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 1.0,
vote_distribution: vec![("representation.numeric.decimal_number".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::IS_FLOAT] = 0.0; feature_sharpen(&mut result, &cf);
assert_eq!(result.label, "representation.numeric.integer_number");
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.starts_with("feature_decimal_to_integer_is_float"));
}
#[test]
fn test_rule_f5b_keeps_decimal_when_fractional() {
let mut result = ColumnResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 1.0,
vote_distribution: vec![("representation.numeric.decimal_number".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::IS_FLOAT] = 1.0;
feature_sharpen(&mut result, &cf);
assert_eq!(result.label, "representation.numeric.decimal_number");
assert!(!result.disambiguation_applied);
}
#[test]
fn test_hs_code_float_parseability() {
let codes = [
"6204.62.40", "8471.30.10",
"8471.30", "6204.62",
];
let per_value: Vec<[f32; FEATURE_DIM]> = codes.iter().map(|s| extract_features(s)).collect();
let cf = aggregate_features(&per_value);
assert!(
cf.mean[feature_idx::IS_FLOAT] < 1.0,
"HS codes with 3-segment entries should have is_float < 1.0, got {}",
cf.mean[feature_idx::IS_FLOAT]
);
assert!(
cf.mean[feature_idx::DIGIT_RATIO] > 0.7,
"HS codes should have high digit ratio, got {}",
cf.mean[feature_idx::DIGIT_RATIO]
);
}
#[test]
fn test_rule_f3_enhanced_float_parseability() {
let mut result = ColumnResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.80,
vote_distribution: vec![
("representation.numeric.decimal_number".to_string(), 0.70),
("geography.transportation.hs_code".to_string(), 0.20),
],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::DIGIT_RATIO] = 0.85;
cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 1.8; cf.mean[feature_idx::IS_FLOAT] = 0.5;
let votes = vec![
("representation.numeric.decimal_number".to_string(), 70),
("geography.transportation.hs_code".to_string(), 20),
];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(result.label, "geography.transportation.hs_code");
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.contains("feature_hs_code"));
}
#[test]
fn test_rule_f6_short_code_demoted_from_file_extension() {
let mut result = ColumnResult {
label: "representation.file.extension".to_string(),
confidence: 0.85,
vote_distribution: vec![
("representation.file.extension".to_string(), 0.70),
("representation.categorical.categorical".to_string(), 0.20),
],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
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] = 1.0; cf.mean[feature_idx::ALPHA_RATIO] = 1.0;
let votes = vec![
("representation.file.extension".to_string(), 70),
("representation.categorical.categorical".to_string(), 20),
];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(result.label, "representation.categorical.categorical");
assert!(result.disambiguation_applied);
assert!(result
.disambiguation_rule
.as_ref()
.unwrap()
.contains("feature_short_code_not_extension"));
}
#[test]
fn test_rule_f6_real_extension_not_demoted() {
let mut result = ColumnResult {
label: "representation.file.extension".to_string(),
confidence: 0.90,
vote_distribution: vec![("representation.file.extension".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::LENGTH] = 4.0; cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 2.0; cf.mean[feature_idx::ALPHA_RATIO] = 0.75;
let votes = vec![("representation.file.extension".to_string(), 90)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(
result.label, "representation.file.extension",
"should NOT demote when values contain dots"
);
assert!(!result.disambiguation_applied);
}
#[test]
fn test_rule_f6_long_values_not_demoted() {
let mut result = ColumnResult {
label: "representation.file.extension".to_string(),
confidence: 0.85,
vote_distribution: vec![("representation.file.extension".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::LENGTH] = 8.0; cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 1.0; cf.mean[feature_idx::ALPHA_RATIO] = 1.0;
let votes = vec![("representation.file.extension".to_string(), 85)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(
result.label, "representation.file.extension",
"should NOT demote when mean length > 4"
);
assert!(!result.disambiguation_applied);
}
#[test]
fn test_rule_f6_fallback_to_categorical() {
let mut result = ColumnResult {
label: "representation.file.extension".to_string(),
confidence: 0.90,
vote_distribution: vec![("representation.file.extension".to_string(), 1.0)],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 100,
detected_locale: None,
is_generic: false,
column_features: None,
};
let mut cf = ColumnFeatures::empty();
cf.mean[feature_idx::LENGTH] = 2.0;
cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 1.0;
cf.mean[feature_idx::ALPHA_RATIO] = 1.0;
let votes = vec![("representation.file.extension".to_string(), 100)];
feature_disambiguate(&mut result, &cf, &votes, 100);
assert_eq!(
result.label, "representation.categorical.categorical",
"should fallback to categorical when no second vote exists"
);
assert!(result.disambiguation_applied);
}
#[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_f3_removed_decimal_stays_decimal() {
let mut result = ColumnResult {
label: "representation.numeric.decimal_number".to_string(),
confidence: 0.80,
vote_distribution: vec![("representation.numeric.decimal_number".to_string(), 0.80)],
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::DIGIT_RATIO] = 0.85; cf.mean[feature_idx::SEGMENT_COUNT_DOT] = 2.5; cf.mean[feature_idx::IS_FLOAT] = 0.3; cf.mean[feature_idx::HAS_NEGATIVE_PREFIX] = 0.0; cf.variance[feature_idx::SEGMENT_COUNT_DOT] = 0.1;
feature_sharpen(&mut result, &cf);
assert_eq!(
result.label, "representation.numeric.decimal_number",
"F3 removed: decimal_number should NOT be overridden to hs_code by feature_sharpen"
);
assert!(
!result.disambiguation_applied,
"No disambiguation should fire for decimal_number with HS code features"
);
}
#[test]
fn test_r20_hs_code_gate_demotes_plain_decimals() {
let values: Vec<String> = vec![
"3.14", "0.887", "-12.5", "100.0", "0.003", "45.67", "1.23", "99.9",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.transportation.hs_code", 0.95, None);
assert!(result.is_some(), "R20 should fire on plain decimals");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.decimal_number");
assert!(rule.starts_with("hs_code_validation_gate"));
}
#[test]
fn test_r20_hs_code_gate_keeps_real_hs_codes() {
let values: Vec<String> = vec![
"8471.30",
"8471.30.00",
"6204.62",
"8517.12",
"0901.21",
"2204.10",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.transportation.hs_code", 0.95, None);
assert!(result.is_none(), "R20 should NOT fire on real HS codes");
}
#[test]
fn test_is_hs_code_format() {
assert!(is_hs_code_format("8471.30"));
assert!(is_hs_code_format("8471.30.00"));
assert!(is_hs_code_format("0901.21.00.10"));
assert!(is_hs_code_format("847130")); assert!(is_hs_code_format("84713000"));
assert!(!is_hs_code_format("3.14"));
assert!(!is_hs_code_format("0.887"));
assert!(!is_hs_code_format("-12.5"));
assert!(!is_hs_code_format("100.0"));
assert!(!is_hs_code_format("45.67"));
assert!(!is_hs_code_format("123"));
assert!(!is_hs_code_format("12.34"));
assert!(!is_hs_code_format("-8471.30"));
}
#[test]
fn test_r21_coordinate_gate_demotes_out_of_range() {
let values: Vec<String> = vec![
"127.013", "573.817", "201.998", "10.0", "224.419", "177.684", "97.335", "671.0", "450.2",
"300.1",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.coordinate.longitude", 0.94, None);
assert!(result.is_some(), "R21 should fire for out-of-range values");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.decimal_number");
assert!(rule.contains("coordinate_plausibility_gate"));
}
#[test]
fn test_r21_coordinate_gate_keeps_real_coordinates() {
let values: Vec<String> = vec![
"-122.4194",
"139.6917",
"2.3522",
"-73.9857",
"151.2093",
"-43.1729",
"28.9784",
"-3.7038",
"103.8198",
"-46.6333",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.coordinate.longitude", 0.95, None);
assert!(
result.is_none()
|| result
.as_ref()
.unwrap()
.1
.contains("coordinate_disambiguation"),
"R21 should not fire for valid coordinates"
);
}
#[test]
fn test_r22_upc_gate_corrects_to_ean() {
let values: Vec<String> = vec![
"1794213764625",
"4293423898067",
"6324920385397",
"3683935437077",
"5078019484874",
"8706648142321",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.commerce.upc", 0.999, None);
assert!(result.is_some(), "R22 should fire for 13-digit values");
let (label, rule) = result.unwrap();
assert_eq!(label, "identity.commerce.ean");
assert!(rule.contains("upc_digit_count_gate"));
}
#[test]
fn test_r22_upc_gate_demotes_wrong_length() {
let values: Vec<String> = vec![
"1966662179",
"6579926978",
"2527909147",
"9953906342",
"2157414996",
"6989529491",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.commerce.upc", 0.94, None);
assert!(result.is_some(), "R22 should fire for non-12-digit values");
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.identifier.numeric_code");
assert!(rule.contains("upc_digit_count_gate"));
}
#[test]
fn test_r22_upc_gate_keeps_real_upc() {
let values: Vec<String> = vec![
"012345678905",
"036000291452",
"070330507227",
"042100005264",
"040000000068",
"041570056103",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.commerce.upc", 0.99, None);
assert!(
result.is_none() || !result.as_ref().unwrap().1.contains("upc_digit_count_gate"),
"R22 should not fire for valid 12-digit UPC"
);
}
#[test]
fn test_r23_isin_gate_corrects_isrc() {
let values: Vec<String> = vec![
"US0378331005",
"GB0002634946",
"DE0007164600",
"JP3435000009",
"FR0000120271",
"CA0585861085",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.commerce.isrc", 0.97, None);
assert!(result.is_some(), "R23 should fire for ISIN-format values");
let (label, rule) = result.unwrap();
assert_eq!(label, "finance.securities.isin");
assert!(rule.contains("isin_format_gate"));
}
#[test]
fn test_r24_issn_gate_corrects_ein() {
let values: Vec<String> = vec![
"1781-2253",
"8371-5342",
"6910-7471",
"2908-3721",
"8987-7548",
"4149-8688",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "identity.government.ein", 0.91, None);
assert!(result.is_some(), "R24 should fire for ISSN-format values");
let (label, rule) = result.unwrap();
assert_eq!(label, "identity.commerce.issn");
assert!(rule.contains("issn_format_gate"));
}
#[test]
fn test_is_isin_format() {
assert!(is_isin_format("US0378331005"));
assert!(is_isin_format("GB0002634946"));
assert!(is_isin_format("AU000000BHP4"));
assert!(is_isin_format("NL0011540547"));
assert!(!is_isin_format("SE3YX3859059")); assert!(!is_isin_format("NLEK47515013")); assert!(!is_isin_format("CAHRM7311593")); assert!(!is_isin_format("US-Z03-98-12345")); assert!(!is_isin_format("1234567890AB")); assert!(!is_isin_format("USABC")); assert!(!is_isin_format("us0378331005")); }
#[test]
fn test_is_issn_format() {
assert!(is_issn_format("1781-2253"));
assert!(is_issn_format("0317-839X")); assert!(is_issn_format("0000-0000"));
assert!(!is_issn_format("12-3456789"));
assert!(!is_issn_format("1234-567"));
assert!(!is_issn_format("12345-6789"));
assert!(!is_issn_format("12345678"));
}
#[test]
fn test_sharpen_f5_numeric_code_demoted_single_vote() {
let mut result = ColumnResult {
label: "representation.identifier.numeric_code".to_string(),
confidence: 0.90,
vote_distribution: vec![("representation.identifier.numeric_code".to_string(), 0.90)],
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::HAS_LEADING_ZERO] = 0.0; cf.mean[feature_idx::IS_FLOAT] = 0.0;
feature_sharpen(&mut result, &cf);
assert_eq!(
result.label, "representation.numeric.integer_number",
"F5 should demote numeric_code without leading zeros via feature_sharpen"
);
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_r5_day_of_week_single_vote() {
let values: Vec<String> = vec!["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.discrete.categorical", 0.80, None);
assert!(result.is_some(), "R5 should fire for day-of-week values");
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.component.day_of_week");
assert_eq!(rule, "day_of_week_name_detection");
}
#[test]
fn test_sharpen_r8_gender_single_vote() {
let values: Vec<String> = vec!["Male", "Female", "Male", "Female", "Male"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.discrete.categorical", 0.80, None);
assert!(result.is_some(), "R8 should fire for gender values");
let (label, rule) = result.unwrap();
assert_eq!(label, "identity.person.gender");
assert_eq!(rule, "gender_detection");
}
#[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 test_sharpen_r12_numeric_single_vote() {
let values: Vec<String> = vec!["2020", "2021", "2022", "2023", "2024"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.numeric.integer_number", 0.80, None);
assert!(result.is_some(), "R12 should fire for year-like values");
let (label, _rule) = result.unwrap();
assert_eq!(label, "datetime.component.year");
}
#[test]
fn test_sharpen_r17_utc_offset_single_vote() {
let values: Vec<String> = vec!["+05:30", "-08:00", "+00:00", "+09:00", "-05:00"]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.numeric.decimal_number", 0.70, None);
assert!(result.is_some(), "R17 should fire for UTC offset values");
let (label, rule) = result.unwrap();
assert_eq!(label, "datetime.offset.utc");
assert_eq!(rule, "utc_offset_override_time");
}
#[test]
fn test_sharpen_attractor_high_confidence_no_demotion() {
let values: Vec<String> = vec!["10001", "90210", "60601", "30301", "94102"]
.into_iter()
.map(String::from)
.collect();
let result = sharpen_attractor_demotion(
&values,
"geography.address.postal_code",
0.95, None, );
assert!(
result.is_none(),
"High confidence (0.95) should NOT trigger attractor demotion"
);
}
#[test]
fn test_sharpen_attractor_low_confidence_demotes() {
let values: Vec<String> = vec!["42", "100", "7", "256", "1024"]
.into_iter()
.map(String::from)
.collect();
let result = sharpen_attractor_demotion(
&values,
"geography.address.postal_code",
0.30, None, );
assert!(
result.is_some(),
"Low confidence (0.30) should trigger attractor demotion"
);
let (label, rule) = result.unwrap();
assert_eq!(label, "representation.numeric.integer_number");
assert!(
rule.starts_with("attractor_demotion_confidence"),
"Rule should be confidence-based demotion, got: {}",
rule
);
}
#[test]
fn test_sharpen_attractor_text_low_confidence_demotes_to_categorical() {
let values: Vec<String> = vec!["alpha", "beta", "gamma", "alpha", "beta"]
.into_iter()
.map(String::from)
.collect();
let result = sharpen_attractor_demotion(
&values,
"identity.person.first_name",
0.40, None,
);
assert!(
result.is_some(),
"Low confidence text attractor should demote"
);
let (label, _rule) = result.unwrap();
assert_eq!(label, "representation.text.word");
}
#[test]
fn test_sharpen_attractor_non_attractor_type_ignored() {
let values: Vec<String> = vec!["hello@example.com"]
.into_iter()
.map(String::from)
.collect();
let result = sharpen_attractor_demotion(
&values,
"identity.person.email",
0.30, None,
);
assert!(
result.is_none(),
"Non-attractor type should never trigger demotion"
);
}
#[test]
fn ac07a_data_uri_header_no_longer_matches_url() {
assert_eq!(
header_hint("data_uri"),
None,
"data_uri should not match url after h.contains(\"uri\") removal"
);
}
#[test]
fn ac07a_uri_exact_match_still_works() {
assert_eq!(header_hint("uri"), Some("technology.internet.url"));
}
#[test]
fn ac07a_url_keyword_still_works() {
assert_eq!(header_hint("download_url"), Some("technology.internet.url"));
assert_eq!(header_hint("redirect_url"), Some("technology.internet.url"));
}
#[test]
fn ac07a_link_href_keywords_still_work() {
assert_eq!(
header_hint("external_link"),
Some("technology.internet.url")
);
assert_eq!(header_hint("href"), Some("technology.internet.url"));
}
#[test]
fn ac07a_request_uri_no_longer_matches_url() {
assert_eq!(
header_hint("request_uri"),
None,
"request_uri should not match url after h.contains(\"uri\") removal"
);
}
#[test]
fn ac07b_r20_still_validates_hs_codes() {
let values: Vec<String> = vec![
"8471.30",
"8471.30.00",
"6204.62",
"8517.12",
"0901.21",
"2204.10",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.transportation.hs_code", 0.95, None);
assert!(
result.is_none(),
"R20 should keep valid hs_code predictions after F3 removal"
);
}
#[test]
fn ac07b_r20_still_demotes_false_hs_codes() {
let values: Vec<String> = vec![
"3.14", "0.887", "-12.5", "100.0", "0.003", "45.67", "1.23", "99.9",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "geography.transportation.hs_code", 0.95, None);
assert!(result.is_some(), "R20 should still demote false hs_code");
let (label, _) = result.unwrap();
assert_eq!(label, "representation.numeric.decimal_number");
}
#[test]
fn ac05_country_exact_match_still_hints_country() {
assert_eq!(header_hint("country"), Some("geography.location.country"));
}
#[test]
fn ac05_alpha2_regex_matches_country_codes() {
let codes = ["AU", "US", "GB", "DE", "FR", "JP", "CN", "BR"];
for code in &codes {
assert!(
code.len() == 2 && code.chars().all(|c| c.is_ascii_uppercase()),
"{} should match alpha-2 pattern",
code
);
}
}
#[test]
fn ac05_alpha2_regex_rejects_state_codes() {
let state_codes = ["NSW", "VIC", "QLD", "CA", "NY"];
for code in &state_codes {
let matches = code.len() == 2 && code.chars().all(|c| c.is_ascii_uppercase());
if code.len() == 3 {
assert!(
!matches,
"{} (3-char) should not match alpha-2 pattern",
code
);
}
}
}
#[test]
fn ac05_alpha2_regex_rejects_lowercase() {
let lower = ["au", "us", "gb"];
for code in &lower {
assert!(
!(code.len() == 2 && code.chars().all(|c| c.is_ascii_uppercase())),
"{} should not match alpha-2 pattern",
code
);
}
}
#[test]
fn ac05_alpha2_regex_rejects_country_names() {
let names = ["Australia", "United States", "Germany", "France"];
for name in &names {
assert!(
!(name.len() == 2 && name.chars().all(|c| c.is_ascii_uppercase())),
"{} should not match alpha-2 pattern",
name
);
}
}
fn vals(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn username_veto_fires_on_login_handles() {
let handles = vals(&[
"tptacek",
"patio11",
"rms",
"jacquesm",
"petercooper",
"dfens",
"schof",
]);
assert!(is_username_handle_shaped(&handles));
}
#[test]
fn username_veto_fires_on_handles_with_digits_underscores() {
let handles = vals(&["peter123", "dc2k08", "adam_null", "steve19", "mr-justin"]);
assert!(is_username_handle_shaped(&handles));
}
#[test]
fn username_veto_skips_real_full_names() {
let names = vals(&[
"John Smith",
"Mary Jane Watson",
"Alan Turing",
"Ada Lovelace",
"Grace Hopper",
]);
assert!(!is_username_handle_shaped(&names));
}
#[test]
fn username_veto_skips_entity_and_org_names() {
let orgs = vals(&[
"Portland Trail Blazers",
"Golden State Warriors",
"New York Knicks",
"Boston Celtics",
]);
assert!(!is_username_handle_shaped(&orgs));
}
#[test]
fn username_veto_skips_mixed_author_lists() {
let mixed = vals(&[
"Jane Doe",
"jsmith",
"Robert C. Martin",
"kent_beck",
"Martin Fowler",
]);
assert!(!is_username_handle_shaped(&mixed));
}
#[test]
fn username_veto_needs_minimum_evidence() {
let scant = vals(&["rms", "", " "]);
assert!(!is_username_handle_shaped(&scant));
}
#[test]
fn username_veto_skips_low_cardinality_vocab() {
let drugs = vals(&[
"ethanol", "ethanol", "ethanol", "morphine", "morphine", "morphine", "ethanol", "morphine",
]);
assert!(!is_username_handle_shaped(&drugs));
let exchanges = vals(&["NMS", "NYQ", "NMS", "NYQ", "NMS", "NYQ", "PCX", "NMS"]);
assert!(!is_username_handle_shaped(&exchanges));
}
#[test]
fn username_veto_still_fires_on_high_cardinality_handles() {
let handles = vals(&[
"ww520",
"KevBurnsJr",
"thinkcomp",
"cjensen",
"pmiller2",
"vic_nyc",
"andrewcooke",
"lanstein",
]);
assert!(is_username_handle_shaped(&handles));
}
#[test]
fn test_datetime_refinement_fires_when_taxonomy_accepts() {
let yaml = r#"
datetime.timestamp.sql_standard:
title: SQL Standard
designation: universal
tier: [TIMESTAMP, timestamp]
release_priority: 5
samples: ["2020-01-03 14:22:09"]
validation:
type: string
pattern: "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{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 mut r = ColumnResult {
label: "unknown".to_string(),
confidence: 0.3,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 2,
detected_locale: None,
is_generic: false,
column_features: None,
};
let sample = vec![
"2001-09-17 00:00:00".to_string(),
"2011-08-30 00:00:00".to_string(),
];
cc.datetime_format_refinement(&mut r, &sample);
assert_eq!(r.label, "datetime.timestamp.sql_standard");
assert_eq!(
r.disambiguation_rule.as_deref(),
Some("datetime_format_refinement")
);
}
#[test]
fn compose_from_sense_runs_sharpen() {
let yaml = r#"
technology.internet.url:
title: URL
designation: universal
tier: [VARCHAR, internet]
release_priority: 3
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 binary_vocab_veto_full_column_catches_rare_count() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let mut counts: Vec<String> = vec!["0".into(); 60];
counts.push("1".into());
counts.push("30".into());
let r = cc
.compose_from_sense("Comments", &counts, "representation.boolean.binary", 0.5)
.unwrap();
assert_eq!(r.label, "representation.numeric.integer_number");
let bins: Vec<String> = vec!["0".into(), "1".into(), "0".into(), "1".into(), "1".into()];
let r2 = cc
.compose_from_sense("flag", &bins, "representation.boolean.binary", 0.5)
.unwrap();
assert_eq!(r2.label, "representation.boolean.binary");
}
#[test]
fn isbn_header_recovery_promotes_checked_isbn() {
let yaml = r#"
identity.commerce.isbn:
title: ISBN
designation: universal
tier: [VARCHAR, commerce]
release_priority: 3
samples: ["0306406152"]
"#;
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 isbns: Vec<String> = vec!["0306406152", "0140449132", "043942089X", "0201633612"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"Primary ISBN10",
&isbns,
"representation.identifier.numeric_code",
1.0,
)
.unwrap();
assert_eq!(r.label, "identity.commerce.isbn", "isbn recovery must fire");
}
#[test]
fn isbn_header_recovery_declines_failed_checksum_and_no_header() {
let mut cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
cc.set_taxonomy({
let mut t = Taxonomy::from_yaml(
"identity.commerce.isbn:\n title: ISBN\n designation: universal\n tier: [VARCHAR, commerce]\n samples: [\"0306406152\"]\n",
)
.unwrap();
t.compile_validators();
t
});
let bad: Vec<String> = vec!["5150000128", "5150000129", "5150000130"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"Primary ISBN10",
&bad,
"representation.numeric.integer_number",
1.0,
)
.unwrap();
assert_ne!(
r.label, "identity.commerce.isbn",
"must decline failed checksum"
);
let isbns: Vec<String> = vec!["0306406152", "0140449132", "043942089X"]
.into_iter()
.map(String::from)
.collect();
let r2 = cc
.compose_from_sense(
"book_code",
&isbns,
"representation.identifier.numeric_code",
1.0,
)
.unwrap();
assert_ne!(
r2.label, "identity.commerce.isbn",
"must decline without header"
);
}
#[test]
fn ceded_leaf_recovery_reasserts_uuid_over_relocated_label() {
let yaml = r#"
representation.identifier.uuid:
title: UUID
designation: universal
tier: [VARCHAR, identifier]
validation:
type: string
pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
samples: ["550e8400-e29b-41d4-a716-446655440000"]
"#;
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 uuids: Vec<String> = vec![
"550e8400-e29b-41d4-a716-446655440000",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"6ba7b811-9dad-11d1-80b4-00c04fd430c8",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"token",
&uuids,
"representation.identifier.alphanumeric_id",
1.0,
)
.unwrap();
assert_eq!(
r.label, "representation.identifier.uuid",
"ceded_leaf_recovery must re-assert uuid from values"
);
}
#[test]
fn ceded_leaf_recovery_declines_non_validating_values() {
let yaml = r#"
representation.identifier.uuid:
title: UUID
designation: universal
tier: [VARCHAR, identifier]
validation:
type: string
pattern: "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
samples: ["550e8400-e29b-41d4-a716-446655440000"]
"#;
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 words: Vec<String> = vec!["apple", "banana", "cherry", "date"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("fruit", &words, "representation.text.word", 1.0)
.unwrap();
assert_ne!(
r.label, "representation.identifier.uuid",
"ceded_leaf_recovery must not fire on non-validating values"
);
}
#[test]
fn unlocode_format_veto_demotes_contradicting_values() {
let yaml = r#"
geography.transportation.unlocode:
title: UN/LOCODE
designation: universal
tier: [VARCHAR, transportation]
samples: ["USNYC"]
validation:
type: string
pattern: '^[A-Z]{2}[A-Z2-9]{3}$'
"#;
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 postcodes: Vec<String> = vec!["CM13 3GF", "SW1A 1AA", "EC1A 1BB"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("loc", &postcodes, "geography.transportation.unlocode", 0.97)
.unwrap();
assert_ne!(
r.label, "geography.transportation.unlocode",
"contradicting values must demote the unlocode overcall"
);
}
#[test]
fn unlocode_format_veto_keeps_valid_unlocodes() {
let yaml = r#"
geography.transportation.unlocode:
title: UN/LOCODE
designation: universal
tier: [VARCHAR, transportation]
samples: ["USNYC"]
validation:
type: string
pattern: '^[A-Z]{2}[A-Z2-9]{3}$'
"#;
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 codes: Vec<String> = vec!["USNYC", "DEHAM", "NLRTM", "GBLON"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("loc", &codes, "geography.transportation.unlocode", 0.97)
.unwrap();
assert_eq!(r.label, "geography.transportation.unlocode");
}
#[test]
fn membership_substance_guard_is_default_on() {
assert!(!rhh::is_disabled("membership_substance_guard"));
}
#[test]
fn membership_guard_demotes_tickers_off_icao() {
let yaml = r#"
geography.transportation.icao_code:
title: ICAO Airport Code
designation: universal
tier: [VARCHAR, transportation]
samples: ["KJFK"]
validation:
type: string
pattern: '^[A-Z]{4}$'
membership: icao_airports
"#;
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 tickers: Vec<String> = vec![
"AAPL", "MSFT", "TSLA", "NVDA", "AMZN", "META", "NFLX", "ORCL", "ADBE", "INTC",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"symbol",
&tickers,
"geography.transportation.icao_code",
0.93,
)
.unwrap();
assert_ne!(
r.label, "geography.transportation.icao_code",
"non-member tickers must demote the icao overcall"
);
assert_eq!(
r.disambiguation_rule.as_deref(),
Some("membership_substance_guard:geography.transportation.icao_code")
);
}
#[test]
fn membership_guard_keeps_real_airport_codes() {
let yaml = r#"
geography.transportation.icao_code:
title: ICAO Airport Code
designation: universal
tier: [VARCHAR, transportation]
samples: ["KJFK"]
validation:
type: string
pattern: '^[A-Z]{4}$'
membership: icao_airports
"#;
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 codes: Vec<String> = vec!["KJFK", "KLAX", "LFPG", "EGLL", "RJTT", "YSSY"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"airport",
&codes,
"geography.transportation.icao_code",
0.93,
)
.unwrap();
assert_eq!(r.label, "geography.transportation.icao_code");
}
#[test]
fn membership_guard_demotes_nonmember_codes_off_iata() {
let yaml = r#"
geography.transportation.iata_code:
title: IATA Airport Code
designation: universal
tier: [VARCHAR, transportation]
samples: ["JFK"]
validation:
type: string
pattern: '^[A-Z]{3}$'
membership: iata_airports
"#;
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 codes: Vec<String> = vec![
"USD", "EUR", "NZD", "INR", "RUB", "NKE", "PFE", "CVX", "XOM",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("code", &codes, "geography.transportation.iata_code", 0.9)
.unwrap();
assert_ne!(
r.label, "geography.transportation.iata_code",
"non-member codes must demote the iata overcall"
);
}
#[test]
fn entity_prose_override_demotes_prose_to_plain_text() {
let values: Vec<String> = vec![
"Type 2 diabetes mellitus without complications",
"Provides consulting and advisory services to businesses",
"Chronic obstructive pulmonary disease unspecified",
"Offers cloud software for enterprise resource planning",
"Alcohol dependence uncomplicated with related disorders",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.text.entity_name", 0.93, None);
let (label, rule) = result.expect("prose should demote off entity_name");
assert_eq!(label, "representation.text.plain_text");
assert!(rule.starts_with("entity_prose_override:"));
}
#[test]
fn entity_prose_override_keeps_org_names_with_connectors() {
let values: Vec<String> = vec![
"Bank of America Corporation",
"McKinsey & Company",
"Fidelity Advisor Leveraged Company Stock Fund",
"University of California Press",
"Museum of Modern Art",
"Procter & Gamble Co",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.text.entity_name", 0.93, None);
assert!(
result.is_none(),
"org names with lowercase connectors must not demote off entity_name"
);
}
#[test]
fn entity_prose_override_keeps_species_binomials() {
let values: Vec<String> = vec![
"Homo sapiens",
"Mus musculus",
"Rattus norvegicus",
"Danio rerio",
]
.into_iter()
.map(String::from)
.collect();
let result = value_sharpen(&values, "representation.text.entity_name", 0.93, None);
assert!(result.is_none(), "species binomials must stay entity_name");
}
#[test]
fn numeric_code_recovery_restores_naics_and_cik() {
let yaml = r#"
representation.identifier.numeric_code:
title: "Numeric Code"
validation:
type: string
pattern: '^[0-9]+$'
tier: [VARCHAR, identifier]
release_priority: 4
samples: ["00120"]
"#;
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 ciks: Vec<String> = vec![
"320193", "1652044", "789019", "1018724", "1318605", "320193", "789019", "1067983",
"1652044", "1318605", "320193", "104169",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("cik", &ciks, "representation.numeric.integer_number", 0.8)
.unwrap();
assert_eq!(
r.label, "representation.identifier.numeric_code",
"rule was {:?}",
r.disambiguation_rule
);
assert!(r
.disambiguation_rule
.as_deref()
.unwrap_or("")
.starts_with("numeric_code_header_recovery:"));
}
#[test]
fn numeric_code_recovery_leaves_quantities_and_postal_alone() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let counts: Vec<String> = vec!["1200", "845", "23000", "410"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"employees",
&counts,
"representation.numeric.integer_number",
0.8,
)
.unwrap();
assert_eq!(r.label, "representation.numeric.integer_number");
let zips: Vec<String> = vec!["90210", "10001", "60601", "94102"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"zip_code",
&zips,
"representation.numeric.integer_number",
0.8,
)
.unwrap();
assert_ne!(r.label, "representation.identifier.numeric_code");
assert!(!r
.disambiguation_rule
.as_deref()
.unwrap_or("")
.starts_with("numeric_code_header_recovery:"));
}
#[test]
fn test_schema_fail_demotion_names_off_swift_bic() {
let values: Vec<String> = (0..30)
.map(|i| format!("ACME HOLDINGS INTERNATIONAL {}", i))
.collect();
let yaml = r#"
finance.banking.swift_bic:
title: "SWIFT/BIC"
validation:
type: string
pattern: "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$"
tier: [VARCHAR, banking]
release_priority: 3
samples: ["DEUTDEFF"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "finance.banking.swift_bic", 0.9, Some(&taxonomy));
let (label, rule) = result.expect("name strings should demote off swift_bic");
assert_eq!(label, "representation.identifier.alphanumeric_id");
assert!(rule.starts_with("schema_fail_demotion:"));
}
#[test]
fn test_schema_fail_demotion_keeps_real_swift_bic() {
let values: Vec<String> = vec![
"DEUTDEFF",
"CHASUS33",
"BARCGB22",
"BNPAFRPP",
"CITIUS33XXX",
"HSBCHKHH",
]
.into_iter()
.map(String::from)
.collect();
let yaml = r#"
finance.banking.swift_bic:
title: "SWIFT/BIC"
validation:
type: string
pattern: "^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$"
tier: [VARCHAR, banking]
release_priority: 3
samples: ["DEUTDEFF"]
"#;
let taxonomy = Taxonomy::from_yaml(yaml).unwrap();
let result = value_sharpen(&values, "finance.banking.swift_bic", 0.9, Some(&taxonomy));
assert!(
result.is_none(),
"real BIC values must not be demoted off swift_bic"
);
}
#[test]
fn naics_recovery_is_default_on() {
assert!(!rhh::is_disabled("naics_industry_recovery"));
}
#[test]
fn naics_recovery_promotes_naics_headed_member_columns() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let naics: Vec<String> = vec![
"541511", "236220", "722511", "621111", "445110", "541511", "722511", "236220", "541512",
"928120",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"NAICS Code",
&naics,
"representation.numeric.integer_number",
0.7,
)
.unwrap();
assert_eq!(
r.label, "identity.industry.naics",
"rule was {:?}",
r.disambiguation_rule
);
}
#[test]
fn naics_recovery_requires_the_header_gate() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let vals: Vec<String> = vec!["11", "21", "23", "31", "42", "44", "11", "21"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"team_size",
&vals,
"representation.numeric.integer_number",
0.7,
)
.unwrap();
assert_ne!(r.label, "identity.industry.naics");
}
#[test]
fn naics_recovery_requires_membership() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let vals: Vec<String> = vec!["100000", "999999", "050000", "130000", "990000"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense(
"naics_code",
&vals,
"representation.numeric.integer_number",
0.7,
)
.unwrap();
assert_ne!(r.label, "identity.industry.naics");
}
#[test]
fn naics_recovery_admits_bare_code_header_for_long_codes() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let vals: Vec<String> = vec![
"541511", "236220", "722511", "621111", "445110", "541512", "722511", "236220",
]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("code", &vals, "representation.numeric.integer_number", 0.7)
.unwrap();
assert_eq!(
r.label, "identity.industry.naics",
"rule was {:?}",
r.disambiguation_rule
);
}
#[test]
fn naics_recovery_rejects_bare_code_header_for_sector_length_values() {
let cc =
ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new("unknown")));
let vals: Vec<String> = vec!["11", "22", "31", "44", "55", "62", "72", "81", "11", "44"]
.into_iter()
.map(String::from)
.collect();
let r = cc
.compose_from_sense("code", &vals, "representation.numeric.integer_number", 0.7)
.unwrap();
assert_ne!(r.label, "identity.industry.naics");
}
#[test]
fn test_structured_string_refinement() {
let yaml = r#"
technology.filesystem.windows_path:
title: Windows Path
designation: universal
tier: [VARCHAR, filesystem]
release_priority: 3
samples: ["C:\\x"]
validation:
type: string
pattern: '^([A-Za-z]:\\|\\\\)[^\r\n]*$'
technology.internet.message_id:
title: Message ID
designation: universal
tier: [VARCHAR, internet]
release_priority: 3
samples: ["<a@b>"]
validation:
type: string
pattern: '^<[^<>@\s]+@[^<>@\s]+>$'
technology.code.qualified_name:
title: Qualified Name
designation: universal
tier: [VARCHAR, code]
release_priority: 3
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 test_datetime_refinement_blocked_when_taxonomy_rejects() {
let yaml = r#"
datetime.timestamp.iso_8601_milliseconds:
title: ISO 8601 ms
designation: universal
tier: [TIMESTAMP, timestamp]
release_priority: 5
samples: ["2020-01-03T14:22:09.123Z"]
validation:
type: string
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"
"#;
let mut tax = Taxonomy::from_yaml(yaml).unwrap();
tax.compile_validators();
let mut cc = ColumnClassifier::with_defaults(Box::new(crate::inference::MockClassifier::new(
"representation.identifier.alphanumeric_id",
)));
cc.set_taxonomy(tax);
let mut r = ColumnResult {
label: "representation.identifier.alphanumeric_id".to_string(),
confidence: 0.5,
vote_distribution: vec![],
disambiguation_applied: false,
disambiguation_rule: None,
samples_used: 2,
detected_locale: None,
is_generic: false,
column_features: None,
};
let sample = vec![
"2013-06-04T01:02:03.123".to_string(),
"2018-01-01T05:30:00.000".to_string(),
];
cc.datetime_format_refinement(&mut r, &sample);
assert_eq!(
r.label, "representation.identifier.alphanumeric_id",
"zoneless iso-millis must not be asserted — taxonomy rejects that leaf"
);
}