just_dna_format.verification¶
just_dna_format.verification ¶
The verification attestation (0.6, RM45) — binding, proof-of-work, and the table's fact hash.
verification.json is the seam that lets a compiled module say whether anything it asserts was
ever checked. The enricher writes it, the compiler reads it and either stamps manifest.verification
or drops the block with a warning. The models live in manifest beside ProvenanceDoc, which is the
same shape one release earlier; the behaviour lives here.
What the attestation is for, stated at its real size. It binds a set of check records to the
authored bytes they were computed over, and adds a proof-of-work so re-stamping is not free. That
defeats the accidental forgery: an attestation left behind after an edit, or copied from another
module, which is the failure that actually happens. It defeats nothing deliberate, and this module is
not built as though it should — a determined writer regenerates a nonce in under a second, exactly as
the enricher does. The real guarantee available in this format is manifest.signature, a detached
Ed25519 signature over artifact.digest made by a party a client pins.
The binding covers the AUTHORED bytes and no others, and the boundary is a decision rather than a
convenience. Every check in the vocabulary compares something a human wrote — a clin_sig, a ref,
an rsID, a PMID, a gene beside a locus — so the authored files are precisely what the claim is about.
The derived sidecars are deliberately outside it: resolution.csv carries a fetched_at per row, so
binding to its bytes would invalidate the attestation on every re-enrichment that changed nothing
anyone claimed. What the boundary costs is real and is stated rather than hidden: re-running the
enricher against a fresher ClinVar leaves the attestation matching, so a consumer reads which
release each check was put against from the record's own release field rather than inferring
currency from the binding.
Line endings are outside it too (RM82, 0.6). The binding reads as , so an editor that
normalizes newlines, or a Git checkout with core.autocrlf, no longer un-closes a module in which no
value moved. It stops there: a BOM, trailing whitespace and a missing final newline are all still
edits, because those are things a human typed rather than things a tool did on their behalf. The
transform lives in integrity.newline_normalized_file_entry, and it is used by this binding and by
nothing else — manifest.inputs[] and artifact.digest still follow every byte, because they answer
are these the exact bytes rather than is this the same module.
The closure (RM73, 0.6) rides this document, and that is the whole design. RM73 asked for a phase
boundary: an attestation that authoring is finished, which a later edit invalidates rather than
silently outliving. Everything that needs is already here — module_hash binds the authored bytes,
and the compiler recomputes it and drops the block on any mismatch — so the closure is one optional
block on VerificationDoc rather than a second sidecar with its own binding, its own staleness rule
and its own transport. It sits outside pow_digest's payload, so no document written before it
existed has been invalidated and closing one re-mines nothing.
The two answer different questions and must not be read as one: the records say these checks were put against these bytes, the closure says a human declared these bytes final. A module may legitimately carry either alone.
module_binding ¶
The hash an attestation is bound to: the authored input files, by name and content.
Deliberately integrity.artifact_digest rather than a second canonicalization of the same idea.
That function is a Merkle root over a (name, sha256, size) listing sorted by name, which is
exactly what is wanted here, and two hand-written canonical forms over one shape is how two
parties end up disagreeing about a digest. The caller decides which entries — see this module's
docstring for why that is the authored set, and just_dna_compiler.compiler.authored_input_entries
for the one function both tiers ask.
The caller also decides how the entries were read, and for the binding that is
integrity.newline_normalized_file_entry rather than file_entry (RM82): the digest and the
size are taken over bytes in which \r\n reads as \n, so an editor or a core.autocrlf
checkout cannot un-close a module it did not change. Passing raw entries here still works and
still hashes fine — it just answers the byte question instead, which is manifest.inputs[]'s job.
Source code in schema/src/just_dna_format/verification.py
verification_signature ¶
Fact-hash of the check records (VERIFICATION_FACT_FIELDS). See integrity.fact_signature.
Order-independent and normalized, like its four siblings, so two producers recording the same checks in a different order hash equal.
Source code in schema/src/just_dna_format/verification.py
pow_digest ¶
The raw digest a nonce is judged on: both hashes plus the nonce, in one fixed spelling.
Both halves are in it on purpose. Binding to module_hash alone would let the records be edited
freely under a nonce that still verifies; binding to signature alone would let a whole
attestation be lifted onto another module.
Source code in schema/src/just_dna_format/verification.py
leading_zero_bits ¶
How many leading zero bits digest has.
meets_difficulty ¶
Whether this nonce's digest carries at least difficulty leading zero bits.
Source code in schema/src/just_dna_format/verification.py
find_nonce ¶
The SMALLEST nonce, counting up from zero, that meets difficulty.
Smallest and not a random search, and the reason is determinism rather than elegance: a random
nonce gives the file different bytes on every run for identical content, which would make
verification.json the one derived sidecar that cannot be reproduced — and this tree pins
reproducibility with tests everywhere else. Counting up also makes the work honest: the expected
cost is 2^difficulty hashes whichever nonce happens to win.
Source code in schema/src/just_dna_format/verification.py
close ¶
close(
module_hash: str,
*,
closed_at: str,
closed_by: str | None = None,
private_key_pem: bytes | None = None,
) -> Closure
Build the closure that says authoring is finished over module_hash (RM73).
Signing is optional and the message signed is module_hash itself, so signing.sign_digest
serves unchanged — it takes whichever digest string it is handed, which is why Signature moved
up beside the models that use it rather than growing a second spelling here.
Nothing about this function decides when it is legitimate to call. That is the caller's job and
it is a real one: closing a document whose records were attested over other bytes would re-bind
them to these, which is why compile_module's side of this drops such records rather than
carrying them across.
Source code in schema/src/just_dna_format/verification.py
attest ¶
attest(
records: Sequence[VerificationRecord],
module_hash: str,
*,
producer: str | None = None,
produced_at: str | None = None,
difficulty: int | None = None,
closure: Closure | None = None,
) -> VerificationDoc
Build the document: hash the records, bind them to module_hash, and mine the nonce.
difficulty is a parameter rather than a hardcoded read of the constant so a test can exercise
the whole path in milliseconds; production callers take the default. A document written at a lower
difficulty than a reader's minimum is rejected by attestation_failure, so nothing is lost by
letting it move.
None rather than the constant as the default, and that is not a style preference: a default
argument is evaluated at import, so writing difficulty=VERIFICATION_DIFFICULTY_BITS in the
signature freezes the value and a test that lowers the constant still pays the full second — on
every call, through every caller that does not thread the parameter. Reading it in the body is
what makes the knob reachable at all.
Source code in schema/src/just_dna_format/verification.py
attestation_failure ¶
attestation_failure(
doc: VerificationDoc,
module_hash: str,
*,
difficulty: int | None = None,
) -> str | None
Why this attestation may not be published, or None when it holds.
A reason rather than a bool, because the three ways it fails want different sentences and a caller that can only say "invalid" sends an author looking in the wrong place: an edited module, a hand-edited record set, and a document written before the difficulty moved are three situations with three remedies.
Never raises and never repairs. The caller's answer to every one of these is the same — warn, and drop the block — so a compile still succeeds and the manifest simply says nothing, which is the correct reading of a record that no longer describes these bytes.
difficulty reads the constant in the body for the reason attest does: a default frozen at
import is a knob no test can turn.
Source code in schema/src/just_dna_format/verification.py
verification_block ¶
The manifest summary of a document whose attestation has already been confirmed.
Deliberately takes no module_hash and performs no check: a function that could either summarize
or refuse would let a caller skip attestation_failure and never notice. The order is fixed at
the one call site — confirm, then summarize.
Source code in schema/src/just_dna_format/verification.py
merge_records ¶
merge_records(
existing: Sequence[VerificationRecord],
fresh: Sequence[VerificationRecord],
*,
existing_still_binds: bool = True,
) -> list[VerificationRecord]
Fold a run's records into what the document already carried — newest wins, per check.
Replace rather than accumulate, because a record answers "what happened the last time this check
was put" and two answers to that are not two facts. A check absent from fresh keeps its earlier
record: a run that did not put a question has said nothing about it, and deleting the older answer
would turn "not asked this time" into "never asked", which is the exact collapse RM45 exists to
undo.
A skipped record does not replace a ran one (RM72), and that is the paragraph above applied
one step further out. A skip is a run that did not put the question — the same fact as an
absent check, spelled as a record instead of as a silence — so the argument that protects the
absent check protects this one, and unconditional newest-wins performed exactly the deletion the
paragraph refuses: check-acmg --offline after a real run, or check-identifiers --no-traits
after a full one, rewrote a true subjects=13 findings=0 verdict to subjects=0 findings=0
skipped=offline. Newest-wins still holds between two records of the same disposition — a fresh
ran replaces an older ran (that is the point of re-running), and a fresh skip replaces an
older skip (the newer reason is the current one).
existing_still_binds is the condition on that protection, and it is not a knob. An answer is
worth protecting because it is still an answer about this module; once the authored bytes have
moved it describes rows that no longer exist, and preserving it over a fresh honest "could not
ask" would publish a stale finding rather than a real one. That case is not hypothetical: it is
how literature behaves when the module's citations change, and a previous round fixed exactly
that defect by writing the skip. So the caller passes whether the document it read was computed
over the bytes it is now being rewritten over (VerificationDoc.module_hash vs the binding), and
with False this is plain newest-wins. The default is True because a caller holding no earlier
document has nothing that could fail the test.
The counter-argument to the protection is real and is answered rather than dismissed: a reader may legitimately want to know that today's run could not reach the source. That is a fact about the run, not about the check, and this is a per-check document — a run-level fact needs a run-level place, which is a separate question and is deliberately not opened here.
Source code in schema/src/just_dna_format/verification.py
read_verification ¶
Load and validate a verification.json. Raises on malformed content, like read_manifest.
Source code in schema/src/just_dna_format/verification.py
write_verification ¶
Write the document as indented JSON, with a trailing newline. Returns the path written.