use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModuleRole {
FrozenKernel,
AdapterOnly,
Regular,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CandidateSourceKind {
TrainingEvidence,
HeldOutLabel,
GeneratedCandidate,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleManifest {
pub name: String,
pub role: ModuleRole,
pub predicates: BTreeSet<String>,
pub held_out: bool,
}
impl ModuleManifest {
pub fn new<I, S>(name: impl Into<String>, role: ModuleRole, predicates: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
name: name.into(),
role,
predicates: predicates.into_iter().map(Into::into).collect(),
held_out: false,
}
}
pub fn with_held_out(mut self, held_out: bool) -> Self {
self.held_out = held_out;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModuleDeclarationKind {
Fact,
Rule,
CandidateSource(CandidateSourceKind),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleDeclaration {
pub module: String,
pub predicate: String,
pub kind: ModuleDeclarationKind,
}
impl ModuleDeclaration {
pub fn fact(module: impl Into<String>, predicate: impl Into<String>) -> Self {
Self {
module: module.into(),
predicate: predicate.into(),
kind: ModuleDeclarationKind::Fact,
}
}
pub fn rule(module: impl Into<String>, predicate: impl Into<String>) -> Self {
Self {
module: module.into(),
predicate: predicate.into(),
kind: ModuleDeclarationKind::Rule,
}
}
pub fn candidate_source(
module: impl Into<String>,
predicate: impl Into<String>,
source: CandidateSourceKind,
) -> Self {
Self {
module: module.into(),
predicate: predicate.into(),
kind: ModuleDeclarationKind::CandidateSource(source),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleBoundaryInput {
pub modules: Vec<ModuleManifest>,
pub declarations: Vec<ModuleDeclaration>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ModuleViolationKind {
FrozenKernelMutation,
AdapterRuleDefinition,
HeldOutLeakage,
UnknownModule,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModuleViolation {
pub kind: ModuleViolationKind,
pub module: String,
pub predicate: String,
pub detail: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ModuleBoundaryReport {
pub violations: Vec<ModuleViolation>,
}
impl ModuleBoundaryReport {
pub fn passed(&self) -> bool {
self.violations.is_empty()
}
}
pub fn diagnose_module_boundaries(input: ModuleBoundaryInput) -> ModuleBoundaryReport {
let modules: BTreeMap<String, ModuleManifest> = input
.modules
.into_iter()
.map(|module| (module.name.clone(), module))
.collect();
let frozen_predicates: BTreeSet<String> = modules
.values()
.filter(|module| module.role == ModuleRole::FrozenKernel)
.flat_map(|module| module.predicates.iter().cloned())
.collect();
let mut violations = Vec::new();
for declaration in input.declarations {
let Some(module) = modules.get(&declaration.module) else {
violations.push(ModuleViolation {
kind: ModuleViolationKind::UnknownModule,
module: declaration.module,
predicate: declaration.predicate,
detail: "declaration references an unknown module".to_string(),
});
continue;
};
if module.role != ModuleRole::FrozenKernel
&& frozen_predicates.contains(&declaration.predicate)
{
violations.push(ModuleViolation {
kind: ModuleViolationKind::FrozenKernelMutation,
module: declaration.module.clone(),
predicate: declaration.predicate.clone(),
detail: "adapter attempted to define a frozen kernel predicate".to_string(),
});
}
if module.role == ModuleRole::AdapterOnly
&& matches!(declaration.kind, ModuleDeclarationKind::Rule)
{
violations.push(ModuleViolation {
kind: ModuleViolationKind::AdapterRuleDefinition,
module: declaration.module.clone(),
predicate: declaration.predicate.clone(),
detail: "adapter-only module declared a rule".to_string(),
});
}
if module.held_out
&& matches!(
declaration.kind,
ModuleDeclarationKind::CandidateSource(CandidateSourceKind::HeldOutLabel)
)
{
violations.push(ModuleViolation {
kind: ModuleViolationKind::HeldOutLeakage,
module: declaration.module,
predicate: declaration.predicate,
detail: "held-out label used as candidate provenance".to_string(),
});
}
}
ModuleBoundaryReport { violations }
}