@mrclrchtr/supi-code-intelligence 6.3.0 → 7.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.
Files changed (136) hide show
  1. package/README.md +164 -82
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  4. package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  9. package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
  43. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +4 -32
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
  51. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +770 -56
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
  54. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +48 -12
  55. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +50 -4
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
  68. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  69. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  70. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  71. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  72. package/node_modules/yaml/package.json +1 -1
  73. package/package.json +5 -5
  74. package/src/analysis/health/diagnostics.ts +1 -2
  75. package/src/analysis/health/file-scope.ts +4 -1
  76. package/src/analysis/health/server-status.ts +1 -1
  77. package/src/analysis/relations/callees.ts +17 -8
  78. package/src/analysis/relations/implementations.ts +1 -2
  79. package/src/analysis/relations/references.ts +8 -4
  80. package/src/analysis/relations/types.ts +6 -30
  81. package/src/analysis/target/anchored.ts +9 -1
  82. package/src/analysis/target/symbol.ts +29 -11
  83. package/src/analysis/target/types.ts +14 -6
  84. package/src/api.ts +2 -0
  85. package/src/session/graph/collect.ts +18 -3
  86. package/src/session/health-refresh.ts +27 -11
  87. package/src/session/health-types.ts +21 -4
  88. package/src/session/health-workflow.ts +28 -22
  89. package/src/session/orientation-types.ts +5 -6
  90. package/src/session/orientation-workflow.ts +10 -2
  91. package/src/session/session.ts +12 -5
  92. package/src/session/target-workflow.ts +9 -6
  93. package/src/substrate/lsp/maintenance.ts +53 -47
  94. package/src/substrate/lsp/source-tracking.ts +252 -0
  95. package/src/tool/code_find/guidance.ts +3 -3
  96. package/src/tool/code_find/spec.ts +5 -4
  97. package/src/tool/code_find/tui.ts +7 -1
  98. package/src/tool/code_graph/details.ts +32 -0
  99. package/src/tool/code_graph/display.ts +54 -0
  100. package/src/tool/code_graph/format.ts +60 -0
  101. package/src/tool/code_graph/guidance.ts +2 -2
  102. package/src/tool/code_graph/markdown.ts +76 -198
  103. package/src/tool/code_graph/read-next.ts +65 -0
  104. package/src/tool/code_graph/result.ts +118 -83
  105. package/src/tool/code_graph/tui.ts +216 -54
  106. package/src/tool/code_health/guidance.ts +3 -5
  107. package/src/tool/code_health/markdown.ts +90 -57
  108. package/src/tool/code_health/refresh-outcome.ts +51 -0
  109. package/src/tool/code_health/refresh-status.ts +130 -43
  110. package/src/tool/code_health/scope.ts +78 -0
  111. package/src/tool/code_health/spec.ts +1 -1
  112. package/src/tool/code_health/tui.ts +29 -38
  113. package/src/tool/code_inspect/guidance.ts +5 -3
  114. package/src/tool/code_inspect/tui.ts +7 -1
  115. package/src/tool/code_orientation/guidance.ts +3 -4
  116. package/src/tool/code_orientation/result.ts +31 -8
  117. package/src/tool/code_orientation/tui.ts +26 -5
  118. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  119. package/src/tool/code_refactor_apply/tui.ts +7 -3
  120. package/src/tool/code_refactor_plan/guidance.ts +3 -5
  121. package/src/tool/code_refactor_plan/tui.ts +7 -1
  122. package/src/tool/code_resolve/guidance.ts +3 -5
  123. package/src/tool/code_resolve/result.ts +13 -10
  124. package/src/tool/code_resolve/tui.ts +18 -1
  125. package/src/tool/guidance.ts +11 -9
  126. package/src/tool/infra/truncate.ts +39 -13
  127. package/src/tool/result/candidate-row.ts +27 -0
  128. package/src/tool/result/errors.ts +21 -6
  129. package/src/tool/result/types.ts +7 -3
  130. package/src/tool/schemas.ts +5 -0
  131. package/src/types/execution.ts +2 -0
  132. package/src/types/index.ts +2 -0
  133. package/src/ui/tui/candidate-legacy-body.ts +69 -0
  134. package/src/ui/tui/candidate-selection.ts +366 -0
  135. package/src/ui/tui/common.ts +26 -31
  136. 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
