@ttsc/evidence 0.28.6 → 0.29.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.
@@ -1,3 +1,4 @@
1
+ import type { TtscLintSeverity } from "@ttsc/lint";
1
2
  import type { ITtscEvidenceGraphReference } from "./ITtscEvidenceGraphReference";
2
3
  /**
3
4
  * What every claim declares: which artifact kind makes the claim, where its
@@ -21,6 +22,12 @@ export interface ITtscEvidenceGraphClaimBase<Type extends string> {
21
22
  * configuration entries.
22
23
  */
23
24
  name?: string;
25
+ /**
26
+ * Diagnostic level for this claim. Omit it or use `undefined` to inherit the
27
+ * outer `evidence/graph` rule level. A reference may override this level
28
+ * again. `"off"` disables the entire claim, including its references.
29
+ */
30
+ severity?: TtscLintSeverity | undefined;
24
31
  /**
25
32
  * Excludes this claim from graph loading and evaluation.
26
33
  *
@@ -1,3 +1,4 @@
1
+ import type { TtscLintSeverity } from "@ttsc/lint";
1
2
  /**
2
3
  * What every evidence population declares: which artifact kind it materializes,
3
4
  * and how strictly the owning claim must acknowledge it.
@@ -25,6 +26,12 @@
25
26
  export interface ITtscEvidenceGraphReferenceBase<Type extends string> {
26
27
  /** Identifies the artifact kind this population materializes. */
27
28
  type: Type;
