@herbertgao/pi-extensions 2026.9.15 → 2026.9.17
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/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
- 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 +9 -6
|
@@ -165,6 +165,10 @@ function formatTurnEnd(response) {
|
|
|
165
165
|
*/
|
|
166
166
|
const TURN_END_SKIP_REASONS = {
|
|
167
167
|
"ipc-error": "no warm pi-lens MCP server answered for this workspace, or its build is stale — start the MCP server, or run `npm run build` if you have edited pi-lens sources",
|
|
168
|
+
// #3255: split out of `ipc-error` because its remedy is different and no
|
|
169
|
+
// rebuild reaches it. A server that was already running when pi-lens was
|
|
170
|
+
// upgraded still owns the endpoint name the previous build derived.
|
|
171
|
+
"no-listener": "nothing is listening on this workspace's warm endpoint — start the MCP server; if pi-lens was upgraded while a server was running, that server still owns the endpoint name of the previous build, so restart it",
|
|
168
172
|
timeout: "the warm pi-lens MCP server did not finish the pass within the hook budget — the turn-end findings are kept and will be delivered on a later Stop",
|
|
169
173
|
"schema-mismatch": "the warm pi-lens MCP server speaks a different turn-end schema than this hook binary — rebuild and restart the server so both halves match",
|
|
170
174
|
"stale-answer": "the warm pi-lens MCP server replied about a superseded state — the findings are kept for a later Stop",
|
|
@@ -321,8 +321,16 @@ function startIpcServer() {
|
|
|
321
321
|
socket.end(`${JSON.stringify({ error: String(err) })}\n`);
|
|
322
322
|
}
|
|
323
323
|
});
|
|
324
|
+
}, {
|
|
325
|
+
label: "mcp-warm-server",
|
|
326
|
+
// #3383: a peer that never terminates its request line gets the same
|
|
327
|
+
// error reply every other unusable request gets, instead of this
|
|
328
|
+
// process holding its bytes until the client's timeout.
|
|
329
|
+
onOverflow: () => socket.end(`${JSON.stringify({ error: "warm request line exceeded the framing limit" })}\n`),
|
|
324
330
|
}));
|
|
325
|
-
|
|
331
|
+
// The stream is already destroyed when this handler runs; the listener
|
|
332
|
+
// remains the surviving teardown witness, while destroy() was a no-op.
|
|
333
|
+
socket.on("error", () => { });
|
|
326
334
|
});
|
|
327
335
|
ipc.on("error", (err) => {
|
|
328
336
|
// Listener failure must not take down the MCP server — warm channel is an
|
|
@@ -1623,28 +1631,32 @@ async function handleRequest(request) {
|
|
|
1623
1631
|
}
|
|
1624
1632
|
}
|
|
1625
1633
|
// --- stdio read loop (newline-delimited JSON) --------------------------------
|
|
1626
|
-
let buffer = "";
|
|
1627
1634
|
process.stdin.setEncoding("utf8");
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
if (request)
|
|
1643
|
-
void handleRequest(request);
|
|
1644
|
-
}
|
|
1645
|
-
newlineIndex = buffer.indexOf("\n");
|
|
1635
|
+
// #3383: framed by the shared reader, so the partial line this loop holds
|
|
1636
|
+
// between chunks has a ceiling. It used to be `buffer += chunk` with none: a
|
|
1637
|
+
// host that never sent a newline grew one JS string until V8 refused the next
|
|
1638
|
+
// concatenation with `RangeError: Invalid string length` — raised inside this
|
|
1639
|
+
// `data` handler, where nothing can catch it, which is the ending #3375 fixed
|
|
1640
|
+
// for child output. `continuous` because this is a stream of requests, not one
|
|
1641
|
+
// request per connection like every socket reader.
|
|
1642
|
+
process.stdin.on("data", createWarmIpcLineReader((raw) => {
|
|
1643
|
+
const line = raw.trim();
|
|
1644
|
+
if (line.length === 0)
|
|
1645
|
+
return;
|
|
1646
|
+
let request;
|
|
1647
|
+
try {
|
|
1648
|
+
request = JSON.parse(line);
|
|
1646
1649
|
}
|
|
1647
|
-
|
|
1650
|
+
catch {
|
|
1651
|
+
sendError(null, -32700, "Parse error");
|
|
1652
|
+
}
|
|
1653
|
+
if (request)
|
|
1654
|
+
void handleRequest(request);
|
|
1655
|
+
}, {
|
|
1656
|
+
label: "mcp-stdio",
|
|
1657
|
+
continuous: true,
|
|
1658
|
+
onOverflow: () => sendError(null, -32700, "Parse error: request line exceeded the framing limit"),
|
|
1659
|
+
}));
|
|
1648
1660
|
process.stdin.on("end", () => {
|
|
1649
1661
|
endSituationalToolTelemetry();
|
|
1650
1662
|
void flushExtensionLog().finally(() => process.exit(0));
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where web-tree-sitter is installed — ONE ladder, for every caller that needs
|
|
3
|
+
* the answer (#3409).
|
|
4
|
+
*
|
|
5
|
+
* Callers: `clients/tree-sitter-client.ts` (the grammar read path and the
|
|
6
|
+
* lazy-fetch write target), `clients/install-diagnostics.ts` (the pasted
|
|
7
|
+
* environment fingerprint) and `scripts/install-selftest.mjs` (the installed
|
|
8
|
+
* host's own diagnostic). Each used to spell its own rungs, and the two that led
|
|
9
|
+
* with a BARE specifier were unconditionally broken on the host pi ships: inside
|
|
10
|
+
* a `bun build --compile` binary `require.resolve("web-tree-sitter")` throws
|
|
11
|
+
* MODULE_NOT_FOUND while `require.resolve("web-tree-sitter/tree-sitter.wasm")` —
|
|
12
|
+
* an explicit file subpath — still resolves.
|
|
13
|
+
*
|
|
14
|
+
* It lives in `scripts/lib/` rather than `clients/` because the installed
|
|
15
|
+
* selftest is packaged (`files[]`) without `clients/`, and this is the house
|
|
16
|
+
* pattern for logic a packaged .mjs script and the TypeScript runtime share
|
|
17
|
+
* (`scripts/lib/skills-predicate.mjs`, `scripts/lib/process-scan.mjs`). It
|
|
18
|
+
* imports nothing but `node:fs` and `node:path`, so it loads even on the hosts
|
|
19
|
+
* whose broken dependency graph the selftest exists to report.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
/** The package this module is about. Also the name its manifest must carry. */
|
|
24
|
+
const WEB_TREE_SITTER = "web-tree-sitter";
|
|
25
|
+
/**
|
|
26
|
+
* The wasm the runtime loads, at the package root — where 0.25.10's `exports`
|
|
27
|
+
* map puts it (`"./tree-sitter.wasm": "./tree-sitter.wasm"`). If a later release
|
|
28
|
+
* moves it (#381's 0.26 migration), the two FALLBACK rungs below stop matching
|
|
29
|
+
* and the resolver rungs, which ask Node for that same subpath, keep working;
|
|
30
|
+
* this constant is then the one line to update.
|
|
31
|
+
*/
|
|
32
|
+
const RUNTIME_WASM = "tree-sitter.wasm";
|
|
33
|
+
/**
|
|
34
|
+
* True when `dir` IS an installed web-tree-sitter package: its own manifest
|
|
35
|
+
* names it, and the wasm the runtime loads is in it.
|
|
36
|
+
*
|
|
37
|
+
* Round 1 review of #3409 (R3418-1): the constructed rungs used to accept any
|
|
38
|
+
* directory that merely EXISTED at `<base>/node_modules/web-tree-sitter`, so an
|
|
39
|
+
* empty or foreign directory of that name became the runtime fetch's write
|
|
40
|
+
* target and a downloaded grammar would land in an unrelated tree. Existence is
|
|
41
|
+
* not identity. The manifest name is the identity; the wasm is what makes the
|
|
42
|
+
* directory the right place to put grammars, since a grammar is only loadable by
|
|
43
|
+
* the web-tree-sitter runtime sitting next to it (the ABI drift #1564 is about).
|
|
44
|
+
*/
|
|
45
|
+
export function isWebTreeSitterPackageDir(dir) {
|
|
46
|
+
try {
|
|
47
|
+
if (!fs.existsSync(path.join(dir, RUNTIME_WASM)))
|
|
48
|
+
return false;
|
|
49
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
50
|
+
return manifest?.name === WEB_TREE_SITTER;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The package directory owning `resolvedFile`: walk up until a directory passes
|
|
58
|
+
* the identity check, or the filesystem root is reached. The walk exists because
|
|
59
|
+
* an `exports` map may point into a subdirectory, so the file's own parent is
|
|
60
|
+
* not necessarily the package; the identity check is what stops the walk, so a
|
|
61
|
+
* resolver that answers outside any web-tree-sitter package yields nothing
|
|
62
|
+
* rather than an unrelated ancestor.
|
|
63
|
+
*/
|
|
64
|
+
function packageDirOf(resolvedFile) {
|
|
65
|
+
let dir = path.dirname(resolvedFile);
|
|
66
|
+
for (;;) {
|
|
67
|
+
if (isWebTreeSitterPackageDir(dir))
|
|
68
|
+
return dir;
|
|
69
|
+
const parent = path.dirname(dir);
|
|
70
|
+
if (parent === dir)
|
|
71
|
+
return undefined;
|
|
72
|
+
dir = parent;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* web-tree-sitter's installed package directory, or undefined.
|
|
77
|
+
*
|
|
78
|
+
* Rung order, most authoritative first:
|
|
79
|
+
* 1. the `tree-sitter.wasm` SUBPATH, which resolves on a compiled host as well
|
|
80
|
+
* as a plain one. This is the rung that makes the non-core grammars
|
|
81
|
+
* fetchable there at all.
|
|
82
|
+
* 2. the BARE specifier, for a future web-tree-sitter whose exports map no
|
|
83
|
+
* longer carries `./tree-sitter.wasm`.
|
|
84
|
+
* 3. pi-lens's own package root, for the temp-dir compile layout (#20) where
|
|
85
|
+
* the resolver's context is a temp directory but the package root still has
|
|
86
|
+
* the right `node_modules`.
|
|
87
|
+
* 4. the working directory, the pre-#3409 last resort.
|
|
88
|
+
*
|
|
89
|
+
* Rungs 1-2 are the resolver's answer and outrank the constructed guesses: the
|
|
90
|
+
* grammars must come from the SAME package whose wasm the runtime loaded. Every
|
|
91
|
+
* rung's candidate passes `isWebTreeSitterPackageDir` before it is returned.
|
|
92
|
+
*
|
|
93
|
+
* `resolve`, `packageRoot` and `cwd` are injected, never defaulted: the caller's
|
|
94
|
+
* resolver context and package root are what differ on a compiled host, so they
|
|
95
|
+
* must not be silently replaced by this module's — and a rung nothing can vary
|
|
96
|
+
* is a rung no test can drive.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveWebTreeSitterPackageDir(deps) {
|
|
99
|
+
for (const specifier of [
|
|
100
|
+
`${WEB_TREE_SITTER}/${RUNTIME_WASM}`,
|
|
101
|
+
WEB_TREE_SITTER,
|
|
102
|
+
]) {
|
|
103
|
+
try {
|
|
104
|
+
const dir = packageDirOf(deps.resolve(specifier));
|
|
105
|
+
if (dir)
|
|
106
|
+
return dir;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
/* next rung */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const base of [deps.packageRoot, deps.cwd]) {
|
|
113
|
+
try {
|
|
114
|
+
const candidate = path.join(base(), "node_modules", WEB_TREE_SITTER);
|
|
115
|
+
if (isWebTreeSitterPackageDir(candidate))
|
|
116
|
+
return candidate;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* next rung */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
@@ -47,7 +47,7 @@ import { findAuxiliaryProfileForSource, retagAuxiliaryDiagnostics, } from "../cl
|
|
|
47
47
|
import { detectFileRole } from "../clients/file-role.js";
|
|
48
48
|
import { STALE_LINE_MARKER } from "../clients/stale-marker.js";
|
|
49
49
|
import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
|
|
50
|
-
import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, } from "./lsp-diagnostics.js";
|
|
50
|
+
import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, renderBatchOutcomeLines, } from "./lsp-diagnostics.js";
|
|
51
51
|
import { demotePastEofDiagnostics, PAST_EOF_STALE_MARKER, resyncDocumentOnPastEof, } from "../clients/diagnostic-line-freshness.js";
|
|
52
52
|
// The widget state exposes the full per-file diagnostic set; this is the tool's
|
|
53
53
|
// own generous display budget per file (independent of the TUI's 12 cap), to
|
|
@@ -197,11 +197,15 @@ captureLspStatusRepaint, getRuntime, isLensGuardEnabled = () => true) {
|
|
|
197
197
|
if ((details.unconfirmedFiles ?? 0) > 0)
|
|
198
198
|
return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.unconfirmedFiles} unconfirmed${details.timedOutFiles ? ` (${details.timedOutFiles} timed out)` : ""}`;
|
|
199
199
|
const outcomeCounts = details.outcomeCounts;
|
|
200
|
+
const tooLargeLines = renderBatchOutcomeLines(details.outcomes ?? []);
|
|
201
|
+
if (tooLargeLines.length > 0)
|
|
202
|
+
return `lens_diagnostics${scope} — too large: ${tooLargeLines.join("; ")}`;
|
|
200
203
|
const notConfirmed = outcomeCounts
|
|
201
204
|
? (outcomeCounts.inconclusive ?? 0) +
|
|
202
205
|
(outcomeCounts.unavailable ?? 0) +
|
|
203
206
|
(outcomeCounts.unsupported ?? 0) +
|
|
204
|
-
(outcomeCounts.failed ?? 0)
|
|
207
|
+
(outcomeCounts.failed ?? 0) +
|
|
208
|
+
(outcomeCounts.too_large ?? 0)
|
|
205
209
|
: 0;
|
|
206
210
|
if (notConfirmed > 0)
|
|
207
211
|
return `lens_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
|
|
@@ -511,13 +515,16 @@ function formatProjectDeltaDiagnostic(diagnostic, stale) {
|
|
|
511
515
|
function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
|
|
512
516
|
const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
|
|
513
517
|
matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
|
|
514
|
-
const gated = gateFindingsByPathFreshness({
|
|
515
|
-
store: "lens-diagnostics-delta-project",
|
|
516
|
-
findings: scoped,
|
|
518
|
+
const { "lens-diagnostics-delta-project": gated } = gateFindingsByPathFreshness({
|
|
517
519
|
cwd,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
sources: {
|
|
521
|
+
"lens-diagnostics-delta-project": {
|
|
522
|
+
findings: scoped,
|
|
523
|
+
scannedAt: report?.generatedAt,
|
|
524
|
+
citedPath: (d) => d.filePath,
|
|
525
|
+
onMissing: "drop",
|
|
526
|
+
},
|
|
527
|
+
},
|
|
521
528
|
});
|
|
522
529
|
const staleSet = new Set(gated.stale);
|
|
523
530
|
// Concatenating live-then-stale reorders a file's demoted rows to the end
|
|
@@ -702,13 +709,16 @@ function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
|
702
709
|
}
|
|
703
710
|
if (flat.length === 0)
|
|
704
711
|
return files;
|
|
705
|
-
const gated = gateFindingsByPathFreshness({
|
|
706
|
-
store: "lens-diagnostics-delta",
|
|
707
|
-
findings: flat,
|
|
712
|
+
const { "lens-diagnostics-delta": gated } = gateFindingsByPathFreshness({
|
|
708
713
|
cwd,
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
714
|
+
sources: {
|
|
715
|
+
"lens-diagnostics-delta": {
|
|
716
|
+
findings: flat,
|
|
717
|
+
scannedAt: effectiveAt,
|
|
718
|
+
citedPath: (f) => f.filePath,
|
|
719
|
+
onMissing: "drop",
|
|
720
|
+
},
|
|
721
|
+
},
|
|
712
722
|
});
|
|
713
723
|
// Two passes (live, then stale) reorder a file's demoted rows to the end
|
|
714
724
|
// of its warnings array rather than the original report order — cosmetic
|
|
@@ -1160,11 +1170,20 @@ function runnerIdOf(diagnostic) {
|
|
|
1160
1170
|
}
|
|
1161
1171
|
/**
|
|
1162
1172
|
* The one project-runner retirement decision. Coverage is authoritative when
|
|
1163
|
-
* present for the runner; the older id-only arm is used only when
|
|
1173
|
+
* present for the runner; the older id-only arm is used only when the runner
|
|
1174
|
+
* declared no coverage at all.
|
|
1175
|
+
*
|
|
1176
|
+
* #2962: an entry with an EMPTY file set is a declaration, not a missing one —
|
|
1177
|
+
* "this producer analysed the root and proved zero files". Selecting it out by
|
|
1178
|
+
* its own size (the pre-#2962 `entry.files.size > 0` filter) dropped it into the
|
|
1179
|
+
* whole-root id-only arm, so one complete opengrep report with
|
|
1180
|
+
* `paths.scanned: []` retired every retained opengrep finding in the tree even
|
|
1181
|
+
* though no file had been scanned. The id-only arm is unchanged for the eight
|
|
1182
|
+
* runners that declare no coverage.
|
|
1164
1183
|
*/
|
|
1165
1184
|
export function runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeCoverage) {
|
|
1166
1185
|
const runnerId = runnerIdOf(diagnostic);
|
|
1167
|
-
const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId
|
|
1186
|
+
const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId);
|
|
1168
1187
|
if (coverage.length === 0) {
|
|
1169
1188
|
return authoritativeRunnerIds?.has(runnerId) ? "retire" : "keep";
|
|
1170
1189
|
}
|
|
@@ -1859,6 +1878,14 @@ async function formatFullMode(cwd, severity, lspService, cacheManager, options =
|
|
|
1859
1878
|
});
|
|
1860
1879
|
}
|
|
1861
1880
|
for (const entry of authoritativeRunnerCoverage) {
|
|
1881
|
+
// #2962: a zero-file declaration retires nothing, so it must neither
|
|
1882
|
+
// write this row (a `runner_coverage_retired` row with count 0 would
|
|
1883
|
+
// report the opposite of what happened) nor BURN the once-per-session
|
|
1884
|
+
// claim — the session's first real coverage row would then be suppressed
|
|
1885
|
+
// by the call that proved no coverage. Its own observation is the
|
|
1886
|
+
// `runner-coverage-empty` ledger row raised at the fresh-fetch seam.
|
|
1887
|
+
if (entry.files.size === 0)
|
|
1888
|
+
continue;
|
|
1862
1889
|
if (!claimPhaseOncePerSession("runner_coverage_retired", entry.runnerId)) {
|
|
1863
1890
|
continue;
|
|
1864
1891
|
}
|
|
@@ -28,6 +28,7 @@ import { countRetainedSuppressedRows, isBlocking, reconcileScanDiagnostics, } fr
|
|
|
28
28
|
import { makeProgressReporter } from "./scan-progress.js";
|
|
29
29
|
import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
|
|
30
30
|
import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
|
|
31
|
+
import { exceedsLspSyncLimits } from "../clients/lsp/content-limits.js";
|
|
31
32
|
const MAX_FILES = 100;
|
|
32
33
|
export const MAX_BATCH_FILES = 100;
|
|
33
34
|
const MAX_DIAGNOSTICS = 200;
|
|
@@ -45,6 +46,30 @@ export const LSP_SEVERITY_FILTERS = [
|
|
|
45
46
|
...Object.values(SEVERITY_NAMES),
|
|
46
47
|
"all",
|
|
47
48
|
];
|
|
49
|
+
function assertNeverBatchFileOutcome(value) {
|
|
50
|
+
throw new Error(`Unhandled batch file outcome: ${String(value)}`);
|
|
51
|
+
}
|
|
52
|
+
/** Render the bounded per-file outcomes shared by batch, directory, and fold surfaces. */
|
|
53
|
+
export function renderBatchOutcomeLines(outcomes, formatFile = (file) => file) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
for (const outcome of outcomes) {
|
|
56
|
+
switch (outcome.outcome) {
|
|
57
|
+
case "too_large":
|
|
58
|
+
lines.push(`${formatFile(outcome.file)}: ${outcome.reason ?? "file too large for LSP diagnostics"}`);
|
|
59
|
+
break;
|
|
60
|
+
case "clean":
|
|
61
|
+
case "findings":
|
|
62
|
+
case "unsupported":
|
|
63
|
+
case "unavailable":
|
|
64
|
+
case "failed":
|
|
65
|
+
case "inconclusive":
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
assertNeverBatchFileOutcome(outcome.outcome);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
48
73
|
function batchFileDeadlineMs() {
|
|
49
74
|
const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
|
|
50
75
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
|
|
@@ -340,6 +365,24 @@ scanRoot, waitMs, serverScope = "all") {
|
|
|
340
365
|
let touched;
|
|
341
366
|
try {
|
|
342
367
|
content = fs.readFileSync(absPath, "utf-8");
|
|
368
|
+
const contentLimit = exceedsLspSyncLimits(content);
|
|
369
|
+
if (contentLimit.tooLarge) {
|
|
370
|
+
const reason = `file too large for LSP diagnostics (${contentLimit.reason})`;
|
|
371
|
+
recordDegradationOnce({
|
|
372
|
+
kind: "lsp-diagnostics-file-too-large",
|
|
373
|
+
subject: absPath,
|
|
374
|
+
reason,
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
diagnostics: [],
|
|
378
|
+
timedOut: false,
|
|
379
|
+
confirmedByTouch: false,
|
|
380
|
+
unconfirmedServerIds: [],
|
|
381
|
+
diagnosticsUnsupportedServerIds: [],
|
|
382
|
+
content,
|
|
383
|
+
tooLargeReason: reason,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
343
386
|
if (isWarmAttached()) {
|
|
344
387
|
// The local sweep's default service wait is bounded to the same
|
|
345
388
|
// 15-second per-file envelope used by the workspace sweep. An explicit
|
|
@@ -387,6 +430,12 @@ scanRoot, waitMs, serverScope = "all") {
|
|
|
387
430
|
maxClientWaitMs: waitMs,
|
|
388
431
|
source: "lsp_diagnostics",
|
|
389
432
|
clientScope: serverScope,
|
|
433
|
+
// #3405: the caller asked whether THIS file is clean right now, and the
|
|
434
|
+
// content above was just read from disk — so it is the file's saved
|
|
435
|
+
// state. Without the save a save-triggered server answers zero
|
|
436
|
+
// diagnostics for every file in the query, which this path would report
|
|
437
|
+
// as clean.
|
|
438
|
+
saved: true,
|
|
390
439
|
});
|
|
391
440
|
timedOut = touched?.inconclusive === true;
|
|
392
441
|
}
|
|
@@ -802,7 +851,10 @@ onConfirmedNoBlockers) {
|
|
|
802
851
|
};
|
|
803
852
|
}
|
|
804
853
|
}
|
|
805
|
-
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
|
|
854
|
+
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
|
|
855
|
+
if (tooLargeReason !== undefined) {
|
|
856
|
+
return { file, diagnostics: [], outcome: "too_large", tooLargeReason };
|
|
857
|
+
}
|
|
806
858
|
const health = lspService.getDiagnosticsHealth?.(file);
|
|
807
859
|
// #570: a timed-out priming check is never a confirmed "clean" — treat it
|
|
808
860
|
// as unconfirmed without consulting the (unrelated) silent-tier
|
|
@@ -918,7 +970,22 @@ onConfirmedNoBlockers) {
|
|
|
918
970
|
// Reserve the token before awaiting this file's LSP result. The direct-file
|
|
919
971
|
// path performs its own confirmation/reconciliation below (#1198).
|
|
920
972
|
const writeIndex = nextWriteIndex?.();
|
|
921
|
-
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
|
|
973
|
+
const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
|
|
974
|
+
if (tooLargeReason !== undefined) {
|
|
975
|
+
return {
|
|
976
|
+
content: [{ type: "text", text: tooLargeReason }],
|
|
977
|
+
details: {
|
|
978
|
+
filePath: absPath,
|
|
979
|
+
mode: "file",
|
|
980
|
+
severity,
|
|
981
|
+
serverScope,
|
|
982
|
+
outcome: "too_large",
|
|
983
|
+
tooLargeReason,
|
|
984
|
+
diagnostics: [],
|
|
985
|
+
totalDiagnostics: 0,
|
|
986
|
+
},
|
|
987
|
+
};
|
|
988
|
+
}
|
|
922
989
|
const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
|
|
923
990
|
const unavailable = lspUnavailableMessage(absPath, lspHealth);
|
|
924
991
|
// #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
|
|
@@ -1077,6 +1144,7 @@ onConfirmedNoBlockers) {
|
|
|
1077
1144
|
mode: "file",
|
|
1078
1145
|
severity,
|
|
1079
1146
|
serverScope,
|
|
1147
|
+
...(unavailable ? { unavailable } : {}),
|
|
1080
1148
|
primaryServerId: primaryId,
|
|
1081
1149
|
primaryDiagnosticsCount: primaryDiags.length,
|
|
1082
1150
|
auxiliaryDiagnosticsCount: auxiliaryDiags.length,
|
|
@@ -1120,7 +1188,12 @@ function tallyConfirmation(results) {
|
|
|
1120
1188
|
let unconfirmed = 0;
|
|
1121
1189
|
let timedOut = 0;
|
|
1122
1190
|
let navigationOnly = 0;
|
|
1191
|
+
let tooLarge = 0;
|
|
1123
1192
|
for (const result of results) {
|
|
1193
|
+
if (result.tooLargeReason) {
|
|
1194
|
+
tooLarge += 1;
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1124
1197
|
if (result.diagnosticsUnsupported) {
|
|
1125
1198
|
navigationOnly += 1;
|
|
1126
1199
|
continue;
|
|
@@ -1138,11 +1211,13 @@ function tallyConfirmation(results) {
|
|
|
1138
1211
|
clean += 1;
|
|
1139
1212
|
}
|
|
1140
1213
|
}
|
|
1141
|
-
return { clean, unconfirmed, timedOut, navigationOnly };
|
|
1214
|
+
return { clean, unconfirmed, timedOut, navigationOnly, tooLarge };
|
|
1142
1215
|
}
|
|
1143
1216
|
function classifyBatchFileOutcome(result) {
|
|
1144
1217
|
if (result.error)
|
|
1145
1218
|
return "failed";
|
|
1219
|
+
if (result.tooLargeReason)
|
|
1220
|
+
return "too_large";
|
|
1146
1221
|
if (result.diagnosticsUnsupported)
|
|
1147
1222
|
return "unsupported";
|
|
1148
1223
|
// A timed-out/unconfirmed answer may contain partial findings, but it cannot
|
|
@@ -1298,6 +1373,7 @@ async function collectBatchDiagnostics(files, severity, lspService, options) {
|
|
|
1298
1373
|
"unsupported",
|
|
1299
1374
|
"unavailable",
|
|
1300
1375
|
"failed",
|
|
1376
|
+
"too_large",
|
|
1301
1377
|
"inconclusive",
|
|
1302
1378
|
].map((outcome) => [
|
|
1303
1379
|
outcome,
|
|
@@ -1353,6 +1429,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1353
1429
|
outcomeCounts.unavailable +
|
|
1354
1430
|
outcomeCounts.unsupported +
|
|
1355
1431
|
outcomeCounts.failed +
|
|
1432
|
+
outcomeCounts.too_large +
|
|
1356
1433
|
incompleteFiles;
|
|
1357
1434
|
if (notConfirmed > 0) {
|
|
1358
1435
|
lines.push("", `Checks not confirmed: ${notConfirmed} (inconclusive/unavailable/unsupported/failed or not started).`);
|
|
@@ -1362,6 +1439,18 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1362
1439
|
}
|
|
1363
1440
|
if (fileErrors.length > 0)
|
|
1364
1441
|
lines.push("", "File errors:", ...fileErrors);
|
|
1442
|
+
const outcomeDetails = results.map((result) => ({
|
|
1443
|
+
file: result.file,
|
|
1444
|
+
outcome: result.outcome,
|
|
1445
|
+
reason: result.tooLargeReason ??
|
|
1446
|
+
result.inconclusiveReason ??
|
|
1447
|
+
result.error ??
|
|
1448
|
+
result.unavailable,
|
|
1449
|
+
}));
|
|
1450
|
+
const tooLargeLines = renderBatchOutcomeLines(outcomeDetails);
|
|
1451
|
+
if (tooLargeLines.length > 0) {
|
|
1452
|
+
lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
|
|
1453
|
+
}
|
|
1365
1454
|
if (lspHealthWarnings.length > 0) {
|
|
1366
1455
|
lines.push("", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10));
|
|
1367
1456
|
}
|
|
@@ -1376,7 +1465,7 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1376
1465
|
`${unconfirmedReasonClause(unconfirmed, timedOut)} NOT the same as 0 diagnostics.`);
|
|
1377
1466
|
}
|
|
1378
1467
|
if (display.length === 0) {
|
|
1379
|
-
if (unconfirmed === 0) {
|
|
1468
|
+
if (unconfirmed === 0 && outcomeCounts.clean === results.length) {
|
|
1380
1469
|
lines.push("", "No diagnostics found.");
|
|
1381
1470
|
}
|
|
1382
1471
|
}
|
|
@@ -1418,7 +1507,10 @@ async function runBatchFileDiagnostics(absPaths, severity, lspService, options)
|
|
|
1418
1507
|
outcomes: results.map((result) => ({
|
|
1419
1508
|
file: result.file,
|
|
1420
1509
|
outcome: result.outcome,
|
|
1421
|
-
reason: result.
|
|
1510
|
+
reason: result.tooLargeReason ??
|
|
1511
|
+
result.inconclusiveReason ??
|
|
1512
|
+
result.error ??
|
|
1513
|
+
result.unavailable,
|
|
1422
1514
|
primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId === result.primaryServerId).length,
|
|
1423
1515
|
auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId !== result.primaryServerId).length,
|
|
1424
1516
|
})),
|
|
@@ -1482,11 +1574,20 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1482
1574
|
}
|
|
1483
1575
|
const wasCapped = collectedFiles.length > MAX_FILES;
|
|
1484
1576
|
const filesToProcess = collectedFiles.slice(0, MAX_FILES);
|
|
1485
|
-
const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
|
|
1577
|
+
const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
|
|
1486
1578
|
const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
|
|
1487
1579
|
// #3088: the directory scan's own suppressed bucket — one summed line.
|
|
1488
1580
|
const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
|
|
1489
1581
|
const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
|
|
1582
|
+
const outcomeDetails = results.map((result) => ({
|
|
1583
|
+
file: result.file,
|
|
1584
|
+
outcome: result.outcome,
|
|
1585
|
+
reason: result.tooLargeReason ??
|
|
1586
|
+
result.inconclusiveReason ??
|
|
1587
|
+
result.error ??
|
|
1588
|
+
result.unavailable,
|
|
1589
|
+
}));
|
|
1590
|
+
const tooLargeLines = renderBatchOutcomeLines(outcomeDetails, (file) => path.relative(absPath, file));
|
|
1490
1591
|
let text;
|
|
1491
1592
|
if (total === 0) {
|
|
1492
1593
|
// #533/#570: an unconfirmed-containing directory result must never
|
|
@@ -1504,7 +1605,12 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1504
1605
|
"LSP unavailable for one or more files:",
|
|
1505
1606
|
...lspHealthWarnings.slice(0, 10),
|
|
1506
1607
|
]
|
|
1507
|
-
:
|
|
1608
|
+
: unconfirmed > 0 || outcomeCounts.clean === filesToProcess.length
|
|
1609
|
+
? [cleanLine]
|
|
1610
|
+
: []),
|
|
1611
|
+
...(tooLargeLines.length > 0
|
|
1612
|
+
? ["", "Files too large for LSP diagnostics:", ...tooLargeLines]
|
|
1613
|
+
: []),
|
|
1508
1614
|
...(suppressedLine ? ["", suppressedLine] : []),
|
|
1509
1615
|
].join("\n");
|
|
1510
1616
|
}
|
|
@@ -1547,6 +1653,9 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1547
1653
|
}
|
|
1548
1654
|
if (suppressedLine)
|
|
1549
1655
|
lines.push("", suppressedLine);
|
|
1656
|
+
if (tooLargeLines.length > 0) {
|
|
1657
|
+
lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
|
|
1658
|
+
}
|
|
1550
1659
|
text = lines.join("\n");
|
|
1551
1660
|
}
|
|
1552
1661
|
return {
|
|
@@ -1574,6 +1683,8 @@ async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
|
|
|
1574
1683
|
truncated,
|
|
1575
1684
|
cleanFiles: clean,
|
|
1576
1685
|
unconfirmedFiles: unconfirmed,
|
|
1686
|
+
outcomes: outcomeDetails,
|
|
1687
|
+
outcomeCounts,
|
|
1577
1688
|
navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : undefined,
|
|
1578
1689
|
timedOutFiles: timedOut > 0 ? timedOut : undefined,
|
|
1579
1690
|
fileErrors: fileErrors.length > 0 ? fileErrors : undefined,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ADR 0001: stale advisory skips disposition policy
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-09-23
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The #3247 review found an asymmetry: the live turn-end arm applies disposition
|
|
10
|
+
policy, but the freshness arm that demotes dependency-drift records does not.
|
|
11
|
+
The stale arm drives delivery-count commits and bounded retirement, so changing
|
|
12
|
+
it would change the freshness gate's accounting rather than merely filter text.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Apply disposition policy on the live arm only. Keep the stale advisory visible
|
|
17
|
+
with its stale marker and its existing bounded delivery accounting.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
|
|
21
|
+
Marked findings in a stale record may remain in the bounded stale advisory.
|
|
22
|
+
This is deliberate and does not recreate the unbounded replay fixed by #3247.
|
|
23
|
+
|
|
24
|
+
## Links
|
|
25
|
+
|
|
26
|
+
- Catalog shape: `AGENTS.md` shape 10.
|
|
27
|
+
- Issue: #3246.
|
|
28
|
+
- PR: #3247, section “Known asymmetry: the stale arm does not run the policy”.
|
|
29
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ADR 0002: workspace hash is a rendezvous derivation
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-09-23
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`workspaceHash` names the warm IPC endpoint and turn-end status file. Separate
|
|
10
|
+
processes derive it at different times and must reproduce identical bytes.
|
|
11
|
+
Path-map normalizers can consult filesystem state, so they cannot be used for
|
|
12
|
+
this cross-process rendezvous.
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
Keep `workspaceHash` as pure string math. Neither the long-lived canonical path
|
|
17
|
+
key nor the process-local ephemeral path-key seam applies.
|
|
18
|
+
|
|
19
|
+
## Consequences
|
|
20
|
+
|
|
21
|
+
The derivation remains independently reproducible and avoids filesystem-state
|
|
22
|
+
staleness. Its deliberate lowercase fold can collide for case-distinct POSIX
|
|
23
|
+
directories; changing those bytes requires a separate invalidation decision.
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- Catalog shapes: `AGENTS.md` shapes 1 and 2.
|
|
28
|
+
- Issue: #3255.
|
|
29
|
+
- PR: #3256; source record: `clients/mcp/ipc.ts:44-77`.
|
|
30
|
+
|