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
@@ -8,20 +8,21 @@ import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProce
8
8
  import { SourceNativeRetirement } from "../../../internal/SourceNativeRetirement";
9
9
  import { createCanonicalTempDirectory } from "../../../internal/createCanonicalTempDirectory";
10
10
  import { runHoldingLock } from "../../../internal/runHoldingLock";
11
- import { NativeSourcePackages } from "./NativeSourcePackages";
12
- import { SourcePluginWorkspace } from "./SourcePluginWorkspace";
13
11
  import { GoSourceInputs } from "./GoSourceInputs";
14
12
  import { GoToolResolution } from "./GoToolResolution";
15
13
  import type { IPluginModuleReplaceDirectory } from "./IPluginModuleReplaceDirectory";
16
14
  import type { ITtscBuildContributor } from "./ITtscBuildContributor";
17
15
  import type { ITtscSourceBuildCachePaths } from "./ITtscSourceBuildCachePaths";
16
+ import { NativeSourcePackages } from "./NativeSourcePackages";
18
17
  import { PluginBinaryUse } from "./PluginBinaryUse";
19
18
  import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
20
19
  import type { PluginBuildLockLease } from "./PluginBuildLockLease";
21
20
  import { PluginBuildLockProtocol } from "./PluginBuildLockProtocol";
21
+ import { PluginContentIdentities } from "./PluginContentIdentities";
22
22
  import { SourceBuildCacheLayout } from "./SourceBuildCacheLayout";
23
23
  import type { SourceBuildFilesystemOperations } from "./SourceBuildFilesystemOperations";
24
24
  import { SourcePluginAdmission } from "./SourcePluginAdmission";
25
+ import { SourcePluginWorkspace } from "./SourcePluginWorkspace";
25
26
  import { acquirePluginBuildLock } from "./acquirePluginBuildLock";
26
27
  import { computeCacheKey } from "./computeCacheKey";
27
28
  import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
@@ -44,12 +45,12 @@ import { withGoBuildCacheLease } from "./withGoBuildCacheLease";
44
45
  * Build one Go source plugin into a cached executable.
45
46
  *
46
47
  * `opts.env` is the effective environment for this build; the caller merges
47
- * inherited and constructor layers using native environment-name identity so
48
- * a programmatic `TtscCompiler` instance
49
- * can pin its own Go toolchain (`TTSC_GO_BINARY`), Go build cache
50
- * (`TTSC_GO_CACHE_DIR`), and Go build variables (`GOFLAGS`, `CGO_*`, …) without
51
- * mutating the shared `process.env`. CLI callers omit it and inherit
52
- * `process.env`, so ambient behavior is unchanged.
48
+ * inherited and constructor layers using native environment-name identity so a
49
+ * programmatic `TtscCompiler` instance can pin its own Go toolchain
50
+ * (`TTSC_GO_BINARY`), Go build cache (`TTSC_GO_CACHE_DIR`), and Go build
51
+ * variables (`GOFLAGS`, `CGO_*`, …) without mutating the shared `process.env`.
52
+ * CLI callers omit it and inherit `process.env`, so ambient behavior is
53
+ * unchanged.
53
54
  *
54
55
  * The cache key includes source and toolchain readings. Before publication the
55
56
  * scratch inputs are compared with their recorded digests and the external
@@ -65,16 +66,16 @@ import { withGoBuildCacheLease } from "./withGoBuildCacheLease";
65
66
  * Optional loader-required package ownership enters the key. Its cold producer
66
67
  * admits the entry and linked sources through Go metadata in the actual
67
68
  * materialized workspace before compilation; legacy cache artifacts have a
