pi-lens 3.8.71 → 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 +106 -1
- package/README.md +8 -0
- package/config/markdownlint/core.json +6 -0
- package/dist/clients/atomic-write.js +83 -0
- package/dist/clients/config-enabled-shape.js +39 -0
- package/dist/clients/diagnostic-dispositions.js +64 -2
- package/dist/clients/dispatch/integration.js +2 -1
- package/dist/clients/dispatch/runners/lsp.js +54 -25
- package/dist/clients/dispatch/runners/markdownlint.js +7 -4
- package/dist/clients/dispatch/runners/pyright.js +1 -0
- package/dist/clients/dispatch/runners/tree-sitter.js +1 -1
- package/dist/clients/env-utils.js +38 -0
- package/dist/clients/file-utils.js +21 -3
- package/dist/clients/instance-registry.js +20 -20
- package/dist/clients/jscpd-client.js +52 -39
- package/dist/clients/language-profile.js +12 -19
- package/dist/clients/lens-config.js +106 -27
- package/dist/clients/lens-engine.js +129 -3
- package/dist/clients/lsp/cascade-tier.js +1 -38
- package/dist/clients/lsp/client.js +3 -1
- package/dist/clients/lsp/index.js +621 -95
- package/dist/clients/lsp/server-strategies.js +3 -224
- package/dist/clients/lsp/server.js +1 -1
- 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-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 +2 -2
- package/dist/clients/pipeline.js +11 -6
- 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/recent-touches.js +6 -11
- package/dist/clients/review-graph/builder.js +170 -13
- package/dist/clients/review-graph/import-resolvers.js +149 -13
- 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 +104 -17
- package/dist/clients/runtime-config.js +4 -0
- package/dist/clients/runtime-coordinator.js +50 -1
- package/dist/clients/runtime-session.js +41 -3
- package/dist/clients/runtime-tool-result.js +4 -3
- package/dist/clients/runtime-turn.js +4 -1
- package/dist/clients/safe-spawn.js +264 -40
- package/dist/clients/session-lifecycle.js +36 -0
- package/dist/clients/session-state-store.js +8 -3
- package/dist/clients/source-filter.js +136 -61
- package/dist/clients/source-walker.js +89 -0
- package/dist/clients/startup-scan.js +189 -111
- 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/warm-attach.js +204 -0
- package/dist/clients/word-index.js +36 -5
- package/dist/clients/workspace-topology.js +266 -0
- package/dist/index.js +13826 -10251
- package/dist/mcp/server.js +125 -10
- package/dist/tools/ast-grep-search.js +21 -0
- package/dist/tools/lens-diagnostic-mark.js +198 -7
- package/dist/tools/lens-diagnostics.js +119 -21
- package/dist/tools/lsp-diagnostics.js +17 -1
- 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/environment-variables.md +10 -0
- package/docs/features.md +7 -1
- package/docs/globalconfig.md +70 -0
- package/docs/servercapabilities.md +1 -1
- package/docs/usage.md +13 -2
- package/package.json +1 -1
- package/scripts/download-grammars.js +1 -0
|
@@ -1,226 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Codifies known server behavior so timing decisions (debounce, retry budget,
|
|
5
|
-
* first-push seeding) are automatic rather than one-size-fits-all.
|
|
6
|
-
*
|
|
7
|
-
* Env var overrides (PI_LENS_LSP_*) always take precedence over strategy values.
|
|
2
|
+
* @deprecated Import wait policy from `./wait-policy/index.js`.
|
|
3
|
+
* Compatibility shim; remove after downstream imports have migrated.
|
|
8
4
|
*/
|
|
9
|
-
|
|
10
|
-
const _require = createRequire(import.meta.url);
|
|
11
|
-
/**
|
|
12
|
-
* Platform/arch → `@ast-grep/cli-<platform>-<arch>[-msvc|-gnu]` native
|
|
13
|
-
* package name, mirroring @ast-grep/cli's own `optionalDependencies` matrix
|
|
14
|
-
* (checked directly against node_modules/@ast-grep/cli/package.json). Each
|
|
15
|
-
* package ships the native `ast-grep`/`ast-grep.exe` binary at its package
|
|
16
|
-
* root (no `bin/` subdir) — see node_modules/@ast-grep/cli-win32-x64-msvc/.
|
|
17
|
-
*/
|
|
18
|
-
function astGrepNativePackageName(platform, arch) {
|
|
19
|
-
switch (platform) {
|
|
20
|
-
case "win32":
|
|
21
|
-
if (arch === "x64")
|
|
22
|
-
return "@ast-grep/cli-win32-x64-msvc";
|
|
23
|
-
if (arch === "arm64")
|
|
24
|
-
return "@ast-grep/cli-win32-arm64-msvc";
|
|
25
|
-
if (arch === "ia32")
|
|
26
|
-
return "@ast-grep/cli-win32-ia32-msvc";
|
|
27
|
-
return undefined;
|
|
28
|
-
case "darwin":
|
|
29
|
-
if (arch === "arm64")
|
|
30
|
-
return "@ast-grep/cli-darwin-arm64";
|
|
31
|
-
if (arch === "x64")
|
|
32
|
-
return "@ast-grep/cli-darwin-x64";
|
|
33
|
-
return undefined;
|
|
34
|
-
case "linux":
|
|
35
|
-
if (arch === "x64")
|
|
36
|
-
return "@ast-grep/cli-linux-x64-gnu";
|
|
37
|
-
if (arch === "arm64")
|
|
38
|
-
return "@ast-grep/cli-linux-arm64-gnu";
|
|
39
|
-
return undefined;
|
|
40
|
-
default:
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve ast-grep's platform-native exe DIRECTLY, skipping the node-bin
|
|
46
|
-
* wrapper (`ast-grep.cmd`/shim → node → cli.js → spawn native exe). One less
|
|
47
|
-
* orphanable process layer (#472): a wrapper's direct child is the node/cmd
|
|
48
|
-
* process, so on abnormal exit the actual ast-grep binary is a grandchild the
|
|
49
|
-
* #234 teardown path never reaches — resolving straight to the native exe
|
|
50
|
-
* means the LSP's direct child IS the real server.
|
|
51
|
-
*
|
|
52
|
-
* `require.resolve` is wrapped in try/catch (ESM-safe via createRequire, same
|
|
53
|
-
* pattern as clients/deps/ast-grep-napi.ts) — returns undefined so the caller
|
|
54
|
-
* falls back to the existing wrapper-based resolution when the platform
|
|
55
|
-
* package isn't installed (it's an optionalDependency; native builds can be
|
|
56
|
-
* absent on unsupported platforms/arches or a partial install).
|
|
57
|
-
*/
|
|
58
|
-
export function resolveAstGrepNativeExe(platform = process.platform, arch = process.arch) {
|
|
59
|
-
const pkgName = astGrepNativePackageName(platform, arch);
|
|
60
|
-
if (!pkgName)
|
|
61
|
-
return undefined;
|
|
62
|
-
const binaryName = platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
63
|
-
try {
|
|
64
|
-
return _require.resolve(`${pkgName}/${binaryName}`);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
71
|
-
typescript: {
|
|
72
|
-
seedFirstPush: true,
|
|
73
|
-
pullRetryBudgetMs: 0,
|
|
74
|
-
debounceMs: 50,
|
|
75
|
-
aggregateWaitMs: 1000,
|
|
76
|
-
expectSemanticSecondPush: false,
|
|
77
|
-
// Tier 3 (#458): typescript-language-server publishes nothing on a
|
|
78
|
-
// clean→clean edit (docs/lsp-capability-matrix.md, re-confirmed
|
|
79
|
-
// 2026-07-12). It's the lone core-set tier-3 server, which is exactly
|
|
80
|
-
// why the cascade lane's in-lane wait is worth skipping for it
|
|
81
|
-
// specifically. Applies to the CLASSIC server only (#524/#529/#558)
|
|
82
|
-
// — TS7's native `tsc --lsp --stdio` variant shares this
|
|
83
|
-
// "typescript" server id but the 2026-07-12 dual-environment
|
|
84
|
-
// re-measurement found it now publishes on clean (a drift from the
|
|
85
|
-
// #541 measurement); cascade-tier.ts's classifier checks the live
|
|
86
|
-
// snapshot's launchVariant and never applies this flag to a
|
|
87
|
-
// native-ts7 instance.
|
|
88
|
-
silentOnClean: true,
|
|
89
|
-
},
|
|
90
|
-
"rust-analyzer": {
|
|
91
|
-
seedFirstPush: false,
|
|
92
|
-
pullRetryBudgetMs: 500,
|
|
93
|
-
debounceMs: 150,
|
|
94
|
-
aggregateWaitMs: 3000,
|
|
95
|
-
expectSemanticSecondPush: true,
|
|
96
|
-
},
|
|
97
|
-
// PythonServer (pyright / basedpyright) — openFilesOnly mode: lazy per-file
|
|
98
|
-
// analysis, startup similar to jedi. seedFirstPush: true because pyright's
|
|
99
|
-
// first publishDiagnostics after didOpen is the complete result for that file.
|
|
100
|
-
python: {
|
|
101
|
-
seedFirstPush: true,
|
|
102
|
-
pullRetryBudgetMs: 0,
|
|
103
|
-
debounceMs: 100,
|
|
104
|
-
aggregateWaitMs: 1500,
|
|
105
|
-
expectSemanticSecondPush: false,
|
|
106
|
-
},
|
|
107
|
-
// jedi-language-server is push-only (no pull diagnostics) and its FIRST
|
|
108
|
-
// publishDiagnostics is the complete result (seedFirstPush). But that first
|
|
109
|
-
// push lands just after didOpen+~1s on cold start (Python/parso import) —
|
|
110
|
-
// measured ~1011ms — so a 1000ms aggregate budget misses it by a hair and
|
|
111
|
-
// returns zero. 3000ms gives cold-start headroom without stalling the warm path.
|
|
112
|
-
"python-jedi": {
|
|
113
|
-
seedFirstPush: true,
|
|
114
|
-
pullRetryBudgetMs: 0,
|
|
115
|
-
debounceMs: 100,
|
|
116
|
-
aggregateWaitMs: 3000,
|
|
117
|
-
expectSemanticSecondPush: false,
|
|
118
|
-
},
|
|
119
|
-
eslint: {
|
|
120
|
-
seedFirstPush: true,
|
|
121
|
-
pullRetryBudgetMs: 0,
|
|
122
|
-
debounceMs: 200,
|
|
123
|
-
aggregateWaitMs: 2000,
|
|
124
|
-
expectSemanticSecondPush: false,
|
|
125
|
-
},
|
|
126
|
-
// Opengrep security scanner (cross-language LSP). It pushes an EMPTY result
|
|
127
|
-
// during the one-time rule-load window at startup, then the real scan after
|
|
128
|
-
// `semgrep/rulesRefreshed` — so never seed the first push. Push-only (no pull
|
|
129
|
-
// diagnostics). Warm per-file scan ~1.3s; the first touch in a session may
|
|
130
|
-
// also pay rule-load (~3.5s cold). aggregateWaitMs is 3500: it's this
|
|
131
|
-
// server's OWN deadline, bounded by the per-edit caller cap as a ceiling
|
|
132
|
-
// (#242), so on a 2500ms-capped edit opengrep waits min(2500, 3500)=2500
|
|
133
|
-
// and on uncapped paths it gets its full 3500. 3500 covers warm and most
|
|
134
|
-
// cold; a cold scan that overruns isn't lost — late diagnostics are cached
|
|
135
|
-
// and surface on the next edit.
|
|
136
|
-
opengrep: {
|
|
137
|
-
seedFirstPush: false,
|
|
138
|
-
pullRetryBudgetMs: 0,
|
|
139
|
-
debounceMs: 250,
|
|
140
|
-
aggregateWaitMs: 3500,
|
|
141
|
-
expectSemanticSecondPush: false,
|
|
142
|
-
// Opengrep re-scans only on a fresh didOpen — didChange is a no-op for it.
|
|
143
|
-
reopenOnResync: true,
|
|
144
|
-
},
|
|
145
|
-
// ast-grep structural linter (sgconfig-gated auxiliary LSP). Push-only,
|
|
146
|
-
// compiles the project rules on the first scan of a session, and — like
|
|
147
|
-
// Opengrep — is re-synced via didClose+didOpen so edits trigger a re-scan.
|
|
148
|
-
// Conservative budget until measured against real projects (#239).
|
|
149
|
-
// ast-grep re-scans on didChange (verified: toggling the violation count
|
|
150
|
-
// 3→1→4→2 returns the correct fresh count each touch, with matching doc
|
|
151
|
-
// versions), so reopen isn't needed and didChange is the lighter path.
|
|
152
|
-
// aggregateWaitMs is 1800: its warm scan is ~1.3s, so 1000 was under-budgeted
|
|
153
|
-
// (only masked before because the old with-auxiliary deadline was a global
|
|
154
|
-
// max() floor; now each server has its own caller-cap-bounded deadline, #242,
|
|
155
|
-
// so the budget must actually cover the scan).
|
|
156
|
-
"ast-grep": {
|
|
157
|
-
seedFirstPush: false,
|
|
158
|
-
pullRetryBudgetMs: 0,
|
|
159
|
-
debounceMs: 150,
|
|
160
|
-
aggregateWaitMs: 1800,
|
|
161
|
-
expectSemanticSecondPush: false,
|
|
162
|
-
reopenOnResync: false,
|
|
163
|
-
},
|
|
164
|
-
// zizmor (GitHub Actions security scanner, auxiliary LSP #272). Push-only
|
|
165
|
-
// (no pull diagnostics). Its audit set is compiled-in — there's no rule-load
|
|
166
|
-
// window like Opengrep — so the FIRST publishDiagnostics after didOpen IS the
|
|
167
|
-
// complete result: seedFirstPush. It re-scans on didChange (FULL sync), so no
|
|
168
|
-
// reopen-on-resync. A native single-workflow audit is sub-second offline;
|
|
169
|
-
// online mode may add a GitHub-API round-trip, so 2000ms gives headroom
|
|
170
|
-
// (bounded by the per-edit caller cap as a ceiling, #242), and any late online
|
|
171
|
-
// finding is cached and surfaces on the next edit.
|
|
172
|
-
zizmor: {
|
|
173
|
-
seedFirstPush: true,
|
|
174
|
-
pullRetryBudgetMs: 0,
|
|
175
|
-
debounceMs: 150,
|
|
176
|
-
aggregateWaitMs: 2000,
|
|
177
|
-
expectSemanticSecondPush: false,
|
|
178
|
-
reopenOnResync: false,
|
|
179
|
-
},
|
|
180
|
-
// typos (source-code spell checker, auxiliary LSP #283). Push-only (no pull
|
|
181
|
-
// diagnostics). Its dictionary is compiled in — there's NO rule-load window
|
|
182
|
-
// like Opengrep — so the FIRST publishDiagnostics after didOpen IS the
|
|
183
|
-
// complete result: seedFirstPush. It re-scans on didChange (FULL sync), so no
|
|
184
|
-
// reopen-on-resync. A native single-file spell scan is sub-100ms, so the seed
|
|
185
|
-
// arrives near-instantly; aggregateWaitMs is a generous ceiling (bounded by
|
|
186
|
-
// the per-edit caller cap, #242) that the early seed resolves well under.
|
|
187
|
-
typos: {
|
|
188
|
-
seedFirstPush: true,
|
|
189
|
-
pullRetryBudgetMs: 0,
|
|
190
|
-
debounceMs: 150,
|
|
191
|
-
aggregateWaitMs: 1500,
|
|
192
|
-
expectSemanticSecondPush: false,
|
|
193
|
-
reopenOnResync: false,
|
|
194
|
-
},
|
|
195
|
-
// marksman (Markdown LSP, #274). Push-based; native binary so the per-file
|
|
196
|
-
// parse is fast, but its value is CROSS-file (broken intra-repo links,
|
|
197
|
-
// missing anchors/heading refs) which needs the workspace index — so the
|
|
198
|
-
// first push after didOpen can be empty before indexing completes. Don't
|
|
199
|
-
// seed it (like opengrep/rust-analyzer); a modest 1500ms aggregate covers
|
|
200
|
-
// warm edits, and any late cross-file finding surfaces on the next touch.
|
|
201
|
-
marksman: {
|
|
202
|
-
seedFirstPush: false,
|
|
203
|
-
pullRetryBudgetMs: 0,
|
|
204
|
-
debounceMs: 150,
|
|
205
|
-
aggregateWaitMs: 1500,
|
|
206
|
-
expectSemanticSecondPush: false,
|
|
207
|
-
// #645: a full-tree sweep opens every markdown file, all racing the
|
|
208
|
-
// SAME one-time workspace index build — pay the 1500ms budget once
|
|
209
|
-
// per sweep (the first markdown file touched), not once per file.
|
|
210
|
-
// 250ms covers a warm per-file publish (native binary, fast parse)
|
|
211
|
-
// once the index has already had a full aggregateWaitMs window.
|
|
212
|
-
workspaceIndexing: true,
|
|
213
|
-
workspaceIndexingWarmWaitMs: 250,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
/** Fallback for unknown servers. Conservative defaults. */
|
|
217
|
-
export const DEFAULT_STRATEGY = {
|
|
218
|
-
seedFirstPush: false,
|
|
219
|
-
pullRetryBudgetMs: 250,
|
|
220
|
-
debounceMs: 150,
|
|
221
|
-
aggregateWaitMs: 1500,
|
|
222
|
-
expectSemanticSecondPush: false,
|
|
223
|
-
};
|
|
224
|
-
export function getStrategy(serverId) {
|
|
225
|
-
return SERVER_DIAGNOSTIC_STRATEGIES[serverId] ?? DEFAULT_STRATEGY;
|
|
226
|
-
}
|
|
5
|
+
export * from "./wait-policy/strategies.js";
|
|
@@ -20,7 +20,7 @@ import { resolveOpengrepConfig } from "../opengrep-config.js";
|
|
|
20
20
|
import { isZizmorAuditTarget, resolveZizmorGitHubToken } from "../zizmor-config.js";
|
|
21
21
|
import { logLatency } from "../latency-logger.js";
|
|
22
22
|
import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
|
|
23
|
-
import { resolveAstGrepNativeExe } from "./
|
|
23
|
+
import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
|
|
24
24
|
import { isCommandAvailableAsync, safeSpawnAsync } from "../safe-spawn.js";
|
|
25
25
|
import { launchLSP } from "./launch.js";
|
|
26
26
|
import { createLombokJdtlsArgs } from "./lombok.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incumbent LSP wait-tier policy for attached sessions (#822).
|
|
3
|
+
*
|
|
4
|
+
* This policy is applied by the incumbent process on behalf of attached
|
|
5
|
+
* sessions. Keep it free of session-local state: no imports from
|
|
6
|
+
* runtime-session, runtime-turn, or warm-attach.
|
|
7
|
+
*/
|
|
8
|
+
import { getServersForFileWithConfig } from "../config.js";
|
|
9
|
+
import { getStrategy } from "./strategies.js";
|
|
10
|
+
/**
|
|
11
|
+
* Classify a SINGLE server (by id, given its live capability snapshot — or
|
|
12
|
+
* `undefined` when none exists yet) as cascade-lane Tier-3 (push-only,
|
|
13
|
+
* silent-on-clean) or not. This is the per-server primitive both
|
|
14
|
+
* `classifyCascadeWaitTier` (file's PRIMARY server only, the cascade lane's
|
|
15
|
+
* original use) and #814's capability-aware AGGREGATE wait (`touchFile`'s
|
|
16
|
+
* `clientScope: "all"` path, `clients/lsp/index.ts`) share — one
|
|
17
|
+
* classification rule, not two copies that could drift. Ambiguous or missing
|
|
18
|
+
* capability data is always `"waits"` (today's behavior) — this function must
|
|
19
|
+
* never be the reason a real answer gets missed.
|
|
20
|
+
*/
|
|
21
|
+
export function classifyServerWaitTier(serverId, snapshot) {
|
|
22
|
+
if (!snapshot)
|
|
23
|
+
return "waits"; // no live snapshot yet — fail-safe
|
|
24
|
+
const mode = snapshot.workspaceDiagnosticsSupport?.mode;
|
|
25
|
+
if (mode === "pull")
|
|
26
|
+
return "pull-capable";
|
|
27
|
+
if (mode !== "push-only")
|
|
28
|
+
return "waits";
|
|
29
|
+
const strategy = getStrategy(serverId);
|
|
30
|
+
if (strategy.silentOnClean !== true)
|
|
31
|
+
return "waits"; // 2*/unknown push-only
|
|
32
|
+
// #524/#529/#541/#558: `silentOnClean` on a server-id-keyed strategy is
|
|
33
|
+
// only proven against the variant it was actually measured against.
|
|
34
|
+
// "typescript" today means either classic typescript-language-server
|
|
35
|
+
// (confirmed silent-on-clean, 2026-07-12 dual-environment re-measurement)
|
|
36
|
+
// or TS7's native `tsc --lsp --stdio` (the SAME re-measurement found it
|
|
37
|
+
// publishes 2 version-less diagnostic sets on clean — NOT silent, a
|
|
38
|
+
// drift from the earlier #541 measurement). A native-ts7 snapshot must
|
|
39
|
+
// NOT inherit the classic verdict: fall through to "waits", the same
|
|
40
|
+
// ambiguous/fail-safe path an unmarked or non-push-only server already
|
|
41
|
+
// takes. `launchVariant === "classic"` or absent (older snapshots that
|
|
42
|
+
// predate the marker) keeps today's tier-3 behavior exactly.
|
|
43
|
+
if (snapshot.launchVariant === "native-ts7")
|
|
44
|
+
return "waits";
|
|
45
|
+
return "tier3-silent";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify whether `filePath`'s PRIMARY language server is a cascade-lane
|
|
49
|
+
* Tier-3 (push-only, silent-on-clean) server. Ambiguous or missing capability
|
|
50
|
+
* data is always `"waits"` (today's behavior) — this function must never be
|
|
51
|
+
* the reason a real answer gets missed. Thin wrapper over
|
|
52
|
+
* `classifyServerWaitTier` — resolves the file's primary server id/snapshot,
|
|
53
|
+
* then defers to the shared per-server rule.
|
|
54
|
+
*/
|
|
55
|
+
export function classifyCascadeWaitTier(lspService, filePath, snapshots) {
|
|
56
|
+
void lspService; // kept in the signature for call-site clarity/typing only
|
|
57
|
+
const servers = getServersForFileWithConfig(filePath).filter((s) => s.role !== "auxiliary");
|
|
58
|
+
const primary = servers[0];
|
|
59
|
+
if (!primary)
|
|
60
|
+
return "waits";
|
|
61
|
+
const snapshot = snapshots.find((s) => s.serverId === primary.id);
|
|
62
|
+
return classifyServerWaitTier(primary.id, snapshot);
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incumbent-owned LSP wait policy and capability inventory boundary (#822).
|
|
3
|
+
*
|
|
4
|
+
* The incumbent applies this policy when it serves attached sessions. Keep
|
|
5
|
+
* this boundary process-neutral and free of session-local state: do not import
|
|
6
|
+
* runtime-session, runtime-turn, or warm-attach here.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./capability-snapshot.js";
|
|
9
|
+
export * from "./classification.js";
|
|
10
|
+
export * from "./strategies.js";
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incumbent LSP wait policy for attached sessions (#822).
|
|
3
|
+
*
|
|
4
|
+
* The incumbent process applies this policy on behalf of every attached
|
|
5
|
+
* session. Keep this module free of session-local state: it must not import
|
|
6
|
+
* runtime-session, runtime-turn, or warm-attach.
|
|
7
|
+
*
|
|
8
|
+
* Per-Server Diagnostic Strategies for pi-lens LSP
|
|
9
|
+
*
|
|
10
|
+
* Codifies known server behavior so timing decisions (debounce, retry budget,
|
|
11
|
+
* first-push seeding) are automatic rather than one-size-fits-all.
|
|
12
|
+
*
|
|
13
|
+
* Env var overrides (PI_LENS_LSP_*) always take precedence over strategy values.
|
|
14
|
+
*/
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
const _require = createRequire(import.meta.url);
|
|
17
|
+
/**
|
|
18
|
+
* Platform/arch → `@ast-grep/cli-<platform>-<arch>[-msvc|-gnu]` native
|
|
19
|
+
* package name, mirroring @ast-grep/cli's own `optionalDependencies` matrix
|
|
20
|
+
* (checked directly against node_modules/@ast-grep/cli/package.json). Each
|
|
21
|
+
* package ships the native `ast-grep`/`ast-grep.exe` binary at its package
|
|
22
|
+
* root (no `bin/` subdir) — see node_modules/@ast-grep/cli-win32-x64-msvc/.
|
|
23
|
+
*/
|
|
24
|
+
function astGrepNativePackageName(platform, arch) {
|
|
25
|
+
switch (platform) {
|
|
26
|
+
case "win32":
|
|
27
|
+
if (arch === "x64")
|
|
28
|
+
return "@ast-grep/cli-win32-x64-msvc";
|
|
29
|
+
if (arch === "arm64")
|
|
30
|
+
return "@ast-grep/cli-win32-arm64-msvc";
|
|
31
|
+
if (arch === "ia32")
|
|
32
|
+
return "@ast-grep/cli-win32-ia32-msvc";
|
|
33
|
+
return undefined;
|
|
34
|
+
case "darwin":
|
|
35
|
+
if (arch === "arm64")
|
|
36
|
+
return "@ast-grep/cli-darwin-arm64";
|
|
37
|
+
if (arch === "x64")
|
|
38
|
+
return "@ast-grep/cli-darwin-x64";
|
|
39
|
+
return undefined;
|
|
40
|
+
case "linux":
|
|
41
|
+
if (arch === "x64")
|
|
42
|
+
return "@ast-grep/cli-linux-x64-gnu";
|
|
43
|
+
if (arch === "arm64")
|
|
44
|
+
return "@ast-grep/cli-linux-arm64-gnu";
|
|
45
|
+
return undefined;
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve ast-grep's platform-native exe DIRECTLY, skipping the node-bin
|
|
52
|
+
* wrapper (`ast-grep.cmd`/shim → node → cli.js → spawn native exe). One less
|
|
53
|
+
* orphanable process layer (#472): a wrapper's direct child is the node/cmd
|
|
54
|
+
* process, so on abnormal exit the actual ast-grep binary is a grandchild the
|
|
55
|
+
* #234 teardown path never reaches — resolving straight to the native exe
|
|
56
|
+
* means the LSP's direct child IS the real server.
|
|
57
|
+
*
|
|
58
|
+
* `require.resolve` is wrapped in try/catch (ESM-safe via createRequire, same
|
|
59
|
+
* pattern as clients/deps/ast-grep-napi.ts) — returns undefined so the caller
|
|
60
|
+
* falls back to the existing wrapper-based resolution when the platform
|
|
61
|
+
* package isn't installed (it's an optionalDependency; native builds can be
|
|
62
|
+
* absent on unsupported platforms/arches or a partial install).
|
|
63
|
+
*/
|
|
64
|
+
export function resolveAstGrepNativeExe(platform = process.platform, arch = process.arch) {
|
|
65
|
+
const pkgName = astGrepNativePackageName(platform, arch);
|
|
66
|
+
if (!pkgName)
|
|
67
|
+
return undefined;
|
|
68
|
+
const binaryName = platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
69
|
+
try {
|
|
70
|
+
return _require.resolve(`${pkgName}/${binaryName}`);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export const SERVER_DIAGNOSTIC_STRATEGIES = {
|
|
77
|
+
typescript: {
|
|
78
|
+
seedFirstPush: true,
|
|
79
|
+
pullRetryBudgetMs: 0,
|
|
80
|
+
debounceMs: 50,
|
|
81
|
+
aggregateWaitMs: 1000,
|
|
82
|
+
expectSemanticSecondPush: false,
|
|
83
|
+
// Tier 3 (#458): typescript-language-server publishes nothing on a
|
|
84
|
+
// clean→clean edit (docs/lsp-capability-matrix.md, re-confirmed
|
|
85
|
+
// 2026-07-12). It's the lone core-set tier-3 server, which is exactly
|
|
86
|
+
// why the cascade lane's in-lane wait is worth skipping for it
|
|
87
|
+
// specifically. Applies to the CLASSIC server only (#524/#529/#558)
|
|
88
|
+
// — TS7's native `tsc --lsp --stdio` variant shares this
|
|
89
|
+
// "typescript" server id but the 2026-07-12 dual-environment
|
|
90
|
+
// re-measurement found it now publishes on clean (a drift from the
|
|
91
|
+
// #541 measurement); wait-policy/classification.ts's classifier checks the live
|
|
92
|
+
// snapshot's launchVariant and never applies this flag to a
|
|
93
|
+
// native-ts7 instance.
|
|
94
|
+
silentOnClean: true,
|
|
95
|
+
},
|
|
96
|
+
"rust-analyzer": {
|
|
97
|
+
seedFirstPush: false,
|
|
98
|
+
pullRetryBudgetMs: 500,
|
|
99
|
+
debounceMs: 150,
|
|
100
|
+
aggregateWaitMs: 3000,
|
|
101
|
+
expectSemanticSecondPush: true,
|
|
102
|
+
},
|
|
103
|
+
// PythonServer (pyright / basedpyright) — openFilesOnly mode: lazy per-file
|
|
104
|
+
// analysis, startup similar to jedi. seedFirstPush: true because pyright's
|
|
105
|
+
// first publishDiagnostics after didOpen is the complete result for that file.
|
|
106
|
+
python: {
|
|
107
|
+
seedFirstPush: true,
|
|
108
|
+
pullRetryBudgetMs: 0,
|
|
109
|
+
debounceMs: 100,
|
|
110
|
+
aggregateWaitMs: 1500,
|
|
111
|
+
expectSemanticSecondPush: false,
|
|
112
|
+
},
|
|
113
|
+
// jedi-language-server is push-only (no pull diagnostics) and its FIRST
|
|
114
|
+
// publishDiagnostics is the complete result (seedFirstPush). But that first
|
|
115
|
+
// push lands just after didOpen+~1s on cold start (Python/parso import) —
|
|
116
|
+
// measured ~1011ms — so a 1000ms aggregate budget misses it by a hair and
|
|
117
|
+
// returns zero. 3000ms gives cold-start headroom without stalling the warm path.
|
|
118
|
+
"python-jedi": {
|
|
119
|
+
seedFirstPush: true,
|
|
120
|
+
pullRetryBudgetMs: 0,
|
|
121
|
+
debounceMs: 100,
|
|
122
|
+
aggregateWaitMs: 3000,
|
|
123
|
+
expectSemanticSecondPush: false,
|
|
124
|
+
},
|
|
125
|
+
eslint: {
|
|
126
|
+
seedFirstPush: true,
|
|
127
|
+
pullRetryBudgetMs: 0,
|
|
128
|
+
debounceMs: 200,
|
|
129
|
+
aggregateWaitMs: 2000,
|
|
130
|
+
expectSemanticSecondPush: false,
|
|
131
|
+
},
|
|
132
|
+
// Opengrep security scanner (cross-language LSP). It pushes an EMPTY result
|
|
133
|
+
// during the one-time rule-load window at startup, then the real scan after
|
|
134
|
+
// `semgrep/rulesRefreshed` — so never seed the first push. Push-only (no pull
|
|
135
|
+
// diagnostics). Warm per-file scan ~1.3s; the first touch in a session may
|
|
136
|
+
// also pay rule-load (~3.5s cold). aggregateWaitMs is 3500: it's this
|
|
137
|
+
// server's OWN deadline, bounded by the per-edit caller cap as a ceiling
|
|
138
|
+
// (#242), so on a 2500ms-capped edit opengrep waits min(2500, 3500)=2500
|
|
139
|
+
// and on uncapped paths it gets its full 3500. 3500 covers warm and most
|
|
140
|
+
// cold; a cold scan that overruns isn't lost — late diagnostics are cached
|
|
141
|
+
// and surface on the next edit.
|
|
142
|
+
opengrep: {
|
|
143
|
+
seedFirstPush: false,
|
|
144
|
+
pullRetryBudgetMs: 0,
|
|
145
|
+
debounceMs: 250,
|
|
146
|
+
aggregateWaitMs: 3500,
|
|
147
|
+
expectSemanticSecondPush: false,
|
|
148
|
+
// Opengrep re-scans only on a fresh didOpen — didChange is a no-op for it.
|
|
149
|
+
reopenOnResync: true,
|
|
150
|
+
},
|
|
151
|
+
// ast-grep structural linter (sgconfig-gated auxiliary LSP). Push-only,
|
|
152
|
+
// compiles the project rules on the first scan of a session, and — like
|
|
153
|
+
// Opengrep — is re-synced via didClose+didOpen so edits trigger a re-scan.
|
|
154
|
+
// Conservative budget until measured against real projects (#239).
|
|
155
|
+
// ast-grep re-scans on didChange (verified: toggling the violation count
|
|
156
|
+
// 3→1→4→2 returns the correct fresh count each touch, with matching doc
|
|
157
|
+
// versions), so reopen isn't needed and didChange is the lighter path.
|
|
158
|
+
// aggregateWaitMs is 1800: its warm scan is ~1.3s, so 1000 was under-budgeted
|
|
159
|
+
// (only masked before because the old with-auxiliary deadline was a global
|
|
160
|
+
// max() floor; now each server has its own caller-cap-bounded deadline, #242,
|
|
161
|
+
// so the budget must actually cover the scan).
|
|
162
|
+
"ast-grep": {
|
|
163
|
+
seedFirstPush: false,
|
|
164
|
+
pullRetryBudgetMs: 0,
|
|
165
|
+
debounceMs: 150,
|
|
166
|
+
aggregateWaitMs: 1800,
|
|
167
|
+
expectSemanticSecondPush: false,
|
|
168
|
+
reopenOnResync: false,
|
|
169
|
+
},
|
|
170
|
+
// zizmor (GitHub Actions security scanner, auxiliary LSP #272). Push-only
|
|
171
|
+
// (no pull diagnostics). Its audit set is compiled-in — there's no rule-load
|
|
172
|
+
// window like Opengrep — so the FIRST publishDiagnostics after didOpen IS the
|
|
173
|
+
// complete result: seedFirstPush. It re-scans on didChange (FULL sync), so no
|
|
174
|
+
// reopen-on-resync. A native single-workflow audit is sub-second offline;
|
|
175
|
+
// online mode may add a GitHub-API round-trip, so 2000ms gives headroom
|
|
176
|
+
// (bounded by the per-edit caller cap as a ceiling, #242), and any late online
|
|
177
|
+
// finding is cached and surfaces on the next edit.
|
|
178
|
+
zizmor: {
|
|
179
|
+
seedFirstPush: true,
|
|
180
|
+
pullRetryBudgetMs: 0,
|
|
181
|
+
debounceMs: 150,
|
|
182
|
+
aggregateWaitMs: 2000,
|
|
183
|
+
expectSemanticSecondPush: false,
|
|
184
|
+
reopenOnResync: false,
|
|
185
|
+
},
|
|
186
|
+
// typos (source-code spell checker, auxiliary LSP #283). Push-only (no pull
|
|
187
|
+
// diagnostics). Its dictionary is compiled in — there's NO rule-load window
|
|
188
|
+
// like Opengrep — so the FIRST publishDiagnostics after didOpen IS the
|
|
189
|
+
// complete result: seedFirstPush. It re-scans on didChange (FULL sync), so no
|
|
190
|
+
// reopen-on-resync. A native single-file spell scan is sub-100ms, so the seed
|
|
191
|
+
// arrives near-instantly; aggregateWaitMs is a generous ceiling (bounded by
|
|
192
|
+
// the per-edit caller cap, #242) that the early seed resolves well under.
|
|
193
|
+
typos: {
|
|
194
|
+
seedFirstPush: true,
|
|
195
|
+
pullRetryBudgetMs: 0,
|
|
196
|
+
debounceMs: 150,
|
|
197
|
+
aggregateWaitMs: 1500,
|
|
198
|
+
expectSemanticSecondPush: false,
|
|
199
|
+
reopenOnResync: false,
|
|
200
|
+
},
|
|
201
|
+
// marksman (Markdown LSP, #274). Push-based; native binary so the per-file
|
|
202
|
+
// parse is fast, but its value is CROSS-file (broken intra-repo links,
|
|
203
|
+
// missing anchors/heading refs) which needs the workspace index — so the
|
|
204
|
+
// first push after didOpen can be empty before indexing completes. Don't
|
|
205
|
+
// seed it (like opengrep/rust-analyzer); a modest 1500ms aggregate covers
|
|
206
|
+
// warm edits, and any late cross-file finding surfaces on the next touch.
|
|
207
|
+
marksman: {
|
|
208
|
+
seedFirstPush: false,
|
|
209
|
+
pullRetryBudgetMs: 0,
|
|
210
|
+
debounceMs: 150,
|
|
211
|
+
aggregateWaitMs: 1500,
|
|
212
|
+
expectSemanticSecondPush: false,
|
|
213
|
+
// #645: a full-tree sweep opens every markdown file, all racing the
|
|
214
|
+
// SAME one-time workspace index build — pay the 1500ms budget once
|
|
215
|
+
// per sweep (the first markdown file touched), not once per file.
|
|
216
|
+
// 250ms covers a warm per-file publish (native binary, fast parse)
|
|
217
|
+
// once the index has already had a full aggregateWaitMs window.
|
|
218
|
+
workspaceIndexing: true,
|
|
219
|
+
workspaceIndexingWarmWaitMs: 250,
|
|
220
|
+
// #799: marksman is push-only and publishes NOTHING on a clean file —
|
|
221
|
+
// there is no pull fallback and no sync-confirm protocol (unlike
|
|
222
|
+
// typescript's tsserver commands), so a clean markdown file's touch
|
|
223
|
+
// waits its full budget with zero signal either way. Marking it
|
|
224
|
+
// `silentOnClean` lets the generic push-only clean-confirm gate
|
|
225
|
+
// (`touchFile`, the `classifyCascadeWaitTier`-driven fallback next to
|
|
226
|
+
// the tsserver sync-confirm block) treat "no publish within budget,
|
|
227
|
+
// notify succeeded" as CONFIRMED clean instead of inconclusive/timed
|
|
228
|
+
// out — closing the observed 2x20s-per-sweep burn on clean markdown
|
|
229
|
+
// (evidence: agents.md timed out at 20009ms/20014ms on both the
|
|
230
|
+
// warm-up attempt and its retry, with zero diagnostics either time).
|
|
231
|
+
silentOnClean: true,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
/** Fallback for unknown servers. Conservative defaults. */
|
|
235
|
+
export const DEFAULT_STRATEGY = {
|
|
236
|
+
seedFirstPush: false,
|
|
237
|
+
pullRetryBudgetMs: 250,
|
|
238
|
+
debounceMs: 150,
|
|
239
|
+
aggregateWaitMs: 1500,
|
|
240
|
+
expectSemanticSecondPush: false,
|
|
241
|
+
};
|
|
242
|
+
export function getStrategy(serverId) {
|
|
243
|
+
return SERVER_DIAGNOSTIC_STRATEGIES[serverId] ?? DEFAULT_STRATEGY;
|
|
244
|
+
}
|