Skip to content

Cache CLI filesystem reads and de-dupe scanner work#127

Merged
LadyBluenotes merged 5 commits intomainfrom
scanner-fixes
May 3, 2026
Merged

Cache CLI filesystem reads and de-dupe scanner work#127
LadyBluenotes merged 5 commits intomainfrom
scanner-fixes

Conversation

@LadyBluenotes
Copy link
Copy Markdown
Member

@LadyBluenotes LadyBluenotes commented May 3, 2026

Summary

This PR reduces repeated filesystem work during Intent CLI scans while preserving existing discovery behavior.

  • add a lightweight per-invocation filesystem cache for package.json reads and skill file discovery
  • expose package.json read/cache hit counts in debug output
  • share the cache across list/load scan paths, including the load fallback scan
  • make skill discovery tolerate inaccessible or non-directory skills paths
  • de-dupe scanner work by exact package root and scanned node_modules directory
  • keep broad node_modules scanning mandatory so undeclared installed packages are still discovered

Behavior Notes

This does not make broad node_modules scanning fallback-only. The scanner still enumerates the same node_modules directories as before; it only skips package roots or scan targets already attempted in the same scan.

Package candidates are de-duped by resolved package root path, not by package name, so multiple installed versions of the same package are still inspected and tracked as variants.

Summary by CodeRabbit

  • New Features

    • Debug output for intent listing/loading now shows filesystem metrics: packageJsonReadCount and packageJsonCacheHits.
    • Scanning now exposes scan statistics in debug output.
  • Bug Fixes

    • Directory read errors during skill discovery are handled gracefully (no unexpected crashes).
  • Refactor

    • Filesystem read caching introduced to reduce redundant package.json reads and improve scan performance.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

@LadyBluenotes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 6 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 85b86a94-2022-4824-a530-2f909611fe3b

📥 Commits

Reviewing files that changed from the base of the PR and between 3f0a096 and a178ca9.

📒 Files selected for processing (1)
  • .changeset/short-sheep-brush.md
📝 Walkthrough

Walkthrough

The PR integrates an IntentFsCache into intent discovery/resolution to memoize package.json reads and skill-file discovery, threads the cache through scanning/walking/resolution codepaths, and exposes packageJsonReadCount/packageJsonCacheHits in scan results and CLI debug output.

Changes

Filesystem Caching & Scan Integration

Layer / File(s) Summary
Cache Types & Implementation
packages/intent/src/fs-cache.ts
New createIntentFsCache() and types (IntentFsCache, IntentFsCacheStats) that memoize readPackageJson/readPackageJsonResult and findSkillFiles, normalize keys with resolve(), return defensive copies for arrays, and track packageJsonReadCount and packageJsonCacheHits.
Scan Types
packages/intent/src/types.ts, packages/intent/src/core/types.ts
ScanResult gains optional stats?: ScanStats; new ScanStats and IntentScanDebugStats (extends ScanStats) added; IntentSkillListDebug now includes scan: IntentScanDebugStats.
Scanner Integration
packages/intent/src/scanner.ts
scanForIntents accepts optional fsCache, creates/uses createIntentFsCache() when absent, replaces local package.json caching with fsCache.readPackageJson, delegates skill discovery to fsCache.findSkillFiles, threads fsCache into createDependencyWalker/registrar, and returns stats in ScanResult. scanIntentPackageAtRoot accepts fsCache?: IntentFsCache.
Dependency Registrar & Walker
packages/intent/src/discovery/register.ts, packages/intent/src/discovery/walk.ts
Registrar deduplicates by filesystem identity (resolve) and exposes scanNodeModulesDir; CreateDependencyWalkerOptions now requires fsCache; readPkgJsonWithWarning uses fsCache.readPackageJsonResult, treating ENOENT as null and warning on other errors; workspace package iteration uses findWorkspacePackages.
Core & Resolution
packages/intent/src/core.ts, packages/intent/src/core/load-resolution.ts
listIntentSkills and resolveIntentSkillInCwd create and pass an IntentFsCache (via withFsCache) into scanForIntents; debug objects now include scan stats (fast-path uses fsCache.getStats()); workspace read/resolution helpers now accept and propagate fsCache.
CLI Debug Output
packages/intent/src/commands/list.ts, packages/intent/src/commands/load.ts
printListDebug and printLoadDebug now include packageJsonReadCount and packageJsonCacheHits from result.debug.scan / loaded.debug.scan when present.
Utilities
packages/intent/src/utils.ts
findSkillFiles wraps readdirSync in try/catch to return partial results on read failures rather than throwing.
Tests
packages/intent/tests/fs-cache.test.ts, packages/intent/tests/scanner.test.ts, packages/intent/tests/core.test.ts, packages/intent/tests/cli.test.ts
New fs-cache tests validate read caching and defensive array copies; scanner/core/cli tests updated to assert presence and numeric values of packageJsonReadCount and packageJsonCacheHits and added scanner cases for node_modules discovery, non-directory skills path, and conflict variants.

