Skip to content

fix: lint only changed canonical files on local branches - #304

Merged
dnth merged 4 commits into
mainfrom
fm/fm-lint-changed-local
Nov 13, 2026
Merged

dnth merged 4 commits into
mainfrom
fm/fm-lint-changed-local

Conversation

@dnth

@dnth dnth commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Ports upstream 833a9a25 (kunchenguid#1925) behavior: bin/fm-lint.sh now picks its file set from git context instead of always sweeping the full canonical set, so a local branch and the No-Mistakes gate stop repeating the full ShellCheck run that CI already owns.

 fm-lint.sh (no explicit paths)
-  full canonical set + --external-sources, on every branch
+  CI (GITHUB_ACTIONS/CI) | main | no merge-base
+    full canonical set + --external-sources
+  local branch with a merge-base
+    changed canonical files only, including tracked uncommitted edits
+    no --external-sources; exclude SC1091,SC2034,SC2153,SC2329
+  local branch with zero changed targets
+    "no changed lint targets" + exit 0, before any dependency check
   explicit paths and --list-files keep their full posture

Changed files:

  • bin/fm-lint.sh - context selection, conditional worker flags, zero-target return.
  • tests/fm-lint.test.sh - fixture-repo cases for changed mode, CI/main/no-merge-base full mode, zero targets without lint dependencies, explicit-path bypass, and exclusion behavior.
  • .no-mistakes.yaml, CONTRIBUTING.md, docs/configuration.md, .github/workflows/ci.yml - invoke the default context-selected mode and point at the header as the owner of the split.

Evidence

  • Before: every local run and the No-Mistakes lint step swept the full canonical set - 444 roots, about 270k lines; the No-Mistakes lint step took 1,630,548 ms (run 01M796S134JFXVZXT5JVHEMWAY).
    After: on this branch the local lint selects 2 changed canonical roots, and exits 0 in about 1 s with local changed-file mode; source following disabled, excluding SC1091,SC2034,SC2153,SC2329.

  • Before: no case proved zero changed targets exit cleanly when ShellCheck or Perl is absent, or when the ShellCheck version differs.
    After: test_zero_changed_files_prints_the_note_and_exits_zero proves all three dependency scenarios print no changed lint targets and exit 0, while the version-rejection test proves a wrong version still fails with a target present.

  • bash tests/fm-lint.test.sh - 19/19 ok, including changed-mode selection, full-mode parity for CI/GITHUB_ACTIONS/main/no merge-base, zero targets, explicit-path bypass, and real-ShellCheck exclusion behavior.

  • env -u CI -u GITHUB_ACTIONS bin/fm-lint.sh - changed mode, 2 roots, exit 0.
    bin/fm-lint.sh bin/fm-lint.sh tests/fm-lint.test.sh - full source-aware posture on the changed roots, exit 0.

  • gh-axi pr checks 304 - 20/20 checks pass, including Lint shell scripts; CI owns the full canonical sweep.

  • Source: upstream 833a9a25 (feat(bin): lint only the changed shard locally, full lint in CI kunchenguid/firstmate#1925, "lint only the changed shard locally, full lint in CI"), with the no---external-sources posture from follow-up a29cdcef (fix: reduce local ShellCheck source-analysis cost kunchenguid/firstmate#3778). Upstream partitions, --fast, and memory-bound machinery are not ported.

Merge Danger

Door: two-way - reverting restores the single full-sweep path; there is no data, schema, or state migration.

Blast Radius: firstmate lint gates only.

Local changed-file runs and the No-Mistakes lint step no longer report the four cross-file codes; CI still runs them over the full canonical set, so coverage is deferred, not dropped. --list-files stays the full inventory, so documented syntax sweeps do not narrow.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • 🚨 bin/fm-lint.sh:264 - AC3 requires: “A branch with no changed canonical files prints a ‘no changed lint targets’ note and exits 0.” The new zero-target exit occurs after dependency checks, so a docs-only branch without ShellCheck exits 127, with another ShellCheck version exits 1, or without Perl exits 127, without printing the required note. The conflicting guards are bin/fm-lint.sh:241 (ShellCheck availability), :248 (Perl availability), and :254 (version). Move the zero-target return before these checks while retaining them whenever targets exist. The zero-target test at tests/fm-lint.test.sh:245 currently supplies all dependencies and misses these cases.

🔧 Fix applied.
2 issues (1 error, 1 warning) still open:

  • 🚨 bin/fm-lint.sh:264 - AC3 requires: “A branch with no changed canonical files prints a ‘no changed lint targets’ note and exits 0.” The new zero-target exit occurs after dependency checks, so a docs-only branch without ShellCheck exits 127, with another ShellCheck version exits 1, or without Perl exits 127, without printing the required note. The conflicting guards are bin/fm-lint.sh:241 (ShellCheck availability), :248 (Perl availability), and :254 (version). Move the zero-target return before these checks while retaining them whenever targets exist. The zero-target test at tests/fm-lint.test.sh:245 currently supplies all dependencies and misses these cases.
  • ⚠️ bin/fm-lint.sh:241 - Round 1 correctly moved the empty-target exit before dependency checks, but left a sibling test assumption behind at tests/fm-lint.test.sh:487–490. On a docs-only local branch with a merge-base, running tests/fm-lint.test.sh now fails: its version-rejection test invokes lint without targets, receives the required successful empty-target result, and labels that a version-check failure. Give that test an explicit fixture path so version rejection is checked whenever lint targets exist; retain the empty-target behavior at bin/fm-lint.sh:241–243.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Local synthetic branches analyze only committed, staged, and unstaged canonical changes using either base reference ✅ pass live cli-transcript.log: local-origin and local-main-fallback
CI flags, main, and unrelated history retain full canonical source-aware analysis ✅ pass live cli-transcript.log: full-CI, full-GITHUB_ACTIONS, full-main, full-no-merge-base
A docs-only branch prints the empty-target note and succeeds despite unavailable lint dependencies ✅ pass live cli-transcript.log: empty-no-dependencies and empty-wrong-version-no-perl
Explicit paths bypass selection and retain full diagnostics while local mode suppresses cross-file findings ✅ pass live cli-transcript.log: explicit-unchanged, explicit-missing-perl, cross-file-local, cross-file-explicit
Inventory queries return the complete canonical set independently of selection mode ✅ pass live cli-transcript.log: inventory-local, inventory-CI, inventory-GITHUB_ACTIONS
  • TMPDIR="$PWD/.local-lint-validation/tmp" bash tests/fm-lint.test.sh — exited 0.
  • python3 .local-lint-validation/drive.py — exercised real CLI selection and fallback behavior with real ShellCheck 0.11.0.
  • Executed staged cross-file fixtures through default and explicit-path modes; verified actual diagnostic differences.
  • Removed disposable fixtures, confirmed clean Git status, and awaited every launched process.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 4 commits November 13, 2026 13:55
Port upstream 833a9a2 (kunchenguid#1925) behavior without its later partition,
--fast, or memory-bound machinery.

On an ordinary local branch with a real merge-base against origin/main
(or local main), bin/fm-lint.sh now lints only the canonical-set files
changed since that merge-base, including uncommitted edits, using plain
local git (no network, no gh). That local pass drops --external-sources
and excludes SC1091, SC2034, SC2153, and SC2329, the codes that need
library context; CI still reports them.

CI (GITHUB_ACTIONS=true or CI=true), the main branch, and a missing
merge-base keep the full canonical source-aware lint, so CI coverage
never depends on a local diff. Zero changed targets print a note and
exit 0. Explicit paths still bypass selection. --list-files stays the
full canonical inventory so the documented syntax sweep cannot narrow.

.no-mistakes.yaml now invokes the default context-selected mode and
drops the serial FM_LINT_JOBS=1 workaround, since changed-file mode no
longer inlines library closures. Headers, CI comments, and contributor
docs describe the split.
@dnth
dnth merged commit 4c6a538 into main Nov 13, 2026
21 checks passed
@dnth
dnth deleted the fm/fm-lint-changed-local branch November 13, 2026 06:32
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