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
|
@@ -78,6 +78,11 @@ import { lineContentHash } from "./read-guard.js";
|
|
|
78
78
|
// below goes through `deferredKey`, which folds the resolved cwd into the Set
|
|
79
79
|
// key, so the two projects can no longer collide.
|
|
80
80
|
const deferredThisSession = new Set();
|
|
81
|
+
let widgetDispositionReconciler = null;
|
|
82
|
+
/** Register the widget's mirror of the durable disposition state. */
|
|
83
|
+
export function registerWidgetDispositionReconciler(reconciler) {
|
|
84
|
+
widgetDispositionReconciler = reconciler;
|
|
85
|
+
}
|
|
81
86
|
function deferredKey(cwd, anchor) {
|
|
82
87
|
return `${normalizeMapKey(cwd)}::${anchor}`;
|
|
83
88
|
}
|
|
@@ -97,7 +102,8 @@ export const normalizeMessage = sharedNormalizeMessage;
|
|
|
97
102
|
// case, symlink/realpath, or slash difference between the two forms silently
|
|
98
103
|
// orphans the agent's own false-positive/flagged mark (a #533 dropped-signal).
|
|
99
104
|
// Canonicalize BOTH inputs through `normalizeMapKey` (the SAME normalizer the
|
|
100
|
-
// read side already relies on — realpathSync.native on Windows
|
|
105
|
+
// read side already relies on — realpathSync.native on Windows, and on POSIX
|
|
106
|
+
// too since #3098) BEFORE computing
|
|
101
107
|
// the relative path, so write and read produce identical anchors regardless of
|
|
102
108
|
// the form the caller held. `normalizeMapKey` is idempotent, so the already-
|
|
103
109
|
// canonicalized read side is unaffected; the realpath I/O is acceptable here
|
|
@@ -357,6 +363,7 @@ function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, i
|
|
|
357
363
|
publishDisposition({
|
|
358
364
|
cwd,
|
|
359
365
|
filePath: target.filePath,
|
|
366
|
+
message: target.message,
|
|
360
367
|
disposition,
|
|
361
368
|
tool: target.tool,
|
|
362
369
|
rule: target.rule,
|
|
@@ -390,6 +397,7 @@ export function markDisposition(cwd, target, disposition, reason, identity) {
|
|
|
390
397
|
const existing = readState(cwd).dispositions?.[anchor];
|
|
391
398
|
if (disposition === "defer") {
|
|
392
399
|
deferredThisSession.add(deferredKey(cwd, anchor));
|
|
400
|
+
widgetDispositionReconciler?.(cwd, target, disposition);
|
|
393
401
|
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity);
|
|
394
402
|
return anchor;
|
|
395
403
|
}
|
|
@@ -407,6 +415,9 @@ export function markDisposition(cwd, target, disposition, reason, identity) {
|
|
|
407
415
|
lineText,
|
|
408
416
|
};
|
|
409
417
|
commitDisposition(cwd, anchor, entry);
|
|
418
|
+
// The durable commit is the guard for this mirror: a failed commit must not
|
|
419
|
+
// make the in-memory widget claim that a mark exists.
|
|
420
|
+
widgetDispositionReconciler?.(cwd, target, disposition);
|
|
410
421
|
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity);
|
|
411
422
|
return anchor;
|
|
412
423
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The effective ast-grep rule catalog for a workspace root (#3053).
|
|
3
|
+
*
|
|
4
|
+
* Before this module, `clients/dispatch/runners/ast-grep-napi.ts` (the NAPI
|
|
5
|
+
* runner) and `clients/dispatch/rule-ignores.ts` (the LSP-seam matcher, #3041)
|
|
6
|
+
* each walked `getAstGrepRuleSources(root)` and resolved rule-id precedence
|
|
7
|
+
* independently — two implementations of the same derivation. They had
|
|
8
|
+
* already drifted once by the time #3053 was filed: #3046 round 2 found the
|
|
9
|
+
* LSP seam claiming an id only when the winning document declared `ignores`,
|
|
10
|
+
* so a project rule that redefines a bundled id with NO `ignores` fired
|
|
11
|
+
* per-edit (the runner) but stayed carved out over LSP (the seam) — the same
|
|
12
|
+
* defect the whole PR existed to remove, just inverted. This module is the
|
|
13
|
+
* one place that walk and that precedence rule now live.
|
|
14
|
+
*
|
|
15
|
+
* Precedence: sources are walked in `getAstGrepRuleSources`'s order (project
|
|
16
|
+
* primary, project secondary, bundled primary, bundled secondary), each
|
|
17
|
+
* loaded with the same loader the runner always used (`loadYamlRulesFresh`
|
|
18
|
+
* for mutable project trees, `loadYamlRules`'s mtime-cached path for
|
|
19
|
+
* immutable bundled catalogs). The FIRST rule sighted for an id — scanning
|
|
20
|
+
* sources in order, and each source's own rules in file order — claims it;
|
|
21
|
+
* no later document for the same id is ever consulted.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately does NOT special-case an id that recurs WITHIN one source's
|
|
24
|
+
* own rule list. That is `duplicateRuleIds` in ast-grep-napi.ts, a
|
|
25
|
+
* runner-only concern layered on top of this catalog's plain result: the
|
|
26
|
+
* runner drops such an id entirely (one Duplicate-rule-id diagnostic, no
|
|
27
|
+
* rule fires for it), but `materializeMergedRuleDir` (clients/sgconfig.ts) —
|
|
28
|
+
* what ast-grep's own LSP binary actually runs — keeps every document an
|
|
29
|
+
* EARLIER source hasn't already claimed, so both copies of a within-source
|
|
30
|
+
* duplicate still survive into what ast-grep publishes over LSP. Baking the
|
|
31
|
+
* runner's drop into this catalog would leave those published findings with
|
|
32
|
+
* no registered `ignores` at all, silently unsuppressed — reopening #3041 for
|
|
33
|
+
* that id (#3046 round 2 F7). So the catalog resolves a within-source
|
|
34
|
+
* duplicate to its first-sighted document like any other collision, and it
|
|
35
|
+
* is each consumer's job to decide what a within-source duplicate means for
|
|
36
|
+
* it — the runner still computes `duplicateRuleIds` itself and skips those
|
|
37
|
+
* ids before ever consulting this catalog's answer for them.
|
|
38
|
+
*/
|
|
39
|
+
import { getAstGrepRuleSources } from "../sgconfig.js";
|
|
40
|
+
import { loadYamlRules, loadYamlRulesFresh, } from "./runners/yaml-rule-parser.js";
|
|
41
|
+
/**
|
|
42
|
+
* Build the effective ast-grep catalog for `root`. See the module docstring
|
|
43
|
+
* for the precedence rule and the within-source-duplicate decision.
|
|
44
|
+
*
|
|
45
|
+
* No try/catch around either loader: every I/O path inside them already
|
|
46
|
+
* swallows its own failure (missing dir, unreadable readdir, unreadable file
|
|
47
|
+
* all yield an empty list — `yaml-rule-parser.ts`'s `findYamlRuleFiles` and
|
|
48
|
+
* `loadYamlRuleFiles`), so a catch here would be a guard no mutation can red.
|
|
49
|
+
*/
|
|
50
|
+
export function buildEffectiveAstGrepCatalog(root) {
|
|
51
|
+
const sources = [];
|
|
52
|
+
const effectiveRules = new Map();
|
|
53
|
+
for (const source of getAstGrepRuleSources(root)) {
|
|
54
|
+
const rules = source.origin === "project"
|
|
55
|
+
? loadYamlRulesFresh(source.dir)
|
|
56
|
+
: loadYamlRules(source.dir);
|
|
57
|
+
sources.push({ source, rules });
|
|
58
|
+
for (const rule of rules) {
|
|
59
|
+
if (effectiveRules.has(rule.id))
|
|
60
|
+
continue;
|
|
61
|
+
effectiveRules.set(rule.id, { rule, source });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { sources, effectiveRules };
|
|
65
|
+
}
|
|
@@ -22,6 +22,7 @@ import { findLocalOpengrepConfig } from "../opengrep-config.js";
|
|
|
22
22
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
23
23
|
import { findLocalZizmorConfig } from "../zizmor-config.js";
|
|
24
24
|
import { classifyDefect } from "./diagnostic-taxonomy.js";
|
|
25
|
+
import { isRuleIgnoredForPath, loadRuleIgnorePatterns, } from "./rule-ignores.js";
|
|
25
26
|
/**
|
|
26
27
|
* Semgrep/opengrep `# nosemgrep` / `# nosemgrep: <rule-id>[,<rule-id>]` inline
|
|
27
28
|
* suppression (#441). A bare `# nosemgrep` drops every finding on its line; the
|
|
@@ -248,8 +249,22 @@ export function isAuxiliaryDiagnosticSuppressed(d, content) {
|
|
|
248
249
|
* files that are suppressed per-edit reappeared wholesale in every
|
|
249
250
|
* `mode=full` sweep. Omitting `opts` (or `opts.fileRole`) keeps every existing
|
|
250
251
|
* 2-arg call site's behavior byte-for-byte unchanged.
|
|
252
|
+
*
|
|
253
|
+
* #3041: `opts.filePath` + `opts.scanRoot` additionally apply each rule's own
|
|
254
|
+
* `ignores` path carve-out (#965) — the THIRD member of the same defect class
|
|
255
|
+
* as #586 and #692 above. `ast-grep scan` applies `ignores` during its own
|
|
256
|
+
* project walk, but `ast-grep lsp` publishes per-document diagnostics WITHOUT
|
|
257
|
+
* applying them (measured against ast-grep 0.45.3), so a rule the NAPI runner
|
|
258
|
+
* correctly skips on `scripts/**` came back over LSP on every `source=lsp`
|
|
259
|
+
* query and every `mode=full` sweep. Both routes reach this function, so the
|
|
260
|
+
* carve-out belongs here rather than in either route's own output merge.
|
|
251
261
|
*/
|
|
252
262
|
export function applyAuxiliarySuppressions(diagnostics, content, opts) {
|
|
263
|
+
// One catalog read per call, not per diagnostic. Both rule loaders behind it
|
|
264
|
+
// are cached (~0.09 ms per call on pi-lens's own catalog), so a per-file call
|
|
265
|
+
// from a sweep costs the same order as the NAPI runner's own per-file load.
|
|
266
|
+
const { filePath, scanRoot } = opts ?? {};
|
|
267
|
+
const ruleIgnores = filePath && scanRoot ? loadRuleIgnorePatterns(scanRoot) : undefined;
|
|
253
268
|
return diagnostics.filter((d) => {
|
|
254
269
|
if (isAuxiliaryDiagnosticSuppressed(d, content))
|
|
255
270
|
return false;
|
|
@@ -258,6 +273,19 @@ export function applyAuxiliarySuppressions(diagnostics, content, opts) {
|
|
|
258
273
|
if (profile?.skipTestFiles)
|
|
259
274
|
return false;
|
|
260
275
|
}
|
|
276
|
+
// Keyed by the diagnostic's own rule id — ast-grep's LSP reports the bare
|
|
277
|
+
// catalog id as `code`. Gated on the producing tool as well: the map is
|
|
278
|
+
// built from the AST-GREP catalog, so without the gate any auxiliary whose
|
|
279
|
+
// own rule id happened to collide with a catalog id (several are generic —
|
|
280
|
+
// `no-raw-types`, `no-string-concat-in-loop`) would be silently dropped on
|
|
281
|
+
// a path an unrelated rule carved out. Losing a finding silently is the
|
|
282
|
+
// harm this whole change exists to stop, so the collision is gated, not
|
|
283
|
+
// accepted (#3041 r2 F4).
|
|
284
|
+
if (ruleIgnores !== undefined &&
|
|
285
|
+
findAuxiliaryProfileForSource(d.source)?.tool === "ast-grep" &&
|
|
286
|
+
isRuleIgnoredForPath(filePath, scanRoot, ruleIgnores.get(String(d.code)))) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
261
289
|
return true;
|
|
262
290
|
});
|
|
263
291
|
}
|
|
@@ -26,10 +26,10 @@ import { getPrimaryDispatchGroup } from "../language-policy.js";
|
|
|
26
26
|
import { resolveLanguageRootForFile } from "../language-profile.js";
|
|
27
27
|
import { logLatency, phaseFinished, phaseStarted } from "../latency-logger.js";
|
|
28
28
|
import { isSpawnableCommand } from "../installer/index.js";
|
|
29
|
-
import { isAbsoluteFilePath, normalizeEphemeralMapKey, normalizeMapKey, } from "../path-utils.js";
|
|
29
|
+
import { isAbsoluteFilePath, isWindowsPath, normalizeEphemeralMapKey, normalizeMapKey, } from "../path-utils.js";
|
|
30
30
|
import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
31
31
|
import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
|
|
32
|
-
import {
|
|
32
|
+
import { probeToolAsync } from "../tool-probe.js";
|
|
33
33
|
import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
|
|
34
34
|
import { classifyProbeFailure, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./runners/utils/availability-policy.js";
|
|
35
35
|
import { recordAvailabilityProbeOverrun, getDispatchAvailabilityGeneration, } from "./runners/utils/runner-helpers.js";
|
|
@@ -147,7 +147,7 @@ export async function checkToolAvailability(command, facts) {
|
|
|
147
147
|
let hostStallMs;
|
|
148
148
|
let probeJoined = false;
|
|
149
149
|
try {
|
|
150
|
-
const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () =>
|
|
150
|
+
const shared = dispatcherProbeFlights.run(`dispatcher:${key}`, () => probeToolAsync(command, ["--version"], {
|
|
151
151
|
timeout: TOOL_PROBE_TIMEOUT_MS,
|
|
152
152
|
}));
|
|
153
153
|
probeJoined = shared.joined;
|
|
@@ -249,7 +249,13 @@ telemetryModel, telemetryProvider) {
|
|
|
249
249
|
const normalizedProjectRoot = normalizeMapKey(path.resolve(projectRoot ?? cwd));
|
|
250
250
|
const normalizedCwd = normalizeMapKey(resolveLanguageRootForFile(absoluteFilePath, cwd));
|
|
251
251
|
const normalizedFilePath = normalizeMapKey(absoluteFilePath);
|
|
252
|
-
|
|
252
|
+
// `resolveRunnerPath` intentionally normalizes map keys, which folds a
|
|
253
|
+
// literal POSIX backslash into a separator. Preserve that one proven
|
|
254
|
+
// classifier distinction while still resolving relative inputs from `cwd`.
|
|
255
|
+
const kindFilePath = !isWindowsPath(filePath) && filePath.includes("\\")
|
|
256
|
+
? path.resolve(cwd, filePath)
|
|
257
|
+
: absoluteFilePath;
|
|
258
|
+
const kind = detectFileKind(kindFilePath);
|
|
253
259
|
const contentPrefix = readFilePrefix(normalizedFilePath);
|
|
254
260
|
const fileRole = detectFileRole(normalizedFilePath, contentPrefix);
|
|
255
261
|
// Captured once here so the generated short-circuit below can emit a
|
|
@@ -282,14 +288,17 @@ telemetryModel, telemetryProvider) {
|
|
|
282
288
|
writeIndex,
|
|
283
289
|
telemetryModel,
|
|
284
290
|
telemetryProvider,
|
|
291
|
+
toolCwdMemo: {},
|
|
285
292
|
async hasTool(command) {
|
|
286
293
|
return checkToolAvailability(command, facts);
|
|
287
294
|
},
|
|
288
|
-
log(message) {
|
|
295
|
+
log(message, level) {
|
|
289
296
|
// #1333: pi owns the terminal — a runner advisory must never be a raw
|
|
290
297
|
// write. Every DispatchContext.log line lands in extension.log instead.
|
|
291
298
|
logExtension({
|
|
292
299
|
subsystem: "dispatch",
|
|
300
|
+
// exactOptionalPropertyTypes: an omitted level must stay omitted.
|
|
301
|
+
...(level !== undefined && { level }),
|
|
293
302
|
message,
|
|
294
303
|
metadata: { filePath: normalizedFilePath, kind },
|
|
295
304
|
});
|
|
@@ -446,31 +455,49 @@ function buildCoverageNotice(ctx, runnerLatencies) {
|
|
|
446
455
|
...new Set(relevant.flatMap((r) => r.unconfirmedServerIds ?? [])),
|
|
447
456
|
];
|
|
448
457
|
if (unconfirmedServerIds.length > 0) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
458
|
+
const deferredIds = new Set(relevant.flatMap((r) => r.deferredServerIds ?? []));
|
|
459
|
+
const markerFor = (ids) => {
|
|
460
|
+
const shown = ids.slice(0, 4);
|
|
461
|
+
const remainder = ids.length - shown.length;
|
|
462
|
+
return `${shown.join(", ")}${remainder > 0 ? ` +${remainder}` : ""}`;
|
|
463
|
+
};
|
|
464
|
+
const deferredServerIds = unconfirmedServerIds.filter((id) => deferredIds.has(id));
|
|
465
|
+
const silentServerIds = unconfirmedServerIds.filter((id) => !deferredIds.has(id));
|
|
466
|
+
// The marker describes this exact scanner set. A scanner can recover
|
|
467
|
+
// while another goes dark on the same file, so the set belongs in the
|
|
468
|
+
// session dedupe identity rather than only kind and path.
|
|
452
469
|
// #2016: these are SCANNER IDS, not filesystem paths. `normalizeMapKey`
|
|
453
470
|
// would realpath each one; on Windows that fails, falls through to
|
|
454
471
|
// `resolveNonExisting`, and resolves the id against the CURRENT process
|
|
455
472
|
// cwd, so the dedupe key differed by platform and by cwd (the #2219
|
|
456
473
|
// non-path-sentinel class). The cheap syntactic fold is what this
|
|
457
|
-
// session-scoped dedupe key actually needs.
|
|
458
|
-
|
|
474
|
+
// session-scoped dedupe key actually needs. Code-unit comparator: the
|
|
475
|
+
// sorted set is a KEY, so ordering must be deterministic across locales
|
|
476
|
+
// — localeCompare is not.
|
|
477
|
+
const dedupeSet = (ids) => [...new Set(ids)]
|
|
459
478
|
.map(normalizeEphemeralMapKey)
|
|
460
|
-
// Code-unit comparator: the sorted set is a dedupe KEY, so ordering
|
|
461
|
-
// must be deterministic across locales — localeCompare is not.
|
|
462
479
|
.sort((a, b) => Number(a > b) - Number(a < b))
|
|
463
480
|
.join(",");
|
|
464
|
-
|
|
481
|
+
// #2810 round 4: the PARTITION is part of the identity, not just the
|
|
482
|
+
// scanner set. The same scanner can be silent on one edit and marked for
|
|
483
|
+
// late delivery on the next; keying on the unconfirmed set alone showed
|
|
484
|
+
// the session whichever message came first and suppressed the other, so a
|
|
485
|
+
// scanner that recovered a delivery path (or lost one) kept the stale
|
|
486
|
+
// wording for the rest of the session.
|
|
487
|
+
const onceKey = `${ctx.kind}:${ctx.filePath}:${dedupeSet(silentServerIds)}|${dedupeSet(deferredServerIds)}`;
|
|
465
488
|
if (coverageNoticeSeen.has(onceKey))
|
|
466
489
|
return undefined;
|
|
467
490
|
coverageNoticeSeen.add(onceKey);
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
491
|
+
const coverageParts = [];
|
|
492
|
+
if (deferredServerIds.length > 0) {
|
|
493
|
+
coverageParts.push(`coverage: ${markerFor(deferredServerIds)} deferred — diagnostics are incomplete; findings arrive at turn end if the scan lands.`);
|
|
494
|
+
}
|
|
495
|
+
if (silentServerIds.length > 0) {
|
|
496
|
+
coverageParts.push(`coverage: ${markerFor(silentServerIds)} silent — diagnostics are incomplete (not a clean result).`);
|
|
497
|
+
}
|
|
471
498
|
return {
|
|
472
499
|
id: `coverage-partial:${ctx.kind}:${path.basename(ctx.filePath)}`,
|
|
473
|
-
message:
|
|
500
|
+
message: coverageParts.join("\n"),
|
|
474
501
|
filePath: ctx.filePath,
|
|
475
502
|
severity: "warning",
|
|
476
503
|
semantic: "warning",
|
|
@@ -837,6 +864,9 @@ async function runGroup(ctx, group, registry, onRunnerResult) {
|
|
|
837
864
|
...(result.unconfirmedServerIds !== undefined && {
|
|
838
865
|
unconfirmedServerIds: result.unconfirmedServerIds,
|
|
839
866
|
}),
|
|
867
|
+
...(result.deferredServerIds !== undefined && {
|
|
868
|
+
deferredServerIds: result.deferredServerIds,
|
|
869
|
+
}),
|
|
840
870
|
});
|
|
841
871
|
logLatency({
|
|
842
872
|
type: "runner",
|
|
@@ -135,7 +135,7 @@ function isCallPassThrough(stmt, paramNames) {
|
|
|
135
135
|
}
|
|
136
136
|
return { pass: true, target: (expr.children ?? [])[0]?.text };
|
|
137
137
|
}
|
|
138
|
-
function calcCyclomaticComplexity(body) {
|
|
138
|
+
export function calcCyclomaticComplexity(body) {
|
|
139
139
|
let cc = 1;
|
|
140
140
|
walk(body, (node) => {
|
|
141
141
|
if (COMPLEXITY_TYPES.has(node.type)) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { logLatency } from "../../latency-logger.js";
|
|
2
2
|
import { isJstsFactFile } from "../../file-kinds.js";
|
|
3
3
|
import { childrenOfType, firstChildOfType, withFactTree, walk, } from "./tree-sitter-facts.js";
|
|
4
|
+
/** Whether the import-facts provider applies to a source path. */
|
|
5
|
+
export function importFactsApplyTo(filePath) {
|
|
6
|
+
return isJstsFactFile(filePath);
|
|
7
|
+
}
|
|
4
8
|
function stripQuotes(raw) {
|
|
5
9
|
return raw.replace(/^["'`]+|["'`]+$/g, "");
|
|
6
10
|
}
|
|
@@ -69,7 +73,7 @@ export const importFactProvider = {
|
|
|
69
73
|
provides: ["file.imports", "file.reexports", "file.importFactsCoverage"],
|
|
70
74
|
requires: ["file.content"],
|
|
71
75
|
appliesTo(ctx) {
|
|
72
|
-
return
|
|
76
|
+
return importFactsApplyTo(ctx.filePath);
|
|
73
77
|
},
|
|
74
78
|
async run(ctx, store) {
|
|
75
79
|
const content = store.getFileFact(ctx.filePath, "file.content");
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE filter stack a findings surface applies before a diagnostic reaches
|
|
3
|
+
* the agent: inline `pi-lens-ignore` comments (#442), stored dispositions
|
|
4
|
+
* (#690 — false-positive/suppress/defer), and the project's `.pi-lens.json`
|
|
5
|
+
* `rules.<id>.disable`/`select` policy, in that order.
|
|
6
|
+
*
|
|
7
|
+
* The order is load-bearing and is the one the per-edit dispatcher established:
|
|
8
|
+
* inline suppression first (the user's in-file intent), then the stored marks,
|
|
9
|
+
* then the project policy — so the policy filter sees the same set the per-edit
|
|
10
|
+
* path produces, with no double counting and no policy-rejected leftovers.
|
|
11
|
+
*
|
|
12
|
+
* #3088: these were three separate calls open-coded in `lens-diagnostics.ts`'s
|
|
13
|
+
* `mode=full` merge and nothing at all on the `lens_diagnostics source=lsp` /
|
|
14
|
+
* `lsp_diagnostics` probe lane — so a finding an agent had marked
|
|
15
|
+
* `false-positive` was hidden in `mode=delta`/`mode=full` and re-reported on
|
|
16
|
+
* the lane `skills/pi-lens-lsp-navigation` steers agents to as PRIMARY. Both
|
|
17
|
+
* lanes call this module now: `applyFindingPolicy` for surfaces that already
|
|
18
|
+
* hold dispatch `Diagnostic`s, `applyLspFindingPolicy` for the probe lane,
|
|
19
|
+
* which holds raw `LSPDiagnostic`s.
|
|
20
|
+
*/
|
|
21
|
+
import { applyDispositions, hasAnyDispositionMarks, } from "../diagnostic-dispositions.js";
|
|
22
|
+
import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
23
|
+
import { retagAuxiliaryDiagnostics } from "./auxiliary-lsp.js";
|
|
24
|
+
import { applyInlineSuppressions } from "./inline-suppressions.js";
|
|
25
|
+
import { applyRulePolicy, rulePolicyMapFromConfig } from "./rule-policy.js";
|
|
26
|
+
import { convertLspDiagnostics } from "./utils/lsp-diagnostics.js";
|
|
27
|
+
/**
|
|
28
|
+
* Inline suppression → stored dispositions → project rule policy, over
|
|
29
|
+
* diagnostics that already carry dispatch identity (`tool`/`rule`/`line`).
|
|
30
|
+
*/
|
|
31
|
+
export function applyFindingPolicy(diagnostics, options) {
|
|
32
|
+
const inlineKept = applyInlineSuppressions(diagnostics, options.content);
|
|
33
|
+
const disposed = filterDisposed(inlineKept, options);
|
|
34
|
+
return { kept: applyRulePolicy(disposed, options.policyMap), inlineKept };
|
|
35
|
+
}
|
|
36
|
+
/** The project's `.pi-lens.json` `rules.<id>.disable`/`select` policy map.
|
|
37
|
+
* One derivation for every surface that filters by it — `loadPiLensProjectConfig`
|
|
38
|
+
* is mtime-cached, so repeated calls in one sweep cost one stat. */
|
|
39
|
+
export function loadProjectRulePolicyMap(cwd) {
|
|
40
|
+
return rulePolicyMapFromConfig(loadPiLensProjectConfig(cwd).rules);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Disposition filter over the candidate identities a mark can carry.
|
|
44
|
+
*
|
|
45
|
+
* With no `identities` provider this is `applyDispositions` on the diagnostics
|
|
46
|
+
* themselves — one anchor pair per diagnostic, the behavior every existing
|
|
47
|
+
* caller has. With one, the SAME `applyDispositions` runs over the flattened
|
|
48
|
+
* candidate list — no second strict/weak implementation to drift away from it
|
|
49
|
+
* (#1617's single-source rule) — and a diagnostic drops when ANY of its
|
|
50
|
+
* candidate identities was disposed.
|
|
51
|
+
*/
|
|
52
|
+
function filterDisposed(diagnostics, options) {
|
|
53
|
+
const identities = options.identities;
|
|
54
|
+
if (!identities) {
|
|
55
|
+
return applyDispositions(diagnostics, options.cwd, options.filePath, options.content);
|
|
56
|
+
}
|
|
57
|
+
// Same zero-I/O hoist the cache-only modes use (#1625 F2): with no marks at
|
|
58
|
+
// all there is nothing a candidate expansion could match, so the probe lane
|
|
59
|
+
// pays no per-finding allocation for the overwhelmingly common empty store.
|
|
60
|
+
if (!diagnostics.length || !hasAnyDispositionMarks(options.cwd)) {
|
|
61
|
+
return diagnostics;
|
|
62
|
+
}
|
|
63
|
+
const expanded = diagnostics.flatMap((diagnostic) => identities(diagnostic).map((identity) => ({
|
|
64
|
+
diagnostic,
|
|
65
|
+
candidate: {
|
|
66
|
+
...identity,
|
|
67
|
+
message: diagnostic.message,
|
|
68
|
+
...(diagnostic.line !== undefined && { line: diagnostic.line }),
|
|
69
|
+
...(diagnostic.semantic !== undefined && {
|
|
70
|
+
semantic: diagnostic.semantic,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
})));
|
|
74
|
+
const survivors = new Set(applyDispositions(expanded.map((entry) => entry.candidate), options.cwd, options.filePath, options.content));
|
|
75
|
+
const disposed = new Set();
|
|
76
|
+
for (const entry of expanded) {
|
|
77
|
+
if (!survivors.has(entry.candidate))
|
|
78
|
+
disposed.add(entry.diagnostic);
|
|
79
|
+
}
|
|
80
|
+
return disposed.size === 0
|
|
81
|
+
? diagnostics
|
|
82
|
+
: diagnostics.filter((diagnostic) => !disposed.has(diagnostic));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* #3088 / AGENTS.md shape 26: the identity spellings a mark against a probe
|
|
86
|
+
* finding can carry, derived from the surfaces that RENDER it rather than
|
|
87
|
+
* guessed:
|
|
88
|
+
*
|
|
89
|
+
* - `convertLspDiagnostics` + `retagAuxiliaryDiagnostics` — the canonical
|
|
90
|
+
* dispatch identity (`tool: "lsp"`, or the auxiliary's own id, and
|
|
91
|
+
* `rule: "<source>:<code>"`). This is what the widget footer, `mode=delta`
|
|
92
|
+
* and `mode=full` render, so it is the spelling of a mark made anywhere
|
|
93
|
+
* else in the session.
|
|
94
|
+
* - `formatDiag` in `tools/lsp-diagnostics.ts` — the probe's OWN text render
|
|
95
|
+
* is `[<source>] (<code>)`, so a mark made from THIS lane's output carries
|
|
96
|
+
* `tool: "<source>"`, `rule: "<code>"`.
|
|
97
|
+
*
|
|
98
|
+
* plus, for each, the form with `tool` omitted: `lens_diagnostic_mark`'s
|
|
99
|
+
* `tool` parameter is optional and #3088's own reproduction omits it. A
|
|
100
|
+
* single-spelling filter honors a mark made on one surface while re-reporting
|
|
101
|
+
* the same finding when the mark was made on the other, which is exactly the
|
|
102
|
+
* non-convergence #3088 reported.
|
|
103
|
+
*/
|
|
104
|
+
function probeIdentities(canonical, raw) {
|
|
105
|
+
const rendered = [
|
|
106
|
+
[canonical.tool, canonical.rule],
|
|
107
|
+
];
|
|
108
|
+
if (raw !== undefined &&
|
|
109
|
+
(raw.source !== undefined || raw.code !== undefined)) {
|
|
110
|
+
rendered.push([
|
|
111
|
+
raw.source,
|
|
112
|
+
raw.code === undefined ? undefined : String(raw.code),
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
return expandRenderedIdentities(rendered);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* #3102: the identity spellings a mark against a surface that renders a
|
|
119
|
+
* finding's RULE but never its TOOL can carry — the turn-end late-auxiliary
|
|
120
|
+
* advisory (`<file>:<line>:<col> [<rule>] <message>`) and the cold-neighbour
|
|
121
|
+
* cascade run (`line N, col M rule=<rule>: <message>`). Both print the
|
|
122
|
+
* canonical dispatch `rule`, so a mark made ELSEWHERE (the widget, `mode=full`)
|
|
123
|
+
* carries the full `(tool, rule)` pair while a mark made from the surface's own
|
|
124
|
+
* output has no tool to pass on — and `lens_diagnostic_mark`'s `tool` parameter
|
|
125
|
+
* is optional. Honoring only one of the two spellings is exactly the
|
|
126
|
+
* non-convergence #3088 reported, so both go through the SAME expansion
|
|
127
|
+
* `probeIdentities` uses.
|
|
128
|
+
*/
|
|
129
|
+
export function renderedRuleIdentities(diagnostic) {
|
|
130
|
+
return expandRenderedIdentities([[diagnostic.tool, diagnostic.rule]]);
|
|
131
|
+
}
|
|
132
|
+
/** Every rendered `(tool, rule)` spelling plus the `tool`-omitted form of
|
|
133
|
+
* each, de-duplicated, in order. One derivation for both callers. */
|
|
134
|
+
function expandRenderedIdentities(rendered) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (const [tool, rule] of rendered) {
|
|
138
|
+
// Both spellings also in the `tool`-omitted form the mark tool admits.
|
|
139
|
+
for (const candidateTool of [tool, undefined]) {
|
|
140
|
+
const key = `${candidateTool ?? ""}\u0000${rule ?? ""}`;
|
|
141
|
+
if (seen.has(key))
|
|
142
|
+
continue;
|
|
143
|
+
seen.add(key);
|
|
144
|
+
out.push({
|
|
145
|
+
...(candidateTool !== undefined && { tool: candidateTool }),
|
|
146
|
+
...(rule !== undefined && { rule }),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/* No drop COUNT is returned here on purpose (round 2 F1): the number an agent
|
|
153
|
+
* is shown has to be scoped to that caller's severity threshold, and this
|
|
154
|
+
* module must not learn about a threshold its other caller (`mode=full`) does
|
|
155
|
+
* not have. `kept` versus the input is all a caller needs to derive its own. */
|
|
156
|
+
/**
|
|
157
|
+
* The probe lane's entry point: the same stack, over RAW LSP diagnostics.
|
|
158
|
+
*
|
|
159
|
+
* Identity comes from the two shared converters the widget footer already runs
|
|
160
|
+
* on this very path (`convertLspDiagnostics` + `retagAuxiliaryDiagnostics`),
|
|
161
|
+
* never a third derivation — a second copy of the conversion giving the same
|
|
162
|
+
* finding a different `rule`, and orphaning every mark against it, is the #692
|
|
163
|
+
* defect itself.
|
|
164
|
+
*
|
|
165
|
+
* `retagAuxiliaryDiagnostics` is used for its in-place re-tag only; its own
|
|
166
|
+
* drop set (native aux comments, `skipTestFiles`) is deliberately ignored here
|
|
167
|
+
* because `applyAuxiliarySuppressions` already applied exactly that filter to
|
|
168
|
+
* these diagnostics at collection time — re-dropping would double-apply a
|
|
169
|
+
* filter this route owns upstream.
|
|
170
|
+
*/
|
|
171
|
+
export function applyLspFindingPolicy(diagnostics, options) {
|
|
172
|
+
if (!diagnostics.length)
|
|
173
|
+
return { kept: diagnostics, inlineKept: diagnostics };
|
|
174
|
+
const content = options.content ?? "";
|
|
175
|
+
// `convertLspDiagnostics` drops entries with no start line, which would
|
|
176
|
+
// break the 1:1 index alignment `retagAuxiliaryDiagnostics` and the map-back
|
|
177
|
+
// below depend on. Partition first so the converted array IS aligned with
|
|
178
|
+
// `anchored`; a line-less entry passes straight through (it carries no line
|
|
179
|
+
// for an anchor to bind to, and dropping it would silently lose a finding
|
|
180
|
+
// this filter has no claim over).
|
|
181
|
+
const anchored = [];
|
|
182
|
+
for (const diagnostic of diagnostics) {
|
|
183
|
+
if (diagnostic.range?.start?.line !== undefined)
|
|
184
|
+
anchored.push(diagnostic);
|
|
185
|
+
}
|
|
186
|
+
if (!anchored.length)
|
|
187
|
+
return { kept: diagnostics, inlineKept: diagnostics };
|
|
188
|
+
const converted = convertLspDiagnostics(anchored, options.filePath);
|
|
189
|
+
retagAuxiliaryDiagnostics(converted, anchored, content, {
|
|
190
|
+
cwd: options.cwd,
|
|
191
|
+
fileRole: options.fileRole,
|
|
192
|
+
});
|
|
193
|
+
// `convertLspDiagnostics` maps its (pre-filtered) input 1:1, so `converted[i]`
|
|
194
|
+
// belongs to `anchored[i]`. Pair them up front and work from the pairing: a
|
|
195
|
+
// converted entry with no counterpart is simply never paired, so it can never
|
|
196
|
+
// be dropped — fail open, never hide a finding this filter could not identify.
|
|
197
|
+
const rawByConverted = new Map();
|
|
198
|
+
for (const [index, diagnostic] of converted.entries()) {
|
|
199
|
+
const raw = anchored[index];
|
|
200
|
+
if (raw !== undefined)
|
|
201
|
+
rawByConverted.set(diagnostic, raw);
|
|
202
|
+
}
|
|
203
|
+
const { kept, inlineKept } = applyFindingPolicy(converted, {
|
|
204
|
+
cwd: options.cwd,
|
|
205
|
+
filePath: options.filePath,
|
|
206
|
+
content,
|
|
207
|
+
policyMap: options.policyMap,
|
|
208
|
+
identities: (diagnostic) => probeIdentities(diagnostic, rawByConverted.get(diagnostic)),
|
|
209
|
+
});
|
|
210
|
+
const survivorsOf = (survivors) => {
|
|
211
|
+
if (survivors.length === converted.length)
|
|
212
|
+
return diagnostics;
|
|
213
|
+
const keptConverted = new Set(survivors);
|
|
214
|
+
const dropped = new Set();
|
|
215
|
+
for (const [diagnostic, raw] of rawByConverted) {
|
|
216
|
+
if (!keptConverted.has(diagnostic))
|
|
217
|
+
dropped.add(raw);
|
|
218
|
+
}
|
|
219
|
+
return diagnostics.filter((d) => !dropped.has(d));
|
|
220
|
+
};
|
|
221
|
+
return { kept: survivorsOf(kept), inlineKept: survivorsOf(inlineKept) };
|
|
222
|
+
}
|