1use serde::{Deserialize, Serialize};
13
14use crate::energy::EnergyModel;
15use crate::residual::{CorrectionDirection, ResidualClass, ResidualEvent};
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct DomainId(pub String);
20
21impl DomainId {
22 pub fn new(id: impl Into<String>) -> Self {
23 Self(id.into())
24 }
25}
26
27impl std::fmt::Display for DomainId {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.write_str(&self.0)
30 }
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
37pub struct WorkspaceSnapshot {
38 pub root: String,
39 pub files: Vec<String>,
41}
42
43impl WorkspaceSnapshot {
44 pub fn new(root: impl Into<String>, files: Vec<String>) -> Self {
45 Self {
46 root: root.into(),
47 files,
48 }
49 }
50
51 pub fn has_file_named(&self, name: &str) -> bool {
53 self.files
54 .iter()
55 .any(|f| f == name || f.ends_with(&format!("/{name}")))
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61pub struct DomainDetection {
62 pub domain: DomainId,
63 pub activated: bool,
64 pub confidence: f64,
66 pub evidence: Vec<String>,
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
71pub struct DomainScope {
72 pub label: String,
73 pub paths: Vec<String>,
74}
75
76#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
80pub struct ResidualSchema {
81 pub classes: Vec<ResidualClass>,
82}
83
84impl ResidualSchema {
85 pub fn new(classes: Vec<ResidualClass>) -> Self {
86 Self { classes }
87 }
88
89 pub fn allows(&self, class: ResidualClass) -> bool {
90 self.classes.contains(&class)
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct VerificationCadence {
104 pub max_mutations_between_checks: u32,
106 pub high_risk_mutations_between_checks: u32,
108}
109
110impl VerificationCadence {
111 pub fn validate(&self) -> Result<(), crate::error::SdkError> {
113 if self.max_mutations_between_checks == 0 || self.high_risk_mutations_between_checks == 0 {
114 return Err(crate::error::SdkError::Domain(
115 "verification cadence bounds must be at least 1".into(),
116 ));
117 }
118 Ok(())
119 }
120}
121
122impl Default for VerificationCadence {
123 fn default() -> Self {
124 Self {
125 max_mutations_between_checks: 4,
126 high_risk_mutations_between_checks: 1,
127 }
128 }
129}
130
131#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
135pub struct HardGatePolicy {
136 pub required_stages: Vec<String>,
138}
139
140#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
142pub struct VerifierSuiteSpec {
143 pub stages: Vec<String>,
145 pub cadence: VerificationCadence,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150pub struct BarrierChannel {
151 pub name: String,
152 pub policy_version: String,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157pub struct BarrierSpec {
158 pub channels: Vec<BarrierChannel>,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "snake_case")]
165pub enum SafetyBarrierDisposition {
166 NotClaimed { reason: String },
169 Registered(BarrierSpec),
171}
172
173pub trait AgentDomainPackage: Send + Sync {
180 fn domain_id(&self) -> DomainId;
182
183 fn detect(&self, workspace: &WorkspaceSnapshot) -> DomainDetection;
185
186 fn residual_schema(&self, scope: &DomainScope) -> ResidualSchema;
188
189 fn energy_model(&self, scope: &DomainScope) -> EnergyModel;
191
192 fn correction_directions(&self, residuals: &[ResidualEvent]) -> Vec<CorrectionDirection>;
196
197 fn tool_entries(&self, scope: &DomainScope) -> Vec<crate::toolset::ToolEntry>;
203
204 fn hard_gate_policy(&self, scope: &DomainScope) -> HardGatePolicy;
206
207 fn verifier_suite(&self, scope: &DomainScope) -> VerifierSuiteSpec;
209
210 fn safety_barrier(&self, scope: &DomainScope) -> SafetyBarrierDisposition;
213
214 fn proxy_geometry(&self, _scope: &DomainScope) -> Option<&dyn crate::stability::ProxyGeometry> {
217 None
218 }
219
220 fn prompt_library(&self) -> &dyn DomainPromptLibrary {
225 &EMPTY_PROMPT_LIBRARY
226 }
227
228 fn search_strategies(
231 &self,
232 _context: &crate::search::SearchContext,
233 ) -> Vec<crate::search::SearchStrategy> {
234 Vec::new()
235 }
236
237 fn correction_packet(
241 &self,
242 _residuals: &[crate::residual::ResidualEvent],
243 ) -> Option<crate::residual::CorrectionPacket> {
244 None
245 }
246
247 fn compare_branch_measurements(
251 &self,
252 _root: &crate::search::DomainMeasurement,
253 _candidates: &[crate::search::BranchMeasurement],
254 ) -> crate::search::BranchSelection {
255 crate::search::BranchSelection::NoneEligible
256 }
257}
258
259pub trait DomainPromptLibrary: Send + Sync {
262 fn stage_templates(&self, stage: &str) -> Vec<crate::prompt::SectionTemplate> {
264 let _ = stage;
265 Vec::new()
266 }
267}
268
269#[derive(Debug)]
271pub struct EmptyDomainPromptLibrary;
272
273impl DomainPromptLibrary for EmptyDomainPromptLibrary {}
274
275static EMPTY_PROMPT_LIBRARY: EmptyDomainPromptLibrary = EmptyDomainPromptLibrary;
276
277#[derive(Default)]
282pub struct DomainRegistry {
283 packages: Vec<std::sync::Arc<dyn AgentDomainPackage>>,
284}
285
286impl std::fmt::Debug for DomainRegistry {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 f.debug_struct("DomainRegistry")
289 .field(
290 "domains",
291 &self
292 .packages
293 .iter()
294 .map(|p| p.domain_id())
295 .collect::<Vec<_>>(),
296 )
297 .finish()
298 }
299}
300
301impl DomainRegistry {
302 pub fn new() -> Self {
303 Self::default()
304 }
305
306 pub fn register(&mut self, package: std::sync::Arc<dyn AgentDomainPackage>) {
309 self.packages.push(package);
310 }
311
312 pub fn len(&self) -> usize {
313 self.packages.len()
314 }
315
316 pub fn is_empty(&self) -> bool {
317 self.packages.is_empty()
318 }
319
320 pub fn domain_ids(&self) -> Vec<DomainId> {
322 self.packages.iter().map(|p| p.domain_id()).collect()
323 }
324
325 pub fn by_id(&self, id: &DomainId) -> Option<std::sync::Arc<dyn AgentDomainPackage>> {
327 self.packages.iter().find(|p| &p.domain_id() == id).cloned()
328 }
329
330 pub fn detect_best(
332 &self,
333 workspace: &WorkspaceSnapshot,
334 ) -> Option<std::sync::Arc<dyn AgentDomainPackage>> {
335 self.packages
336 .iter()
337 .map(|p| (p, p.detect(workspace)))
338 .filter(|(_, d)| d.activated)
339 .max_by(|(_, a), (_, b)| {
340 a.confidence
341 .partial_cmp(&b.confidence)
342 .unwrap_or(std::cmp::Ordering::Equal)
343 })
344 .map(|(p, _)| p.clone())
345 }
346
347 pub fn select(
349 &self,
350 explicit: Option<&DomainId>,
351 workspace: &WorkspaceSnapshot,
352 ) -> Option<std::sync::Arc<dyn AgentDomainPackage>> {
353 match explicit {
354 Some(id) => self.by_id(id),
355 None => self.detect_best(workspace),
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::energy::EnergyModel;
364
365 struct StubDomain {
366 id: &'static str,
367 marker: &'static str,
368 confidence: f64,
369 }
370
371 impl AgentDomainPackage for StubDomain {
372 fn domain_id(&self) -> DomainId {
373 DomainId::new(self.id)
374 }
375 fn detect(&self, ws: &WorkspaceSnapshot) -> DomainDetection {
376 let activated = ws.has_file_named(self.marker);
377 DomainDetection {
378 domain: self.domain_id(),
379 activated,
380 confidence: if activated { self.confidence } else { 0.0 },
381 evidence: vec![],
382 }
383 }
384 fn residual_schema(&self, _: &DomainScope) -> ResidualSchema {
385 ResidualSchema::new(vec![])
386 }
387 fn energy_model(&self, _: &DomainScope) -> EnergyModel {
388 EnergyModel::new(self.id, 0.5)
389 }
390 fn correction_directions(&self, _: &[ResidualEvent]) -> Vec<CorrectionDirection> {
391 vec![]
392 }
393 fn tool_entries(&self, _: &DomainScope) -> Vec<crate::toolset::ToolEntry> {
394 vec![]
395 }
396 fn hard_gate_policy(&self, _: &DomainScope) -> HardGatePolicy {
397 HardGatePolicy::default()
398 }
399 fn verifier_suite(&self, _: &DomainScope) -> VerifierSuiteSpec {
400 VerifierSuiteSpec::default()
401 }
402 fn safety_barrier(&self, _: &DomainScope) -> SafetyBarrierDisposition {
403 SafetyBarrierDisposition::NotClaimed {
404 reason: "stub".into(),
405 }
406 }
407 }
408
409 fn registry() -> DomainRegistry {
410 let mut r = DomainRegistry::new();
411 r.register(std::sync::Arc::new(StubDomain {
412 id: "coding",
413 marker: "Cargo.toml",
414 confidence: 0.9,
415 }));
416 r.register(std::sync::Arc::new(StubDomain {
417 id: "research",
418 marker: "refs.bib",
419 confidence: 0.8,
420 }));
421 r
422 }
423
424 #[test]
425 fn explicit_selection_wins() {
426 let r = registry();
427 let ws = WorkspaceSnapshot::new("/r", vec!["Cargo.toml".into(), "refs.bib".into()]);
428 let chosen = r.select(Some(&DomainId::new("research")), &ws).unwrap();
429 assert_eq!(chosen.domain_id(), DomainId::new("research"));
430 }
431
432 #[test]
433 fn detection_selects_best_when_no_explicit() {
434 let r = registry();
435 let ws = WorkspaceSnapshot::new("/r", vec!["refs.bib".into()]);
436 let chosen = r.select(None, &ws).unwrap();
437 assert_eq!(chosen.domain_id(), DomainId::new("research"));
438 }
439
440 #[test]
441 fn registry_admits_multiple_domains() {
442 let r = registry();
443 assert_eq!(r.len(), 2);
444 assert!(r.by_id(&DomainId::new("coding")).is_some());
445 assert!(r.by_id(&DomainId::new("missing")).is_none());
446 }
447}