@ttsc/evidence 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -169
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +15 -5
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +70 -6
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +5 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +16 -5
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +8 -0
- package/native/base.go +517 -22
- package/native/config.go +114 -5
- package/native/graph.go +332 -70
- package/native/graph_nodes.go +121 -0
- package/native/loader.go +30 -7
- package/native/markdown.go +91 -28
- package/native/model.go +49 -16
- package/native/prisma.go +167 -48
- package/native/prisma_cache.go +40 -16
- package/native/swagger.go +1 -1
- package/native/swagger_cache.go +1 -1
- package/native/typescript.go +120 -9
- package/package.json +5 -5
package/native/graph.go
CHANGED
|
@@ -68,6 +68,13 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
|
|
|
68
68
|
root,
|
|
69
69
|
claimPopulationConfig(config, artifactPrisma),
|
|
70
70
|
)
|
|
71
|
+
problems = append(
|
|
72
|
+
problems,
|
|
73
|
+
typeScriptBaseProblems(
|
|
74
|
+
claimPopulationConfig(config, artifactTypeScript),
|
|
75
|
+
typescript,
|
|
76
|
+
)...,
|
|
77
|
+
)
|
|
71
78
|
problems = append(problems, markdownClaimProblems...)
|
|
72
79
|
problems = append(problems, prismaClaimProblems...)
|
|
73
80
|
// Governance is judged against the configuration as declared, before
|
|
@@ -163,8 +170,14 @@ func claimPopulationConfig(
|
|
|
163
170
|
//
|
|
164
171
|
// TypeScript, Prisma, and Markdown are the three claim-capable artifact kinds.
|
|
165
172
|
// A healthy zero-file match is empty and therefore inactive, including when a
|
|
166
|
-
// typo caused it.
|
|
167
|
-
//
|
|
173
|
+
// typo caused it. An unhealthy population stays active because failed input
|
|
174
|
+
// cannot prove the selected population is empty.
|
|
175
|
+
//
|
|
176
|
+
// Health is the whole test, and a declared root that is not a directory is one
|
|
177
|
+
// of the things it now answers for every kind rather than for two of them.
|
|
178
|
+
// Asking baseDirectoryProblem again here kept a TypeScript claim alive
|
|
179
|
+
// on a fact this function then had no way to report, which is how a bad root
|
|
180
|
+
// came to be answered with a glob diagnostic.
|
|
168
181
|
func activeGraphConfig(
|
|
169
182
|
config graphConfig,
|
|
170
183
|
markdown map[string]*artifactInventory,
|
|
@@ -193,14 +206,19 @@ func claimIsInactive(
|
|
|
193
206
|
claim claimSpec,
|
|
194
207
|
inventories map[string]*artifactInventory,
|
|
195
208
|
) bool {
|
|
209
|
+
// The default arm is unreachable: decodeClaim refuses a reference-only kind
|
|
210
|
+
// and any kind problem returns from Check before activation runs. It stays as
|
|
211
|
+
// the place a fourth claim-capable kind announces itself, and such a kind
|
|
212
|
+
// owes this switch a case and a population loader that records its failures —
|
|
213
|
+
// without both it would go active and empty with nothing to say, which is the
|
|
214
|
+
// shape this function's own history is about.
|
|
196
215
|
switch claim.Type {
|
|
197
216
|
case artifactMarkdown, artifactPrisma, artifactTypeScript:
|
|
198
217
|
default:
|
|
199
218
|
return false
|
|
200
219
|
}
|
|
201
220
|
paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
|
|
202
|
-
if !populationIsHealthy(inventories, claim.Base, paths)
|
|
203
|
-
unreadableBaseProblem(claim.Base, claim.Type) != "" {
|
|
221
|
+
if !populationIsHealthy(inventories, claim.Base, paths) {
|
|
204
222
|
return false
|
|
205
223
|
}
|
|
206
224
|
for _, path := range paths {
|
|
@@ -256,13 +274,17 @@ func materializeClaimStates(
|
|
|
256
274
|
)
|
|
257
275
|
}
|
|
258
276
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
277
|
+
// No claim-side empty-match diagnostic belongs here. A claim arrives
|
|
278
|
+
// already active, which means it either selected a host — so it matched a
|
|
279
|
+
// path — or its population is unhealthy, and an unhealthy one is reported
|
|
280
|
+
// at its own cause by the loader that failed. The message removed from this
|
|
281
|
+
// spot told the author to fix globs that were fine, and the only state that
|
|
282
|
+
// ever reached it was a TypeScript root that did not resolve, which now
|
|
283
|
+
// says so itself.
|
|
265
284
|
hostsByID := map[string]bool{}
|
|
285
|
+
declaredByID := map[string]bool{}
|
|
286
|
+
reviewed := map[*evidenceReview]bool{}
|
|
287
|
+
insideCarrier := map[string]bool{}
|
|
266
288
|
for _, path := range paths {
|
|
267
289
|
for _, unit := range inventories[path].Units {
|
|
268
290
|
if unit.Hidden != "" ||
|
|
@@ -273,16 +295,51 @@ func materializeClaimStates(
|
|
|
273
295
|
hostsByID[unit.ID] = true
|
|
274
296
|
state.Hosts = append(state.Hosts, unit)
|
|
275
297
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
298
|
+
// One physical file can occupy two of this claim's paths. A hard link is
|
|
299
|
+
// a second directory entry for one file and a single walk enumerates
|
|
300
|
+
// both, and the Prisma loader then gives both inventories the same
|
|
301
|
+
// declaration object rather than a copy each, because one citation is one
|
|
302
|
+
// obligation wherever it is read from. Appending each list would count
|
|
303
|
+
// that citation twice, and every duplicate and conflict rule downstream
|
|
304
|
+
// would then name a repair the author cannot perform: the two are one
|
|
305
|
+
// tag, on one line. The hosts just above are collapsed for the same
|
|
306
|
+
// reason.
|
|
307
|
+
for _, declaration := range inventories[path].Declarations {
|
|
308
|
+
if declaredByID[declaration.ID] {
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
declaredByID[declaration.ID] = true
|
|
312
|
+
state.Declarations = append(state.Declarations, declaration)
|
|
313
|
+
}
|
|
280
314
|
// Reviews travel beside declarations and never among them. They carry no
|
|
281
315
|
// obligation of their own; they are looked up by host and target when a
|
|
282
|
-
// reference demands one.
|
|
283
|
-
|
|
284
|
-
|
|
316
|
+
// reference demands one. They are collapsed by object rather than by ID
|
|
317
|
+
// because a review has no identity of its own, and the duplication being
|
|
318
|
+
// removed is exactly one object reached twice. No message depends on it:
|
|
319
|
+
// `newReviewLedger` keeps the first review for a key and a repeat of the
|
|
320
|
+
// same object changes nothing it answers. What this keeps is the list
|
|
321
|
+
// being the union its name says it is, so a later reader that counts
|
|
322
|
+
// reviews rather than looking one up is not counting names of files.
|
|
323
|
+
for _, review := range inventories[path].Reviews {
|
|
324
|
+
if reviewed[review] {
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
reviewed[review] = true
|
|
328
|
+
state.Reviews = append(state.Reviews, review)
|
|
329
|
+
}
|
|
330
|
+
if carrierPaths[path] {
|
|
285
331
|
for _, declaration := range inventories[path].Declarations {
|
|
332
|
+
insideCarrier[declaration.ID] = true
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// A declaration is outside the carriers only when no path carrying it is a
|
|
337
|
+
// carrier. Deciding that per path would put one tag in two places at once
|
|
338
|
+
// for a file the claim selects under two names, and report the placement of
|
|
339
|
+
// whichever name it read second.
|
|
340
|
+
if len(claim.ExclusionCarriers.Patterns) != 0 {
|
|
341
|
+
for _, declaration := range state.Declarations {
|
|
342
|
+
if !insideCarrier[declaration.ID] {
|
|
286
343
|
state.OutsideCarrier[declaration.ID] = true
|
|
287
344
|
}
|
|
288
345
|
}
|
|
@@ -552,7 +609,7 @@ func evaluateEvidenceGraph(
|
|
|
552
609
|
if !declaration.valid() {
|
|
553
610
|
problems = append(
|
|
554
611
|
problems,
|
|
555
|
-
"Malformed @"+string(declaration.Tag)+" declaration at "+declaration.location()+" for "+context+": target and non-empty reason are mandatory. Write '@"+string(declaration.Tag)+" <target> <reason>'
|
|
612
|
+
"Malformed @"+string(declaration.Tag)+" declaration at "+declaration.location()+" for "+context+": target and non-empty reason are mandatory. Write '@"+string(declaration.Tag)+" <target> <reason>'."+untrueTagWarning,
|
|
556
613
|
)
|
|
557
614
|
continue
|
|
558
615
|
}
|
|
@@ -646,6 +703,21 @@ func evaluateEvidenceGraph(
|
|
|
646
703
|
// carrier globs its message must name.
|
|
647
704
|
outsideCarrier := map[string][]string{}
|
|
648
705
|
outsideCarrierGlobs := map[string]string{}
|
|
706
|
+
// A checklist tag speaking for no selected host is a non-participation
|
|
707
|
+
// finding: within its reference it answers nothing, and whether that earns a
|
|
708
|
+
// diagnostic depends on every other obligation the same declaration can
|
|
709
|
+
// discharge. The eligibility comment below promises exactly that for every
|
|
710
|
+
// overlap — an ineligible overlap must not reject a tag already owned
|
|
711
|
+
// elsewhere — so the finding is recorded here and judged after the walk,
|
|
712
|
+
// when `answers` can say whether anything consumed the tag.
|
|
713
|
+
unhosted := map[string][]string{}
|
|
714
|
+
unhostedSelections := map[string]symbolSet{}
|
|
715
|
+
// answers marks a declaration that wrote at least one acknowledgement
|
|
716
|
+
// ledger, or was refused as an aggregate, which is that citation's own
|
|
717
|
+
// diagnostic. A tag the noEvidenceExclude policy refused answers nothing
|
|
718
|
+
// and is deliberately absent, so it can still be reported as unhosted
|
|
719
|
+
// where another reference is a checklist.
|
|
720
|
+
answers := map[string]bool{}
|
|
649
721
|
for _, state := range states {
|
|
650
722
|
if len(state.Paths) == 0 {
|
|
651
723
|
continue
|
|
@@ -667,12 +739,26 @@ func evaluateEvidenceGraph(
|
|
|
667
739
|
}
|
|
668
740
|
single := reference.Spec.Policy.SingleEvidencePerSymbol
|
|
669
741
|
unique := reference.Spec.Policy.UniqueEvidence
|
|
670
|
-
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
742
|
+
checklist := reference.Spec.Policy.Checklist
|
|
743
|
+
// An empty population ends the reference, for every policy alike.
|
|
744
|
+
//
|
|
745
|
+
// singleEvidencePerSymbol used to be excepted, on the argument that each
|
|
746
|
+
// selected host then truthfully cites zero units rather than the one it
|
|
747
|
+
// owes. The count is true and the conclusion does not follow: the
|
|
748
|
+
// materializer has already reported why the population is empty, and this
|
|
749
|
+
// added one message per host asking each of them to cite a unit that does
|
|
750
|
+
// not exist — a repair the population makes impossible, scaled by host
|
|
751
|
+
// count. It is the same derived finding the loader-failure path refuses
|
|
752
|
+
// to produce, and it was refused there for the same reason.
|
|
753
|
+
//
|
|
754
|
+
// The exception was also conditional on len(Paths) != 0, so the identical
|
|
755
|
+
// empty population was judged or skipped depending on whether any file
|
|
756
|
+
// happened to match, which no argument ever covered.
|
|
757
|
+
//
|
|
758
|
+
// Nothing else is lost by leaving early. Every diagnostic below reaches a
|
|
759
|
+
// declaration through reference.UnitsByScope, which an empty population
|
|
760
|
+
// leaves empty, so each one already skips every declaration it visits.
|
|
761
|
+
if len(reference.Units) == 0 {
|
|
676
762
|
continue
|
|
677
763
|
}
|
|
678
764
|
acknowledged := map[string]bool{}
|
|
@@ -681,7 +767,7 @@ func evaluateEvidenceGraph(
|
|
|
681
767
|
evidenceByHostAndScope := map[string]map[string]*evidenceDeclaration{}
|
|
682
768
|
selectedHosts := map[string]*evidenceUnit{}
|
|
683
769
|
evidenceUnitsByHost := map[string]map[string]bool{}
|
|
684
|
-
if single || unique {
|
|
770
|
+
if single || unique || checklist {
|
|
685
771
|
for _, host := range state.Hosts {
|
|
686
772
|
selectedHosts[host.ID] = host
|
|
687
773
|
if single {
|
|
@@ -689,6 +775,28 @@ func evaluateEvidenceGraph(
|
|
|
689
775
|
}
|
|
690
776
|
}
|
|
691
777
|
}
|
|
778
|
+
// A checklist gives the obligation a host dimension, so every map below
|
|
779
|
+
// that answers "was this unit acknowledged" becomes "was it acknowledged
|
|
780
|
+
// *here*". The ordinary maps keep their global keys: outside a checklist
|
|
781
|
+
// one acknowledgement anywhere in the claim is the whole obligation, and
|
|
782
|
+
// that is the behavior every reference written before this option had.
|
|
783
|
+
acknowledgedByHost := map[string]map[string]bool{}
|
|
784
|
+
// aggregateByHost records the units a refused aggregate citation reached
|
|
785
|
+
// on a host. They are not acknowledged, and they are not reported missing
|
|
786
|
+
// either: the aggregate diagnostic already names them and names the one
|
|
787
|
+
// repair that answers both, so listing them again on the host would be
|
|
788
|
+
// the descendant duplication the diagnostics rule forbids.
|
|
789
|
+
aggregateByHost := map[string]map[string]bool{}
|
|
790
|
+
selectedUnitIDs := map[string]bool{}
|
|
791
|
+
if checklist {
|
|
792
|
+
for _, unit := range reference.Units {
|
|
793
|
+
selectedUnitIDs[unit.ID] = true
|
|
794
|
+
}
|
|
795
|
+
for _, host := range state.Hosts {
|
|
796
|
+
acknowledgedByHost[host.ID] = map[string]bool{}
|
|
797
|
+
aggregateByHost[host.ID] = map[string]bool{}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
692
800
|
evidenceHostsByUnit := map[string]map[string]bool{}
|
|
693
801
|
if unique {
|
|
694
802
|
for _, unit := range reference.Units {
|
|
@@ -747,6 +855,72 @@ func evaluateEvidenceGraph(
|
|
|
747
855
|
)
|
|
748
856
|
continue
|
|
749
857
|
}
|
|
858
|
+
// Which acknowledgement ledgers this declaration writes into. The
|
|
859
|
+
// single empty key outside a checklist is the historical global
|
|
860
|
+
// bookkeeping; a checklist writes one entry per selected host the
|
|
861
|
+
// declaration speaks for.
|
|
862
|
+
keyHosts := []string{""}
|
|
863
|
+
if checklist {
|
|
864
|
+
keyHosts = nil
|
|
865
|
+
for _, hostID := range declaration.SemanticHostIDs {
|
|
866
|
+
if selectedHosts[hostID] != nil {
|
|
867
|
+
keyHosts = append(keyHosts, hostID)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if len(keyHosts) == 0 {
|
|
871
|
+
// Under a checklist every acknowledgement is one host's answer, so
|
|
872
|
+
// a tag that speaks for no selected host answers nothing here.
|
|
873
|
+
//
|
|
874
|
+
// Spreading it across the claim instead was tried and withdrawn.
|
|
875
|
+
// The reach it bought is the gathered exclusion ledger that
|
|
876
|
+
// `evidenceExcludeCarriers` exists for, and paying for it here
|
|
877
|
+
// meant deciding "no selected host" is the same statement as "no
|
|
878
|
+
// host owes this", which two ordinary Markdown shapes satisfy by
|
|
879
|
+
// accident: a heading whose title yields no anchor, and a path
|
|
880
|
+
// containing whitespace. Both report a selectable kind while naming
|
|
881
|
+
// no unit, so one tag discharged every item for every host in the
|
|
882
|
+
// claim and reported nothing. The obligation this option states is
|
|
883
|
+
// per host, so the answer belongs on a host.
|
|
884
|
+
//
|
|
885
|
+
// Recorded rather than reported, because "answers nothing here" is
|
|
886
|
+
// not yet "answers nothing". Carrier eligibility is wider than the
|
|
887
|
+
// host gate, so the same tag may be an ordinary sibling
|
|
888
|
+
// reference's gathered exclusion or an overlapping claim's own
|
|
889
|
+
// answer, and a hard diagnostic here left that valid configuration
|
|
890
|
+
// no placement to exist in. The end-of-run block reports the tag
|
|
891
|
+
// once nothing has consumed it.
|
|
892
|
+
unhosted[declaration.ID] = appendUniqueString(
|
|
893
|
+
unhosted[declaration.ID],
|
|
894
|
+
claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
|
|
895
|
+
)
|
|
896
|
+
if unhostedSelections[declaration.ID] == nil {
|
|
897
|
+
unhostedSelections[declaration.ID] = symbolSet{}
|
|
898
|
+
}
|
|
899
|
+
for symbol := range state.Spec.Symbols {
|
|
900
|
+
unhostedSelections[declaration.ID][symbol] = true
|
|
901
|
+
}
|
|
902
|
+
continue
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
answers[declaration.ID] = true
|
|
906
|
+
if checklist &&
|
|
907
|
+
declaration.Tag == tagEvidence &&
|
|
908
|
+
!selectedUnitIDs[scopeID] {
|
|
909
|
+
targets := make([]string, 0, len(covered))
|
|
910
|
+
for _, unit := range covered {
|
|
911
|
+
targets = append(targets, "'"+unit.Target+"'")
|
|
912
|
+
}
|
|
913
|
+
problems = append(
|
|
914
|
+
problems,
|
|
915
|
+
"Aggregate @evidence target '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": this reference is a checklist, so a citation answers for the item it names, and this target names a scope containing "+decimal(len(covered))+" item(s) ("+strings.Join(targets, ", ")+") rather than one of them. Cite each item this host answers for, or write @evidenceExclude on this scope when none of it applies here."+untrueTagWarning,
|
|
916
|
+
)
|
|
917
|
+
for _, unit := range covered {
|
|
918
|
+
for _, hostID := range keyHosts {
|
|
919
|
+
aggregateByHost[hostID][unit.ID] = true
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
continue
|
|
923
|
+
}
|
|
750
924
|
if reference.Spec.Policy.RequireReview {
|
|
751
925
|
if reviewScopes == nil {
|
|
752
926
|
// Withdrawn units are absent from Population by construction, and
|
|
@@ -786,6 +960,13 @@ func evaluateEvidenceGraph(
|
|
|
786
960
|
byScope[scopeID] = declaration
|
|
787
961
|
}
|
|
788
962
|
}
|
|
963
|
+
// These two count the whole selected subtree, which is the aggregate
|
|
964
|
+
// behavior a checklist takes away from a positive tag. They stay on
|
|
965
|
+
// `covered` rather than on `answered` because `rejectChecklistConflicts`
|
|
966
|
+
// refuses both options beside `checklist` at decode, and a reference
|
|
967
|
+
// carrying a decode problem never reaches evaluation. Relaxing that
|
|
968
|
+
// refusal would restore the cascade here silently, so move this to
|
|
969
|
+
// `answered` in the same change.
|
|
789
970
|
if declaration.Tag == tagEvidence && (single || unique) {
|
|
790
971
|
for _, hostID := range declaration.SemanticHostIDs {
|
|
791
972
|
if selectedHosts[hostID] == nil {
|
|
@@ -815,30 +996,62 @@ func evaluateEvidenceGraph(
|
|
|
815
996
|
var conflictingDeclaration *evidenceDeclaration
|
|
816
997
|
var duplicateExclusionUnit *evidenceUnit
|
|
817
998
|
var firstExclusion *evidenceDeclaration
|
|
818
|
-
for
|
|
999
|
+
// A checklist citation answers for the item it names and for nothing
|
|
1000
|
+
// beneath it, so it acknowledges the scope alone rather than the scope's
|
|
1001
|
+
// selected subtree.
|
|
1002
|
+
//
|
|
1003
|
+
// Refusing only the unselected ancestor is not enough, and measuring it
|
|
1004
|
+
// is the only way that shows: with the default Markdown selector the
|
|
1005
|
+
// file is itself an item, so one `@evidence docs/rules.md` resolved to a
|
|
1006
|
+
// selected unit, cascaded through the whole document, and discharged
|
|
1007
|
+
// every heading on that host. The option was a no-op in the first
|
|
1008
|
+
// configuration an adopter writes. The same hole opens for any selector
|
|
1009
|
+
// admitting an ancestor and a descendant together, such as `h1` beside
|
|
1010
|
+
// `h2`.
|
|
1011
|
+
//
|
|
1012
|
+
// `@evidenceExclude` keeps the subtree, because "none of this applies
|
|
1013
|
+
// here" is one decision however many items it covers, and so does every
|
|
1014
|
+
// reference that is not a checklist.
|
|
1015
|
+
answered := covered
|
|
1016
|
+
if checklist && declaration.Tag == tagEvidence {
|
|
1017
|
+
answered = []*evidenceUnit{scopesByID[scopeID]}
|
|
1018
|
+
}
|
|
1019
|
+
for _, unit := range answered {
|
|
819
1020
|
acknowledged[unit.ID] = true
|
|
820
|
-
|
|
821
|
-
|
|
1021
|
+
// Outside a checklist keyHosts is one empty string, so the key is the
|
|
1022
|
+
// unit and this reads exactly as it did before the host dimension
|
|
1023
|
+
// existed. Under a checklist two hosts excluding one item, and one
|
|
1024
|
+
// host citing an item another excludes, are the expected state rather
|
|
1025
|
+
// than a duplicate and a conflict — which is only true because the
|
|
1026
|
+
// key carries the host.
|
|
1027
|
+
for _, hostID := range keyHosts {
|
|
1028
|
+
key := hostID + "\x00" + unit.ID
|
|
1029
|
+
if acknowledgedByHost[hostID] != nil {
|
|
1030
|
+
acknowledgedByHost[hostID][unit.ID] = true
|
|
1031
|
+
}
|
|
1032
|
+
if declaration.Tag == tagEvidence {
|
|
1033
|
+
if first := exclusionByUnit[key]; first != nil && conflictingUnit == nil {
|
|
1034
|
+
conflictingUnit = unit
|
|
1035
|
+
conflictingDeclaration = first
|
|
1036
|
+
}
|
|
1037
|
+
if evidenceByUnit[key] == nil {
|
|
1038
|
+
evidenceByUnit[key] = declaration
|
|
1039
|
+
}
|
|
1040
|
+
continue
|
|
1041
|
+
}
|
|
1042
|
+
if first := evidenceByUnit[key]; first != nil && conflictingUnit == nil {
|
|
822
1043
|
conflictingUnit = unit
|
|
823
1044
|
conflictingDeclaration = first
|
|
824
1045
|
}
|
|
825
|
-
if
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
conflictingUnit = unit
|
|
832
|
-
conflictingDeclaration = first
|
|
833
|
-
}
|
|
834
|
-
if first := exclusionByUnit[unit.ID]; first != nil {
|
|
835
|
-
if duplicateExclusionUnit == nil {
|
|
836
|
-
duplicateExclusionUnit = unit
|
|
837
|
-
firstExclusion = first
|
|
1046
|
+
if first := exclusionByUnit[key]; first != nil {
|
|
1047
|
+
if duplicateExclusionUnit == nil {
|
|
1048
|
+
duplicateExclusionUnit = unit
|
|
1049
|
+
firstExclusion = first
|
|
1050
|
+
}
|
|
1051
|
+
continue
|
|
838
1052
|
}
|
|
839
|
-
|
|
1053
|
+
exclusionByUnit[key] = declaration
|
|
840
1054
|
}
|
|
841
|
-
exclusionByUnit[unit.ID] = declaration
|
|
842
1055
|
}
|
|
843
1056
|
if conflictingUnit != nil {
|
|
844
1057
|
evidence := declaration
|
|
@@ -859,7 +1072,10 @@ func evaluateEvidenceGraph(
|
|
|
859
1072
|
)
|
|
860
1073
|
}
|
|
861
1074
|
}
|
|
862
|
-
|
|
1075
|
+
// Health alone gates the per-host policies. The path count that used to
|
|
1076
|
+
// sit here answered the empty-population question a second time, and a
|
|
1077
|
+
// reference with units has matched a path by construction.
|
|
1078
|
+
if !state.Healthy || !reference.Healthy {
|
|
863
1079
|
continue
|
|
864
1080
|
}
|
|
865
1081
|
if single {
|
|
@@ -874,8 +1090,40 @@ func evaluateEvidenceGraph(
|
|
|
874
1090
|
)
|
|
875
1091
|
}
|
|
876
1092
|
}
|
|
1093
|
+
// A checklist answers coverage per host, and that answer subsumes the
|
|
1094
|
+
// population-wide one: an item no host acknowledged is reported on every
|
|
1095
|
+
// host that owes it. With no selected host there is nobody to report it
|
|
1096
|
+
// on, so the population-wide question is the only one left and stays.
|
|
1097
|
+
perHostCoverage := checklist && len(state.Hosts) != 0
|
|
1098
|
+
if perHostCoverage {
|
|
1099
|
+
for _, host := range state.Hosts {
|
|
1100
|
+
missing := make([]string, 0, len(reference.Units))
|
|
1101
|
+
for _, unit := range reference.Units {
|
|
1102
|
+
if acknowledgedByHost[host.ID][unit.ID] ||
|
|
1103
|
+
aggregateByHost[host.ID][unit.ID] {
|
|
1104
|
+
continue
|
|
1105
|
+
}
|
|
1106
|
+
missing = append(missing, "'"+unit.Target+"'")
|
|
1107
|
+
}
|
|
1108
|
+
if len(missing) == 0 {
|
|
1109
|
+
continue
|
|
1110
|
+
}
|
|
1111
|
+
// Doing the work is named before either tag, for the reason the
|
|
1112
|
+
// population-wide repair below names it first: a repair offering only
|
|
1113
|
+
// the two tags frames an unmet item as a question of which tag to
|
|
1114
|
+
// write.
|
|
1115
|
+
repair := "Do what each item requires and cite it with @evidence on this host, or write @evidenceExclude for an item that does not apply here." + untrueTagWarning
|
|
1116
|
+
if reference.Spec.Policy.NoExclude {
|
|
1117
|
+
repair = "Do what each item requires and cite it with @evidence on this host; this reference forbids @evidenceExclude." + untrueTagWarning
|
|
1118
|
+
}
|
|
1119
|
+
problems = append(
|
|
1120
|
+
problems,
|
|
1121
|
+
"Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" has not acknowledged "+decimal(len(missing))+" of "+decimal(len(reference.Units))+" checklist item(s): "+strings.Join(missing, ", ")+". "+repair,
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
877
1125
|
for _, unit := range reference.Units {
|
|
878
|
-
if !acknowledged[unit.ID] {
|
|
1126
|
+
if !acknowledged[unit.ID] && !perHostCoverage {
|
|
879
1127
|
// Building the host is named first because the other two hide it. A
|
|
880
1128
|
// repair offering only the two tags frames the whole problem as which
|
|
881
1129
|
// tag to write, and an unbuilt unit then leaves as an exclusion.
|
|
@@ -900,10 +1148,27 @@ func evaluateEvidenceGraph(
|
|
|
900
1148
|
}
|
|
901
1149
|
}
|
|
902
1150
|
for _, id := range declarationIDs {
|
|
903
|
-
if resolved[id] == ""
|
|
1151
|
+
if resolved[id] == "" {
|
|
904
1152
|
continue
|
|
905
1153
|
}
|
|
906
1154
|
declaration := declarations[id]
|
|
1155
|
+
// The unhosted report sits outside the participation guard, because the
|
|
1156
|
+
// checklist claim that recorded it also marked this declaration as
|
|
1157
|
+
// participating the moment it admitted the tag as eligible. Participation
|
|
1158
|
+
// says some claim owns the tag; only `answers` says an obligation
|
|
1159
|
+
// consumed it, and `uncertain` withholds the report when a failed loader
|
|
1160
|
+
// makes consumption unknowable, the way the ghost-finding guard below
|
|
1161
|
+
// withholds its own.
|
|
1162
|
+
if obligations := unhosted[id]; len(obligations) != 0 &&
|
|
1163
|
+
!answers[id] && !uncertain[id] {
|
|
1164
|
+
problems = append(
|
|
1165
|
+
problems,
|
|
1166
|
+
"Unhosted @"+string(declaration.Tag)+" at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': a checklist acknowledgement answers for one selected host of its claim, and this declaration sits on no selected host and discharges no other obligation. Move the tag onto a host of a selected kind ("+unhostedSelections[id].names()+") in a claim that owes it."+untrueTagWarning,
|
|
1167
|
+
)
|
|
1168
|
+
}
|
|
1169
|
+
if participates[id] {
|
|
1170
|
+
continue
|
|
1171
|
+
}
|
|
907
1172
|
context := declarationObligationContext(owners[id])
|
|
908
1173
|
if obligations := outsideCarrier[id]; len(obligations) != 0 {
|
|
909
1174
|
problems = append(
|
|
@@ -1023,12 +1288,19 @@ func reviewProblems(
|
|
|
1023
1288
|
//
|
|
1024
1289
|
// Two properties are load-bearing and neither is obvious.
|
|
1025
1290
|
//
|
|
1026
|
-
// The
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1291
|
+
// The index side reads every ledger key from the review's SemanticHostIDs. On
|
|
1292
|
+
// graph hosts these are semantic identities, so the two halves of a merged
|
|
1293
|
+
// identity share a key. Indexing by their distinct source positions instead
|
|
1294
|
+
// refused a review written on `namespace I` for a citation on `interface I`,
|
|
1029
1295
|
// which is placement the graph elsewhere calls not worth a diagnostic and which
|
|
1030
|
-
// `evidence/review` accepts.
|
|
1031
|
-
//
|
|
1296
|
+
// `evidence/review` accepts. An unattached Prisma review has no graph identity,
|
|
1297
|
+
// so it stores its synthetic file-position key in SemanticHostIDs instead.
|
|
1298
|
+
//
|
|
1299
|
+
// The lookup side normally reads the declaration's SemanticHostIDs. The
|
|
1300
|
+
// declaration from that Prisma run is asymmetric: it leaves them empty and
|
|
1301
|
+
// carries the file position in HostID, so `find` falls back there. Only lookup
|
|
1302
|
+
// needs a separate source-position field; the review-side fallback described no
|
|
1303
|
+
// producer. `model.go` states both field contracts.
|
|
1032
1304
|
//
|
|
1033
1305
|
// It is built once per claim rather than scanned per citation. A linear search
|
|
1034
1306
|
// over every review, for every declaration, for every reference, is cubic in the
|
|
@@ -1045,7 +1317,7 @@ func newReviewLedger(reviews []*evidenceReview) *reviewLedger {
|
|
|
1045
1317
|
if review == nil {
|
|
1046
1318
|
continue
|
|
1047
1319
|
}
|
|
1048
|
-
for _, hostID := range
|
|
1320
|
+
for _, hostID := range review.SemanticHostIDs {
|
|
1049
1321
|
key := reviewLedgerKey(hostID, review.Reviews, review.Target)
|
|
1050
1322
|
if ledger.byHostAndTarget[key] == nil {
|
|
1051
1323
|
ledger.byHostAndTarget[key] = review
|
|
@@ -1055,22 +1327,12 @@ func newReviewLedger(reviews []*evidenceReview) *reviewLedger {
|
|
|
1055
1327
|
return ledger
|
|
1056
1328
|
}
|
|
1057
1329
|
|
|
1058
|
-
// reviewLedgerHostIDs is every identity a review answers on.
|
|
1059
|
-
//
|
|
1060
|
-
// The position identity is kept as a fallback for a carrier that has no semantic
|
|
1061
|
-
// host at all, such as an unattached Prisma documentation run, whose declarations
|
|
1062
|
-
// are keyed the same way.
|
|
1063
|
-
func reviewLedgerHostIDs(review *evidenceReview) []string {
|
|
1064
|
-
if len(review.SemanticHostIDs) != 0 {
|
|
1065
|
-
return review.SemanticHostIDs
|
|
1066
|
-
}
|
|
1067
|
-
if review.HostID == "" {
|
|
1068
|
-
return nil
|
|
1069
|
-
}
|
|
1070
|
-
return []string{review.HostID}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
1330
|
// find locates the review bound to one declaration's identity and target.
|
|
1331
|
+
//
|
|
1332
|
+
// The position fallback is declaration-only. A Prisma file-level exclusion is
|
|
1333
|
+
// a valid carrier without a semantic graph host, while the review parsed from
|
|
1334
|
+
// the same unattached run carries the file position as a synthetic ledger key.
|
|
1335
|
+
// Dropping this fallback makes that exclusion impossible to review.
|
|
1074
1336
|
func (ledger *reviewLedger) find(
|
|
1075
1337
|
declaration *evidenceDeclaration,
|
|
1076
1338
|
) *evidenceReview {
|
|
@@ -1179,7 +1441,7 @@ func materializeEntryReference(
|
|
|
1179
1441
|
if failure := loader.failure(entry); failure != "" {
|
|
1180
1442
|
state.Healthy = false
|
|
1181
1443
|
return state, []string{
|
|
1182
|
-
claimLabel(claim) + " " + referenceLabel(reference) + " could not read TypeScript entry '" + entry + "': " + failure + ". Fix filesystem access or the package installation; coverage cannot be evaluated from a partial entry graph.",
|
|
1444
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " could not read TypeScript entry '" + entry + "': " + causeReason(failure) + ". Fix filesystem access or the package installation; coverage cannot be evaluated from a partial entry graph.",
|
|
1183
1445
|
}
|
|
1184
1446
|
}
|
|
1185
1447
|
if len(state.Units) == 0 {
|
|
@@ -1305,7 +1567,7 @@ func materializePackageGlobReference(
|
|
|
1305
1567
|
if walkProblem != "" {
|
|
1306
1568
|
state.Healthy = false
|
|
1307
1569
|
return state, []string{
|
|
1308
|
-
claimLabel(claim) + " " + referenceLabel(reference) + " could not inspect TypeScript package '" + reference.Package + "': " + walkProblem + ". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1570
|
+
claimLabel(claim) + " " + referenceLabel(reference) + " could not inspect TypeScript package '" + reference.Package + "': " + causeReason(walkProblem) + ". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1309
1571
|
}
|
|
1310
1572
|
}
|
|
1311
1573
|
problems := []string{}
|
|
@@ -1318,7 +1580,7 @@ func materializePackageGlobReference(
|
|
|
1318
1580
|
state.Healthy = false
|
|
1319
1581
|
problems = append(
|
|
1320
1582
|
problems,
|
|
1321
|
-
claimLabel(claim)+" "+referenceLabel(reference)+" could not read TypeScript source '"+candidate+"': "+loader.failure(candidate)+". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1583
|
+
claimLabel(claim)+" "+referenceLabel(reference)+" could not read TypeScript source '"+candidate+"': "+causeReason(loader.failure(candidate))+". Fix filesystem access or reinstall the package; coverage cannot be evaluated from a partial population.",
|
|
1322
1584
|
)
|
|
1323
1585
|
continue
|
|
1324
1586
|
}
|