29
+ /**
30
+ * Diagnostic level for this reference's obligation. Omit it or use
31
+ * `undefined` to inherit the claim level, then the outer `evidence/graph`
32
+ * rule level. `"off"` disables this reference's population and obligation.
33
+ */
34
+ severity?: TtscLintSeverity | undefined;
28
35
  /**
29
36
  * Whether this reference refuses `@evidenceExclude` as an acknowledgement.
30
37
  *
package/native/config.go CHANGED
@@ -8,6 +8,8 @@ import (
8
8
  "path"
9
9
  "sort"
10
10
  "strings"
11
+
12
+ "github.com/samchon/ttsc/packages/lint/rule"
11
13
  )
12
14
 
13
15
  func decodeGraphConfig(raw json.RawMessage) (graphConfig, []string) {
@@ -54,7 +56,7 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
54
56
  }
55
57
  problems := rejectUnknownFields(
56
58
  object,
57
- []string{"type", "name", "disabled", "root", "files", "evidenceExcludeCarriers", "symbol", "reference"},
59
+ []string{"type", "name", "severity", "disabled", "root", "files", "evidenceExcludeCarriers", "symbol", "reference"},
58
60
  graphRuleName,
59
61
  path,
60
62
  )
@@ -78,6 +80,8 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
78
80
  problems = append(problems, "Invalid evidence/graph configuration at "+path+".disabled: expected a boolean.")
79
81
  }
80
82
  }
83
+ severity, severityProblems := decodeGraphSeverity(object["severity"], path+".severity")
84
+ problems = append(problems, severityProblems...)
81
85
  root, rootProblems := decodeClaimRoot(object["root"], kind, path+".root")
82
86
  problems = append(problems, rootProblems...)
83
87
  files, fileProblems := decodeFiles(object["files"], path+".files")
@@ -98,6 +102,7 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
98
102
  }
99
103
  return claimSpec{
100
104
  Index: index,
105
+ Severity: severity,
101
106
  Type: kind,
102
107
  Name: name,
103
108
  Disabled: disabled,
@@ -109,16 +114,26 @@ func decodeClaim(raw json.RawMessage, index int) (claimSpec, []string) {
109
114
  }, nil
110
115
  }
111
116
 
112
- // enabledGraphConfig removes explicitly staged claims after their public
117
+ // enabledGraphConfig removes disabled claims and references after their public
113
118
  // configuration has been validated. Keeping the filter separate from decoding
114
- // preserves original claim indexes and prevents disabled entries from hiding a
119
+ // preserves original indexes and prevents disabled entries from hiding a
115
120
  // malformed shape.
116
121
  func enabledGraphConfig(config graphConfig) graphConfig {
117
122
  claims := make([]claimSpec, 0, len(config.Claims))
118
123
  for _, claim := range config.Claims {
119
- if claim.Disabled {
124
+ if claim.Disabled || claim.Severity != nil && *claim.Severity == rule.SeverityOff {
125
+ continue
126
+ }
127
+ references := make([]referenceSpec, 0, len(claim.References))
128
+ for _, reference := range claim.References {
129
+ if reference.Severity == nil || *reference.Severity != rule.SeverityOff {
130
+ references = append(references, reference)
131
+ }
132
+ }
133
+ if len(references) == 0 {
120
134
  continue
121
135
  }
136
+ claim.References = references
122
137
  claims = append(claims, claim)
123
138
  }
124
139
  config.Claims = claims
@@ -183,6 +198,7 @@ func decodeReference(
183
198
  object,
184
199
  []string{
185
200
  "type",
201
+ "severity",
186
202
  "noEvidenceExclude",
187
203
  "uniqueEvidence",
188
204
  "singleEvidencePerSymbol",
@@ -204,6 +220,8 @@ func decodeReference(
204
220
  if problem := rejectForeignTypeScriptReference(claimKind, kind, path); problem != "" {
205
221
  problems = append(problems, problem)
206
222
  }
223
+ severity, severityProblems := decodeGraphSeverity(object["severity"], path+".severity")
224
+ problems = append(problems, severityProblems...)
207
225
  root, rootProblems := decodeRoot(object["root"], kind, path+".root")
208
226
  problems = append(problems, rootProblems...)
209
227
  policy, policyProblems := decodeReferencePolicy(object, kind, path)
@@ -265,14 +283,15 @@ func decodeReference(
265
283
  return referenceSpec{}, problems
266
284
  }
267
285
  return referenceSpec{
268
- Index: index,
269
- Type: kind,
270
- Policy: policy,
271
- Root: root,
272
- Files: files,
273
- Source: source,
274
- Package: packageName,
275
- Symbols: symbols,
286
+ Severity: severity,
287
+ Index: index,
288
+ Type: kind,
289
+ Policy: policy,
290
+ Root: root,
291
+ Files: files,
292
+ Source: source,
293
+ Package: packageName,
294
+ Symbols: symbols,
276
295
  }, nil
277
296
  }
278
297
 
package/native/graph.go CHANGED
@@ -31,6 +31,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
31
31
  reportProblems(ctx, problems)
32
32
  return
33
33
  }
34
+ resolveGraphSeverities(&config, ctx.Severity)
34
35
  config = enabledGraphConfig(config)
35
36
  if len(config.Claims) == 0 {
36
37
  cycle.Corpus = graphCorpus{Config: config}
@@ -46,6 +47,7 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
46
47
  ctx.Report("Evidence graph project root '" + root + "' is not a readable directory. Fix the ttsc project identity before evaluating evidence globs.")
47
48
  return
48
49
  }
50
+ diagnostics := graphDiagnostics{}
49
51
  // Every population is anchored before anything is read, so a loader, a
50
52
  // diagnostic, and the corpus the editor receives all speak of one resolved
51
53
  // base rather than each re-deriving it from the author's spelling.
@@ -68,15 +70,15 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
68
70
  root,
69
71
  claimPopulationConfig(config, artifactPrisma),
70
72
  )
71
- problems = append(
72
- problems,
73
+ diagnostics = append(
74
+ diagnostics,
73
75
  typeScriptBaseProblems(
74
76
  claimPopulationConfig(config, artifactTypeScript),
75
77
  typescript,
76
78
  )...,
77
79
  )
78
- problems = append(problems, markdownClaimProblems...)
79
- problems = append(problems, prismaClaimProblems...)
80
+ diagnostics = append(diagnostics, markdownClaimProblems...)
81
+ diagnostics = append(diagnostics, prismaClaimProblems...)
80
82
  // Governance is judged against the configuration as declared, before
81
83
  // activation drops a claim whose population materialized no unit of its
82
84
  // symbol kind. The question is whether an author put this file in a
@@ -91,10 +93,10 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
91
93
  markdown, markdownProblems := loadMarkdownInventories(root, config)
92
94
  prisma, prismaProblems := loadPrismaInventories(root, config)
93
95
  swagger, swaggerProblems := loadSwaggerInventories(root, config)
94
- problems = append(problems, markdownProblems...)
95
- problems = append(problems, prismaProblems...)
96
- problems = append(problems, swaggerProblems...)
97
- problems = append(problems, unreadableTypeScriptTags(typescript, governed)...)
96
+ diagnostics = append(diagnostics, markdownProblems...)
97
+ diagnostics = append(diagnostics, prismaProblems...)
98
+ diagnostics = append(diagnostics, swaggerProblems...)
99
+ diagnostics = append(diagnostics, unreadableTypeScriptTags(typescript, governed, declared)...)
98
100
  loader := newTypeScriptLoader(root, typescript)
99
101
  states, stateProblems := materializeClaimStates(
100
102
  config,
@@ -104,10 +106,10 @@ func (graphRule) Check(ctx *rule.ProjectContext) {
104
106
  typescript,
105
107
  loader,
106
108
  )
107
- problems = append(problems, stateProblems...)
108
- problems = append(problems, evaluateEvidenceGraph(states, loader)...)
109
- reportProblems(ctx, problems)
110
- if len(problems) == 0 {
109
+ diagnostics = append(diagnostics, stateProblems...)
110
+ diagnostics = append(diagnostics, evaluateEvidenceGraph(states, loader)...)
111
+ diagnostics.report(ctx)
112
+ if len(diagnostics) == 0 {
111
113
  // Published only on a clean evaluation, because the host reads state
112
114
  // from a rule that passed and reporting anything marks this one failed
113
115
  // (`linthost/hints.go:147-149`, `linthost/project_engine.go:68-77`).
@@ -238,10 +240,11 @@ func materializeClaimStates(
238
240
  swagger map[string]*artifactInventory,
239
241
  typescript map[string]*artifactInventory,
240
242
  loader *typeScriptLoader,
241
- ) ([]claimState, []string) {
243
+ ) ([]claimState, graphDiagnostics) {
242
244
  states := make([]claimState, 0, len(config.Claims))
243
- problems := []string{}
245
+ problems := graphDiagnostics{}
244
246
  for _, claim := range config.Claims {
247
+ severity := claim.Level
245
248
  inventories := inventoriesOf(claim.Type, markdown, prisma, swagger, typescript)
246
249
  paths := matchingInventoryPaths(inventories, claim.Base, claim.Files)
247
250
  state := claimState{
@@ -268,8 +271,8 @@ func materializeClaimStates(
268
271
  // exclusion the claim writes, so the misspelling is reported where
269
272
  // it was made rather than as a placement finding on each tag.
270
273
  if len(carrierPaths) == 0 && len(paths) != 0 {
271
- problems = append(
272
- problems,
274
+ problems = problems.add(
275
+ severity,
273
276
  claimLabel(claim)+" declares evidenceExcludeCarriers "+describePopulation(claim.Base, claim.ExclusionCarriers)+", which selects none of its "+decimal(len(paths))+" claim file(s). Point the patterns at a file this claim already selects, or drop the property to accept an exclusion anywhere in the population.",
274
277
  )
275
278
  }
@@ -346,6 +349,7 @@ func materializeClaimStates(
346
349
  }
347
350
  sortUnits(state.Hosts)
348
351
  for _, reference := range claim.References {
352
+ severity := reference.Level
349
353
  referenceInventories := inventoriesOf(
350
354
  reference.Type,
351
355
  markdown,
@@ -359,7 +363,7 @@ func materializeClaimStates(
359
363
  reference,
360
364
  loader,
361
365
  )
362
- problems = append(problems, entryProblems...)
366
+ problems = problems.add(severity, entryProblems...)
363
367
  state.References = append(state.References, entryState)
364
368
  continue
365
369
  }
@@ -369,7 +373,7 @@ func materializeClaimStates(
369
373
  reference,
370
374
  loader,
371
375
  )
372
- problems = append(problems, packageProblems...)
376
+ problems = problems.add(severity, packageProblems...)
373
377
  state.References = append(state.References, packageState)
374
378
  continue
375
379
  }
@@ -380,7 +384,7 @@ func materializeClaimStates(
380
384
  referenceInventories,
381
385
  loader,
382
386
  )
383
- problems = append(problems, localProblems...)
387
+ problems = problems.add(severity, localProblems...)
384
388
  state.References = append(state.References, localState)
385
389
  continue
386
390
  }
@@ -396,13 +400,13 @@ func materializeClaimStates(
396
400
  }
397
401
  if len(referencePaths) == 0 && referenceState.Healthy {
398
402
  if reference.Type == artifactSwagger {
399
- problems = append(
400
- problems,
403
+ problems = problems.add(
404
+ severity,
401
405
  claimLabel(claim)+" "+referenceLabel(reference)+" matched no swagger source for "+describeReferenceSources(reference)+". Fix the reference location; this obligation cannot materialize evidence units without a source.",
402
406
  )
403
407
  } else {
404
- problems = append(
405
- problems,
408
+ problems = problems.add(
409
+ severity,
406
410
  claimLabel(claim)+" "+referenceLabel(reference)+" matched no "+string(reference.Type)+" files for "+describePopulation(reference.Base, reference.Files)+". Fix the reference globs or the root they resolve against; this obligation cannot materialize evidence units without files.",
407
411
  )
408
412
  }
@@ -477,8 +481,8 @@ func materializeClaimStates(
477
481
  len(referenceState.Units) == 0 &&
478
482
  referenceState.Healthy &&
479
483
  !selectedInventoryProblem {
480
- problems = append(
481
- problems,
484
+ problems = problems.add(
485
+ severity,
482
486
  claimLabel(claim)+" "+referenceLabel(reference)+" matched "+decimal(len(referencePaths))+" file(s) but found no selected evidence units ("+reference.Symbols.names()+"). Select symbol kinds present in those files or correct the reference globs.",
483
487
  )
484
488
  }
@@ -492,8 +496,8 @@ func materializeClaimStates(
492
496
  func evaluateEvidenceGraph(
493
497
  states []claimState,
494
498
  loader *typeScriptLoader,
495
- ) []string {
496
- problems := []string{}
499
+ ) graphDiagnostics {
500
+ problems := graphDiagnostics{}
497
501
  targets := map[string]map[string]*evidenceUnit{}
498
502
  markdownTargets := map[string]map[string]*evidenceUnit{}
499
503
  // Scoped targets are keyed by owning file as well as name, which is what
@@ -604,11 +608,12 @@ func evaluateEvidenceGraph(
604
608
 
605
609
  resolved := map[string]string{}
606
610
  for _, id := range declarationIDs {
611
+ severity := ownerSeverity(owners[id])
607
612
  declaration := declarations[id]
608
613
  context := declarationObligationContext(owners[id])
609
614
  if !declaration.valid() {
610
- problems = append(
611
- problems,
615
+ problems = problems.add(
616
+ severity,
612
617
  "Malformed @"+string(declaration.Tag)+" declaration at "+declaration.location()+" for "+context+": target and non-empty reason are mandatory. Write '@"+string(declaration.Tag)+" <target> <reason>'."+untrueTagWarning,
613
618
  )
614
619
  continue
@@ -622,7 +627,7 @@ func evaluateEvidenceGraph(
622
627
  context,
623
628
  )
624
629
  if problem != "" {
625
- problems = append(problems, problem)
630
+ problems = problems.add(severity, problem)
626
631
  continue
627
632
  }
628
633
  resolved[id] = unitID
@@ -630,8 +635,8 @@ func evaluateEvidenceGraph(
630
635
  }
631
636
  if declaration.Type == artifactTypeScript &&
632
637
  looksLikeTypeScriptTarget(declaration.Target, targets, markdownTargets) {
633
- problems = append(
634
- problems,
638
+ problems = problems.add(
639
+ severity,
635
640
  "Unbraced TypeScript evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a target naming a symbol is now written as an inline link, so the citing module's import is what resolves it. Write '@"+string(declaration.Tag)+" {@link "+declaration.Target+"} <reason>' and import the symbol; 'import type' is enough.",
636
641
  )
637
642
  continue
@@ -648,8 +653,8 @@ func evaluateEvidenceGraph(
648
653
  if declaration.Type != artifactTypeScript {
649
654
  addressable, code := splitCodeCandidates(candidates)
650
655
  if len(addressable) == 0 && len(code) != 0 {
651
- problems = append(
652
- problems,
656
+ problems = problems.add(
657
+ severity,
653
658
  "Code evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": a "+string(declaration.Type)+" claim cannot cite a TypeScript symbol, because a symbol citation resolves through the citing module's imports and this artifact has none. Invert the obligation so the code cites this artifact, or move the citation into TypeScript.",
654
659
  )
655
660
  continue
@@ -667,14 +672,11 @@ func evaluateEvidenceGraph(
667
672
  continue
668
673
  }
669
674
  if hidden := hiddenTargets[declaration.Target]; hidden != nil {
670
- problems = append(
671
- problems,
672
- hiddenTargetProblem(declaration, hidden, context),
673
- )
675
+ problems = problems.add(severity, hiddenTargetProblem(declaration, hidden, context))
674
676
  continue
675
677
  }
676
- problems = append(
677
- problems,
678
+ problems = problems.add(
679
+ severity,
678
680
  "Unresolved evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": no configured source materializes that evidence unit. Correct the target, make one of the named references select the source unit this claim actually uses, or remove the tag when this host does not answer for that target."+untrueTagWarning,
679
681
  )
680
682
  case 1:
@@ -687,8 +689,8 @@ func evaluateEvidenceGraph(
687
689
  descriptions = append(descriptions, unit.Readable+" at "+unit.location())
688
690
  }
689
691
  sort.Strings(descriptions)
690
- problems = append(
691
- problems,
692
+ problems = problems.add(
693
+ severity,
692
694
  "Ambiguous evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": it matches "+strings.Join(descriptions, "; ")+". Rename or qualify the source symbols so the target has exactly one meaning.",
693
695
  )
694
696
  }
@@ -697,11 +699,13 @@ func evaluateEvidenceGraph(
697
699
  participates := map[string]bool{}
698
700
  uncertain := map[string]bool{}
699
701
  outOfScope := map[string][]string{}
702
+ outOfScopeLevels := map[string]rule.Severity{}
700
703
  outOfScopeSelections := map[string]symbolSet{}
701
704
  // An exclusion outside its claim's declared carriers is a placement
702
705
  // finding, not a host-kind one, so it carries its own obligations and the
703
706
  // carrier globs its message must name.
704
707
  outsideCarrier := map[string][]string{}
708
+ outsideCarrierLevels := map[string]rule.Severity{}
705
709
  outsideCarrierGlobs := map[string]string{}
706
710
  // A checklist tag speaking for no selected host is a non-participation
707
711
  // finding: within its reference it answers nothing, and whether that earns a
@@ -711,6 +715,7 @@ func evaluateEvidenceGraph(
711
715
  // elsewhere — so the finding is recorded here and judged after the walk,
712
716
  // when `answers` can say whether anything consumed the tag.
713
717
  unhosted := map[string][]string{}
718
+ unhostedLevels := map[string]rule.Severity{}
714
719
  unhostedSelections := map[string]symbolSet{}
715
720
  // answers marks a declaration that wrote at least one acknowledgement
716
721
  // ledger, or was refused as an aggregate, which is that citation's own
@@ -732,6 +737,7 @@ func evaluateEvidenceGraph(
732
737
  // reviewing reference pays nothing.
733
738
  var reviewLedgerForClaim *reviewLedger
734
739
  for _, reference := range state.References {
740
+ severity := reference.Spec.Level
735
741
  if !reference.Healthy {
736
742
  for _, declaration := range state.Declarations {
737
743
  uncertain[declaration.ID] = true
@@ -819,6 +825,7 @@ func evaluateEvidenceGraph(
819
825
  continue
820
826
  }
821
827
  if declaration.Tag == tagExclude && state.OutsideCarrier[declaration.ID] {
828
+ outsideCarrierLevels[declaration.ID] = max(outsideCarrierLevels[declaration.ID], severity)
822
829
  outsideCarrier[declaration.ID] = appendUniqueString(
823
830
  outsideCarrier[declaration.ID],
824
831
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -827,6 +834,7 @@ func evaluateEvidenceGraph(
827
834
  continue
828
835
  }
829
836
  if !declarationEligibleForClaim(declaration, state.Spec) {
837
+ outOfScopeLevels[declaration.ID] = max(outOfScopeLevels[declaration.ID], severity)
830
838
  outOfScope[declaration.ID] = appendUniqueString(
831
839
  outOfScope[declaration.ID],
832
840
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -849,8 +857,8 @@ func evaluateEvidenceGraph(
849
857
  continue
850
858
  }
851
859
  if declaration.Tag == tagExclude && reference.Spec.Policy.NoExclude {
852
- problems = append(
853
- problems,
860
+ problems = problems.add(
861
+ severity,
854
862
  "Forbidden @evidenceExclude for '"+scopesByID[scopeID].Target+"' at "+declaration.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": noEvidenceExclude requires positive @evidence for this reference. Remove the exclusion and cite the target from a selected "+string(state.Spec.Type)+" host that answers for it."+untrueTagWarning,
855
863
  )
856
864
  continue
@@ -889,6 +897,7 @@ func evaluateEvidenceGraph(
889
897
  // answer, and a hard diagnostic here left that valid configuration
890
898
  // no placement to exist in. The end-of-run block reports the tag
891
899
  // once nothing has consumed it.
900
+ unhostedLevels[declaration.ID] = max(unhostedLevels[declaration.ID], severity)
892
901
  unhosted[declaration.ID] = appendUniqueString(
893
902
  unhosted[declaration.ID],
894
903
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -910,8 +919,8 @@ func evaluateEvidenceGraph(
910
919
  for _, unit := range covered {
911
920
  targets = append(targets, "'"+unit.Target+"'")
912
921
  }
913
- problems = append(
914
- problems,
922
+ problems = problems.add(
923
+ severity,
915
924
  "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
925
  )
917
926
  for _, unit := range covered {
@@ -936,7 +945,7 @@ func evaluateEvidenceGraph(
936
945
  if reviewLedgerForClaim == nil {
937
946
  reviewLedgerForClaim = newReviewLedger(state.Reviews)
938
947
  }
939
- problems = append(problems, reviewProblems(
948
+ problems = problems.add(severity, reviewProblems(
940
949
  declaration,
941
950
  scopesByID[scopeID],
942
951
  reference,
@@ -952,8 +961,8 @@ func evaluateEvidenceGraph(
952
961
  evidenceByHostAndScope[declaration.HostID] = byScope
953
962
  }
954
963
  if first := byScope[scopeID]; first != nil {
955
- problems = append(
956
- problems,
964
+ problems = problems.add(
965
+ severity,
957
966
  "Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
958
967
  )
959
968
  } else {
@@ -1060,14 +1069,14 @@ func evaluateEvidenceGraph(
1060
1069
  evidence = conflictingDeclaration
1061
1070
  exclusion = declaration
1062
1071
  }
1063
- problems = append(
1064
- problems,
1072
+ problems = problems.add(
1073
+ severity,
1065
1074
  "Conflicting acknowledgements for '"+conflictingUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": @evidence at "+evidence.location()+" overlaps @evidenceExclude at "+exclusion.location()+". Delete whichever is untrue of this host."+untrueTagWarning,
1066
1075
  )
1067
1076
  }
1068
1077
  if duplicateExclusionUnit != nil {
1069
- problems = append(
1070
- problems,
1078
+ problems = problems.add(
1079
+ severity,
1071
1080
  "Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
1072
1081
  )
1073
1082
  }
@@ -1084,8 +1093,8 @@ func evaluateEvidenceGraph(
1084
1093
  if count == 1 {
1085
1094
  continue
1086
1095
  }
1087
- problems = append(
1088
- problems,
1096
+ problems = problems.add(
1097
+ severity,
1089
1098
  "Evidence host "+host.Readable+" at "+host.location()+" in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" cites "+decimal(count)+" distinct selected evidence unit(s); singleEvidencePerSymbol requires exactly 1. Split this host so each unit has one that owns it, or keep positive @evidence citations on this semantic host to exactly one distinct unit.",
1090
1099
  )
1091
1100
  }
@@ -1116,8 +1125,8 @@ func evaluateEvidenceGraph(
1116
1125
  if reference.Spec.Policy.NoExclude {
1117
1126
  repair = "Do what each item requires and cite it with @evidence on this host; this reference forbids @evidenceExclude." + untrueTagWarning
1118
1127
  }
1119
- problems = append(
1120
- problems,
1128
+ problems = problems.add(
1129
+ severity,
1121
1130
  "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
1131
  )
1123
1132
  }
@@ -1131,8 +1140,8 @@ func evaluateEvidenceGraph(
1131
1140
  if reference.Spec.Policy.NoExclude {
1132
1141
  repair = "Cite the artifact that answers for this unit with @evidence on a selected " + string(state.Spec.Type) + " host, building that artifact first when none does; this reference forbids @evidenceExclude." + untrueTagWarning
1133
1142
  }
1134
- problems = append(
1135
- problems,
1143
+ problems = problems.add(
1144
+ severity,
1136
1145
  "Missing acknowledgement for '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+". "+repair,
1137
1146
  )
1138
1147
  }
@@ -1140,14 +1149,15 @@ func evaluateEvidenceGraph(
1140
1149
  if !unique || hostCount <= 1 {
1141
1150
  continue
1142
1151
  }
1143
- problems = append(
1144
- problems,
1152
+ problems = problems.add(
1153
+ severity,
1145
1154
  "Evidence unit '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+" has "+decimal(hostCount)+" distinct positive evidence host(s); uniqueEvidence allows at most 1. Keep the one selected "+string(state.Spec.Type)+" host that owns this unit and remove the other citation(s).",
1146
1155
  )
1147
1156
  }
1148
1157
  }
1149
1158
  }
1150
1159
  for _, id := range declarationIDs {
1160
+ severity := ownerSeverity(owners[id])
1151
1161
  if resolved[id] == "" {
1152
1162
  continue
1153
1163
  }
@@ -1161,8 +1171,8 @@ func evaluateEvidenceGraph(
1161
1171
  // withholds its own.
1162
1172
  if obligations := unhosted[id]; len(obligations) != 0 &&
1163
1173
  !answers[id] && !uncertain[id] {
1164
- problems = append(
1165
- problems,
1174
+ problems = problems.add(
1175
+ unhostedLevels[id],
1166
1176
  "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
1177
  )
1168
1178
  }
@@ -1171,8 +1181,8 @@ func evaluateEvidenceGraph(
1171
1181
  }
1172
1182
  context := declarationObligationContext(owners[id])
1173
1183
  if obligations := outsideCarrier[id]; len(obligations) != 0 {
1174
- problems = append(
1175
- problems,
1184
+ problems = problems.add(
1185
+ outsideCarrierLevels[id],
1176
1186
  "Misplaced @evidenceExclude at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': evidenceExcludeCarriers confines this claim's exclusions to "+outsideCarrierGlobs[id]+". Move the tag there, or delete it and build what the target requires of this claim."+untrueTagWarning,
1177
1187
  )
1178
1188
  continue
@@ -1183,14 +1193,14 @@ func evaluateEvidenceGraph(
1183
1193
  host = "unsupported or non-exported declaration"
1184
1194
  }
1185
1195
  if declaration.Tag == tagExclude {
1186
- problems = append(
1187
- problems,
1196
+ problems = problems.add(
1197
+ outOfScopeLevels[id],
1188
1198
  "Out-of-scope @evidenceExclude carrier at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': '"+host+"' is not an eligible exclusion carrier in these matching claim files. Move the exclusion to a supported public export or selected declaration host, or use a top-level unattached Prisma documentation comment.",
1189
1199
  )
1190
1200
  continue
1191
1201
  }
1192
- problems = append(
1193
- problems,
1202
+ problems = problems.add(
1203
+ outOfScopeLevels[id],
1194
1204
  "Out-of-scope @"+string(declaration.Tag)+" host at "+declaration.location()+" for "+strings.Join(obligations, "; ")+", target '"+displayTarget(declaration.Target)+"': host kind '"+host+"' is not selected ("+outOfScopeSelections[id].names()+") by any of these claim obligations. Move the declaration to a selected host, or widen the claim symbol selector only when it genuinely owns this target."+untrueTagWarning,
1195
1205
  )
1196
1206
  continue
@@ -1201,8 +1211,8 @@ func evaluateEvidenceGraph(
1201
1211
  // derive a second claim from an incomplete graph.
1202
1212
  continue
1203
1213
  }
1204
- problems = append(
1205
- problems,
1214
+ problems = problems.add(
1215
+ severity,
1206
1216
  "Non-participating @"+string(declaration.Tag)+" target '"+displayTarget(declaration.Target)+"' at "+declaration.location()+" for "+context+": the target resolves, but none of this declaration's configured references selects it. Correct the target or reference, or move the tag to an eligible host or exclusion carrier in the claim that owes it; a resolving tag must discharge at least one obligation."+untrueTagWarning,
1207
1217
  )
1208
1218
  }
@@ -23,9 +23,9 @@ var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-
23
23
  func loadMarkdownInventories(
24
24
  root string,
25
25
  config graphConfig,
26
- ) (map[string]*artifactInventory, []string) {
26
+ ) (map[string]*artifactInventory, graphDiagnostics) {
27
27
  inventories := map[string]*artifactInventory{}
28
- problems := []string{}
28
+ problems := graphDiagnostics{}
29
29
  for _, base := range configuredBases(config, artifactMarkdown) {
30
30
  problems = append(
31
31
  problems,
@@ -39,16 +39,17 @@ func loadMarkdownBase(
39
39
  base populationBase,
40
40
  config graphConfig,
41
41
  inventories map[string]*artifactInventory,
42
- ) []string {
43
- problems := []string{}
42
+ ) graphDiagnostics {
43
+ problems := graphDiagnostics{}
44
+ severity := populationSeverity(config, artifactMarkdown, base, "", "*", false)
44
45
  if problem := baseDirectoryProblem(base, artifactMarkdown); problem != "" {
45
46
  recordPopulationFailure(inventories, artifactMarkdown, base)
46
- return []string{problem}
47
+ return problems.add(severity, problem)
47
48
  }
48
49
  from, resolved := resolvedBaseDirectory(base)
49
50
  if !resolved {
50
51
  recordPopulationFailure(inventories, artifactMarkdown, base)
51
- return []string{unresolvedBaseProblem(base, artifactMarkdown)}
52
+ return problems.add(severity, unresolvedBaseProblem(base, artifactMarkdown))
52
53
  }
53
54
  err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
54
55
  if walkErr != nil {
@@ -82,7 +83,8 @@ func loadMarkdownBase(
82
83
  )
83
84
  if relevant {
84
85
  recordPopulationFailure(inventories, artifactMarkdown, base)
85
- problems = append(problems, problem)
86
+ relative, _ := relativeProjectPath(from, current)
87
+ problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, "*", true), problem)
86
88
  }
87
89
  // `WalkDir` passes a nil entry only for its root, which the guard above
88
90
  // answers, so this error belongs to a directory whose listing failed and
@@ -105,6 +107,7 @@ func loadMarkdownBase(
105
107
  if !matchesConfiguredMarkdownFile(config, base, relative) {
106
108
  return nil
107
109
  }
110
+ severity := populationSeverity(config, artifactMarkdown, base, relative, "*", false)
108
111
  address := base.addressOf(relative)
109
112
  content, readErr := os.ReadFile(current)
110
113
  if readErr != nil {
@@ -113,26 +116,29 @@ func loadMarkdownBase(
113
116
  Type: artifactMarkdown,
114
117
  LoadFailed: true,
115
118
  }
116
- problems = append(problems, "Evidence graph could not read Markdown file '"+address.Display+"': "+causeText(readErr)+". Fix filesystem access or exclude the file from configured globs.")
119
+ problems = problems.add(
120
+ severity,
121
+ "Evidence graph could not read Markdown file '"+address.Display+"': "+causeText(readErr)+". Fix filesystem access or exclude the file from configured globs.",
122
+ )
117
123
  return nil
118
124
  }
119
125
  inventory, _ := scanMarkdownInventory(address, string(content))
120
126
  inventories[address.Key] = inventory
121
127
  for _, inventoryProblem := range inventory.Problems {
122
128
  if selectedByMarkdownPopulation(config, base, relative, inventoryProblem.Symbol) {
123
- problems = append(problems, inventoryProblem.Message)
129
+ problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, inventoryProblem.Symbol, false), inventoryProblem.Message)
124
130
  }
125
131
  }
126
132
  // An unreadable tag is not a health question and not a symbol question
127
133
  // either: the file loaded, its units are complete, and the tag reaches no
128
134
  // host whichever symbol a reference selects. The walk already refuses a
129
135
  // path no configured glob takes, so reaching here is enough to report.
130
- problems = append(problems, inventory.Unreadable...)
136
+ problems = problems.add(severity, inventory.Unreadable...)
131
137
  return nil
132
138
  })
133
139
  if err != nil {
134
140
  recordPopulationFailure(inventories, artifactMarkdown, base)
135
- problems = append(problems, unlistableBaseProblem(base, "Markdown", err))
141
+ problems = problems.add(severity, unlistableBaseProblem(base, "Markdown", err))
136
142
  }
137
143
  return problems
138
144
  }
package/native/model.go CHANGED
@@ -5,6 +5,7 @@ import (
5
5
  "strings"
6
6
 
7
7
  shimast "github.com/microsoft/typescript-go/shim/ast"
8
+ "github.com/samchon/ttsc/packages/lint/rule"
8
9
  )
9
10
 
10
11
  const graphRuleName = "evidence/graph"
@@ -57,6 +58,8 @@ type graphConfig struct {
57
58
  }
58
59
 
59
60
  type claimSpec struct {
61
+ Severity *rule.Severity
62
+ Level rule.Severity
60
63
  Index int
61
64
  Type artifactKind
62
65
  Name string
@@ -84,13 +87,15 @@ type claimSpec struct {
84
87
  }
85
88
 
86
89
  type referenceSpec struct {
87
- Index int
88
- Type artifactKind
89
- Policy referencePolicy
90
- Root string
91
- Base populationBase
92
- Files globSet
93
- Source string
90
+ Severity *rule.Severity
91
+ Level rule.Severity
92
+ Index int
93
+ Type artifactKind
94
+ Policy referencePolicy
95
+ Root string
96
+ Base populationBase
97
+ Files globSet
98
+ Source string
94
99
  // Package moves the base that Files resolves against from the project to an
95
100
  // installed package. With no globs it also becomes the selection itself: the
96
101
  // package's declaration entry defines the population by reachability, while
package/native/prisma.go CHANGED
@@ -12,6 +12,8 @@ import (
12
12
  "sort"
13
13
  "strings"
14
14
  "time"
15
+
16
+ "github.com/samchon/ttsc/packages/lint/rule"
15
17
  )
16
18
 
17
19
  const prismaBridgeTimeout = 60 * time.Second
@@ -127,7 +129,7 @@ type prismaField struct {
127
129
  func loadPrismaInventories(
128
130
  root string,
129
131
  config graphConfig,
130
- ) (map[string]*artifactInventory, []string) {
132
+ ) (map[string]*artifactInventory, graphDiagnostics) {
131
133
  // A graph that names no Prisma glob never pays for one. The walk below is
132
134
  // cheap on such a project — every directory fails the descendant test and is
133
135
  // skipped at once — but "cheap" is still a full listing of the project root
@@ -160,27 +162,28 @@ func loadPrismaInventories(
160
162
  // ten-model schema and a two-hundred-model one pay nearly the same. A
161
163
  // resident host repeats this every cycle, so an unchanged schema would
162
164
  // otherwise be re-parsed on every TypeScript keystroke that rebuilds.
165
+ severity := prismaSetSeverity(config, inventories)
163
166
  digest := prismaContentDigest(root, set.Sources)
164
167
  if outcome, hit := prismaSchemas.lookup(digest); hit {
165
168
  return inventories, append(
166
169
  problems,
167
- prismaUnitsFromOutcome(root, set, inventories, outcome)...,
170
+ prismaUnitsFromOutcome(root, set, inventories, outcome, config)...,
168
171
  )
169
172
  }
170
173
 
171
174
  result, err := normalizePrismaSet(root, set.Sources)
172
175
  if err != nil {
173
176
  message := "Evidence graph could not run its Prisma schema loader: " + causeText(err) + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
174
- return inventories, append(problems, failPrismaSet(inventories, set, message))
177
+ return inventories, problems.add(severity, failPrismaSet(inventories, set, message))
175
178
  }
176
179
 
177
180
  outcome, problem := prismaOutcomeOf(result)
178
181
  if problem != "" {
179
- return inventories, append(problems, failPrismaSet(inventories, set, problem))
182
+ return inventories, problems.add(severity, failPrismaSet(inventories, set, problem))
180
183
  }
181
184
  problems = append(
182
185
  problems,
183
- prismaUnitsFromOutcome(root, set, inventories, outcome)...,
186
+ prismaUnitsFromOutcome(root, set, inventories, outcome, config)...,
184
187
  )
185
188
  rememberPrismaSchema(outcome.digest, outcome)
186
189
  return inventories, problems
@@ -223,27 +226,28 @@ func prismaOutcomeOf(result prismaNormalizationResult) (prismaSetOutcome, string
223
226
  // Order is the digest's, so it must not depend on filesystem enumeration.
224
227
  func configuredPrismaAddresses(
225
228
  config graphConfig,
226
- ) ([]artifactAddress, []string) {
229
+ ) ([]artifactAddress, graphDiagnostics) {
227
230
  addresses, _, problems := configuredPrismaAddressesWithHealth(config)
228
231
  return addresses, problems
229
232
  }
230
233
 
231
234
  func configuredPrismaAddressesWithHealth(
232
235
  config graphConfig,
233
- ) ([]artifactAddress, []populationBase, []string) {
236
+ ) ([]artifactAddress, []populationBase, graphDiagnostics) {
234
237
  addresses := []artifactAddress{}
235
238
  failedBases := []populationBase{}
236
- problems := []string{}
239
+ problems := graphDiagnostics{}
237
240
  for _, base := range configuredBases(config, artifactPrisma) {
241
+ severity := populationSeverity(config, artifactPrisma, base, "", "*", false)
238
242
  if problem := baseDirectoryProblem(base, artifactPrisma); problem != "" {
239
- problems = append(problems, problem)
243
+ problems = problems.add(severity, problem)
240
244
  failedBases = append(failedBases, base)
241
245
  continue
242
246
  }
243
247
  baseFailed := false
244
248
  from, resolved := resolvedBaseDirectory(base)
245
249
  if !resolved {
246
- problems = append(problems, unresolvedBaseProblem(base, artifactPrisma))
250
+ problems = problems.add(severity, unresolvedBaseProblem(base, artifactPrisma))
247
251
  failedBases = append(failedBases, base)
248
252
  continue
249
253
  }
@@ -272,7 +276,8 @@ func configuredPrismaAddressesWithHealth(
272
276
  )
273
277
  if relevant {
274
278
  baseFailed = true
275
- problems = append(problems, problem)
279
+ relative, _ := relativeProjectPath(from, current)
280
+ problems = problems.add(populationSeverity(config, artifactPrisma, base, relative, "*", true), problem)
276
281
  }
277
282
  return filepath.SkipDir
278
283
  }
@@ -295,7 +300,7 @@ func configuredPrismaAddressesWithHealth(
295
300
  })
296
301
  if err != nil {
297
302
  baseFailed = true
298
- problems = append(problems, unlistableBaseProblem(base, "Prisma", err))
303
+ problems = problems.add(severity, unlistableBaseProblem(base, "Prisma", err))
299
304
  }
300
305
  if baseFailed {
301
306
  failedBases = append(failedBases, base)
@@ -647,15 +652,17 @@ func prismaDeclarationsFromComments(
647
652
  comments []prismaCommentRun,
648
653
  hosts map[string]*evidenceUnit,
649
654
  inventories map[string][]*artifactInventory,
650
- ) []string {
651
- problems := []string{}
655
+ levels map[string]rule.Severity,
656
+ ) graphDiagnostics {
657
+ problems := graphDiagnostics{}
652
658
  sequence := 0
653
659
  for _, run := range comments {
660
+ severity := levels[run.Path]
654
661
  location := run.Path + ":" + decimal(run.Line)
655
662
  if run.Form != prismaDocComment {
656
663
  if prismaCommentCarriesTag(run.Body) {
657
- problems = append(
658
- problems,
664
+ problems = problems.add(
665
+ severity,
659
666
  "Evidence tag at "+location+" sits in a '//' line comment, which Prisma discards rather than attaching to the declaration below it. Write the citation on a '///' or '/* */' documentation comment directly above the model, column, or relation it grounds.",
