@ttsc/evidence 0.30.4 → 0.31.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 (66) hide show
  1. package/README.md +14 -6
  2. package/lib/index.d.ts +2 -3
  3. package/lib/index.js +2 -3
  4. package/lib/internal/canonicalDigest.js +12 -6
  5. package/lib/internal/loadPrismaModels.js +52 -7
  6. package/lib/internal/loadSwaggerOperations.js +253 -59
  7. package/lib/internal/normalizeSwaggerDocument.js +320 -2
  8. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
  9. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
  10. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
  11. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
  12. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
  13. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
  14. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
  15. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
  16. package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
  17. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
  18. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
  19. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
  20. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
  21. package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
  22. package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
  23. package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
  24. package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
  25. package/lib/structures/index.d.ts +4 -0
  26. package/lib/structures/index.js +4 -0
  27. package/native/base.go +25 -23
  28. package/native/bridge_trace.go +348 -0
  29. package/native/comments.go +1 -1
  30. package/native/config.go +6 -3
  31. package/native/declaration.go +17 -43
  32. package/native/documentation_regions.go +214 -0
  33. package/native/documented.go +5 -4
  34. package/native/exports.go +1 -1
  35. package/native/fingerprint.go +2 -8
  36. package/native/glob.go +4 -1
  37. package/native/graph.go +59 -23
  38. package/native/graph_nodes.go +2 -1
  39. package/native/hints.go +22 -18
  40. package/native/imports.go +7 -6
  41. package/native/input_reader.go +83 -0
  42. package/native/inputs.go +13 -12
  43. package/native/loader.go +36 -24
  44. package/native/markdown.go +587 -177
  45. package/native/markdown_capture.go +223 -0
  46. package/native/model.go +2 -1
  47. package/native/prisma.go +110 -25
  48. package/native/prisma_cache.go +12 -8
  49. package/native/prisma_locator.go +70 -33
  50. package/native/review.go +8 -7
  51. package/native/rooted_typescript.go +3 -4
  52. package/native/swagger.go +123 -15
  53. package/native/swagger_cache.go +16 -12
  54. package/native/todo.go +12 -0
  55. package/native/typescript.go +129 -25
  56. package/package.json +9 -7
  57. package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
  58. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
  59. package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
  60. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
  61. package/lib/typings/index.d.ts +0 -5
  62. package/lib/typings/index.js +0 -21
  63. /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
  64. /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
  65. /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
  66. /package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.js +0 -0
@@ -29,7 +29,7 @@ type sourceSpan struct {
29
29
  end int
30
30
  }
31
31
 
32
- // forEachCommentToken visits every real comment in `file` in source order.
32
+ // forEachComment visits every real comment in `file` in source order.
33
33
  //
34
34
  // TypeScript's parser, rather than a context-free scanner, owns the lexical
35
35
  // goal for regular expressions, templates, and JSX. The parsed AST retains
package/native/config.go CHANGED
@@ -663,6 +663,9 @@ func normalizeSwaggerSource(value string) (string, string) {
663
663
  }
664
664
  parsed, err := url.Parse(value)
665
665
  if err != nil {
666
+ if displaySwaggerSource(value) != value || strings.Contains(value, "://") {
667
+ return "", "invalid Swagger source '" + displaySwaggerSource(value) + "': the URL could not be parsed."
668
+ }
666
669
  return "", "invalid Swagger source '" + value + "': " + causeText(err) + "."
667
670
  }
668
671
  if !drive && parsed.Scheme != "" {
@@ -670,15 +673,15 @@ func normalizeSwaggerSource(value string) (string, string) {
670
673
  return "", "unsupported URL scheme '" + parsed.Scheme + "'; only http: and https: are supported."
671
674
  }
672
675
  if parsed.Host == "" {
673
- return "", "Swagger URL '" + value + "' has no host."
676
+ return "", "Swagger URL '" + displaySwaggerSource(value) + "' has no host."
674
677
  }
675
678
  if parsed.Fragment != "" {
676
- return "", "Swagger URL '" + value + "' must not contain a fragment."
679
+ return "", "Swagger URL '" + displaySwaggerSource(value) + "' must not contain a fragment."
677
680
  }
678
681
  return value, ""
679
682
  }
