@ttsc/evidence 0.28.6 → 0.30.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.
@@ -0,0 +1,370 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/url"
6
+ "path"
7
+ "path/filepath"
8
+ "sort"
9
+ "strings"
10
+ "unicode"
11
+ )
12
+
13
+ const fileLinkPrefix = "\x00file:"
14
+
15
+ func isFileLinkTarget(target string) bool {
16
+ return strings.HasPrefix(target, fileLinkPrefix)
17
+ }
18
+
19
+ // splitFileLinkBody reads one address, allowing spaces only inside a quoted
20
+ // accessor segment. Paths use percent encoding, so # always separates the file.
21
+ func splitFileLinkBody(body string) (string, string, bool) {
22
+ forced := isFileLinkTarget(body)
23
+ if !forced {
24
+ for _, marker := range inlineLinkTags {
25
+ if strings.HasPrefix(body, marker) {
26
+ return "", "", false
27
+ }
28
+ }
29
+ }
30
+ body = strings.TrimPrefix(body, fileLinkPrefix)
31
+ file, _, qualified := strings.Cut(body, "#")
32
+ if decoded, err := url.PathUnescape(file); err == nil {
33
+ file = decoded
34
+ }
35
+ if !forced && (!qualified || !isTypeScriptPath(file)) {
36
+ return "", "", false
37
+ }
38
+ quoted, escaped := false, false
39
+ end := len(body)
40
+ for index, char := range body {
41
+ if escaped {
42
+ escaped = false
43
+ continue
44
+ }
45
+ if quoted && char == '\\' {
46
+ escaped = true
47
+ continue
48
+ }
49
+ if char == '"' {
50
+ quoted = !quoted
51
+ }
52
+ if !quoted && unicode.IsSpace(char) {
53
+ end = index
54
+ break
55
+ }
56
+ }
57
+ target := body[:end]
58
+ if target == "" {
59
+ return "", strings.TrimSpace(body[end:]), true
60
+ }
61
+ if link, problem := parseFileLink(target); problem == "" {
62
+ target = encodeFileLinkPath(link.File) + "#" + formatFileAccessor(link.Segments)
63
+ }
64
+ return fileLinkPrefix + target, strings.TrimSpace(body[end:]), true
65
+ }
66
+
67
+ type fileLink struct {
68
+ File string
69
+ Segments []string
70
+ }
71
+
72
+ func parseFileLink(target string) (fileLink, string) {
73
+ target = strings.TrimPrefix(target, fileLinkPrefix)
74
+ file, accessor, found := strings.Cut(target, "#")
75
+ if !found || file == "" || accessor == "" {
76
+ return fileLink{}, "write a TypeScript file path followed by '#' and an exported declaration or member"
77
+ }
78
+ decoded, err := url.PathUnescape(file)
79
+ if err != nil || strings.ContainsAny(decoded, "\x00\r\n") || !isTypeScriptPath(decoded) {
80
+ return fileLink{}, "the file must be a TypeScript path with valid percent escapes; use %20 for a space and %23 for a literal '#'"
81
+ }
82
+ normalized := strings.ReplaceAll(decoded, "\\", "/")
83
+ // path.Clean collapses a UNC prefix; preserve that volume spelling.
84
+ unc := strings.HasPrefix(normalized, "//")
85
+ normalized = path.Clean(normalized)
86
+ if unc {
87
+ normalized = "/" + normalized
88
+ }
89
+ segments, problem := parseFileAccessor(accessor)
90
+ if problem != "" {
91
+ return fileLink{}, problem
92
+ }
93
+ return fileLink{File: normalized, Segments: segments}, ""
94
+ }
95
+
96
+ func parseFileAccessor(accessor string) ([]string, string) {
97
+ segments := []string{}
98
+ for len(accessor) != 0 {
99
+ if accessor[0] == '[' {
100
+ closing := -1
101
+ quoted, escaped := false, false
102
+ for index := 1; index < len(accessor); index++ {
103
+ char := accessor[index]
104
+ if escaped {
105
+ escaped = false
106
+ continue
107
+ }
108
+ if quoted && char == '\\' {
109
+ escaped = true
110
+ continue
111
+ }
112
+ if char == '"' {
113
+ quoted = !quoted
114
+ }
115
+ if !quoted && char == ']' {
116
+ closing = index
117
+ break
118
+ }
119
+ }
120
+ if closing < 0 {
121
+ return nil, "close the bracket accessor and its quoted member name"
122
+ }
123
+ literal := accessor[1:closing]
124
+ var segment string
125
+ if len(literal) > 0 && literal[0] == '"' {
126
+ if json.Unmarshal([]byte(literal), &segment) != nil {
127
+ return nil, "a bracket accessor must contain a JSON string or an unsigned integer"
128
+ }
129
+ } else {
130
+ if literal == "" || strings.Trim(literal, "0123456789") != "" || len(literal) > 1 && literal[0] == '0' {
131
+ return nil, "a bracket accessor must contain a JSON string or an unsigned integer"
132
+ }
133
+ segment = literal
134
+ }
135
+ segments = append(segments, segment)
136
+ accessor = accessor[closing+1:]
137
+ } else {
138
+ end := strings.IndexAny(accessor, ".[")
139
+ if end < 0 {
140
+ end = len(accessor)
141
+ }
142
+ segment := accessor[:end]
143
+ if !fileAccessorIdentifier(segment) {
144
+ return nil, "use dotted identifiers or brackets containing a JSON string for literal names"
145
+ }
146
+ segments = append(segments, segment)
147
+ accessor = accessor[end:]
148
+ }
149
+ if accessor == "" {
150
+ break
151
+ }
152
+ if accessor[0] == '.' {
153
+ accessor = accessor[1:]
154
+ if accessor == "" || accessor[0] == '[' {
155
+ return nil, "a dot must be followed by an identifier"
156
+ }
157
+ } else if accessor[0] != '[' {
158
+ return nil, "separate accessor segments with a dot or a bracket"
159
+ }
160
+ }
161
+ if len(segments) == 0 {
162
+ return nil, "name an exported declaration after '#'"
163
+ }
164
+ return segments, ""
165
+ }
166
+
167
+ func fileAccessorIdentifier(value string) bool {
168
+ for index, char := range value {
169
+ if char == '_' || char == '$' || unicode.IsLetter(char) || index > 0 && (unicode.IsDigit(char) || unicode.IsMark(char)) {
170
+ continue
171
+ }
172
+ return false
173
+ }
174
+ return value != ""
175
+ }
176
+
177
+ func formatFileAccessor(segments []string) string {
178
+ var result strings.Builder
179
+ for index, segment := range segments {
180
+ if fileAccessorIdentifier(segment) {
181
+ if index > 0 {
182
+ result.WriteByte('.')
183
+ }
184
+ result.WriteString(segment)
185
+ } else {
186
+ encoded, _ := json.Marshal(segment)
187
+ result.WriteByte('[')
188
+ result.Write(encoded)
189
+ result.WriteByte(']')
190
+ }
191
+ }
192
+ return result.String()
193
+ }
194
+
195
+ func encodeFileLinkPath(file string) string {
196
+ return (&url.URL{Path: file}).EscapedPath()
197
+ }
198
+
199
+ // resolveFileLinkDeclaration selects by module and segmented public accessor.
200
+ // It never searches other files for a matching name. A review still annotates
201
+ // this citation; the existing unit ID owns coverage and content fingerprints.
202
+ func resolveFileLinkDeclaration(declaration *evidenceDeclaration, owners []claimState, loader *typeScriptLoader, index map[int]*fileLinkClaimIndex, context string) (string, string) {
203
+ where := " '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context
204
+ if declaration.Type != artifactMarkdown && declaration.Type != artifactTypeScript {
205
+ return "", "Unsupported file-qualified evidence target" + where + ": use this syntax on a Markdown or TypeScript claim."
206
+ }
207
+ link, problem := parseFileLink(declaration.Target)
208
+ if problem != "" {
209
+ return "", "Malformed file-qualified evidence target" + where + ": " + problem + "."
210
+ }
211
+ absolute := filepath.FromSlash(link.File)
212
+ if !filepath.IsAbs(absolute) {
213
+ absolute = filepath.Join(filepath.Dir(resolveProjectPath(loader.root, declaration.Path)), absolute)
214
+ }
215
+ module := loader.projectPath(filepath.ToSlash(absolute))
216
+ identity, _ := loader.moduleIdentity(module)
217
+ candidates := map[string]*evidenceUnit{}
218
+ hidden := map[string]*evidenceUnit{}
219
+ selectedModule := false
220
+ key := scopedTargetKey{path: identity, target: encodeTypeScriptIdentity(link.Segments)}
221
+ for _, owner := range owners {
222
+ claim := index[owner.Spec.Index]
223
+ if claim == nil {
224
+ continue
225
+ }
226
+ selectedModule = selectedModule || claim.modules[identity]
227
+ for id, unit := range claim.targets[key] {
228
+ candidates[id] = unit
229
+ }
230
+ for id, unit := range claim.hidden[key] {
231
+ hidden[id] = unit
232
+ }
233
+ }
234
+ dynamic, withdrawn := queryFileLinkClaims(index, owners, identity, link.Segments, false)
235
+ for id, unit := range dynamic {
236
+ candidates[id] = unit
237
+ }
238
+ for id, unit := range withdrawn {
239
+ hidden[id] = unit
240
+ }
241
+ if len(candidates) == 1 {
242
+ for id := range candidates {
243
+ return id, ""
244
+ }
245
+ }
246
+ if len(candidates) > 1 {
247
+ descriptions := []string{}
248
+ for _, unit := range candidates {
249
+ descriptions = append(descriptions, unit.Readable+" at "+unit.location())
250
+ }
251
+ sort.Strings(descriptions)
252
+ return "", "Ambiguous file-qualified evidence target" + where + ": " + strings.Join(descriptions, "; ") + ". Select one symbol kind or an unambiguous public accessor."
253
+ }
254
+ if len(hidden) != 0 {
255
+ ids := []string{}
256
+ for id := range hidden {
257
+ ids = append(ids, id)
258
+ }
259
+ sort.Strings(ids)
260
+ return "", hiddenTargetProblem(declaration, hidden[ids[0]], context)
261
+ }
262
+ if !loader.exists(module) {
263
+ return "", "Missing TypeScript evidence file" + where + ": '" + module + "' does not exist. Correct the path relative to the citing file or restore the configured file."
264
+ }
265
+ if !selectedModule {
266
+ return "", "Out-of-population TypeScript evidence target" + where + ": '" + module + "' is not a module selected by this claim's TypeScript references. Configure its files and, for disk sources outside the Program, an explicit root."
267
+ }
268
+ if declarationResolutionUncertain(owners) {
269
+ return "", ""
270
+ }
271
+ return "", diagnoseFileLinkTarget(loader, module, link.Segments, where)
272
+ }
273
+
274
+ type fileLinkClaimIndex struct {
275
+ references map[string][]fileLinkReference
276
+ modules map[string]bool
277
+ targets map[scopedTargetKey]map[string]*evidenceUnit
278
+ hidden map[scopedTargetKey]map[string]*evidenceUnit
279
+ }
280
+
281
+ // Build once per evaluation. Resolution is proportional to the number of
282
+ // owning claims, not their complete populations for every citation.
283
+ func indexFileLinkClaims(states []claimState, loader *typeScriptLoader) map[int]*fileLinkClaimIndex {
284
+ result := map[int]*fileLinkClaimIndex{}
285
+ for _, state := range states {
286
+ index := &fileLinkClaimIndex{modules: map[string]bool{}, targets: map[scopedTargetKey]map[string]*evidenceUnit{}, hidden: map[scopedTargetKey]map[string]*evidenceUnit{}, references: map[string][]fileLinkReference{}}
287
+ result[state.Spec.Index] = index
288
+ for _, reference := range state.References {
289
+ if reference.Spec.Type != artifactTypeScript {
290
+ continue
291
+ }
292
+ scopes := map[string]bool{}
293
+ for _, scope := range reference.Scopes {
294
+ scopes[scope.ID] = true
295
+ }
296
+ if reference.Code != nil {
297
+ modules := map[string]string{}
298
+ for _, entry := range reference.Paths {
299
+ identity, _ := loader.moduleIdentity(entry)
300
+ modules[identity] = entry
301
+ }
302
+ for _, address := range reference.Published {
303
+ identity, _ := loader.moduleIdentity(address.Module)
304
+ modules[identity] = address.Module
305
+ }
306
+ hidden := map[string]bool{}
307
+ for _, unit := range reference.Hidden {
308
+ hidden[unit.ID] = true
309
+ }
310
+ for identity, entry := range modules {
311
+ index.references[identity] = append(index.references[identity], fileLinkReference{entry: entry, code: reference.Code, scopes: scopes, hidden: hidden})
312
+ }
313
+ }
314
+ for _, entry := range reference.Paths {
315
+ module, _ := loader.moduleIdentity(entry)
316
+ index.modules[module] = true
317
+ }
318
+ for _, address := range reference.Published {
319
+ module, _ := loader.moduleIdentity(address.Module)
320
+ index.modules[module] = true
321
+ key := scopedTargetKey{path: module, target: encodeTypeScriptIdentity(address.Segments)}
322
+ target := index.targets
323
+ if address.Unit.Hidden != "" {
324
+ target = index.hidden
325
+ } else if !scopes[address.Unit.ID] {
326
+ continue
327
+ }
328
+ if target[key] == nil {
329
+ target[key] = map[string]*evidenceUnit{}
330
+ }
331
+ target[key][address.Unit.ID] = address.Unit
332
+ }
333
+ }
334
+ }
335
+ return result
336
+ }
337
+
338
+ type fileLinkReference struct {
339
+ entry string
340
+ code *typeScriptExportResolver
341
+ scopes map[string]bool
342
+ hidden map[string]bool
343
+ }
344
+
345
+ // Namespace cycles have infinitely many possible spellings but a citation has
346
+ // finitely many segments. Resolve that path on demand, then apply the same
347
+ // reference membership as the finite population index.
348
+ func queryFileLinkClaims(index map[int]*fileLinkClaimIndex, owners []claimState, module string, segments []string, legacy bool) (map[string]*evidenceUnit, map[string]*evidenceUnit) {
349
+ found, hidden := map[string]*evidenceUnit{}, map[string]*evidenceUnit{}
350
+ for _, owner := range owners {
351
+ claim := index[owner.Spec.Index]
352
+ if claim == nil {
353
+ continue
354
+ }
355
+ for _, reference := range claim.references[module] {
356
+ for _, unit := range reference.code.lookup(reference.entry, segments, legacy) {
357
+ if unit.Hidden != "" {
358
+ if reference.hidden[unit.ID] {
359
+ hidden[unit.ID] = unit
360
+ }
361
+ continue
362
+ }
363
+ if reference.scopes[unit.ID] {
364
+ found[unit.ID] = unit
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return found, hidden
370
+ }
@@ -0,0 +1,160 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ )
8
+
9
+ // Diagnostic inspection never creates an obligation. It distinguishes a
10
+ // declaration the collector intentionally withheld from a misspelled address.
11
+ func diagnoseFileLinkTarget(loader *typeScriptLoader, module string, segments []string, where string) string {
12
+ inventory := loader.inventory(module)
13
+ if inventory == nil {
14
+ return "Unreadable TypeScript evidence target" + where + ": " + loader.failure(module) + ". Restore the configured source file."
15
+ }
16
+ resolver := newTypeScriptExportResolver(loader, []string{module})
17
+ if len(resolver.lookup(module, segments, false)) != 0 {
18
+ return "Unselected TypeScript evidence target" + where + ": the declaration exists but this reference's files or symbol selection does not admit it. Select its symbol kind or cite a selected declaration."
19
+ }
20
+ exports := resolver.accessor(module, segments, nil, false, false)
21
+ sort.SliceStable(exports, func(left, right int) bool { return len(exports[left].Address) > len(exports[right].Address) })
22
+ for _, exported := range exports {
23
+ if exported.Local == "" {
24
+ namespace := formatFileAccessor(exported.Address)
25
+ if len(exported.Address) == len(segments) {
26
+ return "Unsupported TypeScript evidence target" + where + ": '" + namespace + "' is a module namespace, not a declared evidence unit. Cite a supported declaration inside it."
27
+ }
28
+ return "Missing TypeScript evidence export" + where + ": the module namespace '" + namespace + "' exports no declaration named '" + segments[len(exported.Address)] + "'. Correct the name inside that namespace."
29
+ }
30
+ if len(exported.Address) > len(segments) {
31
+ continue
32
+ }
33
+ if encodeTypeScriptIdentity(exported.Address) != encodeTypeScriptIdentity(segments[:len(exported.Address)]) {
34
+ continue
35
+ }
36
+ source := loader.inventory(exported.Path)
37
+ if source == nil || source.Source == nil {
38
+ continue
39
+ }
40
+ local := exported.Local
41
+ // Inventories use public local aliases, while the AST uses bindings.
42
+ for _, entry := range source.Exports {
43
+ if entry.Specifier == "" && entry.Public == local && entry.Local != "" {
44
+ local = entry.Local
45
+ break
46
+ }
47
+ }
48
+ tail := append([]string{local}, segments[len(exported.Address):]...)
49
+ if exported.TypeOnly {
50
+ identity := append([]string{exported.Local}, segments[len(exported.Address):]...)
51
+ for _, unit := range source.Units {
52
+ if unit.ValueSpace && !unit.TypeSpace && encodeTypeScriptIdentity(unit.Identity) == encodeTypeScriptIdentity(identity) {
53
+ return "Type-only TypeScript evidence target" + where + ": this export exposes no value through which the member can be addressed. Use a public value export or cite its type."
54
+ }
55
+ }
56
+ }
57
+ if cause := inspectUnavailableDeclaration(source.Source.Statements, tail); cause != "" {
58
+ return "Unsupported TypeScript evidence target" + where + ": " + cause + ". Cite a supported public declaration."
59
+ }
60
+ if len(segments) > len(exported.Address) {
61
+ return "Missing TypeScript evidence member" + where + ": the exported declaration has no addressable member '" + formatFileAccessor(segments[len(exported.Address):]) + "'. Correct the member name and use '.prototype' for an instance member."
62
+ }
63
+ return "Unsupported TypeScript evidence declaration" + where + ": the export is not a supported type, function, or property unit. Cite a supported declaration."
64
+ }
65
+ if inventory.Source != nil && inventory.Source.Statements != nil {
66
+ for _, statement := range inventory.Source.Statements.Nodes {
67
+ if statement == nil {
68
+ continue
69
+ }
70
+ for _, declared := range topLevelDeclaredNames(statement) {
71
+ if declared.Name == segments[0] {
72
+ return "Unexported TypeScript evidence declaration" + where + ": '" + segments[0] + "' is a local binding, not a public export of this module. Use its public alias or default export."
73
+ }
74
+ }
75
+ }
76
+ }
77
+ if cause := inspectUnavailableDeclaration(inventory.Source.Statements, segments); cause != "" {
78
+ return "Unsupported TypeScript evidence target" + where + ": " + cause + ". Export a supported declaration or correct the target."
79
+ }
80
+ return "Missing TypeScript evidence export" + where + ": the module exports no declaration named '" + segments[0] + "'. Use its public export name or correct the file."
81
+ }
82
+
83
+ func inspectUnavailableDeclaration(statements *shimast.NodeList, segments []string) string {
84
+ if statements == nil || len(segments) == 0 {
85
+ return ""
86
+ }
87
+ for _, statement := range statements.Nodes {
88
+ if statement == nil {
89
+ continue
90
+ }
91
+ if typeScriptDeclarationName(statement) != segments[0] {
92
+ continue
93
+ }
94
+ if len(segments) == 1 {
95
+ if !isSyntacticallyExported(statement) {
96
+ return "the declaration is not exported"
97
+ }
98
+ return "the declaration form is not supported by the evidence unit model"
99
+ }
100
+ tail := segments[1:]
101
+ var members *shimast.NodeList
102
+ class := statement.Kind == shimast.KindClassDeclaration
103
+ switch statement.Kind {
104
+ case shimast.KindClassDeclaration:
105
+ members = statement.AsClassDeclaration().Members
106
+ case shimast.KindInterfaceDeclaration:
107
+ members = statement.AsInterfaceDeclaration().Members
108
+ case shimast.KindTypeAliasDeclaration:
109
+ node := statement.AsTypeAliasDeclaration().Type
110
+ if node != nil && node.Kind == shimast.KindTypeLiteral {
111
+ members = node.AsTypeLiteralNode().Members
112
+ }
113
+ case shimast.KindModuleDeclaration:
114
+ body := statement.AsModuleDeclaration().Body
115
+ if body != nil && body.Kind == shimast.KindModuleBlock {
116
+ return inspectUnavailableDeclaration(body.AsModuleBlock().Statements, tail)
117
+ }
118
+ }
119
+ instance := class && len(tail) > 1 && tail[0] == "prototype"
120
+ if instance {
121
+ tail = tail[1:]
122
+ }
123
+ if members == nil || len(tail) != 1 {
124
+ continue
125
+ }
126
+ for _, member := range members.Nodes {
127
+ if member != nil && member.Kind == shimast.KindConstructor && instance {
128
+ for _, parameter := range member.Parameters() {
129
+ if parameter != nil && isParameterProperty(parameter) && declarationName(parameter.Name()) == tail[0] && !isPublicClassMember(parameter) {
130
+ return "the member is private or protected"
131
+ }
132
+ }
133
+ }
134
+ if member == nil || member.Name() == nil {
135
+ continue
136
+ }
137
+ name := member.Name()
138
+ if name.Kind != shimast.KindIdentifier && name.Kind != shimast.KindPrivateIdentifier && name.Kind != shimast.KindStringLiteral && name.Kind != shimast.KindNumericLiteral {
139
+ continue
140
+ }
141
+ if name.Text() != tail[0] {
142
+ continue
143
+ }
144
+ if class {
145
+ static := shimast.GetCombinedModifierFlags(member)&shimast.ModifierFlagsStatic != 0
146
+ if static == instance {
147
+ continue
148
+ }
149
+ if !isPublicClassMember(member) || name.Kind == shimast.KindPrivateIdentifier {
150
+ return "the member is private or protected"
151
+ }
152
+ }
153
+ if member.Kind == shimast.KindGetAccessor || member.Kind == shimast.KindSetAccessor || member.ModifierFlags()&shimast.ModifierFlagsAccessor != 0 {
154
+ return "accessors are not evidence units"
155
+ }
156
+ return "the member is not exposed as a supported public evidence unit"
157
+ }
158
+ }
159
+ return ""
160
+ }
@@ -161,12 +161,12 @@ func (index *scopeIndex) compute(rootID string) string {
161
161
  if len(scope) == 0 {
162
162
  return ""
163
163
  }
164
- // Ordered by address rather than by walk order, so the value does not depend
164
+ // Ordered by identity rather than by walk order, so the value does not depend
165
165
  // on which file the loader happened to read first, and by symbol as well
166
166
  // because two same-titled Markdown headings answer to one anchor.
167
167
  sort.Slice(scope, func(left int, right int) bool {
168
- if scope[left].Target != scope[right].Target {
169
- return scope[left].Target < scope[right].Target
168
+ if fingerprintIdentity(scope[left]) != fingerprintIdentity(scope[right]) {
169
+ return fingerprintIdentity(scope[left]) < fingerprintIdentity(scope[right])
170
170
  }
171
171
  if scope[left].Symbol != scope[right].Symbol {
172
172
  return scope[left].Symbol < scope[right].Symbol
@@ -178,7 +178,7 @@ func (index *scopeIndex) compute(rootID string) string {
178
178
  // NUL separates the fields because a target may contain any printable
179
179
  // character, and a joined pair that can be re-split ambiguously lets two
180
180
  // different scopes compose one digest.
181
- composite.Write([]byte(unit.Target))
181
+ composite.Write([]byte(fingerprintIdentity(unit)))
182
182
  composite.Write([]byte{0})
183
183
  composite.Write([]byte(unit.Symbol))
184
184
  composite.Write([]byte{0})
@@ -188,6 +188,35 @@ func (index *scopeIndex) compute(rootID string) string {
188
188
  return presentedFingerprint(hex.EncodeToString(composite.Sum(nil)))
189
189
  }
190
190
 
191
+ func fingerprintIdentity(unit *evidenceUnit) string {
192
+ if unit.Type == artifactTypeScript {
193
+ return unit.ID
194
+ }
195
+ return unit.Target
196
+ }
197
+
198
+ // A code review fingerprints the declaring inventory, before any public alias
199
+ // or type-only edge projects its surface. Selection controls permission and
200
+ // coverage; it cannot make one citation require multiple content fingerprints.
201
+ func referenceReviewScopes(reference referenceState, loader *typeScriptLoader) *scopeIndex {
202
+ units := append(append([]*evidenceUnit{}, reference.Population...), reference.Hidden...)
203
+ if reference.Spec.Type != artifactTypeScript {
204
+ return newScopeIndex(units)
205
+ }
206
+ seen := map[string]bool{}
207
+ canonical := []*evidenceUnit{}
208
+ for _, unit := range units {
209
+ if seen[unit.Path] {
210
+ continue
211
+ }
212
+ seen[unit.Path] = true
213
+ if inventory := loader.inventory(unit.Path); inventory != nil {
214
+ canonical = append(canonical, inventory.Units...)
215
+ }
216
+ }
217
+ return newScopeIndex(canonical)
218
+ }
219
+
191
220
  // scopeContribution is what one unit adds to its scope's composite.
192
221
  //
193
222
  // A withdrawn unit contributes the tag that withdrew it. That is what makes a