@shortlink-org/portolan 0.3.0 → 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 (188) hide show
  1. package/README.md +151 -6
  2. package/catalog/model.go +147 -0
  3. package/catalog/roundtrip_test.go +7 -0
  4. package/cli/portolan.mjs +6 -1
  5. package/go.mod +7 -0
  6. package/go.sum +20 -6
  7. package/package.json +10 -9
  8. package/plugin/describe.go +38 -0
  9. package/plugins/README.md +296 -5
  10. package/plugins/extract-argocd/options.schema.json +44 -0
  11. package/plugins/extract-celery/main.py +1 -0
  12. package/plugins/extract-csharp-ddd/README.md +213 -0
  13. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  14. package/plugins/extract-django/README.md +13 -2
  15. package/plugins/extract-django/choices.py +47 -0
  16. package/plugins/extract-django/domain.py +2 -1
  17. package/plugins/extract-django/extract_test.py +13 -0
  18. package/plugins/extract-django/lifecycle.py +2 -28
  19. package/plugins/extract-django/main.py +1 -0
  20. package/plugins/extract-django/rules.py +154 -0
  21. package/plugins/extract-django/rules_test.py +158 -0
  22. package/plugins/extract-go-sqs/options.schema.json +21 -0
  23. package/plugins/extract-http-clients/describe.go +5 -4
  24. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  25. package/plugins/extract-k8s/options.schema.json +32 -0
  26. package/plugins/extract-laravel/Cargo.lock +962 -0
  27. package/plugins/extract-laravel/Cargo.toml +20 -0
  28. package/plugins/extract-laravel/README.md +200 -0
  29. package/plugins/extract-laravel/options.schema.json +68 -0
  30. package/plugins/extract-laravel/rustfmt.toml +1 -0
  31. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  32. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  33. package/plugins/extract-php-ddd/README.md +141 -0
  34. package/plugins/extract-php-ddd/options.schema.json +50 -0
  35. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  36. package/plugins/extract-python-kafka/main.py +1 -0
  37. package/plugins/extract-terraform/options.schema.json +26 -0
  38. package/plugins/extract-ts/main.ts +1 -0
  39. package/plugins/phpscan/Cargo.lock +953 -0
  40. package/plugins/phpscan/Cargo.toml +19 -0
  41. package/plugins/phpscan/rustfmt.toml +1 -0
  42. package/plugins/portolan-go.wasm +0 -0
  43. package/plugins/pyplugin/catalog.py +24 -1
  44. package/plugins/verify-otel/options.schema.json +12 -0
  45. package/portolan.json +130 -3
  46. package/rules/builtin.json +218 -0
  47. package/schema/portolan.schema.json +880 -13
  48. package/scripts/README.md +3 -1
  49. package/scripts/delivery-presets.mjs +36 -3
  50. package/scripts/gen-likec4.mjs +149 -16
  51. package/scripts/gen-likec4.test.mjs +96 -0
  52. package/scripts/gen.mjs +30 -3
  53. package/scripts/gitops-example.test.mjs +108 -0
  54. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  55. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  56. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  57. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  58. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  59. package/scripts/host-plugins/fetch-git.mjs +1 -0
  60. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  61. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  62. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  63. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  64. package/scripts/json-format.mjs +192 -0
  65. package/scripts/json-format.test.mjs +97 -0
  66. package/scripts/local-api.mjs +216 -8
  67. package/scripts/local-api.test.mjs +88 -2
  68. package/scripts/local-discovery.mjs +12 -0
  69. package/scripts/manifest.mjs +10 -1
  70. package/scripts/plugin-host.mjs +23 -1
  71. package/scripts/plugins-fresh.mjs +97 -0
  72. package/scripts/plugins-fresh.test.mjs +64 -0
  73. package/scripts/run-builtin.mjs +26 -5
  74. package/scripts/schema.mjs +138 -19
  75. package/scripts/trace-trials.mjs +176 -0
  76. package/scripts/trace-trials.test.mjs +142 -0
  77. package/scripts/warning-policy.mjs +12 -6
  78. package/scripts/warning-policy.test.mjs +40 -3
  79. package/src/app/Breadcrumbs.test.ts +3 -0
  80. package/src/app/Breadcrumbs.tsx +3 -0
  81. package/src/app/CatalogApp.tsx +2 -0
  82. package/src/app/SidebarFooter.tsx +20 -4
  83. package/src/catalog-index.ts +25 -0
  84. package/src/catalog-model.ts +246 -1
  85. package/src/catalog-validation.ts +104 -2
  86. package/src/catalog.test.ts +40 -1
  87. package/src/components/ChannelRows.test.tsx +76 -1
  88. package/src/components/ChannelRows.tsx +223 -60
  89. package/src/components/DeploymentRows.tsx +131 -0
  90. package/src/components/DocsLinks.test.tsx +27 -0
  91. package/src/components/DocsLinks.tsx +56 -0
  92. package/src/components/FieldTree.tsx +5 -2
  93. package/src/components/HTTPDestinationEvidence.tsx +1 -1
  94. package/src/components/PluginIcon.tsx +77 -0
  95. package/src/components/ProblemRow.tsx +173 -153
  96. package/src/components/RuleMarks.tsx +22 -0
  97. package/src/components/ShapeRows.tsx +24 -20
  98. package/src/data.ts +21 -0
  99. package/src/enrich.test.ts +127 -3
  100. package/src/enrich.ts +104 -6
  101. package/src/er/ErCanvas.tsx +218 -12
  102. package/src/er/GroupNode.tsx +57 -0
  103. package/src/er/StoreHeader.tsx +1 -0
  104. package/src/er/layout.test.ts +85 -2
  105. package/src/er/layout.ts +140 -5
  106. package/src/er/spec.test.ts +17 -0
  107. package/src/er/spec.ts +23 -10
  108. package/src/flow/Recordings.test.tsx +52 -0
  109. package/src/flow/Recordings.tsx +236 -0
  110. package/src/flow/StepDetail.tsx +53 -0
  111. package/src/flow/TraceTrial.tsx +419 -0
  112. package/src/flow/examples.test.ts +33 -0
  113. package/src/flow/examples.ts +37 -0
  114. package/src/flow/outline.test.ts +28 -0
  115. package/src/flow/outline.ts +7 -2
  116. package/src/flow/trace-trial-resume.test.ts +42 -0
  117. package/src/flow/trace-trial-resume.ts +74 -0
  118. package/src/graph/elk.ts +78 -0
  119. package/src/index.css +24 -0
  120. package/src/landing/LandingPage.tsx +15 -11
  121. package/src/landing/ProductTour.tsx +6 -6
  122. package/src/lib/all-problems.ts +27 -17
  123. package/src/lib/centrality.test.ts +251 -0
  124. package/src/lib/centrality.ts +232 -0
  125. package/src/lib/confluence.test.ts +37 -0
  126. package/src/lib/confluence.ts +41 -0
  127. package/src/lib/context-color.ts +1 -1
  128. package/src/lib/deployment-drift.ts +22 -0
  129. package/src/lib/derive.ts +43 -69
  130. package/src/lib/environments.test.ts +83 -0
  131. package/src/lib/environments.ts +57 -0
  132. package/src/lib/integration-url.test.ts +30 -0
  133. package/src/lib/integration-url.ts +63 -0
  134. package/src/lib/kafka-ui.ts +3 -45
  135. package/src/lib/local-api.ts +96 -2
  136. package/src/lib/notion.ts +13 -0
  137. package/src/lib/plugin-index.json +2765 -0
  138. package/src/lib/plugins.test.ts +68 -0
  139. package/src/lib/plugins.ts +264 -0
  140. package/src/lib/problem-flows.test.ts +61 -0
  141. package/src/lib/problem-flows.ts +78 -0
  142. package/src/lib/problem-rules-cel.d.mts +43 -0
  143. package/src/lib/problem-rules-cel.mjs +407 -0
  144. package/src/lib/problem-rules.test.ts +287 -0
  145. package/src/lib/problem-rules.ts +271 -0
  146. package/src/lib/problem-subjects.ts +737 -0
  147. package/src/lib/rule-entries.ts +39 -0
  148. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  149. package/src/lib/rules-deploy.test.ts +133 -0
  150. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  151. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  152. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  153. package/src/lib/rules.test.ts +51 -0
  154. package/src/lib/rules.ts +86 -0
  155. package/src/lib/shape.test.ts +32 -0
  156. package/src/lib/shape.ts +30 -6
  157. package/src/lib/tech.ts +16 -0
  158. package/src/lib/trace-project.test.ts +34 -0
  159. package/src/lib/trace-project.ts +50 -0
  160. package/src/lib/use-problems.ts +23 -0
  161. package/src/lib/warnings.test.ts +9 -0
  162. package/src/lib/warnings.ts +10 -10
  163. package/src/likec4/ids.test.ts +6 -2
  164. package/src/likec4/ids.ts +43 -0
  165. package/src/main.tsx +23 -0
  166. package/src/merge-deployments.test.ts +127 -0
  167. package/src/merge.test.ts +82 -0
  168. package/src/merge.ts +173 -9
  169. package/src/pages/AggregatePage.tsx +57 -7
  170. package/src/pages/ContextMap.tsx +45 -3
  171. package/src/pages/ContextPage.tsx +2 -0
  172. package/src/pages/EventPage.tsx +15 -5
  173. package/src/pages/FlowDetail.tsx +23 -2
  174. package/src/pages/GraphPage.tsx +40 -3
  175. package/src/pages/Overview.tsx +152 -12
  176. package/src/pages/PluginIndex.tsx +190 -0
  177. package/src/pages/Problems.tsx +396 -128
  178. package/src/pages/ServicePage.tsx +58 -2
  179. package/src/pages/Settings.tsx +28 -2
  180. package/src/pages/settings/AboutSettings.tsx +8 -1
  181. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  182. package/src/pages/settings/RecordingSettings.tsx +138 -0
  183. package/src/pages/settings/RulesSettings.tsx +825 -0
  184. package/src/routes.test.ts +9 -0
  185. package/src/routes.ts +22 -1
  186. package/src/lib/data-problems.ts +0 -314
  187. package/src/lib/proto-problems.ts +0 -237
  188. package/src/lib/wire-problems.ts +0 -342
@@ -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
- }