Most of these configs interleave local and remote hook repos -- diet-guard,
screen-locker and wake-alarm carry nine `- repo:` blocks each. Appending at
end of file lands the hook under whichever block happens to be last, and a
local hook definition handed to a remote repo fails with an error naming an
unknown id rather than the real problem. diet-guard's last block happens to be
`local`, so this would have worked by luck there and broken elsewhere.
A config with no local block now refuses rather than guessing.
308 tests, 100% branch coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
The gate is three files per repo -- a delegate, a pre-commit hook entry and a
CI workflow -- and Phase 3 needs them in sixteen more repos. Hand-replication
is precisely how the 250-line cap ended up shipping under four different hook
ids, two of which were independent reimplementations rather than delegates, so
the copy is now a script and the templates have exactly one home.
scripts/install_dependency_freshness_gate.sh <repo> [--check] [--fvm]
Idempotent, and it rewrites a copy that has drifted from the template rather
than leaving it. `--check` reports without writing. `--fvm` pins the Flutter
SDK, and only in a repo that actually holds a pubspec.yaml -- an .fvmrc in a
Python repo declares a toolchain nothing there builds with. An unresolvable
SDK version leaves the pin alone and exits 1: guessing would pin every repo to
a number nobody chose.
The pre-commit block is APPENDED rather than parsed and re-emitted. Every one
of these configs carries comments explaining why its hooks exist, and a YAML
round-trip through the standard library drops all of them.
306 tests, 100% branch coverage on every production module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Two things, because the first is what blocked the second from committing.
**The rot check was scope-blind.** pre-commit passes only the staged
manifests, so a run that sees no package.json cannot learn anything about
whether the fleet-wide typescript hold is still needed -- yet it concluded the
entry was dead and exited 2. Staging two pubspec.yaml files in utils was
enough to make the repo uncommittable. The dead-entry check now runs only
under --all, which is CI and the manual whole-repo run; a subset scan has seen
a subset by construction.
**crdt_sync_dart 0.11.1 and design_system 0.2.1.** Both had dependency
upgrades land after their newest tag, so a consumer moving to "latest tag"
would pick up the pre-upgrade constraints (crdt_sync_dart-v0.11.0 still
carries `crypto: ^3.0.7`, `http: ^1.6.0`, `lints: ^5.0.0`) while carrying its
own new exact pins -- the unsatisfiable-resolution case exact pins create.
Version bump only; the dependency work is already in 922fb36 and 704c67e.
The allowlist end-to-end tests moved to their own file for the 250-line cap,
and `run`/`canned` moved to conftest so both files share one definition.
`canned` is no longer autouse: the resolver tests patch `_fetch` themselves.
293 tests, 100% branch coverage on every production module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Two things that were fine in utils and would not have been in the next forty
repos.
**The allowlist is now fleet-wide plus per-repo.** `typescript-eslint` blocks
TS 7 in EVERY repo that pins typescript -- eight of them and counting -- and a
repo-only allowlist meant hand-copying one entry into eight files that then rot
independently. That is the four-way fork of the 250-line cap, reproduced inside
the mechanism built to avoid it. `~/utils/dependency-freshness.allowlist.yaml`
is inherited by every repo; a repo entry for the same `ecosystem:package`
overrides it.
Inherited entries are exempt from the no-longer-needed check everywhere except
the repo that owns the file. Without that carve-out a fleet-wide hold on
typescript exits 2 in every Flutter app -- it excuses nothing there, which is
the normal case, not rot -- and those repos become uncommittable. Confirmed
against punchme, which now exits 0 while utils still reports the entry as in
use.
**`quarantine.py` bypassed the cache entirely.** It ran for every npm finding
and fetched the FULL packument -- 7MB for react, 16MB for typescript -- with no
TTL and no reuse, inside a pre-commit hook. One finding in utils, but the JS
repos ahead have 16 to 37 each. Now cached under the normal 6h TTL, keyed
`<name>@<dist-tag>`: the tag has to be part of the key rather than clamped
afterwards, because clamping returns the ceiling and the ceiling is frequently
the very release the quarantine is excluding.
**Both cache and shared allowlist are now redirected by AUTOUSE fixtures.** A
quarantine test had already seeded a fake answer for package "x" into the real
~/.cache/dep-freshness/registry.json, and the next run read its own pollution
back and passed for the wrong reason. Removed the entry; the fixture makes it
unrepeatable.
292 tests, 100% branch coverage on every module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Finishes the last two of the eleven manifests in this repo. Exact-pins every
devDependency and takes three majors -- @testing-library/jest-dom 6 -> 7,
jsdom 29 -> 30, pnpm 10.13.1 -> 11.24.0 -- with lint and coverage green in
both (ts_core 42/42, web_ui 102/102, 100% branches).
**TypeScript stays at 6.0.3, and this is the campaign's first real
unsatisfiable conflict.** TS 7.0.2 is latest, but typescript-eslint 8.68.0 --
itself the latest release -- declares `typescript: ">=4.8.4 <6.1.0"` and throws
"does not support TS 7.0" as eslint loads its config. There is no 9.x. Taking
TS 7 would mean dropping every type-aware lint rule from both packages, so the
compiler waits for the linter. Recorded as a `transitive:` allowlist entry,
which is a predicate rather than a date: the gate re-checks the blocker each
run and the entry clears itself when upstream ships support. First use of the
allowlist, and the loud banner now prints on every run here.
**pnpm 11 also made the gate wrong, and the gate was fixed rather than pnpm.**
pnpm 11 quarantines packages published inside a 24h window -- a supply-chain
measure against the period when a compromised publish is live -- and refused
@testing-library/react 16.3.3, published 18h earlier. Reporting a repo as
behind a version its package manager will not install is a finding nobody can
act on: the hook fails, and so does the fix it asks for. `quarantine.py` now
narrows npm's "latest" to the newest release old enough to install, bounded
above by `dist-tags.latest` so a maintenance-line release never outranks the
tag. It fetches the FULL packument (7MB for react, 16MB for typescript) and so
runs only after a finding already exists -- a clean run pays nothing, which a
test asserts by making the fetch raise.
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
`types-requests==2.33.0.20260712` was reported as "not exact-pinned" against
its own value: the regex capped the release segment at three components, and
PyPI stub packages date-stamp a fourth. PEP 440 places no such limit.
Found by running the gate over crdt-sync's newly pinned requirements.txt,
which is the only manifest in the fleet with a four-part pin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
Every CI workflow clones kuhyx/utils into `.utils/` *inside* the repo being
checked, so `--all` walked utils' own manifests and failed the consuming repo
for staleness in a directory that repo does not own and cannot fix.
Caught by punchme's very first CI run: locally `--all --strict` exited 0, and
the same command on the runner reported twenty findings, all of them in
`.utils/web_ui/package.json` and `.utils/ts_core/package.json`. Exactly the
kind of local/CI divergence the gate exists to prevent, in the gate itself.
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