@shortlink-org/portolan 0.2.4 → 0.4.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 (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +28 -15
  35. package/plugin/describe.go +156 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. package/src/lib/wire-problems.ts +0 -342
@@ -1,237 +0,0 @@
1
- // Where a call and the interface it names disagree.
2
- //
3
- // docs/adr/org.0001.md promises this: producers publish their schema, consumers
4
- // keep a narrowed copy, and "a field, method or enum value that differs between
5
- // the two is reported against the consuming service". The extractor retains
6
- // those copies on the consumer; this reader compares them after every source
7
- // has met in the merged catalog.
8
- //
9
- // It is deliberately NOT done in the merge. src/merge.ts is documented as
10
- // union-by-id plus first-non-empty and nothing else, and teaching it to compare
11
- // shapes would put a semantic judgement in the one place that has none. The
12
- // comparison belongs here, over the merged catalog, on the page where the rest
13
- // of that judgement already lives.
14
- //
15
- // This is also NOT the same finding as derive.ts's `rpc` problem. That one is
16
- // about the PEER: a call whose other end is nobody the catalog knows. This one
17
- // is about a call whose peer is known and whose METHOD is not - the copy is
18
- // stale, or the producer removed something, and the two are different repairs.
19
-
20
- import type {
21
- Catalog,
22
- CatalogIndex,
23
- RpcEnum,
24
- RpcMessage,
25
- RpcMethod,
26
- RpcService,
27
- Service,
28
- } from "../catalog";
29
- import type { Problem } from "./derive";
30
-
31
- /**
32
- * Every missing method and every retained consumer descriptor that disagrees
33
- * with the provider's method, field, or enum value.
34
- *
35
- * An unresolved call is skipped: it already has a problem of its own from
36
- * `problems()`, and reporting the same edge twice under two headings would make
37
- * the page look worse than the estate is.
38
- */
39
- export function protoProblems(
40
- catalog: Catalog,
41
- index: CatalogIndex,
42
- ): Problem[] {
43
- const out: Problem[] = [];
44
-
45
- for (const context of catalog.contexts) {
46
- for (const service of context.services) {
47
- for (const call of service.consumes) {
48
- if (call.status === "unresolved") continue;
49
-
50
- // The peer has to be a service in the catalog before the method is
51
- // worth asking about. A call to something outside the estate is a fact,
52
- // not a defect.
53
- if (!index.serviceById.has(call.peer)) continue;
54
-
55
- if (index.rpcProviderByMethod.has(call.id)) continue;
56
-
57
- out.push({
58
- kind: "proto-missing",
59
- // An error, not a staleness warning: the peer is in the catalog and
60
- // does not answer on this. Either the copy is behind or the method is
61
- // gone, and both are a call that will fail.
62
- severity: "error",
63
- context: context.id,
64
- service: service.id,
65
- id: call.id,
66
- peer: call.peer,
67
- note: call.note,
68
- source: call.source,
69
- });
70
- }
71
-
72
- for (const copy of service.copies ?? []) {
73
- const drift = driftAgainstProvider(service, copy, index);
74
- if (drift) out.push(drift);
75
- }
76
- }
77
- }
78
-
79
- return out;
80
- }
81
-
82
- /** Compare one consumer copy only when one of its calls resolves to a provider. */
83
- function driftAgainstProvider(
84
- consumer: Service,
85
- copy: RpcService,
86
- index: CatalogIndex,
87
- ): Problem | undefined {
88
- const calls = new Map(
89
- consumer.consumes
90
- .filter((call) => call.id.startsWith(`${copy.id}/`))
91
- .map((call) => [call.id, call]),
92
- );
93
- const resolved = copy.methods
94
- .map((method) => calls.get(`${copy.id}/${method.name}`))
95
- .find(
96
- (call) =>
97
- call?.status !== "unresolved" && index.serviceById.has(call?.peer ?? ""),
98
- );
99
- if (!resolved) return undefined;
100
-
101
- const providerService = index.serviceById.get(resolved.peer);
102
- const provider = providerService?.provides.find(
103
- (candidate) => candidate.id === copy.id,
104
- );
105
- if (!providerService || !provider) return undefined;
106
-
107
- const differences = compareInterfaces(copy, provider);
108
- if (differences.length === 0) return undefined;
109
-
110
- return {
111
- kind: "proto-drift",
112
- severity: "warning",
113
- context: index.serviceContext.get(consumer.id)?.id ?? "",
114
- service: consumer.id,
115
- id: copy.id,
116
- peer: providerService.id,
117
- note: `Vendored copy differs from ${providerService.id}: ${differences.join("; ")}.`,
118
- source: copy.source,
119
- };
120
- }
121
-
122
- function compareInterfaces(copy: RpcService, provider: RpcService): string[] {
123
- const out: string[] = [];
124
- const methods = new Map(provider.methods.map((method) => [method.name, method]));
125
-
126
- for (const method of copy.methods) {
127
- const actual = methods.get(method.name);
128
- // A missing method already has the stronger proto-missing error.
129
- if (!actual) continue;
130
- compareMethod(method, actual, out);
131
- }
132
-
133
- const messages = new Map(
134
- (provider.messages ?? []).map((message) => [message.name, message]),
135
- );
136
- for (const message of copy.messages ?? []) {
137
- compareMessage(message, messages.get(message.name), out);
138
- }
139
-
140
- const enums = new Map(
141
- (provider.enums ?? []).map((item) => [item.name, item]),
142
- );
143
- for (const item of copy.enums ?? []) {
144
- compareEnum(item, enums.get(item.name), out);
145
- }
146
-
147
- return out;
148
- }
149
-
150
- function compareMethod(
151
- copy: RpcMethod,
152
- provider: RpcMethod,
153
- out: string[],
154
- ): void {
155
- for (const side of ["request", "response"] as const) {
156
- if (copy[side] !== provider[side]) {
157
- out.push(
158
- `${copy.name} ${side} is ${copy[side] || "unnamed"}, provider has ${provider[side] || "unnamed"}`,
159
- );
160
- }
161
- }
162
- if (copy.streaming !== provider.streaming) {
163
- out.push(
164
- `${copy.name} streaming is ${copy.streaming ?? "unary"}, provider has ${provider.streaming ?? "unary"}`,
165
- );
166
- }
167
- }
168
-
169
- /**
170
- * A narrowed copy may omit provider fields it does not read. Every field it
171
- * does carry must still have the provider's type and protobuf number.
172
- */
173
- function compareMessage(
174
- copy: RpcMessage,
175
- provider: RpcMessage | undefined,
176
- out: string[],
177
- ): void {
178
- if (!provider) {
179
- out.push(`message ${copy.name} is absent from the provider`);
180
- return;
181
- }
182
-
183
- const fields = new Map(provider.fields.map((field) => [field.name, field]));
184
- for (const field of copy.fields) {
185
- const actual = fields.get(field.name);
186
- if (!actual) {
187
- out.push(`${copy.name}.${field.name} is absent from the provider`);
188
- continue;
189
- }
190
- if (field.type !== actual.type) {
191
- out.push(
192
- `${copy.name}.${field.name} is ${field.type}, provider has ${actual.type}`,
193
- );
194
- }
195
- if (
196
- field.number !== undefined &&
197
- actual.number !== undefined &&
198
- field.number !== actual.number
199
- ) {
200
- out.push(
201
- `${copy.name}.${field.name} is field ${field.number}, provider has ${actual.number}`,
202
- );
203
- }
204
- }
205
- }
206
-
207
- /** Enum names and numbers are both wire claims, so the sets must match whole. */
208
- function compareEnum(
209
- copy: RpcEnum,
210
- provider: RpcEnum | undefined,
211
- out: string[],
212
- ): void {
213
- if (!provider) {
214
- out.push(`enum ${copy.name} is absent from the provider`);
215
- return;
216
- }
217
-
218
- const copyByName = new Map(copy.values.map((value) => [value.name, value]));
219
- const providerByName = new Map(
220
- provider.values.map((value) => [value.name, value]),
221
- );
222
- for (const value of copy.values) {
223
- const actual = providerByName.get(value.name);
224
- if (!actual) {
225
- out.push(`${copy.name}.${value.name} is absent from the provider`);
226
- } else if (value.number !== actual.number) {
227
- out.push(
228
- `${copy.name}.${value.name} is ${value.number}, provider has ${actual.number}`,
229
- );
230
- }
231
- }
232
- for (const value of provider.values) {
233
- if (!copyByName.has(value.name)) {
234
- out.push(`${copy.name}.${value.name} is missing from the copy`);
235
- }
236
- }
237
- }
@@ -1,342 +0,0 @@
1
- // Where two services meet on one channel, and where a service's document and
2
- // its code do not tell the same story about the bus.
3
- //
4
- // A channel belongs to the service that publishes on it, the way a store
5
- // belongs to the service that writes it: a subscriber to `shop.cart.basket`
6
- // is told, by the name, whose events it is reading and whose schema to vendor.
7
- // A second publisher on the same channel breaks that promise silently - the
8
- // subscriber dispatches on the event's name and never notices the message
9
- // came from somewhere else, until one arrives that it cannot parse.
10
- //
11
- // Two sources say what a service puts on the bus, and they are read out of
12
- // different files by different extractors. The domain says an aggregate raises
13
- // an event and how it leaves, in `wire`. An AsyncAPI document says the service
14
- // declares a channel and what travels on it, in `channels`. Neither is the
15
- // truth on its own - a document is a promise and a wire is what the code does -
16
- // and where they disagree is a fact worth a row, because one of them is stale.
17
- //
18
- // This is the messaging half of `shared-store`. It is done here, over the
19
- // merged catalog, for the same reason the store rule is: each extractor sees
20
- // one repository and cannot know what another declares.
21
-
22
- import type { Catalog, CatalogIndex, ChannelMessage, Event, Service } from "../catalog";
23
- import type { Problem } from "./derive";
24
-
25
- interface Publisher {
26
- service: Service;
27
- context: string;
28
- /** The events of this service whose wire names the channel. */
29
- events: Event[];
30
- /** Whether the service's own document declares it sends here. */
31
- declared: boolean;
32
- }
33
-
34
- export function wireProblems(
35
- catalog: Catalog,
36
- index: CatalogIndex,
37
- ): Problem[] {
38
- return [
39
- ...sharedChannels(catalog),
40
- ...documentAgainstCode(catalog),
41
- ...messageEncodingMismatches(catalog),
42
- ...unresolvedSubscriptions(catalog, index),
43
- ];
44
- }
45
-
46
- interface MessageEndpoint {
47
- context: string;
48
- service: Service;
49
- address: string;
50
- source?: string;
51
- message: ChannelMessage;
52
- }
53
-
54
- /** Producer and subscriber have both named a format, and those formats differ. */
55
- function messageEncodingMismatches(catalog: Catalog): Problem[] {
56
- const sends = new Map<string, MessageEndpoint[]>();
57
- const receives: MessageEndpoint[] = [];
58
- for (const context of catalog.contexts) {
59
- for (const service of context.services) {
60
- for (const channel of service.channels ?? []) {
61
- for (const message of channel.messages) {
62
- const endpoint = { context: context.id, service, address: channel.address, source: channel.source, message };
63
- const key = `${channel.address}\u0000${message.name}`;
64
- if (message.direction === "send") {
65
- const publishers = sends.get(key) ?? [];
66
- publishers.push(endpoint);
67
- sends.set(key, publishers);
68
- } else {
69
- receives.push(endpoint);
70
- }
71
- }
72
- }
73
- }
74
- }
75
-
76
- const out: Problem[] = [];
77
- for (const receiver of receives) {
78
- const expected = wireFormat(receiver.message);
79
- if (!expected) continue;
80
- const key = `${receiver.address}\u0000${receiver.message.name}`;
81
- for (const publisher of sends.get(key) ?? []) {
82
- const actual = wireFormat(publisher.message);
83
- if (!actual || actual === expected) continue;
84
- out.push({
85
- kind: "message-encoding",
86
- severity: "error",
87
- context: receiver.context,
88
- service: receiver.service.id,
89
- id: receiver.service.id,
90
- peer: publisher.service.id,
91
- note: `${receiver.service.id} expects ${receiver.message.name} on ${receiver.address} as ${expected}, but ${publisher.service.id} sends it as ${actual}.`,
92
- source: receiver.source,
93
- });
94
- }
95
- }
96
- return out;
97
- }
98
-
99
- function wireFormat(message: ChannelMessage): string {
100
- if (message.encoding) return message.encoding.trim().toLowerCase();
101
- const contentType = message.contentType?.split(";", 1)[0]?.trim().toLowerCase() ?? "";
102
- if (contentType.includes("msgpack") || contentType.includes("messagepack")) return "msgpack";
103
- return contentType;
104
- }
105
-
106
- /**
107
- * Every service that publishes on a channel another service publishes on
108
- * too: one row per service per such channel, pointing at the other side.
109
- *
110
- * Publishing is either of the two claims - an event whose wire names the
111
- * channel, or a document declaring a send on it - because both are the service
112
- * saying it owns the channel, and a document that says so while the code has
113
- * not caught up is the same collision one release earlier.
114
- *
115
- * A service with several aggregates on one channel is not this: the channel
116
- * is still one service's. An event without a wire is skipped, because a
117
- * channel nobody named cannot be shared.
118
- */
119
- function sharedChannels(catalog: Catalog): Problem[] {
120
- const byChannel = new Map<string, Map<string, Publisher>>();
121
-
122
- const publisher = (
123
- channel: string,
124
- service: Service,
125
- context: string,
126
- ): Publisher => {
127
- const publishers = byChannel.get(channel) ?? new Map<string, Publisher>();
128
- byChannel.set(channel, publishers);
129
- const mine = publishers.get(service.id) ?? {
130
- service,
131
- context,
132
- events: [],
133
- declared: false,
134
- };
135
- publishers.set(service.id, mine);
136
-
137
- return mine;
138
- };
139
-
140
- for (const context of catalog.contexts) {
141
- for (const service of context.services) {
142
- for (const aggregate of service.aggregates) {
143
- for (const event of aggregate.events) {
144
- const channel = event.wire?.channel;
145
- if (!channel) continue;
146
- publisher(channel, service, context.id).events.push(event);
147
- }
148
- }
149
-
150
- for (const channel of service.channels ?? []) {
151
- if (channel.kind && channel.kind !== "event") continue;
152
- if (!sends(channel.messages)) continue;
153
- publisher(channel.address, service, context.id).declared = true;
154
- }
155
- }
156
- }
157
-
158
- const out: Problem[] = [];
159
- for (const [channel, publishers] of byChannel) {
160
- if (publishers.size < 2) continue;
161
- for (const mine of publishers.values()) {
162
- for (const theirs of publishers.values()) {
163
- if (theirs === mine) continue;
164
- const [first] = mine.events;
165
- out.push({
166
- kind: "shared-channel",
167
- // An error, like a second writer in a database: a subscriber vendors
168
- // one publisher's schema, and a message from the other is one it
169
- // will fail on, not one it will merely misfile.
170
- severity: "error",
171
- context: mine.context,
172
- service: mine.service.id,
173
- // The near end is one event when this side has one, so the row leads
174
- // to a page; a side known only from its document leads to the
175
- // service, which is the only page there is.
176
- id: first?.id ?? mine.service.id,
177
- peer: theirs.service.id,
178
- note: `${mine.service.id} publishes on ${channel}, and so does ${theirs.service.id}: ${claim(theirs, channel)}. A subscriber reads both without being told.`,
179
- source: first?.versions[0]?.source ?? sourceOf(mine.service, channel),
180
- });
181
- }
182
- }
183
- }
184
-
185
- return out;
186
- }
187
-
188
- /** What a publisher is putting on a channel, as the catalog knows it. */
189
- function claim(publisher: Publisher, channel: string): string {
190
- if (publisher.events.length > 0) {
191
- return publisher.events.map((e) => e.name).join(", ");
192
- }
193
-
194
- const declared = publisher.service.channels?.find(
195
- (c) => c.address === channel,
196
- );
197
-
198
- return (
199
- declared?.messages
200
- .filter((m) => m.direction === "send")
201
- .map((m) => m.name)
202
- .join(", ") ?? "nothing the catalog can name"
203
- );
204
- }
205
-
206
- /**
207
- * Where a service's document and its own code disagree about the bus.
208
- *
209
- * Both directions are a warning rather than an error, and for the same reason:
210
- * neither claim is wrong on its own. A channel the code publishes on and the
211
- * document does not declare is a document that has fallen behind - or an event
212
- * going out where nobody agreed it would. A channel the document declares and
213
- * no event names is a promise nothing keeps yet.
214
- *
215
- * A service with no document says nothing here. Silence is not a disagreement,
216
- * and a rule that fired on every service without an AsyncAPI file would be a
217
- * page of rows about work nobody has started.
218
- */
219
- function documentAgainstCode(catalog: Catalog): Problem[] {
220
- const out: Problem[] = [];
221
-
222
- for (const context of catalog.contexts) {
223
- for (const service of context.services) {
224
- const channels = service.channels ?? [];
225
- const declared = new Set(
226
- channels
227
- .filter((c) => (!c.kind || c.kind === "event") && sends(c.messages))
228
- .map((c) => c.address),
229
- );
230
- // Channels that only receive say nothing about what the service
231
- // sends: a subscription read out of the code is not a document of
232
- // the service's publications, and holding the events against it
233
- // would report every one of them.
234
- if (declared.size === 0) continue;
235
- const published = new Map<string, Event>();
236
-
237
- for (const aggregate of service.aggregates) {
238
- for (const event of aggregate.events) {
239
- const channel = event.wire?.channel;
240
- if (!channel) continue;
241
- if (!published.has(channel)) published.set(channel, event);
242
- if (declared.has(channel)) continue;
243
-
244
- out.push({
245
- kind: "channel-undeclared",
246
- severity: "warning",
247
- context: context.id,
248
- service: service.id,
249
- id: event.id,
250
- peer: channel,
251
- note: `${event.name} goes out on ${channel}, which ${service.id} does not declare it sends on. A subscriber reading the document does not know this message exists.`,
252
- source: event.versions[0]?.source,
253
- });
254
- }
255
- }
256
-
257
- for (const channel of channels) {
258
- if (channel.kind && channel.kind !== "event") continue;
259
- if (!sends(channel.messages)) continue;
260
- if (published.has(channel.address)) continue;
261
-
262
- out.push({
263
- kind: "channel-unpublished",
264
- severity: "warning",
265
- context: context.id,
266
- service: service.id,
267
- id: service.id,
268
- peer: channel.address,
269
- note: `${service.id} declares it sends on ${channel.address}, and no event of it names that channel: ${channel.messages
270
- .filter((m) => m.direction === "send")
271
- .map((m) => m.name)
272
- .join(", ")}.`,
273
- source: channel.source,
274
- });
275
- }
276
- }
277
- }
278
-
279
- return out;
280
- }
281
-
282
- /**
283
- * A message a service listens for that nothing in the estate publishes.
284
- *
285
- * This is the only edge in the catalog that runs the other way. Everywhere else
286
- * the publisher names its consumers and a name that resolves to nobody is the
287
- * problem; here the subscriber names a message, and the estate is searched for
288
- * whoever puts it on the wire. A subscription that resolves is how two
289
- * repositories that never mention each other are found to be joined.
290
- */
291
- function unresolvedSubscriptions(
292
- catalog: Catalog,
293
- index: CatalogIndex,
294
- ): Problem[] {
295
- // Every name anything in the estate puts on the wire. The index knows the
296
- // events; a document declaring a send is the other half, and it counts for
297
- // the same reason it counts as publishing above - the code may simply not
298
- // have caught up with it yet.
299
- const published = new Set(index.eventByWireName.keys());
300
- for (const context of catalog.contexts) {
301
- for (const service of context.services) {
302
- for (const channel of service.channels ?? []) {
303
- for (const message of channel.messages) {
304
- if (message.direction === "send") published.add(message.name);
305
- }
306
- }
307
- }
308
- }
309
-
310
- const out: Problem[] = [];
311
- for (const context of catalog.contexts) {
312
- for (const service of context.services) {
313
- for (const channel of service.channels ?? []) {
314
- for (const message of channel.messages) {
315
- if (message.direction !== "receive") continue;
316
- if (published.has(message.name)) continue;
317
-
318
- out.push({
319
- kind: "subscription-unresolved",
320
- severity: "warning",
321
- context: context.id,
322
- service: service.id,
323
- id: service.id,
324
- peer: message.name,
325
- note: `${service.id} listens on ${channel.address} for ${message.name}, and nothing in the catalog publishes it. Either the publisher is outside the estate, or the name has drifted.`,
326
- source: channel.source,
327
- });
328
- }
329
- }
330
- }
331
- }
332
-
333
- return out;
334
- }
335
-
336
- function sends(messages: { direction: string }[]): boolean {
337
- return messages.some((m) => m.direction === "send");
338
- }
339
-
340
- function sourceOf(service: Service, channel: string): string | undefined {
341
- return service.channels?.find((c) => c.address === channel)?.source;
342
- }