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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { getProjectDataDir } from "./file-utils.js";
|
|
14
|
+
import { readJsonCache } from "./json-cache-read.js";
|
|
14
15
|
import { normalizeMapKey } from "./path-utils.js";
|
|
15
16
|
// --- Defaults ---
|
|
16
17
|
const DEFAULT_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes
|
|
@@ -72,13 +73,20 @@ export class CacheManager {
|
|
|
72
73
|
return null;
|
|
73
74
|
}
|
|
74
75
|
try {
|
|
75
|
-
const
|
|
76
|
+
const onReadError = (err) => {
|
|
77
|
+
this.log(`Cache read error: ${scanner} — ${err}`);
|
|
78
|
+
};
|
|
79
|
+
const meta = readJsonCache(metaPath, (parsed) => parsed, onReadError);
|
|
80
|
+
if (meta === undefined)
|
|
81
|
+
return null;
|
|
76
82
|
const age = Date.now() - new Date(meta.timestamp).getTime();
|
|
77
83
|
if (age > maxAgeMs) {
|
|
78
84
|
this.log(`Cache stale: ${scanner} (age: ${Math.round(age / 1000)}s, max: ${maxAgeMs / 1000}s)`);
|
|
79
85
|
return null;
|
|
80
86
|
}
|
|
81
|
-
const data =
|
|
87
|
+
const data = readJsonCache(cachePath, (parsed) => parsed, onReadError);
|
|
88
|
+
if (data === undefined)
|
|
89
|
+
return null;
|
|
82
90
|
this.log(`Cache hit: ${scanner} (age: ${Math.round(age / 1000)}s)`);
|
|
83
91
|
return { data, meta };
|
|
84
92
|
}
|
|
@@ -111,7 +119,9 @@ export class CacheManager {
|
|
|
111
119
|
if (!fs.existsSync(metaPath))
|
|
112
120
|
return false;
|
|
113
121
|
try {
|
|
114
|
-
const meta =
|
|
122
|
+
const meta = readJsonCache(metaPath, (parsed) => parsed);
|
|
123
|
+
if (meta === undefined)
|
|
124
|
+
return false;
|
|
115
125
|
const age = Date.now() - new Date(meta.timestamp).getTime();
|
|
116
126
|
return age <= maxAgeMs;
|
|
117
127
|
}
|
|
@@ -150,16 +160,12 @@ export class CacheManager {
|
|
|
150
160
|
lastUpdated: new Date().toISOString(),
|
|
151
161
|
};
|
|
152
162
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
files: {},
|
|
160
|
-
lastUpdated: new Date().toISOString(),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
+
const state = readJsonCache(statePath, (parsed) => parsed);
|
|
164
|
+
return (state ?? {
|
|
165
|
+
...DEFAULT_TURN_STATE,
|
|
166
|
+
files: {},
|
|
167
|
+
lastUpdated: new Date().toISOString(),
|
|
168
|
+
});
|
|
163
169
|
}
|
|
164
170
|
/**
|
|
165
171
|
* Write turn state.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared parser for the `{ enabled?: boolean }` shape used by BOTH the global
|
|
3
|
+
* config (`clients/lens-config.ts`, `~/.pi-lens/config.json`) and the project
|
|
4
|
+
* config (`clients/project-lens-config.ts`, `.pi-lens.json`) for their
|
|
5
|
+
* mutation-control keys (`format`, `autofix`, `actionableWarnings.autoFix`).
|
|
6
|
+
*
|
|
7
|
+
* Extracted (#792) so both loaders parse and warn on the identical shape
|
|
8
|
+
* identically instead of two independent hand-rolled implementations that had
|
|
9
|
+
* diverged in strictness — project warned on invalid `enabled` values via its
|
|
10
|
+
* own `warnInvalidConfigOnce`, global silently dropped them. This module has
|
|
11
|
+
* no dependency on either config module (avoids an import cycle) — callers
|
|
12
|
+
* supply their own `warnInvalid` callback so each config keeps its own
|
|
13
|
+
* warn-once plumbing (keyed by its own config path).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Parse a raw `{ enabled?: boolean }` value.
|
|
17
|
+
*
|
|
18
|
+
* - `undefined` input → `undefined` (key absent from the config).
|
|
19
|
+
* - Non-object (or array) input → `warnInvalid` is called, returns `undefined`.
|
|
20
|
+
* - Object with no `enabled` key → `{}` (present but nothing to say).
|
|
21
|
+
* - `enabled` present but not a boolean → `warnInvalid` is called, returns `{}`.
|
|
22
|
+
* - `enabled` a boolean → `{ enabled }`.
|
|
23
|
+
*/
|
|
24
|
+
export function parseEnabledShape(value, fieldPath, warnInvalid) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
28
|
+
warnInvalid(`${fieldPath} must be an object`);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const raw = value;
|
|
32
|
+
if (!("enabled" in raw))
|
|
33
|
+
return {};
|
|
34
|
+
if (typeof raw.enabled !== "boolean") {
|
|
35
|
+
warnInvalid(`${fieldPath}.enabled must be a boolean`);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
return { enabled: raw.enabled };
|
|
39
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent+user disposition layer over dispatch diagnostics (#690, unifying #181/
|
|
3
|
+
* #503/#504's discussion). Four dispositions:
|
|
4
|
+
*
|
|
5
|
+
* false-positive — the rule misfired. Project-persistent, routed nowhere
|
|
6
|
+
* special yet (telemetry hookup is a fast-follow).
|
|
7
|
+
* suppress — real finding, deliberate policy not to fix. Persistent,
|
|
8
|
+
* but the mechanism is an inline `pi-lens-ignore` comment
|
|
9
|
+
* written into the source (see suppress-writer.ts), not
|
|
10
|
+
* just a store entry — portable, git-visible, discoverable
|
|
11
|
+
* without pi-lens's own store. The store entry here is an
|
|
12
|
+
* audit-trail mirror, not the enforcement point.
|
|
13
|
+
* defer — fix later, not now. Session-ephemeral: held in memory
|
|
14
|
+
* only, so it naturally resurfaces on process restart —
|
|
15
|
+
* never persisted, never needs pruning.
|
|
16
|
+
* flagged — user wants the agent to fix this. Persistent until
|
|
17
|
+
* resolved; surfaced through the existing lens_diagnostics
|
|
18
|
+
* query (tagged), not a separate file/tool the agent has
|
|
19
|
+
* to separately poll.
|
|
20
|
+
*
|
|
21
|
+
* Anchoring: TWO flavors, chosen per-disposition because each one binds to a
|
|
22
|
+
* different thing conceptually:
|
|
23
|
+
*
|
|
24
|
+
* STRICT ("dd:" prefix) — relativeFile|tool|rule|normalizedMessage|
|
|
25
|
+
* lineContentHash(diagnostic's own line). Used ONLY for false-positive: a
|
|
26
|
+
* false-positive judgment is about THIS specific piece of code — if the
|
|
27
|
+
* line is rewritten, the rule earned a fresh chance to fire on the new
|
|
28
|
+
* content, so the mark should NOT follow it. Reuses read-guard's
|
|
29
|
+
* lineContentHash so a no-op formatter/whitespace pass doesn't rot the
|
|
30
|
+
* anchor, while a semantic edit to the flagged line correctly invalidates
|
|
31
|
+
* it.
|
|
32
|
+
* WEAK ("ddw:" prefix) — relativeFile|tool|rule|normalizedMessage, no line
|
|
33
|
+
* hash at all. Used for defer, flagged, and suppress: these are
|
|
34
|
+
* intent-level judgments ("I'll get to this", "fix this", "policy says
|
|
35
|
+
* don't") about a finding identity, not about one exact line's bytes —
|
|
36
|
+
* they must survive incidental edits elsewhere on the flagged line
|
|
37
|
+
* (reformatting, a nearby rename) without silently dropping the mark.
|
|
38
|
+
* suppress's real enforcement is the inline comment (see
|
|
39
|
+
* suppress-writer.ts) which travels with the code by construction; the
|
|
40
|
+
* weak-anchored store entry is just an audit mirror plus a second,
|
|
41
|
+
* belt-and-braces filter.
|
|
42
|
+
*
|
|
43
|
+
* Distinct prefixes ("dd:" vs "ddw:") keep the two id spaces from ever
|
|
44
|
+
* colliding in the same store.
|
|
45
|
+
*
|
|
46
|
+
* Content is hashed only from the diagnostic's own line (for the strict
|
|
47
|
+
* anchor), not a surrounding window as #181's original sketch considered.
|
|
48
|
+
* Two diagnostics on the same file/tool/rule/message whose flagged line
|
|
49
|
+
* happens to have identical content collide on the SAME strict anchor —
|
|
50
|
+
* deliberately: identical content at the same rule/message is a semantically
|
|
51
|
+
* equivalent finding, so marking one intentionally marks all of them (e.g. a
|
|
52
|
+
* copy-pasted line repeated a few times in the same file). If that
|
|
53
|
+
* assumption proves wrong in practice, a surrounding-window hash can be
|
|
54
|
+
* layered on later without changing the store shape.
|
|
55
|
+
*/
|
|
56
|
+
import { createHash } from "node:crypto";
|
|
57
|
+
import * as fs from "node:fs";
|
|
58
|
+
import * as path from "node:path";
|
|
59
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
60
|
+
import { logDispositionEvent } from "./disposition-logger.js";
|
|
61
|
+
import { publishDisposition } from "./disposition-publish.js";
|
|
62
|
+
import { getProjectDataDir } from "./file-utils.js";
|
|
63
|
+
import { normalizeMapKey } from "./path-utils.js";
|
|
64
|
+
import { lineContentHash } from "./read-guard.js";
|
|
65
|
+
// "defer" is session-ephemeral by design (#690) — held only in memory so it
|
|
66
|
+
// resurfaces for free on the next process run, with no expiry/pruning logic
|
|
67
|
+
// needed. Stores WEAK anchors (see module doc) so a deferred finding stays
|
|
68
|
+
// hidden all session even if the flagged line itself is edited.
|
|
69
|
+
const deferredThisSession = new Set();
|
|
70
|
+
/** Exported (#802) so lens-diagnostic-mark's cross-check against live widget
|
|
71
|
+
* diagnostics matches a message the same way anchor derivation does — a
|
|
72
|
+
* second, slightly different normalizer would make a real match invisible. */
|
|
73
|
+
export function normalizeMessage(message) {
|
|
74
|
+
return message.replace(/\s+/g, " ").trim().toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
function relativeFile(filePath, cwd) {
|
|
77
|
+
const rel = path.relative(cwd, filePath).replace(/\\/g, "/");
|
|
78
|
+
return rel && !rel.startsWith("..") ? rel : normalizeMapKey(filePath);
|
|
79
|
+
}
|
|
80
|
+
/** Site-specific anchor — see module doc. Used only for false-positive. */
|
|
81
|
+
export function computeStrictAnchor(args) {
|
|
82
|
+
const lines = args.content?.split(/\r?\n/);
|
|
83
|
+
const lineText = args.line !== undefined && lines ? (lines[args.line - 1] ?? "") : "";
|
|
84
|
+
const parts = [
|
|
85
|
+
relativeFile(args.filePath, args.cwd),
|
|
86
|
+
args.tool ?? "",
|
|
87
|
+
args.rule ?? "",
|
|
88
|
+
normalizeMessage(args.message),
|
|
89
|
+
lineContentHash(lineText),
|
|
90
|
+
];
|
|
91
|
+
return `dd:${createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12)}`;
|
|
92
|
+
}
|
|
93
|
+
/** Intent-level anchor — see module doc. Used for defer/flagged/suppress. */
|
|
94
|
+
export function computeWeakAnchor(args) {
|
|
95
|
+
const parts = [
|
|
96
|
+
relativeFile(args.filePath, args.cwd),
|
|
97
|
+
args.tool ?? "",
|
|
98
|
+
args.rule ?? "",
|
|
99
|
+
normalizeMessage(args.message),
|
|
100
|
+
];
|
|
101
|
+
return `ddw:${createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12)}`;
|
|
102
|
+
}
|
|
103
|
+
/** Both anchors a stored/filtered diagnostic would compute — the one shared
|
|
104
|
+
* derivation both the dispatch-pipeline filter and lens-diagnostics' flagged
|
|
105
|
+
* tag lookup must use so a mark and a fresh diagnostic converge on the same
|
|
106
|
+
* ids. */
|
|
107
|
+
export function anchorsForDiagnostic(cwd, filePath, diagnostic, content) {
|
|
108
|
+
const args = {
|
|
109
|
+
cwd,
|
|
110
|
+
filePath,
|
|
111
|
+
tool: diagnostic.tool,
|
|
112
|
+
rule: diagnostic.rule,
|
|
113
|
+
message: diagnostic.message,
|
|
114
|
+
line: diagnostic.line,
|
|
115
|
+
content,
|
|
116
|
+
};
|
|
117
|
+
return { strict: computeStrictAnchor(args), weak: computeWeakAnchor(args) };
|
|
118
|
+
}
|
|
119
|
+
function statePath(cwd) {
|
|
120
|
+
return path.join(getProjectDataDir(cwd), "cache", "diagnostic-dispositions.json");
|
|
121
|
+
}
|
|
122
|
+
let stateCache = null;
|
|
123
|
+
function readState(cwd) {
|
|
124
|
+
const p = statePath(cwd);
|
|
125
|
+
let stat;
|
|
126
|
+
try {
|
|
127
|
+
stat = fs.statSync(p);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
if (stateCache && stateCache.path === p && stateCache.missing) {
|
|
131
|
+
return stateCache.state;
|
|
132
|
+
}
|
|
133
|
+
const empty = {};
|
|
134
|
+
stateCache = { path: p, missing: true, mtimeMs: -1, size: -1, state: empty };
|
|
135
|
+
return empty;
|
|
136
|
+
}
|
|
137
|
+
if (stateCache &&
|
|
138
|
+
stateCache.path === p &&
|
|
139
|
+
!stateCache.missing &&
|
|
140
|
+
stateCache.mtimeMs === stat.mtimeMs &&
|
|
141
|
+
stateCache.size === stat.size) {
|
|
142
|
+
return stateCache.state;
|
|
143
|
+
}
|
|
144
|
+
let state;
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
147
|
+
state =
|
|
148
|
+
parsed && typeof parsed === "object" ? parsed : {};
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Now that writeState is tmp+rename atomic, a torn read (another process
|
|
152
|
+
// mid-write) can no longer land here — this only fires on genuine
|
|
153
|
+
// corruption/wrong-shape content. Caching `{}` against this stat is still
|
|
154
|
+
// correct, not a permanent trap: any future rewrite of the file (a fix,
|
|
155
|
+
// or this process's own next writeState) changes mtime/size, which
|
|
156
|
+
// invalidates the cache below on the next readState call. Only a file
|
|
157
|
+
// that never changes again would serve empty state forever — and
|
|
158
|
+
// reparsing the same invalid bytes every hot-path call would yield the
|
|
159
|
+
// same `{}` anyway, so the cache costs nothing in that case.
|
|
160
|
+
state = {};
|
|
161
|
+
}
|
|
162
|
+
stateCache = {
|
|
163
|
+
path: p,
|
|
164
|
+
missing: false,
|
|
165
|
+
mtimeMs: stat.mtimeMs,
|
|
166
|
+
size: stat.size,
|
|
167
|
+
state,
|
|
168
|
+
};
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
// Atomic tmp+rename via clients/atomic-write.ts (#762; shared with
|
|
172
|
+
// instance-registry.ts / recent-touches.ts / review-graph/builder.ts): a
|
|
173
|
+
// cross-process reader must never observe a partially-written file —
|
|
174
|
+
// rename() replaces the destination atomically on both POSIX and Windows
|
|
175
|
+
// (libuv uses MOVEFILE_REPLACE_EXISTING), so a concurrent readState sees
|
|
176
|
+
// either the old JSON or the new JSON, never a torn write that fails to
|
|
177
|
+
// parse. Unlike those best-effort writers, `bestEffort: false` here means a
|
|
178
|
+
// failure still propagates (matches the pre-atomic writeFileSync's behavior,
|
|
179
|
+
// which never swallowed errors either) — a disposition mark silently vanishing
|
|
180
|
+
// is a correctness bug for this store, not just a lost observability sample.
|
|
181
|
+
function writeState(cwd, state) {
|
|
182
|
+
const p = statePath(cwd);
|
|
183
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
184
|
+
writeFileAtomic(p, JSON.stringify(state, null, 2), { bestEffort: false });
|
|
185
|
+
// Refresh the cache from the write we just did instead of invalidating it —
|
|
186
|
+
// avoids an immediate re-stat+re-parse of the file we already have in hand,
|
|
187
|
+
// and guards against coarse filesystem mtime granularity making a
|
|
188
|
+
// read-immediately-after-write look like a cache hit on stale data.
|
|
189
|
+
const stat = fs.statSync(p);
|
|
190
|
+
stateCache = {
|
|
191
|
+
path: p,
|
|
192
|
+
missing: false,
|
|
193
|
+
mtimeMs: stat.mtimeMs,
|
|
194
|
+
size: stat.size,
|
|
195
|
+
state,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Test-only escape hatch — the state cache is module-level, so tests that
|
|
199
|
+
* write the store file out-of-band (or across separate cwds sharing a stat
|
|
200
|
+
* coincidence) need to reset it between cases. */
|
|
201
|
+
export function _resetStateCacheForTests() {
|
|
202
|
+
stateCache = null;
|
|
203
|
+
}
|
|
204
|
+
/** Fire-and-forget mark telemetry (see markDisposition's doc): the NDJSON log
|
|
205
|
+
* entry (project-relative path — rule-tuning data, not machine layout) and
|
|
206
|
+
* the bus event (absolute normalized path — in-process consumers navigate).
|
|
207
|
+
* Neither can throw into the mark path; both are already internally
|
|
208
|
+
* fail-safe, but the try/catch keeps a future regression in either from
|
|
209
|
+
* breaking a mark. */
|
|
210
|
+
function emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing) {
|
|
211
|
+
try {
|
|
212
|
+
logDispositionEvent({
|
|
213
|
+
event: "mark",
|
|
214
|
+
disposition,
|
|
215
|
+
tool: target.tool,
|
|
216
|
+
rule: target.rule,
|
|
217
|
+
filePath: relativeFile(target.filePath, cwd),
|
|
218
|
+
line: target.line,
|
|
219
|
+
reason,
|
|
220
|
+
anchor,
|
|
221
|
+
previousDisposition: existing?.disposition,
|
|
222
|
+
});
|
|
223
|
+
publishDisposition({
|
|
224
|
+
cwd,
|
|
225
|
+
filePath: target.filePath,
|
|
226
|
+
disposition,
|
|
227
|
+
tool: target.tool,
|
|
228
|
+
rule: target.rule,
|
|
229
|
+
line: target.line,
|
|
230
|
+
anchor,
|
|
231
|
+
reason,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// never let telemetry break a mark
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Record a disposition. Picks the anchor flavor per-disposition (see module
|
|
240
|
+
* doc): strict for false-positive, weak for everything else. Returns the
|
|
241
|
+
* anchor actually used, so callers (the mark tool) can report/verify it.
|
|
242
|
+
*
|
|
243
|
+
* This is THE single choke point for mark telemetry — the NDJSON log
|
|
244
|
+
* (disposition-logger.ts, #181's FP-rule-tuning signal) and the
|
|
245
|
+
* `pilens:diagnostic:disposition` bus event (disposition-publish.ts) both
|
|
246
|
+
* hang off it, so the agent tool and any future UI caller are covered without
|
|
247
|
+
* per-caller wiring.
|
|
248
|
+
*/
|
|
249
|
+
export function markDisposition(cwd, target, disposition, reason) {
|
|
250
|
+
const anchor = disposition === "false-positive"
|
|
251
|
+
? computeStrictAnchor(target)
|
|
252
|
+
: computeWeakAnchor(target);
|
|
253
|
+
// Captured for BOTH branches: a defer never writes the store, but a store
|
|
254
|
+
// entry can already exist at the same weak anchor (a prior flagged/suppress
|
|
255
|
+
// mark) — the log should record what this mark shadowed either way.
|
|
256
|
+
const existing = readState(cwd).dispositions?.[anchor];
|
|
257
|
+
emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing);
|
|
258
|
+
if (disposition === "defer") {
|
|
259
|
+
deferredThisSession.add(anchor);
|
|
260
|
+
return anchor;
|
|
261
|
+
}
|
|
262
|
+
const state = readState(cwd);
|
|
263
|
+
state.dispositions ??= {};
|
|
264
|
+
const now = new Date().toISOString();
|
|
265
|
+
const capturesFixContext = disposition === "flagged";
|
|
266
|
+
const lineText = capturesFixContext
|
|
267
|
+
? (target.content?.split(/\r?\n/)[target.line !== undefined ? target.line - 1 : -1] ?? existing?.lineText)?.trim()
|
|
268
|
+
: existing?.lineText;
|
|
269
|
+
state.dispositions[anchor] = {
|
|
270
|
+
disposition,
|
|
271
|
+
reason: reason ?? existing?.reason,
|
|
272
|
+
createdAt: existing?.createdAt ?? now,
|
|
273
|
+
lastSeenAt: now,
|
|
274
|
+
line: capturesFixContext ? (target.line ?? existing?.line) : existing?.line,
|
|
275
|
+
lineText,
|
|
276
|
+
};
|
|
277
|
+
writeState(cwd, state);
|
|
278
|
+
return anchor;
|
|
279
|
+
}
|
|
280
|
+
export function getDisposition(cwd, anchor) {
|
|
281
|
+
return readState(cwd).dispositions?.[anchor];
|
|
282
|
+
}
|
|
283
|
+
export function isDeferredThisSession(anchor) {
|
|
284
|
+
return deferredThisSession.has(anchor);
|
|
285
|
+
}
|
|
286
|
+
/** Test-only escape hatch — defer state is module-level (one process = one
|
|
287
|
+
* session), so tests need to reset it between cases. */
|
|
288
|
+
export function _resetDeferredForTests() {
|
|
289
|
+
deferredThisSession.clear();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Drop diagnostics disposed false-positive/suppress, or deferred this session,
|
|
293
|
+
* from `diagnostics`. `flagged` diagnostics are kept as-is — callers that want
|
|
294
|
+
* to surface the flag (e.g. lens_diagnostics' rendering) look it up separately
|
|
295
|
+
* via getDisposition on the WEAK anchor (anchorsForDiagnostic(...).weak).
|
|
296
|
+
*
|
|
297
|
+
* Computes both anchors per diagnostic (cheap — same hash primitive, twice)
|
|
298
|
+
* since false-positive is keyed strict while defer/suppress are keyed weak;
|
|
299
|
+
* see module doc for why each disposition binds the way it does.
|
|
300
|
+
*/
|
|
301
|
+
export function applyDispositions(diagnostics, cwd, filePath, content) {
|
|
302
|
+
if (!diagnostics.length)
|
|
303
|
+
return diagnostics;
|
|
304
|
+
const dispositions = readState(cwd).dispositions;
|
|
305
|
+
if (!dispositions && deferredThisSession.size === 0)
|
|
306
|
+
return diagnostics;
|
|
307
|
+
return diagnostics.filter((d) => {
|
|
308
|
+
const { strict, weak } = anchorsForDiagnostic(cwd, filePath, d, content);
|
|
309
|
+
if (deferredThisSession.has(weak))
|
|
310
|
+
return false;
|
|
311
|
+
if (dispositions?.[strict]?.disposition === "false-positive")
|
|
312
|
+
return false;
|
|
313
|
+
// Belt-and-braces: the inline `pi-lens-ignore` comment is the real
|
|
314
|
+
// suppress enforcement (see suppress-writer.ts) and normally already
|
|
315
|
+
// dropped this finding upstream via applyInlineSuppressions. This is a
|
|
316
|
+
// harmless second cover for the store-only audit trail case.
|
|
317
|
+
if (dispositions?.[weak]?.disposition === "suppress")
|
|
318
|
+
return false;
|
|
319
|
+
return true;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* WEAK-anchor-only disposition filter for the "instant" (cache-only)
|
|
324
|
+
* lens_diagnostics modes (delta/all). Drops diagnostics disposed `suppress`
|
|
325
|
+
* or deferred this session — both WEAK-anchored (`file|tool|rule|message`, no
|
|
326
|
+
* line-content hash; see module doc), so this needs ZERO file I/O: it computes
|
|
327
|
+
* only the weak anchor and never touches the diagnostic's line content.
|
|
328
|
+
*
|
|
329
|
+
* `false-positive` is deliberately NOT filtered here: it is STRICT-anchored,
|
|
330
|
+
* which requires the flagged line's content to re-derive its hash, and reading
|
|
331
|
+
* every findings file just for that would defeat the instant contract of these
|
|
332
|
+
* cache-only modes. A false-positive mark still filters at the next per-edit
|
|
333
|
+
* dispatch (`dispatcher.ts`) and in `mode=full`'s merge — both of which already
|
|
334
|
+
* have file content in hand and call `applyDispositions` (the full,
|
|
335
|
+
* content-based filter). suppress/defer, being intent-level and weak-anchored,
|
|
336
|
+
* are the marks that must apply the instant a query re-serves cached findings,
|
|
337
|
+
* and they do so here without any read.
|
|
338
|
+
*/
|
|
339
|
+
export function applyWeakDispositions(diagnostics, cwd, filePath) {
|
|
340
|
+
if (!diagnostics.length)
|
|
341
|
+
return diagnostics;
|
|
342
|
+
const dispositions = readState(cwd).dispositions;
|
|
343
|
+
if (!dispositions && deferredThisSession.size === 0)
|
|
344
|
+
return diagnostics;
|
|
345
|
+
return diagnostics.filter((d) => {
|
|
346
|
+
const weak = computeWeakAnchor({
|
|
347
|
+
cwd,
|
|
348
|
+
filePath,
|
|
349
|
+
tool: d.tool,
|
|
350
|
+
rule: d.rule,
|
|
351
|
+
message: d.message,
|
|
352
|
+
line: d.line,
|
|
353
|
+
});
|
|
354
|
+
if (deferredThisSession.has(weak))
|
|
355
|
+
return false;
|
|
356
|
+
if (dispositions?.[weak]?.disposition === "suppress")
|
|
357
|
+
return false;
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* `role:"auxiliary"` (clients/lsp/server.ts) + one profile entry here.
|
|
18
18
|
*/
|
|
19
19
|
import { shouldDegradeAuxiliaryLsp } from "../lsp-budget.js";
|
|
20
|
+
import { isSubagentSession } from "../subagent-mode.js";
|
|
20
21
|
import { findLocalOpengrepConfig } from "../opengrep-config.js";
|
|
21
22
|
import { findLocalTyposConfig } from "../typos-config.js";
|
|
22
23
|
import { findLocalZizmorConfig } from "../zizmor-config.js";
|
|
@@ -89,6 +90,11 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
89
90
|
sourceMatch: /ast[-_]?grep/i,
|
|
90
91
|
killSwitchFlag: "no-ast-grep",
|
|
91
92
|
enabledByDefault: true,
|
|
93
|
+
// Matches the in-process ast-grep-napi runner's own `skipTestFiles: true`
|
|
94
|
+
// (#687) — that flag stops applying the moment the ast-grep binary is
|
|
95
|
+
// present, since the napi runner then skips entirely in favor of this
|
|
96
|
+
// LSP surface, which had no test-file gating of its own.
|
|
97
|
+
skipTestFiles: true,
|
|
92
98
|
// The ast-grep LSP runs either the repo's own sgconfig (when present) or
|
|
93
99
|
// pi-lens's shipped baseline sgconfig. In both cases the rule severity is
|
|
94
100
|
// deliberate, so preserve ast-grep's severity semantics: ERROR can block,
|
|
@@ -144,7 +150,12 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
144
150
|
* per-file: once over budget, this session never spawns its auxiliary fleet,
|
|
145
151
|
* rather than flip-flopping file to file. */
|
|
146
152
|
export function enabledAuxiliaryLspServerIds(getFlag) {
|
|
147
|
-
|
|
153
|
+
// #449 slice 2 (budget): skip auxiliaries when machine-wide LSP budget is
|
|
154
|
+
// exceeded. #713 (subagent light mode): reuse the same seam — a subagent
|
|
155
|
+
// session also skips auxiliaries; the parent session already runs them on
|
|
156
|
+
// the same cwd. PI_LENS_SUBAGENT_FULL=1 restores full behavior via
|
|
157
|
+
// isSubagentSession() returning false.
|
|
158
|
+
if (shouldDegradeAuxiliaryLsp() || isSubagentSession())
|
|
148
159
|
return [];
|
|
149
160
|
return AUXILIARY_LSP_PROFILES.flatMap((p) => p.enabledByDefault &&
|
|
150
161
|
!(p.killSwitchFlag && getFlag(p.killSwitchFlag) === true)
|
|
@@ -194,7 +205,78 @@ export function isAuxiliaryDiagnosticSuppressed(d, content) {
|
|
|
194
205
|
* equivalent) suppresses a finding identically whether it's seen via the
|
|
195
206
|
* per-edit dispatch runner or a standalone diagnostics query — previously
|
|
196
207
|
* only the former honored it (#586).
|
|
208
|
+
*
|
|
209
|
+
* #692: `opts.fileRole` additionally drops a diagnostic whose auxiliary
|
|
210
|
+
* profile declares `skipTestFiles` (e.g. ast-grep, #687) when the file is a
|
|
211
|
+
* test file — the per-edit dispatch runner (`clients/dispatch/runners/lsp.ts`)
|
|
212
|
+
* has applied this gate since #687/#688 via its own inline check; the
|
|
213
|
+
* `runWorkspaceDiagnostics` workspace sweep (`clients/lsp/index.ts`) called
|
|
214
|
+
* this function WITHOUT ever passing a fileRole, so ast-grep findings on test
|
|
215
|
+
* files that are suppressed per-edit reappeared wholesale in every
|
|
216
|
+
* `mode=full` sweep. Omitting `opts` (or `opts.fileRole`) keeps every existing
|
|
217
|
+
* 2-arg call site's behavior byte-for-byte unchanged.
|
|
218
|
+
*/
|
|
219
|
+
export function applyAuxiliarySuppressions(diagnostics, content, opts) {
|
|
220
|
+
return diagnostics.filter((d) => {
|
|
221
|
+
if (isAuxiliaryDiagnosticSuppressed(d, content))
|
|
222
|
+
return false;
|
|
223
|
+
if (opts?.fileRole === "test") {
|
|
224
|
+
const profile = findAuxiliaryProfileForSource(d.source);
|
|
225
|
+
if (profile?.skipTestFiles)
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* #692: shared aux re-tag implementation — extracted from the per-edit
|
|
233
|
+
* dispatch runner (`clients/dispatch/runners/lsp.ts`) so a scan/sweep path
|
|
234
|
+
* that reconciles LSP diagnostics into widget state (`lens_diagnostics
|
|
235
|
+
* mode=full`, `lsp_diagnostics`) gives its auxiliary-sourced findings
|
|
236
|
+
* (ast-grep, opengrep, zizmor, typos) the SAME tool re-tag, semantic policy,
|
|
237
|
+
* and defect-class classification the per-edit path always has — previously
|
|
238
|
+
* only the per-edit runner ran this loop, so a scan-reconciled aux finding
|
|
239
|
+
* kept `tool: "lsp"` and lost its curated-repo-rules `blockingAllowed`
|
|
240
|
+
* context entirely (#692).
|
|
241
|
+
*
|
|
242
|
+
* `diagnostics` and `rawLspDiags` must be the SAME length and index-aligned —
|
|
243
|
+
* `convertLspDiagnostics` maps its input 1:1, so this holds for every caller
|
|
244
|
+
* that passes it the same array it just converted. Mutates and returns the
|
|
245
|
+
* SURVIVING subset of `diagnostics` (native-inline-suppressed and
|
|
246
|
+
* `skipTestFiles`-dropped entries removed), mirroring the per-edit runner's
|
|
247
|
+
* prior inline behavior exactly.
|
|
197
248
|
*/
|
|
198
|
-
export function
|
|
199
|
-
|
|
249
|
+
export function retagAuxiliaryDiagnostics(diagnostics, rawLspDiags, content, ctx) {
|
|
250
|
+
const blockingAllowedByProfile = new Map();
|
|
251
|
+
const suppressedIndices = new Set();
|
|
252
|
+
for (let i = 0; i < diagnostics.length; i++) {
|
|
253
|
+
const profile = findAuxiliaryProfileForSource(rawLspDiags[i]?.source);
|
|
254
|
+
if (!profile)
|
|
255
|
+
continue;
|
|
256
|
+
if (profile.skipTestFiles && ctx.fileRole === "test") {
|
|
257
|
+
suppressedIndices.add(i);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (isAuxiliaryDiagnosticSuppressed(rawLspDiags[i], content)) {
|
|
261
|
+
suppressedIndices.add(i);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
let blockingAllowed = blockingAllowedByProfile.get(profile);
|
|
265
|
+
if (blockingAllowed === undefined) {
|
|
266
|
+
blockingAllowed = profile.allowBlocking?.(ctx.cwd) ?? false;
|
|
267
|
+
blockingAllowedByProfile.set(profile, blockingAllowed);
|
|
268
|
+
}
|
|
269
|
+
const d = diagnostics[i];
|
|
270
|
+
d.tool = profile.tool;
|
|
271
|
+
d.semantic = profile.semantic(rawLspDiags[i], { blockingAllowed });
|
|
272
|
+
if (d.semantic !== "blocking" && d.severity === "error") {
|
|
273
|
+
d.severity = "warning";
|
|
274
|
+
}
|
|
275
|
+
const defectClass = profile.defectClass?.(rawLspDiags[i]);
|
|
276
|
+
if (defectClass)
|
|
277
|
+
d.defectClass = defectClass;
|
|
278
|
+
}
|
|
279
|
+
return suppressedIndices.size
|
|
280
|
+
? diagnostics.filter((_, i) => !suppressedIndices.has(i))
|
|
281
|
+
: diagnostics;
|
|
200
282
|
}
|
|
@@ -27,6 +27,7 @@ import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
|
27
27
|
import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
|
|
28
28
|
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
29
29
|
import { classifyDiagnostic } from "./diagnostic-taxonomy.js";
|
|
30
|
+
import { applyDispositions } from "../diagnostic-dispositions.js";
|
|
30
31
|
import { applyInlineSuppressions } from "./inline-suppressions.js";
|
|
31
32
|
import { getToolPlan } from "./plan.js";
|
|
32
33
|
import { resolveRunnerPath } from "./runner-context.js";
|
|
@@ -559,7 +560,11 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
|
|
|
559
560
|
const overlapSuppressed = suppressLintOverlapsWithLsp(dockerOverlapSuppressed);
|
|
560
561
|
const fileContent = ctx.facts.getFileFact(ctx.filePath, "file.content") ?? "";
|
|
561
562
|
const inlineSuppressed = applyInlineSuppressions(overlapSuppressed, fileContent);
|
|
562
|
-
|
|
563
|
+
// #690: agent/user disposition layer — drop false-positive/suppress marks
|
|
564
|
+
// and anything deferred this session. flagged marks are left in place;
|
|
565
|
+
// lens_diagnostics tags them at render time via the same anchor.
|
|
566
|
+
const dispositionFiltered = applyDispositions(inlineSuppressed, ctx.cwd, ctx.filePath, fileContent);
|
|
567
|
+
let visibleDiagnostics = dispositionFiltered;
|
|
563
568
|
let resolvedCount = 0;
|
|
564
569
|
if (ctx.deltaMode && previousBaseline) {
|
|
565
570
|
const filtered = filterDelta(visibleDiagnostics, previousBaseline, (d) => d.id);
|
|
@@ -19,7 +19,8 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { formatCascadeNeighborDiagnostics } from "../cascade-format.js";
|
|
20
20
|
import { logCascade } from "../cascade-logger.js";
|
|
21
21
|
import { getDiagnosticTracker } from "../diagnostic-tracker.js";
|
|
22
|
-
import {
|
|
22
|
+
import { isTierAwareCascadeEnabled, recordOutstandingCascadeTouch, } from "../lsp/cascade-tier.js";
|
|
23
|
+
import { classifyCascadeWaitTier } from "../lsp/wait-policy/index.js";
|
|
23
24
|
import { getServersForFileWithConfig } from "../lsp/config.js";
|
|
24
25
|
import { getLSPService } from "../lsp/index.js";
|
|
25
26
|
import { isExternalOrVendorFile, normalizeMapKey } from "../path-utils.js";
|
|
@@ -821,7 +822,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
821
822
|
coldSnapshotPaths.push(neighborPath);
|
|
822
823
|
continue;
|
|
823
824
|
}
|
|
824
|
-
|
|
825
|
+
// #692: `source: "cascade"` used to be passed here to label `rule`
|
|
826
|
+
// (`cascade:<code>`) — that override is gone (identity must come from
|
|
827
|
+
// the diagnostic's own source; see `scanOrigin`'s doc comment), and
|
|
828
|
+
// cascade neighbor diagnostics are ephemeral display-only output
|
|
829
|
+
// (never reconciled into persisted widget/dedup state), so the label
|
|
830
|
+
// had no remaining purpose and is simply dropped rather than migrated.
|
|
831
|
+
const diags = convertLspDiagnostics(entry.diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
|
|
825
832
|
producedLspData = true;
|
|
826
833
|
const durationMs = Date.now() - neighborStart;
|
|
827
834
|
logCascade({
|
|
@@ -994,7 +1001,11 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
994
1001
|
});
|
|
995
1002
|
if (!rawDiags)
|
|
996
1003
|
return undefined;
|
|
997
|
-
|
|
1004
|
+
// #692: `source: "cascade"` no longer overrides `rule` (see the
|
|
1005
|
+
// doc comment on the sibling call above) — dropped rather than
|
|
1006
|
+
// migrated to `scanOrigin` since cascade output never touches
|
|
1007
|
+
// persisted widget/dedup state.
|
|
1008
|
+
const diags = convertLspDiagnostics(rawDiags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), neighborPath);
|
|
998
1009
|
const durationMs = Date.now() - neighborStart;
|
|
999
1010
|
// Update cache for this neighbor at the current write sequence
|
|
1000
1011
|
if (writeSeq != null) {
|
|
@@ -1051,10 +1062,13 @@ export async function computeCascadeForFile(filePath, cwd, options = {}) {
|
|
|
1051
1062
|
error: String(result.reason),
|
|
1052
1063
|
});
|
|
1053
1064
|
const entry = allDiags.get(normalizeMapKey(neighborPath));
|
|
1065
|
+
// #692: `source: "cascade"` dropped (see the doc comment above the
|
|
1066
|
+
// first cascade call site in this file) — no longer affects `rule`
|
|
1067
|
+
// and cascade output never touches persisted widget/dedup state.
|
|
1054
1068
|
const diags = entry && Date.now() - entry.ts < CASCADE_TTL_MS
|
|
1055
1069
|
? convertLspDiagnostics(entry.diags
|
|
1056
1070
|
.filter((d) => d.severity === 1)
|
|
1057
|
-
.slice(0, MAX_PER_FILE), neighborPath
|
|
1071
|
+
.slice(0, MAX_PER_FILE), neighborPath)
|
|
1058
1072
|
: [];
|
|
1059
1073
|
neighbors.push({
|
|
1060
1074
|
filePath: neighborPath,
|
|
@@ -1162,7 +1176,9 @@ function appendFallbackNeighbors(neighbors, allDiags, normalizedFileKey, cwd) {
|
|
|
1162
1176
|
continue;
|
|
1163
1177
|
if (now - ts > CASCADE_TTL_MS)
|
|
1164
1178
|
continue;
|
|
1165
|
-
|
|
1179
|
+
// #692: `source: "cascade"` dropped — see the doc comment above the
|
|
1180
|
+
// first cascade `convertLspDiagnostics` call site in this file.
|
|
1181
|
+
const errors = convertLspDiagnostics(diags.filter((d) => d.severity === 1).slice(0, MAX_PER_FILE), diagPath);
|
|
1166
1182
|
if (errors.length === 0)
|
|
1167
1183
|
continue;
|
|
1168
1184
|
neighbors.push({
|