Skill Tracer
Skills › Scientific Agent Skills › datalad › provenance.md

provenance.md

253 lines · skills/datalad/references/provenance.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Computational provenance with DataLad
2

Computational provenance with DataLad

Raw lines 1 to 2
3## Why this is the interesting half
4
5Version control tells you that a file changed. Provenance tells you what produced it, from
6what, and with which software. `datalad run` records the command and tracked inputs/outputs in the same commit as the change. Capturing software versions additionally requires a versioned environment specification or a tracked container; an executable name alone does not identify its installed build. `datalad
7rerun` then reads that record back and re-executes it, so "is this reproducible" becomes a
8command rather than an argument.
9

Why this is the interesting half

Version control tells you that a file changed. Provenance tells you what produced it, from what, and with which software. datalad run records the command and tracked inputs/outputs in the same commit as the change. Capturing software versions additionally requires a versioned environment specification or a tracked container; an executable name alone does not identify its installed build. datalad rerun then reads that record back and re-executes it, so "is this reproducible" becomes a command rather than an argument.

Raw lines 3 to 9
10## Principles: STAMPED, and the YODA layout it grew out of
12YODA ("YODAs Organigram on Data Analysis") is the convention DataLad analyses were
13originally built on, and it is still what `datalad create -c yoda` configures. Its three
14principles (one thing one dataset, record where the data came from, record what was done
15to it) describe what a well-formed analysis looks like, but they were inspirational rather
16than operational: there is nothing in them to check a dataset against.
18STAMPED (<https://stamped-principles.org>) is the operationalized successor, formalized by
19some of the original YODA authors. It states seven properties of a reproducible research
20object, each backed by normative MUST/SHOULD/MAY requirements:
22| Property | Core requirement |
23|---|---|
24| **S**elf-contained | Everything essential to replicate the computation is reachable within a single top-level research object |
25| **T**racked | Persistent content identification and provenance are recorded for every component and every modification, including the versions involved |
26| **A**ctionable | The object carries enough instruction to reproduce all results, specified as *executable* specifications rather than prose |
27| **M**odular | Components are organized as independently versioned modules, included directly or linked as subdatasets |
28| **P**ortable | Procedures depend on no undocumented host state; environments are explicitly specified and version controlled |
29| **E**phemeral | Results are produced in disposable environments built only from the object's own contents |
30| **D**istributable | Every referenced module and component is persistently retrievable by others |
32**Actionable is the property `datalad run` exists to satisfy.** A README describing how a
33figure was produced is documentation; a run record is an executable specification, and
34`datalad rerun` is what makes it executable. The same commit satisfies Tracked, because
35the command, its inputs, its outputs, and the versions they were taken at are recorded
36next to the change rather than in a separate log that can drift away from it. Modular maps
37onto subdatasets, Portable onto tracked images and runtime specifications, Ephemeral onto disposable
38execution environments, and Distributable onto
39siblings and RIA stores (see [publishing.md](publishing.md)).
41Two companion resources make this checkable rather than aspirational:
42<https://checklist.stamped-principles.org> walks the requirements as a MUST/SHOULD/MAY
43checklist, and <https://examples.stamped-principles.org> collects worked patterns,
44including ones built directly on `datalad run` and `datalad rerun`.

Principles: STAMPED, and the YODA layout it grew out of

YODA ("YODAs Organigram on Data Analysis") is the convention DataLad analyses were originally built on, and it is still what datalad create -c yoda configures. Its three principles (one thing one dataset, record where the data came from, record what was done to it) describe what a well-formed analysis looks like, but they were inspirational rather than operational: there is nothing in them to check a dataset against.

STAMPED (<https://stamped-principles.org>) is the operationalized successor, formalized by some of the original YODA authors. It states seven properties of a reproducible research object, each backed by normative MUST/SHOULD/MAY requirements:

PropertyCore requirement
Self-containedEverything essential to replicate the computation is reachable within a single top-level research object
TrackedPersistent content identification and provenance are recorded for every component and every modification, including the versions involved
ActionableThe object carries enough instruction to reproduce all results, specified as executable specifications rather than prose
ModularComponents are organized as independently versioned modules, included directly or linked as subdatasets
PortableProcedures depend on no undocumented host state; environments are explicitly specified and version controlled
EphemeralResults are produced in disposable environments built only from the object's own contents
DistributableEvery referenced module and component is persistently retrievable by others

**Actionable is the property datalad run exists to satisfy.** A README describing how a figure was produced is documentation; a run record is an executable specification, and datalad rerun is what makes it executable. The same commit satisfies Tracked, because the command, its inputs, its outputs, and the versions they were taken at are recorded next to the change rather than in a separate log that can drift away from it. Modular maps onto subdatasets, Portable onto tracked images and runtime specifications, Ephemeral onto disposable execution environments, and Distributable onto siblings and RIA stores (see publishing.md).

Two companion resources make this checkable rather than aspirational: <https://checklist.stamped-principles.org> walks the requirements as a MUST/SHOULD/MAY checklist, and <https://examples.stamped-principles.org> collects worked patterns, including ones built directly on datalad run and datalad rerun.

Raw lines 10 to 45
46### The YODA layout in practice
48Apply the layout at creation time:
50```bash
51datalad create -c yoda "my_analysis"
52```
54That produces:
56```text
58├── .gitattributes
59├── CHANGELOG.md
60├── code
61│ ├── .gitattributes
62│ └── README.md
63└── README.md
64```
66The configuration matters more than the directories. Everything in `code/`, plus
67`README.md` and `CHANGELOG.md`, is tracked by Git rather than git-annex, so scripts stay
68directly readable and diffable in a clone that has fetched no data at all. Input data is
69then added as a subdataset:
71```bash
72datalad clone -d . https://github.com/OpenNeuroDatasets/ds000001.git inputs/raw
73```
75The `-d .` is what registers the clone as a subdataset of the analysis rather than leaving
76an unrelated repository sitting inside it.

The YODA layout in practice

Apply the layout at creation time:

datalad create -c yoda "my_analysis"

That produces:

.
├── .gitattributes
├── CHANGELOG.md
├── code
│   ├── .gitattributes
│   └── README.md
└── README.md

The configuration matters more than the directories. Everything in code/, plus README.md and CHANGELOG.md, is tracked by Git rather than git-annex, so scripts stay directly readable and diffable in a clone that has fetched no data at all. Input data is then added as a subdataset:

datalad clone -d . https://github.com/OpenNeuroDatasets/ds000001.git inputs/raw

The -d . is what registers the clone as a subdataset of the analysis rather than leaving an unrelated repository sitting inside it.

Raw lines 46 to 77
78## datalad run
80```
81datalad run [-h] [-d DATASET] [-i PATH] [-o PATH] [--expand {inputs|outputs|both}]
82 [--assume-ready {inputs|outputs|both}] [--explicit] [-m MESSAGE]
83 [--sidecar {yes|no}] [--dry-run {basic|command}]
84 [--on-cmd-failure {error|save|all}] [-J NJOBS]
85 [--version] ...
86```
88| Option | Documented behaviour | Practical consequence |
89|---|---|---|
90| `-i/--input PATH` | "A dependency for the run. Before running the command, the content for this relative path will be retrieved." | The command does not fail on an unfetched pointer, and `rerun` knows what to fetch elsewhere |
91| `-o/--output PATH` | "Prepare this relative path to be an output file of the command." | Unlocks or removes the target so git-annex write protection does not block the write |
92| `--explicit` | "Consider the specification of inputs and outputs to be explicit. Don't warn if the repository is dirty." | Lets a run proceed in a dirty dataset, and saves only the declared outputs |
93| `--assume-ready {inputs\|outputs\|both}` | "Assume that inputs do not need to be retrieved and/or outputs do not need to unlocked or removed." | Skips preparation for speed; only safe when you have already done it |
94| `--expand {inputs\|outputs\|both}` | "Expand globs when storing inputs and/or outputs in the commit message." | Records the concrete file list rather than the glob, which is what you want when the glob's meaning could change |
95| `--dry-run {basic\|command}` | "Do not run the command; just display details about the command execution." | Check placeholder expansion before committing anything |
96| `--sidecar {yes\|no}` | Store the run record in a separate file rather than in the commit message | Keeps long records out of `git log` output |
98Placeholders available in the command string: `{pwd}` (current working directory),
99`{dspath}` (dataset path), `{tmpdir}` (a temporary directory), `{inputs}` and `{outputs}`
100(the values of the corresponding flags), and `{inputs[0]}` for indexed access.
102Globs are permitted in `--input` and `--output`, and multiple flags may be given:
104```bash
105datalad run -m "second-level model" \
106 -i "derivatives/sub-*/func/*_bold.nii.gz" \
107 -i "code/model.py" \
108 -o "results/group_map.nii.gz" \
109 --expand inputs \
110 "python code/model.py {outputs}"
111```

datalad run

datalad run [-h] [-d DATASET] [-i PATH] [-o PATH] [--expand {inputs|outputs|both}]
    [--assume-ready {inputs|outputs|both}] [--explicit] [-m MESSAGE]
    [--sidecar {yes|no}] [--dry-run {basic|command}]
    [--on-cmd-failure {error|save|all}] [-J NJOBS]
    [--version] ...
OptionDocumented behaviourPractical consequence
-i/--input PATH"A dependency for the run. Before running the command, the content for this relative path will be retrieved."The command does not fail on an unfetched pointer, and rerun knows what to fetch elsewhere
-o/--output PATH"Prepare this relative path to be an output file of the command."Unlocks or removes the target so git-annex write protection does not block the write
--explicit"Consider the specification of inputs and outputs to be explicit. Don't warn if the repository is dirty."Lets a run proceed in a dirty dataset, and saves only the declared outputs
`--assume-ready {inputs\outputs\both}`"Assume that inputs do not need to be retrieved and/or outputs do not need to unlocked or removed."Skips preparation for speed; only safe when you have already done it
`--expand {inputs\outputs\both}`"Expand globs when storing inputs and/or outputs in the commit message."Records the concrete file list rather than the glob, which is what you want when the glob's meaning could change
`--dry-run {basic\command}`"Do not run the command; just display details about the command execution."Check placeholder expansion before committing anything
`--sidecar {yes\no}`Store the run record in a separate file rather than in the commit messageKeeps long records out of git log output

Placeholders available in the command string: {pwd} (current working directory), {dspath} (dataset path), {tmpdir} (a temporary directory), {inputs} and {outputs} (the values of the corresponding flags), and {inputs[0]} for indexed access.

Globs are permitted in --input and --output, and multiple flags may be given:

datalad run -m "second-level model" \
  -i "derivatives/sub-*/func/*_bold.nii.gz" \
  -i "code/model.py" \
  -o "results/group_map.nii.gz" \
  --expand inputs \
  "python code/model.py {outputs}"
Raw lines 78 to 112
Runs code python code/model.py :110
113### The run record
115The commit message carries a machine-readable JSON block between the markers
116`=== Do not change lines below ===` and `^^^ Do not change lines above ^^^`. It records
117the command, the dataset ID, the exit status, and the input and output specifications. The
118handbook is explicit that this section is "less for the human user" and exists "for
119DataLad, in particular for the `datalad rerun` command". Editing it by hand, including
120during an interactive rebase, breaks `rerun` silently.
122Two behaviours that surprise people:
124- A run producing no change to the dataset produces no commit at all, exactly as a
125 `datalad save` with nothing to save does. An empty history entry is not evidence the run
126 failed to execute, only that it changed nothing.
127- `datalad run` refuses to start in a dirty dataset. This is the point of the command: a
128 record built on an unknown starting state does not establish anything. Save or discard
129 first, or state the scope with `--explicit`.

The run record

The commit message carries a machine-readable JSON block between the markers === Do not change lines below === and ^^^ Do not change lines above ^^^. It records the command, the dataset ID, the exit status, and the input and output specifications. The handbook is explicit that this section is "less for the human user" and exists "for DataLad, in particular for the datalad rerun command". Editing it by hand, including during an interactive rebase, breaks rerun silently.

Two behaviours that surprise people:

  • A run producing no change to the dataset produces no commit at all, exactly as a datalad save with nothing to save does. An empty history entry is not evidence the run failed to execute, only that it changed nothing.
  • datalad run refuses to start in a dirty dataset. This is the point of the command: a record built on an unknown starting state does not establish anything. Save or discard first, or state the scope with --explicit.
Raw lines 113 to 130
131### Failed command provenance
133DataLad 1.6.5 exposes `--on-cmd-failure error|save|all`. The default `error` stops
134without saving; `save` records changed outputs and returns success; `all` saves
135changed outputs and preserves the failing exit code. Inspect the run record
136`exit` field when evaluating results: a successful DataLad invocation need not
137mean the scientific command succeeded. On replay, a recorded nonzero code can
138be accepted when repeated, and a zero exit is accepted even if the original failed.
139A run producing no tracked changes still has no commit. These options are distinct
140from global result handling via `--on-failure`.

Failed command provenance

DataLad 1.6.5 exposes --on-cmd-failure error|save|all. The default error stops without saving; save records changed outputs and returns success; all saves changed outputs and preserves the failing exit code. Inspect the run record exit field when evaluating results: a successful DataLad invocation need not mean the scientific command succeeded. On replay, a recorded nonzero code can be accepted when repeated, and a zero exit is accepted even if the original failed. A run producing no tracked changes still has no commit. These options are distinct from global result handling via --on-failure.

Raw lines 131 to 141
142## datalad rerun
144```
145datalad rerun [-h] [--since SINCE] [-d DATASET] [-b NAME] [-m MESSAGE] [--onto base]
146 [--script FILE] [--report] [--assume-ready {inputs|outputs|both}] [--explicit]
147 [--on-cmd-failure {error|save|all}] [-J NJOBS] [--version] [REVISION]
148```
150- `REVISION` selects which recorded command to replay and defaults to `HEAD`.
151- `--since SINCE` replays a range: "the commands from all commits that are reachable from
152 revision but not SINCE will be re-executed (in other words, the commands in
153 `git log SINCE..REVISION`)". This is how a multi-step pipeline is replayed in order.
154- `--onto base` gives the "start point for rerunning the commands. If not specified,
155 commands are executed at HEAD." Use `--onto ''` to start at the parent of the first run commit selected
156 for replay. This resets the top-level dataset only; installed subdataset worktrees
157 do not automatically move to their recorded commits.
158- `-b/--branch NAME` creates and checks out a branch before replaying.
159- `--report` displays what would be done without executing, which is the safe first call.
160- `--script FILE` extracts the commands to a file instead of running them, with `-` for
161 stdout. This is how a DataLad history becomes a plain shell script for a reviewer or a
162 cluster submission.
164The reproducibility check worth building into a project:
166```bash
167git tag original-results HEAD
168# Replace FIRST_RUN with the first run commit hash; the ^ includes that run.
169datalad rerun --report --since FIRST_RUN^ original-results
170datalad rerun --onto= -b repro-check --since FIRST_RUN^ original-results
171git diff original-results HEAD -- results/
172```
174This comparison checks tracked output identities, including annex keys. It does not
175by itself establish numerical or scientific validity. Use a disposable clone for a
176fresh-environment check, restore recorded subdataset revisions, and inspect tolerances,
177randomness, and software versions separately. The `FIRST_RUN` example is a template;
178the local regression test substitutes a real commit and checks the regenerated bytes.

datalad rerun

datalad rerun [-h] [--since SINCE] [-d DATASET] [-b NAME] [-m MESSAGE] [--onto base]
    [--script FILE] [--report] [--assume-ready {inputs|outputs|both}] [--explicit]
    [--on-cmd-failure {error|save|all}] [-J NJOBS] [--version] [REVISION]
  • REVISION selects which recorded command to replay and defaults to HEAD.
  • --since SINCE replays a range: "the commands from all commits that are reachable from revision but not SINCE will be re-executed (in other words, the commands in git log SINCE..REVISION)". This is how a multi-step pipeline is replayed in order.
  • --onto base gives the "start point for rerunning the commands. If not specified, commands are executed at HEAD." Use --onto '' to start at the parent of the first run commit selected for replay. This resets the top-level dataset only; installed subdataset worktrees do not automatically move to their recorded commits.
  • -b/--branch NAME creates and checks out a branch before replaying.
  • --report displays what would be done without executing, which is the safe first call.
  • --script FILE extracts the commands to a file instead of running them, with - for stdout. This is how a DataLad history becomes a plain shell script for a reviewer or a cluster submission.

The reproducibility check worth building into a project:

git tag original-results HEAD
# Replace FIRST_RUN with the first run commit hash; the ^ includes that run.
datalad rerun --report --since FIRST_RUN^ original-results
datalad rerun --onto= -b repro-check --since FIRST_RUN^ original-results
git diff original-results HEAD -- results/

This comparison checks tracked output identities, including annex keys. It does not by itself establish numerical or scientific validity. Use a disposable clone for a fresh-environment check, restore recorded subdataset revisions, and inspect tolerances, randomness, and software versions separately. The FIRST_RUN example is a template; the local regression test substitutes a real commit and checks the regenerated bytes.

Raw lines 142 to 179
180## Containers
182`datalad-container` (reviewed release 1.2.6) records the software
183environment alongside the command.
185```
186datalad containers-add [-h] [-u URL] [-d DATASET] [--call-fmt FORMAT]
187 [-i IMAGE] [--update] [--extra-input FILE] [--version] NAME
188```
190Container source options (source-verified, runtime examples illustrative):
192- A local SIF file plus `--call-fmt 'apptainer exec {img} {cmd}'` avoids assuming a
193 `singularity` executable exists when only Apptainer is installed.
194- `docker://debian:stable-slim` builds a Singularity image. In 1.2.6 the released
195 `containers-add` implementation invokes `singularity build` and its guessed call
196 format uses `singularity exec`; it does not automatically substitute `apptainer`
197 for this path. Build the SIF externally with Apptainer, then register the local file.
198- `dhub://debian:stable-slim` uses Docker, saves the pulled image into the dataset,
199 and configures a Docker adapter with the current directory mounted at `/tmp`.
200- `shub://` remains recognized by the extension, but its legacy Singularity Hub
201 service was not verified in this review. Prefer a retrievable local image or an
202 actively maintained registry source.
204Use an immutable registry digest when building an image, retain the resulting image,
205and record its architecture. A mutable tag alone does not pin software. Configure
206required binds/environment/GPU options in the tracked call format and declare overlays
207with `--extra-input`. Container tracking alone does not isolate host mounts or ensure
208cross-platform numerical equivalence.
210```
211datalad containers-run [-h] [-n NAME] [-d DATASET] [-i PATH] [-o PATH] [-m MESSAGE]
212 [--expand {inputs|outputs|both}] [--explicit] [--sidecar {yes|no}] [--version] ...
213```
215`-n/--container-name` selects "the name of or a path to a known container to use for execution, in
216case multiple containers are configured". With exactly one container configured it may be
217omitted. During execution the environment variable `DATALAD_CONTAINER_NAME` holds the name
218of the container in use, which is available to the command itself.
220The image is tracked in the dataset like any other file, so it is annexed content: a
221collaborator gets it with `datalad get` and the provenance record points at a specific
222image rather than at a tag someone may have re-pushed.

Containers

datalad-container (reviewed release 1.2.6) records the software environment alongside the command.

datalad containers-add [-h] [-u URL] [-d DATASET] [--call-fmt FORMAT]
    [-i IMAGE] [--update] [--extra-input FILE] [--version] NAME

Container source options (source-verified, runtime examples illustrative):

  • A local SIF file plus --call-fmt 'apptainer exec {img} {cmd}' avoids assuming a singularity executable exists when only Apptainer is installed.
  • docker://debian:stable-slim builds a Singularity image. In 1.2.6 the released containers-add implementation invokes singularity build and its guessed call format uses singularity exec; it does not automatically substitute apptainer for this path. Build the SIF externally with Apptainer, then register the local file.
  • dhub://debian:stable-slim uses Docker, saves the pulled image into the dataset, and configures a Docker adapter with the current directory mounted at /tmp.
  • shub:// remains recognized by the extension, but its legacy Singularity Hub service was not verified in this review. Prefer a retrievable local image or an actively maintained registry source.

Use an immutable registry digest when building an image, retain the resulting image, and record its architecture. A mutable tag alone does not pin software. Configure required binds/environment/GPU options in the tracked call format and declare overlays with --extra-input. Container tracking alone does not isolate host mounts or ensure cross-platform numerical equivalence.

datalad containers-run [-h] [-n NAME] [-d DATASET] [-i PATH] [-o PATH] [-m MESSAGE]
    [--expand {inputs|outputs|both}] [--explicit] [--sidecar {yes|no}] [--version] ...

-n/--container-name selects "the name of or a path to a known container to use for execution, in case multiple containers are configured". With exactly one container configured it may be omitted. During execution the environment variable DATALAD_CONTAINER_NAME holds the name of the container in use, which is available to the command itself.

The image is tracked in the dataset like any other file, so it is annexed content: a collaborator gets it with datalad get and the provenance record points at a specific image rather than at a tag someone may have re-pushed.

Raw lines 180 to 223
224## Exporting provenance to a standard form
226DataLad's run records are DataLad's own format. Converting them to an interoperable
227representation is an open area rather than a solved one, and this is worth stating plainly
228rather than implying a pipeline exists:
230- **W3C PROV** is the standard target for provenance interchange. See
231 <https://www.w3.org/TR/prov-overview/>.
232- **datalad-metalad** ships a `runprov` extractor that reads DataLad run records, at
233 <https://github.com/datalad/datalad-metalad/blob/master/datalad_metalad/extractors/runprov.py>.
234 Its source exposes run-record extraction and JSON-LD-style provenance; compatibility
235 and exported graph validity were not executed here, so validate them before adoption.
236- **BIDS BEP028** tracks the provenance extension at
237 <https://bids.neuroimaging.io/extensions/beps/bep_028.html>. Check its current proposal and the adopted BIDS
238 version before choosing an export schema; a proposal is not a validated conversion API.
240Until one of those is settled, the durable artifact is the DataLad history itself plus
241`datalad rerun --script`, which produces a plain, reviewable command sequence that does
242not depend on DataLad to read.

Exporting provenance to a standard form

DataLad's run records are DataLad's own format. Converting them to an interoperable representation is an open area rather than a solved one, and this is worth stating plainly rather than implying a pipeline exists:

  • W3C PROV is the standard target for provenance interchange. See <https://www.w3.org/TR/prov-overview/>.
  • datalad-metalad ships a runprov extractor that reads DataLad run records, at <https://github.com/datalad/datalad-metalad/blob/master/datalad_metalad/extractors/runprov.py>. Its source exposes run-record extraction and JSON-LD-style provenance; compatibility and exported graph validity were not executed here, so validate them before adoption.
  • BIDS BEP028 tracks the provenance extension at <https://bids.neuroimaging.io/extensions/beps/bep_028.html>. Check its current proposal and the adopted BIDS version before choosing an export schema; a proposal is not a validated conversion API.

Until one of those is settled, the durable artifact is the DataLad history itself plus datalad rerun --script, which produces a plain, reviewable command sequence that does not depend on DataLad to read.

Raw lines 224 to 243
244## Further reading
246- `datalad run` chapter of the handbook:
247 <https://handbook.datalad.org/en/latest/basics/101-108-run.html>
248- YODA principles: <https://handbook.datalad.org/en/latest/basics/101-127-yoda.html>
249- STAMPED principles (operationalized from YODA): <https://stamped-principles.org>
250 - Compliance checklist: <https://checklist.stamped-principles.org>
251 - Worked examples and stencils: <https://examples.stamped-principles.org>
252- datalad-container documentation:
253 <https://docs.datalad.org/projects/container/en/stable/>

Further reading

  • datalad run chapter of the handbook: <https://handbook.datalad.org/en/latest/basics/101-108-run.html>
  • YODA principles: <https://handbook.datalad.org/en/latest/basics/101-127-yoda.html>
  • STAMPED principles (operationalized from YODA): <https://stamped-principles.org>
  • Compliance checklist: <https://checklist.stamped-principles.org>
  • Worked examples and stencils: <https://examples.stamped-principles.org>
  • datalad-container documentation: <https://docs.datalad.org/projects/container/en/stable/>
Raw lines 244 to 253