@ttsc/evidence 0.25.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +279 -0
  3. package/lib/index.d.ts +65 -0
  4. package/lib/index.js +87 -0
  5. package/lib/internal/loadPrismaModels.d.ts +1 -0
  6. package/lib/internal/loadPrismaModels.js +197 -0
  7. package/lib/internal/loadSwaggerOperations.d.ts +1 -0
  8. package/lib/internal/loadSwaggerOperations.js +156 -0
  9. package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
  10. package/lib/internal/normalizeSwaggerDocument.js +7 -0
  11. package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
  12. package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
  13. package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
  14. package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
  15. package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
  16. package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
  17. package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
  18. package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
  19. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
  20. package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
  21. package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
  22. package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
  23. package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
  24. package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
  25. package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
  26. package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
  27. package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
  28. package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
  29. package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
  30. package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
  31. package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
  32. package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
  33. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
  34. package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
  35. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
  36. package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
  37. package/lib/structures/index.d.ts +13 -0
  38. package/lib/structures/index.js +29 -0
  39. package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
  40. package/lib/typings/ITtscLintContributorRules.js +2 -0
  41. package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
  42. package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
  43. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
  44. package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
  45. package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
  46. package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
  47. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
  48. package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
  49. package/lib/typings/index.d.ts +5 -0
  50. package/lib/typings/index.js +21 -0
  51. package/native/base.go +298 -0
  52. package/native/config.go +798 -0
  53. package/native/declaration.go +240 -0
  54. package/native/documented.go +353 -0
  55. package/native/exports.go +484 -0
  56. package/native/glob.go +207 -0
  57. package/native/graph.go +1409 -0
  58. package/native/health.go +45 -0
  59. package/native/hints.go +261 -0
  60. package/native/imports.go +373 -0
  61. package/native/inputs.go +125 -0
  62. package/native/loader.go +424 -0
  63. package/native/markdown.go +423 -0
  64. package/native/model.go +296 -0
  65. package/native/prisma.go +719 -0
  66. package/native/prisma_cache.go +213 -0
  67. package/native/prisma_locator.go +417 -0
  68. package/native/singular.go +369 -0
  69. package/native/swagger.go +331 -0
  70. package/native/swagger_cache.go +274 -0
  71. package/native/todo.go +138 -0
  72. package/native/typescript.go +1199 -0
  73. package/native/typescript_cache.go +96 -0
  74. package/package.json +64 -0
