pi-lens 3.8.39 → 3.8.41

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 (65) hide show
  1. package/CHANGELOG.md +84 -5
  2. package/README.md +37 -1
  3. package/clients/biome-client.ts +5 -4
  4. package/clients/cache/rule-cache.ts +1 -1
  5. package/clients/complexity-client.ts +1 -1
  6. package/clients/dependency-checker.ts +1 -1
  7. package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
  8. package/clients/dispatch/dispatcher.ts +9 -0
  9. package/clients/dispatch/fact-scheduler.ts +1 -1
  10. package/clients/dispatch/integration.ts +58 -3
  11. package/clients/dispatch/runners/index.ts +2 -0
  12. package/clients/dispatch/runners/semgrep.ts +269 -0
  13. package/clients/dispatch/runners/shellcheck.ts +2 -8
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -11
  15. package/clients/dispatch/tool-profile.ts +1 -0
  16. package/clients/format-service.ts +10 -0
  17. package/clients/formatters.ts +22 -8
  18. package/clients/installer/index.ts +3 -3
  19. package/clients/knip-client.ts +360 -362
  20. package/clients/lsp/aggregation.ts +91 -0
  21. package/clients/lsp/client.ts +91 -38
  22. package/clients/lsp/index.ts +88 -72
  23. package/clients/lsp/launch.ts +107 -34
  24. package/clients/lsp/server-strategies.ts +71 -0
  25. package/clients/lsp/server.ts +76 -57
  26. package/clients/path-utils.ts +17 -0
  27. package/clients/pipeline.ts +23 -5
  28. package/clients/production-readiness.ts +2 -2
  29. package/clients/read-guard-logger.ts +41 -1
  30. package/clients/read-guard-tool-lines.ts +17 -4
  31. package/clients/read-guard.ts +95 -46
  32. package/clients/runtime-agent-end.ts +3 -0
  33. package/clients/runtime-session.ts +5 -0
  34. package/clients/runtime-tool-result.ts +48 -1
  35. package/clients/runtime-turn.ts +48 -4
  36. package/clients/sanitize.ts +1 -1
  37. package/clients/semgrep-config.ts +213 -0
  38. package/clients/tool-policy.ts +1982 -1936
  39. package/clients/tree-sitter-client.ts +1 -1
  40. package/clients/widget-state.ts +283 -0
  41. package/commands/booboo.ts +34 -2
  42. package/index.ts +231 -17
  43. package/package.json +3 -2
  44. package/rules/rule-catalog.json +25 -1
  45. package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
  46. package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
  47. package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
  48. package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
  49. package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
  50. package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
  51. package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
  52. package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
  53. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
  54. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
  55. package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
  56. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
  57. package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
  58. package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
  59. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
  60. package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
  61. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
  62. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
  63. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
  64. package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
  65. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
package/index.ts CHANGED
@@ -7,6 +7,11 @@ import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
7
7
  import { AstGrepClient } from "./clients/ast-grep-client.js";
8
8
  import { loadBootstrapClients } from "./clients/bootstrap.js";
9
9
  import { CacheManager } from "./clients/cache-manager.js";
10
+ import {
11
+ clearWidgetState,
12
+ renderWidget,
13
+ setRenderCallback,
14
+ } from "./clients/widget-state.js";
10
15
  import { getDiagnosticTracker } from "./clients/diagnostic-tracker.js";
