@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/native/prisma.go CHANGED
@@ -146,12 +146,13 @@ func loadPrismaInventories(
146
146
  Type: artifactPrisma,
147
147
  }
148
148
  }
149
- // The set is composed of physical files, deduplicated across bases. Prisma
150
- // parses one schema at a time and a file listed twice is a duplicate
151
- // declaration to its own parser, so two populations that reach one file
152
- // through different roots must still contribute it once.
153
- sources := distinctPrismaSources(addresses)
154
- if len(sources) == 0 {
149
+ // The set is composed of physical files, deduplicated across bases by what
150
+ // each address is on disk. Prisma parses one schema at a time and a file
151
+ // listed twice is a duplicate declaration to its own parser, so two
152
+ // populations that reach one file through different roots contribute it
153
+ // once and are both served by that one parse.
154
+ set := distinctPrismaSources(root, addresses)
155
+ if len(set.Sources) == 0 {
155
156
  return inventories, problems
156
157
  }
157
158
 
@@ -159,27 +160,27 @@ func loadPrismaInventories(
159
160
  // ten-model schema and a two-hundred-model one pay nearly the same. A
160
161
  // resident host repeats this every cycle, so an unchanged schema would
161
162
  // otherwise be re-parsed on every TypeScript keystroke that rebuilds.
162
- digest := prismaContentDigest(root, sources)
163
+ digest := prismaContentDigest(root, set.Sources)
163
164
  if outcome, hit := prismaSchemas.lookup(digest); hit {
164
165
  return inventories, append(
165
166
  problems,
166
- prismaUnitsFromOutcome(root, sources, inventories, outcome)...,
167
+ prismaUnitsFromOutcome(root, set, inventories, outcome)...,
167
168
  )
168
169
  }
169
170
 
170
- result, err := normalizePrismaSet(root, sources)
171
+ result, err := normalizePrismaSet(root, set.Sources)
171
172
  if err != nil {
172
- message := "Evidence graph could not run its Prisma schema loader: " + err.Error() + ". Prisma references require Node.js and a resolvable @prisma/prisma-schema-wasm."
173
- return inventories, append(problems, failPrismaSet(inventories, sources, message))
173
+ 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))
174
175
  }
175
176
 
176
177
  outcome, problem := prismaOutcomeOf(result)
177
178
  if problem != "" {
178
- return inventories, append(problems, failPrismaSet(inventories, sources, problem))
179
+ return inventories, append(problems, failPrismaSet(inventories, set, problem))
179
180
  }
180
181
  problems = append(
181
182
  problems,
182
- prismaUnitsFromOutcome(root, sources, inventories, outcome)...,
183
+ prismaUnitsFromOutcome(root, set, inventories, outcome)...,
183
184
  )
184
185
  rememberPrismaSchema(outcome.digest, outcome)
185
186
  return inventories, problems
