ttsc 0.31.0 → 0.31.2

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 (155) hide show
  1. package/cmd/platform/main.go +2 -2
  2. package/internal/sourceprocess/contain_windows.go +10 -13
  3. package/internal/sourceprocess/started_windows.go +41 -0
  4. package/internal/sourceprocess/windows_started_publication_distinguishes_pending_from_refusal_test.go +76 -0
  5. package/internal/watchhelper/run.go +30 -20
  6. package/internal/watchhelper/run_other.go +4 -4
  7. package/internal/watchhelper/run_windows.go +471 -0
  8. package/internal/watchhelper/watchhelper.go +8 -2
  9. package/internal/watchhelper/windows_notifications_preserve_mutations_without_access_noise_test.go +506 -0
  10. package/lib/compiler/internal/build/BuildExecution.d.ts +4 -2
  11. package/lib/compiler/internal/build/BuildExecution.js +40 -12
  12. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  13. package/lib/compiler/internal/startResidentTransform.js +5 -4
  14. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  15. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  16. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  17. package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
  18. package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
  19. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  20. package/lib/internal/nativeBinary.d.ts +3 -3
  21. package/lib/internal/nativeBinary.js +3 -3
  22. package/lib/internal/pluginSource.d.ts +1 -0
  23. package/lib/internal/pluginSource.js +1 -0
  24. package/lib/internal/pluginSource.js.map +1 -1
  25. package/lib/internal/resolveNodeBinary.d.ts +6 -1
  26. package/lib/internal/resolveNodeBinary.js +6 -2
  27. package/lib/internal/resolveNodeBinary.js.map +1 -1
  28. package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
  29. package/lib/internal/runtimeExecutableIdentity.js +192 -102
  30. package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
  31. package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
  32. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  33. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
  34. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
  35. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
  36. package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
  37. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  38. package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
  39. package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
  40. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  41. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
  42. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
  43. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  44. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +24 -14
  45. package/lib/plugin/internal/load/loadProjectPlugins.js +141 -56
  46. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  47. package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
  48. package/lib/plugin/internal/readCapabilityResolution.js +8 -5
  49. package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
  50. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +48 -0
  51. package/lib/plugin/internal/source/GoEnvironmentReading.js +156 -0
  52. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -0
  53. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +51 -13
  54. package/lib/plugin/internal/source/NativeSourcePackages.js +182 -27
  55. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  56. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +42 -3
  57. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +68 -7
  58. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  59. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.d.ts +5 -3
  60. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js +14 -8
  61. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js.map +1 -1
  62. package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
  63. package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
  64. package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
  65. package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
  66. package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
  67. package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
  68. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
  69. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
  70. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  71. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +53 -19
  72. package/lib/plugin/internal/source/SourcePluginWorkspace.js +111 -22
  73. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  74. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +34 -18
  75. package/lib/plugin/internal/source/buildSourcePlugin.js +52 -24
  76. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  77. package/lib/plugin/internal/source/computeCacheKey.d.ts +25 -7
  78. package/lib/plugin/internal/source/computeCacheKey.js +85 -34
  79. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  80. package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
  81. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +75 -102
  82. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  83. package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
  84. package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
  85. package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
  86. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
  87. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
  88. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
  89. package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
  90. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  91. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +6 -5
  92. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +10 -17
  93. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  94. package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
  95. package/lib/plugin/internal/source/pluginSourceState.js +20 -8
  96. package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
  97. package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
  98. package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
  99. package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
  100. package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
  101. package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
  102. package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
  103. package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
  104. package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
  105. package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
  106. package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
  107. package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
  108. package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
  109. package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
  110. package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
  111. package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
  112. package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
  113. package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
  114. package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
  115. package/lib/plugin/resolveCapabilityPlugins.js +67 -15
  116. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
  117. package/package.json +8 -8
  118. package/src/compiler/internal/build/BuildExecution.ts +46 -13
  119. package/src/compiler/internal/startResidentTransform.ts +12 -3
  120. package/src/compiler/internal/transformProjectInMemory.ts +10 -2
  121. package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
  122. package/src/internal/nativeBinary.ts +3 -3
  123. package/src/internal/pluginSource.ts +1 -0
  124. package/src/internal/resolveNodeBinary.ts +12 -1
  125. package/src/internal/runtimeExecutableIdentity.ts +227 -141
  126. package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
  127. package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
  128. package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
  129. package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
  130. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
  131. package/src/plugin/internal/load/loadProjectPlugins.ts +421 -211
  132. package/src/plugin/internal/readCapabilityResolution.ts +17 -6
  133. package/src/plugin/internal/source/GoEnvironmentReading.ts +178 -0
  134. package/src/plugin/internal/source/NativeSourcePackages.ts +365 -48
  135. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +83 -5
  136. package/src/plugin/internal/source/PluginBuildEnvironmentWitness.ts +18 -8
  137. package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
  138. package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
  139. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
  140. package/src/plugin/internal/source/SourcePluginWorkspace.ts +148 -25
  141. package/src/plugin/internal/source/buildSourcePlugin.ts +104 -36
  142. package/src/plugin/internal/source/computeCacheKey.ts +129 -55
  143. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +152 -105
  144. package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
  145. package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
  146. package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
  147. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +13 -25
  148. package/src/plugin/internal/source/pluginSourceState.ts +28 -12
  149. package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
  150. package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
  151. package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
  152. package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
  153. package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
  154. package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
  155. package/src/plugin/resolveCapabilityPlugins.ts +87 -13
