@mrclrchtr/supi-lsp 11.0.0 → 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.0",
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.0",
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
  }
@@ -173,6 +173,29 @@ ig.ignores(path.join('./abc')) // path.join('./abc') -> 'abc'
173
173
 
174
174
  In other words, each `Pathname` here should be a relative path to the directory of the gitignore rules.
175
175
 
176
+ A `.` or `..` component anywhere else in a `Pathname` is outside the
177
+ convention as well, though no error is thrown for it: the check reads how the
178
+ path starts, so that it costs nothing on a hot path. `path.relative()` never
179
+ produces such a component, and one that carries it is not matched the way
180
+ `git` matches it — `git` drops the component from the path first, `ignore`
181
+ matches the path as it was given.
182
+
183
+ ```js
184
+ // path.relative('/', '/a/./b') -> 'a/b'
185
+
186
+ // Right
187
+ ignore().add('a/b').ignores('a/b') // true
188
+
189
+ // Outside the convention: git matches 'a/b' here and so says true
190
+ ignore().add('a/b').ignores('a/./b') // false
191
+
192
+ // Outside the convention: git matches 'a' here and so says false
193
+ ignore().add('a/.').ignores('a/.') // true
194
+ ```
195
+
196
+ A rule that matches an ancestor still agrees, since the component makes no
197
+ difference to it: `add('a').ignores('a/./b')` is `true` either way.
198
+
176
199
  Suppose the dir structure is:
177
200
 
178
201
  ```
@@ -259,15 +282,23 @@ Returns `function(path)` the filter function.
259
282
 
260
283
  > New in 5.0.0
261
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
+
262
291
  Returns `TestResult`
263
292
 
264
293
  ```ts
265
294
  // Since 5.0.0
266
295
  interface TestResult {
267
296
  ignored: boolean
268
- // true if the `pathname` is finally unignored by some negative pattern
297
+ // true if a negative pattern matched the `pathname` itself, and no parent
298
+ // directory of it is ignored -- see "When both are false" below
269
299
  unignored: boolean
270
- // 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
271
302
  rule?: IgnoreRule
272
303
  }
273
304
 
