@ttsc/evidence 0.29.0 → 0.30.1

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.
package/native/loader.go CHANGED
@@ -22,12 +22,17 @@ import (
22
22
  // that symbol is precisely the one an obligation needs to name — an evidence
23
23
  // graph exists to report the operation the frontend never called.
24
24
  type typeScriptLoader struct {
25
- root string
26
- program map[string]*artifactInventory
27
- parsed map[string]*artifactInventory
28
- resolved map[string]string
29
- failures map[string]string
30
- installs map[string]installedPackageLocation
25
+ boundary *populationBase
26
+ root string
27
+ identityRoot string
28
+ program map[string]*artifactInventory
29
+ programIDs map[string]*artifactInventory
30
+ parsed map[string]*artifactInventory
31
+ resolved map[string]string
32
+ failures map[string]string
33
+ parseFailures map[string]string
34
+ identities map[string]typeScriptModuleIdentity
35
+ installs map[string]installedPackageLocation
31
36
  }
32
37
 
33
38
  func newTypeScriptLoader(
@@ -35,23 +40,40 @@ func newTypeScriptLoader(
35
40
  program map[string]*artifactInventory,
36
41
  ) *typeScriptLoader {
37
42
  loader := &typeScriptLoader{
38
- root: strings.ReplaceAll(root, "\\", "/"),
39
- program: map[string]*artifactInventory{},
40
- parsed: map[string]*artifactInventory{},
41
- resolved: map[string]string{},
42
- failures: map[string]string{},
43
- installs: map[string]installedPackageLocation{},
44
- }
45
- for _, inventory := range program {
43
+ root: strings.ReplaceAll(root, "\\", "/"),
44
+ program: map[string]*artifactInventory{},
45
+ programIDs: map[string]*artifactInventory{},
46
+ parsed: map[string]*artifactInventory{},
47
+ resolved: map[string]string{},
48
+ failures: map[string]string{},
49
+ parseFailures: map[string]string{},
50
+ identities: map[string]typeScriptModuleIdentity{},
51
+ installs: map[string]installedPackageLocation{},
52
+ }
53
+ loader.identityRoot, _ = physicalTypeScriptPath(filepath.FromSlash(loader.root))
54
+ keys := make([]string, 0, len(program))
55
+ for key := range program {
56
+ keys = append(keys, key)
57
+ }
58
+ sort.Strings(keys)
59
+ for _, key := range keys {
60
+ inventory := program[key]
46
61
  if inventory == nil || inventory.Path == "" {
47
62
  continue
48
63
  }
49
64
  location := loader.projectPath(inventory.Path)
65
+ identity, _ := loader.moduleIdentity(location)
66
+ if inventory.Address != identity && inventory.Source != nil {
67
+ inventory = typeScriptInventories.scan(typeScriptModuleAddress(location, identity), inventory.Source)
68
+ }
50
69
  current := loader.program[location]
51
70
  if current == nil ||
52
71
  current.Address != current.Path && inventory.Address == inventory.Path {
53
72
  loader.program[location] = inventory
54
73
  }
74
+ if loader.programIDs[identity] == nil || location == identity {
75
+ loader.programIDs[identity] = inventory
76
+ }
55
77
  }
56
78
  return loader
57
79
  }
@@ -60,24 +82,41 @@ func newTypeScriptLoader(
60
82
  //
61
83
  // The Program's copy wins when it exists so that a file under edit is read as
62
84
  // the editor has it, not as the disk last saw it.
63
- func (loader *typeScriptLoader) inventory(relative string) *artifactInventory {
85
+ func (loader *typeScriptLoader) inventory(relative string) (result *artifactInventory) {
64
86
  if relative == "" {
65
87
  return nil
66
88
  }
67
89
  relative = loader.projectPath(relative)
68
- if inventory := loader.program[relative]; inventory != nil {
90
+ if !loader.withinBoundary(relative) {
91
+ loader.failures[relative] = "the re-export leaves the explicitly configured root"
92
+ return nil
93
+ }
94
+ defer func() {
95
+ if loader.boundary != nil && result != nil && result.Source != nil && len(result.Source.Diagnostics()) != 0 {
96
+ diagnostic := result.Source.Diagnostics()[0]
97
+ loader.failures[relative] = "TypeScript syntax error TS" + decimal(int(diagnostic.Code())) + " at line " + decimal(lineAt(result.Source.Text(), diagnostic.Pos()))
98
+ result = nil
99
+ }
100
+ }()
101
+ if inventory := loader.programInventory(relative); inventory != nil {
69
102
  return inventory
70
103
  }
71
104
  if inventory, cached := loader.parsed[relative]; cached {
105
+ if inventory == nil && loader.parseFailures[relative] != "" {
106
+ loader.failures[relative] = loader.parseFailures[relative]
107
+ }
72
108
  return inventory
73
109
  }
74
110
  loader.parsed[relative] = loader.parse(relative)
111
+ if loader.parsed[relative] == nil {
112
+ loader.parseFailures[relative] = loader.failures[relative]
113
+ }
75
114
  return loader.parsed[relative]
76
115
  }
77
116
 
78
117
  func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
79
118
  relative = loader.projectPath(relative)
80
- content, err := os.ReadFile(path.Join(loader.root, relative))
119
+ content, err := os.ReadFile(resolveProjectPath(loader.root, relative))
81
120
  if err != nil {
82
121
  loader.failures[relative] = err.Error()
83
122
  return nil
@@ -88,7 +127,7 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
88
127
  }
89
128
  file := shimparser.ParseSourceFile(
90
129
  shimast.SourceFileParseOptions{
91
- FileName: path.Join(loader.root, relative),
130
+ FileName: filepath.ToSlash(resolveProjectPath(loader.root, relative)),
92
131
  },
93
132
  string(content),
94
133
  kind,
@@ -97,7 +136,8 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
97
136
  loader.failures[relative] = "the TypeScript parser returned no source file"
98
137
  return nil
99
138
  }
100
- return scanTypeScriptInventory(relative, file)
139
+ identity, _ := loader.moduleIdentity(relative)
140
+ return scanTypeScriptInventoryAt(typeScriptModuleAddress(relative, identity), file)
101
141
  }
102
142
 
103
143
  func (loader *typeScriptLoader) failure(relative string) string {
@@ -111,19 +151,34 @@ func (loader *typeScriptLoader) failure(relative string) string {
111
151
  // without paying to scan it.
112
152
  func (loader *typeScriptLoader) exists(relative string) bool {
113
153
  relative = loader.projectPath(relative)
114
- if loader.program[relative] != nil {
154
+ if loader.programInventory(relative) != nil {
115
155
  return true
116
156
  }
117
157
  return loader.existsOnDisk(relative)
118
158
  }
119
159
 
160
+ // Authored module paths and physical identity keys are separate namespaces.
161
+ // Mixing them can mistake a sibling of a linked root for a different sibling
162
+ // of the physical project. Identity lookup still recovers its editor snapshot.
163
+ func (loader *typeScriptLoader) programInventory(module string) *artifactInventory {
164
+ if inventory := loader.program[module]; inventory != nil {
165
+ return inventory
166
+ }
167
+ if len(loader.programIDs) != 0 {
168
+ if identity, ok := loader.moduleIdentity(module); ok {
169
+ return loader.programIDs[identity]
170
+ }
171
+ }
172
+ return nil
173
+ }
174
+
120
175
  // existsOnDisk probes the filesystem for an already-normalized path.
121
176
  //
122
177
  // The probe is the expensive half of module resolution — one syscall per
123
178
  // candidate, and a watch cycle resolves every re-export in the population — so
124
179
  // callers that can answer from the Program should do that first.
125
180
  func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
126
- info, err := os.Stat(path.Join(loader.root, relative))
181
+ info, err := os.Stat(resolveProjectPath(loader.root, relative))
127
182
  return err == nil && !info.IsDir()
128
183
  }
129
184
 
@@ -135,6 +190,9 @@ func (loader *typeScriptLoader) resolve(from string, specifier string) string {
135
190
  return cached
136
191
  }
137
192
  loader.resolved[key] = loader.resolveUncached(from, specifier)
193
+ if loader.boundary != nil && loader.resolved[key] == "" && loader.failures[from+" -> "+specifier] == "" {
194
+ loader.failures[from+" -> "+specifier] = "the exported module cannot be resolved"
195
+ }
138
196
  return loader.resolved[key]
139
197
  }
140
198
 
@@ -142,12 +200,19 @@ func (loader *typeScriptLoader) resolveUncached(
142
200
  from string,
143
201
  specifier string,
144
202
  ) string {
203
+ failure := from + " -> " + specifier
145
204
  if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
205
+ if loader.boundary != nil {
206
+ if canonical, ok := loader.moduleIdentity(from); ok {
207
+ from = filepath.ToSlash(resolveProjectPath(loader.identityRoot, canonical))
208
+ }
209
+ }
146
210
  base := path.Clean(path.Join(path.Dir(from), specifier))
147
211
  candidates := moduleCandidates(base)
148
212
  normalized := make([]string, 0, len(candidates))
149
213
  for _, candidate := range candidates {
150
- normalized = append(normalized, loader.projectPath(candidate))
214
+ module := loader.projectPath(candidate)
215
+ normalized = append(normalized, module)
151
216
  }
152
217
  // The Program answers first, and not only because it answers without a
153
218
  // syscall. A project that emits beside its sources has both `x.js` and
@@ -155,12 +220,20 @@ func (loader *typeScriptLoader) resolveUncached(
155
220
  // resolving an import to emitted JavaScript would read a module whose
156
221
  // declarations the graph cannot address.
157
222
  for _, candidate := range normalized {
158
- if loader.program[candidate] != nil {
223
+ if loader.programInventory(candidate) != nil {
224
+ if !loader.withinBoundary(candidate) {
225
+ loader.failures[failure] = "the re-export leaves the explicitly configured root"
226
+ return ""
227
+ }
159
228
  return candidate
160
229
  }
161
230
  }
162
231
  for _, candidate := range normalized {
163
232
  if loader.existsOnDisk(candidate) {
233
+ if !loader.withinBoundary(candidate) {
234
+ loader.failures[failure] = "the re-export leaves the explicitly configured root"
235
+ return ""
236
+ }
164
237
  return candidate
165
238
  }
166
239
  }
@@ -169,9 +242,83 @@ func (loader *typeScriptLoader) resolveUncached(
169
242
  if strings.HasPrefix(specifier, "/") {
170
243
  return ""
171
244
  }
245
+ if loader.boundary != nil {
246
+ loader.failures[failure] = "a package re-export is outside the explicitly configured root"
247
+ return ""
248
+ }
172
249
  return loader.resolvePackage(specifier)
173
250
  }
174
251
 
252
+ type typeScriptModuleIdentity struct {
253
+ Path string
254
+ Resolved bool
255
+ }
256
+
257
+ // Module identity follows directory links and file symlinks, while displayed
258
+ // paths and published module addresses retain their authored spellings.
259
+ // Cache once per evaluation so rooted populations share this filesystem work.
260
+ func (loader *typeScriptLoader) moduleIdentity(module string) (string, bool) {
261
+ module = loader.projectPath(module)
262
+ if cached, exists := loader.identities[module]; exists {
263
+ return cached.Path, cached.Resolved
264
+ }
265
+ absolute := resolveProjectPath(loader.root, module)
266
+ resolved, ok := physicalTypeScriptPath(absolute)
267
+ // Resolve both sides before taking a relative identity. Canonicalizing only
268
+ // the file makes a project junction or Windows 8.3 root leak the checkout's
269
+ // absolute location into every unit ID and invalidate unchanged reviews.
270
+ identity := projectRelativeDisplay(loader.identityRoot, resolved)
271
+ loader.identities[module] = typeScriptModuleIdentity{Path: identity, Resolved: ok}
272
+ return identity, ok
273
+ }
274
+
275
+ func physicalTypeScriptPath(absolute string) (string, bool) {
276
+ current := filepath.Clean(absolute)
277
+ // A directory link can introduce another link in an already-walked parent.
278
+ // Settle the complete path, with the same finite bound as directory chains.
279
+ for range 32 {
280
+ resolved, ok := resolveLinkedPath(current)
281
+ if !ok {
282
+ return resolved, false
283
+ }
284
+ resolved = expandTypeScriptPath(resolved)
285
+ if resolved == current {
286
+ return resolved, true
287
+ }
288
+ current = resolved
289
+ }
290
+ return current, false
291
+ }
292
+
293
+ // An unsaved or deleted file still has an identity. Expand the deepest existing
294
+ // prefix so Windows short names do not reappear when EvalSymlinks cannot read
295
+ // the complete path, then restore the missing suffix without changing its case.
296
+ func expandTypeScriptPath(absolute string) string {
297
+ probe := absolute
298
+ suffix := []string{}
299
+ for {
300
+ if final, err := filepath.EvalSymlinks(probe); err == nil {
301
+ if !strings.EqualFold(final, probe) {
302
+ probe = final
303
+ }
304
+ for index := len(suffix) - 1; index >= 0; index-- {
305
+ probe = filepath.Join(probe, suffix[index])
306
+ }
307
+ return probe
308
+ }
309
+ parent := filepath.Dir(probe)
310
+ if parent == probe {
311
+ return absolute
312
+ }
313
+ suffix = append(suffix, filepath.Base(probe))
314
+ probe = parent
315
+ }
316
+ }
317
+
318
+ func typeScriptModuleAddress(display string, identity string) artifactAddress {
319
+ return artifactAddress{Base: populationBase{Default: true}, Relative: identity, Display: display, Key: identity}
320
+ }
321
+
175
322
  // projectPath gives every Program source and module candidate one identity
176
323
  // relative to the ttsc project. A rooted claim may name the same physical file
177
324
  // through `../api`, while an import can arrive there through a different
package/native/model.go CHANGED
@@ -93,9 +93,12 @@ type referenceSpec struct {
93
93
  Type artifactKind
94
94
  Policy referencePolicy
95
95
  Root string
96
- Base populationBase
97
- Files globSet
98
- Source string
96
+ // Rooted preserves explicit disk-loading opt-in even when '.' normalizes
97
+ // to the empty spelling of the project root.
98
+ Rooted bool
99
+ Base populationBase
100
+ Files globSet
101
+ Source string
99
102
  // Package moves the base that Files resolves against from the project to an
100
103
  // installed package. With no globs it also becomes the selection itself: the
101
104
  // package's declaration entry defines the population by reachability, while
@@ -333,6 +336,8 @@ func (declaration *evidenceDeclaration) valid() bool {
333
336
  }
334
337
 
335
338
  type artifactInventory struct {
339
+ // Source supplies declaration diagnostics and the active editor snapshot.
340
+ Source *shimast.SourceFile
336
341
  // Address is the population-relative identity used for units and
337
342
  // declarations. It differs from Path when a configured root moves the
338
343
  // address space while a diagnostic keeps naming the file the way a reader
@@ -449,6 +454,8 @@ type claimState struct {
449
454
  }
450
455
 
451
456
  type referenceState struct {
457
+ // Code resolves finite accessor paths independently of population traversal.
458
+ Code *typeScriptExportResolver
452
459
  Spec referenceSpec
453
460
  Paths []string
454
461
  Units []*evidenceUnit
@@ -0,0 +1,116 @@
1
+ package evidence
2
+
3
+ import (
4
+ "io/fs"
5
+ "os"
6
+ "path/filepath"
7
+ "sort"
8
+ "strings"
9
+ )
10
+
11
+ // A rooted reference selects entries from disk and follows their exports only
12
+ // inside its declared root. Ordinary imports do not expand the population.
13
+ func materializeRootedTypeScriptReference(claim claimSpec, reference referenceSpec, loader *typeScriptLoader) (referenceState, []string) {
14
+ state := referenceState{Spec: reference, Healthy: false, UnitsByScope: map[string][]*evidenceUnit{}}
15
+ context := claimLabel(claim) + " " + referenceLabel(reference)
16
+ base := reference.Base
17
+ if info, err := os.Stat(base.Absolute); err != nil || !info.IsDir() {
18
+ cause := "the path names a file"
19
+ if err != nil {
20
+ cause = causeText(err)
21
+ }
22
+ return state, []string{context + " could not read TypeScript reference root '" + populationRootLabel(base) + "': " + cause + ". Select an existing readable directory containing the reference files."}
23
+ }
24
+ from, resolved := resolvedBaseDirectory(base)
25
+ if !resolved {
26
+ return state, []string{unresolvedBaseProblem(base, artifactTypeScript)}
27
+ }
28
+ from, resolved = physicalTypeScriptPath(from)
29
+ if !resolved {
30
+ return state, []string{unresolvedBaseProblem(base, artifactTypeScript)}
31
+ }
32
+ entries := map[string]bool{}
33
+ problems := []string{}
34
+ err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
35
+ if walkErr != nil {
36
+ return walkErr
37
+ }
38
+ relative, err := filepath.Rel(from, current)
39
+ if err != nil {
40
+ return err
41
+ }
42
+ relative = filepath.ToSlash(relative)
43
+ if entry.IsDir() {
44
+ if relative != "." && (entry.Name() == "node_modules" || !reference.Files.couldMatchDescendant(relative)) {
45
+ return filepath.SkipDir
46
+ }
47
+ return nil
48
+ }
49
+ if isTypeScriptPath(relative) && reference.Files.matches(relative) {
50
+ entries[loader.projectPath(base.display(relative))] = true
51
+ }
52
+ return nil
53
+ })
54
+ if err != nil {
55
+ problems = append(problems, context+" could not list TypeScript root '"+populationRootLabel(base)+"': "+causeText(err)+". Restore filesystem access before evaluating coverage.")
56
+ }
57
+ // An open Program snapshot can precede its first save. It is authoritative
58
+ // on the same terms as an edited file that already exists on disk.
59
+ for module := range loader.program {
60
+ identity, resolved := loader.moduleIdentity(module)
61
+ if !resolved {
62
+ continue
63
+ }
64
+ relative, ok := relativeProjectPath(from, resolveProjectPath(loader.identityRoot, identity))
65
+ if ok && reference.Files.matches(relative) {
66
+ entries[loader.projectPath(base.display(relative))] = true
67
+ }
68
+ }
69
+ for entry := range entries {
70
+ state.Paths = append(state.Paths, entry)
71
+ }
72
+ sort.Strings(state.Paths)
73
+ confined := *loader
74
+ confined.boundary = &base
75
+ confined.resolved = map[string]string{}
76
+ confined.failures = map[string]string{}
77
+ population := materializeEntryUnits(&confined, state.Paths, reference.Symbols)
78
+ state.Units, state.Hidden, state.Published = population.Units, population.Hidden, population.Published
79
+ state.Code = population.Code
80
+ applyTraversedScopes(&state, population.Reached)
81
+ failed := []string{}
82
+ for module := range confined.failures {
83
+ failed = append(failed, module)
84
+ }
85
+ sort.Strings(failed)
86
+ for _, module := range failed {
87
+ problems = append(problems, context+" could not load TypeScript module '"+module+"': "+confined.failures[module]+". Repair the export or explicitly select a root containing its implementation.")
88
+ }
89
+ state.Healthy = len(problems) == 0
90
+ if !state.Healthy {
91
+ return state, problems
92
+ }
93
+ if len(state.Paths) == 0 {
94
+ return state, []string{context + " matched no typescript files for " + describePopulation(base, reference.Files) + ". Correct the configured root and files."}
95
+ }
96
+ if len(state.Units) == 0 {
97
+ return state, []string{context + " found no selected evidence units (" + reference.Symbols.names() + "). Select symbol kinds present in its exported declarations."}
98
+ }
99
+ return state, nil
100
+ }
101
+
102
+ func (loader *typeScriptLoader) withinBoundary(module string) bool {
103
+ if loader.boundary == nil {
104
+ return true
105
+ }
106
+ base, baseOK := loader.moduleIdentity(loader.boundary.Absolute)
107
+ source, sourceOK := loader.moduleIdentity(module)
108
+ if !baseOK || !sourceOK {
109
+ return false
110
+ }
111
+ relative, ok := relativeProjectPath(resolveProjectPath(loader.identityRoot, base), resolveProjectPath(loader.identityRoot, source))
112
+ if !ok || strings.Contains("/"+relative+"/", "/node_modules/") {
113
+ return false
114
+ }
115
+ return true
116
+ }
@@ -71,11 +71,8 @@ func extendTypeScriptInventories(
71
71
  // three kinds converge: health alone now decides that the claim stays active,
72
72
  // and the population it names is the one the author has to repair.
73
73
  //
74
- // Only claim bases can reach a declared root here. A TypeScript reference selects
75
- // the active program with `files` or an installed package with `package` and is
76
- // refused a `root` outright, so every reference base of this kind is the default
77
- // one, which this pass leaves alone: `baseDirectoryProblem` returns on it, and
78
- // the resolver question below is asked only of a root someone declared.
74
+ // Check passes claim populations here. Rooted references validate their disk
75
+ // directories after claim activation in materializeRootedTypeScriptReference.
79
76
  func typeScriptBaseProblems(
80
77
  config graphConfig,
81
78
  inventories map[string]*artifactInventory,
@@ -245,6 +242,7 @@ func scanTypeScriptInventoryAt(
245
242
  file *shimast.SourceFile,
246
243
  ) *artifactInventory {
247
244
  inventory := &artifactInventory{
245
+ Source: file,
248
246
  Address: address.Key,
249
247
  Path: address.Display,
250
248
  Type: artifactTypeScript,
@@ -528,7 +526,7 @@ func collectTypeScriptStatements(
528
526
  if typeOnlyProjection {
529
527
  continue
530
528
  }
531
- name := declarationName(statement.Name())
529
+ name := typeScriptDeclarationName(statement)
532
530
  if name == "" {
533
531
  continue
534
532
  }
@@ -583,7 +581,7 @@ func collectTypeScriptStatements(
583
581
  addTypeScriptHost(supportedHosts, statement, symbol)
584
582
  }
585
583
  case shimast.KindClassDeclaration:
586
- name := declarationName(statement.Name())
584
+ name := typeScriptDeclarationName(statement)
587
585
  if name == "" {
588
586
  continue
589
587
  }
@@ -1377,7 +1375,7 @@ func collectHiddenDeclarationNames(
1377
1375
  if statement == nil {
1378
1376
  continue
1379
1377
  }
1380
- name := declarationName(statement.Name())
1378
+ name := typeScriptDeclarationName(statement)
1381
1379
  if name == "" {
1382
1380
  continue
1383
1381
  }
@@ -1701,6 +1699,12 @@ func walkTypeScriptNode(node *shimast.Node, visit func(*shimast.Node)) {
1701
1699
  type exportedName struct {
1702
1700
  Public string
1703
1701
  TypeOnly bool
1702
+ // IdentityOnly materializes a default's local declaration without
1703
+ // publishing its binding as a named export.
1704
+ IdentityOnly bool
1705
+ // ValueIdentity retains the value side needed by a default alias while
1706
+ // TypeOnly still describes this named export's public restriction.
1707
+ ValueIdentity bool
1704
1708
  }
1705
1709
 
1706
1710
  func collectLocalExportNames(
@@ -1748,6 +1752,21 @@ func collectLocalExportNames(
1748
1752
  })
1749
1753
  }
1750
1754
  }
1755
+ for local, binding := range collectDefaultExportBindings(statements) {
1756
+ if len(exports[local]) == 0 {
1757
+ binding.IdentityOnly = true
1758
+ exports[local] = []exportedName{binding}
1759
+ } else if !binding.TypeOnly {
1760
+ selected := 0
1761
+ for index, name := range exports[local] {
1762
+ current := exports[local][selected]
1763
+ if current.TypeOnly && !name.TypeOnly || current.TypeOnly == name.TypeOnly && name.Public < current.Public {
1764
+ selected = index
1765
+ }
1766
+ }
1767
+ exports[local][selected].ValueIdentity = true
1768
+ }
1769
+ }
1751
1770
  return exports
1752
1771
  }
1753
1772
 
@@ -1787,6 +1806,9 @@ func publicTypeScriptExports(
1787
1806
  names[local] = exportedName{Public: local}
1788
1807
  }
1789
1808
  for _, exported := range exports[local] {
1809
+ if exported.ValueIdentity {
1810
+ exported.TypeOnly = false
1811
+ }
1790
1812
  if exported.TypeOnly && !allowTypeOnly {
1791
1813
  continue
1792
1814
  }
@@ -1820,9 +1842,6 @@ func declarationName(node *shimast.Node) string {
1820
1842
  shimast.KindStringLiteral,
1821
1843
  shimast.KindNumericLiteral:
1822
1844
  name := node.Text()
1823
- if containsWhitespace(name) {
1824
- return ""
1825
- }
1826
1845
  return name
1827
1846
  default:
1828
1847
  return ""
@@ -29,11 +29,12 @@ import (
29
29
  // depend on the hit.
30
30
  var typeScriptInventories = newTypeScriptInventoryCache()
31
31
 
32
- // typeScriptInventoryKey identifies one scan. The address is part of it because
33
- // one physical file can be addressed differently under two configured roots,
34
- // and the address is baked into every unit identity the scan produces.
32
+ // typeScriptInventoryKey identifies one scan. Both the unit address and display
33
+ // path belong to its result. Root aliases can share unit IDs while a file has
34
+ // different project-relative paths; reusing the old path can read another file.
35
35
  type typeScriptInventoryKey struct {
36
36
  address string
37
+ display string
37
38
  file *shimast.SourceFile
38
39
  }
39
40
 
@@ -69,7 +70,7 @@ func (cache *typeScriptInventoryCache) scan(
69
70
  if cache == nil || file == nil {
70
71
  return scanTypeScriptInventoryAt(address, file)
71
72
  }
72
- key := typeScriptInventoryKey{address: address.Key, file: file}
73
+ key := typeScriptInventoryKey{address: address.Key, display: address.Display, file: file}
73
74
  cache.mutex.Lock()
74
75
  if inventory, hit := cache.live[key]; hit {
75
76
  cache.mutex.Unlock()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/evidence",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "Evidence-graph lint contributor for @ttsc/lint: declare provenance with a JSDoc @evidence tag and fail the build when it is missing or dangling.",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -48,13 +48,13 @@
48
48
  "@types/node": "^25.3.0",
49
49
  "rimraf": "^6.1.2",
50
50
  "typescript": "^7.0.2",
51
- "ttsc": "0.29.0",
52
- "@ttsc/lint": "0.29.0"
51
+ "@ttsc/lint": "0.30.1",
52
+ "ttsc": "0.30.1"
53
53
  },
54
54
  "dependencies": {
55
55
  "@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",
56
- "@typia/interface": "14.0.0",
57
- "@typia/utils": "14.0.0",
56
+ "@typia/interface": "^14.0.6",
57
+ "@typia/utils": "^14.0.6",
58
58
  "yaml": "^2.9.0"
59
59
  },
60
60
  "homepage": "https://ttsc.dev",