68
- * different key. Existing binary hits trust the cache producer and key rather than rehashing
69
- * executable bytes. Default caches are managed locally, while explicit roots
70
- * retain caller-managed pruning policy. Every returned cache key registers a
71
- * reader token retained by this process until exit; registration shares the
72
- * builder/collector lease, and other consumers register independent readers.
73
- * Failure to establish ownership propagates. Private opt-in tracing records the
74
- * mandatory key-creation and pre-build witness gates and adds one diagnostic
75
- * observation after Go returns. That diagnostic read adds metadata work, not
76
- * publication permission. A discarded epoch is separately recorded when the
77
- * bounded build owner starts again.
69
+ * different key. Existing binary hits trust the cache producer and key rather
70
+ * than rehashing executable bytes. Default caches are managed locally, while
71
+ * explicit roots retain caller-managed pruning policy. Every returned cache key
72
+ * registers a reader token retained by this process until exit; registration
73
+ * shares the builder/collector lease, and other consumers register independent
74
+ * readers. Failure to establish ownership propagates. Private opt-in tracing
75
+ * records the mandatory key-creation and pre-build witness gates and adds one
76
+ * diagnostic observation after Go returns. That diagnostic read adds metadata
77
+ * work, not publication permission. A discarded epoch is separately recorded
78
+ * when the bounded build owner starts again.
78
79
  *
79
80
  * Explicit worker scopes check cancellation between build phases, copied source
80
81
  * entries and native commands, including reader admission and final
@@ -88,8 +89,8 @@ import { withGoBuildCacheLease } from "./withGoBuildCacheLease";
88
89
  * @evidence contracts/common.md#prohibited-implementation-shortcuts A changed witness is never overwritten or accepted. A fresh epoch takes a new toolchain reading and performs the actual build or qualified adoption; source, native and cleanup failures stay terminal. No selected SDK tool is warmed by a special command, and no foreign environment or memo is rebased. Shared Go objects retain their owner's toolID/actionID contract; same-version custom tool semantics beyond that contract are not newly guaranteed.
89
90
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain effective environment, recorded-input comparisons and their trust/observation limits, reader registration and managed versus explicit roots; option-map comments state their reading provenance with blank member separation.
90
91
  * @evidence contracts/portability.md#os-neutral-implementation Node path/physical-cache/temp APIs preserve native identities; executable resolution and Windows command handling are isolated owners, and the binary filename explicitly follows its executable platform.
91
- * @evidence contracts/performance.md#efficient-algorithms At most three epochs redo native toolchain observation, key construction, materialization and guarded admission; each can run one Go build, preceded by one batched package metadata command when the loader requires ownership admission. The shared compiler reader can itself make three version observations, hence at most nine such cold observations for a persistently moving compiler and no build when its key witness fails. Full-file bytes, entries, path/sort text, contributors, external trees and witness populations drive work. Scoped cancellation is sampled between phases/entries/commands and does not bound native calls or delegated hashing. First source digest readings are reused across epochs, while each scratch is independently checked. Existing lock contention has its separate wait budget and native Go compiler work remains delegated.
92
- * @evidence contracts/performance.md#reuse-equivalent-work Existing binaries and concurrent builders share the version/platform/source/environment key with reader admission before return, assuming trustworthy cache producers and supplied digest maps. Fixed trimpath compilation removes disposable snapshot paths from Go object identities. Shared load readings and sequential source/toolchain comparisons reject observed changes; metadata reuse and unobserved concurrent mutation remain the underlying witnesses' limits.
92
+ * @evidence contracts/performance.md#efficient-algorithms At most three epochs redo native toolchain observation, key construction, materialization and guarded admission; the first can key on the load's witnessed same-directory reading instead of observing the toolchain again, and later ones always observe it. Each can run one Go build, preceded by one batched package metadata command when the loader requires ownership admission. The shared compiler reader can itself make three version observations, hence at most nine such cold observations for a persistently moving compiler and no build when its key witness fails. Full-file bytes, entries, path/sort text, contributors, external trees and witness populations drive work. Scoped cancellation is sampled between phases/entries/commands and does not bound native calls or delegated hashing. First source digest readings are reused across epochs, while each scratch is independently checked. Existing lock contention has its separate wait budget and native Go compiler work remains delegated.
93
+ * @evidence contracts/performance.md#reuse-equivalent-work Existing binaries and concurrent builders share the version/platform/source/environment key with reader admission before return, assuming trustworthy cache producers and supplied digest maps. The key's content digests come from the record store when their separable metadata still matches, so a new process does not read unchanged sources, SDK or compiler bytes to find an existing binary (#1722). Fixed trimpath compilation removes disposable snapshot paths from Go object identities. Shared load readings and sequential source/toolchain comparisons reject observed changes; metadata reuse and unobserved concurrent mutation remain the underlying witnesses' limits.
93
94
  * @evidence contracts/performance.md#bound-retention-and-release-resources Scratch directories and build/cache leases have finally-based cleanup, including cancellation; cancelled work admits no new post-build prune pass. Scratch removal or lease cleanup can fail, and selected synchronous child settlement does not join arbitrary descendants. Repeated commands can repeat external tool effects; only ttsc-owned failed outputs are discarded, and Go-owned object entries retain their existing lifetime. Pending binary cleanup is best-effort. Reader tokens and their process map grow with distinct physical keys until process exit. Managed pruning attempts age/LRU reclamation while protecting live, unknown and selected entries, so it is not a hard disk bound; explicit roots remain caller-managed.
