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