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