@ttsc/evidence 0.30.4 → 0.31.1

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
@@ -0,0 +1,223 @@
1
+ package evidence
2
+
3
+ import (
4
+ "io/fs"
5
+ "sort"
6
+ "strconv"
7
+ "strings"
8
+ )
9
+
10
+ // markdownCapture owns the first Markdown observations of one Check. Its
11
+ // inventories retain the first consumed bytes' interpretation and address;
12
+ // walking another population never rereads an already captured address.
13
+ // Different file selections may require another walk, while equal selections
14
+ // replay the original walk, including failed observations and pruning.
15
+ //
16
+ // The host keeps the witness of each actual operation. This capture neither
17
+ // certifies freshness nor restores withdrawn authority. An edit after an
18
+ // observation belongs to a later Check; downstream reusable delivery still
19
+ // validates the original witnesses. Hints projects the same captured snapshot.
20
+ //
21
+ // Base and artifact-address keys keep citation spaces distinct, while ordered
22
+ // file-glob semantics determine traversal sharing. Loaders apply symbol and
23
+ // severity policy anew. Native paths and directory entries come from the
24
+ // ordinary input reader without an inferred case or symlink policy.
25
+ //
26
+ // A capture parses each addressed file once. Replaying recorded entries costs
27
+ // a traversal of those entries without raw reads or parsing; new file selections
28
+ // may require another discovery. Key construction depends on glob text and
29
+ // population sizes, including sorting distinct ordered glob sets.
30
+ //
31
+ // Check releases its maps and traversal records when evaluation ends. Their
32
+ // storage grows with addressed files, parsed bytes, selected traversal entries
33
+ // and distinct file selections. A clean Corpus retains the inventories needed
34
+ // for Hints. The capture owns no descriptor or task.
35
+ type markdownCapture struct {
36
+ bases map[string]markdownCapturedBase
37
+ walks map[string]markdownCapturedWalk
38
+ inventories map[string]map[string]markdownCapturedInventory
39
+ walkFailures map[string]error
40
+ walkErrors map[string]map[string]error
41
+ }
42
+
43
+ type markdownCapturedBase struct {
44
+ from string
45
+ problem string
46
+ }
47
+
48
+ type markdownCapturedWalk struct {
49
+ entries []markdownWalkObservation
50
+ err error
51
+ }
52
+
53
+ type markdownWalkObservation struct {
54
+ path string
55
+ entry fs.DirEntry
56
+ err error
57
+ }
58
+
59
+ type markdownCapturedInventory struct {
60
+ inventory *artifactInventory
61
+ err error
62
+ }
63
+
64
+ // resolve retains the first base resolution, including an unusable root.
65
+ // Base.Absolute is the existing declared address-space identity, so physical
66
+ // aliases are not collapsed by this capture.
67
+ func (capture *markdownCapture) resolve(base populationBase) (string, string) {
68
+ if prior, ok := capture.bases[base.Absolute]; ok {
69
+ return prior.from, prior.problem
70
+ }
71
+ observation := markdownCapturedBase{problem: baseDirectoryProblem(base, artifactMarkdown)}
72
+ if observation.problem == "" {
73
+ var resolved bool
74
+ observation.from, resolved = resolvedBaseDirectory(base)
75
+ if !resolved {
76
+ observation.problem = unresolvedBaseProblem(base, artifactMarkdown)
77
+ }
78
+ }
79
+ if capture.bases == nil {
80
+ capture.bases = map[string]markdownCapturedBase{}
81
+ }
82
+ capture.bases[base.Absolute] = observation
83
+ return observation.from, observation.problem
84
+ }
85
+
86
+ // walk replays only a traversal with the same file-selection semantics. The
87
+ // loader's traversal decisions depend on those globs, not its symbol/severity
88
+ // projection. A new selection delegates discovery to the ordinary reader.
89
+ func (capture *markdownCapture) walk(base populationBase, from string, config graphConfig, visit fs.WalkDirFunc) error {
90
+ key := markdownWalkKey(base, config)
91
+ if prior, ok := capture.walks[key]; ok {
92
+ for _, observation := range prior.entries {
93
+ if err := visit(observation.path, observation.entry, observation.err); err != nil && err != fs.SkipDir {
94
+ return err
95
+ }
96
+ }
97
+ return prior.err
98
+ }
99
+ if err := capture.walkFailures[base.Absolute]; err != nil {
100
+ return err
101
+ }
102
+ observation := markdownCapturedWalk{}
103
+ observedErrors := map[string]bool{}
104
+ observation.err = base.inputs.WalkDir(from, func(path string, entry fs.DirEntry, err error) error {
105
+ if prior := capture.walkErrors[base.Absolute][path]; prior != nil {
106
+ err = prior
107
+ } else if err != nil {
108
+ if capture.walkErrors == nil {
109
+ capture.walkErrors = map[string]map[string]error{}
110
+ }
111
+ if capture.walkErrors[base.Absolute] == nil {
112
+ capture.walkErrors[base.Absolute] = map[string]error{}
113
+ }
114
+ capture.walkErrors[base.Absolute][path] = err
115
+ }
116
+ if err != nil {
117
+ observedErrors[path] = true
118
+ }
119
+ observation.entries = append(observation.entries, markdownWalkObservation{path: path, entry: entry, err: err})
120
+ return visit(path, entry, err)
121
+ })
122
+ // Broader discovery can omit an earlier failed entry after a deletion or
123
+ // pruning change. Its first failure still belongs to this snapshot; replay
124
+ // it through the new phase's relevance, severity and population-health gate.
125
+ if observation.err == nil {
126
+ for path, err := range capture.walkErrors[base.Absolute] {
127
+ if observedErrors[path] {
128
+ continue
129
+ }
130
+ observation.entries = append(observation.entries, markdownWalkObservation{path: path, err: err})
131
+ if result := visit(path, nil, err); result != nil && result != fs.SkipDir {
132
+ observation.err = result
133
+ break
134
+ }
135
+ }
136
+ }
137
+ if observation.err != nil {
138
+ if capture.walkFailures == nil {
139
+ capture.walkFailures = map[string]error{}
140
+ }
141
+ capture.walkFailures[base.Absolute] = observation.err
142
+ }
143
+ if capture.walks == nil {
144
+ capture.walks = map[string]markdownCapturedWalk{}
145
+ }
146
+ capture.walks[key] = observation
147
+ return observation.err
148
+ }
149
+
150
+ // read retains successful parses and failed reads alike. The first actual
151
+ // reader invocation supplies the only consumed-byte witness for this address;
152
+ // the capture never retries a failure in another phase of the same Check.
153
+ func (capture *markdownCapture) read(address artifactAddress, current string) (*artifactInventory, error) {
154
+ if prior, ok := capture.inventories[address.Base.Absolute][address.Relative]; ok {
155
+ return prior.inventory, prior.err
156
+ }
157
+ content, err := address.Base.inputs.ReadFile(current)
158
+ var inventory *artifactInventory
159
+ if err == nil {
160
+ inventory, _ = scanMarkdownInventory(address, string(content))
161
+ } else {
162
+ inventory = &artifactInventory{Path: address.Display, Type: artifactMarkdown, LoadFailed: true}
163
+ }
164
+ if capture.inventories == nil {
165
+ capture.inventories = map[string]map[string]markdownCapturedInventory{}
166
+ }
167
+ if capture.inventories[address.Base.Absolute] == nil {
168
+ capture.inventories[address.Base.Absolute] = map[string]markdownCapturedInventory{}
169
+ }
170
+ capture.inventories[address.Base.Absolute][address.Relative] = markdownCapturedInventory{inventory: inventory, err: err}
171
+ return inventory, err
172
+ }
173
+
174
+ // markdownWalkKey canonicalizes the union of ordered glob sets for one base.
175
+ // Each set keeps include/exclude order; sorting and deduplicating whole sets
176
+ // reflects that different populations are combined by union during traversal.
177
+ func markdownWalkKey(base populationBase, config graphConfig) string {
178
+ sets := map[string]bool{}
179
+ add := func(candidate populationBase, files globSet) {
180
+ if candidate.Absolute != base.Absolute {
181
+ return
182
+ }
183
+ patterns := make([]string, 0, len(files.Patterns))
184
+ for _, pattern := range files.Patterns {
185
+ value := strings.Join(pattern.Segments, "/")
186
+ if pattern.Exclude {
187
+ value = "1:" + value
188
+ } else {
189
+ value = "0:" + value
190
+ }
191
+ patterns = append(patterns, value)
192
+ }
193
+ var key strings.Builder
194
+ for _, pattern := range patterns {
195
+ key.WriteString(strconv.Itoa(len(pattern)))
196
+ key.WriteByte(':')
197
+ key.WriteString(pattern)
198
+ }
199
+ sets[key.String()] = true
200
+ }
201
+ for _, claim := range config.Claims {
202
+ if claim.Type == artifactMarkdown {
203
+ add(claim.Base, claim.Files)
204
+ }
205
+ for _, reference := range claim.References {
206
+ if reference.Type == artifactMarkdown {
207
+ add(reference.Base, reference.Files)
208
+ }
209
+ }
210
+ }
211
+ keys := make([]string, 0, len(sets))
212
+ for key := range sets {
213
+ keys = append(keys, key)
214
+ }
215
+ sort.Strings(keys)
216
+ var key strings.Builder
217
+ for _, part := range append([]string{base.Absolute}, keys...) {
218
+ key.WriteString(strconv.Itoa(len(part)))
219
+ key.WriteByte(':')
220
+ key.WriteString(part)
221
+ }
222
+ return key.String()
223
+ }
package/native/model.go CHANGED
@@ -54,6 +54,7 @@ const (
54
54
  )
