@orkestrel/probe 0.0.12 → 0.0.13

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.
@@ -12,7 +12,7 @@ let _orkestrel_timeout = require("@orkestrel/timeout");
12
12
  let _orkestrel_lsp = require("@orkestrel/lsp");
13
13
  let _orkestrel_lsp_server = require("@orkestrel/lsp/server");
14
14
  let node_stream = require("node:stream");
15
- let node_timers_promises = require("node:timers/promises");
15
+ let node_child_process = require("node:child_process");
16
16
  let _orkestrel_mcp = require("@orkestrel/mcp");
17
17
  let _orkestrel_mcp_server = require("@orkestrel/mcp/server");
18
18
  let _orkestrel_tool = require("@orkestrel/tool");
@@ -325,6 +325,65 @@ function relativeWorkspaceMessage(workspace, message) {
325
325
  return text;
326
326
  }
327
327
  /**
328
+ * Scans the plain-text output of one compiler run into the diagnostics it reported.
329
+ *
330
+ * @remarks
331
+ * Reads the shape `tsc --pretty false` prints: `path(line,column): error TSnnnn: message` for a
332
+ * diagnostic about a file, and `error TSnnnn: message` for one about a project. A line that carries
333
+ * neither prefix and begins with whitespace is an elaboration of the diagnostic above it and joins
334
+ * that message with a newline; any other line is skipped, so a runtime's own failure text yields no
335
+ * diagnostic and a caller reads that as the fault it is. The compiler counts lines and columns from
336
+ * one and columns in UTF-16 code units, so each location is lowered to the zero-based UTF-16 point
337
+ * `Diagnostic.range` fixes and `end` resolves to `start`, which is the extent this output carries.
338
+ * A warning reads as a diagnostic, because the workspace's own gate reports it the same way. The
339
+ * `TS\d+` token that follows `error` or `warning` is what recognizes the line; nothing is captured
340
+ * from it, because `Diagnostic` carries no field for it.
341
+ *
342
+ * @param text - The compiler's output
343
+ * @returns One record per reported diagnostic, in the compiler's own order
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * scanDiagnostics("src/a.ts(1,14): error TS2322: Type 'string' is not assignable to type 'number'.")
348
+ * // [{ path: 'src/a.ts', range: { start: { line: 0, character: 13 }, end: { line: 0, character: 13 } }, message: "Type 'string' is not assignable to type 'number'." }]
349
+ * scanDiagnostics('error TS5058: The specified path does not exist.')
350
+ * // [{ message: 'The specified path does not exist.' }]
351
+ * ```
352
+ */
353
+ function scanDiagnostics(text) {
354
+ const diagnostics = [];
355
+ for (const line of text.split(/\r\n|\n/)) {
356
+ const located = /^(.+)\((\d+),(\d+)\): (?:error|warning) TS\d+: (.*)$/u.exec(line);
357
+ if (located !== null) {
358
+ const point = {
359
+ line: Number.parseInt(located[2] ?? "1", 10) - 1,
360
+ character: Number.parseInt(located[3] ?? "1", 10) - 1
361
+ };
362
+ diagnostics.push({
363
+ path: normalizePath(located[1] ?? ""),
364
+ range: {
365
+ start: point,
366
+ end: point
367
+ },
368
+ message: located[4] ?? ""
369
+ });
370
+ continue;
371
+ }
372
+ const bare = /^(?:error|warning) TS\d+: (.*)$/u.exec(line);
373
+ if (bare !== null) {
374
+ diagnostics.push({ message: bare[1] ?? "" });
375
+ continue;
376
+ }
377
+ const previous = diagnostics.at(-1);
378
+ if (previous === void 0 || !/^\s+\S/u.test(line)) continue;
379
+ diagnostics[diagnostics.length - 1] = {
380
+ ...previous,
381
+ message: `${previous.message}\n${line}`
382
+ };
383
+ }
384
+ return diagnostics;
385
+ }
386
+ /**
328
387
  * Resolves one installed module from the target workspace.
329
388
  *
330
389
  * @param workspace - The target workspace root
@@ -350,18 +409,35 @@ function resolveWorkspaceModule(workspace, specifier) {
350
409
  cause: error
351
410
  });
352
411
  }
353
- function loadWorkspaceModule(workspace, specifier) {
412
+ /**
413
+ * Loads the installed `vitest/node` module from a target workspace.
414
+ *
415
+ * @remarks
416
+ * The only tool this package drives in process. The compiler is a process this package spawns
417
+ * rather than a module it loads, so `typescript` is resolved through {@link resolveWorkspaceBinary}
418
+ * instead.
419
+ *
420
+ * @param workspace - The target workspace root
421
+ * @returns The installed `vitest/node` module
422
+ * @throws When the workspace cannot load `vitest/node`
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * const vitest = loadWorkspaceVitest(process.cwd())
427
+ * typeof vitest.createVitest === 'function' // true
428
+ * ```
429
+ */
430
+ function loadWorkspaceVitest(workspace) {
354
431
  const outcome = (0, _orkestrel_contract.attempt)(() => {
355
- const require$1 = (0, node_module.createRequire)((0, node_path.resolve)(workspace, "package.json"));
356
- return specifier === "typescript" ? require$1("typescript") : require$1("vitest/node");
432
+ return (0, node_module.createRequire)((0, node_path.resolve)(workspace, "package.json"))("vitest/node");
357
433
  });
358
434
  if (outcome.success) return outcome.value;
359
435
  const error = outcome.error;
360
436
  const missing = readFaultCode(error) === "MODULE_NOT_FOUND";
361
- throw new _src_core.ProbeError(`The workspace cannot load ${specifier}`, {
437
+ throw new _src_core.ProbeError("The workspace cannot load vitest/node", {
362
438
  origin: "workspace",
363
439
  code: missing ? "missing" : "malformed",
364
- context: { name: specifier },
440
+ context: { name: "vitest/node" },
365
441
  cause: error
366
442
  });
367
443
  }
@@ -434,20 +510,26 @@ function readWorkspaceManifest(workspace, name) {
434
510
  /**
435
511
  * Resolves a package's portable JavaScript binary from the target workspace.
436
512
  *
513
+ * @remarks
514
+ * A package publishes its binaries under keys of its own choosing, and that key is often not the
515
+ * package name: `typescript` publishes `tsc`. Name the key as `command` wherever the two differ.
516
+ * Default: the package name.
517
+ *
437
518
  * @param workspace - The target workspace root
438
- * @param name - The installed package name and binary key
519
+ * @param name - The installed package name
520
+ * @param command - The `bin` key to read. Default: `name`
439
521
  * @returns The absolute JavaScript entry named by the package's `bin` field
440
- * @throws When the package does not publish a binary under the requested name
522
+ * @throws When the package does not publish a binary under the requested key
441
523
  *
442
524
  * @example
443
525
  * ```ts
444
526
  * relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'oxlint'))
445
527
  * // 'node_modules/oxlint/bin/oxlint'
446
- * resolveWorkspaceBinary(process.cwd(), 'typescript')
447
- * // throws: typescript does not publish the typescript binary
528
+ * relativeWorkspaceFile(process.cwd(), resolveWorkspaceBinary(process.cwd(), 'typescript', 'tsc'))
529
+ * // 'node_modules/typescript/bin/tsc'
448
530
  * ```
449
531
  */
450
- function resolveWorkspaceBinary(workspace, name) {
532
+ function resolveWorkspaceBinary(workspace, name, command = name) {
451
533
  const manifest = readWorkspaceManifest(workspace, name);
452
534
  const bin = manifest.contents.bin;
453
535
  if (bin === void 0) throw new _src_core.ProbeError(`${name} does not publish a bin field`, {
@@ -456,13 +538,13 @@ function resolveWorkspaceBinary(workspace, name) {
456
538
  context: { name }
457
539
  });
458
540
  if (typeof bin === "string") return (0, node_path.resolve)(manifest.path, "..", bin);
459
- if (!(0, _orkestrel_contract.isRecord)(bin) || !(name in bin)) throw new _src_core.ProbeError(`${name} does not publish the ${name} binary`, {
541
+ if (!(0, _orkestrel_contract.isRecord)(bin) || !(command in bin)) throw new _src_core.ProbeError(`${name} does not publish the ${command} binary`, {
460
542
  origin: "workspace",
461
543
  code: "missing",
462
544
  context: { name }
463
545
  });
464
- const entry = bin[name];
465
- if (typeof entry !== "string") throw new _src_core.ProbeError(`${name} publishes an invalid ${name} binary`, {
546
+ const entry = bin[command];
547
+ if (typeof entry !== "string") throw new _src_core.ProbeError(`${name} publishes an invalid ${command} binary`, {
466
548
  origin: "workspace",
467
549
  code: "malformed",
468
550
  context: {
@@ -563,6 +645,39 @@ function buildRevisionPath(workspace, path, revision) {
563
645
  return `${extension === "" ? file : file.slice(0, -extension.length)}.probe-${revision}${extension}`;
564
646
  }
565
647
  /**
648
+ * Filters one issue list to the distinct issues it carries, in the order they arrived.
649
+ *
650
+ * @remarks
651
+ * A stage that reads one candidate through more than one tool run reports that candidate's
652
+ * diagnostic once per run. Two issues are the same issue when their party, path, message, and range
653
+ * all agree, because those are every value an issue carries.
654
+ *
655
+ * @param issues - The issues to filter
656
+ * @returns The distinct issues, in the order the list carried them
657
+ *
658
+ * @example
659
+ * ```ts
660
+ * const issue: Issue = { origin: 'claimant', path: 'src/core/greeting.ts', message: 'not assignable' }
661
+ * filterUniqueIssues([issue, { ...issue }]).length // 1
662
+ * ```
663
+ */
664
+ function filterUniqueIssues(issues) {
665
+ const seen = /* @__PURE__ */ new Set();
666
+ const distinct = [];
667
+ for (const issue of issues) {
668
+ const key = JSON.stringify([
669
+ issue.origin,
670
+ issue.path,
671
+ issue.message,
672
+ issue.range
673
+ ]);
674
+ if (seen.has(key)) continue;
675
+ seen.add(key);
676
+ distinct.push(issue);
677
+ }
678
+ return distinct;
679
+ }
680
+ /**
566
681
  * Reports whether a path is a workspace module Vitest can cache.
567
682
  *
568
683
  * @param path - The candidate file path
@@ -578,6 +693,75 @@ function matchesWorkspaceModule(path) {
578
693
  return /\.(?:[cm]?[jt]sx?|vue|json)$/.test(path);
579
694
  }
580
695
  /**
696
+ * Reports whether the host that wrote one file is still running.
697
+ *
698
+ * @remarks
699
+ * Signal 0 delivers nothing and reports reachability alone. A process this one may not signal
700
+ * reports `EPERM` and reads as running, and a non-positive identity names a process group rather
701
+ * than a process, so both answer true: the safe direction is to keep a file this package cannot
702
+ * account for.
703
+ *
704
+ * @param id - The process id a file's own revision names
705
+ * @returns True if that process is running or cannot be judged; false otherwise
706
+ *
707
+ * @example
708
+ * ```ts
709
+ * matchesLiveProcess(process.pid) // true
710
+ * matchesLiveProcess(0) // true
711
+ * ```
712
+ */
713
+ function matchesLiveProcess(id) {
714
+ if (!Number.isSafeInteger(id) || id <= 0) return true;
715
+ try {
716
+ process.kill(id, 0);
717
+ return true;
718
+ } catch (error) {
719
+ return readFaultCode(error) === "EPERM";
720
+ }
721
+ }
722
+ /**
723
+ * Collects every regular file a target workspace holds, skipping the trees no inspection reads.
724
+ *
725
+ * @remarks
726
+ * Skips a directory named `.git`, `dist`, or `node_modules` at any depth, and the directory
727
+ * {@link TYPE_MIRROR} names, which would otherwise carry this package's own mirror into the walk. A
728
+ * directory this host cannot list is skipped rather than raised, so one unreadable directory in a
729
+ * consumer's tree is not a reason to refuse the whole walk. A symbolic link is not carried, whether
730
+ * it names a file or a directory, so a file reached only through one is absent from the result; a
731
+ * `Dirent` that is neither a directory nor a regular file is skipped the same way.
732
+ *
733
+ * @param workspace - The target workspace root
734
+ * @returns The absolute path of every regular file the walk found
735
+ *
736
+ * @example
737
+ * ```ts
738
+ * collectWorkspaceFiles(process.cwd()).some((path) => path.endsWith('package.json')) // true
739
+ * ```
740
+ */
741
+ function collectWorkspaceFiles(workspace) {
742
+ const excluded = normalizePath(resolveWorkspaceFile(workspace, _src_core.TYPE_MIRROR));
743
+ const files = [];
744
+ const directories = [(0, node_path.resolve)(workspace)];
745
+ while (directories.length > 0) {
746
+ const directory = directories.pop();
747
+ if (directory === void 0) break;
748
+ let entries = [];
749
+ try {
750
+ entries = (0, node_fs.readdirSync)(directory, { withFileTypes: true });
751
+ } catch {}
752
+ for (const entry of entries) {
753
+ if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
754
+ const path = (0, node_path.join)(directory, entry.name);
755
+ if (entry.isDirectory()) {
756
+ if (normalizePath(path) !== excluded) directories.push(path);
757
+ continue;
758
+ }
759
+ if (entry.isFile()) files.push(path);
760
+ }
761
+ }
762
+ return files;
763
+ }
764
+ /**
581
765
  * Normalizes a caught or foreign error into readable text.
582
766
  *
583
767
  * @param value - The value to describe
@@ -596,9 +780,9 @@ function describeUnknown(value) {
596
780
  return String(value);
597
781
  }
598
782
  /**
599
- * Guards one resident-stage operation with the stage failure contract.
783
+ * Guards one stage operation with the stage failure contract.
600
784
  *
601
- * @param stage - The resident stage serving the operation
785
+ * @param stage - The stage serving the operation
602
786
  * @param operation - The operation to settle
603
787
  * @returns The operation's fulfilled value
604
788
  * @throws The original `ProbeError`, or an instrument-owned malformed failure retaining the cause
@@ -670,12 +854,12 @@ function findRefusedPaths(value) {
670
854
  * sorts every record's keys.
671
855
  *
672
856
  * @remarks
673
- * A parsed TypeScript project carries the absolute root it was parsed at, so the same commit
674
- * checked out at two paths produces two values that describe one configuration. Rewriting those
675
- * members makes the value portable and stops a host path leaking into anything derived from it. A
676
- * path that escapes the workspace is left as it stands, because rewriting it would name a file
677
- * outside the tree by a relative spelling. Key order is the parser's, not the configuration's, so
678
- * sorting removes a difference that means nothing.
857
+ * `tsc --showConfig` spells a path relative to the project file, so a printed record carries an
858
+ * absolute path only where a project itself declares one. Rewriting a contained absolute path to
859
+ * its relative form keeps one commit checked out at two roots reading to one digest. A path that
860
+ * escapes the workspace is left as it stands, because rewriting it would name a file outside the
861
+ * tree by a relative spelling. Key order is the compiler's own and it differs between the supported
862
+ * majors, so sorting removes a difference that means nothing.
679
863
  *
680
864
  * @param workspace - The target workspace root
681
865
  * @param value - The value to rewrite
@@ -776,9 +960,66 @@ function releaseListeners(emitter, capture) {
776
960
  }
777
961
  }
778
962
  //#endregion
963
+ //#region src/server/parsers.ts
964
+ /**
965
+ * Parses the configuration one compiler run printed for a TypeScript project.
966
+ *
967
+ * @remarks
968
+ * Reads what `tsc --showConfig` writes: a JSON record carrying the resolved `compilerOptions`
969
+ * beside the project's own file selection. `compilerOptions` is carried unvalidated, because a
970
+ * caller digests it rather than reading a member out of it. `files` and `include` are read only
971
+ * where the printed record carries one whose entries are all strings, so a member the compiler
972
+ * printed in another shape reads as absent rather than as an empty selection. Text that is not a
973
+ * JSON record, including the diagnostic the compiler prints for a project it refuses, yields
974
+ * `undefined`.
975
+ *
976
+ * @param text - The compiler's printed configuration
977
+ * @returns The project's resolved configuration, or `undefined` when the text carries no record
978
+ *
979
+ * @example
980
+ * ```ts
981
+ * parseProjectConfig('{"compilerOptions":{"strict":true}}')?.compilerOptions // { strict: true }
982
+ * parseProjectConfig("error TS5058: The specified path does not exist.") // undefined
983
+ * ```
984
+ */
985
+ function parseProjectConfig(text) {
986
+ const parsed = (0, _orkestrel_contract.attempt)(() => JSON.parse(text));
987
+ if (!parsed.success || !(0, _orkestrel_contract.isRecord)(parsed.value)) return void 0;
988
+ const files = parsed.value.files;
989
+ const include = parsed.value.include;
990
+ return {
991
+ compilerOptions: parsed.value.compilerOptions,
992
+ ...(0, _orkestrel_contract.isArray)(files) && files.every((entry) => typeof entry === "string") ? { files } : {},
993
+ ...(0, _orkestrel_contract.isArray)(include) && include.every((entry) => typeof entry === "string") ? { include } : {}
994
+ };
995
+ }
996
+ /**
997
+ * Parses the process id one revision identity names.
998
+ *
999
+ * @remarks
1000
+ * Every file and directory this package writes into a target carries the writing host's process id
1001
+ * and a fresh UUID, joined by `-`. A sweep reads that id to decide whether the host that wrote the
1002
+ * name is gone. The whole identity must match, so a name carrying anything else yields `undefined`
1003
+ * and the sweep leaves what it found alone.
1004
+ *
1005
+ * @param revision - The revision identity a name carries
1006
+ * @returns The process id it names, or `undefined` when the text is not a revision identity
1007
+ *
1008
+ * @example
1009
+ * ```ts
1010
+ * parseRevisionOwner('4821-1b4e28ba-2fa1-11d2-883f-0016d3cca427') // 4821
1011
+ * parseRevisionOwner('4821-9f0c') // undefined
1012
+ * ```
1013
+ */
1014
+ function parseRevisionOwner(revision) {
1015
+ 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];
1016
+ return owner === void 0 ? void 0 : Number.parseInt(owner, 10);
1017
+ }
1018
+ //#endregion
779
1019
  //#region src/server/Overlay.ts
780
1020
  /**
781
- * Holds the candidate drafts one inspection substitutes for the files a tool would read from disk.
1021
+ * Implements `OverlayInterface` over a private map from normalized absolute path to candidate text,
1022
+ * minting at construction the `revision` a resident tool caches its answers against.
782
1023
  *
783
1024
  * @remarks
784
1025
  * One overlay belongs to one inspection: the inspection creates it, records every candidate it
@@ -787,55 +1028,44 @@ function releaseListeners(emitter, capture) {
787
1028
  * is what makes a resident tool re-read a path this overlay holds, so two inspections that supply
788
1029
  * different text for one path never share a cached answer.
789
1030
  *
790
- * This overlay matches a lookup key the way the host its stage declares to its tool matches file
791
- * names. A host that resolves two spellings of one file name to one file leaves the tool holding
792
- * whichever spelling it met first — the disk one, where the file is on disk — and the tool asks
793
- * under that, so an overlay matching keys exactly answers nothing and the tool reads the committed
794
- * file instead. Mint the overlay with the sensitivity the stage declares, and one reading decides
795
- * both. The recorded spelling is what `paths` reports and what `covers` matches a directory
796
- * against, so a folded key never reaches the paths a tool is handed and containment stays an exact
797
- * comparison.
1031
+ * A lookup key matches a recorded path exactly, after both pass through `normalizePath`, so a tool
1032
+ * that reports its own paths with backslashes still reaches a candidate recorded with forward
1033
+ * slashes. Case is never folded: the recorded spelling is what `paths` reports, what `covers`
1034
+ * compares a directory against, and what `text` answers under. A stage whose tool resolves two
1035
+ * spellings of one file name to one file therefore reports the path its tool served rather than
1036
+ * matching the two spellings here.
798
1037
  *
799
1038
  * @example
800
1039
  * ```ts
801
1040
  * const overlay = new Overlay()
802
- * overlay.set('/srv/checkout/src/core/greeting.ts', "export const GREETING = 'hi'\n")
803
- * console.log(overlay.text('/srv/checkout/src/core/greeting.ts'))
1041
+ * const path = '/srv/checkout/src/core/factories.ts'
1042
+ * overlay.set(path, "export function createGreeting(): string {\n\treturn 'hi'\n}\n")
1043
+ * console.log(overlay.text(path))
804
1044
  * overlay.clear()
805
1045
  * ```
806
1046
  */
807
1047
  var Overlay = class {
808
1048
  #revision = (0, node_crypto.randomUUID)();
809
- #sensitive;
810
1049
  #candidates = /* @__PURE__ */ new Map();
811
- /**
812
- * Creates an empty candidate set under a fresh identity.
813
- *
814
- * @param options - Construction options
815
- */
816
- constructor(options = {}) {
817
- this.#sensitive = options.sensitive ?? true;
818
- }
819
1050
  get revision() {
820
1051
  return this.#revision;
821
1052
  }
822
1053
  get paths() {
823
- return [...this.#candidates.values()].map(([path]) => path);
1054
+ return [...this.#candidates.keys()];
824
1055
  }
825
1056
  /**
826
1057
  * Records one candidate's text against the absolute path it stands in for.
827
1058
  *
828
1059
  * @remarks
829
- * Recording a candidate under a path this overlay's own matching reads as one already recorded
830
- * replaces that candidate, the way one file holds one text.
1060
+ * Recording a candidate under a path this overlay already holds replaces that candidate, the way
1061
+ * one file holds one text.
831
1062
  *
832
1063
  * @param path - The absolute path the candidate replaces
833
1064
  * @param text - The candidate's full contents
834
1065
  * @returns Nothing
835
1066
  */
836
1067
  set(path, text) {
837
- const recorded = normalizePath(path);
838
- this.#candidates.set(this.#key(recorded), [recorded, text]);
1068
+ this.#candidates.set(normalizePath(path), text);
839
1069
  }
840
1070
  /**
841
1071
  * Reads the candidate text recorded for one absolute path.
@@ -844,7 +1074,7 @@ var Overlay = class {
844
1074
  * @returns The recorded text, or `undefined` when this overlay holds no candidate there
845
1075
  */
846
1076
  text(path) {
847
- return this.#candidates.get(this.#key(normalizePath(path)))?.[1];
1077
+ return this.#candidates.get(normalizePath(path));
848
1078
  }
849
1079
  /**
850
1080
  * Checks whether a candidate sits beneath one directory.
@@ -853,16 +1083,14 @@ var Overlay = class {
853
1083
  * The answer is derived from the paths the overlay holds rather than stored, so it stops being
854
1084
  * true exactly when the inspection that declared those candidates clears them. Both sides pass
855
1085
  * through `normalizePath` first, because a tool that normalizes its own paths asks about a
856
- * directory in a spelling the recorded path may not share. Case is never folded here: this is a
857
- * containment comparison, and it reads the recorded spelling whatever an overlay minted for a
858
- * case-folding host matches its lookup keys by.
1086
+ * directory in a spelling the recorded path may not share.
859
1087
  *
860
1088
  * @param directory - The absolute directory path to check
861
1089
  * @returns True if a candidate path sits beneath the directory; false otherwise
862
1090
  */
863
1091
  covers(directory) {
864
1092
  const base = `${normalizePath(directory).replace(/\/+$/, "")}/`;
865
- for (const [path] of this.#candidates.values()) if (path.startsWith(base)) return true;
1093
+ for (const path of this.#candidates.keys()) if (path.startsWith(base)) return true;
866
1094
  return false;
867
1095
  }
868
1096
  /**
@@ -873,15 +1101,12 @@ var Overlay = class {
873
1101
  clear() {
874
1102
  this.#candidates.clear();
875
1103
  }
876
- #key(path) {
877
- return this.#sensitive ? path : path.toLowerCase();
878
- }
879
1104
  };
880
1105
  //#endregion
881
1106
  //#region package.json
882
- var version = "0.0.12";
1107
+ var version = "0.0.13";
883
1108
  var peerDependencies = {
884
- "oxlint": "^1.80.0",
1109
+ "oxlint": "^1.82.0",
885
1110
  "typescript": "^6.0.3",
886
1111
  "vitest": "^4.1.11"
887
1112
  };
@@ -1166,7 +1391,7 @@ var RuntimeStage = class {
1166
1391
  */
1167
1392
  constructor(workspace = process.cwd()) {
1168
1393
  this.#workspace = workspace;
1169
- const vitest = loadWorkspaceModule(this.#workspace, "vitest/node");
1394
+ const vitest = loadWorkspaceVitest(this.#workspace);
1170
1395
  this.#store(this.#warm(vitest.createVitest));
1171
1396
  }
1172
1397
  get stage() {
@@ -1175,9 +1400,29 @@ var RuntimeStage = class {
1175
1400
  get progress() {
1176
1401
  return this.#progress;
1177
1402
  }
1403
+ /**
1404
+ * Inspects one case.
1405
+ *
1406
+ * @param subject - The candidate drafts and test to inspect
1407
+ * @returns One outcome for this stage
1408
+ * @throws When the workspace's own tool cannot start or has already been destroyed
1409
+ */
1178
1410
  inspect(subject) {
1179
1411
  return guardStage(this.stage, this.#inspect(subject));
1180
1412
  }
1413
+ /**
1414
+ * Tears down the resident tool or the mirror and releases its resources.
1415
+ *
1416
+ * @remarks
1417
+ * A stage abandons every inspection it holds rather than waiting behind one, so teardown never
1418
+ * waits for an inspection to return. An abandoned inspection rejects, either at the stage's own
1419
+ * guard or as the owned tool closes. Teardown is bounded whatever the stage's own tool does: a
1420
+ * tool that answers neither its warming exchange nor its ending is signalled and released at the
1421
+ * stage's own deadline. A coordinator replaces a stage whose worker no longer returns because
1422
+ * teardown neither waits for an inspection nor waits past that deadline.
1423
+ *
1424
+ * @returns A promise that settles after the resident tool or the mirror releases its resources
1425
+ */
1181
1426
  destroy() {
1182
1427
  if (this.#closing !== void 0) return this.#closing;
1183
1428
  this.#closing = guardStage(this.stage, this.#destroy());
@@ -1521,13 +1766,13 @@ var RuntimeStage = class {
1521
1766
  const current = this.#vitest;
1522
1767
  if (current !== void 0 && this.#specifications < _src_core.PROBE_SPECIFICATIONS) return current;
1523
1768
  if (current !== void 0) return this.#store(this.#replace(current));
1524
- const runner = loadWorkspaceModule(this.#workspace, "vitest/node");
1769
+ const runner = loadWorkspaceVitest(this.#workspace);
1525
1770
  return this.#store(this.#specifications < _src_core.PROBE_SPECIFICATIONS ? this.#warm(runner.createVitest) : this.#replace(void 0, runner.createVitest));
1526
1771
  }
1527
1772
  async #replace(current, create) {
1528
1773
  if (current !== void 0) await (await current).close();
1529
1774
  if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("runtime stage");
1530
- const runner = create ?? loadWorkspaceModule(this.#workspace, "vitest/node").createVitest;
1775
+ const runner = create ?? loadWorkspaceVitest(this.#workspace).createVitest;
1531
1776
  const replacement = await this.#warm(runner);
1532
1777
  this.#specifications = 0;
1533
1778
  return replacement;
@@ -1571,7 +1816,7 @@ var RuntimeStage = class {
1571
1816
  }
1572
1817
  #snapshot() {
1573
1818
  const modules = /* @__PURE__ */ new Map();
1574
- for (const path of this.#walk()) {
1819
+ for (const path of collectWorkspaceFiles(this.#workspace)) {
1575
1820
  if (!matchesWorkspaceModule(path)) continue;
1576
1821
  try {
1577
1822
  const digest = (0, node_crypto.createHash)("sha256").update((0, node_fs.readFileSync)(path)).digest("hex");
@@ -1582,12 +1827,12 @@ var RuntimeStage = class {
1582
1827
  return modules;
1583
1828
  }
1584
1829
  #sweep() {
1585
- for (const path of this.#walk()) {
1830
+ for (const path of collectWorkspaceFiles(this.#workspace)) {
1586
1831
  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);
1587
1832
  const revision = match?.[1];
1588
1833
  const owner = match?.[2];
1589
1834
  if (revision === void 0 || owner === void 0) continue;
1590
- if (this.#alive(Number.parseInt(owner, 10))) continue;
1835
+ if (matchesLiveProcess(Number.parseInt(owner, 10))) continue;
1591
1836
  if (!this.#owned(path, revision)) continue;
1592
1837
  try {
1593
1838
  (0, node_fs.unlinkSync)(resolveWorkspaceFile(this.#workspace, (0, node_path.relative)(this.#workspace, path), true));
@@ -1598,35 +1843,6 @@ var RuntimeStage = class {
1598
1843
  const outcome = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readFileSync)(path, "utf8"));
1599
1844
  return outcome.success && (0, _src_core.matchesSpecification)(outcome.value, revision);
1600
1845
  }
1601
- #alive(id) {
1602
- if (!Number.isSafeInteger(id) || id <= 0) return true;
1603
- try {
1604
- process.kill(id, 0);
1605
- return true;
1606
- } catch (error) {
1607
- return readFaultCode(error) === "EPERM";
1608
- }
1609
- }
1610
- *#walk() {
1611
- const directories = [(0, node_path.resolve)(this.#workspace)];
1612
- while (directories.length > 0) {
1613
- const directory = directories.pop();
1614
- if (directory === void 0) break;
1615
- let entries = [];
1616
- try {
1617
- entries = (0, node_fs.readdirSync)(directory, { withFileTypes: true });
1618
- } catch {}
1619
- for (const entry of entries) {
1620
- if (entry.name === ".git" || entry.name === "dist" || entry.name === "node_modules") continue;
1621
- const path = (0, node_path.join)(directory, entry.name);
1622
- if (entry.isDirectory()) {
1623
- directories.push(path);
1624
- continue;
1625
- }
1626
- if (entry.isFile()) yield path;
1627
- }
1628
- }
1629
- }
1630
1846
  #issues(result, file, original) {
1631
1847
  const specification = this.#real(file);
1632
1848
  const issues = [];
@@ -1742,21 +1958,32 @@ var RuntimeStage = class {
1742
1958
  //#endregion
1743
1959
  //#region src/server/stages/TypeStage.ts
1744
1960
  /**
1745
- * Inspects TypeScript source through resident language services from the target workspace.
1961
+ * Inspects TypeScript source by running the target workspace's own compiler over a mirror of it.
1746
1962
  *
1747
1963
  * @remarks
1748
- * Construction starts loading the workspace's compiler and warming one service per project the
1749
- * workspace declares. A candidate draft is checked against the project a call names, or
1750
- * against its own scoped environment project when a call names none, while the test uses the root
1751
- * project. Disk snapshots use their modification time as the service version so dependency edits
1752
- * cannot leave a warm answer stale. Candidate text lives in an overlay the inspection owns, and
1753
- * the language-service host answers existence, reads, and versions from it, so a candidate that
1754
- * exists only as text is importable and a candidate that shadows a disk file is checked as the
1755
- * text the case supplied. A project outside the declared set holds one recycled slot, so a caller
1756
- * varying the project cannot grow the resident set. A language service checks one candidate
1757
- * synchronously, so the stage hands the host's event loop back at each candidate boundary: a
1758
- * caller's deadline is answered within one candidate's check rather than after the whole
1759
- * inspection, and an inspection abandoned at that deadline stops at the next boundary.
1964
+ * The compiler is a process this stage spawns rather than a module it loads, so nothing here holds
1965
+ * a resident program and the host's event loop is free for the whole check. Construction sweeps a
1966
+ * mirror an earlier host left behind, copies the workspace into a fresh one under
1967
+ * {@link TYPE_MIRROR}, and builds each declared project's incremental state there, so the first
1968
+ * inspection is warm.
1969
+ *
1970
+ * Every inspection refreshes the mirror by content digest, writes each candidate draft and the test
1971
+ * at its mirrored declared path, and runs `tsc --noEmit --pretty false` once per distinct selected
1972
+ * project. A draft therefore shadows the file it replaces: a consumer of that path is checked
1973
+ * against the draft's text, and a draft importing a sibling draft resolves to the sibling draft.
1974
+ * Nothing is written outside `tmp/`, and the workspace's own copy of a drafted file never moves.
1975
+ *
1976
+ * Each run reads a scratch project this stage writes beside the mirrored project it extends, so
1977
+ * every relative path the project declares — its own `rootDir`, `include`, `paths`, and the
1978
+ * projects it extends — resolves inside the mirror without being rewritten. The scratch adds the
1979
+ * drafts as `files`, which the project's own `exclude` does not reach, and its incremental state
1980
+ * lives in the mirror so reuse survives the refresh.
1981
+ *
1982
+ * The diagnostics decide the outcome, never the exit code, which the supported compiler majors
1983
+ * disagree on. A diagnostic naming a project file, and one naming no file at all, is the target
1984
+ * tree's own configuration fault and raises rather than reporting, unless the `.json` file is one
1985
+ * the claim itself drafted; every other diagnostic is a claimant issue at the point the compiler
1986
+ * reported, which is the extent the plain-text output carries.
1760
1987
  *
1761
1988
  * @example
1762
1989
  * ```ts
@@ -1767,26 +1994,27 @@ var RuntimeStage = class {
1767
1994
  */
1768
1995
  var TypeStage = class {
1769
1996
  #workspace;
1770
- #typescript;
1771
- #services = /* @__PURE__ */ new Map();
1772
- #resident = /* @__PURE__ */ new Set();
1773
- #options = /* @__PURE__ */ new Map();
1774
- #files = /* @__PURE__ */ new Map();
1775
- #diagnostics = /* @__PURE__ */ new Map();
1776
- #overlay = new Overlay();
1777
- #recycled;
1997
+ #compiler;
1998
+ #revision = `${process.pid}-${(0, node_crypto.randomUUID)()}`;
1999
+ #mirror;
2000
+ #configs = /* @__PURE__ */ new Map();
2001
+ #mirrored = /* @__PURE__ */ new Map();
2002
+ #drafts = /* @__PURE__ */ new Set();
2003
+ #children = /* @__PURE__ */ new Set();
2004
+ #warming;
1778
2005
  #closing;
1779
2006
  #progress = 0;
1780
2007
  /**
1781
- * Starts warming the target workspace's TypeScript service.
2008
+ * Resolves the target workspace's compiler and starts warming its mirror.
1782
2009
  *
1783
2010
  * @param workspace - The target workspace root. Default: the current working directory
1784
2011
  */
1785
2012
  constructor(workspace = process.cwd()) {
1786
2013
  this.#workspace = workspace;
1787
- const typescript = loadWorkspaceModule(this.#workspace, "typescript");
1788
- this.#typescript = this.#warm(typescript);
1789
- this.#typescript.catch(() => {});
2014
+ this.#compiler = resolveWorkspaceBinary(workspace, "typescript", "tsc");
2015
+ this.#mirror = resolveWorkspaceFile(workspace, `${_src_core.TYPE_MIRROR}/${this.#revision}`);
2016
+ this.#warming = this.#warm();
2017
+ this.#warming.catch(() => {});
1790
2018
  }
1791
2019
  get stage() {
1792
2020
  return "type";
@@ -1806,7 +2034,8 @@ var TypeStage = class {
1806
2034
  * @param project - The workspace-relative TypeScript project the candidate drafts are checked
1807
2035
  * against. Default: the scoped project each candidate path infers
1808
2036
  * @returns One outcome for this stage
1809
- * @throws When the resident compiler cannot start or the stage has already been destroyed
2037
+ * @throws When the workspace refuses the mirror, when a project the run reads is malformed, or
2038
+ * when the stage has already been destroyed
1810
2039
  */
1811
2040
  inspect(subject, project) {
1812
2041
  return guardStage(this.stage, this.#inspect(subject, project));
@@ -1815,15 +2044,15 @@ var TypeStage = class {
1815
2044
  * Resolves one project to the path and digest this stage applies for it.
1816
2045
  *
1817
2046
  * @remarks
1818
- * Reads the parse this stage itself applies, filling its cache when the project is not already
1819
- * resident, so the reported digest is the configuration the inspection is judged under rather
1820
- * than a second parse a caller ran. The returned record is a value copy, so a later eviction
1821
- * does not move it.
2047
+ * Reads the configuration the compiler itself prints for the project as the mirror holds it,
2048
+ * refreshed from the workspace first on a cache miss, so the digest names the configuration the
2049
+ * check applies. The reading is cached per project for the life of the stage, so a draft written
2050
+ * afterward cannot move it.
1822
2051
  *
1823
2052
  * @param project - The workspace-relative TypeScript project to resolve
1824
2053
  * @returns The resolved workspace-relative path and the digest of its compiler options
1825
- * @throws When the project escapes the workspace, cannot be parsed, or the stage has already
1826
- * been destroyed
2054
+ * @throws When the project escapes the workspace, when the compiler refuses it, or when the
2055
+ * stage has already been destroyed
1827
2056
  */
1828
2057
  resolve(project) {
1829
2058
  return guardStage(this.stage, this.#resolve(project));
@@ -1834,81 +2063,76 @@ var TypeStage = class {
1834
2063
  return this.#closing;
1835
2064
  }
1836
2065
  async #inspect(subject, project) {
1837
- if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("type stage");
2066
+ this.#refuseDestroyed();
1838
2067
  const started = performance.now();
1839
- const typescript = await this.#typescript;
1840
- if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("type stage");
1841
- const selections = subject.files.map((draft) => ({
2068
+ await this.#warming;
2069
+ this.#refuseDestroyed();
2070
+ const test = resolveWorkspaceFile(this.#workspace, subject.test.path);
2071
+ const resolved = subject.files.map((draft) => ({
1842
2072
  draft,
1843
2073
  path: resolveWorkspaceFile(this.#workspace, draft.path)
1844
- })).map((candidate) => ({
1845
- draft: candidate.draft,
1846
- project: project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path))
1847
2074
  }));
1848
- const root = this.#service(typescript, "tsconfig.json");
1849
- this.#configure(root, "tsconfig.json");
1850
- await this.#unblock();
1851
- for (const selection of selections) {
1852
- this.#configure(this.#service(typescript, selection.project), selection.project);
1853
- await this.#unblock();
2075
+ const groups = /* @__PURE__ */ new Map([[this.#contain("tsconfig.json"), [subject.test]]]);
2076
+ for (const candidate of resolved) {
2077
+ const selected = this.#contain(project ?? inferTypeProject(relativeWorkspaceFile(this.#workspace, candidate.path)));
2078
+ const drafts = groups.get(selected);
2079
+ if (drafts === void 0) groups.set(selected, [candidate.draft]);
2080
+ else drafts.push(candidate.draft);
1854
2081
  }
1855
- const overlay = new Overlay({ sensitive: this.#caseSensitive(typescript) });
1856
- this.#overlay = overlay;
2082
+ this.#progress += 1;
2083
+ for (const selected of groups.keys()) await this.#configure(selected);
2084
+ this.#refuseDestroyed();
1857
2085
  try {
1858
- this.#record(subject.test, overlay);
1859
- for (const draft of subject.files) this.#record(draft, overlay);
1860
- this.#progress += 1;
2086
+ this.#refresh();
2087
+ this.#drafts.add(this.#place(subject.test.text, test));
2088
+ for (const candidate of resolved) this.#drafts.add(this.#place(candidate.draft.text, candidate.path));
1861
2089
  const issues = [];
1862
- const projects = /* @__PURE__ */ new Set();
1863
- issues.push(...this.#issues(typescript, root, subject.test, "tsconfig.json", false, true));
1864
- projects.add("tsconfig.json");
1865
- await this.#unblock();
1866
- for (const selection of selections) {
1867
- const draft = selection.draft;
1868
- const selected = selection.project;
1869
- const service = this.#service(typescript, selected);
1870
- issues.push(...this.#issues(typescript, service, draft, selected, project !== void 0, !projects.has(selected)));
1871
- projects.add(selected);
1872
- await this.#unblock();
2090
+ for (const [selected, drafts] of groups) {
2091
+ this.#refuseDestroyed();
2092
+ issues.push(...await this.#check(selected, drafts));
1873
2093
  }
1874
2094
  return {
1875
2095
  stage: this.stage,
1876
2096
  elapsed: Math.round(performance.now() - started),
1877
- issues
2097
+ issues: filterUniqueIssues(issues)
1878
2098
  };
1879
2099
  } finally {
1880
- overlay.clear();
2100
+ this.#release();
1881
2101
  }
1882
2102
  }
1883
2103
  async #resolve(project) {
1884
- if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("type stage");
1885
- const typescript = await this.#typescript;
1886
- if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("type stage");
1887
- const resolved = resolveWorkspaceFile(this.#workspace, project);
2104
+ this.#refuseDestroyed();
2105
+ const contained = this.#contain(project);
1888
2106
  this.#progress += 1;
1889
- this.#service(typescript, project);
1890
- await this.#unblock();
2107
+ const config = await this.#configure(contained);
1891
2108
  return {
1892
- path: relativeWorkspaceFile(this.#workspace, resolved),
1893
- digest: computeDigest(this.#workspace, this.#options.get(resolved) ?? {})
2109
+ path: contained,
2110
+ digest: computeDigest(this.#workspace, config.compilerOptions)
1894
2111
  };
1895
2112
  }
1896
2113
  async #destroy() {
1897
- await this.#typescript.catch(() => void 0);
1898
- for (const service of this.#services.values()) service.dispose();
1899
- this.#services.clear();
1900
- this.#resident.clear();
1901
- this.#recycled = void 0;
1902
- this.#options.clear();
1903
- this.#files.clear();
1904
- this.#diagnostics.clear();
1905
- }
1906
- async #warm(typescript) {
1907
- for (const project of this.#projects()) {
1908
- this.#resident.add(resolveWorkspaceFile(this.#workspace, project));
1909
- this.#service(typescript, project);
1910
- }
1911
- return typescript;
2114
+ const children = [...this.#children];
2115
+ this.#children.clear();
2116
+ for (const child of children) this.#terminate(child);
2117
+ await this.#warming.catch(() => void 0);
2118
+ try {
2119
+ (0, node_fs.rmSync)(this.#mirror, {
2120
+ recursive: true,
2121
+ force: true,
2122
+ maxRetries: 5,
2123
+ retryDelay: 20
2124
+ });
2125
+ } catch {}
2126
+ this.#configs.clear();
2127
+ this.#mirrored.clear();
2128
+ this.#drafts.clear();
2129
+ }
2130
+ async #warm() {
2131
+ this.#sweep();
2132
+ this.#createMirror();
2133
+ this.#refresh();
2134
+ this.#refuseDestroyed();
2135
+ await Promise.all(this.#projects().map((project) => this.#check(project, [])));
1912
2136
  }
1913
2137
  #projects() {
1914
2138
  const projects = ["tsconfig.json"];
@@ -1924,162 +2148,291 @@ var TypeStage = class {
1924
2148
  }
1925
2149
  return projects;
1926
2150
  }
1927
- async #unblock() {
1928
- await (0, node_timers_promises.setTimeout)(0);
2151
+ #refuseDestroyed() {
1929
2152
  if (this.#closing !== void 0) throw (0, _src_core.createDestroyedError)("type stage");
1930
2153
  }
1931
- #record(draft, overlay) {
1932
- overlay.set(resolveWorkspaceFile(this.#workspace, draft.path), draft.text);
1933
- }
1934
- #service(typescript, project) {
1935
- const path = resolveWorkspaceFile(this.#workspace, project);
1936
- const existing = this.#services.get(path);
1937
- if (existing !== void 0) return existing;
1938
- const spelling = normalizePath(path);
1939
- const config = typescript.readConfigFile(spelling, typescript.sys.readFile);
1940
- if (config.error !== void 0) throw new _src_core.ProbeError(this.#translate(typescript, config.error.messageText), {
1941
- origin: "workspace",
1942
- code: "malformed",
1943
- context: {
1944
- stage: this.stage,
1945
- project
1946
- }
2154
+ #createMirror() {
2155
+ const marker = resolveWorkspaceFile(this.#workspace, `${_src_core.TYPE_MIRROR}/${this.#revision}/.probe/mirror.txt`, true);
2156
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(marker), { recursive: true });
2157
+ (0, node_fs.writeFileSync)(marker, (0, _src_core.formatSpecification)("", this.#revision), {
2158
+ encoding: "utf8",
2159
+ flag: "wx"
1947
2160
  });
1948
- const parsed = typescript.parseJsonConfigFileContent(config.config, typescript.sys, (0, node_path.dirname)(spelling), void 0, spelling);
1949
- if (parsed.errors.length > 0) throw new _src_core.ProbeError(this.#translate(typescript, parsed.errors[0]?.messageText), {
1950
- origin: "workspace",
1951
- code: "malformed",
1952
- context: {
1953
- stage: this.stage,
1954
- project
1955
- }
1956
- });
1957
- this.#options.set(path, parsed.options);
1958
- this.#files.set(path, parsed.fileNames);
1959
- const host = {
1960
- getCompilationSettings: this.#compilationSettings.bind(this, path),
1961
- getScriptFileNames: this.#scriptFiles.bind(this, path),
1962
- getScriptVersion: this.#version.bind(this),
1963
- getScriptSnapshot: this.#snapshot.bind(this, typescript),
1964
- getCurrentDirectory: this.#directory.bind(this),
1965
- getDefaultLibFileName: this.#defaultLibrary.bind(this, typescript),
1966
- fileExists: this.#fileExists.bind(this, typescript),
1967
- readFile: this.#readFile.bind(this, typescript),
1968
- readDirectory: typescript.sys.readDirectory,
1969
- directoryExists: this.#directoryExists.bind(this, typescript),
1970
- getDirectories: typescript.sys.getDirectories,
1971
- useCaseSensitiveFileNames: this.#caseSensitive.bind(this, typescript),
1972
- getNewLine: this.#newline.bind(this, typescript)
1973
- };
1974
- const service = typescript.createLanguageService(host);
1975
- this.#services.set(path, service);
1976
- if (!this.#resident.has(path)) this.#recycle(path);
1977
- return service;
1978
- }
1979
- #compilationSettings(path) {
1980
- return this.#options.get(path) ?? {};
1981
- }
1982
- #scriptFiles(path) {
1983
- return [...this.#files.get(path) ?? [], ...this.#overlay.paths];
1984
- }
1985
- #directory() {
1986
- return this.#workspace;
1987
- }
1988
- #defaultLibrary(typescript, options) {
1989
- return typescript.getDefaultLibFilePath(options);
1990
- }
1991
- #fileExists(typescript, file) {
1992
- return this.#overlay.text(file) !== void 0 || typescript.sys.fileExists(file);
1993
- }
1994
- #readFile(typescript, file) {
1995
- return this.#overlay.text(file) ?? typescript.sys.readFile(file);
1996
2161
  }
1997
- #directoryExists(typescript, directory) {
1998
- return typescript.sys.directoryExists(directory) || this.#overlay.covers(directory);
1999
- }
2000
- #caseSensitive(typescript) {
2001
- return typescript.sys.useCaseSensitiveFileNames;
2162
+ #sweep() {
2163
+ const root = resolveWorkspaceFile(this.#workspace, _src_core.TYPE_MIRROR);
2164
+ let entries = [];
2165
+ try {
2166
+ entries = (0, node_fs.readdirSync)(root, { withFileTypes: true });
2167
+ } catch {
2168
+ return;
2169
+ }
2170
+ for (const entry of entries) {
2171
+ if (!entry.isDirectory()) continue;
2172
+ const owner = parseRevisionOwner(entry.name);
2173
+ if (owner === void 0 || matchesLiveProcess(owner)) continue;
2174
+ const marker = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readFileSync)((0, node_path.join)(root, entry.name, ".probe", "mirror.txt"), "utf8"));
2175
+ if (!marker.success || !(0, _src_core.matchesSpecification)(marker.value, entry.name)) continue;
2176
+ try {
2177
+ (0, node_fs.rmSync)((0, node_path.join)(root, entry.name), {
2178
+ recursive: true,
2179
+ force: true,
2180
+ maxRetries: 5,
2181
+ retryDelay: 20
2182
+ });
2183
+ } catch {}
2184
+ }
2002
2185
  }
2003
- #newline(typescript) {
2004
- return typescript.sys.newLine;
2186
+ #refresh() {
2187
+ const present = /* @__PURE__ */ new Set();
2188
+ for (const path of collectWorkspaceFiles(this.#workspace)) {
2189
+ const contained = relativeWorkspaceFile(this.#workspace, path);
2190
+ present.add(contained);
2191
+ const reading = (0, _orkestrel_contract.attempt)(() => (0, node_fs.readFileSync)(path));
2192
+ if (!reading.success) continue;
2193
+ const digest = (0, node_crypto.createHash)("sha256").update(reading.value).digest("hex");
2194
+ if (this.#mirrored.get(contained) === digest) continue;
2195
+ this.#place(reading.value, path);
2196
+ this.#mirrored.set(contained, digest);
2197
+ }
2198
+ for (const contained of [...this.#mirrored.keys()]) {
2199
+ if (present.has(contained)) continue;
2200
+ this.#remove(contained);
2201
+ this.#mirrored.delete(contained);
2202
+ }
2005
2203
  }
2006
- #translate(typescript, message) {
2007
- return relativeWorkspaceMessage(this.#workspace, typescript.flattenDiagnosticMessageText(message, "\n"));
2204
+ #release() {
2205
+ for (const contained of this.#drafts) {
2206
+ this.#remove(contained);
2207
+ this.#mirrored.delete(contained);
2208
+ }
2209
+ this.#drafts.clear();
2210
+ }
2211
+ #place(text, path) {
2212
+ const contained = relativeWorkspaceFile(this.#workspace, path);
2213
+ const target = this.#mirrorPath(contained);
2214
+ if ((0, _orkestrel_contract.attempt)(() => {
2215
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(target), { recursive: true });
2216
+ (0, node_fs.writeFileSync)(target, text);
2217
+ }).success) return contained;
2218
+ this.#displace(target);
2219
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(target), { recursive: true });
2220
+ (0, node_fs.writeFileSync)(target, text);
2221
+ return contained;
2222
+ }
2223
+ #displace(target) {
2224
+ const ancestors = [];
2225
+ let directory = (0, node_path.dirname)(target);
2226
+ while (directory !== this.#mirror && directory !== (0, node_path.dirname)(directory)) {
2227
+ ancestors.unshift(directory);
2228
+ directory = (0, node_path.dirname)(directory);
2229
+ }
2230
+ for (const ancestor of [...ancestors, target]) {
2231
+ const reading = (0, _orkestrel_contract.attempt)(() => (0, node_fs.lstatSync)(ancestor));
2232
+ if (!reading.success) continue;
2233
+ if (ancestor !== target && reading.value.isDirectory()) continue;
2234
+ if (ancestor === target && !reading.value.isDirectory()) continue;
2235
+ (0, node_fs.rmSync)(ancestor, {
2236
+ recursive: true,
2237
+ force: true,
2238
+ maxRetries: 5,
2239
+ retryDelay: 20
2240
+ });
2241
+ this.#mirrored.delete(relativeWorkspaceFile(this.#mirror, ancestor));
2242
+ }
2008
2243
  }
2009
- #configure(service, project) {
2010
- const path = resolveWorkspaceFile(this.#workspace, project);
2011
- if (this.#diagnostics.has(path)) return;
2012
- this.#diagnostics.set(path, service.getCompilerOptionsDiagnostics());
2244
+ #mirrorPath(contained) {
2245
+ return (0, node_path.join)(this.#mirror, contained);
2013
2246
  }
2014
- #recycle(path) {
2015
- const previous = this.#recycled;
2016
- this.#recycled = path;
2017
- if (previous === void 0 || previous === path) return;
2018
- this.#services.get(previous)?.dispose();
2019
- this.#services.delete(previous);
2020
- this.#options.delete(previous);
2021
- this.#files.delete(previous);
2022
- this.#diagnostics.delete(previous);
2247
+ #contain(project) {
2248
+ return relativeWorkspaceFile(this.#workspace, resolveWorkspaceFile(this.#workspace, project));
2023
2249
  }
2024
- #version(file) {
2025
- if (this.#overlay.text(file) !== void 0) return `virtual:${this.#overlay.revision}`;
2250
+ #remove(contained) {
2026
2251
  try {
2027
- return `disk:${(0, node_fs.statSync)(file).mtimeMs}`;
2028
- } catch {
2029
- return "missing";
2030
- }
2252
+ (0, node_fs.rmSync)(this.#mirrorPath(contained), {
2253
+ force: true,
2254
+ maxRetries: 5,
2255
+ retryDelay: 20
2256
+ });
2257
+ } catch {}
2031
2258
  }
2032
- #snapshot(typescript, file) {
2033
- const text = this.#overlay.text(file) ?? typescript.sys.readFile(file);
2034
- return text === void 0 ? void 0 : typescript.ScriptSnapshot.fromString(text);
2035
- }
2036
- #issues(typescript, service, draft, project, selected, configure) {
2037
- const path = resolveWorkspaceFile(this.#workspace, draft.path);
2038
- const configuration = resolveWorkspaceFile(this.#workspace, project);
2039
- return [
2040
- ...configure ? this.#diagnostics.get(configuration) ?? [] : [],
2041
- ...service.getSyntacticDiagnostics(path),
2042
- ...service.getSemanticDiagnostics(path)
2043
- ].map((diagnostic) => this.#issue(typescript, diagnostic, project, selected));
2044
- }
2045
- #issue(typescript, diagnostic, project, selected) {
2046
- const message = this.#translate(typescript, diagnostic.messageText);
2047
- if (diagnostic.file === void 0) {
2048
- if (selected) throw new _src_core.ProbeError(message, {
2049
- origin: "claimant",
2050
- code: "refused",
2259
+ async #configure(project) {
2260
+ const contained = this.#contain(project);
2261
+ const existing = this.#configs.get(contained);
2262
+ if (existing !== void 0) return existing;
2263
+ this.#refresh();
2264
+ const execution = await this.#spawn([
2265
+ "--showConfig",
2266
+ "-p",
2267
+ contained
2268
+ ], this.#mirror);
2269
+ this.#refuseDestroyed();
2270
+ const config = parseProjectConfig(execution.stdout);
2271
+ if (config === void 0) throw this.#fault(execution, contained);
2272
+ this.#configs.set(contained, config);
2273
+ return config;
2274
+ }
2275
+ async #check(project, drafts) {
2276
+ const config = await this.#configure(project);
2277
+ const scratch = this.#scratch(this.#contain(project), config, drafts);
2278
+ const execution = await this.#spawn([
2279
+ "--noEmit",
2280
+ "--pretty",
2281
+ "false",
2282
+ "-p",
2283
+ `./${scratch}`
2284
+ ], this.#mirror);
2285
+ this.#refuseDestroyed();
2286
+ const diagnostics = scanDiagnostics(execution.stdout);
2287
+ if (diagnostics.length === 0 && execution.status !== 0) {
2288
+ const stderr = execution.stderr.trim();
2289
+ const message = stderr !== "" ? stderr : execution.status === void 0 ? "The compiler reported no diagnostic and was ended by a signal" : `The compiler reported no diagnostic and exited ${execution.status}`;
2290
+ throw new _src_core.ProbeError(this.#translate(message), {
2291
+ origin: "instrument",
2292
+ code: "malformed",
2051
2293
  context: {
2052
2294
  stage: this.stage,
2053
2295
  project
2054
2296
  }
2055
2297
  });
2056
- return {
2057
- origin: "workspace",
2058
- path: project,
2059
- message
2060
- };
2061
2298
  }
2062
- const path = relativeWorkspaceFile(this.#workspace, diagnostic.file.fileName);
2063
- if (diagnostic.start === void 0) return {
2064
- origin: "claimant",
2065
- path,
2066
- message
2299
+ return this.#issues(diagnostics, project);
2300
+ }
2301
+ #scratch(project, config, drafts) {
2302
+ const directory = (0, node_path.dirname)(project);
2303
+ const stem = `${(0, node_path.basename)(project, (0, node_path.extname)(project))}.probe.json`;
2304
+ const contained = directory === "." ? stem : `${directory}/${stem}`;
2305
+ const target = this.#mirrorPath(contained);
2306
+ const buildinfo = (0, node_path.join)(this.#mirror, ".probe", `${project.replaceAll("/", "-")}.tsbuildinfo`);
2307
+ const files = [...(config.files ?? []).filter((entry) => (0, node_fs.existsSync)((0, node_path.resolve)((0, node_path.dirname)(target), entry))), ...drafts.map((draft) => relativeWorkspaceFile((0, node_path.dirname)(target), this.#mirrorPath(this.#contain(draft.path))))];
2308
+ const body = {
2309
+ extends: `./${(0, node_path.basename)(project)}`,
2310
+ compilerOptions: {
2311
+ noEmit: true,
2312
+ declaration: false,
2313
+ emitDeclarationOnly: false,
2314
+ composite: false,
2315
+ incremental: true,
2316
+ tsBuildInfoFile: relativeWorkspaceFile((0, node_path.dirname)(target), buildinfo)
2317
+ },
2318
+ ...files.length === 0 ? {} : { files }
2067
2319
  };
2068
- return {
2069
- origin: "claimant",
2070
- path,
2071
- message,
2072
- range: {
2073
- start: diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start),
2074
- end: diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start + (diagnostic.length ?? 0))
2320
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(target), { recursive: true });
2321
+ (0, node_fs.writeFileSync)(target, `${JSON.stringify(body, void 0, " ")}\n`, "utf8");
2322
+ return contained;
2323
+ }
2324
+ #issues(diagnostics, project) {
2325
+ const issues = [];
2326
+ for (const diagnostic of diagnostics) {
2327
+ const message = this.#translate(diagnostic.message);
2328
+ const resolved = diagnostic.path === void 0 ? void 0 : (0, node_path.resolve)(this.#mirror, diagnostic.path);
2329
+ const drafted = resolved !== void 0 && this.#drafts.has(escapesRoot(this.#mirror, resolved) ? relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved));
2330
+ if (resolved === void 0 || !drafted && (0, node_path.extname)(resolved) === ".json") throw new _src_core.ProbeError(message, {
2331
+ origin: "workspace",
2332
+ code: "malformed",
2333
+ context: {
2334
+ stage: this.stage,
2335
+ project
2336
+ }
2337
+ });
2338
+ const path = escapesRoot(this.#mirror, resolved) ? escapesRoot(this.#workspace, resolved) ? normalizePath(resolved) : relativeWorkspaceFile(this.#workspace, resolved) : relativeWorkspaceFile(this.#mirror, resolved);
2339
+ issues.push({
2340
+ origin: "claimant",
2341
+ path,
2342
+ message,
2343
+ ...diagnostic.range === void 0 ? {} : { range: diagnostic.range }
2344
+ });
2345
+ }
2346
+ return issues;
2347
+ }
2348
+ #translate(message) {
2349
+ return relativeWorkspaceMessage(this.#workspace, relativeWorkspaceMessage(this.#mirror, message));
2350
+ }
2351
+ #fault(execution, project) {
2352
+ const reported = scanDiagnostics(execution.stdout).map((diagnostic) => diagnostic.message);
2353
+ const text = reported.length > 0 ? reported.join("\n") : execution.stdout.trim();
2354
+ return new _src_core.ProbeError(this.#translate(text === "" ? "The compiler printed no configuration" : text), {
2355
+ origin: "workspace",
2356
+ code: "malformed",
2357
+ context: {
2358
+ stage: this.stage,
2359
+ project
2075
2360
  }
2076
- };
2361
+ });
2362
+ }
2363
+ #spawn(args, cwd) {
2364
+ this.#refuseDestroyed();
2365
+ return new Promise((settle, refuse) => {
2366
+ const child = (0, node_child_process.spawn)(process.execPath, [this.#compiler, ...args], {
2367
+ cwd,
2368
+ stdio: [
2369
+ "ignore",
2370
+ "pipe",
2371
+ "pipe"
2372
+ ]
2373
+ });
2374
+ const output = child.stdout;
2375
+ const errors = child.stderr;
2376
+ if (output === null || errors === null) {
2377
+ this.#terminate(child);
2378
+ refuse(new _src_core.ProbeError("The compiler was spawned without its own output streams", {
2379
+ origin: "instrument",
2380
+ code: "malformed",
2381
+ context: { stage: this.stage }
2382
+ }));
2383
+ return;
2384
+ }
2385
+ this.#children.add(child);
2386
+ let stdout = "";
2387
+ let stderr = "";
2388
+ output.setEncoding("utf8");
2389
+ errors.setEncoding("utf8");
2390
+ output.on("data", (chunk) => {
2391
+ stdout += chunk;
2392
+ });
2393
+ errors.on("data", (chunk) => {
2394
+ stderr += chunk;
2395
+ });
2396
+ child.on("error", (error) => {
2397
+ this.#children.delete(child);
2398
+ refuse(new _src_core.ProbeError("The workspace compiler could not be started", {
2399
+ origin: "workspace",
2400
+ code: "malformed",
2401
+ context: {
2402
+ stage: this.stage,
2403
+ path: this.#compiler
2404
+ },
2405
+ cause: error
2406
+ }));
2407
+ });
2408
+ child.on("close", (code) => {
2409
+ this.#children.delete(child);
2410
+ settle({
2411
+ ...code === null ? {} : { status: code },
2412
+ stdout,
2413
+ stderr
2414
+ });
2415
+ });
2416
+ });
2417
+ }
2418
+ #terminate(child) {
2419
+ const id = child.pid;
2420
+ if (process.platform === "win32" && id !== void 0) {
2421
+ (0, node_child_process.spawnSync)("taskkill", [
2422
+ "/pid",
2423
+ String(id),
2424
+ "/t",
2425
+ "/f"
2426
+ ], { stdio: "ignore" });
2427
+ return;
2428
+ }
2429
+ child.kill("SIGTERM");
2077
2430
  }
2078
2431
  };
2079
2432
  //#endregion
2080
2433
  //#region src/server/Probe.ts
2081
2434
  /**
2082
- * Answers claims through resident TypeScript, Oxlint, and Vitest stages.
2435
+ * Answers claims through its type, lint, and runtime stages.
2083
2436
  *
2084
2437
  * @remarks
2085
2438
  * Construction resolves the target workspace's toolchain and begins warming every stage. The boot
@@ -2091,10 +2444,45 @@ var TypeStage = class {
2091
2444
  * claim costs that claim rather than the process. A failed boot is replaced the same way: the next
2092
2445
  * claim runs the controls again rather than inheriting a refusal.
2093
2446
  *
2094
- * @example
2447
+ * @example The claim that earns a receipt
2095
2448
  * ```ts
2096
- * const probe = new Probe({ workspace: '/srv/checkout' })
2449
+ * import type { Claim } from '@orkestrel/probe'
2450
+ * import { Probe } from '@orkestrel/probe/server'
2451
+ *
2452
+ * const claim: Claim = {
2453
+ * project: 'configs/src/tsconfig.core.json',
2454
+ * case: {
2455
+ * files: [
2456
+ * {
2457
+ * path: 'src/core/factories.ts',
2458
+ * text: "export function createGreeting(): string {\n\treturn 'hi'\n}\n",
2459
+ * },
2460
+ * ],
2461
+ * test: {
2462
+ * path: 'tmp/probe/greeting.test.ts',
2463
+ * text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
2464
+ * },
2465
+ * },
2466
+ * control: {
2467
+ * files: [
2468
+ * {
2469
+ * path: 'src/core/factories.ts',
2470
+ * text: "export function createGreeting(): number {\n\treturn 'hi'\n}\n",
2471
+ * },
2472
+ * ],
2473
+ * test: {
2474
+ * path: 'tmp/probe/greeting.test.ts',
2475
+ * text: "import { expect, test } from 'vitest'\nimport { createGreeting } from '../../src/core/factories.js'\ntest('greets', () => expect(createGreeting()).toBe('hi'))\n",
2476
+ * },
2477
+ * stage: 'type',
2478
+ * reason: 'a string returned as a number must not compile',
2479
+ * },
2480
+ * }
2481
+ *
2482
+ * const probe = new Probe({ workspace: process.cwd() })
2097
2483
  * const verdict = await probe.prove(claim)
2484
+ * verdict.digest // 'fcb88a2dee987b8673c1fc7107979470'
2485
+ * verdict.receipt // 'probe:fcb88a2dee987b8673c1fc7107979470:type:typescript@6.0.3:oxlint@1.82.0:vitest@4.1.11:configs/src/tsconfig.core.json@434f59254d58cf2683d453a26bd0d837'
2098
2486
  * await probe.destroy()
2099
2487
  * ```
2100
2488
  */
@@ -2115,7 +2503,7 @@ var Probe = class {
2115
2503
  #arming;
2116
2504
  #closing;
2117
2505
  /**
2118
- * Resolves the target toolchain and starts warming the resident stages.
2506
+ * Resolves the target toolchain and starts warming every stage.
2119
2507
  *
2120
2508
  * @param options - Workspace, deadline, and initial observation hooks
2121
2509
  */
@@ -2280,7 +2668,7 @@ var Probe = class {
2280
2668
  files: [],
2281
2669
  test: typeTest,
2282
2670
  stage: "type",
2283
- reason: "the imported type changed after the resident type host cached it"
2671
+ reason: "the imported type changed on disk between the two inspections"
2284
2672
  }
2285
2673
  };
2286
2674
  const runtimeClaim = {
@@ -2527,7 +2915,9 @@ var Probe = class {
2527
2915
  //#endregion
2528
2916
  //#region src/server/ProbeServer.ts
2529
2917
  /**
2530
- * Serves one probe over this process's Model Context Protocol stdio transport.
2918
+ * Implements `ProbeServerInterface` over a `PassThrough` stream this server owns, binding the
2919
+ * published `prove` tool and the dual-era dispatcher to this process's Model Context Protocol
2920
+ * stdio transport.
2531
2921
  *
2532
2922
  * @remarks
2533
2923
  * Construction creates the probe, publishes the `prove` tool, and binds the dual-era dispatcher to
@@ -2701,20 +3091,25 @@ exports.RuntimeStage = RuntimeStage;
2701
3091
  exports.TypeStage = TypeStage;
2702
3092
  exports.buildRevisionPath = buildRevisionPath;
2703
3093
  exports.captureListeners = captureListeners;
3094
+ exports.collectWorkspaceFiles = collectWorkspaceFiles;
2704
3095
  exports.computeDigest = computeDigest;
2705
3096
  exports.describeUnknown = describeUnknown;
2706
3097
  exports.escapesRoot = escapesRoot;
3098
+ exports.filterUniqueIssues = filterUniqueIssues;
2707
3099
  exports.findRefusedPaths = findRefusedPaths;
2708
3100
  exports.guardStage = guardStage;
2709
3101
  exports.inferDocumentLanguage = inferDocumentLanguage;
2710
3102
  exports.inferTestProject = inferTestProject;
2711
3103
  exports.inferTypeProject = inferTypeProject;
2712
3104
  exports.isRefusedName = isRefusedName;
2713
- exports.loadWorkspaceModule = loadWorkspaceModule;
3105
+ exports.loadWorkspaceVitest = loadWorkspaceVitest;
3106
+ exports.matchesLiveProcess = matchesLiveProcess;
2714
3107
  exports.matchesWorkspaceModule = matchesWorkspaceModule;
2715
3108
  exports.normalizePath = normalizePath;
2716
3109
  exports.normalizeValue = normalizeValue;
2717
3110
  exports.overwriteFile = overwriteFile;
3111
+ exports.parseProjectConfig = parseProjectConfig;
3112
+ exports.parseRevisionOwner = parseRevisionOwner;
2718
3113
  exports.readFaultCode = readFaultCode;
2719
3114
  exports.readWorkspaceManifest = readWorkspaceManifest;
2720
3115
  exports.relativeWorkspaceFile = relativeWorkspaceFile;
@@ -2723,5 +3118,6 @@ exports.releaseListeners = releaseListeners;
2723
3118
  exports.resolveWorkspaceBinary = resolveWorkspaceBinary;
2724
3119
  exports.resolveWorkspaceFile = resolveWorkspaceFile;
2725
3120
  exports.resolveWorkspaceModule = resolveWorkspaceModule;
3121
+ exports.scanDiagnostics = scanDiagnostics;
2726
3122
 
2727
3123
  //# sourceMappingURL=index.cjs.map