ttsc 0.34.3 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/driver/compilation_snapshot.go +267 -0
  2. package/driver/compilation_snapshot_keeps_inputs_across_stages_test.go +130 -0
  3. package/driver/compilation_snapshot_preserves_checked_emission_test.go +100 -0
  4. package/driver/graph.go +9 -3
  5. package/driver/program.go +45 -14
  6. package/lib/compiler/internal/CompilationSnapshot.d.ts +33 -0
  7. package/lib/compiler/internal/CompilationSnapshot.js +90 -0
  8. package/lib/compiler/internal/CompilationSnapshot.js.map +1 -0
  9. package/lib/compiler/internal/build/BuildExecution.d.ts +33 -4
  10. package/lib/compiler/internal/build/BuildExecution.js +81 -8
  11. package/lib/compiler/internal/build/BuildExecution.js.map +1 -1
  12. package/lib/compiler/internal/build/RunBuildOptions.d.ts +2 -0
  13. package/lib/compiler/internal/build/RuntimeBuildCache.d.ts +47 -0
  14. package/lib/compiler/internal/build/RuntimeBuildCache.js +235 -0
  15. package/lib/compiler/internal/build/RuntimeBuildCache.js.map +1 -0
  16. package/lib/compiler/internal/build/RuntimeBuildInputs.d.ts +44 -0
  17. package/lib/compiler/internal/build/RuntimeBuildInputs.js +282 -0
  18. package/lib/compiler/internal/build/RuntimeBuildInputs.js.map +1 -0
  19. package/lib/compiler/internal/build/RuntimeInputProof.d.ts +50 -0
  20. package/lib/compiler/internal/build/RuntimeInputProof.js +315 -0
  21. package/lib/compiler/internal/build/RuntimeInputProof.js.map +1 -0
  22. package/lib/compiler/internal/build/TsgoArguments.d.ts +5 -4
  23. package/lib/compiler/internal/build/TsgoArguments.js +11 -5
  24. package/lib/compiler/internal/build/TsgoArguments.js.map +1 -1
  25. package/lib/compiler/internal/build/appendBuildOutput.js +4 -0
  26. package/lib/compiler/internal/build/appendBuildOutput.js.map +1 -1
  27. package/lib/compiler/internal/build/runBuild.d.ts +3 -3
  28. package/lib/compiler/internal/build/runBuild.js +17 -4
  29. package/lib/compiler/internal/build/runBuild.js.map +1 -1
  30. package/lib/compiler/internal/build/runExternalEmitProvenance.d.ts +12 -13
  31. package/lib/compiler/internal/build/runExternalEmitProvenance.js +24 -18
  32. package/lib/compiler/internal/build/runExternalEmitProvenance.js.map +1 -1
  33. package/lib/compiler/internal/parseNativeTransformOutput.js +4 -0
  34. package/lib/compiler/internal/parseNativeTransformOutput.js.map +1 -1
  35. package/lib/compiler/internal/runNativeCheckWithObservations.d.ts +18 -14
  36. package/lib/compiler/internal/runNativeCheckWithObservations.js +42 -18
  37. package/lib/compiler/internal/runNativeCheckWithObservations.js.map +1 -1
  38. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.d.ts +2 -1
  39. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js +4 -1
  40. package/lib/compiler/internal/sharedHost/inheritedSidecarEnv.js.map +1 -1
  41. package/lib/compiler/internal/transformProjectInMemory.d.ts +3 -3
  42. package/lib/compiler/internal/transformProjectInMemory.js +29 -5
  43. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  44. package/lib/flags/FLAG_SCHEMA.js +7 -0
  45. package/lib/flags/FLAG_SCHEMA.js.map +1 -1
  46. package/lib/launcher/internal/parseTtsxCLI.d.ts +1 -0
  47. package/lib/launcher/internal/parseTtsxCLI.js +1 -0
  48. package/lib/launcher/internal/parseTtsxCLI.js.map +1 -1
  49. package/lib/launcher/internal/prepareExecution.d.ts +2 -0
  50. package/lib/launcher/internal/prepareExecution.js +6 -0
  51. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  52. package/lib/launcher/internal/runTtsx.js +5 -0
  53. package/lib/launcher/internal/runTtsx.js.map +1 -1
  54. package/lib/launcher/internal/runtime/installRuntimeHooks.js +7 -0
  55. package/lib/launcher/internal/runtime/installRuntimeHooks.js.map +1 -1
  56. package/lib/plugin/internal/load/loadProjectPlugins.js +1 -0
  57. package/lib/plugin/internal/load/loadProjectPlugins.js.map +1 -1
  58. package/lib/plugin/internal/source/SourceBuildCacheLayout.d.ts +2 -0
  59. package/lib/plugin/internal/source/SourceBuildCacheLayout.js +3 -0
  60. package/lib/plugin/internal/source/SourceBuildCacheLayout.js.map +1 -1
  61. package/lib/structures/ITtscCompilerTransformation.d.ts +5 -0
  62. package/lib/structures/ITtscPluginCapabilities.d.ts +13 -2
  63. package/package.json +8 -8
  64. package/src/compiler/internal/CompilationSnapshot.ts +97 -0
  65. package/src/compiler/internal/build/BuildExecution.ts +128 -20
  66. package/src/compiler/internal/build/RunBuildOptions.ts +3 -0
  67. package/src/compiler/internal/build/RuntimeBuildCache.ts +252 -0
  68. package/src/compiler/internal/build/RuntimeBuildInputs.ts +349 -0
  69. package/src/compiler/internal/build/RuntimeInputProof.ts +346 -0
  70. package/src/compiler/internal/build/TsgoArguments.ts +11 -5
  71. package/src/compiler/internal/build/appendBuildOutput.ts +4 -0
  72. package/src/compiler/internal/build/runBuild.ts +20 -5
  73. package/src/compiler/internal/build/runExternalEmitProvenance.ts +25 -23
  74. package/src/compiler/internal/parseNativeTransformOutput.ts +5 -0
  75. package/src/compiler/internal/runNativeCheckWithObservations.ts +41 -18
  76. package/src/compiler/internal/sharedHost/inheritedSidecarEnv.ts +8 -1
  77. package/src/compiler/internal/transformProjectInMemory.ts +46 -5
  78. package/src/flags/FLAG_SCHEMA.ts +7 -0
  79. package/src/launcher/internal/parseTtsxCLI.ts +1 -0
  80. package/src/launcher/internal/prepareExecution.ts +13 -0
  81. package/src/launcher/internal/runTtsx.ts +5 -0
  82. package/src/launcher/internal/runtime/installRuntimeHooks.ts +11 -0
  83. package/src/plugin/internal/load/loadProjectPlugins.ts +1 -0
  84. package/src/plugin/internal/source/SourceBuildCacheLayout.ts +4 -0
  85. package/src/structures/ITtscCompilerTransformation.ts +6 -0
  86. package/src/structures/ITtscPluginCapabilities.ts +14 -2
  87. package/utility/host.go +22 -1
@@ -0,0 +1,267 @@
1
+ package driver
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "encoding/json"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "sync"
10
+ "time"
11
+ "unicode/utf8"
12
+
13
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
14
+ "github.com/microsoft/typescript-go/shim/vfs"
15
+ )
16
+
17
+ // compilationSnapshotFS retains the first compiler text, identity and directory
18
+ // answers of one invocation. Sequential native check and transform stages share
19
+ // its private document; a new invocation receives a new document. This is a
20
+ // read-once compiler view, not an atomic snapshot of the whole physical volume.
21
+ // Writes remain with the underlying filesystem and cannot replace captured reads.
22
+ // The invocation owns the uncapped captured text and removes its document after
23
+ // all stages finish. The mutex serializes native compiler readers within a stage;
24
+ // the owning JS coordinator runs stages sequentially, never concurrent writers.
25
+ type compilationSnapshotFS struct {
26
+ vfs.FS
27
+ mu sync.Mutex
28
+ path string
29
+ data compilationSnapshotData
30
+ dirty bool
31
+ }
32
+
33
+ type compilationSnapshotData struct {
34
+ Version int `json:"version"`
35
+ ID string `json:"id"`
36
+ Reads map[string]compilationSnapshotRead `json:"reads"`
37
+ Files map[string]bool `json:"files"`
38
+ Directories map[string]bool `json:"directories"`
39
+ Entries map[string]vfs.Entries `json:"entries"`
40
+ Realpaths map[string]string `json:"realpaths"`
41
+ Stats map[string]*compilationSnapshotInfo `json:"stats"`
42
+ }
43
+
44
+ type compilationSnapshotInfo struct {
45
+ Filename string `json:"name"`
46
+ Length int64 `json:"size"`
47
+ Bits os.FileMode `json:"mode"`
48
+ Modified time.Time `json:"modified"`
49
+ }
50
+
51
+ func (i *compilationSnapshotInfo) Name() string { return i.Filename }
52
+ func (i *compilationSnapshotInfo) Size() int64 { return i.Length }
53
+ func (i *compilationSnapshotInfo) Mode() os.FileMode { return i.Bits }
54
+ func (i *compilationSnapshotInfo) ModTime() time.Time { return i.Modified }
55
+ func (i *compilationSnapshotInfo) IsDir() bool { return i.Bits.IsDir() }
56
+ func (i *compilationSnapshotInfo) Sys() any { return nil }
57
+
58
+ type compilationSnapshotRead struct {
59
+ Text string `json:"text"`
60
+ OK bool `json:"ok"`
61
+ }
62
+
63
+ // compilationSnapshotReadWire keeps normal UTF-8 readable and encodes only
64
+ // text JSON cannot represent losslessly. Decoding happens once when opening a
65
+ // stage, not on each cached compiler read.
66
+ type compilationSnapshotReadWire struct {
67
+ Text string `json:"text"`
68
+ OK bool `json:"ok"`
69
+ Encoding string `json:"encoding,omitempty"`
70
+ }
71
+
72
+ func (r compilationSnapshotRead) MarshalJSON() ([]byte, error) {
73
+ value := compilationSnapshotReadWire{Text: r.Text, OK: r.OK}
74
+ if !utf8.ValidString(value.Text) {
75
+ value.Text = base64.StdEncoding.EncodeToString([]byte(value.Text))
76
+ value.Encoding = "base64"
77
+ }
78
+ return json.Marshal(value)
79
+ }
80
+
81
+ func (r *compilationSnapshotRead) UnmarshalJSON(bytes []byte) error {
82
+ var value compilationSnapshotReadWire
83
+ if err := json.Unmarshal(bytes, &value); err != nil {
84
+ return err
85
+ }
86
+ switch value.Encoding {
87
+ case "":
88
+ case "base64":
89
+ decoded, err := base64.StdEncoding.DecodeString(value.Text)
90
+ if err != nil {
91
+ return fmt.Errorf("invalid compilation snapshot text: %w", err)
92
+ }
93
+ value.Text = string(decoded)
94
+ default:
95
+ return fmt.Errorf("unknown compilation snapshot text encoding %q", value.Encoding)
96
+ }
97
+ r.Text, r.OK = value.Text, value.OK
98
+ return nil
99
+ }
100
+
101
+ // openCompilationSnapshot accepts only an invocation-owned versioned document.
102
+ // Invalid or unreadable transport is an error, never permission to read live
103
+ // inputs under another stage's snapshot identity.
104
+ func openCompilationSnapshot(inner vfs.FS, path string) (*compilationSnapshotFS, error) {
105
+ bytes, err := os.ReadFile(path)
106
+ if err != nil {
107
+ return nil, err
108
+ }
109
+ var data compilationSnapshotData
110
+ if err := json.Unmarshal(bytes, &data); err != nil {
111
+ return nil, err
112
+ }
113
+ if data.Version != 1 || data.ID == "" || data.Reads == nil || data.Files == nil || data.Directories == nil || data.Entries == nil || data.Realpaths == nil || data.Stats == nil {
114
+ return nil, fmt.Errorf("invalid compilation snapshot %q", path)
115
+ }
116
+ return &compilationSnapshotFS{FS: inner, path: path, data: data}, nil
117
+ }
118
+
119
+ func (s *compilationSnapshotFS) key(path string) string {
120
+ return shimtspath.Canonicalize(vfs.CaseSensitivityOf(s.FS), shimtspath.NormalizePath(path))
121
+ }
122
+
123
+ func (s *compilationSnapshotFS) realpath(path shimtspath.RootedPath) shimtspath.RootedPath {
124
+ key := s.key(string(path))
125
+ if value, ok := s.data.Realpaths[key]; ok {
126
+ return shimtspath.RootedPath(value)
127
+ }
128
+ value := s.FS.Realpath(path)
129
+ s.data.Realpaths[key] = string(value)
130
+ s.dirty = true
131
+ return value
132
+ }
133
+
134
+ func (s *compilationSnapshotFS) Realpath(path shimtspath.RootedPath) shimtspath.RootedPath {
135
+ s.mu.Lock()
136
+ defer s.mu.Unlock()
137
+ return s.realpath(path)
138
+ }
139
+
140
+ func (s *compilationSnapshotFS) ReadFile(path shimtspath.RootedFilePath) (string, bool) {
141
+ s.mu.Lock()
142
+ defer s.mu.Unlock()
143
+ key := s.key(string(path))
144
+ if value, ok := s.data.Reads[key]; ok {
145
+ return value.Text, value.OK
146
+ }
147
+ physical := s.realpath(shimtspath.RootedPath(path))
148
+ physicalKey := s.key(string(physical))
149
+ if value, ok := s.data.Reads[physicalKey]; ok {
150
+ s.data.Reads[key] = value
151
+ s.dirty = true
152
+ return value.Text, value.OK
153
+ }
154
+ text, ok := s.FS.ReadFile(path)
155
+ value := compilationSnapshotRead{Text: text, OK: ok}
156
+ s.data.Reads[key] = value
157
+ s.dirty = true
158
+ if ok && filepath.IsAbs(string(physical)) {
159
+ s.data.Reads[physicalKey] = value
160
+ s.dirty = true
161
+ }
162
+ return text, ok
163
+ }
164
+
165
+ func (s *compilationSnapshotFS) FileExists(path shimtspath.RootedFilePath) bool {
166
+ s.mu.Lock()
167
+ defer s.mu.Unlock()
168
+ key := s.key(string(path))
169
+ if value, ok := s.data.Files[key]; ok {
170
+ return value
171
+ }
172
+ value := s.FS.FileExists(path)
173
+ s.data.Files[key] = value
174
+ s.dirty = true
175
+ if value {
176
+ s.realpath(shimtspath.RootedPath(path))
177
+ }
178
+ return value
179
+ }
180
+
181
+ func (s *compilationSnapshotFS) DirectoryExists(path shimtspath.RootedDirectoryPath) bool {
182
+ s.mu.Lock()
183
+ defer s.mu.Unlock()
184
+ key := s.key(string(path))
185
+ if value, ok := s.data.Directories[key]; ok {
186
+ return value
187
+ }
188
+ value := s.FS.DirectoryExists(path)
189
+ s.data.Directories[key] = value
190
+ s.dirty = true
191
+ if value {
192
+ s.realpath(shimtspath.RootedPath(path))
193
+ }
194
+ return value
195
+ }
196
+
197
+ func (s *compilationSnapshotFS) Stat(path shimtspath.RootedPath) vfs.FileInfo {
198
+ s.mu.Lock()
199
+ defer s.mu.Unlock()
200
+ key := s.key(string(path))
201
+ value, found := s.data.Stats[key]
202
+ if !found {
203
+ info := s.FS.Stat(path)
204
+ if info != nil {
205
+ value = &compilationSnapshotInfo{Filename: info.Name(), Length: info.Size(), Bits: info.Mode(), Modified: info.ModTime()}
206
+ }
207
+ s.data.Stats[key] = value
208
+ s.dirty = true
209
+ if info != nil {
210
+ s.realpath(path)
211
+ }
212
+ }
213
+ if value == nil {
214
+ return nil
215
+ }
216
+ return value
217
+ }
218
+
219
+ func (s *compilationSnapshotFS) GetAccessibleEntries(path shimtspath.RootedDirectoryPath) vfs.Entries {
220
+ s.mu.Lock()
221
+ defer s.mu.Unlock()
222
+ key := s.key(string(path))
223
+ value, ok := s.data.Entries[key]
224
+ if !ok {
225
+ value = s.FS.GetAccessibleEntries(path)
226
+ value.Files = append([]string{}, value.Files...)
227
+ value.Directories = append([]string{}, value.Directories...)
228
+ s.data.Entries[key] = value
229
+ s.dirty = true
230
+ }
231
+ // The compiler owns its returned slices and may sort them in place.
232
+ value.Files = append([]string{}, value.Files...)
233
+ value.Directories = append([]string{}, value.Directories...)
234
+ if value.Symlinks != nil {
235
+ copied := make(map[string]struct{}, len(value.Symlinks))
236
+ for key := range value.Symlinks {
237
+ copied[key] = struct{}{}
238
+ }
239
+ value.Symlinks = copied
240
+ }
241
+ return value
242
+ }
243
+
244
+ // save transfers actual read answers to the next sequential native stage. A
245
+ // complete temporary file is renamed over the private document; no reader is
246
+ // allowed to start until this stage returns. Failed publication leaves no new
247
+ // snapshot authority and the stage reports the native failure.
248
+ func (s *compilationSnapshotFS) save() error {
249
+ s.mu.Lock()
250
+ defer s.mu.Unlock()
251
+ if !s.dirty {
252
+ return nil
253
+ }
254
+ bytes, err := json.Marshal(s.data)
255
+ if err != nil {
256
+ return err
257
+ }
258
+ staging := s.path + ".next"
259
+ if err := os.WriteFile(staging, bytes, 0600); err != nil {
260
+ return err
261
+ }
262
+ if err := os.Rename(staging, s.path); err != nil {
263
+ return err
264
+ }
265
+ s.dirty = false
266
+ return nil
267
+ }
@@ -0,0 +1,130 @@
1
+ package driver
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "reflect"
7
+ "sync"
8
+ "testing"
9
+
10
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
11
+ )
12
+
13
+ // TestCompilationSnapshotKeepsInputsAcrossStages verifies captured text,
14
+ // absence, identity and directory answers survive edits and stage handoff.
15
+ // A disk comparison cannot establish which bytes an earlier compiler used.
16
+ //
17
+ // 1. Read authored existing, empty and missing files and directory membership.
18
+ // 2. Edit and create those paths, then read concurrently and from a new stage.
19
+ // 3. Open a new invocation and require it to observe the edited inputs.
20
+ //
21
+ // @evidence contracts/testing.md#behavioral-verification The actual snapshot VFS serves old text and absence after disk mutation, persists those answers to another stage and admits new bytes only in a new invocation.
22
+ // @evidence contracts/testing.md#independent-expectations Literal BEFORE, AFTER, empty and missing states, authored Unicode/invalid byte text and the original directory listing define the oracle independently of snapshot serialization.
23
+ // @evidence contracts/testing.md#distinguishing-cases Existing, empty, Unicode plus invalid UTF-8 text, failed reads, missing existence, returned slice mutation, concurrent readers, sequential stage handoff and a new invocation exercise distinct read-once boundaries; malformed transport must fail.
24
+ // @evidence contracts/testing.md#execution-ownership This in-process driver Go unit calls its maintained VFS directly over a private temporary tree; it starts no compiler, installation or product host.
25
+ func TestCompilationSnapshotKeepsInputsAcrossStages(t *testing.T) {
26
+ root := t.TempDir()
27
+ source := filepath.Join(root, "source.ts")
28
+ empty := filepath.Join(root, "empty.ts")
29
+ encoded := filepath.Join(root, "encoded.ts")
30
+ encodedText := "// \xff\x80\nexport const value = \"한글😀\";\n"
31
+ missing := filepath.Join(root, "missing.ts")
32
+ write := func(file, text string) {
33
+ t.Helper()
34
+ if err := os.WriteFile(file, []byte(text), 0600); err != nil {
35
+ t.Fatal(err)
36
+ }
37
+ }
38
+ write(source, "BEFORE")
39
+ write(empty, "")
40
+ write(encoded, encodedText)
41
+ newDocument := func(id string) string {
42
+ t.Helper()
43
+ file := filepath.Join(t.TempDir(), "snapshot.json")
44
+ write(file, `{"version":1,"id":"`+id+`","reads":{},"files":{},"directories":{},"entries":{},"realpaths":{},"stats":{}}`)
45
+ return file
46
+ }
47
+ document := newDocument("first")
48
+ first, err := openCompilationSnapshot(DefaultFS(), document)
49
+ if err != nil {
50
+ t.Fatal(err)
51
+ }
52
+ file := shimtspath.RootedFilePathFromAbsolute(source)
53
+ absent := shimtspath.RootedFilePathFromAbsolute(missing)
54
+ directory := shimtspath.RootedDirectoryPathFromAbsolute(root)
55
+ if text, ok := first.ReadFile(file); !ok || text != "BEFORE" {
56
+ t.Fatalf("first read: %q %v", text, ok)
57
+ }
58
+ if text, ok := first.ReadFile(shimtspath.RootedFilePathFromAbsolute(empty)); !ok || text != "" {
59
+ t.Fatalf("empty read: %q %v", text, ok)
60
+ }
61
+ if _, ok := first.ReadFile(absent); ok || first.FileExists(absent) {
62
+ t.Fatal("missing input was present")
63
+ }
64
+ if !first.DirectoryExists(directory) {
65
+ t.Fatal("source directory missing")
66
+ }
67
+ if text, ok := first.ReadFile(shimtspath.RootedFilePathFromAbsolute(encoded)); !ok || text != encodedText {
68
+ t.Fatalf("encoded first read: %q %v", text, ok)
69
+ }
70
+ physical := first.Realpath(shimtspath.RootedPath(shimtspath.RootedFilePathFromAbsolute(source)))
71
+ original := first.GetAccessibleEntries(directory)
72
+ expected := append([]string{}, original.Files...)
73
+ original.Files[0] = "caller mutation"
74
+ write(source, "AFTER")
75
+ write(empty, "FILLED")
76
+ write(encoded, "AFTER")
77
+ write(missing, "CREATED")
78
+ var readers sync.WaitGroup
79
+ for range 8 {
80
+ readers.Go(func() {
81
+ if text, ok := first.ReadFile(file); !ok || text != "BEFORE" {
82
+ t.Errorf("concurrent read: %q %v", text, ok)
83
+ }
84
+ })
85
+ }
86
+ readers.Wait()
87
+ if err := first.save(); err != nil {
88
+ t.Fatal(err)
89
+ }
90
+ second, err := openCompilationSnapshot(DefaultFS(), document)
91
+ if err != nil {
92
+ t.Fatal(err)
93
+ }
94
+ if text, ok := second.ReadFile(file); !ok || text != "BEFORE" {
95
+ t.Fatalf("next stage: %q %v", text, ok)
96
+ }
97
+ if text, ok := second.ReadFile(shimtspath.RootedFilePathFromAbsolute(empty)); !ok || text != "" {
98
+ t.Fatalf("empty next stage: %q %v", text, ok)
99
+ }
100
+ if text, ok := second.ReadFile(shimtspath.RootedFilePathFromAbsolute(encoded)); !ok || text != encodedText {
101
+ t.Fatalf("encoded next stage: %q %v", text, ok)
102
+ }
103
+ if _, ok := second.ReadFile(absent); ok || second.FileExists(absent) {
104
+ t.Fatal("next stage admitted a created input")
105
+ }
106
+ if !reflect.DeepEqual(second.GetAccessibleEntries(directory).Files, expected) {
107
+ t.Fatal("directory answer changed")
108
+ }
109
+ if second.Realpath(shimtspath.RootedPath(shimtspath.RootedFilePathFromAbsolute(source))) != physical {
110
+ t.Fatal("physical source coordinate changed")
111
+ }
112
+ fresh, err := openCompilationSnapshot(DefaultFS(), newDocument("next"))
113
+ if err != nil {
114
+ t.Fatal(err)
115
+ }
116
+ if text, ok := fresh.ReadFile(file); !ok || text != "AFTER" {
117
+ t.Fatalf("new invocation: %q %v", text, ok)
118
+ }
119
+ if !fresh.FileExists(absent) {
120
+ t.Fatal("new invocation missed created source")
121
+ }
122
+ write(document, `{"version":2}`)
123
+ if _, err := openCompilationSnapshot(DefaultFS(), document); err == nil {
124
+ t.Fatal("invalid snapshot accepted")
125
+ }
126
+ write(document, "{")
127
+ if _, err := openCompilationSnapshot(DefaultFS(), document); err == nil {
128
+ t.Fatal("malformed snapshot accepted")
129
+ }
130
+ }
@@ -0,0 +1,100 @@
1
+ package driver
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strings"
7
+ "testing"
8
+
9
+ shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
10
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
11
+ )
12
+
13
+ // TestCompilationSnapshotPreservesCheckedEmission verifies consecutive compiler
14
+ // stages check and emit captured text after the physical source is edited.
15
+ // The next invocation must still reject the new ill-typed source.
16
+ //
17
+ // 1. Load and check an authored string assignment under a private snapshot.
18
+ // 2. Introduce an incompatible number, then load and emit the captured source.
19
+ // 3. Start a fresh snapshot and require the actual type error to be reported.
20
+ //
21
+ // @evidence contracts/testing.md#behavioral-verification Real driver loading, diagnostics, graph capture and emission share captured BEFORE text across two native Programs while a new invocation reports TS2322 for the changed disk source.
22
+ // @evidence contracts/testing.md#independent-expectations Authored string and number assignments define TypeScript assignability; literal BEFORE emission and diagnostic code 2322 do not come from implementation snapshots.
23
+ // @evidence contracts/testing.md#distinguishing-cases A same-invocation stage must preserve old successful emission while a new invocation must observe the adjacent type error; source graph identity and output provenance must remain populated.
24
+ // @evidence contracts/testing.md#execution-ownership This Go driver unit exercises linked compiler APIs in-process over private files, without a consumer installation, native build or spawned product host.
25
+ func TestCompilationSnapshotPreservesCheckedEmission(t *testing.T) {
26
+ root := t.TempDir()
27
+ source := filepath.Join(root, "source.ts")
28
+ config := filepath.Join(root, "tsconfig.json")
29
+ document := filepath.Join(t.TempDir(), "inputs.json")
30
+ write := func(file, text string) {
31
+ t.Helper()
32
+ if err := os.WriteFile(file, []byte(text), 0600); err != nil {
33
+ t.Fatal(err)
34
+ }
35
+ }
36
+ fresh := func(id string) {
37
+ write(document, `{"version":1,"id":"`+id+`","reads":{},"files":{},"directories":{},"entries":{},"realpaths":{},"stats":{}}`)
38
+ }
39
+ write(config, `{"compilerOptions":{"target":"es2020","module":"commonjs","types":[],"strict":true},"files":["source.ts"]}`)
40
+ write(source, `export const value: string = "BEFORE";`)
41
+ fresh("first")
42
+ t.Setenv("TTSC_COMPILATION_SNAPSHOT", document)
43
+ load := func() *Program {
44
+ t.Helper()
45
+ program, diagnostics, err := LoadProgram(root, config, LoadProgramOptions{})
46
+ if err != nil || program == nil || len(diagnostics) != 0 {
47
+ t.Fatalf("load: %v %v", err, diagnostics)
48
+ }
49
+ return program
50
+ }
51
+ first := load()
52
+ if diagnostics := first.Diagnostics(); len(diagnostics) != 0 {
53
+ t.Fatalf("first check: %v", diagnostics)
54
+ }
55
+ if err := first.Close(); err != nil {
56
+ t.Fatal(err)
57
+ }
58
+ write(source, `export const value: string = 123;`)
59
+ second := load()
60
+ defer second.Close()
61
+ if diagnostics := second.Diagnostics(); len(diagnostics) != 0 {
62
+ t.Fatalf("captured check: %v", diagnostics)
63
+ }
64
+ graph := NewTransformGraph(second, root)
65
+ if graph.CompilationSnapshot != "first" || len(graph.InputProofFailures) != 0 {
66
+ t.Fatalf("graph lost captured authority: %+v", graph)
67
+ }
68
+ var emitted strings.Builder
69
+ writer, provenance, err := second.NewEmitProvenanceRecorder(func(_ shimtspath.RootedFilePath, text string, _ *shimcompiler.WriteFileData) error {
70
+ emitted.WriteString(text)
71
+ return nil
72
+ })
73
+ if err != nil {
74
+ t.Fatal(err)
75
+ }
76
+ if _, diagnostics, err := second.EmitAll(NewRewriteSet(), writer); err != nil || len(diagnostics) != 0 {
77
+ t.Fatalf("emit: %v %v", err, diagnostics)
78
+ }
79
+ if !strings.Contains(emitted.String(), "BEFORE") || strings.Contains(emitted.String(), "123") {
80
+ t.Fatalf("emitted another input: %s", emitted.String())
81
+ }
82
+ if len(provenance()) == 0 {
83
+ t.Fatal("captured emission lost source ownership")
84
+ }
85
+ if err := second.Close(); err != nil {
86
+ t.Fatal(err)
87
+ }
88
+ fresh("next")
89
+ next := load()
90
+ defer next.Close()
91
+ found := false
92
+ for _, diagnostic := range next.Diagnostics() {
93
+ if diagnostic.Code == 2322 {
94
+ found = true
95
+ }
96
+ }
97
+ if !found {
98
+ t.Fatal("new invocation did not report the edited source's TS2322")
99
+ }
100
+ }
package/driver/graph.go CHANGED
@@ -70,6 +70,9 @@ const bundledScheme = "bundled:///"
70
70
  // @evidenceExclude contracts/performance.md#reuse-equivalent-work This schema carries proof without coordinating artifact reuse.
71
71
  // @evidenceExclude contracts/performance.md#bound-retention-and-release-resources The caller-owned value owns no resident cache or resource.
72
72
  type TransformGraph struct {
73
+ // CompilationSnapshot identifies the shared read-once view of every native stage.
74
+ // It authorizes fresh delivery, not reuse after the disk changes.
75
+ CompilationSnapshot string `json:"compilationSnapshot,omitempty"`
73
76
  // Membership is the effective native root-admission and output-exclusion policy.
74
77
  Membership *TransformMembership `json:"membership,omitempty"`
75
78
  // ContentMapperInputs are declared project dependencies, not observed reads.
@@ -93,14 +96,14 @@ type TransformGraph struct {
93
96
  // `cmd/ttsc api-transform` and the linked-plugin utility host both do.
94
97
  // Returns nil only for a nil or unloaded program.
95
98
  //
96
- // @evidence contracts/common.md#principled-implementation Compiler references, global scope, config ancestry, and replayed predicates form one generation's dependency proof.
99
+ // @evidence contracts/common.md#principled-implementation Compiler references, global scope, config ancestry and replayed predicates form one generation's dependency proof; a shared snapshot identity is attached only after its captured answers are published.
97
100
  // @evidence contracts/common.md#clear-and-simple-design Reference collection, resolution replay, and proof attachment share one output-key owner.
98
101
  // @evidence contracts/common.md#prohibited-implementation-shortcuts Unobserved or contradictory inputs remain failures rather than later filesystem snapshots substituted for compiler reads.
99
102
  // @evidence contracts/common.md#meaningful-documentation Native prose specifies envelope integration and unloaded-program behavior following the documentation skill.
100
103
  // @evidence contracts/portability.md#os-neutral-implementation Keys use native filepath conversion and compiler case policy; identity metadata comes from reported adapter realpaths, whose native VFS error fallback can retain lexical spelling rather than certifying physical resolution.
101
- // @evidence contracts/performance.md#efficient-algorithms Direct adjacency avoids computing every transitive closure. Construction scans resident sources/references, converts path text and sorts per-source targets, globals and resolver inputs; a distinct-input set selects proof attachment, whose rich and legacy projections can perform two observer lookups. Resolver task sorting, native replay reads/metadata/enumeration and observation merging remain part of this call's delegated cost; no work or byte ceiling is imposed here.
104
+ // @evidence contracts/performance.md#efficient-algorithms Direct adjacency avoids computing every transitive closure. Construction scans resident sources/references, converts path text and sorts per-source targets, globals and resolver inputs; a distinct-input set selects proof attachment, whose rich and legacy projections can perform two observer lookups. Resolver task sorting, native replay reads/metadata/enumeration and observation merging remain part of this call's delegated cost; dirty snapshot publication additionally encodes and writes the complete captured text, paths and predicate population. No work or byte ceiling is imposed here.
102
105
  // @evidence contracts/performance.md#reuse-equivalent-work Final proof projection reuses the current Program observer, including its exact physical-target read for a lexical alias; it does not replace missing compiler evidence with a new read. Resolver replay separately performs native queries and merges contradictions into that generation. No prior graph or later filesystem generation is reused by this constructor.
103
- // @evidence contracts/performance.md#bound-retention-and-release-resources Temporary adjacency/input/proof maps grow with resident references, distinct paths and reported predicate bytes. Graph arrays/maps transfer to the caller, while replay observations merge into the caller-owned current Program observer. This call owns no resident historical graph cache or native handle, and supplies no population/byte ceiling or caller-lifetime disposal policy.
106
+ // @evidence contracts/performance.md#bound-retention-and-release-resources Temporary adjacency/input/proof maps grow with resident references, distinct paths and reported predicate bytes. Graph arrays/maps transfer to the caller, while replay observations merge into the caller-owned current Program observer. Snapshot encoding adds a temporary complete document buffer, and a failed rename can leave invocation-owned staging for coordinator cleanup. This call owns no resident historical graph cache or persistent native handle, and supplies no population/byte ceiling or caller-lifetime disposal policy.
104
107
  func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
105
108
  if prog == nil || prog.TSProgram == nil {
106
109
  return nil
@@ -154,6 +157,9 @@ func NewTransformGraph(prog *Program, cwd string) *TransformGraph {
154
157
  graph.InputProofFailures = resolution.Failures
155
158
  }
156
159
  graph.attachInputProof(prog, cwd)
160
+ if snapshot := prog.compilationSnapshot; snapshot != nil && snapshot.save() == nil {
161
+ graph.CompilationSnapshot = snapshot.data.ID
162
+ }
157
163
  return graph
158
164
  }
159
165
 
package/driver/program.go CHANGED
@@ -2,6 +2,7 @@ package driver
2
2
 
3
3
  import (
4
4
  "context"
5
+ "errors"
5
6
  "fmt"
6
7
  "io"
7
8
  "os"
@@ -329,8 +330,9 @@ type Program struct {
329
330
  compilerArgumentInputs []string
330
331
 
331
332
  // FS is the input-observing filesystem, including any source preamble layer.
332
- FS vfs.FS
333
- inputObserver *inputObservationFS
333
+ FS vfs.FS
334
+ compilationSnapshot *compilationSnapshotFS
335
+ inputObserver *inputObservationFS
334
336
 
335
337
  // SourcePreamble is the injected text used when correcting authored positions.
336
338
  SourcePreamble string
@@ -407,16 +409,16 @@ type LoadProgramOptions struct {
407
409
 
408
410
  // Close releases the checker pool lease acquired by LoadProgram and, on the
409
411
  // TypeScript 7.1 target, stops the project's content-mapper processes. Its
410
- // error is the content mappers' close error.
412
+ // error joins content-mapper shutdown and compilation-snapshot publication errors.
411
413
  //
412
- // @evidence contracts/common.md#principled-implementation The recorded release callback returns the checker lease once and is cleared after release; the project hosts then close the mapper project and processes the Program's hosts shared.
414
+ // @evidence contracts/common.md#principled-implementation The checker lease and project hosts are detached once; dirty snapshot answers are published through their detached invocation owner, and mapper and publication errors are joined without skipping mapper shutdown.
413
415
  // @evidence contracts/common.md#clear-and-simple-design Checker and content-mapper ownership share one cleanup operation instead of independent releases by each consumer.
414
- // @evidence contracts/common.md#prohibited-implementation-shortcuts Cleanup invokes the owned callbacks rather than patching the checker pool, and returns the mapper close error instead of fabricating success.
416
+ // @evidence contracts/common.md#prohibited-implementation-shortcuts Cleanup invokes the owned callbacks rather than patching the checker pool, and returns joined mapper/publication errors instead of fabricating success.
415
417
  // @evidence contracts/common.md#meaningful-documentation Native prose names the lease and the mapper processes released and the error returned, following the documentation skill.
416
- // @evidence contracts/portability.md#os-neutral-implementation Checker release is in-process; stopping a mapper process goes through the shim's os/exec boundary.
417
- // @evidenceExclude contracts/performance.md#efficient-algorithms Releasing two recorded owners involves no input-processing algorithm.
418
- // @evidenceExclude contracts/performance.md#reuse-equivalent-work Cleanup does not determine whether compiler outputs may be reused.
419
- // @evidence contracts/performance.md#bound-retention-and-release-resources Program owns one checker lease and its project hosts; Close clears both, so repeated Close releases neither twice, and leaves facade memory lifetime to the caller.
418
+ // @evidence contracts/portability.md#os-neutral-implementation Checker release is in-process; mapper shutdown uses the shim's os/exec boundary, while dirty snapshot publication uses native file writes and same-directory rename.
419
+ // @evidence contracts/performance.md#efficient-algorithms Release bookkeeping is fixed work; a dirty snapshot additionally serializes and writes its complete captured text, paths and predicates, with temporary encoded bytes proportional to that population.
420
+ // @evidence contracts/performance.md#reuse-equivalent-work Detaching publication ownership prevents repeated Close from overwriting a later stage; a clean snapshot avoids repeated serialization. This does not establish output-cache freshness.
421
+ // @evidence contracts/performance.md#bound-retention-and-release-resources Program owns the checker lease, project hosts and one snapshot publication owner; Close detaches them once. The FS stack and caller aliases can still retain captured text and predicate maps until released; invocation storage cleanup belongs to the coordinator, and publication failure can leave staging for that cleanup.
420
422
  func (p *Program) Close() error {
421
423
  if p.checkerRelease != nil {
422
424
  p.checkerRelease()
@@ -424,7 +426,12 @@ func (p *Program) Close() error {
424
426
  }
425
427
  hosts := p.hosts
426
428
  p.hosts = nil
427
- return hosts.Close()
429
+ var snapshotError error
430
+ if snapshot := p.compilationSnapshot; snapshot != nil {
431
+ p.compilationSnapshot = nil
432
+ snapshotError = snapshot.save()
433
+ }
434
+ return errors.Join(hosts.Close(), snapshotError)
428
435
  }
429
436
 
430
437
  // ParseTSConfig parses a tsconfig.json file via tsgo's native JSONC parser.
@@ -590,15 +597,20 @@ func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
590
597
  // Relative cwd is resolved against the process cwd when filepath.Abs succeeds;
591
598
  // callers should provide an absolute anchor. tsconfigPath may be relative to it.
592
599
  //
600
+ // An enabled private compilation-snapshot document shares first-read text and
601
+ // lookup answers across sequential native stages. This is not an atomic volume
602
+ // snapshot. Program owns dirty publication through Close; the invocation
603
+ // coordinator owns removal of the private transport storage.
604
+ //
593
605
  // @evidence contracts/common.md#principled-implementation Plugin preambles enter before parsing, native CLI/config merging resolves options before Program creation, and the facade associates the observing VFS and borrowed checker with that generation; this is not a certificate of every filesystem access by plugins or their host.
594
606
  // @evidence contracts/common.md#clear-and-simple-design One loader orders plugin config, filesystem layers, config overrides and checker acquisition; an optional failure sink transfers only the rejected load's observed recovery inputs.
595
607
  // @evidence contracts/common.md#prohibited-implementation-shortcuts Invalid configuration or CLI input returns before construction; an explicitly empty argument list does not fall through to an ancestor's flags.
596
608
  // @evidence contracts/common.md#meaningful-documentation Native prose and options document one-shot load, path anchoring, force overrides and checker ownership under documentation-skill guidance.
597
609
  // @evidence contracts/portability.md#os-neutral-implementation Native cwd resolution uses filepath/tspath and the chosen VFS provides actual case and path capabilities; semantic wrapper ownership uses an explicit config path.
598
- // @evidence contracts/performance.md#efficient-algorithms One load composes plugin/preamble work, config/CLI parsing, native Program processing and checker acquisition with VFS observation. A requested failed-load graph additionally snapshots, projects and sorts the observed predicate population and invokes the caller's sink. Read-text hashing runs on each observed read, including repeated reads; config/source bytes, native resolution and directory work, plugin callbacks and retained observation maps govern cost rather than only facade construction.
599
- // @evidenceExclude contracts/performance.md#reuse-equivalent-work This one-shot factory does not coordinate callers; Session and resident hosts own cross-request compiler reuse.
600
- // @evidence contracts/performance.md#bound-retention-and-release-resources Errors before checker acquisition return no lease; an optional synchronous sink receives call-owned recovery metadata once after observation starts. Successful loading transfers checker and mapper ownership plus input/plugin ledgers to Program; Close releases owned native resources, while caller reachability controls remaining facade state.
601
- func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program, []Diagnostic, error) {
610
+ // @evidence contracts/performance.md#efficient-algorithms One load composes plugin/preamble work, config/CLI parsing, native Program processing and checker acquisition with VFS observation. A requested failed-load graph additionally snapshots, projects and sorts the observed predicate population and invokes the caller's sink. An enabled snapshot also parses the preceding full document and publishes its dirty complete captured population. Read-text hashing runs on each observed read, including repeated reads; config/source bytes, native resolution and directory work, plugin callbacks and retained observation maps govern cost rather than only facade construction.
611
+ // @evidence contracts/performance.md#reuse-equivalent-work An enabled private document shares first-read answers only across sequential stages of its invocation; fresh invocations start empty. Session and resident hosts retain ownership of cross-request Program reuse.
612
+ // @evidence contracts/performance.md#bound-retention-and-release-resources Errors before checker acquisition return no lease; an optional synchronous sink receives call-owned recovery metadata once after observation starts. Successful loading transfers checker and mapper ownership plus input/plugin ledgers to Program; Close releases owned native resources and detaches snapshot publication; FS/caller reachability controls captured-memory lifetime, and the invocation coordinator removes transport storage.
613
+ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (program *Program, diagnostics []Diagnostic, loadError error) {
602
614
  if !filepath.IsAbs(cwd) {
603
615
  if abs, err := filepath.Abs(cwd); err == nil {
604
616
  cwd = abs
@@ -624,6 +636,24 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
624
636
  if fs == nil {
625
637
  fs = DefaultFS()
626
638
  }
639
+ var snapshot *compilationSnapshotFS
640
+ if filename := os.Getenv("TTSC_COMPILATION_SNAPSHOT"); filename != "" {
641
+ var err error
642
+ snapshot, err = openCompilationSnapshot(fs, filename)
643
+ if err != nil {
644
+ return nil, nil, fmt.Errorf("compilation snapshot: %w", err)
645
+ }
646
+ fs = snapshot
647
+ defer func() {
648
+ if err := snapshot.save(); err != nil {
649
+ if program != nil {
650
+ program.Close()
651
+ program = nil
652
+ }
653
+ loadError = errors.Join(loadError, fmt.Errorf("compilation snapshot publication: %w", err))
654
+ }
655
+ }()
656
+ }
627
657
  inputObserver := newInputObservationFS(fs)
628
658
  loaded := false
629
659
  if options.OnFailureInputs != nil {
@@ -716,6 +746,7 @@ func LoadProgram(cwd, tsconfigPath string, options LoadProgramOptions) (*Program
716
746
  mapperDependencyError: mapperDependencyError,
717
747
  compilerArgumentInputs: compilerArgumentInputs,
718
748
  FS: fs,
749
+ compilationSnapshot: snapshot,
719
750
  inputObserver: inputObserver,
720
751
  SourcePreamble: options.SourcePreamble,
721
752
  }