pi-lens 3.8.73 → 4.0.0
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 +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- 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 +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- 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 +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- 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/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -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-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -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/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -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-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- 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/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- 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/tsx/no-nested-links.yml +4 -6
- 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/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- 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
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
+
import { BoundedLruCache } from "../bounded-cache.js";
|
|
17
18
|
import { normalizeFilePath, walkUpDirs } from "../path-utils.js";
|
|
18
19
|
// Per-process cache of resolved gitdir location keyed by cwd — the gitdir
|
|
19
20
|
// location itself is stable for the life of the process; only HEAD's
|
|
20
21
|
// CONTENT changes (on commit/checkout), so that's re-read fresh every call.
|
|
21
|
-
const _gitDirCache = new
|
|
22
|
+
const _gitDirCache = new BoundedLruCache(32);
|
|
22
23
|
export function _resetGitIdentityCacheForTests() {
|
|
23
24
|
_gitDirCache.clear();
|
|
24
25
|
}
|
|
@@ -90,7 +90,7 @@ function resolveDart(cwd, filePath, source) {
|
|
|
90
90
|
// JS/TS extension from the specifier before re-appending candidate extensions
|
|
91
91
|
// lets that universal pattern resolve to the real source file. Exported so
|
|
92
92
|
// builder.ts's warm `localImportToFile` shares the exact same regex (#694).
|
|
93
|
-
export const JS_TS_EXT_RE = /\.(mjs|cjs|jsx?|tsx?)$/i;
|
|
93
|
+
export const JS_TS_EXT_RE = /\.(mjs|cjs|mts|cts|jsx?|tsx?)$/i;
|
|
94
94
|
/**
|
|
95
95
|
* Ordered candidate list for resolving a relative JS/TS import specifier
|
|
96
96
|
* (already resolved to an absolute base path) to an on-disk file, with
|
|
@@ -122,13 +122,18 @@ function jsTsExtensionCandidates(base, strippedBase, ext) {
|
|
|
122
122
|
candidates.push(`${strippedBase}.mts`);
|
|
123
123
|
if (ext === ".cjs")
|
|
124
124
|
candidates.push(`${strippedBase}.cts`);
|
|
125
|
-
|
|
125
|
+
if (ext === ".mts")
|
|
126
|
+
candidates.push(`${strippedBase}.mts`);
|
|
127
|
+
if (ext === ".cts")
|
|
128
|
+
candidates.push(`${strippedBase}.cts`);
|
|
129
|
+
candidates.push(`${strippedBase}.ts`, `${strippedBase}.tsx`, `${strippedBase}.mts`, `${strippedBase}.cts`);
|
|
126
130
|
// The literal specifier path (covers both "no extension in the specifier"
|
|
127
131
|
// — base === strippedBase — and the compiled/as-written extension itself).
|
|
128
|
-
candidates.push(base);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
candidates.push(base, `${strippedBase}.js`, `${strippedBase}.jsx`, `${strippedBase}.mjs`, `${strippedBase}.cjs`);
|
|
133
|
+
for (const index of ["index.ts", "index.tsx", "index.mts", "index.cts", "index.js", "index.jsx", "index.mjs", "index.cjs"]) {
|
|
134
|
+
candidates.push(path.join(base, index));
|
|
135
|
+
}
|
|
136
|
+
return [...new Set(candidates)];
|
|
132
137
|
}
|
|
133
138
|
export function jsTsCandidatePaths(filePath, source) {
|
|
134
139
|
const base = path.resolve(path.dirname(filePath), source);
|
|
@@ -198,10 +203,16 @@ function workspaceEntryCandidates(pkgRoot) {
|
|
|
198
203
|
// missing/unreadable package.json — fall through to index candidates
|
|
199
204
|
}
|
|
200
205
|
const candidates = [];
|
|
201
|
-
if (main)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
if (main) {
|
|
207
|
+
const mainPath = path.resolve(pkgRoot, main);
|
|
208
|
+
const ext = path.extname(mainPath).toLowerCase();
|
|
209
|
+
const stripped = mainPath.replace(JS_TS_EXT_RE, "");
|
|
210
|
+
candidates.push(...jsTsExtensionCandidates(mainPath, stripped, ext));
|
|
211
|
+
}
|
|
212
|
+
for (const base of [pkgRoot, path.join(pkgRoot, "src")]) {
|
|
213
|
+
candidates.push(path.join(base, "index.ts"), path.join(base, "index.tsx"), path.join(base, "index.mts"), path.join(base, "index.cts"), path.join(base, "index.js"), path.join(base, "index.jsx"), path.join(base, "index.mjs"), path.join(base, "index.cjs"));
|
|
214
|
+
}
|
|
215
|
+
return [...new Set(candidates)];
|
|
205
216
|
}
|
|
206
217
|
/** Candidate paths for a workspace-package SUBPATH import (`@scope/pkg/foo`)
|
|
207
218
|
* — same source-twin-preferring extension probe as a relative import, rooted
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { serveGzipStageWorker, } from "../gzip-stage-write.js";
|
|
2
|
+
// Streamed gzip stage write runs off the main thread via the shared core (see
|
|
3
|
+
// clients/gzip-stage-write.ts); this worker only echoes its routing fields.
|
|
4
|
+
serveGzipStageWorker((request) => ({
|
|
5
|
+
id: request.id,
|
|
6
|
+
cwd: request.cwd,
|
|
7
|
+
generation: request.generation,
|
|
8
|
+
stagePath: request.stagePath,
|
|
9
|
+
elements: request.elements,
|
|
10
|
+
}));
|
|
@@ -76,6 +76,10 @@ export function computeImpactCascade(graph, changedFile, moduleGraph) {
|
|
|
76
76
|
const normalizedFile = normalizeMapKey(changedFile);
|
|
77
77
|
const fileNodeId = graph.fileNodes.get(normalizedFile);
|
|
78
78
|
if (!fileNodeId) {
|
|
79
|
+
// #1023: the changed file has no node in the graph — we CANNOT enumerate
|
|
80
|
+
// its dependents, so this empty result is "couldn't compute", NOT "nothing
|
|
81
|
+
// depends on it". Mark it indeterminate so callers surface an honest
|
|
82
|
+
// advisory instead of an all-clear (#533).
|
|
79
83
|
return {
|
|
80
84
|
filePath: normalizedFile,
|
|
81
85
|
changedSymbols: [],
|
|
@@ -83,6 +87,7 @@ export function computeImpactCascade(graph, changedFile, moduleGraph) {
|
|
|
83
87
|
directCallers: [],
|
|
84
88
|
neighborFiles: [],
|
|
85
89
|
riskFlags: [],
|
|
90
|
+
indeterminate: { reason: "missing_node" },
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
const changedSymbols = graph.changedSymbolsByFile.get(normalizedFile) ?? [];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { normalizeMapKey } from "../path-utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* THE producer/consumer hash contract, in one place (#955 review): scanner
|
|
5
|
+
* publication and builder acceptance must produce byte-identical digests or
|
|
6
|
+
* every lookup silently misses forever. Both sides hash the exact decoded
|
|
7
|
+
* string they parsed.
|
|
8
|
+
*/
|
|
9
|
+
export function reviewGraphIrContentHash(content) {
|
|
10
|
+
return createHash("sha256").update(content).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
// Process-local handoff only. Values are compact extracted data: never source
|
|
13
|
+
// strings, web-tree-sitter trees, or FactStores. Bounds (#955 review — the
|
|
14
|
+
// #886 retention class): a small root bound, a per-root entry cap, and
|
|
15
|
+
// consume-once semantics — an entry is DELETED when the graph accepts it, and
|
|
16
|
+
// the full build clears the root's leftovers when it finishes, so the
|
|
17
|
+
// registry's lifetime is the scan-to-build window, not the process lifetime.
|
|
18
|
+
const MAX_ROOTS = 8;
|
|
19
|
+
const MAX_FILES_PER_ROOT = 20_000;
|
|
20
|
+
const roots = new Map();
|
|
21
|
+
let accepted = 0;
|
|
22
|
+
let rejected = 0;
|
|
23
|
+
function rootKey(cwd) {
|
|
24
|
+
return normalizeMapKey(cwd);
|
|
25
|
+
}
|
|
26
|
+
export function publishReviewGraphFileIr(cwd, entry) {
|
|
27
|
+
// Only consumable entries are worth storing: the builder rejects
|
|
28
|
+
// incomplete entries and has nothing to read without `structural`, so
|
|
29
|
+
// publishing them would be pure retention — and an incomplete late
|
|
30
|
+
// publication must never clobber a still-valid entry (#955 review).
|
|
31
|
+
if (!entry.complete || !entry.structural)
|
|
32
|
+
return;
|
|
33
|
+
const key = rootKey(cwd);
|
|
34
|
+
let files = roots.get(key);
|
|
35
|
+
if (!files) {
|
|
36
|
+
files = new Map();
|
|
37
|
+
roots.set(key, files);
|
|
38
|
+
while (roots.size > MAX_ROOTS) {
|
|
39
|
+
const oldest = roots.keys().next().value;
|
|
40
|
+
if (oldest === undefined)
|
|
41
|
+
break;
|
|
42
|
+
roots.delete(oldest);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const fileKey = normalizeMapKey(entry.filePath);
|
|
46
|
+
files.delete(fileKey);
|
|
47
|
+
while (files.size >= MAX_FILES_PER_ROOT) {
|
|
48
|
+
const oldest = files.keys().next().value;
|
|
49
|
+
if (oldest === undefined)
|
|
50
|
+
break;
|
|
51
|
+
files.delete(oldest);
|
|
52
|
+
}
|
|
53
|
+
files.set(fileKey, { ...entry, filePath: fileKey });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Consume-once: a hash-fresh entry is removed as it is returned. The graph is
|
|
57
|
+
* about to ingest these arrays, so keeping a second registry copy alive would
|
|
58
|
+
* both duplicate the heap and leave a write-through aliasing target (#955
|
|
59
|
+
* review); a later build re-parses or waits for the next scan to republish.
|
|
60
|
+
*/
|
|
61
|
+
export function getFreshReviewGraphFileIr(cwd, filePath, contentHash) {
|
|
62
|
+
const files = roots.get(rootKey(cwd));
|
|
63
|
+
const fileKey = normalizeMapKey(filePath);
|
|
64
|
+
const entry = files?.get(fileKey);
|
|
65
|
+
if (!entry)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (entry.contentHash !== contentHash) {
|
|
68
|
+
// Present but stale — the only honest "rejected" (#955 review: absence
|
|
69
|
+
// is not rejection; counting it made the stat unusable).
|
|
70
|
+
rejected++;
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
files?.delete(fileKey);
|
|
74
|
+
accepted++;
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
/** Build-end/session-reset seam; builds also work when this is never called. */
|
|
78
|
+
export function clearReviewGraphFileIr(cwd) {
|
|
79
|
+
if (cwd === undefined)
|
|
80
|
+
roots.clear();
|
|
81
|
+
else
|
|
82
|
+
roots.delete(rootKey(cwd));
|
|
83
|
+
}
|
|
84
|
+
export function getReviewGraphIrStats() {
|
|
85
|
+
return { accepted, rejected };
|
|
86
|
+
}
|
|
87
|
+
export function resetReviewGraphIrStats() {
|
|
88
|
+
accepted = 0;
|
|
89
|
+
rejected = 0;
|
|
90
|
+
}
|
|
@@ -55,3 +55,49 @@
|
|
|
55
55
|
export function buildSymbolId(filePath, name, kind, startLine) {
|
|
56
56
|
return `${filePath}:${name}:${kind}:${startLine}`;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Structural shape of a kind token in a canonical symbol id: every kind this
|
|
60
|
+
* repo mints — the fixed `buildSymbolId` vocabulary (`function`, `method`,
|
|
61
|
+
* `class`, ...) AND the open-ended LSP-fallback vocabulary from
|
|
62
|
+
* `lspSymbolKindName` (`enum`, `struct`, `type-parameter`, `enum-member`, the
|
|
63
|
+
* `lsp-symbol-<n>` catch-all for unknown LSP `SymbolKind` numbers, ...) —
|
|
64
|
+
* is a lowercase word optionally hyphen-segmented, never containing a colon.
|
|
65
|
+
* Matching this SHAPE (rather than whitelisting specific kind strings) keeps
|
|
66
|
+
* `parseSymbolKey` in agreement with whatever kind vocabulary either minter
|
|
67
|
+
* uses, present or future, without needing a matching update here.
|
|
68
|
+
*/
|
|
69
|
+
const KIND_TOKEN_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
70
|
+
/**
|
|
71
|
+
* Parse both canonical symbol IDs and the legacy `file:name` compatibility
|
|
72
|
+
* shape. Canonical IDs are recognized from their typed line suffix, so drive
|
|
73
|
+
* letters and colons in POSIX filenames are data rather than separators. A
|
|
74
|
+
* known file hint makes the legacy form lossless too; without it, legacy IDs
|
|
75
|
+
* remain inherently ambiguous and use their historical final separator.
|
|
76
|
+
*/
|
|
77
|
+
export function parseSymbolKey(key, knownFilePath) {
|
|
78
|
+
if (key.startsWith("file:")) {
|
|
79
|
+
return { filePath: key.slice("file:".length) };
|
|
80
|
+
}
|
|
81
|
+
const canonical = /^(.*):([^:]*):([^:]+):(\d+)$/.exec(key);
|
|
82
|
+
if (canonical && KIND_TOKEN_RE.test(canonical[3])) {
|
|
83
|
+
return {
|
|
84
|
+
filePath: canonical[1],
|
|
85
|
+
symbolName: canonical[2] || undefined,
|
|
86
|
+
kind: canonical[3],
|
|
87
|
+
line: Number(canonical[4]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (knownFilePath && key.startsWith(`${knownFilePath}:`)) {
|
|
91
|
+
return {
|
|
92
|
+
filePath: knownFilePath,
|
|
93
|
+
symbolName: key.slice(knownFilePath.length + 1) || undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const separator = key.lastIndexOf(":");
|
|
97
|
+
return separator < 0
|
|
98
|
+
? { filePath: key }
|
|
99
|
+
: {
|
|
100
|
+
filePath: key.slice(0, separator),
|
|
101
|
+
symbolName: key.slice(separator + 1) || undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
|
+
import { BoundedLruCache } from "../bounded-cache.js";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { findGoverningTsconfigDir, getDirectoryMarkers, } from "../workspace-topology.js";
|
|
5
|
-
const cache = new
|
|
6
|
-
const referencesCache = new
|
|
6
|
+
const cache = new BoundedLruCache(64);
|
|
7
|
+
const referencesCache = new BoundedLruCache(64);
|
|
7
8
|
/** Strip JSONC comments and trailing commas without touching string contents. */
|
|
8
9
|
function parseJsonc(content) {
|
|
9
10
|
let output = "";
|
|
@@ -44,6 +45,50 @@ function parseJsonc(content) {
|
|
|
44
45
|
}
|
|
45
46
|
return JSON.parse(output.replace(/,\s*([}\]])/g, "$1"));
|
|
46
47
|
}
|
|
48
|
+
function configSignature(configPath) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = fs.statSync(configPath);
|
|
51
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "missing";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function configDependencyPaths(configPath) {
|
|
58
|
+
const paths = new Set();
|
|
59
|
+
const visit = (currentPath) => {
|
|
60
|
+
const normalized = path.resolve(currentPath);
|
|
61
|
+
if (paths.has(normalized))
|
|
62
|
+
return;
|
|
63
|
+
paths.add(normalized);
|
|
64
|
+
let json;
|
|
65
|
+
try {
|
|
66
|
+
json = parseJsonc(fs.readFileSync(normalized, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof json.extends === "string") {
|
|
72
|
+
const parent = resolveExtends(normalized, json.extends);
|
|
73
|
+
if (parent)
|
|
74
|
+
visit(parent);
|
|
75
|
+
}
|
|
76
|
+
for (const reference of json.references ?? []) {
|
|
77
|
+
if (typeof reference?.path !== "string")
|
|
78
|
+
continue;
|
|
79
|
+
const referenced = resolveReferenceConfig(normalized, reference.path);
|
|
80
|
+
if (referenced)
|
|
81
|
+
visit(referenced);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
visit(configPath);
|
|
85
|
+
return [...paths].sort((a, b) => a.localeCompare(b));
|
|
86
|
+
}
|
|
87
|
+
function dependencySignature(configPath) {
|
|
88
|
+
return configDependencyPaths(configPath)
|
|
89
|
+
.map((dependency) => `${dependency}:${configSignature(dependency)}`)
|
|
90
|
+
.join("|");
|
|
91
|
+
}
|
|
47
92
|
function resolveExtends(configPath, value) {
|
|
48
93
|
if (!value.startsWith("."))
|
|
49
94
|
return undefined;
|
|
@@ -180,14 +225,17 @@ function collectReferencedProjects(configPath, result, visited) {
|
|
|
180
225
|
}
|
|
181
226
|
/** Find and parse the nearest governing tsconfig, cached per importer directory. */
|
|
182
227
|
export function parseTsconfigPaths(cwd, homeDir = os.homedir()) {
|
|
183
|
-
const
|
|
228
|
+
const normalizedCwd = path.resolve(cwd);
|
|
229
|
+
const configDir = findGoverningTsconfigDir(normalizedCwd, homeDir);
|
|
230
|
+
const configPath = configDir ? path.join(configDir, "tsconfig.json") : "";
|
|
231
|
+
const signature = configPath ? dependencySignature(configPath) : "missing";
|
|
232
|
+
const key = `${normalizedCwd}|${configPath}|${signature}`;
|
|
184
233
|
const cached = cache.get(key);
|
|
185
234
|
if (cached)
|
|
186
235
|
return cached;
|
|
187
236
|
// Home-guarding is enforced inside findGoverningTsconfigDir's walk itself
|
|
188
237
|
// (via workspace-topology's shared isAtOrAboveHomeDir ceiling), so a hit
|
|
189
238
|
// here is never at/above homeDir.
|
|
190
|
-
const configDir = findGoverningTsconfigDir(key, homeDir);
|
|
191
239
|
if (!configDir) {
|
|
192
240
|
cache.set(key, []);
|
|
193
241
|
return [];
|
|
@@ -224,11 +272,14 @@ export function aliasedImportTargets(specifier, importerDir) {
|
|
|
224
272
|
}
|
|
225
273
|
/** Resolve an exact package-name import through the governing config's project references. */
|
|
226
274
|
export function referencedProjectImportTarget(specifier, importerDir) {
|
|
227
|
-
const
|
|
275
|
+
const normalizedImporterDir = path.resolve(importerDir);
|
|
276
|
+
const governingDir = findGoverningTsconfigDir(normalizedImporterDir);
|
|
277
|
+
const governingPath = governingDir ? path.join(governingDir, "tsconfig.json") : "";
|
|
278
|
+
const key = `${normalizedImporterDir}|${governingPath}|${governingPath ? dependencySignature(governingPath) : "missing"}`;
|
|
228
279
|
let projects = referencesCache.get(key);
|
|
229
280
|
if (!projects) {
|
|
230
281
|
projects = new Map();
|
|
231
|
-
const configDir =
|
|
282
|
+
const configDir = governingDir;
|
|
232
283
|
if (configDir) {
|
|
233
284
|
collectReferencedProjects(path.join(configDir, "tsconfig.json"), projects, new Set());
|
|
234
285
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "node:fs";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { getProjectIgnoreMatcher, isExcludedDirName } from "../file-utils.js";
|
|
9
|
+
import { getProjectIgnoreMatcher, isExcludedDirName, } from "../file-utils.js";
|
|
10
10
|
import { normalizeMapKey } from "../path-utils.js";
|
|
11
11
|
import { getWorkspaceManifestMarkers } from "../workspace-topology.js";
|
|
12
12
|
// Cap on candidate sub-directories probed per workspace glob level (#262).
|
|
@@ -387,6 +387,7 @@ export function buildModuleGraph(cwd) {
|
|
|
387
387
|
}
|
|
388
388
|
export function clearModuleGraphCache() {
|
|
389
389
|
_moduleGraphCache = null;
|
|
390
|
+
_moduleSourceFilesMemo.clear();
|
|
390
391
|
}
|
|
391
392
|
/**
|
|
392
393
|
* Find the module that owns a given file path.
|
|
@@ -419,13 +420,40 @@ export function getDownstreamModules(graph, moduleName) {
|
|
|
419
420
|
}
|
|
420
421
|
return [...downstream];
|
|
421
422
|
}
|
|
423
|
+
const _moduleSourceFilesMemo = new Map();
|
|
424
|
+
const MTIME_GRANULARITY_GUARD_MS = 2_000;
|
|
425
|
+
function isMemoEntryFresh(entry) {
|
|
426
|
+
const validatedAt = Date.now();
|
|
427
|
+
for (const { dir, mtimeMs, stampedAt } of entry.dirs) {
|
|
428
|
+
if (validatedAt - stampedAt < MTIME_GRANULARITY_GUARD_MS)
|
|
429
|
+
return false;
|
|
430
|
+
try {
|
|
431
|
+
if (fs.statSync(dir).mtimeMs !== mtimeMs)
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
422
440
|
/**
|
|
423
|
-
* Get representative source files in a module.
|
|
441
|
+
* Get representative source files in a module. Memoized per module root (see
|
|
442
|
+
* above) — the returned array is a copy; mutating it does not affect the memo.
|
|
424
443
|
*/
|
|
425
444
|
export function getModuleSourceFiles(moduleRoot, maxFiles = 20) {
|
|
426
|
-
const files = [];
|
|
427
445
|
const root = normalizeMapKey(moduleRoot);
|
|
428
446
|
const ignoreMatcher = getProjectIgnoreMatcher(root);
|
|
447
|
+
const cached = _moduleSourceFilesMemo.get(root);
|
|
448
|
+
if (cached &&
|
|
449
|
+
cached.maxFiles === maxFiles &&
|
|
450
|
+
cached.matcher === ignoreMatcher &&
|
|
451
|
+
isMemoEntryFresh(cached)) {
|
|
452
|
+
return [...cached.files];
|
|
453
|
+
}
|
|
454
|
+
const files = [];
|
|
455
|
+
const dirs = [];
|
|
456
|
+
let allDirsStamped = true;
|
|
429
457
|
const visit = (dir, depth) => {
|
|
430
458
|
if (files.length >= maxFiles || depth > 4)
|
|
431
459
|
return;
|
|
@@ -436,6 +464,18 @@ export function getModuleSourceFiles(moduleRoot, maxFiles = 20) {
|
|
|
436
464
|
catch {
|
|
437
465
|
return;
|
|
438
466
|
}
|
|
467
|
+
// Stamp AFTER the readdir: a change landing between readdir and stat
|
|
468
|
+
// records the newer mtime, so validation re-walks (fail-safe direction).
|
|
469
|
+
try {
|
|
470
|
+
dirs.push({
|
|
471
|
+
dir,
|
|
472
|
+
mtimeMs: fs.statSync(dir).mtimeMs,
|
|
473
|
+
stampedAt: Date.now(),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
allDirsStamped = false;
|
|
478
|
+
}
|
|
439
479
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
440
480
|
for (const entry of entries) {
|
|
441
481
|
if (files.length >= maxFiles)
|
|
@@ -458,5 +498,13 @@ export function getModuleSourceFiles(moduleRoot, maxFiles = 20) {
|
|
|
458
498
|
}
|
|
459
499
|
};
|
|
460
500
|
visit(root, 0);
|
|
501
|
+
if (allDirsStamped) {
|
|
502
|
+
_moduleSourceFilesMemo.set(root, {
|
|
503
|
+
files: [...files],
|
|
504
|
+
maxFiles,
|
|
505
|
+
dirs,
|
|
506
|
+
matcher: ignoreMatcher,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
461
509
|
return files;
|
|
462
510
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { isTestMode } from "./env-utils.js";
|
|
3
|
+
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
|
+
const REVIEW_GRAPH_LOG_DIR = getGlobalPiLensDir();
|
|
6
|
+
const REVIEW_GRAPH_LOG_FILE = path.join(REVIEW_GRAPH_LOG_DIR, "review-graph.log");
|
|
7
|
+
const writer = createNdjsonLogger({ filePath: REVIEW_GRAPH_LOG_FILE });
|
|
8
|
+
/**
|
|
9
|
+
* Build the one canonical graph metadata shape used by build and persist logs.
|
|
10
|
+
* Counts come from the graph instance; source-file count may use the exact
|
|
11
|
+
* signature-map count when the build has one, otherwise the graph file index.
|
|
12
|
+
*/
|
|
13
|
+
export function makeReviewGraphBuildMetadata(graph, options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
...(options.buildId === undefined ? {} : { buildId: options.buildId }),
|
|
16
|
+
...(graph.buildGeneration === undefined
|
|
17
|
+
? {}
|
|
18
|
+
: { graphGeneration: graph.buildGeneration }),
|
|
19
|
+
builtAt: graph.builtAt,
|
|
20
|
+
...(options.projectSeq === undefined
|
|
21
|
+
? {}
|
|
22
|
+
: { projectSeq: options.projectSeq }),
|
|
23
|
+
...(options.seqHint === undefined ? {} : { seqHint: options.seqHint }),
|
|
24
|
+
...(options.mode === undefined ? {} : { mode: options.mode }),
|
|
25
|
+
sourceFiles: options.sourceFileCount ?? graph.fileNodes.size,
|
|
26
|
+
...(options.sourceFileCountTruncated ||
|
|
27
|
+
graph.persistCoverage?.sourceFilesTruncated
|
|
28
|
+
? { sourceFilesTruncated: true }
|
|
29
|
+
: {}),
|
|
30
|
+
nodes: graph.nodes.size,
|
|
31
|
+
edges: graph.edges.length,
|
|
32
|
+
...(graph.persistCoverage
|
|
33
|
+
? { persistCoverage: graph.persistCoverage }
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function logReviewGraph(entry) {
|
|
38
|
+
if (isTestMode()) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
writer.log({ ts: new Date().toISOString(), ...entry, pid: process.pid });
|
|
42
|
+
}
|
|
43
|
+
export function getReviewGraphLogPath() {
|
|
44
|
+
return REVIEW_GRAPH_LOG_FILE;
|
|
45
|
+
}
|
|
46
|
+
/** Resolve once all enqueued review-graph writes are on disk (tests/shutdown). */
|
|
47
|
+
export function flushReviewGraphLog() {
|
|
48
|
+
return writer.flush();
|
|
49
|
+
}
|
|
50
|
+
/** Teardown-only: force queued entries to disk before the process exits. */
|
|
51
|
+
export function flushReviewGraphLogSync() {
|
|
52
|
+
writer.flushSync();
|
|
53
|
+
}
|
|
@@ -7,62 +7,36 @@
|
|
|
7
7
|
* Requires: pip install ruff
|
|
8
8
|
* Docs: https://docs.astral.sh/ruff/
|
|
9
9
|
*/
|
|
10
|
+
import { createSubsystemLogger } from "./extension-log.js";
|
|
10
11
|
import * as fs from "node:fs";
|
|
11
12
|
import * as path from "node:path";
|
|
13
|
+
import { createAvailabilityChecker, resolveAvailableOrInstall } from "./dispatch/runners/utils/runner-helpers.js";
|
|
12
14
|
import { isFileKind } from "./file-kinds.js";
|
|
13
15
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
16
|
+
import { ruffConfigArgs } from "./tool-policy.js";
|
|
14
17
|
// --- Client ---
|
|
18
|
+
const ruffAvailability = createAvailabilityChecker("ruff", ".exe");
|
|
15
19
|
export class RuffClient {
|
|
16
|
-
|
|
17
|
-
ensureInFlight = null;
|
|
20
|
+
ruffCommand = "ruff";
|
|
18
21
|
log;
|
|
19
22
|
constructor(verbose = false) {
|
|
20
23
|
this.log = verbose
|
|
21
|
-
? (
|
|
24
|
+
? createSubsystemLogger("ruff")
|
|
22
25
|
: () => { };
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Check if ruff CLI is available, auto-install if not.
|
|
26
29
|
*
|
|
27
|
-
* Re-entrancy safe:
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `DependencyChecker`.
|
|
30
|
+
* Re-entrancy safe: the shared availability seam deduplicates the complete
|
|
31
|
+
* probe/auto-install transaction per cwd and tool, so concurrent callers do
|
|
32
|
+
* not duplicate installation attempts.
|
|
31
33
|
*/
|
|
32
34
|
async ensureAvailable() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
return await this.ensureInFlight;
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
this.ensureInFlight = null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async doEnsureAvailable() {
|
|
46
|
-
// Check if available in PATH
|
|
47
|
-
const result = await safeSpawnAsync("ruff", ["--version"], {
|
|
48
|
-
timeout: 5000,
|
|
49
|
-
});
|
|
50
|
-
this.ruffAvailable = !result.error && result.status === 0;
|
|
51
|
-
if (this.ruffAvailable) {
|
|
52
|
-
this.log(`Ruff found: ${result.stdout.trim()}`);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
// Auto-install via pi-lens installer
|
|
56
|
-
this.log("Ruff not found, attempting auto-install...");
|
|
57
|
-
const { ensureTool } = await import("./installer/index.js");
|
|
58
|
-
const installedPath = await ensureTool("ruff");
|
|
59
|
-
if (installedPath) {
|
|
60
|
-
this.log(`Ruff auto-installed: ${installedPath}`);
|
|
61
|
-
this.ruffAvailable = true;
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
this.log("Ruff auto-install failed");
|
|
65
|
-
return false;
|
|
35
|
+
const resolved = await resolveAvailableOrInstall(ruffAvailability, "ruff", process.cwd());
|
|
36
|
+
if (!resolved)
|
|
37
|
+
return false;
|
|
38
|
+
this.ruffCommand = resolved;
|
|
39
|
+
return true;
|
|
66
40
|
}
|
|
67
41
|
/**
|
|
68
42
|
* Check if a file is a Python file
|
|
@@ -72,8 +46,10 @@ export class RuffClient {
|
|
|
72
46
|
}
|
|
73
47
|
/**
|
|
74
48
|
* Async auto-fix variant for pipeline use (non-blocking spawn).
|
|
49
|
+
* `cwd` is the dispatch language root (used for config discovery); when
|
|
50
|
+
* omitted it defaults to the file's directory.
|
|
75
51
|
*/
|
|
76
|
-
async fixFileAsync(filePath) {
|
|
52
|
+
async fixFileAsync(filePath, cwd) {
|
|
77
53
|
if (!(await this.ensureAvailable())) {
|
|
78
54
|
return {
|
|
79
55
|
success: false,
|
|
@@ -93,19 +69,26 @@ export class RuffClient {
|
|
|
93
69
|
}
|
|
94
70
|
try {
|
|
95
71
|
const before = await fs.promises.readFile(absolutePath, "utf-8");
|
|
96
|
-
|
|
72
|
+
// Shared config-args seam (#1247): the lint runner consumes the same
|
|
73
|
+
// builder, so `check --fix` can never drift to ruff's default rule
|
|
74
|
+
// set when the project lacks its own config and the package-owned
|
|
75
|
+
// core.toml fallback applies.
|
|
76
|
+
const configArgs = ruffConfigArgs(cwd ?? path.dirname(absolutePath));
|
|
77
|
+
const spawnOpts = { timeout: 10000, cwd: cwd ?? path.dirname(absolutePath) };
|
|
78
|
+
const pre = await safeSpawnAsync(this.ruffCommand, [
|
|
97
79
|
"check",
|
|
98
80
|
"--output-format",
|
|
99
81
|
"json",
|
|
100
82
|
"--target-version",
|
|
101
83
|
"py310",
|
|
84
|
+
...configArgs,
|
|
102
85
|
absolutePath,
|
|
103
|
-
],
|
|
86
|
+
], spawnOpts);
|
|
104
87
|
const beforeDiags = pre.stdout?.trim()
|
|
105
88
|
? this.parseOutput(pre.stdout, absolutePath)
|
|
106
89
|
: [];
|
|
107
90
|
const fixableCount = beforeDiags.filter((d) => d.fixable).length;
|
|
108
|
-
const fix = await safeSpawnAsync(
|
|
91
|
+
const fix = await safeSpawnAsync(this.ruffCommand, ["check", "--fix", ...configArgs, absolutePath], { timeout: 15000, cwd: cwd ?? path.dirname(absolutePath) });
|
|
109
92
|
if (fix.error) {
|
|
110
93
|
return {
|
|
111
94
|
success: false,
|