use serde::{Deserialize, Serialize};
use crate::energy::EnergyModel;
use crate::residual::{CorrectionDirection, ResidualClass, ResidualEvent};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DomainId(pub String);
impl DomainId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for DomainId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct WorkspaceSnapshot {
pub root: String,
pub files: Vec<String>,
}
impl WorkspaceSnapshot {
pub fn new(root: impl Into<String>, files: Vec<String>) -> Self {
Self {
root: root.into(),
files,
}
}
pub fn has_file_named(&self, name: &str) -> bool {
self.files
.iter()
.any(|f| f == name || f.ends_with(&format!("/{name}")))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DomainDetection {
pub domain: DomainId,
pub activated: bool,
pub confidence: f64,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DomainScope {
pub label: String,
pub paths: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ResidualSchema {
pub classes: Vec<ResidualClass>,
}
impl ResidualSchema {
pub fn new(classes: Vec<ResidualClass>) -> Self {
Self { classes }
}
pub fn allows(&self, class: ResidualClass) -> bool {
self.classes.contains(&class)
}
}
pub trait AgentDomainPackage: Send + Sync {
fn domain_id(&self) -> DomainId;
fn detect(&self, workspace: &WorkspaceSnapshot) -> DomainDetection;
fn residual_schema(&self, scope: &DomainScope) -> ResidualSchema;
fn energy_model(&self, scope: &DomainScope) -> EnergyModel;
fn correction_directions(&self, residuals: &[ResidualEvent]) -> Vec<CorrectionDirection>;
}
#[derive(Default)]
pub struct DomainRegistry {
packages: Vec<Box<dyn AgentDomainPackage>>,
}
impl std::fmt::Debug for DomainRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DomainRegistry")
.field(
"domains",
&self
.packages
.iter()
.map(|p| p.domain_id())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl DomainRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, package: Box<dyn AgentDomainPackage>) {
self.packages.push(package);
}
pub fn len(&self) -> usize {
self.packages.len()
}
pub fn is_empty(&self) -> bool {
self.packages.is_empty()
}
pub fn domain_ids(&self) -> Vec<DomainId> {
self.packages.iter().map(|p| p.domain_id()).collect()
}
pub fn by_id(&self, id: &DomainId) -> Option<&dyn AgentDomainPackage> {
self.packages
.iter()
.find(|p| &p.domain_id() == id)
.map(|p| p.as_ref())
}
pub fn detect_best(&self, workspace: &WorkspaceSnapshot) -> Option<&dyn AgentDomainPackage> {
self.packages
.iter()
.map(|p| (p, p.detect(workspace)))
.filter(|(_, d)| d.activated)
.max_by(|(_, a), (_, b)| {
a.confidence
.partial_cmp(&b.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(p, _)| p.as_ref())
}
pub fn select(
&self,
explicit: Option<&DomainId>,
workspace: &WorkspaceSnapshot,
) -> Option<&dyn AgentDomainPackage> {
match explicit {
Some(id) => self.by_id(id),
None => self.detect_best(workspace),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::energy::EnergyModel;
struct StubDomain {
id: &'static str,
marker: &'static str,
confidence: f64,
}
impl AgentDomainPackage for StubDomain {
fn domain_id(&self) -> DomainId {
DomainId::new(self.id)
}
fn detect(&self, ws: &WorkspaceSnapshot) -> DomainDetection {
let activated = ws.has_file_named(self.marker);
DomainDetection {
domain: self.domain_id(),
activated,
confidence: if activated { self.confidence } else { 0.0 },
evidence: vec![],
}
}
fn residual_schema(&self, _: &DomainScope) -> ResidualSchema {
ResidualSchema::new(vec![])
}
fn energy_model(&self, _: &DomainScope) -> EnergyModel {
EnergyModel::new(self.id, 0.5)
}
fn correction_directions(&self, _: &[ResidualEvent]) -> Vec<CorrectionDirection> {
vec![]
}
}
fn registry() -> DomainRegistry {
let mut r = DomainRegistry::new();
r.register(Box::new(StubDomain {
id: "coding",
marker: "Cargo.toml",
confidence: 0.9,
}));
r.register(Box::new(StubDomain {
id: "research",
marker: "refs.bib",
confidence: 0.8,
}));
r
}
#[test]
fn explicit_selection_wins() {
let r = registry();
let ws = WorkspaceSnapshot::new("/r", vec!["Cargo.toml".into(), "refs.bib".into()]);
let chosen = r.select(Some(&DomainId::new("research")), &ws).unwrap();
assert_eq!(chosen.domain_id(), DomainId::new("research"));
}
#[test]
fn detection_selects_best_when_no_explicit() {
let r = registry();
let ws = WorkspaceSnapshot::new("/r", vec!["refs.bib".into()]);
let chosen = r.select(None, &ws).unwrap();
assert_eq!(chosen.domain_id(), DomainId::new("research"));
}
#[test]
fn registry_admits_multiple_domains() {
let r = registry();
assert_eq!(r.len(), 2);
assert!(r.by_id(&DomainId::new("coding")).is_some());
assert!(r.by_id(&DomainId::new("missing")).is_none());
}
}