Sequence Diagram

sequenceDiagram
participant CLI
participant Core
participant Scanner
participant Registrar
participant FsCache
participant Disk

CLI->>Core: list/load (--debug)
Core->>Scanner: scanForIntents(..., fsCache?)
alt no fsCache
Core->>FsCache: createIntentFsCache()
end
Core->>Scanner: scanForIntents(with fsCache)
Scanner->>FsCache: findSkillFiles(skillsDir)
Scanner->>Registrar: createPackageRegistrar(scanNodeModulesDir)
Registrar->>FsCache: readPackageJsonResult(packageRoot)
FsCache->>Disk: read package.json (cached)
Disk-->>FsCache: package.json content / error
FsCache-->>Registrar: parsed package.json / null
Registrar->>Scanner: register package entries
Scanner-->>Core: ScanResult + stats
Core-->>CLI: debug output includes packageJsonReadCount & packageJsonCacheHits
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • KyleAMathews

Poem

🐰 I hopped through folders, nibbling JSON crumbs,
Counting reads and caching little sums.
Now scans run lighter, footprints kept precise —
A rabbit’s ledger of reads and cache hits, nice! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR provides a comprehensive summary covering all major changes but lacks the required template structure with sections and checklist items. Fill in the required template with 'Changes' section, 'Checklist' items, and 'Release Impact' section to match repository standards.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding filesystem caching and deduplication of scanner work.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scanner-fixes

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
Review rate limit: 0/1 reviews remaining, refill in 6 minutes and 6 seconds.

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

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 3, 2026

View your CI Pipeline Execution ↗ for commit 1685b96

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-03 18:18:52 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 3, 2026

View your CI Pipeline Execution ↗ for commit d42a075

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 6s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-03 17:25:06 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tanstack/intent@127

commit: 3f0a096

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 3, 2026

Merging this PR will improve performance by 59.96%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 5 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
scans a consumer workspace 61.7 ms 38.5 ms +59.96%

Comparing scanner-fixes (a178ca9) with main (1639791)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (25e6131) during the generation of this report, so 1639791 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/intent/src/core/load-resolution.ts (1)

3-3: 💤 Low value

ESLint [error] on new import: split type and value imports.

The static analysis reports two rule violations on the new line:

  • sort-imports: the type IntentFsCache specifier should precede createIntentFsCache.
  • import/consistent-type-specifier-style: the project prefers a separate top-level import type statement over an inline type modifier.
