Skip to content

feat(standards): scope linting to changed files, with a hygiene opt-in - #165

Merged
twistedmelonman merged 1 commit into
mainfrom
claude/feat-standards-changed-since-1f470ab5
Sep 11, 2026
Merged

feat(standards): scope linting to changed files, with a hygiene opt-in#165
twistedmelonman merged 1 commit into
mainfrom
claude/feat-standards-changed-since-1f470ab5

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

What was broken

standards-check failed PRs for lint debt in files the author never opened.

Measured across the fleet, that is the dominant failure mode. Of 54 runs, 24 failed — and every one was markdownlint and/or shellcheck. yamllint, actionlint, zizmor and node-floor have never failed once. The markdownlint failures are concentrated in three whitespace rules: MD032 blanks-around-lists (46), MD031 blanks-around-fences (36), MD049 emphasis-style (30) — 112 of 119 findings.

Concretely: dev-env CI run 34383919816 failed on docs/plans/2026-08-27-workflow-sha-pinning.md, a file that commit never touched. The commit only added docs/local-code-review-options.md.

Why the existing auto-fix does not cover it

The global pre-commit hook already runs markdownlint --fix, and it works. Verified against the exact file that failed claude-config CI run 34548182880: --fix repaired the MD032 violation and exited 0.

It cannot help here, because a staged-file hook cannot fix a file the commit never staged. The gap is scope, not tooling.

How it was fixed

--changed-since REF narrows the four linters that enumerate through _tracked — shellcheck, yamllint, zizmor, markdownlint — to the files a branch changed, plus new untracked files. The changed set is intersected with the existing tracked-file enumeration, so .gitignore handling and deleted files keep exactly one answer.

actionlint and the Node-floor check find their own inputs and stay whole-repo. Both are cheap and neither has ever failed on this fleet.

Whole-repo remains the behaviour without the flag. In CI:

  • a pull_request run narrows to the PR's changes by default
  • adding the standards:hygiene label and pushing forces a whole-repo sweep, for a deliberate hygiene round
  • any non-pull_request trigger sweeps, since there is no base to diff against

Two false-OK paths closed explicitly

Both were live risks given this codebase's history of checks that report success while doing nothing:

  • an unresolvable --changed-since ref exits 2 rather than computing an empty changed set, which would let every linter pass over zero files
  • a base-SHA fetch failure in CI falls back to a whole-repo sweep, not to an empty diff — noisy but honest, rather than a green check that linted nothing

The run also logs its scope (scope: files changed since <sha> / scope: all tracked files), so the coverage of a green check is recoverable from its log. That is the dev-env#113 complaint.

Verification

Four new assertions, 15/15 passing, shellcheck -S info clean.

The tests pin both directions, because a flag that suppressed everything would satisfy the first assertion alone:

  • control: without the flag, the known-bad fixture must still fail
  • a branch must not inherit debt from a file it did not touch
  • a branch must still fail on violations in a file it did touch — and the untouched file must be absent from the output
  • an unresolvable ref exits 2

The fixture disables hooks via core.hooksPath. Without that the test measures the environment instead of the flag: global branch protection refuses the commit on main, and the global markdownlint --fix hook silently repairs the deliberately-bad fixture. Both were observed doing exactly that during development.

Post-merge step required

Callers pin @standards-check-v1, which is a manually moved tag. Merging this PR changes nothing fleet-wide until that tag is retagged onto the merge commit. Without that step the PR looks done and every repo keeps the old behaviour.

Advances smartwatermelon/dev-env#113.

https://claude.ai/code/session_01TkReZXv8XkcWNbcaiWfcvg

Whole-repo linting failed PRs for debt in files their author never opened.
Measured across the fleet, that was the largest single source of
standards-check failures: 24 of 24 failures were markdownlint and/or
shellcheck, and the markdownlint ones were dominated by three whitespace
rules (MD032 blanks-around-lists, MD031 blanks-around-fences, MD049
emphasis-style) in untouched files.

The local pre-commit hook already runs `markdownlint --fix` and works
correctly — verified against the exact file that failed CI run
34548182880. It cannot help here, because a staged-file hook cannot fix a
file the commit never staged. The gap is scope, not tooling.

--changed-since REF narrows the four linters that enumerate through
_tracked (shellcheck, yamllint, zizmor, markdownlint) to the files a branch
changed, plus new untracked files. actionlint and the Node-floor check find
their own inputs and stay whole-repo; neither has ever failed on this fleet.

Whole-repo remains available and is still the default without the flag. In
CI a PR narrows by default; a deliberate hygiene sweep is requested with the
`standards:hygiene` label, and any non-pull_request trigger sweeps because
there is no base to diff against.

Two false-OK paths are closed explicitly. An unresolvable --changed-since
ref exits 2 rather than computing an empty changed set, which would let
every linter pass over zero files. A base-SHA fetch failure in CI falls back
to a whole-repo sweep rather than to an empty diff — noisy but honest,
instead of a green check that linted nothing.

Tests pin both directions: a branch must not inherit debt from files it did
not touch, and must still fail on violations in files it did. The dirty-file
assertion also checks that the untouched file is absent from the output, so
a flag that suppressed everything would fail. Hooks are disabled in the
fixture via core.hooksPath, because the author's global branch protection
refuses the commit and the global markdownlint --fix hook silently repairs
the deliberately-bad fixture.

15/15 tests pass; shellcheck -S info clean.

Advances smartwatermelon/dev-env#113.

Claude-Session: https://claude.ai/code/session_01TkReZXv8XkcWNbcaiWfcvg
@twistedmelonman
twistedmelonman merged commit a446889 into main Sep 11, 2026
3 checks passed
@twistedmelonman
twistedmelonman deleted the claude/feat-standards-changed-since-1f470ab5 branch September 11, 2026 01:24
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.

1 participant