@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
@@ -1,28 +1,34 @@
1
- /** TUI renderer for code_graph. */
1
+ /** Human transcript renderer for code_graph. */
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { type Component, Container, Spacer, Text } from "@earendil-works/pi-tui";
4
+ import { renderCandidateSelection } from "../../ui/tui/candidate-selection.ts";
4
5
  import {
5
6
  buildSimpleCompact,
6
- buildSimpleHeader,
7
7
  type EvidenceEntry,
8
8
  formatCallPath,
9
9
  formatCallValue,
10
10
  type ResultOptios,
11
+ readEvidenceEntries,
11
12
  renderDomainError,
12
- renderDomainResult,
13
- renderEvidenceLines,
14
13
  renderExecutionError,
15
14
  renderMarkdownDetail,
16
15
  renderPartial,
17
- renderStructuredDetailBody,
18
16
  renderToolDisplaySections,
19
17
  renderTruncationDisclosure,
20
18
  type ToolRendererContext,
21
19
  type ToolResult,
22
20
  } from "../../ui/tui/common.ts";
21
+ import {
22
+ MAX_TUI_DISPLAY_ITEMS,
23
+ readToolDisplaySections,
24
+ truncateDisplayText,
25
+ } from "../result/display.ts";
26
+ import type { ToolDisplaySection } from "../result/types.ts";
27
+ import type { GraphFileGroup } from "./details.ts";
23
28
  import type { CodeGraphToolParams, GraphRelation } from "./execute.ts";
29
+ import { compactGraphLabel, formatGraphEvidence, formatGraphRange } from "./format.ts";
24
30
 
