fix(deps): regenerate lock, fix ruff 0.16.6 findings, guard against future drift - #41
Conversation
The lock was stuck at ruff==0.15.20 while requirements-dev.txt declared 0.16.6 (dependabot #22, #32), so CI's pip install --require-hashes -r requirements-dev.lock never installed the version the source file bumped to. Regenerated with the exact command recorded in the lock's own header: uv pip compile requirements-dev.txt --generate-hashes --universal --python-version 3.11 -o requirements-dev.lock. Verified in a fresh venv: pip install --require-hashes -r requirements-dev.lock installs coverage==7.16.0, pytest==9.1.1, ruff==0.16.6, matching requirements-dev.txt exactly. Fixes #39 (partial: lock/tool drift). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI installed ruff from the drifted lock (0.15.20), so the 0.16.6 rules
requirements-dev.txt already declared never ran. Root-cause fixes, no
noqa and no rule disabling in pyproject/ruff config:
- SIM115 (subagent_usage.py, transcript.py): merge open() into the with
statement instead of a bare fh = open(...) guarded by try/finally.
- PLW1510 (measure_refine_overhead.py, test_refine_gate.py,
test_release_tools.py, test_statusline_layout.py): add explicit
check=False to subprocess.run calls that inspect returncode themselves
and intentionally tolerate nonzero exits.
- I001/RUF100/FURB122 (oklch2srgb.py, test_measure_refine_overhead.py,
test_portable_packaging.py, test_statusline_transcript.py,
test_subagent_usage.py): ruff check --fix for import sorting, an
unused noqa, and fh.write loops replaced with fh.writelines.
- EXE001 (transcript.py, oklch2srgb.py): chmod +x. Both carry a
documented direct-invocation usage line; the shebang was already
correct, only the executable bit was missing.
Boy-scout (coding-standards.md section 4/14): the SIM115 rewrite of
parse_transcript_usage pushed subagent_usage.py past the nesting-depth
cap, and touching measure_refine_overhead.py surfaced a pre-existing
cap violation in collect_prompts. Both extracted into small per-record
helpers (_keyed_usage_record plus _dedupe_by_message, _extract_prompt)
instead of suppressing the check.
Excludes plugins/context-guard/hooks/{stop-context-guard.py,
subagent-tracker.py} and tests/test_context_guard_hooks.py: PR #40
already fixes and formats these.
ruff check plugins tests (0.16.6): 0 findings outside the excluded
files (verified after this commit). tests/test_subagent_usage.py:
18 passed.
Fixes #39 (partial: check findings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whitespace/wrapping only. AST identity verified with ast.dump()
sha256 before/after for every file this change touches:
checkpoint_protocol.py d8580ec5ac68b969... == d8580ec5ac68b969...
refine_gate.py 2ecd82bd5d017134... == 2ecd82bd5d017134...
subagent_usage.py (already conforming after the lint commit)
transcript.py 00b15845a3e03a60... == 00b15845a3e03a60...
oklch2srgb.py 3bf05ec6c2f4fcc6... == 3bf05ec6c2f4fcc6...
test_measure_refine_overhead.py 5341f190... == 5341f190...
test_refine_gate.py 1cd22849c777a1da... == 1cd22849c777a1da...
test_release_tools.py c2a7efb4e3d79189... == c2a7efb4e3d79189...
test_statusline_layout.py 8067ac60... == 8067ac60...
test_statusline_transcript.py b0f5db46b508... == b0f5db46b508...
test_subagent_usage.py 00153473a94361a0... == 00153473a94361a0...
(full sha256 pairs computed against a from-scratch reconstruction:
git-blob original -> reapply the lint fixes from the prior commit ->
hash -> ruff format -> hash again; identical both times for all 11
files.)
Note: for 9 of the 11 files above, the lint-fix commit that precedes
this one already carried ruff-format's output (a scratch-reconstruction
mixup applied format before the copy-back), so this commit's diff is a
no-op for those files and only reformats checkpoint_protocol.py and
refine_gate.py, which had no check findings. ruff format --check
plugins tests: clean outside plugins/context-guard/hooks/{stop-context-
guard.py,subagent-tracker.py} and tests/test_context_guard_hooks.py
(PR #40's scope).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two gaps let #22/#32/#37/#38 merge green while CI kept testing the lock's stale pins: ruff check ran without ruff format --check next to it, and nothing compared requirements-dev.txt against requirements-dev.lock. - Add a "ruff format --check plugins tests" CI step alongside the existing "ruff check" step. - Add tools/check-lock-drift.py: parses top-level name==version pins out of both files and fails with the exact mismatch (package, .txt version, .lock version) when they disagree. Wired into CI right after the lock install step. Deterministic: no subprocess re-compile, no network, no wall-clock dependency, comparable to the check the issue itself used to diagnose the drift. - tests/test_check_lock_drift.py covers the parser and the drift comparison directly, plus main()'s exit codes, plus a live check that the real requirements-dev.{txt,lock} pair in this repo is current. Investigated whether dependabot could regenerate the lock itself instead of a CI guard: it cannot, for this repo's file layout. dependabot-core's pip-compile lockfile matcher hard-requires the lockfile name to end in `.txt` (python/lib/dependabot/python/pip_compile_file_matcher.rb, `return false unless name.end_with?(".txt")`, https://github.com/dependabot/dependabot-core/blob/main/python/lib/dependabot/python/pip_compile_file_matcher.rb), and even then needs either a `--output-file=<name>` pragma in the lockfile matching pip-compile's own header, or a same-basename `.in` manifest (https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference, "pip and pip-compile" section, https://raw.githubusercontent.com/github/docs/main/data/reusables/dependabot/supported-package-managers.md). requirements-dev.lock has neither: it is a `.lock` file, produced by `uv pip compile -o requirements-dev.lock` (not `--output-file=`), from a source file that is itself named `.txt` rather than `.in`. Making dependabot manage this lock natively would require renaming requirements-dev.txt -> requirements-dev.in and requirements-dev.lock -> requirements-dev.txt, which changes the CI install path and is a separate, deliberate decision, not a drive-by rename bundled into this fix. This CI guard is the substitute until that decision is made. Gate evidence: check-lock-drift.py against the real files exits 0; a scratch copy with `ruff==0.16.6` desynced to `0.15.20` exits 1 and names the exact mismatch. Full suite: 80 passed, coverage 93% (fail_under 80). Fixes #39. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixing the drift guard in CI (dd60499) was a substitute for the real gap: dependabot could never regenerate the old requirements-dev.lock, so every future bump would keep needing a manual lock rebuild. Move to the naming dependabot-core's uv ecosystem actually recognizes for a compiled lockfile. requirements-dev.txt (human-edited manifest) -> requirements-dev.in. requirements-dev.lock (uv-compiled, hash-locked) -> requirements-dev.txt, recompiled with: uv pip compile requirements-dev.in --generate-hashes --universal --python-version 3.11 --output-file=requirements-dev.txt which embeds a --output-file=requirements-dev.txt pragma in the header. Re-read uv/lib/dependabot/uv/requirements_file_matcher.rb at a pinned dependabot-core commit before making this change: https://github.com/dependabot/dependabot-core/blob/f3a79fa0711aca171b2174b855d9f21b16237961/uv/lib/dependabot/uv/requirements_file_matcher.rb compiled_file?(file) requires the candidate lockfile to end in .txt, then matches it to its manifest via EITHER the --output-file= pragma above OR a same-basename .in file. requirements-dev.txt now satisfies both. Also read uv/lib/dependabot/uv/file_fetcher.rb at the same commit: its own required_files_message is "Repo must contain a requirements.txt, uv.lock, requirements.in, or pyproject.toml" -- confirming this plain .in/.txt pair needs no pyproject.toml or uv.lock to be picked up by the uv ecosystem. Changed .github/dependabot.yml's pip entry to package-ecosystem: "uv": the pip ecosystem's own matcher (python/lib/dependabot/python/pip_compile_file_matcher.rb) has the identical .txt-only rule, but its resolver shells out to pip-tools' pip-compile, not uv, and would not reproduce this lock's format. The docs table lists uv as v0.11 supported (https://raw.githubusercontent.com/github/docs/main/data/reusables/dependabot/supported-package-managers.md); the installed compiler here is uv 0.11.3. Updated every reference to the old names: .github/workflows/ci.yml and release.yml install steps, tools/check-lock-drift.py (+ its test, now comparing .in vs .txt), README.md, CONTRIBUTING.md, .bestpractices.json. Grepped the tree for both old filenames afterward; only remaining hit is a historical "used to install from ... requirements-dev.lock" sentence in check-lock-drift.py's own docstring, describing the bug this fixes. Added --no-deps to every --require-hashes install site touched by this commit, per this repo's no-deps-gate hook (ADR-1062): a fully hash-pinned closure should not ask pip to re-derive dependencies. Gates: fresh venv install from the new requirements-dev.txt with --require-hashes --no-deps gives coverage==7.16.0, pytest==9.1.1, ruff==0.16.6 (as requirements-dev.in declares). Drift guard: 0 on the real .in/.txt pair, 1 with the exact mismatch on a scratch copy desynced to ruff==0.15.20. Full suite: 80 passed. ruff check and ruff format --check plugins tests: same 8/3 findings as before this commit, all in PR #40's three excluded files. Verified PR #40 (fix/context-guard-smoke-and-structure) at its current tip aec0f18: ruff check and ruff format --check are both clean under 0.16.6 for all three files this PR excludes. The 4 findings measured earlier in tests/test_context_guard_hooks.py are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5fed5bd to
6c359f3
Compare
cdeust
left a comment
There was a problem hiding this comment.
Summary
Verified at the source (not from the PR description): the rebased tip (6c359f3) fixes #39 correctly; the .in/.txt rename genuinely enables dependabot's uv ecosystem to manage the compiled lock (confirmed against dependabot-core@f3a79fa0 RequiremenstFileMatcher#compiled_file?, Uv::FileFetcher, and Python::SharedFileFetcher#required_files_in?/#requirements_file?), the pure-format commit is independently AST-identical (ast.dump() sha256 recomputed from scratch, matches the disclosed hashes for all 10 files), --no-deps installs cleanly with pip check reporting no broken requirements, and no stale requirements-dev.lock reference remains outside a docstring's historical description of the bug. However, one commit labeled as a mechanical lint fix silently changed exception-handling behavior in a way that turns a loud crash into a silently-zeroed cost report, and the new CI drift guard has two real parsing gaps; one of which is exactly the "guard silently passes a real drift" failure mode this review was asked to hunt for, confirmed by mutation testing against the shipped test suite. Verdict: REQUEST CHANGES.
Stakes classification (Move 7)
High. Touches .github/workflows/ci.yml/release.yml (hash-locked supply chain), .github/dependabot.yml, and a 24-file / ~1300-line diff. Not requesting a split: the pure-format commit is independently verified AST-identical, and the format-in-lint-commit mixing is disclosed in both the commit message and PR body.
Move 0: Ledger reconciliation and seen-defect check
- PR body enumerates every commit's scope and rationale; the exclusion of
plugins/context-guard/hooks/{stop-context-guard.py,subagent-tracker.py}andtests/test_context_guard_hooks.pyis not a bare "out of scope"; it cites PR #40 owning those files and reports an independent re-check of PR #40's tip (aec0f18) confirmingruff check/ruff format --checkare clean there. That's a verified scope boundary, not a bypass. No un-issued "pre-existing/unrelated" rationalization found. - One material omission: the PR body describes the
8a53bd8lint commit purely in terms of ruff rule IDs (SIM115, PLW1510, I001, RUF100, FURB122, EXE001) with no mention that the SIM115 rewrite insubagent_usage.pychanged what exceptions are caught (see Blocking #1). This isn't a dismissed defect (the author likely didn't notice it), so it doesn't trigger the hard REFUSED path, but it must be fixed before merge. - Verdict short-circuit: N/A, proceeding to Moves 1-6.
Layer check (Move 1)
No layer violations. tools/check-lock-drift.py is a standalone CLI script with no imports into plugins/; the plugin-side lint fixes don't cross any plugin boundary.
SOLID audit (Move 2)
subagent_usage.py:_keyed_usage_record/_dedupe_by_messageextraction (boy-scout, to stay under the nesting-depth cap) is a clean SRP split; parsing vs. folding. No objection to the refactor shape itself.measure_refine_overhead.py:_extract_promptextraction is the same pattern, same verdict.- No OCP/LSP/ISP/DIP issues found in the diff.
Wiring & contract drift (Move 3)
- New public-ish symbols
_keyed_usage_record,_dedupe_by_message,_extract_promptare all wired (called fromparse_transcript_usage/collect_promptsrespectively). tools/check-lock-drift.pyis new and wired intoci.yml(Verify requirements-dev.txt matches requirements-dev.instep).- No dead code, no bare TODOs, no debug prints found in the diff.
- Contract drift: see Blocking #1;
parse_transcript_usage's documented postcondition ("zeroed if the file is missing/unreadable") no longer matches its actual behavior (also zeroed on malformed per-record usage data), and no caller/test was updated to reflect it.
Test adequacy (Move 4)
tests/test_check_lock_drift.pycovers the happy path, a version mismatch, a missing pin, comment/hash-line skipping, and bothmain()exit codes; good baseline coverage of stated behavior.- Mutation-tested
tools/check-lock-drift.pyin a scratch copy (4 targeted mutants + 1 probe) against the actual shippedtests/test_check_lock_drift.py: !=→==at the mismatch comparison: killed (4 tests fail).- delete the
is Nonebranch: killed (test_pin_missing_from_lock_is_reportedfails). return 1→return 0on drift: killed (test_main_exits_nonzero_on_driftfails).- drop
.lower()in_parse_pins: SURVIVED; zero tests fail. The shipped suite never exercises case-insensitive or separator-normalized matching; this is exactly the mutation-testing signal that names a missing test (Move 4 §3.2 suite-strength requirement). - No test exists for
subagent_usage.py's new exception-scope behavior (Blocking #1); the change is both undisclosed and untested.
Complexity & structure (Move 5)
No size-limit violations. All extracted helpers are well under the 50-line function cap; check-lock-drift.py is 92 lines total. No over-engineering smells.
Security & hygiene (Move 6)
- No security smells.
--no-depsaddition on every--require-hashesinstall site is a genuine hardening (matches ADR-1062 cited in the commit message) and was verified: fresh venv install from the newrequirements-dev.txtwith--require-hashes --no-depssucceeds andpip checkreports no broken requirements (macOS/arm64/py3.14 used as a proxy for CI's ubuntu/py3.11; a missing transitive under--no-depsfails loudly at import/resolve time rather than silently, so this is a reasonable proxy, not a gap). - Commit hygiene: conventional prefixes throughout, each commit independently gated and disclosed, no secrets, no stray binaries.
Issues
Blocking
-
plugins/context-guard/tools/subagent_usage.py(introduced in8a53bd8,parse_transcript_usage); the "mechanical" SIM115 fix (movingopen()into awithstatement) also merged the exception scope: previouslytry: fh = open(...) except (OSError, TypeError, ValueError): return Usage()guarded only the open call, and the record loop ran under a separate baretry/finallythat let aValueError/TypeErrorfromint()inside_ctx_magnitude(e.g. a transcript record with"input_tokens": "abc") propagate. Now both are under oneexcept (OSError, TypeError, ValueError), so that same malformed record silently produces a zeroedUsage()for the entire transcript instead of raising. Reproduced directly: old code raisesValueError: invalid literal for int() with base 10: 'abc'; new code returnsUsage(input_tokens=0, ...)with no error. This contradicts the function's own docstring postcondition ("zeroed if the file is missing/unreadable"; not "if a record is malformed") and turns a detectable data-quality bug into a silent cost-under-report. Not covered by any test intests/test_subagent_usage.py. Required change: either narrow theexceptback to wrapping only theopen()call (restore the crash-on-malformed-data behavior), or if the wider swallow is intentional, document the new postcondition explicitly and add a test asserting it. -
tools/check-lock-drift.py:31,42(_PIN_RE,_parse_pins); extras syntax (pkg[extra]==1.0) is not matched by_PIN_RE([breaks the name-character class), so such a line is silently dropped from both the manifest and lock parse. Empirically confirmed: manifestcoverage[toml]==7.16.0vs. lockcoverage[toml]==5.0.0(a real, large version drift) yieldsfind_drift() == []and the script prints "matches every pin" / exits 0. This is the exact "guard silently passes a real drift" failure mode this review was asked to hunt for. No current pin inrequirements-dev.inuses extras, so it isn't live today, but the guard's entire purpose is to fail loud on drift, and this is a hole in that guarantee for any future extras-pinned dev dependency. Required change: strip an optional\[[^\]]*\]before matching==(e.g. extend_PIN_REor pre-strip extras in_parse_pins), plus a regression test with an extras-pinned package desynced between the two files. -
tools/check-lock-drift.py:42(_parse_pins); PEP 503 name normalization is incomplete: only.lower()is applied, so-vs_(and.) are treated as different package identities.Foo-Bar==2.0in.invsfoo_bar==2.0in.txt(same package, same version) is reported as "declared but absent from the lock" rather than recognized as matching; a false positive rather than a false negative, so it fails loud rather than silently passing drift, but it's still a defect in a diagnostic gate whose value is precision. Confirmed via mutation testing: removing the existing.lower()call from_parse_pinsdoes not fail a single test in the shippedtests/test_check_lock_drift.py; there is no case-insensitivity test at all, so the mutation survives. Required change: normalize withre.sub(r"[-_.]+", "-", name).lower()per PEP 503 in_parse_pins, plus tests for case and separator variants.
Non-blocking
tools/check-lock-drift.pyonly checks manifest→lock (every.inpin present and matching in.txt); it does not flag a package present in.txtbut absent from.in. This is correct scoping, not a gap; a compiled--universallock legitimately carries transitive dependencies (iniconfig,packaging,pluggy,pygments) that never appear in.in, so a reverse check would false-positive on every compile. Worth a one-line docstring note that this is a deliberate one-directional check, so a future reader doesn't "fix" it into a false-positive machine.tools/check-lock-drift.py's docstring still says "compares... every direct dependency"; technically true only for pins the regex actually parses; consider footnoting the extras caveat once #2 above is fixed, so the contract stays honest.
Hand-offs
- None required. The two guard-parsing fixes and the exception-scope fix are small, root-cause, in-repo changes; no architectural or cross-cutting rework needed.
Verdict
REQUEST CHANGES.
Minimum set to unblock merge:
- Fix (or explicitly document + test) the exception-scope widening in
subagent_usage.py::parse_transcript_usageintroduced by8a53bd8. - Fix
_PIN_RE/_parse_pinsintools/check-lock-drift.pyto handle extras syntax without silently dropping the pin from comparison, with a regression test. - Add PEP 503 name normalization (
-/_/case) to_parse_pins, with tests; the current suite's zero-test-failure result on a.lower()-removal mutant demonstrates this path is unverified today.
Everything else in the PR (the rename's dependabot-core compatibility, the AST-identical formatting, --no-deps correctness, and the stale-reference cleanup) checks out against the source and is ready to merge once the above three are addressed.
Three blocking findings from a fresh code review, each fixed with a test that is red before the fix and green after. 1. plugins/context-guard/tools/subagent_usage.py::parse_transcript_usage (SIM115 rewrite in 8a53bd8): the with-statement rewrite widened the except (OSError, TypeError, ValueError) to cover the whole record loop, so a malformed per-record usage payload (e.g. input_tokens: "abc") silently returned a zeroed Usage() instead of raising. Restored the original exception scope, guarding only open(), by extracting _open_transcript(path) so the open() call still satisfies SIM115 (it is the head expression of its own with statement) while the record loop's ValueError/TypeError propagates as before. Test test_parse_raises_on_malformed_record_usage: raw output confirms red against 8a53bd8's subagent_usage.py (prints the zeroed Usage, no raise) and green against this commit (pytest.raises(ValueError) passes). 2. tools/check-lock-drift.py _PIN_RE/_parse_pins: extras syntax (pkg[extra]==1.0) broke the name-character class, silently dropping the pin from both sides of the comparison. coverage[toml]==7.16.0 in the manifest vs. coverage[toml]==5.0.0 in the lock is real, > 1 major version drift; before this fix find_drift() reported nothing and the guard printed "matches every pin", exit 0 -- precisely the silent-drift failure mode this tool exists to prevent. _PIN_RE now accepts an optional [..] extras group. Also added _parse_manifest_pins: every non-blank, non-comment, non-option (-e/-r/...) line in requirements-dev.in that _PIN_RE still can't match now raises ValueError naming the file line, instead of being silently skipped; main() catches it and exits 1 with the message. Raw before/after on the extras repro and on an unparseable-manifest-line repro are in this commit's test run output (tests/test_check_lock_drift.py). 3. Same file, _parse_pins/_parse_manifest_pins: name comparison was case-only (.lower()), so Foo-Bar and foo_bar were treated as different packages -- a false "absent from the lock" for the identical PEP 503 identity at the same version. Added a _normalize() helper implementing PEP 503 (https://peps.python.org/pep-0503/#normalized-names): re.sub(r"[-_.]+", "-", name).lower(). Mutation-tested: removing the .lower() call fails 4 tests, removing the re.sub separator collapse fails 2 tests (test_normalize_lowercases, test_normalize_collapses_separators, test_desynced_separator_variant_is_recognized_as_the_same_package, test_separator_variant_with_real_drift_is_still_caught) -- both mutants independently killed, both restored to the correct implementation before this commit. Non-blocking: added the one-directional-by-design note to check-lock-drift.py's module docstring (manifest -> lock only; a compiled --universal lock legitimately carries transitives like iniconfig, packaging, pluggy, pygments that never appear in the manifest, so the reverse direction would false-positive on every compile). Boy-scout: chmod +x tools/gen-bundle-sbom.py. Seen while running this task's own gate command (ruff check/format --check ... tools, the first time tools/ was in scope for either), same class of fix as the two files corrected the same way in 0d992fe (documented direct-invocation usage, shebang already correct, only the executable bit missing). Gates, fresh venv from requirements-dev.txt with --require-hashes --no-deps (coverage==7.16.0 pytest==9.1.1 ruff==0.16.6): full suite 103 passed. ruff check plugins tests tools and ruff format --check plugins tests tools: both clean. check-lock-drift.py: 0 on the real requirements-dev.in/.txt pair, 1 with the exact mismatch on a scratch ruff==0.15.20 desync and on a coverage[toml] extras desync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cdeust
left a comment
There was a problem hiding this comment.
Re-verification of commit 098c738 (fresh scratch repros, not trusting the commit message)
All three prior blocking findings are fixed. Verdict: APPROVE (pending the CI-scope note below, which is non-blocking).
1. subagent_usage.py exception scope
_open_transcript(path) returns a bare open() handle (no with inside it), narrowly guarded by except (OSError, TypeError, ValueError): return None. The caller does fh = _open_transcript(path); if fh is None: return Usage(); with fh: ....
This is legitimate, not a SIM115 dodge:
ruff check --select SIM115on the file: all checks passed. The rule doesn't fire because the function's only job is to open-and-return; nothing runs betweenopen()succeeding and the caller enteringwith fh:, so there is no window where the handle exists outside a context manager while doing work.- Leak check: wrapped
opento capture the handle instance, forced the malformed-record path. Handle's.closedisTrueafter theValueErrorpropagates, confirmingwith fh:closes it on the exception path. - Behavior repro, red before / green after, reproduced independently:
2. check-lock-drift.py extras / hard-error / markers
Fresh scratch repros against tools/check-lock-drift.py at 098c738:
== extras drift repro (coverage[toml] 7.16.0 vs 5.0.0) ==
lock.txt is out of date with in.txt:
- coverage: requirements-dev.in pins 7.16.0, requirements-dev.txt pins 5.0.0
exit=1
== unparseable manifest line ("this is not a pin line") ==
unparseable requirements-dev.in line 1: 'this is not a pin line'
exit=1
== env marker still handled (colorama ; sys_platform == 'win32') ==
lock3.txt matches every pin in in3.txt.
exit=0
== dash/underscore normalization (Foo-Bar vs foo_bar, same version) ==
lock4.txt matches every pin in in4.txt.
exit=0
All four match the commit's claims exactly.
3. PEP 503 normalization mutation-killed
Re-ran mutation testing against the shipped tests/test_check_lock_drift.py (18 tests, all passing on the real code):
- Drop
.lower()from_normalize(keep only separator collapse): 4 tests fail (test_normalize_lowercases,test_normalize_collapses_separators,test_desynced_separator_variant_is_recognized_as_the_same_package,test_separator_variant_with_real_drift_is_still_caught); matches the commit message exactly. - Drop the
re.subseparator collapse from_normalize(keep only.lower()): 2 tests fail (test_normalize_collapses_separators,test_desynced_separator_variant_is_recognized_as_the_same_package); matches exactly. - Also mutated the extras regex group away entirely: 4 tests fail (
test_parse_pins_handles_extras,test_parse_manifest_pins_handles_extras,test_extras_drift_is_detected_not_silently_passed,test_extras_with_multiple_names_and_marker). - Also replaced the strict-manifest
raise ValueErrorwith a silentcontinue: 2 tests fail (test_manifest_unparseable_line_raises,test_main_reports_unparseable_manifest_line), and it visibly reproduces the exact "guard silently passes a real drift" failure mode this whole review chain exists to close (prints "matches every pin" with a genuinely unparseable line present).
Every mutant that should die, dies. Full suite: 103 passed. ruff check/ruff format --check on plugins tests tools: all clean.
CI scope: tools/ is not linted
.github/workflows/ci.yml runs ruff check plugins tests and ruff format --check plugins tests; tools/ (where the new drift guard lives) is outside both. The commit's own gate evidence ran ruff check/format --check against plugins tests tools locally, and I confirmed ruff check plugins tests tools and ruff format --check plugins tests tools both pass cleanly right now, at zero cost to add.
Recommendation (non-blocking, but worth doing in this PR or a fast follow-up): add tools to both CI lint/format steps. tools/check-lock-drift.py is the safety-critical guard this whole issue is about; a future edit that reintroduces one of the mutants above (or a plain formatting drift) would ship unnoticed by CI's own lint gate, caught only if someone happens to run the test suite against a scratch-desynced pair. The chmod +x tools/gen-bundle-sbom.py boy-scout fix in this commit is fine as-is (documented direct-invocation usage, correct shebang, matches the same EXE001 pattern from 0d992fe).
No modifications made to the branch beyond this verification pass (scratch repros only, working tree restored to 098c738 clean).
tools/ holds check-lock-drift.py, the guard this PR adds; it sat outside CI's own ruff gate. Both commands are clean on tools/ today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Fixes #39. CI installed ruff==0.15.20 from requirements-dev.lock while
requirements-dev.txt declared ruff==0.16.6 (dependabot #22, #32), so every
dependabot pip bump since #22 merged green without the declared linter ever
running in CI.
PLW1510, I001, RUF100, FURB122, EXE001), root cause each time. No
blanket noqa, no rule disabling in config. Two fixes (subagent_usage.py,
measure_refine_overhead.py) needed a small extraction to stay under the
nesting-depth cap once fixed.
(ast.dump() sha256, before and after) verified for every one of them.
Note: because of a scratch-reconstruction sequencing mistake, the format
output for 9 of 11 files landed in the same commit as their lint fixes
(0d992fe) rather than in the dedicated format commit (28d42d6), which
only reformats the 2 files that had no lint findings. AST identity was
verified regardless.
requirements-dev.txt (human-edited manifest) is now requirements-dev.in;
the old requirements-dev.lock (uv-compiled, hash-locked) is now
requirements-dev.txt, recompiled with
uv pip compile requirements-dev.in --generate-hashes --universal
--python-version 3.11 --output-file=requirements-dev.txt, which embeds
a --output-file= pragma naming itself. This is the exact pair
dependabot-core's uv ecosystem matcher requires
(uv/lib/dependabot/uv/requirements_file_matcher.rb, pinned at
https://github.com/dependabot/dependabot-core/blob/f3a79fa0711aca171b2174b855d9f21b16237961/uv/lib/dependabot/uv/requirements_file_matcher.rb):
a .txt lockfile matches its manifest via either that pragma or a
same-basename .in file; requirements-dev.txt now has both.
.github/dependabot.yml's pip entry is now package-ecosystem: "uv" (its
own file_fetcher.rb confirms a plain .in/.txt pair needs no
pyproject.toml or uv.lock: "Repo must contain a requirements.txt,
uv.lock, requirements.in, or pyproject.toml"). Dependabot can now
regenerate requirements-dev.txt itself.
"ruff check" step, and tools/check-lock-drift.py, now a belt-and-
suspenders guard between a hand edit and the next dependabot run rather
than a substitute for one. Covered by tests/test_check_lock_drift.py.
--no-deps, per this repo's own no-deps-gate hook (ADR-1062): a fully
hash-pinned closure should not ask pip to re-derive dependencies.
and release.yml install steps, tools/check-lock-drift.py and its test,
README.md, CONTRIBUTING.md, .bestpractices.json. Grepped the tree
afterward; the only remaining hit is a historical sentence in
check-lock-drift.py's own docstring describing the bug this fixes.
PR #40 status (measured, not assumed)
Checked PR #40 (fix/context-guard-smoke-and-structure) at its current tip
aec0f18 directly under ruff 0.16.6: ruff check and ruff format --check are
both clean on all three files this PR excludes
(plugins/context-guard/hooks/stop-context-guard.py, subagent-tracker.py,
tests/test_context_guard_hooks.py). The 4 findings measured in an earlier
push (tests/test_context_guard_hooks.py: 1 I001, 3 PLW1510) are gone.
Rebasing this branch onto main after #40 merges should be mechanical;
neither #40's diff nor its ruff state should require anything further
here.
Dependabot PRs #37/#38
Both (dependabot/pip/coverage-7.16.1, dependabot/pip/ruff-0.16.8) touch
only the old requirements-dev.txt path under the old "pip" ecosystem
entry. After this PR merges, requirements-dev.txt no longer plays that
role (it's the compiled lock) and the pip ecosystem entry is gone, so
these two PRs cannot be rebased; they need to be closed. Dependabot will
propose equivalent updates against requirements-dev.in under the new "uv"
ecosystem entry on its next scheduled run. No comment posted on either
PR per instructions.
Test plan
--require-hashes --no-deps prints the versions requirements-dev.in
declares
copy and passes on the real .in/.txt pair (raw output in the commit
messages)
clean under 0.16.6 on every file this PR touches
verify and re-engage)
🤖 Generated with Claude Code