{"record":{"id":"294d7b53b8a81ab3","repo":"zeroclaw-labs/zeroclaw","slug":"unknown-otp-domain-category-category-known-ca","errorCode":null,"errorMessage":"Unknown OTP domain category '{category}'. Known categories: {known}","messagePattern":"Unknown OTP domain category '(.+?)'\\. Known categories: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-config/src/domain_matcher.rs","lineNumber":87,"sourceCode":"        self.patterns\n            .iter()\n            .any(|pattern| domain_matches_pattern(pattern, &normalized_domain))\n    }\n\n    pub fn expand_categories(categories: &[String]) -> Result<Vec<String>> {\n        let mut expanded = Vec::new();\n        for category in categories {\n            let normalized = category.trim().to_ascii_lowercase();\n            let Some((_, domains)) = DOMAIN_CATEGORIES\n                .iter()\n                .find(|(name, _)| *name == normalized.as_str())\n            else {\n                let known = DOMAIN_CATEGORIES\n                    .iter()\n                    .map(|(name, _)| *name)\n                    .collect::<Vec<_>>()\n                    .join(\", \");\n                bail!(\"Unknown OTP domain category '{category}'. Known categories: {known}\");\n            };\n            expanded.extend(domains.iter().map(|domain| (*domain).to_string()));\n        }\n        Ok(expanded)\n    }\n\n    pub fn validate_pattern(pattern: &str) -> Result<()> {\n        let _ = normalize_pattern(pattern)?;\n        Ok(())\n    }\n}\n\nfn normalize_domain(raw: &str) -> Option<String> {\n    let mut domain = raw.trim().to_ascii_lowercase();\n    if domain.is_empty() {\n        return None;\n    }\n","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-config/src/domain_matcher.rs#L69-L105","documentation":"DomainMatcher::expand_categories maps named OTP domain categories to their built-in pattern lists. Exactly four categories exist in DOMAIN_CATEGORIES: banking, medical, government, identity_providers. Names are trimmed and lowercased before lookup, so casing never fails — only a genuine typo or unknown name does, and the error lists every valid name.","triggerScenarios":"Calling `DomainMatcher::new(gated_domains, categories)` or `expand_categories(categories)` with any string outside {banking, medical, government, identity_providers} — e.g. \"identity-provider\" (hyphen instead of underscore) or \"finance\".","commonSituations":"Hand-editing the OTP-gated domains config and guessing at category names; copying a category from older docs or a different tool's vocabulary; hyphen/underscore confusion from slug-style naming conventions elsewhere in the config.","solutions":["Use one of the exact names: banking, medical, government, identity_providers","Pre-validate the config's category list against the four known names at load time","If no built-in category fits, enumerate the concrete domain patterns in gated_domains instead of inventing a category"],"exampleFix":"# before\ncategories = [\"identity-provider\"]  # bails\n\n# after\ncategories = [\"identity_providers\"]","handlingStrategy":"validation","validationCode":"const KNOWN_CATEGORIES: &[&str] = &[\"banking\", \"medical\", \"government\", \"identity_providers\"];\n\nfor c in &categories {\n    let n = c.trim().to_ascii_lowercase();\n    if !KNOWN_CATEGORIES.contains(&n.as_str()) {\n        anyhow::bail!(\"unknown OTP category {c}; expected one of {KNOWN_CATEGORIES:?}\");\n    }\n}\nlet matcher = DomainMatcher::new(&gated, &categories)?;","typeGuard":"fn is_known_category(name: &str) -> bool {\n    let n = name.trim().to_ascii_lowercase();\n    matches!(n.as_str(), \"banking\" | \"medical\" | \"government\" | \"identity_providers\")\n}","tryCatchPattern":"match DomainMatcher::new(&gated, &categories) {\n    Ok(m) => m,\n    Err(e) if e.to_string().contains(\"Unknown OTP domain category\") => {\n        anyhow::bail!(\"config error: {e}; fix the otp categories list\")\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Validate category names at config load, not at first OTP check","Prefer copying the four canonical names from the docs table, not from memory","If a category is missing, fall back to explicit gated_domains patterns"],"tags":["domain-matcher","otp","config-validation","enum-value"],"backgroundTag":"unknown-config-enum-value","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}