pi-lens 3.8.72 → 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 (210) hide show
  1. package/CHANGELOG.md +1162 -0
  2. package/README.md +45 -28
  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/deps/js-yaml.js +1 -1
  12. package/dist/clients/diagnostic-dispositions.js +22 -2
  13. package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
  14. package/dist/clients/dispatch/dispatcher.js +16 -1
  15. package/dist/clients/dispatch/fact-store.js +11 -0
  16. package/dist/clients/dispatch/facts/function-facts.js +14 -0
  17. package/dist/clients/dispatch/facts/import-facts.js +100 -101
  18. package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
  19. package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
  20. package/dist/clients/dispatch/integration.js +94 -15
  21. package/dist/clients/dispatch/rules/async-noise.js +1 -0
  22. package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
  23. package/dist/clients/dispatch/runners/ast-grep-napi.js +84 -8
  24. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
  25. package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
  26. package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
  27. package/dist/clients/feature-hints.js +53 -7
  28. package/dist/clients/file-kinds.js +86 -31
  29. package/dist/clients/fix-worklog.js +14 -2
  30. package/dist/clients/generated-artifacts.js +21 -0
  31. package/dist/clients/gzip-stage-write.js +84 -0
  32. package/dist/clients/installer/index.js +358 -129
  33. package/dist/clients/instance-registry.js +12 -1
  34. package/dist/clients/knip-client.js +64 -1
  35. package/dist/clients/language-profile.js +61 -72
  36. package/dist/clients/latency-logger.js +6 -16
  37. package/dist/clients/lens-config.js +128 -165
  38. package/dist/clients/lens-engine.js +39 -5
  39. package/dist/clients/lens-flag-registry.js +285 -0
  40. package/dist/clients/lens-map.js +2 -7
  41. package/dist/clients/log-cleanup.js +6 -17
  42. package/dist/clients/lsp/cascade-tier.js +22 -2
  43. package/dist/clients/lsp/client.js +74 -10
  44. package/dist/clients/lsp/config.js +62 -9
  45. package/dist/clients/lsp/index.js +113 -31
  46. package/dist/clients/lsp/language.js +4 -1
  47. package/dist/clients/lsp/launch.js +8 -6
  48. package/dist/clients/lsp/server.js +84 -29
  49. package/dist/clients/lsp-document-symbols.js +112 -0
  50. package/dist/clients/mcp/analyze.js +5 -4
  51. package/dist/clients/mcp/review.js +36 -3
  52. package/dist/clients/module-report.js +100 -80
  53. package/dist/clients/ndjson-logger.js +51 -21
  54. package/dist/clients/path-keyed-map.js +76 -0
  55. package/dist/clients/path-utils.js +35 -0
  56. package/dist/clients/performance-report.js +327 -0
  57. package/dist/clients/pipeline.js +7 -0
  58. package/dist/clients/project-changes.js +77 -4
  59. package/dist/clients/project-diagnostics/extractors.js +26 -115
  60. package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
  61. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +109 -0
  62. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
  63. package/dist/clients/project-diagnostics/scanner.js +247 -127
  64. package/dist/clients/project-lens-config.js +53 -26
  65. package/dist/clients/project-report.js +39 -14
  66. package/dist/clients/project-scale.js +8 -10
  67. package/dist/clients/project-snapshot-persist-worker.js +6 -0
  68. package/dist/clients/project-snapshot.js +611 -7
  69. package/dist/clients/quiet-window.js +9 -1
  70. package/dist/clients/read-expansion.js +22 -12
  71. package/dist/clients/read-guard.js +78 -0
  72. package/dist/clients/redact/secrets.js +449 -0
  73. package/dist/clients/resource-sampler.js +133 -14
  74. package/dist/clients/reverse-deps.js +48 -0
  75. package/dist/clients/review-graph/builder.js +1522 -179
  76. package/dist/clients/review-graph/persist-worker.js +10 -0
  77. package/dist/clients/review-graph/query.js +5 -0
  78. package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
  79. package/dist/clients/review-graph-logger.js +24 -0
  80. package/dist/clients/runtime-agent-end.js +4 -2
  81. package/dist/clients/runtime-coordinator.js +2 -2
  82. package/dist/clients/runtime-session.js +433 -59
  83. package/dist/clients/runtime-tool-call.js +46 -14
  84. package/dist/clients/runtime-turn.js +97 -26
  85. package/dist/clients/safe-spawn.js +102 -15
  86. package/dist/clients/session-start-observability.js +15 -0
  87. package/dist/clients/session-state-store.js +2 -1
  88. package/dist/clients/sessionstart-logger.js +18 -0
  89. package/dist/clients/sg-runner.js +48 -33
  90. package/dist/clients/source-filter.js +49 -25
  91. package/dist/clients/startup-marker.js +6 -0
  92. package/dist/clients/startup-timing.js +9 -0
  93. package/dist/clients/todo-scanner.js +30 -3
  94. package/dist/clients/tree-sitter-cache.js +147 -139
  95. package/dist/clients/tree-sitter-client.js +1340 -67
  96. package/dist/clients/tree-sitter-logger.js +43 -1
  97. package/dist/clients/tree-sitter-query-loader.js +110 -11
  98. package/dist/clients/tree-sitter-shared.js +43 -19
  99. package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
  100. package/dist/clients/widget-state.js +50 -14
  101. package/dist/clients/word-index-logger.js +44 -0
  102. package/dist/clients/word-index.js +252 -35
  103. package/dist/clients/workspace-topology.js +11 -1
  104. package/dist/index.js +45976 -39387
  105. package/dist/mcp/analyze-cli.js +3 -0
  106. package/dist/mcp/cli.js +110 -0
  107. package/dist/mcp/server.js +63 -8
  108. package/dist/tools/lens-diagnostics.js +47 -5
  109. package/dist/tools/lsp-diagnostics.js +126 -9
  110. package/dist/tools/lsp-navigation.js +5 -28
  111. package/dist/tools/symbol-search.js +21 -2
  112. package/docs/agent-guide.md +247 -0
  113. package/docs/astplayground.md +3 -4
  114. package/docs/audit1.md +5 -5
  115. package/docs/environment-variables.md +153 -11
  116. package/docs/features.md +4 -3
  117. package/docs/globalconfig.md +56 -4
  118. package/docs/language-coverage.md +2 -2
  119. package/docs/mcp.md +17 -16
  120. package/docs/servercapabilities.md +5 -2
  121. package/docs/settings.md +181 -0
  122. package/docs/tools.md +14 -0
  123. package/docs/tree-sitter_rules_catalog.md +11 -31
  124. package/docs/usage.md +13 -0
  125. package/package.json +139 -139
  126. package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
  127. package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
  128. package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
  129. package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
  130. package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
  131. package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
  132. package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
  133. package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
  134. package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
  135. package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
  136. package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
  137. package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
  138. package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
  139. package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
  140. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
  141. package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
  142. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
  143. package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
  144. package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
  145. package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
  146. package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
  147. package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
  148. package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
  149. package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
  150. package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
  151. package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
  152. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
  153. package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
  154. package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
  155. package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
  156. package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
  157. package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
  158. package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
  159. package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
  160. package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
  161. package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
  162. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
  163. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
  164. package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
  165. package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
  166. package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
  167. package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
  168. package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
  169. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
  170. package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
  171. package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
  172. package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
  173. package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
  174. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
  175. package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
  176. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
  177. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
  178. package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
  179. package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
  180. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
  181. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
  182. package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
  183. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
  184. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
  185. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
  186. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
  187. package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +1 -0
  188. package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
  189. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
  190. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
  191. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
  192. package/rules/typos/_typos.toml +17 -0
  193. package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
  194. package/dist/clients/config-enabled-shape.js +0 -39
  195. package/docs/env_variables.md +0 -10
  196. package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
  197. package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
  198. package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
  199. package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
  200. package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
  201. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
  202. package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
  203. package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
  204. package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
  205. package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
  206. package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
  207. package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
  208. package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
  209. package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
  210. package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