🔧 Suggested fix
-import { createIntentFsCache, type IntentFsCache } from '../fs-cache.js'
+import type { IntentFsCache } from '../fs-cache.js'
+import { createIntentFsCache } from '../fs-cache.js'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/core/load-resolution.ts` at line 3, The import mixes a
type and a value which triggers ESLint: split the inline type into a separate
top-level type import and ensure the type import comes before the value import;
replace the single line importing both with two lines — one "import type {
IntentFsCache } from '../fs-cache.js'" and one "import { createIntentFsCache }
from '../fs-cache.js'" — so IntentFsCache is imported as a type-only import
before the createIntentFsCache value import.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/intent/src/utils.ts`:
- Around line 20-28: The current silent catch in the directory read (where
entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })) swallows
failures and makes callers like validate.ts and staleness.ts unable to
distinguish "no skills" from "read failed"; update the catch in the utils.ts
function to either (a) emit a warning/error via the existing logger (e.g.,
processLogger or the module logger) including the directory path and error
details, or (b) set an error flag or counter on the cache/stats object the
function returns so callers (validate.ts, staleness.ts) can detect incomplete
reads — modify the catch block accordingly and ensure the unique symbols
readdirSync usage and the entries variable handling remain consistent so callers
can react to the logged warning or stats flag.

---

Nitpick comments:
In `@packages/intent/src/core/load-resolution.ts`:
- Line 3: The import mixes a type and a value which triggers ESLint: split the
inline type into a separate top-level type import and ensure the type import
comes before the value import; replace the single line importing both with two
lines — one "import type { IntentFsCache } from '../fs-cache.js'" and one
"import { createIntentFsCache } from '../fs-cache.js'" — so IntentFsCache is
imported as a type-only import before the createIntentFsCache value import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16b8a89b-19b2-491a-8e18-46f8fbbc194c

📥 Commits

Reviewing files that changed from the base of the PR and between 25e6131 and d42a075.

📒 Files selected for processing (14)
  • packages/intent/src/commands/list.ts
  • packages/intent/src/commands/load.ts
  • packages/intent/src/core.ts
  • packages/intent/src/core/load-resolution.ts
  • packages/intent/src/core/types.ts
  • packages/intent/src/discovery/walk.ts
  • packages/intent/src/fs-cache.ts
  • packages/intent/src/scanner.ts
  • packages/intent/src/types.ts
  • packages/intent/src/utils.ts
  • packages/intent/tests/cli.test.ts
  • packages/intent/tests/core.test.ts
  • packages/intent/tests/fs-cache.test.ts
  • packages/intent/tests/scanner.test.ts

Comment on lines +20 to +28

let entries: Array<Dirent<string>>
try {
entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })
} catch {
return files
}

for (const entry of entries) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent error handling may mask failures from callers.

The try/catch returns an empty array on directory read failure, but callers like validate.ts and staleness.ts (see relevant code snippets) treat the returned array as authoritative with no way to distinguish "no skills found" from "directory read failed". This could cause:

  • validate.ts: Passing validation when subdirectories failed to read
  • staleness.ts: Incomplete skill metadata collection

Consider either logging a warning on catch, or tracking errors in the cache stats so callers can detect incomplete results.

