Skip to main content

agnix_core/
registry.rs

1//! Validator registry and factory functions.
2
3use std::collections::{HashMap, HashSet};
4
5use crate::file_types::FileType;
6use crate::rules::Validator;
7
8/// Factory function type that creates validator instances.
9pub type ValidatorFactory = fn() -> Box<dyn Validator>;
10
11/// A provider of validator factories.
12///
13/// Implement this trait to supply validators from an external source (e.g., a
14/// plugin or a secondary rule set). The built-in validators are packaged as
15/// a `BuiltinProvider` (internal to the crate).
16///
17/// # Example
18///
19/// ```
20/// use agnix_core::{FileType, ValidatorFactory, ValidatorProvider, ValidatorRegistry};
21///
22/// struct MyProvider;
23///
24/// impl ValidatorProvider for MyProvider {
25///     fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
26///         // Return custom validators here
27///         vec![]
28///     }
29/// }
30///
31/// let registry = ValidatorRegistry::builder()
32///     .with_defaults()
33///     .with_provider(&MyProvider)
34///     .build();
35/// ```
36pub trait ValidatorProvider: Send + Sync {
37    /// Human-readable name for this provider.
38    ///
39    /// Defaults to the unqualified struct name (e.g., `"BuiltinProvider"`).
40    fn name(&self) -> &str {
41        let full = std::any::type_name::<Self>();
42        full.rsplit("::").next().unwrap_or(full)
43    }
44
45    /// Return the validator factories supplied by this provider.
46    fn validators(&self) -> Vec<(FileType, ValidatorFactory)>;
47
48    /// Return validator factories with optional static names.
49    ///
50    /// This is a **performance optimization hook**, not a rename mechanism.
51    /// When a name is `Some(name)`, the registry can skip calling `factory()`
52    /// entirely for disabled validators, avoiding the heap allocation that
53    /// would otherwise be needed just to read the validator's name.
54    ///
55    /// # Name invariant
56    ///
57    /// Each `Some(name)` **must** equal the value returned by `factory().name()`.
58    /// Violating this silently breaks the disabled-validator mechanism:
59    /// `register_named()` checks the static name against the disabled set, so a
60    /// mismatch causes the wrong validator to be excluded or allows a disabled
61    /// validator to slip through undetected. In debug builds, a
62    /// `#[cfg(debug_assertions)]` check inside `register_named()` catches this
63    /// early with zero overhead in release builds.
64    ///
65    /// # Default implementation
66    ///
67    /// The default implementation delegates to
68    /// [`validators()`](ValidatorProvider::validators) and maps each entry
69    /// into a `(FileType, None, factory)` tuple, incurring an extra allocation
70    /// compared to a direct override. Providers that know their validator names
71    /// at compile time should override this method and return `Some(name)` for
72    /// each entry to avoid the overhead.
73    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
74        self.validators()
75            .into_iter()
76            .map(|(ft, f)| (ft, None, f))
77            .collect()
78    }
79}
80
81/// The built-in validator provider shipping with agnix-core.
82///
83/// Contains all built-in validators across all supported file types. Used
84/// internally by [`ValidatorRegistry::with_defaults`] and
85/// [`ValidatorRegistryBuilder::with_defaults`].
86pub(crate) struct BuiltinProvider;
87
88impl ValidatorProvider for BuiltinProvider {
89    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
90        self.named_validators()
91            .into_iter()
92            .map(|(ft, _, f)| (ft, f))
93            .collect()
94    }
95
96    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
97        let providers: &[&dyn ValidatorProvider] = &[
98            &SkillProvider,
99            &ClaudeProvider,
100            &CopilotProvider,
101            &CursorProvider,
102            &GeminiProvider,
103            &RooProvider,
104            &WindsurfProvider,
105            &MiscProvider,
106        ];
107        let result: Vec<_> = providers
108            .iter()
109            .flat_map(|p| p.named_validators())
110            .collect();
111        debug_assert_eq!(
112            result.len(),
113            EXPECTED_BUILTIN_COUNT,
114            "BuiltinProvider produced {} entries but expected {}",
115            result.len(),
116            EXPECTED_BUILTIN_COUNT
117        );
118        result
119    }
120}
121
122/// Registry that maps [`FileType`] values to cached validator instances.
123///
124/// This is the extension point for the validation engine. A
125/// `ValidatorRegistry` owns pre-constructed [`Validator`] instances for each
126/// supported [`FileType`], eliminating per-file instantiation overhead.
127///
128/// Most callers should use [`ValidatorRegistry::with_defaults`] to obtain a
129/// registry pre-populated with all built-in validators. For advanced use cases
130/// (custom providers, disabling validators), use [`ValidatorRegistry::builder`].
131pub struct ValidatorRegistry {
132    /// Cached validator instances, keyed by file type. Each factory is called
133    /// exactly once at registration time; validators_for() returns a reference
134    /// to this pre-built slice.
135    validators: HashMap<FileType, Vec<Box<dyn Validator>>>,
136    disabled_validators: HashSet<String>,
137}
138
139impl ValidatorRegistry {
140    /// Create an empty registry with no registered validators.
141    pub fn new() -> Self {
142        Self {
143            validators: HashMap::new(),
144            disabled_validators: HashSet::new(),
145        }
146    }
147
148    /// Create a registry pre-populated with built-in validators.
149    pub fn with_defaults() -> Self {
150        let mut registry = Self::new();
151        registry.register_defaults();
152        registry
153    }
154
155    /// Create a [`ValidatorRegistryBuilder`] for ergonomic construction.
156    ///
157    /// # Example
158    ///
159    /// ```
160    /// use agnix_core::ValidatorRegistry;
161    ///
162    /// let registry = ValidatorRegistry::builder()
163    ///     .with_defaults()
164    ///     .without_validator("XmlValidator")
165    ///     .build();
166    /// ```
167    pub fn builder() -> ValidatorRegistryBuilder {
168        ValidatorRegistryBuilder::new()
169    }
170
171    /// Register a validator factory for a given file type.
172    ///
173    /// The factory is called exactly once at registration time. If the
174    /// validator's name appears in the disabled set, the instance is
175    /// immediately dropped (the factory is still called once to obtain the
176    /// validator name). For built-in validators registered via
177    /// [`with_defaults()`](ValidatorRegistry::with_defaults), this factory
178    /// call is avoided automatically using static names.
179    pub fn register(&mut self, file_type: FileType, factory: ValidatorFactory) {
180        let instance = factory();
181        if self.disabled_validators.contains(instance.name() as &str) {
182            return;
183        }
184        self.validators.entry(file_type).or_default().push(instance);
185    }
186
187    /// Register a validator factory whose name is already known.
188    ///
189    /// If `name` appears in the disabled set, the factory is never called,
190    /// avoiding the allocation entirely. This is the fast path used by
191    /// `register_defaults()` for built-in validators.
192    ///
193    /// In debug builds, a `#[cfg(debug_assertions)]` block verifies that `name`
194    /// matches `factory().name()`. The check is compiled out entirely in release
195    /// builds, so calling `instance.name()` - a vtable dispatch on
196    /// `Box<dyn Validator>` - incurs zero overhead in production. A mismatch
197    /// means the static name passed to
198    /// [`named_validators()`](ValidatorProvider::named_validators) is wrong,
199    /// which silently breaks the disabled-validator mechanism.
200    fn register_named(&mut self, file_type: FileType, name: &str, factory: ValidatorFactory) {
201        if self.disabled_validators.contains(name) {
202            return;
203        }
204        let instance = factory();
205        #[cfg(debug_assertions)]
206        {
207            let runtime_name = instance.name();
208            assert_eq!(
209                name, runtime_name,
210                "ValidatorProvider name/factory mismatch: static name \"{name}\" \
211                 does not match factory().name() \"{runtime_name}\". The static name \
212                 passed to named_validators() must equal the value returned by \
213                 Validator::name().",
214            );
215        }
216        self.validators.entry(file_type).or_default().push(instance);
217    }
218
219    /// Return the total number of cached validator instances across all file types.
220    pub fn total_validator_count(&self) -> usize {
221        self.validators.values().map(|v| v.len()).sum()
222    }
223
224    /// Return the total number of registered validator instances across all file types.
225    #[deprecated(
226        since = "0.12.2",
227        note = "renamed to total_validator_count() - validators are now cached, not re-instantiated"
228    )]
229    pub fn total_factory_count(&self) -> usize {
230        self.total_validator_count()
231    }
232
233    /// Return a reference to the cached validator instances for the given file type.
234    ///
235    /// Returns an empty slice if no validators are registered for `file_type`.
236    /// Instances whose [`name()`](Validator::name) appeared in the
237    /// `disabled_validators` set were already excluded at registration time.
238    pub fn validators_for(&self, file_type: FileType) -> &[Box<dyn Validator>] {
239        match self.validators.get(&file_type) {
240            Some(v) => v,
241            None => &[],
242        }
243    }
244
245    /// Disable a validator by name at runtime.
246    ///
247    /// The name must match the value returned by [`Validator::name()`]
248    /// (e.g., `"XmlValidator"`). Matching cached instances are removed from all
249    /// file types. This is an O(n) scan over all cached validators, which is
250    /// acceptable since this method is only called at startup.
251    pub fn disable_validator(&mut self, name: &'static str) {
252        if self.disabled_validators.insert(name.to_string()) {
253            self.remove_disabled_from_cache(name);
254        }
255    }
256
257    /// Disable a validator by name from a runtime string.
258    ///
259    /// Prefer [`disable_validator`](ValidatorRegistry::disable_validator) for
260    /// string literals.
261    pub fn disable_validator_owned(&mut self, name: &str) {
262        if self.disabled_validators.insert(name.to_string()) {
263            self.remove_disabled_from_cache(name);
264        }
265    }
266
267    /// Return the number of validator names currently disabled.
268    pub fn disabled_validator_count(&self) -> usize {
269        self.disabled_validators.len()
270    }
271
272    /// Remove cached instances whose name matches the given disabled name.
273    fn remove_disabled_from_cache(&mut self, name: &str) {
274        for instances in self.validators.values_mut() {
275            instances.retain(|v| v.name() != name);
276        }
277    }
278
279    fn register_defaults(&mut self) {
280        for (file_type, name, factory) in BuiltinProvider.named_validators() {
281            match name {
282                Some(n) => self.register_named(file_type, n, factory),
283                None => self.register(file_type, factory),
284            }
285        }
286    }
287}
288
289impl Default for ValidatorRegistry {
290    fn default() -> Self {
291        Self::with_defaults()
292    }
293}
294
295/// Builder for constructing a [`ValidatorRegistry`] with fine-grained control.
296///
297/// Supports adding built-in validators, custom [`ValidatorProvider`]
298/// implementations, individual factories, and disabling validators by name.
299///
300/// # Example
301///
302/// ```
303/// use agnix_core::ValidatorRegistry;
304///
305/// let registry = ValidatorRegistry::builder()
306///     .with_defaults()
307///     .without_validator("PromptValidator")
308///     .without_validator("XmlValidator")
309///     .build();
310///
311/// // The built registry excludes PromptValidator and XmlValidator
312/// assert!(registry.disabled_validator_count() > 0);
313/// ```
314pub struct ValidatorRegistryBuilder {
315    entries: Vec<(FileType, Option<&'static str>, ValidatorFactory)>,
316    disabled_validators: HashSet<String>,
317}
318
319impl ValidatorRegistryBuilder {
320    /// Create a new empty builder.
321    fn new() -> Self {
322        Self {
323            entries: Vec::new(),
324            disabled_validators: HashSet::new(),
325        }
326    }
327
328    /// Add all built-in validators (equivalent to [`ValidatorRegistry::with_defaults`]).
329    ///
330    /// This method is additive: calling it multiple times will register
331    /// duplicate factories. For most use cases, call it once.
332    pub fn with_defaults(&mut self) -> &mut Self {
333        self.with_provider(&BuiltinProvider)
334    }
335
336    /// Add all validators from a [`ValidatorProvider`].
337    pub fn with_provider(&mut self, provider: &dyn ValidatorProvider) -> &mut Self {
338        self.entries.extend(provider.named_validators());
339        self
340    }
341
342    /// Register a single validator factory for a file type.
343    pub fn register(&mut self, file_type: FileType, factory: ValidatorFactory) -> &mut Self {
344        self.entries.push((file_type, None, factory));
345        self
346    }
347
348    /// Mark a validator name as disabled (excluded from the built registry).
349    ///
350    /// The name must match the value returned by [`Validator::name()`]
351    /// (e.g., `"XmlValidator"`).
352    pub fn without_validator(&mut self, name: &'static str) -> &mut Self {
353        self.disabled_validators.insert(name.to_string());
354        self
355    }
356
357    /// Mark a validator name as disabled from a runtime string.
358    ///
359    /// Prefer [`without_validator`](ValidatorRegistryBuilder::without_validator)
360    /// for string literals.
361    pub fn without_validator_owned(&mut self, name: &str) -> &mut Self {
362        self.disabled_validators.insert(name.to_string());
363        self
364    }
365
366    /// Produce a [`ValidatorRegistry`] from this builder.
367    ///
368    /// Note: Calling `build()` a second time produces a registry with no
369    /// disabled validators (the disabled set is consumed via
370    /// [`std::mem::take`]), but the entries list is preserved so all
371    /// non-disabled factories are re-called. Reuse a builder by calling
372    /// configuration methods again before a subsequent `build()`.
373    ///
374    /// For entries added via `with_defaults()` or any provider that overrides
375    /// `named_validators()`, disabled validators skip the factory call
376    /// entirely. Entries added via `register()` always call the factory once
377    /// to obtain the name.
378    pub fn build(&mut self) -> ValidatorRegistry {
379        let mut registry = ValidatorRegistry {
380            validators: HashMap::new(),
381            disabled_validators: std::mem::take(&mut self.disabled_validators),
382        };
383        for &(file_type, name, factory) in &self.entries {
384            match name {
385                Some(n) => registry.register_named(file_type, n, factory),
386                None => registry.register(file_type, factory),
387            }
388        }
389        registry
390    }
391}
392
393// ============================================================================
394// Built-in defaults
395// ============================================================================
396
397/// Expected number of validator registrations across all built-in providers.
398///
399/// Used by `BuiltinProvider` (via `debug_assert_eq!`) and tests to catch
400/// accidental additions or removals without updating all providers.
401const EXPECTED_BUILTIN_COUNT: usize = 71;
402
403// -- Category providers -----------------------------------------------------
404//
405// Each struct groups validators for a related family of file types and
406// implements `ValidatorProvider` with `named_validators()` returning
407// `Some(name)` for the fast-path optimization (skip factory call for
408// disabled validators).
409
410/// Skill file validators.
411struct SkillProvider;
412
413impl ValidatorProvider for SkillProvider {
414    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
415        self.named_validators()
416            .into_iter()
417            .map(|(ft, _, f)| (ft, f))
418            .collect()
419    }
420
421    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
422        vec![
423            (FileType::Skill, Some("SkillValidator"), skill_validator),
424            (
425                FileType::Skill,
426                Some("PerClientSkillValidator"),
427                per_client_skill_validator,
428            ),
429            (FileType::Skill, Some("XmlValidator"), xml_validator),
430            (FileType::Skill, Some("ImportsValidator"), imports_validator),
431        ]
432    }
433}
434
435/// Claude family validators: ClaudeMd, Agent, ClaudeRule.
436struct ClaudeProvider;
437
438impl ValidatorProvider for ClaudeProvider {
439    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
440        self.named_validators()
441            .into_iter()
442            .map(|(ft, _, f)| (ft, f))
443            .collect()
444    }
445
446    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
447        vec![
448            (
449                FileType::ClaudeMd,
450                Some("ClaudeMdValidator"),
451                claude_md_validator,
452            ),
453            (
454                FileType::ClaudeMd,
455                Some("CrossPlatformValidator"),
456                cross_platform_validator,
457            ),
458            (
459                FileType::ClaudeMd,
460                Some("AgentsMdValidator"),
461                agents_md_validator,
462            ),
463            (FileType::ClaudeMd, Some("AmpValidator"), amp_validator),
464            (FileType::ClaudeMd, Some("XmlValidator"), xml_validator),
465            (
466                FileType::ClaudeMd,
467                Some("ImportsValidator"),
468                imports_validator,
469            ),
470            (
471                FileType::ClaudeMd,
472                Some("PromptValidator"),
473                prompt_validator,
474            ),
475            // CodexValidator on ClaudeMd catches AGENTS.override.md files (CDX-003).
476            // The validator early-returns for all other ClaudeMd filenames.
477            (FileType::ClaudeMd, Some("CodexValidator"), codex_validator),
478            (FileType::Agent, Some("AgentValidator"), agent_validator),
479            (FileType::Agent, Some("XmlValidator"), xml_validator),
480            (
481                FileType::ClaudeRule,
482                Some("ClaudeRulesValidator"),
483                claude_rules_validator,
484            ),
485        ]
486    }
487}
488
489/// Copilot family validators.
490struct CopilotProvider;
491
492impl ValidatorProvider for CopilotProvider {
493    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
494        self.named_validators()
495            .into_iter()
496            .map(|(ft, _, f)| (ft, f))
497            .collect()
498    }
499
500    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
501        vec![
502            (
503                FileType::Copilot,
504                Some("CopilotValidator"),
505                copilot_validator,
506            ),
507            (FileType::Copilot, Some("XmlValidator"), xml_validator),
508            (
509                FileType::CopilotScoped,
510                Some("CopilotValidator"),
511                copilot_validator,
512            ),
513            (FileType::CopilotScoped, Some("XmlValidator"), xml_validator),
514            (
515                FileType::CopilotAgent,
516                Some("CopilotValidator"),
517                copilot_validator,
518            ),
519            (FileType::CopilotAgent, Some("XmlValidator"), xml_validator),
520            (
521                FileType::CopilotPrompt,
522                Some("CopilotValidator"),
523                copilot_validator,
524            ),
525            (FileType::CopilotPrompt, Some("XmlValidator"), xml_validator),
526            (
527                FileType::CopilotHooks,
528                Some("CopilotValidator"),
529                copilot_validator,
530            ),
531        ]
532    }
533}
534
535/// Cursor family validators.
536struct CursorProvider;
537
538impl ValidatorProvider for CursorProvider {
539    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
540        self.named_validators()
541            .into_iter()
542            .map(|(ft, _, f)| (ft, f))
543            .collect()
544    }
545
546    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
547        vec![
548            (
549                FileType::CursorRule,
550                Some("CursorValidator"),
551                cursor_validator,
552            ),
553            (
554                FileType::CursorRule,
555                Some("PromptValidator"),
556                prompt_validator,
557            ),
558            (
559                FileType::CursorRule,
560                Some("ClaudeMdValidator"),
561                claude_md_validator,
562            ),
563            (
564                FileType::CursorHooks,
565                Some("CursorValidator"),
566                cursor_validator,
567            ),
568            (
569                FileType::CursorAgent,
570                Some("CursorValidator"),
571                cursor_validator,
572            ),
573            (
574                FileType::CursorEnvironment,
575                Some("CursorValidator"),
576                cursor_validator,
577            ),
578            (
579                FileType::CursorRulesLegacy,
580                Some("CursorValidator"),
581                cursor_validator,
582            ),
583            (
584                FileType::CursorRulesLegacy,
585                Some("PromptValidator"),
586                prompt_validator,
587            ),
588            (
589                FileType::CursorRulesLegacy,
590                Some("ClaudeMdValidator"),
591                claude_md_validator,
592            ),
593        ]
594    }
595}
596
597/// Gemini family validators.
598struct GeminiProvider;
599
600impl ValidatorProvider for GeminiProvider {
601    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
602        self.named_validators()
603            .into_iter()
604            .map(|(ft, _, f)| (ft, f))
605            .collect()
606    }
607
608    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
609        vec![
610            (
611                FileType::GeminiMd,
612                Some("GeminiMdValidator"),
613                gemini_md_validator,
614            ),
615            (
616                FileType::GeminiMd,
617                Some("PromptValidator"),
618                prompt_validator,
619            ),
620            (FileType::GeminiMd, Some("XmlValidator"), xml_validator),
621            (
622                FileType::GeminiMd,
623                Some("ImportsValidator"),
624                imports_validator,
625            ),
626            (
627                FileType::GeminiMd,
628                Some("CrossPlatformValidator"),
629                cross_platform_validator,
630            ),
631            (
632                FileType::GeminiSettings,
633                Some("GeminiSettingsValidator"),
634                gemini_settings_validator,
635            ),
636            (
637                FileType::GeminiExtension,
638                Some("GeminiExtensionValidator"),
639                gemini_extension_validator,
640            ),
641            (
642                FileType::GeminiIgnore,
643                Some("GeminiIgnoreValidator"),
644                gemini_ignore_validator,
645            ),
646        ]
647    }
648}
649
650/// Roo Code validators.
651struct RooProvider;
652
653impl ValidatorProvider for RooProvider {
654    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
655        self.named_validators()
656            .into_iter()
657            .map(|(ft, _, f)| (ft, f))
658            .collect()
659    }
660
661    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
662        vec![
663            (FileType::RooRules, Some("RooCodeValidator"), roo_validator),
664            (FileType::RooModes, Some("RooCodeValidator"), roo_validator),
665            (FileType::RooIgnore, Some("RooCodeValidator"), roo_validator),
666            (
667                FileType::RooModeRules,
668                Some("RooCodeValidator"),
669                roo_validator,
670            ),
671            (FileType::RooMcp, Some("RooCodeValidator"), roo_validator),
672        ]
673    }
674}
675
676/// Windsurf validators.
677struct WindsurfProvider;
678
679impl ValidatorProvider for WindsurfProvider {
680    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
681        self.named_validators()
682            .into_iter()
683            .map(|(ft, _, f)| (ft, f))
684            .collect()
685    }
686
687    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
688        vec![
689            (
690                FileType::WindsurfRule,
691                Some("WindsurfValidator"),
692                windsurf_validator,
693            ),
694            (
695                FileType::WindsurfWorkflow,
696                Some("WindsurfValidator"),
697                windsurf_validator,
698            ),
699            (
700                FileType::WindsurfRulesLegacy,
701                Some("WindsurfValidator"),
702                windsurf_validator,
703            ),
704        ]
705    }
706}
707
708/// Miscellaneous validators that do not belong to a larger family.
709struct MiscProvider;
710
711impl ValidatorProvider for MiscProvider {
712    fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
713        self.named_validators()
714            .into_iter()
715            .map(|(ft, _, f)| (ft, f))
716            .collect()
717    }
718
719    fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
720        vec![
721            (FileType::AmpCheck, Some("AmpValidator"), amp_validator),
722            (FileType::Hooks, Some("HooksValidator"), hooks_validator),
723            (FileType::Plugin, Some("PluginValidator"), plugin_validator),
724            (FileType::Mcp, Some("McpValidator"), mcp_validator),
725            (
726                FileType::ClineRules,
727                Some("ClineValidator"),
728                cline_validator,
729            ),
730            (
731                FileType::ClineRulesFolder,
732                Some("ClineValidator"),
733                cline_validator,
734            ),
735            (
736                FileType::OpenCodeConfig,
737                Some("OpenCodeValidator"),
738                opencode_validator,
739            ),
740            (FileType::AmpSettings, Some("AmpValidator"), amp_validator),
741            (
742                FileType::CodexConfig,
743                Some("CodexConfigValidator"),
744                codex_config_validator,
745            ),
746            (
747                FileType::KiroSteering,
748                Some("KiroSteeringValidator"),
749                kiro_steering_validator,
750            ),
751            (
752                FileType::KiroPower,
753                Some("KiroPowerValidator"),
754                kiro_power_validator,
755            ),
756            (
757                FileType::KiroPower,
758                Some("ImportsValidator"),
759                imports_validator,
760            ),
761            (
762                FileType::KiroPower,
763                Some("CrossPlatformValidator"),
764                cross_platform_validator,
765            ),
766            (FileType::KiroPower, Some("XmlValidator"), xml_validator),
767            (
768                FileType::KiroAgent,
769                Some("KiroAgentValidator"),
770                kiro_agent_validator,
771            ),
772            (
773                FileType::KiroAgent,
774                Some("ImportsValidator"),
775                imports_validator,
776            ),
777            (
778                FileType::KiroHook,
779                Some("KiroHookValidator"),
780                kiro_hook_validator,
781            ),
782            (
783                FileType::KiroHook,
784                Some("ImportsValidator"),
785                imports_validator,
786            ),
787            (
788                FileType::KiroMcp,
789                Some("KiroMcpValidator"),
790                kiro_mcp_validator,
791            ),
792            (
793                FileType::GenericMarkdown,
794                Some("CrossPlatformValidator"),
795                cross_platform_validator,
796            ),
797            (
798                FileType::GenericMarkdown,
799                Some("XmlValidator"),
800                xml_validator,
801            ),
802            (
803                FileType::GenericMarkdown,
804                Some("ImportsValidator"),
805                imports_validator,
806            ),
807        ]
808    }
809}
810
811// ============================================================================
812// Factory functions
813// ============================================================================
814
815fn skill_validator() -> Box<dyn Validator> {
816    Box::new(crate::rules::skill::SkillValidator)
817}
818
819fn per_client_skill_validator() -> Box<dyn Validator> {
820    Box::new(crate::rules::per_client_skill::PerClientSkillValidator)
821}
822
823fn amp_validator() -> Box<dyn Validator> {
824    Box::new(crate::rules::amp::AmpValidator)
825}
826
827fn claude_md_validator() -> Box<dyn Validator> {
828    Box::new(crate::rules::claude_md::ClaudeMdValidator)
829}
830
831fn agents_md_validator() -> Box<dyn Validator> {
832    Box::new(crate::rules::agents_md::AgentsMdValidator)
833}
834
835fn agent_validator() -> Box<dyn Validator> {
836    Box::new(crate::rules::agent::AgentValidator)
837}
838
839fn hooks_validator() -> Box<dyn Validator> {
840    Box::new(crate::rules::hooks::HooksValidator)
841}
842
843fn plugin_validator() -> Box<dyn Validator> {
844    Box::new(crate::rules::plugin::PluginValidator)
845}
846
847fn mcp_validator() -> Box<dyn Validator> {
848    Box::new(crate::rules::mcp::McpValidator)
849}
850
851fn xml_validator() -> Box<dyn Validator> {
852    Box::new(crate::rules::xml::XmlValidator)
853}
854
855fn imports_validator() -> Box<dyn Validator> {
856    Box::new(crate::rules::imports::ImportsValidator)
857}
858
859fn cross_platform_validator() -> Box<dyn Validator> {
860    Box::new(crate::rules::cross_platform::CrossPlatformValidator)
861}
862
863fn prompt_validator() -> Box<dyn Validator> {
864    Box::new(crate::rules::prompt::PromptValidator)
865}
866
867fn copilot_validator() -> Box<dyn Validator> {
868    Box::new(crate::rules::copilot::CopilotValidator)
869}
870
871fn claude_rules_validator() -> Box<dyn Validator> {
872    Box::new(crate::rules::claude_rules::ClaudeRulesValidator)
873}
874
875fn cursor_validator() -> Box<dyn Validator> {
876    Box::new(crate::rules::cursor::CursorValidator)
877}
878
879fn cline_validator() -> Box<dyn Validator> {
880    Box::new(crate::rules::cline::ClineValidator)
881}
882
883fn opencode_validator() -> Box<dyn Validator> {
884    Box::new(crate::rules::opencode::OpenCodeValidator)
885}
886
887fn gemini_md_validator() -> Box<dyn Validator> {
888    Box::new(crate::rules::gemini_md::GeminiMdValidator)
889}
890
891fn gemini_settings_validator() -> Box<dyn Validator> {
892    Box::new(crate::rules::gemini_settings::GeminiSettingsValidator)
893}
894
895fn gemini_extension_validator() -> Box<dyn Validator> {
896    Box::new(crate::rules::gemini_extension::GeminiExtensionValidator)
897}
898
899fn gemini_ignore_validator() -> Box<dyn Validator> {
900    Box::new(crate::rules::gemini_ignore::GeminiIgnoreValidator)
901}
902
903fn codex_validator() -> Box<dyn Validator> {
904    Box::new(crate::rules::codex::CodexValidator)
905}
906
907fn codex_config_validator() -> Box<dyn Validator> {
908    Box::new(crate::rules::codex::CodexConfigValidator)
909}
910
911fn roo_validator() -> Box<dyn Validator> {
912    Box::new(crate::rules::roo::RooCodeValidator)
913}
914
915fn windsurf_validator() -> Box<dyn Validator> {
916    Box::new(crate::rules::windsurf::WindsurfValidator)
917}
918
919fn kiro_steering_validator() -> Box<dyn Validator> {
920    Box::new(crate::rules::kiro_steering::KiroSteeringValidator)
921}
922
923fn kiro_agent_validator() -> Box<dyn Validator> {
924    Box::new(crate::rules::kiro_agent::KiroAgentValidator)
925}
926
927fn kiro_power_validator() -> Box<dyn Validator> {
928    Box::new(crate::rules::kiro_power::KiroPowerValidator)
929}
930
931fn kiro_hook_validator() -> Box<dyn Validator> {
932    Box::new(crate::rules::kiro_hook::KiroHookValidator)
933}
934
935fn kiro_mcp_validator() -> Box<dyn Validator> {
936    Box::new(crate::rules::kiro_mcp::KiroMcpValidator)
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942    use std::sync::atomic::{AtomicUsize, Ordering};
943
944    // ---- BuiltinProvider tests ----
945
946    #[test]
947    fn builtin_provider_returns_expected_count() {
948        let provider = BuiltinProvider;
949        let entries = provider.validators();
950        assert_eq!(
951            entries.len(),
952            EXPECTED_BUILTIN_COUNT,
953            "BuiltinProvider should return the same number of entries as EXPECTED_BUILTIN_COUNT"
954        );
955    }
956
957    #[test]
958    fn builtin_provider_name() {
959        let provider = BuiltinProvider;
960        assert_eq!(provider.name(), "BuiltinProvider");
961    }
962
963    // ---- Builder tests ----
964
965    #[test]
966    fn builder_with_defaults_matches_with_defaults() {
967        let via_builder = ValidatorRegistry::builder().with_defaults().build();
968        let via_direct = ValidatorRegistry::with_defaults();
969
970        assert_eq!(
971            via_builder.total_validator_count(),
972            via_direct.total_validator_count(),
973            "Builder with_defaults should produce the same validator count as with_defaults()"
974        );
975    }
976
977    #[test]
978    fn builder_empty_produces_empty_registry() {
979        let registry = ValidatorRegistry::builder().build();
980        assert_eq!(registry.total_validator_count(), 0);
981    }
982
983    #[test]
984    fn builder_register_adds_single_factory() {
985        let registry = ValidatorRegistry::builder()
986            .register(FileType::Skill, skill_validator)
987            .build();
988
989        assert_eq!(registry.total_validator_count(), 1);
990        let validators = registry.validators_for(FileType::Skill);
991        assert_eq!(validators.len(), 1);
992        assert_eq!(validators[0].name(), "SkillValidator");
993    }
994
995    #[test]
996    fn builder_without_validator_disables() {
997        let registry = ValidatorRegistry::builder()
998            .with_defaults()
999            .without_validator("XmlValidator")
1000            .build();
1001
1002        // XmlValidator should be excluded from Skill validators
1003        let skill_validators = registry.validators_for(FileType::Skill);
1004        let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1005        assert!(
1006            !names.contains(&"XmlValidator"),
1007            "XmlValidator should be disabled, got: {:?}",
1008            names
1009        );
1010
1011        // But SkillValidator should still be present
1012        assert!(
1013            names.contains(&"SkillValidator"),
1014            "SkillValidator should still be present, got: {:?}",
1015            names
1016        );
1017    }
1018
1019    // ---- Custom provider tests ----
1020
1021    struct TestProvider;
1022    impl ValidatorProvider for TestProvider {
1023        fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1024            vec![(FileType::Skill, skill_validator)]
1025        }
1026    }
1027
1028    #[test]
1029    fn custom_provider_adds_validators() {
1030        let registry = ValidatorRegistry::builder()
1031            .with_provider(&TestProvider)
1032            .build();
1033
1034        assert_eq!(registry.total_validator_count(), 1);
1035        let validators = registry.validators_for(FileType::Skill);
1036        assert_eq!(validators.len(), 1);
1037    }
1038
1039    #[test]
1040    fn custom_provider_name() {
1041        let provider = TestProvider;
1042        assert_eq!(provider.name(), "TestProvider");
1043    }
1044
1045    // ---- disable_validator() direct mutation tests ----
1046
1047    #[test]
1048    fn disable_validator_filters_from_results() {
1049        let mut registry = ValidatorRegistry::with_defaults();
1050        assert_eq!(registry.disabled_validator_count(), 0);
1051
1052        registry.disable_validator("XmlValidator");
1053        assert_eq!(registry.disabled_validator_count(), 1);
1054
1055        let skill_validators = registry.validators_for(FileType::Skill);
1056        let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1057        assert!(!names.contains(&"XmlValidator"));
1058    }
1059
1060    // ---- Per-test counting validators (separate statics to avoid races) ----
1061
1062    // Used by register_skips_disabled_validators
1063    static SKIP_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1064
1065    struct SkipCountingValidator;
1066
1067    impl Validator for SkipCountingValidator {
1068        fn validate(
1069            &self,
1070            _path: &std::path::Path,
1071            _content: &str,
1072            _config: &crate::config::LintConfig,
1073        ) -> Vec<crate::diagnostics::Diagnostic> {
1074            Vec::new()
1075        }
1076
1077        fn name(&self) -> &'static str {
1078            "SkipCountingValidator"
1079        }
1080    }
1081
1082    fn skip_counting_validator_factory() -> Box<dyn Validator> {
1083        SKIP_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1084        Box::new(SkipCountingValidator)
1085    }
1086
1087    // Used by register_calls_factory_exactly_once
1088    static ONCE_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1089
1090    struct OnceCountingValidator;
1091
1092    impl Validator for OnceCountingValidator {
1093        fn validate(
1094            &self,
1095            _path: &std::path::Path,
1096            _content: &str,
1097            _config: &crate::config::LintConfig,
1098        ) -> Vec<crate::diagnostics::Diagnostic> {
1099            Vec::new()
1100        }
1101
1102        fn name(&self) -> &'static str {
1103            "OnceCountingValidator"
1104        }
1105    }
1106
1107    fn once_counting_validator_factory() -> Box<dyn Validator> {
1108        ONCE_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1109        Box::new(OnceCountingValidator)
1110    }
1111
1112    // Used by register_calls_factory_exactly_once_via_builder
1113    static BUILDER_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1114
1115    struct BuilderCountingValidator;
1116
1117    impl Validator for BuilderCountingValidator {
1118        fn validate(
1119            &self,
1120            _path: &std::path::Path,
1121            _content: &str,
1122            _config: &crate::config::LintConfig,
1123        ) -> Vec<crate::diagnostics::Diagnostic> {
1124            Vec::new()
1125        }
1126
1127        fn name(&self) -> &'static str {
1128            "BuilderCountingValidator"
1129        }
1130    }
1131
1132    fn builder_counting_validator_factory() -> Box<dyn Validator> {
1133        BUILDER_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1134        Box::new(BuilderCountingValidator)
1135    }
1136
1137    #[test]
1138    fn register_skips_disabled_validators() {
1139        SKIP_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1140
1141        let registry = ValidatorRegistry::builder()
1142            .register(FileType::Skill, skip_counting_validator_factory)
1143            .without_validator("SkipCountingValidator")
1144            .build();
1145
1146        // This exercises the slow (unnamed) path: builder.register() stores None
1147        // for the name, so build() calls registry.register() which always calls
1148        // the factory once to obtain the name. The instance is then discarded.
1149        // Contrast with named_disabled_validator_skips_factory_call which uses
1150        // the fast path (Some(name)) and asserts 0 factory calls.
1151        assert_eq!(SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1152
1153        // No cached instances remain for disabled validators.
1154        let validators = registry.validators_for(FileType::Skill);
1155        assert!(validators.is_empty());
1156
1157        // validators_for() no longer calls factories - counter stays at 1.
1158        assert_eq!(SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1159    }
1160
1161    #[test]
1162    fn disable_nonexistent_validator_is_harmless() {
1163        let mut registry = ValidatorRegistry::with_defaults();
1164        registry.disable_validator("NonExistentValidator");
1165        assert_eq!(registry.disabled_validator_count(), 1);
1166
1167        // Should still work normally
1168        let count_before = ValidatorRegistry::with_defaults().total_validator_count();
1169        assert_eq!(registry.total_validator_count(), count_before);
1170    }
1171
1172    // ---- validators_for filtering ----
1173
1174    #[test]
1175    fn validators_for_returns_all_when_none_disabled() {
1176        let registry = ValidatorRegistry::with_defaults();
1177        let skill_validators = registry.validators_for(FileType::Skill);
1178        // Skill has: SkillValidator, PerClientSkillValidator, XmlValidator, ImportsValidator
1179        assert_eq!(skill_validators.len(), 4);
1180    }
1181
1182    #[test]
1183    fn validators_for_unknown_file_type_returns_empty() {
1184        let registry = ValidatorRegistry::with_defaults();
1185        let validators = registry.validators_for(FileType::Unknown);
1186        assert!(validators.is_empty());
1187    }
1188
1189    // ---- Multiple disabled validators ----
1190
1191    #[test]
1192    fn builder_multiple_without_validators() {
1193        let registry = ValidatorRegistry::builder()
1194            .with_defaults()
1195            .without_validator("XmlValidator")
1196            .without_validator("PromptValidator")
1197            .build();
1198
1199        assert_eq!(registry.disabled_validator_count(), 2);
1200
1201        let skill_names: Vec<&str> = registry
1202            .validators_for(FileType::Skill)
1203            .iter()
1204            .map(|v| v.name())
1205            .collect();
1206        assert!(!skill_names.contains(&"XmlValidator"));
1207
1208        let claude_names: Vec<&str> = registry
1209            .validators_for(FileType::ClaudeMd)
1210            .iter()
1211            .map(|v| v.name())
1212            .collect();
1213        assert!(!claude_names.contains(&"PromptValidator"));
1214        assert!(!claude_names.contains(&"XmlValidator"));
1215    }
1216
1217    #[test]
1218    fn disable_all_validators_for_file_type() {
1219        let registry = ValidatorRegistry::builder()
1220            .with_defaults()
1221            .without_validator("SkillValidator")
1222            .without_validator("PerClientSkillValidator")
1223            .without_validator("XmlValidator")
1224            .without_validator("ImportsValidator")
1225            .build();
1226
1227        assert!(
1228            registry.validators_for(FileType::Skill).is_empty(),
1229            "All Skill validators disabled, should return empty"
1230        );
1231    }
1232
1233    #[test]
1234    fn disable_same_validator_twice_is_idempotent() {
1235        let mut registry = ValidatorRegistry::with_defaults();
1236        registry.disable_validator("XmlValidator");
1237        registry.disable_validator("XmlValidator");
1238        assert_eq!(registry.disabled_validator_count(), 1);
1239    }
1240
1241    #[test]
1242    fn disable_validator_owned_filters_from_results() {
1243        let mut registry = ValidatorRegistry::with_defaults();
1244        let name = String::from("XmlValidator");
1245        registry.disable_validator_owned(&name);
1246        assert_eq!(registry.disabled_validator_count(), 1);
1247
1248        let skill_validators = registry.validators_for(FileType::Skill);
1249        let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1250        assert!(!names.contains(&"XmlValidator"));
1251    }
1252
1253    #[test]
1254    fn disable_validator_owned_twice_is_idempotent() {
1255        let mut registry = ValidatorRegistry::with_defaults();
1256        registry.disable_validator_owned("XmlValidator");
1257        registry.disable_validator_owned("XmlValidator");
1258        assert_eq!(registry.disabled_validator_count(), 1);
1259    }
1260
1261    #[test]
1262    fn mixed_static_and_owned_disable() {
1263        let mut registry = ValidatorRegistry::with_defaults();
1264        registry.disable_validator("XmlValidator");
1265        registry.disable_validator_owned("PromptValidator");
1266        assert_eq!(registry.disabled_validator_count(), 2);
1267
1268        let claude_validators = registry.validators_for(FileType::ClaudeMd);
1269        let names: Vec<&str> = claude_validators.iter().map(|v| v.name()).collect();
1270        assert!(!names.contains(&"XmlValidator"));
1271        assert!(!names.contains(&"PromptValidator"));
1272    }
1273
1274    #[test]
1275    fn builder_without_validator_owned_disables() {
1276        let registry = ValidatorRegistry::builder()
1277            .with_defaults()
1278            .without_validator_owned("XmlValidator")
1279            .build();
1280
1281        let skill_validators = registry.validators_for(FileType::Skill);
1282        let names: Vec<&str> = skill_validators.iter().map(|v| v.name()).collect();
1283        assert!(!names.contains(&"XmlValidator"));
1284    }
1285
1286    // ---- Multiple providers ----
1287
1288    struct ProviderA;
1289    impl ValidatorProvider for ProviderA {
1290        fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1291            vec![(FileType::Skill, skill_validator)]
1292        }
1293    }
1294
1295    struct ProviderB;
1296    impl ValidatorProvider for ProviderB {
1297        fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1298            vec![(FileType::Agent, agent_validator)]
1299        }
1300    }
1301
1302    #[test]
1303    fn builder_multiple_providers() {
1304        let registry = ValidatorRegistry::builder()
1305            .with_provider(&ProviderA)
1306            .with_provider(&ProviderB)
1307            .build();
1308
1309        assert!(!registry.validators_for(FileType::Skill).is_empty());
1310        assert!(!registry.validators_for(FileType::Agent).is_empty());
1311        assert_eq!(registry.total_validator_count(), 2);
1312    }
1313
1314    // ---- Backward compatibility ----
1315
1316    #[test]
1317    fn with_defaults_returns_expected_factories() {
1318        let registry = ValidatorRegistry::with_defaults();
1319        assert_eq!(
1320            registry.total_validator_count(),
1321            EXPECTED_BUILTIN_COUNT,
1322            "with_defaults() should register exactly as many validators as EXPECTED_BUILTIN_COUNT"
1323        );
1324    }
1325
1326    #[test]
1327    fn default_trait_matches_with_defaults() {
1328        let via_default = ValidatorRegistry::default();
1329        let via_explicit = ValidatorRegistry::with_defaults();
1330        assert_eq!(
1331            via_default.total_validator_count(),
1332            via_explicit.total_validator_count()
1333        );
1334    }
1335
1336    // ---- Coverage: every validatable FileType has validators ----
1337
1338    #[test]
1339    fn every_validatable_file_type_has_at_least_one_validator() {
1340        let validatable_types: [FileType; 41] = [
1341            FileType::Skill,
1342            FileType::ClaudeMd,
1343            FileType::Agent,
1344            FileType::AmpCheck,
1345            FileType::Hooks,
1346            FileType::Plugin,
1347            FileType::Mcp,
1348            FileType::Copilot,
1349            FileType::CopilotScoped,
1350            FileType::CopilotAgent,
1351            FileType::CopilotPrompt,
1352            FileType::CopilotHooks,
1353            FileType::ClaudeRule,
1354            FileType::CursorRule,
1355            FileType::CursorHooks,
1356            FileType::CursorAgent,
1357            FileType::CursorEnvironment,
1358            FileType::CursorRulesLegacy,
1359            FileType::ClineRules,
1360            FileType::ClineRulesFolder,
1361            FileType::OpenCodeConfig,
1362            FileType::GeminiMd,
1363            FileType::GeminiSettings,
1364            FileType::AmpSettings,
1365            FileType::GeminiExtension,
1366            FileType::GeminiIgnore,
1367            FileType::CodexConfig,
1368            FileType::RooRules,
1369            FileType::RooModes,
1370            FileType::RooIgnore,
1371            FileType::RooModeRules,
1372            FileType::RooMcp,
1373            FileType::WindsurfRule,
1374            FileType::WindsurfWorkflow,
1375            FileType::WindsurfRulesLegacy,
1376            FileType::KiroSteering,
1377            FileType::KiroPower,
1378            FileType::KiroAgent,
1379            FileType::KiroHook,
1380            FileType::KiroMcp,
1381            FileType::GenericMarkdown,
1382        ];
1383
1384        // Exhaustive match with no wildcard arm - a new variant will cause a
1385        // compile error, forcing the developer to update this test.
1386        for ft in &validatable_types {
1387            match *ft {
1388                FileType::Skill
1389                | FileType::ClaudeMd
1390                | FileType::Agent
1391                | FileType::AmpCheck
1392                | FileType::Hooks
1393                | FileType::Plugin
1394                | FileType::Mcp
1395                | FileType::Copilot
1396                | FileType::CopilotScoped
1397                | FileType::CopilotAgent
1398                | FileType::CopilotPrompt
1399                | FileType::CopilotHooks
1400                | FileType::ClaudeRule
1401                | FileType::CursorRule
1402                | FileType::CursorHooks
1403                | FileType::CursorAgent
1404                | FileType::CursorEnvironment
1405                | FileType::CursorRulesLegacy
1406                | FileType::ClineRules
1407                | FileType::ClineRulesFolder
1408                | FileType::OpenCodeConfig
1409                | FileType::GeminiMd
1410                | FileType::GeminiSettings
1411                | FileType::AmpSettings
1412                | FileType::GeminiExtension
1413                | FileType::GeminiIgnore
1414                | FileType::CodexConfig
1415                | FileType::RooRules
1416                | FileType::RooModes
1417                | FileType::RooIgnore
1418                | FileType::RooModeRules
1419                | FileType::RooMcp
1420                | FileType::WindsurfRule
1421                | FileType::WindsurfWorkflow
1422                | FileType::WindsurfRulesLegacy
1423                | FileType::KiroSteering
1424                | FileType::KiroPower
1425                | FileType::KiroAgent
1426                | FileType::KiroHook
1427                | FileType::KiroMcp
1428                | FileType::GenericMarkdown => (),
1429                FileType::Unknown => {
1430                    panic!("Unknown must not appear in validatable_types")
1431                }
1432            }
1433        }
1434
1435        let registry = ValidatorRegistry::with_defaults();
1436
1437        for ft in &validatable_types {
1438            let validators = registry.validators_for(*ft);
1439            assert!(
1440                !validators.is_empty(),
1441                "{ft:?} has no validators registered in the default registry"
1442            );
1443        }
1444    }
1445
1446    #[test]
1447    fn kiro_file_types_route_to_expected_validators() {
1448        let registry = ValidatorRegistry::with_defaults();
1449
1450        let names_for = |file_type: FileType| -> Vec<&'static str> {
1451            registry
1452                .validators_for(file_type)
1453                .iter()
1454                .map(|validator| validator.name())
1455                .collect()
1456        };
1457
1458        assert_eq!(
1459            names_for(FileType::KiroPower),
1460            vec![
1461                "KiroPowerValidator",
1462                "ImportsValidator",
1463                "CrossPlatformValidator",
1464                "XmlValidator"
1465            ]
1466        );
1467        assert_eq!(
1468            names_for(FileType::KiroAgent),
1469            vec!["KiroAgentValidator", "ImportsValidator"]
1470        );
1471        assert_eq!(
1472            names_for(FileType::KiroHook),
1473            vec!["KiroHookValidator", "ImportsValidator"]
1474        );
1475        assert_eq!(names_for(FileType::KiroMcp), vec!["KiroMcpValidator"]);
1476    }
1477
1478    // ---- Caching correctness tests ----
1479
1480    #[test]
1481    fn validators_for_returns_same_slice_on_repeated_calls() {
1482        let registry = ValidatorRegistry::with_defaults();
1483        let first = registry.validators_for(FileType::Skill);
1484        let second = registry.validators_for(FileType::Skill);
1485
1486        // Both calls must return the same underlying slice (same pointer and length).
1487        assert_eq!(first.len(), second.len());
1488        assert!(
1489            std::ptr::eq(first.as_ptr(), second.as_ptr()),
1490            "validators_for() must return the same cached slice on repeated calls"
1491        );
1492    }
1493
1494    #[test]
1495    fn register_calls_factory_exactly_once() {
1496        ONCE_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1497
1498        let mut registry = ValidatorRegistry::new();
1499        registry.register(FileType::Skill, once_counting_validator_factory);
1500
1501        // Factory called exactly once during register().
1502        assert_eq!(ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1503
1504        // validators_for() should NOT call the factory again.
1505        let _validators = registry.validators_for(FileType::Skill);
1506        assert_eq!(
1507            ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1508            1,
1509            "validators_for() must not re-instantiate cached validators"
1510        );
1511
1512        // Even repeated calls should not increment the counter.
1513        let _validators = registry.validators_for(FileType::Skill);
1514        assert_eq!(ONCE_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1515    }
1516
1517    #[test]
1518    fn register_calls_factory_exactly_once_via_builder() {
1519        BUILDER_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1520
1521        let registry = ValidatorRegistry::builder()
1522            .register(FileType::Skill, builder_counting_validator_factory)
1523            .build();
1524
1525        // Factory called exactly once during build().
1526        assert_eq!(BUILDER_COUNTING_CONSTRUCTED.load(Ordering::SeqCst), 1);
1527
1528        // validators_for() should NOT call the factory again.
1529        let _validators = registry.validators_for(FileType::Skill);
1530        assert_eq!(
1531            BUILDER_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1532            1,
1533            "validators_for() must not re-instantiate cached validators via builder path"
1534        );
1535    }
1536
1537    #[test]
1538    fn disable_after_construction_removes_from_cache() {
1539        let mut registry = ValidatorRegistry::with_defaults();
1540        let total_before = registry.total_validator_count();
1541
1542        // Verify XmlValidator is present before disabling.
1543        let before = registry.validators_for(FileType::Skill);
1544        assert!(
1545            before.iter().any(|v| v.name() == "XmlValidator"),
1546            "XmlValidator should be present before disabling"
1547        );
1548
1549        registry.disable_validator("XmlValidator");
1550
1551        // After disabling, XmlValidator must be absent from the cached slice.
1552        let after = registry.validators_for(FileType::Skill);
1553        assert!(
1554            !after.iter().any(|v| v.name() == "XmlValidator"),
1555            "XmlValidator should be removed after disable_validator()"
1556        );
1557
1558        // Also absent from other file types that had XmlValidator.
1559        let claude_after = registry.validators_for(FileType::ClaudeMd);
1560        assert!(
1561            !claude_after.iter().any(|v| v.name() == "XmlValidator"),
1562            "XmlValidator should be removed from all file types"
1563        );
1564
1565        // XmlValidator appears in 10 file types across built-in providers. Count
1566        // via the static names and verify the total decreases by exactly that
1567        // amount.
1568        let xml_occurrences = BuiltinProvider
1569            .named_validators()
1570            .iter()
1571            .filter(|(_, name, _)| *name == Some("XmlValidator"))
1572            .count();
1573        assert_eq!(
1574            xml_occurrences, 10,
1575            "Expected XmlValidator in 10 BuiltinProvider entries"
1576        );
1577        let total_after = registry.total_validator_count();
1578        assert_eq!(
1579            total_before - total_after,
1580            xml_occurrences,
1581            "Disabling XmlValidator should remove exactly {} instances, \
1582             but removed {}",
1583            xml_occurrences,
1584            total_before - total_after
1585        );
1586    }
1587
1588    #[test]
1589    fn registry_is_send_sync() {
1590        fn assert_send_sync<T: Send + Sync>() {}
1591        assert_send_sync::<ValidatorRegistry>();
1592    }
1593
1594    #[test]
1595    #[allow(deprecated)]
1596    fn deprecated_total_factory_count_matches_total_validator_count() {
1597        let registry = ValidatorRegistry::with_defaults();
1598        assert_eq!(
1599            registry.total_factory_count(),
1600            registry.total_validator_count()
1601        );
1602    }
1603
1604    // ---- Named registration tests ----
1605
1606    #[test]
1607    fn defaults_names_match_factory_names() {
1608        // Every static name in BuiltinProvider must exactly match the name
1609        // returned by the factory-produced instance. A mismatch would silently
1610        // break the disabled-validator fast path.
1611        for (file_type, static_name, factory) in BuiltinProvider.named_validators() {
1612            let static_name = static_name.expect("BuiltinProvider entries must have Some(name)");
1613            let instance = factory();
1614            let runtime_name = instance.name();
1615            assert_eq!(
1616                static_name, runtime_name,
1617                "BuiltinProvider name mismatch for {file_type:?}: \
1618                 static=\"{static_name}\" vs runtime=\"{runtime_name}\""
1619            );
1620        }
1621    }
1622
1623    // Used by named_disabled_validator_skips_factory_call
1624    static NAMED_SKIP_COUNTING_CONSTRUCTED: AtomicUsize = AtomicUsize::new(0);
1625
1626    struct NamedSkipCountingValidator;
1627
1628    impl Validator for NamedSkipCountingValidator {
1629        fn validate(
1630            &self,
1631            _path: &std::path::Path,
1632            _content: &str,
1633            _config: &crate::config::LintConfig,
1634        ) -> Vec<crate::diagnostics::Diagnostic> {
1635            Vec::new()
1636        }
1637
1638        fn name(&self) -> &'static str {
1639            "NamedSkipCountingValidator"
1640        }
1641    }
1642
1643    fn named_skip_counting_validator_factory() -> Box<dyn Validator> {
1644        NAMED_SKIP_COUNTING_CONSTRUCTED.fetch_add(1, Ordering::SeqCst);
1645        Box::new(NamedSkipCountingValidator)
1646    }
1647
1648    #[test]
1649    fn named_disabled_validator_skips_factory_call() {
1650        // Uses a named provider so the builder stores Some("NamedSkipCountingValidator"),
1651        // routing through register_named() in build(). The factory must not be called.
1652        NAMED_SKIP_COUNTING_CONSTRUCTED.store(0, Ordering::SeqCst);
1653
1654        struct NamedCountingProvider;
1655        impl ValidatorProvider for NamedCountingProvider {
1656            fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1657                vec![(FileType::Skill, named_skip_counting_validator_factory)]
1658            }
1659
1660            fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1661                vec![(
1662                    FileType::Skill,
1663                    Some("NamedSkipCountingValidator"),
1664                    named_skip_counting_validator_factory,
1665                )]
1666            }
1667        }
1668
1669        let registry = ValidatorRegistry::builder()
1670            .with_provider(&NamedCountingProvider)
1671            .without_validator("NamedSkipCountingValidator")
1672            .build();
1673
1674        // Factory must NOT have been called - that is the whole point of
1675        // register_named: skip allocation for disabled validators.
1676        assert_eq!(
1677            NAMED_SKIP_COUNTING_CONSTRUCTED.load(Ordering::SeqCst),
1678            0,
1679            "register_named must not call factory for disabled validators"
1680        );
1681
1682        // No cached instances for this type.
1683        assert!(registry.validators_for(FileType::Skill).is_empty());
1684    }
1685
1686    #[test]
1687    fn builtin_provider_named_validators_returns_all_names() {
1688        let provider = BuiltinProvider;
1689        let named = provider.named_validators();
1690
1691        assert_eq!(
1692            named.len(),
1693            EXPECTED_BUILTIN_COUNT,
1694            "named_validators() should return EXPECTED_BUILTIN_COUNT entries"
1695        );
1696
1697        // Every entry must have Some(name).
1698        for (i, (ft, name, _factory)) in named.iter().enumerate() {
1699            assert!(
1700                name.is_some(),
1701                "Entry {i} ({ft:?}) should have Some(name), got None"
1702            );
1703        }
1704
1705        // Self-consistency: validators() and named_validators() must agree on
1706        // count and file types.
1707        let unnamed = provider.validators();
1708        assert_eq!(
1709            unnamed.len(),
1710            named.len(),
1711            "validators() and named_validators() must return the same count"
1712        );
1713        for ((ft_unnamed, _), (ft_named, _, _)) in unnamed.iter().zip(named.iter()) {
1714            assert_eq!(
1715                ft_unnamed, ft_named,
1716                "validators() and named_validators() file types must match"
1717            );
1718        }
1719    }
1720
1721    #[test]
1722    fn custom_provider_named_validators_defaults_to_none() {
1723        // A provider that only implements validators() should get None names
1724        // from the default named_validators() implementation.
1725        let provider = TestProvider;
1726        let named = provider.named_validators();
1727
1728        assert_eq!(named.len(), 1);
1729        let (ft, name, _factory) = &named[0];
1730        assert_eq!(*ft, FileType::Skill);
1731        assert!(
1732            name.is_none(),
1733            "Default named_validators() should yield None names"
1734        );
1735    }
1736
1737    // ---- Name/factory mismatch tests ----
1738
1739    // Validator whose name() returns "ActualName", used to demonstrate the
1740    // mismatch when a provider declares the static name as "WrongName".
1741    struct MismatchedValidator;
1742
1743    impl Validator for MismatchedValidator {
1744        fn validate(
1745            &self,
1746            _path: &std::path::Path,
1747            _content: &str,
1748            _config: &crate::config::LintConfig,
1749        ) -> Vec<crate::diagnostics::Diagnostic> {
1750            vec![]
1751        }
1752
1753        fn name(&self) -> &'static str {
1754            "ActualName"
1755        }
1756    }
1757
1758    fn mismatched_validator_factory() -> Box<dyn Validator> {
1759        Box::new(MismatchedValidator)
1760    }
1761
1762    #[cfg(debug_assertions)]
1763    #[test]
1764    #[should_panic(expected = "name/factory mismatch")]
1765    fn mismatched_named_validator_panics_in_debug() {
1766        // Provider declares the static name as "WrongName" but the factory
1767        // produces a validator with name() = "ActualName". The debug_assert_eq!
1768        // inside register_named() must catch this and panic.
1769        struct MismatchedProvider;
1770        impl ValidatorProvider for MismatchedProvider {
1771            fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1772                // Required by the trait; not exercised by this test path.
1773                vec![(FileType::Skill, mismatched_validator_factory)]
1774            }
1775            fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1776                vec![(
1777                    FileType::Skill,
1778                    Some("WrongName"),
1779                    mismatched_validator_factory,
1780                )]
1781            }
1782        }
1783
1784        // Building the registry triggers register_named() which hits the
1785        // debug_assert_eq! because "WrongName" != "ActualName".
1786        let _registry = ValidatorRegistry::builder()
1787            .with_provider(&MismatchedProvider)
1788            .build();
1789    }
1790
1791    // Not cfg(debug_assertions)-gated: the factory-skip path (early return
1792    // when the static name is in the disabled set) is taken before the
1793    // debug_assert_eq! can fire, so this test holds in both debug and release.
1794    #[test]
1795    fn mismatched_named_validator_silently_skips_when_disabled() {
1796        // Same mismatch as above, but "WrongName" is in the disabled set.
1797        // Because register_named() checks the static name against the disabled
1798        // set before calling factory(), the factory is never called, so the
1799        // debug_assert never fires. However, the validator with the actual
1800        // name "ActualName" is also NOT registered - demonstrating the
1801        // silent-skip failure mode that the invariant documentation warns about.
1802        struct MismatchedProvider;
1803        impl ValidatorProvider for MismatchedProvider {
1804            fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1805                vec![(FileType::Skill, mismatched_validator_factory)]
1806            }
1807            fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1808                vec![(
1809                    FileType::Skill,
1810                    Some("WrongName"),
1811                    mismatched_validator_factory,
1812                )]
1813            }
1814        }
1815
1816        let registry = ValidatorRegistry::builder()
1817            .with_provider(&MismatchedProvider)
1818            .without_validator("WrongName")
1819            .build();
1820
1821        // The factory was never called because "WrongName" matched the
1822        // disabled set. No validators are registered at all.
1823        assert!(
1824            registry.validators_for(FileType::Skill).is_empty(),
1825            "Mismatched static name caused silent skip - no validators registered"
1826        );
1827    }
1828
1829    // This test only runs in release mode: in debug builds the debug_assert_eq!
1830    // inside register_named() would panic when the factory is called (because
1831    // "WrongName" is not in the disabled set), masking the slip-through behavior.
1832    #[cfg(not(debug_assertions))]
1833    #[test]
1834    fn mismatched_named_validator_slip_through_when_real_name_disabled() {
1835        // The dangerous half of the failure mode: the user tries to disable
1836        // "ActualName" (the real validator name), but the provider declared the
1837        // static name as "WrongName". register_named() checks "WrongName"
1838        // against the disabled set - no match - so the factory is called and
1839        // the validator is registered despite the disable request.
1840        struct MismatchedProvider;
1841        impl ValidatorProvider for MismatchedProvider {
1842            fn validators(&self) -> Vec<(FileType, ValidatorFactory)> {
1843                // Required by the trait; not exercised by this test path.
1844                vec![(FileType::Skill, mismatched_validator_factory)]
1845            }
1846            fn named_validators(&self) -> Vec<(FileType, Option<&'static str>, ValidatorFactory)> {
1847                vec![(
1848                    FileType::Skill,
1849                    Some("WrongName"),
1850                    mismatched_validator_factory,
1851                )]
1852            }
1853        }
1854
1855        let registry = ValidatorRegistry::builder()
1856            .with_provider(&MismatchedProvider)
1857            .without_validator("ActualName") // The real name - disable attempt
1858            .build();
1859
1860        // The validator slipped through: register_named() checked "WrongName"
1861        // against the disabled set (which contains "ActualName"), found no
1862        // match, called the factory, and registered the validator - even though
1863        // the user intended to disable it.
1864        let slipped_through = registry.validators_for(FileType::Skill);
1865        assert_eq!(
1866            slipped_through.len(),
1867            1,
1868            "Mismatched static name caused validator to slip through despite disable request"
1869        );
1870        assert_eq!(
1871            slipped_through[0].name(),
1872            "ActualName",
1873            "The registered validator must be the mismatched one"
1874        );
1875    }
1876
1877    // ---- Category provider tests ----
1878
1879    #[test]
1880    fn skill_provider_count() {
1881        assert_eq!(SkillProvider.named_validators().len(), 4);
1882    }
1883
1884    #[test]
1885    fn claude_provider_count() {
1886        assert_eq!(ClaudeProvider.named_validators().len(), 11);
1887    }
1888
1889    #[test]
1890    fn copilot_provider_count() {
1891        assert_eq!(CopilotProvider.named_validators().len(), 9);
1892    }
1893
1894    #[test]
1895    fn cursor_provider_count() {
1896        assert_eq!(CursorProvider.named_validators().len(), 9);
1897    }
1898
1899    #[test]
1900    fn gemini_provider_count() {
1901        assert_eq!(GeminiProvider.named_validators().len(), 8);
1902    }
1903
1904    #[test]
1905    fn roo_provider_count() {
1906        assert_eq!(RooProvider.named_validators().len(), 5);
1907    }
1908
1909    #[test]
1910    fn windsurf_provider_count() {
1911        assert_eq!(WindsurfProvider.named_validators().len(), 3);
1912    }
1913
1914    #[test]
1915    fn misc_provider_count() {
1916        assert_eq!(MiscProvider.named_validators().len(), 22);
1917    }
1918
1919    #[test]
1920    fn all_category_providers_sum_to_expected_count() {
1921        let total = SkillProvider.named_validators().len()
1922            + ClaudeProvider.named_validators().len()
1923            + CopilotProvider.named_validators().len()
1924            + CursorProvider.named_validators().len()
1925            + GeminiProvider.named_validators().len()
1926            + RooProvider.named_validators().len()
1927            + WindsurfProvider.named_validators().len()
1928            + MiscProvider.named_validators().len();
1929        assert_eq!(
1930            total, EXPECTED_BUILTIN_COUNT,
1931            "Sum of all category provider counts must equal EXPECTED_BUILTIN_COUNT"
1932        );
1933    }
1934
1935    #[test]
1936    fn all_category_provider_entries_have_names() {
1937        let providers: &[&dyn ValidatorProvider] = &[
1938            &SkillProvider,
1939            &ClaudeProvider,
1940            &CopilotProvider,
1941            &CursorProvider,
1942            &GeminiProvider,
1943            &RooProvider,
1944            &WindsurfProvider,
1945            &MiscProvider,
1946        ];
1947        for provider in providers {
1948            for (i, (ft, name, _)) in provider.named_validators().iter().enumerate() {
1949                assert!(
1950                    name.is_some(),
1951                    "{}: entry {i} ({ft:?}) should have Some(name), got None",
1952                    provider.name()
1953                );
1954            }
1955        }
1956    }
1957
1958    #[test]
1959    fn category_provider_validators_count_matches_named() {
1960        let providers: &[&dyn ValidatorProvider] = &[
1961            &SkillProvider,
1962            &ClaudeProvider,
1963            &CopilotProvider,
1964            &CursorProvider,
1965            &GeminiProvider,
1966            &RooProvider,
1967            &WindsurfProvider,
1968            &MiscProvider,
1969        ];
1970        for provider in providers {
1971            assert_eq!(
1972                provider.validators().len(),
1973                provider.named_validators().len(),
1974                "{}: validators() and named_validators() counts must match",
1975                provider.name()
1976            );
1977        }
1978    }
1979
1980    #[test]
1981    fn claude_provider_includes_codex_on_claude_md() {
1982        // CDX-003: CodexValidator on ClaudeMd catches AGENTS.override.md files.
1983        let entries = ClaudeProvider.named_validators();
1984        let has_codex_on_claude_md = entries
1985            .iter()
1986            .any(|(ft, name, _)| *ft == FileType::ClaudeMd && *name == Some("CodexValidator"));
1987        assert!(
1988            has_codex_on_claude_md,
1989            "ClaudeProvider must include CodexValidator on ClaudeMd (CDX-003)"
1990        );
1991    }
1992
1993    #[test]
1994    fn codex_validator_only_on_expected_file_types() {
1995        // CodexValidator must only appear on ClaudeMd (CDX-003 and CDX-AG-*).
1996        let entries = BuiltinProvider.named_validators();
1997        for (ft, name, _) in &entries {
1998            if *name == Some("CodexValidator") {
1999                assert!(
2000                    *ft == FileType::ClaudeMd,
2001                    "CodexValidator must only be registered for ClaudeMd, found {:?}",
2002                    ft
2003                );
2004            }
2005        }
2006        let codex_count = entries
2007            .iter()
2008            .filter(|(_, name, _)| *name == Some("CodexValidator"))
2009            .count();
2010        assert_eq!(
2011            codex_count, 1,
2012            "CodexValidator should appear exactly once (ClaudeMd)"
2013        );
2014    }
2015
2016    #[test]
2017    fn codex_config_validator_only_on_codex_config() {
2018        let entries = BuiltinProvider.named_validators();
2019        for (ft, name, _) in &entries {
2020            if *name == Some("CodexConfigValidator") {
2021                assert_eq!(
2022                    *ft,
2023                    FileType::CodexConfig,
2024                    "CodexConfigValidator must only be registered for CodexConfig"
2025                );
2026            }
2027        }
2028        let count = entries
2029            .iter()
2030            .filter(|(_, name, _)| *name == Some("CodexConfigValidator"))
2031            .count();
2032        assert_eq!(
2033            count, 1,
2034            "CodexConfigValidator should appear exactly once (CodexConfig)"
2035        );
2036    }
2037
2038    #[test]
2039    fn builder_second_build_has_no_disabled_validators() {
2040        // build() consumes the disabled set via mem::take. A second call
2041        // produces a registry where previously-disabled validators are active.
2042        let mut builder = ValidatorRegistry::builder();
2043        builder.with_defaults().without_validator("XmlValidator");
2044
2045        let first = builder.build();
2046        let second = builder.build();
2047
2048        // XmlValidator appears 9 times in the default set; first registry has them removed.
2049        let xml_count = BuiltinProvider
2050            .named_validators()
2051            .iter()
2052            .filter(|(_, name, _)| *name == Some("XmlValidator"))
2053            .count();
2054        assert_eq!(
2055            second.total_validator_count() - first.total_validator_count(),
2056            xml_count,
2057            "First registry should have exactly {xml_count} fewer validators (one per XmlValidator registration)"
2058        );
2059        // Second registry has the full count because the disabled set was consumed.
2060        assert_eq!(
2061            second.total_validator_count(),
2062            EXPECTED_BUILTIN_COUNT,
2063            "Second build() must produce a full registry (disabled set was consumed by first build)"
2064        );
2065    }
2066
2067    #[test]
2068    fn builtin_provider_output_matches_sub_provider_concatenation() {
2069        // BuiltinProvider::named_validators() must be a pure flat_map of all
2070        // 8 sub-providers in declaration order with no reordering or deduplication.
2071        let expected: Vec<_> = [
2072            SkillProvider.named_validators(),
2073            ClaudeProvider.named_validators(),
2074            CopilotProvider.named_validators(),
2075            CursorProvider.named_validators(),
2076            GeminiProvider.named_validators(),
2077            RooProvider.named_validators(),
2078            WindsurfProvider.named_validators(),
2079            MiscProvider.named_validators(),
2080        ]
2081        .into_iter()
2082        .flatten()
2083        .collect();
2084
2085        let actual = BuiltinProvider.named_validators();
2086
2087        assert_eq!(
2088            actual.len(),
2089            expected.len(),
2090            "BuiltinProvider entry count must equal sum of sub-providers"
2091        );
2092        for (i, ((aft, aname, _), (eft, ename, _))) in
2093            actual.iter().zip(expected.iter()).enumerate()
2094        {
2095            assert_eq!(
2096                aft, eft,
2097                "Entry {i}: file type mismatch (actual={aft:?}, expected={eft:?})"
2098            );
2099            assert_eq!(
2100                aname, ename,
2101                "Entry {i}: name mismatch (actual={aname:?}, expected={ename:?})"
2102            );
2103        }
2104    }
2105
2106    #[test]
2107    fn builder_with_defaults_called_twice_registers_all_validators_twice() {
2108        // with_defaults() is additive. Calling it twice duplicates every
2109        // built-in validator. This is the documented behaviour (see
2110        // ValidatorRegistryBuilder::with_defaults doc comment).
2111        let registry = ValidatorRegistry::builder()
2112            .with_defaults()
2113            .with_defaults()
2114            .build();
2115        assert_eq!(
2116            registry.total_validator_count(),
2117            EXPECTED_BUILTIN_COUNT * 2,
2118            "Calling with_defaults() twice must register all validators twice"
2119        );
2120    }
2121}