ttsc 0.31.1 → 0.31.2

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 (135) hide show
  1. package/lib/compiler/internal/build/BuildExecution.d.ts +4 -2
  2. package/lib/compiler/internal/build/BuildExecution.js +40 -12
  3. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  4. package/lib/compiler/internal/startResidentTransform.js +5 -4
  5. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  6. package/lib/compiler/internal/transformProjectInMemory.js +9 -7
  7. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  8. package/lib/internal/javascriptRuntimeCapabilities.d.ts +13 -4
  9. package/lib/internal/javascriptRuntimeCapabilities.js +56 -6
  10. package/lib/internal/javascriptRuntimeCapabilities.js.map +1 -1
  11. package/lib/internal/pluginSource.d.ts +1 -0
  12. package/lib/internal/pluginSource.js +1 -0
  13. package/lib/internal/pluginSource.js.map +1 -1
  14. package/lib/internal/resolveNodeBinary.d.ts +6 -1
  15. package/lib/internal/resolveNodeBinary.js +6 -2
  16. package/lib/internal/resolveNodeBinary.js.map +1 -1
  17. package/lib/internal/runtimeExecutableIdentity.d.ts +22 -13
  18. package/lib/internal/runtimeExecutableIdentity.js +192 -102
  19. package/lib/internal/runtimeExecutableIdentity.js.map +1 -1
  20. package/lib/launcher/internal/runtime/installRuntimeHooks.js +58 -4
  21. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  22. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.d.ts +5 -1
  23. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js +7 -2
  24. package/lib/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.js.map +1 -1
  25. package/lib/launcher/internal/ttscserver/runTtscserver.js +8 -1
  26. package/lib/launcher/internal/ttscserver/runTtscserver.js.map +1 -1
  27. package/lib/plugin/internal/CapabilityResolutionFormat.d.ts +21 -62
  28. package/lib/plugin/internal/CapabilityResolutionFormat.js +15 -106
  29. package/lib/plugin/internal/CapabilityResolutionFormat.js.map +1 -1
  30. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.d.ts +6 -0
  31. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js +4 -4
  32. package/lib/plugin/internal/load/PluginDescriptorEvaluationCache.js.map +1 -1
  33. package/lib/plugin/internal/load/loadProjectPlugins.d.ts +12 -1
  34. package/lib/plugin/internal/load/loadProjectPlugins.js +80 -20
  35. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  36. package/lib/plugin/internal/readCapabilityResolution.d.ts +6 -0
  37. package/lib/plugin/internal/readCapabilityResolution.js +8 -5
  38. package/lib/plugin/internal/readCapabilityResolution.js.map +1 -1
  39. package/lib/plugin/internal/source/GoEnvironmentReading.d.ts +9 -3
  40. package/lib/plugin/internal/source/GoEnvironmentReading.js +53 -7
  41. package/lib/plugin/internal/source/GoEnvironmentReading.js.map +1 -1
  42. package/lib/plugin/internal/source/NativeSourcePackages.d.ts +44 -6
  43. package/lib/plugin/internal/source/NativeSourcePackages.js +150 -8
  44. package/lib/plugin/internal/source/NativeSourcePackages.js.map +1 -1
  45. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.d.ts +21 -3
  46. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js +32 -7
  47. package/lib/plugin/internal/source/PluginBuildEnvironmentReadings.js.map +1 -1
  48. package/lib/plugin/internal/source/PluginContentIdentities.d.ts +265 -0
  49. package/lib/plugin/internal/source/PluginContentIdentities.js +452 -0
  50. package/lib/plugin/internal/source/PluginContentIdentities.js.map +1 -0
  51. package/lib/plugin/internal/source/PluginLoadAnswers.d.ts +63 -0
  52. package/lib/plugin/internal/source/PluginLoadAnswers.js +128 -0
  53. package/lib/plugin/internal/source/PluginLoadAnswers.js.map +1 -0
  54. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +10 -0
  55. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +12 -0
  56. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  57. package/lib/plugin/internal/source/SourcePluginWorkspace.d.ts +5 -2
  58. package/lib/plugin/internal/source/SourcePluginWorkspace.js +5 -3
  59. package/lib/plugin/internal/source/SourcePluginWorkspace.js.map +1 -1
  60. package/lib/plugin/internal/source/buildSourcePlugin.d.ts +15 -2
  61. package/lib/plugin/internal/source/buildSourcePlugin.js +27 -4
  62. package/lib/plugin/internal/source/buildSourcePlugin.js.map +1 -1
  63. package/lib/plugin/internal/source/computeCacheKey.d.ts +21 -5
  64. package/lib/plugin/internal/source/computeCacheKey.js +71 -29
  65. package/lib/plugin/internal/source/computeCacheKey.js.map +1 -1
  66. package/lib/plugin/internal/source/hashPluginBuildEnvironment.d.ts +7 -3
  67. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js +74 -35
  68. package/lib/plugin/internal/source/hashPluginBuildEnvironment.js.map +1 -1
  69. package/lib/plugin/internal/source/pluginBuildEnvironment.d.ts +8 -3
  70. package/lib/plugin/internal/source/pluginBuildEnvironment.js +8 -4
  71. package/lib/plugin/internal/source/pluginBuildEnvironment.js.map +1 -1
  72. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js +12 -2
  73. package/lib/plugin/internal/source/pluginBuildEnvironmentWorker.js.map +1 -1
  74. package/lib/plugin/internal/source/pluginBuildVersions.d.ts +2 -2
  75. package/lib/plugin/internal/source/pluginBuildVersions.js +4 -17
  76. package/lib/plugin/internal/source/pluginBuildVersions.js.map +1 -1
  77. package/lib/plugin/internal/source/pluginSourceState.d.ts +6 -2
  78. package/lib/plugin/internal/source/pluginSourceState.js +20 -8
  79. package/lib/plugin/internal/source/pluginSourceState.js.map +1 -1
  80. package/lib/plugin/internal/source/pluginSourceStateHolds.d.ts +7 -2
  81. package/lib/plugin/internal/source/pluginSourceStateHolds.js +12 -7
  82. package/lib/plugin/internal/source/pluginSourceStateHolds.js.map +1 -1
  83. package/lib/plugin/internal/source/processPluginBuildEnvironment.d.ts +7 -2
  84. package/lib/plugin/internal/source/processPluginBuildEnvironment.js +8 -3
  85. package/lib/plugin/internal/source/processPluginBuildEnvironment.js.map +1 -1
  86. package/lib/plugin/internal/source/provenPluginSourceDigest.d.ts +28 -0
  87. package/lib/plugin/internal/source/provenPluginSourceDigest.js +41 -0
  88. package/lib/plugin/internal/source/provenPluginSourceDigest.js.map +1 -0
  89. package/lib/plugin/internal/source/pruneCacheFileRoot.d.ts +3 -2
  90. package/lib/plugin/internal/source/pruneCacheFileRoot.js +3 -2
  91. package/lib/plugin/internal/source/pruneCacheFileRoot.js.map +1 -1
  92. package/lib/plugin/internal/source/ttscPackageVersion.d.ts +26 -0
  93. package/lib/plugin/internal/source/ttscPackageVersion.js +47 -0
  94. package/lib/plugin/internal/source/ttscPackageVersion.js.map +1 -0
  95. package/lib/plugin/internal/writeCapabilityResolution.d.ts +6 -0
  96. package/lib/plugin/internal/writeCapabilityResolution.js +11 -7
  97. package/lib/plugin/internal/writeCapabilityResolution.js.map +1 -1
  98. package/lib/plugin/resolveCapabilityPlugins.js +67 -15
  99. package/lib/plugin/resolveCapabilityPlugins.js.map +1 -1
  100. package/package.json +8 -8
  101. package/src/compiler/internal/build/BuildExecution.ts +46 -13
  102. package/src/compiler/internal/startResidentTransform.ts +12 -3
  103. package/src/compiler/internal/transformProjectInMemory.ts +10 -2
  104. package/src/internal/javascriptRuntimeCapabilities.ts +71 -5
  105. package/src/internal/pluginSource.ts +1 -0
  106. package/src/internal/resolveNodeBinary.ts +12 -1
  107. package/src/internal/runtimeExecutableIdentity.ts +227 -141
  108. package/src/launcher/internal/runtime/installRuntimeHooks.ts +68 -3
  109. package/src/launcher/internal/ttscserver/captureLSPPluginSelectionInputs.ts +11 -2
  110. package/src/launcher/internal/ttscserver/runTtscserver.ts +18 -1
  111. package/src/plugin/internal/CapabilityResolutionFormat.ts +23 -107
  112. package/src/plugin/internal/load/PluginDescriptorEvaluationCache.ts +14 -4
  113. package/src/plugin/internal/load/loadProjectPlugins.ts +212 -94
  114. package/src/plugin/internal/readCapabilityResolution.ts +17 -6
  115. package/src/plugin/internal/source/GoEnvironmentReading.ts +74 -5
  116. package/src/plugin/internal/source/NativeSourcePackages.ts +262 -5
  117. package/src/plugin/internal/source/PluginBuildEnvironmentReadings.ts +41 -5
  118. package/src/plugin/internal/source/PluginContentIdentities.ts +563 -0
  119. package/src/plugin/internal/source/PluginLoadAnswers.ts +151 -0
  120. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +14 -0
  121. package/src/plugin/internal/source/SourcePluginWorkspace.ts +6 -1
  122. package/src/plugin/internal/source/buildSourcePlugin.ts +43 -3
  123. package/src/plugin/internal/source/computeCacheKey.ts +103 -47
  124. package/src/plugin/internal/source/hashPluginBuildEnvironment.ts +144 -29
  125. package/src/plugin/internal/source/pluginBuildEnvironment.ts +9 -2
  126. package/src/plugin/internal/source/pluginBuildEnvironmentWorker.ts +18 -2
  127. package/src/plugin/internal/source/pluginBuildVersions.ts +5 -26
  128. package/src/plugin/internal/source/pluginSourceState.ts +28 -12
  129. package/src/plugin/internal/source/pluginSourceStateHolds.ts +15 -8
  130. package/src/plugin/internal/source/processPluginBuildEnvironment.ts +8 -2
  131. package/src/plugin/internal/source/provenPluginSourceDigest.ts +45 -0
  132. package/src/plugin/internal/source/pruneCacheFileRoot.ts +3 -2
  133. package/src/plugin/internal/source/ttscPackageVersion.ts +45 -0
  134. package/src/plugin/internal/writeCapabilityResolution.ts +29 -8
  135. package/src/plugin/resolveCapabilityPlugins.ts +87 -13
