pi-lens 4.1.5 → 4.2.0
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 +415 -0
- package/README.md +13 -8
- package/config/dependency-cruiser-eager-allowlist.json +6 -1
- package/dist/clients/actionable-warnings.js +30 -4
- package/dist/clients/agent-behavior-client.js +2 -0
- package/dist/clients/analysed-root.js +1 -0
- package/dist/clients/ast-grep-tool-logger.js +1 -1
- package/dist/clients/bash-file-access.js +26 -0
- package/dist/clients/biome-client.js +13 -2
- package/dist/clients/blocker-freshness.js +403 -35
- package/dist/clients/bootstrap.js +6 -1
- package/dist/clients/bounded-telemetry.js +2 -0
- package/dist/clients/cache-observability.js +122 -1
- package/dist/clients/cascade-format.js +200 -4
- package/dist/clients/code-quality-warnings.js +10 -2
- package/dist/clients/complexity-client.js +5 -0
- package/dist/clients/config-diagnostic-codes.js +2 -0
- package/dist/clients/config-resolve.js +6 -3
- package/dist/clients/config-schema.js +17 -0
- package/dist/clients/config-warn.js +2 -2
- package/dist/clients/dead-code-client.js +8 -1
- package/dist/clients/degradation-ledger.js +16 -1
- package/dist/clients/dependency-checker.js +5 -1
- package/dist/clients/diagnostic-dispositions.js +12 -1
- package/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/dist/clients/dispatch/dispatcher.js +47 -17
- package/dist/clients/dispatch/facts/function-facts.js +1 -1
- package/dist/clients/dispatch/facts/import-facts.js +5 -1
- package/dist/clients/dispatch/finding-policy.js +222 -0
- package/dist/clients/dispatch/indent-detect.js +389 -13
- package/dist/clients/dispatch/integration.js +175 -25
- package/dist/clients/dispatch/rule-ignores.js +62 -0
- package/dist/clients/dispatch/runners/actionlint.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/dist/clients/dispatch/runners/biome-check.js +8 -4
- package/dist/clients/dispatch/runners/cpp-check.js +4 -3
- package/dist/clients/dispatch/runners/credo.js +2 -1
- package/dist/clients/dispatch/runners/cue-vet.js +2 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +2 -1
- package/dist/clients/dispatch/runners/detekt.js +2 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +2 -1
- package/dist/clients/dispatch/runners/elixir-check.js +2 -1
- package/dist/clients/dispatch/runners/eslint.js +2 -1
- package/dist/clients/dispatch/runners/fish-indent.js +2 -1
- package/dist/clients/dispatch/runners/gleam-check.js +2 -1
- package/dist/clients/dispatch/runners/go-vet.js +2 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +2 -1
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +3 -1
- package/dist/clients/dispatch/runners/helm-render.js +2 -1
- package/dist/clients/dispatch/runners/htmlhint.js +2 -1
- package/dist/clients/dispatch/runners/javac.js +2 -1
- package/dist/clients/dispatch/runners/ktlint.js +2 -1
- package/dist/clients/dispatch/runners/lsp.js +19 -1
- package/dist/clients/dispatch/runners/markdownlint.js +3 -2
- package/dist/clients/dispatch/runners/mypy.js +2 -1
- package/dist/clients/dispatch/runners/oxlint.js +2 -1
- package/dist/clients/dispatch/runners/php-lint.js +2 -1
- package/dist/clients/dispatch/runners/phpstan.js +2 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +2 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
- package/dist/clients/dispatch/runners/pyright.js +2 -1
- package/dist/clients/dispatch/runners/rubocop.js +2 -1
- package/dist/clients/dispatch/runners/ruff.js +2 -2
- package/dist/clients/dispatch/runners/rust-clippy.js +12 -11
- package/dist/clients/dispatch/runners/shellcheck.js +2 -1
- package/dist/clients/dispatch/runners/shfmt.js +4 -13
- package/dist/clients/dispatch/runners/spellcheck.js +2 -2
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +3 -2
- package/dist/clients/dispatch/runners/stylelint.js +3 -2
- package/dist/clients/dispatch/runners/swiftlint.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +2 -1
- package/dist/clients/dispatch/runners/tflint.js +2 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/dist/clients/dispatch/runners/trivy-config.js +2 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
- package/dist/clients/dispatch/runners/vale.js +2 -1
- package/dist/clients/dispatch/runners/yamllint.js +4 -3
- package/dist/clients/dispatch/runners/zig-check.js +2 -1
- package/dist/clients/dispatch/types.js +4 -1
- package/dist/clients/disposition-publish.js +1 -0
- package/dist/clients/effective-config.js +12 -2
- package/dist/clients/extension-log.js +2 -0
- package/dist/clients/file-kinds.js +26 -1
- package/dist/clients/file-role.js +23 -6
- package/dist/clients/file-utils.js +156 -10
- package/dist/clients/finding-delivery-gate.js +60 -9
- package/dist/clients/format-service.js +39 -21
- package/dist/clients/formatters.js +113 -200
- package/dist/clients/generation-guard.js +6 -2
- package/dist/clients/git-guard.js +11 -3
- package/dist/clients/gitleaks-client.js +42 -6
- package/dist/clients/govulncheck-client.js +48 -7
- package/dist/clients/indent-retarget.js +78 -8
- package/dist/clients/installer/index.js +524 -161
- package/dist/clients/instance-reaper-state.js +7 -0
- package/dist/clients/instance-reaper.js +20 -10
- package/dist/clients/jscpd-client.js +9 -1
- package/dist/clients/knip-client.js +9 -2
- package/dist/clients/language-profile.js +35 -3
- package/dist/clients/language-registry.js +5 -1
- package/dist/clients/latency-logger.js +6 -1
- package/dist/clients/lens-config.js +4 -0
- package/dist/clients/lens-engine.js +12 -3
- package/dist/clients/lens-events.js +2 -1
- package/dist/clients/lens-flag-registry.js +79 -0
- package/dist/clients/live-bus-emitter.js +2 -1
- package/dist/clients/lsp/client.js +25 -21
- package/dist/clients/lsp/config.js +36 -7
- package/dist/clients/lsp/diagnostic-binding.js +24 -1
- package/dist/clients/lsp/document-drift.js +78 -0
- package/dist/clients/lsp/index.js +288 -45
- package/dist/clients/lsp/launch.js +16 -4
- package/dist/clients/lsp/server.js +79 -15
- package/dist/clients/lsp/workspace-diagnostics-cache.js +39 -5
- package/dist/clients/lsp-mutation.js +5 -3
- package/dist/clients/map-with-concurrency.js +12 -8
- package/dist/clients/mcp/analyze.js +3 -0
- package/dist/clients/mcp/session.js +38 -4
- package/dist/clients/ndjson-logger.js +24 -7
- package/dist/clients/observed-mutation.js +40 -2
- package/dist/clients/opaque-mutation-scan.js +23 -3
- package/dist/clients/opengrep-client.js +111 -14
- package/dist/clients/package-manager.js +2 -1
- package/dist/clients/path-utils.js +123 -3
- package/dist/clients/performance-report.js +5 -1
- package/dist/clients/php-cs-fixer-config.js +8 -3
- package/dist/clients/pipeline.js +77 -7
- package/dist/clients/project-diagnostics/cache.js +37 -5
- package/dist/clients/project-diagnostics/fresh-fetch.js +173 -18
- package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
- package/dist/clients/project-diagnostics/scanner.js +59 -3
- package/dist/clients/project-lens-config.js +48 -2
- package/dist/clients/read-guard-logger.js +2 -0
- package/dist/clients/read-guard.js +70 -11
- package/dist/clients/resource-sampler.js +147 -9
- package/dist/clients/review-graph-logger.js +2 -0
- package/dist/clients/ruff-client.js +18 -9
- package/dist/clients/runtime-agent-end.js +42 -5
- package/dist/clients/runtime-context.js +6 -2
- package/dist/clients/runtime-coordinator.js +82 -7
- package/dist/clients/runtime-session.js +33 -5
- package/dist/clients/runtime-tool-call.js +43 -12
- package/dist/clients/runtime-tool-result.js +334 -86
- package/dist/clients/runtime-turn.js +195 -18
- package/dist/clients/safe-spawn.js +258 -5
- package/dist/clients/scratch-tree-policy.js +6 -0
- package/dist/clients/security-scan-client.js +2 -2
- package/dist/clients/session-event-guard.js +119 -6
- package/dist/clients/sg-runner.js +25 -12
- package/dist/clients/sgconfig.js +79 -2
- package/dist/clients/situational-tool-telemetry.js +108 -0
- package/dist/clients/test-runner-client.js +258 -78
- package/dist/clients/test-runner-delivery.js +43 -0
- package/dist/clients/tool-agreement.js +423 -0
- package/dist/clients/tool-config.js +249 -0
- package/dist/clients/tool-cwd.js +270 -0
- package/dist/clients/tool-policy.js +163 -7
- package/dist/clients/tool-probe.js +53 -0
- package/dist/clients/tool-set-policy.js +42 -0
- package/dist/clients/tree-sitter-client.js +120 -0
- package/dist/clients/tree-sitter-query-loader.js +127 -189
- package/dist/clients/trivy-client.js +9 -1
- package/dist/clients/turn-context.js +52 -0
- package/dist/clients/widget-state.js +102 -3
- package/dist/clients/word-index.js +5 -2
- package/dist/clients/write-ordering-guard.js +7 -0
- package/dist/index.js +26968 -21741
- package/dist/mcp/analyze-cli.js +3 -2
- package/dist/mcp/server.js +140 -161
- package/dist/tools/activate-tools.js +10 -7
- package/dist/tools/ast-grep-outline.js +2 -13
- package/dist/tools/ast-grep-replace.js +8 -13
- package/dist/tools/ast-grep-search.js +83 -39
- package/dist/tools/effective-config.js +4 -3
- package/dist/tools/lens-diagnostic-mark.js +8 -11
- package/dist/tools/lens-diagnostics.js +516 -133
- package/dist/tools/lsp-diagnostics.js +265 -182
- package/dist/tools/lsp-navigation.js +20 -44
- package/dist/tools/module-report.js +5 -9
- package/dist/tools/project-report.js +9 -17
- package/dist/tools/render-compact.js +280 -1
- package/dist/tools/symbol-search.js +2 -2
- package/docs/agent-guide.md +3 -4
- package/docs/agent-tools.md +46 -22
- package/docs/audit1.md +5 -5
- package/docs/configuration.md +9 -0
- package/docs/dependencies.md +11 -0
- package/docs/dispositions.md +52 -0
- package/docs/environment-variables.md +2 -7
- package/docs/features.md +7 -7
- package/docs/globalconfig.md +14 -2
- package/docs/language-coverage.md +1 -0
- package/docs/mcp.md +13 -1
- package/docs/pi-lens-fixer.md +48 -18
- package/docs/pi-lens-investigator.md +38 -18
- package/docs/pi-lens-monitor.md +88 -0
- package/docs/pi-lens-retro.md +97 -0
- package/docs/pi-lens-reviewer.md +53 -31
- package/docs/pi-lens-subagent.md +12 -4
- package/docs/public-api-stability.md +1 -0
- package/docs/real-harness.md +46 -0
- package/docs/release-qa-baseline.md +27 -1
- package/docs/servercapabilities.md +1 -6
- package/docs/settings.md +44 -6
- package/docs/tools_improvement2.md +4 -4
- package/docs/usage.md +15 -3
- package/package.json +10 -1
- package/rules/tree-sitter-queries/typescript/sql-injection.yml +23 -7
- package/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/skills/pi-lens-ast-grep/SKILL.md +23 -6
- package/skills/pi-lens-lsp-navigation/SKILL.md +26 -9
- package/dist/tools/ast-dump.js +0 -103
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { Type } from "../clients/deps/typebox.js";
|
|
10
9
|
import { getProjectIgnoreMatcher, isExcludedDirName, } from "../clients/file-utils.js";
|
|
11
10
|
import { getLSPService, groupFilesByPrimaryServer, runPerServerGroups, } from "../clients/lsp/index.js";
|
|
12
11
|
import { buildScopeKey, createWorkspaceDiagnosticsCacheContext, } from "../clients/lsp/workspace-diagnostics-cache.js";
|
|
13
12
|
import { getServersForFileWithConfig, primaryServerId, } from "../clients/lsp/config.js";
|
|
13
|
+
import { mapWithConcurrency } from "../clients/map-with-concurrency.js";
|
|
14
14
|
import { combineAbortSignals, withDeadline, } from "../clients/deadline-utils.js";
|
|
15
15
|
import { applyAuxiliarySuppressions, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
|
|
16
|
+
import { applyLspFindingPolicy, loadProjectRulePolicyMap, } from "../clients/dispatch/finding-policy.js";
|
|
17
|
+
import { hasStrictDispositionMarks } from "../clients/diagnostic-dispositions.js";
|
|
18
|
+
import { recordDegradationOnce } from "../clients/degradation-ledger.js";
|
|
19
|
+
import { logLatency } from "../clients/latency-logger.js";
|
|
16
20
|
import { detectFileRole } from "../clients/file-role.js";
|
|
17
21
|
import { hashDiagnosticContent, touchCompletedConfirmationPolicy, touchCoverageGap, } from "../clients/lsp/diagnostic-binding.js";
|
|
18
22
|
import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
|
|
@@ -20,23 +24,26 @@ import { attemptTsserverSyncDiagnostics } from "../clients/lsp/tsserver-sync.js"
|
|
|
20
24
|
import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
|
|
21
25
|
import { demoteInferredProjectDiagnostics } from "../clients/lsp/inferred-project.js";
|
|
22
26
|
import { isBlocking, reconcileScanDiagnostics, } from "../clients/widget-state.js";
|
|
23
|
-
import {
|
|
24
|
-
import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
|
|
27
|
+
import { makeProgressReporter } from "./scan-progress.js";
|
|
25
28
|
import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
|
|
26
29
|
import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
|
|
27
30
|
const MAX_FILES = 100;
|
|
28
|
-
const MAX_BATCH_FILES = 100;
|
|
31
|
+
export const MAX_BATCH_FILES = 100;
|
|
29
32
|
const MAX_DIAGNOSTICS = 200;
|
|
30
33
|
const DEFAULT_BATCH_CONCURRENCY = 8;
|
|
31
34
|
const MAX_BATCH_CONCURRENCY = 16;
|
|
32
35
|
const DEFAULT_BATCH_FILE_DEADLINE_MS = 15_000;
|
|
33
36
|
// LSP severities: 1=Error, 2=Warning, 3=Information, 4=Hint
|
|
34
|
-
const SEVERITY_NAMES = {
|
|
37
|
+
export const SEVERITY_NAMES = {
|
|
35
38
|
1: "error",
|
|
36
39
|
2: "warning",
|
|
37
40
|
3: "information",
|
|
38
41
|
4: "hint",
|
|
39
42
|
};
|
|
43
|
+
export const LSP_SEVERITY_FILTERS = [
|
|
44
|
+
...Object.values(SEVERITY_NAMES),
|
|
45
|
+
"all",
|
|
46
|
+
];
|
|
40
47
|
function batchFileDeadlineMs() {
|
|
41
48
|
const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
|
|
42
49
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
|
|
@@ -98,46 +105,6 @@ function boundedPositiveInt(value, fallback, min, max) {
|
|
|
98
105
|
* for THIS tool's batch/directory sweep the same way #667 fixed it for the
|
|
99
106
|
* workspace-diagnostics sweep.
|
|
100
107
|
*/
|
|
101
|
-
async function mapWithConcurrency(items, concurrency, mapper, lspService, signal, onProgress) {
|
|
102
|
-
const results = [];
|
|
103
|
-
let completed = 0;
|
|
104
|
-
// Multiple original indices can map to the same file path (duplicate
|
|
105
|
-
// entries in an explicit `paths` batch) — track them as a per-file queue
|
|
106
|
-
// so each occurrence still lands in its own original slot.
|
|
107
|
-
const pendingIndices = new Map();
|
|
108
|
-
items.forEach((item, index) => {
|
|
109
|
-
const queue = pendingIndices.get(item);
|
|
110
|
-
if (queue)
|
|
111
|
-
queue.push(index);
|
|
112
|
-
else
|
|
113
|
-
pendingIndices.set(item, [index]);
|
|
114
|
-
});
|
|
115
|
-
const groups = groupFilesByPrimaryServer(items);
|
|
116
|
-
await runPerServerGroups(groups, concurrency, async (group) => {
|
|
117
|
-
if (signal?.aborted)
|
|
118
|
-
return;
|
|
119
|
-
const first = group.files[0];
|
|
120
|
-
if (first &&
|
|
121
|
-
lspService &&
|
|
122
|
-
!isWarmAttached() &&
|
|
123
|
-
typeof lspService.ensureWarmForSweep === "function") {
|
|
124
|
-
await lspService.ensureWarmForSweep(first, { signal });
|
|
125
|
-
if (signal?.aborted)
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
for (const item of group.files) {
|
|
129
|
-
// Honor cancellation (Escape / turn abort): stop pulling new items
|
|
130
|
-
// rather than grind the whole batch. Completed entries are returned.
|
|
131
|
-
if (signal?.aborted)
|
|
132
|
-
return;
|
|
133
|
-
const index = pendingIndices.get(item).shift();
|
|
134
|
-
results[index] = await mapper(item, index);
|
|
135
|
-
completed += 1;
|
|
136
|
-
onProgress?.(completed, items.length);
|
|
137
|
-
}
|
|
138
|
-
}, signal);
|
|
139
|
-
return results;
|
|
140
|
-
}
|
|
141
108
|
/**
|
|
142
109
|
* Project-ignore predicate rooted at `root`, fail-open. Lets a directory scan
|
|
143
110
|
* honor the user's `.pi-lens.json` / `.gitignore` patterns — not just the
|
|
@@ -216,102 +183,8 @@ nextWriteIndex,
|
|
|
216
183
|
// injecting "Unresolved from this turn" for three more turns after answering
|
|
217
184
|
// "confirmed clean" for the same file. index.ts injects
|
|
218
185
|
// `runtime.retireInlineBlockerOnConfirmedClean`. Optional/undefined in tests.
|
|
219
|
-
onConfirmedNoBlockers) {
|
|
186
|
+
onConfirmedNoBlockers, getService = getLSPService) {
|
|
220
187
|
return {
|
|
221
|
-
name: "lsp_diagnostics",
|
|
222
|
-
label: "LSP Diagnostics",
|
|
223
|
-
description: "Get errors, warnings, and hints from language servers for a file or directory. " +
|
|
224
|
-
"Use BEFORE running builds to proactively check for issues. " +
|
|
225
|
-
"Works on directories by auto-detecting file extensions and scanning all matching files.",
|
|
226
|
-
promptSnippet: "Get LSP diagnostics for a file or directory (use before builds)",
|
|
227
|
-
renderResult: compactRenderResult(({ details, args, isError, text }) => {
|
|
228
|
-
// Streaming progress partials render the live bar (see scanningSummaryLine).
|
|
229
|
-
const scanning = scanningSummaryLine(details, text);
|
|
230
|
-
if (scanning)
|
|
231
|
-
return scanning;
|
|
232
|
-
if (isError) {
|
|
233
|
-
return `lsp_diagnostics — ${text.split("\n")[0] ?? "error"}`;
|
|
234
|
-
}
|
|
235
|
-
const count = details?.totalDiagnostics ?? details?.diagnostics?.length ?? 0;
|
|
236
|
-
const target = baseName(details?.filePath ?? args.path) || "workspace";
|
|
237
|
-
const files = details?.filesChecked ?? details?.filesScanned;
|
|
238
|
-
const scope = typeof files === "number" && files > 1
|
|
239
|
-
? ` across ${files} files`
|
|
240
|
-
: target
|
|
241
|
-
? ` ${target}`
|
|
242
|
-
: "";
|
|
243
|
-
const noun = count === 1 ? "diagnostic" : "diagnostics";
|
|
244
|
-
// #533: a batch/directory result with any unconfirmed files must NEVER
|
|
245
|
-
// compact-render as a bare "N diagnostics" — that erases the fact some
|
|
246
|
-
// files' clean status was never actually confirmed by the server.
|
|
247
|
-
const unconfirmedFiles = details?.unconfirmedFiles ?? 0;
|
|
248
|
-
const navigationOnlyFiles = details?.navigationOnlyFiles ?? 0;
|
|
249
|
-
if (navigationOnlyFiles > 0) {
|
|
250
|
-
return `lsp_diagnostics${scope} — ${count} ${noun} · ${details?.cleanFiles ?? 0} clean · ${navigationOnlyFiles} navigation-only`;
|
|
251
|
-
}
|
|
252
|
-
if (unconfirmedFiles > 0) {
|
|
253
|
-
const cleanFiles = details?.cleanFiles ?? 0;
|
|
254
|
-
const timedOutFiles = details?.timedOutFiles ?? 0;
|
|
255
|
-
const suffix = timedOutFiles > 0 ? ` (${timedOutFiles} timed out)` : "";
|
|
256
|
-
return `lsp_diagnostics${scope} — ${count} ${noun} · ${cleanFiles} clean · ${unconfirmedFiles} unconfirmed${suffix}`;
|
|
257
|
-
}
|
|
258
|
-
const outcomeCounts = details?.outcomeCounts;
|
|
259
|
-
const notConfirmed = outcomeCounts
|
|
260
|
-
? (outcomeCounts.inconclusive ?? 0) +
|
|
261
|
-
(outcomeCounts.unavailable ?? 0) +
|
|
262
|
-
(outcomeCounts.unsupported ?? 0) +
|
|
263
|
-
(outcomeCounts.failed ?? 0)
|
|
264
|
-
: 0;
|
|
265
|
-
if (notConfirmed > 0) {
|
|
266
|
-
return `lsp_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
|
|
267
|
-
}
|
|
268
|
-
if ((details?.incompleteFiles ?? 0) > 0) {
|
|
269
|
-
return `lsp_diagnostics${scope} — incomplete (${details?.incompleteFiles} files not confirmed)`;
|
|
270
|
-
}
|
|
271
|
-
// Single-file mode: 0 diagnostics from an unconfirmed result — either a
|
|
272
|
-
// silent-on-clean server or (#570) a timed-out check — is not a clean
|
|
273
|
-
// render either.
|
|
274
|
-
if (count === 0 && details?.unconfirmed) {
|
|
275
|
-
return details?.timedOut
|
|
276
|
-
? `lsp_diagnostics${scope} — timed out (result may be incomplete)`
|
|
277
|
-
: `lsp_diagnostics${scope} — unconfirmed (server cannot confirm clean)`;
|
|
278
|
-
}
|
|
279
|
-
return `lsp_diagnostics${scope} — ${count} ${noun}`;
|
|
280
|
-
}),
|
|
281
|
-
parameters: Type.Object({
|
|
282
|
-
path: Type.Optional(Type.String({
|
|
283
|
-
description: "File or directory path to check. For directories, all matching source files are scanned.",
|
|
284
|
-
})),
|
|
285
|
-
paths: Type.Optional(Type.Array(Type.String(), {
|
|
286
|
-
minItems: 1,
|
|
287
|
-
maxItems: MAX_BATCH_FILES,
|
|
288
|
-
description: "Explicit files to check as a bounded-concurrency batch. When provided, path is ignored.",
|
|
289
|
-
})),
|
|
290
|
-
severity: Type.Optional(Type.String({
|
|
291
|
-
enum: ["error", "warning", "information", "hint", "all"],
|
|
292
|
-
description: "Filter by severity level (default: all)",
|
|
293
|
-
})),
|
|
294
|
-
concurrency: Type.Optional(Type.Number({
|
|
295
|
-
description: "Batch/directory concurrency, in distinct LSP server groups run in parallel " +
|
|
296
|
-
"(default 8, max 16) — not individual files. Files sharing one server " +
|
|
297
|
-
"(e.g. a same-language batch) are always processed one at a time against " +
|
|
298
|
-
"that server regardless of this value; this caps how many DIFFERENT " +
|
|
299
|
-
"servers run concurrently.",
|
|
300
|
-
})),
|
|
301
|
-
waitMs: Type.Optional(Type.Number({
|
|
302
|
-
description: "Optional per-file LSP wait budget for batch diagnostics. Uses server defaults when omitted.",
|
|
303
|
-
})),
|
|
304
|
-
serverScope: Type.Optional(Type.String({
|
|
305
|
-
enum: ["primary", "all"],
|
|
306
|
-
description: "'primary' (fast, low-noise): only the file's actual language " +
|
|
307
|
-
"server (e.g. typescript) — for 'does this have real type " +
|
|
308
|
-
"errors'. 'all' (default): also touches cross-cutting auxiliary " +
|
|
309
|
-
"scanners (ast-grep, opengrep, zizmor, typos, marksman) attached " +
|
|
310
|
-
"to this file, including findings for files not yet dispatched " +
|
|
311
|
-
"this session. Primary confirmation is always reported " +
|
|
312
|
-
"separately from auxiliary findings regardless of this setting.",
|
|
313
|
-
})),
|
|
314
|
-
}),
|
|
315
188
|
async execute(_toolCallId, params, _signal, onUpdate, ctx) {
|
|
316
189
|
// Escape aborts the turn via ctx.signal; honor both it and the tool-call
|
|
317
190
|
// signal so a batch/directory scan cancels rather than grinding on.
|
|
@@ -327,7 +200,7 @@ onConfirmedNoBlockers) {
|
|
|
327
200
|
? Math.floor(typedParams.waitMs)
|
|
328
201
|
: undefined;
|
|
329
202
|
const serverScope = typedParams.serverScope === "primary" ? "primary" : "all";
|
|
330
|
-
const lspService =
|
|
203
|
+
const lspService = getService();
|
|
331
204
|
if (!lspService) {
|
|
332
205
|
return {
|
|
333
206
|
content: [
|
|
@@ -423,7 +296,17 @@ onConfirmedNoBlockers) {
|
|
|
423
296
|
},
|
|
424
297
|
};
|
|
425
298
|
}
|
|
426
|
-
async function collectDiagnosticsForFile(absPath, lspService,
|
|
299
|
+
async function collectDiagnosticsForFile(absPath, lspService,
|
|
300
|
+
/**
|
|
301
|
+
* #3041: project root the per-rule `ignores` globs (#965) are resolved
|
|
302
|
+
* against. ast-grep's LSP publishes per-document diagnostics without
|
|
303
|
+
* applying them, so this standalone `source=lsp` query has to — the same
|
|
304
|
+
* carve-out the NAPI runner applies on the same file. Positioned before the
|
|
305
|
+
* optional parameters so it is structurally REQUIRED: an optional
|
|
306
|
+
* `string | undefined` would silently disable the carve-out for whichever
|
|
307
|
+
* caller forgot to pass it.
|
|
308
|
+
*/
|
|
309
|
+
scanRoot, waitMs, serverScope = "all") {
|
|
427
310
|
let timedOut = false;
|
|
428
311
|
let content;
|
|
429
312
|
// `touchFile` is the authoritative collection boundary for every scope: it
|
|
@@ -450,9 +333,13 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
|
|
|
450
333
|
const attached = await tryWarmAttachedDiagnostics(absPath, content, attachedBudgetMs, "sweep");
|
|
451
334
|
if (attached?.available) {
|
|
452
335
|
const scopedDiagnostics = serverScope === "primary"
|
|
453
|
-
? attached.response.diagnostics.filter((item) => item.
|
|
336
|
+
? attached.response.diagnostics.filter((item) => item.serverId === primaryServerId(absPath))
|
|
454
337
|
: attached.response.diagnostics;
|
|
455
|
-
const filtered = applyAuxiliarySuppressions(scopedDiagnostics, content, {
|
|
338
|
+
const filtered = applyAuxiliarySuppressions(scopedDiagnostics, content, {
|
|
339
|
+
fileRole: detectFileRole(absPath, content),
|
|
340
|
+
filePath: absPath,
|
|
341
|
+
scanRoot,
|
|
342
|
+
});
|
|
456
343
|
return {
|
|
457
344
|
diagnostics: filtered,
|
|
458
345
|
timedOut: false,
|
|
@@ -512,6 +399,8 @@ async function collectDiagnosticsForFile(absPath, lspService, waitMs, serverScop
|
|
|
512
399
|
const filtered = content !== undefined
|
|
513
400
|
? applyAuxiliarySuppressions(diagnostics, content, {
|
|
514
401
|
fileRole: detectFileRole(absPath, content),
|
|
402
|
+
filePath: absPath,
|
|
403
|
+
scanRoot,
|
|
515
404
|
})
|
|
516
405
|
: diagnostics;
|
|
517
406
|
// #1095: surface the touch's content binding (only a touch that resolved
|
|
@@ -546,6 +435,7 @@ function diagnosticsToFileDiags(file, diagnostics) {
|
|
|
546
435
|
severity: d.severity,
|
|
547
436
|
message: d.message,
|
|
548
437
|
source: d.source,
|
|
438
|
+
serverId: d.serverId,
|
|
549
439
|
code: d.code,
|
|
550
440
|
}));
|
|
551
441
|
}
|
|
@@ -679,6 +569,97 @@ function demoteInferredForFile(file, diagnostics, cwd, lspService) {
|
|
|
679
569
|
service: lspService,
|
|
680
570
|
});
|
|
681
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* #3088: the probe lane's stored-disposition / rule-policy / inline-suppression
|
|
574
|
+
* gate — the same `clients/dispatch/finding-policy.ts` stack `mode=full` and
|
|
575
|
+
* the per-edit dispatcher apply, called once per file at the position
|
|
576
|
+
* `demoteInferredForFile` already established: AFTER the confirmation verdict
|
|
577
|
+
* (which asks "did the server answer for this content" and must not be
|
|
578
|
+
* influenced by a policy drop) and BEFORE the rendered output, the severity
|
|
579
|
+
* filter and `reconcileWidgetFromLspResult`. One filtered set for all three, so
|
|
580
|
+
* the text, the counts and the widget footer cannot disagree — and so the
|
|
581
|
+
* footer reconcile can no longer re-arm a finding the mark-time
|
|
582
|
+
* `reconcileWidgetDisposition` demoted.
|
|
583
|
+
*
|
|
584
|
+
* Fails OPEN: a filter that throws returns the unfiltered set plus one bounded
|
|
585
|
+
* degradation record. A finding that stays visible is recoverable; one silently
|
|
586
|
+
* hidden by a broken filter is the harm this lane exists to prevent
|
|
587
|
+
* (AGENTS.md shape 48).
|
|
588
|
+
*/
|
|
589
|
+
function applyProbeFindingPolicy(file, diagnostics, cwd, content,
|
|
590
|
+
/**
|
|
591
|
+
* Round 2 F1: the caller's severity threshold. The count reported to the
|
|
592
|
+
* agent must be scoped to the SAME threshold the rendered result is, or a
|
|
593
|
+
* `severity: "error"` probe of a repo carrying warning-level marks prints
|
|
594
|
+
* "suppressed by disposition: N … Not a clean verdict" on a result that IS
|
|
595
|
+
* clean at that threshold — the skill's own folder-check recipe would emit
|
|
596
|
+
* the banner on every check. The threshold is applied HERE, in the tool that
|
|
597
|
+
* owns both the render and the record, rather than in the three call sites
|
|
598
|
+
* (three copies of one subtraction) or in
|
|
599
|
+
* `clients/dispatch/finding-policy.ts`, which must stay threshold-free: its
|
|
600
|
+
* other caller (`mode=full`) has no severity axis at all.
|
|
601
|
+
*/
|
|
602
|
+
severity) {
|
|
603
|
+
const started = Date.now();
|
|
604
|
+
try {
|
|
605
|
+
const result = applyLspFindingPolicy(diagnostics, {
|
|
606
|
+
cwd,
|
|
607
|
+
filePath: file,
|
|
608
|
+
content,
|
|
609
|
+
policyMap: loadProjectRulePolicyMap(cwd),
|
|
610
|
+
fileRole: detectFileRole(file, content),
|
|
611
|
+
});
|
|
612
|
+
const inScopeBefore = applySeverityFilter(diagnostics, severity).length;
|
|
613
|
+
const suppressed = inScopeBefore - applySeverityFilter(result.kept, severity).length;
|
|
614
|
+
if (suppressed > 0) {
|
|
615
|
+
// One record per FILE that actually dropped something the caller would
|
|
616
|
+
// otherwise have SEEN, never one per finding (AGENTS.md "bounded
|
|
617
|
+
// observability"). `total` is the in-scope population for the same
|
|
618
|
+
// reason the count is, so the record and the rendered line agree.
|
|
619
|
+
logLatency({
|
|
620
|
+
type: "phase",
|
|
621
|
+
toolName: "lsp_diagnostics",
|
|
622
|
+
filePath: file,
|
|
623
|
+
phase: "lsp_probe_disposition_filter",
|
|
624
|
+
durationMs: Date.now() - started,
|
|
625
|
+
metadata: {
|
|
626
|
+
suppressed,
|
|
627
|
+
total: inScopeBefore,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return { kept: result.kept, inlineKept: result.inlineKept, suppressed };
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
recordDegradationOnce({
|
|
635
|
+
kind: "lsp-probe-finding-policy",
|
|
636
|
+
subject: cwd,
|
|
637
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
638
|
+
});
|
|
639
|
+
return { kept: diagnostics, inlineKept: diagnostics, suppressed: 0 };
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* #3088 AC5/AC7: the content a cache REPLAY needs to evaluate STRICT
|
|
644
|
+
* (`false-positive`) disposition anchors, which hash the finding's own line.
|
|
645
|
+
* A cache hit means the file is unchanged since the entry was recorded, so this
|
|
646
|
+
* is the same content the recording touch read — but it is a disk read the
|
|
647
|
+
* replay path exists to avoid, so it is paid ONLY when the store actually holds
|
|
648
|
+
* a strict mark. With no marks (the overwhelmingly common case) the probe path
|
|
649
|
+
* adds zero I/O and the weak-anchored marks apply without content at all.
|
|
650
|
+
*/
|
|
651
|
+
function replayContentForStrictMarks(file, cwd) {
|
|
652
|
+
if (!hasStrictDispositionMarks(cwd))
|
|
653
|
+
return undefined;
|
|
654
|
+
try {
|
|
655
|
+
return fs.readFileSync(file, "utf-8");
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
// Fail open — an unreadable file degrades to the weak-anchored filter
|
|
659
|
+
// rather than hiding or dropping anything.
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
682
663
|
function reconcileWidgetFromLspResult(file, rawDiags, confirmation, writeIndex, cwd, content,
|
|
683
664
|
// #1095: true when the result's content binding demonstrably mismatches disk
|
|
684
665
|
// (boundToCurrentDisk === false). Such a result — even a NON-EMPTY one, which
|
|
@@ -704,7 +685,9 @@ observedAt) {
|
|
|
704
685
|
scanOrigin: "lsp_diagnostics",
|
|
705
686
|
});
|
|
706
687
|
const retagged = retagAuxiliaryDiagnostics(diagnostics, rawDiags, content ?? "", { cwd, fileRole: detectFileRole(file, content) });
|
|
707
|
-
reconcileScanDiagnostics(file, retagged, true, writeIndex, observedAt);
|
|
688
|
+
const reconciled = reconcileScanDiagnostics(file, retagged, true, writeIndex, observedAt);
|
|
689
|
+
if (!reconciled)
|
|
690
|
+
return { confirmed: false, blocking: true };
|
|
708
691
|
// #1561: the blocking tally comes from the SAME `isBlocking` predicate the
|
|
709
692
|
// footer counts with — no second severity rule for the blocker-retire
|
|
710
693
|
// decision to drift away from.
|
|
@@ -759,7 +742,13 @@ onConfirmedNoBlockers) {
|
|
|
759
742
|
// fresh touch does, so it gets the same demotion. Without this, replaying
|
|
760
743
|
// yesterday's cached blocker would re-cement an orphan file as blocking
|
|
761
744
|
// after mode=full had demoted it.
|
|
762
|
-
const
|
|
745
|
+
const demotedCached = await demoteInferredForFile(file, cached.diagnostics, cwd, lspService);
|
|
746
|
+
// #3088 AC5: a replay is served to the agent exactly like a fresh
|
|
747
|
+
// observation, so it passes the same filter. The cached set is already
|
|
748
|
+
// inline-suppression-clean (the record below stores `inlineKept`), so
|
|
749
|
+
// only the revocable store/config filters run here.
|
|
750
|
+
const cachedPolicy = applyProbeFindingPolicy(file, demotedCached, cwd, replayContentForStrictMarks(file, cwd), severity);
|
|
751
|
+
const cachedDiags = cachedPolicy.kept;
|
|
763
752
|
const filteredDiags = applySeverityFilter(cachedDiags, severity);
|
|
764
753
|
// #692: cached.diagnostics were already suppression-/skipTestFiles-
|
|
765
754
|
// filtered at write time (this same code path); no file content was
|
|
@@ -780,10 +769,13 @@ onConfirmedNoBlockers) {
|
|
|
780
769
|
diagnostics: diagnosticsToFileDiags(file, filteredDiags),
|
|
781
770
|
confirmation,
|
|
782
771
|
primaryServerId: primaryServerId(file),
|
|
772
|
+
...(cachedPolicy.suppressed > 0 && {
|
|
773
|
+
dispositionSuppressed: cachedPolicy.suppressed,
|
|
774
|
+
}),
|
|
783
775
|
};
|
|
784
776
|
}
|
|
785
777
|
}
|
|
786
|
-
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(file, lspService, waitMs, serverScope);
|
|
778
|
+
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
|
|
787
779
|
const health = lspService.getDiagnosticsHealth?.(file);
|
|
788
780
|
// #570: a timed-out priming check is never a confirmed "clean" — treat it
|
|
789
781
|
// as unconfirmed without consulting the (unrelated) silent-tier
|
|
@@ -842,6 +834,9 @@ onConfirmedNoBlockers) {
|
|
|
842
834
|
}
|
|
843
835
|
// #1645 review F3: one demotion rule for every writer of the widget store.
|
|
844
836
|
effectiveRawDiags = await demoteInferredForFile(file, effectiveRawDiags, cwd, lspService);
|
|
837
|
+
// #3088: one filter for the output, the counts and the footer reconcile.
|
|
838
|
+
const policy = applyProbeFindingPolicy(file, effectiveRawDiags, cwd, collectedContent, severity);
|
|
839
|
+
effectiveRawDiags = policy.kept;
|
|
845
840
|
const filteredDiags = applySeverityFilter(effectiveRawDiags, severity);
|
|
846
841
|
const verdict = reconcileWidgetFromLspResult(file, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
|
|
847
842
|
if (verdict.confirmed && !verdict.blocking) {
|
|
@@ -864,7 +859,14 @@ onConfirmedNoBlockers) {
|
|
|
864
859
|
confirmation !== "unconfirmed" &&
|
|
865
860
|
diagnosticsUnsupportedServerIds.length === 0 &&
|
|
866
861
|
unconfirmedServerIds.length === 0) {
|
|
867
|
-
|
|
862
|
+
// #3088: the entry records the INLINE-suppressed set, never the
|
|
863
|
+
// disposition/rule-policy-filtered one. An inline `pi-lens-ignore` comment
|
|
864
|
+
// cannot change without invalidating this content-keyed entry, so baking it
|
|
865
|
+
// in is safe and makes every later replay consistent with a fresh probe; a
|
|
866
|
+
// disposition mark or a `.pi-lens.json` rule can be revoked at any moment,
|
|
867
|
+
// so caching THEIR effect would keep a finding hidden after its mark was
|
|
868
|
+
// removed. The replay branch above re-applies those two on every serve.
|
|
869
|
+
cacheCtx.record(file, scopeKey, policy.inlineKept, stat.mtimeMs, collectedContent !== undefined
|
|
868
870
|
? hashDiagnosticContent(collectedContent)
|
|
869
871
|
: undefined, stat.size);
|
|
870
872
|
}
|
|
@@ -877,6 +879,9 @@ onConfirmedNoBlockers) {
|
|
|
877
879
|
...(skipReason !== undefined && { skipReason }),
|
|
878
880
|
primaryServerId: primaryServerId(file),
|
|
879
881
|
diagnosticsUnsupported: diagnosticsUnsupportedServerIds.length > 0,
|
|
882
|
+
...(policy.suppressed > 0 && {
|
|
883
|
+
dispositionSuppressed: policy.suppressed,
|
|
884
|
+
}),
|
|
880
885
|
};
|
|
881
886
|
}
|
|
882
887
|
async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope = "all", cwd = process.cwd(),
|
|
@@ -886,7 +891,7 @@ onConfirmedNoBlockers) {
|
|
|
886
891
|
// Reserve the token before awaiting this file's LSP result. The direct-file
|
|
887
892
|
// path performs its own confirmation/reconciliation below (#1198).
|
|
888
893
|
const writeIndex = nextWriteIndex?.();
|
|
889
|
-
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, waitMs, serverScope);
|
|
894
|
+
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
|
|
890
895
|
const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
|
|
891
896
|
const unavailable = lspUnavailableMessage(absPath, lspHealth);
|
|
892
897
|
// #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
|
|
@@ -946,6 +951,9 @@ onConfirmedNoBlockers) {
|
|
|
946
951
|
confirmation = "unconfirmed";
|
|
947
952
|
// #1645 review F3: one demotion rule for every writer of the widget store.
|
|
948
953
|
effectiveRawDiags = await demoteInferredForFile(absPath, effectiveRawDiags, cwd, lspService);
|
|
954
|
+
// #3088: one filter for the output, the counts and the footer reconcile.
|
|
955
|
+
const policy = applyProbeFindingPolicy(absPath, effectiveRawDiags, cwd, collectedContent, severity);
|
|
956
|
+
effectiveRawDiags = policy.kept;
|
|
949
957
|
const filtered = applySeverityFilter(effectiveRawDiags, severity);
|
|
950
958
|
const total = filtered.length;
|
|
951
959
|
const truncated = total > MAX_DIAGNOSTICS;
|
|
@@ -965,8 +973,8 @@ onConfirmedNoBlockers) {
|
|
|
965
973
|
});
|
|
966
974
|
}
|
|
967
975
|
const primaryId = primaryServerId(absPath);
|
|
968
|
-
const primaryDiags = limited.filter((d) => d.
|
|
969
|
-
const auxiliaryDiags = limited.filter((d) => d.
|
|
976
|
+
const primaryDiags = limited.filter((d) => d.serverId === primaryId);
|
|
977
|
+
const auxiliaryDiags = limited.filter((d) => d.serverId !== primaryId);
|
|
970
978
|
// Primary confirmation is always its own line, independent of how many
|
|
971
979
|
// auxiliary findings exist — a wall of ast-grep/opengrep noise must never
|
|
972
980
|
// bury whether the actual language server confirmed the file clean.
|
|
@@ -999,8 +1007,12 @@ onConfirmedNoBlockers) {
|
|
|
999
1007
|
// this result does NOT speak for, so "no auxiliary findings" can never be read
|
|
1000
1008
|
// as "the security scanners found nothing".
|
|
1001
1009
|
const coverageLine = unconfirmedServerIds.length > 0
|
|
1002
|
-
? `Auxiliary coverage INCOMPLETE — ${[...unconfirmedServerIds].join(", ")} did not answer within the wait budget, so ${unconfirmedServerIds.length === 1 ? "its findings are" : "their findings are"} NOT included here. This is not a clean bill of health for ${unconfirmedServerIds.length === 1 ? "that scanner" : "those scanners"}; re-check after the next edit
|
|
1010
|
+
? `Auxiliary coverage INCOMPLETE — ${[...unconfirmedServerIds].join(", ")} did not answer within the wait budget, so ${unconfirmedServerIds.length === 1 ? "its findings are" : "their findings are"} NOT included here. This is not a clean bill of health for ${unconfirmedServerIds.length === 1 ? "that scanner" : "those scanners"}; re-check after the next edit. waitMs can extend an ordinary wait, but it cannot override a session-demoted scanner.`
|
|
1003
1011
|
: undefined;
|
|
1012
|
+
// #3088: the #1616 suppressed-bucket rule on this lane — a policy drop is
|
|
1013
|
+
// stated as a count, never left to read as "nothing was wrong here"
|
|
1014
|
+
// (AGENTS.md shape 10). Bounded: one line per call, whatever the count.
|
|
1015
|
+
const suppressedLine = dispositionSuppressedLine(policy.suppressed);
|
|
1004
1016
|
let text;
|
|
1005
1017
|
if (total === 0) {
|
|
1006
1018
|
text = [
|
|
@@ -1008,6 +1020,7 @@ onConfirmedNoBlockers) {
|
|
|
1008
1020
|
"",
|
|
1009
1021
|
unavailable ?? "No auxiliary findings.",
|
|
1010
1022
|
...(coverageLine ? [coverageLine] : []),
|
|
1023
|
+
...(suppressedLine ? [suppressedLine] : []),
|
|
1011
1024
|
].join("\n");
|
|
1012
1025
|
}
|
|
1013
1026
|
else {
|
|
@@ -1021,6 +1034,8 @@ onConfirmedNoBlockers) {
|
|
|
1021
1034
|
}
|
|
1022
1035
|
if (coverageLine)
|
|
1023
1036
|
lines.push("", coverageLine);
|
|
1037
|
+
if (suppressedLine)
|
|
1038
|
+
lines.push("", suppressedLine);
|
|
1024
1039
|
if (unavailable)
|
|
1025
1040
|
lines.unshift(unavailable, "");
|
|
1026
1041
|
if (truncated) {
|
|
@@ -1047,6 +1062,9 @@ onConfirmedNoBlockers) {
|
|
|
1047
1062
|
code: d.code,
|
|
1048
1063
|
})),
|
|
1049
1064
|
totalDiagnostics: total,
|
|
1065
|
+
...(policy.suppressed > 0 && {
|
|
1066
|
+
dispositionSuppressed: policy.suppressed,
|
|
1067
|
+
}),
|
|
1050
1068
|
truncated,
|
|
1051
1069
|
unconfirmed,
|
|
1052
1070
|
timedOut: unconfirmed ? timedOut : undefined,
|
|
@@ -1172,32 +1190,67 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
|
|
|
1172
1190
|
const resolvedCwd = options.cwd ?? process.cwd();
|
|
1173
1191
|
const cacheCtx = createWorkspaceDiagnosticsCacheContext(resolvedCwd);
|
|
1174
1192
|
const scopeKey = buildScopeKey(options.serverScope ?? "all");
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1193
|
+
const resultsByIndex = new Map();
|
|
1194
|
+
let completed = 0;
|
|
1195
|
+
const pendingIndices = new Map();
|
|
1196
|
+
files.forEach((file, index) => {
|
|
1197
|
+
const queue = pendingIndices.get(file);
|
|
1198
|
+
if (queue)
|
|
1199
|
+
queue.push(index);
|
|
1200
|
+
else
|
|
1201
|
+
pendingIndices.set(file, [index]);
|
|
1202
|
+
});
|
|
1203
|
+
const groups = groupFilesByPrimaryServer(files);
|
|
1204
|
+
await runPerServerGroups(groups, options.concurrency, async (group) => {
|
|
1205
|
+
if (options.signal?.aborted)
|
|
1206
|
+
return;
|
|
1207
|
+
const first = group.files[0];
|
|
1208
|
+
if (first &&
|
|
1209
|
+
lspService &&
|
|
1210
|
+
!isWarmAttached() &&
|
|
1211
|
+
typeof lspService.ensureWarmForSweep === "function") {
|
|
1212
|
+
await lspService.ensureWarmForSweep(first, {
|
|
1213
|
+
signal: options.signal,
|
|
1214
|
+
});
|
|
1215
|
+
if (options.signal?.aborted)
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
await mapWithConcurrency(group.files, 1, async (file) => {
|
|
1219
|
+
// Keep duplicate explicit paths in their original result slots.
|
|
1220
|
+
const index = pendingIndices.get(file).shift();
|
|
1221
|
+
const work = collectFileDiagnosticResult(file, severity, lspService, options.waitMs, options.nextWriteIndex, options.serverScope, cacheCtx, scopeKey, resolvedCwd, options.onConfirmedNoBlockers);
|
|
1222
|
+
const bounded = withDeadline(work, {
|
|
1223
|
+
ms: batchFileDeadlineMs(),
|
|
1224
|
+
onTimeout: "undefined",
|
|
1225
|
+
onReject: "undefined",
|
|
1226
|
+
});
|
|
1227
|
+
const result = options.signal
|
|
1228
|
+
? await Promise.race([
|
|
1229
|
+
bounded,
|
|
1230
|
+
new Promise((resolve) => {
|
|
1231
|
+
if (options.signal?.aborted) {
|
|
1232
|
+
resolve(undefined);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
options.signal?.addEventListener("abort", () => resolve(undefined), {
|
|
1236
|
+
once: true,
|
|
1237
|
+
});
|
|
1238
|
+
}),
|
|
1239
|
+
])
|
|
1240
|
+
: await bounded;
|
|
1241
|
+
resultsByIndex.set(index, result ??
|
|
1242
|
+
inconclusiveBatchResult(file, options.signal?.aborted
|
|
1243
|
+
? "Batch aborted before this file completed."
|
|
1244
|
+
: `File check exceeded ${batchFileDeadlineMs()}ms.`));
|
|
1245
|
+
completed += 1;
|
|
1246
|
+
options.onProgress?.(completed, files.length);
|
|
1247
|
+
}, options.signal);
|
|
1248
|
+
}, options.signal);
|
|
1249
|
+
// The retired local pool returned only slots whose mapper had run. Keep that
|
|
1250
|
+
// dense abort contract while restoring the original file order for callers.
|
|
1251
|
+
const results = [...resultsByIndex.entries()]
|
|
1252
|
+
.sort(([left], [right]) => left - right)
|
|
1253
|
+
.map(([, result]) => result);
|
|
1201
1254
|
// Persist whatever was recorded, including a partial/aborted sweep's
|
|
1202
1255
|
// already-completed files — same "don't throw away confirmed work"
|
|
1203
1256
|
// posture as `runWorkspaceDiagnostics`.
|
|
@@ -1228,8 +1281,8 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
|
|
|
1228
1281
|
// `clean`/`unconfirmed` above already reflect ONLY the primary server's
|
|
1229
1282
|
// confirmation; this split does the same job for the listed diagnostics.
|
|
1230
1283
|
const primaryIdByFile = new Map(results.map((r) => [r.file, r.primaryServerId]));
|
|
1231
|
-
const primaryDisplay = display.filter((d) => d.
|
|
1232
|
-
const auxiliaryDisplay = display.filter((d) => d.
|
|
1284
|
+
const primaryDisplay = display.filter((d) => d.serverId === primaryIdByFile.get(d.file));
|
|
1285
|
+
const auxiliaryDisplay = display.filter((d) => d.serverId !== primaryIdByFile.get(d.file));
|
|
1233
1286
|
return {
|
|
1234
1287
|
results,
|
|
1235
1288
|
fileErrors,
|
|
@@ -1256,6 +1309,9 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1256
1309
|
}
|
|
1257
1310
|
const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, incompleteFiles, } = await collectBatchDiagnostics(absPaths, severity, lspService, options);
|
|
1258
1311
|
const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
|
|
1312
|
+
// #3088: the batch's own suppressed bucket — one summed line, never one per
|
|
1313
|
+
// file or per finding.
|
|
1314
|
+
const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
|
|
1259
1315
|
const lines = [
|
|
1260
1316
|
`Files checked: ${results.length}`,
|
|
1261
1317
|
`Total diagnostics: ${total}`,
|
|
@@ -1310,6 +1366,9 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1310
1366
|
lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
1311
1367
|
}
|
|
1312
1368
|
}
|
|
1369
|
+
const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
|
|
1370
|
+
if (suppressedLine)
|
|
1371
|
+
lines.push("", suppressedLine);
|
|
1313
1372
|
return {
|
|
1314
1373
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
1315
1374
|
details: {
|
|
@@ -1323,6 +1382,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1323
1382
|
primaryDiagnosticsCount: primaryDisplay.length,
|
|
1324
1383
|
auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
|
|
1325
1384
|
totalDiagnostics: total,
|
|
1385
|
+
...(dispositionSuppressed > 0 && { dispositionSuppressed }),
|
|
1326
1386
|
truncated,
|
|
1327
1387
|
cleanFiles: clean,
|
|
1328
1388
|
unconfirmedFiles: unconfirmed,
|
|
@@ -1332,8 +1392,8 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1332
1392
|
file: result.file,
|
|
1333
1393
|
outcome: result.outcome,
|
|
1334
1394
|
reason: result.inconclusiveReason ?? result.error ?? result.unavailable,
|
|
1335
|
-
primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.
|
|
1336
|
-
auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.
|
|
1395
|
+
primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId === result.primaryServerId).length,
|
|
1396
|
+
auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId !== result.primaryServerId).length,
|
|
1337
1397
|
})),
|
|
1338
1398
|
outcomeCounts,
|
|
1339
1399
|
incompleteFiles: incompleteFiles > 0 ? incompleteFiles : undefined,
|
|
@@ -1397,6 +1457,9 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1397
1457
|
const filesToProcess = collectedFiles.slice(0, MAX_FILES);
|
|
1398
1458
|
const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
|
|
1399
1459
|
const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
|
|
1460
|
+
// #3088: the directory scan's own suppressed bucket — one summed line.
|
|
1461
|
+
const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
|
|
1462
|
+
const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
|
|
1400
1463
|
let text;
|
|
1401
1464
|
if (total === 0) {
|
|
1402
1465
|
// #533/#570: an unconfirmed-containing directory result must never
|
|
@@ -1415,6 +1478,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1415
1478
|
...lspHealthWarnings.slice(0, 10),
|
|
1416
1479
|
]
|
|
1417
1480
|
: [cleanLine]),
|
|
1481
|
+
...(suppressedLine ? ["", suppressedLine] : []),
|
|
1418
1482
|
].join("\n");
|
|
1419
1483
|
}
|
|
1420
1484
|
else {
|
|
@@ -1454,6 +1518,8 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1454
1518
|
if (truncated) {
|
|
1455
1519
|
lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
|
|
1456
1520
|
}
|
|
1521
|
+
if (suppressedLine)
|
|
1522
|
+
lines.push("", suppressedLine);
|
|
1457
1523
|
text = lines.join("\n");
|
|
1458
1524
|
}
|
|
1459
1525
|
return {
|
|
@@ -1477,6 +1543,7 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1477
1543
|
primaryDiagnosticsCount: primaryDisplay.length,
|
|
1478
1544
|
auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
|
|
1479
1545
|
totalDiagnostics: total,
|
|
1546
|
+
...(dispositionSuppressed > 0 && { dispositionSuppressed }),
|
|
1480
1547
|
truncated,
|
|
1481
1548
|
cleanFiles: clean,
|
|
1482
1549
|
unconfirmedFiles: unconfirmed,
|
|
@@ -1490,6 +1557,22 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1490
1557
|
};
|
|
1491
1558
|
}
|
|
1492
1559
|
// ── helpers ─────────────────────────────────────────────────────────────
|
|
1560
|
+
/**
|
|
1561
|
+
* #3088: the one rendering of the probe lane's policy drop, shared by the
|
|
1562
|
+
* single-file, batch and directory renders so the three cannot word the same
|
|
1563
|
+
* fact differently. Returns `undefined` when nothing was dropped — the #1616
|
|
1564
|
+
* rule asks for a count when there IS one, not a "0 suppressed" line on every
|
|
1565
|
+
* clean probe.
|
|
1566
|
+
*/
|
|
1567
|
+
function dispositionSuppressedLine(suppressed) {
|
|
1568
|
+
if (suppressed <= 0)
|
|
1569
|
+
return undefined;
|
|
1570
|
+
return (`suppressed by disposition: ${suppressed} finding(s) dropped from this ` +
|
|
1571
|
+
"result because they're marked false-positive/won't-fix, disabled by " +
|
|
1572
|
+
"`.pi-lens.json` rule policy, or suppressed by an inline `pi-lens-ignore` " +
|
|
1573
|
+
"comment. Not a clean verdict for those locations — they were found, " +
|
|
1574
|
+
"then intentionally hidden.");
|
|
1575
|
+
}
|
|
1493
1576
|
function applySeverityFilter(diags, severity) {
|
|
1494
1577
|
if (severity === "all")
|
|
1495
1578
|
return diags;
|