ttsc 0.31.1 → 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 (135) hide show
  1. package/lib/compiler/internal/build/BuildExecution.d.ts +4 -2
  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/startResidentTransform.js +5 -4
  5. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  6. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  7. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  8. package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
  9. package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
  10. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  11. package/lib/internal/pluginSource.d.ts +1 -0
  12. package/lib/internal/pluginSource.js +1 -0
  13. package/lib/internal/pluginSource.js.map +1 -1
  14. package/lib/internal/resolveNodeBinary.d.ts +6 -1
  15. package/lib/internal/resolveNodeBinary.js +6 -2
  16. package/lib/internal/resolveNodeBinary.js.map +1 -1
  17. package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
  18. package/lib/internal/runtimeExecutableIdentity.js +192 -102
  19. package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
  20. package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
  21. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  22. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
  23. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
  24. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
  25. package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
  26. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  27. package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
  28. package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
  29. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  30. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
  31. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
  32. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  33. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +12 -1
  34. package/lib/plugin/internal/load/loadProjectPlugins.js +80 -20
  35. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  36. package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
  37. package/lib/plugin/internal/readCapabilityResolution.js +8 -5
  38. package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
  39. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +9 -3
  40. package/lib/plugin/internal/source/GoEnvironmentReading.js +53 -7
  41. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -1
  42. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +44 -6
  43. package/lib/plugin/internal/source/NativeSourcePackages.js +150 -8
  44. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  45. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +21 -3
  46. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +32 -7
  47. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  48. package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
  49. package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
  50. package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
  51. package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
  52. package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
  53. package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
  54. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
  55. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
  56. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  57. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +5 -2
  58. package/lib/plugin/internal/source/SourcePluginWorkspace.js +5 -3
  59. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  60. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +15 -2
  61. package/lib/plugin/internal/source/buildSourcePlugin.js +27 -4
  62. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  63. package/lib/plugin/internal/source/computeCacheKey.d.ts +21 -5
  64. package/lib/plugin/internal/source/computeCacheKey.js +71 -29
  65. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  66. package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
  67. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +74 -35
  68. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  69. package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
  70. package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
  71. package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
  72. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
  73. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
  74. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
  75. package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
  76. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  77. package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
  78. package/lib/plugin/internal/source/pluginSourceState.js +20 -8
  79. package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
  80. package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
  81. package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
  82. package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
  83. package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
  84. package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
  85. package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
  86. package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
  87. package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
  88. package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
  89. package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
  90. package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
  91. package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
  92. package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
  93. package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
  94. package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
  95. package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
  96. package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
  97. package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
  98. package/lib/plugin/resolveCapabilityPlugins.js +67 -15
  99. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
  100. package/package.json +8 -8
  101. package/src/compiler/internal/build/BuildExecution.ts +46 -13
  102. package/src/compiler/internal/startResidentTransform.ts +12 -3
  103. package/src/compiler/internal/transformProjectInMemory.ts +10 -2
  104. package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
  105. package/src/internal/pluginSource.ts +1 -0
  106. package/src/internal/resolveNodeBinary.ts +12 -1
  107. package/src/internal/runtimeExecutableIdentity.ts +227 -141
  108. package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
  109. package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
  110. package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
  111. package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
  112. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
  113. package/src/plugin/internal/load/loadProjectPlugins.ts +212 -94
  114. package/src/plugin/internal/readCapabilityResolution.ts +17 -6
  115. package/src/plugin/internal/source/GoEnvironmentReading.ts +74 -5
  116. package/src/plugin/internal/source/NativeSourcePackages.ts +262 -5
  117. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +41 -5
  118. package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
  119. package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
  120. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
  121. package/src/plugin/internal/source/SourcePluginWorkspace.ts +6 -1
  122. package/src/plugin/internal/source/buildSourcePlugin.ts +43 -3
  123. package/src/plugin/internal/source/computeCacheKey.ts +103 -47
  124. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +144 -29
  125. package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
  126. package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
  127. package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
  128. package/src/plugin/internal/source/pluginSourceState.ts +28 -12
  129. package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
  130. package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
  131. package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
  132. package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
  133. package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
  134. package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
  135. package/src/plugin/resolveCapabilityPlugins.ts +87 -13
@@ -1,7 +1,10 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
3
4
  import { GoSourceInputs } from "./GoSourceInputs";
4
5
  import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
6
+ import type { PluginContentIdentities } from "./PluginContentIdentities";
7
+ import { PluginLoadAnswers } from "./PluginLoadAnswers";
5
8
  import { spawnGoTool } from "./spawnGoTool";
6
9
 
7
10
  /**
@@ -25,6 +28,28 @@ export namespace GoEnvironmentReading {
25
28
  */
26
29
  const goEnvironmentFiles = new Map<string, string | null>();
27
30
 
