@ttsc/unplugin 0.28.5 → 0.29.0

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.
Files changed (86) hide show
  1. package/README.md +15 -11
  2. package/lib/api.js +10 -0
  3. package/lib/api.js.map +1 -1
  4. package/lib/api.mjs +3 -2
  5. package/lib/api.mjs.map +1 -1
  6. package/lib/bun-register.d.cts +10 -7
  7. package/lib/bun-register.d.mts +10 -7
  8. package/lib/bun-register.d.ts +10 -7
  9. package/lib/bun-register.js +90 -24
  10. package/lib/bun-register.js.map +1 -1
  11. package/lib/bun-register.mjs +90 -24
  12. package/lib/bun-register.mjs.map +1 -1
  13. package/lib/bun.d.cts +10 -8
  14. package/lib/bun.d.mts +10 -8
  15. package/lib/bun.d.ts +10 -8
  16. package/lib/bun.js +19 -17
  17. package/lib/bun.js.map +1 -1
  18. package/lib/bun.mjs +20 -18
  19. package/lib/bun.mjs.map +1 -1
  20. package/lib/core/index.d.cts +10 -8
  21. package/lib/core/index.d.mts +10 -8
  22. package/lib/core/index.d.ts +10 -8
  23. package/lib/core/index.js +88 -16
  24. package/lib/core/index.js.map +1 -1
  25. package/lib/core/index.mjs +81 -18
  26. package/lib/core/index.mjs.map +1 -1
  27. package/lib/core/projectDiscovery.d.cts +55 -0
  28. package/lib/core/projectDiscovery.d.mts +55 -0
  29. package/lib/core/projectDiscovery.d.ts +55 -0
  30. package/lib/core/projectDiscovery.js +187 -0
  31. package/lib/core/projectDiscovery.js.map +1 -0
  32. package/lib/core/projectDiscovery.mjs +182 -0
  33. package/lib/core/projectDiscovery.mjs.map +1 -0
  34. package/lib/core/sourceExtensions.d.cts +10 -0
  35. package/lib/core/sourceExtensions.d.mts +10 -0
  36. package/lib/core/sourceExtensions.d.ts +10 -0
  37. package/lib/core/sourceExtensions.js +38 -0
  38. package/lib/core/sourceExtensions.js.map +1 -0
  39. package/lib/core/sourceExtensions.mjs +32 -0
  40. package/lib/core/sourceExtensions.mjs.map +1 -0
  41. package/lib/core/transform.d.cts +96 -16
  42. package/lib/core/transform.d.mts +96 -16
  43. package/lib/core/transform.d.ts +96 -16
  44. package/lib/core/transform.js +1295 -309
  45. package/lib/core/transform.js.map +1 -1
  46. package/lib/core/transform.mjs +1290 -310
  47. package/lib/core/transform.mjs.map +1 -1
  48. package/lib/core/tsconfigPaths.d.cts +55 -3
  49. package/lib/core/tsconfigPaths.d.mts +55 -3
  50. package/lib/core/tsconfigPaths.d.ts +55 -3
  51. package/lib/core/tsconfigPaths.js +260 -46
  52. package/lib/core/tsconfigPaths.js.map +1 -1
  53. package/lib/core/tsconfigPaths.mjs +255 -47
  54. package/lib/core/tsconfigPaths.mjs.map +1 -1
  55. package/lib/core/viteServe.d.cts +18 -21
  56. package/lib/core/viteServe.d.mts +18 -21
  57. package/lib/core/viteServe.d.ts +18 -21
  58. package/lib/core/viteServe.js +88 -47
  59. package/lib/core/viteServe.js.map +1 -1
  60. package/lib/core/viteServe.mjs +89 -49
  61. package/lib/core/viteServe.mjs.map +1 -1
  62. package/lib/next.d.cts +7 -0
  63. package/lib/next.d.mts +7 -0
  64. package/lib/next.d.ts +7 -0
  65. package/lib/next.js +186 -54
  66. package/lib/next.js.map +1 -1
  67. package/lib/next.mjs +186 -55
  68. package/lib/next.mjs.map +1 -1
  69. package/lib/turbopack.d.cts +6 -4
  70. package/lib/turbopack.d.mts +6 -4
  71. package/lib/turbopack.d.ts +6 -4
  72. package/lib/turbopack.js +11 -10
  73. package/lib/turbopack.js.map +1 -1
  74. package/lib/turbopack.mjs +11 -10
  75. package/lib/turbopack.mjs.map +1 -1
  76. package/package.json +7 -4
  77. package/src/bun-register.ts +112 -25
  78. package/src/bun.ts +60 -50
  79. package/src/core/index.ts +114 -19
  80. package/src/core/projectDiscovery.ts +256 -0
  81. package/src/core/sourceExtensions.ts +47 -0
  82. package/src/core/transform.ts +1671 -361
  83. package/src/core/tsconfigPaths.ts +406 -46
  84. package/src/core/viteServe.ts +125 -74
  85. package/src/next.ts +230 -54
  86. package/src/turbopack.ts +11 -10
@@ -7,8 +7,10 @@ var os = require('node:os');
7
7
  var path = require('node:path');
8
8
  var ttsc = require('ttsc');
9
9
  var pathIdentity = require('ttsc/path-identity');
10
+ var projectDiscovery = require('./projectDiscovery.js');
10
11
  var tsconfigPaths = require('./tsconfigPaths.js');
11
12
 
13
+ const TTSC_SEMANTIC_CONFIG_PATH = "TTSC_SEMANTIC_CONFIG_PATH";
12
14
  /**
13
15
  * A verdict about one generation that later deliveries replay instead of
14
16
  * repeating the whole compile behind it.
@@ -110,6 +112,8 @@ class TtscPassVerdictError extends TtscTerminalGenerationError {
110
112
  const TRANSFORM_GENERATION_FAILURES = new WeakMap();
111
113
  /** Retry baselines retained only for attempts that could not be published. */
112
114
  const TRANSFORM_FAILED_GENERATION_VALIDATIONS = new WeakMap();
115
+ /** Internal retained clock-probe ownership for published generations. */
116
+ const TRANSFORM_CLOCK_REFERENCE_DIRECTORIES = new WeakMap();
113
117
  /** Cache promises whose unchanged terminal verdict may be replayed. */
114
118
  const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap();
115
119
  /** Maximum witnesses printed and retained for each failed transform attempt. */
@@ -245,8 +249,7 @@ function clearTtscTransformCache(cache) {
245
249
  * @param id - Bundler module id (may carry a query string or virtual prefix).
246
250
  * @param source - Current file content supplied by the bundler.
247
251
  * @param options - Resolved plugin options.
248
- * @param aliases - Raw bundler alias configuration (Vite array or webpack
249
- * object).
252
+ * @param aliases - Raw Vite alias configuration (object or array).
250
253
  * @param cache - Optional project cache. Callers with a real `buildStart`
251
254
  * boundary declare it through {@link beginTtscTransformBuild}; other hosts
252
255
  * retain persistent validation.
@@ -346,9 +349,11 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
346
349
  }
347
350
  // The compile is fine and simply has nothing for this module, so the
348
351
  // module goes back to the host untransformed rather than failing the
349
- // build (samchon/ttsc#1308). It still counts as delivered in this
350
- // pass, and there is nothing to watch for a file with no output.
352
+ // build (samchon/ttsc#1308). Its project config and selection inputs
353
+ // still decide whether a later generation will contain this module,
354
+ // so hosts must receive the same universal watch-input batch.
351
355
  reportMissingProgramOutput(cached, error, epoch);
356
+ notifyWatchInputs(hooks, cached, file);
352
357
  markCachedSourceServed(cached, file);
353
358
  return undefined;
354
359
  }
@@ -404,6 +409,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
404
409
  throw error;
405
410
  }
406
411
  reportMissingProgramOutput(cached, error, epoch);
412
+ notifyWatchInputs(hooks, cached, file);
407
413
  markCachedSourceServed(cached, file);
408
414
  return undefined;
409
415
  }
@@ -555,7 +561,7 @@ function evictGeneration(cache, key, generation) {
555
561
  void generation.then(disposeCachedTransform, () => undefined);
556
562
  }
557
563
  }
