use crate::memory_core::palace::RoomType;
use uuid::Uuid;
const KEY_SEP: char = '\u{1f}';
pub const ROOM_NAMESPACE: Uuid = Uuid::from_bytes([
193, 10, 247, 149, 38, 17, 83, 26, 166, 218, 54, 18, 11, 146, 68, 19,
]);
pub const DEFAULT_WING_ID: Uuid = Uuid::from_bytes([
80, 71, 66, 222, 8, 75, 84, 80, 164, 24, 67, 237, 14, 82, 201, 141,
]);
pub fn room_label(room: &RoomType) -> String {
match room {
RoomType::Frontend => "Frontend".to_string(),
RoomType::Backend => "Backend".to_string(),
RoomType::Testing => "Testing".to_string(),
RoomType::Planning => "Planning".to_string(),
RoomType::Documentation => "Documentation".to_string(),
RoomType::Research => "Research".to_string(),
RoomType::Configuration => "Configuration".to_string(),
RoomType::Meetings => "Meetings".to_string(),
RoomType::General => "General".to_string(),
RoomType::Custom(s) => s.clone(),
}
}
pub fn room_type_tag(room: &RoomType) -> &'static str {
match room {
RoomType::Frontend => "Frontend",
RoomType::Backend => "Backend",
RoomType::Testing => "Testing",
RoomType::Planning => "Planning",
RoomType::Documentation => "Documentation",
RoomType::Research => "Research",
RoomType::Configuration => "Configuration",
RoomType::Meetings => "Meetings",
RoomType::General => "General",
RoomType::Custom(_) => "Custom",
}
}
pub fn room_type_from_parts(tag: &str, label: &str) -> RoomType {
match tag {
"Frontend" => RoomType::Frontend,
"Backend" => RoomType::Backend,
"Testing" => RoomType::Testing,
"Planning" => RoomType::Planning,
"Documentation" => RoomType::Documentation,
"Research" => RoomType::Research,
"Configuration" => RoomType::Configuration,
"Meetings" => RoomType::Meetings,
"General" => RoomType::General,
_ => RoomType::Custom(label.to_string()),
}
}
pub fn canonical_room_key(wing_id: Uuid, label: &str) -> String {
format!("{wing_id}{KEY_SEP}{}", label.trim().to_lowercase())
}
pub fn default_wing_key(room: &RoomType) -> String {
canonical_room_key(DEFAULT_WING_ID, &room_label(room))
}
pub fn parse_room_preserving_case(name: &str) -> RoomType {
match RoomType::parse(name) {
RoomType::Custom(_) => RoomType::Custom(name.trim().to_string()),
builtin => builtin,
}
}
pub fn mint_room_id(key: &str) -> Uuid {
Uuid::new_v5(&ROOM_NAMESPACE, key.as_bytes())
}
pub fn room_to_uuid(room: &RoomType) -> Uuid {
fold_debug_repr(&format!("{room:?}"))
}
pub fn fold_debug_repr(repr: &str) -> Uuid {
let mut bytes = [0u8; 16];
for (i, b) in repr.bytes().enumerate() {
bytes[i % 16] ^= b.wrapping_add(i as u8);
}
Uuid::from_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_key_is_case_insensitive() {
let a = canonical_room_key(DEFAULT_WING_ID, "Decisions");
let b = canonical_room_key(DEFAULT_WING_ID, " decisions ");
assert_eq!(a, b);
assert!(a.starts_with(&DEFAULT_WING_ID.to_string()));
assert!(a.ends_with("decisions"));
}
#[test]
fn canonical_key_separates_wing_from_label() {
let other = Uuid::from_u128(1);
assert_ne!(
canonical_room_key(DEFAULT_WING_ID, "planning"),
canonical_room_key(other, "planning")
);
}
#[test]
fn room_namespace_matches_its_documented_derivation() {
let ns = Uuid::new_v5(
&Uuid::NAMESPACE_URL,
b"https://github.com/bobmatnyc/trusty-tools/adr-0027/room-namespace",
);
assert_eq!(ROOM_NAMESPACE, ns, "ROOM_NAMESPACE drifted from its doc");
assert_eq!(
DEFAULT_WING_ID,
Uuid::new_v5(&ns, b"default-wing"),
"DEFAULT_WING_ID drifted from its doc"
);
}
#[test]
fn mint_room_id_is_stable() {
let key = canonical_room_key(DEFAULT_WING_ID, "Planning");
let a = mint_room_id(&key);
let b = mint_room_id(&key);
assert_eq!(a, b, "minting must be deterministic across calls");
assert_eq!(a.get_version_num(), 5, "ids are UUIDv5");
assert_ne!(a, Uuid::nil());
}
#[test]
fn mint_room_id_avoids_fold_collisions() {
let bodies: Vec<String> = ('a'..='g')
.map(|c| format!("{c}bcdefghijklmnop{c}rst"))
.collect();
assert_eq!(bodies.len(), 7);
let ids: std::collections::HashSet<Uuid> = bodies
.iter()
.map(|b| mint_room_id(&canonical_room_key(DEFAULT_WING_ID, b)))
.collect();
assert_eq!(ids.len(), bodies.len(), "UUIDv5 must not collide here");
let folded: std::collections::HashSet<Uuid> = bodies
.iter()
.map(|b| room_to_uuid(&RoomType::Custom(b.clone())))
.collect();
assert_eq!(folded.len(), 1, "legacy fold collapses all seven");
}
#[test]
fn fold_matches_room_to_uuid() {
for room in [RoomType::General, RoomType::Custom("work".to_string())] {
assert_eq!(room_to_uuid(&room), fold_debug_repr(&format!("{room:?}")));
}
}
#[test]
fn legacy_fold_matches_live_palace_ids() {
assert_eq!(
room_to_uuid(&RoomType::General).to_string(),
"47667068-7666-7200-0000-000000000000"
);
assert_eq!(
room_to_uuid(&RoomType::Custom("work".to_string())).to_string(),
"43767577-7372-2e29-7f78-7c762e360000"
);
assert_eq!(
room_to_uuid(&RoomType::Custom("status".to_string())).to_string(),
"43767577-7372-2e29-7b7d-6b7f81803038"
);
}
#[test]
fn parse_preserving_case_keeps_custom_spelling() {
assert_eq!(
parse_room_preserving_case(" Sprint Notes "),
RoomType::Custom("Sprint Notes".to_string())
);
assert_eq!(
canonical_room_key(DEFAULT_WING_ID, "Sprint Notes"),
canonical_room_key(DEFAULT_WING_ID, "sprint notes")
);
assert_eq!(parse_room_preserving_case("docs"), RoomType::Documentation);
assert_eq!(parse_room_preserving_case("BACKEND"), RoomType::Backend);
}
#[test]
fn room_type_parts_round_trip() {
let cases = [
RoomType::Frontend,
RoomType::Backend,
RoomType::Testing,
RoomType::Planning,
RoomType::Documentation,
RoomType::Research,
RoomType::Configuration,
RoomType::Meetings,
RoomType::General,
RoomType::Custom("status".to_string()),
];
for room in cases {
let tag = room_type_tag(&room);
let label = room_label(&room);
assert_eq!(room_type_from_parts(tag, &label), room, "{room:?}");
}
}
#[test]
fn unknown_tag_degrades_to_custom() {
assert_eq!(
room_type_from_parts("Kitchen", "kitchen"),
RoomType::Custom("kitchen".to_string())
);
}
}