@ttsc/evidence 0.28.6 → 0.30.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/native/loader.go CHANGED
@@ -22,12 +22,17 @@ import (
22
22
  // that symbol is precisely the one an obligation needs to name — an evidence
23
23
  // graph exists to report the operation the frontend never called.
24
24
  type typeScriptLoader struct {
25
- root string
26
- program map[string]*artifactInventory
27
- parsed map[string]*artifactInventory
28
- resolved map[string]string
29
- failures map[string]string
30
- installs map[string]installedPackageLocation
25
+ boundary *populationBase
26
+ root string
27
+ identityRoot string
28
+ program map[string]*artifactInventory
29
+ programIDs map[string]*artifactInventory
30
+ parsed map[string]*artifactInventory
31
+ resolved map[string]string
32
+ failures map[string]string
33
+ parseFailures map[string]string
34
+ identities map[string]typeScriptModuleIdentity
35
+ installs map[string]installedPackageLocation
31
36
  }
32
37
 
33
38
  func newTypeScriptLoader(
@@ -35,23 +40,40 @@ func newTypeScriptLoader(
35
40
  program map[string]*artifactInventory,
36
41
  ) *typeScriptLoader {
37
42
  loader := &typeScriptLoader{
38
- root: strings.ReplaceAll(root, "\\", "/"),
39
- program: map[string]*artifactInventory{},
40
- parsed: map[string]*artifactInventory{},
41
- resolved: map[string]string{},
42
- failures: map[string]string{},
43
- installs: map[string]installedPackageLocation{},
44
- }
45
- for _, inventory := range program {
43
+ root: strings.ReplaceAll(root, "\\", "/"),
44
+ program: map[string]*artifactInventory{},
45
+ programIDs: map[string]*artifactInventory{},
46
+ parsed: map[string]*artifactInventory{},
47
+ resolved: map[string]string{},
48
+ failures: map[string]string{},
49
+ parseFailures: map[string]string{},
50
+ identities: map[string]typeScriptModuleIdentity{},
51
+ installs: map[string]installedPackageLocation{},
52
+ }
53
+ loader.identityRoot, _ = physicalTypeScriptPath(filepath.FromSlash(loader.root))
54
+ keys := make([]string, 0, len(program))
55
+ for key := range program {
56
+ keys = append(keys, key)
57
+ }
58
+ sort.Strings(keys)
59
+ for _, key := range keys {
60
+ inventory := program[key]
46
61
  if inventory == nil || inventory.Path == "" {
47
62
  continue
48
63
  }
49
64
  location := loader.projectPath(inventory.Path)
65
+ identity, _ := loader.moduleIdentity(location)
66
+ if inventory.Address != identity && inventory.Source != nil {
67
+ inventory = typeScriptInventories.scan(typeScriptModuleAddress(location, identity), inventory.Source)
68
+ }
50
69
  current := loader.program[location]
51
70
  if current == nil ||
52
71
  current.Address != current.Path && inventory.Address == inventory.Path {
53
72
  loader.program[location] = inventory
54
73
  }
74
+ if loader.programIDs[identity] == nil || location == identity {
75
+ loader.programIDs[identity] = inventory
76
+ }
55
77
  }
56
78
  return loader
57
79
  }
@@ -60,24 +82,41 @@ func newTypeScriptLoader(
60
82
  //
61
83
  // The Program's copy wins when it exists so that a file under edit is read as
62
84
  // the editor has it, not as the disk last saw it.
63
- func (loader *typeScriptLoader) inventory(relative string) *artifactInventory {
85
+ func (loader *typeScriptLoader) inventory(relative string) (result *artifactInventory) {
64
86
  if relative == "" {
65
87
  return nil
66
88
  }
67
89
  relative = loader.projectPath(relative)
68
- if inventory := loader.program[relative]; inventory != nil {
90
+ if !loader.withinBoundary(relative) {
91
+ loader.failures[relative] = "the re-export leaves the explicitly configured root"
92
+ return nil
93
+ }
94
+ defer func() {
95
+ if loader.boundary != nil && result != nil && result.Source != nil && len(result.Source.Diagnostics()) != 0 {
96
+ diagnostic := result.Source.Diagnostics()[0]
97
+ loader.failures[relative] = "TypeScript syntax error TS" + decimal(int(diagnostic.Code())) + " at line " + decimal(lineAt(result.Source.Text(), diagnostic.Pos()))
98
+ result = nil
99
+ }
100
+ }()
101
+ if inventory := loader.programInventory(relative); inventory != nil {
69
102
  return inventory
70
103
  }
71
104
  if inventory, cached := loader.parsed[relative]; cached {
105
+ if inventory == nil && loader.parseFailures[relative] != "" {
106
+ loader.failures[relative] = loader.parseFailures[relative]
107
+ }
72
108
  return inventory
73
109
  }
74
110
  loader.parsed[relative] = loader.parse(relative)
111
+ if loader.parsed[relative] == nil {
112
+ loader.parseFailures[relative] = loader.failures[relative]
113
+ }
75
114
  return loader.parsed[relative]
76
115
  }
77
116
 
78
117
  func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
79
118
  relative = loader.projectPath(relative)
80
- content, err := os.ReadFile(path.Join(loader.root, relative))
119
+ content, err := os.ReadFile(resolveProjectPath(loader.root, relative))
81
120
  if err != nil {
82
121
  loader.failures[relative] = err.Error()
83
122
  return nil
@@ -88,7 +127,7 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
88
127
  }
89
128
  file := shimparser.ParseSourceFile(
90
129
  shimast.SourceFileParseOptions{
91
- FileName: path.Join(loader.root, relative),
130
+ FileName: filepath.ToSlash(resolveProjectPath(loader.root, relative)),
92
131
  },
93
132
  string(content),
94
133
  kind,
@@ -97,7 +136,8 @@ func (loader *typeScriptLoader) parse(relative string) *artifactInventory {
97
136
  loader.failures[relative] = "the TypeScript parser returned no source file"
98
137
  return nil
99
138
  }
100
- return scanTypeScriptInventory(relative, file)
139
+ identity, _ := loader.moduleIdentity(relative)
140
+ return scanTypeScriptInventoryAt(typeScriptModuleAddress(relative, identity), file)
101
141
  }
102
142
 
103
143
  func (loader *typeScriptLoader) failure(relative string) string {
@@ -111,19 +151,34 @@ func (loader *typeScriptLoader) failure(relative string) string {
111
151
  // without paying to scan it.
112
152
  func (loader *typeScriptLoader) exists(relative string) bool {
113
153
  relative = loader.projectPath(relative)
114
- if loader.program[relative] != nil {
154
+ if loader.programInventory(relative) != nil {
115
155
  return true
116
156
  }
117
157
  return loader.existsOnDisk(relative)
118
158
  }
119
159
 
160
+ // Authored module paths and physical identity keys are separate namespaces.
161
+ // Mixing them can mistake a sibling of a linked root for a different sibling
162
+ // of the physical project. Identity lookup still recovers its editor snapshot.
163
+ func (loader *typeScriptLoader) programInventory(module string) *artifactInventory {
164
+ if inventory := loader.program[module]; inventory != nil {
165
+ return inventory
166
+ }
167
+ if len(loader.programIDs) != 0 {
168
+ if identity, ok := loader.moduleIdentity(module); ok {
169
+ return loader.programIDs[identity]
170
+ }
171
+ }
172
+ return nil
173
+ }
174
+
120
175
  // existsOnDisk probes the filesystem for an already-normalized path.
121
176
  //
122
177
  // The probe is the expensive half of module resolution — one syscall per
123
178
  // candidate, and a watch cycle resolves every re-export in the population — so
124
179
  // callers that can answer from the Program should do that first.
125
180
  func (loader *typeScriptLoader) existsOnDisk(relative string) bool {
126
- info, err := os.Stat(path.Join(loader.root, relative))
181
+ info, err := os.Stat(resolveProjectPath(loader.root, relative))
127
182
  return err == nil && !info.IsDir()
128
183
  }
129
184
 
@@ -135,6 +190,9 @@ func (loader *typeScriptLoader) resolve(from string, specifier string) string {
135
190
  return cached
136
191
  }
137
192
  loader.resolved[key] = loader.resolveUncached(from, specifier)
193
+ if loader.boundary != nil && loader.resolved[key] == "" && loader.failures[from+" -> "+specifier] == "" {
194
+ loader.failures[from+" -> "+specifier] = "the exported module cannot be resolved"
195
+ }
138
196
  return loader.resolved[key]
139
197
  }
140
198
 
@@ -142,12 +200,19 @@ func (loader *typeScriptLoader) resolveUncached(
142
200
  from string,
143
201
  specifier string,
144
202
  ) string {
203
+ failure := from + " -> " + specifier
145
204
  if strings.HasPrefix(specifier, "./") || strings.HasPrefix(specifier, "../") {
205
+ if loader.boundary != nil {
206
+ if canonical, ok := loader.moduleIdentity(from); ok {
207
+ from = filepath.ToSlash(resolveProjectPath(loader.identityRoot, canonical))
208
+ }
209
+ }
146
210
  base := path.Clean(path.Join(path.Dir(from), specifier))
147
211
  candidates := moduleCandidates(base)
148
212
  normalized := make([]string, 0, len(candidates))
149
213
  for _, candidate := range candidates {
150
- normalized = append(normalized, loader.projectPath(candidate))
214
+ module := loader.projectPath(candidate)
215
+ normalized = append(normalized, module)
151
216
  }
152
217
  // The Program answers first, and not only because it answers without a
153
218
  // syscall. A project that emits beside its sources has both `x.js` and
@@ -155,12 +220,20 @@ func (loader *typeScriptLoader) resolveUncached(
155
220
  // resolving an import to emitted JavaScript would read a module whose
156
221
  // declarations the graph cannot address.
157
222
  for _, candidate := range normalized {
158
- if loader.program[candidate] != nil {
223
+ if loader.programInventory(candidate) != nil {
224
+ if !loader.withinBoundary(candidate) {
225
+ loader.failures[failure] = "the re-export leaves the explicitly configured root"
226
+ return ""
227
+ }
159
228
  return candidate
160
229
  }
161
230
  }
162
231
  for _, candidate := range normalized {
163
232
  if loader.existsOnDisk(candidate) {
233
+ if !loader.withinBoundary(candidate) {
234
+ loader.failures[failure] = "the re-export leaves the explicitly configured root"
235
+ return ""
236
+ }
164
237
  return candidate
165
238
  }
166
239
  }
@@ -169,9 +242,83 @@ func (loader *typeScriptLoader) resolveUncached(
169
242
  if strings.HasPrefix(specifier, "/") {
170
243
  return ""
171
244
  }
245
+ if loader.boundary != nil {
246
+ loader.failures[failure] = "a package re-export is outside the explicitly configured root"
247
+ return ""
248
+ }
172
249
  return loader.resolvePackage(specifier)
173
250
  }
174
251
 
252
+ type typeScriptModuleIdentity struct {
253
+ Path string
254
+ Resolved bool
255
+ }
256
+
257
+ // Module identity follows directory links and file symlinks, while displayed
258
+ // paths and published module addresses retain their authored spellings.
259
+ // Cache once per evaluation so rooted populations share this filesystem work.
260
+ func (loader *typeScriptLoader) moduleIdentity(module string) (string, bool) {
261
+ module = loader.projectPath(module)
262
+ if cached, exists := loader.identities[module]; exists {
263
+ return cached.Path, cached.Resolved
264
+ }
265
+ absolute := resolveProjectPath(loader.root, module)
266
+ resolved, ok := physicalTypeScriptPath(absolute)
267
+ // Resolve both sides before taking a relative identity. Canonicalizing only
268
+ // the file makes a project junction or Windows 8.3 root leak the checkout's
269
+ // absolute location into every unit ID and invalidate unchanged reviews.
270
+ identity := projectRelativeDisplay(loader.identityRoot, resolved)
271
+ loader.identities[module] = typeScriptModuleIdentity{Path: identity, Resolved: ok}
272
+ return identity, ok
273
+ }
274
+
275
+ func physicalTypeScriptPath(absolute string) (string, bool) {
276
+ current := filepath.Clean(absolute)
277
+ // A directory link can introduce another link in an already-walked parent.
278
+ // Settle the complete path, with the same finite bound as directory chains.
279
+ for range 32 {
280
+ resolved, ok := resolveLinkedPath(current)
281
+ if !ok {
282
+ return resolved, false
283
+ }
284
+ resolved = expandTypeScriptPath(resolved)
285
+ if resolved == current {
286
+ return resolved, true
287
+ }
288
+ current = resolved
289
+ }
290
+ return current, false
291
+ }
292
+
293
+ // An unsaved or deleted file still has an identity. Expand the deepest existing
294
+ // prefix so Windows short names do not reappear when EvalSymlinks cannot read
295
+ // the complete path, then restore the missing suffix without changing its case.
296
+ func expandTypeScriptPath(absolute string) string {
297
+ probe := absolute
298
+ suffix := []string{}
299
+ for {
300
+ if final, err := filepath.EvalSymlinks(probe); err == nil {
301
+ if !strings.EqualFold(final, probe) {
302
+ probe = final
303
+ }
304
+ for index := len(suffix) - 1; index >= 0; index-- {
305
+ probe = filepath.Join(probe, suffix[index])
306
+ }
307
+ return probe
308
+ }
309
+ parent := filepath.Dir(probe)
310
+ if parent == probe {
311
+ return absolute
312
+ }
313
+ suffix = append(suffix, filepath.Base(probe))
314
+ probe = parent
315
+ }
316
+ }
317
+
318
+ func typeScriptModuleAddress(display string, identity string) artifactAddress {
319
+ return artifactAddress{Base: populationBase{Default: true}, Relative: identity, Display: display, Key: identity}
320
+ }
321
+
175
322
  // projectPath gives every Program source and module candidate one identity
176
323
  // relative to the ttsc project. A rooted claim may name the same physical file
177
324
  // through `../api`, while an import can arrive there through a different
@@ -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,10 +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
90
+ Severity *rule.Severity
91
+ Level rule.Severity
92
+ Index int
93
+ Type artifactKind
94
+ Policy referencePolicy
95
+ Root string
96
+ // Rooted preserves explicit disk-loading opt-in even when '.' normalizes
97
+ // to the empty spelling of the project root.
98
+ Rooted bool
91
99
  Base populationBase
92
100
  Files globSet
93
101
  Source string
@@ -328,6 +336,8 @@ func (declaration *evidenceDeclaration) valid() bool {
328
336
  }
329
337
 
330
338
  type artifactInventory struct {
339
+ // Source supplies declaration diagnostics and the active editor snapshot.
340
+ Source *shimast.SourceFile
331
341
  // Address is the population-relative identity used for units and
332
342
  // declarations. It differs from Path when a configured root moves the
333
343
  // address space while a diagnostic keeps naming the file the way a reader
@@ -444,6 +454,8 @@ type claimState struct {
444
454
  }
445
455
 
446
456
  type referenceState struct {
457
+ // Code resolves finite accessor paths independently of population traversal.
458
+ Code *typeScriptExportResolver
447
459
  Spec referenceSpec
448
460
  Paths []string
449
461
  Units []*evidenceUnit
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.