558
- /** Close one generation's directory watchers exactly once. */
564
+ /** Release one generation's watchers and retained clock probe exactly once. */
559
565
  function disposeCachedTransform(cached) {
560
566
  const trackers = [
561
567
  cached.projectMutationTracker,
@@ -565,8 +571,21 @@ function disposeCachedTransform(cached) {
565
571
  cached.projectMutationTracker = undefined;
566
572
  cached.hostInputMutationTracker = undefined;
567
573
  cached.candidateMutationTracker = undefined;
568
- for (const tracker of trackers)
569
- tracker?.close();
574
+ const clockReferenceDirectory = TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached);
575
+ TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.delete(cached);
576
+ for (const tracker of trackers) {
577
+ try {
578
+ tracker?.close();
579
+ }
580
+ catch {
581
+ // Disposal is scheduled behind fulfilled generation Promises, so it has
582
+ // no caller that can recover from a close failure. Keep releasing every
583
+ // independent resource and leave no rejected cleanup Promise behind.
584
+ }
585
+ }
586
+ if (clockReferenceDirectory !== undefined) {
587
+ disposeFilesystemClockReference(clockReferenceDirectory);
588
+ }
570
589
  }
571
590
  /**
572
591
  * Derivation states keyed by the compiler result object. One result object is
@@ -597,17 +616,22 @@ function envelopeGraphIndexes(state, props) {
597
616
  if (state.graph !== undefined) {
598
617
  return state.graph;
599
618
  }
619
+ const platform = resultFilesystem(props.result).platform ?? process.platform;
620
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
600
621
  const built = {
601
622
  edges: new Map(),
602
623
  spellings: new Map(),
603
624
  candidates: [],
604
625
  globals: [],
605
626
  configs: [],
606
- members: new Set(),
627
+ resolutionInputs: [],
628
+ memberSpellings: new Set(),
607
629
  speculative: new Set(),
608
630
  inputProofs: new Map(),
609
631
  inputProofFailures: new Map(),
610
632
  inputProofConflicts: new Set(),
633
+ inputObservations: new Map(),
634
+ inputObservationConflicts: new Set(),
611
635
  };
612
636
  const graph = props.result.type === "exception" ? undefined : props.result.graph;
613
637
  if (graph !== undefined) {
@@ -617,7 +641,7 @@ function envelopeGraphIndexes(state, props) {
617
641
  }
618
642
  const absolute = path.resolve(props.projectRoot, source);
619
643
  const identity = derivationIdentity(state, absolute);
620
- built.members.add(identity);
644
+ built.memberSpellings.add(absolute);
621
645
  built.spellings.set(identity, absolute);
622
646
  const entries = built.edges.get(identity) ?? [];
623
647
  entries.push(...targets
@@ -625,7 +649,7 @@ function envelopeGraphIndexes(state, props) {
625
649
  .map((target) => {
626
650
  const absoluteTarget = path.resolve(props.projectRoot, target);
627
651
  const targetIdentity = derivationIdentity(state, absoluteTarget);
628
- built.members.add(targetIdentity);
652
+ built.memberSpellings.add(absoluteTarget);
629
653
  if (!built.spellings.has(targetIdentity)) {
630
654
  built.spellings.set(targetIdentity, absoluteTarget);
631
655
  }
@@ -637,7 +661,7 @@ function envelopeGraphIndexes(state, props) {
637
661
  built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
638
662
  for (const input of [...built.globals, ...built.configs]) {
639
663
  const identity = derivationIdentity(state, input);
640
- built.members.add(identity);
664
+ built.memberSpellings.add(path.resolve(input));
641
665
  if (!built.spellings.has(identity))
642
666
  built.spellings.set(identity, input);
643
667
  }
@@ -649,12 +673,22 @@ function envelopeGraphIndexes(state, props) {
649
673
  for (const [source] of candidateEntries) {
650
674
  const absoluteSource = path.resolve(props.projectRoot, source);
651
675
  const identity = derivationIdentity(state, absoluteSource);
652
- built.members.add(identity);
676
+ built.memberSpellings.add(absoluteSource);
653
677
  if (!built.spellings.has(identity)) {
654
678
  built.spellings.set(identity, absoluteSource);
655
679
  }
656
680
  }
657
- const realized = new Set(built.members);
681
+ const realized = new Set(built.memberSpellings);
682
+ built.resolutionInputs.push(...selectListedFiles(props.projectRoot, graph.resolutionInputs));
683
+ for (const input of built.resolutionInputs) {
684
+ const spelling = path.resolve(input);
685
+ const identity = derivationIdentity(state, input);
686
+ if (!realized.has(spelling))
687
+ built.speculative.add(spelling);
688
+ built.memberSpellings.add(spelling);
689
+ if (!built.spellings.has(identity))
690
+ built.spellings.set(identity, input);
691
+ }
658
692
  for (const [source, candidates] of candidateEntries) {
659
693
  built.candidates.push({
660
694
  source: derivationIdentity(state, path.resolve(props.projectRoot, source)),
@@ -663,26 +697,61 @@ function envelopeGraphIndexes(state, props) {
663
697
  for (const candidate of candidates) {
664
698
  if (typeof candidate !== "string" || candidate.length === 0)
665
699
  continue;
666
- const identity = derivationIdentity(state, path.resolve(props.projectRoot, candidate));
700
+ const absoluteCandidate = path.resolve(props.projectRoot, candidate);
701
+ const identity = derivationIdentity(state, absoluteCandidate);
667
702
  // Edges, globals, configs, and every candidate source are folded in
668
703
  // above, so a path absent from that set is one the envelope reported
669
704
  // only as a candidate.
670
- if (!realized.has(identity))
671
- built.speculative.add(identity);
672
- built.members.add(identity);
705
+ if (!realized.has(absoluteCandidate)) {
706
+ built.speculative.add(absoluteCandidate);
707
+ }
708
+ built.memberSpellings.add(absoluteCandidate);
673
709
  if (!built.spellings.has(identity)) {
674
- built.spellings.set(identity, path.resolve(props.projectRoot, candidate));
710
+ built.spellings.set(identity, absoluteCandidate);
675
711
  }
676
712
  }
677
713
  }
678
- const transformSources = new Set();
714
+ const transformSourceSpellings = new Set();
679
715
  if (props.result.type === "success") {
680
716
  for (const output of Object.keys(props.result.typescript)) {
681
717
  if (!isDeclarationFile(output)) {
682
- transformSources.add(derivationIdentity(state, path.resolve(props.projectRoot, output)));
718
+ const absoluteOutput = path.resolve(props.projectRoot, output);
719
+ transformSourceSpellings.add(absoluteOutput);
683
720
  }
684
721
  }
685
722
  }
723
+ for (const [input, reported] of Object.entries(graph.inputObservations ?? {})) {
724
+ if (input.length === 0)
725
+ continue;
726
+ const absolute = path.resolve(props.projectRoot, input);
727
+ const spelling = path.resolve(absolute);
728
+ if (!built.memberSpellings.has(spelling) &&
729
+ !transformSourceSpellings.has(spelling)) {
730
+ continue;
731
+ }
732
+ const normalized = normalizeGraphInputObservation(reported, platform);
733
+ if (normalized === undefined) {
734
+ built.inputObservationConflicts.add(spelling);
735
+ if (!built.inputProofFailures.has(spelling)) {
736
+ built.inputProofFailures.set(spelling, "malformed-observation");
737
+ }
738
+ continue;
739
+ }
740
+ const previous = built.inputObservations.get(spelling);
741
+ const merged = previous === undefined
742
+ ? normalized
743
+ : mergeGraphInputObservations(previous, normalized);
744
+ if (merged === undefined) {
745
+ built.inputObservations.delete(spelling);
746
+ built.inputObservationConflicts.add(spelling);
747
+ if (!built.inputProofFailures.has(spelling)) {
748
+ built.inputProofFailures.set(spelling, "conflicting-observation");
749
+ }
750
+ }
751
+ else if (!built.inputObservationConflicts.has(spelling)) {
752
+ built.inputObservations.set(spelling, merged);
753
+ }
754
+ }
686
755
  for (const [input, hash] of Object.entries(graph.inputHashes ?? {})) {
687
756
  if (hash !== null &&
688
757
  (typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))) {
@@ -695,28 +764,31 @@ function envelopeGraphIndexes(state, props) {
695
764
  const reportedRealpath = graph.inputRealpaths[input];
696
765
  if (reportedRealpath !== null &&
697
766
  (typeof reportedRealpath !== "string" ||
698
- !path.isAbsolute(reportedRealpath))) {
767
+ !pathApi.isAbsolute(reportedRealpath))) {
699
768
  continue;
700
769
  }
701
770
  const absolute = path.resolve(props.projectRoot, input);
702
- const identity = derivationIdentity(state, absolute);
703
- if (!built.members.has(identity) && !transformSources.has(identity)) {
771
+ const spelling = path.resolve(absolute);
772
+ if (!built.memberSpellings.has(spelling) &&
773
+ !transformSourceSpellings.has(spelling)) {
704
774
  continue;
705
775
  }
706
776
  const proof = {
707
777
  hash,
708
778
  path: absolute,
709
- realpath: reportedRealpath === null ? null : path.resolve(reportedRealpath),
779
+ realpath: reportedRealpath === null
780
+ ? null
781
+ : pathIdentity.resolveFilesystemPath(reportedRealpath, platform),
710
782
  };
711
- const previous = built.inputProofs.get(identity);
783
+ const previous = built.inputProofs.get(spelling);
712
784
  if (previous !== undefined &&
713
785
  (previous.hash !== proof.hash ||
714
786
  !sameHostInputRealpath(previous.realpath, proof.realpath, state.identityContext))) {
715
- built.inputProofs.delete(identity);
716
- built.inputProofConflicts.add(identity);
787
+ built.inputProofs.delete(spelling);
788
+ built.inputProofConflicts.add(spelling);
717
789
  }
718
- else if (!built.inputProofConflicts.has(identity)) {
719
- built.inputProofs.set(identity, proof);
790
+ else if (!built.inputProofConflicts.has(spelling)) {
791
+ built.inputProofs.set(spelling, proof);
720
792
  }
721
793
  }
722
794
  for (const [input, reason] of Object.entries(graph.inputProofFailures ?? {})) {
@@ -724,22 +796,213 @@ function envelopeGraphIndexes(state, props) {
724
796
  continue;
725
797
  }
726
798
  const absolute = path.resolve(props.projectRoot, input);
727
- const identity = derivationIdentity(state, absolute);
728
- if (!built.members.has(identity) && !transformSources.has(identity)) {
799
+ const spelling = path.resolve(absolute);
800
+ if (!built.memberSpellings.has(spelling) &&
801
+ !transformSourceSpellings.has(spelling)) {
802
+ continue;
803
+ }
804
+ const observation = built.inputObservations.get(spelling);
805
+ const legacyProjection = observation === undefined
806
+ ? undefined
807
+ : legacyProjectionOfGraphInputObservation(observation);
808
+ if (built.speculative.has(spelling) &&
809
+ !built.inputProofs.has(spelling) &&
810
+ legacyProjection?.failure === reason) {
729
811
  continue;
730
812
  }
731
- if (built.inputProofs.has(identity)) {
732
- built.inputProofs.delete(identity);
733
- built.inputProofConflicts.add(identity);
813
+ if (built.inputObservations.has(spelling)) {
814
+ built.inputObservations.delete(spelling);
815
+ built.inputObservationConflicts.add(spelling);
734
816
  }
735
- if (!built.inputProofFailures.has(identity)) {
736
- built.inputProofFailures.set(identity, reason);
817
+ if (built.inputProofs.has(spelling)) {
818
+ built.inputProofs.delete(spelling);
819
+ built.inputProofConflicts.add(spelling);
820
+ }
821
+ if (!built.inputProofFailures.has(spelling)) {
822
+ built.inputProofFailures.set(spelling, reason);
737
823
  }
738
824
  }
739
825
  }
740
826
  state.graph = built;
741
827
  return built;
742
828
  }
829
+ /** Normalize one untrusted predicate proof without filling missing predicates. */
830
+ function normalizeGraphInputObservation(value, platform = process.platform) {
831
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
832
+ return undefined;
833
+ }
834
+ const entry = value;
835
+ const observation = {};
836
+ if (Object.prototype.hasOwnProperty.call(entry, "accessibleEntries")) {
837
+ const accessible = entry.accessibleEntries;
838
+ if (typeof accessible !== "object" ||
839
+ accessible === null ||
840
+ Array.isArray(accessible)) {
841
+ return undefined;
842
+ }
843
+ const lists = accessible;
844
+ if (!Array.isArray(lists.directories) ||
845
+ !lists.directories.every((name) => typeof name === "string") ||
846
+ !Array.isArray(lists.files) ||
847
+ !lists.files.every((name) => typeof name === "string")) {
848
+ return undefined;
849
+ }
850
+ observation.accessibleEntries = {
851
+ directories: [...lists.directories],
852
+ files: [...lists.files],
853
+ };
854
+ }
855
+ if (Object.prototype.hasOwnProperty.call(entry, "fileExists")) {
856
+ if (typeof entry.fileExists !== "boolean")
857
+ return undefined;
858
+ observation.fileExists = entry.fileExists;
859
+ }
860
+ if (Object.prototype.hasOwnProperty.call(entry, "directoryExists")) {
861
+ if (typeof entry.directoryExists !== "boolean")
862
+ return undefined;
863
+ observation.directoryExists = entry.directoryExists;
864
+ }
865
+ if (Object.prototype.hasOwnProperty.call(entry, "stat")) {
866
+ if (entry.stat !== "missing" &&
867
+ entry.stat !== "file" &&
868
+ entry.stat !== "directory") {
869
+ return undefined;
870
+ }
871
+ observation.stat = entry.stat;
872
+ }
873
+ if (Object.prototype.hasOwnProperty.call(entry, "readFile")) {
874
+ const read = entry.readFile;
875
+ if (typeof read !== "object" || read === null || Array.isArray(read)) {
876
+ return undefined;
877
+ }
878
+ const result = read;
879
+ if (result.ok === false && result.hash === undefined) {
880
+ observation.readFile = { ok: false };
881
+ }
882
+ else if (result.ok === true &&
883
+ typeof result.hash === "string" &&
884
+ /^[0-9a-f]{64}$/.test(result.hash)) {
885
+ observation.readFile = { hash: result.hash, ok: true };
886
+ }
887
+ else {
888
+ return undefined;
889
+ }
890
+ }
891
+ if (Object.prototype.hasOwnProperty.call(entry, "realpath")) {
892
+ const realpath = entry.realpath;
893
+ if (typeof realpath !== "object" ||
894
+ realpath === null ||
895
+ Array.isArray(realpath)) {
896
+ return undefined;
897
+ }
898
+ const result = realpath;
899
+ if (result.ok === false && result.path === undefined) {
900
+ observation.realpath = { ok: false };
901
+ }
902
+ else if (result.ok === true &&
903
+ typeof result.path === "string" &&
904
+ (platform === "win32" ? path.win32 : path.posix).isAbsolute(result.path)) {
905
+ observation.realpath = {
906
+ ok: true,
907
+ path: pathIdentity.resolveFilesystemPath(result.path, platform),
908
+ };
909
+ }
910
+ else {
911
+ return undefined;
912
+ }
913
+ }
914
+ return Object.keys(observation).length !== 0 &&
915
+ graphInputObservationCompatible(observation)
916
+ ? observation
917
+ : undefined;
918
+ }
919
+ /** Join duplicate lexical keys only when every repeated predicate agrees. */
920
+ function mergeGraphInputObservations(left, right) {
921
+ for (const property of [
922
+ "accessibleEntries",
923
+ "directoryExists",
924
+ "fileExists",
925
+ "readFile",
926
+ "realpath",
927
+ "stat",
928
+ ]) {
929
+ if (left[property] !== undefined &&
930
+ right[property] !== undefined &&
931
+ JSON.stringify(left[property]) !== JSON.stringify(right[property])) {
932
+ return undefined;
933
+ }
934
+ }
935
+ const merged = { ...left, ...right };
936
+ return graphInputObservationCompatible(merged) ? merged : undefined;
937
+ }
938
+ /** Whether one predicate set can describe a single stable filesystem state. */
939
+ function graphInputObservationCompatible(observation) {
940
+ const { accessibleEntries, directoryExists, fileExists, readFile, stat } = observation;
941
+ const hasAccessibleEntries = accessibleEntries !== undefined &&
942
+ (accessibleEntries.directories.length !== 0 ||
943
+ accessibleEntries.files.length !== 0);
944
+ if (hasAccessibleEntries &&
945
+ (fileExists === true ||
946
+ directoryExists === false ||
947
+ (stat !== undefined && stat !== "directory") ||
948
+ readFile?.ok === true)) {
949
+ return false;
950
+ }
951
+ if (fileExists === true && directoryExists === true)
952
+ return false;
953
+ if (stat === "directory" &&
954
+ (fileExists === true || directoryExists === false)) {
955
+ return false;
956
+ }
957
+ if (stat === "file" && (fileExists === false || directoryExists === true)) {
958
+ return false;
959
+ }
960
+ if (stat === "missing" && (fileExists === true || directoryExists === true)) {
961
+ return false;
962
+ }
963
+ if (readFile?.ok === true &&
964
+ (fileExists === false ||
965
+ directoryExists === true ||
966
+ (stat !== undefined && stat !== "file"))) {
967
+ return false;
968
+ }
969
+ return true;
970
+ }
971
+ /**
972
+ * Reconstruct a legacy hash/realpath pair only when the rich predicates prove
973
+ * that projection without collapsing an unreadable file or failed file probe
974
+ * into generic absence.
975
+ */
976
+ function legacyProjectionOfGraphInputObservation(observation) {
977
+ if (observation.readFile?.ok === true) {
978
+ return observation.realpath?.ok === true
979
+ ? {
980
+ hash: observation.readFile.hash,
981
+ realpath: observation.realpath.path,
982
+ }
983
+ : { failure: "realpath-unavailable" };
984
+ }
985
+ const directory = observation.stat === "directory" || observation.directoryExists === true;
986
+ if (directory) {
987
+ return observation.realpath?.ok === true
988
+ ? {
989
+ hash: hashText("ttsc:host-input:directory\0"),
990
+ realpath: observation.realpath.path,
991
+ }
992
+ : { failure: "realpath-unavailable" };
993
+ }
994
+ const file = observation.stat === "file" || observation.fileExists === true;
995
+ if (file) {
996
+ return { failure: "content-unavailable" };
997
+ }
998
+ const missing = observation.stat === "missing" ||
999
+ observation.fileExists === false ||
1000
+ observation.directoryExists === false ||
1001
+ observation.readFile?.ok === false;
1002
+ return missing
1003
+ ? { hash: null, realpath: null }
1004
+ : { failure: "unsupported-input-kind" };
1005
+ }
743
1006
  /**
744
1007
  * {@link pathIdentityKey} memoized inside one envelope's derivation state.
745
1008
  * Callers always pass already-resolved absolute paths, so the input string is a
@@ -774,21 +1037,8 @@ function collectDeclaredIdentities(state, projectRoot, listed) {
774
1037
  return output;
775
1038
  }
776
1039
  /**
777
- * Forward every derived watch input for `file` to the adapter's `addWatchFile`
778
- * hook: the plugin-reported `dependencies[file]` list unioned with the
779
- * host-owned reference graph's contribution (`reach(edges, file)`, `globals`,
780
- * `configs`, and resolution candidates).
781
- *
782
- * Envelope keys mirror the `typescript` keys (project-relative); values may be
783
- * project-relative or absolute. Every path is absolutized against the project
784
- * root and deduplicated; the file itself is dropped (the bundler already
785
- * watches the module it transforms), and so is every path in the disposed
786
- * transform scratch tree (see
787
- * {@link TtscCachedProjectTransform.scratchDirectory}).
788
- */
789
- /**
790
- * Register the failed generation's own project inputs so the host can observe
791
- * the fix.
1040
+ * Register the failed generation's project and external inputs so the host can
1041
+ * observe the fix.
792
1042
  *
793
1043
  * A successful delivery registers the derived watch inputs, which is how a
794
1044
  * type-only file that no bundler graph contains still invalidates the modules
@@ -799,67 +1049,168 @@ function collectDeclaredIdentities(state, projectRoot, listed) {
799
1049
  * arrive: the user repairs a file the bundler does not track, nothing is
800
1050
  * invalidated, and the error stays on screen (samchon/ttsc#1312).
801
1051
  *
802
- * The generation records the project walk even when the compile failed, so the
803
- * files a fix would touch are exactly what it already holds. The cost is paid
804
- * only on a failure, and only until the next compile succeeds and narrows the
805
- * set back to the derived inputs.
1052
+ * A failure envelope can retain exact external input spellings from its graph
1053
+ * and host metadata. A pre-transform typecheck failure may have no graph yet;
1054
+ * its structured diagnostics, or the host's standard diagnostic lines when it
1055
+ * could return only an exception, still name the external files that need a
1056
+ * repair. The cost is paid only on a failure, and only until the next compile
1057
+ * succeeds and narrows the set back to the derived inputs.
806
1058
  */
