Skip to content

Reduce model invalidation to a single version: the pvlearn release - #19

Merged
DerOetzi merged 4 commits into
mainfrom
refactor/single-model-contract-version
Aug 11, 2026
Merged

DerOetzi merged 4 commits into
mainfrom
refactor/single-model-contract-version

Conversation

@DerOetzi

Copy link
Copy Markdown
Contributor

Summary

The sidecar compared three version fields — feature_schema_version, pipeline_version, sklearn_version — while pvlearn_version was recorded but deliberately excluded. That split is both redundant and incomplete.

Redundant: runtime dependencies are pinned with ==, so neither a dependency, nor the schema, nor the pipeline can change without a pvlearn release. Every event the three fields detect is already a new release.

Incomplete, which matters more: the three cover scikit-learn but not numpy or joblib, and the persisted artefact depends on those too — joblib's pickle path is numpy-version-sensitive, visible today as a DeprecationWarning from joblib/numpy_pickle.py. A model pickled under one numpy and unpickled under another is exactly the silent-failure case the sidecar exists to prevent.

The claim that numpy/pandas/scipy do not affect reproduction turned out to rest on a single manual run recorded only as prose in a pyproject.toml comment. No CI job varies a dependency version — both jobs differ only in Python version, and pinned deps make them install identical numpy/pandas/scipy/scikit-learn — so the claim cannot fail if it stops being true. The comment is narrowed to what is actually tested.

Changes

  • ModelMetadata compares one version: the release segment of pvlearn_version. feature_schema_version, pipeline_version, sklearn_version, FEATURE_SCHEMA_VERSION, PIPELINE_VERSION and sklearn_minor_version() are removed.
  • Only the release segment takes part (0.3.0 out of 0.3.0.post2+gd22c402c0.d20260807): setuptools_scm uses local_scheme = "node-and-date", so comparing the full string would invalidate every locally trained model on every commit and again on every new day.
  • Reasoning and alternatives in ADR 0003; ADR 0002's pipeline_version mechanism is marked superseded (its selection decision stands).

A 0.3.0 sidecar still parses — pydantic ignores unknown fields — and is then rejected on its release with a precise reason rather than as unreadable metadata. Covered by a test.

Breaking change: sidecar schema changes and every persisted model is invalidated. Release as 0.4.0.

Test plan

  • pytest — 231 passed
  • ruff check / ruff format --check — clean
  • pyright — 0 errors

DerOetzi and others added 4 commits August 10, 2026 23:43
The sidecar compared three version fields — feature_schema_version,
pipeline_version and sklearn_version — while pvlearn_version was recorded
but excluded. That split was both redundant and incomplete.

Redundant: runtime dependencies are pinned with ==, so neither a
dependency, nor the schema, nor the pipeline can change without a pvlearn
release. Every event the three fields detect is already a new release.

Incomplete, which matters more: the three cover scikit-learn but not
numpy or joblib, and the persisted artefact depends on those too —
joblib's pickle path is numpy-version-sensitive, visible today as a
DeprecationWarning from joblib/numpy_pickle.py. The claim that
numpy/pandas/scipy do not affect reproduction turned out to rest on a
single manual run recorded only as prose in a comment; no CI job varies a
dependency version, so it cannot fail if it stops being true. The
pyproject comment is narrowed to what is actually tested.

Only the release segment is compared, so a model survives the dev builds
between two releases: setuptools_scm uses local_scheme = "node-and-date",
and comparing the full string would invalidate every locally trained
model on every commit and again on every new day.

Reasoning and the alternatives in docs/adr/0003.

Signed-off-by: Johannes Ott <deroetzi@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Johannes Ott <mail@johannes-ott.net>
test_a_sidecar_from_before_adr_0003_is_a_mismatch_not_unreadable built its
sidecar with pvlearn_version="0.3.0" and asserted a mismatch. That holds
only while the release under test is something other than 0.3.0: it passed
locally against a stale editable install (0.2.0.post2) and failed in CI,
where setuptools_scm resolves the v0.3.0 tag and the two sides agree.

0.0.1 is below every published release, so the assertion no longer depends
on which version the test run was built from and needs no upkeep per
release. The sibling test derives its version from release_version() and
already moved along on its own.

Signed-off-by: Johannes Ott <deroetzi@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Johannes Ott <mail@johannes-ott.net>
The comment calling scikit-learn's pin load-bearing claimed the frozen
Phase 0 baseline "is only reproducible against 1.9.0". Same provenance as
the numpy/pandas/scipy claim already narrowed in this branch -- prose in
commit ad94aac, nothing executable behind it -- and this one is also
contradicted by the project's own Phase 1a measurement, recorded in
tests/test_extraction_regression.py: the baseline does not reproduce
bit-for-bit across CPUs even with every version held fixed, with CI landing
5.26% off the MAE. The binding variable that was actually measured is the
machine, not scikit-learn.

test_records_the_versions_it_depends_on looks like it guards the claim but
asserts only that the frozen sidecar records 1.9.0; it would pass unchanged
if a newer scikit-learn reproduced the baseline perfectly. The real guard is
the tolerance comparison in test_extraction_regression.py, which does run in
CI -- the slow marker is declared but nothing deselects it.

Also drops the references to chapters of the deleted Umsetzungsplan that the
previous cleanup missed by grepping for the document name rather than
"chapter". One of them, in freeze_baseline_forecast.py, pointed at the
chapter requiring sklearn_version in the model metadata, which ADR 0003 has
since removed.

Signed-off-by: Johannes Ott <deroetzi@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Johannes Ott <mail@johannes-ott.net>
Records the convention that produced the previous two commits: no ADR, code
comment or docstring may point at an implementation plan, roadmap or phase
document. Those are written for one migration and then deleted, and every
reference to one rots into a pointer at nothing -- deleting
pvlearn-umsetzungsplan.md left nine dangling citations, one of them requiring
a metadata field ADR 0003 had already removed.

An ADR has to stand alone, so where a plan carried reasoning the record needs,
the passage is copied or summarised into the ADR instead. ADR 0001's last
citation is replaced that way: "the canonical, provider-independent schema in
chapter 3.1" now states what that schema actually guarantees and points at
pvlearn/schema.py, which moves with the code and breaks loudly when it stops
existing.

Naming a phase as the provenance of a measurement stays allowed -- it dates a
result rather than sending the reader somewhere. The test is whether removing
the document would leave the sentence broken.

ADR 0003 also picks up the withdrawn scikit-learn reproducibility claim, so
the rule's pointer to it covers both cases rather than one.

Signed-off-by: Johannes Ott <deroetzi@gmail.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Johannes Ott <mail@johannes-ott.net>
@DerOetzi
DerOetzi merged commit 6d8e0b8 into main Aug 11, 2026
5 checks passed
@DerOetzi
DerOetzi deleted the refactor/single-model-contract-version branch August 11, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant