@mrclrchtr/supi-lsp 11.0.1 → 11.1.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/README.md CHANGED
@@ -153,7 +153,7 @@ Caller cancellation or deadline stops only that caller's wait. Explicit diagnost
153
153
 
154
154
  If another request first opens a document during an active semantic or diagnostic request, the client can synchronize and repeat the request once. The retry retains the caller's original signal and deadline. Content, workspace, close, and lifecycle changes still reject stale results. A diagnostic retry collects new request evidence; it does not promote an old cache. This retry is not route recovery. A second enrollment during the retry still rejects the result. Semantic failure reasons state whether the retry failed or was exhausted. The bounded `request.retry` debug event records the retry reason and outcome with the public call's opaque operation ID, when supplied. A completed RPC alone does not establish accepted semantic evidence.
155
155
 
156
- A read-only semantic request can also retry once after an LSP `ContentModified` error (`-32801`). This retry shares the same one-retry budget as enrollment; it does not add another try. The client keeps the original request parameters and checks changes from the first failed input revision. It runs the full input sync before retry and checks current inputs before it accepts a result. An enrollment is still the only input change that can permit a retry. A content, workspace, close, lifecycle, or retirement change blocks it. Client replacement, caller cancellation, and a deadline also block retry. The post-request check rejects a result if inputs change while that attempt runs. This error-code retry applies only to read-only semantic requests. Diagnostics, rename, code actions, and refactors keep the default policy and do not retry `ContentModified`. See the [LSP 3.17 message-ordering rule](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageOrdering), the [retry guard](src/client/client-request-enrollment.ts), and the [client query path](src/client/client.ts).
156
+ A read-only semantic request can also retry once after an LSP `ContentModified` error (`-32801`). This retry shares the same one-retry budget as enrollment; it does not add another try. Before this protocol retry, the client waits 100 ms so an immediate retry does not race the same server analysis. The wait is cancellable, respects the original absolute deadline, and does not delay enrollment-only retries. The client keeps the original request parameters and checks changes from the first failed input revision. It runs the full input sync before retry and checks current inputs before it accepts a result. An enrollment is still the only input change that can permit a retry. A content, workspace, close, lifecycle, or retirement change blocks it. Client replacement, caller cancellation, and a deadline also block retry. The post-request check rejects a result if inputs change while that attempt runs. This error-code retry applies only to read-only semantic requests. Diagnostics, rename, code actions, and refactors keep the default policy and do not retry `ContentModified`. See the [LSP 3.17 message-ordering rule](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageOrdering), the [retry guard](src/client/client-request-enrollment.ts), and the [client query path](src/client/client.ts).
157
157
 
158
158
  Diagnostic collection does not close and reopen a document to obtain confirmation. It does not send a no-op `didChange` for that purpose. Refresh retains unchanged documents and their current server state, and resynchronizes changed or invalidated documents. A client restart may reopen tracked documents to restore client state; that is recovery, not diagnostic confirmation.
159
159
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-runtime",
3
- "version": "11.0.1",
3
+ "version": "11.1.0",
4
4
  "description": "Shared workspace context and capability contracts for code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.0.1",
3
+ "version": "11.1.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,8 @@ export interface DebugRegistryConfig {
14
14
  enabled: boolean;
15
15
  /** What the agent-callable debug tool may return. */
16
16
  agentAccess: DebugAgentAccess;
17
+ /** Whether Agent Run retrospective capture is explicitly enabled. */
18
+ agentRunRetrospectivesEnabled: boolean;
17
19
  /** Maximum number of session-local events to keep in memory. */
18
20
  maxEvents: number;
19
21
  }
