ttsc 0.31.4 → 0.32.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 (140) 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/compiler/internal/build/BuildExecution.js +11 -3
  19. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  20. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  21. package/lib/compiler/internal/project/CompilerProjectSelection.d.ts +9 -8
  22. package/lib/compiler/internal/project/CompilerProjectSelection.js +11 -9
  23. package/lib/compiler/internal/project/CompilerProjectSelection.js.map +1 -1
  24. package/lib/compiler/internal/project/resolveOwningProjectConfig.d.ts +20 -8
  25. package/lib/compiler/internal/project/resolveOwningProjectConfig.js +21 -12
  26. package/lib/compiler/internal/project/resolveOwningProjectConfig.js.map +1 -1
  27. package/lib/compiler/internal/runSingleFileEmit.d.ts +5 -4
  28. package/lib/compiler/internal/runSingleFileEmit.js +5 -4
  29. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  30. package/lib/flags/ParseOptions.d.ts +2 -2
  31. package/lib/internal/E2ETrace.d.ts +1 -0
  32. package/lib/internal/E2ETrace.js.map +1 -1
  33. package/lib/internal/OwnedNativeProcess.d.ts +2 -2
  34. package/lib/internal/OwnedNativeProcess.js +2 -2
  35. package/lib/internal/SourceNativeRetirement.d.ts +8 -7
  36. package/lib/internal/SourceNativeRetirement.js +19 -10
  37. package/lib/internal/SourceNativeRetirement.js.map +1 -1
  38. package/lib/internal/TtsxNodeOptions.d.ts +79 -0
  39. package/lib/internal/TtsxNodeOptions.js +121 -0
  40. package/lib/internal/TtsxNodeOptions.js.map +1 -0
  41. package/lib/internal/javascriptRuntimeCapabilities.js +4 -1
  42. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  43. package/lib/internal/parseWindowsDirectoryCaseSensitivity.d.ts +5 -5
  44. package/lib/internal/parseWindowsDirectoryCaseSensitivity.js +5 -5
  45. package/lib/internal/pathIdentity/createCaseSensitivityObservations.d.ts +35 -0
  46. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js +209 -0
  47. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js.map +1 -0
  48. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js +2 -143
  49. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js.map +1 -1
  50. package/lib/internal/probeLocalProcess.d.ts +5 -1
  51. package/lib/internal/probeLocalProcess.js +9 -7
  52. package/lib/internal/probeLocalProcess.js.map +1 -1
  53. package/lib/internal/projectInputPathIdentity.d.ts +1 -0
  54. package/lib/internal/projectInputPathIdentity.js +1 -0
  55. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  56. package/lib/launcher/internal/parseTtsxCLI.d.ts +5 -4
  57. package/lib/launcher/internal/parseTtsxCLI.js +5 -4
  58. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  59. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  60. package/lib/launcher/internal/resolveSingleFileOutput.d.ts +8 -9
  61. package/lib/launcher/internal/resolveSingleFileOutput.js +8 -9
  62. package/lib/launcher/internal/resolveSingleFileOutput.js.map +1 -1
  63. package/lib/launcher/internal/runTtsc.js +5 -5
  64. package/lib/launcher/internal/runTtsx.js +5 -2
  65. package/lib/launcher/internal/runTtsx.js.map +1 -1
  66. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.d.ts +2 -0
  67. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.js +2 -1
  68. package/lib/launcher/internal/runtime/ProcessOwnedDirectory.js.map +1 -1
  69. package/lib/launcher/internal/runtime/RuntimeEntrySources.d.ts +40 -0
  70. package/lib/launcher/internal/runtime/RuntimeEntrySources.js +57 -0
  71. package/lib/launcher/internal/runtime/RuntimeEntrySources.js.map +1 -0
  72. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.d.ts +5 -4
  73. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js +8 -6
  74. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js.map +1 -1
  75. package/lib/launcher/internal/runtime/commonJsExportNames.d.ts +2 -1
  76. package/lib/launcher/internal/runtime/commonJsExportNames.js +2 -1
  77. package/lib/launcher/internal/runtime/commonJsExportNames.js.map +1 -1
  78. package/lib/launcher/internal/runtime/installRuntimeHooks.js +35 -9
  79. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  80. package/lib/launcher/internal/runtime/isLocalProcessGone.d.ts +1 -1
  81. package/lib/launcher/internal/runtime/isLocalProcessGone.js +4 -4
  82. package/lib/launcher/internal/runtime/isLocalProcessGone.js.map +1 -1
  83. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js +3 -1
  84. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js.map +1 -1
  85. package/lib/launcher/internal/watch/FseventsStreams.d.ts +2 -0
  86. package/lib/launcher/internal/watch/FseventsStreams.js +19 -0
  87. package/lib/launcher/internal/watch/FseventsStreams.js.map +1 -1
  88. package/lib/launcher/internal/watch/WatchTopology.d.ts +74 -12
  89. package/lib/launcher/internal/watch/WatchTopology.js +301 -92
  90. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  91. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.d.ts +4 -1
  92. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js +3 -2
  93. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js.map +1 -1
  94. package/lib/plugin/internal/load/loadProjectPlugins.js +17 -9
  95. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  96. package/lib/plugin/internal/load/resolveNativeSource.js +2 -3
  97. package/lib/plugin/internal/load/resolveNativeSource.js.map +1 -1
  98. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +4 -1
  99. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +9 -2
  100. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  101. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -2
  102. package/package.json +9 -9
  103. package/src/compiler/internal/build/BuildExecution.ts +15 -6
  104. package/src/compiler/internal/build/TsgoArguments.ts +4 -1
  105. package/src/compiler/internal/project/CompilerProjectSelection.ts +14 -14
  106. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +29 -14
  107. package/src/compiler/internal/runSingleFileEmit.ts +11 -7
  108. package/src/flags/ParseOptions.ts +2 -2
  109. package/src/internal/E2ETrace.ts +5 -1
  110. package/src/internal/OwnedNativeProcess.ts +2 -2
  111. package/src/internal/SourceNativeRetirement.ts +42 -19
  112. package/src/internal/TtsxNodeOptions.ts +124 -0
  113. package/src/internal/javascriptRuntimeCapabilities.ts +6 -1
  114. package/src/internal/parseWindowsDirectoryCaseSensitivity.ts +5 -5
  115. package/src/internal/pathIdentity/createCaseSensitivityObservations.ts +242 -0
  116. package/src/internal/pathIdentity/createFilesystemPathIdentityContext.ts +2 -169
  117. package/src/internal/probeLocalProcess.ts +13 -8
  118. package/src/internal/projectInputPathIdentity.ts +1 -0
  119. package/src/launcher/internal/parseTtsxCLI.ts +5 -4
  120. package/src/launcher/internal/prepareExecution.ts +4 -1
  121. package/src/launcher/internal/resolveSingleFileOutput.ts +8 -9
  122. package/src/launcher/internal/runTtsc.ts +5 -5
  123. package/src/launcher/internal/runTtsx.ts +17 -8
  124. package/src/launcher/internal/runtime/ProcessOwnedDirectory.ts +5 -1
  125. package/src/launcher/internal/runtime/RuntimeEntrySources.ts +57 -0
  126. package/src/launcher/internal/runtime/RuntimeProjectOwnership.ts +23 -10
  127. package/src/launcher/internal/runtime/commonJsExportNames.ts +11 -4
  128. package/src/launcher/internal/runtime/installRuntimeHooks.ts +55 -13
  129. package/src/launcher/internal/runtime/isLocalProcessGone.ts +5 -3
  130. package/src/launcher/internal/runtime/resolveRuntimeCleanTargets.ts +17 -5
  131. package/src/launcher/internal/watch/FseventsStreams.ts +22 -0
  132. package/src/launcher/internal/watch/WatchTopology.ts +387 -99
  133. package/src/launcher/internal/watch/projectInputReloadEventShouldNotify.ts +14 -9
  134. package/src/plugin/internal/load/loadProjectPlugins.ts +17 -3
  135. package/src/plugin/internal/load/resolveNativeSource.ts +10 -4
  136. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +10 -1
  137. package/src/structures/internal/TtscCommonOptions.ts +2 -2
  138. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +0 -78
  139. package/internal/graphsymbols/provider.go +0 -650
  140. package/internal/lspserver/lsp_symbols.go +0 -212
