ttsc 0.34.2 → 0.35.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 (76) hide show
  1. package/driver/compileraliases/names.go +69 -0
  2. package/driver/compileraliases/names_test.go +57 -0
  3. package/lib/compiler/internal/build/BuildExecution.d.ts +26 -0
  4. package/lib/compiler/internal/build/BuildExecution.js +13 -3
  5. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  6. package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -0
  7. package/lib/compiler/internal/build/RuntimeBuildCache.d.ts +47 -0
  8. package/lib/compiler/internal/build/RuntimeBuildCache.js +235 -0
  9. package/lib/compiler/internal/build/RuntimeBuildCache.js.map +1 -0
  10. package/lib/compiler/internal/build/RuntimeBuildInputs.d.ts +44 -0
  11. package/lib/compiler/internal/build/RuntimeBuildInputs.js +282 -0
  12. package/lib/compiler/internal/build/RuntimeBuildInputs.js.map +1 -0
  13. package/lib/compiler/internal/build/RuntimeInputProof.d.ts +50 -0
  14. package/lib/compiler/internal/build/RuntimeInputProof.js +315 -0
  15. package/lib/compiler/internal/build/RuntimeInputProof.js.map +1 -0
  16. package/lib/compiler/internal/build/TsgoArguments.d.ts +5 -4
  17. package/lib/compiler/internal/build/TsgoArguments.js +11 -5
  18. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  19. package/lib/compiler/internal/build/runBuild.d.ts +3 -3
  20. package/lib/compiler/internal/build/runBuild.js +17 -4
  21. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  22. package/lib/compiler/internal/resolveNativeTypeScriptPackage.d.ts +27 -0
  23. package/lib/compiler/internal/resolveNativeTypeScriptPackage.js +103 -0
  24. package/lib/compiler/internal/resolveNativeTypeScriptPackage.js.map +1 -0
  25. package/lib/compiler/internal/resolveTsgo.d.ts +7 -6
  26. package/lib/compiler/internal/resolveTsgo.js +16 -35
  27. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  28. package/lib/compiler/internal/runNativeCheckWithObservations.d.ts +18 -14
  29. package/lib/compiler/internal/runNativeCheckWithObservations.js +42 -18
  30. package/lib/compiler/internal/runNativeCheckWithObservations.js.map +1 -1
  31. package/lib/flags/FLAG_SCHEMA.js +7 -0
  32. package/lib/flags/FLAG_SCHEMA.js.map +1 -1
  33. package/lib/internal/nativeBinary.d.ts +4 -0
  34. package/lib/internal/nativeBinary.js +6 -1
  35. package/lib/internal/nativeBinary.js.map +1 -1
  36. package/lib/launcher/internal/parseTtsxCLI.d.ts +1 -0
  37. package/lib/launcher/internal/parseTtsxCLI.js +1 -0
  38. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  39. package/lib/launcher/internal/prepareExecution.d.ts +2 -0
  40. package/lib/launcher/internal/prepareExecution.js +11 -2
  41. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  42. package/lib/launcher/internal/runTtsx.js +5 -0
  43. package/lib/launcher/internal/runTtsx.js.map +1 -1
  44. package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -0
  45. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  46. package/lib/plugin/internal/load/loadProjectPlugins.js +1 -0
  47. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  48. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
  49. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +3 -0
  50. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  51. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +8 -7
  52. package/lib/plugin/internal/source/pluginBuildVersions.js +13 -13
  53. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  54. package/lib/structures/ITtscPluginCapabilities.d.ts +13 -2
  55. package/package.json +9 -9
  56. package/src/compiler/internal/build/BuildExecution.ts +32 -16
  57. package/src/compiler/internal/build/RunBuildOptions.ts +3 -0
  58. package/src/compiler/internal/build/RuntimeBuildCache.ts +252 -0
  59. package/src/compiler/internal/build/RuntimeBuildInputs.ts +349 -0
  60. package/src/compiler/internal/build/RuntimeInputProof.ts +346 -0
  61. package/src/compiler/internal/build/TsgoArguments.ts +11 -5
  62. package/src/compiler/internal/build/runBuild.ts +20 -5
  63. package/src/compiler/internal/resolveNativeTypeScriptPackage.ts +110 -0
  64. package/src/compiler/internal/resolveTsgo.ts +17 -36
  65. package/src/compiler/internal/runNativeCheckWithObservations.ts +41 -18
  66. package/src/flags/FLAG_SCHEMA.ts +7 -0
  67. package/src/internal/nativeBinary.ts +4 -0
  68. package/src/launcher/internal/parseTtsxCLI.ts +1 -0
  69. package/src/launcher/internal/prepareExecution.ts +20 -2
  70. package/src/launcher/internal/runTtsx.ts +5 -0
  71. package/src/launcher/internal/runtime/installRuntimeHooks.ts +11 -0
  72. package/src/plugin/internal/load/loadProjectPlugins.ts +1 -0
  73. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +4 -0
  74. package/src/plugin/internal/source/pluginBuildVersions.ts +13 -15
  75. package/src/structures/ITtscPluginCapabilities.ts +14 -2
  76. package/utility/host.go +22 -1
