ttsc 0.34.3 → 0.36.0

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 (87) hide show
  1. package/driver/compilation_snapshot.go +267 -0
  2. package/driver/compilation_snapshot_keeps_inputs_across_stages_test.go +130 -0
  3. package/driver/compilation_snapshot_preserves_checked_emission_test.go +100 -0
  4. package/driver/graph.go +9 -3
  5. package/driver/program.go +45 -14
  6. package/lib/compiler/internal/CompilationSnapshot.d.ts +33 -0
  7. package/lib/compiler/internal/CompilationSnapshot.js +90 -0
  8. package/lib/compiler/internal/CompilationSnapshot.js.map +1 -0
  9. package/lib/compiler/internal/build/BuildExecution.d.ts +33 -4
  10. package/lib/compiler/internal/build/BuildExecution.js +81 -8
  11. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  12. package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -0
  13. package/lib/compiler/internal/build/RuntimeBuildCache.d.ts +47 -0
  14. package/lib/compiler/internal/build/RuntimeBuildCache.js +235 -0
  15. package/lib/compiler/internal/build/RuntimeBuildCache.js.map +1 -0
  16. package/lib/compiler/internal/build/RuntimeBuildInputs.d.ts +44 -0
  17. package/lib/compiler/internal/build/RuntimeBuildInputs.js +282 -0
  18. package/lib/compiler/internal/build/RuntimeBuildInputs.js.map +1 -0
  19. package/lib/compiler/internal/build/RuntimeInputProof.d.ts +50 -0
  20. package/lib/compiler/internal/build/RuntimeInputProof.js +315 -0
  21. package/lib/compiler/internal/build/RuntimeInputProof.js.map +1 -0
  22. package/lib/compiler/internal/build/TsgoArguments.d.ts +5 -4
  23. package/lib/compiler/internal/build/TsgoArguments.js +11 -5
  24. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  25. package/lib/compiler/internal/build/appendBuildOutput.js +4 -0
  26. package/lib/compiler/internal/build/appendBuildOutput.js.map +1 -1
  27. package/lib/compiler/internal/build/runBuild.d.ts +3 -3
  28. package/lib/compiler/internal/build/runBuild.js +17 -4
  29. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  30. package/lib/compiler/internal/build/runExternalEmitProvenance.d.ts +12 -13
  31. package/lib/compiler/internal/build/runExternalEmitProvenance.js +24 -18
  32. package/lib/compiler/internal/build/runExternalEmitProvenance.js.map +1 -1
  33. package/lib/compiler/internal/parseNativeTransformOutput.js +4 -0
  34. package/lib/compiler/internal/parseNativeTransformOutput.js.map +1 -1
  35. package/lib/compiler/internal/runNativeCheckWithObservations.d.ts +18 -14
  36. package/lib/compiler/internal/runNativeCheckWithObservations.js +42 -18
  37. package/lib/compiler/internal/runNativeCheckWithObservations.js.map +1 -1
  38. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.d.ts +2 -1
  39. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js +4 -1
  40. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js.map +1 -1
  41. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -3
  42. package/lib/compiler/internal/transformProjectInMemory.js +29 -5
  43. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  44. package/lib/flags/FLAG_SCHEMA.js +7 -0
  45. package/lib/flags/FLAG_SCHEMA.js.map +1 -1
  46. package/lib/launcher/internal/parseTtsxCLI.d.ts +1 -0
  47. package/lib/launcher/internal/parseTtsxCLI.js +1 -0
  48. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  49. package/lib/launcher/internal/prepareExecution.d.ts +2 -0
  50. package/lib/launcher/internal/prepareExecution.js +6 -0
  51. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  52. package/lib/launcher/internal/runTtsx.js +5 -0
  53. package/lib/launcher/internal/runTtsx.js.map +1 -1
  54. package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -0
  55. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  56. package/lib/plugin/internal/load/loadProjectPlugins.js +1 -0
  57. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  58. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
  59. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +3 -0
  60. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  61. package/lib/structures/ITtscCompilerTransformation.d.ts +5 -0
  62. package/lib/structures/ITtscPluginCapabilities.d.ts +13 -2
  63. package/package.json +8 -8
  64. package/src/compiler/internal/CompilationSnapshot.ts +97 -0
  65. package/src/compiler/internal/build/BuildExecution.ts +128 -20
  66. package/src/compiler/internal/build/RunBuildOptions.ts +3 -0
  67. package/src/compiler/internal/build/RuntimeBuildCache.ts +252 -0
  68. package/src/compiler/internal/build/RuntimeBuildInputs.ts +349 -0
  69. package/src/compiler/internal/build/RuntimeInputProof.ts +346 -0
  70. package/src/compiler/internal/build/TsgoArguments.ts +11 -5
  71. package/src/compiler/internal/build/appendBuildOutput.ts +4 -0
  72. package/src/compiler/internal/build/runBuild.ts +20 -5
  73. package/src/compiler/internal/build/runExternalEmitProvenance.ts +25 -23
  74. package/src/compiler/internal/parseNativeTransformOutput.ts +5 -0
  75. package/src/compiler/internal/runNativeCheckWithObservations.ts +41 -18
  76. package/src/compiler/internal/sharedHost/inheritedSidecarEnv.ts +8 -1
  77. package/src/compiler/internal/transformProjectInMemory.ts +46 -5
  78. package/src/flags/FLAG_SCHEMA.ts +7 -0
  79. package/src/launcher/internal/parseTtsxCLI.ts +1 -0
  80. package/src/launcher/internal/prepareExecution.ts +13 -0
  81. package/src/launcher/internal/runTtsx.ts +5 -0
  82. package/src/launcher/internal/runtime/installRuntimeHooks.ts +11 -0
  83. package/src/plugin/internal/load/loadProjectPlugins.ts +1 -0
  84. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +4 -0
  85. package/src/structures/ITtscCompilerTransformation.ts +6 -0
  86. package/src/structures/ITtscPluginCapabilities.ts +14 -2
  87. package/utility/host.go +22 -1
