@herbertgao/pi-extensions 2026.9.15 → 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 +5 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- 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-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-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 +8 -5
|
@@ -19,6 +19,8 @@ import * as net from "node:net";
|
|
|
19
19
|
import * as os from "node:os";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { writeFileAtomic } from "../atomic-write.js";
|
|
22
|
+
import { incrementDegradationCount } from "../degradation-ledger.js";
|
|
23
|
+
import { DEFAULT_MAX_OUTPUT_BYTES } from "../spawn-output-cap.js";
|
|
22
24
|
export const WARM_DIAGNOSTICS_SCHEMA_VERSION = 1;
|
|
23
25
|
export const WARM_CODE_ACTION_LOOKUP_LIMIT = 6;
|
|
24
26
|
/**
|
|
@@ -27,9 +29,9 @@ export const WARM_CODE_ACTION_LOOKUP_LIMIT = 6;
|
|
|
27
29
|
* resolved root (lowercased for case-insensitive filesystems), so when they're
|
|
28
30
|
* the same project they meet. Mismatch → the client just falls back to cold.
|
|
29
31
|
*/
|
|
30
|
-
export function ipcPathForCwd(cwd) {
|
|
31
|
-
const hash = workspaceHash(cwd);
|
|
32
|
-
if (
|
|
32
|
+
export function ipcPathForCwd(cwd, platform = process.platform) {
|
|
33
|
+
const hash = workspaceHash(cwd, platform);
|
|
34
|
+
if (platform === "win32") {
|
|
33
35
|
return `\\\\.\\pipe\\pi-lens-mcp-${hash}`;
|
|
34
36
|
}
|
|
35
37
|
return path.join(os.tmpdir(), `pi-lens-mcp-${hash}.sock`);
|
|
@@ -38,21 +40,86 @@ export function ipcPathForCwd(cwd) {
|
|
|
38
40
|
* The single stable per-workspace id both endpoint derivations key on. Every
|
|
39
41
|
* per-workspace side-channel name (socket/pipe, turn-end status file) must come
|
|
40
42
|
* from HERE, so the hook process and the server process cannot drift apart.
|
|
43
|
+
*
|
|
44
|
+
* #1193 P3 decision: this input is NOT a map key and neither path-utils seam
|
|
45
|
+
* belongs here. Writers and readers of this id, and what each one holds:
|
|
46
|
+
*
|
|
47
|
+
* | process | site | input |
|
|
48
|
+
* |--------------------|-----------------------------------------------|-------|
|
|
49
|
+
* | MCP server (warm) | `mcp/server.ts` `IPC_PATH` — listen | its launch cwd |
|
|
50
|
+
* | pi session (warm) | `clients/warm-attach.ts` — listen (pid-scoped) | runtime cwd |
|
|
51
|
+
* | PostToolUse hook | `requestWarmAnalyze` — connect | hook cwd |
|
|
52
|
+
* | Stop hook | `requestWarmTurnEnd` — connect | hook cwd |
|
|
53
|
+
* | Stop hook / health | `turnEndStatusPathForCwd` — write + read a file in tmpdir | cwd |
|
|
54
|
+
*
|
|
55
|
+
* Every row is a SEPARATE process that must reproduce the same 16 hex
|
|
56
|
+
* characters with no shared state, and the rows do not run at the same time —
|
|
57
|
+
* the server derives its id at launch, a hook derives its own minutes or hours
|
|
58
|
+
* later. `normalizeMapKey`/`normalizeFilePath` would make the id depend on
|
|
59
|
+
* filesystem state (`realpathSync.native`, on-disk casing) read at two
|
|
60
|
+
* different moments: a case-only rename, a remounted symlink, or a hook running
|
|
61
|
+
* in a different mount namespace than the server would silently stop the two
|
|
62
|
+
* from meeting. That is the exact staleness PR #2193 was rejected for on the
|
|
63
|
+
* LSP seam. `normalizeEphemeralMapKey` is excluded by its own contract — it is
|
|
64
|
+
* scoped to process-local, same-run keys and says so.
|
|
65
|
+
*
|
|
66
|
+
* So the derivation stays pure string math — and #3255 narrowed its case fold
|
|
67
|
+
* to the platforms whose FILESYSTEM folds case, because "pure" and
|
|
68
|
+
* "unconditional" are not the same thing:
|
|
69
|
+
*
|
|
70
|
+
* - case-INSENSITIVE default (`win32`, `darwin` APFS/HFS+): `Alpha` and `alpha`
|
|
71
|
+
* are ONE directory, so the only failure mode is a MISS. The server row and
|
|
72
|
+
* the hook rows can hold different spellings of it — the server's cwd can be
|
|
73
|
+
* a hand-written `--cwd=` / `PI_LENS_MCP_CWD` while the hook's comes from the
|
|
74
|
+
* payload (see `tests/mcp/turn-end-route.smoke.test.ts`'s win32 case) — so the
|
|
75
|
+
* fold is what makes them meet, and it stays. `darwin` belongs here for a
|
|
76
|
+
* reason in this tree, not by analogy: the incumbent selection that decides
|
|
77
|
+
* two sessions share a root already compares through `normalizeFilePath`
|
|
78
|
+
* (`clients/instance-registry.ts:837`), whose `realpathSync.native` returns
|
|
79
|
+
* on-disk casing on Darwin. A pair that SELECTS each other case-insensitively
|
|
80
|
+
* must be able to MEET.
|
|
81
|
+
* - case-SENSITIVE (Linux, *BSD): `Alpha` and `alpha` are TWO directories, and
|
|
82
|
+
* two spellings of one workspace cannot occur (a mis-cased `--cwd=` names a
|
|
83
|
+
* directory that does not exist). Folding there bought nothing and collided
|
|
84
|
+
* two workspaces onto one socket and one status file — #3255.
|
|
85
|
+
*
|
|
86
|
+
* Known residuals, both fail-safe and both un-closable without a filesystem
|
|
87
|
+
* read this derivation is forbidden to do: a case-insensitive MOUNT on a
|
|
88
|
+
* case-sensitive platform (`nocase` vfat/ntfs3/cifs, ext4 `chattr +F`) now
|
|
89
|
+
* misses instead of meeting — the same blind spot `normalizeFilePath` has
|
|
90
|
+
* there, measured and filed as #3154 — and a case-SENSITIVE APFS volume keeps
|
|
91
|
+
* today's collision.
|
|
92
|
+
*
|
|
93
|
+
* `platform` is an argument, not a `process.platform` read, so every arm is
|
|
94
|
+
* testable from one lane (the seam `normalizePathEntry` uses in
|
|
95
|
+
* `clients/lsp/launch.ts`).
|
|
41
96
|
*/
|
|
42
|
-
function workspaceHash(cwd) {
|
|
43
|
-
const
|
|
97
|
+
function workspaceHash(cwd, platform = process.platform) {
|
|
98
|
+
const resolved = path.resolve(cwd);
|
|
99
|
+
const root = platform === "win32" || platform === "darwin"
|
|
100
|
+
? resolved.toLowerCase()
|
|
101
|
+
: resolved;
|
|
44
102
|
// sha256 (not for security — just a stable short id for the IPC socket/pipe
|
|
45
103
|
// name keyed by cwd; sha256 over sha1 keeps SonarCloud's weak-hash check quiet)
|
|
46
104
|
return crypto.createHash("sha256").update(root).digest("hex").slice(0, 16);
|
|
47
105
|
}
|
|
48
106
|
/** PID-scoped endpoint used by pi sessions. The legacy MCP analyze endpoint
|
|
49
107
|
* remains workspace-scoped for compatibility with the PostToolUse hook. */
|
|
50
|
-
export function diagnosticsIpcPathForCwd(cwd, pid) {
|
|
51
|
-
const base = ipcPathForCwd(cwd);
|
|
52
|
-
if (
|
|
108
|
+
export function diagnosticsIpcPathForCwd(cwd, pid, platform = process.platform) {
|
|
109
|
+
const base = ipcPathForCwd(cwd, platform);
|
|
110
|
+
if (platform === "win32")
|
|
53
111
|
return `${base}-diagnostics-${pid}`;
|
|
54
112
|
return base.replace(/\.sock$/, `-diagnostics-${pid}.sock`);
|
|
55
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* A connect that never landed, as opposed to a socket that opened and then
|
|
116
|
+
* failed. `ENOENT` is the POSIX "no socket file there"; `ECONNREFUSED` is a
|
|
117
|
+
* socket file (or named pipe) with no live listener behind it.
|
|
118
|
+
*/
|
|
119
|
+
function isNoListenerError(error) {
|
|
120
|
+
const code = error?.code;
|
|
121
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
122
|
+
}
|
|
56
123
|
export function contentHash(content) {
|
|
57
124
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
58
125
|
}
|
|
@@ -80,19 +147,14 @@ function requestOverWarmIpc(endpoint, timeoutMs, buildRequest, validate) {
|
|
|
80
147
|
const deadlineAt = Date.now() + timeoutMs;
|
|
81
148
|
const socket = net.createConnection(endpoint);
|
|
82
149
|
socket.setEncoding("utf8");
|
|
83
|
-
let buffer = "";
|
|
84
150
|
const timer = setTimeout(() => finish({ available: false, reason: "timeout" }), timeoutMs);
|
|
85
151
|
timer.unref();
|
|
86
152
|
socket.on("connect", () => {
|
|
87
153
|
socket.write(`${JSON.stringify(buildRequest(deadlineAt))}\n`);
|
|
88
154
|
});
|
|
89
|
-
socket.on("data", (
|
|
90
|
-
buffer += chunk;
|
|
91
|
-
const newline = buffer.indexOf("\n");
|
|
92
|
-
if (newline === -1)
|
|
93
|
-
return;
|
|
155
|
+
socket.on("data", createWarmIpcLineReader((line) => {
|
|
94
156
|
try {
|
|
95
|
-
const message = JSON.parse(
|
|
157
|
+
const message = JSON.parse(line);
|
|
96
158
|
const result = message.result;
|
|
97
159
|
if (message.error || !result) {
|
|
98
160
|
finish({ available: false, reason: "ipc-error" });
|
|
@@ -109,8 +171,14 @@ function requestOverWarmIpc(endpoint, timeoutMs, buildRequest, validate) {
|
|
|
109
171
|
catch {
|
|
110
172
|
finish({ available: false, reason: "schema-mismatch" });
|
|
111
173
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
174
|
+
}, {
|
|
175
|
+
label: "warm-diagnostics-reply",
|
|
176
|
+
onOverflow: () => finish({ available: false, reason: "ipc-error" }),
|
|
177
|
+
}));
|
|
178
|
+
socket.on("error", (error) => finish({
|
|
179
|
+
available: false,
|
|
180
|
+
reason: isNoListenerError(error) ? "no-listener" : "ipc-error",
|
|
181
|
+
}));
|
|
114
182
|
socket.on("close", () => finish({ available: false, reason: "ipc-error" }));
|
|
115
183
|
});
|
|
116
184
|
}
|
|
@@ -201,12 +269,12 @@ export async function requestWarmTurnEnd(cwd, timeoutMs = 55_000) {
|
|
|
201
269
|
: { available: false, reason: ack.reason };
|
|
202
270
|
}
|
|
203
271
|
/** Per-workspace status file, keyed by the same hash as the IPC endpoint. */
|
|
204
|
-
export function turnEndStatusPathForCwd(cwd) {
|
|
205
|
-
return path.join(os.tmpdir(), `pi-lens-turn-end-${workspaceHash(cwd)}.json`);
|
|
272
|
+
export function turnEndStatusPathForCwd(cwd, platform = process.platform) {
|
|
273
|
+
return path.join(os.tmpdir(), `pi-lens-turn-end-${workspaceHash(cwd, platform)}.json`);
|
|
206
274
|
}
|
|
207
|
-
export function readTurnEndStatus(cwd) {
|
|
275
|
+
export function readTurnEndStatus(cwd, platform = process.platform) {
|
|
208
276
|
try {
|
|
209
|
-
const raw = fs.readFileSync(turnEndStatusPathForCwd(cwd), "utf8");
|
|
277
|
+
const raw = fs.readFileSync(turnEndStatusPathForCwd(cwd, platform), "utf8");
|
|
210
278
|
const parsed = JSON.parse(raw);
|
|
211
279
|
if (!parsed || typeof parsed !== "object")
|
|
212
280
|
return undefined;
|
|
@@ -237,10 +305,10 @@ export function readTurnEndStatus(cwd) {
|
|
|
237
305
|
* this is bounded best-effort telemetry (self-heals, errors already
|
|
238
306
|
* swallowed below), and a lock on this path isn't worth it.
|
|
239
307
|
*/
|
|
240
|
-
export function recordTurnEndOutcome(cwd, outcome) {
|
|
308
|
+
export function recordTurnEndOutcome(cwd, outcome, platform = process.platform) {
|
|
241
309
|
try {
|
|
242
310
|
const now = new Date().toISOString();
|
|
243
|
-
const previous = readTurnEndStatus(cwd) ?? { ran: 0, skipped: 0 };
|
|
311
|
+
const previous = readTurnEndStatus(cwd, platform) ?? { ran: 0, skipped: 0 };
|
|
244
312
|
const next = outcome.ran
|
|
245
313
|
? { ...previous, ran: previous.ran + 1, lastRunAt: now }
|
|
246
314
|
: {
|
|
@@ -249,7 +317,15 @@ export function recordTurnEndOutcome(cwd, outcome) {
|
|
|
249
317
|
lastSkipReason: outcome.reason,
|
|
250
318
|
lastSkipAt: now,
|
|
251
319
|
};
|
|
252
|
-
|
|
320
|
+
// Writes THIS workspace's file and nothing else. #3255 round 2 also deleted
|
|
321
|
+
// the file at the retired always-fold name; round 3 removed that, because
|
|
322
|
+
// on a case-sensitive host that name is not an orphan — it is the live
|
|
323
|
+
// current file of the case-variant sibling workspace, and the record
|
|
324
|
+
// carries no ownership field that could tell the two apart. The
|
|
325
|
+
// pre-upgrade file is left to the same existence gate every other stale
|
|
326
|
+
// tmp artifact has: nothing derives that name any more, so nothing reads
|
|
327
|
+
// it except a still-running pre-upgrade server, for which it is correct.
|
|
328
|
+
writeFileAtomic(turnEndStatusPathForCwd(cwd, platform), `${JSON.stringify(next)}\n`);
|
|
253
329
|
}
|
|
254
330
|
catch {
|
|
255
331
|
// telemetry only — a read-only tmpdir must not break the Stop hook
|
|
@@ -272,7 +348,6 @@ export function requestWarmAnalyze(cwd, file, timeoutMs = 30_000) {
|
|
|
272
348
|
};
|
|
273
349
|
const socket = net.createConnection(ipcPathForCwd(cwd));
|
|
274
350
|
socket.setEncoding("utf8");
|
|
275
|
-
let buffer = "";
|
|
276
351
|
const timer = setTimeout(() => {
|
|
277
352
|
socket.destroy();
|
|
278
353
|
finish(undefined);
|
|
@@ -282,45 +357,158 @@ export function requestWarmAnalyze(cwd, file, timeoutMs = 30_000) {
|
|
|
282
357
|
const request = { file, cwd };
|
|
283
358
|
socket.write(`${JSON.stringify(request)}\n`);
|
|
284
359
|
});
|
|
285
|
-
socket.on("data", (
|
|
286
|
-
buffer += chunk;
|
|
287
|
-
const newline = buffer.indexOf("\n");
|
|
288
|
-
if (newline === -1)
|
|
289
|
-
return;
|
|
360
|
+
socket.on("data", createWarmIpcLineReader((line) => {
|
|
290
361
|
try {
|
|
291
|
-
const message = JSON.parse(
|
|
362
|
+
const message = JSON.parse(line);
|
|
292
363
|
finish(message.error ? undefined : message.result);
|
|
293
364
|
}
|
|
294
365
|
catch {
|
|
295
366
|
finish(undefined);
|
|
296
367
|
}
|
|
297
368
|
socket.end();
|
|
298
|
-
}
|
|
369
|
+
}, {
|
|
370
|
+
label: "warm-analyze-reply",
|
|
371
|
+
// `finish` here only clears the timer and resolves — unlike
|
|
372
|
+
// `requestOverWarmIpc`'s, which destroys the socket itself — so the
|
|
373
|
+
// peer that misframed would otherwise keep an open connection and
|
|
374
|
+
// keep writing into a reader that ignores it.
|
|
375
|
+
onOverflow: () => {
|
|
376
|
+
socket.destroy();
|
|
377
|
+
finish(undefined);
|
|
378
|
+
},
|
|
379
|
+
}));
|
|
299
380
|
// No server / connection refused / reset → cold fallback.
|
|
300
381
|
socket.on("error", () => finish(undefined));
|
|
301
382
|
socket.on("close", () => finish(undefined));
|
|
302
383
|
});
|
|
303
384
|
}
|
|
304
385
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* the
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
386
|
+
* The ceiling on ONE newline-framed line, for every reader below (#3383).
|
|
387
|
+
*
|
|
388
|
+
* Deliberately the same number as the spawn output cap rather than a second one
|
|
389
|
+
* to argue about: the largest legitimate line in this protocol is a
|
|
390
|
+
* `requestWarmDiagnostics` request carrying a file's whole content, or the
|
|
391
|
+
* diagnostics reply to it, and 32 MiB is far above anything the read guard lets
|
|
392
|
+
* through while staying 16x below V8's max string length — so the
|
|
393
|
+
* concatenation that crashed the host in #3375 is unreachable here rather than
|
|
394
|
+
* merely unlikely.
|
|
311
395
|
*/
|
|
312
|
-
export
|
|
396
|
+
export const MAX_FRAMED_LINE_BYTES = DEFAULT_MAX_OUTPUT_BYTES;
|
|
397
|
+
/**
|
|
398
|
+
* Newline-framed line reader for the warm IPC sockets and the MCP host's stdin.
|
|
399
|
+
*
|
|
400
|
+
* One-shot by default (#1219): the clients write exactly one newline-terminated
|
|
401
|
+
* request per connection and read one reply, so the server must dispatch at
|
|
402
|
+
* most one line and ignore anything after it — a `data` handler that keeps
|
|
403
|
+
* re-reading the same buffered line re-dispatches the request on stray bytes.
|
|
404
|
+
*
|
|
405
|
+
* BOUNDED since #3383. Every reader used to be `buffer += chunk` with no
|
|
406
|
+
* ceiling, so a peer that never sent a newline grew one JS string until the
|
|
407
|
+
* request's own timeout fired — measured through `requestWarmAnalyze` against a
|
|
408
|
+
* real socket: 64 MiB of newline-free reply, +929 MiB of heap, 20 s of it. Past
|
|
409
|
+
* {@link MAX_FRAMED_LINE_BYTES} the line is DISCARDED, the overflow is recorded
|
|
410
|
+
* once, and the caller's `onOverflow` decides the ending.
|
|
411
|
+
*
|
|
412
|
+
* The bound is on the FRAME, never on what is left over after a chunk's last
|
|
413
|
+
* newline (#3388 review H3388-1). The first version checked only the
|
|
414
|
+
* newline-free remainder, so a peer whose over-limit frame arrived with its
|
|
415
|
+
* newline in the SAME `data` chunk — which is what a single TCP segment or one
|
|
416
|
+
* `socket.write` produces — had its whole line dispatched, bound bypassed:
|
|
417
|
+
* measured at 33,554,433 bytes delivered, 0 overflow callbacks, 0 records. So
|
|
418
|
+
* each complete line is accounted for BEFORE it is dispatched, retained prefix
|
|
419
|
+
* included, and the remainder check below is now only the unterminated case.
|
|
420
|
+
*
|
|
421
|
+
* RESYNC applies to the unterminated case alone. A discarded UNTERMINATED line
|
|
422
|
+
* has a newline still to come, so framing skips to it and the next well-formed
|
|
423
|
+
* line is read normally rather than parsed as the dropped line's tail. A
|
|
424
|
+
* discarded COMPLETE line is already past its own newline, so framing continues
|
|
425
|
+
* from there — resyncing would eat the next line instead.
|
|
426
|
+
*
|
|
427
|
+
* Returns the handler to attach to the stream's `data` event.
|
|
428
|
+
*/
|
|
429
|
+
export function createWarmIpcLineReader(onLine, options) {
|
|
430
|
+
/** The unterminated head of a line, and its size. */
|
|
313
431
|
let buffer = "";
|
|
432
|
+
let bufferedBytes = 0;
|
|
314
433
|
let dispatched = false;
|
|
434
|
+
let resyncing = false;
|
|
435
|
+
/**
|
|
436
|
+
* Drop the frame, record it once, and let the caller end the exchange.
|
|
437
|
+
* `awaitingNewline` is the difference between the two overflow shapes: an
|
|
438
|
+
* unterminated line must be skipped up to the newline that has not arrived
|
|
439
|
+
* yet, a complete one must not, because framing is already past its newline.
|
|
440
|
+
*/
|
|
441
|
+
const overflow = (frameBytes, awaitingNewline) => {
|
|
442
|
+
buffer = "";
|
|
443
|
+
bufferedBytes = 0;
|
|
444
|
+
resyncing = awaitingNewline;
|
|
445
|
+
if (options.continuous !== true)
|
|
446
|
+
dispatched = true;
|
|
447
|
+
incrementDegradationCount({
|
|
448
|
+
kind: "ipc-frame-overflow",
|
|
449
|
+
subject: options.label,
|
|
450
|
+
reason: `discarded ${awaitingNewline ? "an unterminated" : "a complete"} line at ${frameBytes} bytes (limit ${MAX_FRAMED_LINE_BYTES})`,
|
|
451
|
+
metadata: {
|
|
452
|
+
limitBytes: MAX_FRAMED_LINE_BYTES,
|
|
453
|
+
overflowBytes: frameBytes,
|
|
454
|
+
terminated: !awaitingNewline,
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
options.onOverflow?.();
|
|
458
|
+
};
|
|
315
459
|
return (chunk) => {
|
|
316
460
|
if (dispatched)
|
|
317
461
|
return;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
462
|
+
// Every scan below runs over the NEW chunk at an offset, never over the
|
|
463
|
+
// retained buffer, and the retained bytes are carried rather than
|
|
464
|
+
// re-measured. Both matter once the buffer is allowed to reach the bound:
|
|
465
|
+
// `retained.indexOf()` flattens the accumulated rope on every socket read,
|
|
466
|
+
// and one 36 MiB unframed reply arrives as ~550 reads, so the bound alone
|
|
467
|
+
// would trade an unbounded string for seconds of CPU. Measured on
|
|
468
|
+
// `tests/clients/mcp/ipc-frame-bounds.test.ts`, whose real-socket case
|
|
469
|
+
// sends exactly that: 16.2 s with a rescan per read, 1.2 s with this.
|
|
470
|
+
let from = 0;
|
|
471
|
+
if (resyncing) {
|
|
472
|
+
// Still inside the over-long line: retain nothing until its newline.
|
|
473
|
+
const end = chunk.indexOf("\n");
|
|
474
|
+
if (end === -1)
|
|
475
|
+
return;
|
|
476
|
+
resyncing = false;
|
|
477
|
+
from = end + 1;
|
|
478
|
+
}
|
|
479
|
+
let newline = chunk.indexOf("\n", from);
|
|
480
|
+
while (newline !== -1) {
|
|
481
|
+
const segment = chunk.slice(from, newline);
|
|
482
|
+
from = newline + 1;
|
|
483
|
+
// H3388-1: the FRAME's length — this chunk's segment plus whatever was
|
|
484
|
+
// retained for it — decided before the line exists as a string.
|
|
485
|
+
const frameBytes = bufferedBytes + Buffer.byteLength(segment);
|
|
486
|
+
if (frameBytes > MAX_FRAMED_LINE_BYTES) {
|
|
487
|
+
overflow(frameBytes, false);
|
|
488
|
+
if (dispatched)
|
|
489
|
+
return;
|
|
490
|
+
newline = chunk.indexOf("\n", from);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const line = buffer + segment;
|
|
494
|
+
buffer = "";
|
|
495
|
+
bufferedBytes = 0;
|
|
496
|
+
if (options.continuous !== true) {
|
|
497
|
+
dispatched = true;
|
|
498
|
+
onLine(line);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
onLine(line);
|
|
502
|
+
newline = chunk.indexOf("\n", from);
|
|
503
|
+
}
|
|
504
|
+
if (from < chunk.length) {
|
|
505
|
+
const rest = chunk.slice(from);
|
|
506
|
+
buffer += rest;
|
|
507
|
+
bufferedBytes += Buffer.byteLength(rest);
|
|
508
|
+
}
|
|
509
|
+
if (bufferedBytes <= MAX_FRAMED_LINE_BYTES)
|
|
321
510
|
return;
|
|
322
|
-
|
|
323
|
-
onLine(buffer.slice(0, newline));
|
|
511
|
+
overflow(bufferedBytes, true);
|
|
324
512
|
};
|
|
325
513
|
}
|
|
326
514
|
export function createWarmIpcRequestQueue() {
|
|
@@ -10,7 +10,9 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { existsSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { pmBinary, resolveNodePackageManager, runScriptArgs, } from "../package-manager.js";
|
|
13
|
+
import { incrementDegradationCount } from "../degradation-ledger.js";
|
|
13
14
|
import { safeSpawnAsync } from "../safe-spawn.js";
|
|
15
|
+
import { createBoundedOutputSink, DEFAULT_MAX_OUTPUT_BYTES, } from "../spawn-output-cap.js";
|
|
14
16
|
/**
|
|
15
17
|
* Which npm script rebuilds the layout the server is running from. A server at
|
|
16
18
|
* `…/dist/mcp/server.js` is the precompiled dist (`build:dist` recreates it); a
|
|
@@ -27,6 +29,14 @@ export function resolveRebuildScript(serverFilePath) {
|
|
|
27
29
|
* Fork `node <workerPath>` to analyze a file in a fresh process. We spawn node
|
|
28
30
|
* directly (no shell) so an interpreter path containing spaces is safe on
|
|
29
31
|
* Windows — `safeSpawnAsync`'s shell mode does not escape the command itself.
|
|
32
|
+
*
|
|
33
|
+
* #3383: both pipes accumulate through {@link createBoundedOutputSink} rather
|
|
34
|
+
* than `+=`. The worker writes one JSON document to stdout, so an unbounded
|
|
35
|
+
* accumulation here could reach the same ending #3375 fixed at the shared spawn
|
|
36
|
+
* seam — V8 refusing the next concatenation with `RangeError: Invalid string
|
|
37
|
+
* length` inside a `data` handler, uncatchable by this promise. The only thing
|
|
38
|
+
* bounding it before was the `timeoutMs` kill, and a worker can emit hundreds
|
|
39
|
+
* of MiB long before 120 s elapse.
|
|
30
40
|
*/
|
|
31
41
|
export function analyzeFileFresh(workerPath, file, cwd, options = {}, timeoutMs = 120_000) {
|
|
32
42
|
return new Promise((resolve) => {
|
|
@@ -48,8 +58,8 @@ export function analyzeFileFresh(workerPath, file, cwd, options = {}, timeoutMs
|
|
|
48
58
|
});
|
|
49
59
|
return;
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const stdout = createBoundedOutputSink();
|
|
62
|
+
const stderr = createBoundedOutputSink();
|
|
53
63
|
let settled = false;
|
|
54
64
|
const finish = (outcome) => {
|
|
55
65
|
if (settled)
|
|
@@ -63,23 +73,59 @@ export function analyzeFileFresh(workerPath, file, cwd, options = {}, timeoutMs
|
|
|
63
73
|
finish({ error: `fresh analyze timed out after ${timeoutMs}ms` });
|
|
64
74
|
}, timeoutMs);
|
|
65
75
|
child.stdout.setEncoding("utf8");
|
|
66
|
-
child.stdout.on("data", (chunk) =>
|
|
76
|
+
child.stdout.on("data", (chunk) => {
|
|
77
|
+
// Settled already means timed out or failed: keep accumulating and the
|
|
78
|
+
// retained output outlives the promise nobody is awaiting any more.
|
|
79
|
+
if (settled)
|
|
80
|
+
return;
|
|
81
|
+
stdout.append(chunk);
|
|
82
|
+
// Truncated stdout is never parseable JSON, so the run is over. Report
|
|
83
|
+
// the cap as the ending instead of letting `JSON.parse` blame the
|
|
84
|
+
// worker's output shape, and terminate the producer.
|
|
85
|
+
if (!stdout.truncated)
|
|
86
|
+
return;
|
|
87
|
+
incrementDegradationCount({
|
|
88
|
+
kind: "spawn-output-cap-truncated",
|
|
89
|
+
subject: "mcp-fresh-analyze",
|
|
90
|
+
reason: `worker stdout reached the ${DEFAULT_MAX_OUTPUT_BYTES}-byte default cap after ${stdout.observedBytes} bytes; worker terminated`,
|
|
91
|
+
metadata: {
|
|
92
|
+
capBytes: DEFAULT_MAX_OUTPUT_BYTES,
|
|
93
|
+
capSource: "default",
|
|
94
|
+
observedBytes: stdout.observedBytes,
|
|
95
|
+
killed: true,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
child.kill();
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Best-effort: the worker may already be gone, and this runs inside a
|
|
103
|
+
// `data` handler where a throw would be uncatchable.
|
|
104
|
+
}
|
|
105
|
+
finish({
|
|
106
|
+
error: `worker output exceeded ${DEFAULT_MAX_OUTPUT_BYTES} bytes`,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
67
109
|
child.stderr.setEncoding("utf8");
|
|
68
|
-
child.stderr.on("data", (chunk) =>
|
|
110
|
+
child.stderr.on("data", (chunk) => {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
stderr.append(chunk);
|
|
114
|
+
});
|
|
69
115
|
child.on("error", (err) => finish({ error: `failed to fork worker: ${err.message}` }));
|
|
70
116
|
child.on("close", (code) => {
|
|
71
117
|
if (code !== 0) {
|
|
72
118
|
finish({
|
|
73
|
-
error: `worker exited ${code}: ${stderr.trim() || "(no stderr)"}`,
|
|
119
|
+
error: `worker exited ${code}: ${stderr.text.trim() || "(no stderr)"}`,
|
|
74
120
|
});
|
|
75
121
|
return;
|
|
76
122
|
}
|
|
77
123
|
try {
|
|
78
|
-
finish({ result: JSON.parse(stdout) });
|
|
124
|
+
finish({ result: JSON.parse(stdout.text) });
|
|
79
125
|
}
|
|
80
126
|
catch {
|
|
81
127
|
finish({
|
|
82
|
-
error: `worker produced invalid JSON (${stderr.trim() || stdout.slice(0, 200)})`,
|
|
128
|
+
error: `worker produced invalid JSON (${stderr.text.trim() || stdout.text.slice(0, 200)})`,
|
|
83
129
|
});
|
|
84
130
|
}
|
|
85
131
|
});
|
|
@@ -183,13 +183,18 @@ export class OpengrepClient extends SecurityScanClient {
|
|
|
183
183
|
success: true,
|
|
184
184
|
// A warning-level partial report with no scanned paths proves that
|
|
185
185
|
// the producer went cold. A complete empty report still proves a
|
|
186
|
-
// genuine scan, so it
|
|
186
|
+
// genuine scan, so it is still "analysed" for the render layer's
|
|
187
|
+
// analysed-and-found-nothing state (#2970) — but it carries no
|
|
188
|
+
// FILE-level authority, which `analyzedFiles: []` below is what
|
|
189
|
+
// says.
|
|
187
190
|
analyzed: !report.partial || report.scanned.length > 0,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
// #2962: the set is ALWAYS carried, empty included. "This producer
|
|
192
|
+
// declared its coverage and it was zero files" and "this producer
|
|
193
|
+
// declares no coverage at all" are different facts; dropping the
|
|
194
|
+
// empty array collapses them into the second, and the second is
|
|
195
|
+
// what hands a zero-file scan whole-root retirement authority
|
|
196
|
+
// downstream (`runnerRetirementDecision`, tools/lens-diagnostics.ts).
|
|
197
|
+
analyzedFiles: report.scanned.map((file) => realpathOrResolve(path.resolve(cwd, file))),
|
|
193
198
|
...(report.partial ? { partial: true } : {}),
|
|
194
199
|
...(report.partial ? { summary: report.partial.reason } : {}),
|
|
195
200
|
...(report.partial && report.scanned.length === 0
|