@mrclrchtr/supi-code-intelligence 6.4.0 → 7.0.1
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 +164 -82
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
- 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/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
- 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/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +1 -2
- package/src/analysis/health/file-scope.ts +4 -1
- package/src/analysis/health/server-status.ts +1 -1
- package/src/analysis/relations/callees.ts +17 -8
- package/src/analysis/relations/implementations.ts +1 -2
- package/src/analysis/relations/references.ts +8 -4
- package/src/analysis/relations/types.ts +6 -30
- package/src/analysis/target/anchored.ts +9 -1
- package/src/analysis/target/symbol.ts +29 -11
- package/src/analysis/target/types.ts +14 -6
- package/src/api.ts +2 -0
- package/src/session/graph/collect.ts +18 -3
- package/src/session/health-refresh.ts +12 -1
- package/src/session/health-types.ts +18 -6
- package/src/session/health-workflow.ts +21 -19
- package/src/session/orientation-types.ts +5 -6
- package/src/session/orientation-workflow.ts +10 -2
- package/src/session/target-workflow.ts +9 -6
- package/src/substrate/lsp/source-tracking.ts +11 -6
- package/src/tool/code_find/guidance.ts +2 -2
- package/src/tool/code_find/spec.ts +5 -4
- package/src/tool/code_find/tui.ts +7 -1
- package/src/tool/code_graph/details.ts +32 -0
- package/src/tool/code_graph/display.ts +54 -0
- package/src/tool/code_graph/format.ts +60 -0
- package/src/tool/code_graph/guidance.ts +1 -1
- package/src/tool/code_graph/markdown.ts +76 -198
- package/src/tool/code_graph/read-next.ts +65 -0
- package/src/tool/code_graph/result.ts +118 -83
- package/src/tool/code_graph/tui.ts +216 -54
- package/src/tool/code_health/guidance.ts +2 -4
- package/src/tool/code_health/markdown.ts +75 -58
- package/src/tool/code_health/refresh-outcome.ts +41 -1
- package/src/tool/code_health/refresh-status.ts +114 -48
- package/src/tool/code_health/scope.ts +78 -0
- package/src/tool/code_health/spec.ts +1 -1
- package/src/tool/code_health/tui.ts +25 -38
- package/src/tool/code_inspect/tui.ts +7 -1
- package/src/tool/code_orientation/result.ts +31 -8
- package/src/tool/code_orientation/tui.ts +26 -5
- package/src/tool/code_refactor_apply/guidance.ts +1 -1
- package/src/tool/code_refactor_apply/tui.ts +7 -3
- package/src/tool/code_refactor_plan/guidance.ts +1 -1
- package/src/tool/code_refactor_plan/tui.ts +7 -1
- package/src/tool/code_resolve/guidance.ts +2 -4
- package/src/tool/code_resolve/result.ts +13 -10
- package/src/tool/code_resolve/tui.ts +18 -1
- package/src/tool/infra/truncate.ts +39 -13
- package/src/tool/result/candidate-row.ts +27 -0
- package/src/tool/result/errors.ts +21 -6
- package/src/tool/result/types.ts +7 -3
- package/src/tool/schemas.ts +5 -0
- package/src/types/execution.ts +2 -0
- package/src/types/index.ts +2 -0
- package/src/ui/tui/candidate-legacy-body.ts +69 -0
- package/src/ui/tui/candidate-selection.ts +366 -0
- package/src/ui/tui/common.ts +26 -31
- package/src/ui/tui/execution-error.ts +92 -0
|
@@ -23,42 +23,51 @@ interface ApplyPushOptions {
|
|
|
23
23
|
/**
|
|
24
24
|
* Apply one valid push publication.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* state. The first valid publication for a synchronization is tentative; a
|
|
34
|
-
* later valid publication for the same synchronization ID and evidence
|
|
35
|
-
* revision promotes the cache to confirmed (ADR 0021).
|
|
26
|
+
* Ambient pushes are observations, not confirmation evidence. Fail-closed
|
|
27
|
+
* policy (ADR 0020): unversioned pushes are rejected for closed and
|
|
28
|
+
* untracked URIs and for arrivals before the URI's sync moment; versioned
|
|
29
|
+
* pushes for a URI closed by a lifecycle operation are rejected because their
|
|
30
|
+
* version cannot be verified. An unversioned push that arrives after the sync
|
|
31
|
+
* moment of an open document is accepted and re-stamped with that document's
|
|
32
|
+
* current synchronization state. Publication counts remain telemetry only.
|
|
36
33
|
*/
|
|
37
|
-
export function applyPushDiagnostics(options: ApplyPushOptions): {
|
|
38
|
-
accepted:
|
|
39
|
-
promoted: boolean;
|
|
40
|
-
} {
|
|
41
|
-
if (!isValidPublishDiagnosticsParams(options.params)) return { accepted: false, promoted: false };
|
|
34
|
+
export function applyPushDiagnostics(options: ApplyPushOptions): { accepted: boolean } {
|
|
35
|
+
if (!isValidPublishDiagnosticsParams(options.params)) return { accepted: false };
|
|
42
36
|
const openDocument = options.openDocuments.get(options.params.uri);
|
|
43
37
|
if (options.params.version !== undefined) {
|
|
44
|
-
if (!Number.isInteger(options.params.version)) return { accepted: false
|
|
38
|
+
if (!Number.isInteger(options.params.version)) return { accepted: false };
|
|
45
39
|
if (openDocument && options.params.version !== openDocument.version) {
|
|
46
|
-
return { accepted: false
|
|
40
|
+
return { accepted: false };
|
|
47
41
|
}
|
|
48
|
-
if (!openDocument && options.closedVersionedBarrier)
|
|
49
|
-
return { accepted: false, promoted: false };
|
|
42
|
+
if (!openDocument && options.closedVersionedBarrier) return { accepted: false };
|
|
50
43
|
}
|
|
51
|
-
if (!Array.isArray(options.params.diagnostics)) return { accepted: false
|
|
52
|
-
if (!acceptUnversionedPush(options, openDocument)) return { accepted: false
|
|
53
|
-
const entry = buildPushCacheEntry(options, openDocument);
|
|
44
|
+
if (!Array.isArray(options.params.diagnostics)) return { accepted: false };
|
|
45
|
+
if (!acceptUnversionedPush(options, openDocument)) return { accepted: false };
|
|
54
46
|
const previous = options.store.get(options.params.uri);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
// A request-confirmed result owns the synchronization. Ambient pushes may
|
|
48
|
+
// still release waiters and telemetry, but they must not replace that result.
|
|
49
|
+
if (isCurrentRequestEvidence(previous, openDocument, options.evidenceRevision)) {
|
|
50
|
+
return { accepted: true };
|
|
51
|
+
}
|
|
52
|
+
const entry = buildPushCacheEntry(options, openDocument);
|
|
60
53
|
options.store.set(options.params.uri, entry);
|
|
61
|
-
return { accepted: true
|
|
54
|
+
return { accepted: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Test whether a request result still owns the current document generation. */
|
|
58
|
+
function isCurrentRequestEvidence(
|
|
59
|
+
entry: DiagnosticCacheEntry | undefined,
|
|
60
|
+
document: OpenDocumentState | undefined,
|
|
61
|
+
evidenceRevision: number,
|
|
62
|
+
): boolean {
|
|
63
|
+
return Boolean(
|
|
64
|
+
entry &&
|
|
65
|
+
entry.source !== "push" &&
|
|
66
|
+
document &&
|
|
67
|
+
entry.synchronizationId === document.synchronizationId &&
|
|
68
|
+
entry.evidenceRevision === evidenceRevision &&
|
|
69
|
+
document.evidenceRevision === evidenceRevision,
|
|
70
|
+
);
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
/** Gate one unversioned push publication against the sync-moment policy. */
|
|
@@ -77,9 +86,9 @@ function acceptUnversionedPush(
|
|
|
77
86
|
* Build the stored entry for one accepted push publication.
|
|
78
87
|
*
|
|
79
88
|
* Time-gated unversioned acceptance re-stamps the entry with the open
|
|
80
|
-
* document's current synchronization state
|
|
81
|
-
*
|
|
82
|
-
*
|
|
89
|
+
* document's current synchronization state. Versioned pushes keep the
|
|
90
|
+
* current-revision check: a version match alone does not prove a current
|
|
91
|
+
* generation.
|
|
83
92
|
*/
|
|
84
93
|
function buildPushCacheEntry(
|
|
85
94
|
options: ApplyPushOptions,
|
|
@@ -92,9 +101,8 @@ function buildPushCacheEntry(
|
|
|
92
101
|
const evidenceRevision = openDocument?.evidenceRevision;
|
|
93
102
|
const previous = options.store.get(options.params.uri);
|
|
94
103
|
// A later valid publication for the same synchronization ID and evidence
|
|
95
|
-
// revision continues the entry's publication count
|
|
96
|
-
//
|
|
97
|
-
// is promoted. Any other publication starts a fresh count.
|
|
104
|
+
// revision continues the entry's publication count for telemetry. Any
|
|
105
|
+
// other publication starts a fresh count.
|
|
98
106
|
const sameSynchronization =
|
|
99
107
|
synchronizationId !== undefined &&
|
|
100
108
|
previous?.synchronizationId === synchronizationId &&
|
|
@@ -233,7 +241,7 @@ function collectCachedDiagnostics(options: {
|
|
|
233
241
|
options.evidenceRevision,
|
|
234
242
|
);
|
|
235
243
|
// A current tentative error is useful partial evidence. Keep its entry
|
|
236
|
-
// non-current so it cannot establish a clean or settled result
|
|
244
|
+
// non-current so it cannot establish a clean or settled result.
|
|
237
245
|
const tentative = entryCurrent && isTentativePushEntry(entry);
|
|
238
246
|
const confirmed = entryCurrent && !tentative;
|
|
239
247
|
current &&= confirmed;
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// support is enabled only while the static provider shape is valid or the
|
|
7
7
|
// dynamic set for `textDocument/diagnostic` is non-empty.
|
|
8
8
|
|
|
9
|
+
import { posix as posixPath } from "node:path";
|
|
10
|
+
import { uriToFile } from "@mrclrchtr/supi-core/path";
|
|
11
|
+
import { detectLanguageId } from "../utils.ts";
|
|
12
|
+
|
|
9
13
|
/** LSP method for document diagnostic pulls. */
|
|
10
14
|
export const DOCUMENT_DIAGNOSTIC_METHOD = "textDocument/diagnostic";
|
|
11
15
|
|
|
@@ -27,13 +31,67 @@ export function isValidDiagnosticOptions(value: unknown): boolean {
|
|
|
27
31
|
if (typeof value.interFileDependencies !== "boolean") return false;
|
|
28
32
|
if (typeof value.workspaceDiagnostics !== "boolean") return false;
|
|
29
33
|
if (value.identifier !== undefined && typeof value.identifier !== "string") return false;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
return isValidDocumentSelector(value.documentSelector);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Test whether an optional LSP document selector has a valid shape. */
|
|
38
|
+
export function isValidDocumentSelector(value: unknown): boolean {
|
|
39
|
+
if (value === undefined || value === null) return true;
|
|
40
|
+
return (
|
|
41
|
+
Array.isArray(value) &&
|
|
42
|
+
value.every((entry) => {
|
|
43
|
+
if (typeof entry === "string") return entry.length > 0;
|
|
44
|
+
if (!isRecord(entry)) return false;
|
|
45
|
+
return (
|
|
46
|
+
(entry.language === undefined || typeof entry.language === "string") &&
|
|
47
|
+
(entry.scheme === undefined || typeof entry.scheme === "string") &&
|
|
48
|
+
(entry.pattern === undefined || typeof entry.pattern === "string")
|
|
49
|
+
);
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Match one valid document selector against a file URI. */
|
|
55
|
+
export function isDocumentSelectorApplicable(selector: unknown, uri: string): boolean {
|
|
56
|
+
if (selector === undefined || selector === null) return true;
|
|
57
|
+
if (!isValidDocumentSelector(selector)) return false;
|
|
58
|
+
if (!Array.isArray(selector) || selector.length === 0) return false;
|
|
59
|
+
const language = detectLanguageId(uriToFile(uri));
|
|
60
|
+
const scheme = readUriScheme(uri);
|
|
61
|
+
const filePath = uriToFile(uri).replaceAll("\\", "/");
|
|
62
|
+
return selector.some((entry) => {
|
|
63
|
+
if (typeof entry === "string") return entry === language;
|
|
64
|
+
if (!isRecord(entry)) return false;
|
|
65
|
+
if (entry.language !== undefined && entry.language !== language) return false;
|
|
66
|
+
if (entry.scheme !== undefined && entry.scheme !== scheme) return false;
|
|
67
|
+
const pattern = entry.pattern;
|
|
68
|
+
if (pattern === undefined) return true;
|
|
69
|
+
if (typeof pattern !== "string") return false;
|
|
70
|
+
const basename = filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
71
|
+
return globMatches(pattern, filePath) || globMatches(pattern, basename);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readUriScheme(uri: string): string | undefined {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(uri).protocol.replace(/:$/, "");
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
35
80
|
}
|
|
36
|
-
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function globMatches(pattern: string, value: string): boolean {
|
|
84
|
+
try {
|
|
85
|
+
return posixPath.matchesGlob(value, pattern);
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** One validated diagnostic registration retained for selector matching. */
|
|
92
|
+
export interface DiagnosticRegistration {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly options: Record<string, unknown>;
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
/**
|
|
@@ -44,30 +102,38 @@ export function isValidDiagnosticOptions(value: unknown): boolean {
|
|
|
44
102
|
* removes one id; pull stays enabled until the last id is removed.
|
|
45
103
|
*/
|
|
46
104
|
export class ClientDynamicRegistrations {
|
|
47
|
-
private readonly
|
|
105
|
+
private readonly registrationsByMethod = new Map<string, Map<string, Record<string, unknown>>>();
|
|
48
106
|
|
|
49
|
-
/** Record one registration id for a method. Duplicate ids
|
|
50
|
-
register(method: string, id: string): void {
|
|
51
|
-
let
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
this.
|
|
107
|
+
/** Record one registration id for a method. Duplicate ids replace options. */
|
|
108
|
+
register(method: string, id: string, options: Record<string, unknown> = {}): void {
|
|
109
|
+
let registrations = this.registrationsByMethod.get(method);
|
|
110
|
+
if (registrations === undefined) {
|
|
111
|
+
registrations = new Map();
|
|
112
|
+
this.registrationsByMethod.set(method, registrations);
|
|
55
113
|
}
|
|
56
|
-
|
|
114
|
+
registrations.set(id, options);
|
|
57
115
|
}
|
|
58
116
|
|
|
59
117
|
/** Remove one registration id; unknown ids are harmless no-ops. */
|
|
60
118
|
unregister(method: string, id: string): void {
|
|
61
|
-
this.
|
|
119
|
+
this.registrationsByMethod.get(method)?.delete(id);
|
|
62
120
|
}
|
|
63
121
|
|
|
64
122
|
/** Whether any registration id remains active for a method. */
|
|
65
123
|
has(method: string): boolean {
|
|
66
|
-
return (this.
|
|
124
|
+
return (this.registrationsByMethod.get(method)?.size ?? 0) > 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Return active registration options for one method. */
|
|
128
|
+
get(method: string): readonly DiagnosticRegistration[] {
|
|
129
|
+
return Array.from(this.registrationsByMethod.get(method) ?? [], ([id, options]) => ({
|
|
130
|
+
id,
|
|
131
|
+
options,
|
|
132
|
+
}));
|
|
67
133
|
}
|
|
68
134
|
|
|
69
135
|
/** Drop all registrations — capability loss on shutdown, crash, or disposal. */
|
|
70
136
|
clear(): void {
|
|
71
|
-
this.
|
|
137
|
+
this.registrationsByMethod.clear();
|
|
72
138
|
}
|
|
73
139
|
}
|
|
@@ -2,23 +2,37 @@ import {
|
|
|
2
2
|
type CodeQueryResult,
|
|
3
3
|
type CodeRequestControl,
|
|
4
4
|
completedCodeQuery,
|
|
5
|
-
|
|
5
|
+
isCodeRequestDeadlineError,
|
|
6
6
|
partialCodeQuery,
|
|
7
7
|
throwIfCodeRequestInterrupted,
|
|
8
8
|
unavailableCodeQuery,
|
|
9
9
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
10
10
|
import type { Diagnostic } from "../config/types.ts";
|
|
11
11
|
import { TENTATIVE_PUSH_UNAVAILABLE_REASON } from "../diagnostics/evidence.ts";
|
|
12
|
+
import { raceRequestControl } from "../session/readiness.ts";
|
|
12
13
|
import {
|
|
13
14
|
type DiagnosticSynchronization,
|
|
14
15
|
incompleteDiagnosticResult,
|
|
15
|
-
raceDiagnosticPull,
|
|
16
16
|
} from "./client-diagnostic-evidence.ts";
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
type DiagnosticRequestSource,
|
|
19
|
+
isDiagnosticRequestInvalidated,
|
|
20
|
+
} from "./client-diagnostic-request.ts";
|
|
21
|
+
import {
|
|
22
|
+
type DiagnosticObserver,
|
|
23
|
+
type DiagnosticPushWaitOutcome,
|
|
24
|
+
isDiagnosticTimeout,
|
|
25
|
+
} from "./client-diagnostic-timing.ts";
|
|
18
26
|
import type { DiagnosticWaitRegistry } from "./client-diagnostic-waiters.ts";
|
|
19
27
|
|
|
20
28
|
interface FileDiagnosticCollectionOptions {
|
|
21
|
-
|
|
29
|
+
/** Request-based evidence takes priority over all ambient push waits. */
|
|
30
|
+
readonly requestDiagnostics?: (
|
|
31
|
+
timeoutMs: number,
|
|
32
|
+
deadline: number,
|
|
33
|
+
control?: CodeRequestControl,
|
|
34
|
+
) => Promise<boolean>;
|
|
35
|
+
readonly requestSource?: DiagnosticRequestSource;
|
|
22
36
|
readonly syncStart: number;
|
|
23
37
|
readonly maxWaitMs: number;
|
|
24
38
|
readonly request: DiagnosticSynchronization;
|
|
@@ -26,30 +40,46 @@ interface FileDiagnosticCollectionOptions {
|
|
|
26
40
|
readonly observer: DiagnosticObserver;
|
|
27
41
|
readonly waiters: DiagnosticWaitRegistry;
|
|
28
42
|
readonly current: () => boolean;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
43
|
+
/** Current ambient push observation for this synchronization, when present. */
|
|
44
|
+
readonly currentPushObservation: () =>
|
|
45
|
+
| { receivedAt: number; hasDiagnostics: boolean }
|
|
46
|
+
| undefined;
|
|
32
47
|
readonly diagnostics: () => Diagnostic[];
|
|
33
|
-
readonly pullDiagnostics: (timeoutMs: number, signal: AbortSignal) => Promise<boolean>;
|
|
34
|
-
/** Observe the push wait outcome without finishing the observer; the caller finishes once. */
|
|
35
|
-
readonly onPushWait?: (outcome: DiagnosticPushWaitOutcome) => void;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
type PullCollectionOutcome = "failed" | "pull" | "push" | "released";
|
|
39
|
-
|
|
40
50
|
/** Collect fresh pull or push evidence for one synchronized document. */
|
|
41
51
|
export async function collectSynchronizedFileDiagnostics(
|
|
42
52
|
options: FileDiagnosticCollectionOptions,
|
|
43
53
|
control?: CodeRequestControl,
|
|
44
54
|
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
45
55
|
throwIfCodeRequestInterrupted(control);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
return options.requestDiagnostics
|
|
57
|
+
? collectRequestBasedEvidence(options, control)
|
|
58
|
+
: collectPushOnlyEvidence(options, control);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function collectRequestBasedEvidence(
|
|
62
|
+
options: FileDiagnosticCollectionOptions,
|
|
63
|
+
control?: CodeRequestControl,
|
|
64
|
+
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
65
|
+
const requestOutcome = await collectRequestEvidence(options, control);
|
|
66
|
+
if (requestOutcome === "completed") return completedCodeQuery(options.diagnostics());
|
|
67
|
+
if (requestOutcome === "released") {
|
|
68
|
+
return incompleteDiagnosticResult(options.cachedDiagnostics, "released");
|
|
69
|
+
}
|
|
70
|
+
if (options.currentPushObservation()?.hasDiagnostics) {
|
|
71
|
+
return partialCodeQuery(
|
|
72
|
+
options.diagnostics(),
|
|
73
|
+
"Fresh diagnostic requests failed; ambient diagnostics are partial evidence.",
|
|
74
|
+
);
|
|
51
75
|
}
|
|
76
|
+
return incompleteDiagnosticResult(options.cachedDiagnostics, "timed-out");
|
|
77
|
+
}
|
|
52
78
|
|
|
79
|
+
async function collectPushOnlyEvidence(
|
|
80
|
+
options: FileDiagnosticCollectionOptions,
|
|
81
|
+
control?: CodeRequestControl,
|
|
82
|
+
): Promise<CodeQueryResult<Diagnostic[]>> {
|
|
53
83
|
throwIfCodeRequestInterrupted(control);
|
|
54
84
|
if (!options.current()) {
|
|
55
85
|
options.observer.pushWaitCompleted(1, "released");
|
|
@@ -57,20 +87,9 @@ export async function collectSynchronizedFileDiagnostics(
|
|
|
57
87
|
"Diagnostic collection ended before the current document synchronization was confirmed.",
|
|
58
88
|
);
|
|
59
89
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return completedCodeQuery(options.diagnostics());
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const push = await waitForConfirmedPush(options, control);
|
|
66
|
-
if (options.onPushWait) options.onPushWait(push);
|
|
67
|
-
else options.observer.pushWaitCompleted(1, push);
|
|
68
|
-
if (push === "published") {
|
|
69
|
-
return completedCodeQuery(options.diagnostics());
|
|
70
|
-
}
|
|
90
|
+
const push = await waitForPushObservation(options, control);
|
|
91
|
+
options.observer.pushWaitCompleted(1, push);
|
|
71
92
|
if (push === "tentative") {
|
|
72
|
-
// A current tentative error is useful partial evidence, but an empty
|
|
73
|
-
// publication cannot establish that the document is clean (ADR 0021).
|
|
74
93
|
const diagnostics = options.diagnostics();
|
|
75
94
|
return diagnostics.length > 0
|
|
76
95
|
? partialCodeQuery(diagnostics, TENTATIVE_PUSH_UNAVAILABLE_REASON)
|
|
@@ -82,90 +101,87 @@ export async function collectSynchronizedFileDiagnostics(
|
|
|
82
101
|
);
|
|
83
102
|
}
|
|
84
103
|
|
|
104
|
+
/** Collect one request report without using an ambient push as confirmation. */
|
|
105
|
+
async function collectRequestEvidence(
|
|
106
|
+
options: FileDiagnosticCollectionOptions,
|
|
107
|
+
control?: CodeRequestControl,
|
|
108
|
+
): Promise<"completed" | "failed" | "released"> {
|
|
109
|
+
const deadline = Math.min(
|
|
110
|
+
options.syncStart + options.maxWaitMs,
|
|
111
|
+
control?.deadline ?? Number.POSITIVE_INFINITY,
|
|
112
|
+
);
|
|
113
|
+
const remaining = deadline - Date.now();
|
|
114
|
+
if (remaining <= 0) {
|
|
115
|
+
options.observer.requestTimedOut(options.requestSource);
|
|
116
|
+
return "failed";
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const requestDiagnostics = options.requestDiagnostics;
|
|
120
|
+
if (!requestDiagnostics) return "released";
|
|
121
|
+
const completed = await raceRequestControl(
|
|
122
|
+
requestDiagnostics(remaining, deadline, control),
|
|
123
|
+
control,
|
|
124
|
+
);
|
|
125
|
+
if (!completed) {
|
|
126
|
+
options.observer.requestIncomplete(options.requestSource);
|
|
127
|
+
return "released";
|
|
128
|
+
}
|
|
129
|
+
options.observer.requestCompleted(options.requestSource ?? "pull", 1);
|
|
130
|
+
return "completed";
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (isDiagnosticRequestInvalidated(error)) {
|
|
133
|
+
options.observer.requestIncomplete(options.requestSource);
|
|
134
|
+
return "released";
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
control?.signal?.aborted ||
|
|
138
|
+
(control?.deadline !== undefined && Date.now() >= control.deadline)
|
|
139
|
+
) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
const timedOut = isDiagnosticCollectionTimeout(error);
|
|
143
|
+
options.observer.requestFailed(options.requestSource, error);
|
|
144
|
+
options.observer.requestIncomplete(options.requestSource, timedOut);
|
|
145
|
+
return "failed";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isDiagnosticCollectionTimeout(error: unknown): boolean {
|
|
150
|
+
return isCodeRequestDeadlineError(error) || isDiagnosticTimeout(error);
|
|
151
|
+
}
|
|
152
|
+
|
|
85
153
|
/**
|
|
86
|
-
* Wait
|
|
154
|
+
* Wait for ambient push observations within the file collection budget.
|
|
87
155
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* expires. A wait with no publication at all keeps the waiter's
|
|
92
|
-
* "timed-out", and a lifecycle release stays "released".
|
|
156
|
+
* A publication is useful partial evidence, but it does not confirm a
|
|
157
|
+
* synchronization. Continue to the budget so delayed publications can still
|
|
158
|
+
* improve the partial result; a lifecycle release remains definitive.
|
|
93
159
|
*/
|
|
94
|
-
async function
|
|
160
|
+
async function waitForPushObservation(
|
|
95
161
|
options: FileDiagnosticCollectionOptions,
|
|
96
162
|
control?: CodeRequestControl,
|
|
97
163
|
): Promise<DiagnosticPushWaitOutcome> {
|
|
98
|
-
|
|
99
|
-
// starts (ADR 0021). Do not grant repeated callers a new full wait window:
|
|
100
|
-
// bound them by the first operation window or the cached publication age.
|
|
101
|
-
const initialPushReceivedAt = options.currentPushReceivedAt();
|
|
164
|
+
const initialObservation = options.currentPushObservation();
|
|
102
165
|
const waitDeadline = Math.min(
|
|
103
166
|
options.syncStart + options.maxWaitMs,
|
|
104
|
-
|
|
167
|
+
initialObservation === undefined
|
|
105
168
|
? Number.POSITIVE_INFINITY
|
|
106
|
-
:
|
|
169
|
+
: initialObservation.receivedAt + options.maxWaitMs,
|
|
107
170
|
);
|
|
108
|
-
let observedPublication =
|
|
171
|
+
let observedPublication = initialObservation !== undefined;
|
|
109
172
|
for (;;) {
|
|
110
173
|
throwIfCodeRequestInterrupted(control);
|
|
111
|
-
// A republish can arrive after the previous wait released but before the
|
|
112
|
-
// next waiter registers; re-check the store on every loop pass.
|
|
113
|
-
if (options.freshPush()) return "published";
|
|
114
174
|
const push = await options.waiters.waitForPush(
|
|
115
175
|
options.request.uri,
|
|
116
176
|
Math.max(0, waitDeadline - Date.now()),
|
|
117
177
|
control,
|
|
118
178
|
);
|
|
119
|
-
// A lifecycle release ends the wait definitively: a tentative
|
|
120
|
-
// publication observed earlier must not reclassify the release.
|
|
121
179
|
if (push === "released") return "released";
|
|
122
|
-
// A promotion can land while the timed-out waiter tears down; the cache
|
|
123
|
-
// must complete the wait before the timeout classifies the outcome.
|
|
124
|
-
if (options.freshPush()) return "published";
|
|
125
180
|
if (push === "timed-out") {
|
|
126
|
-
return observedPublication || options.
|
|
181
|
+
return observedPublication || options.currentPushObservation() !== undefined
|
|
127
182
|
? "tentative"
|
|
128
183
|
: "timed-out";
|
|
129
184
|
}
|
|
130
185
|
observedPublication = true;
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
|
-
|
|
134
|
-
async function collectPullEvidence(
|
|
135
|
-
options: FileDiagnosticCollectionOptions,
|
|
136
|
-
control?: CodeRequestControl,
|
|
137
|
-
): Promise<PullCollectionOutcome> {
|
|
138
|
-
const remaining = options.maxWaitMs - (Date.now() - options.syncStart);
|
|
139
|
-
if (remaining <= 0) {
|
|
140
|
-
options.observer.pullTimedOut();
|
|
141
|
-
return "failed";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const controller = new AbortController();
|
|
146
|
-
// Link the caller's cancellation to the local pull controller so an
|
|
147
|
-
// in-flight pull receives protocol cancellation and stops promptly.
|
|
148
|
-
const onAbort = () => controller.abort(control?.signal?.reason);
|
|
149
|
-
if (control?.signal?.aborted) onAbort();
|
|
150
|
-
else control?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
151
|
-
try {
|
|
152
|
-
const outcome = await raceDiagnosticPull({
|
|
153
|
-
pull: options.pullDiagnostics(remaining, controller.signal),
|
|
154
|
-
waitForChange: () => options.waiters.waitForChange(),
|
|
155
|
-
freshPush: options.freshPush,
|
|
156
|
-
current: options.current,
|
|
157
|
-
});
|
|
158
|
-
if (outcome !== "pull") controller.abort();
|
|
159
|
-
if (outcome === "pull") options.observer.pullCompleted(1);
|
|
160
|
-
else options.observer.pullFailed(undefined);
|
|
161
|
-
if (outcome === "push") options.observer.pushWaitCompleted(1, "published");
|
|
162
|
-
return outcome;
|
|
163
|
-
} finally {
|
|
164
|
-
control?.signal?.removeEventListener("abort", onAbort);
|
|
165
|
-
}
|
|
166
|
-
} catch (error) {
|
|
167
|
-
if (isCodeRequestInterruption(error, control)) throw error;
|
|
168
|
-
options.observer.pullFailed(error);
|
|
169
|
-
return "failed";
|
|
170
|
-
}
|
|
171
|
-
}
|