"""Decide, for one `Dep` and its registry answer, whether the gate objects. Kept separate from `check.py` so the rules are readable in one screen and testable without a filesystem. The rules differ per constraint style on purpose: * Ordinary packages must carry an exact pin (Q6) — a range is not comparable to a registry answer, which is what made "are we current?" unanswerable. * Q13's carve-out packages may keep a caret; their *lower bound* is compared instead, so a stale `^10.2.0` is still caught. * Toolchain ranges are checked for whether they still admit the current toolchain. `sdk: ^3.12.2` is not stale because Dart shipped 3.13.2 — that constraint already permits it — but `<3.13` would be a real break. """ from __future__ import annotations from dep_freshness._tables import GITCOMMIT, MATRIX, TOOLCHAIN from dep_freshness.constraints import lower_bound, satisfies from dep_freshness.models import Dep, Finding, Severity from dep_freshness.resolve import Answer from dep_freshness.versions import behind, google_api_key # What JitPack accepts as a version, and what a bump pastes into the catalog. _SHORT_SHA = 10 def _commit(dep: Dep, heads: str) -> Finding | None: """A hash pin is current while SOME branch head still starts with it. `heads` is the registry's space-joined list, default branch first; the default head is what a stale finding proposes. """ short = heads.split()[0][:_SHORT_SHA] if not dep.pinned: return Finding(dep, Severity.UNPINNED, short, detail=f"pin a commit ({short})") pin = dep.pinned.lower() if any(head.lower().startswith(pin) for head in heads.split()): return None return Finding( dep, Severity.STALE, short, detail="no branch of the source repository points at this commit any " "more; a commit pin has no release to wait for", ) def _generation(dep: Dep, pin, latest: str) -> Finding | None: """A Google API client is judged inside its own generation only.""" gen, rank = pin for candidate in latest.split(): key = google_api_key(candidate) if key is None or key[0] != gen: continue if key[1] > rank: return Finding(dep, Severity.STALE, candidate) return None return Finding( dep, Severity.UNKNOWN, None, detail=f"the registry has no {gen} generation of this client any more", ) def _toolchain(dep: Dep, latest: str) -> Finding | None: if dep.pinned: if behind(dep.pinned, latest): return Finding(dep, Severity.STALE, latest) return None if dep.constraint == MATRIX: # Standing decision: every repo runs exactly ONE toolchain version and # it is the newest. A matrix is three answers to a one-answer question, # and its older entries are stale by construction. return Finding( dep, Severity.UNPINNED, latest, detail=f"a version MATRIX is not a version: run exactly one " f"{dep.name}, the newest. Replace the whole matrix with a " f'single `{dep.name}-version: "' f'{latest.split(".")[0]}.x"`', ) if not dep.constraint or dep.caret_ok is False: return Finding( dep, Severity.UNPINNED, latest, detail="a channel name is not a version: two machines on the same " "commit can build with different SDKs", ) if not satisfies(dep.constraint, latest): return Finding( dep, Severity.STALE, latest, detail=f"the constraint excludes the current toolchain {latest}", ) return None def judge(dep: Dep, answer: Answer) -> Finding | None: """The gate's objection to `dep`, or None when it is fine.""" if dep.override: return Finding( dep, Severity.OVERRIDE, answer.version, detail="an override pins a transitive dependency nothing else " "watches; allowlist it or remove it", ) if answer.unavailable: return Finding( dep, Severity.UNKNOWN, None, detail="no network and no cached answer", ) latest = answer.version if latest is None: return Finding( dep, Severity.UNKNOWN, None, detail="the registry has no stable release for this package", ) if dep.ecosystem == TOOLCHAIN: return _toolchain(dep, latest) if dep.ecosystem == GITCOMMIT: return _commit(dep, latest) pin = google_api_key(dep.pinned or "") if pin is not None: return _generation(dep, pin, latest) if dep.peer: # A peerDependency declares what a CONSUMER may bring, so exact-pinning # it is actively wrong: it would force every consumer onto one version. # The meaningful question is whether the range still admits latest. if not satisfies(dep.constraint, latest): return Finding( dep, Severity.STALE, latest, detail=f"the peer range excludes the current {latest}", ) return None if dep.pinned is None: if dep.caret_ok: floor = lower_bound(dep.constraint) if floor and behind(floor, latest): return Finding( dep, Severity.STALE, latest, detail=f"range floor {floor} is behind latest stable", ) return None return Finding( dep, Severity.UNPINNED, latest, detail=f"exact-pin it ({latest}) so freshness is checkable", ) if behind(dep.pinned, latest): return Finding(dep, Severity.STALE, latest) if dep.locked and dep.locked != dep.pinned: return Finding( dep, Severity.LOCK_MISMATCH, latest, detail=f"lockfile resolved {dep.locked}, manifest pins {dep.pinned}", ) return None