# ADR-04: Tag / exclude mask materialization **Status**: accepted (2026-05-28) **Blocks**: workstream E (bridge data-provider), workstream G (lineage taxonomy). ## Context nirs4all's `{"tag": filter}` adds a column without removing samples. `{"exclude": filter}` removes samples from training. `{"exclude": [filter_a, filter_b], "mode": "any" | "all"}` produces a union or intersection mask. The bridge must reproduce **and audit** the resulting masks so leakage refusal still applies. ## Decision The bridge **pre-computes masks** in the adapter layer (i.e. inside `nirs4all/bridge/dag_ml_provider.py`), then ships the materialized mask + an audit record to `dag-ml-data`. The materialization itself is offline; the audit is part of the lineage envelope. ### Mask materialization contract 1. The adapter walks every `tag` / `exclude` step at compile time, calls the corresponding filter operator's `fit_predict` on the training partition, and records the resulting boolean mask in the `SampleRelationTable` as a sample-level metadata column (`__tag___` / `__exclude____`). 2. Multi-filter `exclude` masks are materialized via Python set ops (`mask_any = mask_a | mask_b`, `mask_all = mask_a & mask_b`). The combined mask carries the canonical key `__exclude_{mode}_{sha256(sorted(filter_ids))[:12]}__`. 3. `tag` masks are **additive** — they create new metadata columns but never alter the train/test partition. `exclude` masks set `partition = "excluded_from_training"` for the matching rows; the rows remain in the dataset for prediction. ### Lineage audit fields (ADR-11 / ADR-12) Every materialized mask is recorded in the lineage envelope with: - **expression** — the original Python expression (operator class name + constructor args). - **mode** — `"single"` / `"any"` / `"all"` for `exclude`. - **source_columns** — which columns the filter consumed (X, y, metadata key). - **sample_id_set** — the list of `SampleId` values the mask flagged (full list, not just count, so audits are reproducible). - **mask_fingerprint** — SHA-256 of the canonical mask blob; lets the bridge's bundle replay assert masks are identical. ## Consequences - The bridge's data provider materializes masks before the DAG runs; the runtime never re-computes them. - `dag-ml-data`'s `SampleRelationTable` learns an `MaskAuditRecord` reference per materialized mask. - The lineage taxonomy (ADR-11) carries `mask_*` fields that survive RO-Crate / PROV / OpenLineage export. - "Tagged but not excluded" remains expressible — downstream `branch.by_tag` consumes the tag column without partitioning fixed rows. ## Open follow-ups - The parity oracle's `exclude_multi_any_y_and_x` case skips today because `sample_data/regression` is too small for a 2-filter UNION exclusion to leave a viable train set. Pick `regression_2` once available — fixture-size, not contract.