pi-lens 3.8.65 → 3.8.67

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 (96) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +0 -1
  3. package/dist/clients/actionable-warnings-logger.js +11 -36
  4. package/dist/clients/ast-grep-tool-logger.js +11 -36
  5. package/dist/clients/biome-client.js +10 -2
  6. package/dist/clients/bootstrap.js +1 -3
  7. package/dist/clients/cascade-logger.js +7 -12
  8. package/dist/clients/complexity-client.js +397 -623
  9. package/dist/clients/dead-code-logger.js +11 -22
  10. package/dist/clients/dependency-checker.js +15 -2
  11. package/dist/clients/deps/vscode-jsonrpc.js +1 -1
  12. package/dist/clients/diagnostic-logger.js +7 -31
  13. package/dist/clients/dispatch/auxiliary-lsp.js +38 -0
  14. package/dist/clients/dispatch/dispatcher.js +4 -39
  15. package/dist/clients/dispatch/fact-runner.js +9 -57
  16. package/dist/clients/dispatch/facts/comment-facts.js +12 -25
  17. package/dist/clients/dispatch/facts/function-facts.js +161 -132
  18. package/dist/clients/dispatch/facts/import-facts.js +164 -117
  19. package/dist/clients/dispatch/facts/tree-sitter-facts.js +34 -0
  20. package/dist/clients/dispatch/facts/try-catch-facts.js +62 -68
  21. package/dist/clients/dispatch/inline-suppressions.js +61 -0
  22. package/dist/clients/dispatch/integration.js +94 -32
  23. package/dist/clients/dispatch/plan.js +0 -49
  24. package/dist/clients/dispatch/priorities.js +0 -1
  25. package/dist/clients/dispatch/rules/cors-wildcard.js +54 -0
  26. package/dist/clients/dispatch/rules/high-import-coupling.js +37 -0
  27. package/dist/clients/dispatch/rules/no-commented-credentials.js +52 -0
  28. package/dist/clients/dispatch/runners/index.js +2 -7
  29. package/dist/clients/dispatch/runners/lsp.js +18 -6
  30. package/dist/clients/dispatch/runners/tree-sitter.js +11 -64
  31. package/dist/clients/dispatch/runners/utils/runner-helpers.js +28 -33
  32. package/dist/clients/dispatch/tool-profile.js +0 -1
  33. package/dist/clients/dispatch/types.js +1 -1
  34. package/dist/clients/formatters.js +10 -0
  35. package/dist/clients/git-guard.js +1 -1
  36. package/dist/clients/grammar-source.js +65 -2
  37. package/dist/clients/jscpd-client.js +8 -2
  38. package/dist/clients/language-policy.js +2 -2
  39. package/dist/clients/latency-logger.js +10 -15
  40. package/dist/clients/lsp/client.js +74 -11
  41. package/dist/clients/lsp/config.js +61 -3
  42. package/dist/clients/lsp/index.js +43 -3
  43. package/dist/clients/lsp/launch.js +3 -32
  44. package/dist/clients/mcp/session.js +0 -1
  45. package/dist/clients/module-report.js +16 -5
  46. package/dist/clients/ndjson-logger.js +150 -0
  47. package/dist/clients/package-manager.js +65 -0
  48. package/dist/clients/pipeline.js +25 -41
  49. package/dist/clients/project-diagnostics/extractors.js +85 -0
  50. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +44 -0
  51. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +25 -0
  52. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +34 -0
  53. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +38 -0
  54. package/dist/clients/project-diagnostics/runner-adapters/madge.js +47 -0
  55. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +31 -0
  56. package/dist/clients/project-diagnostics/scanner.js +11 -10
  57. package/dist/clients/read-guard-logger.js +11 -36
  58. package/dist/clients/review-graph/builder.js +337 -27
  59. package/dist/clients/review-graph/git-identity.js +150 -0
  60. package/dist/clients/review-graph/service.js +3 -3
  61. package/dist/clients/runtime-coordinator.js +83 -0
  62. package/dist/clients/runtime-session.js +28 -2
  63. package/dist/clients/runtime-tool-result.js +10 -2
  64. package/dist/clients/runtime-turn.js +25 -7
  65. package/dist/clients/sg-runner.js +15 -0
  66. package/dist/clients/test-runner-client.js +12 -6
  67. package/dist/clients/tree-sitter-cache.js +47 -14
  68. package/dist/clients/tree-sitter-client.js +9 -1
  69. package/dist/clients/tree-sitter-logger.js +7 -12
  70. package/dist/clients/tree-sitter-query-loader.js +1 -0
  71. package/dist/clients/tree-sitter-shared.js +113 -0
  72. package/dist/clients/tree-sitter-symbol-extractor.js +19 -12
  73. package/dist/index.js +4 -22
  74. package/dist/tools/lens-diagnostics.js +254 -20
  75. package/grammars/tree-sitter-yaml.wasm +0 -0
  76. package/grammars/tree-sitter-yaml.wasm.json +3 -3
  77. package/package.json +4 -3
  78. package/rules/ast-grep-rules/rule-tests/no-init-return-test.yml +15 -0
  79. package/rules/ast-grep-rules/rules/no-init-return.yml +11 -5
  80. package/rules/tree-sitter-queries/python/python-assert-production.yml +4 -0
  81. package/scripts/download-grammars.js +29 -4
  82. package/scripts/grammars.lock.json +14 -2
  83. package/scripts/install-selftest.mjs +8 -7
  84. package/dist/clients/deps/typescript.js +0 -15
  85. package/dist/clients/dispatch/rules/quality-rules.js +0 -297
  86. package/dist/clients/dispatch/rules/sonar-rules.js +0 -493
  87. package/dist/clients/dispatch/runners/biome.js +0 -69
  88. package/dist/clients/dispatch/runners/python-slop.js +0 -106
  89. package/dist/clients/dispatch/runners/ts-lsp.js +0 -109
  90. package/dist/clients/runner-tracker.js +0 -153
  91. package/dist/clients/ts-service.js +0 -130
  92. package/dist/clients/type-coverage-client.js +0 -128
  93. package/dist/clients/typescript-client.js +0 -509
  94. package/dist/commands/booboo.js +0 -1412
  95. package/rules/python-slop-rules/.sgconfig.yml +0 -4
  96. package/rules/python-slop-rules/rules/slop-rules.yml +0 -647
