@orkestrel/probe 0.0.1

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.
@@ -0,0 +1,2321 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { attempt, compileGuard, compileSchema, isArray, isRecord, schemaToParameters } from "@orkestrel/contract";
6
+ import { CLAIM_SHAPE, ProbeError, computeReceipt, createDestroyedError, formatCheck, formatSpecification, formatVerdict, isClaim, isDraft, isVerdict, matchesSpecification } from "../core/index.js";
7
+ import { Emitter } from "@orkestrel/emitter";
8
+ import { createQueue } from "@orkestrel/queue";
9
+ import { createTimeout } from "@orkestrel/timeout";
10
+ import { spawn } from "node:child_process";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ import { PassThrough } from "node:stream";
13
+ import { setImmediate } from "node:timers/promises";
14
+ import { createMCPLegacy, createMCPServer } from "@orkestrel/mcp";
15
+ import { createStdioServer } from "@orkestrel/mcp/server";
16
+ import { createTool, createToolManager } from "@orkestrel/tool";
17
+ //#region src/server/helpers.ts
18
+ /**
19
+ * Rewrites one path into the forward-slash spelling this package compares and reports paths in.
20
+ *
21
+ * @remarks
22
+ * Windows reports a path with backslashes and accepts either separator, so two spellings of one
23
+ * file compare unequal until they are rewritten. Every path this package keys a map on, matches a
24
+ * prefix against, or hands a caller passes through here first.
25
+ *
26
+ * @param path - The path to rewrite
27
+ * @returns The same path with every backslash replaced by a forward slash
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * normalizePath('src\\core\\greeting.ts') // 'src/core/greeting.ts'
32
+ * normalizePath('src/core/greeting.ts') // 'src/core/greeting.ts'
33
+ * ```
34
+ */
35
+ function normalizePath(path) {
36
+ return path.replaceAll("\\", "/");
37
+ }
38
+ /**
39
+ * Resolves a path inside a target workspace and rejects traversal outside it.
40
+ *
41
+ * @remarks
42
+ * A native `ENAMETOOLONG` or NUL-byte `ERR_INVALID_ARG_VALUE` fault is a claimant refusal. This
43
+ * classifies the fault, not the author: a workspace nested deeply enough that this package's short
44
+ * generated names overflow the host limit also reads as a claimant refusal.
45
+ *
46
+ * @param workspace - The target workspace root
47
+ * @param target - A workspace-relative path
48
+ * @param mutate - If `true`, refuses a symbolic link in an existing descendant; if `false`,
49
+ * resolves lexically. Default: `false`
50
+ * @returns The absolute contained path
51
+ * @throws When the path resolves outside the workspace, mutation would cross a symbolic link, or
52
+ * the target tree cannot be inspected
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * resolveWorkspaceFile('/srv/checkout', 'src/core/greeting.ts')
57
+ * // '/srv/checkout/src/core/greeting.ts'
58
+ * resolveWorkspaceFile('/srv/checkout', '../secrets.env')
59
+ * // throws: Path escapes the workspace: ../secrets.env
60
+ * ```
61
+ */
62
+ function resolveWorkspaceFile(workspace, target, mutate = false) {
63
+ const root = resolve(workspace);
64
+ const file = resolve(root, target);
65
+ const path = relative(root, file);
66
+ if (path === "" || path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new ProbeError(`Path escapes the workspace: ${target}`, {
67
+ origin: "claimant",
68
+ code: "refused",
69
+ context: { path: target }
70
+ });
71
+ if (!mutate) return file;
72
+ try {
73
+ const canonical = realpathSync(root);
74
+ let descendant = root;
75
+ for (const segment of path.split(sep)) {
76
+ descendant = resolve(descendant, segment);
77
+ const outcome = attempt(() => lstatSync(descendant));
78
+ if (!outcome.success) {
79
+ const error = outcome.error;
80
+ if (typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) break;
81
+ throw error;
82
+ }
83
+ if (outcome.value.isSymbolicLink()) throw new ProbeError(`Path crosses a symbolic link: ${target}`, {
84
+ origin: "workspace",
85
+ code: "refused",
86
+ context: { path: target }
87
+ });
88
+ const resolved = realpathSync(descendant);
89
+ const remainder = relative(canonical, resolved);
90
+ if (remainder === ".." || remainder.startsWith(`..${sep}`) || isAbsolute(remainder)) throw new ProbeError(`Path escapes the workspace: ${target}`, {
91
+ origin: "claimant",
92
+ code: "refused",
93
+ context: { path: target }
94
+ });
95
+ }
96
+ } catch (error) {
97
+ if (error instanceof ProbeError) throw error;
98
+ const claimant = typeof error === "object" && error !== null && "code" in error && (error.code === "ENAMETOOLONG" || error.code === "ERR_INVALID_ARG_VALUE");
99
+ throw new ProbeError(`The workspace path cannot be inspected: ${target}`, {
100
+ origin: claimant ? "claimant" : "workspace",
101
+ code: claimant ? "refused" : "malformed",
102
+ context: { path: target },
103
+ cause: error
104
+ });
105
+ }
106
+ return file;
107
+ }
108
+ /**
109
+ * Projects an absolute tool path into the workspace-relative form issues expose.
110
+ *
111
+ * @param workspace - The target workspace root
112
+ * @param file - The path a tool reported
113
+ * @returns A forward-slash workspace-relative path
114
+ *
115
+ * @example
116
+ * ```ts
117
+ * relativeWorkspaceFile('/srv/checkout', '/srv/checkout/src/core/greeting.ts')
118
+ * // 'src/core/greeting.ts'
119
+ * ```
120
+ */
121
+ function relativeWorkspaceFile(workspace, file) {
122
+ return normalizePath(relative(resolve(workspace), resolve(file)));
123
+ }
124
+ /**
125
+ * Resolves one installed module from the target workspace.
126
+ *
127
+ * @param workspace - The target workspace root
128
+ * @param specifier - The module specifier to resolve
129
+ * @returns The installed module entry path
130
+ * @throws When the workspace does not install the module
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * resolveWorkspaceModule(process.cwd(), 'typescript/package.json') ===
135
+ * readWorkspaceManifest(process.cwd(), 'typescript').path // true
136
+ * ```
137
+ */
138
+ function resolveWorkspaceModule(workspace, specifier) {
139
+ const outcome = attempt(() => createRequire(resolve(workspace, "package.json")).resolve(specifier));
140
+ if (outcome.success) return outcome.value;
141
+ const error = outcome.error;
142
+ const missing = typeof error === "object" && error !== null && "code" in error && error.code === "MODULE_NOT_FOUND";
143
+ throw new ProbeError(`The workspace cannot resolve ${specifier}`, {
144
+ origin: "workspace",
145
+ code: missing ? "missing" : "malformed",
146
+ context: { name: specifier },
147
+ cause: error
148
+ });
149
+ }
150
+ function loadWorkspaceModule(workspace, specifier) {
151
+ const outcome = attempt(() => {
152
+ const require = createRequire(resolve(workspace, "package.json"));
153
+ return specifier === "typescript" ? require("typescript") : require("vitest/node");
154
+ });
155
+ if (outcome.success) return outcome.value;
156
+ const error = outcome.error;
157
+ const missing = typeof error === "object" && error !== null && "code" in error && error.code === "MODULE_NOT_FOUND";
158
+ throw new ProbeError(`The workspace cannot load ${specifier}`, {
159
+ origin: "workspace",
160
+ code: missing ? "missing" : "malformed",
161
+ context: { name: specifier },
162
+ cause: error
163
+ });
164
+ }
165
+ /**
166
+ * Reads one installed package manifest from the target workspace.
167
+ *
168
+ * @param workspace - The target workspace root
169
+ * @param name - The installed package name
170
+ * @returns The manifest's absolute path and parsed record
171
+ * @throws When the package cannot be resolved, read, parsed, or does not publish a record manifest
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const manifest = readWorkspaceManifest(process.cwd(), 'typescript')
176
+ * console.log(manifest.contents.version)
177
+ * ```
178
+ */
179
+ function readWorkspaceManifest(workspace, name) {
180
+ let path;
181
+ try {
182
+ path = resolveWorkspaceModule(workspace, `${name}/package.json`);
183
+ } catch (error) {
184
+ if (!(error instanceof ProbeError)) throw error;
185
+ throw new ProbeError(`${name} does not publish a readable manifest`, {
186
+ origin: "workspace",
187
+ code: error.code,
188
+ context: { name },
189
+ cause: error.cause
190
+ });
191
+ }
192
+ const reading = attempt(() => readFileSync(path, "utf8"));
193
+ if (!reading.success) {
194
+ const error = reading.error;
195
+ const missing = typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
196
+ throw new ProbeError(`${name} does not publish a readable manifest`, {
197
+ origin: "workspace",
198
+ code: missing ? "missing" : "malformed",
199
+ context: {
200
+ name,
201
+ path
202
+ },
203
+ cause: error
204
+ });
205
+ }
206
+ const parsing = attempt(() => JSON.parse(reading.value));
207
+ if (!parsing.success) throw new ProbeError(`${name} does not publish a readable manifest`, {
208
+ origin: "workspace",
209
+ code: "malformed",
210
+ context: {
211
+ name,
212
+ path
213
+ },
214
+ cause: parsing.error
215
+ });
216
+ const contents = parsing.value;
217
+ if (!isRecord(contents)) throw new ProbeError(`${name} does not publish a readable manifest`, {
218
+ origin: "workspace",
219
+ code: "malformed",
220
+ context: {
221
+ name,
222
+ path
223
+ }
224
+ });
225
+ return {
226
+ path,
227
+ contents
228
+ };
229
+ }
230
+ /**
231
+ * Resolves a package's portable JavaScript binary from the target workspace.
232
+ *
233
+ * @param workspace - The target workspace root
234
+ * @param name - The installed package name and binary key
235
+ * @returns The absolute JavaScript entry named by the package's `bin` field
236
+ * @throws When the package does not publish a binary under the requested name
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'oxlint'))
241
+ * // 'node_modules/oxlint/bin/oxlint'
242
+ * resolveWorkspaceBinary(process.cwd(), 'typescript')
243
+ * // throws: typescript does not publish the typescript binary
244
+ * ```
245
+ */
246
+ function resolveWorkspaceBinary(workspace, name) {
247
+ const manifest = readWorkspaceManifest(workspace, name);
248
+ const bin = manifest.contents.bin;
249
+ if (bin === void 0) throw new ProbeError(`${name} does not publish a bin field`, {
250
+ origin: "workspace",
251
+ code: "missing",
252
+ context: { name }
253
+ });
254
+ if (typeof bin === "string") return resolve(manifest.path, "..", bin);
255
+ if (!isRecord(bin) || !(name in bin)) throw new ProbeError(`${name} does not publish the ${name} binary`, {
256
+ origin: "workspace",
257
+ code: "missing",
258
+ context: { name }
259
+ });
260
+ const entry = bin[name];
261
+ if (typeof entry !== "string") throw new ProbeError(`${name} publishes an invalid ${name} binary`, {
262
+ origin: "workspace",
263
+ code: "malformed",
264
+ context: {
265
+ name,
266
+ value: entry
267
+ }
268
+ });
269
+ return resolve(manifest.path, "..", entry);
270
+ }
271
+ /**
272
+ * Selects the scoped TypeScript project for one candidate draft path.
273
+ *
274
+ * @param path - The workspace-relative candidate draft path
275
+ * @returns The workspace-relative scoped project path
276
+ * @throws When the path does not name a configured source or application environment
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * inferTypeProject('src/core/greeting.ts') // 'configs/src/tsconfig.core.json'
281
+ * inferTypeProject('tests/src/core/greeting.test.ts')
282
+ * // throws: Cannot infer a scoped TypeScript project for tests/src/core/greeting.test.ts
283
+ * ```
284
+ */
285
+ function inferTypeProject(path) {
286
+ const [axis, environment] = normalizePath(path).split("/");
287
+ if (axis !== "src" && axis !== "app" || environment === void 0 || environment === "") throw new ProbeError(`Cannot infer a scoped TypeScript project for ${path}`, {
288
+ origin: "claimant",
289
+ code: "refused",
290
+ context: { path }
291
+ });
292
+ return `configs/${axis}/tsconfig.${environment}.json`;
293
+ }
294
+ /**
295
+ * Selects the Vitest project whose environment matches one test path.
296
+ *
297
+ * @remarks
298
+ * There is no root-project fallback. A path no configured project collects returns `undefined`,
299
+ * and the runtime stage refuses that claimant test with `code: 'missing'` rather than running it
300
+ * somewhere the workspace never configured.
301
+ *
302
+ * @param path - The workspace-relative test path
303
+ * @returns The matching project's name, or `undefined` when no configured project collects the path
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * inferTestProject('tmp/probe/greeting.test.ts') // 'probe'
308
+ * inferTestProject('tests/src/server/helpers.test.ts') // 'src:server'
309
+ * inferTestProject('tests/config.test.ts') // undefined
310
+ * ```
311
+ */
312
+ function inferTestProject(path) {
313
+ const [root, axis, environment] = normalizePath(path).split("/");
314
+ if (root === "tmp" && axis === "probe") return "probe";
315
+ if (root !== "tests" || axis === void 0 || environment === void 0) return void 0;
316
+ if (axis !== "src" && axis !== "app") return void 0;
317
+ return `${axis}:${environment}`;
318
+ }
319
+ /**
320
+ * Selects the Language Server Protocol language identifier for a source path.
321
+ *
322
+ * @param path - The source path whose extension selects the language
323
+ * @returns The matching JavaScript or TypeScript language identifier
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * inferDocumentLanguage('src/core/greeting.ts') // 'typescript'
328
+ * inferDocumentLanguage('src/browser/Panel.tsx') // 'typescriptreact'
329
+ * ```
330
+ */
331
+ function inferDocumentLanguage(path) {
332
+ const extension = extname(path).toLowerCase();
333
+ if (extension === ".tsx") return "typescriptreact";
334
+ if (extension === ".js" || extension === ".mjs" || extension === ".cjs") return "javascript";
335
+ if (extension === ".jsx") return "javascriptreact";
336
+ return "typescript";
337
+ }
338
+ /**
339
+ * Creates a fresh sibling identity while preserving a test's resolution directory.
340
+ *
341
+ * @param workspace - The target workspace root
342
+ * @param path - The workspace-relative test path
343
+ * @param revision - The fresh revision identity
344
+ * @returns An absolute sibling file path
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * relativeWorkspaceFile(
349
+ * '/srv/checkout',
350
+ * createRevisionFile('/srv/checkout', 'tmp/probe/greeting.test.ts', '4821-9f0c'),
351
+ * )
352
+ * // 'tmp/probe/greeting.test.probe-4821-9f0c.ts'
353
+ * ```
354
+ */
355
+ function createRevisionFile(workspace, path, revision) {
356
+ const file = resolveWorkspaceFile(workspace, path);
357
+ const extension = extname(file);
358
+ return `${extension === "" ? file : file.slice(0, -extension.length)}.probe-${revision}${extension}`;
359
+ }
360
+ /**
361
+ * Reports whether a path is a workspace module Vitest can cache.
362
+ *
363
+ * @param path - The candidate file path
364
+ * @returns True for script, TypeScript, Vue, and JSON modules; false otherwise
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * matchesWorkspaceModule('src/core/greeting.ts') // true
369
+ * matchesWorkspaceModule('src/styles/tokens.css') // false
370
+ * ```
371
+ */
372
+ function matchesWorkspaceModule(path) {
373
+ return /\.(?:[cm]?[jt]sx?|vue|json)$/.test(path);
374
+ }
375
+ /**
376
+ * Reads a JSON-RPC frame's declared byte length.
377
+ *
378
+ * @param header - The frame header text without its terminating empty line
379
+ * @returns The non-negative content length, or `undefined` for an invalid header
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * parseContentLength('Content-Length: 128') // 128
384
+ * parseContentLength('Content-Type: application/json') // undefined
385
+ * ```
386
+ */
387
+ function parseContentLength(header) {
388
+ const text = /(?:^|\r\n)Content-Length:\s*(\d+)(?:\r\n|$)/i.exec(header)?.[1];
389
+ if (text === void 0) return void 0;
390
+ const length = Number.parseInt(text, 10);
391
+ return Number.isSafeInteger(length) && length >= 0 ? length : void 0;
392
+ }
393
+ /**
394
+ * Normalizes a caught or foreign error into readable text.
395
+ *
396
+ * @param value - The value to describe
397
+ * @returns Its message when present, or its string representation
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * describeUnknown(new Error('The lint stage has been destroyed'))
402
+ * // 'The lint stage has been destroyed'
403
+ * describeUnknown(17) // '17'
404
+ * ```
405
+ */
406
+ function describeUnknown(value) {
407
+ if (value instanceof Error) return value.message;
408
+ if (typeof value === "object" && value !== null && "message" in value && typeof value.message === "string") return value.message;
409
+ return String(value);
410
+ }
411
+ /**
412
+ * Names every draft member of a claim-shaped value whose `path` this package's guard refuses.
413
+ *
414
+ * @remarks
415
+ * The published claim schema constrains `Draft.path` to a non-empty string and nothing else, while
416
+ * `isDraft` also refuses an absolute path and one that escapes the workspace. That one member is
417
+ * the whole of the difference between the advertised contract and the enforced one, so a caller
418
+ * refused after satisfying the schema is refused here and nowhere else. The whole input must first
419
+ * satisfy that schema. Each draft is then tested with `isDraft` itself rather than with a second
420
+ * copy of its rule. Reports nothing for a value carrying no draft member, including one refused for
421
+ * a member this contract does not declare.
422
+ *
423
+ * This function lives in the server helpers rather than in core: core's leaf pair, `helpers.ts` and
424
+ * `validators.ts`, may not consume the shapes module this function reads `CLAIM_SHAPE` from, and
425
+ * `ProbeServer` is the sole consumer of the result it returns.
426
+ *
427
+ * @param value - The rejected tool input
428
+ * @returns The dotted member names, in `case` then `control` order, or an empty list
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * const test = { path: 'tmp/probe/greeting.test.ts', text: '' }
433
+ * findRefusedPaths({
434
+ * project: 'tsconfig.json',
435
+ * case: { files: [{ path: '../../etc/hosts', text: '' }], test },
436
+ * control: { files: [], test, stage: 'type', reason: 'must not compile' },
437
+ * })
438
+ * // ['case.files.0.path']
439
+ * ```
440
+ */
441
+ function findRefusedPaths(value) {
442
+ if (!compileGuard(CLAIM_SHAPE)(value)) return [];
443
+ const members = [];
444
+ for (const phase of ["case", "control"]) {
445
+ const subject = value[phase];
446
+ const drafts = /* @__PURE__ */ new Map([[`${phase}.test`, subject.test]]);
447
+ for (const [index, file] of subject.files.entries()) drafts.set(`${phase}.files.${index}`, file);
448
+ for (const [member, draft] of drafts) {
449
+ if (isDraft(draft)) continue;
450
+ members.push(`${member}.path`);
451
+ }
452
+ }
453
+ return members;
454
+ }
455
+ /**
456
+ * Rewrites every workspace-contained absolute path in a value to its workspace-relative form and
457
+ * sorts every record's keys.
458
+ *
459
+ * @remarks
460
+ * A parsed TypeScript project carries the absolute root it was parsed at, so the same commit
461
+ * checked out at two paths produces two values that describe one configuration. Rewriting those
462
+ * members makes the value portable and stops a host path leaking into anything derived from it. A
463
+ * path that escapes the workspace is left as it stands, because rewriting it would name a file
464
+ * outside the tree by a relative spelling. Key order is the parser's, not the configuration's, so
465
+ * sorting removes a difference that means nothing.
466
+ *
467
+ * @param workspace - The target workspace root
468
+ * @param value - The value to rewrite
469
+ * @returns The value with contained absolute paths made relative and every record key sorted
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * normalizeValue('/srv/checkout', { rootDir: '/srv/checkout/src/core', strict: true })
474
+ * // { rootDir: 'src/core', strict: true }
475
+ * ```
476
+ */
477
+ function normalizeValue(workspace, value) {
478
+ const root = resolve(workspace);
479
+ if (typeof value === "string") {
480
+ if (!isAbsolute(value)) return value;
481
+ const path = relative(root, resolve(value));
482
+ if (path === "") return ".";
483
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) return value;
484
+ return normalizePath(path);
485
+ }
486
+ if (isArray(value)) return value.map((entry) => normalizeValue(workspace, entry));
487
+ if (isRecord(value)) {
488
+ const record = {};
489
+ for (const key of Object.keys(value).sort()) record[key] = normalizeValue(workspace, value[key]);
490
+ return record;
491
+ }
492
+ return value;
493
+ }
494
+ /**
495
+ * Computes the canonical digest of one value as it stands in a target workspace.
496
+ *
497
+ * @remarks
498
+ * Digests the normalized value rather than the value itself, so one commit read at two absolute
499
+ * roots produces one digest and a digest carries no host path. The result is the leading 32 hex
500
+ * characters of a SHA-256 over the normalized JSON.
501
+ *
502
+ * @param workspace - The target workspace root the value's paths are read against
503
+ * @param value - The value to digest
504
+ * @returns 32 lowercase hex characters
505
+ *
506
+ * @example
507
+ * ```ts
508
+ * computeDigest('/srv/checkout', { strict: true }).length // 32
509
+ * ```
510
+ */
511
+ function computeDigest(workspace, value) {
512
+ return createHash("sha256").update(JSON.stringify(normalizeValue(workspace, value))).digest("hex").slice(0, 32);
513
+ }
514
+ /**
515
+ * Records the listeners one emitter carries for a set of events.
516
+ *
517
+ * @param emitter - The emitter to read
518
+ * @param events - The event names to record
519
+ * @returns The listeners each named event carries at the moment of the call
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * const capture = captureListeners(process, ['SIGTERM'])
524
+ * capture.get('SIGTERM')?.length === process.listenerCount('SIGTERM') // true
525
+ * ```
526
+ */
527
+ function captureListeners(emitter, events) {
528
+ const capture = /* @__PURE__ */ new Map();
529
+ for (const event of events) capture.set(event, emitter.listeners(event));
530
+ return capture;
531
+ }
532
+ /**
533
+ * Removes every listener one emitter gained for the captured events since its capture.
534
+ *
535
+ * @remarks
536
+ * Compares identity rather than name, so a listener whose name a dependency changes between
537
+ * releases is still recognized and a listener that merely shares a name is still left alone. A
538
+ * listener the capture already held survives, and a listener the capture held that has since been
539
+ * removed is not restored.
540
+ *
541
+ * A gain is not evidence of who caused it, so this reads every gain as the callee's. Call it only
542
+ * across a window nothing else can attach in — a synchronous call, or a call whose handlers land
543
+ * before its first await and that is released as it returns — because a listener anyone else
544
+ * attaches inside that window is removed with the rest. Where a caller cannot promise that window,
545
+ * hold each handler as a field and remove it by reference instead of capturing at all.
546
+ *
547
+ * @param emitter - The emitter to release
548
+ * @param capture - The listeners the emitter carried at capture time
549
+ * @returns Nothing
550
+ *
551
+ * @example
552
+ * ```ts
553
+ * const capture = captureListeners(process, ['SIGTERM'])
554
+ * process.on('SIGTERM', () => {})
555
+ * releaseListeners(process, capture)
556
+ * process.listenerCount('SIGTERM') === capture.get('SIGTERM')?.length // true
557
+ * ```
558
+ */
559
+ function releaseListeners(emitter, capture) {
560
+ for (const [event, captured] of capture) for (const listener of emitter.listeners(event)) {
561
+ if (captured.includes(listener)) continue;
562
+ emitter.removeListener(event, listener);
563
+ }
564
+ }
565
+ //#endregion
566
+ //#region src/server/Overlay.ts
567
+ /**
568
+ * Holds the candidate drafts one inspection substitutes for the files a tool would read from disk.
569
+ *
570
+ * @remarks
571
+ * One overlay belongs to one inspection: the inspection creates it, records every candidate it
572
+ * carries, and clears it when the inspection ends. Each stage adapts the same set to the host its
573
+ * own tool expects rather than sharing one filesystem across tools. A fresh identity per instance
574
+ * is what makes a resident tool re-read a path this overlay holds, so two inspections that supply
575
+ * different text for one path never share a cached answer.
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * const overlay = new Overlay()
580
+ * overlay.set('/srv/checkout/src/core/greeting.ts', "export const GREETING = 'hi'\n")
581
+ * console.log(overlay.text('/srv/checkout/src/core/greeting.ts'))
582
+ * overlay.clear()
583
+ * ```
584
+ */
585
+ var Overlay = class {
586
+ #revision = randomUUID();
587
+ #candidates = /* @__PURE__ */ new Map();
588
+ get revision() {
589
+ return this.#revision;
590
+ }
591
+ get paths() {
592
+ return [...this.#candidates.keys()];
593
+ }
594
+ /**
595
+ * Records one candidate's text against the absolute path it stands in for.
596
+ *
597
+ * @param path - The absolute path the candidate replaces
598
+ * @param text - The candidate's full contents
599
+ * @returns Nothing
600
+ */
601
+ set(path, text) {
602
+ this.#candidates.set(normalizePath(path), text);
603
+ }
604
+ /**
605
+ * Reads the candidate text recorded for one absolute path.
606
+ *
607
+ * @param path - The absolute path to read
608
+ * @returns The recorded text, or `undefined` when this overlay holds no candidate there
609
+ */
610
+ text(path) {
611
+ return this.#candidates.get(normalizePath(path));
612
+ }
613
+ /**
614
+ * Checks whether a candidate sits beneath one directory.
615
+ *
616
+ * @remarks
617
+ * The answer is derived from the paths the overlay holds rather than stored, so it stops being
618
+ * true exactly when the inspection that declared those candidates clears them. Both sides pass
619
+ * through `normalizePath` first, because a tool that normalizes its own paths asks about a
620
+ * directory in a spelling the recorded path may not share.
621
+ *
622
+ * @param directory - The absolute directory path to check
623
+ * @returns True if a candidate path sits beneath the directory; false otherwise
624
+ */
625
+ covers(directory) {
626
+ const base = `${normalizePath(directory).replace(/\/+$/, "")}/`;
627
+ for (const path of this.#candidates.keys()) if (path.startsWith(base)) return true;
628
+ return false;
629
+ }
630
+ /**
631
+ * Releases every candidate.
632
+ *
633
+ * @returns Nothing
634
+ */
635
+ clear() {
636
+ this.#candidates.clear();
637
+ }
638
+ };
639
+ //#endregion
640
+ //#region package.json
641
+ var version = "0.0.1";
642
+ var peerDependencies = {
643
+ "oxlint": "^1.77.0",
644
+ "typescript": "^6.0.0",
645
+ "vitest": "^4.1.0"
646
+ };
647
+ //#endregion
648
+ //#region src/server/stages/LintStage.ts
649
+ /**
650
+ * Inspects virtual documents through one resident Oxlint language server.
651
+ *
652
+ * @remarks
653
+ * Construction starts the target workspace's Oxlint binary with its Language Server Protocol
654
+ * mode. Each inspection opens the supplied text by URI, waits for published diagnostics, and
655
+ * closes the document without writing it to disk.
656
+ *
657
+ * Teardown is bounded at both ends. Warming waits for the server's `initialize` response and the
658
+ * ending waits for its `shutdown` reply, and the protocol leaves both answers to the server, so
659
+ * `destroy` bounds each exchange and takes the ending back with a signal when the bound passes.
660
+ *
661
+ * The URI is the path the candidate declared, so the candidate receives exactly the rule set the
662
+ * workspace's own lint gate applies to that path, including an override anchored to one directory,
663
+ * one file name, or one exact file. A path the workspace's version-control ignore files exclude is
664
+ * a path that gate never lints, and this stage reports the same nothing for it. Reuse of one
665
+ * declared path across inspections is what the caller's own serialization protects: the stage
666
+ * refuses a second inspection of a path it already has open rather than answering either one with
667
+ * the other's diagnostics.
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * const stage = new LintStage('/srv/checkout')
672
+ * const check = await stage.inspect(subject)
673
+ * await stage.destroy()
674
+ * ```
675
+ */
676
+ var LintStage = class {
677
+ #workspace;
678
+ #deadline = 2e3;
679
+ #warmth;
680
+ #responses = /* @__PURE__ */ new Map();
681
+ #failures = /* @__PURE__ */ new Map();
682
+ #documents = /* @__PURE__ */ new Map();
683
+ #publishes = /* @__PURE__ */ new Map();
684
+ #refusals = /* @__PURE__ */ new Map();
685
+ #child;
686
+ #buffer = Buffer.alloc(0);
687
+ #sequence = 0;
688
+ #closing;
689
+ #progress = 0;
690
+ #destroyed = false;
691
+ /**
692
+ * Starts warming the target workspace's Oxlint language server.
693
+ *
694
+ * @param workspace - The target workspace root. Default: the current working directory
695
+ */
696
+ constructor(workspace = process.cwd()) {
697
+ this.#workspace = workspace;
698
+ this.#warmth = this.#warm();
699
+ this.#warmth.catch(() => {});
700
+ }
701
+ get stage() {
702
+ return "lint";
703
+ }
704
+ get progress() {
705
+ return this.#progress;
706
+ }
707
+ async inspect(subject) {
708
+ if (this.#destroyed) throw createDestroyedError("lint stage");
709
+ const started = performance.now();
710
+ await this.#warmth;
711
+ if (this.#destroyed) throw createDestroyedError("lint stage");
712
+ const issues = [];
713
+ for (const draft of [...subject.files, subject.test]) issues.push(...await this.#document(draft));
714
+ return {
715
+ stage: this.stage,
716
+ elapsed: Math.round(performance.now() - started),
717
+ issues
718
+ };
719
+ }
720
+ destroy() {
721
+ if (this.#closing !== void 0) return this.#closing;
722
+ this.#destroyed = true;
723
+ this.#closing = this.#destroy();
724
+ return this.#closing;
725
+ }
726
+ async #destroy() {
727
+ const child = await this.#warmed();
728
+ this.#fail(createDestroyedError("lint stage"));
729
+ if (child === void 0 || this.#ending !== void 0) return;
730
+ const released = new Promise((resolve) => {
731
+ child.once("exit", () => resolve());
732
+ child.once("close", () => resolve());
733
+ });
734
+ await this.#release(child, released);
735
+ }
736
+ async #warmed() {
737
+ const timeout = createTimeout({ ms: this.#deadline });
738
+ timeout.start();
739
+ try {
740
+ return await Promise.race([this.#warmth, this.#expiry(timeout)]);
741
+ } catch {
742
+ return this.#child;
743
+ } finally {
744
+ timeout.clear();
745
+ }
746
+ }
747
+ async #release(child, released) {
748
+ const timeout = createTimeout({ ms: this.#deadline });
749
+ timeout.start();
750
+ try {
751
+ await Promise.race([this.#retire(child).then(() => released), this.#expiry(timeout)]);
752
+ } catch {
753
+ child.kill("SIGKILL");
754
+ await released;
755
+ } finally {
756
+ timeout.clear();
757
+ }
758
+ }
759
+ async #retire(child) {
760
+ try {
761
+ await this.#request("shutdown", void 0);
762
+ this.#notify("exit", void 0);
763
+ } catch {
764
+ child.kill("SIGKILL");
765
+ }
766
+ }
767
+ #expiry(timeout) {
768
+ return new Promise((_resolve, reject) => {
769
+ timeout.signal.addEventListener("abort", () => reject(new ProbeError(`The Oxlint language server did not exit within ${timeout.ms} ms`, {
770
+ origin: "instrument",
771
+ code: "deadline",
772
+ context: {
773
+ stage: this.stage,
774
+ deadline: timeout.ms
775
+ }
776
+ })), { once: true });
777
+ });
778
+ }
779
+ async #warm() {
780
+ const binary = resolveWorkspaceBinary(this.#workspace, "oxlint");
781
+ const child = spawn(process.execPath, [binary, "--lsp"], {
782
+ cwd: this.#workspace,
783
+ stdio: "pipe"
784
+ });
785
+ this.#child = child;
786
+ child.stderr.resume();
787
+ child.stdout.on("data", (chunk) => this.#read(chunk));
788
+ child.stdin.on("error", (error) => this.#fail(error));
789
+ child.on("error", (error) => this.#fail(error));
790
+ child.on("exit", (code, signal) => this.#exit(code, signal));
791
+ await this.#request("initialize", {
792
+ processId: process.pid,
793
+ rootUri: pathToFileURL(this.#workspace).href,
794
+ capabilities: {},
795
+ workspaceFolders: [{
796
+ uri: pathToFileURL(this.#workspace).href,
797
+ name: "workspace"
798
+ }]
799
+ });
800
+ this.#notify("initialized", {});
801
+ return child;
802
+ }
803
+ #document(draft) {
804
+ const uri = pathToFileURL(resolveWorkspaceFile(this.#workspace, draft.path)).href;
805
+ if (this.#publishes.has(uri)) return Promise.reject(new ProbeError(`The lint stage is already inspecting ${draft.path}`, {
806
+ origin: "claimant",
807
+ code: "refused",
808
+ context: {
809
+ stage: this.stage,
810
+ path: draft.path
811
+ }
812
+ }));
813
+ const inspected = new Promise((resolve, reject) => {
814
+ this.#documents.set(uri, normalizePath(draft.path));
815
+ this.#publishes.set(uri, resolve);
816
+ this.#refusals.set(uri, reject);
817
+ }).finally(() => this.#close(uri));
818
+ try {
819
+ this.#notify("textDocument/didOpen", { textDocument: {
820
+ uri,
821
+ languageId: inferDocumentLanguage(draft.path),
822
+ version: this.#sequence,
823
+ text: draft.text
824
+ } });
825
+ this.#progress += 1;
826
+ } catch (error) {
827
+ this.#refusals.get(uri)?.(this.#fault(describeUnknown(error), error, draft.path));
828
+ }
829
+ return inspected;
830
+ }
831
+ get #ending() {
832
+ const child = this.#child;
833
+ if (child === void 0) return void 0;
834
+ if (child.exitCode === null && child.signalCode === null) return void 0;
835
+ return this.#describe(child.exitCode, child.signalCode);
836
+ }
837
+ #describe(code, signal) {
838
+ return signal === null ? `code ${code}` : `signal ${signal}`;
839
+ }
840
+ #fault(message, cause, path) {
841
+ return new ProbeError(message, {
842
+ origin: "instrument",
843
+ code: "malformed",
844
+ context: {
845
+ stage: this.stage,
846
+ ...path === void 0 ? {} : { path }
847
+ },
848
+ ...cause === void 0 ? {} : { cause }
849
+ });
850
+ }
851
+ get #reachable() {
852
+ return this.#ending === void 0 && this.#child?.stdin.writable === true;
853
+ }
854
+ #close(uri) {
855
+ this.#documents.delete(uri);
856
+ this.#publishes.delete(uri);
857
+ this.#refusals.delete(uri);
858
+ if (this.#destroyed || !this.#reachable) return;
859
+ this.#notify("textDocument/didClose", { textDocument: { uri } });
860
+ }
861
+ #request(method, params) {
862
+ this.#sequence += 1;
863
+ const id = this.#sequence;
864
+ const response = new Promise((resolve, reject) => {
865
+ this.#responses.set(id, resolve);
866
+ this.#failures.set(id, reject);
867
+ });
868
+ try {
869
+ this.#send({
870
+ jsonrpc: "2.0",
871
+ id,
872
+ method,
873
+ params
874
+ });
875
+ } catch (error) {
876
+ const reject = this.#failures.get(id);
877
+ this.#responses.delete(id);
878
+ this.#failures.delete(id);
879
+ reject?.(this.#fault(describeUnknown(error), error));
880
+ }
881
+ return response;
882
+ }
883
+ #notify(method, params) {
884
+ this.#sequence += 1;
885
+ this.#send({
886
+ jsonrpc: "2.0",
887
+ method,
888
+ params
889
+ });
890
+ }
891
+ #send(message) {
892
+ const child = this.#child;
893
+ if (this.#ending !== void 0) throw this.#fault(`The Oxlint language server exited with ${this.#ending}`);
894
+ if (child === void 0) throw this.#fault("The Oxlint language server is not running");
895
+ if (!child.stdin.writable) throw this.#fault("The Oxlint language server closed its input");
896
+ const content = JSON.stringify(message);
897
+ const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`;
898
+ child.stdin.write(header + content);
899
+ }
900
+ #read(chunk) {
901
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
902
+ while (this.#frame());
903
+ }
904
+ #frame() {
905
+ const boundary = this.#buffer.indexOf("\r\n\r\n");
906
+ if (boundary < 0) return false;
907
+ const length = parseContentLength(this.#buffer.subarray(0, boundary).toString("ascii"));
908
+ if (length === void 0) {
909
+ this.#fail(this.#fault("Oxlint sent an invalid JSON-RPC frame header"));
910
+ return false;
911
+ }
912
+ const start = boundary + 4;
913
+ const end = start + length;
914
+ if (this.#buffer.length < end) return false;
915
+ const content = this.#buffer.subarray(start, end).toString("utf8");
916
+ this.#buffer = this.#buffer.subarray(end);
917
+ try {
918
+ const message = JSON.parse(content);
919
+ this.#receive(message);
920
+ } catch (error) {
921
+ this.#fail(this.#fault(`Oxlint sent invalid JSON: ${describeUnknown(error)}`, error));
922
+ }
923
+ return true;
924
+ }
925
+ #receive(message) {
926
+ if (typeof message !== "object" || message === null) return;
927
+ if ("id" in message && typeof message.id === "number") {
928
+ const id = message.id;
929
+ const resolve = this.#responses.get(id);
930
+ const reject = this.#failures.get(id);
931
+ this.#responses.delete(id);
932
+ this.#failures.delete(id);
933
+ if ("error" in message && message.error !== void 0) reject?.(this.#fault(describeUnknown(message.error)));
934
+ else resolve?.("result" in message ? message.result : void 0);
935
+ return;
936
+ }
937
+ if (!("method" in message) || message.method !== "textDocument/publishDiagnostics") return;
938
+ if (!("params" in message) || typeof message.params !== "object" || message.params === null) return;
939
+ const params = message.params;
940
+ if (!("uri" in params) || typeof params.uri !== "string") return;
941
+ if (!("diagnostics" in params) || !Array.isArray(params.diagnostics)) return;
942
+ const publish = this.#publishes.get(params.uri);
943
+ if (publish === void 0) return;
944
+ publish(this.#issues(params.uri, params.diagnostics));
945
+ }
946
+ #issues(uri, diagnostics) {
947
+ const path = this.#documents.get(uri);
948
+ if (path === void 0) return [];
949
+ const issues = [];
950
+ for (const diagnostic of diagnostics) {
951
+ if (typeof diagnostic !== "object" || diagnostic === null) continue;
952
+ if (!("message" in diagnostic) || typeof diagnostic.message !== "string") continue;
953
+ if (!("range" in diagnostic) || typeof diagnostic.range !== "object" || diagnostic.range === null) {
954
+ issues.push({
955
+ origin: "claimant",
956
+ path,
957
+ message: diagnostic.message
958
+ });
959
+ continue;
960
+ }
961
+ const range = diagnostic.range;
962
+ if (!("start" in range) || typeof range.start !== "object" || range.start === null) {
963
+ issues.push({
964
+ origin: "claimant",
965
+ path,
966
+ message: diagnostic.message
967
+ });
968
+ continue;
969
+ }
970
+ const start = range.start;
971
+ if (!("line" in start) || typeof start.line !== "number") {
972
+ issues.push({
973
+ origin: "claimant",
974
+ path,
975
+ message: diagnostic.message
976
+ });
977
+ continue;
978
+ }
979
+ issues.push({
980
+ origin: "claimant",
981
+ path,
982
+ message: diagnostic.message,
983
+ line: start.line + 1
984
+ });
985
+ }
986
+ return issues;
987
+ }
988
+ #fail(error) {
989
+ for (const reject of this.#failures.values()) reject(error);
990
+ for (const reject of this.#refusals.values()) reject(error);
991
+ this.#responses.clear();
992
+ this.#failures.clear();
993
+ this.#publishes.clear();
994
+ this.#refusals.clear();
995
+ }
996
+ #exit(code, signal) {
997
+ this.#fail(this.#fault(`The Oxlint language server exited with ${this.#describe(code, signal)}`));
998
+ }
999
+ };
1000
+ //#endregion
1001
+ //#region src/server/stages/RuntimeStage.ts
1002
+ /**
1003
+ * Inspects tests through one resident Vitest service from the target workspace.
1004
+ *
1005
+ * @remarks
1006
+ * Construction starts Vitest with the threads pool and instruments each inline or function-declared
1007
+ * project with a Vite plugin that reads the active inspection's candidate overlay. A selected
1008
+ * string-declared project reports a workspace issue because its configuration carries no runtime
1009
+ * overlay plugin. Every inspection writes one fresh sibling specification, invalidates each
1010
+ * workspace module whose disk content or candidate revision changed, runs that specification,
1011
+ * evicts its result, and deletes the file. Clearing the overlay makes the next snapshot differ from
1012
+ * the candidate revision, so the next inspection invalidates that module and reads disk again.
1013
+ *
1014
+ * Vite retains one unresolved URL for every specification path, so the stage replaces its whole
1015
+ * Vitest service after 64 specifications rather than deleting from each map that service owns. Any
1016
+ * bound holds the retention flat, because the replacement releases everything the instance held,
1017
+ * and 64 is the value chosen. The inspection that crosses the bound pays the replacement
1018
+ * synchronously: it closes the resident service and warms a new one before it runs. On 2026-08-20,
1019
+ * over this package's own workspace on the host `guides/probe.md` § Cost names, two runs of 66
1020
+ * inspections put that one at 480 ms and 269 ms against median inspections of 156 ms and 155 ms, so
1021
+ * budget one call in 64 at 110 ms to 330 ms above the rest.
1022
+ *
1023
+ * Every issue this stage emits names its party, and the stage emits an issue for every party
1024
+ * `Party` declares. An `origin: 'claimant'` issue is a failure Vitest reported about the candidate.
1025
+ * An `origin: 'workspace'` issue names the target tree: a project the root configuration declares
1026
+ * as a path string, into which the stage can install no overlay, and a specification path that
1027
+ * crosses a symbolic link, whose existing components this host cannot inspect, or whose directory
1028
+ * the target tree blocks probe from creating. An `origin: 'instrument'` issue names this package's
1029
+ * own machinery: a specification it could not write after its directory exists, or one it could
1030
+ * not run, evict, or delete, in each case for a reason the target tree does not own, and a run
1031
+ * that reported no test. An unmapped caller test and a test whose named project is absent are claimant failures with
1032
+ * `code: 'missing'`, thrown rather than reported, because a test that never ran is no evidence
1033
+ * about the candidate.
1034
+ *
1035
+ * The write path's physical-containment guarantee covers the claim inputs and the target tree as
1036
+ * inspected. A concurrent process that mutates a path component between the final inspection and
1037
+ * the write is outside that guarantee.
1038
+ *
1039
+ * @example
1040
+ * ```ts
1041
+ * const stage = new RuntimeStage('/srv/checkout')
1042
+ * const check = await stage.inspect(subject)
1043
+ * await stage.destroy()
1044
+ * ```
1045
+ */
1046
+ var RuntimeStage = class {
1047
+ #workspace;
1048
+ #vitest;
1049
+ #overlay = new Overlay();
1050
+ #modules = /* @__PURE__ */ new Map();
1051
+ #revisions = /* @__PURE__ */ new Set();
1052
+ #specifications = 0;
1053
+ #closing;
1054
+ #progress = 0;
1055
+ #destroyed = false;
1056
+ /**
1057
+ * Starts warming the target workspace's Vitest service.
1058
+ *
1059
+ * @param workspace - The target workspace root. Default: the current working directory
1060
+ */
1061
+ constructor(workspace = process.cwd()) {
1062
+ this.#workspace = workspace;
1063
+ const vitest = loadWorkspaceModule(this.#workspace, "vitest/node");
1064
+ this.#vitest = this.#warm(vitest.createVitest);
1065
+ this.#vitest.catch(() => {});
1066
+ }
1067
+ get stage() {
1068
+ return "runtime";
1069
+ }
1070
+ get progress() {
1071
+ return this.#progress;
1072
+ }
1073
+ async inspect(subject) {
1074
+ if (this.#destroyed) throw createDestroyedError("runtime stage");
1075
+ const started = performance.now();
1076
+ const exitCode = process.exitCode;
1077
+ const vitest = await this.#runner();
1078
+ if (this.#destroyed) throw createDestroyedError("runtime stage");
1079
+ const project = this.#project(vitest, subject.test.path);
1080
+ if ("message" in project) return {
1081
+ stage: this.stage,
1082
+ elapsed: Math.round(performance.now() - started),
1083
+ issues: [project]
1084
+ };
1085
+ const overlay = new Overlay();
1086
+ this.#overlay = overlay;
1087
+ try {
1088
+ for (const draft of subject.files) overlay.set(resolveWorkspaceFile(this.#workspace, draft.path), draft.text);
1089
+ this.#revalidate(vitest);
1090
+ const specification = this.#specification(subject.test);
1091
+ if (typeof specification !== "string") return {
1092
+ stage: this.stage,
1093
+ elapsed: Math.round(performance.now() - started),
1094
+ issues: [specification]
1095
+ };
1096
+ const file = specification;
1097
+ this.#specifications += 1;
1098
+ this.#revisions.add(file);
1099
+ let issues = [];
1100
+ let cleanup = [];
1101
+ const task = project.createSpecification(file, void 0, "threads");
1102
+ this.#progress += 1;
1103
+ try {
1104
+ try {
1105
+ const result = await vitest.runTestSpecifications([task], false);
1106
+ issues = this.#issues(result, file, subject.test.path);
1107
+ } catch (error) {
1108
+ if (error instanceof ProbeError) throw error;
1109
+ throw new ProbeError(`The runtime stage could not run the generated specification (${describeUnknown(error)})`, {
1110
+ origin: "instrument",
1111
+ code: "malformed",
1112
+ context: {
1113
+ stage: this.stage,
1114
+ path: subject.test.path
1115
+ },
1116
+ cause: error
1117
+ });
1118
+ }
1119
+ } finally {
1120
+ this.#progress -= 1;
1121
+ process.exitCode = exitCode;
1122
+ this.#revisions.delete(file);
1123
+ cleanup = await this.#evict(vitest, file);
1124
+ try {
1125
+ const contained = resolveWorkspaceFile(this.#workspace, relative(this.#workspace, file), true);
1126
+ if (existsSync(contained)) unlinkSync(contained);
1127
+ } catch (error) {
1128
+ cleanup = [...cleanup, {
1129
+ origin: error instanceof ProbeError && error.origin === "workspace" ? "workspace" : "instrument",
1130
+ path: relativeWorkspaceFile(this.#workspace, file),
1131
+ message: `The runtime stage could not delete the generated specification (${describeUnknown(error)})`
1132
+ }];
1133
+ }
1134
+ }
1135
+ return {
1136
+ stage: this.stage,
1137
+ elapsed: Math.round(performance.now() - started),
1138
+ issues: [...issues, ...cleanup]
1139
+ };
1140
+ } finally {
1141
+ overlay.clear();
1142
+ }
1143
+ }
1144
+ destroy() {
1145
+ if (this.#closing !== void 0) return this.#closing;
1146
+ this.#destroyed = true;
1147
+ this.#closing = this.#destroy();
1148
+ return this.#closing;
1149
+ }
1150
+ async #destroy() {
1151
+ try {
1152
+ for (const file of this.#revisions) try {
1153
+ const contained = resolveWorkspaceFile(this.#workspace, relative(this.#workspace, file), true);
1154
+ if (existsSync(contained)) unlinkSync(contained);
1155
+ } catch {}
1156
+ this.#revisions.clear();
1157
+ const vitest = await this.#vitest.catch(() => void 0);
1158
+ if (vitest !== void 0) {
1159
+ vitest.cancelCurrentRun("keyboard-input").catch(() => {});
1160
+ await vitest.close();
1161
+ }
1162
+ } finally {
1163
+ this.#modules.clear();
1164
+ this.#overlay.clear();
1165
+ }
1166
+ }
1167
+ async #warm(create) {
1168
+ this.#sweep();
1169
+ const output = new PassThrough();
1170
+ output.resume();
1171
+ const signals = captureListeners(process, ["SIGINT", "SIGTERM"]);
1172
+ try {
1173
+ return create("test", {
1174
+ root: this.#workspace,
1175
+ config: resolveWorkspaceFile(this.#workspace, "vite.config.ts"),
1176
+ watch: false,
1177
+ run: true,
1178
+ pool: "threads",
1179
+ reporters: [{
1180
+ onInit() {},
1181
+ onTestRunEnd() {}
1182
+ }]
1183
+ }, { plugins: [{
1184
+ name: "orkestrel-project-instrumentation",
1185
+ enforce: "post",
1186
+ config: this.#configure.bind(this)
1187
+ }] }, {
1188
+ stdout: output,
1189
+ stderr: process.stderr
1190
+ });
1191
+ } finally {
1192
+ releaseListeners(process, signals);
1193
+ }
1194
+ }
1195
+ #configure(config) {
1196
+ const test = config.test;
1197
+ if (test?.projects === void 0) return;
1198
+ test.projects = test.projects.map((project) => this.#augment(project));
1199
+ }
1200
+ #augment(project) {
1201
+ if (typeof project === "string") return project;
1202
+ if (typeof project === "function") return this.#wrap(project);
1203
+ return Promise.resolve(project).then(this.#instrument.bind(this));
1204
+ }
1205
+ #wrap(project) {
1206
+ return async (environment) => this.#instrument(await project(environment));
1207
+ }
1208
+ #instrument(config) {
1209
+ return {
1210
+ ...config,
1211
+ plugins: [...config.plugins ?? [], {
1212
+ name: "orkestrel-runtime-overlay",
1213
+ enforce: "pre",
1214
+ resolveId: this.#resolve.bind(this),
1215
+ load: this.#load.bind(this)
1216
+ }]
1217
+ };
1218
+ }
1219
+ #resolve(id, importer) {
1220
+ const separator = id.lastIndexOf("?");
1221
+ const specifier = separator === -1 ? id : id.slice(0, separator);
1222
+ const suffix = separator === -1 ? "" : id.slice(separator);
1223
+ if (!specifier.startsWith(".") && !isAbsolute(specifier)) return void 0;
1224
+ if (importer === void 0 && !isAbsolute(specifier)) return void 0;
1225
+ const imported = isAbsolute(specifier) ? specifier : resolve(dirname(importer ?? this.#workspace), specifier);
1226
+ const candidates = [imported];
1227
+ const extension = extname(imported);
1228
+ if (extension === ".js") candidates.push(`${imported.slice(0, -extension.length)}.ts`);
1229
+ if (extension === "") candidates.push(`${imported}.ts`);
1230
+ for (const candidate of candidates) {
1231
+ const resolved = `${candidate}${suffix}`;
1232
+ if (this.#load(resolved) !== void 0) return resolved;
1233
+ }
1234
+ }
1235
+ #load(id) {
1236
+ const text = this.#overlay.text(id);
1237
+ if (text !== void 0) return text;
1238
+ const separator = id.lastIndexOf("?");
1239
+ if (separator === -1) return void 0;
1240
+ const query = id.slice(separator + 1);
1241
+ if (!query.startsWith("v=") || query.length === 2 || query.includes("&") || query.includes("#") || query.slice(2).includes("?")) return;
1242
+ return this.#overlay.text(id.slice(0, separator));
1243
+ }
1244
+ #specification(test) {
1245
+ let creating = false;
1246
+ const outcome = attempt(() => {
1247
+ const revision = `${process.pid}-${randomUUID()}`;
1248
+ const file = createRevisionFile(this.#workspace, test.path, revision);
1249
+ const target = relative(this.#workspace, file);
1250
+ resolveWorkspaceFile(this.#workspace, target, true);
1251
+ creating = true;
1252
+ mkdirSync(dirname(file), { recursive: true });
1253
+ creating = false;
1254
+ resolveWorkspaceFile(this.#workspace, target, true);
1255
+ writeFileSync(file, formatSpecification(test.text, revision), {
1256
+ encoding: "utf8",
1257
+ flag: "wx"
1258
+ });
1259
+ return file;
1260
+ });
1261
+ if (outcome.success) return outcome.value;
1262
+ if (outcome.error instanceof ProbeError && outcome.error.origin === "claimant") throw new ProbeError(outcome.error.message, {
1263
+ origin: outcome.error.origin,
1264
+ code: outcome.error.code,
1265
+ context: {
1266
+ ...outcome.error.context,
1267
+ path: test.path
1268
+ },
1269
+ cause: outcome.error.cause
1270
+ });
1271
+ return {
1272
+ origin: outcome.error instanceof ProbeError && outcome.error.origin === "workspace" || creating ? "workspace" : "instrument",
1273
+ path: test.path,
1274
+ message: `The runtime stage could not write the generated specification (${describeUnknown(outcome.error)})`
1275
+ };
1276
+ }
1277
+ #project(vitest, path) {
1278
+ const name = inferTestProject(relative(this.#workspace, resolve(this.#workspace, path)));
1279
+ if (name === void 0) throw new ProbeError("The runtime stage found no configured Vitest project matching the test path", {
1280
+ origin: "claimant",
1281
+ code: "missing",
1282
+ context: {
1283
+ stage: this.stage,
1284
+ path
1285
+ }
1286
+ });
1287
+ const project = vitest.projects.find((candidate) => candidate.name === name);
1288
+ if (project === void 0) throw new ProbeError(`The runtime stage found no configured Vitest project named ${name}`, {
1289
+ origin: "claimant",
1290
+ code: "missing",
1291
+ context: {
1292
+ stage: this.stage,
1293
+ path
1294
+ }
1295
+ });
1296
+ if (!project.vite.config.plugins.some((plugin) => plugin.name === "orkestrel-runtime-overlay")) return {
1297
+ origin: "workspace",
1298
+ path,
1299
+ message: `The runtime stage cannot instrument the string-declared Vitest project ${name} because its configuration carries no runtime overlay plugin`
1300
+ };
1301
+ return project;
1302
+ }
1303
+ async #evict(vitest, file) {
1304
+ try {
1305
+ const ids = [];
1306
+ for (const [id, task] of vitest.state.idMap) {
1307
+ const path = "filepath" in task ? task.filepath : task.file.filepath;
1308
+ if (resolve(path) === resolve(file)) ids.push(id);
1309
+ }
1310
+ for (const id of ids) vitest.state.idMap.delete(id);
1311
+ vitest.state.pathsSet.delete(file);
1312
+ vitest.clearSpecificationsCache(file);
1313
+ vitest.invalidateFile(file);
1314
+ const graphs = vitest.projects.flatMap((project) => Object.values(project.vite.environments).map((environment) => environment.moduleGraph));
1315
+ for (const graph of graphs) {
1316
+ const modules = graph.getModulesByFile(file);
1317
+ graph.onFileDelete(file);
1318
+ if (modules === void 0) continue;
1319
+ for (const module of modules) {
1320
+ for (const importer of module.importers) {
1321
+ importer.importedModules.delete(module);
1322
+ importer.acceptedHmrDeps.delete(module);
1323
+ }
1324
+ if (module.id !== null) graph.idToModuleMap.delete(module.id);
1325
+ graph.urlToModuleMap.delete(module.url);
1326
+ }
1327
+ graph.fileToModulesMap.delete(file);
1328
+ }
1329
+ vitest.watcher.onFileDelete(file);
1330
+ vitest.watcher.invalidates.delete(file);
1331
+ vitest.cache.results.removeFromCache(normalizePath(relative(this.#workspace, file)));
1332
+ await vitest.cache.results.writeToCache();
1333
+ return [];
1334
+ } catch (error) {
1335
+ return [{
1336
+ origin: "instrument",
1337
+ path: relativeWorkspaceFile(this.#workspace, file),
1338
+ message: `The runtime stage could not evict the generated specification (${describeUnknown(error)})`
1339
+ }];
1340
+ }
1341
+ }
1342
+ #runner() {
1343
+ if (this.#specifications < 64) return this.#vitest;
1344
+ this.#specifications = 0;
1345
+ this.#vitest = this.#replace(this.#vitest);
1346
+ this.#vitest.catch(() => {});
1347
+ return this.#vitest;
1348
+ }
1349
+ async #replace(current) {
1350
+ await (await current).close();
1351
+ if (this.#destroyed) throw createDestroyedError("runtime stage");
1352
+ const runner = loadWorkspaceModule(this.#workspace, "vitest/node");
1353
+ return this.#warm(runner.createVitest);
1354
+ }
1355
+ #revalidate(vitest) {
1356
+ const modules = this.#snapshot();
1357
+ for (const [path, digest] of modules) {
1358
+ if (this.#modules.get(path) === digest) continue;
1359
+ this.#invalidate(vitest, path);
1360
+ }
1361
+ for (const path of this.#modules.keys()) {
1362
+ if (modules.has(path)) continue;
1363
+ this.#invalidate(vitest, path);
1364
+ }
1365
+ this.#modules.clear();
1366
+ for (const [path, digest] of modules) this.#modules.set(path, digest);
1367
+ }
1368
+ #invalidate(vitest, path) {
1369
+ vitest.invalidateFile(path);
1370
+ vitest.watcher.invalidates.add(path);
1371
+ }
1372
+ #snapshot() {
1373
+ const modules = /* @__PURE__ */ new Map();
1374
+ for (const path of this.#walk()) {
1375
+ if (!matchesWorkspaceModule(path)) continue;
1376
+ try {
1377
+ const digest = createHash("sha256").update(readFileSync(path)).digest("hex");
1378
+ modules.set(path, digest);
1379
+ } catch {}
1380
+ }
1381
+ for (const path of this.#overlay.paths) modules.set(path, `overlay:${this.#overlay.revision}`);
1382
+ return modules;
1383
+ }
1384
+ #sweep() {
1385
+ for (const path of this.#walk()) {
1386
+ const match = [...basename(path).matchAll(/\.probe-((\d+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?=\.|$)/gu)].at(-1);
1387
+ const revision = match?.[1];
1388
+ const owner = match?.[2];
1389
+ if (revision === void 0 || owner === void 0) continue;
1390
+ if (this.#alive(Number.parseInt(owner, 10))) continue;
1391
+ if (!this.#owned(path, revision)) continue;
1392
+ try {
1393
+ unlinkSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, path), true));
1394
+ } catch {}
1395
+ }
1396
+ }
1397
+ #owned(path, revision) {
1398
+ const outcome = attempt(() => readFileSync(path, "utf8"));
1399
+ return outcome.success && matchesSpecification(outcome.value, revision);
1400
+ }
1401
+ #alive(id) {
1402
+ if (!Number.isSafeInteger(id) || id <= 0) return true;
1403
+ try {
1404
+ process.kill(id, 0);
1405
+ return true;
1406
+ } catch (error) {
1407
+ return error instanceof Error && "code" in error && error.code === "EPERM";
1408
+ }
1409
+ }
1410
+ *#walk() {
1411
+ const directories = [resolve(this.#workspace)];
1412
+ while (directories.length > 0) {
1413
+ const directory = directories.pop();
1414
+ if (directory === void 0) break;
1415
+ let entries = [];
1416
+ try {
1417
+ entries = readdirSync(directory, { withFileTypes: true });
1418
+ } catch {}
1419
+ for (const entry of entries) {
1420
+ if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
1421
+ const path = join(directory, entry.name);
1422
+ if (entry.isDirectory()) {
1423
+ directories.push(path);
1424
+ continue;
1425
+ }
1426
+ if (entry.isFile()) yield path;
1427
+ }
1428
+ }
1429
+ }
1430
+ #issues(result, file, original) {
1431
+ const specification = this.#real(file);
1432
+ const issues = [];
1433
+ for (const module of result.testModules) {
1434
+ const before = issues.length;
1435
+ for (const error of module.errors()) issues.push(this.#issue(error, specification, original));
1436
+ for (const test of module.children.allTests("failed")) {
1437
+ const errors = test.result().errors ?? [];
1438
+ for (const error of errors) issues.push(this.#issue(error, specification, original));
1439
+ }
1440
+ const state = module.state();
1441
+ if (state === "passed") {
1442
+ if (Array.from(module.children.allTests()).length === 0) issues.push({
1443
+ origin: "instrument",
1444
+ path: original,
1445
+ message: "Vitest ran no tests in the module"
1446
+ });
1447
+ for (const test of module.children.allTests("skipped")) issues.push({
1448
+ origin: "instrument",
1449
+ path: original,
1450
+ message: `Vitest did not run the test (${test.fullName})`
1451
+ });
1452
+ continue;
1453
+ }
1454
+ if (state === "skipped") {
1455
+ issues.push({
1456
+ origin: "instrument",
1457
+ path: original,
1458
+ message: "Vitest ran no tests in the module"
1459
+ });
1460
+ continue;
1461
+ }
1462
+ if (state === "failed") {
1463
+ if (issues.length === before) issues.push({
1464
+ origin: "claimant",
1465
+ path: original,
1466
+ message: "Vitest reported a failed test module"
1467
+ });
1468
+ continue;
1469
+ }
1470
+ if (state === "pending" || state === "queued") {
1471
+ issues.push({
1472
+ origin: "instrument",
1473
+ path: original,
1474
+ message: `Vitest did not finish the test module (${state})`
1475
+ });
1476
+ continue;
1477
+ }
1478
+ issues.push({
1479
+ origin: "instrument",
1480
+ path: original,
1481
+ message: `Vitest reported an unrecognized test module state (${state})`
1482
+ });
1483
+ }
1484
+ for (const error of result.unhandledErrors) issues.push(this.#issue(error, specification, original));
1485
+ if (result.testModules.length === 0 && issues.length === 0) issues.push({
1486
+ origin: "instrument",
1487
+ path: original,
1488
+ message: "Vitest returned no test module"
1489
+ });
1490
+ return issues;
1491
+ }
1492
+ #real(path) {
1493
+ const outcome = attempt(() => realpathSync(path));
1494
+ return outcome.success ? outcome.value : path;
1495
+ }
1496
+ #issue(error, specification, original) {
1497
+ const message = describeUnknown(error);
1498
+ if (typeof error !== "object" || error === null || !("stacks" in error)) return {
1499
+ origin: "claimant",
1500
+ path: original,
1501
+ message
1502
+ };
1503
+ const stacks = error.stacks;
1504
+ if (!Array.isArray(stacks)) return {
1505
+ origin: "claimant",
1506
+ path: original,
1507
+ message
1508
+ };
1509
+ for (const stack of stacks) {
1510
+ if (typeof stack !== "object" || stack === null) continue;
1511
+ if (!("file" in stack) || typeof stack.file !== "string") continue;
1512
+ const declared = stack.file.startsWith("file:") ? fileURLToPath(stack.file) : isAbsolute(stack.file) ? stack.file : resolve(this.#workspace, stack.file);
1513
+ const reported = this.#real(declared);
1514
+ const path = reported === specification ? original : relativeWorkspaceFile(this.#real(this.#workspace), reported);
1515
+ if (!("line" in stack) || typeof stack.line !== "number") return {
1516
+ origin: "claimant",
1517
+ path,
1518
+ message
1519
+ };
1520
+ return {
1521
+ origin: "claimant",
1522
+ path,
1523
+ message,
1524
+ line: stack.line
1525
+ };
1526
+ }
1527
+ return {
1528
+ origin: "claimant",
1529
+ path: original,
1530
+ message
1531
+ };
1532
+ }
1533
+ };
1534
+ //#endregion
1535
+ //#region src/server/stages/TypeStage.ts
1536
+ /**
1537
+ * Inspects TypeScript source through resident language services from the target workspace.
1538
+ *
1539
+ * @remarks
1540
+ * Construction starts loading the workspace's compiler and warming one service per project the
1541
+ * workspace declares. A candidate draft is checked against the project a call names, or
1542
+ * against its own scoped environment project when a call names none, while the test uses the root
1543
+ * project. Disk snapshots use their modification time as the service version so dependency edits
1544
+ * cannot leave a warm answer stale. Candidate text lives in an overlay the inspection owns, and
1545
+ * the language-service host answers existence, reads, and versions from it, so a candidate that
1546
+ * exists only as text is importable and a candidate that shadows a disk file is checked as the
1547
+ * text the case supplied. A project outside the declared set holds one recycled slot, so a caller
1548
+ * varying the project cannot grow the resident set. A language service checks one candidate
1549
+ * synchronously, so the stage hands the host's event loop back at each candidate boundary: a
1550
+ * caller's deadline is answered within one candidate's check rather than after the whole
1551
+ * inspection, and an inspection abandoned at that deadline stops at the next boundary.
1552
+ *
1553
+ * @example
1554
+ * ```ts
1555
+ * const stage = new TypeStage('/srv/checkout')
1556
+ * const check = await stage.inspect(subject, 'configs/src/tsconfig.core.json')
1557
+ * await stage.destroy()
1558
+ * ```
1559
+ */
1560
+ var TypeStage = class {
1561
+ #workspace;
1562
+ #typescript;
1563
+ #services = /* @__PURE__ */ new Map();
1564
+ #resident = /* @__PURE__ */ new Set();
1565
+ #options = /* @__PURE__ */ new Map();
1566
+ #files = /* @__PURE__ */ new Map();
1567
+ #diagnostics = /* @__PURE__ */ new Map();
1568
+ #overlay = new Overlay();
1569
+ #recycled;
1570
+ #closing;
1571
+ #progress = 0;
1572
+ #destroyed = false;
1573
+ /**
1574
+ * Starts warming the target workspace's TypeScript service.
1575
+ *
1576
+ * @param workspace - The target workspace root. Default: the current working directory
1577
+ */
1578
+ constructor(workspace = process.cwd()) {
1579
+ this.#workspace = workspace;
1580
+ const typescript = loadWorkspaceModule(this.#workspace, "typescript");
1581
+ this.#typescript = this.#warm(typescript);
1582
+ this.#typescript.catch(() => {});
1583
+ }
1584
+ get stage() {
1585
+ return "type";
1586
+ }
1587
+ get progress() {
1588
+ return this.#progress;
1589
+ }
1590
+ /**
1591
+ * Inspects one case, against a caller-named project where the caller names one.
1592
+ *
1593
+ * @remarks
1594
+ * The `project` parameter is this stage's own, not the stage contract's: the lint and runtime
1595
+ * stages read no project, so `StageInterface` declares one parameter and every caller that
1596
+ * needs this one holds a `TypeStage`.
1597
+ *
1598
+ * @param subject - The candidate drafts and test to inspect
1599
+ * @param project - The workspace-relative TypeScript project the candidate drafts are checked
1600
+ * against. Default: the scoped project each candidate path infers
1601
+ * @returns One outcome for this stage
1602
+ * @throws When the resident compiler cannot start or the stage has already been destroyed
1603
+ */
1604
+ async inspect(subject, project) {
1605
+ if (this.#destroyed) throw createDestroyedError("type stage");
1606
+ const started = performance.now();
1607
+ const typescript = await this.#typescript;
1608
+ if (this.#destroyed) throw createDestroyedError("type stage");
1609
+ const selections = subject.files.map((draft) => ({
1610
+ draft,
1611
+ path: resolveWorkspaceFile(this.#workspace, draft.path)
1612
+ })).map((candidate) => ({
1613
+ draft: candidate.draft,
1614
+ project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
1615
+ }));
1616
+ this.#configure(this.#service(typescript, "tsconfig.json"), "tsconfig.json");
1617
+ for (const selection of selections) this.#configure(this.#service(typescript, selection.project), selection.project);
1618
+ const overlay = new Overlay();
1619
+ this.#overlay = overlay;
1620
+ try {
1621
+ this.#record(subject.test, overlay);
1622
+ for (const draft of subject.files) this.#record(draft, overlay);
1623
+ this.#progress += 1;
1624
+ const issues = [];
1625
+ const projects = /* @__PURE__ */ new Set();
1626
+ const root = this.#service(typescript, "tsconfig.json");
1627
+ issues.push(...this.#issues(typescript, root, subject.test, "tsconfig.json", false, true));
1628
+ projects.add("tsconfig.json");
1629
+ await this.#unblock();
1630
+ for (const selection of selections) {
1631
+ const draft = selection.draft;
1632
+ const selected = selection.project;
1633
+ const service = this.#service(typescript, selected);
1634
+ issues.push(...this.#issues(typescript, service, draft, selected, project !== void 0, !projects.has(selected)));
1635
+ projects.add(selected);
1636
+ await this.#unblock();
1637
+ }
1638
+ return {
1639
+ stage: this.stage,
1640
+ elapsed: Math.round(performance.now() - started),
1641
+ issues
1642
+ };
1643
+ } finally {
1644
+ overlay.clear();
1645
+ }
1646
+ }
1647
+ /**
1648
+ * Resolves one project to the path and digest this stage applies for it.
1649
+ *
1650
+ * @remarks
1651
+ * Reads the parse this stage itself applies, filling its cache when the project is not already
1652
+ * resident, so the reported digest is the configuration the inspection is judged under rather
1653
+ * than a second parse a caller ran. The returned record is a value copy, so a later eviction
1654
+ * does not move it.
1655
+ *
1656
+ * @param project - The workspace-relative TypeScript project to resolve
1657
+ * @returns The resolved workspace-relative path and the digest of its compiler options
1658
+ * @throws When the project escapes the workspace, cannot be parsed, or the stage has already
1659
+ * been destroyed
1660
+ */
1661
+ async resolve(project) {
1662
+ if (this.#destroyed) throw createDestroyedError("type stage");
1663
+ const typescript = await this.#typescript;
1664
+ if (this.#destroyed) throw createDestroyedError("type stage");
1665
+ const resolved = resolveWorkspaceFile(this.#workspace, project);
1666
+ this.#service(typescript, project);
1667
+ return {
1668
+ path: relativeWorkspaceFile(this.#workspace, resolved),
1669
+ digest: computeDigest(this.#workspace, this.#options.get(resolved) ?? {})
1670
+ };
1671
+ }
1672
+ destroy() {
1673
+ if (this.#closing !== void 0) return this.#closing;
1674
+ this.#destroyed = true;
1675
+ this.#closing = this.#destroy();
1676
+ return this.#closing;
1677
+ }
1678
+ async #destroy() {
1679
+ await this.#typescript.catch(() => void 0);
1680
+ for (const service of this.#services.values()) service.dispose();
1681
+ this.#services.clear();
1682
+ this.#resident.clear();
1683
+ this.#recycled = void 0;
1684
+ this.#options.clear();
1685
+ this.#files.clear();
1686
+ this.#diagnostics.clear();
1687
+ }
1688
+ async #warm(typescript) {
1689
+ for (const project of this.#projects()) {
1690
+ this.#resident.add(resolveWorkspaceFile(this.#workspace, project));
1691
+ this.#service(typescript, project);
1692
+ }
1693
+ return typescript;
1694
+ }
1695
+ #projects() {
1696
+ const projects = ["tsconfig.json"];
1697
+ for (const axis of ["src", "app"]) {
1698
+ const directory = resolveWorkspaceFile(this.#workspace, `configs/${axis}`);
1699
+ let names;
1700
+ try {
1701
+ names = readdirSync(directory);
1702
+ } catch {
1703
+ continue;
1704
+ }
1705
+ for (const name of names.filter((entry) => /^tsconfig\.[^.]+\.json$/.test(entry)).sort()) projects.push(`configs/${axis}/${name}`);
1706
+ }
1707
+ return projects;
1708
+ }
1709
+ async #unblock() {
1710
+ await setImmediate();
1711
+ if (this.#destroyed) throw createDestroyedError("type stage");
1712
+ }
1713
+ #record(draft, overlay) {
1714
+ overlay.set(resolveWorkspaceFile(this.#workspace, draft.path), draft.text);
1715
+ }
1716
+ #service(typescript, project) {
1717
+ const path = resolveWorkspaceFile(this.#workspace, project);
1718
+ const existing = this.#services.get(path);
1719
+ if (existing !== void 0) return existing;
1720
+ const config = typescript.readConfigFile(path, typescript.sys.readFile);
1721
+ if (config.error !== void 0) throw new ProbeError(typescript.flattenDiagnosticMessageText(config.error.messageText, "\n"), {
1722
+ origin: "workspace",
1723
+ code: "malformed",
1724
+ context: {
1725
+ stage: this.stage,
1726
+ project
1727
+ }
1728
+ });
1729
+ const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, dirname(path), void 0, path);
1730
+ if (parsed.errors.length > 0) throw new ProbeError(typescript.flattenDiagnosticMessageText(parsed.errors[0]?.messageText ?? "", "\n"), {
1731
+ origin: "workspace",
1732
+ code: "malformed",
1733
+ context: {
1734
+ stage: this.stage,
1735
+ project
1736
+ }
1737
+ });
1738
+ this.#options.set(path, parsed.options);
1739
+ this.#files.set(path, parsed.fileNames);
1740
+ const service = typescript.createLanguageService({
1741
+ getCompilationSettings: () => this.#options.get(path) ?? {},
1742
+ getScriptFileNames: () => [...this.#files.get(path) ?? [], ...this.#overlay.paths],
1743
+ getScriptVersion: (file) => this.#version(file),
1744
+ getScriptSnapshot: (file) => this.#snapshot(typescript, file),
1745
+ getCurrentDirectory: () => this.#workspace,
1746
+ getDefaultLibFileName: (options) => typescript.getDefaultLibFilePath(options),
1747
+ fileExists: (file) => this.#overlay.text(file) !== void 0 || typescript.sys.fileExists(file),
1748
+ readFile: (file) => this.#overlay.text(file) ?? typescript.sys.readFile(file),
1749
+ readDirectory: typescript.sys.readDirectory,
1750
+ directoryExists: (directory) => typescript.sys.directoryExists(directory) || this.#overlay.covers(directory),
1751
+ getDirectories: typescript.sys.getDirectories,
1752
+ useCaseSensitiveFileNames: () => typescript.sys.useCaseSensitiveFileNames,
1753
+ getNewLine: () => typescript.sys.newLine
1754
+ });
1755
+ this.#services.set(path, service);
1756
+ if (!this.#resident.has(path)) this.#recycle(path);
1757
+ return service;
1758
+ }
1759
+ #configure(service, project) {
1760
+ const path = resolveWorkspaceFile(this.#workspace, project);
1761
+ if (this.#diagnostics.has(path)) return;
1762
+ this.#diagnostics.set(path, service.getCompilerOptionsDiagnostics());
1763
+ }
1764
+ #recycle(path) {
1765
+ const previous = this.#recycled;
1766
+ this.#recycled = path;
1767
+ if (previous === void 0 || previous === path) return;
1768
+ this.#services.get(previous)?.dispose();
1769
+ this.#services.delete(previous);
1770
+ this.#options.delete(previous);
1771
+ this.#files.delete(previous);
1772
+ this.#diagnostics.delete(previous);
1773
+ }
1774
+ #version(file) {
1775
+ if (this.#overlay.text(file) !== void 0) return `virtual:${this.#overlay.revision}`;
1776
+ try {
1777
+ return `disk:${statSync(file).mtimeMs}`;
1778
+ } catch {
1779
+ return "missing";
1780
+ }
1781
+ }
1782
+ #snapshot(typescript, file) {
1783
+ const text = this.#overlay.text(file) ?? typescript.sys.readFile(file);
1784
+ return text === void 0 ? void 0 : typescript.ScriptSnapshot.fromString(text);
1785
+ }
1786
+ #issues(typescript, service, draft, project, selected, configure) {
1787
+ const path = resolveWorkspaceFile(this.#workspace, draft.path);
1788
+ const configuration = resolveWorkspaceFile(this.#workspace, project);
1789
+ return [
1790
+ ...configure ? this.#diagnostics.get(configuration) ?? [] : [],
1791
+ ...service.getSyntacticDiagnostics(path),
1792
+ ...service.getSemanticDiagnostics(path)
1793
+ ].map((diagnostic) => this.#issue(typescript, diagnostic, project, selected));
1794
+ }
1795
+ #issue(typescript, diagnostic, project, selected) {
1796
+ const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1797
+ if (diagnostic.file === void 0) {
1798
+ if (selected) throw new ProbeError(message, {
1799
+ origin: "claimant",
1800
+ code: "refused",
1801
+ context: {
1802
+ stage: this.stage,
1803
+ project
1804
+ }
1805
+ });
1806
+ return {
1807
+ origin: "instrument",
1808
+ path: project,
1809
+ message
1810
+ };
1811
+ }
1812
+ const path = relativeWorkspaceFile(this.#workspace, diagnostic.file.fileName);
1813
+ if (diagnostic.start === void 0) return {
1814
+ origin: "claimant",
1815
+ path,
1816
+ message
1817
+ };
1818
+ return {
1819
+ origin: "claimant",
1820
+ path,
1821
+ message,
1822
+ line: diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start).line + 1
1823
+ };
1824
+ }
1825
+ };
1826
+ //#endregion
1827
+ //#region src/server/Probe.ts
1828
+ /**
1829
+ * Answers claims through resident TypeScript, Oxlint, and Vitest stages.
1830
+ *
1831
+ * @remarks
1832
+ * Construction resolves the target workspace's toolchain and begins warming every stage. The boot
1833
+ * controls mutate imported dependencies and refuse service unless the type and runtime stages
1834
+ * report their respective changes. One queue per stage admits inspections in arrival order, one at
1835
+ * a time, so a stage never serves two claims at once and the deadline covers active work rather
1836
+ * than queue wait. Each active stage inspection has a coordinator-owned deadline. An expiry at any
1837
+ * stage abandons that stage and replaces it before the next queued inspection begins, so one slow
1838
+ * claim costs that claim rather than the process. A failed boot is replaced the same way: the next
1839
+ * claim runs the controls again rather than inheriting a refusal.
1840
+ *
1841
+ * @example
1842
+ * ```ts
1843
+ * const probe = new Probe({ workspace: '/srv/checkout' })
1844
+ * const verdict = await probe.prove(claim)
1845
+ * await probe.destroy()
1846
+ * ```
1847
+ */
1848
+ var Probe = class {
1849
+ #workspace;
1850
+ #deadline;
1851
+ #emitter;
1852
+ #toolchain;
1853
+ #type;
1854
+ #lint;
1855
+ #runtime;
1856
+ #typeQueue;
1857
+ #lintQueue;
1858
+ #runtimeQueue;
1859
+ #arming;
1860
+ #closing;
1861
+ #destroyed = false;
1862
+ /**
1863
+ * Resolves the target toolchain and starts warming the resident stages.
1864
+ *
1865
+ * @param options - Workspace, deadline, and initial observation hooks
1866
+ */
1867
+ constructor(options) {
1868
+ this.#workspace = options?.workspace ?? process.cwd();
1869
+ this.#deadline = createTimeout({ ms: options?.deadline ?? 3e4 }).ms;
1870
+ this.#emitter = new Emitter({
1871
+ ...options?.on === void 0 ? {} : { on: options.on },
1872
+ ...options?.error === void 0 ? {} : { error: options.error }
1873
+ });
1874
+ this.#toolchain = Object.freeze({
1875
+ typescript: this.#version("typescript"),
1876
+ oxlint: this.#version("oxlint"),
1877
+ vitest: this.#version("vitest")
1878
+ });
1879
+ this.#type = new TypeStage(this.#workspace);
1880
+ this.#lint = new LintStage(this.#workspace);
1881
+ this.#runtime = new RuntimeStage(this.#workspace);
1882
+ this.#typeQueue = createQueue({
1883
+ concurrency: 1,
1884
+ retries: 0,
1885
+ handler: (inspection) => this.#inspectStage(this.#type, this.#type.progress, this.#type.inspect(inspection.subject, inspection.claim.project), inspection.claim)
1886
+ });
1887
+ this.#lintQueue = createQueue({
1888
+ concurrency: 1,
1889
+ retries: 0,
1890
+ handler: (inspection) => this.#inspectStage(this.#lint, this.#lint.progress, this.#lint.inspect(inspection.subject), inspection.claim)
1891
+ });
1892
+ this.#runtimeQueue = createQueue({
1893
+ concurrency: 1,
1894
+ retries: 0,
1895
+ handler: (inspection) => this.#inspectStage(this.#runtime, this.#runtime.progress, this.#runtime.inspect(inspection.subject), inspection.claim)
1896
+ });
1897
+ this.#arming = this.#arm();
1898
+ this.#arming.catch(() => {});
1899
+ }
1900
+ get emitter() {
1901
+ return this.#emitter;
1902
+ }
1903
+ get toolchain() {
1904
+ return this.#toolchain;
1905
+ }
1906
+ async prove(claim) {
1907
+ try {
1908
+ this.#support();
1909
+ await this.#ready();
1910
+ if (this.#destroyed) throw createDestroyedError("probe");
1911
+ const started = performance.now();
1912
+ const id = randomUUID();
1913
+ const project = await this.#type.resolve(claim.project);
1914
+ const digest = computeDigest(this.#workspace, {
1915
+ case: claim.case,
1916
+ control: claim.control
1917
+ });
1918
+ const subject = Object.freeze(await this.#inspect(claim.case, claim));
1919
+ const control = Object.freeze(await this.#inspect(claim.control, claim));
1920
+ const basis = {
1921
+ id,
1922
+ digest,
1923
+ toolchain: this.#toolchain,
1924
+ project,
1925
+ reason: claim.control.reason,
1926
+ case: subject,
1927
+ control,
1928
+ elapsed: Math.round(performance.now() - started)
1929
+ };
1930
+ const receipt = computeReceipt(basis, claim.control.stage);
1931
+ const verdict = receipt === void 0 ? basis : {
1932
+ ...basis,
1933
+ receipt
1934
+ };
1935
+ this.#emitter.emit("prove", verdict);
1936
+ return verdict;
1937
+ } catch (error) {
1938
+ this.#emitter.emit("error", error);
1939
+ throw error;
1940
+ }
1941
+ }
1942
+ destroy() {
1943
+ if (this.#closing !== void 0) return this.#closing;
1944
+ this.#destroyed = true;
1945
+ this.#closing = this.#destroy();
1946
+ return this.#closing;
1947
+ }
1948
+ async #ready() {
1949
+ const attempt = this.#arming;
1950
+ try {
1951
+ await attempt;
1952
+ return;
1953
+ } catch (error) {
1954
+ if (this.#destroyed) throw error;
1955
+ }
1956
+ if (this.#arming === attempt) {
1957
+ this.#arming = this.#arm();
1958
+ this.#arming.catch(() => {});
1959
+ }
1960
+ await this.#arming;
1961
+ }
1962
+ async #arm() {
1963
+ const created = this.#workbench();
1964
+ try {
1965
+ await this.#boot(created);
1966
+ } catch (error) {
1967
+ throw new ProbeError(`The probe could not arm: ${describeUnknown(error)}`, {
1968
+ origin: "instrument",
1969
+ code: "malformed",
1970
+ cause: error
1971
+ });
1972
+ }
1973
+ this.#emitter.emit("arm", this.#toolchain);
1974
+ }
1975
+ #workbench() {
1976
+ const path = "tmp/probe";
1977
+ const directory = resolveWorkspaceFile(this.#workspace, path);
1978
+ const created = !existsSync(directory);
1979
+ try {
1980
+ mkdirSync(resolveWorkspaceFile(this.#workspace, path, true), { recursive: true });
1981
+ } catch (error) {
1982
+ const code = error instanceof ProbeError && error.origin === "workspace" ? error.code : "malformed";
1983
+ throw new ProbeError(`The probe could not create the boot workbench (${describeUnknown(error)})`, {
1984
+ origin: "workspace",
1985
+ code,
1986
+ context: { path },
1987
+ cause: error
1988
+ });
1989
+ }
1990
+ return created;
1991
+ }
1992
+ async #boot(created) {
1993
+ const revision = `${process.pid}-${randomUUID()}`;
1994
+ const typeDependency = createRevisionFile(this.#workspace, "tmp/probe/arm-type.ts", revision);
1995
+ const runtimeDependency = createRevisionFile(this.#workspace, "tmp/probe/arm-runtime.ts", revision);
1996
+ const typeModule = basename(typeDependency, ".ts");
1997
+ const runtimeModule = basename(runtimeDependency, ".ts");
1998
+ const typeTest = {
1999
+ path: `tmp/probe/${typeModule}.test.ts`,
2000
+ text: `import type { Signal } from './${typeModule}.js'\nimport { expect, test } from 'vitest'\nconst SIGNAL: Signal = 'before'\ntest('revalidates a mutated type', () => {\n\texpect(SIGNAL).toBe('before')\n})\n`
2001
+ };
2002
+ const runtimeTest = {
2003
+ path: `tmp/probe/${runtimeModule}.test.ts`,
2004
+ text: `import { SIGNAL } from './${runtimeModule}.js'\nimport { expect, test } from 'vitest'\ntest('revalidates a mutated value', () => {\n\texpect(SIGNAL).toBe('before')\n})\n`
2005
+ };
2006
+ const typeClaim = {
2007
+ project: "tsconfig.json",
2008
+ case: {
2009
+ files: [],
2010
+ test: typeTest
2011
+ },
2012
+ control: {
2013
+ files: [],
2014
+ test: typeTest,
2015
+ stage: "type",
2016
+ reason: "the imported type changed after the resident type host cached it"
2017
+ }
2018
+ };
2019
+ const runtimeClaim = {
2020
+ project: "tsconfig.json",
2021
+ case: {
2022
+ files: [],
2023
+ test: runtimeTest
2024
+ },
2025
+ control: {
2026
+ files: [],
2027
+ test: runtimeTest,
2028
+ stage: "runtime",
2029
+ reason: "the imported dependency changed after the resident runtime cached it"
2030
+ }
2031
+ };
2032
+ try {
2033
+ resolveWorkspaceFile(this.#workspace, relative(this.#workspace, typeDependency), true);
2034
+ resolveWorkspaceFile(this.#workspace, relative(this.#workspace, runtimeDependency), true);
2035
+ writeFileSync(typeDependency, formatSpecification("export type Signal = string\n", revision), {
2036
+ encoding: "utf8",
2037
+ flag: "wx"
2038
+ });
2039
+ writeFileSync(runtimeDependency, formatSpecification("export const SIGNAL = 'before'\n", revision), {
2040
+ encoding: "utf8",
2041
+ flag: "wx"
2042
+ });
2043
+ const beforeType = await this.#inspect(typeClaim.case, typeClaim);
2044
+ const beforeRuntime = await this.#inspect(runtimeClaim.case, runtimeClaim);
2045
+ const before = [...beforeType, ...beforeRuntime];
2046
+ if (before.some((check) => check.issues.length > 0)) throw new ProbeError(`The probe boot control did not begin clean\n${before.map(formatCheck).join("\n")}`, {
2047
+ origin: "instrument",
2048
+ code: "malformed"
2049
+ });
2050
+ resolveWorkspaceFile(this.#workspace, relative(this.#workspace, typeDependency), true);
2051
+ writeFileSync(typeDependency, formatSpecification("export type Signal = number\n", revision), "utf8");
2052
+ const afterType = await this.#inspect(typeClaim.control, typeClaim);
2053
+ const type = afterType.find((check) => check.stage === typeClaim.control.stage);
2054
+ const tolerant = afterType.find((check) => check.stage === "runtime");
2055
+ if (type === void 0 || type.issues.length === 0) throw new ProbeError(`The probe boot type control did not detect a mutated dependency\n${afterType.map(formatCheck).join("\n")}`, {
2056
+ origin: "instrument",
2057
+ code: "malformed",
2058
+ context: { stage: typeClaim.control.stage }
2059
+ });
2060
+ if (tolerant === void 0 || tolerant.issues.length > 0) throw new ProbeError(`The probe boot type control did not remain runtime-clean\n${afterType.map(formatCheck).join("\n")}`, {
2061
+ origin: "instrument",
2062
+ code: "malformed",
2063
+ context: { stage: "runtime" }
2064
+ });
2065
+ resolveWorkspaceFile(this.#workspace, relative(this.#workspace, runtimeDependency), true);
2066
+ writeFileSync(runtimeDependency, formatSpecification("export const SIGNAL = 'after'\n", revision), "utf8");
2067
+ const afterRuntime = await this.#inspect(runtimeClaim.control, runtimeClaim);
2068
+ const runtime = afterRuntime.find((check) => check.stage === runtimeClaim.control.stage);
2069
+ if (runtime === void 0 || runtime.issues.length === 0) throw new ProbeError(`The probe boot runtime control did not detect a mutated dependency\n${afterRuntime.map(formatCheck).join("\n")}`, {
2070
+ origin: "instrument",
2071
+ code: "malformed",
2072
+ context: { stage: runtimeClaim.control.stage }
2073
+ });
2074
+ } finally {
2075
+ rmSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, typeDependency), true), { force: true });
2076
+ rmSync(resolveWorkspaceFile(this.#workspace, relative(this.#workspace, runtimeDependency), true), { force: true });
2077
+ if (created) try {
2078
+ rmdirSync(resolveWorkspaceFile(this.#workspace, "tmp/probe", true));
2079
+ } catch {}
2080
+ }
2081
+ }
2082
+ #inspect(subject, claim) {
2083
+ const inspection = {
2084
+ subject,
2085
+ claim
2086
+ };
2087
+ const admitted = [
2088
+ this.#typeQueue.enqueue(inspection),
2089
+ this.#lintQueue.enqueue(inspection),
2090
+ this.#runtimeQueue.enqueue(inspection)
2091
+ ];
2092
+ for (const pending of admitted) pending.catch(() => {});
2093
+ return Promise.all(admitted);
2094
+ }
2095
+ async #inspectStage(stage, progress, operation, claim) {
2096
+ const timeout = createTimeout({ ms: this.#deadline });
2097
+ timeout.start();
2098
+ try {
2099
+ return await Promise.race([operation, this.#expiry(timeout, `The ${stage.stage} stage exceeded ${this.#deadline} ms`, stage, progress)]);
2100
+ } catch (error) {
2101
+ if (!timeout.expired) throw error;
2102
+ if (await this.#recycle(stage)) this.#emitter.emit("expire", claim);
2103
+ throw error;
2104
+ } finally {
2105
+ timeout.clear();
2106
+ }
2107
+ }
2108
+ async #recycle(stage) {
2109
+ const timeout = createTimeout({ ms: this.#deadline });
2110
+ timeout.start();
2111
+ try {
2112
+ await Promise.race([stage.destroy(), this.#expiry(timeout, `The ${stage.stage} stage recovery exceeded ${this.#deadline} ms`, stage, stage.progress)]);
2113
+ } catch {} finally {
2114
+ timeout.clear();
2115
+ }
2116
+ if (this.#destroyed) return false;
2117
+ if (stage === this.#type) {
2118
+ this.#type = new TypeStage(this.#workspace);
2119
+ return true;
2120
+ }
2121
+ if (stage === this.#lint) {
2122
+ this.#lint = new LintStage(this.#workspace);
2123
+ return true;
2124
+ }
2125
+ if (stage === this.#runtime) {
2126
+ this.#runtime = new RuntimeStage(this.#workspace);
2127
+ return true;
2128
+ }
2129
+ return false;
2130
+ }
2131
+ #expiry(timeout, message, stage, progress) {
2132
+ return new Promise((_resolve, reject) => {
2133
+ timeout.signal.addEventListener("abort", () => reject(new ProbeError(message, {
2134
+ origin: stage.progress > progress ? "claimant" : "instrument",
2135
+ code: "deadline",
2136
+ context: {
2137
+ stage: stage.stage,
2138
+ deadline: this.#deadline
2139
+ }
2140
+ })), { once: true });
2141
+ });
2142
+ }
2143
+ async #destroy() {
2144
+ try {
2145
+ await this.#arming;
2146
+ } catch {}
2147
+ await Promise.all([
2148
+ this.#type.destroy(),
2149
+ this.#lint.destroy(),
2150
+ this.#runtime.destroy()
2151
+ ]);
2152
+ }
2153
+ #version(name) {
2154
+ const version = readWorkspaceManifest(this.#workspace, name).contents.version;
2155
+ if (typeof version !== "string") throw new ProbeError(`${name} publishes no readable version`, {
2156
+ origin: "workspace",
2157
+ code: "malformed",
2158
+ context: { name }
2159
+ });
2160
+ return version;
2161
+ }
2162
+ #support() {
2163
+ const version = this.#toolchain.typescript;
2164
+ const range = peerDependencies.typescript;
2165
+ const supported = /^\^(\d+)\./u.exec(range)?.[1];
2166
+ const found = /^(\d+)\./u.exec(version)?.[1];
2167
+ if (supported === void 0 || found !== supported) throw new ProbeError(`The supported TypeScript range is ${range}; found ${version}`, {
2168
+ origin: "workspace",
2169
+ code: "malformed",
2170
+ context: {
2171
+ name: "typescript",
2172
+ value: version
2173
+ }
2174
+ });
2175
+ }
2176
+ };
2177
+ //#endregion
2178
+ //#region src/server/ProbeServer.ts
2179
+ /**
2180
+ * Serves one probe over this process's Model Context Protocol stdio transport.
2181
+ *
2182
+ * @remarks
2183
+ * Construction creates the probe, publishes the `prove` tool, and binds the dual-era dispatcher to
2184
+ * the stdio transport. The probe begins warming there, so a harness that spawns the entry pays
2185
+ * arming while its client is still handshaking.
2186
+ *
2187
+ * `start` seizes standard input and registers the signals a harness ends a child with, and records
2188
+ * whether standard input was already flowing. Every listener it attaches is held as a field, and
2189
+ * `destroy` removes exactly those by reference, so a listener a host registers while this server is
2190
+ * serving is still attached and still fires afterwards. The transport reads a stream this server
2191
+ * owns rather than this process's standard input, which is what makes that possible: the
2192
+ * transport's own listeners land on that stream, and the only listeners this server ever puts on
2193
+ * `process.stdin` are the `data`, `close`, and `error` forwarders into it. `destroy` pauses the
2194
+ * stream only when this server is what set it flowing and nothing else is reading it, so a process
2195
+ * that outlives this server reads its own standard input again and receives its own signals.
2196
+ * Teardown removes the signal handlers first: it takes seconds when a boot is in flight, and a
2197
+ * harness that signals again means to kill rather than to queue.
2198
+ *
2199
+ * @example
2200
+ * ```ts
2201
+ * const server = new ProbeServer({ workspace: process.cwd() })
2202
+ * server.start()
2203
+ * await server.destroy()
2204
+ * ```
2205
+ */
2206
+ var ProbeServer = class {
2207
+ #probe;
2208
+ #stream;
2209
+ #transport;
2210
+ #data;
2211
+ #close;
2212
+ #error;
2213
+ #signal;
2214
+ #owns;
2215
+ #closing;
2216
+ /**
2217
+ * Creates the probe this server publishes and binds it to this process's stdio transport.
2218
+ *
2219
+ * @param options - Workspace, deadline, and initial observation hooks for the probe it creates
2220
+ */
2221
+ constructor(options) {
2222
+ this.#probe = new Probe(options);
2223
+ this.#stream = new PassThrough();
2224
+ this.#transport = createStdioServer(createMCPLegacy(this.#publish()), { input: this.#stream });
2225
+ this.#data = this.#receive.bind(this);
2226
+ this.#close = this.#finish.bind(this);
2227
+ this.#error = this.#fail.bind(this);
2228
+ this.#signal = this.#release.bind(this);
2229
+ }
2230
+ start() {
2231
+ if (this.#closing !== void 0) throw createDestroyedError("probe server");
2232
+ if (this.#owns !== void 0) return;
2233
+ this.#owns = process.stdin.readableFlowing !== true;
2234
+ this.#transport.start();
2235
+ process.stdin.on("data", this.#data);
2236
+ process.stdin.on("close", this.#close);
2237
+ process.stdin.on("error", this.#error);
2238
+ process.on("SIGINT", this.#signal);
2239
+ process.on("SIGTERM", this.#signal);
2240
+ }
2241
+ destroy() {
2242
+ if (this.#closing !== void 0) return this.#closing;
2243
+ this.#closing = this.#destroy();
2244
+ return this.#closing;
2245
+ }
2246
+ async #destroy() {
2247
+ process.removeListener("SIGINT", this.#signal);
2248
+ process.removeListener("SIGTERM", this.#signal);
2249
+ this.#transport.stop();
2250
+ process.stdin.removeListener("data", this.#data);
2251
+ process.stdin.removeListener("close", this.#close);
2252
+ process.stdin.removeListener("error", this.#error);
2253
+ if (this.#owns === true && process.stdin.listenerCount("data") === 0) process.stdin.pause();
2254
+ this.#owns = void 0;
2255
+ this.#stream.destroy();
2256
+ await this.#probe.destroy();
2257
+ }
2258
+ #receive(chunk) {
2259
+ this.#stream.write(chunk);
2260
+ }
2261
+ #finish() {
2262
+ this.#stream.end();
2263
+ }
2264
+ #fail(error) {
2265
+ this.#stream.destroy(error);
2266
+ }
2267
+ #release() {
2268
+ this.destroy();
2269
+ }
2270
+ #publish() {
2271
+ const parameters = schemaToParameters(compileSchema(CLAIM_SHAPE));
2272
+ if (parameters === void 0) throw new ProbeError("The claim schema cannot be advertised as tool parameters", {
2273
+ origin: "instrument",
2274
+ code: "malformed"
2275
+ });
2276
+ const tools = createToolManager();
2277
+ tools.add(createTool({
2278
+ name: "prove",
2279
+ description: "Proves a claim with type, lint, and runtime evidence.",
2280
+ parameters,
2281
+ execute: async (input) => {
2282
+ if (!isClaim(input)) {
2283
+ const refused = findRefusedPaths(input);
2284
+ throw new ProbeError(refused.length === 0 ? "The prove tool requires a claim matching the advertised schema" : `The prove tool refuses ${refused.join(", ")}: a draft path must stay inside the workspace, which the advertised schema does not constrain`, {
2285
+ origin: "claimant",
2286
+ code: "refused",
2287
+ context: { value: input }
2288
+ });
2289
+ }
2290
+ return this.#probe.prove(input);
2291
+ }
2292
+ }));
2293
+ return createMCPServer({
2294
+ identity: {
2295
+ name: "probe",
2296
+ version
2297
+ },
2298
+ tools,
2299
+ execution: async ({ call }) => {
2300
+ const result = await tools.execute(call);
2301
+ if (!result.success) return result;
2302
+ if (!isVerdict(result.value)) throw new ProbeError("The prove tool returned an invalid verdict", {
2303
+ origin: "instrument",
2304
+ code: "malformed",
2305
+ context: { value: result.value }
2306
+ });
2307
+ return {
2308
+ resultType: "complete",
2309
+ content: [{
2310
+ type: "text",
2311
+ text: formatVerdict(result.value)
2312
+ }]
2313
+ };
2314
+ }
2315
+ });
2316
+ }
2317
+ };
2318
+ //#endregion
2319
+ export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, captureListeners, computeDigest, createRevisionFile, describeUnknown, findRefusedPaths, inferDocumentLanguage, inferTestProject, inferTypeProject, loadWorkspaceModule, matchesWorkspaceModule, normalizePath, normalizeValue, parseContentLength, readWorkspaceManifest, relativeWorkspaceFile, releaseListeners, resolveWorkspaceBinary, resolveWorkspaceFile, resolveWorkspaceModule };
2320
+
2321
+ //# sourceMappingURL=index.js.map