680
683
  if !drive && strings.Contains(value, "://") {
681
- return "", "invalid Swagger source URL '" + value + "'."
684
+ return "", "invalid Swagger source URL '" + displaySwaggerSource(value) + "'."
682
685
  }
683
686
  // A trailing separator is read before cleaning, because `path.Clean` removes
684
687
  // it — after which `docs/` and `docs` are one string and the author who
@@ -31,18 +31,12 @@ var hiddenDeclarationTags = []string{"@internal", "@hidden", "@ignore"}
31
31
  // sentence silently delete an obligation. Text after the tag is a comment for
32
32
  // humans and is not read.
33
33
  func commentHidingTag(comment string) string {
34
- comment = strings.TrimSpace(comment)
35
- comment = strings.TrimPrefix(comment, "/**")
36
- comment = strings.TrimPrefix(comment, "/*")
37
- comment = strings.TrimSuffix(comment, "*/")
38
- fence := commentFence{}
39
- for _, rawLine := range strings.Split(comment, "\n") {
40
- line := strings.TrimSpace(rawLine)
41
- line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
42
- line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
43
- if fence.consume(line) {
34
+ body, _, _ := documentationCommentBody(comment)
35
+ for _, region := range documentationRegions(body, true) {
36
+ if region.fence {
44
37
  continue
45
38
  }
39
+ line := region.text
46
40
  for _, tag := range hiddenDeclarationTags {
47
41
  if line != tag && !strings.HasPrefix(line, tag+" ") {
48
42
  continue
@@ -65,21 +59,14 @@ func parseCommentDeclarations(
65
59
  comment string,
66
60
  tagBoundaries bool,
67
61
  ) []parsedDeclaration {
68
- trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
69
- leadingLines := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
70
- comment = trimmed
71
- jsdoc := strings.HasPrefix(comment, "/**") || tagBoundaries
72
- comment = strings.TrimPrefix(comment, "/**")
73
- comment = strings.TrimPrefix(comment, "/*")
74
- comment = strings.TrimSuffix(comment, "*/")
75
- lines := strings.Split(comment, "\n")
62
+ body, leadingLines, documented := documentationCommentBody(comment)
63
+ jsdoc := documented || tagBoundaries
76
64
  type pendingDeclaration struct {
77
65
  tag tagKind
78
66
  body []string
79
67
  lineOffset int
80
68
  }
81
69
  var pending *pendingDeclaration
82
- fence := commentFence{}
83
70
  parsed := []parsedDeclaration{}
84
71
  flush := func() {
85
72
  if pending == nil {
@@ -94,15 +81,12 @@ func parseCommentDeclarations(
94
81
  })
95
82
  pending = nil
96
83
  }
97
- for index, rawLine := range lines {
98
- line := strings.TrimSpace(rawLine)
99
- line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
100
- if fence.consume(line) {
101
- if pending != nil {
102
- pending.body = append(pending.body, line)
103
- }
84
+ for index, region := range documentationRegions(body, false) {
85
+ if region.fence {
86
+ flush()
104
87
  continue
105
88
  }
89
+ line := region.text
106
90
  tag, body, found := declarationLine(line)
107
91
  if found {
108
92
  flush()
@@ -329,15 +313,9 @@ func (review parsedReview) marker() string {
329
313
  // other `@`-opening line closes the review above it, so a review sharing a
330
314
  // block with `@param` or a following `@evidence` swallows neither.
331
315
  func parseReviews(comment string) []parsedReview {
332
- trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
333
- leadingLines := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
334
- comment = trimmed
335
- comment = strings.TrimPrefix(comment, "/**")
336
- comment = strings.TrimPrefix(comment, "/*")
337
- comment = strings.TrimSuffix(comment, "*/")
316
+ comment, leadingLines, _ := documentationCommentBody(comment)
338
317
  reviews := []parsedReview{}
339
318
  var pending *parsedReview
340
- fence := commentFence{}
341
319
  var body []string
342
320
  flush := func() {
343
321
  if pending == nil {
@@ -352,16 +330,12 @@ func parseReviews(comment string) []parsedReview {
352
330
  pending = nil
353
331
  body = nil
354
332
  }
355
- for index, rawLine := range strings.Split(comment, "\n") {
356
- line := strings.TrimSpace(rawLine)
357
- line = strings.TrimSpace(strings.TrimPrefix(line, "*"))
358
- line = strings.TrimSpace(strings.TrimPrefix(line, "///"))
359
- if fence.consume(line) {
360
- if pending != nil {
361
- body = append(body, line)
362
- }
333
+ for index, region := range documentationRegions(comment, true) {
334
+ if region.fence {
335
+ flush()
363
336
  continue
364
337
  }
338
+ line := region.text
365
339
  if reviews, remainder, opened := reviewLine(line); opened {
366
340
  flush()
367
341
  pending = &parsedReview{
@@ -471,8 +445,8 @@ func containsWhitespace(value string) bool {
471
445
  return false
472
446
  }
473
447
 
474
- // Fences inside documentation are examples, not declarations. Their text is
475
- // still retained when it belongs to a pending reason or review description.
448
+ // commentFence recognizes example delimiters. Its callers decide whether a
449
+ // recognized example terminates annotation prose or belongs to a TODO's text.
476
450
  type commentFence struct {
477
451
  marker rune
478
452
  length int
@@ -0,0 +1,214 @@
1
+ package evidence
2
+
3
+ import (
4
+ "strings"
5
+ "unicode"
6
+ )
7
+
8
+ // documentationRegionLine preserves one source line while separating annotation
9
+ // prose from a fenced example. A fence is also a pending-annotation boundary.
10
+ type documentationRegionLine struct {
11
+ // text is visible annotation prose with the original line slot retained.
12
+ text string
13
+
14
+ // fence ends pending prose and suppresses tags throughout the example.
15
+ fence bool
16
+ }
17
+
18
+ // documentationCommentBody removes only a real native wrapper. Mapped Prisma
19
+ // and Markdown bodies have no wrapper, so their initial indentation and blank
20
+ // lines must survive to retain relative columns and diagnostic line offsets.
21
+ func documentationCommentBody(comment string) (string, int, bool) {
22
+ trimmed := strings.TrimLeftFunc(comment, unicode.IsSpace)
23
+ prefix := ""
24
+ jsdoc := strings.HasPrefix(trimmed, "/**")
25
+ if jsdoc {
26
+ prefix = "/**"
27
+ } else if strings.HasPrefix(trimmed, "/*") {
28
+ prefix = "/*"
29
+ }
30
+ if prefix == "" {
31
+ return comment, 0, false
32
+ }
33
+ leading := strings.Count(comment[:len(comment)-len(trimmed)], "\n")
34
+ body := strings.TrimPrefix(trimmed, prefix)
35
+ body = strings.TrimSuffix(strings.TrimRightFunc(body, unicode.IsSpace), "*/")
36
+ return body, leading, jsdoc
37
+ }
38
+
39
+ // documentationRegions reads examples after removing comment decoration and
40
+ // ordinary common indentation. It shares region decisions between citations,
41
+ // reviews and withdrawal without merging their distinct semantic records.
42
+ // Fences take precedence over HTML and code spans; HTML contents cannot open a
43
+ // fence. Equal backtick runs protect literal HTML delimiters, including spans
44
+ // crossing a newline. Unmatched code delimiters remain ordinary prose.
45
+ func documentationRegions(body string, slashDecoration bool) []documentationRegionLine {
46
+ lines := strings.Split(body, "\n")
47
+ baseline := -1
48
+ for index, line := range lines {
49
+ line = strings.TrimSuffix(line, "\r")
50
+ stripped := strings.TrimLeft(line, " \t")
51
+ if strings.HasPrefix(stripped, "*") {
52
+ line = strings.TrimPrefix(stripped[1:], " ")
53
+ } else if slashDecoration && strings.HasPrefix(stripped, "///") {
54
+ line = strings.TrimPrefix(stripped[3:], " ")
55
+ }
56
+ lines[index] = line
57
+ if strings.TrimSpace(line) != "" {
58
+ columns, _ := documentationColumns(line)
59
+ if baseline < 0 || columns < baseline {
60
+ baseline = columns
61
+ }
62
+ }
63
+ }
64
+ content := strings.Join(lines, "\n")
65
+ mates := documentationCodeMates(content)
66
+ result := make([]documentationRegionLine, len(lines))
67
+ fence := commentFence{}
68
+ html := false
69
+ codeEnd := 0
70
+ tagEnd := 0
71
+ offset := 0
72
+ for index, line := range lines {
73
+ columns, cursor := documentationColumns(line)
74
+ relative := columns - baseline
75
+ delimiter := " " + line[cursor:]
76
+ if relative >= 0 && relative <= 3 {
77
+ delimiter = strings.Repeat(" ", relative) + line[cursor:]
78
+ }
79
+ if !html && fence.consume(delimiter) {
80
+ result[index].fence = true
81
+ codeEnd, tagEnd = 0, 0
82
+ offset += len(line) + 1
83
+ continue
84
+ }
85
+ masked := []byte(line)
86
+ for at := 0; at < len(line); {
87
+ if html {
88
+ end := len(line)
89
+ if closing := strings.Index(line[at:], "-->"); closing >= 0 {
90
+ end = at + closing + 3
91
+ html = false
92
+ }
93
+ for position := at; position < end; position++ {
94
+ masked[position] = ' '
95
+ }
96
+ at = end
97
+ continue
98
+ }
99
+ if codeEnd > offset+at || tagEnd > offset+at {
100
+ at++
101
+ continue
102
+ }
103
+ if relative < 4 {
104
+ if line[at] == '\\' && at+1 < len(line) {
105
+ at += 2
106
+ continue
107
+ }
108
+ if end, ok := mates[offset+at]; ok {
109
+ codeEnd = end
110
+ at++
111
+ continue
112
+ }
113
+ if strings.HasPrefix(line[at:], "<!--") {
114
+ html = true
115
+ // Empty HTML comments may close with an overlapping final dash.
116
+ if strings.HasPrefix(line[at:], "<!-->") {
117
+ for position := at; position < at+5; position++ {
118
+ masked[position] = ' '
119
+ }
120
+ at += 5
121
+ html = false
122
+ } else if strings.HasPrefix(line[at:], "<!--->") {
123
+ for position := at; position < at+6; position++ {
124
+ masked[position] = ' '
125
+ }
126
+ at += 6
127
+ html = false
128
+ }
129
+ continue
130
+ }
131
+ if line[at] == '<' {
132
+ if end := documentationTagEnd(content, offset+at); end > offset+at {
133
+ tagEnd = end
134
+ at++
135
+ continue
136
+ }
137
+ }
138
+ }
139
+ at++
140
+ }
141
+ result[index].text = strings.TrimSpace(string(masked))
142
+ offset += len(line) + 1
143
+ }
144
+ return result
145
+ }
146
+
147
+ // documentationCodeMates indexes the next equal backtick run in one linear
148
+ // reverse pass. A run without a closing mate must not hide later HTML syntax.
149
+ func documentationCodeMates(content string) map[int]int {
150
+ mates := map[int]int{}
151
+ next := map[int]int{}
152
+ for end := len(content); end > 0; {
153
+ end--
154
+ if content[end] != '`' {
155
+ continue
156
+ }
157
+ start := end
158
+ for start > 0 && content[start-1] == '`' {
159
+ start--
160
+ }
161
+ // Escapes apply to opening prose, not to a closing run inside code. The
162
+ // forward reader skips escaped openers before consulting this mate index.
163
+ length := end - start + 1
164
+ if closing, ok := next[length]; ok {
165
+ mates[start] = closing + length
166
+ }
167
+ next[length] = start
168
+ end = start
169
+ }
170
+ return mates
171
+ }
172
+
173
+ // documentationTagEnd keeps quoted attribute delimiters literal without adding
174
+ // an element-specific policy such as treating every pre element as an example.
175
+ func documentationTagEnd(line string, start int) int {
176
+ cursor := start + 1
177
+ if cursor < len(line) && line[cursor] == '/' {
178
+ cursor++
179
+ }
180
+ if cursor >= len(line) || !((line[cursor] >= 'a' && line[cursor] <= 'z') || (line[cursor] >= 'A' && line[cursor] <= 'Z')) {
181
+ return start
182
+ }
183
+ quote := byte(0)
184
+ for cursor++; cursor < len(line); cursor++ {
185
+ char := line[cursor]
186
+ if quote != 0 {
187
+ if char == quote {
188
+ quote = 0
189
+ }
190
+ } else if char == '\'' || char == '"' {
191
+ quote = char
192
+ } else if char == '>' {
193
+ return cursor + 1
194
+ }
195
+ }
196
+ return len(line)
197
+ }
198
+
199
+ // documentationColumns measures four-column tab stops while retaining the byte
200
+ // position where content starts. Fence grammar consumes the relative columns.
201
+ func documentationColumns(line string) (int, int) {
202
+ columns, cursor := 0, 0
203
+ for cursor < len(line) {
204
+ if line[cursor] == ' ' {
205
+ columns++
206
+ } else if line[cursor] == '\t' {
207
+ columns += 4 - columns%4
208
+ } else {
209
+ break
210
+ }
211
+ cursor++
212
+ }
213
+ return columns, cursor
214
+ }
@@ -14,10 +14,11 @@ import (
14
14
  //
15
15
  // Evidence declarations are read from one place only — the JSDoc blocks a node
16
16
  // reports — so an export without a block is not merely undocumented, it is
17
- // structurally unable to carry a citation. The graph never says so: coverage is
18
- // counted from the evidence side, so an obligation is discharged by whichever
19
- // host does have a block while every undocumented export in the same claim
20
- // contributes nothing and looks exactly like a passing file.
17
+ // structurally unable to carry a citation. Ordinary graph coverage is counted
18
+ // from the evidence side, so another host can discharge the obligation while
19
+ // an undocumented export contributes nothing. A checklist instead keeps every
20
+ // selected host's obligation, including hosts without a block. This rule checks
21
+ // the documentation itself in either mode.
21
22
  type documentedRule struct{}
22
23
 
23
24
  type documentedOptions struct {
package/native/exports.go CHANGED
@@ -214,7 +214,6 @@ func dedupeModuleExports(exports []moduleExport) []moduleExport {
214
214
  return unique
215
215
  }
216
216
 
217
- // reachedSymbol is one public address an entry traversal arrived at.
218
217
  // traversedPopulation is what selecting modules and walking their exports
219
218
  // yields: the obligations, the scopes above them, and the addresses that reach
220
219
  // them.
@@ -243,6 +242,7 @@ type publishedAddress struct {
243
242
  Unit *evidenceUnit
244
243
  }
245
244
 
245
+ // reachedSymbol is one public address an entry traversal arrived at.
246
246
  type reachedSymbol struct {
247
247
  // Address is the accessor path from the entry, segment by segment.
248
248
  Address []string
@@ -31,14 +31,8 @@ func normalizeFingerprintText(text string) string {
31
31
  for len(lines) != 0 && lines[len(lines)-1] == "" {
32
32
  lines = lines[:len(lines)-1]
33
33
  }
34
- // Leading blank lines go for a sharper reason. A TypeScript node's position is
35
- // its *full* start, meaning the end of the previous token, so an undocumented
36
- // declaration's span begins with whatever blank lines and `//` comments sit
37
- // above it. Keeping them made inserting one blank line elsewhere in the file
38
- // expire a review, and inconsistently: the same edit above a *documented*
39
- // declaration is neutral, because the whole leading run through `*/` is
40
- // excluded as a tag position. Dropping them makes both cases agree and honors
41
- // the rule that a reformat which changes no content expires nothing.
34
+ // Leading whitespace remains artifact content. TypeScript declaration spans
35
+ // discard their unowned leading trivia separately in withoutLeadingTrivia.
42
36
  return strings.Join(lines, "\n")
43
37
  }
44
38
 
package/native/glob.go CHANGED
@@ -49,7 +49,10 @@ func compileGlob(raw string) (globPattern, error) {
49
49
  value = strings.TrimPrefix(value, "./")
50
50
  }
51
51
  if value == "" {
52
- return globPattern{}, errors.New("the exclusion marker '!' must be followed by a glob")
52
+ if exclude && strings.TrimPrefix(raw, "!") == "" {
53
+ return globPattern{}, errors.New("the exclusion marker '!' must be followed by a glob")
54
+ }
55
+ return globPattern{}, errors.New("glob '" + raw + "' names no path below the project root")
53
56
  }
54
57
  if filepath.IsAbs(value) ||
55
58
  hasWindowsDrivePrefix(value) ||
package/native/graph.go CHANGED
@@ -1,7 +1,6 @@
1
1
  package evidence
2
2
 
3
3
  import (
4
- "os"
5
4
  "path/filepath"
6
5
  "sort"
7
6
  "strings"
@@ -13,8 +12,25 @@ type graphRule struct{}
13
12
 
14
13
  func (graphRule) Name() string { return graphRuleName }
15
14
 
15
+ // UsesProjectInputReader declares the actual external-input read boundary.
16
+ // Native readers use the supplied generation reader; external bridges withdraw
17
+ // completeness because their independent module/input reads are not observed.
18
+ func (graphRule) UsesProjectInputReader() bool { return true }
19
+
16
20
  func (graphRule) NeedsTypeChecker() bool { return false }
17
21
 
22
+ // Check builds artifact populations and checks each active claim's obligations.
23
+ //
24
+ // Claim populations are loaded first because a healthy empty population owes
25
+ // nothing; its references must not trigger unrelated reads or failures. Failed
26
+ // populations remain active, and their health prevents partial inventories from
27
+ // producing a false coverage result. TypeScript identities come from parsed
28
+ // declarations, while Markdown, Prisma and Swagger retain their own hierarchies.
29
+ //
30
+ // Inventory construction, address resolution and obligation evaluation are
31
+ // separate stages so selectors never invent declarations or decide identity.
32
+ // Successful publication reuses those inventories for editor hints; diagnostics
33
+ // and Program-cycle coordination remain owned by the host's project-rule API.
18
34
  func (graphRule) Check(ctx *rule.ProjectContext) {
19
35
  if ctx == nil {
20
36
  return
@@ -31,6 +47,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
31
47
  reportProblems(ctx, problems)
32
48
  return
33
49
  }
50
+ config.inputs = evidenceInputReader{host: ctx.Inputs}
34
51
  resolveGraphSeverities(&config, ctx.Severity)
35
52
  config = enabledGraphConfig(config)
36
53
  if len(config.Claims) == 0 {
@@ -42,7 +59,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
42
59
  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.")
43
60
  return
44
61
  }
45
- info, err := os.Stat(root)
62
+ info, err := config.inputs.Stat(root)
46
63
  if err != nil || !info.IsDir() {
47
64
  ctx.Report("Evidence graph project root '" + root + "' is not a readable directory. Fix the ttsc project identity before evaluating evidence globs.")
48
65
  return
@@ -62,9 +79,11 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
62
79
  ctx.Sources,
63
80
  claimPopulationConfig(config, artifactTypeScript),
64
81
  )
82
+ markdownSnapshot := &markdownCapture{}
65
83
  markdownClaims, markdownClaimProblems := loadMarkdownInventories(
66
84
  root,
67
85
  claimPopulationConfig(config, artifactMarkdown),
86
+ markdownSnapshot,
68
87
  )
69
88
  prismaClaims, prismaClaimProblems := loadPrismaInventories(
70
89
  root,
@@ -89,15 +108,15 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
89
108
  config = activeGraphConfig(config, markdownClaims, prismaClaims, typescript)
90
109
  governed := map[string]bool{}
91
110
  extendTypeScriptInventories(root, ctx.Sources, config, typescript, nil)
92
- recordGovernedTypeScriptFiles(ctx.Sources, declared, governed)
93
- markdown, markdownProblems := loadMarkdownInventories(root, config)
111
+ recordGovernedTypeScriptFiles(root, ctx.Sources, declared, governed)
112
+ markdown, markdownProblems := loadMarkdownInventories(root, config, markdownSnapshot)
94
113
  prisma, prismaProblems := loadPrismaInventories(root, config)
95
114
  swagger, swaggerProblems := loadSwaggerInventories(root, config)
96
115
  diagnostics = append(diagnostics, markdownProblems...)
97
116
  diagnostics = append(diagnostics, prismaProblems...)
98
117
  diagnostics = append(diagnostics, swaggerProblems...)
99
118
  diagnostics = append(diagnostics, unreadableTypeScriptTags(typescript, governed, declared)...)
100
- loader := newTypeScriptLoader(root, typescript)
119
+ loader := newTypeScriptLoader(root, typescript, config.inputs)
101
120
  states, stateProblems := materializeClaimStates(
102
121
  config,
103
122
  markdown,
@@ -112,7 +131,9 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
112
131
  if len(diagnostics) == 0 {
113
132
  // Published only on a clean evaluation, because the host reads state
114
133
  // from a rule that passed and reporting anything marks this one failed
115
- // (`linthost/hints.go:147-149`, `linthost/project_engine.go:68-77`).
134
+ // (`collectProjectHints` in `linthost/hints.go` skips a rule whose status is
135
+ // not passed, and `projectReporter` in `linthost/project_engine.go` marks a
136
+ // rule failed once it reports).
116
137
  // Setting it unconditionally would not widen the gate; it would only
117
138
  // hide where the gate is.
118
139
  cycle.Corpus = graphCorpus{
@@ -164,7 +185,7 @@ func claimPopulationConfig(
164
185
  claim.References = nil
165
186
  claims = append(claims, claim)
166
187
  }
167
- return graphConfig{Claims: claims}
188
+ return graphConfig{Claims: claims, inputs: config.inputs}
168
189
  }
169
190
 
170
191
  // activeGraphConfig omits healthy claim populations that contain no own unit
@@ -660,14 +681,13 @@ func evaluateEvidenceGraph(
660
681
  continue
661
682
  }
662
683
  candidates := declarationCandidates(declaration.Target, targets, markdownTargets)
663
- // The configuration guard refuses a code population to a claim that
664
- // cannot address one, but the address map is built from every claim at
665
- // once — so a Markdown claim could still land on a symbol materialized
666
- // by some *other* claim's TypeScript reference. Measured: it resolved
667
- // silently, which left repository-wide name uniqueness load-bearing
668
- // through a door the guard does not cover. Closing it here rather than
669
- // by scoping the whole map keeps resolution global for the artifacts
670
- // that are addressed by path, where a shared map costs nothing.
684
+ // The address map is built from every claim at once, so a claim that
685
+ // addresses code only through a file-qualified link, such as a Markdown
686
+ // claim, could still land on a symbol materialized by some *other* claim's
687
+ // TypeScript reference through a plain token. That would resolve silently
688
+ // and make repository-wide name uniqueness load-bearing. Refusing it here
689
+ // rather than scoping the whole map keeps resolution global for the
690
+ // artifacts that are addressed by path, where a shared map costs nothing.
671
691
  if declaration.Type != artifactTypeScript {
672
692
  addressable, code := splitCodeCandidates(candidates)
673
693
  if len(addressable) == 0 && len(code) != 0 {
@@ -937,9 +957,16 @@ func evaluateEvidenceGraph(
937
957
  for _, unit := range covered {
938
958
  targets = append(targets, "'"+unit.Target+"'")
939
959
  }
960
+ // The exclusion alternative is offered only where this reference
961
+ // accepts one: a checklist is the pairing `noEvidenceExclude` is allowed
962
+ // beside, and there the exclusion would be refused as forbidden.
963
+ repair := "Cite each item this host answers for, or write @evidenceExclude on this scope when none of it applies here."
964
+ if reference.Spec.Policy.NoExclude {
965
+ repair = "Cite each item this host answers for; this reference forbids @evidenceExclude."
966
+ }
940
967
  problems = problems.add(
941
968
  severity,
942
- "Aggregate @evidence target '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": this reference is a checklist, so a citation answers for the item it names, and this target names a scope containing "+decimal(len(covered))+" item(s) ("+strings.Join(targets, ", ")+") rather than one of them. Cite each item this host answers for, or write @evidenceExclude on this scope when none of it applies here."+untrueTagWarning,
969
+ "Aggregate @evidence target '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": this reference is a checklist, so a citation answers for the item it names, and this target names a scope containing "+decimal(len(covered))+" item(s) ("+strings.Join(targets, ", ")+") rather than one of them. "+repair+untrueTagWarning,
943
970
  )
944
971
  for _, unit := range covered {
945
972
  for _, hostID := range keyHosts {
@@ -976,7 +1003,7 @@ func evaluateEvidenceGraph(
976
1003
  if first := byScope[scopeID]; first != nil {
977
1004
  problems = problems.add(
978
1005
  severity,
979
- "Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
1006
+ "Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+". Delete the repeated citation.",
980
1007
  )
981
1008
  } else {
982
1009
  byScope[scopeID] = declaration
@@ -1084,13 +1111,13 @@ func evaluateEvidenceGraph(
1084
1111
  }
1085
1112
  problems = problems.add(
1086
1113
  severity,
1087
- "Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+". Delete whichever is untrue of this host."+untrueTagWarning,
1114
+ "Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+". Delete whichever of the two is untrue."+untrueTagWarning,
1088
1115
  )
1089
1116
  }
1090
1117
  if duplicateExclusionUnit != nil {
1091
1118
  problems = problems.add(
1092
1119
  severity,
1093
- "Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
1120
+ "Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+". Delete one of the two exclusions.",
1094
1121
  )
1095
1122
  }
1096
1123
  }
@@ -1108,7 +1135,7 @@ func evaluateEvidenceGraph(
1108
1135
  }
1109
1136
  problems = problems.add(
1110
1137
  severity,
1111
- "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. Split this host so each unit has one that owns it, or keep positive @evidence citations on this semantic host to exactly one distinct unit.",
1138
+ "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: cite the one unit it answers for, or split the host so that each unit has a host of its own."+untrueTagWarning,
1112
1139
  )
1113
1140
  }
1114
1141
  }
@@ -1206,9 +1233,18 @@ func evaluateEvidenceGraph(
1206
1233
  host = "unsupported or non-exported declaration"
1207
1234
  }
1208
1235
  if declaration.Tag == tagExclude {
1236
+ // The carriers differ by artifact kind, so the repair names only the ones
1237
+ // this kind has: an unattached run exists in Prisma schemas alone.
1238
+ repair := "Move the exclusion onto a selected host of the claim."
1239
+ switch declaration.Type {
1240
+ case artifactTypeScript:
1241
+ repair = "Move the exclusion onto a supported public export in a matching claim file."
1242
+ case artifactPrisma:
1243
+ repair = "Move the exclusion onto a selected model, column, or relation, or into a top-level unattached '///' run in a matching claim file."
1244
+ }
1209
1245
  problems = problems.add(
1210
1246
  outOfScopeLevels[id],
1211
- "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.",
1247
+ "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. "+repair+untrueTagWarning,
1212
1248
  )
1213
1249
  continue
1214
1250
  }
@@ -1421,7 +1457,7 @@ func hiddenTargetProblem(
1421
1457
  hidden *evidenceUnit,
1422
1458
  context string,
1423
1459
  ) string {
1424
- 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 removes 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. Do not remove '@hidden' from the target to make this citation legal." + untrueTagWarning
1460
+ 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 removes 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. Do not remove '" + hidden.Hidden + "' from the target to make this citation legal." + untrueTagWarning
1425
1461
  }
1426
1462
 
1427
1463
  func declarationResolutionUncertain(owners []claimState) bool {
@@ -1659,7 +1695,7 @@ func resolveReferenceEntry(
1659
1695
  ) (string, string) {
1660
1696
  entry := loader.packageEntryModule(reference.Package)
1661
1697
  if entry == "" {
1662
- 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'."
1698
+ 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' or 'typings'. A package with none of those resolves only when its 'exports' target or 'main' names TypeScript source itself."
1663
1699
  }
1664
1700
  return entry, ""
1665
1701
  }
@@ -30,7 +30,8 @@ func (graphRule) GraphNodes(ctx *rule.GraphContext) []rule.GraphNode {
30
30
  corpus := cycle.Corpus
31
31
  // Addresses of every materialized unit, so a parent can be named by the
32
32
  // address a citation would use rather than by the rule's internal id. A
33
- // parent outside the selected set resolves to nothing and the host clears it,
33
+ // parent outside the selected set is still named by its address here, and the
34
+ // host clears a parent that no published node carries (`linthost/graph_nodes.go`),
34
35
  // which leaves the child at the top of its chain instead of inventing one.
35
36
  addresses := map[string]string{}
36
37
  for _, inventories := range []map[string]*artifactInventory{