@@ -7,36 +7,40 @@ import type { TtscBuildResult } from "../../structures/internal/TtscBuildResult"
7
7
  import { parseReferenceGraph } from "./parseNativeTransformOutput";
8
8
 
9
9
  /**
10
- * Execute a check with its same-generation input observations, preserving the
11
- * selected host, original diagnostics, streams and status.
10
+ * Execute a check or opted-in build with same-generation input observations,
11
+ * preserving the selected host, original diagnostics, streams and status.
12
12
  *
13
- * The selected native verb must be check before negotiating this check-only
14
- * channel. A check-stage descriptor can also serve effectful fix/format verbs;
15
- * those retain their original result and never acquire an invented check graph.
16
- * An opted-in host writes a private sidecar even when its check fails. Hosts
13
+ * The selected native verb must be check, or build with the separate
14
+ * emitObservations capability, before negotiating its generation channel. A
15
+ * check-stage descriptor can also serve effectful fix/format verbs; those
16
+ * retain their original result and never acquire an invented check graph. An
17
+ * opted-in host writes a private sidecar even when its check fails. Hosts
17
18
  * without that transport run their original command under their existing
18
19
  * descriptor and declared-input contract; omission does not assert complete
19
20
  * driver observations. No check is rerun to infer what the earlier generation
20
21
  * read. Missing metadata after an interrupted failed opted-in check likewise
21
- * withholds reuse; missing or malformed successful metadata is a protocol
22
- * error, not an empty proof.
22
+ * withholds reuse. Missing or malformed successful check metadata is a protocol
23
+ * error. Build metadata is optional cache evidence: unavailable setup, records
24
+ * or cleanup retain the original build result with incomplete observations, so
25
+ * loss of caching cannot turn a fresh successful emit into failure.
23
26
  *
24
27
  * The callback receives only additional transport arguments. This operation
25
28
  * owns their accepted canonical private directory and attempts file and empty
26
29
  * directory removal without recursive deletion. Setup observes ownership before
27
30
  * entering the cleanup boundary, so a setup observation failure can leave the
28
31
  * allocation unclaimed. Cleanup failures retain the original thrown failure or
29
- * completed result as their cause. Before reading and removing the artifact,
30
- * the parent must still have its captured native identity; a replaced directory
31
- * is left untouched and reported as failure. These observations do not provide
32
- * an atomic descriptor or ABA guarantee.
32
+ * completed check result as their cause. Before reading and removing the
33
+ * artifact, the parent must still have its captured native identity; a replaced
34
+ * directory is left untouched and withholds build reuse or reports check
35
+ * failure. These observations do not provide an atomic descriptor or ABA
36
+ * guarantee.
33
37
  *
34
- * @evidence contracts/common.md#principled-implementation Only an actual check verb with the declared capability negotiates same-generation observations; effectful write verbs retain their own result without being projected as reusable check generations. The selected check generation's sidecar carries driver-observed input states; hosts with a different observation contract keep their original command and declared-input authority without fabricating driver completeness or incompleteness.
38
+ * @evidence contracts/common.md#principled-implementation Actual check and build verbs negotiate independent declared observation capabilities; fix/format verbs retain their own result. Build witness IO/protocol failures preserve the one original build with incomplete observations, while checks retain their strict metadata contract. Legacy hosts keep their original command without fabricated completeness.
35
39
  * @evidence contracts/common.md#clear-and-simple-design The actual verb and descriptor opt-in jointly select one adapter for strict wire decoding and private artifact lifetime; its callback retains command and diagnostic normalization ownership.
36
40
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Neither a later project-input query nor a separate transform can reconstruct check observations; unknown hosts receive no guessed flag and malformed metadata cannot become a complete empty observation.
37
41
  * @evidence contracts/common.md#meaningful-documentation Native paragraphs distinguish failed-check metadata, unavailable transport, protocol errors and cleanup ownership, with body and tags separated according to the documentation skill.
38
42
  * @evidence contracts/portability.md#os-neutral-implementation Node native absolute paths locate the canonical private artifact, and an additional argv element conveys it without shell quoting or platform-specific filename guesses.
39
- * @evidence contracts/performance.md#efficient-algorithms One original check callback runs; this adapter adds canonical temp acquisition/native ownership observations, complete sidecar read/JSON decoding and indexed input membership/witness validation. Work and temporary storage follow wire/path bytes and declared inputs plus delegated check runtime; it does not add a second compiler generation or source-content rehash, and supplies no callback timeout or metadata-byte ceiling.
43
+ * @evidence contracts/performance.md#efficient-algorithms One original check or build callback runs; this adapter adds canonical temp acquisition/native ownership observations, complete sidecar read/JSON decoding and indexed input membership/witness validation. Work and temporary storage follow wire/path bytes and declared inputs plus delegated producer runtime; it does not add a second compiler generation or source-content rehash, and supplies no callback timeout or metadata-byte ceiling.
40
44
  *
41
45
  * @evidenceExclude contracts/performance.md#reuse-equivalent-work This adapter supplies the generation's proof to its cache owner rather than retaining or sharing a completed check itself.
42
46
  *
@@ -48,17 +52,32 @@ export function runNativeCheckWithObservations(
48
52
  /** Actual argv verb; legacy direct callers already represent a check. */
