Source code for phenotypic.sdk_._master_io
"""The one home of the v1/v2 master discrimination.
Spec §7.3 moved the metadata join out of the per-image embedded tables and
into finalization, which changes what ``deliverables/master_measurements.parquet``
contains:
* **v1** (pre-inversion) -- every measured row already carried its
publication-time user metadata, because the join happened per image.
* **v2** (post-inversion) -- the master is the un-joined archival set:
intrinsic identity plus measurements. The join lives in the
``deliverables/measurements.*`` mirror.
**Nothing stamps the file** (user ruling, 2026-09-06): the master already
self-describes, and a stamp would be a second on-disk home for a fact the
columns already state. The discrimination therefore lives here, in one
module, so its *retirement condition* has one home too -- spreading a
two-line column check across seven reader modules would give the condition
seven homes and, in practice, none.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable
if TYPE_CHECKING: # pragma: no cover - typing only
import polars as pl
__all__ = ["master_carries_user_metadata", "user_metadata_headers"]
[docs]
def user_metadata_headers(columns: Iterable[str]) -> tuple[str, ...]:
"""Return the headers that could only have come from ``--metadata``.
A master's *intrinsic* metadata is the identity the image carries about
itself: the ``IMAGE``-owned per-image provenance block, plus
``EXPERIMENT.DATASET``, which the CLI inserts from the dataset directory
name rather than from any CSV. Everything else in the metadata namespace
was joined in from the run's ``metadata.csv``.
**Ownership, not the prefix.** ``Metadata_Strain`` is a real schema
member (``GENETIC.STRAIN``), so "carries a ``Metadata_*`` column" does
*not* separate the two shapes -- a v2 master carries
``Metadata_Dataset`` and ``Metadata_ImageName`` and would be misread as
v1 by that test. Namespace detection goes through
:func:`is_metadata_header` and routing through
:func:`metadata_owner_for_header`, never through prefix parsing.
Args:
columns: Column names of a master frame.
Returns:
The user-metadata headers present, in the order given.
"""
from phenotypic.schema import EXPERIMENT, IMAGE
from ._metadata_helpers import is_metadata_header, metadata_owner_for_header
intrinsic = {str(EXPERIMENT.DATASET)}
return tuple(
column
for column in columns
if is_metadata_header(column)
and metadata_owner_for_header(column) is not IMAGE
and column not in intrinsic
)
# V1/V2 MASTER DISCRIMINATION -- DELETE WHEN: no run predating the §7.3
# inversion is still readable, i.e. every master in the wild was written by
# finalize_run's post-inversion path. A v1 master carries user-metadata
# columns because the join happened per-image; a v2 master does not, because
# the join moved to finalization. Nothing else distinguishes them, and
# nothing stamps them. When that condition holds, this function and every
# branch on it are dead code and should go together.
[docs]
def master_carries_user_metadata(frame: "pl.DataFrame") -> bool:
"""Return whether this master predates the §7.3 inversion.
The one genuinely dangerous failure mode in §7 is a reader that filters
or groups a master on a user-metadata column: against a v2 master that
returns **empty** rather than raising. This predicate is what such a
reader branches on.
A v1 run that was given no ``--metadata`` is indistinguishable from a v2
run by this test, and that is expected to be harmless -- neither has
anything to join. ``test_a_v1_metadata_free_master_is_indistinguishable_from_v2``
is the designated falsifier for that expectation.
Args:
frame: A master measurements frame.
Returns:
``True`` when the frame carries at least one user-metadata column.
"""
return bool(user_metadata_headers(frame.columns))