@remnic/coding-graph 9.3.759

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 (86) hide show
  1. package/README.md +130 -0
  2. package/dist/chunk-5I2DBHOQ.js +1042 -0
  3. package/dist/chunk-5I2DBHOQ.js.map +1 -0
  4. package/dist/chunk-CPYJACC5.js +1838 -0
  5. package/dist/chunk-CPYJACC5.js.map +1 -0
  6. package/dist/chunk-ZVCMIM4T.js +216 -0
  7. package/dist/chunk-ZVCMIM4T.js.map +1 -0
  8. package/dist/cypher/query-parser.d.ts +253 -0
  9. package/dist/cypher/query-parser.js +17 -0
  10. package/dist/cypher/query-parser.js.map +1 -0
  11. package/dist/graph-schema.d.ts +84 -0
  12. package/dist/graph-schema.js +17 -0
  13. package/dist/graph-schema.js.map +1 -0
  14. package/dist/graph-store.d.ts +938 -0
  15. package/dist/graph-store.js +16 -0
  16. package/dist/graph-store.js.map +1 -0
  17. package/dist/index.d.ts +1953 -0
  18. package/dist/index.js +3509 -0
  19. package/dist/index.js.map +1 -0
  20. package/grammars/tree-sitter-bash.wasm +0 -0
  21. package/grammars/tree-sitter-c.wasm +0 -0
  22. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  23. package/grammars/tree-sitter-cpp.wasm +0 -0
  24. package/grammars/tree-sitter-go.wasm +0 -0
  25. package/grammars/tree-sitter-java.wasm +0 -0
  26. package/grammars/tree-sitter-javascript.wasm +0 -0
  27. package/grammars/tree-sitter-kotlin.wasm +0 -0
  28. package/grammars/tree-sitter-php.wasm +0 -0
  29. package/grammars/tree-sitter-python.wasm +0 -0
  30. package/grammars/tree-sitter-ruby.wasm +0 -0
  31. package/grammars/tree-sitter-rust.wasm +0 -0
  32. package/grammars/tree-sitter-swift.wasm +0 -0
  33. package/grammars/tree-sitter-tsx.wasm +0 -0
  34. package/grammars/tree-sitter-typescript.wasm +0 -0
  35. package/package.json +79 -0
  36. package/src/co-change.test.ts +175 -0
  37. package/src/co-change.ts +167 -0
  38. package/src/cypher/query-parser.test.ts +1107 -0
  39. package/src/cypher/query-parser.ts +1692 -0
  40. package/src/detect-changes.test.ts +533 -0
  41. package/src/detect-changes.ts +367 -0
  42. package/src/engine/emit.ts +556 -0
  43. package/src/engine/engine.test.ts +1417 -0
  44. package/src/engine/engine.ts +182 -0
  45. package/src/engine/extractors.ts +486 -0
  46. package/src/engine/fixtures.ts +364 -0
  47. package/src/engine/language-sniff.ts +56 -0
  48. package/src/engine/parser-backend.ts +206 -0
  49. package/src/engine/utf16-offsets.ts +68 -0
  50. package/src/git-invoker.test.ts +116 -0
  51. package/src/git-invoker.ts +426 -0
  52. package/src/graph-schema.test.ts +541 -0
  53. package/src/graph-schema.ts +383 -0
  54. package/src/graph-store-pr2.test.ts +1879 -0
  55. package/src/graph-store.test.ts +1420 -0
  56. package/src/graph-store.ts +3489 -0
  57. package/src/index-status.test.ts +303 -0
  58. package/src/index-status.ts +135 -0
  59. package/src/index.ts +384 -0
  60. package/src/lsp/byte-position.ts +173 -0
  61. package/src/lsp/characterization.test.ts +174 -0
  62. package/src/lsp/client.test.ts +275 -0
  63. package/src/lsp/client.ts +484 -0
  64. package/src/lsp/config.ts +219 -0
  65. package/src/lsp/degradation.ts +86 -0
  66. package/src/lsp/fixtures/fake-server.mjs +198 -0
  67. package/src/lsp/framing.test.ts +180 -0
  68. package/src/lsp/framing.ts +177 -0
  69. package/src/lsp/resolution.test.ts +497 -0
  70. package/src/lsp/resolution.ts +483 -0
  71. package/src/lsp/status.ts +140 -0
  72. package/src/lsp/types.ts +167 -0
  73. package/src/reindex.test.ts +1038 -0
  74. package/src/reindex.ts +908 -0
  75. package/src/row-types.ts +45 -0
  76. package/src/semantic/canonical-text.test.ts +150 -0
  77. package/src/semantic/canonical-text.ts +219 -0
  78. package/src/semantic/config.ts +235 -0
  79. package/src/semantic/index.ts +78 -0
  80. package/src/semantic/minhash.test.ts +197 -0
  81. package/src/semantic/minhash.ts +261 -0
  82. package/src/semantic/semantic-query.ts +173 -0
  83. package/src/semantic/semantic.test.ts +1315 -0
  84. package/src/semantic/similarity.ts +268 -0
  85. package/src/semantic/types.ts +145 -0
  86. package/src/semantic/vectors.ts +235 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * LSP resolution pass — upgrades Phase A heuristic edges with real
