@mrclrchtr/supi-code-intelligence 4.10.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +111 -5
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +22 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +245 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +171 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +413 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +690 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +97 -16
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +448 -298
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +70 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +231 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +529 -119
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +12 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +42 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +17 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +350 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +586 -96
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +42 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +92 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +165 -118
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -6
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +221 -77
- package/src/analysis/health/file-scope.ts +184 -0
- package/src/analysis/health/recovery.ts +14 -3
- package/src/analysis/provider.ts +58 -0
- package/src/analysis/readiness.ts +34 -9
- package/src/analysis/refactor/apply.ts +45 -1
- package/src/analysis/refactor/mutation-authority.ts +143 -0
- package/src/analysis/relations/callees.ts +9 -1
- package/src/analysis/relations/types.ts +5 -2
- package/src/analysis/search/ast-scan-timing.ts +9 -2
- package/src/analysis/search/ast-scan.ts +5 -2
- package/src/analysis/search/deadline.ts +2 -2
- package/src/analysis/search/pattern-analysis.ts +54 -33
- package/src/analysis/search/pattern.ts +18 -3
- package/src/analysis/target/anchored.ts +20 -11
- package/src/analysis/target/file.ts +18 -11
- package/src/analysis/target/identity.ts +19 -3
- package/src/analysis/target/symbol.ts +36 -15
- package/src/api.ts +4 -0
- package/src/config.ts +35 -1
- package/src/diagnostics/evidence.ts +51 -0
- package/src/extension.ts +57 -14
- package/src/overview/overview-data.ts +9 -2
- package/src/overview/overview.ts +49 -30
- package/src/overview/types.ts +2 -0
- package/src/session/capability-adapter.ts +12 -3
- package/src/session/find-types.ts +2 -2
- package/src/session/find-workflow.ts +20 -9
- package/src/session/graph/collect.ts +18 -2
- package/src/session/graph-workflow.ts +19 -12
- package/src/session/health-refresh.ts +175 -0
- package/src/session/health-types.ts +43 -15
- package/src/session/health-workflow.ts +69 -54
- package/src/session/input/workflows.ts +2 -2
- package/src/session/inspect/collect.ts +20 -3
- package/src/session/inspect-workflow.ts +12 -6
- package/src/session/orientation/collect.ts +12 -3
- package/src/session/orientation/context-facts.ts +3 -1
- package/src/session/orientation/context-sections.ts +20 -7
- package/src/session/orientation/gather.ts +17 -11
- package/src/session/orientation-types.ts +4 -1
- package/src/session/orientation-workflow.ts +23 -10
- package/src/session/refactor-plans.ts +2 -0
- package/src/session/refactor-workflow.ts +61 -15
- package/src/session/session.ts +29 -2
- package/src/session/target-workflow.ts +54 -23
- package/src/session/workflow-control.ts +7 -3
- package/src/substrate/lsp/lifecycle.ts +26 -46
- package/src/substrate/lsp/maintenance.ts +284 -28
- package/src/substrate/lsp/recovery.ts +11 -5
- package/src/substrate/lsp/state.ts +2 -2
- package/src/substrate/workspace-provider-host.ts +25 -3
- package/src/tool/{find → code_find}/execute.ts +2 -16
- package/src/tool/code_find/guidance.ts +8 -0
- package/src/tool/{find → code_find}/render.ts +21 -4
- package/src/tool/{result/find.ts → code_find/result.ts} +78 -9
- package/src/tool/code_find/spec.ts +37 -0
- package/src/tool/code_find/tui.ts +41 -0
- package/src/tool/code_graph/execute.ts +24 -0
- package/src/tool/code_graph/guidance.ts +6 -0
- package/src/tool/{graph → code_graph}/markdown.ts +1 -1
- package/src/tool/{result/graph.ts → code_graph/result.ts} +111 -3
- package/src/tool/code_graph/spec.ts +39 -0
- package/src/tool/code_graph/tui.ts +117 -0
- package/src/tool/{health → code_health}/execute.ts +2 -13
- package/src/tool/code_health/guidance.ts +8 -0
- package/src/tool/{health → code_health}/markdown.ts +119 -27
- package/src/tool/code_health/refresh-status.ts +105 -0
- package/src/tool/{result/health.ts → code_health/result.ts} +80 -3
- package/src/tool/code_health/spec.ts +41 -0
- package/src/tool/{health → code_health}/tui.ts +95 -98
- package/src/tool/{inspect → code_inspect}/execute.ts +2 -16
- package/src/tool/code_inspect/guidance.ts +6 -0
- package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
- package/src/tool/{result/inspect.ts → code_inspect/result.ts} +143 -2
- package/src/tool/code_inspect/spec.ts +25 -0
- package/src/tool/code_inspect/tui.ts +44 -0
- package/src/tool/code_orientation/execute.ts +25 -0
- package/src/tool/code_orientation/guidance.ts +9 -0
- package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
- package/src/tool/code_orientation/result.ts +248 -0
- package/src/tool/code_orientation/spec.ts +25 -0
- package/src/tool/{orientation → code_orientation}/tui.ts +51 -35
- package/src/tool/code_refactor_apply/execute.ts +17 -0
- package/src/tool/code_refactor_apply/guidance.ts +5 -0
- package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +8 -11
- package/src/tool/code_refactor_apply/spec.ts +26 -0
- package/src/tool/code_refactor_apply/tui.ts +73 -0
- package/src/tool/code_refactor_plan/execute.ts +27 -0
- package/src/tool/code_refactor_plan/guidance.ts +8 -0
- package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +13 -22
- package/src/tool/code_refactor_plan/spec.ts +25 -0
- package/src/tool/code_refactor_plan/tui.ts +54 -0
- package/src/tool/code_resolve/execute.ts +19 -0
- package/src/tool/code_resolve/guidance.ts +8 -0
- package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
- package/src/tool/{result/resolve.ts → code_resolve/result.ts} +79 -29
- package/src/tool/code_resolve/spec.ts +25 -0
- package/src/tool/code_resolve/tui.ts +60 -0
- package/src/tool/guidance.ts +71 -52
- package/src/tool/infra/truncate.ts +40 -0
- package/src/tool/infra/workflow-control.ts +8 -0
- package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
- package/src/tool/register.ts +105 -88
- package/src/tool/result/display.ts +107 -0
- package/src/tool/result/errors.ts +20 -17
- package/src/tool/result/refactor.ts +37 -8
- package/src/tool/result/types.ts +20 -0
- package/src/tool/schemas.ts +26 -157
- package/src/tool/specs.ts +40 -126
- package/src/types/execution.ts +18 -5
- package/src/types/index.ts +8 -1
- package/src/ui/status-command.ts +5 -3
- package/src/ui/status-overlay.ts +4 -0
- package/src/ui/tui/common.ts +108 -10
- package/src/ui/tui/display.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
- package/src/tool/find/tui.ts +0 -39
- package/src/tool/graph/execute.ts +0 -62
- package/src/tool/graph/tui.ts +0 -104
- package/src/tool/inspect/tui.ts +0 -36
- package/src/tool/orientation/execute.ts +0 -62
- package/src/tool/refactor-apply/tui.ts +0 -55
- package/src/tool/refactor-plan/tui.ts +0 -41
- package/src/tool/resolve/execute.ts +0 -36
- package/src/tool/resolve/tui.ts +0 -42
- package/src/tool/result/orientation.ts +0 -132
- /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
- /package/src/tool/{find → code_find}/markdown.ts +0 -0
- /package/src/tool/{find → code_find}/modes.ts +0 -0
- /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CodeQueryResult,
|
|
3
|
+
partialCodeQuery,
|
|
4
|
+
unavailableCodeQuery,
|
|
5
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
6
|
+
import type {
|
|
7
|
+
Diagnostic,
|
|
8
|
+
DocumentDiagnosticReport,
|
|
9
|
+
PublishDiagnosticsParams,
|
|
10
|
+
} from "../config/types.ts";
|
|
11
|
+
import type { DiagnosticStateWait } from "./client-diagnostic-waiters.ts";
|
|
12
|
+
|
|
13
|
+
/** Validate one untrusted LSP diagnostic publication before it enters the cache. */
|
|
14
|
+
export function isValidPublishDiagnosticsParams(value: unknown): value is PublishDiagnosticsParams {
|
|
15
|
+
if (!isRecord(value) || !isValidUri(value.uri)) return false;
|
|
16
|
+
if (value.version !== undefined && !isInteger(value.version)) return false;
|
|
17
|
+
return Array.isArray(value.diagnostics) && value.diagnostics.every(isValidDiagnostic);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Validate one untrusted LSP pull report before it enters the cache. */
|
|
21
|
+
export function isValidDocumentDiagnosticReport(
|
|
22
|
+
value: unknown,
|
|
23
|
+
allowRelatedDocuments = true,
|
|
24
|
+
): value is DocumentDiagnosticReport {
|
|
25
|
+
if (!isRecord(value)) return false;
|
|
26
|
+
// gopls v0.23.0 ships a non-conforming pull report with an empty `kind`
|
|
27
|
+
// discriminator and no resultId (upstream work in progress). Treat a
|
|
28
|
+
// missing/empty kind with an items array as a full report; every other
|
|
29
|
+
// kind value stays strict.
|
|
30
|
+
if (value.kind === "full" || value.kind === undefined || value.kind === "") {
|
|
31
|
+
if (!Array.isArray(value.items) || !value.items.every(isValidDiagnostic)) return false;
|
|
32
|
+
if (value.resultId !== undefined && typeof value.resultId !== "string") return false;
|
|
33
|
+
} else if (value.kind === "unchanged") {
|
|
34
|
+
if (typeof value.resultId !== "string") return false;
|
|
35
|
+
} else {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return allowRelatedDocuments
|
|
39
|
+
? isValidRelatedDocuments(value.relatedDocuments)
|
|
40
|
+
: value.relatedDocuments === undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isValidRelatedDocuments(value: unknown): boolean {
|
|
44
|
+
if (value === undefined) return true;
|
|
45
|
+
if (!isRecord(value)) return false;
|
|
46
|
+
return Object.entries(value).every(
|
|
47
|
+
([uri, report]) => isValidUri(uri) && isValidDocumentDiagnosticReport(report, false),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isValidDiagnostic(value: unknown): value is Diagnostic {
|
|
52
|
+
if (!isRecord(value) || !isValidRange(value.range) || !isValidDiagnosticMessage(value.message)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
isValidOptionalSeverity(value.severity) &&
|
|
57
|
+
isValidOptionalCode(value.code) &&
|
|
58
|
+
isValidOptionalCodeDescription(value.codeDescription, value.code) &&
|
|
59
|
+
isValidOptionalSource(value.source) &&
|
|
60
|
+
isValidOptionalTags(value.tags) &&
|
|
61
|
+
isValidOptionalRelatedInformation(value.relatedInformation)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isValidOptionalSeverity(value: unknown): boolean {
|
|
66
|
+
return value === undefined || isDiagnosticSeverity(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isValidOptionalCode(value: unknown): boolean {
|
|
70
|
+
return value === undefined || isInteger(value) || typeof value === "string";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isValidOptionalCodeDescription(value: unknown, code: unknown): boolean {
|
|
74
|
+
return value === undefined || (code !== undefined && isRecord(value) && isValidUri(value.href));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isValidOptionalSource(value: unknown): boolean {
|
|
78
|
+
return value === undefined || typeof value === "string";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isValidOptionalTags(value: unknown): boolean {
|
|
82
|
+
return (
|
|
83
|
+
value === undefined || (Array.isArray(value) && value.every((tag) => tag === 1 || tag === 2))
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isValidOptionalRelatedInformation(value: unknown): boolean {
|
|
88
|
+
return (
|
|
89
|
+
value === undefined ||
|
|
90
|
+
(Array.isArray(value) && value.every(isValidDiagnosticRelatedInformation))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isValidDiagnosticMessage(value: unknown): boolean {
|
|
95
|
+
if (typeof value === "string") return true;
|
|
96
|
+
return (
|
|
97
|
+
isRecord(value) &&
|
|
98
|
+
(value.kind === "plaintext" || value.kind === "markdown") &&
|
|
99
|
+
typeof value.value === "string"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isValidDiagnosticRelatedInformation(value: unknown): boolean {
|
|
104
|
+
return isRecord(value) && isValidLocation(value.location) && typeof value.message === "string";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isValidLocation(value: unknown): boolean {
|
|
108
|
+
return isRecord(value) && isValidUri(value.uri) && isValidRange(value.range);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isValidRange(value: unknown): value is {
|
|
112
|
+
start: { line: number; character: number };
|
|
113
|
+
end: { line: number; character: number };
|
|
114
|
+
} {
|
|
115
|
+
if (!isRecord(value) || !isValidPosition(value.start) || !isValidPosition(value.end)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
value.start.line < value.end.line ||
|
|
120
|
+
(value.start.line === value.end.line && value.start.character <= value.end.character)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isValidPosition(value: unknown): value is { line: number; character: number } {
|
|
125
|
+
return (
|
|
126
|
+
isRecord(value) &&
|
|
127
|
+
isInteger(value.line) &&
|
|
128
|
+
value.line >= 0 &&
|
|
129
|
+
value.line <= 2_147_483_647 &&
|
|
130
|
+
isInteger(value.character) &&
|
|
131
|
+
value.character >= 0 &&
|
|
132
|
+
value.character <= 2_147_483_647
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isValidUri(value: unknown): value is string {
|
|
137
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
138
|
+
try {
|
|
139
|
+
return new URL(value).protocol.length > 0;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isDiagnosticSeverity(value: unknown): boolean {
|
|
146
|
+
return value === 1 || value === 2 || value === 3 || value === 4;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isInteger(value: unknown): value is number {
|
|
150
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Stored diagnostic data and the protocol evidence that established it. */
|
|
158
|
+
export interface DiagnosticCacheEntry {
|
|
159
|
+
diagnostics: Diagnostic[];
|
|
160
|
+
receivedAt: number;
|
|
161
|
+
source: "pull" | "push";
|
|
162
|
+
synchronizationId?: number;
|
|
163
|
+
evidenceRevision?: number;
|
|
164
|
+
version?: number;
|
|
165
|
+
resultId?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Valid publications observed for the entry's synchronization. The first
|
|
168
|
+
* valid publication for a synchronization is tentative; a later valid
|
|
169
|
+
* publication promotes the entry to confirmed (ADR 0021). A confirmed
|
|
170
|
+
* pull entry counts as the first publication when a push continues the
|
|
171
|
+
* same synchronization, so that push keeps the confirmation. Pull
|
|
172
|
+
* entries as such carry no count and are confirmed on arrival.
|
|
173
|
+
*/
|
|
174
|
+
publications?: number;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Test whether one push entry is still tentative for its synchronization.
|
|
179
|
+
*
|
|
180
|
+
* A pull entry confirms on arrival; a push entry confirms only after a
|
|
181
|
+
* later valid publication for the same synchronization (ADR 0021).
|
|
182
|
+
*/
|
|
183
|
+
export function isTentativePushEntry(entry: DiagnosticCacheEntry | undefined): boolean {
|
|
184
|
+
return Boolean(entry && entry.source === "push" && (entry.publications ?? 1) < 2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One document synchronization that needs current diagnostic evidence. */
|
|
188
|
+
export interface DiagnosticSynchronization {
|
|
189
|
+
readonly uri: string;
|
|
190
|
+
readonly synchronizationId: number;
|
|
191
|
+
readonly evidenceRevision?: number;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Monotonic document version state retained across close and reopen. */
|
|
195
|
+
export type DocumentVersionHistory = Map<string, number>;
|
|
196
|
+
|
|
197
|
+
/** Allocate the next document version for one URI. */
|
|
198
|
+
export function nextDocumentVersion(history: DocumentVersionHistory, uri: string): number {
|
|
199
|
+
const version = (history.get(uri) ?? 0) + 1;
|
|
200
|
+
history.set(uri, version);
|
|
201
|
+
return version;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Test whether a synchronization still names the open document state. */
|
|
205
|
+
export function isCurrentSynchronization(
|
|
206
|
+
openDocs: ReadonlyMap<string, { synchronizationId: number; evidenceRevision?: number }>,
|
|
207
|
+
synchronization: DiagnosticSynchronization,
|
|
208
|
+
): boolean {
|
|
209
|
+
const document = openDocs.get(synchronization.uri);
|
|
210
|
+
return Boolean(
|
|
211
|
+
document?.synchronizationId === synchronization.synchronizationId &&
|
|
212
|
+
(synchronization.evidenceRevision === undefined ||
|
|
213
|
+
document.evidenceRevision === synchronization.evidenceRevision),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Test whether a fresh push confirms the supplied synchronization. */
|
|
218
|
+
export function hasFreshPush(
|
|
219
|
+
store: ReadonlyMap<string, DiagnosticCacheEntry>,
|
|
220
|
+
synchronization: DiagnosticSynchronization,
|
|
221
|
+
currentEvidenceRevision?: number,
|
|
222
|
+
): boolean {
|
|
223
|
+
const entry = store.get(synchronization.uri);
|
|
224
|
+
return Boolean(
|
|
225
|
+
entry?.source === "push" &&
|
|
226
|
+
!isTentativePushEntry(entry) &&
|
|
227
|
+
entry.synchronizationId === synchronization.synchronizationId &&
|
|
228
|
+
(synchronization.evidenceRevision === undefined ||
|
|
229
|
+
entry.evidenceRevision === synchronization.evidenceRevision) &&
|
|
230
|
+
(currentEvidenceRevision === undefined || entry.evidenceRevision === currentEvidenceRevision),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Test whether stored evidence matches a synchronization without a
|
|
236
|
+
* confirmation gate. A tentative push matches here but cannot confirm.
|
|
237
|
+
*/
|
|
238
|
+
export function hasCurrentEvidence(
|
|
239
|
+
store: ReadonlyMap<string, DiagnosticCacheEntry>,
|
|
240
|
+
synchronization: DiagnosticSynchronization,
|
|
241
|
+
currentEvidenceRevision?: number,
|
|
242
|
+
): boolean {
|
|
243
|
+
const entry = store.get(synchronization.uri);
|
|
244
|
+
return Boolean(
|
|
245
|
+
entry &&
|
|
246
|
+
entry.synchronizationId === synchronization.synchronizationId &&
|
|
247
|
+
(synchronization.evidenceRevision === undefined ||
|
|
248
|
+
entry.evidenceRevision === synchronization.evidenceRevision) &&
|
|
249
|
+
(currentEvidenceRevision === undefined || entry.evidenceRevision === currentEvidenceRevision),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Test whether pull or push evidence confirms the supplied synchronization. */
|
|
254
|
+
export function hasFreshEvidence(
|
|
255
|
+
store: ReadonlyMap<string, DiagnosticCacheEntry>,
|
|
256
|
+
synchronization: DiagnosticSynchronization,
|
|
257
|
+
currentEvidenceRevision?: number,
|
|
258
|
+
): boolean {
|
|
259
|
+
return (
|
|
260
|
+
hasCurrentEvidence(store, synchronization, currentEvidenceRevision) &&
|
|
261
|
+
!isTentativePushEntry(store.get(synchronization.uri))
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Return the latest cache update that matches one of the synchronizations.
|
|
267
|
+
*
|
|
268
|
+
* Every accepted publication restarts the settle quiet period, tentative
|
|
269
|
+
* publications included, so this helper does not apply the confirmation
|
|
270
|
+
* gate (ADR 0021).
|
|
271
|
+
*/
|
|
272
|
+
export function latestCurrentEvidenceReceivedAt(
|
|
273
|
+
store: ReadonlyMap<string, DiagnosticCacheEntry>,
|
|
274
|
+
synchronizations: DiagnosticSynchronization[],
|
|
275
|
+
currentEvidenceRevision?: number,
|
|
276
|
+
): number {
|
|
277
|
+
let latest = 0;
|
|
278
|
+
for (const synchronization of synchronizations) {
|
|
279
|
+
const entry = store.get(synchronization.uri);
|
|
280
|
+
if (hasCurrentEvidence(store, synchronization, currentEvidenceRevision)) {
|
|
281
|
+
latest = Math.max(latest, entry?.receivedAt ?? 0);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return latest;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
interface PullRaceOptions {
|
|
288
|
+
readonly pull: Promise<boolean>;
|
|
289
|
+
readonly waitForChange: () => DiagnosticStateWait;
|
|
290
|
+
readonly freshPush: () => boolean;
|
|
291
|
+
readonly current: () => boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Race a pull against fresh push evidence and lifecycle release. */
|
|
295
|
+
export async function raceDiagnosticPull(
|
|
296
|
+
options: PullRaceOptions,
|
|
297
|
+
): Promise<"pull" | "push" | "released"> {
|
|
298
|
+
const pull = options.pull.then((confirmed) =>
|
|
299
|
+
confirmed ? ("pull" as const) : ("released" as const),
|
|
300
|
+
);
|
|
301
|
+
while (options.current()) {
|
|
302
|
+
if (options.freshPush()) return "push";
|
|
303
|
+
const change = options.waitForChange();
|
|
304
|
+
let outcome: "changed" | "pull" | "released";
|
|
305
|
+
try {
|
|
306
|
+
outcome = await Promise.race([pull, change.promise.then(() => "changed" as const)]);
|
|
307
|
+
} finally {
|
|
308
|
+
change.cancel();
|
|
309
|
+
}
|
|
310
|
+
if (outcome !== "changed") return outcome;
|
|
311
|
+
}
|
|
312
|
+
return "released";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Return explicit partial or unavailable evidence after fresh collection fails. */
|
|
316
|
+
export function incompleteDiagnosticResult(
|
|
317
|
+
cachedDiagnostics: Diagnostic[] | null,
|
|
318
|
+
outcome: "released" | "timed-out",
|
|
319
|
+
): CodeQueryResult<Diagnostic[]> {
|
|
320
|
+
if (outcome === "released") {
|
|
321
|
+
return unavailableCodeQuery(
|
|
322
|
+
"Diagnostic collection ended before the current document synchronization was confirmed.",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (cachedDiagnostics) {
|
|
326
|
+
return partialCodeQuery(
|
|
327
|
+
cachedDiagnostics,
|
|
328
|
+
"Fresh diagnostics were not confirmed for the current document synchronization; cached diagnostics are partial evidence.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return unavailableCodeQuery(
|
|
332
|
+
"Fresh diagnostics were not confirmed for the current document synchronization.",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface ApplyPullReportOptions {
|
|
337
|
+
readonly store: Map<string, DiagnosticCacheEntry>;
|
|
338
|
+
readonly uri: string;
|
|
339
|
+
readonly report: DocumentDiagnosticReport;
|
|
340
|
+
readonly previous: DiagnosticCacheEntry | undefined;
|
|
341
|
+
readonly previousResultId: string | undefined;
|
|
342
|
+
readonly synchronizationId: number | undefined;
|
|
343
|
+
readonly evidenceRevision: number;
|
|
344
|
+
readonly isRelatedUriTracked: (uri: string) => boolean;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Apply one valid full or linked unchanged pull report. */
|
|
348
|
+
export function applyPullReport(options: ApplyPullReportOptions): boolean {
|
|
349
|
+
if (!isValidDocumentDiagnosticReport(options.report)) return false;
|
|
350
|
+
const {
|
|
351
|
+
store,
|
|
352
|
+
uri,
|
|
353
|
+
report,
|
|
354
|
+
previous,
|
|
355
|
+
previousResultId,
|
|
356
|
+
synchronizationId,
|
|
357
|
+
evidenceRevision,
|
|
358
|
+
isRelatedUriTracked,
|
|
359
|
+
} = options;
|
|
360
|
+
// A report validated as full (kind "full", "", or absent — gopls v0.23.0)
|
|
361
|
+
// stores its items; only an explicit "unchanged" reuses the previous entry.
|
|
362
|
+
if (report.kind !== "unchanged") {
|
|
363
|
+
if (!Array.isArray(report.items)) return false;
|
|
364
|
+
if (report.resultId !== undefined && typeof report.resultId !== "string") return false;
|
|
365
|
+
store.set(uri, {
|
|
366
|
+
diagnostics: report.items,
|
|
367
|
+
receivedAt: Date.now(),
|
|
368
|
+
source: "pull",
|
|
369
|
+
synchronizationId,
|
|
370
|
+
evidenceRevision,
|
|
371
|
+
resultId: report.resultId,
|
|
372
|
+
});
|
|
373
|
+
applyRelatedPullReports(store, report, evidenceRevision, isRelatedUriTracked);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
if (!previous || previousResultId === undefined || typeof report.resultId !== "string") {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
store.set(uri, {
|
|
380
|
+
...previous,
|
|
381
|
+
receivedAt: Date.now(),
|
|
382
|
+
source: "pull",
|
|
383
|
+
synchronizationId,
|
|
384
|
+
evidenceRevision,
|
|
385
|
+
resultId: report.resultId,
|
|
386
|
+
});
|
|
387
|
+
applyRelatedPullReports(store, report, evidenceRevision, isRelatedUriTracked);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function applyRelatedPullReports(
|
|
392
|
+
store: Map<string, DiagnosticCacheEntry>,
|
|
393
|
+
report: DocumentDiagnosticReport,
|
|
394
|
+
evidenceRevision: number,
|
|
395
|
+
isRelatedUriTracked: (uri: string) => boolean,
|
|
396
|
+
): void {
|
|
397
|
+
for (const [relatedUri, relatedReport] of Object.entries(report.relatedDocuments ?? {})) {
|
|
398
|
+
if (!isValidDocumentDiagnosticReport(relatedReport, false)) continue;
|
|
399
|
+
// Skip explicit unchanged reports; full reports (kind "full", "", or
|
|
400
|
+
// absent) may enter the related-document store.
|
|
401
|
+
if (relatedReport.kind === "unchanged") continue;
|
|
402
|
+
if (isRelatedUriTracked(relatedUri)) continue;
|
|
403
|
+
const existing = store.get(relatedUri);
|
|
404
|
+
if (existing?.evidenceRevision === evidenceRevision) continue;
|
|
405
|
+
store.set(relatedUri, {
|
|
406
|
+
diagnostics: relatedReport.items,
|
|
407
|
+
receivedAt: Date.now(),
|
|
408
|
+
source: "pull",
|
|
409
|
+
evidenceRevision,
|
|
410
|
+
resultId: relatedReport.resultId,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DocumentDiagnosticReport } from "../config/types.ts";
|
|
2
|
+
import type { DiagnosticPullRequest } from "./client-diagnostic-request.ts";
|
|
3
|
+
|
|
4
|
+
/** Transport and capability operations required by diagnostic state. */
|
|
5
|
+
export interface ClientDiagnosticsHost {
|
|
6
|
+
/** Configured server name, for debug-telemetry identity. */
|
|
7
|
+
server: string;
|
|
8
|
+
/** Absolute workspace root, for debug-telemetry identity. */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
isOperational(): boolean;
|
|
11
|
+
supportsPullDiagnostics(): boolean;
|
|
12
|
+
usesIncrementalDocumentSync(): boolean;
|
|
13
|
+
sendNotification(method: string, params: unknown): void;
|
|
14
|
+
pullDocumentDiagnostics(request: DiagnosticPullRequest): Promise<DocumentDiagnosticReport | null>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Bounded push-publication telemetry for one client's diagnostic state.
|
|
2
|
+
//
|
|
3
|
+
// Push-only servers can publish an early result and a later semantic result
|
|
4
|
+
// for one document synchronization (ADR 0021). This tracker records one
|
|
5
|
+
// bounded per-synchronization publication summary per diagnostic operation
|
|
6
|
+
// and one ambient event when a later publication promotes a synchronization
|
|
7
|
+
// that an earlier operation already reported unconfirmed. Events carry only
|
|
8
|
+
// bounded server, workspace, relative-file, synchronization identity, count,
|
|
9
|
+
// and timing data. They never carry diagnostic payloads or source text.
|
|
10
|
+
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
13
|
+
import { boundCwd, truncateIdentity } from "../debug-telemetry.ts";
|
|
14
|
+
import { uriToFile } from "../utils.ts";
|
|
15
|
+
|
|
16
|
+
/** Maximum tracked synchronizations before the oldest entry is evicted. */
|
|
17
|
+
export const MAX_TRACKED_SYNCHRONIZATIONS = 64;
|
|
18
|
+
|
|
19
|
+
/** Maximum synchronization entries in one bounded publication summary. */
|
|
20
|
+
export const MAX_SUMMARY_SYNCHRONIZATIONS = 16;
|
|
21
|
+
|
|
22
|
+
/** Maximum publication count retained for telemetry; 2 means two or more. */
|
|
23
|
+
const MAX_PUBLICATIONS = 2;
|
|
24
|
+
|
|
25
|
+
/** Bounded identity for one publication telemetry observation. */
|
|
26
|
+
export interface DiagnosticPublicationIdentity {
|
|
27
|
+
/** Configured server name. */
|
|
28
|
+
readonly server?: string;
|
|
29
|
+
/** Absolute workspace root. */
|
|
30
|
+
readonly cwd?: string;
|
|
31
|
+
/** Workspace-relative file path; sync-file operations only. */
|
|
32
|
+
readonly file?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One awaited synchronization of a completed diagnostic operation. */
|
|
36
|
+
export interface DiagnosticPublicationSynchronization {
|
|
37
|
+
readonly uri: string;
|
|
38
|
+
readonly synchronizationId: number;
|
|
39
|
+
readonly evidenceRevision: number;
|
|
40
|
+
readonly confirmed: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One bounded per-synchronization publication summary entry. */
|
|
44
|
+
export interface DiagnosticPublicationSummaryEntry {
|
|
45
|
+
readonly synchronizationId: number;
|
|
46
|
+
readonly publications: number;
|
|
47
|
+
readonly firstReceivedAt: number;
|
|
48
|
+
readonly lastReceivedAt: number;
|
|
49
|
+
readonly confirmed: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SynchronizationPublicationState {
|
|
53
|
+
readonly uri: string;
|
|
54
|
+
readonly synchronizationId: number;
|
|
55
|
+
readonly evidenceRevision: number;
|
|
56
|
+
publications: number;
|
|
57
|
+
firstReceivedAt: number;
|
|
58
|
+
lastReceivedAt: number;
|
|
59
|
+
/** When a finished operation first reported this synchronization unconfirmed. */
|
|
60
|
+
unconfirmedAt?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function synchronizationKey(
|
|
64
|
+
uri: string,
|
|
65
|
+
synchronizationId: number,
|
|
66
|
+
evidenceRevision: number,
|
|
67
|
+
): string {
|
|
68
|
+
return `${uri}|${synchronizationId}|${evidenceRevision}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Track bounded push-publication counts for one LSP client. */
|
|
72
|
+
export class DiagnosticPublicationTracker {
|
|
73
|
+
readonly #states = new Map<string, SynchronizationPublicationState>();
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
private readonly identity: { server?: string; cwd?: string },
|
|
77
|
+
private readonly fileFor: (uri: string) => string | undefined,
|
|
78
|
+
) {}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Record one accepted push publication for a synchronization.
|
|
82
|
+
*
|
|
83
|
+
* The state map is bounded to {@link MAX_TRACKED_SYNCHRONIZATIONS}; the
|
|
84
|
+
* oldest tracked synchronization is evicted when the bound is exceeded.
|
|
85
|
+
*/
|
|
86
|
+
record(
|
|
87
|
+
uri: string,
|
|
88
|
+
synchronizationId: number,
|
|
89
|
+
evidenceRevision: number,
|
|
90
|
+
receivedAt: number = Date.now(),
|
|
91
|
+
): void {
|
|
92
|
+
const key = synchronizationKey(uri, synchronizationId, evidenceRevision);
|
|
93
|
+
const existing = this.#states.get(key);
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.publications = Math.min(existing.publications + 1, MAX_PUBLICATIONS);
|
|
96
|
+
existing.lastReceivedAt = receivedAt;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.#states.set(key, {
|
|
100
|
+
uri,
|
|
101
|
+
synchronizationId,
|
|
102
|
+
evidenceRevision,
|
|
103
|
+
publications: 1,
|
|
104
|
+
firstReceivedAt: receivedAt,
|
|
105
|
+
lastReceivedAt: receivedAt,
|
|
106
|
+
});
|
|
107
|
+
if (this.#states.size > MAX_TRACKED_SYNCHRONIZATIONS) {
|
|
108
|
+
const oldest = this.#states.keys().next().value;
|
|
109
|
+
if (oldest !== undefined) this.#states.delete(oldest);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Emit one bounded per-synchronization publication summary.
|
|
115
|
+
*
|
|
116
|
+
* Synchronizations with no observed push publication are omitted. A
|
|
117
|
+
* synchronization that ends unconfirmed is marked so a later promotion can
|
|
118
|
+
* emit the ambient late-republish event.
|
|
119
|
+
*/
|
|
120
|
+
emitSummary(options: {
|
|
121
|
+
readonly operation: "refresh-open" | "sync-file";
|
|
122
|
+
readonly identity: DiagnosticPublicationIdentity;
|
|
123
|
+
readonly synchronizations: readonly DiagnosticPublicationSynchronization[];
|
|
124
|
+
readonly operationId?: string;
|
|
125
|
+
}): void {
|
|
126
|
+
const entries: DiagnosticPublicationSummaryEntry[] = [];
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
for (const synchronization of options.synchronizations) {
|
|
129
|
+
const state = this.#states.get(
|
|
130
|
+
synchronizationKey(
|
|
131
|
+
synchronization.uri,
|
|
132
|
+
synchronization.synchronizationId,
|
|
133
|
+
synchronization.evidenceRevision,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
if (!state) continue;
|
|
137
|
+
entries.push({
|
|
138
|
+
synchronizationId: state.synchronizationId,
|
|
139
|
+
publications: state.publications,
|
|
140
|
+
firstReceivedAt: state.firstReceivedAt,
|
|
141
|
+
lastReceivedAt: state.lastReceivedAt,
|
|
142
|
+
confirmed: synchronization.confirmed,
|
|
143
|
+
});
|
|
144
|
+
if (!synchronization.confirmed) state.unconfirmedAt ??= now;
|
|
145
|
+
else state.unconfirmedAt = undefined;
|
|
146
|
+
}
|
|
147
|
+
if (entries.length === 0) return;
|
|
148
|
+
recordDebugEvent({
|
|
149
|
+
operationId: options.operationId,
|
|
150
|
+
source: "lsp",
|
|
151
|
+
level: "debug",
|
|
152
|
+
category: "diagnostics.publication",
|
|
153
|
+
message: "LSP diagnostic publication summary",
|
|
154
|
+
cwd: boundCwd(options.identity.cwd),
|
|
155
|
+
data: {
|
|
156
|
+
operation: options.operation,
|
|
157
|
+
synchronizations: entries.slice(0, MAX_SUMMARY_SYNCHRONIZATIONS),
|
|
158
|
+
...(options.identity.server !== undefined
|
|
159
|
+
? { server: truncateIdentity(options.identity.server) }
|
|
160
|
+
: {}),
|
|
161
|
+
...(options.identity.file !== undefined
|
|
162
|
+
? { file: truncateIdentity(options.identity.file) }
|
|
163
|
+
: {}),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Observe one promotion of a synchronization by a later publication.
|
|
170
|
+
*
|
|
171
|
+
* The ambient late-republish event fires only when a finished operation
|
|
172
|
+
* previously reported the synchronization unconfirmed. The mark is cleared
|
|
173
|
+
* so one promotion emits at most one event.
|
|
174
|
+
*/
|
|
175
|
+
promoted(
|
|
176
|
+
uri: string,
|
|
177
|
+
synchronizationId: number,
|
|
178
|
+
evidenceRevision: number,
|
|
179
|
+
receivedAt: number = Date.now(),
|
|
180
|
+
): void {
|
|
181
|
+
const state = this.#states.get(synchronizationKey(uri, synchronizationId, evidenceRevision));
|
|
182
|
+
const unconfirmedAt = state?.unconfirmedAt;
|
|
183
|
+
if (!state || state.publications < 2 || unconfirmedAt === undefined) return;
|
|
184
|
+
state.unconfirmedAt = undefined;
|
|
185
|
+
const file = this.fileFor(uri);
|
|
186
|
+
recordDebugEvent({
|
|
187
|
+
source: "lsp",
|
|
188
|
+
level: "debug",
|
|
189
|
+
category: "diagnostics.publication",
|
|
190
|
+
message: "LSP diagnostic late republish",
|
|
191
|
+
cwd: boundCwd(this.identity.cwd),
|
|
192
|
+
data: {
|
|
193
|
+
synchronizationId,
|
|
194
|
+
publications: state.publications,
|
|
195
|
+
receivedAt,
|
|
196
|
+
delayMs: Math.max(0, receivedAt - unconfirmedAt),
|
|
197
|
+
...(this.identity.server !== undefined
|
|
198
|
+
? { server: truncateIdentity(this.identity.server) }
|
|
199
|
+
: {}),
|
|
200
|
+
...(file !== undefined ? { file: truncateIdentity(file) } : {}),
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Return a workspace-relative diagnostic file path for telemetry identity. */
|
|
207
|
+
export function trackerFileIdentityFor(
|
|
208
|
+
cwd: string | undefined,
|
|
209
|
+
): (uri: string) => string | undefined {
|
|
210
|
+
return (uri) => {
|
|
211
|
+
if (cwd === undefined) return undefined;
|
|
212
|
+
return path.relative(cwd, path.resolve(cwd, uriToFile(uri)));
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { CodeRequestDeadlineError } from "@mrclrchtr/supi-code-runtime/api";
|
|
2
|
+
import type { DocumentDiagnosticReport } from "../config/types.ts";
|
|
3
|
+
import { applyPullReport, type DiagnosticCacheEntry } from "./client-diagnostic-evidence.ts";
|
|
4
|
+
import type { DiagnosticPullRequest } from "./client-diagnostic-request.ts";
|
|
5
|
+
|
|
6
|
+
interface PullDiagnosticEvidenceOptions {
|
|
7
|
+
store: Map<string, DiagnosticCacheEntry>;
|
|
8
|
+
uri: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
synchronizationId?: number;
|
|
11
|
+
evidenceRevision: number;
|
|
12
|
+
currentRevision(): number;
|
|
13
|
+
isCurrentSynchronization(): boolean;
|
|
14
|
+
isRelatedUriTracked(uri: string): boolean;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
deadline?: number;
|
|
17
|
+
operationId?: string;
|
|
18
|
+
pull(request: DiagnosticPullRequest): Promise<DocumentDiagnosticReport | null>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Collect one pull report only if its document and workspace generations remain current. */
|
|
22
|
+
export async function pullDiagnosticEvidence(
|
|
23
|
+
options: PullDiagnosticEvidenceOptions,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
const previous = options.store.get(options.uri);
|
|
26
|
+
const previousResultId = previous?.resultId;
|
|
27
|
+
const report = await options.pull({
|
|
28
|
+
uri: options.uri,
|
|
29
|
+
previousResultId,
|
|
30
|
+
timeoutMs: options.timeoutMs,
|
|
31
|
+
signal: options.signal,
|
|
32
|
+
deadline: options.deadline,
|
|
33
|
+
operationId: options.operationId,
|
|
34
|
+
});
|
|
35
|
+
// Final gate: a report that lands after cancellation or deadline expiry
|
|
36
|
+
// must not be applied as evidence.
|
|
37
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
38
|
+
if (options.deadline !== undefined && Date.now() >= options.deadline) {
|
|
39
|
+
throw new CodeRequestDeadlineError();
|
|
40
|
+
}
|
|
41
|
+
if (report === null || report === undefined) {
|
|
42
|
+
throw new Error("Invalid LSP diagnostic report.");
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
options.evidenceRevision !== options.currentRevision() ||
|
|
46
|
+
(options.synchronizationId !== undefined && !options.isCurrentSynchronization())
|
|
47
|
+
) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const applied = applyPullReport({
|
|
51
|
+
store: options.store,
|
|
52
|
+
uri: options.uri,
|
|
53
|
+
report,
|
|
54
|
+
previous,
|
|
55
|
+
previousResultId,
|
|
56
|
+
synchronizationId: options.synchronizationId,
|
|
57
|
+
evidenceRevision: options.evidenceRevision,
|
|
58
|
+
isRelatedUriTracked: options.isRelatedUriTracked,
|
|
59
|
+
});
|
|
60
|
+
if (!applied) throw new Error("Invalid LSP diagnostic report.");
|
|
61
|
+
return true;
|
|
62
|
+
}
|