@orkestrel/probe 0.0.24 → 0.0.26

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.
@@ -4,8 +4,9 @@ import { closeSync, constants, existsSync, ftruncateSync, lstatSync, mkdirSync,
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { attempt, compileGuard, compileSchema, isArray, isError, isNumber, isObject, isRecord, isString, parseJSON, schemaToParameters } from "@orkestrel/contract";
7
+ import { attempt, compileGuard, compileSchema, isArray, isError, isFunction, isNumber, isObject, isRecord, isString, parseJSON, schemaToParameters } from "@orkestrel/contract";
8
8
  import { CLAIM_SHAPE, LINT_DEADLINE, LINT_TEARDOWN, PROBE_DEADLINE, PROBE_KEYS, PROBE_RESTARTS, PROBE_SPECIFICATIONS, PROBE_WARM, ProbeError, RUNTIME_PLUGIN, TYPE_MIRROR, computeReceipt, createDestroyedError, formatCheck, formatReceipt, formatSpecification, formatVerdict, isClaim, isDraft, isProbeError, isVerdict, matchesSpecification } from "../core/index.js";
9
+ import { inspectProcess, killTree } from "@orkestrel/process/server";
9
10
  import { Emitter } from "@orkestrel/emitter";
10
11
  import { createPool, isPoolError } from "@orkestrel/pool";
11
12
  import { createLSPClient, isLSPError } from "@orkestrel/lsp";
@@ -13,10 +14,11 @@ import { addAbortListener } from "node:events";
13
14
  import { createTimeout } from "@orkestrel/timeout";
14
15
  import { createStdioClientTransport } from "@orkestrel/lsp/server";
15
16
  import { PassThrough } from "node:stream";
16
- import { spawn, spawnSync } from "node:child_process";
17
+ import { AsyncLocalStorage } from "node:async_hooks";
18
+ import { spawn } from "node:child_process";
17
19
  import { createStdioServer } from "@orkestrel/mcp/server";
18
20
  import { createTool, createToolManager } from "@orkestrel/tool";
19
- //#region src/server/errors.ts
21
+ // src/server/errors.ts
20
22
  /**
21
23
  * Creates the protocol failure returned when the MCP handshake cannot arm the probe.
22
24
  *
@@ -34,8 +36,7 @@ function createHandshakeError(error) {
34
36
  code: error.code
35
37
  });
36
38
  }
37
- //#endregion
38
- //#region src/server/helpers.ts
39
+ // src/server/helpers.ts
39
40
  /**
40
41
  * Rewrites one path into the forward-slash spelling this package compares and reports paths in.
41
42
  *
@@ -150,13 +151,13 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
150
151
  if (!mutate) return file;
151
152
  try {
152
153
  const canonical = realpathSync(root);
153
- let descendant = root;
154
- for (const segment of path.split(sep)) {
155
- descendant = resolve(descendant, segment);
154
+ path.split(sep).reduce((parent, segment) => {
155
+ if (parent === void 0) return void 0;
156
+ const descendant = resolve(parent, segment);
156
157
  const outcome = attempt(() => lstatSync(descendant));
157
158
  if (!outcome.success) {
158
159
  const code = readFaultCode(outcome.error);
159
- if (code === "ENOENT" || code === "ENOTDIR") break;
160
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
160
161
  throw outcome.error;
161
162
  }
162
163
  if (outcome.value.isSymbolicLink()) throw new ProbeError(`Path crosses a symbolic link: ${target}`, {
@@ -164,12 +165,14 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
164
165
  code: "refused",
165
166
  context: { path: target }
166
167
  });
167
- if (escapesRoot(canonical, realpathSync(descendant))) throw new ProbeError(`Path escapes the workspace: ${target}`, {
168
+ const resolved = realpathSync(descendant);
169
+ if (escapesRoot(canonical, resolved)) throw new ProbeError(`Path escapes the workspace: ${target}`, {
168
170
  origin: "claimant",
169
171
  code: "refused",
170
172
  context: { path: target }
171
173
  });
172
- }
174
+ return descendant;
175
+ }, root);
173
176
  } catch (error) {
174
177
  if (isProbeError(error)) throw error;
175
178
  const code = readFaultCode(error);
@@ -334,14 +337,11 @@ function relativeWorkspaceMessage(workspace, message) {
334
337
  const root = resolve(workspace);
335
338
  const forward = normalizePath(root);
336
339
  const url = pathToFileURL(forward).href;
337
- const spellings = /* @__PURE__ */ new Set([
340
+ return [.../* @__PURE__ */ new Set([
338
341
  url.endsWith("/") ? url : `${url}/`,
339
342
  forward.endsWith("/") ? forward : `${forward}/`,
340
343
  root.endsWith(sep) ? root : `${root}${sep}`
341
- ]);
342
- let text = message;
343
- for (const spelling of spellings) text = text.replaceAll(spelling, (match, offset, source) => /[^\s"'`()[\]{}<>,;:=|]/u.test(source.charAt(offset - 1)) ? match : "");
344
- return text;
344
+ ])].reduce((text, spelling) => text.replaceAll(spelling, (match, offset, source) => /[^\s"'`()[\]{}<>,;:=|]/u.test(source.charAt(offset - 1)) ? match : ""), message);
345
345
  }
346
346
  /**
347
347
  * Scans the plain-text output of one compiler run into the diagnostics it reported.
@@ -370,37 +370,36 @@ function relativeWorkspaceMessage(workspace, message) {
370
370
  * ```
371
371
  */
372
372
  function scanDiagnostics(text) {
373
- const diagnostics = [];
374
- for (const line of text.split(/\r\n|\n/)) {
373
+ const lines = text.split(/\r\n|\n/);
374
+ const headers = lines.map((line, index) => {
375
375
  const located = /^(.+)\((\d+),(\d+)\): (?:error|warning) TS\d+: (.*)$/u.exec(line);
376
376
  if (located !== null) {
377
377
  const point = {
378
378
  line: Number.parseInt(located[2] ?? "1", 10) - 1,
379
379
  character: Number.parseInt(located[3] ?? "1", 10) - 1
380
380
  };
381
- diagnostics.push({
382
- path: normalizePath(located[1] ?? ""),
383
- range: {
384
- start: point,
385
- end: point
386
- },
387
- message: located[4] ?? ""
388
- });
389
- continue;
381
+ return {
382
+ index,
383
+ diagnostic: {
384
+ path: normalizePath(located[1] ?? ""),
385
+ range: {
386
+ start: point,
387
+ end: point
388
+ },
389
+ message: located[4] ?? ""
390
+ }
391
+ };
390
392
  }
391
393
  const bare = /^(?:error|warning) TS\d+: (.*)$/u.exec(line);
392
- if (bare !== null) {
393
- diagnostics.push({ message: bare[1] ?? "" });
394
- continue;
395
- }
396
- const previous = diagnostics.at(-1);
397
- if (previous === void 0 || !/^\s+\S/u.test(line)) continue;
398
- diagnostics[diagnostics.length - 1] = {
399
- ...previous,
400
- message: `${previous.message}\n${line}`
394
+ return bare === null ? void 0 : {
395
+ index,
396
+ diagnostic: { message: bare[1] ?? "" }
401
397
  };
402
- }
403
- return diagnostics;
398
+ }).filter((header) => header !== void 0);
399
+ return headers.map((header, index) => ({
400
+ ...header.diagnostic,
401
+ message: [header.diagnostic.message, ...lines.slice(header.index + 1, headers[index + 1]?.index).filter((line) => /^\s+\S/u.test(line))].join("\n")
402
+ }));
404
403
  }
405
404
  /**
406
405
  * Resolves one installed module from the target workspace.
@@ -475,10 +474,9 @@ function loadWorkspaceVitest(workspace) {
475
474
  * ```
476
475
  */
477
476
  function readWorkspaceManifest(workspace, name) {
478
- let path;
479
- try {
480
- path = resolveWorkspaceModule(workspace, `${name}/package.json`);
481
- } catch (error) {
477
+ const resolved = attempt(() => resolveWorkspaceModule(workspace, `${name}/package.json`));
478
+ if (!resolved.success) {
479
+ const error = resolved.error;
482
480
  if (!isProbeError(error)) throw error;
483
481
  throw new ProbeError(`${name} does not publish a readable manifest`, {
484
482
  origin: "workspace",
@@ -487,6 +485,7 @@ function readWorkspaceManifest(workspace, name) {
487
485
  cause: error.cause
488
486
  });
489
487
  }
488
+ const path = resolved.value;
490
489
  const reading = attempt(() => readFileSync(path, "utf8"));
491
490
  if (!reading.success) {
492
491
  const error = reading.error;
@@ -502,17 +501,17 @@ function readWorkspaceManifest(workspace, name) {
502
501
  cause: error
503
502
  });
504
503
  }
505
- const parsing = attempt(() => JSON.parse(reading.value));
506
- if (!parsing.success) throw new ProbeError(`${name} does not publish a readable manifest`, {
504
+ const parsed = attempt(() => JSON.parse(reading.value));
505
+ if (!parsed.success) throw new ProbeError(`${name} does not publish a readable manifest`, {
507
506
  origin: "workspace",
508
507
  code: "malformed",
509
508
  context: {
510
509
  name,
511
510
  path
512
511
  },
513
- cause: parsing.error
512
+ cause: parsed.error
514
513
  });
515
- const contents = parsing.value;
514
+ const contents = parsed.value;
516
515
  if (!isRecord(contents)) throw new ProbeError(`${name} does not publish a readable manifest`, {
517
516
  origin: "workspace",
518
517
  code: "malformed",
@@ -532,6 +531,8 @@ function readWorkspaceManifest(workspace, name) {
532
531
  * @remarks
533
532
  * A package publishes its binaries under keys of its own choosing, and that key is often not the
534
533
  * package name: `typescript` publishes `tsc`. Name the key as `command` wherever the two differ.
534
+ * Resolution reads `package.json` and its `bin` field, so TypeScript 6 and 7 need no exported
535
+ * `typescript/bin/tsc` subpath.
535
536
  * Default: the package name.
536
537
  *
537
538
  * @param workspace - The target workspace root
@@ -681,20 +682,13 @@ function buildRevisionPath(workspace, path, revision) {
681
682
  * ```
682
683
  */
683
684
  function filterUniqueIssues(issues) {
684
- const seen = /* @__PURE__ */ new Set();
685
- const distinct = [];
686
- for (const issue of issues) {
687
- const key = JSON.stringify([
688
- issue.origin,
689
- issue.path,
690
- issue.message,
691
- issue.range
692
- ]);
693
- if (seen.has(key)) continue;
694
- seen.add(key);
695
- distinct.push(issue);
696
- }
697
- return distinct;
685
+ const groups = Map.groupBy(issues, (issue) => JSON.stringify([
686
+ issue.origin,
687
+ issue.path,
688
+ issue.message,
689
+ issue.range
690
+ ]));
691
+ return Iterator.from(groups.values()).flatMap((group) => group.slice(0, 1)).toArray();
698
692
  }
699
693
  /**
700
694
  * Reports whether a path is a workspace module Vitest can cache.
@@ -712,13 +706,11 @@ function matchesWorkspaceModule(path) {
712
706
  return /\.(?:[cm]?[jt]sx?|vue|json)$/.test(path);
713
707
  }
714
708
  /**
715
- * Reports whether the host that wrote one file is still running.
709
+ * Reports whether a file's owning host is running or cannot be judged.
716
710
  *
717
711
  * @remarks
718
- * Signal 0 delivers nothing and reports reachability alone. A process this one may not signal
719
- * reports `EPERM` and reads as running, and a non-positive identity names a process group rather
720
- * than a process, so both answer true: the safe direction is to keep a file this package cannot
721
- * account for.
712
+ * Keeps a file when its identity is invalid or `inspectProcess` reports existence, denied
713
+ * permission, or an indeterminate host failure. Only a confirmed absent process answers false.
722
714
  *
723
715
  * @param id - The process id a file's own revision names
724
716
  * @returns True if that process is running or cannot be judged; false otherwise
@@ -731,12 +723,7 @@ function matchesWorkspaceModule(path) {
731
723
  */
732
724
  function matchesLiveProcess(id) {
733
725
  if (!isNumber(id) || !Number.isSafeInteger(id) || id <= 0) return true;
734
- try {
735
- process.kill(id, 0);
736
- return true;
737
- } catch (error) {
738
- return readFaultCode(error) === "EPERM";
739
- }
726
+ return inspectProcess(id) !== false;
740
727
  }
741
728
  /**
742
729
  * Collects every regular file a target workspace holds, skipping the trees no inspection reads.
@@ -764,10 +751,8 @@ function collectWorkspaceFiles(workspace) {
764
751
  while (directories.length > 0) {
765
752
  const directory = directories.pop();
766
753
  if (directory === void 0) break;
767
- let entries = [];
768
- try {
769
- entries = readdirSync(directory, { withFileTypes: true });
770
- } catch {}
754
+ const reading = attempt(() => readdirSync(directory, { withFileTypes: true }));
755
+ const entries = reading.success ? reading.value : [];
771
756
  for (const entry of entries) {
772
757
  if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
773
758
  const path = join(directory, entry.name);
@@ -856,17 +841,10 @@ async function guardStage(stage, operation) {
856
841
  */
857
842
  function findRefusedPaths(value) {
858
843
  if (!compileGuard(CLAIM_SHAPE)(value)) return [];
859
- const members = [];
860
- for (const phase of ["case", "control"]) {
844
+ return ["case", "control"].flatMap((phase) => {
861
845
  const subject = value[phase];
862
- const drafts = /* @__PURE__ */ new Map([[`${phase}.test`, subject.test]]);
863
- for (const [index, file] of subject.files.entries()) drafts.set(`${phase}.files.${index}`, file);
864
- for (const [member, draft] of drafts) {
865
- if (isDraft(draft)) continue;
866
- members.push(`${member}.path`);
867
- }
868
- }
869
- return members;
846
+ return [[`${phase}.test`, subject.test], ...Iterator.from(subject.files.entries()).map(([index, file]) => [`${phase}.files.${index}`, file])].flatMap(([member, draft]) => isDraft(draft) ? [] : [`${member}.path`]);
847
+ });
870
848
  }
871
849
  /**
872
850
  * Rewrites every workspace-contained absolute path in a value to its workspace-relative form and
@@ -878,7 +856,8 @@ function findRefusedPaths(value) {
878
856
  * its relative form keeps one commit checked out at two roots reading to one digest. A path that
879
857
  * escapes the workspace is left as it stands, because rewriting it would name a file outside the
880
858
  * tree by a relative spelling. Key order is the compiler's own and it differs between the supported
881
- * majors, so sorting removes a difference that means nothing.
859
+ * majors, so sorting removes a difference that means nothing. A `__proto__` key is dropped, as
860
+ * the earlier assignment-built record dropped it, so a digest stays the same across releases.
882
861
  *
883
862
  * @param workspace - The target workspace root
884
863
  * @param value - The value to rewrite
@@ -900,11 +879,7 @@ function normalizeValue(workspace, value) {
900
879
  return normalizePath(path);
901
880
  }
902
881
  if (isArray(value)) return value.map((entry) => normalizeValue(workspace, entry));
903
- if (isRecord(value)) {
904
- const record = {};
905
- for (const key of Object.keys(value).sort()) record[key] = normalizeValue(workspace, value[key]);
906
- return record;
907
- }
882
+ if (isRecord(value)) return Object.fromEntries(Object.keys(value).sort().filter((key) => key !== "__proto__").map((key) => [key, normalizeValue(workspace, value[key])]));
908
883
  return value;
909
884
  }
910
885
  /**
@@ -941,9 +916,7 @@ function computeDigest(workspace, value) {
941
916
  * ```
942
917
  */
943
918
  function captureListeners(emitter, events) {
944
- const capture = /* @__PURE__ */ new Map();
945
- for (const event of events) capture.set(event, emitter.listeners(event));
946
- return capture;
919
+ return new Map(Iterator.from(events).map((event) => [event, emitter.listeners(event)]));
947
920
  }
948
921
  /**
949
922
  * Removes every listener one emitter gained for the captured events since its capture.
@@ -974,12 +947,11 @@ function captureListeners(emitter, events) {
974
947
  */
975
948
  function releaseListeners(emitter, capture) {
976
949
  for (const [event, captured] of capture) for (const listener of emitter.listeners(event)) {
977
- if (captured.includes(listener)) continue;
950
+ if (captured.includes(listener) || !isFunction(listener)) continue;
978
951
  emitter.removeListener(event, listener);
979
952
  }
980
953
  }
981
- //#endregion
982
- //#region src/server/parsers.ts
954
+ // src/server/parsers.ts
983
955
  /**
984
956
  * Parses the configuration one compiler run printed for a TypeScript project.
985
957
  *
@@ -1034,8 +1006,7 @@ function parseRevisionOwner(revision) {
1034
1006
  const owner = /^(\d+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u.exec(revision)?.[1];
1035
1007
  return owner === void 0 ? void 0 : Number.parseInt(owner, 10);
1036
1008
  }
1037
- //#endregion
1038
- //#region src/server/Overlay.ts
1009
+ // src/server/Overlay.ts
1039
1010
  /**
1040
1011
  * Implements `OverlayInterface` over a private map from normalized absolute path to candidate text,
1041
1012
  * minting at construction the `revision` a resident tool caches its answers against.
@@ -1121,15 +1092,13 @@ var Overlay = class {
1121
1092
  this.#candidates.clear();
1122
1093
  }
1123
1094
  };
1124
- //#endregion
1125
- //#region package.json
1126
- var version = "0.0.24";
1095
+ // package.json
1096
+ var version = "0.0.26";
1127
1097
  var peerDependencies = {
1128
- "typescript": "^6.0.3",
1129
- "vitest": "^4.1.12"
1098
+ "typescript": "^6.0.3 || ^7.0.2",
1099
+ "vitest": "^4.1.12 || ^5.0.5"
1130
1100
  };
1131
- //#endregion
1132
- //#region src/server/stages/LintStage.ts
1101
+ // src/server/stages/LintStage.ts
1133
1102
  /**
1134
1103
  * Inspects virtual documents through one resident Oxlint language server.
1135
1104
  *
@@ -1342,12 +1311,16 @@ var LintStage = class {
1342
1311
  }));
1343
1312
  }
1344
1313
  };
1345
- //#endregion
1346
- //#region src/server/stages/RuntimeStage.ts
1314
+ // src/server/stages/RuntimeStage.ts
1347
1315
  /**
1348
1316
  * Inspects tests through one resident Vitest service from the target workspace.
1349
1317
  *
1350
1318
  * @remarks
1319
+ * Vitest 4.1.12 and 5.0.5 share the mode-first `createVitest` overload used by this stage.
1320
+ * Result eviction removes one file from Vitest 4's results cache. Vitest 5 exposes only a full
1321
+ * cache clear, so eviction clears its results, dependency, and filesystem module caches.
1322
+ * Warming removes termination handlers registered within the factory's async context before
1323
+ * signal delivery, while retaining handlers the host registers outside that context.
1351
1324
  * Starting launches Vitest with the threads pool and instruments each inline or function-declared
1352
1325
  * project with a Vite plugin that reads the active inspection's candidate overlay. A selected
1353
1326
  * string-declared project reports a workspace issue because its configuration carries no runtime
@@ -1501,14 +1474,13 @@ var RuntimeStage = class {
1501
1474
  const file = sep === "\\" ? normalizePath(specification) : specification;
1502
1475
  this.#specifications += 1;
1503
1476
  this.#revisions.add(file);
1504
- let issues = [];
1505
- let cleanup = [];
1477
+ const issues = [];
1506
1478
  const task = project.createSpecification(file, void 0, "threads");
1507
1479
  this.#progress += 1;
1508
1480
  try {
1509
1481
  try {
1510
1482
  const result = await vitest.runTestSpecifications([task], false);
1511
- issues = [...this.#issues(result, file, subject.test.path), ...this.#misses(project, file)];
1483
+ issues.push(...this.#issues(result, file, subject.test.path), ...this.#misses(project, file));
1512
1484
  } catch (error) {
1513
1485
  if (isProbeError(error)) throw error;
1514
1486
  throw new ProbeError(`The runtime stage could not run the generated specification (${describeUnknown(error)})`, {
@@ -1525,22 +1497,22 @@ var RuntimeStage = class {
1525
1497
  this.#progress -= 1;
1526
1498
  process.exitCode = exitCode;
1527
1499
  this.#revisions.delete(file);
1528
- cleanup = await this.#evict(vitest, file);
1500
+ issues.push(...await this.#evict(vitest, file));
1529
1501
  try {
1530
1502
  const contained = resolveWorkspaceFile(this.#workspace, relative(this.#workspace, file), true);
1531
1503
  if (existsSync(contained)) unlinkSync(contained);
1532
1504
  } catch (error) {
1533
- cleanup = [...cleanup, {
1505
+ issues.push({
1534
1506
  origin: isProbeError(error) && error.origin === "workspace" ? "workspace" : "instrument",
1535
1507
  path: relativeWorkspaceFile(this.#workspace, file),
1536
1508
  message: `The runtime stage could not delete the generated specification (${describeUnknown(error)})`
1537
- }];
1509
+ });
1538
1510
  }
1539
1511
  }
1540
1512
  return {
1541
1513
  stage: this.stage,
1542
1514
  elapsed: Math.round(performance.now() - started),
1543
- issues: [...issues, ...cleanup]
1515
+ issues
1544
1516
  };
1545
1517
  } finally {
1546
1518
  this.#loads.clear();
@@ -1572,38 +1544,46 @@ var RuntimeStage = class {
1572
1544
  this.#sweep();
1573
1545
  const output = new PassThrough();
1574
1546
  output.resume();
1547
+ const scope = new AsyncLocalStorage();
1548
+ const listener = this.#captureSignal.bind(this, scope);
1575
1549
  const signals = captureListeners(process, ["SIGINT", "SIGTERM"]);
1576
- let warming;
1550
+ process.on("newListener", listener);
1577
1551
  try {
1578
- warming = factory("test", {
1579
- root: this.#workspace,
1580
- config: resolveWorkspaceFile(this.#workspace, "vite.config.ts"),
1581
- watch: false,
1582
- run: true,
1583
- pool: "threads",
1584
- reporters: [{
1585
- onInit() {},
1586
- onTestRunEnd() {}
1587
- }]
1588
- }, { plugins: [{
1589
- name: "orkestrel-project-instrumentation",
1590
- enforce: "post",
1591
- config: this.#configure.bind(this)
1592
- }] }, {
1593
- stdout: output,
1594
- stderr: process.stderr
1552
+ return await scope.run(true, () => {
1553
+ try {
1554
+ return factory("test", {
1555
+ root: this.#workspace,
1556
+ config: resolveWorkspaceFile(this.#workspace, "vite.config.ts"),
1557
+ watch: false,
1558
+ run: true,
1559
+ pool: "threads",
1560
+ reporters: [{
1561
+ onInit() {},
1562
+ onTestRunEnd() {}
1563
+ }]
1564
+ }, { plugins: [{
1565
+ name: "orkestrel-project-instrumentation",
1566
+ enforce: "post",
1567
+ config: this.#configure.bind(this)
1568
+ }] }, {
1569
+ stdout: output,
1570
+ stderr: process.stderr
1571
+ });
1572
+ } finally {
1573
+ releaseListeners(process, signals);
1574
+ }
1595
1575
  });
1596
1576
  } catch (error) {
1597
1577
  throw this.#configuration(error);
1598
1578
  } finally {
1599
- releaseListeners(process, signals);
1600
- }
1601
- try {
1602
- return await warming;
1603
- } catch (error) {
1604
- throw this.#configuration(error);
1579
+ process.off("newListener", listener);
1580
+ scope.disable();
1605
1581
  }
1606
1582
  }
1583
+ #captureSignal(scope, event, listener) {
1584
+ if (scope.getStore() !== true || event !== "SIGINT" && event !== "SIGTERM") return;
1585
+ queueMicrotask(() => process.off(event, listener));
1586
+ }
1607
1587
  #configure(config) {
1608
1588
  const test = config.test;
1609
1589
  if (test?.projects === void 0) return;
@@ -1659,47 +1639,52 @@ var RuntimeStage = class {
1659
1639
  return text;
1660
1640
  }
1661
1641
  #specification(test) {
1662
- let creating = false;
1663
- let generated;
1664
- const outcome = attempt(() => {
1642
+ const location = attempt(() => {
1665
1643
  const revision = `${process.pid}-${randomUUID()}`;
1666
- const file = buildRevisionPath(this.#workspace, test.path, revision);
1667
- generated = file;
1668
- const target = relative(this.#workspace, file);
1669
- resolveWorkspaceFile(this.#workspace, target, true);
1670
- creating = true;
1671
- mkdirSync(dirname(file), { recursive: true });
1672
- creating = false;
1644
+ return {
1645
+ revision,
1646
+ file: buildRevisionPath(this.#workspace, test.path, revision)
1647
+ };
1648
+ });
1649
+ if (!location.success) return this.#specificationFault(test, location.error);
1650
+ const { revision, file } = location.value;
1651
+ const target = relative(this.#workspace, file);
1652
+ const containment = attempt(() => resolveWorkspaceFile(this.#workspace, target, true));
1653
+ if (!containment.success) return this.#specificationFault(test, containment.error, file);
1654
+ const directory = attempt(() => mkdirSync(dirname(file), { recursive: true }));
1655
+ if (!directory.success) return this.#specificationFault(test, directory.error, file, true);
1656
+ const written = attempt(() => {
1673
1657
  resolveWorkspaceFile(this.#workspace, target, true);
1674
1658
  writeFileSync(file, formatSpecification(test.text, revision), {
1675
1659
  encoding: "utf8",
1676
1660
  flag: "wx"
1677
1661
  });
1678
- return file;
1679
1662
  });
1680
- if (outcome.success) return outcome.value;
1681
- if (isProbeError(outcome.error) && outcome.error.origin === "claimant") throw new ProbeError(outcome.error.message, {
1682
- origin: outcome.error.origin,
1683
- code: outcome.error.code,
1663
+ return written.success ? file : this.#specificationFault(test, written.error, file);
1664
+ }
1665
+ #specificationFault(test, error, generated, directory = false) {
1666
+ if (isProbeError(error) && error.origin === "claimant") throw new ProbeError(error.message, {
1667
+ origin: error.origin,
1668
+ code: error.code,
1684
1669
  context: {
1685
- ...outcome.error.context,
1670
+ ...error.context,
1686
1671
  path: test.path
1687
1672
  },
1688
- cause: outcome.error.cause
1673
+ cause: error.cause
1689
1674
  });
1690
- if (!creating && generated !== void 0 && isRefusedName(generated, outcome.error)) throw new ProbeError(`The runtime stage cannot write a specification beside a test path the host refuses (${describeUnknown(outcome.error)})`, {
1675
+ if (!directory && generated !== void 0 && isRefusedName(generated, error)) throw new ProbeError(`The runtime stage cannot write a specification beside a test path the host refuses (${describeUnknown(error)})`, {
1691
1676
  origin: "claimant",
1692
1677
  code: "refused",
1693
1678
  context: {
1694
1679
  stage: this.stage,
1695
1680
  path: test.path
1696
1681
  },
1697
- cause: outcome.error
1682
+ cause: error
1698
1683
  });
1699
1684
  return {
1700
- origin: isProbeError(outcome.error) && outcome.error.origin === "workspace" || creating ? "workspace" : "instrument",
1685
+ origin: isProbeError(error) && error.origin === "workspace" || directory ? "workspace" : "instrument",
1701
1686
  path: test.path,
1702
- message: `The runtime stage could not write the generated specification (${describeUnknown(outcome.error)})`
1687
+ message: `The runtime stage could not write the generated specification (${describeUnknown(error)})`
1703
1688
  };
1704
1689
  }
1705
1690
  #project(vitest, path) {
@@ -1730,44 +1715,35 @@ var RuntimeStage = class {
1730
1715
  }
1731
1716
  #misses(project, specification) {
1732
1717
  const file = normalizePath(specification);
1733
- const issues = [];
1734
- for (const path of this.#overlay.paths) {
1735
- if (this.#reads.has(path)) continue;
1736
- let served = false;
1737
- let loaded = false;
1738
- for (const environment of Object.values(project.vite.environments)) {
1718
+ return this.#overlay.paths.flatMap((path) => {
1719
+ if (this.#reads.has(path)) return [];
1720
+ const served = Object.values(project.vite.environments).flatMap((environment) => {
1739
1721
  const modules = environment.moduleGraph.getModulesByFile(path);
1740
- if (modules === void 0) continue;
1741
- for (const module of modules) {
1722
+ if (modules === void 0) return [];
1723
+ return Iterator.from(modules).flatMap((module) => {
1742
1724
  const pending = [...module.importers];
1743
1725
  const seen = new Set(pending);
1744
- let reaches = false;
1745
1726
  while (pending.length > 0) {
1746
1727
  const importer = pending.pop();
1747
1728
  if (importer === void 0) break;
1748
- if (importer.file !== null && normalizePath(importer.file) === file) {
1749
- reaches = true;
1750
- break;
1751
- }
1729
+ if (importer.file !== null && normalizePath(importer.file) === file) return [module.id !== null && this.#loads.has(normalizePath(module.id))];
1752
1730
  for (const parent of importer.importers) {
1753
1731
  if (seen.has(parent)) continue;
1754
1732
  seen.add(parent);
1755
1733
  pending.push(parent);
1756
1734
  }
1757
1735
  }
1758
- if (!reaches) continue;
1759
- served = true;
1760
- if (module.id !== null && this.#loads.has(normalizePath(module.id))) loaded = true;
1761
- }
1762
- }
1763
- if (!served) continue;
1764
- issues.push({
1736
+ return [];
1737
+ }).toArray();
1738
+ });
1739
+ if (served.length === 0) return [];
1740
+ const loaded = served.some((value) => value);
1741
+ return [{
1765
1742
  origin: loaded ? "instrument" : "workspace",
1766
1743
  path: relativeWorkspaceFile(this.#workspace, path),
1767
1744
  message: loaded ? "The runtime overlay did not resolve this module" : "The workspace configuration served this module before the runtime overlay"
1768
- });
1769
- }
1770
- return issues;
1745
+ }];
1746
+ });
1771
1747
  }
1772
1748
  async #evict(vitest, file) {
1773
1749
  try {
@@ -1797,8 +1773,16 @@ var RuntimeStage = class {
1797
1773
  }
1798
1774
  vitest.watcher.onFileDelete(file);
1799
1775
  vitest.watcher.invalidates.delete(file);
1800
- vitest.cache.results.removeFromCache(normalizePath(relative(this.#workspace, file)));
1801
- await vitest.cache.results.writeToCache();
1776
+ const results = "results" in vitest.cache ? vitest.cache.results : void 0;
1777
+ if (isObject(results) && "removeFromCache" in results && typeof results.removeFromCache === "function" && "writeToCache" in results && typeof results.writeToCache === "function") {
1778
+ results.removeFromCache(normalizePath(relative(this.#workspace, file)));
1779
+ await results.writeToCache();
1780
+ } else if ("clearCache" in vitest && typeof vitest.clearCache === "function") await vitest.clearCache();
1781
+ else throw new ProbeError("Vitest exposes no cache eviction API", {
1782
+ origin: "instrument",
1783
+ code: "malformed",
1784
+ context: { stage: this.stage }
1785
+ });
1802
1786
  return [];
1803
1787
  } catch (error) {
1804
1788
  return [{
@@ -2001,12 +1985,13 @@ var RuntimeStage = class {
2001
1985
  };
2002
1986
  }
2003
1987
  };
2004
- //#endregion
2005
- //#region src/server/stages/TypeStage.ts
1988
+ // src/server/stages/TypeStage.ts
2006
1989
  /**
2007
1990
  * Inspects TypeScript source by running the target workspace's own compiler over a mirror of it.
2008
1991
  *
2009
1992
  * @remarks
1993
+ * TypeScript 6 and 7 run through the `tsc` entry in their package manifest; neither a compiler API
1994
+ * nor an exported binary subpath is required.
2010
1995
  * The compiler is a process this stage spawns rather than a module it loads, so nothing here holds
2011
1996
  * a resident program and the host's event loop is free for the whole check. Starting sweeps a
2012
1997
  * mirror an earlier host left behind, copies the workspace into a fresh one under
@@ -2044,6 +2029,7 @@ var TypeStage = class {
2044
2029
  #revision = `${process.pid}-${randomUUID()}`;
2045
2030
  #mirror;
2046
2031
  #configs = /* @__PURE__ */ new Map();
2032
+ #validated = /* @__PURE__ */ new Set();
2047
2033
  #mirrored = /* @__PURE__ */ new Map();
2048
2034
  #drafts = /* @__PURE__ */ new Set();
2049
2035
  #children = /* @__PURE__ */ new Set();
@@ -2097,6 +2083,8 @@ var TypeStage = class {
2097
2083
  * refreshed from the workspace first on a cache miss, so the digest names the configuration the
2098
2084
  * check applies. The reading is cached per project for the life of the stage, so a draft written
2099
2085
  * afterward cannot move it.
2086
+ * Before returning the identity, validates the project with `--noEmit` unless an unoverlaid check
2087
+ * already validated it. TypeScript 7's `--showConfig` can succeed for a malformed project.
2100
2088
  *
2101
2089
  * @param project - The workspace-relative TypeScript project to resolve
2102
2090
  * @returns The resolved workspace-relative path and the digest of its compiler options
@@ -2106,6 +2094,14 @@ var TypeStage = class {
2106
2094
  resolve(project) {
2107
2095
  return guardStage(this.stage, this.#resolve(project));
2108
2096
  }
2097
+ /**
2098
+ * Tears down the compiler processes and releases the mirror without awaiting inspections.
2099
+ *
2100
+ * @remarks
2101
+ * On Windows, awaits each tree termination attempt before removing the mirror. Each attempt
2102
+ * uses `PROBE_DEADLINE`, the existing default inspection budget, as its utility timeout.
2103
+ * On POSIX, sends `SIGTERM` to each compiler. Warming settles before the mirror is removed.
2104
+ */
2109
2105
  destroy() {
2110
2106
  if (this.#closing !== void 0) return this.#closing;
2111
2107
  this.#closing = guardStage(this.stage, this.#destroy());
@@ -2156,6 +2152,10 @@ var TypeStage = class {
2156
2152
  const contained = this.#contain(project);
2157
2153
  this.#progress += 1;
2158
2154
  const config = await this.#configure(contained);
2155
+ if (!this.#validated.has(contained)) {
2156
+ this.#refresh();
2157
+ await this.#check(contained, []);
2158
+ }
2159
2159
  return {
2160
2160
  path: contained,
2161
2161
  digest: computeDigest(this.#workspace, config.compilerOptions)
@@ -2164,7 +2164,7 @@ var TypeStage = class {
2164
2164
  async #destroy() {
2165
2165
  const children = [...this.#children];
2166
2166
  this.#children.clear();
2167
- for (const child of children) this.#terminate(child);
2167
+ await Promise.all(children.map((child) => this.#terminate(child)));
2168
2168
  await this.#warming?.catch(() => void 0);
2169
2169
  try {
2170
2170
  rmSync(this.#mirror, {
@@ -2175,6 +2175,7 @@ var TypeStage = class {
2175
2175
  });
2176
2176
  } catch {}
2177
2177
  this.#configs.clear();
2178
+ this.#validated.clear();
2178
2179
  this.#mirrored.clear();
2179
2180
  this.#drafts.clear();
2180
2181
  }
@@ -2187,18 +2188,11 @@ var TypeStage = class {
2187
2188
  await Promise.all(this.#projects().map((project) => this.#check(project, [])));
2188
2189
  }
2189
2190
  #projects() {
2190
- const projects = ["tsconfig.json"];
2191
- for (const axis of ["src", "app"]) {
2191
+ return ["tsconfig.json", ...["src", "app"].flatMap((axis) => {
2192
2192
  const directory = resolveWorkspaceFile(this.#workspace, `configs/${axis}`);
2193
- let names;
2194
- try {
2195
- names = readdirSync(directory);
2196
- } catch {
2197
- continue;
2198
- }
2199
- for (const name of names.filter((entry) => /^tsconfig\.[^.]+\.json$/.test(entry)).sort()) projects.push(`configs/${axis}/${name}`);
2200
- }
2201
- return projects;
2193
+ const reading = attempt(() => readdirSync(directory));
2194
+ return reading.success ? reading.value.filter((entry) => /^tsconfig\.[^.]+\.json$/.test(entry)).sort().map((name) => `configs/${axis}/${name}`) : [];
2195
+ })];
2202
2196
  }
2203
2197
  #refuseDestroyed() {
2204
2198
  if (this.#closing !== void 0) throw createDestroyedError("type stage");
@@ -2213,13 +2207,9 @@ var TypeStage = class {
2213
2207
  }
2214
2208
  #sweep() {
2215
2209
  const root = resolveWorkspaceFile(this.#workspace, TYPE_MIRROR);
2216
- let entries = [];
2217
- try {
2218
- entries = readdirSync(root, { withFileTypes: true });
2219
- } catch {
2220
- return;
2221
- }
2222
- for (const entry of entries) {
2210
+ const reading = attempt(() => readdirSync(root, { withFileTypes: true }));
2211
+ if (!reading.success) return;
2212
+ for (const entry of reading.value) {
2223
2213
  if (!entry.isDirectory()) continue;
2224
2214
  const owner = parseRevisionOwner(entry.name);
2225
2215
  if (owner === void 0 || matchesLiveProcess(owner)) continue;
@@ -2273,12 +2263,7 @@ var TypeStage = class {
2273
2263
  return contained;
2274
2264
  }
2275
2265
  #displace(target) {
2276
- const ancestors = [];
2277
- let directory = dirname(target);
2278
- while (directory !== this.#mirror && directory !== dirname(directory)) {
2279
- ancestors.unshift(directory);
2280
- directory = dirname(directory);
2281
- }
2266
+ const ancestors = relative(this.#mirror, target).split(sep).slice(0, -1).map((_segment, index, segments) => join(this.#mirror, ...segments.slice(0, index + 1)));
2282
2267
  for (const ancestor of [...ancestors, target]) {
2283
2268
  const reading = attempt(() => lstatSync(ancestor));
2284
2269
  if (!reading.success) continue;
@@ -2348,7 +2333,9 @@ var TypeStage = class {
2348
2333
  }
2349
2334
  });
2350
2335
  }
2351
- return this.#issues(diagnostics, project);
2336
+ const issues = this.#issues(diagnostics, project, scratch);
2337
+ if (this.#drafts.size === 0) this.#validated.add(this.#contain(project));
2338
+ return issues;
2352
2339
  }
2353
2340
  #scratch(project, config, drafts) {
2354
2341
  const directory = dirname(project);
@@ -2373,13 +2360,13 @@ var TypeStage = class {
2373
2360
  writeFileSync(target, `${JSON.stringify(body, void 0, " ")}\n`, "utf8");
2374
2361
  return contained;
2375
2362
  }
2376
- #issues(diagnostics, project) {
2363
+ #issues(diagnostics, project, scratch) {
2377
2364
  const issues = [];
2378
2365
  for (const diagnostic of diagnostics) {
2379
2366
  const message = this.#translate(diagnostic.message);
2380
2367
  const resolved = diagnostic.path === void 0 ? void 0 : resolve(this.#mirror, diagnostic.path);
2381
2368
  const drafted = resolved !== void 0 && this.#drafts.has(escapesRoot(this.#mirror, resolved) ? relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved));
2382
- if (resolved === void 0 || !drafted && extname(resolved) === ".json") throw new ProbeError(message, {
2369
+ if (resolved === void 0 || !drafted && extname(resolved) === ".json") throw new ProbeError(message.replaceAll(normalizePath(scratch), normalizePath(project)), {
2383
2370
  origin: "workspace",
2384
2371
  code: "malformed",
2385
2372
  context: {
@@ -2412,40 +2399,43 @@ var TypeStage = class {
2412
2399
  }
2413
2400
  });
2414
2401
  }
2415
- #spawn(args, cwd) {
2402
+ async #spawn(args, cwd) {
2416
2403
  this.#refuseDestroyed();
2417
2404
  const compiler = this.#compiler;
2418
2405
  if (compiler === void 0) throw createDestroyedError("type stage");
2419
- return new Promise((settle, refuse) => {
2420
- const child = spawn(process.execPath, [compiler, ...args], {
2421
- cwd,
2422
- stdio: [
2423
- "ignore",
2424
- "pipe",
2425
- "pipe"
2426
- ]
2427
- });
2428
- const output = child.stdout;
2429
- const errors = child.stderr;
2430
- if (output === null || errors === null) {
2431
- this.#terminate(child);
2432
- refuse(new ProbeError("The compiler was spawned without its own output streams", {
2433
- origin: "instrument",
2434
- code: "malformed",
2435
- context: { stage: this.stage }
2436
- }));
2437
- return;
2406
+ const child = spawn(process.execPath, [compiler, ...args], {
2407
+ cwd,
2408
+ stdio: [
2409
+ "ignore",
2410
+ "pipe",
2411
+ "pipe"
2412
+ ]
2413
+ });
2414
+ this.#children.add(child);
2415
+ const output = child.stdout;
2416
+ const errors = child.stderr;
2417
+ if (output === null || errors === null) {
2418
+ try {
2419
+ await this.#terminate(child);
2420
+ } finally {
2421
+ this.#children.delete(child);
2438
2422
  }
2439
- this.#children.add(child);
2440
- let stdout = "";
2441
- let stderr = "";
2423
+ throw new ProbeError("The compiler was spawned without its own output streams", {
2424
+ origin: "instrument",
2425
+ code: "malformed",
2426
+ context: { stage: this.stage }
2427
+ });
2428
+ }
2429
+ return new Promise((settle, refuse) => {
2430
+ const stdout = [];
2431
+ const stderr = [];
2442
2432
  output.setEncoding("utf8");
2443
2433
  errors.setEncoding("utf8");
2444
2434
  output.on("data", (chunk) => {
2445
- stdout += chunk;
2435
+ stdout.push(chunk);
2446
2436
  });
2447
2437
  errors.on("data", (chunk) => {
2448
- stderr += chunk;
2438
+ stderr.push(chunk);
2449
2439
  });
2450
2440
  child.on("error", (error) => {
2451
2441
  this.#children.delete(child);
@@ -2463,32 +2453,28 @@ var TypeStage = class {
2463
2453
  this.#children.delete(child);
2464
2454
  settle({
2465
2455
  ...code === null ? {} : { status: code },
2466
- stdout,
2467
- stderr
2456
+ stdout: stdout.join(""),
2457
+ stderr: stderr.join("")
2468
2458
  });
2469
2459
  });
2470
2460
  });
2471
2461
  }
2472
- #terminate(child) {
2462
+ async #terminate(child) {
2473
2463
  const id = child.pid;
2474
2464
  if (process.platform === "win32" && id !== void 0) {
2475
- spawnSync("taskkill", [
2476
- "/pid",
2477
- String(id),
2478
- "/t",
2479
- "/f"
2480
- ], { stdio: "ignore" });
2465
+ await killTree(id, PROBE_DEADLINE);
2481
2466
  return;
2482
2467
  }
2483
2468
  child.kill("SIGTERM");
2484
2469
  }
2485
2470
  };
2486
- //#endregion
2487
- //#region src/server/Probe.ts
2471
+ // src/server/Probe.ts
2488
2472
  /**
2489
2473
  * Answers claims through its type, lint, and runtime stages.
2490
2474
  *
2491
2475
  * @remarks
2476
+ * The workspace peers are TypeScript `^6.0.3 || ^7.0.2` and Vitest `^4.1.12 || ^5.0.5`.
2477
+ * Starting refuses TypeScript majors outside 6 and 7 before a stage starts.
2492
2478
  * Construction resolves the target workspace's toolchain. Starting warms every stage and waits
2493
2479
  * for lint and runtime; type warming and the boot controls continue behind that gate. The boot
2494
2480
  * controls mutate imported dependencies and refuse service unless the type and runtime stages
@@ -2537,7 +2523,7 @@ var TypeStage = class {
2537
2523
  * const probe = new Probe({ workspace: process.cwd() })
2538
2524
  * const verdict = await probe.prove(claim)
2539
2525
  * verdict.digest // 'bdf03e5dfd6bd413ead671c7a2940fcf'
2540
- * verdict.receipt // 'probe:bdf03e5dfd6bd413ead671c7a2940fcf:type:typescript@6.0.3:oxlint@1.89.0:vitest@4.1.12:configs/src/tsconfig.core.json@434f59254d58cf2683d453a26bd0d837'
2526
+ * verdict.receipt?.startsWith(`probe:${verdict.digest}:type:`) // true
2541
2527
  * await probe.destroy()
2542
2528
  * ```
2543
2529
  */
@@ -2672,11 +2658,12 @@ var Probe = class {
2672
2658
  return this.#starting;
2673
2659
  }
2674
2660
  async #start(filling, warming) {
2675
- let created = false;
2676
- try {
2661
+ const preparation = attempt(() => {
2677
2662
  this.#support();
2678
- if (this.#arming === void 0) created = this.#workbench();
2679
- } catch (error) {
2663
+ return this.#arming === void 0 && this.#workbench();
2664
+ });
2665
+ if (!preparation.success) {
2666
+ const error = preparation.error;
2680
2667
  this.#surface(error);
2681
2668
  throw error;
2682
2669
  }
@@ -2687,7 +2674,7 @@ var Probe = class {
2687
2674
  throw this.#refuseArm(error);
2688
2675
  }
2689
2676
  if (this.#arming === void 0) {
2690
- this.#arming = this.#arm(warming, created);
2677
+ this.#arming = this.#arm(warming, preparation.value);
2691
2678
  this.#arming.catch(() => {
2692
2679
  if (this.#refusal === void 0) this.#arming = void 0;
2693
2680
  });
@@ -3013,9 +3000,9 @@ var Probe = class {
3013
3000
  #support() {
3014
3001
  const version = this.#toolchain.typescript;
3015
3002
  const range = peerDependencies.typescript;
3016
- const supported = /^\^(\d+)\./u.exec(range)?.[1];
3017
3003
  const found = /^(\d+)\./u.exec(version)?.[1];
3018
- if (supported === void 0 || found !== supported) throw new ProbeError(`The supported TypeScript range is ${range}; found ${version}`, {
3004
+ const supported = range.split(" || ").some((entry) => /^\^(\d+)\./u.exec(entry)?.[1] === found);
3005
+ if (found === void 0 || !supported) throw new ProbeError(`The supported TypeScript range is ${range}; found ${version}`, {
3019
3006
  origin: "workspace",
3020
3007
  code: "malformed",
3021
3008
  context: {
@@ -3025,8 +3012,7 @@ var Probe = class {
3025
3012
  });
3026
3013
  }
3027
3014
  };
3028
- //#endregion
3029
- //#region src/server/ProbeServer.ts
3015
+ // src/server/ProbeServer.ts
3030
3016
  /**
3031
3017
  * Implements `ProbeServerInterface` over a `PassThrough` stream this server owns, binding the
3032
3018
  * published `prove` tool and the dual-era dispatcher to this process's Model Context Protocol
@@ -3265,7 +3251,6 @@ var ProbeServer = class {
3265
3251
  ] : [rendered, minimal]).find((arm) => isBoundedJSON(arm, bounds)) ?? minimal;
3266
3252
  }
3267
3253
  };
3268
- //#endregion
3269
3254
  export { LintStage, Overlay, Probe, ProbeServer, RuntimeStage, TypeStage, buildRevisionPath, captureListeners, collectWorkspaceFiles, computeDigest, createHandshakeError, describeUnknown, escapesRoot, filterUniqueIssues, findRefusedPaths, guardStage, inferDocumentLanguage, inferTestProject, inferTypeProject, isRefusedName, loadWorkspaceVitest, matchesLiveProcess, matchesWorkspaceModule, normalizePath, normalizeValue, overwriteFile, parseProjectConfig, parseRevisionOwner, readFaultCode, readWorkspaceManifest, relativeWorkspaceFile, relativeWorkspaceMessage, releaseListeners, resolveWorkspaceBinary, resolveWorkspaceFile, resolveWorkspaceModule, scanDiagnostics };
3270
3255
 
3271
3256
  //# sourceMappingURL=index.js.map