pi-lens 3.8.71 → 3.8.73
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 +152 -1
- package/README.md +31 -22
- 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/deps/js-yaml.js +1 -1
- package/dist/clients/diagnostic-dispositions.js +64 -2
- package/dist/clients/dispatch/integration.js +2 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +60 -7
- 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 +32 -21
- 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 +707 -109
- 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 +15205 -11330
- 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 +143 -10
- 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 +8 -9
- package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +1 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +1 -0
- package/scripts/download-grammars.js +1 -0
|
@@ -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
|
}
|