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
@@ -1,5 +1,5 @@
1
1
  import { logLatency } from "../../latency-logger.js";
2
- import { childrenOfType, firstChildOfType, parseFactTree, walk, } from "./tree-sitter-facts.js";
2
+ import { childrenOfType, firstChildOfType, withFactTree, walk, } from "./tree-sitter-facts.js";
3
3
  // JS/TS extensions this provider handles (→ tree-sitter typescript/tsx/javascript
4
4
  // grammars via resolveTreeSitterLanguage). Ported off the `typescript` compiler API
5
5
  // onto tree-sitter (#402) — the parse is served from the shared, cached client.
@@ -96,115 +96,114 @@ export const importFactProvider = {
96
96
  }
97
97
  // Grammar unavailable / parse failed / wasm aborted — degrade to empty
98
98
  // (there is no typescript-compiler fallback by design, #402).
99
- const root = await parseFactTree(ctx.filePath, content);
100
- if (!root) {
101
- setEmpty();
102
- return;
103
- }
104
- // --- module-type detection + static import/re-export node collection ---
105
- let hasEsm = false;
106
- let hasCjs = false;
107
- const importNodes = [];
108
- const reExportNodes = [];
109
- for (const child of root.children ?? []) {
110
- if (!child)
111
- continue;
112
- if (child.type === "import_statement") {
113
- hasEsm = true;
114
- importNodes.push(child);
115
- }
116
- else if (child.type === "export_statement") {
117
- const isReExport = Boolean(firstChildOfType(child, "string"));
118
- // `export { x }` / `export * from` / `export { x } from` are ESM
119
- // declarations (like TS's isExportDeclaration); `export const x` /
120
- // `export default` (a wrapped declaration) is NOT counted here.
121
- if (isReExport || firstChildOfType(child, "export_clause")) {
99
+ const parsed = await withFactTree(ctx.filePath, content, (root) => {
100
+ // --- module-type detection + static import/re-export node collection ---
101
+ let hasEsm = false;
102
+ let hasCjs = false;
103
+ const importNodes = [];
104
+ const reExportNodes = [];
105
+ for (const child of root.children ?? []) {
106
+ if (!child)
107
+ continue;
108
+ if (child.type === "import_statement") {
122
109
  hasEsm = true;
110
+ importNodes.push(child);
111
+ }
112
+ else if (child.type === "export_statement") {
113
+ const isReExport = Boolean(firstChildOfType(child, "string"));
114
+ // `export { x }` / `export * from` / `export { x } from` are ESM
115
+ // declarations (like TS's isExportDeclaration); `export const x` /
116
+ // `export default` (a wrapped declaration) is NOT counted here.
117
+ if (isReExport || firstChildOfType(child, "export_clause")) {
118
+ hasEsm = true;
119
+ }
120
+ if (isReExport)
121
+ reExportNodes.push(child);
123
122
  }
124
- if (isReExport)
125
- reExportNodes.push(child);
126
123
  }
127
- }
128
- // --- dynamic import() / require() / module.exports (anywhere in the tree) ---
129
- const dynamicRaw = [];
130
- walk(root, (node) => {
131
- if (node.type === "call_expression") {
132
- const callee = node.children?.[0];
133
- const args = firstChildOfType(node, "arguments");
134
- const strArg = args ? firstChildOfType(args, "string") : undefined;
135
- if (!strArg)
136
- return; // non-string arg (e.g. template literal) — skip
137
- const source = stripQuotes(strArg.text);
138
- if (callee?.type === "import") {
139
- dynamicRaw.push({ source, kind: "import" });
124
+ // --- dynamic import() / require() / module.exports (anywhere in the tree) ---
125
+ const dynamicRaw = [];
126
+ walk(root, (node) => {
127
+ if (node.type === "call_expression") {
128
+ const callee = node.children?.[0];
129
+ const args = firstChildOfType(node, "arguments");
130
+ const strArg = args ? firstChildOfType(args, "string") : undefined;
131
+ if (!strArg)
132
+ return; // non-string arg (e.g. template literal) — skip
133
+ const source = stripQuotes(strArg.text);
134
+ if (callee?.type === "import") {
135
+ dynamicRaw.push({ source, kind: "import" });
136
+ }
137
+ else if (callee?.type === "identifier" && callee.text === "require") {
138
+ dynamicRaw.push({ source, kind: "require" });
139
+ hasCjs = true;
140
+ }
140
141
  }
141
- else if (callee?.type === "identifier" && callee.text === "require") {
142
- dynamicRaw.push({ source, kind: "require" });
143
- hasCjs = true;
142
+ else if (node.type === "member_expression") {
143
+ // module.exports = ...
144
+ const obj = node.children?.[0];
145
+ if (obj?.type === "identifier" && obj.text === "module") {
146
+ const prop = firstChildOfType(node, "property_identifier");
147
+ if (prop?.text === "exports")
148
+ hasCjs = true;
149
+ }
144
150
  }
151
+ });
152
+ const moduleType = hasEsm && !hasCjs
153
+ ? "esm"
154
+ : hasCjs && !hasEsm
155
+ ? "cjs"
156
+ : hasEsm || hasCjs
157
+ ? "esm" // mixed — static imports present, treat as ESM
158
+ : "unknown";
159
+ // --- build entries ---
160
+ const imports = [];
161
+ for (const node of importNodes) {
162
+ const entry = parseStaticImport(node, moduleType);
163
+ if (entry)
164
+ imports.push(entry);
145
165
  }
146
- else if (node.type === "member_expression") {
147
- // module.exports = ...
148
- const obj = node.children?.[0];
149
- if (obj?.type === "identifier" && obj.text === "module") {
150
- const prop = firstChildOfType(node, "property_identifier");
151
- if (prop?.text === "exports")
152
- hasCjs = true;
166
+ let dynamicCount = 0;
167
+ for (const d of dynamicRaw) {
168
+ if (d.kind === "import") {
169
+ imports.push({
170
+ source: d.source,
171
+ names: [],
172
+ isDynamic: true,
173
+ moduleType,
174
+ });
175
+ }
176
+ else {
177
+ imports.push({ source: d.source, names: [], moduleType: "cjs" });
153
178
  }
179
+ dynamicCount++;
154
180
  }
155
- });
156
- const moduleType = hasEsm && !hasCjs
157
- ? "esm"
158
- : hasCjs && !hasEsm
159
- ? "cjs"
160
- : hasEsm || hasCjs
161
- ? "esm" // mixed — static imports present, treat as ESM
162
- : "unknown";
163
- // --- build entries ---
164
- const imports = [];
165
- for (const node of importNodes) {
166
- const entry = parseStaticImport(node, moduleType);
167
- if (entry)
168
- imports.push(entry);
169
- }
170
- let dynamicCount = 0;
171
- for (const d of dynamicRaw) {
172
- if (d.kind === "import") {
173
- imports.push({
174
- source: d.source,
175
- names: [],
176
- isDynamic: true,
177
- moduleType,
178
- });
181
+ store.setFileFact(ctx.filePath, "file.imports", imports);
182
+ const reexports = [];
183
+ for (const node of reExportNodes) {
184
+ const r = parseReExport(node);
185
+ if (r)
186
+ reexports.push(r);
179
187
  }
180
- else {
181
- imports.push({ source: d.source, names: [], moduleType: "cjs" });
188
+ store.setFileFact(ctx.filePath, "file.reexports", reexports);
189
+ // Telemetry: log when a file has dynamic imports or re-exports so we can
190
+ // measure coverage and validate the implementation across real projects.
191
+ if (dynamicCount > 0 || reexports.length > 0) {
192
+ logLatency({
193
+ type: "call_graph_facts",
194
+ filePath: ctx.filePath,
195
+ durationMs: 0,
196
+ metadata: {
197
+ moduleType,
198
+ staticImports: imports.length - dynamicCount,
199
+ dynamicImports: dynamicCount,
200
+ reexports: reexports.length,
201
+ starReexports: reexports.filter((r) => r.names.length === 0).length,
202
+ },
203
+ });
182
204
  }
183
- dynamicCount++;
184
- }
185
- store.setFileFact(ctx.filePath, "file.imports", imports);
186
- const reexports = [];
187
- for (const node of reExportNodes) {
188
- const r = parseReExport(node);
189
- if (r)
190
- reexports.push(r);
191
- }
192
- store.setFileFact(ctx.filePath, "file.reexports", reexports);
193
- // Telemetry: log when a file has dynamic imports or re-exports so we can
194
- // measure coverage and validate the implementation across real projects.
195
- if (dynamicCount > 0 || reexports.length > 0) {
196
- logLatency({
197
- type: "call_graph_facts",
198
- filePath: ctx.filePath,
199
- durationMs: 0,
200
- metadata: {
201
- moduleType,
202
- staticImports: imports.length - dynamicCount,
203
- dynamicImports: dynamicCount,
204
- reexports: reexports.length,
205
- starReexports: reexports.filter((r) => r.names.length === 0).length,
206
- },
207
- });
208
- }
205
+ });
206
+ if (!parsed.parsed)
207
+ setEmpty();
209
208
  },
210
209
  };
@@ -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,6 +9,7 @@
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";
13
14
  import { logLatency } from "../../latency-logger.js";
14
15
  import { hasEslintConfig } from "../../tool-policy.js";
@@ -120,6 +121,22 @@ function explicitRuleFixSuggestion(rule) {
120
121
  function normalizeRuleId(ruleId) {
121
122
  return ruleId.replace(/-js$/, "");
122
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
+ }
123
140
  export function canHandle(filePath) {
124
141
  return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
125
142
  }
@@ -248,7 +265,8 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
248
265
  };
249
266
  // Shared with the raw sgconfig materializer so both surfaces walk the same
250
267
  // workspace-rooted sources in the same precedence order.
251
- const ruleSources = getAstGrepRuleSources(options.projectRoot ?? cwd);
268
+ const ignoreRoot = options.projectRoot ?? cwd;
269
+ const ruleSources = getAstGrepRuleSources(ignoreRoot);
252
270
  for (const source of ruleSources) {
253
271
  let rules;
254
272
  try {
@@ -275,6 +293,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
275
293
  seenRuleIds.add(rule.id);
276
294
  if (blockingOnly && rule.severity !== "error")
277
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;
278
301
  if (suppressLinterOverlap &&
279
302
  LINTER_OVERLAP.has(normalizeRuleId(rule.id)) &&
280
303
  !NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
@@ -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;