Skip to main content

dag_ml_data_core/
handle.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3
4use serde::{Deserialize, Serialize};
5
6use crate::coordinator::{
7    parse_native_branch_view_filter, validate_fingerprint, CoordinatorDataPlanEnvelope,
8    CoordinatorRelation, CoordinatorRelationSet,
9};
10use crate::error::{DataError, Result};
11use crate::ids::{ObservationId, RepresentationId, SampleId, SourceId, TargetId};
12use crate::model::DataView;
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum CoordinatorHandleKind {
17    Data,
18    View,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22pub struct CoordinatorHandleRef {
23    pub handle: u64,
24    pub kind: CoordinatorHandleKind,
25    pub owner_controller: String,
26}
27
28#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
29pub struct CoordinatorDataMaterializationRequest {
30    pub run_id: String,
31    pub node_id: String,
32    pub input_name: String,
33    pub phase: String,
34    #[serde(default)]
35    pub variant_id: Option<String>,
36    #[serde(default)]
37    pub fold_id: Option<String>,
38    pub request_id: String,
39    pub schema_fingerprint: String,
40    pub plan_fingerprint: String,
41    #[serde(default)]
42    pub relation_fingerprint: Option<String>,
43    pub output_representation: RepresentationId,
44    #[serde(default)]
45    pub source_ids: Vec<SourceId>,
46    #[serde(default)]
47    pub require_relations: bool,
48    /// Explicit cohort authority for a companion read, supplied by DAG.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub predict_cohort: Option<crate::PredictCohort>,
51}
52
53impl CoordinatorDataMaterializationRequest {
54    pub fn validate(&self) -> Result<()> {
55        validate_non_empty("run_id", &self.run_id)?;
56        validate_non_empty("node_id", &self.node_id)?;
57        validate_non_empty("input_name", &self.input_name)?;
58        validate_non_empty("phase", &self.phase)?;
59        validate_non_empty("request_id", &self.request_id)?;
60        validate_fingerprint("schema", &self.schema_fingerprint)?;
61        validate_fingerprint("plan", &self.plan_fingerprint)?;
62        if let Some(relation_fingerprint) = &self.relation_fingerprint {
63            validate_fingerprint("relation", relation_fingerprint)?;
64        } else if self.require_relations {
65            return Err(DataError::Validation(format!(
66                "materialization request `{}` on `{}` requires relations but has no relation_fingerprint",
67                self.input_name, self.node_id
68            )));
69        }
70        let unique_sources = self.source_ids.iter().collect::<BTreeSet<_>>();
71        if unique_sources.len() != self.source_ids.len() {
72            return Err(DataError::Validation(format!(
73                "materialization request `{}` on `{}` contains duplicate source ids",
74                self.input_name, self.node_id
75            )));
76        }
77        Ok(())
78    }
79}
80
81#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
82pub struct CoordinatorDataHandleRecord {
83    pub handle: CoordinatorHandleRef,
84    pub run_id: String,
85    pub node_id: String,
86    pub input_name: String,
87    pub phase: String,
88    #[serde(default)]
89    pub variant_id: Option<String>,
90    #[serde(default)]
91    pub fold_id: Option<String>,
92    pub request_id: String,
93    pub schema_fingerprint: String,
94    pub plan_fingerprint: String,
95    #[serde(default)]
96    pub relation_fingerprint: Option<String>,
97    pub plan_id: String,
98    pub output_representation: RepresentationId,
99    #[serde(default)]
100    pub source_ids: Vec<SourceId>,
101    #[serde(default)]
102    pub sample_count: Option<usize>,
103    #[serde(default)]
104    pub relation_record_count: Option<usize>,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
108pub struct CoordinatorDataViewRecord {
109    pub handle: CoordinatorHandleRef,
110    pub parent_handle: CoordinatorHandleRef,
111    pub view: DataView,
112    pub sample_count: usize,
113    pub relation_record_count: usize,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117pub struct CoordinatorTargetValue {
118    pub sample_id: SampleId,
119    pub value: serde_json::Value,
120}
121
122#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
123pub struct CoordinatorTargetTable {
124    pub target_id: TargetId,
125    pub values: Vec<CoordinatorTargetValue>,
126}
127
128impl CoordinatorTargetTable {
129    pub fn validate(&self) -> Result<()> {
130        if self.values.is_empty() {
131            return Err(DataError::Validation(format!(
132                "target table `{}` contains no values",
133                self.target_id
134            )));
135        }
136        let mut seen = BTreeSet::new();
137        for value in &self.values {
138            if !seen.insert(&value.sample_id) {
139                return Err(DataError::Validation(format!(
140                    "target table `{}` contains duplicate sample `{}`",
141                    self.target_id, value.sample_id
142                )));
143            }
144        }
145        Ok(())
146    }
147}
148
149#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150pub struct CoordinatorTargetBlock {
151    pub target_id: TargetId,
152    pub sample_ids: Vec<SampleId>,
153    pub values: Vec<serde_json::Value>,
154}
155
156#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157pub struct CoordinatorMultiTargetBlock {
158    pub target_ids: Vec<TargetId>,
159    pub sample_ids: Vec<SampleId>,
160    /// Target-major values: `values[target_idx][sample_idx]`.
161    pub values: Vec<Vec<serde_json::Value>>,
162    /// Target-major validity masks aligned with `values`.
163    pub validity_masks: Vec<Vec<bool>>,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct CoordinatorFeatureRow {
168    pub observation_id: ObservationId,
169    pub values: Vec<serde_json::Value>,
170}
171
172#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
173pub struct CoordinatorFeatureTable {
174    pub feature_set_id: String,
175    pub representation_id: RepresentationId,
176    pub feature_names: Vec<String>,
177    pub rows: Vec<CoordinatorFeatureRow>,
178}
179
180impl CoordinatorFeatureTable {
181    pub fn validate(&self) -> Result<()> {
182        validate_non_empty("feature_set_id", &self.feature_set_id)?;
183        if self.feature_names.is_empty() {
184            return Err(DataError::Validation(format!(
185                "feature table `{}` contains no features",
186                self.feature_set_id
187            )));
188        }
189        let mut seen_features = BTreeSet::new();
190        for feature_name in &self.feature_names {
191            validate_non_empty("feature_name", feature_name)?;
192            if !seen_features.insert(feature_name) {
193                return Err(DataError::Validation(format!(
194                    "feature table `{}` contains duplicate feature `{}`",
195                    self.feature_set_id, feature_name
196                )));
197            }
198        }
199        if self.rows.is_empty() {
200            return Err(DataError::Validation(format!(
201                "feature table `{}` contains no rows",
202                self.feature_set_id
203            )));
204        }
205        let mut seen_observations = BTreeSet::new();
206        for row in &self.rows {
207            if !seen_observations.insert(&row.observation_id) {
208                return Err(DataError::Validation(format!(
209                    "feature table `{}` contains duplicate observation `{}`",
210                    self.feature_set_id, row.observation_id
211                )));
212            }
213            if row.values.len() != self.feature_names.len() {
214                return Err(DataError::Validation(format!(
215                    "feature table `{}` row `{}` has {} values for {} features",
216                    self.feature_set_id,
217                    row.observation_id,
218                    row.values.len(),
219                    self.feature_names.len()
220                )));
221            }
222        }
223        Ok(())
224    }
225}
226
227#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228pub struct CoordinatorFeatureBlock {
229    pub feature_set_id: String,
230    pub representation_id: RepresentationId,
231    pub feature_names: Vec<String>,
232    pub observation_ids: Vec<ObservationId>,
233    pub sample_ids: Vec<SampleId>,
234    pub values: Vec<Vec<serde_json::Value>>,
235}
236
237/// Typed sibling of [`CoordinatorFeatureBlock`]: the same projection with the
238/// cells flattened row-major into one `Vec<f64>` instead of per-cell JSON
239/// values. Produced by the `*_f64` projection path so a large numeric block
240/// never allocates `rows × cols` boxed [`serde_json::Value`]s. Masked cells
241/// are rejected by that path (it has no `Null` to project them into).
242#[derive(Clone, Debug, PartialEq)]
243pub struct CoordinatorFeatureBlockF64 {
244    pub feature_set_id: String,
245    pub representation_id: RepresentationId,
246    pub feature_names: Vec<String>,
247    pub observation_ids: Vec<ObservationId>,
248    pub sample_ids: Vec<SampleId>,
249    pub values: Vec<f64>,
250}
251
252#[derive(Debug)]
253pub struct CoordinatorHandleArena {
254    owner_controller: String,
255    next_handle: RefCell<u64>,
256    records: RefCell<BTreeMap<u64, CoordinatorDataHandleRecord>>,
257    data_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
258    view_records: RefCell<BTreeMap<u64, CoordinatorDataViewRecord>>,
259    view_relations: RefCell<BTreeMap<u64, CoordinatorRelationSet>>,
260}
261
262impl CoordinatorHandleArena {
263    pub fn new(owner_controller: impl Into<String>) -> Result<Self> {
264        let owner_controller = owner_controller.into();
265        validate_non_empty("owner_controller", &owner_controller)?;
266        Ok(Self {
267            owner_controller,
268            next_handle: RefCell::new(1),
269            records: RefCell::new(BTreeMap::new()),
270            data_relations: RefCell::new(BTreeMap::new()),
271            view_records: RefCell::new(BTreeMap::new()),
272            view_relations: RefCell::new(BTreeMap::new()),
273        })
274    }
275
276    pub fn materialize(
277        &self,
278        envelope: &CoordinatorDataPlanEnvelope,
279        request: &CoordinatorDataMaterializationRequest,
280    ) -> Result<CoordinatorDataHandleRecord> {
281        envelope.validate()?;
282        request.validate()?;
283        validate_request_against_envelope(envelope, request)?;
284        let scoped_relations = materialization_relations(envelope, request)?
285            .map(|relations| scoped_relations_for_materialization(relations, request))
286            .transpose()?;
287
288        let handle = CoordinatorHandleRef {
289            handle: self.next_handle(),
290            kind: CoordinatorHandleKind::Data,
291            owner_controller: self.owner_controller.clone(),
292        };
293        let record = CoordinatorDataHandleRecord {
294            handle: handle.clone(),
295            run_id: request.run_id.clone(),
296            node_id: request.node_id.clone(),
297            input_name: request.input_name.clone(),
298            phase: request.phase.clone(),
299            variant_id: request.variant_id.clone(),
300            fold_id: request.fold_id.clone(),
301            request_id: request.request_id.clone(),
302            schema_fingerprint: request.schema_fingerprint.clone(),
303            plan_fingerprint: request.plan_fingerprint.clone(),
304            relation_fingerprint: request.relation_fingerprint.clone(),
305            plan_id: envelope.plan.id.clone(),
306            output_representation: request.output_representation.clone(),
307            source_ids: request.source_ids.clone(),
308            sample_count: scoped_relations.as_ref().map(|relations| {
309                relations
310                    .records
311                    .iter()
312                    .map(|record| &record.sample_id)
313                    .collect::<BTreeSet<_>>()
314                    .len()
315            }),
316            relation_record_count: scoped_relations
317                .as_ref()
318                .map(|relations| relations.records.len()),
319        };
320        self.records
321            .borrow_mut()
322            .insert(handle.handle, record.clone());
323        if let Some(relations) = scoped_relations {
324            self.data_relations
325                .borrow_mut()
326                .insert(handle.handle, relations);
327        }
328        Ok(record)
329    }
330
331    pub fn make_view(
332        &self,
333        data_handle: u64,
334        view: &DataView,
335    ) -> Result<CoordinatorDataViewRecord> {
336        validate_view(view)?;
337        let parent =
338            self.records
339                .borrow()
340                .get(&data_handle)
341                .cloned()
342                .ok_or(DataError::UnknownHandle {
343                    kind: "data",
344                    handle: data_handle,
345                })?;
346        let relations = self
347            .data_relations
348            .borrow()
349            .get(&data_handle)
350            .cloned()
351            .ok_or_else(|| {
352                DataError::Validation(format!(
353                    "data handle `{data_handle}` has no coordinator relations"
354                ))
355            })?;
356        let selectors = resolved_view_labels(view, &parent)?;
357        let scoped = relations
358            .records
359            .into_iter()
360            .filter(|relation| {
361                selectors.iter().all(|(key, label)| {
362                    relation
363                        .metadata
364                        .get(*key)
365                        .and_then(serde_json::Value::as_str)
366                        == Some(*label)
367                })
368            })
369            .collect::<Vec<_>>();
370        let filtered = filter_relations(&scoped, view)?;
371        let sample_count = unique_sample_count(&filtered);
372        let relation_record_count = filtered.len();
373        let handle = CoordinatorHandleRef {
374            handle: self.next_handle(),
375            kind: CoordinatorHandleKind::View,
376            owner_controller: self.owner_controller.clone(),
377        };
378        let record = CoordinatorDataViewRecord {
379            handle: handle.clone(),
380            parent_handle: parent.handle,
381            view: view.clone(),
382            sample_count,
383            relation_record_count,
384        };
385        self.view_records
386            .borrow_mut()
387            .insert(handle.handle, record.clone());
388        self.view_relations
389            .borrow_mut()
390            .insert(handle.handle, CoordinatorRelationSet { records: filtered });
391        Ok(record)
392    }
393
394    pub fn view_record(&self, handle: u64) -> Option<CoordinatorDataViewRecord> {
395        self.view_records.borrow().get(&handle).cloned()
396    }
397
398    pub fn view_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
399        self.view_relations
400            .borrow()
401            .get(&handle)
402            .cloned()
403            .ok_or(DataError::UnknownHandle {
404                kind: "view",
405                handle,
406            })
407    }
408
409    pub fn data_identity(&self, handle: u64) -> Result<CoordinatorRelationSet> {
410        // A handle is only "unknown" if it is absent from the data-handle
411        // registry. A live handle materialized without scoped relations is
412        // present in `records` but has no `data_relations` entry — that is a
413        // missing-relations contract error, not an unknown/stale handle.
414        if !self.records.borrow().contains_key(&handle) {
415            return Err(DataError::UnknownHandle {
416                kind: "data",
417                handle,
418            });
419        }
420        self.data_relations
421            .borrow()
422            .get(&handle)
423            .cloned()
424            .ok_or_else(|| {
425                DataError::Validation(format!(
426                    "data handle `{handle}` has no coordinator relations"
427                ))
428            })
429    }
430
431    pub fn release_handle(&self, handle: u64) -> bool {
432        if self.view_records.borrow_mut().remove(&handle).is_some() {
433            self.view_relations.borrow_mut().remove(&handle);
434            return true;
435        }
436        if let Some(record) = self.records.borrow_mut().remove(&handle) {
437            self.data_relations.borrow_mut().remove(&handle);
438            let child_views = self
439                .view_records
440                .borrow()
441                .iter()
442                .filter_map(|(view_handle, view_record)| {
443                    (view_record.parent_handle == record.handle).then_some(*view_handle)
444                })
445                .collect::<Vec<_>>();
446            for view_handle in child_views {
447                self.view_records.borrow_mut().remove(&view_handle);
448                self.view_relations.borrow_mut().remove(&view_handle);
449            }
450            return true;
451        }
452        false
453    }
454
455    pub fn target_values(
456        &self,
457        view_handle: u64,
458        target_table: &CoordinatorTargetTable,
459    ) -> Result<CoordinatorTargetBlock> {
460        target_table.validate()?;
461        let relations = self.view_identity(view_handle)?;
462        let values_by_sample = target_table
463            .values
464            .iter()
465            .map(|value| (&value.sample_id, &value.value))
466            .collect::<BTreeMap<_, _>>();
467        let mut seen_samples = BTreeSet::new();
468        let mut sample_ids = Vec::new();
469        let mut values = Vec::new();
470        for relation in relations.records.iter().filter(|relation| {
471            relation
472                .target_id
473                .as_ref()
474                .map(|target_id| target_id == &target_table.target_id)
475                .unwrap_or(true)
476        }) {
477            if !seen_samples.insert(&relation.sample_id) {
478                continue;
479            }
480            let value = values_by_sample.get(&relation.sample_id).ok_or_else(|| {
481                DataError::Validation(format!(
482                    "target table `{}` has no value for sample `{}`",
483                    target_table.target_id, relation.sample_id
484                ))
485            })?;
486            sample_ids.push(relation.sample_id.clone());
487            values.push((*value).clone());
488        }
489        if sample_ids.is_empty() {
490            return Err(DataError::Validation(format!(
491                "view `{view_handle}` contains no samples for target `{}`",
492                target_table.target_id
493            )));
494        }
495        Ok(CoordinatorTargetBlock {
496            target_id: target_table.target_id.clone(),
497            sample_ids,
498            values,
499        })
500    }
501
502    pub fn multi_target_values(
503        &self,
504        view_handle: u64,
505        target_tables: &[CoordinatorTargetTable],
506    ) -> Result<CoordinatorMultiTargetBlock> {
507        if target_tables.is_empty() {
508            return Err(DataError::Validation(
509                "multi-target materialization requires at least one target table".to_string(),
510            ));
511        }
512        let mut seen_targets = BTreeSet::new();
513        for table in target_tables {
514            table.validate()?;
515            if !seen_targets.insert(table.target_id.clone()) {
516                return Err(DataError::Validation(format!(
517                    "multi-target materialization contains duplicate target `{}`",
518                    table.target_id
519                )));
520            }
521        }
522
523        let target_ids = target_tables
524            .iter()
525            .map(|table| table.target_id.clone())
526            .collect::<Vec<_>>();
527        let target_universe = target_ids.iter().collect::<BTreeSet<_>>();
528        let relations = self.view_identity(view_handle)?;
529        let mut seen_samples = BTreeSet::new();
530        let mut sample_ids = Vec::new();
531        for relation in relations.records.iter().filter(|relation| {
532            relation
533                .target_id
534                .as_ref()
535                .map(|target_id| target_universe.contains(target_id))
536                .unwrap_or(true)
537        }) {
538            if seen_samples.insert(relation.sample_id.clone()) {
539                sample_ids.push(relation.sample_id.clone());
540            }
541        }
542        if sample_ids.is_empty() {
543            return Err(DataError::Validation(format!(
544                "view `{view_handle}` contains no samples for requested targets"
545            )));
546        }
547
548        let mut values = Vec::with_capacity(target_tables.len());
549        let mut validity_masks = Vec::with_capacity(target_tables.len());
550        for table in target_tables {
551            let values_by_sample = table
552                .values
553                .iter()
554                .map(|value| (&value.sample_id, &value.value))
555                .collect::<BTreeMap<_, _>>();
556            let mut target_values = Vec::with_capacity(sample_ids.len());
557            let mut validity = Vec::with_capacity(sample_ids.len());
558            for sample_id in &sample_ids {
559                match values_by_sample.get(sample_id) {
560                    Some(value) if !value.is_null() => {
561                        target_values.push((*value).clone());
562                        validity.push(true);
563                    }
564                    Some(_) | None => {
565                        target_values.push(serde_json::Value::Null);
566                        validity.push(false);
567                    }
568                }
569            }
570            values.push(target_values);
571            validity_masks.push(validity);
572        }
573
574        Ok(CoordinatorMultiTargetBlock {
575            target_ids,
576            sample_ids,
577            values,
578            validity_masks,
579        })
580    }
581
582    pub fn feature_values(
583        &self,
584        view_handle: u64,
585        feature_table: &CoordinatorFeatureTable,
586    ) -> Result<CoordinatorFeatureBlock> {
587        feature_table.validate()?;
588        let view_record = self
589            .view_records
590            .borrow()
591            .get(&view_handle)
592            .cloned()
593            .ok_or(DataError::UnknownHandle {
594                kind: "view",
595                handle: view_handle,
596            })?;
597        let parent_record = self
598            .records
599            .borrow()
600            .get(&view_record.parent_handle.handle)
601            .cloned()
602            .ok_or(DataError::UnknownHandle {
603                kind: "data",
604                handle: view_record.parent_handle.handle,
605            })?;
606        if feature_table.representation_id != parent_record.output_representation {
607            return Err(DataError::Validation(format!(
608                "feature table `{}` representation `{}` does not match materialized output representation `{}`",
609                feature_table.feature_set_id,
610                feature_table.representation_id,
611                parent_record.output_representation
612            )));
613        }
614        let relations = self.view_identity(view_handle)?;
615        let selected_indices = selected_feature_indices(feature_table, &view_record.view)?;
616        let rows_by_observation = feature_table
617            .rows
618            .iter()
619            .map(|row| (&row.observation_id, row))
620            .collect::<BTreeMap<_, _>>();
621        let mut observation_ids = Vec::with_capacity(relations.records.len());
622        let mut sample_ids = Vec::with_capacity(relations.records.len());
623        let mut values = Vec::with_capacity(relations.records.len());
624        for relation in &relations.records {
625            let row = rows_by_observation
626                .get(&relation.observation_id)
627                .ok_or_else(|| {
628                    DataError::Validation(format!(
629                        "feature table `{}` has no row for observation `{}`",
630                        feature_table.feature_set_id, relation.observation_id
631                    ))
632                })?;
633            observation_ids.push(relation.observation_id.clone());
634            sample_ids.push(relation.sample_id.clone());
635            values.push(
636                selected_indices
637                    .iter()
638                    .map(|idx| row.values[*idx].clone())
639                    .collect(),
640            );
641        }
642        Ok(CoordinatorFeatureBlock {
643            feature_set_id: feature_table.feature_set_id.clone(),
644            representation_id: feature_table.representation_id.clone(),
645            feature_names: selected_indices
646                .iter()
647                .map(|idx| feature_table.feature_names[*idx].clone())
648                .collect(),
649            observation_ids,
650            sample_ids,
651            values,
652        })
653    }
654
655    pub fn handle_record(&self, handle: u64) -> Option<CoordinatorDataHandleRecord> {
656        self.records.borrow().get(&handle).cloned()
657    }
658
659    pub fn handle_records(&self) -> Vec<CoordinatorDataHandleRecord> {
660        self.records.borrow().values().cloned().collect()
661    }
662
663    fn next_handle(&self) -> u64 {
664        let mut next = self.next_handle.borrow_mut();
665        let handle = *next;
666        *next += 1;
667        handle
668    }
669}
670
671fn validate_request_against_envelope(
672    envelope: &CoordinatorDataPlanEnvelope,
673    request: &CoordinatorDataMaterializationRequest,
674) -> Result<()> {
675    if request.schema_fingerprint != envelope.schema_fingerprint {
676        return Err(DataError::FingerprintMismatch {
677            kind: "schema",
678            expected: envelope.schema_fingerprint.clone(),
679            actual: request.schema_fingerprint.clone(),
680        });
681    }
682    if request.plan_fingerprint != envelope.plan_fingerprint {
683        return Err(DataError::FingerprintMismatch {
684            kind: "plan",
685            expected: envelope.plan_fingerprint.clone(),
686            actual: request.plan_fingerprint.clone(),
687        });
688    }
689    if request.relation_fingerprint != envelope.relation_fingerprint {
690        let none = || "<none>".to_string();
691        return Err(DataError::FingerprintMismatch {
692            kind: "relation",
693            expected: envelope.relation_fingerprint.clone().unwrap_or_else(none),
694            actual: request.relation_fingerprint.clone().unwrap_or_else(none),
695        });
696    }
697    if request.require_relations && materialization_relations(envelope, request)?.is_none() {
698        return Err(DataError::Validation(format!(
699            "materialization request `{}` on `{}` requires coordinator relations",
700            request.input_name, request.node_id
701        )));
702    }
703    if request.output_representation != envelope.plan.output_representation {
704        return Err(DataError::Validation(format!(
705            "materialization request `{}` on `{}` output representation `{}` does not match plan output `{}`",
706            request.input_name,
707            request.node_id,
708            request.output_representation,
709            envelope.plan.output_representation
710        )));
711    }
712    if !request.source_ids.is_empty() {
713        let plan_sources = envelope
714            .plan
715            .steps
716            .iter()
717            .filter_map(|step| step.source_id.as_ref())
718            .collect::<BTreeSet<_>>();
719        for source_id in &request.source_ids {
720            if !plan_sources.contains(source_id) {
721                return Err(DataError::Validation(format!(
722                    "materialization request `{}` on `{}` source `{}` is not present in data plan `{}`",
723                    request.input_name, request.node_id, source_id, envelope.plan.id
724                )));
725            }
726        }
727    }
728    Ok(())
729}
730
731fn materialization_relations<'a>(
732    envelope: &'a CoordinatorDataPlanEnvelope,
733    request: &CoordinatorDataMaterializationRequest,
734) -> Result<Option<&'a CoordinatorRelationSet>> {
735    let prediction = request.phase.eq_ignore_ascii_case("predict")
736        || request.phase.eq_ignore_ascii_case("explain");
737    if let Some(cohort) = &request.predict_cohort {
738        cohort.validate()?;
739        if envelope.predict_cohort.as_ref() != Some(cohort) {
740            return Err(DataError::Validation(
741                "request predict_cohort does not match envelope authority".into(),
742            ));
743        }
744        let companion = request.phase.eq_ignore_ascii_case("fit_cv")
745            || request.phase.eq_ignore_ascii_case("refit");
746        if !(prediction || companion && cohort.role == crate::PredictCohortRole::ExternalTest) {
747            return Err(DataError::Validation(
748                "request phase cannot read this predict cohort".into(),
749            ));
750        }
751        return Ok(Some(
752            &envelope
753                .predict_cohort
754                .as_ref()
755                .expect("authority checked")
756                .relations,
757        ));
758    }
759    if prediction {
760        if let Some(cohort) = &envelope.predict_cohort {
761            return Ok(Some(&cohort.relations));
762        }
763    }
764    Ok(envelope.coordinator_relations.as_ref())
765}
766
767fn scoped_relations_for_materialization(
768    relations: &CoordinatorRelationSet,
769    request: &CoordinatorDataMaterializationRequest,
770) -> Result<CoordinatorRelationSet> {
771    relations.validate()?;
772    if request.source_ids.is_empty() {
773        return Ok(relations.clone());
774    }
775    let source_filter = request.source_ids.iter().collect::<BTreeSet<_>>();
776    let scoped = relations
777        .records
778        .iter()
779        .filter(|relation| {
780            relation
781                .source_id
782                .as_ref()
783                .map(|source_id| source_filter.contains(source_id))
784                .unwrap_or(false)
785        })
786        .cloned()
787        .collect::<Vec<_>>();
788    if scoped.is_empty() {
789        return Err(DataError::Validation(format!(
790            "materialization request `{}` on `{}` selected no coordinator relations for requested source ids",
791            request.input_name, request.node_id
792        )));
793    }
794    let scoped = CoordinatorRelationSet { records: scoped };
795    scoped.validate()?;
796    Ok(scoped)
797}
798
799fn validate_non_empty(label: &str, value: &str) -> Result<()> {
800    if value.trim().is_empty() {
801        return Err(DataError::Validation(format!("{label} must not be empty")));
802    }
803    Ok(())
804}
805
806fn validate_view(view: &DataView) -> Result<()> {
807    for (key, value) in [("partition", &view.partition), ("fold_id", &view.fold_id)] {
808        if let Some(value) = value {
809            validate_non_empty(key, value)?;
810        }
811    }
812    if let Some(samples) = &view.sample_ids {
813        let unique = samples.iter().collect::<BTreeSet<_>>();
814        if unique.len() != samples.len() {
815            return Err(DataError::Validation(
816                "data view contains duplicate sample ids".to_string(),
817            ));
818        }
819    }
820    if let Some(sources) = &view.source_ids {
821        let unique = sources.iter().collect::<BTreeSet<_>>();
822        if unique.len() != sources.len() {
823            return Err(DataError::Validation(
824                "data view contains duplicate source ids".to_string(),
825            ));
826        }
827    }
828    if let Some(columns) = &view.columns {
829        let unique = columns.iter().collect::<BTreeSet<_>>();
830        if unique.len() != columns.len() {
831            return Err(DataError::Validation(
832                "data view contains duplicate columns".to_string(),
833            ));
834        }
835        if columns.iter().any(|column| column.trim().is_empty()) {
836            return Err(DataError::Validation(
837                "data view contains an empty column".to_string(),
838            ));
839        }
840    }
841    if let Some(branch_view) = &view.branch_view {
842        branch_view.validate()?;
843    }
844    Ok(())
845}
846
847/// DAG owns fold membership. Explicit sample IDs are its authoritative
848/// selection and labels are provenance. Without IDs, named labels must match
849/// supplied relation metadata; only whole-handle full_train/predict views need
850/// no row predicate (PREDICT must already be bound to a predict handle).
851fn resolved_view_labels<'a>(
852    view: &'a DataView,
853    parent: &CoordinatorDataHandleRecord,
854) -> Result<Vec<(&'static str, &'a str)>> {
855    if view.sample_ids.is_some() {
856        return Ok(Vec::new());
857    }
858    let whole_handle = match view.partition.as_deref() {
859        Some("full_train") => true,
860        Some("predict") => {
861            if !parent.phase.eq_ignore_ascii_case("predict") {
862                return Err(DataError::Validation("partition=predict requires a PREDICT materialized handle or explicit sample_ids".into()));
863            }
864            true
865        }
866        _ => false,
867    };
868    let mut selectors = Vec::new();
869    if !whole_handle {
870        if let Some(partition) = &view.partition {
871            selectors.push(("partition", partition.as_str()));
872        }
873    }
874    if let Some(fold) = &view.fold_id {
875        if !whole_handle || parent.fold_id.as_ref() != Some(fold) {
876            selectors.push(("fold_id", fold.as_str()));
877        }
878    }
879    Ok(selectors)
880}
881
882fn filter_relations(
883    relations: &[CoordinatorRelation],
884    view: &DataView,
885) -> Result<Vec<CoordinatorRelation>> {
886    let sample_filter = view
887        .sample_ids
888        .as_ref()
889        .map(|sample_ids| sample_ids.iter().collect::<BTreeSet<_>>());
890    let source_filter = view
891        .source_ids
892        .as_ref()
893        .map(|source_ids| source_ids.iter().collect::<BTreeSet<_>>());
894    // When both `view.source_ids` and `view.branch_view` constrain sources, the
895    // two are composed as intersection: a relation must pass both filters.
896    // `source_ids` is the materialization scope (sources loaded into the parent
897    // handle); the branch selector is the subset this branch cares about.
898    // Intersection prevents a branch from silently widening past what was
899    // actually materialized.
900    let mut branch_source_filter: Option<BTreeSet<&SourceId>> = None;
901    let mut branch_metadata_filter: Option<&BTreeMap<String, serde_json::Value>> = None;
902    let mut branch_tag_filter: Option<BTreeSet<&String>> = None;
903    let mut branch_native_filter = None;
904    if let Some(branch_view) = view.branch_view.as_ref() {
905        match branch_view.mode {
906            crate::coordinator::CoordinatorBranchViewMode::BySource => {
907                branch_source_filter = Some(
908                    branch_view
909                        .selector
910                        .source_ids
911                        .iter()
912                        .collect::<BTreeSet<_>>(),
913                );
914            }
915            // A relation matches a `by_metadata` selector iff its own metadata
916            // contains every selector key with an equal value; `by_tag` iff its
917            // tags contain every selector tag.
918            crate::coordinator::CoordinatorBranchViewMode::ByMetadata => {
919                branch_metadata_filter = Some(&branch_view.selector.metadata);
920            }
921            crate::coordinator::CoordinatorBranchViewMode::ByTag => {
922                branch_tag_filter = Some(branch_view.selector.tags.iter().collect::<BTreeSet<_>>());
923            }
924            crate::coordinator::CoordinatorBranchViewMode::Separation => {}
925            crate::coordinator::CoordinatorBranchViewMode::ByFilter => {
926                let filter = branch_view.selector.filter.as_ref().ok_or_else(|| {
927                    DataError::Validation(format!(
928                        "coordinator branch view `{}` mode=by_filter requires filter",
929                        branch_view.view_id
930                    ))
931                })?;
932                branch_native_filter = Some(parse_native_branch_view_filter(
933                    filter,
934                    &format!("coordinator branch view `{}`", branch_view.view_id),
935                )?);
936            }
937        }
938    }
939    let mut filtered = relations
940        .iter()
941        .enumerate()
942        .filter(|relation| {
943            let relation = relation.1;
944            sample_filter
945                .as_ref()
946                .map(|samples| samples.contains(&relation.sample_id))
947                .unwrap_or(true)
948        })
949        .filter(|relation| {
950            let relation = relation.1;
951            source_filter
952                .as_ref()
953                .map(|sources| {
954                    relation
955                        .source_id
956                        .as_ref()
957                        .map(|source_id| sources.contains(source_id))
958                        .unwrap_or(false)
959                })
960                .unwrap_or(true)
961        })
962        .filter(|relation| {
963            let relation = relation.1;
964            branch_source_filter
965                .as_ref()
966                .map(|sources| {
967                    relation
968                        .source_id
969                        .as_ref()
970                        .map(|source_id| sources.contains(source_id))
971                        .unwrap_or(false)
972                })
973                .unwrap_or(true)
974        })
975        .filter(|relation| {
976            let relation = relation.1;
977            branch_metadata_filter
978                .map(|selector| {
979                    selector
980                        .iter()
981                        .all(|(key, value)| relation.metadata.get(key) == Some(value))
982                })
983                .unwrap_or(true)
984        })
985        .filter(|relation| {
986            let relation = relation.1;
987            branch_tag_filter
988                .as_ref()
989                .map(|tags| tags.iter().all(|tag| relation.tags.contains(tag)))
990                .unwrap_or(true)
991        })
992        .filter(|relation| {
993            let relation = relation.1;
994            branch_native_filter
995                .as_ref()
996                .map(|filter| {
997                    filter
998                        .metadata_equals
999                        .iter()
1000                        .all(|(key, value)| relation.metadata.get(key) == Some(value))
1001                        && filter
1002                            .tags_all
1003                            .iter()
1004                            .all(|tag| relation.tags.contains(tag))
1005                })
1006                .unwrap_or(true)
1007        })
1008        .filter(|relation| view.include_augmented || !relation.1.is_augmented)
1009        .filter(|relation| view.include_excluded || !relation.1.excluded)
1010        .map(|(idx, relation)| (idx, relation.clone()))
1011        .collect::<Vec<_>>();
1012    if filtered.is_empty() {
1013        return Err(DataError::Validation(
1014            "data view selected no coordinator relations".to_string(),
1015        ));
1016    }
1017    if let Some(sample_ids) = &view.sample_ids {
1018        let sample_order = sample_ids
1019            .iter()
1020            .enumerate()
1021            .map(|(idx, sample_id)| (sample_id, idx))
1022            .collect::<BTreeMap<_, _>>();
1023        filtered.sort_by_key(|(idx, relation)| {
1024            (
1025                sample_order
1026                    .get(&relation.sample_id)
1027                    .copied()
1028                    .unwrap_or(usize::MAX),
1029                *idx,
1030            )
1031        });
1032    }
1033    Ok(filtered.into_iter().map(|(_, relation)| relation).collect())
1034}
1035
1036fn unique_sample_count(relations: &[CoordinatorRelation]) -> usize {
1037    relations
1038        .iter()
1039        .map(|relation| &relation.sample_id)
1040        .collect::<BTreeSet<_>>()
1041        .len()
1042}
1043
1044fn selected_feature_indices(
1045    table: &CoordinatorFeatureTable,
1046    view: &DataView,
1047) -> Result<Vec<usize>> {
1048    let index_by_name = table
1049        .feature_names
1050        .iter()
1051        .enumerate()
1052        .map(|(idx, name)| (name, idx))
1053        .collect::<BTreeMap<_, _>>();
1054    let indices = if let Some(columns) = &view.columns {
1055        columns
1056            .iter()
1057            .map(|column| {
1058                index_by_name.get(column).copied().ok_or_else(|| {
1059                    DataError::Validation(format!(
1060                        "feature table `{}` has no feature column `{}`",
1061                        table.feature_set_id, column
1062                    ))
1063                })
1064            })
1065            .collect::<Result<Vec<_>>>()?
1066    } else {
1067        (0..table.feature_names.len()).collect()
1068    };
1069    if indices.is_empty() {
1070        return Err(DataError::Validation(format!(
1071            "feature table `{}` selected no feature columns",
1072            table.feature_set_id
1073        )));
1074    }
1075    Ok(indices)
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081    use serde_json::json;
1082
1083    fn envelope() -> CoordinatorDataPlanEnvelope {
1084        serde_json::from_str(include_str!(
1085            "../../../examples/fixtures/oof_campaign/coordinator_data_plan_envelope_nir.json"
1086        ))
1087        .unwrap()
1088    }
1089
1090    fn request() -> CoordinatorDataMaterializationRequest {
1091        serde_json::from_str(include_str!(
1092            "../../../examples/fixtures/oof_campaign/materialization_request_model_base_x.json"
1093        ))
1094        .unwrap()
1095    }
1096
1097    #[test]
1098    fn materializes_validated_coordinator_handle_record() {
1099        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1100        let record = arena.materialize(&envelope(), &request()).unwrap();
1101
1102        assert_eq!(record.handle.handle, 1);
1103        assert_eq!(record.handle.kind, CoordinatorHandleKind::Data);
1104        assert_eq!(record.input_name, "x");
1105        assert_eq!(record.plan_id, "nir-to-tabular");
1106        assert_eq!(record.sample_count, Some(2));
1107        assert_eq!(record.relation_record_count, Some(4));
1108        assert_eq!(arena.handle_record(1), Some(record));
1109        assert_eq!(arena.handle_records().len(), 1);
1110    }
1111
1112    #[test]
1113    fn view_labels_require_membership_but_preserve_host_resolved_views() {
1114        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1115        let record = arena.materialize(&envelope(), &request()).unwrap();
1116        for value in [
1117            json!({"partition":"does-not-exist"}),
1118            json!({"fold_id":"does-not-exist"}),
1119            json!({"partition":"fold_train"}),
1120            json!({"partition":"predict"}),
1121        ] {
1122            let view: DataView = serde_json::from_value(value).unwrap();
1123            assert!(arena.make_view(record.handle.handle, &view).is_err());
1124        }
1125        assert!(serde_json::from_value::<DataView>(json!({"sampl_ids":["S001"]})).is_err());
1126        let explicit: DataView = serde_json::from_value(
1127            json!({"sample_ids":["S001"],"partition":"fold_train","fold_id":"fold0"}),
1128        )
1129        .unwrap();
1130        assert_eq!(
1131            arena
1132                .make_view(record.handle.handle, &explicit)
1133                .unwrap()
1134                .sample_count,
1135            1
1136        );
1137        let all: DataView = serde_json::from_value(json!({"partition":"full_train"})).unwrap();
1138        assert_eq!(
1139            arena
1140                .make_view(record.handle.handle, &all)
1141                .unwrap()
1142                .sample_count,
1143            2
1144        );
1145        let mut predict_request = request();
1146        predict_request.phase = "PREDICT".into();
1147        let predict = arena.materialize(&envelope(), &predict_request).unwrap();
1148        let view: DataView = serde_json::from_value(json!({"partition":"predict"})).unwrap();
1149        assert_eq!(
1150            arena
1151                .make_view(predict.handle.handle, &view)
1152                .unwrap()
1153                .sample_count,
1154            2
1155        );
1156    }
1157
1158    #[test]
1159    fn view_resolves_partition_and_fold_from_supplied_relation_metadata() {
1160        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1161        let mut envelope = envelope();
1162        for relation in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1163            relation.metadata.insert(
1164                "partition".into(),
1165                json!(if relation.sample_id.as_str() == "S001" {
1166                    "train"
1167                } else {
1168                    "validation"
1169                }),
1170            );
1171            relation.metadata.insert("fold_id".into(), json!("fold0"));
1172        }
1173        let record = arena.materialize(&envelope, &request()).unwrap();
1174        let view: DataView =
1175            serde_json::from_value(json!({"partition":"validation","fold_id":"fold0"})).unwrap();
1176        let selected = arena.make_view(record.handle.handle, &view).unwrap();
1177        assert_eq!(selected.sample_count, 1);
1178        assert!(arena
1179            .view_identity(selected.handle.handle)
1180            .unwrap()
1181            .records
1182            .iter()
1183            .all(|row| row.sample_id.as_str() == "S002"));
1184    }
1185
1186    #[test]
1187    fn materialization_refuses_fingerprint_mismatch() {
1188        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1189        let mut request = request();
1190        request.plan_fingerprint = "0".repeat(64);
1191
1192        assert!(arena.materialize(&envelope(), &request).is_err());
1193    }
1194
1195    #[test]
1196    fn materialization_scopes_relations_to_requested_sources() {
1197        let mut envelope = envelope();
1198        let chem = SourceId::new("chem").unwrap();
1199        envelope.plan.steps.push(crate::plan::DataPlanStep {
1200            kind: crate::plan::DataPlanStepKind::Materialize,
1201            source_id: Some(chem.clone()),
1202            adapter_id: None,
1203            input_representation: None,
1204            output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
1205            fit_scope: crate::plan::FitScope::Stateless,
1206            requires_user_choice: false,
1207            metadata: BTreeMap::new(),
1208        });
1209        envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
1210        envelope.relation_fingerprint = None;
1211        envelope
1212            .coordinator_relations
1213            .as_mut()
1214            .unwrap()
1215            .records
1216            .push(CoordinatorRelation {
1217                unit_level: crate::CoordinatorEntityUnitLevel::Observation,
1218                unit_id: None,
1219                rep_id: None,
1220                derived_unit_id: None,
1221                component_observation_ids: Vec::new(),
1222                sample_influence_weight: None,
1223                quality_flag: None,
1224                observation_id: ObservationId::new("chem.S001").unwrap(),
1225                sample_id: SampleId::new("S001").unwrap(),
1226                target_id: Some(TargetId::new("y").unwrap()),
1227                group_id: None,
1228                origin_sample_id: None,
1229                source_id: Some(chem.clone()),
1230                is_augmented: false,
1231                excluded: false,
1232                metadata: BTreeMap::new(),
1233                tags: Vec::new(),
1234            });
1235        envelope.validate().unwrap();
1236
1237        let mut request = request();
1238        request.plan_fingerprint = envelope.plan_fingerprint.clone();
1239        request.relation_fingerprint = None;
1240        request.require_relations = false;
1241        request.source_ids = vec![SourceId::new("nir").unwrap()];
1242
1243        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1244        let data = arena.materialize(&envelope, &request).unwrap();
1245        let view = arena
1246            .make_view(data.handle.handle, &DataView::default())
1247            .unwrap();
1248        let identity = arena.view_identity(view.handle.handle).unwrap();
1249
1250        assert_eq!(data.relation_record_count, Some(4));
1251        assert_eq!(
1252            arena
1253                .data_identity(data.handle.handle)
1254                .unwrap()
1255                .records
1256                .len(),
1257            4
1258        );
1259        assert!(identity
1260            .records
1261            .iter()
1262            .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1263    }
1264
1265    #[test]
1266    fn view_filters_augmented_rows_and_preserves_repetition_identity() {
1267        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1268        let data = arena.materialize(&envelope(), &request()).unwrap();
1269        let view = DataView {
1270            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1271            include_augmented: false,
1272            ..Default::default()
1273        };
1274
1275        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1276        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1277
1278        assert_eq!(view_record.handle.kind, CoordinatorHandleKind::View);
1279        assert_eq!(view_record.sample_count, 1);
1280        assert_eq!(view_record.relation_record_count, 2);
1281        assert_eq!(identity.records.len(), 2);
1282        assert_eq!(identity.records[0].observation_id.as_str(), "obs.S001.base");
1283        assert_eq!(identity.records[1].observation_id.as_str(), "obs.S001.rep1");
1284        assert_eq!(
1285            arena.view_record(view_record.handle.handle),
1286            Some(view_record)
1287        );
1288    }
1289
1290    #[test]
1291    fn view_drops_excluded_rows_for_training_and_keeps_them_otherwise() {
1292        // Mark the sole S002 observation excluded. Under a training policy
1293        // (`include_excluded=false`) it must be filtered out; under a
1294        // validation/predict policy (`include_excluded=true`) it must be kept.
1295        let mut envelope = envelope();
1296        for record in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1297            if record.observation_id.as_str() == "obs.S002.base" {
1298                record.excluded = true;
1299            }
1300        }
1301        // `relation_fingerprint` is a replay key for the *source* relation
1302        // table, not for the derived coordinator_relations we just edited, so
1303        // it stays intact: the embedded coordinator relations are validated
1304        // structurally, and `excluded` does not participate in that fingerprint.
1305        envelope.validate().unwrap();
1306
1307        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1308        let data = arena.materialize(&envelope, &request()).unwrap();
1309
1310        // Training view: excluded S002 row is dropped.
1311        let train_view = DataView {
1312            include_augmented: true,
1313            include_excluded: false,
1314            ..Default::default()
1315        };
1316        let train_record = arena.make_view(data.handle.handle, &train_view).unwrap();
1317        let train_identity = arena.view_identity(train_record.handle.handle).unwrap();
1318        assert!(
1319            train_identity
1320                .records
1321                .iter()
1322                .all(|record| record.sample_id.as_str() != "S002"),
1323            "excluded sample S002 must be absent from a training view"
1324        );
1325        assert_eq!(train_record.relation_record_count, 3);
1326
1327        // Validation/predict view: excluded S002 row is retained.
1328        let predict_view = DataView {
1329            include_augmented: true,
1330            include_excluded: true,
1331            ..Default::default()
1332        };
1333        let predict_record = arena.make_view(data.handle.handle, &predict_view).unwrap();
1334        let predict_identity = arena.view_identity(predict_record.handle.handle).unwrap();
1335        assert!(
1336            predict_identity
1337                .records
1338                .iter()
1339                .any(|record| record.sample_id.as_str() == "S002"),
1340            "excluded sample S002 must be present in a validation/predict view"
1341        );
1342        assert_eq!(predict_record.relation_record_count, 4);
1343    }
1344
1345    #[test]
1346    fn branch_view_by_source_filters_relations_to_branch_sources() {
1347        use crate::coordinator::{
1348            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1349        };
1350
1351        let mut envelope = envelope();
1352        let chem = SourceId::new("chem").unwrap();
1353        envelope.plan.steps.push(crate::plan::DataPlanStep {
1354            kind: crate::plan::DataPlanStepKind::Materialize,
1355            source_id: Some(chem.clone()),
1356            adapter_id: None,
1357            input_representation: None,
1358            output_representation: Some(RepresentationId::new("tabular_numeric").unwrap()),
1359            fit_scope: crate::plan::FitScope::Stateless,
1360            requires_user_choice: false,
1361            metadata: BTreeMap::new(),
1362        });
1363        envelope.plan_fingerprint = crate::data_plan_fingerprint(&envelope.plan).unwrap();
1364        envelope.relation_fingerprint = None;
1365        envelope
1366            .coordinator_relations
1367            .as_mut()
1368            .unwrap()
1369            .records
1370            .push(CoordinatorRelation {
1371                unit_level: crate::CoordinatorEntityUnitLevel::Observation,
1372                unit_id: None,
1373                rep_id: None,
1374                derived_unit_id: None,
1375                component_observation_ids: Vec::new(),
1376                sample_influence_weight: None,
1377                quality_flag: None,
1378                observation_id: ObservationId::new("chem.S001").unwrap(),
1379                sample_id: SampleId::new("S001").unwrap(),
1380                target_id: Some(TargetId::new("y").unwrap()),
1381                group_id: None,
1382                origin_sample_id: None,
1383                source_id: Some(chem.clone()),
1384                is_augmented: false,
1385                excluded: false,
1386                metadata: BTreeMap::new(),
1387                tags: Vec::new(),
1388            });
1389        envelope.validate().unwrap();
1390        let mut request = request();
1391        request.plan_fingerprint = envelope.plan_fingerprint.clone();
1392        request.relation_fingerprint = None;
1393        request.require_relations = false;
1394        request.source_ids = vec![SourceId::new("nir").unwrap(), chem.clone()];
1395
1396        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1397        let data = arena.materialize(&envelope, &request).unwrap();
1398        let view = DataView {
1399            branch_view: Some(CoordinatorBranchView {
1400                view_id: "branch_view:nir".to_string(),
1401                branch_id: "branch:nir_only".to_string(),
1402                mode: CoordinatorBranchViewMode::BySource,
1403                selector: CoordinatorBranchViewSelector {
1404                    source_ids: vec![SourceId::new("nir").unwrap()],
1405                    ..Default::default()
1406                },
1407                allow_overlap: false,
1408                metadata: BTreeMap::new(),
1409            }),
1410            include_augmented: true,
1411            ..Default::default()
1412        };
1413        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1414        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1415
1416        assert!(identity
1417            .records
1418            .iter()
1419            .all(|record| record.source_id.as_ref() == Some(&SourceId::new("nir").unwrap())));
1420        assert!(identity.records.iter().any(|record| record
1421            .observation_id
1422            .as_str()
1423            .starts_with("obs.S001")
1424            || record.observation_id.as_str().starts_with("obs.S002")));
1425    }
1426
1427    #[test]
1428    fn branch_view_separation_does_not_restrict_relations() {
1429        use crate::coordinator::{
1430            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1431        };
1432
1433        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1434        let data = arena.materialize(&envelope(), &request()).unwrap();
1435        let view = DataView {
1436            branch_view: Some(CoordinatorBranchView {
1437                view_id: "branch_view:separation".to_string(),
1438                branch_id: "branch:0".to_string(),
1439                mode: CoordinatorBranchViewMode::Separation,
1440                selector: CoordinatorBranchViewSelector {
1441                    tags: vec!["clean".to_string()],
1442                    ..Default::default()
1443                },
1444                allow_overlap: false,
1445                metadata: BTreeMap::new(),
1446            }),
1447            include_augmented: true,
1448            ..Default::default()
1449        };
1450        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1451        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1452        assert!(!identity.records.is_empty());
1453    }
1454
1455    /// Build the shared envelope, tagging S001 observations with
1456    /// `metadata={"group":"A"}` + `tags=["clean"]` and the S002 observation
1457    /// with `metadata={"group":"B"}` + `tags=["dirty"]`, so `by_metadata` /
1458    /// `by_tag` branch views have something to discriminate on. Editing the
1459    /// derived `coordinator_relations` does not invalidate the source
1460    /// `relation_fingerprint` (it is a replay key for the source table, not the
1461    /// derived view).
1462    fn tagged_envelope() -> CoordinatorDataPlanEnvelope {
1463        let mut envelope = envelope();
1464        for record in &mut envelope.coordinator_relations.as_mut().unwrap().records {
1465            let (group, tag) = if record.sample_id.as_str() == "S002" {
1466                ("B", "dirty")
1467            } else {
1468                ("A", "clean")
1469            };
1470            record
1471                .metadata
1472                .insert("group".to_string(), serde_json::json!(group));
1473            record.tags = vec![tag.to_string()];
1474        }
1475        envelope.validate().unwrap();
1476        envelope
1477    }
1478
1479    #[test]
1480    fn branch_view_by_metadata_filters_relations_natively() {
1481        use crate::coordinator::{
1482            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1483        };
1484
1485        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1486        let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1487
1488        let by_metadata = |group: &str| CoordinatorBranchView {
1489            view_id: format!("branch_view:group_{group}"),
1490            branch_id: format!("branch:group_{group}"),
1491            mode: CoordinatorBranchViewMode::ByMetadata,
1492            selector: CoordinatorBranchViewSelector {
1493                metadata: BTreeMap::from([("group".to_string(), serde_json::json!(group))]),
1494                ..Default::default()
1495            },
1496            allow_overlap: false,
1497            metadata: BTreeMap::new(),
1498        };
1499
1500        // group=A INCLUDES the S001 relations and EXCLUDES S002.
1501        let view_a = DataView {
1502            branch_view: Some(by_metadata("A")),
1503            include_augmented: true,
1504            ..Default::default()
1505        };
1506        let record_a = arena.make_view(data.handle.handle, &view_a).unwrap();
1507        let identity_a = arena.view_identity(record_a.handle.handle).unwrap();
1508        assert!(
1509            identity_a
1510                .records
1511                .iter()
1512                .all(|record| record.sample_id.as_str() == "S001"),
1513            "by_metadata group=A must include only S001 relations"
1514        );
1515        assert!(
1516            identity_a
1517                .records
1518                .iter()
1519                .any(|record| record.sample_id.as_str() == "S001"),
1520            "by_metadata group=A must keep the matching S001 relations"
1521        );
1522
1523        // group=B INCLUDES S002 and EXCLUDES S001.
1524        let view_b = DataView {
1525            branch_view: Some(by_metadata("B")),
1526            include_augmented: true,
1527            ..Default::default()
1528        };
1529        let record_b = arena.make_view(data.handle.handle, &view_b).unwrap();
1530        let identity_b = arena.view_identity(record_b.handle.handle).unwrap();
1531        assert!(
1532            identity_b
1533                .records
1534                .iter()
1535                .all(|record| record.sample_id.as_str() == "S002"),
1536            "by_metadata group=B must exclude S001 relations"
1537        );
1538    }
1539
1540    #[test]
1541    fn branch_view_by_tag_filters_relations_natively() {
1542        use crate::coordinator::{
1543            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1544        };
1545
1546        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1547        let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1548
1549        let by_tag = |tag: &str| CoordinatorBranchView {
1550            view_id: format!("branch_view:tag_{tag}"),
1551            branch_id: format!("branch:tag_{tag}"),
1552            mode: CoordinatorBranchViewMode::ByTag,
1553            selector: CoordinatorBranchViewSelector {
1554                tags: vec![tag.to_string()],
1555                ..Default::default()
1556            },
1557            allow_overlap: false,
1558            metadata: BTreeMap::new(),
1559        };
1560
1561        // tag=clean INCLUDES S001 and EXCLUDES S002.
1562        let view_clean = DataView {
1563            branch_view: Some(by_tag("clean")),
1564            include_augmented: true,
1565            ..Default::default()
1566        };
1567        let record_clean = arena.make_view(data.handle.handle, &view_clean).unwrap();
1568        let identity_clean = arena.view_identity(record_clean.handle.handle).unwrap();
1569        assert!(
1570            identity_clean
1571                .records
1572                .iter()
1573                .all(|record| record.sample_id.as_str() == "S001"),
1574            "by_tag clean must include only S001 relations"
1575        );
1576
1577        // tag=dirty INCLUDES S002 and EXCLUDES S001.
1578        let view_dirty = DataView {
1579            branch_view: Some(by_tag("dirty")),
1580            include_augmented: true,
1581            ..Default::default()
1582        };
1583        let record_dirty = arena.make_view(data.handle.handle, &view_dirty).unwrap();
1584        let identity_dirty = arena.view_identity(record_dirty.handle.handle).unwrap();
1585        assert!(
1586            identity_dirty
1587                .records
1588                .iter()
1589                .all(|record| record.sample_id.as_str() == "S002"),
1590            "by_tag dirty must exclude S001 relations"
1591        );
1592    }
1593
1594    #[test]
1595    fn branch_view_by_filter_filters_closed_metadata_and_tag_predicates() {
1596        use crate::coordinator::{
1597            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1598        };
1599
1600        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1601        let data = arena.materialize(&tagged_envelope(), &request()).unwrap();
1602        let view = DataView {
1603            branch_view: Some(CoordinatorBranchView {
1604                view_id: "branch_view:filter_A_clean".to_string(),
1605                branch_id: "branch:filter_A_clean".to_string(),
1606                mode: CoordinatorBranchViewMode::ByFilter,
1607                selector: CoordinatorBranchViewSelector {
1608                    filter: Some(serde_json::json!({
1609                        "metadata_equals": {"group": "A"},
1610                        "tags_all": ["clean"]
1611                    })),
1612                    ..Default::default()
1613                },
1614                allow_overlap: false,
1615                metadata: BTreeMap::new(),
1616            }),
1617            include_augmented: true,
1618            ..Default::default()
1619        };
1620        let record = arena.make_view(data.handle.handle, &view).unwrap();
1621        let identity = arena.view_identity(record.handle.handle).unwrap();
1622        assert!(
1623            identity
1624                .records
1625                .iter()
1626                .all(|relation| relation.sample_id.as_str() == "S001"),
1627            "closed by_filter must retain exactly the matching relations"
1628        );
1629    }
1630
1631    #[test]
1632    fn branch_view_empty_partition_intersection_raises_a_clear_error() {
1633        // A branch_view scoped to group=B, intersected with a fold restriction
1634        // (sample_ids) that contains ONLY S001 samples, selects no relations —
1635        // the empty partition ∩ fold case. The arena must surface this explicitly
1636        // rather than returning an empty view (no silent mis-coverage).
1637        use crate::coordinator::{
1638            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1639        };
1640
1641        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1642        let envelope = tagged_envelope();
1643        let data = arena.materialize(&envelope, &request()).unwrap();
1644
1645        // The S001 sample id only (group=A); group=B (S002) is disjoint from this.
1646        // `DataView.sample_ids` rejects duplicates, so collect the distinct id.
1647        let s001_samples: Vec<SampleId> = envelope
1648            .coordinator_relations
1649            .as_ref()
1650            .unwrap()
1651            .records
1652            .iter()
1653            .filter(|record| record.sample_id.as_str() == "S001")
1654            .map(|record| record.sample_id.clone())
1655            .collect::<BTreeSet<_>>()
1656            .into_iter()
1657            .collect();
1658        assert!(!s001_samples.is_empty(), "fixture must carry S001 samples");
1659
1660        let view = DataView {
1661            branch_view: Some(CoordinatorBranchView {
1662                view_id: "branch_view:group_B".to_string(),
1663                branch_id: "branch:group_B".to_string(),
1664                mode: CoordinatorBranchViewMode::ByMetadata,
1665                selector: CoordinatorBranchViewSelector {
1666                    metadata: BTreeMap::from([("group".to_string(), serde_json::json!("B"))]),
1667                    ..Default::default()
1668                },
1669                allow_overlap: false,
1670                metadata: BTreeMap::new(),
1671            }),
1672            sample_ids: Some(s001_samples),
1673            include_augmented: true,
1674            ..Default::default()
1675        };
1676        let error = arena
1677            .make_view(data.handle.handle, &view)
1678            .unwrap_err()
1679            .to_string();
1680        assert!(
1681            error.contains("selected no coordinator relations"),
1682            "empty branch ∩ fold must raise a clear error: {error}"
1683        );
1684    }
1685
1686    #[test]
1687    fn branch_view_by_filter_refuses_unknown_predicates_before_selection() {
1688        use crate::coordinator::{
1689            CoordinatorBranchView, CoordinatorBranchViewMode, CoordinatorBranchViewSelector,
1690        };
1691
1692        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1693        let data = arena.materialize(&envelope(), &request()).unwrap();
1694        let view = DataView {
1695            branch_view: Some(CoordinatorBranchView {
1696                view_id: "branch_view:by_filter".to_string(),
1697                branch_id: "branch:0".to_string(),
1698                mode: CoordinatorBranchViewMode::ByFilter,
1699                selector: CoordinatorBranchViewSelector {
1700                    filter: Some(serde_json::json!({"op": "always"})),
1701                    ..Default::default()
1702                },
1703                allow_overlap: false,
1704                metadata: BTreeMap::new(),
1705            }),
1706            include_augmented: true,
1707            ..Default::default()
1708        };
1709        let error = arena
1710            .make_view(data.handle.handle, &view)
1711            .expect_err("unknown by_filter predicates must fail closed");
1712        let message = format!("{error}");
1713        assert!(
1714            message.contains("native predicate"),
1715            "expected native-predicate validation error, got: {message}"
1716        );
1717    }
1718
1719    #[test]
1720    fn target_values_are_sample_level_and_dedup_repetitions() {
1721        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1722        let data = arena.materialize(&envelope(), &request()).unwrap();
1723        let view = DataView {
1724            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1725            include_augmented: false,
1726            ..Default::default()
1727        };
1728        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1729        let target_table = CoordinatorTargetTable {
1730            target_id: TargetId::new("y").unwrap(),
1731            values: vec![
1732                CoordinatorTargetValue {
1733                    sample_id: SampleId::new("S001").unwrap(),
1734                    value: json!(42.0),
1735                },
1736                CoordinatorTargetValue {
1737                    sample_id: SampleId::new("S002").unwrap(),
1738                    value: json!(7.0),
1739                },
1740            ],
1741        };
1742
1743        let target = arena
1744            .target_values(view_record.handle.handle, &target_table)
1745            .unwrap();
1746
1747        assert_eq!(target.target_id.as_str(), "y");
1748        assert_eq!(target.sample_ids, vec![SampleId::new("S001").unwrap()]);
1749        assert_eq!(target.values, vec![json!(42.0)]);
1750    }
1751
1752    #[test]
1753    fn multi_target_values_align_samples_and_emit_validity_masks() {
1754        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1755        let data = arena.materialize(&envelope(), &request()).unwrap();
1756        let view = DataView {
1757            sample_ids: Some(vec![
1758                SampleId::new("S002").unwrap(),
1759                SampleId::new("S001").unwrap(),
1760            ]),
1761            include_augmented: false,
1762            ..Default::default()
1763        };
1764        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1765        let y = CoordinatorTargetTable {
1766            target_id: TargetId::new("y").unwrap(),
1767            values: vec![
1768                CoordinatorTargetValue {
1769                    sample_id: SampleId::new("S001").unwrap(),
1770                    value: json!(42.0),
1771                },
1772                CoordinatorTargetValue {
1773                    sample_id: SampleId::new("S002").unwrap(),
1774                    value: json!(7.0),
1775                },
1776            ],
1777        };
1778        let protein = CoordinatorTargetTable {
1779            target_id: TargetId::new("protein").unwrap(),
1780            values: vec![CoordinatorTargetValue {
1781                sample_id: SampleId::new("S001").unwrap(),
1782                value: json!(12.5),
1783            }],
1784        };
1785
1786        let block = arena
1787            .multi_target_values(view_record.handle.handle, &[y, protein])
1788            .unwrap();
1789
1790        assert_eq!(
1791            block.target_ids,
1792            vec![
1793                TargetId::new("y").unwrap(),
1794                TargetId::new("protein").unwrap()
1795            ]
1796        );
1797        assert_eq!(
1798            block.sample_ids,
1799            vec![
1800                SampleId::new("S002").unwrap(),
1801                SampleId::new("S001").unwrap()
1802            ]
1803        );
1804        assert_eq!(block.values[0], vec![json!(7.0), json!(42.0)]);
1805        assert_eq!(block.validity_masks[0], vec![true, true]);
1806        assert_eq!(block.values[1], vec![json!(null), json!(12.5)]);
1807        assert_eq!(block.validity_masks[1], vec![false, true]);
1808    }
1809
1810    #[test]
1811    fn feature_values_are_observation_level_and_filter_columns() {
1812        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1813        let data = arena.materialize(&envelope(), &request()).unwrap();
1814        let view = DataView {
1815            sample_ids: Some(vec![SampleId::new("S001").unwrap()]),
1816            columns: Some(vec!["f1".to_string()]),
1817            include_augmented: false,
1818            ..Default::default()
1819        };
1820        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1821        let feature_table = CoordinatorFeatureTable {
1822            feature_set_id: "x".to_string(),
1823            representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1824            feature_names: vec!["f0".to_string(), "f1".to_string()],
1825            rows: vec![
1826                CoordinatorFeatureRow {
1827                    observation_id: ObservationId::new("obs.S001.base").unwrap(),
1828                    values: vec![json!(1.0), json!(10.0)],
1829                },
1830                CoordinatorFeatureRow {
1831                    observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1832                    values: vec![json!(2.0), json!(20.0)],
1833                },
1834                CoordinatorFeatureRow {
1835                    observation_id: ObservationId::new("obs.S001.aug0").unwrap(),
1836                    values: vec![json!(3.0), json!(30.0)],
1837                },
1838                CoordinatorFeatureRow {
1839                    observation_id: ObservationId::new("obs.S002.base").unwrap(),
1840                    values: vec![json!(4.0), json!(40.0)],
1841                },
1842            ],
1843        };
1844
1845        let features = arena
1846            .feature_values(view_record.handle.handle, &feature_table)
1847            .unwrap();
1848
1849        assert_eq!(features.feature_set_id, "x");
1850        assert_eq!(features.feature_names, vec!["f1".to_string()]);
1851        assert_eq!(features.representation_id.as_str(), "tabular_numeric");
1852        assert_eq!(
1853            features.observation_ids,
1854            vec![
1855                ObservationId::new("obs.S001.base").unwrap(),
1856                ObservationId::new("obs.S001.rep1").unwrap(),
1857            ]
1858        );
1859        assert_eq!(
1860            features.sample_ids,
1861            vec![
1862                SampleId::new("S001").unwrap(),
1863                SampleId::new("S001").unwrap()
1864            ]
1865        );
1866        assert_eq!(features.values, vec![vec![json!(10.0)], vec![json!(20.0)]]);
1867
1868        let mut wrong_representation = feature_table;
1869        wrong_representation.representation_id = RepresentationId::new("dense_signal").unwrap();
1870        assert!(arena
1871            .feature_values(view_record.handle.handle, &wrong_representation)
1872            .is_err());
1873    }
1874
1875    #[test]
1876    fn view_honors_requested_sample_order_for_identity_targets_and_features() {
1877        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1878        let data = arena.materialize(&envelope(), &request()).unwrap();
1879        let view = DataView {
1880            sample_ids: Some(vec![
1881                SampleId::new("S002").unwrap(),
1882                SampleId::new("S001").unwrap(),
1883            ]),
1884            include_augmented: false,
1885            ..Default::default()
1886        };
1887        let view_record = arena.make_view(data.handle.handle, &view).unwrap();
1888
1889        let identity = arena.view_identity(view_record.handle.handle).unwrap();
1890        assert_eq!(
1891            identity
1892                .records
1893                .iter()
1894                .map(|relation| relation.observation_id.as_str())
1895                .collect::<Vec<_>>(),
1896            vec!["obs.S002.base", "obs.S001.base", "obs.S001.rep1"]
1897        );
1898
1899        let target_table = CoordinatorTargetTable {
1900            target_id: TargetId::new("y").unwrap(),
1901            values: vec![
1902                CoordinatorTargetValue {
1903                    sample_id: SampleId::new("S001").unwrap(),
1904                    value: json!(42.0),
1905                },
1906                CoordinatorTargetValue {
1907                    sample_id: SampleId::new("S002").unwrap(),
1908                    value: json!(7.0),
1909                },
1910            ],
1911        };
1912        let target = arena
1913            .target_values(view_record.handle.handle, &target_table)
1914            .unwrap();
1915        assert_eq!(
1916            target.sample_ids,
1917            vec![
1918                SampleId::new("S002").unwrap(),
1919                SampleId::new("S001").unwrap()
1920            ]
1921        );
1922        assert_eq!(target.values, vec![json!(7.0), json!(42.0)]);
1923
1924        let feature_table = CoordinatorFeatureTable {
1925            feature_set_id: "x".to_string(),
1926            representation_id: RepresentationId::new("tabular_numeric").unwrap(),
1927            feature_names: vec!["f0".to_string(), "f1".to_string()],
1928            rows: vec![
1929                CoordinatorFeatureRow {
1930                    observation_id: ObservationId::new("obs.S001.base").unwrap(),
1931                    values: vec![json!(1.0), json!(10.0)],
1932                },
1933                CoordinatorFeatureRow {
1934                    observation_id: ObservationId::new("obs.S001.rep1").unwrap(),
1935                    values: vec![json!(2.0), json!(20.0)],
1936                },
1937                CoordinatorFeatureRow {
1938                    observation_id: ObservationId::new("obs.S002.base").unwrap(),
1939                    values: vec![json!(4.0), json!(40.0)],
1940                },
1941            ],
1942        };
1943        let features = arena
1944            .feature_values(view_record.handle.handle, &feature_table)
1945            .unwrap();
1946        assert_eq!(
1947            features.observation_ids,
1948            vec![
1949                ObservationId::new("obs.S002.base").unwrap(),
1950                ObservationId::new("obs.S001.base").unwrap(),
1951                ObservationId::new("obs.S001.rep1").unwrap(),
1952            ]
1953        );
1954        assert_eq!(
1955            features.values,
1956            vec![
1957                vec![json!(4.0), json!(40.0)],
1958                vec![json!(1.0), json!(10.0)],
1959                vec![json!(2.0), json!(20.0)],
1960            ]
1961        );
1962    }
1963
1964    #[test]
1965    fn release_data_handle_releases_child_views() {
1966        let arena = CoordinatorHandleArena::new("controller:data.provider").unwrap();
1967        let data = arena.materialize(&envelope(), &request()).unwrap();
1968        let view_record = arena
1969            .make_view(data.handle.handle, &DataView::default())
1970            .unwrap();
1971
1972        assert!(arena.release_handle(data.handle.handle));
1973        assert_eq!(arena.handle_record(data.handle.handle), None);
1974        assert_eq!(arena.view_record(view_record.handle.handle), None);
1975        let error = arena.view_identity(view_record.handle.handle).unwrap_err();
1976        assert_eq!(error.category(), "runtime");
1977        assert_eq!(error.code(), "unknown_handle");
1978        assert_eq!(error.error_code(), 0x0001_0001);
1979        assert!(!arena.release_handle(data.handle.handle));
1980    }
1981}