ttsc 0.32.1 → 0.33.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 (111) hide show
  1. package/cmd/ttsc-cache/account.go +94 -0
  2. package/cmd/ttsc-cache/interruption_test.go +176 -0
  3. package/cmd/ttsc-cache/main.go +252 -0
  4. package/cmd/ttsc-cache/merge.go +324 -0
  5. package/cmd/ttsc-cache/merge_test.go +161 -0
  6. package/cmd/ttsc-cache/prepare.go +330 -0
  7. package/cmd/ttsc-cache/prepare_test.go +456 -0
  8. package/cmd/ttsc-cache/protocol_test.go +119 -0
  9. package/cmd/ttsc-cache/publisher_seal_test.go +135 -0
  10. package/cmd/ttsc-cache/seal.go +427 -0
  11. package/cmd/ttsc-cache/stock_go_failure_test.go +94 -0
  12. package/cmd/ttsc-cache/stock_go_test.go +163 -0
  13. package/cmd/ttsc-cache/store.go +249 -0
  14. package/cmd/ttsc-cache/store_test.go +104 -0
  15. package/cmd/ttsc-cache/tuple.go +107 -0
  16. package/cmd/ttsc-cache/tuple_test.go +62 -0
  17. package/cmd/ttsc-cache/usage.go +73 -0
  18. package/driver/configloaderscratch/canonical_directory_other.go +34 -0
  19. package/driver/configloaderscratch/canonical_directory_windows.go +60 -0
  20. package/driver/configloaderscratch/identity_temp_base.go +81 -0
  21. package/driver/configloaderscratch/identity_temp_base_posix_test.go +124 -0
  22. package/driver/configloaderscratch/identity_temp_base_windows_test.go +156 -0
  23. package/driver/emit_provenance.go +7 -5
  24. package/driver/resolutioninputs/recorder.cjs +16 -4
  25. package/driver/rewrite.go +25 -16
  26. package/driver/write_file_if_changed.go +69 -0
  27. package/driver/write_file_if_changed_test.go +148 -0
  28. package/internal/packageartifacts/manifest.go +288 -0
  29. package/internal/packageartifacts/manifest_test.go +127 -0
  30. package/lib/compiler/internal/build/BuildExecution.d.ts +11 -7
  31. package/lib/compiler/internal/build/BuildExecution.js +22 -8
  32. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  33. package/lib/compiler/internal/build/ResidentCheckWatchSession.d.ts +10 -10
  34. package/lib/compiler/internal/build/ResidentCheckWatchSession.js +19 -14
  35. package/lib/compiler/internal/build/ResidentCheckWatchSession.js.map +1 -1
  36. package/lib/compiler/internal/build/RunBuildOptions.d.ts +8 -1
  37. package/lib/compiler/internal/build/TsgoArguments.d.ts +16 -0
  38. package/lib/compiler/internal/build/TsgoArguments.js +19 -0
  39. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  40. package/lib/compiler/internal/captureProcessOutput.d.ts +16 -13
  41. package/lib/compiler/internal/captureProcessOutput.js +22 -14
  42. package/lib/compiler/internal/captureProcessOutput.js.map +1 -1
  43. package/lib/compiler/internal/startResidentTransform.js +3 -1
  44. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  45. package/lib/compiler/internal/transformProjectInMemory.js +5 -1
  46. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  47. package/lib/launcher/internal/runtime/PluginDescriptorInputReport.d.ts +43 -0
  48. package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js +106 -0
  49. package/lib/launcher/internal/runtime/PluginDescriptorInputReport.js.map +1 -0
  50. package/lib/launcher/internal/runtime/installRuntimeHooks.js +15 -29
  51. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  52. package/lib/launcher/internal/ttscserver/runTtscserver.js +1 -0
  53. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  54. package/lib/launcher/internal/watch/WatchTopology.js +5 -4
  55. package/lib/launcher/internal/watch/WatchTopology.js.map +1 -1
  56. package/lib/plugin/internal/CapabilityResolutionFormat.js +4 -3
  57. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  58. package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
  59. package/lib/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
  60. package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js +6 -0
  61. package/lib/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.js.map +1 -1
  62. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +3 -3
  63. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  64. package/lib/plugin/internal/load/descriptorRecordEnvironment.d.ts +15 -0
  65. package/lib/plugin/internal/load/descriptorRecordEnvironment.js +24 -0
  66. package/lib/plugin/internal/load/descriptorRecordEnvironment.js.map +1 -0
  67. package/lib/plugin/internal/load/loadProjectPlugins.js +28 -26
  68. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  69. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
  70. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +2 -0
  71. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  72. package/lib/plugin/internal/source/SourcePackageArtifacts.d.ts +145 -0
  73. package/lib/plugin/internal/source/SourcePackageArtifacts.js +612 -0
  74. package/lib/plugin/internal/source/SourcePackageArtifacts.js.map +1 -0
  75. package/lib/plugin/internal/source/buildSourcePlugin.js +54 -4
  76. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  77. package/lib/plugin/internal/source/prunePackageArtifactRoot.d.ts +19 -0
  78. package/lib/plugin/internal/source/prunePackageArtifactRoot.js +95 -0
  79. package/lib/plugin/internal/source/prunePackageArtifactRoot.js.map +1 -0
  80. package/lib/plugin/internal/source/resolveCleanTargets.js +1 -1
  81. package/lib/plugin/internal/source/resolveCleanTargets.js.map +1 -1
  82. package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js +9 -4
  83. package/lib/plugin/internal/source/resolveSourceBuildCachePaths.js.map +1 -1
  84. package/lib/plugin/internal/source/selectedRecordIdentityTempParent.d.ts +18 -0
  85. package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js +46 -0
  86. package/lib/plugin/internal/source/selectedRecordIdentityTempParent.js.map +1 -0
  87. package/package.json +8 -8
  88. package/src/compiler/internal/build/BuildExecution.ts +30 -10
  89. package/src/compiler/internal/build/ResidentCheckWatchSession.ts +23 -22
  90. package/src/compiler/internal/build/RunBuildOptions.ts +9 -1
  91. package/src/compiler/internal/build/TsgoArguments.ts +22 -0
  92. package/src/compiler/internal/captureProcessOutput.ts +28 -14
  93. package/src/compiler/internal/startResidentTransform.ts +7 -1
  94. package/src/compiler/internal/transformProjectInMemory.ts +9 -1
  95. package/src/launcher/internal/runtime/PluginDescriptorInputReport.ts +100 -0
  96. package/src/launcher/internal/runtime/installRuntimeHooks.ts +57 -55
  97. package/src/launcher/internal/ttscserver/runTtscserver.ts +5 -0
  98. package/src/launcher/internal/watch/WatchTopology.ts +5 -4
  99. package/src/plugin/internal/CapabilityResolutionFormat.ts +4 -3
  100. package/src/plugin/internal/load/COMMONJS_PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
  101. package/src/plugin/internal/load/PLUGIN_DESCRIPTOR_SHIM_SOURCE.ts +4 -0
  102. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +3 -3
  103. package/src/plugin/internal/load/descriptorRecordEnvironment.ts +24 -0
  104. package/src/plugin/internal/load/loadProjectPlugins.ts +45 -29
  105. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +3 -0
  106. package/src/plugin/internal/source/SourcePackageArtifacts.ts +787 -0
  107. package/src/plugin/internal/source/buildSourcePlugin.ts +82 -12
  108. package/src/plugin/internal/source/prunePackageArtifactRoot.ts +100 -0
  109. package/src/plugin/internal/source/resolveCleanTargets.ts +1 -0
  110. package/src/plugin/internal/source/resolveSourceBuildCachePaths.ts +11 -4
  111. package/src/plugin/internal/source/selectedRecordIdentityTempParent.ts +40 -0
