fix: keep a whole-project check out of gitignored paths - #172
fix: keep a whole-project check out of gitignored paths#172thecodedrift wants to merge 1 commit into
Conversation
`check` reported prose findings from inside directories git ignores. The case that surfaced it was a git worktree at `worktrees/<name>/` — a complete second checkout, so every Vale rule fired again over another branch's documents and the finding count moved when a worktree appeared or disappeared with nothing explaining why. Only Vale was wrong. ast-grep's walker is the `ignore` crate and `sgWalkArgv` passes `--no-ignore hidden` without `vcs`, so a bare scan already skipped ignored trees; that is now documented and pinned by test rather than left as an incidental property of the argv. Vale has no notion of a VCS, so it is handed git's own answer as `--glob` exclusions. The set comes from `git ls-files --others --ignored --exclude-standard --directory -z` — the complement of the tracked set, because `--directory` collapses a wholly-ignored directory to one entry and the result stays short enough to pass as flags. No new dependency: git already folds in nested ignore files, `.git/info/exclude`, the global excludes file and negation patterns. The exclusion belongs to the walk we chose. An explicit path is still checked, a non-git directory falls back to the existing walk, and standing inside an ignored directory — where git answers `./` — ignores nothing rather than returning an empty check. Fixes #166 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
|
@claude /review |
|
Claude finished @thecodedrift's task in 3m 12s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
AssessmentThis is a well-scoped, well-documented fix. Walked the whole diff line by line, traced
One inline finding posted, non-blocking:
No correctness, security, or test-adequacy issues found beyond that. |
| @@ -214,8 +238,18 @@ export async function runVale( | |||
| // its walker declined to open, so once the glob has done its job the skipped | |||
| // files are unrecoverable from the output — and a fix whose only visible | |||
| // effect is that some findings are quietly missing is the bug it replaced. | |||
| // | |||
| // The ignored paths are filtered back out for the same reason the notice | |||
| // exists at all: it must describe the run that happened. An `.adoc` inside | |||
| // `worktrees/` is not a file this run declined to convert — it is a file this | |||
| // run was never going to look at, and naming it would send the reader to | |||
| // investigate a directory the fix above deliberately excluded. | |||
| const converterDependent = await findConverterDependentFiles( | |||
| options.cwd, | |||
| paths | |||
| ); | |||
There was a problem hiding this comment.
[New] Minor efficiency nit: listGitIgnoredEntries (a git subprocess) and findConverterDependentFiles (an fs glob walk) are independent — neither's result feeds the other, only the final .filter(...) combines them — but they're awaited sequentially:
const ignoredEntries = wholeProject
? await listGitIgnoredEntries(options.cwd)
: [];
...
const converterDependent = await findConverterDependentFiles(options.cwd, paths);Running them concurrently (Promise.all) would shave the git spawn + directory walk latency off every whole-project Vale invocation instead of paying for both in series before Vale itself even starts. Not a correctness issue, just avoidable added latency on a path that already spawns a Vale subprocess afterward.
What
A whole-project
taskless checkno longer reports findings from paths git ignores. An explicitly named path is still checked, and a non-git directory still gets the walk that shipped before.Why
checkreported prose findings from inside gitignored directories. The reported case was a git worktree atworktrees/<name>/— a complete second checkout — so every Vale rule fired a second time over another branch's documents, including code an agent was mid-edit on. The finding count then moves when a worktree appears or disappears with nothing in the output explaining why. The general shape is the same fordist/, vendored trees, and scratch directories: a check reporting on files nobody maintains.Only one engine was actually wrong
Worth flagging, because the issue says "every rule fires a second time" and that turns out to be half right.
ast-grep's walker is the
ignorecrate, andsgWalkArgvhas always passed--no-ignore hiddenwithoutvcs— so a bare scan already skipped ignored trees. Re-measured against the pinned 0.41.0: nothing under a gitignoredworktrees/probe/, and nothing under a hidden-and-ignored.turbo/either, which is the case that would expose--no-ignore hiddenif it had ever disabled more than hidden-file skipping. Vale has no notion of a VCS and walked everything.So the two static engines disagreed about which files the project contains, and only the prose findings duplicated — which matches the issue's own reproduction, where all three extra findings were the
docs-npx-cliVale rule.Nothing in
scan.tschanges behavior. What changes is that the property is now documented as load-bearing and pinned by a test, so a later--no-ignore vcscannot hand the ignored tree back to the scan silently.Measured
On a fixture repository (
mixed-engines-project, twosgrules and two Vale rules) withworktrees/gitignored andgit worktree add worktrees/probepresent:check, no worktreecheck, worktree presentcheck worktrees/probeThe two findings that leave are both Vale, both a second copy of a finding already reported against the tracked file. Both ast-grep findings were already absent from the ignored copy before the change.
Mechanism
git ls-files --others --ignored --exclude-standard --directory -z, run once per whole-project Vale invocation, rendered into Vale's single--globalternation..gitignoreis not one file or one syntax question once nested ignore files,.git/info/exclude, a globalcore.excludesFileand negation patterns are involved. A library approximates that; git is it,checkalready shells out to git elsewhere, and it answers in one call. No new dependency.--cached --othersis every file in the project — thousands of positional arguments and anARG_MAXproblem on a large repository.--directorycollapses a wholly-ignored directory to one entry, sonode_modules/is one line rather than forty thousand.-z. A filename may legally contain a newline; without it git quotes and C-escapes such a path and a line split yields two entries naming nothing..taskless/exclusion and via the sameisWholeProjectWalkpredicate — socheck .counts as whole-project andcheck worktrees/probedoes not.gitonPATH, git errors. The engines then walk exactly as before.build/, git answers./— "everything here" — and honouring that would return an empty check with nothing saying why. Today Vale's matcher happens not to matchREADME.mdagainst./**, so this is a hardening of a behavior that was accidentally correct, not a bug fix.buildValeGlobjoins with,inside!{…}, so one comma would split into two wrong patterns. Dropping such an entry means one pathologically-named ignored path is still linted — which is what shipped before, so it is a gap, not a regression. The converter skip notice uses literal string matching and has no such restriction.Also
The converter skip notice no longer names files inside ignored paths. An
.adocunderworktrees/is not a file this run declined to convert — it is a file this run was never going to open, and naming it would send the reader to investigate a directory the exclusion exists to keep out.Verification
pnpm typecheck,pnpm lint,pnpm test(840 tests) all pass. The new bare-walk assertion was confirmed non-vacuous by reverting the source and rebuilding: it fails, and only it fails.Fixes #166