Safe Pipeline Refactoring and Shared Outputs¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive DVC"]
section["Truthful Pipelines Declared Dependencies"]
page["Safe Pipeline Refactoring and Shared Outputs"]
proof["Before-and-after contract proof"]
family --> program --> section --> page
page -.produces.-> proof
flowchart LR
inventory["inventory reads, controls, writes"] --> preserve["state preserved claims"]
preserve --> reshape["change graph shape"]
reshape --> challenge["challenge new boundaries"]
challenge --> converge["prove current and converged"]
Refactoring a pipeline changes more than its diagram. Moving an output, splitting a stage, merging commands, or narrowing a dependency can change:
- which mutation triggers work;
- which stage owns an artifact;
- how invalidation propagates;
- which evidence a reviewer can inspect;
- whether an interrupted run leaves a coherent result.
A safe refactor makes its preserved and changed contracts explicit before editing
dvc.yaml.
Inventory the behavioral contract¶
For every affected stage, write an influence ledger:
| Contract surface | Questions |
|---|---|
| command | which program and arguments define behavior? |
| file reads | which content can change the result? |
| controls | which selected parameter keys affect behavior? |
| outputs | which durable artifacts must the stage restore? |
| consumers | which later stages read each output? |
| semantic assertion | what result value proves current meaning? |
Inspect application code, not just the existing declaration. A refactor that faithfully
copies an already incomplete deps list preserves the defect.
Give every durable artifact one owner¶
A shared intermediate is produced once and consumed by several stages:
flowchart LR
prepare["prepare"] --> features["features.parquet"]
features --> fit["fit"]
features --> inspect["inspect"]
fit --> model["model.json"]
model --> evaluate["evaluate"]
features --> evaluate
The producer declares features.parquet in outs; every consumer declares it in
deps. That creates one producer edge and several consumer edges.
stages:
prepare:
cmd: python -m incident_escalation.prepare
deps:
- data/raw/incidents.csv
- src/incident_escalation/prepare.py
outs:
- data/prepared/features.parquet
fit:
cmd: python -m incident_escalation.fit
deps:
- data/prepared/features.parquet
- src/incident_escalation/fit.py
outs:
- models/escalation-model.json
inspect:
cmd: python -m incident_escalation.inspect
deps:
- data/prepared/features.parquet
- src/incident_escalation/inspect.py
outs:
- reports/inspection.json
Do not let two stages write the same durable path. Shared consumption is normal; shared ownership makes restoration and provenance ambiguous.
Decide whether outputs belong together¶
A stage may own several outputs when they form one atomic scientific claim.
For example, an evaluation command may produce:
evaluation.json;error-slices.csv;calibration.svg.
Keep them together when they share inputs and controls, are refreshed as one evaluation, and failure should invalidate the set. Split them when one is optional debugging output, has different inputs, changes on a different cadence, or has an independent consumer and review meaning.
Use a consequence test:
| Question | Keep together when… | Split when… |
|---|---|---|
| invalidation | every influence applies to every output | influences differ |
| failure | partial result is not useful | one result can succeed independently |
| review | artifacts support one claim | artifacts support distinct claims |
| reuse | consumers need the set | consumers need independent products |
The goal is not maximum stage count. It is honest ownership.
Split an overloaded stage¶
Suppose one stage fits and evaluates:
stages:
train_and_evaluate:
cmd: python -m incident_escalation.train_and_evaluate
deps:
- data/prepared/features.parquet
params:
- fit.model_family
- evaluate.threshold
outs:
- models/escalation-model.json
- reports/evaluation.json
This boundary makes an evaluation threshold change rerun model fitting. More importantly, it hides the model as a meaningful handoff.
Split it only after defining the new intermediate:
stages:
fit:
cmd: python -m incident_escalation.fit
deps:
- data/prepared/features.parquet
params:
- fit.model_family
outs:
- models/escalation-model.json
evaluate:
cmd: python -m incident_escalation.evaluate
deps:
- data/prepared/features.parquet
- models/escalation-model.json
params:
- evaluate.threshold
outs:
- reports/evaluation.json
Now test three mutations:
fit.model_familyrerunsfit, thenevaluatethrough the model.evaluate.thresholdreruns onlyevaluate.- prepared features rerun both consumers.
Those are contract assertions, not merely performance expectations.
Merge only meaningless boundaries¶
Two adjacent stages may be better as one when their intermediate:
- has no independent interpretation;
- is never reviewed or reused;
- exists only as scratch;
- cannot be restored meaningfully on its own;
- makes failure recovery harder rather than clearer.
Before merging, list every downstream consumer. If another stage reads the intermediate, removing it changes a public pipeline boundary. Either preserve a meaningful replacement or update and test every consumer.
Move or rename an output safely¶
An output path participates in producer ownership and every consumer dependency. Treat a move as a graph migration:
sequenceDiagram
participant Reviewer
participant Producer
participant Artifact
participant Consumers
Reviewer->>Producer: record old ownership and assertions
Producer->>Artifact: declare new output path
Reviewer->>Consumers: replace every dependency edge
Reviewer->>Producer: reproduce from missing new output
Producer->>Artifact: restore new path
Artifact->>Consumers: trigger downstream work
Check that:
- the old path is no longer declared;
- no command still reads or writes it;
- the new path is owned by exactly one stage;
- all consumers declare the new path;
- deleting the new artifact marks its owner stale;
- ordinary reproduction rebuilds it and its consumers.
A successful run from an already populated workspace is too weak. Test restoration from the missing new output.
Narrow dependencies with a mutation matrix¶
Replacing data/ with one file may remove false reruns, but it can also hide a lookup
table or schema read. Record both positive and negative cases:
| Mutation | Expected invalidation |
|---|---|
| prepared features change | stage stale |
| declared source module changes | stage stale |
| selected control changes | stage stale |
| unrelated notebook changes | stage unchanged |
| unused raw archive changes | stage unchanged |
Positive cases defend completeness. Negative cases defend precision. A refactor needs both.
Preserve a before-and-after proof¶
Save:
graph-before.txt
declaration-before.yaml
contract-ledger.md
declaration-after.yaml
graph-after.txt
mutation-matrix.tsv
reproduction-receipts/
review-decision.md
The decision should distinguish:
- preserved claims;
- deliberately changed claims;
- removed artifacts;
- new ownership;
- acceptance evidence.
Do not claim lock-file similarity proves behavioral equivalence. A legitimate split or path move changes lock structure. Prove the intended causal behavior instead.
Review checkpoint¶
You can refactor safely when you can:
- inventory actual reads, controls, writes, and consumers;
- assign every durable artifact one producer;
- justify multi-output cohesion by consequence;
- test split-stage propagation with targeted mutations;
- merge only boundaries without independent meaning;
- migrate every edge when moving an output;
- demonstrate positive and negative invalidation cases;
- prove restoration and convergence after the new graph is in place.
The durable question is:
Does the new graph preserve every intended scientific claim while making ownership and invalidation easier to inspect?