@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,798 @@
1
+ package evidence
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/url"
8
+ "path"
9
+ "sort"
10
+ "strings"
11
+ )
12
+
13
+ func decodeGraphConfig(raw json.RawMessage) (graphConfig, []string) {
14
+ var config graphConfig
15
+ if len(bytes.TrimSpace(raw)) == 0 {
16
+ return config, []string{
17
+ "Invalid evidence/graph configuration: the rule requires an ITtscEvidenceGraphConfig options object. Configure it as ['error', { claims: [...] }].",
18
+ }
19
+ }
20
+ object, problem := decodeObject(raw, "configuration")
21
+ if problem != "" {
22
+ return config, []string{problem}
23
+ }
24
+ problems := rejectUnknownFields(object, []string{"claims"}, graphRuleName, "configuration")
25
+ claimRaw, exists := object["claims"]
26
+ if !exists {
27
+ problems = append(problems, "Invalid evidence/graph configuration at claims: the required claim array is missing.")
28
+ return config, problems
29
+ }
30
+ var claims []json.RawMessage
31
+ if err := json.Unmarshal(claimRaw, &claims); err != nil {
32
+ problems = append(problems, "Invalid evidence/graph configuration at claims: expected an array of Markdown or TypeScript claims.")
33
+ return config, problems
34
+ }
35
+ if len(claims) == 0 {
36
+ problems = append(problems, "Invalid evidence/graph configuration at claims: at least one claim is required; an empty graph cannot establish evidence coverage.")
37
+ return config, problems
38
+ }
39
+ for index, claimRaw := range claims {
40
+ claim, claimProblems := decodeClaim(claimRaw, index)
41
+ problems = append(problems, claimProblems...)
42
+ if len(claimProblems) == 0 {
43
+ config.Claims = append(config.Claims, claim)
44
+ }
45
+ }
46
+ return config, problems
47
+ }
48
+
49
+ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
50
+ path := fmt.Sprintf("claims[%d]", index)
51
+ object, problem := decodeObject(raw, path)
52
+ if problem != "" {
53
+ return claimSpec{}, []string{problem}
54
+ }
55
+ problems := rejectUnknownFields(
56
+ object,
57
+ []string{"type", "name", "disabled", "root", "files", "evidenceExcludeCarriers", "symbol", "reference"},
58
+ graphRuleName,
59
+ path,
60
+ )
61
+ kind, kindProblem := decodeArtifactKind(object["type"], path+".type", false)
62
+ if kindProblem != "" {
63
+ problems = append(problems, kindProblem)
64
+ }
65
+ name := ""
66
+ if rawName, exists := object["name"]; exists {
67
+ if err := json.Unmarshal(rawName, &name); err != nil {
68
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+".name: expected a diagnostic-only string label.")
69
+ }
70
+ }
71
+ disabled := false
72
+ if rawDisabled, exists := object["disabled"]; exists {
73
+ switch string(bytes.TrimSpace(rawDisabled)) {
74
+ case "true":
75
+ disabled = true
76
+ case "false":
77
+ default:
78
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+".disabled: expected a boolean.")
79
+ }
80
+ }
81
+ root, rootProblems := decodeClaimRoot(object["root"], kind, path+".root")
82
+ problems = append(problems, rootProblems...)
83
+ files, fileProblems := decodeFiles(object["files"], path+".files")
84
+ problems = append(problems, fileProblems...)
85
+ carriers := globSet{}
86
+ if raw, exists := object["evidenceExcludeCarriers"]; exists {
87
+ decoded, carrierProblems := decodeFiles(raw, path+".evidenceExcludeCarriers")
88
+ problems = append(problems, carrierProblems...)
89
+ carriers = decoded
90
+ }
91
+ symbols, symbolProblems := decodeSymbols(object["symbol"], kind, false, graphRuleName, path+".symbol")
92
+ problems = append(problems, symbolProblems...)
93
+ references, referenceProblems := decodeReferences(kind, object["reference"], path+".reference")
94
+ problems = append(problems, referenceProblems...)
95
+ if len(problems) != 0 {
96
+ return claimSpec{}, problems
97
+ }
98
+ return claimSpec{
99
+ Index: index,
100
+ Type: kind,
101
+ Name: name,
102
+ Disabled: disabled,
103
+ Root: root,
104
+ Files: files,
105
+ ExclusionCarriers: carriers,
106
+ Symbols: symbols,
107
+ References: references,
108
+ }, nil
109
+ }
110
+
111
+ // enabledGraphConfig removes explicitly staged claims after their public
112
+ // configuration has been validated. Keeping the filter separate from decoding
113
+ // preserves original claim indexes and prevents disabled entries from hiding a
114
+ // malformed shape.
115
+ func enabledGraphConfig(config graphConfig) graphConfig {
116
+ claims := make([]claimSpec, 0, len(config.Claims))
117
+ for _, claim := range config.Claims {
118
+ if claim.Disabled {
119
+ continue
120
+ }
121
+ claims = append(claims, claim)
122
+ }
123
+ config.Claims = claims
124
+ return config
125
+ }
126
+
127
+ func decodeReferences(
128
+ claimKind artifactKind,
129
+ raw json.RawMessage,
130
+ path string,
131
+ ) ([]referenceSpec, []string) {
132
+ if len(bytes.TrimSpace(raw)) == 0 {
133
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": the required evidence reference is missing."}
134
+ }
135
+ trimmed := bytes.TrimSpace(raw)
136
+ elements := []json.RawMessage{}
137
+ switch trimmed[0] {
138
+ case '{':
139
+ elements = append(elements, raw)
140
+ case '[':
141
+ if err := json.Unmarshal(raw, &elements); err != nil {
142
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": expected one reference object or an array of reference objects."}
143
+ }
144
+ if len(elements) == 0 {
145
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": an empty reference array creates no coverage obligation; provide at least one evidence reference."}
146
+ }
147
+ default:
148
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": expected one reference object or an array of reference objects."}
149
+ }
150
+ references := make([]referenceSpec, 0, len(elements))
151
+ problems := []string{}
152
+ for index, element := range elements {
153
+ referencePath := path
154
+ if len(elements) > 1 || trimmed[0] == '[' {
155
+ referencePath += "[" + decimal(index) + "]"
156
+ }
157
+ reference, referenceProblems := decodeReference(
158
+ claimKind,
159
+ element,
160
+ index,
161
+ referencePath,
162
+ )
163
+ problems = append(problems, referenceProblems...)
164
+ if len(referenceProblems) == 0 {
165
+ references = append(references, reference)
166
+ }
167
+ }
168
+ return references, problems
169
+ }
170
+
171
+ func decodeReference(
172
+ claimKind artifactKind,
173
+ raw json.RawMessage,
174
+ index int,
175
+ path string,
176
+ ) (referenceSpec, []string) {
177
+ object, problem := decodeObject(raw, path)
178
+ if problem != "" {
179
+ return referenceSpec{}, []string{problem}
180
+ }
181
+ problems := rejectUnknownFields(
182
+ object,
183
+ []string{
184
+ "type",
185
+ "noEvidenceExclude",
186
+ "uniqueEvidence",
187
+ "singleEvidencePerSymbol",
188
+ "package",
189
+ "root",
190
+ "file",
191
+ "files",
192
+ "symbol",
193
+ },
194
+ graphRuleName,
195
+ path,
196
+ )
197
+ kind, kindProblem := decodeArtifactKind(object["type"], path+".type", true)
198
+ if kindProblem != "" {
199
+ problems = append(problems, kindProblem)
200
+ }
201
+ if problem := rejectForeignTypeScriptReference(claimKind, kind, path); problem != "" {
202
+ problems = append(problems, problem)
203
+ }
204
+ root, rootProblems := decodeRoot(object["root"], kind, path+".root")
205
+ problems = append(problems, rootProblems...)
206
+ policy, policyProblems := decodeReferencePolicy(object, path)
207
+ problems = append(problems, policyProblems...)
208
+ files := globSet{}
209
+ source := ""
210
+ packageName := ""
211
+ symbols := symbolSet{}
212
+ if kind != artifactTypeScript {
213
+ if _, exists := object["package"]; exists {
214
+ problems = append(
215
+ problems,
216
+ "Invalid evidence/graph configuration at "+path+".package: only a TypeScript reference can select an installed package; Markdown and Swagger evidence lives in this project.",
217
+ )
218
+ }
219
+ }
220
+ if kind == artifactTypeScript {
221
+ reference, referenceProblems := decodeTypeScriptReference(object, path)
222
+ problems = append(problems, referenceProblems...)
223
+ files = reference.Files
224
+ packageName = reference.Package
225
+ var symbolProblems []string
226
+ symbols, symbolProblems = decodeSymbols(object["symbol"], kind, true, graphRuleName, path+".symbol")
227
+ problems = append(problems, symbolProblems...)
228
+ } else if kind == artifactSwagger {
229
+ if _, exists := object["files"]; exists {
230
+ problems = append(
231
+ problems,
232
+ "Invalid evidence/graph configuration at "+path+".files: a Swagger reference owns one document; use singular 'file' and a reference array for multiple documents.",
233
+ )
234
+ }
235
+ if _, exists := object["symbol"]; exists {
236
+ problems = append(
237
+ problems,
238
+ "Invalid evidence/graph configuration at "+path+".symbol: Swagger references select every operation and do not accept a symbol selector.",
239
+ )
240
+ }
241
+ var sourceProblem string
242
+ source, sourceProblem = decodeSwaggerSource(object["file"], path+".file")
243
+ if sourceProblem != "" {
244
+ problems = append(problems, sourceProblem)
245
+ }
246
+ symbols["operation"] = true
247
+ } else {
248
+ if _, exists := object["file"]; exists {
249
+ problems = append(
250
+ problems,
251
+ "Invalid evidence/graph configuration at "+path+".file: singular 'file' is only supported by Swagger references; Markdown and TypeScript references use 'files' globs.",
252
+ )
253
+ }
254
+ var fileProblems []string
255
+ files, fileProblems = decodeFiles(object["files"], path+".files")
256
+ problems = append(problems, fileProblems...)
257
+ var symbolProblems []string
258
+ symbols, symbolProblems = decodeSymbols(object["symbol"], kind, true, graphRuleName, path+".symbol")
259
+ problems = append(problems, symbolProblems...)
260
+ }
261
+ if len(problems) != 0 {
262
+ return referenceSpec{}, problems
263
+ }
264
+ return referenceSpec{
265
+ Index: index,
266
+ Type: kind,
267
+ Policy: policy,
268
+ Root: root,
269
+ Files: files,
270
+ Source: source,
271
+ Package: packageName,
272
+ Symbols: symbols,
273
+ }, nil
274
+ }
275
+
276
+ // decodeReferencePolicy validates the reference-local acknowledgement options
277
+ // before a disabled claim is filtered. Each option is a flat boolean whose
278
+ // false value means "not configured", so an omitted option and an explicit
279
+ // false preserve the original behavior identically.
280
+ func decodeReferencePolicy(
281
+ object map[string]json.RawMessage,
282
+ path string,
283
+ ) (referencePolicy, []string) {
284
+ policy := referencePolicy{}
285
+ problems := []string{}
286
+ decodeFlag := func(name string, target *bool) {
287
+ value, exists := object[name]
288
+ if !exists {
289
+ return
290
+ }
291
+ // A JSON null decodes into Go's false without complaint, which would
292
+ // make a broken generator's output indistinguishable from an option
293
+ // nobody wrote. Only the two literals are the contract.
294
+ switch string(bytes.TrimSpace(value)) {
295
+ case "true":
296
+ *target = true
297
+ case "false":
298
+ default:
299
+ problems = append(problems, configurationProblem(
300
+ graphRuleName,
301
+ path+"."+name,
302
+ "expected a boolean.",
303
+ ))
304
+ }
305
+ }
306
+ decodeFlag("noEvidenceExclude", &policy.NoExclude)
307
+ decodeFlag("uniqueEvidence", &policy.UniqueEvidence)
308
+ decodeFlag("singleEvidencePerSymbol", &policy.SingleEvidencePerSymbol)
309
+ return policy, problems
310
+ }
311
+
312
+ // rejectForeignTypeScriptReference refuses a code population to a claim that
313
+ // cannot address one.
314
+ //
315
+ // A symbol is cited through an inline link, and that link resolves in the
316
+ // citing module's import scope — which only a TypeScript file has. Every other
317
+ // claim would have to fall back to matching the bare name against one
318
+ // repository-wide table, and that makes symbol-name uniqueness across the whole
319
+ // repository load-bearing: two modules exporting `IPage` make the citation
320
+ // impossible, and the only repair such a diagnostic can offer is renaming
321
+ // production code to suit a lint rule.
322
+ //
323
+ // The check is here rather than at resolution because a configuration error
324
+ // belongs where the configuration is read. Reported once per reference, before
325
+ // any file is opened, instead of once per citation that later fails to resolve.
326
+ //
327
+ // What this gives up is the decision it reverses: documentation can no longer
328
+ // cite code, and the inverse obligation is not the same one. Do not restore the fallback
329
+ // without restoring that record.
330
+ func rejectForeignTypeScriptReference(
331
+ claimKind artifactKind,
332
+ referenceKind artifactKind,
333
+ path string,
334
+ ) string {
335
+ if referenceKind != artifactTypeScript ||
336
+ claimKind == artifactTypeScript ||
337
+ claimKind == "" {
338
+ return ""
339
+ }
340
+ return "Invalid evidence/graph configuration at " + path +
341
+ ".type: only a TypeScript claim can cite TypeScript evidence, because a symbol citation resolves through the citing module's imports and a " +
342
+ string(claimKind) +
343
+ " comment has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript."
344
+ }
345
+
346
+ // decodeTypeScriptReference reads the two ways a TypeScript population is
347
+ // selected: a local or package-relative glob set, or a package's own
348
+ // declaration entry.
349
+ //
350
+ // Both reduce to "produce a file set, then materialize its exported symbols";
351
+ // only the selection differs. `package` moves the base the globs resolve
352
+ // against, so it composes with them rather than replacing them.
353
+ func decodeTypeScriptReference(
354
+ object map[string]json.RawMessage,
355
+ path string,
356
+ ) (referenceSpec, []string) {
357
+ problems := []string{}
358
+ reference := referenceSpec{}
359
+ if raw, exists := object["package"]; exists {
360
+ var value string
361
+ if err := json.Unmarshal(raw, &value); err != nil {
362
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+".package: expected an installed package name.")
363
+ } else if name, problem := normalizePackageName(value); problem != "" {
364
+ problems = append(problems, "Invalid evidence/graph configuration at "+path+".package: "+problem)
365
+ } else {
366
+ reference.Package = name
367
+ }
368
+ }
369
+ if _, exists := object["file"]; exists {
370
+ problems = append(
371
+ problems,
372
+ "Invalid evidence/graph configuration at "+path+".file: singular 'file' is only supported by Swagger references; a TypeScript reference selects its population with 'files' globs.",
373
+ )
374
+ }
375
+ if _, exists := object["files"]; exists {
376
+ files, fileProblems := decodeFiles(object["files"], path+".files")
377
+ problems = append(problems, fileProblems...)
378
+ reference.Files = files
379
+ return reference, problems
380
+ }
381
+ if reference.Package == "" {
382
+ problems = append(
383
+ problems,
384
+ "Invalid evidence/graph configuration at "+path+": a local TypeScript reference needs 'files' globs. There is no implicit project population.",
385
+ )
386
+ }
387
+ // A package with no globs falls back to its own declaration entry, which is
388
+ // the only selection a package can make on the consumer's behalf.
389
+ return reference, problems
390
+ }
391
+
392
+ func normalizePackageName(value string) (string, string) {
393
+ if value == "" {
394
+ return "", "the package name must not be empty."
395
+ }
396
+ if strings.TrimSpace(value) != value {
397
+ return "", "the package name must not have leading or trailing whitespace."
398
+ }
399
+ normalized := strings.ReplaceAll(value, "\\", "/")
400
+ if strings.HasPrefix(normalized, ".") || strings.HasPrefix(normalized, "/") {
401
+ return "", "'" + value + "' is a path rather than a package name; use 'files' for a local population."
402
+ }
403
+ segments := strings.Split(normalized, "/")
404
+ limit := 1
405
+ if strings.HasPrefix(segments[0], "@") {
406
+ limit = 2
407
+ }
408
+ if len(segments) > limit {
409
+ return "", "'" + value + "' names a path inside a package; select the package and narrow it with 'files'."
410
+ }
411
+ if len(segments) < limit {
412
+ return "", "'" + value + "' is an incomplete scoped package name."
413
+ }
414
+ return normalized, ""
415
+ }
416
+
417
+ func decodeArtifactKind(
418
+ raw json.RawMessage,
419
+ path string,
420
+ allowSwagger bool,
421
+ ) (artifactKind, string) {
422
+ if len(bytes.TrimSpace(raw)) == 0 {
423
+ return "", "Invalid evidence/graph configuration at " + path + ": the artifact discriminator is required."
424
+ }
425
+ var value string
426
+ if err := json.Unmarshal(raw, &value); err != nil {
427
+ return "", "Invalid evidence/graph configuration at " + path + ": expected a supported artifact type string."
428
+ }
429
+ switch artifactKind(value) {
430
+ case artifactMarkdown, artifactPrisma, artifactTypeScript:
431
+ return artifactKind(value), ""
432
+ case artifactSwagger:
433
+ if allowSwagger {
434
+ return artifactSwagger, ""
435
+ }
436
+ return "", "Invalid evidence/graph configuration at " + path + ": Swagger is evidence-only and cannot be a claim; expected 'markdown', 'prisma', or 'typescript'."
437
+ default:
438
+ expected := "'markdown', 'prisma', or 'typescript'"
439
+ if allowSwagger {
440
+ expected = "'markdown', 'prisma', 'swagger', or 'typescript'"
441
+ }
442
+ return "", "Invalid evidence/graph configuration at " + path + ": unsupported artifact type '" + value + "'; expected " + expected + "."
443
+ }
444
+ }
445
+
446
+ // decodeClaimRoot admits a Program-backed TypeScript population without
447
+ // widening the Program itself.
448
+ //
449
+ // The root changes the address space and glob base only. The inventory still
450
+ // consists exclusively of ctx.Sources, so a sibling package must be an explicit
451
+ // tsconfig root and no configured directory becomes a filesystem scan.
452
+ func decodeClaimRoot(
453
+ raw json.RawMessage,
454
+ kind artifactKind,
455
+ configPath string,
456
+ ) (string, []string) {
457
+ if len(bytes.TrimSpace(raw)) == 0 {
458
+ return "", nil
459
+ }
460
+ if kind != artifactTypeScript {
461
+ return decodeRoot(raw, kind, configPath)
462
+ }
463
+ return decodePopulationRoot(raw, configPath)
464
+ }
465
+
466
+ // decodeRoot reads the directory a Markdown or Prisma population resolves
467
+ // against.
468
+ //
469
+ // The property is refused on the two artifact kinds that cannot use it, and
470
+ // each refusal names the channel that artifact does have. A TypeScript
471
+ // population is materialized from the ttsc Program, so a directory outside the
472
+ // project contains no file it could ever reach — `package` is the escape there,
473
+ // and it already exists. A Swagger reference names one exact document, so the
474
+ // location belongs in `file`, where it is visible without a second property.
475
+ func decodeRoot(
476
+ raw json.RawMessage,
477
+ kind artifactKind,
478
+ configPath string,
479
+ ) (string, []string) {
480
+ if len(bytes.TrimSpace(raw)) == 0 {
481
+ return "", nil
482
+ }
483
+ switch kind {
484
+ case artifactMarkdown, artifactPrisma:
485
+ case artifactTypeScript:
486
+ return "", []string{
487
+ "Invalid evidence/graph configuration at " + configPath + ": a TypeScript reference selects the active ttsc program with 'files', or an installed package with 'package'; only a TypeScript claim accepts 'root'.",
488
+ }
489
+ case artifactSwagger:
490
+ return "", []string{
491
+ "Invalid evidence/graph configuration at " + configPath + ": a Swagger reference owns one exact document; write the ancestor-relative or absolute location in 'file' instead.",
492
+ }
493
+ default:
494
+ // The discriminator is already reported; a second message about a
495
+ // property of an unknown artifact kind would only add noise.
496
+ return "", nil
497
+ }
498
+ return decodePopulationRoot(raw, configPath)
499
+ }
500
+
501
+ func decodePopulationRoot(
502
+ raw json.RawMessage,
503
+ configPath string,
504
+ ) (string, []string) {
505
+ var value string
506
+ if err := json.Unmarshal(raw, &value); err != nil {
507
+ return "", []string{
508
+ "Invalid evidence/graph configuration at " + configPath + ": expected one directory path this population's globs resolve against.",
509
+ }
510
+ }
511
+ normalized, problem := normalizeRootPath(value)
512
+ if problem != "" {
513
+ return "", []string{"Invalid evidence/graph configuration at " + configPath + ": " + problem}
514
+ }
515
+ return normalized, nil
516
+ }
517
+
518
+ func decodeSwaggerSource(raw json.RawMessage, configPath string) (string, string) {
519
+ if len(bytes.TrimSpace(raw)) == 0 {
520
+ return "", "Invalid evidence/graph configuration at " + configPath + ": the required Swagger file path or URL is missing."
521
+ }
522
+ var value string
523
+ if err := json.Unmarshal(raw, &value); err != nil {
524
+ return "", "Invalid evidence/graph configuration at " + configPath + ": expected one exact file path or http(s) URL."
525
+ }
526
+ source, problem := normalizeSwaggerSource(value)
527
+ if problem != "" {
528
+ return "", "Invalid evidence/graph configuration at " + configPath + ": " + problem
529
+ }
530
+ return source, ""
531
+ }
532
+
533
+ // normalizeSwaggerSource reduces a declared Swagger location to one canonical
534
+ // spelling, without deciding where on the filesystem it may sit.
535
+ //
536
+ // A local path is free to ascend with `..` or to be absolute, and the ordering
537
+ // this restores is the point: the rule already accepts an arbitrary http(s) URL
538
+ // on any host, so refusing `../contracts/swagger.json` refused the one form the
539
+ // author can pin, version, and diff. An OpenAPI document is routinely generated
540
+ // somewhere with no relationship to the project that consumes it — a sibling
541
+ // package's output, a shared contract checkout, a CI artifact directory — and
542
+ // none of those is reachable through a project-relative path.
543
+ //
544
+ // A Windows drive prefix is recognized before the URL parse rather than after
545
+ // it. `url.Parse` reads the single letter of `C:/api/swagger.json` as a scheme,
546
+ // so an absolute Windows path would otherwise be rejected as an unsupported
547
+ // protocol — a diagnostic naming the wrong thing entirely. The drive-relative
548
+ // form stays refused for the reason `files` refuses it: it resolves against
549
+ // whatever directory that drive currently sits on.
550
+ func normalizeSwaggerSource(value string) (string, string) {
551
+ if value == "" {
552
+ return "", "Swagger sources must not be empty."
553
+ }
554
+ if strings.TrimSpace(value) != value {
555
+ return "", "Swagger sources must not have leading or trailing whitespace."
556
+ }
557
+ normalized := strings.ReplaceAll(value, "\\", "/")
558
+ drive := hasWindowsDrivePrefix(normalized)
559
+ if drive && !strings.HasPrefix(normalized[2:], "/") {
560
+ return "", "local Swagger path '" + value + "' is drive-relative, so it resolves against whatever directory that drive currently sits on rather than against a stable base. Write the full path."
561
+ }
562
+ parsed, err := url.Parse(value)
563
+ if err != nil {
564
+ return "", "invalid Swagger source '" + value + "': " + err.Error() + "."
565
+ }
566
+ if !drive && parsed.Scheme != "" {
567
+ if parsed.Scheme != "http" && parsed.Scheme != "https" {
568
+ return "", "unsupported URL scheme '" + parsed.Scheme + "'; only http: and https: are supported."
569
+ }
570
+ if parsed.Host == "" {
571
+ return "", "Swagger URL '" + value + "' has no host."
572
+ }
573
+ if parsed.Fragment != "" {
574
+ return "", "Swagger URL '" + value + "' must not contain a fragment."
575
+ }
576
+ return value, ""
577
+ }
578
+ if !drive && strings.Contains(value, "://") {
579
+ return "", "invalid Swagger source URL '" + value + "'."
580
+ }
581
+ // A trailing separator is read before cleaning, because `path.Clean` removes
582
+ // it — after which `docs/` and `docs` are one string and the author who
583
+ // meant a directory gets a missing-file diagnostic instead.
584
+ directory := strings.HasSuffix(normalized, "/")
585
+ // A UNC share is cleaned by hand, because `path.Clean` collapses its leading
586
+ // `//` into one slash and turns a network location into a local one that
587
+ // still reads like what was written.
588
+ if strings.HasPrefix(normalized, "//") {
589
+ normalized = "//" + strings.TrimPrefix(path.Clean(normalized), "/")
590
+ } else {
591
+ normalized = path.Clean(normalized)
592
+ }
593
+ switch {
594
+ case directory,
595
+ normalized == ".",
596
+ normalized == "..",
597
+ normalized == "/",
598
+ strings.HasSuffix(normalized, "/.."),
599
+ len(normalized) == 2 && hasWindowsDrivePrefix(normalized):
600
+ return "", "Swagger source '" + value + "' names a directory rather than a document; a reference owns one exact file."
601
+ }
602
+ return normalized, ""
603
+ }
604
+
605
+ func decodeFiles(raw json.RawMessage, path string) (globSet, []string) {
606
+ if len(bytes.TrimSpace(raw)) == 0 {
607
+ return globSet{}, []string{"Invalid evidence/graph configuration at " + path + ": the required project-relative glob array is missing."}
608
+ }
609
+ var patterns []string
610
+ if err := json.Unmarshal(raw, &patterns); err != nil {
611
+ return globSet{}, []string{"Invalid evidence/graph configuration at " + path + ": expected an array of project-relative glob strings."}
612
+ }
613
+ if len(patterns) == 0 {
614
+ return globSet{}, []string{"Invalid evidence/graph configuration at " + path + ": at least one positive glob is required."}
615
+ }
616
+ globs, err := newGlobSet(patterns)
617
+ if err != nil {
618
+ return globSet{}, []string{"Invalid evidence/graph configuration at " + path + ": " + err.Error()}
619
+ }
620
+ return globs, nil
621
+ }
622
+
623
+ func decodeSymbols(
624
+ raw json.RawMessage,
625
+ kind artifactKind,
626
+ unit bool,
627
+ ruleName string,
628
+ path string,
629
+ ) (symbolSet, []string) {
630
+ if kind == "" {
631
+ return nil, nil
632
+ }
633
+ values := []string{}
634
+ if len(bytes.TrimSpace(raw)) == 0 {
635
+ switch {
636
+ case kind == artifactMarkdown:
637
+ values = []string{"file", "h1", "h2", "h3", "h4"}
638
+ // A Prisma reference owes coverage per model, not per column. The
639
+ // prior art cites at model granularity for the same reason
640
+ // (`AutoBeDatabase.IModel.evidence`), and defaulting to every member
641
+ // would put `id`, `created_at`, and every back-reference into the
642
+ // denominator — which teaches an author to write filler reasons. A
643
+ // claim keeps every host, because there the selector narrows where a
644
+ // tag may sit rather than what must be covered.
645
+ case kind == artifactPrisma && unit:
646
+ values = []string{"model"}
647
+ case kind == artifactPrisma:
648
+ values = []string{"model", "column", "relation"}
649
+ case kind == artifactTypeScript && unit:
650
+ values = []string{"type"}
651
+ default:
652
+ values = []string{"type", "function", "property"}
653
+ }
654
+ } else {
655
+ trimmed := bytes.TrimSpace(raw)
656
+ switch trimmed[0] {
657
+ case '"':
658
+ var value string
659
+ if err := json.Unmarshal(raw, &value); err != nil {
660
+ return nil, []string{configurationProblem(ruleName, path, "expected a supported symbol string or array.")}
661
+ }
662
+ values = []string{value}
663
+ case '[':
664
+ if err := json.Unmarshal(raw, &values); err != nil {
665
+ return nil, []string{configurationProblem(ruleName, path, "expected a supported symbol string or array.")}
666
+ }
667
+ if len(values) == 0 {
668
+ return nil, []string{configurationProblem(ruleName, path, "an empty symbol array selects no evidence units or declaration hosts.")}
669
+ }
670
+ default:
671
+ return nil, []string{configurationProblem(ruleName, path, "expected a supported symbol string or array.")}
672
+ }
673
+ }
674
+ allowed := map[string]bool{}
675
+ supported := []string{"type", "function", "property"}
676
+ switch kind {
677
+ case artifactMarkdown:
678
+ supported = []string{"file", "h1", "h2", "h3", "h4"}
679
+ case artifactPrisma:
680
+ supported = []string{"model", "column", "relation"}
681
+ }
682
+ for _, symbol := range supported {
683
+ allowed[symbol] = true
684
+ }
685
+ set := symbolSet{}
686
+ problems := []string{}
687
+ for _, value := range values {
688
+ if !allowed[value] {
689
+ problems = append(problems, configurationProblem(ruleName, path, "symbol '"+value+"' is not supported for "+string(kind)+"."))
690
+ continue
691
+ }
692
+ set[value] = true
693
+ }
694
+ return set, problems
695
+ }
696
+
697
+ func decodeObject(raw json.RawMessage, path string) (map[string]json.RawMessage, string) {
698
+ var object map[string]json.RawMessage
699
+ if err := json.Unmarshal(raw, &object); err != nil || object == nil {
700
+ return nil, "Invalid evidence/graph configuration at " + path + ": expected an object."
701
+ }
702
+ return object, ""
703
+ }
704
+
705
+ // configurationProblem opens a configuration diagnostic with the rule that owns
706
+ // the setting.
707
+ //
708
+ // The owning rule is a parameter because two rules share these decoders. A
709
+ // message naming a rule the reader did not configure sends them to edit a
710
+ // setting that is not wrong, and with several rules enabled it is not even
711
+ // obvious which one lied.
712
+ func configurationProblem(ruleName string, path string, message string) string {
713
+ return "Invalid " + ruleName + " configuration at " + path + ": " + message
714
+ }
715
+
716
+ func rejectUnknownFields(
717
+ object map[string]json.RawMessage,
718
+ allowed []string,
719
+ ruleName string,
720
+ path string,
721
+ ) []string {
722
+ known := map[string]bool{}
723
+ for _, name := range allowed {
724
+ known[name] = true
725
+ }
726
+ unknown := []string{}
727
+ for name := range object {
728
+ if !known[name] {
729
+ unknown = append(unknown, name)
730
+ }
731
+ }
732
+ sort.Strings(unknown)
733
+ problems := make([]string, 0, len(unknown))
734
+ for _, name := range unknown {
735
+ if name == "severity" {
736
+ problems = append(problems, configurationProblem(
737
+ ruleName,
738
+ path+".severity",
739
+ "severity belongs only in the outer @ttsc/lint rule setting.",
740
+ ))
741
+ continue
742
+ }
743
+ // The inverted-relation hints below name properties that only ever
744
+ // existed on the graph, so offering them elsewhere would invite a
745
+ // reader to migrate a setting their rule never had.
746
+ if name == "sources" && ruleName == graphRuleName {
747
+ problems = append(problems, configurationProblem(
748
+ ruleName,
749
+ path+".sources",
750
+ "the graph is now declared from the claiming side; declare 'claims', each citing its evidence under 'reference'.",
751
+ ))
752
+ continue
753
+ }
754
+ if name == "citedBy" && ruleName == graphRuleName {
755
+ problems = append(problems, configurationProblem(
756
+ ruleName,
757
+ path+".citedBy",
758
+ "this relation was inverted; declare the evidence this claim cites under 'reference'.",
759
+ ))
760
+ continue
761
+ }
762
+ problems = append(problems, configurationProblem(
763
+ ruleName,
764
+ path+"."+name,
765
+ "unknown property; expected only "+strings.Join(allowed, ", ")+".",
766
+ ))
767
+ }
768
+ return problems
769
+ }
770
+
771
+ func describePatterns(globs globSet) string {
772
+ quoted := make([]string, 0, len(globs.Patterns))
773
+ for _, pattern := range globs.Patterns {
774
+ quoted = append(quoted, "'"+pattern.Raw+"'")
775
+ }
776
+ return "[" + strings.Join(quoted, ", ") + "]"
777
+ }
778
+
779
+ // describePopulation names the patterns a population selects with, and the base
780
+ // they were resolved against when that base is not the project root.
781
+ //
782
+ // The base is stated rather than assumed, because a citation that resolves
783
+ // outside the project has to be repairable from the diagnostic alone: patterns
784
+ // that look correct against a root the reader is imagining are the failure this
785
+ // property introduces, and naming the resolved base is what removes it.
786
+ func describePopulation(base populationBase, globs globSet) string {
787
+ if base.Default {
788
+ return describePatterns(globs)
789
+ }
790
+ return describePatterns(globs) + " under root '" + populationRootLabel(base) + "'"
791
+ }
792
+
793
+ func describeReferenceSources(reference referenceSpec) string {
794
+ if reference.Type != artifactSwagger {
795
+ return describePopulation(reference.Base, reference.Files)
796
+ }
797
+ return "'" + displaySwaggerSource(reference.Source) + "'"
798
+ }