@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.
- package/README.md +130 -0
- package/dist/chunk-5I2DBHOQ.js +1042 -0
- package/dist/chunk-5I2DBHOQ.js.map +1 -0
- package/dist/chunk-CPYJACC5.js +1838 -0
- package/dist/chunk-CPYJACC5.js.map +1 -0
- package/dist/chunk-ZVCMIM4T.js +216 -0
- package/dist/chunk-ZVCMIM4T.js.map +1 -0
- package/dist/cypher/query-parser.d.ts +253 -0
- package/dist/cypher/query-parser.js +17 -0
- package/dist/cypher/query-parser.js.map +1 -0
- package/dist/graph-schema.d.ts +84 -0
- package/dist/graph-schema.js +17 -0
- package/dist/graph-schema.js.map +1 -0
- package/dist/graph-store.d.ts +938 -0
- package/dist/graph-store.js +16 -0
- package/dist/graph-store.js.map +1 -0
- package/dist/index.d.ts +1953 -0
- package/dist/index.js +3509 -0
- package/dist/index.js.map +1 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +79 -0
- package/src/co-change.test.ts +175 -0
- package/src/co-change.ts +167 -0
- package/src/cypher/query-parser.test.ts +1107 -0
- package/src/cypher/query-parser.ts +1692 -0
- package/src/detect-changes.test.ts +533 -0
- package/src/detect-changes.ts +367 -0
- package/src/engine/emit.ts +556 -0
- package/src/engine/engine.test.ts +1417 -0
- package/src/engine/engine.ts +182 -0
- package/src/engine/extractors.ts +486 -0
- package/src/engine/fixtures.ts +364 -0
- package/src/engine/language-sniff.ts +56 -0
- package/src/engine/parser-backend.ts +206 -0
- package/src/engine/utf16-offsets.ts +68 -0
- package/src/git-invoker.test.ts +116 -0
- package/src/git-invoker.ts +426 -0
- package/src/graph-schema.test.ts +541 -0
- package/src/graph-schema.ts +383 -0
- package/src/graph-store-pr2.test.ts +1879 -0
- package/src/graph-store.test.ts +1420 -0
- package/src/graph-store.ts +3489 -0
- package/src/index-status.test.ts +303 -0
- package/src/index-status.ts +135 -0
- package/src/index.ts +384 -0
- package/src/lsp/byte-position.ts +173 -0
- package/src/lsp/characterization.test.ts +174 -0
- package/src/lsp/client.test.ts +275 -0
- package/src/lsp/client.ts +484 -0
- package/src/lsp/config.ts +219 -0
- package/src/lsp/degradation.ts +86 -0
- package/src/lsp/fixtures/fake-server.mjs +198 -0
- package/src/lsp/framing.test.ts +180 -0
- package/src/lsp/framing.ts +177 -0
- package/src/lsp/resolution.test.ts +497 -0
- package/src/lsp/resolution.ts +483 -0
- package/src/lsp/status.ts +140 -0
- package/src/lsp/types.ts +167 -0
- package/src/reindex.test.ts +1038 -0
- package/src/reindex.ts +908 -0
- package/src/row-types.ts +45 -0
- package/src/semantic/canonical-text.test.ts +150 -0
- package/src/semantic/canonical-text.ts +219 -0
- package/src/semantic/config.ts +235 -0
- package/src/semantic/index.ts +78 -0
- package/src/semantic/minhash.test.ts +197 -0
- package/src/semantic/minhash.ts +261 -0
- package/src/semantic/semantic-query.ts +173 -0
- package/src/semantic/semantic.test.ts +1315 -0
- package/src/semantic/similarity.ts +268 -0
- package/src/semantic/types.ts +145 -0
- 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
|
+
}
|