Every Flutter app here declares `flutter_launcher_icons` twice: as a dev
dependency, and as a TOP-LEVEL configuration block for the same tool. The line
lookup broke ties by indentation alone, so the top-level block won and the
gate told you to fix a line that carries no version at all -- lyricanki
reported `pubspec.yaml:64` while the caret being complained about is at 48.
The pubspec parser now says which top-level keys may hold a version, and a key
found outside them is only a fallback. `environment` is in that list on
purpose: without it the `sdk:` inside a `flutter: {sdk: flutter}` block would
outrank the SDK constraint it is meant to find.
Only the pubspec parser passes the hint. JSON and TOML indent their section
headers differently, so they keep the old behaviour unchanged.
310 tests, 100% branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Two npm-specific corrections found by running the gate over web_ui.
**`peerDependencies` must not be exact-pinned.** The gate was reporting
`react: ">=19"` as "not exact-pinned", and acting on that would have been
actively harmful: a peer range declares what a CONSUMER may bring, so pinning
it forces every consumer of @kuhyx/web-ui onto one exact React. Peers are now
judged the way toolchain ranges are -- does the range still admit latest? --
and only reported when it does not.
**`packageManager` is a real dependency.** corepack enforces it, so
`pnpm@10.13.1` is an exact pin that nothing was checking. It is now read and
compared, which immediately surfaced that both TS packages sit on pnpm 10.13.1
against 11.24.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
"Are we on the newest version?" was not mechanically answerable across these
repos: constraint styles had drifted per-ecosystem and a caret range is
"newest-compatible" by definition, which gives a checker nothing to compare.
This gate makes it answerable and then enforces the answer.
Checks two things at once -- that every dependency carries an exact pin, and
that the pin equals its registry's latest STABLE release -- across pub.dev,
PyPI, npm, crates.io, the Go proxy, kuhyx/utils git tags, and the Flutter /
Node / Python toolchains.
Three deliberate design points:
* Pre-releases are rejected per-ecosystem, because every ecosystem leaks them
differently and each leak fails GREEN. npm lets a maintainer point
`dist-tags.latest` at an rc; crates.io's `max_version` includes them; PyPI's
legacy `info.version` includes yanked releases. Unit-tested with fixtures,
since an end-to-end run cannot surface a wrong "up to date".
* Offline, the gate serves an expired cache entry and PASSES with a DEGRADED
banner (exit 3 only under --strict, which is CI). `--no-verify` is banned
here, so a hook that hard-failed offline would leave no way to commit at all.
* The allowlist has two entry classes. `blocked_by: transitive:<pkg>@<ver>` is
a predicate that clears itself when upstream moves and must NOT carry a
date; everything else is discretionary and requires `expires`, capped at 90
days. Exact pins plus latest-everywhere is provably unsatisfiable when a
dependency internally constrains a shared transitive, and expiring the
predicate class on a calendar would turn the gate red for a reason no commit
could fix. An entry with nothing left to excuse is itself an error.
One hook id, one shim, one shared implementation -- always delegating, so it
cannot fork the way the 250-line cap forked into four ids, two of which were
independent reimplementations.
256 tests, 100% branch coverage on every module of the gate. Also adds the
workflow that runs them, which incidentally gives md_naming's 71 tests their
first CI run.
Not yet installed in any repo: a repo that gets the gate before its deps are
bumped is instantly and permanently red, so the gate and the upgrade land
together, per repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK