@herbertgao/pi-extensions 2026.9.14 → 2026.9.16
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/README.md +8 -3
- package/THIRD_PARTY_NOTICES.md +28 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
- package/node_modules/pi-jev-auto-mode/CHANGELOG.md +8 -0
- package/node_modules/pi-jev-auto-mode/README.md +7 -0
- package/node_modules/pi-jev-auto-mode/package.json +1 -1
- package/node_modules/pi-jev-auto-mode/src/extension.ts +21 -2
- package/node_modules/pi-jev-auto-mode/src/records.ts +68 -2
- package/node_modules/pi-jev-auto-mode/src/settings.ts +22 -0
- package/node_modules/pi-jev-auto-mode/src/ui.ts +10 -0
- package/node_modules/pi-lens/CHANGELOG.md +498 -0
- package/node_modules/pi-lens/README.md +45 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
- package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
- package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
- package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
- package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
- package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
- package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
- package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
- package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
- package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
- package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
- package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
- package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
- package/node_modules/pi-lens/dist/index.js +5695 -3733
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
- package/node_modules/pi-lens/dist/mcp/server.js +33 -21
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
- package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
- package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
- package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
- package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
- package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
- package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
- package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
- package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
- package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
- package/node_modules/pi-lens/docs/adr/README.md +26 -0
- package/node_modules/pi-lens/docs/agent-tools.md +18 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
- package/node_modules/pi-lens/docs/configuration.md +55 -1
- package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
- package/node_modules/pi-lens/docs/environment-variables.md +61 -4
- package/node_modules/pi-lens/docs/features.md +2 -2
- package/node_modules/pi-lens/docs/globalconfig.md +6 -1
- package/node_modules/pi-lens/docs/language-coverage.md +17 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
- package/node_modules/pi-lens/docs/settings.md +12 -3
- package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
- package/node_modules/pi-lens/package.json +5 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
- package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
- package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
- package/node_modules/pi-mcp-adapter/README.md +12 -5
- package/node_modules/pi-mcp-adapter/config.ts +31 -8
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +37 -20
- package/node_modules/pi-mcp-adapter/init.ts +14 -15
- package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +3 -2
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
- package/node_modules/pi-mcp-adapter/types.ts +2 -2
- package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
- package/node_modules/pi-mcp-adapter/utils.ts +11 -3
- package/node_modules/pi-multi-account/CHANGELOG.md +6 -0
- package/node_modules/pi-multi-account/README.md +15 -1
- package/node_modules/pi-multi-account/index.ts +295 -41
- package/node_modules/pi-multi-account/package.json +1 -1
- package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
- package/node_modules/pi-next-cue/LICENSE +21 -0
- package/node_modules/pi-next-cue/README.md +81 -0
- package/node_modules/pi-next-cue/index.ts +358 -0
- package/node_modules/pi-next-cue/package.json +28 -0
- package/package.json +10 -7
|
@@ -22,12 +22,17 @@
|
|
|
22
22
|
* exactly the registry's `kind: "file"` rows. Registry owns WHICH surfaces are
|
|
23
23
|
* deprecated; this module owns only their precedence.
|
|
24
24
|
*
|
|
25
|
-
* Pure data
|
|
25
|
+
* Pure data, one walk, and — as of the global-config-location PR (refs #2457,
|
|
26
|
+
* #2426) — the ONE existence probe and memoized resolution that decide which
|
|
27
|
+
* file supplies the global tier. The probe is a single `statSync` under the
|
|
28
|
+
* resolution memo; no other file reads, no state beyond that one memo.
|
|
26
29
|
*/
|
|
30
|
+
import * as fs from "node:fs";
|
|
27
31
|
import * as os from "node:os";
|
|
28
32
|
import * as path from "node:path";
|
|
29
33
|
import { DEPRECATED_CONFIG_SURFACES } from "./config-diagnostic-codes.js";
|
|
30
|
-
import {
|
|
34
|
+
import { errorClassName } from "./error-class.js";
|
|
35
|
+
import { isAtOrAboveHomeDir, isWindowsPath, normalizeFilePath, realpathOrResolve, walkUpDirs, } from "./path-utils.js";
|
|
31
36
|
/** The one canonical PROJECT config file. Never deprecated. */
|
|
32
37
|
export const CANONICAL_PROJECT_CONFIG_FILE = ".pi-lens.json";
|
|
33
38
|
/** Basename of the one canonical GLOBAL config file inside `~/.pi-lens/`. */
|
|
@@ -139,9 +144,167 @@ export function configSearchDirs(startDir, homeDir = os.homedir()) {
|
|
|
139
144
|
}
|
|
140
145
|
return dirs;
|
|
141
146
|
}
|
|
147
|
+
function defaultGlobalConfigExists(file) {
|
|
148
|
+
try {
|
|
149
|
+
return fs.statSync(file).isFile();
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
// ENOENT is the ordinary ABSENT answer the tier walk consumes. Any other
|
|
153
|
+
// stat failure (ENOTDIR when a file sits where a directory belongs,
|
|
154
|
+
// EACCES, ELOOP) propagates: the discriminated probe in
|
|
155
|
+
// `resolveGlobalConfigLocation` retains the errored tier instead of
|
|
156
|
+
// silently switching config sources (#3251 review H2, defect shape 48),
|
|
157
|
+
// and the load's read of the retained path is reported by the existing
|
|
158
|
+
// `PILENS_CFG_0001` seam.
|
|
159
|
+
if (error.code === "ENOENT")
|
|
160
|
+
return false;
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
142
164
|
/**
|
|
143
|
-
* The
|
|
144
|
-
*
|
|
165
|
+
* The agent-dir config path for one `PI_CODING_AGENT_DIR` value, shared by the
|
|
166
|
+
* resolution and the recognized-location set so the two can never spell the
|
|
167
|
+
* same location differently.
|
|
168
|
+
*
|
|
169
|
+
* Shape-aware joining (defect shape 2): a Windows-shaped agent dir is joined
|
|
170
|
+
* with `path.win32` regardless of host — the host-default `path.resolve` on
|
|
171
|
+
* POSIX treats a drive-letter path as RELATIVE and mangles it, which made a
|
|
172
|
+
* case-folded Windows spelling unmatchable (#3251 review M1).
|
|
173
|
+
*/
|
|
174
|
+
function agentConfigPathFor(agentDir) {
|
|
175
|
+
return isWindowsPath(agentDir)
|
|
176
|
+
? path.win32.join(path.win32.resolve(agentDir), "extensions", "pi-lens.json")
|
|
177
|
+
: path.join(path.resolve(agentDir), "extensions", "pi-lens.json");
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Resolve which file supplies the global config tier (see
|
|
181
|
+
* `GlobalConfigLocationSource` for the order).
|
|
182
|
+
*
|
|
183
|
+
* Lives here rather than in `lens-config.ts` (#2426 review round 3, S1):
|
|
184
|
+
* this module is the one that already states "WHERE pi-lens config lives",
|
|
185
|
+
* and BOTH discovery seams that must refuse the resolved global path
|
|
186
|
+
* (`project-lens-config.ts`, `workspace-topology.ts`) already import from
|
|
187
|
+
* here — importing the resolver from `lens-config.ts` would close a cycle
|
|
188
|
+
* (`lens-config.ts` itself imports from `project-lens-config.ts`).
|
|
189
|
+
* `lens-config.ts` re-exports the accessors so its own, pre-existing import
|
|
190
|
+
* sites are unaffected.
|
|
191
|
+
*/
|
|
192
|
+
export function resolveGlobalConfigLocation(options) {
|
|
193
|
+
// An options object means the caller OWNS the resolution context (a test
|
|
194
|
+
// or explicitly-scoped caller): the ambient host env must not leak into it,
|
|
195
|
+
// or a test that resolves-then-writes "under its own home" lands in the
|
|
196
|
+
// maintainer's real config (dogfooded live: ambient PI_CODING_AGENT_DIR
|
|
197
|
+
// redirected a suite's writeConfig into ~/.config/pi/agent/extensions/
|
|
198
|
+
// pi-lens.json). Owned contexts therefore ignore PI_CODING_AGENT_DIR
|
|
199
|
+
// entirely; only the production resolution (no options) walks the
|
|
200
|
+
// host-relative tier. PI_LENS_HOME does not participate in this resolution
|
|
201
|
+
// at all — the canonical default keeps its released homedir spelling, and
|
|
202
|
+
// the #2457 split-brain (which concerns the machine root, not the config
|
|
203
|
+
// file) stays a separate issue.
|
|
204
|
+
const owned = options !== undefined;
|
|
205
|
+
const homeDir = options?.homeDir ?? os.homedir();
|
|
206
|
+
const legacyDefault = path.join(homeDir, ".pi-lens", CANONICAL_GLOBAL_CONFIG_FILE);
|
|
207
|
+
// Byte-compatible with the released override: any non-empty value wins,
|
|
208
|
+
// untrimmed, exactly as before.
|
|
209
|
+
const override = process.env.PI_LENS_CONFIG_PATH;
|
|
210
|
+
if (override) {
|
|
211
|
+
return { path: path.resolve(override), source: "pi-lens-config-path" };
|
|
212
|
+
}
|
|
213
|
+
// The probe is DISCRIMINATED (#3251 review H2, defect shape 48): "error"
|
|
214
|
+
// (ENOTDIR when a file sits where a directory belongs, EACCES, ELOOP) is
|
|
215
|
+
// NOT absent — the errored tier is RETAINED, so the load's read of that
|
|
216
|
+
// same path fails and the existing `reportConfigReadFailure` seam
|
|
217
|
+
// (`PILENS_CFG_0001`) reports it. Absorbing the error as ABSENT would
|
|
218
|
+
// silently select a lower tier: different user settings applying with no
|
|
219
|
+
// diagnostic, and the read seam would only ever see the finally-read path,
|
|
220
|
+
// never the probe that failed.
|
|
221
|
+
let probeError;
|
|
222
|
+
const probe = (file) => {
|
|
223
|
+
try {
|
|
224
|
+
return (options?.exists ?? defaultGlobalConfigExists)(file)
|
|
225
|
+
? "present"
|
|
226
|
+
: "absent";
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// The caught error is carried to the retained tier's resolution so
|
|
230
|
+
// the loader's retention record can name the error CLASS (never
|
|
231
|
+
// the message, per the #2431/#2451 rule).
|
|
232
|
+
probeError = error;
|
|
233
|
+
return "error";
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const agentDir = owned ? undefined : process.env.PI_CODING_AGENT_DIR?.trim();
|
|
237
|
+
const agentPath = agentDir ? agentConfigPathFor(agentDir) : undefined;
|
|
238
|
+
const legacyProbe = probe(legacyDefault);
|
|
239
|
+
if (legacyProbe === "present") {
|
|
240
|
+
if (agentPath !== undefined && probe(agentPath) === "present") {
|
|
241
|
+
return {
|
|
242
|
+
path: legacyDefault,
|
|
243
|
+
source: "legacy-default-existing",
|
|
244
|
+
shadowedPath: agentPath,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return { path: legacyDefault, source: "legacy-default-existing" };
|
|
248
|
+
}
|
|
249
|
+
if (legacyProbe === "error") {
|
|
250
|
+
// Fail closed at the location the probe could not evaluate: the file
|
|
251
|
+
// might exist, and switching sources would silently swap the user's
|
|
252
|
+
// settings. The subsequent read reports the failure (`PILENS_CFG_0001`),
|
|
253
|
+
// and the loader emits the probe-failed retention record.
|
|
254
|
+
return {
|
|
255
|
+
path: legacyDefault,
|
|
256
|
+
source: "legacy-default-unprobed",
|
|
257
|
+
existsProbeFailed: {
|
|
258
|
+
path: legacyDefault,
|
|
259
|
+
errorClassName: errorClassName(probeError),
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (agentPath !== undefined) {
|
|
264
|
+
const agentProbe = probe(agentPath);
|
|
265
|
+
if (agentProbe === "present") {
|
|
266
|
+
return { path: agentPath, source: "pi-coding-agent-dir" };
|
|
267
|
+
}
|
|
268
|
+
if (agentProbe === "error") {
|
|
269
|
+
// Same rule one tier down: the agent-dir identity is retained, and
|
|
270
|
+
// the read of it reports the failure rather than the resolution
|
|
271
|
+
// silently falling to the canonical default.
|
|
272
|
+
return {
|
|
273
|
+
path: agentPath,
|
|
274
|
+
source: "pi-coding-agent-dir-unprobed",
|
|
275
|
+
existsProbeFailed: {
|
|
276
|
+
path: agentPath,
|
|
277
|
+
errorClassName: errorClassName(probeError),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { path: legacyDefault, source: "canonical-default" };
|
|
283
|
+
}
|
|
284
|
+
let memoizedGlobalConfigResolution;
|
|
285
|
+
function globalConfigEnvFingerprint() {
|
|
286
|
+
return JSON.stringify([
|
|
287
|
+
process.env.PI_LENS_CONFIG_PATH,
|
|
288
|
+
process.env.PI_CODING_AGENT_DIR,
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
291
|
+
export function getProductionGlobalConfigResolution() {
|
|
292
|
+
const fingerprint = globalConfigEnvFingerprint();
|
|
293
|
+
if (memoizedGlobalConfigResolution?.fingerprint !== fingerprint) {
|
|
294
|
+
memoizedGlobalConfigResolution = {
|
|
295
|
+
fingerprint,
|
|
296
|
+
resolution: resolveGlobalConfigLocation(),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return memoizedGlobalConfigResolution.resolution;
|
|
300
|
+
}
|
|
301
|
+
/** Test-only: drop the memoized production resolution so the next call re-probes. */
|
|
302
|
+
export function resetGlobalConfigLocationCache() {
|
|
303
|
+
memoizedGlobalConfigResolution = undefined;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* The absolute path of the canonical GLOBAL config (see
|
|
307
|
+
* `GlobalConfigLocationSource` for the full resolution order).
|
|
145
308
|
*
|
|
146
309
|
* Lives here rather than in `lens-config.ts` (#2426 review round 3, S1),
|
|
147
310
|
* which is where it originated: this module is the one that already states
|
|
@@ -152,9 +315,74 @@ export function configSearchDirs(startDir, homeDir = os.homedir()) {
|
|
|
152
315
|
* `project-lens-config.ts`. `lens-config.ts` re-exports this so its own,
|
|
153
316
|
* pre-existing import sites are unaffected.
|
|
154
317
|
*/
|
|
155
|
-
export function getPiLensGlobalConfigPath(homeDir
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
318
|
+
export function getPiLensGlobalConfigPath(homeDir) {
|
|
319
|
+
return homeDir === undefined
|
|
320
|
+
? getProductionGlobalConfigResolution().path
|
|
321
|
+
: resolveGlobalConfigLocation({ homeDir }).path;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Whether `candidate` matches ANY location the global config tier recognizes —
|
|
325
|
+
* the resolved file, the legacy default, and the agent-dir target.
|
|
326
|
+
*
|
|
327
|
+
* The one predicate every project-config discovery seam consults before it
|
|
328
|
+
* adopts a candidate (#783's nested layering, the upward walk, and the
|
|
329
|
+
* workspace marker index): a file the global tier recognizes must not be
|
|
330
|
+
* discovered a second time as a project config — the discovery loader would
|
|
331
|
+
* full-validate it, ignore every global-only key in it, and advise the user
|
|
332
|
+
* to set those keys in the very file it is ignoring. The check covers the
|
|
333
|
+
* POTENTIAL set, not just whichever file currently won the read: a second
|
|
334
|
+
* recognized file that exists but lost the tier walk (the grandfathered
|
|
335
|
+
* legacy file beside an adopted agent-dir file, say) is equally not a project
|
|
336
|
+
* config.
|
|
337
|
+
*
|
|
338
|
+
* Comparison is alias-anchored on BOTH sides (#3251 review M1 round 2):
|
|
339
|
+
* each path is canonicalized through `realpathOrResolve` (resolving existing
|
|
340
|
+
* symlink aliases; a nonexistent path falls back to its resolved spelling —
|
|
341
|
+
* the documented fallback) and then `normalizeFilePath` (separator-agnostic,
|
|
342
|
+
* case-insensitive on Windows, casing-adoption). A symlinked-home alias of a
|
|
343
|
+
* recognized location is therefore the same identity as its real spelling,
|
|
344
|
+
* and a case-folded or separator-differing Windows spelling cannot dodge the
|
|
345
|
+
* refusal either.
|
|
346
|
+
*/
|
|
347
|
+
export function isResolvedGlobalConfigPath(candidate) {
|
|
348
|
+
return recognizedGlobalConfigPaths().has(canonicalPathIdentity(candidate));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Canonical path identity for one candidate: realpath-anchored so an
|
|
352
|
+
* existing symlink alias resolves to the same identity as its real spelling
|
|
353
|
+
* (#3251 review M1 round 2), with `realpathOrResolve`'s documented
|
|
354
|
+
* nonexistent-path fallback (the resolved spelling, dot-folded and
|
|
355
|
+
* case-normalized by `normalizeFilePath` — which pays its own `realpath`
|
|
356
|
+
* only when the path exists). The discovery seams consult this predicate
|
|
357
|
+
* per config candidate, and the env-derived recognized set is canonicalized
|
|
358
|
+
* with the SAME helper once per env fingerprint, so both sides of the
|
|
359
|
+
* comparison share one identity function.
|
|
360
|
+
*/
|
|
361
|
+
export function canonicalPathIdentity(candidate) {
|
|
362
|
+
// Shape-aware (defect shape 2): a Windows-shaped path is resolved with
|
|
363
|
+
// `path.win32` — the host resolver on POSIX treats a drive-letter path as
|
|
364
|
+
// RELATIVE and mangles it before any comparison could run. On a real
|
|
365
|
+
// Windows host this arm is the native one, and `normalizeFilePath`'s
|
|
366
|
+
// Windows branch does the alias resolution itself (realpathSync.native
|
|
367
|
+
// plus canonical-casing adoption).
|
|
368
|
+
if (isWindowsPath(candidate)) {
|
|
369
|
+
return normalizeFilePath(path.win32.resolve(candidate));
|
|
370
|
+
}
|
|
371
|
+
return normalizeFilePath(realpathOrResolve(candidate));
|
|
372
|
+
}
|
|
373
|
+
let recognizedCache;
|
|
374
|
+
function recognizedGlobalConfigPaths() {
|
|
375
|
+
const fingerprint = globalConfigEnvFingerprint();
|
|
376
|
+
if (recognizedCache?.fingerprint !== fingerprint) {
|
|
377
|
+
const paths = new Set();
|
|
378
|
+
const override = process.env.PI_LENS_CONFIG_PATH;
|
|
379
|
+
if (override)
|
|
380
|
+
paths.add(canonicalPathIdentity(path.resolve(override)));
|
|
381
|
+
paths.add(canonicalPathIdentity(path.join(os.homedir(), ".pi-lens", CANONICAL_GLOBAL_CONFIG_FILE)));
|
|
382
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
383
|
+
if (agentDir)
|
|
384
|
+
paths.add(canonicalPathIdentity(agentConfigPathFor(agentDir)));
|
|
385
|
+
recognizedCache = { fingerprint, paths };
|
|
386
|
+
}
|
|
387
|
+
return recognizedCache.paths;
|
|
160
388
|
}
|
|
@@ -60,7 +60,7 @@ import * as fs from "node:fs";
|
|
|
60
60
|
import * as os from "node:os";
|
|
61
61
|
import * as path from "node:path";
|
|
62
62
|
import { DEPRECATED_CONFIG_SURFACES, } from "./config-diagnostic-codes.js";
|
|
63
|
-
import { CANONICAL_GLOBAL_CONFIG_FILE, CANONICAL_PROJECT_CONFIG_FILE, GLOBAL_CONFIG_LOCATIONS, LEGACY_ROOT_LSP_KEYS, LSP_NAMESPACE_KEY, PROJECT_CONFIG_LOCATIONS, configSearchDirs, } from "./config-locations.js";
|
|
63
|
+
import { CANONICAL_GLOBAL_CONFIG_FILE, CANONICAL_PROJECT_CONFIG_FILE, GLOBAL_CONFIG_LOCATIONS, isResolvedGlobalConfigPath, LEGACY_ROOT_LSP_KEYS, LSP_NAMESPACE_KEY, PROJECT_CONFIG_LOCATIONS, configSearchDirs, } from "./config-locations.js";
|
|
64
64
|
// The core's halves are imported directly rather than through
|
|
65
65
|
// `config-core/index.js` (#2426). The barrel re-exports the whole vocabulary
|
|
66
66
|
// including `process-spec.js`, which reaches `project-trust.js` and the
|
|
@@ -126,6 +126,12 @@ function collectDocuments(dir, locations, tier, onReadError) {
|
|
|
126
126
|
const documents = [];
|
|
127
127
|
for (const location of locations) {
|
|
128
128
|
const file = path.join(dir, location.relativePath);
|
|
129
|
+
// The global tier reads its file by absolute path; a candidate that IS
|
|
130
|
+
// that file is refused rather than adopted as a project/nested-project
|
|
131
|
+
// document, which would double-read and double-validate it
|
|
132
|
+
// (global-config-location PR, refs #2457).
|
|
133
|
+
if (tier !== "global" && isResolvedGlobalConfigPath(file))
|
|
134
|
+
continue;
|
|
129
135
|
const outcome = readConfigDocument(file);
|
|
130
136
|
if (outcome.status === "missing")
|
|
131
137
|
continue;
|
|
@@ -37,10 +37,21 @@ let ledgerGeneration = 0;
|
|
|
37
37
|
export function getDegradationLedgerGeneration() {
|
|
38
38
|
return ledgerGeneration;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Test-only population probe for bounded-container regressions. Keep this
|
|
42
|
+
* read-only and deliberately expose sizes, not the retained identities.
|
|
43
|
+
*/
|
|
44
|
+
export function _getDegradationLedgerStateForTests() {
|
|
45
|
+
return {
|
|
46
|
+
onceKeys: onceKeys.size,
|
|
47
|
+
tallies: tallies.size,
|
|
48
|
+
retainedEntries: [...groups.values()].reduce((total, group) => total + group.entries.length, 0),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
40
51
|
export function recordDegradation(record) {
|
|
41
52
|
try {
|
|
42
53
|
const kind = boundedKind(record.kind);
|
|
43
|
-
const subject =
|
|
54
|
+
const subject = subjectForLedger(record.subject);
|
|
44
55
|
const reason = truncateForLedger(record.reason);
|
|
45
56
|
let group = groups.get(kind);
|
|
46
57
|
if (!group) {
|
|
@@ -67,7 +78,7 @@ export function recordDegradation(record) {
|
|
|
67
78
|
export function recordDegradationOnce(record) {
|
|
68
79
|
try {
|
|
69
80
|
const kind = boundedKind(record.kind);
|
|
70
|
-
const subject =
|
|
81
|
+
const subject = subjectForLedger(record.subject);
|
|
71
82
|
const key = `${kind}\0${subject}`;
|
|
72
83
|
if (onceKeys.has(key))
|
|
73
84
|
return;
|
|
@@ -96,7 +107,7 @@ export function recordDegradationOnce(record) {
|
|
|
96
107
|
export function incrementDegradationCount(record) {
|
|
97
108
|
try {
|
|
98
109
|
const kind = boundedKind(record.kind);
|
|
99
|
-
const subject =
|
|
110
|
+
const subject = subjectForLedger(record.subject);
|
|
100
111
|
const reason = truncateForLedger(record.reason);
|
|
101
112
|
const key = `${kind}\0${subject}`;
|
|
102
113
|
const count = (tallies.get(key) ?? 0) + 1;
|
|
@@ -172,6 +183,25 @@ function boundLedgerMetadata(metadata) {
|
|
|
172
183
|
function isPowerOfTwo(value) {
|
|
173
184
|
return value > 0 && (value & (value - 1)) === 0;
|
|
174
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* A subject is an IDENTITY: it is the row's discriminator, the `filePath` of
|
|
188
|
+
* its durable latency row, and the text `renderDegradationLines` prints after
|
|
189
|
+
* the count. A blank one names nothing — `⚠ kind: 1 — : reason` — so blank and
|
|
190
|
+
* missing are the same case and both read `unknown` (#3389 verify round 2,
|
|
191
|
+
* F3395-02: a socket error carrying `code: ""` wrote an empty subject).
|
|
192
|
+
*
|
|
193
|
+
* This is deliberately NOT `normalizeForLedger`'s job, though every ledger
|
|
194
|
+
* field passes through it: that normalizer keeps falsy primitives on purpose
|
|
195
|
+
* (`tests/clients/ledger-bounds.test.ts` "keeps primitives, including falsy
|
|
196
|
+
* ones"), because a metadata VALUE of `""` or `0` is data, not absence. Only
|
|
197
|
+
* the identity fields fold blank into missing, and `0`/`false` still name
|
|
198
|
+
* something here too — the check reads the NORMALIZED text, never the caller's
|
|
199
|
+
* truthiness.
|
|
200
|
+
*/
|
|
201
|
+
function subjectForLedger(value) {
|
|
202
|
+
const subject = truncateForLedger(value);
|
|
203
|
+
return subject.trim() === "" ? "unknown" : subject;
|
|
204
|
+
}
|
|
175
205
|
function boundedKind(value) {
|
|
176
206
|
const kind = truncateForLedger(value);
|
|
177
207
|
if (groups.has(kind) || kind === OVERFLOW_KIND)
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Requires: npm install -D madge
|
|
9
9
|
* Docs: https://github.com/pahen/madge
|
|
10
10
|
*/
|
|
11
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
11
12
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
12
13
|
import * as fs from "node:fs";
|
|
13
14
|
import * as path from "node:path";
|
|
@@ -36,34 +37,79 @@ export function buildMadgeArgs(target, projectRoot) {
|
|
|
36
37
|
if (fs.existsSync(tsConfig)) {
|
|
37
38
|
args.push("--ts-config", tsConfig);
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// skipped
|
|
42
|
-
//
|
|
43
|
-
|
|
40
|
+
// No `--warning`: madge gates it on `!program.json` (bin/cli.js:195) and this
|
|
41
|
+
// argv always passes `--json`, so the flag is inert; ungated it prints the
|
|
42
|
+
// skipped-file list to STDOUT (lib/output.js), which would corrupt the JSON
|
|
43
|
+
// this reader parses. The skip list is therefore unreachable through any
|
|
44
|
+
// argv this builder emits — `parseMadgeSkips` could only ever return zeros
|
|
45
|
+
// (#3436). The lost visibility is disclosed once per session/root by
|
|
46
|
+
// `recordDegradationOnce` from `parseMadgeCycles`, the one reader both
|
|
47
|
+
// madge lanes pass through, instead of parsed from bytes madge never
|
|
48
|
+
// writes.
|
|
49
|
+
args.push("--json", target);
|
|
44
50
|
return args;
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
|
-
*
|
|
48
|
-
* package specifiers (bare names / subpaths like `web-tree-sitter`,
|
|
49
|
-
* `vitest/config`) are expected — madge doesn't traverse node_modules. We flag
|
|
50
|
-
* only **local** skips (relative/absolute paths), which are the ones that could
|
|
51
|
-
* silently drop an internal edge and hide a cycle.
|
|
53
|
+
* The ONE reader of madge's `--circular --json` contract (#3428).
|
|
52
54
|
*
|
|
53
|
-
*
|
|
55
|
+
* madge prints an array of cycles, each cycle an array of member paths —
|
|
56
|
+
* `[["src/a.ts", "src/b.ts"]]` — not a dependency graph. Upstream 8.0.0:
|
|
57
|
+
* `bin/cli.js:258-268` hands `Madge.circular()` (an Array, `lib/api.js:103`) to
|
|
58
|
+
* `output.circular`, which for `--json` is `printJSON(circular)`
|
|
59
|
+
* (`lib/output.js:71-73`). The graph OBJECT is what `--json` prints WITHOUT
|
|
60
|
+
* `--circular`, and `buildMadgeArgs` always passes `--circular`.
|
|
61
|
+
*
|
|
62
|
+
* This function exists because there were two readers and they disagreed: the
|
|
63
|
+
* whole-project scan read the array with `Object.entries`, so every cycle it
|
|
64
|
+
* reported was anchored at the array INDEX (`<root>/0`, a path that does not
|
|
65
|
+
* exist) and every member was resolved against the extension host's cwd. Both
|
|
66
|
+
* callers now share this one reader.
|
|
67
|
+
*
|
|
68
|
+
* `baseDir` is the directory madge's relative members resolve against, which is
|
|
69
|
+
* the TARGET it was pointed at (measured, madge 8.0.0: `--json <root>` prints
|
|
70
|
+
* `src/a.ts`; `--json <root>/src/a.ts` prints `a.ts`), so each caller passes
|
|
71
|
+
* the base its own target implies.
|
|
72
|
+
*
|
|
73
|
+
* Malformed JSON throws, which both callers already treat as "did not parse". A
|
|
74
|
+
* parse that is not an array does NOT throw: it yields zero cycles, so an
|
|
75
|
+
* unexpected shape reports no findings instead of turning a completed scan into
|
|
76
|
+
* a failed one (#2154 — a failed madge scan is a COLD lane in `mode=full`).
|
|
77
|
+
*
|
|
78
|
+
* This reader is also the ONE place both madge lanes (the turn-end
|
|
79
|
+
* `runMadgeSpawn` and the session-start `runScanProject`) turn bytes into
|
|
80
|
+
* findings, so the argv's blind spot is recorded here rather than once per
|
|
81
|
+
* caller (#3436). `projectRoot` is the ledger subject, not the resolve
|
|
82
|
+
* `baseDir`: a single-file target resolves members against its own directory
|
|
83
|
+
* but the gap is a property of the project's scan, disclosed once per
|
|
84
|
+
* session/root.
|
|
54
85
|
*/
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
86
|
+
function parseMadgeCycles(stdout, baseDir, projectRoot) {
|
|
87
|
+
const parsed = JSON.parse(stdout || "[]");
|
|
88
|
+
const cycles = Array.isArray(parsed) ? parsed : [];
|
|
89
|
+
const circular = [];
|
|
90
|
+
const circularFiles = new Set();
|
|
91
|
+
for (const cycle of cycles) {
|
|
92
|
+
const resolvedPaths = cycle.map((f) => path.resolve(baseDir, f));
|
|
93
|
+
for (const f of resolvedPaths) {
|
|
94
|
+
circularFiles.add(f);
|
|
95
|
+
}
|
|
96
|
+
circular.push({
|
|
97
|
+
file: resolvedPaths[0],
|
|
98
|
+
path: resolvedPaths,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// madge's `--warning` is inert under `--json` (bin/cli.js:195 gates it on
|
|
102
|
+
// `!program.json`) and, ungated, prints the skip list to stdout where it
|
|
103
|
+
// would corrupt the JSON this reader parses, so no argv `buildMadgeArgs`
|
|
104
|
+
// emits can report which LOCAL files madge failed to resolve. A skipped
|
|
105
|
+
// local file can hide a real cycle, so the gap is disclosed once per
|
|
106
|
+
// session/root instead of being read as a clean graph (#3436).
|
|
107
|
+
recordDegradationOnce({
|
|
108
|
+
kind: "madge-skip-visibility-unavailable",
|
|
109
|
+
subject: projectRoot,
|
|
110
|
+
reason: "madge --warning is inert under --json; skipped-file visibility is unavailable for this scan",
|
|
111
|
+
});
|
|
112
|
+
return { circular, circularFiles };
|
|
67
113
|
}
|
|
68
114
|
/**
|
|
69
115
|
* Defensive cap on concurrent madge spawns for a single `checkFilesBatch`
|
|
@@ -520,7 +566,6 @@ export class DependencyChecker {
|
|
|
520
566
|
circular: spawnResult.circular.filter((d) => d.file === normalized || d.path.includes(normalized)),
|
|
521
567
|
checked: true,
|
|
522
568
|
cacheHit: false,
|
|
523
|
-
localSkips: spawnResult.localSkips,
|
|
524
569
|
};
|
|
525
570
|
}
|
|
526
571
|
/**
|
|
@@ -542,31 +587,20 @@ export class DependencyChecker {
|
|
|
542
587
|
this.log(`Check error: ${result.error.message}`);
|
|
543
588
|
return { ok: false };
|
|
544
589
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
}
|
|
556
|
-
circular.push({
|
|
557
|
-
file: resolvedPaths[0],
|
|
558
|
-
path: resolvedPaths,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
const skips = parseMadgeSkips(result.stderr || "");
|
|
562
|
-
if (skips.local.length > 0) {
|
|
563
|
-
this.log(`madge skipped ${skips.local.length} local file(s) (possible silent cycle-miss): ${skips.local.slice(0, 5).join(", ")}`);
|
|
564
|
-
}
|
|
590
|
+
// madge resolves its printed members against the TARGET, which this
|
|
591
|
+
// lane points at a single FILE, so they come back relative to that
|
|
592
|
+
// file's directory — `a.ts`/`b.ts` for a `src/a.ts` target, and
|
|
593
|
+
// `../lib/b.ts` for a cycle that leaves it (measured with madge 8.0.0,
|
|
594
|
+
// see parseMadgeCycles). Resolving them against `projectRoot` instead
|
|
595
|
+
// anchors `<root>/a.ts`, a path that does not exist, and keys the
|
|
596
|
+
// shared `circularFiles` set with it (#3435). `path.dirname` of the
|
|
597
|
+
// target is the file-independent form of "the directory madge printed
|
|
598
|
+
// relative to"; a top-level target degenerates to `projectRoot`.
|
|
599
|
+
const { circular, circularFiles } = parseMadgeCycles(result.stdout || "", path.dirname(normalized), projectRoot);
|
|
565
600
|
return {
|
|
566
601
|
ok: true,
|
|
567
602
|
circular,
|
|
568
603
|
circularFiles,
|
|
569
|
-
localSkips: skips.local.length,
|
|
570
604
|
};
|
|
571
605
|
}
|
|
572
606
|
catch (err) {
|
|
@@ -729,7 +763,6 @@ export class DependencyChecker {
|
|
|
729
763
|
circular: spawnResult.circular.filter((d) => d.file === entry.normalized || d.path.includes(entry.normalized)),
|
|
730
764
|
checked: true,
|
|
731
765
|
cacheHit: false,
|
|
732
|
-
localSkips: spawnResult.localSkips,
|
|
733
766
|
});
|
|
734
767
|
}
|
|
735
768
|
// A batch that learned nothing publishes nothing — advancing the
|
|
@@ -800,25 +833,15 @@ export class DependencyChecker {
|
|
|
800
833
|
this.log(`Scan error: ${result.error.message}`);
|
|
801
834
|
return { circular: [], count: 0 };
|
|
802
835
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const circularFiles =
|
|
807
|
-
for (const [file, deps] of Object.entries(data)) {
|
|
808
|
-
if (Array.isArray(deps) && deps.length > 0) {
|
|
809
|
-
const resolvedFile = path.resolve(file);
|
|
810
|
-
circularFiles.add(resolvedFile);
|
|
811
|
-
circular.push({
|
|
812
|
-
file: resolvedFile,
|
|
813
|
-
path: [resolvedFile, ...deps.map((d) => path.resolve(d))],
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
}
|
|
836
|
+
// The scan's target IS `projectRoot`, so that is the base madge's
|
|
837
|
+
// relative members resolve against (#3428: this site used to read the
|
|
838
|
+
// cycle array as a graph and anchor every finding at `<hostCwd>/0`).
|
|
839
|
+
const { circular, circularFiles } = parseMadgeCycles(result.stdout || "", projectRoot, projectRoot);
|
|
817
840
|
this.publishState(gen, circular, circularFiles);
|
|
818
|
-
// #2154: the one madge site that parsed
|
|
819
|
-
// other return here (missing root, no top-level source file,
|
|
820
|
-
// unavailable, spawn error, parse throw) is the SAME empty shape
|
|
821
|
-
// must not be read as "no cycles in this project".
|
|
841
|
+
// #2154: the one madge site that parsed madge's output for this root.
|
|
842
|
+
// Every other return here (missing root, no top-level source file,
|
|
843
|
+
// madge unavailable, spawn error, parse throw) is the SAME empty shape
|
|
844
|
+
// and must not be read as "no cycles in this project".
|
|
822
845
|
return { circular, count: circular.length, analyzed: true };
|
|
823
846
|
}
|
|
824
847
|
catch (err) {
|
|
@@ -118,10 +118,42 @@ export const normalizeMessage = sharedNormalizeMessage;
|
|
|
118
118
|
// file, and the module doc/comments above referencing `relativeFile`, keep
|
|
119
119
|
// working unchanged.
|
|
120
120
|
const relativeFile = sharedRelativeFile;
|
|
121
|
+
/**
|
|
122
|
+
* The SPAN component of a strict anchor: the `lineContentHash` of the
|
|
123
|
+
* diagnostic's OWN line in `content`. `undefined` when there is no line to
|
|
124
|
+
* hash — no content, no line, or a line outside the content's range.
|
|
125
|
+
*
|
|
126
|
+
* Exported for #3183. The widget store's retention lifetime asks the same
|
|
127
|
+
* question a strict anchor does — "is the marked line still in this file?" —
|
|
128
|
+
* so it reuses this derivation rather than re-spelling it; a second spelling is
|
|
129
|
+
* how the store's answer drifts from the filter's (the retention identity's own
|
|
130
|
+
* drift from `computeWeakAnchor`'s parts cost #3158 a round). Note what is NOT
|
|
131
|
+
* here: the cwd/path component `stableFindingId` adds. Both sides of a
|
|
132
|
+
* retention comparison are the same file, so the span alone discriminates
|
|
133
|
+
* exactly as well while needing no `cwd` — which is why the store can ask it at
|
|
134
|
+
* all (see `retentionIdentity` in `clients/widget-state.ts`).
|
|
135
|
+
*/
|
|
136
|
+
export function strictAnchorSpan(content, line) {
|
|
137
|
+
if (content === undefined || line === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const lines = content.split(/\r?\n/);
|
|
140
|
+
if (line < 1 || line > lines.length)
|
|
141
|
+
return undefined;
|
|
142
|
+
return lineContentHash(lines[line - 1] ?? "");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Every span {@link strictAnchorSpan} could produce for `content` — one hash
|
|
146
|
+
* per line (#3183). Membership answers "does the line a mark was made against
|
|
147
|
+
* still exist in this file", wherever it has since moved to.
|
|
148
|
+
*/
|
|
149
|
+
export function strictAnchorSpansIn(content) {
|
|
150
|
+
const spans = new Set();
|
|
151
|
+
for (const line of content.split(/\r?\n/))
|
|
152
|
+
spans.add(lineContentHash(line));
|
|
153
|
+
return spans;
|
|
154
|
+
}
|
|
121
155
|
/** Site-specific anchor — see module doc. Used only for false-positive. */
|
|
122
156
|
export function computeStrictAnchor(args) {
|
|
123
|
-
const lines = args.content?.split(/\r?\n/);
|
|
124
|
-
const lineText = args.line !== undefined && lines ? (lines[args.line - 1] ?? "") : "";
|
|
125
157
|
return stableFindingId("dd:", {
|
|
126
158
|
cwd: args.cwd,
|
|
127
159
|
filePath: args.filePath,
|
|
@@ -129,7 +161,11 @@ export function computeStrictAnchor(args) {
|
|
|
129
161
|
args.tool ?? "",
|
|
130
162
|
args.rule ?? "",
|
|
131
163
|
normalizeMessage(args.message),
|
|
132
|
-
|
|
164
|
+
// An absent/out-of-range line hashes the empty string, exactly as the
|
|
165
|
+
// pre-#3183 `lines[args.line - 1] ?? ""` fallback did: stable, but less
|
|
166
|
+
// resistant to a same-file/rule/message collision (see
|
|
167
|
+
// `DispositionAnchorArgs.content`).
|
|
168
|
+
strictAnchorSpan(args.content, args.line) ?? lineContentHash(""),
|
|
133
169
|
],
|
|
134
170
|
});
|
|
135
171
|
}
|