@@ -3,64 +3,44 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { threadId } from "node:worker_threads";
5
5
 
6
+ import { PluginContentIdentities } from "../plugin/internal/source/PluginContentIdentities";
6
7
  import { E2ETrace } from "./E2ETrace";
7
8
 
8
9
  /**
9
10
  * Fingerprint an absolute executable's spelling, target and actual bytes.
10
11
  *
11
12
  * A changed link, file, or read failure yields no reusable identity. Metadata
12
- * brackets the streamed read; it is not used to reuse an earlier content hash.
13
- * Concurrent writes that evade every filesystem observation remain outside this
14
- * snapshot guarantee.
13
+ * brackets the streamed read. Concurrent writes that evade every filesystem
14
+ * observation remain outside this snapshot guarantee.
15
15
  *
16
- * @evidence contracts/common.md#principled-implementation The identity joins lexical-link metadata, the selected physical file metadata and SHA-256 of that opened file; matching metadata alone cannot authorize reuse after bytes change. Descriptor and pathname observations must still agree after reading.
17
- * @evidence contracts/common.md#clear-and-simple-design This one native owner supplies the same content proof to runtime capability and plugin caches; callers decide whether their other inputs permit reuse.
18
- * @evidence contracts/common.md#prohibited-implementation-shortcuts Supported Node filesystem and crypto APIs inspect the actual candidate; no fixture identity, foreign mutation or metadata-only shortcut substitutes for content.
19
- * @evidence contracts/common.md#meaningful-documentation Native prose explains absolute-path scope, failure meaning, hash freshness and the remaining concurrent-mutation limitation, with separate prose and tags.
20
- * @evidence contracts/portability.md#os-neutral-implementation Node native realpath and bigint stat preserve link/target identity on supported hosts; absolute paths use native platform semantics, and unreadable or non-regular targets cannot be reused.
21
- * @evidence contracts/performance.md#efficient-algorithms A sequential read hashes B bytes with one 64 KiB buffer instead of a complete executable allocation. Native path/realpath/stat observations and serialized physical spelling/metadata add text/lookup costs; B and spelling lengths have no quota here. Returned identity storage includes the physical path as well as the fixed-length digest.
22
- *
23
- * @evidenceExclude contracts/performance.md#reuse-equivalent-work This operation establishes whether later work is equivalent; retaining a digest by metadata would reproduce the stale-byte defect it must prevent.
16
+ * With a record store, the bytes of an unchanged executable are proven from its
17
+ * metadata instead of streamed again (`PluginContentIdentities`, #1723): the
18
+ * recorded digest stands only while the physical file's identity, size and both
19
+ * stamps match the record and its modification stamp is separable from a
20
+ * reference the store minted. A plugin load asks for this identity several
21
+ * times, and every launch is a new process, so streaming an 85 MB runtime each
22
+ * time cost about 0.9 s per launch. Without a store, or when that proof is
23
+ * unavailable, the file is streamed as before.
24
24
  *
25
- * @evidence contracts/performance.md#bound-retention-and-release-resources One descriptor and one fixed buffer belong to this synchronous call; finally attempts closure after read/observation failures as well as success. A close failure returns no identity but cannot confirm native release. Returned path/metadata/digest text transfers to the caller and no helper history survives. Opt-in opened/closed/close-failed observations carry one trace-only call id and actual descriptor, PID/thread and already observed physical identity. The existing finally performs the single close; no observer retries it or converts an unavailable identity to success. An observed close is release of this Node descriptor, not proof that an OS image lock or another actor has released the executable.
25
+ * @param runtime The executable, absolute.
26
+ * @param identities Optional record store of the caller's plugin cache.
27
+ * @evidence contracts/common.md#principled-implementation The identity joins lexical-link metadata, the selected physical file metadata and the SHA-256 of that file's bytes, streamed under metadata brackets or proven by a record whose separable signature still matches; descriptor and pathname observations must still agree after a read.
28
+ * @evidence contracts/common.md#clear-and-simple-design This one native owner supplies the same identity to runtime capability and plugin caches; the record store owns metadata reuse and a private helper owns the bracketed stream.
29
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Metadata alone never stands for bytes: reuse needs a record written under unchanged separable metadata around an actual read, which a same-tick rewrite or restored timestamps cannot satisfy because change time and the minted reference move.
30
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains absolute-path scope, failure meaning, the record proof and the remaining concurrent-mutation limitation, with separate prose and tags.
31
+ * @evidence contracts/portability.md#os-neutral-implementation Node native realpath and bigint stat preserve link/target identity on supported hosts; absolute paths use native platform semantics, and unreadable or non-regular targets cannot be reused.
32
+ * @evidence contracts/performance.md#efficient-algorithms A proven record costs three stats and one small file read; otherwise a sequential read hashes B bytes with one 64 KiB buffer instead of a complete executable allocation. Native path/realpath/stat observations and serialized metadata add text/lookup costs.
33
+ * @evidence contracts/performance.md#reuse-equivalent-work Processes share an executable digest only through the record store's separable-signature proof; without a store every call streams the file.
34
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One descriptor and one fixed buffer belong to a streamed read; finally attempts closure after read/observation failures as well as success. A close failure returns no identity but cannot confirm native release. Opt-in opened/closed/close-failed observations carry one trace-only call id and actual descriptor, PID/thread and already observed physical identity. Records belong to the single-file collector.
26
35
  */
