requirements.txt used >= floors and installed with no hashes, so every CI run and
install re-resolved to whatever PyPI served. A poisoned release of any dep
(fastapi, aiohttp, lxml, pillow...) would land unverified.
- requirements.lock: 42 pkgs pinned to exact versions + SHA256 (1258 hashes), uv
(targeted py3.11 to match CI image)
- Makefile install + CI: pip install --require-hashes -r requirements.lock
- requirements.txt stays the loose source; make pins-lock regenerates
Validated: lock installs under --require-hashes, 460 tests pass under resolved versions.