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
@@ -669,7 +669,7 @@ export class TreeSitterClient {
669
669
  let hash = 0;
670
670
  for (let i = 0; i < pattern.length; i++) {
671
671
  const char = pattern.charCodeAt(i);
672
- hash = ((hash << 5) - hash + char) | 0;
672
+ hash = ((hash << 5) - hash + char) | 0; // NOSONAR: intentional 32-bit truncation for hash stability, not float→int conversion
673
673
  }
674
674
  return `${languageId}:${hash.toString(36)}`;
675
675
  }
@@ -0,0 +1,283 @@
1
+ import * as path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
+
5
+ // ── Types ────────────────────────────────────────────────────────────────────
6
+
7
+ export interface WidgetDiagnostic {
8
+ severity: string;
9
+ message: string;
10
+ line?: number;
11
+ col?: number;
12
+ rule?: string;
13
+ tool?: string;
14
+ uri?: string;
15
+ }
16
+
17
+ interface FileRecord {
18
+ filePath: string;
19
+ runners: Map<string, { status: string; count: number; durationMs?: number }>;
20
+ formatters: Map<string, { changed: boolean; success: boolean }>;
21
+ diagnostics: WidgetDiagnostic[];
22
+ touchedAt: number;
23
+ }
24
+
25
+ interface LspRecord {
26
+ serverId: string;
27
+ root: string;
28
+ status: "spawning" | "ready" | "failed";
29
+ durationMs?: number;
30
+ }
31
+
32
+ // ── Module state ─────────────────────────────────────────────────────────────
33
+
34
+ const files = new Map<string, FileRecord>();
35
+ const lspServers = new Map<string, LspRecord>();
36
+ let sessionLanguages: string[] = [];
37
+ let requestRenderFn: (() => void) | null = null;
38
+
39
+ // ── Public API ────────────────────────────────────────────────────────────────
40
+
41
+ export function setRenderCallback(fn: () => void): void {
42
+ requestRenderFn = fn;
43
+ }
44
+
45
+ export function clearWidgetState(): void {
46
+ files.clear();
47
+ lspServers.clear();
48
+ sessionLanguages = [];
49
+ }
50
+
51
+ export function setSessionLanguages(langs: string[]): void {
52
+ sessionLanguages = langs;
53
+ requestRender();
54
+ }
55
+
56
+ export function recordFormatter(
57
+ filePath: string,
58
+ formatter: string,
59
+ changed: boolean,
60
+ success: boolean,
61
+ ): void {
62
+ const rec = getOrCreate(filePath);
63
+ rec.formatters.set(formatter, { changed, success });
64
+ rec.touchedAt = Date.now();
65
+ files.set(filePath, rec);
66
+ requestRender();
67
+ }
68
+
69
+ export function recordRunner(
70
+ filePath: string,
71
+ runnerId: string,
72
+ status: string,
73
+ diagnosticCount: number,
74
+ durationMs?: number,
75
+ ): void {
76
+ const rec = getOrCreate(filePath);
77
+ rec.runners.set(runnerId, { status, count: diagnosticCount, durationMs });
78
+ rec.touchedAt = Date.now();
79
+ files.set(filePath, rec);
80
+ requestRender();
81
+ }
82
+
83
+ export function recordDiagnostics(
84
+ filePath: string,
85
+ diagnostics: Array<{
86
+ tool?: string;
87
+ rule?: string;
88
+ id?: string;
89
+ message?: string;
90
+ line?: number;
91
+ column?: number;
92
+ severity?: string;
93
+ }>,
94
+ ): void {
95
+ const rec = getOrCreate(filePath);
96
+ const base = pathToFileURL(filePath).href;
97
+ rec.diagnostics = diagnostics.map((d) => {
98
+ const rule = d.rule ?? d.id;
99
+ const uri =
100
+ d.line != null
101
+ ? `${base}#L${d.line}${d.column != null ? `:${d.column}` : ""}`
102
+ : base;
103
+ return {
104
+ severity: d.severity ?? "info",
105
+ message: d.message ?? "",
106
+ line: d.line,
107
+ col: d.column,
108
+ rule,
109
+ tool: d.tool,
110
+ uri,
111
+ };
112
+ });
113
+ rec.touchedAt = Date.now();
114
+ files.set(filePath, rec);
115
+ requestRender();
116
+ }
117
+
118
+ export function recordLsp(
119
+ serverId: string,
120
+ root: string,
121
+ status: "spawn_start" | "spawn_success" | "spawn_failed" | "unavailable",
122
+ durationMs?: number,
123
+ ): void {
124
+ const key = `${serverId}@${root}`;
125
+ const mapped =
126
+ status === "spawn_start"
127
+ ? "spawning"
128
+ : status === "spawn_success"
129
+ ? "ready"
130
+ : "failed";
131
+ lspServers.set(key, { serverId, root, status: mapped, durationMs });
132
+ requestRender();
133
+ }
134
+
135
+ // ── Render ────────────────────────────────────────────────────────────────────
136
+
137
+ export function renderWidget(
138
+ width: number,
139
+ theme: {
140
+ fg: (color: string, s: string) => string;
141
+ },
142
+ ): string[] {
143
+ const dim = (s: string) => theme.fg("dim", s);
144
+ const red = (s: string) => theme.fg("error", s);
145
+ const yellow = (s: string) => theme.fg("warning", s);
146
+ const green = (s: string) => theme.fg("success", s);
147
+ const cyan = (s: string) => theme.fg("accent", s);
148
+ const w = Math.max(1, width || 80);
149
+
150
+ if (files.size === 0 && lspServers.size === 0) return [];
151
+
152
+ const lines: string[] = [];
153
+
154
+ // Header — counts from deduplicated files only
155
+ const deduped = dedupeByBasename([...files.values()]);
156
+ const sorted = deduped.slice(0, 5);
157
+ const langStr = sessionLanguages.slice(0, 6).join(" ");
158
+ const totalErrors = countTotalIn("error", deduped);
159
+ const totalWarnings = countTotalIn("warning", deduped);
160
+ const summary =
161
+ totalErrors > 0
162
+ ? red(`●${totalErrors}E`) +
163
+ (totalWarnings > 0 ? " " + yellow(`▲${totalWarnings}W`) : "")
164
+ : totalWarnings > 0
165
+ ? yellow(`▲${totalWarnings}W`)
166
+ : files.size > 0
167
+ ? green("✓ clean")
168
+ : "";
169
+ const header = ` ${cyan("pi-lens")}${langStr ? " " + dim(langStr) : ""}${summary ? " " + summary : ""}`;
170
+ lines.push(fitLine(header, w));
171
+
172
+ // File list — most recently touched first, dedup by basename (last wins), cap at 5
173
+ for (const rec of sorted) {
174
+ const base = path.basename(rec.filePath);
175
+ const errors = rec.diagnostics.filter((d) => d.severity === "error").length;
176
+ const warnings = rec.diagnostics.filter(
177
+ (d) => d.severity === "warning",
178
+ ).length;
179
+ const dot = errors > 0 ? red("●") : warnings > 0 ? yellow("▲") : green("✓");
180
+ const runnerNames = [...rec.runners.entries()]
181
+ .filter(([, r]) => r.status !== "skipped")
182
+ .map(([id]) => id)
183
+ .join(" ");
184
+ const counts =
185
+ errors > 0
186
+ ? " " +
187
+ red(`${errors}E`) +
188
+ (warnings > 0 ? " " + yellow(`${warnings}W`) : "")
189
+ : warnings > 0
190
+ ? " " + yellow(`${warnings}W`)
191
+ : " " + dim("clean");
192
+ const changedFormatters = [...rec.formatters.entries()]
193
+ .filter(([, f]) => f.changed)
194
+ .map(([name]) => name);
195
+ const formatMark =
196
+ changedFormatters.length > 0
197
+ ? dim(` fmt:${changedFormatters.join(",")}`)
198
+ : "";
199
+ const row = ` ${dot} ${base} ${dim(runnerNames)}${formatMark}${counts}`;
200
+ lines.push(fitLine(row, w));
201
+ }
202
+
203
+ // Diagnostics — errors from the most recently touched file that has them
204
+ const withErrors = sorted.filter((r) =>
205
+ r.diagnostics.some((d) => d.severity === "error"),
206
+ );
207
+ if (withErrors.length > 0) {
208
+ const rec = withErrors[0];
209
+ lines.push(fitLine(dim("─".repeat(Math.min(w, 60))), w));
210
+ lines.push(fitLine(` ${dim(path.basename(rec.filePath))}`, w));
211
+ const errors = rec.diagnostics
212
+ .filter((d) => d.severity === "error")
213
+ .slice(0, 5);
214
+ const warnings =
215
+ errors.length < 5
216
+ ? rec.diagnostics
217
+ .filter((d) => d.severity === "warning")
218
+ .slice(0, 5 - errors.length)
219
+ : [];
220
+ for (const d of [...errors, ...warnings]) {
221
+ const sev = d.severity === "error" ? red("●") : yellow("▲");
222
+ const loc = d.line != null ? osc8(d.uri ?? "", `L${d.line}`) : "";
223
+ const rule = d.rule ? dim(` ${d.rule}`) : "";
224
+ const prefix = ` ${sev} ${loc}${rule} `;
225
+ const msgWidth = Math.max(1, w - visibleWidth(prefix));
226
+ const msg = fitLine(d.message, msgWidth, "…");
227
+ lines.push(fitLine(`${prefix}${msg}`, w));
228
+ }
229
+ }
230
+
231
+ // LSP status — only spawning servers (ready ones are quiet)
232
+ const spawning = [...lspServers.values()].filter(
233
+ (s) => s.status === "spawning",
234
+ );
235
+ if (spawning.length > 0) {
236
+ const ids = spawning.map((s) => s.serverId).join(" ");
237
+ lines.push(fitLine(` ${dim(`LSP spawning: ${ids}`)}`, w));
238
+ }
239
+
240
+ return lines;
241
+ }
242
+
243
+ // ── Helpers ──────────────────────────────────────────────────────────────────
244
+
245
+ function getOrCreate(filePath: string): FileRecord {
246
+ return (
247
+ files.get(filePath) ?? {
248
+ filePath,
249
+ runners: new Map(),
250
+ formatters: new Map(),
251
+ diagnostics: [],
252
+ touchedAt: Date.now(),
253
+ }
254
+ );
255
+ }
256
+
257
+ function countTotalIn(severity: string, recs: FileRecord[]): number {
258
+ let n = 0;
259
+ for (const rec of recs)
260
+ n += rec.diagnostics.filter((d) => d.severity === severity).length;
261
+ return n;
262
+ }
263
+
264
+ function requestRender(): void {
265
+ requestRenderFn?.();
266
+ }
267
+
268
+ function osc8(uri: string, label: string): string {
269
+ if (!uri) return label;
270
+ return `\x1b]8;;${uri}\x1b\\${label}\x1b]8;;\x1b\\`;
271
+ }
272
+
273
+ function fitLine(s: string, maxWidth: number, ellipsis = "..."): string {
274
+ return truncateToWidth(s, Math.max(0, maxWidth), ellipsis);
275
+ }
276
+
277
+ function dedupeByBasename(recs: FileRecord[]): FileRecord[] {
278
+ const seen = new Map<string, FileRecord>();
279
+ for (const r of [...recs].sort((a, b) => a.touchedAt - b.touchedAt)) {
280
+ seen.set(path.basename(r.filePath), r);
281
+ }
282
+ return [...seen.values()].sort((a, b) => b.touchedAt - a.touchedAt);
283
+ }
@@ -37,6 +37,38 @@ import type { TypeCoverageClient } from "../clients/type-coverage-client.js";
37
37
  // Side-effect import: registers all fact providers and fact rules
