ttsc 0.31.6 → 0.32.1

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 (261) hide show
  1. package/cmd/ttsc/compiler_argument_cwd_test.go +38 -38
  2. package/cmd/ttscserver/main.go +2 -12
  3. package/cmd/ttscserver/resolve_launch_uses_native_flag_authority_test.go +7 -7
  4. package/driver/lsp.go +4 -43
  5. package/driver/rewrite.go +1 -1
  6. package/internal/e2etrace/completion_hints.go +3 -3
  7. package/internal/e2etrace/completion_hints_test.go +26 -12
  8. package/internal/graph/build.go +5 -6
  9. package/internal/graph/graph.go +3 -3
  10. package/internal/graph/line_model.go +2 -35
  11. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +13 -36
  12. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +9 -3
  13. package/internal/lspserver/lsp_plugin.go +19 -0
  14. package/internal/lspserver/lsp_proxy.go +9 -129
  15. package/internal/lspserver/lsp_server.go +0 -8
  16. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +6 -36
  17. package/internal/sourceprocess/windows_target_owns_suspended_admission_test.go +5 -5
  18. package/lib/TtscCompiler.js +1 -1
  19. package/lib/TtscCompiler.js.map +1 -1
  20. package/lib/compiler/internal/ResidentCheckProcess.d.ts +74 -96
  21. package/lib/compiler/internal/ResidentCheckProcess.js +150 -335
  22. package/lib/compiler/internal/ResidentCheckProcess.js.map +1 -1
  23. package/lib/compiler/internal/ResidentCheckProcessHost.d.ts +127 -0
  24. package/lib/compiler/internal/ResidentCheckProcessHost.js +447 -0
  25. package/lib/compiler/internal/ResidentCheckProcessHost.js.map +1 -0
  26. package/lib/compiler/internal/ResidentCheckProcessProtocol.d.ts +64 -0
  27. package/lib/compiler/internal/ResidentCheckProcessProtocol.js +3 -0
  28. package/lib/compiler/internal/ResidentCheckProcessProtocol.js.map +1 -0
  29. package/lib/compiler/internal/build/BuildExecution.d.ts +103 -1
  30. package/lib/compiler/internal/build/BuildExecution.js +92 -12
  31. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  32. package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +16 -11
  33. package/lib/compiler/internal/build/ResidentCheckWatchSession.js +37 -21
  34. package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
  35. package/lib/compiler/internal/build/RunBuildOptions.d.ts +6 -5
  36. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  37. package/lib/compiler/internal/build/WatchInputPublication.d.ts +59 -0
  38. package/lib/compiler/internal/build/WatchInputPublication.js +93 -0
  39. package/lib/compiler/internal/build/WatchInputPublication.js.map +1 -0
  40. package/lib/compiler/internal/build/runBuild.d.ts +29 -2
  41. package/lib/compiler/internal/build/runBuild.js +45 -19
  42. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  43. package/lib/compiler/internal/buildNativeCompiler.d.ts +2 -0
  44. package/lib/compiler/internal/buildNativeCompiler.js +1 -0
  45. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  46. package/lib/compiler/internal/compileProjectInMemory.js +4 -2
  47. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  48. package/lib/compiler/internal/project/CompilerProjectSelection.d.ts +9 -8
  49. package/lib/compiler/internal/project/CompilerProjectSelection.js +11 -9
  50. package/lib/compiler/internal/project/CompilerProjectSelection.js.map +1 -1
  51. package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +25 -8
  52. package/lib/compiler/internal/project/resolveOwningProjectConfig.js +26 -14
  53. package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
  54. package/lib/compiler/internal/readEffectiveCompilerOptions.js +1 -1
  55. package/lib/compiler/internal/readEffectiveCompilerOptions.js.map +1 -1
  56. package/lib/compiler/internal/resolveBinary.d.ts +1 -0
  57. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  58. package/lib/compiler/internal/resolveTsgo.d.ts +6 -0
  59. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  60. package/lib/compiler/internal/runSingleFileEmit.d.ts +32 -4
  61. package/lib/compiler/internal/runSingleFileEmit.js +124 -89
  62. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  63. package/lib/compiler/internal/startResidentTransform.js +8 -2
  64. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  65. package/lib/compiler/internal/transformProjectInMemory.js +11 -3
  66. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  67. package/lib/compiler/internal/transformProjectInWorker.d.ts +1 -1
  68. package/lib/compiler/internal/transformProjectInWorker.js +3 -2
  69. package/lib/compiler/internal/transformProjectInWorker.js.map +1 -1
  70. package/lib/flags/ParseOptions.d.ts +2 -2
  71. package/lib/internal/E2ETrace.js.map +1 -1
  72. package/lib/internal/OwnedNativeProcess.d.ts +2 -2
  73. package/lib/internal/OwnedNativeProcess.js +2 -2
  74. package/lib/internal/SourceNativeRetirement.d.ts +8 -7
  75. package/lib/internal/SourceNativeRetirement.js +19 -10
  76. package/lib/internal/SourceNativeRetirement.js.map +1 -1
  77. package/lib/internal/TtsxNodeOptions.d.ts +79 -0
  78. package/lib/internal/TtsxNodeOptions.js +121 -0
  79. package/lib/internal/TtsxNodeOptions.js.map +1 -0
  80. package/lib/internal/javascriptRuntimeCapabilities.js +4 -1
  81. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  82. package/lib/internal/parseWindowsDirectoryCaseSensitivity.d.ts +5 -5
  83. package/lib/internal/parseWindowsDirectoryCaseSensitivity.js +5 -5
  84. package/lib/internal/pathIdentity/createCaseSensitivityObservations.d.ts +35 -0
  85. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js +209 -0
  86. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js.map +1 -0
  87. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js +2 -143
  88. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js.map +1 -1
  89. package/lib/internal/projectInputPathIdentity.d.ts +1 -0
  90. package/lib/internal/projectInputPathIdentity.js +1 -0
  91. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  92. package/lib/internal/workerModuleUrl.d.ts +28 -0
  93. package/lib/internal/workerModuleUrl.js +40 -0
  94. package/lib/internal/workerModuleUrl.js.map +1 -0
  95. package/lib/launcher/internal/buildSingleRootProject.js +2 -1
  96. package/lib/launcher/internal/buildSingleRootProject.js.map +1 -1
  97. package/lib/launcher/internal/getCompilerVersionText.js +3 -2
  98. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  99. package/lib/launcher/internal/parseTtsxCLI.d.ts +5 -4
  100. package/lib/launcher/internal/parseTtsxCLI.js +5 -4
  101. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  102. package/lib/launcher/internal/prepareExecution.js +2 -1
  103. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  104. package/lib/launcher/internal/resolveSingleFileOutput.d.ts +8 -9
  105. package/lib/launcher/internal/resolveSingleFileOutput.js +8 -9
  106. package/lib/launcher/internal/resolveSingleFileOutput.js.map +1 -1
  107. package/lib/launcher/internal/runTtsc.js +91 -41
  108. package/lib/launcher/internal/runTtsc.js.map +1 -1
  109. package/lib/launcher/internal/runTtsx.js +5 -2
  110. package/lib/launcher/internal/runTtsx.js.map +1 -1
  111. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.d.ts +10 -6
  112. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js +11 -9
  113. package/lib/launcher/internal/runtime/CommonJsRuntimeSource.js.map +1 -1
  114. package/lib/launcher/internal/runtime/RuntimeEntrySources.d.ts +40 -0
  115. package/lib/launcher/internal/runtime/RuntimeEntrySources.js +57 -0
  116. package/lib/launcher/internal/runtime/RuntimeEntrySources.js.map +1 -0
  117. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.d.ts +5 -4
  118. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js +8 -6
  119. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js.map +1 -1
  120. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.d.ts +20 -0
  121. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js +27 -0
  122. package/lib/launcher/internal/runtime/advertiseTypeScriptExtensions.js.map +1 -0
  123. package/lib/launcher/internal/runtime/commonJsExportNames.d.ts +2 -1
  124. package/lib/launcher/internal/runtime/commonJsExportNames.js +2 -1
  125. package/lib/launcher/internal/runtime/commonJsExportNames.js.map +1 -1
  126. package/lib/launcher/internal/runtime/installRuntimeHooks.d.ts +5 -3
  127. package/lib/launcher/internal/runtime/installRuntimeHooks.js +42 -12
  128. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  129. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js +3 -1
  130. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js.map +1 -1
  131. package/lib/launcher/internal/watch/AsyncWatchTopology.d.ts +113 -0
  132. package/lib/launcher/internal/watch/AsyncWatchTopology.js +232 -0
  133. package/lib/launcher/internal/watch/AsyncWatchTopology.js.map +1 -0
  134. package/lib/launcher/internal/watch/WatchSettlement.d.ts +49 -0
  135. package/lib/launcher/internal/watch/WatchSettlement.js +70 -0
  136. package/lib/launcher/internal/watch/WatchSettlement.js.map +1 -0
  137. package/lib/launcher/internal/watch/WatchTopology.d.ts +103 -13
  138. package/lib/launcher/internal/watch/WatchTopology.js +380 -95
  139. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  140. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.d.ts +121 -0
  141. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js +3 -0
  142. package/lib/launcher/internal/watch/WatchTopologyWorkerProtocol.js.map +1 -0
  143. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.d.ts +4 -1
  144. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js +3 -2
  145. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js.map +1 -1
  146. package/lib/launcher/internal/watch/serveWatchTopologyWorker.d.ts +19 -0
  147. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js +62 -0
  148. package/lib/launcher/internal/watch/serveWatchTopologyWorker.js.map +1 -0
  149. package/lib/launcher/internal/watch/watchTopologyWorker.d.ts +1 -0
  150. package/lib/launcher/internal/watch/watchTopologyWorker.js +22 -0
  151. package/lib/launcher/internal/watch/watchTopologyWorker.js.map +1 -0
  152. package/lib/plugin/CapabilityPluginResolver.d.ts +1 -1
  153. package/lib/plugin/CapabilityPluginResolver.js +3 -2
  154. package/lib/plugin/CapabilityPluginResolver.js.map +1 -1
  155. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +15 -12
  156. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +13 -11
  157. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  158. package/lib/plugin/internal/load/PluginPackageResolution.d.ts +2 -2
  159. package/lib/plugin/internal/load/PluginPackageResolution.js +13 -7
  160. package/lib/plugin/internal/load/PluginPackageResolution.js.map +1 -1
  161. package/lib/plugin/internal/load/ProjectPluginEntries.d.ts +23 -7
  162. package/lib/plugin/internal/load/ProjectPluginEntries.js +36 -14
  163. package/lib/plugin/internal/load/ProjectPluginEntries.js.map +1 -1
  164. package/lib/plugin/internal/load/collectProjectHostInputs.d.ts +3 -1
  165. package/lib/plugin/internal/load/collectProjectHostInputs.js +5 -2
  166. package/lib/plugin/internal/load/collectProjectHostInputs.js.map +1 -1
  167. package/lib/plugin/internal/load/hasProjectPluginEntries.d.ts +3 -1
  168. package/lib/plugin/internal/load/hasProjectPluginEntries.js +4 -2
  169. package/lib/plugin/internal/load/hasProjectPluginEntries.js.map +1 -1
  170. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +36 -5
  171. package/lib/plugin/internal/load/loadProjectPlugins.js +477 -421
  172. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  173. package/lib/plugin/internal/load/pluginResolutionFailure.d.ts +18 -0
  174. package/lib/plugin/internal/load/pluginResolutionFailure.js +32 -0
  175. package/lib/plugin/internal/load/pluginResolutionFailure.js.map +1 -0
  176. package/lib/plugin/internal/load/resolveNativeSource.js +2 -3
  177. package/lib/plugin/internal/load/resolveNativeSource.js.map +1 -1
  178. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +1 -1
  179. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +3 -2
  180. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  181. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +4 -1
  182. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +9 -2
  183. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  184. package/lib/structures/ITtscCompilerContext.d.ts +12 -12
  185. package/lib/structures/ITtscPlugin.d.ts +7 -5
  186. package/lib/structures/ITtscPluginFactoryContext.d.ts +9 -8
  187. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -2
  188. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +12 -6
  189. package/package.json +9 -9
  190. package/src/TtscCompiler.ts +1 -1
  191. package/src/compiler/internal/ResidentCheckProcess.ts +159 -374
  192. package/src/compiler/internal/ResidentCheckProcessHost.ts +502 -0
  193. package/src/compiler/internal/ResidentCheckProcessProtocol.ts +53 -0
  194. package/src/compiler/internal/build/BuildExecution.ts +123 -11
  195. package/src/compiler/internal/build/ResidentCheckWatchSession.ts +41 -24
  196. package/src/compiler/internal/build/RunBuildOptions.ts +9 -5
  197. package/src/compiler/internal/build/TsgoArguments.ts +4 -1
  198. package/src/compiler/internal/build/WatchInputPublication.ts +106 -0
  199. package/src/compiler/internal/build/runBuild.ts +58 -34
  200. package/src/compiler/internal/buildNativeCompiler.ts +4 -0
  201. package/src/compiler/internal/compileProjectInMemory.ts +8 -2
  202. package/src/compiler/internal/project/CompilerProjectSelection.ts +14 -14
  203. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +42 -16
  204. package/src/compiler/internal/readEffectiveCompilerOptions.ts +1 -1
  205. package/src/compiler/internal/resolveBinary.ts +4 -1
  206. package/src/compiler/internal/resolveTsgo.ts +7 -0
  207. package/src/compiler/internal/runSingleFileEmit.ts +145 -101
  208. package/src/compiler/internal/startResidentTransform.ts +9 -2
  209. package/src/compiler/internal/transformProjectInMemory.ts +12 -3
  210. package/src/compiler/internal/transformProjectInWorker.ts +10 -2
  211. package/src/flags/ParseOptions.ts +2 -2
  212. package/src/internal/E2ETrace.ts +3 -1
  213. package/src/internal/OwnedNativeProcess.ts +2 -2
  214. package/src/internal/SourceNativeRetirement.ts +42 -19
  215. package/src/internal/TtsxNodeOptions.ts +124 -0
  216. package/src/internal/javascriptRuntimeCapabilities.ts +6 -1
  217. package/src/internal/parseWindowsDirectoryCaseSensitivity.ts +5 -5
  218. package/src/internal/pathIdentity/createCaseSensitivityObservations.ts +242 -0
  219. package/src/internal/pathIdentity/createFilesystemPathIdentityContext.ts +2 -169
  220. package/src/internal/projectInputPathIdentity.ts +1 -0
  221. package/src/internal/workerModuleUrl.ts +34 -0
  222. package/src/launcher/internal/buildSingleRootProject.ts +2 -0
  223. package/src/launcher/internal/getCompilerVersionText.ts +3 -2
  224. package/src/launcher/internal/parseTtsxCLI.ts +5 -4
  225. package/src/launcher/internal/prepareExecution.ts +6 -1
  226. package/src/launcher/internal/resolveSingleFileOutput.ts +8 -9
  227. package/src/launcher/internal/runTtsc.ts +127 -49
  228. package/src/launcher/internal/runTtsx.ts +17 -8
  229. package/src/launcher/internal/runtime/CommonJsRuntimeSource.ts +11 -9
  230. package/src/launcher/internal/runtime/RuntimeEntrySources.ts +57 -0
  231. package/src/launcher/internal/runtime/RuntimeProjectOwnership.ts +23 -10
  232. package/src/launcher/internal/runtime/advertiseTypeScriptExtensions.ts +25 -0
  233. package/src/launcher/internal/runtime/commonJsExportNames.ts +11 -4
  234. package/src/launcher/internal/runtime/installRuntimeHooks.ts +62 -16
  235. package/src/launcher/internal/runtime/resolveRuntimeCleanTargets.ts +17 -5
  236. package/src/launcher/internal/watch/AsyncWatchTopology.ts +267 -0
  237. package/src/launcher/internal/watch/WatchSettlement.ts +67 -0
  238. package/src/launcher/internal/watch/WatchTopology.ts +461 -102
  239. package/src/launcher/internal/watch/WatchTopologyWorkerProtocol.ts +106 -0
  240. package/src/launcher/internal/watch/projectInputReloadEventShouldNotify.ts +14 -9
  241. package/src/launcher/internal/watch/serveWatchTopologyWorker.ts +75 -0
  242. package/src/launcher/internal/watch/watchTopologyWorker.ts +29 -0
  243. package/src/plugin/CapabilityPluginResolver.ts +9 -2
  244. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +17 -12
  245. package/src/plugin/internal/load/PluginPackageResolution.ts +14 -9
  246. package/src/plugin/internal/load/ProjectPluginEntries.ts +40 -15
  247. package/src/plugin/internal/load/collectProjectHostInputs.ts +7 -1
  248. package/src/plugin/internal/load/hasProjectPluginEntries.ts +8 -3
  249. package/src/plugin/internal/load/loadProjectPlugins.ts +683 -593
  250. package/src/plugin/internal/load/pluginResolutionFailure.ts +37 -0
  251. package/src/plugin/internal/load/resolveNativeSource.ts +10 -4
  252. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +8 -2
  253. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +10 -1
  254. package/src/structures/ITtscCompilerContext.ts +12 -12
  255. package/src/structures/ITtscPlugin.ts +7 -5
  256. package/src/structures/ITtscPluginFactoryContext.ts +9 -8
  257. package/src/structures/internal/TtscCommonOptions.ts +2 -2
  258. package/src/structures/internal/TtscSingleFileEmitOptions.ts +22 -11
  259. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +0 -78
  260. package/internal/graphsymbols/provider.go +0 -650
  261. package/internal/lspserver/lsp_symbols.go +0 -212
