ttsc 0.31.1 → 0.31.3

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 (146) hide show
  1. package/lib/compiler/internal/build/BuildExecution.d.ts +6 -3
  2. package/lib/compiler/internal/build/BuildExecution.js +40 -12
  3. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  4. package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -1
  5. package/lib/compiler/internal/startResidentTransform.js +5 -4
  6. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  7. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  8. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  9. package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
  10. package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
  11. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  12. package/lib/internal/pluginSource.d.ts +1 -0
  13. package/lib/internal/pluginSource.js +1 -0
  14. package/lib/internal/pluginSource.js.map +1 -1
  15. package/lib/internal/resolveNodeBinary.d.ts +6 -1
  16. package/lib/internal/resolveNodeBinary.js +6 -2
  17. package/lib/internal/resolveNodeBinary.js.map +1 -1
  18. package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
  19. package/lib/internal/runtimeExecutableIdentity.js +192 -102
  20. package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
  21. package/lib/launcher/internal/runTtsc.js +2 -2
  22. package/lib/launcher/internal/runTtsc.js.map +1 -1
  23. package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
  24. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  25. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
  26. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
  27. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
  28. package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
  29. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  30. package/lib/launcher/internal/watch/WatchTopology.d.ts +20 -3
  31. package/lib/launcher/internal/watch/WatchTopology.js +31 -9
  32. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  33. package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
  34. package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
  35. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  36. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
  37. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
  38. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  39. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +17 -3
  40. package/lib/plugin/internal/load/loadProjectPlugins.js +86 -23
  41. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  42. package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
  43. package/lib/plugin/internal/readCapabilityResolution.js +8 -5
  44. package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
  45. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +9 -3
  46. package/lib/plugin/internal/source/GoEnvironmentReading.js +53 -7
  47. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -1
  48. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +44 -6
  49. package/lib/plugin/internal/source/NativeSourcePackages.js +150 -8
  50. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  51. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +21 -3
  52. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +32 -7
  53. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  54. package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
  55. package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
  56. package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
  57. package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
  58. package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
  59. package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
  60. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
  61. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
  62. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  63. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +13 -2
  64. package/lib/plugin/internal/source/SourcePluginWorkspace.js +140 -4
  65. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  66. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +15 -2
  67. package/lib/plugin/internal/source/buildSourcePlugin.js +27 -4
  68. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  69. package/lib/plugin/internal/source/computeCacheKey.d.ts +21 -5
  70. package/lib/plugin/internal/source/computeCacheKey.js +71 -29
  71. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  72. package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
  73. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +74 -35
  74. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  75. package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
  76. package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
  77. package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
  78. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
  79. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
  80. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
  81. package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
  82. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  83. package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
  84. package/lib/plugin/internal/source/pluginSourceState.js +20 -8
  85. package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
  86. package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
  87. package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
  88. package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
  89. package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
  90. package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
  91. package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
  92. package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
  93. package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
  94. package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
  95. package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
  96. package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
  97. package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
  98. package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
  99. package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
  100. package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
  101. package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
  102. package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
  103. package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
  104. package/lib/plugin/resolveCapabilityPlugins.js +67 -15
  105. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
  106. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -1
  107. package/package.json +8 -8
  108. package/src/compiler/internal/build/BuildExecution.ts +48 -14
  109. package/src/compiler/internal/build/RunBuildOptions.ts +2 -1
  110. package/src/compiler/internal/startResidentTransform.ts +12 -3
  111. package/src/compiler/internal/transformProjectInMemory.ts +10 -2
  112. package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
  113. package/src/internal/pluginSource.ts +1 -0
  114. package/src/internal/resolveNodeBinary.ts +12 -1
  115. package/src/internal/runtimeExecutableIdentity.ts +227 -141
  116. package/src/launcher/internal/runTtsc.ts +6 -2
  117. package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
  118. package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
  119. package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
  120. package/src/launcher/internal/watch/WatchTopology.ts +38 -9
  121. package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
  122. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
  123. package/src/plugin/internal/load/loadProjectPlugins.ts +226 -98
  124. package/src/plugin/internal/readCapabilityResolution.ts +17 -6
  125. package/src/plugin/internal/source/GoEnvironmentReading.ts +74 -5
  126. package/src/plugin/internal/source/NativeSourcePackages.ts +262 -5
  127. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +41 -5
  128. package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
  129. package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
  130. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
  131. package/src/plugin/internal/source/SourcePluginWorkspace.ts +176 -2
  132. package/src/plugin/internal/source/buildSourcePlugin.ts +43 -3
  133. package/src/plugin/internal/source/computeCacheKey.ts +103 -47
  134. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +144 -29
  135. package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
  136. package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
  137. package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
  138. package/src/plugin/internal/source/pluginSourceState.ts +28 -12
  139. package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
  140. package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
  141. package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
  142. package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
  143. package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
  144. package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
  145. package/src/plugin/resolveCapabilityPlugins.ts +87 -13
  146. package/src/structures/internal/TtscSingleFileEmitOptions.ts +8 -1
