@mrclrchtr/supi-code-intelligence 6.0.1 → 6.2.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 (147) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  4. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  7. package/node_modules/@mrclrchtr/supi-lsp/README.md +26 -4
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-jsonrpc/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.d.ts +1 -1
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.d.ts +2 -2
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.d.ts +9 -9
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-protocol/package.json +3 -3
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.d.ts +2 -2
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/esm/main.js +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.d.ts +2 -2
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/lib/umd/main.js +1 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/vscode-languageserver-types/package.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-lsp/package.json +4 -5
  24. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +14 -7
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +1 -1
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +0 -5
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +6 -3
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +2 -1
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +5 -2
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +2 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +23 -5
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +40 -18
  33. package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +164 -30
  34. package/node_modules/@mrclrchtr/supi-lsp/src/config/lsp-settings.ts +9 -87
  35. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +8 -1
  36. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +4 -5
  37. package/node_modules/@mrclrchtr/supi-lsp/src/config/types.ts +1 -29
  38. package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +7 -21
  39. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-severity.ts +6 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +11 -14
  41. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +0 -5
  42. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +0 -1
  43. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +29 -49
  44. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +1 -1
  45. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -8
  46. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +1 -9
  47. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +12 -6
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +9 -1
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +26 -41
  50. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +849 -117
  51. package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +6 -4
  52. package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +59 -28
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +27 -19
  54. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +10 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +26 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registration.ts +0 -17
  57. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +118 -27
  58. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +7 -2
  59. package/node_modules/@mrclrchtr/supi-lsp/src/session/scanner.ts +24 -21
  60. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +33 -5
  61. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +19 -40
  62. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +13 -19
  63. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +287 -0
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +6 -18
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  66. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +5 -0
  67. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  68. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
  69. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +5 -0
  70. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -5
  71. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  72. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  73. package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +1 -11
  74. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +19 -0
  75. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +5 -23
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +3 -21
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +5 -9
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +10 -20
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +7 -1
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +9 -5
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +134 -9
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-name.ts +96 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +11 -12
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +39 -56
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +9 -7
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/js-binding-pattern.ts +26 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +2 -21
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +144 -12
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +50 -37
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/scope.ts +138 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +0 -7
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +1 -14
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +0 -10
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +6 -8
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +34 -10
  96. package/package.json +5 -5
  97. package/src/analysis/capability/capability-warnings.ts +4 -22
  98. package/src/analysis/health/recovery.ts +4 -0
  99. package/src/analysis/health/server-status.ts +64 -0
  100. package/src/analysis/health/signals.ts +1 -0
  101. package/src/analysis/provider.ts +2 -1
  102. package/src/analysis/search/ast-scan-timing.ts +4 -2
  103. package/src/analysis/search/ast-scan.ts +1 -1
  104. package/src/analysis/target/symbol.ts +28 -16
  105. package/src/analysis/target/types.ts +1 -0
  106. package/src/api.ts +1 -0
  107. package/src/extension.ts +4 -2
  108. package/src/session/find-workflow.ts +5 -12
  109. package/src/session/health-refresh.ts +30 -4
  110. package/src/session/health-types.ts +27 -2
  111. package/src/session/health-workflow.ts +5 -1
  112. package/src/session/input/health-refactor.ts +16 -14
  113. package/src/session/refactor-types.ts +14 -1
  114. package/src/session/refactor-workflow.ts +21 -16
  115. package/src/session/semantic-demand.ts +36 -0
  116. package/src/session/target-workflow.ts +9 -12
  117. package/src/substrate/lsp/lifecycle.ts +0 -3
  118. package/src/substrate/lsp/maintenance.ts +4 -5
  119. package/src/substrate/lsp/settings.ts +6 -9
  120. package/src/substrate/lsp/state.ts +0 -2
  121. package/src/substrate/workspace-provider-host.ts +2 -2
  122. package/src/tool/code_health/file-scope-markdown.ts +43 -0
  123. package/src/tool/code_health/guidance.ts +2 -2
  124. package/src/tool/code_health/markdown.ts +81 -69
  125. package/src/tool/code_health/refresh-outcome.ts +46 -0
  126. package/src/tool/code_health/refresh-status.ts +166 -23
  127. package/src/tool/code_health/result.ts +15 -5
  128. package/src/tool/code_health/spec.ts +2 -1
  129. package/src/tool/code_health/tui.ts +42 -8
  130. package/src/tool/code_refactor_apply/guidance.ts +3 -2
  131. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  132. package/src/tool/register.ts +5 -2
  133. package/src/tool/result/errors.ts +1 -0
  134. package/src/tool/result/types.ts +6 -0
  135. package/src/tool/schemas.ts +21 -11
  136. package/src/ui/footer.ts +7 -5
  137. package/src/ui/status-command.ts +31 -0
  138. package/src/ui/status-overlay.ts +69 -23
  139. package/node_modules/@mrclrchtr/supi-lsp/src/config/server-actions.ts +0 -59
  140. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +0 -24
  141. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +0 -132
  142. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +0 -158
  143. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +0 -167
  144. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -42
  145. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-registration.ts +0 -37
  146. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/service-registry.ts +0 -30
  147. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/structure.ts +0 -8
