# DAG-ML Coordinator Short Spec Status: working normative spec. This document is the short alignment contract for the product. It resolves the ambiguity left by the long design documents: DAG-ML is a Rust pipeline coordinator, not an ML algorithm library and not a data framework. ## Product Goal DAG-ML must virtually execute any data pipeline that can be expressed as: 1. a compiled DAG of data/model/control dependencies; 2. an experimental campaign plan: variants, splits, repetitions, refit, predict, explain; 3. external operators reachable through language bindings/controllers; 4. typed data contracts and identity tables supplied by `dag-ml-data` or an equivalent data provider. The product promise is: - reproducible execution; - traceable execution; - leakage-safe OOF campaigns; - real compiled DAG semantics; - high-performance Rust orchestration and scheduling; - external operators, data buffers and fitted objects kept outside the core. ## Non-Negotiable Principles 1. The Rust core owns logic, not domain algorithms. 2. Operators are external payloads: sklearn estimators, torch modules, C++ methods, bioinformatics tools, filters, splitters, augmenters. 3. Controllers live in bindings or native plugin layers. They adapt external operators to the Rust core contract. 4. The Rust core owns compilation, planning, validation, scheduling, RNG, fingerprints, lineage, cache keys, OOF joins, and campaign state. 5. The Rust core never inspects feature buffers, images, spectra, tensors, sequences, graphs, or fitted model internals. 6. The Rust core may inspect only identity, fold membership, prediction tables, target/scoring tables, descriptors, fingerprints and controller manifests. 7. Every join, split, merge and prediction use is keyed by stable identities, not by row position. 8. Unsafe leakage paths are refused by default and must be explicit, traceable and searchable when allowed. ## Ownership Boundary | Area | Rust core owns | Binding/controller owns | Data layer owns | |---|---|---|---| | DSL | compiled IR target, not syntax sugar | frontend syntax helpers | none | | DAG | graph topology, ports, branches, merges | external node behavior | source contracts only | | Campaign | variants, split plan, phases, refit/replay | splitter/model execution | sample relation facts | | Controllers | manifest validation, invocation protocol | actual adapter code | none | | Data | views by identity, DataPlan refs | host handles | schema, sources, adapters, collation | | Execution | scheduler, tasks, phase gates, reducers | fit/predict/transform/split calls | materialization | | Reproducibility | RNG contexts, fingerprints, canonical plans | local RNG consumption from seed | schema/data fingerprints | | Traceability | lineage records, cache keys, artifact refs | artifact serialization hooks | fitted data-adapter refs | | OOF safety | fold validation, prediction store, OOF join | emits predictions with identity | group/origin/repetition facts | ## Vocabulary ### Operator External business payload. Examples: sklearn estimator, torch module, C++ PLS, BLAST wrapper, aligner, filter predicate, augmenter, splitter. An operator is never owned by the Rust core. Frontend syntax should therefore prefer minimal opaque references when the operator is unambiguous. A bare `SNV` is compiled as a transform node whose operator payload remains `"SNV"`; a binding-side registry may instantiate the class and route it to a generic transformer controller. Verbose descriptors (`class`, `function`, explicit params, controller hints) are only required when the payload needs parameters or when registry routing would otherwise be ambiguous. If controller manifests are present while compiling a DSL graph or execution plan, their `operator_selectors` may also classify a minimal alias before the graph ports are frozen. This lets a custom bare alias stay compact even when Rust cannot infer its category by name; if multiple operator kinds claim it, the compiler rejects the alias and asks for explicit syntax. ### Controller Binding-side or native adapter that knows how to call an operator. A Python controller may call sklearn. A native controller may call C++. An R controller may call mlr3. A bioinformatics controller may call a local library or process. Controllers are registered through a manifest and invoked through ABI/binding contracts. ### Rust Core The coordinator. It compiles, plans, validates, schedules and records execution. It treats controllers as callable endpoints with declared capabilities. ### Data Provider The owner of source storage and heavy buffers. For this workspace, `dag-ml-data` is the reference contract. Other data providers can exist if they satisfy the same identity/data-plan boundary. ## Controller Contract Each controller must expose a manifest equivalent to: | Field | Purpose | |---|---| | `controller_id` | stable unique id | | `controller_version` | semantic version used in fingerprints | | `operator_kind` | model, transform, splitter, augmenter, filter, metric, generator, tuner | | `supported_phases` | compile, plan, fit_cv, select, refit, predict, explain | | `input_ports` / `output_ports` | typed port contracts | | `data_requirements` | `ModelInputSpec`, aux inputs, required sources or target | | `capabilities` | deterministic, thread_safe, process_safe, needs_python_gil, emits_predictions, consumes_oof_predictions, emits_artifacts, stateful, emits_relation, accepts_task_batch, accepts_static_subgraph | | `operator_selectors` | optional alias/class/function/ref/type selectors used by the registry to route opaque operator payloads before falling back to generic kind routing | | `batch_patterns` | optional static patterns for controller-side multitask execution, including allowed node kinds, axes, limits and compatibility constraints | | `fit_scope` | stateless, fold_train, full_train, inference_only | | `rng_policy` | uses core seed, ignores seed, externally deterministic, nondeterministic | | `artifact_policy` | serializable, host_only, content_addressed, replay_required | Minimum callable operations: | Operation | Used for | |---|---| | `describe` | manifest and static contracts | | `plan` | controller-specific plan details, no heavy compute | | `invoke` | phase-specific execution over handles and identity views | | `invoke_batch` | optional execution of a validated group of logical tasks or a closed static subgraph | | `release` | lifecycle cleanup for host handles | Specialized operations such as `fit`, `predict`, `transform`, `split`, `augment`, `score` may exist behind `invoke`, but the Rust core should schedule typed tasks rather than know library-specific APIs. `invoke_batch` is never a license for controller-owned topology. It is a transport and scheduling optimisation for logical tasks that the Rust core has already planned. A controller may use it to launch many preprocessings on one GPU call or to execute a closed, static subgraph, but it must return one logical response per member task so cache, OOF validation, lineage and replay remain coordinator-owned. Controller inputs: - opaque data handles; - selected identity view; - phase; - fold id, branch path, variant id, trial id; - deterministic `SeedContext`; - data-plan/fingerprint refs; - controller params. Data-plan references are `DataBinding` contracts owned by the coordinator: - node id and input name; - `dag-ml-data` request id; - schema fingerprint; - data-plan fingerprint; - optional relation fingerprint; - output representation; - feature set id used for `feature_arrow` requests, defaulting to input name when omitted; - feature fusion selectors may be passed through the same `feature_arrow` bytes-view when a data provider supports `dag-ml-data` multi-source fusion; - source ids; - view policy for fold train, fold validation, refit and predict. The actual data plan and relation table remain external. `dag-ml-data` can emit a coordinator envelope containing these fingerprints plus coordinator relation records; `dag-ml` validates that an execution campaign binds to the exact envelope before a controller receives any handle. The shared serialization contract is versioned as `CoordinatorDataPlanEnvelope` v1. `dag-ml` consumes the subset represented by `ExternalDataPlanEnvelope`, rejects unsupported future versions at runtime, and publishes the current JSON Schema at `docs/contracts/coordinator_data_plan_envelope.schema.json`. The schema is the wire-contract artifact; Rust validation remains responsible for semantic checks such as fingerprint equality and relation membership in the active campaign fold set. At execution time, the scheduler does not give the controller the raw materialized data handle directly. It asks the data provider for a scoped view derived from the active phase and `DataViewPolicy`: `FIT_CV` tasks receive a fold-train view for fitting and a separate fold-validation view for OOF prediction, refit/full-train views carry the full training sample ids, and replay predict views are marked as predict partitions. The handles visible in `TaskRequest` are scoped data-view handles, and the same request carries a `data_views` map keyed like `input_handles` so bindings can inspect the selected partition without guessing from the handle. The parent handle remains traceability state owned by the provider. Unsafe data views are rejected unless declared explicitly: `FIT_CV` fitting cannot use full-train or validation partitions, validation/predict views cannot include augmented rows, and excluded rows cannot be included unless the corresponding `DataViewPolicy.unsafe_flags` entry is present. Controller outputs: - opaque data/model/artifact handles; - prediction blocks; - sample relation deltas; - metrics; - structured errors; - artifact refs or serialization bytes. ## Public Method Shape The data/model piloting contract must be visible in public Rust and binding APIs. It must not be hidden inside controller-specific side effects. Minimum public coordinator flow: | Method | Inputs | Output | |---|---|---| | `compile` | frontend pipeline IR, frontend registry | `GraphSpec`, `CampaignSpec` | | `plan` | graph, campaign, controller manifests, dataset schema, data planner | immutable `ExecutionPlan` | | `fit_cv` | execution plan, data provider handle, controller registry, stores | `CVResult` with prediction and lineage refs | | `select` | CV result, ranking policy | `SelectedGraph` / selected variant refs | | `refit` | selected graph, execution plan, data provider handle, stores | `RefitResult` / bundle inputs | | `export_bundle` | selected graph, refit result, artifacts, fingerprints | `ExecutionBundle` | | `predict` | bundle, new data provider handle | prediction blocks | | `explain` | bundle, new data provider handle, target node/method | explanation payload refs | Minimum controller-facing request/response shapes: | Type | Required fields | |---|---| | `ControllerPlanRequest` | node id, operator params, phase set, data requirements, input/output ports, data schema fingerprint | | `SplitRequest` | identity table, sample relation table, split policy, seed context | | `TaskRequest` | phase, node id, fold id, branch path, variant id plus generated choices/fingerprint, data view, data-plan refs, input handles, prediction input metadata, artifact input metadata, seed context | | `TaskResponse` | output handles, prediction blocks, sample relation deltas, metrics, artifacts, lineage payload | | `TaskBatchRequest` | batch id, batch pattern id, canonical member task ids, ordered `TaskRequest` members, parent seed, resource limits | | `TaskBatchResponse` | batch id, one `TaskResponse` per member task id, optional parent batch metrics/lineage | Every shape-changing operation must declare the affected domain: | Domain | Examples | Contract | |---|---|---| | row domain | sample filtering, sample augmentation, repetitions, group splits | changes identity/sample relation or view membership | | feature domain | preprocessing, feature augmentation, feature selection, source fusion | changes representation/schema/feature names | | target domain | y-transform, target aggregation, multi-target mapping | changes target space and inverse-transform requirements | | prediction domain | model predict, OOF join, aggregation of repetitions | changes prediction block shape and aggregation level | ## DAG, Campaign Plan And Splits This is the key correction. There are two related but distinct plans: ### GraphPlan The compiled DAG of dependency semantics: - transforms; - y-transforms; - models; - feature joins; - prediction joins; - source joins; - fork/map/branch; - aggregators; - filters; - augmentation nodes; - generator nodes; - tuners; - explain nodes. Graph nodes describe dependency and phase behavior. External behavior is executed by controllers. ### CampaignPlan The experimental execution plan around the graph: - root seed; - variants/search space; - split strategy; - repeated campaigns; - nested CV policy; - selection/ranking policy; - refit policy; - predict/explain replay policy; - scheduler/resource policy. ### Generation Ownership Generation has two different meanings and the ownership must stay explicit: - compile-time generation of variants/search spaces belongs to `dag-ml`, because it changes campaign fingerprints, variant ids, seeds, lineage and selection; - runtime generation of data, features, models or synthetic samples is an external operator capability. The payload lives in controllers or `dag-ml-data`, while `dag-ml` validates seeds, origin/sample relations, shape deltas, fold boundaries and lineage. The current core represents compile-time generation through `GenerationSpec` and `VariantPlan`. Choices may carry typed node-parameter overrides; the scheduler lowers those into the effective `NodePlan.params` and `params_fingerprint` sent to controllers for that variant. When a graph declares `search_space_fingerprint`, plan compilation verifies it against the canonical campaign `GenerationSpec` fingerprint before variants are enumerated. Runtime generators are graph nodes/controllers with explicit capabilities such as `generates_data`, `generates_model` or `expands_variants`; they are not allowed to mutate identity or training boundaries without emitting relation and shape deltas. ### Splitters Splitters are not ordinary data-transform operators. A splitter is a controller capability invoked by the coordinator during campaign planning or early execution. It produces a `FoldSet`. The Rust core validates the `FoldSet` against identity, group, target, repetition and origin constraints. Native identity splitters such as KFold and GroupKFold may be shipped by the Rust core for deterministic cross-language behavior. Feature-dependent splitters such as KS/SPXY/sklearn splitters are external controller calls. Implementation consequence: - `NodeKind::Split` must not mean "data transform node". - If a split appears in frontend syntax, the compiler must lower it into `CampaignPlan.split_invocation`, not a normal feature-flow node. - A legacy or compatibility graph node may exist only as a control node that emits no feature data and is excluded from model/data transforms. ## Identity And Leakage Units The coordinator must distinguish physical rows from logical samples. This is critical for products with repeated measurements: several `X` observations can share one `Y`. | Identity | Meaning | Owned by | |---|---|---| | `ObservationId` | physical row/acquisition in one source | data provider | | `SampleId` | logical sample requested by the user | data provider, validated by core | | `TargetId` | target/label unit; may cover one or more samples | data provider, validated by core | | `GroupId` | leakage unit such as plant, product, patient, plot, batch | data provider, validated by core | | `OriginId` | original sample/observation from which an augmented row was derived | data provider/controller, validated by core | | `RepetitionId` | repeated acquisition id inside a sample | data provider | Split policies must declare the leakage unit: | `split_unit` | Rule | |---|---| | `observation` | only the same observation is atomic; unsafe for repeated X / one Y unless explicitly allowed | | `sample` | all observations of one sample stay on the same fold side | | `target` | all samples sharing one target stay on the same fold side | | `group` | all samples/observations sharing one group stay on the same fold side | Defaults: - repeated measurements default to `split_unit="sample"`; - group ids, when present and requested, dominate sample ids for leakage; - augmentation origin constraints are always checked unless explicitly disabled by a traceable unsafe policy; - fold membership is stored at sample/target/group level and broadcast to observations during materialization. The core refuses a split when: - a requested split unit is absent from the sample relation table; - one leakage unit appears in both train and validation of a fold; - an augmented row derived from a validation origin appears in train; - repeated observations of the same sample are split across train/validation under `split_unit="sample"`; - a feature-dependent splitter returns folds not expressible in stable ids. ### Heterogeneous Multi-Source Units For pipelines where one physical sample carries several observations per source with asymmetric cardinalities, unit domains are named by an `EntityUnitLevel`: `physical_sample` (the `SampleId`-level leakage/target/output unit), `source_sample` (one source's observations of a physical sample), `observation` (one acquisition, `ObservationId`) and `combo` (a derived observation built from one observation per source for the same physical sample). In the mainline a `combo` is a relation-backed derived observation/unit carrying `component_observation_ids` and `origin_sample_id`; it is not a public `PredictionLevel`. The public prediction levels stay `observation`/`sample`/`target`/`group`, and cartesian/combo work reduces to sample-level output by identity, never by row position. Promoting `combo` or `source_sample` to a first-class public prediction level is a deferred, explicitly-gated decision. The frozen vocabulary (`EntityUnitLevel`, `PredictionUnitId`, `ReductionPlan`, `RepresentationPlan`, `FitInfluencePolicy`), the mainline decision and the ADR-02 migration checklist are in `docs/adr/ADR-19-multisource-unit-vocabulary.md`. ## Repetitions, Aggregation And Refit Repeated observations are first-class. The core must support models trained on individual observations while also evaluating and selecting on aggregated sample-level predictions. Example: several spectra (`ObservationId`) for the same product (`SampleId`) with one chemical value (`TargetId`). Required prediction levels: | Level | Meaning | |---|---| | `observation` | one prediction per physical acquisition | | `sample` | aggregation of observations for the same sample | | `target` | aggregation of samples for the same target unit | | `group` | aggregation at group level, only if explicitly requested | Required aggregation policies: | Policy field | Purpose | |---|---| | `aggregation_level` | observation, sample, target, group | | `method` | mean, weighted_mean, median, vote, custom_controller | | `weights` | none, quality, repetition_count, controller_emitted | | `emit_parallel_metrics` | whether raw and aggregated metrics are both computed | | `selection_metric_level` | which level drives variant/model selection | | `store_raw_predictions` | keep observation-level predictions for audit | | `store_aggregated_predictions` | keep aggregated predictions for ranking/replay | The Rust core implements deterministic observation-to-sample and sample-to-target/group aggregation for `mean`, `median`, `vote` and `weighted_mean`. Weighted means require an explicit weight policy; `controller_emitted` and `quality` weights are read from observation prediction blocks, while `repetition_count` weights sample-to-target/group aggregation by the number of observations attached to each sample. `custom_controller` remains an explicit controller responsibility. FIT_CV requirements: 1. Models may fit on observation-level rows if the data plan says so. 2. Validation predictions are first captured at the controller-emitted level. 3. The Rust core aggregates predictions by identity according to policy. 4. Metrics are computed in parallel when requested. The core provides identity-aligned regression scoring for validated sample/target/group prediction blocks (`mse`, `rmse`, `mae`, `r2`) and refuses positional-only or mismatched unit sets. Metric reports preserve the scored prediction origin: producer node, partition, optional fold and optional prediction id. 5. Selection must declare which metric level is authoritative. Selection policies can require a `metric_level`, and candidates whose metric metadata is missing or comes from another level are rejected before ranking. 6. OOF joins use the declared aggregation level and must not mix raw and aggregated predictions implicitly. 7. Replay-facing prediction contracts carry `prediction_level` explicitly. Sample-level replay caches carry `sample_ids`; target/group replay caches carry `unit_ids` typed as `PredictionUnitId`, validated in bundle records, payloads and file/in-memory stores. Aggregated caches are not preloaded into the sample OOF store. REFIT requirements: 1. The final fit boundary is the selected training universe, excluding held-out test samples. 2. Refit may train on all repeated observations of selected samples if the `DataModelShapePlan` declares observation-level fitting. 3. Refit prediction outputs must preserve the same aggregation policy used for selection unless the user explicitly chooses a different predict policy. 4. Final prediction blocks may store both raw repetition predictions and aggregated sample predictions. 5. A bundle must record whether the selected model was chosen by observation, sample, target or group metrics. 6. Prediction requirements, prediction-cache records, prediction-cache payloads and `NodeTask.prediction_inputs` must preserve the prediction level, so a target/group metric decision cannot be replayed through an implicit sample-level cache. ## Data/Model Shape Plan The coordinator must make shape control explicit because augmentations, selection, fusion and aggregation change what a controller sees. Each model or transform node receives a `DataModelShapePlan`: | Field | Meaning | |---|---| | `input_granularity` | observation, sample, target, group | | `target_granularity` | observation, sample, target, group | | `fit_rows` | train observations/samples allowed for fit | | `predict_rows` | validation/test/final rows expected for predict | | `feature_namespace` | source/branch/augmentation prefixes | | `feature_schema_fingerprint` | stable identity of column/feature layout | | `target_space` | raw, transformed, scaled, encoded | | `aggregation_policy` | how controller predictions are reduced | | `augmentation_policy` | sample/feature augmentation rules | | `selection_policy` | feature selection and supervised selection fit scope | Shape-changing controllers must return a shape delta: | Delta | Examples | Required validation | |---|---|---| | `row_delta` | sample filter, sample augmentation, separation branch | identity and fold boundaries remain valid | | `feature_delta` | feature augmentation, selection, source fusion | feature schema fingerprint changes deterministically | | `target_delta` | y scaling, target encoding | inverse-transform and target space are recorded | | `prediction_delta` | probability output, repetition aggregation | prediction columns and aggregation level are recorded | The core validates deltas before downstream tasks can consume them. Feature deltas are checked against any declared `feature_schema_fingerprint`; lineage may also echo the data/model shape and aggregation-policy fingerprints, and when present those fingerprints must match the compiled `NodePlan`. ## Augmentation, Selection, Filtering And Fusion These operations are leakage-sensitive because they can change rows, features or target spaces. ### Sample Augmentation Sample augmentation creates new observations or samples. Default policy: - FIT_CV: train partition only; - validation/test rows are never augmented for training metrics; - each augmented row carries `OriginId`; - an augmented row inherits target, group and fold boundary from its origin; - OOF and scoring are reported on original identities unless policy opts into augmented reporting. The default Rust contract rejects sample augmentation across all partitions, or sample augmentation without origin/target/group inheritance, unless the corresponding `AugmentationPolicy.unsafe_flags` entry is present. Forbidden by default: - validation-origin augmentation appearing in train; - augmented rows counted as independent samples for group/sample metrics; - augmented validation/test rows entering view requests without an explicit unsafe flag; - sample augmentation after a prediction join unless explicitly declared safe. ### Feature Augmentation Feature augmentation changes columns/features but not identity. Rules: - stateless feature augmentation may run on train/validation/test if declared deterministic and fitted nowhere; - stateful feature augmentation must fit on fold train and apply to validation; - supervised feature augmentation is treated like supervised feature selection and must use fold-train targets only; - feature namespaces and schema fingerprints must change deterministically; - source/branch/augmentation provenance is preserved for merge and explain. ### Feature Selection Feature selection is a transform that may be supervised or unsupervised. Rules: - supervised feature selection fits only inside the current train boundary; - selected feature masks are artifacts with fold/refit lineage; - supervised feature selection must store masks so CV/refit replay can audit which features were fitted inside each train boundary; - validation/test/final data only receive `apply`, never `fit`; - downstream feature joins must verify compatible selected schemas or use an explicit missing-feature policy; - selection masks used at REFIT are recorded separately from CV-fold masks. ### Sample Filtering / Exclusion Filtering changes row membership. Rules: - filters must declare whether they affect train only, predict only, all partitions, or branch-local views; - train-only exclusion cannot silently remove validation samples from scoring; - separation branches must produce disjoint or explicitly overlapping identity sets according to branch policy; - selector-driven separation branches are declared as campaign `branch_view_plans` over source ids, metadata, tags or provider-native filter expressions before host data providers materialize branch-local views; - all filters emit a `row_delta` with before/after identity fingerprints. ### Source Fusion And Merge Feature/source fusion changes feature shape and possibly missingness. Rules: - sample alignment remains data-layer work, but the core records and validates the chosen alignment plan fingerprint; - feature joins are namespace-stable and branch-aware; - prediction joins are OOF-checked by the core; - mixed joins must declare which inputs are raw features and which are predictions, with separate leakage checks. ## Phase Model The coordinator executes: ```text COMPILE -> PLAN -> FIT_CV -> SELECT -> REFIT -> PREDICT -> EXPLAIN ``` | Phase | Rust core responsibility | Controller responsibility | |---|---|---| | `COMPILE` | parse frontend IR into graph/campaign specs, freeze topology | provide descriptors if needed | | `PLAN` | resolve controllers, ports, data plans, variants, split invocations | describe/plan only | | `FIT_CV` | schedule fold/variant/branch tasks, enforce fold views, record predictions | fit/transform/predict/split/augment | | `SELECT` | rank variants, reject unsafe variants by policy | optional metric helpers | | `REFIT` | replay selected graph on full train boundary | fit final artifacts | | `PREDICT` | replay bundle on new data, validate schema/fingerprints | predict/transform only | | `EXPLAIN` | replay to explanation target | controller-specific explanation | ## ExecutionPlan An `ExecutionPlan` is the Rust-owned, immutable plan after compile and plan: - graph fingerprint; - campaign fingerprint; - data-plan fingerprints; - controller manifests and versions; - variant list or lazy enumerator; - split invocation specs and resulting fold fingerprints; - leakage unit policy; - data/model shape plans per node; - aggregation policies for prediction/evaluation/refit; - topological order and deterministic parallel node levels; - phase execution schedules expanded by variant and fold; - controller multitask group templates for eligible known task groups or closed static subgraphs; - phase gates per node; - expected input/output contracts; - cache key templates; - lineage templates; - scheduler policy. No controller may mutate the `ExecutionPlan`. ## Runtime Execution The Rust core runs task batches in deterministic dependency order. Parallelism dimensions: - variants; - folds; - branches; - independent DAG nodes; - controller multitask groups; - controller-declared internal parallelism. Scheduler rules: 1. Ready tasks may run in parallel only if dependencies and resource policy allow. 2. Reduction order is canonical: sort by variant id, fold id, branch path, node id. 3. Nested parallelism is controlled by controller capabilities and resource policy. 4. Python/GIL-bound controllers may be process-scheduled; native thread-safe controllers may be thread-scheduled. 5. Compatible ready tasks may be coalesced into a controller multitask batch only when a manifest-declared pattern admits them. Default compatibility is same phase, same fold, same data view and no hidden dependency between members. 6. Batches can be split by resource limits, and may fall back to scalar `TaskRequest`s unless the manifest explicitly marks the group as required. 7. A task result is accepted only after runtime validation. ## Runtime Validation Before dispatch, the Rust core checks: - required ports exist; - phase is allowed by controller manifest; - data plan fingerprint matches; - requested data views match the active phase, fold id and partition; - view sample ids are in the active fold/partition; - validation prediction sample ids are contained in the fold-validation view; - leakage unit membership is compatible with the active fold; - task data/model shape plan matches the phase and controller manifest; - seed is derived from the canonical path; - unsafe policy is explicit if required. For `TaskBatchRequest`, every member `TaskRequest` is checked first. The batch itself is then checked against the selected `batch_patterns` entry: allowed node kinds, coalescing axes, canonical member order, phase/fold/view/branch compatibility, `max_tasks` and resource hints. After dispatch, the Rust core checks: - output ports match declared contracts; - prediction blocks carry producer, fold, partition, sample ids and target names; - no prediction row is positional-only; - train/validation/test/final partitions are legal for the phase; - augmentation origins do not cross fold boundaries; - group/repetition/target leakage units remain on one side of a fold; - shape deltas are declared before downstream consumption; - aggregation level and prediction columns match the node policy; - artifact refs, portable backend/URI/content metadata and handle lifetimes are registered; - lineage was recorded. For `TaskBatchResponse`, every member response is validated as if it had been returned by a scalar invocation. Parent batch metrics and lineage are retained only as extra provenance; they cannot replace member-level records. ## OOF And Leakage Rules OOF safety is a Rust core invariant. Rules: 1. Training a downstream model on upstream predictions requires validation OOF predictions by default. 2. `partition="train"` predictions cannot feed downstream training unless `allow_train_predictions_as_features=true` and a second lineage flag records `leakage_acknowledged=true`. 3. Every producer must provide exactly one validation prediction per requested sample unless an explicit aggregation policy says otherwise. 4. Producers merged by a prediction join must share compatible fold structure. 5. Augmented observations inherit origin leakage boundaries. 6. Group, target and repetition leakage units are validated from sample relation facts supplied by the data layer. 7. OOF joins are by sample identity, never by row position. 8. Repetition aggregation is explicit; raw observation predictions and aggregated sample/target predictions are never silently mixed. 9. Refit uses the selected aggregation and shape policies unless the predict/refit policy explicitly overrides them and records the override. ### Stacking Refit Coverage Policy Prediction-stacking meta-nodes default to `stacking_oof_refit_contract.policy="require_full_coverage"`. Under that default, REFIT may train a meta-model only when each upstream OOF producer covers the complete refit sample universe from validation OOF blocks. Partial OOF coverage is rejected with the stable cause `partial_oof_without_policy`. A stacking node may explicitly opt out of meta-model REFIT by carrying `metadata.stacking_oof_refit_contract.policy="cv_only"`, or may run the CV path and skip REFIT only when coverage is incomplete with `"skip_refit_on_incomplete_oof"`. Both policies keep malformed OOF invalid: non-validation blocks, missing fold ids, unknown folds, fold/sample mismatches and duplicate validation samples are refused before any skip decision is accepted. ## Traceability Every accepted task emits or updates a `LineageRecord` containing: - run id; - graph/campaign/variant fingerprints; - node id; - phase; - fold id; - branch path; - controller id/version; - params fingerprint; - data-plan fingerprint; - data/model shape fingerprint; - aggregation level and policy fingerprint; - input lineage refs; - output handle/artifact refs; - seed; - unsafe flags; - metrics; - timing and resource hints. The lineage graph must be enough to answer: - why was this sample in this fold? - which controller produced this artifact? - which seed and params were used? - which data schema and data plan were used? - did any unsafe leakage path occur? - can this predict/explain run replay the training bundle? Bundle and prediction-cache payload schemas are versioned artifacts. The core publishes a `SchemaMigrationPolicy` for each artifact with current/min readable and writable versions plus explicit automatic migration edges. No implicit migration is allowed: old versions are accepted only if the policy declares a migration edge, future versions are refused, and version `0` is always invalid. ## Performance Requirements The Rust core must be designed as the high-performance coordination layer: - compact immutable specs; - canonical fingerprints without reparsing large payloads; - parallel scheduler over variants/folds/branches/nodes; - deterministic reducers; - no feature-buffer copies through the core; - opaque handle arenas with explicit release; - prediction store optimized for identity joins; - controller capability-aware scheduling; - zero-copy Arrow/DLPack boundaries where identity, predictions or tensors cross. The core must not chase performance by moving domain algorithms inside Rust. The performance target is orchestration, validation, scheduling, lineage, and OOF joins at scale. ## Confrontation With Current nirs4all Pipeline Current nirs4all is the prototype. It proves the product need, but not the final architecture. | nirs4all today | DAG-ML target | |---|---| | Python-only runtime | Rust core with Python/R/JS/native bindings | | Conceptual DAG reconstructed from sequential steps | Real compiled immutable DAG | | Controllers do work and also carry some orchestration side effects | Controllers only execute declared external behavior | | Split step mutates pipeline/dataset state | Splitter controller invocation produces `FoldSet` in `CampaignPlan` | | Branching stored in mutable contexts/snapshots | `Fork/Map` graph semantics with explicit branch path | | Merge reconstructs features/predictions in Python controllers | Rust-owned feature/prediction join contracts and OOF validation | | Repetition averages such as `avg` / `w_avg` are controller/runtime conventions | Aggregation policy is explicit, fingerprinted and evaluated in parallel with raw predictions | | Several spectra can share one product target through dataset conventions | `ObservationId`, `SampleId`, `TargetId`, `GroupId`, `OriginId` are first-class leakage identities | | Feature/sample augmentation and selection are controller-specific effects | Shape deltas, fit scopes and augmentation/selection policies are validated by the core | | NIRS-shaped data assumptions leak through dataset/model logic | Generic data contracts through `dag-ml-data` | | Dynamic routing through Python registry | Binding registries export controller manifests to Rust | | Trace, artifacts and prediction store are pipeline-specific | Rust-owned lineage/cache/artifact/prediction contracts | | Reproducibility depends on Python conventions and local discipline | Core-derived seeds, fingerprints and deterministic reducers | | Leakage safety exists but is embedded in controllers | Leakage safety is a core invariant checked around every task | The migration path is not "port nirs4all as-is". It is: 1. extract the implicit DAG and campaign semantics; 2. encode them as `GraphPlan` and `CampaignPlan`; 3. move orchestration, validation and scheduling to Rust; 4. keep operators external through controllers; 5. keep data generic through data contracts; 6. replay nirs4all use cases as conformance tests, not as product constraints. ## What Must Be Implemented Next The next implementation layer must make the coordinator visible: 1. `ControllerManifest` 2. `ControllerRegistry` 3. `CampaignSpec` 4. `GraphPlan` 5. `ExecutionPlan` 6. `NodePlan` 7. `NodeTask` 8. `NodeResult` 9. `RunContext` 10. sequential scheduler 11. in-memory prediction store 12. in-memory lineage recorder 13. mock controller conformance tests 14. split invocation model that produces and validates `FoldSet` 15. `LeakageUnitPolicy` 16. `AggregationPolicy` 17. `DataModelShapePlan` 18. `ShapeDelta` 19. augmentation, feature-selection and filtering policies The existing OOF/fold/data-plan code remains useful, but it is not the product shape by itself. The product shape starts when a compiled `ExecutionPlan` drives controllers through tasks and validates their outputs. ## Acceptance Checks For This Spec An implementation is aligned only if all answers are "yes": 1. Can the core compile a frozen graph and campaign plan without executing external code? 2. Can splitters be external controllers while the core owns the validated `FoldSet`? 3. Can model/process/augmentation/filter operators remain external payloads? 4. Can the scheduler parallelize variants, folds and branches without changing results? 5. Can the core reject OOF leakage without inspecting feature buffers? 6. Can a run be replayed from fingerprints, controller versions, data-plan refs, artifacts and lineage? 7. Can the same core coordinate Python, native C++, R, JS or bioinformatics controllers with the same safety rules? 8. Can repeated observations with one target be trained, scored, aggregated, selected and refit without leakage or hidden metric-level changes? 9. Can feature/sample augmentation, feature selection, filtering and fusion declare shape deltas that the core validates before downstream use? If any answer is "no", the implementation has drifted from the product goal.