@@ -79,12 +79,23 @@ export interface ITtscPluginCapabilities {
79
79
  * from an output filename or configuration prediction.
80
80
  *
81
81
  * When omitted, ttsc does not pass the option to a strict third-party host. A
82
- * runtime operation requiring verified emit provenance must then report that
83
- * the selected host does not support it.
82
+ * runtime operation then uses the existing externally observed provenance
83
+ * adapter, whose incomplete proof remains visible to the runtime consumer.
84
84
  *
85
85
  * @default false
86
86
  */
87
87
  emitProvenance?: true;
88
+ /**
89
+ * Whether build accepts `--emit-observations-json=<absolute private path>`
90
+ * and publishes its generation's compiler and native input observations.
91
+ * Missing support keeps runtime emits fresh rather than assuming reusable
92
+ * input completeness. This protocol is independent of output ownership. Hosts
93
+ * publish the same observation envelope as checkObservations, including
94
+ * failures, before releasing their Program.
95
+ *
96
+ * @default false
97
+ */
98
+ emitObservations?: true;
88
99
  /**
89
100
  * Whether the sidecar accepts ttsc's `--project-context-json` identity
90
101
  * protocol. The payload keeps lexical selection paths, physical Program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ttsc",
3
- "version": "0.34.3",
3
+ "version": "0.36.0",
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",
@@ -64,13 +64,13 @@
64
64
  },
65
65
  "optionalDependencies": {
66
66
  "fsevents": "^2.3.3",
67
- "@ttsc/linux-x64": "0.34.3",
68
- "@ttsc/linux-arm": "0.34.3",
69
- "@ttsc/darwin-x64": "0.34.3",
70
- "@ttsc/darwin-arm64": "0.34.3",
71
- "@ttsc/win32-x64": "0.34.3",
72
- "@ttsc/win32-arm64": "0.34.3",
73
- "@ttsc/linux-arm64": "0.34.3"
67
+ "@ttsc/linux-x64": "0.36.0",
68
+ "@ttsc/linux-arm64": "0.36.0",
69
+ "@ttsc/linux-arm": "0.36.0",
70
+ "@ttsc/win32-x64": "0.36.0",
71
+ "@ttsc/darwin-x64": "0.36.0",
72
+ "@ttsc/darwin-arm64": "0.36.0",
73
+ "@ttsc/win32-arm64": "0.36.0"
74
74
  },
75
75
  "sideEffects": [
76
76
  "./lib/register.js",
@@ -0,0 +1,97 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
6
+ import { SidecarEnvironment } from "./sharedHost/SidecarEnvironment";
7
+
8
+ /**
9
+ * Own one read-once compiler view across sequential native stages. Its private
10
+ * document stores actual compiler text and lookup answers, including absence;
11
+ * each invocation starts empty and removes its storage after its final stage.
12
+ * Disk freshness remains a separate cache-admission decision.
13
+ *
14
+ * @evidence contracts/common.md#principled-implementation A unique invocation ID and versioned private document share first-read answers through the driver's supported VFS, without changing source paths or the selected compiler.
15
+ * @evidence contracts/common.md#clear-and-simple-design One synchronous owner brackets the sequential compiler operation and its transport cleanup.
16
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The driver consumes real captured text; no filesystem methods or compiler internals are replaced.
17
+ * @evidence contracts/common.md#meaningful-documentation Prose states read-once semantics, sequential stage ownership, absence and independent freshness admission.
18
+ * @evidence contracts/portability.md#os-neutral-implementation Native temporary-directory creation and path APIs own transport; environment merging preserves Windows key semantics.
19
+ * @evidence contracts/performance.md#efficient-algorithms Construction writes an empty document; each native stage parses prior captured text/predicates and dirty flushes encode and write the complete reached population. Normal UTF-8 remains plain text, while invalid UTF-8 is encoded losslessly and decoded once per stage.
20
+ * @evidence contracts/performance.md#reuse-equivalent-work Only stages of this invocation share captured answers; unrelated invocations have fresh IDs and documents.
21
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The synchronous owner attempts identity-checked cleanup after success or failure; native captured bytes have no population cap. A failed initial identity read or cleanup can leave storage behind; cleanup failure retains the original thrown error or completed result.
22
+ */
23
+ export namespace CompilationSnapshot {
24
+ export const ENV = "TTSC_COMPILATION_SNAPSHOT";
25
+
26
+ /**
27
+ * Run sequential native stages with a new invocation-owned input document.
28
+ * The supplied environment is copied; same-directory identity guards cleanup,
29
+ * and cleanup failure retains the operation error or completed result.
30
+ *
31
+ * @evidence contracts/common.md#principled-implementation A unique invocation ID and versioned private document share first-read answers through the driver's supported VFS, without changing source paths or the selected compiler.
32
+ * @evidence contracts/common.md#clear-and-simple-design One synchronous owner brackets the sequential compiler operation and its transport cleanup.
33
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The driver consumes real captured text; no filesystem methods or compiler internals are replaced.
34
+ * @evidence contracts/common.md#meaningful-documentation Prose states read-once semantics, sequential stage ownership, absence and independent freshness admission.
35
+ * @evidence contracts/portability.md#os-neutral-implementation Native temporary-directory creation and path APIs own transport; environment merging preserves Windows key semantics.
36
+ * @evidence contracts/performance.md#efficient-algorithms Construction writes an empty document; each native stage parses prior captured text/predicates and dirty flushes encode and write the complete reached population. Normal UTF-8 remains plain text, while invalid UTF-8 is encoded losslessly and decoded once per stage.
37
+ * @evidence contracts/performance.md#reuse-equivalent-work Only stages of this invocation share captured answers; unrelated invocations have fresh IDs and documents.
38
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The synchronous owner attempts identity-checked cleanup after success or failure; native captured bytes have no population cap. A failed initial identity read or cleanup can leave storage behind; cleanup failure retains the original thrown error or completed result.
39
+ */
40
+ export function run<T>(
41
+ environment: NodeJS.ProcessEnv | undefined,
42
+ operation: (environment: NodeJS.ProcessEnv) => T,
43
+ ): T {
44
+ const directory = createCanonicalTempDirectory("ttsc-input-snapshot-");
45
+ const ownership = fs.lstatSync(directory);
46
+ let completed: T | undefined;
47
+ let failed = false;
48
+ let failure: unknown;
49
+ try {
50
+ const file = path.join(directory, "inputs.json");
51
+ fs.writeFileSync(
52
+ file,
53
+ JSON.stringify({
54
+ version: 1,
55
+ id: crypto.randomUUID(),
56
+ reads: {},
57
+ files: {},
58
+ directories: {},
59
+ entries: {},
60
+ realpaths: {},
61
+ stats: {},
62
+ }),
63
+ );
64
+ const env = SidecarEnvironment.merge(environment, { [ENV]: file });
65
+ completed = operation(env);
66
+ return completed;
67
+ } catch (error) {
68
+ failed = true;
69
+ failure = error;
70
+ throw error;
71
+ } finally {
72
+ try {
73
+ const current = fs.lstatSync(directory);
74
+ if (
75
+ !current.isDirectory() ||
76
+ current.isSymbolicLink() ||
77
+ current.dev !== ownership.dev ||
78
+ current.ino !== ownership.ino
79
+ )
80
+ throw new Error("Compilation snapshot directory ownership changed");
81
+ fs.rmSync(directory, { recursive: true });
82
+ } catch (error) {
83
+ if (failed)
84
+ throw new AggregateError(
85
+ [failure, error],
86
+ "Compilation snapshot and cleanup failed",
87
+ { cause: failure },
88
+ );
89
+ throw new AggregateError(
90
+ [completed, error],
91
+ "Compilation completed but snapshot cleanup failed",
92
+ { cause: completed },
93
+ );
94
+ }
95
+ }
96
+ }
97
+ }
@@ -13,6 +13,8 @@ import type { TtscBuildOptions } from "../../../structures/internal/TtscBuildOpt
13
13
  import type { TtscBuildResult } from "../../../structures/internal/TtscBuildResult";
14
14
  import type { TtscCommonOptions } from "../../../structures/internal/TtscCommonOptions";
15
15
  import type { TtscPluginSourceInputsHandler } from "../../../structures/internal/TtscSingleFileEmitOptions";
16
+ import { CompilationSnapshot } from "../CompilationSnapshot";
17
+ import { compileProjectInMemory } from "../compileProjectInMemory";
16
18
  import { outputText } from "../outputText";
17
19
  import { CompilerProjectSelection } from "../project/CompilerProjectSelection";
18
20
  import { readProjectConfig } from "../project/readProjectConfig";
@@ -226,6 +228,11 @@ export namespace BuildExecution {
226
228
  clearInheritedTsgoArgs(env, extra);
227
229
  }
228
230
  clearInheritedSemanticConfigPath(env, extra);
231
+ SidecarEnvironment.write(
232
+ env,
233
+ CompilationSnapshot.ENV,
234
+ SidecarEnvironment.read(extra, CompilationSnapshot.ENV),
235
+ );
229
236
  // The anchor is per-invocation state owned by this host: when this run
230
237
  // declared none (and the caller's env does not name one), drop any value
231
238
  // inherited from an ancestor ttsc process so a nested build never
@@ -363,22 +370,45 @@ export namespace BuildExecution {
363
370
  * opted-in native host reports its captured generation, while the direct
364
371
  * compiler adapter admits only a stable observed selection and supported
365
372
  * output layout. Unknown ownership remains explicit for consumers rather than
366
- * guessed.
373
+ * guessed. Native runtime stages share one invocation-owned read-once view;
374
+ * configured checks followed by the exact installed compiler emit through its
375
+ * matching native target, while custom compiler executables remain
376
+ * delegated.
367
377
  *
368
378
  * @evidence contracts/common.md#principled-implementation Ordered checks block emit on failure and retain negotiated same-generation input observations for the actual check verb; effectful fix/format results keep their distinct command contract without fabricated reusable check metadata; transforms use one compatible host. Normally completed native partial emission and supported stable external emission can retain actual-write provenance alongside unchanged nonzero status, while interrupted or unknown generations establish no proof.
369
379
  * @evidence contracts/common.md#clear-and-simple-design This dispatcher owns phase policy while argv, spawning, normalization and fallback comparison remain shared helpers used by one-shot and watch lanes.
370
380
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Format bypass follows its write-only contract; capability-based diagnostics/provenance and failure fallback preserve supported producer semantics without plugin-name shortcuts, foreign mutation or filename-based ownership guesses.
371
381
  * @evidence contracts/common.md#meaningful-documentation Native prose states ordering, format effects and thrown boundary failures; branch comments explain nonobvious emission and display policies.
372
382
  * @evidence contracts/portability.md#os-neutral-implementation Commands preserve native executable/argv/cwd boundaries; provenance uses a canonical unique temporary directory and absolute native wire paths without post-compile realpath reconstruction or OS-derived case folding.
373
- * @evidence contracts/performance.md#efficient-algorithms Plugin/stage selection scans the configured population; each command includes argv/env publication and native runtime probes under its helpers. Required proof adds artifact or external inspection/JSON/source-executable observation work, plus association/path processing. Accumulating phase reports can recopy earlier bytes; there is no supplied bound on total reports, proof bytes or child duration.
374
- * @evidence contracts/performance.md#reuse-equivalent-work A check host declaring TypeScript diagnostics avoids an equivalent extra successful check, while compatible transform plugins share one host; effectful configured checks still execute in order.
375
- * @evidence contracts/performance.md#bound-retention-and-release-resources Phase results and command/proof records stay build-local and returned data transfer to the caller, with uncapped report/association bytes. Native proof setup observes ownership before finally, so setup failure can leave allocation unclaimed; accepted artifacts reach identity-checked cleanup attempts. Replaced parents remain untouched. Cleanup aggregates an earlier throw or nonzero result; other cleanup errors propagate without certifying release. Delegated capture/descendant release remains with its owner; no resident history is retained here.
383
+ * @evidence contracts/performance.md#efficient-algorithms Plugin/stage selection scans the configured population; each command includes argv/env publication and native runtime probes under its helpers. Required proof adds artifact or external inspection/JSON/source-executable observation work, plus association/path processing. Native runtime snapshot stages parse the preceding full document and dirty stages serialize/write its complete captured population. Accumulating phase reports can recopy earlier bytes; there is no supplied bound on total reports, proof bytes or child duration.
384
+ * @evidence contracts/performance.md#reuse-equivalent-work A check host declaring TypeScript diagnostics avoids an equivalent extra successful check, while compatible transform plugins share one host; effectful configured checks still execute in order. Captured answers are shared only across the sequential native stages of this invocation, independently of later cache admission.
385
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Phase results and command/proof records stay build-local and returned data transfer to the caller, with uncapped report/association bytes. Native proof setup observes ownership before finally, so setup failure can leave allocation unclaimed; accepted artifacts reach identity-checked cleanup attempts. Replaced parents remain untouched. Cleanup aggregates an earlier throw or nonzero result; other cleanup errors propagate without certifying release. The snapshot coordinator attempts identity-checked transport cleanup and retains an original error or completed result when cleanup fails. Captured text and full document buffers follow reached input bytes without a cap. Delegated capture/descendant release remains with its owner; no resident history is retained here.
376
386
  */
377
387
  export function runPreparedBuild(
378
388
  options: RunBuildOptions,
379
389
  timing: BuildTiming.BuildTiming,
380
390
  execution: ReturnType<typeof resolveExecutionContext>,
381
391
  buildOptions: RunBuildOptions,
392
+ ): TtscBuildResult {
393
+ if (
394
+ buildOptions.forceEmitProvenance === true &&
395
+ execution.nativePlugins.length !== 0
396
+ )
397
+ return CompilationSnapshot.run(buildOptions.env, (env) =>
398
+ runPreparedSnapshotBuild(options, timing, execution, {
399
+ ...buildOptions,
400
+ env,
401
+ }),
402
+ );
403
+ return runPreparedSnapshotBuild(options, timing, execution, buildOptions);
404
+ }
405
+
406
+ /** Execute one already selected build without replacing compiler authority. */
407
+ function runPreparedSnapshotBuild(
408
+ options: RunBuildOptions,
409
+ timing: BuildTiming.BuildTiming,
410
+ execution: ReturnType<typeof resolveExecutionContext>,
411
+ buildOptions: RunBuildOptions,
382
412
  ): TtscBuildResult {
383
413
  if (execution.compilerSelection.inspectionError !== undefined) {
384
414
  const diagnosed = runTsgo(execution, [], buildOptions);
@@ -463,6 +493,14 @@ export namespace BuildExecution {
463
493
  execution,
464
494
  );
465
495
  }
496
+ } else if (
497
+ buildOptions.forceEmitProvenance === true &&
498
+ isProjectCompiler(execution, buildOptions)
499
+ ) {
500
+ result = appendBuildOutput(
501
+ checked,
502
+ runSnapshotCompilerBuild(execution, buildOptions),
503
+ );
466
504
  } else {
467
505
  if (
468
506
  buildOptions.skipDiagnosticsCheck !== true &&
@@ -728,20 +766,33 @@ export namespace BuildExecution {
728
766
  ): TtscBuildResult {
729
767
  const host = selectSharedHostPlugin(plugins);
730
768
  const run = (provenancePath?: string): TtscBuildResult =>
731
- runNativePluginCommand(
732
- host,
733
- NativePluginArguments.createNativeBuildArgs(
734
- execution,
735
- options,
736
- plugins,
737
- provenancePath,
738
- ),
739
- options,
740
- execution,
741
- "ttsc.build",
742
- timing,
743
- NativePluginArguments.transformHostTimingLabel(plugins),
744
- TsgoArguments.createNativeBuildTsgoArgs(execution, options),
769
+ runNativeCheckWithObservations(
770
+ {
771
+ capabilities:
772
+ options.runtimeCacheDir === undefined
773
+ ? undefined
774
+ : host.capabilities,
775
+ },
776
+ (observationArgs) =>
777
+ runNativePluginCommand(
778
+ host,
779
+ [
780
+ ...NativePluginArguments.createNativeBuildArgs(
781
+ execution,
782
+ options,
783
+ plugins,
784
+ provenancePath,
785
+ ),
786
+ ...observationArgs,
787
+ ],
788
+ options,
789
+ execution,
790
+ "ttsc.build",
791
+ timing,
792
+ NativePluginArguments.transformHostTimingLabel(plugins),
793
+ TsgoArguments.createNativeBuildTsgoArgs(execution, options),
794
+ ),
795
+ "build",
745
796
  );
746
797
  if (options.forceEmitProvenance !== true) return run();
747
798
  if (host.capabilities?.emitProvenance === true)
@@ -966,6 +1017,60 @@ export namespace BuildExecution {
966
1017
  );
967
1018
  }
968
1019
 
1020
+ /**
1021
+ * Preserve custom compiler executables while recognizing the project's exact
1022
+ * compiler.
1023
+ */
1024
+ function isProjectCompiler(
1025
+ execution: ReturnType<typeof resolveExecutionContext>,
1026
+ options: RunBuildOptions,
1027
+ ): boolean {
1028
+ if (execution.tsgo.version !== "custom") return true;
1029
+ try {
1030
+ const env = SidecarEnvironment.merge(process.env, options.env, {
1031
+ TTSC_TSGO_BINARY: undefined,
1032
+ });
1033
+ const installed = resolveTsgo({ cwd: execution.projectRoot, env });
1034
+ return (
1035
+ fs.realpathSync.native(installed.binary) ===
1036
+ fs.realpathSync.native(execution.tsgo.binary)
1037
+ );
1038
+ } catch {
1039
+ return false;
1040
+ }
1041
+ }
1042
+
1043
+ /** Emit through the selected upstream target's native VFS after native checks. */
1044
+ function runSnapshotCompilerBuild(
1045
+ execution: ReturnType<typeof resolveExecutionContext>,
1046
+ options: RunBuildOptions,
1047
+ ): TtscBuildResult {
1048
+ const forwarded = TsgoArguments.createNativeBuildTsgoArgs(
1049
+ execution,
1050
+ options,
1051
+ );
1052
+ const env = SidecarEnvironment.merge(options.env, {
1053
+ [TSGO_ARGS_ENV]: JSON.stringify(forwarded),
1054
+ [TSGO_ARGS_CWD_ENV]: execution.compilerSelection.compilerArgsCwd,
1055
+ });
1056
+ const compiled = compileProjectInMemory({
1057
+ cwd: execution.projectRoot,
1058
+ projectRoot: execution.projectRoot,
1059
+ tsconfig: execution.tsconfig,
1060
+ env,
1061
+ plugins: [],
1062
+ cacheDir: options.cacheDir,
1063
+ });
1064
+ const emittedFiles: string[] = [];
1065
+ for (const [name, text] of Object.entries(compiled.output)) {
1066
+ const output = path.resolve(execution.projectRoot, name);
1067
+ fs.mkdirSync(path.dirname(output), { recursive: true });
1068
+ fs.writeFileSync(output, text);
1069
+ emittedFiles.push(output);
1070
+ }
1071
+ return { ...compiled.result, emittedFiles };
1072
+ }
1073
+
969
1074
  /**
970
1075
  * Run `tsgo` with the full emit arguments and parse `TSFILE:` lines from
971
1076
  * stdout into `emittedFiles`. Internally requested TSFILE lines are stripped;
@@ -1365,6 +1470,7 @@ export namespace BuildExecution {
1365
1470
  });
1366
1471
  let pluginSetupFailure: TtscBuildResult | undefined;
1367
1472
  let nativePlugins: ITtscLoadedNativePlugin[] = [];
1473
+ let pluginLoad: ReturnType<typeof loadProjectPlugins> | undefined;
1368
1474
  // Opened only for an invocation that loads plugins, so a project without
1369
1475
  // any never gains a cache directory; the load and every later native spawn
1370
1476
  // of this invocation prove their unchanged inputs through it (#1722, #1723).
@@ -1387,7 +1493,7 @@ export namespace BuildExecution {
1387
1493
  cacheDir,
1388
1494
  env,
1389
1495
  });
1390
- nativePlugins = (yield* loadProjectPlugins.steps({
1496
+ pluginLoad = yield* loadProjectPlugins.steps({
1391
1497
  binary: resolveBinary({ env }) ?? "",
1392
1498
  cacheDir,
1393
1499
  cwd,
@@ -1398,7 +1504,8 @@ export namespace BuildExecution {
1398
1504
  pluginConfigDir: options.pluginConfigDir,
1399
1505
  projectRoot,
1400
1506
  tsconfig,
1401
- })).nativePlugins;
1507
+ });
1508
+ nativePlugins = pluginLoad.nativePlugins;
1402
1509
  } else {
1403
1510
  yield options.onWatchInputs?.([]);
1404
1511
  }
@@ -1415,6 +1522,7 @@ export namespace BuildExecution {
1415
1522
  cwd,
1416
1523
  ...(identities === undefined ? {} : { identities }),
1417
1524
  nativePlugins,
1525
+ ...(pluginLoad === undefined ? {} : { pluginLoad }),
1418
1526
  pluginConfigDir: resolvePluginConfigDir({
1419
1527
  cwd,
1420
1528
  pluginConfigDir: options.pluginConfigDir,
@@ -20,6 +20,9 @@ import type {
20
20
  * @evidenceExclude contracts/performance.md#reuse-equivalent-work A type declaration computes nothing, so there is no work to share.
21
21
  */
22
22
  export type RunBuildOptions = TtscBuildOptions & {
23
+ /** Persistent private-runtime emit documents; absence keeps ordinary builds. */
24
+ runtimeCacheDir?: string;
25
+
23
26
  /**
24
27
  * Skip the independent direct-compiler type-check gate and the added
25
28
  * `--noEmitOnError` guard. Configured plugin checks and native hosts still
@@ -0,0 +1,252 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { E2ETrace } from "../../../internal/E2ETrace";
6
+ import { recordCacheFileUse } from "../../../plugin/internal/source/recordCacheFileUse";
7
+ import type { TtscBuildResult } from "../../../structures/internal/TtscBuildResult";
8
+
9
+ /**
10
+ * Persist completed private runtime emits as checksummed, atomically published
11
+ * documents. Readers copy admitted bytes into their own run directory, so cache
12
+ * collection and concurrent publications cannot remove an executing run's emit.
13
+ * Input discovery and producer-proof admission belong to the supplied owners.
14
+ *
15
+ * @evidence contracts/common.md#principled-implementation Content-addressed snapshots select complete checksummed result and output documents. SHA-256 consumes exact UTF-8 text; native normalization rejects absolute, dot, parent and Windows drive-relative output coordinates. Relocation changes producer output coordinates and private-root stream text while retaining source paths.
16
+ * @evidence contracts/common.md#clear-and-simple-design One operation coordinates lookup, proof, restore and publication, with private hashing, containment and relocation helpers. Input capture and producer validation remain separate callbacks.
17
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Misses execute the original build exactly once; failed or absent proofs never manufacture successful output. Every transported output must be a normalized private descendant, without filename or consumer-name exceptions.
18
+ * @evidence contracts/common.md#meaningful-documentation Native prose distinguishes persistent storage, run-owned copies and proof ownership; run states callback stability and failure behavior, and private helper comments identify byte, path and relocation domains.
19
+ * @evidence contracts/portability.md#os-neutral-implementation Native paths, lstat, relative/join and rename own storage and containment, including Windows drive-relative rejection. Source coordinates remain unchanged; transported symlinks are refused rather than followed.
20
+ * @evidence contracts/performance.md#efficient-algorithms Lookup hashes snapshot text and reads/hashes the bounded document. Restore and publication scale with output bytes and file count, without unrelated source traversal; normalization scans path bytes and relocation scans output paths and stream text.
21
+ * @evidence contracts/performance.md#reuse-equivalent-work Equal snapshots reuse completed emits only after independent producer admission and unchanged recapture. A restored run owns its copied bytes and rebased output ledger; concurrent publishers may duplicate compilation but cannot expose partial documents.
22
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Documents are capped at 128 MiB, with fresh output collection limited to a 0.7 raw-byte budget before encoding, and join the existing collected single-file cache population. Snapshots and decoded bytes are invocation-owned; staging removal is attempted in finally and run owners release output. Cleanup or collector failures can retain files; helpers retain no handles or history.
23
+ */
24
+ export namespace RuntimeBuildCache {
25
+ const FORMAT = "ttsx-runtime-build-v1";
26
+ const MAX_BYTES = 128 * 1024 * 1024;
27
+
28
+ /**
29
+ * Reuse a proven private emit, or invoke the original producer once. Capture
30
+ * returns a deterministic complete input snapshot, or undefined to decline
31
+ * caching. Admission must validate the producer's extra input observations;
32
+ * the second argument is the output root that producer actually used. Cache
33
+ * IO or cache-admission failure costs reuse only. Errors thrown by the
34
+ * original build retain their normal failure policy.
35
+ *
36
+ * @evidence contracts/common.md#principled-implementation Lookup and publication bracket current input snapshots, preserve the actual producer result and require explicit extra-input admission; cache-hit output is copied before the result is rebased.
37
+ * @evidence contracts/common.md#clear-and-simple-design Snapshot, proof and build callbacks expose their independent responsibilities while the operation alone owns storage and restore cleanup.
38
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Only successful complete emits are published; malformed, oversized, damaged or unproved documents fall through to the caller's real producer without changing compiler selection.
39
+ * @evidence contracts/common.md#meaningful-documentation Native prose states complete-snapshot meaning, producer-root coordinates, callback error policy and recoverable cache IO.
40
+ * @evidence contracts/portability.md#os-neutral-implementation Native lstat refuses aliases, path.relative checks containment and native mkdir/write/rename preserve byte payloads and host path semantics.
41
+ * @evidence contracts/performance.md#efficient-algorithms Snapshot owners dominate input work; document read/parse/hash and base64 restore scale with bounded encoded bytes, while publication walks only private output and serializes it once.
42
+ * @evidence contracts/performance.md#reuse-equivalent-work A format-and-snapshot digest names one candidate; checksum, output grammar, producer admission and unchanged recapture are independent reuse gates. Every miss calls build once.
43
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Call-local decoded outputs and serialized documents are bounded by the 128 MiB admission ceiling, except fresh output collection precedes serialization; partial restores are unlinked and staging removal is attempted in finally. Collector failures can retain files under the existing cache policy.
44
+ */
45
+ export function run(props: {
46
+ /** Collected single-file cache root, or undefined to bypass persistence. */
47
+ directory?: string;
48
+ /** Empty private output directory owned by this build's caller. */
49
+ emitDir: string;
50
+ /** Fresh complete input snapshot; undefined preserves ordinary execution. */
51
+ capture: () => string | undefined;
52
+ /** Producer-specific proof, with its original private output root. */
53
+ admit: (result: TtscBuildResult, emitDir: string) => boolean;
54
+ /** Original build operation, including its diagnostic policy. */
55
+ build: () => TtscBuildResult;
56
+ }): TtscBuildResult {
57
+ if (props.directory === undefined) return props.build();
58
+ let snapshot: string | undefined;
59
+ try {
60
+ snapshot = props.capture();
61
+ } catch {
62
+ /* An unavailable proof is a miss. */
63
+ }
64
+ if (snapshot === undefined) return props.build();
65
+ const key = digest(FORMAT + "\0" + snapshot);
66
+ const file = path.join(props.directory, key + ".json");
67
+ const restored: string[] = [];
68
+ try {
69
+ const stat = fs.lstatSync(file);
70
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_BYTES)
71
+ throw new Error("unusable runtime cache document");
72
+ const document = JSON.parse(fs.readFileSync(file, "utf8"));
73
+ if (
74
+ document.format !== FORMAT ||
75
+ typeof document.payload !== "string" ||
76
+ document.hash !== digest(document.payload)
77
+ )
78
+ throw new Error("invalid runtime cache checksum");
79
+ const entry = JSON.parse(document.payload);
80
+ if (
81
+ entry.snapshot !== snapshot ||
82
+ typeof entry.emitDir !== "string" ||
83
+ !path.isAbsolute(entry.emitDir) ||
84
+ entry.result?.status !== 0 ||
85
+ !Array.isArray(entry.outputs) ||
86
+ !props.admit(entry.result, entry.emitDir)
87
+ )
88
+ throw new Error("unproved runtime cache document");
89
+ const names = new Set<string>();
90
+ for (const output of entry.outputs) {
91
+ if (
92
+ !Array.isArray(output) ||
93
+ output.length !== 2 ||
94
+ typeof output[0] !== "string" ||
95
+ typeof output[1] !== "string" ||
96
+ !safeRelative(output[0]) ||
97
+ names.has(output[0]) ||
98
+ Buffer.from(output[1], "base64").toString("base64") !== output[1]
99
+ )
100
+ throw new Error("invalid runtime cache output");
101
+ names.add(output[0]);
102
+ }
103
+ for (const output of Object.keys(entry.result.emittedSources ?? {}))
104
+ if (!names.has(path.relative(entry.emitDir, output)))
105
+ throw new Error("missing runtime cache provenance output");
106
+ for (const [name, bytes] of entry.outputs) {
107
+ const target = path.join(props.emitDir, name);
108
+ fs.mkdirSync(path.dirname(target), { recursive: true });
109
+ fs.writeFileSync(target, Buffer.from(bytes, "base64"), { flag: "wx" });
110
+ restored.push(target);
111
+ }
112
+ if (
113
+ props.capture() !== snapshot ||
114
+ !props.admit(entry.result, entry.emitDir)
115
+ )
116
+ throw new Error("runtime inputs moved during restore");
117
+ recordCacheFileUse(file);
118
+ E2ETrace.capabilityResolution("runtime-build-cache", {
119
+ outcome: "reused",
120
+ key,
121
+ });
122
+ return relocate(entry.result, entry.emitDir, props.emitDir);
123
+ } catch {
124
+ for (const output of restored)
125
+ try {
126
+ fs.unlinkSync(output);
127
+ } catch {
128
+ /* Rejected cache output cannot prevent the ordinary producer. */
129
+ }
130
+ }
131
+ const result = props.build();
132
+ if (result.status !== 0) return result;
133
+ let temporary: string | undefined;
134
+ try {
135
+ const outputs: [string, string][] = [];
136
+ let outputBytes = 0;
137
+ const pending = [props.emitDir];
138
+ while (pending.length !== 0) {
139
+ const directory = pending.pop()!;
140
+ for (const child of fs.readdirSync(directory, {
141
+ withFileTypes: true,
142
+ })) {
143
+ const target = path.join(directory, child.name);
144
+ if (child.isSymbolicLink()) throw new Error("aliased runtime output");
145
+ if (child.isDirectory()) pending.push(target);
146
+ else if (child.isFile()) {
147
+ outputBytes += fs.statSync(target).size;
148
+ if (outputBytes > MAX_BYTES * 0.7) return result;
149
+ outputs.push([
150
+ path.relative(props.emitDir, target),
151
+ fs.readFileSync(target).toString("base64"),
152
+ ]);
153
+ } else throw new Error("nonordinary runtime output");
154
+ }
155
+ }
156
+ if (outputs.length === 0) return result;
157
+ const payload = JSON.stringify({
158
+ snapshot,
159
+ emitDir: props.emitDir,
160
+ result,
161
+ outputs,
162
+ });
163
+ const text = JSON.stringify({
164
+ format: FORMAT,
165
+ hash: digest(payload),
166
+ payload,
167
+ });
168
+ if (Buffer.byteLength(text) > MAX_BYTES) return result;
169
+ if (props.capture() !== snapshot || !props.admit(result, props.emitDir))
170
+ return result;
171
+ fs.mkdirSync(props.directory, { recursive: true });
172
+ if (fs.lstatSync(props.directory).isSymbolicLink()) return result;
173
+ temporary = path.join(
174
+ props.directory,
175
+ `${key}.${crypto.randomUUID()}.tmp`,
176
+ );
177
+ fs.writeFileSync(temporary, text, { flag: "wx" });
178
+ fs.renameSync(temporary, file);
179
+ E2ETrace.capabilityResolution("runtime-build-cache", {
180
+ outcome: "recorded",
181
+ key,
182
+ });
183
+ } catch {
184
+ /* Persistence never replaces a fresh successful result. */
185
+ } finally {
186
+ if (temporary !== undefined) {
187
+ try {
188
+ fs.unlinkSync(temporary);
189
+ } catch {
190
+ /* Rename or later collection owns it. */
191
+ }
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+
197
+ /** Hash encoded input or document bytes with SHA-256. */
198
+ function digest(text: string): string {
199
+ return crypto.createHash("sha256").update(text).digest("hex");
200
+ }
201
+
202
+ /** Admit only a nonempty relative descendant coordinate. */
203
+ function safeRelative(name: string): boolean {
204
+ return (
205
+ name.length !== 0 &&
206
+ !name.includes("\0") &&
207
+ !path.isAbsolute(name) &&
208
+ path.parse(name).root === "" &&
209
+ path.normalize(name) === name &&
210
+ name !== "." &&
211
+ name !== ".." &&
212
+ !name.startsWith(".." + path.sep)
213
+ );
214
+ }
215
+
216
+ /** Move producer-owned output coordinates to the current private run. */
217
+ function relocate(
218
+ result: TtscBuildResult,
219
+ previous: string,
220
+ current: string,
221
+ ): TtscBuildResult {
222
+ const move = (output: string): string => {
223
+ const relative = path.relative(previous, output);
224
+ if (!safeRelative(relative))
225
+ throw new Error("runtime provenance escapes its output root");
226
+ return path.join(current, relative);
227
+ };
228
+ return {
229
+ ...result,
230
+ emittedFiles: result.emittedFiles?.map(move),
231
+ emittedSources:
232
+ result.emittedSources === undefined
233
+ ? undefined
234
+ : Object.fromEntries(
235
+ Object.entries(result.emittedSources).map(([output, sources]) => [
236
+ move(output),
237
+ sources,
238
+ ]),
239
+ ),
240
+ emittedSourceProofFailures:
241
+ result.emittedSourceProofFailures === undefined
242
+ ? undefined
243
+ : Object.fromEntries(
244
+ Object.entries(result.emittedSourceProofFailures).map(
245
+ ([output, reason]) => [move(output), reason],
246
+ ),
247
+ ),
248
+ stdout: result.stdout.split(previous).join(current),
249
+ stderr: result.stderr.split(previous).join(current),
250
+ };
251
+ }
252
+ }