ttsc 0.31.0 → 0.31.1

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 (52) 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/internal/nativeBinary.d.ts +3 -3
  11. package/lib/internal/nativeBinary.js +3 -3
  12. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +12 -13
  13. package/lib/plugin/internal/load/loadProjectPlugins.js +61 -36
  14. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  15. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +42 -0
  16. package/lib/plugin/internal/source/GoEnvironmentReading.js +110 -0
  17. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -0
  18. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +7 -7
  19. package/lib/plugin/internal/source/NativeSourcePackages.js +33 -20
  20. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  21. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +21 -0
  22. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +36 -0
  23. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  24. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.d.ts +5 -3
  25. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js +14 -8
  26. package/lib/plugin/internal/source/PluginBuildEnvironmentWitness.js.map +1 -1
  27. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +50 -19
  28. package/lib/plugin/internal/source/SourcePluginWorkspace.js +109 -22
  29. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  30. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +19 -16
  31. package/lib/plugin/internal/source/buildSourcePlugin.js +27 -22
  32. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  33. package/lib/plugin/internal/source/computeCacheKey.d.ts +4 -2
  34. package/lib/plugin/internal/source/computeCacheKey.js +15 -6
  35. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  36. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +2 -68
  37. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  38. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.d.ts +6 -5
  39. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js +10 -17
  40. package/lib/plugin/internal/source/pluginModuleReplaceDirectories.js.map +1 -1
  41. package/package.json +8 -8
  42. package/src/internal/nativeBinary.ts +3 -3
  43. package/src/plugin/internal/load/loadProjectPlugins.ts +209 -117
  44. package/src/plugin/internal/source/GoEnvironmentReading.ts +109 -0
  45. package/src/plugin/internal/source/NativeSourcePackages.ts +103 -43
  46. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +42 -0
  47. package/src/plugin/internal/source/PluginBuildEnvironmentWitness.ts +18 -8
  48. package/src/plugin/internal/source/SourcePluginWorkspace.ts +143 -25
  49. package/src/plugin/internal/source/buildSourcePlugin.ts +62 -34
  50. package/src/plugin/internal/source/computeCacheKey.ts +26 -8
  51. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +8 -76
  52. package/src/plugin/internal/source/pluginModuleReplaceDirectories.ts +13 -25
