@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,369 @@
1
+ package evidence
2
+
3
+ import (
4
+ "path"
5
+ "sort"
6
+ "strings"
7
+
8
+ shimast "github.com/microsoft/typescript-go/shim/ast"
9
+
10
+ "github.com/samchon/ttsc/packages/lint/rule"
11
+ )
12
+
13
+ // singularRule enforces that a file declares exactly one public identity and
14
+ // takes that identity's name.
15
+ //
16
+ // The unit counted is an identity, not an export. TypeScript declaration
17
+ // merging exposes one name through several declarations — `export interface I`
18
+ // beside `export namespace I`, `export const x` beside `export default x` — and
19
+ // every one of those forms is a single identity here, so the permitted merges
20
+ // fall out of what is counted rather than being carved out of it.
21
+ type singularRule struct{}
22
+
23
+ func (singularRule) Name() string { return singularRuleName }
24
+
25
+ func (singularRule) Visits() []shimast.Kind {
26
+ return []shimast.Kind{shimast.KindSourceFile}
27
+ }
28
+
29
+ func (singularRule) NeedsTypeChecker() bool { return false }
30
+
31
+ func (singularRule) VisitsDeclarationFiles() bool { return false }
32
+
33
+ func (singularRule) AcceptsTtscLintOptions() bool { return false }
34
+
35
+ func (singularRule) Check(ctx *rule.Context, node *shimast.Node) {
36
+ if ctx == nil || ctx.File == nil || node == nil {
37
+ return
38
+ }
39
+ if node.Kind != shimast.KindSourceFile {
40
+ return
41
+ }
42
+ surface := collectPublicSurface(ctx.File)
43
+ content := ctx.File.Text()
44
+
45
+ // One finding per file, most structural first. A file that declares two
46
+ // identities has to be split before its name can mean anything, and an
47
+ // anonymous default has to be named before a name can be compared.
48
+ if len(surface.Identities) > 1 {
49
+ ctx.Report(
50
+ surface.Identities[1].Node,
51
+ "A file declares exactly one public identity, but this one declares "+
52
+ decimal(len(surface.Identities))+": "+
53
+ describeIdentities(surface.Identities, content)+
54
+ ". Move every extra identity to its own file, or re-export them from a barrel; merged declarations of one name count once.",
55
+ )
56
+ return
57
+ }
58
+ if surface.AnonymousDefault != nil {
59
+ ctx.Report(
60
+ surface.AnonymousDefault,
61
+ "An anonymous default export has no name for its file to take. Name the exported declaration, or export a named declaration instead of an anonymous default.",
62
+ )
63
+ return
64
+ }
65
+ if len(surface.Identities) == 0 {
66
+ return
67
+ }
68
+
69
+ base, extension := splitModuleBaseName(ctx.File.FileName())
70
+ if base == "index" {
71
+ return
72
+ }
73
+ identity := surface.Identities[0]
74
+ names := identity.matchable()
75
+ for _, name := range names {
76
+ if name == base {
77
+ return
78
+ }
79
+ }
80
+ ctx.Report(
81
+ identity.Node,
82
+ "A file takes the name of its public identity, but '"+base+extension+
83
+ "' declares '"+strings.Join(names, "' / '")+"'. Rename the file to '"+
84
+ names[0]+extension+"', or rename the identity to '"+base+"'.",
85
+ )
86
+ }
87
+
88
+ func init() { rule.Register(singularRule{}) }
89
+
90
+ // publicIdentity is one locally declared name that this file exposes.
91
+ //
92
+ // Addresses holds the public names a consumer can import, which is what the
93
+ // file name must match. It excludes `default`, which names no identity: a file
94
+ // whose only exposure is `export default x` is still the file of `x`.
95
+ type publicIdentity struct {
96
+ Local string
97
+ Addresses []string
98
+ Node *shimast.Node
99
+ }
100
+
101
+ func (identity publicIdentity) matchable() []string {
102
+ if len(identity.Addresses) != 0 {
103
+ return identity.Addresses
104
+ }
105
+ return []string{identity.Local}
106
+ }
107
+
108
+ type publicSurface struct {
109
+ Identities []publicIdentity
110
+ AnonymousDefault *shimast.Node
111
+ }
112
+
113
+ // collectPublicSurface reduces a source file's top-level statements to the
114
+ // identities it owns.
115
+ //
116
+ // Ownership is declaration ownership, never mere re-exposure. An export
117
+ // declaration carrying a module specifier declares nothing here, so a barrel
118
+ // owns no identity and needs no exemption; an export list or default assignment
119
+ // naming an import binding is the same case one step removed.
120
+ func collectPublicSurface(file *shimast.SourceFile) publicSurface {
121
+ surface := publicSurface{}
122
+ if file == nil || file.Statements == nil {
123
+ return surface
124
+ }
125
+ order := []string{}
126
+ nodes := map[string]*shimast.Node{}
127
+ addresses := map[string]map[string]bool{}
128
+ owned := map[string]bool{}
129
+
130
+ declare := func(name string, node *shimast.Node) {
131
+ if name == "" {
132
+ return
133
+ }
134
+ if _, exists := nodes[name]; !exists {
135
+ order = append(order, name)
136
+ nodes[name] = node
137
+ addresses[name] = map[string]bool{}
138
+ }
139
+ }
140
+ expose := func(local string, public string) {
141
+ if _, exists := nodes[local]; !exists {
142
+ return
143
+ }
144
+ owned[local] = true
145
+ if public != "" {
146
+ addresses[local][public] = true
147
+ }
148
+ }
149
+
150
+ for _, statement := range file.Statements.Nodes {
151
+ if statement == nil {
152
+ continue
153
+ }
154
+ for _, declared := range topLevelDeclaredNames(statement) {
155
+ declare(declared.Name, declared.Node)
156
+ }
157
+ }
158
+ for _, statement := range file.Statements.Nodes {
159
+ if statement == nil {
160
+ continue
161
+ }
162
+ switch statement.Kind {
163
+ case shimast.KindExportDeclaration:
164
+ for _, alias := range localExportAliases(statement) {
165
+ expose(alias.Local, alias.Public)
166
+ }
167
+ case shimast.KindExportAssignment:
168
+ assignment := statement.AsExportAssignment()
169
+ if assignment == nil {
170
+ continue
171
+ }
172
+ local := ""
173
+ if assignment.Expression != nil &&
174
+ assignment.Expression.Kind == shimast.KindIdentifier {
175
+ local = declarationName(assignment.Expression)
176
+ }
177
+ if local == "" || nodes[local] == nil {
178
+ // `export default 1` and `export default fromAnotherModule` both
179
+ // expose something this file cannot be named after.
180
+ if local == "" {
181
+ surface.AnonymousDefault = statement
182
+ }
183
+ continue
184
+ }
185
+ expose(local, "")
186
+ default:
187
+ if !isSyntacticallyExported(statement) {
188
+ continue
189
+ }
190
+ declared := topLevelDeclaredNames(statement)
191
+ for _, name := range declared {
192
+ expose(name.Name, name.Name)
193
+ }
194
+ if len(declared) == 0 && isDefaultExported(statement) {
195
+ surface.AnonymousDefault = statement
196
+ }
197
+ }
198
+ }
199
+
200
+ for _, local := range order {
201
+ if !owned[local] {
202
+ continue
203
+ }
204
+ public := make([]string, 0, len(addresses[local]))
205
+ for name := range addresses[local] {
206
+ public = append(public, name)
207
+ }
208
+ sort.Strings(public)
209
+ surface.Identities = append(surface.Identities, publicIdentity{
210
+ Local: local,
211
+ Addresses: public,
212
+ Node: nodes[local],
213
+ })
214
+ }
215
+ return surface
216
+ }
217
+
218
+ // declaredName pairs a module-scope name with the identifier that spells it.
219
+ //
220
+ // The identifier is kept rather than the statement because both the reported
221
+ // range and the reported line come from it. A statement's own position starts
222
+ // before its leading trivia, so anchoring there underlines the blank line above
223
+ // the declaration and names the wrong line in the message.
224
+ type declaredName struct {
225
+ Name string
226
+ Node *shimast.Node
227
+ }
228
+
229
+ // topLevelDeclaredNames returns the names a statement declares in module scope.
230
+ //
231
+ // Only identifier names qualify. A module declaration named by a string literal
232
+ // is an augmentation of another module rather than an identity of this one, and
233
+ // counting it would report every ambient declaration file as an extra identity.
234
+ func topLevelDeclaredNames(statement *shimast.Node) []declaredName {
235
+ switch statement.Kind {
236
+ case shimast.KindInterfaceDeclaration,
237
+ shimast.KindTypeAliasDeclaration,
238
+ shimast.KindFunctionDeclaration,
239
+ shimast.KindClassDeclaration,
240
+ shimast.KindEnumDeclaration,
241
+ shimast.KindModuleDeclaration:
242
+ name := statement.Name()
243
+ if name == nil || name.Kind != shimast.KindIdentifier {
244
+ return nil
245
+ }
246
+ if declared := declarationName(name); declared != "" {
247
+ return []declaredName{{Name: declared, Node: name}}
248
+ }
249
+ return nil
250
+ case shimast.KindVariableStatement:
251
+ variable := statement.AsVariableStatement()
252
+ if variable == nil || variable.DeclarationList == nil {
253
+ return nil
254
+ }
255
+ list := variable.DeclarationList.AsVariableDeclarationList()
256
+ if list == nil || list.Declarations == nil {
257
+ return nil
258
+ }
259
+ names := []declaredName{}
260
+ for _, declaration := range list.Declarations.Nodes {
261
+ if declaration == nil {
262
+ continue
263
+ }
264
+ for _, binding := range bindingIdentifierNodes(declaration.Name()) {
265
+ if name := declarationName(binding); name != "" {
266
+ names = append(names, declaredName{Name: name, Node: binding})
267
+ }
268
+ }
269
+ }
270
+ return names
271
+ default:
272
+ return nil
273
+ }
274
+ }
275
+
276
+ // exportAlias is one entry of an export list: the local declaration it names,
277
+ // and the public name it gives that declaration.
278
+ //
279
+ // Public is empty for `export { x as default }`, which exposes `x` without
280
+ // giving it an addressable name — the same state `export default x` produces.
281
+ type exportAlias struct {
282
+ Local string
283
+ Public string
284
+ }
285
+
286
+ // localExportAliases lists the exposures an export list creates, skipping any
287
+ // list that re-exports from another module.
288
+ //
289
+ // The result is a slice rather than a local-keyed map because one local may be
290
+ // exported under several names. Collapsing those to one would drop every name
291
+ // but the last, and a file legitimately named after a dropped name would then
292
+ // be reported.
293
+ func localExportAliases(statement *shimast.Node) []exportAlias {
294
+ aliases := []exportAlias{}
295
+ declaration := statement.AsExportDeclaration()
296
+ if declaration == nil ||
297
+ declaration.ModuleSpecifier != nil ||
298
+ declaration.ExportClause == nil ||
299
+ declaration.ExportClause.Kind != shimast.KindNamedExports {
300
+ return aliases
301
+ }
302
+ named := declaration.ExportClause.AsNamedExports()
303
+ if named == nil || named.Elements == nil {
304
+ return aliases
305
+ }
306
+ for _, element := range named.Elements.Nodes {
307
+ if element == nil || element.Kind != shimast.KindExportSpecifier {
308
+ continue
309
+ }
310
+ specifier := element.AsExportSpecifier()
311
+ if specifier == nil {
312
+ continue
313
+ }
314
+ localNode := specifier.PropertyName
315
+ if localNode == nil {
316
+ localNode = specifier.Name()
317
+ }
318
+ local := declarationName(localNode)
319
+ public := declarationName(specifier.Name())
320
+ if local == "" || public == "" {
321
+ continue
322
+ }
323
+ if public == "default" {
324
+ public = ""
325
+ }
326
+ aliases = append(aliases, exportAlias{Local: local, Public: public})
327
+ }
328
+ return aliases
329
+ }
330
+
331
+ func isDefaultExported(node *shimast.Node) bool {
332
+ return node != nil &&
333
+ shimast.GetCombinedModifierFlags(node)&shimast.ModifierFlagsDefault != 0
334
+ }
335
+
336
+ // splitModuleBaseName separates a module's base name from its final extension.
337
+ //
338
+ // Only the final extension is removed. A dotted infix such as `foo.test.ts`
339
+ // keeps `foo.test`, which no identifier can equal, so such a file is reported
340
+ // rather than silently matched against a prefix of itself.
341
+ func splitModuleBaseName(fileName string) (string, string) {
342
+ base := path.Base(strings.ReplaceAll(fileName, "\\", "/"))
343
+ extension := path.Ext(base)
344
+ return strings.TrimSuffix(base, extension), extension
345
+ }
346
+
347
+ func describeIdentities(identities []publicIdentity, content string) string {
348
+ described := make([]string, 0, len(identities))
349
+ for _, identity := range identities {
350
+ described = append(
351
+ described,
352
+ "'"+strings.Join(identity.matchable(), "' / '")+"' (line "+
353
+ decimal(lineAt(content, identityOffset(identity.Node)))+")",
354
+ )
355
+ }
356
+ return strings.Join(described, ", ")
357
+ }
358
+
359
+ // identityOffset points at the last character of a declared name.
360
+ //
361
+ // A node's own start position sits before its leading trivia, so the end is
362
+ // what keeps a reported line on the declaration rather than on the blank line
363
+ // above it.
364
+ func identityOffset(node *shimast.Node) int {
365
+ if node == nil || node.End() <= 0 {
366
+ return 0
367
+ }
368
+ return node.End() - 1
369
+ }
@@ -0,0 +1,331 @@
1
+ package evidence
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "net/url"
9
+ "os"
10
+ "os/exec"
11
+ "sort"
12
+ "strings"
13
+ "time"
14
+ )
15
+
16
+ const swaggerBridgeTimeout = 60 * time.Second
17
+ const swaggerBridgeOutputLimit = 64 * 1024 * 1024
18
+ const swaggerBridgeErrorLimit = 64 * 1024
19
+
20
+ const swaggerBridgeScript = `
21
+ const path = require("node:path");
22
+ const { createRequire } = require("node:module");
23
+
24
+ const root = process.argv[1];
25
+ const projectRequire = createRequire(path.join(root, "package.json"));
26
+ const manifest = projectRequire.resolve("@ttsc/evidence/package.json");
27
+ const pluginRequire = createRequire(manifest);
28
+ const normalizer = pluginRequire(
29
+ path.join(path.dirname(manifest), "lib", "internal", "loadSwaggerOperations.js"),
30
+ );
31
+
32
+ let input = "";
33
+ process.stdin.setEncoding("utf8");
34
+ process.stdin.on("data", (chunk) => { input += chunk; });
35
+ process.stdin.on("end", async () => {
36
+ try {
37
+ const result = await normalizer.loadSwaggerOperations(JSON.parse(input));
38
+ process.stdout.write(JSON.stringify(result));
39
+ } catch (error) {
40
+ process.stderr.write(error instanceof Error ? error.message : String(error));
41
+ process.exitCode = 1;
42
+ }
43
+ });
44
+ `
45
+
46
+ type swaggerNormalizationRequest struct {
47
+ Root string `json:"root"`
48
+ Sources []string `json:"sources"`
49
+ }
50
+
51
+ type swaggerNormalizationResult struct {
52
+ Documents []swaggerDocumentInventory `json:"documents"`
53
+ Problems []swaggerDocumentProblem `json:"problems"`
54
+ }
55
+
56
+ // swaggerDocumentInventory is one normalized document.
57
+ //
58
+ // Digest is the SHA-256 of the bytes the normalizer itself read, and it is what
59
+ // the result is remembered under. Hashing here rather than trusting the digest
60
+ // this process took beforehand is what makes the key exact: the normalizer runs
61
+ // in another process and opens the file again, so a write landing between the
62
+ // two reads would otherwise bind one document's operations to another
63
+ // document's bytes — an entry that answers a later cycle with the wrong
64
+ // document, forever.
65
+ //
66
+ // It is empty for a remote source, which never participates in reuse.
67
+ type swaggerDocumentInventory struct {
68
+ Source string `json:"source"`
69
+ Operations []swaggerOperation `json:"operations"`
70
+ Digest string `json:"digest"`
71
+ }
72
+
73
+ // swaggerDocumentProblem is one source the normalizer refused. Digest carries
74
+ // the same meaning as on an inventory, so a document that cannot be normalized
75
+ // is remembered as a failure rather than re-normalized on every later cycle.
76
+ type swaggerDocumentProblem struct {
77
+ Source string `json:"source"`
78
+ Message string `json:"message"`
79
+ Digest string `json:"digest"`
80
+ }
81
+
82
+ type swaggerOperation struct {
83
+ Method string `json:"method"`
84
+ Path string `json:"path"`
85
+ }
86
+
87
+ func loadSwaggerInventories(
88
+ root string,
89
+ config graphConfig,
90
+ ) (map[string]*artifactInventory, []string) {
91
+ sources := configuredSwaggerSources(config)
92
+ inventories := map[string]*artifactInventory{}
93
+ for _, source := range sources {
94
+ inventories[source] = &artifactInventory{
95
+ Path: source,
96
+ Type: artifactSwagger,
97
+ }
98
+ }
99
+ if len(sources) == 0 {
100
+ return inventories, nil
101
+ }
102
+
103
+ // Normalizing a document costs a Node process, and the process start
104
+ // dominates the parse — a three-operation document and a
105
+ // two-hundred-operation one pay nearly the same. A resident host repeats
106
+ // this every cycle, so an unchanged document is re-normalized on every
107
+ // TypeScript keystroke that triggers a rebuild.
108
+ digests := swaggerContentDigests(root, sources)
109
+ pending := []string{}
110
+ problems := []string{}
111
+ for _, source := range sources {
112
+ outcome, hit := lookupSwaggerDocument(source, digests[source])
113
+ if !hit {
114
+ pending = append(pending, source)
115
+ continue
116
+ }
117
+ problems = append(
118
+ problems,
119
+ swaggerUnitsFromOutcome(source, inventories[source], outcome)...,
120
+ )
121
+ }
122
+ if len(pending) == 0 {
123
+ return inventories, problems
124
+ }
125
+
126
+ result, err := normalizeSwaggerSources(root, pending)
127
+ if err != nil {
128
+ 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."
129
+ for _, source := range pending {
130
+ inventories[source].LoadFailed = true
131
+ inventories[source].Problems = append(
132
+ inventories[source].Problems,
133
+ inventoryProblem{Symbol: "operation", Message: message},
134
+ )
135
+ }
136
+ return inventories, append(problems, message)
137
+ }
138
+
139
+ seen := map[string]bool{}
140
+ for _, document := range result.Documents {
141
+ inventory := inventories[document.Source]
142
+ if inventory == nil {
143
+ problems = append(
144
+ problems,
145
+ "Evidence graph Swagger normalizer returned an unconfigured source '"+displaySwaggerSource(document.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
146
+ )
147
+ continue
148
+ }
149
+ if seen[document.Source] {
150
+ problems = append(
151
+ problems,
152
+ "Evidence graph Swagger normalizer returned source '"+displaySwaggerSource(document.Source)+"' more than once. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
153
+ )
154
+ continue
155
+ }
156
+ seen[document.Source] = true
157
+ outcome := swaggerDocumentOutcome{Operations: document.Operations}
158
+ problems = append(
159
+ problems,
160
+ swaggerUnitsFromOutcome(document.Source, inventory, outcome)...,
161
+ )
162
+ rememberSwaggerDocument(document.Source, document.Digest, outcome)
163
+ }
164
+ for _, problem := range result.Problems {
165
+ inventory := inventories[problem.Source]
166
+ if inventory == nil {
167
+ problems = append(
168
+ problems,
169
+ "Evidence graph Swagger normalizer rejected an unconfigured source '"+displaySwaggerSource(problem.Source)+"'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree.",
170
+ )
171
+ continue
172
+ }
173
+ seen[problem.Source] = true
174
+ outcome := swaggerDocumentOutcome{
175
+ Rejected: true,
176
+ Problem: problem.Message,
177
+ }
178
+ problems = append(
179
+ problems,
180
+ swaggerUnitsFromOutcome(problem.Source, inventory, outcome)...,
181
+ )
182
+ rememberSwaggerDocument(problem.Source, problem.Digest, outcome)
183
+ }
184
+ for _, source := range pending {
185
+ if seen[source] {
186
+ continue
187
+ }
188
+ message := "Evidence graph Swagger normalizer returned no result for '" + displaySwaggerSource(source) + "'. Reinstall @ttsc/evidence; the native and JavaScript bridge contracts disagree."
189
+ inventories[source].LoadFailed = true
190
+ inventories[source].Problems = append(
191
+ inventories[source].Problems,
192
+ inventoryProblem{Symbol: "operation", Message: message},
193
+ )
194
+ problems = append(problems, message)
195
+ }
196
+ return inventories, problems
197
+ }
198
+
199
+ func configuredSwaggerSources(config graphConfig) []string {
200
+ unique := map[string]bool{}
201
+ sources := []string{}
202
+ for _, claim := range config.Claims {
203
+ for _, reference := range claim.References {
204
+ if reference.Type != artifactSwagger {
205
+ continue
206
+ }
207
+ if unique[reference.Source] {
208
+ continue
209
+ }
210
+ unique[reference.Source] = true
211
+ sources = append(sources, reference.Source)
212
+ }
213
+ }
214
+ sort.Strings(sources)
215
+ return sources
216
+ }
217
+
218
+ func normalizeSwaggerSources(
219
+ root string,
220
+ sources []string,
221
+ ) (swaggerNormalizationResult, error) {
222
+ request, err := json.Marshal(swaggerNormalizationRequest{
223
+ Root: root,
224
+ Sources: sources,
225
+ })
226
+ if err != nil {
227
+ return swaggerNormalizationResult{}, err
228
+ }
229
+ node := os.Getenv("TTSC_NODE_BINARY")
230
+ if node == "" {
231
+ node, err = exec.LookPath("node")
232
+ if err != nil {
233
+ return swaggerNormalizationResult{}, errors.New("Node.js executable was not found")
234
+ }
235
+ }
236
+ ctx, cancel := context.WithTimeout(context.Background(), swaggerBridgeTimeout)
237
+ defer cancel()
238
+ command := exec.CommandContext(ctx, node, "-e", swaggerBridgeScript, root)
239
+ command.Dir = root
240
+ command.Stdin = bytes.NewReader(request)
241
+ stdout := &limitedBuffer{Limit: swaggerBridgeOutputLimit}
242
+ stderr := &limitedBuffer{Limit: swaggerBridgeErrorLimit}
243
+ command.Stdout = stdout
244
+ command.Stderr = stderr
245
+ if err := command.Run(); err != nil {
246
+ if ctx.Err() == context.DeadlineExceeded {
247
+ return swaggerNormalizationResult{}, errors.New("Swagger normalizer exceeded its 60 second timeout")
248
+ }
249
+ detail := strings.TrimSpace(stderr.String())
250
+ if detail == "" {
251
+ detail = err.Error()
252
+ }
253
+ return swaggerNormalizationResult{}, errors.New(detail)
254
+ }
255
+ var result swaggerNormalizationResult
256
+ if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
257
+ return swaggerNormalizationResult{}, errors.New("Swagger normalizer returned invalid JSON: " + err.Error())
258
+ }
259
+ return result, nil
260
+ }
261
+
262
+ func swaggerOperationUnit(
263
+ source string,
264
+ operation swaggerOperation,
265
+ ) (*evidenceUnit, string) {
266
+ method := strings.TrimSpace(operation.Method)
267
+ operationPath := operation.Path
268
+ if method == "" || strings.ContainsAny(method, ":\t\r\n ") {
269
+ return nil, "Swagger source '" + displaySwaggerSource(source) + "' contains an operation method that cannot form a '<METHOD>:<path>' evidence target."
270
+ }
271
+ if !strings.HasPrefix(operationPath, "/") || containsWhitespace(operationPath) {
272
+ return nil, "Swagger source '" + displaySwaggerSource(source) + "' contains operation path '" + operationPath + "', which cannot form a whitespace-free '<METHOD>:<path>' evidence target."
273
+ }
274
+ target := strings.ToUpper(method) + ":" + operationPath
275
+ readable := "Swagger operation '" + strings.ToUpper(method) + " " + operationPath + "'"
276
+ return &evidenceUnit{
277
+ ID: "swagger:" + source + ":" + target,
278
+ Target: target,
279
+ Type: artifactSwagger,
280
+ Symbol: "operation",
281
+ Path: displaySwaggerSource(source),
282
+ Readable: readable,
283
+ }, ""
284
+ }
285
+
286
+ // isRemoteSwaggerSource reports whether a normalized Swagger source names a URL
287
+ // rather than a project-relative file.
288
+ //
289
+ // Two callers depend on it and both fail quietly when it is wrong: a remote
290
+ // source declared as a project input is rejected by the host and takes the
291
+ // whole snapshot down with it, and a remote source admitted to the content
292
+ // cache would be answered from bytes that were never fetched.
293
+ //
294
+ // The scheme is read through `url.Parse` rather than matched as a prefix
295
+ // because `normalizeSwaggerSource` stores the author's spelling, and a scheme
296
+ // is case-insensitive — `HTTPS://host/s.json` is accepted there and would slip
297
+ // past a literal `https://` comparison.
298
+ func isRemoteSwaggerSource(source string) bool {
299
+ parsed, err := url.Parse(source)
300
+ return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https")
301
+ }
302
+
303
+ func displaySwaggerSource(source string) string {
304
+ parsed, err := url.Parse(source)
305
+ if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
306
+ return source
307
+ }
308
+ if parsed.User != nil {
309
+ parsed.User = url.User("***")
310
+ }
311
+ if parsed.RawQuery != "" {
312
+ parsed.RawQuery = "<redacted>"
313
+ }
314
+ return parsed.String()
315
+ }
316
+
317
+ type limitedBuffer struct {
318
+ bytes.Buffer
319
+ Limit int
320
+ }
321
+
322
+ func (buffer *limitedBuffer) Write(content []byte) (int, error) {
323
+ if buffer.Len()+len(content) > buffer.Limit {
324
+ remaining := buffer.Limit - buffer.Len()
325
+ if remaining > 0 {
326
+ _, _ = buffer.Buffer.Write(content[:remaining])
327
+ }
328
+ return len(content), errors.New("process output exceeded its limit")
329
+ }
330
+ return buffer.Buffer.Write(content)
331
+ }