Skip to content

fix(deps): regenerate lock, fix ruff 0.16.6 findings, guard against future drift - #41

Merged
cdeust merged 7 commits into
mainfrom
fix/dev-lock-drift
Sep 26, 2026
Merged

cdeust merged 7 commits into
mainfrom
fix/dev-lock-drift

Conversation

@cdeust

@cdeust cdeust commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

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.

  • Fixed every ruff check plugins tests finding under 0.16.6 (SIM115,
    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.
  • ruff format (0.16.6) applied to the files this PR touches. AST identity
    (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.
  • Fixed the drift at its source rather than only guarding against it.
    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.
  • Added a "ruff format --check plugins tests" CI step next to the existing
    "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.
  • Every --require-hashes install site touched by this PR now also carries
    --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.
  • Every reference to the old filenames updated: .github/workflows/ci.yml
    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

  • Fresh-venv install from the new requirements-dev.txt with
    --require-hashes --no-deps prints the versions requirements-dev.in
    declares
  • tools/check-lock-drift.py fails on a deliberately desynced scratch
    copy and passes on the real .in/.txt pair (raw output in the commit
    messages)
  • ruff check plugins tests and ruff format --check plugins tests
    clean under 0.16.6 on every file this PR touches
  • Full suite: 80 passed, coverage 93% (fail_under 80)
  • CI green after rebase onto main once fix(context-guard): repair README smoke test, decompose oversized hooks, bump 2.0.2 #40 merges (orchestrator to
    verify and re-engage)

🤖 Generated with Claude Code

cdeust and others added 5 commits September 26, 2026 10:19
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>

@cdeust cdeust left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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} and tests/test_context_guard_hooks.py is 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) confirming ruff check/ruff format --check are 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 8a53bd8 lint commit purely in terms of ruff rule IDs (SIM115, PLW1510, I001, RUF100, FURB122, EXE001) with no mention that the SIM115 rewrite in subagent_usage.py changed 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_message extraction (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_prompt extraction 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_prompt are all wired (called from parse_transcript_usage/collect_prompts respectively).
  • tools/check-lock-drift.py is new and wired into ci.yml (Verify requirements-dev.txt matches requirements-dev.in step).
  • 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.py covers the happy path, a version mismatch, a missing pin, comment/hash-line skipping, and both main() exit codes; good baseline coverage of stated behavior.
  • Mutation-tested tools/check-lock-drift.py in a scratch copy (4 targeted mutants + 1 probe) against the actual shipped tests/test_check_lock_drift.py:
  • != → == at the mismatch comparison: killed (4 tests fail).
  • delete the is None branch: killed (test_pin_missing_from_lock_is_reported fails).
  • return 1 → return 0 on drift: killed (test_main_exits_nonzero_on_drift fails).
  • 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-deps addition on every --require-hashes install site is a genuine hardening (matches ADR-1062 cited in the commit message) and was verified: fresh venv install from the new requirements-dev.txt with --require-hashes --no-deps succeeds and pip check reports no broken requirements (macOS/arm64/py3.14 used as a proxy for CI's ubuntu/py3.11; a missing transitive under --no-deps fails 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 in 8a53bd8, parse_transcript_usage); the "mechanical" SIM115 fix (moving open() into a with statement) also merged the exception scope: previously try: fh = open(...) except (OSError, TypeError, ValueError): return Usage() guarded only the open call, and the record loop ran under a separate bare try/finally that let a ValueError/TypeError from int() inside _ctx_magnitude (e.g. a transcript record with "input_tokens": "abc") propagate. Now both are under one except (OSError, TypeError, ValueError), so that same malformed record silently produces a zeroed Usage() for the entire transcript instead of raising. Reproduced directly: old code raises ValueError: invalid literal for int() with base 10: 'abc'; new code returns Usage(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 in tests/test_subagent_usage.py. Required change: either narrow the except back to wrapping only the open() 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: manifest coverage[toml]==7.16.0 vs. lock coverage[toml]==5.0.0 (a real, large version drift) yields find_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 in requirements-dev.in uses 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_RE or 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.0 in .in vs foo_bar==2.0 in .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_pins does not fail a single test in the shipped tests/test_check_lock_drift.py; there is no case-insensitivity test at all, so the mutation survives. Required change: normalize with re.sub(r"[-_.]+", "-", name).lower() per PEP 503 in _parse_pins, plus tests for case and separator variants.

Non-blocking

  • tools/check-lock-drift.py only checks manifest→lock (every .in pin present and matching in .txt); it does not flag a package present in .txt but absent from .in. This is correct scoping, not a gap; a compiled --universal lock 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:

  1. Fix (or explicitly document + test) the exception-scope widening in subagent_usage.py::parse_transcript_usage introduced by 8a53bd8.
  2. Fix _PIN_RE/_parse_pins in tools/check-lock-drift.py to handle extras syntax without silently dropping the pin from comparison, with a regression test.
  3. 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 cdeust left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 SIM115 on the file: all checks passed. The rule doesn't fire because the function's only job is to open-and-return; nothing runs between open() succeeding and the caller entering with fh:, so there is no window where the handle exists outside a context manager while doing work.
  • Leak check: wrapped open to capture the handle instance, forced the malformed-record path. Handle's .closed is True after the ValueError propagates, confirming with fh: closes it on the exception path.
  • Behavior repro, red before / green after, reproduced independently:
    • Before (8a53bd8): su.parse_transcript_usage(path) on a record with "input_tokens": "abc" returns a zeroed Usage(), no exception. RED.
    • After (098c738): same input raises ValueError: invalid literal for int() with base 10: 'abc'. GREEN, matches origin/main's pre-8a53bd8 behavior exactly.

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.sub separator 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 ValueError with a silent continue: 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>
@cdeust
cdeust marked this pull request as ready for review September 26, 2026 08:56
@cdeust
cdeust merged commit 2fa1905 into main Sep 26, 2026
3 checks passed
@cdeust
cdeust deleted the fix/dev-lock-drift branch September 26, 2026 09:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI lint runs ruff 0.15.20 from the lock while requirements-dev.txt declares 0.16.6

1 participant