@@ -234,42 +235,58 @@ func configuredPrismaAddressesWithHealth(
234
235
  failedBases := []populationBase{}
235
236
  problems := []string{}
236
237
  for _, base := range configuredBases(config, artifactPrisma) {
237
- if problem := unreadableBaseProblem(base, artifactPrisma); problem != "" {
238
+ if problem := baseDirectoryProblem(base, artifactPrisma); problem != "" {
238
239
  problems = append(problems, problem)
239
240
  failedBases = append(failedBases, base)
240
241
  continue
241
242
  }
242
243
  baseFailed := false
243
- err := filepath.WalkDir(base.Absolute, func(current string, entry fs.DirEntry, walkErr error) error {
244
+ from, resolved := resolvedBaseDirectory(base)
245
+ if !resolved {
246
+ problems = append(problems, unresolvedBaseProblem(base, artifactPrisma))
247
+ failedBases = append(failedBases, base)
248
+ continue
249
+ }
250
+ err := filepath.WalkDir(from, func(current string, entry fs.DirEntry, walkErr error) error {
244
251
  if walkErr != nil {
245
- relative, ok := relativeProjectPath(base.Absolute, current)
246
- relevant := ok &&
247
- (matchesConfiguredPrismaFile(config, base, relative) ||
248
- couldContainConfiguredPrisma(config, base, relative))
249
- if !relevant {
250
- if entry != nil && entry.IsDir() {
251
- return filepath.SkipDir
252
- }
253
- return nil
252
+ // The walk root belongs to its population by construction, so a
253
+ // failure to list it is a failure of the population. The relevance test
254
+ // below is written for an entry inside the base and reached the base
255
+ // itself only by accident, on a decision the glob shape made;
256
+ // `loadMarkdownBase` records that history in full. Leaving this
257
+ // exemption in one walker would decide an identical filesystem state by
258
+ // artifact kind.
259
+ if current == from {
260
+ return walkErr
254
261
  }
255
- baseFailed = true
256
- problems = append(problems, "Evidence graph could not inspect '"+current+"': "+walkErr.Error()+". Fix filesystem access so configured Prisma sources can be indexed.")
257
- if entry != nil && entry.IsDir() {
258
- return filepath.SkipDir
262
+ problem, relevant := unreadableEntryProblem(
263
+ base,
264
+ from,
265
+ "Prisma",
266
+ current,
267
+ walkErr,
268
+ func(relative string) bool {
269
+ return matchesConfiguredPrismaFile(config, base, relative) ||
270
+ couldContainConfiguredPrisma(config, base, relative)
271
+ },
272
+ )
273
+ if relevant {
274
+ baseFailed = true
275
+ problems = append(problems, problem)
259
276
  }
260
- return nil
277
+ return filepath.SkipDir
261
278
  }
262
279
  if entry.IsDir() {
263
- if current == base.Absolute {
280
+ if current == from {
264
281
  return nil
265
282
  }
266
- relative, ok := relativeProjectPath(base.Absolute, current)
283
+ relative, ok := relativeProjectPath(from, current)
267
284
  if !ok || !couldContainConfiguredPrisma(config, base, relative) {
268
285
  return filepath.SkipDir
269
286
  }
270
287
  return nil
271
288
  }
272
- relative, ok := relativeProjectPath(base.Absolute, current)
289
+ relative, ok := relativeProjectPath(from, current)
273
290
  if !ok || !matchesConfiguredPrismaFile(config, base, relative) {
274
291
  return nil
275
292
  }
@@ -278,7 +295,7 @@ func configuredPrismaAddressesWithHealth(
278
295
  })
279
296
  if err != nil {
280
297
  baseFailed = true
281
- problems = append(problems, "Evidence graph could not walk Prisma root '"+populationRootLabel(base)+"': "+err.Error()+".")
298
+ problems = append(problems, unlistableBaseProblem(base, "Prisma", err))
282
299
  }
283
300
  if baseFailed {
284
301
  failedBases = append(failedBases, base)
@@ -290,30 +307,133 @@ func configuredPrismaAddressesWithHealth(
290
307
  return addresses, failedBases, problems
291
308
  }
292
309
 
310
+ // prismaSourceSet is the parser's input: one spelling per physical schema file,
311
+ // beside every spelling the configuration reached that file by.
312
+ //
313
+ // Prisma parses a set, and a file handed to it twice is a duplicate declaration
314
+ // to its own parser — so the set has to be one entry per file on disk, not one
315
+ // per configured address. The spellings are kept because everything the parse
316
+ // produces is addressed by the entry that was sent, while the inventories
317
+ // waiting for it are addressed by whichever root each population reached the
318
+ // file through.
319
+ type prismaSourceSet struct {
320
+ // Sources is the ordered set handed to the parser, the digest, and the
321
+ // locator, one entry per physical file.
322
+ Sources []string
323
+ // Spellings maps each entry of Sources to every address display naming the
324
+ // same file, that entry included.
325
+ Spellings map[string][]string
326
+ }
327
+
293
328
  // distinctPrismaSources reduces the configured addresses to the physical files
294
329
  // the parser is handed, in one stable order.
295
- func distinctPrismaSources(addresses []artifactAddress) []string {
330
+ //
331
+ // Identity is the file's own, taken from the filesystem rather than from the
332
+ // path that named it. Two populations reach one schema through different roots
333
+ // precisely when their spellings differ — a package rooted at its installed
334
+ // location and at its source checkout is the layout a package manager produces
335
+ // — so a spelling-keyed set sends that file to the parser twice and the parser
336
+ // rejects the whole set for a model declared twice. `os.SameFile` answers for
337
+ // the link, the junction, the hard link, and the case-insensitive volume
338
+ // alike, none of which a comparison of two strings can see.
339
+ //
340
+ // A file that cannot be stat'ed stands alone under its own spelling, which is
341
+ // the behavior every set had before identity was physical: the digest declines
342
+ // it and the loader reports it unreadable at its own path.
343
+ //
344
+ // The cost is one `os.Stat` per distinct configured address, and on Windows one
345
+ // file open beside it for each address that reaches a comparison, because
346
+ // `os.SameFile` loads the volume and file index there through `CreateFile`
347
+ // rather than from the stat — once per result, since the id is cached on it.
348
+ // Against that, the digest now reads one file per physical file rather than one
349
+ // per address, so in the very layout this exists for the reads go down as the
350
+ // stats go up. Both are per load of this artifact kind, of which a pass
351
+ // performs two — the claim-side populations and then the whole configuration —
352
+ // over a set an adopter counts in files.
353
+ func distinctPrismaSources(root string, addresses []artifactAddress) prismaSourceSet {
354
+ type physicalSchema struct {
355
+ identity os.FileInfo
356
+ spellings []string
357
+ }
358
+ files := []*physicalSchema{}
296
359
  seen := map[string]bool{}
297
- sources := []string{}
298
360
  for _, address := range addresses {
299
361
  if seen[address.Display] {
300
362
  continue
301
363
  }
302
364
  seen[address.Display] = true
303
- sources = append(sources, address.Display)
365
+ identity, err := os.Stat(resolveProjectPath(root, address.Display))
366
+ var host *physicalSchema
367
+ if err == nil {
368
+ for _, file := range files {
369
+ if file.identity != nil && os.SameFile(file.identity, identity) {
370
+ host = file
371
+ break
372
+ }
373
+ }
374
+ }
375
+ if host == nil {
376
+ host = &physicalSchema{}
377
+ if err == nil {
378
+ host.identity = identity
379
+ }
380
+ files = append(files, host)
381
+ }
382
+ host.spellings = append(host.spellings, address.Display)
383
+ }
384
+ set := prismaSourceSet{
385
+ Sources: []string{},
386
+ Spellings: map[string][]string{},
387
+ }
388
+ for _, file := range files {
389
+ // The set's own order is sorted below, but which spelling represents a
390
+ // file is what every unit, declaration, and diagnostic of it will be
391
+ // addressed by. Taking the smallest makes that choice a property of the
392
+ // configuration rather than of the order a walk happened to produce.
393
+ sort.Strings(file.spellings)
394
+ set.Sources = append(set.Sources, file.spellings[0])
395
+ set.Spellings[file.spellings[0]] = file.spellings
396
+ }
397
+ sort.Strings(set.Sources)
398
+ return set
399
+ }
400
+
401
+ // prismaInventoriesBySource indexes the inventories waiting on each entry of
402
+ // the parsed set.
403
+ //
404
+ // This is where one parse result becomes every population's. The parse, the
405
+ // locations, and the comment scan all speak the set's spelling; an inventory
406
+ // speaks its own population's. Resolving the second through the first is what
407
+ // keeps a schema reached by two roots from serving only the root that happened
408
+ // to sort first.
409
+ func prismaInventoriesBySource(
410
+ inventories map[string]*artifactInventory,
411
+ set prismaSourceSet,
412
+ ) map[string][]*artifactInventory {
413
+ byDisplay := prismaInventoriesByDisplay(inventories)
414
+ indexed := map[string][]*artifactInventory{}
415
+ for _, source := range set.Sources {
416
+ // Every source is the spelling of an address, and every address owns an
417
+ // inventory, so no entry here is empty. It is written as an unconditional
418
+ // fan-out rather than guarded against a state that cannot arise, because a
419
+ // guard over one would say the opposite of what the loader guarantees.
420
+ for _, spelling := range set.Spellings[source] {
421
+ indexed[source] = append(indexed[source], byDisplay[spelling]...)
422
+ }
304
423
  }
305
- sort.Strings(sources)
306
- return sources
424
+ return indexed
307
425
  }
308
426
 
309
- // prismaInventoriesByDisplay indexes the inventories that share one physical
310
- // file.
427
+ // prismaInventoriesByDisplay indexes the inventories that share one spelling of
428
+ // one physical file.
311
429
  //
312
430
  // Two populations reaching one schema through different roots own separate
313
431
  // inventories of it, because each answers a different set of globs — while the
314
432
  // file itself is parsed once and its models are located once. Everything derived
315
433
  // from those bytes therefore has to reach every inventory of the file rather
316
- // than one of them.
434
+ // than one of them, which is what `prismaInventoriesBySource` composes on top
435
+ // of this index; a population whose root spells the file differently is not
436
+ // visible here.
317
437
  func prismaInventoriesByDisplay(
318
438
  inventories map[string]*artifactInventory,
319
439
  ) map[string][]*artifactInventory {
@@ -327,7 +447,8 @@ func prismaInventoriesByDisplay(
327
447
  return indexed
328
448
  }
329
449
 
330
- // failPrismaSet records one whole-set failure against every file of the set.
450
+ // failPrismaSet records one whole-set failure against every inventory of every
451
+ // file of the set.
331
452
  //
332
453
  // The symbol is `*` rather than `model`, and the difference is load-bearing. A
333
454
  // reference reads an inventory problem only when it selects that problem's
@@ -338,11 +459,11 @@ func prismaInventoriesByDisplay(
338
459
  // failure that belongs to the whole set belongs to every selector over it.
339
460
  func failPrismaSet(
340
461
  inventories map[string]*artifactInventory,
341
- sources []string,
462
+ set prismaSourceSet,
342
463
  message string,
343
464
  ) string {
344
- indexed := prismaInventoriesByDisplay(inventories)
345
- for _, source := range sources {
465
+ indexed := prismaInventoriesBySource(inventories, set)
466
+ for _, source := range set.Sources {
346
467
  for _, inventory := range indexed[source] {
347
468
  inventory.Problems = append(inventory.Problems, inventoryProblem{
348
469
  Symbol: "*",
@@ -586,7 +707,6 @@ func prismaDeclarationsFromComments(
586
707
  // nothing reads back — the same dead end the attached case just escaped.
587
708
  for _, review := range parseReviews(run.Body) {
588
709
  shared := &evidenceReview{
589
- HostID: "prisma:" + run.Path + ":file",
590
710
  SemanticHostIDs: []string{"prisma:" + run.Path + ":file"},
591
711
  Reviews: review.Reviews,
592
712
  Type: artifactPrisma,
@@ -664,7 +784,6 @@ func prismaDeclarationsFromComments(
664
784
  // author cannot perform.
665
785
  for _, review := range parseReviews(run.Body) {
666
786
  shared := &evidenceReview{
667
- HostID: host.ID,
668
787
  SemanticHostIDs: []string{host.ID},
669
788
  Reviews: review.Reviews,
670
789
  Type: artifactPrisma,
@@ -765,7 +884,7 @@ func prismaBuriedTag(trimmed string) bool {
765
884
  // the only place a location survives: a successful parse carries no positions
766
885
  // at all, while a rejection names the file and line the author has to open.
767
886
  func prismaNormalizationFailure(message string) string {
768
- reason := strings.TrimSpace(message)
887
+ reason := causeReason(strings.TrimSpace(message))
769
888
  if reason == "" {
770
889
  reason = "the parser reported no reason"
771
890
  }
@@ -158,47 +158,71 @@ func rememberPrismaSchema(digest string, outcome prismaSetOutcome) {
158
158
  prismaSchemas.store(digest, outcome)
159
159
  }
160
160
 
161
- // prismaUnitsFromOutcome rebuilds the set's units and files them under the
162
- // schema file each one is written in.
161
+ // prismaUnitsFromOutcome rebuilds the set's units and files them into every
162
+ // inventory of the schema file each one is written in.
163
163
  //
164
164
  // Units are rebuilt per cycle rather than remembered, because a unit carries
165
165
  // its location while the parsed models do not: what was cached is a property of
166
166
  // the schema, not of where its text sits.
167
+ //
168
+ // One unit is one pointer, shared by every population of its file rather than
169
+ // copied per population. A copy would carry a second identity for one model,
170
+ // and the graph keys hosts, selections, and acknowledgements by that identity —
171
+ // so the same model would owe its obligations twice and satisfy them once. The
172
+ // location a copy would exist to restate is the set's spelling of the file,
173
+ // which is a path that opens.
167
174
  func prismaUnitsFromOutcome(
168
175
  root string,
169
- sources []string,
176
+ set prismaSourceSet,
170
177
  inventories map[string]*artifactInventory,
171
178
  outcome prismaSetOutcome,
172
179
  ) []string {
173
180
  if outcome.Rejected {
174
181
  return []string{failPrismaSet(
175
182
  inventories,
176
- sources,
183
+ set,
177
184
  prismaNormalizationFailure(outcome.Problem),
178
185
  )}
179
186
  }
180
- locations, comments := locatePrismaDeclarations(root, sources)
187
+ locations, comments := locatePrismaDeclarations(root, set.Sources)
181
188
  fallback := ""
182
- if len(sources) != 0 {
183
- fallback = sources[0]
189
+ if len(set.Sources) != 0 {
190
+ fallback = set.Sources[0]
191
+ }
192
+ indexed := prismaInventoriesBySource(inventories, set)
193
+ // Every inventory of the set, for the unit whose file the scan could not
194
+ // name. Each inventory belongs to exactly one source, so this is a union
195
+ // rather than a merge.
196
+ everywhere := []*artifactInventory{}
197
+ for _, source := range set.Sources {
198
+ everywhere = append(everywhere, indexed[source]...)
184
199
  }
185
- indexed := prismaInventoriesByDisplay(inventories)
186
200
  hosts := map[string]*evidenceUnit{}
187
201
  for _, model := range outcome.Models {
188
202
  for _, unit := range prismaModelUnits(model) {
189
203
  key := joinPrismaIdentity(unit.Identity)
190
204
  location, found := locations[key]
191
- if !found {
205
+ var hosted []*artifactInventory
206
+ if found {
207
+ unit.Path = location.Path
208
+ unit.Line = location.Line
209
+ hosted = indexed[unit.Path]
210
+ } else {
192
211
  // Locating is subordinate: a name the scan did not find keeps a
193
- // file-level location and its full participation in coverage,
194
- // because every file of the set matches the globs that selected
195
- // it. A missing line costs precision, never an obligation.
196
- location = prismaLocation{Path: fallback}
212
+ // file-level location and its full participation in coverage. A
213
+ // missing line costs precision, never an obligation — which is why
214
+ // it is filed into every inventory of the set rather than into the
215
+ // ones that happen to spell the location it was handed. The set spans
216
+ // populations whose roots name its files differently and every one of
217
+ // them selected files of this set, so charging the unit to the first
218
+ // source's populations alone would drop it from the rest with nothing
219
+ // said. That state is reached whenever a file the digest read a moment
220
+ // earlier cannot be read again, which a Windows lock is enough to do.
221
+ unit.Path = fallback
222
+ hosted = everywhere
197
223
  }
198
- unit.Path = location.Path
199
- unit.Line = location.Line
200
224
  hosts[key] = unit
201
- for _, inventory := range indexed[unit.Path] {
225
+ for _, inventory := range hosted {
202
226
  inventory.Units = append(inventory.Units, unit)
203
227
  }
204
228
  }
package/native/swagger.go CHANGED
@@ -132,7 +132,7 @@ func loadSwaggerInventories(
132
132
 
133
133
  result, err := normalizeSwaggerSources(root, pending)
134
134
  if err != nil {
135
- message := "Evidence graph could not run its Swagger normalizer: " + err.Error() + ". Swagger references require Node.js and the installed @typia/interface, @typia/utils, and yaml dependencies."
135
+ message := "Evidence graph could not run its Swagger normalizer: " + causeText(err) + ". Swagger references require Node.js and the installed @typia/interface, @typia/utils, and yaml dependencies."
136
136
  for _, source := range pending {
137
137
  inventories[source].LoadFailed = true
138
138
  inventories[source].Problems = append(
@@ -262,7 +262,7 @@ func swaggerUnitsFromOutcome(
262
262
  // that trails off after a colon reads like a formatting bug rather than a
263
263
  // broken document.
264
264
  func swaggerNormalizationFailure(source string, message string) string {
265
- reason := strings.TrimSpace(message)
265
+ reason := causeReason(strings.TrimSpace(message))
266
266
  if reason == "" {
267
267
  reason = "the normalizer reported no reason"
268
268
  }
@@ -32,13 +32,14 @@ func extendTypeScriptInventories(
32
32
  inventories map[string]*artifactInventory,
33
33
  governed map[string]bool,
34
34
  ) {
35
- bases := configuredBases(config, artifactTypeScript)
35
+ bases := typeScriptMatchBases(config)
36
36
  for _, file := range sources {
37
37
  if file == nil || !isTypeScriptPath(file.FileName()) {
38
38
  continue
39
39
  }
40
- for _, base := range bases {
41
- relative, ok := relativeProjectPath(base.Absolute, file.FileName())
40
+ for _, entry := range bases {
41
+ base := entry.base
42
+ relative, ok := entry.relativeOf(file.FileName())
42
43
  if !ok || !isTypeScriptPath(relative) {
43
44
  continue
44
45
  }
@@ -54,6 +55,67 @@ func extendTypeScriptInventories(
54
55
  }
55
56
  }
56
57
 
58
+ // typeScriptBaseProblems reports every declared TypeScript root that is not an
59
+ // existing directory and marks the population behind it failed.
60
+ //
61
+ // The other two claim-capable kinds get this from their loaders: the Markdown
62
+ // and Prisma walkers meet the root before they list anything, report it, and
63
+ // record a population failure that suppresses the glob diagnostic as its
64
+ // derivative. TypeScript materializes from `ctx.Sources` and walks nothing, so
65
+ // no loader is ever in a position to notice, and the population came back
66
+ // healthy and empty — which routed the author to `matched no typescript files`
67
+ // and a lecture on glob syntax for a mistake that was one directory name. The
68
+ // rule had already computed the right sentence and used it as a boolean.
69
+ //
70
+ // Recording the failure rather than only reporting the string is what makes the
71
+ // three kinds converge: health alone now decides that the claim stays active,
72
+ // and the population it names is the one the author has to repair.
73
+ //
74
+ // Only claim bases can reach a declared root here. A TypeScript reference selects
75
+ // the active program with `files` or an installed package with `package` and is
76
+ // refused a `root` outright, so every reference base of this kind is the default
77
+ // one, which this pass leaves alone: `baseDirectoryProblem` returns on it, and
78
+ // the resolver question below is asked only of a root someone declared.
79
+ func typeScriptBaseProblems(
80
+ config graphConfig,
81
+ inventories map[string]*artifactInventory,
82
+ ) []string {
83
+ problems := []string{}
84
+ for _, base := range configuredBases(config, artifactTypeScript) {
85
+ problem := baseDirectoryProblem(base, artifactTypeScript)
86
+ // The stat accepts a chain of links the resolver will not finish, and this
87
+ // kind has no walk to notice it afterwards, so the gate asks here. Without
88
+ // it the population came back empty and the claim deactivated in silence,
89
+ // which is the state a linked root was repaired to remove.
90
+ //
91
+ // Only a declared root is asked. The default base is the project root the
92
+ // host resolved, which both entry points realpath before this rule sees it,
93
+ // and a Program spells its sources against the same root, so the first
94
+ // comparison matches and no resolution is wanted here. Refusing anyway turns
95
+ // a population that works into an error: measured as a graph closing at zero
96
+ // diagnostics becoming one refusal that fails every claim on the base nearly
97
+ // every project uses.
98
+ //
99
+ // A Markdown or Prisma population on that base still answers, because those
100
+ // walkers generate the paths they compare and a link they cannot resolve
101
+ // leaves them nothing to walk; a project declaring neither has nothing that
102
+ // reports it. A chain long enough to survive the host's own realpath is
103
+ // longer than the platform follows, so this is the residue of a shape
104
+ // production does not reach rather than a hole in what it does.
105
+ if problem == "" && !base.Default {
106
+ if _, resolved := resolvedBaseDirectory(base); !resolved {
107
+ problem = unresolvedBaseProblem(base, artifactTypeScript)
108
+ }
109
+ }
110
+ if problem == "" {
111
+ continue
112
+ }
113
+ recordPopulationFailure(inventories, artifactTypeScript, base)
114
+ problems = append(problems, problem)
115
+ }
116
+ return problems
117
+ }
118
+
57
119
  // isTypeScriptPath is asked once per source file per configured base on every
58
120
  // rebuild, so it compares the suffix in place rather than lowercasing the whole
59
121
  // path into a fresh string to answer four fixed questions.
@@ -67,6 +129,56 @@ func isTypeScriptPath(path string) bool {
67
129
  return false
68
130
  }
69
131
 
132
+ // typeScriptMatchBase pairs a configured base with the directory a Program
133
+ // source path is measured against.
134
+ //
135
+ // This kind walks nothing, so the link asymmetry #1258 removed from the two
136
+ // walkers looked like a walk problem and was left here. It is not: `os.Stat`
137
+ // accepts a linked root as a directory, and a Program reports whatever path its
138
+ // tsconfig resolved, so when the two disagree about the link every source fails
139
+ // the comparison, the claim selects nothing, and it deactivates without a word.
140
+ // Measured before the repair: a claim rooted at a junction over its own project
141
+ // produced no diagnostic at all.
142
+ //
143
+ // Both spellings are tried, because the Program may report either, and the
144
+ // address is composed from the declared base in both cases, so a source reached
145
+ // through a link is identified exactly as it would be without one. The
146
+ // resolution is per base and the comparison is per source, so the extra spelling
147
+ // costs nothing where no link is involved.
148
+ type typeScriptMatchBase struct {
149
+ base populationBase
150
+ resolved string
151
+ }
152
+
153
+ func typeScriptMatchBases(config graphConfig) []typeScriptMatchBase {
154
+ bases := configuredBases(config, artifactTypeScript)
155
+ entries := make([]typeScriptMatchBase, 0, len(bases))
156
+ for _, base := range bases {
157
+ // An unresolved chain is reported by the gate for a declared root, and this
158
+ // comparison then has only the declared spelling to offer: keeping the link
159
+ // the resolver stopped on would spend a second comparison against a path no
160
+ // source can sit under, and the guard in `relativeOf` reads exactly this
161
+ // equality to skip it. For the default base nothing reports it here, which is
162
+ // the trade the gate's own comment states.
163
+ resolved, ok := resolvedBaseDirectory(base)
164
+ if !ok {
165
+ resolved = base.Absolute
166
+ }
167
+ entries = append(entries, typeScriptMatchBase{base: base, resolved: resolved})
168
+ }
169
+ return entries
170
+ }
171
+
172
+ func (entry typeScriptMatchBase) relativeOf(name string) (string, bool) {
173
+ if relative, ok := relativeProjectPath(entry.base.Absolute, name); ok {
174
+ return relative, true
175
+ }
176
+ if entry.resolved == entry.base.Absolute {
177
+ return "", false
178
+ }
179
+ return relativeProjectPath(entry.resolved, name)
180
+ }
181
+
70
182
  func relativeProjectPath(root string, absolute string) (string, bool) {
71
183
  if root == "" || absolute == "" {
72
184
  return "", false
@@ -1553,7 +1665,6 @@ func collectTypeScriptDeclarations(
1553
1665
  // from every acknowledgement map in evaluation.
1554
1666
  for _, review := range parseReviews(content[entry.node.Pos():entry.node.End()]) {
1555
1667
  inventory.Reviews = append(inventory.Reviews, &evidenceReview{
1556
- HostID: hostID,
1557
1668
  SemanticHostIDs: semanticHostIDs,
1558
1669
  Reviews: review.Reviews,
1559
1670
  Type: artifactTypeScript,
@@ -1814,18 +1925,18 @@ func recordGovernedTypeScriptFiles(
1814
1925
  if governed == nil {
1815
1926
  return
1816
1927
  }
1817
- bases := configuredBases(declared, artifactTypeScript)
1928
+ bases := typeScriptMatchBases(declared)
1818
1929
  for _, file := range sources {
1819
1930
  if file == nil || !isTypeScriptPath(file.FileName()) {
1820
1931
  continue
1821
1932
  }
1822
- for _, base := range bases {
1823
- relative, ok := relativeProjectPath(base.Absolute, file.FileName())
1933
+ for _, entry := range bases {
1934
+ relative, ok := entry.relativeOf(file.FileName())
1824
1935
  if !ok || !isTypeScriptPath(relative) {
1825
1936
  continue
1826
1937
  }
1827
- if matchesConfiguredTypeScriptFile(declared, base, relative) {
1828
- governed[base.addressOf(relative).Key] = true
1938
+ if matchesConfiguredTypeScriptFile(declared, entry.base, relative) {
1939
+ governed[entry.base.addressOf(relative).Key] = true
1829
1940
  }
1830
1941
  }
1831
1942
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/evidence",
3
- "version": "0.26.2",
3
+ "version": "0.28.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": {
@@ -48,13 +48,13 @@
48
48
  "@types/node": "^25.3.0",
49
49
  "rimraf": "^6.1.2",
50
50
  "typescript": "^7.0.2",
51
- "@ttsc/lint": "0.26.2",
52
- "ttsc": "0.26.2"
51
+ "@ttsc/lint": "0.28.0",
52
+ "ttsc": "0.28.0"
53
53
  },
54
54
  "dependencies": {
55
55
  "@prisma/prisma-schema-wasm": "7.10.0-2.d0368e25c88e68caa4d283b5f2d88d1b76ed9356",
56
- "@typia/interface": "13.2.0",
57
- "@typia/utils": "13.2.0",
56
+ "@typia/interface": "14.0.0",
57
+ "@typia/utils": "14.0.0",
58
58
  "yaml": "^2.9.0"
59
59
  },
60
60
  "homepage": "https://ttsc.dev",