@@ -4,6 +4,8 @@ import path from "node:path";
4
4
 
5
5
  import { captureProcessOutput } from "../compiler/internal/captureProcessOutput";
6
6
  import { SidecarEnvironment } from "../compiler/internal/sharedHost/SidecarEnvironment";
7
+ import type { PluginContentIdentities } from "../plugin/internal/source/PluginContentIdentities";
8
+ import { PluginLoadAnswers } from "../plugin/internal/source/PluginLoadAnswers";
7
9
  import { E2ETrace } from "./E2ETrace";
8
10
  import type { IJavaScriptRuntimeCapabilities } from "./IJavaScriptRuntimeCapabilities";
9
11
  import { OwnedSynchronousProcess } from "./OwnedSynchronousProcess";
@@ -20,28 +22,56 @@ import { spawnSyncWithLowDescriptors } from "./spawnSyncWithLowDescriptors";
20
22
  * Cache-ineligible candidates do not acquire executable proofs used only by
21
23
  * cache lookup and publication.
22
24
  *
23
- * @evidence contracts/common.md#principled-implementation A child reports its own feature availability and executable; byte-aware before/after identity and same-executable proof authorize reuse only for a stable absolute runtime without NODE_OPTIONS preload authority.
25
+ * With a record store, the executable identity is proven from metadata and a
26
+ * probe answer recorded for that identity is reused by a later process (#1723);
27
+ * a recorded answer must still name an executable this runtime is.
28
+ *
29
+ * @param runtime The interpreter to probe.
30
+ * @param env Caller environment merged over this process's.
31
+ * @param cwd Working directory of the probe.
32
+ * @param identities Optional record store of the caller's plugin cache.
33
+ * @evidence contracts/common.md#principled-implementation A child reports its own feature availability and executable; byte-aware before/after identity and same-executable proof authorize reuse only for a stable absolute runtime without NODE_OPTIONS preload authority, in process or through an answer recorded under that identity.
24
34
  * @evidence contracts/common.md#clear-and-simple-design One probe owner coordinates identity, spawning and parsing; the shared fingerprint helper owns file-content proof and the low-descriptor helper owns the POSIX launch distinction.
25
35
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Feature availability comes from the actual interpreter rather than its name; descriptor fallback handles a supported kernel constraint and failures produce unsupported capabilities, not fabricated success.
26
36
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain measured capabilities, reuse restrictions and freshness, with comments separating platform launch and cache permission from tags.
27
37
  * @evidence contracts/portability.md#os-neutral-implementation Shared environment merge and lookup apply native name identity to caller precedence and NODE_OPTIONS preload authority, including Windows aliases; Node spawn receives an executable and argv without shell syntax, and only POSIX descriptor exhaustion uses the isolated broker.
28
- * @evidence contracts/performance.md#efficient-algorithms Environment merge/name handling first decides cache eligibility. Eligible hits read B executable bytes; eligible misses acquire before/after content proofs around the actual probe. Ineligible calls retain the fresh child without reading executable bytes solely for the forbidden cache. Probe spawning/parsing, a possible descriptor-exhaustion retry and capture-file reads add text/environment/native lookup costs and transient output storage; this adapter sets no explicit byte quota.
29
- * @evidence contracts/performance.md#reuse-equivalent-work Stable absolute candidates that report their own executable share measured capability results, including false feature flags; changed executable bytes, link/target identity, preload options or wrapper identity require another probe, while failed probes without executable identity are not reused.
38
+ * @evidence contracts/performance.md#efficient-algorithms Environment merge/name handling first decides cache eligibility. Eligible hits prove the executable from a record (three stats and one small read) or read B executable bytes; eligible misses acquire before/after identities around the actual probe. Ineligible calls retain the fresh child without reading executable bytes solely for the forbidden cache. Probe spawning/parsing, a possible descriptor-exhaustion retry and capture-file reads add text/environment/native lookup costs and transient output storage; this adapter sets no explicit byte quota.
39
+ * @evidence contracts/performance.md#reuse-equivalent-work Stable absolute candidates that report their own executable share measured capability results, including false feature flags, within the process and across processes through the record store keyed by the full identity; changed executable bytes, link/target identity, preload options or wrapper identity require another probe, while failed probes without executable identity are not reused.
30
40
  * @evidence contracts/performance.md#bound-retention-and-release-resources The synchronous probe owns child completion and fallback captures; finally attempts capture close/removal, whose helper suppresses cleanup failures, so native/file release is unconfirmed on failure. Capability entries retain absolute spellings and path/identity text with no eviction quota; failed identity validation can remove an entry. Returned capability copies transfer to callers.
31
41
  */
32
42
  export function javascriptRuntimeCapabilities(
33
43
  runtime: string,
34
44
  env: NodeJS.ProcessEnv,
35
45
  cwd: string,
46
+ identities?: PluginContentIdentities.Store,
36
47
  ): IJavaScriptRuntimeCapabilities {
37
48
  const effectiveEnv = SidecarEnvironment.merge(process.env, env);
38
49
  const cacheKey = runtimeCapabilityCacheKey(runtime, effectiveEnv);
39
50
  const beforeIdentity =
40
- cacheKey === undefined ? undefined : runtimeExecutableIdentity(runtime);
51
+ cacheKey === undefined
52
+ ? undefined
53
+ : runtimeExecutableIdentity(runtime, identities);
41
54
  if (cacheKey !== undefined && beforeIdentity !== undefined) {
42
55
  const cached = runtimeCapabilityCache.get(cacheKey);
43
56
  if (cached?.identity === beforeIdentity) return { ...cached.capabilities };
44
57
  runtimeCapabilityCache.delete(cacheKey);
58
+ // WARNING (#1723): a launch is a new process, so the memo above is empty
59
+ // on every CLI run. The same proven executable answers the same probe; the
60
+ // record store carries that answer across processes.
61
+ const recorded = recordedCapabilities(
62
+ PluginLoadAnswers.read(identities, RUNTIME_CAPABILITIES, [
63
+ cacheKey,
64
+ beforeIdentity,
65
+ ]),
66
+ runtime,
67
+ );
68
+ if (recorded !== undefined) {
69
+ runtimeCapabilityCache.set(cacheKey, {
70
+ capabilities: { ...recorded },
71
+ identity: beforeIdentity,
72
+ });
73
+ return recorded;
74
+ }
45
75
  }
46
76
  const args = [
47
77
  "-e",
@@ -113,7 +143,9 @@ export function javascriptRuntimeCapabilities(
113
143
  // This removes a process spawn from the common path without
114
144
  // authorizing a replaced or redirected runtime in a long-lived host.
115
145
  const afterIdentity =
116
- cacheKey === undefined ? undefined : runtimeExecutableIdentity(runtime);
146
+ cacheKey === undefined
147
+ ? undefined
148
+ : runtimeExecutableIdentity(runtime, identities);
117
149
  if (
118
150
  cacheKey !== undefined &&
119
151
  capabilities.executable !== undefined &&
@@ -125,10 +157,44 @@ export function javascriptRuntimeCapabilities(
125
157
  capabilities: { ...capabilities },
126
158
  identity: afterIdentity,
127
159
  });
160
+ PluginLoadAnswers.write(
161
+ identities,
162
+ RUNTIME_CAPABILITIES,
163
+ [cacheKey, afterIdentity],
164
+ capabilities,
165
+ );
128
166
  }
129
167
  return capabilities;
130
168
  }
131
169
 
170
+ /** Answer family of measured runtime capabilities (`PluginLoadAnswers`). */
171
+ const RUNTIME_CAPABILITIES = "javascript-runtime-capabilities";
172
+
173
+ /**
174
+ * A recorded probe answer in the shape the probe produces, still naming an
175
+ * executable this runtime is, or `undefined`.
176
+ */
177
+ function recordedCapabilities(
178
+ value: unknown,
179
+ runtime: string,
180
+ ): IJavaScriptRuntimeCapabilities | undefined {
181
+ if (typeof value !== "object" || value === null) return undefined;
182
+ const recorded = value as Partial<IJavaScriptRuntimeCapabilities>;
183
+ if (
184
+ typeof recorded.bun !== "boolean" ||
185
+ typeof recorded.registerHooks !== "boolean" ||
186
+ typeof recorded.executable !== "string" ||
187
+ !path.isAbsolute(recorded.executable) ||
188
+ !sameRuntimeExecutable(runtime, recorded.executable)
189
+ )
190
+ return undefined;
191
+ return {
192
+ bun: recorded.bun,
193
+ executable: recorded.executable,
194
+ registerHooks: recorded.registerHooks,
195
+ };
196
+ }
197
+
132
198
  type RuntimeCapabilityCacheEntry = {
133
199
  capabilities: IJavaScriptRuntimeCapabilities;
134
200
  identity: string;
@@ -22,5 +22,6 @@ export * from "../plugin/internal/source/pluginSourceDigest";
22
22
  export * from "../plugin/internal/source/pluginSourceState";
23
23
  export * from "../plugin/internal/source/pluginSourceStateHolds";
24
24
  export * from "../plugin/internal/source/processPluginBuildEnvironment";
25
+ export * from "../plugin/internal/source/provenPluginSourceDigest";
25
26
  export * from "../plugin/internal/source/PluginBuildEnvironmentReadings";
26
27
  export * from "../plugin/internal/source/prunesPluginSourceDirectory";
@@ -1,4 +1,5 @@
1
1
  import { SidecarEnvironment } from "../compiler/internal/sharedHost/SidecarEnvironment";
2
+ import type { PluginContentIdentities } from "../plugin/internal/source/PluginContentIdentities";
2
3
  import { javascriptRuntimeCapabilities } from "./javascriptRuntimeCapabilities";
3
4
 
4
5
  /**
@@ -7,6 +8,10 @@ import { javascriptRuntimeCapabilities } from "./javascriptRuntimeCapabilities";
7
8
  * Bun can directly evaluate a descriptor, but it does not implement the
8
9
  * synchronous `module.registerHooks` contract used by those loaders.
9
10
  *
11
+ * @param env Effective environment naming `TTSC_NODE_BINARY`.
12
+ * @param cwd Working directory of the probes.
13
+ * @param identities Optional record store that lets probes of an unchanged
14
+ * runtime be answered across processes (#1723).
10
15
  * @evidence contracts/common.md#principled-implementation Ordered candidates are accepted only after an actual runtime probe reports non-Bun registerHooks support and its absolute executable; names alone do not establish the loader capability.
11
16
  * @evidence contracts/common.md#clear-and-simple-design Candidate precedence and duplicate suppression remain here, while the shared probe owns launch, capability parsing and freshness policy.
12
17
  * @evidence contracts/common.md#prohibited-implementation-shortcuts The node spelling is a supported final discovery candidate, not an assumed successful runtime; incompatible candidates are skipped without patching modules or globals.
@@ -19,6 +24,7 @@ import { javascriptRuntimeCapabilities } from "./javascriptRuntimeCapabilities";
19
24
  export function resolveNodeBinary(
20
25
  env: NodeJS.ProcessEnv = process.env,
21
26
  cwd: string = process.cwd(),
27
+ identities?: PluginContentIdentities.Store,
22
28
  ): string | undefined {
23
29
  const candidates = [
24
30
  SidecarEnvironment.read(env, "TTSC_NODE_BINARY"),
@@ -31,7 +37,12 @@ export function resolveNodeBinary(
31
37
  if (candidate === undefined || candidate.trim() === "") continue;
32
38
  if (seen.has(candidate)) continue;
33
39
  seen.add(candidate);
34
- const capabilities = javascriptRuntimeCapabilities(candidate, env, cwd);
40
+ const capabilities = javascriptRuntimeCapabilities(
41
+ candidate,
42
+ env,
43
+ cwd,
44
+ identities,
45
+ );
35
46
  if (
36
47
  !capabilities.bun &&
37
48
  capabilities.registerHooks &&
@@ -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
  }
@@ -19,6 +19,7 @@ import { createFilesystemPathIdentityContext } from "../../internal/pathIdentity
19
19
  import { isFilesystemPathIdentityWithin } from "../../internal/pathIdentity/isFilesystemPathIdentityWithin";
20
20
  import { resolvePhysicalPath } from "../../internal/pathIdentity/resolvePhysicalPath";
21
21
  import { resolveSafeCacheCleanupTargets } from "../../internal/resolveSafeCacheCleanupTargets";
22
+ import type { PluginContentIdentities } from "../../plugin/internal/source/PluginContentIdentities";
22
23
  import { SourceBuildCacheLayout } from "../../plugin/internal/source/SourceBuildCacheLayout";
23
24
  import { legacyGlobalCacheTargets } from "../../plugin/internal/source/legacyGlobalCacheTargets";
24
25
  import { resolveCleanTargets } from "../../plugin/internal/source/resolveCleanTargets";
@@ -634,8 +635,11 @@ function runWatch(
634
635
  const invocation = {
635
636
  ...(checkOnly ? { ...options, emit: false } : options),
636
637
  cwd,
637
- onWatchInputs: (inputs: readonly string[]) => {
638
- topology?.setExtraInputs(inputs);
638
+ onWatchInputs: (
639
+ inputs: readonly string[],
640
+ identities?: PluginContentIdentities.Store,
641
+ ) => {
642
+ topology?.setExtraInputs(inputs, identities);
639
643
  },
640
644
  onProjectInputs: (inputs: ITtscProjectInputSnapshot) => {
641
645
  topology?.setProjectInputs(inputs);