49
53
  command: string = "check",
50
54
  ): TtscBuildResult {
51
- if (command !== "check" || plugin.capabilities?.checkObservations !== true)
55
+ const emitting =
56
+ command === "build" && plugin.capabilities?.emitObservations === true;
57
+ if (
58
+ !emitting &&
59
+ (command !== "check" || plugin.capabilities?.checkObservations !== true)
60
+ )
52
61
  return run([]);
53
62
 
54
- const directory = createCanonicalTempDirectory("ttsc-check-observations-");
55
- const ownership = fs.lstatSync(directory);
63
+ let directory: string;
64
+ let ownership: fs.Stats;
65
+ try {
66
+ directory = createCanonicalTempDirectory("ttsc-check-observations-");
67
+ ownership = fs.lstatSync(directory);
68
+ } catch (error) {
69
+ if (emitting) return { ...run([]), observationsComplete: false };
70
+ throw error;
71
+ }
56
72
  const file = path.join(directory, "observations.json");
57
73
  let completed: TtscBuildResult | undefined;
58
74
  let failed = false;
59
75
  let failure: unknown;
60
76
  try {
61
- completed = run(["--check-observations-json=" + file]);
77
+ completed = run([
78
+ (emitting ? "--emit-observations-json=" : "--check-observations-json=") +
79
+ file,
80
+ ]);
62
81
  if (completed.processCompletedNormally !== true)
63
82
  return { ...completed, observationsComplete: false };
64
83
  let text: string;
@@ -68,6 +87,7 @@ export function runNativeCheckWithObservations(
68
87
  throw new Error("check observation artifact is not an ordinary file");
69
88
  text = fs.readFileSync(file, "utf8");
70
89
  } catch (error) {
90
+ if (emitting) return { ...completed, observationsComplete: false };
71
91
  if (
72
92
  completed.status !== 0 &&
73
93
  (error as NodeJS.ErrnoException).code === "ENOENT"
@@ -80,6 +100,7 @@ export function runNativeCheckWithObservations(
80
100
  try {
81
101
  return { ...completed, ...parseObservations(JSON.parse(text)) };
82
102
  } catch (error) {
103
+ if (emitting) return { ...completed, observationsComplete: false };
83
104
  throw new Error("ttsc: invalid check observation metadata", {
84
105
  cause: new AggregateError([completed, error]),
85
106
  });
@@ -108,6 +129,8 @@ export function runNativeCheckWithObservations(
108
129
  }
109
130
  }
110
131
  if (errors.length !== 0) {
132
+ if (emitting && completed !== undefined && !failed)
133
+ return { ...completed, observationsComplete: false };
111
134
  const primary = failed ? failure : completed;
112
135
  throw new AggregateError(
113
136
  [primary, ...errors],
@@ -352,6 +352,13 @@ export const FLAG_SCHEMA: readonly FlagSpec[] = [
352
352
  // -------------------------------------------------------------------------
353
353
  // ttsx-specific options.
354
354
  // -------------------------------------------------------------------------
355
+ {
356
+ name: "--no-cache",
357
+ kind: "boolean",
358
+ subcommands: ["ttsx"],
359
+ consumedBy: ["launcher"],
360
+ description: "Disable persistent runtime project emit reuse.",
361
+ },
355
362
  {
356
363
  name: "--require",
357
364
  aliases: ["-r"],
@@ -7,5 +7,9 @@
7
7
  * `@ttsc/unplugin` runs its native watch helpers, imports them from here, so
8
8
  * the helper always comes from the binary ttsc itself uses and no consumer
9
9
  * carries a copy of the rules.
10
+ *
11
+ * `resolveTsgo` selects the consumer's native TypeScript compiler, including
12
+ * npm aliases targeting TypeScript used alongside legacy tooling.
10
13
  */
11
14
  export { resolveBinary } from "../compiler/internal/resolveBinary";
15
+ export { resolveTsgo } from "../compiler/internal/resolveTsgo";
@@ -120,6 +120,7 @@ export function parseTtsxCLI(argv: readonly string[]) {
120
120
  checkers: getNumber(result, "--checkers"),
121
121
  cwd: getString(result, "--cwd"),
122
122
  entry,
123
+ noCache: getBoolean(result, "--no-cache") === true,
123
124
  noPlugins: getBoolean(result, "--no-plugins") === true,
124
125
  passthrough: postEntryArgs,
125
126
  preload,
@@ -65,6 +65,9 @@ export function prepareExecution(
65
65
 
66
66
  /** Internal cache key for more than one checked entry in this process. */
67
67
  runtimeCacheKey?: string;
68
+
69
+ /** False bypasses persistent project emits while keeping normal execution. */
70
+ runtimeCache?: boolean;
68
71
  } = {},
69
72
  ): {
70
73
  /** Claimed runtime generation whose cleanup ownership transfers to the caller. */
@@ -383,6 +386,15 @@ function createProjectContext(
383
386
  runtimeCacheKey,
384
387
  processDir,
385
388
  pluginCacheDir: explicitCacheDir === undefined ? undefined : cacheDir,
389
+ runtimeBuildCacheDir:
390
+ options.runtimeCache === false ||
391
+ env.TTSX_DISABLE_CACHE === "1" ||
392
+ env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1"
393
+ ? undefined
394
+ : path.join(
395
+ cacheRoot,
396
+ SourceBuildCacheLayout.RUNTIME_BUILD_CACHE_DIRNAME,
397
+ ),
386
398
  orphanCacheDir: path.join(
387
399
  cacheRoot,
388
400
  SourceBuildCacheLayout.ORPHAN_CACHE_DIRNAME,
@@ -527,6 +539,7 @@ function buildProject(
527
539
  emit: true,
528
540
  env: options.env,
529
541
  cacheDir: context.pluginCacheDir,
542
+ runtimeCacheDir: context.runtimeBuildCacheDir,
530
543
  outDir: context.emitDir,
531
544
  // Every output this build writes stays in ttsx's private directory: a
532
545
  // declared `declarationDir`, `tsBuildInfoFile`, or `outFile`, and any
@@ -743,7 +756,9 @@ function collectLinkDirectories(projectRoot: string): string[] {
743
756
  }
744
757
 
745
758
  /**
746
- * Whether mirroring `directory` would reach a filesystem or temporary root.
759
+ * Whether mirroring `directory` would reach a filesystem root, the temporary
760
+ * root or any of its ancestors. A caller can place its temporary root inside
761
+ * its project; its parents still contain shared entries outside that project.
747
762
  * Identity comparison is required on Windows, where `os.tmpdir()` can carry an
748
763
  * 8.3 component while a project created below it is returned in long spelling.
749
764
  */
@@ -754,7 +769,10 @@ function isUnsafeVirtualParent(
754
769
  const resolved = identities.resolve(directory);
755
770
  const root = identities.resolve(path.parse(resolved.path).root);
756
771
  const temporaryRoot = identities.resolve(os.tmpdir());
757
- return resolved.key === root.key || resolved.key === temporaryRoot.key;
772
+ return (
773
+ resolved.key === root.key ||
774
+ identities.isWithin(resolved.path, temporaryRoot.path)
775
+ );
758
776
  }
759
777
 
760
778
  /**
@@ -110,6 +110,7 @@ async function run(
110
110
  cacheDir: resolveCacheDir(cwd, parsed.cacheDir),
111
111
  checkers: parsed.checkers,
112
112
  cwd,
113
+ env: parsed.noCache ? { TTSX_DISABLE_CACHE: "1" } : undefined,
113
114
  passthrough: parsed.tsgoFlags,
114
115
  // `--no-plugins` builds the entry's owning project with plugin
115
116
  // discovery and loading disabled. ttsc's own config loaders use it
@@ -121,6 +122,7 @@ async function run(
121
122
  // tsconfig deliberately does not satisfy.
122
123
  plugins: parsed.noPlugins ? false : undefined,
123
124
  project: parsed.project,
125
+ runtimeCache: !parsed.noCache,
124
126
  singleThreaded: parsed.singleThreaded,
125
127
  });
126
128
  await settleSignals();
@@ -159,6 +161,7 @@ function printHelp(): void {
159
161
  " -P, --project <file> Use an explicit tsconfig.json",
160
162
  " --cwd <dir> Resolve entry/project relative to this directory",
161
163
  " --cache-dir <dir> Override the runner and source-plugin cache root",
164
+ " --no-cache Disable persistent runtime project emit reuse",
162
165
  " --binary <path> Use an explicit tsgo binary",
163
166
  " --no-plugins Build the project without ttsc plugins",
164
167
  " -r, --require <module> Preload a module before the entrypoint",
@@ -244,6 +247,7 @@ async function runJavaScriptEntry(
244
247
  ];
245
248
  const env: NodeJS.ProcessEnv = {
246
249
  ...process.env,
250
+ ...(parsed.noCache ? { TTSX_DISABLE_CACHE: "1" } : {}),
247
251
  // ttsc's own helpers run with what the user passed (#1740).
248
252
  ...TtsxNodeOptions.give(
249
253
  process.env,
@@ -329,6 +333,7 @@ async function runPreparedEntry(
329
333
  ];
330
334
  const runtimeEnv: NodeJS.ProcessEnv = {
331
335
  ...process.env,
336
+ ...(parsed.noCache ? { TTSX_DISABLE_CACHE: "1" } : {}),
332
337
  // ttsc's own helpers run with what the user passed (#1740).
333
338
  ...TtsxNodeOptions.give(
334
339
  process.env,
@@ -22,7 +22,9 @@ import { createCanonicalTempDirectory } from "../../../internal/createCanonicalT
22
22
  import { runtimeExecutableIdentity } from "../../../internal/runtimeExecutableIdentity";
23
23
  import { moduleResolutionBaseSelects } from "../../../plugin/internal/load/moduleResolutionBaseSelects";
24
24
  import { PluginContentIdentities } from "../../../plugin/internal/source/PluginContentIdentities";
25
+ import { SourceBuildCacheLayout } from "../../../plugin/internal/source/SourceBuildCacheLayout";
25
26
  import { recordCacheFileUse } from "../../../plugin/internal/source/recordCacheFileUse";
27
+ import { resolveSourceBuildCachePaths } from "../../../plugin/internal/source/resolveSourceBuildCachePaths";
26
28
  import { buildSingleRootProject } from "../buildSingleRootProject";
27
29
  import { inlineServedSourceMap } from "../inlineServedSourceMap";
28
30
  import { runtimeCompilerArgs } from "../runtimeCompilerArgs";
@@ -2192,6 +2194,15 @@ function buildDependency(
2192
2194
  fs.mkdirSync(emitDir, { recursive: true });
2193
2195
  const result = runBuild({
2194
2196
  cwd: project.root,
2197
+ runtimeCacheDir:
2198
+ process.env.TTSX_DISABLE_CACHE === "1" ||
2199
+ process.env.TTSC_PLUGIN_DESCRIPTOR_LOAD === "1"
2200
+ ? undefined
2201
+ : path.join(
2202
+ runtimeRecordStore()?.root ??
2203
+ resolveSourceBuildCachePaths(project.root).root,
2204
+ SourceBuildCacheLayout.RUNTIME_BUILD_CACHE_DIRNAME,
2205
+ ),
2195
2206
  forceEmitProvenance: true,
2196
2207
  // Emit-only means diagnostics never withhold the emit, so a dependency's
2197
2208
  // own `noEmitOnError` is switched off: honoured, it would turn any
@@ -824,6 +824,7 @@ export namespace loadProjectPlugins {
824
824
  ...record.capabilities,
825
825
  compilerArgsCwd: true,
826
826
  emitProvenance: true,
827
+ emitObservations: true,
827
828
  }
828
829
  : record.capabilities,
829
830
  config: record.config,
@@ -80,6 +80,9 @@ export namespace SourceBuildCacheLayout {
80
80
  /** Directory of the lowered orphan sources inside the cache root. */
81
81
  export const ORPHAN_CACHE_DIRNAME = "ttsx-orphan";
82
82
 
83
+ /** Checksummed completed runtime project emits, independent of run lifetime. */
84
+ export const RUNTIME_BUILD_CACHE_DIRNAME = "runtime-builds";
85
+
83
86
  /**
84
87
  * The parts of the cache root whose entries are single files, each collected
85
88
  * by `pruneCacheFileRoot` and removed by `ttsc clean`.
@@ -90,6 +93,7 @@ export namespace SourceBuildCacheLayout {
90
93
  IDENTITY_CACHE_DIRNAME,
91
94
  ANSWER_CACHE_DIRNAME,
92
95
  ORPHAN_CACHE_DIRNAME,
96
+ RUNTIME_BUILD_CACHE_DIRNAME,
93
97
  ];
94
98
 
95
99
  /**
@@ -1,7 +1,6 @@
1
- import fs from "node:fs";
2
- import { createRequire } from "node:module";
3
1
  import path from "node:path";
4
2
 
3
+ import { resolveNativeTypeScriptPackage } from "../../../compiler/internal/resolveNativeTypeScriptPackage";
5
4
  import { ttscPackageVersion } from "./ttscPackageVersion";
6
5
 
7
6
  /**
@@ -14,14 +13,15 @@ import { ttscPackageVersion } from "./ttscPackageVersion";
14
13
  * consumer that keeps a transform's output beyond the process that produced it,
15
14
  * as a persisted shared compile of `@ttsc/unplugin` is, names it by these
16
15
  * versions with the rule the build applies instead of a copy of it: ttsc's own
17
- * package version, and the version of the `typescript` package the project
18
- * resolves, or `"unknown"` when resolution/read/parse fails or its version is
19
- * absent. The host label falls back to `"0.0.0"` and is cached even after an
20
- * unsuccessful first read. The host installation is assumed stable for the
21
- * process lifetime; changing its manifest does not refresh that label.
16
+ * package version, and the version of the native TypeScript package the project
17
+ * selects (declared npm aliases before `typescript`), or `"unknown"` when
18
+ * resolution/read/parse fails or its version is absent. The host label falls
19
+ * back to `"0.0.0"` and is cached even after an unsuccessful first read. The
20
+ * host installation is assumed stable for the process lifetime; changing its
21
+ * manifest does not refresh that label.
22
22
  *
23
- * @param projectRoot The project base used to resolve
24
- * `typescript/package.json`.
23
+ * @param projectRoot The project base used to resolve the native TypeScript
24
+ * manifest.
25
25
  * @evidence contracts/common.md#principled-implementation Package-version labels supply the same metadata tuple used by plugin keys; absent/unreadable metadata receives the documented unknown/default label rather than an executable identity certificate.
26
26
  * @evidence contracts/common.md#clear-and-simple-design The host label comes from its own owner (`ttscPackageVersion`), which a reader needing only that label calls without this function's project resolution; the project-version reader is separate because project resolution varies per call.
27
27
  * @evidence contracts/common.md#prohibited-implementation-shortcuts Node package resolution determines the actual project dependency; no particular project or fixture version is embedded.
@@ -40,14 +40,12 @@ export function pluginBuildVersions(projectRoot: string): {
40
40
 
41
41
  function readTsgoVersion(projectRoot: string): string {
42
42
  try {
43
- const projectRequire = createRequire(
43
+ const selected = resolveNativeTypeScriptPackage(
44
44
  path.join(projectRoot, "package.json"),
45
45
  );
46
- const pkgPath = projectRequire.resolve("typescript/package.json");
47
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as {
48
- version?: string;
49
- };
50
- return pkg.version ?? "unknown";
46
+ return typeof selected?.manifest.version === "string"
47
+ ? selected.manifest.version
48
+ : "unknown";
51
49
  } catch {
52
50
  return "unknown";
53
51
  }
@@ -83,13 +83,25 @@ export interface ITtscPluginCapabilities {
83
83
  * from an output filename or configuration prediction.
84
84
  *
85
85
  * When omitted, ttsc does not pass the option to a strict third-party host. A
86
- * runtime operation requiring verified emit provenance must then report that
87
- * the selected host does not support it.
86
+ * runtime operation then uses the existing externally observed provenance
87
+ * adapter, whose incomplete proof remains visible to the runtime consumer.
88
88
  *
89
89
  * @default false
90
90
  */
91
91
  emitProvenance?: true;
92
92
 
93
+ /**
94
+ * Whether build accepts `--emit-observations-json=<absolute private path>`
95
+ * and publishes its generation's compiler and native input observations.
96
+ * Missing support keeps runtime emits fresh rather than assuming reusable
97
+ * input completeness. This protocol is independent of output ownership. Hosts
98
+ * publish the same observation envelope as checkObservations, including
99
+ * failures, before releasing their Program.
100
+ *
101
+ * @default false
102
+ */
103
+ emitObservations?: true;
104
+
93
105
  /**
94
106
  * Whether the sidecar accepts ttsc's `--project-context-json` identity
95
107
  * protocol. The payload keeps lexical selection paths, physical Program
package/utility/host.go CHANGED
@@ -40,6 +40,9 @@ type hostOptions struct {
40
40
  // checkObservationsJSON is the caller-owned absolute check input result path.
41
41
  checkObservationsJSON string
42
42
 
43
+ // emitObservationsJSON is the caller-owned absolute build input result path.
44
+ emitObservationsJSON string
45
+
43
46
  // observationsIncomplete points to the resident host's committed generation
44
47
  // state. It is nil for non-resident calls and updated only after success.
45
48
  observationsIncomplete *bool
@@ -201,6 +204,10 @@ func RunBuild(args []string) int {
201
204
  // diagnostics. Analysis-only success writes an empty map. The original build
202
205
  // status remains authoritative; absent or invalid metadata supplies no ownership
203
206
  // proof. Callers own the artifact's removal.
207
+ // A separately negotiated absolute emit-observations path receives this same
208
+ // Program's compiler and native input witnesses before its checker lease closes,
209
+ // including failing loaded builds. Unavailable optional observations withhold
210
+ // reuse without changing the build status. They supply no authority by themselves.
204
211
  // Program.Close returns its checker lease; it does not remove emitted files or
205
212
  // private metadata. Summary and diagnostic stream writes are best-effort here,
206
213
  // so a zero status does not certify delivery to the supplied io.Writer values.
@@ -211,7 +218,7 @@ func RunBuild(args []string) int {
211
218
  // @evidence contracts/common.md#meaningful-documentation Native paragraphs identify stream ownership, failure statuses, private metadata admission and caller artifact removal following the documentation skill.
212
219
  // @evidence contracts/portability.md#os-neutral-implementation Driver loading and DefaultWriteFile own native paths and filesystem APIs; output streams are not replaced globally.
213
220
  // @evidence contracts/performance.md#efficient-algorithms Admission and loading include argument/plugin-manifest parsing, native project and diagnostic work. Emission dispatches linked AST transforms through the driver, which owns native generation and one authored-coordinate correction. The final writer indexes compiler carrier/map paths and shifts generated map coordinates only when it inserts missing preamble text, including JSON/base64/VLQ and output-byte work. Optional provenance adds output-path candidate collection, successful-write bookkeeping, source-proof checks, owner sorting and JSON/file publication; costs depend on source/graph, output and metadata bytes rather than only the number of emit calls.
214
- // @evidence contracts/performance.md#reuse-equivalent-work The same loaded compiler generation validates and emits; linked program hooks remain latched rather than independently reexecuted for output files.
221
+ // @evidence contracts/performance.md#reuse-equivalent-work The same loaded compiler generation validates, emits and optionally publishes its input witnesses; linked program hooks remain latched rather than independently reexecuted for output files. Completed-generation reuse belongs to the caller.
215
222
  // @evidence contracts/performance.md#bound-retention-and-release-resources Successful loading transfers one Program checker lease to a deferred Close on analysis-only, normal and failure returns. The final writer retains output-path decisions and maps awaiting their carrier without a byte cap, releases pending map entries as their carrier arrives, and reports unfinished pairs at emit completion. Facade graphs, provenance maps, writer closures and serialization buffers lose local ownership when the invocation ends; Close does not destroy all program memory, supplied-stream state or emitted disk artifacts. Callers own stream lifetime and metadata/output removal, with no invocation-enforced byte cap.
216
223
  func RunBuildWithIO(args []string, stdout, stderr io.Writer) int {
217
224
  opts, ok := parseHostOptions("build", args, stdout, stderr)
@@ -224,6 +231,13 @@ func RunBuildWithIO(args []string, stdout, stderr io.Writer) int {
224
231
  return 2
225
232
  }
226
233
  defer prog.Close()
234
+ defer func() {
235
+ if opts.emitObservationsJSON != "" {
236
+ if err := driver.WriteCheckObservationsJSON(opts.emitObservationsJSON, prog); err != nil {
237
+ fmt.Fprintf(opts.stderr, "ttsc utility: emit observations write failed: %v\n", err)
238
+ }
239
+ }
240
+ }()
227
241
  driver.WritePrettyDiagnostics(opts.stderr, diags, opts.cwd)
228
242
  if opts.noEmit {
229
243
  if len(diags) != 0 {
@@ -446,8 +460,10 @@ func parseHostOptions(command string, args []string, stdout, stderr io.Writer) (
446
460
  tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
447
461
  provenanceJSON := ""
448
462
  checkObservationsJSON := ""
463
+ emitObservationsJSON := ""
449
464
  if command == "build" {
450
465
  fs.StringVar(&provenanceJSON, "emit-provenance-json", "", "private absolute emit provenance result path")
466
+ fs.StringVar(&emitObservationsJSON, "emit-observations-json", "", "private absolute build input result path")
451
467
  }
452
468
  if command == "check" {
453
469
  fs.StringVar(&checkObservationsJSON, "check-observations-json", "", "private absolute check input result path")
@@ -463,6 +479,10 @@ func parseHostOptions(command string, args []string, stdout, stderr io.Writer) (
463
479
  fmt.Fprintln(stderr, "ttsc utility: check observations path must be absolute")
464
480
  return hostOptions{}, false
465
481
  }
482
+ if emitObservationsJSON != "" && !filepath.IsAbs(emitObservationsJSON) {
483
+ fmt.Fprintln(stderr, "ttsc utility: emit observations path must be absolute")
484
+ return hostOptions{}, false
485
+ }
466
486
  var tsgoArgs []string
467
487
  if *tsgoArgsRaw != "" {
468
488
  if err := json.Unmarshal([]byte(*tsgoArgsRaw), &tsgoArgs); err != nil {
@@ -507,6 +527,7 @@ func parseHostOptions(command string, args []string, stdout, stderr io.Writer) (
507
527
  stderr: stderr,
508
528
  provenanceJSON: provenanceJSON,
509
529
  checkObservationsJSON: checkObservationsJSON,
530
+ emitObservationsJSON: emitObservationsJSON,
510
531
  }, true
511
532
  }
512
533