@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/prisma_cache.go
CHANGED
|
@@ -88,12 +88,19 @@ func (cache *prismaCache) store(digest string, outcome prismaSetOutcome) {
|
|
|
88
88
|
|
|
89
89
|
// copyPrismaOutcome deep-copies the models, because a caller builds units from
|
|
90
90
|
// them while another cycle may be reading the same entry.
|
|
91
|
+
//
|
|
92
|
+
// The copy is written field by field so that only `Fields` is reallocated, and
|
|
93
|
+
// that is exactly why it has to name every other field: one left out is served
|
|
94
|
+
// as its zero value on every cache hit and on no cache miss. `Digest` was, so a
|
|
95
|
+
// resident host asked for one model fingerprint on its first cycle and a
|
|
96
|
+
// different one on every cycle after, which no edit could repair.
|
|
91
97
|
func copyPrismaOutcome(outcome prismaSetOutcome) prismaSetOutcome {
|
|
92
98
|
models := make([]prismaModel, 0, len(outcome.Models))
|
|
93
99
|
for _, model := range outcome.Models {
|
|
94
100
|
models = append(models, prismaModel{
|
|
95
101
|
Name: model.Name,
|
|
96
102
|
Documentation: model.Documentation,
|
|
103
|
+
Digest: model.Digest,
|
|
97
104
|
Fields: append([]prismaField(nil), model.Fields...),
|
|
98
105
|
})
|
|
99
106
|
}
|
package/native/review.go
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
5
|
+
|
|
6
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// reviewRule requires a verification statement beside every citation.
|
|
10
|
+
//
|
|
11
|
+
// `evidence/graph` proves that a target resolves, that its host is eligible, and
|
|
12
|
+
// that every selected unit is acknowledged. All three are properties of the
|
|
13
|
+
// graph as it stands, so none of them records that anyone read the cited unit
|
|
14
|
+
// and compared it against the citing declaration. An author writes one prose
|
|
15
|
+
// reason answering why this declaration answers for that target, and is never
|
|
16
|
+
// asked what they actually checked. Those are different questions, and a
|
|
17
|
+
// fabricated citation clears the second one as easily as the first because the
|
|
18
|
+
// second one is never put.
|
|
19
|
+
//
|
|
20
|
+
// The rule puts it. Every `@evidence` and `@evidenceExclude` on a public
|
|
21
|
+
// identity must be answered by an `@evidenceReview` naming the same target, so a
|
|
22
|
+
// citation cannot be produced without a separately addressed statement written
|
|
23
|
+
// as its own act.
|
|
24
|
+
//
|
|
25
|
+
// What it does not do is judge whether that statement is sincere. The project's
|
|
26
|
+
// own rule forbids it: a rule guessing at prose teaches authors to write filler
|
|
27
|
+
// that passes. What expires a review when the cited content moves is the
|
|
28
|
+
// fingerprint `evidence/graph` validates under `requireReview`; this rule
|
|
29
|
+
// carries the token without interpreting it, so a project can adopt the
|
|
30
|
+
// discipline before it adopts the expiry.
|
|
31
|
+
type reviewRule struct{}
|
|
32
|
+
|
|
33
|
+
func (reviewRule) Name() string { return reviewRuleName }
|
|
34
|
+
|
|
35
|
+
func (reviewRule) Visits() []shimast.Kind {
|
|
36
|
+
return []shimast.Kind{shimast.KindSourceFile}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func (reviewRule) NeedsTypeChecker() bool { return false }
|
|
40
|
+
|
|
41
|
+
func (reviewRule) VisitsDeclarationFiles() bool { return false }
|
|
42
|
+
|
|
43
|
+
// AcceptsTtscLintOptions refuses an options slot.
|
|
44
|
+
//
|
|
45
|
+
// The marker is mandatory rather than decorative: `rule.OptionsRule` documents
|
|
46
|
+
// that a contributor rule defaults to *accepting* options, so an unimplemented
|
|
47
|
+
// marker would let this rule take a configuration object it never validates.
|
|
48
|
+
// There is nothing to select here — a citation on any public identity owes a
|
|
49
|
+
// review — and per-directory scoping belongs in the outer `files` setting.
|
|
50
|
+
func (reviewRule) AcceptsTtscLintOptions() bool { return false }
|
|
51
|
+
|
|
52
|
+
func (reviewRule) Check(ctx *rule.Context, node *shimast.Node) {
|
|
53
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if node.Kind != shimast.KindSourceFile {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
for _, host := range documentedHosts(ctx.File) {
|
|
60
|
+
judgeReviewedHost(ctx, host)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func init() { rule.Register(reviewRule{}) }
|
|
65
|
+
|
|
66
|
+
// judgeReviewedHost pairs one identity's citations against its reviews.
|
|
67
|
+
//
|
|
68
|
+
// The unit judged is an identity rather than a declaration, which is the
|
|
69
|
+
// boundary `evidence/documented` already uses and the one the graph judges
|
|
70
|
+
// citations on: a merged identity is one host, so a citation may sit on any of
|
|
71
|
+
// its declarations and so may the review that answers it. Judging declarations
|
|
72
|
+
// instead would demand a review on the half that happens to carry the tag,
|
|
73
|
+
// which is placement the graph itself calls not worth a diagnostic.
|
|
74
|
+
func judgeReviewedHost(ctx *rule.Context, host documentedHost) {
|
|
75
|
+
cited, reviewed := readHostTags(ctx.File, host)
|
|
76
|
+
for _, key := range cited.order {
|
|
77
|
+
if reviewed.byKey[key] != nil {
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
unreviewed := cited.byKey[key]
|
|
81
|
+
// The repair names the matching review tag, not a generic one. An exclusion
|
|
82
|
+
// reviewed with `@evidenceReview` stays unreviewed, so telling its author to
|
|
83
|
+
// write that tag would send them into the one mistake this split exists to
|
|
84
|
+
// prevent.
|
|
85
|
+
ctx.Report(
|
|
86
|
+
host.Node,
|
|
87
|
+
"Unreviewed @"+string(unreviewed.tag)+" for '"+displayTarget(unreviewed.target)+"' on "+host.describe()+
|
|
88
|
+
". "+acknowledgementQuestion(unreviewed.tag)+" Nothing states what was verified. Add '"+
|
|
89
|
+
reviewMarkerFor(unreviewed.tag)+" "+displayTarget(unreviewed.target)+" <what you checked>' to the same documentation block, or correct this host when what you checked did not hold. "+reviewExample(unreviewed.tag)+untrueReviewWarning,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
for _, key := range reviewed.order {
|
|
93
|
+
review := reviewed.byKey[key]
|
|
94
|
+
// A review with no target is malformed and nothing else. Reporting it as
|
|
95
|
+
// an orphan as well would name two repairs for one mistake, and the
|
|
96
|
+
// orphan repair — "correct the target to match a citation" — is the
|
|
97
|
+
// malformed one restated.
|
|
98
|
+
if review.Target == "" {
|
|
99
|
+
ctx.Report(
|
|
100
|
+
host.Node,
|
|
101
|
+
"Malformed "+review.marker()+" on "+host.describe()+
|
|
102
|
+
": target and non-empty description are mandatory. Write '"+review.marker()+" <target> <what you checked>'.",
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if review.Description == "" {
|
|
107
|
+
ctx.Report(
|
|
108
|
+
host.Node,
|
|
109
|
+
"Malformed "+review.marker()+" for '"+displayTarget(review.Target)+"' on "+host.describe()+
|
|
110
|
+
": the target is written and the description is empty, so nothing states what was verified. Write '"+
|
|
111
|
+
review.marker()+" "+displayTarget(review.Target)+" <what you checked>'.",
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if _, found := cited.byKey[key]; found {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
// A review whose target is acknowledged by the *other* tag is the mistake
|
|
119
|
+
// worth naming precisely, because the author did the work and filed it under
|
|
120
|
+
// the wrong question. Saying only "this identity cites no such target" would
|
|
121
|
+
// send them looking for a typo that is not there.
|
|
122
|
+
if other, found := cited.byKey[reviewKey(otherTag(review.Reviews), review.Target)]; found {
|
|
123
|
+
ctx.Report(
|
|
124
|
+
host.Node,
|
|
125
|
+
"Mismatched "+review.marker()+" for '"+displayTarget(review.Target)+"' on "+host.describe()+
|
|
126
|
+
": that target is acknowledged by @"+string(other.tag)+", which "+reviewMarkerFor(other.tag)+
|
|
127
|
+
" answers for, and the two ask different questions. Rewrite this review as '"+reviewMarkerFor(other.tag)+
|
|
128
|
+
" "+displayTarget(review.Target)+" <what you checked>', or add the @"+string(review.Reviews)+
|
|
129
|
+
" it answers for, or remove it. Rewriting is wrong if that review is already written.",
|
|
130
|
+
)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
ctx.Report(
|
|
134
|
+
host.Node,
|
|
135
|
+
"Orphan "+review.marker()+" for '"+displayTarget(review.Target)+"' on "+host.describe()+
|
|
136
|
+
": this identity carries no @"+string(review.Reviews)+" for that target, so the review answers nothing. Correct the target, add the acknowledgement it reviews when this host does answer for that target, or remove the review."+untrueReviewWarning,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
for _, duplicate := range reviewed.duplicated {
|
|
140
|
+
ctx.Report(
|
|
141
|
+
host.Node,
|
|
142
|
+
"Duplicate "+reviewMarkerFor(duplicate.tag)+" for '"+displayTarget(duplicate.target)+"' on "+host.describe()+
|
|
143
|
+
": one acknowledgement is verified once. Keep the review that states what was checked and remove the other.",
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// acknowledgementQuestion is what a review of this tag has to answer.
|
|
149
|
+
//
|
|
150
|
+
// The two are not the same question and the diagnostic says which one is open. A
|
|
151
|
+
// citation is verified by checking that this declaration does what the cited unit
|
|
152
|
+
// describes. An exclusion is verified by checking that the unit genuinely does not
|
|
153
|
+
// apply here, which no reading of this declaration can establish on its own.
|
|
154
|
+
// reviewExample shows the shape of a check that answers this tag's question.
|
|
155
|
+
//
|
|
156
|
+
// The two examples differ because the two checks do. A citation is answered by
|
|
157
|
+
// reading the target and exercising the host against it. An exclusion is
|
|
158
|
+
// answered by finding what does own the unit, which is work outside the
|
|
159
|
+
// declaration the tag sits on.
|
|
160
|
+
func reviewExample(tag tagKind) string {
|
|
161
|
+
if tag == tagExclude {
|
|
162
|
+
return "A review of an exclusion names where the unit is handled instead, as in 'confirmed ShoppingSaleProvider owns it and no DTO publishes it'."
|
|
163
|
+
}
|
|
164
|
+
return "A review of a citation names what you read or ran, as in 'read the section's three rules and ran the checkout test'."
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func acknowledgementQuestion(tag tagKind) string {
|
|
168
|
+
if tag == tagExclude {
|
|
169
|
+
return "The exclusion states that this claim does not cover that target."
|
|
170
|
+
}
|
|
171
|
+
return "The citation states why this declaration answers for that target."
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// reviewMarkerFor names the review tag that answers for one acknowledgement.
|
|
175
|
+
func reviewMarkerFor(tag tagKind) string {
|
|
176
|
+
if tag == tagExclude {
|
|
177
|
+
return "@evidenceExcludeReview"
|
|
178
|
+
}
|
|
179
|
+
return "@evidenceReview"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// otherTag is the acknowledgement a review was not written for.
|
|
183
|
+
func otherTag(tag tagKind) tagKind {
|
|
184
|
+
if tag == tagExclude {
|
|
185
|
+
return tagEvidence
|
|
186
|
+
}
|
|
187
|
+
return tagExclude
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// reviewKey pairs an acknowledgement kind with a target.
|
|
191
|
+
//
|
|
192
|
+
// Both halves are the identity. One target may be cited by one claim and excluded
|
|
193
|
+
// by another from the same host, and those are two decisions owing two different
|
|
194
|
+
// verifications; keying on the target alone would let either review answer both.
|
|
195
|
+
// NUL separates them because a target may contain any printable character.
|
|
196
|
+
func reviewKey(tag tagKind, target string) string {
|
|
197
|
+
return string(tag) + "\x00" + target
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// acknowledgement is one tag-and-target pair a host wrote.
|
|
201
|
+
type acknowledgement struct {
|
|
202
|
+
tag tagKind
|
|
203
|
+
target string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// citedTargets is the acknowledgement set of one identity, in written order.
|
|
207
|
+
//
|
|
208
|
+
// Order is the author's rather than a map's, because two findings on one host
|
|
209
|
+
// are read as a list and a list that reorders between runs cannot be diffed.
|
|
210
|
+
//
|
|
211
|
+
// The key pairs the tag with the target rather than being the target alone. One
|
|
212
|
+
// target may be cited by one claim and excluded by another from the same host, and
|
|
213
|
+
// those are two decisions owing two different verifications.
|
|
214
|
+
type citedTargets struct {
|
|
215
|
+
order []string
|
|
216
|
+
byKey map[string]acknowledgement
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// reviewedTargets is the review set of one identity, keyed the same way.
|
|
220
|
+
//
|
|
221
|
+
// Duplicates are collected rather than overwritten. One acknowledgement is
|
|
222
|
+
// verified once, and a second review of it is a finding: either the author
|
|
223
|
+
// reviewed the same thing twice, or two different verifications are competing and
|
|
224
|
+
// only one survives into whatever a later reader trusts.
|
|
225
|
+
type reviewedTargets struct {
|
|
226
|
+
order []string
|
|
227
|
+
byKey map[string]*parsedReview
|
|
228
|
+
duplicated []acknowledgement
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// readHostTags collects the citations and reviews of one identity.
|
|
232
|
+
//
|
|
233
|
+
// Every declaration of the identity is read, not only the first. A merged
|
|
234
|
+
// identity's citation may sit on any of them, so a review restricted to the
|
|
235
|
+
// founding declaration would report a missing review for a citation that is
|
|
236
|
+
// answered two lines away.
|
|
237
|
+
//
|
|
238
|
+
// Blocks are deduplicated by position because TypeScript cascades one leading
|
|
239
|
+
// block onto nested nodes: a variable statement and each of its declarations all
|
|
240
|
+
// report the same comment, and reading it three times would turn one review into
|
|
241
|
+
// two duplicates of itself.
|
|
242
|
+
func readHostTags(
|
|
243
|
+
file *shimast.SourceFile,
|
|
244
|
+
host documentedHost,
|
|
245
|
+
) (citedTargets, reviewedTargets) {
|
|
246
|
+
cited := citedTargets{byKey: map[string]acknowledgement{}}
|
|
247
|
+
reviewed := reviewedTargets{byKey: map[string]*parsedReview{}}
|
|
248
|
+
content := file.Text()
|
|
249
|
+
seen := map[int]bool{}
|
|
250
|
+
// Already in source order. `orderIdentityDeclarations` sorts every identity's
|
|
251
|
+
// declarations by position and dereferences `Pos()` to do it, so a nil node
|
|
252
|
+
// would have panicked there long before this rule saw the host. Re-sorting here
|
|
253
|
+
// asserted the opposite: its comparator called a nil pair both less than and
|
|
254
|
+
// not less than each other, which is not an ordering, and the guard it existed
|
|
255
|
+
// for could never fire.
|
|
256
|
+
nodes := host.Nodes
|
|
257
|
+
if len(nodes) == 0 {
|
|
258
|
+
nodes = []*shimast.Node{host.Node}
|
|
259
|
+
}
|
|
260
|
+
for _, node := range nodes {
|
|
261
|
+
if node == nil {
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
for _, doc := range node.JSDoc(file) {
|
|
265
|
+
if doc == nil || doc.Pos() < 0 || doc.End() > len(content) {
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
if seen[doc.Pos()] {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
seen[doc.Pos()] = true
|
|
272
|
+
comment := content[doc.Pos():doc.End()]
|
|
273
|
+
for _, declaration := range parseDeclarations(comment) {
|
|
274
|
+
// A malformed citation is the graph's finding, and it names a repair
|
|
275
|
+
// this rule cannot: without a target there is nothing for a review to
|
|
276
|
+
// be addressed to. Demanding one here would report a second finding
|
|
277
|
+
// whose repair the first one has to be performed before.
|
|
278
|
+
if declaration.Target == "" {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
key := reviewKey(declaration.Tag, declaration.Target)
|
|
282
|
+
if _, found := cited.byKey[key]; !found {
|
|
283
|
+
cited.order = append(cited.order, key)
|
|
284
|
+
cited.byKey[key] = acknowledgement{
|
|
285
|
+
tag: declaration.Tag,
|
|
286
|
+
target: declaration.Target,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Reset per block, because that is the scope a duplicate lives in.
|
|
291
|
+
seenReviewBlocks := map[string]bool{}
|
|
292
|
+
for _, review := range parseReviews(comment) {
|
|
293
|
+
if review.Target == "" {
|
|
294
|
+
// A review with no target at all is reported against the identity
|
|
295
|
+
// rather than against a target nobody wrote.
|
|
296
|
+
recordTargetlessReview(&reviewed, review.Reviews)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
key := reviewKey(review.Reviews, review.Target)
|
|
300
|
+
// A duplicate is two reviews in one block, not one review on each
|
|
301
|
+
// declaration of an identity. An overload set and an `interface` beside
|
|
302
|
+
// its `namespace` are normally written by copying the block, so each
|
|
303
|
+
// half carries the same citation and the same review; counting across
|
|
304
|
+
// blocks reported that idiom as a duplicate while every citation was in
|
|
305
|
+
// fact reviewed exactly once. Citations are already deduplicated across
|
|
306
|
+
// blocks the same way, and the asymmetry was the whole defect.
|
|
307
|
+
if seenReviewBlocks[key] {
|
|
308
|
+
reviewed.duplicated = appendUniqueAcknowledgement(
|
|
309
|
+
reviewed.duplicated,
|
|
310
|
+
acknowledgement{tag: review.Reviews, target: review.Target},
|
|
311
|
+
)
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
seenReviewBlocks[key] = true
|
|
315
|
+
if _, found := reviewed.byKey[key]; found {
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
stored := review
|
|
319
|
+
reviewed.order = append(reviewed.order, key)
|
|
320
|
+
reviewed.byKey[key] = &stored
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return cited, reviewed
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// recordTargetlessReview records a review whose body held no target.
|
|
328
|
+
//
|
|
329
|
+
// It is filed under the empty target so the judging pass reports it through the
|
|
330
|
+
// same walk as every other review, instead of this collector growing a second
|
|
331
|
+
// reporting path that has to be kept in step with the first. Filing it once per
|
|
332
|
+
// kind is deliberate: two empty tags of one kind are one mistake with one repair,
|
|
333
|
+
// while an empty citation review and an empty exclusion review are two mistakes
|
|
334
|
+
// whose repairs name different tags.
|
|
335
|
+
func recordTargetlessReview(reviewed *reviewedTargets, reviews tagKind) {
|
|
336
|
+
key := reviewKey(reviews, "")
|
|
337
|
+
if _, found := reviewed.byKey[key]; found {
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
reviewed.order = append(reviewed.order, key)
|
|
341
|
+
reviewed.byKey[key] = &parsedReview{Reviews: reviews}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// appendUniqueAcknowledgement keeps one entry per tag-and-target pair.
|
|
345
|
+
func appendUniqueAcknowledgement(
|
|
346
|
+
values []acknowledgement,
|
|
347
|
+
candidate acknowledgement,
|
|
348
|
+
) []acknowledgement {
|
|
349
|
+
for _, value := range values {
|
|
350
|
+
if value == candidate {
|
|
351
|
+
return values
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return append(values, candidate)
|
|
355
|
+
}
|
package/native/swagger.go
CHANGED
|
@@ -82,6 +82,13 @@ type swaggerDocumentProblem struct {
|
|
|
82
82
|
type swaggerOperation struct {
|
|
83
83
|
Method string `json:"method"`
|
|
84
84
|
Path string `json:"path"`
|
|
85
|
+
// Digest is the operation's content, hashed by the bridge that normalized it.
|
|
86
|
+
//
|
|
87
|
+
// This side never sees the document, so the value travels with the identity
|
|
88
|
+
// rather than being recomputed here. The document-wide digest beside it is a
|
|
89
|
+
// cache key and answers a different question: one value shared by every
|
|
90
|
+
// operation expires every review in a document whenever any part of it moves.
|
|
91
|
+
Digest string `json:"digest"`
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
func loadSwaggerInventories(
|
|
@@ -280,6 +287,7 @@ func swaggerOperationUnit(
|
|
|
280
287
|
Symbol: "operation",
|
|
281
288
|
Path: displaySwaggerSource(source),
|
|
282
289
|
Readable: readable,
|
|
290
|
+
Digest: operation.Digest,
|
|
283
291
|
}, ""
|
|
284
292
|
}
|
|
285
293
|
|