@ttsc/evidence 0.27.0 → 0.28.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 +82 -169
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +15 -5
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +70 -6
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +5 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +16 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +8 -0
- package/native/base.go +517 -22
- package/native/config.go +114 -5
- package/native/graph.go +332 -70
- package/native/graph_nodes.go +121 -0
- package/native/loader.go +30 -7
- package/native/markdown.go +91 -28
- package/native/model.go +49 -16
- package/native/prisma.go +167 -48
- package/native/prisma_cache.go +40 -16
- package/native/swagger.go +1 -1
- package/native/swagger_cache.go +1 -1
- package/native/typescript.go +120 -9
- package/package.json +5 -5
package/native/prisma.go
CHANGED
|
@@ -146,12 +146,13 @@ func loadPrismaInventories(
|
|
|
146
146
|
Type: artifactPrisma,
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
-
// The set is composed of physical files, deduplicated across bases
|
|
150
|
-
// parses one schema at a time and a file
|
|
151
|
-
// declaration to its own parser, so two
|
|
152
|
-
// through different roots
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
// The set is composed of physical files, deduplicated across bases by what
|
|
150
|
+
// each address is on disk. Prisma parses one schema at a time and a file
|
|
151
|
+
// listed twice is a duplicate declaration to its own parser, so two
|
|
152
|
+
// populations that reach one file through different roots contribute it
|
|
153
|
+
// once and are both served by that one parse.
|
|
154
|
+
set := distinctPrismaSources(root, addresses)
|
|
155
|
+
if len(set.Sources) == 0 {
|
|
155
156
|
return inventories, problems
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -159,27 +160,27 @@ func loadPrismaInventories(
|
|
|
159
160
|
// ten-model schema and a two-hundred-model one pay nearly the same. A
|
|
160
161
|
// resident host repeats this every cycle, so an unchanged schema would
|
|
161
162
|
// otherwise be re-parsed on every TypeScript keystroke that rebuilds.
|
|
162
|
-
digest := prismaContentDigest(root,
|
|
163
|
+
digest := prismaContentDigest(root, set.Sources)
|
|
163
164
|
if outcome, hit := prismaSchemas.lookup(digest); hit {
|
|
164
165
|
return inventories, append(
|
|
165
166
|
problems,
|
|
166
|
-
prismaUnitsFromOutcome(root,
|
|
167
|
+
prismaUnitsFromOutcome(root, set, inventories, outcome)...,
|
|
167
168
|
)
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
result, err := normalizePrismaSet(root,
|
|
171
|
+
result, err := normalizePrismaSet(root, set.Sources)
|
|
171
172
|
if err != nil {
|
|
172
|
-
message := "Evidence graph could not run its Prisma schema loader: " + err
|
|
173
|
-
return inventories, append(problems, failPrismaSet(inventories,
|
|
173
|
+
message := "Evidence graph could not run its Prisma schema loader: " + causeText(err) + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
|
|
174
|
+
return inventories, append(problems, failPrismaSet(inventories, set, message))
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
outcome, problem := prismaOutcomeOf(result)
|
|
177
178
|
if problem != "" {
|
|
178
|
-
return inventories, append(problems, failPrismaSet(inventories,
|
|
179
|
+
return inventories, append(problems, failPrismaSet(inventories, set, problem))
|
|
179
180
|
}
|
|
180
181
|
problems = append(
|
|
181
182
|
problems,
|
|
182
|
-
prismaUnitsFromOutcome(root,
|
|
183
|
+
prismaUnitsFromOutcome(root, set, inventories, outcome)...,
|
|
183
184
|
)
|
|
184
185
|
rememberPrismaSchema(outcome.digest, outcome)
|
|
185
186
|
return inventories, problems
|
|
@@ -234,42 +235,58 @@ func configuredPrismaAddressesWithHealth(
|
|
|
234
235
|
failedBases := []populationBase{}
|
|
235
236
|
problems := []string{}
|
|
236
237
|
for _, base := range configuredBases(config, artifactPrisma) {
|
|
237
|
-
if problem :=
|
|
238
|
+
if problem := baseDirectoryProblem(base, artifactPrisma); problem != "" {
|
|
238
239
|
problems = append(problems, problem)
|
|
239
240
|
failedBases = append(failedBases, base)
|
|
240
241
|
continue
|
|
241
242
|
}
|
|
242
243
|
baseFailed := false
|
|
243
|
-
|
|
244
|
+
from, resolved := resolvedBaseDirectory(base)
|
|
245
|
+
if !resolved {
|
|
246
|
+
problems = append(problems, unresolvedBaseProblem(base, artifactPrisma))
|
|
247
|
+
failedBases = append(failedBases, base)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
244
251
|
if walkErr != nil {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return
|
|
252
|
+
// The walk root belongs to its population by construction, so a
|
|
253
|
+
// failure to list it is a failure of the population. The relevance test
|
|
254
|
+
// below is written for an entry inside the base and reached the base
|
|
255
|
+
// itself only by accident, on a decision the glob shape made;
|
|
256
|
+
// `loadMarkdownBase` records that history in full. Leaving this
|
|
257
|
+
// exemption in one walker would decide an identical filesystem state by
|
|
258
|
+
// artifact kind.
|
|
259
|
+
if current == from {
|
|
260
|
+
return walkErr
|
|
254
261
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
problem, relevant := unreadableEntryProblem(
|
|
263
|
+
base,
|
|
264
|
+
from,
|
|
265
|
+
"Prisma",
|
|
266
|
+
current,
|
|
267
|
+
walkErr,
|
|
268
|
+
func(relative string) bool {
|
|
269
|
+
return matchesConfiguredPrismaFile(config, base, relative) ||
|
|
270
|
+
couldContainConfiguredPrisma(config, base, relative)
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
if relevant {
|
|
274
|
+
baseFailed = true
|
|
275
|
+
problems = append(problems, problem)
|
|
259
276
|
}
|
|
260
|
-
return
|
|
277
|
+
return filepath.SkipDir
|
|
261
278
|
}
|
|
262
279
|
if entry.IsDir() {
|
|
263
|
-
if current ==
|
|
280
|
+
if current == from {
|
|
264
281
|
return nil
|
|
265
282
|
}
|
|
266
|
-
relative, ok := relativeProjectPath(
|
|
283
|
+
relative, ok := relativeProjectPath(from, current)
|
|
267
284
|
if !ok || !couldContainConfiguredPrisma(config, base, relative) {
|
|
268
285
|
return filepath.SkipDir
|
|
269
286
|
}
|
|
270
287
|
return nil
|
|
271
288
|
}
|
|
272
|
-
relative, ok := relativeProjectPath(
|
|
289
|
+
relative, ok := relativeProjectPath(from, current)
|
|
273
290
|
if !ok || !matchesConfiguredPrismaFile(config, base, relative) {
|
|
274
291
|
return nil
|
|
275
292
|
}
|
|
@@ -278,7 +295,7 @@ func configuredPrismaAddressesWithHealth(
|
|
|
278
295
|
})
|
|
279
296
|
if err != nil {
|
|
280
297
|
baseFailed = true
|
|
281
|
-
problems = append(problems, "
|
|
298
|
+
problems = append(problems, unlistableBaseProblem(base, "Prisma", err))
|
|
282
299
|
}
|
|
283
300
|
if baseFailed {
|
|
284
301
|
failedBases = append(failedBases, base)
|
|
@@ -290,30 +307,133 @@ func configuredPrismaAddressesWithHealth(
|
|
|
290
307
|
return addresses, failedBases, problems
|
|
291
308
|
}
|
|
292
309
|
|
|
310
|
+
// prismaSourceSet is the parser's input: one spelling per physical schema file,
|
|
311
|
+
// beside every spelling the configuration reached that file by.
|
|
312
|
+
//
|
|
313
|
+
// Prisma parses a set, and a file handed to it twice is a duplicate declaration
|
|
314
|
+
// to its own parser — so the set has to be one entry per file on disk, not one
|
|
315
|
+
// per configured address. The spellings are kept because everything the parse
|
|
316
|
+
// produces is addressed by the entry that was sent, while the inventories
|
|
317
|
+
// waiting for it are addressed by whichever root each population reached the
|
|
318
|
+
// file through.
|
|
319
|
+
type prismaSourceSet struct {
|
|
320
|
+
// Sources is the ordered set handed to the parser, the digest, and the
|
|
321
|
+
// locator, one entry per physical file.
|
|
322
|
+
Sources []string
|
|
323
|
+
// Spellings maps each entry of Sources to every address display naming the
|
|
324
|
+
// same file, that entry included.
|
|
325
|
+
Spellings map[string][]string
|
|
326
|
+
}
|
|
327
|
+
|
|
293
328
|
// distinctPrismaSources reduces the configured addresses to the physical files
|
|
294
329
|
// the parser is handed, in one stable order.
|
|
295
|
-
|
|
330
|
+
//
|
|
331
|
+
// Identity is the file's own, taken from the filesystem rather than from the
|
|
332
|
+
// path that named it. Two populations reach one schema through different roots
|
|
333
|
+
// precisely when their spellings differ — a package rooted at its installed
|
|
334
|
+
// location and at its source checkout is the layout a package manager produces
|
|
335
|
+
// — so a spelling-keyed set sends that file to the parser twice and the parser
|
|
336
|
+
// rejects the whole set for a model declared twice. `os.SameFile` answers for
|
|
337
|
+
// the link, the junction, the hard link, and the case-insensitive volume
|
|
338
|
+
// alike, none of which a comparison of two strings can see.
|
|
339
|
+
//
|
|
340
|
+
// A file that cannot be stat'ed stands alone under its own spelling, which is
|
|
341
|
+
// the behavior every set had before identity was physical: the digest declines
|
|
342
|
+
// it and the loader reports it unreadable at its own path.
|
|
343
|
+
//
|
|
344
|
+
// The cost is one `os.Stat` per distinct configured address, and on Windows one
|
|
345
|
+
// file open beside it for each address that reaches a comparison, because
|
|
346
|
+
// `os.SameFile` loads the volume and file index there through `CreateFile`
|
|
347
|
+
// rather than from the stat — once per result, since the id is cached on it.
|
|
348
|
+
// Against that, the digest now reads one file per physical file rather than one
|
|
349
|
+
// per address, so in the very layout this exists for the reads go down as the
|
|
350
|
+
// stats go up. Both are per load of this artifact kind, of which a pass
|
|
351
|
+
// performs two — the claim-side populations and then the whole configuration —
|
|
352
|
+
// over a set an adopter counts in files.
|
|
353
|
+
func distinctPrismaSources(root string, addresses []artifactAddress) prismaSourceSet {
|
|
354
|
+
type physicalSchema struct {
|
|
355
|
+
identity os.FileInfo
|
|
356
|
+
spellings []string
|
|
357
|
+
}
|
|
358
|
+
files := []*physicalSchema{}
|
|
296
359
|
seen := map[string]bool{}
|
|
297
|
-
sources := []string{}
|
|
298
360
|
for _, address := range addresses {
|
|
299
361
|
if seen[address.Display] {
|
|
300
362
|
continue
|
|
301
363
|
}
|
|
302
364
|
seen[address.Display] = true
|
|
303
|
-
|
|
365
|
+
identity, err := os.Stat(resolveProjectPath(root, address.Display))
|
|
366
|
+
var host *physicalSchema
|
|
367
|
+
if err == nil {
|
|
368
|
+
for _, file := range files {
|
|
369
|
+
if file.identity != nil && os.SameFile(file.identity, identity) {
|
|
370
|
+
host = file
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if host == nil {
|
|
376
|
+
host = &physicalSchema{}
|
|
377
|
+
if err == nil {
|
|
378
|
+
host.identity = identity
|
|
379
|
+
}
|
|
380
|
+
files = append(files, host)
|
|
381
|
+
}
|
|
382
|
+
host.spellings = append(host.spellings, address.Display)
|
|
383
|
+
}
|
|
384
|
+
set := prismaSourceSet{
|
|
385
|
+
Sources: []string{},
|
|
386
|
+
Spellings: map[string][]string{},
|
|
387
|
+
}
|
|
388
|
+
for _, file := range files {
|
|
389
|
+
// The set's own order is sorted below, but which spelling represents a
|
|
390
|
+
// file is what every unit, declaration, and diagnostic of it will be
|
|
391
|
+
// addressed by. Taking the smallest makes that choice a property of the
|
|
392
|
+
// configuration rather than of the order a walk happened to produce.
|
|
393
|
+
sort.Strings(file.spellings)
|
|
394
|
+
set.Sources = append(set.Sources, file.spellings[0])
|
|
395
|
+
set.Spellings[file.spellings[0]] = file.spellings
|
|
396
|
+
}
|
|
397
|
+
sort.Strings(set.Sources)
|
|
398
|
+
return set
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// prismaInventoriesBySource indexes the inventories waiting on each entry of
|
|
402
|
+
// the parsed set.
|
|
403
|
+
//
|
|
404
|
+
// This is where one parse result becomes every population's. The parse, the
|
|
405
|
+
// locations, and the comment scan all speak the set's spelling; an inventory
|
|
406
|
+
// speaks its own population's. Resolving the second through the first is what
|
|
407
|
+
// keeps a schema reached by two roots from serving only the root that happened
|
|
408
|
+
// to sort first.
|
|
409
|
+
func prismaInventoriesBySource(
|
|
410
|
+
inventories map[string]*artifactInventory,
|
|
411
|
+
set prismaSourceSet,
|
|
412
|
+
) map[string][]*artifactInventory {
|
|
413
|
+
byDisplay := prismaInventoriesByDisplay(inventories)
|
|
414
|
+
indexed := map[string][]*artifactInventory{}
|
|
415
|
+
for _, source := range set.Sources {
|
|
416
|
+
// Every source is the spelling of an address, and every address owns an
|
|
417
|
+
// inventory, so no entry here is empty. It is written as an unconditional
|
|
418
|
+
// fan-out rather than guarded against a state that cannot arise, because a
|
|
419
|
+
// guard over one would say the opposite of what the loader guarantees.
|
|
420
|
+
for _, spelling := range set.Spellings[source] {
|
|
421
|
+
indexed[source] = append(indexed[source], byDisplay[spelling]...)
|
|
422
|
+
}
|
|
304
423
|
}
|
|
305
|
-
|
|
306
|
-
return sources
|
|
424
|
+
return indexed
|
|
307
425
|
}
|
|
308
426
|
|
|
309
|
-
// prismaInventoriesByDisplay indexes the inventories that share one
|
|
310
|
-
// file.
|
|
427
|
+
// prismaInventoriesByDisplay indexes the inventories that share one spelling of
|
|
428
|
+
// one physical file.
|
|
311
429
|
//
|
|
312
430
|
// Two populations reaching one schema through different roots own separate
|
|
313
431
|
// inventories of it, because each answers a different set of globs — while the
|
|
314
432
|
// file itself is parsed once and its models are located once. Everything derived
|
|
315
433
|
// from those bytes therefore has to reach every inventory of the file rather
|
|
316
|
-
// than one of them
|
|
434
|
+
// than one of them, which is what `prismaInventoriesBySource` composes on top
|
|
435
|
+
// of this index; a population whose root spells the file differently is not
|
|
436
|
+
// visible here.
|
|
317
437
|
func prismaInventoriesByDisplay(
|
|
318
438
|
inventories map[string]*artifactInventory,
|
|
319
439
|
) map[string][]*artifactInventory {
|
|
@@ -327,7 +447,8 @@ func prismaInventoriesByDisplay(
|
|
|
327
447
|
return indexed
|
|
328
448
|
}
|
|
329
449
|
|
|
330
|
-
// failPrismaSet records one whole-set failure against every
|
|
450
|
+
// failPrismaSet records one whole-set failure against every inventory of every
|
|
451
|
+
// file of the set.
|
|
331
452
|
//
|
|
332
453
|
// The symbol is `*` rather than `model`, and the difference is load-bearing. A
|
|
333
454
|
// reference reads an inventory problem only when it selects that problem's
|
|
@@ -338,11 +459,11 @@ func prismaInventoriesByDisplay(
|
|
|
338
459
|
// failure that belongs to the whole set belongs to every selector over it.
|
|
339
460
|
func failPrismaSet(
|
|
340
461
|
inventories map[string]*artifactInventory,
|
|
341
|
-
|
|
462
|
+
set prismaSourceSet,
|
|
342
463
|
message string,
|
|
343
464
|
) string {
|
|
344
|
-
indexed :=
|
|
345
|
-
for _, source := range
|
|
465
|
+
indexed := prismaInventoriesBySource(inventories, set)
|
|
466
|
+
for _, source := range set.Sources {
|
|
346
467
|
for _, inventory := range indexed[source] {
|
|
347
468
|
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
348
469
|
Symbol: "*",
|
|
@@ -586,7 +707,6 @@ func prismaDeclarationsFromComments(
|
|
|
586
707
|
// nothing reads back — the same dead end the attached case just escaped.
|
|
587
708
|
for _, review := range parseReviews(run.Body) {
|
|
588
709
|
shared := &evidenceReview{
|
|
589
|
-
HostID: "prisma:" + run.Path + ":file",
|
|
590
710
|
SemanticHostIDs: []string{"prisma:" + run.Path + ":file"},
|
|
591
711
|
Reviews: review.Reviews,
|
|
592
712
|
Type: artifactPrisma,
|
|
@@ -664,7 +784,6 @@ func prismaDeclarationsFromComments(
|
|
|
664
784
|
// author cannot perform.
|
|
665
785
|
for _, review := range parseReviews(run.Body) {
|
|
666
786
|
shared := &evidenceReview{
|
|
667
|
-
HostID: host.ID,
|
|
668
787
|
SemanticHostIDs: []string{host.ID},
|
|
669
788
|
Reviews: review.Reviews,
|
|
670
789
|
Type: artifactPrisma,
|
|
@@ -765,7 +884,7 @@ func prismaBuriedTag(trimmed string) bool {
|
|
|
765
884
|
// the only place a location survives: a successful parse carries no positions
|
|
766
885
|
// at all, while a rejection names the file and line the author has to open.
|
|
767
886
|
func prismaNormalizationFailure(message string) string {
|
|
768
|
-
reason := strings.TrimSpace(message)
|
|
887
|
+
reason := causeReason(strings.TrimSpace(message))
|
|
769
888
|
if reason == "" {
|
|
770
889
|
reason = "the parser reported no reason"
|
|
771
890
|
}
|
package/native/prisma_cache.go
CHANGED
|
@@ -158,47 +158,71 @@ func rememberPrismaSchema(digest string, outcome prismaSetOutcome) {
|
|
|
158
158
|
prismaSchemas.store(digest, outcome)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// prismaUnitsFromOutcome rebuilds the set's units and files them
|
|
162
|
-
// schema file each one is written in.
|
|
161
|
+
// prismaUnitsFromOutcome rebuilds the set's units and files them into every
|
|
162
|
+
// inventory of the schema file each one is written in.
|
|
163
163
|
//
|
|
164
164
|
// Units are rebuilt per cycle rather than remembered, because a unit carries
|
|
165
165
|
// its location while the parsed models do not: what was cached is a property of
|
|
166
166
|
// the schema, not of where its text sits.
|
|
167
|
+
//
|
|
168
|
+
// One unit is one pointer, shared by every population of its file rather than
|
|
169
|
+
// copied per population. A copy would carry a second identity for one model,
|
|
170
|
+
// and the graph keys hosts, selections, and acknowledgements by that identity —
|
|
171
|
+
// so the same model would owe its obligations twice and satisfy them once. The
|
|
172
|
+
// location a copy would exist to restate is the set's spelling of the file,
|
|
173
|
+
// which is a path that opens.
|
|
167
174
|
func prismaUnitsFromOutcome(
|
|
168
175
|
root string,
|
|
169
|
-
|
|
176
|
+
set prismaSourceSet,
|
|
170
177
|
inventories map[string]*artifactInventory,
|
|
171
178
|
outcome prismaSetOutcome,
|
|
172
179
|
) []string {
|
|
173
180
|
if outcome.Rejected {
|
|
174
181
|
return []string{failPrismaSet(
|
|
175
182
|
inventories,
|
|
176
|
-
|
|
183
|
+
set,
|
|
177
184
|
prismaNormalizationFailure(outcome.Problem),
|
|
178
185
|
)}
|
|
179
186
|
}
|
|
180
|
-
locations, comments := locatePrismaDeclarations(root,
|
|
187
|
+
locations, comments := locatePrismaDeclarations(root, set.Sources)
|
|
181
188
|
fallback := ""
|
|
182
|
-
if len(
|
|
183
|
-
fallback =
|
|
189
|
+
if len(set.Sources) != 0 {
|
|
190
|
+
fallback = set.Sources[0]
|
|
191
|
+
}
|
|
192
|
+
indexed := prismaInventoriesBySource(inventories, set)
|
|
193
|
+
// Every inventory of the set, for the unit whose file the scan could not
|
|
194
|
+
// name. Each inventory belongs to exactly one source, so this is a union
|
|
195
|
+
// rather than a merge.
|
|
196
|
+
everywhere := []*artifactInventory{}
|
|
197
|
+
for _, source := range set.Sources {
|
|
198
|
+
everywhere = append(everywhere, indexed[source]...)
|
|
184
199
|
}
|
|
185
|
-
indexed := prismaInventoriesByDisplay(inventories)
|
|
186
200
|
hosts := map[string]*evidenceUnit{}
|
|
187
201
|
for _, model := range outcome.Models {
|
|
188
202
|
for _, unit := range prismaModelUnits(model) {
|
|
189
203
|
key := joinPrismaIdentity(unit.Identity)
|
|
190
204
|
location, found := locations[key]
|
|
191
|
-
|
|
205
|
+
var hosted []*artifactInventory
|
|
206
|
+
if found {
|
|
207
|
+
unit.Path = location.Path
|
|
208
|
+
unit.Line = location.Line
|
|
209
|
+
hosted = indexed[unit.Path]
|
|
210
|
+
} else {
|
|
192
211
|
// Locating is subordinate: a name the scan did not find keeps a
|
|
193
|
-
// file-level location and its full participation in coverage
|
|
194
|
-
//
|
|
195
|
-
// it
|
|
196
|
-
location
|
|
212
|
+
// file-level location and its full participation in coverage. A
|
|
213
|
+
// missing line costs precision, never an obligation — which is why
|
|
214
|
+
// it is filed into every inventory of the set rather than into the
|
|
215
|
+
// ones that happen to spell the location it was handed. The set spans
|
|
216
|
+
// populations whose roots name its files differently and every one of
|
|
217
|
+
// them selected files of this set, so charging the unit to the first
|
|
218
|
+
// source's populations alone would drop it from the rest with nothing
|
|
219
|
+
// said. That state is reached whenever a file the digest read a moment
|
|
220
|
+
// earlier cannot be read again, which a Windows lock is enough to do.
|
|
221
|
+
unit.Path = fallback
|
|
222
|
+
hosted = everywhere
|
|
197
223
|
}
|
|
198
|
-
unit.Path = location.Path
|
|
199
|
-
unit.Line = location.Line
|
|
200
224
|
hosts[key] = unit
|
|
201
|
-
for _, inventory := range
|
|
225
|
+
for _, inventory := range hosted {
|
|
202
226
|
inventory.Units = append(inventory.Units, unit)
|
|
203
227
|
}
|
|
204
228
|
}
|
package/native/swagger.go
CHANGED
|
@@ -132,7 +132,7 @@ func loadSwaggerInventories(
|
|
|
132
132
|
|
|
133
133
|
result, err := normalizeSwaggerSources(root, pending)
|
|
134
134
|
if err != nil {
|
|
135
|
-
message := "Evidence graph could not run its Swagger normalizer: " + err
|
|
135
|
+
message := "Evidence graph could not run its Swagger normalizer: " + causeText(err) + ". Swagger references require Node.js and the installed @typia/interface, @typia/utils, and yaml dependencies."
|
|
136
136
|
for _, source := range pending {
|
|
137
137
|
inventories[source].LoadFailed = true
|
|
138
138
|
inventories[source].Problems = append(
|
package/native/swagger_cache.go
CHANGED
|
@@ -262,7 +262,7 @@ func swaggerUnitsFromOutcome(
|
|
|
262
262
|
// that trails off after a colon reads like a formatting bug rather than a
|
|
263
263
|
// broken document.
|
|
264
264
|
func swaggerNormalizationFailure(source string, message string) string {
|
|
265
|
-
reason := strings.TrimSpace(message)
|
|
265
|
+
reason := causeReason(strings.TrimSpace(message))
|
|
266
266
|
if reason == "" {
|
|
267
267
|
reason = "the normalizer reported no reason"
|
|
268
268
|
}
|
package/native/typescript.go
CHANGED
|
@@ -32,13 +32,14 @@ func extendTypeScriptInventories(
|
|
|
32
32
|
inventories map[string]*artifactInventory,
|
|
33
33
|
governed map[string]bool,
|
|
34
34
|
) {
|
|
35
|
-
bases :=
|
|
35
|
+
bases := typeScriptMatchBases(config)
|
|
36
36
|
for _, file := range sources {
|
|
37
37
|
if file == nil || !isTypeScriptPath(file.FileName()) {
|
|
38
38
|
continue
|
|
39
39
|
}
|
|
40
|
-
for _,
|
|
41
|
-
|
|
40
|
+
for _, entry := range bases {
|
|
41
|
+
base := entry.base
|
|
42
|
+
relative, ok := entry.relativeOf(file.FileName())
|
|
42
43
|
if !ok || !isTypeScriptPath(relative) {
|
|
43
44
|
continue
|
|
44
45
|
}
|
|
@@ -54,6 +55,67 @@ func extendTypeScriptInventories(
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// typeScriptBaseProblems reports every declared TypeScript root that is not an
|
|
59
|
+
// existing directory and marks the population behind it failed.
|
|
60
|
+
//
|
|
61
|
+
// The other two claim-capable kinds get this from their loaders: the Markdown
|
|
62
|
+
// and Prisma walkers meet the root before they list anything, report it, and
|
|
63
|
+
// record a population failure that suppresses the glob diagnostic as its
|
|
64
|
+
// derivative. TypeScript materializes from `ctx.Sources` and walks nothing, so
|
|
65
|
+
// no loader is ever in a position to notice, and the population came back
|
|
66
|
+
// healthy and empty — which routed the author to `matched no typescript files`
|
|
67
|
+
// and a lecture on glob syntax for a mistake that was one directory name. The
|
|
68
|
+
// rule had already computed the right sentence and used it as a boolean.
|
|
69
|
+
//
|
|
70
|
+
// Recording the failure rather than only reporting the string is what makes the
|
|
71
|
+
// three kinds converge: health alone now decides that the claim stays active,
|
|
72
|
+
// and the population it names is the one the author has to repair.
|
|
73
|
+
//
|
|
74
|
+
// Only claim bases can reach a declared root here. A TypeScript reference selects
|
|
75
|
+
// the active program with `files` or an installed package with `package` and is
|
|
76
|
+
// refused a `root` outright, so every reference base of this kind is the default
|
|
77
|
+
// one, which this pass leaves alone: `baseDirectoryProblem` returns on it, and
|
|
78
|
+
// the resolver question below is asked only of a root someone declared.
|
|
79
|
+
func typeScriptBaseProblems(
|
|
80
|
+
config graphConfig,
|
|
81
|
+
inventories map[string]*artifactInventory,
|
|
82
|
+
) []string {
|
|
83
|
+
problems := []string{}
|
|
84
|
+
for _, base := range configuredBases(config, artifactTypeScript) {
|
|
85
|
+
problem := baseDirectoryProblem(base, artifactTypeScript)
|
|
86
|
+
// The stat accepts a chain of links the resolver will not finish, and this
|
|
87
|
+
// kind has no walk to notice it afterwards, so the gate asks here. Without
|
|
88
|
+
// it the population came back empty and the claim deactivated in silence,
|
|
89
|
+
// which is the state a linked root was repaired to remove.
|
|
90
|
+
//
|
|
91
|
+
// Only a declared root is asked. The default base is the project root the
|
|
92
|
+
// host resolved, which both entry points realpath before this rule sees it,
|
|
93
|
+
// and a Program spells its sources against the same root, so the first
|
|
94
|
+
// comparison matches and no resolution is wanted here. Refusing anyway turns
|
|
95
|
+
// a population that works into an error: measured as a graph closing at zero
|
|
96
|
+
// diagnostics becoming one refusal that fails every claim on the base nearly
|
|
97
|
+
// every project uses.
|
|
98
|
+
//
|
|
99
|
+
// A Markdown or Prisma population on that base still answers, because those
|
|
100
|
+
// walkers generate the paths they compare and a link they cannot resolve
|
|
101
|
+
// leaves them nothing to walk; a project declaring neither has nothing that
|
|
102
|
+
// reports it. A chain long enough to survive the host's own realpath is
|
|
103
|
+
// longer than the platform follows, so this is the residue of a shape
|
|
104
|
+
// production does not reach rather than a hole in what it does.
|
|
105
|
+
if problem == "" && !base.Default {
|
|
106
|
+
if _, resolved := resolvedBaseDirectory(base); !resolved {
|
|
107
|
+
problem = unresolvedBaseProblem(base, artifactTypeScript)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if problem == "" {
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
recordPopulationFailure(inventories, artifactTypeScript, base)
|
|
114
|
+
problems = append(problems, problem)
|
|
115
|
+
}
|
|
116
|
+
return problems
|
|
117
|
+
}
|
|
118
|
+
|
|
57
119
|
// isTypeScriptPath is asked once per source file per configured base on every
|
|
58
120
|
// rebuild, so it compares the suffix in place rather than lowercasing the whole
|
|
59
121
|
// path into a fresh string to answer four fixed questions.
|
|
@@ -67,6 +129,56 @@ func isTypeScriptPath(path string) bool {
|
|
|
67
129
|
return false
|
|
68
130
|
}
|
|
69
131
|
|
|
132
|
+
// typeScriptMatchBase pairs a configured base with the directory a Program
|
|
133
|
+
// source path is measured against.
|
|
134
|
+
//
|
|
135
|
+
// This kind walks nothing, so the link asymmetry #1258 removed from the two
|
|
136
|
+
// walkers looked like a walk problem and was left here. It is not: `os.Stat`
|
|
137
|
+
// accepts a linked root as a directory, and a Program reports whatever path its
|
|
138
|
+
// tsconfig resolved, so when the two disagree about the link every source fails
|
|
139
|
+
// the comparison, the claim selects nothing, and it deactivates without a word.
|
|
140
|
+
// Measured before the repair: a claim rooted at a junction over its own project
|
|
141
|
+
// produced no diagnostic at all.
|
|
142
|
+
//
|
|
143
|
+
// Both spellings are tried, because the Program may report either, and the
|
|
144
|
+
// address is composed from the declared base in both cases, so a source reached
|
|
145
|
+
// through a link is identified exactly as it would be without one. The
|
|
146
|
+
// resolution is per base and the comparison is per source, so the extra spelling
|
|
147
|
+
// costs nothing where no link is involved.
|
|
148
|
+
type typeScriptMatchBase struct {
|
|
149
|
+
base populationBase
|
|
150
|
+
resolved string
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func typeScriptMatchBases(config graphConfig) []typeScriptMatchBase {
|
|
154
|
+
bases := configuredBases(config, artifactTypeScript)
|
|
155
|
+
entries := make([]typeScriptMatchBase, 0, len(bases))
|
|
156
|
+
for _, base := range bases {
|
|
157
|
+
// An unresolved chain is reported by the gate for a declared root, and this
|
|
158
|
+
// comparison then has only the declared spelling to offer: keeping the link
|
|
159
|
+
// the resolver stopped on would spend a second comparison against a path no
|
|
160
|
+
// source can sit under, and the guard in `relativeOf` reads exactly this
|
|
161
|
+
// equality to skip it. For the default base nothing reports it here, which is
|
|
162
|
+
// the trade the gate's own comment states.
|
|
163
|
+
resolved, ok := resolvedBaseDirectory(base)
|
|
164
|
+
if !ok {
|
|
165
|
+
resolved = base.Absolute
|
|
166
|
+
}
|
|
167
|
+
entries = append(entries, typeScriptMatchBase{base: base, resolved: resolved})
|
|
168
|
+
}
|
|
169
|
+
return entries
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func (entry typeScriptMatchBase) relativeOf(name string) (string, bool) {
|
|
173
|
+
if relative, ok := relativeProjectPath(entry.base.Absolute, name); ok {
|
|
174
|
+
return relative, true
|
|
175
|
+
}
|
|
176
|
+
if entry.resolved == entry.base.Absolute {
|
|
177
|
+
return "", false
|
|
178
|
+
}
|
|
179
|
+
return relativeProjectPath(entry.resolved, name)
|
|
180
|
+
}
|
|
181
|
+
|
|
70
182
|
func relativeProjectPath(root string, absolute string) (string, bool) {
|
|
71
183
|
if root == "" || absolute == "" {
|
|
72
184
|
return "", false
|
|
@@ -1553,7 +1665,6 @@ func collectTypeScriptDeclarations(
|
|
|
1553
1665
|
// from every acknowledgement map in evaluation.
|
|
1554
1666
|
for _, review := range parseReviews(content[entry.node.Pos():entry.node.End()]) {
|
|
1555
1667
|
inventory.Reviews = append(inventory.Reviews, &evidenceReview{
|
|
1556
|
-
HostID: hostID,
|
|
1557
1668
|
SemanticHostIDs: semanticHostIDs,
|
|
1558
1669
|
Reviews: review.Reviews,
|
|
1559
1670
|
Type: artifactTypeScript,
|
|
@@ -1814,18 +1925,18 @@ func recordGovernedTypeScriptFiles(
|
|
|
1814
1925
|
if governed == nil {
|
|
1815
1926
|
return
|
|
1816
1927
|
}
|
|
1817
|
-
bases :=
|
|
1928
|
+
bases := typeScriptMatchBases(declared)
|
|
1818
1929
|
for _, file := range sources {
|
|
1819
1930
|
if file == nil || !isTypeScriptPath(file.FileName()) {
|
|
1820
1931
|
continue
|
|
1821
1932
|
}
|
|
1822
|
-
for _,
|
|
1823
|
-
relative, ok :=
|
|
1933
|
+
for _, entry := range bases {
|
|
1934
|
+
relative, ok := entry.relativeOf(file.FileName())
|
|
1824
1935
|
if !ok || !isTypeScriptPath(relative) {
|
|
1825
1936
|
continue
|
|
1826
1937
|
}
|
|
1827
|
-
if matchesConfiguredTypeScriptFile(declared, base, relative) {
|
|
1828
|
-
governed[base.addressOf(relative).Key] = true
|
|
1938
|
+
if matchesConfiguredTypeScriptFile(declared, entry.base, relative) {
|
|
1939
|
+
governed[entry.base.addressOf(relative).Key] = true
|
|
1829
1940
|
}
|
|
1830
1941
|
}
|
|
1831
1942
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/evidence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Evidence-graph lint contributor for @ttsc/lint: declare provenance with a JSDoc @evidence tag and fail the build when it is missing or dangling.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"@types/node": "^25.3.0",
|
|
49
49
|
"rimraf": "^6.1.2",
|
|
50
50
|
"typescript": "^7.0.2",
|
|
51
|
-
"ttsc": "0.
|
|
52
|
-
"
|
|
51
|
+
"@ttsc/lint": "0.28.0",
|
|
52
|
+
"ttsc": "0.28.0"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",
|
|
56
|
-
"@typia/interface": "
|
|
57
|
-
"@typia/utils": "
|
|
56
|
+
"@typia/interface": "14.0.0",
|
|
57
|
+
"@typia/utils": "14.0.0",
|
|
58
58
|
"yaml": "^2.9.0"
|
|
59
59
|
},
|
|
60
60
|
"homepage": "https://ttsc.dev",
|