1#![forbid(unsafe_code)]
2
3use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SkillDescriptor {
18 pub name: String,
19 pub version: String,
20 pub dependencies: Vec<String>,
21}
22
23impl SkillDescriptor {
24 pub fn new(name: &str, version: &str) -> Self {
25 Self {
26 name: name.to_string(),
27 version: version.to_string(),
28 dependencies: Vec::new(),
29 }
30 }
31
32 pub fn with_dependency(mut self, dep: &str) -> Self {
33 self.dependencies.push(dep.to_string());
34 self
35 }
36
37 pub fn with_dependencies(mut self, deps: &[&str]) -> Self {
38 self.dependencies = deps.iter().map(|s| s.to_string()).collect();
39 self
40 }
41}
42
43#[derive(Debug, Clone)]
47pub struct Constellation {
48 pub name: String,
49 pub version: String,
50 pub description: String,
51 pub skills: Vec<SkillDescriptor>,
52}
53
54impl Constellation {
55 pub fn new(name: &str) -> Self {
56 Self {
57 name: name.to_string(),
58 version: "0.1.0".to_string(),
59 description: String::new(),
60 skills: Vec::new(),
61 }
62 }
63
64 pub fn skill_names(&self) -> Vec<&str> {
66 self.skills.iter().map(|s| s.name.as_str()).collect()
67 }
68
69 pub fn has_skill(&self, name: &str) -> bool {
71 self.skills.iter().any(|s| s.name == name)
72 }
73
74 pub fn unique_dependency_count(&self) -> usize {
76 let mut deps = HashSet::new();
77 for skill in &self.skills {
78 for dep in &skill.dependencies {
79 deps.insert(dep.clone());
80 }
81 }
82 deps.len()
83 }
84}
85
86#[derive(Debug)]
90pub struct ConstellationBuilder {
91 name: String,
92 version: String,
93 description: String,
94 skills: Vec<SkillDescriptor>,
95}
96
97impl ConstellationBuilder {
98 pub fn new(name: &str) -> Self {
99 Self {
100 name: name.to_string(),
101 version: "0.1.0".to_string(),
102 description: String::new(),
103 skills: Vec::new(),
104 }
105 }
106
107 pub fn version(mut self, v: &str) -> Self {
108 self.version = v.to_string();
109 self
110 }
111
112 pub fn description(mut self, desc: &str) -> Self {
113 self.description = desc.to_string();
114 self
115 }
116
117 pub fn add_skill(mut self, skill: SkillDescriptor) -> Self {
118 self.skills.push(skill);
119 self
120 }
121
122 pub fn build(self) -> Constellation {
123 Constellation {
124 name: self.name,
125 version: self.version,
126 description: self.description,
127 skills: self.skills,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct ResolutionError {
136 pub message: String,
137}
138
139impl ResolutionError {
140 pub fn new(msg: &str) -> Self {
141 Self { message: msg.to_string() }
142 }
143}
144
145impl std::fmt::Display for ResolutionError {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 write!(f, "ResolutionError: {}", self.message)
148 }
149}
150
151pub struct ConstellationResolver;
155
156impl ConstellationResolver {
157 pub fn collect_dependencies(constellation: &Constellation) -> HashMap<String, HashSet<String>> {
160 let mut dep_map: HashMap<String, HashSet<String>> = HashMap::new();
161 for skill in &constellation.skills {
162 for dep in &skill.dependencies {
163 dep_map.entry(dep.clone())
164 .or_default()
165 .insert(skill.name.clone());
166 }
167 }
168 dep_map
169 }
170
171 pub fn detect_conflicts(constellation: &Constellation) -> Vec<ResolutionError> {
174 let mut errors = Vec::new();
175
176 let mut seen = HashSet::new();
178 for skill in &constellation.skills {
179 if !seen.insert(&skill.name) {
180 errors.push(ResolutionError::new(&format!(
181 "Duplicate skill: {}", skill.name
182 )));
183 }
184 }
185
186 let skill_names: HashSet<&str> = constellation.skills.iter().map(|s| s.name.as_str()).collect();
188 for skill in &constellation.skills {
189 for dep in &skill.dependencies {
190 if skill_names.contains(dep.as_str()) {
191 if let Some(dep_skill) = constellation.skills.iter().find(|s| s.name == *dep) {
193 if dep_skill.dependencies.contains(&skill.name) {
194 errors.push(ResolutionError::new(&format!(
195 "Circular dependency: {} <-> {}", skill.name, dep
196 )));
197 }
198 }
199 }
200 }
201 }
202
203 errors
204 }
205
206 pub fn root_skills(constellation: &Constellation) -> Vec<&SkillDescriptor> {
208 let skill_names: HashSet<&str> = constellation.skills.iter().map(|s| s.name.as_str()).collect();
209 constellation.skills.iter()
210 .filter(|s| !s.dependencies.iter().any(|d| skill_names.contains(d.as_str())))
211 .collect()
212 }
213
214 pub fn dependency_order(constellation: &Constellation) -> Result<Vec<String>, ResolutionError> {
217 let skill_names: HashSet<&str> = constellation.skills.iter().map(|s| s.name.as_str()).collect();
218 let mut in_degree: HashMap<&str, usize> = HashMap::new();
219 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
220
221 for skill in &constellation.skills {
222 in_degree.entry(&skill.name).or_insert(0);
223 adj.entry(&skill.name).or_default();
224 }
225
226 for skill in &constellation.skills {
227 for dep in &skill.dependencies {
228 if skill_names.contains(dep.as_str()) {
229 *in_degree.entry(&skill.name).or_insert(0) += 1;
230 adj.entry(dep.as_str()).or_default().push(&skill.name);
231 }
232 }
233 }
234
235 let mut queue: Vec<&str> = in_degree.iter()
236 .filter(|(_, °)| deg == 0)
237 .map(|(&name, _)| name)
238 .collect();
239
240 let mut result = Vec::new();
241 while let Some(name) = queue.pop() {
242 result.push(name.to_string());
243 if let Some(neighbors) = adj.get(name) {
244 for &neighbor in neighbors {
245 if let Some(deg) = in_degree.get_mut(neighbor) {
246 *deg -= 1;
247 if *deg == 0 {
248 queue.push(neighbor);
249 }
250 }
251 }
252 }
253 }
254
255 if result.len() != constellation.skills.len() {
256 return Err(ResolutionError::new("Circular dependency detected in topological sort"));
257 }
258
259 Ok(result)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub enum DeploymentTarget {
268 Esp32,
270 Wasm,
272 Native,
274}
275
276impl std::fmt::Display for DeploymentTarget {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self {
279 DeploymentTarget::Esp32 => write!(f, "esp32"),
280 DeploymentTarget::Wasm => write!(f, "wasm"),
281 DeploymentTarget::Native => write!(f, "native"),
282 }
283 }
284}
285
286#[derive(Debug, Clone)]
290pub struct CompiledConstellation {
291 pub constellation_name: String,
292 pub target: DeploymentTarget,
293 pub skill_count: usize,
294 pub estimated_size_bytes: usize,
295 pub checksum: String,
296}
297
298pub struct ConstellationCompiler;
302
303impl ConstellationCompiler {
304 pub fn compile(
308 constellation: &Constellation,
309 target: DeploymentTarget,
310 ) -> Result<CompiledConstellation, ResolutionError> {
311 let conflicts = ConstellationResolver::detect_conflicts(constellation);
312 if !conflicts.is_empty() {
313 return Err(ResolutionError::new(&format!(
314 "Cannot compile: {} conflicts detected", conflicts.len()
315 )));
316 }
317
318 let base_size = match target {
320 DeploymentTarget::Esp32 => 4096,
321 DeploymentTarget::Wasm => 2048,
322 DeploymentTarget::Native => 8192,
323 };
324 let estimated_size = base_size * constellation.skills.len().max(1);
325
326 let mut hash_val: u64 = 0;
328 for byte in constellation.name.bytes() {
329 hash_val = hash_val.wrapping_mul(31).wrapping_add(byte as u64);
330 }
331 for skill in &constellation.skills {
332 for byte in skill.name.bytes() {
333 hash_val = hash_val.wrapping_mul(31).wrapping_add(byte as u64);
334 }
335 }
336 let checksum = format!("{:016x}", hash_val);
337
338 Ok(CompiledConstellation {
339 constellation_name: constellation.name.clone(),
340 target,
341 skill_count: constellation.skills.len(),
342 estimated_size_bytes: estimated_size,
343 checksum,
344 })
345 }
346}
347
348#[derive(Debug)]
352pub struct ConstellationRegistry {
353 constellations: HashMap<String, Constellation>,
354}
355
356impl ConstellationRegistry {
357 pub fn new() -> Self {
358 Self { constellations: HashMap::new() }
359 }
360
361 pub fn register(&mut self, constellation: Constellation) {
363 self.constellations.insert(constellation.name.clone(), constellation);
364 }
365
366 pub fn get(&self, name: &str) -> Option<&Constellation> {
368 self.constellations.get(name)
369 }
370
371 pub fn unregister(&mut self, name: &str) -> bool {
373 self.constellations.remove(name).is_some()
374 }
375
376 pub fn list_names(&self) -> Vec<&str> {
378 self.constellations.keys().map(|s| s.as_str()).collect()
379 }
380
381 pub fn find_by_skill(&self, skill_name: &str) -> Vec<&Constellation> {
383 self.constellations.values()
384 .filter(|c| c.has_skill(skill_name))
385 .collect()
386 }
387
388 pub fn count(&self) -> usize {
390 self.constellations.len()
391 }
392}
393
394impl Default for ConstellationRegistry {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_skill_descriptor_builder() {
408 let skill = SkillDescriptor::new("navigation", "1.0")
409 .with_dependencies(&["math", "sensor"]);
410 assert_eq!(skill.name, "navigation");
411 assert_eq!(skill.dependencies.len(), 2);
412 }
413
414 #[test]
415 fn test_constellation_new() {
416 let c = Constellation::new("room-alpha");
417 assert_eq!(c.name, "room-alpha");
418 assert!(c.skills.is_empty());
419 }
420
421 #[test]
422 fn test_constellation_has_skill() {
423 let mut c = Constellation::new("room-alpha");
424 c.skills.push(SkillDescriptor::new("nav", "1.0"));
425 assert!(c.has_skill("nav"));
426 assert!(!c.has_skill("comms"));
427 }
428
429 #[test]
430 fn test_constellation_skill_names() {
431 let mut c = Constellation::new("room-alpha");
432 c.skills.push(SkillDescriptor::new("nav", "1.0"));
433 c.skills.push(SkillDescriptor::new("comms", "1.0"));
434 assert_eq!(c.skill_names(), vec!["nav", "comms"]);
435 }
436
437 #[test]
438 fn test_builder_basic() {
439 let c = ConstellationBuilder::new("room-beta")
440 .version("2.0")
441 .description("Test room")
442 .add_skill(SkillDescriptor::new("nav", "1.0"))
443 .add_skill(SkillDescriptor::new("sensor", "1.0").with_dependency("nav"))
444 .build();
445 assert_eq!(c.name, "room-beta");
446 assert_eq!(c.version, "2.0");
447 assert_eq!(c.skills.len(), 2);
448 }
449
450 #[test]
451 fn test_resolver_collect_dependencies() {
452 let c = ConstellationBuilder::new("test")
453 .add_skill(SkillDescriptor::new("a", "1.0").with_dependencies(&["math", "log"]))
454 .add_skill(SkillDescriptor::new("b", "1.0").with_dependencies(&["math"]))
455 .build();
456 let deps = ConstellationResolver::collect_dependencies(&c);
457 assert_eq!(deps.len(), 2); assert_eq!(deps.get("math").unwrap().len(), 2); }
460
461 #[test]
462 fn test_resolver_no_conflicts() {
463 let c = ConstellationBuilder::new("test")
464 .add_skill(SkillDescriptor::new("a", "1.0"))
465 .add_skill(SkillDescriptor::new("b", "1.0"))
466 .build();
467 let conflicts = ConstellationResolver::detect_conflicts(&c);
468 assert!(conflicts.is_empty());
469 }
470
471 #[test]
472 fn test_resolver_duplicate_conflict() {
473 let c = ConstellationBuilder::new("test")
474 .add_skill(SkillDescriptor::new("dup", "1.0"))
475 .add_skill(SkillDescriptor::new("dup", "1.0"))
476 .build();
477 let conflicts = ConstellationResolver::detect_conflicts(&c);
478 assert_eq!(conflicts.len(), 1);
479 assert!(conflicts[0].message.contains("Duplicate"));
480 }
481
482 #[test]
483 fn test_resolver_circular_conflict() {
484 let c = ConstellationBuilder::new("test")
485 .add_skill(SkillDescriptor::new("a", "1.0").with_dependency("b"))
486 .add_skill(SkillDescriptor::new("b", "1.0").with_dependency("a"))
487 .build();
488 let conflicts = ConstellationResolver::detect_conflicts(&c);
489 assert!(conflicts.iter().any(|e| e.message.contains("Circular")));
490 }
491
492 #[test]
493 fn test_resolver_root_skills() {
494 let c = ConstellationBuilder::new("test")
495 .add_skill(SkillDescriptor::new("base", "1.0"))
496 .add_skill(SkillDescriptor::new("derived", "1.0").with_dependency("base"))
497 .build();
498 let roots = ConstellationResolver::root_skills(&c);
499 assert_eq!(roots.len(), 1);
500 assert_eq!(roots[0].name, "base");
501 }
502
503 #[test]
504 fn test_resolver_dependency_order() {
505 let c = ConstellationBuilder::new("test")
506 .add_skill(SkillDescriptor::new("c", "1.0").with_dependency("b"))
507 .add_skill(SkillDescriptor::new("b", "1.0").with_dependency("a"))
508 .add_skill(SkillDescriptor::new("a", "1.0"))
509 .build();
510 let order = ConstellationResolver::dependency_order(&c).unwrap();
511 let a_pos = order.iter().position(|n| n == "a").unwrap();
512 let b_pos = order.iter().position(|n| n == "b").unwrap();
513 let c_pos = order.iter().position(|n| n == "c").unwrap();
514 assert!(a_pos < b_pos);
515 assert!(b_pos < c_pos);
516 }
517
518 #[test]
519 fn test_resolver_dependency_order_cycle_fails() {
520 let c = ConstellationBuilder::new("test")
521 .add_skill(SkillDescriptor::new("a", "1.0").with_dependency("b"))
522 .add_skill(SkillDescriptor::new("b", "1.0").with_dependency("a"))
523 .build();
524 assert!(ConstellationResolver::dependency_order(&c).is_err());
525 }
526
527 #[test]
528 fn test_compiler_esp32() {
529 let c = ConstellationBuilder::new("room-x")
530 .add_skill(SkillDescriptor::new("nav", "1.0"))
531 .add_skill(SkillDescriptor::new("comms", "1.0"))
532 .build();
533 let compiled = ConstellationCompiler::compile(&c, DeploymentTarget::Esp32).unwrap();
534 assert_eq!(compiled.target, DeploymentTarget::Esp32);
535 assert_eq!(compiled.skill_count, 2);
536 assert!(compiled.estimated_size_bytes > 0);
537 }
538
539 #[test]
540 fn test_compiler_wasm() {
541 let c = ConstellationBuilder::new("room-y")
542 .add_skill(SkillDescriptor::new("skill", "1.0"))
543 .build();
544 let compiled = ConstellationCompiler::compile(&c, DeploymentTarget::Wasm).unwrap();
545 assert_eq!(compiled.target, DeploymentTarget::Wasm);
546 assert!(compiled.estimated_size_bytes < 100000); }
548
549 #[test]
550 fn test_compiler_conflict_fails() {
551 let c = ConstellationBuilder::new("bad")
552 .add_skill(SkillDescriptor::new("dup", "1.0"))
553 .add_skill(SkillDescriptor::new("dup", "1.0"))
554 .build();
555 assert!(ConstellationCompiler::compile(&c, DeploymentTarget::Native).is_err());
556 }
557
558 #[test]
559 fn test_registry_register_and_get() {
560 let mut reg = ConstellationRegistry::new();
561 let c = ConstellationBuilder::new("room-a")
562 .add_skill(SkillDescriptor::new("nav", "1.0"))
563 .build();
564 reg.register(c);
565 assert!(reg.get("room-a").is_some());
566 assert!(reg.get("room-b").is_none());
567 }
568
569 #[test]
570 fn test_registry_find_by_skill() {
571 let mut reg = ConstellationRegistry::new();
572 reg.register(ConstellationBuilder::new("room-a")
573 .add_skill(SkillDescriptor::new("nav", "1.0"))
574 .build());
575 reg.register(ConstellationBuilder::new("room-b")
576 .add_skill(SkillDescriptor::new("comms", "1.0"))
577 .build());
578 let found = reg.find_by_skill("nav");
579 assert_eq!(found.len(), 1);
580 assert_eq!(found[0].name, "room-a");
581 }
582
583 #[test]
584 fn test_registry_unregister() {
585 let mut reg = ConstellationRegistry::new();
586 reg.register(Constellation::new("room-x"));
587 assert!(reg.unregister("room-x"));
588 assert!(!reg.unregister("room-x"));
589 assert_eq!(reg.count(), 0);
590 }
591
592 #[test]
593 fn test_compiled_checksum_deterministic() {
594 let c = ConstellationBuilder::new("test")
595 .add_skill(SkillDescriptor::new("x", "1.0"))
596 .build();
597 let c1 = ConstellationCompiler::compile(&c, DeploymentTarget::Native).unwrap();
598 let c2 = ConstellationCompiler::compile(&c, DeploymentTarget::Native).unwrap();
599 assert_eq!(c1.checksum, c2.checksum);
600 }
601}