@@ -21,6 +23,7 @@ export interface DebugRegistryConfig {
21
23
  export const DEBUG_REGISTRY_DEFAULTS: DebugRegistryConfig = {
22
24
  enabled: false,
23
25
  agentAccess: "sanitized",
26
+ agentRunRetrospectivesEnabled: false,
24
27
  maxEvents: 100,
25
28
  };
26
29
 
@@ -75,6 +78,9 @@ export interface DebugEventQueryResult {
75
78
  /** Receives a sanitized event whenever the registry records one. */
76
79
  export type DebugEventListener = (event: DebugEventView) => void;
77
80
 
81
+ /** Receives sanitized events for persistence; separate from ordinary event subscriptions. */
82
+ export type DebugPersistenceSink = (event: DebugEventView) => void;
83
+
78
84
  export interface DebugSummary {
79
85
  total: number;
80
86
  byLevel: Partial<Record<DebugLevel, number>>;
@@ -85,6 +91,7 @@ interface DebugRegistryState {
85
91
  config: DebugRegistryConfig;
86
92
  events: DebugEvent[];
87
93
  listeners: Set<DebugEventListener>;
94
+ persistenceSinks: Set<DebugPersistenceSink>;
88
95
  nextId: number;
89
96
  }
90
97
 
@@ -111,13 +118,15 @@ function getState(): DebugRegistryState {
111
118
  config: cloneConfig(DEBUG_REGISTRY_DEFAULTS),
112
119
  events: [],
113
120
  listeners: new Set(),
121
+ persistenceSinks: new Set(),
114
122
  nextId: 1,
115
123
  };
116
124
  (globalThis as Record<symbol, unknown>)[REGISTRY_KEY] = state;
117
125
  }
118
126
  // Keep the shared registry compatible with extension reloads that reuse an
119
- // instance created before listeners existed.
127
+ // instance created before listener sets existed.
120
128
  if (!state.listeners) state.listeners = new Set();
129
+ if (!state.persistenceSinks) state.persistenceSinks = new Set();
121
130
  return state;
122
131
  }
123
132
 
@@ -200,6 +209,19 @@ export function isDebugRegistryEnabled(): boolean {
200
209
  return getState().config.enabled;
201
210
  }
202
211
 
212
+ /**
213
+ * Return whether a retrospective can be captured: Debug and its explicit opt-in
214
+ * must be enabled, and at least one persistence sink must be registered.
215
+ */
216
+ export function canRecordAgentRunRetrospective(): boolean {
217
+ const state = getState();
218
+ return (
219
+ state.config.enabled &&
220
+ state.config.agentRunRetrospectivesEnabled &&
221
+ state.persistenceSinks.size > 0
222
+ );
223
+ }
224
+
203
225
  /** Best-effort redaction helper for data exposed through sanitized debug views. */
204
226
  export function redactDebugData<T>(value: T): T {
205
227
  return redactValue(value, 8) as T;
@@ -226,6 +248,16 @@ export function subscribeDebugEvents(listener: DebugEventListener): () => void {
226
248
  return () => state.listeners.delete(listener);
227
249
  }
228
250
 
251
+ /**
252
+ * Register a sanitized event persistence sink. Sinks are tracked separately from
253
+ * ordinary listeners so eligibility checks only succeed when logging is active.
254
+ */
255
+ export function subscribeDebugPersistence(sink: DebugPersistenceSink): () => void {
256
+ const state = getState();
257
+ state.persistenceSinks.add(sink);
258
+ return () => state.persistenceSinks.delete(sink);
259
+ }
260
+
229
261
  /** Record a session-local debug event if debugging is enabled. */
230
262
  export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
231
263
  const state = getState();
@@ -251,6 +283,13 @@ export function recordDebugEvent(input: DebugEventInput): DebugEvent | null {
251
283
  // Debug-event consumers must not alter producer behavior.
252
284
  }
253
285
  }
286
+ for (const sink of state.persistenceSinks) {
287
+ try {
288
+ sink(toSanitizedView(event));
289
+ } catch {
290
+ // Persistence failures must not alter producer behavior.
291
+ }
292
+ }
254
293
  return {
255
294
  ...event,
256
295
  data: event.data === undefined ? undefined : redactDebugData(event.data),
@@ -309,5 +348,6 @@ export function resetDebugRegistry(): void {
309
348
  state.config = cloneConfig(DEBUG_REGISTRY_DEFAULTS);
310
349
  state.events = [];
311
350
  state.listeners.clear();
351
+ state.persistenceSinks.clear();
312
352
  state.nextId = 1;
313
353
  }
@@ -282,6 +282,12 @@ Returns `function(path)` the filter function.
282
282
 
283
283
  > New in 5.0.0
284
284
 
285
+ **This method is NOT meant for ordinary users or everyday use.** If all you need is to tell whether a `pathname` is ignored, this is not the method you are looking for.
286
+
287
+ `.test()` is meant for packages that build upon `ignore`. Besides whether a `pathname` is ignored, it tells whether a negative pattern unignored it, which such a package can use to control its own caching, such as what to cache and when a cached result can be reused.
288
+
289
+ If you only need to know whether a `pathname` should be ignored, use `.ignores()` instead. It returns that answer as a boolean, and always agrees with `.test(pathname).ignored`.
290
+
285
291
  Returns `TestResult`
286
292
 
287
293
  ```ts
@@ -291,7 +297,8 @@ interface TestResult {
291
297
  // true if a negative pattern matched the `pathname` itself, and no parent
292
298
  // directory of it is ignored -- see "When both are false" below
293
299
  unignored: boolean
294
- // The `IgnoreRule` which ignores the pathname
300
+ // The `IgnoreRule` which ignores the pathname: the last rule to match, the
301
+ // one `git check-ignore -v` reports
295
302
  rule?: IgnoreRule
296
303
  }
297
304
 
@@ -339,13 +346,15 @@ To tell the two situations apart, test the parent directories of the `pathname`,
339
346
  ['foobar/'].some(dir => ig2.test(dir).unignored) // true
340
347
  ```
341
348
 
342
- `unignored` is meant for packages that implement their own caching mechanism upon `node-ignore`. To simply tell whether a `pathname` is ignored, use `.ignores()` or check `TestResult::ignored`.
343
-
344
349
  ## .checkIgnore(target: string): TestResult
345
350
 
346
351
  > new in 7.0.0
347
352
 
348
- Debugs gitignore / exclude files, which is equivalent to `git check-ignore -v`. Usually this method is used for other packages to implement the function of `git check-ignore -v` upon `node-ignore`
353
+ Debugs gitignore / exclude files the way `git check-ignore -v` does. Usually this method is used for other packages to implement the function of `git check-ignore -v` upon `node-ignore`. Apart from the [known differences](#known-differences-from-git):
354
+
355
+ - `ignored` is what `git check-ignore` answers for `target`.
356
+ - When `ignored` is `true`, `rule` is the rule `git check-ignore -v` reports. Its `pattern` is the pattern as it was added, so trailing spaces that git trims are still there.
357
+ - When the last pattern to match is a negative one, `git check-ignore -v` reports it, while `.checkIgnore()` returns `unignored: true` and no `rule`. For a directory passed with a trailing slash, `unignored` is also `true` when a negative pattern re-includes the directory itself, which `git check-ignore -v` does not report.
349
358
 
350
359
  - **target** `string` the target to test.
351
360
 
@@ -471,13 +480,30 @@ not in `git`. For ASCII paths and patterns there is no difference.
471
480
  code runs against a real repository, pass the value of the repository's
472
481
  `core.ignorecase` explicitly.
473
482
 
474
- ### `***` and friends follow the documentation, not the binary
483
+ ### A run of asterisks
484
+
485
+ `git` reads a run of two or more asterisks as a globstar whenever the run
486
+ fills a path segment: it starts the pattern or follows a `/`, and it ends
487
+ the pattern or comes before a `/`. `ignore` does the same, so `***/foo`,
488
+ `a/***` and `a/***/b` behave exactly as `**/foo`, `a/**` and `a/**/b`.
489
+
490
+ `git` takes two more cases as a globstar, and `ignore` does not:
491
+
492
+ - **A run right after a literal prefix**, as in `a**/b` or `a***/b`. Before
493
+ matching, `git` strips the part of a pattern that holds no wildcard, and
494
+ the run then starts what is left. gitignore(5) lists the positions where
495
+ `**` is special and calls every other run "regular asterisks", which is
496
+ what `ignore` does here.
497
+ - **A run next to an escaped slash**, as in `**\/b` or `x\/**/b`.
498
+
499
+ ```js
500
+ ignore().add('a**/b').ignores('axy/z/b') // false; git: true
501
+ ignore().add('**\\/b').ignores('x/y/b') // false; git: true
502
+ ignore().add('x\\/**/b').ignores('x/y/z/b') // false; git: true
503
+ ```
475
504
 
476
- gitignore(5) says a run of more than two asterisks is "considered regular
477
- asterisks". The `git` binary, however, strips the literal prefix of a
478
- pattern before matching, which makes patterns like `***/foo`, `a/***` and
479
- `a**/b` behave as `**` globstars there. `ignore` follows the documented
480
- behavior. If you need the globstar, write `**`.
505
+ To get a globstar in either case, give the run a segment of its own, such as
506
+ `a*/**/b`.
481
507
 
482
508
  ### `checkIgnore()` and a directory passed with a trailing slash
483
509
 
@@ -50,8 +50,9 @@ declare namespace ignore {
50
50
  */
51
51
  unignored: boolean
52
52
  /**
53
- * The rule that ignores the pathname. Present exactly when `ignored` is
54
- * true.
53
+ * The rule that ignores the pathname, or the parent directory it is
54
+ * ignored with: the last rule to match, which is the rule
55
+ * `git check-ignore -v` reports. Present exactly when `ignored` is true.
55
56
  */
56
57
  rule?: IgnoreRule
57
58
  }
@@ -15,6 +15,8 @@ const ESCAPE = '\\'
15
15
  const REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/
16
16
  const BOM = '\uFEFF'
17
17
  const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/
18
+ // A run of two or more asterisks is a globstar (#175)
19
+ const REGEX_ANCHORED_GLOBSTAR = /^\/\*{2,}$/
18
20
  const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/
19
21
  const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/
20
22
  const REGEX_SPLITALL_CRLF = /\r?\n/g
@@ -423,6 +425,21 @@ const REPLACERS = [
423
425
  ESCAPE + SPACE
424
426
  ],
425
427
 
428
+ // git's wildmatch() reads a run of two or more asterisks as one globstar
429
+ // whenever the run fills a path segment: it starts the pattern or follows
430
+ // a `/`, and it ends the pattern or comes before a `/`. A run of two is
431
+ // already `**`; a longer one is folded into `**` here, so the steps
432
+ // below only ever have to know `**`.
433
+ // `***/foo` -> `**/foo`, `a/***/b` -> `a/**/b`, `a/***` -> `a/**`
434
+ // git also takes a run beside a literal prefix (`a***/b`) or beside an
435
+ // escaped slash (`***\/b`) as a globstar, and this does not; the README
436
+ // lists both, under "A run of asterisks".
437
+ [
438
+ /(^|\/)\*{3,}(?=\/|$)/g,
439
+ (_, start) => `${start}**`,
440
+ '*'
441
+ ],
442
+
426
443
  // Escape metacharacters
427
444
  // which is written down by users but means special for regular expressions.
428
445
 
@@ -853,6 +870,15 @@ const pinWildcards = source => {
853
870
 
854
871
  // @param {pattern}
855
872
  const makeRegexPrefix = pattern => {
873
+ // `/**` matches every path, as `**` does. The translation below only finds
874
+ // a globstar beside a slash it keeps, and the leading one is turned into
875
+ // an anchor, so `/**` would come out as `/*` and match one level only.
876
+ // Here rather than in `createRule`, so that `checkSourceOf`, which
877
+ // compiles `/**/` again without its trailing slash, gets it too.
878
+ if (REGEX_ANCHORED_GLOBSTAR.test(pattern)) {
879
+ pattern = '**'
880
+ }
881
+
856
882
  const {source, sources} = extractBrackets(pattern)
857
883
 
858
884
  const replaced = REPLACERS.reduce(
@@ -1271,17 +1297,10 @@ class RuleManager {
1271
1297
 
1272
1298
  // Test one single path without recursively checking parent directories
1273
1299
  //
1274
- // - checkUnignored `boolean` whether should check if the path is unignored,
1275
- // setting `checkUnignored` to `false` could reduce additional
1276
- // path matching.
1277
- // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
1278
-
1279
- // @returns {TestResult} true if a file is ignored
1280
- test (path, checkUnignored, mode) {
1281
- let ignored = false
1282
- let unignored = false
1283
- let matchedRule
1300
+ // - mode `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
1284
1301
 
1302
+ // @returns {TestResult}
1303
+ test (path, mode) {
1285
1304
  // Most of a .gitignore is patterns with no slash in them, and running
1286
1305
  // those against the whole path makes the regular expression engine walk
1287
1306
  // every directory name on the way to the only segment that could match.
@@ -1296,52 +1315,40 @@ class RuleManager {
1296
1315
  ? basenameOf(path)
1297
1316
  : path
1298
1317
 
1318
+ // The last rule to match decides, as in git, and it is the rule
1319
+ // `git check-ignore -v` reports, so the scan runs from the end and stops
1320
+ // at the first match.
1299
1321
  // A plain loop rather than `forEach`, so that `path`, `basename` and
1300
1322
  // `shortcut` are locals. As a callback they became closure variables,
1301
1323
  // and reaching for one of those once per rule cost 10% of a scan over a
1302
1324
  // large rule set -- more than the shortcut they were there to serve.
1303
- for (let index = 0; index < length; index ++) {
1325
+ for (let index = length - 1; index >= 0; index --) {
1304
1326
  const rule = rules[index]
1305
- const {negative} = rule
1306
-
1307
- // | ignored : unignored
1308
- // -------- | ---------------------------------------
1309
- // negative | 0:0 | 0:1 | 1:0 | 1:1
1310
- // -------- | ------- | ------- | ------- | --------
1311
- // 0 | TEST | TEST | SKIP | X
1312
- // 1 | TESTIF | SKIP | TEST | X
1313
-
1314
- // - SKIP: always skip
1315
- // - TEST: always test
1316
- // - TESTIF: only test if checkUnignored
1317
- // - X: that never happen
1318
- const skip = unignored === negative && ignored !== unignored
1319
- || negative && !ignored && !unignored && !checkUnignored
1320
-
1321
- if (!skip && rule[mode].test(
1327
+
1328
+ if (rule[mode].test(
1322
1329
  shortcut && rule._basenameOnly
1323
1330
  ? basename
1324
1331
  : path
1325
1332
  )) {
1326
- ignored = !negative
1327
- unignored = negative
1333
+ const {negative} = rule
1328
1334
 
1329
- matchedRule = negative
1330
- ? UNDEFINED
1331
- : rule
1332
- }
1333
- }
1335
+ const ret = {
1336
+ ignored: !negative,
1337
+ unignored: negative
1338
+ }
1334
1339
 
1335
- const ret = {
1336
- ignored,
1337
- unignored
1338
- }
1340
+ if (!negative) {
1341
+ ret.rule = rule
1342
+ }
1339
1343
 
1340
- if (matchedRule) {
1341
- ret.rule = matchedRule
1344
+ return ret
1345
+ }
1342
1346
  }
1343
1347
 
1344
- return ret
1348
+ return {
1349
+ ignored: false,
1350
+ unignored: false
1351
+ }
1345
1352
  }
1346
1353
  }
1347
1354
 
@@ -1457,7 +1464,7 @@ class Ignore {
1457
1464
  }
1458
1465
 
1459
1466
  // @returns {TestResult}
1460
- _test (originalPath, cache, checkUnignored) {
1467
+ _test (originalPath, cache) {
1461
1468
  const path = originalPath
1462
1469
  // Supports nullable path
1463
1470
  && checkPath.convert(originalPath)
@@ -1470,7 +1477,7 @@ class Ignore {
1470
1477
  : RETURN_FALSE
1471
1478
  )
1472
1479
 
1473
- return this._t(path, cache, checkUnignored)
1480
+ return this._t(path, cache)
1474
1481
  }
1475
1482
 
1476
1483
  checkIgnore (path) {
@@ -1483,7 +1490,7 @@ class Ignore {
1483
1490
  // Like `git check-ignore`: a directory excluded by itself or by an
1484
1491
  // ancestor settles it, and only then is the literal 'abc/' matched
1485
1492
  // against the rules (see `checkSourceOf`).
1486
- const dir = this._t(path, this._testCache, true)
1493
+ const dir = this._t(path, this._testCache)
1487
1494
 
1488
1495
  if (dir.ignored) {
1489
1496
  return dir
@@ -1501,10 +1508,7 @@ class Ignore {
1501
1508
  path,
1502
1509
 
1503
1510
  // The cache for the result of a certain checking
1504
- cache,
1505
-
1506
- // Whether should check if the path is unignored
1507
- checkUnignored
1511
+ cache
1508
1512
  ) {
1509
1513
  if (path in cache) {
1510
1514
  return cache[path]
@@ -1514,18 +1518,18 @@ class Ignore {
1514
1518
 
1515
1519
  // If the path contains a parent directory, check the parent first
1516
1520
  const parent = parentPath
1517
- ? this._t(parentPath, cache, checkUnignored)
1521
+ ? this._t(parentPath, cache)
1518
1522
  : UNDEFINED
1519
1523
 
1520
1524
  return cache[path] = parent && parent.ignored
1521
1525
  // > It is not possible to re-include a file if a parent directory of
1522
1526
  // > that file is excluded.
1523
1527
  ? parent
1524
- : this._rules.test(path, checkUnignored, MODE_IGNORE)
1528
+ : this._rules.test(path, MODE_IGNORE)
1525
1529
  }
1526
1530
 
1527
1531
  ignores (path) {
1528
- return this._test(path, this._ignoreCache, false).ignored
1532
+ return this._test(path, this._ignoreCache).ignored
1529
1533
  }
1530
1534
 
1531
1535
  createFilter () {
@@ -1538,7 +1542,7 @@ class Ignore {
1538
1542
 
1539
1543
  // @returns {TestResult}
1540
1544
  test (path) {
1541
- return this._test(path, this._testCache, true)
1545
+ return this._test(path, this._testCache)
1542
1546
  }
1543
1547
  }
1544
1548
 
@@ -26,6 +26,8 @@ var ESCAPE = '\\';
26
26
  var REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/;
27
27
  var BOM = "\uFEFF";
28
28
  var REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
29
+ // A run of two or more asterisks is a globstar (#175)
30
+ var REGEX_ANCHORED_GLOBSTAR = /^\/\*{2,}$/;
29
31
  var REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
30
32
  var REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
31
33
  var REGEX_SPLITALL_CRLF = /\r?\n/g;
@@ -386,6 +388,18 @@ var REPLACERS = [
386
388
  /(\\+)( ?)/g, function (_, run, space) {
387
389
  return space ? run.slice(0, run.length - run.length % 2) + SPACE : run;
388
390
  }, ESCAPE + SPACE],
391
+ // git's wildmatch() reads a run of two or more asterisks as one globstar
392
+ // whenever the run fills a path segment: it starts the pattern or follows
393
+ // a `/`, and it ends the pattern or comes before a `/`. A run of two is
394
+ // already `**`; a longer one is folded into `**` here, so the steps
395
+ // below only ever have to know `**`.
396
+ // `***/foo` -> `**/foo`, `a/***/b` -> `a/**/b`, `a/***` -> `a/**`
397
+ // git also takes a run beside a literal prefix (`a***/b`) or beside an
398
+ // escaped slash (`***\/b`) as a globstar, and this does not; the README
399
+ // lists both, under "A run of asterisks".
400
+ [/(^|\/)\*{3,}(?=\/|$)/g, function (_, start) {
401
+ return "".concat(start, "**");
402
+ }, '*'],
389
403
  // Escape metacharacters
390
404
  // which is written down by users but means special for regular expressions.
391
405
 
@@ -754,6 +768,14 @@ var pinWildcards = function pinWildcards(source) {
754
768
 
755
769
  // @param {pattern}
756
770
  var makeRegexPrefix = function makeRegexPrefix(pattern) {
771
+ // `/**` matches every path, as `**` does. The translation below only finds
772
+ // a globstar beside a slash it keeps, and the leading one is turned into
773
+ // an anchor, so `/**` would come out as `/*` and match one level only.
774
+ // Here rather than in `createRule`, so that `checkSourceOf`, which
775
+ // compiles `/**/` again without its trailing slash, gets it too.
776
+ if (REGEX_ANCHORED_GLOBSTAR.test(pattern)) {
777
+ pattern = '**';
778
+ }
757
779
  var _extractBrackets = extractBrackets(pattern),
758
780
  source = _extractBrackets.source,
759
781
  sources = _extractBrackets.sources;
@@ -1081,19 +1103,12 @@ var RuleManager = /*#__PURE__*/function () {
1081
1103
 
1082
1104
  // Test one single path without recursively checking parent directories
1083
1105
  //
1084
- // - checkUnignored `boolean` whether should check if the path is unignored,
1085
- // setting `checkUnignored` to `false` could reduce additional
1086
- // path matching.
1087
- // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
1106
+ // - mode `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
1088
1107
 
1089
- // @returns {TestResult} true if a file is ignored
1108
+ // @returns {TestResult}
1090
1109
  }, {
1091
1110
  key: "test",
1092
- value: function test(path, checkUnignored, mode) {
1093
- var ignored = false;
1094
- var unignored = false;
1095
- var matchedRule;
1096
-
1111
+ value: function test(path, mode) {
1097
1112
  // Most of a .gitignore is patterns with no slash in them, and running
1098
1113
  // those against the whole path makes the regular expression engine walk
1099
1114
  // every directory name on the way to the only segment that could match.
@@ -1104,40 +1119,31 @@ var RuleManager = /*#__PURE__*/function () {
1104
1119
  var shortcut = this._basenameCount * 2 >= length;
1105
1120
  var basename = shortcut ? basenameOf(path) : path;
1106
1121
 
1122
+ // The last rule to match decides, as in git, and it is the rule
1123
+ // `git check-ignore -v` reports, so the scan runs from the end and stops
1124
+ // at the first match.
1107
1125
  // A plain loop rather than `forEach`, so that `path`, `basename` and
1108
1126
  // `shortcut` are locals. As a callback they became closure variables,
1109
1127
  // and reaching for one of those once per rule cost 10% of a scan over a
1110
1128
  // large rule set -- more than the shortcut they were there to serve.
1111
- for (var index = 0; index < length; index++) {
1129
+ for (var index = length - 1; index >= 0; index--) {
1112
1130
  var rule = rules[index];
1113
- var negative = rule.negative;
1114
-
1115
- // | ignored : unignored
1116
- // -------- | ---------------------------------------
1117
- // negative | 0:0 | 0:1 | 1:0 | 1:1
1118
- // -------- | ------- | ------- | ------- | --------
1119
- // 0 | TEST | TEST | SKIP | X
1120
- // 1 | TESTIF | SKIP | TEST | X
1121
-
1122
- // - SKIP: always skip
1123
- // - TEST: always test
1124
- // - TESTIF: only test if checkUnignored
1125
- // - X: that never happen
1126
- var skip = unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored;
1127
- if (!skip && rule[mode].test(shortcut && rule._basenameOnly ? basename : path)) {
1128
- ignored = !negative;
1129
- unignored = negative;
1130
- matchedRule = negative ? UNDEFINED : rule;
1131
+ if (rule[mode].test(shortcut && rule._basenameOnly ? basename : path)) {
1132
+ var negative = rule.negative;
1133
+ var ret = {
1134
+ ignored: !negative,
1135
+ unignored: negative
1136
+ };
1137
+ if (!negative) {
1138
+ ret.rule = rule;
1139
+ }
1140
+ return ret;
1131
1141
  }
1132
1142
  }
1133
- var ret = {
1134
- ignored: ignored,
1135
- unignored: unignored
1143
+ return {
1144
+ ignored: false,
1145
+ unignored: false
1136
1146
  };
1137
- if (matchedRule) {
1138
- ret.rule = matchedRule;
1139
- }
1140
- return ret;
1141
1147
  }
1142
1148
  }]);
1143
1149
  return RuleManager;
@@ -1249,12 +1255,12 @@ var Ignore = /*#__PURE__*/function () {
1249
1255
  // @returns {TestResult}
1250
1256
  }, {
1251
1257
  key: "_test",
1252
- value: function _test(originalPath, cache, checkUnignored) {
1258
+ value: function _test(originalPath, cache) {
1253
1259
  var path = originalPath
1254
1260
  // Supports nullable path
1255
1261
  && checkPath.convert(originalPath);
1256
1262
  checkPath(path, originalPath, this._strictPathCheck ? throwError : RETURN_FALSE);
1257
- return this._t(path, cache, checkUnignored);
1263
+ return this._t(path, cache);
1258
1264
  }
1259
1265
  }, {
1260
1266
  key: "checkIgnore",
@@ -1268,7 +1274,7 @@ var Ignore = /*#__PURE__*/function () {
1268
1274
  // Like `git check-ignore`: a directory excluded by itself or by an
1269
1275
  // ancestor settles it, and only then is the literal 'abc/' matched
1270
1276
  // against the rules (see `checkSourceOf`).
1271
- var dir = this._t(path, this._testCache, true);
1277
+ var dir = this._t(path, this._testCache);
1272
1278
  if (dir.ignored) {
1273
1279
  return dir;
1274
1280
  }
@@ -1281,25 +1287,23 @@ var Ignore = /*#__PURE__*/function () {
1281
1287
  // The path to be tested
1282
1288
  path,
1283
1289
  // The cache for the result of a certain checking
1284
- cache,
1285
- // Whether should check if the path is unignored
1286
- checkUnignored) {
1290
+ cache) {
1287
1291
  if (path in cache) {
1288
1292
  return cache[path];
1289
1293
  }
1290
1294
  var parentPath = parentOf(path);
1291
1295
 
1292
1296
  // If the path contains a parent directory, check the parent first
1293
- var parent = parentPath ? this._t(parentPath, cache, checkUnignored) : UNDEFINED;
1297
+ var parent = parentPath ? this._t(parentPath, cache) : UNDEFINED;
1294
1298
  return cache[path] = parent && parent.ignored
1295
1299
  // > It is not possible to re-include a file if a parent directory of
1296
1300
  // > that file is excluded.
1297
- ? parent : this._rules.test(path, checkUnignored, MODE_IGNORE);
1301
+ ? parent : this._rules.test(path, MODE_IGNORE);
1298
1302
  }
1299
1303
  }, {
1300
1304
  key: "ignores",
1301
1305
  value: function ignores(path) {
1302
- return this._test(path, this._ignoreCache, false).ignored;
1306
+ return this._test(path, this._ignoreCache).ignored;
1303
1307
  }
1304
1308
  }, {
1305
1309
  key: "createFilter",
@@ -1319,7 +1323,7 @@ var Ignore = /*#__PURE__*/function () {
1319
1323
  }, {
1320
1324
  key: "test",
1321
1325
  value: function test(path) {
1322
- return this._test(path, this._testCache, true);
1326
+ return this._test(path, this._testCache);
1323
1327
  }
1324
1328
  }]);
1325
1329
  return Ignore;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ignore",
3
- "version": "7.0.13",
3
+ "version": "7.0.14",
4
4
  "description": "Ignore is a manager and filter for .gitignore rules, the one used by eslint, gitbook and many others.",
5
5
  "types": "index.d.ts",
6
6
  "files": [
@@ -6,6 +6,11 @@
6
6
 
7
7
  This npm module implements the base messaging protocol spoken between a VSCode language server and a VSCode language client.
8
8
 
9
+ The Node.js entry point requires Node.js 14.17.0 or newer. The runtime abstraction layer exposes
10
+ `RAL().crypto.randomUUID()` for cryptographically generated v4 UUIDs. The browser implementation
11
+ uses native `crypto.randomUUID()` when available and falls back to `crypto.getRandomValues()`,
12
+ including in non-secure contexts. Custom RAL implementations must provide this capability.
13
+
9
14
  The npm module can also be used standalone to establish a [JSON-RPC](http://www.jsonrpc.org/) channel between
10
15
  a client and a server. Below an example how to setup a JSON-RPC connection. First the client side.
11
16
 
@@ -145,6 +145,14 @@ class PromiseImpl {
145
145
  exports.PromiseImpl = PromiseImpl;
146
146
  const _textEncoder = new TextEncoder();
147
147
  const _ril = Object.freeze({
148
+ crypto: Object.freeze({
149
+ randomUUID() {
150
+ if (typeof globalThis.crypto.randomUUID === 'function') {
151
+ return globalThis.crypto.randomUUID();
152
+ }
153
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => (+c ^ (globalThis.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4)))).toString(16));
154
+ }
155
+ }),
148
156
  messageBuffer: Object.freeze({
149
157
  create: (encoding) => new MessageBuffer(encoding)
150
158
  }),
@@ -418,11 +418,16 @@ function createMessageConnection(messageReader, messageWriter, _logger, options)
418
418
  }
419
419
  finally {
420
420
  if (result instanceof Promise) {
421
+ // Both settlements must release the parallelism slot. Written as
422
+ // .then(...).catch(...) the rejection path skipped the decrement, so
423
+ // one failed dispatch left `inFlight` permanently elevated and
424
+ // triggerMessageQueue() bailed out forever after.
421
425
  result.then(() => {
422
- inFlight--;
423
- triggerMessageQueue();
424
426
  }).catch((error) => {
425
427
  logger.error(`Processing message queue failed: ${error.toString()}`);
428
+ }).finally(() => {
429
+ inFlight--;
430
+ triggerMessageQueue();
426
431
  });
427
432
  }
428
433
  else {
@@ -42,7 +42,15 @@ interface _WritableStream {
42
42
  }
43
43
  interface _DuplexStream extends _ReadableStream, _WritableStream {
44
44
  }
45
+ /**
46
+ * Runtime abstraction implemented by this npm module.
47
+ * External implementations are not supported. Changes to this interface do not
48
+ * require a major version bump.
49
+ */
45
50
  interface RAL {
51
+ readonly crypto: {
52
+ randomUUID(): string;
53
+ };
46
54
  readonly applicationJson: {
47
55
  readonly encoder: ContentTypeEncoder;
48
56
  readonly decoder: ContentTypeDecoder;
@@ -46,12 +46,26 @@ export declare class StreamMessageWriter extends WriteableStreamMessageWriter {
46
46
  export declare function generateRandomPipeName(): string;
47
47
  export interface PipeTransport {
48
48
  onConnected(): Promise<[MessageReader, MessageWriter]>;
49
+ /**
50
+ * Closes the underlying server if a client hasn't connected yet. Callers
51
+ * should invoke this if they abandon the transport before `onConnected`
52
+ * resolves (for example because the server process failed to start) to
53
+ * avoid leaking the listening pipe.
54
+ */
55
+ close(): void;
49
56
  }
50
57
  export declare function createClientPipeTransport(pipeName: string, encoding?: RAL.MessageBufferEncoding): Promise<PipeTransport>;
51
58
  export declare function createServerPipeTransport(pipeName: string, encoding?: RAL.MessageBufferEncoding): [MessageReader, MessageWriter];
52
59
  export interface SocketTransport {
53
60
  port(): number;
54
61
  onConnected(): Promise<[MessageReader, MessageWriter]>;
62
+ /**
63
+ * Closes the underlying server if a client hasn't connected yet. Callers
64
+ * should invoke this if they abandon the transport before `onConnected`
65
+ * resolves (for example because the server process failed to start) to
66
+ * avoid leaking the listening socket.
67
+ */
68
+ close(): void;
55
69
  }
56
70
  export declare function createClientSocketTransport(port: number, encoding?: RAL.MessageBufferEncoding): Promise<SocketTransport>;
57
71
  export declare function createServerSocketTransport(port: number, encoding?: RAL.MessageBufferEncoding): [MessageReader, MessageWriter];
@@ -84,7 +84,7 @@ class IPCMessageWriter extends api_1.AbstractMessageWriter {
84
84
  this.errorCount = 0;
85
85
  const eventEmitter = this.process;
86
86
  eventEmitter.on('error', (error) => this.fireError(error));
87
- eventEmitter.on('close', () => this.fireClose);
87
+ eventEmitter.on('close', () => this.fireClose());
88
88
  }
89
89
  write(msg) {
90
90
  try {
@@ -119,7 +119,7 @@ class PortMessageReader extends api_1.AbstractMessageReader {
119
119
  constructor(port) {
120
120
  super();
121
121
  this.onData = new api_1.Emitter;
122
- port.on('close', () => this.fireClose);
122
+ port.on('close', () => this.fireClose());
123
123
  port.on('error', (error) => this.fireError(error));
124
124
  port.on('message', (message) => {
125
125
  this.onData.fire(message);
@@ -227,7 +227,8 @@ function createClientPipeTransport(pipeName, encoding = 'utf-8') {
227
227
  server.listen(pipeName, () => {
228
228
  server.removeListener('error', reject);
229
229
  resolve({
230
- onConnected: () => { return connected; }
230
+ onConnected: () => { return connected; },
231
+ close: () => { server.close(); }
231
232
  });
232
233
  });
233
234
  });
@@ -263,7 +264,8 @@ function createClientSocketTransport(port, encoding = 'utf-8') {
263
264
  const boundPort = address.port;
264
265
  resolve({
265
266
  port: () => boundPort,
266
- onConnected: () => { return connected; }
267
+ onConnected: () => { return connected; },
268
+ close: () => { server.close(); }
267
269
  });
268
270
  });
269
271
  });
@@ -5,6 +5,7 @@
5
5
  * ------------------------------------------------------------------------------------------ */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const util_1 = require("util");
8
+ const crypto_1 = require("crypto");
8
9
  const api_1 = require("../common/api");
9
10
  class MessageBuffer extends api_1.AbstractMessageBuffer {
10
11
  static emptyBuffer = Buffer.allocUnsafe(0);
@@ -30,7 +31,7 @@ class MessageBuffer extends api_1.AbstractMessageBuffer {
30
31
  return buffer instanceof Buffer ? buffer : Buffer.from(buffer);
31
32
  }
32
33
  else {
33
- return buffer instanceof Buffer ? buffer.slice(0, length) : Buffer.from(buffer, 0, length);
34
+ return buffer instanceof Buffer ? buffer.slice(0, length) : Buffer.from(buffer.subarray(0, length));
34
35
  }
35
36
  }
36
37
  allocNative(length) {
@@ -99,6 +100,9 @@ class WritableStreamWrapper {
99
100
  }
100
101
  }
101
102
  const _ril = Object.freeze({
103
+ crypto: Object.freeze({
104
+ randomUUID: () => (0, crypto_1.randomUUID)()
105
+ }),
102
106
  messageBuffer: Object.freeze({
103
107
  create: (encoding) => new MessageBuffer(encoding)
104
108
  }),
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "vscode-jsonrpc",
3
3
  "description": "A json rpc implementation over streams",
4
- "version": "9.0.3",
4
+ "version": "10.0.0",
5
5
  "author": "Microsoft Corporation",
6
6
  "license": "MIT",
7
+ "homepage": "https://github.com/microsoft/vscode-languageserver-node/tree/main/jsonrpc",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/Microsoft/vscode-languageserver-node.git",
@@ -13,7 +14,7 @@
13
14
  "url": "https://github.com/Microsoft/vscode-languageserver-node/issues"
14
15
  },
15
16
  "engines": {
16
- "node": ">=14.0.0"
17
+ "node": ">=14.17.0"
17
18
  },
18
19
  "exports": {
19
20
  ".": {
@@ -31,6 +32,7 @@
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/msgpack-lite": "^0.1.12",
35
+ "@types/node": "22.13.14",
34
36
  "msgpack-lite": "^0.1.26"
35
37
  },
36
38
  "scripts": {
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "vscode-languageserver-protocol",
3
3
  "description": "VSCode Language Server Protocol implementation",
4
- "version": "3.18.4",
4
+ "version": "3.18.5",
5
5
  "author": "Microsoft Corporation",
6
6
  "license": "MIT",
7
+ "homepage": "https://github.com/microsoft/vscode-languageserver-node/tree/main/protocol",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/Microsoft/vscode-languageserver-node.git",
@@ -27,8 +28,8 @@
27
28
  }
28
29
  },
29
30
  "dependencies": {
30
- "vscode-jsonrpc": "9.0.3",
31
- "vscode-languageserver-types": "3.18.4"
31
+ "vscode-jsonrpc": "10.0.0",
32
+ "vscode-languageserver-types": "3.18.5"
32
33
  },
33
34
  "scripts": {
34
35
  "prepublishOnly": "npm run all:publish",
@@ -222,7 +222,7 @@ export var ColorPresentation;
222
222
  function is(value) {
223
223
  const candidate = value;
224
224
  return Is.objectLiteral(candidate) && Is.string(candidate.label)
225
- && (Is.undefined(candidate.textEdit) || TextEdit.is(candidate))
225
+ && (Is.undefined(candidate.textEdit) || TextEdit.is(candidate.textEdit))
226
226
  && (Is.undefined(candidate.additionalTextEdits) || Is.typedArray(candidate.additionalTextEdits, TextEdit.is));
227
227
  }
228
228
  ColorPresentation.is = is;
@@ -279,7 +279,7 @@ export var FoldingRange;
279
279
  */
280
280
  function is(value) {
281
281
  const candidate = value;
282
- return Is.objectLiteral(candidate) && Is.uinteger(candidate.startLine) && Is.uinteger(candidate.startLine)
282
+ return Is.objectLiteral(candidate) && Is.uinteger(candidate.startLine) && Is.uinteger(candidate.endLine)
283
283
  && (Is.undefined(candidate.startCharacter) || Is.uinteger(candidate.startCharacter))
284
284
  && (Is.undefined(candidate.endCharacter) || Is.uinteger(candidate.endCharacter))
285
285
  && (Is.undefined(candidate.kind) || Is.string(candidate.kind));
@@ -233,7 +233,7 @@
233
233
  function is(value) {
234
234
  var candidate = value;
235
235
  return Is.objectLiteral(candidate) && Is.string(candidate.label)
236
- && (Is.undefined(candidate.textEdit) || TextEdit.is(candidate))
236
+ && (Is.undefined(candidate.textEdit) || TextEdit.is(candidate.textEdit))
237
237
  && (Is.undefined(candidate.additionalTextEdits) || Is.typedArray(candidate.additionalTextEdits, TextEdit.is));
238
238
  }
239
239
  ColorPresentation.is = is;
@@ -290,7 +290,7 @@
290
290
  */
291
291
  function is(value) {
292
292
  var candidate = value;
293
- return Is.objectLiteral(candidate) && Is.uinteger(candidate.startLine) && Is.uinteger(candidate.startLine)
293
+ return Is.objectLiteral(candidate) && Is.uinteger(candidate.startLine) && Is.uinteger(candidate.endLine)
294
294
  && (Is.undefined(candidate.startCharacter) || Is.uinteger(candidate.startCharacter))
295
295
  && (Is.undefined(candidate.endCharacter) || Is.uinteger(candidate.endCharacter))
296
296
  && (Is.undefined(candidate.kind) || Is.string(candidate.kind));
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "vscode-languageserver-types",
3
3
  "description": "Types used by the Language server for node",
4
- "version": "3.18.4",
4
+ "version": "3.18.5",
5
5
  "author": "Microsoft Corporation",
6
6
  "license": "MIT",
7
+ "homepage": "https://github.com/microsoft/vscode-languageserver-node/tree/main/types",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/Microsoft/vscode-languageserver-node.git",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "11.0.1",
3
+ "version": "11.1.0",
4
4
  "description": "Language Server Protocol runtime for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,11 +34,11 @@
34
34
  "dependencies": {
35
35
  "ignore": "^7.0.5",
36
36
  "typescript": "6.0.3",
37
- "vscode-jsonrpc": "^9.0.0",
38
- "vscode-languageserver-protocol": "^3.18.0",
39
- "vscode-languageserver-types": "^3.18.0",
40
- "@mrclrchtr/supi-core": "11.0.1",
41
- "@mrclrchtr/supi-code-runtime": "11.0.1"
37
+ "vscode-jsonrpc": "^10.0.0",
38
+ "vscode-languageserver-protocol": "^3.18.5",
39
+ "vscode-languageserver-types": "^3.18.5",
40
+ "@mrclrchtr/supi-code-runtime": "11.1.0",
41
+ "@mrclrchtr/supi-core": "11.1.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -82,6 +82,9 @@ import { SemanticInputSynchronizationError } from "./client-semantic-input-error
82
82
  import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
83
83
 
84
84
  const SHUTDOWN_TIMEOUT_MS = 5_000;
85
+ // ContentModified can outlive the server's progress notifications. Yield before
86
+ // spending the single retry instead of immediately racing the same analysis.
87
+ const CONTENT_MODIFIED_RETRY_DELAY_MS = 100;
85
88
 
86
89
  /** Bounded stderr tail kept for startup-failure error messages. */
87
90
  const STDERR_TAIL_MAX_CHARS = 4_000;
@@ -1498,6 +1501,9 @@ export class LspClient {
1498
1501
  for (;;) {
1499
1502
  const attempt: SemanticRequestAttemptState = { inputRevision: undefined };
1500
1503
  try {
1504
+ if (retryGuard.retryReason === "content-modified") {
1505
+ await this.waitForContentModifiedRetry(control);
1506
+ }
1501
1507
  const data = await this.runSemanticRequestAttempt<T>({
1502
1508
  method,
1503
1509
  params,
@@ -1515,6 +1521,21 @@ export class LspClient {
1515
1521
  }
1516
1522
  }
1517
1523
 
1524
+ /** Pause only the protocol retry, without extending the caller's deadline. */
1525
+ private async waitForContentModifiedRetry(control?: CodeRequestControl): Promise<void> {
1526
+ let timer: ReturnType<typeof setTimeout> | undefined;
1527
+ try {
1528
+ await raceRequestControl(
1529
+ new Promise<void>((resolve) => {
1530
+ timer = setTimeout(resolve, CONTENT_MODIFIED_RETRY_DELAY_MS);
1531
+ }),
1532
+ control,
1533
+ );
1534
+ } finally {
1535
+ if (timer) clearTimeout(timer);
1536
+ }
1537
+ }
1538
+
1518
1539
  /** Return only when another attempt is permitted; retain the cause when retry stops. */
1519
1540
  private prepareSemanticRetry(options: {
1520
1541
  method: string;
@@ -2975,7 +2975,7 @@ export class LspManager {
2975
2975
  control?: CodeRequestControl,
2976
2976
  ): Promise<{ kind: "enrolled" } | { kind: "unavailable"; reason: string }> {
2977
2977
  throwIfCodeRequestInterrupted(control);
2978
- const files = [...new Set(filePaths)].sort();
2978
+ const files = [...new Set(filePaths)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
2979
2979
  const routeKey = clientKey(client.name, client.root);
2980
2980
  if (this.clients.get(routeKey) !== client) {
2981
2981
  return { kind: "unavailable", reason: "The semantic route changed during rename planning." };