@@ -116,14 +116,16 @@ export namespace BuildExecution {
116
116
  *
117
117
  * Caller-provided values override inherited defaults, except the invocation's
118
118
  * selected compiler remains authoritative. Absent per-invocation payloads
119
- * remove stale inherited state rather than leaking an ancestor's project.
119
+ * remove stale inherited state rather than leaking an ancestor's project. The
120
+ * emitting-watch write policy is always cleared here and only the selected
121
+ * build command may arm it for its direct native child.
120
122
  *
121
- * @evidence contracts/common.md#principled-implementation Environment layering preserves caller overrides while the resolved compiler wins last; per-run compiler/config/link payloads are published or cleared according to invocation ownership.
123
+ * @evidence contracts/common.md#principled-implementation Environment layering preserves caller overrides while the resolved compiler wins last; per-run compiler/config/link payloads are published or cleared according to invocation ownership, and the emitting-watch write policy is cleared until an authorized build child selects it.
122
124
  * @evidence contracts/common.md#clear-and-simple-design Shared environment helpers own merging and key access; runtime discovery precedes the pure composer that owns invocation payload publication.
123
125
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Supported environment channels convey compiler/plugin context without patching strict sidecar parsers; clearing inherited payloads corrects ownership rather than compensating for a stale ancestor selection.
124
126
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs explain config anchoring, linked sources, precedence and absent-payload effects without mixing prose into tags.
125
127
  * @evidence contracts/portability.md#os-neutral-implementation Native environment helpers normalize Windows key aliases while retaining POSIX case distinctions; node:path builds the default launcher path and binary values remain native executable selections.
126
- * @evidence contracts/performance.md#efficient-algorithms Environment layering copies inherited/caller fields, and Windows channel reads/writes repeatedly scan native-equivalent key names with key-text normalization costs. Linked selection/projection/JSON adds plugin count and payload work. Ordered runtime-candidate probes are delegated before publication and remain native process costs of this call.
128
+ * @evidence contracts/performance.md#efficient-algorithms Environment layering copies inherited/caller fields, and Windows channel reads/writes repeatedly scan native-equivalent key names with key-text normalization costs, including one write-policy removal. Linked selection/projection/JSON adds plugin count and payload work. Ordered runtime-candidate probes are delegated before publication and remain native process costs of this call.
127
129
  *
128
130
  * @evidenceExclude contracts/performance.md#reuse-equivalent-work Fresh environment composition depends on current invocation and mutable process environment, and coordinates no persistent producer.
129
131
  * @evidence contracts/performance.md#bound-retention-and-release-resources The fresh environment and intermediate linked payload have invocation-local ownership and transfer to the spawn caller. Runtime probe acquisition/timeout/cleanup belongs to the capability owner, not a native-release guarantee made here; this function stores no child handle or global payload.
@@ -149,6 +151,9 @@ export namespace BuildExecution {
149
151
  // The invocation's resolved compiler owns the final environment layer.
150
152
  { TTSC_TSGO_BINARY: execution.tsgo.binary },
151
153
  );
154
+ // This opt-in belongs to an emitting watch child, never to a nested build
155
+ // or a host reached through an inherited process environment.
156
+ SidecarEnvironment.write(env, "TTSC_WATCH_WRITE_IF_CHANGED", undefined);
152
157
  return composeNativePluginEnv(
153
158
  env,
154
159
  extra,
@@ -164,9 +169,11 @@ export namespace BuildExecution {
164
169
  *
165
170
  * The caller owns the fresh environment and the selected Node executable.
166
171
  * Executable capability discovery remains at the native spawn boundary; this
167
- * composer only writes protocol values and removes stale payloads.
172
+ * composer only writes protocol values and removes stale payloads. The
173
+ * invocation's opened content-identity store supplies native config-loader
174
+ * scratch placement, or clears an inherited store when none was opened.
168
175
  *
169
- * @evidence contracts/common.md#principled-implementation The selected compiler and Node executable own their channels; config, forwarded arguments and linked transforms are published or cleared according to this invocation and explicit caller ownership.
176
+ * @evidence contracts/common.md#principled-implementation The selected compiler and Node executable own their channels; config, content-identity scratch root, forwarded arguments and linked transforms are published or cleared according to this invocation and explicit caller ownership.
170
177
  * @evidence contracts/common.md#clear-and-simple-design A synchronous payload composer consumes the existing merged environment, keeping native executable discovery in nativePluginEnv without copying environment layers again.
171
178
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Supplied executable selections are preserved without guessing capabilities, launching a substitute producer or mutating the global environment.
172
179
  * @evidence contracts/common.md#meaningful-documentation Native prose states the required fresh layered environment, caller ownership and the separation of runtime discovery from protocol publication.
@@ -186,6 +193,11 @@ export namespace BuildExecution {
186
193
  CompilerProjectSelection.assertCurrent(execution.compilerSelection);
187
194
  SidecarEnvironment.write(env, "TTSC_NODE_BINARY", nodeBinary);
188
195
  SidecarEnvironment.write(env, "TTSC_TSGO_BINARY", execution.tsgo.binary);
196
+ SidecarEnvironment.write(
197
+ env,
198
+ "TTSC_NATIVE_PLUGIN_RECORD_ROOT",
199
+ execution.identities?.root,
200
+ );
189
201
  // Forwarded tsgo argv is per-invocation state this host owns, exactly like
190
202
  // the config anchor below: publish this run's payload, or drop whatever an
191
203
  // ancestor ttsc process left behind when this lane forwards nothing.
@@ -1104,7 +1116,7 @@ export namespace BuildExecution {
1104
1116
  * intact.
1105
1117
  */
1106
1118
  function runNativeCheckPlugins(
1107
- options: TtscBuildOptions,
1119
+ options: RunBuildOptions,
1108
1120
  execution: ReturnType<typeof resolveExecutionContext>,
1109
1121
  timing: BuildTiming.BuildTiming,
1110
1122
  ): TtscBuildResult {
@@ -1133,7 +1145,7 @@ export namespace BuildExecution {
1133
1145
  "ttsc.check",
1134
1146
  timing,
1135
1147
  `ttsc check plugin ${plugin.name} time`,
1136
- TsgoArguments.createNativeTsgoArgs(options),
1148
+ TsgoArguments.createNativeCheckTsgoArgs(execution, options),
1137
1149
  ),
1138
1150
  args[0],
1139
1151
  );
@@ -1212,7 +1224,7 @@ export namespace BuildExecution {
1212
1224
  * the elapsed interval also includes environment preparation, not only
1213
1225
  * execution of the selected plugin.
1214
1226
  *
1215
- * @evidence contracts/common.md#principled-implementation The selected plugin receives the composed invocation context, and actual exit status/output become a normalized result while native launch errors remain thrown failures.
1227
+ * @evidence contracts/common.md#principled-implementation The selected plugin receives the composed invocation context; only an emitting watch build child receives the write-if-changed opt-in. Actual exit status/output become a normalized result while native launch errors remain thrown failures.
1216
1228
  * @evidence contracts/common.md#clear-and-simple-design One command boundary owns environment publication, timing and output normalization for native build and check phases.
1217
1229
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Execution uses the loaded binary and supported protocol rather than patching its implementation or manufacturing success on launch failure.
1218
1230
  * @evidence contracts/common.md#meaningful-documentation Native prose distinguishes result failures, thrown launch failures and error/timing label attribution.
@@ -1226,7 +1238,7 @@ export namespace BuildExecution {
1226
1238
  export function runNativePluginCommand(
1227
1239
  plugin: ITtscLoadedNativePlugin,
1228
1240
  args: readonly string[],
1229
- options: TtscBuildOptions,
1241
+ options: RunBuildOptions,
1230
1242
  execution: ReturnType<typeof resolveExecutionContext>,
1231
1243
  label: string,
1232
1244
  timing: BuildTiming.BuildTiming,
@@ -1234,9 +1246,17 @@ export namespace BuildExecution {
1234
1246
  tsgoArgs?: string,
1235
1247
  ): TtscBuildResult {
1236
1248
  const startedAt = process.hrtime.bigint();
1249
+ const env = nativePluginEnv(options.env, execution, plugin, tsgoArgs);
1250
+ if (
1251
+ label === "ttsc.build" &&
1252
+ options.emit !== false &&
1253
+ options.watchWriteIfChanged === true
1254
+ ) {
1255
+ SidecarEnvironment.write(env, "TTSC_WATCH_WRITE_IF_CHANGED", "1");
1256
+ }
1237
1257
  const res = spawnNative(plugin.binary, args, {
1238
1258
  cwd: execution.projectRoot,
1239
- env: nativePluginEnv(options.env, execution, plugin, tsgoArgs),
1259
+ env,
1240
1260
  encoding: "utf8",
1241
1261
  });
1242
1262
  BuildTiming.recordTiming(timing, timingLabel, startedAt);
@@ -20,13 +20,13 @@ import { residentCheckRequest } from "./residentCheckRequest";
20
20
  import { takeResidentCheckEntryRequest } from "./takeResidentCheckEntryRequest";
21
21
 
22
22
  /**
23
- * Analysis-only watch coordinator.
23
+ * Watch coordinator for retained project selection and analysis-only Programs.
24
24
  *
25
25
  * The selected project and compatible check-stage processes stay resident
26
26
  * across ordinary source/data edits. A config, root-set, contributor, or plugin
27
27
  * topology transition calls for a full reset before the next cycle. Emit and
28
- * transform lanes can pass through the coordinator, but compatibility checks
29
- * keep them on the established one-shot path without starting sidecars.
28
+ * transform lanes retain only project and plugin selection; each cycle runs a
29
+ * fresh one-shot build without starting resident check sidecars.
30
30
  *
31
31
  * The watch launcher must serialize cycles and use reload when invocation
32
32
  * selection or startup environment changes. A session does not independently
@@ -34,13 +34,13 @@ import { takeResidentCheckEntryRequest } from "./takeResidentCheckEntryRequest";
34
34
  * `TTSC_WATCH_DEBUG_INPUTS` reports why selection is reset and the active
35
35
  * sidecar count being retired, and distinguishes a failed resident transport.
36
36
  *
37
- * @evidence contracts/common.md#principled-implementation Stable invocation selection and serialized cycles let compatible analysis-only checks retain their Program; explicit reload and observed input-topology changes reset selection before another cycle. Initial capture and warm refresh await the watch owner's publication before retaining topology; warm refresh compares physical dependency identity while publishing every declared-alias update.
37
+ * @evidence contracts/common.md#principled-implementation Stable invocation selection and serialized cycles let compatible analysis-only checks retain their Program and emitting lanes retain only selection; explicit reload and observed input-topology changes reset selection before another cycle. Initial capture and warm refresh await the watch owner's publication before retaining topology; warm refresh compares physical dependency identity while publishing every declared-alias update.
38
38
  * @evidence contracts/common.md#clear-and-simple-design The session owns selected execution, dependency snapshot, process identities and per-entry delivery buffers while shared BuildExecution owns one-shot phase and failure policy. Initial capture wraps the existing discovery callback, and warm refresh joins discovery, comparison and watch registration without another topology owner.
39
39
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Capability-aware residency retains the actual transport-failure one-shot path and distinct configured check positions. Pending or rejected registration cannot restamp topology, and unchanged physical dependencies cannot bypass declared-alias publication.
40
40
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs state supported lanes, reset causes and the caller's serialization/stable-invocation premise; private capture and refresh prose distinguish committed topology, physical comparison and declared-alias publication, while run and dispose document lifecycle effects.
41
41
  * @evidence contracts/portability.md#os-neutral-implementation Native filesystem checks, cache touch helpers and child creation retain selected binary paths and delegated executable/argv/environment handling. BuildExecution owns native discovery and path normalization; initial capture forwards its snapshot without native operations and warm comparison preserves physical and declared representations.
42
42
  * @evidence contracts/performance.md#efficient-algorithms Per-cycle command planning, key serialization and buffer unions process configured checks and pending path text; sorting/comparison bytes, native cache observations, subscribed input discovery and delegated checks add work. Initial capture adds one shallow options copy and one awaited callback; warm refresh adds one native discovery, linear normalized comparison and callback, avoiding reset when topology is unchanged. Accumulated output can repeatedly copy earlier phase text.
43
- * @evidence contracts/performance.md#reuse-equivalent-work Process keys include binary/name/argv/compiler payload, while stable invocation context and reload guard the remaining startup inputs. Accepted snapshots serve warm topology comparison only after watch registration completes; execution reuse also requires equal physical dependency topology.
43
+ * @evidence contracts/performance.md#reuse-equivalent-work Process keys include binary/name/argv/compiler payload, while stable invocation context and reload guard the remaining startup inputs. Accepted snapshots serve warm topology comparison only after watch registration completes; execution reuse also requires equal physical dependency topology. Emitting cycles reuse descriptor and plugin selection, then load a fresh native Program.
44
44
  * @evidence contracts/performance.md#bound-retention-and-release-resources Initial capture closes over the session and caller for its build; warm handoff retains the previous and next snapshots until successful registration replaces the accepted population, while rejection preserves it. Reset clears active maps, execution and snapshots while retiring children and join promises remain separately retained. Unknown joins retain rejection and prevent replacement; delegated termination is not descendant settlement. Stable options bound active process keys by configured checks, but pending unique paths and repeated disposal chains have no independent historical ceiling.
45
45
  */
46
46
  export class ResidentCheckWatchSession {
@@ -67,20 +67,20 @@ export class ResidentCheckWatchSession {
67
67
  * stable until reload; callers must not run overlapping cycles.
68
68
  *
69
69
  * The first cycle, and any cycle after `change.reload`, resolves the project
70
- * and plugins and starts resident check processes for compatible entries.
71
- * Later cycles reuse them and forward `change` as an incremental request. A
70
+ * and plugins. Compatible check entries may then start resident processes;
71
+ * emitting entries reuse selection but rebuild their Program each cycle. A
72
72
  * resident process that fails is retired and its entry falls back to the
73
73
  * one-shot check for that cycle. Plugin and project watch handoffs complete
74
74
  * before checks begin; reused topology is republished through the same
75
75
  * completion boundary.
76
76
  *
77
- * @evidence contracts/common.md#principled-implementation Compatible no-emit check-only selection uses the same diagnostic/failure policy as one-shot execution; reload, missing binaries and changed normalized dependency topology cold-resolve the next execution.
77
+ * @evidence contracts/common.md#principled-implementation Compatible no-emit check-only selection uses the same diagnostic/failure policy as one-shot execution; emitting selection retains its plugins but runs a fresh build, and reload, missing binaries and changed normalized dependency topology cold-resolve the next execution.
78
78
  * @evidence contracts/common.md#clear-and-simple-design The cycle separates setup/reuse, topology refresh, ordered checks and final diagnostics/timing, delegating each phase to its owning shared operation.
79
79
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Failed resident transport retires that process and uses the established real check command; the fallback neither drops forwarded compiler options nor converts a nonzero check into success.
80
80
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs document serialization, stable startup inputs, reload and failed-sidecar behavior instead of claiming arbitrary option changes are handled automatically.
81
81
  * @evidence contracts/portability.md#os-neutral-implementation Native binary existence and cache freshness use filesystem helpers; all commands preserve selected cwd and use native process/environment abstractions without shell interpolation.
82
- * @evidence contracts/performance.md#efficient-algorithms Warm cycles avoid project/plugin re-resolution until a missing binary or changed subscribed topology resets selection. Native existence/cache touches, optional onProjectInputs discovery, command/key construction, per-entry unions/sorts, delegated host work and output normalization/merging retain their actual entry/text/byte costs.
83
- * @evidence contracts/performance.md#reuse-equivalent-work Selected context and capability-supported sidecars survive only stable invocation cycles; change requests update Programs, reload/topology transitions reset them, and effectful configured entries remain separately scheduled.
82
+ * @evidence contracts/performance.md#efficient-algorithms Warm cycles avoid project/plugin re-resolution until a missing binary or changed subscribed topology resets selection. Emitting cycles still run whole-program native work; existence/cache touches, optional onProjectInputs discovery, command/key construction, per-entry unions/sorts, delegated host work and output normalization/merging retain their actual entry/text/byte costs.
83
+ * @evidence contracts/performance.md#reuse-equivalent-work Selected context and capability-supported check sidecars survive only stable invocation cycles; change requests update resident check Programs, while emitting cycles reuse selection and reload a fresh native Program. Reload/topology transitions reset selection, and effectful configured entries remain separately scheduled.
84
84
  * @evidence contracts/performance.md#bound-retention-and-release-resources The session retains active sidecars and per-entry pending changes; transport failure retires its process and awaits actual close before fallback. Reset clears active state but not unknown retiring ownership or its rejection. Unconsumed path sets have no finite bound while an earlier entry repeatedly fails; child close does not certify descendants.
85
85
  */
86
86
  public async run(
@@ -120,18 +120,6 @@ export class ResidentCheckWatchSession {
120
120
  await this.resetForRun("preparation-result");
121
121
  return BuildTiming.appendTimingOutput(prepared.result, timing);
122
122
  }
123
- if (!residentCheckExecutionIsCompatible(buildOptions, execution)) {
124
- await this.resetForRun("incompatible-execution");
125
- return BuildTiming.appendTimingOutput(
126
- BuildExecution.runPreparedBuild(
127
- options,
128
- timing,
129
- execution,
130
- buildOptions,
131
- ),
132
- timing,
133
- );
134
- }
135
123
  this.execution = execution;
136
124
  } else {
137
125
  // The session keeps running the plugin binaries it resolved without
@@ -161,6 +149,19 @@ export class ResidentCheckWatchSession {
161
149
  }
162
150
  }
163
151
 
152
+ // An emitting cycle still loads a fresh native Program. Only its stable
153
+ // project/plugin selection survives, so a source edit does not reevaluate
154
+ // descriptors while config, plugin, and input-topology changes reset it.
155
+ if (!residentCheckExecutionIsCompatible(buildOptions, execution)) {
156
+ return BuildTiming.appendTimingOutput(
157
+ BuildExecution.runPreparedBuild(options, timing, execution, {
158
+ ...buildOptions,
159
+ watchWriteIfChanged: true,
160
+ }),
161
+ timing,
162
+ );
163
+ }
164
+
164
165
  const checked = await this.runCheckPlugins(
165
166
  buildOptions,
166
167
  execution,
@@ -10,7 +10,7 @@ import type {
10
10
  * only ttsc's own lanes set (ttsx's private runtime builds, single-file emit,
11
11
  * watch).
12
12
  *
13
- * @evidence contracts/common.md#principled-implementation Public build selection is intersected with internal lane controls, preserving optional defaults while distinguishing diagnostic gating, compiler-output isolation and watch callbacks.
13
+ * @evidence contracts/common.md#principled-implementation Public build selection is intersected with internal lane controls, preserving optional defaults while distinguishing diagnostic gating, compiler-output isolation, emitting-watch output policy and watch callbacks.
14
14
  * @evidence contracts/common.md#clear-and-simple-design Lane-specific decisions are explicit options on the shared build boundary rather than hidden global switches or duplicated compiler APIs.
15
15
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Internal controls express supported runtime/watch requirements; source-map and inferred-root exceptions are documented lane policies, not test-specific escape flags.
16
16
  * @evidence contracts/common.md#meaningful-documentation Native member paragraphs explain ownership, optional-state effects and the rootDir premise, with blank lines separating documented properties.
@@ -118,6 +118,14 @@ export type RunBuildOptions = TtscBuildOptions & {
118
118
  */
119
119
  privateEmitRootDir?: string;
120
120
 
121
+ /**
122
+ * Allow the native driver used by an emitting watch transform host to leave
123
+ * byte-identical regular outputs in place. Only the watch coordinator sets
124
+ * this for a one-shot emit cycle; other builds and check commands clear the
125
+ * corresponding child environment channel.
126
+ */
127
+ watchWriteIfChanged?: boolean;
128
+
121
129
  /**
122
130
  * Receives selected native-plugin source roots after the project resolves.
123
131
  * The watch launcher uses these roots to invalidate a sidecar when its Go
@@ -220,6 +220,28 @@ export namespace TsgoArguments {
220
220
  );
221
221
  }
222
222
 
223
+ /**
224
+ * Give a check host the same private source layout as the emitting pass.
225
+ * Although the host checks without emitting, an injected output directory
226
+ * changes TypeScript-Go's inferred root during Program creation. Keep the
227
+ * diagnostic build-information destination separate from emitted state.
228
+ *
229
+ * @evidence contracts/common.md#principled-implementation Private runtime root policy is shared by check and emit hosts while a user's declared root retains precedence.
230
+ * @evidence contracts/common.md#clear-and-simple-design The existing root selector and native payload composer own the two independent decisions.
231
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The shipped driver consumes ordinary compiler arguments; no host flag or compiler extension is introduced.
232
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains why a no-emit host still needs the private layout root.
233
+ * @evidence contracts/portability.md#os-neutral-implementation The selected native root spelling crosses the JSON argument channel without shell interpolation.
234
+ * @evidence contracts/performance.md#efficient-algorithms A fixed root argument pair precedes existing argument filtering and serialization.
235
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work The payload belongs to one invocation and retains no result.
236
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Only a returned string transfers to the spawn owner.
237
+ */
238
+ export function createNativeCheckTsgoArgs(
239
+ execution: ReturnType<typeof BuildExecution.resolveExecutionContext>,
240
+ options: RunBuildOptions,
241
+ ): string | undefined {
242
+ return createNativeTsgoArgs(options, pinnedRootDirArgs(execution, options));
243
+ }
244
+
223
245
  /**
224
246
  * Forward the tsgo flags ttsc did not recognize to a native sidecar as one
225
247
  * JSON-encoded payload. The sidecar replays them through tsgo's own option
@@ -2,7 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import { createCanonicalTempDirectory } from "../../internal/createCanonicalTempDirectory";
5
+ import { selectedRecordIdentityTempParent } from "../../plugin/internal/source/selectedRecordIdentityTempParent";
5
6
  import type { CapturedProcessOutput } from "./CapturedProcessOutput";
7
+ import { SidecarEnvironment } from "./sharedHost/SidecarEnvironment";
6
8
 
7
9
  /**
8
10
  * Acquire two private files and descriptors for a child's output.
@@ -12,26 +14,38 @@ import type { CapturedProcessOutput } from "./CapturedProcessOutput";
12
14
  * string limits; file capture does not make output unbounded.
13
15
  *
14
16
  * The directory is per-call, so ordinary concurrent captures use separate
15
- * destinations. This is not held-directory protection against namespace
16
- * mutation. Descriptor acquisition failures attempt rollback; cleanup failures
17
- * are suppressed, and the temp creator can leave an unclaimed postflight
18
- * allocation. The caller owns the returned capture and must dispose it after
19
- * reading; read failures propagate, while disposal is idempotent and cleanup is
20
- * best effort.
17
+ * destinations. A descriptor child with a selected record root captures below
18
+ * its existing identities directory, keeping a project-root TEMP from changing
19
+ * the missing-input proof during evaluation. Other callers use system TEMP.
21
20
  *
22
- * @evidence contracts/common.md#principled-implementation Precreated files receive exact child stream bytes through inherited descriptors; decoding happens on read, and I/O errors cannot masquerade as empty successful output.
23
- * @evidence contracts/common.md#clear-and-simple-design One capture owns two descriptors and their private directory. Acquisition rollback and disposal share cleanup helpers, leaving consumers responsible for a finally boundary.
24
- * @evidence contracts/common.md#prohibited-implementation-shortcuts Supported Node filesystem and descriptor APIs capture diagnostics without foreign mutation, expected-output substitution or an arbitrary output ceiling.
25
- * @evidence contracts/common.md#meaningful-documentation Separate purpose, limits and ownership paragraphs follow the documentation skill and explain actual failure effects rather than claiming unlimited output.
26
- * @evidence contracts/portability.md#os-neutral-implementation Canonical temporary-directory ownership and path.join represent native paths; Node manages native descriptors. Removal remains best effort because inherited Windows handles may keep files live.
27
- * @evidence contracts/performance.md#efficient-algorithms Two streams require a fixed number of filesystem calls, including delegated canonical-parent/child observations and native creation/open work. Parent/path text and native lookup are not constant-cost; reading B bytes and optional decoding allocate complete raw/output text in O(B) storage. No output-byte quota is imposed by this capture owner.
21
+ * This is not held-directory protection against namespace mutation. Descriptor
22
+ * acquisition failures attempt rollback; cleanup failures are suppressed, and
23
+ * the temp creator can leave an unclaimed postflight allocation. The caller
24
+ * owns the returned capture and must dispose it after reading; read failures
25
+ * propagate, while disposal is idempotent and cleanup is best effort.
26
+ *
27
+ * @evidence contracts/common.md#principled-implementation Precreated files receive exact child stream bytes through inherited descriptors; a descriptor load uses only its preexisting, canonical selected identities directory, while other captures retain system TEMP. Decoding happens on read, and I/O errors cannot masquerade as empty successful output.
28
+ * @evidence contracts/common.md#clear-and-simple-design One capture owns two descriptors and their private directory. The shared record-parent selector checks the descriptor's existing cache before allocation; acquisition rollback and disposal share cleanup helpers.
29
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Supported Node filesystem and descriptor APIs capture diagnostics without foreign mutation, expected-output substitution or an arbitrary output ceiling; an absent or invalid private root does not invent a cache.
30
+ * @evidence contracts/common.md#meaningful-documentation Separate purpose, placement, limits and ownership paragraphs explain the descriptor-only placement and actual failure effects rather than claiming unlimited output.
31
+ * @evidence contracts/portability.md#os-neutral-implementation The shared selector confirms an ordinary physical identities child through native realpath and link-preserving stat; canonical temporary-directory ownership and path.join represent native paths. Removal remains best effort because inherited Windows handles may keep files live.
32
+ * @evidence contracts/performance.md#efficient-algorithms Descriptor selection adds a fixed number of native path observations before the fixed capture filesystem calls. Parent/path text and native lookup are not constant-cost; reading B bytes and optional decoding allocate complete raw/output text in O(B) storage. No output-byte quota is imposed by this capture owner.
28
33
  *
29
34
  * @evidenceExclude contracts/performance.md#reuse-equivalent-work Each child requires independent effectful output destinations; sharing prior captures would mix bytes and ownership rather than reuse equivalent work.
30
35
  *
31
36
  * @evidence contracts/performance.md#bound-retention-and-release-resources One accepted directory and two descriptors transfer to the capture until disposal. Acquisition rollback/disposal attempt close/removal; errors are suppressed, so closure and file reclamation can remain unconfirmed. Disposal marks itself consumed before cleanup and does not retry; caller-reachable capture closures retain path/state references until discarded. Output bytes are uncapped here.
32
37
  */
33
- export function captureProcessOutput(): CapturedProcessOutput {
34
- const directory = createCanonicalTempDirectory("ttsc-spawn-");
38
+ export function captureProcessOutput(
39
+ env: NodeJS.ProcessEnv = process.env,
40
+ ): CapturedProcessOutput {
41
+ const recordRoot =
42
+ env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1"
43
+ ? SidecarEnvironment.read(env, "TTSC_PLUGIN_RECORD_ROOT")
44
+ : undefined;
45
+ const directory = createCanonicalTempDirectory(
46
+ "ttsc-spawn-",
47
+ selectedRecordIdentityTempParent(recordRoot),
48
+ );
35
49
  const stdoutPath = path.join(directory, "stdout");
36
50
  const stderrPath = path.join(directory, "stderr");
37
51
  let stdoutFd: number;
@@ -121,7 +121,8 @@ export function startResidentTransform(
121
121
  * tsconfig) so config-file discovery walks the real project instead of the
122
122
  * wrapper's temp-dir ancestry, and forwards linked transform plugins via
123
123
  * `TTSC_LINKED_PLUGINS_JSON`. The load's record store proves the Node runtime
124
- * from metadata instead of streaming it again (#1723).
124
+ * from metadata instead of streaming it again (#1723), and supplies the
125
+ * invocation-owned native config-loader scratch root when available.
125
126
  */
126
127
  function residentEnv(
127
128
  context: ITtscCompilerContext,
@@ -146,6 +147,11 @@ function residentEnv(
146
147
  );
147
148
  const node = resolveNodeBinary(env, projectRoot, identities);
148
149
  SidecarEnvironment.write(env, "TTSC_NODE_BINARY", node);
150
+ SidecarEnvironment.write(
151
+ env,
152
+ "TTSC_NATIVE_PLUGIN_RECORD_ROOT",
153
+ identities?.root,
154
+ );
149
155
  // The anchor is per-invocation state owned by this host: when this run
150
156
  // declared none (and the caller's env does not name one), drop any value
151
157
  // inherited from an ancestor ttsc process so a nested build never
@@ -820,7 +820,10 @@ function serializeNativePlugins(
820
820
  * instead of the wrapper's temp-dir ancestry. For transform plugins, also
821
821
  * passes `TTSC_LINKED_PLUGINS_JSON` when linked sources are present. The load's
822
822
  * record store proves the Node runtime from metadata instead of streaming it
823
- * before every spawn of every generation (#1723).
823
+ * before every spawn of every generation (#1723). Its preexisting identity
824
+ * directory is offered to native config loaders as scratch storage on the
825
+ * project's volume; a nested ttsx descriptor load does not inherit the
826
+ * descriptor-only record-root channel.
824
827
  */
825
828
  function nativePluginEnv(
826
829
  options: ITtscCompilerContext,
@@ -846,6 +849,11 @@ function nativePluginEnv(
846
849
  );
847
850
  const node = resolveNodeBinary(env, projectRoot, identities);
848
851
  SidecarEnvironment.write(env, "TTSC_NODE_BINARY", node);
852
+ SidecarEnvironment.write(
853
+ env,
854
+ "TTSC_NATIVE_PLUGIN_RECORD_ROOT",
855
+ identities?.root,
856
+ );
849
857
  // The anchor is per-invocation state owned by this host: when this run
850
858
  // declared none (and the caller's env does not name one), drop any value
851
859
  // inherited from an ancestor ttsc process so a nested build never
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs";
2
+
3
+ import { PluginDescriptorInputObservation } from "./PluginDescriptorInputObservation";
4
+
5
+ /**
6
+ * Own the isolated evaluator's ordered NDJSON report until its final snapshot.
7
+ * One append descriptor is shared across records for the same destination. A
8
+ * changed destination closes the previous one, and any open, write or close
9
+ * failure permanently invalidates observation completeness.
10
+ *
11
+ * @evidence contracts/common.md#principled-implementation The in-memory envelope receives ordered records before synchronous file writes; finalization closes the report before the shim snapshots completeness, and every I/O refusal invalidates that proof.
12
+ * @evidence contracts/common.md#clear-and-simple-design One namespace owns report append, path switch and final close while the runtime hooks own input observation.
13
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The writer uses its own Node descriptor without replacing application filesystem functions or inferring missing input proof.
14
+ * @evidence contracts/common.md#meaningful-documentation This comment states destination identity, finalization order and failure meaning.
15
+ * @evidence contracts/portability.md#os-neutral-implementation Native output paths are passed to Node's file APIs and UTF-8 Buffer offsets count bytes on every host.
16
+ * @evidence contracts/performance.md#efficient-algorithms A batch joins and encodes once; partial writes resume at the unwritten byte offset, without reopening for every observed input.
17
+ * @evidence contracts/performance.md#reuse-equivalent-work Consecutive records for the same output reuse its open descriptor; a changed path closes the prior report before opening another.
18
+ * @evidence contracts/performance.md#bound-retention-and-release-resources At most one descriptor is retained per process, from first write through finalization, destination change, write failure or process exit; failed close is never retried blindly and invalidates the proof.
19
+ */
20
+ export namespace PluginDescriptorInputReport {
21
+ /**
22
+ * Append observations in order, invalidating proof if the report fails.
23
+ *
24
+ * @evidence contracts/common.md#principled-implementation The envelope records each supplied line first; any missing output, open failure, incomplete write or close refusal invalidates its completion bit.
25
+ * @evidence contracts/common.md#clear-and-simple-design One append operation owns the current output descriptor and serialized batch.
26
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Writes use this module's own descriptor, with no application method replacement or invented input record.
27
+ * @evidence contracts/common.md#meaningful-documentation The comment states ordering and failure effects.
28
+ * @evidence contracts/portability.md#os-neutral-implementation Node opens the native path and Buffer offsets count UTF-8 bytes independently of host character width.
29
+ * @evidence contracts/performance.md#efficient-algorithms Each batch joins and encodes once, then retries only the unwritten suffix after a partial write.
30
+ * @evidence contracts/performance.md#reuse-equivalent-work The same destination reuses its open descriptor across all batches in the evaluator; path changes close it first.
31
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One descriptor stays owned until finish, path switch, error or process exit, and a failed write closes it immediately.
32
+ */
33
+ export function append(lines: readonly string[]): void {
34
+ if (lines.length === 0) return;
35
+ if (process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE !== "1") return;
36
+ PluginDescriptorInputObservation.record(lines);
37
+ const out = process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_OUT;
38
+ if (out === undefined || out.length === 0) {
39
+ PluginDescriptorInputObservation.invalidate();
40
+ return;
41
+ }
42
+ try {
43
+ if (report?.out !== out) close();
44
+ if (report === undefined) {
45
+ report = { fd: fs.openSync(out, "a"), out };
46
+ if (!exitRegistered) {
47
+ process.on("exit", close);
48
+ exitRegistered = true;
49
+ }
50
+ }
51
+ const bytes = Buffer.from(lines.join(""), "utf8");
52
+ for (let offset = 0; offset < bytes.length; ) {
53
+ const written = fs.writeSync(
54
+ report.fd,
55
+ bytes,
56
+ offset,
57
+ bytes.length - offset,
58
+ );
59
+ if (written === 0)
60
+ throw new Error("descriptor input report made no progress");
61
+ offset += written;
62
+ }
63
+ } catch {
64
+ PluginDescriptorInputObservation.invalidate();
65
+ close();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Close the report and suspend recording before the final snapshot.
71
+ *
72
+ * @evidence contracts/common.md#principled-implementation The shim calls this after descriptor serialization so a close failure is included in the later completeness snapshot.
73
+ * @evidence contracts/common.md#clear-and-simple-design One final transition joins report closure and input recording suspension.
74
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Only the owned descriptor and protocol flag are changed, without treating a failed close as success.
75
+ * @evidence contracts/common.md#meaningful-documentation The comment names closure, recording suspension and snapshot order.
76
+ * @evidence contracts/portability.md#os-neutral-implementation The helper delegates native descriptor closure to Node and changes a process-local string flag only.
77
+ * @evidenceExclude contracts/performance.md#efficient-algorithms One close and flag assignment are independent of report size.
78
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work Effectful finalization cannot share another evaluator's close.
79
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Finalization releases the active descriptor, and repeating it after ownership clears is harmless.
80
+ */
81
+ export function finish(): void {
82
+ close();
83
+ process.env.TTSC_PLUGIN_DESCRIPTOR_INPUTS_ACTIVE = "0";
84
+ }
85
+ }
86
+
87
+ let report: { fd: number; out: string } | undefined;
88
+ let exitRegistered = false;
89
+
90
+ /** Clear uncertain ownership before closing the current report. */
91
+ function close(): void {
92
+ const current = report;
93
+ report = undefined;
94
+ if (current === undefined) return;
95
+ try {
96
+ fs.closeSync(current.fd);
97
+ } catch {
98
+ PluginDescriptorInputObservation.invalidate();
99
+ }
100
+ }