@herbertgao/pi-extensions 2026.8.7 → 2026.8.9
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 -6
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
- package/node_modules/@czottmann/pi-automode/README.md +78 -112
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
- package/node_modules/@czottmann/pi-automode/package.json +9 -2
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +63 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +78 -16
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +13 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +30 -17
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
- package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
- package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
- package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
- package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
- package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
- package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
- package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
- package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
- package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
- package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
- package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
- package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-recap/README.md +5 -3
- package/node_modules/@herbertgao/pi-recap/package.json +9 -3
- package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
- package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
- package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
- package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
- package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
- package/node_modules/@herbertgao/pi-rename/README.md +13 -11
- package/node_modules/@herbertgao/pi-rename/package.json +13 -3
- package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
- package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
- package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
- package/node_modules/@herbertgao/pi-stash/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +4 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +154 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +7 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +3 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +137 -96
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +44 -32
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +56 -34
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +50 -4
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +90 -0
- package/node_modules/pi-lens/README.md +106 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +97 -6
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +16 -6
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +14 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +22 -1
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +8 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +10 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +19 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +35 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +8 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +110 -23
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +117 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +16 -14
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +8 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +22 -0
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +5 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +7 -2
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +27 -2
- package/node_modules/pi-lens/dist/clients/lsp/client.js +144 -5
- package/node_modules/pi-lens/dist/clients/lsp/index.js +151 -6
- package/node_modules/pi-lens/dist/clients/module-report.js +14 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +18 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +7 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +88 -19
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +44 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +41 -0
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +47 -2
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/index.js +1224 -393
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +10 -3
- package/node_modules/pi-lens/package.json +18 -5
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-web-access/CHANGELOG.md +27 -0
- package/node_modules/pi-web-access/README.md +8 -7
- package/node_modules/pi-web-access/chrome-cookies.ts +96 -20
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/extract.ts +12 -2
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +28 -10
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +43 -14
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/package.json +19 -18
|
@@ -189,16 +189,41 @@ export function recordOutstandingCascadeTouch(entry) {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
/**
|
|
193
|
-
|
|
192
|
+
/**
|
|
193
|
+
* #1910: session-boundary reset for the outstanding-touch registry and its
|
|
194
|
+
* sweep-scoped `_expiredSinceLastSweep`/`_evictedSinceLastSweep` counters.
|
|
195
|
+
* Both predate #1899 and were never wired into `handleSessionStart` — a
|
|
196
|
+
* session replacement inherited the prior session's outstanding touches, and
|
|
197
|
+
* a stray eviction/expiry landing between a sweep and the boundary attributed
|
|
198
|
+
* its count to the NEXT session's first reconcile gauge. Wired primary-only,
|
|
199
|
+
* same as every other entry in that reset block: a concurrently-live
|
|
200
|
+
* secondary never reaches `handleSessionStart` at all (the #473 guard in
|
|
201
|
+
* index.ts), so it can never race this reset against the still-live
|
|
202
|
+
* primary's outstanding touches.
|
|
203
|
+
*/
|
|
204
|
+
export function resetCascadeTierSessionState() {
|
|
194
205
|
_outstandingTouches.clear();
|
|
195
206
|
_expiredSinceLastSweep = 0;
|
|
196
207
|
_evictedSinceLastSweep = 0;
|
|
197
208
|
}
|
|
209
|
+
/** Test-only: clear the outstanding-touch registry between test cases. */
|
|
210
|
+
export function _resetOutstandingCascadeTouchesForTests() {
|
|
211
|
+
resetCascadeTierSessionState();
|
|
212
|
+
}
|
|
198
213
|
/** Test-only: peek at the registry without mutating it. */
|
|
199
214
|
export function _getOutstandingCascadeTouchesForTests() {
|
|
200
215
|
return [..._outstandingTouches.values()];
|
|
201
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Test-only: peek at the sweep-scoped expired/evicted counters without
|
|
219
|
+
* mutating them. Exists so a probe of `resetCascadeTierSessionState` can
|
|
220
|
+
* assert on THESE counters directly, rather than only on the registry map —
|
|
221
|
+
* a future counter added here and left out of the reset would otherwise
|
|
222
|
+
* stay invisible to any probe that only checks the map went empty.
|
|
223
|
+
*/
|
|
224
|
+
export function _getCascadeTierSweepCountersForTests() {
|
|
225
|
+
return { expired: _expiredSinceLastSweep, evicted: _evictedSinceLastSweep };
|
|
226
|
+
}
|
|
202
227
|
/**
|
|
203
228
|
* Reconcile every outstanding Tier-3 touch against the LSP client's current
|
|
204
229
|
* diagnostics cache. For each:
|
|
@@ -1422,6 +1422,41 @@ function setupConnectionLifecycle(state, recentStderr) {
|
|
|
1422
1422
|
});
|
|
1423
1423
|
}
|
|
1424
1424
|
});
|
|
1425
|
+
// #1969: the exit CAUSE, in the degradation ledger.
|
|
1426
|
+
//
|
|
1427
|
+
// The `exit` log above is one latency.log line per death. That line is
|
|
1428
|
+
// necessary but not sufficient: an ast-grep child died 14 times in one day
|
|
1429
|
+
// with `code=1` and empty stderr, and the only thing visible downstream was
|
|
1430
|
+
// 19 `lsp_client_skipped_broken` cooldowns and 32 coverage gaps — fallout
|
|
1431
|
+
// with no cause attached. The ledger is what a session-end health read
|
|
1432
|
+
// looks at, so the cause belongs there too, counted per server.
|
|
1433
|
+
//
|
|
1434
|
+
// `close` rather than `exit`: `close` fires only after the child's stdio
|
|
1435
|
+
// streams have drained, so "stderr was empty" is a statement about the
|
|
1436
|
+
// server rather than a race against the pipe. `exit` can arrive first with
|
|
1437
|
+
// stderr still buffered.
|
|
1438
|
+
//
|
|
1439
|
+
// Gated on `shutdownRequested` exactly as the exit log is — an eviction or
|
|
1440
|
+
// an ordinary teardown is not a degradation and must not be recorded.
|
|
1441
|
+
// `incrementDegradationCount` counts every occurrence and retains the
|
|
1442
|
+
// latest reason per server, so a server dying in a loop shows as one entry
|
|
1443
|
+
// with an honest count rather than N entries.
|
|
1444
|
+
state.lspProcess.process.on("close", (code, signal) => {
|
|
1445
|
+
if (state.shutdownRequested)
|
|
1446
|
+
return;
|
|
1447
|
+
try {
|
|
1448
|
+
const stderrTail = recentStderr(20);
|
|
1449
|
+
incrementDegradationCount({
|
|
1450
|
+
kind: "lsp-server-unexpected-close",
|
|
1451
|
+
subject: state.serverId,
|
|
1452
|
+
reason: `closed unprompted: code=${code ?? "none"} signal=${signal ?? "none"} ` +
|
|
1453
|
+
`stderr=${stderrTail.length > 0 ? `present(${stderrTail.length}b)` : "empty"} root=${state.root}`,
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
// Telemetry must never break the observed path.
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1425
1460
|
}
|
|
1426
1461
|
/** Margin between each source's own request timeout and the race's backstop
|
|
1427
1462
|
* deadline, so the natural "all sources settled" path wins over the backstop
|
|
@@ -2750,13 +2785,85 @@ function armNavLateAnswerTelemetry(args) {
|
|
|
2750
2785
|
// as an unhandled rejection.
|
|
2751
2786
|
});
|
|
2752
2787
|
}
|
|
2788
|
+
/**
|
|
2789
|
+
* #1969: pick the request the liveness probe may send to THIS server.
|
|
2790
|
+
*
|
|
2791
|
+
* The probe only needs a round-trip, so any request works — but it must be one
|
|
2792
|
+
* the server advertised. #1277 originally hardcoded `workspace/symbol` because
|
|
2793
|
+
* it needs no open document, and treated `MethodNotFound` as proof of life.
|
|
2794
|
+
* That rationale is sound for the round-trip, and wrong as a default: a server
|
|
2795
|
+
* that does not implement the method logs an error for every probe. ast-grep's
|
|
2796
|
+
* tower_lsp backend wrote "got a 'workspace/symbol' request, but it is not
|
|
2797
|
+
* implemented" on each one, and its `code=1` deaths clustered after them
|
|
2798
|
+
* (#1969). pi-lens must not keep pushing an unimplemented method at a server
|
|
2799
|
+
* just to learn it is awake.
|
|
2800
|
+
*
|
|
2801
|
+
* The ladder, best first:
|
|
2802
|
+
* 1. `workspace/symbol` when `workspaceSymbolProvider` is advertised — the
|
|
2803
|
+
* #1277 probe, unchanged, and still needs no open document.
|
|
2804
|
+
* 2. `textDocument/documentSymbol` on an already-open document when
|
|
2805
|
+
* `documentSymbolProvider` is advertised. Read-only, scoped to a document
|
|
2806
|
+
* the server already has, and cheap.
|
|
2807
|
+
* 3. `textDocument/hover` at the start of an already-open document when
|
|
2808
|
+
* `hoverProvider` is advertised. This step is what keeps ast-grep covered:
|
|
2809
|
+
* `docs/servercapabilities.md:53` records its advertised set as
|
|
2810
|
+
* codeActionProvider, executeCommandProvider, hoverProvider and
|
|
2811
|
+
* textDocumentSync — no symbol provider of either kind. #1714's own
|
|
2812
|
+
* measurement (clients/lsp/index.ts, `paceAuxNotify`) timed hover against
|
|
2813
|
+
* the real ast-grep binary at 1 ms idle and 2086 ms behind 30 `didOpen`s,
|
|
2814
|
+
* the same ordering proof it recorded for `workspace/symbol`. So the
|
|
2815
|
+
* notify throttle keeps a real barrier here, not a hollow one.
|
|
2816
|
+
* 4. Nothing. The caller then reports liveness from process and connection
|
|
2817
|
+
* state alone, and records that it did.
|
|
2818
|
+
*
|
|
2819
|
+
* `MethodNotFound`-counts-as-alive still holds for every probe: dynamic
|
|
2820
|
+
* registration can retract a capability after `initialize`, and an error reply
|
|
2821
|
+
* is still a reply. What changes is that pi-lens no longer PROVOKES that error
|
|
2822
|
+
* on every probe.
|
|
2823
|
+
*/
|
|
2824
|
+
function chooseLivenessProbe(state) {
|
|
2825
|
+
if (state.operationSupport?.workspaceSymbol === true) {
|
|
2826
|
+
return {
|
|
2827
|
+
method: "workspace/symbol",
|
|
2828
|
+
params: { query: LIVENESS_PING_QUERY },
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
const openUri = firstOpenDocumentUri(state);
|
|
2832
|
+
if (openUri === undefined)
|
|
2833
|
+
return undefined;
|
|
2834
|
+
if (state.operationSupport?.documentSymbol === true) {
|
|
2835
|
+
return {
|
|
2836
|
+
method: "textDocument/documentSymbol",
|
|
2837
|
+
params: { textDocument: { uri: openUri } },
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
if (state.operationSupport?.hover === true) {
|
|
2841
|
+
return {
|
|
2842
|
+
method: "textDocument/hover",
|
|
2843
|
+
params: {
|
|
2844
|
+
textDocument: { uri: openUri },
|
|
2845
|
+
position: { line: 0, character: 0 },
|
|
2846
|
+
},
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
return undefined;
|
|
2850
|
+
}
|
|
2851
|
+
/** URI of any document this client currently holds open, if it holds one. */
|
|
2852
|
+
function firstOpenDocumentUri(state) {
|
|
2853
|
+
for (const normalizedPath of state.openDocuments) {
|
|
2854
|
+
const uri = state.openDocumentUris?.get(normalizedPath);
|
|
2855
|
+
if (uri !== undefined)
|
|
2856
|
+
return uri;
|
|
2857
|
+
}
|
|
2858
|
+
return undefined;
|
|
2859
|
+
}
|
|
2753
2860
|
// #1277: cheap liveness round-trip used by the silent-clean gates in
|
|
2754
2861
|
// `index.ts`. `isAlive()`/`checkAlive()` only look at process/connection
|
|
2755
2862
|
// state — a server that accepted the notify write and then wedged (still
|
|
2756
2863
|
// running, connection still open, just never replying) reads as "alive" by
|
|
2757
2864
|
// those checks even though it will never answer anything again. This sends a
|
|
2758
|
-
// real request (`
|
|
2759
|
-
//
|
|
2865
|
+
// real request (see `chooseLivenessProbe` for which, #1969) and reports
|
|
2866
|
+
// whether the connection round-tripped it — success, a genuine
|
|
2760
2867
|
// protocol-level error (e.g. MethodNotFound), and a stream-destroyed/
|
|
2761
2868
|
// cancelled response (safeSendRequest swallows those to `undefined`, so the
|
|
2762
2869
|
// final `isClientAlive` re-check is what catches "died mid-flight") ALL count
|
|
@@ -2766,10 +2873,30 @@ function armNavLateAnswerTelemetry(args) {
|
|
|
2766
2873
|
async function clientPingLiveness(state, timeoutMs = LIVENESS_PING_TIMEOUT_MS) {
|
|
2767
2874
|
if (!isClientAlive(state))
|
|
2768
2875
|
return false;
|
|
2876
|
+
const probe = chooseLivenessProbe(state);
|
|
2877
|
+
if (probe === undefined) {
|
|
2878
|
+
// #1969: no advertised method to probe with, so this answer comes from
|
|
2879
|
+
// process and connection state alone — exactly the checks the probe
|
|
2880
|
+
// exists to strengthen. Say so in the ledger rather than let a weaker
|
|
2881
|
+
// verdict pass as the strong one. Rising edge per server, via the
|
|
2882
|
+
// ledger's own tally; the count carries the repeats.
|
|
2883
|
+
try {
|
|
2884
|
+
incrementDegradationCount({
|
|
2885
|
+
kind: "lsp-liveness-probe-unsupported",
|
|
2886
|
+
subject: state.serverId,
|
|
2887
|
+
reason: "no advertised request method to probe with (no " +
|
|
2888
|
+
"workspaceSymbolProvider, and no open document with " +
|
|
2889
|
+
"documentSymbolProvider or hoverProvider); liveness from " +
|
|
2890
|
+
"process and connection state only",
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
catch {
|
|
2894
|
+
// Telemetry must never break the observed path.
|
|
2895
|
+
}
|
|
2896
|
+
return isClientAlive(state);
|
|
2897
|
+
}
|
|
2769
2898
|
try {
|
|
2770
|
-
await withTimeout(safeSendRequest(state.connection,
|
|
2771
|
-
query: LIVENESS_PING_QUERY,
|
|
2772
|
-
}), timeoutMs);
|
|
2899
|
+
await withTimeout(safeSendRequest(state.connection, probe.method, probe.params), timeoutMs);
|
|
2773
2900
|
}
|
|
2774
2901
|
catch (err) {
|
|
2775
2902
|
if (err instanceof Error && err.message.startsWith("Timeout after")) {
|
|
@@ -3134,6 +3261,18 @@ export async function createLSPClient(options) {
|
|
|
3134
3261
|
}), initializeTimeoutMs);
|
|
3135
3262
|
}
|
|
3136
3263
|
catch (err) {
|
|
3264
|
+
// #1969: claim this kill BEFORE issuing it. `setupConnectionLifecycle`
|
|
3265
|
+
// ran above, so its `exit` and `close` handlers are already armed on
|
|
3266
|
+
// this child, and both read `state.shutdownRequested` to tell a crash
|
|
3267
|
+
// from a teardown we asked for. The kill below is one we asked for.
|
|
3268
|
+
// Without this line it reported as an unprompted death, fabricating a
|
|
3269
|
+
// `lsp-server-unexpected-close` entry reading
|
|
3270
|
+
// "code=1 signal=none stderr=empty" — character for character the
|
|
3271
|
+
// ast-grep signature this issue exists to make trustworthy — every time
|
|
3272
|
+
// a server merely failed to complete its handshake. It also silenced a
|
|
3273
|
+
// false `lsp_server_unexpected_exit` latency line that this path has
|
|
3274
|
+
// been writing since the #615 follow-up.
|
|
3275
|
+
state.shutdownRequested = true;
|
|
3137
3276
|
// Hard-kill the hung process so it doesn't become a zombie.
|
|
3138
3277
|
// SIGTERM alone is unreliable on Windows for cmd.exe/PowerShell trees.
|
|
3139
3278
|
const pid = lspProcess.pid;
|
|
@@ -555,9 +555,15 @@ async function collectWorkspaceDiagnosticFiles(root, maxFiles = getMaxWorkspaceD
|
|
|
555
555
|
continue;
|
|
556
556
|
await walk(full);
|
|
557
557
|
}
|
|
558
|
-
else if (
|
|
559
|
-
|
|
560
|
-
|
|
558
|
+
else if (
|
|
559
|
+
// #1974: the getServersForFileWithConfig lookup (an in-memory
|
|
560
|
+
// extension/pathFilter match against the registered LSP servers) is
|
|
561
|
+
// cheap relative to isIgnored's per-call minimatch pattern compile,
|
|
562
|
+
// so it gates first — the same order-independent shape as the four
|
|
563
|
+
// walkers fixed for #1974.
|
|
564
|
+
entry.isFile() &&
|
|
565
|
+
getServersForFileWithConfig(full).length > 0 &&
|
|
566
|
+
!ignoreMatcher.isIgnored(full, false)) {
|
|
561
567
|
files.push(full);
|
|
562
568
|
}
|
|
563
569
|
}
|
|
@@ -576,6 +582,21 @@ export class LSPService {
|
|
|
576
582
|
/** Server/root pairs that already emitted unavailable for the current occurrence. */
|
|
577
583
|
unavailableLogged = new Set();
|
|
578
584
|
optionalDisabled = new Set();
|
|
585
|
+
/**
|
|
586
|
+
* #1934 review F1: what the last COMPLETED `spawnClient` call for a
|
|
587
|
+
* (server, root) key decided, written by that call at every point it
|
|
588
|
+
* returns without a client. This is a direct signal, deliberately NOT an
|
|
589
|
+
* inference from breaker state: the "binary unavailable while installs are
|
|
590
|
+
* disabled" branch sets a cooldown yet is a POLICY decline by its own
|
|
591
|
+
* comment, so reading the cooldown mislabels it as a server failure.
|
|
592
|
+
*
|
|
593
|
+
* Not a latch, and nothing re-arms it at `session_start`. Every read sits
|
|
594
|
+
* in the same microtask as the `await` of the spawn promise that just
|
|
595
|
+
* wrote it, so a stale entry is unreachable: a read is always preceded by
|
|
596
|
+
* its own attempt's write. Cardinality matches `state.clients` — one entry
|
|
597
|
+
* per (server, root) — and a successful spawn deletes its entry.
|
|
598
|
+
*/
|
|
599
|
+
lastSpawnVerdict = new Map();
|
|
579
600
|
/** Consecutive failure counts for exponential backoff circuit breaker */
|
|
580
601
|
failureCounts = new Map();
|
|
581
602
|
/**
|
|
@@ -1589,9 +1610,20 @@ export class LSPService {
|
|
|
1589
1610
|
const withBudget = async () => {
|
|
1590
1611
|
if (servers.length === 0)
|
|
1591
1612
|
return undefined;
|
|
1613
|
+
// #1934: the first server whose acquisition ERRORED, as opposed to
|
|
1614
|
+
// cleanly declining. Kept so a selection that served nobody still
|
|
1615
|
+
// says which server the pool actually tried and failed to spawn.
|
|
1616
|
+
let erroredServerId;
|
|
1592
1617
|
// Try each matching server
|
|
1593
1618
|
for (const server of servers) {
|
|
1594
|
-
|
|
1619
|
+
// A box, not a `let`: control-flow analysis cannot see the callback
|
|
1620
|
+
// write and would narrow a plain local to its initializer.
|
|
1621
|
+
const acquisition = {
|
|
1622
|
+
outcome: "declined",
|
|
1623
|
+
};
|
|
1624
|
+
const spawned = await this.ensureClientForServer(filePath, server, resolvedRoots, noteSpawnInFlight, (reported) => {
|
|
1625
|
+
acquisition.outcome = reported;
|
|
1626
|
+
});
|
|
1595
1627
|
if (spawned) {
|
|
1596
1628
|
logLatency({
|
|
1597
1629
|
type: "phase",
|
|
@@ -1601,10 +1633,36 @@ export class LSPService {
|
|
|
1601
1633
|
metadata: {
|
|
1602
1634
|
serverId: server.id,
|
|
1603
1635
|
candidateCount: servers.length,
|
|
1636
|
+
// Emitted RAW, never coerced to a "safe" value: a served
|
|
1637
|
+
// client always reports `warm-reuse` or `cold-spawn`, so a
|
|
1638
|
+
// `declined` here would be a real reporting bug and must be
|
|
1639
|
+
// visible in the log rather than laundered into a lie.
|
|
1640
|
+
outcome: acquisition.outcome,
|
|
1604
1641
|
},
|
|
1605
1642
|
});
|
|
1606
1643
|
return spawned;
|
|
1607
1644
|
}
|
|
1645
|
+
if (acquisition.outcome === "spawn-failure") {
|
|
1646
|
+
erroredServerId ??= server.id;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (erroredServerId !== undefined) {
|
|
1650
|
+
// Same record, same denominator as the two served outcomes. Bounded
|
|
1651
|
+
// by the LSP breaker, not by a latch: a spawn failure always cools
|
|
1652
|
+
// the (server, root) key down, so the next touch takes the
|
|
1653
|
+
// `lsp_client_skipped_broken` early return and reports `declined`
|
|
1654
|
+
// instead of reaching here again until the cooldown expires.
|
|
1655
|
+
logLatency({
|
|
1656
|
+
type: "phase",
|
|
1657
|
+
phase: "lsp_client_selected",
|
|
1658
|
+
filePath,
|
|
1659
|
+
durationMs: 0,
|
|
1660
|
+
metadata: {
|
|
1661
|
+
serverId: erroredServerId,
|
|
1662
|
+
candidateCount: servers.length,
|
|
1663
|
+
outcome: "spawn-failure",
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1608
1666
|
}
|
|
1609
1667
|
const unavailable = (await Promise.all(servers.map(async (server) => {
|
|
1610
1668
|
const root = await server.root(filePath);
|
|
@@ -1753,6 +1811,10 @@ export class LSPService {
|
|
|
1753
1811
|
if (this.checkDestroyed())
|
|
1754
1812
|
return undefined;
|
|
1755
1813
|
const servers = getServersForFileWithConfig(filePath);
|
|
1814
|
+
// #1934: the (server, root) pairs that COULD have served this file and
|
|
1815
|
+
// were cold. A server with no resolvable root is not a pool miss — it
|
|
1816
|
+
// never had a slot to miss — so it stays out of this list.
|
|
1817
|
+
const missed = [];
|
|
1756
1818
|
for (const server of servers) {
|
|
1757
1819
|
const root = await this.resolveServerRoot(server, filePath);
|
|
1758
1820
|
if (!root)
|
|
@@ -1762,6 +1824,27 @@ export class LSPService {
|
|
|
1762
1824
|
if (existing?.isAlive()) {
|
|
1763
1825
|
return { client: existing, info: server };
|
|
1764
1826
|
}
|
|
1827
|
+
missed.push(key);
|
|
1828
|
+
}
|
|
1829
|
+
// #1934: an empty `missed` means "this file has no language server here",
|
|
1830
|
+
// which is the normal answer for most reads and must not be logged as a
|
|
1831
|
+
// pool miss. Callers run per file in the cascade quiet window and on the
|
|
1832
|
+
// read-expansion path, so a raw record would be a per-file log storm:
|
|
1833
|
+
// the ledger counts every miss exactly and only the FIRST per candidate
|
|
1834
|
+
// set also writes the detailed record.
|
|
1835
|
+
if (missed.length > 0) {
|
|
1836
|
+
emitBounded("lsp_warm_client_missing", missed.join(","), {
|
|
1837
|
+
filePath,
|
|
1838
|
+
durationMs: 0,
|
|
1839
|
+
metadata: {
|
|
1840
|
+
serverIds: missed.map((key) => key.slice(0, key.indexOf(":"))),
|
|
1841
|
+
roots: missed.map((key) => key.slice(key.indexOf(":") + 1)),
|
|
1842
|
+
},
|
|
1843
|
+
}, {
|
|
1844
|
+
ledgerKind: "lsp-warm-client-missing",
|
|
1845
|
+
risingEdgePer: "identity",
|
|
1846
|
+
reason: `no warm client for ${missed.join(",")}`,
|
|
1847
|
+
});
|
|
1765
1848
|
}
|
|
1766
1849
|
return undefined;
|
|
1767
1850
|
}
|
|
@@ -1972,7 +2055,22 @@ export class LSPService {
|
|
|
1972
2055
|
}
|
|
1973
2056
|
this.documentDrift.recordSynced(filePath, content, at);
|
|
1974
2057
|
}
|
|
1975
|
-
|
|
2058
|
+
/**
|
|
2059
|
+
* #1934 review F1: record what a `spawnClient` call decided, at the point
|
|
2060
|
+
* it decides it. Called on EVERY path that returns without a client, so
|
|
2061
|
+
* "no verdict" cannot silently mean "failed".
|
|
2062
|
+
*
|
|
2063
|
+
* `"failed"` is a server failure: the spawn or the initialize handshake
|
|
2064
|
+
* went wrong. `"declined"` is policy or lifecycle: host trust refused the
|
|
2065
|
+
* binary, the service shut down mid-spawn, or the binary is absent while
|
|
2066
|
+
* installs are disabled. That last one sets a breaker cooldown but is NOT
|
|
2067
|
+
* a failure, which is exactly why the outcome cannot be inferred from
|
|
2068
|
+
* breaker state.
|
|
2069
|
+
*/
|
|
2070
|
+
noteSpawnVerdict(key, verdict) {
|
|
2071
|
+
this.lastSpawnVerdict.set(key, verdict);
|
|
2072
|
+
}
|
|
2073
|
+
async ensureClientForServer(filePath, server, resolvedRoots, onSpawnInFlight, onOutcome) {
|
|
1976
2074
|
const handoff = this.generationHandoff;
|
|
1977
2075
|
if (handoff) {
|
|
1978
2076
|
await handoff;
|
|
@@ -2043,6 +2141,10 @@ export class LSPService {
|
|
|
2043
2141
|
logSessionStart(`lsp warm-start ${server.id}: reused root=${root} file=${filePath}`);
|
|
2044
2142
|
this.warmStartLogged.add(key);
|
|
2045
2143
|
}
|
|
2144
|
+
// #1934: the pool paid nothing. This is the outcome the reuse rate
|
|
2145
|
+
// is built from, so it is reported on the ONE path that returns a
|
|
2146
|
+
// client without spawning.
|
|
2147
|
+
onOutcome?.("warm-reuse");
|
|
2046
2148
|
return { client: existing, info: server };
|
|
2047
2149
|
}
|
|
2048
2150
|
// Dead client — was previously alive, now needs respawn
|
|
@@ -2269,7 +2371,24 @@ export class LSPService {
|
|
|
2269
2371
|
// which strands the settled promise and stops the server respawning.
|
|
2270
2372
|
onSpawnInFlight?.(server.id);
|
|
2271
2373
|
try {
|
|
2272
|
-
|
|
2374
|
+
const spawned = await spawnPromise;
|
|
2375
|
+
// #1934: a client here cost a process WAIT, whether this caller
|
|
2376
|
+
// started the spawn or joined another caller's in-flight promise.
|
|
2377
|
+
// Either way the selection was not served from the warm pool.
|
|
2378
|
+
//
|
|
2379
|
+
// The verdict read is synchronous and sits in the same microtask as
|
|
2380
|
+
// the await above, so it can only see the attempt just settled.
|
|
2381
|
+
onOutcome?.(spawned
|
|
2382
|
+
? "cold-spawn"
|
|
2383
|
+
: this.lastSpawnVerdict.get(key) === "failed"
|
|
2384
|
+
? "spawn-failure"
|
|
2385
|
+
: "declined");
|
|
2386
|
+
return spawned;
|
|
2387
|
+
}
|
|
2388
|
+
catch (err) {
|
|
2389
|
+
// A throwing spawn promise is an errored acquisition by definition.
|
|
2390
|
+
onOutcome?.("spawn-failure");
|
|
2391
|
+
throw err;
|
|
2273
2392
|
}
|
|
2274
2393
|
finally {
|
|
2275
2394
|
if (this.state.inFlight.get(key) === spawnPromise) {
|
|
@@ -2293,6 +2412,7 @@ export class LSPService {
|
|
|
2293
2412
|
// failure, and the user may grant trust later in the same session.
|
|
2294
2413
|
if (!isLspSpawnAllowedByTrust()) {
|
|
2295
2414
|
logSessionStart(`lsp spawn ${server.id}: refused — ${projectTrustDenialReason()}`);
|
|
2415
|
+
this.noteSpawnVerdict(key, "declined");
|
|
2296
2416
|
return undefined;
|
|
2297
2417
|
}
|
|
2298
2418
|
const isOptionalServer = OPTIONAL_LSP_SERVER_IDS.has(server.id); // NOSONAR: set intentionally empty — no optional servers configured yet
|
|
@@ -2312,6 +2432,7 @@ export class LSPService {
|
|
|
2312
2432
|
// pi-lens-ignore: missing-error-propagation — best-effort kill on aborted spawn
|
|
2313
2433
|
}
|
|
2314
2434
|
logSessionStart(`lsp spawn ${server.id}: aborted (service shut down mid-spawn)`);
|
|
2435
|
+
this.noteSpawnVerdict(key, "declined");
|
|
2315
2436
|
return undefined;
|
|
2316
2437
|
}
|
|
2317
2438
|
if (!spawned) {
|
|
@@ -2325,6 +2446,17 @@ export class LSPService {
|
|
|
2325
2446
|
if (!allowInstall) {
|
|
2326
2447
|
logSessionStart(`lsp spawn ${server.id}: unavailable with install disabled; temporary cooldown only`);
|
|
2327
2448
|
this.state.broken.set(key, Date.now() + BROKEN_BASE_COOLDOWN_MS);
|
|
2449
|
+
// #1934 review F1: this branch sets a cooldown but is a POLICY
|
|
2450
|
+
// decline by the comment above — the binary may appear on PATH
|
|
2451
|
+
// later in the same session, so it never counts toward permanent
|
|
2452
|
+
// disablement and the cooldown has no ladder. With installs
|
|
2453
|
+
// disabled or a project untrusted, a missing binary reaches here
|
|
2454
|
+
// once per 15s per (server, root) for the whole session. Calling
|
|
2455
|
+
// that a spawn failure would write thousands of mislabeled
|
|
2456
|
+
// records a day, so it reads as a decline. The event is already
|
|
2457
|
+
// recorded once by `lsp_client_unavailable` and by the
|
|
2458
|
+
// `sessionstart.log` line above.
|
|
2459
|
+
this.noteSpawnVerdict(key, "declined");
|
|
2328
2460
|
return undefined;
|
|
2329
2461
|
}
|
|
2330
2462
|
const uCount = (this.failureCounts.get(key) ?? 0) + 1;
|
|
@@ -2336,6 +2468,9 @@ export class LSPService {
|
|
|
2336
2468
|
this.recordBreaker(key, `permanently disabled after ${uCount} unavailable spawns`);
|
|
2337
2469
|
logSessionStart(`lsp spawn ${server.id}: permanently disabled after ${uCount} failures`);
|
|
2338
2470
|
}
|
|
2471
|
+
// Installs were allowed and the server is still unavailable: a real
|
|
2472
|
+
// failure, on the exponential ladder toward permanent disablement.
|
|
2473
|
+
this.noteSpawnVerdict(key, "failed");
|
|
2339
2474
|
return undefined;
|
|
2340
2475
|
}
|
|
2341
2476
|
const override = getServerInitOverride(server.id, filePath);
|
|
@@ -2353,6 +2488,7 @@ export class LSPService {
|
|
|
2353
2488
|
if (this.isDestroyed) {
|
|
2354
2489
|
client.shutdown({ fast: true }).catch(() => { });
|
|
2355
2490
|
logSessionStart(`lsp spawn ${server.id}: aborted (service shut down mid-initialize)`);
|
|
2491
|
+
this.noteSpawnVerdict(key, "declined");
|
|
2356
2492
|
return undefined;
|
|
2357
2493
|
}
|
|
2358
2494
|
const wsDiag = typeof client.getWorkspaceDiagnosticsSupport === "function"
|
|
@@ -2364,6 +2500,9 @@ export class LSPService {
|
|
|
2364
2500
|
};
|
|
2365
2501
|
this.state.clients.set(key, client);
|
|
2366
2502
|
this.unavailableLogged.delete(key);
|
|
2503
|
+
// #1934 review F1: a success retires the previous verdict, so the map
|
|
2504
|
+
// never outlives the attempts it describes.
|
|
2505
|
+
this.lastSpawnVerdict.delete(key);
|
|
2367
2506
|
this.state.clientSpawnedAt.set(key, Date.now());
|
|
2368
2507
|
this.clientLastUsedAt.set(key, Date.now());
|
|
2369
2508
|
this.scheduleTypeScriptIdleEviction(key);
|
|
@@ -2404,6 +2543,8 @@ export class LSPService {
|
|
|
2404
2543
|
if (isOptionalServer) {
|
|
2405
2544
|
this.optionalDisabled.add(key);
|
|
2406
2545
|
}
|
|
2546
|
+
// The spawn or the initialize handshake threw: a server failure.
|
|
2547
|
+
this.noteSpawnVerdict(key, "failed");
|
|
2407
2548
|
return undefined;
|
|
2408
2549
|
}
|
|
2409
2550
|
}
|
|
@@ -6479,6 +6620,10 @@ export class LSPService {
|
|
|
6479
6620
|
});
|
|
6480
6621
|
this.state.clients.clear();
|
|
6481
6622
|
this.state.broken.clear();
|
|
6623
|
+
// #1934 review F1: map hygiene alongside the breaker it sits next to.
|
|
6624
|
+
// Not load-bearing — every read follows its own attempt's write — but a
|
|
6625
|
+
// verdict for a client generation that no longer exists is dead weight.
|
|
6626
|
+
this.lastSpawnVerdict.clear();
|
|
6482
6627
|
// #1459: every gated client is gone, so no outstanding-write record can
|
|
6483
6628
|
// describe a live one. The gate's identity check already neutralises a stale
|
|
6484
6629
|
// entry; clearing keeps the map honest rather than relying on that.
|
|
@@ -1338,10 +1338,19 @@ export async function moduleReport(file, cwd, options) {
|
|
|
1338
1338
|
let graph;
|
|
1339
1339
|
let graphFileCap;
|
|
1340
1340
|
let callGraphIdentity;
|
|
1341
|
+
// #1961 review F4: the blind read now serves a snapshot stamped at a
|
|
1342
|
+
// different commit. project_report says so in its trust notes; this report
|
|
1343
|
+
// reads through the SAME accessor, so without this it would present the
|
|
1344
|
+
// other branch's importers and call edges as current, under a builtAt that
|
|
1345
|
+
// looks fresh. Computed per call, never cached.
|
|
1346
|
+
let revisionDriftNote;
|
|
1341
1347
|
try {
|
|
1342
|
-
const { getCachedReviewGraph, getReviewGraphCacheIdentity, getReviewGraphSizeSkipVerdict, } = await import("./review-graph/builder.js");
|
|
1348
|
+
const { getCachedReviewGraph, getReviewGraphCacheIdentity, getReviewGraphSizeSkipVerdict, getReviewGraphRevisionDrift, formatReviewGraphRevisionDriftNote, } = await import("./review-graph/builder.js");
|
|
1343
1349
|
graph = getCachedReviewGraph(cwd);
|
|
1344
1350
|
graphFileCap = getReviewGraphSizeSkipVerdict(cwd)?.maxFileCount;
|
|
1351
|
+
const drift = graph ? getReviewGraphRevisionDrift(cwd) : undefined;
|
|
1352
|
+
if (drift)
|
|
1353
|
+
revisionDriftNote = formatReviewGraphRevisionDriftNote(drift);
|
|
1345
1354
|
callGraphIdentity = graph
|
|
1346
1355
|
? (() => {
|
|
1347
1356
|
const identity = getReviewGraphCacheIdentity(cwd, graph);
|
|
@@ -1374,6 +1383,10 @@ export async function moduleReport(file, cwd, options) {
|
|
|
1374
1383
|
const api = topLevel.filter((entry) => entry.exported);
|
|
1375
1384
|
const internal = topLevel.filter((entry) => !entry.exported);
|
|
1376
1385
|
const warnings = [...(extractionWarnings ?? [])];
|
|
1386
|
+
// Every graph-derived section below (who-uses-this, blast radius, call
|
|
1387
|
+
// edges) came from the served snapshot, so the caveat belongs with them.
|
|
1388
|
+
if (revisionDriftNote)
|
|
1389
|
+
warnings.push(revisionDriftNote);
|
|
1377
1390
|
if (callbackError) {
|
|
1378
1391
|
warnings.push(`Failed to extract callbacks: ${callbackError}`);
|
|
1379
1392
|
logLatency({
|