Worked Investigation: Proving Hidden Runtime Drift¶
Page Maps¶
graph LR
family["Reproducible Research"]
program["Deep Dive DVC"]
section["Execution Environments Reproducible Inputs"]
page["Worked Investigation: Proving Hidden Runtime Drift"]
capstone["Runtime contract audit"]
family --> program --> section --> page
page -.executes.-> capstone
flowchart LR
reproduce["reproduce paired cases"] --> predict["predict planner behavior"]
predict --> inspect["inspect preserved receipts"]
inspect --> prove["prove hidden cause"]
prove --> repair["choose contract repair"]
repair --> review["write bounded conclusion"]
This investigation begins with a result that looks reassuring: after runtime policy changes, ordinary DVC reproduction says the pipeline is up to date and the output remains stable.
The investigation shows why that stability is defective. A process variable changes the meaning of the result, but the variable is absent from the stage declaration. DVC correctly evaluates its graph and reuses an output produced under old runtime meaning.
You will follow the actual receipts produced by the capstone audit. No instructor-only setup is required.
The claim under review¶
The intended claim is:
Each result renders the total according to the current
REPORT_STYLEpolicy.
The renderer adds 3 and 5. With REPORT_STYLE=dot, it writes:
With REPORT_STYLE=comma, it should write:
The punctuation is deliberately semantic in this specimen. It makes stale runtime meaning visible without relying on accidental platform variation.
Produce an isolated evidence packet¶
From the repository root, run:
Set a shell variable for shorter commands:
Confirm the audit itself passed:
Expected finding names:
BASELINE_CONVERGES
DECLARED_RUNTIME_CHANGE_IS_STALE
DECLARED_RUNTIME_REBUILD_ADOPTS_POLICY
HIDDEN_RUNTIME_CHANGE_IS_INVISIBLE
ORDINARY_REPRO_PRESERVES_STALE_RUNTIME_MEANING
FORCED_RUN_CHANGES_OUTPUT_WITH_STABLE_DECLARATION
FINGERPRINT_EXPOSES_RUNTIME_MISMATCH
Three rows have decision REJECT even though their test result is PASS. The audit
successfully reproduced behaviors that a production contract must reject.
Understand the paired design¶
Open the settled baseline declaration:
Both stages share:
data/source.txt;scripts/render_runtime.py;- the same Python process;
- the same output schema.
Their runtime policy enters differently:
| Stage | Policy source | Is policy identity declared? |
|---|---|---|
declared_runtime |
environment/runtime.env |
yes, in deps |
hidden_runtime |
process variable REPORT_STYLE |
no |
flowchart TD
data["source: 3 and 5"] --> declared["declared_runtime"]
data --> hidden["hidden_runtime"]
renderer["same renderer"] --> declared
renderer --> hidden
contract["runtime.env"] --> declared
process["REPORT_STYLE"] -. "used by process, absent from graph" .-> hidden
declared --> declaredResult["declared result"]
hidden --> hiddenResult["hidden result"]
This design isolates visibility to the planner. It does not claim that all real local-versus-CI incidents are this tidy.
Make predictions before following receipts¶
Copy and complete this table in your notes:
| Change | Expected status | Expected ordinary repro | Expected result |
|---|---|---|---|
| contract file: dot to comma | |||
| process variable: dot to comma |
Use only the declaration to predict. DVC can compare declared file identity. It cannot compare an environment-variable value that is neither in command text nor represented by a dependency or parameter.
The expected predictions are:
| Change | Expected status | Expected ordinary repro | Expected result |
|---|---|---|---|
| contract file: dot to comma | declared stage stale | stage runs | 8,00 |
| process variable: dot to comma | empty | stage skips | retained 8.00 |
Now use evidence to test them.
Establish the converged baseline¶
Inspect the baseline finding:
It establishes:
- both stages initially produced
dotsemantics; - a second repro skipped both stages;
- status was empty after settling.
This baseline matters. Without it, later status might reflect an unrelated incomplete run instead of the deliberate policy change.
The supported baseline claim is limited:
Under the initial dot context, both declared stages settled and produced matching results.
It does not yet say how either stage responds to runtime change.
Follow the declared change¶
The audit changes environment/runtime.env from:
to:
Inspect status:
The receipt names:
Before execution, the result still contains old dot semantics. That is normal stale state: the declared dependency changed and the stage has not yet rebuilt.
Inspect the repro receipt and current result:
cat "$audit/evidence/declared-change-repro.stdout.txt"
cat "$audit/workspace/declared-change/results/declared.txt"
The stage runs and the result adopts comma semantics. The complete route is:
sequenceDiagram
participant Policy as runtime.env
participant DVC as DVC planner
participant Stage as declared_runtime
participant Result as declared.txt
Policy->>Policy: change dot to comma
DVC->>Policy: compare dependency identity
DVC->>Stage: mark stale and execute
Stage->>Result: write style=comma, rendered=8,00
Each link has evidence: changed dependency, stale receipt, execution receipt, current result.
Follow the hidden change without intervening¶
The paired workspace changes process state to REPORT_STYLE=comma but leaves dvc.yaml
unchanged.
Compare fingerprints:
Python, implementation, platform, and machine match. report_style differs.
Now inspect pre-repro status:
It is:
This does not contradict the fingerprint. The files answer different questions:
- fingerprint: did recorded runtime context differ?
- DVC status: did declared graph state differ?
Inspect ordinary repro:
cat "$audit/evidence/hidden-change-ordinary-repro.stdout.txt"
cat "$audit/workspace/hidden-change/results/hidden.txt"
DVC skips the stage, and the result remains style=dot and rendered=8.00.
At this point, a strong finding is:
The current runtime receipt says comma, while the retained output says dot. Because ordinary repro skipped, the output was not produced under the current context.
Do not call this conditional variation. Only one current-context execution exists so far: none.
Use force to test the suspected cause¶
The audit executes:
Read the preserved receipt:
Then inspect the report's before-and-after evidence:
jq '.findings[] |
select(.finding == "FORCED_RUN_CHANGES_OUTPUT_WITH_STABLE_DECLARATION")' \
"$audit/report.json"
The result changes from 8.00 to 8,00. This supports the causal inference:
Holding the specimen's other recorded facts constant, changing report style changes the output when the stage executes.
The report also shows:
dvc.yamlhas one stable hash before and after force;dvc.lockchanges after execution;- output identity changes after execution.
That proves force did not add a declaration edge.
Reconstruct the causal argument¶
Write the argument as linked evidence, not a list of clues:
- Baseline control: both stages converge under dot policy.
- Isolated contrast: paired fingerprints differ only on
report_style. - Visibility contrast: declared file change appears in status; hidden process change does not.
- Planner consequence: declared ordinary repro runs; hidden ordinary repro skips.
- Semantic consequence: declared output adopts comma; hidden output retains dot.
- Causal probe: forced hidden execution under comma changes output to comma.
- Contract check: declaration stays unchanged, so the missing influence remains missing.
The conclusion is stronger than “environment drift happened”:
REPORT_STYLEis an influential hidden input. Its absence fromdvc.yamlallows ordinary repro to preserve a result with obsolete runtime meaning.
Evaluate repair proposals¶
| Proposal | Future change visible? | Result reviewable? | Verdict |
|---|---|---|---|
always use --force |
no | only per forced run | reject as operating contract |
| print variable in logs | no | context observable but not enforced | insufficient |
| widen result tolerance | no | stale output still unexecuted | category error |
add runtime contract file to deps |
yes | dependency and result evidence join | suitable |
| make report style a DVC parameter | yes | control value appears in stage state | suitable |
| enforce approved value in CI and join receipt | externally | yes if receipt is bound to run | suitable |
| remove formatting dependence | influence disappears | simpler claim | suitable if product allows |
The best repair depends on ownership. For this repository specimen, the declared contract file is the clearest route because the value is a small, non-secret policy input.
Draft the repaired declaration¶
A direct repair would make the hidden stage consume a contract:
hidden_runtime:
cmd: >-
python scripts/render_runtime.py
--source data/source.txt
--contract environment/runtime.env
--output results/hidden.txt
deps:
- data/source.txt
- environment/runtime.env
- scripts/render_runtime.py
outs:
- results/hidden.txt
The exercise is conceptual; do not edit the governed specimen during the audit. Its intentional defect is required for the comparison and self-test.
A repair acceptance test should prove:
- baseline settles;
- changing only the contract makes the repaired stage stale;
- ordinary repro executes it;
- output adopts current policy;
- a second repro converges.
“The YAML looks right” is not enough.
Write the review note¶
A concise, evidence-backed note:
The baseline converged with both stages rendering
8.00. The drift fingerprint changed onlyreport_stylefrom dot to comma. The declared case named its runtime contract as a changed dependency, rebuilt, and produced8,00. The hidden case had empty status, ordinary repro skipped, and its output retained dot semantics. Forced execution under comma changed the result and lock while the declaration hash remained stable. This proves report style is causal but undeclared. Reject the retained hidden result and represent report style as a dependency or enforced run-bound control.
The note avoids three overclaims:
- it does not say all environment drift behaves this way;
- it does not call an empty status result a DVC failure;
- it does not claim force repaired the graph.
Transfer the method to a local-versus-CI incident¶
A real incident is noisier. Use the same proof structure:
| Controlled specimen | Local-versus-CI analogue |
|---|---|
| paired declaration | compare stage and command identities |
| paired fingerprints | compare governed executor facts |
| preserved status | capture status before rerun |
| ordinary repro receipt | prove whether disputed stage ran |
| forced causal probe | reproduce one suspected difference in isolation |
| declaration hash | check whether repair changed the contract |
| semantic result | compare the claim-relevant output |
If many runtime fields differ, isolate them in smaller experiments. Do not leap directly from a broad fingerprint diff to a causal conclusion.
Investigation checklist¶
Before closing a runtime-drift review, confirm:
- The result claim and comparison method are explicit.
- Code, data, parameters, and declaration identities were checked.
- Pre-intervention status and results were preserved.
- Each compared result has current-execution evidence.
- The suspected runtime difference is recorded in structured form.
- A controlled intervention supports the causal claim.
- Diagnostic force is not presented as contract repair.
- The repair closes the demonstrated visibility or enforcement gap.
- The conclusion states what the evidence does not prove.
The investigation is complete when a missed-class learner can reproduce the observations, the causal reasoning, and the repair decision from the packet alone.