@@ -1,8 +1,7 @@
1
1
  // LSP settings registration for the code-intelligence umbrella extension.
2
2
  //
3
- // Always-on LSP policy: the global `lsp.enabled` and `lsp.active` keys
4
- // are deprecated and ignored. Per-language disable via
5
- // `lsp.servers.<language>.enabled: false` is the only supported opt-out.
3
+ // Per-language disable via `lsp.servers.<language>.enabled: false` is the
4
+ // supported opt-out.
6
5
  //
7
6
  // Registered fields:
8
7
  // - exclude: stringList
@@ -21,11 +20,9 @@ import {
21
20
  } from "@mrclrchtr/supi-core/settings";
22
21
  import { type LspSettings, loadConfig } from "@mrclrchtr/supi-lsp/api";
23
22
 
24
- const LSP_DEFAULTS: LspSettings = {
25
- enabled: true,
26
- active: [],
27
- exclude: [],
28
- };
23
+ const LSP_DEFAULTS = {
24
+ exclude: [] as string[],
25
+ } satisfies LspSettings;
29
26
 
30
27
  /** Discover configured servers from the defaults + effective LSP config. */
31
28
  function getConfiguredServers(cwd: string): string[] {
@@ -153,7 +150,7 @@ export function registerLspSettings(pi: ExtensionAPI): void {
153
150
  kind: "stringList" as const,
154
151
  key: "exclude",
155
152
  label: "Exclude Patterns",
156
- description: "Gitignore patterns to suppress LSP diagnostics (comma-separated)",
153
+ description: "Gitignore patterns for automatic LSP workspace work (comma-separated)",
157
154
  },
158
155
  {
159
156
  kind: "custom" as const,
@@ -22,7 +22,6 @@ export interface LspAdapterState {
22
22
  controller: LspRuntimeController | null;
23
23
  providerLease: WorkspaceProviderHostLease | null;
24
24
  inspector: LspInspectorState;
25
- lspActive: boolean;
26
25
  /** Snapshot of workspace sentinel files (package.json, tsconfig, lockfiles) for change detection. */
27
26
  sentinelSnapshot: Map<string, number>;
28
27
  /** Fired for runtime lifecycle changes that require a footer render. */
@@ -34,7 +33,6 @@ export function createLspAdapterState(): LspAdapterState {
34
33
  controller: null,
35
34
  providerLease: null,
36
35
  inspector: { handle: null, close: null },
37
- lspActive: false,
38
36
  sentinelSnapshot: new Map(),
39
37
  stateChanges: new EventTarget(),
40
38
  };
@@ -1,7 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { getDefaultWorkspaceRuntime } from "@mrclrchtr/supi-code-runtime/api";
4
- import { LspRuntimeController, scanWorkspaceSentinels } from "@mrclrchtr/supi-lsp/api";
4
+ import { LspRuntimeController } from "@mrclrchtr/supi-lsp/api";
5
5
  import { TreeSitterRuntimeController } from "@mrclrchtr/supi-tree-sitter/api";
6
6
 
7
7
  const HOSTS = Symbol.for("supi-code-intelligence/workspace-provider-hosts");
@@ -77,7 +77,7 @@ class WorkspaceProviderHost {
77
77
  lspController: projectTrusted ? this.#lsp : null,
78
78
  sentinelSnapshot:
79
79
  projectTrusted && this.#lsp?.kind === "ready"
80
- ? scanWorkspaceSentinels(this.cwd)
80
+ ? (this.#lsp.workspaceRuntime?.scanWorkspaceSentinels() ?? new Map())
81
81
  : new Map(),
82
82
  release: async () => {
83
83
  if (released) return;
@@ -0,0 +1,43 @@
1
+ import type { HealthDiagnosticObservation } from "../../session/health-types.ts";
2
+
3
+ /** Render the tsconfig coverage result for a single-file health request. */
4
+ export function renderFileScopeStatus(
5
+ lines: string[],
6
+ observation: Extract<
7
+ HealthDiagnosticObservation,
8
+ { kind: "completed" | "partial" | "unavailable" }
9
+ >,
10
+ cwd: string,
11
+ ): void {
12
+ if (observation.scope.kind !== "file" || !observation.scopeStatus) return;
13
+
14
+ const decision = observation.scopeStatus;
15
+ switch (decision.status) {
16
+ case "included":
17
+ lines.push(
18
+ `**Tsconfig**: covered by ${formatConfigPath(decision.configPath, cwd)} — part of workspace diagnostics.`,
19
+ );
20
+ return;
21
+ case "excluded":
22
+ lines.push(
23
+ `**Tsconfig**: NOT covered by ${formatConfigPath(decision.configPath, cwd)} — not part of workspace diagnostics.`,
24
+ );
25
+ return;
26
+ case "no-config":
27
+ lines.push("**Tsconfig**: no tsconfig.json or jsconfig.json found — nothing filtered.");
28
+ return;
29
+ case "out-of-tree":
30
+ lines.push("**Tsconfig**: outside the project root — not part of workspace diagnostics.");
31
+ }
32
+ }
33
+
34
+ /** Format a config path relative to the current workspace. */
35
+ function formatConfigPath(configPath: string | null, cwd: string): string {
36
+ if (!configPath) return "no config";
37
+ return `\`${makeRelative(cwd, configPath)}\``;
38
+ }
39
+
40
+ /** Format a workspace path without exposing an absolute root. */
41
+ export function makeRelative(cwd: string, file: string): string {
42
+ return file.startsWith(cwd) ? file.slice(cwd.length + 1) : file;
43
+ }
@@ -1,8 +1,8 @@
1
1
  export const toolDescription =
2
- "Report live diagnostics as observations, language-server status, and final semantic health state. Tracked-file snapshots do not prove workspace completeness; server inventory is workspace-wide.";
2
+ "Report live diagnostics as observations, language-server route status, and final semantic health state. Tracked-file snapshots do not prove workspace completeness; server inventory and route issue counts are workspace-wide. Server-only calls are passive.";
3
3
 
4
4
  export const promptSnippet = "live workspace health observations";
5
5
 
6
6
  export const promptGuidelines = [
7
- "Use code_health with refresh:true before relying on potentially stale diagnostics.",
7
+ "Use code_health with refresh:true before relying on potentially stale diagnostics; explicit refresh can recover crashed routes with tracked files in scope.",
8
8
  ];
@@ -6,9 +6,20 @@
6
6
  */
7
7
 
8
8
  import type { CapabilityWarningReport } from "../../analysis/capability/capability-warnings.ts";
9
+ import {
10
+ formatProjectServerRoot,
11
+ formatProjectServerRouteSummary,
12
+ formatProjectServerStatusReason,
13
+ } from "../../analysis/health/server-status.ts";
14
+ import { makeRelative, renderFileScopeStatus } from "./file-scope-markdown.ts";
15
+ import {
16
+ formatProcessCrashRecovery,
17
+ formatRefreshElapsed,
18
+ formatStaleDiagnosticRestarts as formatStaleDiagnosticRestartsText,
19
+ isFileReadinessPending,
20
+ } from "./refresh-outcome.ts";
9
21
  import type {
10
22
  HealthData,
11
- HealthDiagnosticObservation,
12
23
  HealthDiagnosticScope,
13
24
  HealthRefreshAttempt,
14
25
  HealthResultAssembly,
@@ -28,7 +39,9 @@ export function renderHealthResult(result: HealthResultAssembly, cwd: string): s
28
39
  renderRefreshStatus(lines, data, hasDiagnostics, cwd);
29
40
  if (hasDiagnostics) renderDiagnosticsSection(lines, data, cwd);
30
41
  if (semanticRequested) renderCapabilityWarningsSection(lines, result.details.capabilityWarnings);
31
- if (hasSection("servers")) renderServersSection(lines, data, sectionStatus(result, "servers"));
42
+ if (hasSection("servers")) {
43
+ renderServersSection(lines, data, sectionStatus(result, "servers"), cwd);
44
+ }
32
45
  return lines.join("\n");
33
46
  }
34
47
 
@@ -41,19 +54,25 @@ function renderRefreshStatus(
41
54
  if (!hasDiagnostics) return;
42
55
 
43
56
  switch (data.refresh.kind) {
44
- case "completed":
57
+ case "completed": {
58
+ const fileReadinessPending = isFileReadinessPending(data.refresh, data.semanticState);
45
59
  lines.push(
46
- `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh))}`,
60
+ `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending))}`,
47
61
  );
48
62
  lines.push(`**Stale assessment**: ${asSentence(staleAssessmentText(data.refresh))}`);
49
63
  lines.push("");
50
64
  return;
65
+ }
51
66
  case "failed": {
52
67
  const evidence = data.refresh.diagnosticEvidence
53
68
  ? `; ${formatDiagnosticEvidence(data.refresh.diagnosticEvidence)}`
54
69
  : "";
70
+ const processCrashRecovery = data.refresh.processCrashRecovery
71
+ ? formatProcessCrashRecovery(data.refresh.processCrashRecovery)
72
+ : null;
73
+ const staleRestart = formatStaleDiagnosticRestarts(data.refresh);
55
74
  lines.push(
56
- `**${refreshAttemptLabel(data.refresh)}**: failed — ${data.refresh.reason}${evidence}`,
75
+ `**${refreshAttemptLabel(data.refresh)}**: failed — ${data.refresh.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`,
57
76
  );
58
77
  lines.push("");
59
78
  return;
@@ -77,14 +96,27 @@ function renderRefreshStatus(
77
96
 
78
97
  function completedRefreshText(
79
98
  attempt: Extract<HealthRefreshAttempt, { kind: "completed" }>,
99
+ fileReadinessPending = attempt.fileReadiness === "pending",
80
100
  ): string {
81
- const base =
82
- attempt.attemptedActiveClients === 0 && attempt.restartedClients === 0
101
+ const processCrashRecovery = formatProcessCrashRecovery(attempt.processCrashRecovery);
102
+ const noOp =
103
+ !fileReadinessPending &&
104
+ attempt.attemptedActiveClients === 0 &&
105
+ attempt.restartedClients === 0 &&
106
+ processCrashRecovery === null;
107
+ const base = fileReadinessPending
108
+ ? "readiness pending — LSP may still be warming; retry shortly"
109
+ : noOp
83
110
  ? "completed no-op — no active clients were targeted"
84
- : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted, ${attempt.restartedClients} restarted`;
111
+ : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted`;
112
+ const staleRestart = noOp ? null : formatStaleDiagnosticRestarts(attempt);
113
+ const outcome = [staleRestart, processCrashRecovery].filter(
114
+ (value): value is string => value !== null,
115
+ );
116
+ const withOutcome = outcome.length > 0 ? `${base}; ${outcome.join("; ")}` : base;
85
117
  return attempt.operationScope === "workspace-runtime"
86
- ? `${base}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
87
- : base;
118
+ ? `${withOutcome}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
119
+ : withOutcome;
88
120
  }
89
121
 
90
122
  function refreshAttemptLabel(attempt: Pick<HealthRefreshAttempt, "operationScope">): string {
@@ -110,7 +142,7 @@ function renderLastAttempt(lines: string[], attempt: HealthRefreshAttempt, cwd:
110
142
  const outcome = refreshAttemptOutcome(attempt);
111
143
  const label = refreshAttemptLabel(attempt);
112
144
  lines.push(
113
- `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
145
+ `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
114
146
  );
115
147
  }
116
148
 
@@ -118,24 +150,35 @@ function renderRetainedAttempt(lines: string[], attempt: HealthRefreshAttempt, c
118
150
  const label =
119
151
  attempt.operationScope === "file-runtime" ? "File LSP maintenance" : "Diagnostic refresh";
120
152
  lines.push(
121
- `**${label}**: not requested for this call. Previous attempt ${asSentence(refreshAttemptOutcome(attempt))} Started ${formatElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
153
+ `**${label}**: not requested for this call. Previous attempt ${asSentence(refreshAttemptOutcome(attempt))} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
122
154
  );
123
155
  }
124
156
 
125
157
  function refreshAttemptOutcome(attempt: HealthRefreshAttempt): string {
126
- return attempt.kind === "completed"
127
- ? completedRefreshText(attempt)
128
- : `failed — ${attempt.reason}${attempt.diagnosticEvidence ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}` : ""}`;
158
+ if (attempt.kind === "completed") return completedRefreshText(attempt);
159
+ const processCrashRecovery = attempt.processCrashRecovery
160
+ ? formatProcessCrashRecovery(attempt.processCrashRecovery)
161
+ : null;
162
+ const staleRestart = formatStaleDiagnosticRestarts(attempt);
163
+ const evidence = attempt.diagnosticEvidence
164
+ ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
165
+ : "";
166
+ return `failed — ${attempt.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`;
129
167
  }
130
168
 
131
169
  function refreshOperationScopeText(attempt: HealthRefreshAttempt): string {
132
170
  return attempt.operationScope === "file-runtime" ? "file runtime" : "workspace runtime";
133
171
  }
134
172
 
135
- function formatElapsed(milliseconds: number): string {
136
- const seconds = Math.max(0, Math.round(milliseconds / 1_000));
137
- if (seconds < 60) return `${seconds}s ago`;
138
- return `${Math.round(seconds / 60)}m ago`;
173
+ function formatStaleDiagnosticRestarts(attempt: {
174
+ kind?: HealthRefreshAttempt["kind"];
175
+ operationScope: HealthRefreshAttempt["operationScope"];
176
+ restartedClients?: number;
177
+ }): string | null {
178
+ if (attempt.operationScope !== "workspace-runtime" || attempt.restartedClients === undefined) {
179
+ return null;
180
+ }
181
+ return formatStaleDiagnosticRestartsText(attempt.restartedClients);
139
182
  }
140
183
 
141
184
  function renderCapabilityWarningsSection(
@@ -158,7 +201,8 @@ function renderCapabilityWarningsSection(
158
201
  function renderStatusLine(lines: string[], data: HealthData, semanticRequested: boolean): void {
159
202
  if (!semanticRequested) return;
160
203
 
161
- lines.push(`**LSP**: ${displaySemanticStatus(data)}`);
204
+ const routeSummary = formatProjectServerRouteSummary(data.servers);
205
+ lines.push(`**LSP**: ${displaySemanticStatus(data)}${routeSummary ? ` — ${routeSummary}` : ""}`);
162
206
  if (data.structuralStatus) lines.push(`**Structural**: ${data.structuralStatus}`);
163
207
  lines.push("");
164
208
  }
@@ -185,49 +229,6 @@ function renderDiagnosticsSection(lines: string[], data: HealthData, cwd: string
185
229
  lines.push("");
186
230
  }
187
231
 
188
- /**
189
- * Render the tsconfig coverage verdict for a single-file health request.
190
- *
191
- * Written in plain vocabulary (no basis terms) so a reader without tsconfig
192
- * background can act on it: the sentence names the deciding config and states
193
- * whether the file's errors are part of workspace diagnostics. The structured
194
- * decision basis stays in the diagnostics.scope debug event.
195
- */
196
- function renderFileScopeStatus(
197
- lines: string[],
198
- observation: Extract<
199
- HealthDiagnosticObservation,
200
- { kind: "completed" | "partial" | "unavailable" }
201
- >,
202
- cwd: string,
203
- ): void {
204
- if (observation.scope.kind !== "file" || !observation.scopeStatus) return;
205
-
206
- const decision = observation.scopeStatus;
207
- switch (decision.status) {
208
- case "included":
209
- lines.push(
210
- `**Tsconfig**: covered by ${formatConfigPath(decision.configPath, cwd)} — part of workspace diagnostics.`,
211
- );
212
- return;
213
- case "excluded":
214
- lines.push(
215
- `**Tsconfig**: NOT covered by ${formatConfigPath(decision.configPath, cwd)} — not part of workspace diagnostics.`,
216
- );
217
- return;
218
- case "no-config":
219
- lines.push("**Tsconfig**: no tsconfig.json or jsconfig.json found — nothing filtered.");
220
- return;
221
- case "out-of-tree":
222
- lines.push("**Tsconfig**: outside the project root — not part of workspace diagnostics.");
223
- }
224
- }
225
-
226
- function formatConfigPath(configPath: string | null, cwd: string): string {
227
- if (!configPath) return "no config";
228
- return `\`${makeRelative(cwd, configPath)}\``;
229
- }
230
-
231
232
  function formatDiagnosticEvidence(evidence: {
232
233
  requested: number;
233
234
  confirmed: number;
@@ -242,7 +243,14 @@ function renderDiagnosticObservation(lines: string[], data: HealthData, cwd: str
242
243
  const observation = data.diagnostics;
243
244
  if (observation.kind === "not-requested") return;
244
245
  if (observation.kind === "unavailable") {
245
- lines.push(`Diagnostics unavailable — ${asSentence(observation.reason)}`);
246
+ if (
247
+ observation.scope.kind === "file" &&
248
+ isFileReadinessPending(currentRefreshAttempt(data), data.semanticState)
249
+ ) {
250
+ lines.push("Diagnostics pending — LSP may still be warming; retry shortly.");
251
+ } else {
252
+ lines.push(`Diagnostics unavailable — ${asSentence(observation.reason)}`);
253
+ }
246
254
  return;
247
255
  }
248
256
  if (observation.kind === "partial") {
@@ -261,6 +269,10 @@ function renderDiagnosticObservation(lines: string[], data: HealthData, cwd: str
261
269
  renderDiagnosticEntries(lines, data, cwd);
262
270
  }
263
271
 
272
+ function currentRefreshAttempt(data: HealthData): HealthRefreshAttempt | null {
273
+ return data.refresh.kind === "completed" || data.refresh.kind === "failed" ? data.refresh : null;
274
+ }
275
+
264
276
  function asSentence(text: string): string {
265
277
  return /[.!?]$/.test(text) ? text : `${text}.`;
266
278
  }
@@ -316,6 +328,7 @@ function renderServersSection(
316
328
  lines: string[],
317
329
  data: HealthData,
318
330
  status: "complete" | "partial" | "unavailable" | undefined,
331
+ cwd: string,
319
332
  ): void {
320
333
  lines.push("### Servers");
321
334
  lines.push("");
@@ -329,8 +342,11 @@ function renderServersSection(
329
342
  } else {
330
343
  for (const server of data.servers) {
331
344
  const statusIcon = server.status === "running" ? "✓" : "✗";
345
+ const reason = server.statusReason
346
+ ? ` — ${formatProjectServerStatusReason(server.statusReason)}`
347
+ : "";
332
348
  lines.push(
333
- `- ${statusIcon} **${server.name}** (${server.fileTypes.join(", ")}) — ${server.status}`,
349
+ `- ${statusIcon} **${server.name}** @ \`${formatProjectServerRoot(cwd, server.root)}\` (${server.fileTypes.join(", ")}) — ${server.status}${reason}`,
334
350
  );
335
351
  }
336
352
  }
@@ -359,7 +375,3 @@ function formatDiagnosticScope(scope: HealthDiagnosticScope, cwd: string): strin
359
375
  function plural(count: number): string {
360
376
  return count === 1 ? "" : "s";
361
377
  }
362
-
363
- function makeRelative(cwd: string, file: string): string {
364
- return file.startsWith(cwd) ? file.slice(cwd.length + 1) : file;
365
- }
@@ -0,0 +1,46 @@
1
+ import type { ProcessCrashRecoverySummary } from "@mrclrchtr/supi-lsp/api";
2
+ import type { SemanticHealthState } from "../../session/health-types.ts";
3
+
4
+ /** Report whether a file health result is waiting for semantic readiness. */
5
+ export function isFileReadinessPending(
6
+ attempt: { operationScope?: string; fileReadiness?: string } | null | undefined,
7
+ semanticState: SemanticHealthState | null | undefined,
8
+ ): boolean {
9
+ if (attempt?.operationScope !== undefined) {
10
+ return (
11
+ attempt.operationScope === "file-runtime" &&
12
+ (attempt.fileReadiness === "pending" ||
13
+ (attempt.fileReadiness === undefined && semanticState?.kind === "pending"))
14
+ );
15
+ }
16
+ return semanticState?.kind === "pending";
17
+ }
18
+
19
+ /** Format the outcome of stale-diagnostic client restarts. */
20
+ export function formatStaleDiagnosticRestarts(restartedClients: number): string {
21
+ return `stale diagnostic restarts: ${restartedClients} client${plural(restartedClients)} restarted`;
22
+ }
23
+
24
+ /** Format a non-empty process-crash route recovery outcome. */
25
+ export function formatProcessCrashRecovery(
26
+ summary: ProcessCrashRecoverySummary | null | undefined,
27
+ ): string | null {
28
+ if (
29
+ !summary ||
30
+ (summary.attemptedRoutes === 0 && summary.recoveredRoutes === 0 && summary.failedRoutes === 0)
31
+ ) {
32
+ return null;
33
+ }
34
+ return `process-crash recovery: ${summary.recoveredRoutes} route${plural(summary.recoveredRoutes)} recovered (${summary.attemptedRoutes} attempted, ${summary.failedRoutes} failed)`;
35
+ }
36
+
37
+ /** Format the age of a retained refresh attempt for a status line. */
38
+ export function formatRefreshElapsed(milliseconds: number): string {
39
+ const seconds = Math.max(0, Math.round(milliseconds / 1_000));
40
+ if (seconds < 60) return `${seconds}s ago`;
41
+ return `${Math.round(seconds / 60)}m ago`;
42
+ }
43
+
44
+ function plural(count: number): string {
45
+ return count === 1 ? "" : "s";
46
+ }
@@ -1,3 +1,11 @@
1
+ import type { ProcessCrashRecoverySummary } from "@mrclrchtr/supi-lsp/api";
2
+ import {
3
+ formatProcessCrashRecovery,
4
+ formatStaleDiagnosticRestarts,
5
+ isFileReadinessPending,
6
+ } from "./refresh-outcome.ts";
7
+ import { readSemanticHealthState } from "./semantic-state.ts";
8
+
1
9
  /** Render current and retained health refresh status for the TUI. */
2
10
 
3
11
  export function readRefreshStatus(data: Record<string, unknown> | null): string | null {
@@ -5,25 +13,10 @@ export function readRefreshStatus(data: Record<string, unknown> | null): string
5
13
  if (!refresh) return null;
6
14
 
7
15
  switch (refresh.kind) {
8
- case "completed": {
9
- const attempted = readNumber(refresh.attemptedActiveClients);
10
- const restarted = readNumber(refresh.restartedClients);
11
- const stale = readRecord(refresh.staleAssessment);
12
- const noOp = attempted === 0 && restarted === 0;
13
- const label = refreshAttemptLabel(refresh);
14
- const base = noOp
15
- ? `${label} completed no-op`
16
- : `${label} completed: ${attempted} clients targeted, ${restarted} restarted`;
17
- const evidence = formatDiagnosticEvidence(readRecord(refresh.diagnosticEvidence));
18
- const withEvidence = evidence ? `${base}; ${evidence}` : base;
19
- return stale?.suspected === true
20
- ? `${withEvidence}; stale pattern suspected in ${readNumber(stale.matchedFileCount)} files`
21
- : withEvidence;
22
- }
23
- case "failed": {
24
- const evidence = formatDiagnosticEvidence(readRecord(refresh.diagnosticEvidence));
25
- return `${refreshAttemptLabel(refresh)} failed${typeof refresh.reason === "string" ? `: ${refresh.reason}` : ""}${evidence ? `; ${evidence}` : ""}`;
26
- }
16
+ case "completed":
17
+ return formatCompletedRefreshStatus(refresh, isPendingFileReadiness(data));
18
+ case "failed":
19
+ return formatFailedRefreshStatus(refresh);
27
20
  case "not-attempted":
28
21
  return `refresh attempt not started${formatLastRefreshSuffix(refresh)}`;
29
22
  case "not-requested":
@@ -33,6 +26,89 @@ export function readRefreshStatus(data: Record<string, unknown> | null): string
33
26
  }
34
27
  }
35
28
 
29
+ function formatCompletedRefreshStatus(
30
+ refresh: Record<string, unknown>,
31
+ fileReadinessPending: boolean,
32
+ ): string {
33
+ const attempted = readNumber(refresh.attemptedActiveClients);
34
+ const restarted = readNumber(refresh.restartedClients);
35
+ const processCrashRecovery = formatProcessCrashRecovery(
36
+ readProcessCrashRecovery(refresh.processCrashRecovery),
37
+ );
38
+ const noOp =
39
+ !fileReadinessPending && attempted === 0 && restarted === 0 && processCrashRecovery === null;
40
+ const label = refreshAttemptLabel(refresh);
41
+ const base = fileReadinessPending
42
+ ? `${label} waiting — LSP may still be warming; retry shortly`
43
+ : noOp
44
+ ? `${label} completed no-op`
45
+ : `${label} completed: ${attempted} clients targeted`;
46
+ const outcomes = formatRefreshOutcomes(refresh, restarted, processCrashRecovery, noOp);
47
+ const withOutcome = outcomes.length > 0 ? `${base}; ${outcomes.join("; ")}` : base;
48
+ const evidence = formatDiagnosticEvidence(readRecord(refresh.diagnosticEvidence));
49
+ const withEvidence = evidence ? `${withOutcome}; ${evidence}` : withOutcome;
50
+ const stale = readRecord(refresh.staleAssessment);
51
+ return stale?.suspected === true
52
+ ? `${withEvidence}; stale pattern suspected in ${readNumber(stale.matchedFileCount)} files`
53
+ : withEvidence;
54
+ }
55
+
56
+ function formatFailedRefreshStatus(refresh: Record<string, unknown>): string {
57
+ const processCrashRecovery = formatProcessCrashRecovery(
58
+ readProcessCrashRecovery(refresh.processCrashRecovery),
59
+ );
60
+ const staleRestart = formatStaleDiagnosticRestartsForRecord(refresh);
61
+ const evidence = formatDiagnosticEvidence(readRecord(refresh.diagnosticEvidence));
62
+ const reason = typeof refresh.reason === "string" ? `: ${refresh.reason}` : "";
63
+ return `${refreshAttemptLabel(refresh)} failed${reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence ? `; ${evidence}` : ""}`;
64
+ }
65
+
66
+ function formatRefreshOutcomes(
67
+ refresh: Record<string, unknown>,
68
+ restarted: number,
69
+ processCrashRecovery: string | null,
70
+ noOp: boolean,
71
+ ): string[] {
72
+ const outcomes: string[] = [];
73
+ if (refresh.operationScope === "workspace-runtime" && !noOp) {
74
+ outcomes.push(formatStaleDiagnosticRestarts(restarted));
75
+ }
76
+ if (processCrashRecovery) outcomes.push(processCrashRecovery);
77
+ return outcomes;
78
+ }
79
+
80
+ /** Render only the material current recovery outcome for compact TUI chrome. */
81
+ export function readCompactRefreshStatus(data: Record<string, unknown> | null): string | null {
82
+ const refresh = readRecord(data?.refresh);
83
+ if (!refresh || (refresh.kind !== "completed" && refresh.kind !== "failed")) return null;
84
+
85
+ const processCrashRecovery = formatProcessCrashRecovery(
86
+ readProcessCrashRecovery(refresh.processCrashRecovery),
87
+ );
88
+ if (refresh.kind === "failed") {
89
+ const staleRestart = formatStaleDiagnosticRestartsForRecord(refresh);
90
+ return (
91
+ [staleRestart, processCrashRecovery]
92
+ .filter((value): value is string => value !== null)
93
+ .join("; ") || null
94
+ );
95
+ }
96
+ const fileReadinessPending = isPendingFileReadiness(data);
97
+ if (
98
+ !processCrashRecovery &&
99
+ readNumber(refresh.restartedClients) === 0 &&
100
+ !fileReadinessPending
101
+ ) {
102
+ return null;
103
+ }
104
+
105
+ const staleRestart = formatStaleDiagnosticRestartsForRecord(refresh);
106
+ const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
107
+ return [staleRestart, processCrashRecovery, readiness]
108
+ .filter((value): value is string => value !== null)
109
+ .join("; ");
110
+ }
111
+
36
112
  export function readPreviousRefreshStatus(data: Record<string, unknown> | null): string | null {
37
113
  const refresh = readRecord(data?.refresh);
38
114
  if (!refresh || (refresh.kind !== "not-requested" && refresh.kind !== "not-attempted")) {
@@ -41,11 +117,23 @@ export function readPreviousRefreshStatus(data: Record<string, unknown> | null):
41
117
  const attempt = readRecord(refresh.lastAttempt);
42
118
  if (!attempt) return null;
43
119
  const evidence = formatCompactDiagnosticEvidence(readRecord(attempt.diagnosticEvidence));
120
+ const processCrashRecovery = formatProcessCrashRecovery(
121
+ readProcessCrashRecovery(attempt.processCrashRecovery),
122
+ );
123
+ const staleRestart = formatStaleDiagnosticRestartsForRecord(attempt);
124
+ const fileReadinessPending =
125
+ attempt.kind === "completed" &&
126
+ isFileReadinessPending(attempt, readSemanticHealthState(data?.semanticState));
127
+ const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
128
+ const outcome = [readiness, staleRestart, processCrashRecovery, evidence]
129
+ .filter((value): value is string => value !== null)
130
+ .join("; ");
44
131
  if (attempt.kind === "failed") {
45
- return `last ${refreshAttemptLabel(attempt)} failed${evidence ? ` (${evidence})` : ""}`;
132
+ return `last ${refreshAttemptLabel(attempt)} failed${outcome ? ` (${outcome})` : ""}`;
46
133
  }
47
134
  if (attempt.kind === "completed") {
48
- return `last ${refreshAttemptLabel(attempt)} completed${evidence ? ` (${evidence})` : ""}`;
135
+ const status = fileReadinessPending ? "waiting" : "completed";
136
+ return `last ${refreshAttemptLabel(attempt)} ${status}${outcome ? ` (${outcome})` : ""}`;
49
137
  }
50
138
  return null;
51
139
  }
@@ -54,16 +142,45 @@ function refreshAttemptLabel(attempt: Record<string, unknown>): string {
54
142
  return attempt.operationScope === "file-runtime" ? "file maintenance attempt" : "refresh attempt";
55
143
  }
56
144
 
145
+ function formatStaleDiagnosticRestartsForRecord(attempt: Record<string, unknown>): string | null {
146
+ if (
147
+ attempt.operationScope !== "workspace-runtime" ||
148
+ typeof attempt.restartedClients !== "number"
149
+ ) {
150
+ return null;
151
+ }
152
+ const processCrashRecovery = formatProcessCrashRecovery(
153
+ readProcessCrashRecovery(attempt.processCrashRecovery),
154
+ );
155
+ const noOp =
156
+ attempt.kind === "completed" &&
157
+ readNumber(attempt.attemptedActiveClients) === 0 &&
158
+ attempt.restartedClients === 0 &&
159
+ processCrashRecovery === null;
160
+ return noOp ? null : formatStaleDiagnosticRestarts(attempt.restartedClients);
161
+ }
162
+
57
163
  function formatLastRefreshSuffix(refresh: Record<string, unknown>): string {
58
164
  const attempt = readRecord(refresh.lastAttempt);
59
165
  if (!attempt) return "";
60
166
  const evidence = formatDiagnosticEvidence(readRecord(attempt.diagnosticEvidence));
167
+ const staleRestart = formatStaleDiagnosticRestartsForRecord(attempt);
168
+ const processCrashRecovery = formatProcessCrashRecovery(
169
+ readProcessCrashRecovery(attempt.processCrashRecovery),
170
+ );
171
+ const fileReadinessPending =
172
+ attempt.kind === "completed" && isFileReadinessPending(attempt, null);
173
+ const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
174
+ const outcome = [readiness, staleRestart, processCrashRecovery, evidence]
175
+ .filter((value): value is string => value !== null)
176
+ .join("; ");
61
177
  if (attempt.kind === "failed") {
62
178
  const reason = typeof attempt.reason === "string" ? `: ${attempt.reason}` : "";
63
- return `; last ${refreshAttemptLabel(attempt)} failed${reason}${evidence ? `; ${evidence}` : ""}`;
179
+ return `; last ${refreshAttemptLabel(attempt)} failed${reason}${outcome ? `; ${outcome}` : ""}`;
64
180
  }
65
181
  if (attempt.kind === "completed") {
66
- return `; last ${refreshAttemptLabel(attempt)} completed${evidence ? `; ${evidence}` : ""}`;
182
+ const status = fileReadinessPending ? "waiting" : "completed";
183
+ return `; last ${refreshAttemptLabel(attempt)} ${status}${outcome ? `; ${outcome}` : ""}`;
67
184
  }
68
185
  return "";
69
186
  }
@@ -80,6 +197,32 @@ function formatDiagnosticEvidence(evidence: Record<string, unknown> | null): str
80
197
  return `${counts[0]} requested, ${counts[1]} confirmed, ${counts[2]} unconfirmed, ${counts[3]} failed, ${counts[4]} removed`;
81
198
  }
82
199
 
200
+ export function isPendingFileReadiness(data: Record<string, unknown> | null): boolean {
201
+ const observation = readRecord(data?.diagnosticObservation);
202
+ const scope = readRecord(observation?.scope);
203
+ if (scope?.kind !== "file") return false;
204
+
205
+ const refresh = readRecord(data?.refresh);
206
+ const attempt = refresh && typeof refresh.operationScope === "string" ? refresh : null;
207
+ return isFileReadinessPending(attempt, readSemanticHealthState(data?.semanticState));
208
+ }
209
+
210
+ function readProcessCrashRecovery(value: unknown): ProcessCrashRecoverySummary | null {
211
+ const summary = readRecord(value);
212
+ if (!summary) return null;
213
+ const attemptedRoutes = summary.attemptedRoutes;
214
+ const recoveredRoutes = summary.recoveredRoutes;
215
+ const failedRoutes = summary.failedRoutes;
216
+ if (
217
+ !isEvidenceCount(attemptedRoutes) ||
218
+ !isEvidenceCount(recoveredRoutes) ||
219
+ !isEvidenceCount(failedRoutes)
220
+ ) {
221
+ return null;
222
+ }
223
+ return { attemptedRoutes, recoveredRoutes, failedRoutes };
224
+ }
225
+
83
226
  function readEvidenceCounts(evidence: Record<string, unknown> | null): number[] | null {
84
227
  if (!evidence) return null;
85
228
  const counts = [