@ttsc/evidence 0.25.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
@@ -0,0 +1,1409 @@
1
+ package evidence
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "sort"
7
+ "strings"
8
+
9
+ "github.com/samchon/ttsc/packages/lint/rule"
10
+ )
11
+
12
+ type graphRule struct{}
13
+
14
+ func (graphRule) Name() string { return graphRuleName }
15
+
16
+ func (graphRule) NeedsTypeChecker() bool { return false }
17
+
18
+ func (graphRule) Check(ctx *rule.ProjectContext) {
19
+ if ctx == nil {
20
+ return
21
+ }
22
+ typeScriptInventories.beginCycle()
23
+ cycle := &graphCycleState{}
24
+ // SetState survives a later project finding and belongs only to this
25
+ // Program cycle. File rules can therefore coordinate diagnostics even when
26
+ // the graph itself fails, while Hints remains protected by the host's
27
+ // passed-only publication gate.
28
+ ctx.SetState(cycle)
29
+ config, problems := decodeGraphConfig(ctx.Options)
30
+ if len(problems) != 0 {
31
+ reportProblems(ctx, problems)
32
+ return
33
+ }
34
+ config = enabledGraphConfig(config)
35
+ if len(config.Claims) == 0 {
36
+ cycle.Corpus = graphCorpus{Config: config}
37
+ return
38
+ }
39
+ root := evidenceProjectRoot(ctx.Identity)
40
+ if root == "" {
41
+ ctx.Report("Evidence graph could not resolve the project root. Run ttsc with a project config or explicit project root so project-relative evidence globs have one stable base.")
42
+ return
43
+ }
44
+ info, err := os.Stat(root)
45
+ if err != nil || !info.IsDir() {
46
+ ctx.Report("Evidence graph project root '" + root + "' is not a readable directory. Fix the ttsc project identity before evaluating evidence globs.")
47
+ return
48
+ }
49
+ // Every population is anchored before anything is read, so a loader, a
50
+ // diagnostic, and the corpus the editor receives all speak of one resolved
51
+ // base rather than each re-deriving it from the author's spelling.
52
+ resolveGraphBases(root, &config)
53
+
54
+ // A claim with no selected own unit has nothing that can own an
55
+ // acknowledgement. Materialize only claim-side populations first, so an
56
+ // inactive claim cannot make its reference loaders perform work or report
57
+ // diagnostics.
58
+ typescript := loadTypeScriptInventories(
59
+ root,
60
+ ctx.Sources,
61
+ claimPopulationConfig(config, artifactTypeScript),
62
+ )
63
+ markdownClaims, markdownClaimProblems := loadMarkdownInventories(
64
+ root,
65
+ claimPopulationConfig(config, artifactMarkdown),
66
+ )
67
+ prismaClaims, prismaClaimProblems := loadPrismaInventories(
68
+ root,
69
+ claimPopulationConfig(config, artifactPrisma),
70
+ )
71
+ problems = append(problems, markdownClaimProblems...)
72
+ problems = append(problems, prismaClaimProblems...)
73
+ config = activeGraphConfig(config, markdownClaims, prismaClaims, typescript)
74
+ extendTypeScriptInventories(root, ctx.Sources, config, typescript)
75
+ markdown, markdownProblems := loadMarkdownInventories(root, config)
76
+ prisma, prismaProblems := loadPrismaInventories(root, config)
77
+ swagger, swaggerProblems := loadSwaggerInventories(root, config)
78
+ problems = append(problems, markdownProblems...)
79
+ problems = append(problems, prismaProblems...)
80
+ problems = append(problems, swaggerProblems...)
81
+ loader := newTypeScriptLoader(root, typescript)
82
+ states, stateProblems := materializeClaimStates(
83
+ config,
84
+ markdown,
85
+ prisma,
86
+ swagger,
87
+ typescript,
88
+ loader,
89
+ )
90
+ problems = append(problems, stateProblems...)
91
+ problems = append(problems, evaluateEvidenceGraph(states, loader)...)
92
+ reportProblems(ctx, problems)
93
+ if len(problems) == 0 {
94
+ // Published only on a clean evaluation, because the host reads state
95
+ // from a rule that passed and reporting anything marks this one failed
96
+ // (`linthost/hints.go:147-149`, `linthost/project_engine.go:68-77`).
97
+ // Setting it unconditionally would not widen the gate; it would only
98
+ // hide where the gate is.
99
+ cycle.Corpus = graphCorpus{
100
+ Config: config,
101
+ Markdown: markdown,
102
+ Prisma: prisma,
103
+ Swagger: swagger,
104
+ }
105
+ }
106
+ }
107
+
108
+ func init() {
109
+ rule.RegisterProject(graphRule{})
110
+ }
111
+
112
+ func evidenceProjectRoot(identity rule.ProjectIdentity) string {
113
+ for _, candidate := range []string{
114
+ identity.PhysicalProjectRoot,
115
+ identity.LogicalProjectRoot,
116
+ identity.ExplicitProjectRoot,
117
+ identity.InvocationCwd,
118
+ } {
119
+ if candidate == "" {
120
+ continue
121
+ }
122
+ absolute, err := filepath.Abs(candidate)
123
+ if err == nil {
124
+ return filepath.Clean(absolute)
125
+ }
126
+ }
127
+ return ""
128
+ }
129
+
130
+ // claimPopulationConfig removes every reference and other artifact kind from
131
+ // the loader input used to decide activation.
132
+ //
133
+ // This is a loading boundary, not merely an evaluation filter: a claim cannot
134
+ // become inactive because a failed reference was inspected before the claim's
135
+ // own selected host population was known.
136
+ func claimPopulationConfig(
137
+ config graphConfig,
138
+ kind artifactKind,
139
+ ) graphConfig {
140
+ claims := make([]claimSpec, 0, len(config.Claims))
141
+ for _, claim := range config.Claims {
142
+ if claim.Type != kind {
143
+ continue
144
+ }
145
+ claim.References = nil
146
+ claims = append(claims, claim)
147
+ }
148
+ return graphConfig{Claims: claims}
149
+ }
150
+
151
+ // activeGraphConfig omits healthy claim populations that contain no own unit
152
+ // selected by the claim's symbol set.
153
+ //
154
+ // TypeScript, Prisma, and Markdown are the three claim-capable artifact kinds.
155
+ // A healthy zero-file match is empty and therefore inactive, including when a
156
+ // typo caused it. Unhealthy or unreadable populations stay active because
157
+ // failed input cannot prove the selected population is empty.
158
+ func activeGraphConfig(
159
+ config graphConfig,
160
+ markdown map[string]*artifactInventory,
161
+ prisma map[string]*artifactInventory,
162
+ typescript map[string]*artifactInventory,
163
+ ) graphConfig {
164
+ active := make([]claimSpec, 0, len(config.Claims))
165
+ for _, claim := range config.Claims {
166
+ inventories := inventoriesOf(
167
+ claim.Type,
168
+ markdown,
169
+ prisma,
170
+ map[string]*artifactInventory{},
171
+ typescript,
172
+ )
173
+ if claimIsInactive(claim, inventories) {
174
+ continue
175
+ }
176
+ active = append(active, claim)
177
+ }
178
+ config.Claims = active
179
+ return config
180
+ }
181
+
182
+ func claimIsInactive(
183
+ claim claimSpec,
184
+ inventories map[string]*artifactInventory,
185
+ ) bool {
186
+ switch claim.Type {
187
+ case artifactMarkdown, artifactPrisma, artifactTypeScript:
188
+ default:
189
+ return false
190
+ }
191
+ paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
192
+ if !populationIsHealthy(inventories, claim.Base, paths) ||
193
+ unreadableBaseProblem(claim.Base, claim.Type) != "" {
194
+ return false
195
+ }
196
+ for _, path := range paths {
197
+ for _, unit := range inventories[path].Units {
198
+ if unit.Hidden == "" && claim.Symbols.contains(unit.Symbol) {
199
+ return false
200
+ }
201
+ }
202
+ }
203
+ return true
204
+ }
205
+
206
+ func materializeClaimStates(
207
+ config graphConfig,
208
+ markdown map[string]*artifactInventory,
209
+ prisma map[string]*artifactInventory,
210
+ swagger map[string]*artifactInventory,
211
+ typescript map[string]*artifactInventory,
212
+ loader *typeScriptLoader,
213
+ ) ([]claimState, []string) {
214
+ states := make([]claimState, 0, len(config.Claims))
215
+ problems := []string{}
216
+ for _, claim := range config.Claims {
217
+ inventories := inventoriesOf(claim.Type, markdown, prisma, swagger, typescript)
218
+ paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
219
+ state := claimState{
220
+ Spec: claim,
221
+ Paths: paths,
222
+ Healthy: populationIsHealthy(inventories, claim.Base, paths),
223
+ OutsideCarrier: map[string]bool{},
224
+ }
225
+ carrierPaths := map[string]bool{}
226
+ if len(claim.ExclusionCarriers.Patterns) != 0 {
227
+ // The carriers narrow the claim's own population and never widen
228
+ // it, so a pattern matching a file this claim does not select
229
+ // contributes nothing and leaves the set empty.
230
+ claimPaths := map[string]bool{}
231
+ for _, path := range paths {
232
+ claimPaths[path] = true
233
+ }
234
+ for _, carrier := range matchingInventoryPaths(inventories, claim.Base, claim.ExclusionCarriers) {
235
+ if claimPaths[carrier] {
236
+ carrierPaths[carrier] = true
237
+ }
238
+ }
239
+ // A carrier set that selects nothing would silently refuse every
240
+ // exclusion the claim writes, so the misspelling is reported where
241
+ // it was made rather than as a placement finding on each tag.
242
+ if len(carrierPaths) == 0 && len(paths) != 0 {
243
+ problems = append(
244
+ problems,
245
+ claimLabel(claim)+" declares evidenceExcludeCarriers "+describePopulation(claim.Base, claim.ExclusionCarriers)+", which selects none of its "+decimal(len(paths))+" claim file(s). Point the patterns at a file this claim already selects, or drop the property to accept an exclusion anywhere in the population.",
246
+ )
247
+ }
248
+ }
249
+ if len(paths) == 0 && state.Healthy {
250
+ problems = append(
251
+ problems,
252
+ claimLabel(claim)+" matched no "+string(claim.Type)+" files for "+describePopulation(claim.Base, claim.Files)+". Fix the globs or the root they resolve against; '*' stays within one segment, '**' crosses segments, and a bare directory is not recursive.",
253
+ )
254
+ }
255
+ hostsByID := map[string]bool{}
256
+ for _, path := range paths {
257
+ for _, unit := range inventories[path].Units {
258
+ if unit.Hidden != "" ||
259
+ !claim.Symbols.contains(unit.Symbol) ||
260
+ hostsByID[unit.ID] {
261
+ continue
262
+ }
263
+ hostsByID[unit.ID] = true
264
+ state.Hosts = append(state.Hosts, unit)
265
+ }
266
+ state.Declarations = append(
267
+ state.Declarations,
268
+ inventories[path].Declarations...,
269
+ )
270
+ if len(claim.ExclusionCarriers.Patterns) != 0 && !carrierPaths[path] {
271
+ for _, declaration := range inventories[path].Declarations {
272
+ state.OutsideCarrier[declaration.ID] = true
273
+ }
274
+ }
275
+ }
276
+ sortUnits(state.Hosts)
277
+ for _, reference := range claim.References {
278
+ referenceInventories := inventoriesOf(
279
+ reference.Type,
280
+ markdown,
281
+ prisma,
282
+ swagger,
283
+ typescript,
284
+ )
285
+ if reference.Type == artifactTypeScript && reference.entrySelected() {
286
+ entryState, entryProblems := materializeEntryReference(
287
+ claim,
288
+ reference,
289
+ loader,
290
+ )
291
+ problems = append(problems, entryProblems...)
292
+ state.References = append(state.References, entryState)
293
+ continue
294
+ }
295
+ if reference.Type == artifactTypeScript && reference.Package != "" {
296
+ packageState, packageProblems := materializePackageGlobReference(
297
+ claim,
298
+ reference,
299
+ loader,
300
+ )
301
+ problems = append(problems, packageProblems...)
302
+ state.References = append(state.References, packageState)
303
+ continue
304
+ }
305
+ if reference.Type == artifactTypeScript {
306
+ localState, localProblems := materializeLocalTypeScriptReference(
307
+ claim,
308
+ reference,
309
+ referenceInventories,
310
+ loader,
311
+ )
312
+ problems = append(problems, localProblems...)
313
+ state.References = append(state.References, localState)
314
+ continue
315
+ }
316
+ referencePaths := matchingReferencePaths(
317
+ referenceInventories,
318
+ reference,
319
+ )
320
+ referenceState := referenceState{
321
+ Spec: reference,
322
+ Paths: referencePaths,
323
+ UnitsByScope: map[string][]*evidenceUnit{},
324
+ Healthy: populationIsHealthy(referenceInventories, reference.Base, referencePaths),
325
+ }
326
+ if len(referencePaths) == 0 && referenceState.Healthy {
327
+ if reference.Type == artifactSwagger {
328
+ problems = append(
329
+ problems,
330
+ claimLabel(claim)+" "+referenceLabel(reference)+" matched no swagger source for "+describeReferenceSources(reference)+". Fix the reference location; this obligation cannot materialize evidence units without a source.",
331
+ )
332
+ } else {
333
+ problems = append(
334
+ problems,
335
+ claimLabel(claim)+" "+referenceLabel(reference)+" matched no "+string(reference.Type)+" files for "+describePopulation(reference.Base, reference.Files)+". Fix the reference globs or the root they resolve against; this obligation cannot materialize evidence units without files.",
336
+ )
337
+ }
338
+ }
339
+ selectedInventoryProblem := false
340
+ availableUnits := map[string]*evidenceUnit{}
341
+ selectedUnits := map[string]bool{}
342
+ for _, path := range referencePaths {
343
+ for _, inventoryProblem := range referenceInventories[path].Problems {
344
+ if inventoryProblem.Symbol == "*" ||
345
+ reference.Symbols.contains(inventoryProblem.Symbol) {
346
+ selectedInventoryProblem = true
347
+ }
348
+ }
349
+ for _, unit := range referenceInventories[path].Units {
350
+ // A withdrawn declaration is not an obligation and not an
351
+ // aggregate scope either, so it never enters the map that
352
+ // promotes ancestors. It is still recorded, so a citation
353
+ // naming it is answered with the tag rather than with a
354
+ // target that appears not to exist.
355
+ if unit.Hidden != "" {
356
+ if !selectedUnits[unit.ID] {
357
+ selectedUnits[unit.ID] = true
358
+ referenceState.Hidden = append(
359
+ referenceState.Hidden,
360
+ unit,
361
+ )
362
+ }
363
+ continue
364
+ }
365
+ availableUnits[unit.ID] = unit
366
+ if !reference.Symbols.contains(unit.Symbol) ||
367
+ selectedUnits[unit.ID] {
368
+ continue
369
+ }
370
+ selectedUnits[unit.ID] = true
371
+ referenceState.Units = append(referenceState.Units, unit)
372
+ }
373
+ }
374
+ sortUnits(referenceState.Units)
375
+ scopesByID := map[string]*evidenceUnit{}
376
+ for _, unit := range referenceState.Units {
377
+ for scope := unit; scope != nil; scope = availableUnits[scope.ParentID] {
378
+ referenceState.UnitsByScope[scope.ID] = append(
379
+ referenceState.UnitsByScope[scope.ID],
380
+ unit,
381
+ )
382
+ if scopesByID[scope.ID] == nil {
383
+ scopesByID[scope.ID] = scope
384
+ referenceState.Scopes = append(referenceState.Scopes, scope)
385
+ }
386
+ if scope.ParentID == "" {
387
+ break
388
+ }
389
+ }
390
+ }
391
+ sortUnits(referenceState.Scopes)
392
+ if len(referencePaths) != 0 &&
393
+ len(referenceState.Units) == 0 &&
394
+ referenceState.Healthy &&
395
+ !selectedInventoryProblem {
396
+ problems = append(
397
+ problems,
398
+ claimLabel(claim)+" "+referenceLabel(reference)+" matched "+decimal(len(referencePaths))+" file(s) but materialized no selected evidence units ("+reference.Symbols.names()+"). Select symbol kinds present in those files or correct the reference globs.",
399
+ )
400
+ }
401
+ state.References = append(state.References, referenceState)
402
+ }
403
+ states = append(states, state)
404
+ }
405
+ return states, problems
406
+ }
407
+
408
+ func evaluateEvidenceGraph(
409
+ states []claimState,
410
+ loader *typeScriptLoader,
411
+ ) []string {
412
+ problems := []string{}
413
+ targets := map[string]map[string]*evidenceUnit{}
414
+ markdownTargets := map[string]map[string]*evidenceUnit{}
415
+ // Scoped targets are keyed by owning file as well as name, which is what
416
+ // makes import-scope resolution unambiguous: two modules exporting `get`
417
+ // never compete, because resolution already knows which file it landed in.
418
+ scopedTargets := map[scopedTargetKey]map[string]*evidenceUnit{}
419
+ // The same two indexes over the units a documentation tag withdrew. A
420
+ // citation that lands here resolved to a real declaration and must be told
421
+ // that the tag is why the target is not evidence; the alternative is an
422
+ // unresolved-target message that sends the author looking for a typo.
423
+ hiddenTargets := map[string]*evidenceUnit{}
424
+ scopedHidden := map[scopedTargetKey]*evidenceUnit{}
425
+ for _, state := range states {
426
+ for _, reference := range state.References {
427
+ for _, unit := range reference.Hidden {
428
+ for _, address := range append([]string{unit.Target}, unit.Aliases...) {
429
+ hiddenTargets[address] = unit
430
+ }
431
+ }
432
+ for _, address := range reference.Published {
433
+ if address.Unit.Hidden == "" {
434
+ continue
435
+ }
436
+ scopedHidden[scopedTargetKey{
437
+ path: address.Module,
438
+ target: address.Address,
439
+ }] = address.Unit
440
+ }
441
+ if len(reference.Published) == 0 {
442
+ for _, unit := range reference.Hidden {
443
+ for _, address := range append([]string{unit.Target}, unit.Aliases...) {
444
+ scopedHidden[scopedTargetKey{
445
+ path: unit.Path,
446
+ target: address,
447
+ }] = unit
448
+ }
449
+ }
450
+ }
451
+ // A traversed address is valid in the module that publishes it, not in
452
+ // the one that declares the symbol. Identity still belongs to the
453
+ // declaring file; only reachability moves, and a declaration several
454
+ // selected modules publish is citable from each of them.
455
+ scopeIDs := map[string]bool{}
456
+ for _, unit := range reference.Scopes {
457
+ scopeIDs[unit.ID] = true
458
+ }
459
+ for _, address := range reference.Published {
460
+ if !scopeIDs[address.Unit.ID] {
461
+ continue
462
+ }
463
+ key := scopedTargetKey{path: address.Module, target: address.Address}
464
+ if scopedTargets[key] == nil {
465
+ scopedTargets[key] = map[string]*evidenceUnit{}
466
+ }
467
+ scopedTargets[key][address.Unit.ID] = address.Unit
468
+ }
469
+ for _, unit := range reference.Scopes {
470
+ for _, address := range append([]string{unit.Target}, unit.Aliases...) {
471
+ if targets[address] == nil {
472
+ targets[address] = map[string]*evidenceUnit{}
473
+ }
474
+ targets[address][unit.ID] = unit
475
+ }
476
+ if unit.Type == artifactMarkdown {
477
+ if markdownTargets[unit.Target] == nil {
478
+ markdownTargets[unit.Target] = map[string]*evidenceUnit{}
479
+ }
480
+ markdownTargets[unit.Target][unit.ID] = unit
481
+ }
482
+ // A population whose addresses came from traversal already indexed
483
+ // every module that publishes this unit. Only a population addressed
484
+ // by its declaring files needs this fallback.
485
+ if unit.Type == artifactTypeScript && len(reference.Published) == 0 {
486
+ for _, address := range append([]string{unit.Target}, unit.Aliases...) {
487
+ key := scopedTargetKey{path: unit.Path, target: address}
488
+ if scopedTargets[key] == nil {
489
+ scopedTargets[key] = map[string]*evidenceUnit{}
490
+ }
491
+ scopedTargets[key][unit.ID] = unit
492
+ }
493
+ }
494
+ }
495
+ }
496
+ }
497
+
498
+ declarations := map[string]*evidenceDeclaration{}
499
+ owners := map[string][]claimState{}
500
+ for _, state := range states {
501
+ for _, declaration := range state.Declarations {
502
+ declarations[declaration.ID] = declaration
503
+ seen := false
504
+ for _, owner := range owners[declaration.ID] {
505
+ if owner.Spec.Index == state.Spec.Index {
506
+ seen = true
507
+ break
508
+ }
509
+ }
510
+ if !seen {
511
+ owners[declaration.ID] = append(owners[declaration.ID], state)
512
+ }
513
+ }
514
+ }
515
+ declarationIDs := make([]string, 0, len(declarations))
516
+ for id := range declarations {
517
+ declarationIDs = append(declarationIDs, id)
518
+ }
519
+ sort.Strings(declarationIDs)
520
+
521
+ resolved := map[string]string{}
522
+ for _, id := range declarationIDs {
523
+ declaration := declarations[id]
524
+ context := declarationObligationContext(owners[id])
525
+ if !declaration.valid() {
526
+ problems = append(
527
+ problems,
528
+ "Malformed @"+string(declaration.Tag)+" declaration at "+declaration.location()+" for "+context+": target and non-empty reason are mandatory. Write '@"+string(declaration.Tag)+" <target> <reason>'.",
529
+ )
530
+ continue
531
+ }
532
+ if isInlineLinkTarget(declaration.Target) {
533
+ unitID, problem := resolveInlineLinkDeclaration(
534
+ declaration,
535
+ loader,
536
+ scopedTargets,
537
+ scopedHidden,
538
+ context,
539
+ )
540
+ if problem != "" {
541
+ problems = append(problems, problem)
542
+ continue
543
+ }
544
+ resolved[id] = unitID
545
+ continue
546
+ }
547
+ if declaration.Type == artifactTypeScript &&
548
+ looksLikeTypeScriptTarget(declaration.Target, targets, markdownTargets) {
549
+ problems = append(
550
+ problems,
551
+ "Unbraced TypeScript evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a target naming a symbol is now written as an inline link, so the citing module's import is what resolves it. Write '@"+string(declaration.Tag)+" {@link "+declaration.Target+"} <reason>' and import the symbol; 'import type' is enough.",
552
+ )
553
+ continue
554
+ }
555
+ candidates := declarationCandidates(declaration.Target, targets, markdownTargets)
556
+ // The configuration guard refuses a code population to a claim that
557
+ // cannot address one, but the address map is built from every claim at
558
+ // once — so a Markdown claim could still land on a symbol materialized
559
+ // by some *other* claim's TypeScript reference. Measured: it resolved
560
+ // silently, which left repository-wide name uniqueness load-bearing
561
+ // through a door the guard does not cover. Closing it here rather than
562
+ // by scoping the whole map keeps resolution global for the artifacts
563
+ // that are addressed by path, where a shared map costs nothing.
564
+ if declaration.Type != artifactTypeScript {
565
+ addressable, code := splitCodeCandidates(candidates)
566
+ if len(addressable) == 0 && len(code) != 0 {
567
+ problems = append(
568
+ problems,
569
+ "Code evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a "+string(declaration.Type)+" claim cannot cite a TypeScript symbol, because a symbol citation resolves through the citing module's imports and this artifact has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript.",
570
+ )
571
+ continue
572
+ }
573
+ candidates = addressable
574
+ }
575
+ switch len(candidates) {
576
+ case 0:
577
+ // A failed reference population may contain the declaration's target;
578
+ // absence from the partial address map proves nothing until that
579
+ // population is healthy again. Its loader diagnostic already names
580
+ // the repair boundary, so an unresolved-target diagnostic here would
581
+ // be a derivative false claim.
582
+ if declarationResolutionUncertain(owners[id]) {
583
+ continue
584
+ }
585
+ if hidden := hiddenTargets[declaration.Target]; hidden != nil {
586
+ problems = append(
587
+ problems,
588
+ hiddenTargetProblem(declaration, hidden, context),
589
+ )
590
+ continue
591
+ }
592
+ problems = append(
593
+ problems,
594
+ "Unresolved evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": no configured source materializes that evidence unit. Correct the target, or make one of the named references select the source unit this claim actually uses.",
595
+ )
596
+ case 1:
597
+ for unitID := range candidates {
598
+ resolved[id] = unitID
599
+ }
600
+ default:
601
+ descriptions := make([]string, 0, len(candidates))
602
+ for _, unit := range candidates {
603
+ descriptions = append(descriptions, unit.Readable+" at "+unit.location())
604
+ }
605
+ sort.Strings(descriptions)
606
+ problems = append(
607
+ problems,
608
+ "Ambiguous evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": it matches "+strings.Join(descriptions, "; ")+". Rename or qualify the source symbols so the target has exactly one meaning.",
609
+ )
610
+ }
611
+ }
612
+
613
+ participates := map[string]bool{}
614
+ uncertain := map[string]bool{}
615
+ outOfScope := map[string][]string{}
616
+ outOfScopeSelections := map[string]symbolSet{}
617
+ // An exclusion outside its claim's declared carriers is a placement
618
+ // finding, not a host-kind one, so it carries its own obligations and the
619
+ // carrier globs its message must name.
620
+ outsideCarrier := map[string][]string{}
621
+ outsideCarrierGlobs := map[string]string{}
622
+ for _, state := range states {
623
+ if len(state.Paths) == 0 {
624
+ continue
625
+ }
626
+ if !state.Healthy {
627
+ for _, declaration := range state.Declarations {
628
+ uncertain[declaration.ID] = true
629
+ }
630
+ }
631
+ for _, reference := range state.References {
632
+ if !reference.Healthy {
633
+ for _, declaration := range state.Declarations {
634
+ uncertain[declaration.ID] = true
635
+ }
636
+ }
637
+ single := reference.Spec.Policy.SingleEvidencePerSymbol
638
+ unique := reference.Spec.Policy.UniqueEvidence
639
+ // An empty population owes nothing and normally ends the reference
640
+ // here. A healthy empty one still judges hosts under
641
+ // singleEvidencePerSymbol, because every selected host then truthfully
642
+ // cites zero units rather than the one it owes.
643
+ if len(reference.Units) == 0 &&
644
+ (!single || !state.Healthy || !reference.Healthy || len(reference.Paths) == 0) {
645
+ continue
646
+ }
647
+ acknowledged := map[string]bool{}
648
+ evidenceByUnit := map[string]*evidenceDeclaration{}
649
+ exclusionByUnit := map[string]*evidenceDeclaration{}
650
+ evidenceByHostAndScope := map[string]map[string]*evidenceDeclaration{}
651
+ selectedHosts := map[string]*evidenceUnit{}
652
+ evidenceUnitsByHost := map[string]map[string]bool{}
653
+ if single || unique {
654
+ for _, host := range state.Hosts {
655
+ selectedHosts[host.ID] = host
656
+ if single {
657
+ evidenceUnitsByHost[host.ID] = map[string]bool{}
658
+ }
659
+ }
660
+ }
661
+ evidenceHostsByUnit := map[string]map[string]bool{}
662
+ if unique {
663
+ for _, unit := range reference.Units {
664
+ evidenceHostsByUnit[unit.ID] = map[string]bool{}
665
+ }
666
+ }
667
+ scopesByID := map[string]*evidenceUnit{}
668
+ for _, scope := range reference.Scopes {
669
+ scopesByID[scope.ID] = scope
670
+ }
671
+ for _, declaration := range state.Declarations {
672
+ scopeID := resolved[declaration.ID]
673
+ covered := reference.UnitsByScope[scopeID]
674
+ if len(covered) == 0 {
675
+ continue
676
+ }
677
+ if declaration.Tag == tagExclude && state.OutsideCarrier[declaration.ID] {
678
+ outsideCarrier[declaration.ID] = appendUniqueString(
679
+ outsideCarrier[declaration.ID],
680
+ claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
681
+ )
682
+ outsideCarrierGlobs[declaration.ID] = describePopulation(state.Spec.Base, state.Spec.ExclusionCarriers)
683
+ continue
684
+ }
685
+ if !declarationEligibleForClaim(declaration, state.Spec) {
686
+ outOfScope[declaration.ID] = appendUniqueString(
687
+ outOfScope[declaration.ID],
688
+ claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
689
+ )
690
+ if outOfScopeSelections[declaration.ID] == nil {
691
+ outOfScopeSelections[declaration.ID] = symbolSet{}
692
+ }
693
+ for symbol := range state.Spec.Symbols {
694
+ outOfScopeSelections[declaration.ID][symbol] = true
695
+ }
696
+ continue
697
+ }
698
+ // Physical file ownership, resolved reference scope, and host
699
+ // eligibility decide which overlapping claims this declaration
700
+ // belongs to. A declaration may participate in several eligible
701
+ // obligations, but an ineligible overlap must not reject one
702
+ // already owned elsewhere.
703
+ participates[declaration.ID] = true
704
+ if !state.Healthy || !reference.Healthy {
705
+ continue
706
+ }
707
+ if declaration.Tag == tagExclude && reference.Spec.Policy.NoExclude {
708
+ problems = append(
709
+ problems,
710
+ "Forbidden @evidenceExclude for '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": noEvidenceExclude requires positive @evidence for this reference. Remove the exclusion and cite the target from a selected "+string(state.Spec.Type)+" host.",
711
+ )
712
+ continue
713
+ }
714
+ if declaration.Tag == tagEvidence && declaration.HostID != "" {
715
+ byScope := evidenceByHostAndScope[declaration.HostID]
716
+ if byScope == nil {
717
+ byScope = map[string]*evidenceDeclaration{}
718
+ evidenceByHostAndScope[declaration.HostID] = byScope
719
+ }
720
+ if first := byScope[scopeID]; first != nil {
721
+ problems = append(
722
+ problems,
723
+ "Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
724
+ )
725
+ } else {
726
+ byScope[scopeID] = declaration
727
+ }
728
+ }
729
+ if declaration.Tag == tagEvidence && (single || unique) {
730
+ for _, hostID := range declaration.SemanticHostIDs {
731
+ if selectedHosts[hostID] == nil {
732
+ continue
733
+ }
734
+ for _, unit := range covered {
735
+ if single {
736
+ evidenceUnitsByHost[hostID][unit.ID] = true
737
+ }
738
+ if !unique {
739
+ continue
740
+ }
741
+ // A covered unit is a selected unit of this reference,
742
+ // so the counter usually exists. Allocating on demand
743
+ // keeps a future scope map that reaches wider from
744
+ // turning a count into a nil-map panic in the compiler.
745
+ hosts := evidenceHostsByUnit[unit.ID]
746
+ if hosts == nil {
747
+ hosts = map[string]bool{}
748
+ evidenceHostsByUnit[unit.ID] = hosts
749
+ }
750
+ hosts[hostID] = true
751
+ }
752
+ }
753
+ }
754
+ var conflictingUnit *evidenceUnit
755
+ var conflictingDeclaration *evidenceDeclaration
756
+ var duplicateExclusionUnit *evidenceUnit
757
+ var firstExclusion *evidenceDeclaration
758
+ for _, unit := range covered {
759
+ acknowledged[unit.ID] = true
760
+ if declaration.Tag == tagEvidence {
761
+ if first := exclusionByUnit[unit.ID]; first != nil && conflictingUnit == nil {
762
+ conflictingUnit = unit
763
+ conflictingDeclaration = first
764
+ }
765
+ if evidenceByUnit[unit.ID] == nil {
766
+ evidenceByUnit[unit.ID] = declaration
767
+ }
768
+ continue
769
+ }
770
+ if first := evidenceByUnit[unit.ID]; first != nil && conflictingUnit == nil {
771
+ conflictingUnit = unit
772
+ conflictingDeclaration = first
773
+ }
774
+ if first := exclusionByUnit[unit.ID]; first != nil {
775
+ if duplicateExclusionUnit == nil {
776
+ duplicateExclusionUnit = unit
777
+ firstExclusion = first
778
+ }
779
+ continue
780
+ }
781
+ exclusionByUnit[unit.ID] = declaration
782
+ }
783
+ if conflictingUnit != nil {
784
+ evidence := declaration
785
+ exclusion := conflictingDeclaration
786
+ if declaration.Tag == tagExclude {
787
+ evidence = conflictingDeclaration
788
+ exclusion = declaration
789
+ }
790
+ problems = append(
791
+ problems,
792
+ "Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+".",
793
+ )
794
+ }
795
+ if duplicateExclusionUnit != nil {
796
+ problems = append(
797
+ problems,
798
+ "Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
799
+ )
800
+ }
801
+ }
802
+ if !state.Healthy || !reference.Healthy || len(reference.Paths) == 0 {
803
+ continue
804
+ }
805
+ if single {
806
+ for _, host := range state.Hosts {
807
+ count := len(evidenceUnitsByHost[host.ID])
808
+ if count == 1 {
809
+ continue
810
+ }
811
+ problems = append(
812
+ problems,
813
+ "Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" cites "+decimal(count)+" distinct selected evidence unit(s); singleEvidencePerSymbol requires exactly 1. Keep positive @evidence citations on this semantic host to exactly one distinct unit.",
814
+ )
815
+ }
816
+ }
817
+ for _, unit := range reference.Units {
818
+ if !acknowledged[unit.ID] {
819
+ repair := "Use @evidence on a selected " + string(state.Spec.Type) + " host or @evidenceExclude on an eligible carrier."
820
+ if reference.Spec.Policy.NoExclude {
821
+ repair = "Use @evidence on a selected " + string(state.Spec.Type) + " host; this reference forbids @evidenceExclude."
822
+ }
823
+ problems = append(
824
+ problems,
825
+ "Missing acknowledgement for '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+". "+repair,
826
+ )
827
+ }
828
+ hostCount := len(evidenceHostsByUnit[unit.ID])
829
+ if !unique || hostCount <= 1 {
830
+ continue
831
+ }
832
+ problems = append(
833
+ problems,
834
+ "Evidence unit '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" has "+decimal(hostCount)+" distinct positive evidence host(s); uniqueEvidence allows at most 1. Keep the one selected "+string(state.Spec.Type)+" host that owns this unit and remove the other citation(s).",
835
+ )
836
+ }
837
+ }
838
+ }
839
+ for _, id := range declarationIDs {
840
+ if resolved[id] == "" || participates[id] {
841
+ continue
842
+ }
843
+ declaration := declarations[id]
844
+ context := declarationObligationContext(owners[id])
845
+ if obligations := outsideCarrier[id]; len(obligations) != 0 {
846
+ problems = append(
847
+ problems,
848
+ "Misplaced @evidenceExclude at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': evidenceExcludeCarriers confines this claim's exclusions to "+outsideCarrierGlobs[id]+". Move the tag there, or delete it and implement the target this claim owes.",
849
+ )
850
+ continue
851
+ }
852
+ if obligations := outOfScope[id]; len(obligations) != 0 {
853
+ host := declaration.Hosts.names()
854
+ if len(declaration.Hosts) == 0 {
855
+ host = "unsupported or non-exported declaration"
856
+ }
857
+ if declaration.Tag == tagExclude {
858
+ problems = append(
859
+ problems,
860
+ "Out-of-scope @evidenceExclude carrier at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': '"+host+"' is not an eligible exclusion carrier in these matching claim files. Move the exclusion to a supported public export or selected declaration host, or use a top-level unattached Prisma documentation comment.",
861
+ )
862
+ continue
863
+ }
864
+ problems = append(
865
+ problems,
866
+ "Out-of-scope @"+string(declaration.Tag)+" host at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': host kind '"+host+"' is not selected ("+outOfScopeSelections[id].names()+") by any of these claim obligations. Move the declaration to a selected host, or widen only the claim symbol selector that genuinely owns this target.",
867
+ )
868
+ continue
869
+ }
870
+ if uncertain[id] {
871
+ // A failed loader makes non-participation unknowable. Its direct
872
+ // diagnostic is the repair path; adding a ghost finding here would
873
+ // derive a second claim from an incomplete graph.
874
+ continue
875
+ }
876
+ problems = append(
877
+ problems,
878
+ "Non-participating @"+string(declaration.Tag)+" target '"+displayTarget(declaration.Target)+"' at "+declaration.location()+" for "+context+": the target resolves, but none of this declaration's configured references selects it. Correct the target or reference, or move the tag to an eligible host or exclusion carrier in the claim that owes it; a resolving tag must discharge at least one obligation.",
879
+ )
880
+ }
881
+ return problems
882
+ }
883
+
884
+ // declarationEligibleForClaim keeps ownership evidence on the selected host
885
+ // while allowing an intentional exclusion to live on a claim-file carrier.
886
+ func declarationEligibleForClaim(
887
+ declaration *evidenceDeclaration,
888
+ claim claimSpec,
889
+ ) bool {
890
+ if claim.Symbols.intersects(declaration.Hosts) {
891
+ return true
892
+ }
893
+ return declaration.Tag == tagExclude && declaration.ExclusionCarrier
894
+ }
895
+
896
+ // hiddenTargetProblem names the documentation tag that withdrew a cited
897
+ // declaration.
898
+ //
899
+ // The repair is the author's either way, and which repair is right depends on
900
+ // which of the two statements is wrong — the tag or the citation — so the
901
+ // message states both rather than choosing.
902
+ func hiddenTargetProblem(
903
+ declaration *evidenceDeclaration,
904
+ hidden *evidenceUnit,
905
+ context string,
906
+ ) string {
907
+ return "Hidden evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": " + hidden.Readable + " at " + hidden.location() + " carries '" + hidden.Hidden + "' in its documentation comment, which withdraws it from the evidence population along with everything nested inside it. Remove the tag if the declaration is public contract, or drop this citation if it is not."
908
+ }
909
+
910
+ func declarationResolutionUncertain(owners []claimState) bool {
911
+ for _, owner := range owners {
912
+ for _, reference := range owner.References {
913
+ if !reference.Healthy {
914
+ return true
915
+ }
916
+ }
917
+ }
918
+ return false
919
+ }
920
+
921
+ // materializeEntryReference builds a population by walking an entry module's
922
+ // export graph rather than by matching paths.
923
+ //
924
+ // The entry is what a consumer can actually import, so the population is the
925
+ // public contract instead of whatever files a glob happened to sweep in. It is
926
+ // also the only selection that can reach a package symbol nothing imports,
927
+ // because such a symbol is absent from the Program by definition.
928
+ func materializeEntryReference(
929
+ claim claimSpec,
930
+ reference referenceSpec,
931
+ loader *typeScriptLoader,
932
+ ) (referenceState, []string) {
933
+ state := referenceState{
934
+ Spec: reference,
935
+ UnitsByScope: map[string][]*evidenceUnit{},
936
+ Healthy: true,
937
+ }
938
+ entry, problem := resolveReferenceEntry(claim, reference, loader)
939
+ if problem != "" {
940
+ return state, []string{problem}
941
+ }
942
+ state.Paths = []string{entry}
943
+ population := materializeEntryUnits(loader, []string{entry}, reference.Symbols)
944
+ state.Units = population.Units
945
+ state.Hidden = population.Hidden
946
+ state.Published = population.Published
947
+ if failure := loader.failure(entry); failure != "" {
948
+ state.Healthy = false
949
+ return state, []string{
950
+ claimLabel(claim) + " " + referenceLabel(reference) + " could not read TypeScript entry '" + entry + "': " + failure + ". Fix filesystem access or the package installation; coverage cannot be evaluated from a partial entry graph.",
951
+ }
952
+ }
953
+ if len(state.Units) == 0 {
954
+ return state, []string{
955
+ claimLabel(claim) + " " + referenceLabel(reference) + " reached no selected evidence units (" + reference.Symbols.names() + ") from entry '" + entry + "'. Select symbol kinds the entry exposes, or point the entry at the module that declares them.",
956
+ }
957
+ }
958
+ applyTraversedScopes(&state, population.Reached)
959
+ return state, nil
960
+ }
961
+
962
+ // applyTraversedScopes gives a traversed population its containment closure.
963
+ //
964
+ // Containment follows the declaration hierarchy rather than the address text. A
965
+ // type and a callable may share one public name, so treating a common address
966
+ // prefix as ownership would make an unrelated same-name declaration an ancestor
967
+ // and turn every citation of that name ambiguous. Every reached declaration
968
+ // takes part, which keeps an unselected ancestor addressable as the aggregate
969
+ // scope of its selected descendants.
970
+ func applyTraversedScopes(state *referenceState, reached []*evidenceUnit) {
971
+ sortUnits(state.Units)
972
+ available := map[string]*evidenceUnit{}
973
+ for _, unit := range reached {
974
+ available[unit.ID] = unit
975
+ }
976
+ scopesByID := map[string]*evidenceUnit{}
977
+ for _, unit := range state.Units {
978
+ for scope := unit; scope != nil; scope = available[scope.ParentID] {
979
+ state.UnitsByScope[scope.ID] = append(state.UnitsByScope[scope.ID], unit)
980
+ if scopesByID[scope.ID] == nil {
981
+ scopesByID[scope.ID] = scope
982
+ state.Scopes = append(state.Scopes, scope)
983
+ }
984
+ if scope.ParentID == "" {
985
+ break
986
+ }
987
+ }
988
+ }
989
+ sortUnits(state.Scopes)
990
+ }
991
+
992
+ // materializeLocalTypeScriptReference selects modules of the active project
993
+ // with globs and takes the population from what those modules publish.
994
+ //
995
+ // A glob names modules, while an obligation is owed by symbols. So a matched
996
+ // module contributes its own exports and everything it re-exports: a barrel is
997
+ // selected for the surface it presents, and pulling that surface apart into
998
+ // whichever files happen to declare it would make the same population depend on
999
+ // how the sources are laid out.
1000
+ func materializeLocalTypeScriptReference(
1001
+ claim claimSpec,
1002
+ reference referenceSpec,
1003
+ inventories map[string]*artifactInventory,
1004
+ loader *typeScriptLoader,
1005
+ ) (referenceState, []string) {
1006
+ paths := matchingReferencePaths(inventories, reference)
1007
+ state := referenceState{
1008
+ Spec: reference,
1009
+ Paths: paths,
1010
+ UnitsByScope: map[string][]*evidenceUnit{},
1011
+ Healthy: populationIsHealthy(inventories, reference.Base, paths),
1012
+ }
1013
+ if len(paths) == 0 {
1014
+ if !state.Healthy {
1015
+ return state, nil
1016
+ }
1017
+ return state, []string{
1018
+ claimLabel(claim) + " " + referenceLabel(reference) + " matched no typescript files for " + describePopulation(reference.Base, reference.Files) + ". Fix the reference globs or the root they resolve against; this obligation cannot materialize evidence units without files.",
1019
+ }
1020
+ }
1021
+ selectedInventoryProblem := false
1022
+ for _, path := range paths {
1023
+ for _, inventoryProblem := range inventories[path].Problems {
1024
+ if inventoryProblem.Symbol == "*" ||
1025
+ reference.Symbols.contains(inventoryProblem.Symbol) {
1026
+ selectedInventoryProblem = true
1027
+ }
1028
+ }
1029
+ }
1030
+ population := materializeEntryUnits(loader, paths, reference.Symbols)
1031
+ state.Units = population.Units
1032
+ state.Hidden = population.Hidden
1033
+ state.Published = population.Published
1034
+ applyTraversedScopes(&state, population.Reached)
1035
+ if !state.Healthy {
1036
+ return state, nil
1037
+ }
1038
+ if len(state.Units) == 0 && !selectedInventoryProblem {
1039
+ return state, []string{
1040
+ claimLabel(claim) + " " + referenceLabel(reference) + " matched " + decimal(len(paths)) + " file(s) but materialized no selected evidence units (" + reference.Symbols.names() + "). Select symbol kinds present in those files or correct the reference globs.",
1041
+ }
1042
+ }
1043
+ return state, nil
1044
+ }
1045
+
1046
+ // materializePackageGlobReference narrows an installed package with globs that
1047
+ // resolve against the package root.
1048
+ //
1049
+ // Narrowing a large SDK to one area is what makes the obligation adoptable at
1050
+ // all. The globs are written as a consumer thinks of the package — `lib/api/**`
1051
+ // — rather than carrying the `node_modules` prefix, which is an installation
1052
+ // detail rather than part of the package's shape.
1053
+ //
1054
+ // A matched module is an entry, not a leaf: its own exports and everything it
1055
+ // re-exports both belong to the population, because a glob selects modules
1056
+ // while an obligation is owed by the symbols those modules publish.
1057
+ func materializePackageGlobReference(
1058
+ claim claimSpec,
1059
+ reference referenceSpec,
1060
+ loader *typeScriptLoader,
1061
+ ) (referenceState, []string) {
1062
+ state := referenceState{
1063
+ Spec: reference,
1064
+ UnitsByScope: map[string][]*evidenceUnit{},
1065
+ Healthy: true,
1066
+ }
1067
+ base := referenceBase(loader, reference)
1068
+ candidates, walkProblem := loader.walk(base)
1069
+ if walkProblem != "" {
1070
+ state.Healthy = false
1071
+ return state, []string{
1072
+ claimLabel(claim) + " " + referenceLabel(reference) + " could not inspect TypeScript package '" + reference.Package + "': " + walkProblem + ". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
1073
+ }
1074
+ }
1075
+ problems := []string{}
1076
+ for _, candidate := range candidates {
1077
+ relative := strings.TrimPrefix(strings.TrimPrefix(candidate, base), "/")
1078
+ if !reference.Files.matches(relative) {
1079
+ continue
1080
+ }
1081
+ if loader.inventory(candidate) == nil {
1082
+ state.Healthy = false
1083
+ problems = append(
1084
+ problems,
1085
+ claimLabel(claim)+" "+referenceLabel(reference)+" could not read TypeScript source '"+candidate+"': "+loader.failure(candidate)+". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
1086
+ )
1087
+ continue
1088
+ }
1089
+ state.Paths = append(state.Paths, candidate)
1090
+ }
1091
+ if len(state.Paths) == 0 {
1092
+ if !state.Healthy {
1093
+ return state, problems
1094
+ }
1095
+ return state, []string{
1096
+ claimLabel(claim) + " " + referenceLabel(reference) + " matched no files inside package '" + reference.Package + "' for " + describePatterns(reference.Files) + ". The globs resolve against the package root, not the project root. Check that the installed package actually contains those paths before changing the reference: an empty population demands nothing, so a claim that reaches this state reports full coverage while checking nothing.",
1097
+ }
1098
+ }
1099
+ // The glob decides membership, the entry decides addresses. A matched module
1100
+ // is still traversed as an entry, because a matched barrel owes what it
1101
+ // publishes rather than only what its own file declares — but that traversal
1102
+ // is used for nothing except which units are in. Their addresses come from
1103
+ // the package entry, which is the only module a consumer has a specifier
1104
+ // for, and it is under that specifier that an inline link is resolved.
1105
+ // Publishing a narrowed unit under the matched module instead is what made
1106
+ // `functional.health.get` collapse to `get` and left it with no spelling
1107
+ // that resolves.
1108
+ membership := materializeEntryUnits(loader, state.Paths, reference.Symbols)
1109
+ population := membership
1110
+ if entry := loader.packageEntryModule(reference.Package); entry != "" {
1111
+ population = narrowTraversedPopulation(
1112
+ materializeEntryUnits(loader, []string{entry}, reference.Symbols),
1113
+ membership,
1114
+ )
1115
+ }
1116
+ state.Units = population.Units
1117
+ state.Hidden = population.Hidden
1118
+ state.Published = population.Published
1119
+ applyTraversedScopes(&state, population.Reached)
1120
+ if !state.Healthy {
1121
+ return state, problems
1122
+ }
1123
+ if len(state.Units) == 0 {
1124
+ return state, []string{
1125
+ claimLabel(claim) + " " + referenceLabel(reference) + " matched " + decimal(len(state.Paths)) + " file(s) inside package '" + reference.Package + "' but materialized no selected evidence units (" + reference.Symbols.names() + ") reachable from the package entry. Select symbol kinds present in those files, correct the globs, or narrow to modules the entry publishes.",
1126
+ }
1127
+ }
1128
+ return state, nil
1129
+ }
1130
+
1131
+ func resolveReferenceEntry(
1132
+ claim claimSpec,
1133
+ reference referenceSpec,
1134
+ loader *typeScriptLoader,
1135
+ ) (string, string) {
1136
+ entry := loader.packageEntryModule(reference.Package)
1137
+ if entry == "" {
1138
+ return "", claimLabel(claim) + " " + referenceLabel(reference) + " could not resolve the declaration entry of package '" + reference.Package + "'. Install it, or select its declarations with 'files'; the entry is read from the 'types' condition of 'exports', then 'typesVersions', then 'types'."
1139
+ }
1140
+ return entry, ""
1141
+ }
1142
+
1143
+ // scopedTargetKey identifies one address inside one module.
1144
+ //
1145
+ // A struct key rather than a joined string: the map is built once per unit per
1146
+ // reference and read once per citation, and concatenating two paths to hash
1147
+ // them allocates a string whose only purpose is to be thrown away.
1148
+ type scopedTargetKey struct {
1149
+ path string
1150
+ target string
1151
+ }
1152
+
1153
+ // looksLikeTypeScriptTarget reports whether an unbraced target names a symbol.
1154
+ //
1155
+ // The migration diagnostic has to be told apart from an ordinary typo, and the
1156
+ // signal is that the same spelling still materializes as a TypeScript unit
1157
+ // somewhere in the configured graph. A Markdown path or a Swagger operation
1158
+ // never does, so neither is mistaken for a symbol that lost its braces.
1159
+ func looksLikeTypeScriptTarget(
1160
+ target string,
1161
+ targets map[string]map[string]*evidenceUnit,
1162
+ markdownTargets map[string]map[string]*evidenceUnit,
1163
+ ) bool {
1164
+ if len(markdownTargets[target]) != 0 {
1165
+ return false
1166
+ }
1167
+ for _, unit := range targets[target] {
1168
+ if unit.Type == artifactTypeScript {
1169
+ return true
1170
+ }
1171
+ }
1172
+ return false
1173
+ }
1174
+
1175
+ // resolveInlineLinkDeclaration resolves a braced target through the citing
1176
+ // module's imports, the way TypeScript resolves the same name.
1177
+ //
1178
+ // Every failure gets its own diagnostic. A single "unresolved" would leave the
1179
+ // author guessing which of four independent things went wrong, and three of
1180
+ // them are repaired in completely different places.
1181
+ func resolveInlineLinkDeclaration(
1182
+ declaration *evidenceDeclaration,
1183
+ loader *typeScriptLoader,
1184
+ scopedTargets map[scopedTargetKey]map[string]*evidenceUnit,
1185
+ scopedHidden map[scopedTargetKey]*evidenceUnit,
1186
+ context string,
1187
+ ) (string, string) {
1188
+ target := inlineLinkTarget(declaration.Target)
1189
+ if declaration.Type != artifactTypeScript {
1190
+ return "", "Inline link target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": only a TypeScript declaration can cite through an inline link, because resolution runs through the citing module's imports and a " + string(declaration.Type) + " comment has none. Use a path-addressed target selected by one of the named references; a TypeScript symbol must instead be cited from a TypeScript claim."
1191
+ }
1192
+ inventory := loader.inventory(declaration.Path)
1193
+ if inventory == nil {
1194
+ return "", "Inline link target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": the citing file is not part of the TypeScript program, so it has no import scope to resolve against. Include the file in the project or move the citation to a configured TypeScript claim file."
1195
+ }
1196
+ segments := strings.Split(target, ".")
1197
+ binding, imported := inventory.Imports[segments[0]]
1198
+ if !imported {
1199
+ return "", "Unimported evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + segments[0] + "' is not imported by this module, so the citation names a symbol this file does not reference. Import it; 'import type' is enough and is erased at emit."
1200
+ }
1201
+ // Resolution goes through the same loader the population uses, so a citation
1202
+ // can reach a package entry that never entered the Program — which is the
1203
+ // only way an import of an installed SDK resolves at all.
1204
+ resolvedPath := loader.resolve(declaration.Path, binding.Specifier)
1205
+ if resolvedPath == "" {
1206
+ return "", "Unresolved module '" + binding.Specifier + "' for evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": the specifier resolves to no TypeScript file reachable from this project. Correct the import, or make the named reference reach the module."
1207
+ }
1208
+ remaining := segments[1:]
1209
+ if !binding.Namespace {
1210
+ remaining = append([]string{binding.Imported}, remaining...)
1211
+ }
1212
+ if len(remaining) == 0 {
1213
+ return "", "Incomplete evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": a namespace import names a module rather than a unit. Name a symbol inside '" + binding.Specifier + "' that the named reference selects."
1214
+ }
1215
+ name := strings.Join(remaining, ".")
1216
+ candidates := scopedTargets[scopedTargetKey{path: resolvedPath, target: name}]
1217
+ switch len(candidates) {
1218
+ case 0:
1219
+ if hidden := scopedHidden[scopedTargetKey{
1220
+ path: resolvedPath,
1221
+ target: name,
1222
+ }]; hidden != nil {
1223
+ return "", hiddenTargetProblem(declaration, hidden, context)
1224
+ }
1225
+ return "", "Unreachable evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + resolvedPath + "' declares no selected unit named '" + name + "'. Correct the target, or widen the named reference's files and symbol selection so that unit is configured evidence."
1226
+ case 1:
1227
+ for _, unit := range candidates {
1228
+ return unit.ID, ""
1229
+ }
1230
+ return "", ""
1231
+ default:
1232
+ // One module may spell one name in two declaration spaces — a type and a
1233
+ // callable, say. Resolution landed in the right file and still cannot say
1234
+ // which unit was meant, and picking one silently would acknowledge an
1235
+ // obligation the author never cited.
1236
+ descriptions := make([]string, 0, len(candidates))
1237
+ for _, unit := range candidates {
1238
+ descriptions = append(descriptions, unit.Readable+" at "+unit.location())
1239
+ }
1240
+ sort.Strings(descriptions)
1241
+ return "", "Ambiguous evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": '" + resolvedPath + "' declares " + strings.Join(descriptions, "; ") + " under that name. Narrow the named reference's symbol selection so the target has exactly one meaning."
1242
+ }
1243
+ }
1244
+
1245
+ func declarationCandidates(
1246
+ target string,
1247
+ targets map[string]map[string]*evidenceUnit,
1248
+ markdownTargets map[string]map[string]*evidenceUnit,
1249
+ ) map[string]*evidenceUnit {
1250
+ candidates := map[string]*evidenceUnit{}
1251
+ for id, unit := range targets[target] {
1252
+ candidates[id] = unit
1253
+ }
1254
+ normalized := normalizeMarkdownTarget(target)
1255
+ if normalized != target {
1256
+ for id, unit := range markdownTargets[normalized] {
1257
+ candidates[id] = unit
1258
+ }
1259
+ }
1260
+ return candidates
1261
+ }
1262
+
1263
+ // splitCodeCandidates separates the units a non-TypeScript claim may address
1264
+ // from the ones only an inline link can reach.
1265
+ //
1266
+ // Both halves are returned because the caller has to tell "this target names
1267
+ // something else entirely" from "this target names a symbol, and that is the
1268
+ // problem". Reporting the second as an unresolved target would be true and
1269
+ // useless: the unit exists, and nothing in the message would say why naming it
1270
+ // here cannot work.
1271
+ func splitCodeCandidates(
1272
+ candidates map[string]*evidenceUnit,
1273
+ ) (map[string]*evidenceUnit, map[string]*evidenceUnit) {
1274
+ addressable := map[string]*evidenceUnit{}
1275
+ code := map[string]*evidenceUnit{}
1276
+ for id, unit := range candidates {
1277
+ if unit.Type == artifactTypeScript {
1278
+ code[id] = unit
1279
+ continue
1280
+ }
1281
+ addressable[id] = unit
1282
+ }
1283
+ return addressable, code
1284
+ }
1285
+
1286
+ func inventoriesOf(
1287
+ kind artifactKind,
1288
+ markdown map[string]*artifactInventory,
1289
+ prisma map[string]*artifactInventory,
1290
+ swagger map[string]*artifactInventory,
1291
+ typescript map[string]*artifactInventory,
1292
+ ) map[string]*artifactInventory {
1293
+ switch kind {
1294
+ case artifactMarkdown:
1295
+ return markdown
1296
+ case artifactPrisma:
1297
+ return prisma
1298
+ case artifactSwagger:
1299
+ return swagger
1300
+ case artifactTypeScript:
1301
+ return typescript
1302
+ default:
1303
+ return map[string]*artifactInventory{}
1304
+ }
1305
+ }
1306
+
1307
+ func matchingReferencePaths(
1308
+ inventories map[string]*artifactInventory,
1309
+ reference referenceSpec,
1310
+ ) []string {
1311
+ if reference.Type != artifactSwagger {
1312
+ return matchingInventoryPaths(inventories, reference.Base, reference.Files)
1313
+ }
1314
+ if inventories[reference.Source] == nil {
1315
+ return nil
1316
+ }
1317
+ return []string{reference.Source}
1318
+ }
1319
+
1320
+ // matchingInventoryPaths selects the files one population owns.
1321
+ //
1322
+ // Matching runs against the base-relative path rather than against the key,
1323
+ // which is what keeps a pattern written as `requirements/**/*.md` meaning the
1324
+ // same thing whether its root is the project or a directory two levels above
1325
+ // it. An address composed for another base is skipped outright, so a file
1326
+ // loaded for one root is never offered to a population that cannot address it.
1327
+ func matchingInventoryPaths(
1328
+ inventories map[string]*artifactInventory,
1329
+ base populationBase,
1330
+ globs globSet,
1331
+ ) []string {
1332
+ paths := []string{}
1333
+ for key := range inventories {
1334
+ relative, owned := base.relativeOf(key)
1335
+ if !owned || !globs.matches(relative) {
1336
+ continue
1337
+ }
1338
+ paths = append(paths, key)
1339
+ }
1340
+ sort.Strings(paths)
1341
+ return paths
1342
+ }
1343
+
1344
+ func sortUnits(units []*evidenceUnit) {
1345
+ sort.Slice(units, func(left int, right int) bool {
1346
+ if units[left].Target != units[right].Target {
1347
+ return units[left].Target < units[right].Target
1348
+ }
1349
+ return units[left].ID < units[right].ID
1350
+ })
1351
+ }
1352
+
1353
+ func claimLabel(claim claimSpec) string {
1354
+ label := "Claim " + decimal(claim.Index+1)
1355
+ if claim.Name != "" {
1356
+ label += " ('" + claim.Name + "')"
1357
+ }
1358
+ return label
1359
+ }
1360
+
1361
+ func referenceLabel(reference referenceSpec) string {
1362
+ if reference.Type == artifactSwagger {
1363
+ return "reference " + decimal(reference.Index+1) + " (swagger operations)"
1364
+ }
1365
+ return "reference " + decimal(reference.Index+1) + " (" + string(reference.Type) + ", symbols: " + reference.Symbols.names() + ")"
1366
+ }
1367
+
1368
+ func declarationObligationContext(owners []claimState) string {
1369
+ groups := make([]string, 0, len(owners))
1370
+ for _, owner := range owners {
1371
+ references := make([]string, 0, len(owner.Spec.References))
1372
+ for _, reference := range owner.Spec.References {
1373
+ references = append(references, referenceLabel(reference))
1374
+ }
1375
+ if len(references) == 0 {
1376
+ groups = append(groups, claimLabel(owner.Spec))
1377
+ continue
1378
+ }
1379
+ groups = append(
1380
+ groups,
1381
+ claimLabel(owner.Spec)+" across "+strings.Join(references, ", "),
1382
+ )
1383
+ }
1384
+ if len(groups) == 0 {
1385
+ return "no matched claim"
1386
+ }
1387
+ return strings.Join(groups, "; ")
1388
+ }
1389
+
1390
+ func appendUniqueString(values []string, candidate string) []string {
1391
+ for _, value := range values {
1392
+ if value == candidate {
1393
+ return values
1394
+ }
1395
+ }
1396
+ return append(values, candidate)
1397
+ }
1398
+
1399
+ func reportProblems(ctx *rule.ProjectContext, problems []string) {
1400
+ sort.Strings(problems)
1401
+ previous := ""
1402
+ for _, problem := range problems {
1403
+ if problem == "" || problem == previous {
1404
+ continue
1405
+ }
1406
+ ctx.Report(problem)
1407
+ previous = problem
1408
+ }
1409
+ }