1use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ModuleRole {
8 FrozenKernel,
10 AdapterOnly,
12 Regular,
14}
15
16#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum CandidateSourceKind {
19 TrainingEvidence,
21 HeldOutLabel,
23 GeneratedCandidate,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ModuleManifest {
30 pub name: String,
32 pub role: ModuleRole,
34 pub predicates: BTreeSet<String>,
36 pub held_out: bool,
38}
39
40impl ModuleManifest {
41 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 pub fn with_held_out(mut self, held_out: bool) -> Self {
57 self.held_out = held_out;
58 self
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub enum ModuleDeclarationKind {
65 Fact,
67 Rule,
69 CandidateSource(CandidateSourceKind),
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ModuleDeclaration {
76 pub module: String,
78 pub predicate: String,
80 pub kind: ModuleDeclarationKind,
82}
83
84impl ModuleDeclaration {
85 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct ModuleBoundaryInput {
120 pub modules: Vec<ModuleManifest>,
122 pub declarations: Vec<ModuleDeclaration>,
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
128pub enum ModuleViolationKind {
129 FrozenKernelMutation,
131 AdapterRuleDefinition,
133 HeldOutLeakage,
135 UnknownModule,
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct ModuleViolation {
142 pub kind: ModuleViolationKind,
144 pub module: String,
146 pub predicate: String,
148 pub detail: String,
150}
151
152#[derive(Clone, Debug, Default, PartialEq, Eq)]
154pub struct ModuleBoundaryReport {
155 pub violations: Vec<ModuleViolation>,
157}
158
159impl ModuleBoundaryReport {
160 pub fn passed(&self) -> bool {
162 self.violations.is_empty()
163 }
164}
165
166pub 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}