ttsc 0.31.5 → 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 (123) 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 +1 -1
  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.js.map +1 -1
  32. package/lib/internal/OwnedNativeProcess.d.ts +2 -2
  33. package/lib/internal/OwnedNativeProcess.js +2 -2
  34. package/lib/internal/SourceNativeRetirement.d.ts +8 -7
  35. package/lib/internal/SourceNativeRetirement.js +19 -10
  36. package/lib/internal/SourceNativeRetirement.js.map +1 -1
  37. package/lib/internal/TtsxNodeOptions.d.ts +79 -0
  38. package/lib/internal/TtsxNodeOptions.js +121 -0
  39. package/lib/internal/TtsxNodeOptions.js.map +1 -0
  40. package/lib/internal/javascriptRuntimeCapabilities.js +4 -1
  41. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  42. package/lib/internal/parseWindowsDirectoryCaseSensitivity.d.ts +5 -5
  43. package/lib/internal/parseWindowsDirectoryCaseSensitivity.js +5 -5
  44. package/lib/internal/pathIdentity/createCaseSensitivityObservations.d.ts +35 -0
  45. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js +209 -0
  46. package/lib/internal/pathIdentity/createCaseSensitivityObservations.js.map +1 -0
  47. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js +2 -143
  48. package/lib/internal/pathIdentity/createFilesystemPathIdentityContext.js.map +1 -1
  49. package/lib/internal/projectInputPathIdentity.d.ts +1 -0
  50. package/lib/internal/projectInputPathIdentity.js +1 -0
  51. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  52. package/lib/launcher/internal/parseTtsxCLI.d.ts +5 -4
  53. package/lib/launcher/internal/parseTtsxCLI.js +5 -4
  54. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  55. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  56. package/lib/launcher/internal/resolveSingleFileOutput.d.ts +8 -9
  57. package/lib/launcher/internal/resolveSingleFileOutput.js +8 -9
  58. package/lib/launcher/internal/resolveSingleFileOutput.js.map +1 -1
  59. package/lib/launcher/internal/runTtsc.js +5 -5
  60. package/lib/launcher/internal/runTtsx.js +5 -2
  61. package/lib/launcher/internal/runTtsx.js.map +1 -1
  62. package/lib/launcher/internal/runtime/RuntimeEntrySources.d.ts +40 -0
  63. package/lib/launcher/internal/runtime/RuntimeEntrySources.js +57 -0
  64. package/lib/launcher/internal/runtime/RuntimeEntrySources.js.map +1 -0
  65. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.d.ts +5 -4
  66. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js +8 -6
  67. package/lib/launcher/internal/runtime/RuntimeProjectOwnership.js.map +1 -1
  68. package/lib/launcher/internal/runtime/commonJsExportNames.d.ts +2 -1
  69. package/lib/launcher/internal/runtime/commonJsExportNames.js +2 -1
  70. package/lib/launcher/internal/runtime/commonJsExportNames.js.map +1 -1
  71. package/lib/launcher/internal/runtime/installRuntimeHooks.js +35 -9
  72. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  73. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js +3 -1
  74. package/lib/launcher/internal/runtime/resolveRuntimeCleanTargets.js.map +1 -1
  75. package/lib/launcher/internal/watch/WatchTopology.d.ts +74 -12
  76. package/lib/launcher/internal/watch/WatchTopology.js +301 -92
  77. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  78. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.d.ts +4 -1
  79. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js +3 -2
  80. package/lib/launcher/internal/watch/projectInputReloadEventShouldNotify.js.map +1 -1
  81. package/lib/plugin/internal/load/loadProjectPlugins.js +13 -7
  82. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  83. package/lib/plugin/internal/load/resolveNativeSource.js +2 -3
  84. package/lib/plugin/internal/load/resolveNativeSource.js.map +1 -1
  85. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +4 -1
  86. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +9 -2
  87. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  88. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -2
  89. package/package.json +9 -9
  90. package/src/compiler/internal/build/BuildExecution.ts +5 -2
  91. package/src/compiler/internal/build/TsgoArguments.ts +4 -1
  92. package/src/compiler/internal/project/CompilerProjectSelection.ts +14 -14
  93. package/src/compiler/internal/project/resolveOwningProjectConfig.ts +29 -14
  94. package/src/compiler/internal/runSingleFileEmit.ts +11 -7
  95. package/src/flags/ParseOptions.ts +2 -2
  96. package/src/internal/E2ETrace.ts +3 -1
  97. package/src/internal/OwnedNativeProcess.ts +2 -2
  98. package/src/internal/SourceNativeRetirement.ts +42 -19
  99. package/src/internal/TtsxNodeOptions.ts +124 -0
  100. package/src/internal/javascriptRuntimeCapabilities.ts +6 -1
  101. package/src/internal/parseWindowsDirectoryCaseSensitivity.ts +5 -5
  102. package/src/internal/pathIdentity/createCaseSensitivityObservations.ts +242 -0
  103. package/src/internal/pathIdentity/createFilesystemPathIdentityContext.ts +2 -169
  104. package/src/internal/projectInputPathIdentity.ts +1 -0
  105. package/src/launcher/internal/parseTtsxCLI.ts +5 -4
  106. package/src/launcher/internal/prepareExecution.ts +4 -1
  107. package/src/launcher/internal/resolveSingleFileOutput.ts +8 -9
  108. package/src/launcher/internal/runTtsc.ts +5 -5
  109. package/src/launcher/internal/runTtsx.ts +17 -8
  110. package/src/launcher/internal/runtime/RuntimeEntrySources.ts +57 -0
  111. package/src/launcher/internal/runtime/RuntimeProjectOwnership.ts +23 -10
  112. package/src/launcher/internal/runtime/commonJsExportNames.ts +11 -4
  113. package/src/launcher/internal/runtime/installRuntimeHooks.ts +55 -13
  114. package/src/launcher/internal/runtime/resolveRuntimeCleanTargets.ts +17 -5
  115. package/src/launcher/internal/watch/WatchTopology.ts +387 -99
  116. package/src/launcher/internal/watch/projectInputReloadEventShouldNotify.ts +14 -9
  117. package/src/plugin/internal/load/loadProjectPlugins.ts +13 -1
  118. package/src/plugin/internal/load/resolveNativeSource.ts +10 -4
  119. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +10 -1
  120. package/src/structures/internal/TtscCommonOptions.ts +2 -2
  121. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +0 -78
  122. package/internal/graphsymbols/provider.go +0 -650
  123. package/internal/lspserver/lsp_symbols.go +0 -212
