@ttsc/evidence 0.29.0 → 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,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
package/native/graph.go CHANGED
@@ -378,6 +378,12 @@ func materializeClaimStates(
378
378
  continue
379
379
  }
380
380
  if reference.Type == artifactTypeScript {
381
+ if reference.Rooted {
382
+ rootedState, rootedProblems := materializeRootedTypeScriptReference(claim, reference, loader)
383
+ problems = problems.add(severity, rootedProblems...)
384
+ state.References = append(state.References, rootedState)
385
+ continue
386
+ }
381
387
  localState, localProblems := materializeLocalTypeScriptReference(
382
388
  claim,
383
389
  reference,
@@ -607,6 +613,7 @@ func evaluateEvidenceGraph(
607
613
  sort.Strings(declarationIDs)
608
614
 
609
615
  resolved := map[string]string{}
616
+ fileLinks := indexFileLinkClaims(states, loader)
610
617
  for _, id := range declarationIDs {
611
618
  severity := ownerSeverity(owners[id])
612
619
  declaration := declarations[id]
@@ -618,12 +625,23 @@ func evaluateEvidenceGraph(
618
625
  )
619
626
  continue
620
627
  }
628
+ if isFileLinkTarget(declaration.Target) {
629
+ unitID, problem := resolveFileLinkDeclaration(declaration, owners[id], loader, fileLinks, context)
630
+ if problem != "" {
631
+ problems = problems.add(severity, problem)
632
+ } else {
633
+ resolved[id] = unitID
634
+ }
635
+ continue
636
+ }
621
637
  if isInlineLinkTarget(declaration.Target) {
622
638
  unitID, problem := resolveInlineLinkDeclaration(
623
639
  declaration,
624
640
  loader,
625
641
  scopedTargets,
626
642
  scopedHidden,
643
+ fileLinks,
644
+ states,
627
645
  context,
628
646
  )
629
647
  if problem != "" {
@@ -655,7 +673,7 @@ func evaluateEvidenceGraph(
655
673
  if len(addressable) == 0 && len(code) != 0 {
656
674
  problems = problems.add(
657
675
  severity,
658
- "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.",
676
+ "Code evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": an unqualified symbol has no module identity in this artifact. In Markdown, write '@link <file.ts>#<Accessor> <reason>' using a path relative to this document. Otherwise move the citation to a TypeScript claim.",
659
677
  )
660
678
  continue
661
679
  }
@@ -935,12 +953,7 @@ func evaluateEvidenceGraph(
935
953
  // Withdrawn units are absent from Population by construction, and
936
954
  // the scope composite still needs their identities so a member
937
955
  // leaving the public surface moves the fingerprint.
938
- reviewScopes = newScopeIndex(
939
- append(
940
- append([]*evidenceUnit{}, reference.Population...),
941
- reference.Hidden...,
942
- ),
943
- )
956
+ reviewScopes = referenceReviewScopes(reference, loader)
944
957
  }
945
958
  if reviewLedgerForClaim == nil {
946
959
  reviewLedgerForClaim = newReviewLedger(state.Reviews)
@@ -1448,6 +1461,7 @@ func materializeEntryReference(
1448
1461
  state.Units = population.Units
1449
1462
  state.Hidden = population.Hidden
1450
1463
  state.Published = population.Published
1464
+ state.Code = population.Code
1451
1465
  if failure := loader.failure(entry); failure != "" {
1452
1466
  state.Healthy = false
1453
1467
  return state, []string{
@@ -1539,6 +1553,7 @@ func materializeLocalTypeScriptReference(
1539
1553
  state.Units = population.Units
1540
1554
  state.Hidden = population.Hidden
1541
1555
  state.Published = population.Published
1556
+ state.Code = population.Code
1542
1557
  applyTraversedScopes(&state, population.Reached)
1543
1558
  if !state.Healthy {
1544
1559
  return state, nil
@@ -1624,6 +1639,7 @@ func materializePackageGlobReference(
1624
1639
  state.Units = population.Units
1625
1640
  state.Hidden = population.Hidden
1626
1641
  state.Published = population.Published
1642
+ state.Code = population.Code
1627
1643
  applyTraversedScopes(&state, population.Reached)
1628
1644
  if !state.Healthy {
1629
1645
  return state, problems
@@ -1691,6 +1707,8 @@ func resolveInlineLinkDeclaration(
1691
1707
  loader *typeScriptLoader,
1692
1708
  scopedTargets map[scopedTargetKey]map[string]*evidenceUnit,
1693
1709
  scopedHidden map[scopedTargetKey]*evidenceUnit,
1710
+ fileLinks map[int]*fileLinkClaimIndex,
1711
+ states []claimState,
1694
1712
  context string,
1695
1713
  ) (string, string) {
1696
1714
  target := inlineLinkTarget(declaration.Target)
@@ -1721,7 +1739,19 @@ func resolveInlineLinkDeclaration(
1721
1739
  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."
1722
1740
  }
1723
1741
  name := strings.Join(remaining, ".")
1724
- candidates := scopedTargets[scopedTargetKey{path: resolvedPath, target: name}]
1742
+ identity, _ := loader.moduleIdentity(resolvedPath)
1743
+ candidates, hidden := queryFileLinkClaims(fileLinks, states, identity, remaining, true)
1744
+ for id, unit := range scopedTargets[scopedTargetKey{path: resolvedPath, target: name}] {
1745
+ candidates[id] = unit
1746
+ }
1747
+ if len(candidates) == 0 && len(hidden) != 0 {
1748
+ ids := make([]string, 0, len(hidden))
1749
+ for id := range hidden {
1750
+ ids = append(ids, id)
1751
+ }
1752
+ sort.Strings(ids)
1753
+ return "", hiddenTargetProblem(declaration, hidden[ids[0]], context)
1754
+ }
1725
1755
  switch len(candidates) {
1726
1756
  case 0:
1727
1757
  if hidden := scopedHidden[scopedTargetKey{
package/native/imports.go CHANGED
@@ -19,6 +19,7 @@ type importBinding struct {
19
19
  Specifier string
20
20
  Imported string
21
21
  Namespace bool
22
+ TypeOnly bool
22
23
  }
23
24
 
24
25
  // collectImportBindings indexes a file's imports by the local name each binds.
@@ -52,6 +53,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
52
53
  Local: name,
53
54
  Specifier: specifier,
54
55
  Imported: "default",
56
+ TypeOnly: clause.IsTypeOnly(),
55
57
  }
56
58
  }
57
59
  if clause.NamedBindings == nil {
@@ -64,6 +66,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
64
66
  Local: name,
65
67
  Specifier: specifier,
66
68
  Namespace: true,
69
+ TypeOnly: clause.IsTypeOnly(),
67
70
  }
68
71
  }
69
72
  case shimast.KindNamedImports:
@@ -91,6 +94,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
91
94
  Local: local,
92
95
  Specifier: specifier,
93
96
  Imported: imported,
97
+ TypeOnly: clause.IsTypeOnly() || specifierNode.IsTypeOnly,
94
98
  }
95
99
  }
96
100
  }
@@ -124,19 +128,28 @@ var typeScriptModuleExtensions = []string{
124
128
  // must spell its sibling as `./x.js`, so refusing to map it back would make the
125
129
  // correct import form unresolvable.
126
130
  func moduleCandidates(base string) []string {
127
- candidates := []string{base}
128
- stripped := base
129
- for _, emitted := range []string{".js", ".mjs", ".cjs"} {
130
- if strings.HasSuffix(base, emitted) {
131
- stripped = strings.TrimSuffix(base, emitted)
132
- break
131
+ if isTypeScriptPath(base) {
132
+ return []string{base}
133
+ }
134
+ substitutions := map[string][]string{
135
+ ".js": {".ts", ".tsx", ".d.ts"},
136
+ ".mjs": {".mts", ".d.mts"},
137
+ ".cjs": {".cts", ".d.cts"},
138
+ }
139
+ if extensions, emitted := substitutions[path.Ext(base)]; emitted {
140
+ stripped := strings.TrimSuffix(base, path.Ext(base))
141
+ candidates := make([]string, 0, len(extensions))
142
+ for _, extension := range extensions {
143
+ candidates = append(candidates, stripped+extension)
133
144
  }
145
+ return candidates
134
146
  }
147
+ candidates := []string{}
135
148
  for _, extension := range typeScriptModuleExtensions {
136
- candidates = append(candidates, stripped+extension)
149
+ candidates = append(candidates, base+extension)
137
150
  }
138
151
  for _, extension := range typeScriptModuleExtensions {
139
- candidates = append(candidates, path.Join(stripped, "index"+extension))
152
+ candidates = append(candidates, path.Join(base, "index"+extension))
140
153
  }
141
154
  return candidates
142
155
  }
package/native/inputs.go CHANGED
@@ -35,10 +35,8 @@ func (graphRule) ProjectInputs(ctx *rule.ProjectInputContext) []rule.ProjectInpu
35
35
  // graphProjectInputs collects the external dependency of every configured
36
36
  // obligation.
37
37
  //
38
- // TypeScript claims and references are absent by design. Their inventories are
39
- // materialized from `ctx.Sources`, which is the Program the host already
40
- // watches, so declaring them again would ask for a second watcher on a file
41
- // that already has one.
38
+ // Program sources are already watched. Rooted TypeScript references additionally
39
+ // declare the directory containing their disk-loaded export closure.
42
40
  func graphProjectInputs(config graphConfig) []rule.ProjectInput {
43
41
  config = enabledGraphConfig(config)
44
42
  inputs := []rule.ProjectInput{}
@@ -49,6 +47,17 @@ func graphProjectInputs(config graphConfig) []rule.ProjectInput {
49
47
  }
50
48
  for _, reference := range claim.References {
51
49
  switch reference.Type {
50
+ case artifactTypeScript:
51
+ if reference.Rooted {
52
+ // Export traversal can reach a module outside the entry globs, and
53
+ // this pre-Program protocol cannot discover that closure. The root
54
+ // is its explicit boundary, including future files and repairs.
55
+ pattern := "**"
56
+ if reference.Root != "" {
57
+ pattern = strings.TrimSuffix(reference.Root, "/") + "/**"
58
+ }
59
+ inputs = append(inputs, rule.ProjectInput{Kind: rule.ProjectInputGlob, Pattern: pattern})
60
+ }
52
61
  case artifactMarkdown, artifactPrisma:
53
62
  inputs = append(inputs, globInputs(reference.Root, reference.Files)...)
54
63
  case artifactSwagger: