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_rule_metadata(id: &str) -> Option<(&'static str, &'static str, &'static str)> {
87 RULES_METADATA
88 .iter()
89 .find(|(rule_id, _, _, _)| *rule_id == id)
90 .map(|(_, category, severity, tool)| (*category, *severity, *tool))
91}
92
93pub fn get_tool_for_prefix(prefix: &str) -> Option<&'static str> {
109 TOOL_RULE_PREFIXES
110 .iter()
111 .find(|(p, _)| *p == prefix)
112 .map(|(_, tool)| *tool)
113}
114
115pub fn get_prefixes_for_tool(tool: &str) -> Vec<&'static str> {
126 TOOL_RULE_PREFIXES
127 .iter()
128 .filter(|(_, t)| t.eq_ignore_ascii_case(tool))
129 .map(|(prefix, _)| *prefix)
130 .collect()
131}
132
133pub fn is_valid_tool(tool: &str) -> bool {
137 VALID_TOOLS.iter().any(|t| t.eq_ignore_ascii_case(tool))
138}
139
140pub fn normalize_tool_name(tool: &str) -> Option<&'static str> {
154 VALID_TOOLS
155 .iter()
156 .find(|t| t.eq_ignore_ascii_case(tool))
157 .copied()
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 #[allow(clippy::const_is_empty)]
166 fn test_rules_data_not_empty() {
167 assert!(!RULES_DATA.is_empty(), "RULES_DATA should not be empty");
168 }
169
170 #[test]
171 fn test_rule_count() {
172 assert_eq!(rule_count(), RULES_DATA.len());
173 }
174
175 #[test]
176 fn test_get_rule_name_exists() {
177 let name = get_rule_name("AS-001");
179 assert!(name.is_some(), "AS-001 should exist");
180 }
181
182 #[test]
183 fn test_get_rule_name_not_exists() {
184 let name = get_rule_name("NONEXISTENT-999");
185 assert!(name.is_none(), "Nonexistent rule should return None");
186 }
187
188 #[test]
189 fn test_no_duplicate_ids() {
190 let mut ids: Vec<&str> = RULES_DATA.iter().map(|(id, _)| *id).collect();
191 let original_len = ids.len();
192 ids.sort();
193 ids.dedup();
194 assert_eq!(ids.len(), original_len, "Should have no duplicate rule IDs");
195 }
196
197 #[test]
200 #[allow(clippy::const_is_empty)]
201 fn test_rules_metadata_not_empty() {
202 assert!(
203 !RULES_METADATA.is_empty(),
204 "RULES_METADATA should not be empty"
205 );
206 }
207
208 #[test]
209 fn test_rules_metadata_same_length_as_rules_data() {
210 assert_eq!(
211 RULES_METADATA.len(),
212 RULES_DATA.len(),
213 "RULES_METADATA and RULES_DATA should have the same number of entries"
214 );
215 }
216
217 #[test]
218 fn test_get_rule_metadata_as_001() {
219 let meta = get_rule_metadata("AS-001");
220 assert!(meta.is_some(), "AS-001 should have metadata");
221 let (category, severity, _tool) = meta.unwrap();
222 assert_eq!(category, "agent-skills");
223 assert_eq!(severity, "HIGH");
224 }
225
226 #[test]
227 fn test_get_rule_metadata_cc_hk_001() {
228 let meta = get_rule_metadata("CC-HK-001");
229 assert!(meta.is_some(), "CC-HK-001 should have metadata");
230 let (category, severity, tool) = meta.unwrap();
231 assert!(!category.is_empty(), "category should not be empty");
232 assert!(!severity.is_empty(), "severity should not be empty");
233 assert_eq!(tool, "claude-code");
234 }
235
236 #[test]
237 fn test_get_rule_metadata_nonexistent() {
238 let meta = get_rule_metadata("NONEXISTENT-999");
239 assert!(meta.is_none(), "Nonexistent rule should return None");
240 }
241
242 #[test]
243 fn test_get_rule_metadata_tool_may_be_empty() {
244 let meta = get_rule_metadata("AS-001");
246 assert!(meta.is_some());
247 let (_category, _severity, tool) = meta.unwrap();
248 assert_eq!(tool, "", "AS-001 should have empty tool (generic rule)");
250 }
251
252 #[test]
255 #[allow(clippy::const_is_empty)]
256 fn test_valid_tools_not_empty() {
257 assert!(!VALID_TOOLS.is_empty(), "VALID_TOOLS should not be empty");
258 }
259
260 #[test]
261 fn test_valid_tools_contains_claude_code() {
262 assert!(
263 VALID_TOOLS.contains(&"claude-code"),
264 "VALID_TOOLS should contain 'claude-code'"
265 );
266 }
267
268 #[test]
269 fn test_valid_tools_contains_github_copilot() {
270 assert!(
271 VALID_TOOLS.contains(&"github-copilot"),
272 "VALID_TOOLS should contain 'github-copilot'"
273 );
274 }
275
276 #[test]
277 fn test_valid_tools_contains_cursor() {
278 assert!(
279 VALID_TOOLS.contains(&"cursor"),
280 "VALID_TOOLS should contain 'cursor'"
281 );
282 }
283
284 #[test]
285 fn test_valid_tools_helper() {
286 let tools = valid_tools();
287 assert!(!tools.is_empty());
288 assert!(tools.contains(&"claude-code"));
289 }
290
291 #[test]
294 #[allow(clippy::const_is_empty)]
295 fn test_authoring_families_not_empty() {
296 assert!(
297 !AUTHORING_FAMILIES.is_empty(),
298 "AUTHORING_FAMILIES should not be empty"
299 );
300 }
301
302 #[test]
303 fn test_authoring_families_contains_core_families() {
304 let families = authoring_families();
305 assert!(families.contains(&"skill"));
306 assert!(families.contains(&"agent"));
307 assert!(families.contains(&"hooks"));
308 assert!(families.contains(&"mcp"));
309 }
310
311 #[test]
312 fn test_authoring_catalog_json_is_valid_json() {
313 let parsed: serde_json::Value = serde_json::from_str(authoring_catalog_json())
314 .expect("AUTHORING_CATALOG_JSON should be valid JSON");
315 assert!(
316 parsed.is_object(),
317 "authoring catalog should be a JSON object"
318 );
319 }
320
321 #[test]
324 #[allow(clippy::const_is_empty)]
325 fn test_tool_rule_prefixes_not_empty() {
326 assert!(
327 !TOOL_RULE_PREFIXES.is_empty(),
328 "TOOL_RULE_PREFIXES should not be empty"
329 );
330 }
331
332 #[test]
333 fn test_tool_rule_prefixes_cc_hk() {
334 let found = TOOL_RULE_PREFIXES
336 .iter()
337 .find(|(prefix, _)| *prefix == "CC-HK-");
338 assert!(found.is_some(), "Should have CC-HK- prefix");
339 assert_eq!(found.unwrap().1, "claude-code");
340 }
341
342 #[test]
343 fn test_tool_rule_prefixes_cop() {
344 let found = TOOL_RULE_PREFIXES
346 .iter()
347 .find(|(prefix, _)| *prefix == "COP-");
348 assert!(found.is_some(), "Should have COP- prefix");
349 assert_eq!(found.unwrap().1, "github-copilot");
350 }
351
352 #[test]
353 fn test_tool_rule_prefixes_cur() {
354 let found = TOOL_RULE_PREFIXES
356 .iter()
357 .find(|(prefix, _)| *prefix == "CUR-");
358 assert!(found.is_some(), "Should have CUR- prefix");
359 assert_eq!(found.unwrap().1, "cursor");
360 }
361
362 #[test]
363 fn test_get_tool_for_prefix_claude_code() {
364 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
365 assert_eq!(get_tool_for_prefix("CC-SK-"), Some("claude-code"));
366 assert_eq!(get_tool_for_prefix("CC-AG-"), Some("claude-code"));
367 assert_eq!(get_tool_for_prefix("CC-PL-"), Some("claude-code"));
368 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
371 }
372
373 #[test]
374 fn test_get_tool_for_prefix_copilot() {
375 assert_eq!(get_tool_for_prefix("COP-"), Some("github-copilot"));
376 }
377
378 #[test]
379 fn test_get_tool_for_prefix_cursor() {
380 assert_eq!(get_tool_for_prefix("CUR-"), Some("cursor"));
381 }
382
383 #[test]
384 fn test_get_tool_for_prefix_generic() {
385 assert_eq!(get_tool_for_prefix("MCP-"), None);
388 assert_eq!(get_tool_for_prefix("XML-"), None);
389 assert_eq!(get_tool_for_prefix("XP-"), None);
390 }
394
395 #[test]
396 fn test_get_tool_for_prefix_unknown() {
397 assert_eq!(get_tool_for_prefix("UNKNOWN-"), None);
398 }
399
400 #[test]
403 fn test_mixed_tool_prefix_as() {
404 assert_eq!(get_tool_for_prefix("AS-"), None);
407 }
408
409 #[test]
410 fn test_mixed_tool_prefix_cc_mem() {
411 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
415 }
416
417 #[test]
418 fn test_consistent_tool_prefix_cc_hk() {
419 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
422 }
423
424 #[test]
425 fn test_get_prefixes_for_tool_claude_code() {
426 let prefixes = get_prefixes_for_tool("claude-code");
427 assert!(!prefixes.is_empty());
428 assert!(prefixes.contains(&"CC-HK-"));
429 assert!(prefixes.contains(&"CC-SK-"));
430 assert!(prefixes.contains(&"CC-AG-"));
431 assert!(prefixes.contains(&"CC-PL-"));
432 assert!(!prefixes.contains(&"CC-MEM-"));
435 }
436
437 #[test]
438 fn test_get_prefixes_for_tool_copilot() {
439 let prefixes = get_prefixes_for_tool("github-copilot");
440 assert!(!prefixes.is_empty());
441 assert!(prefixes.contains(&"COP-"));
442 }
443
444 #[test]
445 fn test_get_prefixes_for_tool_cursor() {
446 let prefixes = get_prefixes_for_tool("cursor");
447 assert!(!prefixes.is_empty());
448 assert!(prefixes.contains(&"CUR-"));
449 }
450
451 #[test]
452 fn test_get_prefixes_for_tool_unknown() {
453 let prefixes = get_prefixes_for_tool("unknown-tool");
454 assert!(prefixes.is_empty());
455 }
456
457 #[test]
460 fn test_is_valid_tool_claude_code() {
461 assert!(is_valid_tool("claude-code"));
462 assert!(is_valid_tool("Claude-Code")); assert!(is_valid_tool("CLAUDE-CODE")); }
465
466 #[test]
467 fn test_is_valid_tool_copilot() {
468 assert!(is_valid_tool("github-copilot"));
469 assert!(is_valid_tool("GitHub-Copilot")); }
471
472 #[test]
473 fn test_is_valid_tool_unknown() {
474 assert!(!is_valid_tool("unknown-tool"));
475 assert!(!is_valid_tool(""));
476 }
477
478 #[test]
481 fn test_normalize_tool_name_claude_code() {
482 assert_eq!(normalize_tool_name("claude-code"), Some("claude-code"));
483 assert_eq!(normalize_tool_name("Claude-Code"), Some("claude-code"));
484 assert_eq!(normalize_tool_name("CLAUDE-CODE"), Some("claude-code"));
485 }
486
487 #[test]
488 fn test_normalize_tool_name_copilot() {
489 assert_eq!(
490 normalize_tool_name("github-copilot"),
491 Some("github-copilot")
492 );
493 assert_eq!(
494 normalize_tool_name("GitHub-Copilot"),
495 Some("github-copilot")
496 );
497 }
498
499 #[test]
500 fn test_normalize_tool_name_unknown() {
501 assert_eq!(normalize_tool_name("unknown-tool"), None);
502 assert_eq!(normalize_tool_name(""), None);
503 }
504
505 #[test]
508 fn test_get_prefixes_for_tool_empty_string() {
509 let prefixes = get_prefixes_for_tool("");
511 assert!(
512 prefixes.is_empty(),
513 "Empty string tool should return empty Vec"
514 );
515 }
516
517 #[test]
518 fn test_get_prefixes_for_tool_unknown_tool() {
519 let prefixes = get_prefixes_for_tool("nonexistent-tool");
521 assert!(prefixes.is_empty(), "Unknown tool should return empty Vec");
522 }
523
524 #[test]
525 fn test_get_prefixes_for_tool_claude_code_multiple_prefixes() {
526 let prefixes = get_prefixes_for_tool("claude-code");
528 assert!(
529 prefixes.len() > 1,
530 "claude-code should have multiple prefixes, got {}",
531 prefixes.len()
532 );
533 assert!(
535 prefixes.contains(&"CC-HK-"),
536 "claude-code prefixes should include CC-HK-"
537 );
538 assert!(
539 prefixes.contains(&"CC-SK-"),
540 "claude-code prefixes should include CC-SK-"
541 );
542 }
543
544 #[test]
547 fn test_get_tool_for_prefix_empty_string() {
548 assert_eq!(
550 get_tool_for_prefix(""),
551 None,
552 "Empty prefix should return None"
553 );
554 }
555
556 #[test]
557 fn test_get_tool_for_prefix_unknown_prefix() {
558 assert_eq!(
560 get_tool_for_prefix("NONEXISTENT-"),
561 None,
562 "Unknown prefix should return None"
563 );
564 assert_eq!(
565 get_tool_for_prefix("XX-"),
566 None,
567 "XX- prefix should return None"
568 );
569 }
570
571 #[test]
572 fn test_get_tool_for_prefix_partial_match_not_supported() {
573 assert_eq!(
575 get_tool_for_prefix("CC-"),
576 None,
577 "Partial prefix CC- (without HK/SK/AG) should not match"
578 );
579 assert_eq!(
580 get_tool_for_prefix("C"),
581 None,
582 "Single character should not match"
583 );
584 }
585}