What a scan does, and what a named key does¶
They behave differently on purpose.
- A scan —
digline list,--run latest,digline view— steps over a document it cannot read and says how many it stepped over. A survey stops at nothing it merely fails to recognise. What it must never do is skip in silence, so the count is reported by schema version. - A key asked for by name is refused instead. There the caller named that file, and must be told it cannot be read.
$ digline list --suite suite.py
KEY CREATED ENV COMMIT CASES
2026-08-26T15-44-09-492722-00-00-e7421ec503ccefe8 2026-08-26T15:44:09.492722+00:00 staging b304442 2
* = current baseline
ignored: 1 run(s) at schema 5
run `digline migrate` to bring them up to date
The first version of the listing raised on the first foreign file, which made
--run latest fail the morning after a release for a reason that had nothing to
do with the run being asked for.
A file whose name is not its run's key is left out first, before its
schema is looked at. A run's key is key_of(created_at, config_hash), the name
digline files it under, and the store answers to that key alone (ADR 0040). A
file renamed or copied by hand is named in the note with what to do:
left out for their name: 'rossi-mario' holds a run not among the listed runs: rename it to 2026-10-04T09-36-41-494925-00-00-8a745d27d002e7dd.json
A copy says the name is already taken and tells nobody to rename it. Two files
that hold one run, neither under its key, are both named and neither is told
to take the name, because which is the original is not known. Where the key
is not a safe file name, the note says so and proposes none. It is not a
migration: migrate rewrites a document in place and never renames one.
--run latest steps over such a file, so one Finder copy no longer stops it
for the whole suite (#429), and the note never says the file was newer.
A document at the current schema that is not a run passes the scan. The
scan reads only schema_version, created_at and config_hash, so it is the
read after it that refuses the document: a run with a mandatory field missing, or one filed under another
suite or tenant. digline list, digline log and digline view leave that run
out and name it beside what the scan skipped: refused: 1 run(s): <key>
(<why>). Until #314 they failed whole on it. --run latest still refuses,
and on purpose: a document that cannot be read has no created_at anyone can
trust, so nothing can say whether it was newer than the run latest would
otherwise pick, and comparing or promoting the wrong run in silence is worse
than stopping.
Migrating¶
--dry-run says what would happen and writes nothing:
$ digline migrate --suite suite.py --dry-run
would migrate 2026-08-26T15-44-09-282929-00-00-e7421ec503ccefe8.json from schema 5
1 would migrate, 2 already current, 0 refused
$ digline migrate --suite suite.py
migrated 2026-08-26T15-44-09-282929-00-00-e7421ec503ccefe8.json from schema 5
1 migrated, 2 already current, 0 refused
Each file is rewritten in place only after it has been re-read with the new
schema: a document that does not parse back is not a document that was migrated.
The write goes through a temporary file and os.replace, so an interruption
never leaves half a run behind.
Additive and non-additive¶
- Additive bumps — a field added with a defensible default — are carried forward.
- Non-additive ones are refused, and the refusal names what is missing.
There is no
tenantto invent for a file written before perimeters existed, and inventing one would put a run inside a perimeter nobody put it in.
A refused file is left exactly as it was. 0 refused in the tally is the
ordinary case; anything else is a decision for a person, not for the tool.
Additive does not mean empty. Schema 9 added the raw per-sample scores and
the interval they span, and the step derives them from metadata["scores"] —
a list every sampled run has carried since sampling existed. Reading what is
already in the document is not inventing, and it is the difference between a
promoted baseline being a noise floor on the day you upgrade and being one after
you re-promote everything. A run at samples=1 gains nothing but the version
number, because at one sample there is no interval: [score, score] would be a
noise floor of zero width dressed as a measurement.
Derived can still change what a run says. Schema 17's step writes the
identity a calibration band binds by, taking it from the verdicts the case
already holds. Under 16 a band bound by name, and a band for a check that names
its score otherwise bound nothing and read as held. After migrating, that band
is read. A run that exited 0 can read as exit 2, and a committed baseline
can come to say that its calibration case was outside its band. Nothing about
the run changed: the document now says what happened. That is why migrate is a
person's decision from 0.21.0 on, and why an agent proposes it and does not run
it. A file whose verdicts do not say which check its band belongs to is refused
and left as it was.
One thing the step deliberately leaves alone: an aggregate — precision, accuracy — records no interval on migration. Sizing one needs the marks and the declared assertion, and a run file carries neither, so a migrated baseline sizes the noise of its cases and reports the noise of its aggregates as not known. The aggregate floors arrive when you promote a run produced under this release.
See also¶
api.md— the public APIadr/0002-three-worlds-and-where-the-data-lives.md— the tenant as a perimeter