@mrclrchtr/supi-code-intelligence 6.2.0 → 6.4.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 (68) hide show
  1. package/README.md +2 -0
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  4. package/node_modules/@mrclrchtr/supi-lsp/README.md +2 -0
  5. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  7. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/README.md +52 -0
  8. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +785 -129
  9. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +660 -98
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +10 -4
  11. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  12. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +16 -2
  13. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +4 -32
  14. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
  15. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +106 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +4 -4
  17. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +513 -113
  18. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -15
  19. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +40 -33
  20. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +36 -6
  21. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +1 -1
  22. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-path-policy.ts +18 -9
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  24. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  25. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/README.md +29 -1
  26. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs +99 -19
  27. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.cjs.map +4 -4
  28. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js +94 -18
  29. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.js.map +4 -4
  30. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  31. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +14 -14
  32. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +13 -8
  33. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs +97 -19
  34. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.cjs.map +4 -4
  35. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts +28 -8
  36. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.cts.map +1 -1
  37. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts +28 -8
  38. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.d.ts.map +1 -1
  39. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js +92 -18
  40. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.js.map +4 -4
  41. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  42. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +17 -17
  43. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +7 -4
  44. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  45. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +2 -2
  46. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  47. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +2 -2
  48. package/package.json +5 -5
  49. package/src/session/health-refresh.ts +19 -12
  50. package/src/session/health-types.ts +8 -3
  51. package/src/session/health-workflow.ts +7 -3
  52. package/src/session/session.ts +12 -5
  53. package/src/substrate/lsp/maintenance.ts +53 -47
  54. package/src/substrate/lsp/settings.ts +2 -1
  55. package/src/substrate/lsp/source-tracking.ts +247 -0
  56. package/src/tool/code_find/guidance.ts +3 -3
  57. package/src/tool/code_graph/guidance.ts +2 -2
  58. package/src/tool/code_health/guidance.ts +3 -3
  59. package/src/tool/code_health/markdown.ts +21 -5
  60. package/src/tool/code_health/refresh-outcome.ts +48 -7
  61. package/src/tool/code_health/refresh-status.ts +83 -16
  62. package/src/tool/code_health/tui.ts +4 -0
  63. package/src/tool/code_inspect/guidance.ts +5 -3
  64. package/src/tool/code_orientation/guidance.ts +3 -4
  65. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  66. package/src/tool/code_refactor_plan/guidance.ts +3 -5
  67. package/src/tool/code_resolve/guidance.ts +3 -3
  68. package/src/tool/guidance.ts +11 -9
