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