@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,423 @@
1
+ package evidence
2
+
3
+ import (
4
+ "io/fs"
5
+ "os"
6
+ "path/filepath"
7
+ "regexp"
8
+ "strings"
9
+ "unicode"
10
+ )
11
+
12
+ var markdownCommentPattern = regexp.MustCompile(`(?s)<!--(.*?)-->`)
13
+ var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-]*)\}\s*$`)
14
+
15
+ // loadMarkdownInventories reads every configured Markdown population, once per
16
+ // distinct base.
17
+ //
18
+ // One walk per base rather than one walk for the project, because a population
19
+ // that declares a root sits outside the tree the project walk covers — and a
20
+ // walk that started high enough to cover both would descend through everything
21
+ // between them. Two populations sharing a base share one walk, so the cost
22
+ // tracks the roots an author declared rather than the populations they wrote.
23
+ func loadMarkdownInventories(
24
+ root string,
25
+ config graphConfig,
26
+ ) (map[string]*artifactInventory, []string) {
27
+ inventories := map[string]*artifactInventory{}
28
+ problems := []string{}
29
+ for _, base := range configuredBases(config, artifactMarkdown) {
30
+ problems = append(
31
+ problems,
32
+ loadMarkdownBase(base, config, inventories)...,
33
+ )
34
+ }
35
+ return inventories, problems
36
+ }
37
+
38
+ func loadMarkdownBase(
39
+ base populationBase,
40
+ config graphConfig,
41
+ inventories map[string]*artifactInventory,
42
+ ) []string {
43
+ problems := []string{}
44
+ if problem := unreadableBaseProblem(base, artifactMarkdown); problem != "" {
45
+ recordPopulationFailure(inventories, artifactMarkdown, base)
46
+ return []string{problem}
47
+ }
48
+ err := filepath.WalkDir(base.Absolute, func(current string, entry fs.DirEntry, walkErr error) error {
49
+ if walkErr != nil {
50
+ relative, ok := relativeProjectPath(base.Absolute, current)
51
+ relevant := ok &&
52
+ (matchesConfiguredMarkdownFile(config, base, relative) ||
53
+ couldContainConfiguredMarkdown(config, base, relative))
54
+ if !relevant {
55
+ if entry != nil && entry.IsDir() {
56
+ return filepath.SkipDir
57
+ }
58
+ return nil
59
+ }
60
+ recordPopulationFailure(inventories, artifactMarkdown, base)
61
+ problems = append(problems, "Evidence graph could not inspect '"+current+"': "+walkErr.Error()+". Fix filesystem access so configured Markdown sources can be indexed.")
62
+ if entry != nil && entry.IsDir() {
63
+ return filepath.SkipDir
64
+ }
65
+ return nil
66
+ }
67
+ if entry.IsDir() {
68
+ if current != base.Absolute {
69
+ relative, ok := relativeProjectPath(base.Absolute, current)
70
+ if !ok || !couldContainConfiguredMarkdown(config, base, relative) {
71
+ return filepath.SkipDir
72
+ }
73
+ }
74
+ return nil
75
+ }
76
+ relative, ok := relativeProjectPath(base.Absolute, current)
77
+ if !ok {
78
+ return nil
79
+ }
80
+ if !matchesConfiguredMarkdownFile(config, base, relative) {
81
+ return nil
82
+ }
83
+ address := base.addressOf(relative)
84
+ content, readErr := os.ReadFile(current)
85
+ if readErr != nil {
86
+ inventories[address.Key] = &artifactInventory{
87
+ Path: address.Display,
88
+ Type: artifactMarkdown,
89
+ LoadFailed: true,
90
+ }
91
+ problems = append(problems, "Evidence graph could not read Markdown file '"+address.Display+"': "+readErr.Error()+". Fix filesystem access or exclude the file from configured globs.")
92
+ return nil
93
+ }
94
+ inventory, _ := scanMarkdownInventory(address, string(content))
95
+ inventories[address.Key] = inventory
96
+ for _, inventoryProblem := range inventory.Problems {
97
+ if selectedByMarkdownReference(config, base, relative, inventoryProblem.Symbol) {
98
+ problems = append(problems, inventoryProblem.Message)
99
+ }
100
+ }
101
+ return nil
102
+ })
103
+ if err != nil {
104
+ recordPopulationFailure(inventories, artifactMarkdown, base)
105
+ problems = append(problems, "Evidence graph could not walk Markdown root '"+populationRootLabel(base)+"': "+err.Error()+".")
106
+ }
107
+ return problems
108
+ }
109
+
110
+ func scanMarkdownInventory(
111
+ address artifactAddress,
112
+ content string,
113
+ ) (*artifactInventory, []string) {
114
+ // The target is the path inside the population's base, while the location is
115
+ // the path a reader opens. They are the same string for a project-rooted
116
+ // population and deliberately differ for a rooted one: a citation that keeps
117
+ // working when the document set is adopted by a sibling package cannot carry
118
+ // that package's distance from the documents.
119
+ path := address.Relative
120
+ inventory := &artifactInventory{
121
+ Path: address.Display,
122
+ Type: artifactMarkdown,
123
+ }
124
+ problems := []string{}
125
+ targetablePath := !containsWhitespace(path)
126
+ fileUnitID := ""
127
+ if targetablePath {
128
+ fileUnitID = "markdown:" + address.Key + ":file"
129
+ inventory.Units = append(inventory.Units, &evidenceUnit{
130
+ ID: fileUnitID,
131
+ Target: path,
132
+ Type: artifactMarkdown,
133
+ Symbol: "file",
134
+ Path: address.Display,
135
+ Line: 1,
136
+ Readable: "Markdown file",
137
+ })
138
+ } else {
139
+ problem := "Markdown file '" + address.Display + "' cannot form an evidence target because its path contains whitespace. Rename the file so '@evidence <target> <reason>' can represent its path as one target token."
140
+ problems = append(problems, problem)
141
+ inventory.Problems = append(inventory.Problems, inventoryProblem{
142
+ Symbol: "*",
143
+ Message: problem,
144
+ })
145
+ }
146
+
147
+ lines := strings.Split(content, "\n")
148
+ hostAtLine := make([]string, len(lines))
149
+ hostIDAtLine := make([]string, len(lines))
150
+ fencedAtLine := make([]bool, len(lines))
151
+ currentHost := "file"
152
+ currentHostID := fileUnitID
153
+ fenceMarker := rune(0)
154
+ fenceLength := 0
155
+ inHTMLComment := false
156
+ headingUnitIDs := [5]string{}
157
+ for index, rawLine := range lines {
158
+ line := strings.TrimSuffix(rawLine, "\r")
159
+ trimmed := strings.TrimLeft(line, " \t")
160
+ if marker, length, remainder, ok := markdownFence(line); ok {
161
+ fencedAtLine[index] = true
162
+ hostIDAtLine[index] = currentHostID
163
+ if fenceMarker == 0 {
164
+ fenceMarker = marker
165
+ fenceLength = length
166
+ } else if marker == fenceMarker &&
167
+ length >= fenceLength &&
168
+ strings.TrimSpace(remainder) == "" {
169
+ fenceMarker = 0
170
+ fenceLength = 0
171
+ }
172
+ hostAtLine[index] = currentHost
173
+ continue
174
+ }
175
+ if fenceMarker != 0 {
176
+ fencedAtLine[index] = true
177
+ hostAtLine[index] = currentHost
178
+ hostIDAtLine[index] = currentHostID
179
+ continue
180
+ }
181
+ if inHTMLComment {
182
+ if strings.Contains(trimmed, "-->") {
183
+ inHTMLComment = false
184
+ }
185
+ hostAtLine[index] = currentHost
186
+ hostIDAtLine[index] = currentHostID
187
+ continue
188
+ }
189
+ if strings.HasPrefix(trimmed, "<!--") {
190
+ remainder := strings.TrimPrefix(trimmed, "<!--")
191
+ if !strings.Contains(remainder, "-->") {
192
+ inHTMLComment = true
193
+ }
194
+ hostAtLine[index] = currentHost
195
+ hostIDAtLine[index] = currentHostID
196
+ continue
197
+ }
198
+ level, title, ok := markdownHeading(line)
199
+ if ok {
200
+ currentHost = "h" + decimal(level)
201
+ currentHostID = "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1)
202
+ if level <= 4 {
203
+ for descendantLevel := level; descendantLevel <= 4; descendantLevel++ {
204
+ headingUnitIDs[descendantLevel] = ""
205
+ }
206
+ }
207
+ if level <= 4 && targetablePath {
208
+ title, anchor := markdownHeadingIdentity(title)
209
+ if anchor == "" {
210
+ problems = append(
211
+ problems,
212
+ "Markdown evidence unit at "+address.Display+":"+decimal(index+1)+" has no resolvable anchor. Add a non-empty heading title or an explicit '{#anchor}' suffix.",
213
+ )
214
+ inventory.Problems = append(inventory.Problems, inventoryProblem{
215
+ Symbol: currentHost,
216
+ Message: problems[len(problems)-1],
217
+ })
218
+ } else {
219
+ parentID := fileUnitID
220
+ for ancestorLevel := level - 1; ancestorLevel >= 1; ancestorLevel-- {
221
+ if headingUnitIDs[ancestorLevel] != "" {
222
+ parentID = headingUnitIDs[ancestorLevel]
223
+ break
224
+ }
225
+ }
226
+ unit := &evidenceUnit{
227
+ ID: "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1),
228
+ ParentID: parentID,
229
+ Target: path + "#" + anchor,
230
+ Type: artifactMarkdown,
231
+ Symbol: currentHost,
232
+ Path: address.Display,
233
+ Line: index + 1,
234
+ Readable: "Markdown " + strings.ToUpper(currentHost) + " '" + title + "'",
235
+ }
236
+ inventory.Units = append(inventory.Units, unit)
237
+ headingUnitIDs[level] = unit.ID
238
+ }
239
+ }
240
+ }
241
+ hostAtLine[index] = currentHost
242
+ hostIDAtLine[index] = currentHostID
243
+ }
244
+
245
+ sequence := 0
246
+ for _, match := range markdownCommentPattern.FindAllStringSubmatchIndex(content, -1) {
247
+ if len(match) < 4 {
248
+ continue
249
+ }
250
+ commentStart := match[0]
251
+ line := lineAt(content, commentStart)
252
+ if line <= 0 || line > len(lines) || fencedAtLine[line-1] {
253
+ continue
254
+ }
255
+ comment := content[match[2]:match[3]]
256
+ for _, parsed := range parseDeclarations(comment) {
257
+ sequence++
258
+ inventory.Declarations = append(inventory.Declarations, &evidenceDeclaration{
259
+ ID: "markdown:" + address.Key + ":" + decimal(line+parsed.LineOffset) + ":" + decimal(sequence),
260
+ HostID: hostIDAtLine[line-1],
261
+ SemanticHostIDs: []string{hostIDAtLine[line-1]},
262
+ Type: artifactMarkdown,
263
+ Tag: parsed.Tag,
264
+ Target: parsed.Target,
265
+ Reason: parsed.Reason,
266
+ Hosts: symbolSet{hostAtLine[line-1]: true},
267
+ Path: address.Display,
268
+ Line: line + parsed.LineOffset,
269
+ Sequence: sequence,
270
+ })
271
+ }
272
+ }
273
+ return inventory, problems
274
+ }
275
+
276
+ // matchesConfiguredMarkdownFile reports whether a population rooted at this base
277
+ // selects the file.
278
+ //
279
+ // The base is compared before the globs, because a walk covers one base at a
280
+ // time and another base's patterns say nothing about a path inside this one.
281
+ // Without that comparison a project-rooted `docs/**` would sweep in the
282
+ // `docs` directory of every declared root.
283
+ func matchesConfiguredMarkdownFile(
284
+ config graphConfig,
285
+ base populationBase,
286
+ path string,
287
+ ) bool {
288
+ for _, claim := range config.Claims {
289
+ if claim.Type == artifactMarkdown &&
290
+ claim.Base.Absolute == base.Absolute &&
291
+ claim.Files.matches(path) {
292
+ return true
293
+ }
294
+ for _, reference := range claim.References {
295
+ if reference.Type == artifactMarkdown &&
296
+ reference.Base.Absolute == base.Absolute &&
297
+ reference.Files.matches(path) {
298
+ return true
299
+ }
300
+ }
301
+ }
302
+ return false
303
+ }
304
+
305
+ func couldContainConfiguredMarkdown(
306
+ config graphConfig,
307
+ base populationBase,
308
+ directory string,
309
+ ) bool {
310
+ for _, claim := range config.Claims {
311
+ if claim.Type == artifactMarkdown &&
312
+ claim.Base.Absolute == base.Absolute &&
313
+ claim.Files.couldMatchDescendant(directory) {
314
+ return true
315
+ }
316
+ for _, reference := range claim.References {
317
+ if reference.Type == artifactMarkdown &&
318
+ reference.Base.Absolute == base.Absolute &&
319
+ reference.Files.couldMatchDescendant(directory) {
320
+ return true
321
+ }
322
+ }
323
+ }
324
+ return false
325
+ }
326
+
327
+ func selectedByMarkdownReference(
328
+ config graphConfig,
329
+ base populationBase,
330
+ path string,
331
+ symbol string,
332
+ ) bool {
333
+ for _, claim := range config.Claims {
334
+ for _, reference := range claim.References {
335
+ if reference.Type == artifactMarkdown &&
336
+ reference.Base.Absolute == base.Absolute &&
337
+ reference.Files.matches(path) &&
338
+ (symbol == "*" || reference.Symbols.contains(symbol)) {
339
+ return true
340
+ }
341
+ }
342
+ }
343
+ return false
344
+ }
345
+
346
+ func markdownFence(line string) (rune, int, string, bool) {
347
+ indent := 0
348
+ for indent < len(line) && line[indent] == ' ' {
349
+ indent++
350
+ }
351
+ if indent > 3 {
352
+ return 0, 0, "", false
353
+ }
354
+ runes := []rune(line[indent:])
355
+ if len(runes) < 3 || (runes[0] != '`' && runes[0] != '~') {
356
+ return 0, 0, "", false
357
+ }
358
+ count := 1
359
+ for count < len(runes) && runes[count] == runes[0] {
360
+ count++
361
+ }
362
+ if count < 3 {
363
+ return 0, 0, "", false
364
+ }
365
+ remainder := string(runes[count:])
366
+ if runes[0] == '`' && strings.Contains(remainder, "`") {
367
+ return 0, 0, "", false
368
+ }
369
+ return runes[0], count, remainder, true
370
+ }
371
+
372
+ func markdownHeading(line string) (int, string, bool) {
373
+ space := 0
374
+ for space < len(line) && line[space] == ' ' && space < 4 {
375
+ space++
376
+ }
377
+ if space > 3 || space >= len(line) || line[space] != '#' {
378
+ return 0, "", false
379
+ }
380
+ level := 0
381
+ for space+level < len(line) && line[space+level] == '#' {
382
+ level++
383
+ }
384
+ if level == 0 || level > 6 {
385
+ return 0, "", false
386
+ }
387
+ next := space + level
388
+ if next < len(line) && line[next] != ' ' && line[next] != '\t' {
389
+ return 0, "", false
390
+ }
391
+ title := strings.TrimSpace(line[next:])
392
+ trimmedHashes := strings.TrimRight(title, "#")
393
+ if trimmedHashes != title && (trimmedHashes == "" || strings.HasSuffix(trimmedHashes, " ") || strings.HasSuffix(trimmedHashes, "\t")) {
394
+ title = strings.TrimSpace(trimmedHashes)
395
+ }
396
+ return level, title, true
397
+ }
398
+
399
+ func markdownHeadingIdentity(title string) (string, string) {
400
+ if match := explicitAnchorPattern.FindStringSubmatch(title); len(match) == 2 {
401
+ cleanTitle := strings.TrimSpace(explicitAnchorPattern.ReplaceAllString(title, ""))
402
+ return cleanTitle, match[1]
403
+ }
404
+ return title, markdownSlug(title)
405
+ }
406
+
407
+ func markdownSlug(title string) string {
408
+ var builder strings.Builder
409
+ lastHyphen := false
410
+ for _, char := range strings.ToLower(title) {
411
+ switch {
412
+ case unicode.IsLetter(char), unicode.IsNumber(char), char == '_':
413
+ builder.WriteRune(char)
414
+ lastHyphen = false
415
+ case char == '-' || unicode.IsSpace(char):
416
+ if builder.Len() > 0 && !lastHyphen {
417
+ builder.WriteRune('-')
418
+ lastHyphen = true
419
+ }
420
+ }
421
+ }
422
+ return strings.Trim(builder.String(), "-")
423
+ }