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
@@ -23,12 +23,12 @@ import { pluginDescriptorFailureReason } from "../pluginDescriptorFailureReason"
23
23
  import { pluginDescriptorProcessFailure } from "../pluginDescriptorProcessFailure";
24
24
  import { NativeSourcePackages } from "../source/NativeSourcePackages";
25
25
  import { SourcePluginWorkspace } from "../source/SourcePluginWorkspace";
26
- import { resolvePluginGoModule } from "../source/resolvePluginGoModule";
27
26
  import { buildSourcePlugin } from "../source/buildSourcePlugin";
28
27
  import { isPathWithin } from "../source/isPathWithin";
29
28
  import { pluginBuildVersions } from "../source/pluginBuildVersions";
30
29
  import { pluginModuleReplaceDirectories } from "../source/pluginModuleReplaceDirectories";
31
30
  import { pluginSourceState } from "../source/pluginSourceState";
31
+ import { resolvePluginGoModule } from "../source/resolvePluginGoModule";
32
32
  import { COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE } from "./COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE";
33
33
  import { PLUGIN_DESCRIPTOR_SHIM_SOURCE } from "./PLUGIN_DESCRIPTOR_SHIM_SOURCE";
34
34
  import { PluginDescriptorAdmission } from "./PluginDescriptorAdmission";
@@ -61,19 +61,18 @@ import { visitImportMappedCandidates } from "./visitImportMappedCandidates";
61
61
  * under generic host manifests while preserving source-relative input layout.
62
62
  * Main proposals must agree in their full owning module; linked proposals must
63
63
  * agree inside every selected transform host. Errors or disagreement terminate
64
- * the load rather than changing the proposed kind.
65
- * Watch callers first receive structurally selected source/module/contributor
66
- * repair interests, before metadata can reject them. Valid records then narrow
67
- * those interests to the exact build directories; interests alone never admit
68
- * a plugin, certify a kind or supply reusable source-state proof.
69
- * Returns the ordered native plugins, parsed project config, recorded
70
- * JavaScript-host inputs and unresolved selection candidates, and the keyed
71
- * state of reported Go source directories supplied to the builds
72
- * (`pluginSources`): each plugin's module root and each contributor's source,
73
- * with its state (`pluginSourceState`), the sources as the build read them
74
- * together with the environment a build there is keyed on. Directories within
75
- * ttsc's installed package, including its overlays and the fallback
76
- * linked-plugin host, are keyed but omitted from this report under the
64
+ * the load rather than changing the proposed kind. Watch callers first receive
65
+ * structurally selected source/module/contributor repair interests, before
66
+ * metadata can reject them. Valid records then narrow those interests to the
67
+ * exact build directories; interests alone never admit a plugin, certify a kind
68
+ * or supply reusable source-state proof. Returns the ordered native plugins,
69
+ * parsed project config, recorded JavaScript-host inputs and unresolved
70
+ * selection candidates, and the keyed state of reported Go source directories
71
+ * supplied to the builds (`pluginSources`): each plugin's module root and each
72
+ * contributor's source, with its state (`pluginSourceState`), the sources as
73
+ * the build read them together with the environment a build there is keyed on.
74
+ * Directories within ttsc's installed package, including its overlays and the
75
+ * fallback linked-plugin host, are keyed but omitted from this report under the
77
76
  * installed-package/version ownership policy. That policy does not prove the
78
77
  * installation cannot be edited in place. Descriptor completeness relies on the
79
78
  * runtime recorder's status and the descriptor's explicit external-read