@@ -5,26 +5,20 @@
5
5
  * input to phasing decisions in the issue). Mirrors `ast-grep-tool-logger.ts`
6
6
  * for shape + size-based rotation.
7
7
  */
8
- import * as fs from "node:fs";
9
8
  import * as path from "node:path";
10
9
  import { isTestMode } from "./env-utils.js";
11
10
  import { getGlobalPiLensDir } from "./file-utils.js";
11
+ import { createNdjsonLogger } from "./ndjson-logger.js";
12
12
  const LOG_DIR = getGlobalPiLensDir();
13
13
  const LOG_FILE = path.join(LOG_DIR, "dead-code.log");
14
14
  const LOG_BACKUP_FILE = path.join(LOG_DIR, "dead-code.log.1");
15
15
  const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_DEAD_CODE_LOG_MAX_BYTES ?? "1048576", 10) ||
16
16
  1048576);
17
- function rotateIfNeeded() {
18
- try {
19
- const stat = fs.statSync(LOG_FILE);
20
- if (stat.size >= MAX_LOG_BYTES) {
21
- fs.renameSync(LOG_FILE, LOG_BACKUP_FILE);
22
- }
23
- }
24
- catch {
25
- // no file yet, or rename raced — nothing to rotate
26
- }
27
- }
17
+ const writer = createNdjsonLogger({
18
+ filePath: LOG_FILE,
19
+ maxBytes: MAX_LOG_BYTES,
20
+ backupPath: LOG_BACKUP_FILE,
21
+ });
28
22
  /**
29
23
  * Append one scan event. Fire-and-forget: telemetry must never break a scan, so
30
24
  * every fs error is swallowed. Skipped under test mode to keep the suite from
@@ -33,14 +27,9 @@ function rotateIfNeeded() {
33
27
  export function logDeadCodeScan(event) {
34
28
  if (isTestMode())
35
29
  return;
36
- try {
37
- if (!fs.existsSync(LOG_DIR))
38
- fs.mkdirSync(LOG_DIR, { recursive: true });
39
- rotateIfNeeded();
40
- const row = JSON.stringify({ ts: new Date().toISOString(), ...event });
41
- fs.appendFileSync(LOG_FILE, row + "\n");
42
- }
43
- catch {
44
- // telemetry is best-effort
45
- }
30
+ writer.log({ ts: new Date().toISOString(), ...event });
31
+ }
32
+ /** Resolve once all enqueued dead-code writes are on disk (tests/shutdown). */
33
+ export function flushDeadCodeLog() {
34
+ return writer.flush();
46
35
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
+ import { findNodeToolBinary } from "./package-manager.js";
13
14
  import { safeSpawnAsync } from "./safe-spawn.js";
14
15
  /**
15
16
  * Build madge's argv for a circular-dependency scan.
@@ -77,6 +78,16 @@ export class DependencyChecker {
77
78
  ? (msg) => console.error(`[deps] ${msg}`)
78
79
  : () => { };
79
80
  }
81
+ /**
82
+ * Resolve how to invoke madge for `cwd`: a local/global-installed binary
83
+ * (npm/pnpm/yarn/bun) if found, else `npx madge` (#375). `prefix` is prepended
84
+ * to the madge args (empty for a resolved binary, `["madge"]` for the npx
85
+ * fallback).
86
+ */
87
+ async resolveMadge(cwd) {
88
+ const bin = await findNodeToolBinary("madge", cwd);
89
+ return bin ? { cmd: bin, prefix: [] } : { cmd: "npx", prefix: ["madge"] };
90
+ }
80
91
  /**
81
92
  * Check if madge is available, auto-install if not
82
93
  */
@@ -250,7 +261,8 @@ export class DependencyChecker {
250
261
  this.log(`Imports changed for ${path.basename(normalized)}, checking dependencies...`);
251
262
  // Run madge on the specific file (fast)
252
263
  try {
253
- const result = await safeSpawnAsync("npx", ["madge", ...buildMadgeArgs(normalized, projectRoot)], {
264
+ const { cmd, prefix } = await this.resolveMadge(projectRoot);
265
+ const result = await safeSpawnAsync(cmd, [...prefix, ...buildMadgeArgs(normalized, projectRoot)], {
254
266
  timeout: 15000,
255
267
  cwd: projectRoot,
256
268
  });
@@ -344,7 +356,8 @@ export class DependencyChecker {
344
356
  }
345
357
  async runScanProject(projectRoot) {
346
358
  try {
347
- const result = await safeSpawnAsync("npx", ["madge", ...buildMadgeArgs(projectRoot, projectRoot)], {
359
+ const { cmd, prefix } = await this.resolveMadge(projectRoot);
360
+ const result = await safeSpawnAsync(cmd, [...prefix, ...buildMadgeArgs(projectRoot, projectRoot)], {
348
361
  timeout: 30000,
349
362
  cwd: projectRoot,
350
363
  });
@@ -1 +1 @@
1
- export { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node";
1
+ export { CancellationTokenSource, createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node";
@@ -3,17 +3,12 @@
3
3
  *
4
4
  * Log file: ~/.pi-lens/logs/{date}.jsonl
5
5
  */
6
- import * as fs from "node:fs";
7
6
  import * as os from "node:os";
8
7
  import * as path from "node:path";
9
8
  import { isTestMode } from "./env-utils.js";
9
+ import { createNdjsonLogger } from "./ndjson-logger.js";
10
10
  function getLogDir() {
11
- const home = os.homedir();
12
- const logDir = path.join(home, ".pi-lens", "logs");
13
- if (!fs.existsSync(logDir)) {
14
- fs.mkdirSync(logDir, { recursive: true });
15
- }
16
- return logDir;
11
+ return path.join(os.homedir(), ".pi-lens", "logs");
17
12
  }
18
13
  function getLogFile() {
19
14
  const date = new Date().toISOString().split("T")[0];
@@ -28,30 +23,15 @@ export function getDiagnosticLogger() {
28
23
  return _logger;
29
24
  }
30
25
  export function createDiagnosticLogger() {
31
- const pending = [];
32
- let writing = false;
33
- const writePending = async () => {
34
- if (writing || pending.length === 0)
35
- return;
36
- writing = true;
37
- const toWrite = pending.splice(0, pending.length);
38
- const lines = toWrite.map((e) => JSON.stringify(e)).join("\n") + "\n";
39
- try {
40
- await fs.promises.appendFile(getLogFile(), lines);
41
- }
42
- catch (err) {
43
- // pi-lens-ignore: missing-error-propagation — fire-and-forget log write, must not throw
44
- console.error("Failed to write diagnostic log:", err);
45
- }
46
- writing = false;
47
- };
26
+ // Lazy filePath: the log file is keyed on the current date, resolved per
27
+ // drain so a long-lived logger rolls over at midnight.
28
+ const writer = createNdjsonLogger({ filePath: () => getLogFile() });
48
29
  return {
49
30
  log(entry) {
50
31
  if (isTestMode()) {
51
32
  return;
52
33
  }
53
- pending.push(entry);
54
- writePending(); // async, non-blocking
34
+ writer.log(entry); // async, non-blocking
55
35
  },
56
36
  logCaught(d, context, shownInline = false) {
57
37
  this.log({
@@ -77,11 +57,7 @@ export function createDiagnosticLogger() {
77
57
  });
78
58
  },
79
59
  async flush() {
80
- // Drain any buffered entries, then wait for the write to finish.
81
- await writePending();
82
- while (writing) {
83
- await new Promise((resolve) => setTimeout(resolve, 10));
84
- }
60
+ await writer.flush();
85
61
  },
86
62
  };
87
63
  }
@@ -20,6 +20,42 @@ import { findLocalOpengrepConfig } from "../opengrep-config.js";
20
20
  import { findLocalTyposConfig } from "../typos-config.js";
21
21
  import { findLocalZizmorConfig } from "../zizmor-config.js";
22
22
  import { classifyDefect } from "./diagnostic-taxonomy.js";
23
+ /**
24
+ * Semgrep/opengrep `# nosemgrep` / `# nosemgrep: <rule-id>[,<rule-id>]` inline
25
+ * suppression (#441). A bare `# nosemgrep` drops every finding on its line; the
26
+ * `: <ids>` form drops only the listed rule ids. `d.code` is the semgrep rule id.
27
+ * Also accepts the `//` comment form.
28
+ *
29
+ * Matches Semgrep placement: honored on the finding's OWN line (inline or not),
30
+ * and on the line ABOVE only when that line is a STANDALONE comment (no code before
31
+ * it) — so `a() # nosemgrep` suppresses a finding on `a()` but not the next line.
32
+ */
33
+ const NOSEMGREP_RE = /(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?/i;
34
+ const NOSEMGREP_STANDALONE_RE = /^\s*(?:#|\/\/)\s*nosemgrep(?::\s*(.+))?\s*$/i;
35
+ export function isNosemgrepSuppressed(d, content) {
36
+ const startLine = d.range?.start?.line; // 0-based
37
+ if (startLine == null)
38
+ return false;
39
+ const lines = content.split("\n");
40
+ const ruleId = String(d.code ?? "");
41
+ const checkLine = (text, standaloneOnly) => {
42
+ if (!text)
43
+ return false;
44
+ const m = (standaloneOnly ? NOSEMGREP_STANDALONE_RE : NOSEMGREP_RE).exec(text);
45
+ if (!m)
46
+ return false;
47
+ if (m[1] === undefined)
48
+ return true; // bare nosemgrep → suppress the line
49
+ return m[1]
50
+ .split(",")
51
+ .map((s) => s.trim())
52
+ .filter(Boolean)
53
+ .includes(ruleId);
54
+ };
55
+ // The finding's own line (inline OK), then the line above (standalone comment only).
56
+ return (checkLine(lines[startLine], false) ||
57
+ checkLine(lines[startLine - 1], true));
58
+ }
23
59
  export const AUXILIARY_LSP_PROFILES = [
24
60
  {
25
61
  serverId: "opengrep",
@@ -37,6 +73,8 @@ export const AUXILIARY_LSP_PROFILES = [
37
73
  allowBlocking: (cwd) => Boolean(findLocalOpengrepConfig(cwd)),
38
74
  semantic: (d, { blockingAllowed }) => blockingAllowed && d.severity === 1 ? "blocking" : "warning",
39
75
  defectClass: (d) => classifyDefect(String(d.code ?? ""), "opengrep", d.message ?? ""),
76
+ // Honor the canonical Semgrep suppression the user already knows (#441).
77
+ isSuppressed: isNosemgrepSuppressed,
40
78
  },
41
79
  {
42
80
  serverId: "ast-grep",
@@ -27,6 +27,7 @@ import { loadPiLensProjectConfig } from "../project-lens-config.js";
27
27
  import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
28
28
  import { safeSpawnAsync } from "../safe-spawn.js";
29
29
  import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
30
+ import { applyInlineSuppressions } from "./inline-suppressions.js";
30
31
  import { getToolPlan } from "./plan.js";
31
32
  import { resolveRunnerPath } from "./runner-context.js";
32
33
  import { getToolProfile } from "./tool-profile.js";
@@ -187,42 +188,6 @@ function dedupeOverlappingDiagnostics(diagnostics) {
187
188
  }
188
189
  return [...byKey.values()];
189
190
  }
190
- /**
191
- * Apply inline suppression comments.
192
- * Syntax: `// pi-lens-ignore: rule-id` (JS/TS) or `# pi-lens-ignore: rule-id` (Python/Ruby/etc.)
193
- * Place on the same line as the diagnostic or the line immediately above it.
194
- */
195
- function applyInlineSuppressions(diagnostics, content) {
196
- if (!content || !diagnostics.length)
197
- return diagnostics;
198
- // Build a set of (line, ruleId) pairs that are suppressed.
199
- // Line numbers are 1-based to match diagnostic line numbers.
200
- const suppressed = new Set();
201
- const lines = content.split("\n");
202
- const SUPPRESS_RE = /(?:\/\/|#)\s*pi-lens-ignore:\s*(.+)/;
203
- for (let i = 0; i < lines.length; i++) {
204
- const m = SUPPRESS_RE.exec(lines[i]);
205
- if (!m)
206
- continue;
207
- const rules = m[1]
208
- .split(",")
209
- .map((r) => r.trim())
210
- .filter(Boolean);
211
- const suppressedLine = i + 1; // same line (1-based)
212
- const nextLine = i + 2; // next line (1-based)
213
- for (const ruleId of rules) {
214
- suppressed.add(`${suppressedLine}:${ruleId}`);
215
- suppressed.add(`${nextLine}:${ruleId}`);
216
- }
217
- }
218
- if (suppressed.size === 0)
219
- return diagnostics;
220
- return diagnostics.filter((d) => {
221
- const ruleId = d.rule ?? d.id ?? "";
222
- const line = d.line ?? 1;
223
- return !suppressed.has(`${line}:${ruleId}`);
224
- });
225
- }
226
191
  function suppressLintOverlapsWithLsp(diagnostics) {
227
192
  const lspBySpanClass = new Set();
228
193
  const lspByLine = new Set();
@@ -230,7 +195,7 @@ function suppressLintOverlapsWithLsp(diagnostics) {
230
195
  return getToolProfile(tool).lintLike;
231
196
  };
232
197
  for (const d of diagnostics) {
233
- if (d.tool !== "lsp" && d.tool !== "ts-lsp")
198
+ if (d.tool !== "lsp")
234
199
  continue;
235
200
  const line = d.line ?? 1;
236
201
  const defectClass = d.defectClass ?? classifyDiagnostic(d);
@@ -240,7 +205,7 @@ function suppressLintOverlapsWithLsp(diagnostics) {
240
205
  if (lspByLine.size === 0)
241
206
  return diagnostics;
242
207
  return diagnostics.filter((d) => {
243
- if (d.tool === "lsp" || d.tool === "ts-lsp")
208
+ if (d.tool === "lsp")
244
209
  return true;
245
210
  if (!isLintTool(d.tool))
246
211
  return true;
@@ -628,7 +593,7 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
628
593
  const inlineFixed = fixedItems;
629
594
  const coverageNotice = buildCoverageNotice(ctx, runnerLatencies);
630
595
  // Format output — only blocking issues shown inline
631
- // Warnings tracked but not shown (noise) — surfaced via /lens-booboo
596
+ // Warnings tracked but not shown (noise) — surfaced via lens_diagnostics
632
597
  const blockerOutput = formatDiagnostics(inlineBlockers, "blocking");
633
598
  let output = blockerOutput;
634
599
  output += formatDiagnostics(inlineFixed, "fixed");
@@ -1,4 +1,3 @@
1
- import { registerRule } from "./fact-rule-runner.js";
2
1
  import { scheduleProviders } from "./fact-scheduler.js";
3
2
  const providers = [];
4
3
  export function registerProvider(p) {
@@ -8,64 +7,17 @@ export function clearProviders() {
8
7
  providers.length = 0;
9
8
  }
10
9
  /**
11
- * The TypeScript-compiler-backed fact providers + rules are registered LAZILY,
12
- * the first time any dispatch actually runs — NOT at module import. This keeps
13
- * the 24 MB `typescript` dependency out of pi-lens's eager entry graph, so a
14
- * failure to resolve it (#285/#335: a package-manager layout the runtime can't
15
- * traverse) degrades to "no TS-based structural analysis" instead of crashing
16
- * the whole extension at load. `runProviders` is the single seam every dispatch
17
- * path funnels through, so registering here covers them all (integration's three
18
- * entries + the project scanner) with no per-caller wiring.
10
+ * Run the registered fact providers for `ctx`, in dependency order.
11
+ *
12
+ * Providers + fact rules are registered eagerly at `integration.ts` import (the
13
+ * dispatch entry) including the tree-sitter-backed providers. That's safe for
14
+ * pi-lens's eager graph because the parsing stack loads `web-tree-sitter` (an
15
+ * optional dep) via a dynamic `import()` inside `client.init()`, not at module
16
+ * import, so an unavailable grammar/runtime degrades there rather than crashing
17
+ * the extension at load. As of #402 nothing here uses the `typescript` compiler,
18
+ * so the old lazy-import-with-degrade indirection (#285/#335) is gone.
19
19
  */
20
- let tsUnitsEnsured = null;
21
- export function ensureTypeScriptDispatchUnits() {
22
- tsUnitsEnsured ??= (async () => {
23
- try {
24
- const [tryCatch, fn, comment, imp, quality, sonar] = await Promise.all([
25
- import("./facts/try-catch-facts.js"),
26
- import("./facts/function-facts.js"),
27
- import("./facts/comment-facts.js"),
28
- import("./facts/import-facts.js"),
29
- import("./rules/quality-rules.js"),
30
- import("./rules/sonar-rules.js"),
31
- ]);
32
- registerProvider(tryCatch.tryCatchFactProvider);
33
- registerProvider(fn.functionFactProvider);
34
- registerProvider(comment.commentFactProvider);
35
- registerProvider(imp.importFactProvider);
36
- registerRule(quality.highImportCouplingRule);
37
- registerRule(quality.noBooleanParamsRule);
38
- registerRule(quality.noComplexConditionalsRule);
39
- registerRule(sonar.commentedCredentialsRule);
40
- registerRule(sonar.commentedOutCodeRule);
41
- registerRule(sonar.corsWildcardRule);
42
- registerRule(sonar.duplicateStringLiteralRule);
43
- registerRule(sonar.dynamicRegexpRule);
44
- registerRule(sonar.functionInLoopRule);
45
- registerRule(sonar.maxSwitchCasesRule);
46
- }
47
- catch (err) {
48
- // Degrade, don't crash: name the failure + emit the install fingerprint
49
- // so a reporter has the full picture. tsUnitsEnsured stays resolved, so we
50
- // don't re-attempt (and re-log) on every subsequent dispatch.
51
- try {
52
- const { collectInstallDiagnostics, formatInstallDiagnostics } = await import("../install-diagnostics.js");
53
- console.error(`[pi-lens] TypeScript-based dispatch analysis disabled (degraded mode): ${err?.message ?? String(err)}`);
54
- console.error(formatInstallDiagnostics(collectInstallDiagnostics(), err));
55
- }
56
- catch {
57
- console.error(`[pi-lens] TypeScript-based dispatch analysis disabled: ${err?.message ?? String(err)}`);
58
- }
59
- }
60
- })();
61
- return tsUnitsEnsured;
62
- }
63
- /** Test-only: drop the memoized lazy-registration so it can be re-exercised. */
64
- export function _resetTypeScriptDispatchUnitsForTests() {
65
- tsUnitsEnsured = null;
66
- }
67
20
  export async function runProviders(ctx) {
68
- await ensureTypeScriptDispatchUnits();
69
21
  const applicable = providers.filter((p) => p.appliesTo(ctx));
70
22
  const ordered = scheduleProviders(applicable);
71
23
  for (const provider of ordered) {
@@ -1,4 +1,4 @@
1
- import { ts } from "../../deps/typescript.js";
1
+ import { extractFactsFromTree, walk } from "./tree-sitter-facts.js";
2
2
  export const commentFactProvider = {
3
3
  id: "fact.file.comments",
4
4
  provides: ["file.comments"],
@@ -6,30 +6,17 @@ export const commentFactProvider = {
6
6
  appliesTo(ctx) {
7
7
  return /\.tsx?$/.test(ctx.filePath);
8
8
  },
9
- run(ctx, store) {
10
- const content = store.getFileFact(ctx.filePath, "file.content");
11
- if (!content) {
12
- store.setFileFact(ctx.filePath, "file.comments", []);
13
- return;
14
- }
15
- const sourceFile = ts.createSourceFile(ctx.filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
16
- const comments = [];
17
- const pushComment = (pos, end) => {
18
- const lc = sourceFile.getLineAndCharacterOfPosition(pos);
19
- comments.push({
20
- line: lc.line + 1,
21
- text: content.slice(pos, end),
9
+ async run(ctx, store) {
10
+ await extractFactsFromTree(ctx, store, { "file.comments": [] }, (root) => {
11
+ // Tree-sitter attaches comments as `comment` nodes wherever they occur; a
12
+ // pre-order walk yields them in source order (matching the old scanner pass).
13
+ const comments = [];
14
+ walk(root, (node) => {
15
+ if (node.type === "comment") {
16
+ comments.push({ line: node.startPosition.row + 1, text: node.text });
17
+ }
22
18
  });
23
- };
24
- const scan = ts.createScanner(sourceFile.languageVersion, false, sourceFile.languageVariant, content);
25
- let token = scan.scan();
26
- while (token !== ts.SyntaxKind.EndOfFileToken) {
27
- if (token === ts.SyntaxKind.SingleLineCommentTrivia ||
28
- token === ts.SyntaxKind.MultiLineCommentTrivia) {
29
- pushComment(scan.getTokenPos(), scan.getTextPos());
30
- }
31
- token = scan.scan();
32
- }
33
- store.setFileFact(ctx.filePath, "file.comments", comments);
19
+ return { "file.comments": comments };
20
+ });
34
21
  },
35
22
  };