@remnic/coding-graph 9.6.38 → 9.6.39
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/dist/index.d.ts +9 -0
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/lsp/resolution.test.ts +125 -0
- package/src/lsp/resolution.ts +35 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/coding-graph",
|
|
3
|
-
"version": "9.6.
|
|
3
|
+
"version": "9.6.39",
|
|
4
4
|
"description": "Web-tree-sitter powered symbol-extraction engine + SQLite knowledge-graph store for codebase memory (Tier 1: TypeScript, TSX, JavaScript, Python, Go, Rust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Bash). Optional companion of @remnic/core — install only when coding-graph features are needed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"web-tree-sitter": "^0.25.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@remnic/core": "^9.6.
|
|
44
|
+
"@remnic/core": "^9.6.39"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.10.0",
|
|
48
48
|
"tsup": "^8.5.1",
|
|
49
49
|
"tsx": "^4.19.0",
|
|
50
50
|
"typescript": "^5.7.0",
|
|
51
|
-
"@remnic/core": "9.6.
|
|
51
|
+
"@remnic/core": "9.6.39"
|
|
52
52
|
},
|
|
53
53
|
"license": "MIT",
|
|
54
54
|
"repository": {
|
|
@@ -295,12 +295,135 @@ test("executor: definition returns empty → unresolved", async () => {
|
|
|
295
295
|
client: mockClient,
|
|
296
296
|
nodeLocator: locator,
|
|
297
297
|
applyUpgrades: async () => {},
|
|
298
|
+
// Warm-up retry (issue #1933) is exercised by its own tests below;
|
|
299
|
+
// this test asserts the definitive-empty path.
|
|
300
|
+
warmupRetryDelayMs: 0,
|
|
298
301
|
});
|
|
299
302
|
|
|
300
303
|
assert.equal(result.upgraded, 0);
|
|
301
304
|
assert.equal(result.unresolved, 1);
|
|
302
305
|
});
|
|
303
306
|
|
|
307
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
308
|
+
// Warm-up retry (issue #1933): tsserver answers pre-project-load definition
|
|
309
|
+
// requests with [] instead of an error. An empty result before the server
|
|
310
|
+
// has proven warm is retried once; the retry's answer is final.
|
|
311
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
test("executor: warm-up retry — empty first answer retried once, retry result upgrades", async () => {
|
|
314
|
+
const requests = planLspUpgrades(
|
|
315
|
+
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
316
|
+
{ maxRequests: 100 },
|
|
317
|
+
).requests;
|
|
318
|
+
|
|
319
|
+
// Stateful mock: first definition call returns [], second returns the
|
|
320
|
+
// real location (project finished loading between the two).
|
|
321
|
+
let calls = 0;
|
|
322
|
+
const client = {
|
|
323
|
+
definition: async () => {
|
|
324
|
+
calls += 1;
|
|
325
|
+
if (calls === 1) return { ok: true as const, locations: [] };
|
|
326
|
+
return {
|
|
327
|
+
ok: true as const,
|
|
328
|
+
locations: [{
|
|
329
|
+
uri: "file:///src/target.ts",
|
|
330
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 6 } },
|
|
331
|
+
}],
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
didOpen: () => {},
|
|
335
|
+
dispose: async () => {},
|
|
336
|
+
} as unknown as LspClient;
|
|
337
|
+
|
|
338
|
+
const applied: EdgeUpgrade[] = [];
|
|
339
|
+
const result = await executeLspResolution(requests, {
|
|
340
|
+
client,
|
|
341
|
+
nodeLocator: () => "mod.target",
|
|
342
|
+
applyUpgrades: async (upgrades) => {
|
|
343
|
+
applied.push(...upgrades);
|
|
344
|
+
},
|
|
345
|
+
warmupRetryDelayMs: 5,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
assert.equal(calls, 2, "empty pre-warm answer must be retried exactly once");
|
|
349
|
+
assert.equal(result.upgraded, 1, "the retry's location produces the upgrade");
|
|
350
|
+
assert.equal(result.unresolved, 0);
|
|
351
|
+
assert.equal(applied.length, 1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("executor: warm-up retry — only ONE retry is paid; later empties are definitive", async () => {
|
|
355
|
+
const requests = planLspUpgrades(
|
|
356
|
+
[
|
|
357
|
+
makeCallSite("src/a.ts", "missing1", 0, "a.caller"),
|
|
358
|
+
makeCallSite("src/a.ts", "missing2", 10, "a.caller"),
|
|
359
|
+
],
|
|
360
|
+
{ maxRequests: 100 },
|
|
361
|
+
).requests;
|
|
362
|
+
|
|
363
|
+
let calls = 0;
|
|
364
|
+
const client = {
|
|
365
|
+
definition: async () => {
|
|
366
|
+
calls += 1;
|
|
367
|
+
return { ok: true as const, locations: [] };
|
|
368
|
+
},
|
|
369
|
+
didOpen: () => {},
|
|
370
|
+
dispose: async () => {},
|
|
371
|
+
} as unknown as LspClient;
|
|
372
|
+
|
|
373
|
+
const result = await executeLspResolution(requests, {
|
|
374
|
+
client,
|
|
375
|
+
nodeLocator: () => null,
|
|
376
|
+
applyUpgrades: async () => {},
|
|
377
|
+
warmupRetryDelayMs: 5,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Site 1: initial + one warm-up retry = 2 calls; the server is then
|
|
381
|
+
// considered warm. Site 2: exactly 1 call, its empty answer is final.
|
|
382
|
+
assert.equal(calls, 3, "one warm-up retry total, not one per site");
|
|
383
|
+
assert.equal(result.upgraded, 0);
|
|
384
|
+
assert.equal(result.unresolved, 2);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("executor: warm-up retry — a non-empty FIRST answer marks the server warm (no retry paid)", async () => {
|
|
388
|
+
const requests = planLspUpgrades(
|
|
389
|
+
[
|
|
390
|
+
makeCallSite("src/a.ts", "target", 0, "a.caller"),
|
|
391
|
+
makeCallSite("src/a.ts", "missing", 10, "a.caller"),
|
|
392
|
+
],
|
|
393
|
+
{ maxRequests: 100 },
|
|
394
|
+
).requests;
|
|
395
|
+
|
|
396
|
+
let calls = 0;
|
|
397
|
+
const client = {
|
|
398
|
+
definition: async () => {
|
|
399
|
+
calls += 1;
|
|
400
|
+
if (calls === 1) {
|
|
401
|
+
return {
|
|
402
|
+
ok: true as const,
|
|
403
|
+
locations: [{
|
|
404
|
+
uri: "file:///src/target.ts",
|
|
405
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 6 } },
|
|
406
|
+
}],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return { ok: true as const, locations: [] };
|
|
410
|
+
},
|
|
411
|
+
didOpen: () => {},
|
|
412
|
+
dispose: async () => {},
|
|
413
|
+
} as unknown as LspClient;
|
|
414
|
+
|
|
415
|
+
const result = await executeLspResolution(requests, {
|
|
416
|
+
client,
|
|
417
|
+
nodeLocator: () => "mod.target",
|
|
418
|
+
applyUpgrades: async () => {},
|
|
419
|
+
warmupRetryDelayMs: 5,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
assert.equal(calls, 2, "warm server: the second site's empty answer is definitive, no retry");
|
|
423
|
+
assert.equal(result.upgraded, 1);
|
|
424
|
+
assert.equal(result.unresolved, 1);
|
|
425
|
+
});
|
|
426
|
+
|
|
304
427
|
test("executor: mid-batch applyUpgrades failure → caught, counted as unresolved", async () => {
|
|
305
428
|
const requests = planLspUpgrades(
|
|
306
429
|
[makeCallSite("src/a.ts", "target", 0, "a.caller")],
|
|
@@ -488,6 +611,8 @@ test("executor: workspaceRoot resolves repo-relative paths to absolute URIs", as
|
|
|
488
611
|
nodeLocator: () => null,
|
|
489
612
|
applyUpgrades: async () => {},
|
|
490
613
|
workspaceRoot: "/workspace",
|
|
614
|
+
// This test asserts URI shapes, not warm-up behavior (issue #1933).
|
|
615
|
+
warmupRetryDelayMs: 0,
|
|
491
616
|
});
|
|
492
617
|
|
|
493
618
|
assert.equal(didOpenUris.length, 1);
|
package/src/lsp/resolution.ts
CHANGED
|
@@ -209,6 +209,15 @@ export function planLspUpgrades(
|
|
|
209
209
|
export interface ResolveOptions {
|
|
210
210
|
readonly client: LspClient;
|
|
211
211
|
readonly nodeLocator: NodeLocator;
|
|
212
|
+
/**
|
|
213
|
+
* Warm-up retry delay in ms (issue #1933). Language servers (tsserver
|
|
214
|
+
* in particular) load projects ASYNCHRONOUSLY after `didOpen`; a
|
|
215
|
+
* definition request that arrives too early returns an EMPTY location
|
|
216
|
+
* array — indistinguishable from "definitely no definition". Until the
|
|
217
|
+
* server has proven warm (any non-empty response), an empty result is
|
|
218
|
+
* retried ONCE after this delay. Default 2500. Set 0 to disable.
|
|
219
|
+
*/
|
|
220
|
+
readonly warmupRetryDelayMs?: number;
|
|
212
221
|
/**
|
|
213
222
|
* Apply a batch of edge upgrades atomically. Called once per file batch.
|
|
214
223
|
* MUST be transactional — if it throws, zero upgrades from this batch
|
|
@@ -297,6 +306,14 @@ export async function executeLspResolution(
|
|
|
297
306
|
let unresolved = 0;
|
|
298
307
|
let degradation: LspDegradation | undefined;
|
|
299
308
|
|
|
309
|
+
// Warm-up tracking (issue #1933): false until the server returns its
|
|
310
|
+
// first NON-EMPTY definition, or until one warm-up retry has been paid.
|
|
311
|
+
// tsserver answers pre-project-load requests with [] instead of an
|
|
312
|
+
// error, so without the retry every first-run resolution silently
|
|
313
|
+
// reports unresolved and Phase B never upgrades anything.
|
|
314
|
+
let serverWarm = false;
|
|
315
|
+
const warmupRetryDelayMs = options.warmupRetryDelayMs ?? 2_500;
|
|
316
|
+
|
|
300
317
|
for (const [filePath, batchReqs] of byFile) {
|
|
301
318
|
// Send all definition requests for this file, collecting upgrades.
|
|
302
319
|
const upgrades: EdgeUpgrade[] = [];
|
|
@@ -327,11 +344,28 @@ export async function executeLspResolution(
|
|
|
327
344
|
break;
|
|
328
345
|
}
|
|
329
346
|
|
|
330
|
-
|
|
347
|
+
let defResult = await client.definition({
|
|
331
348
|
textDocument: { uri: filePathToUri(req.filePath, options.workspaceRoot) },
|
|
332
349
|
position: req.position,
|
|
333
350
|
});
|
|
334
351
|
|
|
352
|
+
// Warm-up retry (issue #1933): an empty result from a not-yet-warm
|
|
353
|
+
// server is indeterminate, not definitive. Pay ONE bounded delay,
|
|
354
|
+
// re-ask, and treat the server as warm from then on — whatever the
|
|
355
|
+
// retry returns is the real answer.
|
|
356
|
+
if (defResult.ok && defResult.locations.length === 0 && !serverWarm && warmupRetryDelayMs > 0) {
|
|
357
|
+
await new Promise<void>((resolve) => setTimeout(resolve, warmupRetryDelayMs));
|
|
358
|
+
serverWarm = true;
|
|
359
|
+
const retryResult = await client.definition({
|
|
360
|
+
textDocument: { uri: filePathToUri(req.filePath, options.workspaceRoot) },
|
|
361
|
+
position: req.position,
|
|
362
|
+
});
|
|
363
|
+
defResult = retryResult;
|
|
364
|
+
}
|
|
365
|
+
if (defResult.ok && defResult.locations.length > 0) {
|
|
366
|
+
serverWarm = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
335
369
|
if (!defResult.ok) {
|
|
336
370
|
// Distinguish "server problem" (stop the whole pass) from
|
|
337
371
|
// "this particular definition returned nothing" (continue).
|