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 fn test_authoring_families_not_empty() {
218 assert!(
219 !AUTHORING_FAMILIES.is_empty(),
220 "AUTHORING_FAMILIES should not be empty"
221 );
222 }
223
224 #[test]
225 fn test_authoring_families_contains_core_families() {
226 let families = authoring_families();
227 assert!(families.contains(&"skill"));
228 assert!(families.contains(&"agent"));
229 assert!(families.contains(&"hooks"));
230 assert!(families.contains(&"mcp"));
231 }
232
233 #[test]
234 fn test_authoring_catalog_json_is_valid_json() {
235 let parsed: serde_json::Value = serde_json::from_str(authoring_catalog_json())
236 .expect("AUTHORING_CATALOG_JSON should be valid JSON");
237 assert!(
238 parsed.is_object(),
239 "authoring catalog should be a JSON object"
240 );
241 }
242
243 #[test]
246 #[allow(clippy::const_is_empty)]
247 fn test_tool_rule_prefixes_not_empty() {
248 assert!(
249 !TOOL_RULE_PREFIXES.is_empty(),
250 "TOOL_RULE_PREFIXES should not be empty"
251 );
252 }
253
254 #[test]
255 fn test_tool_rule_prefixes_cc_hk() {
256 let found = TOOL_RULE_PREFIXES
258 .iter()
259 .find(|(prefix, _)| *prefix == "CC-HK-");
260 assert!(found.is_some(), "Should have CC-HK- prefix");
261 assert_eq!(found.unwrap().1, "claude-code");
262 }
263
264 #[test]
265 fn test_tool_rule_prefixes_cop() {
266 let found = TOOL_RULE_PREFIXES
268 .iter()
269 .find(|(prefix, _)| *prefix == "COP-");
270 assert!(found.is_some(), "Should have COP- prefix");
271 assert_eq!(found.unwrap().1, "github-copilot");
272 }
273
274 #[test]
275 fn test_tool_rule_prefixes_cur() {
276 let found = TOOL_RULE_PREFIXES
278 .iter()
279 .find(|(prefix, _)| *prefix == "CUR-");
280 assert!(found.is_some(), "Should have CUR- prefix");
281 assert_eq!(found.unwrap().1, "cursor");
282 }
283
284 #[test]
285 fn test_get_tool_for_prefix_claude_code() {
286 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
287 assert_eq!(get_tool_for_prefix("CC-SK-"), Some("claude-code"));
288 assert_eq!(get_tool_for_prefix("CC-AG-"), Some("claude-code"));
289 assert_eq!(get_tool_for_prefix("CC-PL-"), Some("claude-code"));
290 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
293 }
294
295 #[test]
296 fn test_get_tool_for_prefix_copilot() {
297 assert_eq!(get_tool_for_prefix("COP-"), Some("github-copilot"));
298 }
299
300 #[test]
301 fn test_get_tool_for_prefix_cursor() {
302 assert_eq!(get_tool_for_prefix("CUR-"), Some("cursor"));
303 }
304
305 #[test]
306 fn test_get_tool_for_prefix_generic() {
307 assert_eq!(get_tool_for_prefix("MCP-"), None);
310 assert_eq!(get_tool_for_prefix("XML-"), None);
311 assert_eq!(get_tool_for_prefix("XP-"), None);
312 }
316
317 #[test]
318 fn test_get_tool_for_prefix_unknown() {
319 assert_eq!(get_tool_for_prefix("UNKNOWN-"), None);
320 }
321
322 #[test]
325 fn test_mixed_tool_prefix_as() {
326 assert_eq!(get_tool_for_prefix("AS-"), None);
329 }
330
331 #[test]
332 fn test_mixed_tool_prefix_cc_mem() {
333 assert_eq!(get_tool_for_prefix("CC-MEM-"), None);
337 }
338
339 #[test]
340 fn test_consistent_tool_prefix_cc_hk() {
341 assert_eq!(get_tool_for_prefix("CC-HK-"), Some("claude-code"));
344 }
345
346 #[test]
347 fn test_get_prefixes_for_tool_claude_code() {
348 let prefixes = get_prefixes_for_tool("claude-code");
349 assert!(!prefixes.is_empty());
350 assert!(prefixes.contains(&"CC-HK-"));
351 assert!(prefixes.contains(&"CC-SK-"));
352 assert!(prefixes.contains(&"CC-AG-"));
353 assert!(prefixes.contains(&"CC-PL-"));
354 assert!(!prefixes.contains(&"CC-MEM-"));
357 }
358
359 #[test]
360 fn test_get_prefixes_for_tool_copilot() {
361 let prefixes = get_prefixes_for_tool("github-copilot");
362 assert!(!prefixes.is_empty());
363 assert!(prefixes.contains(&"COP-"));
364 }
365
366 #[test]
367 fn test_get_prefixes_for_tool_cursor() {
368 let prefixes = get_prefixes_for_tool("cursor");
369 assert!(!prefixes.is_empty());
370 assert!(prefixes.contains(&"CUR-"));
371 }
372
373 #[test]
374 fn test_get_prefixes_for_tool_unknown() {
375 let prefixes = get_prefixes_for_tool("unknown-tool");
376 assert!(prefixes.is_empty());
377 }
378
379 #[test]
382 fn test_is_valid_tool_claude_code() {
383 assert!(is_valid_tool("claude-code"));
384 assert!(is_valid_tool("Claude-Code")); assert!(is_valid_tool("CLAUDE-CODE")); }
387
388 #[test]
389 fn test_is_valid_tool_copilot() {
390 assert!(is_valid_tool("github-copilot"));
391 assert!(is_valid_tool("GitHub-Copilot")); }
393
394 #[test]
395 fn test_is_valid_tool_unknown() {
396 assert!(!is_valid_tool("unknown-tool"));
397 assert!(!is_valid_tool(""));
398 }
399
400 #[test]
403 fn test_normalize_tool_name_claude_code() {
404 assert_eq!(normalize_tool_name("claude-code"), Some("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 }
408
409 #[test]
410 fn test_normalize_tool_name_copilot() {
411 assert_eq!(
412 normalize_tool_name("github-copilot"),
413 Some("github-copilot")
414 );
415 assert_eq!(
416 normalize_tool_name("GitHub-Copilot"),
417 Some("github-copilot")
418 );
419 }
420
421 #[test]
422 fn test_normalize_tool_name_unknown() {
423 assert_eq!(normalize_tool_name("unknown-tool"), None);
424 assert_eq!(normalize_tool_name(""), None);
425 }
426
427 #[test]
430 fn test_get_prefixes_for_tool_empty_string() {
431 let prefixes = get_prefixes_for_tool("");
433 assert!(
434 prefixes.is_empty(),
435 "Empty string tool should return empty Vec"
436 );
437 }
438
439 #[test]
440 fn test_get_prefixes_for_tool_unknown_tool() {
441 let prefixes = get_prefixes_for_tool("nonexistent-tool");
443 assert!(prefixes.is_empty(), "Unknown tool should return empty Vec");
444 }
445
446 #[test]
447 fn test_get_prefixes_for_tool_claude_code_multiple_prefixes() {
448 let prefixes = get_prefixes_for_tool("claude-code");
450 assert!(
451 prefixes.len() > 1,
452 "claude-code should have multiple prefixes, got {}",
453 prefixes.len()
454 );
455 assert!(
457 prefixes.contains(&"CC-HK-"),
458 "claude-code prefixes should include CC-HK-"
459 );
460 assert!(
461 prefixes.contains(&"CC-SK-"),
462 "claude-code prefixes should include CC-SK-"
463 );
464 }
465
466 #[test]
469 fn test_get_tool_for_prefix_empty_string() {
470 assert_eq!(
472 get_tool_for_prefix(""),
473 None,
474 "Empty prefix should return None"
475 );
476 }
477
478 #[test]
479 fn test_get_tool_for_prefix_unknown_prefix() {
480 assert_eq!(
482 get_tool_for_prefix("NONEXISTENT-"),
483 None,
484 "Unknown prefix should return None"
485 );
486 assert_eq!(
487 get_tool_for_prefix("XX-"),
488 None,
489 "XX- prefix should return None"
490 );
491 }
492
493 #[test]
494 fn test_get_tool_for_prefix_partial_match_not_supported() {
495 assert_eq!(
497 get_tool_for_prefix("CC-"),
498 None,
499 "Partial prefix CC- (without HK/SK/AG) should not match"
500 );
501 assert_eq!(
502 get_tool_for_prefix("C"),
503 None,
504 "Single character should not match"
505 );
506 }
507}