38
38
  import "../clients/dispatch/integration.js";
39
39
 
40
+ const ROOT_MARKERS = ["package.json", "tsconfig.json", ".git", "Cargo.toml", "go.mod", "pyproject.toml"];
41
+
42
+ function hasRootMarker(dir: string): boolean {
43
+ return ROOT_MARKERS.some((m) => nodeFs.existsSync(path.join(dir, m)));
44
+ }
45
+
46
+ function resolveProjectRoot(startDir: string): string {
47
+ // Walk up: find nearest ancestor with a root marker
48
+ let dir = startDir;
49
+ const fsRoot = path.parse(dir).root;
50
+ while (dir !== fsRoot) {
51
+ if (hasRootMarker(dir)) return dir;
52
+ const parent = path.dirname(dir);
53
+ if (parent === dir) break;
54
+ dir = parent;
55
+ }
56
+
57
+ // Walk down one level: if exactly one immediate subdir has a root marker, use it
58
+ try {
59
+ const entries = nodeFs.readdirSync(startDir, { withFileTypes: true });
60
+ const candidates = entries
61
+ .filter((e) => e.isDirectory())
62
+ .map((e) => path.join(startDir, e.name))
63
+ .filter(hasRootMarker);
64
+ if (candidates.length === 1) return candidates[0];
65
+ } catch {
66
+ // unreadable dir — fall through
67
+ }
68
+
69
+ return startDir;
70
+ }
71
+
40
72
  // Module-level singleton — web-tree-sitter WASM must only be initialized once per process
41
73
  let _sharedTreeSitterClient: TreeSitterClient | null = null;
42
74
  function getSharedTreeSitterClient(): TreeSitterClient {
@@ -91,7 +123,7 @@ export async function handleBooboo(
91
123
  pi: ExtensionAPI,
92
124
  ) {
93
125
  const requestedPath = args.trim() || ctx.cwd || process.cwd();
94
- const targetPath = path.resolve(requestedPath);
126
+ const targetPath = resolveProjectRoot(path.resolve(requestedPath));
95
127
  const reviewRoot = targetPath;
96
128
 
97
129
  const categoryKey = (name: string) => name.toLowerCase().replace(/\s+/g, "-");
@@ -1751,7 +1783,7 @@ function findTopSimilarPairs(
1751
1783
 
1752
1784
  if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
1753
1785
  // Canonical pair key (sorted to avoid duplicates)
1754
- const pairKey = [entry1.id, entry2.id].sort().join("::");
1786
+ const pairKey = [entry1.id, entry2.id].sort((a, b) => a.localeCompare(b)).join("::");
1755
1787
  if (seenPairs.has(pairKey)) continue;
1756
1788
  seenPairs.add(pairKey);
1757
1789