|
| 1 | +# version-skew-yank-awareness |
| 2 | + |
| 3 | +**Completed:** 2026-08-18 |
| 4 | +**Type:** feature · **Target:** PyAutoHeart · **PR:** PyAutoHeart#148 (merged, |
| 5 | +`7659f153`; branch `feature/version-skew-yank-awareness`) |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Closed the yank gap the `version_skew` Heart leg acknowledged in its own |
| 10 | +docstring: the tick check compares floors against **local git tags**, so it |
| 11 | +could not see the other way a floor goes bad — the release it names being |
| 12 | +**yanked on PyPI** afterwards (the 2026-07 incident shape, where every floor |
| 13 | +named the yanked `2026.7.6.649`). Filed as the surviving loose end when the |
| 14 | +Phase 4 tracker retired ([[release-version-sync-back-to-main]]); shipped the |
| 15 | +same day. |
| 16 | + |
| 17 | +## What shipped |
| 18 | + |
| 19 | +`python -m heart.checks.version_skew --pypi` — a deep, on-demand leg that asks |
| 20 | +the PyPI JSON API whether each workspace floor still names an installable |
| 21 | +(non-yanked) release and whether *any* installable release satisfies it. |
| 22 | + |
| 23 | +- **Statuses → readiness:** `UNSATISFIABLE` (nothing installable ≥ floor — |
| 24 | + every candidate yanked; same defect class as the tag leg) → **RED**; |
| 25 | + `FLOOR_YANKED` (floor itself yanked/absent but newer installable releases |
| 26 | + satisfy it — floors are `>=` bounds so installs still resolve) → **YELLOW** |
| 27 | + "bump the floor"; `UNKNOWN` (PyPI unreachable) → **STALE**, never a false |
| 28 | + block; `OK`/`BAD` as in the tag leg. |
| 29 | +- **Tick untouched:** network-bound, so never run from `tick.sh` — on-demand / |
| 30 | + nightly only, behind the explicit `--pypi` flag. |
| 31 | +- **Sidecar state** (`version_skew_pypi.json`): the tick's `version_skew.json` |
| 32 | + rewrite can never clobber on-demand PyPI evidence, and vice versa. An absent |
| 33 | + slice is no signal in readiness and the dashboard. |
| 34 | +- `run_pypi()` side-effect-free like `run()` (persistence in `main()` only); |
| 35 | + one PyPI fetch per distinct package, not per workspace. Wired through |
| 36 | + `state.py` snapshot, readiness legs + score weights, and a "Version skew |
| 37 | + (PyPI)" dashboard section. |
| 38 | +- **Validated:** 484 tests pass (15 new); live probe against real PyPI |
| 39 | + (`autolens`, 421 releases): `2026.7.9.1 → OK`, the incident release |
| 40 | + `2026.7.6.649 → FLOOR_YANKED`, `2099.1.1.1 → UNSATISFIABLE`, |
| 41 | + `garbage → BAD`. |
| 42 | + |
| 43 | +## Key findings / traps |
| 44 | + |
| 45 | +- **Tenant firewall caught a real leak on the first CI round:** the |
| 46 | + one-fetch-per-package test named `HowToLens` — a *new* instance fact in |
| 47 | + organ code (`repos_sync.py --check --only "tenant firewall (organ code)"`). |
| 48 | + Fixed by using `autolens_assistant` (an already-present fact in that file, |
| 49 | + same `autolens` package mapping). When testing organ code, pick instance |
| 50 | + names the file already carries; the firewall treats new ones as drift even |
| 51 | + in tests. |
| 52 | +- **Verdict-shape reasoning recorded in the check itself:** a yanked floor |
| 53 | + with newer installable releases is deliberately YELLOW, not RED — `>=` |
| 54 | + semantics mean installs still resolve; only "nothing installable ≥ floor" |
| 55 | + blocks. Offline degrades to UNKNOWN/STALE so an offline dev box can never |
| 56 | + produce a false RED. |
| 57 | +- Fork (b) of the version model stands: this reads state only — no |
| 58 | + commit-back behaviour was added anywhere. |
| 59 | + |
| 60 | +## Original prompt |
| 61 | + |
| 62 | +# version_skew: flag a floor that names a PyPI-yanked release |
| 63 | + |
| 64 | +Type: feature |
| 65 | +Target: PyAutoHeart |
| 66 | +Repos: |
| 67 | +- PyAutoHeart |
| 68 | +Difficulty: small |
| 69 | +Autonomy: supervised |
| 70 | +Priority: low |
| 71 | +Status: formalised |
| 72 | + |
| 73 | +## Why |
| 74 | + |
| 75 | +The `version_skew` Heart leg (reworked under build-chain #155 Phase 4 task 2, |
| 76 | +PyAutoHeart#96) enforces "a floor must name an *installable* release" only |
| 77 | +against **local git tags**: UNSATISFIABLE fires when |
| 78 | +`version.minimum_library_version` exceeds the newest `YYYY.M.D.B` release tag. |
| 79 | +It cannot see the other way a floor goes bad — the release it names being |
| 80 | +**yanked on PyPI afterwards** (as `2026.7.6.649` was; that yank is what |
| 81 | +originally exposed the "floors named a yanked release" bug that this check now |
| 82 | +half-guards). The gap is acknowledged in the check itself |
| 83 | +(`heart/checks/version_skew.py:33`: "a release that was later *yanked* on PyPI — |
| 84 | +that needs the PyPI API, not git tags") and was left unowned when the Phase 4 |
| 85 | +tracker (`complete/2026/08/release-version-sync-back-to-main.md`) retired. |
| 86 | + |
| 87 | +## Scope |
| 88 | + |
| 89 | +- Extend `version_skew` (or add a sibling non-tick check, if network access |
| 90 | + disqualifies it from the tick path — the current check is deliberately |
| 91 | + local-tags-only, no import/network) to query the PyPI JSON API for the |
| 92 | + floor's version and flag `yanked: true` per package. |
| 93 | +- Verdict shape should mirror the existing one: a yanked floor is the same |
| 94 | + class of defect as UNSATISFIABLE (no installable version satisfies "exactly |
| 95 | + this floor"), but the floor semantics (>=) mean a yanked floor with newer |
| 96 | + non-yanked releases still resolves — decide whether that is RED, YELLOW, or |
| 97 | + informational, and record the reasoning. |
| 98 | +- Offline/API-failure behaviour must be UNKNOWN/STALE, never a false RED — |
| 99 | + match how the tag-based check treats unresolvable repos. |
| 100 | + |
| 101 | +## Constraints |
| 102 | + |
| 103 | +- Do not slow the readiness tick: if the PyPI call cannot be cached or made |
| 104 | + optional, keep it out of the tick path (nightly / on-demand only). |
| 105 | +- Fork (b) of the version model stands (mains authoritative, floors + tags + |
| 106 | + wheels as the live signals — see the retired tracker). This check reads |
| 107 | + state; it must not resurrect any commit-back behaviour. |
0 commit comments