ttsc 0.31.2 → 0.31.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ttsc",
3
- "version": "0.31.2",
3
+ "version": "0.31.3",
4
4
  "description": "General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -61,13 +61,13 @@
61
61
  },
62
62
  "optionalDependencies": {
63
63
  "fsevents": "^2.3.3",
64
- "@ttsc/linux-x64": "0.31.2",
65
- "@ttsc/darwin-x64": "0.31.2",
66
- "@ttsc/linux-arm64": "0.31.2",
67
- "@ttsc/win32-x64": "0.31.2",
68
- "@ttsc/win32-arm64": "0.31.2",
69
- "@ttsc/darwin-arm64": "0.31.2",
70
- "@ttsc/linux-arm": "0.31.2"
64
+ "@ttsc/linux-arm": "0.31.3",
65
+ "@ttsc/linux-x64": "0.31.3",
66
+ "@ttsc/linux-arm64": "0.31.3",
67
+ "@ttsc/darwin-arm64": "0.31.3",
68
+ "@ttsc/darwin-x64": "0.31.3",
69
+ "@ttsc/win32-x64": "0.31.3",
70
+ "@ttsc/win32-arm64": "0.31.3"
71
71
  },
72
72
  "sideEffects": [
73
73
  "./lib/register.js",
@@ -12,6 +12,7 @@ import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITt
12
12
  import type { TtscBuildOptions } from "../../../structures/internal/TtscBuildOptions";
13
13
  import type { TtscBuildResult } from "../../../structures/internal/TtscBuildResult";
14
14
  import type { TtscCommonOptions } from "../../../structures/internal/TtscCommonOptions";
15
+ import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
15
16
  import { outputText } from "../outputText";
16
17
  import { readProjectConfig } from "../project/readProjectConfig";
17
18
  import { CompilerProjectSelection } from "../project/CompilerProjectSelection";
@@ -1190,7 +1191,7 @@ export namespace BuildExecution {
1190
1191
  export function resolveExecutionContext(
1191
1192
  options: TtscCommonOptions & {
1192
1193
  emit?: boolean;
1193
- onWatchInputs?: (inputs: readonly string[]) => void;
1194
+ onWatchInputs?: TtscPluginSourceInputsHandler;
1194
1195
  resolvedProject?: ITtscParsedProjectConfig;
1195
1196
  tsconfig?: string;
1196
1197
  },
@@ -1,6 +1,7 @@
1
1
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
2
2
  import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITtscProjectInputSnapshot";
3
3
  import type { TtscBuildOptions } from "../../../structures/internal/TtscBuildOptions";
4
+ import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
4
5
 
5
6
  /**
6
7
  * Options of one {@link runBuild} call: the public build options plus the knobs
@@ -120,7 +121,7 @@ export type RunBuildOptions = TtscBuildOptions & {
120
121
  * The watch launcher uses these roots to invalidate a sidecar when its Go
121
122
  * implementation changes between rebuilds.
122
123
  */
123
- onWatchInputs?: (inputs: readonly string[]) => void;
124
+ onWatchInputs?: TtscPluginSourceInputsHandler;
124
125
 
125
126
  /**
126
127
  * Receives the reconciled project-rule filesystem dependency snapshot. Called
@@ -19,6 +19,7 @@ import { createFilesystemPathIdentityContext } from "../../internal/pathIdentity
19
19
  import { isFilesystemPathIdentityWithin } from "../../internal/pathIdentity/isFilesystemPathIdentityWithin";
20
20
  import { resolvePhysicalPath } from "../../internal/pathIdentity/resolvePhysicalPath";
21
21
  import { resolveSafeCacheCleanupTargets } from "../../internal/resolveSafeCacheCleanupTargets";
22
+ import type { PluginContentIdentities } from "../../plugin/internal/source/PluginContentIdentities";
22
23
  import { SourceBuildCacheLayout } from "../../plugin/internal/source/SourceBuildCacheLayout";
23
24
  import { legacyGlobalCacheTargets } from "../../plugin/internal/source/legacyGlobalCacheTargets";
24
25
  import { resolveCleanTargets } from "../../plugin/internal/source/resolveCleanTargets";
@@ -634,8 +635,11 @@ function runWatch(
634
635
  const invocation = {
635
636
  ...(checkOnly ? { ...options, emit: false } : options),
636
637
  cwd,
637
- onWatchInputs: (inputs: readonly string[]) => {
638
- topology?.setExtraInputs(inputs);
638
+ onWatchInputs: (
639
+ inputs: readonly string[],
640
+ identities?: PluginContentIdentities.Store,
641
+ ) => {
642
+ topology?.setExtraInputs(inputs, identities);
639
643
  },
640
644
  onProjectInputs: (inputs: ITtscProjectInputSnapshot) => {
641
645
  topology?.setProjectInputs(inputs);
@@ -14,9 +14,9 @@ import { type ProjectInputPathIdentityContext } from "../../../internal/pathIden
14
14
  import { createProjectInputPathIdentityContext } from "../../../internal/pathIdentity/createProjectInputPathIdentityContext";
15
15
  import { isProjectInputPathIdentityWithin } from "../../../internal/pathIdentity/isProjectInputPathIdentityWithin";
16
16
  import { resolveProjectInputPath } from "../../../internal/pathIdentity/resolveProjectInputPath";
17
+ import { PluginContentIdentities } from "../../../plugin/internal/source/PluginContentIdentities";
17
18
  import { collectPluginSourceDirectories } from "../../../plugin/internal/source/collectPluginSourceDirectories";
18
19
  import { pluginSourceCovers } from "../../../plugin/internal/source/pluginSourceCovers";
19
- import { pluginSourceDigest } from "../../../plugin/internal/source/pluginSourceDigest";
20
20
  import { prunesPluginSourceDirectory } from "../../../plugin/internal/source/prunesPluginSourceDirectory";
21
21
  import type { ITtscParsedProjectConfig } from "../../../structures/internal/ITtscParsedProjectConfig";
22
22
  import type { ITtscProjectInputSnapshot } from "../../../structures/internal/ITtscProjectInputSnapshot";
@@ -84,6 +84,13 @@ export class WatchTopology {
84
84
  */
85
85
  private pluginInputStates = new Map<string, string>();
86
86
 
87
+ /**
88
+ * The record store of the latest plugin load that reported the inputs, which
89
+ * proves their states from the plugin cache's records (#1728). Absent before
90
+ * a load reports one, or when it opened none; the states are read then.
91
+ */
92
+ private pluginInputIdentities: PluginContentIdentities.Store | undefined;
93
+
87
94
  /**
88
95
  * The plugin inputs notifications named since the last decision, by path key,
89
96
  * each with the locations noted for it (`notePluginNotification`).
@@ -331,17 +338,33 @@ export class WatchTopology {
331
338
  * observation. This does not freeze the source tree or prove every transient
332
339
  * edit was seen.
333
340
  *
341
+ * WARNING (#1728): take each baseline through the reporting load's record
342
+ * store (`identities`), never by reading the sources again. The load proves
343
+ * the same directories from the plugin cache's records a moment later, and a
344
+ * content read here cost every watch session start 0.3-0.6 s for typia's Go
345
+ * module. The store proves only a digest whose metadata still holds and is
346
+ * separable from its reference, so the baseline is the digest a content read
347
+ * would return at this moment.
348
+ *
349
+ * @param inputs Plugin source directories and files the load reported.
350
+ * @param identities The reporting load's record store, if it opened one.
334
351
  * @evidence contracts/common.md#principled-implementation Published plugin inputs define the source corpus, and retaining existing pre-build baselines keeps unreported edits visible.
335
352
  * @evidence contracts/common.md#clear-and-simple-design Deduplication, baseline retention and compiler/watch reconciliation remain ordered phases.
336
353
  * @evidence contracts/common.md#prohibited-implementation-shortcuts The actual plugin source digest applies its owning source-selection and pruning policy; named plugins or fixture output do not replace observed input state.
337
354
  * @evidence contracts/common.md#meaningful-documentation Native prose explains baseline timing and why existing inputs are not restamped following the documentation skill.
338
355
  * @evidence contracts/portability.md#os-neutral-implementation Native path resolution and canonical backend registration govern source trees; lexical keys preserve declarations and unknown names, folding ASCII only under measured insensitive parents rather than an OS-derived rule.
339
- * @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests. Those reads scan the selected pruned corpus, then refresh(false) also pays project/compiler listing, directory traversal and watcher reconciliation; fixed publication count does not bound delegated bytes or backend work.
340
- * @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state.
356
+ * @evidence contracts/performance.md#efficient-algorithms Input/path keys deduplicate with text and native case/ancestor work; only newly admitted inputs compute initial source digests, each proven from the selected files' metadata when the load's records hold and read from the selected pruned corpus otherwise; refresh(false) also pays project/compiler listing, directory traversal and watcher reconciliation; fixed publication count does not bound delegated bytes or backend work.
357
+ * @evidence contracts/performance.md#reuse-equivalent-work Unchanged ordered input sets return immediately; existing inputs reuse their recorded digest until actual notification processing establishes changed consumed state; a new input's digest is the one the load recorded while its metadata holds.
341
358
  * @evidence contracts/performance.md#bound-retention-and-release-resources Publications replace input/digest populations and case-observation caches without a population cap; synchronization attempts obsolete watcher closure. Native/supplied closure has its own lifetime and can throw; owner close does not prove every backend has stopped or every retained snapshot byte was released.
342
359
  */
343
- public setExtraInputs(inputs: readonly string[]): void {
360
+ public setExtraInputs(
361
+ inputs: readonly string[],
362
+ identities?: PluginContentIdentities.Store,
363
+ ): void {
344
364
  this.beginLexicalTransaction();
365
+ // The latest load's store also serves later notifications; its references
366
+ // only grow older, which can make fewer stamps separable but never more.
367
+ this.pluginInputIdentities = identities;
345
368
  const next = uniqueExistingPaths(inputs, this.lexicalIdentities);
346
369
  if (arraysEqual(this.extraInputs, next)) return;
347
370
  this.extraInputs = next;
@@ -354,7 +377,8 @@ export class WatchTopology {
354
377
  const key = this.pathKey(input);
355
378
  states.set(
356
379
  key,
357
- this.pluginInputStates.get(key) ?? pluginInputState(input),
380
+ this.pluginInputStates.get(key) ??
381
+ pluginInputState(input, this.pluginInputIdentities),
358
382
  );
359
383
  }
360
384
  this.pluginInputStates = states;
@@ -1664,7 +1688,7 @@ export class WatchTopology {
1664
1688
  const key = this.pathKey(input);
1665
1689
  const locations = pending.get(key);
1666
1690
  if (locations === undefined) continue;
1667
- const state = pluginInputState(input);
1691
+ const state = pluginInputState(input, this.pluginInputIdentities);
1668
1692
  if (this.pluginInputStates.get(key) === state) continue;
1669
1693
  this.pluginInputStates.set(key, state);
1670
1694
  for (const location of locations) {
@@ -2739,12 +2763,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
2739
2763
  * What a plugin build keys a plugin input on: the digest of the files its build
2740
2764
  * reads from a source directory (`pluginSourceDigest`), or a file's bytes. An
2741
2765
  * input that cannot be read has a state of its own, so the change that made it
2742
- * unreadable is reported and the build names the failure.
2766
+ * unreadable is reported and the build names the failure. A directory's digest
2767
+ * comes from the load's record store while the selected files' metadata holds
2768
+ * there (#1728), and from the files otherwise.
2743
2769
  */
2744
- function pluginInputState(input: string): string {
2770
+ function pluginInputState(
2771
+ input: string,
2772
+ identities: PluginContentIdentities.Store | undefined,
2773
+ ): string {
2745
2774
  try {
2746
2775
  return WatchPaths.isDirectory(input)
2747
- ? `directory:${pluginSourceDigest(input)}`
2776
+ ? `directory:${PluginContentIdentities.sourceDirectory(identities, input)}`
2748
2777
  : `file:${fingerprintProjectInputFile(input)}`;
2749
2778
  } catch (error) {
2750
2779
  return `unreadable:${error instanceof Error ? error.message : String(error)}`;
@@ -130,8 +130,14 @@ export function loadProjectPlugins(options: {
130
130
  */
131
131
  identities?: PluginContentIdentities.Store;
132
132
 
133
- /** Observe plugin build roots before building, including failed builds. */
134
- onWatchInputs?: (inputs: readonly string[]) => void;
133
+ /**
134
+ * Observe plugin build roots before building, including failed builds, with
135
+ * the record store that proves their states (#1728).
136
+ */
137
+ onWatchInputs?: (
138
+ inputs: readonly string[],
139
+ identities?: PluginContentIdentities.Store,
140
+ ) => void;
135
141
 
136
142
  /** Caller-selected anchor for plugin-owned configuration discovery. */
137
143
  pluginConfigDir?: string;
@@ -425,7 +431,10 @@ export function loadProjectPlugins(options: {
425
431
  ])
426
432
  .filter(reportsPluginSource),
427
433
  );
428
- options.onWatchInputs([...interests].sort());
434
+ // WARNING (#1728): hand the watch owner this load's store with the roots.
435
+ // It baselines each root before any build reads it; without the store it
436
+ // read every source the load proves from records a moment later.
437
+ options.onWatchInputs([...interests].sort(), sourceIdentities);
429
438
  const initialSize = interests.size;
430
439
  for (const candidate of candidates) {
431
440
  // Nested source-only proposals replace the outer root manifests. Do not
@@ -440,7 +449,7 @@ export function loadProjectPlugins(options: {
440
449
  if (reportsPluginSource(directory)) interests.add(directory);
441
450
  }
442
451
  if (interests.size !== initialSize)
443
- options.onWatchInputs([...interests].sort());
452
+ options.onWatchInputs([...interests].sort(), sourceIdentities);
444
453
  }
445
454
  const proposals = NativeSourcePackages.propose(
446
455
  candidates.map((candidate) => ({
@@ -529,6 +538,7 @@ export function loadProjectPlugins(options: {
529
538
  watchReplacements,
530
539
  packageReaders,
531
540
  ),
541
+ sourceIdentities,
532
542
  );
533
543
  const linkedContributors = records
534
544
  .filter((record) => record.stage === "transform")
@@ -1,12 +1,15 @@
1
+ import crypto from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
5
 
5
6
  import { OwnedSynchronousProcess } from "../../../internal/OwnedSynchronousProcess";
7
+ import { runtimeExecutableIdentity } from "../../../internal/runtimeExecutableIdentity";
6
8
  import { GoEnvironmentReading } from "./GoEnvironmentReading";
7
9
  import { GoSourceInputs } from "./GoSourceInputs";
8
10
  import { PluginBuildEnvironmentWitness } from "./PluginBuildEnvironmentWitness";
9
- import type { PluginContentIdentities } from "./PluginContentIdentities";
11
+ import { PluginContentIdentities } from "./PluginContentIdentities";
12
+ import { PluginLoadAnswers } from "./PluginLoadAnswers";
10
13
  import { SourcePluginAdmission } from "./SourcePluginAdmission";
11
14
  import { copiesPluginSourceEntry } from "./copiesPluginSourceEntry";
12
15
  import { formatGoWorkPath } from "./formatGoWorkPath";
@@ -239,9 +242,17 @@ export namespace SourcePluginWorkspace {
239
242
  * registry shares native reader state from the first watch query onward; its
240
243
  * owner must keep one fixed effective environment and discard it on retry.
241
244
  * Caller views share that native state and syntax, while each default
242
- * diagnostic label remains with its current caller. An optional record store
243
- * only carries the Go environment-file discovery hint across processes; it
244
- * never carries a parse.
245
+ * diagnostic label remains with its current caller.
246
+ *
247
+ * An optional record store carries the Go environment-file discovery hint
248
+ * across processes, and the parse of identical bytes by the same Go command
249
+ * (#1729): every launch otherwise ran `go mod edit -json` on an unchanged
250
+ * `go.mod`. A recorded parse is keyed by the manifest bytes, the content
251
+ * identities of the launcher and of the `GOROOT/bin/go` the live `go env
252
+ * -json GOROOT` reports, the `GOENV` file's bytes, and every `GO*`/`CGO_*`
253
+ * variable the command receives. It is consulted and written only under the
254
+ * same qualified tool witness that admits in-load sharing, so the live
255
+ * witness query still runs on every load.
245
256
  *
246
257
  * @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.
247
258
  * @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.
@@ -249,7 +260,7 @@ export namespace SourcePluginWorkspace {
249
260
  * @evidence contracts/common.md#meaningful-documentation Native prose states this operation's authority and delegated boundaries; fields retain separate native comments.
250
261
  * @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.
251
262
  * @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.
252
- * @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.
263
+ * @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 never native containment authority. Across loads only a parse recorded under the content identities of the bytes, both Go executables, the GOENV file and the Go variables is reused, and only under a qualified live tool witness. Caller views share native state while preserving failure attribution; diagnostic labels are not syntax cache inputs.
253
264
  * @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.
254
265
  */
255
266
  export function createGoModReader(
@@ -279,6 +290,9 @@ export namespace SourcePluginWorkspace {
279
290
  const cache = new Map<string, GoModInfo>();
280
291
  const environment = { ...env };
281
292
  let toolWitness: PluginBuildEnvironmentWitness.Record | undefined;
293
+ // The tool part of a recorded parse's identity, formed with each qualified
294
+ // witness; undefined without a store or a provable tool.
295
+ let toolIdentity: unknown;
282
296
  const reader: GoModReader = {
283
297
  read(dir, diagnosticPlugin = pluginName) {
284
298
  OwnedSynchronousProcess.checkpoint();
@@ -343,6 +357,16 @@ export namespace SourcePluginWorkspace {
343
357
  complete && PluginBuildEnvironmentWitness.holds(witness)
344
358
  ? witness
345
359
  : undefined;
360
+ toolIdentity =
361
+ toolWitness === undefined || identities === undefined
362
+ ? undefined
363
+ : goModToolIdentity(
364
+ goBinary,
365
+ observed!.GOROOT as string,
366
+ observed!.GOENV as string,
367
+ environment,
368
+ identities,
369
+ );
346
370
  }
347
371
  // UTF-8 decoding is lossy for malformed input. Only identical bytes
348
372
  // can share Go syntax, even when Go serializes two inputs identically.
@@ -351,6 +375,26 @@ export namespace SourcePluginWorkspace {
351
375
  if (cached !== undefined) {
352
376
  return cached;
353
377
  }
378
+ // WARNING (#1729): a new process asks the record store before running
379
+ // Go on bytes this exact command already parsed. Never key this on a
380
+ // path or on the reader's metadata witness alone.
381
+ const answer =
382
+ toolIdentity === undefined
383
+ ? undefined
384
+ : [
385
+ toolIdentity,
386
+ crypto.createHash("sha256").update(bytes).digest("hex"),
387
+ ];
388
+ const recalled =
389
+ answer === undefined
390
+ ? undefined
391
+ : recallGoModInfo(
392
+ PluginLoadAnswers.read(identities, GO_MOD_SYNTAX, answer),
393
+ );
394
+ if (recalled !== undefined) {
395
+ cache.set(byteKey, recalled);
396
+ return recalled;
397
+ }
354
398
  const info = readGoModInfo(
355
399
  resolved,
356
400
  goBinary,
@@ -370,7 +414,14 @@ export namespace SourcePluginWorkspace {
370
414
  );
371
415
  // An unavailable environment observation can still execute a fresh Go
372
416
  // parse, but it cannot authorize sharing that result on another call.
373
- if (toolWitness !== undefined) cache.set(byteKey, info);
417
+ if (toolWitness !== undefined) {
418
+ cache.set(byteKey, info);
419
+ if (answer !== undefined)
420
+ PluginLoadAnswers.write(identities, GO_MOD_SYNTAX, answer, {
421
+ modulePath: info.modulePath,
422
+ directives: info.directives,
423
+ });
424
+ }
374
425
  return info;
375
426
  },
376
427
  };
@@ -450,6 +501,124 @@ export namespace SourcePluginWorkspace {
450
501
  };
451
502
  }
452
503
 
504
+ /** Answer family of `go mod edit -json` parses (`PluginLoadAnswers`). */
505
+ const GO_MOD_SYNTAX = "go-mod-syntax";
506
+
507
+ /**
508
+ * Everything besides the manifest bytes that decides how `go mod edit -json`
509
+ * parses them, by content: the launcher, the `GOROOT/bin/go` the live query
510
+ * reported, the `GOENV` file that can select another toolchain, and the Go
511
+ * variables the command receives. `undefined` when an executable cannot be
512
+ * identified or the file cannot be read, so the parse runs.
513
+ */
514
+ function goModToolIdentity(
515
+ goBinary: string,
516
+ goRoot: string,
517
+ goEnv: string,
518
+ env: NodeJS.ProcessEnv,
519
+ identities: PluginContentIdentities.Store,
520
+ ): unknown {
521
+ const dispatchedPath = path.join(
522
+ goRoot,
523
+ "bin",
524
+ process.platform === "win32" ? "go.exe" : "go",
525
+ );
526
+ // Only an executable the store can prove from metadata may key the record:
527
+ // streaming both executables on a device without a reference would cost
528
+ // more than the parse it replaces.
529
+ if (
530
+ !provableFromMetadata(goBinary, identities) ||
531
+ !provableFromMetadata(dispatchedPath, identities)
532
+ )
533
+ return undefined;
534
+ const launcher = runtimeExecutableIdentity(goBinary, identities);
535
+ const dispatched = runtimeExecutableIdentity(dispatchedPath, identities);
536
+ if (launcher === undefined || dispatched === undefined) return undefined;
537
+ let goEnvFile: string | null;
538
+ if (goEnv === "off") goEnvFile = "off";
539
+ else
540
+ try {
541
+ goEnvFile = crypto
542
+ .createHash("sha256")
543
+ .update(fs.readFileSync(goEnv))
544
+ .digest("hex");
545
+ } catch (error) {
546
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT")
547
+ return undefined;
548
+ goEnvFile = null;
549
+ }
550
+ return [
551
+ 1,
552
+ launcher,
553
+ goRoot,
554
+ dispatched,
555
+ goEnv,
556
+ goEnvFile,
557
+ Object.entries(GoSourceInputs.goBuildEnv(goBinary, undefined, env))
558
+ .filter(
559
+ (entry): entry is [string, string] =>
560
+ entry[1] !== undefined && /^(GO|CGO_)/i.test(entry[0]),
561
+ )
562
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
563
+ ];
564
+ }
565
+
566
+ /**
567
+ * Whether the file's stamp is separable from the store's reference on its
568
+ * device, the precondition for proving its bytes from a record.
569
+ */
570
+ function provableFromMetadata(
571
+ file: string,
572
+ identities: PluginContentIdentities.Store,
573
+ ): boolean {
574
+ try {
575
+ return PluginContentIdentities.separable(
576
+ identities.references,
577
+ fs.statSync(file, { bigint: true }),
578
+ );
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+
584
+ /**
585
+ * A recorded parse in the shape `readGoModInfo` returns, or `undefined` when
586
+ * the record does not have it.
587
+ */
588
+ function recallGoModInfo(value: unknown): GoModInfo | undefined {
589
+ if (typeof value !== "object" || value === null) return undefined;
590
+ const record = value as { modulePath?: unknown; directives?: unknown };
591
+ if (
592
+ (record.modulePath !== null && typeof record.modulePath !== "string") ||
593
+ !Array.isArray(record.directives) ||
594
+ !record.directives.every(isReplaceDirective)
595
+ )
596
+ return undefined;
597
+ const directives = record.directives as NonNullable<GoModJson["Replace"]>;
598
+ return {
599
+ modulePath: record.modulePath,
600
+ directives,
601
+ replacements: directives
602
+ .map(jsonReplacementToGoModReplacement)
603
+ .filter((replacement) => replacement !== null),
604
+ };
605
+ }
606
+
607
+ function isReplaceDirective(value: unknown): boolean {
608
+ if (typeof value !== "object" || value === null) return false;
609
+ const directive = value as Record<string, unknown>;
610
+ return [directive.Old, directive.New].every(
611
+ (side) =>
612
+ side === undefined ||
613
+ (typeof side === "object" &&
614
+ side !== null &&
615
+ ["Path", "Version"].every((key) => {
616
+ const field = (side as Record<string, unknown>)[key];
617
+ return field === undefined || typeof field === "string";
618
+ })),
619
+ );
620
+ }
621
+
453
622
  function jsonReplacementToGoModReplacement(
454
623
  replacement: NonNullable<GoModJson["Replace"]>[number],
455
624
  ): GoModReplacement | null {
@@ -1,3 +1,4 @@
1
+ import type { PluginContentIdentities } from "../../plugin/internal/source/PluginContentIdentities";
1
2
  import type { ITtscProjectInputSnapshot } from "./ITtscProjectInputSnapshot";
2
3
  import type { TtscCommonOptions } from "./TtscCommonOptions";
3
4
 
@@ -39,6 +40,9 @@ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
39
40
  * Executable modules, linked source roots, contributors and external local
40
41
  * module replacements can be reported. ttsc's own package sources are omitted;
41
42
  * this notification is not the complete compiler or project-rule input graph.
43
+ * The reporting load also hands over its record store, if it opened one, so the
44
+ * owner proves each root's state from the plugin cache's records instead of
45
+ * reading the sources the load is about to prove (#1728).
42
46
  *
43
47
  * @evidence contracts/common.md#principled-implementation Readonly roots describe the actual native-plugin sources whose later edits need observation.
44
48
  * @evidence contracts/common.md#clear-and-simple-design A void notification leaves watcher construction and lifetime with the invocation owner.
@@ -49,7 +53,10 @@ export interface TtscSingleFileEmitOptions extends TtscCommonOptions {
49
53
  * @evidenceExclude contracts/performance.md#efficient-algorithms A type declaration chooses no processing strategy.
50
54
  * @evidenceExclude contracts/performance.md#reuse-equivalent-work A type declaration computes nothing, so there is no work to share.
51
55
  */
52
- export type TtscPluginSourceInputsHandler = (inputs: readonly string[]) => void;
56
+ export type TtscPluginSourceInputsHandler = (
57
+ inputs: readonly string[],
58
+ identities?: PluginContentIdentities.Store,
59
+ ) => void;
53
60
 
54
61
  /**
55
62
  * Receive the discovered project-rule filesystem topology for the watch owner.