pi-lens 3.8.73 → 3.8.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +1116 -0
  2. package/README.md +37 -21
  3. package/dist/clients/atomic-write.js +3 -3
  4. package/dist/clients/bus-events-logger.js +5 -1
  5. package/dist/clients/cache/rule-cache.js +43 -2
  6. package/dist/clients/cache-observability.js +211 -0
  7. package/dist/clients/call-graph.js +20 -0
  8. package/dist/clients/cascade-logger.js +5 -1
  9. package/dist/clients/complexity-client.js +4 -7
  10. package/dist/clients/dependency-checker.js +151 -20
  11. package/dist/clients/diagnostic-dispositions.js +22 -2
  12. package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
  13. package/dist/clients/dispatch/dispatcher.js +16 -1
  14. package/dist/clients/dispatch/fact-store.js +11 -0
  15. package/dist/clients/dispatch/facts/function-facts.js +14 -0
  16. package/dist/clients/dispatch/facts/import-facts.js +100 -101
  17. package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
  18. package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
  19. package/dist/clients/dispatch/integration.js +94 -15
  20. package/dist/clients/dispatch/rules/async-noise.js +1 -0
  21. package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
  22. package/dist/clients/dispatch/runners/ast-grep-napi.js +24 -1
  23. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
  24. package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
  25. package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
  26. package/dist/clients/feature-hints.js +53 -7
  27. package/dist/clients/file-kinds.js +86 -31
  28. package/dist/clients/fix-worklog.js +14 -2
  29. package/dist/clients/generated-artifacts.js +21 -0
  30. package/dist/clients/gzip-stage-write.js +84 -0
  31. package/dist/clients/installer/index.js +358 -129
  32. package/dist/clients/knip-client.js +64 -1
  33. package/dist/clients/language-profile.js +61 -72
  34. package/dist/clients/latency-logger.js +6 -16
  35. package/dist/clients/lens-config.js +128 -165
  36. package/dist/clients/lens-engine.js +39 -5
  37. package/dist/clients/lens-flag-registry.js +285 -0
  38. package/dist/clients/lens-map.js +2 -7
  39. package/dist/clients/log-cleanup.js +6 -17
  40. package/dist/clients/lsp/cascade-tier.js +22 -2
  41. package/dist/clients/lsp/client.js +74 -10
  42. package/dist/clients/lsp/config.js +62 -9
  43. package/dist/clients/lsp/index.js +34 -24
  44. package/dist/clients/lsp/language.js +4 -1
  45. package/dist/clients/lsp/launch.js +8 -6
  46. package/dist/clients/lsp/server.js +84 -29
  47. package/dist/clients/lsp-document-symbols.js +112 -0
  48. package/dist/clients/mcp/analyze.js +5 -4
  49. package/dist/clients/mcp/review.js +36 -3
  50. package/dist/clients/module-report.js +100 -80
  51. package/dist/clients/ndjson-logger.js +51 -21
  52. package/dist/clients/path-keyed-map.js +76 -0
  53. package/dist/clients/path-utils.js +35 -0
  54. package/dist/clients/performance-report.js +327 -0
  55. package/dist/clients/pipeline.js +7 -0
  56. package/dist/clients/project-changes.js +77 -4
  57. package/dist/clients/project-diagnostics/extractors.js +26 -115
  58. package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
  59. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +109 -0
  60. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
  61. package/dist/clients/project-diagnostics/scanner.js +247 -127
  62. package/dist/clients/project-lens-config.js +53 -26
  63. package/dist/clients/project-report.js +39 -14
  64. package/dist/clients/project-scale.js +8 -10
  65. package/dist/clients/project-snapshot-persist-worker.js +6 -0
  66. package/dist/clients/project-snapshot.js +611 -7
  67. package/dist/clients/quiet-window.js +9 -1
  68. package/dist/clients/read-expansion.js +22 -12
  69. package/dist/clients/read-guard.js +78 -0
  70. package/dist/clients/redact/secrets.js +449 -0
  71. package/dist/clients/resource-sampler.js +133 -14
  72. package/dist/clients/reverse-deps.js +48 -0
  73. package/dist/clients/review-graph/builder.js +1522 -179
  74. package/dist/clients/review-graph/persist-worker.js +10 -0
  75. package/dist/clients/review-graph/query.js +5 -0
  76. package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
  77. package/dist/clients/review-graph-logger.js +24 -0
  78. package/dist/clients/runtime-agent-end.js +4 -2
  79. package/dist/clients/runtime-coordinator.js +2 -2
  80. package/dist/clients/runtime-session.js +433 -59
  81. package/dist/clients/runtime-tool-call.js +46 -14
  82. package/dist/clients/runtime-turn.js +97 -26
  83. package/dist/clients/safe-spawn.js +102 -15
  84. package/dist/clients/session-start-observability.js +15 -0
  85. package/dist/clients/session-state-store.js +2 -1
  86. package/dist/clients/sessionstart-logger.js +18 -0
  87. package/dist/clients/sg-runner.js +48 -33
  88. package/dist/clients/source-filter.js +49 -25
  89. package/dist/clients/startup-marker.js +6 -0
  90. package/dist/clients/startup-timing.js +9 -0
  91. package/dist/clients/todo-scanner.js +30 -3
  92. package/dist/clients/tree-sitter-cache.js +147 -139
  93. package/dist/clients/tree-sitter-client.js +1340 -67
  94. package/dist/clients/tree-sitter-logger.js +43 -1
  95. package/dist/clients/tree-sitter-query-loader.js +110 -11
  96. package/dist/clients/tree-sitter-shared.js +43 -19
  97. package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
  98. package/dist/clients/widget-state.js +50 -14
  99. package/dist/clients/word-index-logger.js +44 -0
  100. package/dist/clients/word-index.js +252 -35
  101. package/dist/clients/workspace-topology.js +11 -1
  102. package/dist/index.js +45544 -39255
  103. package/dist/mcp/analyze-cli.js +3 -0
  104. package/dist/mcp/cli.js +110 -0
  105. package/dist/mcp/server.js +63 -8
  106. package/dist/tools/lens-diagnostics.js +47 -5
  107. package/dist/tools/lsp-navigation.js +5 -28
  108. package/dist/tools/symbol-search.js +21 -2
  109. package/docs/agent-guide.md +247 -0
  110. package/docs/astplayground.md +3 -4
  111. package/docs/audit1.md +5 -5
  112. package/docs/environment-variables.md +153 -11
  113. package/docs/features.md +4 -3
  114. package/docs/globalconfig.md +56 -4
  115. package/docs/language-coverage.md +2 -2
  116. package/docs/mcp.md +17 -16
  117. package/docs/servercapabilities.md +5 -2
  118. package/docs/settings.md +181 -0
  119. package/docs/tools.md +14 -0
  120. package/docs/tree-sitter_rules_catalog.md +11 -31
  121. package/docs/usage.md +13 -0
  122. package/package.json +139 -138
  123. package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
  124. package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
  125. package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
  126. package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
  127. package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
  128. package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
  129. package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
  130. package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
  131. package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
  132. package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
  133. package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
  134. package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
  135. package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
  136. package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
  137. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
  138. package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
  139. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
  140. package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
  141. package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
  142. package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
  143. package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
  144. package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
  145. package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
  146. package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
  147. package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
  148. package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
  149. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
  150. package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
  151. package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
  152. package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
  153. package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
  154. package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
  155. package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
  156. package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
  157. package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
  158. package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
  159. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
  160. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
  161. package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
  162. package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
  163. package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
  164. package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
  165. package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
  166. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
  167. package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
  168. package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
  169. package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
  170. package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
  171. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
  172. package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
  173. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
  174. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
  175. package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
  176. package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
  177. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
  178. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
  179. package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
  180. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
  181. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
  182. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
  183. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
  184. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
  185. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
  186. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
  187. package/rules/typos/_typos.toml +17 -0
  188. package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
  189. package/dist/clients/config-enabled-shape.js +0 -39
  190. package/docs/env_variables.md +0 -10
  191. package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
  192. package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
  193. package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
  194. package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
  195. package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
  196. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
  197. package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
  198. package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
  199. package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
  200. package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
  201. package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
  202. package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
  203. package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
  204. package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
  205. package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
