ASTRA declares the decision space — every methodological choice that could plausibly move a number, with its options and the reasoning behind each. fromwhere records what actually ran — the SHA256 of every input as it was when the output was made, who is answerable for the primary files, and the Content Credentials on the figure that leaves the repository.
The join between them is the command. Every recipe in astra.yaml is wrapped in
fromwhere run, and every decision reaches the command line, so the command
fromwhere writes into provenance.toml names the options that produced the artifact
sitting beside it:
[artifact."results/peak.json"]
command = "uv run python src/peak.py --fit polynomial --estimator fit_peak""Which universe is this number from?" becomes a question about the record rather than about somebody's memory.
Both tools wrap the work from the outside, so both belong on your PATH rather
than in the project environment — and pyproject.toml here mentions neither:
uv tool install astra-analysis
uv tool install fromwhereuv lock
uv run python src/run.py # the baseline universe
uv run python src/run.py universes/smoothed.yaml # the alternative
fromwhere statussrc/run.py is a ~60-line runner and is not part of either tool. astra
validates and inspects; it never executes recipes, because the choice of runner
is yours and the spec should not encode one. This is the smallest one that
substitutes the placeholders and runs the commands in dependency order.
The declarations are a one-time authoring step. Nothing produces the dataset or the scripts, and a declaration is a statement by a person:
fromwhere declare data/performance.csv --mine
fromwhere declare src/curves.py --mine --with-ai 'Claude Opus 5'
# ...and the rest of src/provenance.toml is in version control, along with every artifact it describes.
That is not incidental to the example: the record is the evidence, and a
project that ignores its own record is one where nobody can check anything
without rebuilding first. Clone this and fromwhere status has an answer
immediately.
uv run python src/run.py && cat results/peak.json
# peak C_P 0.4010 at λ = 2.80
uv run python src/run.py universes/smoothed.yaml && cat results/peak.json
# peak C_P 0.3691 at λ = 2.71Same data, same code, two defensible readings, and an 8% difference in the number a paper would quote. Neither is wrong. What would be wrong is for that choice to live in a plotting call where no reader could find it — the silent default, which ASTRA exists to prevent and which nothing else flags.
peak_estimator: fit_peak is marked requires: [curve_fit.polynomial], because
taking the maximum of a curve you did not fit is not a coherent request.
astra universe check catches that pairing before anything runs.
fromwhere records the files a command names, not the ones a script imports.
src/curves.py holds the fit shared by the plots and the peak — precisely so a
figure and the number quoted from it cannot disagree — but it appears nowhere on
the command line, so fromwhere could not see it. Editing it changed every result
and made nothing stale.
The fix is one flag, and it is in the recipes:
fromwhere run --no-sign -i {inputs.measurements} -i src/curves.py ...
Worth knowing in general: fromwhere hashes what the command mentions, which
catches the entry-point script for free and misses everything it imports. If a
module can change a result, pass it with -i.
--no-sign on the two panels, and its absence on performance.svg.
A C2PA manifest carries a fresh instance ID and a timestamp, so signing identical bytes twice produces two different files. Sign an intermediate and every no-op edit produces a new hash and a rebuild of everything downstream.
So sign at the boundary: the artifact that leaves the repository. The panels never travel; the stacked figure does. That also reads correctly as a statement of intent — this is the artifact I am putting my name on.
results/peak.json is recorded but not signed, because JSON has nowhere to put
a manifest. For that output the line in provenance.toml is the only provenance
there is, which is exactly why the header at the top of that file is not
decoration.
Without fromwhere, astra.yaml says which options should have produced the
figure and cannot say which ones did, nor whether the file on disk is still the
one they produced.
Without ASTRA, provenance.toml records --fit polynomial faithfully and has
nothing to say about what else could have been chosen, why, or what it would
have cost. A record of one path through a decision space that is nowhere
written down.
astra and fromwhere are on your PATH as tools, not in pyproject.toml —
they have to be, because both wrap the recipe command from the outside. So the
environment the science runs in is pinned by uv.lock, and the two tools
wrapping it are not.
That matters a little less here than elsewhere: astra never executes anything,
so it cannot change a number. fromwhere can only record. Still, the general
shape holds — fromwhere records the lock of the environment the command ran in,
and whatever wraps that command is outside it. See the top-level README.