94
95
  */
95
96
  export function buildSourcePlugin(opts: {
@@ -101,7 +102,10 @@ export function buildSourcePlugin(opts: {
101
102
  env?: NodeJS.ProcessEnv;
102
103
 
103
104
  /** Loader-required ownership of the entry and named linked packages. */
104
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
105
+ packageOwnership?: readonly {
106
+ entry: string;
107
+ kind: "executable" | "linked";
108
+ }[];
105
109
 
106
110
  /**
107
111
  * Digests of the environment each build directory is keyed on, shared by
@@ -110,7 +114,21 @@ export function buildSourcePlugin(opts: {
110
114
  */
111
115
  environmentDigests?: Map<string, string>;
112
116
 
117
+ /**
118
+ * Witnesses of the readings in `environmentDigests` (`computeCacheKey`), so
119
+ * the first epoch keys on a reading package selection already took. A
120
+ * discarded epoch reads the toolchain again instead.
121
+ */
122
+ environmentWitnesses?: Map<string, PluginBuildEnvironmentWitness.Record>;
123
+
113
124
  filesystem?: Partial<SourceBuildFilesystemOperations>;
125
+
126
+ /**
127
+ * Record store the loader opened for this load, so its key reuses content
128
+ * identities across processes (#1722); a standalone build opens its own.
129
+ */
130
+ identities?: PluginContentIdentities.Store;
131
+
114
132
  label?: string;
115
133
  overlayDirs?: readonly string[];
116
134
  quiet?: boolean;
@@ -123,6 +141,9 @@ export function buildSourcePlugin(opts: {
123
141
  */
124
142
  sourceDigests?: Map<string, string>;
125
143
 
144
+ /** Pinned-tool manifest readers shared only by this loader request. */
145
+ goModReaders?: Map<string, SourcePluginWorkspace.GoModReader>;
146
+
126
147
  ttscVersion: string;
127
148
  tsgoVersion: string;
128
149
  }): string {
@@ -134,9 +155,17 @@ export function buildSourcePlugin(opts: {
134
155
  for (let attempt = 0; ; attempt += 1) {
135
156
  OwnedSynchronousProcess.checkpoint();
136
157
  const environmentDigests = new Map(opts.environmentDigests);
158
+ // Only the first epoch may key on a reading taken before it; a discarded
159
+ // epoch means the toolchain moved, so later ones read it again.
160
+ const environmentWitnesses =
161
+ opts.environmentWitnesses === undefined
162
+ ? undefined
163
+ : new Map(attempt === 0 ? opts.environmentWitnesses : []);
137
164
  try {
138
165
  const binary = buildSourcePluginAttempt(
139
- opts,
166
+ attempt === 0
167
+ ? { ...opts, environmentWitnesses }
168
+ : { ...opts, environmentWitnesses, goModReaders: undefined },
140
169
  env,
141
170
  sourceDigests,
142
171
  environmentDigests,
@@ -146,6 +175,8 @@ export function buildSourcePlugin(opts: {
146
175
  opts.sourceDigests?.set(directory, digest);
147
176
  for (const [directory, digest] of environmentDigests)
148
177
  opts.environmentDigests?.set(directory, digest);
178
+ for (const [directory, witness] of environmentWitnesses ?? [])
179
+ opts.environmentWitnesses?.set(directory, witness);
149
180
  return binary;
150
181
  } catch (error) {
151
182
  if (
@@ -174,7 +205,9 @@ function buildSourcePluginAttempt(
174
205
  ): string {
175
206
  OwnedSynchronousProcess.checkpoint();
176
207
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
177
- const overlayDirs = [...(opts.overlayDirs ?? SourcePluginWorkspace.findTtscOverlayDirs())].sort();
208
+ const overlayDirs = [
209
+ ...(opts.overlayDirs ?? SourcePluginWorkspace.findTtscOverlayDirs()),
210
+ ].sort();
178
211
  const contributors = opts.contributors ?? [];
179
212
  const compiler = resolveGoCompiler(env);
180
213
  const goBinary = GoToolResolution.resolveGoToolForBuild(
@@ -184,8 +217,27 @@ function buildSourcePluginAttempt(
184
217
  );
185
218
  ensureExecutableGoToolchain(goBinary, compiler.bundled);
186
219
  OwnedSynchronousProcess.checkpoint();
220
+ const identities =
221
+ opts.identities ??
222
+ PluginContentIdentities.open({
223
+ projectRoot: opts.baseDir,
224
+ cacheDir: opts.cacheDir,
225
+ env,
226
+ sources: [
227
+ dir,
228
+ ...overlayDirs,
229
+ ...contributors.map((contributor) => contributor.source),
230
+ ],
231
+ });
187
232
  // The digest of every directory the key covers, as the key read it, which
188
233
  // the build proves against what it compiled.
234
+ const goModReader = SourcePluginWorkspace.createGoModReader(
235
+ goBinary,
236
+ opts.pluginName,
237
+ env,
238
+ opts.goModReaders,
239
+ identities,
240
+ );
189
241
  const environmentWitness: PluginBuildEnvironmentWitness.Record = new Map();
190
242
  const key = computeCacheKey({
191
243
  contributors,
@@ -194,10 +246,13 @@ function buildSourcePluginAttempt(
194
246
  entry,
195
247
  env,
196
248
  environmentWitness,
249
+ goModReader,
197
250
  filesystem: opts.filesystem,
198
251
  goBinary,
252
+ identities,
199
253
  overlayDirs,
200
254
  environmentDigests,
255
+ environmentWitnesses: opts.environmentWitnesses,
201
256
  sourceDigests,
202
257
  ttscVersion: opts.ttscVersion,
203
258
  tsgoVersion: opts.tsgoVersion,
@@ -317,6 +372,7 @@ function buildSourcePluginAttempt(
317
372
  env,
318
373
  environmentWitness,
319
374
  goBinary,
375
+ goModReader,
320
376
  normalizeGoToolPermissions: compiler.bundled,
321
377
  key,
322
378
  keyedDigests: sourceDigests,
@@ -394,7 +450,10 @@ function compileSourcePlugin(opts: {
394
450
  binaryPath: string;
395
451
  cacheDir: string;
396
452
  contributors: readonly ITtscBuildContributor[];
397
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
453
+ packageOwnership?: readonly {
454
+ entry: string;
455
+ kind: "executable" | "linked";
456
+ }[];
398
457
  dir: string;
399
458
  entry: string;
400
459
  env: NodeJS.ProcessEnv;
@@ -406,6 +465,7 @@ function compileSourcePlugin(opts: {
406
465
  goBuildCacheRoot: string;
407
466
  manageGoBuildCache: boolean;
408
467
  normalizeGoToolPermissions: boolean;
468
+ goModReader: SourcePluginWorkspace.GoModReader;
409
469
  key: string;
410
470
 
411
471
  /** The digest of every directory the key covers, as the key read it. */
@@ -461,6 +521,7 @@ function compileSourcePlugin(opts: {
461
521
  opts.dir,
462
522
  opts.env,
463
523
  opts.goBinary,
524
+ opts.goModReader,
464
525
  );
465
526
  OwnedSynchronousProcess.checkpoint();
466
527
  // Every source the build would otherwise read in place, an overlay and
@@ -487,11 +548,7 @@ function compileSourcePlugin(opts: {
487
548
  opts.goBinary,
488
549
  opts.env,
489
550
  );
490
- const goModReader = SourcePluginWorkspace.createGoModReader(
491
- opts.goBinary,
492
- opts.pluginName,
493
- opts.env,
494
- );
551
+ const goModReader = opts.goModReader;
495
552
  OwnedSynchronousProcess.checkpoint();
496
553
  if (opts.contributors.length > 0) {
497
554
  mergeContributors({
@@ -543,12 +600,20 @@ function compileSourcePlugin(opts: {
543
600
  const observations = NativeSourcePackages.read({
544
601
  cwd: scratchDir,
545
602
  entries: opts.packageOwnership.map((input) => input.entry),
546
- env: GoSourceInputs.goBuildEnv(opts.goBinary, goBuildCacheRoot, opts.env),
603
+ env: GoSourceInputs.goBuildEnv(
604
+ opts.goBinary,
605
+ goBuildCacheRoot,
606
+ opts.env,
607
+ ),
547
608
  goBinary: opts.goBinary,
548
609
  pluginName: opts.pluginName,
549
610
  });
550
611
  opts.packageOwnership.forEach((input, index) => {
551
- NativeSourcePackages.kind(observations[index]!, `${opts.pluginName} ${input.entry}`, input.kind);
612
+ NativeSourcePackages.kind(
613
+ observations[index]!,
614
+ `${opts.pluginName} ${input.entry}`,
615
+ input.kind,
616
+ );
552
617
  });
553
618
  }
554
619
  runGoBuild(
@@ -629,10 +694,14 @@ function compileSourcePlugin(opts: {
629
694
  taskFailure = { error };
630
695
  throw error;
631
696
  } finally {
632
- SourceNativeRetirement.releaseResource(scratchDir, () => {
633
- fs.rmSync(scratchDir, { recursive: true, force: true });
634
- SourceNativeRetirement.forget(opts.cacheDir);
635
- }, taskFailure);
697
+ SourceNativeRetirement.releaseResource(
698
+ scratchDir,
699
+ () => {
700
+ fs.rmSync(scratchDir, { recursive: true, force: true });
701
+ SourceNativeRetirement.forget(opts.cacheDir);
702
+ },
703
+ taskFailure,
704
+ );
636
705
  }
637
706
  }
638
707
 
@@ -1009,7 +1078,6 @@ function resolveSourceBuildTarget(opts: {
1009
1078
  return { dir: moduleRoot, entry, source };
1010
1079
  }
1011
1080
 
1012
-
1013
1081
  /**
1014
1082
  * The path, below a build's scratch directory, of the tree holding its copies
1015
1083
  * of the sources outside the module.
@@ -5,10 +5,11 @@ import path from "node:path";
5
5
  import { GoToolResolution } from "./GoToolResolution";
6
6
  import type { ITtscBuildContributor } from "./ITtscBuildContributor";
7
7
  import type { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
8
+ import { PluginContentIdentities } from "./PluginContentIdentities";
8
9
  import type { SourceBuildFilesystemOperations } from "./SourceBuildFilesystemOperations";
10
+ import type { SourcePluginWorkspace } from "./SourcePluginWorkspace";
9
11
  import { hashPluginBuildEnvironment } from "./hashPluginBuildEnvironment";
10
12
  import { pluginModuleReplaceDirectories } from "./pluginModuleReplaceDirectories";
11
- import { pluginSourceDigest } from "./pluginSourceDigest";
12
13
 
13
14
  /**
14
15
  * Compute a 32-character lowercase key from a plugin build's SHA-256 digest.
@@ -20,8 +21,8 @@ import { pluginSourceDigest } from "./pluginSourceDigest";
20
21
  * source files. An optional required package-ownership population also frames
21
22
  * the key, separating binaries admitted by the matching cold producer from
22
23
  * legacy artifacts; requests without that authority retain their existing key.
23
- * Contributors are sorted by name; unique names remove input
24
- * order dependence, while equal-name rows retain the sort's input order.
24
+ * Contributors are sorted by name; unique names remove input order dependence,
25
+ * while equal-name rows retain the sort's input order.
25
26
  *
26
27
  * Each source directory enters the key as its digest (`pluginSourceDigest`),
27
28
  * which the transform envelope reports, with the environment below, as the
@@ -29,10 +30,12 @@ import { pluginSourceDigest } from "./pluginSourceDigest";
29
30
  * values are trusted producer readings, not revalidated content observations.
30
31
  * `sourceDigests` carries the digests one load already took: every build of the
31
32
  * load keys on one reading of each directory, and the load reports those
32
- * readings. The environment enters through `hashPluginBuildEnvironment`, the
33
- * rule that reported state takes it from too, and `environmentDigests` carries
34
- * the digest of each build directory's, so the load reports it without a second
35
- * `go env` run.
33
+ * readings. The environment enters as the digest of
34
+ * `hashPluginBuildEnvironment`, the rule that reported state takes it from too,
35
+ * and `environmentDigests` carries the digest of each build directory's, so the
36
+ * load reports it without a second `go env` run. With `environmentWitnesses`, a
37
+ * reading the load already took for the same directory, such as package
38
+ * selection's, keys the build too, and its witness joins the build's.
36
39
  *
37
40
  * The `ttsc cache` CLI and plugin build pipeline share this key computation.
38
41
  *
@@ -42,7 +45,7 @@ import { pluginSourceDigest } from "./pluginSourceDigest";
42
45
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain input coverage and the provenance of reported digests; documented optional output maps have blank separation between members.
43
46
  * @evidence contracts/portability.md#os-neutral-implementation Native roots are resolved with Node path APIs and executable resolution; platform/architecture intentionally distinguish incompatible binary artifacts.
44
47
  * @evidence contracts/performance.md#efficient-algorithms Source digest misses enumerate/sort paths and read individual files in full, not bounded streaming chunks. Overlay/contributor ordering compares path/name text; replacement discovery can read a manifest/run Go and native containment queries. Toolchain/env hashing delegates native metadata/probes/full bytes and memo checks without an unmeasured dominant-cost ranking; JSON framing processes all version/entry/label/digest text.
45
- * @evidence contracts/performance.md#reuse-equivalent-work Caller-owned sourceDigests share supplied absolute-directory readings across roles without independently verifying their provenance/currentness. EnvironmentDigests receives this call's selected environment identity so reporting can avoid another probe; it does not skip environment hashing. Reuse inherits the selected population and metadata/producer premises.
48
+ * @evidence contracts/performance.md#reuse-equivalent-work Caller-owned sourceDigests share supplied absolute-directory readings across roles without independently verifying their provenance/currentness. A supplied record store lets a new process reuse a directory, SDK or executable digest only while its separable metadata signature matches the recorded one (#1722), so unchanged inputs are not re-read per process. EnvironmentDigests receives this call's selected environment identity so reporting can avoid another probe. A same-directory reading of the same load, with its witness, replaces a second environment probe; the witness then joins this build's, which the build compares after it ran, and a non-default byte adapter never shares. Reuse inherits the selected population and metadata/producer premises.
46
49
  *
47
50
  * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Output maps belong to the enclosing load; this operation retains no handles or process-wide records itself.
48
51
  */
@@ -53,7 +56,10 @@ export function computeCacheKey(inputs: {
53
56
  env?: NodeJS.ProcessEnv;
54
57
 
55
58
  /** Required package ownership, admitted by the matching cold producer. */
56
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
59
+ packageOwnership?: readonly {
60
+ entry: string;
61
+ kind: "executable" | "linked";
62
+ }[];
57
63
 
58
64
  /**
59
65
  * Digests of the environment each build directory is keyed on
@@ -62,6 +68,14 @@ export function computeCacheKey(inputs: {
62
68
  */
63
69
  environmentDigests?: Map<string, string>;
64
70
 
71
+ /**
72
+ * Witness of each reading in `environmentDigests` this load took, by
73
+ * directory. A digest with its witness stands for this call's own reading:
74
+ * the key takes it instead of running Go again and copies the witness into
75
+ * `environmentWitness`. Filled with this call's reading when it takes one.
76
+ */
77
+ environmentWitnesses?: Map<string, PluginBuildEnvironmentWitness.Record>;
78
+
65
79
  /**
66
80
  * Receives selected native toolchain dependencies from the environment reader
67
81
  * for later comparison under its metadata-distinguishability premise; it is
@@ -72,6 +86,15 @@ export function computeCacheKey(inputs: {
72
86
  /** Byte adapter for delegated SDK identity; plugin source digests use fs. */
73
87
  filesystem?: Partial<SourceBuildFilesystemOperations>;
74
88
  goBinary?: string;
89
+ goModReader?: SourcePluginWorkspace.GoModReader;
90
+
91
+ /**
92
+ * Record store that proves unchanged sources, overlays, contributors, GOROOT
93
+ * and executables from metadata in a new process (#1722). Without it every
94
+ * process reads their bytes.
95
+ */
96
+ identities?: PluginContentIdentities.Store;
97
+
75
98
  overlayDirs?: readonly string[];
76
99
 
77
100
  /**
@@ -102,69 +125,117 @@ export function computeCacheKey(inputs: {
102
125
  hash.update(JSON.stringify(["platform", process.platform, process.arch]));
103
126
  hash.update(JSON.stringify(["entry", inputs.entry]));
104
127
  if (inputs.packageOwnership !== undefined)
105
- hash.update(JSON.stringify(["package-ownership", 1,
106
- [...inputs.packageOwnership].sort((left, right) =>
107
- left.entry < right.entry ? -1 : left.entry > right.entry ? 1 :
108
- left.kind < right.kind ? -1 : left.kind > right.kind ? 1 : 0),
109
- ]));
128
+ hash.update(
129
+ JSON.stringify([
130
+ "package-ownership",
131
+ 1,
132
+ [...inputs.packageOwnership].sort((left, right) =>
133
+ left.entry < right.entry
134
+ ? -1
135
+ : left.entry > right.entry
136
+ ? 1
137
+ : left.kind < right.kind
138
+ ? -1
139
+ : left.kind > right.kind
140
+ ? 1
141
+ : 0,
142
+ ),
143
+ ]),
144
+ );
110
145
  // Private materialization changes native package working directories. Keep
111
146
  // binaries produced by the former absolute-ancestry layout out of this
112
147
  // layout's cache admissions, including callers with no SDK overlays.
113
148
  hash.update(JSON.stringify(["external-source-layout", 1]));
114
- // The same framed values enter the key and the environment-only digest,
115
- // which pluginBuildEnvironment reads for this directory.
116
- const environment = crypto.createHash("sha256");
117
- hashPluginBuildEnvironment(
118
- {
119
- update: (data: string) => {
120
- hash.update(data);
121
- environment.update(data);
122
- },
123
- },
124
- goBinary,
125
- inputs.dir,
126
- env,
127
- filesystem,
128
- inputs.environmentWitness,
129
- );
130
- inputs.environmentDigests?.set(
131
- path.resolve(inputs.dir),
132
- environment.digest("hex"),
149
+ const overlays = [...(inputs.overlayDirs ?? [])].sort();
150
+ // Hash contributors in sorted-by-name order so two consumers with the
151
+ // same logical set produce the same key regardless of declaration order
152
+ // in the host's plugin descriptor.
153
+ const sortedContributors = [...(inputs.contributors ?? [])].sort((a, b) =>
154
+ a.name === b.name ? 0 : a.name < b.name ? -1 : 1,
133
155
  );
134
- hashSourceDirectory(hash, "plugin", inputs.dir, inputs.sourceDigests);
156
+ // A store whose root lies inside a directory this key hashes would write
157
+ // records into that source, so such a store proves nothing here. The build
158
+ // rejects that cache placement (`SourcePluginAdmission`) once every source
159
+ // is known; replacement targets narrow the store below, before their own
160
+ // digests, since only Go's reading of the manifest names them.
161
+ let identities = PluginContentIdentities.outside(inputs.identities, [
162
+ inputs.dir,
163
+ ...overlays,
164
+ ...sortedContributors.map((contributor) => contributor.source),
165
+ ]);
166
+ // The key frames the environment digest pluginBuildEnvironment reports for
167
+ // this directory. WARNING (#1721): package selection reads that environment
168
+ // first in the same load; reading it again here doubled `go env` and
169
+ // `go version` per load. A reading taken through another byte adapter is
170
+ // not this one's, so only the default adapter shares it.
171
+ const environmentDirectory = path.resolve(inputs.dir);
172
+ const sharedDigest =
173
+ inputs.filesystem === undefined
174
+ ? inputs.environmentDigests?.get(environmentDirectory)
175
+ : undefined;
176
+ const sharedWitness =
177
+ sharedDigest === undefined
178
+ ? undefined
179
+ : inputs.environmentWitnesses?.get(environmentDirectory);
180
+ let environment: string;
181
+ if (sharedDigest !== undefined && sharedWitness !== undefined) {
182
+ // The build compares this witness after it ran, exactly as it would its
183
+ // own reading's, so a toolchain change since the reading still refuses.
184
+ for (const [file, state] of sharedWitness)
185
+ if (!inputs.environmentWitness?.has(file))
186
+ inputs.environmentWitness?.set(file, state);
187
+ environment = sharedDigest;
188
+ } else {
189
+ const witness =
190
+ inputs.environmentWitness ??
191
+ (inputs.environmentWitnesses === undefined ? undefined : new Map());
192
+ const reading = crypto.createHash("sha256");
193
+ hashPluginBuildEnvironment(
194
+ reading,
195
+ goBinary,
196
+ inputs.dir,
197
+ env,
198
+ filesystem,
199
+ witness,
200
+ identities,
201
+ );
202
+ environment = reading.digest("hex");
203
+ inputs.environmentDigests?.set(environmentDirectory, environment);
204
+ if (inputs.filesystem === undefined && witness !== undefined)
205
+ inputs.environmentWitnesses?.set(environmentDirectory, new Map(witness));
206
+ }
207
+ hash.update(JSON.stringify(["environment", environment]));
208
+ const directory = (label: string, root: string): void =>
209
+ hashSourceDirectory(hash, label, root, inputs.sourceDigests, identities);
210
+ directory("plugin", inputs.dir);
135
211
  // Local replacement targets outside the module supply separately keyed
136
212
  // sources. buildSourcePlugin snapshots and proves their copies before
137
- // redirecting the Go replacement directives to the scratch tree.
138
- for (const replacement of pluginModuleReplaceDirectories(
213
+ // redirecting the Go replacement directives to the scratch tree. Go reads the
214
+ // manifest after the plugin's digest, so an edit made meanwhile lands between
215
+ // that digest and the build's copy, which `requireKeyedSource` refuses.
216
+ const replacements = pluginModuleReplaceDirectories(
139
217
  inputs.dir,
140
218
  env,
141
219
  goBinary,
142
- )) {
143
- hashSourceDirectory(
144
- hash,
220
+ inputs.goModReader,
221
+ );
222
+ identities = PluginContentIdentities.outside(
223
+ identities,
224
+ replacements.map((replacement) => replacement.directory),
225
+ );
226
+ for (const replacement of replacements) {
227
+ directory(
145
228
  `replace:${replacement.modulePath}${
146
229
  replacement.version === undefined ? "" : `@${replacement.version}`
147
230
  }`,
148
231
  replacement.directory,
149
- inputs.sourceDigests,
150
232
  );
151
233
  }
152
- for (const [index, dir] of [...(inputs.overlayDirs ?? [])].sort().entries()) {
153
- hashSourceDirectory(hash, `overlay:${index}`, dir, inputs.sourceDigests);
234
+ for (const [index, dir] of overlays.entries()) {
235
+ directory(`overlay:${index}`, dir);
154
236
  }
155
- // Hash contributors in sorted-by-name order so two consumers with the
156
- // same logical set produce the same key regardless of declaration order
157
- // in the host's plugin descriptor.
158
- const sortedContributors = [...(inputs.contributors ?? [])].sort((a, b) =>
159
- a.name === b.name ? 0 : a.name < b.name ? -1 : 1,
160
- );
161
237
  for (const contributor of sortedContributors) {
162
- hashSourceDirectory(
163
- hash,
164
- `contributor:${contributor.name}`,
165
- contributor.source,
166
- inputs.sourceDigests,
167
- );
238
+ directory(`contributor:${contributor.name}`, contributor.source);
168
239
  }
169
240
  return hash.digest("hex").slice(0, 32);
170
241
  }
@@ -179,11 +250,14 @@ function hashSourceDirectory(
179
250
  label: string,
180
251
  root: string,
181
252
  digests: Map<string, string> | undefined,
253
+ identities: PluginContentIdentities.Store | undefined,
182
254
  ): void {
183
255
  const directory = path.resolve(root);
184
256
  let digest = digests?.get(directory);
185
257
  if (digest === undefined) {
186
- digest = pluginSourceDigest(directory);
258
+ // WARNING (#1722): the load-scoped map above ends with the process. Only the
259
+ // record store spares a new process from reading every source byte again.
260
+ digest = PluginContentIdentities.sourceDirectory(identities, directory);
187
261
  digests?.set(directory, digest);
188
262
  }
189
263
  hash.update(JSON.stringify(["dir", label, digest]));