Skip to Content
ConceptsA number in a paper

A number in a paper

Provenance answers one question: how was this result produced? A claim asks the other one, and until now Ligase could not answer it: what is behind the number I published?

The asymmetry this closes

ligase methods is unusually careful about what it does not know. It names a tool it has no reference for rather than leaving it out of the reference list, because “a tool silently missing from a reference list lets an incomplete methods section look complete” (D24). It names a sample identifier that is really a file name (D26). It names a step whose production it cannot account for, rather than printing a paragraph with a silent hole in it.

Three different promises, and all the same one: the record names what it cannot account for.

Now look at what it was silent about. The paper says variants were called above 1% allele frequency. The pipeline says min_freq = 0.05, because the threshold moved in June and the sentence did not. Both statements are in front of you, in the same terminal, and nothing connects them. The engine will tell you that fastp has no citation and say nothing at all about the number in your abstract — because nothing has ever told it what that number is.

A claim is how it is told.

What a claim is

A sentence a paper states, and the step that produced it:

pipe = lg.Pipeline( samples.map(lambda s: resistance(call(align(s, hxb2), hxb2))), claims=[ lg.Claim( "variants were called above 1% allele frequency", step="call", parameter="min_freq", value=0.01, ), ], )

The sentence is yours and is never checked. Everything else is: that call is a step this pipeline produces, that min_freq is a parameter it takes, and that the run held it at 0.01. What comes back is the digest of what the step produced, and the date that computation happened:

writing up 1 step in pipeline.py
 
Analyses were run with Ligase 0.4.0+14.g6c6a8b4 on linux-x86_64.
 
`call` ran on one input, as `cat {ref} > {out} && echo {min_freq} >
/dev/null`, with min_freq = 0.01, against the HXB2 reference (K03455.1, 16
bp, MD5 56b74a652b3ed2f610263b8bb423167c).
 
References
 
Njeri K. Ligase: dimension-checked, reproducible computational biology
pipelines (version 0.4.0+14.g6c6a8b4).
https://github.com/Neoforge-Labs/ligase
 
Claims
 
"variants were called above 1% allele frequency" rests on `call`, where
min_freq = 0.01 as this claim states, canonical 4a366b6cfeb0, recorded 3
November 2026 on linux-x86_64.

Why the sentence and the value are two fields

A paper writes 1%. The pipeline holds 0.01. Comparing the sentence against the parameter would be a guess about arithmetic in English, and a mechanism built on that guess would be wrong in both directions — calling correct papers wrong and, worse, occasionally calling a wrong one right.

So they are separate and each does one job. text is what you grep the write-up for with the manuscript open beside it. value is what the engine compares, written the way the pipeline holds it.

The three verdicts

Bound. Every check passed. The number rests on a computation this write-up accounts for, named by key.

Contradicted. Something stands behind the number and disagrees with it. This is the drift above, caught:

writing up 1 step in pipeline.py
 
Analyses were run with Ligase 0.4.0+14.g6c6a8b4 on linux-x86_64.
 
`call` ran on one input, as `cat {ref} > {out} && echo {min_freq} >
/dev/null`, with min_freq = 0.05, against the HXB2 reference (K03455.1, 16
bp, MD5 56b74a652b3ed2f610263b8bb423167c).
 
References
 
Njeri K. Ligase: dimension-checked, reproducible computational biology
pipelines (version 0.4.0+14.g6c6a8b4).
https://github.com/Neoforge-Labs/ligase
 
Claims
 
"variants were called above 1% allele frequency" states min_freq = 0.01, and
`call` ran with min_freq = 0.05. One of the two is wrong, and this
write-up cannot tell you which.

Both numbers are stated, so the fix needs no investigation — and the write-up does not guess which of the two is wrong, because it cannot know whether the paper or the pipeline is the thing that should move.

Unbacked. Nothing in the run stands behind it at all:

writing up 1 step in pipeline.py
 