25
- /** Render the compact code_graph call header. */
31
+ /** Render the compact call header from the requested selector. */
26
32
  export function renderGraphCall(
27
33
  args: unknown,
28
34
  theme: Theme,
@@ -30,84 +36,240 @@ export function renderGraphCall(
30
36
  ): Text {
31
37
  const params = (args ?? {}) as Partial<CodeGraphToolParams>;
32
38
  const relations: GraphRelation[] = Array.isArray(params.relations)
33
- ? (params.relations as GraphRelation[])
39
+ ? params.relations
34
40
  : ["references"];
35
- const relationLabel = formatRelations(relations);
36
-
37
- let content = theme.fg("toolTitle", "code_graph");
38
- if (relationLabel) content += ` ${theme.fg("accent", relationLabel)}`;
39
-
41
+ const label = formatRelations(relations);
40
42
  const target = formatTarget(params.target);
41
- if (target) content += ` ${theme.fg("muted", target)}`;
42
-
43
- return new Text(content, 0, 0);
43
+ return new Text(
44
+ theme.fg("toolTitle", "code_graph") +
45
+ (label ? ` ${theme.fg("accent", label)}` : "") +
46
+ (target ? ` ${theme.fg("muted", target)}` : ""),
47
+ 0,
48
+ 0,
49
+ );
44
50
  }
45
51
 
46
- /** Render code_graph progress, status, and structured result details. */
52
+ /** Render one structured body, with no duplicate agent Markdown. */
47
53
  export function renderGraphResult(
48
54
  result: ToolResult,
49
55
  options: ResultOptios,
50
56
  theme: Theme,
51
57
  context: ToolRendererContext | undefined,
52
- ): Container | Text {
58
+ ): Component {
53
59
  if (options.isPartial) return renderPartial("Collecting relations…", theme);
54
-
55
- const details =
56
- result.details?.type === "search" ? (result.details.data as Record<string, unknown>) : null;
57
- const executionError = renderExecutionError(context, "code_graph failed", theme);
60
+ const executionError = renderExecutionError(
61
+ result,
62
+ { isError: context?.isError, expanded: options.expanded, label: "code_graph failed" },
63
+ theme,
64
+ );
58
65
  if (executionError) return executionError;
66
+ const candidateSelection = renderCandidateSelection(result, options, theme, "graph.candidates");
67
+ if (candidateSelection) return candidateSelection;
59
68
  const domainError = renderDomainError(result, theme);
60
- if (domainError) return renderDomainResult(result, options, theme, domainError);
69
+ if (domainError) return renderGraphDomainError(result, options.expanded, domainError, theme);
61
70
 
62
- if (!options.expanded) {
63
- const compact = buildSimpleCompact(details ?? undefined, theme);
64
- const truncation = renderTruncationDisclosure(result, theme);
65
- if (!truncation) return compact;
66
- const container = new Container();
67
- container.addChild(compact);
71
+ const data = asRecord(result.details?.data);
72
+ const container = new Container();
73
+ if (result.details?.type === "graph" && data) {
74
+ renderGraphBody(data, result, { container, expanded: options.expanded, theme });
75
+ } else {
76
+ // Older persisted results have only generic search details.
77
+ container.addChild(buildSimpleCompact(data ?? undefined, theme));
78
+ if (options.expanded)
79
+ renderToolDisplaySections(container, result.details?.displaySections, theme);
80
+ }
81
+ const truncation = renderTruncationDisclosure(result, theme);
82
+ if (truncation) {
68
83
  container.addChild(new Spacer(1));
69
84
  container.addChild(truncation);
70
- return container;
71
85
  }
86
+ return container;
87
+ }
72
88
 
73
- const container = new Container();
74
- const header = buildSimpleHeader(details ?? undefined, theme);
75
- if (header) container.addChild(header);
89
+ interface SectionView {
90
+ rel: string;
91
+ label: string;
92
+ unavailable: boolean;
93
+ message: string | null;
94
+ externalCount: number;
95
+ scope: Record<string, unknown> | null;
96
+ fileGroups: GraphFileGroup[];
97
+ }
76
98
 
77
- const evidenceLists = details?.evidenceLists as EvidenceEntry[] | undefined;
78
- if (evidenceLists && evidenceLists.length > 0) {
99
+ function renderGraphBody(
100
+ data: Record<string, unknown>,
101
+ result: ToolResult,
102
+ options: { container: Container; expanded: boolean; theme: Theme },
103
+ ): void {
104
+ const { container, expanded, theme } = options;
105
+ const target =
106
+ typeof data.targetName === "string" ? compactGraphLabel(data.targetName) : "Target";
107
+ const file = typeof data.targetFile === "string" ? truncateDisplayText(data.targetFile) : "";
108
+ container.addChild(new Text(theme.fg("accent", `${target}${file ? ` — ${file}` : ""}`), 0, 0));
109
+ const evidence = readEvidenceEntries(data.evidenceLists);
110
+ const displays = readToolDisplaySections(result.details?.displaySections);
111
+ const sections = readSections(data.sections);
112
+ const summaries: string[] = [];
113
+ for (const section of sections) {
114
+ const display = displays.find((entry) => entry.key === `graph.${section.rel}`);
115
+ const metadata = evidence.find((entry) => entry.key.startsWith(`${section.rel}.`));
116
+ const summary = sectionSummary(section, metadata, display);
117
+ if (!expanded) {
118
+ summaries.push(summary);
119
+ continue;
120
+ }
79
121
  container.addChild(new Spacer(1));
80
- renderEvidenceLines(container, evidenceLists, theme);
122
+ container.addChild(
123
+ new Text(theme.fg(section.unavailable ? "warning" : "accent", summary), 0, 0),
124
+ );
125
+ renderRelationBody(container, { section, display, targetName: data.targetName }, theme);
81
126
  }
127
+ if (!expanded)
128
+ container.addChild(
129
+ new Text(theme.fg("muted", summaries.join(" · ") || "No graph sections"), 0, 0),
130
+ );
131
+ }
82
132
 
83
- renderToolDisplaySections(container, result.details?.displaySections, theme);
84
- renderStructuredDetailBody(container, details ?? undefined, theme);
85
- const truncation = renderTruncationDisclosure(result, theme);
86
- if (truncation) {
87
- container.addChild(new Spacer(1));
88
- container.addChild(truncation);
133
+ function renderRelationBody(
134
+ container: Container,
135
+ input: { section: SectionView; display: ToolDisplaySection | undefined; targetName: unknown },
136
+ theme: Theme,
137
+ ): void {
138
+ const { section, display, targetName } = input;
139
+ if (section.unavailable) {
140
+ if (section.message)
141
+ container.addChild(new Text(theme.fg("warning", truncateDisplayText(section.message)), 0, 0));
142
+ return;
143
+ }
144
+ const scope = scopeLabel(section.scope, targetName);
145
+ if (scope) container.addChild(new Text(theme.fg("dim", scope), 0, 0));
146
+ const rows = display?.lines ?? [];
147
+ let index = 0;
148
+ const groups =
149
+ section.fileGroups.reduce((sum, group) => sum + group.count, 0) === rows.length
150
+ ? section.fileGroups
151
+ : [];
152
+ for (const group of groups) {
153
+ if (index >= rows.length) break;
154
+ container.addChild(new Text(theme.fg("dim", group.file), 0, 0));
155
+ for (const row of rows.slice(index, index + group.count))
156
+ container.addChild(new Text(theme.fg("muted", ` ${row}`), 0, 0));
157
+ index += group.count;
89
158
  }
90
- renderMarkdownDetail(container, result, theme);
159
+ // Older results contain full paths in each row and have no file groups.
160
+ for (const row of rows.slice(index)) container.addChild(new Text(theme.fg("muted", row), 0, 0));
161
+ }
162
+
163
+ function renderGraphDomainError(
164
+ result: ToolResult,
165
+ expanded: boolean,
166
+ domainError: Text,
167
+ theme: Theme,
168
+ ): Container | Text {
169
+ if (!expanded) return domainError;
170
+ const container = new Container();
171
+ container.addChild(domainError);
172
+ const displays = readToolDisplaySections(result.details?.displaySections);
173
+ renderToolDisplaySections(container, displays, theme);
174
+ if (displays.length === 0) renderMarkdownDetail(container, result, theme);
91
175
  return container;
92
176
  }
93
177
 
178
+ function sectionSummary(
179
+ section: SectionView,
180
+ metadata: EvidenceEntry | undefined,
181
+ display: ToolDisplaySection | undefined,
182
+ ): string {
183
+ if (section.unavailable) return `${section.label}: unavailable`;
184
+ if (!metadata) return `${section.label}: evidence unavailable`;
185
+ // The display has its own bounded row count; the underlying evidence total is unchanged.
186
+ const shown = display
187
+ ? { ...metadata, shownCount: display.shownCount, omittedCount: display.omittedCount }
188
+ : metadata;
189
+ return `${section.label}: ${formatGraphEvidence(shown, section.rel === "callees" ? "call sites" : "locations", section.externalCount)}`;
190
+ }
191
+
192
+ function readSections(value: unknown): SectionView[] {
193
+ if (!Array.isArray(value)) return [];
194
+ return value.slice(0, 3).flatMap((item) => {
195
+ const section = asRecord(item);
196
+ if (
197
+ !section ||
198
+ typeof section.rel !== "string" ||
199
+ !["references", "callees", "implements"].includes(section.rel) ||
200
+ (section.source !== "structural" && section.source !== "semantic")
201
+ )
202
+ return [];
203
+ const rel = section.rel;
204
+ const source = section.source;
205
+ const depth =
206
+ rel === "callees" && (section.depth === "direct" || section.depth === "deep")
207
+ ? `, ${section.depth}`
208
+ : "";
209
+ return [
210
+ {
211
+ rel,
212
+ label: `${rel} (${source}${depth})`,
213
+ unavailable: section.status === "unavailable",
214
+ message: typeof section.message === "string" ? section.message : null,
215
+ externalCount: typeof section.externalCount === "number" ? section.externalCount : 0,
216
+ scope: asRecord(section.enclosingScope),
217
+ fileGroups: readFileGroups(section.fileGroups),
218
+ },
219
+ ];
220
+ });
221
+ }
222
+
223
+ function readFileGroups(value: unknown): GraphFileGroup[] {
224
+ if (!Array.isArray(value)) return [];
225
+ const groups: GraphFileGroup[] = [];
226
+ for (const entry of value.slice(0, MAX_TUI_DISPLAY_ITEMS)) {
227
+ const group = asRecord(entry);
228
+ if (
229
+ !group ||
230
+ typeof group.file !== "string" ||
231
+ typeof group.count !== "number" ||
232
+ !Number.isInteger(group.count) ||
233
+ group.count < 1 ||
234
+ group.count > MAX_TUI_DISPLAY_ITEMS
235
+ )
236
+ return [];
237
+ groups.push({ file: truncateDisplayText(group.file), count: group.count });
238
+ }
239
+ return groups;
240
+ }
241
+
242
+ function scopeLabel(scope: Record<string, unknown> | null, targetName: unknown): string | null {
243
+ if (!scope || typeof scope.startLine !== "number" || typeof scope.endLine !== "number")
244
+ return null;
245
+ const name =
246
+ typeof scope.name === "string" && scope.name !== targetName
247
+ ? `${compactGraphLabel(scope.name)} — `
248
+ : "";
249
+ const range = formatGraphRange({ startLine: scope.startLine, endLine: scope.endLine });
250
+ return `Scope: ${name}${range}; source expressions, not resolved symbols`;
251
+ }
252
+
253
+ function asRecord(value: unknown): Record<string, unknown> | null {
254
+ return value !== null && typeof value === "object" && !Array.isArray(value)
255
+ ? (value as Record<string, unknown>)
256
+ : null;
257
+ }
258
+
94
259
  function formatRelations(relations: readonly GraphRelation[]): string {
95
- if (relations.length === 0) return "";
96
- if (relations.length === 1) return `→ ${relations[0]}`;
97
- if (relations.length <= 3) return `→ ${relations.join(", ")}`;
98
- return `→ ${relations.slice(0, 2).join(", ")} +${relations.length - 2}`;
260
+ return relations.length ? `→ ${relations.slice(0, 3).join(", ")}` : "";
99
261
  }
100
262
 
101
263
  function formatTarget(target: unknown): string {
102
- if (!target || typeof target !== "object") return "";
103
- const record = target as Record<string, unknown>;
104
-
105
- if (record.symbol && typeof record.symbol === "object") {
106
- const query = formatCallValue((record.symbol as Record<string, unknown>).query);
264
+ const record = asRecord(target);
265
+ if (!record) return "";
266
+ const symbol = asRecord(record.symbol);
267
+ if (symbol) {
268
+ const query = formatCallValue(symbol.query);
107
269
  return query ? `of ${query}` : "";
108
270
  }
109
- if (record.anchor && typeof record.anchor === "object") {
110
- const anchor = record.anchor as Record<string, unknown>;
271
+ const anchor = asRecord(record.anchor);
272
+ if (anchor) {
111
273
  const file = formatCallPath(anchor.file);
112
274
  const line = typeof anchor.line === "number" ? `:${anchor.line}` : "";
113
275
  return file ? `at ${file}${line}` : "";
@@ -1,8 +1,6 @@
1
1
  export const toolDescription =
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.";
2
+ "Report live diagnostics and language-server health. Use refresh:true when you need fresh diagnostic evidence; without it, diagnostics may be a tracked-file snapshot. Diagnostic snapshots do not prove that the whole workspace is clean; server inventory and route-status counts are always workspace-wide.";
3
3
 
4
- export const promptSnippet = "live workspace health observations";
4
+ export const promptSnippet = "check live diagnostics and language-server health";
5
5
 
6
- export const promptGuidelines = [
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
- ];
6
+ export const promptGuidelines: string[] = [];
@@ -1,10 +1,4 @@
1
- /**
2
- * Markdown renderer for code_health results.
3
- *
4
- * Renders structured health data from the code_health executor into
5
- * readable markdown sections keyed by requested `include` values.
6
- */
7
-
1
+ /** Markdown renderer for code_health results. */
8
2
  import type { CapabilityWarningReport } from "../../analysis/capability/capability-warnings.ts";
9
3
  import {
10
4
  formatProjectServerRoot,
@@ -15,7 +9,10 @@ import { makeRelative, renderFileScopeStatus } from "./file-scope-markdown.ts";
15
9
  import {
16
10
  formatProcessCrashRecovery,
17
11
  formatRefreshElapsed,
12
+ formatSourceTracking,
13
+ formatSourceTrackingDetails,
18
14
  formatStaleDiagnosticRestarts as formatStaleDiagnosticRestartsText,
15
+ formatStartupRetry,
19
16
  isFileReadinessPending,
20
17
  } from "./refresh-outcome.ts";
21
18
  import type {
@@ -51,72 +48,93 @@ function renderRefreshStatus(
51
48
  hasDiagnostics: boolean,
52
49
  cwd: string,
53
50
  ): void {
54
- if (!hasDiagnostics) return;
55
-
56
- switch (data.refresh.kind) {
57
- case "completed": {
58
- const fileReadinessPending = isFileReadinessPending(data.refresh, data.semanticState);
59
- lines.push(
60
- `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending))}`,
61
- );
62
- lines.push(`**Stale assessment**: ${asSentence(staleAssessmentText(data.refresh))}`);
63
- lines.push("");
64
- return;
65
- }
66
- case "failed": {
67
- const evidence = data.refresh.diagnosticEvidence
68
- ? `; ${formatDiagnosticEvidence(data.refresh.diagnosticEvidence)}`
69
- : "";
70
- const processCrashRecovery = data.refresh.processCrashRecovery
71
- ? formatProcessCrashRecovery(data.refresh.processCrashRecovery)
72
- : null;
73
- const staleRestart = formatStaleDiagnosticRestarts(data.refresh);
74
- lines.push(
75
- `**${refreshAttemptLabel(data.refresh)}**: failed ${data.refresh.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`,
76
- );
77
- lines.push("");
78
- return;
79
- }
80
- case "not-attempted":
81
- lines.push(`**Diagnostic refresh attempt**: not started — ${data.refresh.reason}`);
82
- if (data.refresh.lastAttempt) renderLastAttempt(lines, data.refresh.lastAttempt, cwd);
83
- lines.push("");
84
- return;
85
- case "not-requested":
86
- if (data.refresh.lastAttempt) {
87
- renderRetainedAttempt(lines, data.refresh.lastAttempt, cwd);
88
- } else {
89
- lines.push(
90
- "**Diagnostic refresh attempt**: not requested this session. Use `refresh: true` to try one.",
91
- );
92
- }
93
- lines.push("");
51
+ if (!hasDiagnostics && data.refresh.kind !== "completed" && data.refresh.kind !== "failed") {
52
+ return;
53
+ }
54
+ if (data.refresh.kind === "completed") {
55
+ renderCompletedRefreshStatus(lines, data);
56
+ return;
57
+ }
58
+ if (data.refresh.kind === "failed") {
59
+ renderFailedRefreshStatus(lines, data.refresh);
60
+ return;
61
+ }
62
+ if (data.refresh.kind === "not-attempted") {
63
+ lines.push(`**Diagnostic refresh attempt**: not started — ${data.refresh.reason}`);
64
+ if (data.refresh.lastAttempt) renderLastAttempt(lines, data.refresh.lastAttempt, cwd);
65
+ lines.push("");
66
+ return;
67
+ }
68
+ if (data.refresh.lastAttempt) {
69
+ renderRetainedAttempt(lines, data.refresh.lastAttempt, cwd);
70
+ } else {
71
+ lines.push(
72
+ "**Diagnostic refresh attempt**: not requested this session. Use `refresh: true` to try one.",
73
+ );
94
74
  }
75
+ lines.push("");
76
+ }
77
+
78
+ function renderCompletedRefreshStatus(lines: string[], data: HealthData): void {
79
+ if (data.refresh.kind !== "completed") return;
80
+ const fileReadinessPending = isFileReadinessPending(data.refresh, data.semanticState);
81
+ lines.push(
82
+ `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending, false))}`,
83
+ );
84
+ renderSourceTracking(lines, data.refresh);
85
+ lines.push(`**Stale assessment**: ${asSentence(staleAssessmentText(data.refresh))}`);
86
+ lines.push("");
87
+ }
88
+
89
+ function renderFailedRefreshStatus(
90
+ lines: string[],
91
+ attempt: Extract<HealthRefreshAttempt, { kind: "failed" }>,
92
+ ): void {
93
+ const outcome = failedRefreshText(attempt, false);
94
+ lines.push(`**${refreshAttemptLabel(attempt)}**: ${asSentence(outcome)}`);
95
+ renderSourceTracking(lines, attempt);
96
+ lines.push("");
95
97
  }
96
98
 
97
99
  function completedRefreshText(
98
100
  attempt: Extract<HealthRefreshAttempt, { kind: "completed" }>,
99
101
  fileReadinessPending = attempt.fileReadiness === "pending",
102
+ includeSourceTracking = true,
100
103
  ): string {
101
104
  const processCrashRecovery = formatProcessCrashRecovery(attempt.processCrashRecovery);
105
+ const startupRetry = formatStartupRetry(attempt.startupRetry);
102
106
  const noOp =
103
107
  !fileReadinessPending &&
104
108
  attempt.attemptedActiveClients === 0 &&
105
109
  attempt.restartedClients === 0 &&
106
- processCrashRecovery === null;
110
+ processCrashRecovery === null &&
111
+ startupRetry === null;
107
112
  const base = fileReadinessPending
108
113
  ? "readiness pending — LSP may still be warming; retry shortly"
109
114
  : noOp
110
115
  ? "completed no-op — no active clients were targeted"
111
116
  : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted`;
112
117
  const staleRestart = noOp ? null : formatStaleDiagnosticRestarts(attempt);
113
- const outcome = [staleRestart, processCrashRecovery].filter(
114
- (value): value is string => value !== null,
115
- );
118
+ const sourceTracking = formatSourceTracking(attempt.sourceTracking);
119
+ const outcome = [
120
+ includeSourceTracking ? sourceTracking : null,
121
+ staleRestart,
122
+ processCrashRecovery,
123
+ startupRetry,
124
+ ].filter((value): value is string => value !== null);
116
125
  const withOutcome = outcome.length > 0 ? `${base}; ${outcome.join("; ")}` : base;
126
+ const withScope = `${withOutcome}; maintenance scope: ${refreshOperationScopeText(attempt)}`;
117
127
  return attempt.operationScope === "workspace-runtime"
118
- ? `${withOutcome}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
119
- : withOutcome;
128
+ ? `${withScope}; maintenance evidence: ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
129
+ : withScope;
130
+ }
131
+
132
+ function renderSourceTracking(
133
+ lines: string[],
134
+ attempt: Pick<HealthRefreshAttempt, "sourceTracking">,
135
+ ): void {
136
+ const sourceTracking = formatSourceTrackingDetails(attempt.sourceTracking);
137
+ if (sourceTracking) lines.push(`**Source discovery**: ${asSentence(sourceTracking)}`);
120
138
  }
121
139
 
122
140
  function refreshAttemptLabel(attempt: Pick<HealthRefreshAttempt, "operationScope">): string {
@@ -142,7 +160,7 @@ function renderLastAttempt(lines: string[], attempt: HealthRefreshAttempt, cwd:
142
160
  const outcome = refreshAttemptOutcome(attempt);
143
161
  const label = refreshAttemptLabel(attempt);
144
162
  lines.push(
145
- `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}; operation scope: ${refreshOperationScopeText(attempt)}.`,
163
+ `**Previous ${label.toLowerCase()}**: ${asSentence(outcome)} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested evidence scope: ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}.`,
146
164
  );
147
165
  }
148
166
 
@@ -150,20 +168,35 @@ function renderRetainedAttempt(lines: string[], attempt: HealthRefreshAttempt, c
150
168
  const label =
151
169
  attempt.operationScope === "file-runtime" ? "File LSP maintenance" : "Diagnostic refresh";
152
170
  lines.push(
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)}.`,
171
+ `**${label}**: not requested for this call. Previous attempt ${asSentence(refreshAttemptOutcome(attempt))} Started ${formatRefreshElapsed(Date.now() - attempt.attemptedAt)}; requested evidence scope: ${formatDiagnosticScope(attempt.requestedDiagnosticScope, cwd)}.`,
154
172
  );
155
173
  }
156
174
 
157
175
  function refreshAttemptOutcome(attempt: HealthRefreshAttempt): string {
158
176
  if (attempt.kind === "completed") return completedRefreshText(attempt);
177
+ return failedRefreshText(attempt);
178
+ }
179
+
180
+ function failedRefreshText(
181
+ attempt: Extract<HealthRefreshAttempt, { kind: "failed" }>,
182
+ includeSourceTracking = true,
183
+ ): string {
159
184
  const processCrashRecovery = attempt.processCrashRecovery
160
185
  ? formatProcessCrashRecovery(attempt.processCrashRecovery)
161
186
  : null;
187
+ const startupRetry = attempt.startupRetry ? formatStartupRetry(attempt.startupRetry) : null;
162
188
  const staleRestart = formatStaleDiagnosticRestarts(attempt);
189
+ const sourceTracking = includeSourceTracking
190
+ ? formatSourceTracking(attempt.sourceTracking)
191
+ : null;
163
192
  const evidence = attempt.diagnosticEvidence
164
- ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
193
+ ? `; maintenance evidence: ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
165
194
  : "";
166
- return `failed — ${attempt.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`;
195
+ return `failed — ${stripTrailingPunctuation(attempt.reason)}; maintenance scope: ${refreshOperationScopeText(attempt)}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${startupRetry ? `; ${startupRetry}` : ""}${sourceTracking ? `; ${sourceTracking}` : ""}${evidence}`;
196
+ }
197
+
198
+ function stripTrailingPunctuation(text: string): string {
199
+ return text.replace(/[.!?]+$/, "");
167
200
  }
168
201
 
169
202
  function refreshOperationScopeText(attempt: HealthRefreshAttempt): string {
@@ -1,8 +1,11 @@
1
1
  import type {
2
2
  ProcessCrashRecoveryEntry,
3
3
  ProcessCrashRecoveryReport,
4
+ StartupRetryEntry,
5
+ StartupRetryReport,
4
6
  } from "@mrclrchtr/supi-lsp/api";
5
7
  import type { SemanticHealthState } from "../../session/health-types.ts";
8
+ import type { SourceTrackingReport } from "../../substrate/lsp/source-tracking.ts";
6
9
 
7
10
  /** Report whether a file health result is waiting for semantic readiness. */
8
11
  export function isFileReadinessPending(
@@ -64,6 +67,54 @@ function formatProcessCrashRecoveryEntry(entry: ProcessCrashRecoveryEntry): stri
64
67
  return `${entry.name} @ ${entry.root}: ${outcome}${action}${failure}`;
65
68
  }
66
69
 
70
+ /** Format initial-start retry counts for the compact health view. */
71
+ export function formatCompactStartupRetry(
72
+ report: StartupRetryReport | null | undefined,
73
+ ): string | null {
74
+ if (!report || (report.recoveredRoutes === 0 && report.failedRoutes === 0)) return null;
75
+ return `startup retry: ${report.recoveredRoutes} route${plural(report.recoveredRoutes)} recovered (${report.failedRoutes} failed)`;
76
+ }
77
+
78
+ /** Format initial-start retry counts and bounded route entries. */
79
+ export function formatStartupRetry(report: StartupRetryReport | null | undefined): string | null {
80
+ const summary = formatCompactStartupRetry(report);
81
+ if (!summary || !report) return summary;
82
+ const entries = report.entries.map(formatStartupRetryEntry);
83
+ const omitted =
84
+ report.omittedEntries > 0
85
+ ? `${report.omittedEntries} more route${plural(report.omittedEntries)}`
86
+ : null;
87
+ const details = [...entries, omitted]
88
+ .filter((value): value is string => value !== null)
89
+ .join("; ");
90
+ return details ? `${summary}; ${details}` : summary;
91
+ }
92
+
93
+ function formatStartupRetryEntry(entry: StartupRetryEntry): string {
94
+ const outcome = entry.outcome.replaceAll("-", " ");
95
+ const action = entry.nextAction ? `; next: ${entry.nextAction.replaceAll("-", " ")}` : "";
96
+ const failure = entry.failureMessage ? `; ${entry.failureMessage}` : "";
97
+ return `${entry.name} @ ${entry.root}: ${outcome}${action}${failure}`;
98
+ }
99
+
100
+ /** Format source discovery and bounded tracking counts separately from diagnostics. */
101
+ export function formatSourceTracking(
102
+ report: SourceTrackingReport | null | undefined,
103
+ ): string | null {
104
+ const details = formatSourceTrackingDetails(report);
105
+ return details ? `source discovery: ${details}` : null;
106
+ }
107
+
108
+ /** Format source tracking counts without a label for a standalone heading. */
109
+ export function formatSourceTrackingDetails(
110
+ report: SourceTrackingReport | null | undefined,
111
+ ): string | null {
112
+ if (!report) return null;
113
+ const inventory =
114
+ report.status === "complete" ? "complete" : `limited (${report.reason ?? "unknown reason"})`;
115
+ return `${inventory}; ${report.observedFileCount} source files observed, ${report.discovered.length} discovered, ${report.tracked.length} tracked, ${report.skipped.length} skipped, ${report.unavailable.length} unavailable, ${report.deferred} deferred`;
116
+ }
117
+
67
118
  /** Format the age of a retained refresh attempt for a status line. */
68
119
  export function formatRefreshElapsed(milliseconds: number): string {
69
120
  const seconds = Math.max(0, Math.round(milliseconds / 1_000));