pi-lens 3.8.69 → 3.8.70

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 (133) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +15 -13
  3. package/dist/clients/bootstrap.js +3 -1
  4. package/dist/clients/bus-events-logger.js +51 -0
  5. package/dist/clients/bus-publish.js +45 -2
  6. package/dist/clients/code-quality-warnings.js +27 -0
  7. package/dist/clients/dead-code-client.js +5 -23
  8. package/dist/clients/diagnostics-publish.js +38 -2
  9. package/dist/clients/dispatch/auxiliary-lsp.js +62 -6
  10. package/dist/clients/dispatch/facts/function-facts.js +83 -0
  11. package/dist/clients/dispatch/rules/async-noise.js +1 -1
  12. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
  13. package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
  14. package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
  15. package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
  16. package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
  17. package/dist/clients/dispatch/rules/high-complexity.js +12 -1
  18. package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
  19. package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
  20. package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
  21. package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
  22. package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
  23. package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
  24. package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
  25. package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
  26. package/dist/clients/dispatch/runners/lsp.js +22 -6
  27. package/dist/clients/gitleaks-client.js +40 -5
  28. package/dist/clients/installer/index.js +123 -6
  29. package/dist/clients/instance-reaper.js +229 -2
  30. package/dist/clients/instance-registry.js +71 -2
  31. package/dist/clients/knip-client.js +35 -38
  32. package/dist/clients/language-profile.js +7 -11
  33. package/dist/clients/lens-engine.js +12 -0
  34. package/dist/clients/log-cleanup.js +61 -19
  35. package/dist/clients/lsp/cascade-tier.js +31 -19
  36. package/dist/clients/lsp/client.js +52 -7
  37. package/dist/clients/lsp/config.js +27 -1
  38. package/dist/clients/lsp/index.js +547 -59
  39. package/dist/clients/lsp/server-strategies.js +17 -8
  40. package/dist/clients/lsp/server.js +63 -27
  41. package/dist/clients/lsp-budget.js +148 -0
  42. package/dist/clients/mcp/session.js +1 -0
  43. package/dist/clients/middle-man-analysis.js +260 -0
  44. package/dist/clients/module-report.js +173 -11
  45. package/dist/clients/ndjson-logger.js +24 -0
  46. package/dist/clients/opengrep-client.js +217 -0
  47. package/dist/clients/path-utils.js +40 -0
  48. package/dist/clients/pipeline.js +1 -1
  49. package/dist/clients/project-diagnostics/extractors.js +21 -1
  50. package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
  51. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
  52. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
  53. package/dist/clients/quiet-window.js +57 -2
  54. package/dist/clients/read-expansion.js +2 -3
  55. package/dist/clients/read-guard-tool-lines.js +10 -0
  56. package/dist/clients/resource-sampler.js +254 -0
  57. package/dist/clients/review-graph/builder.js +265 -11
  58. package/dist/clients/review-graph/symbol-id.js +57 -0
  59. package/dist/clients/runtime-session.js +40 -6
  60. package/dist/clients/runtime-turn.js +53 -11
  61. package/dist/clients/safe-spawn.js +48 -2
  62. package/dist/clients/source-filter.js +12 -25
  63. package/dist/clients/source-walker.js +66 -0
  64. package/dist/clients/startup-scan.js +37 -42
  65. package/dist/clients/symbol-containment.js +24 -0
  66. package/dist/clients/test-runner-client.js +544 -29
  67. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  68. package/dist/clients/widget-state.js +61 -1
  69. package/dist/clients/write-ordering-guard.js +58 -0
  70. package/dist/clients/zizmor-config.js +27 -0
  71. package/dist/index.js +6640 -2947
  72. package/dist/mcp/server.js +95 -19
  73. package/dist/tools/activate-tools.js +72 -0
  74. package/dist/tools/ast-dump.js +2 -8
  75. package/dist/tools/lens-diagnostics.js +290 -30
  76. package/dist/tools/lsp-diagnostics.js +544 -104
  77. package/dist/tools/module-report.js +18 -5
  78. package/docs/agent-tooling-improvement-plan.md +660 -0
  79. package/docs/agent-tools.md +129 -0
  80. package/docs/ast-grep_rules_catalog.md +564 -0
  81. package/docs/astplayground.md +178 -0
  82. package/docs/audit1.md +177 -0
  83. package/docs/custom-rules.md +231 -0
  84. package/docs/dependencies.md +54 -0
  85. package/docs/env_variables.md +10 -0
  86. package/docs/environment-variables.md +67 -0
  87. package/docs/fable.md +139 -0
  88. package/docs/features.md +338 -0
  89. package/docs/globalconfig.md +79 -0
  90. package/docs/inspiration_gograph.md +601 -0
  91. package/docs/inspiration_neovim.md +1795 -0
  92. package/docs/inspiration_neovim2.md +402 -0
  93. package/docs/inspiration_tree_sitter_rules.md +369 -0
  94. package/docs/inspiration_tree_sitter_rules_investigation.md +328 -0
  95. package/docs/inspiration_tree_sitter_rules_round2.md +470 -0
  96. package/docs/language-coverage.md +48 -0
  97. package/docs/lsp-bridge-analysis.md +444 -0
  98. package/docs/lsp-capability-matrix.md +157 -0
  99. package/docs/lsp-latency-benchmark.md +169 -0
  100. package/docs/mcp.md +438 -0
  101. package/docs/module-report-read-symbol.md +258 -0
  102. package/docs/newinspiration.md +338 -0
  103. package/docs/servercapabilities.md +118 -0
  104. package/docs/subagent-compat.md +281 -0
  105. package/docs/tools.md +13 -0
  106. package/docs/tools_improvement2.md +345 -0
  107. package/docs/tree-sitter_rules_catalog.md +296 -0
  108. package/docs/usage.md +129 -0
  109. package/docs/word-index.md +150 -0
  110. package/grammars/tree-sitter-c.wasm +0 -0
  111. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  112. package/grammars/tree-sitter-cpp.wasm +0 -0
  113. package/grammars/tree-sitter-dart.wasm +0 -0
  114. package/grammars/tree-sitter-elixir.wasm +0 -0
  115. package/grammars/tree-sitter-kotlin.wasm +0 -0
  116. package/grammars/tree-sitter-lua.wasm +0 -0
  117. package/grammars/tree-sitter-ocaml.wasm +0 -0
  118. package/grammars/tree-sitter-php.wasm +0 -0
  119. package/grammars/tree-sitter-ruby.wasm +0 -0
  120. package/grammars/tree-sitter-swift.wasm +0 -0
  121. package/grammars/tree-sitter-toml.wasm +0 -0
  122. package/grammars/tree-sitter-vue.wasm +0 -0
  123. package/grammars/tree-sitter-zig.wasm +0 -0
  124. package/package.json +5 -1
  125. package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
  126. package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
  127. package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
  128. package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
  129. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +1 -1
  130. package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
  131. package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
  132. package/skills/pi-lens-ast-grep/SKILL.md +3 -1
  133. package/dist/clients/production-readiness.js +0 -493
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared test-framework-call recognition for `high-fan-out` and `high-complexity`
3
+ * (#577). A normal `it()`/`describe()` body naturally invokes many assertion/mock
4
+ * helpers — that's test structure, not a coordination smell or genuine complexity.
5
+ *
6
+ * Two complementary heuristics, both call-name-based (no `ctx.fileRole` gating —
7
+ * these calls only exist in test-framework code, so the signal is safe to apply
8
+ * unconditionally):
9
+ *
10
+ * 1. `isTestFrameworkNoiseCall` — filters individual "meaningful calls" the same
11
+ * way `console.*`/`Math.*`/etc. are already excluded in high-fan-out. Handles
12
+ * assertion-heavy `it()` bodies directly.
13
+ * 2. `isTestSuiteOrganizer` — a `describe()`/`it()`/`test()` wrapper's own
14
+ * FunctionSummary aggregates every call and branch from ALL of its nested test
15
+ * bodies (the shared tree-sitter walk in function-facts.ts does not stop at
16
+ * nested-function boundaries, since each nested callback also gets its own,
17
+ * correctly-scoped, FunctionSummary). That aggregation is not real fan-out or
18
+ * complexity — it's the sum of independent nested tests. A function is an
19
+ * "organizer" if it directly invokes `it`/`test`/`describe` (or their
20
+ * `.only`/`.skip`/`.each` variants) — i.e. it exists to group other tests, not
21
+ * to implement logic itself. This does NOT exempt genuinely complex/tangled
22
+ * test HELPER functions, since those don't call `it`/`describe`/`test`
23
+ * themselves.
24
+ */
25
+ const EXPECT_CHAIN_PREFIX = "expect(";
26
+ const TEST_LIFECYCLE_NAMES = new Set([
27
+ "it",
28
+ "test",
29
+ "describe",
30
+ "beforeeach",
31
+ "aftereach",
32
+ "beforeall",
33
+ "afterall",
34
+ ]);
35
+ const TEST_LIFECYCLE_PREFIXES = ["it.", "test.", "describe."];
36
+ const MOCK_LIBRARY_PREFIXES = ["vi.", "jest."];
37
+ /**
38
+ * True if `call` (a raw callee string from `FunctionSummary.outgoingCalls`) is
39
+ * test-framework noise: an `expect(...)` assertion chain, a test lifecycle call
40
+ * (`it`/`describe`/`test`/`beforeEach`/etc., including `.only`/`.skip`/`.each`
41
+ * variants), or a mock-library call (`vi.*`/`jest.*`).
42
+ */
43
+ export function isTestFrameworkNoiseCall(call) {
44
+ const lower = call.toLowerCase();
45
+ if (lower === "expect" || lower.startsWith(EXPECT_CHAIN_PREFIX))
46
+ return true;
47
+ if (TEST_LIFECYCLE_NAMES.has(lower))
48
+ return true;
49
+ if (TEST_LIFECYCLE_PREFIXES.some((p) => lower.startsWith(p)))
50
+ return true;
51
+ if (MOCK_LIBRARY_PREFIXES.some((p) => lower.startsWith(p)))
52
+ return true;
53
+ return false;
54
+ }
55
+ /**
56
+ * True if this function's (unfiltered) outgoing calls include a direct call to
57
+ * `it`/`test`/`describe` — i.e. it groups nested test bodies rather than
58
+ * implementing logic itself. Its aggregated fan-out/complexity reflects the sum
59
+ * of independent nested tests, not one function's real signal.
60
+ */
61
+ export function isTestSuiteOrganizer(calls) {
62
+ return calls.some((call) => {
63
+ const lower = call.toLowerCase();
64
+ const base = lower.split("(")[0].split(".")[0];
65
+ return base === "it" || base === "test" || base === "describe";
66
+ });
67
+ }
@@ -1,3 +1,4 @@
1
+ import { isTestSuiteOrganizer } from "./framework-call-noise.js";
1
2
  // Defaults match the historical hardcoded values so behavior is unchanged for
2
3
  // projects without a config. Project-specific overrides are read from the
3
4
  // per-dispatch context; these mutable fallbacks exist for tests/legacy direct
@@ -35,6 +36,16 @@ export const highComplexityRule = {
35
36
  const configuredCcThreshold = ctx.projectConfig?.rules["high-complexity"]?.threshold;
36
37
  const activeCcThreshold = configuredCcThreshold ?? ccThreshold;
37
38
  for (const f of fns) {
39
+ // A describe()/it()/test() wrapper's own complexity/nesting-depth
40
+ // aggregates branches from ALL of its nested test bodies (#577) — e.g. a
41
+ // `for` loop inside each of several `it()`s sums into the enclosing
42
+ // `describe()`'s cyclomatic complexity, even though no single test is
43
+ // complex. Unlike high-fan-out's call-name filter, there's no per-call
44
+ // signal to exclude for branch counting, so the equivalent fix here is to
45
+ // skip the organizer function itself. Genuinely complex test HELPER
46
+ // functions don't call it/describe/test themselves, so they're unaffected.
47
+ if (isTestSuiteOrganizer(f.outgoingCalls))
48
+ continue;
38
49
  const ccBreached = f.cyclomaticComplexity >= activeCcThreshold;
39
50
  const depthBreached = f.maxNestingDepth >= depthThreshold;
40
51
  if (!ccBreached && !depthBreached)
@@ -46,7 +57,7 @@ export const highComplexityRule = {
46
57
  parts.push(`nesting depth ${f.maxNestingDepth}`);
47
58
  diagnostics.push({
48
59
  id: `high-complexity:${ctx.filePath}:${f.line}`,
49
- tool: "high-complexity",
60
+ tool: "fact-rules",
50
61
  rule: "high-complexity",
51
62
  filePath: ctx.filePath,
52
63
  line: f.line,
@@ -1,3 +1,4 @@
1
+ import { isTestFrameworkNoiseCall, isTestSuiteOrganizer, } from "./framework-call-noise.js";
1
2
  // Default matches the historical hardcoded value so behavior is unchanged for
2
3
  // projects without a config. Project-specific overrides are read from the
3
4
  // per-dispatch context; this mutable fallback exists for tests/legacy direct
@@ -27,7 +28,16 @@ export const highFanOutRule = {
27
28
  const configuredFanOutThreshold = ctx.projectConfig?.rules["high-fan-out"]?.threshold;
28
29
  const activeFanOutThreshold = configuredFanOutThreshold ?? fanOutThreshold;
29
30
  for (const f of fns) {
30
- // Filter out noise: utility calls, logging, type assertions
31
+ // A describe()/it()/test() wrapper's own call list aggregates every call
32
+ // from ALL of its nested test bodies (#577) — not real fan-out. Genuinely
33
+ // complex test HELPER functions don't call it/describe/test themselves, so
34
+ // they're unaffected.
35
+ if (isTestSuiteOrganizer(f.outgoingCalls))
36
+ continue;
37
+ // Filter out noise: utility calls, logging, type assertions, and
38
+ // test-framework calls (expect/it/describe/vi.*/jest.*, #577) — assertion-
39
+ // and mock-heavy test bodies naturally call many of these; that's test
40
+ // structure, not a coordination smell.
31
41
  const meaningful = f.outgoingCalls.filter((c) => {
32
42
  const lower = c.toLowerCase();
33
43
  return (!lower.startsWith("console.") &&
@@ -42,13 +52,14 @@ export const highFanOutRule = {
42
52
  c !== "resolve" &&
43
53
  c !== "reject" &&
44
54
  c !== "next" &&
45
- c !== "done");
55
+ c !== "done" &&
56
+ !isTestFrameworkNoiseCall(c));
46
57
  });
47
58
  if (meaningful.length < activeFanOutThreshold)
48
59
  continue;
49
60
  diagnostics.push({
50
61
  id: `high-fan-out:${ctx.filePath}:${f.line}`,
51
- tool: "high-fan-out",
62
+ tool: "fact-rules",
52
63
  rule: "high-fan-out",
53
64
  filePath: ctx.filePath,
54
65
  line: f.line,
@@ -23,7 +23,7 @@ export const highImportCouplingRule = {
23
23
  return [
24
24
  {
25
25
  id: `high-import-coupling:${ctx.filePath}:1:1`,
26
- tool: "high-import-coupling",
26
+ tool: "fact-rules",
27
27
  rule: "high-import-coupling",
28
28
  filePath: ctx.filePath,
29
29
  line: 1,
@@ -46,7 +46,7 @@ export const missingErrorPropagationRule = {
46
46
  continue;
47
47
  diagnostics.push({
48
48
  id: `missing-error-propagation:${ctx.filePath}:${c.line}`,
49
- tool: "missing-error-propagation",
49
+ tool: "fact-rules",
50
50
  rule: "missing-error-propagation",
51
51
  filePath: ctx.filePath,
52
52
  line: c.line,
@@ -34,7 +34,7 @@ export const commentedCredentialsRule = {
34
34
  if (p.test(line)) {
35
35
  diagnostics.push({
36
36
  id: `no-commented-credentials:${ctx.filePath}:${i + 1}`,
37
- tool: "no-commented-credentials",
37
+ tool: "fact-rules",
38
38
  rule: "no-commented-credentials",
39
39
  filePath: ctx.filePath,
40
40
  line: i + 1,
@@ -23,7 +23,7 @@ export const passThroughWrappersRule = {
23
23
  continue;
24
24
  diagnostics.push({
25
25
  id: `pass-through-wrapper:${ctx.filePath}:${fn.line}:${fn.column}`,
26
- tool: "pass-through-wrappers",
26
+ tool: "fact-rules",
27
27
  filePath: ctx.filePath,
28
28
  line: fn.line,
29
29
  column: fn.column,
@@ -25,7 +25,7 @@ export const placeholderCommentsRule = {
25
25
  continue;
26
26
  diagnostics.push({
27
27
  id: `placeholder-comments:${ctx.filePath}:${comment.line}:1`,
28
- tool: "placeholder-comments",
28
+ tool: "fact-rules",
29
29
  filePath: ctx.filePath,
30
30
  line: comment.line,
31
31
  column: 1,
@@ -80,7 +80,7 @@ export const unsafeBoundaryRule = {
80
80
  continue;
81
81
  diagnostics.push({
82
82
  id: `unsafe-boundary:${ctx.filePath}:${f.line}`,
83
- tool: "unsafe-boundary",
83
+ tool: "fact-rules",
84
84
  rule: "unsafe-boundary",
85
85
  filePath: ctx.filePath,
86
86
  line: f.line,
@@ -38,14 +38,31 @@ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
38
38
  const MAX_MATCHES_PER_RULE = 10;
39
39
  /** Maximum total diagnostics per file to prevent output spam */
40
40
  const MAX_TOTAL_DIAGNOSTICS = 50;
41
- /** Rules already covered by tree-sitter runner (priority 14, runs first) */
42
- const TREE_SITTER_OVERLAP = new Set([
43
- "constructor-super",
44
- "empty-catch",
45
- "long-parameter-list",
46
- "nested-ternary",
47
- "no-dupe-class-members",
48
- ]);
41
+ /**
42
+ * #660: this runner used to skip a hardcoded set of rule ids
43
+ * (`constructor-super`, `empty-catch`, `long-parameter-list`,
44
+ * `nested-ternary`, `no-dupe-class-members`) on the assumption that the
45
+ * tree-sitter query runner (priority 14) already covered them, to avoid
46
+ * double-reporting. That assumption was false for every entry: three of
47
+ * them (`nested-ternary`, `long-parameter-list`, `no-dupe-class-members`)
48
+ * have no active tree-sitter query — their would-be queries either live
49
+ * under `rules/tree-sitter-queries/typescript-disabled/` (excluded from
50
+ * loading, see clients/tree-sitter-query-loader.ts) or were never written —
51
+ * so those three rule ids had ZERO coverage in the NAPI fallback runner
52
+ * (used when the ast-grep binary isn't installed) despite having a
53
+ * perfectly good, shipped, active ast-grep rule sitting right there. The
54
+ * other two (`constructor-super`, `empty-catch`) are disabled everywhere
55
+ * (ast-grep AND tree-sitter, see rules-disabled/, #206), so skipping them
56
+ * was already a no-op. The whole skip-set has been removed; if tree-sitter
57
+ * coverage is ever added back for one of these rule ids, reintroduce a
58
+ * scoped skip alongside the query that actually covers it — don't recreate
59
+ * a blanket assumption-based list.
60
+ *
61
+ * Note: `no-dupe-class-members` didn't actually fire immediately
62
+ * post-removal — its rule YAML uses a top-level `utils:` block that this
63
+ * runner's native-config passthrough dropped entirely, a separate bug
64
+ * (affecting 5 shipped rules, not just this one) fixed in #663.
65
+ */
49
66
  /**
50
67
  * Rules commonly covered by ESLint/Biome correctness checks.
51
68
  * We can suppress these from ast-grep in lint-enabled projects to reduce noise.
@@ -103,6 +120,32 @@ function normalizeRuleId(ruleId) {
103
120
  export function canHandle(filePath) {
104
121
  return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
105
122
  }
123
+ /**
124
+ * The TypeScript grammar is a syntactic superset of JavaScript, so a
125
+ * `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
128
+ * TS-only syntax, but a `TypeScript`-tagged rule with a plain-JS-compatible
129
+ * body would equally double-fire alongside a `JavaScript` twin on a `.ts`
130
+ * file. Without this, `language:` reads as a real filter but isn't one for
131
+ * ts↔js pairs, so twin rules sharing a base name (e.g. `hardcoded-url` /
132
+ * `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.
135
+ */
136
+ export function ruleLanguageForFile(filePath) {
137
+ const ext = path.extname(filePath).toLowerCase();
138
+ switch (ext) {
139
+ case ".ts":
140
+ case ".tsx":
141
+ return "typescript";
142
+ case ".js":
143
+ case ".jsx":
144
+ return "javascript";
145
+ default:
146
+ return undefined;
147
+ }
148
+ }
106
149
  export function getLang(filePath, sgModule) {
107
150
  const ext = path.extname(filePath).toLowerCase();
108
151
  switch (ext) {
@@ -169,9 +212,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
169
212
  const maxMatchesPerRule = options.maxMatchesPerRule ?? MAX_MATCHES_PER_RULE;
170
213
  const maxTotalDiagnostics = options.maxTotalDiagnostics ?? MAX_TOTAL_DIAGNOSTICS;
171
214
  const blockingOnly = options.blockingOnly === true;
215
+ const log = options.log;
172
216
  const diagnostics = [];
173
217
  const seenRuleIds = new Set();
174
218
  const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
219
+ const fileLang = ruleLanguageForFile(filePath);
175
220
  // Shared with the raw sgconfig materializer so both surfaces walk the same
176
221
  // workspace-rooted sources in the same precedence order.
177
222
  const ruleSources = getAstGrepRuleSources(options.projectRoot ?? cwd);
@@ -205,9 +250,6 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
205
250
  !NON_SUPPRESSIBLE.has(normalizeRuleId(rule.id))) {
206
251
  continue;
207
252
  }
208
- // Skip rules already handled by tree-sitter runner (priority 14)
209
- if (TREE_SITTER_OVERLAP.has(rule.id))
210
- continue;
211
253
  // Skip rules whose top-level pattern is overly broad ($NAME, $X, etc.)
212
254
  // without additional structural constraints to narrow matches.
213
255
  if (rule.rule &&
@@ -219,6 +261,12 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
219
261
  if (lang && lang !== "typescript" && lang !== "javascript") {
220
262
  continue;
221
263
  }
264
+ // Scope TypeScript/JavaScript-tagged rules to the file's actual
265
+ // grammar (#657) — otherwise a `-js` twin sharing generic node
266
+ // kinds with its TS sibling double-fires on every .ts file.
267
+ if (lang && fileLang && lang !== fileLang) {
268
+ continue;
269
+ }
222
270
  if (blockingOnly && rule.rule) {
223
271
  const complexity = calculateRuleComplexity(rule.rule);
224
272
  if (complexity > MAX_BLOCKING_RULE_COMPLEXITY) {
@@ -232,18 +280,26 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
232
280
  // Delegate matching to napi's native engine, which handles the
233
281
  // full ast-grep rule grammar (pattern, kind, has/inside/follows/
234
282
  // precedes/stopBy/field/nthChild, any/all/not) plus metavariable
235
- // `constraints` (#206). A faithful js-yaml parse feeds the rule
283
+ // `constraints` (#206) AND top-level `utils` reusable named
284
+ // matchers referenced via `matches: <name>` inside `rule`
285
+ // (#663; `NapiConfig.utils: Record<string, Rule>` per
286
+ // @ast-grep/napi's types, same shape napi already expects for
287
+ // `rule`/`constraints`). A faithful js-yaml parse feeds the rule
236
288
  // object straight through. If napi rejects the rule (a malformed
237
- // or invalid-kind rule), skip it never silently match nothing
238
- // through a partial interpreter.
239
- const nativeConfig = rule.constraints
240
- ? { rule: rule.rule, constraints: rule.constraints }
241
- : { rule: rule.rule };
289
+ // or invalid-kind rule, or an unresolved `matches:` reference),
290
+ // skip it — never silently match nothing through a partial
291
+ // interpreter.
292
+ const nativeConfig = { rule: rule.rule };
293
+ if (rule.constraints)
294
+ nativeConfig.constraints = rule.constraints;
295
+ if (rule.utils)
296
+ nativeConfig.utils = rule.utils;
242
297
  try {
243
298
  matches = rootNode.findAll(nativeConfig);
244
299
  }
245
- catch {
300
+ catch (err) {
246
301
  matches = [];
302
+ log?.(`ast-grep-napi: rule "${rule.id}" rejected by native engine (${err instanceof Error ? err.message : String(err)})`);
247
303
  }
248
304
  const limitedMatches = matches.slice(0, maxMatchesPerRule);
249
305
  for (const match of limitedMatches) {
@@ -355,6 +411,7 @@ const astGrepNapiRunner = {
355
411
  const diagnostics = evaluateAstGrepRules(ctx.filePath, rootNode, ctx.cwd, ctx.kind, {
356
412
  blockingOnly: ctx.blockingOnly,
357
413
  projectRoot: ctx.projectRoot,
414
+ log: (message) => ctx.log(message),
358
415
  });
359
416
  const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
360
417
  let semantic = "none";
@@ -16,7 +16,7 @@ import { RUNTIME_CONFIG } from "../../runtime-config.js";
16
16
  import { PRIORITY } from "../priorities.js";
17
17
  import { resolveRunnerPath } from "../runner-context.js";
18
18
  import { convertLspDiagnostics } from "../utils/lsp-diagnostics.js";
19
- import { enabledAuxiliaryLspServerIds, findAuxiliaryProfileForSource, } from "../auxiliary-lsp.js";
19
+ import { enabledAuxiliaryLspServerIds, findAuxiliaryProfileForSource, isAuxiliaryDiagnosticSuppressed, } from "../auxiliary-lsp.js";
20
20
  import { readFileContent } from "./utils.js";
21
21
  const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
22
22
  const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
@@ -104,6 +104,11 @@ const lspRunner = {
104
104
  // spawn that didn't complete in the budget, or LSP unavailable for this
105
105
  // file) — distinct from `[]`, which means the server replied with zero.
106
106
  let lspClientReady = true;
107
+ // True when touchFile ran but couldn't confirm its result within
108
+ // budget (notify write and/or diagnostics wait timed out on at least
109
+ // one spawned server) — an empty `lspDiags` in that case is NOT a
110
+ // confirmed clean result and must not be reported as one (#570).
111
+ let diagnosticsInconclusive = false;
107
112
  let failureReason = "";
108
113
  const content = readFileContent(ctx.filePath);
109
114
  if (!content) {
@@ -133,6 +138,7 @@ const lspRunner = {
133
138
  }
134
139
  else {
135
140
  lspDiags = touched;
141
+ diagnosticsInconclusive = touched.inconclusive === true;
136
142
  }
137
143
  }
138
144
  catch (err) {
@@ -173,6 +179,18 @@ const lspRunner = {
173
179
  // published late still land in the client cache and surface then.
174
180
  return { status: "skipped", diagnostics: [], semantic: "none" };
175
181
  }
182
+ if (diagnosticsInconclusive) {
183
+ // The touch ran and a client was ready, but the notify write and/or
184
+ // diagnostics wait hit their deadline before the server confirmed
185
+ // completion — `lspDiags` (even if non-empty) is not a trustworthy
186
+ // merged result. Same treatment as `!lspClientReady`: report
187
+ // "skipped" rather than "succeeded" with a possibly-incomplete
188
+ // diagnostics list, so the coverage notice flags the gap instead of
189
+ // the footer reading this as a confirmed clean/partial result (#570).
190
+ // Diagnostics that do arrive late still land in the client cache and
191
+ // surface on the next edit.
192
+ return { status: "skipped", diagnostics: [], semantic: "none" };
193
+ }
176
194
  if (lspDiags.length === 0) {
177
195
  return {
178
196
  status: "succeeded",
@@ -216,11 +234,9 @@ const lspRunner = {
216
234
  const profile = findAuxiliaryProfileForSource(validLspDiags[i]?.source);
217
235
  if (!profile)
218
236
  continue;
219
- if (profile.isSuppressed) {
220
- if (profile.isSuppressed(validLspDiags[i], content)) {
221
- suppressedIndices.add(i);
222
- continue;
223
- }
237
+ if (isAuxiliaryDiagnosticSuppressed(validLspDiags[i], content)) {
238
+ suppressedIndices.add(i);
239
+ continue;
224
240
  }
225
241
  let blockingAllowed = blockingAllowedByProfile.get(profile);
226
242
  if (blockingAllowed === undefined) {
@@ -17,6 +17,13 @@
17
17
  * - `gitleaks` reference in `package.json` deps, OR
18
18
  * - a git pre-commit hook (.husky/, .git/hooks/) referencing gitleaks
19
19
  *
20
+ * `lens_diagnostics mode=full`'s fresh-fetch path (`clients/project-diagnostics/
21
+ * fresh-fetch.ts`) uses a looser "smart-default" gate instead — any tracked
22
+ * git repo, via `hasGitRepo` — since that's an explicitly-requested
23
+ * comprehensive review where gitleaks's low cost and advisory-only findings
24
+ * make the stricter default needlessly conservative. session_start and
25
+ * per-edit dispatch keep the strict gate above unchanged.
26
+ *
20
27
  * If the gate trips, the runner auto-installs gitleaks from GitHub releases
21
28
  * (installer entry registered in clients/installer/index.ts) and runs
22
29
  * `gitleaks detect --no-git --report-format json` against the analysis root.
@@ -95,6 +102,26 @@ export function hasGitleaksSignal(cwd) {
95
102
  }
96
103
  return false;
97
104
  }
105
+ /**
106
+ * "Smart-default" tier from #130's own considered-but-unshipped options:
107
+ * fire whenever the project is a tracked git repo, not only when an explicit
108
+ * gitleaks signal is present. gitleaks's own scan target is "a git repo's
109
+ * history/tree" — nearly every project qualifies, so this is meaningfully
110
+ * looser than {@link hasGitleaksSignal}. Used ONLY by `mode=full`'s
111
+ * fresh-fetch path (an explicitly-requested comprehensive review, where
112
+ * gitleaks's low cost — ~10MB binary, no external DB pull — and advisory-only
113
+ * findings make the stricter opt-in gate needlessly conservative); session_start
114
+ * and per-edit dispatch keep the strict {@link hasGitleaksSignal} gate
115
+ * unchanged, so day-to-day noise/cost stays exactly as conservative as before.
116
+ */
117
+ export function hasGitRepo(cwd) {
118
+ try {
119
+ return fs.existsSync(path.join(cwd, ".git"));
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
98
125
  // --- Client ---
99
126
  export class GitleaksClient extends SecurityScanClient {
100
127
  constructor(verbose = false) {
@@ -107,6 +134,10 @@ export class GitleaksClient extends SecurityScanClient {
107
134
  static hasGitleaksSignal(cwd) {
108
135
  return hasGitleaksSignal(cwd);
109
136
  }
137
+ /** Smart-default tier (#130) — see {@link hasGitRepo}'s doc comment. */
138
+ static hasGitRepo(cwd) {
139
+ return hasGitRepo(cwd);
140
+ }
110
141
  /**
111
142
  * Auto-install via the GitHub-release path (registered in
112
143
  * `clients/installer/index.ts`) when gitleaks isn't already on PATH.
@@ -118,18 +149,22 @@ export class GitleaksClient extends SecurityScanClient {
118
149
  /**
119
150
  * Scan a directory tree for secrets.
120
151
  *
121
- * Skips early when the directory shows no gitleaks opt-in signal. When
122
- * gitleaks is unavailable, returns an empty result with an explanatory
123
- * summary rather than failing the session_start task.
152
+ * Skips early when the directory shows no gitleaks opt-in signal — unless
153
+ * `requireSignal: false` (the `mode=full` fresh-fetch path uses this to
154
+ * apply the looser #130 "smart-default" gate, {@link hasGitRepo}, instead;
155
+ * session_start and per-edit dispatch never pass this, so their behavior
156
+ * is unchanged). When gitleaks is unavailable, returns an empty result
157
+ * with an explanatory summary rather than failing the session_start task.
124
158
  *
125
159
  * Re-entrancy safe: concurrent calls against the same root share a
126
160
  * single gitleaks process (mirrors `KnipClient` / `JscpdClient` /
127
161
  * `GovulncheckClient`).
128
162
  */
129
- async scan(cwd) {
163
+ async scan(cwd, options) {
130
164
  const targetDir = path.resolve(cwd);
131
165
  const scannedAt = new Date().toISOString();
132
- if (!GitleaksClient.hasGitleaksSignal(targetDir)) {
166
+ const requireSignal = options?.requireSignal ?? true;
167
+ if (requireSignal && !GitleaksClient.hasGitleaksSignal(targetDir)) {
133
168
  return {
134
169
  ...EMPTY_RESULT,
135
170
  success: true,