3
+ * definition-lookup results from language servers.
4
+ *
5
+ * Two halves (issue #1555 step 4):
6
+ *
7
+ * 1. **Planner** (pure): `planLspUpgrades(unresolvedCallSites, budget)`
8
+ * decides which call sites to query and in what order. Pure function —
9
+ * no side effects, no I/O. Deterministic output for deterministic input.
10
+ *
11
+ * 2. **Executor**: sends `textDocument/didOpen` + `textDocument/definition`
12
+ * for each planned request via the LSP client, maps returned locations
13
+ * back to graph nodes by file + half-open span containment (rule 35),
14
+ * and applies edge upgrades transactionally per batch. A mid-batch
15
+ * failure leaves zero partial upgrades (rule 25 — tested).
16
+ *
17
+ * Budgets (issue #1555): `maxRequestsPerRun` caps the worst case. Remaining
18
+ * call sites keep their Phase A resolution. Everything degrades to Phase A
19
+ * results with a tagged degradation surfaced in index_status.
20
+ *
21
+ * Files whose ingest failed are excluded (rule 44 — the executor never
22
+ * queries for a file that isn't in the store).
23
+ */
24
+
25
+ import path from "node:path";
26
+
27
+ import type { CodingGraphLanguage } from "@remnic/core";
28
+
29
+ import type { LspClient } from "./client.js";
30
+ import { uriToPath } from "./client.js";
31
+ import type { LspDegradation } from "./degradation.js";
32
+ import {
33
+ buildLineOffsetMap,
34
+ byteOffsetToPosition,
35
+ positionToByteOffset,
36
+ type LineOffsetMap,
37
+ } from "./byte-position.js";
38
+ import type { LspLocation } from "./types.js";
39
+
40
+ // ──────────────────────────────────────────────────────────────────────────
41
+ // Input types — what the resolution pass consumes
42
+ // ──────────────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * A call site that Phase A left unresolved or at low confidence. The
46
+ * resolution pass will query the LSP server for its definition.
47
+ */
48
+ export interface UnresolvedCallSite {
49
+ /** Repo-relative file path of the CALLER (the file containing the call). */
50
+ readonly filePath: string;
51
+ readonly language: CodingGraphLanguage;
52
+ /** Full file content — needed for byte↔position conversion. */
53
+ readonly content: string;
54
+ /** Byte offset of the callee name in the source (for the definition query position). */
55
+ readonly calleeByteOffset: number;
56
+ /** The callee name as extracted by Phase A (for logging/debugging). */
57
+ readonly calleeName: string;
58
+ /** The caller's qualified name (source node for the edge). */
59
+ readonly srcQualifiedName: string;
60
+ }
61
+
62
+ /**
63
+ * A planned LSP definition request — the planner's output. Each request
64
+ * targets one call site at one position in one file.
65
+ */
66
+ export interface PlannedLspRequest {
67
+ readonly filePath: string;
68
+ readonly language: CodingGraphLanguage;
69
+ readonly content: string;
70
+ readonly calleeName: string;
71
+ readonly srcQualifiedName: string;
72
+ /** LSP position derived from calleeByteOffset via line-offset map. */
73
+ readonly position: { readonly line: number; readonly character: number };
74
+ }
75
+
76
+ /**
77
+ * The planner's budget — how many requests can be sent this run.
78
+ */
79
+ export interface LspBudget {
80
+ readonly maxRequests: number;
81
+ }
82
+
83
+ /**
84
+ * Result of the planner — the requests to send, plus how many call sites
85
+ * were deferred due to budget exhaustion.
86
+ */
87
+ export interface PlanResult {
88
+ readonly requests: readonly PlannedLspRequest[];
89
+ readonly budgetExhausted: number;
90
+ }
91
+
92
+ /**
93
+ * A location that the LSP server returned, mapped to a byte offset in
94
+ * a known file.
95
+ */
96
+ export interface ResolvedLocation {
97
+ readonly filePath: string;
98
+ readonly startByte: number;
99
+ readonly endByte: number;
100
+ }
101
+
102
+ /**
103
+ * Result of the resolution pass.
104
+ */
105
+ export interface ResolutionResult {
106
+ /** Edges upgraded from heuristic → lsp with resolved dst node. */
107
+ readonly upgraded: number;
108
+ /** LSP returned no location or the location didn't map to an indexed node. */
109
+ readonly unresolved: number;
110
+ /** Call sites skipped because maxRequestsPerRun was reached. */
111
+ readonly budgetExhausted: number;
112
+ /** Degradation if the pass could not run (server crashed, protocol error). */
113
+ readonly degradation?: LspDegradation;
114
+ }
115
+
116
+ // ──────────────────────────────────────────────────────────────────────────
117
+ // Node-location lookup — the seam between LSP locations and graph nodes.
118
+ // Implemented by the caller (the store-backed resolution executor) so
119
+ // the planner stays pure and testable without a database.
120
+ // ──────────────────────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Look up a graph node by file path + byte span containment (rule 35 —
124
+ * half-open). Returns the node's qualified name if exactly one node's
125
+ * span contains the byte offset, or null if none/ambiguous.
126
+ *
127
+ * The executor provides this closure backed by the GraphStore; tests
128
+ * inject a mock.
129
+ */
130
+ export type NodeLocator = (
131
+ filePath: string,
132
+ byteOffset: number,
133
+ ) => string | null;
134
+
135
+ /**
136
+ * Context for mapping an LSP location to a graph node. Provides the
137
+ * caller's file path and content (for same-file definitions), plus
138
+ * optional workspace-root normalization and cross-file content resolution.
139
+ */
140
+ export interface MapLocationContext {
141
+ /** Repo-relative path of the caller file. */
142
+ readonly callerFilePath: string;
143
+ /** Full content of the caller file. */
144
+ readonly callerContent: string;
145
+ /** Workspace root for normalizing absolute LSP URIs to repo-relative paths. */
146
+ readonly workspaceRoot?: string;
147
+ /** Resolve content for a target file path (repo-relative). */
148
+ readonly resolveContent?: (filePath: string) => string | null;
149
+ }
150
+
151
+ // ──────────────────────────────────────────────────────────────────────────
152
+ // Planner — pure function, no side effects
153
+ // ──────────────────────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Plan which call sites to resolve via LSP. Pure — deterministic output
157
+ * for deterministic input. Orders requests by file path then byte offset
158
+ * for predictable, reviewable batches (rule 38 — byte-stable ordering).
159
+ *
160
+ * Budget enforcement: at most `budget.maxRequests` requests are planned.
161
+ * Excess call sites are counted in `budgetExhausted` so the caller can
162
+ * surface the degradation.
163
+ */
164
+ export function planLspUpgrades(
165
+ callSites: readonly UnresolvedCallSite[],
166
+ budget: LspBudget,
167
+ ): PlanResult {
168
+ // Sort for deterministic ordering (rule 38): by file path, then byte offset.
169
+ const sorted = [...callSites].sort(
170
+ (a, b) =>
171
+ a.filePath.localeCompare(b.filePath) ||
172
+ a.calleeByteOffset - b.calleeByteOffset,
173
+ );
174
+
175
+ const max = Math.max(0, budget.maxRequests);
176
+ const planned = sorted.slice(0, max);
177
+ const exhausted = sorted.length - planned.length;
178
+
179
+ // Pre-compute line-offset maps per unique file to avoid recomputation
180
+ // in the executor. Group by content identity (same content → same map).
181
+ const maps = new Map<string, LineOffsetMap>();
182
+ const requests: PlannedLspRequest[] = planned.map((cs) => {
183
+ let map = maps.get(cs.filePath);
184
+ if (!map) {
185
+ map = buildLineOffsetMap(cs.content);
186
+ maps.set(cs.filePath, map);
187
+ }
188
+ const position = byteOffsetToPosition(cs.content, cs.calleeByteOffset, map);
189
+ return {
190
+ filePath: cs.filePath,
191
+ language: cs.language,
192
+ content: cs.content,
193
+ calleeName: cs.calleeName,
194
+ srcQualifiedName: cs.srcQualifiedName,
195
+ position,
196
+ };
197
+ });
198
+
199
+ return { requests, budgetExhausted: exhausted };
200
+ }
201
+
202
+ // ──────────────────────────────────────────────────────────────────────────
203
+ // Executor — sends definition requests, maps locations, applies upgrades
204
+ // ──────────────────────────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * Options for the resolution executor.
208
+ */
209
+ export interface ResolveOptions {
210
+ readonly client: LspClient;
211
+ readonly nodeLocator: NodeLocator;
212
+ /**
213
+ * Apply a batch of edge upgrades atomically. Called once per file batch.
214
+ * MUST be transactional — if it throws, zero upgrades from this batch
215
+ * persist (rule 25). Each upgrade is an edge `{srcQualifiedName,
216
+ * dstQualifiedName, type: "CALLS", confidence, provenance: "lsp"}`.
217
+ */
218
+ readonly applyUpgrades: (
219
+ upgrades: readonly EdgeUpgrade[],
220
+ ) => Promise<void>;
221
+ /**
222
+ * Workspace root for resolving repo-relative file paths to absolute LSP
223
+ * URIs and normalizing returned URIs back to repo-relative paths.
224
+ */
225
+ readonly workspaceRoot?: string;
226
+ /**
227
+ * Resolve the content of a target file by repo-relative path. Used for
228
+ * cross-file definition positions — without this, cross-file byte-offset
229
+ * conversion falls back to the caller's content (best-effort).
230
+ */
231
+ readonly resolveContent?: (filePath: string) => string | null;
232
+ }
233
+
234
+ /**
235
+ * A single edge upgrade — the output of a successful definition lookup.
236
+ */
237
+ export interface EdgeUpgrade {
238
+ readonly srcQualifiedName: string;
239
+ readonly dstQualifiedName: string;
240
+ readonly type: string;
241
+ readonly confidence: number;
242
+ readonly provenance: "lsp";
243
+ }
244
+
245
+ /**
246
+ * Map a {@link CodingGraphLanguage} to its LSP languageId string. Most
247
+ * tier-1 languages use their own name; a few differ (tsx → typescriptreact,
248
+ * bash → shellscript).
249
+ */
250
+ const LANGUAGE_ID_MAP: Partial<Record<CodingGraphLanguage, string>> = {
251
+ tsx: "typescriptreact",
252
+ bash: "shellscript",
253
+ };
254
+
255
+ /**
256
+ * Execute the resolution pass: for each planned request, send a
257
+ * `textDocument/didOpen` + `textDocument/definition` query, map the
258
+ * returned location to a graph node, and collect edge upgrades. Upgrades
259
+ * are applied in file-batched transactions — a mid-batch failure leaves
260
+ * zero partial upgrades.
261
+ *
262
+ * Never throws — degrades to Phase A results with a tagged degradation.
263
+ */
264
+ export async function executeLspResolution(
265
+ requests: readonly PlannedLspRequest[],
266
+ options: ResolveOptions,
267
+ ): Promise<ResolutionResult> {
268
+ const { client, nodeLocator, applyUpgrades } = options;
269
+
270
+ // Group requests by file for batched transactional application.
271
+ const byFile = new Map<string, PlannedLspRequest[]>();
272
+ for (const req of requests) {
273
+ let batch = byFile.get(req.filePath);
274
+ if (!batch) {
275
+ batch = [];
276
+ byFile.set(req.filePath, batch);
277
+ }
278
+ batch.push(req);
279
+ }
280
+
281
+ let upgraded = 0;
282
+ let unresolved = 0;
283
+ let degradation: LspDegradation | undefined;
284
+
285
+ for (const [filePath, batchReqs] of byFile) {
286
+ // Send all definition requests for this file, collecting upgrades.
287
+ const upgrades: EdgeUpgrade[] = [];
288
+ let batchFailed = false;
289
+
290
+ // Open the document with full text before querying definitions (LSP 3.17).
291
+ // The server needs the content to answer definition requests accurately.
292
+ const firstReq = batchReqs[0];
293
+ client.didOpen({
294
+ uri: filePathToUri(filePath, options.workspaceRoot),
295
+ languageId: LANGUAGE_ID_MAP[firstReq.language] ?? firstReq.language,
296
+ version: 1,
297
+ text: firstReq.content,
298
+ });
299
+
300
+ for (let i = 0; i < batchReqs.length; i++) {
301
+ const req = batchReqs[i];
302
+ if (degradation) {
303
+ // Server died in a previous batch — count remaining unattempted.
304
+ unresolved += batchReqs.length - i;
305
+ break;
306
+ }
307
+
308
+ const defResult = await client.definition({
309
+ textDocument: { uri: filePathToUri(req.filePath, options.workspaceRoot) },
310
+ position: req.position,
311
+ });
312
+
313
+ if (!defResult.ok) {
314
+ // Distinguish "server problem" (stop the whole pass) from
315
+ // "this particular definition returned nothing" (continue).
316
+ if (
317
+ defResult.degradation.code === "server_crashed" ||
318
+ defResult.degradation.code === "protocol_error"
319
+ ) {
320
+ degradation = defResult.degradation;
321
+ // Count this request plus all remaining unattempted requests
322
+ // in this batch as unresolved for accurate reporting.
323
+ unresolved += batchReqs.length - i;
324
+ // Mark the batch failed so collected upgrades are NOT committed
325
+ // (rule 25 — per-batch atomicity; a degraded batch must not
326
+ // persist partial LSP edges).
327
+ batchFailed = true;
328
+ break;
329
+ }
330
+ // request_timeout / request_error — count as unresolved, continue.
331
+ unresolved++;
332
+ continue;
333
+ }
334
+
335
+ const dstQName = mapLocationToNode(
336
+ defResult.locations,
337
+ {
338
+ callerFilePath: req.filePath,
339
+ callerContent: req.content,
340
+ workspaceRoot: options.workspaceRoot,
341
+ resolveContent: options.resolveContent,
342
+ },
343
+ nodeLocator,
344
+ );
345
+ if (dstQName === null) {
346
+ unresolved++;
347
+ continue;
348
+ }
349
+ upgrades.push({
350
+ srcQualifiedName: req.srcQualifiedName,
351
+ dstQualifiedName: dstQName,
352
+ type: "CALLS",
353
+ confidence: 0.9,
354
+ provenance: "lsp",
355
+ });
356
+ }
357
+
358
+ if (batchFailed) {
359
+ // The batch was poisoned — collected upgrades must NOT be committed.
360
+ // Count them as unresolved for reporting accuracy.
361
+ unresolved += upgrades.length;
362
+ break;
363
+ }
364
+
365
+ // Apply upgrades transactionally per file batch. If the apply throws,
366
+ // zero upgrades from this batch persist (rule 25 — the applyUpgrades
367
+ // callback MUST be transactional).
368
+ if (upgrades.length > 0) {
369
+ try {
370
+ await applyUpgrades(upgrades);
371
+ upgraded += upgrades.length;
372
+ } catch {
373
+ // The apply failed — degrade but don't crash. Upgrades from this
374
+ // batch are lost (the callback's transaction rolled back). Edges
375
+ // from already-applied batches survive (they were in separate
376
+ // transactions — this is the documented per-batch isolation).
377
+ // Count the lost upgrades as unresolved for reporting.
378
+ unresolved += upgrades.length;
379
+ }
380
+ }
381
+ }
382
+
383
+ return {
384
+ upgraded,
385
+ unresolved,
386
+ budgetExhausted: 0, // set by the caller from the planner result
387
+ degradation,
388
+ };
389
+ }
390
+
391
+ // ──────────────────────────────────────────────────────────────────────────
392
+ // Location → node mapping (rule 35 — half-open span containment)
393
+ // ──────────────────────────────────────────────────────────────────────────
394
+
395
+ /**
396
+ * Map an LSP definition location to a graph node's qualified name.
397
+ *
398
+ * Uses half-open span containment (rule 35): the location's start byte
399
+ * must be within `[node.spanStart, node.spanEnd)` for the node to match.
400
+ * If multiple locations are returned, the FIRST one that maps to a node
401
+ * wins (LSP servers typically return the most relevant definition first).
402
+ *
403
+ * For same-file definitions, the caller's content is used for byte-offset
404
+ * conversion (exact). For cross-file definitions, `context.resolveContent`
405
+ * is used to fetch the target file's content; if unavailable, the caller's
406
+ * content is used as a best-effort fallback.
407
+ *
408
+ * Returns null if no location maps to an indexed node.
409
+ */
410
+ export function mapLocationToNode(
411
+ locations: readonly LspLocation[],
412
+ context: MapLocationContext,
413
+ nodeLocator: NodeLocator,
414
+ ): string | null {
415
+ for (const loc of locations) {
416
+ const filePath = normalizeLocationPath(loc.uri, context.workspaceRoot);
417
+
418
+ // Resolve content for this location's file.
419
+ let content: string;
420
+ if (filePath === context.callerFilePath) {
421
+ // Same file as the caller — use the caller's exact content.
422
+ content = context.callerContent;
423
+ } else if (context.resolveContent) {
424
+ // Cross-file — use the content resolver for exact conversion.
425
+ const resolved = context.resolveContent(filePath);
426
+ if (resolved === null) continue; // file not indexed — skip
427
+ content = resolved;
428
+ } else {
429
+ // Cross-file without a resolver — best-effort fallback to caller
430
+ // content. The byte offset may be imprecise for files with different
431
+ // line lengths or Unicode, but the half-open containment check
432
+ // tolerates small offsets.
433
+ content = context.callerContent;
434
+ }
435
+
436
+ const map = buildLineOffsetMap(content);
437
+ const startByte = positionToByteOffset(content, loc.range.start, map);
438
+ const qName = nodeLocator(filePath, startByte);
439
+ if (qName !== null) return qName;
440
+ }
441
+ return null;
442
+ }
443
+
444
+ // ──────────────────────────────────────────────────────────────────────────
445
+ // URI helpers
446
+ // ──────────────────────────────────────────────────────────────────────────
447
+
448
+ /**
449
+ * Convert a repo-relative file path to a `file://` URI for LSP requests.
450
+ * When `workspaceRoot` is provided, relative paths are resolved against
451
+ * it to produce an absolute URI (e.g. `file:///workspace/src/a.ts`). LSP
452
+ * servers do not reinterpret URIs relative to `rootUri`, so the URI must
453
+ * be absolute.
454
+ */
455
+ function filePathToUri(filePath: string, workspaceRoot?: string): string {
456
+ const isAbsolute = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
457
+ if (isAbsolute) {
458
+ return `file://${filePath.replace(/\\/g, "/")}`;
459
+ }
460
+ if (workspaceRoot) {
461
+ const abs = path.join(workspaceRoot, filePath).replace(/\\/g, "/");
462
+ return `file://${abs}`;
463
+ }
464
+ // No workspace root — best-effort for backward compatibility.
465
+ return `file:///${filePath.replace(/\\/g, "/")}`;
466
+ }
467
+
468
+ /**
469
+ * Normalize an LSP `Location.uri` to a repo-relative file path. Strips the
470
+ * workspace root prefix so the result matches the store's canonical paths.
471
+ * Without `workspaceRoot`, returns the absolute path from the URI.
472
+ */
473
+ function normalizeLocationPath(uri: string, workspaceRoot?: string): string {
474
+ const absPath = uriToPath(uri);
475
+ if (workspaceRoot) {
476
+ const root = path.resolve(workspaceRoot);
477
+ const rel = path.relative(root, absPath);
478
+ if (!rel.startsWith("..") && rel !== "") {
479
+ return rel.replace(/\\/g, "/");
480
+ }
481
+ }
482
+ return absPath;
483
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * LSP status surfacing — per-language LSP state for index_status and
3
+ * `remnic doctor` (issue #1555 step 5).
4
+ *
5
+ * Reports per-language: enabled / probed / degraded(code) / requests_used.
6
+ * A missing server is normal, not an error — the status entry shows
7
+ * `probed: false` with a degradation code so the operator knows LSP
8
+ * resolution is available but inactive for that language.
9
+ */
10
+
11
+ import type { LspDegradationCode } from "./degradation.js";
12
+ import type { LspConfig } from "./config.js";
13
+ import type { CodingGraphLanguage } from "@remnic/core";
14
+ import type { ResolutionResult } from "./resolution.js";
15
+
16
+ // ──────────────────────────────────────────────────────────────────────────
17
+ // Status entry — one per configured language
18
+ // ──────────────────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * Per-language LSP status. Surfaced in `index_status` and rendered as a
22
+ * single line by `remnic doctor`.
23
+ */
24
+ export interface LspStatusEntry {
25
+ readonly language: CodingGraphLanguage;
26
+ /** Master switch state (codingGraph.lsp.enabled). */
27
+ readonly enabled: boolean;
28
+ /** Server binary found and initialize handshake succeeded. */
29
+ readonly probed: boolean;
30
+ /** True if the last resolution pass degraded (timeout/crash/protocol). */
31
+ readonly degraded: boolean;
32
+ /** Specific degradation code if `degraded` is true. */
33
+ readonly degradationCode?: LspDegradationCode;
34
+ /** Definition requests sent in the last index run. */
35
+ readonly requestsUsed: number;
36
+ }
37
+
38
+ // ──────────────────────────────────────────────────────────────────────────
39
+ // Status computation
40
+ // ──────────────────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Input for LSP status computation — the probe results and resolution
44
+ * results from the last index run. The caller (the index pipeline)
45
+ * collects these during the run and passes them here.
46
+ */
47
+ export interface LspStatusInput {
48
+ readonly config: LspConfig;
49
+ /**
50
+ * Per-language probe results from the last run. `true` = server found
51
+ * and handshake succeeded.
52
+ */
53
+ readonly probeResults: ReadonlyMap<CodingGraphLanguage, boolean>;
54
+ /**
55
+ * Per-language degradation codes from the last run. A language in this
56
+ * map with a code means the resolution pass degraded for that language.
57
+ */
58
+ readonly degradations: ReadonlyMap<CodingGraphLanguage, LspDegradationCode>;
59
+ /**
60
+ * Per-language definition request counts from the last run.
61
+ */
62
+ readonly requestCounts: ReadonlyMap<CodingGraphLanguage, number>;
63
+ /**
64
+ * The languages configured for resolution (from the index run's
65
+ * candidate set). Only these languages get status entries.
66
+ */
67
+ readonly languages: readonly CodingGraphLanguage[];
68
+ }
69
+
70
+ /**
71
+ * Compute per-language LSP status entries. Pure — no side effects.
72
+ * Returns one entry per language in `languages`.
73
+ */
74
+ export function getLspStatus(input: LspStatusInput): readonly LspStatusEntry[] {
75
+ return input.languages.map((lang) => {
76
+ const probed = input.probeResults.get(lang) ?? false;
77
+ const degradationCode = input.degradations.get(lang);
78
+ const requestsUsed = input.requestCounts.get(lang) ?? 0;
79
+ return {
80
+ language: lang,
81
+ enabled: input.config.enabled,
82
+ probed,
83
+ degraded: degradationCode !== undefined,
84
+ degradationCode,
85
+ requestsUsed,
86
+ };
87
+ });
88
+ }
89
+
90
+ // ──────────────────────────────────────────────────────────────────────────
91
+ // Doctor rendering — one line per language
92
+ // ──────────────────────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Render a single LSP status line for `remnic doctor`. Format:
96
+ *
97
+ * typescript: lsp [probed] 12 requests
98
+ * python: lsp [degraded:request_timeout] 3 requests
99
+ * go: lsp [not_probed] 0 requests
100
+ * rust: lsp [disabled]
101
+ */
102
+ export function formatLspStatusLine(entry: LspStatusEntry): string {
103
+ if (!entry.enabled) {
104
+ return `${entry.language}: lsp [disabled]`;
105
+ }
106
+ const parts: string[] = [];
107
+ if (entry.probed) {
108
+ parts.push("probed");
109
+ } else {
110
+ parts.push("not_probed");
111
+ }
112
+ if (entry.degraded && entry.degradationCode) {
113
+ parts[parts.length - 1] = `degraded:${entry.degradationCode}`;
114
+ }
115
+ const stateStr = parts.join(" ");
116
+ return `${entry.language}: lsp [${stateStr}] ${entry.requestsUsed} requests`;
117
+ }
118
+
119
+ // ──────────────────────────────────────────────────────────────────────────
120
+ // Resolution result → status input adapter
121
+ // ──────────────────────────────────────────────────────────────────────────
122
+
123
+ /**
124
+ * Adapter: convert a {@link ResolutionResult} into the per-language
125
+ * degradation + request-count maps that {@link getLspStatus} consumes.
126
+ *
127
+ * The resolution pass runs once per language (each language has its own
128
+ * server). This helper is called once per language to populate the maps.
129
+ */
130
+ export function resolutionResultToStatusMaps(
131
+ language: CodingGraphLanguage,
132
+ result: ResolutionResult,
133
+ degradations: Map<CodingGraphLanguage, LspDegradationCode>,
134
+ requestCounts: Map<CodingGraphLanguage, number>,
135
+ ): void {
136
+ requestCounts.set(language, result.upgraded + result.unresolved);
137
+ if (result.degradation) {
138
+ degradations.set(language, result.degradation.code);
139
+ }
140
+ }