807
1059
  function notifyFailedGenerationInputs(hooks, cached) {
808
1060
  const addWatchFile = hooks?.addWatchFile;
809
- if (addWatchFile === undefined) {
1061
+ const addWatchFiles = hooks?.addWatchFiles;
1062
+ if (addWatchFile === undefined && addWatchFiles === undefined) {
810
1063
  return;
811
1064
  }
1065
+ const inputs = [];
1066
+ const seen = new Set();
1067
+ const append = (input) => {
1068
+ const spelling = path.resolve(input);
1069
+ if (seen.has(spelling) ||
1070
+ isTransformScratchInput(spelling, cached.scratchDirectory)) {
1071
+ return;
1072
+ }
1073
+ seen.add(spelling);
1074
+ // No evidence, deliberately. A failed generation is replayed for the rest
1075
+ // of its pass without re-proving its inputs, so the adapter must observe
1076
+ // the current availability itself.
1077
+ inputs.push({ file: spelling });
1078
+ };
812
1079
  for (const key of Object.keys(cached.inputHashes)) {
813
- const input = path.resolve(cached.projectRoot, key);
814
- if (isTransformScratchInput(input, cached.scratchDirectory)) {
815
- continue;
1080
+ append(path.resolve(cached.projectRoot, key));
1081
+ }
1082
+ // The project walk deliberately excludes node_modules and cannot reach
1083
+ // sibling-project or out-of-root inputs. The generation already retained
1084
+ // their exact lexical spellings, so keep that ownership on the failure path
1085
+ // instead of deriving a narrower second answer.
1086
+ for (const input of cached.externalInputPaths ?? []) {
1087
+ append(input);
1088
+ }
1089
+ if (cached.result.type === "failure") {
1090
+ for (const diagnostic of cached.result.diagnostics) {
1091
+ if (typeof diagnostic.file === "string" && diagnostic.file.length !== 0) {
1092
+ append(path.resolve(cached.projectRoot, diagnostic.file));
1093
+ }
1094
+ }
1095
+ }
1096
+ else if (cached.result.type === "exception") {
1097
+ for (const diagnostic of selectExceptionDiagnosticFiles(cached.result.error)) {
1098
+ append(path.resolve(cached.projectRoot, diagnostic));
816
1099
  }
817
- // No evidence argument, deliberately. `missing: false` would be a claim
818
- // this path cannot back: a failed generation is replayed for the rest of
819
- // its pass without re-proving its inputs, so the walk that recorded them
820
- // may be older than the delivery, and one of them having been deleted is a
821
- // live reason for that compile to have failed. Letting the adapter probe
822
- // also routes an absent input to the missing-input poll, which is the only
823
- // channel through which restoring it can invalidate anything: a bundler
824
- // watch on a path that does not exist registers nothing, and no module
825
- // graph carries a type-only input. It costs one `existsSync` per input,
826
- // and only where the adapter reads evidence at all, which is Vite serve.
827
- addWatchFile(input);
1100
+ }
1101
+ if (addWatchFiles !== undefined) {
1102
+ addWatchFiles(inputs);
1103
+ return;
1104
+ }
1105
+ for (const input of inputs) {
1106
+ addWatchFile(input.file);
828
1107
  }
829
1108
  }
1109
+ /**
1110
+ * Extract file spellings only from the two standard TypeScript diagnostic
1111
+ * forms.
1112
+ */
1113
+ function selectExceptionDiagnosticFiles(error) {
1114
+ const files = [];
1115
+ for (const line of formatUnknownError(error).split(/\r?\n/)) {
1116
+ const colon = /^(.+):\d+:\d+\s+-\s+(?:error|warning|suggestion|message)\s+TS\d+:\s+.+$/i.exec(line);
1117
+ const parenthesized = /^(.+?)\(\d+,\d+\):\s+(?:error|warning|suggestion|message)\s+TS\d+:\s+.+$/i.exec(line);
1118
+ const file = colon?.[1] ?? parenthesized?.[1];
1119
+ if (file !== undefined && file.trim().length !== 0) {
1120
+ files.push(file.trim());
1121
+ }
1122
+ }
1123
+ return files;
1124
+ }
1125
+ /**
1126
+ * Forward every derived watch input for `file` to the adapter's `addWatchFile`
1127
+ * hook: the plugin-reported `dependencies[file]` list unioned with the
1128
+ * host-owned reference graph's contribution (`reach(edges, file)`, `globals`,
1129
+ * `configs`, and resolution candidates).
1130
+ *
1131
+ * Envelope keys mirror the `typescript` keys (project-relative); values may be
1132
+ * project-relative or absolute. Every path is absolutized against the project
1133
+ * root and deduplicated; the file itself is dropped (the bundler already
1134
+ * watches the module it transforms), and so is every path in the disposed
1135
+ * transform scratch tree (see
1136
+ * {@link TtscCachedProjectTransform.scratchDirectory}).
1137
+ */
830
1138
  function notifyWatchInputs(hooks, cached, file) {
831
1139
  const addWatchFile = hooks?.addWatchFile;
832
- if (addWatchFile === undefined) {
1140
+ const addWatchFiles = hooks?.addWatchFiles;
1141
+ if (addWatchFile === undefined && addWatchFiles === undefined) {
833
1142
  return;
834
1143
  }
835
1144
  const state = envelopeDerivation(cached);
836
1145
  const external = cached.externalInputHashes ?? {};
837
- for (const input of selectWatchInputs({
1146
+ const inputs = selectWatchInputs({
838
1147
  file,
839
1148
  projectRoot: cached.projectRoot,
840
1149
  result: cached.result,
841
1150
  scratchDirectory: cached.scratchDirectory,
842
1151
  temporaryTsconfig: cached.temporaryTsconfig,
843
- })) {
1152
+ }).map((input) => {
844
1153
  // Hand the adapter the identity this generation already resolved and the
845
- // existence state it already recorded. Both are memoized per generation,
846
- // while an adapter deriving them itself pays a `realpath`, a directory
847
- // listing, and an `existsSync` per input on every delivery of every module
848
- // (samchon/ttsc#1246).
1154
+ // exact state it already recorded. Both are memoized per generation, while
1155
+ // an adapter deriving them itself pays repeated filesystem reads and can
1156
+ // accidentally attach a later state to an earlier transform.
849
1157
  const identity = derivationIdentity(state, input);
850
- addWatchFile(input, {
851
- identity,
852
- missing: external[identity] === MISSING_INPUT_STATE,
853
- });
1158
+ const spelling = path.resolve(input);
1159
+ const observation = cached.externalInputObservations?.[spelling];
1160
+ const missing = observation?.fileExists === false ||
1161
+ state.graph?.inputProofs.get(spelling)?.hash === null ||
1162
+ external[identity] === MISSING_INPUT_STATE;
1163
+ const projectKey = toProjectKey(cached.projectRoot, input, state.identityContext);
1164
+ const externalHash = Object.prototype.hasOwnProperty.call(external, identity)
1165
+ ? external[identity]
1166
+ : undefined;
1167
+ const projectHash = Object.prototype.hasOwnProperty.call(cached.inputHashes, projectKey)
1168
+ ? cached.inputHashes[projectKey]
1169
+ : undefined;
1170
+ const graphRealpaths = cached.externalInputRealpaths ?? {};
1171
+ const stateEvidence = observation !== undefined
1172
+ ? { codec: "predicates", observation }
1173
+ : externalHash !== undefined &&
1174
+ Object.prototype.hasOwnProperty.call(graphRealpaths, identity)
1175
+ ? {
1176
+ codec: "graph",
1177
+ hash: externalHash,
1178
+ realpath: graphRealpaths[identity] ?? null,
1179
+ }
1180
+ : externalHash !== undefined
1181
+ ? { codec: "host", hash: externalHash }
1182
+ : projectHash !== undefined
1183
+ ? { codec: "host", hash: projectHash }
1184
+ : undefined;
1185
+ return {
1186
+ file: input,
1187
+ evidence: {
1188
+ identity,
1189
+ missing,
1190
+ ...(stateEvidence === undefined ? {} : { state: stateEvidence }),
1191
+ unavailable: observation?.fileExists === false
1192
+ ? "not-file"
1193
+ : missing
1194
+ ? "missing"
1195
+ : undefined,
1196
+ },
1197
+ };
1198
+ });
1199
+ if (addWatchFiles !== undefined) {
1200
+ addWatchFiles(inputs);
1201
+ return;
1202
+ }
1203
+ for (const input of inputs) {
1204
+ addWatchFile(input.file, input.evidence);
854
1205
  }
855
1206
  }
856
1207
  /**
857
1208
  * Derive the absolute, deduplicated watch-input list for a single file.
858
1209
  *
859
- * By default the derivation is a union: `dependencies[file] reach(edges,
860
- * file) ∪ globals configs`. The plugin-reported list can only widen the
861
- * host-owned language-semantic bound, never narrow it. Resolution candidates
862
- * remain part of that host-owned bound in both modes.
1210
+ * By default the derivation unions plugin dependencies, the reachable graph,
1211
+ * globals, configs, importer resolver inputs, and universal resolver inputs.
1212
+ * The plugin-reported list can only widen the host-owned language-semantic
1213
+ * bound, never narrow it. Resolver inputs remain in that bound in both modes.
863
1214
  *
864
1215
  * An envelope that lists `file` in `dependenciesComplete` narrows it to
865
1216
  * `dependencies[file] ∪ configs`: the plugin declared its reported list the
@@ -871,10 +1222,12 @@ function notifyWatchInputs(hooks, cached, file) {
871
1222
  * volatile keeps the baseline: the two declarations contradict, so the
872
1223
  * conservative one wins.
873
1224
  *
874
- * The derived list is a pure function of the envelope and the file's filesystem
875
- * identity, so it is computed at most once per generation per file: sibling and
876
- * repeated deliveries replay the per-envelope memo ({@link envelopeDerivation})
877
- * instead of re-walking the graph. Returns an empty list on exceptions.
1225
+ * The derived list is a pure function of the envelope and the delivered file's
1226
+ * normalized lexical spelling. Graph traversal uses its filesystem identity,
1227
+ * but lexical inputs exclude only that exact spelling, so two aliases of one
1228
+ * source require distinct lists. Repeated deliveries of one spelling replay the
1229
+ * per-envelope memo ({@link envelopeDerivation}) instead of re-walking the
1230
+ * graph. Returns an empty list on exceptions.
878
1231
  */
879
1232
  function selectWatchInputs(props) {
880
1233
  if (props.result.type === "exception") {
@@ -882,12 +1235,13 @@ function selectWatchInputs(props) {
882
1235
  }
883
1236
  const state = envelopeDerivation(props);
884
1237
  const fileIdentity = derivationIdentity(state, props.file);
885
- const memoized = state.watchInputs.get(fileIdentity);
1238
+ const fileSpelling = path.resolve(props.file);
1239
+ const memoized = state.watchInputs.get(fileSpelling);
886
1240
  if (memoized !== undefined) {
887
1241
  return memoized;
888
1242
  }
889
1243
  const derived = deriveWatchInputs(state, props, fileIdentity);
890
- state.watchInputs.set(fileIdentity, derived);
1244
+ state.watchInputs.set(fileSpelling, derived);
891
1245
  return derived;
892
1246
  }
893
1247
  /** Compute one file's watch-input list over the shared per-envelope state. */
@@ -934,7 +1288,7 @@ function deriveWatchInputs(state, props, fileIdentity) {
934
1288
  !isVolatileFile(state, props),
935
1289
  }))
936
1290
  appendPhysical(input);
937
- // Resolution candidates, plugin dependencies, and universal host inputs
1291
+ // Resolver inputs, plugin dependencies, and universal host inputs
938
1292
  // preserve lexical aliases. Physical deduplication would collapse
939
1293
  // `alias/selection.cjs` into the selected target path, so a bundler would
940
1294
  // watch only the target and miss a symlink/junction retarget.
@@ -951,22 +1305,23 @@ function selectHostInputs(props) {
951
1305
  : selectListedFiles(props.projectRoot, props.result.hostInputs);
952
1306
  }
953
1307
  /**
954
- * Return the module-resolution paths that can supersede a currently resolved
955
- * module reachable from `file`. They remain host-owned even when a plugin
956
- * declares `dependenciesComplete`: plugin code cannot vouch for a compiler
957
- * resolution change that occurs without any plugin input changing.
1308
+ * Return exact importer-owned and universal resolver inputs for `file`. They
1309
+ * remain host-owned even when a plugin declares `dependenciesComplete`: plugin
1310
+ * code cannot vouch for a compiler resolution or automatic type change that
1311
+ * occurs without any plugin input changing.
958
1312
  *
959
- * Candidate entries and their source identities come from the shared
960
- * per-envelope state, so one delivery scans only the candidates themselves
961
- * instead of re-resolving every candidate source.
1313
+ * Importer entries and their source identities come from the shared
1314
+ * per-envelope state, so one delivery scans only the recorded inputs instead of
1315
+ * re-resolving every source.
962
1316
  */
963
1317
  function selectResolutionCandidateInputs(graph, state, props) {
964
- if (props.result.type === "exception" ||
965
- props.result.graph?.candidates === undefined) {
1318
+ if (props.result.type === "exception" || props.result.graph === undefined) {
966
1319
  return [];
967
1320
  }
968
1321
  const reachable = new Set(selectReachableSources(graph, state, props.file).map((source) => derivationIdentity(state, source)));
969
- const output = [];
1322
+ const output = [...graph.resolutionInputs];
1323
+ if (props.result.graph.candidates === undefined)
1324
+ return output;
970
1325
  for (const entry of graph.candidates) {
971
1326
  if (!reachable.has(entry.source)) {
972
1327
  continue;
@@ -1095,9 +1450,11 @@ function declaresCompleteDependencies(state, props) {
1095
1450
  return declared.has(derivationIdentity(state, props.file));
1096
1451
  }
1097
1452
  /**
1098
- * Extract the absolute, deduplicated dependency list for a single file from the
1099
- * compiler result. Mirrors {@link selectTransformedSource}'s key lookup: fast
1100
- * project-relative match first, then a per-envelope identity index. Returns an
1453
+ * Extract the absolute, lexical-spelling-deduplicated dependency list for a
1454
+ * single file from the compiler result. Mirrors
1455
+ * {@link selectTransformedSource}'s key lookup: fast project-relative match
1456
+ * first, then a per-envelope identity index. Distinct lexical aliases must
1457
+ * survive so bundlers observe a later symlink or junction retarget. Returns an
1101
1458
  * empty list on exceptions or when the plugin reported nothing.
1102
1459
  */
1103
1460
  function selectFileDependencies(props) {
@@ -1120,17 +1477,15 @@ function selectFileDependencies(props) {
1120
1477
  }
1121
1478
  const output = [];
1122
1479
  const seen = new Set();
1123
- const fileIdentity = derivationIdentity(state, props.file);
1124
1480
  for (const entry of entries) {
1125
1481
  if (typeof entry !== "string" || entry.length === 0) {
1126
1482
  continue;
1127
1483
  }
1128
1484
  const absolute = path.resolve(props.projectRoot, entry);
1129
- const identity = derivationIdentity(state, absolute);
1130
- if (identity === fileIdentity || seen.has(identity)) {
1485
+ if (seen.has(absolute)) {
1131
1486
  continue;
1132
1487
  }
1133
- seen.add(identity);
1488
+ seen.add(absolute);
1134
1489
  output.push(absolute);
1135
1490
  }
1136
1491
  return output;
@@ -1233,6 +1588,7 @@ function matchesCachedSource(cached, file, source, epoch) {
1233
1588
  // out-of-walk snapshot — before any of this pass's deliveries may be
1234
1589
  // settled against it. That proof is what a per-pass recompile used to buy
1235
1590
  // (samchon/ttsc#1300), at a walk instead of a compile.
1591
+ refreshFilesystemClockReference(TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached), resultFilesystem(cached.result));
1236
1592
  if (!matchesCompleteInputSnapshot(cached, currentKey, source)) {
1237
1593
  return false;
1238
1594
  }
@@ -1244,6 +1600,7 @@ function matchesCachedSource(cached, file, source, epoch) {
1244
1600
  return true;
1245
1601
  }
1246
1602
  }
1603
+ refreshFilesystemClockReference(TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.get(cached), resultFilesystem(cached.result));
1247
1604
  if (cached.result.type !== "exception" &&
1248
1605
  cached.result.graph !== undefined &&
1249
1606
  cached.projectSnapshotComplete === true &&
@@ -1310,26 +1667,36 @@ function matchesNarrowPersistentInputs(cached, file) {
1310
1667
  }
1311
1668
  /**
1312
1669
  * Validate one derived input against the generation, skipping the content read
1313
- * while the recorded metadata signature still holds.
1670
+ * while the recorded metadata signature still holds and its freshly minted
1671
+ * clock reference remains safe.
1314
1672
  *
1315
1673
  * Sibling deliveries of one generation share most of their derived inputs, and
1316
1674
  * `graph.globals` is shared by every one of them, so re-reading and re-hashing
1317
1675
  * the whole derived set per delivery multiplies one generation's proven bytes
1318
1676
  * by the module count. The derived set is proven the same way the universal
1319
1677
  * descriptor inputs are ({@link matchesUniversalHostInputs}), under the same
1320
- * rules: an unchanged signature stands in for the content comparison, and any
1321
- * signature change falls back to the full comparison. A signature is recorded
1322
- * only around a read nothing raced, only for a recorded state that came from
1323
- * reading the input rather than from failing to, and only while the observed
1324
- * filesystem's own clock has provably left the stamp's tick
1325
- * ({@link stampSeparable}), so a same-length rewrite inside that tick cannot
1326
- * hide behind an unchanged signature.
1678
+ * rules: a currently separable unchanged signature stands in for the content
1679
+ * comparison, and any signature or clock-ordering change falls back to the full
1680
+ * comparison. A signature is recorded only around a read nothing raced, only
1681
+ * for a recorded state that came from reading the input rather than from
1682
+ * failing to, and only while the observed filesystem's own clock has provably
1683
+ * left the stamp's tick ({@link stampSeparable}), so a same-length rewrite
1684
+ * inside that tick cannot hide behind an unchanged signature.
1327
1685
  *
1328
1686
  * The signature carries the physical identity of both the lexical path and its
1329
1687
  * link target ({@link inputMetadataSignature}), so retargeting a symlink or
1330
1688
  * junction moves it and the skipped realpath comparison cannot be evaded.
1331
1689
  */
1332
1690
  function matchesProvenInput(cached, state, input) {
1691
+ const observation = cached.externalInputObservations?.[path.resolve(input)];
1692
+ if (observation !== undefined) {
1693
+ if (observation.fileExists === false &&
1694
+ Object.keys(observation).length === 1 &&
1695
+ notifiesAbsence(cached, input)) {
1696
+ return true;
1697
+ }
1698
+ return matchesRecordedInput(cached, input);
1699
+ }
1333
1700
  const slot = inputSignatureSlot(cached, state, input);
1334
1701
  if (slot === undefined) {
1335
1702
  return matchesRecordedInput(cached, input);
@@ -1344,7 +1711,9 @@ function matchesProvenInput(cached, state, input) {
1344
1711
  }
1345
1712
  const filesystem = resultFilesystem(cached.result);
1346
1713
  const before = inputMetadataEvidence(input, filesystem);
1347
- if (before !== undefined && slot.signatures[slot.key] === before.signature) {
1714
+ if (before !== undefined &&
1715
+ before.separable &&
1716
+ slot.signatures[slot.key] === before.signature) {
1348
1717
  return true;
1349
1718
  }
1350
1719
  if (!matchesRecordedInput(cached, input)) {
@@ -1393,6 +1762,9 @@ function notifiesAbsence(cached, input) {
1393
1762
  */
1394
1763
  function inputSignatureSlot(cached, state, input) {
1395
1764
  const identity = derivationIdentity(state, input);
1765
+ if (cached.externalInputObservations?.[path.resolve(input)] !== undefined) {
1766
+ return undefined;
1767
+ }
1396
1768
  const external = cached.externalInputHashes ?? {};
1397
1769
  if (Object.prototype.hasOwnProperty.call(external, identity)) {
1398
1770
  // The recorded hash is identity-keyed because aliases of one physical file
@@ -1437,7 +1809,8 @@ function matchesUniversalHostInputEntries(cached, validation) {
1437
1809
  for (const entry of validation.entries.values()) {
1438
1810
  const evidence = inputMetadataEvidence(entry.path, filesystem);
1439
1811
  if (entry.signature !== undefined &&
1440
- evidence?.signature === entry.signature)
1812
+ evidence?.signature === entry.signature &&
1813
+ (entry.strict === true || evidence.separable))
1441
1814
  continue;
1442
1815
  if (entry.strict === true)
1443
1816
  return false;
@@ -1666,8 +2039,8 @@ function captureUniversalHostInputValidation(cached, currentFile) {
1666
2039
  */
1667
2040
  const MISSING_INPUT_STATE = "missing";
1668
2041
  /**
1669
- * The highest stamp each observed filesystem clock has provably minted, keyed
1670
- * by the operations object that observes it and, inside, by reporting device.
2042
+ * The current stamp an adapter-owned probe has minted, keyed by the operations
2043
+ * object that observes it and, inside, by reporting device.
1671
2044
  *
1672
2045
  * A filesystem stamps a write once per clock tick, so two same-length writes
1673
2046
  * inside one tick are indistinguishable by metadata alone. A signature may
@@ -1675,99 +2048,99 @@ const MISSING_INPUT_STATE = "missing";
1675
2048
  * it, and that guarantee needs a reference instant the observed filesystem
1676
2049
  * itself produced: once some stamp on the same device is strictly newer than an
1677
2050
  * input's modification stamp, that input's tick is provably over, so any later
1678
- * write must mint a newer stamp and move the signature. That is git's
1679
- * racily-clean index rule, adapted to a read-only contract: where git compares
1680
- * entries against the index file's own timestamp, this floor accumulates every
1681
- * stamp the cache-owned operations report, seeded per generation by
1682
- * {@link mintFilesystemClockReference}.
1683
- *
1684
- * The process clock never participates: both sides of every comparison are
1685
- * stamps the same filesystem clock minted, at the same granularity, so a
1686
- * filesystem clock running behind (or ahead of) the host process changes
1687
- * nothing.
2051
+ * write must mint a newer stamp and move the signature. This adapts git's
2052
+ * racily-clean index rule: both sides of the comparison come from the same
2053
+ * reporting device, and the newer side is a write the adapter itself made.
1688
2054
  *
1689
- * Accumulating observed stamps is deliberately weaker than git's own reference,
1690
- * which is a single stamp git minted itself. A stamp this floor accepts may
1691
- * instead have been _set_ rather than minted, and a set stamp is dangerous only
1692
- * when it lands in the future: the floor is a maximum, so a restored past stamp
1693
- * never raises it. One future-dated file a stamp-preserving extraction or
1694
- * copy from a machine whose clock ran ahead pushes its device's floor past
1695
- * the present and reopens the same-tick window for every other input on that
1696
- * device until the clock catches up. A clock that jumps backwards strands the
1697
- * floor above the present the same way, a different hazard from the constant
1698
- * offset the paragraph above is about: an offset moves both operands together
1699
- * and changes nothing, a jump moves only the present.
2055
+ * A timestamp merely observed on another input cannot advance the reference.
2056
+ * Tools may assign modification times, and a filesystem clock can move
2057
+ * backwards independently of the process clock. Neither a passive historical
2058
+ * maximum nor `Date.now()` proves what stamp a write would mint now. The probe
2059
+ * is therefore rewritten immediately before every validation that may reuse a
2060
+ * content signature, and its previous reference is cleared before the write. A
2061
+ * failed refresh or a different reporting device declines the optimization and
2062
+ * retains the content comparison (samchon/ttsc#1344).
1700
2063
  *
1701
- * The minted probe is not enough on its own to replace observed stamps: it
1702
- * lands on the scratch volume, which is frequently not the inputs' volume (a
1703
- * project on `D:` with `TEMP` on `C:`), and a probe-only floor would then
1704
- * decline every _content_ signature, so every input carrying bytes would be
1705
- * re-read on every delivery. A strict blocker keeps its signature either way,
1706
- * because it proves a kind rather than content. Observed stamps keep the common
1707
- * case working; the probe covers the case they cannot, a tree whose files were
1708
- * all written inside one tick.
2064
+ * The probe lives in an adapter-owned temporary directory outside the project.
2065
+ * That directory may be on another volume, such as `C:` when a project lives on
2066
+ * `D:`. Such a split-volume generation safely keeps reading content because no
2067
+ * same-device reference exists; it never substitutes a process-clock guess or
2068
+ * writes a probe into the user's project.
1709
2069
  */
1710
- const FILESYSTEM_CLOCK_FLOORS = new WeakMap();
1711
- /** Return one observed filesystem's per-device clock floor, creating it. */
1712
- function filesystemClockFloors(filesystem) {
1713
- let floors = FILESYSTEM_CLOCK_FLOORS.get(filesystem);
1714
- if (floors === undefined) {
1715
- floors = new Map();
1716
- FILESYSTEM_CLOCK_FLOORS.set(filesystem, floors);
1717
- }
1718
- return floors;
1719
- }
1720
- /** Raise a device's clock floor with the stamps one observation reported. */
1721
- function observeFilesystemClock(filesystem, stats) {
1722
- const floors = filesystemClockFloors(filesystem);
1723
- const stamp = stats.mtimeNs > stats.ctimeNs ? stats.mtimeNs : stats.ctimeNs;
1724
- const current = floors.get(stats.dev);
1725
- if (current === undefined || stamp > current) {
1726
- floors.set(stats.dev, stamp);
2070
+ const FILESYSTEM_CLOCK_REFERENCES = new WeakMap();
2071
+ /** Return one observed filesystem's current per-device references. */
2072
+ function filesystemClockReferences(filesystem) {
2073
+ let references = FILESYSTEM_CLOCK_REFERENCES.get(filesystem);
2074
+ if (references === undefined) {
2075
+ references = new Map();
2076
+ FILESYSTEM_CLOCK_REFERENCES.set(filesystem, references);
1727
2077
  }
2078
+ return references;
1728
2079
  }
1729
2080
  /**
1730
2081
  * Report whether a later write to the observed path is guaranteed to move its
1731
- * modification stamp: the device's clock floor holds a stamp strictly newer, so
1732
- * the tick that minted the stamp is provably over. The floor was observed
2082
+ * modification stamp: the device's current probe holds a stamp strictly newer,
2083
+ * so the tick that minted the stamp is provably over. The probe was written
1733
2084
  * before the caller's content read began, which is the ordering the guarantee
1734
2085
  * needs — a stamp minted before the read proves every post-read write lands in
1735
2086
  * a newer tick.
1736
2087
  */
1737
2088
  function stampSeparable(filesystem, stats) {
1738
- const floor = filesystemClockFloors(filesystem).get(stats.dev);
1739
- return floor !== undefined && stats.mtimeNs < floor;
2089
+ const reference = filesystemClockReferences(filesystem).get(stats.dev);
2090
+ return reference !== undefined && stats.mtimeNs < reference;
1740
2091
  }
1741
2092
  /**
1742
- * Mint a reference instant for this generation and feed it into the observed
1743
- * filesystem's clock floor.
2093
+ * Replace every prior clock proof with one reference minted by a fresh write.
1744
2094
  *
1745
- * The scratch directory is a write the adapter already owns, deliberately
2095
+ * The reference directory is storage the adapter already owns, deliberately
1746
2096
  * outside the project root, so stamping a probe file there produces a
1747
2097
  * freshly-minted "now" without touching the user's project — the analogue of
1748
2098
  * git writing its index. The probe is observed through the cache-owned
1749
2099
  * operations and keyed by the device those operations report, so it only ever
1750
- * separates stamps on the filesystem that actually minted it; when the scratch
1751
- * volume differs from the inputs' volume, or the observed filesystem cannot see
1752
- * the probe at all, nothing is proven and signature recording simply stays
1753
- * declined until passively observed stamps separate an input on their own.
2100
+ * separates stamps on the filesystem that actually minted it. When its volume
2101
+ * differs from the inputs' volume, or the observed filesystem cannot see the
2102
+ * probe at all, nothing is proven and content comparison continues.
1754
2103
  *
1755
- * Relocating the scratch directory onto the inputs' volume would make the probe
1756
- * universal, but it would also move every compiler and plugin temporary write
1757
- * into the project's parent (frequently a monorepo root or a home directory)
1758
- * for those layouts. That is a product decision about where ttsc writes, not a
1759
- * property of this rule, so the cross-volume case degrades to more reads here
1760
- * rather than being bought with it.
2104
+ * Forcing the reference onto the inputs' volume would require writing into the
2105
+ * user's project or an otherwise unowned neighboring directory. That is not a
2106
+ * valid price for this optimization, so the cross-volume case degrades to more
2107
+ * reads instead.
1761
2108
  */
1762
- function mintFilesystemClockReference(scratchDirectory, filesystem) {
2109
+ function refreshFilesystemClockReference(referenceDirectory, filesystem) {
2110
+ const references = filesystemClockReferences(filesystem);
2111
+ references.clear();
2112
+ if (referenceDirectory === undefined)
2113
+ return;
1763
2114
  try {
1764
- const probe = path.join(scratchDirectory, "clock-reference");
1765
- fs.writeFileSync(probe, "");
1766
- observeFilesystemClock(filesystem, filesystem.lstat(probe));
2115
+ const probe = path.join(referenceDirectory, "clock-reference");
2116
+ fs.writeFileSync(probe, `${process.hrtime.bigint()}\n`);
2117
+ const stats = filesystem.lstat(probe);
2118
+ if (stats.isFile()) {
2119
+ references.set(stats.dev, stats.mtimeNs);
2120
+ }
1767
2121
  }
1768
2122
  catch {
1769
- // The absence of a reference declines signature recording; it never
1770
- // invalidates a generation.
2123
+ // A failed refresh leaves no reference, so content comparison continues.
2124
+ references.clear();
2125
+ }
2126
+ }
2127
+ /** Remove only the known probe and its now-empty owned directory. */
2128
+ function disposeFilesystemClockReference(referenceDirectory) {
2129
+ try {
2130
+ fs.rmSync(path.join(referenceDirectory, "clock-reference"), {
2131
+ force: true,
2132
+ });
2133
+ }
2134
+ catch {
2135
+ // The probe may already have disappeared; the directory removal below is
2136
+ // still safe because it is deliberately non-recursive.
2137
+ }
2138
+ try {
2139
+ fs.rmdirSync(referenceDirectory);
2140
+ }
2141
+ catch {
2142
+ // Eviction schedules cleanup without awaiting its Promise. A foreign entry
2143
+ // or a concurrent removal leaves, at worst, an empty temporary directory.
1771
2144
  }
1772
2145
  }
1773
2146
  /** Metadata identity whose stability lets a generation reuse a content hash. */
@@ -1778,12 +2151,10 @@ function inputMetadataSignature(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS
1778
2151
  function inputMetadataEvidence(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
1779
2152
  try {
1780
2153
  const link = filesystem.lstat(file);
1781
- observeFilesystemClock(filesystem, link);
1782
2154
  let target = link;
1783
2155
  if (link.isSymbolicLink()) {
1784
2156
  try {
1785
2157
  target = filesystem.statBigInt(file);
1786
- observeFilesystemClock(filesystem, target);
1787
2158
  }
1788
2159
  catch {
1789
2160
  // Keep a broken link in the existing-input manifest. Its own metadata
@@ -1849,7 +2220,18 @@ function hostInputStateHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
1849
2220
  }
1850
2221
  /** Fingerprint the text/kind state returned by TypeScript-Go's filesystem. */
1851
2222
  function graphInputStateHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
2223
+ const kind = compilerStatKind(file, filesystem);
2224
+ const readHash = graphInputReadHash(file, filesystem, kind);
2225
+ if (readHash !== null)
2226
+ return readHash;
2227
+ return kind === "directory" ? hashText("ttsc:host-input:directory\0") : null;
2228
+ }
2229
+ /** Hash only a successful compiler-style ReadFile result, without kind fallback. */
2230
+ function graphInputReadHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, observedKind) {
2231
+ const kind = observedKind ?? compilerStatKind(file, filesystem);
1852
2232
  try {
2233
+ if (kind === "directory")
2234
+ return null;
1853
2235
  const bytes = filesystem.readFile(file);
1854
2236
  if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
1855
2237
  const even = bytes.subarray(2, 2 + Math.floor((bytes.length - 2) / 2) * 2);
@@ -1869,15 +2251,131 @@ function graphInputStateHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
1869
2251
  return hashText(content);
1870
2252
  }
1871
2253
  catch {
2254
+ return null;
2255
+ }
2256
+ }
2257
+ /** Current result of TypeScript-Go's Stat predicate. */
2258
+ function compilerStatKind(file, filesystem) {
2259
+ try {
2260
+ return filesystem.stat(file).isDirectory() ? "directory" : "file";
2261
+ }
2262
+ catch {
2263
+ return "missing";
2264
+ }
2265
+ }
2266
+ /** Return the exact recorded predicates that no longer hold for one spelling. */
2267
+ function graphInputObservationFailures(file, observation, filesystem, identities) {
2268
+ const failures = [];
2269
+ if (observation.accessibleEntries !== undefined) {
2270
+ const current = compilerAccessibleEntries(file, filesystem);
2271
+ if (JSON.stringify(current) !== JSON.stringify(observation.accessibleEntries)) {
2272
+ failures.push("accessible-entries-changed");
2273
+ }
2274
+ }
2275
+ const kind = observation.fileExists !== undefined ||
2276
+ observation.directoryExists !== undefined ||
2277
+ observation.stat !== undefined ||
2278
+ observation.readFile !== undefined
2279
+ ? compilerStatKind(file, filesystem)
2280
+ : undefined;
2281
+ if (observation.fileExists !== undefined &&
2282
+ (kind === "file") !== observation.fileExists) {
2283
+ failures.push("file-exists-changed");
2284
+ }
2285
+ if (observation.directoryExists !== undefined &&
2286
+ (kind === "directory") !== observation.directoryExists) {
2287
+ failures.push("directory-exists-changed");
2288
+ }
2289
+ if (observation.stat !== undefined && kind !== observation.stat) {
2290
+ failures.push("stat-changed");
2291
+ }
2292
+ if (observation.readFile !== undefined) {
2293
+ const currentHash = graphInputReadHash(file, filesystem, kind);
2294
+ if ((observation.readFile.ok && currentHash !== observation.readFile.hash) ||
2295
+ (!observation.readFile.ok && currentHash !== null)) {
2296
+ failures.push("read-file-changed");
2297
+ }
2298
+ }
2299
+ if (observation.realpath !== undefined) {
2300
+ const currentRealpath = compilerInputRealpathObservation(file, filesystem);
2301
+ if (observation.realpath.ok !== currentRealpath.ok ||
2302
+ (observation.realpath.ok &&
2303
+ currentRealpath.ok &&
2304
+ !sameHostInputRealpath(observation.realpath.path, currentRealpath.path, identities))) {
2305
+ failures.push("realpath-changed");
2306
+ }
2307
+ }
2308
+ return failures;
2309
+ }
2310
+ /** Replay TypeScript-Go's accessible-entry classification and sorted order. */
2311
+ function compilerAccessibleEntries(directory, filesystem) {
2312
+ const directories = [];
2313
+ const files = [];
2314
+ const pathApi = filesystem.platform === "win32" ? path.win32 : path.posix;
2315
+ let entries;
2316
+ try {
2317
+ entries = filesystem.readdir(directory);
2318
+ }
2319
+ catch {
2320
+ return { directories, files };
2321
+ }
2322
+ for (const entry of entries) {
2323
+ if (entry.isDirectory()) {
2324
+ directories.push(entry.name);
2325
+ continue;
2326
+ }
2327
+ if (entry.isFile()) {
2328
+ files.push(entry.name);
2329
+ continue;
2330
+ }
2331
+ if (!entry.isSymbolicLink())
2332
+ continue;
1872
2333
  try {
1873
- return filesystem.stat(file).isDirectory()
1874
- ? hashText("ttsc:host-input:directory\0")
1875
- : null;
2334
+ const target = filesystem.stat(pathApi.join(directory, entry.name));
2335
+ if (target.isDirectory())
2336
+ directories.push(entry.name);
2337
+ else if (target.isFile())
2338
+ files.push(entry.name);
1876
2339
  }
1877
2340
  catch {
1878
- return null;
2341
+ // TypeScript-Go omits inaccessible and broken linked entries.
1879
2342
  }
1880
2343
  }
2344
+ directories.sort();
2345
+ files.sort();
2346
+ return { directories, files };
2347
+ }
2348
+ /** Validate one predicate-preserving graph proof against a filesystem view. */
2349
+ function validateGraphInputObservation(file, observation, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
2350
+ const normalized = normalizeGraphInputObservation(observation, filesystem.platform);
2351
+ if (normalized === undefined)
2352
+ return ["proof-conflict"];
2353
+ return graphInputObservationFailures(file, normalized, filesystem, createHostPathIdentityContext(filesystem));
2354
+ }
2355
+ /** Whether every compiler-time predicate still returns its recorded value. */
2356
+ function matchesGraphInputObservation(file, observation, filesystem, identities) {
2357
+ return (graphInputObservationFailures(file, observation, filesystem, identities)
2358
+ .length === 0);
2359
+ }
2360
+ /** Replay TypeScript-Go's Realpath result, including its lexical fallback. */
2361
+ function compilerInputRealpathObservation(file, filesystem) {
2362
+ try {
2363
+ const realpath = filesystem.realpath(file);
2364
+ return realpath.length === 0
2365
+ ? { ok: false }
2366
+ : {
2367
+ ok: true,
2368
+ path: pathIdentity.resolveFilesystemPath(realpath, filesystem.platform),
2369
+ };
2370
+ }
2371
+ catch {
2372
+ // TypeScript-Go's OS and io filesystems return the cleaned input spelling
2373
+ // when native realpath resolution fails; they do not expose the failure.
2374
+ return {
2375
+ ok: true,
2376
+ path: pathIdentity.resolveFilesystemPath(file, filesystem.platform),
2377
+ };
2378
+ }
1881
2379
  }
1882
2380
  /** Physical target selected by a lexical host-input path. */
1883
2381
  function hostInputRealpath(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
@@ -2021,6 +2519,9 @@ function matchesExternalInputRealpaths(cached) {
2021
2519
  const state = envelopeDerivation(cached);
2022
2520
  const filesystem = resultFilesystem(cached.result);
2023
2521
  for (const input of cached.externalInputPaths ?? []) {
2522
+ if (cached.externalInputObservations?.[path.resolve(input)] !== undefined) {
2523
+ continue;
2524
+ }
2024
2525
  const identity = derivationIdentity(state, input);
2025
2526
  if (!Object.prototype.hasOwnProperty.call(expected, identity))
2026
2527
  continue;
@@ -2044,15 +2545,16 @@ function captureExternalInputSnapshot(cached, paths) {
2044
2545
  // A non-declaration transform output is a compiler-realized source even when
2045
2546
  // a malformed or legacy graph omitted its node. Its output was computed from
2046
2547
  // compiler-time bytes, so a post-compile host read cannot prove coherence.
2047
- const transformSources = new Set();
2548
+ const transformSourceSpellings = new Set();
2048
2549
  if (cached.result.type === "success") {
2049
2550
  for (const output of Object.keys(cached.result.typescript)) {
2050
2551
  if (!isDeclarationFile(output)) {
2051
- transformSources.add(derivationIdentity(state, path.resolve(cached.projectRoot, output)));
2552
+ transformSourceSpellings.add(path.resolve(cached.projectRoot, output));
2052
2553
  }
2053
2554
  }
2054
2555
  }
2055
2556
  const hashes = {};
2557
+ const observations = {};
2056
2558
  const realpaths = {};
2057
2559
  const signatures = {};
2058
2560
  const failures = createGenerationProofFailures();
@@ -2069,26 +2571,55 @@ function captureExternalInputSnapshot(cached, paths) {
2069
2571
  };
2070
2572
  for (const input of paths) {
2071
2573
  const identity = derivationIdentity(state, input);
2072
- // A member the envelope reported only as a resolution candidate falls
2574
+ const spelling = path.resolve(input);
2575
+ const predicateObservation = graph.inputObservations.get(spelling);
2576
+ const predicateConflict = graph.inputObservationConflicts.has(spelling);
2577
+ if (graph.speculative.has(spelling) &&
2578
+ (predicateObservation !== undefined || predicateConflict)) {
2579
+ if (predicateConflict || predicateObservation === undefined) {
2580
+ complete = false;
2581
+ recordGenerationProofFailure(failures, {
2582
+ domain: "external",
2583
+ kind: "graph-proof-conflict",
2584
+ detail: graph.inputProofFailures.get(spelling),
2585
+ path: input,
2586
+ });
2587
+ continue;
2588
+ }
2589
+ const mismatches = graphInputObservationFailures(input, predicateObservation, filesystem, state.identityContext);
2590
+ if (mismatches.length !== 0)
2591
+ complete = false;
2592
+ for (const kind of mismatches) {
2593
+ recordGenerationProofFailure(failures, {
2594
+ domain: "external",
2595
+ kind: `graph-${kind}`,
2596
+ path: input,
2597
+ });
2598
+ }
2599
+ observations[spelling] = predicateObservation;
2600
+ continue;
2601
+ }
2602
+ // A member the envelope reported only as a resolver input falls
2073
2603
  // through to the recorded-state branch below, the same evidence a
2074
2604
  // plugin-declared dependency path carries. Its absence still invalidates
2075
2605
  // the generation when it appears, because `missing` is recorded state.
2076
- const realizedTransformSource = transformSources.has(identity);
2606
+ const realizedTransformSource = transformSourceSpellings.has(spelling);
2077
2607
  const speculativeOnly = !realizedTransformSource &&
2078
- graph.speculative.has(identity) &&
2079
- !graph.inputProofs.has(identity) &&
2080
- !graph.inputProofConflicts.has(identity);
2081
- if ((realizedTransformSource || graph.members.has(identity)) &&
2608
+ graph.speculative.has(spelling) &&
2609
+ !graph.inputProofs.has(spelling) &&
2610
+ !graph.inputProofConflicts.has(spelling) &&
2611
+ !graph.inputProofFailures.has(spelling);
2612
+ if ((realizedTransformSource || graph.memberSpellings.has(spelling)) &&
2082
2613
  !speculativeOnly) {
2083
- const proof = graph.inputProofs.get(identity);
2084
- if (proof === undefined || graph.inputProofConflicts.has(identity)) {
2614
+ const proof = graph.inputProofs.get(spelling);
2615
+ if (proof === undefined || graph.inputProofConflicts.has(spelling)) {
2085
2616
  complete = false;
2086
2617
  recordGenerationProofFailure(failures, {
2087
2618
  domain: "external",
2088
- kind: graph.inputProofConflicts.has(identity)
2619
+ kind: graph.inputProofConflicts.has(spelling)
2089
2620
  ? "graph-proof-conflict"
2090
2621
  : "graph-proof-missing",
2091
- detail: graph.inputProofFailures.get(identity),
2622
+ detail: graph.inputProofFailures.get(spelling),
2092
2623
  path: input,
2093
2624
  });
2094
2625
  continue;
@@ -2133,7 +2664,14 @@ function captureExternalInputSnapshot(cached, paths) {
2133
2664
  if (hash !== null)
2134
2665
  record(input, before, after);
2135
2666
  }
2136
- return { complete, failures, hashes, realpaths, signatures };
2667
+ return {
2668
+ complete,
2669
+ failures,
2670
+ hashes,
2671
+ observations,
2672
+ realpaths,
2673
+ signatures,
2674
+ };
2137
2675
  }
2138
2676
  /** Explain every graph member that no longer matches the compiler's state. */
2139
2677
  function compilerGraphInputProofFailures(cached) {
@@ -2141,6 +2679,7 @@ function compilerGraphInputProofFailures(cached) {
2141
2679
  if (cached.result.type === "exception" ||
2142
2680
  cached.result.graph === undefined ||
2143
2681
  (cached.result.graph.inputHashes === undefined &&
2682
+ cached.result.graph.inputObservations === undefined &&
2144
2683
  cached.result.graph.inputRealpaths === undefined &&
2145
2684
  cached.result.graph.inputProofFailures === undefined)) {
2146
2685
  // Legacy sidecars remain compatible for ordinary in-project graphs. Their
@@ -2151,34 +2690,89 @@ function compilerGraphInputProofFailures(cached) {
2151
2690
  const state = envelopeDerivation(cached);
2152
2691
  const filesystem = resultFilesystem(cached.result);
2153
2692
  const graph = envelopeGraphIndexes(state, cached);
2154
- for (const identity of graph.members) {
2155
- const proof = graph.inputProofs.get(identity);
2156
- const spelling = proof?.path ?? graph.spellings.get(identity) ?? cached.projectRoot;
2693
+ const predicateSpellings = new Set();
2694
+ const predicateConflictSpellings = new Set();
2695
+ for (const [spelling, observation] of graph.inputObservations) {
2696
+ predicateSpellings.add(spelling);
2697
+ for (const kind of graphInputObservationFailures(spelling, observation, filesystem, state.identityContext)) {
2698
+ recordGenerationProofFailure(failures, {
2699
+ domain: "graph",
2700
+ kind,
2701
+ path: spelling,
2702
+ });
2703
+ }
2704
+ }
2705
+ for (const spelling of graph.inputObservationConflicts) {
2706
+ predicateSpellings.add(spelling);
2707
+ predicateConflictSpellings.add(spelling);
2708
+ recordGenerationProofFailure(failures, {
2709
+ domain: "graph",
2710
+ kind: "proof-conflict",
2711
+ detail: graph.inputProofFailures.get(spelling),
2712
+ path: spelling,
2713
+ });
2714
+ }
2715
+ for (const spelling of graph.memberSpellings) {
2716
+ const proof = graph.inputProofs.get(spelling);
2157
2717
  if (isTransformScratchInput(spelling, cached.scratchDirectory)) {
2158
2718
  continue;
2159
2719
  }
2160
- // A speculative candidate has no compile-time read to prove. Requiring one
2161
- // would void every generation of every project whose resolution passes over
2162
- // a higher-priority spelling, which is every project with a dependency
2163
- // typed by a declaration file (samchon/ttsc#1245). It is validated instead
2164
- // against the state {@link captureExternalInputSnapshot} recorded for it.
2165
- if (proof === undefined && graph.speculative.has(identity)) {
2720
+ if (predicateConflictSpellings.has(spelling)) {
2166
2721
  continue;
2167
2722
  }
2168
- if (graph.inputProofConflicts.has(identity)) {
2723
+ if (graph.inputProofConflicts.has(spelling)) {
2169
2724
  recordGenerationProofFailure(failures, {
2170
2725
  domain: "graph",
2171
2726
  kind: "proof-conflict",
2172
- detail: graph.inputProofFailures.get(identity),
2727
+ detail: graph.inputProofFailures.get(spelling),
2173
2728
  path: spelling,
2174
2729
  });
2175
2730
  continue;
2176
2731
  }
2732
+ if (graph.inputProofFailures.has(spelling)) {
2733
+ recordGenerationProofFailure(failures, {
2734
+ domain: "graph",
2735
+ kind: "proof-missing",
2736
+ detail: graph.inputProofFailures.get(spelling),
2737
+ path: spelling,
2738
+ });
2739
+ continue;
2740
+ }
2741
+ const observation = graph.inputObservations.get(spelling);
2742
+ if (observation !== undefined && proof !== undefined) {
2743
+ const projection = legacyProjectionOfGraphInputObservation(observation);
2744
+ if (projection.failure !== undefined ||
2745
+ projection.hash !== proof.hash ||
2746
+ !sameHostInputRealpath(projection.realpath, proof.realpath, state.identityContext)) {
2747
+ recordGenerationProofFailure(failures, {
2748
+ domain: "graph",
2749
+ kind: "proof-conflict",
2750
+ detail: projection.failure,
2751
+ path: spelling,
2752
+ });
2753
+ }
2754
+ // The rich predicates were already replayed above. Their legacy
2755
+ // projection is an internal producer-consistency check, never a reason
2756
+ // to read the same filesystem input again. A projection that cannot
2757
+ // represent the observation is itself inconsistent with a supplied
2758
+ // legacy proof, rather than permission to trust either representation.
2759
+ continue;
2760
+ }
2761
+ if (graph.speculative.has(spelling) &&
2762
+ predicateSpellings.has(spelling) &&
2763
+ !graph.inputProofFailures.has(spelling)) {
2764
+ continue;
2765
+ }
2766
+ // A legacy sidecar can report a resolver candidate without a compiler
2767
+ // predicate. Validate it against the generation snapshot instead.
2768
+ if (proof === undefined && graph.speculative.has(spelling)) {
2769
+ continue;
2770
+ }
2177
2771
  if (proof === undefined) {
2178
2772
  recordGenerationProofFailure(failures, {
2179
2773
  domain: "graph",
2180
2774
  kind: "proof-missing",
2181
- detail: graph.inputProofFailures.get(identity),
2775
+ detail: graph.inputProofFailures.get(spelling),
2182
2776
  path: spelling,
2183
2777
  });
2184
2778
  continue;
@@ -2210,6 +2804,10 @@ function matchesRecordedInput(cached, input) {
2210
2804
  ? cached.inputHashes[projectKey]
2211
2805
  : undefined;
2212
2806
  const identity = derivationIdentity(state, input);
2807
+ const observation = cached.externalInputObservations?.[path.resolve(input)];
2808
+ if (observation !== undefined) {
2809
+ return matchesGraphInputObservation(input, observation, filesystem, state.identityContext);
2810
+ }
2213
2811
  const externalHash = (cached.externalInputHashes ?? {})[identity];
2214
2812
  const externalRealpaths = cached.externalInputRealpaths;
2215
2813
  const graphInput = externalRealpaths !== undefined &&
@@ -2247,9 +2845,20 @@ function markCachedSourceServed(cached, file) {
2247
2845
  * can fold the identical input universe into their own cache fingerprints.
2248
2846
  */
2249
2847
  function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy) {
2250
- return collectProjectInputSnapshot(projectRoot, identities, filesystem, undefined, {
2848
+ return collectProjectInputHashSnapshot(projectRoot, identities, filesystem, policy).hashes;
2849
+ }
2850
+ /**
2851
+ * Hash the project walk and retain whether every attempted directory and file
2852
+ * was observed coherently. Cache-key hosts must reject an incomplete set.
2853
+ */
2854
+ function collectProjectInputHashSnapshot(projectRoot, identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy) {
2855
+ const snapshot = collectProjectInputSnapshot(projectRoot, identities, filesystem, undefined, {
2251
2856
  policy,
2252
- }).hashes;
2857
+ });
2858
+ return {
2859
+ complete: snapshot.complete && snapshot.directoryComplete,
2860
+ hashes: snapshot.hashes,
2861
+ };
2253
2862
  }
2254
2863
  /** Hash project files and snapshot the directory topology in one walk. */
2255
2864
  function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, proven, options) {
@@ -2278,10 +2887,11 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
2278
2887
  const before = inputMetadataEvidence(file, filesystem);
2279
2888
  // A file whose signature still equals the one captured around the read
2280
2889
  // that produced the recorded hash carries that content, so the whole
2281
- // project does not have to be re-read to prove one delivery. A signature
2282
- // that was already proven stays proven: its stamp has not moved since the
2283
- // clock provably left its tick.
2890
+ // project does not have to be re-read to prove one delivery. Recheck the
2891
+ // clock ordering as well: rollback can make a formerly safe floor unable
2892
+ // to answer for a new write (samchon/ttsc#1344).
2284
2893
  if (before !== undefined &&
2894
+ before.separable &&
2285
2895
  proven !== undefined &&
2286
2896
  proven.signatures[key] === before.signature &&
2287
2897
  Object.prototype.hasOwnProperty.call(proven.hashes, key)) {
@@ -2398,7 +3008,7 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, pol
2398
3008
  : `unstable:${before}:${after ?? "missing"}`,
2399
3009
  };
2400
3010
  for (const entry of entries) {
2401
- if (isIgnoredProjectDirectory(entry.name)) {
3011
+ if (projectDiscovery.isIgnoredProjectDirectory(entry.name)) {
2402
3012
  continue;
2403
3013
  }
2404
3014
  const file = path.join(current, entry.name);
@@ -2472,7 +3082,7 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, pol
2472
3082
  }
2473
3083
  /**
2474
3084
  * Return a directory's metadata stamp, used to detect that its membership moved
2475
- * _while_ the walk was enumerating it, and to feed the observed-clock floor.
3085
+ * _while_ the walk was enumerating it.
2476
3086
  *
2477
3087
  * This is the right instrument for that job and the wrong one for comparing two
2478
3088
  * generations: it moves for ignored entries too. {@link walkProjectInputs}
@@ -2481,9 +3091,6 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, pol
2481
3091
  function projectDirectorySignature(directory, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
2482
3092
  try {
2483
3093
  const stats = filesystem.statBigInt(directory);
2484
- // Directory stamps are minted by the same clock as file stamps, so every
2485
- // walk observation also raises the clock floor that separates them.
2486
- observeFilesystemClock(filesystem, stats);
2487
3094
  if (!stats.isDirectory()) {
2488
3095
  return undefined;
2489
3096
  }
@@ -2535,6 +3142,25 @@ function openDirectoryWatch(filesystem, directory, listener, onError) {
2535
3142
  watcher.on("error", onError);
2536
3143
  return { close: () => watcher.close() };
2537
3144
  }
3145
+ /** Detach and close every watcher, then preserve the first cleanup failure. */
3146
+ function closeDirectoryWatches(watchers) {
3147
+ const owned = watchers.splice(0);
3148
+ let failed = false;
3149
+ let failure;
3150
+ for (const watcher of owned) {
3151
+ try {
3152
+ watcher.close();
3153
+ }
3154
+ catch (error) {
3155
+ if (!failed) {
3156
+ failed = true;
3157
+ failure = error;
3158
+ }
3159
+ }
3160
+ }
3161
+ if (failed)
3162
+ throw failure;
3163
+ }
2538
3164
  /** Watch every walked directory for membership changes after generation. */
2539
3165
  async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = tsconfigPaths.PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
2540
3166
  const tracker = {
@@ -2549,11 +3175,7 @@ async function createProjectMutationTracker(directories, filesystem = DEFAULT_FI
2549
3175
  return tracker;
2550
3176
  }
2551
3177
  const watchers = [];
2552
- tracker.close = () => {
2553
- for (const watcher of watchers)
2554
- watcher.close();
2555
- watchers.length = 0;
2556
- };
3178
+ tracker.close = () => closeDirectoryWatches(watchers);
2557
3179
  for (const directory of directories) {
2558
3180
  try {
2559
3181
  watchers.push(openDirectoryWatch(filesystem, directory.path, (eventType, filename) => {
@@ -2620,11 +3242,7 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
2620
3242
  return tracker;
2621
3243
  }
2622
3244
  const watchers = [];
2623
- tracker.close = () => {
2624
- for (const watcher of watchers)
2625
- watcher.close();
2626
- watchers.length = 0;
2627
- };
3245
+ tracker.close = () => closeDirectoryWatches(watchers);
2628
3246
  for (const location of locations) {
2629
3247
  try {
2630
3248
  const names = new Set(location.names);
@@ -2795,14 +3413,40 @@ membership) {
2795
3413
  return;
2796
3414
  broker.trackers.delete(id);
2797
3415
  active.ready();
2798
- broker.child.send?.({ id, op: "remove" });
3416
+ let failed = false;
3417
+ let failure;
3418
+ try {
3419
+ broker.child.send?.({ id, op: "remove" });
3420
+ }
3421
+ catch (error) {
3422
+ failed = true;
3423
+ failure = error;
3424
+ }
2799
3425
  if (broker.trackers.size === 0) {
2800
- broker.child.disconnect?.();
2801
- broker.child.kill();
2802
3426
  if (windowsProjectMutationBroker === broker) {
2803
3427
  windowsProjectMutationBroker = undefined;
2804
3428
  }
3429
+ try {
3430
+ broker.child.disconnect?.();
3431
+ }
3432
+ catch (error) {
3433
+ if (!failed) {
3434
+ failed = true;
3435
+ failure = error;
3436
+ }
3437
+ }
3438
+ try {
3439
+ broker.child.kill();
3440
+ }
3441
+ catch (error) {
3442
+ if (!failed) {
3443
+ failed = true;
3444
+ failure = error;
3445
+ }
3446
+ }
2805
3447
  }
3448
+ if (failed)
3449
+ throw failure;
2806
3450
  };
2807
3451
  broker.child.send?.({
2808
3452
  allEvents,
@@ -3087,7 +3731,7 @@ function isProjectWalkPath(root, file, _identities = createHostPathIdentityConte
3087
3731
  const segments = relative.split(path.sep);
3088
3732
  // The last segment is the file itself, which the walk names rather than
3089
3733
  // descends into, so only the directory components decide walk membership.
3090
- if (segments.slice(0, -1).some(isIgnoredProjectDirectory)) {
3734
+ if (segments.slice(0, -1).some(projectDiscovery.isIgnoredProjectDirectory)) {
3091
3735
  return false;
3092
3736
  }
3093
3737
  if (isExcludedProjectDirectory(path.dirname(path.resolve(file)), policy)) {
@@ -3144,10 +3788,197 @@ function collectExternalInputHashes(paths, filesystem = DEFAULT_FILESYSTEM_OPERA
3144
3788
  }
3145
3789
  return hashes;
3146
3790
  }
3791
+ /** Capture the stable file predicate used by implicit project discovery. */
3792
+ function captureWatchInputFileBaseline(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
3793
+ const capture = () => {
3794
+ const identities = createHostPathIdentityContext(filesystem);
3795
+ let fileExists = false;
3796
+ try {
3797
+ fileExists = filesystem.stat(file).isFile();
3798
+ }
3799
+ catch {
3800
+ // Project discovery rejects every candidate not proven to be a file.
3801
+ }
3802
+ return {
3803
+ fileExists,
3804
+ identity: pathIdentityKey(file, identities),
3805
+ };
3806
+ };
3807
+ try {
3808
+ const before = capture();
3809
+ const after = capture();
3810
+ return stableStringify(before) === stableStringify(after)
3811
+ ? after
3812
+ : undefined;
3813
+ }
3814
+ catch {
3815
+ return undefined;
3816
+ }
3817
+ }
3818
+ /**
3819
+ * Capture one stable main-process baseline that can be compared with any
3820
+ * generation-owned watch-input evidence. Two equal broad observations are
3821
+ * required so a cache key never publishes a torn path state.
3822
+ */
3823
+ function captureWatchInputBaseline(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
3824
+ const capture = () => {
3825
+ const identities = createHostPathIdentityContext(filesystem);
3826
+ const stat = compilerStatKind(file, filesystem);
3827
+ return {
3828
+ directoryExists: stat === "directory",
3829
+ fileExists: stat === "file",
3830
+ graphHash: graphInputStateHash(file, filesystem) ?? MISSING_INPUT_STATE,
3831
+ graphReadHash: graphInputReadHash(file, filesystem),
3832
+ hostHash: hostInputStateHash(file, filesystem) ?? MISSING_INPUT_STATE,
3833
+ identity: pathIdentityKey(file, identities),
3834
+ realpath: compilerInputRealpathObservation(file, filesystem),
3835
+ stat,
3836
+ };
3837
+ };
3838
+ try {
3839
+ const before = capture();
3840
+ const after = capture();
3841
+ return stableStringify(before) === stableStringify(after)
3842
+ ? after
3843
+ : undefined;
3844
+ }
3845
+ catch {
3846
+ return undefined;
3847
+ }
3848
+ }
3849
+ /** Compare generation evidence with the main process's exact key baseline. */
3850
+ function watchInputEvidenceMatchesBaseline(evidence, baseline) {
3851
+ if (!isWatchInputKeyBaseline(baseline) ||
3852
+ evidence.identity !== baseline.identity ||
3853
+ evidence.state === undefined) {
3854
+ return false;
3855
+ }
3856
+ const broad = broadWatchInputBaseline(baseline);
3857
+ if (evidence.state.codec === "host") {
3858
+ return broad !== undefined && evidence.state.hash === broad.hostHash;
3859
+ }
3860
+ const identities = createHostPathIdentityContext();
3861
+ if (evidence.state.codec === "graph") {
3862
+ return (broad !== undefined &&
3863
+ evidence.state.hash === broad.graphHash &&
3864
+ sameHostInputRealpath(evidence.state.realpath, broad.realpath.ok ? broad.realpath.path : null, identities));
3865
+ }
3866
+ const observation = evidence.state.observation;
3867
+ if (observation.fileExists !== undefined &&
3868
+ observation.fileExists !== baseline.fileExists) {
3869
+ return false;
3870
+ }
3871
+ if (observation.directoryExists !== undefined &&
3872
+ (broad === undefined ||
3873
+ observation.directoryExists !== broad.directoryExists)) {
3874
+ return false;
3875
+ }
3876
+ if (observation.stat !== undefined &&
3877
+ (broad === undefined || observation.stat !== broad.stat)) {
3878
+ return false;
3879
+ }
3880
+ if (observation.readFile !== undefined &&
3881
+ (broad === undefined ||
3882
+ (observation.readFile.ok &&
3883
+ observation.readFile.hash !== broad.graphReadHash) ||
3884
+ (!observation.readFile.ok && broad.graphReadHash !== null))) {
3885
+ return false;
3886
+ }
3887
+ if (observation.realpath !== undefined) {
3888
+ if (broad === undefined) {
3889
+ return false;
3890
+ }
3891
+ if (observation.realpath.ok !== broad.realpath.ok) {
3892
+ return false;
3893
+ }
3894
+ if (observation.realpath.ok &&
3895
+ broad.realpath.ok &&
3896
+ !sameHostInputRealpath(observation.realpath.path, broad.realpath.path, identities)) {
3897
+ return false;
3898
+ }
3899
+ }
3900
+ return true;
3901
+ }
3902
+ /** Validate the complete narrow or broad shape stored in a key baseline. */
3903
+ function isWatchInputKeyBaseline(baseline) {
3904
+ if (!isPlainRecord(baseline))
3905
+ return false;
3906
+ const keys = Object.keys(baseline).sort();
3907
+ if (typeof baseline.fileExists !== "boolean" ||
3908
+ typeof baseline.identity !== "string" ||
3909
+ !isAbsoluteFilesystemPath(baseline.identity)) {
3910
+ return false;
3911
+ }
3912
+ if (stableStringify(keys) === stableStringify(["fileExists", "identity"])) {
3913
+ return true;
3914
+ }
3915
+ if (stableStringify(keys) !==
3916
+ stableStringify([
3917
+ "directoryExists",
3918
+ "fileExists",
3919
+ "graphHash",
3920
+ "graphReadHash",
3921
+ "hostHash",
3922
+ "identity",
3923
+ "realpath",
3924
+ "stat",
3925
+ ])) {
3926
+ return false;
3927
+ }
3928
+ if (typeof baseline.directoryExists !== "boolean" ||
3929
+ !isWatchInputStateHash(baseline.graphHash) ||
3930
+ !(baseline.graphReadHash === null || isContentHash(baseline.graphReadHash)) ||
3931
+ !isWatchInputStateHash(baseline.hostHash) ||
3932
+ !isWatchInputRealpathBaseline(baseline.realpath) ||
3933
+ (baseline.stat !== "directory" &&
3934
+ baseline.stat !== "file" &&
3935
+ baseline.stat !== "missing")) {
3936
+ return false;
3937
+ }
3938
+ return (baseline.fileExists === (baseline.stat === "file") &&
3939
+ baseline.directoryExists === (baseline.stat === "directory"));
3940
+ }
3941
+ /** Whether an unknown value is one ordinary JSON object. */
3942
+ function isPlainRecord(value) {
3943
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
3944
+ return false;
3945
+ }
3946
+ const prototype = Object.getPrototypeOf(value);
3947
+ return prototype === Object.prototype || prototype === null;
3948
+ }
3949
+ /** Whether one identity can name an absolute path on either supported syntax. */
3950
+ function isAbsoluteFilesystemPath(value) {
3951
+ return path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
3952
+ }
3953
+ /** Whether a serialized hash is a content digest. */
3954
+ function isContentHash(value) {
3955
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
3956
+ }
3957
+ /** Whether a baseline state is either a digest or the missing marker. */
3958
+ function isWatchInputStateHash(value) {
3959
+ return value === MISSING_INPUT_STATE || isContentHash(value);
3960
+ }
3961
+ /** Validate the serialized Realpath predicate as an exact discriminated union. */
3962
+ function isWatchInputRealpathBaseline(value) {
3963
+ if (!isPlainRecord(value) || typeof value.ok !== "boolean")
3964
+ return false;
3965
+ const keys = Object.keys(value).sort();
3966
+ if (value.ok === false) {
3967
+ return stableStringify(keys) === stableStringify(["ok"]);
3968
+ }
3969
+ return (stableStringify(keys) === stableStringify(["ok", "path"]) &&
3970
+ typeof value.path === "string" &&
3971
+ isAbsoluteFilesystemPath(value.path));
3972
+ }
3973
+ /** Return a broad baseline after the complete entry has been validated. */
3974
+ function broadWatchInputBaseline(baseline) {
3975
+ return "directoryExists" in baseline ? baseline : undefined;
3976
+ }
3147
3977
  /**
3148
3978
  * Re-check a cached mixed graph/dependency input set with its owning codec,
3149
3979
  * reusing the recorded hash of any input whose metadata signature still holds
3150
- * and reporting the signatures this pass captured.
3980
+ * under a freshly minted same-device reference and reporting the signatures
3981
+ * this pass captured.
3151
3982
  *
3152
3983
  * The caller adopts those signatures only once every input is proven unchanged,
3153
3984
  * so a signature never outlives the content comparison that justified it.
@@ -3170,6 +4001,13 @@ function matchesCachedExternalInputs(cached) {
3170
4001
  Object.keys(cached.externalInputHashes ?? {})) {
3171
4002
  const identity = derivationIdentity(state, file);
3172
4003
  const spelling = path.resolve(file);
4004
+ const observation = cached.externalInputObservations?.[spelling];
4005
+ if (observation !== undefined) {
4006
+ if (!matchesGraphInputObservation(file, observation, filesystem, state.identityContext)) {
4007
+ matches = false;
4008
+ }
4009
+ continue;
4010
+ }
3173
4011
  // Reuse the recorded hash of an out-of-walk input whose signature still
3174
4012
  // equals the one captured around the read that proved it. The signature is
3175
4013
  // keyed by this exact spelling, so an alias of the same physical file
@@ -3178,6 +4016,7 @@ function matchesCachedExternalInputs(cached) {
3178
4016
  if (before !== undefined &&
3179
4017
  Object.prototype.hasOwnProperty.call(recordedSignatures, spelling) &&
3180
4018
  Object.prototype.hasOwnProperty.call(recordedHashes, identity) &&
4019
+ before.separable &&
3181
4020
  before.signature === recordedSignatures[spelling]) {
3182
4021
  continue;
3183
4022
  }
@@ -3223,6 +4062,13 @@ function selectExternalInputPaths(props) {
3223
4062
  const identities = createHostPathIdentityContext(filesystem);
3224
4063
  const resolutionCandidates = new Set();
3225
4064
  const graph = props.result.graph;
4065
+ const graphState = envelopeDerivation({
4066
+ result: props.result,
4067
+ });
4068
+ const graphIndexes = envelopeGraphIndexes(graphState, {
4069
+ projectRoot: props.projectRoot,
4070
+ result: props.result,
4071
+ });
3226
4072
  // Every transform output key names the source file whose transformed text it
3227
4073
  // carries. Keep an out-of-walk source in the external snapshot instead of
3228
4074
  // injecting it into the project-walk key universe (samchon/ttsc#252).
@@ -3234,7 +4080,11 @@ function selectExternalInputPaths(props) {
3234
4080
  members.push(...targets);
3235
4081
  }
3236
4082
  }
3237
- for (const listed of [graph.globals, graph.configs]) {
4083
+ for (const listed of [
4084
+ graph.globals,
4085
+ graph.configs,
4086
+ graph.resolutionInputs,
4087
+ ]) {
3238
4088
  if (Array.isArray(listed)) {
3239
4089
  members.push(...listed);
3240
4090
  }
@@ -3249,7 +4099,12 @@ function selectExternalInputPaths(props) {
3249
4099
  }
3250
4100
  const absolute = path.resolve(props.projectRoot, candidate);
3251
4101
  members.push(candidate);
3252
- resolutionCandidates.add(pathIdentityKey(absolute, identities));
4102
+ resolutionCandidates.add(path.resolve(absolute));
4103
+ }
4104
+ }
4105
+ for (const input of graph.resolutionInputs ?? []) {
4106
+ if (typeof input === "string" && input.length !== 0) {
4107
+ resolutionCandidates.add(path.resolve(props.projectRoot, input));
3253
4108
  }
3254
4109
  }
3255
4110
  }
@@ -3265,7 +4120,7 @@ function selectExternalInputPaths(props) {
3265
4120
  // Plugin discovery inputs deliberately include absent config and
3266
4121
  // resolution probes. A project walk cannot snapshot a path that does
3267
4122
  // not exist yet, even when its spelling lies below projectRoot.
3268
- resolutionCandidates.add(pathIdentityKey(path.resolve(props.projectRoot, input), identities));
4123
+ resolutionCandidates.add(path.resolve(props.projectRoot, input));
3269
4124
  }
3270
4125
  }
3271
4126
  }
@@ -3281,7 +4136,10 @@ function selectExternalInputPaths(props) {
3281
4136
  const absolute = path.resolve(props.projectRoot, member);
3282
4137
  const spelling = path.resolve(absolute);
3283
4138
  const identity = pathIdentityKey(absolute, identities);
3284
- const missingCandidate = resolutionCandidates.has(identity) && !filesystem.exists(absolute);
4139
+ const observation = graphIndexes.inputObservations.get(spelling);
4140
+ const missingCandidate = resolutionCandidates.has(spelling) &&
4141
+ (observation?.fileExists === false ||
4142
+ (observation === undefined && !filesystem.exists(absolute)));
3285
4143
  if (identity === excluded ||
3286
4144
  isTransformScratchInput(absolute, props.scratchDirectory) ||
3287
4145
  seen.has(spelling) ||
@@ -3324,6 +4182,13 @@ function selectNotifiableAbsentInputs(props) {
3324
4182
  return empty;
3325
4183
  }
3326
4184
  const identities = createHostPathIdentityContext(props.filesystem);
4185
+ const graphState = envelopeDerivation({
4186
+ result: props.result,
4187
+ });
4188
+ const graphIndexes = envelopeGraphIndexes(graphState, {
4189
+ projectRoot: props.projectRoot,
4190
+ result: props.result,
4191
+ });
3327
4192
  const excluded = props.temporaryTsconfig === undefined
3328
4193
  ? undefined
3329
4194
  : pathIdentityKey(props.temporaryTsconfig, identities);
@@ -3336,7 +4201,10 @@ function selectNotifiableAbsentInputs(props) {
3336
4201
  // them. Sharing a set would let one silently answer for the other.
3337
4202
  const seen = new Set();
3338
4203
  const chain = new Set();
3339
- for (const candidates of Object.values(graph.candidates ?? {})) {
4204
+ for (const candidates of [
4205
+ ...Object.values(graph.candidates ?? {}),
4206
+ graph.resolutionInputs ?? [],
4207
+ ]) {
3340
4208
  if (!Array.isArray(candidates)) {
3341
4209
  continue;
3342
4210
  }
@@ -3346,11 +4214,14 @@ function selectNotifiableAbsentInputs(props) {
3346
4214
  }
3347
4215
  const absolute = path.resolve(props.projectRoot, candidate);
3348
4216
  const spelling = path.resolve(absolute);
4217
+ const observation = graphIndexes.inputObservations.get(spelling);
4218
+ const absentAsFile = observation?.fileExists === false ||
4219
+ (observation === undefined && !props.filesystem.exists(absolute));
3349
4220
  if (seen.has(spelling) ||
3350
4221
  isTransformScratchInput(absolute, props.scratchDirectory) ||
3351
4222
  (excluded !== undefined &&
3352
4223
  pathIdentityKey(absolute, identities) === excluded) ||
3353
- props.filesystem.exists(absolute)) {
4224
+ !absentAsFile) {
3354
4225
  continue;
3355
4226
  }
3356
4227
  seen.add(spelling);
@@ -3448,19 +4319,6 @@ function insideProject(directory, projectRoot) {
3448
4319
  * number; the bound stays sound and is merely not tight.
3449
4320
  */
3450
4321
  const NOTIFIABLE_ABSENCE_DIRECTORY_LIMIT = 512;
3451
- function isIgnoredProjectDirectory(name) {
3452
- // The residue of what used to be a fifteen-name list, kept to the three
3453
- // directories no tsconfig can name and no program can contain: the VCS
3454
- // store, the package manager's tree (TypeScript's own default `exclude`
3455
- // carries it too), and ttsc's own plugin cache. Everything else the old list
3456
- // guessed at, and guessing was wrong in both directions: a bundler writing
3457
- // to an unnamed directory changed project membership with its own output,
3458
- // while a real source directory named `build` or `temp` was dropped from the
3459
- // walk and its new files were never seen (samchon/ttsc#1307). Those are now
3460
- // decided by `ITtscProjectMembershipPolicy`, which reads the configuration
3461
- // that actually knows.
3462
- return name === ".git" || name === ".ttsc" || name === "node_modules";
3463
- }
3464
4322
  /**
3465
4323
  * Whether the resolved configuration keeps this directory out of the program.
3466
4324
  *
@@ -3685,6 +4543,9 @@ function selectDeclaredProjectInputKeys(props) {
3685
4543
  if (Array.isArray(graph.configs))
3686
4544
  for (const input of graph.configs)
3687
4545
  add(input);
4546
+ if (Array.isArray(graph.resolutionInputs))
4547
+ for (const input of graph.resolutionInputs)
4548
+ add(input);
3688
4549
  for (const [source, candidates] of Object.entries(graph.candidates ?? {})) {
3689
4550
  add(source);
3690
4551
  if (Array.isArray(candidates))
@@ -3898,6 +4759,10 @@ function createUnstableGenerationError(projectRoot, attempts, validation) {
3898
4759
  }
3899
4760
  });
3900
4761
  lines.push(" Stop writes to the listed inputs before compilation, or fix the producer that omitted or contradicted the listed proof.");
4762
+ // The failed snapshot remains useful as immutable retry evidence, but it can
4763
+ // never serve a module. Release every live resource before its terminal
4764
+ // verdict is retained in the cache.
4765
+ disposeCachedTransform(validation.cached);
3901
4766
  return new TtscUnstableGenerationError(lines.join("\n"), validation);
3902
4767
  }
3903
4768
  function hashText(input) {
@@ -3907,9 +4772,10 @@ async function transformProject(props) {
3907
4772
  const attempts = [];
3908
4773
  for (let attempt = 0; attempt < TRANSFORM_GENERATION_ATTEMPTS; attempt += 1) {
3909
4774
  const cached = await captureTransformGeneration(props);
3910
- if (!props.trackProjectMembership ||
3911
- cached.result.type !== "success" ||
3912
- cached.projectSnapshotComplete === true) {
4775
+ if (cached.configStateComplete !== false &&
4776
+ (!props.trackProjectMembership ||
4777
+ cached.result.type !== "success" ||
4778
+ cached.projectSnapshotComplete === true)) {
3913
4779
  return cached;
3914
4780
  }
3915
4781
  attempts.push(TRANSFORM_GENERATION_FAILURES.get(cached.result) ??
@@ -3930,21 +4796,43 @@ async function transformProject(props) {
3930
4796
  async function captureTransformGeneration(props) {
3931
4797
  const projectRoot = path.dirname(props.tsconfig);
3932
4798
  const scratchDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
4799
+ let clockReferenceDirectory;
4800
+ let retainClockReferenceDirectory = false;
3933
4801
  let tracker;
3934
4802
  let retainTracker = false;
3935
4803
  let hostInputTracker;
3936
4804
  let candidateTracker;
3937
4805
  let retainHostInputTracker = false;
3938
4806
  let retainCandidateTracker = false;
4807
+ let captured;
3939
4808
  try {
3940
- const configured = createTransformTsconfig(props, scratchDirectory);
4809
+ if (props.trackProjectMembership) {
4810
+ try {
4811
+ clockReferenceDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
4812
+ }
4813
+ catch {
4814
+ // A retained probe is an optimization. The live compiler scratch can
4815
+ // still authorize capture, and later validations will compare bytes.
4816
+ }
4817
+ }
4818
+ const materializesConfig = Object.keys(props.compilerOptions).length !== 0 ||
4819
+ Object.keys(props.aliasPaths).length !== 0;
4820
+ const tsconfigState = readTransformTsconfigState(props.tsconfig, materializesConfig);
4821
+ const configured = createTransformTsconfig(props, scratchDirectory, tsconfigState);
3941
4822
  const temporaryTsconfig = configured.path === props.tsconfig ? undefined : configured.path;
4823
+ const compilerEnvironment = transformScratchEnvironment(scratchDirectory);
4824
+ if (temporaryTsconfig === undefined) {
4825
+ delete compilerEnvironment[TTSC_SEMANTIC_CONFIG_PATH];
4826
+ }
4827
+ else {
4828
+ compilerEnvironment[TTSC_SEMANTIC_CONFIG_PATH] = props.tsconfig;
4829
+ }
3942
4830
  const identities = createHostPathIdentityContext(props.filesystem);
3943
4831
  // Read from the project's own tsconfig rather than the generated one: a
3944
4832
  // relative `outDir` is anchored at the config that declares it, and the
3945
4833
  // generated config lives in a system temp directory. The caller's
3946
4834
  // compiler-options overlay still wins, since it wins for the compile too.
3947
- const membershipPolicy = tsconfigPaths.mergeMembershipPolicyOverlay(tsconfigPaths.readProjectMembershipPolicy(props.tsconfig), props.compilerOptions, projectRoot);
4835
+ const membershipPolicy = tsconfigPaths.mergeMembershipPolicyOverlay(tsconfigState.membershipPolicy, props.compilerOptions, projectRoot);
3948
4836
  const before = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
3949
4837
  tracker = props.trackProjectMembership
3950
4838
  ? await createProjectMutationTracker(before.projectDirectories, props.filesystem, membershipPolicy)
@@ -3961,13 +4849,16 @@ async function captureTransformGeneration(props) {
3961
4849
  plugins: props.plugins,
3962
4850
  projectRoot,
3963
4851
  tsconfig: configured.path,
3964
- env: transformScratchEnvironment(scratchDirectory),
4852
+ env: compilerEnvironment,
3965
4853
  }).transform());
3966
4854
  TRANSFORM_RESULT_FILESYSTEM.set(result, props.filesystem);
4855
+ const configStable = tsconfigState.signature === undefined ||
4856
+ tsconfigState.signature ===
4857
+ readTransformTsconfigState(props.tsconfig, true).signature;
3967
4858
  // Mint the generation's clock reference after the compile and before any
3968
4859
  // signature-recording read below, so every input written before the
3969
4860
  // compile sits in a provably finished tick when its signature is captured.
3970
- mintFilesystemClockReference(scratchDirectory, props.filesystem);
4861
+ refreshFilesystemClockReference(clockReferenceDirectory ?? scratchDirectory, props.filesystem);
3971
4862
  const persistentHostInputs = selectPersistentHostInputs({
3972
4863
  filesystem: props.filesystem,
3973
4864
  projectRoot,
@@ -4030,7 +4921,8 @@ async function captureTransformGeneration(props) {
4030
4921
  result,
4031
4922
  scratchDirectory,
4032
4923
  });
4033
- const walkStable = walkSnapshotComplete(before, declaredInputs) &&
4924
+ const walkStable = configStable &&
4925
+ walkSnapshotComplete(before, declaredInputs) &&
4034
4926
  walkSnapshotComplete(inputSnapshot, declaredInputs) &&
4035
4927
  sameHashes(before.hashes, inputSnapshot.hashes, declaredInputs) &&
4036
4928
  sameHashes(before.fileSignatures, inputSnapshot.fileSignatures, declaredInputs) &&
@@ -4066,6 +4958,7 @@ async function captureTransformGeneration(props) {
4066
4958
  externalInputHashes: {},
4067
4959
  externalInputRealpaths: {},
4068
4960
  externalInputPaths,
4961
+ configStateComplete: configStable,
4069
4962
  inputHashes: inputSnapshot.hashes,
4070
4963
  inputSignatures: inputSnapshot.provenSignatures,
4071
4964
  membershipPolicy,
@@ -4084,6 +4977,7 @@ async function captureTransformGeneration(props) {
4084
4977
  cached.sourceHashes = captureTransformSourceHashes(cached, props.currentFile, currentSourceHash);
4085
4978
  const externalInputSnapshot = captureExternalInputSnapshot(cached, externalInputPaths);
4086
4979
  cached.externalInputHashes = externalInputSnapshot.hashes;
4980
+ cached.externalInputObservations = externalInputSnapshot.observations;
4087
4981
  cached.externalInputRealpaths = externalInputSnapshot.realpaths;
4088
4982
  cached.externalInputSignatures = externalInputSnapshot.signatures;
4089
4983
  // Evaluate every half, rather than short-circuiting, so a generation that
@@ -4091,6 +4985,13 @@ async function captureTransformGeneration(props) {
4091
4985
  // only on the failing path, where the alternative is recompiling the whole
4092
4986
  // project for every remaining module.
4093
4987
  const failures = createGenerationProofFailures();
4988
+ if (!configStable) {
4989
+ recordGenerationProofFailure(failures, {
4990
+ domain: "project",
4991
+ kind: "config-state-changed",
4992
+ path: props.tsconfig,
4993
+ });
4994
+ }
4094
4995
  if (!walkStable) {
4095
4996
  recordProjectSnapshotFailures(failures, {
4096
4997
  before,
@@ -4146,32 +5047,80 @@ async function captureTransformGeneration(props) {
4146
5047
  retainTracker = notifying && tracker !== undefined;
4147
5048
  retainHostInputTracker = notifying && hostInputTracker !== undefined;
4148
5049
  retainCandidateTracker = notifying && candidateTracker !== undefined;
4149
- return cached;
5050
+ if (clockReferenceDirectory !== undefined) {
5051
+ retainClockReferenceDirectory = true;
5052
+ }
5053
+ captured = cached;
4150
5054
  }
4151
5055
  finally {
5056
+ let cleanupFailed = false;
5057
+ let cleanupFailure;
4152
5058
  try {
4153
- if (!retainTracker && tracker !== undefined) {
4154
- tracker.close();
4155
- }
4156
- }
4157
- finally {
4158
5059
  try {
4159
- if (!retainHostInputTracker && hostInputTracker !== undefined) {
4160
- hostInputTracker.close();
5060
+ if (!retainTracker && tracker !== undefined) {
5061
+ tracker.close();
4161
5062
  }
4162
5063
  }
4163
5064
  finally {
4164
5065
  try {
4165
- if (!retainCandidateTracker && candidateTracker !== undefined) {
4166
- candidateTracker.close();
5066
+ if (!retainHostInputTracker && hostInputTracker !== undefined) {
5067
+ hostInputTracker.close();
4167
5068
  }
4168
5069
  }
4169
5070
  finally {
4170
- fs.rmSync(scratchDirectory, { force: true, recursive: true });
5071
+ try {
5072
+ if (!retainCandidateTracker && candidateTracker !== undefined) {
5073
+ candidateTracker.close();
5074
+ }
5075
+ }
5076
+ finally {
5077
+ try {
5078
+ fs.rmSync(scratchDirectory, { force: true, recursive: true });
5079
+ }
5080
+ finally {
5081
+ if (!retainClockReferenceDirectory &&
5082
+ clockReferenceDirectory !== undefined) {
5083
+ disposeFilesystemClockReference(clockReferenceDirectory);
5084
+ }
5085
+ }
5086
+ }
4171
5087
  }
4172
5088
  }
4173
5089
  }
5090
+ catch (error) {
5091
+ cleanupFailed = true;
5092
+ cleanupFailure = error;
5093
+ }
5094
+ if (cleanupFailed) {
5095
+ // The generation never leaves this function when local cleanup fails.
5096
+ // Close everything that was waiting to transfer, without letting a
5097
+ // secondary teardown error replace the first failure.
5098
+ for (const retained of [
5099
+ retainTracker ? tracker : undefined,
5100
+ retainHostInputTracker ? hostInputTracker : undefined,
5101
+ retainCandidateTracker ? candidateTracker : undefined,
5102
+ ]) {
5103
+ try {
5104
+ retained?.close();
5105
+ }
5106
+ catch {
5107
+ // Continue releasing the other generation-owned resources.
5108
+ }
5109
+ }
5110
+ if (retainClockReferenceDirectory &&
5111
+ clockReferenceDirectory !== undefined) {
5112
+ disposeFilesystemClockReference(clockReferenceDirectory);
5113
+ }
5114
+ throw cleanupFailure;
5115
+ }
4174
5116
  }
5117
+ if (captured === undefined) {
5118
+ throw new Error("ttsc: transform generation capture produced no result");
5119
+ }
5120
+ if (clockReferenceDirectory !== undefined) {
5121
+ TRANSFORM_CLOCK_REFERENCE_DIRECTORIES.set(captured, clockReferenceDirectory);
5122
+ }
5123
+ return captured;
4175
5124
  }
4176
5125
  /** Exclude disposed transform scratch from live host-input tracking. */
4177
5126
  function selectPersistentHostInputs(props) {
@@ -4191,17 +5140,55 @@ function selectPersistentHostInputs(props) {
4191
5140
  return pathIdentityKey(input, identities) !== temporary;
4192
5141
  });
4193
5142
  }
4194
- function createTransformTsconfig(props, scratchDirectory) {
4195
- const compilerOptions = normalizeCompilerOptionsForGeneratedTsconfig({
5143
+ /** Read every wrapper-dependent view and bind it to one config-chain state. */
5144
+ function readTransformTsconfigState(tsconfig, materializesConfig) {
5145
+ const membershipPolicy = tsconfigPaths.readProjectMembershipPolicy(tsconfig);
5146
+ if (!materializesConfig) {
5147
+ return {
5148
+ effectivePaths: {},
5149
+ membershipPolicy,
5150
+ templateCompilerOptions: {},
5151
+ templateFileSpecs: {},
5152
+ };
5153
+ }
5154
+ const effectivePaths = tsconfigPaths.readEffectiveTsconfigPaths(tsconfig);
5155
+ const templateCompilerOptions = tsconfigPaths.readEffectiveTsconfigTemplateCompilerOptions(tsconfig);
5156
+ const templateFileSpecs = tsconfigPaths.readEffectiveTsconfigTemplateFileSpecs(tsconfig);
5157
+ const sources = tsconfigPaths.readTsconfigSourceSnapshot(tsconfig);
5158
+ return {
5159
+ effectivePaths,
5160
+ membershipPolicy,
5161
+ signature: hashText(JSON.stringify({
5162
+ effectivePaths,
5163
+ membershipPolicy,
5164
+ sources,
5165
+ templateCompilerOptions,
5166
+ templateFileSpecs,
5167
+ })),
5168
+ templateCompilerOptions,
5169
+ templateFileSpecs,
5170
+ };
5171
+ }
5172
+ function createTransformTsconfig(props, scratchDirectory, state) {
5173
+ const overlay = normalizeCompilerOptionsForGeneratedTsconfig({
4196
5174
  ...props.compilerOptions,
4197
- ...createAliasCompilerOptions(props),
5175
+ ...createAliasCompilerOptions(props, state.effectivePaths),
4198
5176
  }, path.dirname(props.tsconfig));
4199
- if (Object.keys(compilerOptions).length === 0) {
5177
+ if (Object.keys(overlay).length === 0) {
4200
5178
  return { path: props.tsconfig };
4201
5179
  }
5180
+ // A `${configDir}` value survives every `extends` hop and binds only at the
5181
+ // final consumer. The scratch wrapper would therefore move it out of the
5182
+ // project even for an unrelated overlay. Re-state only those inherited
5183
+ // values as absolute paths so the wrapper remains semantically transparent.
5184
+ const compilerOptions = {
5185
+ ...state.templateCompilerOptions,
5186
+ ...overlay,
5187
+ };
4202
5188
  const file = path.join(scratchDirectory, "tsconfig.json");
4203
5189
  fs.writeFileSync(file, JSON.stringify({
4204
5190
  extends: normalizePath(props.tsconfig),
5191
+ ...state.templateFileSpecs,
4205
5192
  compilerOptions,
4206
5193
  }, null, 2), "utf8");
4207
5194
  return { path: file };
@@ -4334,15 +5321,17 @@ function withTransformScratchEnvironment(scratchDirectory, callback) {
4334
5321
  function normalizeCompilerOptionsForGeneratedTsconfig(compilerOptions, tsconfigDir) {
4335
5322
  const output = { ...compilerOptions };
4336
5323
  // Scalar path fields: resolve each against the original tsconfig directory.
4337
- for (const key of ["declarationDir", "outDir", "rootDir"]) {
5324
+ for (const key of tsconfigPaths.CONFIG_DIR_TEMPLATE_SCALAR_OPTIONS) {
4338
5325
  if (typeof output[key] === "string") {
4339
- output[key] = path.resolve(tsconfigDir, output[key]);
5326
+ output[key] = tsconfigPaths.resolveConfigDirTemplatePath(tsconfigDir, output[key]);
4340
5327
  }
4341
5328
  }
4342
5329
  // Array path fields: resolve each element individually.
4343
- for (const key of ["rootDirs", "typeRoots"]) {
5330
+ for (const key of tsconfigPaths.CONFIG_DIR_TEMPLATE_LIST_OPTIONS) {
4344
5331
  if (Array.isArray(output[key])) {
4345
- output[key] = output[key].map((entry) => typeof entry === "string" ? path.resolve(tsconfigDir, entry) : entry);
5332
+ output[key] = output[key].map((entry) => typeof entry === "string"
5333
+ ? tsconfigPaths.resolveConfigDirTemplatePath(tsconfigDir, entry)
5334
+ : entry);
4346
5335
  }
4347
5336
  }
4348
5337
  const paths = readPaths(output.paths);
@@ -4392,13 +5381,13 @@ function normalizePluginConfigForGeneratedTsconfig(entry, tsconfigDir) {
4392
5381
  * No `baseUrl` is emitted: TypeScript-Go removed the option (TS5102), and all
4393
5382
  * targets are absolute so none is needed.
4394
5383
  */
4395
- function createAliasCompilerOptions(props) {
5384
+ function createAliasCompilerOptions(props, effectivePaths) {
4396
5385
  if (Object.keys(props.aliasPaths).length === 0) {
4397
5386
  return {};
4398
5387
  }
4399
5388
  return {
4400
5389
  paths: {
4401
- ...tsconfigPaths.readEffectiveTsconfigPaths(props.tsconfig),
5390
+ ...effectivePaths,
4402
5391
  ...readPaths(props.compilerOptions.paths),
4403
5392
  ...props.aliasPaths,
4404
5393
  },
@@ -4720,9 +5709,9 @@ function stripTerminalEscapes(text) {
4720
5709
  *
4721
5710
  * If `tsconfig` is supplied it is returned as-is (absolute) or resolved from
4722
5711
  * `process.cwd()` (relative). Otherwise the function walks ancestor directories
4723
- * starting at `file`'s directory, returning the first `tsconfig.json` found.
4724
- * Falls back to `<cwd>/tsconfig.json` when no ancestor contains one; the
4725
- * compiler will error if that file does not exist, which is the correct
5712
+ * starting at `file`'s directory, returning the first `tsconfig.json` proven to
5713
+ * be a file. Falls back to `<cwd>/tsconfig.json` when no ancestor contains one;
5714
+ * the compiler will error if that file does not exist, which is the correct
4726
5715
  * behavior for a mis-configured project.
4727
5716
  */
4728
5717
  function resolveTsconfig(file, tsconfig, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
@@ -4731,18 +5720,9 @@ function resolveTsconfig(file, tsconfig, filesystem = DEFAULT_FILESYSTEM_OPERATI
4731
5720
  ? tsconfig
4732
5721
  : path.resolve(process.cwd(), tsconfig);
4733
5722
  }
4734
- let current = path.dirname(file);
4735
- while (true) {
4736
- const candidate = path.join(current, "tsconfig.json");
4737
- if (filesystem.exists(candidate)) {
4738
- return candidate;
4739
- }
4740
- const parent = path.dirname(current);
4741
- // Reached filesystem root, stop walking.
4742
- if (parent === current) {
4743
- break;
4744
- }
4745
- current = parent;
5723
+ const discovered = projectDiscovery.findNearestProjectTsconfig(path.dirname(file), filesystem);
5724
+ if (discovered !== undefined) {
5725
+ return discovered;
4746
5726
  }
4747
5727
  return path.resolve(process.cwd(), "tsconfig.json");
4748
5728
  }
@@ -4767,15 +5747,21 @@ function normalizePath(file) {
4767
5747
  }
4768
5748
 
4769
5749
  exports.beginTtscTransformBuild = beginTtscTransformBuild;
5750
+ exports.captureWatchInputBaseline = captureWatchInputBaseline;
5751
+ exports.captureWatchInputFileBaseline = captureWatchInputFileBaseline;
4770
5752
  exports.collectExternalInputHashes = collectExternalInputHashes;
5753
+ exports.collectProjectInputHashSnapshot = collectProjectInputHashSnapshot;
4771
5754
  exports.collectProjectInputHashes = collectProjectInputHashes;
4772
5755
  exports.createTransformResult = createTransformResult;
4773
5756
  exports.createTtscTransformCache = createTtscTransformCache;
4774
5757
  exports.isDeclarationFile = isDeclarationFile;
4775
5758
  exports.isProjectWalkPath = isProjectWalkPath;
5759
+ exports.isWatchInputKeyBaseline = isWatchInputKeyBaseline;
4776
5760
  exports.normalizeHostInputName = normalizeHostInputName;
4777
5761
  exports.pathIdentityKey = pathIdentityKey;
4778
5762
  exports.resetTtscTransformCache = resetTtscTransformCache;
4779
5763
  exports.stripQuery = stripQuery;
4780
5764
  exports.transformTtsc = transformTtsc;
5765
+ exports.validateGraphInputObservation = validateGraphInputObservation;
5766
+ exports.watchInputEvidenceMatchesBaseline = watchInputEvidenceMatchesBaseline;
4781
5767
  //# sourceMappingURL=transform.js.map