31
+ /**
32
+ * Answer family that carries the same location hint across processes, so a
33
+ * new process does not run `go env` once merely to learn it (#1722). A wrong
34
+ * or stale hint only costs the existing rediscovery run.
35
+ */
36
+ const ENVIRONMENT_FILE_HINT = "go-environment-file";
37
+
38
+ /**
39
+ * What chooses the file's location: `GOENV`, else `os.UserConfigDir()`
40
+ * (`%AppData%`, `$XDG_CONFIG_HOME`, `$HOME`, Plan 9's `$home`). The hint
41
+ * across processes is keyed by these alone, because a launch rarely repeats
42
+ * every unrelated variable, such as a per-run trace path, and a hint that
43
+ * misses costs the rediscovery run on every launch.
44
+ */
45
+ const LOCATION_VARIABLES = [
46
+ "GOENV",
47
+ "APPDATA",
48
+ "XDG_CONFIG_HOME",
49
+ "HOME",
50
+ "home",
51
+ ] as const;
52
+
28
53
  /**
29
54
  * Run `go env -json` for the requested keys and `GOENV`, the file `go env -w`
30
55
  * writes, witnessing that file before the run reads it.
@@ -38,6 +63,11 @@ export namespace GoEnvironmentReading {
38
63
  * one witnessed. A file that keeps moving between runs leaves the reading
39
64
  * unwitnessable, so a kept reading or a build keyed on it never holds.
40
65
  *
66
+ * The location hint also comes from the record store when one is supplied, so
67
+ * a new process witnesses the right file on its first run instead of running
68
+ * `go env` once only to learn the name.
69
+ *
70
+ * @param identities Optional record store carrying the hint across processes.
41
71
  * @returns The parsed object, or `undefined` when native acquisition or JSON
42
72
  * object decoding failed.
43
73
  * @evidence contracts/common.md#principled-implementation Actual Go supplies values and the environment-file path; pre-read metadata and bounded rediscovery preserve the earliest file witness instead of attaching a later state to an earlier read.
@@ -46,8 +76,8 @@ export namespace GoEnvironmentReading {
46
76
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain why file discovery can repeat and distinguish unavailable acquisition from current authority.
47
77
  * @evidence contracts/portability.md#os-neutral-implementation Go reports its actual environment-file location; native path metadata and the existing Go process owner supply observation and execution semantics.
48
78
  * @evidence contracts/performance.md#efficient-algorithms Hashing the complete tool/environment identity and up to three native Go queries process full name/value/output bytes. Requested keys contribute argument bytes; file discovery uses metadata rather than an SDK walk.
49
- * @evidence contracts/performance.md#reuse-equivalent-work Only file-location discovery hints are shared across equal selected-tool/environment identities. Current Go values and file authority are reacquired for every call.
50
- * @evidence contracts/performance.md#bound-retention-and-release-resources The module retains one file-location hint per historical tool/environment identity, without eviction, but no positive reading or native handle. Command capture/retirement belongs to spawnGoTool and the caller owns the transferred witness.
79
+ * @evidence contracts/performance.md#reuse-equivalent-work Only file-location discovery hints are shared: in process across equal selected-tool/environment identities, and through the record store across equal selected tools and location variables, since the reading verifies any hint against the file Go reports. Current Go values and file authority are reacquired for every call.
80
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The module retains one file-location hint per historical tool/environment identity, without eviction, but no positive reading or native handle; persisted hints belong to the single-file collector. Command capture/retirement belongs to spawnGoTool and the caller owns the transferred witness.
51
81
  */