27
- export function runtimeExecutableIdentity(runtime: string): string | undefined {
36
+ export function runtimeExecutableIdentity(
37
+ runtime: string,
38
+ identities?: PluginContentIdentities.Store,
39
+ ): string | undefined {
28
40
  if (!path.isAbsolute(runtime)) return undefined;
29
41
  const startedAt = new Date().toISOString();
30
- let descriptor: number | undefined;
31
- let traceLease: Readonly<Record<string, string | number>> | undefined;
32
42
  let stage = "lexical-stat";
33
- const unavailable = (
34
- reason: string,
35
- expected?: string,
36
- observed?: string,
37
- observations?: { before: fs.BigIntStats; after: fs.BigIntStats },
38
- ) => {
39
- E2ETrace.capabilityResolution("runtime-executable-identity-unavailable", {
40
- runtime,
41
- stage,
42
- reason,
43
- expected,
44
- observed,
45
- expectedAtimeNs:
46
- observations === undefined
47
- ? undefined
48
- : String(observations.before.atimeNs),
49
- observedAtimeNs:
50
- observations === undefined
51
- ? undefined
52
- : String(observations.after.atimeNs),
53
- expectedBirthtimeNs:
54
- observations === undefined
55
- ? undefined
56
- : String(observations.before.birthtimeNs),
57
- observedBirthtimeNs:
58
- observations === undefined
59
- ? undefined
60
- : String(observations.after.birthtimeNs),
61
- });
62
- return undefined;
63
- };
43
+ let refused: IUnavailable | undefined;
64
44
  try {
65
45
  const lexical = fs.lstatSync(runtime, { bigint: true });
66
46
  stage = "physical-realpath";
@@ -68,93 +48,30 @@ export function runtimeExecutableIdentity(runtime: string): string | undefined {
68
48
  stage = "physical-stat";
69
49
  const physical = fs.statSync(physicalPath, { bigint: true });
70
50
  if (!physical.isFile())
71
- return unavailable("not-regular", undefined, fileIdentity(physical));
72
- stage = "open";
73
- descriptor = fs.openSync(physicalPath, "r");
74
- if (process.env.TTSC_E2E_TRACE) {
75
- traceLease = {
76
- callId: String(++identityTraceOrdinal),
77
- runtime,
78
- physicalPath,
79
- descriptor,
80
- pid: process.pid,
81
- threadId,
82
- dev: String(physical.dev),
83
- ino: String(physical.ino),
84
- };
85
- E2ETrace.capabilityResolution(
86
- "runtime-executable-identity-opened",
87
- traceLease,
88
- );
89
- }
90
- stage = "opened-stat";
91
- const opened = fs.fstatSync(descriptor, { bigint: true });
92
- if (fileIdentity(opened) !== fileIdentity(physical))
93
- return unavailable(
94
- "opened-file-changed",
95
- fileIdentity(physical),
96
- fileIdentity(opened),
97
- );
98
- const hash = crypto.createHash("sha256");
99
- const buffer = Buffer.allocUnsafe(64 * 1024);
100
- let remaining = opened.size;
101
- stage = "read";
102
- while (remaining > 0n) {
103
- const requested =
104
- remaining > BigInt(buffer.length) ? buffer.length : Number(remaining);
105
- const length = fs.readSync(descriptor, buffer, 0, requested, null);
106
- if (length === 0)
107
- return unavailable(
108
- "premature-eof",
109
- String(opened.size),
110
- String(opened.size - remaining),
111
- );
112
- hash.update(buffer.subarray(0, length));
113
- remaining -= BigInt(length);
114
- }
115
- stage = "post-read-opened-stat";
116
- const afterOpened = fs.fstatSync(descriptor, { bigint: true });
117
- if (fileIdentity(afterOpened) !== fileIdentity(opened))
118
- return unavailable(
119
- "opened-file-changed-during-read",
120
- fileIdentity(opened),
121
- fileIdentity(afterOpened),
122
- { before: opened, after: afterOpened },
123
- );
124
- stage = "post-read-realpath";
125
- const afterPath = fs.realpathSync.native(runtime);
126
- if (afterPath !== physicalPath)
127
- return unavailable("physical-target-changed", physicalPath, afterPath);
128
- stage = "post-read-lexical-stat";
129
- const afterLexical = fs.lstatSync(runtime, { bigint: true });
130
- if (fileIdentity(afterLexical) !== fileIdentity(lexical))
131
- return unavailable(
132
- "lexical-file-changed-during-read",
133
- fileIdentity(lexical),
134
- fileIdentity(afterLexical),
135
- );
136
- stage = "post-read-physical-stat";
137
- const afterPhysical = fs.statSync(physicalPath, { bigint: true });
138
- if (fileIdentity(afterPhysical) !== fileIdentity(physical))
139
- return unavailable(
140
- "physical-file-changed-during-read",
141
- fileIdentity(physical),
142
- fileIdentity(afterPhysical),
143
- );
144
- const digest = hash.digest("hex");
145
- E2ETrace.capabilityResolution("runtime-executable-identity-observed", {
146
- runtime,
147
- physicalPath,
148
- startedAt,
149
- finishedAt: new Date().toISOString(),
150
- lexical: fileIdentity(lexical),
151
- physical: fileIdentity(physical),
152
- digest,
153
- openedAtimeNs: String(opened.atimeNs),
154
- afterOpenedAtimeNs: String(afterOpened.atimeNs),
155
- openedBirthtimeNs: String(opened.birthtimeNs),
156
- afterOpenedBirthtimeNs: String(afterOpened.birthtimeNs),
157
- });
51
+ return unavailable(runtime, {
52
+ stage,
53
+ reason: "not-regular",
54
+ observed: fileIdentity(physical),
55
+ });
56
+ const read = (): string => {
57
+ const streamed = streamDigest(runtime, physicalPath, physical, lexical, {
58
+ startedAt,
59
+ });
60
+ if (typeof streamed === "string") return streamed;
61
+ refused = streamed;
62
+ throw new Error(streamed.reason);
63
+ };
64
+ const digest =
65
+ identities === undefined
66
+ ? read()
67
+ : PluginContentIdentities.digest(
68
+ identities,
69
+ "executable",
70
+ physicalPath,
71
+ (references) =>
72
+ PluginContentIdentities.fileObservation(references, physicalPath),
73
+ read,
74
+ );
158
75
  return [
159
76
  physicalPath,
160
77
  fileIdentity(lexical),
@@ -163,10 +80,71 @@ export function runtimeExecutableIdentity(runtime: string): string | undefined {
163
80
  ].join("\0");
164
81
  } catch (error) {
165
82
  return unavailable(
166
- "filesystem-error",
167
- undefined,
168
- error instanceof Error ? error.message : String(error),
83
+ runtime,
84
+ refused ?? {
85
+ stage,
86
+ reason: "filesystem-error",
87
+ observed: error instanceof Error ? error.message : String(error),
88
+ },
169
89
  );
90
+ }
91
+ }
92
+
93
+ /** Why an identity could not be formed, at the stage that found it. */
94
+ interface IUnavailable {
95
+ stage: string;
96
+ reason: string;
97
+ expected?: string;
98
+ observed?: string;
99
+ observations?: { before: fs.BigIntStats; after: fs.BigIntStats };
100
+ }
101
+
102
+ function unavailable(runtime: string, failure: IUnavailable): undefined {
103
+ E2ETrace.capabilityResolution("runtime-executable-identity-unavailable", {
104
+ runtime,
105
+ stage: failure.stage,
106
+ reason: failure.reason,
107
+ expected: failure.expected,
108
+ observed: failure.observed,
109
+ expectedAtimeNs:
110
+ failure.observations === undefined
111
+ ? undefined
112
+ : String(failure.observations.before.atimeNs),
113
+ observedAtimeNs:
114
+ failure.observations === undefined
115
+ ? undefined
116
+ : String(failure.observations.after.atimeNs),
117
+ expectedBirthtimeNs:
118
+ failure.observations === undefined
119
+ ? undefined
120
+ : String(failure.observations.before.birthtimeNs),
121
+ observedBirthtimeNs:
122
+ failure.observations === undefined
123
+ ? undefined
124
+ : String(failure.observations.after.birthtimeNs),
125
+ });
126
+ return undefined;
127
+ }
128
+
129
+ /**
130
+ * Stream the physical file's bytes between metadata observations of the open
131
+ * descriptor, the physical path and the lexical spelling, returning the digest
132
+ * or what moved. The descriptor is closed in finally; a failed close refuses
133
+ * the identity.
134
+ */
135
+ function streamDigest(
136
+ runtime: string,
137
+ physicalPath: string,
138
+ physical: fs.BigIntStats,
139
+ lexical: fs.BigIntStats,
140
+ timing: { startedAt: string },
141
+ ): string | IUnavailable {
142
+ let stage = "open";
143
+ let descriptor: number | undefined;
144
+ let traceLease: Readonly<Record<string, string | number>> | undefined;
145
+ let result: string | IUnavailable;
146
+ try {
147
+ result = observe();
170
148
  } finally {
171
149
  if (descriptor !== undefined) {
172
150
  try {
@@ -185,13 +163,121 @@ export function runtimeExecutableIdentity(runtime: string): string | undefined {
185
163
  error: error instanceof Error ? error.message : String(error),
186
164
  },
187
165
  );
188
- stage = "close";
189
- return unavailable(
190
- "descriptor-close-error",
191
- undefined,
192
- error instanceof Error ? error.message : String(error),
166
+ // A digest whose descriptor could not be closed is refused, as before.
167
+ result = {
168
+ stage: "close",
169
+ reason: "descriptor-close-error",
170
+ observed: error instanceof Error ? error.message : String(error),
171
+ };
172
+ }
173
+ }
174
+ }
175
+ return result;
176
+
177
+ function observe(): string | IUnavailable {
178
+ try {
179
+ descriptor = fs.openSync(physicalPath, "r");
180
+ if (process.env.TTSC_E2E_TRACE) {
181
+ traceLease = {
182
+ callId: String(++identityTraceOrdinal),
183
+ runtime,
184
+ physicalPath,
185
+ descriptor,
186
+ pid: process.pid,
187
+ threadId,
188
+ dev: String(physical.dev),
189
+ ino: String(physical.ino),
190
+ };
191
+ E2ETrace.capabilityResolution(
192
+ "runtime-executable-identity-opened",
193
+ traceLease,
193
194
  );
194
195
  }
196
+ stage = "opened-stat";
197
+ const opened = fs.fstatSync(descriptor, { bigint: true });
198
+ if (fileIdentity(opened) !== fileIdentity(physical))
199
+ return {
200
+ stage,
201
+ reason: "opened-file-changed",
202
+ expected: fileIdentity(physical),
203
+ observed: fileIdentity(opened),
204
+ };
205
+ const hash = crypto.createHash("sha256");
206
+ const buffer = Buffer.allocUnsafe(64 * 1024);
207
+ let remaining = opened.size;
208
+ stage = "read";
209
+ while (remaining > 0n) {
210
+ const requested =
211
+ remaining > BigInt(buffer.length) ? buffer.length : Number(remaining);
212
+ const length = fs.readSync(descriptor, buffer, 0, requested, null);
213
+ if (length === 0)
214
+ return {
215
+ stage,
216
+ reason: "premature-eof",
217
+ expected: String(opened.size),
218
+ observed: String(opened.size - remaining),
219
+ };
220
+ hash.update(buffer.subarray(0, length));
221
+ remaining -= BigInt(length);
222
+ }
223
+ stage = "post-read-opened-stat";
224
+ const afterOpened = fs.fstatSync(descriptor, { bigint: true });
225
+ if (fileIdentity(afterOpened) !== fileIdentity(opened))
226
+ return {
227
+ stage,
228
+ reason: "opened-file-changed-during-read",
229
+ expected: fileIdentity(opened),
230
+ observed: fileIdentity(afterOpened),
231
+ observations: { before: opened, after: afterOpened },
232
+ };
233
+ stage = "post-read-realpath";
234
+ const afterPath = fs.realpathSync.native(runtime);
235
+ if (afterPath !== physicalPath)
236
+ return {
237
+ stage,
238
+ reason: "physical-target-changed",
239
+ expected: physicalPath,
240
+ observed: afterPath,
241
+ };
242
+ stage = "post-read-lexical-stat";
243
+ const afterLexical = fs.lstatSync(runtime, { bigint: true });
244
+ if (fileIdentity(afterLexical) !== fileIdentity(lexical))
245
+ return {
246
+ stage,
247
+ reason: "lexical-file-changed-during-read",
248
+ expected: fileIdentity(lexical),
249
+ observed: fileIdentity(afterLexical),
250
+ };
251
+ stage = "post-read-physical-stat";
252
+ const afterPhysical = fs.statSync(physicalPath, { bigint: true });
253
+ if (fileIdentity(afterPhysical) !== fileIdentity(physical))
254
+ return {
255
+ stage,
256
+ reason: "physical-file-changed-during-read",
257
+ expected: fileIdentity(physical),
258
+ observed: fileIdentity(afterPhysical),
259
+ };
260
+ const digest = hash.digest("hex");
261
+ E2ETrace.capabilityResolution("runtime-executable-identity-observed", {
262
+ runtime,
263
+ physicalPath,
264
+ startedAt: timing.startedAt,
265
+ finishedAt: new Date().toISOString(),
266
+ lexical: fileIdentity(lexical),
267
+ physical: fileIdentity(physical),
268
+ digest,
269
+ openedAtimeNs: String(opened.atimeNs),
270
+ afterOpenedAtimeNs: String(afterOpened.atimeNs),
271
+ openedBirthtimeNs: String(opened.birthtimeNs),
272
+ afterOpenedBirthtimeNs: String(afterOpened.birthtimeNs),
273
+ });
274
+ return digest;
275
+ } catch (error) {
276
+ return {
277
+ stage,
278
+ reason: "filesystem-error",
279
+ observed: error instanceof Error ? error.message : String(error),
280
+ };
195
281
  }
196
282
  }
197
283
  }
@@ -15,11 +15,13 @@ import { runBuild } from "../../../compiler/internal/build/runBuild";
15
15
  import { readProjectConfig } from "../../../compiler/internal/project/readProjectConfig";
16
16
  import { resolveOwningProjectConfig } from "../../../compiler/internal/project/resolveOwningProjectConfig";
17
17
  import { resolveTsgo } from "../../../compiler/internal/resolveTsgo";
18
+ import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
18
19
  import { spawnNative } from "../../../compiler/internal/spawnNative";
19
20
  import { E2ETrace } from "../../../internal/E2ETrace";
20
21
  import { createCanonicalTempDirectory } from "../../../internal/createCanonicalTempDirectory";
21
22
  import { runtimeExecutableIdentity } from "../../../internal/runtimeExecutableIdentity";
22
23
  import { moduleResolutionBaseSelects } from "../../../plugin/internal/load/moduleResolutionBaseSelects";
24
+ import { PluginContentIdentities } from "../../../plugin/internal/source/PluginContentIdentities";
23
25
  import { recordCacheFileUse } from "../../../plugin/internal/source/recordCacheFileUse";
24
26
  import { buildSingleRootProject } from "../buildSingleRootProject";
25
27
  import { inlineServedSourceMap } from "../inlineServedSourceMap";
@@ -684,6 +686,17 @@ function observePluginDescriptorInputOnce(record: {
684
686
  (record.hash !== undefined && record.hash !== observedHash) ||
685
687
  (record.realpath !== undefined && record.realpath !== observedRealpath) ||
686
688
  (record.signature !== undefined && record.signature !== afterSignature);
689
+ // A missing path's signature is its nearest existing ancestor's metadata,
690
+ // so a write anywhere in that ancestor refuses the proof; the trace names
691
+ // which observation moved.
692
+ if (unstable)
693
+ E2ETrace.capabilityResolution("plugin-descriptor-input-unstable", {
694
+ resolved: record.resolved,
695
+ recordedSignature: record.signature,
696
+ beforeSignature,
697
+ afterSignature,
698
+ hashChanged: record.hash !== undefined && record.hash !== observedHash,
699
+ });
687
700
  return `${JSON.stringify({
688
701
  ...record,
689
702
  hash: observedHash,
@@ -1479,12 +1492,63 @@ function orphanCacheRoot(): string {
1479
1492
  * Read afresh at every use, never remembered by path: a long-lived process can
1480
1493
  * lower orphans before and after the compiler at that path is replaced, and an
1481
1494
  * entry lowered by the new one must not be recorded under the old one's key for
1482
- * a later process to adopt.
1495
+ * a later process to adopt. A fresh use proves an unchanged compiler from the
1496
+ * run's plugin cache records (`runtimeRecordStore`), which observe its metadata
1497
+ * again each time.
1483
1498
  */
1484
1499
  function compilerIdentity(binary: string): string {
1485
- return runtimeExecutableIdentity(binary) ?? crypto.randomUUID();
1500
+ return (
1501
+ runtimeExecutableIdentity(binary, runtimeRecordStore()) ??
1502
+ crypto.randomUUID()
1503
+ );
1486
1504
  }
1487
1505
 
1506
+ /**
1507
+ * The record store of this run's plugin cache root, or `undefined` without one.
1508
+ *
1509
+ * WARNING (#1726): every ttsx process and every descriptor evaluation streamed
1510
+ * the whole compiler executable (24.5 MB) to key its orphan and dependency
1511
+ * builds, although the bytes had not changed since the last process. The root
1512
+ * is the one a descriptor evaluator's parent load selected
1513
+ * (`TTSC_PLUGIN_RECORD_ROOT`), or the cache root of this run's manifest; a
1514
+ * process with neither streams as before and never selects a root itself.
1515
+ */
1516
+ function runtimeRecordStore(): PluginContentIdentities.Store | undefined {
1517
+ const descriptorRoot =
1518
+ process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1"
1519
+ ? SidecarEnvironment.read(process.env, "TTSC_PLUGIN_RECORD_ROOT")
1520
+ : undefined;
1521
+ const owner = RuntimeManifestRegistry.runtimeManifests().find(
1522
+ (candidate) =>
1523
+ typeof candidate.orphanCacheDir === "string" &&
1524
+ candidate.orphanCacheDir.length !== 0,
1525
+ );
1526
+ const root =
1527
+ descriptorRoot !== undefined && path.isAbsolute(descriptorRoot)
1528
+ ? descriptorRoot
1529
+ : owner === undefined
1530
+ ? undefined
1531
+ : path.dirname(owner.orphanCacheDir!);
1532
+ if (root === undefined) return undefined;
1533
+ // One store per root serves the process: its references only grow older,
1534
+ // which can make fewer stamps separable but never more, and each identity
1535
+ // still observes the compiler's metadata afresh.
1536
+ if (runtimeStore?.root !== root)
1537
+ runtimeStore = {
1538
+ root,
1539
+ store: PluginContentIdentities.open({
1540
+ projectRoot: root,
1541
+ cacheDir: root,
1542
+ env: process.env,
1543
+ }),
1544
+ };
1545
+ return runtimeStore.store;
1546
+ }
1547
+
1548
+ let runtimeStore:
1549
+ | { root: string; store: PluginContentIdentities.Store | undefined }
1550
+ | undefined;
1551
+
1488
1552
  /** The version of this ttsc package, which owns the orphan post-processing. */
1489
1553
  function ownPackageVersion(): string {
1490
1554
  if (ownPackageVersionCache === undefined) {
@@ -2020,6 +2084,7 @@ function dependencyCachePaths(
2020
2084
  try {
2021
2085
  compilerProof = runtimeExecutableIdentity(
2022
2086
  resolveTsgo({ cwd: path.dirname(tsconfig) }).binary,
2087
+ runtimeRecordStore(),
2023
2088
  );
2024
2089
  } catch {
2025
2090
  // An unobservable executable gets a unique, non-reusable generation key.
@@ -2045,7 +2110,7 @@ function dependencyCachePaths(
2045
2110
  function assertCompilerStillCurrent(tsconfig: string, proof?: string): void {
2046
2111
  if (proof === undefined) return;
2047
2112
  const binary = resolveTsgo({ cwd: path.dirname(tsconfig) }).binary;
2048
- const current = runtimeExecutableIdentity(binary);
2113
+ const current = runtimeExecutableIdentity(binary, runtimeRecordStore());
2049
2114
  if (current !== proof) {
2050
2115
  E2ETrace.capabilityResolution("runtime-compiler-publication-refused", {
2051
2116
  tsconfig,
@@ -34,7 +34,8 @@ import { LSPProjectInputDigest } from "./LSPProjectInputDigest";
34
34
  * detected drift; unavailable markers or changes between reads are not a proof
35
35
  * that the complete filesystem stayed fixed throughout capture.
36
36
  *
37
- * @param loaded The plugin load of the session.
37
+ * @param loaded The plugin load of the session, with the project whose plugin
38
+ * cache proves its plugin sources from records.
38
39
  * @returns The inputs, or `undefined` when the load no longer describes the
39
40
  * filesystem and the selection has to be loaded again.
40
41
  * @evidence contracts/common.md#principled-implementation The manifest groups candidate/source-file digests by directory. Recorded content and physical-target proofs and all plugin-source states are compared again before return; a detected mismatch rejects capture, while sequential reads and unproven paths do not certify every post-load change.
@@ -53,6 +54,7 @@ export function captureLSPPluginSelectionInputs(loaded: {
53
54
  hostInputRealpaths: Readonly<Record<string, string | null>>;
54
55
  hostInputs: readonly string[];
55
56
  pluginSources: Readonly<Record<string, string>>;
57
+ project?: { root: string };
56
58
  }): ILSPPluginSelectionInputs | undefined {
57
59
  const excluded = new Set(
58
60
  loaded.deferredHostInputs.map((file) => path.resolve(file)),
@@ -85,8 +87,15 @@ export function captureLSPPluginSelectionInputs(loaded: {
85
87
  )
86
88
  )
87
89
  return undefined;
90
+ // The load's project names the plugin cache whose records prove the sources
91
+ // and the SDK, so a session start does not read them again (#1725).
88
92
  for (const [directory, state] of Object.entries(loaded.pluginSources))
89
- if (!pluginSourceStateHolds(directory, state)) return undefined;
93
+ if (
94
+ !pluginSourceStateHolds(directory, state, {
95
+ projectRoot: loaded.project?.root,
96
+ })
97
+ )
98
+ return undefined;
90
99
  return {
91
100
  omittedNames: GoSourceInputs.OMITTED_SOURCE_FILE_NAMES,
92
101
  omittedSuffixes: GoSourceInputs.OMITTED_SOURCE_FILE_SUFFIXES,
@@ -13,6 +13,7 @@ import { spawnNative } from "../../../compiler/internal/spawnNative";
13
13
  import { E2ETrace } from "../../../internal/E2ETrace";
14
14
  import { resolveNodeBinary } from "../../../internal/resolveNodeBinary";
15
15
  import { loadProjectPlugins } from "../../../plugin/internal/load/loadProjectPlugins";
16
+ import type { PluginContentIdentities } from "../../../plugin/internal/source/PluginContentIdentities";
16
17
  import type { ITtscLoadedNativePlugin } from "../../../structures/internal/ITtscLoadedNativePlugin";
17
18
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
18
19
  import type { ITtscProjectIdentity } from "../../../structures/internal/ITtscProjectIdentity";
@@ -165,6 +166,13 @@ type LSPExecutionContext = {
165
166
  */
166
167
  selectionInputs?: ILSPPluginSelectionInputs;
167
168
 
169
+ /**
170
+ * Record store the confirming plugin load opened, which proves the Node
171
+ * runtime from metadata instead of streaming it again (#1726); absent for a
172
+ * project without plugins, which gains no cache directory.
173
+ */
174
+ identities?: PluginContentIdentities.Store;
175
+
168
176
  tsgoBinary: string;
169
177
  };
170
178
 
@@ -196,6 +204,7 @@ function resolveTtscserverEnv(launch: LSPLaunchOptions): TtscserverEnvironment {
196
204
  const context = resolveLspExecutionContext(launch);
197
205
  const env = lspSidecarEnvironment({
198
206
  cwd: context.projectContext?.logicalProjectRoot ?? path.resolve(launch.cwd),
207
+ identities: context.identities,
199
208
  pluginConfigOrigin: context.projectContext?.pluginConfigOrigin,
200
209
  tsgoBinary: context.tsgoBinary,
201
210
  });
@@ -244,6 +253,7 @@ function resolveTtscserverEnv(launch: LSPLaunchOptions): TtscserverEnvironment {
244
253
 
245
254
  function lspSidecarEnvironment(options: {
246
255
  cwd: string;
256
+ identities: PluginContentIdentities.Store | undefined;
247
257
  pluginConfigOrigin: string | undefined;
248
258
  tsgoBinary: string;
249
259
  }): NodeJS.ProcessEnv {
@@ -253,7 +263,7 @@ function lspSidecarEnvironment(options: {
253
263
  SidecarEnvironment.read(process.env, "TTSC_TTSX_BINARY") ??
254
264
  path.join(__dirname, "..", "..", "..", "launcher", "ttsx.js"),
255
265
  });
256
- const node = resolveNodeBinary(env, options.cwd);
266
+ const node = resolveNodeBinary(env, options.cwd, options.identities);
257
267
  SidecarEnvironment.write(env, "TTSC_NODE_BINARY", node);
258
268
  SidecarEnvironment.write(
259
269
  env,
@@ -305,6 +315,7 @@ function resolveLspExecutionContext(
305
315
  resolveFrom: __filename,
306
316
  });
307
317
  const initialProjectInputs = captureInitialLSPProjectInputs({
318
+ identities: loaded.identities,
308
319
  nativePlugins: loaded.nativePlugins,
309
320
  pluginConfigOrigin,
310
321
  project: selectedProject,
@@ -323,6 +334,7 @@ function resolveLspExecutionContext(
323
334
  resolveFrom: __filename,
324
335
  });
325
336
  const confirmedProjectInputs = captureInitialLSPProjectInputs({
337
+ identities: confirmation.identities,
326
338
  nativePlugins: confirmation.nativePlugins,
327
339
  pluginConfigOrigin,
328
340
  project: confirmedProject,
@@ -345,6 +357,9 @@ function resolveLspExecutionContext(
345
357
  initialProjectInputs: confirmedProjectInputs,
346
358
  nativePlugins: confirmation.nativePlugins,
347
359
  selectionInputs,
360
+ ...(confirmation.identities === undefined
361
+ ? {}
362
+ : { identities: confirmation.identities }),
348
363
  projectContext: {
349
364
  ...confirmedProject.identity,
350
365
  ...(pluginConfigOrigin === undefined ? {} : { pluginConfigOrigin }),
@@ -377,6 +392,7 @@ function loadLSPProjectPlugins(
377
392
  }
378
393
 
379
394
  function captureInitialLSPProjectInputs(options: {
395
+ identities: PluginContentIdentities.Store | undefined;
380
396
  nativePlugins: readonly ITtscLoadedNativePlugin[];
381
397
  pluginConfigOrigin: string | undefined;
382
398
  project: ITtscParsedProjectConfig;
@@ -414,6 +430,7 @@ function captureInitialLSPProjectInputs(options: {
414
430
  }
415
431
  const env = lspSidecarEnvironment({
416
432
  cwd: options.project.root,
433
+ identities: options.identities,
417
434
  pluginConfigOrigin: options.pluginConfigOrigin,
418
435
  tsgoBinary: options.tsgoBinary,
419
436
  });