Analyses were run with Ligase 0.4.0+14.g6c6a8b4 on linux-x86_64.
 
`call` ran on one input, as `cat {ref} > {out} && echo {min_freq} >
/dev/null`, with min_freq = 0.01, against the HXB2 reference (K03455.1, 16
bp, MD5 56b74a652b3ed2f610263b8bb423167c).
 
References
 
Njeri K. Ligase: dimension-checked, reproducible computational biology
pipelines (version 0.4.0+14.g6c6a8b4).
https://github.com/Neoforge-Labs/ligase
 
Claims
 
"three resistance mutations were found" has nothing behind it: nothing in
this pipeline produces `resistance`. It produces `call`. Name the step
that produces this number, or drop the claim.

There are five ways to land here and the complaint distinguishes them, because a renamed step, a dropped sample, a renamed parameter and a swapped tool are four different one-line fixes. A claim whose step is a gap is the fifth: the step exists, its record does not, and a claim resting on a step nothing is known about rests on nothing.

Why a declaration, and not a guess

The engine does not open your VCF and count its rows. It could do that for the formats it knows and would have to skip the rest, which is the lenient-canonicaliser failure in a new costume: a mechanism that verified some numbers and silently waved the others through.

What it does instead is narrower and honest. A claim with a value is compared. A claim without one is bound — to a digest, a key and a date, so a reviewer with the bundle can find the computation and rerun it. Neither is “the engine checked your arithmetic”, and the pages do not say it is.

The one thing a claim must never be is a sentence reprinted under a heading. M13 put it in one line: a declaration nothing can check is a comment. A Claims section that merely echoed your prose would make an unchecked paragraph look checked, which is the exact failure D24 refuses in the reference list — so every part of a claim except its text is checked against the run, and a claim that cannot survive that is named rather than printed.

What a claim is bound to

The computation, never the occasion. A claim binds to the step key, and a key is already the identity of a computation: the same code, the same parameters, the same inputs, the same tool versions. So a step served from the cache backs its claim completely, and the date you see is when that computation happened — which may be months before the run that wrote the paper up.

This is deliberate and it is D45. The alternative would make ligase methods --force the only honest way to write up a paper, which is the opposite of what a content-addressed cache is for.

What a claim is bound to and what it is cited by are then two different things, and conflating them cost a consumer a test. The key is the identity of a computation, so it moves when the computation’s code, parameters, inputs or tool versions move — and cite= moves the key while the artifact stays byte-identical and nothing reruns. The digest of the output’s canonical form is what survives such an edit, and it is what audit and a bundle verify against, so that digest is the identifier a write-up prints and a paper quotes (D59), under the name that says which digest it is (D64).

The same decision has a second half: a claim is in no cache key. Editing a sentence in your manuscript reruns nothing. A mechanism whose cost was a re-alignment per draft would simply not be kept up to date, which is the worst available outcome for a feature whose entire job is to be current.

And a claim may name where its number lives

Everything above checks a claim against the record: the step, the sample, the parameter, the tool version. The number itself was the one part that could not be checked, because nothing told the engine where it was.

at=lg.cell(column="f1", row=0) tells it, and then ligase methods reads that cell and compares it against value= within a tolerance= the claim states. The verdict is verified where they agree and contradicted where they do not, and a contradicted claim exits non-zero — so CI fails when a manuscript’s number stops matching its artifact (D61).

Four things it will not do, and each is the same refusal in a different place. It will not go looking for a number in output it was not pointed at. It will not parse a format it was not told. It will not default the tolerance, because a claim to three decimals and a claim to machine precision are different claims. And it will not call a claim wrong because it could not find the cell — an unreadable location is reported as unbacked, which is “I could not check”, not “you are wrong”.

Where to go next

Binding a number you published is the task, with a runnable example. The number in my paper is not the number my pipeline produces is the failure, by symptom. The internals say how the checks are made and what they deliberately do not do.

Last updated on