@@ -0,0 +1,45 @@
1
+ package evidence
2
+
3
+ // recordPopulationFailure preserves a loader failure that belongs to a whole
4
+ // population rather than to one file.
5
+ //
6
+ // A synthetic inventory keeps the existing loader boundary intact: callers
7
+ // still receive the inventories and diagnostics they asked for, while graph
8
+ // materialization can tell "matched nothing" from "could not inspect enough to
9
+ // know what matched". Its NUL-prefixed key cannot be produced by a normalized
10
+ // artifact address and matchingInventoryPaths rejects it through relativeOf.
11
+ func recordPopulationFailure(
12
+ inventories map[string]*artifactInventory,
13
+ kind artifactKind,
14
+ base populationBase,
15
+ ) {
16
+ if inventories == nil || base.Absolute == "" {
17
+ return
18
+ }
19
+ key := "\x00load-failure:" + string(kind) + ":" + base.Absolute
20
+ inventories[key] = &artifactInventory{
21
+ Type: kind,
22
+ LoadFailed: true,
23
+ FailureBase: base.Absolute,
24
+ }
25
+ }
26
+
27
+ func populationIsHealthy(
28
+ inventories map[string]*artifactInventory,
29
+ base populationBase,
30
+ paths []string,
31
+ ) bool {
32
+ for _, inventory := range inventories {
33
+ if inventory != nil &&
34
+ inventory.LoadFailed &&
35
+ inventory.FailureBase == base.Absolute {
36
+ return false
37
+ }
38
+ }
39
+ for _, path := range paths {
40
+ if inventory := inventories[path]; inventory != nil && inventory.LoadFailed {
41
+ return false
42
+ }
43
+ }
44
+ return true
45
+ }
@@ -0,0 +1,261 @@
1
+ package evidence
2
+
3
+ import (
4
+ "sort"
5
+ "sync"
6
+
7
+ "github.com/samchon/ttsc/packages/lint/rule"
8
+ )
9
+
10
+ // graphCorpus is what a passing Check publishes for Hints to project.
11
+ //
12
+ // It carries the inventories Check already built rather than the paths to
13
+ // rebuild them from. The graph loads Markdown and Swagger to do its own job, so
14
+ // handing that result forward costs a build nothing; reloading inside Hints
15
+ // would read every source a second time to answer a question the rule had just
16
+ // finished answering.
17
+ //
18
+ // What the editor path does pay is Check itself. A hints request runs a fresh
19
+ // project cycle (`linthost/hints.go:74`), so every source the graph reads is
20
+ // read again there — including an HTTP(S) Swagger reference, which is fetched.
21
+ // That cost is inherent to answering from a current index rather than a stale
22
+ // one, and it is bounded the same way a build is: a source that fails to load
23
+ // is reported by Check, which fails the rule, which withdraws the corpus.
24
+ type graphCorpus struct {
25
+ Config graphConfig
26
+ Markdown map[string]*artifactInventory
27
+ Prisma map[string]*artifactInventory
28
+ Swagger map[string]*artifactInventory
29
+ }
30
+
31
+ // graphCycleState is the immutable graph corpus plus the one piece of mutable
32
+ // coordination file rules need during this Program cycle.
33
+ //
34
+ // The host exposes project state, not a lifecycle identifier, to contributor
35
+ // file rules. Keeping the diagnostic gate here gives every parallel file walk
36
+ // one cycle-scoped rendezvous and lets the host discard it on the next watch
37
+ // rebuild. The mutex owns only a set membership check; graph data remains
38
+ // immutable.
39
+ type graphCycleState struct {
40
+ Corpus graphCorpus
41
+ Diagnostics programDiagnosticGate
42
+ }
43
+
44
+ type programDiagnosticGate struct {
45
+ mutex sync.Mutex
46
+ reported map[string]bool
47
+ }
48
+
49
+ func (gate *programDiagnosticGate) first(key string) bool {
50
+ if gate == nil {
51
+ return true
52
+ }
53
+ gate.mutex.Lock()
54
+ defer gate.mutex.Unlock()
55
+ if gate.reported == nil {
56
+ gate.reported = map[string]bool{}
57
+ }
58
+ if gate.reported[key] {
59
+ return false
60
+ }
61
+ gate.reported[key] = true
62
+ return true
63
+ }
64
+
65
+ // Hints projects the configured evidence population into editor completions.
66
+ //
67
+ // A target has to be reproduced exactly, and the anchor an author cannot recall
68
+ // is the one this offers. The corpus is a value: the host serializes it and the
69
+ // LSP proxy answers from cache long after the lint process exited, so nothing
70
+ // here can be computed per keystroke.
71
+ //
72
+ // The corpus is available only while the graph passes, and that is a host gate
73
+ // rather than a choice made here. `linthost/hints.go:147-149` skips a rule whose
74
+ // snapshot is not `ProjectRulePassed` or whose state is nil, and
75
+ // `projectReporter.Report` sets `failed = true` unconditionally
76
+ // (`linthost/project_engine.go:68-77`). So the cycle that reports an unmet
77
+ // obligation is the cycle that withdraws the completions — which is the cycle an
78
+ // author is most likely to be writing a citation in. Nothing in this package can
79
+ // widen that; it widens upstream, by letting a project rule publish state it
80
+ // reported against.
81
+ func (graphRule) Hints(ctx *rule.HintContext) []rule.Hint {
82
+ if ctx == nil {
83
+ return nil
84
+ }
85
+ cycle, published := ctx.State.(*graphCycleState)
86
+ if !published || cycle == nil {
87
+ return nil
88
+ }
89
+ corpus := cycle.Corpus
90
+ hints := []rule.Hint{}
91
+ for _, trigger := range evidenceHintTriggers {
92
+ exclusion := trigger.After == "@evidenceExclude "
93
+ units := selectedCompletionUnits(
94
+ corpus.Config,
95
+ corpus.Markdown,
96
+ corpus.Prisma,
97
+ corpus.Swagger,
98
+ exclusion,
99
+ )
100
+ routes := selectsTypeScriptReference(corpus.Config, exclusion)
101
+ if routes {
102
+ hints = append(hints, typeScriptRouteHint(trigger))
103
+ }
104
+ for _, unit := range units {
105
+ hints = append(hints, rule.Hint{
106
+ Insert: unit.Target,
107
+ Detail: unit.Readable,
108
+ Trigger: trigger,
109
+ })
110
+ }
111
+ }
112
+ return hints
113
+ }
114
+
115
+ // evidenceHintTriggers names both tag positions a target can be written in.
116
+ //
117
+ // Each `After` ends where the target begins, which is what makes the text the
118
+ // author has typed so far the editor's filter. The trailing space also keeps
119
+ // the two apart: `"@evidence "` cannot occur inside `"@evidenceExclude "`,
120
+ // because the character following `@evidence` there is `E`.
121
+ var evidenceHintTriggers = []rule.HintTrigger{
122
+ {Scope: rule.HintScopeJSDoc, After: "@evidence "},
123
+ {Scope: rule.HintScopeJSDoc, After: "@evidenceExclude "},
124
+ }
125
+
126
+ // typeScriptRouteHint routes the author into TypeScript's own completion.
127
+ //
128
+ // The host merges a corpus into the upstream response and cannot remove from
129
+ // it, so narrowing TypeScript's symbol list is not available to us. Putting the
130
+ // author inside that list is: once the brace opens, the language service
131
+ // answers every keystroke after it, against the import scope at the cursor —
132
+ // which a corpus built once per Program could never know.
133
+ //
134
+ // The inserted text is deliberately unclosed. `Insert` is verbatim with no
135
+ // snippet expansion, so the cursor lands where the text ends; `{@link }` would
136
+ // park it one character past the only position where completion fires. A
137
+ // missing brace is visible and one keystroke, and the target grammar catches it
138
+ // if forgotten. A cursor in the wrong place is silent.
139
+ func typeScriptRouteHint(trigger rule.HintTrigger) rule.Hint {
140
+ return rule.Hint{
141
+ Insert: "{@link ",
142
+ Label: "{@link",
143
+ Detail: "TypeScript symbol, resolved through this file's imports",
144
+ Trigger: trigger,
145
+ }
146
+ }
147
+
148
+ // selectsTypeScriptReference reports whether any claim cites TypeScript.
149
+ //
150
+ // The entry is withheld otherwise, because a graph citing only Markdown cannot
151
+ // resolve an inline-link target — offering the grammar there would hand the
152
+ // author an unresolved-target diagnostic for taking a suggestion.
153
+ //
154
+ // The condition is necessarily global. A corpus answers a keystroke with no
155
+ // file context, so it cannot tell which claim owns the file the cursor is in;
156
+ // a repository mixing a TypeScript-citing claim with a Markdown-only one offers
157
+ // the entry in both. Narrowing that needs a per-file corpus, which the contract
158
+ // deliberately does not have.
159
+ func selectsTypeScriptReference(config graphConfig, exclusion bool) bool {
160
+ for _, claim := range config.Claims {
161
+ for _, reference := range claim.References {
162
+ if reference.Type == artifactTypeScript &&
163
+ (!exclusion || !reference.Policy.NoExclude) {
164
+ return true
165
+ }
166
+ }
167
+ }
168
+ return false
169
+ }
170
+
171
+ // selectedCompletionUnits collects the units some configured reference selects,
172
+ // in the order they should be offered.
173
+ //
174
+ // Slice order is the corpus's only ranking channel, so it answers what an
175
+ // author cannot supply from memory. A heading's generated anchor is neither
176
+ // visible in the project tree nor guessable from its text, so selected headings
177
+ // come first; a Markdown file path is typed as easily as it is read, so file
178
+ // targets follow; Swagger operations come last, being both few and mechanical.
179
+ //
180
+ // Order therefore serves the author who is still browsing, and the filter
181
+ // serves the one who is not. No hint sets Label, so the editor lists and
182
+ // filters on the target itself: typing `docs/pricing` narrows to that
183
+ // document's anchors, which is the flow this ranking is for — an author writes
184
+ // a citation because they are implementing a document they have already chosen.
185
+ //
186
+ // Labelling a heading by its text would invert that. `sale` would match and
187
+ // `docs/pricing` would match nothing, two documents sharing a heading would
188
+ // become indistinguishable entries, and the listed text would stop being the
189
+ // inserted text — three costs to serve recall-by-title, which Detail already
190
+ // serves by being visible. Leaving Label empty keeps what is shown and what is
191
+ // written identical by construction.
192
+ //
193
+ // TypeScript units are absent on purpose. TypeScript's own language service
194
+ // completes symbols inside `{@link}` against the scope at the cursor, which a
195
+ // corpus built once per Program cannot know — offering one here would duplicate
196
+ // a correct list with a worse one.
197
+ func selectedCompletionUnits(
198
+ config graphConfig,
199
+ markdown map[string]*artifactInventory,
200
+ prisma map[string]*artifactInventory,
201
+ swagger map[string]*artifactInventory,
202
+ exclusion bool,
203
+ ) []*evidenceUnit {
204
+ ranked := map[string][]*evidenceUnit{}
205
+ seen := map[string]bool{}
206
+ for _, claim := range config.Claims {
207
+ for _, reference := range claim.References {
208
+ if exclusion && reference.Policy.NoExclude {
209
+ continue
210
+ }
211
+ inventories := inventoriesOf(
212
+ reference.Type,
213
+ markdown,
214
+ prisma,
215
+ swagger,
216
+ map[string]*artifactInventory{},
217
+ )
218
+ for _, path := range matchingReferencePaths(inventories, reference) {
219
+ inventory := inventories[path]
220
+ if inventory == nil {
221
+ continue
222
+ }
223
+ for _, unit := range inventory.Units {
224
+ if unit.Hidden != "" ||
225
+ !reference.Symbols.contains(unit.Symbol) ||
226
+ seen[unit.ID] {
227
+ continue
228
+ }
229
+ seen[unit.ID] = true
230
+ group := completionRank(unit)
231
+ ranked[group] = append(ranked[group], unit)
232
+ }
233
+ }
234
+ }
235
+ }
236
+ units := []*evidenceUnit{}
237
+ for _, group := range []string{"heading", "file", "operation", "model"} {
238
+ tier := ranked[group]
239
+ sort.SliceStable(tier, func(left int, right int) bool {
240
+ if tier[left].Path != tier[right].Path {
241
+ return tier[left].Path < tier[right].Path
242
+ }
243
+ return tier[left].Line < tier[right].Line
244
+ })
245
+ units = append(units, tier...)
246
+ }
247
+ return units
248
+ }
249
+
250
+ func completionRank(unit *evidenceUnit) string {
251
+ switch {
252
+ case unit.Type == artifactSwagger:
253
+ return "operation"
254
+ case unit.Type == artifactPrisma:
255
+ return "model"
256
+ case unit.Symbol == "file":
257
+ return "file"
258
+ default:
259
+ return "heading"
260
+ }
261
+ }
@@ -0,0 +1,373 @@
1
+ package evidence
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path"
7
+ "strings"
8
+
9
+ shimast "github.com/microsoft/typescript-go/shim/ast"
10
+ )
11
+
12
+ // importBinding is one name a module brings into scope, and where it came from.
13
+ //
14
+ // Imported is empty for a namespace import, which contributes no segment of its
15
+ // own: `import * as api` makes `api.functional` mean `functional` inside the
16
+ // resolved module, while `import { ISale }` makes `ISale` mean `ISale` there.
17
+ type importBinding struct {
18
+ Local string
19
+ Specifier string
20
+ Imported string
21
+ Namespace bool
22
+ }
23
+
24
+ // collectImportBindings indexes a file's imports by the local name each binds.
25
+ //
26
+ // Type-only imports are included deliberately. A citation-only import should be
27
+ // `import type`, because it is erased at emit and creates no runtime dependency
28
+ // or cycle — so the form the rule recommends must be the form it can resolve.
29
+ func collectImportBindings(file *shimast.SourceFile) map[string]importBinding {
30
+ bindings := map[string]importBinding{}
31
+ if file == nil || file.Statements == nil {
32
+ return bindings
33
+ }
34
+ for _, statement := range file.Statements.Nodes {
35
+ if statement == nil || statement.Kind != shimast.KindImportDeclaration {
36
+ continue
37
+ }
38
+ declaration := statement.AsImportDeclaration()
39
+ if declaration == nil || declaration.ImportClause == nil {
40
+ continue
41
+ }
42
+ specifier := stringLiteralText(declaration.ModuleSpecifier)
43
+ if specifier == "" {
44
+ continue
45
+ }
46
+ clause := declaration.ImportClause.AsImportClause()
47
+ if clause == nil {
48
+ continue
49
+ }
50
+ if name := declarationName(declaration.ImportClause.Name()); name != "" {
51
+ bindings[name] = importBinding{
52
+ Local: name,
53
+ Specifier: specifier,
54
+ Imported: "default",
55
+ }
56
+ }
57
+ if clause.NamedBindings == nil {
58
+ continue
59
+ }
60
+ switch clause.NamedBindings.Kind {
61
+ case shimast.KindNamespaceImport:
62
+ if name := declarationName(clause.NamedBindings.Name()); name != "" {
63
+ bindings[name] = importBinding{
64
+ Local: name,
65
+ Specifier: specifier,
66
+ Namespace: true,
67
+ }
68
+ }
69
+ case shimast.KindNamedImports:
70
+ named := clause.NamedBindings.AsNamedImports()
71
+ if named == nil || named.Elements == nil {
72
+ continue
73
+ }
74
+ for _, element := range named.Elements.Nodes {
75
+ if element == nil || element.Kind != shimast.KindImportSpecifier {
76
+ continue
77
+ }
78
+ specifierNode := element.AsImportSpecifier()
79
+ if specifierNode == nil {
80
+ continue
81
+ }
82
+ local := declarationName(element.Name())
83
+ imported := local
84
+ if specifierNode.PropertyName != nil {
85
+ imported = declarationName(specifierNode.PropertyName)
86
+ }
87
+ if local == "" || imported == "" {
88
+ continue
89
+ }
90
+ bindings[local] = importBinding{
91
+ Local: local,
92
+ Specifier: specifier,
93
+ Imported: imported,
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return bindings
99
+ }
100
+
101
+ func stringLiteralText(node *shimast.Node) string {
102
+ if node == nil || node.Kind != shimast.KindStringLiteral {
103
+ return ""
104
+ }
105
+ return node.Text()
106
+ }
107
+
108
+ // typeScriptModuleExtensions are tried in the order TypeScript itself prefers.
109
+ var typeScriptModuleExtensions = []string{
110
+ ".ts",
111
+ ".tsx",
112
+ ".mts",
113
+ ".cts",
114
+ ".d.ts",
115
+ ".d.mts",
116
+ ".d.cts",
117
+ }
118
+
119
+ // splitPackageSpecifier separates a package name from a deep-import subpath,
120
+ // keeping the leading segment of a scoped name attached to its scope.
121
+ // moduleCandidates lists the files a specifier may denote, most specific first.
122
+ //
123
+ // The `.js` rewrite is not a convenience: under `nodenext` a TypeScript source
124
+ // must spell its sibling as `./x.js`, so refusing to map it back would make the
125
+ // correct import form unresolvable.
126
+ 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
133
+ }
134
+ }
135
+ for _, extension := range typeScriptModuleExtensions {
136
+ candidates = append(candidates, stripped+extension)
137
+ }
138
+ for _, extension := range typeScriptModuleExtensions {
139
+ candidates = append(candidates, path.Join(stripped, "index"+extension))
140
+ }
141
+ return candidates
142
+ }
143
+
144
+ func splitPackageSpecifier(specifier string) (string, string) {
145
+ segments := strings.Split(specifier, "/")
146
+ if len(segments) == 0 || segments[0] == "" {
147
+ return "", ""
148
+ }
149
+ count := 1
150
+ if strings.HasPrefix(segments[0], "@") {
151
+ if len(segments) < 2 {
152
+ return "", ""
153
+ }
154
+ count = 2
155
+ }
156
+ name := strings.Join(segments[:count], "/")
157
+ subpath := strings.Join(segments[count:], "/")
158
+ if subpath != "" {
159
+ subpath = "./" + subpath
160
+ }
161
+ return name, subpath
162
+ }
163
+
164
+ func readPackageManifest(root string, relative string) map[string]json.RawMessage {
165
+ content, err := os.ReadFile(path.Join(strings.ReplaceAll(root, "\\", "/"), relative))
166
+ if err != nil {
167
+ return nil
168
+ }
169
+ manifest := map[string]json.RawMessage{}
170
+ if err := json.Unmarshal(content, &manifest); err != nil {
171
+ return nil
172
+ }
173
+ return manifest
174
+ }
175
+
176
+ func packageTypeEntry(
177
+ manifest map[string]json.RawMessage,
178
+ subpath string,
179
+ ) string {
180
+ if manifest == nil {
181
+ return ""
182
+ }
183
+ key := "."
184
+ if subpath != "" {
185
+ key = subpath
186
+ }
187
+ if entry := exportsTypeEntry(manifest["exports"], key); entry != "" {
188
+ return entry
189
+ }
190
+ if entry := typesVersionsEntry(manifest["typesVersions"], key); entry != "" {
191
+ return entry
192
+ }
193
+ if subpath == "" {
194
+ for _, field := range []string{"types", "typings"} {
195
+ var value string
196
+ if err := json.Unmarshal(manifest[field], &value); err == nil && value != "" {
197
+ return value
198
+ }
199
+ }
200
+ }
201
+ // Every declaration channel above has been asked first, because a package
202
+ // that names one has said where its declarations are and its runtime entry
203
+ // is not it. Only when none answers is the runtime entry considered, and
204
+ // only when it names TypeScript: a source-first workspace package has no
205
+ // other way to say where its entry is.
206
+ if entry := exportsRuntimeEntry(manifest["exports"], key); entry != "" {
207
+ return entry
208
+ }
209
+ if subpath != "" {
210
+ return ""
211
+ }
212
+ var main string
213
+ if err := json.Unmarshal(manifest["main"], &main); err == nil {
214
+ return typeScriptEntryTarget(main)
215
+ }
216
+ return ""
217
+ }
218
+
219
+ // typeScriptEntryTarget returns target when it names TypeScript itself.
220
+ //
221
+ // A published package points its runtime entry at emitted JavaScript and keeps
222
+ // its declarations behind a `types` condition, so following that entry would
223
+ // address the wrong file. A source-first workspace package has no emit to point
224
+ // at: pnpm links `packages/api` into `node_modules`, and its entry names
225
+ // `./src/index.ts`, which is both what a consumer imports and where the
226
+ // declarations are. Refusing it there leaves a linked package with no
227
+ // resolvable entry, and a reference that cannot find its entry publishes its
228
+ // units under the module that matched instead — which is how
229
+ // `functional.health.get` collapses to `get`.
230
+ func typeScriptEntryTarget(target string) string {
231
+ for _, extension := range typeScriptModuleExtensions {
232
+ if strings.HasSuffix(target, extension) {
233
+ return target
234
+ }
235
+ }
236
+ return ""
237
+ }
238
+
239
+ // exportsTypeEntry reads the `types` condition of an exports map.
240
+ //
241
+ // The map may be a bare string, a condition object, or a subpath object whose
242
+ // values are either. Only the `types` condition is followed, because a
243
+ // citation addresses declarations rather than the runtime entry `import` and
244
+ // `require` name. [exportsRuntimeEntry] answers for the package that has no
245
+ // declaration channel at all.
246
+ func exportsTypeEntry(raw json.RawMessage, key string) string {
247
+ if len(raw) == 0 {
248
+ return ""
249
+ }
250
+ // A bare-string exports map names the runtime entry only, and a citation
251
+ // addresses declarations, so there is nothing here to follow.
252
+ var direct string
253
+ if err := json.Unmarshal(raw, &direct); err == nil {
254
+ return ""
255
+ }
256
+ object := map[string]json.RawMessage{}
257
+ if err := json.Unmarshal(raw, &object); err != nil {
258
+ return ""
259
+ }
260
+ if _, subpaths := object["."]; subpaths || strings.HasPrefix(key, "./") {
261
+ entry, exists := object[key]
262
+ if !exists {
263
+ return ""
264
+ }
265
+ return exportsConditionEntry(entry)
266
+ }
267
+ if key != "." {
268
+ return ""
269
+ }
270
+ return exportsConditionEntry(raw)
271
+ }
272
+
273
+ func exportsConditionEntry(raw json.RawMessage) string {
274
+ if len(raw) == 0 {
275
+ return ""
276
+ }
277
+ var direct string
278
+ if err := json.Unmarshal(raw, &direct); err == nil {
279
+ return ""
280
+ }
281
+ object := map[string]json.RawMessage{}
282
+ if err := json.Unmarshal(raw, &object); err != nil {
283
+ return ""
284
+ }
285
+ if types, exists := object["types"]; exists {
286
+ var value string
287
+ if err := json.Unmarshal(types, &value); err == nil {
288
+ return value
289
+ }
290
+ return exportsConditionEntry(types)
291
+ }
292
+ for _, condition := range []string{"import", "default"} {
293
+ if nested, exists := object[condition]; exists {
294
+ if entry := exportsConditionEntry(nested); entry != "" {
295
+ return entry
296
+ }
297
+ }
298
+ }
299
+ return ""
300
+ }
301
+
302
+ // exportsRuntimeEntry reads the runtime entry of an exports map, and returns
303
+ // it only when it names TypeScript.
304
+ //
305
+ // It mirrors [exportsTypeEntry] over the same three shapes, following the
306
+ // target a consumer actually imports rather than the `types` condition. That
307
+ // target is the declarations exactly when it is TypeScript, which is the
308
+ // source-first workspace package [typeScriptEntryTarget] describes.
309
+ func exportsRuntimeEntry(raw json.RawMessage, key string) string {
310
+ if len(raw) == 0 {
311
+ return ""
312
+ }
313
+ var direct string
314
+ if err := json.Unmarshal(raw, &direct); err == nil {
315
+ if key != "." {
316
+ return ""
317
+ }
318
+ return typeScriptEntryTarget(direct)
319
+ }
320
+ object := map[string]json.RawMessage{}
321
+ if err := json.Unmarshal(raw, &object); err != nil {
322
+ return ""
323
+ }
324
+ if _, subpaths := object["."]; subpaths || strings.HasPrefix(key, "./") {
325
+ entry, exists := object[key]
326
+ if !exists {
327
+ return ""
328
+ }
329
+ return runtimeConditionEntry(entry)
330
+ }
331
+ if key != "." {
332
+ return ""
333
+ }
334
+ return runtimeConditionEntry(raw)
335
+ }
336
+
337
+ func runtimeConditionEntry(raw json.RawMessage) string {
338
+ if len(raw) == 0 {
339
+ return ""
340
+ }
341
+ var direct string
342
+ if err := json.Unmarshal(raw, &direct); err == nil {
343
+ return typeScriptEntryTarget(direct)
344
+ }
345
+ object := map[string]json.RawMessage{}
346
+ if err := json.Unmarshal(raw, &object); err != nil {
347
+ return ""
348
+ }
349
+ for _, condition := range []string{"import", "default", "require"} {
350
+ if nested, exists := object[condition]; exists {
351
+ if entry := runtimeConditionEntry(nested); entry != "" {
352
+ return entry
353
+ }
354
+ }
355
+ }
356
+ return ""
357
+ }
358
+
359
+ func typesVersionsEntry(raw json.RawMessage, key string) string {
360
+ if len(raw) == 0 || key != "." {
361
+ return ""
362
+ }
363
+ versions := map[string]map[string][]string{}
364
+ if err := json.Unmarshal(raw, &versions); err != nil {
365
+ return ""
366
+ }
367
+ for _, mapping := range versions {
368
+ if entries, exists := mapping["*"]; exists && len(entries) != 0 {
369
+ return entries[0]
370
+ }
371
+ }
372
+ return ""
373
+ }