@@ -0,0 +1,563 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { SidecarEnvironment } from "../../../compiler/internal/sharedHost/SidecarEnvironment";
7
+ import { E2ETrace } from "../../../internal/E2ETrace";
8
+ import { SourceBuildCacheLayout } from "./SourceBuildCacheLayout";
9
+ import { pluginSourceCovers } from "./pluginSourceCovers";
10
+ import { pluginSourceDigest } from "./pluginSourceDigest";
11
+ import { pluginSourceFilesSignature } from "./pluginSourceFilesSignature";
12
+ import { recordCacheFileUse } from "./recordCacheFileUse";
13
+ import { resolveSourceBuildCachePaths } from "./resolveSourceBuildCachePaths";
14
+ import { ttscPackageVersion } from "./ttscPackageVersion";
15
+
16
+ /**
17
+ * Content identities a plugin load proves from metadata instead of bytes, kept
18
+ * in the plugin cache root so they outlive the process that read them.
19
+ *
20
+ * WARNING (#1721, #1722, #1723): a warm plugin load must not pay for its
21
+ * inputs' bytes. Before this owner existed, every launch, every `ttsx` process,
22
+ * every `@ttsc/unplugin` worker and the build-environment worker re-read the
23
+ * plugin module, the ttsc overlays, the whole GOROOT and the Go and JavaScript
24
+ * runtimes, because each earlier fix (#1186, #1712) kept its reuse inside one
25
+ * process. A process is not the unit of reuse: a CLI launch is one process. Any
26
+ * new content identity on the load path belongs here, and a memo that only
27
+ * lives in module state is not a fix for this class.
28
+ *
29
+ * A record pairs the digest of a selected population with the metadata
30
+ * signature of that population, and is trusted only under the separable-stamp
31
+ * rule settled by #1227 and #1344: every stamp must be strictly older than a
32
+ * reference minted on the same device by this store's own write, so no later
33
+ * write can share a recorded tick. References are minted on the cache root's
34
+ * volume and on the system temporary directory's volume, which together hold
35
+ * the project, its plugins and toolchain, and a system-installed runtime. A
36
+ * record is written only when the signature observed before the content read
37
+ * equals the one observed after it and both are separable. Reuse still assumes
38
+ * the filesystem reports writes in its metadata (identity, size, modification
39
+ * and change times); change time moves on every write and cannot be restored by
40
+ * an ordinary process. A population on a device without a reference, an
41
+ * unseparable stamp, a missing or foreign record, or any observation failure
42
+ * falls back to reading content, exactly as before this owner.
43
+ *
44
+ * Records live in single-file cache parts (`SourceBuildCacheLayout`), so the
45
+ * opportunistic collector ages them out and `ttsc clean` removes them. Their
46
+ * identity includes the record format and the ttsc version, so a release that
47
+ * changes a digest rule never reads another release's records.
48
+ *
49
+ * @evidence contracts/common.md#principled-implementation Metadata stands for recorded content only under the established separable-stamp rule, with before/after agreement around the content read; every unproved premise falls back to the content reader.
50
+ * @evidence contracts/common.md#clear-and-simple-design One store owns opening, record identity, reuse and publication; callers supply only the population observation and the content reader they already own.
51
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts No path-only or process-permanent memo stands for content; failures and unseparable stamps cause the real read rather than a guessed digest.
52
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs state the regression this owner prevents, the proof premises, the fallback and the storage lifetime.
53
+ * @evidence contracts/portability.md#os-neutral-implementation References are keyed by the native device that minted them and stamps are compared per device; no timestamp precision or platform name is assumed.
54
+ * @evidenceExclude contracts/performance.md#efficient-algorithms The namespace groups operations; each member states its own work.
55
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work Members define the reuse identity and its validity.
56
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Members define what they retain; the namespace holds no state.
57
+ */
58
+ export namespace PluginContentIdentities {
59
+ /**
60
+ * One opened record store: the physical cache root and the clock references
61
+ * minted when it was opened.
62
+ *
63
+ * @evidence contracts/common.md#principled-implementation The reference is minted by this store's own write before any observation it judges, so stamps strictly older than it cannot share a later write's tick.
64
+ * @evidence contracts/common.md#clear-and-simple-design Root, references and version travel together so every record of one load is judged against the references minted for it and one product version.
65
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The record stores observed state, not an expected digest.
66
+ * @evidence contracts/common.md#meaningful-documentation Members document their provenance and lifetime.
67
+ * @evidence contracts/portability.md#os-neutral-implementation The reference map is keyed by native device identifiers.
68
+ * @evidenceExclude contracts/performance.md#efficient-algorithms A record type runs no algorithm.
69
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work The type grants no reuse by itself; digest owns it.
70
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The opener owns the store for one load or one build; at most two device references are retained.
71
+ */
72
+ export interface Store {
73
+ /** Physical cache root that holds the identity and answer parts. */
74
+ readonly root: string;
75
+
76
+ /**
77
+ * Modification stamp of a probe written by this store, by device. A stamp
78
+ * on a device absent here is never separable.
79
+ */
80
+ readonly references: ReadonlyMap<bigint, bigint>;
81
+
82
+ /** Ttsc package version entering every record identity. */
83
+ readonly version: string;
84
+ }
85
+
86
+ /**
87
+ * One population's metadata signature and whether every stamp in it is
88
+ * separable from the store's reference.
89
+ *
90
+ * @evidence contracts/common.md#principled-implementation Signature and separability are observed together from the same metadata reading.
91
+ * @evidence contracts/common.md#clear-and-simple-design Two fields carry exactly what digest compares and admits.
92
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The signature is never treated as content; only the paired record digest is.
93
+ * @evidence contracts/common.md#meaningful-documentation Members state their meaning.
94
+ * @evidenceExclude contracts/portability.md#os-neutral-implementation The observer that builds it owns native metadata semantics.
95
+ * @evidenceExclude contracts/performance.md#efficient-algorithms A record type runs no algorithm.
96
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work The type grants no reuse by itself.
97
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The caller owns its short lifetime.
98
+ */
99
+ export interface Observation {
100
+ /** Whether every stamp precedes the reference of its own device. */
101
+ readonly separable: boolean;
102
+
103
+ /** Metadata signature of the whole selected population. */
104
+ readonly signature: string;
105
+ }
106
+
107
+ /**
108
+ * Open the store under a plugin cache root and mint its clock reference, or
109
+ * return `undefined` when the root cannot hold records, so callers read
110
+ * content as they did without one.
111
+ *
112
+ * The root follows the plugin cache policy (`resolveSourceBuildCachePaths`):
113
+ * an explicit `cacheDir`, `TTSC_CACHE_DIR`, or the workspace-local default. A
114
+ * default root is marked as selected before its first write, as every other
115
+ * default writer does, so a later root search never mistakes the new payload
116
+ * for an older orphan. One reference is minted in the root's identity
117
+ * directory, and the store exists only when that write succeeds. Another is
118
+ * minted in a private per-user directory below the system temporary
119
+ * directory, so an executable or SDK on the system volume is separable even
120
+ * when the project lives on another volume.
121
+ *
122
+ * A root inside any of `sources` is refused before anything is written: the
123
+ * build rejects such a cache (`SourcePluginAdmission`), and a record written
124
+ * there would change the very source it describes.
125
+ *
126
+ * @param props.projectRoot Project whose cache root is selected.
127
+ * @param props.cacheDir Explicit caller cache root, when one was given.
128
+ * @param props.env Effective environment selecting `TTSC_CACHE_DIR`.
129
+ * @param props.sources Plugin source directories the root must lie outside.
130
+ * @evidence contracts/common.md#principled-implementation The plugin cache policy selects the root, default roots are marked before payload, and the reference comes from a fresh owned probe; any failure withholds the store so callers keep the content path.
131
+ * @evidence contracts/common.md#clear-and-simple-design Root policy and the probe are delegated to their existing owners; this function composes them once per load.
132
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts No wall clock or historical stamp substitutes for the minted reference.
133
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs state root selection, the marker ordering and the undefined fallback.
134
+ * @evidence contracts/portability.md#os-neutral-implementation Native path resolution and the probe's device identity carry platform semantics; Windows environment names are read case-insensitively by the shared reader.
135
+ * @evidence contracts/performance.md#efficient-algorithms Root selection can walk ancestors and read manifests; each of the two probes performs one directory creation, one write, one lstat and one removal; the temporary one also checks its directory's ownership. No plugin source is read.
136
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work Each load needs a freshly minted reference; reusing an older one would defeat the separation premise.
137
+ * @evidence contracts/performance.md#bound-retention-and-release-resources Each probe is removed in its owner's finally; the per-user temporary directory stays, empty, for later loads so the temporary directory itself does not move. The returned store retains at most two device references and two strings.
138
+ */
139
+ export function open(props: {
140
+ projectRoot: string;
141
+ cacheDir?: string;
142
+ env: NodeJS.ProcessEnv;
143
+ sources?: readonly string[];
144
+ }): Store | undefined {
145
+ try {
146
+ const selected = resolveSourceBuildCachePaths(
147
+ props.projectRoot,
148
+ props.cacheDir,
149
+ props.env,
150
+ ).root;
151
+ if (coversRoot(selected, props.sources ?? [])) return undefined;
152
+ const root =
153
+ props.cacheDir || SidecarEnvironment.read(props.env, "TTSC_CACHE_DIR")
154
+ ? selected
155
+ : SourceBuildCacheLayout.markDefaultWorkspaceCacheRoot(selected);
156
+ const owned = mintReference(
157
+ path.join(root, SourceBuildCacheLayout.IDENTITY_CACHE_DIRNAME),
158
+ );
159
+ if (owned.size === 0) return undefined;
160
+ // The cache root's device covers the project, its installed plugins and
161
+ // the toolchain bundled with them. A runtime or SDK installed elsewhere,
162
+ // typically on the system drive, is judged against a second reference
163
+ // minted in a per-user directory below the system temporary directory.
164
+ const references = new Map([...temporaryReference(), ...owned]);
165
+ // WARNING (#1726): resolve no module here. A descriptor evaluator opens a
166
+ // store under its resolution recorder, and any lookup becomes an input of
167
+ // the descriptor it did not read (`ttscPackageVersion`).
168
+ return { root, references, version: ttscPackageVersion() };
169
+ } catch {
170
+ return undefined;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * The store when its root lies outside every one of `sources`, otherwise
176
+ * `undefined`. A loader opens its store before descriptor evaluation names
177
+ * the plugin sources, then narrows it here before any source is proven, so a
178
+ * misplaced cache, which the build rejects, never receives records inside a
179
+ * user's source tree.
180
+ *
181
+ * @param store The opened store, if any.
182
+ * @param sources Plugin source directories the root must lie outside.
183
+ * @evidence contracts/common.md#principled-implementation The build's own containment predicate (`pluginSourceCovers`) decides, so a root below a pruned directory is admitted exactly as the build admits it.
184
+ * @evidence contracts/common.md#clear-and-simple-design One predicate shared by open and late narrowing.
185
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts A covered root withdraws persistence rather than writing into a source.
186
+ * @evidence contracts/common.md#meaningful-documentation Native prose explains the two-phase opening.
187
+ * @evidence contracts/portability.md#os-neutral-implementation Containment uses native path grammar.
188
+ * @evidence contracts/performance.md#efficient-algorithms One relative-path check per source.
189
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work A predicate over current arguments.
190
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Nothing is retained.
191
+ */
192
+ export function outside(
193
+ store: Store | undefined,
194
+ sources: readonly string[],
195
+ ): Store | undefined {
196
+ return store === undefined || coversRoot(store.root, sources)
197
+ ? undefined
198
+ : store;
199
+ }
200
+
201
+ /**
202
+ * The digest of one population: the recorded one while its metadata still
203
+ * holds and is separable, otherwise a fresh content reading, recorded when
204
+ * its metadata held still and separable around the read.
205
+ *
206
+ * A population whose selection would include the store's own root is never
207
+ * recorded, since writing a record would change what it describes; a root
208
+ * below a pruned directory such as `node_modules` is outside every
209
+ * selection.
210
+ *
211
+ * @param store The opened store, or `undefined` to read content only.
212
+ * @param kind Population family; records of different families never mix.
213
+ * @param subject Absolute population path identifying the record.
214
+ * @param observe Current signature of the population against references.
215
+ * @param read The owning content reader; its result is the digest.
216
+ * @evidence contracts/common.md#principled-implementation A record is used only when the current separable signature equals the recorded one; publication requires equal separable signatures before and after the actual content read.
217
+ * @evidence contracts/common.md#clear-and-simple-design The caller keeps population selection and content hashing; this function owns only lookup, admission and publication.
218
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Corrupt, foreign or unproved records cause the real read; the record cannot widen what the caller's reader covers.
219
+ * @evidence contracts/common.md#meaningful-documentation Native paragraphs state the reuse condition and the self-containment exclusion; parameters describe their roles.
220
+ * @evidence contracts/portability.md#os-neutral-implementation Separability is the observer's per-device judgement; record files use native path joins and atomic same-directory replacement.
221
+ * @evidence contracts/performance.md#efficient-algorithms A hit costs one population observation and one small record read; a miss adds the content read and a second observation. Record identity hashes fixed framing text.
222
+ * @evidence contracts/performance.md#reuse-equivalent-work Processes share a digest only for the same family, subject, version and current separable signature that the producing read was bracketed by.
223
+ * @evidence contracts/performance.md#bound-retention-and-release-resources One small file per subject is published atomically and aged out by the single-file collector; hits refresh its use time; nothing is retained in memory.
224
+ */
225
+ export function digest(
226
+ store: Store | undefined,
227
+ kind: string,
228
+ subject: string,
229
+ observe: (
230
+ references: ReadonlyMap<bigint, bigint>,
231
+ ) => Observation | undefined,
232
+ read: () => string,
233
+ ): string {
234
+ if (
235
+ store === undefined ||
236
+ pluginSourceCovers(subject, store.root, "directory")
237
+ )
238
+ return read();
239
+ const file = recordFile(store, kind, subject);
240
+ const before = observe(store.references);
241
+ if (before?.separable === true) {
242
+ const known = readRecord(file);
243
+ if (
244
+ known !== undefined &&
245
+ known.signature === before.signature &&
246
+ known.version === store.version &&
247
+ known.kind === kind &&
248
+ known.subject === subject
249
+ ) {
250
+ recordCacheFileUse(file);
251
+ E2ETrace.capabilityResolution("plugin-content-identity", {
252
+ kind,
253
+ subject,
254
+ outcome: "reused",
255
+ });
256
+ return known.digest;
257
+ }
258
+ }
259
+ const value = read();
260
+ const after = observe(store.references);
261
+ const recorded =
262
+ before?.separable === true &&
263
+ after?.separable === true &&
264
+ before.signature === after.signature;
265
+ if (recorded) {
266
+ try {
267
+ fs.mkdirSync(path.dirname(file), { recursive: true });
268
+ SourceBuildCacheLayout.replaceCacheMetadataFile(
269
+ file,
270
+ JSON.stringify({
271
+ format: FORMAT,
272
+ version: store.version,
273
+ kind,
274
+ subject,
275
+ signature: after.signature,
276
+ digest: value,
277
+ } satisfies IRecord),
278
+ );
279
+ } catch {
280
+ // A record is an optimization; the digest just read stands.
281
+ }
282
+ }
283
+ E2ETrace.capabilityResolution("plugin-content-identity", {
284
+ kind,
285
+ subject,
286
+ outcome: recorded ? "recorded" : "read",
287
+ });
288
+ return value;
289
+ }
290
+
291
+ /**
292
+ * The `pluginSourceDigest` of a plugin source directory, proven from the
293
+ * metadata of exactly the files that digest reads.
294
+ *
295
+ * @param store The opened store, or `undefined` to read content only.
296
+ * @param directory The source directory.
297
+ * @evidence contracts/common.md#principled-implementation The signature walks the same `collectPluginSourceFiles` population the digest reads, with the capability cache's separable-stamp evidence; additions, removals and renames change that population.
298
+ * @evidence contracts/common.md#clear-and-simple-design Selection and hashing remain with their owners; this adapter only names the family and subject.
299
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Links and unreadable files fail as the content reader would; no metadata stands for a file the digest does not read.
300
+ * @evidence contracts/common.md#meaningful-documentation Native prose names the shared population.
301
+ * @evidence contracts/portability.md#os-neutral-implementation Native lstat device/inode/link/mode/size/stamps form each file's signature.
302
+ * @evidence contracts/performance.md#efficient-algorithms A hit enumerates and stats F files without reading content; a miss also reads B bytes and stats again.
303
+ * @evidence contracts/performance.md#reuse-equivalent-work Loads in any process share a directory digest while its separable signature holds.
304
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Storage belongs to digest.
305
+ */
306
+ export function sourceDirectory(
307
+ store: Store | undefined,
308
+ directory: string,
309
+ ): string {
310
+ const resolved = path.resolve(directory);
311
+ return digest(
312
+ store,
313
+ "plugin-source",
314
+ resolved,
315
+ (references) =>
316
+ pluginSourceFilesSignature(resolved, sourceEvidence(references)),
317
+ () => pluginSourceDigest(resolved),
318
+ );
319
+ }
320
+
321
+ /**
322
+ * One regular file's metadata signature, following links, and whether its
323
+ * stamp is separable; `undefined` when it cannot be stated.
324
+ *
325
+ * @param references The store's device references.
326
+ * @param file The file, normally already resolved to its physical path.
327
+ * @evidence contracts/common.md#principled-implementation Device, inode, mode, size and both stamps move on replacement or write; separability requires a same-device reference strictly newer than the modification stamp.
328
+ * @evidence contracts/common.md#clear-and-simple-design One stat produces both fields used by digest.
329
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts A non-regular or unreadable file yields no observation, so the content reader decides.
330
+ * @evidence contracts/common.md#meaningful-documentation Native prose states the following-link semantics and undefined meaning.
331
+ * @evidence contracts/portability.md#os-neutral-implementation Node bigint stat supplies native identity and nanosecond stamps.
332
+ * @evidence contracts/performance.md#efficient-algorithms One native stat and fixed numeric formatting.
333
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work This is a fresh observation.
334
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Nothing is retained.
335
+ */
336
+ export function fileObservation(
337
+ references: ReadonlyMap<bigint, bigint>,
338
+ file: string,
339
+ ): Observation | undefined {
340
+ try {
341
+ const stats = fs.statSync(file, { bigint: true });
342
+ if (!stats.isFile()) return undefined;
343
+ return {
344
+ separable: separable(references, stats),
345
+ signature: [
346
+ stats.dev,
347
+ stats.ino,
348
+ stats.mode,
349
+ stats.size,
350
+ stats.mtimeNs,
351
+ stats.ctimeNs,
352
+ ].join(":"),
353
+ };
354
+ } catch {
355
+ return undefined;
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Whether a stamp is strictly older than the reference its device minted.
361
+ *
362
+ * @param references The store's device references.
363
+ * @param stats Observed metadata.
364
+ * @evidence contracts/common.md#principled-implementation Only a strictly newer same-device reference separates the recorded modification tick.
365
+ * @evidence contracts/common.md#clear-and-simple-design One lookup and one comparison.
366
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts A missing reference is never replaced by wall time.
367
+ * @evidence contracts/common.md#meaningful-documentation Native prose states the strict ordering.
368
+ * @evidence contracts/portability.md#os-neutral-implementation References are matched by native device identifier.
369
+ * @evidence contracts/performance.md#efficient-algorithms One map lookup and one bigint comparison.
370
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work A scalar predicate.
371
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources Nothing is retained.
372
+ */
373
+ export function separable(
374
+ references: ReadonlyMap<bigint, bigint>,
375
+ stats: fs.BigIntStats,
376
+ ): boolean {
377
+ const minted = references.get(stats.dev);
378
+ return minted !== undefined && stats.mtimeNs < minted;
379
+ }
380
+
381
+ /**
382
+ * A clock reference minted by a write of the cache's own: the modification
383
+ * stamp a fresh probe file got in `directory`, by the device that reported
384
+ * it.
385
+ *
386
+ * A stamp strictly older than the same device's fresh reference satisfies the
387
+ * separation policy. Reuse still assumes the filesystem's reported metadata
388
+ * reflects writes; the comparison cannot certify arbitrary timestamp
389
+ * restoration or future clock behavior. The probe is the cache's own file,
390
+ * named for this call, and removal is attempted at once, so concurrent
391
+ * readers never lend each other a reference. A failed write, or a population
392
+ * on another device, leaves nothing separable, and the proof reads the
393
+ * files.
394
+ *
395
+ * @param directory Cache-owned directory the probe is written in.
396
+ * @evidence contracts/common.md#principled-implementation A unique freshly-written file supplies its native device/mtime reference; acquisition failure clears the witness, and observers require a matching device and strictly older reported stamp under the metadata policy.
397
+ * @evidence contracts/common.md#clear-and-simple-design The function returns only device-to-stamp evidence and owns its transient probe from creation through cleanup.
398
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Random probe identity avoids borrowing a concurrent call's witness; failed acquisition does not substitute wall-clock guesses for device evidence.
399
+ * @evidence contracts/common.md#meaningful-documentation Native JSDoc explains ended ticks, cross-device refusal and probe lifetime in separate paragraphs; tag separation follows the documentation skill.
400
+ * @evidence contracts/portability.md#os-neutral-implementation Native joins and bigint lstat use the actual filesystem's device/stamp values; no fixed timestamp precision or OS-specific temp path is assumed.
401
+ * @evidence contracts/performance.md#efficient-algorithms Probe path/UUID/hrtime text construction and mkdir/write/lstat/removal queries produce a witness without reading any population bytes.
402
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work Each proof needs a fresh clock reference; sharing an earlier probe would defeat the rollback/separation premise.
403
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The unique call-owned probe is removed in finally even after acquisition failure; removal is best effort and the returned map contains at most its single observed device.
404
+ */
405
+ export function mintReference(
406
+ directory: string,
407
+ ): ReadonlyMap<bigint, bigint> {
408
+ const references = new Map<bigint, bigint>();
409
+ const probe = path.join(
410
+ directory,
411
+ `clock-${process.pid}-${crypto.randomUUID()}.probe`,
412
+ );
413
+ try {
414
+ fs.mkdirSync(directory, { recursive: true });
415
+ fs.writeFileSync(probe, `${process.hrtime.bigint()}\n`);
416
+ const stats = fs.lstatSync(probe, { bigint: true });
417
+ references.set(stats.dev, stats.mtimeNs);
418
+ } catch {
419
+ references.clear();
420
+ } finally {
421
+ try {
422
+ fs.rmSync(probe, { force: true });
423
+ } catch {
424
+ // A leftover probe contains this call's hrtime text, not source data.
425
+ }
426
+ }
427
+ return references;
428
+ }
429
+
430
+ /**
431
+ * One plugin source file's metadata signature, and whether its stamp is
432
+ * separable from `references`, for `pluginSourceFilesSignature`.
433
+ *
434
+ * The files the digest reads are regular files (`collectPluginSourceFiles`),
435
+ * so these fields are the observer's metadata signature. Native metadata
436
+ * reflecting a write/replacement is a reuse premise; the signature is not
437
+ * itself a content hash or proof against arbitrary timestamp restoration.
438
+ *
439
+ * @param references The clock references minted for this proof.
440
+ * @evidence contracts/common.md#principled-implementation lstat device/inode/link/mode/size/timestamps form the regular-file metadata signature; strict mtime-before-reference marks separation for a matching device under the native metadata policy, without observing content.
441
+ * @evidence contracts/common.md#clear-and-simple-design A closure carries one proof's device references and maps each requested file to signature/separation together.
442
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts Unreadable files return no witness, and unmatched device or same/newer stamps cannot authorize digest reuse.
443
+ * @evidence contracts/common.md#meaningful-documentation Native JSDoc states the regular-file premise and reference origin, with separate acknowledgment prose following the documentation skill.
444
+ * @evidence contracts/portability.md#os-neutral-implementation Metadata comes from native bigint lstat and actual device IDs; separators join numeric fields only and no host path is manually parsed.
445
+ * @evidence contracts/performance.md#efficient-algorithms Each invocation performs native path/lstat work and formats seven bigint fields without reading content.
446
+ * @evidence contracts/performance.md#reuse-equivalent-work The closure reuses one reference map across files in the same proof, while each file's current metadata is read anew.
447
+ * @evidence contracts/performance.md#bound-retention-and-release-resources The closure retains the supplied reference map until its consumer releases it; no file handle or global population is retained.
448
+ */
449
+ export function sourceEvidence(
450
+ references: ReadonlyMap<bigint, bigint>,
451
+ ): (file: string) => { separable: boolean; signature: string } | undefined {
452
+ return (file) => {
453
+ try {
454
+ const stats = fs.lstatSync(file, { bigint: true });
455
+ return {
456
+ separable: separable(references, stats),
457
+ signature: [
458
+ stats.dev,
459
+ stats.ino,
460
+ stats.nlink,
461
+ stats.mode,
462
+ stats.size,
463
+ stats.mtimeNs,
464
+ stats.ctimeNs,
465
+ ].join(":"),
466
+ };
467
+ } catch {
468
+ return undefined;
469
+ }
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Record format tag. Move it whenever the record shape or the meaning of a
475
+ * family's digest changes; the ttsc version already separates releases.
476
+ */
477
+ const FORMAT = "ttsc-content-identity-v1";
478
+
479
+ /** Directory below the system temporary directory that holds its probes. */
480
+ const TEMPORARY_REFERENCE_DIRNAME = "ttsc-clock-references";
481
+
482
+ interface IRecord {
483
+ format: string;
484
+ version: string;
485
+ kind: string;
486
+ subject: string;
487
+ signature: string;
488
+ digest: string;
489
+ }
490
+
491
+ // The temporary-volume reference lives in one directory reused by every
492
+ // load: creating and removing an entry of the temporary directory itself on
493
+ // each load would move that directory's metadata, which is the signature of
494
+ // every missing path below it a descriptor proof observed (#1721). A shared
495
+ // directory such as POSIX `/tmp` is writable by every user and a fixed name
496
+ // there could be another user's, so the directory is named for this user,
497
+ // created private, and used only while it is a real directory this user owns
498
+ // that nobody else can write to.
499
+ function temporaryReference(): ReadonlyMap<bigint, bigint> {
500
+ try {
501
+ const uid =
502
+ typeof process.getuid === "function" ? process.getuid() : undefined;
503
+ const directory = path.join(
504
+ os.tmpdir(),
505
+ uid === undefined
506
+ ? TEMPORARY_REFERENCE_DIRNAME
507
+ : `${TEMPORARY_REFERENCE_DIRNAME}-${uid}`,
508
+ );
509
+ try {
510
+ fs.mkdirSync(directory, { mode: 0o700 });
511
+ } catch (error) {
512
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST")
513
+ return new Map();
514
+ }
515
+ const stats = fs.lstatSync(directory);
516
+ if (
517
+ !stats.isDirectory() ||
518
+ (uid !== undefined && (stats.uid !== uid || (stats.mode & 0o022) !== 0))
519
+ )
520
+ return new Map();
521
+ return mintReference(directory);
522
+ } catch {
523
+ return new Map();
524
+ }
525
+ }
526
+
527
+ function coversRoot(root: string, sources: readonly string[]): boolean {
528
+ const resolved = path.resolve(root);
529
+ return sources.some((source) =>
530
+ pluginSourceCovers(path.resolve(source), resolved, "directory"),
531
+ );
532
+ }
533
+
534
+ function recordFile(store: Store, kind: string, subject: string): string {
535
+ const key = crypto
536
+ .createHash("sha256")
537
+ .update(JSON.stringify([FORMAT, store.version, kind, subject]))
538
+ .digest("hex");
539
+ return path.join(
540
+ store.root,
541
+ SourceBuildCacheLayout.IDENTITY_CACHE_DIRNAME,
542
+ `${key}.json`,
543
+ );
544
+ }
545
+
546
+ function readRecord(file: string): IRecord | undefined {
547
+ try {
548
+ const value = JSON.parse(
549
+ fs.readFileSync(file, "utf8"),
550
+ ) as Partial<IRecord>;
551
+ return value.format === FORMAT &&
552
+ typeof value.version === "string" &&
553
+ typeof value.kind === "string" &&
554
+ typeof value.subject === "string" &&
555
+ typeof value.signature === "string" &&
556
+ typeof value.digest === "string"
557
+ ? (value as IRecord)
558
+ : undefined;
559
+ } catch {
560
+ return undefined;
561
+ }
562
+ }
563
+ }