@@ -11,6 +11,7 @@ import { spawnNative } from "../../../compiler/internal/spawnNative";
11
11
  import { normalizeCompilerEnumValue } from "../../../flags/normalizeCompilerEnumValue";
12
12
  import { readCompilerOptionValues } from "../../../flags/readCompilerOptionValues";
13
13
  import { type ProjectInputPathIdentityContext } from "../../../internal/pathIdentity/ProjectInputPathIdentityContext";
14
+ import { createCaseSensitivityObservations } from "../../../internal/pathIdentity/createCaseSensitivityObservations";
14
15
  import { createProjectInputPathIdentityContext } from "../../../internal/pathIdentity/createProjectInputPathIdentityContext";
15
16
  import { isProjectInputPathIdentityWithin } from "../../../internal/pathIdentity/isProjectInputPathIdentityWithin";
16
17
  import { resolveProjectInputPath } from "../../../internal/pathIdentity/resolveProjectInputPath";
@@ -46,26 +47,26 @@ import { watchDirectory } from "./watchDirectory";
46
47
  * output for source and declaration membership. An explicit reader supplies the
47
48
  * same absolute-path membership and failure contract for each resolved project.
48
49
  * 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.
50
+ * filesystem identity remain this topology's responsibility. Response frames
51
+ * are observed once per project refresh at its compiler cwd; that option view
52
+ * serves all project-output queries while the original argv reaches compiler
53
+ * membership. A second observation rejects response changes across membership
54
+ * resolution. Response files join the config reload set; these observations do
55
+ * not pin files or detect a change restored in between.
55
56
  *
56
57
  * `TTSC_WATCH_DEBUG_INPUTS` reports the named event, observed population deltas
57
58
  * and reload decision. These diagnostics reuse the decision's existing inputs;
58
59
  * they perform no extra filesystem observations.
59
60
  *
60
61
  * @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.
62
+ * @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
63
  * @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
64
  * @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
65
  * @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
66
  *
66
67
  * @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#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.
69
+ * @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
70
  */
