1use organism_pack::{
9 AgentMeta, CapabilityRequirement, ContextKey, IntentBinding, IntentPacket, IntentResolver,
10 InvariantClass, InvariantMeta, PackProfile, PackRequirement, ResolutionLevel, Reversibility,
11};
12
13#[derive(Debug, Clone)]
16pub struct RegisteredPack {
17 pub name: String,
18 pub description: String,
19 pub fact_prefixes: Vec<String>,
20 pub agent_names: Vec<String>,
21 pub invariant_names: Vec<String>,
22 pub agent_count: usize,
23 pub invariant_count: usize,
24 pub context_keys_read: Vec<ContextKey>,
25 pub context_keys_written: Vec<ContextKey>,
26 pub has_acceptance_invariants: bool,
27 pub profile: PackProfile,
28}
29
30#[derive(Debug, Clone)]
31pub struct RegisteredCapability {
32 pub name: String,
33 pub description: String,
34}
35
36#[derive(Debug, Clone, Default)]
39pub struct Registry {
40 packs: Vec<RegisteredPack>,
41 capabilities: Vec<RegisteredCapability>,
42}
43
44impl Registry {
45 #[must_use]
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn register_pack_with_profile_if_missing(
54 &mut self,
55 name: &'static str,
56 agents: &[AgentMeta],
57 invariants: &[InvariantMeta],
58 profile: &PackProfile,
59 ) {
60 if self.packs.iter().any(|pack| pack.name == name) {
61 return;
62 }
63
64 self.register_pack_with_profile(name, agents, invariants, profile);
65 }
66
67 pub fn register_pack_with_profile(
69 &mut self,
70 name: impl Into<String>,
71 agents: &[AgentMeta],
72 invariants: &[InvariantMeta],
73 profile: &PackProfile,
74 ) {
75 let name = name.into();
76 let fact_prefixes: Vec<String> = agents
77 .iter()
78 .map(|a| a.fact_prefix.to_string())
79 .collect::<std::collections::HashSet<_>>()
80 .into_iter()
81 .collect();
82 let agent_names = agents.iter().map(|a| a.name.to_string()).collect();
83 let invariant_names = invariants.iter().map(|i| i.name.to_string()).collect();
84 let description = agents
85 .iter()
86 .map(|a| a.description)
87 .collect::<Vec<_>>()
88 .join("; ");
89
90 let context_keys_read: Vec<ContextKey> = agents
91 .iter()
92 .flat_map(|a| a.dependencies.iter().copied())
93 .collect::<std::collections::HashSet<_>>()
94 .into_iter()
95 .collect();
96 let context_keys_written: Vec<ContextKey> = agents
97 .iter()
98 .map(|a| a.target_key)
99 .collect::<std::collections::HashSet<_>>()
100 .into_iter()
101 .collect();
102 let has_acceptance_invariants = invariants
103 .iter()
104 .any(|i| i.class == InvariantClass::Acceptance);
105
106 self.packs.push(RegisteredPack {
107 name,
108 description,
109 fact_prefixes,
110 agent_names,
111 invariant_names,
112 agent_count: agents.len(),
113 invariant_count: invariants.len(),
114 context_keys_read,
115 context_keys_written,
116 has_acceptance_invariants,
117 profile: profile.clone(),
118 });
119 }
120
121 pub fn register_pack(
123 &mut self,
124 name: impl Into<String>,
125 agents: &[AgentMeta],
126 invariants: &[InvariantMeta],
127 ) {
128 self.register_pack_with_profile(name, agents, invariants, &PackProfile::default());
129 }
130
131 pub fn register_pack_raw(&mut self, pack: RegisteredPack) {
133 self.packs.push(pack);
134 }
135
136 pub fn register_capability(&mut self, name: impl Into<String>, description: impl Into<String>) {
138 self.capabilities.push(RegisteredCapability {
139 name: name.into(),
140 description: description.into(),
141 });
142 }
143
144 #[must_use]
145 pub fn packs_for_prefix(&self, prefix: &str) -> Vec<&RegisteredPack> {
146 self.packs
147 .iter()
148 .filter(|p| p.fact_prefixes.iter().any(|fp| fp == prefix))
149 .collect()
150 }
151
152 #[must_use]
153 pub fn packs_for_prefixes(&self, prefixes: &[&str]) -> Vec<&RegisteredPack> {
154 self.packs
155 .iter()
156 .filter(|p| {
157 p.fact_prefixes
158 .iter()
159 .any(|fp| prefixes.contains(&fp.as_str()))
160 })
161 .collect()
162 }
163
164 #[must_use]
165 pub fn packs_for_entity(&self, entity: &str) -> Vec<&RegisteredPack> {
166 let entity = entity.to_lowercase();
167 self.packs
168 .iter()
169 .filter(|p| p.profile.entities.iter().any(|e| *e == entity))
170 .collect()
171 }
172
173 #[must_use]
174 pub fn packs_for_keyword(&self, keyword: &str) -> Vec<&RegisteredPack> {
175 let keyword = keyword.to_lowercase();
176 self.packs
177 .iter()
178 .filter(|p| p.profile.keywords.iter().any(|k| *k == keyword))
179 .collect()
180 }
181
182 #[must_use]
183 pub fn packs_writing_key(&self, key: ContextKey) -> Vec<&RegisteredPack> {
184 self.packs
185 .iter()
186 .filter(|p| p.context_keys_written.contains(&key))
187 .collect()
188 }
189
190 #[must_use]
191 pub fn packs_handling_irreversible(&self) -> Vec<&RegisteredPack> {
192 self.packs
193 .iter()
194 .filter(|p| p.profile.handles_irreversible && p.has_acceptance_invariants)
195 .collect()
196 }
197
198 #[must_use]
199 pub fn packs_requiring_capability(&self, capability: &str) -> Vec<&RegisteredPack> {
200 self.packs
201 .iter()
202 .filter(|p| p.profile.required_capabilities.contains(&capability))
203 .collect()
204 }
205
206 #[must_use]
207 pub fn search_packs(&self, query: &str) -> Vec<&RegisteredPack> {
208 let query = query.to_lowercase();
209 self.packs
210 .iter()
211 .filter(|p| {
212 p.name.to_lowercase().contains(&query)
213 || p.description.to_lowercase().contains(&query)
214 || p.profile.keywords.iter().any(|k| k.contains(&query))
215 || p.profile.entities.iter().any(|e| e.contains(&query))
216 })
217 .collect()
218 }
219
220 #[must_use]
221 pub fn has_capability(&self, name: &str) -> bool {
222 self.capabilities.iter().any(|c| c.name == name)
223 }
224
225 #[must_use]
226 pub fn search_capabilities(&self, query: &str) -> Vec<&RegisteredCapability> {
227 let query = query.to_lowercase();
228 self.capabilities
229 .iter()
230 .filter(|c| {
231 c.name.to_lowercase().contains(&query)
232 || c.description.to_lowercase().contains(&query)
233 })
234 .collect()
235 }
236
237 #[must_use]
238 pub fn packs(&self) -> &[RegisteredPack] {
239 &self.packs
240 }
241
242 #[must_use]
243 pub fn capabilities(&self) -> &[RegisteredCapability] {
244 &self.capabilities
245 }
246}
247
248pub struct StructuralResolver<'a> {
263 registry: &'a Registry,
264}
265
266const STRONG_CONTEXT_FLOW_ANCHOR_CONFIDENCE: f64 = 0.75;
267
268impl<'a> StructuralResolver<'a> {
269 #[must_use]
270 pub fn new(registry: &'a Registry) -> Self {
271 Self { registry }
272 }
273}
274
275#[allow(clippy::too_many_lines)]
276impl IntentResolver for StructuralResolver<'_> {
277 fn level(&self) -> ResolutionLevel {
278 ResolutionLevel::Structural
279 }
280
281 fn resolve(&self, intent: &IntentPacket, current: &IntentBinding) -> IntentBinding {
282 let mut binding = current.clone();
283 let already_bound: std::collections::HashSet<String> =
284 binding.packs.iter().map(|p| p.pack_name.clone()).collect();
285
286 let mut matched: Vec<(String, String, f64)> = Vec::new(); let prefixes = extract_fact_prefixes(&intent.context);
290 for prefix in &prefixes {
291 for pack in self.registry.packs_for_prefix(prefix) {
292 if !already_bound.contains(&pack.name) {
293 matched.push((
294 pack.name.clone(),
295 format!("fact prefix '{prefix}' → pack '{}'", pack.name),
296 0.9,
297 ));
298 }
299 }
300 }
301
302 for constraint in &intent.constraints {
304 for pack in &self.registry.packs {
305 if pack.invariant_names.iter().any(|i| constraint.contains(i))
306 && !already_bound.contains(&pack.name)
307 {
308 matched.push((
309 pack.name.clone(),
310 format!("constraint '{constraint}' → invariant in '{}'", pack.name),
311 0.85,
312 ));
313 }
314 }
315 }
316
317 let entities = extract_entities(&intent.outcome);
319 for entity in &entities {
320 for pack in self.registry.packs_for_entity(entity) {
321 if !already_bound.contains(&pack.name) {
322 matched.push((
323 pack.name.clone(),
324 format!("entity '{entity}' → pack '{}'", pack.name),
325 0.75,
326 ));
327 }
328 }
329 }
330
331 let keywords = extract_keywords(&intent.outcome);
333 for keyword in &keywords {
334 for pack in self.registry.packs_for_keyword(keyword) {
335 if !already_bound.contains(&pack.name) {
336 matched.push((
337 pack.name.clone(),
338 format!("keyword '{keyword}' → pack '{}'", pack.name),
339 0.65,
340 ));
341 }
342 }
343 }
344
345 if intent.reversibility == Reversibility::Irreversible {
347 for pack in self.registry.packs_handling_irreversible() {
348 if !already_bound.contains(&pack.name) {
349 matched.push((
350 pack.name.clone(),
351 format!(
352 "irreversible intent → pack '{}' has Acceptance invariants",
353 pack.name
354 ),
355 0.7,
356 ));
357 }
358 }
359 }
360
361 let needed_keys = extract_context_keys(&intent.context);
365 let anchored_pack_names = already_bound
366 .iter()
367 .cloned()
368 .chain(
369 matched
370 .iter()
371 .filter(|(_, _, confidence)| {
372 *confidence >= STRONG_CONTEXT_FLOW_ANCHOR_CONFIDENCE
373 })
374 .map(|(pack_name, _, _)| pack_name.clone()),
375 )
376 .collect::<std::collections::HashSet<_>>();
377 let anchored_packs = anchored_pack_names
378 .iter()
379 .filter_map(|pack_name| {
380 self.registry
381 .packs
382 .iter()
383 .find(|pack| &pack.name == pack_name)
384 })
385 .collect::<Vec<_>>();
386
387 let anchor_written_keys = anchored_packs
388 .iter()
389 .flat_map(|pack| pack.context_keys_written.iter().copied())
390 .filter(|key| needed_keys.contains(key))
391 .collect::<std::collections::HashSet<_>>();
392
393 for pack in &anchored_packs {
394 for key in pack
395 .context_keys_written
396 .iter()
397 .filter(|key| needed_keys.contains(key))
398 {
399 if !already_bound.contains(&pack.name) {
400 matched.push((
401 pack.name.clone(),
402 format!(
403 "anchored context flow → pack '{}' writes needed {key:?}",
404 pack.name
405 ),
406 0.78,
407 ));
408 }
409 }
410 }
411
412 if needed_keys.len() >= 2 && !anchor_written_keys.is_empty() {
413 for pack in &self.registry.packs {
414 if already_bound.contains(&pack.name) || anchored_pack_names.contains(&pack.name) {
415 continue;
416 }
417
418 let Some(read_key) = pack
419 .context_keys_read
420 .iter()
421 .copied()
422 .find(|key| anchor_written_keys.contains(key))
423 else {
424 continue;
425 };
426 let Some(write_key) = pack
427 .context_keys_written
428 .iter()
429 .copied()
430 .find(|key| needed_keys.contains(key) && *key != read_key)
431 else {
432 continue;
433 };
434
435 matched.push((
436 pack.name.clone(),
437 format!("context flow bridge {read_key:?} → {write_key:?} from anchored packs"),
438 0.72,
439 ));
440 }
441 }
442
443 let forbidden_keywords: Vec<String> = intent
445 .forbidden
446 .iter()
447 .map(|f| f.action.to_lowercase())
448 .collect();
449 matched.retain(|(pack_name, _, _)| {
450 if let Some(pack) = self.registry.packs.iter().find(|p| &p.name == pack_name) {
451 !pack.profile.keywords.iter().any(|k| {
452 forbidden_keywords
453 .iter()
454 .any(|f| f.contains(k) || k.contains(f.as_str()))
455 })
456 } else {
457 true
458 }
459 });
460
461 let mut needed_capabilities: Vec<(String, String)> = Vec::new();
464 let all_pack_names: Vec<String> = already_bound
465 .iter()
466 .chain(matched.iter().map(|(name, _, _)| name))
467 .cloned()
468 .collect();
469 for pack_name in &all_pack_names {
470 if let Some(pack) = self.registry.packs.iter().find(|p| &p.name == pack_name) {
471 for cap in pack.profile.required_capabilities {
472 if !binding.capabilities.iter().any(|c| c.capability == *cap)
473 && !needed_capabilities.iter().any(|(c, _)| c == *cap)
474 {
475 needed_capabilities.push((
476 (*cap).to_string(),
477 format!("required by pack '{pack_name}'"),
478 ));
479 }
480 }
481 }
482 }
483
484 let mut best: std::collections::HashMap<String, (String, f64)> =
486 std::collections::HashMap::new();
487 for (pack_name, reason, confidence) in matched {
488 let entry = best
489 .entry(pack_name.clone())
490 .or_insert((reason.clone(), 0.0));
491 if confidence > entry.1 {
492 *entry = (reason, confidence);
493 }
494 }
495
496 for (pack_name, (reason, confidence)) in best {
497 binding.packs.push(PackRequirement {
498 pack_name,
499 reason,
500 confidence: converge_pack::UnitInterval::clamped(confidence),
501 source: ResolutionLevel::Structural,
502 });
503 }
504
505 for (cap, reason) in needed_capabilities {
506 binding.capabilities.push(CapabilityRequirement {
507 capability: cap.into(),
508 reason,
509 confidence: converge_pack::UnitInterval::clamped(0.85),
510 source: ResolutionLevel::Structural,
511 });
512 }
513
514 if !binding
515 .resolution
516 .levels_attempted
517 .contains(&ResolutionLevel::Structural)
518 {
519 binding
520 .resolution
521 .levels_attempted
522 .push(ResolutionLevel::Structural);
523 binding
524 .resolution
525 .levels_contributed
526 .push(ResolutionLevel::Structural);
527 }
528
529 binding
530 }
531}
532
533fn extract_fact_prefixes(context: &serde_json::Value) -> Vec<String> {
536 let mut prefixes = Vec::new();
537 collect_prefixes(context, &mut prefixes);
538 prefixes.sort();
539 prefixes.dedup();
540 prefixes
541}
542
543fn collect_prefixes(value: &serde_json::Value, prefixes: &mut Vec<String>) {
544 match value {
545 serde_json::Value::String(s) => {
546 if let Some(colon) = s.find(':') {
547 let candidate = &s[..=colon];
548 if candidate.len() >= 3 && candidate.chars().next().is_some_and(char::is_alphabetic)
549 {
550 prefixes.push(candidate.to_string());
551 }
552 }
553 }
554 serde_json::Value::Array(arr) => {
555 for v in arr {
556 collect_prefixes(v, prefixes);
557 }
558 }
559 serde_json::Value::Object(map) => {
560 for (key, v) in map {
561 if key.ends_with(':') {
562 prefixes.push(key.clone());
563 }
564 collect_prefixes(v, prefixes);
565 }
566 }
567 _ => {}
568 }
569}
570
571fn extract_context_keys(context: &serde_json::Value) -> Vec<ContextKey> {
572 let mut keys = Vec::new();
573 if let Some(obj) = context.as_object() {
576 for key in obj.keys() {
577 let k = key.to_lowercase();
578 if k == "evaluations" || k == "evaluation" {
579 keys.push(ContextKey::Evaluations);
580 }
581 if k == "strategies" || k == "strategy" {
582 keys.push(ContextKey::Strategies);
583 }
584 if k == "proposals" || k == "proposal" {
585 keys.push(ContextKey::Proposals);
586 }
587 if k == "constraints" || k == "constraint" {
588 keys.push(ContextKey::Constraints);
589 }
590 if k == "signals" || k == "signal" {
591 keys.push(ContextKey::Signals);
592 }
593 }
594 }
595 keys.dedup();
596 keys
597}
598
599fn extract_entities(outcome: &str) -> Vec<String> {
600 let outcome = outcome.to_lowercase();
601 let known_entities = [
602 "lead",
603 "vendor",
604 "contract",
605 "employee",
606 "expense",
607 "deal",
608 "partner",
609 "subscription",
610 "asset",
611 "ticket",
612 "campaign",
613 "feature",
614 "release",
615 "incident",
616 "policy",
617 "approval",
618 "budget",
619 "team",
620 "persona",
621 "skill",
622 "credential",
623 "patent",
624 "signal",
625 "hypothesis",
626 "experiment",
627 ];
628 known_entities
629 .iter()
630 .filter(|e| outcome.contains(**e))
631 .map(|e| (*e).to_string())
632 .collect()
633}
634
635const STOP_WORDS: &[&str] = &[
636 "this", "that", "with", "from", "have", "your", "into", "about", "would", "there", "their",
637 "they", "them", "when", "what", "where", "which", "shall", "could", "should", "will", "been",
638 "does", "done", "each", "every", "some", "than", "then", "also", "just", "only", "most",
639 "such", "very", "more", "over", "under", "after", "before", "between", "through", "during",
640 "without", "within", "along", "across", "against", "around", "upon", "onto", "produce",
641 "process", "create", "update", "manage", "handle", "ensure", "track", "check", "verify",
642 "analyze", "review", "prepare", "complete",
643];
644
645fn extract_keywords(outcome: &str) -> Vec<String> {
646 outcome
647 .to_lowercase()
648 .split_whitespace()
649 .filter(|w| w.len() >= 4)
650 .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric()).to_string())
651 .filter(|w| !w.is_empty() && !STOP_WORDS.contains(&w.as_str()))
652 .collect()
653}