@@ -283,18 +314,51 @@ interface IgnoreRule {
283
314
  ```
284
315
 
285
316
  - `{ignored: true, unignored: false}`: the `pathname` is ignored
286
- - `{ignored: false, unignored: true}`: the `pathname` is unignored
287
- - `{ignored: false, unignored: false}`: the `pathname` is never matched by any ignore rules.
317
+ - `{ignored: false, unignored: true}`: a negative pattern matched the `pathname` itself
318
+ - `{ignored: false, unignored: false}`: no rule matched the `pathname` itself, and no parent directory of it is ignored
319
+
320
+ ### When both are `false`
321
+
322
+ `{ignored: false, unignored: false}` covers two different situations:
323
+
324
+ - no rule matched the `pathname`, nor any of its parent directories, or
325
+ - no rule matched the `pathname` itself, but one of its parent directories is unignored by a negative pattern: the `pathname` is unignored **indirectly**.
326
+
327
+ `unignored` stays `false` in the second case because only `ignored` passes down from a parent directory. A `pathname` whose parent directory is ignored is ignored as well — git never re-includes a file whose parent directory is excluded — but one whose parent directory is unignored is decided on its own, by the rules that match the `pathname` itself.
328
+
329
+ ```js
330
+ const ig2 = ignore().add([
331
+ 'foo*',
332
+ '!foobar'
333
+ ])
334
+
335
+ // The directory `foobar/` is unignored directly, by `!foobar`
336
+ ig2.test('foobar/') // {ignored: false, unignored: true}
337
+
338
+ // No rule matches `foobar/x.js` itself. It is not ignored only because its
339
+ // parent directory `foobar/` is not, so it is unignored indirectly
340
+ ig2.test('foobar/x.js') // {ignored: false, unignored: false}
341
+ ```
342
+
343
+ To tell the two situations apart, test the parent directories of the `pathname`, each with a trailing slash: the `pathname` is unignored indirectly if any of them is `unignored`.
344
+
345
+ ```js
346
+ ['foobar/'].some(dir => ig2.test(dir).unignored) // true
347
+ ```
288
348
 
289
349
  ## .checkIgnore(target: string): TestResult
290
350
 
291
351
  > new in 7.0.0
292
352
 
293
- 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.
294
358
 
295
359
  - **target** `string` the target to test.
296
360
 
297
- Returns `TestResult`
361
+ Returns `TestResult`, the same shape `.test()` returns, so `{ignored: false, unignored: false}` carries the [same two meanings](#when-both-are-false) here.
298
362
 
299
363
  ```js
300
364
  ig.add({
@@ -305,10 +369,10 @@ ig.add({
305
369
  const {
306
370
  ignored,
307
371
  rule
308
- } = checkIgnore('foo/')
372
+ } = ig.checkIgnore('foo/')
309
373
 
310
374
  if (ignored) {
311
- console.log(`.gitignore:${result}:${rule.mark}:${rule.pattern} foo/`)
375
+ console.log(`.gitignore:${rule.mark}:${rule.pattern} foo/`)
312
376
  }
313
377
 
314
378
  // .gitignore:60:foo/* foo/
@@ -416,13 +480,30 @@ not in `git`. For ASCII paths and patterns there is no difference.
416
480
  code runs against a real repository, pass the value of the repository's
417
481
  `core.ignorecase` explicitly.
418
482
 
419
- ### `***` 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
+ ```
420
504
 
421
- gitignore(5) says a run of more than two asterisks is "considered regular
422
- asterisks". The `git` binary, however, strips the literal prefix of a
423
- pattern before matching, which makes patterns like `***/foo`, `a/***` and
424
- `a**/b` behave as `**` globstars there. `ignore` follows the documented
425
- 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`.
426
507
 
427
508
  ### `checkIgnore()` and a directory passed with a trailing slash
428
509
 
@@ -1,27 +1,62 @@
1
- type Pathname = string
2
-
3
- interface IgnoreRule {
4
- pattern: string
5
- mark?: string
6
- negative: boolean
7
- }
8
-
9
- interface TestResult {
10
- ignored: boolean
11
- unignored: boolean
12
- rule?: IgnoreRule
13
- }
14
-
15
- interface PatternParams {
16
- pattern: string
17
- mark?: string
18
- }
19
-
20
1
  /**
21
2
  * Creates new ignore manager.
22
3
  */
23
4
  declare function ignore(options?: ignore.Options): ignore.Ignore
24
5
  declare namespace ignore {
6
+ /**
7
+ * A path relative to the directory the rules belong to, as
8
+ * `path.relative()` returns it.
9
+ */
10
+ type Pathname = string
11
+
12
+ interface PatternParams {
13
+ pattern: string
14
+ /**
15
+ * Carried through to `IgnoreRule.mark`, for packages that build upon
16
+ * `ignore`.
17
+ */
18
+ mark?: string
19
+ }
20
+
21
+ interface IgnoreRule {
22
+ /**
23
+ * The original pattern.
24
+ */
25
+ pattern: string
26
+ /**
27
+ * Carried through from `PatternParams.mark`.
28
+ */
29
+ mark?: string
30
+ /**
31
+ * Whether the pattern is a negative one, such as `!foo`.
32
+ */
33
+ negative: boolean
34
+ }
35
+
36
+ interface TestResult {
37
+ /**
38
+ * Whether the pathname is ignored, by a rule or because a parent
39
+ * directory of it is.
40
+ */
41
+ ignored: boolean
42
+ /**
43
+ * True if a negative pattern matched the pathname itself, and no parent
44
+ * directory of it is ignored.
45
+ *
46
+ * Only `ignored` passes down from a parent directory, so a pathname that
47
+ * is not ignored only because a parent directory was unignored keeps
48
+ * this false, as does one no rule applies to at all. To tell those two
49
+ * apart, test the parent directories, each with a trailing slash.
50
+ */
51
+ unignored: boolean
52
+ /**
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.
56
+ */
57
+ rule?: IgnoreRule
58
+ }
59
+
25
60
  interface Ignore {
26
61
  /**
27
62
  * Adds one or several rules to the current manager.
@@ -54,7 +89,8 @@ declare namespace ignore {
54
89
  ignores(pathname: Pathname): boolean
55
90
 
56
91
  /**
57
- * Returns whether pathname should be ignored or unignored
92
+ * Tests the pathname against the rules. `ignored` and `unignored` can
93
+ * both be false: see `TestResult.unignored` for the two cases.
58
94
  * @param {string} pathname a path to check
59
95
  * @returns TestResult
60
96
  */