11
16
  import {
12
17
  getCascadeSessionStats,
@@ -46,8 +51,21 @@ import {
46
51
  } from "./clients/runtime-context.js";
47
52
  import { RuntimeCoordinator } from "./clients/runtime-coordinator.js";
48
53
  import { handleSessionStart } from "./clients/runtime-session.js";
49
- import { handleToolResult } from "./clients/runtime-tool-result.js";
54
+ import {
55
+ clearLastAnalyzedStateCache,
56
+ handleToolResult,
57
+ } from "./clients/runtime-tool-result.js";
50
58
  import { cancelLSPIdleReset, handleTurnEnd } from "./clients/runtime-turn.js";
59
+ import { isExternalOrVendorFile } from "./clients/path-utils.js";
60
+ import { safeSpawnAsync } from "./clients/safe-spawn.js";
61
+ import {
62
+ createStarterSemgrepConfig,
63
+ findLocalSemgrepConfig,
64
+ loadPiLensSemgrepConfig,
65
+ removePiLensSemgrepConfig,
66
+ resolveSemgrepConfig,
67
+ savePiLensSemgrepConfig,
68
+ } from "./clients/semgrep-config.js";
51
69
  import { TreeSitterClient } from "./clients/tree-sitter-client.js";
52
70
  import { handleBooboo } from "./commands/booboo.js";
53
71
  import { initI18n, t } from "./i18n.js";
@@ -201,12 +219,16 @@ function isLspCapableFile(filePath: string): boolean {
201
219
  return LANGUAGE_POLICY[kind]?.lspCapable !== false;
202
220
  }
203
221
 
204
- function shouldSkipLspAutoTouch(filePath: string): boolean {
222
+ function shouldSkipLspAutoTouch(
223
+ filePath: string,
224
+ projectRoot: string,
225
+ ): boolean {
205
226
  const normalized = path.resolve(filePath).replace(/\\/g, "/").toLowerCase();
206
227
  const base = path.basename(filePath).toLowerCase();
207
228
 
208
229
  if (normalized.includes("/.pi-lens/")) return true;
209
230
  if (normalized.includes("/.harness/")) return true;
231
+ if (isExternalOrVendorFile(filePath, projectRoot)) return true;
210
232
  if (
211
233
  base === "stdout.jsonl" ||
212
234
  base === "stderr.txt" ||
@@ -295,6 +317,13 @@ export default function (pi: ExtensionAPI) {
295
317
 
296
318
  // --- Flags ---
297
319
 
320
+ pi.registerFlag("no-lens", {
321
+ description:
322
+ "Start pi-lens disabled for this session. Re-enable with /lens-toggle.",
323
+ type: "boolean",
324
+ default: false,
325
+ });
326
+
298
327
  pi.registerFlag("no-lsp", {
299
328
  description:
300
329
  "Disable unified LSP diagnostics and use language-specific fallbacks (for example ts-lsp, pyright)",
@@ -341,14 +370,148 @@ export default function (pi: ExtensionAPI) {
341
370
  default: false,
342
371
  });
343
372
 
373
+ pi.registerFlag("lens-semgrep", {
374
+ description:
375
+ "Enable Semgrep dispatch when a Semgrep config is available (or with --lens-semgrep-config)",
376
+ type: "boolean",
377
+ default: false,
378
+ });
379
+
380
+ pi.registerFlag("lens-semgrep-config", {
381
+ description:
382
+ "Semgrep config for dispatch: local path, auto, p/<pack>, or r/<rule>. Requires --lens-semgrep.",
383
+ type: "string",
384
+ default: "",
385
+ });
386
+
344
387
  pi.registerFlag("no-read-guard", {
345
388
  description: "Disable read-before-edit behavior monitor",
346
389
  type: "boolean",
347
390
  default: false,
348
391
  });
349
392
 
393
+ let lensEnabled = !pi.getFlag("no-lens");
394
+
350
395
  // --- Commands ---
351
396
 
397
+ pi.registerCommand("lens-toggle", {
398
+ description:
399
+ "Toggle pi-lens on/off for the current session. Usage: /lens-toggle",
400
+ handler: async (_args, ctx) => {
401
+ lensEnabled = !lensEnabled;
402
+ ctx.ui.notify(
403
+ lensEnabled
404
+ ? "pi-lens enabled for this session."
405
+ : "pi-lens disabled for this session. Run /lens-toggle again to resume.",
406
+ lensEnabled ? "info" : "warning",
407
+ );
408
+ },
409
+ });
410
+
411
+ pi.registerCommand("lens-semgrep", {
412
+ description:
413
+ "Manage Semgrep dispatch. Usage: /lens-semgrep status | enable [--config <auto|p/pack|path>] | disable | init",
414
+ handler: async (args, ctx) => {
415
+ const parts = normalizeCommandArgs(args);
416
+ const action = parts[0] ?? "status";
417
+ const cwd = ctx.cwd ?? runtime.projectRoot;
418
+
419
+ function readConfigArg(): string | undefined {
420
+ const flagIndex = parts.findIndex(
421
+ (part) => part === "--config" || part === "-c",
422
+ );
423
+ if (flagIndex >= 0) return parts[flagIndex + 1];
424
+ return parts[1] && !parts[1].startsWith("-") ? parts[1] : undefined;
425
+ }
426
+
427
+ if (action === "enable") {
428
+ const config = readConfigArg();
429
+ const localConfig = findLocalSemgrepConfig(cwd);
430
+ if (!config && !localConfig) {
431
+ ctx.ui.notify(
432
+ [
433
+ "Semgrep dispatch not enabled yet: no local .semgrep.yml was found.",
434
+ "Use `/lens-semgrep init` to create a starter local config, or `/lens-semgrep enable --config auto` / `p/<pack>` if you want Semgrep registry/platform configuration.",
435
+ "pi-lens will not auto-install Semgrep; install it with pipx/uv/brew first and login only if your chosen Semgrep config requires it.",
436
+ ].join("\n"),
437
+ "warning",
438
+ );
439
+ return;
440
+ }
441
+
442
+ const savedPath = savePiLensSemgrepConfig(cwd, {
443
+ enabled: true,
444
+ ...(config ? { config } : {}),
445
+ });
446
+ ctx.ui.notify(
447
+ `Semgrep dispatch enabled (${config ? `config: ${config}` : `local config: ${localConfig}`}). Saved ${savedPath}`,
448
+ "info",
449
+ );
450
+ return;
451
+ }
452
+
453
+ if (action === "disable") {
454
+ const savedPath = savePiLensSemgrepConfig(cwd, { enabled: false });
455
+ ctx.ui.notify(`Semgrep dispatch disabled. Saved ${savedPath}`, "info");
456
+ return;
457
+ }
458
+
459
+ if (action === "clear") {
460
+ const removed = removePiLensSemgrepConfig(cwd);
461
+ ctx.ui.notify(
462
+ removed
463
+ ? "Removed .pi-lens/semgrep.json; Semgrep now auto-enables only when local .semgrep.yml exists."
464
+ : "No .pi-lens/semgrep.json found.",
465
+ "info",
466
+ );
467
+ return;
468
+ }
469
+
470
+ if (action === "init") {
471
+ const configPath = createStarterSemgrepConfig(cwd);
472
+ const savedPath = savePiLensSemgrepConfig(cwd, { enabled: true });
473
+ ctx.ui.notify(
474
+ `Created starter Semgrep config at ${configPath} and enabled Semgrep dispatch (${savedPath}).`,
475
+ "info",
476
+ );
477
+ return;
478
+ }
479
+
480
+ if (action !== "status") {
481
+ ctx.ui.notify(
482
+ "Usage: /lens-semgrep status | enable [--config <auto|p/pack|path>] | disable | clear | init",
483
+ "warning",
484
+ );
485
+ return;
486
+ }
487
+
488
+ const localConfig = findLocalSemgrepConfig(cwd);
489
+ const piLensConfig = loadPiLensSemgrepConfig(cwd);
490
+ const resolved = resolveSemgrepConfig(cwd, {
491
+ enabled: Boolean(pi.getFlag("lens-semgrep")),
492
+ config: pi.getFlag("lens-semgrep-config"),
493
+ });
494
+ const version = await safeSpawnAsync("semgrep", ["--version"], {
495
+ cwd,
496
+ timeout: 5000,
497
+ });
498
+ const lines = [
499
+ "🔎 SEMGREP DISPATCH",
500
+ `CLI: ${!version.error && version.status === 0 ? `installed (${(version.stdout || version.stderr).trim()})` : "not found on PATH"}`,
501
+ `Local config: ${localConfig ?? "none"}`,
502
+ `pi-lens config: ${piLensConfig ? JSON.stringify(piLensConfig) : "none"}`,
503
+ `Effective: ${resolved.enabled ? "enabled" : "disabled"}`,
504
+ `Config arg: ${resolved.configArg ?? "none"}`,
505
+ ];
506
+ if (resolved.reason) lines.push(`Reason: ${resolved.reason}`);
507
+ lines.push(
508
+ "",
509
+ "No auto-install. Token/login is only needed for Semgrep AppSec/Pro/managed configs; local .semgrep.yml scans do not require a token.",
510
+ );
511
+ ctx.ui.notify(lines.join("\n"), resolved.enabled ? "info" : "warning");
512
+ },
513
+ });
514
+
352
515
  pi.registerCommand("lens-booboo", {
353
516
  description:
354
517
  "Full codebase review: design smells, complexity, AI slop detection, TODOs, dead code, duplicates, type coverage. Results saved to .pi-lens/reviews/. Usage: /lens-booboo [path]",
@@ -762,6 +925,20 @@ export default function (pi: ExtensionAPI) {
762
925
  resetLSPService,
763
926
  });
764
927
  ctx.ui && updateLspStatus(ctx.ui.setStatus, ctx.ui.theme);
928
+ clearWidgetState();
929
+ if (ctx.ui?.setWidget) {
930
+ ctx.ui.setWidget(
931
+ "pi-lens",
932
+ (tui: any, theme: any) => {
933
+ setRenderCallback(() => tui.requestRender());
934
+ return {
935
+ render: (width: number) => renderWidget(width, theme),
936
+ invalidate: () => setRenderCallback(() => {}),
937
+ };
938
+ },
939
+ { placement: "belowEditor" },
940
+ );
941
+ }
765
942
  } catch (sessionErr) {
766
943
  dbg(`session_start crashed: ${sessionErr}`);
767
944
  dbg(`session_start crash stack: ${(sessionErr as Error).stack}`);
@@ -770,6 +947,7 @@ export default function (pi: ExtensionAPI) {
770
947
 
771
948
  pi.on("tool_call", async (event, ctx) => {
772
949
  const toolName = (event as { toolName?: string }).toolName ?? "";
950
+ if (!lensEnabled) return;
773
951
  if (
774
952
  pi.getFlag("lens-guard") &&
775
953
  isGitCommitOrPushAttempt(toolName, event.input)
@@ -812,8 +990,16 @@ export default function (pi: ExtensionAPI) {
812
990
  );
813
991
  if (!nodeFs.existsSync(filePath)) return;
814
992
 
993
+ const isExternalOrVendor = isExternalOrVendorFile(
994
+ filePath,
995
+ runtime.projectRoot,
996
+ );
997
+
815
998
  const lspCapableFile = isLspCapableFile(filePath);
816
- const lspAutoTouchSkipped = shouldSkipLspAutoTouch(filePath);
999
+ const lspAutoTouchSkipped = shouldSkipLspAutoTouch(
1000
+ filePath,
1001
+ runtime.projectRoot,
1002
+ );
817
1003
  const lspAutoTouchEligible = lspCapableFile && !lspAutoTouchSkipped;
818
1004
  const shouldWarmReadLsp =
819
1005
  toolName === "read" &&
@@ -907,6 +1093,7 @@ export default function (pi: ExtensionAPI) {
907
1093
  if (
908
1094
  toolName === "read" &&
909
1095
  !pi.getFlag("no-lsp") &&
1096
+ !isExternalOrVendor &&
910
1097
  filePath &&
911
1098
  readInput &&
912
1099
  requestedReadLimit != null &&
@@ -961,7 +1148,7 @@ export default function (pi: ExtensionAPI) {
961
1148
  }
962
1149
 
963
1150
  // --- Read-Before-Edit Guard: record reads ---
964
- if (toolName === "read" && filePath) {
1151
+ if (toolName === "read" && filePath && !isExternalOrVendor) {
965
1152
  const totalLines = countFileLines(filePath);
966
1153
  const deliveredLimit = effectiveReadLimit ?? 1;
967
1154
  logReadGuardEvent({
@@ -1002,6 +1189,7 @@ export default function (pi: ExtensionAPI) {
1002
1189
  // Record complexity baseline for historical tracking (booboo/tdi).
1003
1190
  // Not shown inline - just captured for delta analysis.
1004
1191
  if (
1192
+ !isExternalOrVendor &&
1005
1193
  complexityClient.isSupportedFile(filePath) &&
1006
1194
  !runtime.complexityBaselines.has(filePath)
1007
1195
  ) {
@@ -1073,14 +1261,18 @@ export default function (pi: ExtensionAPI) {
1073
1261
  .map(({ label, value, corrected }) => {
1074
1262
  const preview = value.trimStart().slice(0, 60).replace(/\n/g, "↵");
1075
1263
  return (
1076
- `${label} ("${preview}…") uses different indentation than the file. ` +
1077
- `Corrected value (use this exactly):\n\n${corrected}`
1264
+ `${label} ("${preview}…") has mismatched indentation (tabs vs spaces).\n` +
1265
+ `Replace ${label} with this verbatim (do not shorten, do not change newText):\n\n${corrected}`
1078
1266
  );
1079
1267
  })
1080
1268
  .join("\n\n");
1081
1269
  return {
1082
1270
  block: true,
1083
- reason: `🔄 RETRYABLE — Indentation mismatch in oldText\n\n${details}`,
1271
+ reason:
1272
+ `🔄 RETRYABLE — Indentation mismatch detected\n\n` +
1273
+ `The file uses a different indentation style than your oldText. ` +
1274
+ `Retry the same edit call immediately with the corrected oldText shown below — copy it exactly as-is.\n\n` +
1275
+ details,
1084
1276
  };
1085
1277
  }
1086
1278
  }
@@ -1090,11 +1282,8 @@ export default function (pi: ExtensionAPI) {
1090
1282
  typeof readGuard?.isNewFile !== "function" ||
1091
1283
  !readGuard.isNewFile(filePath);
1092
1284
  if (readGuard && isExistingFile) {
1093
- const { touchedLines, preflightError } = getTouchedLinesForGuard(
1094
- event,
1095
- filePath,
1096
- runtime.telemetrySessionId,
1097
- );
1285
+ const { touchedLines, editRanges, preflightError } =
1286
+ getTouchedLinesForGuard(event, filePath, runtime.telemetrySessionId);
1098
1287
  if (preflightError) {
1099
1288
  return { block: true, reason: preflightError };
1100
1289
  }
@@ -1110,7 +1299,7 @@ export default function (pi: ExtensionAPI) {
1110
1299
  });
1111
1300
  const verdict =
1112
1301
  typeof readGuard.checkEdit === "function"
1113
- ? readGuard.checkEdit(filePath, touchedLines)
1302
+ ? readGuard.checkEdit(filePath, touchedLines, editRanges)
1114
1303
  : { action: "allow" as const };
1115
1304
  if (verdict.action === "block") {
1116
1305
  return {
@@ -1133,12 +1322,30 @@ export default function (pi: ExtensionAPI) {
1133
1322
  const dupeWarnings: string[] = [];
1134
1323
  const exportRe =
1135
1324
  /export\s+(?:async\s+)?(?:function|class|const|let|type|interface)\s+(\w+)/g;
1325
+ // Read current on-disk content once so we can check whether the file
1326
+ // being written already owns a given export (e.g. it IS the source and
1327
+ // another file merely re-exports from it). cachedExports only tracks one
1328
+ // file per name — whichever was scanned first — so a re-exporter can
1329
+ // win the slot and incorrectly shadow the original definition.
1330
+ let currentFileExports: Set<string> | undefined;
1331
+ if (filePath && nodeFs.existsSync(filePath)) {
1332
+ try {
1333
+ const currentContent = nodeFs.readFileSync(filePath, "utf-8");
1334
+ currentFileExports = new Set<string>();
1335
+ for (const m of currentContent.matchAll(exportRe)) {
1336
+ currentFileExports.add(m[1]);
1337
+ }
1338
+ } catch {
1339
+ // non-fatal — fall back to no current-export knowledge
1340
+ }
1341
+ }
1136
1342
  for (const match of newContent.matchAll(exportRe)) {
1137
1343
  const name = match[1];
1138
1344
  const existingFile = runtime.cachedExports.get(name);
1139
1345
  if (
1140
1346
  existingFile &&
1141
- path.resolve(existingFile) !== path.resolve(filePath)
1347
+ path.resolve(existingFile) !== path.resolve(filePath) &&
1348
+ !currentFileExports?.has(name)
1142
1349
  ) {
1143
1350
  dupeWarnings.push(
1144
1351
  `\`${name}\` already exists in ${path.relative(runtime.projectRoot, existingFile)}`,
@@ -1148,7 +1355,9 @@ export default function (pi: ExtensionAPI) {
1148
1355
  if (dupeWarnings.length > 0) {
1149
1356
  return {
1150
1357
  block: true,
1151
- reason: "🔴 STOP - Redefining existing export(s). Import instead:\n" + dupeWarnings.map((w) => " • " + w).join("\n"),
1358
+ reason:
1359
+ "🔴 STOP - Redefining existing export(s). Import instead:\n" +
1360
+ dupeWarnings.map((w) => " • " + w).join("\n"),
1152
1361
  };
1153
1362
  }
1154
1363
 
@@ -1241,6 +1450,7 @@ export default function (pi: ExtensionAPI) {
1241
1450
  // Real-time feedback on file writes/edits
1242
1451
  // biome-ignore lint/suspicious/noExplicitAny: pi.on overload mismatch for tool_result event type
1243
1452
  (pi as any).on("tool_result", async (event: any) => {
1453
+ if (!lensEnabled) return;
1244
1454
  updateRuntimeIdentityFromEvent(event);
1245
1455
  const { biomeClient, ruffClient, metricsClient, agentBehaviorClient } =
1246
1456
  await loadBootstrapClients();
@@ -1263,11 +1473,13 @@ export default function (pi: ExtensionAPI) {
1263
1473
 
1264
1474
  // --- Turn end: batch jscpd/madge on collected files, then clear state ---
1265
1475
  // Clear cascade snapshot at start of each new turn so stale data never leaks
1266
- pi.on("turn_start", () => {
1476
+ pi.on("turn_start", (_event: any) => {
1267
1477
  runtime.beginTurn();
1478
+ clearLastAnalyzedStateCache();
1268
1479
  });
1269
1480
 
1270
1481
  pi.on("agent_end", async (_event, ctx) => {
1482
+ if (!lensEnabled) return;
1271
1483
  try {
1272
1484
  await handleAgentEnd({
1273
1485
  ctxCwd: ctx.cwd,
@@ -1286,7 +1498,8 @@ export default function (pi: ExtensionAPI) {
1286
1498
  }
1287
1499
  });
1288
1500
 
1289
- pi.on("turn_end", async (_event, ctx) => {
1501
+ pi.on("turn_end", async (_event: any, ctx) => {
1502
+ if (!lensEnabled) return;
1290
1503
  try {
1291
1504
  const { jscpdClient, knipClient, depChecker, testRunnerClient } =
1292
1505
  await loadBootstrapClients();
@@ -1331,6 +1544,7 @@ export default function (pi: ExtensionAPI) {
1331
1544
  event: { messages?: Array<{ role: string; content: unknown }> } | unknown,
1332
1545
  ctx: { cwd?: string },
1333
1546
  ) => {
1547
+ if (!lensEnabled) return;
1334
1548
  try {
1335
1549
  const cwd = ctx.cwd ?? process.cwd();
1336
1550
  const turnEndFindings = consumeTurnEndFindings(cacheManager, cwd);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.8.39",
3
+ "version": "3.8.41",
4
4
  "type": "module",
5
- "description": "Real-time code feedback for pi \u2014 LSP, linters, formatters, type-checking, structural analysis & booboo",
5
+ "description": "Real-time code feedback for pi LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/apmantza/pi-lens.git"
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@ast-grep/napi": "^0.42.1",
66
+ "@mariozechner/pi-tui": "^0.72.1",
66
67
  "minimatch": "^10.2.5",
67
68
  "typebox": "^1.0.0",
68
69
  "typescript": "^6.0.3",
@@ -110,6 +110,30 @@
110
110
  { "rule_id": "raise-application-error-codes", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "invalid-error-code", "severity_default": "error", "confidence": "high", "status": "active" },
111
111
  { "rule_id": "no-synchronize", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "synchronize-usage", "severity_default": "error", "confidence": "high", "status": "active" },
112
112
  { "rule_id": "send-file-mimetype", "engine": "tree-sitter", "language": "python", "family": "reliability", "scope": "function", "canonical_concept": "send-file-missing-mimetype", "severity_default": "error", "confidence": "high", "status": "active" },
113
- { "rule_id": "noexcept-functions", "engine": "tree-sitter", "language": "cpp", "family": "reliability", "scope": "function", "canonical_concept": "missing-noexcept", "severity_default": "error", "confidence": "medium", "status": "active" }
113
+ { "rule_id": "noexcept-functions", "engine": "tree-sitter", "language": "cpp", "family": "reliability", "scope": "function", "canonical_concept": "missing-noexcept", "severity_default": "error", "confidence": "medium", "status": "active" },
114
+ { "rule_id": "no-octal-values", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "file", "canonical_concept": "octal-literal-usage", "severity_default": "error", "confidence": "high", "status": "active" },
115
+ { "rule_id": "short-circuit-logic", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "non-short-circuit-operator", "severity_default": "error", "confidence": "high", "status": "active" },
116
+ { "rule_id": "infinite-loop", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "infinite-loop", "severity_default": "error", "confidence": "medium", "status": "active" },
117
+ { "rule_id": "infinite-recursion", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "infinite-recursion", "severity_default": "error", "confidence": "medium", "status": "active" },
118
+ { "rule_id": "name-capitalization-conflict", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "class", "canonical_concept": "name-capitalization-conflict", "severity_default": "error", "confidence": "medium", "status": "active" },
119
+ { "rule_id": "no-super-torchscript", "engine": "tree-sitter", "language": "python", "family": "reliability", "scope": "function", "canonical_concept": "torchscript-super-call", "severity_default": "error", "confidence": "high", "status": "active" },
120
+ { "rule_id": "unnecessary-bit-ops", "engine": "tree-sitter", "language": "cpp", "family": "maintainability", "scope": "function", "canonical_concept": "unnecessary-bit-operation", "severity_default": "error", "confidence": "high", "status": "active" },
121
+ { "rule_id": "unnecessary-bit-ops-java", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "unnecessary-bit-operation", "severity_default": "error", "confidence": "high", "status": "active" },
122
+ { "rule_id": "switch-case-termination-js", "engine": "tree-sitter", "language": "javascript", "family": "reliability", "scope": "function", "canonical_concept": "switch-case-no-termination", "severity_default": "error", "confidence": "high", "status": "active" },
123
+ { "rule_id": "nchar-nvarchar2-bytes", "engine": "tree-sitter", "language": "plsql", "family": "reliability", "scope": "function", "canonical_concept": "nchar-size-in-bytes", "severity_default": "error", "confidence": "high", "status": "active" },
124
+ { "rule_id": "tests-include-assertions", "engine": "tree-sitter", "language": "java", "family": "maintainability", "scope": "function", "canonical_concept": "test-without-assertion", "severity_default": "error", "confidence": "high", "status": "active" },
125
+ { "rule_id": "mockito-initialized", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "class", "canonical_concept": "mockito-not-initialized", "severity_default": "error", "confidence": "high", "status": "active" },
126
+ { "rule_id": "resources-closed", "engine": "tree-sitter", "language": "java", "family": "reliability", "scope": "function", "canonical_concept": "resource-leak", "severity_default": "error", "confidence": "medium", "status": "active" },
127
+ { "rule_id": "lock-table", "engine": "tree-sitter", "language": "plsql", "family": "maintainability", "scope": "function", "canonical_concept": "lock-table-usage", "severity_default": "error", "confidence": "high", "status": "active" },
128
+ { "rule_id": "lock-table-cobol", "engine": "tree-sitter", "language": "cobol", "family": "maintainability", "scope": "file", "canonical_concept": "lock-table-usage", "severity_default": "error", "confidence": "high", "status": "active" },
129
+ { "rule_id": "await-in-loop", "engine": "tree-sitter", "language": "typescript", "family": "performance", "scope": "function", "canonical_concept": "await-inside-loop", "severity_default": "error", "confidence": "high", "status": "active" },
130
+ { "rule_id": "switch-case-termination", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "switch-case-no-termination", "severity_default": "error", "confidence": "high", "status": "active" },
131
+ { "rule_id": "no-octal-values", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "file", "canonical_concept": "octal-literal-usage", "severity_default": "error", "confidence": "high", "status": "active" },
132
+ { "rule_id": "short-circuit-logic", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "function", "canonical_concept": "non-short-circuit-operator", "severity_default": "error", "confidence": "high", "status": "active" },
133
+ { "rule_id": "infinite-loop", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "infinite-loop", "severity_default": "error", "confidence": "medium", "status": "active" },
134
+ { "rule_id": "self-assignment", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "self-assignment", "severity_default": "error", "confidence": "high", "status": "active" },
135
+ { "rule_id": "duplicate-function-arg", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "duplicate-parameter-name", "severity_default": "error", "confidence": "high", "status": "active" },
136
+ { "rule_id": "empty-switch-case", "engine": "tree-sitter", "language": "typescript", "family": "reliability", "scope": "function", "canonical_concept": "empty-switch-case", "severity_default": "error", "confidence": "high", "status": "active" },
137
+ { "rule_id": "default-not-last", "engine": "tree-sitter", "language": "typescript", "family": "maintainability", "scope": "function", "canonical_concept": "default-clause-not-last", "severity_default": "error", "confidence": "high", "status": "active" }
114
138
  ]
115
139
  }
@@ -0,0 +1,35 @@
1
+ # COBOL LOCK TABLE
2
+ # Detects LOCK TABLE in COBOL
3
+ id: lock-table-cobol
4
+ name: LOCK TABLE Should Not Be Used
5
+ severity: error
6
+ category: maintainability
7
+ defect_class: correctness
8
+ inline_tier: blocking
9
+ language: cobol
10
+
11
+ message: "LOCK TABLE should not be used"
12
+
13
+ description: |
14
+ LOCK TABLE causes concurrency issues. Use row-level locking.
15
+
16
+ query: |
17
+ (lock_table_statement
18
+ (LOCK) @LOCK)
19
+
20
+ metavars:
21
+ - LOCK
22
+
23
+ tags:
24
+ - maintainability
25
+ - cobol
26
+ - bad-practice
27
+
28
+ examples:
29
+ bad: |
30
+ EXEC SQL LOCK TABLE employees IN EXCLUSIVE MODE END-EXEC -- BAD
31
+
32
+ good: |
33
+ EXEC SQL SELECT * FROM employees FOR UPDATE END-EXEC -- GOOD
34
+
35
+ has_fix: false
@@ -0,0 +1,58 @@
1
+ # Unnecessary Bit Operations
2
+ # Detects bit operations that have no effect
3
+ id: unnecessary-bit-ops
4
+ name: Unnecessary Bit Operations Should Not Be Performed
5
+ severity: error
6
+ category: maintainability
7
+ defect_class: correctness
8
+ inline_tier: blocking
9
+ language: cpp
10
+
11
+ message: "Unnecessary bit operation - has no effect"
12
+
13
+ description: |
14
+ Operations like (x | 0), (x & -1), (x ^ 0) have no effect.
15
+ They indicate confusion or incomplete refactoring. Remove them.
16
+
17
+ ✅ FIX: Remove unnecessary operation
18
+
19
+ ```cpp
20
+ int y = x; // GOOD - simplified
21
+ ```
22
+
23
+ query: |
24
+ (binary_expression
25
+ (identifier) @VAR
26
+ "|" @OP
27
+ (number_literal) @ZERO (#eq? @ZERO "0"))
28
+ (binary_expression
29
+ (identifier) @VAR
30
+ "&" @OP
31
+ (number_literal) @VAL (#match? @VAL "^-?1+$"))
32
+ (binary_expression
33
+ (identifier) @VAR
34
+ "^" @OP
35
+ (number_literal) @ZERO (#eq? @ZERO "0"))
36
+
37
+ metavars:
38
+ - VAR
39
+ - OP
40
+ - ZERO
41
+ - VAL
42
+
43
+ tags:
44
+ - maintainability
45
+ - cpp
46
+ - suspicious
47
+
48
+ examples:
49
+ bad: |
50
+ int y = x | 0; // BAD - no effect
51
+ int z = x & -1; // BAD - no effect
52
+ int w = x ^ 0; // BAD - no effect
53
+
54
+ good: |
55
+ int y = x; // GOOD - simplified
56
+
57
+ has_fix: true
58
+ fix_action: remove_bit_operation
@@ -0,0 +1,58 @@
1
+ # Infinite Loop
2
+ # Detects potentially infinite while(true) loops without break
3
+ id: infinite-loop
4
+ name: Loops Should Not Be Infinite
5
+ severity: error
6
+ category: reliability
7
+ defect_class: correctness
8
+ inline_tier: blocking
9
+ language: java
10
+
11
+ message: "Loop appears to be infinite with no break condition"
12
+
13
+ description: |
14
+ while(true) or for(;;) without a break condition creates an
15
+ infinite loop. Ensure there's an exit condition with break,
16
+ return, or System.exit().
17
+
18
+ ✅ FIX: Add break condition or use scheduled executor
19
+
20
+ ```java
21
+ while (running) { // GOOD - control flag
22
+ if (shouldStop()) break;
23
+ }
24
+ ```
25
+
26
+ query: |
27
+ (while_statement
28
+ condition: (true) @COND
29
+ body: (block) @BODY)
30
+ (for_statement
31
+ condition: (null)
32
+ body: (block) @BODY)
33
+
34
+ metavars:
35
+ - COND
36
+ - BODY
37
+
38
+ post_filter: no_break_or_return
39
+
40
+ tags:
41
+ - reliability
42
+ - java
43
+ - cert
44
+ - bugs
45
+
46
+ examples:
47
+ bad: |
48
+ while (true) { // BAD - infinite
49
+ doWork();
50
+ }
51
+
52
+ good: |
53
+ while (true) { // GOOD - has exit
54
+ if (shouldStop()) break;
55
+ doWork();
56
+ }
57
+
58
+ has_fix: false