52
82
  export function read(
53
83
  goBinary: string,
@@ -55,6 +85,7 @@ export namespace GoEnvironmentReading {
55
85
  env: NodeJS.ProcessEnv,
56
86
  keys: readonly string[],
57
87
  witness: PluginBuildEnvironmentWitness.Record | undefined,
88
+ identities?: PluginContentIdentities.Store,
58
89
  ): Record<string, unknown> | undefined {
59
90
  const memo = crypto.createHash("sha256").update(goBinary);
60
91
  for (const [key, value] of Object.entries(env).sort(([left], [right]) =>
@@ -65,6 +96,15 @@ export namespace GoEnvironmentReading {
65
96
  }
66
97
  const memoKey = memo.digest("hex");
67
98
  let named = goEnvironmentFiles.get(memoKey);
99
+ if (named === undefined) {
100
+ const hinted = PluginLoadAnswers.read(
101
+ identities,
102
+ ENVIRONMENT_FILE_HINT,
103
+ hintKey(goBinary, env),
104
+ );
105
+ if (hinted === null || typeof hinted === "string") named = hinted;
106
+ }
107
+ const remembered = named;
68
108
  for (let attempt = 0; attempt < 3; attempt += 1) {
69
109
  if (named !== undefined && named !== null)
70
110
  PluginBuildEnvironmentWitness.add(witness, named);
@@ -94,16 +134,45 @@ export namespace GoEnvironmentReading {
94
134
  parsed.GOENV !== "off"
95
135
  ? parsed.GOENV
96
136
  : null;
97
- if (reported === named) return parsed;
137
+ if (reported === named) {
138
+ goEnvironmentFiles.set(memoKey, named);
139
+ if (named !== remembered)
140
+ PluginLoadAnswers.write(
141
+ identities,
142
+ ENVIRONMENT_FILE_HINT,
143
+ hintKey(goBinary, env),
144
+ named,
145
+ );
146
+ return parsed;
147
+ }
98
148
  if (named !== undefined && named !== null) witness?.delete(named);
99
149
  named = reported;
100
150
  goEnvironmentFiles.set(memoKey, named);
101
151
  // No file to witness: the reading depends on none.
102
- if (named === null) return parsed;
103
- if (witness === undefined) return parsed;
152
+ if (named === null || witness === undefined) {
153
+ if (named !== remembered)
154
+ PluginLoadAnswers.write(
155
+ identities,
156
+ ENVIRONMENT_FILE_HINT,
157
+ hintKey(goBinary, env),
158
+ named,
159
+ );
160
+ return parsed;
161
+ }
104
162
  }
105
163
  if (named !== undefined && named !== null)
106
164
  PluginBuildEnvironmentWitness.refuse(witness, named);
107
165
  return undefined;
108
166
  }
167
+
168
+ // A wrong hint is safe: the reading compares it with the `GOENV` Go reports
169
+ // and runs again with the reported file witnessed.
170
+ function hintKey(goBinary: string, env: NodeJS.ProcessEnv): unknown {
171
+ return [
172
+ goBinary,
173
+ LOCATION_VARIABLES.map(
174
+ (name) => SidecarEnvironment.read(env, name) ?? null,
175
+ ),
176
+ ];
177
+ }
109
178
  }
@@ -1,3 +1,4 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
 
@@ -7,9 +8,14 @@ import { SourceNativeRetirement } from "../../../internal/SourceNativeRetirement
7
8
  import { createCanonicalTempDirectory } from "../../../internal/createCanonicalTempDirectory";
8
9
  import { GoSourceInputs } from "./GoSourceInputs";
9
10
  import { GoToolResolution } from "./GoToolResolution";
11
+ import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
12
+ import { PluginContentIdentities } from "./PluginContentIdentities";
13
+ import { PluginLoadAnswers } from "./PluginLoadAnswers";
10
14
  import { SourcePluginAdmission } from "./SourcePluginAdmission";
11
15
  import { SourcePluginWorkspace } from "./SourcePluginWorkspace";
12
16
  import { ensureExecutableGoToolchain } from "./ensureExecutableGoToolchain";
17
+ import { hashPluginBuildEnvironment } from "./hashPluginBuildEnvironment";
18
+ import { pluginSourceDigest } from "./pluginSourceDigest";
13
19
  import { resolveGoCompiler } from "./resolveGoCompiler";
14
20
  import { resolvePluginGoModule } from "./resolvePluginGoModule";
15
21
  import { spawnGoTool } from "./spawnGoTool";
@@ -26,7 +32,7 @@ import { spawnGoTool } from "./spawnGoTool";
26
32
  * @evidence contracts/common.md#meaningful-documentation Native prose distinguishes metadata selection from compilation and later materialized admission.
27
33
  * @evidence contracts/portability.md#os-neutral-implementation Native path/fs operations and the shared Go tool selector preserve environment-name and path identity without blanket case folding; Go itself applies target platform and cgo selection.
28
34
  * @evidence contracts/performance.md#efficient-algorithms One list command observes all distinct package entries in each prepared context. Context materialization traverses and copies admitted host/contributor files; workspace setup delegates manifest commands and list skips dependency loading. Framing and JSON parsing process complete output bytes, and retained results scale with selected packages/files.
29
- * @evidence contracts/performance.md#reuse-equivalent-work Entries are deduplicated within a contextual observation; callers group equivalent module contexts for one load. No metadata result is cached across source, environment or toolchain changes. Cold admission uses the already prepared real build workspace.
35
+ * @evidence contracts/performance.md#reuse-equivalent-work Entries are deduplicated within a contextual observation; callers group equivalent module contexts for one load. With a record store, a successful selection is answered across loads and processes under an identity built from the content of everything Go reads, so any source, overlay, contributor, manifest, Go variable or toolchain change observes Go again (#1721); erroneous selections are never recorded. Cold admission uses the already prepared real build workspace.
30
36
  * @evidence contracts/performance.md#bound-retention-and-release-resources Each metadata scratch has finally-based retirement-aware cleanup, and capture resources belong to spawnGoTool. Unknown native retirement retains scratch under the existing scope; results and complete output remain caller-owned with no independent byte ceiling.
31
37
  */
32
38
  export namespace NativeSourcePackages {
@@ -59,6 +65,35 @@ export namespace NativeSourcePackages {
59
65
  Error?: { Err: string };
60
66
  }
61
67
 
68
+ /**
69
+ * One load's readings, shared by its package selections and its builds:
70
+ * source directory digests and module build-environment digests, each by
71
+ * absolute directory. The builds fill and report the same maps, so a load
72
+ * reads each directory and each module's toolchain once.
73
+ *
74
+ * @evidence contracts/common.md#principled-implementation Both maps carry readings already taken in this load under its fixed effective environment; builds still take their own environment reading with a witness.
75
+ * @evidence contracts/common.md#clear-and-simple-design Two maps already owned by the loader travel together.
76
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The maps never outlive the load; cross-process reuse belongs to the record store.
77
+ * @evidence contracts/common.md#meaningful-documentation Members state their keys and values.
78
+ * @evidence contracts/portability.md#os-neutral-implementation Keys are native absolute paths.
79
+ * @evidenceExclude contracts/performance.md#efficient-algorithms A record type runs no algorithm.
80
+ * @evidence contracts/performance.md#reuse-equivalent-work Selections and builds of one load share one reading per directory and module.
81
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The loader owns both maps for one load.
82
+ */
83
+ export interface SelectionReadings {
84
+ /** `pluginSourceDigest` of each directory this load read. */
85
+ sources?: Map<string, string>;
86
+
87
+ /** Build-environment digest of each module root this load read. */
88
+ environments?: Map<string, string>;
89
+
90
+ /**
91
+ * Witness of each reading in `environments`, so a build of the same
92
+ * directory keys on that reading instead of probing Go again.
93
+ */
94
+ witnesses?: Map<string, PluginBuildEnvironmentWitness.Record>;
95
+ }
96
+
62
97
  /**
63
98
  * Observe entries together in a private module layout for their context.
64
99
  *
@@ -68,7 +103,7 @@ export namespace NativeSourcePackages {
68
103
  * @evidence contracts/common.md#meaningful-documentation Native prose states this operation's authority and delegated boundaries; fields retain separate native comments.
69
104
  * @evidence contracts/portability.md#os-neutral-implementation Native copy/path and selected tool/environment owners preserve platform identity; contribution target containment is checked.
70
105
  * @evidence contracts/performance.md#efficient-algorithms Copies admitted host/contributor bytes and enumerates entries; workspace setup reads manifests and one list command skips dependencies.
71
- * @evidence contracts/performance.md#reuse-equivalent-work All entries share one prepared context; optional readers are scoped to one load and the same selected tool/effective environment.
106
+ * @evidence contracts/performance.md#reuse-equivalent-work All entries share one prepared context; optional readers are scoped to one load and the same selected tool/effective environment. A supplied record store answers an equal selection without copying or running Go, under the content identity of the module, contributors, overlays, proposal manifests, toolchain/environment digest, Go variables, entries and command; an answer is recorded only when the copies matched their keyed digests, no package reported an error and the overlays still held their digests after Go ran.
72
107
  * @evidence contracts/performance.md#bound-retention-and-release-resources Finally releases owned scratch through retirement authority; unknown native closure defers removal and capture cleanup remains delegated.
73
108
  */
74
109
  export function inspect(opts: {
@@ -78,6 +113,18 @@ export namespace NativeSourcePackages {
78
113
  packages: readonly { entry: string; source?: string; name?: string }[];
79
114
  readers?: Map<string, SourcePluginWorkspace.GoModReader>;
80
115
  proposal?: boolean;
116
+
117
+ /**
118
+ * Record store under which an equal selection is answered without Go
119
+ * (#1721); without it every load runs the full observation.
120
+ */
121
+ identities?: PluginContentIdentities.Store;
122
+
123
+ /**
124
+ * The load's source and environment readings, shared with its builds so one
125
+ * load reads each directory and each module's toolchain once.
126
+ */
127
+ readings?: SelectionReadings;
81
128
  }): Package[] {
82
129
  const env = SidecarEnvironment.merge(opts.env ?? process.env);
83
130
  const { moduleRoot } = resolvePluginGoModule(opts.source, opts.pluginName);
@@ -88,6 +135,22 @@ export namespace NativeSourcePackages {
88
135
  moduleRoot,
89
136
  );
90
137
  ensureExecutableGoToolchain(goBinary, compiler.bundled);
138
+ // WARNING (#1721): never drop this lookup or narrow its identity. Without
139
+ // it every warm launch and every unplugin generation copied the module
140
+ // twice and ran `go list` twice (6 s+ on Windows) to rediscover an answer
141
+ // its unchanged inputs already determined.
142
+ const identity =
143
+ opts.identities === undefined
144
+ ? undefined
145
+ : selectionIdentity(opts, moduleRoot, goBinary, env, opts.identities);
146
+ if (identity !== undefined) {
147
+ const recalled = recallSelection(
148
+ PluginLoadAnswers.read(opts.identities, SELECTION_ANSWER, identity),
149
+ moduleRoot,
150
+ opts.packages.length,
151
+ );
152
+ if (recalled !== undefined) return recalled;
153
+ }
91
154
  const scratch = createCanonicalTempDirectory("ttsc-package-selection-");
92
155
  let taskFailure: { error: unknown } | undefined;
93
156
  try {
@@ -96,6 +159,12 @@ export namespace NativeSourcePackages {
96
159
  retainedPaths: [scratch],
97
160
  });
98
161
  SourcePluginWorkspace.materialize(moduleRoot, scratch);
162
+ // An answer is recorded only for the copy its identity describes: a
163
+ // module edited between the identity reading and the copy observed
164
+ // other bytes than the key names.
165
+ let copiesKeyed =
166
+ identity === undefined ||
167
+ pluginSourceDigest(scratch) === identity.module;
99
168
  if (opts.proposal === true) {
100
169
  // Only root manifests change. Relative Go inputs retain the source
101
170
  // module layout, while its irrelevant graph/toolchain stays inactive.
@@ -128,12 +197,17 @@ export namespace NativeSourcePackages {
128
197
  `ttsc: package selection target already exists: ${target}`,
129
198
  );
130
199
  SourcePluginWorkspace.materialize(input.source, target);
200
+ if (identity !== undefined)
201
+ copiesKeyed &&=
202
+ pluginSourceDigest(target) ===
203
+ identity.entries.find((entry) => entry[0] === input.entry)?.[2];
131
204
  }
132
205
  const reader = SourcePluginWorkspace.createGoModReader(
133
206
  goBinary,
134
207
  opts.pluginName,
135
208
  env,
136
209
  opts.readers,
210
+ opts.identities,
137
211
  );
138
212
  SourcePluginWorkspace.writeGoWork(
139
213
  scratch,
@@ -143,13 +217,33 @@ export namespace NativeSourcePackages {
143
217
  env,
144
218
  reader,
145
219
  );
146
- return read({
220
+ const observed = read({
147
221
  cwd: scratch,
148
222
  entries: opts.packages.map((input) => input.entry),
149
223
  env: GoSourceInputs.goBuildEnv(goBinary, undefined, env),
150
224
  goBinary,
151
225
  pluginName: opts.pluginName,
152
226
  });
227
+ if (
228
+ identity !== undefined &&
229
+ copiesKeyed &&
230
+ observed.every((input) => input.Error === undefined) &&
231
+ JSON.stringify(overlayDigests(opts.identities)) ===
232
+ JSON.stringify(identity.overlays)
233
+ )
234
+ PluginLoadAnswers.write(
235
+ opts.identities,
236
+ SELECTION_ANSWER,
237
+ identity,
238
+ observed.map((input) => ({
239
+ ...input,
240
+ Dir:
241
+ input.Dir === undefined
242
+ ? undefined
243
+ : path.relative(scratch, input.Dir),
244
+ })),
245
+ );
246
+ return observed;
153
247
  } catch (error) {
154
248
  taskFailure = { error };
155
249
  throw error;
@@ -173,13 +267,15 @@ export namespace NativeSourcePackages {
173
267
  * @evidence contracts/common.md#meaningful-documentation Native prose states this operation's authority and delegated boundaries; fields retain separate native comments.
174
268
  * @evidence contracts/portability.md#os-neutral-implementation Native module roots retain their selected spelling, without blanket case folding.
175
269
  * @evidence contracts/performance.md#efficient-algorithms One grouping pass plus contextual copies and metadata work for each distinct module; output storage scales with input population.
176
- * @evidence contracts/performance.md#reuse-equivalent-work Equivalent entries in a module share one list; overlay module observations share the load's selected-tool readers, without global metadata caching.
270
+ * @evidence contracts/performance.md#reuse-equivalent-work Equivalent entries in a module share one list; overlay module observations share the load's selected-tool readers, and the load's readings and record store reach each contextual observation so equal selections are answered from their content identity.
177
271
  * @evidence contracts/performance.md#bound-retention-and-release-resources Grouping/maps/results live for this call/load; scratch and native captures remain with inspect/read.
178
272
  */
179
273
  export function ownPackages(
180
274
  inputs: readonly { source: string; label: string }[],
181
275
  env: NodeJS.ProcessEnv,
182
276
  readers = new Map<string, SourcePluginWorkspace.GoModReader>(),
277
+ identities?: PluginContentIdentities.Store,
278
+ readings?: SelectionReadings,
183
279
  ): Package[] {
184
280
  const groups = new Map<string, Array<{ index: number; entry: string }>>();
185
281
  inputs.forEach((input, index) => {
@@ -196,6 +292,8 @@ export namespace NativeSourcePackages {
196
292
  pluginName: first.label,
197
293
  env,
198
294
  readers,
295
+ identities,
296
+ readings,
199
297
  packages: group.map(({ entry }) => ({ entry })),
200
298
  });
201
299
  group.forEach(({ index }, offset) => {
@@ -220,13 +318,15 @@ export namespace NativeSourcePackages {
220
318
  * @evidence contracts/common.md#meaningful-documentation Native prose states deterministic proposal authority and mandatory final ownership confirmation.
221
319
  * @evidence contracts/portability.md#os-neutral-implementation Native module/tool/environment owners preserve candidate-relative tool and source layout identity.
222
320
  * @evidence contracts/performance.md#efficient-algorithms Groups inputs by module and actual/prospective mode, copies each admitted module once per context, and batches entries through Go metadata; retained work scales with source bytes and distinct contexts.
223
- * @evidence contracts/performance.md#reuse-equivalent-work Equivalent entries share one observation and per-load manifest readers; no result crosses a load or changed context.
321
+ * @evidence contracts/performance.md#reuse-equivalent-work Equivalent entries share one observation and per-load manifest readers; a result crosses loads only through the record store's content identity, so a changed context observes Go again.
224
322
  * @evidence contracts/performance.md#bound-retention-and-release-resources Group records live for this call; scratch/process cleanup remains with inspect and no independent output byte cap is imposed.
225
323
  */
226
324
  export function propose(
227
325
  inputs: readonly { source: string; label: string; ownModule?: boolean }[],
228
326
  env: NodeJS.ProcessEnv,
229
327
  readers = new Map<string, SourcePluginWorkspace.GoModReader>(),
328
+ identities?: PluginContentIdentities.Store,
329
+ readings?: SelectionReadings,
230
330
  ): Array<{ observation: Package; ownModule: boolean }> {
231
331
  const groups = new Map<
232
332
  string,
@@ -249,6 +349,8 @@ export namespace NativeSourcePackages {
249
349
  pluginName: first.label,
250
350
  env,
251
351
  readers,
352
+ identities,
353
+ readings,
252
354
  proposal: !group[0]!.ownModule,
253
355
  packages: group.map(({ entry }) => ({ entry })),
254
356
  });
@@ -426,3 +528,158 @@ export namespace NativeSourcePackages {
426
528
  function genericHostSource(): string {
427
529
  return path.resolve(__dirname, "../../../..", "cmd/utility-host");
428
530
  }
531
+
532
+ /** Answer family of package selections (`PluginLoadAnswers`). */
533
+ const SELECTION_ANSWER = "go-package-selection";
534
+
535
+ /** Fields `read` asks Go for; part of the selection identity. */
536
+ const SELECTION_FIELDS = "-json=Dir,Name,GoFiles,CgoFiles,Error";
537
+
538
+ /**
539
+ * Everything a package selection's answer depends on, by content: the copied
540
+ * module, each contributor copied into it, every ttsc overlay the generated
541
+ * workspace uses, the generic host manifests a proposal substitutes, the
542
+ * toolchain and build environment Go reads, every `GO*`/`CGO_*` variable the
543
+ * `go list` process receives, the entries and the fixed command. Paths stay
544
+ * out, so equal content in another location shares the answer, as the binary
545
+ * key does.
546
+ */
547
+ interface ISelectionIdentity {
548
+ format: 1;
549
+ command: readonly string[];
550
+ mode: "proposal" | "actual";
551
+ module: string;
552
+ environment: string;
553
+ variables: [string, string][];
554
+ manifests: (string | null)[] | null;
555
+ overlays: string[];
556
+ entries: [string, string | null, string | null][];
557
+ }
558
+
559
+ function selectionIdentity(
560
+ opts: Parameters<typeof NativeSourcePackages.inspect>[0],
561
+ moduleRoot: string,
562
+ goBinary: string,
563
+ env: NodeJS.ProcessEnv,
564
+ identities: PluginContentIdentities.Store,
565
+ ): ISelectionIdentity | undefined {
566
+ try {
567
+ const readings = opts.readings ?? {};
568
+ const directory = (root: string): string => {
569
+ const resolved = path.resolve(root);
570
+ let digest = readings.sources?.get(resolved);
571
+ if (digest === undefined) {
572
+ digest = PluginContentIdentities.sourceDirectory(identities, resolved);
573
+ readings.sources?.set(resolved, digest);
574
+ }
575
+ return digest;
576
+ };
577
+ const resolvedRoot = path.resolve(moduleRoot);
578
+ let environment = readings.environments?.get(resolvedRoot);
579
+ if (environment === undefined) {
580
+ const hash = crypto.createHash("sha256");
581
+ const witness: PluginBuildEnvironmentWitness.Record = new Map();
582
+ hashPluginBuildEnvironment(
583
+ hash,
584
+ goBinary,
585
+ moduleRoot,
586
+ env,
587
+ { readFile: (location) => fs.readFileSync(location) },
588
+ witness,
589
+ identities,
590
+ );
591
+ environment = hash.digest("hex");
592
+ readings.environments?.set(resolvedRoot, environment);
593
+ readings.witnesses?.set(resolvedRoot, witness);
594
+ }
595
+ const host =
596
+ opts.proposal === true
597
+ ? resolvePluginGoModule(genericHostSource(), opts.pluginName).moduleRoot
598
+ : undefined;
599
+ return {
600
+ format: 1,
601
+ command: [
602
+ "list",
603
+ ...GoSourceInputs.BUILD_FLAGS,
604
+ "-find",
605
+ "-e",
606
+ SELECTION_FIELDS,
607
+ ],
608
+ mode: opts.proposal === true ? "proposal" : "actual",
609
+ module: directory(moduleRoot),
610
+ environment,
611
+ variables: Object.entries(
612
+ GoSourceInputs.goBuildEnv(goBinary, undefined, env),
613
+ )
614
+ .filter(
615
+ (entry): entry is [string, string] =>
616
+ entry[1] !== undefined && /^(GO|CGO_)/i.test(entry[0]),
617
+ )
618
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
619
+ manifests:
620
+ host === undefined
621
+ ? null
622
+ : ["go.mod", "go.sum"].map((name) =>
623
+ fileDigest(path.join(host, name)),
624
+ ),
625
+ overlays: SourcePluginWorkspace.findTtscOverlayDirs().map(directory),
626
+ entries: opts.packages.map((input) => [
627
+ input.entry,
628
+ input.name ?? null,
629
+ input.source === undefined ? null : directory(input.source),
630
+ ]),
631
+ };
632
+ } catch {
633
+ // An identity that cannot be formed only costs the real observation.
634
+ return undefined;
635
+ }
636
+ }
637
+
638
+ /** The digest of every overlay module, in the generated workspace's order. */
639
+ function overlayDigests(
640
+ identities: PluginContentIdentities.Store | undefined,
641
+ ): string[] {
642
+ return SourcePluginWorkspace.findTtscOverlayDirs().map((directory) =>
643
+ PluginContentIdentities.sourceDirectory(identities, directory),
644
+ );
645
+ }
646
+
647
+ /** A small manifest's content digest, or null when it does not exist. */
648
+ function fileDigest(file: string): string | null {
649
+ try {
650
+ return crypto
651
+ .createHash("sha256")
652
+ .update(fs.readFileSync(file))
653
+ .digest("hex");
654
+ } catch (error) {
655
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
656
+ throw error;
657
+ }
658
+ }
659
+
660
+ /**
661
+ * A recorded selection rebased onto the module it now describes, or `undefined`
662
+ * when the record does not have the shape `read` produces.
663
+ */
664
+ function recallSelection(
665
+ value: unknown,
666
+ moduleRoot: string,
667
+ count: number,
668
+ ): NativeSourcePackages.Package[] | undefined {
669
+ if (!Array.isArray(value) || value.length !== count) return undefined;
670
+ try {
671
+ const packages = NativeSourcePackages.parse(
672
+ value.map((input) => JSON.stringify(input)).join("\n"),
673
+ );
674
+ if (packages.length !== count) return undefined;
675
+ return packages.map((input) => ({
676
+ ...input,
677
+ Dir:
678
+ input.Dir === undefined
679
+ ? undefined
680
+ : path.resolve(moduleRoot, input.Dir),
681
+ }));
682
+ } catch {
683
+ return undefined;
684
+ }
685
+ }
@@ -4,6 +4,7 @@ import { SHARE_ENV, Worker } from "node:worker_threads";
4
4
 
5
5
  import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
6
6
  import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
7
+ import { PluginContentIdentities } from "./PluginContentIdentities";
7
8
  import { pluginBuildEnvironment } from "./pluginBuildEnvironment";
8
9
 
9
10
  /**
@@ -120,21 +121,46 @@ export namespace PluginBuildEnvironmentReadings {
120
121
  * Read synchronously for clients whose API owns synchronous native work.
121
122
  * Explicit refresh preserves the original fresh-comparison semantics.
122
123
  *
124
+ * WARNING (#1725): the map above ends with the process, and every consumer
125
+ * that proves a plugin-source state starts a new one: a bundler worker, a
126
+ * capability host, the language server. Without the project's record store
127
+ * each of them walked and hashed the whole GOROOT (1.7 s for the bundled
128
+ * toolchain) on its first proof. A caller that names its project lets a miss
129
+ * prove the SDK and executables from the plugin cache's records.
130
+ *
131
+ * @param directory Plugin source directory whose build environment is read.
132
+ * @param refresh Bypass a cached reading.
133
+ * @param projectRoot Project whose plugin cache root holds the records.
123
134
  * @evidence contracts/common.md#principled-implementation The same native reader constructs a digest paired with its pre-read witness; optional cached reuse validates current variables/metadata. A fresh synchronous reading is published without a separate post-read qualification here and relies on the native reader's premises.
124
135
  * @evidence contracts/common.md#clear-and-simple-design The synchronous boundary shares cached authority and delegates fresh observation to its owning reader.
125
136
  * @evidence contracts/common.md#prohibited-implementation-shortcuts A requested refresh actually probes the selected native toolchain.
126
137
  * @evidence contracts/common.md#meaningful-documentation Native prose identifies the synchronous execution owner and explicit refresh meaning.
127
138
  * @evidence contracts/portability.md#os-neutral-implementation Native Go selection and filesystem witnesses remain delegated to the shared reader.
128
139
  * @evidence contracts/performance.md#efficient-algorithms Hits pay complete variable-text indexing and native witness queries. Misses/refresh merge environment values, perform actual toolchain/SDK/content work, then rebuild the full variable-text key for publication; native/path/file/name/value bytes contribute cost beyond invocation count.
129
- * @evidence contracts/performance.md#reuse-equivalent-work Synchronous and asynchronous clients share the same qualified readings.
140
+ * @evidence contracts/performance.md#reuse-equivalent-work Synchronous and asynchronous clients share the same qualified readings; a miss of a caller naming its project takes SDK and executable digests from the record store across processes.
130
141
  * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Retention belongs to the namespace; this call publishes one replacement record.
131
142
  */
132
- export function read(directory: string, refresh = false): string {
143
+ export function read(
144
+ directory: string,
145
+ refresh = false,
146
+ projectRoot?: string,
147
+ ): string {
133
148
  const known = refresh ? undefined : cached(directory);
134
149
  if (known !== undefined) return known;
135
150
  const env = SidecarEnvironment.merge(process.env);
136
151
  const witness: PluginBuildEnvironmentWitness.Record = new Map();
137
- const environment = pluginBuildEnvironment(directory, env, witness);
152
+ const environment = pluginBuildEnvironment(
153
+ directory,
154
+ env,
155
+ witness,
156
+ projectRoot === undefined
157
+ ? undefined
158
+ : PluginContentIdentities.open({
159
+ projectRoot,
160
+ env,
161
+ sources: [directory],
162
+ }),
163
+ );
138
164
  readings.set(key(directory, env), { environment, witness });
139
165
  return environment;
140
166
  }
@@ -148,6 +174,14 @@ export namespace PluginBuildEnvironmentReadings {
148
174
  * requests can create a worker after retirement. Refresh bypasses a cached
149
175
  * reading but can still share an equivalent in-flight request.
150
176
  *
177
+ * A caller that names its project lets the worker prove the SDK and
178
+ * executables from the plugin cache's records (#1722). The worker is its own
179
+ * isolate, so without them it reads the whole GOROOT once per process, which
180
+ * a bundler or runtime restarting on every edit pays every time.
181
+ *
182
+ * @param directory Plugin source directory whose build environment is read.
183
+ * @param refresh Bypass a cached reading.
184
+ * @param projectRoot Project whose plugin cache root holds the records.
151
185
  * @evidence contracts/common.md#principled-implementation Worker results publish only under their exact current variable identity and still-current native pre-read witness; changed or unwitnessable transfer windows reject instead of publishing authority or retrying indefinitely.
152
186
  * @evidence contracts/common.md#clear-and-simple-design Qualified hits return immediately; equivalent misses share one pending promise and exclusive queued worker request.
153
187
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Go/environment/SDK preparation occurs in the worker rather than blocking the async host or fabricating a digest from compiler output.
@@ -160,6 +194,7 @@ export namespace PluginBuildEnvironmentReadings {
160
194
  export async function prepare(
161
195
  directory: string,
162
196
  refresh = false,
197
+ projectRoot?: string,
163
198
  ): Promise<string> {
164
199
  const known = refresh ? undefined : cached(directory);
165
200
  if (known !== undefined) return known;
@@ -168,7 +203,7 @@ export namespace PluginBuildEnvironmentReadings {
168
203
  const existing = pending.get(identity);
169
204
  if (existing !== undefined) return existing;
170
205
  const request = (async () => {
171
- const reading = await observe(directory, env);
206
+ const reading = await observe(directory, env, projectRoot);
172
207
  if (
173
208
  identity !== key(directory, process.env) ||
174
209
  !PluginBuildEnvironmentWitness.holds(reading.witness)
@@ -200,6 +235,7 @@ export namespace PluginBuildEnvironmentReadings {
200
235
  function observe(
201
236
  directory: string,
202
237
  env: NodeJS.ProcessEnv,
238
+ projectRoot: string | undefined,
203
239
  ): Promise<Reading> {
204
240
  const request = queue.then(
205
241
  () =>
@@ -240,7 +276,7 @@ export namespace PluginBuildEnvironmentReadings {
240
276
  current.on("error", onError);
241
277
  current.on("exit", onExit);
242
278
  try {
243
- current.postMessage({ directory, env });
279
+ current.postMessage({ directory, env, projectRoot });
244
280
  } catch (error) {
245
281
  release(false);
246
282
  reject(error);