pi-lens 3.8.70 → 3.8.72
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 +129 -0
- package/README.md +24 -0
- package/config/markdownlint/core.json +6 -0
- package/dist/clients/atomic-write.js +83 -0
- package/dist/clients/bus-events-logger.js +10 -8
- package/dist/clients/cache/rule-cache.js +9 -5
- package/dist/clients/cache-manager.js +19 -13
- package/dist/clients/config-enabled-shape.js +39 -0
- package/dist/clients/diagnostic-dispositions.js +360 -0
- package/dist/clients/dispatch/auxiliary-lsp.js +85 -3
- package/dist/clients/dispatch/dispatcher.js +6 -1
- package/dist/clients/dispatch/integration.js +21 -5
- package/dist/clients/dispatch/runners/lsp.js +59 -57
- package/dist/clients/dispatch/runners/markdownlint.js +7 -4
- package/dist/clients/dispatch/runners/oxlint.js +1 -1
- package/dist/clients/dispatch/runners/pyright.js +1 -0
- package/dist/clients/dispatch/runners/tree-sitter.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +68 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
- package/dist/clients/disposition-logger.js +39 -0
- package/dist/clients/disposition-publish.js +109 -0
- package/dist/clients/env-utils.js +38 -0
- package/dist/clients/file-utils.js +115 -36
- package/dist/clients/format-events-publish.js +299 -0
- package/dist/clients/git-tracked-ignore.js +208 -0
- package/dist/clients/instance-registry.js +94 -24
- package/dist/clients/jscpd-client.js +52 -39
- package/dist/clients/json-cache-read.js +65 -0
- package/dist/clients/language-profile.js +16 -19
- package/dist/clients/lens-config.js +106 -27
- package/dist/clients/lens-engine.js +129 -3
- package/dist/clients/lens-map.js +1079 -0
- package/dist/clients/lsp/aggregation.js +69 -2
- package/dist/clients/lsp/cascade-tier.js +1 -38
- package/dist/clients/lsp/client.js +75 -8
- package/dist/clients/lsp/index.js +1093 -95
- package/dist/clients/lsp/server-strategies.js +3 -224
- package/dist/clients/lsp/server.js +18 -2
- package/dist/clients/lsp/tsserver-sync.js +145 -0
- package/dist/clients/lsp/wait-policy/capability-snapshot.js +1 -0
- package/dist/clients/lsp/wait-policy/classification.js +63 -0
- package/dist/clients/lsp/wait-policy/index.js +10 -0
- package/dist/clients/lsp/wait-policy/strategies.js +244 -0
- package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
- package/dist/clients/lsp-budget.js +54 -16
- package/dist/clients/mcp/analyze.js +1 -1
- package/dist/clients/mcp/host-shim.js +12 -8
- package/dist/clients/mcp/ipc.js +118 -0
- package/dist/clients/mcp/session.js +10 -2
- package/dist/clients/opengrep-config.js +2 -10
- package/dist/clients/path-utils.js +26 -0
- package/dist/clients/pipeline.js +15 -6
- package/dist/clients/project-diagnostics/cache.js +5 -12
- package/dist/clients/project-diagnostics/fresh-fetch.js +20 -1
- package/dist/clients/project-diagnostics/scanner.js +37 -6
- package/dist/clients/project-lens-config.js +167 -3
- package/dist/clients/project-report.js +627 -0
- package/dist/clients/project-scale.js +239 -0
- package/dist/clients/project-scan-policy.js +11 -1
- package/dist/clients/project-snapshot.js +3 -6
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/recent-touches.js +6 -11
- package/dist/clients/review-graph/builder.js +247 -64
- package/dist/clients/review-graph/import-resolvers.js +191 -21
- package/dist/clients/review-graph/tsconfig-paths.js +242 -0
- package/dist/clients/review-graph/workspace-modules.js +21 -9
- package/dist/clients/rules-scanner.js +15 -2
- package/dist/clients/runtime-agent-end.js +127 -14
- package/dist/clients/runtime-config.js +4 -0
- package/dist/clients/runtime-coordinator.js +59 -2
- package/dist/clients/runtime-session.js +93 -13
- package/dist/clients/runtime-tool-call.js +789 -0
- package/dist/clients/runtime-tool-result.js +18 -4
- package/dist/clients/runtime-turn.js +12 -2
- package/dist/clients/safe-spawn.js +264 -40
- package/dist/clients/session-lifecycle.js +36 -0
- package/dist/clients/session-state-store.js +14 -12
- package/dist/clients/sgconfig.js +2 -9
- package/dist/clients/source-filter.js +136 -56
- package/dist/clients/source-walker.js +89 -0
- package/dist/clients/startup-scan.js +229 -84
- package/dist/clients/subagent-mode.js +14 -3
- package/dist/clients/todo-scanner.js +5 -1
- package/dist/clients/tool-policy.js +26 -4
- package/dist/clients/typos-config.js +2 -11
- package/dist/clients/warm-attach.js +204 -0
- package/dist/clients/widget-state.js +22 -0
- package/dist/clients/word-index.js +36 -5
- package/dist/clients/workspace-topology.js +266 -0
- package/dist/clients/zizmor-config.js +2 -10
- package/dist/index.js +35072 -29093
- package/dist/mcp/server.js +125 -10
- package/dist/tools/ast-grep-search.js +21 -0
- package/dist/tools/lens-diagnostic-mark.js +349 -0
- package/dist/tools/lens-diagnostics.js +159 -27
- package/dist/tools/lsp-diagnostics.js +116 -120
- package/dist/tools/lsp-navigation.js +11 -2
- package/dist/tools/project-report.js +94 -0
- package/dist/tools/symbol-search.js +32 -8
- package/docs/agent-tools.md +11 -4
- package/docs/dispositions.md +85 -0
- package/docs/environment-variables.md +26 -0
- package/docs/fable.md +10 -9
- package/docs/features.md +7 -1
- package/docs/globalconfig.md +70 -0
- package/docs/servercapabilities.md +1 -1
- package/docs/tools.md +3 -0
- package/docs/usage.md +13 -2
- package/package.json +6 -1
- package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
- package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +12 -0
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
- package/scripts/download-grammars.js +1 -0
- package/docs/agent-tooling-improvement-plan.md +0 -660
- package/docs/inspiration_gograph.md +0 -601
- package/docs/inspiration_neovim.md +0 -1795
- package/docs/inspiration_neovim2.md +0 -402
- package/docs/inspiration_tree_sitter_rules.md +0 -369
- package/docs/inspiration_tree_sitter_rules_investigation.md +0 -328
- package/docs/inspiration_tree_sitter_rules_round2.md +0 -470
- package/docs/lsp-bridge-analysis.md +0 -444
- package/docs/newinspiration.md +0 -338
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-ocaml.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-toml.wasm +0 -0
- package/grammars/tree-sitter-vue.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
|
@@ -16,8 +16,10 @@ import { RUNTIME_CONFIG } from "../../runtime-config.js";
|
|
|
16
16
|
import { PRIORITY } from "../priorities.js";
|
|
17
17
|
import { resolveRunnerPath } from "../runner-context.js";
|
|
18
18
|
import { convertLspDiagnostics } from "../utils/lsp-diagnostics.js";
|
|
19
|
-
import { enabledAuxiliaryLspServerIds,
|
|
19
|
+
import { enabledAuxiliaryLspServerIds, retagAuxiliaryDiagnostics, } from "../auxiliary-lsp.js";
|
|
20
20
|
import { readFileContent } from "./utils.js";
|
|
21
|
+
import { tryWarmAttachedCodeActions, tryWarmAttachedDiagnostics, } from "../../warm-attach.js";
|
|
22
|
+
import { contentHash, WARM_CODE_ACTION_LOOKUP_LIMIT, } from "../../mcp/ipc.js";
|
|
21
23
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
22
24
|
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
23
25
|
const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
@@ -27,7 +29,6 @@ const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
|
|
|
27
29
|
// after the cap still land in the client's cache and surface on the
|
|
28
30
|
// next edit. Overridable via PI_LENS_LSP_DIAGNOSTICS_MAX_WAIT_MS.
|
|
29
31
|
const LSP_DIAGNOSTICS_WAIT_MS = 2500;
|
|
30
|
-
const MAX_CODE_ACTION_LOOKUPS = 6;
|
|
31
32
|
const MAX_CODE_ACTION_TITLES = 3;
|
|
32
33
|
function normalizeActionTitle(title) {
|
|
33
34
|
return title.replace(/\s+/g, " ").trim();
|
|
@@ -109,6 +110,7 @@ const lspRunner = {
|
|
|
109
110
|
// one spawned server) — an empty `lspDiags` in that case is NOT a
|
|
110
111
|
// confirmed clean result and must not be reported as one (#570).
|
|
111
112
|
let diagnosticsInconclusive = false;
|
|
113
|
+
let usedWarmAttach = false;
|
|
112
114
|
let failureReason = "";
|
|
113
115
|
const content = readFileContent(ctx.filePath);
|
|
114
116
|
if (!content) {
|
|
@@ -124,21 +126,27 @@ const lspRunner = {
|
|
|
124
126
|
// path so their warm diagnostics merge into this same result.
|
|
125
127
|
const auxiliaryServerIds = enabledAuxiliaryLspServerIds((f) => ctx.pi.getFlag(f));
|
|
126
128
|
try {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
const attached = await tryWarmAttachedDiagnostics(ctx.filePath, content, Math.max(LSP_SPAWN_BUDGET_MS, LSP_DIAGNOSTICS_WAIT_MS));
|
|
130
|
+
usedWarmAttach = attached?.available === true;
|
|
131
|
+
const touched = attached?.available
|
|
132
|
+
? attached.response.diagnostics
|
|
133
|
+
: await lspService.touchFile(ctx.filePath, content, {
|
|
134
|
+
diagnostics: "document",
|
|
135
|
+
collectDiagnostics: true,
|
|
136
|
+
clientScope: auxiliaryServerIds.length > 0 ? "with-auxiliary" : "primary",
|
|
137
|
+
auxiliaryServerIds,
|
|
138
|
+
maxClientWaitMs: LSP_SPAWN_BUDGET_MS,
|
|
139
|
+
maxDiagnosticsWaitMs: LSP_DIAGNOSTICS_WAIT_MS,
|
|
140
|
+
source: "dispatch-lsp-runner",
|
|
141
|
+
});
|
|
136
142
|
if (touched === undefined) {
|
|
137
143
|
lspClientReady = false;
|
|
138
144
|
}
|
|
139
145
|
else {
|
|
140
146
|
lspDiags = touched;
|
|
141
|
-
diagnosticsInconclusive =
|
|
147
|
+
diagnosticsInconclusive =
|
|
148
|
+
touched
|
|
149
|
+
.inconclusive === true;
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
catch (err) {
|
|
@@ -206,56 +214,50 @@ const lspRunner = {
|
|
|
206
214
|
const blockingDiagIndexes = validLspDiags
|
|
207
215
|
.map((d, idx) => ({ d, idx }))
|
|
208
216
|
.filter(({ d }) => d.severity === 1)
|
|
209
|
-
.slice(0,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
.slice(0, WARM_CODE_ACTION_LOOKUP_LIMIT);
|
|
218
|
+
if (usedWarmAttach) {
|
|
219
|
+
// Diagnostics have already succeeded. Code actions are optional
|
|
220
|
+
// enrichment, so ANY IPC failure degrades to today's skip without
|
|
221
|
+
// promoting the attached session to a local LSP fleet.
|
|
222
|
+
const ranges = blockingDiagIndexes.map(({ d }) => ({
|
|
223
|
+
start: d.range.start,
|
|
224
|
+
end: d.range.end ?? d.range.start,
|
|
225
|
+
}));
|
|
226
|
+
const result = await tryWarmAttachedCodeActions(ctx.filePath, contentHash(content), ranges, LSP_DIAGNOSTICS_WAIT_MS);
|
|
227
|
+
if (result?.available) {
|
|
228
|
+
result.response.actions.forEach((actions, responseIndex) => {
|
|
229
|
+
const diagnosticIndex = blockingDiagIndexes[responseIndex]?.idx;
|
|
230
|
+
const suggestion = buildCodeActionSuggestion(actions);
|
|
231
|
+
if (diagnosticIndex !== undefined && suggestion) {
|
|
232
|
+
fixSuggestionByIndex.set(diagnosticIndex, suggestion);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
222
235
|
}
|
|
223
|
-
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await Promise.all(blockingDiagIndexes.map(async ({ d, idx }) => {
|
|
239
|
+
try {
|
|
240
|
+
const start = d.range.start;
|
|
241
|
+
const end = d.range.end ?? d.range.start;
|
|
242
|
+
const actions = await lspService.codeAction(ctx.filePath, start.line, start.character, end.line, end.character);
|
|
243
|
+
const suggestion = buildCodeActionSuggestion(actions);
|
|
244
|
+
if (suggestion) {
|
|
245
|
+
fixSuggestionByIndex.set(idx, suggestion);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Best-effort enrichment only; base diagnostics remain authoritative.
|
|
250
|
+
}
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
224
253
|
const diagnostics = convertLspDiagnostics(validLspDiags, diagnosticPath, { fixSuggestionByIndex });
|
|
225
254
|
// convertLspDiagnostics maps validLspDiags 1:1, so re-tag any
|
|
226
255
|
// auxiliary-sourced diagnostics (opengrep emits source "Semgrep", …) with
|
|
227
256
|
// their tool id + semantic policy — language-server diagnostics keep "lsp".
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
const suppressedIndices = new Set();
|
|
233
|
-
for (let i = 0; i < diagnostics.length; i++) {
|
|
234
|
-
const profile = findAuxiliaryProfileForSource(validLspDiags[i]?.source);
|
|
235
|
-
if (!profile)
|
|
236
|
-
continue;
|
|
237
|
-
if (isAuxiliaryDiagnosticSuppressed(validLspDiags[i], content)) {
|
|
238
|
-
suppressedIndices.add(i);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
let blockingAllowed = blockingAllowedByProfile.get(profile);
|
|
242
|
-
if (blockingAllowed === undefined) {
|
|
243
|
-
blockingAllowed = profile.allowBlocking?.(ctx.cwd) ?? false;
|
|
244
|
-
blockingAllowedByProfile.set(profile, blockingAllowed);
|
|
245
|
-
}
|
|
246
|
-
const d = diagnostics[i];
|
|
247
|
-
d.tool = profile.tool;
|
|
248
|
-
d.semantic = profile.semantic(validLspDiags[i], { blockingAllowed });
|
|
249
|
-
if (d.semantic !== "blocking" && d.severity === "error") {
|
|
250
|
-
d.severity = "warning";
|
|
251
|
-
}
|
|
252
|
-
const defectClass = profile.defectClass?.(validLspDiags[i]);
|
|
253
|
-
if (defectClass)
|
|
254
|
-
d.defectClass = defectClass;
|
|
255
|
-
}
|
|
256
|
-
const keptDiagnostics = suppressedIndices.size
|
|
257
|
-
? diagnostics.filter((_, i) => !suppressedIndices.has(i))
|
|
258
|
-
: diagnostics;
|
|
257
|
+
// #692: shared with the scan/sweep reconcile paths (`retagAuxiliaryDiagnostics`
|
|
258
|
+
// in `../auxiliary-lsp.js`) so a scan-reconciled aux finding gets identical
|
|
259
|
+
// tool/semantic/defectClass tagging instead of keeping tool "lsp".
|
|
260
|
+
const keptDiagnostics = retagAuxiliaryDiagnostics(diagnostics, validLspDiags, content, { cwd: ctx.cwd, fileRole: ctx.fileRole });
|
|
259
261
|
const hasErrors = keptDiagnostics.some((d) => d.semantic === "blocking");
|
|
260
262
|
const resultSemantic = hasErrors
|
|
261
263
|
? "blocking"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvePackagePath } from "../../package-root.js";
|
|
1
2
|
import { safeSpawnAsync } from "../../safe-spawn.js";
|
|
2
3
|
import { getLinterPolicyForCwd, hasMarkdownlintConfig, } from "../../tool-policy.js";
|
|
3
4
|
import { PRIORITY } from "../priorities.js";
|
|
@@ -93,9 +94,6 @@ const markdownlintRunner = {
|
|
|
93
94
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
94
95
|
}
|
|
95
96
|
const hasConfig = hasMarkdownlintConfig(cwd);
|
|
96
|
-
if (!hasConfig) {
|
|
97
|
-
// Run with sensible defaults even without explicit config
|
|
98
|
-
}
|
|
99
97
|
let cmd = null;
|
|
100
98
|
if (await (markdownlint.isAvailableAsync(cwd))) {
|
|
101
99
|
cmd = markdownlint.getCommand(cwd);
|
|
@@ -105,7 +103,12 @@ const markdownlintRunner = {
|
|
|
105
103
|
}
|
|
106
104
|
if (!cmd)
|
|
107
105
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
108
|
-
const configArgs = hasConfig
|
|
106
|
+
const configArgs = hasConfig
|
|
107
|
+
? []
|
|
108
|
+
: [
|
|
109
|
+
"--config",
|
|
110
|
+
resolvePackagePath(import.meta.url, "config/markdownlint/core.json"),
|
|
111
|
+
];
|
|
109
112
|
const result = await safeSpawnAsync(cmd, [...configArgs, ctx.filePath], {
|
|
110
113
|
timeout: 15000,
|
|
111
114
|
cwd,
|
|
@@ -44,7 +44,7 @@ const oxlintRunner = {
|
|
|
44
44
|
appliesTo: ["jsts"],
|
|
45
45
|
priority: PRIORITY.LINT_SECONDARY,
|
|
46
46
|
enabledByDefault: true,
|
|
47
|
-
skipTestFiles:
|
|
47
|
+
skipTestFiles: false,
|
|
48
48
|
async run(ctx) {
|
|
49
49
|
const cwd = ctx.cwd || process.cwd();
|
|
50
50
|
const policy = getJstsLintPolicyForCwd(cwd);
|
|
@@ -23,7 +23,7 @@ function runBlastRadiusInBackground(cwd, filePath, languageId, facts) {
|
|
|
23
23
|
void (async () => {
|
|
24
24
|
try {
|
|
25
25
|
const graph = await buildOrUpdateGraph(cwd, [filePath], facts);
|
|
26
|
-
const impact = computeImpactCascade(graph, filePath);
|
|
26
|
+
const impact = computeImpactCascade(graph, filePath, cwd);
|
|
27
27
|
logTreeSitter({
|
|
28
28
|
phase: "blast_radius",
|
|
29
29
|
filePath,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the `pi-lens-ignore` suppression comment that inline-suppressions.ts
|
|
3
|
+
* already knows how to read (#690's `suppress` disposition). The reader
|
|
4
|
+
* accepts either `//` or `#` on any line regardless of language, but the
|
|
5
|
+
* WRITER must pick the syntactically valid comment character for the target
|
|
6
|
+
* file — an errant `#` line breaks parsing in a `//`-comment language and
|
|
7
|
+
* vice versa.
|
|
8
|
+
*/
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
const HASH_COMMENT_EXTENSIONS = new Set([
|
|
11
|
+
".py",
|
|
12
|
+
".rb",
|
|
13
|
+
".sh",
|
|
14
|
+
".bash",
|
|
15
|
+
".zsh",
|
|
16
|
+
".yml",
|
|
17
|
+
".yaml",
|
|
18
|
+
".toml",
|
|
19
|
+
".r",
|
|
20
|
+
".pl",
|
|
21
|
+
".pm",
|
|
22
|
+
".ex",
|
|
23
|
+
".exs",
|
|
24
|
+
".elixir",
|
|
25
|
+
".dockerfile",
|
|
26
|
+
".gitignore",
|
|
27
|
+
".conf",
|
|
28
|
+
".cfg",
|
|
29
|
+
".ini",
|
|
30
|
+
]);
|
|
31
|
+
function commentPrefixFor(filePath) {
|
|
32
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
33
|
+
return HASH_COMMENT_EXTENSIONS.has(ext) ? "#" : "//";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Insert a `pi-lens-ignore: <rule>` comment on the line immediately above
|
|
37
|
+
* `line` (1-based), matching inline-suppressions.ts's "same line or line
|
|
38
|
+
* above" read semantics. If the line above already carries a pi-lens-ignore
|
|
39
|
+
* comment, the rule is appended to its list instead of adding a duplicate
|
|
40
|
+
* comment line. Returns the updated content; throws if `line` is out of range.
|
|
41
|
+
*/
|
|
42
|
+
export function insertSuppressComment(content, filePath, line, rule) {
|
|
43
|
+
const lines = content.split(/\r?\n/);
|
|
44
|
+
if (line < 1 || line > lines.length) {
|
|
45
|
+
throw new Error(`line ${line} is out of range (file has ${lines.length} lines)`);
|
|
46
|
+
}
|
|
47
|
+
const prefix = commentPrefixFor(filePath);
|
|
48
|
+
const aboveIdx = line - 2; // 0-based index of the line immediately above
|
|
49
|
+
const existingAbove = aboveIdx >= 0 ? lines[aboveIdx] : undefined;
|
|
50
|
+
const suppressRe = /((?:\/\/|#)\s*pi-lens-ignore:\s*)(.+)$/;
|
|
51
|
+
const match = existingAbove !== undefined ? suppressRe.exec(existingAbove) : null;
|
|
52
|
+
if (match && existingAbove !== undefined) {
|
|
53
|
+
const rules = match[2]
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((r) => r.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
if (!rules.includes(rule))
|
|
58
|
+
rules.push(rule);
|
|
59
|
+
lines[aboveIdx] = existingAbove.slice(0, match.index) + match[1] + rules.join(", ");
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
// Match the indentation of the flagged line so the inserted comment lines
|
|
63
|
+
// up rather than sitting at column 0.
|
|
64
|
+
const targetLine = lines[line - 1] ?? "";
|
|
65
|
+
const indent = /^\s*/.exec(targetLine)?.[0] ?? "";
|
|
66
|
+
lines.splice(line - 1, 0, `${indent}${prefix} pi-lens-ignore: ${rule}`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
@@ -7,7 +7,9 @@ export function convertLspDiagnostics(diags, filePath, options = {}) {
|
|
|
7
7
|
const severity = severityMap[d.severity] ?? "info";
|
|
8
8
|
const semantic = d.severity === 1 ? "blocking" : (d.severity === 2 ? "warning" : "none");
|
|
9
9
|
const code = String(d.code ?? "unknown");
|
|
10
|
-
|
|
10
|
+
// #692: identity ALWAYS derives from the diagnostic's own source — never
|
|
11
|
+
// from a caller-supplied scan label (see `scanOrigin`'s doc comment above).
|
|
12
|
+
const source = d.source ?? tool;
|
|
11
13
|
const hasSuggestion = options.fixSuggestionByIndex?.has(idx) ?? false;
|
|
12
14
|
return {
|
|
13
15
|
id: `${tool}:${code}:${d.range.start.line}`,
|
|
@@ -23,6 +25,7 @@ export function convertLspDiagnostics(diags, filePath, options = {}) {
|
|
|
23
25
|
autoFixAvailable: false,
|
|
24
26
|
fixKind: hasSuggestion ? "suggestion" : undefined,
|
|
25
27
|
fixSuggestion: options.fixSuggestionByIndex?.get(idx),
|
|
28
|
+
scanOrigin: options.scanOrigin,
|
|
26
29
|
};
|
|
27
30
|
});
|
|
28
31
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON telemetry log for diagnostic disposition marks (#690). Same shape as
|
|
3
|
+
* clients/actionable-warnings-logger.ts: machine-global (getGlobalPiLensDir),
|
|
4
|
+
* size-capped with a single `.log.1` backup, isTestMode no-op.
|
|
5
|
+
*
|
|
6
|
+
* Why a log at all: #181 identified false-positive marks flowing to telemetry
|
|
7
|
+
* as THE highest-value rule-tuning signal — which shipped rules misfire, on
|
|
8
|
+
* what tools, how often — and this is that hookup. The disposition store
|
|
9
|
+
* itself is not enough: it keeps only the LATEST entry per anchor (a re-mark
|
|
10
|
+
* overwrites), and `defer` marks never touch the store at all (in-memory by
|
|
11
|
+
* design), so this log is the only durable trace of both re-mark history and
|
|
12
|
+
* defer activity.
|
|
13
|
+
*/
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { isTestMode } from "./env-utils.js";
|
|
16
|
+
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
17
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
18
|
+
const DISPOSITION_LOG_DIR = getGlobalPiLensDir();
|
|
19
|
+
const DISPOSITION_LOG_FILE = path.join(DISPOSITION_LOG_DIR, "dispositions.log");
|
|
20
|
+
const DISPOSITION_LOG_BACKUP_FILE = path.join(DISPOSITION_LOG_DIR, "dispositions.log.1");
|
|
21
|
+
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_DISPOSITION_LOG_MAX_BYTES ?? "1048576", 10) || 1048576);
|
|
22
|
+
const writer = createNdjsonLogger({
|
|
23
|
+
filePath: DISPOSITION_LOG_FILE,
|
|
24
|
+
maxBytes: MAX_LOG_BYTES,
|
|
25
|
+
backupPath: DISPOSITION_LOG_BACKUP_FILE,
|
|
26
|
+
});
|
|
27
|
+
export function logDispositionEvent(entry) {
|
|
28
|
+
if (isTestMode()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
32
|
+
}
|
|
33
|
+
export function getDispositionLogPath() {
|
|
34
|
+
return DISPOSITION_LOG_FILE;
|
|
35
|
+
}
|
|
36
|
+
/** Resolve once all enqueued disposition writes are on disk. */
|
|
37
|
+
export function flushDispositionLog() {
|
|
38
|
+
return writer.flush();
|
|
39
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publishes `pilens:diagnostic:disposition` on pi's shared `pi.events` bus
|
|
3
|
+
* (#690).
|
|
4
|
+
*
|
|
5
|
+
* Sibling to `clients/diagnostics-publish.ts` (#502) rather than a new export
|
|
6
|
+
* inside it, per the "owns nothing in common" rule spelled out in
|
|
7
|
+
* format-events-publish.ts's header: the event is diagnostic-DOMAIN, but it
|
|
8
|
+
* shares none of diagnostics-publish's module state or contract —
|
|
9
|
+
* dispositions are point-in-time FACTS ("a mark just happened"), not
|
|
10
|
+
* full-replace state snapshots, so the seq counter, the
|
|
11
|
+
* previously-reported-dirty set, and the whole staleness/replace consumer
|
|
12
|
+
* contract over there simply don't apply here. Only the emit plumbing shape
|
|
13
|
+
* and the `PI_LENS_BUS_PUBLISH` kill switch (`isBusPublishEnabled`, imported
|
|
14
|
+
* from bus-publish.ts) are shared, same as every other producer sibling.
|
|
15
|
+
*
|
|
16
|
+
* Fire-and-forget: publishing must never affect the mark path's success or
|
|
17
|
+
* latency. Any failure is swallowed; `dbg` fires at most once per process on
|
|
18
|
+
* first failure so a wired caller can log it without spamming.
|
|
19
|
+
*
|
|
20
|
+
* Versioning policy: frozen-additive, same discipline as #482/#502. New
|
|
21
|
+
* optional fields may be added under `v: 1`; a breaking change to an existing
|
|
22
|
+
* field's meaning must bump `v`.
|
|
23
|
+
*/
|
|
24
|
+
import { logBusEvent } from "./bus-events-logger.js";
|
|
25
|
+
import { isBusPublishEnabled } from "./bus-publish.js";
|
|
26
|
+
import { normalizeFilePath } from "./path-utils.js";
|
|
27
|
+
export const BUS_DISPOSITION_EVENT = "pilens:diagnostic:disposition";
|
|
28
|
+
export const BUS_DISPOSITION_VERSION = 1;
|
|
29
|
+
let busEmit;
|
|
30
|
+
let hasLoggedFailure = false;
|
|
31
|
+
let hasLoggedUnwired = false;
|
|
32
|
+
let hasLoggedDisabled = false;
|
|
33
|
+
/**
|
|
34
|
+
* Wire the emit function from pi's `pi.events` bus. Called once at extension
|
|
35
|
+
* factory time from index.ts, same call as `wireDiagnosticsBusEmitter` (#502)
|
|
36
|
+
* — both producers share the identical `pi.events.emit` binding. Never called
|
|
37
|
+
* ⇒ `publishDisposition` no-ops, which is exactly the state unit tests and
|
|
38
|
+
* the MCP server path run in (no pi host, no `pi.events`).
|
|
39
|
+
*/
|
|
40
|
+
export function wireDispositionBusEmitter(emitFn) {
|
|
41
|
+
busEmit = emitFn;
|
|
42
|
+
}
|
|
43
|
+
/** Test-only: reset module state between test files. */
|
|
44
|
+
export function _resetDispositionPublishForTests() {
|
|
45
|
+
busEmit = undefined;
|
|
46
|
+
hasLoggedFailure = false;
|
|
47
|
+
hasLoggedUnwired = false;
|
|
48
|
+
hasLoggedDisabled = false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Publish one `pilens:diagnostic:disposition` event for a mark (one event per
|
|
52
|
+
* markDisposition call). Fire-and-forget: never throws, never awaited.
|
|
53
|
+
*/
|
|
54
|
+
export function publishDisposition(args) {
|
|
55
|
+
if (!isBusPublishEnabled()) {
|
|
56
|
+
if (!hasLoggedDisabled) {
|
|
57
|
+
hasLoggedDisabled = true;
|
|
58
|
+
logBusEvent({
|
|
59
|
+
event: BUS_DISPOSITION_EVENT,
|
|
60
|
+
outcome: "skipped_disabled",
|
|
61
|
+
cwd: normalizeFilePath(args.cwd),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!busEmit) {
|
|
67
|
+
if (!hasLoggedUnwired) {
|
|
68
|
+
hasLoggedUnwired = true;
|
|
69
|
+
logBusEvent({
|
|
70
|
+
event: BUS_DISPOSITION_EVENT,
|
|
71
|
+
outcome: "skipped_unwired",
|
|
72
|
+
cwd: normalizeFilePath(args.cwd),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const payload = {
|
|
79
|
+
v: BUS_DISPOSITION_VERSION,
|
|
80
|
+
source: "pi-lens",
|
|
81
|
+
cwd: normalizeFilePath(args.cwd),
|
|
82
|
+
filePath: normalizeFilePath(args.filePath),
|
|
83
|
+
disposition: args.disposition,
|
|
84
|
+
tool: args.tool,
|
|
85
|
+
rule: args.rule,
|
|
86
|
+
line: args.line,
|
|
87
|
+
anchor: args.anchor,
|
|
88
|
+
reason: args.reason,
|
|
89
|
+
};
|
|
90
|
+
busEmit(BUS_DISPOSITION_EVENT, payload);
|
|
91
|
+
logBusEvent({
|
|
92
|
+
event: BUS_DISPOSITION_EVENT,
|
|
93
|
+
outcome: "emitted",
|
|
94
|
+
cwd: payload.cwd,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
logBusEvent({
|
|
99
|
+
event: BUS_DISPOSITION_EVENT,
|
|
100
|
+
outcome: "emit_failed",
|
|
101
|
+
cwd: normalizeFilePath(args.cwd),
|
|
102
|
+
error: String(err),
|
|
103
|
+
});
|
|
104
|
+
if (!hasLoggedFailure) {
|
|
105
|
+
hasLoggedFailure = true;
|
|
106
|
+
args.dbg?.(`disposition-publish: pilens:diagnostic:disposition emit failed (further failures suppressed): ${err}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -43,3 +43,41 @@ export function toPositiveFinite(value) {
|
|
|
43
43
|
const num = typeof value === "number" ? value : Number(value);
|
|
44
44
|
return Number.isFinite(num) && num > 0 ? num : 0;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Factory for the lazy-memoized "positive-finite-number env override, else a
|
|
48
|
+
* default" getter pattern repeated across `startup-scan.ts` (#699, #758) and
|
|
49
|
+
* originally documented as house style in `runtime-config.ts` / `slow-fs.ts` /
|
|
50
|
+
* `subagent-mode.ts` (#763).
|
|
51
|
+
*
|
|
52
|
+
* `get()` reads `process.env[envName]` through {@link toPositiveFinite} the
|
|
53
|
+
* first time it's called, memoizes the result (or `fallback` when the env var
|
|
54
|
+
* is unset/non-finite/non-positive), and returns the memo on every subsequent
|
|
55
|
+
* call. `_resetForTests()` clears the memo so a test can flip the env var and
|
|
56
|
+
* observe a fresh read (matching the existing `_resetForTests` convention).
|
|
57
|
+
*
|
|
58
|
+
* Deliberately does NOT touch `process.env` until `get()` is first called —
|
|
59
|
+
* that's the entire point of the lazy pattern: importing a module must never
|
|
60
|
+
* have a side effect on process env at load time.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const _ttl = lazyEnvNumber("PI_LENS_STARTUP_SCAN_VERDICT_TTL_MS", DEFAULT_TTL_MS);
|
|
65
|
+
* export const getStartupScanVerdictTtlMs = _ttl.get;
|
|
66
|
+
* export const _resetStartupScanVerdictTtlForTests = _ttl._resetForTests;
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function lazyEnvNumber(envName, fallback) {
|
|
70
|
+
let memo;
|
|
71
|
+
return {
|
|
72
|
+
get() {
|
|
73
|
+
if (memo !== undefined)
|
|
74
|
+
return memo;
|
|
75
|
+
const envValue = toPositiveFinite(process.env[envName]);
|
|
76
|
+
memo = envValue > 0 ? envValue : fallback;
|
|
77
|
+
return memo;
|
|
78
|
+
},
|
|
79
|
+
_resetForTests() {
|
|
80
|
+
memo = undefined;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|