pi-lens 3.8.73 → 3.8.74
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 +1116 -0
- package/README.md +37 -21
- package/dist/clients/atomic-write.js +3 -3
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/cache/rule-cache.js +43 -2
- package/dist/clients/cache-observability.js +211 -0
- package/dist/clients/call-graph.js +20 -0
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/complexity-client.js +4 -7
- package/dist/clients/dependency-checker.js +151 -20
- package/dist/clients/diagnostic-dispositions.js +22 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +16 -1
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +14 -0
- package/dist/clients/dispatch/facts/import-facts.js +100 -101
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/integration.js +94 -15
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +24 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +86 -31
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/generated-artifacts.js +21 -0
- package/dist/clients/gzip-stage-write.js +84 -0
- package/dist/clients/installer/index.js +358 -129
- package/dist/clients/knip-client.js +64 -1
- package/dist/clients/language-profile.js +61 -72
- package/dist/clients/latency-logger.js +6 -16
- package/dist/clients/lens-config.js +128 -165
- package/dist/clients/lens-engine.js +39 -5
- package/dist/clients/lens-flag-registry.js +285 -0
- package/dist/clients/lens-map.js +2 -7
- package/dist/clients/log-cleanup.js +6 -17
- package/dist/clients/lsp/cascade-tier.js +22 -2
- package/dist/clients/lsp/client.js +74 -10
- package/dist/clients/lsp/config.js +62 -9
- package/dist/clients/lsp/index.js +34 -24
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +8 -6
- package/dist/clients/lsp/server.js +84 -29
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/mcp/analyze.js +5 -4
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/module-report.js +100 -80
- package/dist/clients/ndjson-logger.js +51 -21
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +35 -0
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +7 -0
- package/dist/clients/project-changes.js +77 -4
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +109 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +247 -127
- package/dist/clients/project-lens-config.js +53 -26
- package/dist/clients/project-report.js +39 -14
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +611 -7
- package/dist/clients/quiet-window.js +9 -1
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard.js +78 -0
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +133 -14
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +1522 -179
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph-logger.js +24 -0
- package/dist/clients/runtime-agent-end.js +4 -2
- package/dist/clients/runtime-coordinator.js +2 -2
- package/dist/clients/runtime-session.js +433 -59
- package/dist/clients/runtime-tool-call.js +46 -14
- package/dist/clients/runtime-turn.js +97 -26
- package/dist/clients/safe-spawn.js +102 -15
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +2 -1
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +48 -33
- package/dist/clients/source-filter.js +49 -25
- package/dist/clients/startup-marker.js +6 -0
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tree-sitter-cache.js +147 -139
- package/dist/clients/tree-sitter-client.js +1340 -67
- package/dist/clients/tree-sitter-logger.js +43 -1
- package/dist/clients/tree-sitter-query-loader.js +110 -11
- package/dist/clients/tree-sitter-shared.js +43 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
- package/dist/clients/widget-state.js +50 -14
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +252 -35
- package/dist/clients/workspace-topology.js +11 -1
- package/dist/index.js +45544 -39255
- package/dist/mcp/analyze-cli.js +3 -0
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +63 -8
- package/dist/tools/lens-diagnostics.js +47 -5
- package/dist/tools/lsp-navigation.js +5 -28
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/environment-variables.md +153 -11
- package/docs/features.md +4 -3
- package/docs/globalconfig.md +56 -4
- package/docs/language-coverage.md +2 -2
- package/docs/mcp.md +17 -16
- package/docs/servercapabilities.md +5 -2
- package/docs/settings.md +181 -0
- package/docs/tools.md +14 -0
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +13 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/config-enabled-shape.js +0 -39
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -65,6 +65,26 @@ export function impact(graph, startKey, maxDepth = 3, minWeight = 0.1) {
|
|
|
65
65
|
// Sort by depth then symbolKey for stable output
|
|
66
66
|
return results.sort((a, b) => a.depth - b.depth || a.symbolKey.localeCompare(b.symbolKey));
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits a `SymbolKey` into its file and symbol-name parts. Two shapes exist
|
|
70
|
+
* (see `buildCallGraph` above): the normal `"filePath:symbolName"` form, and
|
|
71
|
+
* the file-level fallback `"file:filePath"` used when no enclosing symbol was
|
|
72
|
+
* found for a call site (module-level code). Splitting at the LAST colon
|
|
73
|
+
* (rather than a naive `split(":")`) means a Windows drive letter in
|
|
74
|
+
* `filePath` (`C:\...`) is never mistaken for the name/file separator; the
|
|
75
|
+
* `"file:"` fallback is matched as a literal prefix first so it isn't
|
|
76
|
+
* misparsed by the same last-colon rule.
|
|
77
|
+
*/
|
|
78
|
+
export function parseSymbolKey(key) {
|
|
79
|
+
if (key.startsWith("file:")) {
|
|
80
|
+
return { filePath: key.slice("file:".length) };
|
|
81
|
+
}
|
|
82
|
+
const idx = key.lastIndexOf(":");
|
|
83
|
+
if (idx === -1)
|
|
84
|
+
return { filePath: key };
|
|
85
|
+
const symbolName = key.slice(idx + 1);
|
|
86
|
+
return { filePath: key.slice(0, idx), symbolName: symbolName || undefined };
|
|
87
|
+
}
|
|
68
88
|
/**
|
|
69
89
|
* Format an impact result set as a compact human-readable summary.
|
|
70
90
|
* Example: "handleToolResult (WillBreak) → handleAgentEnd (MayBreak) → 3 Review callers"
|
|
@@ -2,9 +2,13 @@ import * as path from "node:path";
|
|
|
2
2
|
import { isTestMode } from "./env-utils.js";
|
|
3
3
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
4
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
|
+
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
5
6
|
const CASCADE_LOG_DIR = getGlobalPiLensDir();
|
|
6
7
|
const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
|
|
7
|
-
const writer = createNdjsonLogger({
|
|
8
|
+
const writer = createNdjsonLogger({
|
|
9
|
+
filePath: CASCADE_LOG_FILE,
|
|
10
|
+
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
11
|
+
});
|
|
8
12
|
export function logCascade(entry) {
|
|
9
13
|
if (isTestMode()) {
|
|
10
14
|
return;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
|
-
import { firstChildOfType,
|
|
15
|
+
import { firstChildOfType, withTreeSitterRoot, resolveTreeSitterLanguage, walk, } from "./tree-sitter-shared.js";
|
|
16
16
|
// JS/TS/JSX share one grammar shape.
|
|
17
17
|
const JSTS = {
|
|
18
18
|
functionLike: new Set([
|
|
@@ -387,21 +387,18 @@ export class ComplexityClient {
|
|
|
387
387
|
if (!nodes)
|
|
388
388
|
return null;
|
|
389
389
|
let content;
|
|
390
|
-
let root;
|
|
391
390
|
try {
|
|
392
391
|
if (!fs.existsSync(absolutePath))
|
|
393
392
|
return null;
|
|
394
393
|
content = fs.readFileSync(absolutePath, "utf-8");
|
|
395
|
-
root = await parseTreeSitterRoot(absolutePath, content);
|
|
396
394
|
}
|
|
397
395
|
catch (err) {
|
|
398
|
-
this.log(`Read
|
|
396
|
+
this.log(`Read error for ${filePath}: ${err.message}`);
|
|
399
397
|
return null;
|
|
400
398
|
}
|
|
401
|
-
if (!root)
|
|
402
|
-
return null;
|
|
403
399
|
try {
|
|
404
|
-
|
|
400
|
+
const parsed = await withTreeSitterRoot(absolutePath, content, (root) => this.computeMetrics(absolutePath, content, root, nodes));
|
|
401
|
+
return parsed.parsed ? parsed.value : null;
|
|
405
402
|
}
|
|
406
403
|
catch (err) {
|
|
407
404
|
this.log(`Analysis error for ${filePath}: ${err.message}`);
|
|
@@ -60,6 +60,29 @@ export function parseMadgeSkips(stderr) {
|
|
|
60
60
|
const local = specifiers.filter((s) => s.startsWith(".") || s.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(s));
|
|
61
61
|
return { total, local };
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Defensive cap on concurrent madge spawns for a single `checkFilesBatch`
|
|
65
|
+
* call. A turn usually only touches 1-3 import-changed files, so this rarely
|
|
66
|
+
* binds — it just guards against a pathological turn (bulk rename/move) from
|
|
67
|
+
* fork-bombing subprocesses.
|
|
68
|
+
*/
|
|
69
|
+
const MADGE_BATCH_CONCURRENCY = 6;
|
|
70
|
+
/** Run `mapper` over `items` with at most `concurrency` in flight at once. */
|
|
71
|
+
async function mapWithConcurrency(items, concurrency, mapper) {
|
|
72
|
+
if (items.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
let nextIndex = 0;
|
|
75
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
76
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
77
|
+
while (true) {
|
|
78
|
+
const index = nextIndex++;
|
|
79
|
+
if (index >= items.length)
|
|
80
|
+
return;
|
|
81
|
+
await mapper(items[index]);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
await Promise.all(workers);
|
|
85
|
+
}
|
|
63
86
|
// --- Client ---
|
|
64
87
|
export class DependencyChecker {
|
|
65
88
|
available = null;
|
|
@@ -258,6 +281,33 @@ export class DependencyChecker {
|
|
|
258
281
|
return promise;
|
|
259
282
|
}
|
|
260
283
|
async runCheckFile(normalized, projectRoot) {
|
|
284
|
+
const spawnResult = await this.runMadgeSpawn(normalized, projectRoot);
|
|
285
|
+
if (!spawnResult.ok) {
|
|
286
|
+
return {
|
|
287
|
+
hasCircular: false,
|
|
288
|
+
circular: [],
|
|
289
|
+
checked: false,
|
|
290
|
+
cacheHit: false,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
this.lastCircular = spawnResult.circular;
|
|
294
|
+
this.circularFiles = spawnResult.circularFiles;
|
|
295
|
+
return {
|
|
296
|
+
hasCircular: spawnResult.circular.length > 0,
|
|
297
|
+
circular: spawnResult.circular.filter((d) => d.file === normalized || d.path.includes(normalized)),
|
|
298
|
+
checked: true,
|
|
299
|
+
cacheHit: false,
|
|
300
|
+
localSkips: spawnResult.localSkips,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Run madge on a single file and parse its cycle output. Pure: unlike
|
|
305
|
+
* `runCheckFile`, this does NOT mutate `lastCircular`/`circularFiles` — it
|
|
306
|
+
* hands the parsed result back so callers can apply the shared-state update
|
|
307
|
+
* themselves (`runCheckFile` does so immediately; `checkFilesBatch` defers
|
|
308
|
+
* it so concurrent spawns can't clobber each other's writes, see #766).
|
|
309
|
+
*/
|
|
310
|
+
async runMadgeSpawn(normalized, projectRoot) {
|
|
261
311
|
this.log(`Imports changed for ${path.basename(normalized)}, checking dependencies...`);
|
|
262
312
|
// Run madge on the specific file (fast)
|
|
263
313
|
try {
|
|
@@ -268,12 +318,7 @@ export class DependencyChecker {
|
|
|
268
318
|
});
|
|
269
319
|
if (result.error) {
|
|
270
320
|
this.log(`Check error: ${result.error.message}`);
|
|
271
|
-
return {
|
|
272
|
-
hasCircular: false,
|
|
273
|
-
circular: [],
|
|
274
|
-
checked: false,
|
|
275
|
-
cacheHit: false,
|
|
276
|
-
};
|
|
321
|
+
return { ok: false };
|
|
277
322
|
}
|
|
278
323
|
const output = result.stdout || "[]";
|
|
279
324
|
const parsed = JSON.parse(output);
|
|
@@ -291,29 +336,115 @@ export class DependencyChecker {
|
|
|
291
336
|
path: resolvedPaths,
|
|
292
337
|
});
|
|
293
338
|
}
|
|
294
|
-
this.lastCircular = circular;
|
|
295
|
-
this.circularFiles = circularFiles;
|
|
296
339
|
const skips = parseMadgeSkips(result.stderr || "");
|
|
297
340
|
if (skips.local.length > 0) {
|
|
298
341
|
this.log(`madge skipped ${skips.local.length} local file(s) (possible silent cycle-miss): ${skips.local.slice(0, 5).join(", ")}`);
|
|
299
342
|
}
|
|
300
|
-
return {
|
|
301
|
-
hasCircular: circular.length > 0,
|
|
302
|
-
circular: circular.filter((d) => d.file === normalized || d.path.includes(normalized)),
|
|
303
|
-
checked: true,
|
|
304
|
-
cacheHit: false,
|
|
305
|
-
localSkips: skips.local.length,
|
|
306
|
-
};
|
|
343
|
+
return { ok: true, circular, circularFiles, localSkips: skips.local.length };
|
|
307
344
|
}
|
|
308
345
|
catch (err) {
|
|
309
346
|
this.log(`Check error: ${err.message}`);
|
|
310
|
-
return {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
347
|
+
return { ok: false };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** Build the cache-hit `DepCheckResult` for `normalized` from current shared state. */
|
|
351
|
+
buildCachedResult(normalized) {
|
|
352
|
+
return {
|
|
353
|
+
hasCircular: this.circularFiles.has(normalized),
|
|
354
|
+
circular: this.lastCircular.filter((d) => d.file === normalized || d.path.includes(normalized)),
|
|
355
|
+
checked: true,
|
|
356
|
+
cacheHit: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Batch-check multiple files for circular deps in one turn-end pass,
|
|
361
|
+
* running the madge subprocess spawns concurrently (bounded by
|
|
362
|
+
* `MADGE_BATCH_CONCURRENCY`) instead of one at a time (#766).
|
|
363
|
+
*
|
|
364
|
+
* Equivalence with the sequential `for…await checkFile(file)` loop it
|
|
365
|
+
* replaces:
|
|
366
|
+
* - Classification (existence + `importsChanged`) runs synchronously in
|
|
367
|
+
* original array order first — identical to what each sequential
|
|
368
|
+
* `checkFile()` call would do, including the `importCache` side effect.
|
|
369
|
+
* - Only the actual madge spawns for import-changed ("miss") files run
|
|
370
|
+
* concurrently; each one's parsed result stays LOCAL (no shared-state
|
|
371
|
+
* write) until every spawn has settled.
|
|
372
|
+
* - The shared `lastCircular`/`circularFiles` state is then folded by
|
|
373
|
+
* replaying the miss results in ORIGINAL array order (not completion
|
|
374
|
+
* order), so the final state — and each cache-hit file's result, which
|
|
375
|
+
* reads the state as of its position in the array — is byte-for-byte
|
|
376
|
+
* what the sequential last-write-wins loop would have produced. No
|
|
377
|
+
* concurrent write can clobber another's, because none are applied
|
|
378
|
+
* until after `Promise.all` resolves.
|
|
379
|
+
*/
|
|
380
|
+
async checkFilesBatch(filePaths, cwd) {
|
|
381
|
+
const projectRoot = path.resolve(cwd || process.cwd());
|
|
382
|
+
const results = new Map();
|
|
383
|
+
const entries = [];
|
|
384
|
+
for (const file of filePaths) {
|
|
385
|
+
const normalized = path.resolve(projectRoot, file);
|
|
386
|
+
if (!fs.existsSync(normalized)) {
|
|
387
|
+
entries.push({ kind: "missing", file });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
entries.push(this.importsChanged(normalized)
|
|
391
|
+
? { kind: "miss", file, normalized }
|
|
392
|
+
: { kind: "hit", file, normalized });
|
|
393
|
+
}
|
|
394
|
+
const missEntries = entries.filter((e) => e.kind === "miss");
|
|
395
|
+
const notAvailableResult = {
|
|
396
|
+
hasCircular: false,
|
|
397
|
+
circular: [],
|
|
398
|
+
checked: false,
|
|
399
|
+
cacheHit: false,
|
|
400
|
+
};
|
|
401
|
+
if (missEntries.length > 0 && !(await this.ensureAvailable())) {
|
|
402
|
+
// madge unavailable: mirrors checkFile()'s "not available" branch for
|
|
403
|
+
// every miss; hits still read whatever shared state already exists.
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (entry.kind === "hit") {
|
|
406
|
+
results.set(entry.file, this.buildCachedResult(entry.normalized));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
results.set(entry.file, notAvailableResult);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
// Run the concurrent (bounded) madge spawns for the miss set only. Each
|
|
415
|
+
// result stays local — keyed by normalized path — until folded below.
|
|
416
|
+
const spawnResults = new Map();
|
|
417
|
+
await mapWithConcurrency(missEntries, MADGE_BATCH_CONCURRENCY, async (entry) => {
|
|
418
|
+
spawnResults.set(entry.normalized, await this.runMadgeSpawn(entry.normalized, projectRoot));
|
|
419
|
+
});
|
|
420
|
+
// Fold in original order: each miss overwrites the shared state exactly
|
|
421
|
+
// as the sequential loop would, and each hit is resolved against the
|
|
422
|
+
// state as folded up to (but not past) its own position.
|
|
423
|
+
for (const entry of entries) {
|
|
424
|
+
if (entry.kind === "missing") {
|
|
425
|
+
results.set(entry.file, notAvailableResult);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (entry.kind === "hit") {
|
|
429
|
+
results.set(entry.file, this.buildCachedResult(entry.normalized));
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const spawnResult = spawnResults.get(entry.normalized);
|
|
433
|
+
if (!spawnResult || !spawnResult.ok) {
|
|
434
|
+
results.set(entry.file, notAvailableResult);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
this.lastCircular = spawnResult.circular;
|
|
438
|
+
this.circularFiles = spawnResult.circularFiles;
|
|
439
|
+
results.set(entry.file, {
|
|
440
|
+
hasCircular: spawnResult.circular.length > 0,
|
|
441
|
+
circular: spawnResult.circular.filter((d) => d.file === entry.normalized || d.path.includes(entry.normalized)),
|
|
442
|
+
checked: true,
|
|
314
443
|
cacheHit: false,
|
|
315
|
-
|
|
444
|
+
localSkips: spawnResult.localSkips,
|
|
445
|
+
});
|
|
316
446
|
}
|
|
447
|
+
return results;
|
|
317
448
|
}
|
|
318
449
|
/**
|
|
319
450
|
* Format circular dependency warning for LLM
|
|
@@ -73,9 +73,29 @@ const deferredThisSession = new Set();
|
|
|
73
73
|
export function normalizeMessage(message) {
|
|
74
74
|
return message.replace(/\s+/g, " ").trim().toLowerCase();
|
|
75
75
|
}
|
|
76
|
+
// Anchor derivation chokepoint (#1024, #210 class): the `dd:`/`ddw:` id builders
|
|
77
|
+
// (computeStrictAnchor/computeWeakAnchor) both derive their path component here,
|
|
78
|
+
// so a mark and its later lookup diverge whenever the two callers pass different
|
|
79
|
+
// path FORMS of the same file. That is exactly the bug: the mark tool
|
|
80
|
+
// (lens-diagnostic-mark.ts) passes a RAW cwd / `path.resolve(cwd, arg)`, while
|
|
81
|
+
// the dispatch read side (dispatcher.ts createDispatchContext) passes
|
|
82
|
+
// `normalizeMapKey`-canonicalized cwd/filePath — so a Windows drive/segment
|
|
83
|
+
// case, symlink/realpath, or slash difference between the two forms silently
|
|
84
|
+
// orphans the agent's own false-positive/flagged mark (a #533 dropped-signal).
|
|
85
|
+
// Canonicalize BOTH inputs through `normalizeMapKey` (the SAME normalizer the
|
|
86
|
+
// read side already relies on — realpathSync.native on Windows) BEFORE computing
|
|
87
|
+
// the relative path, so write and read produce identical anchors regardless of
|
|
88
|
+
// the form the caller held. `normalizeMapKey` is idempotent, so the already-
|
|
89
|
+
// canonicalized read side is unaffected; the realpath I/O is acceptable here
|
|
90
|
+
// because dispositions are marked/applied far less often than the per-write
|
|
91
|
+
// widget hot path, and the read side already pays exactly this cost. Semantics
|
|
92
|
+
// are unchanged: the `..`-escape fallback still returns the canonical filePath,
|
|
93
|
+
// only now in the same canonical form the non-escape branch uses.
|
|
76
94
|
function relativeFile(filePath, cwd) {
|
|
77
|
-
const
|
|
78
|
-
|
|
95
|
+
const canonicalCwd = normalizeMapKey(cwd);
|
|
96
|
+
const canonicalFile = normalizeMapKey(filePath);
|
|
97
|
+
const rel = path.relative(canonicalCwd, canonicalFile).replace(/\\/g, "/");
|
|
98
|
+
return rel && !rel.startsWith("..") ? rel : canonicalFile;
|
|
79
99
|
}
|
|
80
100
|
/** Site-specific anchor — see module doc. Used only for false-positive. */
|
|
81
101
|
export function computeStrictAnchor(args) {
|
|
@@ -58,6 +58,35 @@ export function isNosemgrepSuppressed(d, content) {
|
|
|
58
58
|
return (checkLine(lines[startLine], false) ||
|
|
59
59
|
checkLine(lines[startLine - 1], true));
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* zizmor's own native inline suppression: `# zizmor: ignore[audit-id[,audit-id]]`
|
|
63
|
+
* on the finding's own line (https://docs.zizmor.sh/usage/#ignoring-results —
|
|
64
|
+
* zizmor calls this "inline ignores"). Honoring it here mirrors
|
|
65
|
+
* `isNosemgrepSuppressed` for Opengrep — a per-finding suppression path a repo
|
|
66
|
+
* can reach for even without its own `zizmor.yml` (#971): e.g. a `# zizmor:
|
|
67
|
+
* ignore[artipacked]` on a checkout-only job's `actions/checkout` step, or
|
|
68
|
+
* `# zizmor: ignore[adhoc-packages]` on a `npm pack` tarball's local install
|
|
69
|
+
* line, both cases the audit has no way to tell "checkout-only, no artifact
|
|
70
|
+
* upload" or "this is testing our own just-built tarball" apart from a
|
|
71
|
+
* remote-package install without this local, human-authored signal.
|
|
72
|
+
*/
|
|
73
|
+
const ZIZMOR_IGNORE_RE = /#\s*zizmor:\s*ignore\[([^\]]+)\]/i;
|
|
74
|
+
export function isZizmorIgnoreSuppressed(d, content) {
|
|
75
|
+
const startLine = d.range?.start?.line; // 0-based
|
|
76
|
+
if (startLine == null)
|
|
77
|
+
return false;
|
|
78
|
+
const line = content.split("\n")[startLine];
|
|
79
|
+
if (!line)
|
|
80
|
+
return false;
|
|
81
|
+
const match = ZIZMOR_IGNORE_RE.exec(line);
|
|
82
|
+
if (!match)
|
|
83
|
+
return false;
|
|
84
|
+
const ruleId = String(d.code ?? "").toLowerCase();
|
|
85
|
+
return match[1]
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((s) => s.trim().toLowerCase())
|
|
88
|
+
.includes(ruleId);
|
|
89
|
+
}
|
|
61
90
|
// #277 R7: every profile below used this exact same rule (ERROR-severity
|
|
62
91
|
// findings block only when the workspace opted into curated/authored rules;
|
|
63
92
|
// everything else stays advisory) — shared here instead of copy-pasted per
|
|
@@ -118,6 +147,11 @@ export const AUXILIARY_LSP_PROFILES = [
|
|
|
118
147
|
allowBlocking: (cwd) => Boolean(findLocalZizmorConfig(cwd)),
|
|
119
148
|
semantic: blockOnErrorWhenAllowed,
|
|
120
149
|
defectClass: (d) => classifyDefect(String(d.code ?? ""), "zizmor", d.message ?? ""),
|
|
150
|
+
// Honor zizmor's own native per-finding suppression (#971) — a documented
|
|
151
|
+
// escape hatch for a workflow-context judgment call the audit itself has no
|
|
152
|
+
// way to make (checkout-only vs. artifact-uploading job; a locally-built
|
|
153
|
+
// tarball install vs. an arbitrary remote package).
|
|
154
|
+
isSuppressed: isZizmorIgnoreSuppressed,
|
|
121
155
|
},
|
|
122
156
|
{
|
|
123
157
|
serverId: "typos",
|
|
@@ -22,6 +22,7 @@ import { isTestFile } from "../file-utils.js";
|
|
|
22
22
|
import { getPrimaryDispatchGroup } from "../language-policy.js";
|
|
23
23
|
import { resolveLanguageRootForFile } from "../language-profile.js";
|
|
24
24
|
import { logLatency } from "../latency-logger.js";
|
|
25
|
+
import { isSpawnableCommand } from "../installer/index.js";
|
|
25
26
|
import { normalizeMapKey } from "../path-utils.js";
|
|
26
27
|
import { loadPiLensProjectConfig } from "../project-lens-config.js";
|
|
27
28
|
import { RUNTIME_CONFIG, getRunnerTimeoutFloorMs } from "../runtime-config.js";
|
|
@@ -79,6 +80,12 @@ async function checkToolAvailability(command, facts) {
|
|
|
79
80
|
if (cached !== undefined) {
|
|
80
81
|
return cached;
|
|
81
82
|
}
|
|
83
|
+
// A command that isn't even on disk can't pass a --version probe; the ~μs
|
|
84
|
+
// stat/PATH walk saves a guaranteed-to-fail spawn round-trip per cold tool.
|
|
85
|
+
if (!(await isSpawnableCommand(command))) {
|
|
86
|
+
facts.setSessionFact(key, false);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
82
89
|
try {
|
|
83
90
|
const result = await safeSpawnAsync(command, ["--version"], {
|
|
84
91
|
timeout: 5000,
|
|
@@ -563,7 +570,15 @@ export async function dispatchForFile(ctx, groups, registry, onRunnerResult) {
|
|
|
563
570
|
// #690: agent/user disposition layer — drop false-positive/suppress marks
|
|
564
571
|
// and anything deferred this session. flagged marks are left in place;
|
|
565
572
|
// lens_diagnostics tags them at render time via the same anchor.
|
|
566
|
-
|
|
573
|
+
//
|
|
574
|
+
// #1030: anchor + store MUST key off the PROJECT ROOT, not ctx.cwd. In a
|
|
575
|
+
// monorepo ctx.cwd is the nested language root (resolveLanguageRootForFile,
|
|
576
|
+
// used for tool/config resolution), but the mark tool writes dispositions
|
|
577
|
+
// under runtime.projectRoot. Keying the read on ctx.cwd computed a different
|
|
578
|
+
// anchor AND opened a different diagnostic-dispositions.json (getProjectDataDir
|
|
579
|
+
// is keyed on cwd), so every false-positive/flagged/defer on a file under a
|
|
580
|
+
// nested marker silently no-op'd. Read from the project root to match the write.
|
|
581
|
+
const dispositionFiltered = applyDispositions(inlineSuppressed, ctx.projectRoot ?? ctx.cwd, ctx.filePath, fileContent);
|
|
567
582
|
let visibleDiagnostics = dispositionFiltered;
|
|
568
583
|
let resolvedCount = 0;
|
|
569
584
|
if (ctx.deltaMode && previousBaseline) {
|
|
@@ -19,6 +19,17 @@ export class FactStore {
|
|
|
19
19
|
hasFileFact(filePath, factId) {
|
|
20
20
|
return this.fileFacts.get(normalizeMapKey(filePath))?.has(factId) ?? false;
|
|
21
21
|
}
|
|
22
|
+
/** Drop one file fact without disturbing the file's derived facts.
|
|
23
|
+
* Removes the per-file entry too when the deleted fact was its last value. */
|
|
24
|
+
deleteFileFact(filePath, factId) {
|
|
25
|
+
const key = normalizeMapKey(filePath);
|
|
26
|
+
const facts = this.fileFacts.get(key);
|
|
27
|
+
if (!facts)
|
|
28
|
+
return;
|
|
29
|
+
facts.delete(factId);
|
|
30
|
+
if (facts.size === 0)
|
|
31
|
+
this.fileFacts.delete(key);
|
|
32
|
+
}
|
|
22
33
|
/** Clear facts for one specific file only. Use at the start of each per-file dispatch call.
|
|
23
34
|
* Preserves facts for other files computed in the same turn.
|
|
24
35
|
* Normalizes filePath internally — callers pass raw paths. */
|
|
@@ -217,6 +217,19 @@ function collectReceiverTypes(body, params) {
|
|
|
217
217
|
});
|
|
218
218
|
return types;
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Does this function/method declaration have an explicit `: Promise<...>`
|
|
222
|
+
* return type? The `type_annotation` for a return type is a DIRECT child of
|
|
223
|
+
* the function node itself (after `formal_parameters`, before the body) —
|
|
224
|
+
* distinct from a parameter's own `type_annotation`, which is nested inside
|
|
225
|
+
* `formal_parameters` and therefore invisible to `firstChildOfType` here.
|
|
226
|
+
*/
|
|
227
|
+
function hasExplicitPromiseReturnType(node) {
|
|
228
|
+
const returnType = firstChildOfType(node, "type_annotation");
|
|
229
|
+
if (!returnType)
|
|
230
|
+
return false;
|
|
231
|
+
return /:\s*Promise\b/.test(returnType.text);
|
|
232
|
+
}
|
|
220
233
|
function hasAwaitInNode(node) {
|
|
221
234
|
let found = false;
|
|
222
235
|
walk(node, (n) => {
|
|
@@ -283,6 +296,7 @@ export const functionFactProvider = {
|
|
|
283
296
|
isPassThroughWrapper: passThrough.pass,
|
|
284
297
|
passThroughTarget: passThrough.target,
|
|
285
298
|
isBoundaryWrapper,
|
|
299
|
+
hasExplicitPromiseReturnType: hasExplicitPromiseReturnType(node),
|
|
286
300
|
cyclomaticComplexity: calcCyclomaticComplexity(body),
|
|
287
301
|
maxNestingDepth: calcMaxNestingDepth(body),
|
|
288
302
|
outgoingCalls: collectOutgoingCalls(body),
|