Skip to content

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

reaches(
    manifest: ModuleManifest | Mapping[str, Any],
) -> bool | None

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
def reaches(self, manifest: ModuleManifest | Mapping[str, Any]) -> bool | None:
    """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.
    """
    if self.requires is None:
        return None
    return all(manifest_carries(manifest, path) for path in self.requires)

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

compiled_under: str | None

None when the manifest stamped no compiler version (S88): the interval has no lower bound.

manifest_fields instance-attribute

manifest_fields: tuple[str, ...]

Fields that moved inside the asked interval. An overshooting link's are withheld here.

declared instance-attribute

declared: tuple[DeclaredChange, ...]

Declarations attributable to the asked interval. An overshooting link's are withheld here.

covered instance-attribute

covered: tuple[str, ...]

The releases whose records were folded in, newest first. Empty for a self-interval.

complete instance-attribute

complete: bool

Whether the chain covered (compiled_under, current] exactly, with no gap and no overshoot.

span instance-attribute

span: tuple[str | None, str]

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

out_of_span_manifest_fields: tuple[str, ...] = ()

Fields a link covering a WIDER span reports, which may have moved outside the asked one.

out_of_span_declared class-attribute instance-attribute

out_of_span_declared: tuple[DeclaredChange, ...] = ()

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

output_differs: bool | None

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

corrections: tuple[DeclaredChange, ...]

The declared changes where what we published was wrong, not merely absent.

declared_for

declared_for(
    manifest: ModuleManifest | Mapping[str, Any],
) -> tuple[DeclaredChange, ...]

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
def declared_for(self, manifest: ModuleManifest | Mapping[str, Any]) -> tuple[DeclaredChange, ...]:
    """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.
    """
    return tuple(change for change in self.declared if change.reaches(manifest) is not False)

RosterEntry dataclass

RosterEntry(
    field: str, recompute: str, condition: str | None
)

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.

field instance-attribute

field: str

The dotted manifest path.

recompute instance-attribute

recompute: str

How, in terms of the compiler's public surface.

condition instance-attribute

condition: str | None

None when the equality is unconditional; otherwise the case where it legitimately fails.

manifest_carries

manifest_carries(
    manifest: ModuleManifest | Mapping[str, Any], path: str
) -> bool

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
def manifest_carries(manifest: ModuleManifest | Mapping[str, Any], path: str) -> bool:
    """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.
    """
    cursor: Any = manifest
    for segment in path.split("."):
        if isinstance(cursor, Mapping):
            cursor = cursor.get(segment, _MISSING)
        else:
            cursor = getattr(cursor, segment, _MISSING)
        if cursor is _MISSING or cursor is None:
            return False
    return True

release_version

release_version(stamp: str) -> str

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
def release_version(stamp: str) -> str:
    """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.
    """
    token = stamp.strip().rsplit(" ", 1)[-1]
    try:
        parse_version(token)
    except ValueError as exc:
        raise ValueError(
            f"a compiler version stamp must end in MAJOR.MINOR.PATCH "
            f"(`0.7.0` or `just-dna-compiler 0.7.0`), got: {stamp!r}"
        ) from exc
    return token

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 is False with complete=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 None on every axis nothing measured. Never an empty result, never False.
  • a backwards interval — an artifact compiled under something newer than current, which happens whenever a consumer downgrades — is None throughout. 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
def 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 is `False`
      with `complete=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 `None` on every axis nothing measured. Never an empty result,
      never `False`.
    * **a backwards interval** — an artifact compiled under something *newer* than `current`, which
      happens whenever a consumer downgrades — is `None` throughout. 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.
    """
    table = RELEASE_RECORDS if records is None else records
    current = release_version(current)
    if compiled_under is None or not compiled_under.strip():
        return RecompileAnswer(
            compiled_under=None,
            current=current,
            axes=_unknown_axes(),
            manifest_fields=(),
            declared=(),
            covered=(),
            complete=False,
            span=(None, current),
        )
    compiled_under = release_version(compiled_under)
    low = parse_version(compiled_under)
    high = parse_version(current)

    if low == high:
        return RecompileAnswer(
            compiled_under=compiled_under,
            current=current,
            axes=dict.fromkeys(VALID_RELEASE_OUTPUT_AXES, False),
            manifest_fields=(),
            declared=(),
            covered=(),
            complete=True,
            span=(compiled_under, current),
        )
    if low > high:
        return RecompileAnswer(
            compiled_under=compiled_under,
            current=current,
            axes=_unknown_axes(),
            manifest_fields=(),
            declared=(),
            covered=(),
            complete=False,
            span=(current, compiled_under),
        )

    axes: dict[str, bool | None] = dict.fromkeys(VALID_RELEASE_OUTPUT_AXES, False)
    fields: set[str] = set()
    outside_fields: set[str] = set()
    declared: list[DeclaredChange] = []
    outside_declared: list[DeclaredChange] = []
    covered: list[str] = []
    cursor = high
    complete = True
    reached = high
    # Walk down the `previous` chain. Each hop is one release's measurement; a missing hop is a gap
    # and folds in as all-unknown rather than being skipped, which is the difference between "we
    # measured nothing there" and "nothing happened there".
    while cursor > low:
        record = table.get(str(cursor))
        if record is None:
            axes = {axis: _kleene_or(axes[axis], None) for axis in axes}
            complete = False
            break
        step = parse_version(record.previous)
        if step >= cursor:
            raise ValueError(
                f"release record {record.version} points at {record.previous}, which is not below "
                "it — the chain would not terminate"
            )
        # A link reaching below the asked bound answers a wider question. Its axes are blunted, and
        # its detail goes to the out-of-span tuples for the same reason — reporting a correction
        # under `corrections` while the axis beside it says *cannot say* would be one object
        # contradicting itself, and `corrections` is the field a registry spends a version number on.
        overshoots = step < low
        contribution = _blunt_to_unknown(record.axes) if overshoots else record.axes
        axes = {axis: _kleene_or(axes[axis], contribution.get(axis)) for axis in axes}
        if overshoots:
            outside_fields.update(record.manifest_fields)
            outside_declared.extend(record.declared)
            complete = False
        else:
            if record.axes.get("manifest_fields") is not False:
                fields.update(record.manifest_fields)
            declared.extend(record.declared)
        covered.append(record.version)
        reached = step
        cursor = step

    return RecompileAnswer(
        compiled_under=compiled_under,
        current=current,
        axes=axes,
        manifest_fields=tuple(sorted(fields)),
        declared=tuple(declared),
        covered=tuple(covered),
        complete=complete,
        span=(str(min(reached, low)), current),
        out_of_span_manifest_fields=tuple(sorted(outside_fields)),
        out_of_span_declared=tuple(outside_declared),
    )