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,18 +1,38 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { parseEnabledShape } from "./config-enabled-shape.js";
|
|
5
|
+
import { findNestedProjectMutationValue, } from "./project-lens-config.js";
|
|
4
6
|
export function getPiLensGlobalConfigPath(homeDir = os.homedir()) {
|
|
5
7
|
const override = process.env.PI_LENS_CONFIG_PATH;
|
|
6
8
|
if (override)
|
|
7
9
|
return path.resolve(override);
|
|
8
10
|
return path.join(homeDir, ".pi-lens", "config.json");
|
|
9
11
|
}
|
|
12
|
+
const warnedInvalidGlobalConfigs = new Set();
|
|
13
|
+
/**
|
|
14
|
+
* Same warn-once-per-(path, reason) contract as project-lens-config.ts's
|
|
15
|
+
* `warnInvalidConfigOnce` — a malformed global config value is logged once
|
|
16
|
+
* and then treated as absent, rather than silently dropped (#792).
|
|
17
|
+
*/
|
|
18
|
+
function warnInvalidGlobalConfigOnce(configPath, reason) {
|
|
19
|
+
const key = `${configPath}:${reason}`;
|
|
20
|
+
if (warnedInvalidGlobalConfigs.has(key))
|
|
21
|
+
return;
|
|
22
|
+
warnedInvalidGlobalConfigs.add(key);
|
|
23
|
+
console.error(`[pi-lens] ignoring invalid global config ${configPath}: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
/** For tests that need to force the warn-once cache to reset between cases. */
|
|
26
|
+
export function resetGlobalConfigWarnCache() {
|
|
27
|
+
warnedInvalidGlobalConfigs.clear();
|
|
28
|
+
}
|
|
10
29
|
export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath()) {
|
|
11
30
|
try {
|
|
12
31
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
13
32
|
if (!parsed || typeof parsed !== "object")
|
|
14
33
|
return undefined;
|
|
15
34
|
const raw = parsed;
|
|
35
|
+
const warnInvalid = (reason) => warnInvalidGlobalConfigOnce(configPath, reason);
|
|
16
36
|
const dispatchRaw = raw.dispatch;
|
|
17
37
|
const dispatch = dispatchRaw && typeof dispatchRaw === "object"
|
|
18
38
|
? dispatchRaw
|
|
@@ -21,19 +41,17 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
21
41
|
const widget = widgetRaw && typeof widgetRaw === "object"
|
|
22
42
|
? widgetRaw
|
|
23
43
|
: undefined;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
44
|
+
const format = parseEnabledShape(raw.format, "format", warnInvalid);
|
|
45
|
+
const autofix = parseEnabledShape(raw.autofix, "autofix", warnInvalid);
|
|
46
|
+
const formatModeRaw = raw.format;
|
|
47
|
+
const formatModeSource = formatModeRaw && typeof formatModeRaw === "object"
|
|
48
|
+
? formatModeRaw
|
|
27
49
|
: undefined;
|
|
28
50
|
const actionableWarningsRaw = raw.actionableWarnings;
|
|
29
51
|
const actionableWarnings = actionableWarningsRaw && typeof actionableWarningsRaw === "object"
|
|
30
52
|
? actionableWarningsRaw
|
|
31
53
|
: undefined;
|
|
32
|
-
const
|
|
33
|
-
const actionableWarningsAutoFix = actionableWarningsAutoFixRaw &&
|
|
34
|
-
typeof actionableWarningsAutoFixRaw === "object"
|
|
35
|
-
? actionableWarningsAutoFixRaw
|
|
36
|
-
: undefined;
|
|
54
|
+
const actionableWarningsAutoFix = parseEnabledShape(actionableWarnings?.autoFix, "actionableWarnings.autoFix", warnInvalid);
|
|
37
55
|
const contextInjectionRaw = raw.contextInjection;
|
|
38
56
|
const contextInjection = contextInjectionRaw && typeof contextInjectionRaw === "object"
|
|
39
57
|
? contextInjectionRaw
|
|
@@ -42,8 +60,9 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
42
60
|
const turnSummary = turnSummaryRaw && typeof turnSummaryRaw === "object"
|
|
43
61
|
? turnSummaryRaw
|
|
44
62
|
: undefined;
|
|
45
|
-
const formatMode =
|
|
46
|
-
|
|
63
|
+
const formatMode = formatModeSource?.mode === "immediate" ||
|
|
64
|
+
formatModeSource?.mode === "deferred"
|
|
65
|
+
? formatModeSource.mode
|
|
47
66
|
: undefined;
|
|
48
67
|
const ignore = Array.isArray(raw.ignore)
|
|
49
68
|
? raw.ignore.filter((p) => typeof p === "string")
|
|
@@ -66,10 +85,11 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
66
85
|
: undefined,
|
|
67
86
|
format: format
|
|
68
87
|
? {
|
|
69
|
-
enabled:
|
|
88
|
+
enabled: format.enabled,
|
|
70
89
|
mode: formatMode,
|
|
71
90
|
}
|
|
72
91
|
: undefined,
|
|
92
|
+
autofix: autofix ? { enabled: autofix.enabled } : undefined,
|
|
73
93
|
actionableWarnings: actionableWarnings
|
|
74
94
|
? {
|
|
75
95
|
enabled: typeof actionableWarnings.enabled === "boolean"
|
|
@@ -82,11 +102,7 @@ export function loadPiLensGlobalConfig(configPath = getPiLensGlobalConfigPath())
|
|
|
82
102
|
? actionableWarnings.deltaOnly
|
|
83
103
|
: undefined,
|
|
84
104
|
autoFix: actionableWarningsAutoFix
|
|
85
|
-
? {
|
|
86
|
-
enabled: typeof actionableWarningsAutoFix.enabled === "boolean"
|
|
87
|
-
? actionableWarningsAutoFix.enabled
|
|
88
|
-
: undefined,
|
|
89
|
-
}
|
|
105
|
+
? { enabled: actionableWarningsAutoFix.enabled }
|
|
90
106
|
: undefined,
|
|
91
107
|
}
|
|
92
108
|
: undefined,
|
|
@@ -119,6 +135,9 @@ export function getGlobalWidgetDefaultVisible(configPath) {
|
|
|
119
135
|
export function getGlobalAutoformatEnabled(configPath) {
|
|
120
136
|
return loadPiLensGlobalConfig(configPath)?.format?.enabled !== false;
|
|
121
137
|
}
|
|
138
|
+
export function getGlobalAutofixEnabled(configPath) {
|
|
139
|
+
return loadPiLensGlobalConfig(configPath)?.autofix?.enabled !== false;
|
|
140
|
+
}
|
|
122
141
|
export function getGlobalImmediateFormatDefault(configPath) {
|
|
123
142
|
return loadPiLensGlobalConfig(configPath)?.format?.mode === "immediate";
|
|
124
143
|
}
|
|
@@ -128,32 +147,92 @@ export function getGlobalContextInjectionEnabled(configPath) {
|
|
|
128
147
|
export function getGlobalTurnSummaryEnabled(configPath) {
|
|
129
148
|
return loadPiLensGlobalConfig(configPath)?.turnSummary?.enabled === true;
|
|
130
149
|
}
|
|
131
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a flag AND report which config tier decided it — same precedence
|
|
152
|
+
* as {@link resolvePiLensFlag} (which now delegates here), just also
|
|
153
|
+
* returning the `source` so callers can log e.g.
|
|
154
|
+
* "(--no-autofix, source=project)" instead of a bare boolean (#792).
|
|
155
|
+
*
|
|
156
|
+
* Precedence (unchanged, maintainer decision — project wins over global,
|
|
157
|
+
* including re-enabling; only an explicit CLI disabling flag outranks
|
|
158
|
+
* project config): cli → project → global → default.
|
|
159
|
+
*/
|
|
160
|
+
export function resolvePiLensFlagWithSource(name, value, config, projectConfig, editedFilePath, projectRoot) {
|
|
132
161
|
if (value)
|
|
133
|
-
return value;
|
|
162
|
+
return { value, source: "cli" };
|
|
163
|
+
const mutationFlag = name === "no-autoformat" ||
|
|
164
|
+
name === "no-autofix" ||
|
|
165
|
+
name === "lens-actionable-warning-autofix"
|
|
166
|
+
? name
|
|
167
|
+
: undefined;
|
|
168
|
+
const nested = mutationFlag && editedFilePath && projectRoot
|
|
169
|
+
? findNestedProjectMutationValue(mutationFlag, editedFilePath, projectRoot)
|
|
170
|
+
: undefined;
|
|
171
|
+
const nestedSource = nested
|
|
172
|
+
? path.resolve(nested.dir) === path.resolve(projectRoot)
|
|
173
|
+
? "project"
|
|
174
|
+
: `nested-project:${nested.dir}`
|
|
175
|
+
: undefined;
|
|
134
176
|
if (name === "no-autoformat") {
|
|
135
|
-
|
|
177
|
+
if (nested)
|
|
178
|
+
return { value: !nested.value, source: nestedSource };
|
|
179
|
+
if (projectConfig?.format?.enabled !== undefined) {
|
|
180
|
+
return { value: !projectConfig.format.enabled, source: "project" };
|
|
181
|
+
}
|
|
182
|
+
if (config?.format?.enabled === false) {
|
|
183
|
+
return { value: true, source: "global" };
|
|
184
|
+
}
|
|
185
|
+
return { value: false, source: "default" };
|
|
186
|
+
}
|
|
187
|
+
if (name === "no-autofix") {
|
|
188
|
+
if (nested)
|
|
189
|
+
return { value: !nested.value, source: nestedSource };
|
|
190
|
+
if (projectConfig?.autofix?.enabled !== undefined) {
|
|
191
|
+
return { value: !projectConfig.autofix.enabled, source: "project" };
|
|
192
|
+
}
|
|
193
|
+
if (config?.autofix?.enabled === false) {
|
|
194
|
+
return { value: true, source: "global" };
|
|
195
|
+
}
|
|
196
|
+
return { value: false, source: "default" };
|
|
136
197
|
}
|
|
137
198
|
if (name === "immediate-format") {
|
|
138
|
-
|
|
199
|
+
const immediate = config?.format?.mode === "immediate";
|
|
200
|
+
return { value: immediate, source: immediate ? "global" : "default" };
|
|
139
201
|
}
|
|
140
202
|
if (name === "lens-actionable-warnings") {
|
|
141
|
-
|
|
203
|
+
const enabled = config?.actionableWarnings?.enabled === true;
|
|
204
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
142
205
|
}
|
|
143
206
|
if (name === "lens-actionable-warning-actions") {
|
|
144
|
-
|
|
207
|
+
const enabled = config?.actionableWarnings?.includeLspCodeActions === true;
|
|
208
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
145
209
|
}
|
|
146
210
|
if (name === "lens-actionable-warning-autofix") {
|
|
147
|
-
|
|
211
|
+
if (nested)
|
|
212
|
+
return { value: nested.value, source: nestedSource };
|
|
213
|
+
if (projectConfig?.actionableWarnings?.autoFix?.enabled !== undefined) {
|
|
214
|
+
return {
|
|
215
|
+
value: projectConfig.actionableWarnings.autoFix.enabled,
|
|
216
|
+
source: "project",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const enabled = config?.actionableWarnings?.autoFix?.enabled === true;
|
|
220
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
148
221
|
}
|
|
149
222
|
if (name === "lens-actionable-warning-all") {
|
|
150
|
-
|
|
223
|
+
const all = config?.actionableWarnings?.deltaOnly === false;
|
|
224
|
+
return { value: all, source: all ? "global" : "default" };
|
|
151
225
|
}
|
|
152
226
|
if (name === "no-lens-context") {
|
|
153
|
-
|
|
227
|
+
const disabled = config?.contextInjection?.enabled === false;
|
|
228
|
+
return { value: disabled, source: disabled ? "global" : "default" };
|
|
154
229
|
}
|
|
155
230
|
if (name === "lens-turn-summary") {
|
|
156
|
-
|
|
231
|
+
const enabled = config?.turnSummary?.enabled === true;
|
|
232
|
+
return { value: enabled, source: enabled ? "global" : "default" };
|
|
157
233
|
}
|
|
158
|
-
return value;
|
|
234
|
+
return { value, source: "default" };
|
|
235
|
+
}
|
|
236
|
+
export function resolvePiLensFlag(name, value, config, projectConfig, editedFilePath, projectRoot) {
|
|
237
|
+
return resolvePiLensFlagWithSource(name, value, config, projectConfig, editedFilePath, projectRoot).value;
|
|
159
238
|
}
|
|
@@ -21,6 +21,7 @@ import { getLSPService } from "./lsp/index.js";
|
|
|
21
21
|
import { getOrLoadWarmWordIndex } from "./mcp/analyze.js";
|
|
22
22
|
import { scanProjectDiagnostics } from "./project-diagnostics/scanner.js";
|
|
23
23
|
import * as path from "node:path";
|
|
24
|
+
import { minimatch } from "./deps/minimatch.js";
|
|
24
25
|
import { normalizeMapKey } from "./path-utils.js";
|
|
25
26
|
import { loadProjectSnapshot } from "./project-snapshot.js";
|
|
26
27
|
import { centralityFromReverseDeps, deserializeWordIndex, searchWordIndex, triggerBackgroundWordIndexBuild, } from "./word-index.js";
|
|
@@ -31,6 +32,7 @@ export { ipcPathForCwd, requestWarmAnalyze, } from "./mcp/ipc.js";
|
|
|
31
32
|
export { analyzeFileFresh, resolveRebuildScript, runRebuild, summarizeScan, } from "./mcp/review.js";
|
|
32
33
|
export { runSessionStart, runTurnEnd, } from "./mcp/session.js";
|
|
33
34
|
export { moduleReport, readEnclosing, readSymbol, renderCompactModuleReport, } from "./module-report.js";
|
|
35
|
+
export { projectReport, renderCompactProjectReport, } from "./project-report.js";
|
|
34
36
|
// --- Query wrappers (own the remaining internal reach-ins) -------------------
|
|
35
37
|
/** Recent dispatch latency reports (latency.log schema), newest first. */
|
|
36
38
|
export function recentLatency(limit = 5, fileFilter) {
|
|
@@ -45,6 +47,19 @@ export function recentLatency(limit = 5, fileFilter) {
|
|
|
45
47
|
export function projectScan(cwd, maxFiles) {
|
|
46
48
|
return scanProjectDiagnostics({ cwd, tier: "cheap", maxFiles });
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* #784: `scanTruncated` (#760) reaches this seam intact but, until now, no
|
|
52
|
+
* caller rendered it — a capped scan read as a complete clean sweep to the
|
|
53
|
+
* agent/user. One shared, unit-testable line adapters can append to their
|
|
54
|
+
* rendered summary, matching the #777 warm-skip notify's override-hint style.
|
|
55
|
+
* Returns `undefined` when the scan was not truncated (no line to append).
|
|
56
|
+
*/
|
|
57
|
+
export function scanTruncationNotice(snapshot) {
|
|
58
|
+
if (!snapshot.scanTruncated)
|
|
59
|
+
return undefined;
|
|
60
|
+
return (`⚠ Scan truncated at ${snapshot.filesScanned} file(s) — results are partial; ` +
|
|
61
|
+
"raise maxProjectFiles in .pi-lens.json to scan fully.");
|
|
62
|
+
}
|
|
48
63
|
/** Alive LSP client count + per-server status. */
|
|
49
64
|
export function lspStatus() {
|
|
50
65
|
const lsp = getLSPService();
|
|
@@ -69,6 +84,97 @@ export function ensureLspConfig(cwd) {
|
|
|
69
84
|
export function resourceFootprint() {
|
|
70
85
|
return getResourceFootprintSnapshot();
|
|
71
86
|
}
|
|
87
|
+
// Same language identifiers ast_grep_search's `lang` param accepts
|
|
88
|
+
// (tools/shared.ts's LANGUAGES) mapped to source file extensions. Duplicated
|
|
89
|
+
// here rather than imported — clients/ never reaches into tools/ (see
|
|
90
|
+
// AGENTS.md's MCP-mirror layering note) — so this is symbol_search's own small
|
|
91
|
+
// copy, scoped to what its `lang` filter needs (extension matching only, no
|
|
92
|
+
// AST/LSP concerns).
|
|
93
|
+
const SYMBOL_SEARCH_LANG_EXTENSIONS = {
|
|
94
|
+
bash: [".sh", ".bash"],
|
|
95
|
+
c: [".c", ".h"],
|
|
96
|
+
cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
|
|
97
|
+
csharp: [".cs"],
|
|
98
|
+
css: [".css", ".scss", ".less"],
|
|
99
|
+
elixir: [".ex", ".exs"],
|
|
100
|
+
go: [".go"],
|
|
101
|
+
haskell: [".hs", ".lhs"],
|
|
102
|
+
html: [".html", ".htm"],
|
|
103
|
+
java: [".java"],
|
|
104
|
+
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
105
|
+
json: [".json", ".jsonc", ".json5"],
|
|
106
|
+
kotlin: [".kt", ".kts"],
|
|
107
|
+
lua: [".lua"],
|
|
108
|
+
nix: [".nix"],
|
|
109
|
+
php: [".php"],
|
|
110
|
+
python: [".py", ".pyi"],
|
|
111
|
+
ruby: [".rb", ".rake", ".gemspec"],
|
|
112
|
+
rust: [".rs"],
|
|
113
|
+
scala: [".scala", ".sc"],
|
|
114
|
+
solidity: [".sol"],
|
|
115
|
+
swift: [".swift"],
|
|
116
|
+
tsx: [".tsx"],
|
|
117
|
+
typescript: [".ts", ".mts", ".cts"],
|
|
118
|
+
yaml: [".yaml", ".yml"],
|
|
119
|
+
};
|
|
120
|
+
function fileMatchesLang(file, lang) {
|
|
121
|
+
const exts = SYMBOL_SEARCH_LANG_EXTENSIONS[lang];
|
|
122
|
+
if (!exts)
|
|
123
|
+
return false;
|
|
124
|
+
return exts.includes(path.extname(file).toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
/** Same glob semantics as ast_grep_search's `paths`: a bare directory/file
|
|
127
|
+
* entry scopes its whole subtree (via a `/**` suffix match), a full glob
|
|
128
|
+
* pattern is matched as-is. Matched relative to `cwd` so absolute index paths
|
|
129
|
+
* and repo-relative globs line up regardless of platform path separators. */
|
|
130
|
+
function fileMatchesPathGlobs(file, cwd, globs) {
|
|
131
|
+
const rel = path.relative(cwd, file).split(path.sep).join("/");
|
|
132
|
+
const options = { dot: true, nocase: process.platform === "win32" };
|
|
133
|
+
return globs.some((raw) => {
|
|
134
|
+
const pattern = raw.split("\\").join("/");
|
|
135
|
+
return (minimatch(rel, pattern, options) ||
|
|
136
|
+
minimatch(rel, `${pattern}/**`, options));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function buildSymbolSearchFileFilter(cwd, options) {
|
|
140
|
+
const paths = options.paths?.filter((p) => p.trim().length > 0);
|
|
141
|
+
const lang = options.lang?.trim();
|
|
142
|
+
if (!paths?.length && !lang)
|
|
143
|
+
return undefined;
|
|
144
|
+
return (file) => {
|
|
145
|
+
if (paths?.length && !fileMatchesPathGlobs(file, cwd, paths))
|
|
146
|
+
return false;
|
|
147
|
+
if (lang && !fileMatchesLang(file, lang))
|
|
148
|
+
return false;
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Attaches read-only graph signals to already-ranked hits (#771) — mutates
|
|
154
|
+
* each hit in place. `fanIn` reuses the SAME `centrality` map `symbolSearch`
|
|
155
|
+
* already computed for its ranking boost (zero extra cost); `complexity` is
|
|
156
|
+
* the highest per-symbol `cyclomaticComplexity` the (warm) review graph
|
|
157
|
+
* recorded for that file. Caller only invokes this when `graph` is defined —
|
|
158
|
+
* never builds one itself.
|
|
159
|
+
*/
|
|
160
|
+
function annotateSymbolSearchHitsWithGraph(hits, centrality, graph) {
|
|
161
|
+
for (const hit of hits) {
|
|
162
|
+
const normalized = normalizeMapKey(path.resolve(hit.file));
|
|
163
|
+
const fanIn = centrality.get(hit.file) ?? 0;
|
|
164
|
+
let complexity;
|
|
165
|
+
for (const symbolId of graph.symbolNodesByFile.get(normalized) ?? []) {
|
|
166
|
+
const raw = graph.nodes.get(symbolId)?.metadata?.cyclomaticComplexity;
|
|
167
|
+
if (typeof raw === "number" &&
|
|
168
|
+
(complexity === undefined || raw > complexity)) {
|
|
169
|
+
complexity = raw;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
hit.annotations = {
|
|
173
|
+
fanIn,
|
|
174
|
+
...(complexity !== undefined ? { complexity } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
72
178
|
function toSymbolSearchHit(result) {
|
|
73
179
|
const line = result.lines[0] ?? 1;
|
|
74
180
|
return {
|
|
@@ -98,8 +204,22 @@ function toSymbolSearchHit(result) {
|
|
|
98
204
|
* pilens_analyze yet this process, or #348 phase 2's forward-index isn't
|
|
99
205
|
* available) — this function's public contract (available/hint/results shape)
|
|
100
206
|
* is unchanged either way.
|
|
207
|
+
*
|
|
208
|
+
* `options.paths`/`options.lang` (#771) scope the word index BEFORE ranking
|
|
209
|
+
* (`searchWordIndex`'s `fileFilter`), so a surviving file's score is identical
|
|
210
|
+
* to an unfiltered run; omitting both reproduces prior output byte-for-byte.
|
|
211
|
+
*
|
|
212
|
+
* Every hit is additionally annotated (#771) with the graph signals already
|
|
213
|
+
* available when the cached review graph happens to be warm —
|
|
214
|
+
* `getCachedReviewGraph` (`clients/review-graph/builder.ts`) is a READ-ONLY
|
|
215
|
+
* accessor: an in-memory miss falls through to a persisted-disk read, and
|
|
216
|
+
* NOTHING here ever triggers a fresh build. A cold cache (no in-memory graph,
|
|
217
|
+
* no persisted snapshot) simply omits `annotations` — this function's latency
|
|
218
|
+
* profile is unchanged either way. The module is dynamic-imported (mirroring
|
|
219
|
+
* module-report.ts's own lazy load of it) so an unused review graph costs
|
|
220
|
+
* nothing on this hot path.
|
|
101
221
|
*/
|
|
102
|
-
export function symbolSearch(query, cwd, limit = 20) {
|
|
222
|
+
export async function symbolSearch(query, cwd, limit = 20, options = {}) {
|
|
103
223
|
const snapshot = loadProjectSnapshot(cwd);
|
|
104
224
|
const index = getOrLoadWarmWordIndex(cwd) ?? deserializeWordIndex(snapshot?.wordIndex);
|
|
105
225
|
if (!index) {
|
|
@@ -114,11 +234,17 @@ export function symbolSearch(query, cwd, limit = 20) {
|
|
|
114
234
|
// Boost well-connected files using the snapshot's reverse-dependency
|
|
115
235
|
// (importedBy) counts; snapshot keys are normalized, index keys are raw.
|
|
116
236
|
const centrality = centralityFromReverseDeps(index, snapshot?.reverseDeps, (file) => normalizeMapKey(path.resolve(file)));
|
|
117
|
-
const
|
|
237
|
+
const fileFilter = buildSymbolSearchFileFilter(cwd, options);
|
|
238
|
+
const results = searchWordIndex(index, query, { limit, centrality, fileFilter });
|
|
239
|
+
const hits = results.map(toSymbolSearchHit);
|
|
240
|
+
const { getCachedReviewGraph } = await import("./review-graph/builder.js");
|
|
241
|
+
const graph = getCachedReviewGraph(cwd);
|
|
242
|
+
if (graph)
|
|
243
|
+
annotateSymbolSearchHitsWithGraph(hits, centrality, graph);
|
|
118
244
|
return {
|
|
119
245
|
available: true,
|
|
120
246
|
query,
|
|
121
|
-
results:
|
|
247
|
+
results: hits,
|
|
122
248
|
snapshotGeneratedAt: snapshot?.generatedAt,
|
|
123
249
|
};
|
|
124
250
|
}
|
|
@@ -57,8 +57,7 @@ import { logCascade } from "../cascade-logger.js";
|
|
|
57
57
|
import { logLatency } from "../latency-logger.js";
|
|
58
58
|
import { normalizeMapKey } from "../path-utils.js";
|
|
59
59
|
import { registerQuietWindowTask } from "../quiet-window.js";
|
|
60
|
-
|
|
61
|
-
import { getStrategy } from "./server-strategies.js";
|
|
60
|
+
export { classifyCascadeWaitTier, classifyServerWaitTier, } from "./wait-policy/classification.js";
|
|
62
61
|
// --- Kill switch (lazy, memoized — house style per clients/runtime-config.ts /
|
|
63
62
|
// clients/quiet-window.ts's isQuietWindowEnabled) ---
|
|
64
63
|
let _enabledCache;
|
|
@@ -75,42 +74,6 @@ export function isTierAwareCascadeEnabled() {
|
|
|
75
74
|
export function _resetTierAwareCascadeEnabledForTests() {
|
|
76
75
|
_enabledCache = undefined;
|
|
77
76
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Classify whether `filePath`'s PRIMARY language server is a cascade-lane
|
|
80
|
-
* Tier-3 (push-only, silent-on-clean) server. Ambiguous or missing capability
|
|
81
|
-
* data is always `"waits"` (today's behavior) — this function must never be
|
|
82
|
-
* the reason a real answer gets missed.
|
|
83
|
-
*/
|
|
84
|
-
export function classifyCascadeWaitTier(lspService, filePath, snapshots) {
|
|
85
|
-
void lspService; // kept in the signature for call-site clarity/typing only
|
|
86
|
-
const servers = getServersForFileWithConfig(filePath).filter((s) => s.role !== "auxiliary");
|
|
87
|
-
const primary = servers[0];
|
|
88
|
-
if (!primary)
|
|
89
|
-
return "waits";
|
|
90
|
-
const snapshot = snapshots.find((s) => s.serverId === primary.id);
|
|
91
|
-
if (!snapshot)
|
|
92
|
-
return "waits"; // no live snapshot yet — fail-safe
|
|
93
|
-
const mode = snapshot.workspaceDiagnosticsSupport?.mode;
|
|
94
|
-
if (mode !== "push-only")
|
|
95
|
-
return "waits"; // pull servers are always affirmative
|
|
96
|
-
const strategy = getStrategy(primary.id);
|
|
97
|
-
if (strategy.silentOnClean !== true)
|
|
98
|
-
return "waits"; // 2*/unknown push-only
|
|
99
|
-
// #524/#529/#541/#558: `silentOnClean` on a server-id-keyed strategy is
|
|
100
|
-
// only proven against the variant it was actually measured against.
|
|
101
|
-
// "typescript" today means either classic typescript-language-server
|
|
102
|
-
// (confirmed silent-on-clean, 2026-07-12 dual-environment re-measurement)
|
|
103
|
-
// or TS7's native `tsc --lsp --stdio` (the SAME re-measurement found it
|
|
104
|
-
// publishes 2 version-less diagnostic sets on clean — NOT silent, a
|
|
105
|
-
// drift from the earlier #541 measurement). A native-ts7 snapshot must
|
|
106
|
-
// NOT inherit the classic verdict: fall through to "waits", the same
|
|
107
|
-
// ambiguous/fail-safe path an unmarked or non-push-only server already
|
|
108
|
-
// takes. `launchVariant === "classic"` or absent (older snapshots that
|
|
109
|
-
// predate the marker) keeps today's tier-3 behavior exactly.
|
|
110
|
-
if (snapshot.launchVariant === "native-ts7")
|
|
111
|
-
return "waits";
|
|
112
|
-
return "tier3-silent";
|
|
113
|
-
}
|
|
114
77
|
// Keyed by normalized file path. A later touch for the same file simply
|
|
115
78
|
// replaces the earlier entry (only the most recent touch matters — an
|
|
116
79
|
// older touch's diagnostics, if they ever arrive, are still a strict superset
|
|
@@ -22,7 +22,7 @@ import { applyWorkspaceEdit } from "./edits.js";
|
|
|
22
22
|
import { recordLspChild, removeLspChild } from "../instance-registry.js";
|
|
23
23
|
import { normalizeMapKey, uriToPath } from "./path-utils.js";
|
|
24
24
|
import { ADVERTISED_POSITION_ENCODINGS, convertCharacterOffset, lineTextAt, negotiatePositionEncoding, } from "./position-encoding.js";
|
|
25
|
-
import { getStrategy } from "./
|
|
25
|
+
import { getStrategy } from "./wait-policy/index.js";
|
|
26
26
|
import { WatchedFilesQueue } from "./watch-queue.js";
|
|
27
27
|
// Opt-in publishDiagnostics trace (PILENS_PUB_DEBUG=1) — read once, negligible
|
|
28
28
|
// hot-path cost. Surfaces each server's publish behavior (version + count) to
|
|
@@ -703,6 +703,8 @@ export async function clientWaitForDiagnostics(state, filePath, timeoutMs, optio
|
|
|
703
703
|
if (outcome.status === "clean")
|
|
704
704
|
sawClean = true;
|
|
705
705
|
}
|
|
706
|
+
if (options.pullOnly)
|
|
707
|
+
return;
|
|
706
708
|
if (outcome.status === "found" || sawClean)
|
|
707
709
|
return;
|
|
708
710
|
}
|