1use std::collections::{BTreeMap, BTreeSet};
40
41use serde::{Deserialize, Serialize};
42
43use crate::controller::{
44 ArtifactPolicy, ControllerCapability, ControllerFitScope, ControllerManifest,
45 ControllerRegistry, OperatorSelector, RngPolicy,
46};
47use crate::data::{ModelInputPortSpec, ModelInputSpec, MODEL_INPUT_SPEC_SCHEMA_VERSION};
48use crate::error::{DagMlError, Result};
49use crate::graph::{NodeKind, PortCardinality, PortKind, PortSpec};
50use crate::ids::ControllerId;
51use crate::phase::Phase;
52
53pub const REPRESENTATION_TABULAR_NUMERIC: &str = "tabular_numeric";
59
60pub const REPRESENTATION_TARGET_NUMERIC: &str = "target_numeric";
65
66const FROZEN_REPRESENTATION_TYPES: &[(&str, &str)] = &[
76 (REPRESENTATION_TABULAR_NUMERIC, "table"),
78 ("tabular_mixed", "table"),
79 ("signal_1d", "dense_signal"),
81 ("signal_with_processings", "dense_signal"),
82 ("feature_block_set", "multi_block"),
83 (REPRESENTATION_TARGET_NUMERIC, "target"),
85 ("target_categorical", "target"),
86 ("target_numeric_matrix", "target"),
87 ("target_categorical_matrix", "target"),
88 ("sample_metadata", "metadata"),
90];
91
92pub fn representation_type_id(representation_id: &str) -> Option<&'static str> {
97 FROZEN_REPRESENTATION_TYPES
98 .iter()
99 .find(|(id, _)| *id == representation_id)
100 .map(|(_, type_id)| *type_id)
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct ManifestKindTemplate {
109 pub supported_phases: BTreeSet<Phase>,
111 pub fit_scope: ControllerFitScope,
113 pub capabilities: BTreeSet<ControllerCapability>,
115 pub input_ports: Vec<PortSpec>,
117 pub output_ports: Vec<PortSpec>,
119}
120
121pub fn manifest_kind_template(kind: &NodeKind) -> ManifestKindTemplate {
129 let training_phases = || BTreeSet::from([Phase::FitCv, Phase::Refit, Phase::Predict]);
130 match kind {
131 NodeKind::Transform => ManifestKindTemplate {
132 supported_phases: training_phases(),
133 fit_scope: ControllerFitScope::FoldTrain,
134 capabilities: stateless_compute_capabilities(),
135 input_ports: vec![represented_port(
136 "x",
137 PortKind::Data,
138 REPRESENTATION_TABULAR_NUMERIC,
139 )],
140 output_ports: vec![represented_port(
141 "x_out",
142 PortKind::Data,
143 REPRESENTATION_TABULAR_NUMERIC,
144 )],
145 },
146 NodeKind::YTransform => ManifestKindTemplate {
147 supported_phases: training_phases(),
148 fit_scope: ControllerFitScope::FoldTrain,
149 capabilities: stateless_compute_capabilities(),
150 input_ports: vec![represented_port(
151 "y",
152 PortKind::Target,
153 REPRESENTATION_TARGET_NUMERIC,
154 )],
155 output_ports: vec![represented_port(
156 "y_out",
157 PortKind::Target,
158 REPRESENTATION_TARGET_NUMERIC,
159 )],
160 },
161 NodeKind::Model => ManifestKindTemplate {
162 supported_phases: training_phases(),
163 fit_scope: ControllerFitScope::FoldTrain,
164 capabilities: {
165 let mut capabilities = stateless_compute_capabilities();
166 capabilities.insert(ControllerCapability::EmitsPredictions);
167 capabilities.insert(ControllerCapability::EmitsArtifacts);
168 capabilities.insert(ControllerCapability::Stateful);
169 capabilities
170 },
171 input_ports: vec![represented_port(
172 "x",
173 PortKind::Data,
174 REPRESENTATION_TABULAR_NUMERIC,
175 )],
176 output_ports: vec![
177 opaque_port("y_hat", PortKind::Prediction, PortCardinality::One),
178 opaque_port("model", PortKind::Artifact, PortCardinality::One),
179 ],
180 },
181 NodeKind::PredictionJoin => ManifestKindTemplate {
182 supported_phases: training_phases(),
183 fit_scope: ControllerFitScope::FoldTrain,
184 capabilities: {
185 let mut capabilities = base_capabilities();
186 capabilities.insert(ControllerCapability::ConsumesOofPredictions);
187 capabilities.insert(ControllerCapability::EmitsPredictions);
188 capabilities
189 },
190 input_ports: vec![opaque_port(
191 "oof",
192 PortKind::Prediction,
193 PortCardinality::Many,
194 )],
195 output_ports: vec![opaque_port(
196 "oof",
197 PortKind::Prediction,
198 PortCardinality::One,
199 )],
200 },
201 _ => ManifestKindTemplate {
202 supported_phases: training_phases(),
203 fit_scope: ControllerFitScope::FoldTrain,
204 capabilities: base_capabilities(),
205 input_ports: Vec::new(),
206 output_ports: Vec::new(),
207 },
208 }
209}
210
211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct HostControllerSpec {
223 pub controller_id: String,
225 pub controller_version: String,
227 pub operator_kind: NodeKind,
229 #[serde(default)]
232 pub priority: u32,
233 #[serde(default)]
237 pub added_capabilities: BTreeSet<ControllerCapability>,
238 #[serde(default)]
241 pub operator_selectors: Vec<OperatorSelector>,
242 #[serde(default = "default_rng_policy")]
244 pub rng_policy: RngPolicy,
245 #[serde(default = "default_artifact_policy")]
247 pub artifact_policy: ArtifactPolicy,
248 #[serde(default)]
250 pub data_requirements: Option<serde_json::Value>,
251 #[serde(default)]
255 pub input_ports: Option<Vec<PortSpec>>,
256 #[serde(default)]
258 pub output_ports: Option<Vec<PortSpec>>,
259}
260
261impl HostControllerSpec {
262 pub fn new(
264 controller_id: impl Into<String>,
265 controller_version: impl Into<String>,
266 operator_kind: NodeKind,
267 ) -> Self {
268 Self {
269 controller_id: controller_id.into(),
270 controller_version: controller_version.into(),
271 operator_kind,
272 priority: 0,
273 added_capabilities: BTreeSet::new(),
274 operator_selectors: Vec::new(),
275 rng_policy: default_rng_policy(),
276 artifact_policy: default_artifact_policy(),
277 data_requirements: None,
278 input_ports: None,
279 output_ports: None,
280 }
281 }
282
283 pub fn derive(&self) -> Result<ControllerManifest> {
297 let ManifestKindTemplate {
298 supported_phases,
299 fit_scope,
300 mut capabilities,
301 input_ports,
302 output_ports,
303 } = manifest_kind_template(&self.operator_kind);
304 capabilities.extend(self.added_capabilities.iter().copied());
305
306 let input_ports = self.input_ports.clone().unwrap_or(input_ports);
307 let output_ports = self.output_ports.clone().unwrap_or(output_ports);
308 let data_requirements = match &self.data_requirements {
309 Some(requirements) => Some(requirements.clone()),
310 None => default_data_requirements(&input_ports)?,
311 };
312
313 let manifest = ControllerManifest {
314 controller_id: ControllerId::new(self.controller_id.clone())?,
315 controller_version: self.controller_version.clone(),
316 operator_kind: self.operator_kind.clone(),
317 priority: self.priority,
318 supported_phases,
319 input_ports,
320 output_ports,
321 data_requirements,
322 capabilities,
323 operator_selectors: self.operator_selectors.clone(),
324 fit_scope,
325 rng_policy: self.rng_policy,
326 artifact_policy: self.artifact_policy,
327 };
328 manifest.validate()?;
329 Ok(manifest)
330 }
331}
332
333pub fn derive_host_controller_registry(specs: &[HostControllerSpec]) -> Result<ControllerRegistry> {
339 let mut registry = ControllerRegistry::new();
340 for spec in specs {
341 registry.register(spec.derive()?)?;
342 }
343 Ok(registry)
344}
345
346fn default_rng_policy() -> RngPolicy {
347 RngPolicy::UsesCoreSeed
348}
349
350fn default_artifact_policy() -> ArtifactPolicy {
351 ArtifactPolicy::Serializable
352}
353
354fn base_capabilities() -> BTreeSet<ControllerCapability> {
355 BTreeSet::from([
356 ControllerCapability::Deterministic,
357 ControllerCapability::ThreadSafe,
358 ControllerCapability::ProcessSafe,
359 ])
360}
361
362fn stateless_compute_capabilities() -> BTreeSet<ControllerCapability> {
363 let mut capabilities = base_capabilities();
364 capabilities.insert(ControllerCapability::UsesCoreRng);
365 capabilities
366}
367
368fn represented_port(name: &str, kind: PortKind, representation: &str) -> PortSpec {
369 PortSpec {
370 name: name.to_string(),
371 kind,
372 representation: Some(representation.to_string()),
373 cardinality: PortCardinality::One,
374 unit_level: None,
375 alignment_key: None,
376 target_level: None,
377 description: String::new(),
378 }
379}
380
381fn default_data_requirements(input_ports: &[PortSpec]) -> Result<Option<serde_json::Value>> {
394 let mut ports = Vec::new();
395 for port in input_ports {
396 if !matches!(port.kind, PortKind::Data | PortKind::Target) {
397 continue;
398 }
399 let Some(representation) = port.representation.as_deref() else {
400 continue;
401 };
402 let Some(type_id) = representation_type_id(representation) else {
403 return Ok(None);
404 };
405 ports.push(ModelInputPortSpec {
406 name: port.name.clone(),
407 accepted_representations: vec![representation.to_string()],
408 accepted_types: vec![type_id.to_string()],
409 rank: None,
410 multi_source: false,
411 optional: matches!(port.cardinality, PortCardinality::Optional),
412 metadata: BTreeMap::new(),
413 });
414 }
415 if ports.is_empty() {
416 return Ok(None);
417 }
418 let spec = ModelInputSpec {
419 schema_version: MODEL_INPUT_SPEC_SCHEMA_VERSION,
420 ports,
421 default_fusion: None,
422 fit_influence_policy: None,
423 metadata: BTreeMap::new(),
424 };
425 let requirements = serde_json::to_value(&spec).map_err(|error| {
426 DagMlError::ControllerValidation(format!(
427 "failed to encode synthesized data_requirements: {error}"
428 ))
429 })?;
430 Ok(Some(requirements))
431}
432
433fn opaque_port(name: &str, kind: PortKind, cardinality: PortCardinality) -> PortSpec {
434 PortSpec {
435 name: name.to_string(),
436 kind,
437 representation: None,
438 cardinality,
439 unit_level: None,
440 alignment_key: None,
441 target_level: None,
442 description: String::new(),
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use std::collections::BTreeMap;
449
450 use serde_json::json;
451
452 use super::*;
453 use crate::graph::NodeSpec;
454 use crate::ids::NodeId;
455
456 const VERSION: &str = "0.10.0";
457
458 fn capabilities(values: &[ControllerCapability]) -> BTreeSet<ControllerCapability> {
459 values.iter().copied().collect()
460 }
461
462 fn node_with_operator(kind: NodeKind, operator: Option<serde_json::Value>) -> NodeSpec {
463 NodeSpec {
464 id: NodeId::new("node:under-test").unwrap(),
465 kind,
466 operator,
467 params: BTreeMap::new(),
468 ports: crate::graph::PortSchema::default(),
469 metadata: BTreeMap::new(),
470 seed_label: None,
471 }
472 }
473
474 #[test]
478 fn transform_template_matches_bridge_manifest() {
479 let mut spec = HostControllerSpec::new(
480 "controller:nirs4all.transform",
481 VERSION,
482 NodeKind::Transform,
483 );
484 spec.priority = 20;
485 let manifest = spec.derive().expect("transform derives");
486
487 assert_eq!(manifest.operator_kind, NodeKind::Transform);
488 assert_eq!(manifest.priority, 20);
489 assert_eq!(
490 manifest.supported_phases,
491 BTreeSet::from([Phase::FitCv, Phase::Refit, Phase::Predict])
492 );
493 assert_eq!(
494 manifest.capabilities,
495 capabilities(&[
496 ControllerCapability::Deterministic,
497 ControllerCapability::ThreadSafe,
498 ControllerCapability::ProcessSafe,
499 ControllerCapability::UsesCoreRng,
500 ])
501 );
502 assert_eq!(
503 manifest.input_ports,
504 vec![represented_port(
505 "x",
506 PortKind::Data,
507 REPRESENTATION_TABULAR_NUMERIC
508 )]
509 );
510 assert_eq!(
511 manifest.output_ports,
512 vec![represented_port(
513 "x_out",
514 PortKind::Data,
515 REPRESENTATION_TABULAR_NUMERIC
516 )]
517 );
518 assert_eq!(manifest.fit_scope, ControllerFitScope::FoldTrain);
519 assert_eq!(manifest.rng_policy, RngPolicy::UsesCoreSeed);
520 assert_eq!(manifest.artifact_policy, ArtifactPolicy::Serializable);
521 assert!(manifest.operator_selectors.is_empty());
522 }
523
524 #[test]
525 fn y_transform_template_targets_y_ports() {
526 let manifest = HostControllerSpec::new(
527 "controller:nirs4all.y_transform",
528 VERSION,
529 NodeKind::YTransform,
530 )
531 .derive()
532 .expect("y_transform derives");
533
534 assert_eq!(
535 manifest.input_ports,
536 vec![represented_port(
537 "y",
538 PortKind::Target,
539 REPRESENTATION_TARGET_NUMERIC
540 )]
541 );
542 assert_eq!(
543 manifest.output_ports,
544 vec![represented_port(
545 "y_out",
546 PortKind::Target,
547 REPRESENTATION_TARGET_NUMERIC
548 )]
549 );
550 assert_eq!(
551 manifest.capabilities,
552 capabilities(&[
553 ControllerCapability::Deterministic,
554 ControllerCapability::ThreadSafe,
555 ControllerCapability::ProcessSafe,
556 ControllerCapability::UsesCoreRng,
557 ])
558 );
559 }
560
561 #[test]
562 fn model_template_emits_prediction_and_artifact_ports() {
563 let manifest =
564 HostControllerSpec::new("controller:nirs4all.model", VERSION, NodeKind::Model)
565 .derive()
566 .expect("model derives");
567
568 assert_eq!(
569 manifest.capabilities,
570 capabilities(&[
571 ControllerCapability::Deterministic,
572 ControllerCapability::ThreadSafe,
573 ControllerCapability::ProcessSafe,
574 ControllerCapability::UsesCoreRng,
575 ControllerCapability::EmitsPredictions,
576 ControllerCapability::EmitsArtifacts,
577 ControllerCapability::Stateful,
578 ])
579 );
580 assert_eq!(
581 manifest.input_ports,
582 vec![represented_port(
583 "x",
584 PortKind::Data,
585 REPRESENTATION_TABULAR_NUMERIC
586 )]
587 );
588 assert_eq!(
589 manifest.output_ports,
590 vec![
591 opaque_port("y_hat", PortKind::Prediction, PortCardinality::One),
592 opaque_port("model", PortKind::Artifact, PortCardinality::One),
593 ]
594 );
595 }
596
597 #[test]
598 fn prediction_join_template_matches_merge_concat() {
599 let manifest = HostControllerSpec::new(
600 "controller:nirs4all.merge_concat",
601 VERSION,
602 NodeKind::PredictionJoin,
603 )
604 .derive()
605 .expect("prediction_join derives");
606
607 assert_eq!(
608 manifest.capabilities,
609 capabilities(&[
610 ControllerCapability::Deterministic,
611 ControllerCapability::ThreadSafe,
612 ControllerCapability::ProcessSafe,
613 ControllerCapability::ConsumesOofPredictions,
614 ControllerCapability::EmitsPredictions,
615 ])
616 );
617 assert_eq!(
618 manifest.input_ports,
619 vec![opaque_port(
620 "oof",
621 PortKind::Prediction,
622 PortCardinality::Many
623 )]
624 );
625 assert_eq!(
626 manifest.output_ports,
627 vec![opaque_port(
628 "oof",
629 PortKind::Prediction,
630 PortCardinality::One
631 )]
632 );
633 }
634
635 #[test]
639 fn meta_model_specialization_overrides_ports_and_caps() {
640 let mut spec =
641 HostControllerSpec::new("controller:nirs4all.meta_model", VERSION, NodeKind::Model);
642 spec.priority = 20;
643 spec.added_capabilities
644 .insert(ControllerCapability::ConsumesOofPredictions);
645 spec.input_ports = Some(vec![opaque_port(
646 "oof",
647 PortKind::Prediction,
648 PortCardinality::Many,
649 )]);
650 spec.operator_selectors.push(OperatorSelector {
651 refs: BTreeSet::from(["nirs4all.meta_model".to_string()]),
652 ..OperatorSelector::default()
653 });
654 let manifest = spec.derive().expect("meta_model derives");
655
656 assert_eq!(
657 manifest.capabilities,
658 capabilities(&[
659 ControllerCapability::Deterministic,
660 ControllerCapability::ThreadSafe,
661 ControllerCapability::ProcessSafe,
662 ControllerCapability::UsesCoreRng,
663 ControllerCapability::ConsumesOofPredictions,
664 ControllerCapability::EmitsPredictions,
665 ControllerCapability::EmitsArtifacts,
666 ControllerCapability::Stateful,
667 ])
668 );
669 assert_eq!(
670 manifest.input_ports,
671 vec![opaque_port(
672 "oof",
673 PortKind::Prediction,
674 PortCardinality::Many
675 )]
676 );
677 assert_eq!(
679 manifest.output_ports,
680 vec![
681 opaque_port("y_hat", PortKind::Prediction, PortCardinality::One),
682 opaque_port("model", PortKind::Artifact, PortCardinality::One),
683 ]
684 );
685 assert_eq!(manifest.operator_selectors.len(), 1);
686 }
687
688 #[test]
692 fn selector_specialization_outranks_generic_in_registry() {
693 let mut pls = HostControllerSpec::new("controller:methods.pls", VERSION, NodeKind::Model);
694 pls.priority = 10;
695 pls.operator_selectors.push(OperatorSelector {
696 aliases: BTreeSet::from(["PLSRegression".to_string(), "PLS".to_string()]),
697 ..OperatorSelector::default()
698 });
699 let registry = derive_host_controller_registry(&[
700 HostControllerSpec::new("controller:nirs4all.model", VERSION, NodeKind::Model),
701 pls,
702 ])
703 .expect("registry derives");
704
705 let pls_node = node_with_operator(NodeKind::Model, Some(json!({"class": "PLSRegression"})));
706 assert_eq!(
707 registry
708 .resolve_for_node(&pls_node)
709 .unwrap()
710 .controller_id
711 .as_str(),
712 "controller:methods.pls"
713 );
714
715 let generic_node = node_with_operator(NodeKind::Model, Some(json!({"class": "Ridge"})));
716 assert_eq!(
717 registry
718 .resolve_for_node(&generic_node)
719 .unwrap()
720 .controller_id
721 .as_str(),
722 "controller:nirs4all.model"
723 );
724 }
725
726 #[test]
727 fn derive_propagates_validation_failure_for_empty_version() {
728 let spec = HostControllerSpec::new("controller:nirs4all.model", "", NodeKind::Model);
729 let error = spec.derive().unwrap_err().to_string();
730 assert!(error.contains("empty version"), "unexpected error: {error}");
731 }
732
733 #[test]
736 fn derive_rejects_override_that_violates_capability_invariant() {
737 let mut spec =
738 HostControllerSpec::new("controller:bad.transform", VERSION, NodeKind::Transform);
739 spec.output_ports = Some(vec![opaque_port(
740 "leak",
741 PortKind::Prediction,
742 PortCardinality::One,
743 )]);
744 let error = spec.derive().unwrap_err().to_string();
745 assert!(
746 error.contains("lacks emits_predictions"),
747 "unexpected error: {error}"
748 );
749 }
750
751 #[test]
752 fn generic_template_for_unmapped_kind_validates() {
753 let manifest = HostControllerSpec::new("controller:host.tag", VERSION, NodeKind::Tag)
755 .derive()
756 .expect("tag derives");
757 assert!(manifest.input_ports.is_empty());
758 assert!(manifest.output_ports.is_empty());
759 assert_eq!(
760 manifest.capabilities,
761 capabilities(&[
762 ControllerCapability::Deterministic,
763 ControllerCapability::ThreadSafe,
764 ControllerCapability::ProcessSafe,
765 ])
766 );
767 }
768
769 #[test]
770 fn host_controller_spec_round_trips_through_json() {
771 let mut spec =
772 HostControllerSpec::new("controller:nirs4all.model", VERSION, NodeKind::Model);
773 spec.priority = 20;
774 spec.added_capabilities
775 .insert(ControllerCapability::NeedsPythonGil);
776 let encoded = serde_json::to_string(&spec).expect("encode");
777 let decoded: HostControllerSpec = serde_json::from_str(&encoded).expect("decode");
778 assert_eq!(spec, decoded);
779 assert_eq!(spec.derive().unwrap(), decoded.derive().unwrap());
781 }
782
783 #[test]
784 fn minimal_json_descriptor_applies_defaults() {
785 let spec: HostControllerSpec = serde_json::from_value(json!({
787 "controller_id": "controller:nirs4all.transform",
788 "controller_version": VERSION,
789 "operator_kind": "transform",
790 }))
791 .expect("decode minimal");
792 assert_eq!(spec.priority, 0);
793 assert_eq!(spec.rng_policy, RngPolicy::UsesCoreSeed);
794 assert_eq!(spec.artifact_policy, ArtifactPolicy::Serializable);
795 let manifest = spec.derive().expect("derives");
796 assert_eq!(manifest.operator_kind, NodeKind::Transform);
797 }
798
799 #[test]
805 fn representation_type_id_maps_frozen_registry_ids() {
806 assert_eq!(
807 representation_type_id(REPRESENTATION_TABULAR_NUMERIC),
808 Some("table")
809 );
810 assert_eq!(
811 representation_type_id(REPRESENTATION_TARGET_NUMERIC),
812 Some("target")
813 );
814 assert_eq!(representation_type_id("signal_1d"), Some("dense_signal"));
815 assert_eq!(
816 representation_type_id("feature_block_set"),
817 Some("multi_block")
818 );
819 assert_eq!(representation_type_id("sample_metadata"), Some("metadata"));
820 assert_eq!(representation_type_id("not_a_real_representation"), None);
821 }
822
823 #[test]
826 fn model_template_synthesizes_tabular_data_requirements() {
827 let manifest =
828 HostControllerSpec::new("controller:nirs4all.model", VERSION, NodeKind::Model)
829 .derive()
830 .expect("model derives");
831 assert_eq!(
832 manifest.input_ports[0].representation.as_deref(),
833 Some(REPRESENTATION_TABULAR_NUMERIC)
834 );
835 let spec = manifest
836 .model_input_spec()
837 .expect("data_requirements parse")
838 .expect("model has synthesized data_requirements");
839 assert_eq!(spec.schema_version, MODEL_INPUT_SPEC_SCHEMA_VERSION);
840 assert_eq!(spec.ports.len(), 1);
841 assert_eq!(spec.ports[0].name, "x");
842 assert_eq!(
843 spec.ports[0].accepted_representations,
844 vec![REPRESENTATION_TABULAR_NUMERIC.to_string()]
845 );
846 assert_eq!(spec.ports[0].accepted_types, vec!["table".to_string()]);
847 assert!(!spec.ports[0].optional);
848 }
849
850 #[test]
854 fn y_transform_synthesizes_target_data_requirements() {
855 let manifest = HostControllerSpec::new(
856 "controller:nirs4all.y_transform",
857 VERSION,
858 NodeKind::YTransform,
859 )
860 .derive()
861 .expect("y_transform derives");
862 assert_eq!(
863 manifest.input_ports[0].representation.as_deref(),
864 Some(REPRESENTATION_TARGET_NUMERIC)
865 );
866 let spec = manifest
867 .model_input_spec()
868 .expect("data_requirements parse")
869 .expect("y_transform has synthesized data_requirements");
870 assert_eq!(spec.ports.len(), 1);
871 assert_eq!(spec.ports[0].name, "y");
872 assert_eq!(
873 spec.ports[0].accepted_representations,
874 vec![REPRESENTATION_TARGET_NUMERIC.to_string()]
875 );
876 assert_eq!(spec.ports[0].accepted_types, vec!["target".to_string()]);
877 }
878
879 #[test]
882 fn prediction_join_has_no_data_requirements() {
883 let manifest = HostControllerSpec::new(
884 "controller:nirs4all.merge_concat",
885 VERSION,
886 NodeKind::PredictionJoin,
887 )
888 .derive()
889 .expect("prediction_join derives");
890 assert!(manifest.data_requirements.is_none());
891 assert!(manifest.model_input_spec().unwrap().is_none());
892 }
893
894 #[test]
897 fn host_supplied_data_requirements_take_precedence_over_synthesis() {
898 let mut spec =
899 HostControllerSpec::new("controller:nirs4all.model", VERSION, NodeKind::Model);
900 spec.data_requirements = Some(json!({
901 "schema_version": 1,
902 "ports": [{
903 "name": "x",
904 "accepted_representations": ["signal_1d", "signal_with_processings"],
905 "accepted_types": ["dense_signal"],
906 }],
907 }));
908 let manifest = spec.derive().expect("model derives");
909 let parsed = manifest
910 .model_input_spec()
911 .expect("parse")
912 .expect("present");
913 assert_eq!(
914 parsed.ports[0].accepted_representations,
915 vec![
916 "signal_1d".to_string(),
917 "signal_with_processings".to_string()
918 ]
919 );
920 }
921
922 #[test]
926 fn port_override_with_known_representation_resyncs_data_requirements() {
927 let mut spec = HostControllerSpec::new("controller:methods.pls", VERSION, NodeKind::Model);
928 spec.input_ports = Some(vec![represented_port("x", PortKind::Data, "signal_1d")]);
929 let manifest = spec.derive().expect("model derives");
930 let parsed = manifest.model_input_spec().unwrap().expect("present");
931 assert_eq!(parsed.ports.len(), 1);
932 assert_eq!(
933 parsed.ports[0].accepted_representations,
934 vec!["signal_1d".to_string()]
935 );
936 assert_eq!(
937 parsed.ports[0].accepted_types,
938 vec!["dense_signal".to_string()]
939 );
940 }
941
942 #[test]
945 fn port_override_with_unknown_representation_skips_synthesis() {
946 let mut spec = HostControllerSpec::new("controller:methods.pls", VERSION, NodeKind::Model);
947 spec.input_ports = Some(vec![represented_port(
948 "x",
949 PortKind::Data,
950 "totally_unregistered_representation",
951 )]);
952 let manifest = spec.derive().expect("model derives");
953 assert!(
954 manifest.data_requirements.is_none(),
955 "unknown representation must not be auto-typed"
956 );
957 }
958}