@@ -17,8 +17,8 @@ import { spawnGoTool } from "./spawnGoTool";
17
17
  /**
18
18
  * Observe Go-selected packages under the native builder's workspace policy.
19
19
  *
20
- * Metadata does not compile dependencies or certify a later build. A cold
21
- * build repeats ownership admission in its proven, materialized workspace.
20
+ * Metadata does not compile dependencies or certify a later build. A cold build
21
+ * repeats ownership admission in its proven, materialized workspace.
22
22
  *
23
23
  * @evidence contracts/common.md#principled-implementation The selected Go tool, effective environment, shared workspace writer and actual copied package inputs determine package names and errors; Go remains the build-constraint and package-validity authority.
24
24
  * @evidence contracts/common.md#clear-and-simple-design One namespace separates contextual observation, metadata framing and ownership admission; source-only contributions are observed inside their selected host module.
@@ -99,7 +99,10 @@ export namespace NativeSourcePackages {
99
99
  if (opts.proposal === true) {
100
100
  // Only root manifests change. Relative Go inputs retain the source
101
101
  // module layout, while its irrelevant graph/toolchain stays inactive.
102
- const hostModule = resolvePluginGoModule(genericHostSource(), opts.pluginName).moduleRoot;
102
+ const hostModule = resolvePluginGoModule(
103
+ genericHostSource(),
104
+ opts.pluginName,
105
+ ).moduleRoot;
103
106
  for (const name of ["go.mod", "go.sum"]) {
104
107
  const manifest = path.join(hostModule, name);
105
108
  const target = path.join(scratch, name);
@@ -110,19 +113,28 @@ export namespace NativeSourcePackages {
110
113
  for (const input of opts.packages) {
111
114
  if (input.source === undefined) continue;
112
115
  const contributor = { source: input.source, name: input.name! };
113
- SourcePluginAdmission.requireContributorPackage(opts.pluginName, contributor);
116
+ SourcePluginAdmission.requireContributorPackage(
117
+ opts.pluginName,
118
+ contributor,
119
+ );
114
120
  const target = path.resolve(scratch, input.entry);
115
- if (path.relative(scratch, target).startsWith("..") || target === scratch)
121
+ if (
122
+ path.relative(scratch, target).startsWith("..") ||
123
+ target === scratch
124
+ )
116
125
  throw new Error("ttsc: invalid package selection target");
117
126
  if (fs.existsSync(target))
118
- throw new Error(`ttsc: package selection target already exists: ${target}`);
127
+ throw new Error(
128
+ `ttsc: package selection target already exists: ${target}`,
129
+ );
119
130
  SourcePluginWorkspace.materialize(input.source, target);
120
131
  }
121
- let reader = opts.readers?.get(goBinary);
122
- if (reader === undefined) {
123
- reader = SourcePluginWorkspace.createGoModReader(goBinary, opts.pluginName, env);
124
- opts.readers?.set(goBinary, reader);
125
- }
132
+ const reader = SourcePluginWorkspace.createGoModReader(
133
+ goBinary,
134
+ opts.pluginName,
135
+ env,
136
+ opts.readers,
137
+ );
126
138
  SourcePluginWorkspace.writeGoWork(
127
139
  scratch,
128
140
  SourcePluginWorkspace.findTtscOverlayDirs(),
@@ -142,9 +154,13 @@ export namespace NativeSourcePackages {
142
154
  taskFailure = { error };
143
155
  throw error;
144
156
  } finally {
145
- SourceNativeRetirement.releaseResource(scratch, () => {
146
- fs.rmSync(scratch, { recursive: true, force: true });
147
- }, taskFailure);
157
+ SourceNativeRetirement.releaseResource(
158
+ scratch,
159
+ () => {
160
+ fs.rmSync(scratch, { recursive: true, force: true });
161
+ },
162
+ taskFailure,
163
+ );
148
164
  }
149
165
  }
150
166
 
@@ -182,7 +198,9 @@ export namespace NativeSourcePackages {
182
198
  readers,
183
199
  packages: group.map(({ entry }) => ({ entry })),
184
200
  });
185
- group.forEach(({ index }, offset) => { output[index] = observations[offset]!; });
201
+ group.forEach(({ index }, offset) => {
202
+ output[index] = observations[offset]!;
203
+ });
186
204
  }
187
205
  return output;
188
206
  }
@@ -190,11 +208,11 @@ export namespace NativeSourcePackages {
190
208
  /**
191
209
  * Propose ownership with the candidate-selected tool before final admission.
192
210
  *
193
- * Nested transform sources retain their admitted source-module layout but
194
- * use unchanged generic host root manifests in scratch. Their outer module
195
- * graph and toolchain are not source-only build authority. Module-root and
196
- * check sources retain their actual owning module. Executable proposals must
197
- * agree in the actual own module; linked proposals in each chosen actual host.
211
+ * Nested transform sources retain their admitted source-module layout but use
212
+ * unchanged generic host root manifests in scratch. Their outer module graph
213
+ * and toolchain are not source-only build authority. Module-root and check
214
+ * sources retain their actual owning module. Executable proposals must agree
215
+ * in the actual own module; linked proposals in each chosen actual host.
198
216
  *
199
217
  * @evidence contracts/common.md#principled-implementation Candidate tool selection occurs at the original owning module and effective environment; only prospective nested source-only manifests change in owned scratch before Go interprets the normal generated workspace.
200
218
  * @evidence contracts/common.md#clear-and-simple-design One grouping pass shares each module/proposal context and returns whether actual own-module observation already occurred.
@@ -210,7 +228,10 @@ export namespace NativeSourcePackages {
210
228
  env: NodeJS.ProcessEnv,
211
229
  readers = new Map<string, SourcePluginWorkspace.GoModReader>(),
212
230
  ): Array<{ observation: Package; ownModule: boolean }> {
213
- const groups = new Map<string, Array<{ index: number; entry: string; ownModule: boolean }>>();
231
+ const groups = new Map<
232
+ string,
233
+ Array<{ index: number; entry: string; ownModule: boolean }>
234
+ >();
214
235
  inputs.forEach((input, index) => {
215
236
  const target = resolvePluginGoModule(input.source, input.label);
216
237
  const ownModule = input.ownModule === true || target.entry === ".";
@@ -219,11 +240,15 @@ export namespace NativeSourcePackages {
219
240
  group.push({ index, entry: target.entry, ownModule });
220
241
  groups.set(key, group);
221
242
  });
222
- const output: Array<{ observation: Package; ownModule: boolean }> = new Array(inputs.length);
243
+ const output: Array<{ observation: Package; ownModule: boolean }> =
244
+ new Array(inputs.length);
223
245
  for (const group of groups.values()) {
224
246
  const first = inputs[group[0]!.index]!;
225
247
  const observations = inspect({
226
- source: first.source, pluginName: first.label, env, readers,
248
+ source: first.source,
249
+ pluginName: first.label,
250
+ env,
251
+ readers,
227
252
  proposal: !group[0]!.ownModule,
228
253
  packages: group.map(({ entry }) => ({ entry })),
229
254
  });
@@ -258,21 +283,38 @@ export namespace NativeSourcePackages {
258
283
  OwnedSynchronousProcess.checkpoint();
259
284
  const result = spawnGoTool(
260
285
  opts.goBinary,
261
- ["list", ...GoSourceInputs.BUILD_FLAGS, "-find", "-e", "-json=Dir,Name,GoFiles,CgoFiles,Error", ...entries],
286
+ [
287
+ "list",
288
+ ...GoSourceInputs.BUILD_FLAGS,
289
+ "-find",
290
+ "-e",
291
+ "-json=Dir,Name,GoFiles,CgoFiles,Error",
292
+ ...entries,
293
+ ],
262
294
  { cwd: opts.cwd, encoding: "utf8", env: opts.env, windowsHide: true },
263
295
  );
264
296
  OwnedSynchronousProcess.checkpoint();
265
297
  if (result.error !== undefined)
266
- throw new Error(SourcePluginWorkspace.goSpawnFailureMessage(
267
- `selecting Go packages for plugin "${opts.pluginName}"`,
268
- opts.pluginName, opts.goBinary, opts.cwd, result.error,
269
- ));
298
+ throw new Error(
299
+ SourcePluginWorkspace.goSpawnFailureMessage(
300
+ `selecting Go packages for plugin "${opts.pluginName}"`,
301
+ opts.pluginName,
302
+ opts.goBinary,
303
+ opts.cwd,
304
+ result.error,
305
+ ),
306
+ );
270
307
  if (result.status !== 0)
271
- throw new Error(`ttsc: selecting Go packages for plugin "${opts.pluginName}" failed:\n${result.stderr || result.stdout}`);
308
+ throw new Error(
309
+ `ttsc: selecting Go packages for plugin "${opts.pluginName}" failed:\n${result.stderr || result.stdout}`,
310
+ );
272
311
  const packages = parse(result.stdout);
273
- const selected = new Map(packages.map((input) => [
274
- input.Dir === undefined ? undefined : fs.realpathSync.native(input.Dir), input,
275
- ]));
312
+ const selected = new Map(
313
+ packages.map((input) => [
314
+ input.Dir === undefined ? undefined : fs.realpathSync.native(input.Dir),
315
+ input,
316
+ ]),
317
+ );
276
318
  return opts.entries.map((entry) => {
277
319
  const dir = fs.realpathSync.native(path.resolve(opts.cwd, entry));
278
320
  const input = selected.get(dir);
@@ -300,12 +342,21 @@ export namespace NativeSourcePackages {
300
342
  expected?: "executable" | "linked",
301
343
  ): "executable" | "linked" {
302
344
  if (input.Error !== undefined)
303
- throw new Error(`ttsc: plugin "${label}" Go package selection failed: ${input.Error.Err}`);
304
- if (input.Name === undefined || (input.GoFiles?.length ?? 0) + (input.CgoFiles?.length ?? 0) === 0)
305
- throw new Error(`ttsc: plugin "${label}" source has no Go-selected production files: ${input.Dir ?? "unknown package"}`);
345
+ throw new Error(
346
+ `ttsc: plugin "${label}" Go package selection failed: ${input.Error.Err}`,
347
+ );
348
+ if (
349
+ input.Name === undefined ||
350
+ (input.GoFiles?.length ?? 0) + (input.CgoFiles?.length ?? 0) === 0
351
+ )
352
+ throw new Error(
353
+ `ttsc: plugin "${label}" source has no Go-selected production files: ${input.Dir ?? "unknown package"}`,
354
+ );
306
355
  const actual = input.Name === "main" ? "executable" : "linked";
307
356
  if (expected !== undefined && actual !== expected)
308
- throw new Error(`ttsc: plugin "${label}" Go package ownership disagrees with its proposal: selected ${actual}, expected ${expected} (${input.Dir ?? "unknown package"})`);
357
+ throw new Error(
358
+ `ttsc: plugin "${label}" Go package ownership disagrees with its proposal: selected ${actual}, expected ${expected} (${input.Dir ?? "unknown package"})`,
359
+ );
309
360
  return actual;
310
361
  }
311
362
 
@@ -331,7 +382,8 @@ export namespace NativeSourcePackages {
331
382
  const token = output[offset]!;
332
383
  if (start < 0) {
333
384
  if (/\s/.test(token)) continue;
334
- if (token !== "{") throw new Error("ttsc: invalid Go package metadata stream");
385
+ if (token !== "{")
386
+ throw new Error("ttsc: invalid Go package metadata stream");
335
387
  start = offset;
336
388
  depth = 1;
337
389
  continue;
@@ -347,17 +399,25 @@ export namespace NativeSourcePackages {
347
399
  if (typeof input !== "object" || input === null || Array.isArray(input))
348
400
  throw new Error("ttsc: invalid Go package metadata object");
349
401
  const record = input as Package;
350
- if ((record.Dir !== undefined && typeof record.Dir !== "string") ||
351
- (record.Name !== undefined && typeof record.Name !== "string") ||
352
- [record.GoFiles, record.CgoFiles].some((files) => files !== undefined &&
353
- (!Array.isArray(files) || files.some((file) => typeof file !== "string"))) ||
354
- (record.Error !== undefined && (record.Error === null || typeof record.Error.Err !== "string")))
402
+ if (
403
+ (record.Dir !== undefined && typeof record.Dir !== "string") ||
404
+ (record.Name !== undefined && typeof record.Name !== "string") ||
405
+ [record.GoFiles, record.CgoFiles].some(
406
+ (files) =>
407
+ files !== undefined &&
408
+ (!Array.isArray(files) ||
409
+ files.some((file) => typeof file !== "string")),
410
+ ) ||
411
+ (record.Error !== undefined &&
412
+ (record.Error === null || typeof record.Error.Err !== "string"))
413
+ )
355
414
  throw new Error("ttsc: invalid Go package metadata fields");
356
415
  packages.push(record);
357
416
  start = -1;
358
417
  }
359
418
  }
360
- if (start >= 0) throw new Error("ttsc: incomplete Go package metadata stream");
419
+ if (start >= 0)
420
+ throw new Error("ttsc: incomplete Go package metadata stream");
361
421
  return packages;
362
422
  }
363
423
  }
@@ -74,6 +74,48 @@ export namespace PluginBuildEnvironmentReadings {
74
74
  : undefined;
75
75
  }
76
76
 
77
+ /**
78
+ * Qualify a batch in one synchronous observation transaction. Shared SDK
79
+ * paths and the complete variable identity are read once, even when several
80
+ * plugin directories use them. Missing readings remain explicitly missing.
81
+ *
82
+ * WARNING (#1712): this map belongs only to the following synchronous proof.
83
+ * Never retain it across an await, delivery, build boundary or publication. A
84
+ * process-permanent positive witness would miss changed Go tools and GOENV
85
+ * files (#1516); independently checking it in every helper instead multiplies
86
+ * a thousands-file GOROOT scan by modules and proof layers.
87
+ *
88
+ * @evidence contracts/common.md#principled-implementation One current variable snapshot and native path observation are compared against each reading's own expected witness; conflicting expected states cannot both pass.
89
+ * @evidence contracts/common.md#clear-and-simple-design One batch operation owns shared validation without changing synchronous reads or native worker publication boundaries.
90
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Undefined is retained for absent or stale authority; no positive verdict survives this explicit observation transaction.
91
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs state scope, missing semantics and the performance/correctness regressions prevented by the lifetime restriction.
92
+ * @evidence contracts/portability.md#os-neutral-implementation Existing native witness keys preserve filesystem, link and ambient-variable distinctions; no platform case assumption is added.
93
+ * @evidence contracts/performance.md#efficient-algorithms Full variables are indexed once; each distinct witnessed path is queried once while each reading compares its own witness entries. Work is proportional to variable bytes, total witness entries and unique native dependencies rather than repeated native SDK scans.
94
+ * @evidence contracts/performance.md#reuse-equivalent-work Only current native observations within this synchronous batch are shared; the returned digests do not authorize reuse after another asynchronous boundary.
95
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The native observation map is call-local and the digest map transfers to its immediate proof caller; no handle or historical registry is acquired.
96
+ */
97
+ export function cachedAll(
98
+ directories: Iterable<string>,
99
+ ): ReadonlyMap<string, string | undefined> {
100
+ let variables: string | undefined;
101
+ const current = new Map<string, string>();
102
+ const result = new Map<string, string | undefined>();
103
+ for (const directory of directories) {
104
+ if (result.has(directory)) continue;
105
+ const known = readings.get(
106
+ directory + (variables ??= key("", process.env)),
107
+ );
108
+ result.set(
109
+ directory,
110
+ known !== undefined &&
111
+ PluginBuildEnvironmentWitness.holds(known.witness, undefined, current)
112
+ ? known.environment
113
+ : undefined,
114
+ );
115
+ }
116
+ return result;
117
+ }
118
+
77
119
  /**
78
120
  * Read synchronously for clients whose API owns synchronous native work.
79
121
  * Explicit refresh preserves the original fresh-comparison semantics.
@@ -144,7 +144,9 @@ export namespace PluginBuildEnvironmentWitness {
144
144
  * observation. The existing private opt-in trace retains the first actual
145
145
  * mismatch without repeating its lookup or changing the refusal. An optional
146
146
  * observation label separates preparation and build epochs in that trace; it
147
- * does not change validation or authorize publication.
147
+ * does not change validation or authorize publication. A supplied native
148
+ * observation map belongs to one synchronous batch only; never reuse it after
149
+ * an await, delivery, environment mutation or publication boundary.
148
150
  *
149
151
  * @evidence contracts/common.md#principled-implementation Universal comparison requires every dependency to match its pre-read signature and immediately rejects a refused or changed path.
150
152
  * @evidence contracts/common.md#clear-and-simple-design Validation uses the same signature helper as capture, keeping identity and timestamp policy in one place.
@@ -153,16 +155,24 @@ export namespace PluginBuildEnvironmentWitness {
153
155
  * @evidence contracts/portability.md#os-neutral-implementation Validation reads actual Node metadata using the same link-following semantics as capture.
154
156
  * @evidence contracts/performance.md#efficient-algorithms Validation iterates dependencies until mismatch and performs native stat/link queries or ambient-variable serialization. Windows environment matching enumerates names and lowercases text per witnessed variable, so P alone does not bound that work. Native path/link/name/value/numeric bytes contribute cost; file content is not read and unavailable native observations share the helper's missing marker. An enabled private trace serializes the first mismatch's already-computed text through its existing bounded sink.
155
157
  *
156
- * @evidenceExclude contracts/performance.md#reuse-equivalent-work Each call must establish current validity; caching that answer would conceal external changes.
158
+ * @evidence contracts/performance.md#reuse-equivalent-work A caller-owned synchronous batch may share current native observations, comparing each witness against its own expected state; ordinary calls and all later observation phases establish new validity.
157
159
  * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Validation retains no handles or additional historical state.
158
160
  */