@@ -22,6 +22,38 @@ const EMPTY_RESULT = {
22
22
  unlistedDeps: [],
23
23
  };
24
24
  const ANALYSIS_TIMEOUT_MS = 30_000;
25
+ /**
26
+ * Every package name referenced as a KEY (at any nesting depth — npm's
27
+ * `overrides` and pnpm's `pnpm.overrides` allow nested "for this dependency's
28
+ * sub-dependency" overrides) in `package.json`'s `overrides`, `resolutions`
29
+ * (Yarn's equivalent), or `pnpm.overrides` fields. These are the project's
30
+ * own explicit signal that a package is deliberately present to pin a
31
+ * resolution — not a source-imported dependency knip's import graph can see.
32
+ * Missing/malformed `package.json` degrades to an empty set (never throws) —
33
+ * this is a best-effort narrowing, not a required input.
34
+ */
35
+ export function readOverridePinnedPackageNames(targetDir) {
36
+ const names = new Set();
37
+ let pkg;
38
+ try {
39
+ pkg = JSON.parse(fs.readFileSync(path.join(targetDir, "package.json"), "utf-8"));
40
+ }
41
+ catch {
42
+ return names;
43
+ }
44
+ const collectKeys = (value) => {
45
+ if (!value || typeof value !== "object" || Array.isArray(value))
46
+ return;
47
+ for (const [key, nested] of Object.entries(value)) {
48
+ names.add(key);
49
+ collectKeys(nested);
50
+ }
51
+ };
52
+ collectKeys(pkg.overrides);
53
+ collectKeys(pkg.resolutions);
54
+ collectKeys(pkg.pnpm?.overrides);
55
+ return names;
56
+ }
25
57
  // --- Client ---