@@ -2,15 +2,14 @@
2
2
  * Fact-provider-specific tree-sitter helper (#402).
3
3
  *
4
4
  * The generic node-walk utilities (`walk`, `childrenOfType`, `firstChildOfType`,
5
- * `parseTreeSitterRoot`, `TsNode`) live in `clients/tree-sitter-shared.ts` so both
5
+ * `withTreeSitterRoot`, `TsNode`) live in `clients/tree-sitter-shared.ts` so both
6
6
  * the fact extractors and the complexity client share them. This module re-exports
7
7
  * them for the extractors and adds the FactProvider-specific `extractFactsFromTree`
8
8
  * shell.
9
9
  */
10
- import { childrenOfType, firstChildOfType, parseTreeSitterRoot, walk, } from "../../tree-sitter-shared.js";
10
+ import { childrenOfType, firstChildOfType, withTreeSitterRoot, walk, } from "../../tree-sitter-shared.js";
11
11
  export { childrenOfType, firstChildOfType, walk, };
12
- /** Parse `content` for `filePath` into a root node (or null). Alias of the shared helper. */
13
- export const parseFactTree = parseTreeSitterRoot;
12
+ export const withFactTree = withTreeSitterRoot;
14
13
  /**
15
14
  * Provider shell for tree-sitter-backed fact extractors: read `file.content`,
16
15
  * parse via the shared client, and hand the root node to `extract`. On empty
@@ -27,8 +26,6 @@ export async function extractFactsFromTree(ctx, store, defaults, extract) {
27
26
  const content = store.getFileFact(ctx.filePath, "file.content");
28
27
  if (!content)
29
28
  return writeAll(defaults);
30
- const root = await parseFactTree(ctx.filePath, content);
31
- if (!root)
32
- return writeAll(defaults);
33
- writeAll(extract(root, content));
29
+ const parsed = await withFactTree(ctx.filePath, content, (root) => extract(root, content));
30
+ writeAll(parsed.parsed ? parsed.value : defaults);
34
31
  }
@@ -1,5 +1,16 @@
1
1
  import { extractFactsFromTree, firstChildOfType, walk, } from "./tree-sitter-facts.js";
2
2
  // --- Helpers ---
3
+ /** Does `node`'s subtree contain an `assignment_expression` (`x = ...`, `x += ...`, …)? */
4
+ function hasAssignmentExpression(node) {
5
+ if (!node)
6
+ return false;
7
+ let found = false;
8
+ walk(node, (n) => {
9
+ if (n.type === "assignment_expression")
10
+ found = true;
11
+ });
12
+ return found;
13
+ }
3
14
  function isOnlyWhitespaceOrComments(text) {
4
15
  let stripped = text.replace(/\/\*[\s\S]*?\*\//g, "");
5
16
  stripped = stripped.replace(/\/\/[^\n]*/g, "");
@@ -74,6 +85,7 @@ export const tryCatchFactProvider = {
74
85
  const catchReturnsDefault = DEFAULT_VALUE_PATTERN.test(bodyText);
75
86
  const catchReturnsStructuredError = STRUCTURED_ERROR_PATTERN.test(bodyText);
76
87
  const isDocumentedLocalFallback = EXPLAINING_COMMENT_PATTERN.test(bodyText);
88
+ const catchHasFallbackAssignment = hasAssignmentExpression(catchBody);
77
89
  const catchLogsOnly = hasLogging &&
78
90
  !hasRethrow &&
79
91
  !catchReturnsDefault &&
@@ -95,6 +107,7 @@ export const tryCatchFactProvider = {
95
107
  catchReturnsDefault,
96
108
  catchReturnsStructuredError,
97
109
  isDocumentedLocalFallback,
110
+ catchHasFallbackAssignment,
98
111
  tryResolvesLocalValues,
99
112
  isFilesystemExistenceProbe,
100
113
  boundaryCategory,
@@ -25,8 +25,8 @@ import { getServersForFileWithConfig } from "../lsp/config.js";
25
25
  import { getLSPService } from "../lsp/index.js";
26
26
  import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
27
27
  import { getProjectIgnoreMatcher } from "../file-utils.js";
28
- import { clearReviewGraphWorkspaceCache, getLastGraphBuildInfo, } from "../review-graph/builder.js";
29
- import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
28
+ import { clearReviewGraphWorkspaceCache, getGraphImportChanges, getLastGraphBuildInfo, } from "../review-graph/builder.js";
29
+ import { buildReverseDependencyIndexFromGraph, getAffectedFilesFromIndex, patchReverseDependencyIndex, writeReverseDependencyIndexToSnapshot, } from "../reverse-deps.js";
30
30
  import { buildOrUpdateGraph, computeImpactCascade, computeTransitiveImpact, formatImpactCascade, } from "../review-graph/service.js";
31
31
  import { clearModuleGraphCache } from "../review-graph/workspace-modules.js";
32
32
  import { RUNTIME_CONFIG } from "../runtime-config.js";
@@ -498,17 +498,15 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
498
498
  // `wordIndex`/`fileContent` doc comments for the cold/no-forward-index
499
499
  // no-op rules.
500
500
  //
501
- // Deliberately keyed by `path.resolve(filePath)`, NOT `normalizedFile`
502
- // (which is `normalizeMapKey`'d — realpath-canonicalized + lowercased on
503
- // Windows). The word index's OWN keys come from `collectWordIndexDocs` →
504
- // `collectSourceFilesAsync`'s file walk, which yields plain
505
- // `path.resolve()`-joined paths (native separators, on-disk casing as
506
- // reported by the walk, no realpath call). Keying this update with the
507
- // cascade's normalized key would silently create a SECOND, orphaned entry
508
- // next to the walker's original-cased entry instead of replacing it —
509
- // exactly the kind of divergence the equivalence-property test is meant to
510
- // catch, so the key shape here must match the build path's, not the
511
- // cascade/graph's own (different) normalization scheme.
501
+ // Keyed by `path.resolve(filePath)` (native separators, tool-input casing).
502
+ // Since #1025 the word index's path maps are `PathKeyedMap`s that fold every
503
+ // key through `wordIndexKey` (`normalizeEphemeralMapKey` — slash-fold +
504
+ // win32-lowercase) INTERNALLY, so this per-edit key and the build path's own
505
+ // walk-derived key (`collectWordIndexDocs` → `collectSourceFilesAsync`)
506
+ // collapse to the same entry regardless of on-disk-vs-input casing/separator.
507
+ // The old hazard — this update silently orphaning a SECOND entry next to the
508
+ // walker's original-cased one — is now structurally impossible at the map
509
+ // layer, so this seam no longer has to hand-match the build path's key shape.
512
510
  updateWordIndexForCascade({
513
511
  wordIndex,
514
512
  filePath: nodePath.resolve(filePath),
@@ -544,15 +542,32 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
544
542
  const graphBuildInfo = getLastGraphBuildInfo();
545
543
  const workspaceKey = normalizeMapKey(cwd);
546
544
  const cachedReverseDeps = reverseDepsIndexCache.get(workspaceKey);
545
+ const importDelta = getGraphImportChanges(graph);
546
+ // A one-step delta is only usable against an index cached at exactly the
547
+ // delta's predecessor generation. Builds minted elsewhere (mcp analyze,
548
+ // lens-map, session warm) advance generations whose import changes this
549
+ // delta does not cover — reusing/patching across that gap stamps a stale
550
+ // index as current, and generation equality then hides the loss forever
551
+ // (#939 review finding 1).
552
+ const deltaContiguous = importDelta !== undefined &&
553
+ importDelta.fromGeneration !== undefined &&
554
+ cachedReverseDeps !== undefined &&
555
+ cachedReverseDeps.generation === importDelta.fromGeneration;
556
+ const importChanges = deltaContiguous ? importDelta.changes : undefined;
557
+ const importsChanged = importChanges?.some((change) => change.existedBefore !== change.existsAfter ||
558
+ change.priorTargets.length !== change.newTargets.length ||
559
+ change.priorTargets.some((target, index) => target !== change.newTargets[index]));
547
560
  const canReuse = reverseDepsReuseEnabled() &&
548
561
  cachedReverseDeps !== undefined &&
549
- graph.buildGeneration !== undefined &&
550
- cachedReverseDeps.generation === graph.buildGeneration;
562
+ ((graph.buildGeneration !== undefined &&
563
+ cachedReverseDeps.generation === graph.buildGeneration) ||
564
+ (importChanges !== undefined && !importsChanged));
551
565
  let reverseDepsIndex;
552
566
  let reverseDepsSaved;
553
567
  if (canReuse && cachedReverseDeps) {
554
568
  reverseDepsIndex = cachedReverseDeps.index;
555
569
  reverseDepsSaved = cachedReverseDeps.savedToSnapshot;
570
+ cachedReverseDeps.generation = graph.buildGeneration;
556
571
  logCascade({
557
572
  phase: "reverse_deps_cache",
558
573
  filePath,
@@ -565,6 +580,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
565
580
  },
566
581
  });
567
582
  }
583
+ else if (cachedReverseDeps && importChanges && importsChanged) {
584
+ reverseDepsIndex = patchReverseDependencyIndex(cachedReverseDeps.index, importChanges);
585
+ reverseDepsSaved = writeReverseDependencyIndexToSnapshot({
586
+ cwd,
587
+ index: reverseDepsIndex,
588
+ dbg,
589
+ });
590
+ reverseDepsIndexCache.set(workspaceKey, {
591
+ index: reverseDepsIndex,
592
+ savedToSnapshot: reverseDepsSaved,
593
+ generation: graph.buildGeneration,
594
+ });
595
+ logCascade({
596
+ phase: "reverse_deps_cache",
597
+ filePath,
598
+ durationMs: Date.now() - graphStart,
599
+ metadata: {
600
+ action: "patched_import_changes",
601
+ changedFileCount: importChanges.length,
602
+ savedToSnapshot: reverseDepsSaved,
603
+ },
604
+ });
605
+ }
568
606
  else {
569
607
  reverseDepsIndex = buildReverseDependencyIndexFromGraph({
570
608
  cwd,
@@ -614,6 +652,29 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
614
652
  },
615
653
  });
616
654
  impact = computeImpactCascade(graph, normalizedFile, cwd);
655
+ // #1023: buildOrUpdateGraph returns an EMPTY graph (seeded only with the
656
+ // changed file's own symbols) when the repo is over
657
+ // PI_LENS_REVIEW_GRAPH_MAX_FILES (`too_many_files`) or the root is unsafe
658
+ // (`unsafe_root`) — both stamp mode "skipped" on the already-read
659
+ // graphBuildInfo. Computing impact against that empty graph yields zero
660
+ // neighbors for EVERY edit, indistinguishable from a genuine clean leaf.
661
+ // Thread the ALREADY-KNOWN degraded state (never re-derived) onto the
662
+ // result so the turn-end seam surfaces an honest advisory instead of a
663
+ // silent all-clear (#533). Keyed strictly off the degraded build mode —
664
+ // NOT off `neighborFiles.length === 0` — so a healthy build with a real
665
+ // empty dependent set stays silent (no crying wolf).
666
+ if (graphBuildInfo.mode === "skipped" && !impact.indeterminate) {
667
+ impact.indeterminate = {
668
+ reason: "graph_degraded",
669
+ detail: graphBuildInfo.skipReason === "too_many_files"
670
+ ? `review graph disabled — ${graphBuildInfo.sourceFileCount ?? "?"} files over the ${graphBuildInfo.maxFileCount ?? "?"} cap`
671
+ : graphBuildInfo.skipReason === "unsafe_root"
672
+ ? "review graph skipped — workspace root is at/above home dir"
673
+ : `review graph unavailable (${graphBuildInfo.skipReason ?? "skipped"})`,
674
+ sourceFileCount: graphBuildInfo.sourceFileCount,
675
+ maxFileCount: graphBuildInfo.maxFileCount,
676
+ };
677
+ }
617
678
  const reverseDepNeighbors = getAffectedFilesFromIndex(reverseDepsIndex, normalizedFile, 1, MAX_FILES * 2);
618
679
  logCascade({
619
680
  phase: "reverse_deps_cache",
@@ -1117,6 +1178,20 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1117
1178
  cascadeSessionStats.diagnosticsSurfaced += diagCount;
1118
1179
  cascadeSessionStats.coldSnapshotTouches += coldSnapshotPaths.length;
1119
1180
  if (!formatted) {
1181
+ // #1023: an indeterminate compute (degraded/cold/missing-node graph) must
1182
+ // NOT collapse into "no_neighbors" — that is the exact silent all-clear the
1183
+ // bug is about. Distinguish it by the marker threaded onto `impact`, never
1184
+ // by `visibleNeighbors.length === 0` alone (a healthy leaf is also empty).
1185
+ if (impact.indeterminate) {
1186
+ return {
1187
+ filePath,
1188
+ result: undefined,
1189
+ neighborCount: visibleNeighbors.length,
1190
+ diagnosticCount: diagCount,
1191
+ skipReason: "indeterminate",
1192
+ indeterminate: impact.indeterminate,
1193
+ };
1194
+ }
1120
1195
  const skipReason = visibleNeighbors.length === 0 ? "no_neighbors" : "clean";
1121
1196
  return {
1122
1197
  filePath,
@@ -1132,6 +1207,10 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
1132
1207
  result: { filePath, impact, neighbors: visibleNeighbors, formatted },
1133
1208
  neighborCount: visibleNeighbors.length,
1134
1209
  diagnosticCount: diagCount,
1210
+ // #1023: even when some fallback neighbors surfaced, a degraded graph means
1211
+ // the dependent set is INCOMPLETE — carry the marker so the turn-end seam
1212
+ // still notes downstream impact was under-computed this turn.
1213
+ ...(impact.indeterminate && { indeterminate: impact.indeterminate }),
1135
1214
  };
1136
1215
  }
1137
1216
  function diagnosticDeltaKey(diagnostic) {
@@ -18,6 +18,7 @@ export const asyncNoiseRule = {
18
18
  !fn.hasAwait &&
19
19
  !fn.hasReturnAwaitCall &&
20
20
  !fn.isPassThroughWrapper &&
21
+ !fn.hasExplicitPromiseReturnType &&
21
22
  !ASYNC_INTERFACE_PATTERN.test(fn.name) &&
22
23
  // Skip single-statement functions — likely interface stubs or thin wrappers
23
24
  fn.statementCount > 1) {
@@ -53,6 +53,13 @@ function hasCatchCoverage(fn, catches) {
53
53
  // Catch body contains a return statement → structured error result
54
54
  if (/\breturn\b/.test(c.bodyText))
55
55
  return true;
56
+ // Catch body falls back by mutating existing state (e.g. extending a
57
+ // validity window on refresh failure) instead of rethrowing/logging/
58
+ // returning. That's still a real handler — the exception doesn't
59
+ // escape uncaught — just expressed via assignment rather than one of
60
+ // the three shapes above (#969).
61
+ if (c.catchHasFallbackAssignment)
62
+ return true;
56
63
  return false;
57
64
  });
58
65
  }
@@ -9,10 +9,14 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { loadAstGrepNapi, } from "../../deps/ast-grep-napi.js";
12
+ import { minimatch } from "../../deps/minimatch.js";
12
13
  import { getAstGrepRuleSources, } from "../../sgconfig.js";
14
+ import { logLatency } from "../../latency-logger.js";
13
15
  import { hasEslintConfig } from "../../tool-policy.js";
14
16
  import { enabledAuxiliaryLspServerIds } from "../auxiliary-lsp.js";
15
17
  import { classifyDefect } from "../diagnostic-taxonomy.js";
18
+ import { isAuxiliaryLspAlive } from "../../lsp/index.js";
19
+ import { resolveAstGrepNativeExe } from "../../lsp/wait-policy/index.js";
16
20
  import { PRIORITY } from "../priorities.js";
17
21
  import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, loadYamlRules, loadYamlRulesFresh, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
18
22
  // Lazy load the napi package
@@ -117,28 +121,48 @@ function explicitRuleFixSuggestion(rule) {
117
121
  function normalizeRuleId(ruleId) {
118
122
  return ruleId.replace(/-js$/, "");
119
123
  }
124
+ /**
125
+ * `filePath` relative to `root`, forward-slashed, for matching a rule's
126
+ * `ignores` globs (#965). Falls back to the absolute (slash-normalized) path
127
+ * when `filePath` isn't under `root` (e.g. an out-of-tree temp file), so a
128
+ * glob like `scripts/**` simply never matches rather than throwing.
129
+ */
130
+ function relativeForIgnoreGlob(filePath, root) {
131
+ const rel = path.relative(root, filePath);
132
+ return (rel.startsWith("..") ? filePath : rel).split(path.sep).join("/");
133
+ }
134
+ function matchesRuleIgnores(filePath, root, patterns) {
135
+ if (!patterns || patterns.length === 0)
136
+ return false;
137
+ const rel = relativeForIgnoreGlob(filePath, root);
138
+ return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
139
+ }
120
140
  export function canHandle(filePath) {
121
141
  return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
122
142
  }
123
143
  /**
124
144
  * The TypeScript grammar is a syntactic superset of JavaScript, so a
125
145
  * `JavaScript`-tagged rule using generic node kinds (`variable_declarator`,
126
- * `assignment_expression`, …) still matches against a parsed `.ts`/`.tsx`
127
- * root — and vice versa isn't an issue since JS files never parse
146
+ * `assignment_expression`, …) still matches against a parsed `.ts` root
147
+ * — and vice versa isn't an issue since JS files never parse
128
148
  * TS-only syntax, but a `TypeScript`-tagged rule with a plain-JS-compatible
129
149
  * body would equally double-fire alongside a `JavaScript` twin on a `.ts`
130
150
  * file. Without this, `language:` reads as a real filter but isn't one for
131
151
  * ts↔js pairs, so twin rules sharing a base name (e.g. `hardcoded-url` /
132
152
  * `hardcoded-url-js`) both match the same construct in the SAME runner
133
- * invocation (#657). Returns undefined for extensions this scoping doesn't
134
- * apply to (css/html), where no filtering is added.
153
+ * invocation (#657). TSX is deliberately its own grammar here: the primary
154
+ * ast-grep CLI/LSP also treats `tsx` as distinct from `typescript`, so a
155
+ * language-tagged rule only runs on the exact grammar it names. Returns
156
+ * undefined for extensions this scoping doesn't apply to (css/html), where
157
+ * no filtering is added.
135
158
  */
136
159
  export function ruleLanguageForFile(filePath) {
137
160
  const ext = path.extname(filePath).toLowerCase();
138
161
  switch (ext) {
139
162
  case ".ts":
140
- case ".tsx":
141
163
  return "typescript";
164
+ case ".tsx":
165
+ return "tsx";
142
166
  case ".js":
143
167
  case ".jsx":
144
168
  return "javascript";
@@ -213,13 +237,36 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
213
237
  const maxTotalDiagnostics = options.maxTotalDiagnostics ?? MAX_TOTAL_DIAGNOSTICS;
214
238
  const blockingOnly = options.blockingOnly === true;
215
239
  const log = options.log;
240
+ const unsupportedLanguageLog = options.unsupportedLanguageLog ?? new Set();
216
241
  const diagnostics = [];
217
242
  const seenRuleIds = new Set();
218
243
  const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
219
244
  const fileLang = ruleLanguageForFile(filePath);
245
+ // Unsupported-language skips are expected in bulk (every non-jsts rule in the
246
+ // catalog, e.g. ~30 Python rules) — aggregate them into ONE latency-log entry
247
+ // per evaluation instead of per-rule terminal lines (#282 follow-up).
248
+ const newlyUnsupported = new Map();
249
+ const flushUnsupportedRuleSkips = () => {
250
+ if (newlyUnsupported.size === 0)
251
+ return;
252
+ logLatency({
253
+ type: "phase",
254
+ phase: "astgrep_napi_unsupported_rules_skipped",
255
+ filePath,
256
+ durationMs: 0,
257
+ metadata: {
258
+ skippedByLanguage: Object.fromEntries(Array.from(newlyUnsupported.entries(), ([language, ruleIds]) => [
259
+ language,
260
+ { count: ruleIds.length, ruleIds },
261
+ ])),
262
+ },
263
+ });
264
+ newlyUnsupported.clear();
265
+ };
220
266
  // Shared with the raw sgconfig materializer so both surfaces walk the same
221
267
  // workspace-rooted sources in the same precedence order.
222
- const ruleSources = getAstGrepRuleSources(options.projectRoot ?? cwd);
268
+ const ignoreRoot = options.projectRoot ?? cwd;
269
+ const ruleSources = getAstGrepRuleSources(ignoreRoot);
223
270
  for (const source of ruleSources) {
224
271
  let rules;
225
272
  try {
@@ -233,6 +280,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
233
280
  }
234
281
  const duplicates = duplicateRuleIds(rules);
235
282
  if (appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicates, source, filePath, maxTotalDiagnostics)) {
283
+ flushUnsupportedRuleSkips();
236
284
  return diagnostics;
237
285
  }
238
286
  const duplicateSet = new Set(duplicates);
@@ -245,6 +293,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
245
293
  seenRuleIds.add(rule.id);
246
294
  if (blockingOnly && rule.severity !== "error")
247
295
  continue;
296
+ // Per-rule path carve-out (#965): a rule that's noise on CLI scripts or
297
+ // a project's own logging sink (e.g. no-console-except-error firing
298
+ // inside scripts/** or lib/logger.ts) opts out via `ignores`.
299
+ if (matchesRuleIgnores(filePath, ignoreRoot, rule.ignores))
300
+ continue;
248
301
  if (suppressLinterOverlap &&
249
302
  LINTER_OVERLAP.has(normalizeRuleId(rule.id)) &&
250
303
  !NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
@@ -258,7 +311,16 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
258
311
  continue;
259
312
  }
260
313
  const lang = rule.language?.toLowerCase();
261
- if (lang && lang !== "typescript" && lang !== "javascript") {
314
+ if (lang &&
315
+ lang !== "typescript" &&
316
+ lang !== "tsx" &&
317
+ lang !== "javascript") {
318
+ if (!unsupportedLanguageLog.has(rule.id)) {
319
+ unsupportedLanguageLog.add(rule.id);
320
+ const ids = newlyUnsupported.get(lang) ?? [];
321
+ ids.push(rule.id);
322
+ newlyUnsupported.set(lang, ids);
323
+ }
262
324
  continue;
263
325
  }
264
326
  // Scope TypeScript/JavaScript-tagged rules to the file's actual
@@ -338,6 +400,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
338
400
  }
339
401
  }
340
402
  }
403
+ flushUnsupportedRuleSkips();
341
404
  return diagnostics;
342
405
  }
343
406
  // --- Runner Definition ---
@@ -357,7 +420,20 @@ const astGrepNapiRunner = {
357
420
  // report against the LSP's `tool: ast-grep` diagnostics. Resume ONLY as the
358
421
  // fallback when the binary is absent / can't spawn (Gate B).
359
422
  const astGrepLspEnabled = enabledAuxiliaryLspServerIds((f) => ctx.pi?.getFlag?.(f)).includes("ast-grep");
360
- if (astGrepLspEnabled && (await ctx.hasTool("ast-grep"))) {
423
+ // Gate B asks whether the LSP will handle this file, not whether a bare
424
+ // `ast-grep` command happens to be on PATH. The launcher first tries the
425
+ // platform-native package binary, then PATH; mirror that resolution here.
426
+ // A live client covers the already-warm case, while a resolvable binary
427
+ // covers the cold case before the LSP has spawned for this root.
428
+ const astGrepLspAlive = astGrepLspEnabled
429
+ ? await isAuxiliaryLspAlive("ast-grep", ctx.filePath)
430
+ : false;
431
+ let astGrepBinaryResolvable = false;
432
+ if (astGrepLspEnabled && !astGrepLspAlive) {
433
+ astGrepBinaryResolvable =
434
+ Boolean(resolveAstGrepNativeExe()) || (await ctx.hasTool("ast-grep"));
435
+ }
436
+ if (astGrepLspEnabled && (astGrepLspAlive || astGrepBinaryResolvable)) {
361
437
  return { status: "skipped", diagnostics: [], semantic: "none" };
362
438
  }
363
439
  const sgModule = await loadSg();
@@ -19,7 +19,16 @@ let psAnalyzerAvailable = undefined;
19
19
  const PS_TIMEOUT_MS = 30000;
20
20
  function spawnPs(cmd, args, timeoutMs = PS_TIMEOUT_MS) {
21
21
  return new Promise((resolve) => {
22
- const child = spawn(cmd, args, { windowsHide: true, shell: false });
22
+ let child;
23
+ try {
24
+ child = spawn(cmd, args, { windowsHide: true, shell: false });
25
+ }
26
+ catch {
27
+ // SYNCHRONOUS spawn throw (Windows `spawn UNKNOWN`/EINVAL, the pidusage
28
+ // bug class, #533) — this best-effort runner must resolve, not reject.
29
+ resolve({ stdout: "", stderr: "", status: null });
30
+ return;
31
+ }
23
32
  let stdout = "";
24
33
  let stderr = "";
25
34
  let settled = false;