@@ -0,0 +1,247 @@
1
+ import { statSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import {
4
+ type CodeRequestControl,
5
+ throwIfCodeRequestInterrupted,
6
+ } from "@mrclrchtr/supi-code-runtime/api";
7
+ import { isWithinOrEqual } from "@mrclrchtr/supi-core/api";
8
+ import type {
9
+ BulkTrackFileOutcome,
10
+ WorkspaceLspRuntime,
11
+ WorkspaceSourceInventory,
12
+ } from "@mrclrchtr/supi-lsp/api";
13
+ import { isMissingFileError, MAX_BULK_TRACK_FILES } from "@mrclrchtr/supi-lsp/api";
14
+
15
+ /** Typed session state for sentinel maintenance and automatic source tracking. */
16
+ export interface LspMaintenanceState {
17
+ readonly sentinelSnapshot: Map<string, number>;
18
+ readonly sourceBaseline: ReadonlySet<string> | null;
19
+ readonly createdSourceQueue: readonly string[];
20
+ }
21
+
22
+ /** Source discovery and tracking facts from one broad diagnostic refresh. */
23
+ export interface SourceTrackingReport {
24
+ readonly status: WorkspaceSourceInventory["status"];
25
+ readonly reason: WorkspaceSourceInventory["reason"];
26
+ readonly observedFileCount: number;
27
+ readonly discovered: readonly string[];
28
+ readonly tracked: readonly string[];
29
+ readonly unsupported: readonly string[];
30
+ readonly unavailable: readonly string[];
31
+ readonly deferred: number;
32
+ }
33
+
34
+ /** Create empty source state with an optional lifecycle-seeded sentinel snapshot. */
35
+ export function createLspMaintenanceState(
36
+ sentinelSnapshot: Map<string, number> = new Map(),
37
+ ): LspMaintenanceState {
38
+ return {
39
+ sentinelSnapshot,
40
+ sourceBaseline: null,
41
+ createdSourceQueue: [],
42
+ };
43
+ }
44
+
45
+ /** Replace the sentinel portion of maintenance state without source state. */
46
+ export function withSentinelSnapshot(
47
+ state: LspMaintenanceState,
48
+ sentinelSnapshot: Map<string, number>,
49
+ ): LspMaintenanceState {
50
+ return { ...state, sentinelSnapshot };
51
+ }
52
+
53
+ /** Discover source additions and process a bounded queue for one refresh. */
54
+ export async function trackCreatedSources(options: {
55
+ readonly runtime: WorkspaceLspRuntime;
56
+ readonly cwd: string;
57
+ readonly state: LspMaintenanceState;
58
+ readonly scope?: string | null;
59
+ readonly control?: CodeRequestControl;
60
+ }): Promise<{ state: LspMaintenanceState; report: SourceTrackingReport }> {
61
+ const inventory = await options.runtime.scanWorkspaceSources(options.control);
62
+ throwIfCodeRequestInterrupted(options.control);
63
+ if (inventory.status === "limited") {
64
+ return {
65
+ state: options.state,
66
+ report: createReport({
67
+ cwd: options.cwd,
68
+ inventory,
69
+ discovered: [],
70
+ tracked: [],
71
+ unsupported: [],
72
+ unavailable: [],
73
+ deferred: options.state.createdSourceQueue,
74
+ control: options.control,
75
+ }),
76
+ };
77
+ }
78
+
79
+ const discovered = discoverCreatedPaths(
80
+ options.state.sourceBaseline,
81
+ inventory.files,
82
+ options.control,
83
+ );
84
+ const queue = removeMissingPaths(
85
+ options.state.sourceBaseline === null
86
+ ? []
87
+ : deduplicatePaths([...options.state.createdSourceQueue, ...discovered], options.control),
88
+ options.control,
89
+ );
90
+ const selected = selectScopedPaths(queue, options.cwd, options.scope, options.control).slice(
91
+ 0,
92
+ MAX_BULK_TRACK_FILES,
93
+ );
94
+ const batch =
95
+ selected.length === 0
96
+ ? { outcomes: [] as const }
97
+ : await options.runtime.bulkTrackFiles(selected, options.control);
98
+ const nextQueue = retainQueuePaths(queue, selected, batch.outcomes, options.control);
99
+ const nextState: LspMaintenanceState = {
100
+ sentinelSnapshot: options.state.sentinelSnapshot,
101
+ sourceBaseline: new Set(inventory.files),
102
+ createdSourceQueue: nextQueue,
103
+ };
104
+
105
+ const tracked = pathsForOutcome(batch.outcomes, "tracked");
106
+ const unsupported = pathsForOutcome(batch.outcomes, "unsupported");
107
+ const unavailable = pathsForOutcome(batch.outcomes, "unavailable");
108
+ return {
109
+ state: nextState,
110
+ report: createReport({
111
+ cwd: options.cwd,
112
+ inventory,
113
+ discovered,
114
+ tracked,
115
+ unsupported,
116
+ unavailable,
117
+ deferred: nextQueue,
118
+ control: options.control,
119
+ }),
120
+ };
121
+ }
122
+
123
+ function discoverCreatedPaths(
124
+ baseline: ReadonlySet<string> | null,
125
+ current: readonly string[],
126
+ control?: CodeRequestControl,
127
+ ): string[] {
128
+ if (baseline === null) return [];
129
+ const discovered: string[] = [];
130
+ for (const filePath of current) {
131
+ throwIfCodeRequestInterrupted(control);
132
+ if (!baseline.has(filePath)) discovered.push(filePath);
133
+ }
134
+ return discovered;
135
+ }
136
+
137
+ function selectScopedPaths(
138
+ queue: readonly string[],
139
+ cwd: string,
140
+ scope: string | null | undefined,
141
+ control?: CodeRequestControl,
142
+ ): string[] {
143
+ const resolvedScope = scope === null || scope === undefined ? null : path.resolve(cwd, scope);
144
+ const selected: string[] = [];
145
+ for (const filePath of queue) {
146
+ throwIfCodeRequestInterrupted(control);
147
+ if (resolvedScope === null || isWithinOrEqual(resolvedScope, filePath)) selected.push(filePath);
148
+ }
149
+ return selected;
150
+ }
151
+
152
+ function retainQueuePaths(
153
+ queue: readonly string[],
154
+ selected: readonly string[],
155
+ outcomes: readonly BulkTrackFileOutcome[],
156
+ control?: CodeRequestControl,
157
+ ): string[] {
158
+ const selectedSet = new Set(selected);
159
+ const outcomeByPath = new Map(outcomes.map((outcome) => [outcome.file, outcome] as const));
160
+ const unstarted: string[] = [];
161
+ const unavailable: string[] = [];
162
+
163
+ for (const filePath of queue) {
164
+ throwIfCodeRequestInterrupted(control);
165
+ if (!selectedSet.has(filePath)) {
166
+ unstarted.push(filePath);
167
+ continue;
168
+ }
169
+ const outcome = outcomeByPath.get(filePath);
170
+ if (!outcome) {
171
+ unstarted.push(filePath);
172
+ } else if (outcome.kind === "unavailable") {
173
+ unavailable.push(filePath);
174
+ }
175
+ }
176
+ return [...unstarted, ...unavailable];
177
+ }
178
+
179
+ function pathsForOutcome(
180
+ outcomes: readonly BulkTrackFileOutcome[],
181
+ kind: BulkTrackFileOutcome["kind"],
182
+ ): string[] {
183
+ return outcomes.filter((outcome) => outcome.kind === kind).map((outcome) => outcome.file);
184
+ }
185
+
186
+ function createReport(options: {
187
+ cwd: string;
188
+ inventory: WorkspaceSourceInventory;
189
+ discovered: readonly string[];
190
+ tracked: readonly string[];
191
+ unsupported: readonly string[];
192
+ unavailable: readonly string[];
193
+ deferred: readonly string[];
194
+ control?: CodeRequestControl;
195
+ }): SourceTrackingReport {
196
+ return {
197
+ status: options.inventory.status,
198
+ reason: options.inventory.reason,
199
+ observedFileCount: options.inventory.observedFileCount,
200
+ discovered: displayPaths(options.cwd, options.discovered, options.control),
201
+ tracked: displayPaths(options.cwd, options.tracked, options.control),
202
+ unsupported: displayPaths(options.cwd, options.unsupported, options.control),
203
+ unavailable: displayPaths(options.cwd, options.unavailable, options.control),
204
+ deferred: options.deferred.length,
205
+ };
206
+ }
207
+
208
+ function displayPaths(
209
+ cwd: string,
210
+ filePaths: readonly string[],
211
+ control?: CodeRequestControl,
212
+ ): string[] {
213
+ const displayed: string[] = [];
214
+ for (const filePath of filePaths) {
215
+ throwIfCodeRequestInterrupted(control);
216
+ const relativePath = path.relative(cwd, filePath);
217
+ displayed.push((relativePath || path.basename(filePath)).replaceAll(path.sep, "/"));
218
+ }
219
+ return displayed;
220
+ }
221
+
222
+ function deduplicatePaths(filePaths: readonly string[], control?: CodeRequestControl): string[] {
223
+ const unique = new Set<string>();
224
+ for (const filePath of filePaths) {
225
+ throwIfCodeRequestInterrupted(control);
226
+ unique.add(filePath);
227
+ }
228
+ return [...unique];
229
+ }
230
+
231
+ function removeMissingPaths(filePaths: readonly string[], control?: CodeRequestControl): string[] {
232
+ const existing: string[] = [];
233
+ for (const filePath of filePaths) {
234
+ throwIfCodeRequestInterrupted(control);
235
+ if (!isMissingFile(filePath)) existing.push(filePath);
236
+ }
237
+ return existing;
238
+ }
239
+
240
+ function isMissingFile(filePath: string): boolean {
241
+ try {
242
+ statSync(filePath);
243
+ return false;
244
+ } catch (error) {
245
+ return isMissingFileError(error);
246
+ }
247
+ }
@@ -1,8 +1,8 @@
1
1
  export const toolDescription =
2
- 'Search source shape with mode:"ast" or LSP workspace symbols with mode:"semantic". Modes never silently fall back. Unscoped AST scans cover visible supported files, skipping hidden and generated/dependency directories.';
2
+ "Search code structure or workspace symbols for structural or semantic matches. It never silently falls back to another mode or to text search.";
3
3
 
4
- export const promptSnippet = "explicit structural or semantic code search";
4
+ export const promptSnippet = "search code structure or workspace symbols";
5
5
 
6
6
  export const promptGuidelines = [
7
- "Use code_find for structural or semantic search evidence; use PI grep for literal/regex source search and code_graph references for symbol-identity relationships.",
7
+ "Use code_find for structural or semantic search, grep for text search, and code_graph for symbol relationships.",
8
8
  ];
@@ -1,6 +1,6 @@
1
1
  export const toolDescription =
2
- "Collect references, structural callees, and implementations for one target. Callees match source shape, not symbol identity.";
2
+ "Find symbol references and implementations, and list calls from a target's enclosing scope. Callee results match source shape, not symbol identity.";
3
3
 
4
- export const promptSnippet = "provider-backed and structural relation evidence";
4
+ export const promptSnippet = "trace code relationships";
5
5
 
6
6
  export const promptGuidelines: string[] = [];
@@ -1,8 +1,8 @@
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. 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
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.",
7
+ "Use code_health with `refresh: true` before relying on diagnostics that can be stale.",
8
8
  ];
