Skip to main content

xlog_logic/
module_diagnostics.rs

1//! Reusable diagnostics for XLOG module boundary audits.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5/// Role assigned to a module in a boundary audit.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ModuleRole {
8    /// Read-only kernel module whose predicates cannot be mutated by adapters.
9    FrozenKernel,
10    /// Adapter module that may add facts but not learned/core rules.
11    AdapterOnly,
12    /// Unrestricted module.
13    Regular,
14}
15
16/// Candidate provenance kind for leakage diagnostics.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum CandidateSourceKind {
19    /// Candidate came from training evidence.
20    TrainingEvidence,
21    /// Candidate came from a held-out label and must not drive induction.
22    HeldOutLabel,
23    /// Candidate was generated without label leakage.
24    GeneratedCandidate,
25}
26
27/// Module manifest used by diagnostics.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ModuleManifest {
30    /// Module name.
31    pub name: String,
32    /// Boundary role.
33    pub role: ModuleRole,
34    /// Predicates owned by this module.
35    pub predicates: BTreeSet<String>,
36    /// Whether this module represents held-out data.
37    pub held_out: bool,
38}
39
40impl ModuleManifest {
41    /// Construct a module manifest.
42    pub fn new<I, S>(name: impl Into<String>, role: ModuleRole, predicates: I) -> Self
43    where
44        I: IntoIterator<Item = S>,
45        S: Into<String>,
46    {
47        Self {
48            name: name.into(),
49            role,
50            predicates: predicates.into_iter().map(Into::into).collect(),
51            held_out: false,
52        }
53    }
54
55    /// Mark whether the module contains held-out data.
56    pub fn with_held_out(mut self, held_out: bool) -> Self {
57        self.held_out = held_out;
58        self
59    }
60}
61
62/// Kind of module declaration.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum ModuleDeclarationKind {
65    /// Fact-only declaration.
66    Fact,
67    /// Rule declaration.
68    Rule,
69    /// Candidate provenance declaration.
70    CandidateSource(CandidateSourceKind),
71}
72
73/// Declaration observed during module-boundary analysis.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ModuleDeclaration {
76    /// Module containing the declaration.
77    pub module: String,
78    /// Predicate named by the declaration.
79    pub predicate: String,
80    /// Declaration kind.
81    pub kind: ModuleDeclarationKind,
82}
83
84impl ModuleDeclaration {
85    /// Fact declaration.
86    pub fn fact(module: impl Into<String>, predicate: impl Into<String>) -> Self {
87        Self {
88            module: module.into(),
89            predicate: predicate.into(),
90            kind: ModuleDeclarationKind::Fact,
91        }
92    }
93
94    /// Rule declaration.
95    pub fn rule(module: impl Into<String>, predicate: impl Into<String>) -> Self {
96        Self {
97            module: module.into(),
98            predicate: predicate.into(),
99            kind: ModuleDeclarationKind::Rule,
100        }
101    }
102
103    /// Candidate provenance declaration.
104    pub fn candidate_source(
105        module: impl Into<String>,
106        predicate: impl Into<String>,
107        source: CandidateSourceKind,
108    ) -> Self {
109        Self {
110            module: module.into(),
111            predicate: predicate.into(),
112            kind: ModuleDeclarationKind::CandidateSource(source),
113        }
114    }
115}
116
117/// Input to the module boundary diagnostic pass.
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct ModuleBoundaryInput {
120    /// Module manifests.
121    pub modules: Vec<ModuleManifest>,
122    /// Declarations to audit.
123    pub declarations: Vec<ModuleDeclaration>,
124}
125
126/// Violation kind emitted by module boundary diagnostics.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub enum ModuleViolationKind {
129    /// A non-kernel module attempted to define a frozen kernel predicate.
130    FrozenKernelMutation,
131    /// An adapter-only module declared a rule.
132    AdapterRuleDefinition,
133    /// Held-out labels were used as candidate provenance.
134    HeldOutLeakage,
135    /// A declaration referenced an unknown module.
136    UnknownModule,
137}
138
139/// One module diagnostic violation.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct ModuleViolation {
142    /// Violation kind.
143    pub kind: ModuleViolationKind,
144    /// Module where the violation occurred.
145    pub module: String,
146    /// Predicate involved in the violation.
147    pub predicate: String,
148    /// Human-readable diagnostic detail.
149    pub detail: String,
150}
151
152/// Module boundary diagnostic report.
153#[derive(Clone, Debug, Default, PartialEq, Eq)]
154pub struct ModuleBoundaryReport {
155    /// Violations found by the audit.
156    pub violations: Vec<ModuleViolation>,
157}
158
159impl ModuleBoundaryReport {
160    /// Whether the audited boundary passed.
161    pub fn passed(&self) -> bool {
162        self.violations.is_empty()
163    }
164}
165
166/// Diagnose frozen-kernel, adapter-only, and held-out leakage boundaries.
167pub fn diagnose_module_boundaries(input: ModuleBoundaryInput) -> ModuleBoundaryReport {
168    let modules: BTreeMap<String, ModuleManifest> = input
169        .modules
170        .into_iter()
171        .map(|module| (module.name.clone(), module))
172        .collect();
173    let frozen_predicates: BTreeSet<String> = modules
174        .values()
175        .filter(|module| module.role == ModuleRole::FrozenKernel)
176        .flat_map(|module| module.predicates.iter().cloned())
177        .collect();
178
179    let mut violations = Vec::new();
180    for declaration in input.declarations {
181        let Some(module) = modules.get(&declaration.module) else {
182            violations.push(ModuleViolation {
183                kind: ModuleViolationKind::UnknownModule,
184                module: declaration.module,
185                predicate: declaration.predicate,
186                detail: "declaration references an unknown module".to_string(),
187            });
188            continue;
189        };
190
191        if module.role != ModuleRole::FrozenKernel
192            && frozen_predicates.contains(&declaration.predicate)
193        {
194            violations.push(ModuleViolation {
195                kind: ModuleViolationKind::FrozenKernelMutation,
196                module: declaration.module.clone(),
197                predicate: declaration.predicate.clone(),
198                detail: "adapter attempted to define a frozen kernel predicate".to_string(),
199            });
200        }
201
202        if module.role == ModuleRole::AdapterOnly
203            && matches!(declaration.kind, ModuleDeclarationKind::Rule)
204        {
205            violations.push(ModuleViolation {
206                kind: ModuleViolationKind::AdapterRuleDefinition,
207                module: declaration.module.clone(),
208                predicate: declaration.predicate.clone(),
209                detail: "adapter-only module declared a rule".to_string(),
210            });
211        }
212
213        if module.held_out
214            && matches!(
215                declaration.kind,
216                ModuleDeclarationKind::CandidateSource(CandidateSourceKind::HeldOutLabel)
217            )
218        {
219            violations.push(ModuleViolation {
220                kind: ModuleViolationKind::HeldOutLeakage,
221                module: declaration.module,
222                predicate: declaration.predicate,
223                detail: "held-out label used as candidate provenance".to_string(),
224            });
225        }
226    }
227
228    ModuleBoundaryReport { violations }
229}