pi-lens 3.8.21 → 3.8.23

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 (92) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/dispatcher.ts +75 -91
  4. package/clients/dispatch/fact-provider-types.ts +22 -0
  5. package/clients/dispatch/fact-rule-runner.ts +22 -0
  6. package/clients/dispatch/fact-runner.ts +28 -0
  7. package/clients/dispatch/fact-scheduler.ts +78 -0
  8. package/clients/dispatch/fact-store.ts +67 -0
  9. package/clients/dispatch/facts/comment-facts.ts +59 -0
  10. package/clients/dispatch/facts/file-content.ts +20 -0
  11. package/clients/dispatch/facts/function-facts.ts +177 -0
  12. package/clients/dispatch/facts/try-catch-facts.ts +80 -0
  13. package/clients/dispatch/integration.ts +130 -24
  14. package/clients/dispatch/priorities.ts +22 -0
  15. package/clients/dispatch/rules/async-noise.ts +43 -0
  16. package/clients/dispatch/rules/error-obscuring.ts +40 -0
  17. package/clients/dispatch/rules/error-swallowing.ts +35 -0
  18. package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
  19. package/clients/dispatch/rules/placeholder-comments.ts +47 -0
  20. package/clients/dispatch/runners/architect.ts +2 -1
  21. package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
  22. package/clients/dispatch/runners/biome-check.ts +40 -8
  23. package/clients/dispatch/runners/biome.ts +2 -1
  24. package/clients/dispatch/runners/eslint.ts +34 -6
  25. package/clients/dispatch/runners/go-vet.ts +2 -1
  26. package/clients/dispatch/runners/golangci-lint.ts +2 -1
  27. package/clients/dispatch/runners/index.ts +29 -27
  28. package/clients/dispatch/runners/lsp.ts +60 -4
  29. package/clients/dispatch/runners/oxlint.ts +2 -1
  30. package/clients/dispatch/runners/pyright.ts +2 -1
  31. package/clients/dispatch/runners/python-slop.ts +2 -1
  32. package/clients/dispatch/runners/rubocop.ts +2 -1
  33. package/clients/dispatch/runners/ruff.ts +2 -1
  34. package/clients/dispatch/runners/rust-clippy.ts +2 -1
  35. package/clients/dispatch/runners/shellcheck.ts +2 -1
  36. package/clients/dispatch/runners/similarity.ts +2 -1
  37. package/clients/dispatch/runners/spellcheck.ts +2 -1
  38. package/clients/dispatch/runners/sqlfluff.ts +2 -1
  39. package/clients/dispatch/runners/tree-sitter.ts +469 -1
  40. package/clients/dispatch/runners/ts-lsp.ts +2 -1
  41. package/clients/dispatch/runners/type-safety.ts +2 -1
  42. package/clients/dispatch/runners/yamllint.ts +2 -1
  43. package/clients/dispatch/tool-profile.ts +40 -0
  44. package/clients/dispatch/types.ts +3 -13
  45. package/clients/lsp/client.ts +366 -12
  46. package/clients/lsp/index.ts +374 -76
  47. package/clients/lsp/launch.ts +42 -2
  48. package/clients/lsp/server.ts +186 -12
  49. package/clients/pipeline.ts +2 -2
  50. package/clients/runtime-context.ts +2 -2
  51. package/clients/runtime-session.ts +43 -5
  52. package/clients/session-summary.ts +21 -0
  53. package/clients/tree-sitter-client.ts +162 -0
  54. package/clients/tree-sitter-logger.ts +47 -0
  55. package/clients/tree-sitter-query-loader.ts +13 -2
  56. package/index.ts +67 -17
  57. package/package.json +3 -1
  58. package/rules/rule-catalog.json +64 -0
  59. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  60. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  61. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  62. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  63. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  64. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  65. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  66. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  67. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  68. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  69. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  70. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  71. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  72. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  73. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  74. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  75. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  76. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  77. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  78. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  79. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  80. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  81. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  82. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  83. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  84. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  85. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  86. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  87. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  88. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  89. package/scripts/validate-rule-catalog.mjs +227 -0
  90. package/skills/lsp-navigation/SKILL.md +15 -3
  91. package/tools/lsp-navigation.js +466 -79
  92. package/tools/lsp-navigation.ts +587 -85
