just_dna_format.release_records¶
just_dna_format.release_records ¶
What a release changed about compiled output — the channel Principle 3 names (RM126).
The charter, as amended on 2026-08-21, lets a corrected derivation ship in any release but never silently: each release declares its corrections, readable offline and without recompiling. Until this module existed the charter named a surface that was not there.
The question it answers, and the two it does not. A consumer holding a stored artifact can
already ask is the stored input still legal? (validate_spec says ok) and was this compiled by
a contract-incompatible compiler? (compare versions; a patch is compatible). Neither is would
recompiling this artifact produce different output than the stored one? — and that question is not
hypothetical. All sixteen reference_examples/ compiled under 0.6.1 and again under 0.6.6, from
byte-identical spec inputs across an interval that is entirely patch releases, changed a published
manifest field on every single one; ten moved artifact.digest; none moved content_signature. Six
changed a published, indexed manifest field with both hashes byte-identical, which is exactly
the shape a digest comparison, a signature comparison and a revalidate all correctly report as no
change.
This is a fact channel, never a verdict. There is deliberately no should_rebuild: the same fact
costs a registry an immutable PATCH and a local cache a free rebuild, so the decision belongs to the
consumer and only the fact is ours. needs_recompile is named for the question, and it answers with
the per-axis breakdown plus the declared correction/addition split — the half no diff can compute,
because only the person who fixed the bug knows whether the stored value was wrong or merely
absent.
Why this module and not _ALL_MODELS. Every member of reference._ALL_MODELS describes module
content — a row an author writes or a compiler emits into a spec directory or an artifact. A release
record describes this package, so admitting it would make authoring_reference() — the drift-proof
description a consumer renders instead of a hand-kept spec dump — advertise a table nobody
authors. The compensating control is the pair of equality guards in
schema/tests/test_release_records.py, which walk the same registries the _ALL_MODELS guards walk;
and both vocabularies live in vocab.py, so test_vocab_separator.py's discovery-by-inspection
covers them exactly as it covers every other closed set. Do not re-file the exclusion as an
oversight.
Scope, stated because a silence would be read as a claim. v1 measures compiler-derived
outputs — the manifest, the parquet schemas and the parquet bytes. Enricher-side outputs
(resolution.csv and the fact sidecars) are unmeasured, which is not the same claim as
unchanged.
DeclaredChange ¶
Bases: BaseModel
One thing a release changed about compiled output, and whether what we published was WRONG.
This is the half of the record a measurement cannot produce. A differ sees stats.genes move
from [] to ["CYP2C19"] and studies.parquet gain a curator column, and the two look
identical to it: a field changed. They are not the same fact. The first was wrong — the
module always named that gene on 1,332 rows and the published value said otherwise, so every
artifact holding it is serving a value we no longer stand behind. The second was merely
absent — the column did not exist, and no earlier artifact could have carried it.
A consumer routes those differently, and only this repo holds the knowledge that separates them.
reaches ¶
Whether this change can reach the module manifest describes — three answers.
False is the certain one and the only one a consumer acts on: the manifest lacks a path
the change requires, so the value this change corrected or added was never in that module.
True means not excluded by what the record states — requires is a necessary condition,
not the exact reach. RM110 is the worked case: it reaches modules whose gene_metrics came
from the snapshot route, and ("gene_metrics",) over-approximates that in the safe
direction. None means the record does not state the reach at all (RM121's stats.genes
correction reached modules whose lead table named no gene, which presence cannot spell), and
a consumer keeps the change — the house algebra, where unknown is never False.
The predicate lives here rather than in a consumer because the grammar is ours (S90): a
registry reading target's first segment would break the day a target is spelled some
third way, and would be re-deriving the rule from a field's spelling.
Source code in schema/src/just_dna_format/release_records.py
ReleaseRecord ¶
Bases: BaseModel
What one release changed about compiled output, over the interval (previous, version].
Keyed on an interval, not on a field, and that is load-bearing rather than incidental. The question a consumer has is always compiled under X, installed Y, and the interval shape is what gives convergence for free: the interval from a version to itself is empty, so an automated sweep acting on this cannot mint a fresh PATCH every run, forever. A field-keyed table — or a "latest known defect" list — would fire for a version compiled by the exact compiler now installed and would not have the property. It also bounds a false positive to one wasted version number per module, ever, which is what makes acting on it unattended defensible at all.
axes is tri-state per axis and every member is present. True moved, False measured and
did not move, None not measured on this interval — which is not False. A release that
adds an axis cannot retroactively measure the ones before it, and a record that quietly reported
False there would be worse than no record, because a consumer would stop recompiling on the
strength of a silence.
A release where nothing moved records a measured zero with its evidence, never silence. An
absent record and an all-False record answer different questions.
unmeasured is a denominator, not an exemption (RM139). axes is a claim about the modules
the sweep could compare, and a minor that adds an authored column and exercises it in the corpus
leaves at least one module the previous release cannot compile at all — so there is no before
state to compare it against, and no measurement to declare. The 0.7.0 cut stated that fact in
evidence prose, which the gate cannot read, so the tag had to be waved through by hand.
Naming the modules in a field the gate checks for equality is the opposite of an escape
hatch: it cannot cover a module that compiled on both sides, it cannot cover one this release
broke, and listing a module the sweep did measure is itself reported.
RecompileAnswer
dataclass
¶
RecompileAnswer(
compiled_under: str | None,
current: str,
axes: dict[str, bool | None],
manifest_fields: tuple[str, ...],
declared: tuple[DeclaredChange, ...],
covered: tuple[str, ...],
complete: bool,
span: tuple[str | None, str],
out_of_span_manifest_fields: tuple[str, ...] = (),
out_of_span_declared: tuple[DeclaredChange, ...] = (),
)
The facts about an interval. Not a verdict — see this module's docstring.
axes is tri-state per axis under Kleene semantics: a single measured True beats any number of
Nones (something moved, whatever else is unknown), while a False survives only when every
release in the interval was measured and none of them moved.
compiled_under
instance-attribute
¶
None when the manifest stamped no compiler version (S88): the interval has no lower bound.
manifest_fields
instance-attribute
¶
Fields that moved inside the asked interval. An overshooting link's are withheld here.
declared
instance-attribute
¶
Declarations attributable to the asked interval. An overshooting link's are withheld here.
covered
instance-attribute
¶
The releases whose records were folded in, newest first. Empty for a self-interval.
complete
instance-attribute
¶
Whether the chain covered (compiled_under, current] exactly, with no gap and no overshoot.
span
instance-attribute
¶
The interval the answer actually describes, which an overshooting link makes wider than asked.
(None, current) for an unstamped compiled_under — an interval with no lower bound.
out_of_span_manifest_fields
class-attribute
instance-attribute
¶
Fields a link covering a WIDER span reports, which may have moved outside the asked one.
out_of_span_declared
class-attribute
instance-attribute
¶
The same for declarations, and the reason both of these exist rather than being folded in.
_blunt_to_unknown already refuses to narrow a True axis, so an overshooting link answers
cannot say — and folding that link's manifest_fields and declared into the main tuples
anyway would contradict the axis in the same object. A registry acting on corrections burns an
immutable PATCH, and this moved somewhere in a wider interval is not a fact about the artifact
in front of them. Withheld here rather than dropped: they are still the best evidence available,
and a caller who wants them can read them knowing what they are.
output_differs
property
¶
Kleene OR over RECOMPILE_DRIVING_AXES — warnings is excluded by construction.
None means cannot say, and a consumer must read it as such: the whole point of the axis
being tri-state is that a silence is not a licence to stop recompiling.
corrections
property
¶
The declared changes where what we published was wrong, not merely absent.
declared_for ¶
The declarations that can reach the module manifest describes (S90, RM201).
Drops a change only where reaches answers False — a stated requirement the manifest
does not meet. A change whose reach is unstated (None) is kept, not dropped: the
registry that filters corrections by this spends an immutable PATCH per module it keeps,
and the cost of keeping one it did not need is one version number, where the cost of
dropping one it needed is a module serving a value we have said is wrong.
Source code in schema/src/just_dna_format/release_records.py
RosterEntry
dataclass
¶
One manifest field a consumer can recompute from the authored rows, and when they cannot.
The roster is the half of RM126 that shrinks it. For a field that is a pure function of the authored rows, a consumer holding the stored spec can compute the current answer directly — no enrichment, no parquet, no network, and no need to consult the record table at all. What the interval table then has to cover is only what a consumer cannot recompute.
manifest_carries ¶
Whether a manifest carries path non-null — the one predicate DeclaredChange.requires uses.
Walks the dotted path block by block, over the pydantic model a consumer gets from
read_manifest or over the plain mapping they get from json.load, so the two spellings of a
manifest answer alike. A block that is None (an optional block the module never carried) fails
the walk at that segment; a leaf that is None is absent too, because the record's manifest_fields
grammar names published values and an unset optional field publishes none.
Source code in schema/src/just_dna_format/release_records.py
release_version ¶
The bare MAJOR.MINOR.PATCH out of whatever a caller is holding.
manifest.compilation.compiler_version reads just-dna-compiler 0.6.1, package name included,
and that string is what a consumer has in their hand. Making them strip it before they can ask a
question is the kind of unstated convention that ends up implemented three different ways in
three different registries, so this accepts either spelling and returns the canonical one.
Raises on anything else — just-dna-compiler unknown included. A malformed version is a caller
bug, not an unknown fact: the tri-state in this module is about whether the table covers an
interval, and quietly answering unknown here would hide a typo behind the same silence a real
gap uses. An absent stamp is not a malformed one — needs_recompile answers unknown for
None/blank before it reaches here (S88), so this function only ever sees a stamp somebody wrote.
The refusal quotes the whole stamp, not the last token: just-dna-compiler 0.6.6
(marketplace-server) used to be refused as '(marketplace-server)', which names nothing the
caller wrote. The package name is deliberately not checked — one version across the workspace
is the rule, so just-dna-format 0.6.6 names the same release.
Source code in schema/src/just_dna_format/release_records.py
needs_recompile ¶
needs_recompile(
compiled_under: str | None,
current: str,
records: dict[str, ReleaseRecord] | None = None,
) -> RecompileAnswer
What changed about compiled output between the release an artifact was compiled under and now.
compiled_under is manifest.compilation.compiler_version; current is the release the caller
is holding. Both are explicit and neither is defaulted — this module lives in the format
tier, which cannot know which compiler is installed, and defaulting to its own version would
answer with the wrong package's number.
An absent stamp is the unknown arm, not a crash (S88). Compilation.compiler_version is
str | None, so a manifest that stamped nothing is well-formed and comes back through
read_manifest as None; a registry walking manifests it did not produce will be handed one.
None, "" and whitespace are one fact — nobody stamped — and answer alike: every axis None,
complete=False, compiled_under=None, span=(None, current). That is the purest unknown
provenance this table can be asked about, and the answer it already gives for a release it has
no record of. A stamp that is present and unreadable ("0.7", "v0.7.0",
"0.6.6+local", a trailing note in parentheses) is a different state and still raises: it was
asked and cannot be read, which is a caller's bug to fix, and the ValueError names the whole
stamp so they can. Absent, malformed, uncovered: three states, two answers, and the middle one
is the only refusal.
Composition is a union over the releases in (a, b], walked along each record's previous
link. Storage is therefore linear in releases rather than quadratic, and moved-and-moved-back
still counts as moved, which is the right reading for staleness: a consumer asking whether their
stored bytes match a recompile is not asking whether the difference is interesting.
Three answers that are not the same and must not collapse into one another:
- the self-interval —
compiled_under == current— is empty, so every axis isFalsewithcomplete=True, even for a version this table has never heard of. That is the convergence property S65 made a hard requirement, and it holds structurally rather than by a special case in a caller. - an uncovered interval answers
Noneon every axis nothing measured. Never an empty result, neverFalse. - a backwards interval — an artifact compiled under something newer than
current, which happens whenever a consumer downgrades — isNonethroughout. The union is not symmetric: a correction declared forward is not a correction backward, and this table records what a release did, not what undoing it would do.
Source code in schema/src/just_dna_format/release_records.py
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 | |