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
@@ -8,13 +8,12 @@ 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";
@@ -22,6 +21,7 @@ import { PluginBuildLockProtocol } from "./PluginBuildLockProtocol";
22
21
  import { SourceBuildCacheLayout } from "./SourceBuildCacheLayout";
23
22
  import type { SourceBuildFilesystemOperations } from "./SourceBuildFilesystemOperations";
24
23
  import { SourcePluginAdmission } from "./SourcePluginAdmission";
24
+ import { SourcePluginWorkspace } from "./SourcePluginWorkspace";
25
25
  import { acquirePluginBuildLock } from "./acquirePluginBuildLock";
26
26
  import { computeCacheKey } from "./computeCacheKey";
27
27
  import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
@@ -44,12 +44,12 @@ import { withGoBuildCacheLease } from "./withGoBuildCacheLease";
44
44
  * Build one Go source plugin into a cached executable.
45
45
  *
46
46
  * `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.
47
+ * inherited and constructor layers using native environment-name identity so a
48
+ * programmatic `TtscCompiler` instance can pin its own Go toolchain
49
+ * (`TTSC_GO_BINARY`), Go build cache (`TTSC_GO_CACHE_DIR`), and Go build
50
+ * variables (`GOFLAGS`, `CGO_*`, …) without mutating the shared `process.env`.
51
+ * CLI callers omit it and inherit `process.env`, so ambient behavior is
52
+ * unchanged.
53
53
  *
54
54
  * The cache key includes source and toolchain readings. Before publication the
55
55
  * scratch inputs are compared with their recorded digests and the external
@@ -65,16 +65,16 @@ import { withGoBuildCacheLease } from "./withGoBuildCacheLease";
65
65
  * Optional loader-required package ownership enters the key. Its cold producer
66
66
  * admits the entry and linked sources through Go metadata in the actual
67
67
  * 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.
68
+ * different key. Existing binary hits trust the cache producer and key rather
69
+ * than rehashing executable bytes. Default caches are managed locally, while
70
+ * explicit roots retain caller-managed pruning policy. Every returned cache key
71
+ * registers a reader token retained by this process until exit; registration
72
+ * shares the builder/collector lease, and other consumers register independent
73
+ * readers. Failure to establish ownership propagates. Private opt-in tracing
74
+ * records the mandatory key-creation and pre-build witness gates and adds one
75
+ * diagnostic observation after Go returns. That diagnostic read adds metadata
76
+ * work, not publication permission. A discarded epoch is separately recorded
77
+ * when the bounded build owner starts again.
78
78
  *
79
79
  * Explicit worker scopes check cancellation between build phases, copied source
80
80
  * entries and native commands, including reader admission and final
@@ -101,7 +101,10 @@ export function buildSourcePlugin(opts: {
101
101
  env?: NodeJS.ProcessEnv;
102
102
 
103
103
  /** Loader-required ownership of the entry and named linked packages. */
104
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
104
+ packageOwnership?: readonly {
105
+ entry: string;
106
+ kind: "executable" | "linked";
107
+ }[];
105
108
 
106
109
  /**
107
110
  * Digests of the environment each build directory is keyed on, shared by
@@ -123,6 +126,9 @@ export function buildSourcePlugin(opts: {
123
126
  */
124
127
  sourceDigests?: Map<string, string>;
125
128
 
129
+ /** Pinned-tool manifest readers shared only by this loader request. */
130
+ goModReaders?: Map<string, SourcePluginWorkspace.GoModReader>;
131
+
126
132
  ttscVersion: string;
127
133
  tsgoVersion: string;
128
134
  }): string {
@@ -136,7 +142,7 @@ export function buildSourcePlugin(opts: {
136
142
  const environmentDigests = new Map(opts.environmentDigests);
137
143
  try {
138
144
  const binary = buildSourcePluginAttempt(
139
- opts,
145
+ attempt === 0 ? opts : { ...opts, goModReaders: undefined },
140
146
  env,
141
147
  sourceDigests,
142
148
  environmentDigests,
@@ -174,7 +180,9 @@ function buildSourcePluginAttempt(
174
180
  ): string {
175
181
  OwnedSynchronousProcess.checkpoint();
176
182
  const { dir, entry, source } = resolveSourceBuildTarget(opts);
177
- const overlayDirs = [...(opts.overlayDirs ?? SourcePluginWorkspace.findTtscOverlayDirs())].sort();
183
+ const overlayDirs = [
184
+ ...(opts.overlayDirs ?? SourcePluginWorkspace.findTtscOverlayDirs()),
185
+ ].sort();
178
186
  const contributors = opts.contributors ?? [];
179
187
  const compiler = resolveGoCompiler(env);
180
188
  const goBinary = GoToolResolution.resolveGoToolForBuild(
@@ -186,6 +194,12 @@ function buildSourcePluginAttempt(
186
194
  OwnedSynchronousProcess.checkpoint();
187
195
  // The digest of every directory the key covers, as the key read it, which
188
196
  // the build proves against what it compiled.
197
+ const goModReader = SourcePluginWorkspace.createGoModReader(
198
+ goBinary,
199
+ opts.pluginName,
200
+ env,
201
+ opts.goModReaders,
202
+ );
189
203
  const environmentWitness: PluginBuildEnvironmentWitness.Record = new Map();
190
204
  const key = computeCacheKey({
191
205
  contributors,
@@ -194,6 +208,7 @@ function buildSourcePluginAttempt(
194
208
  entry,
195
209
  env,
196
210
  environmentWitness,
211
+ goModReader,
197
212
  filesystem: opts.filesystem,
198
213
  goBinary,
199
214
  overlayDirs,
@@ -317,6 +332,7 @@ function buildSourcePluginAttempt(
317
332
  env,
318
333
  environmentWitness,
319
334
  goBinary,
335
+ goModReader,
320
336
  normalizeGoToolPermissions: compiler.bundled,
321
337
  key,
322
338
  keyedDigests: sourceDigests,
@@ -394,7 +410,10 @@ function compileSourcePlugin(opts: {
394
410
  binaryPath: string;
395
411
  cacheDir: string;
396
412
  contributors: readonly ITtscBuildContributor[];
397
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
413
+ packageOwnership?: readonly {
414
+ entry: string;
415
+ kind: "executable" | "linked";
416
+ }[];
398
417
  dir: string;
399
418
  entry: string;
400
419
  env: NodeJS.ProcessEnv;
@@ -406,6 +425,7 @@ function compileSourcePlugin(opts: {
406
425
  goBuildCacheRoot: string;
407
426
  manageGoBuildCache: boolean;
408
427
  normalizeGoToolPermissions: boolean;
428
+ goModReader: SourcePluginWorkspace.GoModReader;
409
429
  key: string;
410
430
 
411
431
  /** The digest of every directory the key covers, as the key read it. */
@@ -461,6 +481,7 @@ function compileSourcePlugin(opts: {
461
481
  opts.dir,
462
482
  opts.env,
463
483
  opts.goBinary,
484
+ opts.goModReader,
464
485
  );
465
486
  OwnedSynchronousProcess.checkpoint();
466
487
  // Every source the build would otherwise read in place, an overlay and
@@ -487,11 +508,7 @@ function compileSourcePlugin(opts: {
487
508
  opts.goBinary,
488
509
  opts.env,
489
510
  );
490
- const goModReader = SourcePluginWorkspace.createGoModReader(
491
- opts.goBinary,
492
- opts.pluginName,
493
- opts.env,
494
- );
511
+ const goModReader = opts.goModReader;
495
512
  OwnedSynchronousProcess.checkpoint();
496
513
  if (opts.contributors.length > 0) {
497
514
  mergeContributors({
@@ -543,12 +560,20 @@ function compileSourcePlugin(opts: {
543
560
  const observations = NativeSourcePackages.read({
544
561
  cwd: scratchDir,
545
562
  entries: opts.packageOwnership.map((input) => input.entry),
546
- env: GoSourceInputs.goBuildEnv(opts.goBinary, goBuildCacheRoot, opts.env),
563
+ env: GoSourceInputs.goBuildEnv(
564
+ opts.goBinary,
565
+ goBuildCacheRoot,
566
+ opts.env,
567
+ ),
547
568
  goBinary: opts.goBinary,
548
569
  pluginName: opts.pluginName,
549
570
  });
550
571
  opts.packageOwnership.forEach((input, index) => {
551
- NativeSourcePackages.kind(observations[index]!, `${opts.pluginName} ${input.entry}`, input.kind);
572
+ NativeSourcePackages.kind(
573
+ observations[index]!,
574
+ `${opts.pluginName} ${input.entry}`,
575
+ input.kind,
576
+ );
552
577
  });
553
578
  }
554
579
  runGoBuild(
@@ -629,10 +654,14 @@ function compileSourcePlugin(opts: {
629
654
  taskFailure = { error };
630
655
  throw error;
631
656
  } finally {
632
- SourceNativeRetirement.releaseResource(scratchDir, () => {
633
- fs.rmSync(scratchDir, { recursive: true, force: true });
634
- SourceNativeRetirement.forget(opts.cacheDir);
635
- }, taskFailure);
657
+ SourceNativeRetirement.releaseResource(
658
+ scratchDir,
659
+ () => {
660
+ fs.rmSync(scratchDir, { recursive: true, force: true });
661
+ SourceNativeRetirement.forget(opts.cacheDir);
662
+ },
663
+ taskFailure,
664
+ );
636
665
  }
637
666
  }
638
667
 
@@ -1009,7 +1038,6 @@ function resolveSourceBuildTarget(opts: {
1009
1038
  return { dir: moduleRoot, entry, source };
1010
1039
  }
1011
1040
 
1012
-
1013
1041
  /**
1014
1042
  * The path, below a build's scratch directory, of the tree holding its copies
1015
1043
  * of the sources outside the module.
@@ -6,6 +6,7 @@ import { GoToolResolution } from "./GoToolResolution";
6
6
  import type { ITtscBuildContributor } from "./ITtscBuildContributor";
7
7
  import type { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
8
8
  import type { SourceBuildFilesystemOperations } from "./SourceBuildFilesystemOperations";
9
+ import type { SourcePluginWorkspace } from "./SourcePluginWorkspace";
9
10
  import { hashPluginBuildEnvironment } from "./hashPluginBuildEnvironment";
10
11
  import { pluginModuleReplaceDirectories } from "./pluginModuleReplaceDirectories";
11
12
  import { pluginSourceDigest } from "./pluginSourceDigest";
@@ -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
@@ -53,7 +54,10 @@ export function computeCacheKey(inputs: {
53
54
  env?: NodeJS.ProcessEnv;
54
55
 
55
56
  /** Required package ownership, admitted by the matching cold producer. */
56
- packageOwnership?: readonly { entry: string; kind: "executable" | "linked" }[];
57
+ packageOwnership?: readonly {
58
+ entry: string;
59
+ kind: "executable" | "linked";
60
+ }[];
57
61
 
58
62
  /**
59
63
  * Digests of the environment each build directory is keyed on
@@ -72,6 +76,7 @@ export function computeCacheKey(inputs: {
72
76
  /** Byte adapter for delegated SDK identity; plugin source digests use fs. */
73
77
  filesystem?: Partial<SourceBuildFilesystemOperations>;
74
78
  goBinary?: string;
79
+ goModReader?: SourcePluginWorkspace.GoModReader;
75
80
  overlayDirs?: readonly string[];
76
81
 
77
82
  /**
@@ -102,11 +107,23 @@ export function computeCacheKey(inputs: {
102
107
  hash.update(JSON.stringify(["platform", process.platform, process.arch]));
103
108
  hash.update(JSON.stringify(["entry", inputs.entry]));
104
109
  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
- ]));
110
+ hash.update(
111
+ JSON.stringify([
112
+ "package-ownership",
113
+ 1,
114
+ [...inputs.packageOwnership].sort((left, right) =>
115
+ left.entry < right.entry
116
+ ? -1
117
+ : left.entry > right.entry
118
+ ? 1
119
+ : left.kind < right.kind
120
+ ? -1
121
+ : left.kind > right.kind
122
+ ? 1
123
+ : 0,
124
+ ),
125
+ ]),
126
+ );
110
127
  // Private materialization changes native package working directories. Keep
111
128
  // binaries produced by the former absolute-ancestry layout out of this
112
129
  // layout's cache admissions, including callers with no SDK overlays.
@@ -139,6 +156,7 @@ export function computeCacheKey(inputs: {
139
156
  inputs.dir,
140
157
  env,
141
158
  goBinary,
159
+ inputs.goModReader,
142
160
  )) {
143
161
  hashSourceDirectory(
144
162
  hash,
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
+ import { GoEnvironmentReading } from "./GoEnvironmentReading";
5
6
  import { GoSourceInputs } from "./GoSourceInputs";
6
7
  import { GoToolResolution } from "./GoToolResolution";
7
8
  import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
@@ -377,7 +378,13 @@ function resolveGoBuildEnvironment(
377
378
  ): Map<string, string> {
378
379
  const values = new Map<string, string>();
379
380
  if (goBinary !== undefined) {
380
- const parsed = readGoEnvironment(goBinary, cwd, env, witness);
381
+ const parsed = GoEnvironmentReading.read(
382
+ goBinary,
383
+ cwd,
384
+ env,
385
+ GO_BUILD_ENV_KEYS,
386
+ witness,
387
+ );
381
388
  if (parsed !== undefined) {
382
389
  try {
383
390
  for (const key of GO_BUILD_ENV_KEYS) {
@@ -408,81 +415,6 @@ function resolveGoBuildEnvironment(
408
415
  return values;
409
416
  }
410
417
 
411
- /**
412
- * The Go environment file each Go tool and environment was last seen to name,
413
- * which lets a reading witness that file before `go env` reads it.
414
- */
415
- const goEnvironmentFiles = new Map<string, string | null>();
416
-
417
- /**
418
- * Run `go env -json` for the build keys and `GOENV`, the file `go env -w`
419
- * writes, witnessing that file before the run reads it.
420
- *
421
- * The file decides the reading without being part of it, so only its metadata
422
- * is kept, and it must be taken before the read: metadata taken after would
423
- * describe an edit that landed between the two, and a reading of the old
424
- * content would pass as proven. Which file `go env` reads is known only from
425
- * its answer, so the file this Go tool and environment named last is witnessed
426
- * first, and a reading that names another is taken again with that one
427
- * witnessed. A file that keeps moving between runs leaves the reading
428
- * unwitnessable, so a kept reading or a build keyed on it never holds.
429
- *
430
- * @returns The parsed answer, or `undefined` when `go env` failed.
431
- */
432
- function readGoEnvironment(
433
- goBinary: string,
434
- cwd: string,
435
- env: NodeJS.ProcessEnv,
436
- witness: PluginBuildEnvironmentWitness.Record | undefined,
437
- ): Record<string, unknown> | undefined {
438
- const memo = crypto.createHash("sha256").update(goBinary);
439
- for (const [key, value] of Object.entries(env).sort(([left], [right]) =>
440
- left < right ? -1 : left > right ? 1 : 0,
441
- )) {
442
- if (value !== undefined)
443
- memo.update(`\0${key.length}:${key}${value.length}:${value}`);
444
- }
445
- const memoKey = memo.digest("hex");
446
- let named = goEnvironmentFiles.get(memoKey);
447
- for (let attempt = 0; attempt < 3; attempt += 1) {
448
- if (named !== undefined && named !== null)
449
- PluginBuildEnvironmentWitness.add(witness, named);
450
- const result = spawnGoTool(
451
- goBinary,
452
- ["env", "-json", ...GO_BUILD_ENV_KEYS, "GOENV"],
453
- {
454
- cwd,
455
- encoding: "utf8",
456
- env: GoSourceInputs.goBuildEnv(goBinary, undefined, env),
457
- windowsHide: true,
458
- },
459
- );
460
- if (result.error !== undefined || result.status !== 0) return undefined;
461
- let parsed: Record<string, unknown>;
462
- try {
463
- parsed = JSON.parse(result.stdout) as Record<string, unknown>;
464
- } catch {
465
- return undefined;
466
- }
467
- const reported =
468
- typeof parsed.GOENV === "string" &&
469
- parsed.GOENV !== "" &&
470
- parsed.GOENV !== "off"
471
- ? parsed.GOENV
472
- : null;
473
- if (reported === named) return parsed;
474
- if (named !== undefined && named !== null) witness?.delete(named);
475
- named = reported;
476
- goEnvironmentFiles.set(memoKey, named);
477
- // No file to witness: the reading depends on none.
478
- if (named === null) return parsed;
479
- if (witness === undefined) return parsed;
480
- }
481
- if (named !== undefined && named !== null)
482
- PluginBuildEnvironmentWitness.refuse(witness, named);
483
- return undefined;
484
- }
485
-
486
418
  function normalizeGoBuildEnvValue(
487
419
  key: string,
488
420
  value: string,
@@ -2,12 +2,11 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { resolvePhysicalPath } from "../../../internal/pathIdentity/resolvePhysicalPath";
5
- import { GoSourceInputs } from "./GoSourceInputs";
6
5
  import { GoToolResolution } from "./GoToolResolution";
7
6
  import type { IPluginModuleReplaceDirectory } from "./IPluginModuleReplaceDirectory";
7
+ import { SourcePluginWorkspace } from "./SourcePluginWorkspace";
8
8
  import { resolveGoCompiler } from "./resolveGoCompiler";
9
9
  import { selectPluginModuleReplaceDirectories } from "./selectPluginModuleReplaceDirectories";
10
- import { spawnGoTool } from "./spawnGoTool";
11
10
 
12
11
  /**
13
12
  * The local directories outside a plugin's Go module that its `go.mod`
@@ -38,19 +37,21 @@ import { spawnGoTool } from "./spawnGoTool";
38
37
  * fails or supplies an unusable JSON result. A no-marker manifest skips Go
39
38
  * parsing here; this is not general go.mod validation.
40
39
  * @evidence contracts/common.md#principled-implementation When the replace marker triggers a query, Go parses its grammar. Unversioned local targets selected outside best-effort root/target spellings are reported, resolving relative paths from the original module; unavailable identity resolution does not certify complete physical containment.
41
- * @evidence contracts/common.md#clear-and-simple-design A cheap no-replace scan avoids an unnecessary subprocess, while Go JSON parsing and physical containment stay in one owning operation.
40
+ * @evidence contracts/common.md#clear-and-simple-design A cheap no-replace scan avoids unnecessary parsing; the common selected-tool/load reader owns Go syntax, and this operation owns current physical containment.
42
41
  * @evidence contracts/common.md#prohibited-implementation-shortcuts The implementation uses Go's supported mod-edit interface instead of a handwritten grammar or special-casing particular dependency names.
43
42
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain external versus internal replacement ownership, original-directory resolution and returned spellings; failure behavior is documented.
44
43
  * @evidence contracts/portability.md#os-neutral-implementation Node path and physical-path resolution determine containment; Go local-path syntax accepts native absolute paths and Windows-relative backslashes only on Windows.
45
- * @evidence contracts/performance.md#efficient-algorithms Full manifest read/UTF-8 marker scan can skip Go parsing. Otherwise native tool/env/capture and full JSON work precede a pass over all parsed directives, not only returned ones; each selected local target uses native best-effort identity resolution, which can traverse ancestors/probe case. Sorting returned module/version strings processes their bytes, with complete manifest/output/path arrays retained transiently and no replacement content digest scan here.
44
+ * @evidence contracts/performance.md#efficient-algorithms Full manifest read/UTF-8 marker scan can skip Go parsing. Otherwise selected-tool reader acquisition and a current manifest read precede a pass over all parsed directives, not only returned ones. Equivalent bytes share Go JSON acquisition only in the supplied fixed-environment load registry; a new syntax record pays native tool/env/capture and full JSON costs; each selected local target uses native best-effort identity resolution, which can traverse ancestors/probe case. Sorting returned module/version strings processes their bytes, with complete manifest/output/path arrays retained transiently and no replacement content digest scan here.
46
45
  *
47
- * @evidenceExclude contracts/performance.md#reuse-equivalent-work This reader establishes the current module directives; callers share resulting source readings in computeCacheKey, not a stale directive memo here.
48
- * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This operation acquires no retained handle or historical registry; spawnGoTool owns synchronous capture and its cleanup attempts, which can fail. Manifest/JSON/native identity contexts are call-owned and selected records transfer to the caller without pinning future directory state.
46
+ * @evidence contracts/performance.md#reuse-equivalent-work The optional pinned-tool/load reader or registry shares Go parsing of identical current bytes with package observation and workspace creation. Physical replacement containment is recomputed every call, never borrowed from that syntax memo.
47
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources This operation acquires no retained handle; the supplied load registry retains syntax readers until its caller releases the load. Without it the reader is call-owned. Native capture cleanup remains delegated and can fail; current physical identity contexts are call-owned and returned directories do not pin future state.
49
48
  */
50
49
  export function pluginModuleReplaceDirectories(
51
50
  moduleRoot: string,
52
51
  env: NodeJS.ProcessEnv,
53
52
  goBinary?: string,
53
+ reader?: SourcePluginWorkspace.GoModReader,
54
+ readers?: Map<string, SourcePluginWorkspace.GoModReader>,
54
55
  ): IPluginModuleReplaceDirectory[] {
55
56
  const root = path.resolve(moduleRoot);
56
57
  let text: string;
@@ -70,27 +71,14 @@ export function pluginModuleReplaceDirectories(
70
71
  env,
71
72
  root,
72
73
  );
73
- const result = spawnGoTool(go, ["mod", "edit", "-json"], {
74
- cwd: root,
75
- encoding: "utf8",
76
- env: GoSourceInputs.goBuildEnv(go, undefined, env),
77
- windowsHide: true,
78
- });
79
- if (result.error !== undefined || result.status !== 0)
80
- throw new Error(
81
- `ttsc: reading ${path.join(root, "go.mod")} failed: ${
82
- result.error?.message ?? (result.stderr || result.stdout)
83
- }`,
84
- );
85
- const parsed = JSON.parse(result.stdout) as {
86
- Replace?: readonly {
87
- New?: { Path?: string; Version?: string };
88
- Old?: { Path?: string; Version?: string };
89
- }[];
90
- };
74
+ const parsed = (
75
+ reader ?? SourcePluginWorkspace.createGoModReader(go, root, env, readers)
76
+ ).read(root);
77
+ // Parsed syntax may be shared; physical containment is current native state
78
+ // and must be recomputed even when these manifest bytes are unchanged.
91
79
  return selectPluginModuleReplaceDirectories(
92
80
  root,
93
- parsed.Replace ?? [],
81
+ parsed.directives,
94
82
  resolvePhysicalPath,
95
83
  );
96
84
  }