@@ -86,6 +86,38 @@ function nodeBinCandidates(root: string, baseName: string): string[] {
86
86
  return [localBase, baseName];
87
87
  }
88
88
 
89
+ function rubyBinCandidates(baseName: string): string[] {
90
+ const candidates: string[] = [];
91
+ const userProfile = process.env.USERPROFILE;
92
+ if (userProfile) {
93
+ candidates.push(
94
+ path.join(
95
+ userProfile,
96
+ ".local",
97
+ "share",
98
+ "mise",
99
+ "installs",
100
+ "ruby",
101
+ "bin",
102
+ `${baseName}.bat`,
103
+ ),
104
+ );
105
+ candidates.push(
106
+ path.join(
107
+ userProfile,
108
+ ".asdf",
109
+ "installs",
110
+ "ruby",
111
+ "bin",
112
+ `${baseName}.bat`,
113
+ ),
114
+ );
115
+ }
116
+ candidates.push(path.join("C:\\Ruby34-x64", "bin", `${baseName}.bat`));
117
+ candidates.push(path.join("C:\\Ruby33-x64", "bin", `${baseName}.bat`));
118
+ return candidates;
119
+ }
120
+
89
121
  async function launchWithDirectOrPackageManager(
90
122
  directCommands: string[],
91
123
  packageName: string,
@@ -424,6 +456,55 @@ export const TypeScriptServer: LSPServerInfo = {
424
456
  },
425
457
  };
426
458
 
