source_boundary.rs (9985B)
1 use std::fs; 2 use std::path::{Path, PathBuf}; 3 4 struct ForbiddenEventName { 5 pattern: &'static str, 6 reason: &'static str, 7 } 8 9 const FORBIDDEN_EVENT_NAMES: &[ForbiddenEventName] = &[ 10 ForbiddenEventName { 11 pattern: "RadrootsPublicKey", 12 reason: "public author keys must use radroots_identity::PublicKey", 13 }, 14 ForbiddenEventName { 15 pattern: "pub fn from_wire_unchecked", 16 reason: "unchecked signed-event construction must not be public API", 17 }, 18 ForbiddenEventName { 19 pattern: "WireEventParts", 20 reason: "event construction must use Nip01EventWireParts", 21 }, 22 ForbiddenEventName { 23 pattern: "RadrootsFrozenEventDraft", 24 reason: "generic event construction must use GenericEventDraft", 25 }, 26 ForbiddenEventName { 27 pattern: "RadrootsNostrEvent", 28 reason: "product-level event surfaces must use protocol-neutral domain names", 29 }, 30 ForbiddenEventName { 31 pattern: "RadrootsNostrEventRef", 32 reason: "product-level event references must use EventRef", 33 }, 34 ForbiddenEventName { 35 pattern: "RadrootsNostrEventPtr", 36 reason: "product-level event pointers must use EventPtr", 37 }, 38 ForbiddenEventName { 39 pattern: "RadrootsSignedNostrEvent", 40 reason: "signed-event surfaces must use SignedEvent", 41 }, 42 ForbiddenEventName { 43 pattern: "RadrootsSignedNostrEventParts", 44 reason: "signed-event parts must use protocol-neutral domain names", 45 }, 46 ForbiddenEventName { 47 pattern: "KIND_TRADE_VALIDATION_RECEIPT", 48 reason: "the prototype receipt kind is retired in favor of typed RHI attestations", 49 }, 50 ForbiddenEventName { 51 pattern: "TRADE_VALIDATION_EVENT_KINDS", 52 reason: "the prototype receipt family is retired", 53 }, 54 ForbiddenEventName { 55 pattern: concat!("is_trade_validation_", "receipt_kind"), 56 reason: "the prototype receipt classifier is retired", 57 }, 58 ForbiddenEventName { 59 pattern: "is_trade_validation_event_kind", 60 reason: "the prototype validation-event classifier is retired", 61 }, 62 ForbiddenEventName { 63 pattern: "is_trade_validation_service_event_kind", 64 reason: "the unused prototype service-event classifier is retired", 65 }, 66 ForbiddenEventName { 67 pattern: "is_trade_validation_service_request_kind", 68 reason: "the unused prototype service-request classifier is retired", 69 }, 70 ForbiddenEventName { 71 pattern: "is_trade_validation_service_result_kind", 72 reason: "the unused prototype service-result classifier is retired", 73 }, 74 ForbiddenEventName { 75 pattern: "trade_validation_service_result_kind_for_request", 76 reason: "the unused prototype service-kind mapper is retired", 77 }, 78 ForbiddenEventName { 79 pattern: "trade_validation_service_request_kind_for_result", 80 reason: "the unused prototype service-kind mapper is retired", 81 }, 82 ForbiddenEventName { 83 pattern: "RadrootsTradeValidationReceipt", 84 reason: "the prototype receipt DTO is retired in favor of the final evidence report", 85 }, 86 ForbiddenEventName { 87 pattern: "radroots.trade.validation_receipt.v1", 88 reason: "the prototype receipt contract is retired", 89 }, 90 ]; 91 92 const RETIRED_EVENT_MODULE_PATHS: &[&str] = &[ 93 "radroots_event::event_head", 94 "radroots_event::events", 95 "radroots_event::ids", 96 "radroots_event::kinds", 97 ]; 98 99 #[test] 100 fn generic_event_source_does_not_reintroduce_old_core_nostr_event_names() { 101 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) 102 .parent() 103 .and_then(Path::parent) 104 .expect("lib repo root"); 105 let mut findings = Vec::new(); 106 107 for path in source_boundary_guard_files(repo_root) { 108 let relative_path = relative_path(repo_root, path.as_path()); 109 let source = fs::read_to_string(path.as_path()).expect("read source file"); 110 for forbidden in FORBIDDEN_EVENT_NAMES { 111 if forbidden_event_name_allowed(relative_path.as_str(), forbidden.pattern) { 112 continue; 113 } 114 if contains_forbidden_event_name(source.as_str(), forbidden.pattern) { 115 findings.push(format!( 116 "{} contains retired event concept `{}`: {}", 117 relative_path, forbidden.pattern, forbidden.reason 118 )); 119 } 120 } 121 } 122 123 assert!( 124 findings.is_empty(), 125 "radroots_event source-boundary violations:\n{}", 126 findings.join("\n") 127 ); 128 } 129 130 #[test] 131 fn first_party_sources_do_not_reintroduce_retired_event_module_paths() { 132 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) 133 .parent() 134 .and_then(Path::parent) 135 .expect("lib repo root"); 136 let mut findings = Vec::new(); 137 138 for path in source_boundary_guard_files(repo_root) { 139 let relative_path = relative_path(repo_root, path.as_path()); 140 let source = fs::read_to_string(path.as_path()).expect("read source file"); 141 for retired_path in RETIRED_EVENT_MODULE_PATHS { 142 if source.contains(retired_path) { 143 findings.push(format!( 144 "{relative_path} contains retired event module path `{retired_path}`" 145 )); 146 } 147 } 148 } 149 150 assert!( 151 findings.is_empty(), 152 "retired radroots_event module paths remain:\n{}", 153 findings.join("\n") 154 ); 155 } 156 157 #[test] 158 fn public_api_has_no_redundant_radroots_type_prefixes() { 159 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) 160 .parent() 161 .and_then(Path::parent) 162 .expect("lib repo root"); 163 let baseline = fs::read_to_string(repo_root.join("contracts/api_baselines/radroots_event.txt")) 164 .expect("read radroots_event public API baseline"); 165 let mut prefixed = baseline 166 .split(|character: char| !is_identifier_character(character)) 167 .filter(|identifier| { 168 identifier 169 .strip_prefix("Radroots") 170 .and_then(|suffix| suffix.chars().next()) 171 .is_some_and(|character| character.is_ascii_uppercase()) 172 }) 173 .collect::<Vec<_>>(); 174 prefixed.sort_unstable(); 175 prefixed.dedup(); 176 177 assert!( 178 prefixed.is_empty(), 179 "radroots_event public API contains redundant prefixed identifiers: {}", 180 prefixed.join(", ") 181 ); 182 } 183 184 #[test] 185 fn typed_authoring_mapping_is_not_implemented_in_the_generic_event_draft() { 186 let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) 187 .parent() 188 .and_then(Path::parent) 189 .expect("lib repo root"); 190 let draft = fs::read_to_string(repo_root.join("crates/event/src/draft.rs")) 191 .expect("read generic event draft source"); 192 193 for retired in [ 194 "from_authored_update", 195 "from_authored_reply", 196 "from_authored_profile", 197 "from_typed_parts", 198 "TypedAuthoringKind", 199 "typed_authoring", 200 ] { 201 assert!( 202 !draft.contains(retired), 203 "generic authored input retains codec-owned typed mapping `{retired}`" 204 ); 205 } 206 } 207 208 fn source_boundary_guard_files(repo_root: &Path) -> Vec<PathBuf> { 209 let mut paths = Vec::new(); 210 for relative_root in ["crates", "contracts"] { 211 let root = repo_root.join(relative_root); 212 if root.exists() { 213 collect_source_boundary_guard_files(root.as_path(), &mut paths); 214 } 215 } 216 paths.sort(); 217 paths 218 } 219 220 fn collect_source_boundary_guard_files(root: &Path, paths: &mut Vec<PathBuf>) { 221 for entry in fs::read_dir(root).expect("read source directory") { 222 let entry = entry.expect("source entry"); 223 let path = entry.path(); 224 if path.is_dir() { 225 if matches!( 226 path.file_name().and_then(|file_name| file_name.to_str()), 227 Some("generated" | "target") 228 ) { 229 continue; 230 } 231 collect_source_boundary_guard_files(path.as_path(), paths); 232 continue; 233 } 234 235 if path.file_name().and_then(|file_name| file_name.to_str()) == Some("source_boundary.rs") { 236 continue; 237 } 238 239 if path.ends_with("event_store/src/nip09/reconciliation_v1/result_vector_executor.rs") { 240 // This predecessor is an authenticated immutable artifact. Active NIP-09 241 // execution uses the maintained successor surface; the frozen source must 242 // retain its historical bytes so its provenance check remains meaningful. 243 continue; 244 } 245 246 if matches!( 247 path.extension().and_then(|extension| extension.to_str()), 248 Some("rs") | Some("toml") | Some("md") 249 ) { 250 paths.push(path); 251 } 252 } 253 } 254 255 fn forbidden_event_name_allowed(relative_path: &str, pattern: &str) -> bool { 256 pattern == "RadrootsNostrEvent" && is_nostr_protocol_context(relative_path) 257 } 258 259 fn is_nostr_protocol_context(relative_path: &str) -> bool { 260 relative_path.starts_with("crates/nostr/") 261 || relative_path.starts_with("crates/nostrdb/") 262 || relative_path.starts_with("crates/nostr_signer/") 263 || relative_path.starts_with("crates/transport_nostr/") 264 } 265 266 fn contains_forbidden_event_name(source: &str, pattern: &str) -> bool { 267 source.match_indices(pattern).any(|(index, _)| { 268 let before = source[..index].chars().next_back(); 269 let after = source[index + pattern.len()..].chars().next(); 270 before.is_none_or(|character| !is_identifier_character(character)) 271 && after.is_none_or(|character| !is_identifier_character(character)) 272 }) 273 } 274 275 fn is_identifier_character(character: char) -> bool { 276 character == '_' || character.is_ascii_alphanumeric() 277 } 278 279 fn relative_path(root: &Path, path: &Path) -> String { 280 path.strip_prefix(root) 281 .expect("source path is under repo root") 282 .to_string_lossy() 283 .replace('\\', "/") 284 }