@@ -7,10 +7,12 @@ import { CompilerProjectSelection } from "../../../compiler/internal/project/Com
7
7
  import { readJsoncFile } from "../../../compiler/internal/project/readJsoncFile";
8
8
  import { readProjectConfig } from "../../../compiler/internal/project/readProjectConfig";
9
9
  import { resolveTsgo } from "../../../compiler/internal/resolveTsgo";
10
+ import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
10
11
  import { spawnNative } from "../../../compiler/internal/spawnNative";
11
12
  import { normalizeCompilerEnumValue } from "../../../flags/normalizeCompilerEnumValue";
12
13
  import { readCompilerOptionValues } from "../../../flags/readCompilerOptionValues";
13
14
  import { type ProjectInputPathIdentityContext } from "../../../internal/pathIdentity/ProjectInputPathIdentityContext";
15
+ import { createCaseSensitivityObservations } from "../../../internal/pathIdentity/createCaseSensitivityObservations";
14
16
  import { createProjectInputPathIdentityContext } from "../../../internal/pathIdentity/createProjectInputPathIdentityContext";
15
17
  import { isProjectInputPathIdentityWithin } from "../../../internal/pathIdentity/isProjectInputPathIdentityWithin";
16
18
  import { resolveProjectInputPath } from "../../../internal/pathIdentity/resolveProjectInputPath";
