1pub mod classifier;
17pub mod collaboration;
18pub mod compiler;
19pub mod execution;
20pub mod experience;
21pub mod factories;
22pub mod formation;
23pub mod guru;
24pub mod huddle;
25pub mod outcome;
26pub mod provenance;
27pub mod readiness;
28pub mod registry;
29pub mod stall;
30pub mod templates;
31pub mod tournament;
32pub mod vendor_selection;
33
34pub use classifier::{ProblemClassifierSuggestor, extract_classification};
35pub use collaboration::{
36 CollaborationParticipant, CollaborationRunner, CollaborationRunnerError, TransitionRecord,
37};
38pub use compiler::{
39 CandidateConsideration, CandidateDisposition, CatalogCompileFailure, CompiledFormationPlan,
40 CompiledSuggestorRole, DataContract, FormationCompileError, FormationCompileRequest,
41 FormationCompiler, FormationCompilerCatalogs, GovernanceClass, ProviderDescriptor,
42 ProviderDescriptorCatalog, RejectionReason, ReplayMode, RoleDecision, RoleProviderAssignment,
43 SelectionReason, SuggestorDescriptor, SuggestorDescriptorCatalog,
44};
45pub use execution::{
46 ExecutableSuggestorCatalog, FormationExecutionRecord, FormationInstantiationError,
47};
48pub use experience::{ExperienceEnvelopeSink, FormationExperienceObserver};
49pub use factories::register_default_factories;
50pub use formation::{Formation, FormationError, FormationResult, Seed};
51pub use guru::{CandidateScore, FormationGuru, GuruError, GuruSelection, SelectionTrace};
52pub use huddle::{
53 ConsensusEvaluator, DisagreementMap, DisagreementMapper, RoundConventions, RoundStarter,
54 RoundSynthesizer, SynthesisProducer, TerminalPredicate,
55};
56pub use organism_pack::{
57 CapabilityRequirement, DeclarativeBinding, IntentBinding, IntentResolver, PackRequirement,
58 ResolutionLevel, ResolutionTrace,
59};
60pub use outcome::{
61 BusinessQualitySignal, FormationOutcomeRecord, FormationOutcomeStatus, FormationRunScope,
62 OutcomeProviderAssignment, OutcomeRosterMember, QualityScoreBps, QualityScoreError,
63};
64pub use readiness::{
65 BudgetProbe, CredentialProbe, GapSeverity, PackProbe, ReadinessConfirmation, ReadinessGap,
66 ReadinessItem, ReadinessProbe, ReadinessReport, ResourceKind, check as check_readiness,
67};
68pub use registry::{RegisteredCapability, RegisteredPack, Registry, StructuralResolver};
69pub use stall::RoleStallSuggestor;
70pub use templates::{
71 CostHint, cost_hint_for, decision_formation, diligence_formation, evaluation_formation,
72 planning_formation, research_formation, standard_formation_catalog, template_id_for,
73};
74pub use tournament::{FormationScore, FormationTournament, TournamentError, TournamentResult};
75pub use vendor_selection::{
76 VendorSelectionFlow, VendorSelectionFlowSpec, vendor_selection_formation_catalog,
77 vendor_selection_lifecycle,
78};
79
80use organism_catalog::DiscoveryCatalog;
81
82use converge_kernel::admission::{
83 AdmissionActor, AdmissionContent, AdmissionError, AdmissionReceipt, AdmissionRequest,
84 AdmissionSource, admit_observation,
85};
86use converge_kernel::formation::{FormationCatalog, SuggestorCapability};
87use converge_kernel::{ContextKey, ContextState, ConvergeError};
88use organism_intent::admission::{self, Admission};
89use organism_pack::IntentPacket;
90use std::sync::Arc;
91
92#[derive(Debug)]
94pub struct OrganismResult {
95 pub winning_formation: String,
97 pub converge_result: converge_kernel::ConvergeResult,
99}
100
101#[derive(Debug, Clone)]
107pub struct ScoredCatalogCandidate {
108 pub index: usize,
114 pub candidate: CompiledFormationPlan,
115 pub score: FormationScore,
116}
117
118#[derive(Debug, Clone)]
132pub struct CatalogTournamentOutcome {
133 pub winner_index: usize,
135 pub scored_candidates: Vec<ScoredCatalogCandidate>,
139 pub priors: Vec<organism_learning::PriorCalibration>,
141}
142
143impl CatalogTournamentOutcome {
144 #[must_use]
150 pub fn winner(&self) -> Option<&ScoredCatalogCandidate> {
151 self.scored_candidates
152 .iter()
153 .find(|sc| sc.index == self.winner_index)
154 }
155}
156
157#[derive(Debug, thiserror::Error)]
159pub enum PipelineError {
160 #[error("admission rejected: {0}")]
161 Rejected(String),
162 #[error("formation compile error: {0}")]
163 Compile(#[from] FormationCompileError),
164 #[error("catalog compile error: {0}")]
168 CatalogCompile(#[from] CatalogCompileFailure),
169 #[error("formation instantiation error: {0}")]
170 Instantiate(#[from] FormationInstantiationError),
171 #[error("all formations failed: {0}")]
172 AllFormationsFailed(String),
173 #[error("formation error: {0}")]
174 Formation(#[from] FormationError),
175 #[error("tournament error: {0}")]
177 Tournament(String),
178}
179
180#[derive(Debug, thiserror::Error)]
183pub enum IntentAdmissionError {
184 #[error("admission rejected: {0}")]
186 Rejected(String),
187 #[error("admission request invalid: {0}")]
189 AdmissionRequest(#[from] AdmissionError),
190 #[error("intent payload could not be serialized: {0}")]
192 Serialize(String),
193 #[error("converge admission failed: {0}")]
195 Converge(String),
196}
197
198impl From<ConvergeError> for IntentAdmissionError {
199 fn from(err: ConvergeError) -> Self {
200 Self::Converge(err.to_string())
201 }
202}
203
204pub struct Runtime;
214
215impl Runtime {
216 pub fn new() -> Self {
217 Self
218 }
219
220 pub fn admit_intent(
244 &self,
245 intent: &IntentPacket,
246 actor: AdmissionActor,
247 source: AdmissionSource,
248 context: &mut ContextState,
249 ) -> Result<AdmissionReceipt, IntentAdmissionError> {
250 gate_admission(intent).map_err(|err| match err {
251 PipelineError::Rejected(msg) => IntentAdmissionError::Rejected(msg),
252 other => IntentAdmissionError::Rejected(other.to_string()),
253 })?;
254
255 let payload = serde_json::to_string(intent)
256 .map_err(|err| IntentAdmissionError::Serialize(err.to_string()))?;
257 let admission_body = AdmissionContent::new(payload)?;
258 let request = AdmissionRequest::new(
259 actor,
260 source,
261 ContextKey::Seeds,
262 format!("intent:{}", intent.id),
263 admission_body,
264 )?;
265 let receipt = admit_observation(context, request)?;
266 Ok(receipt)
267 }
268
269 pub fn select_formation<'cat>(
284 &self,
285 intent: &IntentPacket,
286 catalog: &'cat FormationCatalog,
287 capabilities: &[SuggestorCapability],
288 ) -> Result<GuruSelection<'cat>, GuruError> {
289 FormationGuru::new(catalog).select(intent, capabilities)
290 }
291
292 pub fn compile_formation(
297 &self,
298 intent: &IntentPacket,
299 request: &FormationCompileRequest,
300 catalogs: &FormationCompilerCatalogs,
301 ) -> Result<CompiledFormationPlan, PipelineError> {
302 gate_admission(intent)?;
303 Ok(FormationCompiler::new().compile(request, catalogs)?)
304 }
305
306 pub fn compile_and_instantiate_formation(
312 &self,
313 intent: &IntentPacket,
314 request: &FormationCompileRequest,
315 catalogs: &FormationCompilerCatalogs,
316 executables: &ExecutableSuggestorCatalog,
317 seeds: impl IntoIterator<Item = Seed>,
318 ) -> Result<(CompiledFormationPlan, Formation), PipelineError> {
319 let plan = self.compile_formation(intent, request, catalogs)?;
320 let formation = executables.instantiate(&plan, seeds)?;
321 Ok((plan, formation))
322 }
323
324 pub async fn compile_and_run_formation(
330 &self,
331 intent: &IntentPacket,
332 request: &FormationCompileRequest,
333 catalogs: &FormationCompilerCatalogs,
334 executables: &ExecutableSuggestorCatalog,
335 seeds: impl IntoIterator<Item = Seed>,
336 observer: Option<Arc<dyn converge_kernel::ExperienceEventObserver>>,
337 ) -> Result<FormationExecutionRecord, PipelineError> {
338 let (plan, formation) =
339 self.compile_and_instantiate_formation(intent, request, catalogs, executables, seeds)?;
340 let result = if let Some(observer) = observer {
341 formation.run_with_event_observer(observer).await?
342 } else {
343 formation.run().await?
344 };
345
346 Ok(FormationExecutionRecord::from_plan_and_result(plan, result))
347 }
348
349 pub fn compile_formation_from_catalog(
363 &self,
364 intent: &IntentPacket,
365 request: &FormationCompileRequest,
366 formation_templates: &FormationCatalog,
367 catalog: &DiscoveryCatalog,
368 providers: &ProviderDescriptorCatalog,
369 advisory_order: Option<&[String]>,
370 ) -> Result<CompiledFormationPlan, PipelineError> {
371 gate_admission(intent)?;
372 Ok(FormationCompiler::new().compile_from_catalog(
373 request,
374 formation_templates,
375 catalog,
376 providers,
377 advisory_order,
378 )?)
379 }
380
381 #[allow(clippy::too_many_arguments)]
384 pub fn compile_and_instantiate_from_catalog(
385 &self,
386 intent: &IntentPacket,
387 request: &FormationCompileRequest,
388 formation_templates: &FormationCatalog,
389 catalog: &DiscoveryCatalog,
390 providers: &ProviderDescriptorCatalog,
391 executables: &ExecutableSuggestorCatalog,
392 seeds: impl IntoIterator<Item = Seed>,
393 advisory_order: Option<&[String]>,
394 ) -> Result<(CompiledFormationPlan, Formation), PipelineError> {
395 let outcome = self.compile_formation_from_catalog(
396 intent,
397 request,
398 formation_templates,
399 catalog,
400 providers,
401 advisory_order,
402 )?;
403 let formation = executables.instantiate(&outcome, seeds)?;
404 Ok((outcome, formation))
405 }
406
407 #[allow(clippy::too_many_arguments)]
423 pub async fn compile_k_and_run_tournament<F>(
424 &self,
425 intent: &IntentPacket,
426 request: &FormationCompileRequest,
427 formation_templates: &FormationCatalog,
428 catalog: &DiscoveryCatalog,
429 providers: &ProviderDescriptorCatalog,
430 executables: &ExecutableSuggestorCatalog,
431 seeds_fn: F,
432 k: usize,
433 ) -> Result<CatalogTournamentOutcome, PipelineError>
434 where
435 F: Fn(usize, &CompiledFormationPlan) -> Vec<Seed>,
436 {
437 gate_admission(intent)?;
438
439 let candidates = FormationCompiler::new().compile_k_candidates(
440 request,
441 formation_templates,
442 catalog,
443 providers,
444 k,
445 )?;
446
447 if candidates.is_empty() {
448 return Err(PipelineError::Tournament(
449 "compile_k_candidates returned no candidates".to_string(),
450 ));
451 }
452
453 let mut formations: Vec<Formation> = Vec::with_capacity(candidates.len());
454 for (index, candidate) in candidates.iter().enumerate() {
455 let seeds = seeds_fn(index, candidate);
456 let label = format!("{}#{index}", candidate.template_id);
460 let formation = executables.instantiate_with_label(candidate, seeds, label)?;
461 formations.push(formation);
462 }
463
464 let tournament = FormationTournament::new(intent.id, request.plan_id, formations);
465 let tournament_result = tournament
466 .run()
467 .await
468 .map_err(|err| PipelineError::Tournament(err.to_string()))?;
469
470 let mut scored_candidates: Vec<ScoredCatalogCandidate> =
473 Vec::with_capacity(candidates.len());
474 for score in &tournament_result.all_scores {
475 let index = candidate_index_from_label(&score.label).ok_or_else(|| {
476 PipelineError::Tournament(format!(
477 "tournament returned an unjoinable score label: {label}",
478 label = score.label
479 ))
480 })?;
481 let candidate = candidates.get(index).cloned().ok_or_else(|| {
482 PipelineError::Tournament(format!(
483 "score index {index} out of range (k = {})",
484 candidates.len()
485 ))
486 })?;
487 scored_candidates.push(ScoredCatalogCandidate {
488 index,
489 candidate,
490 score: score.clone(),
491 });
492 }
493 scored_candidates.sort_by_key(|sc| sc.index);
496
497 let winner_index =
498 candidate_index_from_label(&tournament_result.winner.label).ok_or_else(|| {
499 PipelineError::Tournament(format!(
500 "tournament winner has unjoinable label: {label}",
501 label = tournament_result.winner.label
502 ))
503 })?;
504
505 Ok(CatalogTournamentOutcome {
506 winner_index,
507 scored_candidates,
508 priors: tournament_result.priors,
509 })
510 }
511
512 pub async fn handle(
529 &self,
530 intent: IntentPacket,
531 formations: Vec<Formation>,
532 ) -> Result<OrganismResult, PipelineError> {
533 gate_admission(&intent)?;
536
537 let mut results = Vec::new();
542 let mut errors = Vec::new();
543
544 for formation in formations {
545 match formation.run().await {
546 Ok(result) => results.push(result),
547 Err(e) => errors.push(e.to_string()),
548 }
549 }
550
551 if results.is_empty() {
552 return Err(PipelineError::AllFormationsFailed(errors.join("; ")));
553 }
554
555 let winner = results.into_iter().next().unwrap();
559
560 Ok(OrganismResult {
561 winning_formation: winner.label,
562 converge_result: winner.converge_result,
563 })
564 }
565}
566
567fn candidate_index_from_label(label: &str) -> Option<usize> {
572 label.rsplit_once('#')?.1.parse::<usize>().ok()
573}
574
575fn gate_admission(intent: &IntentPacket) -> Result<(), PipelineError> {
576 match admission::admit(intent) {
577 Admission::Admit => Ok(()),
578 Admission::Reject(err) => Err(PipelineError::Rejected(err.to_string())),
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::provenance::ORGANISM_RUNTIME_PROVENANCE;
586 use chrono::{Duration, Utc};
587 use converge_kernel::formation::{
588 FormationTemplateQuery, ProfileSnapshot, SuggestorCapability, SuggestorRole,
589 };
590 use converge_kernel::{AgentEffect, Context, ContextKey, Suggestor};
591 use converge_pack::{Provenance, ProvenanceSource, TextPayload};
592 use converge_provider::{BackendRequirements, CostClass, LatencyClass};
593
594 fn id(n: u128) -> uuid::Uuid {
595 uuid::Uuid::from_u128(n)
596 }
597
598 fn valid_intent() -> IntentPacket {
599 IntentPacket::new("select the best AI vendor", Utc::now() + Duration::hours(1))
600 }
601
602 #[test]
610 fn catalog_tournament_winner_lookup_safe_when_lower_index_dropped() {
611 let candidate = CompiledFormationPlan {
615 plan_id: id(0xCAFE),
616 correlation_id: id(0xBEEF),
617 tenant_id: None,
618 template_id: "winner-template".into(),
619 template_kind: converge_kernel::formation::FormationKind::Static,
620 roster: Vec::new(),
621 provider_assignments: Vec::new(),
622 trace: Vec::new(),
623 decisions: Vec::new(),
624 };
625 let score = FormationScore {
626 label: "winner-template#1".to_string(),
627 score: 0.9,
628 converged: true,
629 cycles: 1,
630 criteria_met: 0,
631 criteria_total: 0,
632 };
633 let outcome = CatalogTournamentOutcome {
634 winner_index: 1,
635 scored_candidates: vec![ScoredCatalogCandidate {
636 index: 1,
637 candidate,
638 score,
639 }],
640 priors: Vec::new(),
641 };
642
643 let winner = outcome.winner().expect("winner must be present");
646 assert_eq!(winner.index, 1);
647 assert!((winner.score.score - 0.9).abs() < f64::EPSILON);
648 }
649
650 fn profile(
651 name: &str,
652 role: SuggestorRole,
653 writes: Vec<ContextKey>,
654 capabilities: Vec<SuggestorCapability>,
655 ) -> ProfileSnapshot {
656 ProfileSnapshot {
657 name: name.to_string(),
658 role,
659 output_keys: writes,
660 cost_hint: CostClass::Low,
661 latency_hint: LatencyClass::Interactive,
662 capabilities,
663 confidence_min: 0.7,
664 confidence_max: 0.95,
665 }
666 }
667
668 fn request() -> FormationCompileRequest {
669 FormationCompileRequest::new(
670 id(1),
671 id(2),
672 FormationTemplateQuery::new()
673 .with_keyword("diligence-evaluate-decide")
674 .with_entity("VendorSelectionDecisionRecord"),
675 )
676 .with_tenant_id("tenant-a")
677 .with_domain_tag("vendor-selection")
678 }
679
680 fn catalogs() -> FormationCompilerCatalogs {
681 let policy_requirements = BackendRequirements::access_policy().with_replay();
682 FormationCompilerCatalogs::new(vendor_selection_formation_catalog())
683 .with_suggestor(
684 SuggestorDescriptor::new(
685 "market-scan",
686 profile(
687 "market-scan",
688 SuggestorRole::Signal,
689 vec![ContextKey::Signals],
690 vec![SuggestorCapability::KnowledgeRetrieval],
691 ),
692 )
693 .with_domain_tag("vendor-selection"),
694 )
695 .with_suggestor(
696 SuggestorDescriptor::new(
697 "weighted-evaluator",
698 profile(
699 "weighted-evaluator",
700 SuggestorRole::Evaluation,
701 vec![ContextKey::Evaluations],
702 vec![SuggestorCapability::Analytics],
703 ),
704 )
705 .with_domain_tag("vendor-selection"),
706 )
707 .with_suggestor(
708 SuggestorDescriptor::new(
709 "policy-gate",
710 profile(
711 "policy-gate",
712 SuggestorRole::Constraint,
713 vec![ContextKey::Constraints],
714 vec![SuggestorCapability::PolicyEnforcement],
715 ),
716 )
717 .with_domain_tag("vendor-selection")
718 .with_backend_requirements(policy_requirements.clone()),
719 )
720 .with_suggestor(
721 SuggestorDescriptor::new(
722 "decision-synthesis",
723 profile(
724 "decision-synthesis",
725 SuggestorRole::Synthesis,
726 vec![ContextKey::Proposals],
727 vec![SuggestorCapability::LlmReasoning],
728 ),
729 )
730 .with_domain_tag("vendor-selection"),
731 )
732 .with_provider(
733 ProviderDescriptor::new(
734 "cedar-local",
735 "Cedar local policy engine",
736 policy_requirements,
737 )
738 .with_role_affinity(SuggestorRole::Constraint)
739 .with_domain_tag("vendor-selection"),
740 )
741 }
742
743 struct FixtureSuggestor {
744 name: &'static str,
745 dependencies: Vec<ContextKey>,
746 output: ContextKey,
747 }
748
749 impl FixtureSuggestor {
750 fn new(name: &'static str, dependencies: Vec<ContextKey>, output: ContextKey) -> Self {
751 Self {
752 name,
753 dependencies,
754 output,
755 }
756 }
757 }
758
759 #[async_trait::async_trait]
760 impl Suggestor for FixtureSuggestor {
761 fn name(&self) -> &str {
762 self.name
763 }
764
765 fn dependencies(&self) -> &[ContextKey] {
766 &self.dependencies
767 }
768
769 fn provenance(&self) -> Provenance {
770 ORGANISM_RUNTIME_PROVENANCE.provenance()
771 }
772
773 fn accepts(&self, ctx: &dyn Context) -> bool {
774 self.dependencies.iter().any(|key| ctx.has(*key)) && !ctx.has(self.output)
775 }
776
777 async fn execute(&self, _ctx: &dyn Context) -> AgentEffect {
778 AgentEffect::with_proposal(
779 crate::provenance::ORGANISM_RUNTIME_PROVENANCE.proposed_fact(
780 self.output,
781 format!("{}-output", self.name),
782 TextPayload::new(format!("{} produced compiled-role output", self.name)),
783 ),
784 )
785 }
786 }
787
788 fn executable_catalog() -> ExecutableSuggestorCatalog {
789 let mut catalog = ExecutableSuggestorCatalog::new();
790 catalog
791 .register_factory("market-scan", || {
792 FixtureSuggestor::new("market-scan", vec![ContextKey::Seeds], ContextKey::Signals)
793 })
794 .expect("market-scan factory");
795 catalog
796 .register_factory("weighted-evaluator", || {
797 FixtureSuggestor::new(
798 "weighted-evaluator",
799 vec![ContextKey::Signals],
800 ContextKey::Evaluations,
801 )
802 })
803 .expect("weighted-evaluator factory");
804 catalog
805 .register_factory("policy-gate", || {
806 FixtureSuggestor::new(
807 "policy-gate",
808 vec![ContextKey::Evaluations],
809 ContextKey::Constraints,
810 )
811 })
812 .expect("policy-gate factory");
813 catalog
814 .register_factory("decision-synthesis", || {
815 FixtureSuggestor::new(
816 "decision-synthesis",
817 vec![ContextKey::Evaluations, ContextKey::Constraints],
818 ContextKey::Proposals,
819 )
820 })
821 .expect("decision-synthesis factory");
822 catalog
823 }
824
825 #[test]
826 fn runtime_selects_decision_template_for_decision_intent() {
827 let catalog = standard_formation_catalog();
828 let caps = [
829 SuggestorCapability::LlmReasoning,
830 SuggestorCapability::PolicyEnforcement,
831 SuggestorCapability::Analytics,
832 ];
833 let intent = IntentPacket::new(
834 "decide which vendor to approve",
835 Utc::now() + Duration::hours(1),
836 );
837
838 let selection = Runtime::new()
839 .select_formation(&intent, &catalog, &caps)
840 .expect("decision intent matches the standard catalog");
841
842 assert_eq!(selection.primary.id(), "organism-decision");
843 assert_eq!(
844 selection.classification.class,
845 organism_intent::problem::ProblemClass::Decision
846 );
847 }
848
849 #[test]
850 fn runtime_compiles_after_admission() {
851 let plan = Runtime::new()
852 .compile_formation(&valid_intent(), &request(), &catalogs())
853 .expect("valid vendor-selection intent should compile");
854
855 assert_eq!(plan.template_id, "vendor-selection-decide");
856 assert_eq!(plan.correlation_id, id(2));
857 assert_eq!(plan.tenant_id.as_deref(), Some("tenant-a"));
858 }
859
860 #[test]
861 fn runtime_rejects_invalid_intent_before_compile() {
862 let invalid_intent = IntentPacket::new(" ", Utc::now() + Duration::hours(1));
863
864 let error = Runtime::new()
865 .compile_formation(&invalid_intent, &request(), &catalogs())
866 .expect_err("blank intent should fail admission");
867
868 assert!(matches!(error, PipelineError::Rejected(_)));
869 }
870
871 #[tokio::test]
872 async fn runtime_compiles_and_runs_executable_plan_with_outcome_record() {
873 let seed = Seed {
874 key: ContextKey::Seeds,
875 id: "vendor-selection-intent".into(),
876 content: "select the AI governance vendor".to_string(),
877 provenance: ORGANISM_RUNTIME_PROVENANCE.provenance(),
878 };
879
880 let record = Runtime::new()
881 .compile_and_run_formation(
882 &valid_intent(),
883 &request(),
884 &catalogs(),
885 &executable_catalog(),
886 vec![seed],
887 None,
888 )
889 .await
890 .expect("plan should compile and run");
891
892 assert_eq!(record.plan.template_id, "vendor-selection-decide");
893 assert_eq!(record.outcome.status, FormationOutcomeStatus::Converged);
894 assert_eq!(record.outcome.scope.correlation_id, id(2));
895
896 assert!(record.result.converge_result.converged);
897 assert!(
898 record
899 .result
900 .converge_result
901 .context
902 .has(ContextKey::Signals)
903 );
904 assert!(
905 record
906 .result
907 .converge_result
908 .context
909 .has(ContextKey::Evaluations)
910 );
911 assert!(
912 record
913 .result
914 .converge_result
915 .context
916 .has(ContextKey::Constraints)
917 );
918 assert!(
919 record
920 .result
921 .converge_result
922 .context
923 .has(ContextKey::Proposals)
924 );
925 }
926
927 #[test]
928 fn runtime_reports_missing_executable_factories() {
929 let seed = Seed {
930 key: ContextKey::Seeds,
931 id: "vendor-selection-intent".into(),
932 content: "select the AI governance vendor".to_string(),
933 provenance: ORGANISM_RUNTIME_PROVENANCE.provenance(),
934 };
935 let mut partial = ExecutableSuggestorCatalog::new();
936 partial
937 .register_factory("market-scan", || {
938 FixtureSuggestor::new("market-scan", vec![ContextKey::Seeds], ContextKey::Signals)
939 })
940 .expect("market-scan factory");
941
942 let Err(error) = Runtime::new().compile_and_instantiate_formation(
943 &valid_intent(),
944 &request(),
945 &catalogs(),
946 &partial,
947 vec![seed],
948 ) else {
949 panic!("missing executable factories should fail explicitly");
950 };
951
952 match error {
953 PipelineError::Instantiate(
954 FormationInstantiationError::MissingSuggestorFactories { suggestor_ids },
955 ) => {
956 assert!(suggestor_ids.contains(&"weighted-evaluator".to_string()));
957 assert!(suggestor_ids.contains(&"policy-gate".to_string()));
958 assert!(suggestor_ids.contains(&"decision-synthesis".to_string()));
959 }
960 other => panic!("unexpected error: {other:?}"),
961 }
962 }
963}