@ttsc/unplugin 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 (171) hide show
  1. package/lib/core/bridge/TtscProjectRecord.d.cts +8 -0
  2. package/lib/core/bridge/TtscProjectRecord.d.mts +8 -0
  3. package/lib/core/bridge/TtscProjectRecord.d.ts +8 -0
  4. package/lib/core/bridge/readProjectRecordFile.js +5 -1
  5. package/lib/core/bridge/readProjectRecordFile.js.map +1 -1
  6. package/lib/core/bridge/readProjectRecordFile.mjs +5 -1
  7. package/lib/core/bridge/readProjectRecordFile.mjs.map +1 -1
  8. package/lib/core/observer/WatchScope.d.cts +3 -3
  9. package/lib/core/observer/WatchScope.d.mts +3 -3
  10. package/lib/core/observer/WatchScope.d.ts +3 -3
  11. package/lib/core/observer/createInputObserver.d.cts +7 -8
  12. package/lib/core/observer/createInputObserver.d.mts +7 -8
  13. package/lib/core/observer/createInputObserver.d.ts +7 -8
  14. package/lib/core/observer/createInputObserver.js +7 -8
  15. package/lib/core/observer/createInputObserver.js.map +1 -1
  16. package/lib/core/observer/createInputObserver.mjs +7 -8
  17. package/lib/core/observer/createInputObserver.mjs.map +1 -1
  18. package/lib/core/transform/envelope/generationCaseSensitivity.d.cts +32 -0
  19. package/lib/core/transform/envelope/generationCaseSensitivity.d.mts +32 -0
  20. package/lib/core/transform/envelope/generationCaseSensitivity.d.ts +32 -0
  21. package/lib/core/transform/envelope/generationCaseSensitivity.js +39 -0
  22. package/lib/core/transform/envelope/generationCaseSensitivity.js.map +1 -0
  23. package/lib/core/transform/envelope/generationCaseSensitivity.mjs +37 -0
  24. package/lib/core/transform/envelope/generationCaseSensitivity.mjs.map +1 -0
  25. package/lib/core/transform/envelope/selectExternalInputPaths.js +2 -1
  26. package/lib/core/transform/envelope/selectExternalInputPaths.js.map +1 -1
  27. package/lib/core/transform/envelope/selectExternalInputPaths.mjs +2 -1
  28. package/lib/core/transform/envelope/selectExternalInputPaths.mjs.map +1 -1
  29. package/lib/core/transform/envelope/selectNotifiableAbsentInputs.js +2 -1
  30. package/lib/core/transform/envelope/selectNotifiableAbsentInputs.js.map +1 -1
  31. package/lib/core/transform/envelope/selectNotifiableAbsentInputs.mjs +2 -1
  32. package/lib/core/transform/envelope/selectNotifiableAbsentInputs.mjs.map +1 -1
  33. package/lib/core/transform/filesystem/createHostPathIdentityContext.d.cts +9 -1
  34. package/lib/core/transform/filesystem/createHostPathIdentityContext.d.mts +9 -1
  35. package/lib/core/transform/filesystem/createHostPathIdentityContext.d.ts +9 -1
  36. package/lib/core/transform/filesystem/createHostPathIdentityContext.js +10 -2
  37. package/lib/core/transform/filesystem/createHostPathIdentityContext.js.map +1 -1
  38. package/lib/core/transform/filesystem/createHostPathIdentityContext.mjs +10 -2
  39. package/lib/core/transform/filesystem/createHostPathIdentityContext.mjs.map +1 -1
  40. package/lib/core/transform/generation/captureTransformGeneration.js +10 -2
  41. package/lib/core/transform/generation/captureTransformGeneration.js.map +1 -1
  42. package/lib/core/transform/generation/captureTransformGeneration.mjs +10 -2
  43. package/lib/core/transform/generation/captureTransformGeneration.mjs.map +1 -1
  44. package/lib/core/transform/generation/selectPersistentHostInputs.js +2 -1
  45. package/lib/core/transform/generation/selectPersistentHostInputs.js.map +1 -1
  46. package/lib/core/transform/generation/selectPersistentHostInputs.mjs +2 -1
  47. package/lib/core/transform/generation/selectPersistentHostInputs.mjs.map +1 -1
  48. package/lib/core/transform/inputs/nativeInputPredicateState.js.map +1 -1
  49. package/lib/core/transform/inputs/nativeInputPredicateState.mjs.map +1 -1
  50. package/lib/core/transform/tracker/broker/watchBrokerSource.js +14 -0
  51. package/lib/core/transform/tracker/broker/watchBrokerSource.js.map +1 -1
  52. package/lib/core/transform/tracker/broker/watchBrokerSource.mjs +14 -0
  53. package/lib/core/transform/tracker/broker/watchBrokerSource.mjs.map +1 -1
  54. package/lib/core/transform/tracker/createHostInputMutationTracker.d.cts +7 -1
  55. package/lib/core/transform/tracker/createHostInputMutationTracker.d.mts +7 -1
  56. package/lib/core/transform/tracker/createHostInputMutationTracker.d.ts +7 -1
  57. package/lib/core/transform/tracker/createHostInputMutationTracker.js +28 -2
  58. package/lib/core/transform/tracker/createHostInputMutationTracker.js.map +1 -1
  59. package/lib/core/transform/tracker/createHostInputMutationTracker.mjs +28 -2
  60. package/lib/core/transform/tracker/createHostInputMutationTracker.mjs.map +1 -1
  61. package/lib/core/transform/tracker/createProjectMutationTracker.d.cts +10 -7
  62. package/lib/core/transform/tracker/createProjectMutationTracker.d.mts +10 -7
  63. package/lib/core/transform/tracker/createProjectMutationTracker.d.ts +10 -7
  64. package/lib/core/transform/tracker/createProjectMutationTracker.js +82 -38
  65. package/lib/core/transform/tracker/createProjectMutationTracker.js.map +1 -1
  66. package/lib/core/transform/tracker/createProjectMutationTracker.mjs +82 -38
  67. package/lib/core/transform/tracker/createProjectMutationTracker.mjs.map +1 -1
  68. package/lib/core/transform/transformTtsc.js +5 -1
  69. package/lib/core/transform/transformTtsc.js.map +1 -1
  70. package/lib/core/transform/transformTtsc.mjs +5 -1
  71. package/lib/core/transform/transformTtsc.mjs.map +1 -1
  72. package/lib/core/transform/watch/TtscWatchInputBaseline.d.cts +2 -1
  73. package/lib/core/transform/watch/TtscWatchInputBaseline.d.mts +2 -1
  74. package/lib/core/transform/watch/TtscWatchInputBaseline.d.ts +2 -1
  75. package/lib/core/transform/watch/isWatchInputKeyBaseline.js.map +1 -1
  76. package/lib/core/transform/watch/isWatchInputKeyBaseline.mjs.map +1 -1
  77. package/lib/core/transform/watch/notifyProjectRecord.d.cts +3 -4
  78. package/lib/core/transform/watch/notifyProjectRecord.d.mts +3 -4
  79. package/lib/core/transform/watch/notifyProjectRecord.d.ts +3 -4
  80. package/lib/core/transform/watch/notifyProjectRecord.js +5 -4
  81. package/lib/core/transform/watch/notifyProjectRecord.js.map +1 -1
  82. package/lib/core/transform/watch/notifyProjectRecord.mjs +5 -4
  83. package/lib/core/transform/watch/notifyProjectRecord.mjs.map +1 -1
  84. package/lib/core/transform/watch/recordedDependencyWitnesses.d.cts +30 -0
  85. package/lib/core/transform/watch/recordedDependencyWitnesses.d.mts +30 -0
  86. package/lib/core/transform/watch/recordedDependencyWitnesses.d.ts +30 -0
  87. package/lib/core/transform/watch/recordedDependencyWitnesses.js +51 -0
  88. package/lib/core/transform/watch/recordedDependencyWitnesses.js.map +1 -0
  89. package/lib/core/transform/watch/recordedDependencyWitnesses.mjs +49 -0
  90. package/lib/core/transform/watch/recordedDependencyWitnesses.mjs.map +1 -0
  91. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.d.cts +1 -2
  92. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.d.mts +1 -2
  93. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.d.ts +1 -2
  94. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.js +1 -2
  95. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.js.map +1 -1
  96. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.mjs +1 -2
  97. package/lib/core/transform/watch/watchInputEvidenceMatchesBaseline.mjs.map +1 -1
  98. package/lib/core/tsconfig/TsconfigReadTransaction.d.cts +11 -8
  99. package/lib/core/tsconfig/TsconfigReadTransaction.d.mts +11 -8
  100. package/lib/core/tsconfig/TsconfigReadTransaction.d.ts +11 -8
  101. package/lib/core/tsconfig/TsconfigReadTransaction.js +11 -8
  102. package/lib/core/tsconfig/TsconfigReadTransaction.js.map +1 -1
  103. package/lib/core/tsconfig/TsconfigReadTransaction.mjs +11 -8
  104. package/lib/core/tsconfig/TsconfigReadTransaction.mjs.map +1 -1
  105. package/lib/core/tsconfig/findDeclaredCompilerOption.d.cts +3 -2
  106. package/lib/core/tsconfig/findDeclaredCompilerOption.d.mts +3 -2
  107. package/lib/core/tsconfig/findDeclaredCompilerOption.d.ts +3 -2
  108. package/lib/core/tsconfig/findDeclaredCompilerOption.js +3 -2
  109. package/lib/core/tsconfig/findDeclaredCompilerOption.js.map +1 -1
  110. package/lib/core/tsconfig/findDeclaredCompilerOption.mjs +3 -2
  111. package/lib/core/tsconfig/findDeclaredCompilerOption.mjs.map +1 -1
  112. package/lib/core/tsconfig/findDeclaredFileSpecs.d.cts +3 -2
  113. package/lib/core/tsconfig/findDeclaredFileSpecs.d.mts +3 -2
  114. package/lib/core/tsconfig/findDeclaredFileSpecs.d.ts +3 -2
  115. package/lib/core/tsconfig/findDeclaredFileSpecs.js +3 -2
  116. package/lib/core/tsconfig/findDeclaredFileSpecs.js.map +1 -1
  117. package/lib/core/tsconfig/findDeclaredFileSpecs.mjs +3 -2
  118. package/lib/core/tsconfig/findDeclaredFileSpecs.mjs.map +1 -1
  119. package/lib/core/tsconfig/findDeclaredPaths.d.cts +3 -3
  120. package/lib/core/tsconfig/findDeclaredPaths.d.mts +3 -3
  121. package/lib/core/tsconfig/findDeclaredPaths.d.ts +3 -3
  122. package/lib/core/tsconfig/findDeclaredPaths.js +2 -2
  123. package/lib/core/tsconfig/findDeclaredPaths.mjs +2 -2
  124. package/lib/core/tsconfig/findDeclaredValue.d.cts +2 -1
  125. package/lib/core/tsconfig/findDeclaredValue.d.mts +2 -1
  126. package/lib/core/tsconfig/findDeclaredValue.d.ts +2 -1
  127. package/lib/core/tsconfig/findDeclaredValue.js +2 -1
  128. package/lib/core/tsconfig/findDeclaredValue.js.map +1 -1
  129. package/lib/core/tsconfig/findDeclaredValue.mjs +2 -1
  130. package/lib/core/tsconfig/findDeclaredValue.mjs.map +1 -1
  131. package/lib/core/tsconfig/readEffectiveTsconfigTemplateCompilerOptions.js +1 -1
  132. package/lib/core/tsconfig/readEffectiveTsconfigTemplateCompilerOptions.mjs +1 -1
  133. package/lib/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.d.cts +2 -2
  134. package/lib/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.d.mts +2 -2
  135. package/lib/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.d.ts +2 -2
  136. package/lib/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.js +2 -2
  137. package/lib/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.mjs +2 -2
  138. package/lib/core/tsconfig/readProjectMembershipPolicy.d.cts +3 -3
  139. package/lib/core/tsconfig/readProjectMembershipPolicy.d.mts +3 -3
  140. package/lib/core/tsconfig/readProjectMembershipPolicy.d.ts +3 -3
  141. package/lib/core/tsconfig/readProjectMembershipPolicy.js +4 -4
  142. package/lib/core/tsconfig/readProjectMembershipPolicy.mjs +4 -4
  143. package/package.json +4 -4
  144. package/src/core/bridge/TtscProjectRecord.ts +9 -0
  145. package/src/core/bridge/readProjectRecordFile.ts +5 -1
  146. package/src/core/observer/WatchScope.ts +3 -3
  147. package/src/core/observer/createInputObserver.ts +7 -8
  148. package/src/core/transform/envelope/generationCaseSensitivity.ts +46 -0
  149. package/src/core/transform/envelope/selectExternalInputPaths.ts +5 -1
  150. package/src/core/transform/envelope/selectNotifiableAbsentInputs.ts +5 -1
  151. package/src/core/transform/filesystem/createHostPathIdentityContext.ts +9 -1
  152. package/src/core/transform/generation/captureTransformGeneration.ts +10 -0
  153. package/src/core/transform/generation/selectPersistentHostInputs.ts +5 -1
  154. package/src/core/transform/inputs/nativeInputPredicateState.ts +1 -3
  155. package/src/core/transform/tracker/broker/watchBrokerSource.ts +14 -0
  156. package/src/core/transform/tracker/createHostInputMutationTracker.ts +28 -1
  157. package/src/core/transform/tracker/createProjectMutationTracker.ts +91 -42
  158. package/src/core/transform/transformTtsc.ts +5 -1
  159. package/src/core/transform/watch/TtscWatchInputBaseline.ts +2 -1
  160. package/src/core/transform/watch/isWatchInputKeyBaseline.ts +4 -2
  161. package/src/core/transform/watch/notifyProjectRecord.ts +5 -4
  162. package/src/core/transform/watch/recordedDependencyWitnesses.ts +50 -0
  163. package/src/core/transform/watch/watchInputEvidenceMatchesBaseline.ts +1 -2
  164. package/src/core/tsconfig/TsconfigReadTransaction.ts +11 -8
  165. package/src/core/tsconfig/findDeclaredCompilerOption.ts +3 -2
  166. package/src/core/tsconfig/findDeclaredFileSpecs.ts +3 -2
  167. package/src/core/tsconfig/findDeclaredPaths.ts +3 -3
  168. package/src/core/tsconfig/findDeclaredValue.ts +2 -1
  169. package/src/core/tsconfig/readEffectiveTsconfigTemplateCompilerOptions.ts +1 -1
  170. package/src/core/tsconfig/readEffectiveTsconfigTemplateFileSpecs.ts +2 -2
  171. package/src/core/tsconfig/readProjectMembershipPolicy.ts +4 -4