@@ -329,30 +328,41 @@ export function loadProjectPlugins(options: {
329
328
  const { ttsc: ttscVersion, tsgo: tsgoVersion } = pluginBuildVersions(
330
329
  context.projectRoot,
331
330
  );
332
- const candidates = plugins.map((plugin, index) => {
333
- const stage = PluginDescriptorAdmission.stage(plugin);
334
- validatePluginSource(plugin);
335
- const contributors = validatePluginContributors(plugin);
336
- const source = resolvePluginSource(plugin.source, context.projectRoot);
337
- const label = pluginLabel(plugin, entries[index]!.config, index);
338
- requirePluginSource(source, label);
339
- return { plugin, index, stage, contributors, source, label };
340
- }).map((candidate) => ({
341
- ...candidate,
342
- ...resolvePluginGoModule(candidate.source, candidate.label),
343
- }));
344
- const watchReplacements = options.onWatchInputs === undefined
345
- ? undefined
346
- : new Map<string, readonly string[]>();
331
+ const candidates = plugins
332
+ .map((plugin, index) => {
333
+ const stage = PluginDescriptorAdmission.stage(plugin);
334
+ validatePluginSource(plugin);
335
+ const contributors = validatePluginContributors(plugin);
336
+ const source = resolvePluginSource(plugin.source, context.projectRoot);
337
+ const label = pluginLabel(plugin, entries[index]!.config, index);
338
+ requirePluginSource(source, label);
339
+ return { plugin, index, stage, contributors, source, label };
340
+ })
341
+ .map((candidate) => ({
342
+ ...candidate,
343
+ ...resolvePluginGoModule(candidate.source, candidate.label),
344
+ }));
345
+ // One fixed-effective-environment load owns manifest syntax sharing from the
346
+ // first watch query through metadata and builds. Do not allocate this after
347
+ // watch projection: that silently repeats selected-Go work (#1712).
348
+ const packageReaders = new Map<string, SourcePluginWorkspace.GoModReader>();
349
+ const watchReplacements =
350
+ options.onWatchInputs === undefined
351
+ ? undefined
352
+ : new Map<string, readonly string[]>();
347
353
  if (options.onWatchInputs !== undefined) {
348
354
  // The prospective context reads the admitted module layout and anchors its
349
355
  // tool there before kind exists. Keep both possible final-root baselines:
350
356
  // a linked package keeps packageDir, and an executable keeps moduleRoot.
351
- const interests = new Set(candidates.flatMap((candidate) => [
352
- candidate.moduleRoot,
353
- candidate.packageDir,
354
- ...(candidate.contributors ?? []).map((input) => input.source),
355
- ]).filter(reportsPluginSource));
357
+ const interests = new Set(
358
+ candidates
359
+ .flatMap((candidate) => [
360
+ candidate.moduleRoot,
361
+ candidate.packageDir,
362
+ ...(candidate.contributors ?? []).map((input) => input.source),
363
+ ])
364
+ .filter(reportsPluginSource),
365
+ );
356
366
  options.onWatchInputs([...interests].sort());
357
367
  const initialSize = interests.size;
358
368
  for (const candidate of candidates) {
@@ -360,77 +370,102 @@ export function loadProjectPlugins(options: {
360
370
  // activate their irrelevant module graph just to discover watch inputs.
361
371
  if (candidate.stage === "transform" && candidate.entry !== ".") continue;
362
372
  for (const directory of pluginReplacementWatchDirectories(
363
- candidate.moduleRoot, effectiveEnv, watchReplacements,
364
- )) if (reportsPluginSource(directory)) interests.add(directory);
373
+ candidate.moduleRoot,
374
+ effectiveEnv,
375
+ watchReplacements,
376
+ packageReaders,
377
+ ))
378
+ if (reportsPluginSource(directory)) interests.add(directory);
365
379
  }
366
380
  if (interests.size !== initialSize)
367
381
  options.onWatchInputs([...interests].sort());
368
382
  }
369
- const packageReaders = new Map<string, SourcePluginWorkspace.GoModReader>();
370
383
  const proposals = NativeSourcePackages.propose(
371
384
  candidates.map((candidate) => ({
372
- ...candidate, ownModule: candidate.stage !== "transform",
373
- })), effectiveEnv, packageReaders,
385
+ ...candidate,
386
+ ownModule: candidate.stage !== "transform",
387
+ })),
388
+ effectiveEnv,
389
+ packageReaders,
374
390
  );