- * Returns whether the publication was accepted and whether it promoted a
27
- * tentative entry to confirmed. Fail-closed policy (ADR 0020): unversioned
28
- * pushes are rejected for closed and untracked URIs and for arrivals before
29
- * the URI's sync moment; versioned pushes for a URI closed by a lifecycle
30
- * operation are rejected because their version cannot be verified. An
31
- * unversioned push that arrives after the sync moment of an open document
32
- * is accepted and re-stamped with that document's current synchronization
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: boolean;
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, promoted: false };
38
+ if (!Number.isInteger(options.params.version)) return { accepted: false };
45
39
  if (openDocument && options.params.version !== openDocument.version) {
46
- return { accepted: false, promoted: 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, promoted: false };
52
- if (!acceptUnversionedPush(options, openDocument)) return { accepted: false, promoted: 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
- const promoted =
56
- entry.source === "push" &&
57
- entry.publications !== undefined &&
58
- entry.publications >= 2 &&
59
- isTentativePushEntry(previous);
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, promoted };
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, so the push proves that
81
- * document's synchronization. Versioned pushes keep the current-revision
82
- * check: a version match alone does not prove a current generation.
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: a confirmed pull
96
- // entry keeps the synchronization confirmed, and a tentative push entry
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 (ADR 0021).
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
- if (value.documentSelector !== undefined && value.documentSelector !== null) {
31
- if (!Array.isArray(value.documentSelector)) return false;
32
- if (!value.documentSelector.every((entry) => typeof entry === "string" || isRecord(entry))) {
33
- return false;
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
- return true;
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 idsByMethod = new Map<string, Set<string>>();
105
+ private readonly registrationsByMethod = new Map<string, Map<string, Record<string, unknown>>>();
48
106
 
49
- /** Record one registration id for a method. Duplicate ids are harmless. */
50
- register(method: string, id: string): void {
51
- let ids = this.idsByMethod.get(method);
52
- if (ids === undefined) {
53
- ids = new Set();
54
- this.idsByMethod.set(method, ids);
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
- ids.add(id);
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.idsByMethod.get(method)?.delete(id);
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.idsByMethod.get(method)?.size ?? 0) > 0;
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.idsByMethod.clear();
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
- isCodeRequestInterruption,
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 type { DiagnosticObserver, DiagnosticPushWaitOutcome } from "./client-diagnostic-timing.ts";
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
- readonly supportsPull: boolean;
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
- readonly freshPush: () => boolean;
30
- /** Receive time of a current tentative push that is already cached. */
31
- readonly currentPushReceivedAt: () => number | undefined;
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
- if (options.supportsPull) {
47
- const pullOutcome = await collectPullEvidence(options, control);
48
- if (pullOutcome === "pull" || pullOutcome === "push") {
49
- return completedCodeQuery(options.diagnostics());
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
- if (options.freshPush()) {
61
- options.observer.pushWaitCompleted(1, "published");
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 until a confirmed push publication settles the synchronization.
154
+ * Wait for ambient push observations within the file collection budget.
87
155
  *
88
- * Every accepted publication releases the waiter. A confirmed push ends the
89
- * wait as "published". A publication that stays tentative (no republish
90
- * arrived for the same synchronization) ends as "tentative" when the budget
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 waitForConfirmedPush(
160
+ async function waitForPushObservation(
95
161
  options: FileDiagnosticCollectionOptions,
96
162
  control?: CodeRequestControl,
97
163
  ): Promise<DiagnosticPushWaitOutcome> {
98
- // A tentative publication may already sit in the cache when the wait
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
- initialPushReceivedAt === undefined
167
+ initialObservation === undefined
105
168
  ? Number.POSITIVE_INFINITY
106
- : initialPushReceivedAt + options.maxWaitMs,
169
+ : initialObservation.receivedAt + options.maxWaitMs,
107
170
  );
108
- let observedPublication = initialPushReceivedAt !== undefined;
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.currentPushReceivedAt() !== undefined
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
- }