# DSL Parity With nirs4all Pipelines Status: working contract. Goal: the dag-ml DSL must be at least as expressive as the current nirs4all pipeline surface, while keeping dag-ml's architecture: operators stay external, controllers live in bindings/hosts, and Rust compiles, validates, schedules and audits the DAG, campaign, OOF and leakage contracts. This document tracks semantic parity first. The strict `PipelineDslSpec` is the portable canonical form. A JSON compatibility importer now lowers serialized nirs4all-style list/dict pipelines into that canonical form; Python object and YAML frontends should be thin host-side serializers around the same importer. ## Non-Negotiable Design Rules - Splits are campaign/controller invocations (`split_invocation`), not graph operators. - Any data/model shape mutation must be visible in public DSL fields: `shape`, `aggregation_policy`, `augmentation_policy`, `selection_policy`, `train_params` and `tuning`. - OOF safety is graph-visible through prediction edges with `requires_oof` and fold alignment; refit/replay consumes validated caches/bundles. - Branch, merge, concat, source fusion and stacking are DAG structure, not Python call-order side effects. - Runtime behavior belongs to controllers. The DSL records enough intent and contracts for Rust to reject unsafe execution and for bindings to route tasks. - Minimal aliases are the preferred frontend surface when an operator is unambiguous. `SNV` should compile as an opaque transform payload; the Python or native controller registry resolves it through manifest `operator_selectors` to the matching controller. Verbose forms are reserved for parameters, explicit ids, controller hints or ambiguous routing. - Bindings should therefore expose registry aliases as first-class UX, not as a compatibility afterthought. A bare alias such as `SNV` or `StandardScaler` remains the representation in the graph; a `TransformerMixin` controller may build the concrete object and execute it after Rust has selected that controller by alias/class/function/ref/type selectors. - When a controller registry is available during DSL compilation (`compile-pipeline-dsl --controllers` or `build-pipeline-dsl-plan`), aliases that are not obvious by Rust heuristics may still be reclassified by matching `operator_selectors` before ports are frozen. For example, `ElasticSpectra` can stay a bare string while a model controller declares that alias; if two operator kinds claim the same alias, compilation requires explicit syntax. ## Parity Matrix | nirs4all surface | dag-ml DSL representation | Contract status | |---|---|---| | plain preprocessing/transform step | `kind: "transform"` | compiled to `NodeKind::Transform` | | model step, named model, explicit params | `kind: "model"`, `operator`, `params`, `metadata` | compiled to `NodeKind::Model` | | tuner / finetune controller | `kind: "tuner"` or alias `kind: "finetune"`, `operator`, `params`, `tuning` | compiled to external `NodeKind::Tuner`; Rust treats it as an OOF-producing predictive node while the tuning engine and concrete estimator lifecycle remain controller-owned | | target/y processing | `kind: "y_transform"` | compiled to `NodeKind::YTransform` with target ports | | splitters (`KFold`, `GroupKFold`, SPXY, fold files) | top-level `split_invocation` in campaign template | deliberately outside graph nodes | | sequential grouping (`[...]`) | `kind: "sequential"` | inlined during compilation while preserving child node contracts | | sample augmentation | `kind: "sample_augmentation"` plus mandatory `shape.augmentation_policy` | compiled as augmentation with `dsl_augmentation_kind=sample`; unsafe scopes refused | | feature augmentation | `kind: "feature_augmentation"` plus `shape` | compiled as augmentation with `dsl_augmentation_kind=feature` | | runtime data generation / synthetic samples | `kind: "data_generation"` or alias `kind: "generation"` plus mandatory `shape` | compiled as `NodeKind::Generator` with `dsl_generation_kind=data`; actual generation remains external, while Rust validates seed, fold scope, origin/group/target inheritance and shape contracts | | feature selection / shape-changing processing | `shape.selection_policy`, `feature_namespace`, schema fingerprints | shape plan validated and attached to campaign | | sample filters | `kind: "sample_filter"` or `kind: "filter"` | compiled to exclusion/filter nodes with explicit `dsl_filter_kind` metadata | | concat transform / multi-view feature fusion | `kind: "concat_transform"` with branch transforms | compiled to `NodeKind::FeatureJoin` | | duplicated branches | `kind: "branch"`, `mode: "duplication"` | multiple branch predictions retained | | separation branches by source/metadata/tag/filter | `branch.mode`, `branch.selector`, per-branch selectors | compiled to graph intent plus campaign `branch_view_plans`; provider materialization remains host-side | | multiple models per branch | multiple `kind: "model"` steps inside a branch | compiled into distinct OOF inputs for downstream merge | | merge predictions/features/original data | `kind: "merge"`, `merge_mode`, `output_as`, `include_original_data`, `selectors` | compiled to join nodes with OOF prediction edges and branch data edges; `features`/`sources` consume transformed branch outputs, `all`/`mixed` can consume branch data plus OOF predictions plus original data | | merge plus immediate meta-model | `kind: "merge_model"` convenience | compiled as model consuming OOF prediction inputs and optional original data | | stacking, multi-model top-level stacks | repeated `model` steps then `merge`/`merge_model` | pending predictions are preserved until consumed | | per-branch/per-model selection (`best`, `top_k`, `all`) | `merge.selectors` with branch/model/input scopes | selector targets and `top_k`/metric requirements are compile-validated; scoring remains controller policy | | finetune / hyperparameter search metadata | `tuning` or `finetune_params`, plus generation dimensions/variants | tuning intent is graph-visible on model/tuner nodes and generation dimensions; concrete search remains controller-side | | final train params | `train_params` | preserved as `dsl_train_params` metadata | | `_range_`, `_log_range_`, `_grid_`, param `_or_`, `pick`, `arrange`, `count` | `variants`, explicit `generation_dimensions`, or compact `generators` on DSL nodes | compiled into deterministic `GenerationSpec` dimensions | | structural `_or_` over step chains | `kind: "generator"`, `mode: "or"`, `branches`, `pick`/`arrange`/`count` | expanded into explicit OOF-producing choices with namespaced node ids and generator metadata | | structural `_cartesian_` over pipeline stages | `kind: "generator"`, `mode: "cartesian"`, `stages` | expanded into explicit Cartesian OOF-producing choices with namespaced node ids and fold-safe downstream merge inputs | | serialized list/dict nirs4all surface | top-level `pipeline` array with `preprocessing`, `model`, `branch`, `merge`, `_or_`, `_cartesian_`, `_chain_`, `_grid_`, `_range_`, `_log_range_`, `_zip_`, `_sample_` | compatibility importer lowers to canonical DSL; data-only branch feature merges and merge dicts are compiled, and data-only generator stages are fused with downstream model generators so OOF choices stay complete | | minimal aliases / plain operator refs | short strings plus `{"class": ...}`, `{"function": ...}`, `{"ref": ...}`, `{"type": ...}` and `{"name": ..., "step": ...}` wrappers | Rust infers only safe planning class: splitters become campaign split invocations, obvious estimators become model nodes, obvious tuners such as `OptunaTuner` become tuner nodes, chart aliases become chart nodes. With a controller registry, selector-only aliases can reclassify unknown transform defaults to model/tuner/etc. before graph planning; cross-kind matches are rejected as ambiguous | | multiple nirs4all splitter declarations | one campaign `split_invocation` with `params.compat_split_chain` | splitters remain outside graph nodes while preserving train/test + CV chains for host split controllers | | multisource data | `data_bindings.source_ids`, branch/source selectors, source joins | contract surface present; richer materialization belongs to dag-ml-data | | repetition/sample/group aggregation | top-level/shape `aggregation_policy`, target/group OOF cache contracts | core runtime implemented for sample/target/group OOF | | tag/exclude filters | `kind: "tag"` and `kind: "exclude"` | compiled to graph nodes | | charts/reports | `kind: "chart"` | compiled as a chart node; host controller decides side effects | ## Current Gaps - Native JSON compatibility import exists for serialized nirs4all-style list/dict syntax, including minimal aliases and plain `class`/`function` descriptors. Direct Python object/YAML parsing is still a binding-layer task: hosts must serialize live objects and splitters into portable descriptors before handing the DSL to Rust. - The DSL node kinds compile and validate graph contracts. Smoke controllers now execute transform/model/tuner/data-generation/mixed-join paths, but production execution still needs host controller support for each operator family. - Separation branch materialization by source/metadata/tag/filter now has an explicit campaign `branch_view_plans` contract. Production runtime-complete support still requires dag-ml-data providers and host controllers to consume those selectors when creating branch-local views. - Merge selector scopes and basic selection contracts are compile-validated, and OOF edges are enforced; actual metric scoring and ranking remain the responsibility of selection and merge controllers. - Runtime generation is represented as external generator nodes/controllers for data-producing generators. The next open boundary is richer generated-model and provider-native generation backends; actual generator implementations should stay outside Rust unless they are pure DAG/campaign coordination primitives. ## Required Regression Coverage - Compile every nirs4all canonical sample category into strict DSL equivalents: linear, feature augmentation, sample augmentation, branch, stacking/merge, concat transform, filters/splits, finetune/tuner, multisource and all-features. - For each shape-changing step, assert that `DataModelShapePlan` exists and rejects unsafe augmentation/selection scopes. - For every stacking/merge pattern, assert that upstream prediction edges carry `requires_oof=true` and fold alignment. - For repeated/grouped data, assert that target/group OOF cache requirements survive bundle capture and replay.