fix(ci): lint and SAST the same files locally and in CI, and pin the two together - #11
Merged
Conversation
…two together
The pre-commit hooks were BROADER than CI, which is the dangerous direction. `ruff check` in CI named
six explicit paths; the ruff-check hook has no `exclude` and so runs on every changed Python file.
bandit in CI scanned `-r messagefoundry tee`; its hook scanned everything but tests/harness/samples.
So harness/ (73 findings), tee/ (12), samples/ and docker/ were gated locally and by NOTHING in CI.
Touching any file there failed the commit on errors no CI run could report and no green build had ever
been able to reveal. I hit it twice in one day: a harness import-sort during an unrelated flake fix,
and a one-line UP017 in scripts/ that I reverted rather than drag four unrelated bandit findings into
a release PR. A hook LOOSER than CI is an annoyance -- CI still catches it. A hook STRICTER than CI
blocks work on a standard the project does not enforce, and that is what makes people reach for
`--no-verify` and lose every other gate with it.
Scope now has ONE definition per tool instead of being re-stated, differently, in two files:
* ruff -- CI runs `ruff check .` (matching `ruff format --check .`, which was ALREADY repo-wide
while lint was not). pyproject's [tool.ruff] extend-exclude is the single source of truth.
* bandit -- CI runs `-r .` with an --exclude list identical to the hook's, and the hook's exclude
was widened to match. Widening surfaced 29 real findings that CI had never been able to see:
- 9 in scripts/ (subprocess in claim_check / vuln_metrics / assert_semgrep_handler_taint, one
urlopen, two try/except/continue in a hand-run Kerberos lab probe). All are established,
already-reviewed patterns -- two of them ALREADY carried `# noqa: S603`/`S310` for ruff's copy
of the same rules; bandit does not read noqa, so they now carry `# nosec` as well.
- 20 in packaging/messagefoundry-webconsole/tests/, literal test credentials (B105/B106). That
is the same "tests carry non-production idioms" case as tests/, one directory deeper, so the
exclude on BOTH sides now names it.
Recording a mistake worth not repeating: I first measured this as "0 findings, widening is free"
and said so. bandit was not installed in the worktree venv, `python -m bandit` failed, and the
JSON parse of its error swallowed the failure as an empty result set. The corrected run reports
`Total lines of code: 114206` -- a scanned-volume number the broken run never printed, and the
thing that distinguishes a clean scan from one that never ran.
99 findings cleared. 88 by safe auto-fix; the rest deliberately, because they were NOT interchangeable:
* B905 `zip()` without strict= -- three sites, three different answers, which is exactly why a blanket
`strict=False` would have been a fake fix. failover.py and sender.py build both operands from the
same source one line above, so strict=True is correct and will catch a future divergence that
zip's default would silently truncate. remote.py is the pairwise idiom `zip(x, x[1:])` where the
lengths differ BY DESIGN -- strict=True would raise on every non-empty input -- so it became
itertools.pairwise, which says "adjacent pairs" outright and drops the slice copy.
* SIM105 -- five sites ruff refused to auto-fix, all for the same reason: each carries a comment
(`# pragma: no cover`, "underlying socket already deleted", a two-line rationale) that the
contextlib.suppress transform would DESTROY. Two more would need a new import purely to satisfy a
style preference. Grandfathered per-line with the reason, which is the approach [tool.ruff.lint]
already documents for exactly this case.
docs/benchmarks/results/ is now ruff-excluded: those are archived measurement ARTIFACTS -- the exact
scripts a published number was produced with. Reformatting them edits the record, and a result you
cannot reproduce from the script filed beside it is worthless.
tests/test_lint_scope_parity.py reads both configurations and compares them, so narrowing one forces
narrowing the other. Mutation-verified in BOTH directions: re-narrowing CI's ruff fails it, and adding
an `exclude` to the ruff hook fails it. It asserts the CONTRACT, not a particular scope -- widen or
narrow freely, provided both sides move together.
Full suite green: 8914 passed, 818 skipped.
wshallwshall
added a commit
that referenced
this pull request
Aug 5, 2026
…sambiguate PR citations that already mis-resolve (#209) * fix(quality): the two advisory gates wrote a status glyph into their own summaries liveness.py and c901_delta.py each emitted a check mark on their clean-result line, and c901_delta rendered its complexity table with a non-ASCII arrow. Both write via sys.stderr.write when no summary file is given, and stderr defaults to backslashreplace, so a stock Windows cp1252 console silently mangled the text at exit 0 rather than raising. Verified by running the pre-edit scripts from HEAD under PYTHONIOENCODING=cp1252: neither crashed, both corrupted. That is the worse failure -- a crash is loud and self- reporting; silent corruption at exit 0 is not. CI was never affected, because the step summary is opened utf-8, so this was a local-run defect only. Say the word instead (CLAUDE.md section 11). PASS trails each sentence because that is where the glyph sat -- the minimal edit -- and it leaves the substring tests/test_c901_delta.py:291 asserts ("No function was introduced over the threshold") byte-identical. That assertion is a containment check rather than a prefix check, so it does not by itself forbid a leading token; nothing in the repo pins the placement either way. The three arrows become ASCII ->, matching lines 227 and 313 of the same file, which already wrote the identical relation that way. Both files now carry zero characters cp1252 cannot encode. * docs(quality): document /simplify, and take the status glyphs out of the rubric Code_Quality_Standards.md carried 40 check marks and one red circle as status markers, which CLAUDE.md section 11 forbids outside the two machine-parsed backlog files. All 41 are gone. Most sat beside the word they decorated and were simply deleted, that word carrying the meaning on its own; only two were genuine rewrites -- the red circle in Appendix A.2 became Failing, and the Appendix A.3 legend, where the glyph was the subject, became prose. Adds section 5.1 as the single home for /simplify: a local, human-invoked review that APPLIES its fixes rather than reporting them, which is why it runs before the local quartet rather than after -- running it after would mutate a tree the quartet just certified. It is not one of the five measurement gates, sits outside the AI companion section 6.5 local gate, and carries no Built status, because it ships with Claude Code rather than with this project and so leaves no artifact here to score. CLAUDE.md gains a "Before you verify" heading so the instruction is not governed by a pass-gate it cannot satisfy: /simplify applies edits and returns no verdict. Also corrects a PRE-EXISTING Appendix A.3 error that removing the glyphs surfaced rather than introduced: the legend glossed its status marker as advisory under two PR numbers, across a five-item list whose fifth item is blocking and shipped under a third. That legend is byte-identical in every commit this file has existed in. HANDOFF-mutation-coverage.md prescribed typing a check mark back into the rubric, which would have undone the pass -- so the removal was not durable. Its instruction is corrected, along with its stale signal numbering and column name, and it now says to write the word. * docs(quality): the handoff cited the PR that built the gates, not the one that restatused them The parenthetical said signals 7 and 8 were "restatused in v0.8 (#1040)". Two different pull requests, verified by subject: 7540f260 ci(quality): mutation (#7) + diff-coverage (#8) advisory gates (#1040) 46714159 docs(rubric): v0.8 -- restatus signals 7 + 8 to Built (#1044) PR #1040 built the gates; PR #1044 did the v0.8 restatus the sentence is about. Caught by the session sweeping citation ambiguity in these docs. Written in the settled "PR #NNN" form rather than a bare number, which is what that sweep is standardising. Bare and bolded #N stays for backlog items, and the distinction is load-bearing in code: backlog_status_check.py defines _CL_EXPLICIT as BACKLOG\s+#(\d+) to pick out item citations, so writing "BACKLOG #1040" for a pull request would make that regex misread it as an item. * docs(quality): the rubric cited pull requests as bare "#N", which already resolves to the wrong item #1020, #1028, #1040 and #1047 are PULL REQUEST numbers (97b79fef, 42756bdb, 7540f260, 3d6c8adf -- subject-anchored search). A bare "#N" in this corpus reads as a backlog item, so these send a reader to the wrong document. #1020 is not a future risk. Backlog item 1020 exists on main, filed 2026-08-04, about a first-run bootstrap Administrator with no email address -- while the rubric uses #1020 four times to mean the PyPI sdist private-doc leak fix. Those four citations already land on the wrong item. 1028 is next; 1040 and 1047 are still unallocated. "PR #N" is the settled repo form rather than a new one, and the repo encodes the distinction in code: backlog_status_check.py defines _CL_EXPLICIT to match the word BACKLOG followed by whitespace and a number, under the comment "Unambiguous CHANGELOG citations of a *backlog item* (not a PR number)". That regex is also why the inverse rewrite is unavailable -- prefixing a pull request number with that word would make the parser read it as an item. 39 markers inserted: 38 in the rubric, 1 in the handoff. The rubric's 40 four-digit citations carry 38 markers because one slash-joined run takes a single "PRs " across its three tokens. Deliberately left alone: the 11 rubric-signal citations (#3, #6, #7, #8, #9, #10, #11). Six of those numbers are also real backlog items, so the ambiguity is genuine, but resolving it is prose surgery ("signal 7", not "PR #7") and sits with the owner as a separate decision. Analysis and the marking script are the work of the session on claude/sleepy-villani-df328d. Verified here as markers-only: stripping every marker from both the committed and the working text yields identical files. * backlog: file 1029 -- the /simplify placement decision had no number to cite Filed closed: the documentation is the whole deliverable, and it shipped in the three commits below this one. REWRITTEN BEFORE FILING. The draft item, written when the change was first made, described a structure that the remediation then reverted -- it claimed a sixth row in the section 5 gate table, a Built status, and a CLAUDE.md bullet. None of those is what shipped: section 5's table is unchanged at five rows, Built is a claim the document explicitly declines to make, and CLAUDE.md carries a "Before you verify" heading placed ahead of the verification list rather than a bullet inside it. Every claim in the filed item was read from the working tree at 17c5212 rather than recalled. APPENDED, NOT INSERTED, and the instruction to insert was checked rather than followed. It was handed over on the grounds that 1028, 1030 and 1031 are being filed concurrently. No such rule exists and the file does not follow one: 108 items carry 10 descending adjacent pairs in file order, the tail running 1019, 1018, 1024, 1026, 1025, 1027; backlog_status_check.py enforces no ordering -- its only sort is a citation report at line 190; this file's own header states only that the numbered items are intentionally deferred, and docs/README.md calls them "ranked", which is not numeric order. So nothing states an ordering rule for the numbered items, and imposing numeric order would discard whatever the existing arrangement encodes. Appending is where every recent item sits. The only real interaction with the concurrent items is a textual end-of-file conflict that resolves by keeping both. Validated with the canonical parser rather than a hand-rolled scan (CLAUDE.md section 11): parse_items reports item 1029 with closed=['<check>'], open=[], is_open=False -- exactly one banner, no OPEN/CLOSED contradiction. The hygiene gate at its ci.yml invocation reports OK, 304 items, each declaring exactly one status. Number allocated via scripts/coord/alloc.ps1, never by grepping for the next free one. * docs: correct the CRLF rationale in this branch's earlier merge commit The merge commit 95cd856 states that docs/BACKLOG.md "is 100 percent CRLF" and that "a resolver that normalises to LF produces a clean-looking merge that churns every line". That is FALSE about the stored file, and this commit exists so the correction travels with the claim -- this repository composes its squash body from the concatenated commit messages, so both land on main together. Every committed revision of that file is pure LF. Measured on the blobs rather than the working tree: origin/main CRLF=0 bareLF=5045 453c95f CRLF=0 bareLF=4994 95cd856 CRLF=0 bareLF=5065 working tree CRLF=5065 bareLF=0 core.autocrlf = true CRLF exists only as the checkout materialisation. The original measurement read bytes on disk and reported them as the stored form -- the working tree answered a question about the object store. THE RESOLUTION ITSELF WAS CORRECT; only the stated reason was wrong. The resolver read and wrote with newline="", so it preserved the on-disk form byte-for-byte and let autocrlf normalise on the way in -- the same outcome a resolver that ignored line endings entirely would have produced. THE INSTRUMENT THAT SETTLES THIS IS CHURN, NOT A LINE-ENDING COUNT: git diff --numstat 453c95f 95cd856 -- docs/BACKLOG.md 71 0 docs/BACKLOG.md 71 added, zero removed -- items 1030, 1031 and 1032 plus one seam blank line. A resolver that had normalised would show thousands of lines on BOTH sides. That reconciles to the line against the independent resolution of the same collision on another branch, which came back 70/0 and needed no seam line. Anyone resolving the next end-of-file collision in this file should run the numstat check and should not chase line endings that are not there.
wshallwshall
added a commit
that referenced
this pull request
Aug 5, 2026
… surfaced 1033. The rubric cites its own eleven signals as bare #N, and six of those numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are closed items. Ten citations on four lines, re-measured against 780ee1d. Owner ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in the same file were fixed in PR #209; this is the short-number half that was deliberately left out of that scope. Two traps are recorded because each has already caught a reader. The #3 at L120 is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting it silently breaks the link, and a prior census listed it as a signal because it counted tokens without printing context. And L299/L319 use backslash-escaped forms: a grep attempt during this triage returned ZERO matches on a file that demonstrably contains them, and the empty result was believed until a self-tested pattern contradicted it. The item says to prove the pattern fires before trusting a count from it. 1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing refusing an admin's direct push to main, and since the cutover that push is publication -- so the one control has a silent off switch that depends on an environment variable. As of today the shim switches off three guards rather than one, the two added alongside it being the namespace allowlist and the tip-tree check. Filed with the adjacent gaps in the same class rather than separately: a fresh clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and --no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item states plainly that a client-side hook cannot be the sole control and that the durable answer is server-side, with the shim as defence in depth. Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next free one. Validated with parse_items rather than a hand-rolled scan: 114 items, zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner. Hygiene gate OK at 309 across both ledger files.
wshallwshall
added a commit
that referenced
this pull request
Aug 5, 2026
…d what it CARRIES (#213) * fix(hooks): the push guard asked where a push LANDS, and nothing asked what it CARRIES Two guards, both for paths the existing PROTECTED check waves through. GUARD A -- namespace allowlist. Refuse any push whose remote ref is outside refs/heads/ or refs/tags/. That is the shape of git push --mirror, which offers every ref in the clone including remote-tracking namespaces. A mirror push was refused before only INCIDENTALLY: it also offers local main as an update of refs/heads/main, so PROTECTED happened to fire. That is a property of one branch's state, not a rule, and it evaporates the moment main is up to date. GUARD B -- content check. Refuse a push whose ref's tip tree carries docs/security. That directory is gitignored, and an ignore rule governs only UNTRACKED paths, so it does nothing about a ref whose history already tracks those files. The path this closes is the likeliest of the set and is not a mirror at all: branch off a ref of that lineage and push it as an ordinary branch, which every other check here permits by design. PROVEN, not assumed. Both guards exercised via crafted pre-push stdin against a throwaway repo, with the fixture self-checked in both directions first (a fixture whose add -f lost to the ignore rule would make every assertion pass vacuously): case new old(HEAD) remote-tracking ref, mirror shape 1 0 ordinary branch, clean tip 0 0 ordinary branch, tip carries docs/security 1 0 tag push, clean tip 0 0 delete an unprotected branch 0 0 direct push to main 1 1 The old-guard column is the negative control: it returned 0 for exactly the two cases these guards add, so this is new coverage rather than restated behaviour. Refusals were checked to name the right reason, not merely to exit 1. WHAT THESE ARE NOT, stated in the code because the difference decides what a green run entitles anyone to conclude. Guard B reads the TIP TREE only -- a branch that added and then removed the files passes with a dirty history, so it is not a history check. It matches paths, not content. Every check here is skipped by --no-verify, by MEFOR_ALLOW_DIRECT_PUSH=1, and by the installed shim's own fail-open, which prints "THE PUSH GUARD IS OFF for this push" and exits 0 when python does not resolve. A fresh clone or new worktree has no hook at all until install-git-hooks.ps1 runs. A client-side hook cannot be the sole control and the docstring says so. Also fixes a false docstring in the test file, which asserted that git push --all sends every ref. It does not -- --all is refs/heads only, while bundle create --all and rev-list --all mean every ref. That belief is what makes someone treat --all and --mirror as interchangeable. 109 tests pass; ruff and mypy clean. * docs(ledger): record the vault-ref cleanup, and retire a warning that was true when written 489 refs carrying docs/security content were deleted from this clone on 2026-08-05 with git update-ref -d, across THREE namespaces: refs/remotes/vault (20), refs/remotes/vaultall (466), and refs/vault (3). That third sits outside refs/remotes entirely and held the newest, densest content, so a cleanup scoped to refs/remotes would have missed it. THE STANDING WARNING AGAINST THIS IS NOW STALE, NOT WRONG. LEDGER-GATE.md and alloc.ps1 both named "deleting its refs" as the hazard the allocator ratchet defends against. Re-measured directly: BACKLOG max is 1032 and sub-floor max 353 both with and without the refs, ADR max 0161 either way, the allocator emits max+1 and never fills gaps, and the ratchets already persist 1031 / 1000 / 160. The warning was accurate when written, in the era when the floor did depend on the ref sweep; the ratchet and the public-boundary split made it independent since. It is updated rather than deleted, because the principle it teaches still holds. THE MULTISESSION PLAN GAVE A COMMAND THAT NO LONGER WORKS, and its description of the ref was wrong when written. It called vault/main a remote-tracking ref; git rev-parse --symbolic-full-name resolved it to refs/vault/main, and refs/remotes/vault/main never existed. Nor was a remote named vault ever configured -- only origin. The refs were orphaned namespaces from two direct-URL fetches on 2026-07-28, 45 seconds apart. Sessions should read the vault ledger from the separate MessageFoundry-vault clone instead. REVERSIBILITY, since deleting refs is only safe if it is undoable. A manifest of 489 refname/SHA pairs (464 unique commits -- 25 refs share a tip) is held outside this repo, durably, inside the vault clone's own .git. The objects remain addressable here, and every tip is REACHABLE from the vault clone's own refs, so they are gc-safe there rather than merely undeleted. gc.auto is set to 0 in this clone: it was unset with 7060 loose objects against a default threshold of 6700, already over, so a routine command could have fired an auto-gc and converted a reversible ref deletion into permanent loss. NOTHING WAS EVER PUBLISHED FROM THESE REFS. origin/main, all 30 origin refs and all 195 local branches carry zero docs/security files at tip and in history, confirmed three independent ways; the two graphs share no merge base. * backlog: file 1033 and 1034 -- the two follow-ups the push-guard work surfaced 1033. The rubric cites its own eleven signals as bare #N, and six of those numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are closed items. Ten citations on four lines, re-measured against 780ee1d. Owner ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in the same file were fixed in PR #209; this is the short-number half that was deliberately left out of that scope. Two traps are recorded because each has already caught a reader. The #3 at L120 is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting it silently breaks the link, and a prior census listed it as a signal because it counted tokens without printing context. And L299/L319 use backslash-escaped forms: a grep attempt during this triage returned ZERO matches on a file that demonstrably contains them, and the empty result was believed until a self-tested pattern contradicted it. The item says to prove the pattern fires before trusting a count from it. 1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing refusing an admin's direct push to main, and since the cutover that push is publication -- so the one control has a silent off switch that depends on an environment variable. As of today the shim switches off three guards rather than one, the two added alongside it being the namespace allowlist and the tip-tree check. Filed with the adjacent gaps in the same class rather than separately: a fresh clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and --no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item states plainly that a client-side hook cannot be the sole control and that the durable answer is server-side, with the shim as defence in depth. Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next free one. Validated with parse_items rather than a hand-rolled scan: 114 items, zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner. Hygiene gate OK at 309 across both ledger files.
wshallwshall
added a commit
that referenced
this pull request
Aug 7, 2026
Retiring a backlog item moves it verbatim from docs/BACKLOG.md into
docs/archive/backlog/BACKLOG-CLOSED.md. Every citation that named the live file
keeps pointing at a file the item is no longer in. The link still resolves, so
nothing in CI can see it. #1094 fixed two such markers in CLAUDE.md section 12;
this is the same defect at repo scale.
73 citations across 34 files, href-only. No prose was rewritten. Visible labels
changed ONLY where leaving them would contradict the target -- a label reading
`BACKLOG.md` pointing at the archive -- and then only to `BACKLOG-CLOSED.md`.
THE TEST IS "DOES THE CITED FILE CONTAIN THE ITEM", NOT "IS THE ITEM CLOSED".
Those differ, and keying on closure would corrupt correct citations: #1073 is
closed and still legitimately in the live ledger. Item locations came from
parse_items imported from scripts/docs/backlog_status_check.py, per CLAUDE.md
section 11 -- never a hand-rolled scan of the banner alphabet.
DELIBERATELY NOT TOUCHED, each for a stated reason:
Both ledger files -- ZERO edits to docs/BACKLOG.md and BACKLOG-CLOSED.md.
Only two sites named them and both are excluded, so this change costs no
conflict against the merge trains or the pending #1096 filing. The one real
site (#322 at BACKLOG.md:2720) is left because the file is the most
contended in the repo and the item number is visible in plain text a search
away.
docs/CONNECTIONS.md:2436 -- the #27 serial/ASTM row. Already fixed on a branch
inside merge train #274. Sweeping it from origin/main would re-fix stale
text and collide.
QUOTATIONS OF THE DEFECT. docs/BACKLOG.md:6319, inside #1094, reads "Two of
its markers CITED [`docs/BACKLOG.md`](BACKLOG.md) #26 and #27" -- past
tense, describing rot that is already fixed. Repointing it would corrupt a
historical record. A regex cannot tell this from a live pointer, which is
the reason this was not done with sed.
THE WRONG-NUMBER CLASS, which is a different defect and must not be swept into
this one. ADR 0068:10 cites #11 and ADR 0113:9 cites #239; both numbers are
absent from the live ledger, but the ARCHIVE's #11 ("`check` dry-run
cross-products") and #239 ("Re-measure Steps view estate coverage") are
unrelated to WebAuthn passkeys and to a Windows tray manager respectively.
Repointing would convert a vague reference into a confidently wrong one that
lands the reader on the wrong item. Left, and reported.
MIXED-LOCATION LINKS, where one link covers items in both files so no single
target is correct: docs/AI-OFF-MATRIX.md:50 (six items), docs/adr/0001:13
(#1 archived, #3 live), THROUGHPUT-IMPROVEMENTS.md:215 (#62 live, so its
link is already correct).
VERIFICATION
- Plan applied by literal replacement on the named line only, requiring the
quoted string to occur EXACTLY ONCE there; a mismatch aborts rather than
fuzzy-matching. Dry run: 73/73 clean, 0 problems, before anything was
written.
- All 35 distinct anchor fragments introduced match exactly one real "## N."
heading in the archive, checked after applying, with a known-bad fragment
run through the same check to prove it can report a miss. Fragments were
derived with a slugger that does NOT collapse consecutive spaces -- the
doubled hyphens are correct, not typos.
- All 74 archive hrefs in the changed files resolve to the archive from their
own directory depth; the relative prefix differs by depth and was computed
per file, not pattern-matched.
- Coverage confirmed with a DELIBERATELY LOOSER regex than the one that built
the work list: it finds exactly one wrong-file site outside this change set,
docs/CONNECTIONS.md:2436, which is the intended exclusion.
- backlog_status_check.py: OK, 365 items. No mixed line endings introduced.
All 34 changed files are markdown; diff is 67 insertions / 67 deletions,
line-for-line.
- Staged by explicit path from the plan, cross-checked against git's modified
set, so nothing another session is editing was swept in.
Not included: the broken-href class (13 sites, mostly (docs/BACKLOG.md) written
from inside docs/testing/master-test-plan/), the 12 line anchors past EOF, and
the 31 in-range anchors that drifted onto unrelated text. Those are separate
classes under #1095 and are catchable by a link checker, which this repo still
does not run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The pre-commit hooks were broader than CI — the dangerous direction.
ruff checkin CI named six explicit paths; theruff-checkhook has noexclude, so it runs on every changed Python file. bandit in CI scanned-r messagefoundry tee; its hook scanned everything excepttests/harness/samples.So
harness/(73 findings),tee/(12),samples/anddocker/were gated locally and by nothing in CI. Touching any file there failed the commit on errors no CI run could report, and that no green build had ever been able to reveal. I hit it twice in one day: aharness/import-sort during an unrelated flake fix, and a one-lineUP017inscripts/that I reverted rather than drag four unrelated bandit findings into a release PR.A hook looser than CI is an annoyance — CI still catches it. A hook stricter than CI blocks work on a standard the project doesn't enforce, and that's what makes people reach for
--no-verifyand lose every other gate with it.Scope now has one definition per tool
ruff check ., matchingruff format --check ., which was already repo-wide while lint was not.pyproject's[tool.ruff] extend-excludeis the single source of truth.-r .with an--excludelist identical to the hook's; the hook's exclude was widened to match.99 ruff findings cleared — 88 auto, 11 deliberately
They were not interchangeable:
B905—zip()withoutstrict=. Three sites, three different answers, which is exactly why a blanketstrict=Falsewould have been a fake fix.failover.py/sender.pybuild both operands from the same source one line above →strict=True, which will catch a future divergence that zip's default would silently truncate.remote.pyis the pairwise idiomzip(x, x[1:])where lengths differ by design —strict=Truewould raise on every non-empty input → becameitertools.pairwise.SIM105— five sites ruff refused to auto-fix, all for the same reason: each carries a comment (# pragma: no cover,"underlying socket already deleted", a two-line rationale) that thecontextlib.suppresstransform would destroy. Two more would need a new import purely for a style preference. Grandfathered per-line with the reason — the approach[tool.ruff.lint]already documents for this case.docs/benchmarks/results/is now ruff-excluded: archived measurement artifacts, the exact scripts a published number came from. Reformatting them edits the record.Widening bandit surfaced 29 findings CI had never seen
scripts/— subprocess, one urlopen, twotry/except/continuein a hand-run Kerberos lab probe. All established reviewed patterns; two already carried# noqa: S603/S310for ruff's copy of the same rules, and bandit doesn't readnoqa, so they now carry# nosectoo.packaging/messagefoundry-webconsole/tests/— literal test credentials (B105/B106). Same "tests carry non-production idioms" case astests/, one directory deeper; both excludes now name it.A mistake worth recording
I first measured this as "0 findings, widening is free" and said so. bandit wasn't installed in the worktree venv —
python -m banditfailed, and my JSON parse of its error swallowed the failure as an empty result set. The corrected run reportsTotal lines of code: 114206, a scanned-volume number the broken run never printed. That number is what distinguishes a clean scan from one that never ran, which is the same failure this PR exists to remove.Drift prevention
tests/test_lint_scope_parity.pyreads both configurations and compares them, so narrowing one forces narrowing the other. Mutation-verified in both directions: re-narrowing CI's ruff fails it, and adding anexcludeto the ruff hook fails it. It asserts the contract, not a particular scope — widen or narrow freely, provided both sides move together.Full suite green: 8914 passed, 818 skipped.
🤖 Generated with Claude Code