55
55
 
56
56
  type graphConfig struct {
57
+ inputs evidenceInputReader
57
58
  Claims []claimSpec
58
59
  }
59
60
 
@@ -282,7 +283,7 @@ type evidenceUnit struct {
282
283
  // So a nested change moves both its own unit's digest and every enclosing
283
284
  // unit's. `scopeIndex` composes ancestors and descendants anyway, so detection
284
285
  // is unaffected, but nothing here may assume a unit's digest is independent of
285
- // its subtree. A feature built on that assumption was reverted once already.
286
+ // its subtree.
286
287
  //
287
288
  // Removing the documentation comment is what stops the review from
288
289
  // invalidating itself: writing an `@evidenceReview` inside a unit that is
package/native/prisma.go CHANGED
@@ -163,15 +163,32 @@ func loadPrismaInventories(
163
163
  // resident host repeats this every cycle, so an unchanged schema would
164
164
  // otherwise be re-parsed on every TypeScript keystroke that rebuilds.
165
165
  severity := prismaSetSeverity(config, inventories)
166
- digest := prismaContentDigest(root, set.Sources)
166
+ // The bridge owns additional module/file reads outside this native reader.
167
+ // Neither a memo hit nor a later disk hash proves those producer inputs.
168
+ config.inputs.Unavailable()
169
+ digest := prismaContentDigest(root, set.Sources, config.inputs)
170
+ trace := newEvidenceBridgeTrace("prisma")
171
+ if trace != nil {
172
+ trace.nativeLookup = true
173
+ trace.record("bridge-lookup", os.Getpid(), map[string]any{
174
+ "bridge": "prisma", "root": root, "requestId": prismaSetID,
175
+ "sources": set.Sources, "nativeDigest": digest, "nativeLookup": true,
176
+ })
177
+ }
167
178
  if outcome, hit := prismaSchemas.lookup(digest); hit {
179
+ if trace != nil {
180
+ trace.record("bridge-cache-hit", os.Getpid(), map[string]any{"bridge": "prisma", "nativeDigest": digest})
181
+ }
168
182
  return inventories, append(
169
183
  problems,
170
184
  prismaUnitsFromOutcome(root, set, inventories, outcome, config)...,
171
185
  )
172
186
  }
173
187
 
174
- result, err := normalizePrismaSet(root, set.Sources)
188
+ if trace != nil {
189
+ trace.record("bridge-cache-miss", os.Getpid(), map[string]any{"bridge": "prisma", "nativeDigest": digest})
190
+ }
191
+ result, err := normalizePrismaSet(root, set.Sources, trace)
175
192
  if err != nil {
176
193
  message := "Evidence graph could not run its Prisma schema loader: " + causeText(err) + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
177
194
  return inventories, problems.add(severity, failPrismaSet(inventories, set, message))
@@ -221,9 +238,12 @@ func prismaOutcomeOf(result prismaNormalizationResult) (prismaSetOutcome, string
221
238
  }
222
239
 
223
240
  // configuredPrismaAddresses lists the schema files every configured Prisma glob
224
- // selects, claim and reference alike, once per population base.
241
+ // selects, claim and reference alike, once per population base, and drops the
242
+ // list of bases that could not be listed.
225
243
  //
226
- // Order is the digest's, so it must not depend on filesystem enumeration.
244
+ // The list is ordered by address key rather than by filesystem enumeration, so
245
+ // the same configuration answers in one order. The order of the parsed set is
246
+ // not taken from here: `distinctPrismaSources` sorts the spellings it keeps.
227
247
  func configuredPrismaAddresses(
228
248
  config graphConfig,
229
249
  ) ([]artifactAddress, graphDiagnostics) {
@@ -231,6 +251,9 @@ func configuredPrismaAddresses(
231
251
  return addresses, problems
232
252
  }
233
253
 
254
+ // configuredPrismaAddressesWithHealth is `configuredPrismaAddresses` together
255
+ // with the bases whose listing failed, so a caller can report the population as
256
+ // failed rather than as healthy and empty.
234
257
  func configuredPrismaAddressesWithHealth(
235
258
  config graphConfig,
236
259
  ) ([]artifactAddress, []populationBase, graphDiagnostics) {
@@ -251,7 +274,7 @@ func configuredPrismaAddressesWithHealth(
251
274
  failedBases = append(failedBases, base)
252
275
  continue
253
276
  }
254
- err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
277
+ err := base.inputs.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
255
278
  if walkErr != nil {
256
279
  // The walk root belongs to its population by construction, so a
257
280
  // failure to list it is a failure of the population. The relevance test
@@ -367,7 +390,7 @@ func distinctPrismaSources(root string, addresses []artifactAddress) prismaSourc
367
390
  continue
368
391
  }
369
392
  seen[address.Display] = true
370
- identity, err := os.Stat(resolveProjectPath(root, address.Display))
393
+ identity, err := address.Base.inputs.Stat(resolveProjectPath(root, address.Display))
371
394
  var host *physicalSchema
372
395
  if err == nil {
373
396
  for _, file := range files {
@@ -457,7 +480,8 @@ func prismaInventoriesByDisplay(
457
480
  //
458
481
  // The symbol is `*` rather than `model`, and the difference is load-bearing. A
459
482
  // reference reads an inventory problem only when it selects that problem's
460
- // symbol (`graph.go:169-174`), so a set that failed to parse would look
483
+ // symbol or the problem's symbol is `*` (the reference materializers in
484
+ // `graph.go`), so a set that failed to parse would look
461
485
  // problem-free to a reference selecting only columns or relations — which then
462
486
  // reports that its globs "materialized no selected evidence units", sending the
463
487
  // author to widen a selector when the schema is what could not be read. A
@@ -540,22 +564,42 @@ func couldContainConfiguredPrisma(
540
564
  return false
541
565
  }
542
566
 
567
+ // normalizePrismaSet retains the real Node request/Run/unmarshal boundary.
568
+ // Optional private trace context pairs a caller's already used lookup digest;
569
+ // direct calls allocate an explicitly unpaired observation. Neither context nor
570
+ // observer outcome is added to the request, result or cache identity.
543
571
  func normalizePrismaSet(
544
572
  root string,
545
573
  sources []string,
574
+ traces ...*evidenceBridgeTrace,
546
575
  ) (prismaNormalizationResult, error) {
576
+ var trace *evidenceBridgeTrace
577
+ if len(traces) != 0 {
578
+ trace = traces[0]
579
+ } else {
580
+ trace = newEvidenceBridgeTrace("prisma")
581
+ }
547
582
  request, err := json.Marshal(prismaNormalizationRequest{
548
583
  Root: root,
549
584
  Sets: []prismaSetRequest{{ID: prismaSetID, Files: sources}},
550
585
  })
551
586
  if err != nil {
587
+ trace.preparationFailure(err)
552
588
  return prismaNormalizationResult{}, err
553
589
  }
590
+ if trace != nil {
591
+ trace.record("bridge-request", os.Getpid(), map[string]any{
592
+ "bridge": "prisma", "root": root, "requestId": prismaSetID,
593
+ "sources": sources, "nativeLookup": trace.nativeLookup,
594
+ })
595
+ }
554
596
  node := os.Getenv("TTSC_NODE_BINARY")
555
597
  if node == "" {
556
598
  node, err = exec.LookPath("node")
557
599
  if err != nil {
558
- return prismaNormalizationResult{}, errors.New("Node.js executable was not found")
600
+ failure := errors.New("Node.js executable was not found")
601
+ trace.preparationFailure(failure)
602
+ return prismaNormalizationResult{}, failure
559
603
  }
560
604
  }
561
605
  ctx, cancel := context.WithTimeout(context.Background(), prismaBridgeTimeout)
@@ -567,20 +611,43 @@ func normalizePrismaSet(
567
611
  stderr := &limitedBuffer{Limit: prismaBridgeErrorLimit}
568
612
  command.Stdout = stdout
569
613
  command.Stderr = stderr
570
- if err := command.Run(); err != nil {
571
- if ctx.Err() == context.DeadlineExceeded {
614
+ start := trace.attempt(command)
615
+ runErr := command.Run()
616
+ deadlineExceeded := false
617
+ if runErr != nil {
618
+ deadlineExceeded = ctx.Err() == context.DeadlineExceeded
619
+ }
620
+ end := time.Time{}
621
+ if trace != nil {
622
+ end = time.Now().UTC()
623
+ }
624
+ if runErr != nil {
625
+ trace.result(command, stdout, stderr, start, end, runErr, "not-attempted", nil, nil, nil)
626
+ if deadlineExceeded {
572
627
  return prismaNormalizationResult{}, errors.New("Prisma schema loader exceeded its 60 second timeout")
573
628
  }
574
629
  detail := strings.TrimSpace(stderr.String())
575
630
  if detail == "" {
576
- detail = err.Error()
631
+ detail = runErr.Error()
577
632
  }
578
633
  return prismaNormalizationResult{}, errors.New(detail)
579
634
  }
580
635
  var result prismaNormalizationResult
581
636
  if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
637
+ trace.result(command, stdout, stderr, start, end, nil, "failed", err, nil, nil)
582
638
  return prismaNormalizationResult{}, errors.New("Prisma schema loader returned invalid JSON: " + err.Error())
583
639
  }
640
+ if trace != nil {
641
+ documents := make([]string, 0, len(result.Documents))
642
+ problems := make([]string, 0, len(result.Problems))
643
+ for _, document := range result.Documents {
644
+ documents = append(documents, document.ID)
645
+ }
646
+ for _, problem := range result.Problems {
647
+ problems = append(problems, problem.ID)
648
+ }
649
+ trace.result(command, stdout, stderr, start, end, nil, "succeeded", nil, documents, problems)
650
+ }
584
651
  return result, nil
585
652
  }
586
653
 
@@ -645,9 +712,11 @@ func prismaModelUnits(model prismaModel) []*evidenceUnit {
645
712
  // so both host a citation here. `//` is discarded by Prisma itself and is the
646
713
  // only form that cannot. A top-level unattached `///` run is the one
647
714
  // exclusion-only carrier.
648
- // Every other unusable placement — documenting nothing,
649
- // documenting something this graph does not address, or burying the tag behind
650
- // an extra slash — names the move that fixes it.
715
+ //
716
+ // Every other unusable placement, whether a comment trailing code on its own
717
+ // line, documenting nothing, documenting something this graph does not
718
+ // address, or burying the tag behind an extra slash, names the move that fixes
719
+ // it.
651
720
  func prismaDeclarationsFromComments(
652
721
  comments []prismaCommentRun,
653
722
  hosts map[string]*evidenceUnit,
@@ -659,6 +728,19 @@ func prismaDeclarationsFromComments(
659
728
  for _, run := range comments {
660
729
  severity := levels[run.Path]
661
730
  location := run.Path + ":" + decimal(run.Line)
731
+ if run.Trailing && run.Form != prismaLineComment {
732
+ // Prisma reads a trailing `///` or `/* */` as documentation of the field
733
+ // on its own line, so claiming it documents nothing, or that Prisma
734
+ // discards it, would be false. The graph reads citations only above the
735
+ // declaration, and this says so.
736
+ if prismaCommentCarriesTag(run.Body) {
737
+ problems = problems.add(
738
+ severity,
739
+ "Evidence tag at "+location+" sits in a comment that trails code on its own line. A citation is read only from a documentation comment on the lines directly above the model, column, or relation it grounds, so nothing resolves this one. Move the citation directly above that declaration.",
740
+ )
741
+ }
742
+ continue
743
+ }
662
744
  if run.Form != prismaDocComment {
663
745
  if prismaCommentCarriesTag(run.Body) {
664
746
  problems = problems.add(
@@ -824,8 +906,11 @@ func prismaHostSymbols(host *evidenceUnit) symbolSet {
824
906
  // prismaCommentCarriesTag reports whether a comment body opens a citation on
825
907
  // any of its lines.
826
908
  func prismaCommentCarriesTag(body string) bool {
827
- for _, line := range strings.Split(body, "\n") {
828
- trimmed := strings.TrimSpace(line)
909
+ for _, region := range documentationRegions(body, false) {
910
+ if region.fence {
911
+ continue
912
+ }
913
+ trimmed := region.text
829
914
  if _, _, found := declarationLine(trimmed); found {
830
915
  return true
831
916
  }
@@ -847,8 +932,11 @@ func prismaCommentCarriesTag(body string) bool {
847
932
  // an author reading the file sees a citation that does nothing.
848
933
  func prismaBuriedTagLines(body string) []int {
849
934
  offsets := []int{}
850
- for offset, line := range strings.Split(body, "\n") {
851
- trimmed := strings.TrimSpace(line)
935
+ for offset, region := range documentationRegions(body, false) {
936
+ if region.fence {
937
+ continue
938
+ }
939
+ trimmed := region.text
852
940
  if _, _, found := declarationLine(trimmed); found {
853
941
  continue
854
942
  }
@@ -862,14 +950,11 @@ func prismaBuriedTagLines(body string) []int {
862
950
  // prismaBuriedTag reports whether a line would open a citation once its leading
863
951
  // comment punctuation is removed.
864
952
  //
865
- // Two shapes bury a tag, and both are one keystroke from a citation that works.
953
+ // An extra slash buries a tag one keystroke from a citation that works.
866
954
  // A fourth slash makes `//// @evidence` a doc comment whose text begins with a
867
- // slash, and a JSDoc-style block hands Prisma its own asterisks as content:
868
- // measured, `/** @evidence x */` reaches the parser's documentation as
869
- // `* @evidence x`, and the multi-line form keeps a leading asterisk on every
870
- // line of it. In both the tag no longer opens its line, so nothing parses it —
871
- // and until this stripped asterisks too, nothing reported it either. A schema
872
- // author arriving from JSDoc writes the second shape by habit.
955
+ // slash. The tag no longer opens its line, so the declaration parser cannot
956
+ // read it. JSDoc-style leading asterisks are different: the shared comment
957
+ // parser already strips them, so those block comments host citations normally.
873
958
  //
874
959
  // Only leading punctuation is stripped, so prose that merely mentions the tag
875
960
  // somewhere in a sentence is untouched. Over-reporting an ordinary comment
@@ -3,7 +3,6 @@ package evidence
3
3
  import (
4
4
  "crypto/sha256"
5
5
  "encoding/hex"
6
- "os"
7
6
  "strings"
8
7
  "sync"
9
8
 
@@ -118,17 +117,22 @@ func copyPrismaOutcome(outcome prismaSetOutcome) prismaSetOutcome {
118
117
  //
119
118
  // The composition is the whole cache key, which is what makes staleness
120
119
  // structural rather than improbable: identical bytes in identical order parse
121
- // to identical models, so a hit can only return what the current schema means.
122
- // The path is folded in beside each file's own hash, so moving a model between
123
- // two files of one set — which changes nothing about the bytes as a whole —
124
- // still changes the key, and so does adding or removing a file.
120
+ // to identical models, so a hit returns what the current schema means to the
121
+ // parser that first read it. The key names no project root and no parser
122
+ // installation, deliberately, so equal schemas share one entry across roots; a
123
+ // resident host serving two projects that resolve different parser builds
124
+ // shares the entry between them. The path is folded in beside each file's own
125
+ // hash, so moving a model between two files of one set — which changes nothing
126
+ // about the bytes as a whole — still changes the key, and so does adding or
127
+ // removing a file. That is the key only: a per-unit digest folds in no path.
125
128
  //
126
129
  // An unreadable file yields no digest at all, which keeps the set out of the
127
130
  // cache and leaves the loader's own diagnostic for it.
128
- func prismaContentDigest(root string, sources []string) string {
131
+ func prismaContentDigest(root string, sources []string, readers ...evidenceInputReader) string {
132
+ inputs := inputReader(readers)
129
133
  composite := sha256.New()
130
134
  for _, source := range sources {
131
- content, err := os.ReadFile(resolveProjectPath(root, source))
135
+ content, err := inputs.ReadFile(resolveProjectPath(root, source))
132
136
  if err != nil {
133
137
  return ""
134
138
  }
@@ -187,7 +191,7 @@ func prismaUnitsFromOutcome(
187
191
  prismaNormalizationFailure(outcome.Problem),
188
192
  ))
189
193
  }
190
- locations, comments := locatePrismaDeclarations(root, set.Sources)
194
+ locations, comments := locatePrismaDeclarations(root, set.Sources, config.inputs)
191
195
  fallback := ""
192
196
  if len(set.Sources) != 0 {
193
197
  fallback = set.Sources[0]