@@ -12,6 +12,7 @@ import { TRANSFORM_RESULT_MEMBERSHIP } from "../cache/TRANSFORM_RESULT_MEMBERSHI
12
12
  import type { TtscCachedProjectTransform } from "../cache/TtscCachedProjectTransform";
13
13
  import { refreshFilesystemClockReference } from "../clock/refreshFilesystemClockReference";
14
14
  import { reportDivergentDelivery } from "../diagnostics/reportDivergentDelivery";
15
+ import { generationCaseSensitivity } from "../envelope/generationCaseSensitivity";
15
16
  import { selectDeclaredProjectInputKeys } from "../envelope/selectDeclaredProjectInputKeys";
16
17
  import { selectExternalInputPaths } from "../envelope/selectExternalInputPaths";
17
18
  import { selectNotifiableAbsentInputs } from "../envelope/selectNotifiableAbsentInputs";
@@ -457,6 +458,13 @@ export async function captureTransformGeneration(props: {
457
458
  clockReferenceDirectory ?? scratchDirectory,
458
459
  props.filesystem,
459
460
  );
461
+ // Every context capturing this generation below shares the generation's
462
+ // directory case answers, rather than each asking again (samchon/ttsc#1742).
463
+ const generationCase = generationCaseSensitivity({
464
+ filesystem: props.filesystem,
465
+ projectRoot,
466
+ result,
467
+ });
460
468
  const persistentHostInputs = selectPersistentHostInputs({
461
469
  filesystem: props.filesystem,
462
470
  projectRoot,
@@ -518,6 +526,7 @@ export async function captureTransformGeneration(props: {
518
526
  projectRoot,
519
527
  result,
520
528
  }),
529
+ generationCase,
521
530
  )
522
531
  : undefined;
523
532
  compileTrace?.("bridge-tracker-lifecycle", {
@@ -566,6 +575,7 @@ export async function captureTransformGeneration(props: {
566
575
  .filter((input) => !absentCandidates.has(input))
567
576
  .map((input) => [path.resolve(input), "presence" as const]),
568
577
  ),
578
+ generationCase,
569
579
  )
570
580
  : undefined;
571
581
  compileTrace?.("bridge-tracker-lifecycle", {
@@ -1,5 +1,6 @@
1
1
  import type { ITtscCompilerTransformation } from "ttsc";
2
2
 
3
+ import { generationCaseSensitivity } from "../envelope/generationCaseSensitivity";
3
4
  import { selectListedFiles } from "../envelope/selectListedFiles";
4
5
  import type { TtscTransformFilesystemOperations } from "../filesystem/TtscTransformFilesystemOperations";
5
6
  import { createHostPathIdentityContext } from "../filesystem/createHostPathIdentityContext";
@@ -43,7 +44,10 @@ export function selectPersistentHostInputs(props: {
43
44
  props.temporaryTsconfig === undefined
44
45
  )
45
46
  return inputs;
46
- const identities = createHostPathIdentityContext(props.filesystem);
47
+ const identities = createHostPathIdentityContext(
48
+ props.filesystem,
49
+ generationCaseSensitivity(props),
50
+ );
47
51
  const temporary =
48
52
  props.temporaryTsconfig === undefined
49
53
  ? undefined
@@ -69,9 +69,7 @@ export function nativeInputPredicateState(
69
69
  } else if (kind === "optional-file") {
70
70
  try {
71
71
  digest = filesystem.stat(file).isFile()
72
- ? hash(
73
- Buffer.concat([Buffer.from("file\0"), readBytes()]),
74
- )
72
+ ? hash(Buffer.concat([Buffer.from("file\0"), readBytes()]))
75
73
  : hash("missing\0");
76
74
  } catch (error) {
77
75
  if (!missing(error)) return undefined;
@@ -119,6 +119,8 @@ const registrations = new Map();
119
119
  const DROPPED = 0x1 | 0x2 | 0x4 | 0x8 | 0x20 | 0x40 | 0x80;
120
120
  const MODIFIED = 0x400 | 0x1000 | 0x2000 | 0x4000 | 0x8000;
121
121
  const RENAMED = 0x100 | 0x200 | 0x800;
122
+ const CREATED = 0x100;
123
+ const MOVED = 0x200 | 0x800;
122
124
  const fsevents = load();
123
125
  let probeSequence = 0;
124
126
 
@@ -203,6 +205,8 @@ function stream(location, deliver, registration) {
203
205
  const root = probe === undefined ? directory : probe.root;
204
206
  const within = (parent, file) => file === parent || file.startsWith(parent.endsWith("/") ? parent : parent + "/");
205
207
  const opened = { close: undefined, location: location.directory, pending: new Map(), probe, proven: probe === undefined };
208
+ // Anything born before this instant was not created in this stream.
209
+ const openedAt = Date.now();
206
210
  const stop = fsevents.watch(root, (file, flags) => {
207
211
  // Loss authority is independent of the path, including our own probe.
208
212
  if ((flags & DROPPED) !== 0) {
@@ -232,6 +236,16 @@ function stream(location, deliver, registration) {
232
236
  }
233
237
  const filename = file.slice(directory.length + 1);
234
238
  if (location.recursive !== true && filename.includes("/")) return;
239
+ // FSEvents keeps ItemCreated on later writes to a path it saw created, so a
240
+ // plain edit of a file older than this stream arrived as a rename, a
241
+ // membership change that replaced generations for an unrelated write. An
242
+ // object born before the stream opened was not created in it; its other
243
+ // flags decide. A removal or a rename keeps the event structural.
244
+ if ((flags & CREATED) !== 0 && (flags & MOVED) === 0 && (flags & MODIFIED) !== 0) {
245
+ try {
246
+ if (fs.lstatSync(file).birthtimeMs < openedAt) flags &= ~CREATED;
247
+ } catch {}
248
+ }
235
249
  deliver((flags & MODIFIED) !== 0 && (flags & RENAMED) === 0 ? "change" : "rename", filename);
236
250
  });
237
251
  opened.close = () => {
@@ -114,12 +114,21 @@ export async function createHostInputMutationTracker(
114
114
  * filesystem.
115
115
  */
116
116
  scopes?: ReadonlyMap<string, TtscTrackedInputScope>,
117
+ /**
118
+ * The case answers of the generation this tracker watches, for its baseline
119
+ * (`generationCaseSensitivity`); omitted, the baseline observes for itself.
120
+ * An event's re-check always observes afresh.
121
+ */
122
+ caseSensitive?: TtscTransformFilesystemOperations["caseSensitive"],
117
123
  ): Promise<TtscProjectMutationTracker> {
118
124
  const paths =
119
125
  (filesystem.platform ?? process.platform) === "win32"
120
126
  ? path.win32
121
127
  : path.posix;
122
- const identities = createHostPathIdentityContext(filesystem);
128
+ const identities = createHostPathIdentityContext(
129
+ filesystem,
130
+ caseSensitive ?? filesystem.caseSensitive,
131
+ );
123
132
  // The project root in both of its spellings: an input the compiler reported
124
133
  // physically is the project's as much as one spelled as the project was
125
134
  // named, and the root's observer is opened once, under the name.
@@ -399,6 +408,17 @@ export async function createHostInputMutationTracker(
399
408
  * an event the backend could not attribute to a name may concern any input
400
409
  * below the watched directory, so it always counts.
401
410
  */
411
+ // Whether a path names an existing object that no other name can reach:
412
+ // not a link, and not a file with a second hard link, whose writes could
413
+ // land on a tracked input without an event on its name.
414
+ const distinctExistingObject = (location: string): boolean => {
415
+ try {
416
+ const stats = filesystem.lstat(location);
417
+ return !stats.isSymbolicLink() && !(stats.isFile() && stats.nlink > 1n);
418
+ } catch {
419
+ return false;
420
+ }
421
+ };
402
422
  const classify = (
403
423
  directory: string,
404
424
  filename: string | null,
@@ -477,6 +497,13 @@ export async function createHostInputMutationTracker(
477
497
  return "mutation";
478
498
  }
479
499
  }
500
+ // An object that exists now resolves to its own identity under every alias,
501
+ // so one that is neither a link nor a second name of a file, and resolves
502
+ // to nothing tracked above, is a different object. A recursive backend
503
+ // (macOS, Windows) hears every write below the project root, and
504
+ // withdrawing authority for each sent every later delivery past its
505
+ // notifications.
506
+ if (distinctExistingObject(changed)) return undefined;
480
507
  // A missing native alias cannot be recovered by current realpath. Neither
481
508
  // ASCII spelling nor OS name proves this event unrelated to a tracked path.
482
509
  // Keep exact metadata validation without inventing a membership mutation.
@@ -36,11 +36,14 @@ import { watchLocationIdentity } from "./watchLocationIdentity";
36
36
  * recorded-state validation supplies the actual verdict. Content witnesses
37
37
  * preserve captured native input aliases, then reject only an observed normal
38
38
  * directory excluded in both lexical and native spellings. Unknown, missing and
39
- * linked ancestry remains conservative. Membership rejection still withdraws
40
- * notification authority even when content is outside the walk. Named events
41
- * also compare current native identity and case policy with the retained
42
- * identity transaction. Retargeting withdraws notification authority without
43
- * changing the meaning of earlier recorded event spellings.
39
+ * linked ancestry remains conservative. Membership rejection withdraws
40
+ * notification authority except below such a proven excluded directory, so a
41
+ * recursive backend's writes to a cache or output tree cost no proof, nor does
42
+ * an edit to an existing unlinked file whose every name the program cannot
43
+ * admit (samchon/ttsc#1737). Named events also compare current native identity
44
+ * and case policy with the retained identity transaction. Retargeting withdraws
45
+ * notification authority without changing the meaning of earlier recorded event
46
+ * spellings.
44
47
  *
45
48
  * @evidence contracts/common.md#principled-implementation
46
49
  * The project walk and membership policy own structural relevance; content
@@ -48,8 +51,8 @@ import { watchLocationIdentity } from "./watchLocationIdentity";
48
51
  * A lexical nonmatch is uncertainty rather than native alias exclusion.
49
52
  * Changed native identity premises also withdraw notification proof while
50
53
  * membership and content witnesses keep their independent classifications.
51
- * Content exclusion requires normal directory identity and both policy
52
- * spellings; captured native input aliases remain content witnesses.
54
+ * Content and membership exclusion require normal directory identity and both
55
+ * policy spellings; captured native input aliases remain content witnesses.
53
56
  * @evidence contracts/common.md#clear-and-simple-design
54
57
  * One root, directory index and filter set define the tracker; broker and local
55
58
  * paths share those decisions rather than duplicating backend-specific policies.
@@ -145,6 +148,73 @@ export async function createProjectMutationTracker(
145
148
  .filter((directory) => directory.relevant)
146
149
  .map((directory) => paths.resolve(directory.path)),
147
150
  );
151
+ // Whether an event path lies below a directory the walk excludes, proven by
152
+ // an observed normal directory excluded in both its lexical and its native
153
+ // spelling. A missing ancestor, one outside the root, or a link before that
154
+ // directory proves nothing. Content stops at the excluded directory, since a
155
+ // captured input reached through a deeper link is already caught by its
156
+ // identity above. Membership has no identity to catch a new file created
157
+ // through such a link (`node_modules/linked -> src`), so for it the whole
158
+ // path must be links-free.
159
+ const excludedFromWalk = (changed: string, wholePath: boolean): boolean => {
160
+ const relative = paths.relative(root, paths.dirname(changed));
161
+ if (
162
+ relative === ".." ||
163
+ relative.startsWith(".." + paths.sep) ||
164
+ paths.isAbsolute(relative)
165
+ )
166
+ return false;
167
+ let directory = root;
168
+ let excluded = false;
169
+ for (const component of relative.split(paths.sep).filter(Boolean)) {
170
+ directory = paths.join(directory, component);
171
+ try {
172
+ const before = filesystem.lstat(directory);
173
+ if (!before.isDirectory() || before.isSymbolicLink()) return false;
174
+ const physical = filesystem.realpath(directory);
175
+ const after = filesystem.lstat(directory);
176
+ if (
177
+ !after.isDirectory() ||
178
+ after.isSymbolicLink() ||
179
+ before.dev !== after.dev ||
180
+ before.ino !== after.ino
181
+ )
182
+ return false;
183
+ if (
184
+ !excluded &&
185
+ !isProjectWalkDirectory(directory, policy, filesystem.platform) &&
186
+ !isProjectWalkDirectory(physical, policy, filesystem.platform)
187
+ ) {
188
+ if (!wholePath) return true;
189
+ excluded = true;
190
+ }
191
+ } catch {
192
+ // Missing/deleted ancestors and unknown native spellings remain events.
193
+ return false;
194
+ }
195
+ }
196
+ return excluded;
197
+ };
198
+ // Whether an event names an existing file no spelling can make a program
199
+ // member: a regular file with no second hard link, whose own and physical
200
+ // names are both names the program cannot admit. A directory alias can move
201
+ // a file into the program but cannot rename it, and a removed name, a link
202
+ // or a directory stays conservative.
203
+ const existingNonProgramFile = (changed: string): boolean => {
204
+ try {
205
+ const stats = filesystem.lstat(changed);
206
+ if (!stats.isFile() || stats.nlink > 1n) return false;
207
+ return (
208
+ !isPossibleProgramFileName(paths.basename(changed), policy) &&
209
+ !isPossibleProgramFileName(
210
+ paths.basename(filesystem.realpath(changed)),
211
+ policy,
212
+ )
213
+ );
214
+ } catch {
215
+ return false;
216
+ }
217
+ };
148
218
  const reportsMembership = (location: string, filename: string): boolean => {
149
219
  const changed = paths.join(location, filename);
150
220
  const current = createHostPathIdentityContext(filesystem);
@@ -169,8 +239,19 @@ export async function createProjectMutationTracker(
169
239
  filesystem,
170
240
  );
171
241
  // Current realpath cannot identify a deleted alias. Policy checks only
172
- // lexical spellings, so rejection cannot establish unchanged program state.
173
- if (!membership) tracker.unverified = true;
242
+ // lexical spellings, so rejection cannot establish unchanged program state,
243
+ // except below a directory the walk provably excludes. A recursive backend
244
+ // (macOS, Windows) hears every write under the root, a cache or an output
245
+ // directory among them, and withdrawing authority for each sent every later
246
+ // delivery to the whole-snapshot proof. An existing file whose every name
247
+ // the program cannot admit is no member either, a stylesheet or an asset
248
+ // edited in a source directory among them, on every backend.
249
+ if (
250
+ !membership &&
251
+ !excludedFromWalk(changed, true) &&
252
+ !existingNonProgramFile(changed)
253
+ )
254
+ tracker.unverified = true;
174
255
  return membership;
175
256
  };
176
257
  // Content belongs to the walk too. A recursive native backend can report
@@ -201,39 +282,7 @@ export async function createProjectMutationTracker(
201
282
  tracker.unverified = true;
202
283
  return true;
203
284
  }
204
- const relative = paths.relative(root, paths.dirname(changed));
205
- if (
206
- relative === ".." ||
207
- relative.startsWith(".." + paths.sep) ||
208
- paths.isAbsolute(relative)
209
- )
210
- return true;
211
- let directory = root;
212
- for (const component of relative.split(paths.sep).filter(Boolean)) {
213
- directory = paths.join(directory, component);
214
- try {
215
- const before = filesystem.lstat(directory);
216
- if (!before.isDirectory() || before.isSymbolicLink()) return true;
217
- const physical = filesystem.realpath(directory);
218
- const after = filesystem.lstat(directory);
219
- if (
220
- !after.isDirectory() ||
221
- after.isSymbolicLink() ||
222
- before.dev !== after.dev ||
223
- before.ino !== after.ino
224
- )
225
- return true;
226
- if (
227
- !isProjectWalkDirectory(directory, policy, filesystem.platform) &&
228
- !isProjectWalkDirectory(physical, policy, filesystem.platform)
229
- )
230
- return false;
231
- } catch {
232
- // Missing/deleted ancestors and unknown native spellings remain events.
233
- return true;
234
- }
235
- }
236
- return true;
285
+ return !excludedFromWalk(changed, false);
237
286
  };
238
287
  const reportsNewMembership = (
239
288
  location: string,
@@ -33,6 +33,7 @@ import type { TtscTransformHooks } from "./watch/TtscTransformHooks";
33
33
  import type { TtscWatchSelection } from "./watch/TtscWatchSelection";
34
34
  import { notifyRejectedGenerationInputs } from "./watch/notifyRejectedGenerationInputs";
35
35
  import { prepareProjectRecordDirectories } from "./watch/prepareProjectRecordDirectories";
36
+ import { recordedDependencyWitnesses } from "./watch/recordedDependencyWitnesses";
36
37
 
37
38
  /**
38
39
  * Apply the ttsc plugin transform to a single source file.
@@ -282,10 +283,13 @@ export async function transformTtsc(
282
283
  cache === undefined
283
284
  ? undefined
284
285
  : TRANSFORM_CACHE_CASE_POLICIES.get(cache)?.get(key),
286
+ // A cache without a list of its own takes the names the project's
287
+ // record carries from an earlier process (samchon/ttsc#1743).
285
288
  witnessedDependencies:
286
289
  cache === undefined
287
290
  ? undefined
288
- : TRANSFORM_CACHE_DEPENDENCY_WITNESSES.get(cache)?.get(key),
291
+ : (TRANSFORM_CACHE_DEPENDENCY_WITNESSES.get(cache)?.get(key) ??
292
+ recordedDependencyWitnesses(hooks?.project, tsconfig)),
289
293
  });
290
294
  cache?.set(key, transformed);
291
295
  }
@@ -23,7 +23,8 @@ import type { TtscWatchInputFileBaseline } from "./TtscWatchInputFileBaseline";
23
23
  export interface TtscWatchInputBaseline extends TtscWatchInputFileBaseline {
24
24
  /**
25
25
  * Current version-one native codec facts, captured only for requested kinds.
26
- * These observations do not certify a producer's identityStable flag or scope.
26
+ * These observations do not certify a producer's identityStable flag or
27
+ * scope.
27
28
  */
28
29
  nativePredicates?: Partial<
29
30
  Record<
@@ -115,9 +115,11 @@ function isNativePredicateBaselines(value: unknown): boolean {
115
115
  stableStringify(Object.keys(state).sort()) !==
116
116
  stableStringify(["digest", "realpath"]) ||
117
117
  !isContentHash(state.digest) ||
118
- !(state.realpath === null ||
118
+ !(
119
+ state.realpath === null ||
119
120
  (typeof state.realpath === "string" &&
120
- isAbsoluteFilesystemPath(state.realpath)))
121
+ isAbsoluteFilesystemPath(state.realpath))
122
+ )
121
123
  ) {
122
124
  return false;
123
125
  }
@@ -73,10 +73,9 @@ const HANDED = new WeakMap<
73
73
  * loses acceptance and enters persistence again without rereading generation
74
74
  * inputs. Hosts and bridge readers own current byte proof, including concurrent
75
75
  * changes and unreadable/torn bytes; availability here cannot exclude removal
76
- * after the check. A host that keeps no
77
- * snapshot of the file, Rollup's cache, is handed the digest of the bytes
78
- * written for the generation to compare against instead
79
- * (`TtscProjectRegistration.digest`).
76
+ * after the check. A host that keeps no snapshot of the file, Rollup's cache,
77
+ * is handed the digest of the bytes written for the generation to compare
78
+ * against instead (`TtscProjectRegistration.digest`).
80
79
  *
81
80
  * A record lives below the host's tool directory, or, when that cannot be
82
81
  * written, below the fallback the host accepts (`fallbackToolDirectory`,
@@ -226,7 +225,9 @@ function recordOf(
226
225
  cached: TtscCachedProjectTransform,
227
226
  inputs: readonly TtscWatchInput[],
228
227
  ): TtscProjectRecord {
228
+ const dependencies = cached.externalDependencyInputs ?? [];
229
229
  const record: TtscProjectRecord = {
230
+ ...(dependencies.length === 0 ? {} : { dependencies }),
230
231
  inputs: {},
231
232
  membership:
232
233
  cached.projectDirectories === undefined
@@ -0,0 +1,50 @@
1
+ import { projectRecordFile } from "../../bridge/projectRecordFile";
2
+ import { readProjectRecordFile } from "../../bridge/readProjectRecordFile";
3
+ import type { TtscTransformHooks } from "./TtscTransformHooks";
4
+
5
+ /**
6
+ * The dependency-only paths the project record of `tsconfig` carries
7
+ * (`TtscProjectRecord.dependencies`), for a transform cache that has no witness
8
+ * list of its own yet.
9
+ *
10
+ * A compile learns a plugin-reported dependency path only from its own
11
+ * envelope, and one it had no witness for compiles again
12
+ * (`onlyLearnedCompileFacts`). A transform cache keeps the paths in memory
13
+ * (`TRANSFORM_CACHE_DEPENDENCY_WITNESSES`), so every new cache, a dev-server
14
+ * start or a build process, compiled such a project twice (samchon/ttsc#1743).
15
+ * The record the previous process wrote names them. They only say what to read:
16
+ * the capture still witnesses each path afresh, a path the compile no longer
17
+ * reports costs one read, and one the record does not name is retried as
18
+ * before.
19
+ *
20
+ * @param project The host's record directories, when it keeps records.
21
+ * @param tsconfig The project's tsconfig, as the adapter names it.
22
+ * @returns The record's paths, or `undefined` when no readable record names
23
+ * any.
24
+ * @evidence contracts/common.md#principled-implementation The record is the host's persisted project state, written from a completed generation; its paths are hints that the capture re-witnesses, never certified state.
25
+ * @evidence contracts/common.md#clear-and-simple-design One reader selects the first readable record of the host's directories, in the order the writer tries them.
26
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts No witness or digest is taken from the record; an unreadable or absent record supplies nothing.
27
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain the doubled first compile, the hint semantics and the absent cases.
28
+ * @evidence contracts/portability.md#os-neutral-implementation Record paths come from the host's own directories through `projectRecordFile`.
29
+ * @evidence contracts/performance.md#efficient-algorithms At most two record reads and JSON parses, only when a cache has no witness list for the key.
30
+ * @evidence contracts/performance.md#reuse-equivalent-work A new process reuses the dependency names the last generation's record holds instead of compiling once to learn them.
31
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The returned list belongs to the caller; nothing is retained here.
32
+ */
33
+ export function recordedDependencyWitnesses(
34
+ project: TtscTransformHooks["project"],
35
+ tsconfig: string,
36
+ ): readonly string[] | undefined {
37
+ if (project === undefined) return undefined;
38
+ for (const directory of [
39
+ project.toolDirectory,
40
+ ...(project.fallbackToolDirectory === undefined
41
+ ? []
42
+ : [project.fallbackToolDirectory]),
43
+ ]) {
44
+ const record = readProjectRecordFile(
45
+ projectRecordFile(directory, tsconfig),
46
+ );
47
+ if (record !== undefined) return record.dependencies;
48
+ }
49
+ return undefined;
50
+ }
@@ -10,8 +10,7 @@ import { isWatchInputKeyBaseline } from "./isWatchInputKeyBaseline";
10
10
  *
11
11
  * A codec matches only the facts the baseline actually captured. Native,
12
12
  * listing and plugin-tree observations require their optional payloads;
13
- * membership is a
14
- * separate project walk and cannot match one path's baseline.
13
+ * membership is a separate project walk and cannot match one path's baseline.
15
14
  *
16
15
  * @evidence contracts/common.md#principled-implementation Identity and every recorded codec fact must agree with a validated baseline; native predicates additionally require a supported version, stable producer identity, matching raw digest and physical target. Narrow discovery facts and absent optional native/listing/tree payloads cannot establish broader observations.
17
16
  * @evidence contracts/common.md#clear-and-simple-design Codec branches reuse the strict baseline guard and native realpath comparator, with one private refinement exposing the broader carrier.
@@ -7,14 +7,16 @@ import { resolveExtendsConfig } from "./resolveExtendsConfig";
7
7
  import { resolveRealPath } from "./resolveRealPath";
8
8
 
9
9
  /**
10
- * Share config observations and contextual selections within one synchronous read.
10
+ * Share config observations and contextual selections within one synchronous
11
+ * read.
11
12
  *
12
13
  * Lexical paths own parsing and relative extends resolution. Physical paths
13
14
  * only guard ancestry cycles. A completed selection can be borrowed when the
14
15
  * current ancestors intersect its encountered physical identities exactly as
15
16
  * they did originally; otherwise a different cycle cut requires evaluation.
16
17
  * Ordered source observations travel with a completed selection, including
17
- * unresolved file candidates, so reuse preserves the caller's invalidation inputs.
18
+ * unresolved file candidates, so reuse preserves the caller's invalidation
19
+ * inputs.
18
20
  *
19
21
  * Create a new transaction for each independent freshness observation. This
20
22
  * owner neither validates reuse across reads nor supplies compiler diagnostics.
@@ -94,12 +96,13 @@ export class TsconfigReadTransaction {
94
96
  /**
95
97
  * Select one declaration, preserving its lexical directory and observations.
96
98
  *
97
- * An undefined own selection inherits. Null masks inheritance without supplying
98
- * a value, as an invalid own file list does; a parent ignores that empty result.
99
- * First queries search later bases first; last queries visit all bases forward.
100
- * The caller's physical ancestor set is copied once and never mutated.
101
- * Selectors must preserve the parsed inputs and give the same declaration for
102
- * the same source; query reuse does not repeat their incidental effects.
99
+ * An undefined own selection inherits. Null masks inheritance without
100
+ * supplying a value, as an invalid own file list does; a parent ignores that
101
+ * empty result. First queries search later bases first; last queries visit
102
+ * all bases forward. The caller's physical ancestor set is copied once and
103
+ * never mutated. Selectors must preserve the parsed inputs and give the same
104
+ * declaration for the same source; query reuse does not repeat their
105
+ * incidental effects.
103
106
  *
104
107
  * @evidence contracts/common.md#principled-implementation A completed answer is reusable only when its encountered physical identities have the same intersection with current ancestry. That preserves every observed cycle decision, including cuts; own declarations and first/last traversal retain lexical origins and key-specific precedence.
105
108
  * @evidence contracts/common.md#clear-and-simple-design A query-local visitor owns selection and contextual completed answers; transaction nodes separately share lexical source, identity and edge observations across keys.
@@ -6,8 +6,9 @@ import { findDeclaredValue } from "./findDeclaredValue";
6
6
  *
7
7
  * Own key presence is significant even for null or undefined values; the caller
8
8
  * decides whether the selected option value is usable. An optional
9
- * decoded-source map or graph transaction belongs to one caller read; independent
10
- * option searches retain their selectors and physical ancestry contexts.
9
+ * decoded-source map or graph transaction belongs to one caller read;
10
+ * independent option searches retain their selectors and physical ancestry
11
+ * contexts.
11
12
  *
12
13
  * @evidence contracts/common.md#principled-implementation
13
14
  * A wrapper preserves key presence through the generic selector, so the own
@@ -13,8 +13,9 @@ import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
13
13
  * anchored at the named declaring config, including independent link aliases.
14
14
  *
15
15
  * @returns The list and its directory, undefined when no array is selected, or
16
- * null when the root config cannot be read as an object. A supplied transaction
17
- * belongs to this freshness observation; later reads require a fresh owner.
16
+ * null when the root config cannot be read as an object. A supplied
17
+ * transaction belongs to this freshness observation; later reads require a
18
+ * fresh owner.
18
19
  * @evidence contracts/common.md#principled-implementation
19
20
  * Own invalid values stop inheritance at that node. Forward inherited
20
21
  * traversal keeps the last array and ignores empty selections, preserving
@@ -1,5 +1,5 @@
1
- import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
2
1
  import type { IDeclaredPaths } from "./IDeclaredPaths";
2
+ import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
3
3
  import { findDeclaredValue } from "./findDeclaredValue";
4
4
 
5
5
  /**
@@ -7,8 +7,8 @@ import { findDeclaredValue } from "./findDeclaredValue";
7
7
  * rooted at `tsconfig`. The own config wins over its bases; within an `extends`
8
8
  * array, later entries win over earlier ones. `seen` breaks circular chains;
9
9
  * the compiler reports the actual config error. Optional decoded sources or
10
- * graph observations share only within the caller's current read; physical cycle
11
- * guards remain branch-local.
10
+ * graph observations share only within the caller's current read; physical
11
+ * cycle guards remain branch-local.
12
12
  *
13
13
  * @evidence contracts/common.md#principled-implementation
14
14
  * The selector accepts a non-array paths record and the shared value reader
@@ -7,7 +7,8 @@ import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
7
7
  * remain anchored at the named config; physical identity only cuts branch
8
8
  * cycles. Independent aliases can therefore resolve different relative bases.
9
9
  * Missing or malformed configs remain best-effort absence; the compiler owns
10
- * configuration diagnostics. Unresolved file candidates remain observed inputs.
10
+ * configuration diagnostics. Unresolved file candidates remain observed
11
+ * inputs.
11
12
  *
12
13
  * A supplied transaction shares source, identity and edge observations across
13
14
  * keys. A decoded-source map remains supported for callers that own that map;
@@ -1,8 +1,8 @@
1
1
  import path from "node:path";
2
2
 
3
- import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
4
3
  import { CONFIG_DIR_TEMPLATE_LIST_OPTIONS } from "./CONFIG_DIR_TEMPLATE_LIST_OPTIONS";
5
4
  import { CONFIG_DIR_TEMPLATE_SCALAR_OPTIONS } from "./CONFIG_DIR_TEMPLATE_SCALAR_OPTIONS";
5
+ import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
6
6
  import { absolutizePathsTarget } from "./absolutizePathsTarget";
7
7
  import { findDeclaredCompilerOption } from "./findDeclaredCompilerOption";
8
8
  import { findDeclaredPaths } from "./findDeclaredPaths";
@@ -15,8 +15,8 @@ const CONFIG_DIR_TEMPLATE_FILE_SPECS = ["exclude", "files", "include"] as const;
15
15
  * Lists use the compiler's last-inherited-array rule: a later base declaring
16
16
  * null cannot erase an earlier array. Invalid elements are preserved so this
17
17
  * overlay does not hide the compiler's configuration diagnostics. The three
18
- * list searches share graph observations only within this call; a later call reads
19
- * current bytes in a fresh transaction.
18
+ * list searches share graph observations only within this call; a later call
19
+ * reads current bytes in a fresh transaction.
20
20
  *
21
21
  * @param configDir The directory `${configDir}` stands for, as
22
22
  * `readEffectiveTsconfigTemplateCompilerOptions` takes it.
@@ -2,9 +2,9 @@ import path from "node:path";
2
2
 
3
3
  import { TYPESCRIPT_TRANSFORM_EXTENSIONS } from "../source/TYPESCRIPT_TRANSFORM_EXTENSIONS";
4
4
  import type { ITtscProjectMembershipPolicy } from "./ITtscProjectMembershipPolicy";
5
- import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
6
5
  import { JAVASCRIPT_INPUT_EXTENSIONS } from "./JAVASCRIPT_INPUT_EXTENSIONS";
7
6
  import { OUTPUT_DIRECTORY_OPTIONS } from "./OUTPUT_DIRECTORY_OPTIONS";
7
+ import { TsconfigReadTransaction } from "./TsconfigReadTransaction";
8
8
  import { absolutizePathsTarget } from "./absolutizePathsTarget";
9
9
  import { findDeclaredFileSpecs } from "./findDeclaredFileSpecs";
10
10
  import { findDeclaredValue } from "./findDeclaredValue";
@@ -40,9 +40,9 @@ const DEFAULT_INCLUDE_SPEC = "**/*";
40
40
  * refuses to guess in the direction that loses correctness.
41
41
  *
42
42
  * Source, identity and extends observations share this transaction. Completed
43
- * selections, including absence, require matching physical ancestry intersections.
44
- * Later calls start fresh, so unchanged metadata cannot conceal changed source
45
- * text or resolution.
43
+ * selections, including absence, require matching physical ancestry
44
+ * intersections. Later calls start fresh, so unchanged metadata cannot conceal
45
+ * changed source text or resolution.
46
46
  *
47
47
  * @evidence contracts/common.md#principled-implementation
48
48
  * Config-derived root specs, allowed extensions and exclusion provenance