@ttsc/evidence 0.25.0 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -2
- package/lib/internal/canonicalDigest.d.ts +1 -0
- package/lib/internal/canonicalDigest.js +97 -0
- package/lib/internal/loadPrismaModels.js +9 -2
- package/lib/internal/loadSwaggerOperations.js +96 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +48 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +3 -3
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +4 -4
- package/lib/typings/ITtscLintContributorRules.d.ts +23 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -29
- package/native/comments.go +162 -0
- package/native/config.go +2 -0
- package/native/declaration.go +213 -0
- package/native/documented.go +12 -12
- package/native/exports.go +64 -7
- package/native/fingerprint.go +351 -0
- package/native/graph.go +252 -16
- package/native/markdown.go +263 -0
- package/native/model.go +179 -1
- package/native/prisma.go +59 -3
- package/native/prisma_cache.go +7 -0
- package/native/review.go +355 -0
- package/native/swagger.go +8 -0
- package/native/typescript.go +697 -64
- package/native/unreadable.go +155 -0
- package/package.json +3 -3
package/native/markdown.go
CHANGED
|
@@ -98,6 +98,11 @@ func loadMarkdownBase(
|
|
|
98
98
|
problems = append(problems, inventoryProblem.Message)
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
// An unreadable tag is not a health question and not a symbol question
|
|
102
|
+
// either: the file loaded, its units are complete, and the tag reaches no
|
|
103
|
+
// host whichever symbol a reference selects. The walk already refuses a
|
|
104
|
+
// path no configured glob takes, so reaching here is enough to report.
|
|
105
|
+
problems = append(problems, inventory.Unreadable...)
|
|
101
106
|
return nil
|
|
102
107
|
})
|
|
103
108
|
if err != nil {
|
|
@@ -148,6 +153,16 @@ func scanMarkdownInventory(
|
|
|
148
153
|
hostAtLine := make([]string, len(lines))
|
|
149
154
|
hostIDAtLine := make([]string, len(lines))
|
|
150
155
|
fencedAtLine := make([]bool, len(lines))
|
|
156
|
+
// commentAtLine marks the lines a citation or a review can live on, so the
|
|
157
|
+
// content digest can leave them out. A fenced block is not marked: an
|
|
158
|
+
// `<!-- -->` inside one hosts no tag, and its text is content of the section.
|
|
159
|
+
commentAtLine := make([]bool, len(lines))
|
|
160
|
+
// The nearest heading *unit* enclosing each line, which is not the same as its
|
|
161
|
+
// host: a heading may open a region without materializing a unit. Kept apart
|
|
162
|
+
// from hostIDAtLine because that value decides where a declaration sits, and
|
|
163
|
+
// widening it would move citations rather than only digests.
|
|
164
|
+
digestHostIDAtLine := make([]string, len(lines))
|
|
165
|
+
currentDigestHostID := fileUnitID
|
|
151
166
|
currentHost := "file"
|
|
152
167
|
currentHostID := fileUnitID
|
|
153
168
|
fenceMarker := rune(0)
|
|
@@ -160,6 +175,11 @@ func scanMarkdownInventory(
|
|
|
160
175
|
if marker, length, remainder, ok := markdownFence(line); ok {
|
|
161
176
|
fencedAtLine[index] = true
|
|
162
177
|
hostIDAtLine[index] = currentHostID
|
|
178
|
+
// Fenced content is content. It hosts no tag, so it is never excluded as a
|
|
179
|
+
// tag position, and leaving it unattributed would drop every code block out
|
|
180
|
+
// of its section's digest: rewriting the example in a cited section would
|
|
181
|
+
// then expire nothing.
|
|
182
|
+
digestHostIDAtLine[index] = currentDigestHostID
|
|
163
183
|
if fenceMarker == 0 {
|
|
164
184
|
fenceMarker = marker
|
|
165
185
|
fenceLength = length
|
|
@@ -176,6 +196,7 @@ func scanMarkdownInventory(
|
|
|
176
196
|
fencedAtLine[index] = true
|
|
177
197
|
hostAtLine[index] = currentHost
|
|
178
198
|
hostIDAtLine[index] = currentHostID
|
|
199
|
+
digestHostIDAtLine[index] = currentDigestHostID
|
|
179
200
|
continue
|
|
180
201
|
}
|
|
181
202
|
if inHTMLComment {
|
|
@@ -184,6 +205,8 @@ func scanMarkdownInventory(
|
|
|
184
205
|
}
|
|
185
206
|
hostAtLine[index] = currentHost
|
|
186
207
|
hostIDAtLine[index] = currentHostID
|
|
208
|
+
digestHostIDAtLine[index] = currentDigestHostID
|
|
209
|
+
commentAtLine[index] = true
|
|
187
210
|
continue
|
|
188
211
|
}
|
|
189
212
|
if strings.HasPrefix(trimmed, "<!--") {
|
|
@@ -193,6 +216,8 @@ func scanMarkdownInventory(
|
|
|
193
216
|
}
|
|
194
217
|
hostAtLine[index] = currentHost
|
|
195
218
|
hostIDAtLine[index] = currentHostID
|
|
219
|
+
digestHostIDAtLine[index] = currentDigestHostID
|
|
220
|
+
commentAtLine[index] = true
|
|
196
221
|
continue
|
|
197
222
|
}
|
|
198
223
|
level, title, ok := markdownHeading(line)
|
|
@@ -204,6 +229,21 @@ func scanMarkdownInventory(
|
|
|
204
229
|
headingUnitIDs[descendantLevel] = ""
|
|
205
230
|
}
|
|
206
231
|
}
|
|
232
|
+
// A heading that materializes no unit still opens a region, and that
|
|
233
|
+
// region's content belongs to the nearest heading unit enclosing it. An
|
|
234
|
+
// H5, and an H2 whose title yields no anchor, are both such headings.
|
|
235
|
+
// Carrying the previous unit forward instead would attribute the region to
|
|
236
|
+
// whatever unit the walk happened to see last, which is a sibling rather
|
|
237
|
+
// than an ancestor when the skipped heading is shallower: editing text
|
|
238
|
+
// under an anchorless H2 would then expire a review of the H3 above it,
|
|
239
|
+
// which does not contain that text.
|
|
240
|
+
currentDigestHostID = fileUnitID
|
|
241
|
+
for ancestorLevel := level - 1; ancestorLevel >= 1; ancestorLevel-- {
|
|
242
|
+
if headingUnitIDs[ancestorLevel] != "" {
|
|
243
|
+
currentDigestHostID = headingUnitIDs[ancestorLevel]
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
}
|
|
207
247
|
if level <= 4 && targetablePath {
|
|
208
248
|
title, anchor := markdownHeadingIdentity(title)
|
|
209
249
|
if anchor == "" {
|
|
@@ -235,13 +275,17 @@ func scanMarkdownInventory(
|
|
|
235
275
|
}
|
|
236
276
|
inventory.Units = append(inventory.Units, unit)
|
|
237
277
|
headingUnitIDs[level] = unit.ID
|
|
278
|
+
currentDigestHostID = unit.ID
|
|
238
279
|
}
|
|
239
280
|
}
|
|
240
281
|
}
|
|
241
282
|
hostAtLine[index] = currentHost
|
|
242
283
|
hostIDAtLine[index] = currentHostID
|
|
284
|
+
digestHostIDAtLine[index] = currentDigestHostID
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
reportUnreadableMarkdownTags(inventory, address.Display, lines, fencedAtLine, commentAtLine)
|
|
288
|
+
|
|
245
289
|
sequence := 0
|
|
246
290
|
for _, match := range markdownCommentPattern.FindAllStringSubmatchIndex(content, -1) {
|
|
247
291
|
if len(match) < 4 {
|
|
@@ -269,10 +313,74 @@ func scanMarkdownInventory(
|
|
|
269
313
|
Sequence: sequence,
|
|
270
314
|
})
|
|
271
315
|
}
|
|
316
|
+
for _, review := range parseReviews(comment) {
|
|
317
|
+
inventory.Reviews = append(inventory.Reviews, &evidenceReview{
|
|
318
|
+
HostID: hostIDAtLine[line-1],
|
|
319
|
+
SemanticHostIDs: []string{hostIDAtLine[line-1]},
|
|
320
|
+
Reviews: review.Reviews,
|
|
321
|
+
Type: artifactMarkdown,
|
|
322
|
+
Target: review.Target,
|
|
323
|
+
Fingerprint: review.Fingerprint,
|
|
324
|
+
Description: review.Description,
|
|
325
|
+
Path: address.Display,
|
|
326
|
+
Line: line + review.LineOffset,
|
|
327
|
+
})
|
|
328
|
+
}
|
|
272
329
|
}
|
|
330
|
+
assignMarkdownDigests(inventory, lines, digestHostIDAtLine, commentAtLine)
|
|
273
331
|
return inventory, problems
|
|
274
332
|
}
|
|
275
333
|
|
|
334
|
+
// assignMarkdownDigests gives every unit the text it alone owns.
|
|
335
|
+
//
|
|
336
|
+
// A heading owns its own line and the body under it up to the next heading, and a
|
|
337
|
+
// deeper heading starts a unit of its own, so the partition is exactly what
|
|
338
|
+
// `digestHostIDAtLine` records while walking. Composing a subtree belongs to
|
|
339
|
+
// `scopeIndex`, which is why nothing is folded in here: an H2 whose own body never
|
|
340
|
+
// changed keeps its own digest even when an H3 beneath it did.
|
|
341
|
+
//
|
|
342
|
+
// This is where Markdown and TypeScript genuinely differ. A document can be
|
|
343
|
+
// partitioned into disjoint regions, so a Markdown unit's digest really is
|
|
344
|
+
// independent of its subtree. A declaration cannot: `interface ISale`
|
|
345
|
+
// textually contains the members it declares, so a TypeScript unit's digest
|
|
346
|
+
// covers its descendants whether anything wants it to or not.
|
|
347
|
+
// `evidenceUnit.Digest` records the consequence; do not carry the Markdown
|
|
348
|
+
// intuition across.
|
|
349
|
+
//
|
|
350
|
+
// HTML comment lines are dropped because that is where a Markdown citation and
|
|
351
|
+
// its review live. Leaving them in would make writing the review change the
|
|
352
|
+
// digest the review's own fingerprint is checked against.
|
|
353
|
+
func assignMarkdownDigests(
|
|
354
|
+
inventory *artifactInventory,
|
|
355
|
+
lines []string,
|
|
356
|
+
digestHostIDAtLine []string,
|
|
357
|
+
commentAtLine []bool,
|
|
358
|
+
) {
|
|
359
|
+
owned := map[string][]string{}
|
|
360
|
+
for index := range lines {
|
|
361
|
+
id := digestHostIDAtLine[index]
|
|
362
|
+
if index < len(commentAtLine) && commentAtLine[index] {
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
if id == "" {
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
// A comment opening after prose on the same line is still a tag position:
|
|
369
|
+
// the declaration scan runs over the whole document, so it finds a citation
|
|
370
|
+
// or a review there. Only the comment span comes out, never the line, or the
|
|
371
|
+
// prose beside it would vanish from the digest and a real content change
|
|
372
|
+
// would stop expiring anything.
|
|
373
|
+
content := markdownCommentPattern.ReplaceAllString(
|
|
374
|
+
strings.TrimSuffix(lines[index], "\r"),
|
|
375
|
+
"",
|
|
376
|
+
)
|
|
377
|
+
owned[id] = append(owned[id], content)
|
|
378
|
+
}
|
|
379
|
+
for _, unit := range inventory.Units {
|
|
380
|
+
unit.Digest = contentDigest(strings.Join(owned[unit.ID], "\n"))
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
276
384
|
// matchesConfiguredMarkdownFile reports whether a population rooted at this base
|
|
277
385
|
// selects the file.
|
|
278
386
|
//
|
|
@@ -421,3 +529,158 @@ func markdownSlug(title string) string {
|
|
|
421
529
|
}
|
|
422
530
|
return strings.Trim(builder.String(), "-")
|
|
423
531
|
}
|
|
532
|
+
|
|
533
|
+
// reportUnreadableMarkdownTags records every tag written where this artifact
|
|
534
|
+
// kind cannot read one.
|
|
535
|
+
//
|
|
536
|
+
// A Markdown declaration is read from an HTML comment, so the tag renders
|
|
537
|
+
// invisibly and the author sees the same source either way. Written as prose it
|
|
538
|
+
// reaches no host and used to be discarded without a word, leaving the coverage
|
|
539
|
+
// diagnostic that follows to name the reference and suggest writing the
|
|
540
|
+
// citation the author had already written. TypeScript answers this shape and
|
|
541
|
+
// the Prisma bridge answers its own; this is the kind that was left silent.
|
|
542
|
+
//
|
|
543
|
+
// A fenced block is an example rather than a citation and stays silent, which
|
|
544
|
+
// is not a concession: this product's own documentation shows tags inside
|
|
545
|
+
// fences, and reporting them would fail its build. An indented code block is
|
|
546
|
+
// the same case in another spelling, so four leading spaces are read as code
|
|
547
|
+
// rather than as prose.
|
|
548
|
+
//
|
|
549
|
+
// The tag has to open its line, which is the discrimination every reader in
|
|
550
|
+
// this package performs, so a sentence mentioning one describes it rather than
|
|
551
|
+
// declaring it.
|
|
552
|
+
func reportUnreadableMarkdownTags(
|
|
553
|
+
inventory *artifactInventory,
|
|
554
|
+
location string,
|
|
555
|
+
lines []string,
|
|
556
|
+
fencedAtLine []bool,
|
|
557
|
+
commentAtLine []bool,
|
|
558
|
+
) {
|
|
559
|
+
if inventory == nil {
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
rendered := false
|
|
563
|
+
for index, rawLine := range lines {
|
|
564
|
+
line := strings.TrimSuffix(rawLine, "
|
|
565
|
+
if opens, closes := renderedCodeEdges(line); opens || closes {
|
|
566
|
+
rendered = opens
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
if rendered {
|
|
570
|
+
continue
|
|
571
|
+
}
|
|
572
|
+
if index < len(fencedAtLine) && fencedAtLine[index] {
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
575
|
+
if index < len(commentAtLine) && commentAtLine[index] {
|
|
576
|
+
continue
|
|
577
|
+
}
|
|
578
|
+
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, " ") {
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
trimmed := markdownLineContent(line)
|
|
582
|
+
if tag, _, found := declarationLine(trimmed); found {
|
|
583
|
+
inventory.Unreadable = append(
|
|
584
|
+
inventory.Unreadable,
|
|
585
|
+
unreadableMarkdownProblem("@"+string(tag), location, index+1),
|
|
586
|
+
)
|
|
587
|
+
continue
|
|
588
|
+
}
|
|
589
|
+
if reviews, _, opened := reviewLine(trimmed); opened {
|
|
590
|
+
inventory.Unreadable = append(
|
|
591
|
+
inventory.Unreadable,
|
|
592
|
+
unreadableMarkdownProblem(reviewMarkerFor(reviews), location, index+1),
|
|
593
|
+
)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// markdownLineContent drops the markers that carry a line rather than say
|
|
599
|
+
// anything.
|
|
600
|
+
//
|
|
601
|
+
// A citation written as a bullet or inside a quote is the same mistake as one
|
|
602
|
+
// written bare, and an author reaching for a list is if anything more likely
|
|
603
|
+
// than one writing a lone paragraph. Reading the line without its marker is
|
|
604
|
+
// what lets the report name them, while the tag still has to be the first
|
|
605
|
+
// content on the line, so a sentence mentioning one goes on describing it.
|
|
606
|
+
func markdownLineContent(line string) string {
|
|
607
|
+
content := strings.TrimSpace(line)
|
|
608
|
+
for {
|
|
609
|
+
stripped := strings.TrimSpace(strings.TrimPrefix(content, ">"))
|
|
610
|
+
if stripped != content {
|
|
611
|
+
content = stripped
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
if marker := markdownListMarker(content); marker != 0 {
|
|
615
|
+
content = strings.TrimSpace(content[marker:])
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
return content
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// markdownListMarker reports the length of a leading list marker, or zero.
|
|
623
|
+
func markdownListMarker(content string) int {
|
|
624
|
+
for _, bullet := range []string{"- ", "* ", "+ "} {
|
|
625
|
+
if strings.HasPrefix(content, bullet) {
|
|
626
|
+
return len(bullet)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
digits := 0
|
|
630
|
+
for digits < len(content) && content[digits] >= '0' && content[digits] <= '9' {
|
|
631
|
+
digits++
|
|
632
|
+
}
|
|
633
|
+
if digits == 0 || digits+1 >= len(content) {
|
|
634
|
+
return 0
|
|
635
|
+
}
|
|
636
|
+
if punctuation := content[digits]; punctuation != '.' && punctuation != ')' {
|
|
637
|
+
return 0
|
|
638
|
+
}
|
|
639
|
+
if content[digits+1] != ' ' {
|
|
640
|
+
return 0
|
|
641
|
+
}
|
|
642
|
+
return digits + 2
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// renderedCodeEdges reports whether a line opens or closes a block that renders
|
|
646
|
+
// as code without being a fence.
|
|
647
|
+
//
|
|
648
|
+
// A documentation site shows examples through more than one syntax. An MDX page
|
|
649
|
+
// passes a template literal to a component, and an HTML page uses `<pre>`; both
|
|
650
|
+
// render as code, so both are examples in the sense a fence is, and the repair
|
|
651
|
+
// this diagnostic names would delete the example from the rendered page rather
|
|
652
|
+
// than fix anything. Only the two edges are recognized, because a page that
|
|
653
|
+
// opens one and never closes it is a page whose own build fails first.
|
|
654
|
+
func renderedCodeEdges(line string) (bool, bool) {
|
|
655
|
+
lowered := strings.ToLower(line)
|
|
656
|
+
switch {
|
|
657
|
+
case strings.Contains(lowered, "<pre"):
|
|
658
|
+
return !strings.Contains(lowered, "</pre>"), strings.Contains(lowered, "</pre>")
|
|
659
|
+
case strings.Contains(lowered, "</pre>"):
|
|
660
|
+
return false, true
|
|
661
|
+
case strings.Contains(line, "={`"):
|
|
662
|
+
return !strings.Contains(line, "`}"), strings.Contains(line, "`}")
|
|
663
|
+
case strings.Contains(line, "`}"):
|
|
664
|
+
return false, true
|
|
665
|
+
}
|
|
666
|
+
return false, false
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// unreadableMarkdownProblem names the position and the move that fixes it.
|
|
670
|
+
func unreadableMarkdownProblem(tag string, location string, line int) string {
|
|
671
|
+
return "Unreadable " + tag + " at " + location + ":" + decimal(line) +
|
|
672
|
+
": a Markdown declaration is read from an HTML comment, and this line is prose, so nothing reads the tag." +
|
|
673
|
+
" Wrap it as '<!-- " + tag + " <target> " + unreadableMarkdownField(tag) + " -->'."
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// unreadableMarkdownField names what follows a target for this tag.
|
|
677
|
+
//
|
|
678
|
+
// A review carries a description of what was checked rather than a reason, and
|
|
679
|
+
// every other review diagnostic in this package says so. One template for both
|
|
680
|
+
// families would tell an author to write the wrong field.
|
|
681
|
+
func unreadableMarkdownField(tag string) string {
|
|
682
|
+
if strings.HasSuffix(tag, "Review") {
|
|
683
|
+
return "<what you checked>"
|
|
684
|
+
}
|
|
685
|
+
return "<reason>"
|
|
686
|
+
}
|
package/native/model.go
CHANGED
|
@@ -15,6 +15,27 @@ const documentedRuleName = "evidence/documented"
|
|
|
15
15
|
|
|
16
16
|
const todoRuleName = "evidence/todo"
|
|
17
17
|
|
|
18
|
+
const reviewRuleName = "evidence/review"
|
|
19
|
+
|
|
20
|
+
// Appended to every diagnostic a tag could silence, because the cheapest way to
|
|
21
|
+
// clear one of these is to write a tag that is not true, and the reader is
|
|
22
|
+
// usually an agent whose next action is conditioned on this text alone.
|
|
23
|
+
//
|
|
24
|
+
// It names the motive rather than the truth value. A tag written to pass may
|
|
25
|
+
// happen to be true and is still wrong, and "do not write a false tag" invites
|
|
26
|
+
// exactly that reading.
|
|
27
|
+
//
|
|
28
|
+
// "Leave standing" rather than "write", because writing is only one of the ways
|
|
29
|
+
// a falsehood clears a diagnostic. A tag can also be moved onto a declaration
|
|
30
|
+
// that does not own its target, or kept when the conflicting one is deleted.
|
|
31
|
+
// One verb covers all three, so every diagnostic carries the same sentence and
|
|
32
|
+
// the reader meets one rule rather than three variants of it.
|
|
33
|
+
const untrueTagWarning = " Never leave an untrue tag standing just to pass this check; it removes the error, not the problem."
|
|
34
|
+
|
|
35
|
+
// The same warning for the two review tags, which are written about a check
|
|
36
|
+
// rather than about a host.
|
|
37
|
+
const untrueReviewWarning = " Never leave an untrue review standing just to pass this check; it removes the error, not the problem."
|
|
38
|
+
|
|
18
39
|
type artifactKind string
|
|
19
40
|
|
|
20
41
|
const (
|
|
@@ -87,6 +108,45 @@ type referencePolicy struct {
|
|
|
87
108
|
// SingleEvidencePerSymbol requires exactly one distinct selected unit from
|
|
88
109
|
// every selected semantic claim host, including the hosts carrying no tag.
|
|
89
110
|
SingleEvidencePerSymbol bool
|
|
111
|
+
// RequireReview demands that every acknowledgement of this population carry
|
|
112
|
+
// an `@evidenceReview` whose fingerprint matches the cited scope's current
|
|
113
|
+
// content, so a review expires when the thing it reviewed moves.
|
|
114
|
+
RequireReview bool
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// evidenceReview is one verification statement bound to its host and target.
|
|
118
|
+
//
|
|
119
|
+
// It is a sibling of evidenceDeclaration rather than a variant of it. Nothing
|
|
120
|
+
// that counts acknowledgements may reach a review, so the two never share a
|
|
121
|
+
// slice, a map, or a type. Target is the token as written and is compared
|
|
122
|
+
// against a declaration's target rather than resolved again: the citation and
|
|
123
|
+
// its review spell one address, and resolving twice would let a review answer a
|
|
124
|
+
// scope its citation does not name.
|
|
125
|
+
type evidenceReview struct {
|
|
126
|
+
// HostID is the source-position identity, kept only so a carrier with no
|
|
127
|
+
// semantic identity can still be matched. It must not be the primary key: a
|
|
128
|
+
// merged identity's halves sit at different positions and therefore carry
|
|
129
|
+
// different HostIDs, so matching on it alone refuses a review written on
|
|
130
|
+
// `namespace I` for a citation on `interface I` — placement the graph
|
|
131
|
+
// elsewhere calls not worth a diagnostic, and which `evidence/review` accepts.
|
|
132
|
+
HostID string
|
|
133
|
+
// SemanticHostIDs are the selected graph identities this review is written on,
|
|
134
|
+
// which is what a citation is matched by.
|
|
135
|
+
SemanticHostIDs []string
|
|
136
|
+
// Reviews names which acknowledgement this review answers for. It is part of
|
|
137
|
+
// the match, never inferred: verifying a citation and verifying an exclusion
|
|
138
|
+
// are opposite questions, so a review of one must not discharge the other.
|
|
139
|
+
Reviews tagKind
|
|
140
|
+
Type artifactKind
|
|
141
|
+
Target string
|
|
142
|
+
Fingerprint string
|
|
143
|
+
Description string
|
|
144
|
+
Path string
|
|
145
|
+
Line int
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func (review *evidenceReview) location() string {
|
|
149
|
+
return review.Path + ":" + decimal(review.Line)
|
|
90
150
|
}
|
|
91
151
|
|
|
92
152
|
// entrySelected reports whether this reference materializes by traversal.
|
|
@@ -152,6 +212,56 @@ type evidenceUnit struct {
|
|
|
152
212
|
// never selected and never hosts a declaration; it is retained only so a
|
|
153
213
|
// citation of it can be told why the target it names is not there.
|
|
154
214
|
Hidden string
|
|
215
|
+
// ValueSpace marks a unit reached only through a value: a function, a
|
|
216
|
+
// variable, a class member, or a member of an interface merged with a class.
|
|
217
|
+
//
|
|
218
|
+
// A type-only export exposes no value, so it exposes none of these either.
|
|
219
|
+
// The collector answers that for an export written in the declaring file,
|
|
220
|
+
// where it knows the declaration kind in hand. A re-export naming another
|
|
221
|
+
// module arrives at traversal time with no such context, which is what this
|
|
222
|
+
// field supplies: the same question, asked once at materialization and
|
|
223
|
+
// answered wherever reachability arrives through a type-only edge.
|
|
224
|
+
//
|
|
225
|
+
// It is read together with TypeSpace rather than alone, because a merged
|
|
226
|
+
// identity is written by more than one collector and a plain assignment would
|
|
227
|
+
// let source order decide.
|
|
228
|
+
ValueSpace bool
|
|
229
|
+
// TypeSpace marks a unit some declaration of which is reached without a
|
|
230
|
+
// value, and it wins over ValueSpace.
|
|
231
|
+
//
|
|
232
|
+
// One identity can be spelled by two collectors: `interface Order { member }`
|
|
233
|
+
// beside `namespace Order { export const member }` is one `property` unit
|
|
234
|
+
// written once by the member collector and once by the variable one. A
|
|
235
|
+
// type-only export exposes the interface half, so the unit survives, and the
|
|
236
|
+
// answer must not depend on which half the author wrote first.
|
|
237
|
+
TypeSpace bool
|
|
238
|
+
// Digest is this unit's content, hashed after normalization and with every
|
|
239
|
+
// position a tag can live in removed.
|
|
240
|
+
//
|
|
241
|
+
// How much "its content" is depends on the artifact, and the difference is
|
|
242
|
+
// load-bearing rather than incidental. A Markdown heading owns the lines
|
|
243
|
+
// between it and the next heading, so a subsection's body belongs to the
|
|
244
|
+
// subsection. A TypeScript unit owns its whole declaration text, which
|
|
245
|
+
// **contains** every nested member's text, because that is what a declaration
|
|
246
|
+
// is: there is no reading of `interface ISale` that excludes the members it
|
|
247
|
+
// declares, callables included.
|
|
248
|
+
//
|
|
249
|
+
// So a nested change moves both its own unit's digest and every enclosing
|
|
250
|
+
// unit's. `scopeIndex` composes ancestors and descendants anyway, so detection
|
|
251
|
+
// is unaffected, but nothing here may assume a unit's digest is independent of
|
|
252
|
+
// its subtree. A feature built on that assumption was reverted once already.
|
|
253
|
+
//
|
|
254
|
+
// Removing the documentation comment is what stops the review from
|
|
255
|
+
// invalidating itself: writing an `@evidenceReview` inside a unit that is
|
|
256
|
+
// itself cited would otherwise change the digest that the review's own
|
|
257
|
+
// fingerprint is checked against, and the repair would never terminate.
|
|
258
|
+
//
|
|
259
|
+
// Empty when the bridge that read this artifact reported no content for the
|
|
260
|
+
// unit, which is a loader gap rather than a configuration one: every
|
|
261
|
+
// reference kind may require a review, and every bridge digests what it
|
|
262
|
+
// parsed. A consumer of an empty digest reports nothing rather than
|
|
263
|
+
// comparing against nothing.
|
|
264
|
+
Digest string
|
|
155
265
|
}
|
|
156
266
|
|
|
157
267
|
func (unit *evidenceUnit) location() string {
|
|
@@ -204,7 +314,18 @@ type artifactInventory struct {
|
|
|
204
314
|
Type artifactKind
|
|
205
315
|
Units []*evidenceUnit
|
|
206
316
|
Declarations []*evidenceDeclaration
|
|
207
|
-
|
|
317
|
+
// Reviews are the verification statements this artifact carries, kept apart
|
|
318
|
+
// from Declarations so no consumer counting acknowledgements can reach one.
|
|
319
|
+
Reviews []*evidenceReview
|
|
320
|
+
Problems []inventoryProblem
|
|
321
|
+
// Unreadable lists the tags this artifact carries in a position nothing can
|
|
322
|
+
// read, already worded as diagnostics.
|
|
323
|
+
//
|
|
324
|
+
// They are kept apart from Problems because they are not a health question.
|
|
325
|
+
// The file loaded and its units are complete, so treating one as a failed
|
|
326
|
+
// population would suppress the obligations the author still owes while
|
|
327
|
+
// telling them only about a comment.
|
|
328
|
+
Unreadable []string
|
|
208
329
|
// LoadFailed distinguishes an unreadable or rejected artifact from a
|
|
209
330
|
// healthy artifact that legitimately materializes no selected units.
|
|
210
331
|
// Coverage is a completeness claim, so a failed inventory cannot be used
|
|
@@ -232,6 +353,18 @@ type artifactInventory struct {
|
|
|
232
353
|
// physical JSDoc declaration to semantic claim-host identities, then releases
|
|
233
354
|
// it; callers with no such use leave it nil.
|
|
234
355
|
UnitNodes map[string][]*shimast.Node
|
|
356
|
+
// UnitContent maps a unit ID to the nodes whose text is that identity's
|
|
357
|
+
// content, and is a subset of UnitNodes.
|
|
358
|
+
//
|
|
359
|
+
// Belonging to an identity and being its content are different questions,
|
|
360
|
+
// and a variable is where they part. TypeScript attaches a variable's
|
|
361
|
+
// leading documentation to the statement wrapper, so the wrapper is a
|
|
362
|
+
// position this identity owns — but the wrapper is also where its siblings
|
|
363
|
+
// are declared, and their text is not this identity's content. Answering the
|
|
364
|
+
// second question with the first makes one declarator's edit move another
|
|
365
|
+
// declarator's fingerprint. Each declaration site states which of its nodes
|
|
366
|
+
// is which; nothing here is derived from spans.
|
|
367
|
+
UnitContent map[string][]*shimast.Node
|
|
235
368
|
}
|
|
236
369
|
|
|
237
370
|
func (inventory *artifactInventory) recordUnitNode(id string, node *shimast.Node) {
|
|
@@ -241,6 +374,23 @@ func (inventory *artifactInventory) recordUnitNode(id string, node *shimast.Node
|
|
|
241
374
|
inventory.UnitNodes[id] = append(inventory.UnitNodes[id], node)
|
|
242
375
|
}
|
|
243
376
|
|
|
377
|
+
// recordUnitContent records a node as this identity's content, and as a
|
|
378
|
+
// position it owns.
|
|
379
|
+
//
|
|
380
|
+
// Content is recorded through the position index rather than beside it, so the
|
|
381
|
+
// subset the field documents holds by construction instead of by two callers
|
|
382
|
+
// agreeing.
|
|
383
|
+
func (inventory *artifactInventory) recordUnitContent(id string, node *shimast.Node) {
|
|
384
|
+
if inventory == nil || node == nil {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
inventory.recordUnitNode(id, node)
|
|
388
|
+
if inventory.UnitNodes == nil || inventory.UnitContent == nil {
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
inventory.UnitContent[id] = append(inventory.UnitContent[id], node)
|
|
392
|
+
}
|
|
393
|
+
|
|
244
394
|
type inventoryProblem struct {
|
|
245
395
|
Symbol string
|
|
246
396
|
Message string
|
|
@@ -251,6 +401,7 @@ type claimState struct {
|
|
|
251
401
|
Paths []string
|
|
252
402
|
Hosts []*evidenceUnit
|
|
253
403
|
Declarations []*evidenceDeclaration
|
|
404
|
+
Reviews []*evidenceReview
|
|
254
405
|
References []referenceState
|
|
255
406
|
Healthy bool
|
|
256
407
|
// OutsideCarrier names declarations this claim selected from a file its
|
|
@@ -264,6 +415,12 @@ type referenceState struct {
|
|
|
264
415
|
Paths []string
|
|
265
416
|
Units []*evidenceUnit
|
|
266
417
|
Scopes []*evidenceUnit
|
|
418
|
+
// Population is every unit this reference's files materialized, selected or
|
|
419
|
+
// not. Units and Scopes are both narrowed by the `symbol` selector, so
|
|
420
|
+
// neither can answer what a cited scope structurally contains, and a review
|
|
421
|
+
// fingerprint has to be a property of the cited address rather than of the
|
|
422
|
+
// reference that asked for it.
|
|
423
|
+
Population []*evidenceUnit
|
|
267
424
|
// Published names the module-and-address pairs a citation may use, for a
|
|
268
425
|
// population selected by walking module exports. Left empty when the
|
|
269
426
|
// population's addresses belong to the files that declare them.
|
|
@@ -294,3 +451,24 @@ func decimal(value int) string {
|
|
|
294
451
|
}
|
|
295
452
|
return string(digits)
|
|
296
453
|
}
|
|
454
|
+
|
|
455
|
+
// markSpace records which space this declaration of the unit is reached
|
|
456
|
+
// through, letting type-space win.
|
|
457
|
+
//
|
|
458
|
+
// A plain assignment made the answer depend on which half of a merged identity
|
|
459
|
+
// the author wrote first, in the one shape where two collectors write one unit:
|
|
460
|
+
// an interface member beside a namespace member of the same name. The
|
|
461
|
+
// suppression it feeds is a silent one, so the divergence was a build going
|
|
462
|
+
// green or red on declaration order with no message either way.
|
|
463
|
+
//
|
|
464
|
+
// Every site goes through here, including the ones that can only ever be value
|
|
465
|
+
// space. A site that assigns the field directly is correct until the day its
|
|
466
|
+
// address collides with a type-space one, and that is exactly the day nobody is
|
|
467
|
+
// looking at it.
|
|
468
|
+
func (unit *evidenceUnit) markSpace(valueSpace bool) {
|
|
469
|
+
if valueSpace {
|
|
470
|
+
unit.ValueSpace = true
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
unit.TypeSpace = true
|
|
474
|
+
}
|
package/native/prisma.go
CHANGED
|
@@ -94,15 +94,24 @@ type prismaSetProblem struct {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
type prismaModel struct {
|
|
97
|
-
Name string
|
|
98
|
-
Documentation string
|
|
99
|
-
|
|
97
|
+
Name string `json:"name"`
|
|
98
|
+
Documentation string `json:"documentation"`
|
|
99
|
+
// Digest is the model's own declaration, hashed by the bridge that parsed it,
|
|
100
|
+
// with its documentation and its fields left out. A field is a unit of its
|
|
101
|
+
// own and the scope composes them, so folding them in here would make one
|
|
102
|
+
// field's edit expire a review of every sibling.
|
|
103
|
+
Digest string `json:"digest"`
|
|
104
|
+
Fields []prismaField `json:"fields"`
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
type prismaField struct {
|
|
103
108
|
Name string `json:"name"`
|
|
104
109
|
Symbol string `json:"symbol"`
|
|
105
110
|
Documentation string `json:"documentation"`
|
|
111
|
+
// Digest is the field's own declaration, with its documentation left out.
|
|
112
|
+
// Its type, its attributes, and their arguments reach no other part of this
|
|
113
|
+
// process, so this is the only place a change to them is visible.
|
|
114
|
+
Digest string `json:"digest"`
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
// loadPrismaInventories materializes one inventory per configured schema file.
|
|
@@ -469,6 +478,7 @@ func prismaModelUnits(model prismaModel) []*evidenceUnit {
|
|
|
469
478
|
Symbol: "model",
|
|
470
479
|
Readable: "Prisma model '" + model.Name + "'",
|
|
471
480
|
Hidden: modelHidden,
|
|
481
|
+
Digest: model.Digest,
|
|
472
482
|
}}
|
|
473
483
|
seen := map[string]bool{}
|
|
474
484
|
for _, field := range model.Fields {
|
|
@@ -493,6 +503,7 @@ func prismaModelUnits(model prismaModel) []*evidenceUnit {
|
|
|
493
503
|
Symbol: field.Symbol,
|
|
494
504
|
Readable: "Prisma " + field.Symbol + " '" + model.Name + "." + field.Name + "'",
|
|
495
505
|
Hidden: fieldHidden,
|
|
506
|
+
Digest: field.Digest,
|
|
496
507
|
})
|
|
497
508
|
}
|
|
498
509
|
return units
|
|
@@ -566,6 +577,29 @@ func prismaDeclarationsFromComments(
|
|
|
566
577
|
inventory.Declarations = append(inventory.Declarations, declaration)
|
|
567
578
|
}
|
|
568
579
|
}
|
|
580
|
+
// An unattached run is the one position that accepts `@evidenceExclude`
|
|
581
|
+
// and never `@evidence`, and a lint-only `.schema` ledger is built out of
|
|
582
|
+
// exactly these. Its exclusions owe a review like any other
|
|
583
|
+
// acknowledgement, so the review has to be readable here too. Collecting
|
|
584
|
+
// it only where a declaration is attached would leave the ledger reporting
|
|
585
|
+
// every exclusion unreviewed forever, with the repair written in a comment
|
|
586
|
+
// nothing reads back — the same dead end the attached case just escaped.
|
|
587
|
+
for _, review := range parseReviews(run.Body) {
|
|
588
|
+
shared := &evidenceReview{
|
|
589
|
+
HostID: "prisma:" + run.Path + ":file",
|
|
590
|
+
SemanticHostIDs: []string{"prisma:" + run.Path + ":file"},
|
|
591
|
+
Reviews: review.Reviews,
|
|
592
|
+
Type: artifactPrisma,
|
|
593
|
+
Target: review.Target,
|
|
594
|
+
Fingerprint: review.Fingerprint,
|
|
595
|
+
Description: review.Description,
|
|
596
|
+
Path: run.Path,
|
|
597
|
+
Line: run.Line + review.LineOffset,
|
|
598
|
+
}
|
|
599
|
+
for _, inventory := range hosted {
|
|
600
|
+
inventory.Reviews = append(inventory.Reviews, shared)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
569
603
|
continue
|
|
570
604
|
}
|
|
571
605
|
if run.Key == "" {
|
|
@@ -622,6 +656,28 @@ func prismaDeclarationsFromComments(
|
|
|
622
656
|
inventory.Declarations = append(inventory.Declarations, declaration)
|
|
623
657
|
}
|
|
624
658
|
}
|
|
659
|
+
// Reviews are read here for the same reason the citations are. Without
|
|
660
|
+
// this, a Prisma claim citing a reference that requires a review reports
|
|
661
|
+
// every citation unreviewed forever, and the repair the message names —
|
|
662
|
+
// write the tag in this documentation comment — has no effect, because
|
|
663
|
+
// nothing reads the tag back out. A diagnostic must never name a repair the
|
|
664
|
+
// author cannot perform.
|
|
665
|
+
for _, review := range parseReviews(run.Body) {
|
|
666
|
+
shared := &evidenceReview{
|
|
667
|
+
HostID: host.ID,
|
|
668
|
+
SemanticHostIDs: []string{host.ID},
|
|
669
|
+
Reviews: review.Reviews,
|
|
670
|
+
Type: artifactPrisma,
|
|
671
|
+
Target: review.Target,
|
|
672
|
+
Fingerprint: review.Fingerprint,
|
|
673
|
+
Description: review.Description,
|
|
674
|
+
Path: run.Path,
|
|
675
|
+
Line: run.Line + review.LineOffset,
|
|
676
|
+
}
|
|
677
|
+
for _, inventory := range hosted {
|
|
678
|
+
inventory.Reviews = append(inventory.Reviews, shared)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
625
681
|
}
|
|
626
682
|
return problems
|
|
627
683
|
}
|