@remnic/coding-graph 9.6.38 → 9.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/coding-graph",
3
- "version": "9.6.38",
3
+ "version": "9.7.0",
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.38"
44
+ "@remnic/core": "^9.7.0"
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.38"
51
+ "@remnic/core": "9.7.0"
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);
@@ -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
- const defResult = await client.definition({
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).