26
58
  export class KnipClient {
27
59
  knipAvailable = null;
@@ -206,7 +238,38 @@ export class KnipClient {
206
238
  summary: "No issues found",
207
239
  };
208
240
  }
209
- return this.parseOutput(output);
241
+ return this.dropOverridePinnedDeps(this.parseOutput(output), targetDir);
242
+ }
243
+ /**
244
+ * Drop `dependency`/`devDependency` issues for a package that's also
245
+ * referenced as an npm `overrides` (or Yarn `resolutions` / pnpm
246
+ * `pnpm.overrides`) key in this project's `package.json` (#968).
247
+ *
248
+ * A direct devDependency whose only job is pinning a vulnerable
249
+ * transitive/peer resolution has no source import — that's WORKING AS
250
+ * INTENDED, not dead code, and knip has no concept of "this dependency
251
+ * exists only to satisfy an overrides entry" (it only sees imports).
252
+ * `overrides`/`resolutions` are the project's own explicit, unambiguous
253
+ * signal that the package is deliberately present — the same class of
254
+ * signal `hardcoded-url`'s `SCREAMING_SNAKE_CASE` constant-name carve-out
255
+ * and `ts-ssrf`'s constant-identifier carve-out lean on elsewhere in this
256
+ * codebase — so this narrows the finding rather than suppressing
257
+ * `dependency`/`devDependency` issues wholesale: a devDependency that
258
+ * ISN'T also an overrides/resolutions key is still reported.
259
+ */
260
+ dropOverridePinnedDeps(result, targetDir) {
261
+ if (result.unusedDeps.length === 0)
262
+ return result;
263
+ const pinned = readOverridePinnedPackageNames(targetDir);
264
+ if (pinned.size === 0)
265
+ return result;
266
+ const isPinnedDepIssue = (issue) => (issue.type === "dependency" || issue.type === "devDependency") &&
267
+ (pinned.has(issue.name) || (!!issue.package && pinned.has(issue.package)));
268
+ const issues = result.issues.filter((issue) => !isPinnedDepIssue(issue));
269
+ const unusedDeps = result.unusedDeps.filter((issue) => !isPinnedDepIssue(issue));
270
+ return unusedDeps.length === result.unusedDeps.length
271
+ ? result
272
+ : { ...result, issues, unusedDeps };
210
273
  }
