1include!(concat!(env!("OUT_DIR"), "/rules_data.rs"));
40
41pub fn rule_count() -> usize {
43 RULES_DATA.len()
44}
45
46pub fn get_rule_name(id: &str) -> Option<&'static str> {
48 RULES_DATA
49 .iter()
50 .find(|(rule_id, _)| *rule_id == id)
51 .map(|(_, name)| *name)
52}
53
54pub fn valid_tools() -> &'static [&'static str] {
58 VALID_TOOLS
59}
60
61pub fn authoring_families() -> &'static [&'static str] {
63 AUTHORING_FAMILIES
64}
65
66pub fn authoring_catalog_json() -> &'static str {
68 AUTHORING_CATALOG_JSON
69}
70
71pub fn get_tool_for_prefix(prefix: &str) -> Option<&'static str> {
87 TOOL_RULE_PREFIXES
88 .iter()
89 .find(|(p, _)| *p == prefix)
90 .map(|(_, tool)| *tool)
91}
92
93pub fn get_prefixes_for_tool(tool: &str) -> Vec<&'static str> {
104 TOOL_RULE_PREFIXES
105 .iter()
106 .filter(|(_, t)| t.eq_ignore_ascii_case(tool))
107 .map(|(prefix, _)| *prefix)
108 .collect()
109}
110
111pub fn is_valid_tool(tool: &str) -> bool {
115 VALID_TOOLS.iter().any(|t| t.eq_ignore_ascii_case(tool))
116}
117
118pub fn normalize_tool_name(tool: &str) -> Option<&'static str> {
132 VALID_TOOLS
133 .iter()
134 .find(|t| t.eq_ignore_ascii_case(tool))
135 .copied()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 #[allow(clippy::const_is_empty)]
144 fn test_rules_data_not_empty() {
145 assert!(!RULES_DATA.is_empty(), "RULES_DATA should not be empty");
146 }
147
148 #[test]
149 fn test_rule_count() {
150 assert_eq!(rule_count(), RULES_DATA.len());
151 }
152
153 #[test]
154 fn test_get_rule_name_exists() {
155 let name = get_rule_name("AS-001");
157 assert!(name.is_some(), "AS-001 should exist");
158 }
159
160 #[test]
161 fn test_get_rule_name_not_exists() {
162 let name = get_rule_name("NONEXISTENT-999");
163 assert!(name.is_none(), "Nonexistent rule should return None");
164 }
165
166 #[test]
167 fn test_no_duplicate_ids() {
168 let mut ids: Vec<&str> = RULES_DATA.iter().map(|(id, _)| *id).collect();
169 let original_len = ids.len();
170 ids.sort();
171 ids.dedup();
172 assert_eq!(ids.len(), original_len, "Should have no duplicate rule IDs");
173 }
174
175 #[test]
178 #[allow(clippy::const_is_empty)]
179 fn test_valid_tools_not_empty() {
180 assert!(!VALID_TOOLS.is_empty(), "VALID_TOOLS should not be empty");
181 }
182
183 #[test]
184 fn test_valid_tools_contains_claude_code() {
185 assert!(
186 VALID_TOOLS.contains(&"claude-code"),
187 "VALID_TOOLS should contain 'claude-code'"
188 );
189 }
190
191 #[test]
192 fn test_valid_tools_contains_github_copilot() {
193 assert!(
194 VALID_TOOLS.contains(&"github-copilot"),
195 "VALID_TOOLS should contain 'github-copilot'"
196 );
197 }
198
199 #[test]
200 fn test_valid_tools_contains_cursor() {
201 assert!(
202 VALID_TOOLS.contains(&"cursor"),
203 "VALID_TOOLS should contain 'cursor'"
204 );
205 }
206
207 #[test]
208 fn test_valid_tools_helper() {
209 let tools = valid_tools();
210 assert!(!tools.is_empty());
211 assert!(tools.contains(&"claude-code"));
212 }
213
214 #[test]
217 #[allow(clippy::const_is_empty)]
218 fn test_authoring_families_not_empty() {
219 assert!(
220 !AUTHORING_FAMILIES.is_empty(),
221 "AUTHORING_FAMILIES should not be empty"
222 );
223 }
224
225 #[test]
226 fn test_authoring_families_contains_core_families() {
227 let families = authoring_families();
228 assert!(families.contains(&"skill"));
229 assert!(families.contains(&"agent"));
230 assert!(families.contains(&"hooks"));
231 assert!(families.contains(&"mcp"));
232 }
233
234 #[test]
235 fn test_authoring_catalog_json_is_valid_json() {
236 let parsed: serde_json::Value = serde_json::from_str(authoring_catalog_json())
237 .expect("AUTHORING_CATALOG_JSON should be valid JSON");
238 assert!(
239 parsed.is_object(),
240 "authoring catalog should be a JSON object"
241 );
242 }
243
244 #[test]
247 #[allow(clippy::const_is_empty)]
248 fn test_tool_rule_prefixes_not_empty() {
249 assert!(
250 !TOOL_RULE_PREFIXES.is_empty(),
251 "TOOL_RULE_PREFIXES should not be empty"
252 );
253 }
254
255 #[test]
256 fn test_tool_rule_prefixes_cc_hk() {
257 let found = TOOL_RULE_PREFIXES
259 .iter()
260 .find(|(prefix, _)| *prefix == "CC-HK-");
261 assert!(found.is_some(), "Should have CC-HK- prefix");
262 assert_eq!(found.unwrap().1, "claude-code");
263 }
264
265 #[test]
266 fn test_tool_rule_prefixes_cop() {
267 let found = TOOL_RULE_PREFIXES
269 .iter()
270 .find(|(prefix, _)| *prefix == "COP-");
271 assert!(found.is_some(), "Should have COP- prefix");
272 assert_eq!(found.unwrap().1, "github-copilot");
273 }
274
275 #[test]
276 fn test_tool_rule_prefixes_cur() {
277 let found = TOOL_RULE_PREFIXES
279 .iter()
280 .find(|(prefix, _)| *prefix == "CUR-");
281 assert!(found.is_some(), "Should have CUR- prefix");
282 assert_eq!(found.unwrap().1, "cursor");
283 }
284
285 #[test]
286 fn test_get_tool_for_prefix_claude_code() {
287 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
288 assert_eq!(get_tool_for_prefix("CC-SK-"), Some("claude-code"));
289 assert_eq!(get_tool_for_prefix("CC-AG-"), Some("claude-code"));
290 assert_eq!(get_tool_for_prefix("CC-PL-"), Some("claude-code"));
291 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
294 }
295
296 #[test]
297 fn test_get_tool_for_prefix_copilot() {
298 assert_eq!(get_tool_for_prefix("COP-"), Some("github-copilot"));
299 }
300
301 #[test]
302 fn test_get_tool_for_prefix_cursor() {
303 assert_eq!(get_tool_for_prefix("CUR-"), Some("cursor"));
304 }
305
306 #[test]
307 fn test_get_tool_for_prefix_generic() {
308 assert_eq!(get_tool_for_prefix("MCP-"), None);
311 assert_eq!(get_tool_for_prefix("XML-"), None);
312 assert_eq!(get_tool_for_prefix("XP-"), None);
313 }
317
318 #[test]
319 fn test_get_tool_for_prefix_unknown() {
320 assert_eq!(get_tool_for_prefix("UNKNOWN-"), None);
321 }
322
323 #[test]
326 fn test_mixed_tool_prefix_as() {
327 assert_eq!(get_tool_for_prefix("AS-"), None);
330 }
331
332 #[test]
333 fn test_mixed_tool_prefix_cc_mem() {
334 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
338 }
339
340 #[test]
341 fn test_consistent_tool_prefix_cc_hk() {
342 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
345 }
346
347 #[test]
348 fn test_get_prefixes_for_tool_claude_code() {
349 let prefixes = get_prefixes_for_tool("claude-code");
350 assert!(!prefixes.is_empty());
351 assert!(prefixes.contains(&"CC-HK-"));
352 assert!(prefixes.contains(&"CC-SK-"));
353 assert!(prefixes.contains(&"CC-AG-"));
354 assert!(prefixes.contains(&"CC-PL-"));
355 assert!(!prefixes.contains(&"CC-MEM-"));
358 }
359
360 #[test]
361 fn test_get_prefixes_for_tool_copilot() {
362 let prefixes = get_prefixes_for_tool("github-copilot");
363 assert!(!prefixes.is_empty());
364 assert!(prefixes.contains(&"COP-"));
365 }
366
367 #[test]
368 fn test_get_prefixes_for_tool_cursor() {
369 let prefixes = get_prefixes_for_tool("cursor");
370 assert!(!prefixes.is_empty());
371 assert!(prefixes.contains(&"CUR-"));
372 }
373
374 #[test]
375 fn test_get_prefixes_for_tool_unknown() {
376 let prefixes = get_prefixes_for_tool("unknown-tool");
377 assert!(prefixes.is_empty());
378 }
379
380 #[test]
383 fn test_is_valid_tool_claude_code() {
384 assert!(is_valid_tool("claude-code"));
385 assert!(is_valid_tool("Claude-Code")); assert!(is_valid_tool("CLAUDE-CODE")); }
388
389 #[test]
390 fn test_is_valid_tool_copilot() {
391 assert!(is_valid_tool("github-copilot"));
392 assert!(is_valid_tool("GitHub-Copilot")); }
394
395 #[test]
396 fn test_is_valid_tool_unknown() {
397 assert!(!is_valid_tool("unknown-tool"));
398 assert!(!is_valid_tool(""));
399 }
400
401 #[test]
404 fn test_normalize_tool_name_claude_code() {
405 assert_eq!(normalize_tool_name("claude-code"), Some("claude-code"));
406 assert_eq!(normalize_tool_name("Claude-Code"), Some("claude-code"));
407 assert_eq!(normalize_tool_name("CLAUDE-CODE"), Some("claude-code"));
408 }
409
410 #[test]
411 fn test_normalize_tool_name_copilot() {
412 assert_eq!(
413 normalize_tool_name("github-copilot"),
414 Some("github-copilot")
415 );
416 assert_eq!(
417 normalize_tool_name("GitHub-Copilot"),
418 Some("github-copilot")
419 );
420 }
421
422 #[test]
423 fn test_normalize_tool_name_unknown() {
424 assert_eq!(normalize_tool_name("unknown-tool"), None);
425 assert_eq!(normalize_tool_name(""), None);
426 }
427
428 #[test]
431 fn test_get_prefixes_for_tool_empty_string() {
432 let prefixes = get_prefixes_for_tool("");
434 assert!(
435 prefixes.is_empty(),
436 "Empty string tool should return empty Vec"
437 );
438 }
439
440 #[test]
441 fn test_get_prefixes_for_tool_unknown_tool() {
442 let prefixes = get_prefixes_for_tool("nonexistent-tool");
444 assert!(prefixes.is_empty(), "Unknown tool should return empty Vec");
445 }
446
447 #[test]
448 fn test_get_prefixes_for_tool_claude_code_multiple_prefixes() {
449 let prefixes = get_prefixes_for_tool("claude-code");
451 assert!(
452 prefixes.len() > 1,
453 "claude-code should have multiple prefixes, got {}",
454 prefixes.len()
455 );
456 assert!(
458 prefixes.contains(&"CC-HK-"),
459 "claude-code prefixes should include CC-HK-"
460 );
461 assert!(
462 prefixes.contains(&"CC-SK-"),
463 "claude-code prefixes should include CC-SK-"
464 );
465 }
466
467 #[test]
470 fn test_get_tool_for_prefix_empty_string() {
471 assert_eq!(
473 get_tool_for_prefix(""),
474 None,
475 "Empty prefix should return None"
476 );
477 }
478
479 #[test]
480 fn test_get_tool_for_prefix_unknown_prefix() {
481 assert_eq!(
483 get_tool_for_prefix("NONEXISTENT-"),
484 None,
485 "Unknown prefix should return None"
486 );
487 assert_eq!(
488 get_tool_for_prefix("XX-"),
489 None,
490 "XX- prefix should return None"
491 );
492 }
493
494 #[test]
495 fn test_get_tool_for_prefix_partial_match_not_supported() {
496 assert_eq!(
498 get_tool_for_prefix("CC-"),
499 None,
500 "Partial prefix CC- (without HK/SK/AG) should not match"
501 );
502 assert_eq!(
503 get_tool_for_prefix("C"),
504 None,
505 "Single character should not match"
506 );
507 }
508}