Provenance and auditing
What a run recorded about itself: the versions every tool actually ran at, every parameter as it was hashed, and the references by accession and checksum — and, the other way round, what a number you published rests on.
Methods
class Methods(
steps: 'list[StepMethods]' = <factory>,
gaps: 'list[MethodsGap]' = <factory>,
references: 'list[PinnedReference]' = <factory>,
citations: 'list[str]' = <factory>,
uncited: 'list[str]' = <factory>,
claims: 'list[ClaimCheck]' = <factory>,
engines: 'list[str]' = <factory>,
platforms: 'list[str]' = <factory>,
summary: 'str' = '',
text: 'str' = '',
bundle: 'Bundle | None' = None,
verification: 'Verification | None' = None,
) -> NoneHow a set of results was produced.
Attributes
| Attribute | Description |
|---|---|
steps | Each distinct way a step was run, in the order the pipeline runs them. |
gaps | Steps nothing is known about. |
references | Every reference any step was given, pinned, with no duplicates. |
citations | The reference list, as a methods section prints it. |
uncited | Tools with nothing to cite — named rather than omitted, so a short reference list cannot pass for a complete one. |
claims | Each claim the pipeline declared, and what became of it. Empty for a pipeline that declared none, which is why nothing about an existing write-up changes. |
engines | Every version of Ligase that produced any of the results. One entry for a run on one machine; more for a distributed run where a worker was on an older wheel. |
platforms | Every platform any of the results were produced on. One entry for a run on one machine; more for a mixed cluster, or an ARM workstation beside x86 nodes. |
summary | One line — 12 steps, all accounted for. |
text | The paragraph and the reference list, as the command line prints them. |
bundle | The bundle written, where one was asked for. |
verification | What checking a bundle found, where one was asked for. |
Methods.accounted_for
accounted_for: bool # propertyWhether every step’s production is recorded and every claim bound.
False means the write-up has a hole in it and says so. A number the analysis does not support is such a hole: a property with this name that returned True for one would be making the engine’s own promise falsely.
Methods.tools
tools: list[tuple[str, str]] # propertyEvery tool any step used, with the version it ran at.
Methods.unbacked
unbacked: list[ClaimCheck] # propertyThe claims this write-up cannot stand behind.
Contradicted ones included: a number the run positively disagrees with is not a number the write-up stands behind.
StepMethods
class StepMethods(
step: 'str',
nodes: 'int',
scopes: 'list[str]',
scope_kind: 'str | None',
tools: 'list[tuple[str,
str]]',
parameters: 'list[tuple[str,
str]]',
command: 'str | None',
) -> NoneOne way a step was run.
One written step becomes one node per sample, and all of them are one entry here. Where two nodes of a step genuinely were run differently they are two entries, because then the difference is the thing worth reporting.
Attributes
| Attribute | Description |
|---|---|
step | The step’s name, as written. |
nodes | How many nodes this covers. |
scopes | What tells them apart, e.g. the sample identifiers. |
scope_kind | What kind of thing that is, e.g. sample. |
tools | Each tool and the whole version it printed. |
parameters | Each plain parameter and its value, as it was hashed. |
command | The command as written in the pipeline, placeholders and all, or None for a step whose body is Python. |
MethodsGap
class MethodsGap(step: 'str', scopes: 'list[str]', nodes: 'int') -> NoneA step nothing is known about.
Its output is in the cache from a run that recorded no provenance, and
nothing can reconstruct how it was produced: the command that would have
said so is not in the hash, by design. ligase run --force runs it
again and records it.
Attributes
| Attribute | Description |
|---|---|
step | The step’s name. |
scopes | What tells its nodes apart. |
nodes | How many of them are unaccounted for. |
Claim
class Claim(
text: 'str',
step: 'str',
scope: 'str | None' = None,
parameter: 'str | None' = None,
tool: 'str | None' = None,
value: 'str | float | None' = None,
at: 'Cell | None' = None,
tolerance: 'float | None' = None,
) -> NoneA number or a phrase a paper states, and where it came from.
pipe = lg.Pipeline( outputs, claims=[ lg.Claim( “variants were called above 1% allele frequency”, step=“call”, parameter=“min_freq”, value=0.01, ), ], )
text and value are separate on purpose. A paper writes 1%; the
pipeline holds 0.01. Comparing the sentence against the parameter
would be a guess about arithmetic in English, so the sentence is what a
reader greps the write-up for and value is what the engine compares —
written the way the pipeline holds it.
Everything except text is checked against the run: the step is one the
write-up accounts for, the sample is one it ran on, the parameter or tool
is one the record holds, and the value agrees. A claim that survives all
of that is bound to the step key and the output hash behind it; one
that does not is reported rather than reprinted, because a declaration
nothing can check is a comment.
A claim never enters a cache key, so editing one reruns nothing.
Raises
| Raises | When |
|---|---|
ValueError | If it names both a parameter and a tool, gives a value with nothing to compare it against, names either with no value, gives at without a value and a tolerance, or gives at beside a parameter or a tool — each of which is a declaration with nothing behind it, or two declarations in one. |
Attributes
| Attribute | Description |
|---|---|
text | The sentence as the paper states it. Never checked, and cannot be. |
step | The step that produced it, by name. |
scope | The sample it is about, where it is about one. None means the step across every sample it ran on, which is right for “every sample was interpreted” and wrong for a count from one of them. There is no inference. |
parameter | The parameter it restates, where it restates one. |
tool | The tool whose version it states, where it states one. |
value | What the paper says that parameter or version is, rendered the way the run recorded it — or, beside at, the number the paper publishes. |
at | Where in the step’s output the number lives, from lg.cell. Given, the engine reads that cell and compares: the claim comes back verified or contradicted rather than merely bound. Left out, the claim rests on the computation and the number is not checked, which is what it has always done. |
tolerance | How far the number in the output may be from the one the paper states. Required with at, and never defaulted: a claim to three decimals and a claim to machine precision are different claims, and a default would invent a precision nobody stated. |
Claim.located
located: tuple[float, str, int, float] | None # propertyThe number, the cell and the tolerance, as the engine takes them.
None for a claim that names no cell, which is most claims.
__post_init__ has already refused every incomplete combination,
so this reads the three parts without checking them again.
Claim.rendered
rendered: str | None # propertyThe value as the engine compares it: text, however it was given.
value=0.01 is what a person writes and 0.01 is how the parameter
was hashed, so one str() is the whole conversion. A number in a
paper is text.
None for a located claim, whose value is a number the engine
compares numerically rather than a string the record holds.
ClaimCheck
class ClaimCheck(
claim: 'Claim',
verdict: 'str',
reason: 'str',
recorded: 'str | None',
bound: 'list[BoundTo]' = <factory>,
) -> NoneOne claim, and what the run had to say about it.
Attributes
| Attribute | Description |
|---|---|
claim | The claim as it was declared. |
verdict | bound, verified, unbacked or contradicted. verified is bound and more: the number was read out of the output and agrees. contradicted for a located claim means the number in the sentence is not the number in the bytes. |
reason | The sentence the write-up prints for it, so a script reporting one claim says what ligase methods says. |
recorded | What the run recorded, where that disagrees with the claim — and, for a verified claim, the number that was read. None otherwise. |
bound | The computations it rests on. Empty for an unbacked claim; one entry for a claim naming a sample, and one per sample for a claim that names none. |
ClaimCheck.is_bound
is_bound: bool # propertyWhether the write-up can stand behind the claim.
True for a verified claim as well as a bound one: verified is bound
and more, so a property that excluded it would make
accounted_for false for the claims the engine is most sure of.
ClaimCheck.is_verified
is_verified: bool # propertyWhether the number was read out of the output and agreed.
Cell
class Cell(column: 'str', row: 'int' = 0) -> NoneA cell of a step’s output: a column by name, a row by position.
lg.Claim( “malaria F1 = 0.333 against seven curated targets”, step=“validate”, value=0.333, at=lg.cell(column=“f1”, row=0), tolerance=5e-4, )
Row 0 is the first row after the header, because that is the first row of data and a claim is about data. A ranking’s top row is therefore row 0, which is the row its own rank 1 is.
The engine reads the place it is given and nowhere else. It will not go looking for a number in output it was not pointed at, and it parses one format — a TSV with a header — rather than guessing at others. A claim that cannot say where its number is stays step-only, which is a legitimate state and not an error.
Attributes
| Attribute | Description |
|---|---|
column | The column’s name, as the output’s header spells it. |
row | The row’s position among the data rows, counting from zero. |
cell
cell(*, column: 'str', row: 'int' = 0) -> 'Cell'Name the cell of a step’s output a claim’s number lives in.
Parameters
| Parameter | Description |
|---|---|
column | The column’s name, as the output’s header spells it. |
row | The row’s position among the data rows, from zero. Defaults to the first row of data, which is the common case for a one-row summary table. |
Returns
A location a Claim can carry in at=.
BoundTo
class BoundTo(
scope: 'str | None',
key: 'str',
canonical: 'str',
recorded_at: 'int',
platform: 'str',
) -> NoneOne computation a claim rests on.
The step key, not the occasion. A key is the identity of a computation — the same code, parameters, inputs and tool versions — so a step served from the cache backs its claim completely, and what this adds is when that computation happened, which may be long before the run that wrote the claim up.
Attributes
| Attribute | Description |
|---|---|
scope | The sample it was for, where anything tells the nodes apart. |
key | The step key, in full. |
canonical | The canonical hash of what it produced. |
recorded_at | When the computation happened, as seconds since the Unix epoch — not when this run fetched it. |
platform | The platform it happened on. |
Bundle
class Bundle(
root: 'str',
files: 'list[str]',
steps: 'int',
gaps: 'int',
) -> NoneA reproducibility bundle that was written.
Attributes
| Attribute | Description |
|---|---|
root | Where it is. |
files | Everything in it, relative to the root. |
steps | How many steps it accounts for. |
gaps | How many it could not. |
Verification
class Verification(
results: 'list[ResultCheck]' = <factory>,
inputs: 'list[InputCheck]' = <factory>,
reproduced: 'bool' = False,
verdict: 'str' = 'missing',
summary: 'str' = '',
text: 'str' = '',
) -> NoneWhat checking a bundle against this machine found.
Attributes
| Attribute | Description |
|---|---|
results | Every result the bundle recorded. |
inputs | Every input it recorded. |
reproduced | Whether every recorded result came out the same. |
verdict | The worst thing found. |
summary | One line — 12 results: 4 identical, 8 reproduced. |
text | The whole report as the command line prints it. |
Verification.differing
differing: list[ResultCheck] # propertyThe results that did not come out the same.
Verification.moved
moved: list[InputCheck] # propertyThe inputs that are not the ones the bundle was built from.
Worth reading before any differs verdict: a changed input explains
a changed output, and most of a bundle’s inputs are named rather
than carried — a local file by content hash, a pinned remote by
locator and digest — so they are the ones that can have moved. A
recorded query cannot appear here, because its response body
travels in the bundle and is replayed from it.
PinnedReference
class PinnedReference(
param: 'str',
path: 'str',
build: 'str | None',
accession: 'str | None',
contigs: 'int',
length: 'int',
checksum: 'str | None',
fingerprint: 'str',
) -> NoneA reference sequence, pinned as firmly as its file allows.
Nothing here is declared or guessed. The accession is the sequence name
where the name is one — K03455.1 carries its own version — and the
checksum is the MD5 the SAM specification defines for @SQ M5, over the
uppercased sequence with whitespace removed.
Attributes
| Attribute | Description |
|---|---|
param | The parameter it filled. |
path | Where it was when the step ran. |
build | The label it was declared under, e.g. HXB2. Display only. |
accession | The accession with its version, where the sequence name is one, and None where it is not — chr1 is a name, not an accession, and a methods section must not cite it as one. |
contigs | How many sequences it holds. |
length | Their total length in bases. |
checksum | The MD5 of the sequence, for a single-sequence reference. |
fingerprint | A short digest of the whole identity, always present. |
AuditReport
class AuditReport(
steps: 'list[StepAudit]' = <factory>,
failures: 'list[StepFailure]' = <factory>,
verdict: 'str' = 'deterministic',
summary: 'str' = '',
text: 'str' = '',
) -> NoneWhat an audit found.
Attributes
| Attribute | Description |
|---|---|
steps | Every node audited, in graph order. |
failures | Every node that could not be audited because it failed. |
verdict | The worst verdict any node got — the worst rather than the commonest, since one irreproducible sample in a cohort is an irreproducible step. |
summary | One line — 4 steps: 1 deterministic, 3 reproducible. |
text | The whole report as the command line prints it, worded by the engine so that every surface reads the same. |
AuditReport.reproducible
reproducible: bool # propertyWhether every node audited agreed with itself.
AuditReport.unreproducible
unreproducible: list[StepAudit] # propertyThe nodes whose two runs disagreed about the result.
StepAudit
class StepAudit(
step: 'str',
sample: 'str | None',
scope: 'str | None',
verdict: 'str',
first: 'str',
second: 'str',
first_canonical: 'str',
second_canonical: 'str',
first_duration_ms: 'int',
second_duration_ms: 'int',
node: 'int',
seed: 'tuple[str,
str] | None' = None,
) -> NoneWhat two runs of one step showed.
Attributes
| Attribute | Description |
|---|---|
step | The step’s name. |
sample | The sample it was for, or None for a step that spans more than one. |
scope | How a report names this node where its step’s name is shared — sample SRR8525904 — or None where the name is already unambiguous. |
verdict | deterministic, reproducible, or not reproducible. |
first | The first run’s output hash. |
second | The second run’s output hash. |
first_canonical | What the first run’s output means. |
second_canonical | What the second run’s output means. |
first_duration_ms | How long the first run took. |
second_duration_ms | How long the second run took. |
node | Which node of the graph it was. |
seed | The parameter this step declared as its seed and the value it was held at, as ("seed", "42"). None for a step that declared none. A step with a seed is never deterministic, however its bytes fall: byte equality under a fixed seed says the seed held, not that the step has no randomness in it. The reproducible it gets instead is the best verdict available to it. |
StepAudit.reproducible
reproducible: bool # propertyWhether the two runs agreed on the result.
True for a step whose bytes moved and whose meaning did not: that is what reproducible means here.
Report
class Report(
errors: 'list[CheckError]',
summary: 'str',
text: 'str',
) -> NoneWhat a check found.
Attributes
| Attribute | Description |
|---|---|
errors | One error per failing step, in the order the steps run. A problem that hits four samples is four errors, each naming its own sample. |
summary | One line summarising the lot — 4 errors in 4 steps. — and the steps it counts are places rather than names, since one written step is many steps under fan-out. |
text | The whole report as the command line prints it: each distinct problem stated once, with every sample it affects named, and the summary at the end. Worded by the engine so that every surface reports a check the same way. |
RunReport
class RunReport(
steps: 'list[StepRun]' = <factory>,
failures: 'list[StepFailure]' = <factory>,
summary: 'str' = '',
text: 'str' = '',
hashed: 'int' = 0,
prefiltered: 'int' = 0,
canonicalised: 'int' = 0,
worker: 'str | None' = None,
divergences: 'list[Divergence]' = <factory>,
) -> NoneEverything a run did.
Attributes
| Attribute | Description |
|---|---|
steps | Every step that produced an output, in graph order. |
failures | Every step that did not. |
summary | One line — 12 steps: 8 ran, 4 from cache. — counting places rather than names, since one written step is many steps under fan-out. |
text | The whole report as the command line prints it, worded by the engine so that every surface reads the same. |
hashed | Input files that had to be read and hashed. |
prefiltered | Input files whose hash was already known from their size and mtime, and so were not read at all. |
canonicalised | Files that had to be parsed to find out what they mean. Like hashed, this is zero on a run that found everything it needed already known. |
worker | What this worker called itself, or None for an ordinary run. |
divergences | Steps another worker computed to a different answer. Empty for almost every run, and for every run on one machine. |
RunReport.cached
cached: list[StepRun] # propertyThe steps that were served from the cache.
RunReport.outputs
outputs(self, step: 'str') -> 'list[str]'Where one step’s outputs are, in graph order.
Parameters
| Parameter | Description |
|---|---|
step | The step’s name. Under fan-out there is one output per sample. |
Returns
One path per node of that name.
RunReport.ran
ran: list[StepRun] # propertyThe steps that actually ran, in graph order.
RunReport.skipped
skipped: list[StepRun] # propertyThe steps an upstream rerun turned out not to change.
StepRun
class StepRun(
step: 'str',
sample: 'str | None',
outcome: 'str',
output: 'str',
key: 'str',
hash: 'str',
canonical: 'str',
duration_ms: 'int',
node: 'int',
notes: 'tuple[str,
...]' = (),
) -> NoneOne step that produced an output.
Attributes
| Attribute | Description |
|---|---|
step | The step’s name. |
sample | The sample it was for, or None for a step that spans more than one — naming such a step after one of its samples would send you to the wrong data. |
outcome | ran, cached, or skipped. skipped is early cutoff: the step’s key was already known although something above it reran, because that rerun produced the same output as before. |
output | Where the output is, in the content-addressed store. |
key | The cache key, as hex. |
hash | The hash of the output’s bytes, as hex, which is also its name in the store. |
canonical | The hash of what the output means, as hex: its records sorted and its volatile headers stripped. This is what the steps below were keyed on, so two runs that agree here skip everything downstream however far their bytes diverged. |
duration_ms | How long it took, when it ran; zero otherwise. |
node | Which node of the graph it was. |
notes | What was noticed about this output and its hash cannot say — one sentence each, and usually none. A note is not a failure: the step ran and its result stands. Two things put them here. Canonicalising the output finds the first kind, of which there is one in v0.1: a sample identifier that is a file name rather than a sample, which makes this output’s canonical hash depend on where that file was. The step’s own body says the second kind — an assumption its work rested on, which the file it wrote need not show. A resistance reading made without coverage is taking every position the caller did not report to be reference, and says so here (D32). |
StepRun.ran
ran: bool # propertyWhether this step actually ran, rather than being served.