@@ -46,26 +48,26 @@ import { watchDirectory } from "./watchDirectory";
46
48
  * output for source and declaration membership. An explicit reader supplies the
47
49
  * same absolute-path membership and failure contract for each resolved project.
48
50
  * Configuration files, project-reference roots, output inference and native
49
- * filesystem identity remain this topology's responsibility.
50
- * Response frames are observed once per project refresh at its compiler cwd;
51
- * that option view serves all project-output queries while the original argv
52
- * reaches compiler membership. A second observation rejects response changes
53
- * across membership resolution. Response files join the config reload set;
54
- * these observations do not pin files or detect a change restored in between.
51
+ * filesystem identity remain this topology's responsibility. Response frames
52
+ * are observed once per project refresh at its compiler cwd; that option view
53
+ * serves all project-output queries while the original argv reaches compiler
54
+ * membership. A second observation rejects response changes across membership
55
+ * resolution. Response files join the config reload set; these observations do
56
+ * not pin files or detect a change restored in between.
55
57
  *
56
58
  * `TTSC_WATCH_DEBUG_INPUTS` reports the named event, observed population deltas
57
59
  * and reload decision. These diagnostics reuse the decision's existing inputs;
58
60
  * they perform no extra filesystem observations.
59
61
  *
60
62
  * @evidence contracts/common.md#principled-implementation The compiler-input operation's membership, published rule inputs and actual content fingerprints qualify notifications; its native default obtains the real compiler list. Post-registration reconciliation compares baselines to recover observed handoff changes, not an atomic guarantee against every transient mutation. Explicit operations preserve native defaults without replacing global filesystem or compiler methods. Project-input attribution: Observed member deltas establish attribution; stale selected names cannot override them, while directory names retain supported population causality and unrelated multiple members remain unnamed.
61
- * @evidence contracts/common.md#clear-and-simple-design Compiler, plugin and project-input watch populations keep their own baselines and callbacks under one topology owner; small classifiers separate membership, selection and handle replacement. Four distinct operations own directory subscriptions, file subscriptions, immediate reload-directory reads and compiler membership; existing callers need no new argument. Project-input attribution: One private reconciliation separates member deltas from immediate resolution-directory digests before choosing a supported event name or an observed single member.
63
+ * @evidence contracts/common.md#clear-and-simple-design Compiler, plugin and project-input watch populations keep their own baselines and callbacks under one topology owner; small classifiers separate membership, selection and handle replacement. Five distinct operations own directory subscriptions, file subscriptions, immediate reload-directory reads, compiler membership and each decision's case-policy observations; existing callers need no new argument. Project-input attribution: One private reconciliation separates member deltas from immediate resolution-directory digests before choosing a supported event name or an observed single member.
62
64
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Supported native backends and explicit gap/replacement ownership replace foreign watcher patching; failed observation is reported rather than represented as complete coverage. The constructor manufactures no observations and bypasses no input or content proof; supplied operations retain the existing callback and result contracts. Project-input attribution: Native event names do not manufacture a delta; the current and previous physical membership identify already observed names without filename or fixture exceptions.
63
65
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain membership authority, plugin fingerprinting, publication races, physical registration and public lifecycle operations following the documentation skill. Parameters identify each supplied operation and native default beside the constructor. Project-input attribution: Native paragraphs explain stale attention, directory causality, accompanying resolution digests and unselected immediate entries.
64
66
  * @evidence contracts/portability.md#os-neutral-implementation Native paths use Node APIs and registration uses realpath; compiler/plugin maps preserve lexical aliases and fold only measured insensitive ASCII components. Project inputs use physical identities, while unknown/native Unicode relations remain conservative event candidates rather than identity proof. Registration paths and read options are passed unchanged; compiler-input readers supply absolute membership paths without replacing the topology's native identity policy. Project-input attribution: The supplied filesystem identity transaction establishes selected names and ancestry without an OS-wide case assumption or lexical alias guess.
65
67
  *
66
- * @evidence contracts/performance.md#efficient-algorithms Identity-keyed maps separate membership, baselines and handle registries; reconciliation delegates project/config discovery, native compiler listing, filesystem traversal/hashing and backend callbacks, with costs described by its members. Construction retains operation references and empty indexes without a filesystem observation or build. Attribution indexed visits additionally pay path/key text, ancestor and native identity work; shared ancestor keys avoid repeating the same walk within a transaction.
67
- * @evidence contracts/performance.md#reuse-equivalent-work Current membership, content baselines and live registrations belong to one topology; reconciliation owners decide their reuse against fresh premises. Supplied operation identities are retained without adding an observation-result cache or assuming changed inputs remain equivalent. Project-input attribution: This classifier borrows one scan's deltas and identity transaction without retaining cross-event answers.
68
- * @evidence contracts/performance.md#bound-retention-and-release-resources Current and pending maps and live subscriptions remain topology-owned without a population cap; reconciliation attempts obsolete handle retirement. Constructor acquires no handle; directory arrays and attribution deltas are call-local. close invokes supplied close methods but does not await backend closure, and a thrown close can interrupt later map cleanup; retained snapshots remain until the facade is released.
68
+ * @evidence contracts/performance.md#efficient-algorithms Identity-keyed maps separate membership, baselines and handle registries; reconciliation delegates project/config discovery, native compiler listing, filesystem traversal/hashing and backend callbacks, with costs described by its members. A directory-triggered refresh additionally visits R active reload files even on failure and compares their metadata/owner snapshots, reading bytes only when those snapshots changed. Changed-owner and notification arrays and a reported-key set require O(R) references; moved owners share one existing watcher synchronization and post-registration reconciliation, whose selected-file traversal remains part of the operation. No additional compiler listing is requested. Construction retains operation references and empty indexes without a filesystem observation or build. Attribution indexed visits additionally pay path/key text, ancestor and native identity work; shared ancestor keys avoid repeating the same walk within a transaction.
69
+ * @evidence contracts/performance.md#reuse-equivalent-work Current membership, content baselines and live registrations belong to one topology; reconciliation owners decide their reuse against fresh premises. Supplied operation identities are retained without assuming changed inputs remain equivalent. Each project's last compiler listing is kept with its arguments and answers only a plugin-input change, which cannot move Program membership; every other refresh lists again. Notifications delivered in one poll iteration are decided together in the check phase, so one change lists and rescans once. Project-input attribution: This classifier borrows one scan's deltas and identity transaction without retaining cross-event answers.
70
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Current and pending maps and live subscriptions remain topology-owned without a population cap; compiler listings are replaced by each resolution and hold only its projects; reconciliation attempts obsolete handle retirement. Constructor acquires no handle; directory arrays and attribution deltas are call-local. close invokes supplied close methods but does not await backend closure, and a thrown close can interrupt later map cleanup; retained snapshots remain until the facade is released.
69
71
  */