💡 Suggested improvement: Add warning on read failure
   let entries: Array<Dirent<string>>
   try {
     entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })
   } catch {
+    // Optionally log or track that this directory failed to read
+    // console.warn(`Warning: Could not read directory ${dir}`)
     return files
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let entries: Array<Dirent<string>>
try {
entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })
} catch {
return files
}
for (const entry of entries) {
let entries: Array<Dirent<string>>
try {
entries = readdirSync(dir, { withFileTypes: true, encoding: 'utf8' })
} catch {
// Optionally log or track that this directory failed to read
// console.warn(`Warning: Could not read directory ${dir}`)
return files
}
for (const entry of entries) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/utils.ts` around lines 20 - 28, The current silent catch
in the directory read (where entries = readdirSync(dir, { withFileTypes: true,
encoding: 'utf8' })) swallows failures and makes callers like validate.ts and
staleness.ts unable to distinguish "no skills" from "read failed"; update the
catch in the utils.ts function to either (a) emit a warning/error via the
existing logger (e.g., processLogger or the module logger) including the
directory path and error details, or (b) set an error flag or counter on the
cache/stats object the function returns so callers (validate.ts, staleness.ts)
can detect incomplete reads — modify the catch block accordingly and ensure the
unique symbols readdirSync usage and the entries variable handling remain
consistent so callers can react to the logged warning or stats flag.

@LadyBluenotes LadyBluenotes changed the title Cache package manifest reads during discovery Cache CLI filesystem reads and de-dupe scanner work May 3, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/intent/tests/scanner.test.ts (2)

122-129: ⚡ Quick win

Assert a real cache hit in this fixture.

These checks only prove that stats is wired through. This scan rereads the discovered package during dependency walking, so packageJsonCacheHits should also be > 0 here; otherwise a regression that bypasses the cache would still pass.

Suggested test tightening
     expect(result.stats).toEqual(
       expect.objectContaining({
         packageJsonReadCount: expect.any(Number),
         packageJsonCacheHits: expect.any(Number),
       }),
     )
     expect(result.stats!.packageJsonReadCount).toBeGreaterThan(0)
+    expect(result.stats!.packageJsonCacheHits).toBeGreaterThan(0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/scanner.test.ts` around lines 122 - 129, The test
currently only asserts that stats are present and that packageJsonReadCount > 0,
but doesn't verify an actual cache hit; update the assertion for the scan result
(referencing result.stats and the packageJsonCacheHits stat) to assert that
packageJsonCacheHits is greater than zero (e.g.,
expect(result.stats!.packageJsonCacheHits).toBeGreaterThan(0)) so the fixture
validates that the package.json cache was used during dependency walking.

131-149: ⚡ Quick win

Cover the scanIntentPackageAtRoot() fallback path too.

This only exercises the full scan. packages/intent/src/scanner.ts now routes the single-package fallback through the same cached skill discovery branch, so adding the same non-directory skills case there would protect the load fallback from regressing independently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/scanner.test.ts` around lines 131 - 149, Add a test
that exercises the single-package fallback path by creating a package at the
repository root with a package.json containing an intent block and a
non-directory "skills" file, then call scanIntentPackageAtRoot (in addition to
the existing scanForIntents test) and assert the returned package exists and its
skills array is empty; this mirrors the existing test but targets the fallback
branch in packages/intent/src/scanner.ts to prevent regressions in
scanIntentPackageAtRoot.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/intent/tests/scanner.test.ts`:
- Around line 122-129: The test currently only asserts that stats are present
and that packageJsonReadCount > 0, but doesn't verify an actual cache hit;
update the assertion for the scan result (referencing result.stats and the
packageJsonCacheHits stat) to assert that packageJsonCacheHits is greater than
zero (e.g., expect(result.stats!.packageJsonCacheHits).toBeGreaterThan(0)) so
the fixture validates that the package.json cache was used during dependency
walking.
- Around line 131-149: Add a test that exercises the single-package fallback
path by creating a package at the repository root with a package.json containing
an intent block and a non-directory "skills" file, then call
scanIntentPackageAtRoot (in addition to the existing scanForIntents test) and
assert the returned package exists and its skills array is empty; this mirrors
the existing test but targets the fallback branch in
packages/intent/src/scanner.ts to prevent regressions in
scanIntentPackageAtRoot.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c7e601b-1570-4ae1-9fb0-64438e1b1f44

📥 Commits

Reviewing files that changed from the base of the PR and between d42a075 and 3f0a096.

📒 Files selected for processing (4)
  • packages/intent/src/discovery/register.ts
  • packages/intent/src/discovery/walk.ts
  • packages/intent/src/scanner.ts
  • packages/intent/tests/scanner.test.ts

@LadyBluenotes LadyBluenotes merged commit caade06 into main May 3, 2026
5 checks passed
@LadyBluenotes LadyBluenotes deleted the scanner-fixes branch May 3, 2026 18:18
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