@@ -15,6 +15,7 @@ const resolveTsgo_1 = require("../../../compiler/internal/resolveTsgo");
15
15
  const spawnNative_1 = require("../../../compiler/internal/spawnNative");
16
16
  const normalizeCompilerEnumValue_1 = require("../../../flags/normalizeCompilerEnumValue");
17
17
  const readCompilerOptionValues_1 = require("../../../flags/readCompilerOptionValues");
18
+ const createCaseSensitivityObservations_1 = require("../../../internal/pathIdentity/createCaseSensitivityObservations");
18
19
  const createProjectInputPathIdentityContext_1 = require("../../../internal/pathIdentity/createProjectInputPathIdentityContext");
19
20
  const isProjectInputPathIdentityWithin_1 = require("../../../internal/pathIdentity/isProjectInputPathIdentityWithin");
20
21
  const resolveProjectInputPath_1 = require("../../../internal/pathIdentity/resolveProjectInputPath");
@@ -44,26 +45,26 @@ const watchDirectory_1 = require("./watchDirectory");
44
45
  * output for source and declaration membership. An explicit reader supplies the
45
46
  * same absolute-path membership and failure contract for each resolved project.
46
47
  * Configuration files, project-reference roots, output inference and native
47
- * filesystem identity remain this topology's responsibility.
48
- * Response frames are observed once per project refresh at its compiler cwd;
49
- * that option view serves all project-output queries while the original argv
50
- * reaches compiler membership. A second observation rejects response changes
51
- * across membership resolution. Response files join the config reload set;
52
- * these observations do not pin files or detect a change restored in between.
48
+ * filesystem identity remain this topology's responsibility. Response frames
49
+ * are observed once per project refresh at its compiler cwd; that option view
50
+ * serves all project-output queries while the original argv reaches compiler
51
+ * membership. A second observation rejects response changes across membership
52
+ * resolution. Response files join the config reload set; these observations do
53
+ * not pin files or detect a change restored in between.
53
54
  *
54
55
  * `TTSC_WATCH_DEBUG_INPUTS` reports the named event, observed population deltas
55
56
  * and reload decision. These diagnostics reuse the decision's existing inputs;
56
57
  * they perform no extra filesystem observations.
57
58
  *
58
59
  * @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.
59
- * @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.
60
+ * @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.
60
61
  * @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.
61
62
  * @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.
62
63
  * @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.
63
64
  *
64
65
  * @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.
65
- * @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.
66
- * @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.
66
+ * @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.
67
+ * @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.
67
68
  */
