Skip to content

Commit 7659f15

Browse files
authored
Merge pull request #148 from PyAutoLabs/feature/version-skew-yank-awareness
version_skew: deep --pypi leg — flag floors naming yanked releases
2 parents b7a6523 + 34a3f00 commit 7659f15

6 files changed

Lines changed: 289 additions & 10 deletions

File tree

heart/checks/version_skew.py

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,25 @@
2929
- **UNKNOWN** — the library isn't checked out / carries no release tags, so the
3030
newest release can't be resolved; surfaced as caution, never a hard block.
3131
32-
Not covered here (deeper, non-tick checks own it): whether the floor names a
33-
release that was later *yanked* on PyPI — that needs the PyPI API, not git tags.
32+
The yank gap is owned by the **deep PyPI leg** (``--pypi``): git tags cannot see
33+
a release being *yanked* on PyPI after the fact, so ``--pypi`` asks the PyPI
34+
JSON API whether each floor still names an installable (non-yanked) release and
35+
whether *any* installable release satisfies it. Network — never part of the
36+
tick; run it on demand or from a nightly. Its statuses:
37+
38+
- **UNSATISFIABLE** — no installable release >= floor exists on PyPI (every
39+
candidate yanked): same defect class as the tag-based UNSATISFIABLE.
40+
- **FLOOR_YANKED** — the floor version itself is yanked/absent but a newer
41+
installable release satisfies it; floors are ``>=`` so installs still
42+
resolve — warn, fix by bumping the floor.
43+
- **OK** / **BAD** / **UNKNOWN** — as above; UNKNOWN covers PyPI unreachable
44+
(offline is caution, never a false hard block).
45+
3446
An informational "floor lags far behind newest" signal is a possible future add.
3547
36-
The result lands at ``$HEART_STATE_DIR/version_skew.json``.
48+
The tick result lands at ``$HEART_STATE_DIR/version_skew.json``; the ``--pypi``
49+
leg at ``version_skew_pypi.json`` (a sibling file, so the tick never clobbers
50+
on-demand evidence).
3751
"""
3852

3953
from __future__ import annotations
@@ -149,20 +163,103 @@ def run(root: Path = PYAUTO_ROOT) -> dict[str, Any]:
149163
return {"workspaces": workspaces}
150164

151165

166+
# --------------------------------------------------------------------------- #
167+
# Deep PyPI leg (``--pypi``) — yank-awareness. Network; never run from the tick.
168+
# --------------------------------------------------------------------------- #
169+
170+
PYPI_URL = "https://pypi.org/pypi/{package}/json"
171+
PYPI_TIMEOUT_S = 10
172+
173+
174+
def fetch_pypi_releases(package: str) -> dict[str, list] | None:
175+
"""The package's ``releases`` map from the PyPI JSON API, or None when the
176+
API is unreachable/unparseable — offline must degrade to UNKNOWN, never a
177+
false hard block."""
178+
import urllib.request
179+
180+
try:
181+
with urllib.request.urlopen(
182+
PYPI_URL.format(package=package), timeout=PYPI_TIMEOUT_S
183+
) as resp:
184+
data = json.load(resp)
185+
except Exception:
186+
return None
187+
releases = data.get("releases")
188+
return releases if isinstance(releases, dict) else None
189+
190+
191+
def _installable(files: list) -> bool:
192+
"""A release is installable iff at least one of its files is not yanked
193+
(PyPI marks yank per file; a fileless release installs nothing)."""
194+
return any(isinstance(f, dict) and not f.get("yanked") for f in files or [])
195+
196+
197+
def pypi_floor_status(floor: str | None, releases: dict[str, list] | None) -> str:
198+
"""OK / FLOOR_YANKED / UNSATISFIABLE / UNKNOWN / BAD for one floor vs PyPI.
199+
200+
Floors are ``>=`` bounds, so a yanked floor with a newer installable
201+
release still resolves at install time — that is FLOOR_YANKED (fix by
202+
bumping the floor to an installable release), not a hard block. No
203+
installable release >= floor at all is the same defect class as the
204+
tag-based UNSATISFIABLE.
205+
"""
206+
if releases is None:
207+
return "UNKNOWN"
208+
ft = _tuple(floor or "")
209+
if ft is None:
210+
return "BAD"
211+
installable = {
212+
v.strip()
213+
for v, files in releases.items()
214+
if _TAG_RE.match(v.strip()) and _installable(files)
215+
}
216+
if not any(_tuple(v) >= ft for v in installable):
217+
return "UNSATISFIABLE"
218+
return "OK" if (floor or "").strip() in installable else "FLOOR_YANKED"
219+
220+
221+
def run_pypi(root: Path = PYAUTO_ROOT) -> dict[str, Any]:
222+
"""Side-effect-free like run(): one PyPI fetch per distinct package, one
223+
entry per floored workspace."""
224+
releases_by_package: dict[str, dict[str, list] | None] = {}
225+
workspaces = []
226+
for workspace, (repo, pkg) in workspace_library().items():
227+
floor = read_workspace_floor(workspace, root)
228+
if floor is None:
229+
continue # no floor recorded → not a candidate
230+
if pkg not in releases_by_package:
231+
releases_by_package[pkg] = fetch_pypi_releases(pkg)
232+
workspaces.append(
233+
{
234+
"workspace": workspace,
235+
"library": repo,
236+
"package": pkg,
237+
"floor": floor,
238+
"status": pypi_floor_status(floor, releases_by_package[pkg]),
239+
}
240+
)
241+
return {"workspaces": workspaces}
242+
243+
152244
def main(argv: list[str]) -> int:
153-
result = run()
245+
pypi = "--pypi" in argv
246+
result = run_pypi() if pypi else run()
154247
sys.path.insert(0, str(HEART_HOME))
155248
from heart import state
156249

157-
# Persist only here, at the tick/CLI entrypoint — run() is side-effect-free
158-
# so library callers (and the test suite) can never clobber live state.
159-
state.atomic_write_json(HEART_STATE_DIR / "version_skew.json", result)
250+
# Persist only here, at the tick/CLI entrypoint — run()/run_pypi() are
251+
# side-effect-free so library callers (and the test suite) can never
252+
# clobber live state. The --pypi leg gets its own sidecar so the tick's
253+
# version_skew.json rewrite never clobbers on-demand PyPI evidence.
254+
name = "version_skew_pypi.json" if pypi else "version_skew.json"
255+
state.atomic_write_json(HEART_STATE_DIR / name, result)
160256

161257
from heart.heart_color import c_ok, c_warn, c_fail, c_info, c_meta, glyph_ok, glyph_warn, glyph_fail
162258

163259
workspaces = result["workspaces"]
164260
unsatisfiable = [w for w in workspaces if w["status"] == "UNSATISFIABLE"]
165261
bad = [w for w in workspaces if w["status"] == "BAD"]
262+
yanked = [w for w in workspaces if w["status"] == "FLOOR_YANKED"]
166263
unknown = [w for w in workspaces if w["status"] == "UNKNOWN"]
167264
blocking = unsatisfiable + bad # release-blocking statuses
168265
if blocking:
@@ -173,13 +270,19 @@ def main(argv: list[str]) -> int:
173270
if bad:
174271
parts.append(c_warn(f"{len(bad)} bad"))
175272
label = " ".join(parts)
176-
elif unknown:
273+
elif yanked or unknown:
177274
glyph = glyph_warn()
178-
label = c_warn(f"{len(unknown)} unknown")
275+
parts = []
276+
if yanked:
277+
parts.append(c_warn(f"{len(yanked)} floor yanked"))
278+
if unknown:
279+
parts.append(c_warn(f"{len(unknown)} unknown"))
280+
label = " ".join(parts)
179281
else:
180282
glyph = glyph_ok()
181283
label = c_ok(f"{len(workspaces)} floors satisfiable")
182-
print(f"{glyph} {c_info('version_skew')} {label} {c_meta(f'({len(workspaces)} floors)')}")
284+
check_name = "version_skew --pypi" if pypi else "version_skew"
285+
print(f"{glyph} {c_info(check_name)} {label} {c_meta(f'({len(workspaces)} floors)')}")
183286
return 0
184287

185288

heart/dashboard.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,23 @@ def build_board(
545545
elif skew:
546546
sections.append(Section("version_skew", "Version skew", OK, "all floors satisfiable", []))
547547

548+
# Version skew — PyPI yank leg (deep `version_skew --pypi`; the slice is
549+
# absent until that on-demand probe has run, so no section = not yet run) --
550+
skew_pypi = (snapshot.get("version_skew_pypi") or {}).get("workspaces") or []
551+
pypi_off = [w for w in skew_pypi if isinstance(w, dict) and w.get("status") not in ("OK", None)]
552+
pypi_blocking = [w for w in pypi_off if str(w.get("status")).upper() in ("UNSATISFIABLE", "BAD")]
553+
if pypi_off:
554+
st = FAIL if pypi_blocking else WARN
555+
summary = f"{len(pypi_blocking)} blocking" if pypi_blocking else f"{len(pypi_off)} unresolved"
556+
details = [
557+
f"{w.get('status')}: {w.get('workspace')} floor {w.get('floor')} "
558+
f"({w.get('package')} on PyPI)"
559+
for w in pypi_off[:8]
560+
]
561+
sections.append(Section("version_skew_pypi", "Version skew (PyPI)", st, summary, details))
562+
elif skew_pypi:
563+
sections.append(Section("version_skew_pypi", "Version skew (PyPI)", OK, "all floors installable", []))
564+
548565
# Install verification ---------------------------------------------------
549566
vi = snapshot.get("verify_install") or {}
550567
if isinstance(vi, dict) and "ready" in vi:

heart/readiness.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@
126126
"parked": (5, 15),
127127
"skew_bad": (25, 50),
128128
"skew_unknown": (10, 30),
129+
"skew_pypi_unsatisfiable": (25, 50),
130+
"skew_pypi_bad": (25, 50),
131+
"skew_pypi_floor_yanked": (8, 24),
132+
"skew_pypi_unknown": (10, 30),
129133
"install_not_ready": (40, 40),
130134
"install_non_release": (10, 10),
131135
"install_stale": (10, 10),
@@ -402,6 +406,37 @@ def scope_local(msg: str, key: str) -> None:
402406
stale.append(f"{w.get('workspace')}: newest {w.get('library')} release unknown")
403407
hit("skew_unknown")
404408

409+
# --- version skew, PyPI yank leg (deep `version_skew --pypi`; the slice is
410+
# absent until that on-demand probe has run — absence is no signal) ---
411+
skew_pypi = snapshot.get("version_skew_pypi")
412+
if isinstance(skew_pypi, dict):
413+
for w in skew_pypi.get("workspaces") or []:
414+
if not isinstance(w, dict):
415+
continue
416+
status = str(w.get("status", "")).upper()
417+
if status == "UNSATISFIABLE":
418+
red.append(
419+
f"{w.get('workspace')}: no installable {w.get('package')} release "
420+
f"on PyPI satisfies floor {w.get('floor')} (all candidates yanked)"
421+
)
422+
hit("skew_pypi_unsatisfiable")
423+
elif status == "BAD":
424+
red.append(
425+
f"{w.get('workspace')}: unparseable floor {w.get('floor')} (PyPI leg)"
426+
)
427+
hit("skew_pypi_bad")
428+
elif status == "FLOOR_YANKED":
429+
yellow.append(
430+
f"{w.get('workspace')}: floor {w.get('floor')} names a yanked/"
431+
f"unavailable {w.get('package')} release — bump the floor"
432+
)
433+
hit("skew_pypi_floor_yanked")
434+
elif status == "UNKNOWN":
435+
stale.append(
436+
f"{w.get('workspace')}: PyPI unreachable for {w.get('package')}"
437+
)
438+
hit("skew_pypi_unknown")
439+
405440
# --- manifest drift (YELLOW — identity hygiene vs PyAutoMind/repos.yaml) ---
406441
manifest = snapshot.get("manifest_drift")
407442
if isinstance(manifest, dict) and manifest.get("available"):

heart/state.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def aggregate() -> dict[str, Any]:
8080
"test_run": _read_json_or_default(HEART_STATE_DIR / "test_run.json", {}),
8181
"workspace_testmode_timing": _read_json_or_default(HEART_STATE_DIR / "workspace_testmode_timing.json", {}),
8282
"version_skew": _read_json_or_default(HEART_STATE_DIR / "version_skew.json", {}),
83+
"version_skew_pypi": _read_json_or_default(HEART_STATE_DIR / "version_skew_pypi.json", {}),
8384
"manifest_drift": _read_json_or_default(HEART_STATE_DIR / "manifest_drift.json", {}),
8485
"verify_install": _read_json_or_default(HEART_STATE_DIR / "verify_install.json", {}),
8586
"url_check": _read_json_or_default(HEART_STATE_DIR / "url_check.json", {}),

tests/test_readiness.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,44 @@ def test_version_skew_unknown_is_stale_tier():
178178
assert any("release unknown" in r for r in v["stale_reasons"])
179179

180180

181+
# --- version skew, PyPI yank leg (deep `version_skew --pypi`) ------------------
182+
# The slice is absent from make_snapshot(), so the all-green test already proves
183+
# absence is no signal (the probe is on-demand, not part of the tick).
184+
185+
def test_version_skew_pypi_unsatisfiable_is_red():
186+
snap = make_snapshot(version_skew_pypi={"workspaces": [
187+
{"workspace": "autolens_workspace", "library": "PyAutoLens", "package": "autolens",
188+
"floor": "2026.7.6.649", "status": "UNSATISFIABLE"}
189+
]})
190+
v = compute(snap)
191+
assert v["verdict"] == "red"
192+
assert any("no installable" in r and "yanked" in r for r in v["red_reasons"])
193+
assert v["score"] == 75
194+
195+
196+
def test_version_skew_pypi_yanked_floor_is_yellow():
197+
# Floors are >= bounds: a yanked floor with newer installable releases
198+
# still resolves at install time — warn (bump the floor), never block.
199+
snap = make_snapshot(version_skew_pypi={"workspaces": [
200+
{"workspace": "autolens_workspace", "library": "PyAutoLens", "package": "autolens",
201+
"floor": "2026.7.6.649", "status": "FLOOR_YANKED"}
202+
]})
203+
v = compute(snap)
204+
assert v["verdict"] == "yellow"
205+
assert any("yanked" in r and "bump the floor" in r for r in v["yellow_reasons"])
206+
207+
208+
def test_version_skew_pypi_unknown_is_stale_tier():
209+
# PyPI unreachable (offline box) must degrade to caution, never a false RED.
210+
snap = make_snapshot(version_skew_pypi={"workspaces": [
211+
{"workspace": "autolens_workspace", "library": "PyAutoLens", "package": "autolens",
212+
"floor": "2026.7.9.1", "status": "UNKNOWN"}
213+
]})
214+
v = compute(snap)
215+
assert v["verdict"] == "stale"
216+
assert any("PyPI unreachable" in r for r in v["stale_reasons"])
217+
218+
181219
def test_install_verification_failed_is_red():
182220
snap = make_snapshot(verify_install={
183221
"ready": False, "ts": "2026-06-01T00:00:00+00:00",

tests/test_version_skew.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,72 @@ def test_autolens_assistant_is_a_polled_workspace():
121121
assert mapping["autolens_assistant"] == ("PyAutoLens", "autolens")
122122

123123

124+
# --- deep PyPI yank leg (--pypi) ----------------------------------------------
125+
126+
def _files(*yanked):
127+
return [{"yanked": y} for y in yanked]
128+
129+
130+
# 2026.7.6.649 fully yanked (the real 2026-07 incident); two installable newer.
131+
RELEASES = {
132+
"2026.7.6.649": _files(True, True),
133+
"2026.7.9.1": _files(False, False),
134+
"2026.7.15.1": _files(False),
135+
}
136+
137+
138+
@pytest.mark.parametrize("floor,releases,expected", [
139+
("2026.7.9.1", RELEASES, "OK"), # floor installable
140+
("2026.7.6.649", RELEASES, "FLOOR_YANKED"), # floor yanked, newer installable
141+
("2026.7.1.1", RELEASES, "FLOOR_YANKED"), # floor absent from PyPI, newer installable
142+
("2026.8.1.1", RELEASES, "UNSATISFIABLE"), # nothing >= floor exists
143+
("2026.7.9.1", {"2026.7.9.1": _files(True)}, "UNSATISFIABLE"), # everything >= floor yanked
144+
("2026.7.9.1", {"2026.7.9.1": []}, "UNSATISFIABLE"), # fileless release installs nothing
145+
("not.a.version", RELEASES, "BAD"),
146+
("2026.7.9.1", None, "UNKNOWN"), # PyPI unreachable → never a false block
147+
])
148+
def test_pypi_floor_status(floor, releases, expected):
149+
assert vs.pypi_floor_status(floor, releases) == expected
150+
151+
152+
def test_run_pypi_one_fetch_per_package(tmp_path, monkeypatch):
153+
# autolens_workspace and autolens_assistant both map to package `autolens`
154+
# → the probe must fetch each distinct package once, not once per workspace.
155+
for ws in ("autolens_workspace", "autolens_assistant"):
156+
cfg = tmp_path / ws / "config"
157+
cfg.mkdir(parents=True)
158+
(cfg / "general.yaml").write_text("version:\n minimum_library_version: 2026.7.9.1\n")
159+
calls = []
160+
monkeypatch.setattr(vs, "fetch_pypi_releases", lambda pkg: calls.append(pkg) or RELEASES)
161+
result = vs.run_pypi(root=tmp_path)
162+
assert calls == ["autolens"]
163+
by_ws = {w["workspace"]: w for w in result["workspaces"]}
164+
assert by_ws["autolens_workspace"]["status"] == "OK"
165+
assert by_ws["autolens_assistant"]["package"] == "autolens"
166+
167+
168+
def test_run_pypi_offline_is_unknown(tmp_path, monkeypatch):
169+
ws = tmp_path / "autolens_workspace" / "config"
170+
ws.mkdir(parents=True)
171+
(ws / "general.yaml").write_text("version:\n minimum_library_version: 2026.7.9.1\n")
172+
monkeypatch.setattr(vs, "fetch_pypi_releases", lambda pkg: None)
173+
result = vs.run_pypi(root=tmp_path)
174+
w = {x["workspace"]: x for x in result["workspaces"]}["autolens_workspace"]
175+
assert w["status"] == "UNKNOWN"
176+
177+
178+
def test_run_pypi_flags_yanked_floor(tmp_path, monkeypatch):
179+
# The 2026-07 incident shape: floor names the yanked release while newer
180+
# installable releases exist → FLOOR_YANKED (warn), not a hard block.
181+
ws = tmp_path / "autolens_workspace" / "config"
182+
ws.mkdir(parents=True)
183+
(ws / "general.yaml").write_text("version:\n minimum_library_version: 2026.7.6.649\n")
184+
monkeypatch.setattr(vs, "fetch_pypi_releases", lambda pkg: RELEASES)
185+
result = vs.run_pypi(root=tmp_path)
186+
w = {x["workspace"]: x for x in result["workspaces"]}["autolens_workspace"]
187+
assert w["status"] == "FLOOR_YANKED"
188+
189+
124190
# --- state-dir isolation (the 2026-07-15 clobber incident's sibling) -----------
125191

126192
def test_run_writes_nothing_to_state_dir(tmp_path):
@@ -144,3 +210,22 @@ def test_main_persists_result_to_state_dir(monkeypatch):
144210
assert vs.main(["version_skew"]) == 0
145211
written = json.loads((Path(os.environ["HEART_STATE_DIR"]) / "version_skew.json").read_text())
146212
assert written == {"workspaces": []}
213+
214+
215+
def test_main_pypi_persists_to_sibling_file(monkeypatch):
216+
"""--pypi writes version_skew_pypi.json and never touches the tick's
217+
version_skew.json — the tick must not clobber on-demand PyPI evidence and
218+
vice versa."""
219+
import json
220+
import os
221+
from pathlib import Path
222+
state_dir = Path(os.environ["HEART_STATE_DIR"])
223+
tick_file = state_dir / "version_skew.json"
224+
tick_before = tick_file.read_text() if tick_file.is_file() else None
225+
payload = {"workspaces": [{"workspace": "autolens_workspace", "status": "FLOOR_YANKED"}]}
226+
monkeypatch.setattr(vs, "run_pypi", lambda root=vs.PYAUTO_ROOT: payload)
227+
assert vs.main(["version_skew", "--pypi"]) == 0
228+
written = json.loads((state_dir / "version_skew_pypi.json").read_text())
229+
assert written == payload
230+
tick_after = tick_file.read_text() if tick_file.is_file() else None
231+
assert tick_after == tick_before

0 commit comments

Comments
 (0)