@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.
- package/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
package/native/prisma.go
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"errors"
|
|
8
|
+
"io/fs"
|
|
9
|
+
"os"
|
|
10
|
+
"os/exec"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"sort"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const prismaBridgeTimeout = 60 * time.Second
|
|
18
|
+
const prismaBridgeOutputLimit = 64 * 1024 * 1024
|
|
19
|
+
const prismaBridgeErrorLimit = 64 * 1024
|
|
20
|
+
|
|
21
|
+
// prismaSetID names the one schema set this graph parses.
|
|
22
|
+
//
|
|
23
|
+
// A project declares one Prisma schema. Prisma itself enforces that — two
|
|
24
|
+
// `datasource` blocks in one parse are its own error — so splitting the
|
|
25
|
+
// configured files into several independent parses would invent a topology the
|
|
26
|
+
// tool being wrapped does not have. Every configured Prisma glob, claim and
|
|
27
|
+
// reference alike, therefore contributes to a single ordered set, and the
|
|
28
|
+
// identity round-trips through the bridge so a contract drift is caught rather
|
|
29
|
+
// than silently reassigned.
|
|
30
|
+
const prismaSetID = "schema"
|
|
31
|
+
|
|
32
|
+
const prismaBridgeScript = `
|
|
33
|
+
const path = require("node:path");
|
|
34
|
+
const { createRequire } = require("node:module");
|
|
35
|
+
|
|
36
|
+
const root = process.argv[1];
|
|
37
|
+
const projectRequire = createRequire(path.join(root, "package.json"));
|
|
38
|
+
const manifest = projectRequire.resolve("@ttsc/evidence/package.json");
|
|
39
|
+
const pluginRequire = createRequire(manifest);
|
|
40
|
+
const loader = pluginRequire(
|
|
41
|
+
path.join(path.dirname(manifest), "lib", "internal", "loadPrismaModels.js"),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
let input = "";
|
|
45
|
+
process.stdin.setEncoding("utf8");
|
|
46
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
47
|
+
process.stdin.on("end", async () => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await loader.loadPrismaModels(JSON.parse(input));
|
|
50
|
+
process.stdout.write(JSON.stringify(result));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
process.stderr.write(error instanceof Error ? error.message : String(error));
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
type prismaNormalizationRequest struct {
|
|
59
|
+
Root string `json:"root"`
|
|
60
|
+
Sets []prismaSetRequest `json:"sets"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type prismaSetRequest struct {
|
|
64
|
+
ID string `json:"id"`
|
|
65
|
+
Files []string `json:"files"`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type prismaNormalizationResult struct {
|
|
69
|
+
Documents []prismaSetInventory `json:"documents"`
|
|
70
|
+
Problems []prismaSetProblem `json:"problems"`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// prismaSetInventory is one parsed schema set.
|
|
74
|
+
//
|
|
75
|
+
// Digest is the composition the loader itself computed from the bytes it read,
|
|
76
|
+
// and it is what the result is remembered under. Hashing there rather than
|
|
77
|
+
// trusting the digest this process took beforehand is what makes the key exact:
|
|
78
|
+
// the loader runs in another process and opens the files again, so a write
|
|
79
|
+
// landing between the two reads would otherwise bind one schema's models to
|
|
80
|
+
// another schema's bytes.
|
|
81
|
+
type prismaSetInventory struct {
|
|
82
|
+
ID string `json:"id"`
|
|
83
|
+
Models []prismaModel `json:"models"`
|
|
84
|
+
Digest string `json:"digest"`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// prismaSetProblem is a set the parser refused. Digest carries the same meaning
|
|
88
|
+
// as on an inventory, so a schema that cannot be parsed is remembered as a
|
|
89
|
+
// failure rather than re-parsed on every later cycle.
|
|
90
|
+
type prismaSetProblem struct {
|
|
91
|
+
ID string `json:"id"`
|
|
92
|
+
Message string `json:"message"`
|
|
93
|
+
Digest string `json:"digest"`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type prismaModel struct {
|
|
97
|
+
Name string `json:"name"`
|
|
98
|
+
Documentation string `json:"documentation"`
|
|
99
|
+
Fields []prismaField `json:"fields"`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type prismaField struct {
|
|
103
|
+
Name string `json:"name"`
|
|
104
|
+
Symbol string `json:"symbol"`
|
|
105
|
+
Documentation string `json:"documentation"`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// loadPrismaInventories materializes one inventory per configured schema file.
|
|
109
|
+
//
|
|
110
|
+
// The population comes from Prisma's own parser and the locations come from a
|
|
111
|
+
// native scan, and the split is load-bearing rather than convenient. The parser
|
|
112
|
+
// answers what exists — including which fields are relations, which no amount
|
|
113
|
+
// of reading text recovers cheaply, because a back-reference carries no
|
|
114
|
+
// attribute at all — but its payload has no line, column, or even file for
|
|
115
|
+
// anything it returns. The scan answers only where a name is written, and it
|
|
116
|
+
// can neither add a unit nor remove one, so a scan that misses costs a precise
|
|
117
|
+
// location and never a silently smaller coverage denominator.
|
|
118
|
+
func loadPrismaInventories(
|
|
119
|
+
root string,
|
|
120
|
+
config graphConfig,
|
|
121
|
+
) (map[string]*artifactInventory, []string) {
|
|
122
|
+
// A graph that names no Prisma glob never pays for one. The walk below is
|
|
123
|
+
// cheap on such a project — every directory fails the descendant test and is
|
|
124
|
+
// skipped at once — but "cheap" is still a full listing of the project root
|
|
125
|
+
// on every cycle of every consumer who does not use this artifact kind.
|
|
126
|
+
if !configuresPrisma(config) {
|
|
127
|
+
return map[string]*artifactInventory{}, nil
|
|
128
|
+
}
|
|
129
|
+
addresses, failedBases, problems := configuredPrismaAddressesWithHealth(config)
|
|
130
|
+
inventories := map[string]*artifactInventory{}
|
|
131
|
+
for _, base := range failedBases {
|
|
132
|
+
recordPopulationFailure(inventories, artifactPrisma, base)
|
|
133
|
+
}
|
|
134
|
+
for _, address := range addresses {
|
|
135
|
+
inventories[address.Key] = &artifactInventory{
|
|
136
|
+
Path: address.Display,
|
|
137
|
+
Type: artifactPrisma,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// The set is composed of physical files, deduplicated across bases. Prisma
|
|
141
|
+
// parses one schema at a time and a file listed twice is a duplicate
|
|
142
|
+
// declaration to its own parser, so two populations that reach one file
|
|
143
|
+
// through different roots must still contribute it once.
|
|
144
|
+
sources := distinctPrismaSources(addresses)
|
|
145
|
+
if len(sources) == 0 {
|
|
146
|
+
return inventories, problems
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Parsing costs a Node process, and the process start dominates it — a
|
|
150
|
+
// ten-model schema and a two-hundred-model one pay nearly the same. A
|
|
151
|
+
// resident host repeats this every cycle, so an unchanged schema would
|
|
152
|
+
// otherwise be re-parsed on every TypeScript keystroke that rebuilds.
|
|
153
|
+
digest := prismaContentDigest(root, sources)
|
|
154
|
+
if outcome, hit := prismaSchemas.lookup(digest); hit {
|
|
155
|
+
return inventories, append(
|
|
156
|
+
problems,
|
|
157
|
+
prismaUnitsFromOutcome(root, sources, inventories, outcome)...,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
result, err := normalizePrismaSet(root, sources)
|
|
162
|
+
if err != nil {
|
|
163
|
+
message := "Evidence graph could not run its Prisma schema loader: " + err.Error() + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
|
|
164
|
+
return inventories, append(problems, failPrismaSet(inventories, sources, message))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
outcome, problem := prismaOutcomeOf(result)
|
|
168
|
+
if problem != "" {
|
|
169
|
+
return inventories, append(problems, failPrismaSet(inventories, sources, problem))
|
|
170
|
+
}
|
|
171
|
+
problems = append(
|
|
172
|
+
problems,
|
|
173
|
+
prismaUnitsFromOutcome(root, sources, inventories, outcome)...,
|
|
174
|
+
)
|
|
175
|
+
rememberPrismaSchema(outcome.digest, outcome)
|
|
176
|
+
return inventories, problems
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// prismaOutcomeOf reduces one bridge answer to the single set that was asked
|
|
180
|
+
// for, and reports a contract drift rather than guessing past it.
|
|
181
|
+
//
|
|
182
|
+
// Every branch here describes a bridge that answered something other than what
|
|
183
|
+
// it was asked. None of them can be repaired by editing a schema, so each names
|
|
184
|
+
// the installation instead — and none may fall through to "no models", which is
|
|
185
|
+
// a schema whose every obligation is vacuously satisfied.
|
|
186
|
+
func prismaOutcomeOf(result prismaNormalizationResult) (prismaSetOutcome, string) {
|
|
187
|
+
drift := "Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree."
|
|
188
|
+
if len(result.Documents)+len(result.Problems) != 1 {
|
|
189
|
+
return prismaSetOutcome{}, "Evidence graph Prisma loader answered " +
|
|
190
|
+
decimal(len(result.Documents)+len(result.Problems)) +
|
|
191
|
+
" schema sets for one request. " + drift
|
|
192
|
+
}
|
|
193
|
+
for _, document := range result.Documents {
|
|
194
|
+
if document.ID != prismaSetID {
|
|
195
|
+
return prismaSetOutcome{}, "Evidence graph Prisma loader returned an unconfigured schema set '" + document.ID + "'. " + drift
|
|
196
|
+
}
|
|
197
|
+
return prismaSetOutcome{Models: document.Models, digest: document.Digest}, ""
|
|
198
|
+
}
|
|
199
|
+
problem := result.Problems[0]
|
|
200
|
+
if problem.ID != prismaSetID {
|
|
201
|
+
return prismaSetOutcome{}, "Evidence graph Prisma loader rejected an unconfigured schema set '" + problem.ID + "'. " + drift
|
|
202
|
+
}
|
|
203
|
+
return prismaSetOutcome{
|
|
204
|
+
Rejected: true,
|
|
205
|
+
Problem: problem.Message,
|
|
206
|
+
digest: problem.Digest,
|
|
207
|
+
}, ""
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// configuredPrismaAddresses lists the schema files every configured Prisma glob
|
|
211
|
+
// selects, claim and reference alike, once per population base.
|
|
212
|
+
//
|
|
213
|
+
// Order is the digest's, so it must not depend on filesystem enumeration.
|
|
214
|
+
func configuredPrismaAddresses(
|
|
215
|
+
config graphConfig,
|
|
216
|
+
) ([]artifactAddress, []string) {
|
|
217
|
+
addresses, _, problems := configuredPrismaAddressesWithHealth(config)
|
|
218
|
+
return addresses, problems
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func configuredPrismaAddressesWithHealth(
|
|
222
|
+
config graphConfig,
|
|
223
|
+
) ([]artifactAddress, []populationBase, []string) {
|
|
224
|
+
addresses := []artifactAddress{}
|
|
225
|
+
failedBases := []populationBase{}
|
|
226
|
+
problems := []string{}
|
|
227
|
+
for _, base := range configuredBases(config, artifactPrisma) {
|
|
228
|
+
if problem := unreadableBaseProblem(base, artifactPrisma); problem != "" {
|
|
229
|
+
problems = append(problems, problem)
|
|
230
|
+
failedBases = append(failedBases, base)
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
baseFailed := false
|
|
234
|
+
err := filepath.WalkDir(base.Absolute, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
235
|
+
if walkErr != nil {
|
|
236
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
237
|
+
relevant := ok &&
|
|
238
|
+
(matchesConfiguredPrismaFile(config, base, relative) ||
|
|
239
|
+
couldContainConfiguredPrisma(config, base, relative))
|
|
240
|
+
if !relevant {
|
|
241
|
+
if entry != nil && entry.IsDir() {
|
|
242
|
+
return filepath.SkipDir
|
|
243
|
+
}
|
|
244
|
+
return nil
|
|
245
|
+
}
|
|
246
|
+
baseFailed = true
|
|
247
|
+
problems = append(problems, "Evidence graph could not inspect '"+current+"': "+walkErr.Error()+". Fix filesystem access so configured Prisma sources can be indexed.")
|
|
248
|
+
if entry != nil && entry.IsDir() {
|
|
249
|
+
return filepath.SkipDir
|
|
250
|
+
}
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
if entry.IsDir() {
|
|
254
|
+
if current == base.Absolute {
|
|
255
|
+
return nil
|
|
256
|
+
}
|
|
257
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
258
|
+
if !ok || !couldContainConfiguredPrisma(config, base, relative) {
|
|
259
|
+
return filepath.SkipDir
|
|
260
|
+
}
|
|
261
|
+
return nil
|
|
262
|
+
}
|
|
263
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
264
|
+
if !ok || !matchesConfiguredPrismaFile(config, base, relative) {
|
|
265
|
+
return nil
|
|
266
|
+
}
|
|
267
|
+
addresses = append(addresses, base.addressOf(relative))
|
|
268
|
+
return nil
|
|
269
|
+
})
|
|
270
|
+
if err != nil {
|
|
271
|
+
baseFailed = true
|
|
272
|
+
problems = append(problems, "Evidence graph could not walk Prisma root '"+populationRootLabel(base)+"': "+err.Error()+".")
|
|
273
|
+
}
|
|
274
|
+
if baseFailed {
|
|
275
|
+
failedBases = append(failedBases, base)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
sort.Slice(addresses, func(left int, right int) bool {
|
|
279
|
+
return addresses[left].Key < addresses[right].Key
|
|
280
|
+
})
|
|
281
|
+
return addresses, failedBases, problems
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// distinctPrismaSources reduces the configured addresses to the physical files
|
|
285
|
+
// the parser is handed, in one stable order.
|
|
286
|
+
func distinctPrismaSources(addresses []artifactAddress) []string {
|
|
287
|
+
seen := map[string]bool{}
|
|
288
|
+
sources := []string{}
|
|
289
|
+
for _, address := range addresses {
|
|
290
|
+
if seen[address.Display] {
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
seen[address.Display] = true
|
|
294
|
+
sources = append(sources, address.Display)
|
|
295
|
+
}
|
|
296
|
+
sort.Strings(sources)
|
|
297
|
+
return sources
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// prismaInventoriesByDisplay indexes the inventories that share one physical
|
|
301
|
+
// file.
|
|
302
|
+
//
|
|
303
|
+
// Two populations reaching one schema through different roots own separate
|
|
304
|
+
// inventories of it, because each answers a different set of globs — while the
|
|
305
|
+
// file itself is parsed once and its models are located once. Everything derived
|
|
306
|
+
// from those bytes therefore has to reach every inventory of the file rather
|
|
307
|
+
// than one of them.
|
|
308
|
+
func prismaInventoriesByDisplay(
|
|
309
|
+
inventories map[string]*artifactInventory,
|
|
310
|
+
) map[string][]*artifactInventory {
|
|
311
|
+
indexed := map[string][]*artifactInventory{}
|
|
312
|
+
for _, inventory := range inventories {
|
|
313
|
+
if inventory == nil {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
indexed[inventory.Path] = append(indexed[inventory.Path], inventory)
|
|
317
|
+
}
|
|
318
|
+
return indexed
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// failPrismaSet records one whole-set failure against every file of the set.
|
|
322
|
+
//
|
|
323
|
+
// The symbol is `*` rather than `model`, and the difference is load-bearing. A
|
|
324
|
+
// reference reads an inventory problem only when it selects that problem's
|
|
325
|
+
// symbol (`graph.go:169-174`), so a set that failed to parse would look
|
|
326
|
+
// problem-free to a reference selecting only columns or relations — which then
|
|
327
|
+
// reports that its globs "materialized no selected evidence units", sending the
|
|
328
|
+
// author to widen a selector when the schema is what could not be read. A
|
|
329
|
+
// failure that belongs to the whole set belongs to every selector over it.
|
|
330
|
+
func failPrismaSet(
|
|
331
|
+
inventories map[string]*artifactInventory,
|
|
332
|
+
sources []string,
|
|
333
|
+
message string,
|
|
334
|
+
) string {
|
|
335
|
+
indexed := prismaInventoriesByDisplay(inventories)
|
|
336
|
+
for _, source := range sources {
|
|
337
|
+
for _, inventory := range indexed[source] {
|
|
338
|
+
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
339
|
+
Symbol: "*",
|
|
340
|
+
Message: message,
|
|
341
|
+
})
|
|
342
|
+
inventory.LoadFailed = true
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return message
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// configuresPrisma reports whether any claim or reference selects this artifact
|
|
349
|
+
// kind at all.
|
|
350
|
+
func configuresPrisma(config graphConfig) bool {
|
|
351
|
+
for _, claim := range config.Claims {
|
|
352
|
+
if claim.Type == artifactPrisma {
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
for _, reference := range claim.References {
|
|
356
|
+
if reference.Type == artifactPrisma {
|
|
357
|
+
return true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func matchesConfiguredPrismaFile(
|
|
365
|
+
config graphConfig,
|
|
366
|
+
base populationBase,
|
|
367
|
+
path string,
|
|
368
|
+
) bool {
|
|
369
|
+
for _, claim := range config.Claims {
|
|
370
|
+
if claim.Type == artifactPrisma &&
|
|
371
|
+
claim.Base.Absolute == base.Absolute &&
|
|
372
|
+
claim.Files.matches(path) {
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
for _, reference := range claim.References {
|
|
376
|
+
if reference.Type == artifactPrisma &&
|
|
377
|
+
reference.Base.Absolute == base.Absolute &&
|
|
378
|
+
reference.Files.matches(path) {
|
|
379
|
+
return true
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func couldContainConfiguredPrisma(
|
|
387
|
+
config graphConfig,
|
|
388
|
+
base populationBase,
|
|
389
|
+
directory string,
|
|
390
|
+
) bool {
|
|
391
|
+
for _, claim := range config.Claims {
|
|
392
|
+
if claim.Type == artifactPrisma &&
|
|
393
|
+
claim.Base.Absolute == base.Absolute &&
|
|
394
|
+
claim.Files.couldMatchDescendant(directory) {
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
for _, reference := range claim.References {
|
|
398
|
+
if reference.Type == artifactPrisma &&
|
|
399
|
+
reference.Base.Absolute == base.Absolute &&
|
|
400
|
+
reference.Files.couldMatchDescendant(directory) {
|
|
401
|
+
return true
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
func normalizePrismaSet(
|
|
409
|
+
root string,
|
|
410
|
+
sources []string,
|
|
411
|
+
) (prismaNormalizationResult, error) {
|
|
412
|
+
request, err := json.Marshal(prismaNormalizationRequest{
|
|
413
|
+
Root: root,
|
|
414
|
+
Sets: []prismaSetRequest{{ID: prismaSetID, Files: sources}},
|
|
415
|
+
})
|
|
416
|
+
if err != nil {
|
|
417
|
+
return prismaNormalizationResult{}, err
|
|
418
|
+
}
|
|
419
|
+
node := os.Getenv("TTSC_NODE_BINARY")
|
|
420
|
+
if node == "" {
|
|
421
|
+
node, err = exec.LookPath("node")
|
|
422
|
+
if err != nil {
|
|
423
|
+
return prismaNormalizationResult{}, errors.New("Node.js executable was not found")
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
ctx, cancel := context.WithTimeout(context.Background(), prismaBridgeTimeout)
|
|
427
|
+
defer cancel()
|
|
428
|
+
command := exec.CommandContext(ctx, node, "-e", prismaBridgeScript, root)
|
|
429
|
+
command.Dir = root
|
|
430
|
+
command.Stdin = bytes.NewReader(request)
|
|
431
|
+
stdout := &limitedBuffer{Limit: prismaBridgeOutputLimit}
|
|
432
|
+
stderr := &limitedBuffer{Limit: prismaBridgeErrorLimit}
|
|
433
|
+
command.Stdout = stdout
|
|
434
|
+
command.Stderr = stderr
|
|
435
|
+
if err := command.Run(); err != nil {
|
|
436
|
+
if ctx.Err() == context.DeadlineExceeded {
|
|
437
|
+
return prismaNormalizationResult{}, errors.New("Prisma schema loader exceeded its 60 second timeout")
|
|
438
|
+
}
|
|
439
|
+
detail := strings.TrimSpace(stderr.String())
|
|
440
|
+
if detail == "" {
|
|
441
|
+
detail = err.Error()
|
|
442
|
+
}
|
|
443
|
+
return prismaNormalizationResult{}, errors.New(detail)
|
|
444
|
+
}
|
|
445
|
+
var result prismaNormalizationResult
|
|
446
|
+
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
447
|
+
return prismaNormalizationResult{}, errors.New("Prisma schema loader returned invalid JSON: " + err.Error())
|
|
448
|
+
}
|
|
449
|
+
return result, nil
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// prismaModelUnits turns one parsed model into its unit and its members'.
|
|
453
|
+
//
|
|
454
|
+
// A relation is a field, so a two-sided relation is two units, one owned by
|
|
455
|
+
// each model — including a self-relation, where both sides belong to the same
|
|
456
|
+
// model and share one relation name. Collapsing them by that shared name would
|
|
457
|
+
// give one unit two parents, which the hierarchy has no way to express.
|
|
458
|
+
func prismaModelUnits(model prismaModel) []*evidenceUnit {
|
|
459
|
+
modelID := "prisma:" + model.Name
|
|
460
|
+
// A schema author marking a model internal has made the same declaration a
|
|
461
|
+
// TypeScript author makes with the same tag, and a model that is out of the
|
|
462
|
+
// surface takes its fields with it.
|
|
463
|
+
modelHidden := commentHidingTag(model.Documentation)
|
|
464
|
+
units := []*evidenceUnit{{
|
|
465
|
+
ID: modelID,
|
|
466
|
+
Target: modelID,
|
|
467
|
+
Identity: []string{model.Name},
|
|
468
|
+
Type: artifactPrisma,
|
|
469
|
+
Symbol: "model",
|
|
470
|
+
Readable: "Prisma model '" + model.Name + "'",
|
|
471
|
+
Hidden: modelHidden,
|
|
472
|
+
}}
|
|
473
|
+
seen := map[string]bool{}
|
|
474
|
+
for _, field := range model.Fields {
|
|
475
|
+
if field.Symbol != "column" && field.Symbol != "relation" {
|
|
476
|
+
continue
|
|
477
|
+
}
|
|
478
|
+
target := modelID + "." + field.Name
|
|
479
|
+
if seen[target] {
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
seen[target] = true
|
|
483
|
+
fieldHidden := modelHidden
|
|
484
|
+
if fieldHidden == "" {
|
|
485
|
+
fieldHidden = commentHidingTag(field.Documentation)
|
|
486
|
+
}
|
|
487
|
+
units = append(units, &evidenceUnit{
|
|
488
|
+
ID: target,
|
|
489
|
+
ParentID: modelID,
|
|
490
|
+
Target: target,
|
|
491
|
+
Identity: []string{model.Name, field.Name},
|
|
492
|
+
Type: artifactPrisma,
|
|
493
|
+
Symbol: field.Symbol,
|
|
494
|
+
Readable: "Prisma " + field.Symbol + " '" + model.Name + "." + field.Name + "'",
|
|
495
|
+
Hidden: fieldHidden,
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
return units
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// prismaDeclarationsFromComments turns the scanned comment runs into
|
|
502
|
+
// declarations, and reports every citation that cannot become one.
|
|
503
|
+
//
|
|
504
|
+
// Silence is the failure this rule exists to remove, so a tag is never simply
|
|
505
|
+
// skipped. Which forms may host was settled by measurement rather than by
|
|
506
|
+
// preference: `///` and `/* */` both reach the parser's `documentation` and
|
|
507
|
+
// both are emitted into the generated client and into prisma-markdown's ERD,
|
|
508
|
+
// so both host a citation here. `//` is discarded by Prisma itself and is the
|
|
509
|
+
// only form that cannot. A top-level unattached `///` run is the one
|
|
510
|
+
// exclusion-only carrier.
|
|
511
|
+
// Every other unusable placement — documenting nothing,
|
|
512
|
+
// documenting something this graph does not address, or burying the tag behind
|
|
513
|
+
// an extra slash — names the move that fixes it.
|
|
514
|
+
func prismaDeclarationsFromComments(
|
|
515
|
+
comments []prismaCommentRun,
|
|
516
|
+
hosts map[string]*evidenceUnit,
|
|
517
|
+
inventories map[string][]*artifactInventory,
|
|
518
|
+
) []string {
|
|
519
|
+
problems := []string{}
|
|
520
|
+
sequence := 0
|
|
521
|
+
for _, run := range comments {
|
|
522
|
+
location := run.Path + ":" + decimal(run.Line)
|
|
523
|
+
if run.Form != prismaDocComment {
|
|
524
|
+
if prismaCommentCarriesTag(run.Body) {
|
|
525
|
+
problems = append(
|
|
526
|
+
problems,
|
|
527
|
+
"Evidence tag at "+location+" sits in a '//' line comment, which Prisma discards rather than attaching to the declaration below it. Write the citation on a '///' or '/* */' documentation comment directly above the model, column, or relation it grounds.",
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
if run.FileLevel {
|
|
533
|
+
hosted := inventories[run.Path]
|
|
534
|
+
if len(hosted) == 0 {
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
for _, offset := range prismaBuriedTagLines(run.Body) {
|
|
538
|
+
problems = append(
|
|
539
|
+
problems,
|
|
540
|
+
"Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
for _, parsed := range parseCommentDeclarations(run.Body, true) {
|
|
544
|
+
sequence++
|
|
545
|
+
line := run.Line + parsed.LineOffset
|
|
546
|
+
if parsed.Tag == tagEvidence {
|
|
547
|
+
problems = append(
|
|
548
|
+
problems,
|
|
549
|
+
"@evidence at "+run.Path+":"+decimal(line)+" is on a file-level Prisma exclusion carrier. Move ownership evidence directly above the selected model, column, or relation it grounds; only @evidenceExclude may be unattached at file level.",
|
|
550
|
+
)
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
declaration := &evidenceDeclaration{
|
|
554
|
+
ID: "prisma:" + run.Path + ":" + decimal(line) + ":" + decimal(sequence),
|
|
555
|
+
HostID: "prisma:" + run.Path + ":file",
|
|
556
|
+
Type: artifactPrisma,
|
|
557
|
+
Tag: parsed.Tag,
|
|
558
|
+
Target: parsed.Target,
|
|
559
|
+
Reason: parsed.Reason,
|
|
560
|
+
ExclusionCarrier: true,
|
|
561
|
+
Path: run.Path,
|
|
562
|
+
Line: line,
|
|
563
|
+
Sequence: sequence,
|
|
564
|
+
}
|
|
565
|
+
for _, inventory := range hosted {
|
|
566
|
+
inventory.Declarations = append(inventory.Declarations, declaration)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
if run.Key == "" {
|
|
572
|
+
if prismaCommentCarriesTag(run.Body) {
|
|
573
|
+
problems = append(
|
|
574
|
+
problems,
|
|
575
|
+
"Evidence tag at "+location+" documents no declaration. Prisma attaches a '///' comment to the declaration that immediately follows it, so a blank line before a top-level block, a block attribute, or a closing brace leaves the comment documenting nothing. Move the citation directly above the model, column, or relation it grounds.",
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
continue
|
|
579
|
+
}
|
|
580
|
+
host := hosts[run.Key]
|
|
581
|
+
if host == nil {
|
|
582
|
+
if prismaCommentCarriesTag(run.Body) {
|
|
583
|
+
problems = append(
|
|
584
|
+
problems,
|
|
585
|
+
"Evidence tag at "+location+" documents '"+run.Key+"', which is not a model, column, or relation. A model or a view and their members host an evidence citation; an enum, a composite type, and a datasource or generator setting do not.",
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
continue
|
|
589
|
+
}
|
|
590
|
+
hosted := inventories[run.Path]
|
|
591
|
+
if len(hosted) == 0 {
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
594
|
+
for _, offset := range prismaBuriedTagLines(run.Body) {
|
|
595
|
+
problems = append(
|
|
596
|
+
problems,
|
|
597
|
+
"Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
for _, parsed := range parseCommentDeclarations(run.Body, true) {
|
|
601
|
+
sequence++
|
|
602
|
+
line := run.Line + parsed.LineOffset
|
|
603
|
+
// One declaration, one identity, shared by every inventory of the
|
|
604
|
+
// file it is written in. Two populations that reached this schema
|
|
605
|
+
// through different roots each need to see it, and giving each its
|
|
606
|
+
// own copy would make one comment two declarations in the map
|
|
607
|
+
// `evaluateEvidenceGraph` keys by ID.
|
|
608
|
+
declaration := &evidenceDeclaration{
|
|
609
|
+
ID: "prisma:" + run.Path + ":" + decimal(line) + ":" + decimal(sequence),
|
|
610
|
+
HostID: host.ID,
|
|
611
|
+
SemanticHostIDs: []string{host.ID},
|
|
612
|
+
Type: artifactPrisma,
|
|
613
|
+
Tag: parsed.Tag,
|
|
614
|
+
Target: parsed.Target,
|
|
615
|
+
Reason: parsed.Reason,
|
|
616
|
+
Hosts: prismaHostSymbols(host),
|
|
617
|
+
Path: run.Path,
|
|
618
|
+
Line: line,
|
|
619
|
+
Sequence: sequence,
|
|
620
|
+
}
|
|
621
|
+
for _, inventory := range hosted {
|
|
622
|
+
inventory.Declarations = append(inventory.Declarations, declaration)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return problems
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// prismaHostSymbols reports the host kinds a declaration on this unit satisfies.
|
|
630
|
+
//
|
|
631
|
+
// A unit withdrawn from the surface by its own documentation tag hosts nothing:
|
|
632
|
+
// it is not a selected claim host, and with no host kind it is not an exclusion
|
|
633
|
+
// carrier either. The declaration is still recorded, so the citation on it is
|
|
634
|
+
// reported rather than silently discarded.
|
|
635
|
+
func prismaHostSymbols(host *evidenceUnit) symbolSet {
|
|
636
|
+
if host == nil || host.Hidden != "" {
|
|
637
|
+
return nil
|
|
638
|
+
}
|
|
639
|
+
return symbolSet{host.Symbol: true}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// prismaCommentCarriesTag reports whether a comment body opens a citation on
|
|
643
|
+
// any of its lines.
|
|
644
|
+
func prismaCommentCarriesTag(body string) bool {
|
|
645
|
+
for _, line := range strings.Split(body, "\n") {
|
|
646
|
+
trimmed := strings.TrimSpace(line)
|
|
647
|
+
if _, _, found := declarationLine(trimmed); found {
|
|
648
|
+
return true
|
|
649
|
+
}
|
|
650
|
+
if prismaBuriedTag(trimmed) {
|
|
651
|
+
return true
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return false
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// prismaBuriedTagLines lists the offsets of lines whose citation is buried
|
|
658
|
+
// behind extra slashes.
|
|
659
|
+
//
|
|
660
|
+
// A fourth slash is content, not syntax: Prisma's `(!"///") ~ "//"` lookahead
|
|
661
|
+
// makes `//// @evidence ...` a doc comment whose text begins `/ @evidence ...`.
|
|
662
|
+
// The tag therefore no longer opens its line, so nothing parses it — and
|
|
663
|
+
// nothing reported it either, which made a one-keystroke slip the quietest
|
|
664
|
+
// failure in this artifact kind. The comment is real, the schema keeps it, and
|
|
665
|
+
// an author reading the file sees a citation that does nothing.
|
|
666
|
+
func prismaBuriedTagLines(body string) []int {
|
|
667
|
+
offsets := []int{}
|
|
668
|
+
for offset, line := range strings.Split(body, "\n") {
|
|
669
|
+
trimmed := strings.TrimSpace(line)
|
|
670
|
+
if _, _, found := declarationLine(trimmed); found {
|
|
671
|
+
continue
|
|
672
|
+
}
|
|
673
|
+
if prismaBuriedTag(trimmed) {
|
|
674
|
+
offsets = append(offsets, offset)
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return offsets
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// prismaBuriedTag reports whether a line would open a citation once its leading
|
|
681
|
+
// comment punctuation is removed.
|
|
682
|
+
//
|
|
683
|
+
// Two shapes bury a tag, and both are one keystroke from a citation that works.
|
|
684
|
+
// A fourth slash makes `//// @evidence` a doc comment whose text begins with a
|
|
685
|
+
// slash, and a JSDoc-style block hands Prisma its own asterisks as content:
|
|
686
|
+
// measured, `/** @evidence x */` reaches the parser's documentation as
|
|
687
|
+
// `* @evidence x`, and the multi-line form keeps a leading asterisk on every
|
|
688
|
+
// line of it. In both the tag no longer opens its line, so nothing parses it —
|
|
689
|
+
// and until this stripped asterisks too, nothing reported it either. A schema
|
|
690
|
+
// author arriving from JSDoc writes the second shape by habit.
|
|
691
|
+
//
|
|
692
|
+
// Only leading punctuation is stripped, so prose that merely mentions the tag
|
|
693
|
+
// somewhere in a sentence is untouched. Over-reporting an ordinary comment
|
|
694
|
+
// would teach an author to stop reading these diagnostics, which costs more
|
|
695
|
+
// than the case being caught.
|
|
696
|
+
func prismaBuriedTag(trimmed string) bool {
|
|
697
|
+
stripped := strings.TrimSpace(strings.TrimLeft(trimmed, "/"))
|
|
698
|
+
if stripped == trimmed {
|
|
699
|
+
return false
|
|
700
|
+
}
|
|
701
|
+
_, _, found := declarationLine(stripped)
|
|
702
|
+
return found
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// prismaNormalizationFailure words a rejected schema identically whether the
|
|
706
|
+
// rejection arrived from the parser this cycle or from memory.
|
|
707
|
+
//
|
|
708
|
+
// The parser's own report is preserved rather than summarized, because it is
|
|
709
|
+
// the only place a location survives: a successful parse carries no positions
|
|
710
|
+
// at all, while a rejection names the file and line the author has to open.
|
|
711
|
+
func prismaNormalizationFailure(message string) string {
|
|
712
|
+
reason := strings.TrimSpace(message)
|
|
713
|
+
if reason == "" {
|
|
714
|
+
reason = "the parser reported no reason"
|
|
715
|
+
}
|
|
716
|
+
return "Evidence graph could not parse the configured Prisma schema: " +
|
|
717
|
+
reason +
|
|
718
|
+
". Fix the schema so Prisma can read it."
|
|
719
|
+
}
|