211
274
  async getKnipEnvironment(targetDir) {
212
275
  const { getToolEnvironment } = await import("./installer/index.js");
@@ -1,48 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { detectFileKind } from "./file-kinds.js";
3
+ import { CODE_KINDS, detectFileKind, DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "./file-kinds.js";
4
4
  import { getProjectIgnoreMatcher } from "./file-utils.js";
5
+ import { direntsHaveMarkerGlobMatch } from "./path-utils.js";
5
6
  import { LANGUAGE_POLICY, } from "./language-policy.js";
6
7
  import { getSourceFiles } from "./scan-utils.js";
7
8
  import { readDirEntriesSafe, shouldRecurseIntoDir } from "./source-walker.js";
8
9
  import { findNearestDirWithAnyBasename } from "./workspace-topology.js";
9
- export const SUPPORTED_FILE_KINDS = [
10
- "jsts",
11
- "python",
12
- "go",
13
- "rust",
14
- "cxx",
15
- "cmake",
16
- "fish",
17
- "shell",
18
- "json",
19
- "markdown",
20
- "css",
21
- "yaml",
22
- "sql",
23
- "ruby",
24
- "html",
25
- "docker",
26
- "php",
27
- "powershell",
28
- "prisma",
29
- "csharp",
30
- "fsharp",
31
- "java",
32
- "kotlin",
33
- "swift",
34
- "dart",
35
- "lua",
36
- "zig",
37
- "haskell",
38
- "elixir",
39
- "gleam",
40
- "ocaml",
41
- "clojure",
42
- "terraform",
43
- "nix",
44
- "toml",
45
- ];
10
+ /** Every registered kind participates in project-language detection (#894). */
11
+ export const SUPPORTED_FILE_KINDS = Object.keys(KIND_EXTENSIONS);
46
12
  const PROJECT_MARKERS_BY_KIND = {
47
13
  jsts: ["package.json", "tsconfig.json", "jsconfig.json"],
48
14
  python: ["pyproject.toml", "requirements.txt", "setup.py", "setup.cfg"],
@@ -72,6 +38,8 @@ const PROJECT_MARKERS_BY_KIND = {
72
38
  terraform: [".terraform.lock.hcl"],
73
39
  nix: ["flake.nix"],
74
40
  toml: ["pyproject.toml", "Cargo.toml", "taplo.toml"],
41
+ csharp: DOTNET_CSHARP_ROOT_MARKERS,
42
+ fsharp: DOTNET_FSHARP_ROOT_MARKERS,
75
43
  };
76
44
  const ROOT_MARKERS_BY_KIND = {
77
45
  jsts: [
@@ -114,7 +82,19 @@ const ROOT_MARKERS_BY_KIND = {
114
82
  terraform: [".terraform.lock.hcl"],
115
83
  nix: ["flake.nix"],
116
84
  toml: ["pyproject.toml", "Cargo.toml", "taplo.toml"],
85
+ csharp: DOTNET_CSHARP_ROOT_MARKERS,
86
+ fsharp: DOTNET_FSHARP_ROOT_MARKERS,
117
87
  };
88
+ function hasProjectMarker(projectRoot, marker) {
89
+ if (!marker.includes("*"))
90
+ return fs.existsSync(path.join(projectRoot, marker));
91
+ try {
92
+ return direntsHaveMarkerGlobMatch(fs.readdirSync(projectRoot, { withFileTypes: true }), marker);
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
118
98
  // Process-lifetime memo keyed on projectRoot. Only populated when the
119
99
  // caller did not pass an explicit `sourceFiles` array — the explicit-array
120
100
  // case is used by the warmup pipeline to inject pre-collected files and
@@ -142,7 +122,7 @@ function computeProjectLanguageProfile(projectRoot, sourceFiles) {
142
122
  if (!markers)
143
123
  continue;
144
124
  for (const marker of markers) {
145
- if (fs.existsSync(path.join(projectRoot, marker))) {
125
+ if (hasProjectMarker(projectRoot, marker)) {
146
126
  present[kind] = true;
147
127
  configured[kind] = true;
148
128
  break;
@@ -240,38 +220,26 @@ export function resolveLanguageRootForFile(filePath, workspaceRoot) {
240
220
  // the tree itself), and the result is stored in the shared
241
221
  // `languageProfileCache` so the subsequent sync caller skips the walk.
242
222
  // ---------------------------------------------------------------------------
243
- // Extensions accepted as project source files. Mirrors the discovery rules
244
- // used by scan-utils.ts but inlined here to keep this file self-contained
245
- // and avoid pulling in source-filter's heavier dependency graph during
246
- // warmup.
247
- const WARMUP_SOURCE_EXTS = new Set([
248
- ".ts",
249
- ".tsx",
250
- ".js",
251
- ".jsx",
252
- ".mjs",
253
- ".cjs",
254
- ".py",
255
- ".go",
256
- ".rs",
257
- ".rb",
258
- ".java",
259
- ".kt",
260
- ".swift",
261
- ".c",
262
- ".cc",
263
- ".cpp",
264
- ".cxx",
265
- ".h",
266
- ".hpp",
267
- ".cs",
268
- ]);
223
+ // Keep the warmup walker lightweight (no source-filter dependency), but derive
224
+ // its extension gate from the same authority as every other project-wide
225
+ // enumeration. Generated-artifact filtering remains intentionally absent here:
226
+ // warmup only needs language presence and never opens file contents.
227
+ export const WARMUP_SOURCE_EXTS = new Set(SUPPORTED_FILE_KINDS.flatMap((kind) => KIND_EXTENSIONS[kind]));
228
+ // Extensions belonging to CODE_KINDS — same derivation, used to give program
229
+ // source priority within the capped warmup budget (#894 review, see below).
230
+ const WARMUP_CODE_EXTS = new Set(SUPPORTED_FILE_KINDS.filter((kind) => CODE_KINDS.has(kind)).flatMap((kind) => KIND_EXTENSIONS[kind]));
269
231
  // Language detection needs which languages are PRESENT, not every file — so the
270
232
  // warmup walk is hard-capped. Without this, a walk rooted at a too-broad directory
271
233
  // (e.g. $HOME, if startup-scan's canWarmCaches guard is bypassed) traverses the
272
234
  // entire tree — #250's multi-hour home-dir scans. Generous enough to detect every
273
235
  // language present in any real project (mirrors startup-scan's 2000 limit).
274
236
  const MAX_WARMUP_SOURCE_FILES = 2000;
237
+ // Total matched-file ceiling for the warmup walk, as a multiple of `maxFiles`
238
+ // (#894 review): with per-category budgets the walk no longer stops at the
239
+ // first `maxFiles` matches, so this keeps its total work deterministic —
240
+ // at the default budget it tolerates up to ~20k data/doc files ahead of the
241
+ // code dirs before giving up on finding more code files.
242
+ const MATCHED_FILES_CEILING_FACTOR = 10;
275
243
  export async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP_SOURCE_FILES, yieldEvery = 100) {
276
244
  const root = path.resolve(rootDir);
277
245
  const ignoreMatcher = getProjectIgnoreMatcher(root);
@@ -280,9 +248,23 @@ export async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP
280
248
  // detection. Fail-open on no-git/spawn failure.
281
249
  await ignoreMatcher.ensureTrackedIndex();
282
250
  const stack = [root];
283
- const out = [];
251
+ // #894 review: `maxFiles` is a PER-CATEGORY budget — code kinds and
252
+ // data/doc kinds (NON_CODE_KINDS: json/yaml/markdown/…) fill separate
253
+ // buffers. With a single shared budget, 2000+ locale/fixture JSON files
254
+ // encountered before the code dirs exhausted the cap and flipped
255
+ // `present[kind]` to false for the project's real languages. Each buffer
256
+ // keeps the original #250 cap; the walk stops once the code budget is
257
+ // full (real languages are then detected; extra non-code presence can't
258
+ // justify more walking) or after `MATCHED_FILES_CEILING_FACTOR * maxFiles`
259
+ // matching files total — a deterministic work bound in the same spirit as
260
+ // #250's cap, generous enough that a locale/fixture pile ahead of the code
261
+ // dirs can't starve detection, tight enough that a misrooted /
262
+ // data-dominated tree stays bounded (#250/#758 class).
263
+ const codeOut = [];
264
+ const nonCodeOut = [];
265
+ let matchedSeen = 0;
284
266
  let processedSinceYield = 0;
285
- while (stack.length > 0) {
267
+ walk: while (stack.length > 0) {
286
268
  const current = stack.pop();
287
269
  if (!current)
288
270
  continue;
@@ -303,10 +285,17 @@ export async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP
303
285
  const ext = path.extname(entry.name).toLowerCase();
304
286
  if (!WARMUP_SOURCE_EXTS.has(ext))
305
287
  continue;
306
- out.push(fullPath);
307
- // Hard cap — language detection only needs presence (#250).
308
- if (out.length >= maxFiles)
309
- return out;
288
+ matchedSeen += 1;
289
+ const bucket = WARMUP_CODE_EXTS.has(ext) ? codeOut : nonCodeOut;
290
+ // Per-category hard cap — language detection only needs
291
+ // presence (#250), so overflowing files of a full category
292
+ // are dropped rather than evicting the other category.
293
+ if (bucket.length < maxFiles)
294
+ bucket.push(fullPath);
295
+ if (codeOut.length >= maxFiles ||
296
+ matchedSeen >= MATCHED_FILES_CEILING_FACTOR * maxFiles) {
297
+ break walk;
298
+ }
310
299
  }
311
300
  if (++processedSinceYield % yieldEvery === 0) {
312
301
  // See countSourceFilesWithinLimitAsync for why setImmediate.
@@ -314,7 +303,7 @@ export async function collectSourceFilesForWarmup(rootDir, maxFiles = MAX_WARMUP
314
303
  }
315
304
  }
316
305
  }
317
- return out;
306
+ return [...codeOut, ...nonCodeOut];
318
307
  }
319
308
  export async function detectProjectLanguageProfileAsync(projectRoot) {
320
309
  const cached = languageProfileCache.get(projectRoot);
@@ -1,16 +1,19 @@
1
- import * as fs from "node:fs";
2
1
  import * as path from "node:path";
3
2
  import { isTestMode } from "./env-utils.js";
4
3
  import { getGlobalPiLensDir } from "./file-utils.js";
5
4
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
+ import { getMaxLogSizeMB } from "./log-cleanup.js";
6
6
  const LATENCY_LOG_DIR = getGlobalPiLensDir();
7
7
  const LATENCY_LOG_FILE = path.join(LATENCY_LOG_DIR, "latency.log");
8
- const writer = createNdjsonLogger({ filePath: LATENCY_LOG_FILE });
8
+ const writer = createNdjsonLogger({
9
+ filePath: LATENCY_LOG_FILE,
10
+ maxBytes: getMaxLogSizeMB() * 1024 * 1024,
11
+ });
9
12
  export function logLatency(entry) {
10
13
  if (isTestMode()) {
11
14
  return;
12
15
  }
13
- writer.log({ ts: new Date().toISOString(), ...entry });
16
+ writer.log({ ...entry, ts: new Date().toISOString(), pid: process.pid });
14
17
  }
15
18
  export function getLatencyLogPath() {
16
19
  return LATENCY_LOG_FILE;
@@ -19,19 +22,6 @@ export function getLatencyLogPath() {
19
22
  export function flushLatencyLog() {
20
23
  return writer.flush();
21
24
  }
22
- export function readLatencyLog(limit = 100) {
23
- try {
24
- const content = fs.readFileSync(LATENCY_LOG_FILE, "utf-8");
25
- const lines = content.trim().split(/\r?\n/).filter(Boolean);
26
- return lines
27
- .slice(-limit)
28
- .map((line) => JSON.parse(line))
29
- .reverse();
30
- }
31
- catch {
32
- return [];
33
- }
34
- }
35
25
  export function clearLatencyLog() {
36
26
  // Enqueue the truncate in the same serialized queue so a clear cannot race a
37
27
  // pending drain. Await flushLatencyLog() if you need the file empty on disk.