Skip to main content

knowledge_base_validation/
additional_validator.rs

1use crate::{Diagnostic, ValidationLayer, diagnostic, validator};
2use knowledge_base_snapshot::RepositorySnapshot;
3use std::path::{Path, PathBuf};
4
5/// The single immutable repository view shared by composed domain validators.
6///
7/// The snapshot contains canonical structured resources. Entity-context Markdown
8/// remains part of generic validation and is deliberately not included here.
9#[derive(Clone, Copy, Debug)]
10pub struct ValidationContext<'a> {
11    repository_root: &'a Path,
12    snapshot: &'a RepositorySnapshot,
13}
14
15impl<'a> ValidationContext<'a> {
16    fn new(repository_root: &'a Path, snapshot: &'a RepositorySnapshot) -> Self {
17        Self { repository_root, snapshot }
18    }
19
20    /// The root used for this validation pass.
21    pub fn repository_root(&self) -> &'a Path {
22        self.repository_root
23    }
24
25    /// The shared, read-only structured repository snapshot.
26    pub fn snapshot(&self) -> &'a RepositorySnapshot {
27        self.snapshot
28    }
29}
30
31/// A domain-specific, read-only validator for a complete knowledge-base repository.
32///
33/// Diagnostics must use paths relative to `repository`, so they remain meaningful when
34/// mutations validate a temporary staged copy.
35pub trait KnowledgeBaseValidator: Send + Sync {
36    fn validate(&self, context: &ValidationContext<'_>) -> Vec<Diagnostic>;
37}
38
39impl<F> KnowledgeBaseValidator for F
40where
41    F: for<'a> Fn(&ValidationContext<'a>) -> Vec<Diagnostic> + Send + Sync,
42{
43    fn validate(&self, context: &ValidationContext<'_>) -> Vec<Diagnostic> {
44        self(context)
45    }
46}
47
48/// Validates a repository with the built-in rules followed by every supplied domain validator.
49///
50/// All validators run even when another validator reports diagnostics. The returned diagnostics
51/// are sorted deterministically by path, line, identifier, message, and validation layer.
52pub fn validate_repository_with<'a>(root: impl AsRef<Path>, validators: impl IntoIterator<Item = &'a dyn KnowledgeBaseValidator>) -> Vec<Diagnostic> {
53    let root = root.as_ref();
54    let mut diagnostics = validator::validate_repository(root);
55    let validators = validators.into_iter().collect::<Vec<_>>();
56    if !validators.is_empty() {
57        match RepositorySnapshot::load(root) {
58            Ok(snapshot) => {
59                let context = ValidationContext::new(root, &snapshot);
60                for validator in validators {
61                    diagnostics.extend(validator.validate(&context));
62                }
63            }
64            Err(error) if diagnostics.is_empty() => {
65                let path = error.path().strip_prefix(root).map(PathBuf::from).unwrap_or_else(|_| error.path().to_path_buf());
66                diagnostics.push(Diagnostic {
67                    layer: ValidationLayer::Schema,
68                    path,
69                    line: None,
70                    identifier: None,
71                    message: format!("cannot load shared repository snapshot: {error}"),
72                });
73            }
74            Err(_) => {}
75        }
76    }
77    diagnostic::sort_diagnostics(&mut diagnostics);
78    diagnostics
79}