159
- export function holds(witness: Record, observation?: string): boolean {
161
+ export function holds(
162
+ witness: Record,
163
+ observation?: string,
164
+ currentReadings?: Map<string, string>,
165
+ ): boolean {
160
166
  for (const [file, recorded] of witness) {
161
- const current = file.startsWith(ENVIRONMENT_PREFIX)
162
- ? environmentSignature(file.slice(ENVIRONMENT_PREFIX.length))
163
- : file.startsWith(LINK_PREFIX)
164
- ? linkSignature(file.slice(LINK_PREFIX.length))
165
- : signature(file);
167
+ let current = currentReadings?.get(file);
168
+ if (current === undefined) {
169
+ current = file.startsWith(ENVIRONMENT_PREFIX)
170
+ ? environmentSignature(file.slice(ENVIRONMENT_PREFIX.length))
171
+ : file.startsWith(LINK_PREFIX)
172
+ ? linkSignature(file.slice(LINK_PREFIX.length))
173
+ : signature(file);
174
+ currentReadings?.set(file, current);
175
+ }
166
176
  if (current !== recorded) {
167
177
  E2ETrace.capabilityResolution(
168
178
  "plugin-build-environment-witness-mismatch",
@@ -3,9 +3,11 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
5
  import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProcess";
6
+ import { GoEnvironmentReading } from "./GoEnvironmentReading";
6
7
  import { GoSourceInputs } from "./GoSourceInputs";
7
- import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
8
+ import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
8
9
  import { SourcePluginAdmission } from "./SourcePluginAdmission";
10
+ import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
9
11
  import { formatGoWorkPath } from "./formatGoWorkPath";
10
12
  import { spawnGoTool } from "./spawnGoTool";
11
13
 
@@ -13,20 +15,23 @@ import { spawnGoTool } from "./spawnGoTool";
13
15
  * Share generated Go workspace policy between package selection and building.
14
16
  *
15
17
  * Package selection must use the builder's overlays, generated workspace and
16
- * selected tool rather than an ancestor go.work found in a source directory.
17
- * A load may share installed-overlay manifest records under its pinned tool
18
- * and effective environment; each unique scratch is read independently. The
19
- * cold build owns a fresh reader over its proven materialized inputs. Go
20
- * supplies the workspace's required Go and toolchain directives. These reads
21
- * preserve the existing installation/version and sequential-stability limits.
18
+ * selected tool rather than an ancestor go.work found in a source directory. A
19
+ * load shares early replacement watch, package and build manifest records under
20
+ * its pinned tool and effective environment; identical current bytes in
21
+ * independently proven scratch inputs share their Go parse. A moved native tool
22
+ * clears that reader's syntax memo, and a toolchain retry starts a new reader
23
+ * scope. Each caller has a diagnostic view over the shared native reader state;
24
+ * one plugin cannot inherit another plugin's failure label (#1720). Go supplies
25
+ * the workspace's required Go and toolchain directives. These reads preserve
26
+ * the existing installation/version and sequential-stability limits.
22
27
  *
23
28
  * @evidence contracts/common.md#principled-implementation Shared manifest acquisition, managed replacement admission, overlay filtering and Go-owned workspace version selection keep metadata and compilation under the same workspace policy; callers own source materialization and effective environment.
24
29
  * @evidence contracts/common.md#clear-and-simple-design One namespace owns the generated workspace and its module reader, overlay discovery and Go launch diagnostics; no second manifest parser or build-tag interpreter is introduced.
25
30
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Actual Go mod edit and work use commands interpret manifests; selected tool errors propagate and no static workspace version replaces Go's decision.
26
31
  * @evidence contracts/common.md#meaningful-documentation Native prose explains shared policy and caller ownership; private helpers remain part of this operation's review surface.
27
32
  * @evidence contracts/portability.md#os-neutral-implementation Native fs/path operations preserve path spelling and formatGoWorkPath owns Go syntax quoting; subprocesses use the existing native wrapper owner with hidden windows.
28
- * @evidence contracts/performance.md#efficient-algorithms Workspace construction scans module and replacement populations; each module reader memoizes absolute directory results within its owning load or materialized build. Overlay discovery recursively scans admitted directories and sorts paths; native Go output and JSON parsing scale with complete bytes.
29
- * @evidence contracts/performance.md#reuse-equivalent-work A metadata load shares equivalent installed-overlay observations under one pinned tool/effective environment; unique scratch paths are independent. A materialized build uses its own reader, and no memo crosses a load or changed environment. Metadata and build share policy rather than pretending their independently materialized sources are identical.
33
+ * @evidence contracts/performance.md#efficient-algorithms Workspace construction scans module and replacement populations; each reader reads current manifest bytes and memoizes equivalent Go syntax within its pinned native tool/load. Overlay discovery recursively scans admitted directories and sorts paths; native Go output and JSON parsing scale with complete bytes.
34
+ * @evidence contracts/performance.md#reuse-equivalent-work Metadata and materialized builds share Go parsing only for identical current manifest bytes under the same pinned tool/effective environment. Independent materialization/content proof remains with each caller; tool movement clears syntax reuse and retries start another scope.
30
35
  * @evidence contracts/performance.md#bound-retention-and-release-resources Readers retain module records only for their enclosing load or materialized build; command capture cleanup belongs to spawnGoTool and scratch lifetime belongs to the caller. Native commands can mutate Go-owned state and their selected-child completion does not prove arbitrary descendants retired.
31
36
  */
32
37
  export namespace SourcePluginWorkspace {
@@ -55,7 +60,8 @@ export namespace SourcePluginWorkspace {
55
60
  }
56
61
 
57
62
  /**
58
- * Write the native builder's generated workspace and let Go select its version.
63
+ * Write the native builder's generated workspace and let Go select its
64
+ * version.
59
65
  *
60
66
  * @evidence contracts/common.md#principled-implementation Actual manifest records drive managed overlay filtering and replacement admission; Go work use supplies required directives.
61
67
  * @evidence contracts/common.md#clear-and-simple-design The writer shares one reader across source/overlay queries and delegates manifest interpretation to Go.
@@ -173,6 +179,7 @@ export namespace SourcePluginWorkspace {
173
179
  interface GoModInfo {
174
180
  readonly modulePath: string | null;
175
181
  readonly replacements: readonly GoModReplacement[];
182
+ readonly directives: NonNullable<GoModJson["Replace"]>;
176
183
  }
177
184
 
178
185
  /**
@@ -190,17 +197,19 @@ export namespace SourcePluginWorkspace {
190
197
  export interface GoModReader {
191
198
  /**
192
199
  * Observe the module at this native directory within the reader context.
200
+ * The optional diagnostic owner overrides the creating caller's label only;
201
+ * it changes no selected-tool, environment or byte-content authority.
193
202
  *
194
203
  * @evidence contracts/common.md#principled-implementation The signature associates a native directory with the selected reader's Go-owned module and replacement observation.
195
- * @evidence contracts/common.md#clear-and-simple-design One input and one record expose the manifest policy needed by the workspace writer.
204
+ * @evidence contracts/common.md#clear-and-simple-design A directory and optional diagnostic label expose manifest policy without treating the label as native cache authority.
196
205
  * @evidence contracts/common.md#prohibited-implementation-shortcuts The signature supplies no parser, alternate selection policy or stability certificate.
197
- * @evidence contracts/common.md#meaningful-documentation Native prose states that the result belongs to the reader context rather than a context-independent directory cache.
206
+ * @evidence contracts/common.md#meaningful-documentation Native prose states selected-context result ownership and the optional caller label, which changes failure attribution only.
198
207
  * @evidence contracts/portability.md#os-neutral-implementation The directory is a native filesystem path; its resolution belongs to the implementing reader.
199
208
  * @evidence contracts/performance.md#efficient-algorithms A method signature executes no algorithm; createGoModReader owns acquisition and parsing.
200
209
  * @evidence contracts/performance.md#reuse-equivalent-work Implementations supply observations within their selected tool/environment scope; this signature establishes no cross-context memo.
201
210
  * @evidence contracts/performance.md#bound-retention-and-release-resources The signature acquires no resource; record retention belongs to the reader's enclosing load or materialized build.
202
211
  */
203
- read(dir: string): GoModInfo;
212
+ read(dir: string, diagnosticPlugin?: string): GoModInfo;
204
213
  }
205
214
 
206
215
  interface GoModJson {
@@ -224,36 +233,144 @@ export namespace SourcePluginWorkspace {
224
233
  }
225
234
 
226
235
  /**
227
- * Read each module once through Go within one selected-tool context.
236
+ * Read current manifest bytes and share identical Go parses within one pinned
237
+ * selected-tool/load context, including copied scratch modules. An optional
238
+ * registry shares native reader state from the first watch query onward; its
239
+ * owner must keep one fixed effective environment and discard it on retry.
240
+ * Caller views share that native state and syntax, while each default
241
+ * diagnostic label remains with its current caller.
228
242
  *
229
- * @evidence contracts/common.md#principled-implementation Actual go mod edit JSON determines module/replacement identities; missing go.mod returns the existing empty module observation.
230
- * @evidence contracts/common.md#clear-and-simple-design An absolute-directory map wraps one manifest acquisition helper.
243
+ * @evidence contracts/common.md#principled-implementation Actual go mod edit JSON determines module/replacement identities; current bytes, selected/dispatched-tool metadata and Go's environment-file witness qualify reuse under the existing metadata-distinguishability premise. Environment values are copied at reader creation. Missing go.mod returns the existing empty module observation, and moving tool/manifest inputs cannot publish a parsed record.
244
+ * @evidence contracts/common.md#clear-and-simple-design One factory acquires load-owned native reader state by selected tool; a thin caller view forwards its diagnostic label without duplicating the byte-content map. Current reads and post-parse byte/tool comparisons qualify insertion.
231
245
  * @evidence contracts/common.md#prohibited-implementation-shortcuts No custom Go manifest parser or global context-insensitive memo is introduced.
232
246
  * @evidence contracts/common.md#meaningful-documentation Native prose states this operation's authority and delegated boundaries; fields retain separate native comments.
233
247
  * @evidence contracts/portability.md#os-neutral-implementation Native path resolution and temporary cwd avoid deep Windows working-directory limits; the selected tool handles manifest syntax.
234
- * @evidence contracts/performance.md#efficient-algorithms Each distinct directory runs one existence query and at most one Go mod edit plus complete JSON parsing.
235
- * @evidence contracts/performance.md#reuse-equivalent-work The caller may share this reader only within one load and the same pinned tool/effective environment; directory results are not cross-load authority.
236
- * @evidence contracts/performance.md#bound-retention-and-release-resources The map grows with distinct observed directories for its caller's lifetime; process/output capture cleanup remains delegated.
248
+ * @evidence contracts/performance.md#efficient-algorithms Reader acquisition performs a selected-tool registry lookup; every nonmissing read qualifies tool/environment metadata and reads current manifest bytes. A cold or stale witness first observes the selected launcher; an unavailable or nonfile launcher skips optional environment acquisition and permits only the actual uncached parse attempt. An observed file lends its same pre-read stat to the witness before acquiring current Go values and GOENV evidence through the common reader, without a GOROOT walk. Only complete native GOENV/GOROOT locations qualify syntax sharing; a dispatched tool is witnessed before parsing. Unavailable native environment acquisition disables syntax sharing while preserving actual parse success or failure. Each distinct byte string that earns stable successful syntax in that tool epoch runs Go mod edit and complete JSON parsing once, then rechecks bytes/tool metadata before caching; copied manifests share that parse. Failed queries remain uncached and report the current caller's diagnostic label.
249
+ * @evidence contracts/performance.md#reuse-equivalent-work The caller may share this reader only within one load and the same pinned tool/effective environment; parsed syntax is not cross-load or native containment authority. Caller views share native state while preserving failure attribution; diagnostic labels are not syntax cache inputs.
250
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The map retains distinct manifest bytes and parsed syntax for its caller's load/build lifetime; process/output capture cleanup remains delegated.
237
251
  */
238
252
  export function createGoModReader(
239
253
  goBinary: string,
240
254
  pluginName: string,
241
255
  env: NodeJS.ProcessEnv,
256
+ readers?: Map<string, GoModReader>,
242
257
  ): GoModReader {
258
+ const existing = readers?.get(goBinary);
259
+ if (existing !== undefined)
260
+ return {
261
+ read: (dir, diagnosticPlugin = pluginName) =>
262
+ existing.read(dir, diagnosticPlugin),
263
+ };
264
+ // WARNING (#1712): parse identical bytes once in this pinned tool/load
265
+ // context, including early replacement watch projection and copied scratch
266
+ // manifests. The optional registry belongs to one fixed effective environment
267
+ // and one load; it is never shared with a toolchain retry. Always read current bytes;
268
+ // a pathname or metadata-only memo misses manifest edits. Never promote
269
+ // this map to a process cache: tool/environment epochs change Go authority.
270
+ // Fixed variable strings do not freeze GOENV or a dispatched Go executable.
271
+ // Requalify their actual native evidence before every syntax hit; missing
272
+ // environment authority permits a fresh parse only, never positive reuse.
273
+ // WARNING (#1720): diagnostic names are per caller, not a syntax/cache input.
274
+ // Shared views must forward their current label to every failed native read.
243
275
  const cache = new Map<string, GoModInfo>();
244
- return {
245
- read(dir) {
276
+ const environment = { ...env };
277
+ let toolWitness: PluginBuildEnvironmentWitness.Record | undefined;
278
+ const reader: GoModReader = {
279
+ read(dir, diagnosticPlugin = pluginName) {
246
280
  OwnedSynchronousProcess.checkpoint();
247
281
  const resolved = path.resolve(dir);
248
- const cached = cache.get(resolved);
282
+ const manifest = path.join(resolved, "go.mod");
283
+ let bytes: Buffer;
284
+ try {
285
+ bytes = fs.readFileSync(manifest);
286
+ } catch (error) {
287
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
288
+ return emptyGoModInfo();
289
+ throw error;
290
+ }
291
+ if (
292
+ toolWitness === undefined ||
293
+ !PluginBuildEnvironmentWitness.holds(toolWitness)
294
+ ) {
295
+ cache.clear();
296
+ const witness: PluginBuildEnvironmentWitness.Record = new Map();
297
+ let launcher: fs.BigIntStats | undefined;
298
+ try {
299
+ launcher = fs.statSync(goBinary, { bigint: true });
300
+ } catch {
301
+ // Unavailable metadata cannot qualify optional syntax reuse.
302
+ }
303
+ if (launcher?.isFile() === true)
304
+ PluginBuildEnvironmentWitness.add(witness, goBinary, launcher);
305
+ // GOENV can change toolchain selection without changing env strings.
306
+ // Share its existing native discovery owner; do not scan GOROOT here.
307
+ // Do not launch a missing/unobservable tool twice just to learn that
308
+ // optional cache qualification is unavailable (#1712). The actual
309
+ // mod-edit attempt below still owns its current failure or success.
310
+ const observed =
311
+ launcher?.isFile() === true
312
+ ? GoEnvironmentReading.read(
313
+ goBinary,
314
+ os.tmpdir(),
315
+ environment,
316
+ ["GOROOT"],
317
+ witness,
318
+ )
319
+ : undefined;
320
+ if (
321
+ typeof observed?.GOROOT === "string" &&
322
+ path.isAbsolute(observed.GOROOT)
323
+ )
324
+ PluginBuildEnvironmentWitness.add(
325
+ witness,
326
+ path.join(
327
+ observed.GOROOT,
328
+ "bin",
329
+ process.platform === "win32" ? "go.exe" : "go",
330
+ ),
331
+ );
332
+ const complete =
333
+ typeof observed?.GOROOT === "string" &&
334
+ path.isAbsolute(observed.GOROOT) &&
335
+ typeof observed.GOENV === "string" &&
336
+ (observed.GOENV === "off" || path.isAbsolute(observed.GOENV));
337
+ toolWitness =
338
+ complete && PluginBuildEnvironmentWitness.holds(witness)
339
+ ? witness
340
+ : undefined;
341
+ }
342
+ // UTF-8 decoding is lossy for malformed input. Only identical bytes
343
+ // can share Go syntax, even when Go serializes two inputs identically.
344
+ const byteKey = bytes.toString("base64");
345
+ const cached = cache.get(byteKey);
249
346
  if (cached !== undefined) {
250
347
  return cached;
251
348
  }
252
- const info = readGoModInfo(resolved, goBinary, pluginName, env);
253
- cache.set(resolved, info);
349
+ const info = readGoModInfo(
350
+ resolved,
351
+ goBinary,
352
+ diagnosticPlugin,
353
+ environment,
354
+ );
355
+ if (!fs.readFileSync(manifest).equals(bytes))
356
+ throw new Error(
357
+ `ttsc: go.mod changed while Go was reading ${manifest}`,
358
+ );
359
+ if (
360
+ toolWitness !== undefined &&
361
+ !PluginBuildEnvironmentWitness.holds(toolWitness)
362
+ )
363
+ throw new Error(
364
+ `ttsc: Go tool or environment changed while reading ${manifest}`,
365
+ );
366
+ // An unavailable environment observation can still execute a fresh Go
367
+ // parse, but it cannot authorize sharing that result on another call.
368
+ if (toolWitness !== undefined) cache.set(byteKey, info);
254
369
  return info;
255
370
  },
256
371
  };
372
+ readers?.set(goBinary, reader);
373
+ return reader;
257
374
  }
258
375
 
259
376
  function readGoModInfo(
@@ -313,6 +430,7 @@ export namespace SourcePluginWorkspace {
313
430
 
314
431
  return {
315
432
  modulePath: json.Module?.Path ?? null,
433
+ directives: json.Replace ?? [],
316
434
  replacements: (json.Replace ?? [])
317
435
  .map(jsonReplacementToGoModReplacement)
318
436
  .filter((replacement) => replacement !== null),
@@ -322,6 +440,7 @@ export namespace SourcePluginWorkspace {
322
440
  function emptyGoModInfo(): GoModInfo {
323
441
  return {
324
442
  modulePath: null,
443
+ directives: [],
325
444
  replacements: [],
326
445
  };
327
446
  }
@@ -441,6 +560,5 @@ export namespace SourcePluginWorkspace {
441
560
  }
442
561
  }
443
562
 
444
-
445
563
  const TTSC_GO_MODULE_PATH = "github.com/samchon/ttsc/packages/ttsc";
446
564
  }