@ttsc/evidence 0.31.5 → 0.32.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 +13 -0
- package/native/declaration.go +169 -50
- package/native/documentation_regions.go +51 -0
- package/native/markdown.go +247 -16
- package/native/prisma.go +6 -8
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -117,6 +117,19 @@ One claim: the components under `src` implement the docs, so every H2 and H3 und
|
|
|
117
117
|
|
|
118
118
|
The configuration is written once. The tags are written forever: `@evidence` cites, `@evidenceReview` verifies, and `@evidenceExclude` declines. [The tag reference](https://ttsc.dev/docs/evidence/tags) has the full grammar.
|
|
119
119
|
|
|
120
|
+
A reason that needs paragraphs, a list or subheadings goes under an H2 to H4 heading, with the target on the heading and the explanation below it:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
/**
|
|
124
|
+
* ## @evidence docs/discount.md#coupon-stacking
|
|
125
|
+
*
|
|
126
|
+
* Renders the per-issuer limit this section defines.
|
|
127
|
+
*
|
|
128
|
+
* - Reads the limit from props.
|
|
129
|
+
* - Names the issuer that reached it.
|
|
130
|
+
*/
|
|
131
|
+
```
|
|
132
|
+
|
|
120
133
|
A false tag removes the error, not the problem. `@evidenceReview` asks for a separate statement of what was verified, which a human then judges:
|
|
121
134
|
|
|
122
135
|
```ts
|
package/native/declaration.go
CHANGED
|
@@ -60,70 +60,177 @@ func parseCommentDeclarations(
|
|
|
60
60
|
tagBoundaries bool,
|
|
61
61
|
) []parsedDeclaration {
|
|
62
62
|
body, leadingLines, documented := documentationCommentBody(comment)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
return parseRegionDeclarations(
|
|
64
|
+
documentationRegions(body, false),
|
|
65
|
+
documented || tagBoundaries,
|
|
66
|
+
leadingLines,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// parseRegionDeclarations reads the citations and exclusions of documentation
|
|
71
|
+
// lines whose examples are already classified.
|
|
72
|
+
//
|
|
73
|
+
// It owns the annotation grammar independently of where the lines came from, so
|
|
74
|
+
// every host that can spell an annotation agrees on where one begins, what it
|
|
75
|
+
// says and where it ends. tagBoundaries has the meaning documented on
|
|
76
|
+
// parseCommentDeclarations.
|
|
77
|
+
func parseRegionDeclarations(
|
|
78
|
+
regions []documentationRegionLine,
|
|
79
|
+
tagBoundaries bool,
|
|
80
|
+
leadingLines int,
|
|
81
|
+
) []parsedDeclaration {
|
|
82
|
+
var pending *pendingAnnotation
|
|
70
83
|
parsed := []parsedDeclaration{}
|
|
71
84
|
flush := func() {
|
|
72
85
|
if pending == nil {
|
|
73
86
|
return
|
|
74
87
|
}
|
|
75
|
-
target, reason :=
|
|
88
|
+
target, reason := pending.split()
|
|
76
89
|
parsed = append(parsed, parsedDeclaration{
|
|
77
|
-
Tag: pending.tag,
|
|
90
|
+
Tag: pending.opener.tag,
|
|
78
91
|
Target: target,
|
|
79
92
|
Reason: reason,
|
|
80
93
|
LineOffset: leadingLines + pending.lineOffset,
|
|
81
94
|
})
|
|
82
95
|
pending = nil
|
|
83
96
|
}
|
|
84
|
-
for index, region := range
|
|
97
|
+
for index, region := range regions {
|
|
85
98
|
if region.fence {
|
|
86
|
-
|
|
99
|
+
// An example never supplies prose. A line-form annotation ends at it; a
|
|
100
|
+
// heading section resumes after it, because headings rather than examples
|
|
101
|
+
// decide where a section ends.
|
|
102
|
+
if !pending.section() {
|
|
103
|
+
flush()
|
|
104
|
+
}
|
|
87
105
|
continue
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
tag, body, found := declarationLine(line)
|
|
91
|
-
if found {
|
|
107
|
+
if opener, opened := region.opener(); opened {
|
|
92
108
|
flush()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
// A review closes the declaration above it in every host, and it does so
|
|
110
|
+
// regardless of tagBoundaries. That flag answers whether *another tool's*
|
|
111
|
+
// `@tag` is a boundary, which is a property of the host's comment grammar:
|
|
112
|
+
// an HTML comment has no field syntax, so `@architecture approved this` is
|
|
113
|
+
// ordinary prose belonging to the reason above it, and a test pins that.
|
|
114
|
+
//
|
|
115
|
+
// A review is not another tool's tag. It belongs to this grammar, so it is
|
|
116
|
+
// always a boundary. Without this, an `@evidenceReview` written under an
|
|
117
|
+
// `@evidence` inside one Markdown comment was swallowed into that citation's
|
|
118
|
+
// reason: the review vanished and the reason grew a sentence its author
|
|
119
|
+
// addressed to a different question.
|
|
120
|
+
if !opener.review {
|
|
121
|
+
pending = &pendingAnnotation{opener: opener, lineOffset: index}
|
|
97
122
|
}
|
|
98
123
|
continue
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
// regardless of tagBoundaries. That flag answers whether *another tool's*
|
|
102
|
-
// `@tag` is a boundary, which is a property of the host's comment grammar:
|
|
103
|
-
// an HTML comment has no field syntax, so `@architecture approved this` is
|
|
104
|
-
// ordinary prose belonging to the reason above it, and a test pins that.
|
|
105
|
-
//
|
|
106
|
-
// A review is not another tool's tag. It belongs to this grammar, so it is
|
|
107
|
-
// always a boundary. Without this, an `@evidenceReview` written under an
|
|
108
|
-
// `@evidence` inside one Markdown comment was swallowed into that citation's
|
|
109
|
-
// reason: the review vanished and the reason grew a sentence its author
|
|
110
|
-
// addressed to a different question.
|
|
111
|
-
if _, _, opened := reviewLine(line); opened {
|
|
125
|
+
if pending.closedBy(region) {
|
|
112
126
|
flush()
|
|
113
127
|
continue
|
|
114
128
|
}
|
|
115
|
-
if
|
|
129
|
+
if tagBoundaries && region.opensTagBoundary() {
|
|
116
130
|
flush()
|
|
117
131
|
continue
|
|
118
132
|
}
|
|
119
133
|
if pending != nil {
|
|
120
|
-
pending.
|
|
134
|
+
pending.lines = append(pending.lines, region.text)
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
flush()
|
|
124
138
|
return parsed
|
|
125
139
|
}
|
|
126
140
|
|
|
141
|
+
// annotationOpener is the start of one annotation as a documentation line
|
|
142
|
+
// spells it.
|
|
143
|
+
//
|
|
144
|
+
// The line form opens with the tag itself. The heading form is an H2, H3 or H4
|
|
145
|
+
// heading whose title opens with the tag, and it exists for explanations long
|
|
146
|
+
// enough to need paragraphs, lists and subheadings of their own.
|
|
147
|
+
type annotationOpener struct {
|
|
148
|
+
// review reports a verification statement rather than an acknowledgement.
|
|
149
|
+
review bool
|
|
150
|
+
|
|
151
|
+
// tag is the acknowledgement kind, or for a review the kind it answers for.
|
|
152
|
+
tag tagKind
|
|
153
|
+
|
|
154
|
+
// body is what follows the tag on the opening line: the target, and any
|
|
155
|
+
// reason or review remainder written beside it.
|
|
156
|
+
body string
|
|
157
|
+
|
|
158
|
+
// heading is the depth of a heading-form opener, or zero for the line form.
|
|
159
|
+
heading int
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// openAnnotation reads the annotation one documentation line opens, if any.
|
|
163
|
+
//
|
|
164
|
+
// A heading of another depth stays an ordinary heading, and no heading opens
|
|
165
|
+
// the line form because its first character is `#`. The five tags share one
|
|
166
|
+
// spelling in both forms, so the two forms cannot disagree about which tag a
|
|
167
|
+
// line carries: the title of a heading is read by the same matchers as a line.
|
|
168
|
+
func openAnnotation(text string, heading int, title string) (annotationOpener, bool) {
|
|
169
|
+
if heading != 0 {
|
|
170
|
+
if heading < 2 || heading > 4 {
|
|
171
|
+
return annotationOpener{}, false
|
|
172
|
+
}
|
|
173
|
+
text = title
|
|
174
|
+
}
|
|
175
|
+
if tag, body, found := declarationLine(text); found {
|
|
176
|
+
return annotationOpener{tag: tag, body: body, heading: heading}, true
|
|
177
|
+
}
|
|
178
|
+
if reviews, remainder, opened := reviewLine(text); opened {
|
|
179
|
+
return annotationOpener{review: true, tag: reviews, body: remainder, heading: heading}, true
|
|
180
|
+
}
|
|
181
|
+
return annotationOpener{}, false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// opensAcknowledgement reports whether a documentation line opens a citation
|
|
185
|
+
// or an exclusion in either form, for a caller that reports a tag it cannot
|
|
186
|
+
// read and so needs only to know that one is there.
|
|
187
|
+
func opensAcknowledgement(text string, heading int, title string) bool {
|
|
188
|
+
opener, opened := openAnnotation(text, heading, title)
|
|
189
|
+
return opened && !opener.review
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// pendingAnnotation accumulates one annotation until a boundary ends it.
|
|
193
|
+
type pendingAnnotation struct {
|
|
194
|
+
opener annotationOpener
|
|
195
|
+
|
|
196
|
+
// lines are the documentation lines after the opener that continue it.
|
|
197
|
+
lines []string
|
|
198
|
+
|
|
199
|
+
// lineOffset is the opener's line within the documentation body.
|
|
200
|
+
lineOffset int
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// section reports whether this is a heading-form annotation. A nil receiver is
|
|
204
|
+
// no annotation at all.
|
|
205
|
+
func (pending *pendingAnnotation) section() bool {
|
|
206
|
+
return pending != nil && pending.opener.heading != 0
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// closedBy reports whether a heading ends this annotation's explanation.
|
|
210
|
+
//
|
|
211
|
+
// Only a heading section has that structure. A heading of its own depth or
|
|
212
|
+
// shallower is the next part of the surrounding document; a deeper heading
|
|
213
|
+
// divides the explanation itself and stays inside it. A line-form annotation
|
|
214
|
+
// keeps the continuation it always had, which a heading does not end.
|
|
215
|
+
func (pending *pendingAnnotation) closedBy(region documentationRegionLine) bool {
|
|
216
|
+
return pending.section() && region.heading != 0 && region.heading <= pending.opener.heading
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// split returns the annotation's target and the text after it.
|
|
220
|
+
//
|
|
221
|
+
// A line-form target may be written on the line after its tag, so its lines
|
|
222
|
+
// are split as one body, as they always were. A heading section's target must
|
|
223
|
+
// be on the heading: the heading body is split alone, which keeps the
|
|
224
|
+
// explanation from ever becoming a target and keeps an unterminated link on the
|
|
225
|
+
// heading from borrowing a closing brace from the prose under it.
|
|
226
|
+
func (pending *pendingAnnotation) split() (string, string) {
|
|
227
|
+
if !pending.section() {
|
|
228
|
+
return splitDeclarationBody(strings.Join(append([]string{pending.opener.body}, pending.lines...), "\n"))
|
|
229
|
+
}
|
|
230
|
+
target, remainder := splitDeclarationBody(pending.opener.body)
|
|
231
|
+
return target, strings.TrimSpace(strings.Join(append([]string{remainder}, pending.lines...), "\n"))
|
|
232
|
+
}
|
|
233
|
+
|
|
127
234
|
func declarationLine(line string) (tagKind, string, bool) {
|
|
128
235
|
for _, candidate := range []struct {
|
|
129
236
|
marker string
|
|
@@ -314,43 +421,55 @@ func (review parsedReview) marker() string {
|
|
|
314
421
|
// block with `@param` or a following `@evidence` swallows neither.
|
|
315
422
|
func parseReviews(comment string) []parsedReview {
|
|
316
423
|
comment, leadingLines, _ := documentationCommentBody(comment)
|
|
424
|
+
return parseRegionReviews(documentationRegions(comment, true), leadingLines)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// parseRegionReviews reads the reviews of documentation lines whose examples
|
|
428
|
+
// are already classified, under the grammar parseRegionDeclarations applies to
|
|
429
|
+
// acknowledgements. A heading-form review may carry its fingerprint on the
|
|
430
|
+
// heading or open its description with it.
|
|
431
|
+
func parseRegionReviews(regions []documentationRegionLine, leadingLines int) []parsedReview {
|
|
317
432
|
reviews := []parsedReview{}
|
|
318
|
-
var pending *
|
|
319
|
-
var body []string
|
|
433
|
+
var pending *pendingAnnotation
|
|
320
434
|
flush := func() {
|
|
321
435
|
if pending == nil {
|
|
322
436
|
return
|
|
323
437
|
}
|
|
324
|
-
target, remainder :=
|
|
438
|
+
target, remainder := pending.split()
|
|
325
439
|
fingerprint, description := splitReviewFingerprint(remainder)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
440
|
+
reviews = append(reviews, parsedReview{
|
|
441
|
+
Reviews: pending.opener.tag,
|
|
442
|
+
Target: target,
|
|
443
|
+
Fingerprint: fingerprint,
|
|
444
|
+
Description: description,
|
|
445
|
+
LineOffset: leadingLines + pending.lineOffset,
|
|
446
|
+
})
|
|
330
447
|
pending = nil
|
|
331
|
-
body = nil
|
|
332
448
|
}
|
|
333
|
-
for index, region := range
|
|
449
|
+
for index, region := range regions {
|
|
334
450
|
if region.fence {
|
|
335
|
-
|
|
451
|
+
if !pending.section() {
|
|
452
|
+
flush()
|
|
453
|
+
}
|
|
336
454
|
continue
|
|
337
455
|
}
|
|
338
|
-
|
|
339
|
-
if reviews, remainder, opened := reviewLine(line); opened {
|
|
456
|
+
if opener, opened := region.opener(); opened {
|
|
340
457
|
flush()
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
LineOffset: leadingLines + index,
|
|
458
|
+
if opener.review {
|
|
459
|
+
pending = &pendingAnnotation{opener: opener, lineOffset: index}
|
|
344
460
|
}
|
|
345
|
-
body = []string{remainder}
|
|
346
461
|
continue
|
|
347
462
|
}
|
|
348
|
-
if
|
|
463
|
+
if pending.closedBy(region) {
|
|
464
|
+
flush()
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
if region.opensTagBoundary() {
|
|
349
468
|
flush()
|
|
350
469
|
continue
|
|
351
470
|
}
|
|
352
471
|
if pending != nil {
|
|
353
|
-
|
|
472
|
+
pending.lines = append(pending.lines, region.text)
|
|
354
473
|
}
|
|
355
474
|
}
|
|
356
475
|
flush()
|
|
@@ -13,6 +13,52 @@ type documentationRegionLine struct {
|
|
|
13
13
|
|
|
14
14
|
// fence ends pending prose and suppresses tags throughout the example.
|
|
15
15
|
fence bool
|
|
16
|
+
|
|
17
|
+
// heading is the ATX depth when the line is a heading, or zero. A heading
|
|
18
|
+
// annotation's explanation ends at a heading of its own depth or shallower.
|
|
19
|
+
heading int
|
|
20
|
+
|
|
21
|
+
// title is the heading text without closing hashes or an explicit anchor,
|
|
22
|
+
// which are heading syntax and never part of an annotation's target or reason.
|
|
23
|
+
title string
|
|
24
|
+
|
|
25
|
+
// explanatory marks prose that can only continue an explanation: it opens no
|
|
26
|
+
// one-line annotation and ends none. Visible Markdown is read this way,
|
|
27
|
+
// because rendered prose carries annotations only under headings.
|
|
28
|
+
explanatory bool
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// opener reads the annotation this line opens, if any. An explanatory line
|
|
32
|
+
// opens one only as a heading.
|
|
33
|
+
func (region documentationRegionLine) opener() (annotationOpener, bool) {
|
|
34
|
+
if region.explanatory && region.heading == 0 {
|
|
35
|
+
return annotationOpener{}, false
|
|
36
|
+
}
|
|
37
|
+
return openAnnotation(region.text, region.heading, region.title)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// opensTagBoundary reports whether the line opens some `@tag`, which ends a
|
|
41
|
+
// pending explanation in hosts whose grammar gives tags that meaning.
|
|
42
|
+
func (region documentationRegionLine) opensTagBoundary() bool {
|
|
43
|
+
return !region.explanatory && strings.HasPrefix(region.text, "@")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// documentationHeading reads an ATX heading from one line of documentation
|
|
47
|
+
// prose or of a Markdown file's heading view, either of which may keep up to
|
|
48
|
+
// three columns of margin.
|
|
49
|
+
//
|
|
50
|
+
// It uses the Markdown scanner's own heading grammar, so a heading inside a
|
|
51
|
+
// documentation comment and a heading in a Markdown file agree on depth,
|
|
52
|
+
// required spacing and closing hashes. The explicit `{#anchor}` suffix is
|
|
53
|
+
// removed because it names the heading for Markdown addressing, which an
|
|
54
|
+
// annotation's target or reason must never absorb.
|
|
55
|
+
func documentationHeading(text string) (int, string) {
|
|
56
|
+
level, title, ok := markdownHeading(text)
|
|
57
|
+
if !ok {
|
|
58
|
+
return 0, ""
|
|
59
|
+
}
|
|
60
|
+
title, _ = markdownHeadingIdentity(title)
|
|
61
|
+
return level, title
|
|
16
62
|
}
|
|
17
63
|
|
|
18
64
|
// documentationCommentBody removes only a real native wrapper. Mapped Prisma
|
|
@@ -139,6 +185,11 @@ func documentationRegions(body string, slashDecoration bool) []documentationRegi
|
|
|
139
185
|
at++
|
|
140
186
|
}
|
|
141
187
|
result[index].text = strings.TrimSpace(string(masked))
|
|
188
|
+
// A heading allows the same three columns of margin a fence does; four or
|
|
189
|
+
// more make the line indented code, which opens and ends nothing.
|
|
190
|
+
if relative >= 0 && relative <= 3 {
|
|
191
|
+
result[index].heading, result[index].title = documentationHeading(result[index].text)
|
|
192
|
+
}
|
|
142
193
|
offset += len(line) + 1
|
|
143
194
|
}
|
|
144
195
|
return result
|
package/native/markdown.go
CHANGED
|
@@ -665,13 +665,55 @@ func scanMarkdownInventory(
|
|
|
665
665
|
currentHost := "file"
|
|
666
666
|
currentHostID := fileUnitID
|
|
667
667
|
lexical, commentSpans := scanMarkdownRegions(content, lines)
|
|
668
|
+
sections := markdownAnnotationSections(lexical)
|
|
669
|
+
sectionAt := make([]int, len(lines))
|
|
670
|
+
for index := range sectionAt {
|
|
671
|
+
sectionAt[index] = -1
|
|
672
|
+
}
|
|
673
|
+
for position, section := range sections {
|
|
674
|
+
for index := section.start; index < section.end; index++ {
|
|
675
|
+
sectionAt[index] = position
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// The latest outline heading at each depth, so an annotation section can
|
|
679
|
+
// name the heading it is nested in. Annotation headings never enter it: they
|
|
680
|
+
// are metadata about a section, not sections of their own.
|
|
681
|
+
outlineHosts := [7]markdownHost{}
|
|
682
|
+
sectionOwners := make([]markdownHost, len(sections))
|
|
668
683
|
headingUnitIDs := [5]string{}
|
|
669
684
|
for index := range lines {
|
|
685
|
+
if position := sectionAt[index]; position >= 0 {
|
|
686
|
+
section := sections[position]
|
|
687
|
+
if index == section.start {
|
|
688
|
+
// An annotation answers for the section it is nested in: the nearest
|
|
689
|
+
// heading shallower than its own, or the file. A heading of its own
|
|
690
|
+
// depth is a sibling it does not belong to.
|
|
691
|
+
sectionOwners[position] = markdownHost{symbol: "file", id: fileUnitID}
|
|
692
|
+
for depth := section.level - 1; depth >= 1; depth-- {
|
|
693
|
+
if outlineHosts[depth].symbol != "" {
|
|
694
|
+
sectionOwners[position] = outlineHosts[depth]
|
|
695
|
+
break
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Every line of the section, including a comment written in it, belongs
|
|
700
|
+
// to that owner, and none of them is any unit's content: an annotation is
|
|
701
|
+
// metadata exactly as an HTML comment is, so writing or editing one
|
|
702
|
+
// expires no review.
|
|
703
|
+
hostAtLine[index] = sectionOwners[position].symbol
|
|
704
|
+
hostIDAtLine[index] = sectionOwners[position].id
|
|
705
|
+
digestHostIDAtLine[index] = ""
|
|
706
|
+
continue
|
|
707
|
+
}
|
|
670
708
|
visible := lexical[index].Heading
|
|
671
709
|
level, title, ok := markdownHeading(visible)
|
|
672
710
|
if ok {
|
|
673
711
|
currentHost = "h" + decimal(level)
|
|
674
712
|
currentHostID = "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1)
|
|
713
|
+
outlineHosts[level] = markdownHost{symbol: currentHost, id: currentHostID}
|
|
714
|
+
for deeper := level + 1; deeper < len(outlineHosts); deeper++ {
|
|
715
|
+
outlineHosts[deeper] = markdownHost{}
|
|
716
|
+
}
|
|
675
717
|
if level <= 4 {
|
|
676
718
|
for descendantLevel := level; descendantLevel <= 4; descendantLevel++ {
|
|
677
719
|
headingUnitIDs[descendantLevel] = ""
|
|
@@ -750,7 +792,7 @@ func scanMarkdownInventory(
|
|
|
750
792
|
digestHostIDAtLine[index] = currentDigestHostID
|
|
751
793
|
}
|
|
752
794
|
|
|
753
|
-
reportUnreadableMarkdownTags(inventory, address.Display, lexical)
|
|
795
|
+
reportUnreadableMarkdownTags(inventory, address.Display, lexical, sections)
|
|
754
796
|
|
|
755
797
|
sequence := 0
|
|
756
798
|
for _, match := range commentSpans {
|
|
@@ -795,10 +837,50 @@ func scanMarkdownInventory(
|
|
|
795
837
|
})
|
|
796
838
|
}
|
|
797
839
|
}
|
|
840
|
+
for position, section := range sections {
|
|
841
|
+
owner := sectionOwners[position]
|
|
842
|
+
regions := markdownSectionRegions(lines, lexical, section)
|
|
843
|
+
for _, parsed := range parseRegionDeclarations(regions, false, 0) {
|
|
844
|
+
sequence++
|
|
845
|
+
line := section.start + 1 + parsed.LineOffset
|
|
846
|
+
inventory.Declarations = append(inventory.Declarations, &evidenceDeclaration{
|
|
847
|
+
ID: "markdown:" + address.Key + ":" + decimal(line) + ":" + decimal(sequence),
|
|
848
|
+
HostID: owner.id,
|
|
849
|
+
SemanticHostIDs: []string{owner.id},
|
|
850
|
+
Type: artifactMarkdown,
|
|
851
|
+
Tag: parsed.Tag,
|
|
852
|
+
Target: parsed.Target,
|
|
853
|
+
Reason: parsed.Reason,
|
|
854
|
+
Hosts: symbolSet{owner.symbol: true},
|
|
855
|
+
Path: address.Display,
|
|
856
|
+
Line: line,
|
|
857
|
+
Sequence: sequence,
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
for _, review := range parseRegionReviews(regions, 0) {
|
|
861
|
+
inventory.Reviews = append(inventory.Reviews, &evidenceReview{
|
|
862
|
+
SemanticHostIDs: []string{owner.id},
|
|
863
|
+
Reviews: review.Reviews,
|
|
864
|
+
Type: artifactMarkdown,
|
|
865
|
+
Target: review.Target,
|
|
866
|
+
Fingerprint: review.Fingerprint,
|
|
867
|
+
Description: review.Description,
|
|
868
|
+
Path: address.Display,
|
|
869
|
+
Line: section.start + 1 + review.LineOffset,
|
|
870
|
+
})
|
|
871
|
+
}
|
|
872
|
+
}
|
|
798
873
|
assignMarkdownDigests(inventory, content, lines, digestHostIDAtLine, commentSpans)
|
|
799
874
|
return inventory, problems
|
|
800
875
|
}
|
|
801
876
|
|
|
877
|
+
// markdownHost names the host a Markdown line belongs to: the symbol a claim
|
|
878
|
+
// selects it by and the identity its declarations record.
|
|
879
|
+
type markdownHost struct {
|
|
880
|
+
symbol string
|
|
881
|
+
id string
|
|
882
|
+
}
|
|
883
|
+
|
|
802
884
|
// assignMarkdownDigests gives every unit the text it alone owns.
|
|
803
885
|
//
|
|
804
886
|
// A heading owns its own line and the body under it up to the next heading, and a
|
|
@@ -978,6 +1060,132 @@ func selectedByMarkdownPopulation(
|
|
|
978
1060
|
return false
|
|
979
1061
|
}
|
|
980
1062
|
|
|
1063
|
+
// markdownAnnotationSection is one visible heading annotation: the zero-based
|
|
1064
|
+
// line of its opening heading, the line after its explanation, and the
|
|
1065
|
+
// opening heading's depth.
|
|
1066
|
+
type markdownAnnotationSection struct {
|
|
1067
|
+
start int
|
|
1068
|
+
end int
|
|
1069
|
+
level int
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// markdownAnnotationSections finds every visible H2-H4 annotation heading and
|
|
1073
|
+
// the explanation under it.
|
|
1074
|
+
//
|
|
1075
|
+
// The headings are the outline's own: the lexical heading view decides them,
|
|
1076
|
+
// so a heading inside code, a rendered example or a comment opens nothing, and
|
|
1077
|
+
// one inside a blockquote or a list is not a heading of the document at all. A
|
|
1078
|
+
// section ends at the next heading of its depth or shallower, which is the next
|
|
1079
|
+
// part of the document, or at the next annotation heading, which starts its
|
|
1080
|
+
// own. A deeper ordinary heading divides the explanation and stays inside it.
|
|
1081
|
+
func markdownAnnotationSections(lexical []markdownLexicalLine) []markdownAnnotationSection {
|
|
1082
|
+
sections := []markdownAnnotationSection{}
|
|
1083
|
+
for index := 0; index < len(lexical); {
|
|
1084
|
+
level, title := documentationHeading(lexical[index].Heading)
|
|
1085
|
+
if _, opened := openAnnotation(lexical[index].Heading, level, title); level == 0 || !opened {
|
|
1086
|
+
index++
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
end := index + 1
|
|
1090
|
+
for ; end < len(lexical); end++ {
|
|
1091
|
+
next, nextTitle := documentationHeading(lexical[end].Heading)
|
|
1092
|
+
if next == 0 {
|
|
1093
|
+
continue
|
|
1094
|
+
}
|
|
1095
|
+
if _, opened := openAnnotation(lexical[end].Heading, next, nextTitle); opened || next <= level {
|
|
1096
|
+
break
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
sections = append(sections, markdownAnnotationSection{start: index, end: end, level: level})
|
|
1100
|
+
index = end
|
|
1101
|
+
}
|
|
1102
|
+
return sections
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// markdownSectionRegions presents one visible annotation section to the shared
|
|
1106
|
+
// annotation grammar.
|
|
1107
|
+
//
|
|
1108
|
+
// Headings keep their outline reading, so the grammar sees the same depths the
|
|
1109
|
+
// outline does. Every other line is explanatory prose: it can explain, but in
|
|
1110
|
+
// rendered Markdown only a heading carries an annotation, so it opens and ends
|
|
1111
|
+
// none.
|
|
1112
|
+
func markdownSectionRegions(
|
|
1113
|
+
lines []string,
|
|
1114
|
+
lexical []markdownLexicalLine,
|
|
1115
|
+
section markdownAnnotationSection,
|
|
1116
|
+
) []documentationRegionLine {
|
|
1117
|
+
regions := make([]documentationRegionLine, 0, section.end-section.start)
|
|
1118
|
+
for index := section.start; index < section.end; index++ {
|
|
1119
|
+
if level, title := documentationHeading(lexical[index].Heading); level != 0 {
|
|
1120
|
+
regions = append(regions, documentationRegionLine{
|
|
1121
|
+
text: strings.TrimSpace(lexical[index].Heading),
|
|
1122
|
+
heading: level,
|
|
1123
|
+
title: title,
|
|
1124
|
+
explanatory: true,
|
|
1125
|
+
})
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
regions = append(regions, documentationRegionLine{
|
|
1129
|
+
text: markdownReadableProse(strings.TrimSuffix(lines[index], "\r"), lexical[index]),
|
|
1130
|
+
explanatory: true,
|
|
1131
|
+
})
|
|
1132
|
+
}
|
|
1133
|
+
return regions
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// markdownReadableProse returns what a reader of the rendered document reads
|
|
1137
|
+
// as prose on one line, or nothing when the line is no prose.
|
|
1138
|
+
//
|
|
1139
|
+
// Whether a line explains is decided by the prose view: a line left empty once
|
|
1140
|
+
// code, rendered examples, comments and escapes are removed is an example or
|
|
1141
|
+
// metadata, so an example cannot supply a missing reason. Two visible lines are
|
|
1142
|
+
// no prose either. A closing fence is the code block's syntax although the
|
|
1143
|
+
// parser leaves it outside the block, and a line opening a tag is an
|
|
1144
|
+
// annotation nothing reads, which is reported on its own rather than becoming
|
|
1145
|
+
// the explanation of another.
|
|
1146
|
+
//
|
|
1147
|
+
// What an explaining line says is read from the heading view instead, which
|
|
1148
|
+
// removes comments but keeps inline code and the other inline text a reader
|
|
1149
|
+
// reads, so a review description shown in a diagnostic keeps its words.
|
|
1150
|
+
func markdownReadableProse(line string, lexical markdownLexicalLine) string {
|
|
1151
|
+
if _, opened := markdownProseTag(markdownLineContent(lexical.Prose)); opened {
|
|
1152
|
+
return ""
|
|
1153
|
+
}
|
|
1154
|
+
prose := markdownKeptText(line, lexical.Prose)
|
|
1155
|
+
if prose == "" {
|
|
1156
|
+
return ""
|
|
1157
|
+
}
|
|
1158
|
+
if _, _, _, fence := markdownFence(markdownLineContent(prose)); fence {
|
|
1159
|
+
return ""
|
|
1160
|
+
}
|
|
1161
|
+
return markdownKeptText(line, lexical.Heading)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// markdownKeptText keeps the bytes of a line that a lexical view left
|
|
1165
|
+
// unmasked. A view never moves a byte, so comparing positions is exact, and a
|
|
1166
|
+
// sentinel a view writes over masked bytes differs from the source it hides.
|
|
1167
|
+
func markdownKeptText(line string, view string) string {
|
|
1168
|
+
kept := []byte(line)
|
|
1169
|
+
for index := range kept {
|
|
1170
|
+
if index >= len(view) || view[index] != line[index] {
|
|
1171
|
+
kept[index] = ' '
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return strings.TrimSpace(string(kept))
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// markdownProseTag returns the tag a line of visible prose opens, spelled the
|
|
1178
|
+
// way an unreadable-tag diagnostic names it.
|
|
1179
|
+
func markdownProseTag(content string) (string, bool) {
|
|
1180
|
+
if tag, _, found := declarationLine(content); found {
|
|
1181
|
+
return "@" + string(tag), true
|
|
1182
|
+
}
|
|
1183
|
+
if reviews, _, opened := reviewLine(content); opened {
|
|
1184
|
+
return reviewMarkerFor(reviews), true
|
|
1185
|
+
}
|
|
1186
|
+
return "", false
|
|
1187
|
+
}
|
|
1188
|
+
|
|
981
1189
|
func markdownFence(line string) (rune, int, string, bool) {
|
|
982
1190
|
indent := 0
|
|
983
1191
|
for indent < len(line) && line[indent] == ' ' {
|
|
@@ -1060,12 +1268,17 @@ func markdownSlug(title string) string {
|
|
|
1060
1268
|
// reportUnreadableMarkdownTags records every tag written where this artifact
|
|
1061
1269
|
// kind cannot read one.
|
|
1062
1270
|
//
|
|
1063
|
-
// A Markdown declaration is read from an HTML comment,
|
|
1064
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1271
|
+
// A Markdown declaration is read from an HTML comment or from an H2, H3 or H4
|
|
1272
|
+
// annotation heading of the document. Written as prose it reaches no host and
|
|
1273
|
+
// used to be discarded without a word, leaving the coverage diagnostic that
|
|
1274
|
+
// follows to name the reference and suggest writing the citation the author
|
|
1275
|
+
// had already written. TypeScript answers this shape and the Prisma bridge
|
|
1276
|
+
// answers its own; this is the kind that was left silent.
|
|
1277
|
+
//
|
|
1278
|
+
// A heading whose title opens a tag is the same mistake when it carries
|
|
1279
|
+
// nothing: an H1, H5 or H6, or a heading inside a blockquote or a list, which
|
|
1280
|
+
// is no heading of the document's outline. Every heading that does open an
|
|
1281
|
+
// annotation section is skipped, since the section reads it.
|
|
1069
1282
|
//
|
|
1070
1283
|
// A fenced block is an example rather than a citation and stays silent, which
|
|
1071
1284
|
// is not a concession: this product's own documentation shows tags inside
|
|
@@ -1082,18 +1295,28 @@ func reportUnreadableMarkdownTags(
|
|
|
1082
1295
|
inventory *artifactInventory,
|
|
1083
1296
|
location string,
|
|
1084
1297
|
lexical []markdownLexicalLine,
|
|
1298
|
+
sections []markdownAnnotationSection,
|
|
1085
1299
|
) {
|
|
1086
1300
|
if inventory == nil {
|
|
1087
1301
|
return
|
|
1088
1302
|
}
|
|
1303
|
+
openers := make(map[int]bool, len(sections))
|
|
1304
|
+
for _, section := range sections {
|
|
1305
|
+
openers[section.start] = true
|
|
1306
|
+
}
|
|
1089
1307
|
for index, line := range lexical {
|
|
1090
1308
|
trimmed := markdownLineContent(line.Prose)
|
|
1091
|
-
if tag,
|
|
1092
|
-
inventory.Unreadable = append(inventory.Unreadable, unreadableMarkdownProblem(
|
|
1309
|
+
if tag, found := markdownProseTag(trimmed); found {
|
|
1310
|
+
inventory.Unreadable = append(inventory.Unreadable, unreadableMarkdownProblem(tag, location, index+1, false))
|
|
1093
1311
|
continue
|
|
1094
1312
|
}
|
|
1095
|
-
if
|
|
1096
|
-
|
|
1313
|
+
if openers[index] {
|
|
1314
|
+
continue
|
|
1315
|
+
}
|
|
1316
|
+
if level, title := documentationHeading(trimmed); level != 0 {
|
|
1317
|
+
if tag, found := markdownProseTag(title); found {
|
|
1318
|
+
inventory.Unreadable = append(inventory.Unreadable, unreadableMarkdownProblem(tag, location, index+1, true))
|
|
1319
|
+
}
|
|
1097
1320
|
}
|
|
1098
1321
|
}
|
|
1099
1322
|
}
|
|
@@ -1145,11 +1368,19 @@ func markdownListMarker(content string) int {
|
|
|
1145
1368
|
return digits + 2
|
|
1146
1369
|
}
|
|
1147
1370
|
|
|
1148
|
-
// unreadableMarkdownProblem names the position and the
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1371
|
+
// unreadableMarkdownProblem names the position and the moves that fix it.
|
|
1372
|
+
//
|
|
1373
|
+
// Both carriers are named because either repairs the line: a visible heading
|
|
1374
|
+
// keeps a long explanation readable, and a comment keeps a short one out of the
|
|
1375
|
+
// rendered page.
|
|
1376
|
+
func unreadableMarkdownProblem(tag string, location string, line int, heading bool) string {
|
|
1377
|
+
cause := "a Markdown declaration is read from an HTML comment or from an H2, H3 or H4 annotation heading, and this line is prose"
|
|
1378
|
+
if heading {
|
|
1379
|
+
cause = "a Markdown declaration is read from an HTML comment or from an H2, H3 or H4 heading of the document itself, and this heading is neither"
|
|
1380
|
+
}
|
|
1381
|
+
return "Unreadable " + tag + " at " + location + ":" + decimal(line) + ": " + cause + ", so nothing reads the tag." +
|
|
1382
|
+
" Write it as a '## " + tag + " <target>' heading with " + unreadableMarkdownField(tag) + " below it," +
|
|
1383
|
+
" or wrap it as '<!-- " + tag + " <target> " + unreadableMarkdownField(tag) + " -->'."
|
|
1153
1384
|
}
|
|
1154
1385
|
|
|
1155
1386
|
// unreadableMarkdownField names what follows a target for this tag.
|
package/native/prisma.go
CHANGED
|
@@ -910,11 +910,10 @@ func prismaCommentCarriesTag(body string) bool {
|
|
|
910
910
|
if region.fence {
|
|
911
911
|
continue
|
|
912
912
|
}
|
|
913
|
-
|
|
914
|
-
if _, _, found := declarationLine(trimmed); found {
|
|
913
|
+
if opensAcknowledgement(region.text, region.heading, region.title) {
|
|
915
914
|
return true
|
|
916
915
|
}
|
|
917
|
-
if prismaBuriedTag(
|
|
916
|
+
if prismaBuriedTag(region.text) {
|
|
918
917
|
return true
|
|
919
918
|
}
|
|
920
919
|
}
|
|
@@ -936,11 +935,10 @@ func prismaBuriedTagLines(body string) []int {
|
|
|
936
935
|
if region.fence {
|
|
937
936
|
continue
|
|
938
937
|
}
|
|
939
|
-
|
|
940
|
-
if _, _, found := declarationLine(trimmed); found {
|
|
938
|
+
if opensAcknowledgement(region.text, region.heading, region.title) {
|
|
941
939
|
continue
|
|
942
940
|
}
|
|
943
|
-
if prismaBuriedTag(
|
|
941
|
+
if prismaBuriedTag(region.text) {
|
|
944
942
|
offsets = append(offsets, offset)
|
|
945
943
|
}
|
|
946
944
|
}
|
|
@@ -965,8 +963,8 @@ func prismaBuriedTag(trimmed string) bool {
|
|
|
965
963
|
if stripped == trimmed {
|
|
966
964
|
return false
|
|
967
965
|
}
|
|
968
|
-
|
|
969
|
-
return
|
|
966
|
+
heading, title := documentationHeading(stripped)
|
|
967
|
+
return opensAcknowledgement(stripped, heading, title)
|
|
970
968
|
}
|
|
971
969
|
|
|
972
970
|
// prismaNormalizationFailure words a rejected schema identically whether the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttsc/evidence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@ttsc/lint": ">=0.31.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@wrtnlabs/evidence": "^0.
|
|
48
|
+
"@wrtnlabs/evidence": "^0.5.0",
|
|
49
49
|
"@types/node": "^25.3.0",
|
|
50
50
|
"rimraf": "^6.1.2",
|
|
51
51
|
"typescript": "^7.0.2",
|
|
52
|
-
"@ttsc/lint": "0.
|
|
53
|
-
"ttsc": "0.
|
|
52
|
+
"@ttsc/lint": "0.32.0",
|
|
53
|
+
"ttsc": "0.32.0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",
|