Skip to content

fix(utils): require a project marker before ordinary-word dirs exclude - #1763

Open
TalexDreamSoul wants to merge 1 commit into
masterfrom
fix/1727-dev-dir-policy
Open

fix(utils): require a project marker before ordinary-word dirs exclude#1763
TalexDreamSoul wants to merge 1 commit into
masterfrom
fix/1727-dev-dir-policy

Conversation

@TalexDreamSoul

@TalexDreamSoul TalexDreamSoul commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes the last two acceptance criteria of #1727. The first two — root-anchoring SYSTEM_BLACKLISTED_DIRS, and keeping ~/Library / %APPDATA% excluded — landed in #1728 and #1731.

The decision #1727 asked for

A decision recorded on whether development names apply under user document roots.

Not by name alone. node_modules is nobody's document folder and keeps excluding wherever it appears. build, dist, out, bin, target, coverage, logs, tmp, temp and cache are ordinary English words, and they now exclude only where a project marker (package.json, Cargo.toml, go.mod, Makefile, .git, *.csproj, …) sits beside the directory.

That is the signal ripgrep, fd and VS Code use for the same question, and it is free here: scanDirectoryInto has already called readdir on the parent before it descends, so the sibling list is in hand. One map, no extra syscall.

path before after
~/Documents/build skipped, silently indexed
~/Documents/build/2026 skipped, silently indexed
~/Movies/tmp, ~/Pictures/cache, ~/Documents/logs skipped indexed
~/Projects/app/dist (next to package.json) skipped skipped
~/Documents/x/node_modules skipped skipped
/usr/bin, /var/tmp skipped skipped

The half a leaf-name-only fix would have missed

PATH_PATTERNS.DEV_PATHS carries /build\// and CACHE_PATHS carries /\/tmp\// — unanchored, so they match an ancestor segment. Relaxing the leaf name alone would have made ~/Documents/build indexable while everything under it stayed excluded: a fix that passes a shallow test and still loses the user's files.

Those two pattern sets are therefore skipped for callers that supply a sibling list. Such a caller is walking top-down and has already had the per-level rule applied at every ancestor, and every name the patterns carry is either in DEV_BLACKLISTED_DIRS / TEMP_BLACKLISTED_DIRS or starts with a dot, so nothing is lost. SYSTEM_PATHS stays unconditional — it is the one of the three that catches what the per-level name rule cannot, /usr/bin being exactly that case once bin is relaxed.

Blast radius

TraversalContext is optional and only scanDirectoryInto passes it. Index upserts, manual adds and the file-level containing-path check read as "cannot tell" and keep the pre-#1727 answer, so they are byte-for-byte unchanged. undefined and [] are deliberately distinct: never-looked vs. read-and-found-nothing.

One reason string changes: /usr/bin was reported as development-path and is now system-path. No caller branches on the value — every one of them tests === null or truthiness.

Verification

  • packages/utils full suite: 189 files, 1378 passed, 1 skipped
  • 14 new tests in file-filter-project-context.test.ts, both directions
  • pnpm lint (the utils CI gate) clean
  • Negative control: with the new constants kept but the service logic reverted to HEAD, the 4 tests that encode the fix fail and the 10 that guard unchanged behaviour pass. The tests fail on the bug, not just pass on the fix.

Fixes #1727

Summary by CodeRabbit

  • Bug Fixes

    • Improved directory scanning to recognize project context using nearby project files and metadata.
    • Reduced false exclusions for ordinary directories that share names with development or cache folders.
    • Preserved strict exclusions for system paths, hidden entries, bundles, dependency folders, and configured blacklists.
    • Maintained consistent filtering when project context is unavailable, including nested paths and case variations.
  • Tests

    • Added comprehensive coverage for project-aware filtering and traversal scenarios.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a19825b-b1ff-4bca-9b60-721ef7758d2f

📥 Commits

Reviewing files that changed from the base of the PR and between b8e1224 and 96d09be.

📒 Files selected for processing (5)
  • packages/utils/__tests__/file-filter-project-context.test.ts
  • packages/utils/__tests__/file-filter-traversal-anchor.test.ts
  • packages/utils/common/file-filter-service.ts
  • packages/utils/common/file-scan-constants.ts
  • packages/utils/common/file-scan-utils.ts

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The file scanner now passes sibling entry names to directory filtering. Context-dependent blacklist names are excluded when project markers are present, while unconditional exclusions remain active. Tests cover marker variants, nested paths, case-insensitivity, context-free callers, and empty sibling context.

Changes

Project-context directory filtering

Layer / File(s) Summary
Filtering contracts and project-marker logic
packages/utils/common/file-scan-constants.ts, packages/utils/common/file-filter-service.ts
Defines context-dependent directory names and project markers. FileFilterService applies sibling-aware leaf and path filtering.
Recursive sibling-context propagation
packages/utils/common/file-scan-utils.ts
Collects current directory entry names and passes them to recursive getTraversalExclusionReason calls.
Context-dependent filtering coverage
packages/utils/__tests__/file-filter-project-context.test.ts, packages/utils/__tests__/file-filter-traversal-anchor.test.ts
Covers project markers, unconditional exclusions, context-free behavior, empty sibling context, and clarified strict filtering behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 96d09

