@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.
@@ -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
  }
@@ -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