@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
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ CATEGORY_ORDER,
4
+ hasPluginLabel,
5
+ landingInputs,
6
+ pluginIcon,
7
+ pluginIndex,
8
+ pluginsByCategory,
9
+ runtimeLabel,
10
+ } from "./plugins";
11
+ import { techGlyph } from "./tech";
12
+
13
+ describe("plugin index", () => {
14
+ it("is written by npm run schema and names every plugin once", () => {
15
+ expect(pluginIndex.length).toBeGreaterThan(0);
16
+ const names = pluginIndex.map((entry) => entry.name);
17
+ expect(new Set(names).size).toBe(names.length);
18
+ });
19
+
20
+ it("gives every plugin a category the page has a heading for and a phase to run in", () => {
21
+ for (const entry of pluginIndex) {
22
+ expect(CATEGORY_ORDER, entry.name).toContain(entry.category);
23
+ expect(entry.phases.length, entry.name).toBeGreaterThan(0);
24
+ expect(entry.summary, entry.name).not.toBe("");
25
+ }
26
+ });
27
+
28
+ // A plugin added without a display name would reach the landing page as its
29
+ // manifest key. This is the test that says so before a reader does.
30
+ it("has a display name for every plugin", () => {
31
+ const unnamed = pluginIndex.filter((entry) => !hasPluginLabel(entry.name)).map((entry) => entry.name);
32
+ expect(unnamed).toEqual([]);
33
+ });
34
+
35
+ // A brand named here that the mark list does not carry would draw as the
36
+ // generic code glyph, which is a wrong picture rather than a missing one.
37
+ it("resolves every brand mark it names", () => {
38
+ const unresolved = pluginIndex
39
+ .map((entry) => pluginIcon(entry.name, entry.category))
40
+ .filter((icon): icon is { brand: string } => "brand" in icon && techGlyph(icon.brand) === null)
41
+ .map((icon) => icon.brand);
42
+ expect(unresolved).toEqual([]);
43
+ });
44
+
45
+ it("falls back to the category's glyph for a plugin nobody named", () => {
46
+ expect(pluginIcon("something-new", "messaging")).toEqual({ lucide: "message" });
47
+ });
48
+
49
+ it("groups by category in reading order and drops empty groups", () => {
50
+ const groups = pluginsByCategory();
51
+ const order = groups.map((group) => group.category);
52
+ expect(order).toEqual(CATEGORY_ORDER.filter((category) => order.includes(category)));
53
+ for (const group of groups) expect(group.plugins.length).toBeGreaterThan(0);
54
+ });
55
+
56
+ it("lists what is read on the landing page, not what is made", () => {
57
+ const inputs = landingInputs();
58
+ expect(inputs.map((group) => group.label)).toContain("languages");
59
+ expect(inputs.flatMap((group) => group.items)).toContain("Go");
60
+ expect(inputs.flatMap((group) => group.items)).not.toContain("Markdown");
61
+ });
62
+
63
+ it("names the toolchain a host process asks for", () => {
64
+ expect(runtimeLabel({ runtime: "wasm" } as never)).toBe("WASM sandbox");
65
+ expect(runtimeLabel({ runtime: "host" } as never)).toBe("in the host");
66
+ expect(runtimeLabel({ runtime: "process", toolchain: "cargo" } as never)).toBe("host process · cargo");
67
+ });
68
+ });
@@ -0,0 +1,264 @@
1
+ // The plugin index: every plugin the package ships, as it describes itself.
2
+ //
3
+ // `src/lib/plugin-index.json` is written by `npm run schema` from the answer
4
+ // each plugin gives to `describe` and from how portolan.json runs it. Nothing
5
+ // here is typed by hand except the words a reader sees: the category titles,
6
+ // and a display name per plugin, because "go-sqs" is a manifest key and "SQS"
7
+ // is what the landing page says.
8
+
9
+ import { PRODUCT_REPOSITORY } from "./product";
10
+ import raw from "./plugin-index.json";
11
+
12
+ export type PluginCategory =
13
+ | "code"
14
+ | "contracts"
15
+ | "messaging"
16
+ | "data"
17
+ | "infrastructure"
18
+ | "repository"
19
+ | "documents"
20
+ | "evidence"
21
+ | "sources"
22
+ | "exports";
23
+
24
+ export type PluginPhase = "extract" | "verify" | "generate";
25
+
26
+ export interface PluginOption {
27
+ type?: string | string[];
28
+ description?: string;
29
+ }
30
+
31
+ export interface PluginEntry {
32
+ /** The name the manifest declares it under - what a step names. */
33
+ name: string;
34
+ /** What the plugin calls itself: its directory under plugins/. */
35
+ plugin: string;
36
+ summary: string;
37
+ category: PluginCategory;
38
+ phases: PluginPhase[];
39
+ needs?: string[];
40
+ runtime: "wasm" | "process" | "host";
41
+ /** The command a host process asks the build for; absent for a module. */
42
+ toolchain?: string;
43
+ /** Repository-relative path to the plugin's source. */
44
+ source: string;
45
+ options: {
46
+ properties?: Record<string, PluginOption>;
47
+ required?: string[];
48
+ };
49
+ }
50
+
51
+ // The JSON's inferred type is the union of every plugin's option keys, which
52
+ // is why the cast goes through unknown: the shape is what matters, not which
53
+ // keys one file happens to have.
54
+ export const pluginIndex: PluginEntry[] = raw as unknown as PluginEntry[];
55
+
56
+ /** The order the categories read in: what is read first, then what is made. */
57
+ export const CATEGORY_ORDER: PluginCategory[] = [
58
+ "code",
59
+ "contracts",
60
+ "messaging",
61
+ "data",
62
+ "infrastructure",
63
+ "repository",
64
+ "documents",
65
+ "evidence",
66
+ "sources",
67
+ "exports",
68
+ ];
69
+
70
+ /**
71
+ * A glyph without a brand: one of the lucide icons PluginIcon knows to draw.
72
+ * Named here as words rather than components so this module stays free of
73
+ * React and the tests can read it.
74
+ */
75
+ export type PluginGlyph =
76
+ | "code"
77
+ | "contract"
78
+ | "message"
79
+ | "database"
80
+ | "cloud"
81
+ | "repository"
82
+ | "pen"
83
+ | "shield"
84
+ | "fork"
85
+ | "share"
86
+ | "inbox"
87
+ | "file-code"
88
+ | "package"
89
+ | "waves"
90
+ | "workflow"
91
+ | "users"
92
+ | "globe"
93
+ | "terminal"
94
+ | "book"
95
+ | "spell-check"
96
+ | "route";
97
+
98
+ /** A brand's own mark, looked up by the name `techGlyph` answers to, or a lucide glyph. */
99
+ export type PluginIconSpec = { brand: string } | { lucide: PluginGlyph };
100
+
101
+ export const CATEGORY_LABEL: Record<PluginCategory, { title: string; what: string; icon: PluginGlyph }> = {
102
+ code: {
103
+ title: "Languages",
104
+ what: "The aggregates, events and use cases a service declares in its source.",
105
+ icon: "code",
106
+ },
107
+ contracts: {
108
+ title: "Contracts",
109
+ what: "What a service provides and calls, read from the interface it publishes.",
110
+ icon: "contract",
111
+ },
112
+ messaging: {
113
+ title: "Queues and topics",
114
+ what: "The subjects, queues and jobs a service sends on and listens to.",
115
+ icon: "message",
116
+ },
117
+ data: {
118
+ title: "Data stores",
119
+ what: "The stores a service keeps, and the shape of what is in them.",
120
+ icon: "database",
121
+ },
122
+ infrastructure: {
123
+ title: "Infrastructure",
124
+ what: "What a service is deployed on, and the wiring between the pieces that only the infrastructure knows.",
125
+ icon: "cloud",
126
+ },
127
+ repository: {
128
+ title: "Repository",
129
+ what: "What the repository says about itself: its metadata and its task runners.",
130
+ icon: "repository",
131
+ },
132
+ documents: {
133
+ title: "Written by hand",
134
+ what: "Decisions, glossaries and flows that people wrote down.",
135
+ icon: "pen",
136
+ },
137
+ evidence: {
138
+ title: "Evidence",
139
+ what: "The catalog checked against something outside the code.",
140
+ icon: "shield",
141
+ },
142
+ sources: {
143
+ title: "Other repositories",
144
+ what: "Trees fetched from elsewhere, pinned, for the extractors to read.",
145
+ icon: "fork",
146
+ },
147
+ exports: {
148
+ title: "Exports",
149
+ what: "The catalog turned into something else.",
150
+ icon: "share",
151
+ },
152
+ };
153
+
154
+ /**
155
+ * What a plugin is called where a person reads it, and the mark beside the
156
+ * name, keyed by manifest name. A brand mark where the plugin reads one
157
+ * technology and the reader knows its logo; a lucide glyph where it reads a
158
+ * format or a convention that has none.
159
+ */
160
+ const PLUGIN_META: Record<string, { label: string; icon: PluginIconSpec }> = {
161
+ project: { label: "Project metadata", icon: { lucide: "repository" } },
162
+ commands: { label: "Task runners", icon: { lucide: "terminal" } },
163
+ "go-domain": { label: "Go", icon: { brand: "Go" } },
164
+ "ts-domain": { label: "TypeScript", icon: { brand: "TypeScript" } },
165
+ "rust-domain": { label: "Rust", icon: { brand: "Rust" } },
166
+ "laravel-domain": { label: "Laravel", icon: { brand: "Laravel" } },
167
+ "php-ddd": { label: "PHP by context", icon: { brand: "Symfony" } },
168
+ "csharp-ddd": { label: "C# by module", icon: { brand: "C#" } },
169
+ "java-domain": { label: "Java", icon: { brand: "Java" } },
170
+ "django-domain": { label: "Django", icon: { brand: "Django" } },
171
+ celery: { label: "Celery", icon: { brand: "Celery" } },
172
+ "python-kafka": { label: "Kafka", icon: { brand: "Kafka" } },
173
+ openapi: { label: "OpenAPI", icon: { brand: "OpenAPI" } },
174
+ wsdl: { label: "WSDL", icon: { lucide: "file-code" } },
175
+ "http-clients": { label: "HTTP clients", icon: { lucide: "globe" } },
176
+ redis: { label: "Redis", icon: { brand: "Redis" } },
177
+ river: { label: "River", icon: { lucide: "waves" } },
178
+ watermill: { label: "Watermill", icon: { lucide: "workflow" } },
179
+ "go-nats": { label: "NATS", icon: { brand: "NATS" } },
180
+ "go-sqs": { label: "SQS", icon: { lucide: "inbox" } },
181
+ terraform: { label: "Terraform", icon: { brand: "Terraform" } },
182
+ k8s: { label: "Kubernetes manifests", icon: { brand: "Kubernetes" } },
183
+ "k8s-cluster": { label: "Kubernetes cluster", icon: { brand: "Kubernetes" } },
184
+ asyncapi: { label: "AsyncAPI", icon: { lucide: "file-code" } },
185
+ graphql: { label: "GraphQL", icon: { brand: "GraphQL" } },
186
+ sql: { label: "SQL", icon: { lucide: "database" } },
187
+ markdown: { label: "Markdown", icon: { brand: "Markdown" } },
188
+ mermaid: { label: "Mermaid", icon: { brand: "Mermaid" } },
189
+ backstage: { label: "Backstage", icon: { brand: "Backstage" } },
190
+ otel: { label: "OpenTelemetry", icon: { brand: "OpenTelemetry" } },
191
+ codeowners: { label: "CODEOWNERS", icon: { lucide: "users" } },
192
+ adr: { label: "Decision records", icon: { lucide: "book" } },
193
+ glossary: { label: "Glossary", icon: { lucide: "spell-check" } },
194
+ flows: { label: "Flows", icon: { lucide: "route" } },
195
+ bsr: { label: "Buf Schema Registry", icon: { lucide: "package" } },
196
+ git: { label: "Git", icon: { brand: "Git" } },
197
+ csr: { label: "Confluent Schema Registry", icon: { lucide: "package" } },
198
+ argocd: { label: "Argo CD", icon: { lucide: "cloud" } },
199
+ "argocd-gitops": { label: "Argo CD manifests", icon: { lucide: "cloud" } },
200
+ "csr-schemas": { label: "Registry schemas", icon: { lucide: "file-code" } },
201
+ proto: { label: "Protobuf", icon: { lucide: "file-code" } },
202
+ };
203
+
204
+ export function pluginLabel(name: string): string {
205
+ return PLUGIN_META[name]?.label ?? name;
206
+ }
207
+
208
+ /** True when the plugin has a name a reader would recognise, not a manifest key. */
209
+ export function hasPluginLabel(name: string): boolean {
210
+ return name in PLUGIN_META;
211
+ }
212
+
213
+ /** The mark beside a plugin's name; a plugin nobody named yet gets its category's glyph. */
214
+ export function pluginIcon(name: string, category?: PluginCategory): PluginIconSpec {
215
+ return PLUGIN_META[name]?.icon ?? { lucide: CATEGORY_LABEL[category ?? "code"].icon };
216
+ }
217
+
218
+ export function pluginByName(name: string): PluginEntry | undefined {
219
+ return pluginIndex.find((entry) => entry.name === name);
220
+ }
221
+
222
+ export interface PluginGroup {
223
+ category: PluginCategory;
224
+ title: string;
225
+ what: string;
226
+ icon: PluginGlyph;
227
+ plugins: PluginEntry[];
228
+ }
229
+
230
+ /** The index by category, in reading order, each group sorted by display name. */
231
+ export function pluginsByCategory(entries: PluginEntry[] = pluginIndex): PluginGroup[] {
232
+ return CATEGORY_ORDER.map((category) => ({
233
+ category,
234
+ ...CATEGORY_LABEL[category],
235
+ plugins: entries
236
+ .filter((entry) => entry.category === category)
237
+ .sort((a, b) => pluginLabel(a.name).localeCompare(pluginLabel(b.name))),
238
+ })).filter((group) => group.plugins.length > 0);
239
+ }
240
+
241
+ /** The categories the landing page lists as inputs: what is read, not what is made. */
242
+ const LANDING_CATEGORIES: PluginCategory[] = ["code", "contracts", "messaging", "data", "evidence"];
243
+
244
+ /** One line per input category, the display names of what reads it. */
245
+ export function landingInputs(entries: PluginEntry[] = pluginIndex): { label: string; items: string[] }[] {
246
+ return pluginsByCategory(entries)
247
+ .filter((group) => LANDING_CATEGORIES.includes(group.category))
248
+ .map((group) => ({
249
+ label: group.title.toLowerCase(),
250
+ items: group.plugins.map((entry) => pluginLabel(entry.name)),
251
+ }));
252
+ }
253
+
254
+ /** Where the plugin's source is read, on the public repository. */
255
+ export function pluginSourceHref(entry: PluginEntry): string {
256
+ return `${PRODUCT_REPOSITORY}/tree/main/${entry.source}`;
257
+ }
258
+
259
+ /** The runtime as a reader would say it. */
260
+ export function runtimeLabel(entry: PluginEntry): string {
261
+ if (entry.runtime === "wasm") return "WASM sandbox";
262
+ if (entry.runtime === "host") return "in the host";
263
+ return entry.toolchain ? `host process · ${entry.toolchain}` : "host process";
264
+ }
@@ -0,0 +1,61 @@
1
+ // A problem row leads to the step that crosses its edge, or to the flows
2
+ // its service is on when the subject is not an edge.
3
+
4
+ import { describe, expect, it } from "vitest";
5
+ import { rawCatalog } from "../test-catalog";
6
+ import { buildIndex, validateCatalog, walkSteps } from "../catalog";
7
+ import type { Catalog } from "../catalog";
8
+ import type { Problem } from "./derive";
9
+ import { flowsOfProblem } from "./problem-flows";
10
+
11
+ const catalog = validateCatalog(JSON.parse(JSON.stringify(rawCatalog)) as unknown as Catalog);
12
+ const index = buildIndex(catalog);
13
+
14
+ const problem = (id: string, service: string): Problem => ({ rule: "x", severity: "warning", context: "", service, id, peer: "", note: undefined, source: undefined });
15
+
16
+ /** The first step in the estate of one kind with a ref, and its flow and number. */
17
+ function firstStep(kind: "rpc" | "event") {
18
+ for (const flow of catalog.flows) {
19
+ const steps = walkSteps(flow.steps);
20
+ const at = steps.findIndex((step) => step.kind === kind && step.ref);
21
+ if (at >= 0) return { flow, step: steps[at]!, number: at + 1 };
22
+ }
23
+ throw new Error(`the fixture has no ${kind} step with a ref`);
24
+ }
25
+
26
+ describe("flowsOfProblem", () => {
27
+ it("lands a call on the step that makes it", () => {
28
+ const { flow, step, number } = firstStep("rpc");
29
+ const hits = flowsOfProblem(catalog, index, "call", problem(step.ref!, step.from));
30
+ expect(hits.map((hit) => hit.flow.id)).toContain(flow.id);
31
+ const hit = hits.find((candidate) => candidate.flow.id === flow.id)!;
32
+ expect(hit.stepId).toBe(step.id);
33
+ expect(hit.number).toBe(number);
34
+ });
35
+
36
+ it("lands an event on the step that carries it, once per flow", () => {
37
+ const { flow, step } = firstStep("event");
38
+ const hits = flowsOfProblem(catalog, index, "event", problem(step.ref!, step.from));
39
+ expect(hits.some((hit) => hit.flow.id === flow.id && hit.stepId === step.id)).toBe(true);
40
+ expect(new Set(hits.map((hit) => hit.flow.id)).size).toBe(hits.length);
41
+ });
42
+
43
+ it("lands a table on the steps that touch its store", () => {
44
+ const access = catalog.flows.flatMap((flow) => walkSteps(flow.steps)).find((step) => step.storeAccess);
45
+ if (!access?.storeAccess) return;
46
+ const store = index.storeById.get(access.storeAccess.store)!;
47
+ const table = store.tables[0]!;
48
+ const hits = flowsOfProblem(catalog, index, "table", problem(table.id, store.owner));
49
+ expect(hits.length).toBeGreaterThan(0);
50
+ expect(hits.every((hit) => hit.stepId !== undefined)).toBe(true);
51
+ expect(flowsOfProblem(catalog, index, "column", problem(`${table.id}.${table.columns[0]!.name}`, store.owner)).length).toBe(hits.length);
52
+ });
53
+
54
+ it("gives a service-shaped row the flows the service is on, without a step", () => {
55
+ const service = catalog.contexts[0]!.services[0]!;
56
+ const hits = flowsOfProblem(catalog, index, "service", problem(service.id, service.id));
57
+ expect(hits.length).toBe(catalog.flows.filter((flow) => flow.participants.some((p) => p.id === service.id)).length);
58
+ expect(hits.every((hit) => hit.stepId === undefined)).toBe(true);
59
+ expect(flowsOfProblem(catalog, index, "deployment", problem("argocd/x", ""))).toEqual([]);
60
+ });
61
+ });
@@ -0,0 +1,78 @@
1
+ // Where a problem's edge is met in the flows.
2
+ //
3
+ // A row on the Problems page names one end of an edge; the flows are where
4
+ // that edge is crossed, and the flow's page is where a reader sees what
5
+ // happens before and after it. So a row that knows its flows leads to the
6
+ // step, not to the flow's first line - and a row whose subject is a service
7
+ // leads to the flows the service is on, which is the best a service-shaped
8
+ // row can do.
9
+
10
+ import type { Catalog, CatalogIndex, Flow } from "../catalog";
11
+ import { walkSteps } from "../catalog";
12
+ import type { Problem } from "./derive";
13
+ import type { RuleSubject } from "./problem-rules-cel.mjs";
14
+
15
+ export interface FlowHit {
16
+ flow: Flow;
17
+ /** The step that crosses the edge, when one does; a flow the service is merely on has none. */
18
+ stepId?: string;
19
+ number?: number;
20
+ }
21
+
22
+ /** The store a table's or a column's id is under, by longest known prefix. */
23
+ function storeOf(index: CatalogIndex, id: string): string | null {
24
+ const parts = id.split(".");
25
+ for (let n = parts.length - 1; n >= 1; n -= 1) {
26
+ const candidate = parts.slice(0, n).join(".");
27
+ if (index.storeById.has(candidate)) return candidate;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function stepsWhere(catalog: Catalog, matches: (step: ReturnType<typeof walkSteps>[number]) => boolean): FlowHit[] {
33
+ const out: FlowHit[] = [];
34
+ for (const flow of catalog.flows) {
35
+ const steps = walkSteps(flow.steps);
36
+ const at = steps.findIndex(matches);
37
+ if (at >= 0) out.push({ flow, stepId: steps[at]!.id, number: at + 1 });
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function flowsOn(catalog: Catalog, serviceId: string): FlowHit[] {
43
+ if (!serviceId) return [];
44
+ return catalog.flows.filter((flow) => flow.participants.some((participant) => participant.id === serviceId)).map((flow) => ({ flow }));
45
+ }
46
+
47
+ /**
48
+ * The flows a problem is met in, one hit per flow, the crossing step first
49
+ * when the subject is an edge and the service's flows when it is not.
50
+ */
51
+ export function flowsOfProblem(catalog: Catalog, index: CatalogIndex, over: RuleSubject, problem: Problem): FlowHit[] {
52
+ switch (over) {
53
+ case "call":
54
+ return stepsWhere(catalog, (step) => step.ref === problem.id);
55
+ // A channel row is an event when the service has one on the address and
56
+ // the service otherwise; the second kind has no step to land on.
57
+ case "event":
58
+ case "consumer":
59
+ case "channel": {
60
+ const hits = stepsWhere(catalog, (step) => step.ref === problem.id);
61
+ return hits.length > 0 || index.eventById.has(problem.id) ? hits : flowsOn(catalog, problem.service);
62
+ }
63
+ case "table":
64
+ case "column": {
65
+ const store = storeOf(index, problem.id);
66
+ return store ? stepsWhere(catalog, (step) => step.storeAccess?.store === store) : [];
67
+ }
68
+ case "flow":
69
+ return catalog.flows.filter((flow) => flow.id === problem.id).map((flow) => ({ flow }));
70
+ case "aggregate":
71
+ return stepsWhere(catalog, (step) => step.ref?.startsWith(`${problem.id}.`) === true || step.ref?.startsWith(`${problem.id}/`) === true);
72
+ case "service":
73
+ case "copy":
74
+ case "subscription":
75
+ case "deployment":
76
+ return flowsOn(catalog, problem.service);
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ export type RuleSubject =
2
+ | "service"
3
+ | "call"
4
+ | "copy"
5
+ | "event"
6
+ | "consumer"
7
+ | "channel"
8
+ | "subscription"
9
+ | "table"
10
+ | "column"
11
+ | "deployment"
12
+ | "flow"
13
+ | "aggregate";
14
+
15
+ export type RuleSeverity = "error" | "warning";
16
+
17
+ /** A CEL type name, as the schema spells it. */
18
+ export type FieldType = "string" | "int" | "bool" | "list<string>";
19
+
20
+ export interface SubjectDefinition {
21
+ description: string;
22
+ schema: Record<string, FieldType>;
23
+ }
24
+
25
+ export const SUBJECTS: Record<RuleSubject, SubjectDefinition>;
26
+ export const ESTATE_SCHEMA: Record<string, FieldType>;
27
+ export const SUBJECT_NAMES: RuleSubject[];
28
+ export const SEVERITIES: RuleSeverity[];
29
+ export const RULE_ID: RegExp;
30
+
31
+ /** A parsed expression: call it with the context to evaluate. */
32
+ export type CompiledExpression = (context: Record<string, unknown>) => unknown;
33
+
34
+ export function environmentFor(subject: RuleSubject): unknown;
35
+
36
+ export function compileExpression(subject: RuleSubject, source: string, type: "bool" | "string"): CompiledExpression;
37
+
38
+ export function problemRuleProblems(entries: unknown, builtinIds: Iterable<string>, path?: string): string[];
39
+
40
+ export function ruleExpressionProblems(
41
+ rule: { id: string; over?: string; title?: string; when?: string; message?: string; peer?: string },
42
+ at?: string,
43
+ ): string[];