Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/run_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,29 @@ jobs:
# and CI still reports green. Guarded by
# tests/test_falsification_extras_actually_installed.py.
- name: Verify optional extras are installed
run: poetry run python -c "import views_frames"
run: poetry run python -c "import views_frames, importlib.metadata as m; v = m.version('views-frames'); print('resolved views-frames', v); assert v.split('.')[0] != '1', 'the resolver picked the floor major; the 2.x end of the range would go untested'"

- name: Run tests
run: |
set -e
poetry run pytest tests/

# The `frames` extra admits two views-frames majors (>=1.10.2,<3; #91). The step
# above runs on whatever the resolver picks (asserted to be the 2.x end in the
# verify step), so the FLOOR would be untested. This reads the floor from
# pyproject.toml (one source, no literal to drift), pins it, and runs the whole
# suite on it. Nothing after this step touches Python, so nothing is restored;
# a later Python-touching step must reinstall the extras first (the held workflow
# text forces that review). Guarded by the held workflow text.
- name: Run the suite on the views-frames floor
run: |
set -e
FLOOR=$(poetry run python -c "import tomllib, re; d = tomllib.load(open('pyproject.toml', 'rb')); p = d.get('project', {}); e = [x for x in p.get('dependencies', []) + sum(p.get('optional-dependencies', {}).values(), []) if x.startswith('views-frames')]; spec = re.sub(r'^views-frames(\[[^\]]*\])?', '', e[0]) if e else d['tool']['poetry']['dependencies']['views-frames']['version']; print([s for s in spec.replace(' ', '').split(',') if s.startswith('>=')][0][2:])")
echo "views-frames floor from pyproject.toml: $FLOOR"
poetry run pip install --quiet "views-frames==$FLOOR"
poetry run python -c "import importlib.metadata as m, sys; assert m.version('views-frames') == sys.argv[1], m.version('views-frames')" "$FLOOR"
poetry run pytest tests/ -q

# Structural documentation checks (CIC references, cross-ADR integrity, unfilled
# placeholders). Complements tests/test_documentation_contracts.py, which asserts
# that documented claims match code. Neither ran in CI before 2026-08-02, which is
Expand Down
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,23 @@ provided they were announced here.

## [Unreleased]

_Nothing yet._
### Changed

- **The `frames` extra no longer excludes views-frames 2.x** (`>=1.10.2,<3`, was `<2`).
Reported by views-pipeline-core's 3.3.0 range review (#91): views-frames 2.0.0 has been
published since 2026-08-18 and this cap, carried into any environment that requests the
extra, excluded it. What this changes is this package's own contract only — a consumer
whose own bounds exclude views-frames 2.x must widen those too before it resolves.
Measured before widening: the full suite (at 2.0.0's count) on views-frames 1.10.2,
1.11.0 and 2.0.0, and a `MetricFrame` saved under either major loads under the other
with identical rows and values. Safe because this package's only runtime import from
views-frames is the `FrameMetadata` dataclass (its tests also call the envelope contract
`views_frames.conformance.assert_frame_envelope`, whose body is unchanged between the
majors); views-frames 2.0.0's ADR-028 changes (index type, read-only `.values`,
`map_estimate` refusals) never reach it. Guards now pin the declared range and that
import surface, and CI runs the whole suite on the range's floor as well as on the
resolved version, asserting the resolved one is the 2.x end. Nothing changes in what
this package emits; MINOR under ADR-022 §5.

---

