use std::collections::BTreeSet;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy)]
pub struct VocabNamespace {
pub prefix: &'static str,
pub keys: &'static [&'static str],
}
impl VocabNamespace {
pub fn contains(&self, key: &str) -> bool {
let bare = key
.strip_prefix(self.prefix)
.map_or(key, |suffix| suffix.strip_prefix('.').unwrap_or(suffix));
self.keys.contains(&bare)
}
}
pub const SESSION: VocabNamespace = VocabNamespace {
prefix: "harn.session",
keys: &[
"id",
"op",
"outcome",
"schema",
"rows",
"bytes",
"kind",
"duration_ms",
],
};
pub const PERMISSION: VocabNamespace = VocabNamespace {
prefix: "harn.permission",
keys: &[
"action",
"decision",
"rule",
"scope",
"actor",
"duration_ms",
"outcome",
],
};
pub const MCP: VocabNamespace = VocabNamespace {
prefix: "harn.mcp",
keys: &[
"server",
"tool",
"outcome",
"restart_count",
"cache_hit",
"duration_ms",
"scope",
],
};
pub const COMPACTION: VocabNamespace = VocabNamespace {
prefix: "harn.compaction",
keys: &[
"strategy",
"input_tokens",
"output_tokens",
"trigger",
"duration_ms",
"outcome",
],
};
pub const PG: VocabNamespace = VocabNamespace {
prefix: "harn.pg",
keys: &[
"query_name",
"rows",
"duration_ms",
"pool_size",
"wait_ms",
"outcome",
],
};
pub const HTTP: VocabNamespace = VocabNamespace {
prefix: "harn.http",
keys: &[
"status",
"method",
"path",
"body_kind",
"duration_ms",
"outcome",
],
};
pub const COMMON: VocabNamespace = VocabNamespace {
prefix: "harn",
keys: &[
"tenant_id",
"request_id",
"trace_id",
"span_id",
"scope_set",
"service",
],
};
pub const ALL: &[VocabNamespace] = &[SESSION, PERMISSION, MCP, COMPACTION, PG, HTTP, COMMON];
pub fn namespace_for(key: &str) -> Option<&'static VocabNamespace> {
ALL.iter().find(|ns| {
key.starts_with(ns.prefix) && key.as_bytes().get(ns.prefix.len()).copied() == Some(b'.')
})
}
pub fn is_known_key(key: &str) -> bool {
namespace_for(key).is_some_and(|ns| ns.contains(key))
}
pub fn is_violation(key: &str) -> bool {
namespace_for(key).is_some_and(|ns| !ns.contains(key))
}
pub fn declared_keys() -> &'static BTreeSet<String> {
static CACHE: OnceLock<BTreeSet<String>> = OnceLock::new();
CACHE.get_or_init(|| {
let mut set = BTreeSet::new();
for ns in ALL {
for key in ns.keys {
set.insert(format!("{}.{}", ns.prefix, key));
}
}
set
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_key_accepts_bare_and_qualified_forms() {
assert!(SESSION.contains("id"));
assert!(SESSION.contains("harn.session.id"));
assert!(MCP.contains("tool"));
assert!(MCP.contains("harn.mcp.tool"));
}
#[test]
fn unknown_key_under_known_namespace_is_violation() {
assert!(is_violation("harn.mcp.boops"));
assert!(!is_violation("harn.mcp.tool"));
}
#[test]
fn keys_outside_harn_prefix_are_not_violations() {
assert!(!is_violation("user.id"));
assert!(!is_violation("custom.key"));
assert!(!is_known_key("custom.key"));
}
#[test]
fn namespace_for_handles_overlapping_prefixes() {
assert_eq!(
namespace_for("harn.session.id").map(|ns| ns.prefix),
Some("harn.session")
);
assert_eq!(
namespace_for("harn.tenant_id").map(|ns| ns.prefix),
Some("harn")
);
}
#[test]
fn declared_keys_lists_each_namespace_entry() {
let keys = declared_keys();
assert!(keys.contains("harn.session.id"));
assert!(keys.contains("harn.mcp.tool"));
assert!(keys.contains("harn.pg.query_name"));
assert!(keys.contains("harn.request_id"));
}
}