@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
@@ -7,6 +7,8 @@
7
7
  * bundle.
8
8
  */
9
9
 
10
+ import { djangoAggregateCandidates, djangoAggregateMessage } from "./django-aggregates.ts";
11
+
10
12
  export type SetupPhase = "extract" | "verify" | "generate";
11
13
 
12
14
  export interface SetupProject {
@@ -59,6 +61,24 @@ export interface SetupRunStep extends SetupStep {
59
61
  * fix; a failed step's error stays out, because an error may quote anything.
60
62
  */
61
63
  warnings: string[];
64
+ diagnostics: SetupDiagnostic[];
65
+ }
66
+
67
+ export type SetupDiagnosticSeverity = "error" | "warning" | "info";
68
+
69
+ export interface SetupDiagnostic {
70
+ aggregateCandidates?: import("./django-aggregates").DjangoAggregateCandidates;
71
+ plugin: string;
72
+ rule: string;
73
+ severity: SetupDiagnosticSeverity;
74
+ action: string;
75
+ message: string;
76
+ count: number;
77
+ project: string;
78
+ phase: SetupPhase;
79
+ ref?: string;
80
+ suppressed: boolean;
81
+ suppressionReason?: string;
62
82
  }
63
83
 
64
84
  export interface SetupRun {
@@ -249,6 +269,10 @@ function runStepFrom(
249
269
  .filter((warning): warning is string => typeof warning === "string" && warning.trim() !== "")
250
270
  .slice(0, MAX_WARNINGS)
251
271
  .map((warning) => warning.slice(0, MAX_WARNING_LENGTH));
272
+ const diagnostics = array(item.diagnostics)
273
+ .map((diagnostic) => diagnosticFrom(diagnostic, item.plugin as string, item.phase as SetupPhase))
274
+ .filter((diagnostic): diagnostic is SetupDiagnostic => diagnostic !== null)
275
+ .slice(0, MAX_WARNINGS);
252
276
 
253
277
  return {
254
278
  ordinal: item.ordinal,
@@ -267,6 +291,40 @@ function runStepFrom(
267
291
  changedCount: item.changedCount,
268
292
  files,
269
293
  warnings,
294
+ diagnostics,
295
+ };
296
+ }
297
+
298
+ function diagnosticFrom(value: unknown, plugin: string, phase: SetupPhase): SetupDiagnostic | null {
299
+ const item = record(value);
300
+ if (
301
+ item.plugin !== plugin || item.phase !== phase ||
302
+ typeof item.rule !== "string" || !/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(item.rule) ||
303
+ !["error", "warning", "info"].includes(item.severity as string) ||
304
+ typeof item.action !== "string" || item.action.trim() === "" ||
305
+ typeof item.message !== "string" || item.message.trim() === "" ||
306
+ !wholeNumber(item.count) || item.count < 1 ||
307
+ typeof item.project !== "string" ||
308
+ typeof item.suppressed !== "boolean"
309
+ ) return null;
310
+ const ref = typeof item.ref === "string" && item.ref.trim() !== "" ? item.ref.slice(0, MAX_WARNING_LENGTH) : undefined;
311
+ const suppressionReason = typeof item.suppressionReason === "string" && item.suppressionReason.trim() !== ""
312
+ ? item.suppressionReason.slice(0, MAX_WARNING_LENGTH)
313
+ : undefined;
314
+ if (item.suppressed && !suppressionReason) return null;
315
+ return {
316
+ plugin,
317
+ rule: item.rule,
318
+ severity: item.severity as SetupDiagnosticSeverity,
319
+ action: item.action.slice(0, MAX_WARNING_LENGTH),
320
+ message: djangoAggregateMessage(item.message).slice(0, MAX_WARNING_LENGTH),
321
+ ...(djangoAggregateCandidates(item.message) ? { aggregateCandidates: djangoAggregateCandidates(item.message)! } : {}),
322
+ count: item.count,
323
+ project: item.project.slice(0, MAX_WARNING_LENGTH),
324
+ phase,
325
+ ...(ref ? { ref } : {}),
326
+ suppressed: item.suppressed,
327
+ ...(suppressionReason ? { suppressionReason } : {}),
270
328
  };
271
329
  }
272
330
 
@@ -211,6 +211,38 @@ describe("schemaChanges", () => {
211
211
  expect(removed.map((f) => f.name)).toEqual(["gone"]);
212
212
  });
213
213
 
214
+ it("a field whose rules moved is changed, with the rules it had", () => {
215
+ const rules = {
216
+ id: "x.y.R",
217
+ versions: [
218
+ {
219
+ version: "v1",
220
+ fields: [
221
+ { name: "code", type: "string", doc: "", rules: [{ name: "min_len", value: "8" }] },
222
+ { name: "who", type: "string", doc: "" },
223
+ { name: "same", type: "string", doc: "", rules: [{ name: "in", value: "a, b" }, { name: "unique" }] },
224
+ { name: "both", type: "int32", doc: "", required: true },
225
+ ],
226
+ },
227
+ {
228
+ version: "v2",
229
+ fields: [
230
+ { name: "code", type: "string", doc: "", rules: [{ name: "min_len", value: "12" }] },
231
+ { name: "who", type: "string", doc: "", required: true, rules: [{ name: "format", value: "uuid" }] },
232
+ // The same rules in another order are the same rules.
233
+ { name: "same", type: "string", doc: "", rules: [{ name: "unique" }, { name: "in", value: "a, b" }] },
234
+ { name: "both", type: "int64", doc: "" },
235
+ ],
236
+ },
237
+ ],
238
+ } as unknown as Event;
239
+ const { byField } = schemaChanges(rules, "v2");
240
+ expect(byField.get("code")).toEqual({ change: "changed", rulesFrom: "≥ 8 chars" });
241
+ expect(byField.get("who")).toEqual({ change: "changed", rulesFrom: "" });
242
+ expect(byField.get("same")).toBeUndefined();
243
+ expect(byField.get("both")).toEqual({ change: "changed", from: "int32", rulesFrom: "required" });
244
+ });
245
+
214
246
  it("agrees with the fixture: OrderPlaced v2 added channel", () => {
215
247
  const placed = eventById("shop.oms.order.OrderPlaced");
216
248
  const { byField, removed } = schemaChanges(placed, "v2");
package/src/lib/shape.ts CHANGED
@@ -26,6 +26,7 @@ import type {
26
26
  } from "../catalog";
27
27
  import { aggregateBlocks, blockFields, enumsOf } from "../catalog";
28
28
  import type { DefUsage } from "./derive";
29
+ import { ruleMarks } from "./rules";
29
30
 
30
31
  /** How many of the base type a field holds. */
31
32
  export type Cardinality = "one" | "many" | "map";
@@ -336,13 +337,19 @@ export interface FieldChange {
336
337
  change: Change;
337
338
  /** The type the field had in the previous version, when it changed. */
338
339
  from?: string;
340
+ /**
341
+ * The rules the field had in the previous version, as the marks read -
342
+ * "required, ≤ 8 chars" - when this version changed them; "" when it had
343
+ * none. The row itself shows what they are now.
344
+ */
345
+ rulesFrom?: string;
339
346
  }
340
347
 
341
348
  /**
342
349
  * What a version did to the schema: the fields it added, the ones whose
343
- * type it changed, and - by name, since they are no longer in the version's
344
- * own list - the ones it dropped. The first version changes nothing: there
345
- * is nothing before it to differ from.
350
+ * type or rules it changed, and - by name, since they are no longer in the
351
+ * version's own list - the ones it dropped. The first version changes
352
+ * nothing: there is nothing before it to differ from.
346
353
  */
347
354
  export function schemaChanges(
348
355
  event: Event,
@@ -360,9 +367,14 @@ export function schemaChanges(
360
367
  const now = new Set(current.fields.map((f) => f.name));
361
368
  for (const field of current.fields) {
362
369
  const was = before.get(field.name);
363
- if (!was) byField.set(field.name, { change: "new" });
364
- else if (was.type !== field.type)
365
- byField.set(field.name, { change: "changed", from: was.type });
370
+ if (!was) {
371
+ byField.set(field.name, { change: "new" });
372
+ continue;
373
+ }
374
+ const change: FieldChange = { change: "changed" };
375
+ if (was.type !== field.type) change.from = was.type;
376
+ if (!sameRules(was, field)) change.rulesFrom = ruleMarks(was).map((m) => m.text).join(", ");
377
+ if (change.from !== undefined || change.rulesFrom !== undefined) byField.set(field.name, change);
366
378
  }
367
379
  for (const field of prev.fields) {
368
380
  if (!now.has(field.name)) removed.push(field);
@@ -370,6 +382,18 @@ export function schemaChanges(
370
382
  return { byField, removed };
371
383
  }
372
384
 
385
+ /**
386
+ * Whether two versions of a field ask the same of its value. The order the
387
+ * rules are listed in is the source's, not a fact about the value, so it
388
+ * is not a difference.
389
+ */
390
+ function sameRules(a: Field, b: Field): boolean {
391
+ if (Boolean(a.required) !== Boolean(b.required)) return false;
392
+ const key = (field: Field) =>
393
+ (field.rules ?? []).map((r) => `${r.name}=${r.value ?? ""}`).sort().join("\n");
394
+ return key(a) === key(b);
395
+ }
396
+
373
397
  // ---------------------------------------------------------------------------
374
398
  // An enum and a lifecycle.
375
399
  // ---------------------------------------------------------------------------
package/src/lib/tech.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  siApachepulsar,
31
31
  siArgo,
32
32
  siAuth0,
33
+ siBackstage,
33
34
  siBun,
34
35
  siC,
35
36
  siCelery,
@@ -37,6 +38,7 @@ import {
37
38
  siClickhouse,
38
39
  siClojure,
39
40
  siCloudflare,
41
+ siConfluence,
40
42
  siCockroachlabs,
41
43
  siConsul,
42
44
  siCouchbase,
@@ -93,7 +95,9 @@ import {
93
95
  siLinkerd,
94
96
  siLua,
95
97
  siMariadb,
98
+ siMarkdown,
96
99
  siMeilisearch,
100
+ siMermaid,
97
101
  siMinio,
98
102
  siMongodb,
99
103
  siMqtt,
@@ -107,6 +111,7 @@ import {
107
111
  siNginx,
108
112
  siNodedotjs,
109
113
  siNomad,
114
+ siNotion,
110
115
  siNpm,
111
116
  siOllama,
112
117
  siOpenapiinitiative,
@@ -346,6 +351,11 @@ export const TECH_MARKS: readonly TechMark[] = [
346
351
  mark(siJenkins),
347
352
  mark(siArgo, "Argo CD", "ArgoCD"),
348
353
 
354
+ // Where a team writes. Not read off a manifest; the marks are for the
355
+ // reader-side integrations that open the wiki from a page.
356
+ mark(siConfluence),
357
+ mark(siNotion),
358
+
349
359
  // Observability.
350
360
  mark(siOpentelemetry, "otel"),
351
361
  mark(siPrometheus),
@@ -374,6 +384,11 @@ export const TECH_MARKS: readonly TechMark[] = [
374
384
  mark(siJest),
375
385
  mark(siVitest),
376
386
  mark(siCypress),
387
+
388
+ // What the generators write to.
389
+ mark(siMarkdown),
390
+ mark(siMermaid),
391
+ mark(siBackstage),
377
392
  ];
378
393
 
379
394
  const BY_KEY: ReadonlyMap<string, TechGlyph> = (() => {
@@ -417,5 +432,6 @@ export const STORE_KIND_GLYPH: Record<StoreKind, TechGlyph | null> = {
417
432
  clickhouse: techGlyph("clickhouse"),
418
433
  // simple-icons was asked to drop Amazon's marks, S3's among them.
419
434
  s3: null,
435
+ dynamodb: null,
420
436
  other: null,
421
437
  };
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { projectForFlow, projectsForFlow } from "./trace-project";
4
+
5
+ const projects = [
6
+ { id: "auth", name: "Auth", root: "examples/auth", context: "auth", service: "auth" },
7
+ { id: "shop-cart", name: "Cart", root: "examples/shop/cart", context: "shop", service: "cart" },
8
+ { id: "shop-oms", name: "OMS", root: "examples/shop/oms", group: "shop", component: "oms" },
9
+ ];
10
+
11
+ const flow = (owner: string, ...services: string[]) => ({
12
+ owner,
13
+ participants: [
14
+ { id: "client", kind: "actor" as const, context: null },
15
+ ...services.map((id) => ({ id, kind: "service" as const, context: id.split(".")[0] ?? null })),
16
+ { id: "bus", kind: "broker" as const, context: null },
17
+ ],
18
+ });
19
+
20
+ describe("which project a recording of a flow belongs to", () => {
21
+ it("is the one project in the flow's context", () => {
22
+ expect(projectForFlow(projects, flow("auth", "auth.auth"))?.id).toBe("auth");
23
+ });
24
+
25
+ it("is the project whose service the flow runs through when the context has several", () => {
26
+ expect(projectForFlow(projects, flow("shop", "shop.oms", "shop.pricing"))?.id).toBe("shop-oms");
27
+ expect(projectsForFlow(projects, flow("shop", "shop.cart")).map((p) => p.id)).toEqual(["shop-cart", "shop-oms"]);
28
+ });
29
+
30
+ it("is nobody's when the context has no project, or two the flow runs through", () => {
31
+ expect(projectForFlow(projects, flow("payments", "payments.ledger"))).toBeNull();
32
+ expect(projectForFlow(projects, flow("shop", "shop.cart", "shop.oms"))).toBeNull();
33
+ });
34
+ });
@@ -0,0 +1,50 @@
1
+ // Which project a recording of a flow belongs to.
2
+ //
3
+ // A recording is kept beside the project whose verify step reads it, and a
4
+ // flow says which context owns it - one level up from a project. The project
5
+ // is the one in that context whose service the flow runs through; when the
6
+ // context has one project, that one; otherwise the page has to ask.
7
+
8
+ import type { Flow } from "../catalog";
9
+ import type { SetupProject } from "./setup-info";
10
+
11
+ /** The projects a flow's recording could be kept under, the likeliest first. */
12
+ export function projectsForFlow(
13
+ projects: readonly SetupProject[],
14
+ flow: Pick<Flow, "owner" | "participants">,
15
+ ): SetupProject[] {
16
+ const inContext = projects.filter(
17
+ (project) => (project.group ?? project.context) === flow.owner,
18
+ );
19
+ const services = new Set(
20
+ flow.participants
21
+ .filter((p) => p.kind === "service" && p.context === flow.owner)
22
+ .map((p) => p.id.split(".").slice(1).join(".")),
23
+ );
24
+ const runsThrough = (project: SetupProject) =>
25
+ services.has(project.component ?? project.service ?? "");
26
+
27
+ return [...inContext].sort(
28
+ (a, b) => Number(runsThrough(b)) - Number(runsThrough(a)) || a.id.localeCompare(b.id),
29
+ );
30
+ }
31
+
32
+ /** The one project a recording of the flow goes to, or null when it has to be asked. */
33
+ export function projectForFlow(
34
+ projects: readonly SetupProject[],
35
+ flow: Pick<Flow, "owner" | "participants">,
36
+ ): SetupProject | null {
37
+ const candidates = projectsForFlow(projects, flow);
38
+ if (candidates.length === 1) return candidates[0]!;
39
+ if (candidates.length === 0) return null;
40
+ const services = new Set(
41
+ flow.participants
42
+ .filter((p) => p.kind === "service" && p.context === flow.owner)
43
+ .map((p) => p.id.split(".").slice(1).join(".")),
44
+ );
45
+ const through = candidates.filter((project) =>
46
+ services.has(project.component ?? project.service ?? ""),
47
+ );
48
+
49
+ return through.length === 1 ? through[0]! : null;
50
+ }
@@ -0,0 +1,23 @@
1
+ // The problems of the live catalog, for a component.
2
+ //
3
+ // Five places show the same list - the page, the sidebar badge, the overview,
4
+ // the landing's demo, the rules table - and each used to compute it once on
5
+ // mount. Now the list depends on the rules in force, which the Settings page
6
+ // changes under a running app, so the computation subscribes to them and the
7
+ // five places re-render together when a switch is flipped.
8
+
9
+ import { useMemo } from "react";
10
+ import { catalog, index } from "../data";
11
+ import { evaluateProblems } from "./all-problems";
12
+ import type { Problem } from "./derive";
13
+ import { useProblemRules } from "./problem-rules";
14
+ import type { RuleEvaluation } from "./problem-rules";
15
+
16
+ export function useProblemEvaluation(): RuleEvaluation {
17
+ const rules = useProblemRules();
18
+ return useMemo(() => evaluateProblems(catalog, index, rules), [rules]);
19
+ }
20
+
21
+ export function useProblems(): Problem[] {
22
+ return useProblemEvaluation().problems;
23
+ }
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { groupDiagnostics, groupWarnings, warningDiagnostic } from "./warnings";
3
+
4
+ describe("warning diagnostics", () => {
5
+ it("assigns a stable rule, severity and action to known warnings", () => {
6
+ const diagnostic = warningDiagnostic({
7
+ plugin: "openapi",
8
+ message: "aviacore-api: no operationId on 2 of 9 operations; listed by verb and path: POST /book, POST /cancel",
9
+ });
10
+
11
+ expect(diagnostic).toMatchObject({
12
+ plugin: "openapi",
13
+ rule: "openapi.missing-operation-id",
14
+ severity: "warning",
15
+ ref: "aviacore-api",
16
+ suppressed: false,
17
+ });
18
+ expect(diagnostic.action).toContain("operationId");
19
+ });
20
+
21
+ it("groups repetitions by plugin, rule and severity", () => {
22
+ const groups = groupWarnings([
23
+ { plugin: "openapi", message: "api.v1: no operationId on 1 of 3 operations; listed by verb and path: POST /book" },
24
+ { plugin: "openapi", message: "api.v2: no operationId on 1 of 3 operations; listed by verb and path: POST /cancel" },
25
+ { plugin: "wsdl", message: "b/schema.xsd: duplicate declaration CodeType and 2 more in namespace urn:common; the declarations in a/schema.xsd are used" },
26
+ ]);
27
+
28
+ expect(groups.map(({ plugin, rule, count }) => ({ plugin, rule, count }))).toEqual([
29
+ { plugin: "openapi", rule: "openapi.missing-operation-id", count: 2 },
30
+ { plugin: "wsdl", rule: "schema.duplicate-declaration", count: 1 },
31
+ ]);
32
+ });
33
+
34
+ it("keeps one rule across the wordings the plugins emit for it", () => {
35
+ const rules = [
36
+ "calls ledger.v1 and the manifest names no peer for it; add it under `peers` to say which service answers, until then the calls are unresolved",
37
+ "src/app.rs: port `clock: Clock` is neither a domain port, a use case nor a client; its calls are left out of the flow",
38
+ "bus/router.go:12:3: Watermill handler orders is registered on topic `cfg.Topic`, which this reader cannot resolve to a literal, a constant, a config default or a caller's argument; the handler is kept with its topic unresolved",
39
+ ].map((message) => warningDiagnostic({ plugin: "x", message }).rule);
40
+ expect(rules).toEqual(["catalog.unmapped-proto-peer", "flow.unknown-port", "watermill.unresolved-topic"]);
41
+ });
42
+
43
+ it("marks read and parse failures as errors and expected absence as info", () => {
44
+ expect(warningDiagnostic({ plugin: "sql", message: "schema.sql could not be parsed" }).severity).toBe("error");
45
+ expect(warningDiagnostic({ plugin: "river", message: "root: no River jobs were found" }).severity).toBe("info");
46
+ });
47
+
48
+ it("groups persisted policy decisions without evaluating policies in the browser", () => {
49
+ const diagnostic = warningDiagnostic({ plugin: "openapi", message: "api: no operationId on 1 of 3 operations; listed by verb and path: POST /book" });
50
+ const groups = groupDiagnostics([{ ...diagnostic, suppressed: true, suppressionReason: "Owned upstream." }]);
51
+ expect(groups[0]).toMatchObject({ suppressed: true, suppressionReason: "Owned upstream.", count: 1 });
52
+ });
53
+
54
+ it("keeps unrelated unknown warnings in separate fallback rules", () => {
55
+ const groups = groupWarnings([
56
+ { plugin: "custom", message: "service: frobnicator needs attention" },
57
+ { plugin: "custom", message: "service: another limitation was observed" },
58
+ ]);
59
+
60
+ expect(groups).toHaveLength(2);
61
+ expect(groups.every((group) => group.rule.startsWith("plugin.custom.other-"))).toBe(true);
62
+ });
63
+ });