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.
- package/CHANGELOG.md +84 -5
- package/README.md +37 -1
- package/clients/biome-client.ts +5 -4
- package/clients/cache/rule-cache.ts +1 -1
- package/clients/complexity-client.ts +1 -1
- package/clients/dependency-checker.ts +1 -1
- package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
- package/clients/dispatch/dispatcher.ts +9 -0
- package/clients/dispatch/fact-scheduler.ts +1 -1
- package/clients/dispatch/integration.ts +58 -3
- package/clients/dispatch/runners/index.ts +2 -0
- package/clients/dispatch/runners/semgrep.ts +269 -0
- package/clients/dispatch/runners/shellcheck.ts +2 -8
- package/clients/dispatch/runners/tree-sitter.ts +32 -11
- package/clients/dispatch/tool-profile.ts +1 -0
- package/clients/format-service.ts +10 -0
- package/clients/formatters.ts +22 -8
- package/clients/installer/index.ts +3 -3
- package/clients/knip-client.ts +360 -362
- package/clients/lsp/aggregation.ts +91 -0
- package/clients/lsp/client.ts +91 -38
- package/clients/lsp/index.ts +88 -72
- package/clients/lsp/launch.ts +107 -34
- package/clients/lsp/server-strategies.ts +71 -0
- package/clients/lsp/server.ts +76 -57
- package/clients/path-utils.ts +17 -0
- package/clients/pipeline.ts +23 -5
- package/clients/production-readiness.ts +2 -2
- package/clients/read-guard-logger.ts +41 -1
- package/clients/read-guard-tool-lines.ts +17 -4
- package/clients/read-guard.ts +95 -46
- package/clients/runtime-agent-end.ts +3 -0
- package/clients/runtime-session.ts +5 -0
- package/clients/runtime-tool-result.ts +48 -1
- package/clients/runtime-turn.ts +48 -4
- package/clients/sanitize.ts +1 -1
- package/clients/semgrep-config.ts +213 -0
- package/clients/tool-policy.ts +1982 -1936
- package/clients/tree-sitter-client.ts +1 -1
- package/clients/widget-state.ts +283 -0
- package/commands/booboo.ts +34 -2
- package/index.ts +231 -17
- package/package.json +3 -2
- package/rules/rule-catalog.json +25 -1
- package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
- package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
- package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
- package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
- package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
- package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
- package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
- package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
- package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
- package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
- package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
- 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
|
+
}
|
package/commands/booboo.ts
CHANGED
|
@@ -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
|
|