From d3d2af69a04a11dac444f3268268623a351fbcb5 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Sun, 13 Sep 2026 13:22:03 +0200 Subject: [PATCH 1/3] feat(rules): add pnpm store and Yarn cache rules pnpm and Yarn keep regenerable package caches alongside npm's, but the embedded database only classified _cacache, so their caches scanned as unknown and never reached the report. pnpm-store covers the store under the pnpm home directory on all three platforms (**/pnpm/store/**) plus the legacy and configured ~/.pnpm-store. yarn-cache covers the Yarn 1 global caches (Linux, macOS, Windows) and Yarn Berry's per-project .yarn/cache and global ~/.yarn/berry/cache. Both are safe on the npm-cache reasoning: removing them only makes the next install re-download packages. The patterns name the cache directories, not their parents, so the pnpm binary and global installs under the pnpm home, and Yarn's release binaries, plugins and SDKs under .yarn, remain unknown. Fixture rows in EMBEDDED_RULE_FIXTURES pin a canonical path per rule, and dedicated tests cover each platform default plus the non-cache neighbours. --- .../155-pnpm-yarn-cache-rules.added.md | 3 + crates/diskern-core/rules/base.json | 14 ++++ crates/diskern-core/src/rules.rs | 84 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 changelog.d/155-pnpm-yarn-cache-rules.added.md diff --git a/changelog.d/155-pnpm-yarn-cache-rules.added.md b/changelog.d/155-pnpm-yarn-cache-rules.added.md new file mode 100644 index 0000000..b998194 --- /dev/null +++ b/changelog.d/155-pnpm-yarn-cache-rules.added.md @@ -0,0 +1,3 @@ +- Add safe rules for the pnpm package store and the Yarn download caches + (Yarn 1 global cache, Yarn Berry per-project and global caches), + classified as regenerable package-manager cache. diff --git a/crates/diskern-core/rules/base.json b/crates/diskern-core/rules/base.json index 689ad0f..d165a8b 100644 --- a/crates/diskern-core/rules/base.json +++ b/crates/diskern-core/rules/base.json @@ -64,6 +64,20 @@ "verdict": "safe", "description": "npm's download cache. Re-fetched on demand; removing it only makes the next install download packages again." }, + { + "id": "pnpm-store", + "patterns": ["**/pnpm/store/**", "**/.pnpm-store/**"], + "category": "package_manager_cache", + "verdict": "safe", + "description": "pnpm's content-addressable package store, under the pnpm home directory or a configured store dir. Re-fetched on demand; removing it only makes the next install download and unpack packages again." + }, + { + "id": "yarn-cache", + "patterns": ["**/.cache/yarn/**", "**/library/caches/yarn/**", "**/appdata/local/yarn/cache/**", "**/.yarn/cache/**", "**/.yarn/berry/cache/**"], + "category": "package_manager_cache", + "verdict": "safe", + "description": "Yarn's download cache — the Yarn 1 global cache and Yarn Berry's per-project and global caches. Re-fetched on demand; removing it only makes the next install download packages again." + }, { "id": "unix-system-logs", "patterns": ["/var/log/**", "/private/var/log/**"], diff --git a/crates/diskern-core/src/rules.rs b/crates/diskern-core/src/rules.rs index 7c3b9b8..ddbbe59 100644 --- a/crates/diskern-core/src/rules.rs +++ b/crates/diskern-core/src/rules.rs @@ -463,6 +463,78 @@ mod tests { } } + #[test] + fn pnpm_store_is_safe_to_remove() { + let db = RulesDb::embedded(); + for path in [ + "/home/u/.local/share/pnpm/store/v3/files/aa/bb", + "/Users/u/Library/pnpm/store/v3/files/aa/bb", + "C:\\Users\\u\\AppData\\Local\\pnpm\\store\\v3\\files\\aa\\bb", + "/home/u/.pnpm-store/v3/files/aa/bb", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!( + cat, + Category::PackageManagerCache, + "{path} matched {rule:?}" + ); + assert_eq!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + + /// The pnpm home directory holds the store, but also the pnpm binary + /// itself and globally installed packages — neither is a download + /// cache. + #[test] + fn pnpm_home_outside_the_store_is_not_marked_safe() { + let db = RulesDb::embedded(); + for path in [ + "/Users/u/Library/pnpm/pnpm", + "/home/u/.local/share/pnpm/global/5/.npmrc", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!(cat, Category::Unknown, "{path} matched {rule:?}"); + assert_ne!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + + #[test] + fn yarn_cache_is_safe_to_remove() { + let db = RulesDb::embedded(); + for path in [ + "/home/u/.cache/yarn/v6/npm-react-18.2.0.zip", + "/Users/u/Library/Caches/Yarn/v6/npm-react-18.2.0.zip", + "C:\\Users\\u\\AppData\\Local\\Yarn\\Cache\\v6\\npm-react-18.2.0.zip", + "/home/u/proj/.yarn/cache/react-npm-18.2.0.zip", + "/home/u/.yarn/berry/cache/react-npm-18.2.0.zip", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!( + cat, + Category::PackageManagerCache, + "{path} matched {rule:?}" + ); + assert_eq!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + + /// A project's `.yarn` directory keeps the Yarn release binary, + /// plugins and editor SDKs next to `cache` — only `cache` is + /// re-downloaded packages. + #[test] + fn yarn_support_files_outside_the_cache_are_not_marked_safe() { + let db = RulesDb::embedded(); + for path in [ + "/home/u/proj/.yarn/releases/yarn-4.5.0.cjs", + "/home/u/proj/.yarn/sdks/typescript/bin/tsc", + "/home/u/proj/.yarnrc.yml", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!(cat, Category::Unknown, "{path} matched {rule:?}"); + assert_ne!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + /// An installed application's own repair binary is not a reclaimable /// download. `unknown` is the right answer: report::build drops those, /// so it never reaches the user as an actionable row. @@ -539,6 +611,18 @@ mod tests { Category::PackageManagerCache, Verdict::Safe, ), + ( + "/home/u/.local/share/pnpm/store/v3/files/aa/bb", + "pnpm-store", + Category::PackageManagerCache, + Verdict::Safe, + ), + ( + "/Users/u/Library/Caches/Yarn/v6/npm-react-18.2.0.zip", + "yarn-cache", + Category::PackageManagerCache, + Verdict::Safe, + ), ( "/var/log/apt/history.log", "unix-system-logs", From d95bcc40efd7b528c87707a6bc28c2368a7d4db5 Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Mon, 14 Sep 2026 14:10:47 +0200 Subject: [PATCH 2/3] fix(rules): classify Yarn PnP caches as review, not safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PnP loader reads installed packages directly from the cache archives — a project's .yarn/cache and the global .yarn/berry/cache — so quarantining them breaks imports until yarn install restores the cache, not just slows the next install. Split them out of yarn-cache into yarn-pnp-cache (verdict: review); the Yarn 1 global caches stay safe since they only back downloads. Adds local and global PnP fixtures. --- crates/diskern-core/rules/base.json | 11 +++++++++-- crates/diskern-core/src/rules.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/diskern-core/rules/base.json b/crates/diskern-core/rules/base.json index d165a8b..fab6d06 100644 --- a/crates/diskern-core/rules/base.json +++ b/crates/diskern-core/rules/base.json @@ -73,10 +73,17 @@ }, { "id": "yarn-cache", - "patterns": ["**/.cache/yarn/**", "**/library/caches/yarn/**", "**/appdata/local/yarn/cache/**", "**/.yarn/cache/**", "**/.yarn/berry/cache/**"], + "patterns": ["**/.cache/yarn/**", "**/library/caches/yarn/**", "**/appdata/local/yarn/cache/**"], "category": "package_manager_cache", "verdict": "safe", - "description": "Yarn's download cache — the Yarn 1 global cache and Yarn Berry's per-project and global caches. Re-fetched on demand; removing it only makes the next install download packages again." + "description": "Yarn 1's global download cache. Re-fetched on demand; removing it only makes the next install download packages again." + }, + { + "id": "yarn-pnp-cache", + "patterns": ["**/.yarn/cache/**", "**/.yarn/berry/cache/**"], + "category": "package_manager_cache", + "verdict": "review", + "description": "Yarn Berry's package cache — a project's .yarn/cache or the global .yarn/berry/cache. With Plug'n'Play these archives back live imports, so quarantining them breaks the app until yarn install restores them; only a pure download cache under the node-modules linker." }, { "id": "unix-system-logs", diff --git a/crates/diskern-core/src/rules.rs b/crates/diskern-core/src/rules.rs index ddbbe59..af1cf3c 100644 --- a/crates/diskern-core/src/rules.rs +++ b/crates/diskern-core/src/rules.rs @@ -505,6 +505,25 @@ mod tests { "/home/u/.cache/yarn/v6/npm-react-18.2.0.zip", "/Users/u/Library/Caches/Yarn/v6/npm-react-18.2.0.zip", "C:\\Users\\u\\AppData\\Local\\Yarn\\Cache\\v6\\npm-react-18.2.0.zip", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!( + cat, + Category::PackageManagerCache, + "{path} matched {rule:?}" + ); + assert_eq!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + + /// Yarn Berry's PnP loader reads packages straight out of the cache + /// archives — a project's `.yarn/cache` or the global `.yarn/berry/cache` + /// — so these paths are `review`, not `safe`: quarantining them breaks + /// imports until `yarn install` restores the cache. + #[test] + fn yarn_pnp_caches_are_review_not_safe() { + let db = RulesDb::embedded(); + for path in [ "/home/u/proj/.yarn/cache/react-npm-18.2.0.zip", "/home/u/.yarn/berry/cache/react-npm-18.2.0.zip", ] { @@ -514,7 +533,7 @@ mod tests { Category::PackageManagerCache, "{path} matched {rule:?}" ); - assert_eq!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + assert_eq!(verdict, Verdict::Review, "{path} matched {rule:?}"); } } @@ -623,6 +642,12 @@ mod tests { Category::PackageManagerCache, Verdict::Safe, ), + ( + "/home/u/proj/.yarn/cache/react-npm-18.2.0.zip", + "yarn-pnp-cache", + Category::PackageManagerCache, + Verdict::Review, + ), ( "/var/log/apt/history.log", "unix-system-logs", From caecb82c4827a89338228db6030c7e07446a989b Mon Sep 17 00:00:00 2001 From: Louis Deconinck Date: Mon, 14 Sep 2026 19:47:35 +0200 Subject: [PATCH 3/3] docs(rules): spell out Plug'n'Play so typos stays green --- crates/diskern-core/src/rules.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/diskern-core/src/rules.rs b/crates/diskern-core/src/rules.rs index af1cf3c..8d117a8 100644 --- a/crates/diskern-core/src/rules.rs +++ b/crates/diskern-core/src/rules.rs @@ -516,8 +516,8 @@ mod tests { } } - /// Yarn Berry's PnP loader reads packages straight out of the cache - /// archives — a project's `.yarn/cache` or the global `.yarn/berry/cache` + /// Yarn Berry's Plug'n'Play loader reads packages straight out of the + /// cache archives — a project's `.yarn/cache` or the global `.yarn/berry/cache` /// — so these paths are `review`, not `safe`: quarantining them breaks /// imports until `yarn install` restores the cache. #[test]