375
- const records = candidates.map(({ plugin, index, stage, contributors, source, packageDir }) => {
376
- const { kind, moduleRoot } = resolveNativeSource(source, plugin, entries[index]!.config, index, {
377
- env: effectiveEnv, observation: proposals[index]!.observation,
378
- });
379
- if (kind === "linked" && stage !== "transform") {
380
- throw new Error(
381
- `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
391
+ const records = candidates.map(
392
+ ({ plugin, index, stage, contributors, source, packageDir }) => {
393
+ const { kind, moduleRoot } = resolveNativeSource(
394
+ source,
395
+ plugin,
396
+ entries[index]!.config,
397
+ index,
398
+ {
399
+ env: effectiveEnv,
400
+ observation: proposals[index]!.observation,
401
+ },
382
402
  );
383
- }
384
- const linkedContributorName =
385
- kind === "linked"
386
- ? `linked_${String(index).padStart(6, "0")}`
387
- : undefined;
388
- const hostInputs = validatePluginHostInputs(plugin, index);
389
- const pluginHostInputHashes = validatePluginHostInputHashes(
390
- plugin,
391
- index,
392
- hostInputs,
393
- );
394
- const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
395
- plugin,
396
- index,
397
- hostInputs,
398
- );
399
- return {
400
- capabilities: plugin.capabilities,
401
- contributors,
402
- config: entries[index]!.config,
403
- kind,
404
- label: pluginLabel(plugin, entries[index]!.config, index),
405
- linkedContributorName,
406
- name: plugin.name,
407
- reportsTypeScriptDiagnostics:
408
- plugin.reportsTypeScriptDiagnostics === true,
409
- request: loadedEntries[index]!.request,
410
- hostInputHashes: mergePluginHostInputHashes(
411
- loadedEntries[index]!.hostInputHashes,
412
- pluginHostInputHashes,
413
- loadedEntries[index]!.hostInputs,
403
+ if (kind === "linked" && stage !== "transform") {
404
+ throw new Error(
405
+ `ttsc: plugin "${pluginLabel(plugin, entries[index]!.config, index)}" source is a linked Go package, but only transform-stage plugins can be linked into a compiler host`,
406
+ );
407
+ }
408
+ const linkedContributorName =
409
+ kind === "linked"
410
+ ? `linked_${String(index).padStart(6, "0")}`
411
+ : undefined;
412
+ const hostInputs = validatePluginHostInputs(plugin, index);
413
+ const pluginHostInputHashes = validatePluginHostInputHashes(
414
+ plugin,
415
+ index,
414
416
  hostInputs,
415
- ),
416
- hostInputRealpaths: mergePluginHostInputHashes(
417
- loadedEntries[index]!.hostInputRealpaths,
418
- pluginHostInputRealpaths,
419
- loadedEntries[index]!.hostInputs,
417
+ );
418
+ const pluginHostInputRealpaths = validatePluginHostInputRealpaths(
419
+ plugin,
420
+ index,
420
421
  hostInputs,
421
- ),
422
- hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
423
- moduleRoot,
424
- packageDir,
425
- source,
426
- stage,
427
- };
428
- });
429
- const executableCandidates = candidates.filter((candidate) =>
430
- records[candidate.index]!.kind === "executable" && !proposals[candidate.index]!.ownModule);
422
+ );
423
+ return {
424
+ capabilities: plugin.capabilities,
425
+ contributors,
426
+ config: entries[index]!.config,
427
+ kind,
428
+ label: pluginLabel(plugin, entries[index]!.config, index),
429
+ linkedContributorName,
430
+ name: plugin.name,
431
+ reportsTypeScriptDiagnostics:
432
+ plugin.reportsTypeScriptDiagnostics === true,
433
+ request: loadedEntries[index]!.request,
434
+ hostInputHashes: mergePluginHostInputHashes(
435
+ loadedEntries[index]!.hostInputHashes,
436
+ pluginHostInputHashes,
437
+ loadedEntries[index]!.hostInputs,
438
+ hostInputs,
439
+ ),
440
+ hostInputRealpaths: mergePluginHostInputHashes(
441
+ loadedEntries[index]!.hostInputRealpaths,
442
+ pluginHostInputRealpaths,
443
+ loadedEntries[index]!.hostInputs,
444
+ hostInputs,
445
+ ),
446
+ hostInputs: [...loadedEntries[index]!.hostInputs, ...hostInputs],
447
+ moduleRoot,
448
+ packageDir,
449
+ source,
450
+ stage,
451
+ };
452
+ },
453
+ );
454
+ const executableCandidates = candidates.filter(
455
+ (candidate) =>
456
+ records[candidate.index]!.kind === "executable" &&
457
+ !proposals[candidate.index]!.ownModule,
458
+ );
431
459
  // Admission can now narrow the provisional interests without restamping the
432
460
  // retained package/module baselines. Final host errors still keep these roots.
433
- options.onWatchInputs?.(pluginBuildDirectories(records, effectiveEnv, watchReplacements));
461
+ options.onWatchInputs?.(
462
+ pluginBuildDirectories(
463
+ records,
464
+ effectiveEnv,
465
+ watchReplacements,
466
+ packageReaders,
467
+ ),
468
+ );
434
469
  const linkedContributors = records
435
470
  .filter((record) => record.stage === "transform")
436
471
  .flatMap((record) =>
@@ -448,59 +483,96 @@ export function loadProjectPlugins(options: {
448
483
  // a second executable host can select a genuinely different Go context.
449
484
  const admittedExecutables = new Set<string>();
450
485
  if (linkedContributors.length !== 0) {
451
- const hosts = transformHosts.length === 0
452
- ? [{ source: path.join(ttscPackageRoot(), "cmd", "utility-host"), label: "linked-plugin-host" }]
453
- : transformHosts;
454
- const contexts = new Map<string, { source: string; label: string; records: typeof transformHosts }>();
486
+ const hosts =
487
+ transformHosts.length === 0
488
+ ? [
489
+ {
490
+ source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
491
+ label: "linked-plugin-host",
492
+ },
493
+ ]
494
+ : transformHosts;
495
+ const contexts = new Map<
496
+ string,
497
+ { source: string; label: string; records: typeof transformHosts }
498
+ >();
455
499
  for (const host of hosts) {
456
- const moduleRoot = resolvePluginGoModule(host.source, host.label).moduleRoot;
500
+ const moduleRoot = resolvePluginGoModule(
501
+ host.source,
502
+ host.label,
503
+ ).moduleRoot;
457
504
  let context = contexts.get(moduleRoot);
458
505
  if (context === undefined) {
459
506
  context = { source: host.source, label: host.label, records: [] };
460
507
  contexts.set(moduleRoot, context);
461
508
  }
462
509
  }
463
- for (const record of transformHosts) contexts.get(record.moduleRoot)!.records.push(record);
510
+ for (const record of transformHosts)
511
+ contexts.get(record.moduleRoot)!.records.push(record);
464
512
  for (const host of contexts.values()) {
465
513
  const moduleHosts = host.records;
466
514
  const hostEntries = moduleHosts.map((record) => ({
467
515
  entry: resolvePluginGoModule(record.source, record.label).entry,
468
516
  }));
469
517
  const observations = NativeSourcePackages.inspect({
470
- source: host.source, pluginName: host.label, env: effectiveEnv,
518
+ source: host.source,
519
+ pluginName: host.label,
520
+ env: effectiveEnv,
471
521
  readers: packageReaders,
472
522
  packages: [
473
523
  ...hostEntries,
474
524
  ...linkedContributors.map((input) => ({
475
- ...input, entry: `./contrib/${input.name}`,
525
+ ...input,
526
+ entry: `./contrib/${input.name}`,
476
527
  })),
477
528
  ],
478
529
  });
479
530
  moduleHosts.forEach((record, index) => {
480
- NativeSourcePackages.kind(observations[index]!, record.label, "executable");
531
+ NativeSourcePackages.kind(
532
+ observations[index]!,
533
+ record.label,
534
+ "executable",
535
+ );
481
536
  admittedExecutables.add(record.source);
482
537
  });
483
538
  linkedContributors.forEach((input, index) => {
484
- NativeSourcePackages.kind(observations[hostEntries.length + index]!, `${input.name} inside host ${host.label}`, "linked");
539
+ NativeSourcePackages.kind(
540
+ observations[hostEntries.length + index]!,
541
+ `${input.name} inside host ${host.label}`,
542
+ "linked",
543
+ );
485
544
  });
486
545
  }
487
546
  }
488
547
  // A nested main proposal still needs its actual owning manifests. Hosts
489
548
  // with linked contributors were admitted together above; other equivalent
490
549
  // entries share one standalone observation, with no repeated host copy.
491
- const standaloneCandidates = executableCandidates.filter((candidate) =>
492
- !admittedExecutables.has(candidate.source));
550
+ const standaloneCandidates = executableCandidates.filter(
551
+ (candidate) => !admittedExecutables.has(candidate.source),
552
+ );
493
553
  const executablePackages = NativeSourcePackages.ownPackages(
494
- standaloneCandidates, effectiveEnv, packageReaders,
554
+ standaloneCandidates,
555
+ effectiveEnv,
556
+ packageReaders,
495
557
  );
496
558
  standaloneCandidates.forEach((candidate, index) => {
497
- NativeSourcePackages.kind(executablePackages[index]!, candidate.label, "executable");
559
+ NativeSourcePackages.kind(
560
+ executablePackages[index]!,
561
+ candidate.label,
562
+ "executable",
563
+ );
498
564
  });
499
565
  const packageOwnership = (source: string, label: string, linked: boolean) => [
500
- { entry: resolvePluginGoModule(source, label).entry, kind: "executable" as const },
501
- ...(linked ? linkedContributors.map((input) => ({
502
- entry: `./contrib/${input.name}`, kind: "linked" as const,
503
- })) : []),
566
+ {
567
+ entry: resolvePluginGoModule(source, label).entry,
568
+ kind: "executable" as const,
569
+ },
570
+ ...(linked
571
+ ? linkedContributors.map((input) => ({
572
+ entry: `./contrib/${input.name}`,
573
+ kind: "linked" as const,
574
+ }))
575
+ : []),
504
576
  ];
505
577
  // One reading of each source directory, shared by every build below and
506
578
  // reported as the state the binaries were keyed on.
@@ -517,6 +589,7 @@ export function loadProjectPlugins(options: {
517
589
  cacheDir: options.cacheDir,
518
590
  contributors: mergeContributors(record.contributors, hostContributors),
519
591
  env: effectiveEnv,
592
+ goModReaders: packageReaders,
520
593
  pluginName: record.label,
521
594
  packageOwnership: packageOwnership(record.source, record.label, true),
522
595
  source: record.source,
@@ -534,11 +607,14 @@ export function loadProjectPlugins(options: {
534
607
  cacheDir: options.cacheDir,
535
608
  contributors: linkedContributors,
536
609
  env: effectiveEnv,
610
+ goModReaders: packageReaders,
537
611
  label: "linked plugin host",
538
612
  pluginName: "linked-plugin-host",
539
613
  source: path.join(ttscPackageRoot(), "cmd", "utility-host"),
540
614
  packageOwnership: packageOwnership(
541
- path.join(ttscPackageRoot(), "cmd", "utility-host"), "linked-plugin-host", true,
615
+ path.join(ttscPackageRoot(), "cmd", "utility-host"),
616
+ "linked-plugin-host",
617
+ true,
542
618
  ),
543
619
  environmentDigests,
544
620
  sourceDigests,
@@ -561,8 +637,13 @@ export function loadProjectPlugins(options: {
561
637
  cacheDir: options.cacheDir,
562
638
  contributors: record.contributors,
563
639
  env: effectiveEnv,
640
+ goModReaders: packageReaders,
564
641
  pluginName: record.label,
565
- packageOwnership: packageOwnership(record.source, record.label, false),
642
+ packageOwnership: packageOwnership(
643
+ record.source,
644
+ record.label,
645
+ false,
646
+ ),
566
647
  source: record.source,
567
648
  environmentDigests,
568
649
  sourceDigests,
@@ -2136,6 +2217,7 @@ function pluginBuildDirectories(
2136
2217
  }[],
2137
2218
  env: NodeJS.ProcessEnv,
2138
2219
  replacements?: Map<string, readonly string[]>,
2220
+ readers?: Map<string, SourcePluginWorkspace.GoModReader>,
2139
2221
  ): string[] {
2140
2222
  const directories = new Set<string>();
2141
2223
  for (const record of records) {
@@ -2146,8 +2228,12 @@ function pluginBuildDirectories(
2146
2228
  );
2147
2229
  if (record.kind === "executable") {
2148
2230
  for (const directory of pluginReplacementWatchDirectories(
2149
- record.moduleRoot, env, replacements,
2150
- )) directories.add(directory);
2231
+ record.moduleRoot,
2232
+ env,
2233
+ replacements,
2234
+ readers,
2235
+ ))
2236
+ directories.add(directory);
2151
2237
  }
2152
2238
  for (const contributor of record.contributors ?? [])
2153
2239
  directories.add(path.resolve(contributor.source));
@@ -2164,13 +2250,19 @@ function pluginReplacementWatchDirectories(
2164
2250
  moduleRoot: string,
2165
2251
  env: NodeJS.ProcessEnv,
2166
2252
  observed?: Map<string, readonly string[]>,
2253
+ readers?: Map<string, SourcePluginWorkspace.GoModReader>,
2167
2254
  ): readonly string[] {
2168
2255
  const previous = observed?.get(moduleRoot);
2169
2256
  if (previous !== undefined) return previous;
2170
2257
  let directories: readonly string[];
2171
2258
  try {
2172
- directories = pluginModuleReplaceDirectories(moduleRoot, env)
2173
- .map((replacement) => replacement.directory);
2259
+ directories = pluginModuleReplaceDirectories(
2260
+ moduleRoot,
2261
+ env,
2262
+ undefined,
2263
+ undefined,
2264
+ readers,
2265
+ ).map((replacement) => replacement.directory);
2174
2266
  } catch {
2175
2267
  // An unreadable manifest/tool is not valid metadata. The already observed
2176
2268
  // module retains its repair path; actual proposal/build refusal propagates.
@@ -0,0 +1,109 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { GoSourceInputs } from "./GoSourceInputs";
4
+ import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
5
+ import { spawnGoTool } from "./spawnGoTool";
6
+
7
+ /**
8
+ * Acquire current Go values together with their environment-file evidence.
9
+ * Build digests and syntax readers share this owner; a file-location hint is
10
+ * never a retained positive environment verdict (#1516, #1712).
11
+ *
12
+ * @evidence contracts/common.md#principled-implementation The acquisition pairs actual Go output with native pre-read file evidence; callers retain their own completeness and reuse policy.
13
+ * @evidence contracts/common.md#clear-and-simple-design One namespace owns environment-file discovery and native acquisition for build and manifest clients.
14
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts No handwritten default GOENV path or process-permanent environment success replaces native Go.
15
+ * @evidence contracts/common.md#meaningful-documentation Prose distinguishes observation from a discovery hint and identifies both consumers.
16
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation The read member owns native observation and execution.
17
+ * @evidenceExclude contracts/performance.md#efficient-algorithms The read member owns acquisition and identity costs.
18
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work The read member owns hint sharing; no result cache is exposed.
19
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The read member describes the private hint map and delegated native lifetime.
20
+ */
21
+ export namespace GoEnvironmentReading {
22
+ /**
23
+ * The Go environment file each Go tool and environment was last seen to name,
24
+ * which lets a reading witness that file before `go env` reads it.
25
+ */
26
+ const goEnvironmentFiles = new Map<string, string | null>();
27
+
28
+ /**
29
+ * Run `go env -json` for the requested keys and `GOENV`, the file `go env -w`
30
+ * writes, witnessing that file before the run reads it.
31
+ *
32
+ * The file decides the reading without being part of it, so only its metadata
33
+ * is kept, and it must be taken before the read: metadata taken after would
34
+ * describe an edit that landed between the two, and a reading of the old
35
+ * content would pass as proven. Which file `go env` reads is known only from
36
+ * its answer, so the file this Go tool and environment named last is
37
+ * witnessed first, and a reading that names another is taken again with that
38
+ * one witnessed. A file that keeps moving between runs leaves the reading
39
+ * unwitnessable, so a kept reading or a build keyed on it never holds.
40
+ *
41
+ * @returns The parsed object, or `undefined` when native acquisition or JSON
42
+ * object decoding failed.
43
+ * @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.
44
+ * @evidence contracts/common.md#clear-and-simple-design One acquisition serves build hashing and manifest syntax qualification; callers choose keys and own interpretation and cache admission.
45
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts A remembered file name is only a discovery hint. Every call executes Go, and unresolved movement marks its witness unproven rather than lending a historical positive verdict.
46
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain why file discovery can repeat and distinguish unavailable acquisition from current authority.
47
+ * @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
+ * @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.
51
+ */
52
+ export function read(
53
+ goBinary: string,
54
+ cwd: string,
55
+ env: NodeJS.ProcessEnv,
56
+ keys: readonly string[],
57
+ witness: PluginBuildEnvironmentWitness.Record | undefined,
58
+ ): Record<string, unknown> | undefined {
59
+ const memo = crypto.createHash("sha256").update(goBinary);
60
+ for (const [key, value] of Object.entries(env).sort(([left], [right]) =>
61
+ left < right ? -1 : left > right ? 1 : 0,
62
+ )) {
63
+ if (value !== undefined)
64
+ memo.update(`\0${key.length}:${key}${value.length}:${value}`);
65
+ }
66
+ const memoKey = memo.digest("hex");
67
+ let named = goEnvironmentFiles.get(memoKey);
68
+ for (let attempt = 0; attempt < 3; attempt += 1) {
69
+ if (named !== undefined && named !== null)
70
+ PluginBuildEnvironmentWitness.add(witness, named);
71
+ const result = spawnGoTool(goBinary, ["env", "-json", ...keys, "GOENV"], {
72
+ cwd,
73
+ encoding: "utf8",
74
+ env: GoSourceInputs.goBuildEnv(goBinary, undefined, env),
75
+ windowsHide: true,
76
+ });
77
+ if (result.error !== undefined || result.status !== 0) return undefined;
78
+ let parsed: Record<string, unknown>;
79
+ try {
80
+ const decoded: unknown = JSON.parse(result.stdout);
81
+ if (
82
+ decoded === null ||
83
+ typeof decoded !== "object" ||
84
+ Array.isArray(decoded)
85
+ )
86
+ return undefined;
87
+ parsed = decoded as Record<string, unknown>;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ const reported =
92
+ typeof parsed.GOENV === "string" &&
93
+ parsed.GOENV !== "" &&
94
+ parsed.GOENV !== "off"
95
+ ? parsed.GOENV
96
+ : null;
97
+ if (reported === named) return parsed;
98
+ if (named !== undefined && named !== null) witness?.delete(named);
99
+ named = reported;
100
+ goEnvironmentFiles.set(memoKey, named);
101
+ // No file to witness: the reading depends on none.
102
+ if (named === null) return parsed;
103
+ if (witness === undefined) return parsed;
104
+ }
105
+ if (named !== undefined && named !== null)
106
+ PluginBuildEnvironmentWitness.refuse(witness, named);
107
+ return undefined;
108
+ }
109
+ }