68
69
  class WatchTopology {
69
70
  options;
@@ -72,6 +73,7 @@ class WatchTopology {
72
73
  openFileWatch;
73
74
  readDirectory;
74
75
  getCompilerInputs;
76
+ observeCaseSensitivity;
75
77
  analysisOnly = false;
76
78
  closed = false;
77
79
  compilerPostRegistrationMembershipRefresh = false;
@@ -102,15 +104,36 @@ class WatchTopology {
102
104
  /** The plugin inputs whose directories the last sync already watched. */
103
105
  watchedExtraInputs = new Set();
104
106
  compilerFileSnapshots = new Map();
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
+ compilerListings = new Map();
113
+ /**
114
+ * Notifications the current poll iteration delivered, decided together in the
115
+ * check phase (`deferNotification`).
116
+ */
117
+ pendingNotifications = new Map();
118
+ notificationsScheduled = false;
105
119
  files = new Map();
106
- lexicalIdentities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)({
107
- throwOnRealpathError: false,
108
- });
120
+ /**
121
+ * The current decision's directory case-policy observations, which every
122
+ * identity context the decision creates shares (samchon/ttsc#1742).
123
+ */
124
+ caseObservations;
125
+ lexicalIdentities;
109
126
  fileWatchers = new Map();
110
127
  observedDirectories = new Map();
111
128
  outputFiles = new Map();
112
129
  outputs = new Map();
113
130
  projectInputFingerprints = new Map();
131
+ /**
132
+ * The metadata and physical owner each matched project-input file had when
133
+ * its fingerprint was taken, by match key; a named notification re-reads only
134
+ * the files whose metadata moved (samchon/ttsc#1748).
135
+ */
136
+ projectInputFileSnapshots = new Map();
114
137
  projectInputMatches = new Map();
115
138
  projectInputs = {
116
139
  files: [],
@@ -150,14 +173,18 @@ class WatchTopology {
150
173
  * membership; the default invokes the native compiler. Readers throw
151
174
  * listing failures and retain ownership of their returned array, which this
152
175
  * topology does not mutate.
176
+ * @param observeCaseSensitivity Create one decision's directory case-policy
177
+ * observations; the default asks the native filesystem.
153
178
  */
154
- constructor(options, callbacks, openDirectoryWatch = watchDirectory_1.watchDirectory, openFileWatch = node_fs_1.default.watch, readDirectory = node_fs_1.default.readdirSync, getCompilerInputs = listCompilerInputs) {
179
+ constructor(options, callbacks, openDirectoryWatch = watchDirectory_1.watchDirectory, openFileWatch = node_fs_1.default.watch, readDirectory = node_fs_1.default.readdirSync, getCompilerInputs = listCompilerInputs, observeCaseSensitivity = createCaseSensitivityObservations_1.createCaseSensitivityObservations) {
155
180
  this.options = options;
156
181
  this.callbacks = callbacks;
157
182
  this.openDirectoryWatch = openDirectoryWatch;
158
183
  this.openFileWatch = openFileWatch;
159
184
  this.readDirectory = readDirectory;
160
185
  this.getCompilerInputs = getCompilerInputs;
186
+ this.observeCaseSensitivity = observeCaseSensitivity;
187
+ this.beginLexicalTransaction();
161
188
  }
162
189
  /**
163
190
  * Re-resolve compiler inputs and notify when the resolved topology changes,
@@ -177,16 +204,31 @@ class WatchTopology {
177
204
  refresh(notify) {
178
205
  this.refreshCompilerInputs(notify, false);
179
206
  }
180
- refreshCompilerInputs(notify, skipUnobservedProjectInputWatchRoots) {
207
+ /**
208
+ * @param reuseListings Answer each project's membership with its last listing
209
+ * under the same arguments instead of asking the compiler again. Only a
210
+ * refresh whose cause cannot move Program membership passes it.
211
+ */
212
+ refreshCompilerInputs(notify, skipUnobservedProjectInputWatchRoots, reuseListings = false) {
181
213
  this.beginLexicalTransaction();
182
- const next = resolveWatchTopology(this.options, this.extraInputs, this.lexicalIdentities, this.getCompilerInputs);
214
+ const listings = new Map();
215
+ const next = resolveWatchTopology(this.options, this.extraInputs, this.lexicalIdentities, (project, options) => {
216
+ const question = listingQuestion(project, options);
217
+ const last = this.compilerListings.get(project.path);
218
+ const inputs = reuseListings && last?.question === question
219
+ ? last.inputs
220
+ : this.getCompilerInputs(project, options);
221
+ listings.set(project.path, { inputs, question });
222
+ return inputs;
223
+ });
224
+ this.compilerListings = listings;
183
225
  const compilerProgramMembershipChange = next.analysisOnly &&
184
226
  WatchPaths_1.WatchPaths.mapsEqual(this.reloadFiles, next.reloadFiles) &&
185
227
  WatchPaths_1.WatchPaths.mapsEqual(this.outputFiles, next.outputFiles) &&
186
228
  WatchPaths_1.WatchPaths.mapsEqual(this.outputs, next.outputs)
187
229
  ? compilerMembershipChange(this.files, next.files)
188
230
  : [];
189
- const projectInputProgramOverlap = projectInputCompilerMembershipChange(this.projectInputs, compilerProgramMembershipChange);
231
+ const projectInputProgramOverlap = projectInputCompilerMembershipChange(this.projectInputs, compilerProgramMembershipChange, this.projectInputIdentities());
190
232
  const projectInputProgramChange = compilerProgramMembershipChange.length !== 0 &&
191
233
  projectInputProgramOverlap.length ===
192
234
  compilerProgramMembershipChange.length
@@ -260,7 +302,7 @@ class WatchTopology {
260
302
  */
261
303
  acknowledgeProjectInputCompilerMembership(changed) {
262
304
  const matches = this.collectProjectInputMatches();
263
- const fingerprints = fingerprintProjectInputMatches(matches, this.readDirectory);
305
+ const { fingerprints, snapshots } = fingerprintProjectInputMatches(matches, this.readDirectory);
264
306
  const changedInputs = projectInputChangedPaths({
265
307
  next: matches,
266
308
  nextFingerprints: fingerprints,
@@ -268,7 +310,7 @@ class WatchTopology {
268
310
  previousFingerprints: this.projectInputFingerprints,
269
311
  });
270
312
  const population = this.projectInputPopulation();
271
- const causedBy = projectInputCompilerMembershipProjectChanges(changed, population.globs);
313
+ const causedBy = projectInputCompilerMembershipProjectChanges(changed, population.globs, this.projectInputIdentities());
272
314
  const reloadInput = {
273
315
  causedBy,
274
316
  changed: causedBy.length === 1 ? causedBy[0] : undefined,
@@ -277,7 +319,7 @@ class WatchTopology {
277
319
  reloadDirectories: population.reloadDirectories,
278
320
  reloadFiles: population.reloadFiles ?? [],
279
321
  };
280
- const reload = (0, projectInputReloadEventShouldNotify_1.projectInputReloadEventShouldNotify)(reloadInput);
322
+ const reload = (0, projectInputReloadEventShouldNotify_1.projectInputReloadEventShouldNotify)(reloadInput, this.projectInputIdentities());
281
323
  reportProjectInputDecision({
282
324
  source: "compiler-membership",
283
325
  ...reloadInput,
@@ -289,6 +331,7 @@ class WatchTopology {
289
331
  // instead of disappearing behind the warm compiler-membership handoff.
290
332
  this.projectInputMatches = matches;
291
333
  this.projectInputFingerprints = fingerprints;
334
+ this.projectInputFileSnapshots = snapshots;
292
335
  return reload;
293
336
  }
294
337
  /**
@@ -313,8 +356,8 @@ class WatchTopology {
313
356
  * @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.
314
357
  * @evidence contracts/common.md#meaningful-documentation Native prose explains baseline timing and why existing inputs are not restamped following the documentation skill.
315
358
  * @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.
316
- * @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.
317
- * @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.
359
+ * @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.
360
+ * @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.
318
361
  * @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.
319
362
  */
320
363
  setExtraInputs(inputs, identities) {
@@ -337,7 +380,12 @@ class WatchTopology {
337
380
  pluginInputState(input, this.pluginInputIdentities));
338
381
  }
339
382
  this.pluginInputStates = states;
340
- this.refresh(false);
383
+ // Plugin inputs join the watched files and directories, never Program
384
+ // membership, so the compiler's last answer stands. A load reports up to
385
+ // three different sets, and asking again for each spawned tsgo three times
386
+ // per load (samchon/ttsc#1741). A membership change meanwhile reaches the
387
+ // refresh its own notification starts, which lists again.
388
+ this.refreshCompilerInputs(false, false, true);
341
389
  }
342
390
  /**
343
391
  * Reconcile project-rule dependencies, retaining absent files and empty glob
@@ -353,7 +401,8 @@ class WatchTopology {
353
401
  * @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.
354
402
  */
355
403
  setProjectInputs(inputs) {
356
- const next = normalizeProjectInputSnapshot(inputs);
404
+ this.beginLexicalTransaction();
405
+ const next = normalizeProjectInputSnapshot(inputs, this.projectInputIdentities());
357
406
  // The declared spellings are recorded even when the normalized snapshot did
358
407
  // not move, because a republication can carry a new alias for identities
359
408
  // that already matched, and anchoring the spelling that was retired would
@@ -362,7 +411,7 @@ class WatchTopology {
362
411
  inputs.declared === undefined
363
412
  ? inputs
364
413
  : { ...inputs.declared, root: inputs.root };
365
- if (projectInputSnapshotsEqual(this.projectInputs, next)) {
414
+ if (projectInputSnapshotsEqual(this.projectInputs, next, this.projectInputIdentities())) {
366
415
  this.syncProjectInputWatchers();
367
416
  return;
368
417
  }
@@ -374,7 +423,9 @@ class WatchTopology {
374
423
  { ...(inputs.declared ?? inputs), root: inputs.root },
375
424
  ]);
376
425
  this.projectInputMatches = this.collectProjectInputMatches();
377
- this.projectInputFingerprints = fingerprintProjectInputMatches(this.projectInputMatches, this.readDirectory);
426
+ const baseline = fingerprintProjectInputMatches(this.projectInputMatches, this.readDirectory);
427
+ this.projectInputFingerprints = baseline.fingerprints;
428
+ this.projectInputFileSnapshots = baseline.snapshots;
378
429
  this.syncProjectInputWatchers();
379
430
  }
380
431
  /**
@@ -406,10 +457,24 @@ class WatchTopology {
406
457
  }
407
458
  /** Start fresh native case observations for one synchronous watch decision. */
408
459
  beginLexicalTransaction() {
460
+ this.caseObservations = this.observeCaseSensitivity();
409
461
  this.lexicalIdentities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)({
462
+ caseSensitive: this.caseObservations,
410
463
  throwOnRealpathError: false,
411
464
  });
412
465
  }
466
+ /**
467
+ * A strict project-input identity context for the current decision. Its
468
+ * physical paths are resolved afresh, and its case answers are the
469
+ * decision's: one decision builds such contexts per declaration and per
470
+ * candidate, and each asked every directory again, which on Windows is an
471
+ * `fsutil` process per question (samchon/ttsc#1742).
472
+ */
473
+ projectInputIdentities() {
474
+ return (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)({
475
+ caseSensitive: this.caseObservations,
476
+ });
477
+ }
413
478
  /** Keep lexical alias ownership while sharing this decision's case probes. */
414
479
  pathKey(location) {
415
480
  return WatchPaths_1.WatchPaths.pathKey(location, this.lexicalIdentities);
@@ -487,32 +552,93 @@ class WatchTopology {
487
552
  }
488
553
  }
489
554
  }
490
- (0, syncWatchers_1.syncWatchers)(this.directoryWatchers, desired, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), process.platform === "win32", (event, filename, gap) => {
491
- this.beginLexicalTransaction();
492
- const changed = filename === null ? undefined : node_path_1.default.resolve(location, filename);
493
- const pluginInput = changed ?? location;
494
- if (this.isPluginInput(pluginInput)) {
495
- this.notePluginNotification(pluginInput);
496
- return;
497
- }
498
- const plan = (0, planCompilerDirectoryWatchEvent_1.planCompilerDirectoryWatchEvent)({
499
- changed,
500
- event,
501
- exists: node_fs_1.default.existsSync,
502
- location,
503
- identities: this.lexicalIdentities,
504
- platform: process.platform,
505
- trackedFiles: this.files,
506
- });
507
- this.rearmFileWatchers(plan.rearm);
508
- for (const file of this.compilerChangesToReport(plan.changes, changed, gap === true)) {
509
- this.reportCompilerFileChange(file);
510
- }
511
- if (plan.refresh)
512
- this.refreshFromDirectory(location, changed);
513
- }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
555
+ (0, syncWatchers_1.syncWatchers)(this.directoryWatchers, desired, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), process.platform === "win32", (event, filename, gap) => this.deferNotification({
556
+ gap: gap === true,
557
+ lane: "compiler",
558
+ location,
559
+ changed: filename === null
560
+ ? undefined
561
+ : node_path_1.default.resolve(location, filename),
562
+ rename: event === "rename",
563
+ })), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
514
564
  return [...this.directoryWatchers].some(([key, watcher]) => previous.get(key) !== watcher);
515
565
  }
566
+ /**
567
+ * Decide a notification in the event loop's check phase, together with every
568
+ * other notification the same poll iteration delivered (samchon/ttsc#1741).
569
+ *
570
+ * One filesystem change usually arrives as several notifications, rename and
571
+ * change or create and modify, and each listed the compiler inputs or
572
+ * rescanned the project inputs again. Identical notifications of one lane,
573
+ * location and path merge, keeping a rename or a gap any of them carried, and
574
+ * the batch refreshes compiler membership once after deciding them all. The
575
+ * refresh therefore starts after every notification it absorbs, and one
576
+ * delivered later schedules another batch.
577
+ */
578
+ deferNotification(notification) {
579
+ if (this.closed)
580
+ return;
581
+ const key = [
582
+ notification.lane,
583
+ notification.location,
584
+ notification.changed ?? "",
585
+ ].join("\0");
586
+ const pending = this.pendingNotifications.get(key);
587
+ if (pending !== undefined) {
588
+ pending.rename ||= notification.rename;
589
+ pending.gap ||= notification.gap;
590
+ return;
591
+ }
592
+ this.pendingNotifications.set(key, { ...notification });
593
+ if (this.notificationsScheduled)
594
+ return;
595
+ this.notificationsScheduled = true;
596
+ setImmediate(() => this.decideNotifications());
597
+ }
598
+ decideNotifications() {
599
+ this.notificationsScheduled = false;
600
+ const pending = [...this.pendingNotifications.values()];
601
+ this.pendingNotifications.clear();
602
+ if (this.closed)
603
+ return;
604
+ const refreshes = [];
605
+ for (const notification of pending) {
606
+ if (notification.lane === "project-input") {
607
+ this.refreshProjectInputs(notification.location, notification.changed);
608
+ continue;
609
+ }
610
+ this.decideCompilerDirectoryNotification(notification, refreshes);
611
+ }
612
+ if (refreshes.length === 0 || this.closed)
613
+ return;
614
+ this.beginLexicalTransaction();
615
+ this.refreshFromDirectories(refreshes);
616
+ }
617
+ /** Decide one merged directory notification of the compiler lane. */
618
+ decideCompilerDirectoryNotification(notification, refreshes) {
619
+ this.beginLexicalTransaction();
620
+ const { changed, location } = notification;
621
+ const pluginInput = changed ?? location;
622
+ if (this.isPluginInput(pluginInput)) {
623
+ this.notePluginNotification(pluginInput);
624
+ return;
625
+ }
626
+ const plan = (0, planCompilerDirectoryWatchEvent_1.planCompilerDirectoryWatchEvent)({
627
+ changed,
628
+ event: notification.rename ? "rename" : "change",
629
+ exists: node_fs_1.default.existsSync,
630
+ location,
631
+ identities: this.lexicalIdentities,
632
+ platform: process.platform,
633
+ trackedFiles: this.files,
634
+ });
635
+ this.rearmFileWatchers(plan.rearm);
636
+ for (const file of this.compilerChangesToReport(plan.changes, changed, notification.gap)) {
637
+ this.reportCompilerFileChange(file);
638
+ }
639
+ if (plan.refresh)
640
+ refreshes.push({ changed, location });
641
+ }
516
642
  /**
517
643
  * Reconcile tracked compiler files after a newly registered watcher returns.
518
644
  *
@@ -607,6 +733,7 @@ class WatchTopology {
607
733
  if (node_fs_1.default.existsSync(location)) {
608
734
  if (this.projectInputMatches.size !== 0) {
609
735
  const key = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)({
736
+ caseSensitive: this.caseObservations,
610
737
  throwOnRealpathError: false,
611
738
  }).resolve(location).key;
612
739
  if (this.projectInputMatches.has(key) &&
@@ -701,7 +828,7 @@ class WatchTopology {
701
828
  return;
702
829
  const previous = new Map(this.projectInputWatchers);
703
830
  const previousLinks = new Map(this.projectInputLinkWatchers);
704
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
831
+ const identities = this.projectInputIdentities();
705
832
  const desired = new Map();
706
833
  const required = new Map();
707
834
  for (const file of this.projectInputDeclarations("file")) {
@@ -737,10 +864,15 @@ class WatchTopology {
737
864
  active.set(identity.key, identity.path);
738
865
  }
739
866
  this.projectInputRequiredWatchRoots = required;
740
- (0, syncWatchers_1.syncWatchers)(this.projectInputWatchers, active, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), true, (_event, filename) => {
741
- const changed = filename === null ? undefined : node_path_1.default.resolve(location, filename);
742
- this.refreshProjectInputs(location, changed);
743
- }), (location, error) => {
867
+ (0, syncWatchers_1.syncWatchers)(this.projectInputWatchers, active, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), true, (_event, filename) => this.deferNotification({
868
+ gap: false,
869
+ lane: "project-input",
870
+ location,
871
+ changed: filename === null
872
+ ? undefined
873
+ : node_path_1.default.resolve(location, filename),
874
+ rename: false,
875
+ })), (location, error) => {
744
876
  const key = identities.resolve(location).key;
745
877
  const firstFailure = !this.projectInputRejectedWatchRoots.has(key);
746
878
  this.projectInputRejectedWatchRoots.add(key);
@@ -796,10 +928,15 @@ class WatchTopology {
796
928
  desired.set(identities.resolve(parent).key, parent);
797
929
  }
798
930
  }
799
- (0, syncWatchers_1.syncWatchers)(this.projectInputLinkWatchers, desired, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), false, (_event, filename) => {
800
- const changed = filename === null ? undefined : node_path_1.default.resolve(location, filename);
801
- this.refreshProjectInputs(location, changed);
802
- }), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
931
+ (0, syncWatchers_1.syncWatchers)(this.projectInputLinkWatchers, desired, (location) => this.openDirectoryWatch(watcherRegistrationPath(location), false, (_event, filename) => this.deferNotification({
932
+ gap: false,
933
+ lane: "project-input",
934
+ location,
935
+ changed: filename === null
936
+ ? undefined
937
+ : node_path_1.default.resolve(location, filename),
938
+ rename: false,
939
+ })), (location, error) => this.callbacks.onError(location, error), () => this.closed === false);
803
940
  }
804
941
  /** Drop the watcher that just reported a directory replacement. */
805
942
  retireProjectInputWatcher(location, identities) {
@@ -857,6 +994,7 @@ class WatchTopology {
857
994
  return;
858
995
  this.projectInputRecoveryScheduled = true;
859
996
  queueMicrotask(() => {
997
+ this.beginLexicalTransaction();
860
998
  try {
861
999
  let previousRejectionCount = -1;
862
1000
  while (this.closed === false &&
@@ -895,6 +1033,7 @@ class WatchTopology {
895
1033
  this.projectInputPostRegistrationReconciliationScheduled = false;
896
1034
  if (this.closed)
897
1035
  return;
1036
+ this.beginLexicalTransaction();
898
1037
  this.refreshPublishedProjectInputIdentities();
899
1038
  this.refreshProjectInputs(this.projectInputs.root, undefined, true);
900
1039
  });
@@ -908,8 +1047,9 @@ class WatchTopology {
908
1047
  * that must observe later target changes.
909
1048
  */
910
1049
  refreshPublishedProjectInputIdentities() {
911
- const next = normalizeProjectInputSnapshot(this.declaredProjectInputs);
912
- if (projectInputSnapshotsEqual(this.projectInputs, next))
1050
+ const identities = this.projectInputIdentities();
1051
+ const next = normalizeProjectInputSnapshot(this.declaredProjectInputs, identities);
1052
+ if (projectInputSnapshotsEqual(this.projectInputs, next, identities))
913
1053
  return;
914
1054
  this.projectInputs = next;
915
1055
  this.pruneProjectInputWatchRoots([next, this.declaredProjectInputs]);
@@ -997,7 +1137,7 @@ class WatchTopology {
997
1137
  const retained = this.projectInputWatchRoots.get(key);
998
1138
  if (retained !== undefined && WatchPaths_1.WatchPaths.isDirectory(retained))
999
1139
  return retained;
1000
- const resolved = ProjectInputWatchRules_1.ProjectInputWatchRules.projectInputRecursiveWatchRoot(target, this.projectInputs.root);
1140
+ const resolved = ProjectInputWatchRules_1.ProjectInputWatchRules.projectInputRecursiveWatchRoot(target, this.projectInputs.root, this.projectInputIdentities());
1001
1141
  if (resolved !== undefined)
1002
1142
  this.projectInputWatchRoots.set(key, resolved);
1003
1143
  return resolved;
@@ -1006,7 +1146,7 @@ class WatchTopology {
1006
1146
  this.beginLexicalTransaction();
1007
1147
  try {
1008
1148
  const previous = this.projectInputMatches;
1009
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
1149
+ const identities = this.projectInputIdentities();
1010
1150
  // One population for the whole decision. Every question below is asked of
1011
1151
  // the same declarations, and rebuilding it per question would both cost
1012
1152
  // more and let two answers disagree about what was declared.
@@ -1033,12 +1173,25 @@ class WatchTopology {
1033
1173
  }
1034
1174
  const next = this.collectProjectInputMatches();
1035
1175
  const membershipChanged = WatchPaths_1.WatchPaths.mapsEqual(previous, next) === false;
1036
- const nextFingerprints = changed === undefined ||
1176
+ // A named notification reads the named path's bytes and any file whose
1177
+ // metadata or owner moved; one that names nothing reads every file
1178
+ // (samchon/ttsc#1748).
1179
+ const scanned = changed === undefined ||
1037
1180
  membershipChanged ||
1038
1181
  directlyMatched ||
1039
1182
  topologyMatched
1040
- ? fingerprintProjectInputMatches(next, this.readDirectory)
1041
- : this.projectInputFingerprints;
1183
+ ? fingerprintProjectInputMatches(next, this.readDirectory, changed === undefined
1184
+ ? undefined
1185
+ : {
1186
+ fingerprints: this.projectInputFingerprints,
1187
+ named: identities.resolve(changed).key,
1188
+ snapshots: this.projectInputFileSnapshots,
1189
+ })
1190
+ : {
1191
+ fingerprints: this.projectInputFingerprints,
1192
+ snapshots: this.projectInputFileSnapshots,
1193
+ };
1194
+ const nextFingerprints = scanned.fingerprints;
1042
1195
  const contentChanged = WatchPaths_1.WatchPaths.mapsEqual(this.projectInputFingerprints, nextFingerprints) === false;
1043
1196
  const changedInputs = projectInputChangedPaths({
1044
1197
  next,
@@ -1065,7 +1218,7 @@ class WatchTopology {
1065
1218
  reloadDirectories: population.reloadDirectories ?? [],
1066
1219
  reloadFiles: population.reloadFiles ?? [],
1067
1220
  };
1068
- const reload = (0, projectInputReloadEventShouldNotify_1.projectInputReloadEventShouldNotify)(reloadInput);
1221
+ const reload = (0, projectInputReloadEventShouldNotify_1.projectInputReloadEventShouldNotify)(reloadInput, identities);
1069
1222
  const invalidate = (0, projectInputMembershipInvalidatesProgram_1.projectInputMembershipInvalidatesProgram)({
1070
1223
  changed: reconciledChange,
1071
1224
  changedInputs,
@@ -1075,6 +1228,7 @@ class WatchTopology {
1075
1228
  });
1076
1229
  this.projectInputMatches = next;
1077
1230
  this.projectInputFingerprints = nextFingerprints;
1231
+ this.projectInputFileSnapshots = scanned.snapshots;
1078
1232
  this.syncProjectInputWatchers(skipUnobservedProjectInputWatchRoots);
1079
1233
  // A JSON/TS/JS project-input member can simultaneously enter or leave
1080
1234
  // the compiler Program. Reconcile the compiler watch snapshot before
@@ -1126,7 +1280,7 @@ class WatchTopology {
1126
1280
  }
1127
1281
  }
1128
1282
  collectProjectInputMatches() {
1129
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
1283
+ const identities = this.projectInputIdentities();
1130
1284
  const matches = new Map();
1131
1285
  // Both spellings are scanned, and each is resolved here rather than when
1132
1286
  // the snapshot arrived. A declaration reached through a symlink otherwise
@@ -1207,20 +1361,37 @@ class WatchTopology {
1207
1361
  return matches;
1208
1362
  }
1209
1363
  refreshFromDirectory(location, changed) {
1210
- if (changed !== undefined &&
1211
- WatchPaths_1.WatchPaths.isDirectory(changed) &&
1212
- this.isCompilerOutputDirectory(changed) === false &&
1213
- this.isProjectInputDirectory(changed) === false) {
1214
- this.observedDirectories.set(this.pathKey(changed), changed);
1364
+ this.refreshFromDirectories([{ changed, location }]);
1365
+ }
1366
+ /**
1367
+ * Refresh compiler membership once for every directory a decision asked
1368
+ * about. A failed refresh still reports, once each, the reload inputs every
1369
+ * requested change names.
1370
+ */
1371
+ refreshFromDirectories(refreshes) {
1372
+ for (const { changed } of refreshes) {
1373
+ if (changed !== undefined &&
1374
+ WatchPaths_1.WatchPaths.isDirectory(changed) &&
1375
+ this.isCompilerOutputDirectory(changed) === false &&
1376
+ this.isProjectInputDirectory(changed) === false) {
1377
+ this.observedDirectories.set(this.pathKey(changed), changed);
1378
+ }
1215
1379
  }
1216
1380
  try {
1217
1381
  this.refresh(true);
1218
1382
  }
1219
1383
  catch (error) {
1220
- for (const reload of (0, reloadInputsForFailedTopologyRefresh_1.reloadInputsForFailedTopologyRefresh)(this.reloadFiles.values(), changed, this.lexicalIdentities)) {
1221
- this.callbacks.onInputChange({ kind: "config", path: reload });
1384
+ const reported = new Set();
1385
+ for (const { changed } of refreshes) {
1386
+ for (const reload of (0, reloadInputsForFailedTopologyRefresh_1.reloadInputsForFailedTopologyRefresh)(this.reloadFiles.values(), changed, this.lexicalIdentities)) {
1387
+ const key = this.pathKey(reload);
1388
+ if (reported.has(key))
1389
+ continue;
1390
+ reported.add(key);
1391
+ this.callbacks.onInputChange({ kind: "config", path: reload });
1392
+ }
1222
1393
  }
1223
- this.callbacks.onError(location, error);
1394
+ this.callbacks.onError(refreshes[0].location, error);
1224
1395
  }
1225
1396
  }
1226
1397
  isCompilerOutputDirectory(location) {
@@ -1265,7 +1436,7 @@ class WatchTopology {
1265
1436
  }
1266
1437
  isProjectInputDirectory(location) {
1267
1438
  const resolved = node_path_1.default.resolve(location);
1268
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
1439
+ const identities = this.projectInputIdentities();
1269
1440
  return (this.projectInputs.files.some((file) => identities.isWithin(resolved, file) ||
1270
1441
  identities.isWithin(node_path_1.default.dirname(file), resolved)) ||
1271
1442
  (this.projectInputs.reloadFiles ?? []).some((file) => identities.isWithin(resolved, file) ||
@@ -1378,6 +1549,23 @@ class WatchTopology {
1378
1549
  }
1379
1550
  }
1380
1551
  exports.WatchTopology = WatchTopology;
1552
+ /**
1553
+ * Everything a compiler listing of `project` is asked with: the project, and
1554
+ * each option `listCompilerInputs` passes to the compiler. Two refreshes with
1555
+ * the same question ask the compiler the same thing; only the disk can answer
1556
+ * them differently.
1557
+ */
1558
+ function listingQuestion(project, options) {
1559
+ return JSON.stringify([
1560
+ project.path,
1561
+ project.root,
1562
+ options.binary ?? null,
1563
+ options.compilerArgsCwd ?? null,
1564
+ options.env ?? null,
1565
+ options.passthrough ?? null,
1566
+ options.pinCompilerProject ?? null,
1567
+ ]);
1568
+ }
1381
1569
  function resolveWatchTopology(options, extraInputs, identities, getCompilerInputs) {
1382
1570
  const selection = CompilerProjectSelection_1.CompilerProjectSelection.read(options);
1383
1571
  if (selection.inspectionError !== undefined)
@@ -1811,8 +1999,7 @@ function uniqueExistingPaths(paths, identities) {
1811
1999
  }
1812
2000
  return [...unique.values()];
1813
2001
  }
1814
- function normalizeProjectInputSnapshot(snapshot) {
1815
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
2002
+ function normalizeProjectInputSnapshot(snapshot, identities) {
1816
2003
  const files = new Map();
1817
2004
  const globs = new Map();
1818
2005
  const reloadDirectories = new Map();
@@ -1841,8 +2028,7 @@ function normalizeProjectInputSnapshot(snapshot) {
1841
2028
  root: identities.resolve(snapshot.root).path,
1842
2029
  };
1843
2030
  }
1844
- function projectInputSnapshotsEqual(left, right) {
1845
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
2031
+ function projectInputSnapshotsEqual(left, right, identities) {
1846
2032
  return (
1847
2033
  // Every sibling comparison in this class decides on identity, and this one
1848
2034
  // is only safe today because both operands come from the same normalizer.
@@ -1885,7 +2071,7 @@ function isSymbolicLink(location) {
1885
2071
  return false;
1886
2072
  }
1887
2073
  }
1888
- function matchesProjectInput(snapshot, location, identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)()) {
2074
+ function matchesProjectInput(snapshot, location, identities) {
1889
2075
  const key = identities.resolve(location).key;
1890
2076
  return (snapshot.files.some((file) => identities.resolve(file).key === key) ||
1891
2077
  (snapshot.reloadFiles ?? []).some((file) => identities.resolve(file).key === key) ||
@@ -1971,8 +2157,7 @@ function compilerMembershipChange(previous, next) {
1971
2157
  }
1972
2158
  return [...changed.values()].sort();
1973
2159
  }
1974
- function projectInputCompilerMembershipChange(snapshot, changed) {
1975
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
2160
+ function projectInputCompilerMembershipChange(snapshot, changed, identities) {
1976
2161
  return changed.filter((location) => matchesProjectInput(snapshot, location, identities) &&
1977
2162
  ProjectInputWatchRules_1.ProjectInputWatchRules.projectInputPathMayAffectProgram(location));
1978
2163
  }
@@ -1985,8 +2170,7 @@ function projectInputCompilerMembershipChange(snapshot, changed) {
1985
2170
  * immediate-entry delta instead of turning ordinary project data into a cold
1986
2171
  * selection reload. The deepest matching glob owns the most specific delivery.
1987
2172
  */
1988
- function projectInputCompilerMembershipProjectChanges(locations, globs) {
1989
- const identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)();
2173
+ function projectInputCompilerMembershipProjectChanges(locations, globs, identities) {
1990
2174
  const roots = globs.map((glob) => identities.resolve((0, literalGlobRoot_1.literalGlobRoot)(glob)));
1991
2175
  const changes = new Map();
1992
2176
  for (const location of locations) {
@@ -2003,14 +2187,39 @@ function projectInputCompilerMembershipProjectChanges(locations, globs) {
2003
2187
  }
2004
2188
  return [...changes.values()].sort();
2005
2189
  }
2006
- function fingerprintProjectInputMatches(matches, readDirectory) {
2190
+ /**
2191
+ * Fingerprint every matched project input: a directory by its entries, a file
2192
+ * by its bytes.
2193
+ *
2194
+ * Without `previous`, every file is read. With it, a file keeps its previous
2195
+ * digest while its metadata and physical owner hold, unless it is the `named`
2196
+ * path, whose bytes are always read: a notification that names a file is the
2197
+ * one whose same-size rewrite within a timestamp tick metadata cannot show.
2198
+ */
2199
+ function fingerprintProjectInputMatches(matches, readDirectory, previous) {
2007
2200
  const fingerprints = new Map();
2201
+ const snapshots = new Map();
2008
2202
  for (const [key, location] of matches) {
2009
- fingerprints.set(key, WatchPaths_1.WatchPaths.isDirectory(location)
2010
- ? fingerprintProjectInputDirectory(location, readDirectory)
2011
- : fingerprintProjectInputFile(location));
2012
- }
2013
- return fingerprints;
2203
+ if (WatchPaths_1.WatchPaths.isDirectory(location)) {
2204
+ fingerprints.set(key, fingerprintProjectInputDirectory(location, readDirectory));
2205
+ continue;
2206
+ }
2207
+ const metadata = compilerFileSnapshot(location, "");
2208
+ const before = previous?.snapshots.get(key);
2209
+ const kept = previous?.fingerprints.get(key);
2210
+ const fingerprint = previous !== undefined &&
2211
+ key !== previous.named &&
2212
+ before !== undefined &&
2213
+ kept !== undefined &&
2214
+ metadata.content !== "" &&
2215
+ before.content === metadata.content &&
2216
+ before.owner === metadata.owner
2217
+ ? kept
2218
+ : fingerprintProjectInputFile(location);
2219
+ fingerprints.set(key, fingerprint);
2220
+ snapshots.set(key, { ...metadata, fingerprint });
2221
+ }
2222
+ return { fingerprints, snapshots };
2014
2223
  }
2015
2224
  function fingerprintProjectInputFile(location) {
2016
2225
  try {
@@ -2052,7 +2261,7 @@ function fingerprintProjectInputDirectory(location, readDirectory) {
2052
2261
  return "";
2053
2262
  }
2054
2263
  }
2055
- function matchesProjectInputGlob(pattern, location, identities = (0, createProjectInputPathIdentityContext_1.createProjectInputPathIdentityContext)()) {
2264
+ function matchesProjectInputGlob(pattern, location, identities) {
2056
2265
  const root = identities.resolve((0, literalGlobRoot_1.literalGlobRoot)(pattern));
2057
2266
  const candidate = identities.resolve(location);
2058
2267
  if (!(0, isProjectInputPathIdentityWithin_1.isProjectInputPathIdentityWithin)(root.key, candidate.key))