pi-lens 3.8.72 → 3.8.73

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.
@@ -11,7 +11,7 @@ import { getProjectIgnoreMatcher, isExcludedDirName, } from "../clients/file-uti
11
11
  import { getLSPService, groupFilesByPrimaryServer, runPerServerGroups, } from "../clients/lsp/index.js";
12
12
  import { buildScopeKey, createWorkspaceDiagnosticsCacheContext, } from "../clients/lsp/workspace-diagnostics-cache.js";
13
13
  import { primaryServerId } from "../clients/lsp/config.js";
14
- import { combineAbortSignals } from "../clients/deadline-utils.js";
14
+ import { combineAbortSignals, withDeadline } from "../clients/deadline-utils.js";
15
15
  import { applyAuxiliarySuppressions, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
16
16
  import { detectFileRole } from "../clients/file-role.js";
17
17
  import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
@@ -59,6 +59,7 @@ const MAX_BATCH_FILES = 100;
59
59
  const MAX_DIAGNOSTICS = 200;
60
60
  const DEFAULT_BATCH_CONCURRENCY = 8;
61
61
  const MAX_BATCH_CONCURRENCY = 16;
62
+ const DEFAULT_BATCH_FILE_DEADLINE_MS = 15_000;
62
63
  // LSP severities: 1=Error, 2=Warning, 3=Information, 4=Hint
63
64
  const SEVERITY_NAMES = {
64
65
  1: "error",
@@ -66,6 +67,10 @@ const SEVERITY_NAMES = {
66
67
  3: "information",
67
68
  4: "hint",
68
69
  };
70
+ function batchFileDeadlineMs() {
71
+ const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
72
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
73
+ }
69
74
  // #646: `primaryServerId` moved to clients/lsp/config.ts so this tool and
70
75
  // tools/lens-diagnostics.ts's mode=full sweep share the exact same
71
76
  // primary-vs-auxiliary classification instead of each keeping its own copy.
@@ -252,6 +257,19 @@ nextWriteIndex) {
252
257
  const suffix = timedOutFiles > 0 ? ` (${timedOutFiles} timed out)` : "";
253
258
  return `lsp_diagnostics${scope} — ${count} ${noun} · ${cleanFiles} clean · ${unconfirmedFiles} unconfirmed${suffix}`;
254
259
  }
260
+ const outcomeCounts = details?.outcomeCounts;
261
+ const notConfirmed = outcomeCounts
262
+ ? (outcomeCounts.inconclusive ?? 0) +
263
+ (outcomeCounts.unavailable ?? 0) +
264
+ (outcomeCounts.unsupported ?? 0) +
265
+ (outcomeCounts.failed ?? 0)
266
+ : 0;
267
+ if (notConfirmed > 0) {
268
+ return `lsp_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
269
+ }
270
+ if ((details?.incompleteFiles ?? 0) > 0) {
271
+ return `lsp_diagnostics${scope} — incomplete (${details?.incompleteFiles} files not confirmed)`;
272
+ }
255
273
  // Single-file mode: 0 diagnostics from an unconfirmed result — either a
256
274
  // silent-on-clean server or (#570) a timed-out check — is not a clean
257
275
  // render either.
@@ -321,12 +339,29 @@ nextWriteIndex) {
321
339
  details: {},
322
340
  };
323
341
  }
324
- if (Array.isArray(typedParams.paths) &&
325
- typedParams.paths.length > 0) {
326
- const absPaths = typedParams.paths
327
- .filter((entry) => typeof entry === "string" && entry.trim().length > 0)
328
- .slice(0, MAX_BATCH_FILES)
329
- .map((entry) => path.isAbsolute(entry) ? entry : path.resolve(cwd, entry));
342
+ if (Array.isArray(typedParams.paths) && typedParams.paths.length > 0) {
343
+ if (typedParams.paths.length > MAX_BATCH_FILES) {
344
+ return {
345
+ content: [{
346
+ type: "text",
347
+ text: `paths accepts at most ${MAX_BATCH_FILES} files; received ${typedParams.paths.length}.`,
348
+ }],
349
+ isError: true,
350
+ details: { mode: "batch", filesChecked: 0 },
351
+ };
352
+ }
353
+ const rawPaths = typedParams.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
354
+ if (rawPaths.length !== typedParams.paths.length) {
355
+ return {
356
+ content: [{ type: "text", text: "paths must contain non-empty file paths." }],
357
+ isError: true,
358
+ details: { mode: "batch", filesChecked: 0 },
359
+ };
360
+ }
361
+ // Preserve input order (including duplicate entries); normalize each
362
+ // path before grouping so Windows separators and dot segments cannot
363
+ // change cache/group identity. Explicit lists never enter the walker.
364
+ const absPaths = rawPaths.map((entry) => path.normalize(path.isAbsolute(entry) ? entry : path.resolve(cwd, entry)));
330
365
  return runBatchFileDiagnostics(absPaths, severity, lspService, {
331
366
  concurrency,
332
367
  waitMs,
@@ -799,6 +834,34 @@ function tallyConfirmation(results) {
799
834
  }
800
835
  return { clean, unconfirmed, timedOut };
801
836
  }
837
+ function classifyBatchFileOutcome(result) {
838
+ if (result.error)
839
+ return "failed";
840
+ // A timed-out/unconfirmed answer may contain partial findings, but it cannot
841
+ // honestly be called a complete findings result. Keep the raw findings for
842
+ // investigation while making the aggregate incomplete.
843
+ if (result.timedOut || result.confirmation === "unconfirmed") {
844
+ return "inconclusive";
845
+ }
846
+ if (result.diagnostics.length > 0)
847
+ return "findings";
848
+ if (result.unavailable) {
849
+ return result.primaryServerId ? "unavailable" : "unsupported";
850
+ }
851
+ if (!result.primaryServerId)
852
+ return "unsupported";
853
+ return "clean";
854
+ }
855
+ function inconclusiveBatchResult(file, reason) {
856
+ return {
857
+ file,
858
+ diagnostics: [],
859
+ confirmation: "unconfirmed",
860
+ timedOut: true,
861
+ outcome: "inconclusive",
862
+ inconclusiveReason: reason,
863
+ };
864
+ }
802
865
  /**
803
866
  * #570: build the explanatory clause for a batch/directory result that has
804
867
  * unconfirmed files, distinguishing timed-out checks from the pre-existing
@@ -846,7 +909,32 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
846
909
  const resolvedCwd = options.cwd ?? process.cwd();
847
910
  const cacheCtx = createWorkspaceDiagnosticsCacheContext(resolvedCwd);
848
911
  const scopeKey = buildScopeKey(options.serverScope ?? "all");
849
- const results = await mapWithConcurrency(files, options.concurrency, (file) => collectFileDiagnosticResult(file, severity, lspService, options.waitMs, options.nextWriteIndex, options.serverScope, cacheCtx, scopeKey, resolvedCwd), lspService, options.signal, options.onProgress);
912
+ const results = await mapWithConcurrency(files, options.concurrency, async (file) => {
913
+ const work = collectFileDiagnosticResult(file, severity, lspService, options.waitMs, options.nextWriteIndex, options.serverScope, cacheCtx, scopeKey, resolvedCwd);
914
+ const bounded = withDeadline(work, {
915
+ ms: batchFileDeadlineMs(),
916
+ onTimeout: "undefined",
917
+ onReject: "undefined",
918
+ });
919
+ const result = options.signal
920
+ ? await Promise.race([
921
+ bounded,
922
+ new Promise((resolve) => {
923
+ if (options.signal?.aborted) {
924
+ resolve(undefined);
925
+ return;
926
+ }
927
+ options.signal?.addEventListener("abort", () => resolve(undefined), {
928
+ once: true,
929
+ });
930
+ }),
931
+ ])
932
+ : await bounded;
933
+ return (result ??
934
+ inconclusiveBatchResult(file, options.signal?.aborted
935
+ ? "Batch aborted before this file completed."
936
+ : `File check exceeded ${batchFileDeadlineMs()}ms.`));
937
+ }, lspService, options.signal, options.onProgress);
850
938
  // Persist whatever was recorded, including a partial/aborted sweep's
851
939
  // already-completed files — same "don't throw away confirmed work"
852
940
  // posture as `runWorkspaceDiagnostics`.
@@ -858,6 +946,10 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
858
946
  const truncated = total > MAX_DIAGNOSTICS;
859
947
  const display = truncated ? allDiags.slice(0, MAX_DIAGNOSTICS) : allDiags;
860
948
  const { clean, unconfirmed, timedOut } = tallyConfirmation(results);
949
+ for (const result of results) {
950
+ result.outcome = classifyBatchFileOutcome(result);
951
+ }
952
+ const outcomeCounts = Object.fromEntries(["clean", "findings", "unsupported", "unavailable", "failed", "inconclusive"].map((outcome) => [outcome, results.filter((result) => result.outcome === outcome).length]));
861
953
  // Per-file primary-server lookup so a flattened multi-file `display` list
862
954
  // can still be split into "primary findings" vs "auxiliary findings" —
863
955
  // `clean`/`unconfirmed` above already reflect ONLY the primary server's
@@ -877,6 +969,8 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
877
969
  clean,
878
970
  unconfirmed,
879
971
  timedOut,
972
+ outcomeCounts,
973
+ incompleteFiles: Math.max(0, files.length - results.length),
880
974
  };
881
975
  }
882
976
  async function runBatchFileDiagnostics(absPaths, severity, lspService, options) {
@@ -887,7 +981,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
887
981
  details: { mode: "batch", severity, filesChecked: 0 },
888
982
  };
889
983
  }
890
- const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, } = await collectBatchDiagnostics(absPaths, severity, lspService, options);
984
+ const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, incompleteFiles, } = await collectBatchDiagnostics(absPaths, severity, lspService, options);
891
985
  const lines = [
892
986
  `Files checked: ${results.length}`,
893
987
  `Total diagnostics: ${total}`,
@@ -895,6 +989,20 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
895
989
  ];
896
990
  if (options.waitMs !== undefined)
897
991
  lines.push(`Wait budget: ${options.waitMs}ms`);
992
+ lines.push(`Outcomes: ${Object.entries(outcomeCounts)
993
+ .map(([outcome, count]) => `${outcome}=${count}`)
994
+ .join(", ")}`);
995
+ const notConfirmed = outcomeCounts.inconclusive +
996
+ outcomeCounts.unavailable +
997
+ outcomeCounts.unsupported +
998
+ outcomeCounts.failed +
999
+ incompleteFiles;
1000
+ if (notConfirmed > 0) {
1001
+ lines.push("", `Checks not confirmed: ${notConfirmed} (inconclusive/unavailable/unsupported/failed or not started).`);
1002
+ }
1003
+ if (incompleteFiles > 0) {
1004
+ lines.push("", `Batch incomplete: ${incompleteFiles} file${incompleteFiles === 1 ? "" : "s"} were not confirmed because the batch was aborted.`);
1005
+ }
898
1006
  if (fileErrors.length > 0)
899
1007
  lines.push("", "File errors:", ...fileErrors);
900
1008
  if (lspHealthWarnings.length > 0) {
@@ -945,6 +1053,15 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
945
1053
  cleanFiles: clean,
946
1054
  unconfirmedFiles: unconfirmed,
947
1055
  timedOutFiles: timedOut > 0 ? timedOut : undefined,
1056
+ outcomes: results.map((result) => ({
1057
+ file: result.file,
1058
+ outcome: result.outcome,
1059
+ reason: result.inconclusiveReason ?? result.error ?? result.unavailable,
1060
+ primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.source === result.primaryServerId).length,
1061
+ auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.source !== result.primaryServerId).length,
1062
+ })),
1063
+ outcomeCounts,
1064
+ incompleteFiles: incompleteFiles > 0 ? incompleteFiles : undefined,
948
1065
  fileErrors: fileErrors.length > 0 ? fileErrors : undefined,
949
1066
  lspHealthWarnings: lspHealthWarnings.length > 0 ? lspHealthWarnings : undefined,
950
1067
  },
@@ -6,7 +6,7 @@ server's `initialize` handshake. Generated by `node scripts/server-capabilities.
6
6
  absent on the generating host are listed under *Unavailable* — run in a
7
7
  provisioned environment (the nightly) to capture those rows.
8
8
 
9
- _Last generated: 2026-07-26 on linux; 30 servers captured, 11 unavailable._
9
+ _Last generated: 2026-07-28 on linux; 30 servers captured, 11 unavailable._
10
10
 
11
11
  ## Diagnostic mode + navigation/edit operations
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.8.72",
3
+ "version": "3.8.73",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsc --project tsconfig.build.json",
17
- "build:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npx --yes -p typescript@6 tsc --project tsconfig.dist.json --noCheck && npm run bundle:dist",
17
+ "build:dist": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npx --yes -p typescript@7.0.2 tsc --project tsconfig.dist.json --noCheck && npm run bundle:dist",
18
18
  "bundle:dist": "node scripts/bundle-dist.mjs",
19
19
  "prepare": "npm run build:dist && node scripts/download-grammars.js --core --dest grammars",
20
20
  "lint": "tsc --project tsconfig.json",
@@ -82,10 +82,10 @@
82
82
  "banner.png"
83
83
  ],
84
84
  "dependencies": {
85
- "@ast-grep/cli": "^0.44.0",
86
- "@ast-grep/napi": "^0.44.0",
87
- "@earendil-works/pi-tui": "^0.80.2",
88
- "js-yaml": "^4.1.1",
85
+ "@ast-grep/cli": "^0.45.0",
86
+ "@ast-grep/napi": "^0.45.0",
87
+ "@earendil-works/pi-tui": "^0.82.1",
88
+ "js-yaml": "^5.2.2",
89
89
  "minimatch": "^10.2.5",
90
90
  "pidusage": "^4.0.1",
91
91
  "typebox": "^1.0.0",
@@ -104,12 +104,11 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "@biomejs/biome": "^2.4.10",
107
- "@earendil-works/pi-coding-agent": "^0.80.2",
108
- "@types/js-yaml": "^4.0.9",
107
+ "@earendil-works/pi-coding-agent": "^0.82.1",
109
108
  "@types/node": "^26.0.0",
110
109
  "@types/pidusage": "^2.0.5",
111
110
  "@vitest/coverage-v8": "^4.1.5",
112
- "typescript": "^6.0.3",
111
+ "typescript": "^7.0.2",
113
112
  "typescript-language-server": "^5.1.3",
114
113
  "vitest": "^4.1.1"
115
114
  },
@@ -4,6 +4,7 @@ id: no-equality-in-for-condition
4
4
  name: Loose equality in for-loop condition
5
5
  severity: warning
6
6
  category: bug
7
+ defect_class: correctness
7
8
  inline_tier: warning
8
9
  language: typescript
9
10
 
@@ -4,6 +4,7 @@ id: no-jump-in-finally
4
4
  name: Control-flow jump in finally block
5
5
  severity: warning
6
6
  category: bug
7
+ defect_class: silent-error
7
8
  inline_tier: warning
8
9
  language: typescript
9
10