This change narrows ordinary-word directory exclusions to project-marked locations while preserving system, cache, and dependency exclusions; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant scanDirectoryInto
  participant FileSystem
  participant FileFilterService
  scanDirectoryInto->>FileSystem: Read directory entries
  FileSystem-->>scanDirectoryInto: Return sibling names
  scanDirectoryInto->>FileFilterService: Evaluate path with siblingNames
  FileFilterService-->>scanDirectoryInto: Return exclusion reason
  scanDirectoryInto->>scanDirectoryInto: Recurse with sibling context
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: ordinary-word directories now require a project marker before exclusion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1727-dev-dir-policy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploying tuff with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2ecd318
Status: ✅  Deploy successful!
Preview URL: https://72d11124.tuff-dso.pages.dev
Branch Preview URL: https://fix-1727-dev-dir-policy.tuff-dso.pages.dev

View logs

`build`, `dist`, `out`, `bin`, `target`, `coverage`, `logs`, `tmp`, `temp` and
`cache` were matched on the leaf name at any depth, so a folder a user created
under an index root was skipped for being named an ordinary English word. The
check runs before `readdir`, so nothing errored, nothing incremented
`errorCount`, and the file was simply absent from search -- indistinguishable
from a file that does not exist.

They now exclude only where the caller can show a project marker
(`package.json`, `Cargo.toml`, `.git`, ...) sits beside the directory: the same
signal ripgrep, fd and VS Code use for this question, and free here because the
traversal has already read the parent before it descends. `node_modules` is
nobody's document folder and keeps excluding everywhere.

`PATH_PATTERNS.DEV_PATHS` / `CACHE_PATHS` restate those names as unanchored
substrings that match an *ancestor* segment, so relaxing the leaf name alone
would have fixed `~/Documents/build` and still lost `~/Documents/build/2026` --
half a fix, and the half that looks right in a shallow test. They are skipped
for callers walking top-down, which have already had the per-level rule applied
at every ancestor. `SYSTEM_PATHS` stays unconditional: it is what keeps
`/usr/bin` excluded once `bin` is relaxed.

Callers that pass no sibling list keep the pre-#1727 answer, so index upserts,
manual adds and the file-level containing-path check are unchanged.

Fixes #1727
@TalexDreamSoul

Copy link
Copy Markdown
Contributor Author

Wiring verification

I said in review that this PR had no end-to-end verification, and that the fixture experiment I tried did not run — the control file came back unindexed, which proved the experiment was broken rather than the filter aggressive. Here is what I did instead, and what it does and does not establish.

The risk worth checking is not "is the rule right" — the unit tests cover that. It is "is the new context actually supplied by the real scanner, at the right level, in the shape the filter reads". A context that no production caller populates would leave walksEveryLevel permanently false and this PR would be inert, while every unit test still passed. That is a static property, so I traced it statically rather than staging another live scan.

The capability is derived, not passed as a flag:

file-filter-service.ts:340   const walksEveryLevel = context?.siblingNames !== undefined;

The recursion supplies it from entries it already has — this is the line that makes the PR live, and the one absent on master:

file-scan-utils.ts:457   const entryNames = entries.map((entry) => entry.name);
file-scan-utils.ts:459     await scanDirectoryInto(subDir, opts, excludePaths, depth + 1, out, stats, sink, entryNames)

so every directory below a scan root is judged against its own sibling list, and a scan root — which has no parent that was read — passes undefined and keeps the stricter pre-existing answer. That asymmetry is the right way round: the widening never applies at a level where the evidence for it was never gathered.

And the production index path is that same code, not a parallel implementation:

file-provider.ts:841,922        scanDirectory: (rootPath, excludePaths, runOptions) => …
addon/files/utils.ts:42-47      → globalScanDirectory   (a field remap, no traversal logic of its own)
file-scan-utils.ts              → scanDirectoryInto

consumed by both file-provider-full-scan-run-service.ts:76 and file-provider-reconciliation-run-service.ts:193, so full scans and reconciliation runs both go through it.

Worked through the fixture case by hand against that chain: Documents/build is judged against ['build','notes'], no project marker, indexed; Projects/app/dist is judged against ['package.json','dist'], marker present, excluded. That is the intended semantics.

What this does not establish. No live scan was run and no rows were read out of search-index.db — this is a trace of the call graph, not an observation of the outcome. It rules out the inert-context failure, which was the one I actually doubted; it would not catch something that goes wrong only against a real filesystem, such as a permission-denied directory or a symlink loop changing which entries readdir returns.

@TalexDreamSoul

Copy link
Copy Markdown
Contributor Author

Disclosure against #318, which scopes the remaining indexing-memory risk to exactly the function this PR changes.

That issue's residual is that fs.readdir(dirPath, { withFileTypes: true }) is unbounded for a single directory with huge fan-out — measured there at 95.7 MiB and climbing for 100k entries in one directory. This PR adds an allocation on that path:

const entryNames = entries.map((entry) => entry.name);

retained for the duration of each directory's recursion, alongside entries. Measured:

entries entries added entryNames overhead
100,000 19.08 MiB 0.76 MiB 4.0%
200,000 41.20 MiB 1.53 MiB 3.7%

The strings are shared with the Dirent objects, so this is the pointer array only. A Dirent stand-in was used, so the ratio is the reliable part, not the absolute MiB.

Not proposing a change here. The comment at that line already says the map "costs one map, no syscall", which is true about syscalls and silent about memory — worth saying out loud on an issue that is specifically about this function's retention. It is also compatible with the opendir streaming fix #318 is weighing: names-only retention is ~4% of the current per-directory cost, so collecting names while streaming would keep this PR's filter working and still take the big win.

Also noted on #318: that issue's tree-level numbers were measured on the assumption the dev/temp filter half stays as-is, which is what this PR changes. They should be re-measured after this lands rather than carried forward.

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.

Any folder named Library, Applications, System, private, var, tmp, dev, cache, logs… is silently skipped, at any depth

1 participant