660
667
  )
661
668
  }
@@ -667,8 +674,8 @@ func prismaDeclarationsFromComments(
667
674
  continue
668
675
  }
669
676
  for _, offset := range prismaBuriedTagLines(run.Body) {
670
- problems = append(
671
- problems,
677
+ problems = problems.add(
678
+ severity,
672
679
  "Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
673
680
  )
674
681
  }
@@ -676,8 +683,8 @@ func prismaDeclarationsFromComments(
676
683
  sequence++
677
684
  line := run.Line + parsed.LineOffset
678
685
  if parsed.Tag == tagEvidence {
679
- problems = append(
680
- problems,
686
+ problems = problems.add(
687
+ severity,
681
688
  "@evidence at "+run.Path+":"+decimal(line)+" is on a file-level Prisma exclusion carrier. Move ownership evidence directly above the selected model, column, or relation it grounds; only @evidenceExclude may be unattached at file level.",
682
689
  )
683
690
  continue
@@ -724,8 +731,8 @@ func prismaDeclarationsFromComments(
724
731
  }
725
732
  if run.Key == "" {
726
733
  if prismaCommentCarriesTag(run.Body) {
727
- problems = append(
728
- problems,
734
+ problems = problems.add(
735
+ severity,
729
736
  "Evidence tag at "+location+" documents no declaration. Prisma attaches a '///' comment to the declaration that immediately follows it, so a blank line before a top-level block, a block attribute, or a closing brace leaves the comment documenting nothing. Move the citation directly above the model, column, or relation it grounds.",
730
737
  )
731
738
  }
@@ -734,8 +741,8 @@ func prismaDeclarationsFromComments(
734
741
  host := hosts[run.Key]
735
742
  if host == nil {
736
743
  if prismaCommentCarriesTag(run.Body) {
737
- problems = append(
738
- problems,
744
+ problems = problems.add(
745
+ severity,
739
746
  "Evidence tag at "+location+" documents '"+run.Key+"', which is not a model, column, or relation. A model or a view and their members host an evidence citation; an enum, a composite type, and a datasource or generator setting do not.",
740
747
  )
741
748
  }
@@ -746,8 +753,8 @@ func prismaDeclarationsFromComments(
746
753
  continue
747
754
  }
748
755
  for _, offset := range prismaBuriedTagLines(run.Body) {
749
- problems = append(
750
- problems,
756
+ problems = problems.add(
757
+ severity,
751
758
  "Evidence tag at "+run.Path+":"+decimal(run.Line+offset)+" is buried behind an extra slash. Prisma reads a fourth slash as content, so the tag no longer opens its documentation line and nothing resolves it. Write exactly three slashes.",
752
759
  )
753
760
  }
@@ -6,6 +6,8 @@ import (
6
6
  "os"
7
7
  "strings"
8
8
  "sync"
9
+
10
+ "github.com/samchon/ttsc/packages/lint/rule"
9
11
  )
10
12
 
11
13
  // prismaCacheLimit bounds the cache so a resident host cannot grow without end.
@@ -176,13 +178,14 @@ func prismaUnitsFromOutcome(
176
178
  set prismaSourceSet,
177
179
  inventories map[string]*artifactInventory,
178
180
  outcome prismaSetOutcome,
179
- ) []string {
181
+ config graphConfig,
182
+ ) graphDiagnostics {
180
183
  if outcome.Rejected {
181
- return []string{failPrismaSet(
184
+ return graphDiagnostics{}.add(prismaSetSeverity(config, inventories), failPrismaSet(
182
185
  inventories,
183
186
  set,
184
187
  prismaNormalizationFailure(outcome.Problem),
185
- )}
188
+ ))
186
189
  }
187
190
  locations, comments := locatePrismaDeclarations(root, set.Sources)
188
191
  fallback := ""
@@ -230,7 +233,16 @@ func prismaUnitsFromOutcome(
230
233
  for _, inventory := range inventories {
231
234
  sortUnits(inventory.Units)
232
235
  }
233
- return prismaDeclarationsFromComments(comments, hosts, indexed)
236
+ levels := map[string]rule.Severity{}
237
+ for key, inventory := range inventories {
238
+ levels[inventory.Path] = max(levels[inventory.Path], inventorySeverity(config, artifactPrisma, key, "*"))
239
+ }
240
+ for _, source := range set.Sources {
241
+ for _, spelling := range set.Spellings[source] {
242
+ levels[source] = max(levels[source], levels[spelling])
243
+ }
244
+ }
245
+ return prismaDeclarationsFromComments(comments, hosts, indexed, levels)
234
246
  }
235
247
 
236
248
  // joinPrismaIdentity renders a unit's identity the way the locator keys one.
@@ -0,0 +1,156 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "sort"
6
+
7
+ "github.com/samchon/ttsc/packages/lint/rule"
8
+ )
9
+
10
+ func decodeGraphSeverity(raw json.RawMessage, path string) (*rule.Severity, []string) {
11
+ if len(raw) == 0 {
12
+ return nil, nil
13
+ }
14
+ var severity rule.Severity
15
+ var value any
16
+ if err := json.Unmarshal(raw, &value); err != nil {
17
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": expected a lint severity value."}
18
+ }
19
+ switch value {
20
+ case "off", float64(0):
21
+ severity = rule.SeverityOff
22
+ case "warning", "warn", float64(1):
23
+ severity = rule.SeverityWarn
24
+ case "error", float64(2):
25
+ severity = rule.SeverityError
26
+ default:
27
+ return nil, []string{"Invalid evidence/graph configuration at " + path + ": expected 'off', 'warning', 'warn', 'error', 0, 1, or 2; omit severity or use undefined to inherit the enclosing level."}
28
+ }
29
+ return &severity, nil
30
+ }
31
+
32
+ // Resolve inheritance once, before populations are loaded. A nil severity is
33
+ // unset; an explicit zero disables its population.
34
+ func resolveGraphSeverities(config *graphConfig, severity rule.Severity) {
35
+ for i := range config.Claims {
36
+ claim := &config.Claims[i]
37
+ claim.Level = severity
38
+ if claim.Severity != nil {
39
+ claim.Level = *claim.Severity
40
+ }
41
+ for j := range claim.References {
42
+ reference := &claim.References[j]
43
+ reference.Level = claim.Level
44
+ if reference.Severity != nil {
45
+ reference.Level = *reference.Severity
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ type graphDiagnostic struct {
52
+ Message string
53
+ Severity rule.Severity
54
+ }
55
+
56
+ type graphDiagnostics []graphDiagnostic
57
+
58
+ func (problems graphDiagnostics) add(severity rule.Severity, messages ...string) graphDiagnostics {
59
+ for _, message := range messages {
60
+ problems = append(problems, graphDiagnostic{Message: message, Severity: severity})
61
+ }
62
+ return problems
63
+ }
64
+
65
+ func (problems graphDiagnostics) report(ctx *rule.ProjectContext) {
66
+ levels := map[string]rule.Severity{}
67
+ for _, problem := range problems {
68
+ levels[problem.Message] = max(levels[problem.Message], problem.Severity)
69
+ }
70
+ messages := make([]string, 0, len(levels))
71
+ for message := range levels {
72
+ messages = append(messages, message)
73
+ }
74
+ sort.Strings(messages)
75
+ for _, message := range messages {
76
+ ctx.ReportSeverity(levels[message], message)
77
+ }
78
+ }
79
+
80
+ // A shared source problem belongs to every population that reads it. Keep the
81
+ // strongest owning level without pooling independent coverage obligations.
82
+ func populationSeverity(config graphConfig, kind artifactKind, base populationBase, path string, symbol string, descendants bool) rule.Severity {
83
+ var severity rule.Severity
84
+ matches := func(candidate populationBase, files globSet, symbols symbolSet) bool {
85
+ return candidate.Absolute == base.Absolute &&
86
+ (path == "" || files.matches(path) || descendants && files.couldMatchDescendant(path)) &&
87
+ (symbol == "*" || symbols.contains(symbol))
88
+ }
89
+ for _, claim := range config.Claims {
90
+ if claim.Type == kind && matches(claim.Base, claim.Files, claim.Symbols) {
91
+ severity = max(severity, claim.Level)
92
+ }
93
+ for _, reference := range claim.References {
94
+ if reference.Type == kind && reference.Package == "" && matches(reference.Base, reference.Files, reference.Symbols) {
95
+ severity = max(severity, reference.Level)
96
+ }
97
+ }
98
+ }
99
+ return severity
100
+ }
101
+
102
+ func artifactSeverity(config graphConfig, kind artifactKind) rule.Severity {
103
+ var severity rule.Severity
104
+ for _, claim := range config.Claims {
105
+ if claim.Type == kind {
106
+ severity = max(severity, claim.Level)
107
+ }
108
+ for _, reference := range claim.References {
109
+ if reference.Type == kind {
110
+ severity = max(severity, reference.Level)
111
+ }
112
+ }
113
+ }
114
+ return severity
115
+ }
116
+
117
+ func swaggerSeverity(config graphConfig, source string) rule.Severity {
118
+ var severity rule.Severity
119
+ for _, claim := range config.Claims {
120
+ for _, reference := range claim.References {
121
+ if reference.Type == artifactSwagger && reference.Source == source {
122
+ severity = max(severity, reference.Level)
123
+ }
124
+ }
125
+ }
126
+ return severity
127
+ }
128
+
129
+ func ownerSeverity(owners []claimState) rule.Severity {
130
+ var severity rule.Severity
131
+ for _, owner := range owners {
132
+ severity = max(severity, owner.Spec.Level)
133
+ }
134
+ return severity
135
+ }
136
+
137
+ func inventorySeverity(config graphConfig, kind artifactKind, address string, symbol string) rule.Severity {
138
+ var severity rule.Severity
139
+ for _, base := range configuredBases(config, kind) {
140
+ if relative, ok := base.relativeOf(address); ok {
141
+ severity = max(severity, populationSeverity(config, kind, base, relative, symbol, false))
142
+ }
143
+ }
144
+ return severity
145
+ }
146
+
147
+ // Empty or failed populations contribute no file to the shared Prisma parse.
148
+ func prismaSetSeverity(config graphConfig, inventories map[string]*artifactInventory) rule.Severity {
149
+ var severity rule.Severity
150
+ for address, inventory := range inventories {
151
+ if inventory != nil && inventory.FailureBase == "" {
152
+ severity = max(severity, inventorySeverity(config, artifactPrisma, address, "*"))
153
+ }
154
+ }
155
+ return severity
156
+ }
package/native/swagger.go CHANGED
@@ -11,6 +11,8 @@ import (
11
11
  "sort"
12
12
  "strings"
13
13
  "time"
14
+
15
+ "github.com/samchon/ttsc/packages/lint/rule"
14
16
  )
15
17
 
16
18
  const swaggerBridgeTimeout = 60 * time.Second
@@ -94,7 +96,7 @@ type swaggerOperation struct {
94
96
  func loadSwaggerInventories(
95
97
  root string,
96
98
  config graphConfig,
97
- ) (map[string]*artifactInventory, []string) {
99
+ ) (map[string]*artifactInventory, graphDiagnostics) {
98
100
  sources := configuredSwaggerSources(config)
99
101
  inventories := map[string]*artifactInventory{}
100
102
  for _, source := range sources {
@@ -114,15 +116,16 @@ func loadSwaggerInventories(
114
116
  // TypeScript keystroke that triggers a rebuild.
115
117
  digests := swaggerContentDigests(root, sources)
116
118
  pending := []string{}
117
- problems := []string{}
119
+ problems := graphDiagnostics{}
120
+ severity := artifactSeverity(config, artifactSwagger)
118
121
  for _, source := range sources {
119
122
  outcome, hit := lookupSwaggerDocument(source, digests[source])
120
123
  if !hit {
121
124
  pending = append(pending, source)
122
125
  continue
123
126
  }
124
- problems = append(
125
- problems,
127
+ problems = problems.add(
128
+ swaggerSeverity(config, source),
126
129
  swaggerUnitsFromOutcome(source, inventories[source], outcome)...,
127
130
  )
128
131
  }
@@ -140,30 +143,34 @@ func loadSwaggerInventories(
140
143
  inventoryProblem{Symbol: "operation", Message: message},
141
144
  )
142
145
  }
143
- return inventories, append(problems, message)
146
+ var pendingSeverity rule.Severity
147
+ for _, source := range pending {
148
+ pendingSeverity = max(pendingSeverity, swaggerSeverity(config, source))
149
+ }
150
+ return inventories, problems.add(pendingSeverity, message)
144
151
  }
145
152
 
146
153
  seen := map[string]bool{}
147
154
  for _, document := range result.Documents {
148
155
  inventory := inventories[document.Source]
149
156
  if inventory == nil {
150
- problems = append(
151
- problems,
157
+ problems = problems.add(
158
+ severity,
152
159
  "Evidence graph Swagger normalizer returned an unconfigured source '"+displaySwaggerSource(document.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
153
160
  )
154
161
  continue
155
162
  }
156
163
  if seen[document.Source] {
157
- problems = append(
158
- problems,
164
+ problems = problems.add(
165
+ swaggerSeverity(config, document.Source),
159
166
  "Evidence graph Swagger normalizer returned source '"+displaySwaggerSource(document.Source)+"' more than once. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
160
167
  )
161
168
  continue
162
169
  }
163
170
  seen[document.Source] = true
164
171
  outcome := swaggerDocumentOutcome{Operations: document.Operations}
165
- problems = append(
166
- problems,
172
+ problems = problems.add(
173
+ swaggerSeverity(config, document.Source),
167
174
  swaggerUnitsFromOutcome(document.Source, inventory, outcome)...,
168
175
  )
169
176
  rememberSwaggerDocument(document.Source, document.Digest, outcome)
@@ -171,8 +178,8 @@ func loadSwaggerInventories(
171
178
  for _, problem := range result.Problems {
172
179
  inventory := inventories[problem.Source]
173
180
  if inventory == nil {
174
- problems = append(
175
- problems,
181
+ problems = problems.add(
182
+ severity,
176
183
  "Evidence graph Swagger normalizer rejected an unconfigured source '"+displaySwaggerSource(problem.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
177
184
  )
178
185
  continue
@@ -182,8 +189,8 @@ func loadSwaggerInventories(
182
189
  Rejected: true,
183
190
  Problem: problem.Message,
184
191
  }
185
- problems = append(
186
- problems,
192
+ problems = problems.add(
193
+ swaggerSeverity(config, problem.Source),
187
194
  swaggerUnitsFromOutcome(problem.Source, inventory, outcome)...,
188
195
  )
189
196
  rememberSwaggerDocument(problem.Source, problem.Digest, outcome)
@@ -198,7 +205,7 @@ func loadSwaggerInventories(
198
205
  inventories[source].Problems,
199
206
  inventoryProblem{Symbol: "operation", Message: message},
200
207
  )
201
- problems = append(problems, message)
208
+ problems = problems.add(swaggerSeverity(config, source), message)
202
209
  }
203
210
  return inventories, problems
204
211
  }
@@ -79,8 +79,8 @@ func extendTypeScriptInventories(
79
79
  func typeScriptBaseProblems(
80
80
  config graphConfig,
81
81
  inventories map[string]*artifactInventory,
82
- ) []string {
83
- problems := []string{}
82
+ ) graphDiagnostics {
83
+ problems := graphDiagnostics{}
84
84
  for _, base := range configuredBases(config, artifactTypeScript) {
85
85
  problem := baseDirectoryProblem(base, artifactTypeScript)
86
86
  // The stat accepts a chain of links the resolver will not finish, and this
@@ -111,7 +111,7 @@ func typeScriptBaseProblems(
111
111
  continue
112
112
  }
113
113
  recordPopulationFailure(inventories, artifactTypeScript, base)
114
- problems = append(problems, problem)
114
+ problems = problems.add(populationSeverity(config, artifactTypeScript, base, "", "*", false), problem)
115
115
  }
116
116
  return problems
117
117
  }
@@ -1,7 +1,6 @@
1
1
  package evidence
2
2
 
3
3
  import (
4
- "sort"
5
4
  "strings"
6
5
 
7
6
  shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -142,14 +141,14 @@ func readableCommentBody(comment string) string {
142
141
  func unreadableTypeScriptTags(
143
142
  inventories map[string]*artifactInventory,
144
143
  governed map[string]bool,
145
- ) []string {
146
- reported := []string{}
144
+ config graphConfig,
145
+ ) graphDiagnostics {
146
+ reported := graphDiagnostics{}
147
147
  for address, inventory := range inventories {
148
- if inventory == nil || !governed[address] {
148
+ if inventory == nil || len(inventory.Unreadable) == 0 || !governed[address] {
149
149
  continue
150
150
  }
151
- reported = append(reported, inventory.Unreadable...)
151
+ reported = reported.add(inventorySeverity(config, artifactTypeScript, address, "*"), inventory.Unreadable...)
152
152
  }
153
- sort.Strings(reported)
154
153
  return reported
155
154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/evidence",
3
- "version": "0.28.6",
3
+ "version": "0.29.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": {
@@ -42,14 +42,14 @@
42
42
  "url": "https://github.com/samchon/ttsc/issues"
43
43
  },
44
44
  "peerDependencies": {
45
- "@ttsc/lint": ">=0.24.0"
45
+ "@ttsc/lint": ">0.28.6"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.3.0",
49
49
  "rimraf": "^6.1.2",
50
50
  "typescript": "^7.0.2",
51
- "@ttsc/lint": "0.28.6",
52
- "ttsc": "0.28.6"
51
+ "ttsc": "0.29.0",
52
+ "@ttsc/lint": "0.29.0"
53
53
  },
54
54
  "dependencies": {
55
55
  "@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",