@orkestrel/probe 0.0.3 → 0.0.5

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.
@@ -3,13 +3,13 @@ let node_fs = require("node:fs");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_module = require("node:module");
5
5
  let node_path = require("node:path");
6
+ let node_url = require("node:url");
6
7
  let _orkestrel_contract = require("@orkestrel/contract");
7
8
  let _src_core = require("../core/index.cjs");
8
9
  let _orkestrel_emitter = require("@orkestrel/emitter");
9
10
  let _orkestrel_queue = require("@orkestrel/queue");
10
11
  let _orkestrel_timeout = require("@orkestrel/timeout");
11
12
  let node_child_process = require("node:child_process");
12
- let node_url = require("node:url");
13
13
  let node_stream = require("node:stream");
14
14
  let node_timers_promises = require("node:timers/promises");
15
15
  let _orkestrel_mcp = require("@orkestrel/mcp");
@@ -107,6 +107,46 @@ function resolveWorkspaceFile(workspace, target, mutate = false) {
107
107
  return file;
108
108
  }
109
109
  /**
110
+ * Overwrites a file that already exists, through a descriptor that refuses a symbolic link at the
111
+ * final component.
112
+ *
113
+ * @remarks
114
+ * A containment walk and the write that follows it are separate calls, so a process that swaps the
115
+ * final component between them decides where the bytes land. `writeFileSync` opens with the default
116
+ * `w` flag, which follows a symbolic link at that component and creates the file when it is absent,
117
+ * so the walk's reading does not bind the write. Opening `O_WRONLY | O_TRUNC | O_NOFOLLOW` moves
118
+ * that decision into the call that writes: a link at the final component refuses the open, and the
119
+ * omitted `O_CREAT` fails `ENOENT` for a target that has gone since the walk saw it. This is the
120
+ * mutating counterpart of the `wx` flag every create in this package uses. Which code the refusal
121
+ * carries is the host's; Linux with Node v22.22.2 reported `ELOOP` on 2026-08-24.
122
+ *
123
+ * A directory component stays open, because Node exposes no descriptor-relative call to walk and
124
+ * write through one set of descriptors. Where a host's Node build defines no `O_NOFOLLOW`, that flag
125
+ * contributes nothing to the flag set and the final component is open on that host too.
126
+ *
127
+ * @param file - The absolute path to overwrite
128
+ * @param text - The text to write
129
+ * @returns Nothing
130
+ * @throws The host's own fault when the final component is a symbolic link, when the file is
131
+ * absent, or when the host refuses the descriptor
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * const file = resolveWorkspaceFile('/srv/checkout', 'tmp/probe/arm-type.ts', true)
136
+ * writeFileSync(file, 'export type Signal = string\n', { encoding: 'utf8', flag: 'wx' })
137
+ * overwriteFile(file, 'export type Signal = number\n')
138
+ * readFileSync(file, 'utf8') // 'export type Signal = number\n'
139
+ * ```
140
+ */
141
+ function overwriteFile(file, text) {
142
+ const descriptor = (0, node_fs.openSync)(file, node_fs.constants.O_WRONLY | node_fs.constants.O_TRUNC | node_fs.constants.O_NOFOLLOW);
143
+ try {
144
+ (0, node_fs.writeFileSync)(descriptor, text, "utf8");
145
+ } finally {
146
+ (0, node_fs.closeSync)(descriptor);
147
+ }
148
+ }
149
+ /**
110
150
  * Reports whether a fault means the host refuses the name a caller supplied for a file to create.
111
151
  *
112
152
  * @remarks
@@ -170,6 +210,60 @@ function relativeWorkspaceFile(workspace, file) {
170
210
  return normalizePath((0, node_path.relative)((0, node_path.resolve)(workspace), (0, node_path.resolve)(file)));
171
211
  }
172
212
  /**
213
+ * Projects the paths one tool named in a message into the forms this package's issues expose.
214
+ *
215
+ * @remarks
216
+ * An issue's `path` is workspace-relative, and its message is prose a tool wrote about the same
217
+ * tree. Left alone that prose names a file by the host's own layout, so one issue reports a path two
218
+ * ways and a reader on another checkout is sent to a directory that is not theirs. Each spelling of
219
+ * the root is removed: the absolute path, the backslash spelling a Windows tool writes, and the
220
+ * `file:` URL a runtime names a module by. A root the host itself names ends in a separator, and the
221
+ * paths beneath it continue straight from it rather than from one more separator. A path outside the
222
+ * workspace stands as it is, because a relative spelling of it would name a file the tree does not
223
+ * hold.
224
+ *
225
+ * A spelling of the root counts only where a path begins: at the start of the message, or after a
226
+ * character no path carries. A directory whose own name ends in this root's text holds a different
227
+ * tree, so the root inside a longer path names a file this workspace does not hold and that path
228
+ * survives whole.
229
+ *
230
+ * Only the root is removed. A name is otherwise the tool's, including a `.probe-` name that reads
231
+ * like a specification this package generates: the stage that wrote one knows the exact name it
232
+ * wrote and renames that name itself, which is the only reading that separates its own file from a
233
+ * file the target tree owns.
234
+ *
235
+ * Everything else the tool wrote survives verbatim, separators included. A message carries text
236
+ * that is not a path — a runtime escapes a newline as a backslash when it prints a string inline,
237
+ * and a compiler quotes a string literal with its escapes intact — so rewriting every separator
238
+ * would corrupt the evidence the message exists to carry. The cost is that the path left behind
239
+ * keeps the separator the tool chose, which on Windows is not the one `Issue.path` uses.
240
+ *
241
+ * @param workspace - The target workspace root
242
+ * @param message - The message a tool reported
243
+ * @returns The message with the host's own layout removed from every path that begins with it
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * relativeWorkspaceMessage('/srv/checkout', 'Cannot read /srv/checkout/tsconfig.json')
248
+ * // 'Cannot read tsconfig.json'
249
+ * relativeWorkspaceMessage('/srv/checkout', 'Cannot read /mirror/srv/checkout/tsconfig.json')
250
+ * // 'Cannot read /mirror/srv/checkout/tsconfig.json'
251
+ * ```
252
+ */
253
+ function relativeWorkspaceMessage(workspace, message) {
254
+ const root = (0, node_path.resolve)(workspace);
255
+ const forward = normalizePath(root);
256
+ const url = (0, node_url.pathToFileURL)(forward).href;
257
+ const spellings = /* @__PURE__ */ new Set([
258
+ url.endsWith("/") ? url : `${url}/`,
259
+ forward.endsWith("/") ? forward : `${forward}/`,
260
+ root.endsWith(node_path.sep) ? root : `${root}${node_path.sep}`
261
+ ]);
262
+ let text = message;
263
+ for (const spelling of spellings) text = text.replaceAll(spelling, (match, offset, source) => /[^\s"'`()[\]{}<>,;:=|]/u.test(source.charAt(offset - 1)) ? match : "");
264
+ return text;
265
+ }
266
+ /**
173
267
  * Resolves one installed module from the target workspace.
174
268
  *
175
269
  * @param workspace - The target workspace root
@@ -457,6 +551,32 @@ function describeUnknown(value) {
457
551
  return String(value);
458
552
  }
459
553
  /**
554
+ * Guards one resident-stage operation with the stage failure contract.
555
+ *
556
+ * @param stage - The resident stage serving the operation
557
+ * @param operation - The operation to settle
558
+ * @returns The operation's fulfilled value
559
+ * @throws The original `ProbeError`, or an instrument-owned malformed failure retaining the cause
560
+ *
561
+ * @example
562
+ * ```ts
563
+ * await guardStage('type', stage.inspect(subject))
564
+ * ```
565
+ */
566
+ async function guardStage(stage, operation) {
567
+ try {
568
+ return await operation;
569
+ } catch (error) {
570
+ if (error instanceof _src_core.ProbeError) throw error;
571
+ throw new _src_core.ProbeError(`The ${stage} stage could not serve (${describeUnknown(error)})`, {
572
+ origin: "instrument",
573
+ code: "malformed",
574
+ context: { stage },
575
+ cause: error
576
+ });
577
+ }
578
+ }
579
+ /**
460
580
  * Names every draft member of a claim-shaped value whose `path` this package's guard refuses.
461
581
  *
462
582
  * @remarks
@@ -686,11 +806,11 @@ var Overlay = class {
686
806
  };
687
807
  //#endregion
688
808
  //#region package.json
689
- var version = "0.0.3";
809
+ var version = "0.0.5";
690
810
  var peerDependencies = {
691
- "oxlint": "^1.79.0",
692
- "typescript": "^6.0.3",
693
- "vitest": "^4.1.11"
811
+ "oxlint": "^1.77.0",
812
+ "typescript": "^6.0.0",
813
+ "vitest": "^4.1.0"
694
814
  };
695
815
  //#endregion
696
816
  //#region src/server/stages/LintStage.ts
@@ -752,7 +872,16 @@ var LintStage = class {
752
872
  get progress() {
753
873
  return this.#progress;
754
874
  }
755
- async inspect(subject) {
875
+ inspect(subject) {
876
+ return guardStage(this.stage, this.#inspect(subject));
877
+ }
878
+ destroy() {
879
+ if (this.#closing !== void 0) return this.#closing;
880
+ this.#destroyed = true;
881
+ this.#closing = guardStage(this.stage, this.#destroy());
882
+ return this.#closing;
883
+ }
884
+ async #inspect(subject) {
756
885
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("lint stage");
757
886
  const started = performance.now();
758
887
  await this.#warmth;
@@ -765,12 +894,6 @@ var LintStage = class {
765
894
  issues
766
895
  };
767
896
  }
768
- destroy() {
769
- if (this.#closing !== void 0) return this.#closing;
770
- this.#destroyed = true;
771
- this.#closing = this.#destroy();
772
- return this.#closing;
773
- }
774
897
  async #destroy() {
775
898
  const child = await this.#warmed();
776
899
  this.#fail((0, _src_core.createDestroyedError)("lint stage"));
@@ -1058,6 +1181,13 @@ var LintStage = class {
1058
1181
  * workspace module whose disk content or candidate revision changed, runs that specification,
1059
1182
  * evicts its result, and deletes the file. Clearing the overlay makes the next snapshot differ from
1060
1183
  * the candidate revision, so the next inspection invalidates that module and reads disk again.
1184
+ * After the run, the stage checks each covered module reachable from the generated specification
1185
+ * against the overlay paths its loader served. A target-config loader that served first produces a
1186
+ * workspace issue; a module id probe's loader received but failed to match produces an instrument
1187
+ * issue.
1188
+ * The runtime overlay leaves bare specifier resolution to Vite: it replaces a bare import only
1189
+ * after Vite resolves that import to a covered file. The type stage overlays the declared test
1190
+ * path, while this stage executes a generated sibling and overlays only the candidate files.
1061
1191
  *
1062
1192
  * Vite retains one unresolved URL for every specification path, so the stage replaces its whole
1063
1193
  * Vitest service after 64 specifications rather than deleting from each map that service owns. Any
@@ -1081,9 +1211,12 @@ var LintStage = class {
1081
1211
  * test path the host refuses to create is a claimant failure with `code: 'refused'`. Each is thrown
1082
1212
  * rather than reported, because a test that never ran is no evidence about the candidate.
1083
1213
  *
1084
- * The write path's physical-containment guarantee covers the claim inputs and the target tree as
1085
- * inspected. A concurrent process that mutates a path component between the final inspection and
1086
- * the write is outside that guarantee.
1214
+ * Physical containment covers the claim inputs, the target tree as the walk inspected it, and the
1215
+ * final component at the moment this stage writes or unlinks it: a specification is created with the
1216
+ * `wx` flag, which fails rather than following a symbolic link that appeared after the walk, and an
1217
+ * unlink names the final component itself rather than what it points at. A directory component
1218
+ * swapped for a symbolic link between the walk and the write stays open, because Node exposes no
1219
+ * descriptor-relative call to pin the walk and the write to one set of descriptors.
1087
1220
  *
1088
1221
  * @example
1089
1222
  * ```ts
@@ -1096,6 +1229,8 @@ var RuntimeStage = class {
1096
1229
  #workspace;
1097
1230
  #vitest;
1098
1231
  #overlay = new Overlay();
1232
+ #loads = /* @__PURE__ */ new Set();
1233
+ #reads = /* @__PURE__ */ new Set();
1099
1234
  #modules = /* @__PURE__ */ new Map();
1100
1235
  #revisions = /* @__PURE__ */ new Set();
1101
1236
  #specifications = 0;
@@ -1110,8 +1245,7 @@ var RuntimeStage = class {
1110
1245
  constructor(workspace = process.cwd()) {
1111
1246
  this.#workspace = workspace;
1112
1247
  const vitest = loadWorkspaceModule(this.#workspace, "vitest/node");
1113
- this.#vitest = this.#warm(vitest.createVitest);
1114
- this.#vitest.catch(() => {});
1248
+ this.#store(this.#warm(vitest.createVitest));
1115
1249
  }
1116
1250
  get stage() {
1117
1251
  return "runtime";
@@ -1119,7 +1253,16 @@ var RuntimeStage = class {
1119
1253
  get progress() {
1120
1254
  return this.#progress;
1121
1255
  }
1122
- async inspect(subject) {
1256
+ inspect(subject) {
1257
+ return guardStage(this.stage, this.#inspect(subject));
1258
+ }
1259
+ destroy() {
1260
+ if (this.#closing !== void 0) return this.#closing;
1261
+ this.#destroyed = true;
1262
+ this.#closing = guardStage(this.stage, this.#destroy());
1263
+ return this.#closing;
1264
+ }
1265
+ async #inspect(subject) {
1123
1266
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("runtime stage");
1124
1267
  const started = performance.now();
1125
1268
  const exitCode = process.exitCode;
@@ -1133,6 +1276,8 @@ var RuntimeStage = class {
1133
1276
  };
1134
1277
  const overlay = new Overlay();
1135
1278
  this.#overlay = overlay;
1279
+ this.#loads.clear();
1280
+ this.#reads.clear();
1136
1281
  try {
1137
1282
  for (const draft of subject.files) overlay.set(resolveWorkspaceFile(this.#workspace, draft.path), draft.text);
1138
1283
  this.#revalidate(vitest);
@@ -1152,7 +1297,7 @@ var RuntimeStage = class {
1152
1297
  try {
1153
1298
  try {
1154
1299
  const result = await vitest.runTestSpecifications([task], false);
1155
- issues = this.#issues(result, file, subject.test.path);
1300
+ issues = [...this.#issues(result, file, subject.test.path), ...this.#misses(project, file)];
1156
1301
  } catch (error) {
1157
1302
  if (error instanceof _src_core.ProbeError) throw error;
1158
1303
  throw new _src_core.ProbeError(`The runtime stage could not run the generated specification (${describeUnknown(error)})`, {
@@ -1187,15 +1332,11 @@ var RuntimeStage = class {
1187
1332
  issues: [...issues, ...cleanup]
1188
1333
  };
1189
1334
  } finally {
1335
+ this.#loads.clear();
1336
+ this.#reads.clear();
1190
1337
  overlay.clear();
1191
1338
  }
1192
1339
  }
1193
- destroy() {
1194
- if (this.#closing !== void 0) return this.#closing;
1195
- this.#destroyed = true;
1196
- this.#closing = this.#destroy();
1197
- return this.#closing;
1198
- }
1199
1340
  async #destroy() {
1200
1341
  try {
1201
1342
  for (const file of this.#revisions) try {
@@ -1203,13 +1344,15 @@ var RuntimeStage = class {
1203
1344
  if ((0, node_fs.existsSync)(contained)) (0, node_fs.unlinkSync)(contained);
1204
1345
  } catch {}
1205
1346
  this.#revisions.clear();
1206
- const vitest = await this.#vitest.catch(() => void 0);
1347
+ const vitest = await this.#vitest?.catch(() => void 0);
1207
1348
  if (vitest !== void 0) {
1208
1349
  vitest.cancelCurrentRun("keyboard-input").catch(() => {});
1209
1350
  await vitest.close();
1210
1351
  }
1211
1352
  } finally {
1212
1353
  this.#modules.clear();
1354
+ this.#loads.clear();
1355
+ this.#reads.clear();
1213
1356
  this.#overlay.clear();
1214
1357
  }
1215
1358
  }
@@ -1218,8 +1361,9 @@ var RuntimeStage = class {
1218
1361
  const output = new node_stream.PassThrough();
1219
1362
  output.resume();
1220
1363
  const signals = captureListeners(process, ["SIGINT", "SIGTERM"]);
1364
+ let warming;
1221
1365
  try {
1222
- return create("test", {
1366
+ warming = create("test", {
1223
1367
  root: this.#workspace,
1224
1368
  config: resolveWorkspaceFile(this.#workspace, "vite.config.ts"),
1225
1369
  watch: false,
@@ -1237,9 +1381,16 @@ var RuntimeStage = class {
1237
1381
  stdout: output,
1238
1382
  stderr: process.stderr
1239
1383
  });
1384
+ } catch (error) {
1385
+ throw this.#configuration(error);
1240
1386
  } finally {
1241
1387
  releaseListeners(process, signals);
1242
1388
  }
1389
+ try {
1390
+ return await warming;
1391
+ } catch (error) {
1392
+ throw this.#configuration(error);
1393
+ }
1243
1394
  }
1244
1395
  #configure(config) {
1245
1396
  const test = config.test;
@@ -1266,7 +1417,7 @@ var RuntimeStage = class {
1266
1417
  };
1267
1418
  }
1268
1419
  #resolve(id, importer) {
1269
- const separator = id.lastIndexOf("?");
1420
+ const separator = id.indexOf("?");
1270
1421
  const specifier = separator === -1 ? id : id.slice(0, separator);
1271
1422
  const suffix = separator === -1 ? "" : id.slice(separator);
1272
1423
  if (!specifier.startsWith(".") && !(0, node_path.isAbsolute)(specifier)) return void 0;
@@ -1278,17 +1429,22 @@ var RuntimeStage = class {
1278
1429
  if (extension === "") candidates.push(`${imported}.ts`);
1279
1430
  for (const candidate of candidates) {
1280
1431
  const resolved = `${candidate}${suffix}`;
1281
- if (this.#load(resolved) !== void 0) return resolved;
1432
+ if (this.#overlay.text(candidate) !== void 0) return resolved;
1282
1433
  }
1283
1434
  }
1284
1435
  #load(id) {
1285
- const text = this.#overlay.text(id);
1286
- if (text !== void 0) return text;
1287
- const separator = id.lastIndexOf("?");
1288
- if (separator === -1) return void 0;
1289
- const query = id.slice(separator + 1);
1290
- if (!query.startsWith("v=") || query.length === 2 || query.includes("&") || query.includes("#") || query.slice(2).includes("?")) return;
1291
- return this.#overlay.text(id.slice(0, separator));
1436
+ this.#loads.add(normalizePath(id));
1437
+ const separator = id.indexOf("?");
1438
+ const path = separator === -1 ? id : id.slice(0, separator);
1439
+ const text = this.#overlay.text(path);
1440
+ if (text === void 0) return void 0;
1441
+ this.#reads.add(normalizePath(path));
1442
+ if (separator !== -1 && /(?:^|&)raw(?:&|$)/u.test(id.slice(separator + 1))) return {
1443
+ code: `export default ${JSON.stringify(text)}`,
1444
+ map: { mappings: "" },
1445
+ moduleType: "js"
1446
+ };
1447
+ return text;
1292
1448
  }
1293
1449
  #specification(test) {
1294
1450
  let creating = false;
@@ -1360,6 +1516,47 @@ var RuntimeStage = class {
1360
1516
  };
1361
1517
  return project;
1362
1518
  }
1519
+ #misses(project, specification) {
1520
+ const file = normalizePath(specification);
1521
+ const issues = [];
1522
+ for (const path of this.#overlay.paths) {
1523
+ if (this.#reads.has(path)) continue;
1524
+ let served = false;
1525
+ let loaded = false;
1526
+ for (const environment of Object.values(project.vite.environments)) {
1527
+ const modules = environment.moduleGraph.getModulesByFile(path);
1528
+ if (modules === void 0) continue;
1529
+ for (const module of modules) {
1530
+ const pending = [...module.importers];
1531
+ const seen = new Set(pending);
1532
+ let reaches = false;
1533
+ while (pending.length > 0) {
1534
+ const importer = pending.pop();
1535
+ if (importer === void 0) break;
1536
+ if (importer.file !== null && normalizePath(importer.file) === file) {
1537
+ reaches = true;
1538
+ break;
1539
+ }
1540
+ for (const parent of importer.importers) {
1541
+ if (seen.has(parent)) continue;
1542
+ seen.add(parent);
1543
+ pending.push(parent);
1544
+ }
1545
+ }
1546
+ if (!reaches) continue;
1547
+ served = true;
1548
+ if (module.id !== null && this.#loads.has(normalizePath(module.id))) loaded = true;
1549
+ }
1550
+ }
1551
+ if (!served) continue;
1552
+ issues.push({
1553
+ origin: loaded ? "instrument" : "workspace",
1554
+ path: relativeWorkspaceFile(this.#workspace, path),
1555
+ message: loaded ? "The runtime overlay did not resolve this module" : "The workspace configuration served this module before the runtime overlay"
1556
+ });
1557
+ }
1558
+ return issues;
1559
+ }
1363
1560
  async #evict(vitest, file) {
1364
1561
  try {
1365
1562
  const ids = [];
@@ -1400,17 +1597,38 @@ var RuntimeStage = class {
1400
1597
  }
1401
1598
  }
1402
1599
  #runner() {
1403
- if (this.#specifications < 64) return this.#vitest;
1404
- this.#specifications = 0;
1405
- this.#vitest = this.#replace(this.#vitest);
1406
- this.#vitest.catch(() => {});
1407
- return this.#vitest;
1600
+ const current = this.#vitest;
1601
+ if (current !== void 0 && this.#specifications < 64) return current;
1602
+ if (current !== void 0) return this.#store(this.#replace(current));
1603
+ const runner = loadWorkspaceModule(this.#workspace, "vitest/node");
1604
+ return this.#store(this.#specifications < 64 ? this.#warm(runner.createVitest) : this.#replace(void 0, runner.createVitest));
1408
1605
  }
1409
- async #replace(current) {
1410
- await (await current).close();
1606
+ async #replace(current, create) {
1607
+ if (current !== void 0) await (await current).close();
1411
1608
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("runtime stage");
1412
- const runner = loadWorkspaceModule(this.#workspace, "vitest/node");
1413
- return this.#warm(runner.createVitest);
1609
+ const runner = create ?? loadWorkspaceModule(this.#workspace, "vitest/node").createVitest;
1610
+ const replacement = await this.#warm(runner);
1611
+ this.#specifications = 0;
1612
+ return replacement;
1613
+ }
1614
+ #store(vitest) {
1615
+ this.#vitest = vitest;
1616
+ vitest.catch(() => {
1617
+ if (this.#vitest === vitest) this.#vitest = void 0;
1618
+ });
1619
+ return vitest;
1620
+ }
1621
+ #configuration(error) {
1622
+ if ((0, _src_core.isProbeError)(error)) return error;
1623
+ return new _src_core.ProbeError(`The runtime stage could not load vite.config.ts (${describeUnknown(error)})`, {
1624
+ origin: "workspace",
1625
+ code: "malformed",
1626
+ context: {
1627
+ stage: this.stage,
1628
+ path: "vite.config.ts"
1629
+ },
1630
+ cause: error
1631
+ });
1414
1632
  }
1415
1633
  #revalidate(vitest) {
1416
1634
  const modules = this.#snapshot();
@@ -1555,7 +1773,7 @@ var RuntimeStage = class {
1555
1773
  return outcome.success ? outcome.value : path;
1556
1774
  }
1557
1775
  #issue(error, specification, original) {
1558
- const message = describeUnknown(error);
1776
+ const message = relativeWorkspaceMessage(this.#workspace, describeUnknown(error)).replaceAll((0, node_path.basename)(specification), (0, node_path.basename)(original));
1559
1777
  if (typeof error !== "object" || error === null || !("stacks" in error)) return {
1560
1778
  origin: "claimant",
1561
1779
  path: original,
@@ -1662,7 +1880,33 @@ var TypeStage = class {
1662
1880
  * @returns One outcome for this stage
1663
1881
  * @throws When the resident compiler cannot start or the stage has already been destroyed
1664
1882
  */
1665
- async inspect(subject, project) {
1883
+ inspect(subject, project) {
1884
+ return guardStage(this.stage, this.#inspect(subject, project));
1885
+ }
1886
+ /**
1887
+ * Resolves one project to the path and digest this stage applies for it.
1888
+ *
1889
+ * @remarks
1890
+ * Reads the parse this stage itself applies, filling its cache when the project is not already
1891
+ * resident, so the reported digest is the configuration the inspection is judged under rather
1892
+ * than a second parse a caller ran. The returned record is a value copy, so a later eviction
1893
+ * does not move it.
1894
+ *
1895
+ * @param project - The workspace-relative TypeScript project to resolve
1896
+ * @returns The resolved workspace-relative path and the digest of its compiler options
1897
+ * @throws When the project escapes the workspace, cannot be parsed, or the stage has already
1898
+ * been destroyed
1899
+ */
1900
+ resolve(project) {
1901
+ return guardStage(this.stage, this.#resolve(project));
1902
+ }
1903
+ destroy() {
1904
+ if (this.#closing !== void 0) return this.#closing;
1905
+ this.#destroyed = true;
1906
+ this.#closing = guardStage(this.stage, this.#destroy());
1907
+ return this.#closing;
1908
+ }
1909
+ async #inspect(subject, project) {
1666
1910
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("type stage");
1667
1911
  const started = performance.now();
1668
1912
  const typescript = await this.#typescript;
@@ -1674,8 +1918,13 @@ var TypeStage = class {
1674
1918
  draft: candidate.draft,
1675
1919
  project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
1676
1920
  }));
1677
- this.#configure(this.#service(typescript, "tsconfig.json"), "tsconfig.json");
1678
- for (const selection of selections) this.#configure(this.#service(typescript, selection.project), selection.project);
1921
+ const root = this.#service(typescript, "tsconfig.json");
1922
+ this.#configure(root, "tsconfig.json");
1923
+ await this.#unblock();
1924
+ for (const selection of selections) {
1925
+ this.#configure(this.#service(typescript, selection.project), selection.project);
1926
+ await this.#unblock();
1927
+ }
1679
1928
  const overlay = new Overlay();
1680
1929
  this.#overlay = overlay;
1681
1930
  try {
@@ -1684,7 +1933,6 @@ var TypeStage = class {
1684
1933
  this.#progress += 1;
1685
1934
  const issues = [];
1686
1935
  const projects = /* @__PURE__ */ new Set();
1687
- const root = this.#service(typescript, "tsconfig.json");
1688
1936
  issues.push(...this.#issues(typescript, root, subject.test, "tsconfig.json", false, true));
1689
1937
  projects.add("tsconfig.json");
1690
1938
  await this.#unblock();
@@ -1705,37 +1953,19 @@ var TypeStage = class {
1705
1953
  overlay.clear();
1706
1954
  }
1707
1955
  }
1708
- /**
1709
- * Resolves one project to the path and digest this stage applies for it.
1710
- *
1711
- * @remarks
1712
- * Reads the parse this stage itself applies, filling its cache when the project is not already
1713
- * resident, so the reported digest is the configuration the inspection is judged under rather
1714
- * than a second parse a caller ran. The returned record is a value copy, so a later eviction
1715
- * does not move it.
1716
- *
1717
- * @param project - The workspace-relative TypeScript project to resolve
1718
- * @returns The resolved workspace-relative path and the digest of its compiler options
1719
- * @throws When the project escapes the workspace, cannot be parsed, or the stage has already
1720
- * been destroyed
1721
- */
1722
- async resolve(project) {
1956
+ async #resolve(project) {
1723
1957
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("type stage");
1724
1958
  const typescript = await this.#typescript;
1725
1959
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("type stage");
1726
1960
  const resolved = resolveWorkspaceFile(this.#workspace, project);
1961
+ this.#progress += 1;
1727
1962
  this.#service(typescript, project);
1963
+ await this.#unblock();
1728
1964
  return {
1729
1965
  path: relativeWorkspaceFile(this.#workspace, resolved),
1730
1966
  digest: computeDigest(this.#workspace, this.#options.get(resolved) ?? {})
1731
1967
  };
1732
1968
  }
1733
- destroy() {
1734
- if (this.#closing !== void 0) return this.#closing;
1735
- this.#destroyed = true;
1736
- this.#closing = this.#destroy();
1737
- return this.#closing;
1738
- }
1739
1969
  async #destroy() {
1740
1970
  await this.#typescript.catch(() => void 0);
1741
1971
  for (const service of this.#services.values()) service.dispose();
@@ -1768,7 +1998,7 @@ var TypeStage = class {
1768
1998
  return projects;
1769
1999
  }
1770
2000
  async #unblock() {
1771
- await (0, node_timers_promises.setImmediate)();
2001
+ await (0, node_timers_promises.setTimeout)(0);
1772
2002
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("type stage");
1773
2003
  }
1774
2004
  #record(draft, overlay) {
@@ -1780,7 +2010,7 @@ var TypeStage = class {
1780
2010
  if (existing !== void 0) return existing;
1781
2011
  const spelling = normalizePath(path);
1782
2012
  const config = typescript.readConfigFile(spelling, typescript.sys.readFile);
1783
- if (config.error !== void 0) throw new _src_core.ProbeError(this.#translate(typescript, config.error.messageText, path), {
2013
+ if (config.error !== void 0) throw new _src_core.ProbeError(this.#translate(typescript, config.error.messageText), {
1784
2014
  origin: "workspace",
1785
2015
  code: "malformed",
1786
2016
  context: {
@@ -1789,7 +2019,7 @@ var TypeStage = class {
1789
2019
  }
1790
2020
  });
1791
2021
  const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, (0, node_path.dirname)(spelling), void 0, spelling);
1792
- if (parsed.errors.length > 0) throw new _src_core.ProbeError(this.#translate(typescript, parsed.errors[0]?.messageText, path), {
2022
+ if (parsed.errors.length > 0) throw new _src_core.ProbeError(this.#translate(typescript, parsed.errors[0]?.messageText), {
1793
2023
  origin: "workspace",
1794
2024
  code: "malformed",
1795
2025
  context: {
@@ -1846,8 +2076,8 @@ var TypeStage = class {
1846
2076
  #newline(typescript) {
1847
2077
  return typescript.sys.newLine;
1848
2078
  }
1849
- #translate(typescript, message, path) {
1850
- return normalizePath(typescript.flattenDiagnosticMessageText(message, "\n")).replaceAll(normalizePath(path), relativeWorkspaceFile(this.#workspace, path));
2079
+ #translate(typescript, message) {
2080
+ return relativeWorkspaceMessage(this.#workspace, typescript.flattenDiagnosticMessageText(message, "\n"));
1851
2081
  }
1852
2082
  #configure(service, project) {
1853
2083
  const path = resolveWorkspaceFile(this.#workspace, project);
@@ -1886,7 +2116,7 @@ var TypeStage = class {
1886
2116
  ].map((diagnostic) => this.#issue(typescript, diagnostic, project, selected));
1887
2117
  }
1888
2118
  #issue(typescript, diagnostic, project, selected) {
1889
- const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
2119
+ const message = this.#translate(typescript, diagnostic.messageText);
1890
2120
  if (diagnostic.file === void 0) {
1891
2121
  if (selected) throw new _src_core.ProbeError(message, {
1892
2122
  origin: "claimant",
@@ -1897,7 +2127,7 @@ var TypeStage = class {
1897
2127
  }
1898
2128
  });
1899
2129
  return {
1900
- origin: "instrument",
2130
+ origin: "workspace",
1901
2131
  path: project,
1902
2132
  message
1903
2133
  };
@@ -1949,6 +2179,8 @@ var Probe = class {
1949
2179
  #typeQueue;
1950
2180
  #lintQueue;
1951
2181
  #runtimeQueue;
2182
+ #deadlines = /* @__PURE__ */ new WeakSet();
2183
+ #typeTail = Promise.resolve();
1952
2184
  #arming;
1953
2185
  #closing;
1954
2186
  #destroyed = false;
@@ -1999,11 +2231,12 @@ var Probe = class {
1999
2231
  async prove(claim) {
2000
2232
  try {
2001
2233
  this.#support();
2234
+ this.#admit(claim);
2002
2235
  await this.#ready();
2003
2236
  if (this.#destroyed) throw (0, _src_core.createDestroyedError)("probe");
2004
2237
  const started = performance.now();
2005
2238
  const id = (0, node_crypto.randomUUID)();
2006
- const project = await this.#type.resolve(claim.project);
2239
+ const project = await this.#resolve(claim);
2007
2240
  const digest = computeDigest(this.#workspace, {
2008
2241
  case: claim.case,
2009
2242
  control: claim.control
@@ -2141,7 +2374,7 @@ var Probe = class {
2141
2374
  code: "malformed"
2142
2375
  });
2143
2376
  resolveWorkspaceFile(this.#workspace, (0, node_path.relative)(this.#workspace, typeDependency), true);
2144
- (0, node_fs.writeFileSync)(typeDependency, (0, _src_core.formatSpecification)("export type Signal = number\n", revision), "utf8");
2377
+ overwriteFile(typeDependency, (0, _src_core.formatSpecification)("export type Signal = number\n", revision));
2145
2378
  const afterType = await this.#inspect(typeClaim.control, typeClaim);
2146
2379
  const type = afterType.find((check) => check.stage === typeClaim.control.stage);
2147
2380
  const tolerant = afterType.find((check) => check.stage === "runtime");
@@ -2156,7 +2389,7 @@ var Probe = class {
2156
2389
  context: { stage: "runtime" }
2157
2390
  });
2158
2391
  resolveWorkspaceFile(this.#workspace, (0, node_path.relative)(this.#workspace, runtimeDependency), true);
2159
- (0, node_fs.writeFileSync)(runtimeDependency, (0, _src_core.formatSpecification)("export const SIGNAL = 'after'\n", revision), "utf8");
2392
+ overwriteFile(runtimeDependency, (0, _src_core.formatSpecification)("export const SIGNAL = 'after'\n", revision));
2160
2393
  const afterRuntime = await this.#inspect(runtimeClaim.control, runtimeClaim);
2161
2394
  const runtime = afterRuntime.find((check) => check.stage === runtimeClaim.control.stage);
2162
2395
  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")}`, {
@@ -2185,8 +2418,14 @@ var Probe = class {
2185
2418
  for (const pending of admitted) pending.catch(() => {});
2186
2419
  return Promise.all(admitted);
2187
2420
  }
2188
- #inspectType(inspection) {
2189
- return this.#inspectStage(this.#type, this.#type.progress, this.#type.inspect(inspection.subject, inspection.claim.project), inspection.claim);
2421
+ async #inspectType(inspection) {
2422
+ const release = await this.#admitType();
2423
+ try {
2424
+ const stage = this.#type;
2425
+ return await this.#inspectStage(stage, stage.progress, stage.inspect(inspection.subject, inspection.claim.project), inspection.claim);
2426
+ } finally {
2427
+ release();
2428
+ }
2190
2429
  }
2191
2430
  #inspectLint(inspection) {
2192
2431
  return this.#inspectStage(this.#lint, this.#lint.progress, this.#lint.inspect(inspection.subject), inspection.claim);
@@ -2195,19 +2434,28 @@ var Probe = class {
2195
2434
  return this.#inspectStage(this.#runtime, this.#runtime.progress, this.#runtime.inspect(inspection.subject), inspection.claim);
2196
2435
  }
2197
2436
  async #inspectStage(stage, progress, operation, claim) {
2437
+ try {
2438
+ return await this.#bound(operation, `The ${stage.stage} stage exceeded ${this.#deadline} ms`, stage, progress);
2439
+ } catch (error) {
2440
+ this.#emitExpiry(stage, claim);
2441
+ throw error;
2442
+ }
2443
+ }
2444
+ async #bound(operation, message, stage, progress) {
2198
2445
  const timeout = (0, _orkestrel_timeout.createTimeout)({ ms: this.#deadline });
2199
2446
  timeout.start();
2200
2447
  try {
2201
- return await Promise.race([operation, this.#expiry(timeout, `The ${stage.stage} stage exceeded ${this.#deadline} ms`, stage, progress)]);
2448
+ return await Promise.race([operation, this.#expiry(timeout, message, stage, progress)]);
2202
2449
  } catch (error) {
2203
2450
  if (!timeout.expired) throw error;
2204
- if (await this.#recycle(stage)) this.#emitter.emit("expire", claim);
2451
+ await this.#recycle(stage);
2205
2452
  throw error;
2206
2453
  } finally {
2207
2454
  timeout.clear();
2208
2455
  }
2209
2456
  }
2210
2457
  async #recycle(stage) {
2458
+ if (this.#destroyed) return false;
2211
2459
  const timeout = (0, _orkestrel_timeout.createTimeout)({ ms: this.#deadline });
2212
2460
  timeout.start();
2213
2461
  try {
@@ -2230,16 +2478,45 @@ var Probe = class {
2230
2478
  }
2231
2479
  return false;
2232
2480
  }
2481
+ async #admitType() {
2482
+ const previous = this.#typeTail;
2483
+ const turn = Promise.withResolvers();
2484
+ this.#typeTail = turn.promise;
2485
+ await previous;
2486
+ return turn.resolve;
2487
+ }
2488
+ async #resolve(claim) {
2489
+ const release = await this.#admitType();
2490
+ try {
2491
+ const stage = this.#type;
2492
+ try {
2493
+ return await this.#bound(stage.resolve(claim.project), `The type stage project resolution exceeded ${this.#deadline} ms`, stage, stage.progress);
2494
+ } catch (error) {
2495
+ this.#emitExpiry(stage, claim);
2496
+ throw error;
2497
+ }
2498
+ } finally {
2499
+ release();
2500
+ }
2501
+ }
2502
+ #emitExpiry(stage, claim) {
2503
+ if (stage === this.#type || stage === this.#lint || stage === this.#runtime) return;
2504
+ this.#emitter.emit("expire", claim);
2505
+ }
2233
2506
  #expiry(timeout, message, stage, progress) {
2234
2507
  return new Promise((_resolve, reject) => {
2235
- timeout.signal.addEventListener("abort", () => reject(new _src_core.ProbeError(message, {
2236
- origin: stage.progress > progress ? "claimant" : "instrument",
2237
- code: "deadline",
2238
- context: {
2239
- stage: stage.stage,
2240
- deadline: this.#deadline
2241
- }
2242
- })), { once: true });
2508
+ timeout.signal.addEventListener("abort", () => {
2509
+ const error = new _src_core.ProbeError(message, {
2510
+ origin: stage.progress > progress ? "claimant" : "instrument",
2511
+ code: "deadline",
2512
+ context: {
2513
+ stage: stage.stage,
2514
+ deadline: this.#deadline
2515
+ }
2516
+ });
2517
+ this.#deadlines.add(error);
2518
+ reject(error);
2519
+ }, { once: true });
2243
2520
  });
2244
2521
  }
2245
2522
  async #destroy() {
@@ -2247,11 +2524,18 @@ var Probe = class {
2247
2524
  await this.#arming;
2248
2525
  } catch {}
2249
2526
  await Promise.all([
2250
- this.#type.destroy(),
2251
- this.#lint.destroy(),
2252
- this.#runtime.destroy()
2527
+ this.#destroyStage(this.#type),
2528
+ this.#destroyStage(this.#lint),
2529
+ this.#destroyStage(this.#runtime)
2253
2530
  ]);
2254
2531
  }
2532
+ async #destroyStage(stage) {
2533
+ try {
2534
+ await this.#bound(stage.destroy(), `The ${stage.stage} stage teardown exceeded ${this.#deadline} ms`, stage, stage.progress);
2535
+ } catch (error) {
2536
+ if (!(error instanceof _src_core.ProbeError) || !this.#deadlines.has(error)) throw error;
2537
+ }
2538
+ }
2255
2539
  #version(name) {
2256
2540
  const version = readWorkspaceManifest(this.#workspace, name).contents.version;
2257
2541
  if (typeof version !== "string") throw new _src_core.ProbeError(`${name} publishes no readable version`, {
@@ -2261,6 +2545,21 @@ var Probe = class {
2261
2545
  });
2262
2546
  return version;
2263
2547
  }
2548
+ #admit(claim) {
2549
+ const subject = claim.case;
2550
+ const control = claim.control;
2551
+ if (subject.test.path !== control.test.path) return;
2552
+ if (subject.test.text !== control.test.text) return;
2553
+ if (subject.files.length !== control.files.length) return;
2554
+ if (!subject.files.every((draft, index) => {
2555
+ const other = control.files[index];
2556
+ return other !== void 0 && other.path === draft.path && other.text === draft.text;
2557
+ })) return;
2558
+ throw new _src_core.ProbeError("The control must differ from the case; it carries the same candidate drafts and the same test", {
2559
+ origin: "claimant",
2560
+ code: "refused"
2561
+ });
2562
+ }
2264
2563
  #support() {
2265
2564
  const version = this.#toolchain.typescript;
2266
2565
  const range = peerDependencies.typescript;
@@ -2431,6 +2730,7 @@ exports.computeDigest = computeDigest;
2431
2730
  exports.createRevisionFile = createRevisionFile;
2432
2731
  exports.describeUnknown = describeUnknown;
2433
2732
  exports.findRefusedPaths = findRefusedPaths;
2733
+ exports.guardStage = guardStage;
2434
2734
  exports.inferDocumentLanguage = inferDocumentLanguage;
2435
2735
  exports.inferTestProject = inferTestProject;
2436
2736
  exports.inferTypeProject = inferTypeProject;
@@ -2439,9 +2739,11 @@ exports.loadWorkspaceModule = loadWorkspaceModule;
2439
2739
  exports.matchesWorkspaceModule = matchesWorkspaceModule;
2440
2740
  exports.normalizePath = normalizePath;
2441
2741
  exports.normalizeValue = normalizeValue;
2742
+ exports.overwriteFile = overwriteFile;
2442
2743
  exports.parseContentLength = parseContentLength;
2443
2744
  exports.readWorkspaceManifest = readWorkspaceManifest;
2444
2745
  exports.relativeWorkspaceFile = relativeWorkspaceFile;
2746
+ exports.relativeWorkspaceMessage = relativeWorkspaceMessage;
2445
2747
  exports.releaseListeners = releaseListeners;
2446
2748
  exports.resolveWorkspaceBinary = resolveWorkspaceBinary;
2447
2749
  exports.resolveWorkspaceFile = resolveWorkspaceFile;