@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/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,18 +373,24 @@ 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
  }
376
380
  if reference.Type == artifactTypeScript {
381
+ if reference.Rooted {
382
+ rootedState, rootedProblems := materializeRootedTypeScriptReference(claim, reference, loader)
383
+ problems = problems.add(severity, rootedProblems...)
384
+ state.References = append(state.References, rootedState)
385
+ continue
386
+ }
377
387
  localState, localProblems := materializeLocalTypeScriptReference(
378
388
  claim,
379
389
  reference,
380
390
  referenceInventories,
381
391
  loader,
382
392
  )
383
- problems = append(problems, localProblems...)
393
+ problems = problems.add(severity, localProblems...)
384
394
  state.References = append(state.References, localState)
385
395
  continue
386
396
  }
@@ -396,13 +406,13 @@ func materializeClaimStates(
396
406
  }
397
407
  if len(referencePaths) == 0 && referenceState.Healthy {
398
408
  if reference.Type == artifactSwagger {
399
- problems = append(
400
- problems,
409
+ problems = problems.add(
410
+ severity,
401
411
  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
412
  )
403
413
  } else {
404
- problems = append(
405
- problems,
414
+ problems = problems.add(
415
+ severity,
406
416
  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
417
  )
408
418
  }
@@ -477,8 +487,8 @@ func materializeClaimStates(
477
487
  len(referenceState.Units) == 0 &&
478
488
  referenceState.Healthy &&
479
489
  !selectedInventoryProblem {
480
- problems = append(
481
- problems,
490
+ problems = problems.add(
491
+ severity,
482
492
  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
493
  )
484
494
  }
@@ -492,8 +502,8 @@ func materializeClaimStates(
492
502
  func evaluateEvidenceGraph(
493
503
  states []claimState,
494
504
  loader *typeScriptLoader,
495
- ) []string {
496
- problems := []string{}
505
+ ) graphDiagnostics {
506
+ problems := graphDiagnostics{}
497
507
  targets := map[string]map[string]*evidenceUnit{}
498
508
  markdownTargets := map[string]map[string]*evidenceUnit{}
499
509
  // Scoped targets are keyed by owning file as well as name, which is what
@@ -603,26 +613,39 @@ func evaluateEvidenceGraph(
603
613
  sort.Strings(declarationIDs)
604
614
 
605
615
  resolved := map[string]string{}
616
+ fileLinks := indexFileLinkClaims(states, loader)
606
617
  for _, id := range declarationIDs {
618
+ severity := ownerSeverity(owners[id])
607
619
  declaration := declarations[id]
608
620
  context := declarationObligationContext(owners[id])
609
621
  if !declaration.valid() {
610
- problems = append(
611
- problems,
622
+ problems = problems.add(
623
+ severity,
612
624
  "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
625
  )
614
626
  continue
615
627
  }
628
+ if isFileLinkTarget(declaration.Target) {
629
+ unitID, problem := resolveFileLinkDeclaration(declaration, owners[id], loader, fileLinks, context)
630
+ if problem != "" {
631
+ problems = problems.add(severity, problem)
632
+ } else {
633
+ resolved[id] = unitID
634
+ }
635
+ continue
636
+ }
616
637
  if isInlineLinkTarget(declaration.Target) {
617
638
  unitID, problem := resolveInlineLinkDeclaration(
618
639
  declaration,
619
640
  loader,
620
641
  scopedTargets,
621
642
  scopedHidden,
643
+ fileLinks,
644
+ states,
622
645
  context,
623
646
  )
624
647
  if problem != "" {
625
- problems = append(problems, problem)
648
+ problems = problems.add(severity, problem)
626
649
  continue
627
650
  }
628
651
  resolved[id] = unitID
@@ -630,8 +653,8 @@ func evaluateEvidenceGraph(
630
653
  }
631
654
  if declaration.Type == artifactTypeScript &&
632
655
  looksLikeTypeScriptTarget(declaration.Target, targets, markdownTargets) {
633
- problems = append(
634
- problems,
656
+ problems = problems.add(
657
+ severity,
635
658
  "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
659
  )
637
660
  continue
@@ -648,9 +671,9 @@ func evaluateEvidenceGraph(
648
671
  if declaration.Type != artifactTypeScript {
649
672
  addressable, code := splitCodeCandidates(candidates)
650
673
  if len(addressable) == 0 && len(code) != 0 {
651
- problems = append(
652
- problems,
653
- "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.",
674
+ problems = problems.add(
675
+ severity,
676
+ "Code evidence target '"+declaration.Target+"' at "+declaration.location()+" for "+context+": an unqualified symbol has no module identity in this artifact. In Markdown, write '@link <file.ts>#<Accessor> <reason>' using a path relative to this document. Otherwise move the citation to a TypeScript claim.",
654
677
  )
655
678
  continue
656
679
  }
@@ -667,14 +690,11 @@ func evaluateEvidenceGraph(
667
690
  continue
668
691
  }
669
692
  if hidden := hiddenTargets[declaration.Target]; hidden != nil {
670
- problems = append(
671
- problems,
672
- hiddenTargetProblem(declaration, hidden, context),
673
- )
693
+ problems = problems.add(severity, hiddenTargetProblem(declaration, hidden, context))
674
694
  continue
675
695
  }
676
- problems = append(
677
- problems,
696
+ problems = problems.add(
697
+ severity,
678
698
  "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
699
  )
680
700
  case 1:
@@ -687,8 +707,8 @@ func evaluateEvidenceGraph(
687
707
  descriptions = append(descriptions, unit.Readable+" at "+unit.location())
688
708
  }
689
709
  sort.Strings(descriptions)
690
- problems = append(
691
- problems,
710
+ problems = problems.add(
711
+ severity,
692
712
  "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
713
  )
694
714
  }
@@ -697,11 +717,13 @@ func evaluateEvidenceGraph(
697
717
  participates := map[string]bool{}
698
718
  uncertain := map[string]bool{}
699
719
  outOfScope := map[string][]string{}
720
+ outOfScopeLevels := map[string]rule.Severity{}
700
721
  outOfScopeSelections := map[string]symbolSet{}
701
722
  // An exclusion outside its claim's declared carriers is a placement
702
723
  // finding, not a host-kind one, so it carries its own obligations and the
703
724
  // carrier globs its message must name.
704
725
  outsideCarrier := map[string][]string{}
726
+ outsideCarrierLevels := map[string]rule.Severity{}
705
727
  outsideCarrierGlobs := map[string]string{}
706
728
  // A checklist tag speaking for no selected host is a non-participation
707
729
  // finding: within its reference it answers nothing, and whether that earns a
@@ -711,6 +733,7 @@ func evaluateEvidenceGraph(
711
733
  // elsewhere — so the finding is recorded here and judged after the walk,
712
734
  // when `answers` can say whether anything consumed the tag.
713
735
  unhosted := map[string][]string{}
736
+ unhostedLevels := map[string]rule.Severity{}
714
737
  unhostedSelections := map[string]symbolSet{}
715
738
  // answers marks a declaration that wrote at least one acknowledgement
716
739
  // ledger, or was refused as an aggregate, which is that citation's own
@@ -732,6 +755,7 @@ func evaluateEvidenceGraph(
732
755
  // reviewing reference pays nothing.
733
756
  var reviewLedgerForClaim *reviewLedger
734
757
  for _, reference := range state.References {
758
+ severity := reference.Spec.Level
735
759
  if !reference.Healthy {
736
760
  for _, declaration := range state.Declarations {
737
761
  uncertain[declaration.ID] = true
@@ -819,6 +843,7 @@ func evaluateEvidenceGraph(
819
843
  continue
820
844
  }
821
845
  if declaration.Tag == tagExclude && state.OutsideCarrier[declaration.ID] {
846
+ outsideCarrierLevels[declaration.ID] = max(outsideCarrierLevels[declaration.ID], severity)
822
847
  outsideCarrier[declaration.ID] = appendUniqueString(
823
848
  outsideCarrier[declaration.ID],
824
849
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -827,6 +852,7 @@ func evaluateEvidenceGraph(
827
852
  continue
828
853
  }
829
854
  if !declarationEligibleForClaim(declaration, state.Spec) {
855
+ outOfScopeLevels[declaration.ID] = max(outOfScopeLevels[declaration.ID], severity)
830
856
  outOfScope[declaration.ID] = appendUniqueString(
831
857
  outOfScope[declaration.ID],
832
858
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -849,8 +875,8 @@ func evaluateEvidenceGraph(
849
875
  continue
850
876
  }
851
877
  if declaration.Tag == tagExclude && reference.Spec.Policy.NoExclude {
852
- problems = append(
853
- problems,
878
+ problems = problems.add(
879
+ severity,
854
880
  "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
881
  )
856
882
  continue
@@ -889,6 +915,7 @@ func evaluateEvidenceGraph(
889
915
  // answer, and a hard diagnostic here left that valid configuration
890
916
  // no placement to exist in. The end-of-run block reports the tag
891
917
  // once nothing has consumed it.
918
+ unhostedLevels[declaration.ID] = max(unhostedLevels[declaration.ID], severity)
892
919
  unhosted[declaration.ID] = appendUniqueString(
893
920
  unhosted[declaration.ID],
894
921
  claimLabel(state.Spec)+" "+referenceLabel(reference.Spec),
@@ -910,8 +937,8 @@ func evaluateEvidenceGraph(
910
937
  for _, unit := range covered {
911
938
  targets = append(targets, "'"+unit.Target+"'")
912
939
  }
913
- problems = append(
914
- problems,
940
+ problems = problems.add(
941
+ severity,
915
942
  "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
943
  )
917
944
  for _, unit := range covered {
@@ -926,17 +953,12 @@ func evaluateEvidenceGraph(
926
953
  // Withdrawn units are absent from Population by construction, and
927
954
  // the scope composite still needs their identities so a member
928
955
  // leaving the public surface moves the fingerprint.
929
- reviewScopes = newScopeIndex(
930
- append(
931
- append([]*evidenceUnit{}, reference.Population...),
932
- reference.Hidden...,
933
- ),
934
- )
956
+ reviewScopes = referenceReviewScopes(reference, loader)
935
957
  }
936
958
  if reviewLedgerForClaim == nil {
937
959
  reviewLedgerForClaim = newReviewLedger(state.Reviews)
938
960
  }
939
- problems = append(problems, reviewProblems(
961
+ problems = problems.add(severity, reviewProblems(
940
962
  declaration,
941
963
  scopesByID[scopeID],
942
964
  reference,
@@ -952,8 +974,8 @@ func evaluateEvidenceGraph(
952
974
  evidenceByHostAndScope[declaration.HostID] = byScope
953
975
  }
954
976
  if first := byScope[scopeID]; first != nil {
955
- problems = append(
956
- problems,
977
+ problems = problems.add(
978
+ severity,
957
979
  "Duplicate @evidence for '"+scopesByID[scopeID].Target+"' on the same host at "+declaration.location()+"; first declared at "+first.location()+".",
958
980
  )
959
981
  } else {
@@ -1060,14 +1082,14 @@ func evaluateEvidenceGraph(
1060
1082
  evidence = conflictingDeclaration
1061
1083
  exclusion = declaration
1062
1084
  }
1063
- problems = append(
1064
- problems,
1085
+ problems = problems.add(
1086
+ severity,
1065
1087
  "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
1088
  )
1067
1089
  }
1068
1090
  if duplicateExclusionUnit != nil {
1069
- problems = append(
1070
- problems,
1091
+ problems = problems.add(
1092
+ severity,
1071
1093
  "Duplicate @evidenceExclude for '"+duplicateExclusionUnit.Target+"' in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+": exclusion at "+declaration.location()+" overlaps exclusion at "+firstExclusion.location()+".",
1072
1094
  )
1073
1095
  }
@@ -1084,8 +1106,8 @@ func evaluateEvidenceGraph(
1084
1106
  if count == 1 {
1085
1107
  continue
1086
1108
  }
1087
- problems = append(
1088
- problems,
1109
+ problems = problems.add(
1110
+ severity,
1089
1111
  "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
1112
  )
1091
1113
  }
@@ -1116,8 +1138,8 @@ func evaluateEvidenceGraph(
1116
1138
  if reference.Spec.Policy.NoExclude {
1117
1139
  repair = "Do what each item requires and cite it with @evidence on this host; this reference forbids @evidenceExclude." + untrueTagWarning
1118
1140
  }
1119
- problems = append(
1120
- problems,
1141
+ problems = problems.add(
1142
+ severity,
1121
1143
  "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
1144
  )
1123
1145
  }
@@ -1131,8 +1153,8 @@ func evaluateEvidenceGraph(
1131
1153
  if reference.Spec.Policy.NoExclude {
1132
1154
  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
1155
  }
1134
- problems = append(
1135
- problems,
1156
+ problems = problems.add(
1157
+ severity,
1136
1158
  "Missing acknowledgement for '"+unit.Target+"' ("+unit.Readable+" at "+unit.location()+") in "+claimLabel(state.Spec)+" "+referenceLabel(reference.Spec)+". "+repair,
1137
1159
  )
1138
1160
  }
@@ -1140,14 +1162,15 @@ func evaluateEvidenceGraph(
1140
1162
  if !unique || hostCount <= 1 {
1141
1163
  continue
1142
1164
  }
1143
- problems = append(
1144
- problems,
1165
+ problems = problems.add(
1166
+ severity,
1145
1167
  "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
1168
  )
1147
1169
  }
1148
1170
  }
1149
1171
  }
1150
1172
  for _, id := range declarationIDs {
1173
+ severity := ownerSeverity(owners[id])
1151
1174
  if resolved[id] == "" {
1152
1175
  continue
1153
1176
  }
@@ -1161,8 +1184,8 @@ func evaluateEvidenceGraph(
1161
1184
  // withholds its own.
1162
1185
  if obligations := unhosted[id]; len(obligations) != 0 &&
1163
1186
  !answers[id] && !uncertain[id] {
1164
- problems = append(
1165
- problems,
1187
+ problems = problems.add(
1188
+ unhostedLevels[id],
1166
1189
  "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
1190
  )
1168
1191
  }
@@ -1171,8 +1194,8 @@ func evaluateEvidenceGraph(
1171
1194
  }
1172
1195
  context := declarationObligationContext(owners[id])
1173
1196
  if obligations := outsideCarrier[id]; len(obligations) != 0 {
1174
- problems = append(
1175
- problems,
1197
+ problems = problems.add(
1198
+ outsideCarrierLevels[id],
1176
1199
  "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
1200
  )
1178
1201
  continue
@@ -1183,14 +1206,14 @@ func evaluateEvidenceGraph(
1183
1206
  host = "unsupported or non-exported declaration"
1184
1207
  }
1185
1208
  if declaration.Tag == tagExclude {
1186
- problems = append(
1187
- problems,
1209
+ problems = problems.add(
1210
+ outOfScopeLevels[id],
1188
1211
  "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
1212
  )
1190
1213
  continue
1191
1214
  }
1192
- problems = append(
1193
- problems,
1215
+ problems = problems.add(
1216
+ outOfScopeLevels[id],
1194
1217
  "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
1218
  )
1196
1219
  continue
@@ -1201,8 +1224,8 @@ func evaluateEvidenceGraph(
1201
1224
  // derive a second claim from an incomplete graph.
1202
1225
  continue
1203
1226
  }
1204
- problems = append(
1205
- problems,
1227
+ problems = problems.add(
1228
+ severity,
1206
1229
  "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
1230
  )
1208
1231
  }
@@ -1438,6 +1461,7 @@ func materializeEntryReference(
1438
1461
  state.Units = population.Units
1439
1462
  state.Hidden = population.Hidden
1440
1463
  state.Published = population.Published
1464
+ state.Code = population.Code
1441
1465
  if failure := loader.failure(entry); failure != "" {
1442
1466
  state.Healthy = false
1443
1467
  return state, []string{
@@ -1529,6 +1553,7 @@ func materializeLocalTypeScriptReference(
1529
1553
  state.Units = population.Units
1530
1554
  state.Hidden = population.Hidden
1531
1555
  state.Published = population.Published
1556
+ state.Code = population.Code
1532
1557
  applyTraversedScopes(&state, population.Reached)
1533
1558
  if !state.Healthy {
1534
1559
  return state, nil
@@ -1614,6 +1639,7 @@ func materializePackageGlobReference(
1614
1639
  state.Units = population.Units
1615
1640
  state.Hidden = population.Hidden
1616
1641
  state.Published = population.Published
1642
+ state.Code = population.Code
1617
1643
  applyTraversedScopes(&state, population.Reached)
1618
1644
  if !state.Healthy {
1619
1645
  return state, problems
@@ -1681,6 +1707,8 @@ func resolveInlineLinkDeclaration(
1681
1707
  loader *typeScriptLoader,
1682
1708
  scopedTargets map[scopedTargetKey]map[string]*evidenceUnit,
1683
1709
  scopedHidden map[scopedTargetKey]*evidenceUnit,
1710
+ fileLinks map[int]*fileLinkClaimIndex,
1711
+ states []claimState,
1684
1712
  context string,
1685
1713
  ) (string, string) {
1686
1714
  target := inlineLinkTarget(declaration.Target)
@@ -1711,7 +1739,19 @@ func resolveInlineLinkDeclaration(
1711
1739
  return "", "Incomplete evidence target '" + displayTarget(declaration.Target) + "' at " + declaration.location() + " for " + context + ": a namespace import names a module rather than a unit. Name a symbol inside '" + binding.Specifier + "' that the named reference selects."
1712
1740
  }
1713
1741
  name := strings.Join(remaining, ".")
1714
- candidates := scopedTargets[scopedTargetKey{path: resolvedPath, target: name}]
1742
+ identity, _ := loader.moduleIdentity(resolvedPath)
1743
+ candidates, hidden := queryFileLinkClaims(fileLinks, states, identity, remaining, true)
1744
+ for id, unit := range scopedTargets[scopedTargetKey{path: resolvedPath, target: name}] {
1745
+ candidates[id] = unit
1746
+ }
1747
+ if len(candidates) == 0 && len(hidden) != 0 {
1748
+ ids := make([]string, 0, len(hidden))
1749
+ for id := range hidden {
1750
+ ids = append(ids, id)
1751
+ }
1752
+ sort.Strings(ids)
1753
+ return "", hiddenTargetProblem(declaration, hidden[ids[0]], context)
1754
+ }
1715
1755
  switch len(candidates) {
1716
1756
  case 0:
1717
1757
  if hidden := scopedHidden[scopedTargetKey{
package/native/imports.go CHANGED
@@ -19,6 +19,7 @@ type importBinding struct {
19
19
  Specifier string
20
20
  Imported string
21
21
  Namespace bool
22
+ TypeOnly bool
22
23
  }
23
24
 
24
25
  // collectImportBindings indexes a file's imports by the local name each binds.
@@ -52,6 +53,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
52
53
  Local: name,
53
54
  Specifier: specifier,
54
55
  Imported: "default",
56
+ TypeOnly: clause.IsTypeOnly(),
55
57
  }
56
58
  }
57
59
  if clause.NamedBindings == nil {
@@ -64,6 +66,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
64
66
  Local: name,
65
67
  Specifier: specifier,
66
68
  Namespace: true,
69
+ TypeOnly: clause.IsTypeOnly(),
67
70
  }
68
71
  }
69
72
  case shimast.KindNamedImports:
@@ -91,6 +94,7 @@ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
91
94
  Local: local,
92
95
  Specifier: specifier,
93
96
  Imported: imported,
97
+ TypeOnly: clause.IsTypeOnly() || specifierNode.IsTypeOnly,
94
98
  }
95
99
  }
96
100
  }
@@ -124,19 +128,28 @@ var typeScriptModuleExtensions = []string{
124
128
  // must spell its sibling as `./x.js`, so refusing to map it back would make the
125
129
  // correct import form unresolvable.
126
130
  func moduleCandidates(base string) []string {
127
- candidates := []string{base}
128
- stripped := base
129
- for _, emitted := range []string{".js", ".mjs", ".cjs"} {
130
- if strings.HasSuffix(base, emitted) {
131
- stripped = strings.TrimSuffix(base, emitted)
132
- break
131
+ if isTypeScriptPath(base) {
132
+ return []string{base}
133
+ }
134
+ substitutions := map[string][]string{
135
+ ".js": {".ts", ".tsx", ".d.ts"},
136
+ ".mjs": {".mts", ".d.mts"},
137
+ ".cjs": {".cts", ".d.cts"},
138
+ }
139
+ if extensions, emitted := substitutions[path.Ext(base)]; emitted {
140
+ stripped := strings.TrimSuffix(base, path.Ext(base))
141
+ candidates := make([]string, 0, len(extensions))
142
+ for _, extension := range extensions {
143
+ candidates = append(candidates, stripped+extension)
133
144
  }
145
+ return candidates
134
146
  }
147
+ candidates := []string{}
135
148
  for _, extension := range typeScriptModuleExtensions {
136
- candidates = append(candidates, stripped+extension)
149
+ candidates = append(candidates, base+extension)
137
150
  }
138
151
  for _, extension := range typeScriptModuleExtensions {
139
- candidates = append(candidates, path.Join(stripped, "index"+extension))
152
+ candidates = append(candidates, path.Join(base, "index"+extension))
140
153
  }
141
154
  return candidates
142
155
  }
package/native/inputs.go CHANGED
@@ -35,10 +35,8 @@ func (graphRule) ProjectInputs(ctx *rule.ProjectInputContext) []rule.ProjectInpu
35
35
  // graphProjectInputs collects the external dependency of every configured
36
36
  // obligation.
37
37
  //
38
- // TypeScript claims and references are absent by design. Their inventories are
39
- // materialized from `ctx.Sources`, which is the Program the host already
40
- // watches, so declaring them again would ask for a second watcher on a file
41
- // that already has one.
38
+ // Program sources are already watched. Rooted TypeScript references additionally
39
+ // declare the directory containing their disk-loaded export closure.
42
40
  func graphProjectInputs(config graphConfig) []rule.ProjectInput {
43
41
  config = enabledGraphConfig(config)
44
42
  inputs := []rule.ProjectInput{}
@@ -49,6 +47,17 @@ func graphProjectInputs(config graphConfig) []rule.ProjectInput {
49
47
  }
50
48
  for _, reference := range claim.References {
51
49
  switch reference.Type {
50
+ case artifactTypeScript:
51
+ if reference.Rooted {
52
+ // Export traversal can reach a module outside the entry globs, and
53
+ // this pre-Program protocol cannot discover that closure. The root
54
+ // is its explicit boundary, including future files and repairs.
55
+ pattern := "**"
56
+ if reference.Root != "" {
57
+ pattern = strings.TrimSuffix(reference.Root, "/") + "/**"
58
+ }
59
+ inputs = append(inputs, rule.ProjectInput{Kind: rule.ProjectInputGlob, Pattern: pattern})
60
+ }
52
61
  case artifactMarkdown, artifactPrisma:
53
62
  inputs = append(inputs, globInputs(reference.Root, reference.Files)...)
54
63
  case artifactSwagger: