ttsc 0.28.4 → 0.28.6

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 (38) hide show
  1. package/cmd/ttsc/api_compile.go +6 -4
  2. package/cmd/ttsc/api_transform.go +6 -4
  3. package/cmd/ttsc/build.go +7 -6
  4. package/cmd/ttscgraph/serve.go +70 -77
  5. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +26 -0
  6. package/cmd/ttscgraph/serve_session_tracks_all_module_specifier_forms_test.go +1 -2
  7. package/driver/graph.go +67 -35
  8. package/driver/input_observation.go +305 -115
  9. package/driver/input_observation_test.go +203 -0
  10. package/driver/program.go +28 -4
  11. package/driver/resolution_candidates.go +247 -741
  12. package/driver/resolution_candidates_test.go +341 -0
  13. package/lib/compiler/internal/project/readProjectConfig.js +9 -7
  14. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  15. package/lib/compiler/internal/runBuild.js +1 -0
  16. package/lib/compiler/internal/runBuild.js.map +1 -1
  17. package/lib/compiler/internal/sharedHostHelpers.d.ts +4 -0
  18. package/lib/compiler/internal/sharedHostHelpers.js +11 -1
  19. package/lib/compiler/internal/sharedHostHelpers.js.map +1 -1
  20. package/lib/compiler/internal/startResidentTransform.js +1 -0
  21. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  22. package/lib/compiler/internal/transformProjectInMemory.js +165 -8
  23. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  24. package/lib/internal/projectInputPathIdentity.js +8 -7
  25. package/lib/internal/projectInputPathIdentity.js.map +1 -1
  26. package/lib/structures/ITtscCompilerTransformation.d.ts +57 -19
  27. package/package.json +8 -8
  28. package/shim/compiler/incremental.go +52 -0
  29. package/shim/compiler/resolution.go +444 -0
  30. package/shim/core/shim.go +15 -1
  31. package/src/compiler/internal/project/readProjectConfig.ts +9 -7
  32. package/src/compiler/internal/runBuild.ts +2 -0
  33. package/src/compiler/internal/sharedHostHelpers.ts +14 -0
  34. package/src/compiler/internal/startResidentTransform.ts +2 -0
  35. package/src/compiler/internal/transformProjectInMemory.ts +193 -8
  36. package/src/internal/projectInputPathIdentity.ts +8 -7
  37. package/src/structures/ITtscCompilerTransformation.ts +65 -19
  38. package/utility/host.go +8 -7
@@ -4,6 +4,7 @@ import (
4
4
  "crypto/sha256"
5
5
  "encoding/hex"
6
6
  "path/filepath"
7
+ "slices"
7
8
  "sync"
8
9
 
9
10
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
@@ -15,32 +16,62 @@ var observedDirectoryDigest = func() string {
15
16
  return hex.EncodeToString(digest[:])
16
17
  }()
17
18
 
18
- type observedInputKind uint8
19
-
20
19
  type inputProofFailure string
21
20
 
22
21
  const (
23
- inputProofContentChanged inputProofFailure = "content-changed"
24
- inputProofContentUnavailable inputProofFailure = "content-unavailable"
25
- inputProofInvalidPath inputProofFailure = "invalid-path"
26
- inputProofKindChanged inputProofFailure = "kind-changed"
27
- inputProofRealpathChanged inputProofFailure = "realpath-changed"
28
- inputProofRealpathUnavailable inputProofFailure = "realpath-unavailable"
29
- inputProofUnobserved inputProofFailure = "unobserved"
30
- inputProofUnsupportedInputKind inputProofFailure = "unsupported-input-kind"
22
+ inputProofAccessibleEntriesChanged inputProofFailure = "accessible-entries-changed"
23
+ inputProofContentChanged inputProofFailure = "content-changed"
24
+ inputProofContentUnavailable inputProofFailure = "content-unavailable"
25
+ inputProofDirectoryExistsChanged inputProofFailure = "directory-exists-changed"
26
+ inputProofFileExistsChanged inputProofFailure = "file-exists-changed"
27
+ inputProofInvalidPath inputProofFailure = "invalid-path"
28
+ inputProofPredicateConflict inputProofFailure = "predicate-conflict"
29
+ inputProofResolutionChanged inputProofFailure = "resolution-changed"
30
+ inputProofRealpathChanged inputProofFailure = "realpath-changed"
31
+ inputProofRealpathUnavailable inputProofFailure = "realpath-unavailable"
32
+ inputProofStatChanged inputProofFailure = "stat-changed"
33
+ inputProofUnobserved inputProofFailure = "unobserved"
34
+ inputProofUnsupportedInputKind inputProofFailure = "unsupported-input-kind"
31
35
  )
32
36
 
33
- const (
34
- observedInputMissing observedInputKind = iota
35
- observedInputFile
36
- observedInputDirectory
37
- )
37
+ // TransformInputReadObservation is the exact result of one compiler ReadFile
38
+ // predicate. A failed read carries OK=false and no guessed filesystem kind.
39
+ type TransformInputReadObservation struct {
40
+ OK bool `json:"ok"`
41
+ Hash string `json:"hash,omitempty"`
42
+ }
43
+
44
+ // TransformInputRealpathObservation is the exact result of one compiler
45
+ // Realpath predicate or an identity read already performed beside a successful
46
+ // existence predicate.
47
+ type TransformInputRealpathObservation struct {
48
+ OK bool `json:"ok"`
49
+ Path string `json:"path,omitempty"`
50
+ }
51
+
52
+ // TransformInputEntriesObservation is the exact result of one compiler
53
+ // GetAccessibleEntries predicate. Both lists retain TypeScript-Go's sorted
54
+ // lexical child names, including followed directory links and junctions.
55
+ type TransformInputEntriesObservation struct {
56
+ Directories []string `json:"directories"`
57
+ Files []string `json:"files"`
58
+ }
59
+
60
+ // TransformInputObservation preserves independent compiler filesystem
61
+ // predicates for one lexical path. False FileExists and true DirectoryExists
62
+ // are compatible constraints, not a path-kind race.
63
+ type TransformInputObservation struct {
64
+ AccessibleEntries *TransformInputEntriesObservation `json:"accessibleEntries,omitempty"`
65
+ DirectoryExists *bool `json:"directoryExists,omitempty"`
66
+ FileExists *bool `json:"fileExists,omitempty"`
67
+ ReadFile *TransformInputReadObservation `json:"readFile,omitempty"`
68
+ Realpath *TransformInputRealpathObservation `json:"realpath,omitempty"`
69
+ Stat *string `json:"stat,omitempty"`
70
+ }
38
71
 
39
72
  type observedInput struct {
40
- contentHash *string
41
- failure inputProofFailure
42
- kind observedInputKind
43
- realpath *string
73
+ failure inputProofFailure
74
+ proof TransformInputObservation
44
75
  }
45
76
 
46
77
  // inputObservationFS records the exact disk state returned through the
@@ -49,32 +80,32 @@ type observedInput struct {
49
80
  // of attaching post-compile disk hashes to an earlier result.
50
81
  type inputObservationFS struct {
51
82
  vfs.FS
52
- caseSensitive bool
53
- mu sync.Mutex
54
- observations map[string]observedInput
83
+ caseSensitive bool
84
+ mu sync.Mutex
85
+ observations map[string]observedInput
86
+ observationOrder []string
87
+ observationSpellings map[string]string
55
88
  }
56
89
 
57
90
  func newInputObservationFS(inner vfs.FS) *inputObservationFS {
58
91
  return &inputObservationFS{
59
- FS: inner,
60
- caseSensitive: inner.UseCaseSensitiveFileNames(),
61
- observations: map[string]observedInput{},
92
+ FS: inner,
93
+ caseSensitive: inner.UseCaseSensitiveFileNames(),
94
+ observations: map[string]observedInput{},
95
+ observationSpellings: map[string]string{},
62
96
  }
63
97
  }
64
98
 
65
99
  func (fs *inputObservationFS) FileExists(path string) bool {
66
100
  exists := fs.FS.FileExists(path)
101
+ proof := TransformInputObservation{FileExists: boolPointer(exists)}
67
102
  if exists {
68
103
  // Existence participates in resolution, but only ReadFile returns bytes
69
104
  // that can influence the resident Program. Do not duplicate every
70
105
  // resolver probe with an eager file read.
71
- fs.observe(path, observedInput{
72
- kind: observedInputFile,
73
- realpath: fs.currentRealpath(path),
74
- })
75
- } else {
76
- fs.observe(path, observedInput{kind: observedInputMissing})
106
+ proof.Realpath = fs.currentRealpath(path)
77
107
  }
108
+ fs.observe(path, observedInput{proof: proof})
78
109
  return exists
79
110
  }
80
111
 
@@ -84,49 +115,66 @@ func (fs *inputObservationFS) ReadFile(path string) (string, bool) {
84
115
  digest := sha256.Sum256([]byte(contents))
85
116
  hash := hex.EncodeToString(digest[:])
86
117
  fs.observe(path, observedInput{
87
- contentHash: &hash,
88
- kind: observedInputFile,
89
- realpath: fs.currentRealpath(path),
118
+ proof: TransformInputObservation{
119
+ ReadFile: &TransformInputReadObservation{OK: true, Hash: hash},
120
+ Realpath: fs.currentRealpath(path),
121
+ },
90
122
  })
91
123
  } else {
92
- fs.observe(path, observedInput{kind: observedInputMissing})
124
+ fs.observe(path, observedInput{
125
+ proof: TransformInputObservation{
126
+ ReadFile: &TransformInputReadObservation{OK: false},
127
+ },
128
+ })
93
129
  }
94
130
  return contents, ok
95
131
  }
96
132
 
97
133
  func (fs *inputObservationFS) DirectoryExists(path string) bool {
98
134
  exists := fs.FS.DirectoryExists(path)
135
+ proof := TransformInputObservation{DirectoryExists: boolPointer(exists)}
99
136
  if exists {
100
- fs.observe(path, observedInput{
101
- kind: observedInputDirectory,
102
- realpath: fs.currentRealpath(path),
103
- })
104
- } else {
105
- fs.observe(path, observedInput{kind: observedInputMissing})
137
+ proof.Realpath = fs.currentRealpath(path)
106
138
  }
139
+ fs.observe(path, observedInput{proof: proof})
107
140
  return exists
108
141
  }
109
142
 
110
143
  func (fs *inputObservationFS) GetAccessibleEntries(path string) vfs.Entries {
111
- // DirectoryExists records directory identity on the ordinary compiler path.
112
- // An empty listing alone cannot distinguish a missing directory from an
113
- // existing empty one without issuing an extra stat for every enumeration.
114
- return fs.FS.GetAccessibleEntries(path)
144
+ entries := fs.FS.GetAccessibleEntries(path)
145
+ fs.observe(path, observedInput{
146
+ proof: TransformInputObservation{
147
+ AccessibleEntries: &TransformInputEntriesObservation{
148
+ Directories: append([]string{}, entries.Directories...),
149
+ Files: append([]string{}, entries.Files...),
150
+ },
151
+ },
152
+ })
153
+ return entries
115
154
  }
116
155
 
117
156
  func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
118
157
  info := fs.FS.Stat(path)
158
+ kind := "missing"
119
159
  if info == nil {
120
- fs.observe(path, observedInput{kind: observedInputMissing})
160
+ fs.observe(path, observedInput{
161
+ proof: TransformInputObservation{Stat: &kind},
162
+ })
121
163
  } else if info.IsDir() {
164
+ kind = "directory"
122
165
  fs.observe(path, observedInput{
123
- kind: observedInputDirectory,
124
- realpath: fs.currentRealpath(path),
166
+ proof: TransformInputObservation{
167
+ Stat: &kind,
168
+ Realpath: fs.currentRealpath(path),
169
+ },
125
170
  })
126
171
  } else {
172
+ kind = "file"
127
173
  fs.observe(path, observedInput{
128
- kind: observedInputFile,
129
- realpath: fs.currentRealpath(path),
174
+ proof: TransformInputObservation{
175
+ Stat: &kind,
176
+ Realpath: fs.currentRealpath(path),
177
+ },
130
178
  })
131
179
  }
132
180
  return info
@@ -134,20 +182,28 @@ func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
134
182
 
135
183
  func (fs *inputObservationFS) Realpath(path string) string {
136
184
  realpath := fs.FS.Realpath(path)
137
- if realpath != "" {
138
- resolved := filepath.Clean(realpath)
139
- fs.observeRealpath(path, &resolved)
140
- }
185
+ fs.observe(path, observedInput{
186
+ proof: TransformInputObservation{Realpath: realpathObservation(realpath)},
187
+ })
141
188
  return realpath
142
189
  }
143
190
 
144
- func (fs *inputObservationFS) currentRealpath(path string) *string {
145
- realpath := fs.FS.Realpath(path)
191
+ func (fs *inputObservationFS) currentRealpath(path string) *TransformInputRealpathObservation {
192
+ return realpathObservation(fs.FS.Realpath(path))
193
+ }
194
+
195
+ func realpathObservation(realpath string) *TransformInputRealpathObservation {
146
196
  if realpath == "" {
147
- return nil
197
+ return &TransformInputRealpathObservation{OK: false}
198
+ }
199
+ return &TransformInputRealpathObservation{
200
+ OK: true,
201
+ Path: filepath.Clean(realpath),
148
202
  }
149
- resolved := filepath.Clean(realpath)
150
- return &resolved
203
+ }
204
+
205
+ func boolPointer(value bool) *bool {
206
+ return &value
151
207
  }
152
208
 
153
209
  func (fs *inputObservationFS) observationKey(path string) string {
@@ -167,13 +223,17 @@ func (fs *inputObservationFS) observe(path string, next observedInput) {
167
223
  }
168
224
  fs.mu.Lock()
169
225
  defer fs.mu.Unlock()
226
+ if _, found := fs.observationSpellings[key]; !found {
227
+ fs.observationSpellings[key] = filepath.Clean(path)
228
+ fs.observationOrder = append(fs.observationOrder, key)
229
+ }
170
230
  keys := []string{key}
171
231
  // A compiler read can arrive through an 8.3, case-variant, or already-real
172
232
  // spelling while resolution recorded the selected lexical alias. Index the
173
233
  // returned bytes by the final physical path too, so proof can join the two
174
234
  // observations without another disk read.
175
- if next.contentHash != nil && next.realpath != nil {
176
- physicalKey := fs.observationKey(*next.realpath)
235
+ if next.proof.ReadFile != nil && next.proof.ReadFile.OK && next.proof.Realpath != nil && next.proof.Realpath.OK {
236
+ physicalKey := fs.observationKey(next.proof.Realpath.Path)
177
237
  if physicalKey != "" && physicalKey != key {
178
238
  keys = append(keys, physicalKey)
179
239
  }
@@ -197,108 +257,238 @@ func (fs *inputObservationFS) mergeObservation(key string, next observedInput) {
197
257
  fs.failObservation(key, previous, next.failure)
198
258
  return
199
259
  }
200
- if previous.kind != next.kind {
201
- fs.failObservation(key, previous, inputProofKindChanged)
260
+ if previous.proof.FileExists != nil && next.proof.FileExists != nil && *previous.proof.FileExists != *next.proof.FileExists {
261
+ fs.failObservation(key, previous, inputProofFileExistsChanged)
202
262
  return
203
263
  }
204
- if !sameOptionalString(previous.realpath, next.realpath) {
205
- fs.failObservation(key, previous, inputProofRealpathChanged)
264
+ if previous.proof.DirectoryExists != nil && next.proof.DirectoryExists != nil && *previous.proof.DirectoryExists != *next.proof.DirectoryExists {
265
+ fs.failObservation(key, previous, inputProofDirectoryExistsChanged)
206
266
  return
207
267
  }
208
- if previous.contentHash != nil && next.contentHash != nil && *previous.contentHash != *next.contentHash {
268
+ if previous.proof.Stat != nil && next.proof.Stat != nil && *previous.proof.Stat != *next.proof.Stat {
269
+ fs.failObservation(key, previous, inputProofStatChanged)
270
+ return
271
+ }
272
+ if previous.proof.ReadFile != nil && next.proof.ReadFile != nil && !sameReadObservation(previous.proof.ReadFile, next.proof.ReadFile) {
209
273
  fs.failObservation(key, previous, inputProofContentChanged)
210
274
  return
211
275
  }
212
- if previous.contentHash == nil && next.contentHash != nil {
213
- previous.contentHash = next.contentHash
276
+ if previous.proof.Realpath != nil && next.proof.Realpath != nil && !sameRealpathObservation(previous.proof.Realpath, next.proof.Realpath) {
277
+ fs.failObservation(key, previous, inputProofRealpathChanged)
278
+ return
279
+ }
280
+ if previous.proof.AccessibleEntries != nil && next.proof.AccessibleEntries != nil && !sameEntriesObservation(previous.proof.AccessibleEntries, next.proof.AccessibleEntries) {
281
+ fs.failObservation(key, previous, inputProofAccessibleEntriesChanged)
282
+ return
283
+ }
284
+ if previous.proof.AccessibleEntries == nil {
285
+ previous.proof.AccessibleEntries = next.proof.AccessibleEntries
286
+ }
287
+ if previous.proof.FileExists == nil {
288
+ previous.proof.FileExists = next.proof.FileExists
289
+ }
290
+ if previous.proof.DirectoryExists == nil {
291
+ previous.proof.DirectoryExists = next.proof.DirectoryExists
292
+ }
293
+ if previous.proof.Stat == nil {
294
+ previous.proof.Stat = next.proof.Stat
295
+ }
296
+ if previous.proof.ReadFile == nil {
297
+ previous.proof.ReadFile = next.proof.ReadFile
298
+ }
299
+ if previous.proof.Realpath == nil {
300
+ previous.proof.Realpath = next.proof.Realpath
301
+ }
302
+ if !transformInputObservationCompatible(previous.proof) {
303
+ fs.failObservation(key, previous, inputProofPredicateConflict)
304
+ return
214
305
  }
215
306
  fs.observations[key] = previous
216
307
  }
217
308
 
218
- func (fs *inputObservationFS) failObservation(key string, observed observedInput, failure inputProofFailure) {
219
- observed.contentHash = nil
220
- observed.failure = failure
221
- observed.realpath = nil
222
- fs.observations[key] = observed
309
+ func sameEntriesObservation(left, right *TransformInputEntriesObservation) bool {
310
+ return slices.Equal(left.Directories, right.Directories) && slices.Equal(left.Files, right.Files)
311
+ }
312
+
313
+ // observedPaths returns the exact lexical spellings on which this wrapper
314
+ // observed at least one filesystem predicate, in first-observation order.
315
+ func (fs *inputObservationFS) observedPaths() []string {
316
+ fs.mu.Lock()
317
+ defer fs.mu.Unlock()
318
+ output := make([]string, 0, len(fs.observationOrder))
319
+ for _, key := range fs.observationOrder {
320
+ output = append(output, fs.observationSpellings[key])
321
+ }
322
+ return output
223
323
  }
224
324
 
225
- func (fs *inputObservationFS) observeRealpath(path string, realpath *string) {
325
+ func (fs *inputObservationFS) observedAccessibleEntries(path string) bool {
226
326
  key := fs.observationKey(path)
227
327
  if key == "" {
228
- return
328
+ return false
229
329
  }
230
330
  fs.mu.Lock()
231
331
  defer fs.mu.Unlock()
232
- previous, found := fs.observations[key]
233
- if !found {
234
- // A realpath by itself cannot prove whether the path was a file or
235
- // directory. The ordinary resolver probes record that state separately.
332
+ observation, found := fs.observations[key]
333
+ return found && observation.proof.AccessibleEntries != nil
334
+ }
335
+
336
+ // mergeFrom joins a replay transaction into the compiler-time observation
337
+ // set. Any predicate that changed between construction and replay becomes a
338
+ // stable proof failure instead of authorizing output from mixed generations.
339
+ func (fs *inputObservationFS) mergeFrom(replay *inputObservationFS) {
340
+ if replay == nil {
236
341
  return
237
342
  }
238
- if previous.failure != "" {
239
- return
343
+ replay.mu.Lock()
344
+ observations := make(map[string]observedInput, len(replay.observations))
345
+ for key, observation := range replay.observations {
346
+ observations[key] = observation
240
347
  }
241
- if !sameOptionalString(previous.realpath, realpath) {
242
- fs.failObservation(key, previous, inputProofRealpathChanged)
348
+ order := append([]string{}, replay.observationOrder...)
349
+ spellings := make(map[string]string, len(replay.observationSpellings))
350
+ for key, spelling := range replay.observationSpellings {
351
+ spellings[key] = spelling
352
+ }
353
+ replay.mu.Unlock()
354
+
355
+ fs.mu.Lock()
356
+ defer fs.mu.Unlock()
357
+ for _, key := range order {
358
+ if _, found := fs.observationSpellings[key]; !found {
359
+ fs.observationSpellings[key] = spellings[key]
360
+ fs.observationOrder = append(fs.observationOrder, key)
361
+ }
362
+ }
363
+ for key, observation := range observations {
364
+ fs.mergeObservation(key, observation)
243
365
  }
244
366
  }
245
367
 
246
- func sameOptionalString(left, right *string) bool {
247
- if left == nil || right == nil {
248
- return left == nil && right == nil
368
+ func (fs *inputObservationFS) failObservation(key string, observed observedInput, failure inputProofFailure) {
369
+ observed.failure = failure
370
+ fs.observations[key] = observed
371
+ }
372
+
373
+ func sameReadObservation(left, right *TransformInputReadObservation) bool {
374
+ return left.OK == right.OK && left.Hash == right.Hash
375
+ }
376
+
377
+ func sameRealpathObservation(left, right *TransformInputRealpathObservation) bool {
378
+ return left.OK == right.OK && left.Path == right.Path
379
+ }
380
+
381
+ func transformInputObservationCompatible(observation TransformInputObservation) bool {
382
+ hasAccessibleEntries := observation.AccessibleEntries != nil &&
383
+ (len(observation.AccessibleEntries.Directories) != 0 || len(observation.AccessibleEntries.Files) != 0)
384
+ if hasAccessibleEntries &&
385
+ ((observation.FileExists != nil && *observation.FileExists) ||
386
+ (observation.DirectoryExists != nil && !*observation.DirectoryExists) ||
387
+ (observation.Stat != nil && *observation.Stat != "directory") ||
388
+ (observation.ReadFile != nil && observation.ReadFile.OK)) {
389
+ return false
390
+ }
391
+ if observation.FileExists != nil && *observation.FileExists && observation.DirectoryExists != nil && *observation.DirectoryExists {
392
+ return false
249
393
  }
250
- return *left == *right
394
+ if observation.Stat != nil {
395
+ switch *observation.Stat {
396
+ case "directory":
397
+ if (observation.FileExists != nil && *observation.FileExists) ||
398
+ (observation.DirectoryExists != nil && !*observation.DirectoryExists) {
399
+ return false
400
+ }
401
+ case "file":
402
+ if (observation.FileExists != nil && !*observation.FileExists) ||
403
+ (observation.DirectoryExists != nil && *observation.DirectoryExists) {
404
+ return false
405
+ }
406
+ case "missing":
407
+ if (observation.FileExists != nil && *observation.FileExists) ||
408
+ (observation.DirectoryExists != nil && *observation.DirectoryExists) {
409
+ return false
410
+ }
411
+ default:
412
+ return false
413
+ }
414
+ }
415
+ if observation.ReadFile != nil && observation.ReadFile.OK {
416
+ if (observation.FileExists != nil && !*observation.FileExists) ||
417
+ (observation.DirectoryExists != nil && *observation.DirectoryExists) ||
418
+ (observation.Stat != nil && *observation.Stat != "file") {
419
+ return false
420
+ }
421
+ }
422
+ return true
251
423
  }
252
424
 
253
- // proof returns a stable compiler-time state. A nil hash/realpath with an empty
254
- // failure is an explicit JSON null for a path observed missing; a non-empty
255
- // failure explains why no complete, internally consistent proof exists.
256
- func (fs *inputObservationFS) proof(path string) (hash, realpath *string, failure inputProofFailure) {
425
+ // predicateProof returns every compatible compiler filesystem constraint for
426
+ // path without collapsing different predicates into a guessed object kind.
427
+ func (fs *inputObservationFS) predicateProof(path string) (TransformInputObservation, inputProofFailure) {
257
428
  key := fs.observationKey(path)
258
429
  if key == "" {
259
- return nil, nil, inputProofInvalidPath
430
+ return TransformInputObservation{}, inputProofInvalidPath
260
431
  }
261
432
  fs.mu.Lock()
262
433
  observation, found := fs.observations[key]
263
434
  // TypeScript-Go can probe a lexical symlink candidate and then read the
264
435
  // selected source by its physical filename. Reuse that exact observed read
265
436
  // for the alias instead of issuing an eager duplicate read from FileExists.
266
- if found && observation.failure == "" && observation.kind == observedInputFile && observation.contentHash == nil && observation.realpath != nil {
267
- targetKey := fs.observationKey(*observation.realpath)
437
+ if found && observation.failure == "" && observation.proof.FileExists != nil && *observation.proof.FileExists && observation.proof.ReadFile == nil && observation.proof.Realpath != nil && observation.proof.Realpath.OK {
438
+ targetKey := fs.observationKey(observation.proof.Realpath.Path)
268
439
  target, targetFound := fs.observations[targetKey]
269
- if targetFound && target.failure == "" && target.kind == observedInputFile && target.contentHash != nil && sameOptionalString(target.realpath, observation.realpath) {
270
- targetHash := *target.contentHash
271
- observation.contentHash = &targetHash
440
+ if targetFound && target.failure == "" && target.proof.ReadFile != nil && target.proof.ReadFile.OK && target.proof.Realpath != nil && sameRealpathObservation(target.proof.Realpath, observation.proof.Realpath) {
441
+ read := *target.proof.ReadFile
442
+ observation.proof.ReadFile = &read
272
443
  }
273
444
  }
274
445
  fs.mu.Unlock()
275
446
  if !found {
276
- return nil, nil, inputProofUnobserved
447
+ return TransformInputObservation{}, inputProofUnobserved
277
448
  }
278
449
  if observation.failure != "" {
279
- return nil, nil, observation.failure
450
+ return TransformInputObservation{}, observation.failure
280
451
  }
281
- switch observation.kind {
282
- case observedInputMissing:
283
- return nil, nil, ""
284
- case observedInputDirectory:
285
- hash := observedDirectoryDigest
286
- if observation.realpath == nil {
452
+ return observation.proof, ""
453
+ }
454
+
455
+ // proof returns a stable compiler-time state. A nil hash/realpath with an empty
456
+ // failure is an explicit JSON null for a path observed missing; a non-empty
457
+ // failure explains why no complete, internally consistent proof exists.
458
+ func (fs *inputObservationFS) proof(path string) (hash, realpath *string, failure inputProofFailure) {
459
+ observation, failure := fs.predicateProof(path)
460
+ if failure != "" {
461
+ return nil, nil, failure
462
+ }
463
+ if observation.ReadFile != nil && observation.ReadFile.OK {
464
+ if observation.Realpath == nil || !observation.Realpath.OK {
287
465
  return nil, nil, inputProofRealpathUnavailable
288
466
  }
289
- realpath := *observation.realpath
467
+ hash := observation.ReadFile.Hash
468
+ realpath := observation.Realpath.Path
290
469
  return &hash, &realpath, ""
291
- case observedInputFile:
292
- if observation.contentHash == nil || observation.realpath == nil {
293
- if observation.contentHash == nil {
294
- return nil, nil, inputProofContentUnavailable
295
- }
470
+ }
471
+ directory := (observation.Stat != nil && *observation.Stat == "directory") ||
472
+ (observation.DirectoryExists != nil && *observation.DirectoryExists)
473
+ if directory {
474
+ hash := observedDirectoryDigest
475
+ if observation.Realpath == nil || !observation.Realpath.OK {
296
476
  return nil, nil, inputProofRealpathUnavailable
297
477
  }
298
- hash := *observation.contentHash
299
- realpath := *observation.realpath
478
+ realpath := observation.Realpath.Path
300
479
  return &hash, &realpath, ""
301
- default:
302
- return nil, nil, inputProofUnsupportedInputKind
303
480
  }
481
+ file := (observation.Stat != nil && *observation.Stat == "file") ||
482
+ (observation.FileExists != nil && *observation.FileExists)
483
+ if file {
484
+ return nil, nil, inputProofContentUnavailable
485
+ }
486
+ missing := (observation.Stat != nil && *observation.Stat == "missing") ||
487
+ (observation.FileExists != nil && !*observation.FileExists) ||
488
+ (observation.DirectoryExists != nil && !*observation.DirectoryExists) ||
489
+ (observation.ReadFile != nil && !observation.ReadFile.OK)
490
+ if missing {
491
+ return nil, nil, ""
492
+ }
493
+ return nil, nil, inputProofUnsupportedInputKind
304
494
  }