70
71
  export class WatchTopology {
71
72
  private analysisOnly = false;
@@ -102,15 +103,42 @@ export class WatchTopology {
102
103
  /** The plugin inputs whose directories the last sync already watched. */
103
104
  private watchedExtraInputs = new Set<string>();
104
105
  private compilerFileSnapshots = new Map<string, CompilerFileSnapshot>();
106
+
107
+ /**
108
+ * The last compiler listing of each project the topology resolved, by its
109
+ * config path, with the arguments it was asked under (`listingQuestion`). A
110
+ * change of plugin inputs reuses it (samchon/ttsc#1741).
111
+ */
112
+ private compilerListings = new Map<string, CompilerListing>();
113
+
114
+ /**
115
+ * Notifications the current poll iteration delivered, decided together in the
116
+ * check phase (`deferNotification`).
117
+ */
118
+ private pendingNotifications = new Map<string, PendingNotification>();
119
+ private notificationsScheduled = false;
105
120
  private files = new Map<string, string>();
106
- private lexicalIdentities = createProjectInputPathIdentityContext({
107
- throwOnRealpathError: false,
108
- });
121
+
122
+ /**
123
+ * The current decision's directory case-policy observations, which every
124
+ * identity context the decision creates shares (samchon/ttsc#1742).
125
+ */
126
+ private caseObservations!: ReturnType<
127
+ typeof createCaseSensitivityObservations
128
+ >;
129
+ private lexicalIdentities!: ProjectInputPathIdentityContext;
109
130
  private fileWatchers = new Map<string, fs.FSWatcher>();
110
131
  private observedDirectories = new Map<string, string>();
111
132
  private outputFiles = new Map<string, string>();
112
133
  private outputs = new Map<string, string>();
113
134
  private projectInputFingerprints = new Map<string, string>();
135
+
136
+ /**
137
+ * The metadata and physical owner each matched project-input file had when
138
+ * its fingerprint was taken, by match key; a named notification re-reads only
139
+ * the files whose metadata moved (samchon/ttsc#1748).
140
+ */
141
+ private projectInputFileSnapshots = new Map<string, CompilerFileSnapshot>();
114
142
  private projectInputMatches = new Map<string, string>();
115
143
  private projectInputs: ITtscProjectInputSnapshot = {
116
144
  files: [],
@@ -154,6 +182,8 @@ export class WatchTopology {
154
182
  * membership; the default invokes the native compiler. Readers throw
155
183
  * listing failures and retain ownership of their returned array, which this
156
184
  * topology does not mutate.
185
+ * @param observeCaseSensitivity Create one decision's directory case-policy
186
+ * observations; the default asks the native filesystem.
157
187
  */
158
188
  public constructor(
159
189
  private readonly options: WatchTopologyOptions,
@@ -162,7 +192,10 @@ export class WatchTopology {
162
192
  private readonly openFileWatch: typeof fs.watch = fs.watch,
163
193
  private readonly readDirectory: typeof fs.readdirSync = fs.readdirSync,
164
194
  private readonly getCompilerInputs: typeof listCompilerInputs = listCompilerInputs,
165
- ) {}
195
+ private readonly observeCaseSensitivity: typeof createCaseSensitivityObservations = createCaseSensitivityObservations,
196
+ ) {
197
+ this.beginLexicalTransaction();
198
+ }
166
199
 
167
200
  /**
168
201
  * Re-resolve compiler inputs and notify when the resolved topology changes,
@@ -183,17 +216,34 @@ export class WatchTopology {
183
216
  this.refreshCompilerInputs(notify, false);
184
217
  }
185
218
 
219
+ /**
220
+ * @param reuseListings Answer each project's membership with its last listing
221
+ * under the same arguments instead of asking the compiler again. Only a
222
+ * refresh whose cause cannot move Program membership passes it.
223
+ */
186
224
  private refreshCompilerInputs(
187
225
  notify: boolean,
188
226
  skipUnobservedProjectInputWatchRoots: boolean,
227
+ reuseListings: boolean = false,
189
228
  ): void {
190
229
  this.beginLexicalTransaction();
230
+ const listings = new Map<string, CompilerListing>();
191
231
  const next = resolveWatchTopology(
192
232
  this.options,
193
233
  this.extraInputs,
194
234
  this.lexicalIdentities,
195
- this.getCompilerInputs,
235
+ (project, options) => {
236
+ const question = listingQuestion(project, options);
237
+ const last = this.compilerListings.get(project.path);
238
+ const inputs =
239
+ reuseListings && last?.question === question
240
+ ? last.inputs
241
+ : this.getCompilerInputs(project, options);
242
+ listings.set(project.path, { inputs, question });
243
+ return inputs;
244
+ },
196
245
  );
246
+ this.compilerListings = listings;
197
247
  const compilerProgramMembershipChange =
198
248
  next.analysisOnly &&
199
249
  WatchPaths.mapsEqual(this.reloadFiles, next.reloadFiles) &&
@@ -204,6 +254,7 @@ export class WatchTopology {
204
254
  const projectInputProgramOverlap = projectInputCompilerMembershipChange(
205
255
  this.projectInputs,
206
256
  compilerProgramMembershipChange,
257
+ this.projectInputIdentities(),
207
258
  );
208
259
  const projectInputProgramChange =
209
260
  compilerProgramMembershipChange.length !== 0 &&
@@ -293,7 +344,7 @@ export class WatchTopology {
293
344
  changed: readonly string[],
294
345
  ): boolean {
295
346
  const matches = this.collectProjectInputMatches();
296
- const fingerprints = fingerprintProjectInputMatches(
347
+ const { fingerprints, snapshots } = fingerprintProjectInputMatches(
297
348
  matches,
298
349
  this.readDirectory,
299
350
  );
@@ -307,6 +358,7 @@ export class WatchTopology {
307
358
  const causedBy = projectInputCompilerMembershipProjectChanges(
308
359
  changed,
309
360
  population.globs,
361
+ this.projectInputIdentities(),
310
362
  );
311
363
  const reloadInput = {
312
364
  causedBy,
@@ -316,7 +368,10 @@ export class WatchTopology {
316
368
  reloadDirectories: population.reloadDirectories,
317
369
  reloadFiles: population.reloadFiles ?? [],
318
370
  };
319
- const reload = projectInputReloadEventShouldNotify(reloadInput);
371
+ const reload = projectInputReloadEventShouldNotify(
372
+ reloadInput,
373
+ this.projectInputIdentities(),
374
+ );
320
375
  reportProjectInputDecision({
321
376
  source: "compiler-membership",
322
377
  ...reloadInput,
@@ -328,6 +383,7 @@ export class WatchTopology {
328
383
  // instead of disappearing behind the warm compiler-membership handoff.
329
384
  this.projectInputMatches = matches;
330
385
  this.projectInputFingerprints = fingerprints;
386
+ this.projectInputFileSnapshots = snapshots;
331
387
  return reload;
332
388
  }
333
389
 
@@ -353,8 +409,8 @@ export class WatchTopology {
353
409
  * @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
410
  * @evidence contracts/common.md#meaningful-documentation Native prose explains baseline timing and why existing inputs are not restamped following the documentation skill.
355
411
  * @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.
412
+ * @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.
413
+ * @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
414
  * @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
415
  */
360
416
  public setExtraInputs(
@@ -382,7 +438,12 @@ export class WatchTopology {
382
438
  );
383
439
  }
384
440
  this.pluginInputStates = states;
385
- this.refresh(false);
441
+ // Plugin inputs join the watched files and directories, never Program
442
+ // membership, so the compiler's last answer stands. A load reports up to
443
+ // three different sets, and asking again for each spawned tsgo three times
444
+ // per load (samchon/ttsc#1741). A membership change meanwhile reaches the
445
+ // refresh its own notification starts, which lists again.
446
+ this.refreshCompilerInputs(false, false, true);
386
447
  }
387
448
 
388
449
  /**
@@ -399,7 +460,11 @@ export class WatchTopology {
399
460
  * @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
461
  */
401
462
  public setProjectInputs(inputs: ITtscProjectInputSnapshot): void {
402
- const next = normalizeProjectInputSnapshot(inputs);
463
+ this.beginLexicalTransaction();
464
+ const next = normalizeProjectInputSnapshot(
465
+ inputs,
466
+ this.projectInputIdentities(),
467
+ );
403
468
  // The declared spellings are recorded even when the normalized snapshot did
404
469
  // not move, because a republication can carry a new alias for identities
405
470
  // that already matched, and anchoring the spelling that was retired would
@@ -408,7 +473,13 @@ export class WatchTopology {
408
473
  inputs.declared === undefined
409
474
  ? inputs
410
475
  : { ...inputs.declared, root: inputs.root };
411
- if (projectInputSnapshotsEqual(this.projectInputs, next)) {
476
+ if (
477
+ projectInputSnapshotsEqual(
478
+ this.projectInputs,
479
+ next,
480
+ this.projectInputIdentities(),
481
+ )
482
+ ) {
412
483
  this.syncProjectInputWatchers();
413
484
  return;
414
485
  }
@@ -420,10 +491,12 @@ export class WatchTopology {
420
491
  { ...(inputs.declared ?? inputs), root: inputs.root },
421
492
  ]);
422
493
  this.projectInputMatches = this.collectProjectInputMatches();
423
- this.projectInputFingerprints = fingerprintProjectInputMatches(
494
+ const baseline = fingerprintProjectInputMatches(
424
495
  this.projectInputMatches,
425
496
  this.readDirectory,
426
497
  );
498
+ this.projectInputFingerprints = baseline.fingerprints;
499
+ this.projectInputFileSnapshots = baseline.snapshots;
427
500
  this.syncProjectInputWatchers();
428
501
  }
429
502
 
@@ -457,11 +530,26 @@ export class WatchTopology {
457
530
 
458
531
  /** Start fresh native case observations for one synchronous watch decision. */
459
532
  private beginLexicalTransaction(): void {
533
+ this.caseObservations = this.observeCaseSensitivity();
460
534
  this.lexicalIdentities = createProjectInputPathIdentityContext({
535
+ caseSensitive: this.caseObservations,
461
536
  throwOnRealpathError: false,
462
537
  });
463
538
  }
464
539
 
540
+ /**
541
+ * A strict project-input identity context for the current decision. Its
542
+ * physical paths are resolved afresh, and its case answers are the
543
+ * decision's: one decision builds such contexts per declaration and per
544
+ * candidate, and each asked every directory again, which on Windows is an
545
+ * `fsutil` process per question (samchon/ttsc#1742).
546
+ */
547
+ private projectInputIdentities(): ProjectInputPathIdentityContext {
548
+ return createProjectInputPathIdentityContext({
549
+ caseSensitive: this.caseObservations,
550
+ });
551
+ }
552
+
465
553
  /** Keep lexical alias ownership while sharing this decision's case probes. */
466
554
  private pathKey(location: string): string {
467
555
  return WatchPaths.pathKey(location, this.lexicalIdentities);
@@ -585,34 +673,17 @@ export class WatchTopology {
585
673
  this.openDirectoryWatch(
586
674
  watcherRegistrationPath(location),
587
675
  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,
676
+ (event, filename, gap) =>
677
+ this.deferNotification({
678
+ gap: gap === true,
679
+ lane: "compiler",
601
680
  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
- },
681
+ changed:
682
+ filename === null
683
+ ? undefined
684
+ : path.resolve(location, filename),
685
+ rename: event === "rename",
686
+ }),
616
687
  ),
617
688
  (location, error) => this.callbacks.onError(location, error),
618
689
  () => this.closed === false,
@@ -622,6 +693,87 @@ export class WatchTopology {
622
693
  );
623
694
  }
624
695
 
696
+ /**
697
+ * Decide a notification in the event loop's check phase, together with every
698
+ * other notification the same poll iteration delivered (samchon/ttsc#1741).
699
+ *
700
+ * One filesystem change usually arrives as several notifications, rename and
701
+ * change or create and modify, and each listed the compiler inputs or
702
+ * rescanned the project inputs again. Identical notifications of one lane,
703
+ * location and path merge, keeping a rename or a gap any of them carried, and
704
+ * the batch refreshes compiler membership once after deciding them all. The
705
+ * refresh therefore starts after every notification it absorbs, and one
706
+ * delivered later schedules another batch.
707
+ */
708
+ private deferNotification(notification: PendingNotification): void {
709
+ if (this.closed) return;
710
+ const key = [
711
+ notification.lane,
712
+ notification.location,
713
+ notification.changed ?? "",
714
+ ].join("\0");
715
+ const pending = this.pendingNotifications.get(key);
716
+ if (pending !== undefined) {
717
+ pending.rename ||= notification.rename;
718
+ pending.gap ||= notification.gap;
719
+ return;
720
+ }
721
+ this.pendingNotifications.set(key, { ...notification });
722
+ if (this.notificationsScheduled) return;
723
+ this.notificationsScheduled = true;
724
+ setImmediate(() => this.decideNotifications());
725
+ }
726
+
727
+ private decideNotifications(): void {
728
+ this.notificationsScheduled = false;
729
+ const pending = [...this.pendingNotifications.values()];
730
+ this.pendingNotifications.clear();
731
+ if (this.closed) return;
732
+ const refreshes: DirectoryRefresh[] = [];
733
+ for (const notification of pending) {
734
+ if (notification.lane === "project-input") {
735
+ this.refreshProjectInputs(notification.location, notification.changed);
736
+ continue;
737
+ }
738
+ this.decideCompilerDirectoryNotification(notification, refreshes);
739
+ }
740
+ if (refreshes.length === 0 || this.closed) return;
741
+ this.beginLexicalTransaction();
742
+ this.refreshFromDirectories(refreshes);
743
+ }
744
+
745
+ /** Decide one merged directory notification of the compiler lane. */
746
+ private decideCompilerDirectoryNotification(
747
+ notification: PendingNotification,
748
+ refreshes: DirectoryRefresh[],
749
+ ): void {
750
+ this.beginLexicalTransaction();
751
+ const { changed, location } = notification;
752
+ const pluginInput = changed ?? location;
753
+ if (this.isPluginInput(pluginInput)) {
754
+ this.notePluginNotification(pluginInput);
755
+ return;
756
+ }
757
+ const plan = planCompilerDirectoryWatchEvent({
758
+ changed,
759
+ event: notification.rename ? "rename" : "change",
760
+ exists: fs.existsSync,
761
+ location,
762
+ identities: this.lexicalIdentities,
763
+ platform: process.platform,
764
+ trackedFiles: this.files,
765
+ });
766
+ this.rearmFileWatchers(plan.rearm);
767
+ for (const file of this.compilerChangesToReport(
768
+ plan.changes,
769
+ changed,
770
+ notification.gap,
771
+ )) {
772
+ this.reportCompilerFileChange(file);
773
+ }
774
+ if (plan.refresh) refreshes.push({ changed, location });
775
+ }
776
+
625
777
  /**
626
778
  * Reconcile tracked compiler files after a newly registered watcher returns.
627
779
  *
@@ -730,6 +882,7 @@ export class WatchTopology {
730
882
  if (fs.existsSync(location)) {
731
883
  if (this.projectInputMatches.size !== 0) {
732
884
  const key = createProjectInputPathIdentityContext({
885
+ caseSensitive: this.caseObservations,
733
886
  throwOnRealpathError: false,
734
887
  }).resolve(location).key;
735
888
  if (
@@ -840,7 +993,7 @@ export class WatchTopology {
840
993
  if (this.closed) return;
841
994
  const previous = new Map(this.projectInputWatchers);
842
995
  const previousLinks = new Map(this.projectInputLinkWatchers);
843
- const identities = createProjectInputPathIdentityContext();
996
+ const identities = this.projectInputIdentities();
844
997
  const desired = new Map<string, string>();
845
998
  const required = new Map<string, string>();
846
999
  for (const file of this.projectInputDeclarations("file")) {
@@ -924,11 +1077,17 @@ export class WatchTopology {
924
1077
  this.openDirectoryWatch(
925
1078
  watcherRegistrationPath(location),
926
1079
  true,
927
- (_event, filename) => {
928
- const changed =
929
- filename === null ? undefined : path.resolve(location, filename);
930
- this.refreshProjectInputs(location, changed);
931
- },
1080
+ (_event, filename) =>
1081
+ this.deferNotification({
1082
+ gap: false,
1083
+ lane: "project-input",
1084
+ location,
1085
+ changed:
1086
+ filename === null
1087
+ ? undefined
1088
+ : path.resolve(location, filename),
1089
+ rename: false,
1090
+ }),
932
1091
  ),
933
1092
  (location, error) => {
934
1093
  const key = identities.resolve(location).key;
@@ -1003,11 +1162,17 @@ export class WatchTopology {
1003
1162
  this.openDirectoryWatch(
1004
1163
  watcherRegistrationPath(location),
1005
1164
  false,
1006
- (_event, filename) => {
1007
- const changed =
1008
- filename === null ? undefined : path.resolve(location, filename);
1009
- this.refreshProjectInputs(location, changed);
1010
- },
1165
+ (_event, filename) =>
1166
+ this.deferNotification({
1167
+ gap: false,
1168
+ lane: "project-input",
1169
+ location,
1170
+ changed:
1171
+ filename === null
1172
+ ? undefined
1173
+ : path.resolve(location, filename),
1174
+ rename: false,
1175
+ }),
1011
1176
  ),
1012
1177
  (location, error) => this.callbacks.onError(location, error),
1013
1178
  () => this.closed === false,
@@ -1088,6 +1253,7 @@ export class WatchTopology {
1088
1253
  if (this.projectInputRecoveryScheduled) return;
1089
1254
  this.projectInputRecoveryScheduled = true;
1090
1255
  queueMicrotask(() => {
1256
+ this.beginLexicalTransaction();
1091
1257
  try {
1092
1258
  let previousRejectionCount = -1;
1093
1259
  while (
@@ -1131,6 +1297,7 @@ export class WatchTopology {
1131
1297
  queueMicrotask(() => {
1132
1298
  this.projectInputPostRegistrationReconciliationScheduled = false;
1133
1299
  if (this.closed) return;
1300
+ this.beginLexicalTransaction();
1134
1301
  this.refreshPublishedProjectInputIdentities();
1135
1302
  this.refreshProjectInputs(this.projectInputs.root, undefined, true);
1136
1303
  });
@@ -1145,8 +1312,13 @@ export class WatchTopology {
1145
1312
  * that must observe later target changes.
1146
1313
  */
1147
1314
  private refreshPublishedProjectInputIdentities(): void {
1148
- const next = normalizeProjectInputSnapshot(this.declaredProjectInputs);
1149
- if (projectInputSnapshotsEqual(this.projectInputs, next)) return;
1315
+ const identities = this.projectInputIdentities();
1316
+ const next = normalizeProjectInputSnapshot(
1317
+ this.declaredProjectInputs,
1318
+ identities,
1319
+ );
1320
+ if (projectInputSnapshotsEqual(this.projectInputs, next, identities))
1321
+ return;
1150
1322
  this.projectInputs = next;
1151
1323
  this.pruneProjectInputWatchRoots([next, this.declaredProjectInputs]);
1152
1324
  }
@@ -1264,6 +1436,7 @@ export class WatchTopology {
1264
1436
  const resolved = ProjectInputWatchRules.projectInputRecursiveWatchRoot(
1265
1437
  target,
1266
1438
  this.projectInputs.root,
1439
+ this.projectInputIdentities(),
1267
1440
  );
1268
1441
  if (resolved !== undefined) this.projectInputWatchRoots.set(key, resolved);
1269
1442
  return resolved;
@@ -1277,7 +1450,7 @@ export class WatchTopology {
1277
1450
  this.beginLexicalTransaction();
1278
1451
  try {
1279
1452
  const previous = this.projectInputMatches;
1280
- const identities = createProjectInputPathIdentityContext();
1453
+ const identities = this.projectInputIdentities();
1281
1454
  // One population for the whole decision. Every question below is asked of
1282
1455
  // the same declarations, and rebuilding it per question would both cost
1283
1456
  // more and let two answers disagree about what was declared.
@@ -1315,13 +1488,30 @@ export class WatchTopology {
1315
1488
  }
1316
1489
  const next = this.collectProjectInputMatches();
1317
1490
  const membershipChanged = WatchPaths.mapsEqual(previous, next) === false;
1318
- const nextFingerprints =
1491
+ // A named notification reads the named path's bytes and any file whose
1492
+ // metadata or owner moved; one that names nothing reads every file
1493
+ // (samchon/ttsc#1748).
1494
+ const scanned =
1319
1495
  changed === undefined ||
1320
1496
  membershipChanged ||
1321
1497
  directlyMatched ||
1322
1498
  topologyMatched
1323
- ? fingerprintProjectInputMatches(next, this.readDirectory)
1324
- : this.projectInputFingerprints;
1499
+ ? fingerprintProjectInputMatches(
1500
+ next,
1501
+ this.readDirectory,
1502
+ changed === undefined
1503
+ ? undefined
1504
+ : {
1505
+ fingerprints: this.projectInputFingerprints,
1506
+ named: identities.resolve(changed).key,
1507
+ snapshots: this.projectInputFileSnapshots,
1508
+ },
1509
+ )
1510
+ : {
1511
+ fingerprints: this.projectInputFingerprints,
1512
+ snapshots: this.projectInputFileSnapshots,
1513
+ };
1514
+ const nextFingerprints = scanned.fingerprints;
1325
1515
  const contentChanged =
1326
1516
  WatchPaths.mapsEqual(
1327
1517
  this.projectInputFingerprints,
@@ -1352,7 +1542,10 @@ export class WatchTopology {
1352
1542
  reloadDirectories: population.reloadDirectories ?? [],
1353
1543
  reloadFiles: population.reloadFiles ?? [],
1354
1544
  };
1355
- const reload = projectInputReloadEventShouldNotify(reloadInput);
1545
+ const reload = projectInputReloadEventShouldNotify(
1546
+ reloadInput,
1547
+ identities,
1548
+ );
1356
1549
  const invalidate = projectInputMembershipInvalidatesProgram({
1357
1550
  changed: reconciledChange,
1358
1551
  changedInputs,
@@ -1362,6 +1555,7 @@ export class WatchTopology {
1362
1555
  });
1363
1556
  this.projectInputMatches = next;
1364
1557
  this.projectInputFingerprints = nextFingerprints;
1558
+ this.projectInputFileSnapshots = scanned.snapshots;
1365
1559
  this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
1366
1560
  // A JSON/TS/JS project-input member can simultaneously enter or leave
1367
1561
  // the compiler Program. Reconcile the compiler watch snapshot before
@@ -1416,7 +1610,7 @@ export class WatchTopology {
1416
1610
  }
1417
1611
 
1418
1612
  private collectProjectInputMatches(): Map<string, string> {
1419
- const identities = createProjectInputPathIdentityContext();
1613
+ const identities = this.projectInputIdentities();
1420
1614
  const matches = new Map<string, string>();
1421
1615
  // Both spellings are scanned, and each is resolved here rather than when
1422
1616
  // the snapshot arrived. A declaration reached through a symlink otherwise
@@ -1510,25 +1704,42 @@ export class WatchTopology {
1510
1704
  }
1511
1705
 
1512
1706
  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);
1707
+ this.refreshFromDirectories([{ changed, location }]);
1708
+ }
1709
+
1710
+ /**
1711
+ * Refresh compiler membership once for every directory a decision asked
1712
+ * about. A failed refresh still reports, once each, the reload inputs every
1713
+ * requested change names.
1714
+ */
1715
+ private refreshFromDirectories(refreshes: readonly DirectoryRefresh[]): void {
1716
+ for (const { changed } of refreshes) {
1717
+ if (
1718
+ changed !== undefined &&
1719
+ WatchPaths.isDirectory(changed) &&
1720
+ this.isCompilerOutputDirectory(changed) === false &&
1721
+ this.isProjectInputDirectory(changed) === false
1722
+ ) {
1723
+ this.observedDirectories.set(this.pathKey(changed), changed);
1724
+ }
1520
1725
  }
1521
1726
  try {
1522
1727
  this.refresh(true);
1523
1728
  } 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 });
1729
+ const reported = new Set<string>();
1730
+ for (const { changed } of refreshes) {
1731
+ for (const reload of reloadInputsForFailedTopologyRefresh(
1732
+ this.reloadFiles.values(),
1733
+ changed,
1734
+ this.lexicalIdentities,
1735
+ )) {
1736
+ const key = this.pathKey(reload);
1737
+ if (reported.has(key)) continue;
1738
+ reported.add(key);
1739
+ this.callbacks.onInputChange({ kind: "config", path: reload });
1740
+ }
1530
1741
  }
1531
- this.callbacks.onError(location, error);
1742
+ this.callbacks.onError(refreshes[0]!.location, error);
1532
1743
  }
1533
1744
  }
1534
1745
 
@@ -1591,7 +1802,7 @@ export class WatchTopology {
1591
1802
 
1592
1803
  private isProjectInputDirectory(location: string): boolean {
1593
1804
  const resolved = path.resolve(location);
1594
- const identities = createProjectInputPathIdentityContext();
1805
+ const identities = this.projectInputIdentities();
1595
1806
  return (
1596
1807
  this.projectInputs.files.some(
1597
1808
  (file) =>
@@ -1764,6 +1975,46 @@ type CompilerFileSnapshot = {
1764
1975
  owner: string;
1765
1976
  };
1766
1977
 
1978
+ type PendingNotification = {
1979
+ changed: string | undefined;
1980
+ gap: boolean;
1981
+ lane: "compiler" | "project-input";
1982
+ location: string;
1983
+ rename: boolean;
1984
+ };
1985
+
1986
+ type DirectoryRefresh = {
1987
+ changed: string | undefined;
1988
+ location: string;
1989
+ };
1990
+
1991
+ type CompilerListing = {
1992
+ /** The reader's own array, which the topology never mutates. */
1993
+ inputs: string[];
1994
+ question: string;
1995
+ };
1996
+
1997
+ /**
1998
+ * Everything a compiler listing of `project` is asked with: the project, and
1999
+ * each option `listCompilerInputs` passes to the compiler. Two refreshes with
2000
+ * the same question ask the compiler the same thing; only the disk can answer
2001
+ * them differently.
2002
+ */
2003
+ function listingQuestion(
2004
+ project: ITtscParsedProjectConfig,
2005
+ options: WatchTopologyOptions,
2006
+ ): string {
2007
+ return JSON.stringify([
2008
+ project.path,
2009
+ project.root,
2010
+ options.binary ?? null,
2011
+ options.compilerArgsCwd ?? null,
2012
+ options.env ?? null,
2013
+ options.passthrough ?? null,
2014
+ options.pinCompilerProject ?? null,
2015
+ ]);
2016
+ }
2017
+
1767
2018
  type CompilerFileMovement = {
1768
2019
  content: boolean;
1769
2020
  owner: boolean;
@@ -1836,7 +2087,10 @@ function resolveWatchTopology(
1836
2087
  addPaths(reloadFiles, inspected.responseFiles, identities);
1837
2088
  const compilerInputs = getCompilerInputs(project, options);
1838
2089
  CompilerProjectSelection.assertCurrent(selection);
1839
- const compilerOutputs = resolveCompilerOutputs(project, inspected.options);
2090
+ const compilerOutputs = resolveCompilerOutputs(
2091
+ project,
2092
+ inspected.options,
2093
+ );
1840
2094
  addPaths(outputFiles, compilerOutputs.files, identities);
1841
2095
  addPaths(
1842
2096
  outputFiles,
@@ -2355,8 +2609,8 @@ function uniqueExistingPaths(
2355
2609
 
2356
2610
  function normalizeProjectInputSnapshot(
2357
2611
  snapshot: ITtscProjectInputSnapshot,
2612
+ identities: ProjectInputPathIdentityContext,
2358
2613
  ): ITtscProjectInputSnapshot {
2359
- const identities = createProjectInputPathIdentityContext();
2360
2614
  const files = new Map<string, string>();
2361
2615
  const globs = new Map<string, string>();
2362
2616
  const reloadDirectories = new Map<string, string>();
@@ -2389,8 +2643,8 @@ function normalizeProjectInputSnapshot(
2389
2643
  function projectInputSnapshotsEqual(
2390
2644
  left: ITtscProjectInputSnapshot,
2391
2645
  right: ITtscProjectInputSnapshot,
2646
+ identities: ProjectInputPathIdentityContext,
2392
2647
  ): boolean {
2393
- const identities = createProjectInputPathIdentityContext();
2394
2648
  return (
2395
2649
  // Every sibling comparison in this class decides on identity, and this one
2396
2650
  // is only safe today because both operands come from the same normalizer.
@@ -2445,7 +2699,7 @@ function isSymbolicLink(location: string): boolean {
2445
2699
  function matchesProjectInput(
2446
2700
  snapshot: ITtscProjectInputSnapshot,
2447
2701
  location: string,
2448
- identities = createProjectInputPathIdentityContext(),
2702
+ identities: ProjectInputPathIdentityContext,
2449
2703
  ): boolean {
2450
2704
  const key = identities.resolve(location).key;
2451
2705
  return (
@@ -2572,8 +2826,8 @@ function compilerMembershipChange(
2572
2826
  function projectInputCompilerMembershipChange(
2573
2827
  snapshot: ITtscProjectInputSnapshot,
2574
2828
  changed: readonly string[],
2829
+ identities: ProjectInputPathIdentityContext,
2575
2830
  ): string[] {
2576
- const identities = createProjectInputPathIdentityContext();
2577
2831
  return changed.filter(
2578
2832
  (location) =>
2579
2833
  matchesProjectInput(snapshot, location, identities) &&
@@ -2593,8 +2847,8 @@ function projectInputCompilerMembershipChange(
2593
2847
  function projectInputCompilerMembershipProjectChanges(
2594
2848
  locations: readonly string[],
2595
2849
  globs: readonly string[],
2850
+ identities: ProjectInputPathIdentityContext,
2596
2851
  ): string[] {
2597
- const identities = createProjectInputPathIdentityContext();
2598
2852
  const roots = globs.map((glob) => identities.resolve(literalGlobRoot(glob)));
2599
2853
  const changes = new Map<string, string>();
2600
2854
  for (const location of locations) {
@@ -2614,20 +2868,54 @@ function projectInputCompilerMembershipProjectChanges(
2614
2868
  return [...changes.values()].sort();
2615
2869
  }
2616
2870
 
2871
+ /**
2872
+ * Fingerprint every matched project input: a directory by its entries, a file
2873
+ * by its bytes.
2874
+ *
2875
+ * Without `previous`, every file is read. With it, a file keeps its previous
2876
+ * digest while its metadata and physical owner hold, unless it is the `named`
2877
+ * path, whose bytes are always read: a notification that names a file is the
2878
+ * one whose same-size rewrite within a timestamp tick metadata cannot show.
2879
+ */
2617
2880
  function fingerprintProjectInputMatches(
2618
2881
  matches: ReadonlyMap<string, string>,
2619
2882
  readDirectory: typeof fs.readdirSync,
2620
- ): Map<string, string> {
2883
+ previous?: {
2884
+ fingerprints: ReadonlyMap<string, string>;
2885
+ named: string;
2886
+ snapshots: ReadonlyMap<string, CompilerFileSnapshot>;
2887
+ },
2888
+ ): {
2889
+ fingerprints: Map<string, string>;
2890
+ snapshots: Map<string, CompilerFileSnapshot>;
2891
+ } {
2621
2892
  const fingerprints = new Map<string, string>();
2893
+ const snapshots = new Map<string, CompilerFileSnapshot>();
2622
2894
  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;
2895
+ if (WatchPaths.isDirectory(location)) {
2896
+ fingerprints.set(
2897
+ key,
2898
+ fingerprintProjectInputDirectory(location, readDirectory),
2899
+ );
2900
+ continue;
2901
+ }
2902
+ const metadata = compilerFileSnapshot(location, "");
2903
+ const before = previous?.snapshots.get(key);
2904
+ const kept = previous?.fingerprints.get(key);
2905
+ const fingerprint =
2906
+ previous !== undefined &&
2907
+ key !== previous.named &&
2908
+ before !== undefined &&
2909
+ kept !== undefined &&
2910
+ metadata.content !== "" &&
2911
+ before.content === metadata.content &&
2912
+ before.owner === metadata.owner
2913
+ ? kept
2914
+ : fingerprintProjectInputFile(location);
2915
+ fingerprints.set(key, fingerprint);
2916
+ snapshots.set(key, { ...metadata, fingerprint });
2917
+ }
2918
+ return { fingerprints, snapshots };
2631
2919
  }
2632
2920
 
2633
2921
  function fingerprintProjectInputFile(location: string): string {
@@ -2675,7 +2963,7 @@ function fingerprintProjectInputDirectory(
2675
2963
  function matchesProjectInputGlob(
2676
2964
  pattern: string,
2677
2965
  location: string,
2678
- identities = createProjectInputPathIdentityContext(),
2966
+ identities: ProjectInputPathIdentityContext,
2679
2967
  ): boolean {
2680
2968
  const root = identities.resolve(literalGlobRoot(pattern));
2681
2969
  const candidate = identities.resolve(location);