Skip to main content

organism_runtime/
registry.rs

1//! Registry — the runtime's catalog of available packs, capabilities, and invariants.
2//!
3//! Intent resolution queries the registry to find what's available.
4//! Apps register what they've wired up; the resolver searches across it.
5//! Standard pack catalogs (e.g. `organism-domain::register_standard_packs`)
6//! sit on top, registering domain content into a `Registry`.
7
8use organism_pack::{
9    AgentMeta, CapabilityRequirement, ContextKey, IntentBinding, IntentPacket, IntentResolver,
10    InvariantClass, InvariantMeta, PackProfile, PackRequirement, ResolutionLevel, Reversibility,
11};
12
13// ── Registered Pack ────────────────────────────────────────────────
14
15#[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// ── Registry ───────────────────────────────────────────────────────
37
38#[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    /// Register a domain pack only if a pack with the same name is not already
51    /// registered. Used by standard-pack catalogs (e.g. `organism-domain`) to
52    /// be safely callable multiple times.
53    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    /// Register a domain pack with full metadata and profile.
68    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    /// Register a domain pack (without profile — uses default).
122    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    /// Register a pack directly from a raw struct.
132    pub fn register_pack_raw(&mut self, pack: RegisteredPack) {
133        self.packs.push(pack);
134    }
135
136    /// Register an available capability.
137    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
248// ── Structural Resolver (multi-dimension) ──────────────────────────
249
250/// Level 2 resolver with 10 matching dimensions:
251///
252/// 1. Fact prefix matching
253/// 2. Constraint → invariant matching
254/// 3. Context key flow matching
255/// 4. Domain entity matching
256/// 5. Keyword matching
257/// 6. Reversibility matching
258/// 7. Forbidden action filtering
259/// 8. Capability affinity
260/// 9. HITL requirement matching
261/// 10. Blueprint expansion (TODO)
262pub 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(); // (pack, reason, confidence)
287
288        // ── Dimension 1: Fact prefix matching ──────────────────────
289        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        // ── Dimension 2: Constraint → invariant matching ───────────
303        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        // ── Dimension 4: Domain entity matching ────────────────────
318        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        // ── Dimension 5: Keyword matching ──────────────────────────
332        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        // ── Dimension 6: Reversibility matching ────────────────────
346        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        // ── Dimension 3: Context key flow matching ─────────────────
362        // Context keys are useful only when they extend a stronger anchor.
363        // Avoid global fan-out like "all packs writing Evaluations".
364        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        // ── Dimension 7: Forbidden action filtering ────────────────
444        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        // ── Dimension 8: Capability affinity ───────────────────────
462        // If ANY bound pack (declarative or matched) requires capabilities, add them.
463        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        // Deduplicate: keep highest confidence per pack
485        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
533// ── Extraction helpers ─────────────────────────────────────────────
534
535fn 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    // Only match explicit ContextKey references in structured fields,
574    // not incidental word matches in free text.
575    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}