459
+ export const DenoServer: LSPServerInfo = {
460
+ id: "deno",
461
+ name: "Deno Language Server",
462
+ installPolicy: "none",
463
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
464
+ root: createRootDetector(["deno.json", "deno.jsonc"]),
465
+ async spawn(root) {
466
+ try {
467
+ const proc = await launchLSP("deno", ["lsp"], { cwd: root });
468
+ return { process: proc, source: "direct" };
469
+ } catch {
470
+ return undefined;
471
+ }
472
+ },
473
+ };
474
+
475
+ export const BiomeLspServer: LSPServerInfo = {
476
+ id: "biome-lsp",
477
+ name: "Biome LSP Proxy",
478
+ installPolicy: "package-manager",
479
+ extensions: [
480
+ ".ts",
481
+ ".tsx",
482
+ ".js",
483
+ ".jsx",
484
+ ".mjs",
485
+ ".cjs",
486
+ ".mts",
487
+ ".cts",
488
+ ".json",
489
+ ".jsonc",
490
+ ".css",
491
+ ".scss",
492
+ ".sass",
493
+ ".less",
494
+ ],
495
+ root: PriorityRoot([["biome.json", "biome.jsonc", "package.json"], [".git"]]),
496
+ async spawn(root, options) {
497
+ const launched = await launchWithDirectOrPackageManager(
498
+ nodeBinCandidates(root, "biome"),
499
+ "@biomejs/biome",
500
+ ["lsp-proxy"],
501
+ { cwd: root, allowInstall: options?.allowInstall },
502
+ );
503
+ if (!launched) return undefined;
504
+ return { process: launched.process, source: launched.source };
505
+ },
506
+ };
507
+
427
508
  export const PythonServer: LSPServerInfo = {
428
509
  id: "python",
429
510
  name: "Pyright Language Server",
@@ -507,7 +588,7 @@ export const PythonServer: LSPServerInfo = {
507
588
  }
508
589
 
509
590
  // Spawn the LSP server
510
- let proc;
591
+ let proc: LSPProcess;
511
592
  if (langserverPath) {
512
593
  // Use resolved langserver path
513
594
  proc = await launchLSP(langserverPath, ["--stdio"], {
@@ -567,6 +648,30 @@ export const PythonServer: LSPServerInfo = {
567
648
  },
568
649
  };
569
650
 
651
+ export const PythonPylspServer: LSPServerInfo = {
652
+ id: "python-pylsp",
653
+ name: "Python LSP Server (pylsp)",
654
+ installPolicy: "none",
655
+ extensions: [".py", ".pyi"],
656
+ root: createRootDetector([
657
+ ".git",
658
+ "pyproject.toml",
659
+ "setup.py",
660
+ "setup.cfg",
661
+ "requirements.txt",
662
+ "Pipfile",
663
+ "poetry.lock",
664
+ ]),
665
+ async spawn(root) {
666
+ try {
667
+ const proc = await launchLSP("pylsp", [], { cwd: root });
668
+ return { process: proc, source: "direct" };
669
+ } catch {
670
+ return undefined;
671
+ }
672
+ },
673
+ };
674
+
570
675
  export const GoServer: LSPServerInfo = {
571
676
  id: "go",
572
677
  name: "gopls",
@@ -642,24 +747,48 @@ export const RubyServer: LSPServerInfo = {
642
747
  [],
643
748
  { cwd: root, allowInstall: options?.allowInstall },
644
749
  async () => {
645
- try {
646
- return await launchLSP("ruby-lsp", [], { cwd: root });
647
- } catch {
648
- const fallback = await launchWithDirectOrPackageManager(
649
- nodeBinCandidates(root, "solargraph"),
650
- "solargraph",
651
- ["stdio"],
652
- { cwd: root, allowInstall: options?.allowInstall },
653
- );
654
- if (!fallback) throw new Error("ENOENT: command not found");
655
- return fallback.process;
750
+ for (const command of ["ruby-lsp", ...rubyBinCandidates("ruby-lsp")]) {
751
+ try {
752
+ return await launchLSP(command, [], { cwd: root });
753
+ } catch {
754
+ // try next ruby-lsp candidate
755
+ }
656
756
  }
757
+
758
+ for (const command of ["solargraph", ...rubyBinCandidates("solargraph")]) {
759
+ try {
760
+ return await launchLSP(command, ["stdio"], { cwd: root });
761
+ } catch {
762
+ // try next solargraph candidate
763
+ }
764
+ }
765
+
766
+ throw new Error("ENOENT: command not found");
657
767
  },
658
768
  );
659
769
  return proc ? { process: proc, source: "interactive" } : undefined;
660
770
  },
661
771
  };
662
772
 
773
+ export const RubySolargraphServer: LSPServerInfo = {
774
+ id: "ruby-solargraph",
775
+ name: "Solargraph",
776
+ installPolicy: "none",
777
+ extensions: [".rb", ".rake", ".gemspec", ".ru"],
778
+ root: PriorityRoot([["Gemfile", ".ruby-version"], [".git"]]),
779
+ async spawn(root) {
780
+ for (const command of ["solargraph", ...rubyBinCandidates("solargraph")]) {
781
+ try {
782
+ const proc = await launchLSP(command, ["stdio"], { cwd: root });
783
+ return { process: proc, source: "direct" };
784
+ } catch {
785
+ // try next candidate
786
+ }
787
+ }
788
+ return undefined;
789
+ },
790
+ };
791
+
663
792
  export const PHPServer: LSPServerInfo = {
664
793
  id: "php",
665
794
  name: "Intelephense",
@@ -691,6 +820,16 @@ export const CSharpServer = createInteractiveServer({
691
820
  command: "csharp-ls",
692
821
  });
693
822
 
823
+ export const OmniSharpServer = createInteractiveServer({
824
+ id: "omnisharp",
825
+ name: "OmniSharp",
826
+ extensions: [".cs"],
827
+ root: createRootDetector([".sln", ".csproj", ".slnx"]),
828
+ language: "csharp",
829
+ command: "OmniSharp",
830
+ args: ["--languageserver"],
831
+ });
832
+
694
833
  export const FSharpServer = createInteractiveServer({
695
834
  id: "fsharp",
696
835
  name: "FSAutocomplete",
@@ -912,6 +1051,34 @@ export const JsonServer: LSPServerInfo = {
912
1051
  },
913
1052
  };
914
1053
 
1054
+ export const HtmlServer: LSPServerInfo = {
1055
+ id: "html",
1056
+ name: "VSCode HTML Language Server",
1057
+ installPolicy: "package-manager",
1058
+ extensions: [".html", ".htm"],
1059
+ root: PriorityRoot([["package.json", "index.html", "vite.config.ts"], [".git"]]),
1060
+ async spawn(_root, options) {
1061
+ const launched = await launchWithDirectOrPackageManager(
1062
+ nodeBinCandidates(process.cwd(), "vscode-html-language-server"),
1063
+ "vscode-html-languageserver-bin",
1064
+ ["--stdio"],
1065
+ { cwd: process.cwd(), allowInstall: options?.allowInstall },
1066
+ );
1067
+ if (!launched) return undefined;
1068
+ return { process: launched.process, source: launched.source };
1069
+ },
1070
+ };
1071
+
1072
+ export const TomlServer = createInteractiveServer({
1073
+ id: "toml",
1074
+ name: "Taplo",
1075
+ extensions: [".toml"],
1076
+ root: PriorityRoot([["pyproject.toml", "Cargo.toml", "taplo.toml"], [".git"]]),
1077
+ language: "toml",
1078
+ command: "taplo",
1079
+ args: ["lsp", "stdio"],
1080
+ });
1081
+
915
1082
  export const PrismaServer: LSPServerInfo = {
916
1083
  id: "prisma",
917
1084
  name: "Prisma Language Server",
@@ -1036,12 +1203,17 @@ export const CssServer: LSPServerInfo = {
1036
1203
 
1037
1204
  export const LSP_SERVERS: LSPServerInfo[] = [
1038
1205
  TypeScriptServer,
1206
+ DenoServer,
1207
+ BiomeLspServer,
1039
1208
  PythonServer,
1209
+ PythonPylspServer,
1040
1210
  GoServer,
1041
1211
  RustServer,
1042
1212
  RubyServer,
1213
+ RubySolargraphServer,
1043
1214
  PHPServer,
1044
1215
  CSharpServer,
1216
+ OmniSharpServer,
1045
1217
  FSharpServer,
1046
1218
  JavaServer,
1047
1219
  KotlinServer,
@@ -1061,6 +1233,8 @@ export const LSP_SERVERS: LSPServerInfo[] = [
1061
1233
  DockerServer,
1062
1234
  YamlServer,
1063
1235
  JsonServer,
1236
+ HtmlServer,
1237
+ TomlServer,
1064
1238
  PrismaServer,
1065
1239
  // Web frameworks & styling
1066
1240
  VueServer,
@@ -672,8 +672,8 @@ export async function runPipeline(
672
672
  for (const e of limited) {
673
673
  const line = (e.range?.start?.line ?? 0) + 1;
674
674
  const col = (e.range?.start?.character ?? 0) + 1;
675
- const code = e.code ? ` [${e.code}]` : "";
676
- c += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
675
+ const code = e.code ? ` code=${String(e.code)}` : "";
676
+ c += `\n line ${line}, col ${col}${code}: ${e.message.split("\n")[0].slice(0, 100)}`;
677
677
  }
678
678
  c += `${suffix}\n</diagnostics>`;
679
679
  }
@@ -29,7 +29,7 @@ export function consumeTurnEndFindings(
29
29
  export function consumeSessionStartGuidance(
30
30
  cacheManager: CacheManager,
31
31
  cwd: string,
32
- ): { messages: Array<{ role: "system"; content: string }> } | undefined {
32
+ ): { messages: Array<{ role: "user"; content: string }> } | undefined {
33
33
  const guidance = cacheManager.readCache<{ content: string }>(
34
34
  "session-start-guidance",
35
35
  cwd,
@@ -45,7 +45,7 @@ export function consumeSessionStartGuidance(
45
45
  return {
46
46
  messages: [
47
47
  {
48
- role: "system",
48
+ role: "user",
49
49
  content: `[pi-lens] Session guidance:\n\n${guidance.data.content}`,
50
50
  },
51
51
  ],
@@ -62,11 +62,27 @@ interface SessionStartDeps {
62
62
  resetLSPService: () => void;
63
63
  }
64
64
 
65
+ type StartupMode = "full" | "minimal" | "quick";
66
+
65
67
  function isCommandAvailable(command: string, args: string[] = ["--version"]): boolean {
66
68
  const result = safeSpawn(command, args, { timeout: 5000 });
67
69
  return !result.error && result.status === 0;
68
70
  }
69
71
 
72
+ function resolveStartupMode(): StartupMode {
73
+ const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
74
+ if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
75
+ return envMode;
76
+ }
77
+
78
+ const argv = process.argv;
79
+ if (argv.includes("--print") || argv.includes("-p")) {
80
+ return "quick";
81
+ }
82
+
83
+ return "full";
84
+ }
85
+
70
86
  function getLanguageInstallHints(
71
87
  languageProfile: ReturnType<typeof detectProjectLanguageProfile>,
72
88
  ): string[] {
@@ -99,6 +115,9 @@ export async function handleSessionStart(
99
115
  deps: SessionStartDeps,
100
116
  ): Promise<void> {
101
117
  const sessionStartMs = Date.now();
118
+ const startupMode = resolveStartupMode();
119
+ const allowBootstrapTasks = startupMode === "full";
120
+ const quickMode = startupMode === "quick";
102
121
  const {
103
122
  ctxCwd,
104
123
  getFlag,
@@ -131,10 +150,14 @@ export async function handleSessionStart(
131
150
  runtime.complexityBaselines.clear();
132
151
  resetDispatchBaselines();
133
152
  runtime.resetForSession();
153
+ dbg(`session_start startup mode: ${startupMode}`);
134
154
 
135
155
  if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
136
156
  resetLSPService();
137
157
  dbg("session_start: LSP service reset");
158
+ dbg(
159
+ "session_start: phase0 workspace diagnostics observation enabled (capability probe only)",
160
+ );
138
161
  }
139
162
 
140
163
  if (getFlag("auto-install")) {
@@ -166,7 +189,7 @@ export async function handleSessionStart(
166
189
  log(`Active tools: ${tools.join(", ")}`);
167
190
  dbg(`session_start tools: ${tools.join(", ")}`);
168
191
 
169
- if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
192
+ if (allowBootstrapTasks && getFlag("lens-lsp") && !getFlag("no-lsp")) {
170
193
  const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
171
194
  if (cleaned.length > 0) {
172
195
  notify(
@@ -179,6 +202,15 @@ export async function handleSessionStart(
179
202
 
180
203
  const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
181
204
  const cwd = ctxCwd ?? process.cwd();
205
+ if (quickMode) {
206
+ runtime.projectRoot = cwd;
207
+ dbg(
208
+ "session_start: quick mode active - skipping language profiling, preinstall, scans, and error debt baseline",
209
+ );
210
+ dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
211
+ return;
212
+ }
213
+
182
214
  const startupScan = resolveStartupScanContext(cwd);
183
215
  const scanRoot = startupScan.projectRoot ?? cwd;
184
216
  const useScanRootForSignals =
@@ -217,7 +249,9 @@ export async function handleSessionStart(
217
249
  return true;
218
250
  });
219
251
 
220
- if (startupDefaults.length > 0) {
252
+ if (!allowBootstrapTasks) {
253
+ dbg("session_start: skipping tool preinstall (startup mode)");
254
+ } else if (startupDefaults.length > 0) {
221
255
  dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
222
256
  for (const tool of startupDefaults) {
223
257
  const startedAt = Date.now();
@@ -247,7 +281,7 @@ export async function handleSessionStart(
247
281
  dbg("session_start: no language defaults selected for pre-install");
248
282
  }
249
283
 
250
- {
284
+ if (allowBootstrapTasks) {
251
285
  const pkgPath = path.join(analysisRoot, "package.json");
252
286
  try {
253
287
  const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
@@ -272,6 +306,8 @@ export async function handleSessionStart(
272
306
  } catch {
273
307
  // no package.json at cwd root
274
308
  }
309
+ } else {
310
+ dbg("session_start: skipping prettier preinstall probe (startup mode)");
275
311
  }
276
312
 
277
313
  const hasArchitectRules = architectClient.loadConfig(analysisRoot);
@@ -349,7 +385,9 @@ export async function handleSessionStart(
349
385
  // Each consumer already handles the "not ready yet" case gracefully
350
386
  // (cachedExports.size > 0, cachedProjectIndex != null, cache miss paths).
351
387
 
352
- if (!startupScan.canWarmCaches) {
388
+ if (!allowBootstrapTasks) {
389
+ dbg("session_start: skipping startup background scans (startup mode)");
390
+ } else if (!startupScan.canWarmCaches) {
353
391
  dbg(
354
392
  `session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
355
393
  );
@@ -500,7 +538,7 @@ export async function handleSessionStart(
500
538
  `session_start: background scans launched (${startupNotes.length} startup note(s))`,
501
539
  );
502
540
 
503
- const errorDebtEnabled = getFlag("error-debt");
541
+ const errorDebtEnabled = allowBootstrapTasks && getFlag("error-debt");
504
542
  const pendingDebt = cacheManager.readCache<{
505
543
  pendingCheck: boolean;
506
544
  baselineTestsPassed: boolean;
@@ -4,6 +4,13 @@
4
4
 
5
5
  import type { SessionStats } from "./diagnostic-tracker.js";
6
6
 
7
+ export interface SlopScoreSummary {
8
+ totalRuleDiagnostics: number;
9
+ totalKlocWritten: number;
10
+ scorePerKloc: number;
11
+ ruleCounts: Array<{ ruleId: string; count: number }>;
12
+ }
13
+
7
14
  export function formatSessionSummary(stats: SessionStats): string {
8
15
  if (stats.totalShown === 0) return "";
9
16
 
@@ -30,3 +37,17 @@ function pct(part: number, total: number): string {
30
37
  if (total === 0) return "0";
31
38
  return Math.round((part / total) * 100).toString();
32
39
  }
40
+
41
+ export function formatSlopScoreSummary(summary: SlopScoreSummary): string {
42
+ if (summary.totalRuleDiagnostics === 0 || summary.totalKlocWritten <= 0) {
43
+ return "";
44
+ }
45
+
46
+ const topRules = summary.ruleCounts.slice(0, 3);
47
+ const detail =
48
+ topRules.length > 0
49
+ ? ` (${topRules.map((entry) => `${entry.ruleId} ×${entry.count}`).join(", ")})`
50
+ : "";
51
+
52
+ return `Slop score: ${summary.scorePerKloc.toFixed(1)}/KLOC${detail}`;
53
+ }
@@ -892,6 +892,26 @@ export class TreeSitterClient {
892
892
  if (!secretPatterns.some((p) => p.test(varName))) continue;
893
893
  }
894
894
 
895
+ // Go: only keep bare-return-call matches when enclosing function returns error.
896
+ if (postFilter === "returns_error") {
897
+ const first = Object.values(captures)[0];
898
+ if (!first) continue;
899
+ const funcNode = this.navigator.findParent(first, [
900
+ "function_declaration",
901
+ "method_declaration",
902
+ ]);
903
+ if (!funcNode) continue;
904
+
905
+ const fnText = String(funcNode.text ?? "");
906
+ const signature = fnText.split("{", 1)[0]?.trim() ?? "";
907
+ const returnPartMatch = signature.match(
908
+ /func\s*(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(.*)$/s,
909
+ );
910
+ const returnPart = returnPartMatch?.[1]?.trim() ?? "";
911
+ const returnsError = returnPart.length > 0 && /\berror\b/.test(returnPart);
912
+ if (!returnsError) continue;
913
+ }
914
+
895
915
  // Python: except body that only contains pass (effectively empty)
896
916
  if (postFilter === "python_empty_except") {
897
917
  const bodyNode = captures.BODY;
@@ -921,6 +941,148 @@ export class TreeSitterClient {
921
941
  }
922
942
  }
923
943
 
944
+ // TS security/concurrency: strict sink filtering (query predicates are not reliable in this runtime)
945
+ if (postFilter === "ts_command_injection_sink") {
946
+ const mod = captures.MOD?.text ?? "";
947
+ const fn = captures.FN?.text ?? "";
948
+ if (mod !== "child_process") continue;
949
+ if (!/^(exec|execSync)$/.test(fn)) continue;
950
+ }
951
+
952
+ if (postFilter === "ts_ssrf_sink") {
953
+ const fn = captures.FN?.text ?? "";
954
+ const obj = captures.OBJ?.text ?? "";
955
+ const allowedFns = new Set([
956
+ "fetch",
957
+ "request",
958
+ "get",
959
+ "post",
960
+ "put",
961
+ "patch",
962
+ "delete",
963
+ ]);
964
+ if (!allowedFns.has(fn)) continue;
965
+
966
+ if (!obj) {
967
+ if (fn !== "fetch") continue;
968
+ } else {
969
+ const allowedObjs = new Set([
970
+ "axios",
971
+ "http",
972
+ "https",
973
+ "got",
974
+ "request",
975
+ "superagent",
976
+ "undici",
977
+ ]);
978
+ if (!allowedObjs.has(obj)) continue;
979
+ }
980
+ }
981
+
982
+ if (postFilter === "ts_weak_hash_algorithm") {
983
+ const fn = captures.FN?.text ?? "";
984
+ const alg = captures.ALG?.text ?? "";
985
+ if (fn !== "createHash") continue;
986
+ if (!/^(md5|sha1)$/i.test(alg)) continue;
987
+ }
988
+
989
+ if (postFilter === "ts_insecure_random_source") {
990
+ const obj = captures.OBJ?.text ?? "";
991
+ const fn = captures.FN?.text ?? "";
992
+ if (obj !== "Math" || fn !== "random") continue;
993
+ }
994
+
995
+ if (postFilter === "ts_detached_async_call") {
996
+ const fn = captures.FN?.text ?? "";
997
+ if (!/(Async$|fetch$|request$)/.test(fn)) {
998
+ continue;
999
+ }
1000
+ }
1001
+
1002
+ if (postFilter === "py_command_injection_sink") {
1003
+ const mod = captures.MOD?.text ?? "";
1004
+ const fn = captures.FN?.text ?? "";
1005
+ const kw = captures.KW?.text ?? "";
1006
+ const isOs = mod === "os" && /^(system|popen)$/.test(fn);
1007
+ const isSubprocess =
1008
+ mod === "subprocess" &&
1009
+ /^(run|Popen|call|check_output|check_call)$/.test(fn) &&
1010
+ kw === "shell";
1011
+ if (!isOs && !isSubprocess) continue;
1012
+ }
1013
+
1014
+ if (postFilter === "go_command_injection_sink") {
1015
+ const pkg = captures.PKG?.text ?? "";
1016
+ const fn = captures.FN?.text ?? "";
1017
+ const shell = captures.SHELL?.text ?? "";
1018
+ const flag = captures.FLAG?.text ?? "";
1019
+ if (pkg !== "exec") continue;
1020
+ if (!/^(Command|CommandContext)$/.test(fn)) continue;
1021
+ if (!/^"(sh|bash|zsh|cmd|powershell|pwsh)"$/.test(shell)) continue;
1022
+ if (!/^"(-c|\/c)"$/.test(flag)) continue;
1023
+ }
1024
+
1025
+ if (postFilter === "ruby_command_injection_sink") {
1026
+ const fn = captures.FN?.text ?? "";
1027
+ if (!/^(system|exec|spawn|popen|capture3|capture2|capture2e)$/.test(fn)) {
1028
+ continue;
1029
+ }
1030
+ }
1031
+
1032
+ if (postFilter === "py_ssrf_sink") {
1033
+ const mod = captures.MOD?.text ?? "";
1034
+ const fn = captures.FN?.text ?? "";
1035
+ if (mod !== "requests") continue;
1036
+ if (!/^(get|post|put|patch|delete|request|head|options)$/.test(fn))
1037
+ continue;
1038
+ }
1039
+
1040
+ if (postFilter === "py_path_traversal_sink") {
1041
+ const fn = captures.FN?.text ?? "";
1042
+ if (
1043
+ !/^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$/.test(
1044
+ fn,
1045
+ )
1046
+ )
1047
+ continue;
1048
+ }
1049
+
1050
+ if (postFilter === "go_path_traversal_sink") {
1051
+ const pkg = captures.PKG?.text ?? "";
1052
+ const fn = captures.FN?.text ?? "";
1053
+ if (!/^(os|ioutil)$/.test(pkg)) continue;
1054
+ if (!/^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(fn))
1055
+ continue;
1056
+ }
1057
+
1058
+ if (postFilter === "py_sql_injection_sink") {
1059
+ const fn = captures.FN?.text ?? "";
1060
+ if (!/^(execute|executemany|query|raw)$/.test(fn)) continue;
1061
+ }
1062
+
1063
+ if (postFilter === "go_sql_injection_sink") {
1064
+ const dbFn = captures.DBFN?.text ?? "";
1065
+ const fmtPkg = captures.FMTPKG?.text ?? "";
1066
+ const fmtFn = captures.FMTFN?.text ?? "";
1067
+ if (!/^(Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext)$/.test(dbFn))
1068
+ continue;
1069
+ if (fmtPkg !== "fmt" || fmtFn !== "Sprintf") continue;
1070
+ }
1071
+
1072
+ if (postFilter === "py_insecure_deserialization_sink") {
1073
+ const mod = captures.MOD?.text ?? "";
1074
+ const fn = captures.FN?.text ?? "";
1075
+ if (!/^(pickle|yaml)$/.test(mod)) continue;
1076
+ if (!/^(load|loads|unsafe_load)$/.test(fn)) continue;
1077
+ }
1078
+
1079
+ if (postFilter === "ruby_insecure_deserialization_sink") {
1080
+ const mod = captures.MOD?.text ?? "";
1081
+ const fn = captures.FN?.text ?? "";
1082
+ if (!/^(Marshal|YAML|Psych)$/.test(mod)) continue;
1083
+ if (!/^(load|unsafe_load)$/.test(fn)) continue;
1084
+ }
1085
+
924
1086
  // Use first capture for position info
925
1087
  if (match.captures.length > 0) {
926
1088
  const firstNode = match.captures[0].node;
@@ -0,0 +1,47 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
5
+ const TREE_SITTER_LOG_DIR = path.join(os.homedir(), ".pi-lens");
6
+ const TREE_SITTER_LOG_FILE = path.join(TREE_SITTER_LOG_DIR, "tree-sitter.log");
7
+
8
+ try {
9
+ if (!fs.existsSync(TREE_SITTER_LOG_DIR)) {
10
+ fs.mkdirSync(TREE_SITTER_LOG_DIR, { recursive: true });
11
+ }
12
+ } catch {}
13
+
14
+ export interface TreeSitterLogEntry {
15
+ ts?: string;
16
+ phase:
17
+ | "runner_start"
18
+ | "runner_skip"
19
+ | "queries_loaded"
20
+ | "query_error"
21
+ | "runner_complete"
22
+ | "entity_diff"
23
+ | "blast_radius";
24
+ filePath: string;
25
+ languageId?: string;
26
+ queryId?: string;
27
+ status?: string;
28
+ diagnostics?: number;
29
+ blocking?: number;
30
+ queryCount?: number;
31
+ effectiveQueryCount?: number;
32
+ cacheHit?: boolean;
33
+ reason?: string;
34
+ error?: string;
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+
38
+ export function logTreeSitter(entry: TreeSitterLogEntry): void {
39
+ const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
40
+ try {
41
+ fs.appendFileSync(TREE_SITTER_LOG_FILE, line);
42
+ } catch {}
43
+ }
44
+
45
+ export function getTreeSitterLogPath(): string {
46
+ return TREE_SITTER_LOG_FILE;
47
+ }