70
72
  export class WatchTopology {
71
73
  private analysisOnly = false;
@@ -102,15 +104,42 @@ export class WatchTopology {
102
104
  /** The plugin inputs whose directories the last sync already watched. */
103
105
  private watchedExtraInputs = new Set<string>();
104
106
  private compilerFileSnapshots = new Map<string, CompilerFileSnapshot>();
107
+
108
+ /**
109
+ * The last compiler listing of each project the topology resolved, by its
110
+ * config path, with the arguments it was asked under (`listingQuestion`). A
111
+ * change of plugin inputs reuses it (samchon/ttsc#1741).
112
+ */
113
+ private compilerListings = new Map<string, CompilerListing>();
114
+
115
+ /**
116
+ * Notifications the current poll iteration delivered, decided together in the
117
+ * check phase (`deferNotification`).
118
+ */
119
+ private pendingNotifications = new Map<string, PendingNotification>();
120
+ private notificationsScheduled = false;
105
121
  private files = new Map<string, string>();
106
- private lexicalIdentities = createProjectInputPathIdentityContext({
107
- throwOnRealpathError: false,
108
- });
122
+
123
+ /**
124
+ * The current decision's directory case-policy observations, which every
125
+ * identity context the decision creates shares (samchon/ttsc#1742).
126
+ */
127
+ private caseObservations!: ReturnType<
128
+ typeof createCaseSensitivityObservations
129
+ >;
130
+ private lexicalIdentities!: ProjectInputPathIdentityContext;
109
131
  private fileWatchers = new Map<string, fs.FSWatcher>();
110
132
  private observedDirectories = new Map<string, string>();
111
133
  private outputFiles = new Map<string, string>();
112
134
  private outputs = new Map<string, string>();
113
135
  private projectInputFingerprints = new Map<string, string>();
136
+
137
+ /**
138
+ * The metadata and physical owner each matched project-input file had when
139
+ * its fingerprint was taken, by match key; a named notification re-reads only
140
+ * the files whose metadata moved (samchon/ttsc#1748).
141
+ */
142
+ private projectInputFileSnapshots = new Map<string, CompilerFileSnapshot>();
114
143
  private projectInputMatches = new Map<string, string>();
115
144
  private projectInputs: ITtscProjectInputSnapshot = {
116
145
  files: [],
@@ -154,6 +183,8 @@ export class WatchTopology {
154
183
  * membership; the default invokes the native compiler. Readers throw
155
184
  * listing failures and retain ownership of their returned array, which this
156
185
  * topology does not mutate.
186
+ * @param observeCaseSensitivity Create one decision's directory case-policy
187
+ * observations; the default asks the native filesystem.
157
188
  */
158
189
  public constructor(
159
190
  private readonly options: WatchTopologyOptions,
@@ -162,7 +193,10 @@ export class WatchTopology {
162
193
  private readonly openFileWatch: typeof fs.watch = fs.watch,
163
194
  private readonly readDirectory: typeof fs.readdirSync = fs.readdirSync,
164
195
  private readonly getCompilerInputs: typeof listCompilerInputs = listCompilerInputs,
165
- ) {}
196
+ private readonly observeCaseSensitivity: typeof createCaseSensitivityObservations = createCaseSensitivityObservations,
197
+ ) {
198
+ this.beginLexicalTransaction();
199
+ }
166
200
 
167
201
  /**
168
202
  * Re-resolve compiler inputs and notify when the resolved topology changes,
@@ -183,17 +217,67 @@ export class WatchTopology {
183
217
  this.refreshCompilerInputs(notify, false);
184
218
  }
185
219
 
220
+ /**
221
+ * Wait until delivered notifications and registration reconciliation finish.
222
+ *
223
+ * A completed build does not imply a quiet topology: its final refresh can
224
+ * queue another decision and another build (#1756). Yield through the check
225
+ * phase before inspecting every scheduled owner so a decision queued by
226
+ * another decision remains part of this barrier. Later filesystem changes
227
+ * belong to a later barrier; this does not freeze the filesystem.
228
+ *
229
+ * @evidence contracts/common.md#principled-implementation A check-phase observation follows delivered events, and every notification, recovery and registration queue must be empty before completion.
230
+ * @evidence contracts/common.md#clear-and-simple-design The topology owns the barrier because it alone owns all five scheduled queues.
231
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Queue state establishes completion without a quiet-time estimate or an invented filesystem snapshot.
232
+ * @evidence contracts/common.md#meaningful-documentation Native prose distinguishes delivered work from later mutations and explains the post-build queued-decision race.
233
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation The barrier observes owned JavaScript queues without a separate native operation.
234
+ * @evidence contracts/performance.md#efficient-algorithms Each check turn inspects five flags and two map sizes; notification processing retains its owning traversal and native-operation costs.
235
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work Each call observes current work rather than reusing an earlier completion.
236
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One immediate callback and promise remain live per waiting call; continuous new work can prevent completion and no deadline is imposed.
237
+ */
238
+ public async whenIdle(): Promise<void> {
239
+ do {
240
+ await new Promise<void>((resolve) => setImmediate(resolve));
241
+ } while (
242
+ !this.closed &&
243
+ (this.notificationsScheduled ||
244
+ this.pluginNotificationsScheduled ||
245
+ this.compilerPostRegistrationReconciliationScheduled ||
246
+ this.projectInputRecoveryScheduled ||
247
+ this.projectInputPostRegistrationReconciliationScheduled ||
248
+ this.pendingNotifications.size !== 0 ||
249
+ this.pendingPluginNotifications.size !== 0)
250
+ );
251
+ }
252
+
253
+ /**
254
+ * @param reuseListings Answer each project's membership with its last listing
255
+ * under the same arguments instead of asking the compiler again. Only a
256
+ * refresh whose cause cannot move Program membership passes it.
257
+ */
186
258
  private refreshCompilerInputs(
187
259
  notify: boolean,
188
260
  skipUnobservedProjectInputWatchRoots: boolean,
261
+ reuseListings: boolean = false,
189
262
  ): void {
190
263
  this.beginLexicalTransaction();
264
+ const listings = new Map<string, CompilerListing>();
191
265
  const next = resolveWatchTopology(
192
266
  this.options,
193
267
  this.extraInputs,
194
268
  this.lexicalIdentities,
195
- this.getCompilerInputs,
269
+ (project, options) => {
270
+ const question = listingQuestion(project, options);
271
+ const last = this.compilerListings.get(project.path);
272
+ const inputs =
273
+ reuseListings && last?.question === question
274
+ ? last.inputs
275
+ : this.getCompilerInputs(project, options);
276
+ listings.set(project.path, { inputs, question });
277
+ return inputs;
278
+ },
196
279
  );
280
+ this.compilerListings = listings;
197
281
  const compilerProgramMembershipChange =
198
282
  next.analysisOnly &&
199
283
  WatchPaths.mapsEqual(this.reloadFiles, next.reloadFiles) &&
@@ -204,6 +288,7 @@ export class WatchTopology {
204
288
  const projectInputProgramOverlap = projectInputCompilerMembershipChange(
205
289
  this.projectInputs,
206
290
  compilerProgramMembershipChange,
291
+ this.projectInputIdentities(),
207
292
  );
208
293
  const projectInputProgramChange =
209
294
  compilerProgramMembershipChange.length !== 0 &&
@@ -293,7 +378,7 @@ export class WatchTopology {
293
378
  changed: readonly string[],
294
379
  ): boolean {
295
380
  const matches = this.collectProjectInputMatches();
296
- const fingerprints = fingerprintProjectInputMatches(
381
+ const { fingerprints, snapshots } = fingerprintProjectInputMatches(
297
382
  matches,
298
383
  this.readDirectory,
299
384
  );
@@ -307,6 +392,7 @@ export class WatchTopology {
307
392
  const causedBy = projectInputCompilerMembershipProjectChanges(
308
393
  changed,
309
394
  population.globs,
395
+ this.projectInputIdentities(),
310
396
  );
311
397
  const reloadInput = {
312
398
  causedBy,
@@ -316,7 +402,10 @@ export class WatchTopology {
316
402
  reloadDirectories: population.reloadDirectories,
317
403
  reloadFiles: population.reloadFiles ?? [],
318
404
  };
319
- const reload = projectInputReloadEventShouldNotify(reloadInput);
405
+ const reload = projectInputReloadEventShouldNotify(
406
+ reloadInput,
407
+ this.projectInputIdentities(),
408
+ );
320
409
  reportProjectInputDecision({
321
410
  source: "compiler-membership",
322
411
  ...reloadInput,
@@ -328,6 +417,7 @@ export class WatchTopology {
328
417
  // instead of disappearing behind the warm compiler-membership handoff.
329
418
  this.projectInputMatches = matches;
330
419
  this.projectInputFingerprints = fingerprints;
420
+ this.projectInputFileSnapshots = snapshots;
331
421
  return reload;
332
422
  }
333
423
 
@@ -353,8 +443,8 @@ export class WatchTopology {
353
443
  * @evidence contracts/common.md#prohibited-implementation-shortcuts The actual plugin source digest applies its owning source-selection and pruning policy; named plugins or fixture output do not replace observed input state.
354
444
  * @evidence contracts/common.md#meaningful-documentation Native prose explains baseline timing and why existing inputs are not restamped following the documentation skill.
355
445
  * @evidence contracts/portability.md#os-neutral-implementation Native path resolution and canonical backend registration govern source trees; lexical keys preserve declarations and unknown names, folding ASCII only under measured insensitive parents rather than an OS-derived rule.
356
- * @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests, each proven from the selected files' metadata when the load's records hold and read from the selected pruned corpus otherwise; refresh(false) also pays project/compiler listing, directory traversal and watcher reconciliation; fixed publication count does not bound delegated bytes or backend work.
357
- * @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state; a new input's digest is the one the load recorded while its metadata holds.
446
+ * @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests, each proven from the selected files' metadata when the load's records hold and read from the selected pruned corpus otherwise; the following reconciliation pays project configuration, directory traversal and watcher work but answers membership from each project's last compiler listing; fixed publication count does not bound delegated bytes or backend work.
447
+ * @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; a changed set reuses each project's last compiler listing under the same arguments, since plugin inputs cannot move Program membership; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state; a new input's digest is the one the load recorded while its metadata holds.
358
448
  * @evidence contracts/performance.md#bound-retention-and-release-resources Publications replace input/digest populations and case-observation caches without a population cap; synchronization attempts obsolete watcher closure. Native/supplied closure has its own lifetime and can throw; owner close does not prove every backend has stopped or every retained snapshot byte was released.
359
449
  */
360
450
  public setExtraInputs(
@@ -382,7 +472,12 @@ export class WatchTopology {
382
472
  );
383
473
  }
384
474
  this.pluginInputStates = states;
385
- this.refresh(false);
475
+ // Plugin inputs join the watched files and directories, never Program
476
+ // membership, so the compiler's last answer stands. A load reports up to
477
+ // three different sets, and asking again for each spawned tsgo three times
478
+ // per load (samchon/ttsc#1741). A membership change meanwhile reaches the
479
+ // refresh its own notification starts, which lists again.
480
+ this.refreshCompilerInputs(false, false, true);
386
481
  }
387
482
 
388
483
  /**
@@ -399,7 +494,11 @@ export class WatchTopology {
399
494
  * @evidence contracts/performance.md#bound-retention-and-release-resources Publication prunes retired declaration-owner choices and replaces uncapped match/fingerprint populations. Watcher retirement invokes close without awaiting backend completion, and supplied errors can interrupt cleanup. Weak output-overlap keys do not retain old identity transactions; the current declaration snapshot can borrow caller arrays until replaced or the facade is released.
400
495
  */
401
496
  public setProjectInputs(inputs: ITtscProjectInputSnapshot): void {
402
- const next = normalizeProjectInputSnapshot(inputs);
497
+ this.beginLexicalTransaction();
498
+ const next = normalizeProjectInputSnapshot(
499
+ inputs,
500
+ this.projectInputIdentities(),
501
+ );
403
502
  // The declared spellings are recorded even when the normalized snapshot did
404
503
  // not move, because a republication can carry a new alias for identities
405
504
  // that already matched, and anchoring the spelling that was retired would
@@ -408,7 +507,13 @@ export class WatchTopology {
408
507
  inputs.declared === undefined
409
508
  ? inputs
410
509
  : { ...inputs.declared, root: inputs.root };
411
- if (projectInputSnapshotsEqual(this.projectInputs, next)) {
510
+ if (
511
+ projectInputSnapshotsEqual(
512
+ this.projectInputs,
513
+ next,
514
+ this.projectInputIdentities(),
515
+ )
516
+ ) {
412
517
  this.syncProjectInputWatchers();
413
518
  return;
414
519
  }
@@ -420,10 +525,12 @@ export class WatchTopology {
420
525
  { ...(inputs.declared ?? inputs), root: inputs.root },
421
526
  ]);
422
527
  this.projectInputMatches = this.collectProjectInputMatches();
423
- this.projectInputFingerprints = fingerprintProjectInputMatches(
528
+ const baseline = fingerprintProjectInputMatches(
424
529
  this.projectInputMatches,
425
530
  this.readDirectory,
426
531
  );
532
+ this.projectInputFingerprints = baseline.fingerprints;
533
+ this.projectInputFileSnapshots = baseline.snapshots;
427
534
  this.syncProjectInputWatchers();
428
535
  }
429
536
 
@@ -457,11 +564,26 @@ export class WatchTopology {
457
564
 
458
565
  /** Start fresh native case observations for one synchronous watch decision. */
459
566
  private beginLexicalTransaction(): void {
567
+ this.caseObservations = this.observeCaseSensitivity();
460
568
  this.lexicalIdentities = createProjectInputPathIdentityContext({
569
+ caseSensitive: this.caseObservations,
461
570
  throwOnRealpathError: false,
462
571
  });
463
572
  }
464
573
 
574
+ /**
575
+ * A strict project-input identity context for the current decision. Its
576
+ * physical paths are resolved afresh, and its case answers are the
577
+ * decision's: one decision builds such contexts per declaration and per
578
+ * candidate, and each asked every directory again, which on Windows is an
579
+ * `fsutil` process per question (samchon/ttsc#1742).
580
+ */
581
+ private projectInputIdentities(): ProjectInputPathIdentityContext {
582
+ return createProjectInputPathIdentityContext({
583
+ caseSensitive: this.caseObservations,
584
+ });
585
+ }
586
+
465
587
  /** Keep lexical alias ownership while sharing this decision's case probes. */
466
588
  private pathKey(location: string): string {
467
589
  return WatchPaths.pathKey(location, this.lexicalIdentities);
@@ -585,34 +707,17 @@ export class WatchTopology {
585
707
  this.openDirectoryWatch(
586
708
  watcherRegistrationPath(location),
587
709
  process.platform === "win32",
588
- (event, filename, gap) => {
589
- this.beginLexicalTransaction();
590
- const changed =
591
- filename === null ? undefined : path.resolve(location, filename);
592
- const pluginInput = changed ?? location;
593
- if (this.isPluginInput(pluginInput)) {
594
- this.notePluginNotification(pluginInput);
595
- return;
596
- }
597
- const plan = planCompilerDirectoryWatchEvent({
598
- changed,
599
- event,
600
- exists: fs.existsSync,
710
+ (event, filename, gap) =>
711
+ this.deferNotification({
712
+ gap: gap === true,
713
+ lane: "compiler",
601
714
  location,
602
- identities: this.lexicalIdentities,
603
- platform: process.platform,
604
- trackedFiles: this.files,
605
- });
606
- this.rearmFileWatchers(plan.rearm);
607
- for (const file of this.compilerChangesToReport(
608
- plan.changes,
609
- changed,
610
- gap === true,
611
- )) {
612
- this.reportCompilerFileChange(file);
613
- }
614
- if (plan.refresh) this.refreshFromDirectory(location, changed);
615
- },
715
+ changed:
716
+ filename === null
717
+ ? undefined
718
+ : path.resolve(location, filename),
719
+ rename: event === "rename",
720
+ }),
616
721
  ),
617
722
  (location, error) => this.callbacks.onError(location, error),
618
723
  () => this.closed === false,
@@ -622,6 +727,87 @@ export class WatchTopology {
622
727
  );
623
728
  }
624
729
 
730
+ /**
731
+ * Decide a notification in the event loop's check phase, together with every
732
+ * other notification the same poll iteration delivered (samchon/ttsc#1741).
733
+ *
734
+ * One filesystem change usually arrives as several notifications, rename and
735
+ * change or create and modify, and each listed the compiler inputs or
736
+ * rescanned the project inputs again. Identical notifications of one lane,
737
+ * location and path merge, keeping a rename or a gap any of them carried, and
738
+ * the batch refreshes compiler membership once after deciding them all. The
739
+ * refresh therefore starts after every notification it absorbs, and one
740
+ * delivered later schedules another batch.
741
+ */
742
+ private deferNotification(notification: PendingNotification): void {
743
+ if (this.closed) return;
744
+ const key = [
745
+ notification.lane,
746
+ notification.location,
747
+ notification.changed ?? "",
748
+ ].join("\0");
749
+ const pending = this.pendingNotifications.get(key);
750
+ if (pending !== undefined) {
751
+ pending.rename ||= notification.rename;
752
+ pending.gap ||= notification.gap;
753
+ return;
754
+ }
755
+ this.pendingNotifications.set(key, { ...notification });
756
+ if (this.notificationsScheduled) return;
757
+ this.notificationsScheduled = true;
758
+ setImmediate(() => this.decideNotifications());
759
+ }
760
+
761
+ private decideNotifications(): void {
762
+ this.notificationsScheduled = false;
763
+ const pending = [...this.pendingNotifications.values()];
764
+ this.pendingNotifications.clear();
765
+ if (this.closed) return;
766
+ const refreshes: DirectoryRefresh[] = [];
767
+ for (const notification of pending) {
768
+ if (notification.lane === "project-input") {
769
+ this.refreshProjectInputs(notification.location, notification.changed);
770
+ continue;
771
+ }
772
+ this.decideCompilerDirectoryNotification(notification, refreshes);
773
+ }
774
+ if (refreshes.length === 0 || this.closed) return;
775
+ this.beginLexicalTransaction();
776
+ this.refreshFromDirectories(refreshes);
777
+ }
778
+
779
+ /** Decide one merged directory notification of the compiler lane. */
780
+ private decideCompilerDirectoryNotification(
781
+ notification: PendingNotification,
782
+ refreshes: DirectoryRefresh[],
783
+ ): void {
784
+ this.beginLexicalTransaction();
785
+ const { changed, location } = notification;
786
+ const pluginInput = changed ?? location;
787
+ if (this.isPluginInput(pluginInput)) {
788
+ this.notePluginNotification(pluginInput);
789
+ return;
790
+ }
791
+ const plan = planCompilerDirectoryWatchEvent({
792
+ changed,
793
+ event: notification.rename ? "rename" : "change",
794
+ exists: fs.existsSync,
795
+ location,
796
+ identities: this.lexicalIdentities,
797
+ platform: process.platform,
798
+ trackedFiles: this.files,
799
+ });
800
+ this.rearmFileWatchers(plan.rearm);
801
+ for (const file of this.compilerChangesToReport(
802
+ plan.changes,
803
+ changed,
804
+ notification.gap,
805
+ )) {
806
+ this.reportCompilerFileChange(file);
807
+ }
808
+ if (plan.refresh) refreshes.push({ changed, location });
809
+ }
810
+
625
811
  /**
626
812
  * Reconcile tracked compiler files after a newly registered watcher returns.
627
813
  *
@@ -730,6 +916,7 @@ export class WatchTopology {
730
916
  if (fs.existsSync(location)) {
731
917
  if (this.projectInputMatches.size !== 0) {
732
918
  const key = createProjectInputPathIdentityContext({
919
+ caseSensitive: this.caseObservations,
733
920
  throwOnRealpathError: false,
734
921
  }).resolve(location).key;
735
922
  if (
@@ -840,7 +1027,7 @@ export class WatchTopology {
840
1027
  if (this.closed) return;
841
1028
  const previous = new Map(this.projectInputWatchers);
842
1029
  const previousLinks = new Map(this.projectInputLinkWatchers);
843
- const identities = createProjectInputPathIdentityContext();
1030
+ const identities = this.projectInputIdentities();
844
1031
  const desired = new Map<string, string>();
845
1032
  const required = new Map<string, string>();
846
1033
  for (const file of this.projectInputDeclarations("file")) {
@@ -924,11 +1111,17 @@ export class WatchTopology {
924
1111
  this.openDirectoryWatch(
925
1112
  watcherRegistrationPath(location),
926
1113
  true,
927
- (_event, filename) => {
928
- const changed =
929
- filename === null ? undefined : path.resolve(location, filename);
930
- this.refreshProjectInputs(location, changed);
931
- },
1114
+ (_event, filename) =>
1115
+ this.deferNotification({
1116
+ gap: false,
1117
+ lane: "project-input",
1118
+ location,
1119
+ changed:
1120
+ filename === null
1121
+ ? undefined
1122
+ : path.resolve(location, filename),
1123
+ rename: false,
1124
+ }),
932
1125
  ),
933
1126
  (location, error) => {
934
1127
  const key = identities.resolve(location).key;
@@ -1003,11 +1196,17 @@ export class WatchTopology {
1003
1196
  this.openDirectoryWatch(
1004
1197
  watcherRegistrationPath(location),
1005
1198
  false,
1006
- (_event, filename) => {
1007
- const changed =
1008
- filename === null ? undefined : path.resolve(location, filename);
1009
- this.refreshProjectInputs(location, changed);
1010
- },
1199
+ (_event, filename) =>
1200
+ this.deferNotification({
1201
+ gap: false,
1202
+ lane: "project-input",
1203
+ location,
1204
+ changed:
1205
+ filename === null
1206
+ ? undefined
1207
+ : path.resolve(location, filename),
1208
+ rename: false,
1209
+ }),
1011
1210
  ),
1012
1211
  (location, error) => this.callbacks.onError(location, error),
1013
1212
  () => this.closed === false,
@@ -1088,6 +1287,7 @@ export class WatchTopology {
1088
1287
  if (this.projectInputRecoveryScheduled) return;
1089
1288
  this.projectInputRecoveryScheduled = true;
1090
1289
  queueMicrotask(() => {
1290
+ this.beginLexicalTransaction();
1091
1291
  try {
1092
1292
  let previousRejectionCount = -1;
1093
1293
  while (
@@ -1131,6 +1331,7 @@ export class WatchTopology {
1131
1331
  queueMicrotask(() => {
1132
1332
  this.projectInputPostRegistrationReconciliationScheduled = false;
1133
1333
  if (this.closed) return;
1334
+ this.beginLexicalTransaction();
1134
1335
  this.refreshPublishedProjectInputIdentities();
1135
1336
  this.refreshProjectInputs(this.projectInputs.root, undefined, true);
1136
1337
  });
@@ -1145,8 +1346,13 @@ export class WatchTopology {
1145
1346
  * that must observe later target changes.
1146
1347
  */
1147
1348
  private refreshPublishedProjectInputIdentities(): void {
1148
- const next = normalizeProjectInputSnapshot(this.declaredProjectInputs);
1149
- if (projectInputSnapshotsEqual(this.projectInputs, next)) return;
1349
+ const identities = this.projectInputIdentities();
1350
+ const next = normalizeProjectInputSnapshot(
1351
+ this.declaredProjectInputs,
1352
+ identities,
1353
+ );
1354
+ if (projectInputSnapshotsEqual(this.projectInputs, next, identities))
1355
+ return;
1150
1356
  this.projectInputs = next;
1151
1357
  this.pruneProjectInputWatchRoots([next, this.declaredProjectInputs]);
1152
1358
  }
@@ -1264,6 +1470,7 @@ export class WatchTopology {
1264
1470
  const resolved = ProjectInputWatchRules.projectInputRecursiveWatchRoot(
1265
1471
  target,
1266
1472
  this.projectInputs.root,
1473
+ this.projectInputIdentities(),
1267
1474
  );
1268
1475
  if (resolved !== undefined) this.projectInputWatchRoots.set(key, resolved);
1269
1476
  return resolved;
@@ -1277,7 +1484,7 @@ export class WatchTopology {
1277
1484
  this.beginLexicalTransaction();
1278
1485
  try {
1279
1486
  const previous = this.projectInputMatches;
1280
- const identities = createProjectInputPathIdentityContext();
1487
+ const identities = this.projectInputIdentities();
1281
1488
  // One population for the whole decision. Every question below is asked of
1282
1489
  // the same declarations, and rebuilding it per question would both cost
1283
1490
  // more and let two answers disagree about what was declared.
@@ -1315,13 +1522,30 @@ export class WatchTopology {
1315
1522
  }
1316
1523
  const next = this.collectProjectInputMatches();
1317
1524
  const membershipChanged = WatchPaths.mapsEqual(previous, next) === false;
1318
- const nextFingerprints =
1525
+ // A named notification reads the named path's bytes and any file whose
1526
+ // metadata or owner moved; one that names nothing reads every file
1527
+ // (samchon/ttsc#1748).
1528
+ const scanned =
1319
1529
  changed === undefined ||
1320
1530
  membershipChanged ||
1321
1531
  directlyMatched ||
1322
1532
  topologyMatched
1323
- ? fingerprintProjectInputMatches(next, this.readDirectory)
1324
- : this.projectInputFingerprints;
1533
+ ? fingerprintProjectInputMatches(
1534
+ next,
1535
+ this.readDirectory,
1536
+ changed === undefined
1537
+ ? undefined
1538
+ : {
1539
+ fingerprints: this.projectInputFingerprints,
1540
+ named: identities.resolve(changed).key,
1541
+ snapshots: this.projectInputFileSnapshots,
1542
+ },
1543
+ )
1544
+ : {
1545
+ fingerprints: this.projectInputFingerprints,
1546
+ snapshots: this.projectInputFileSnapshots,
1547
+ };
1548
+ const nextFingerprints = scanned.fingerprints;
1325
1549
  const contentChanged =
1326
1550
  WatchPaths.mapsEqual(
1327
1551
  this.projectInputFingerprints,
@@ -1352,7 +1576,10 @@ export class WatchTopology {
1352
1576
  reloadDirectories: population.reloadDirectories ?? [],
1353
1577
  reloadFiles: population.reloadFiles ?? [],
1354
1578
  };
1355
- const reload = projectInputReloadEventShouldNotify(reloadInput);
1579
+ const reload = projectInputReloadEventShouldNotify(
1580
+ reloadInput,
1581
+ identities,
1582
+ );
1356
1583
  const invalidate = projectInputMembershipInvalidatesProgram({
1357
1584
  changed: reconciledChange,
1358
1585
  changedInputs,
@@ -1362,6 +1589,7 @@ export class WatchTopology {
1362
1589
  });
1363
1590
  this.projectInputMatches = next;
1364
1591
  this.projectInputFingerprints = nextFingerprints;
1592
+ this.projectInputFileSnapshots = scanned.snapshots;
1365
1593
  this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
1366
1594
  // A JSON/TS/JS project-input member can simultaneously enter or leave
1367
1595
  // the compiler Program. Reconcile the compiler watch snapshot before
@@ -1416,7 +1644,7 @@ export class WatchTopology {
1416
1644
  }
1417
1645
 
1418
1646
  private collectProjectInputMatches(): Map<string, string> {
1419
- const identities = createProjectInputPathIdentityContext();
1647
+ const identities = this.projectInputIdentities();
1420
1648
  const matches = new Map<string, string>();
1421
1649
  // Both spellings are scanned, and each is resolved here rather than when
1422
1650
  // the snapshot arrived. A declaration reached through a symlink otherwise
@@ -1510,25 +1738,78 @@ export class WatchTopology {
1510
1738
  }
1511
1739
 
1512
1740
  private refreshFromDirectory(location: string, changed?: string): void {
1513
- if (
1514
- changed !== undefined &&
1515
- WatchPaths.isDirectory(changed) &&
1516
- this.isCompilerOutputDirectory(changed) === false &&
1517
- this.isProjectInputDirectory(changed) === false
1518
- ) {
1519
- this.observedDirectories.set(this.pathKey(changed), changed);
1741
+ this.refreshFromDirectories([{ changed, location }]);
1742
+ }
1743
+
1744
+ /**
1745
+ * Refresh compiler membership once for every directory a decision asked
1746
+ * about. A failed refresh still reports, once each, the reload inputs every
1747
+ * requested change names.
1748
+ *
1749
+ * A replacement can be delivered under only its vanished temporary name,
1750
+ * including when its malformed bytes prevent refresh. Reconcile active
1751
+ * reload-file snapshots on either outcome, deduplicate failed-refresh
1752
+ * recovery against actual reported movement, and preserve the original
1753
+ * refresh error after recovery. Batch moved owners through the existing rearm
1754
+ * operation before notifying, because a POSIX per-file handle still owns the
1755
+ * old inode. Unchanged snapshots stay quiet unless the existing
1756
+ * failed-refresh selector explicitly names or cannot find the reload input.
1757
+ */
1758
+ private refreshFromDirectories(refreshes: readonly DirectoryRefresh[]): void {
1759
+ for (const { changed } of refreshes) {
1760
+ if (
1761
+ changed !== undefined &&
1762
+ WatchPaths.isDirectory(changed) &&
1763
+ this.isCompilerOutputDirectory(changed) === false &&
1764
+ this.isProjectInputDirectory(changed) === false
1765
+ ) {
1766
+ this.observedDirectories.set(this.pathKey(changed), changed);
1767
+ }
1520
1768
  }
1769
+ let refreshFailure: { error: unknown } | undefined;
1521
1770
  try {
1522
1771
  this.refresh(true);
1523
1772
  } catch (error) {
1524
- for (const reload of reloadInputsForFailedTopologyRefresh(
1525
- this.reloadFiles.values(),
1526
- changed,
1527
- this.lexicalIdentities,
1528
- )) {
1529
- this.callbacks.onInputChange({ kind: "config", path: reload });
1773
+ refreshFailure = { error };
1774
+ }
1775
+ try {
1776
+ if (this.closed) return;
1777
+ const changed: string[] = [];
1778
+ const rearm: string[] = [];
1779
+ const reported = new Set<string>();
1780
+ for (const reload of this.reloadFiles.values()) {
1781
+ if (this.closed) return;
1782
+ const movement = this.compilerFileMovement(reload);
1783
+ if (movement.owner) rearm.push(reload);
1784
+ if (this.compilerMovementReports(reload, movement))
1785
+ changed.push(reload);
1530
1786
  }
1531
- this.callbacks.onError(location, error);
1787
+ if (rearm.length !== 0) this.rearmFileWatchers(rearm, true);
1788
+ for (const reload of changed) {
1789
+ if (this.closed) return;
1790
+ reported.add(this.pathKey(reload));
1791
+ this.reportCompilerFileChange(reload);
1792
+ }
1793
+ if (refreshFailure !== undefined) {
1794
+ for (const { changed } of refreshes) {
1795
+ for (const reload of reloadInputsForFailedTopologyRefresh(
1796
+ this.reloadFiles.values(),
1797
+ changed,
1798
+ this.lexicalIdentities,
1799
+ )) {
1800
+ if (this.closed) return;
1801
+ const key = this.pathKey(reload);
1802
+ if (reported.has(key)) continue;
1803
+ reported.add(key);
1804
+ this.callbacks.onInputChange({ kind: "config", path: reload });
1805
+ }
1806
+ }
1807
+ }
1808
+ } catch (error) {
1809
+ this.callbacks.onError(refreshes[0]!.location, error);
1810
+ } finally {
1811
+ if (refreshFailure !== undefined)
1812
+ this.callbacks.onError(refreshes[0]!.location, refreshFailure.error);
1532
1813
  }
1533
1814
  }
1534
1815
 
@@ -1591,7 +1872,7 @@ export class WatchTopology {
1591
1872
 
1592
1873
  private isProjectInputDirectory(location: string): boolean {
1593
1874
  const resolved = path.resolve(location);
1594
- const identities = createProjectInputPathIdentityContext();
1875
+ const identities = this.projectInputIdentities();
1595
1876
  return (
1596
1877
  this.projectInputs.files.some(
1597
1878
  (file) =>
@@ -1764,6 +2045,46 @@ type CompilerFileSnapshot = {
1764
2045
  owner: string;
1765
2046
  };
1766
2047
 
2048
+ type PendingNotification = {
2049
+ changed: string | undefined;
2050
+ gap: boolean;
2051
+ lane: "compiler" | "project-input";
2052
+ location: string;
2053
+ rename: boolean;
2054
+ };
2055
+
2056
+ type DirectoryRefresh = {
2057
+ changed: string | undefined;
2058
+ location: string;
2059
+ };
2060
+
2061
+ type CompilerListing = {
2062
+ /** The reader's own array, which the topology never mutates. */
2063
+ inputs: string[];
2064
+ question: string;
2065
+ };
2066
+
2067
+ /**
2068
+ * Everything a compiler listing of `project` is asked with: the project, and
2069
+ * each option `listCompilerInputs` passes to the compiler. Two refreshes with
2070
+ * the same question ask the compiler the same thing; only the disk can answer
2071
+ * them differently.
2072
+ */
2073
+ function listingQuestion(
2074
+ project: ITtscParsedProjectConfig,
2075
+ options: WatchTopologyOptions,
2076
+ ): string {
2077
+ return JSON.stringify([
2078
+ project.path,
2079
+ project.root,
2080
+ options.binary ?? null,
2081
+ options.compilerArgsCwd ?? null,
2082
+ options.env ?? null,
2083
+ options.passthrough ?? null,
2084
+ options.pinCompilerProject ?? null,
2085
+ ]);
2086
+ }
2087
+
1767
2088
  type CompilerFileMovement = {
1768
2089
  content: boolean;
1769
2090
  owner: boolean;
@@ -1836,7 +2157,10 @@ function resolveWatchTopology(
1836
2157
  addPaths(reloadFiles, inspected.responseFiles, identities);
1837
2158
  const compilerInputs = getCompilerInputs(project, options);
1838
2159
  CompilerProjectSelection.assertCurrent(selection);
1839
- const compilerOutputs = resolveCompilerOutputs(project, inspected.options);
2160
+ const compilerOutputs = resolveCompilerOutputs(
2161
+ project,
2162
+ inspected.options,
2163
+ );
1840
2164
  addPaths(outputFiles, compilerOutputs.files, identities);
1841
2165
  addPaths(
1842
2166
  outputFiles,
@@ -1949,10 +2273,11 @@ function listCompilerInputs(
1949
2273
  project: ITtscParsedProjectConfig,
1950
2274
  options: WatchTopologyOptions,
1951
2275
  ): string[] {
2276
+ const env = SidecarEnvironment.merge(process.env, options.env);
1952
2277
  const tsgo = resolveTsgo({
1953
2278
  binary: options.binary,
1954
2279
  cwd: project.root,
1955
- env: options.env,
2280
+ env,
1956
2281
  });
1957
2282
  const result = spawnNative(
1958
2283
  tsgo.binary,
@@ -1967,7 +2292,7 @@ function listCompilerInputs(
1967
2292
  ],
1968
2293
  {
1969
2294
  cwd: options.compilerArgsCwd ?? project.root,
1970
- env: { ...process.env, ...options.env },
2295
+ env,
1971
2296
  encoding: "utf8",
1972
2297
  },
1973
2298
  );
@@ -2355,8 +2680,8 @@ function uniqueExistingPaths(
2355
2680
 
2356
2681
  function normalizeProjectInputSnapshot(
2357
2682
  snapshot: ITtscProjectInputSnapshot,
2683
+ identities: ProjectInputPathIdentityContext,
2358
2684
  ): ITtscProjectInputSnapshot {
2359
- const identities = createProjectInputPathIdentityContext();
2360
2685
  const files = new Map<string, string>();
2361
2686
  const globs = new Map<string, string>();
2362
2687
  const reloadDirectories = new Map<string, string>();
@@ -2389,8 +2714,8 @@ function normalizeProjectInputSnapshot(
2389
2714
  function projectInputSnapshotsEqual(
2390
2715
  left: ITtscProjectInputSnapshot,
2391
2716
  right: ITtscProjectInputSnapshot,
2717
+ identities: ProjectInputPathIdentityContext,
2392
2718
  ): boolean {
2393
- const identities = createProjectInputPathIdentityContext();
2394
2719
  return (
2395
2720
  // Every sibling comparison in this class decides on identity, and this one
2396
2721
  // is only safe today because both operands come from the same normalizer.
@@ -2445,7 +2770,7 @@ function isSymbolicLink(location: string): boolean {
2445
2770
  function matchesProjectInput(
2446
2771
  snapshot: ITtscProjectInputSnapshot,
2447
2772
  location: string,
2448
- identities = createProjectInputPathIdentityContext(),
2773
+ identities: ProjectInputPathIdentityContext,
2449
2774
  ): boolean {
2450
2775
  const key = identities.resolve(location).key;
2451
2776
  return (
@@ -2572,8 +2897,8 @@ function compilerMembershipChange(
2572
2897
  function projectInputCompilerMembershipChange(
2573
2898
  snapshot: ITtscProjectInputSnapshot,
2574
2899
  changed: readonly string[],
2900
+ identities: ProjectInputPathIdentityContext,
2575
2901
  ): string[] {
2576
- const identities = createProjectInputPathIdentityContext();
2577
2902
  return changed.filter(
2578
2903
  (location) =>
2579
2904
  matchesProjectInput(snapshot, location, identities) &&
@@ -2593,8 +2918,8 @@ function projectInputCompilerMembershipChange(
2593
2918
  function projectInputCompilerMembershipProjectChanges(
2594
2919
  locations: readonly string[],
2595
2920
  globs: readonly string[],
2921
+ identities: ProjectInputPathIdentityContext,
2596
2922
  ): string[] {
2597
- const identities = createProjectInputPathIdentityContext();
2598
2923
  const roots = globs.map((glob) => identities.resolve(literalGlobRoot(glob)));
2599
2924
  const changes = new Map<string, string>();
2600
2925
  for (const location of locations) {
@@ -2614,20 +2939,54 @@ function projectInputCompilerMembershipProjectChanges(
2614
2939
  return [...changes.values()].sort();
2615
2940
  }
2616
2941
 
2942
+ /**
2943
+ * Fingerprint every matched project input: a directory by its entries, a file
2944
+ * by its bytes.
2945
+ *
2946
+ * Without `previous`, every file is read. With it, a file keeps its previous
2947
+ * digest while its metadata and physical owner hold, unless it is the `named`
2948
+ * path, whose bytes are always read: a notification that names a file is the
2949
+ * one whose same-size rewrite within a timestamp tick metadata cannot show.
2950
+ */
2617
2951
  function fingerprintProjectInputMatches(
2618
2952
  matches: ReadonlyMap<string, string>,
2619
2953
  readDirectory: typeof fs.readdirSync,
2620
- ): Map<string, string> {
2954
+ previous?: {
2955
+ fingerprints: ReadonlyMap<string, string>;
2956
+ named: string;
2957
+ snapshots: ReadonlyMap<string, CompilerFileSnapshot>;
2958
+ },
2959
+ ): {
2960
+ fingerprints: Map<string, string>;
2961
+ snapshots: Map<string, CompilerFileSnapshot>;
2962
+ } {
2621
2963
  const fingerprints = new Map<string, string>();
2964
+ const snapshots = new Map<string, CompilerFileSnapshot>();
2622
2965
  for (const [key, location] of matches) {
2623
- fingerprints.set(
2624
- key,
2625
- WatchPaths.isDirectory(location)
2626
- ? fingerprintProjectInputDirectory(location, readDirectory)
2627
- : fingerprintProjectInputFile(location),
2628
- );
2629
- }
2630
- return fingerprints;
2966
+ if (WatchPaths.isDirectory(location)) {
2967
+ fingerprints.set(
2968
+ key,
2969
+ fingerprintProjectInputDirectory(location, readDirectory),
2970
+ );
2971
+ continue;
2972
+ }
2973
+ const metadata = compilerFileSnapshot(location, "");
2974
+ const before = previous?.snapshots.get(key);
2975
+ const kept = previous?.fingerprints.get(key);
2976
+ const fingerprint =
2977
+ previous !== undefined &&
2978
+ key !== previous.named &&
2979
+ before !== undefined &&
2980
+ kept !== undefined &&
2981
+ metadata.content !== "" &&
2982
+ before.content === metadata.content &&
2983
+ before.owner === metadata.owner
2984
+ ? kept
2985
+ : fingerprintProjectInputFile(location);
2986
+ fingerprints.set(key, fingerprint);
2987
+ snapshots.set(key, { ...metadata, fingerprint });
2988
+ }
2989
+ return { fingerprints, snapshots };
2631
2990
  }
2632
2991
 
2633
2992
  function fingerprintProjectInputFile(location: string): string {
@@ -2675,7 +3034,7 @@ function fingerprintProjectInputDirectory(
2675
3034
  function matchesProjectInputGlob(
2676
3035
  pattern: string,
2677
3036
  location: string,
2678
- identities = createProjectInputPathIdentityContext(),
3037
+ identities: ProjectInputPathIdentityContext,
2679
3038
  ): boolean {
2680
3039
  const root = identities.resolve(literalGlobRoot(pattern));
2681
3040
  const candidate = identities.resolve(location);