@@ -15,6 +15,7 @@ import { makeRelative, renderFileScopeStatus } from "./file-scope-markdown.ts";
15
15
  import {
16
16
  formatProcessCrashRecovery,
17
17
  formatRefreshElapsed,
18
+ formatSourceTracking,
18
19
  formatStaleDiagnosticRestarts as formatStaleDiagnosticRestartsText,
19
20
  isFileReadinessPending,
20
21
  } from "./refresh-outcome.ts";
@@ -57,8 +58,9 @@ function renderRefreshStatus(
57
58
  case "completed": {
58
59
  const fileReadinessPending = isFileReadinessPending(data.refresh, data.semanticState);
59
60
  lines.push(
60
- `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending))}`,
61
+ `**${refreshAttemptLabel(data.refresh)}**: ${asSentence(completedRefreshText(data.refresh, fileReadinessPending, false))}`,
61
62
  );
63
+ renderSourceTracking(lines, data.refresh);
62
64
  lines.push(`**Stale assessment**: ${asSentence(staleAssessmentText(data.refresh))}`);
63
65
  lines.push("");
64
66
  return;
@@ -74,6 +76,7 @@ function renderRefreshStatus(
74
76
  lines.push(
75
77
  `**${refreshAttemptLabel(data.refresh)}**: failed — ${data.refresh.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`,
76
78
  );
79
+ renderSourceTracking(lines, data.refresh);
77
80
  lines.push("");
78
81
  return;
79
82
  }
@@ -97,6 +100,7 @@ function renderRefreshStatus(
97
100
  function completedRefreshText(
98
101
  attempt: Extract<HealthRefreshAttempt, { kind: "completed" }>,
99
102
  fileReadinessPending = attempt.fileReadiness === "pending",
103
+ includeSourceTracking = true,
100
104
  ): string {
101
105
  const processCrashRecovery = formatProcessCrashRecovery(attempt.processCrashRecovery);
102
106
  const noOp =
@@ -110,15 +114,26 @@ function completedRefreshText(
110
114
  ? "completed no-op — no active clients were targeted"
111
115
  : `completed — ${attempt.attemptedActiveClients} active client${plural(attempt.attemptedActiveClients)} targeted`;
112
116
  const staleRestart = noOp ? null : formatStaleDiagnosticRestarts(attempt);
113
- const outcome = [staleRestart, processCrashRecovery].filter(
114
- (value): value is string => value !== null,
115
- );
117
+ const sourceTracking = formatSourceTracking(attempt.sourceTracking);
118
+ const outcome = [
119
+ includeSourceTracking ? sourceTracking : null,
120
+ staleRestart,
121
+ processCrashRecovery,
122
+ ].filter((value): value is string => value !== null);
116
123
  const withOutcome = outcome.length > 0 ? `${base}; ${outcome.join("; ")}` : base;
117
124
  return attempt.operationScope === "workspace-runtime"
118
125
  ? `${withOutcome}; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
119
126
  : withOutcome;
120
127
  }
121
128
 
129
+ function renderSourceTracking(
130
+ lines: string[],
131
+ attempt: Pick<HealthRefreshAttempt, "sourceTracking">,
132
+ ): void {
133
+ const sourceTracking = formatSourceTracking(attempt.sourceTracking);
134
+ if (sourceTracking) lines.push(`**Source discovery**: ${asSentence(sourceTracking)}`);
135
+ }
136
+
122
137
  function refreshAttemptLabel(attempt: Pick<HealthRefreshAttempt, "operationScope">): string {
123
138
  return attempt.operationScope === "file-runtime"
124
139
  ? "File LSP maintenance attempt"
@@ -160,10 +175,11 @@ function refreshAttemptOutcome(attempt: HealthRefreshAttempt): string {
160
175
  ? formatProcessCrashRecovery(attempt.processCrashRecovery)
161
176
  : null;
162
177
  const staleRestart = formatStaleDiagnosticRestarts(attempt);
178
+ const sourceTracking = formatSourceTracking(attempt.sourceTracking);
163
179
  const evidence = attempt.diagnosticEvidence
164
180
  ? `; ${formatDiagnosticEvidence(attempt.diagnosticEvidence)}`
165
181
  : "";
166
- return `failed — ${attempt.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${evidence}`;
182
+ return `failed — ${attempt.reason}${staleRestart ? `; ${staleRestart}` : ""}${processCrashRecovery ? `; ${processCrashRecovery}` : ""}${sourceTracking ? `; ${sourceTracking}` : ""}${evidence}`;
167
183
  }
168
184
 
169
185
  function refreshOperationScopeText(attempt: HealthRefreshAttempt): string {
@@ -1,5 +1,9 @@
1
- import type { ProcessCrashRecoverySummary } from "@mrclrchtr/supi-lsp/api";
1
+ import type {
2
+ ProcessCrashRecoveryEntry,
3
+ ProcessCrashRecoveryReport,
4
+ } from "@mrclrchtr/supi-lsp/api";
2
5
  import type { SemanticHealthState } from "../../session/health-types.ts";
6
+ import type { SourceTrackingReport } from "../../substrate/lsp/source-tracking.ts";
3
7
 
4
8
  /** Report whether a file health result is waiting for semantic readiness. */
5
9
  export function isFileReadinessPending(
@@ -21,17 +25,54 @@ export function formatStaleDiagnosticRestarts(restartedClients: number): string
21
25
  return `stale diagnostic restarts: ${restartedClients} client${plural(restartedClients)} restarted`;
22
26
  }
23
27
 
24
- /** Format a non-empty process-crash route recovery outcome. */
25
- export function formatProcessCrashRecovery(
26
- summary: ProcessCrashRecoverySummary | null | undefined,
28
+ /** Format process-crash counts for the compact health view. */
29
+ export function formatCompactProcessCrashRecovery(
30
+ report: ProcessCrashRecoveryReport | null | undefined,
27
31
  ): string | null {
28
32
  if (
29
- !summary ||
30
- (summary.attemptedRoutes === 0 && summary.recoveredRoutes === 0 && summary.failedRoutes === 0)
33
+ !report ||
34
+ (report.recoveredRoutes === 0 &&
35
+ report.skippedRoutes === 0 &&
36
+ report.failedRoutes === 0 &&
37
+ report.exhaustedRoutes === 0)
31
38
  ) {
32
39
  return null;
33
40
  }
34
- return `process-crash recovery: ${summary.recoveredRoutes} route${plural(summary.recoveredRoutes)} recovered (${summary.attemptedRoutes} attempted, ${summary.failedRoutes} failed)`;
41
+ return `process-crash recovery: ${report.recoveredRoutes} route${plural(report.recoveredRoutes)} recovered (${report.skippedRoutes} skipped, ${report.failedRoutes} failed, ${report.exhaustedRoutes} exhausted)`;
42
+ }
43
+
44
+ /** Format process-crash counts and bounded route entries for detailed health output. */
45
+ export function formatProcessCrashRecovery(
46
+ report: ProcessCrashRecoveryReport | null | undefined,
47
+ ): string | null {
48
+ const summary = formatCompactProcessCrashRecovery(report);
49
+ if (!summary || !report) return summary;
50
+ const entries = report.entries.map(formatProcessCrashRecoveryEntry);
51
+ const omitted =
52
+ report.omittedEntries > 0
53
+ ? `${report.omittedEntries} more route${plural(report.omittedEntries)}`
54
+ : null;
55
+ const details = [...entries, omitted]
56
+ .filter((value): value is string => value !== null)
57
+ .join("; ");
58
+ return details ? `${summary}; ${details}` : summary;
59
+ }
60
+
61
+ function formatProcessCrashRecoveryEntry(entry: ProcessCrashRecoveryEntry): string {
62
+ const outcome = entry.outcome.replaceAll("-", " ");
63
+ const action = entry.nextAction ? `; next: ${entry.nextAction.replaceAll("-", " ")}` : "";
64
+ const failure = entry.failureMessage ? `; ${entry.failureMessage}` : "";
65
+ return `${entry.name} @ ${entry.root}: ${outcome}${action}${failure}`;
66
+ }
67
+
68
+ /** Format source discovery and bounded tracking counts separately from diagnostics. */
69
+ export function formatSourceTracking(
70
+ report: SourceTrackingReport | null | undefined,
71
+ ): string | null {
72
+ if (!report) return null;
73
+ const inventory =
74
+ report.status === "complete" ? "complete" : `limited (${report.reason ?? "unknown reason"})`;
75
+ return `source discovery: ${inventory}; ${report.observedFileCount} source files observed, ${report.discovered.length} discovered, ${report.tracked.length} tracked, ${report.unsupported.length} unsupported, ${report.unavailable.length} unavailable, ${report.deferred} deferred`;
35
76
  }
36
77
 
37
78
  /** Format the age of a retained refresh attempt for a status line. */
@@ -1,5 +1,11 @@
1
- import type { ProcessCrashRecoverySummary } from "@mrclrchtr/supi-lsp/api";
2
1
  import {
2
+ MAX_PROCESS_CRASH_RECOVERY_ENTRIES,
3
+ type ProcessCrashRecoveryEntry,
4
+ type ProcessCrashRecoveryOutcome,
5
+ type ProcessCrashRecoveryReport,
6
+ } from "@mrclrchtr/supi-lsp/api";
7
+ import {
8
+ formatCompactProcessCrashRecovery,
3
9
  formatProcessCrashRecovery,
4
10
  formatStaleDiagnosticRestarts,
5
11
  isFileReadinessPending,
@@ -8,6 +14,14 @@ import { readSemanticHealthState } from "./semantic-state.ts";
8
14
 
9
15
  /** Render current and retained health refresh status for the TUI. */
10
16
 
17
+ /** Read source discovery counts from structured health details. */
18
+ export function readSourceTrackingStatus(source: Record<string, unknown> | null): string | null {
19
+ if (!source || (source.status !== "complete" && source.status !== "limited")) return null;
20
+ const reason =
21
+ source.status === "limited" && typeof source.reason === "string" ? ` (${source.reason})` : "";
22
+ return `${source.status}${reason}: ${readNumber(source.observedFileCount)} observed, ${readArrayLength(source.discovered)} discovered, ${readArrayLength(source.tracked)} tracked, ${readArrayLength(source.unsupported)} unsupported, ${readArrayLength(source.unavailable)} unavailable, ${readNumber(source.deferred)} deferred`;
23
+ }
24
+
11
25
  export function readRefreshStatus(data: Record<string, unknown> | null): string | null {
12
26
  const refresh = readRecord(data?.refresh);
13
27
  if (!refresh) return null;
@@ -82,19 +96,21 @@ export function readCompactRefreshStatus(data: Record<string, unknown> | null):
82
96
  const refresh = readRecord(data?.refresh);
83
97
  if (!refresh || (refresh.kind !== "completed" && refresh.kind !== "failed")) return null;
84
98
 
85
- const processCrashRecovery = formatProcessCrashRecovery(
99
+ const sourceTracking = formatSourceTracking(readRecord(refresh.sourceTracking));
100
+ const processCrashRecovery = formatCompactProcessCrashRecovery(
86
101
  readProcessCrashRecovery(refresh.processCrashRecovery),
87
102
  );
88
103
  if (refresh.kind === "failed") {
89
104
  const staleRestart = formatStaleDiagnosticRestartsForRecord(refresh);
90
105
  return (
91
- [staleRestart, processCrashRecovery]
106
+ [sourceTracking, staleRestart, processCrashRecovery]
92
107
  .filter((value): value is string => value !== null)
93
108
  .join("; ") || null
94
109
  );
95
110
  }
96
111
  const fileReadinessPending = isPendingFileReadiness(data);
97
112
  if (
113
+ !sourceTracking &&
98
114
  !processCrashRecovery &&
99
115
  readNumber(refresh.restartedClients) === 0 &&
100
116
  !fileReadinessPending
@@ -104,7 +120,7 @@ export function readCompactRefreshStatus(data: Record<string, unknown> | null):
104
120
 
105
121
  const staleRestart = formatStaleDiagnosticRestartsForRecord(refresh);
106
122
  const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
107
- return [staleRestart, processCrashRecovery, readiness]
123
+ return [sourceTracking, staleRestart, processCrashRecovery, readiness]
108
124
  .filter((value): value is string => value !== null)
109
125
  .join("; ");
110
126
  }
@@ -117,7 +133,8 @@ export function readPreviousRefreshStatus(data: Record<string, unknown> | null):
117
133
  const attempt = readRecord(refresh.lastAttempt);
118
134
  if (!attempt) return null;
119
135
  const evidence = formatCompactDiagnosticEvidence(readRecord(attempt.diagnosticEvidence));
120
- const processCrashRecovery = formatProcessCrashRecovery(
136
+ const sourceTracking = formatSourceTracking(readRecord(attempt.sourceTracking));
137
+ const processCrashRecovery = formatCompactProcessCrashRecovery(
121
138
  readProcessCrashRecovery(attempt.processCrashRecovery),
122
139
  );
123
140
  const staleRestart = formatStaleDiagnosticRestartsForRecord(attempt);
@@ -125,7 +142,7 @@ export function readPreviousRefreshStatus(data: Record<string, unknown> | null):
125
142
  attempt.kind === "completed" &&
126
143
  isFileReadinessPending(attempt, readSemanticHealthState(data?.semanticState));
127
144
  const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
128
- const outcome = [readiness, staleRestart, processCrashRecovery, evidence]
145
+ const outcome = [readiness, sourceTracking, staleRestart, processCrashRecovery, evidence]
129
146
  .filter((value): value is string => value !== null)
130
147
  .join("; ");
131
148
  if (attempt.kind === "failed") {
@@ -164,6 +181,7 @@ function formatLastRefreshSuffix(refresh: Record<string, unknown>): string {
164
181
  const attempt = readRecord(refresh.lastAttempt);
165
182
  if (!attempt) return "";
166
183
  const evidence = formatDiagnosticEvidence(readRecord(attempt.diagnosticEvidence));
184
+ const sourceTracking = formatSourceTracking(readRecord(attempt.sourceTracking));
167
185
  const staleRestart = formatStaleDiagnosticRestartsForRecord(attempt);
168
186
  const processCrashRecovery = formatProcessCrashRecovery(
169
187
  readProcessCrashRecovery(attempt.processCrashRecovery),
@@ -171,7 +189,7 @@ function formatLastRefreshSuffix(refresh: Record<string, unknown>): string {
171
189
  const fileReadinessPending =
172
190
  attempt.kind === "completed" && isFileReadinessPending(attempt, null);
173
191
  const readiness = fileReadinessPending ? "LSP may still be warming; retry shortly" : null;
174
- const outcome = [readiness, staleRestart, processCrashRecovery, evidence]
192
+ const outcome = [readiness, sourceTracking, staleRestart, processCrashRecovery, evidence]
175
193
  .filter((value): value is string => value !== null)
176
194
  .join("; ");
177
195
  if (attempt.kind === "failed") {
@@ -185,6 +203,15 @@ function formatLastRefreshSuffix(refresh: Record<string, unknown>): string {
185
203
  return "";
186
204
  }
187
205
 
206
+ function formatSourceTracking(source: Record<string, unknown> | null): string | null {
207
+ const status = readSourceTrackingStatus(source);
208
+ return status ? `source discovery ${status}` : null;
209
+ }
210
+
211
+ function readArrayLength(value: unknown): number {
212
+ return Array.isArray(value) ? value.length : 0;
213
+ }
214
+
188
215
  function formatCompactDiagnosticEvidence(evidence: Record<string, unknown> | null): string | null {
189
216
  const counts = readEvidenceCounts(evidence);
190
217
  if (!counts) return null;
@@ -207,20 +234,60 @@ export function isPendingFileReadiness(data: Record<string, unknown> | null): bo
207
234
  return isFileReadinessPending(attempt, readSemanticHealthState(data?.semanticState));
208
235
  }
209
236
 
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;
237
+ function readProcessCrashRecovery(value: unknown): ProcessCrashRecoveryReport | null {
238
+ const report = readRecord(value);
239
+ if (!report) return null;
240
+ const recoveredRoutes = report.recoveredRoutes;
241
+ const skippedRoutes = report.skippedRoutes;
242
+ const failedRoutes = report.failedRoutes;
243
+ const exhaustedRoutes = report.exhaustedRoutes;
244
+ const omittedEntries = report.omittedEntries;
216
245
  if (
217
- !isEvidenceCount(attemptedRoutes) ||
218
246
  !isEvidenceCount(recoveredRoutes) ||
219
- !isEvidenceCount(failedRoutes)
247
+ !isEvidenceCount(skippedRoutes) ||
248
+ !isEvidenceCount(failedRoutes) ||
249
+ !isEvidenceCount(exhaustedRoutes) ||
250
+ !isEvidenceCount(omittedEntries) ||
251
+ !Array.isArray(report.entries) ||
252
+ report.entries.length > MAX_PROCESS_CRASH_RECOVERY_ENTRIES
220
253
  ) {
221
254
  return null;
222
255
  }
223
- return { attemptedRoutes, recoveredRoutes, failedRoutes };
256
+ const entries = report.entries
257
+ .map(readProcessCrashRecoveryEntry)
258
+ .filter((entry): entry is ProcessCrashRecoveryEntry => entry !== null);
259
+ if (entries.length !== report.entries.length) return null;
260
+ return { recoveredRoutes, skippedRoutes, failedRoutes, exhaustedRoutes, entries, omittedEntries };
261
+ }
262
+
263
+ function readProcessCrashRecoveryEntry(value: unknown): ProcessCrashRecoveryEntry | null {
264
+ const entry = readRecord(value);
265
+ if (!entry || typeof entry.name !== "string" || typeof entry.root !== "string") return null;
266
+ if (!isProcessCrashRecoveryOutcome(entry.outcome)) return null;
267
+ if (
268
+ entry.nextAction !== undefined &&
269
+ entry.nextAction !== "use-exact-file" &&
270
+ entry.nextAction !== "reload-workspace"
271
+ ) {
272
+ return null;
273
+ }
274
+ if (entry.failureMessage !== undefined && typeof entry.failureMessage !== "string") return null;
275
+ return {
276
+ name: entry.name,
277
+ root: entry.root,
278
+ outcome: entry.outcome,
279
+ ...(entry.nextAction !== undefined ? { nextAction: entry.nextAction } : {}),
280
+ ...(entry.failureMessage !== undefined ? { failureMessage: entry.failureMessage } : {}),
281
+ };
282
+ }
283
+
284
+ function isProcessCrashRecoveryOutcome(value: unknown): value is ProcessCrashRecoveryOutcome {
285
+ return (
286
+ value === "recovered" ||
287
+ value === "skipped-no-retained-file" ||
288
+ value === "recovery-failed" ||
289
+ value === "recovery-exhausted"
290
+ );
224
291
  }
225
292
 
226
293
  function readEvidenceCounts(evidence: Record<string, unknown> | null): number[] | null {
@@ -29,6 +29,7 @@ import {
29
29
  readCompactRefreshStatus,
30
30
  readPreviousRefreshStatus,
31
31
  readRefreshStatus,
32
+ readSourceTrackingStatus,
32
33
  } from "./refresh-status.ts";
33
34
  import { formatSemanticHealthState, readSemanticHealthState } from "./semantic-state.ts";
34
35
 
@@ -192,6 +193,8 @@ function buildStatusBar(data: Record<string, unknown> | null, theme: Theme): Tex
192
193
  const semanticStatus = readSemanticStatus(data);
193
194
  const structuralStatus = readString(data, "structuralStatus");
194
195
  const refreshStatus = readRefreshStatus(data);
196
+ const refresh = readRecord(data?.refresh);
197
+ const sourceTracking = readSourceTrackingStatus(readRecord(refresh?.sourceTracking));
195
198
 
196
199
  const lspColor = semanticStatus.startsWith("ready") ? "success" : "warning";
197
200
  const structuralColor = structuralStatus === "ready" ? "success" : "muted";
@@ -204,6 +207,7 @@ function buildStatusBar(data: Record<string, unknown> | null, theme: Theme): Tex
204
207
  lines.push(`Tree-sitter: ${theme.fg(structuralColor, structuralStatus)}`);
205
208
  }
206
209
  if (refreshStatus) lines.push(`Diagnostics: ${theme.fg("dim", refreshStatus)}`);
210
+ if (sourceTracking) lines.push(`Source discovery: ${theme.fg("dim", sourceTracking)}`);
207
211
 
208
212
  return new Text(lines.join(" "), 0, 0);
209
213
  }
@@ -1,6 +1,8 @@
1
1
  export const toolDescription =
2
- "Inspect one exact point for syntax, the narrowest enclosing declaration, hover, definition, and nearby diagnostics. Use for point facts, not broad Orientation.";
2
+ "Inspect one source location for syntax, its enclosing declaration, hover information, definitions, and nearby diagnostics. Use it for point-local facts, not broad code context.";
3
3
 
4
- export const promptSnippet = "factual point inspection";
4
+ export const promptSnippet = "inspect a source location";
5
5
 
6
- export const promptGuidelines: string[] = [];
6
+ export const promptGuidelines = [
7
+ "Use code_inspect for diagnostics near a source location; use code_health for broader diagnostics or language-server status.",
8
+ ];
@@ -1,9 +1,8 @@
1
1
  export const toolDescription =
2
- "Orient around the workspace or one path, module, or target focus before surgical work. Omit focus for workspace Orientation. Workspace Orientation includes manifest and dependency inventories. For one-file work, use focused path Orientation. Directory focus may surface local instruction files.";
2
+ "Return observed workspace or focused code context to choose what source to inspect next. Omit `focus` for workspace context. A directory focus can surface local instruction files.";
3
3
 
4
- export const promptSnippet = "workspace/path/module/symbol Orientation";
4
+ export const promptSnippet = "orient around workspace or code context";
5
5
 
6
6
  export const promptGuidelines = [
7
- "Use code_orientation before broad file reading when you need direct workspace, package, directory, file, or symbol facts.",
8
- "Use code_graph for relationships and code_health for provider or diagnostic state.",
7
+ "Use code_orientation before broad source reading when workspace or path context can narrow the files.",
9
8
  ];