Expand Down
1 change: 1 addition & 0 deletions documentation/ADRs/022_evolution_and_stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ This policy is checked at two points:
- [ ] Does the version bump match the change class (rule 5)?
- [ ] Do the release notes list every breaking change with its migration?
- [ ] Does `MetricFrame`'s format or axis vocabulary change? If so, has it been agreed with views-reporting and views-pipeline-core?
- [ ] Has any dependency this package caps (`numpy`, `scipy`, `views-frames`) published a release outside the declared range since the last release? If so, is the cap measured and deliberate, or stale? *(Added 2026-09-19, #91: the `frames` extra's `<2` cap outlived views-frames 2.0.0 by a month and was noticed by a consumer, not by a release of ours.)*

## Consequences

Expand Down
2 changes: 1 addition & 1 deletion documentation/CICs/MetricFrame.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ It is a string-keyed value object — **not** a spatiotemporal `(time, unit)` fr
- **`identifiers`**: dict containing exactly the keys in `AXES`, each a 1-D length-`N` array of strings.
- **`metadata`**: optional `MetricFrameMetadata`; defaults to an empty one.
- **Assumes** the caller has already decided what belongs in the frame. Construction is a structural gate, not a semantic one.
- **Requires** the optional `views-frames` dependency (`pip install views-evaluation[frames]`). The module is import-gated in `views_evaluation/__init__.py` on `find_spec`, so the core API stays importable without it (ADR-011 minimal core).
- **Requires** the optional `views-frames` dependency (`pip install views-evaluation[frames]`), any version in `>=1.10.2,<3` — both majors measured 2026-09-19 (#91). The module is import-gated in `views_evaluation/__init__.py` on `find_spec`, so the core API stays importable without it (ADR-011 minimal core). This package's only runtime import from views-frames is the `FrameMetadata` dataclass (six keyword fields, `to_dict`, `from_dict`); its second dependency is the envelope contract `views_frames.conformance.assert_frame_envelope`, called by the tests, whose body is unchanged between the majors although views-frames moved its `CONFORMANCE_FLOOR` to 2.0.0. `tests/test_falsification_extras_actually_installed.py::TestViewsFramesRange` pins the range and the one-name import surface, `tests/test_metric_frame.py::TestViewsFramesSurface` pins `FrameMetadata`'s behaviour on the installed major, and CI runs the whole suite on the range's floor as well as on the resolved version.

---

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ readme = "README.md"
python = ">=3.11,<3.15"
numpy = "^1.26.4"
scipy = "^1.11" # Level-0 kernels (EMD, Pearson); ADR-011 permits numpy and scipy
views-frames = {version = ">=1.10.2,<2", optional = true}
views-frames = {version = ">=1.10.2,<3", optional = true} # both majors measured 2026-09-19 (#91); the only name used is FrameMetadata

[tool.poetry.extras]
frames = ["views-frames"]
Expand Down
22 changes: 19 additions & 3 deletions reports/technical_risk_register.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Technical Risk Register — views-evaluation

**Last updated:** 2026-09-18
**Last updated:** 2026-09-19
**Total open concerns:** 14
**Governing ADR:** ADR-023
**Citation convention:** `Location` fields name files and symbols (functions, classes, sections), not line numbers — line numbers drift as soon as anything is inserted above them.
Expand All @@ -14,6 +14,21 @@
> review then found four gaps in the rewrite (linked-worktree `commondir`, locale-dependent
> reads, unchecked ref contents, an unpinned name gate) — all fixed and tested before merge. 18 → 17.

> **2026-09-19 — #91: the `frames` extra widened to views-frames `<3`.** views-pipeline-core's 3.3.0
> range review found that our `<2` cap excluded views-frames 2.0.0 (published 2026-08-18) from any
> environment requesting the extra. (The review of this change found that the report's premise — that
> our cap was the *only* thing holding reporting environments on 1.x — was false: that consumer also
> declares its own `<2.0.0`; so the note below states only this repo's contract.) Measured on
> 1.10.2, 1.11.0 and 2.0.0 (full suite green on each; cross-major save/load identical) and widened;
> guards pin the range, the one-name import surface and `FrameMetadata`'s behaviour; CI runs the whole
> suite on the floor and asserts the resolved version is the 2.x end. Recorded under C-38 as its mirror
> case, C-38's trigger widened, C-41 updated, and ADR-022 §7 gained a checklist line so a capped
> dependency's new major is noticed at the next release rather than by a consumer. The guard audit
> found survivors on four of the new guards (a PEP 621 table read second, `seed=0` dropped by a falsy
> `to_dict`, an environment marker on the entry, `getattr`/re-aliasing on a views_frames alias, and
> floor-step checks that a `-k` or `|| true` slipped past), all closed and re-verified red. Ships as
> 2.1.0 (MINOR). Count unchanged.

> **2026-09-18 — release 2.0.0 (S8, #72).** The MAJOR that ends the pandas-free epic: `to_dataframe()`
> and the `dataframe` extra gone one release cycle after 1.1.0's `DeprecationWarning` (ADR-022 §2).
> §3.2 notifications posted before the tag and cited by comment ID in the checklist (views-pipeline-core
Expand Down Expand Up @@ -288,10 +303,11 @@ Root-cause groupings (added 2026-06-26; expanded then largely closed 2026-08-02
- **Description:** On 2026-08-02 the `views-frames` floor was raised from `^1.4` to `>=1.10.2,<2` and merged to `development` and `main`. **`views-reporting` consumes this repo by git branch**, not by version (`views-evaluation = { git = "...", branch = "development" }` in its `[tool.uv.sources]`), with no release, no version bump, and no opportunity for it to opt in. **Nobody checked the consumer's resolution before tightening the constraint** — that is the whole entry. Three separate attempts to write down what that consumer's lock actually held were each wrong, and the third went stale 62 seconds after it was written when the consumer committed, pushed and tagged a release. The correct conclusion is not a better narration of someone else's lockfile: it is that a constraint change here must not be made on an assumption about resolution there, and that a consumer tracking a branch receives such changes with none of a release's gates.
- **The sharper case.** A `[tool.uv.sources]` override drops only the *first-party* specifier; it collapses the package's candidate set graph-wide, so any **other** package requiring an excluded version makes the whole resolve unsatisfiable. Widening one repo's pin can therefore break a *different* repo's resolve, and widening must proceed in dependency order.
- **Why this is not C-35:** C-35 is about not exercising a consumer's *call path*. This is about not checking a consumer's *dependency resolution*. A library can be perfectly compatible at the API level and still be unresolvable, and no test in either repo would show it — the failure appears at install time, in their CI, after our merge.
- **Trigger:** The next time any constraint in `pyproject.toml` is narrowed — a raised floor, a lowered ceiling, a new required dependency — while any consumer resolves this repo from a branch rather than a published version. Branch-tracking makes every merge a release for that consumer, without any of a release's gates.
- **Trigger:** The next time any constraint in `pyproject.toml` is narrowed — a raised floor, a lowered ceiling, a new required dependency — while any consumer resolves this repo from a branch rather than a published version; **and (added 2026-09-19, #91) whenever a dependency this repo caps publishes a release outside the declared range** — the ADR-022 §7 checklist now asks at every release, because a month passed between views-frames 2.0.0 and anyone noticing the `<2` cap. Branch-tracking makes every merge a release for that consumer, without any of a release's gates.
- **Location:** `pyproject.toml` (dependency constraints); `views-reporting/pyproject.toml` (formerly the `[tool.uv.sources]` branch-tracking entry; **as of 2026-09-16 it declares `views-evaluation[frames]>=1.0.0,<2.0.0`**, a plain version range)
- **Source:** falsification audit round 3 (2026-08-02); consumer state re-checked 2026-09-16 (repo-assimilation)
- **State update (2026-09-16):** views-reporting has executed this entry's cheapest mitigation — it now pins a published version range rather than a branch, so this repo's merges no longer reach it un-gated. The trigger remains valid for any *other* consumer that resolves from a branch, and the sharper resolver-collapse case above is unchanged. No branch-tracking consumer is known at the time of writing; that is an observation, not a guarantee (the 2026-08-02 narration failures above are the reason this entry does not describe consumer state in more detail). Inward-facing counterpart: C-41.
- **The mirror case (2026-09-19, #91):** a constraint can also be too *narrow* for too long. The `frames` extra capped views-frames `<2` for a month after views-frames 2.0.0 shipped, and the cap travels through the extra into every environment that requests it — an environment we do not own. Found by views-pipeline-core's range review, not ours; and their report's premise that our cap was the *only* bound in play was itself wrong (the consumer concerned also declares its own `<2.0.0`), which is the C-38 narration hazard in the other direction. Widened to `<3` on measurement (suite green on 1.10.2, 1.11.0 and 2.0.0; cross-major save/load round trip); `TestViewsFramesRange` and `TestViewsFramesSurface` pin the range, the one-name import surface and `FrameMetadata`'s behaviour, and CI runs the whole suite on the floor and asserts the resolved version is the 2.x end. The lesson is the same as this entry's: a dependency constraint in this repo is a decision about a consumer's environment, in both directions.
- **Mitigation path:** Cheapest durable fix is to remove the reason for branch-tracking: now that the emit is published, views-reporting can revert to a plain version pin, at which point our merges stop reaching it un-gated. Failing that, treat narrowing a constraint as a breaking change under ADR-022 §3 and check the locks of known branch-tracking consumers first — the same "execute the real consumer's path" rule as C-35, applied to resolution rather than to calls.
- **Note:** ADR-022 §3.2 already requires notifying known consumers before a release. Branch-tracking sidesteps that entirely, because there is no release to notify about. See C-35, C-36.

Expand All @@ -300,7 +316,7 @@ Root-cause groupings (added 2026-06-26; expanded then largely closed 2026-08-02

### C-41 — CI resolves dependencies fresh each run, tests one interpreter of four declared, under a numpy 1.x ceiling
- **Tier:** 3 (Medium) — reproducibility and coverage gap that raises the cost of every dependency change and every green-build claim; no correctness impact today.
- **Description:** No `poetry.lock` is committed (the `.gitignore` comment leaves it optional and none exists), so each `poetry install --all-extras` in `run_pytest.yml` resolves anew, and "CI passed" does not name the versions it passed against. `pyproject.toml` declares `python = ">=3.11,<3.15"` while `run_pytest.yml` pins `python-version: "3.11"` — three of the four claimed interpreters are never exercised. `numpy = "^1.26.4"` caps at the last 1.x line while `scikit-learn` (`^1.6.0`), `scipy` (`^1.11`) and `views-frames` (`>=1.10.2,<2`) each admit every later minor release below 2.0, so a *minor* release of any of them that drops numpy 1.x support makes the resolve unsatisfiable, or — worse — resolves to a different combination between two runs with no diff to show it. The local environment used for the 2026-09-16 assimilation resolved to numpy 1.26.4, scipy 1.15.1, scikit-learn 1.7.2, pandas 1.5.3, views-frames 1.10.2; nothing records that CI resolved the same.
- **Description:** No `poetry.lock` is committed (the `.gitignore` comment leaves it optional and none exists), so each `poetry install --all-extras` in `run_pytest.yml` resolves anew, and "CI passed" does not name the versions it passed against. `pyproject.toml` declares `python = ">=3.11,<3.15"` while `run_pytest.yml` pins `python-version: "3.11"` — three of the four claimed interpreters are never exercised. `numpy = "^1.26.4"` caps at the last 1.x line while `scikit-learn` (`^1.6.0`), `scipy` (`^1.11`) and `views-frames` (`>=1.10.2,<2`; **`<3` since 2026-09-19**, #91 — the admitted set now spans a whole second major line, and `run_pytest.yml`'s verify step is the first CI step that prints and asserts which views-frames it resolved, a partial mitigation of this entry) each admit every later minor release below 2.0, so a *minor* release of any of them that drops numpy 1.x support makes the resolve unsatisfiable, or — worse — resolves to a different combination between two runs with no diff to show it. The local environment used for the 2026-09-16 assimilation resolved to numpy 1.26.4, scipy 1.15.1, scikit-learn 1.7.2, pandas 1.5.3, views-frames 1.10.2; nothing records that CI resolved the same.
- **Trigger:** When scikit-learn, scipy or views-frames publishes a release that requires numpy ≥ 2, or when a maintainer adds Python 3.12+ to the CI matrix, or when a CI run goes red with no change to this repo — check what CI actually resolved, and whether it can be reproduced.
- **Location:** `pyproject.toml` (`numpy = "^1.26.4"`, `python = ">=3.11,<3.15"`, caret ranges on `scikit-learn` and `scipy` that admit future 1.x minors); `.github/workflows/run_pytest.yml` (`python-version: "3.11"`; `poetry install --all-extras` without a lock); absence of `poetry.lock`
- **Source:** repo-assimilation (2026-09-16)
Expand Down
Loading
Loading