@ttsc/evidence 0.30.3 → 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.
- package/README.md +14 -6
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -3
- package/lib/internal/canonicalDigest.js +12 -6
- package/lib/internal/loadPrismaModels.js +52 -7
- package/lib/internal/loadSwaggerOperations.js +253 -59
- package/lib/internal/normalizeSwaggerDocument.js +320 -2
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
- package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
- package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
- package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
- package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
- package/lib/structures/index.d.ts +4 -0
- package/lib/structures/index.js +4 -0
- package/native/base.go +25 -23
- package/native/bridge_trace.go +348 -0
- package/native/comments.go +1 -1
- package/native/config.go +6 -3
- package/native/declaration.go +17 -43
- package/native/documentation_regions.go +214 -0
- package/native/documented.go +5 -4
- package/native/exports.go +1 -1
- package/native/fingerprint.go +2 -8
- package/native/glob.go +4 -1
- package/native/graph.go +59 -23
- package/native/graph_nodes.go +2 -1
- package/native/hints.go +22 -18
- package/native/imports.go +7 -6
- package/native/input_reader.go +83 -0
- package/native/inputs.go +13 -12
- package/native/loader.go +36 -24
- package/native/markdown.go +587 -177
- package/native/markdown_capture.go +223 -0
- package/native/model.go +2 -1
- package/native/prisma.go +110 -25
- package/native/prisma_cache.go +12 -8
- package/native/prisma_locator.go +70 -33
- package/native/review.go +8 -7
- package/native/rooted_typescript.go +3 -4
- package/native/swagger.go +123 -15
- package/native/swagger_cache.go +16 -12
- package/native/todo.go +12 -0
- package/native/typescript.go +129 -25
- package/package.json +9 -7
- package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
- package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
- package/lib/typings/index.d.ts +0 -5
- package/lib/typings/index.js +0 -21
- /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
- /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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 :=
|
|
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 :=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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 =
|
|
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
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
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 _,
|
|
828
|
-
|
|
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,
|
|
851
|
-
|
|
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
|
-
//
|
|
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
|
|
868
|
-
//
|
|
869
|
-
//
|
|
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
|
package/native/prisma_cache.go
CHANGED
|
@@ -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
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
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 :=
|
|
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]
|