@shortlink-org/portolan 0.2.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +174 -6
  2. package/catalog/enum_test.go +46 -0
  3. package/catalog/evidence_test.go +35 -0
  4. package/catalog/model.go +1213 -0
  5. package/catalog/roundtrip_test.go +210 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/init.test.mjs +6 -1
  8. package/cli/portolan.mjs +14 -1
  9. package/cli/portolan.test.mjs +49 -0
  10. package/go.mod +21 -0
  11. package/go.sum +34 -0
  12. package/internal/gocall/README.md +19 -0
  13. package/internal/gocall/analyze.go +189 -0
  14. package/internal/gocall/analyze_test.go +107 -0
  15. package/internal/gohttp/analyze.go +2562 -0
  16. package/internal/gohttp/destination.go +373 -0
  17. package/internal/gohttp/endpoints.go +1067 -0
  18. package/internal/gohttp/roots.go +320 -0
  19. package/internal/gohttp/typed.go +96 -0
  20. package/internal/goscan/constants.go +85 -0
  21. package/internal/goscan/goscan_test.go +227 -0
  22. package/internal/goscan/index.go +629 -0
  23. package/internal/goscan/index_test.go +66 -0
  24. package/internal/goscan/names.go +52 -0
  25. package/internal/goscan/parse_test.go +11 -0
  26. package/internal/goscan/source.go +37 -0
  27. package/internal/goscan/tree.go +284 -0
  28. package/internal/goscan/types.go +99 -0
  29. package/internal/wsdl/ids.go +127 -0
  30. package/internal/wsdl/ids_test.go +21 -0
  31. package/internal/wsdl/model.go +70 -0
  32. package/internal/wsdl/parse.go +949 -0
  33. package/internal/wsdl/parse_test.go +170 -0
  34. package/package.json +28 -15
  35. package/plugin/describe.go +156 -0
  36. package/plugin/describe_test.go +114 -0
  37. package/plugin/protocol.go +141 -0
  38. package/plugin/schematest/schematest.go +126 -0
  39. package/plugins/README.md +404 -50
  40. package/plugins/cmd/portolan-http-clients/main.go +19 -0
  41. package/plugins/extract-argocd/options.schema.json +44 -0
  42. package/plugins/extract-celery/extract.py +0 -2
  43. package/plugins/extract-celery/extract_test.py +1 -1
  44. package/plugins/extract-celery/main.py +1 -0
  45. package/plugins/extract-csharp-ddd/README.md +213 -0
  46. package/plugins/extract-csharp-ddd/options.schema.json +55 -0
  47. package/plugins/extract-django/README.md +52 -19
  48. package/plugins/extract-django/choices.py +47 -0
  49. package/plugins/extract-django/domain.py +30 -18
  50. package/plugins/extract-django/extract.py +21 -7
  51. package/plugins/extract-django/extract_test.py +68 -2
  52. package/plugins/extract-django/lifecycle.py +4 -28
  53. package/plugins/extract-django/main.py +1 -0
  54. package/plugins/extract-django/operations.py +1 -1
  55. package/plugins/extract-django/routing_test.py +109 -1
  56. package/plugins/extract-django/rules.py +154 -0
  57. package/plugins/extract-django/rules_test.py +158 -0
  58. package/plugins/extract-django/store.py +1 -1
  59. package/plugins/extract-django/transport.py +101 -55
  60. package/plugins/extract-django/verbs.py +241 -0
  61. package/plugins/extract-go/README.md +47 -0
  62. package/plugins/extract-go-sqs/options.schema.json +21 -0
  63. package/plugins/extract-http-clients/describe.go +20 -0
  64. package/plugins/extract-http-clients/describe_test.go +11 -0
  65. package/plugins/extract-http-clients/extract.go +740 -0
  66. package/plugins/extract-http-clients/extract_test.go +1561 -0
  67. package/plugins/extract-http-clients/main.go +41 -0
  68. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  69. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  70. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  71. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  72. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  73. package/plugins/extract-k8s/options.schema.json +32 -0
  74. package/plugins/extract-laravel/Cargo.lock +962 -0
  75. package/plugins/extract-laravel/Cargo.toml +20 -0
  76. package/plugins/extract-laravel/README.md +200 -0
  77. package/plugins/extract-laravel/options.schema.json +68 -0
  78. package/plugins/extract-laravel/rustfmt.toml +1 -0
  79. package/plugins/extract-php-ddd/Cargo.lock +972 -0
  80. package/plugins/extract-php-ddd/Cargo.toml +22 -0
  81. package/plugins/extract-php-ddd/README.md +141 -0
  82. package/plugins/extract-php-ddd/options.schema.json +50 -0
  83. package/plugins/extract-php-ddd/rustfmt.toml +1 -0
  84. package/plugins/extract-python-kafka/extract.py +0 -2
  85. package/plugins/extract-python-kafka/extract_test.py +1 -1
  86. package/plugins/extract-python-kafka/main.py +1 -0
  87. package/plugins/extract-terraform/options.schema.json +26 -0
  88. package/plugins/extract-ts/extract.test.ts +2 -2
  89. package/plugins/extract-ts/extract.ts +4 -5
  90. package/plugins/extract-ts/graphql.test.ts +1 -1
  91. package/plugins/extract-ts/main.ts +1 -0
  92. package/plugins/openapi/ids.go +261 -0
  93. package/plugins/openapi/ids_test.go +98 -0
  94. package/plugins/phpscan/Cargo.lock +953 -0
  95. package/plugins/phpscan/Cargo.toml +19 -0
  96. package/plugins/phpscan/rustfmt.toml +1 -0
  97. package/plugins/portolan-go.wasm +0 -0
  98. package/plugins/pyplugin/catalog.py +24 -1
  99. package/plugins/pyplugin/protocol.py +1 -5
  100. package/plugins/verify-otel/options.schema.json +12 -0
  101. package/portolan.json +132 -4
  102. package/rules/builtin.json +218 -0
  103. package/schema/portolan.schema.json +905 -4
  104. package/scripts/README.md +21 -13
  105. package/scripts/catalog-sources.mjs +6 -0
  106. package/scripts/delivery-presets.mjs +57 -14
  107. package/scripts/diff.mjs +5 -1
  108. package/scripts/django-aggregates.test.mjs +58 -0
  109. package/scripts/gen-likec4.mjs +150 -17
  110. package/scripts/gen-likec4.test.mjs +96 -0
  111. package/scripts/gen.mjs +148 -118
  112. package/scripts/gitops-example.test.mjs +108 -0
  113. package/scripts/go-discovery.test.mjs +30 -0
  114. package/scripts/history.mjs +186 -3
  115. package/scripts/history.test.mjs +1 -1
  116. package/scripts/host-plugins/fetch-argocd.mjs +338 -0
  117. package/scripts/host-plugins/fetch-argocd.options.json +49 -0
  118. package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
  119. package/scripts/host-plugins/fetch-bsr.mjs +1 -0
  120. package/scripts/host-plugins/fetch-csr.mjs +1 -0
  121. package/scripts/host-plugins/fetch-git.mjs +78 -21
  122. package/scripts/host-plugins/fetch-git.test.mjs +62 -8
  123. package/scripts/host-plugins/fetch-k8s.mjs +263 -0
  124. package/scripts/host-plugins/fetch-k8s.options.json +50 -0
  125. package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
  126. package/scripts/host-plugins/k8s-topology.mjs +183 -0
  127. package/scripts/json-format.mjs +192 -0
  128. package/scripts/json-format.test.mjs +97 -0
  129. package/scripts/local-api.mjs +287 -12
  130. package/scripts/local-api.test.mjs +150 -5
  131. package/scripts/local-discovery.mjs +94 -9
  132. package/scripts/manifest.mjs +14 -3
  133. package/scripts/manifest.test.mjs +24 -0
  134. package/scripts/output-diff.mjs +94 -0
  135. package/scripts/output-diff.test.mjs +36 -0
  136. package/scripts/package-smoke.mjs +62 -4
  137. package/scripts/plugin-host.mjs +45 -3
  138. package/scripts/plugin-host.test.mjs +9 -0
  139. package/scripts/plugin-wasm-worker.mjs +4 -1
  140. package/scripts/plugins-fresh.mjs +97 -0
  141. package/scripts/plugins-fresh.test.mjs +64 -0
  142. package/scripts/provenance.mjs +72 -0
  143. package/scripts/provenance.test.mjs +149 -0
  144. package/scripts/run-builtin.mjs +62 -7
  145. package/scripts/schema.mjs +167 -19
  146. package/scripts/trace-trials.mjs +176 -0
  147. package/scripts/trace-trials.test.mjs +142 -0
  148. package/scripts/warning-policy.mjs +167 -0
  149. package/scripts/warning-policy.test.mjs +93 -0
  150. package/src/app/Breadcrumbs.test.ts +3 -0
  151. package/src/app/Breadcrumbs.tsx +3 -0
  152. package/src/app/CatalogApp.tsx +2 -0
  153. package/src/app/Sidebar.tsx +3 -3
  154. package/src/app/SidebarFooter.tsx +20 -4
  155. package/src/catalog-docs.test.ts +64 -0
  156. package/src/catalog-docs.ts +35 -0
  157. package/src/catalog-error.test.ts +15 -0
  158. package/src/catalog-index.ts +25 -0
  159. package/src/catalog-model.ts +293 -5
  160. package/src/catalog-validation.ts +113 -2
  161. package/src/catalog.test.ts +40 -1
  162. package/src/chat/Starter.tsx +5 -11
  163. package/src/chat/tools.test.ts +27 -0
  164. package/src/chat/tools.ts +5 -9
  165. package/src/components/CatalogStamp.tsx +10 -8
  166. package/src/components/ChannelRows.test.tsx +76 -1
  167. package/src/components/ChannelRows.tsx +223 -60
  168. package/src/components/DeploymentRows.tsx +131 -0
  169. package/src/components/DocsLinks.test.tsx +27 -0
  170. package/src/components/DocsLinks.tsx +56 -0
  171. package/src/components/FieldTree.tsx +5 -2
  172. package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
  173. package/src/components/HTTPDestinationEvidence.tsx +31 -0
  174. package/src/components/Integrations.tsx +1 -1
  175. package/src/components/MachineDocs.tsx +6 -5
  176. package/src/components/MethodRows.tsx +9 -2
  177. package/src/components/PluginIcon.tsx +77 -0
  178. package/src/components/ProblemRow.tsx +173 -153
  179. package/src/components/RelationEvidence.test.tsx +14 -0
  180. package/src/components/RelationEvidence.tsx +53 -0
  181. package/src/components/RuleMarks.tsx +22 -0
  182. package/src/components/ShapeRows.tsx +24 -20
  183. package/src/data.ts +46 -7
  184. package/src/enrich.test.ts +459 -4
  185. package/src/enrich.ts +308 -7
  186. package/src/er/ErCanvas.tsx +218 -12
  187. package/src/er/GroupNode.tsx +57 -0
  188. package/src/er/StoreHeader.tsx +1 -0
  189. package/src/er/layout.test.ts +85 -2
  190. package/src/er/layout.ts +140 -5
  191. package/src/er/spec.test.ts +17 -0
  192. package/src/er/spec.ts +23 -10
  193. package/src/flow/Recordings.test.tsx +52 -0
  194. package/src/flow/Recordings.tsx +236 -0
  195. package/src/flow/StepDetail.tsx +59 -0
  196. package/src/flow/TraceTrial.tsx +419 -0
  197. package/src/flow/evidence.test.ts +16 -0
  198. package/src/flow/evidence.ts +34 -0
  199. package/src/flow/examples.test.ts +33 -0
  200. package/src/flow/examples.ts +37 -0
  201. package/src/flow/outline.test.ts +28 -0
  202. package/src/flow/outline.ts +7 -2
  203. package/src/flow/trace-trial-resume.test.ts +42 -0
  204. package/src/flow/trace-trial-resume.ts +74 -0
  205. package/src/graph/elk.ts +78 -0
  206. package/src/index.css +68 -0
  207. package/src/landing/DraggableReveal.tsx +3 -2
  208. package/src/landing/EvidencePipeline.tsx +105 -0
  209. package/src/landing/LandingPage.tsx +17 -70
  210. package/src/landing/ProductTour.tsx +6 -6
  211. package/src/lib/all-problems.ts +27 -17
  212. package/src/lib/catalog-diff.ts +1 -1
  213. package/src/lib/centrality.test.ts +251 -0
  214. package/src/lib/centrality.ts +232 -0
  215. package/src/lib/confluence.test.ts +37 -0
  216. package/src/lib/confluence.ts +41 -0
  217. package/src/lib/context-color.ts +1 -1
  218. package/src/lib/deployment-drift.ts +22 -0
  219. package/src/lib/derive.ts +43 -69
  220. package/src/lib/django-aggregates.d.mts +9 -0
  221. package/src/lib/django-aggregates.mjs +36 -0
  222. package/src/lib/django-aggregates.test.ts +29 -0
  223. package/src/lib/django-aggregates.ts +5 -0
  224. package/src/lib/environments.test.ts +83 -0
  225. package/src/lib/environments.ts +57 -0
  226. package/src/lib/integration-url.test.ts +30 -0
  227. package/src/lib/integration-url.ts +63 -0
  228. package/src/lib/kafka-ui.ts +3 -45
  229. package/src/lib/local-api.ts +116 -4
  230. package/src/lib/notion.ts +13 -0
  231. package/src/lib/plugin-index.json +2765 -0
  232. package/src/lib/plugins.test.ts +68 -0
  233. package/src/lib/plugins.ts +264 -0
  234. package/src/lib/problem-flows.test.ts +61 -0
  235. package/src/lib/problem-flows.ts +78 -0
  236. package/src/lib/problem-rules-cel.d.mts +43 -0
  237. package/src/lib/problem-rules-cel.mjs +407 -0
  238. package/src/lib/problem-rules.test.ts +287 -0
  239. package/src/lib/problem-rules.ts +271 -0
  240. package/src/lib/problem-subjects.ts +737 -0
  241. package/src/lib/rule-entries.ts +39 -0
  242. package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
  243. package/src/lib/rules-deploy.test.ts +133 -0
  244. package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
  245. package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
  246. package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
  247. package/src/lib/rules.test.ts +51 -0
  248. package/src/lib/rules.ts +86 -0
  249. package/src/lib/setup-info.test.ts +17 -0
  250. package/src/lib/setup-info.ts +58 -0
  251. package/src/lib/shape.test.ts +32 -0
  252. package/src/lib/shape.ts +30 -6
  253. package/src/lib/tech.ts +16 -0
  254. package/src/lib/trace-project.test.ts +34 -0
  255. package/src/lib/trace-project.ts +50 -0
  256. package/src/lib/use-problems.ts +23 -0
  257. package/src/lib/warnings.test.ts +63 -0
  258. package/src/lib/warnings.ts +260 -0
  259. package/src/likec4/ids.test.ts +6 -2
  260. package/src/likec4/ids.ts +43 -0
  261. package/src/main.tsx +23 -0
  262. package/src/map/ContextMapGraph.tsx +76 -32
  263. package/src/merge-deployments.test.ts +127 -0
  264. package/src/merge.test.ts +82 -0
  265. package/src/merge.ts +189 -18
  266. package/src/pages/AggregatePage.tsx +65 -14
  267. package/src/pages/ContextMap.tsx +45 -3
  268. package/src/pages/ContextPage.tsx +8 -5
  269. package/src/pages/EventPage.tsx +15 -5
  270. package/src/pages/FlowDetail.tsx +23 -2
  271. package/src/pages/GraphPage.tsx +40 -3
  272. package/src/pages/Overview.tsx +152 -12
  273. package/src/pages/PluginIndex.tsx +190 -0
  274. package/src/pages/Problems.tsx +396 -128
  275. package/src/pages/ServicePage.tsx +62 -5
  276. package/src/pages/Settings.tsx +217 -43
  277. package/src/pages/settings/AboutSettings.tsx +8 -1
  278. package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
  279. package/src/pages/settings/IntegrationsSettings.tsx +63 -17
  280. package/src/pages/settings/RecordingSettings.tsx +138 -0
  281. package/src/pages/settings/RulesSettings.tsx +825 -0
  282. package/src/routes.test.ts +9 -0
  283. package/src/routes.ts +22 -1
  284. package/src/selection/DetailPanel.tsx +15 -0
  285. package/src/virtual-provenance.d.ts +11 -0
  286. package/vite.config.ts +5 -0
  287. package/scripts/vendor-lock.mjs +0 -58
  288. package/scripts/vendor-lock.test.mjs +0 -69
  289. package/src/lib/data-problems.ts +0 -314
  290. package/src/lib/proto-problems.ts +0 -237
  291. package/src/lib/wire-problems.ts +0 -342
@@ -1,5 +1,5 @@
1
1
  import { FileText } from "lucide-react";
2
- import { activeCatalogProfile } from "../data";
2
+ import { activeCatalogDocs, activeCatalogProfile } from "../data";
3
3
 
4
4
  // The site as a language model reads it. The build (scripts/site-docs.mjs)
5
5
  // places the generated markdown under docs/ and llms.txt at the root, in the
@@ -10,11 +10,12 @@ import { activeCatalogProfile } from "../data";
10
10
  const base = import.meta.env.BASE_URL;
11
11
 
12
12
  const suffix = activeCatalogProfile.id === "portolan" ? "" : `${activeCatalogProfile.id}/`;
13
- const docs = `${base}docs/${suffix}`;
14
13
  const FILES = [
15
- { href: suffix ? `${docs}llms.txt` : `${base}llms.txt`, label: "llms.txt", title: "An index of every page, for a model that fetches on demand" },
16
- { href: suffix ? `${docs}llms-full.txt` : `${base}llms-full.txt`, label: "llms-full.txt", title: "Every page in one file, for a model with a context window" },
17
- { href: docs, label: "docs/", title: "The generated markdown, page by page" },
14
+ ...(activeCatalogDocs ? [
15
+ { href: activeCatalogDocs.index, label: "llms.txt", title: "An index of every page, for a model that fetches on demand" },
16
+ { href: activeCatalogDocs.full, label: "llms-full.txt", title: "Every page in one file, for a model with a context window" },
17
+ { href: activeCatalogDocs.pages, label: "docs/", title: "The generated markdown, page by page" },
18
+ ] : []),
18
19
  { href: `${base}exports/mermaid/${suffix}`, label: "Mermaid", title: "Standalone sequence diagrams for every architecture flow" },
19
20
  ...(activeCatalogProfile.id === "example"
20
21
  ? [{ href: `${base}exports/backstage/catalog-info.yaml`, label: "Backstage", title: "Components, systems, APIs and resources for a Backstage catalog" }]
@@ -167,8 +167,15 @@ export function MethodRows({
167
167
  className={method.deprecated ? "line-through" : undefined}
168
168
  />
169
169
  {method.http ? (
170
- <span className="chip mono" title="the route this operation answers on">
171
- {method.http.method} {method.http.path}
170
+ <span
171
+ className="chip mono"
172
+ title={
173
+ method.http.method
174
+ ? "the route this operation answers on"
175
+ : "the route this operation answers on; the HTTP verb is not declared in source"
176
+ }
177
+ >
178
+ {method.http.method ? `${method.http.method} ${method.http.path}` : method.http.path}
172
179
  </span>
173
180
  ) : null}
174
181
  {method.soap ? (
@@ -0,0 +1,77 @@
1
+ // The mark beside a plugin's name: the brand's own where the plugin reads one
2
+ // technology the reader knows by its logo, a lucide glyph where it reads a
3
+ // format or a convention that has none. Both are drawn at the size and weight
4
+ // of the lucide row they sit in, which is what TechIcon's opened viewBox is for.
5
+
6
+ import {
7
+ BookOpen,
8
+ Cloud,
9
+ CodeXml,
10
+ Database,
11
+ FileCode2,
12
+ FileSignature,
13
+ FolderGit2,
14
+ GitFork,
15
+ Globe,
16
+ Inbox,
17
+ MessageSquare,
18
+ Package,
19
+ PenLine,
20
+ Route,
21
+ Share2,
22
+ ShieldCheck,
23
+ SpellCheck,
24
+ Terminal,
25
+ Users,
26
+ Waves,
27
+ Workflow,
28
+ } from "lucide-react";
29
+ import type { LucideIcon } from "lucide-react";
30
+
31
+ import type { PluginGlyph, PluginIconSpec } from "../lib/plugins";
32
+ import { techGlyph } from "../lib/tech";
33
+ import { TechIcon } from "./TechIcon";
34
+
35
+ const GLYPHS: Record<PluginGlyph, LucideIcon> = {
36
+ code: CodeXml,
37
+ contract: FileSignature,
38
+ message: MessageSquare,
39
+ database: Database,
40
+ cloud: Cloud,
41
+ repository: FolderGit2,
42
+ pen: PenLine,
43
+ shield: ShieldCheck,
44
+ fork: GitFork,
45
+ share: Share2,
46
+ inbox: Inbox,
47
+ "file-code": FileCode2,
48
+ package: Package,
49
+ waves: Waves,
50
+ workflow: Workflow,
51
+ users: Users,
52
+ globe: Globe,
53
+ terminal: Terminal,
54
+ book: BookOpen,
55
+ "spell-check": SpellCheck,
56
+ route: Route,
57
+ };
58
+
59
+ export function PluginIcon({
60
+ icon,
61
+ size = 14,
62
+ className = "",
63
+ }: {
64
+ icon: PluginIconSpec;
65
+ size?: number;
66
+ className?: string;
67
+ }) {
68
+ if ("brand" in icon) {
69
+ const glyph = techGlyph(icon.brand);
70
+ // A brand the mark list does not carry is a mistake the test catches; at
71
+ // run time it falls back to the generic code glyph rather than to nothing.
72
+ if (glyph) return <TechIcon glyph={glyph} size={size} className={className} />;
73
+ return <CodeXml size={size} aria-hidden className={`block shrink-0 ${className}`} />;
74
+ }
75
+ const Glyph = GLYPHS[icon.lucide];
76
+ return <Glyph size={size} aria-hidden className={`block shrink-0 ${className}`} />;
77
+ }
@@ -1,13 +1,29 @@
1
- // One problem, as a row: what the near end is, where both ends live, and
2
- // the words for what is wrong with the edge between them. The problems page
3
- // lists every one of these; the overview shows the first few.
1
+ // One problem, as a row: the edge, the words for what is wrong with it,
2
+ // where it is met in the flows, and where the code is.
3
+ //
4
+ // The row is light on purpose: under a rule's heading on the Problems page
5
+ // the rule's words are said once, above, and the row is the edge and its
6
+ // note. On its own - the overview shows the first few - it wears the rule's
7
+ // words too, as a link to the rule. The rule's subject says where the near
8
+ // end leads: an event has a page, a table has a canvas, a channel, a call
9
+ // and a deployment are shown on their service's page.
4
10
 
5
11
  import { Link } from "react-router";
12
+ import { ExternalLink } from "lucide-react";
13
+ import { catalog, index } from "../data";
6
14
  import type { Problem } from "../lib/derive";
7
15
  import { ctxStyle } from "../lib/context-color";
16
+ import type { Kind } from "../lib/kinds";
8
17
  import { staggerStyle } from "../lib/motion";
18
+ import { flowsOfProblem } from "../lib/problem-flows";
19
+ import { useProblemRules } from "../lib/problem-rules";
20
+ import type { RuleSubject } from "../lib/problem-rules";
21
+ import { sourceHref } from "../lib/source-link";
9
22
  import {
23
+ aggregatePath,
10
24
  eventPath,
25
+ flowPath,
26
+ paths,
11
27
  servicePath,
12
28
  storePath,
13
29
  tablePath,
@@ -17,171 +33,175 @@ import { KindIcon } from "./kind";
17
33
  import { Ident } from "./Ident";
18
34
 
19
35
  /** The icon a problem row carries: what the near end of the edge IS. */
20
- const KIND_OF: Record<Problem["kind"], "service" | "event" | "table"> = {
21
- rpc: "service",
36
+ export const ICON_OF: Record<RuleSubject, Kind> = {
37
+ service: "service",
38
+ call: "service",
39
+ copy: "service",
40
+ event: "event",
22
41
  consumer: "event",
23
- "cross-service-fk": "table",
24
- "cross-service-lineage": "table",
25
- "shared-store": "table",
26
- "persistence-drift": "table",
27
- "column-type": "table",
28
- "outbox-payload": "table",
29
- "proto-missing": "service",
30
- "proto-drift": "service",
31
- "shared-channel": "event",
32
- "channel-undeclared": "event",
33
- "channel-unpublished": "service",
34
- "message-encoding": "service",
35
- "subscription-unresolved": "service",
42
+ channel: "event",
43
+ subscription: "service",
44
+ table: "table",
45
+ column: "table",
46
+ deployment: "service",
47
+ flow: "flow",
48
+ aggregate: "aggregate",
36
49
  };
37
50
 
38
- const KIND_NOTE: Record<Problem["kind"], string> = {
39
- rpc: "the provider of this call is not in the catalog",
40
- consumer: "this consumer of the event is not in the catalog",
41
- "cross-service-fk": "foreign key across a service boundary",
42
- "cross-service-lineage": "a value copied from another service's schema",
43
- "shared-store": "a second service writes this database",
44
- "persistence-drift": "this table no longer carries the aggregate it claims",
45
- "column-type": "column type and domain type disagree",
46
- "outbox-payload": "an outbox with no payload column",
47
- "proto-missing":
48
- "the provider is in the catalog but answers on no such method",
49
- "proto-drift": "the vendored proto and provider schema disagree",
50
- "shared-channel": "a second service publishes on this channel",
51
- "channel-undeclared":
52
- "this event goes out on a channel the service does not declare",
53
- "channel-unpublished": "a declared channel no event of this service names",
54
- "message-encoding": "publisher and subscriber use different payload encodings",
55
- "subscription-unresolved":
56
- "nothing in the catalog publishes what this service listens for",
57
- };
51
+ /** A table or a view, whichever the id turns out to name. */
52
+ function relationPath(id: string): string | null {
53
+ return tablePath(id) ?? viewPath(id);
54
+ }
55
+
56
+ /** A column's relation: its id minus the column, which is what the canvas can show. */
57
+ function columnPath(id: string): string | null {
58
+ return relationPath(id.split(".").slice(0, -1).join("."));
59
+ }
58
60
 
59
- /** Where the near end of a problem lives, by what kind of edge it is. */
60
- function nearPath(problem: Problem): string | null {
61
- switch (problem.kind) {
62
- case "rpc":
63
- // The near end is the CALLING service either way. `rpc` is a call whose
64
- // peer is nobody; this is a call whose peer is known and whose method is
65
- // not - and in both the thing to go and look at is the caller.
66
- case "proto-missing":
67
- case "proto-drift":
68
- return servicePath(problem.service);
61
+ /** Where the near end of a problem lives, by what the rule is about. */
62
+ export function nearPath(over: RuleSubject, problem: Problem): string | null {
63
+ switch (over) {
64
+ case "event":
69
65
  case "consumer":
70
- case "channel-undeclared":
71
66
  return eventPath(problem.id);
72
- // The near end is an event when the service has one on the channel, and
73
- // the service itself when the claim comes from its document alone.
74
- case "shared-channel":
75
- return eventPath(problem.id) ?? servicePath(problem.service);
76
- case "channel-unpublished":
77
- case "message-encoding":
78
- case "subscription-unresolved":
79
- return servicePath(problem.service);
80
- case "shared-store":
81
- case "persistence-drift":
82
- case "outbox-payload":
83
- return tablePath(problem.id);
84
- // The id is "<relation>.<column>", so the relation is its own id minus the
85
- // last segment — and the relation is what the canvas can actually show.
86
- case "cross-service-fk":
87
- case "column-type":
88
- return relationPath(problem.id.split(".").slice(0, -1).join("."));
89
- // Lineage is stated on a column, except when a view states it whole, so
90
- // the id is one or the other and both are looked up the same way.
91
- case "cross-service-lineage":
92
- return (
93
- relationPath(problem.id) ??
94
- relationPath(problem.id.split(".").slice(0, -1).join("."))
95
- );
67
+ // A channel row is an event of the service when it has one on the
68
+ // address, and the service itself when the claim comes from its document.
69
+ case "channel":
70
+ return eventPath(problem.id) ?? (problem.service ? servicePath(problem.service) : null);
71
+ case "table":
72
+ return relationPath(problem.id);
73
+ case "column":
74
+ return columnPath(problem.id);
75
+ case "flow":
76
+ return flowPath(problem.id);
77
+ case "aggregate":
78
+ return aggregatePath(problem.id);
79
+ case "service":
80
+ case "call":
81
+ case "copy":
82
+ case "subscription":
83
+ case "deployment":
84
+ return problem.service ? servicePath(problem.service) : null;
96
85
  }
97
86
  }
98
87
 
99
- /** A table or a view, whichever the id turns out to name. */
100
- function relationPath(id: string): string | null {
101
- return tablePath(id) ?? viewPath(id);
88
+ /** Where the far end lives, when it is the id of something the catalog has. */
89
+ function peerPath(peer: string): string | null {
90
+ if (!peer) return null;
91
+ return servicePath(peer) ?? eventPath(peer) ?? relationPath(peer) ?? columnPath(peer) ?? storePath(peer);
102
92
  }
103
93
 
104
- /** Where the far end lives, when the catalog knows it. */
105
- function peerPath(problem: Problem): string | null {
106
- switch (problem.kind) {
107
- case "cross-service-fk":
108
- return tablePath(problem.peer);
109
- case "cross-service-lineage":
110
- return (
111
- relationPath(problem.peer) ??
112
- relationPath(problem.peer.split(".").slice(0, -1).join("."))
113
- );
114
- case "shared-store":
115
- case "shared-channel":
116
- case "message-encoding":
117
- return servicePath(problem.peer);
118
- case "outbox-payload":
119
- return storePath(problem.peer);
120
- default:
121
- return null;
122
- }
94
+ /** An http(s) URL the note ends with, for a row whose far end is a page elsewhere - the deployer's. */
95
+ function urlIn(note: string | undefined): string | null {
96
+ const match = note?.match(/https?:\/\/\S+$/);
97
+ return match ? match[0] : null;
123
98
  }
124
99
 
125
- export function ProblemRow({ problem, index }: { problem: Problem; index: number }) {
126
- const near = nearPath(problem);
127
- const peerTo = peerPath(problem);
128
- const tone =
129
- problem.severity === "error"
130
- ? "var(--status-unresolved)"
131
- : "var(--status-declared)";
100
+ const MAX_FLOWS = 4;
101
+
102
+ export function ProblemRow({
103
+ problem,
104
+ index: at,
105
+ showRule = true,
106
+ }: {
107
+ problem: Problem;
108
+ index: number;
109
+ /** Say which rule made the row. Off under a heading that already says so. */
110
+ showRule?: boolean;
111
+ }) {
112
+ const rules = useProblemRules();
113
+ const rule = rules.find((candidate) => candidate.id === problem.rule);
114
+ const over = rule?.over ?? "service";
115
+ const near = nearPath(over, problem);
116
+ const peerTo = peerPath(problem.peer);
117
+ const note = rule?.note ?? problem.rule;
118
+ const service = problem.service ? index.serviceById.get(problem.service) : undefined;
119
+ const source = problem.source ? sourceHref(problem.source, service) : null;
120
+ const url = over === "deployment" ? urlIn(problem.note) : null;
121
+ const text = url && problem.note ? problem.note.slice(0, problem.note.length - url.length).trim() : problem.note;
122
+ const flows = flowsOfProblem(catalog, index, over, problem);
123
+ const tone = problem.severity === "error" ? "text-unresolved" : "text-declared";
132
124
 
133
125
  return (
134
- <div
135
- className="stagger-in flex flex-wrap items-baseline gap-x-3 gap-y-1 rounded-control border px-3 py-2"
136
- style={{ ...staggerStyle(index), borderColor: tone }}
137
- >
138
- <KindIcon kind={KIND_OF[problem.kind]} />
139
- {near ? (
140
- <Link
141
- to={near}
142
- data-nav-item
143
- className="mono rounded-control text-accent hover:underline"
144
- title={problem.id}
145
- >
146
- {problem.id}
147
- </Link>
148
- ) : (
149
- <Ident value={problem.id} />
150
- )}
151
- <span aria-hidden className="text-muted">
152
-
153
- </span>
154
- {peerTo ? (
155
- <Link
156
- to={peerTo}
157
- className="mono rounded-control hover:underline"
158
- style={{ color: tone }}
159
- title={problem.peer}
160
- >
161
- {problem.peer}
162
- </Link>
163
- ) : (
164
- <Ident
165
- value={problem.peer}
166
- className={
167
- problem.severity === "error" ? "text-unresolved" : "text-declared"
168
- }
169
- title={`${problem.peer} — ${KIND_NOTE[problem.kind]}. Click to copy.`}
170
- />
171
- )}
172
- <span className="chip ctx" style={ctxStyle(problem.context)}>
173
- <span aria-hidden className="dot" />
174
- {problem.context}
175
- </span>
176
- <span className="mono ml-auto text-muted">{KIND_NOTE[problem.kind]}</span>
177
- {problem.note ? (
178
- <p className="w-full border-l-2 pl-2 border-line-strong text-muted">
179
- {problem.note}
180
- </p>
181
- ) : null}
182
- {problem.source ? (
183
- <div className="mono w-full text-muted">
184
- <Ident value={problem.source} />
126
+ <div className="stagger-in px-3 py-2.5" style={staggerStyle(at)} data-rule={problem.rule}>
127
+ <div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
128
+ <KindIcon kind={ICON_OF[over]} className="self-center" />
129
+ {near ? (
130
+ <Link to={near} data-nav-item className="mono rounded-control text-accent hover:underline" title={problem.id}>
131
+ {problem.id}
132
+ </Link>
133
+ ) : (
134
+ <Ident value={problem.id} />
135
+ )}
136
+ {/* A rule may name no far end; then there is no arrow to draw. */}
137
+ {problem.peer ? (
138
+ <>
139
+ <span aria-hidden className="text-faint">
140
+
141
+ </span>
142
+ {peerTo ? (
143
+ <Link to={peerTo} className={`mono rounded-control hover:underline ${tone}`} title={problem.peer}>
144
+ {problem.peer}
145
+ </Link>
146
+ ) : (
147
+ <Ident value={problem.peer} className={tone} title={`${problem.peer} — ${note}. Click to copy.`} />
148
+ )}
149
+ </>
150
+ ) : null}
151
+ {problem.context ? (
152
+ <span className="chip ctx" style={ctxStyle(problem.context)}>
153
+ <span aria-hidden className="dot" />
154
+ {problem.context}
155
+ </span>
156
+ ) : null}
157
+ {showRule ? (
158
+ <Link
159
+ to={`${paths.settingsRules()}#rule-${problem.rule}`}
160
+ className="mono ml-auto max-w-[45%] truncate rounded-control text-muted hover:text-ink hover:underline"
161
+ title={`rule ${problem.rule}${rule?.action ? ` — ${rule.action}` : ""}`}
162
+ >
163
+ {note}
164
+ </Link>
165
+ ) : null}
166
+ </div>
167
+ {text ? <p className="mt-1 max-w-prose text-muted">{text}</p> : null}
168
+ {flows.length > 0 || source || url ? (
169
+ <div className="mono mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-muted">
170
+ {flows.length > 0 ? (
171
+ <span className="flex flex-wrap items-center gap-1">
172
+ <span className="text-faint">in</span>
173
+ {flows.slice(0, MAX_FLOWS).map((hit) => (
174
+ <Link
175
+ key={hit.flow.id}
176
+ to={hit.stepId ? paths.flowStep(hit.flow.slug, hit.stepId) : paths.flow(hit.flow.slug)}
177
+ className="chip border-line-strong hover:border-accent hover:text-accent"
178
+ title={hit.stepId ? `${hit.flow.name}, step ${hit.number}` : hit.flow.name}
179
+ >
180
+ {hit.flow.name}
181
+ {hit.number ? <span className="text-faint">·{hit.number}</span> : null}
182
+ </Link>
183
+ ))}
184
+ {flows.length > MAX_FLOWS ? (
185
+ <span className="text-faint" title={flows.slice(MAX_FLOWS).map((hit) => hit.flow.name).join(", ")}>
186
+ +{flows.length - MAX_FLOWS}
187
+ </span>
188
+ ) : null}
189
+ </span>
190
+ ) : null}
191
+ {problem.source ? (
192
+ source ? (
193
+ <a href={source} target="_blank" rel="noreferrer" className="truncate rounded-control text-accent hover:underline" title={problem.source}>
194
+ {problem.source} ↗
195
+ </a>
196
+ ) : (
197
+ <Ident value={problem.source} className="truncate" />
198
+ )
199
+ ) : null}
200
+ {url ? (
201
+ <a href={url} target="_blank" rel="noreferrer" className="flex items-center gap-1 rounded-control text-accent hover:underline">
202
+ <ExternalLink size={12} aria-hidden /> open in the deployer
203
+ </a>
204
+ ) : null}
185
205
  </div>
186
206
  ) : null}
187
207
  </div>
@@ -0,0 +1,14 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ import { expect, it } from "vitest";
3
+ import { RelationEvidencePanel } from "./RelationEvidence";
4
+
5
+ it("explains an ambiguous binding without choosing a candidate", () => {
6
+ const html = renderToStaticMarkup(<RelationEvidencePanel items={[
7
+ { kind: "binding", rule: "provider-signature", source: "di.go:12", symbol: "Build" },
8
+ { kind: "unresolved", rule: "ambiguous-binding", candidates: ["First", "Second"] },
9
+ ]} />);
10
+ for (const value of ["Why this relation exists", "inferred", "di.go:12", "Several implementations remain possible", "First", "Second"]) expect(html).toContain(value);
11
+ });
12
+ it("shows the absence of evidence explicitly", () => {
13
+ expect(renderToStaticMarkup(<RelationEvidencePanel items={[]} />)).toContain("No source evidence recorded");
14
+ });
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+ import type { RelationEvidence } from "../catalog";
3
+
4
+ const labels: Record<RelationEvidence["kind"], string> = {
5
+ "call-site": "Call site", function: "Function", binding: "Dependency binding",
6
+ contract: "Contract", resolution: "Catalog resolution", unresolved: "Unresolved",
7
+ };
8
+ const rules: Record<string, string> = {
9
+ "source-expression": "Expression in source",
10
+ "source-function": "Enclosing source function",
11
+ "provider-signature": "Provider signature (inferred)",
12
+ "store-port-convention": "Storage field, type or package name (inferred)",
13
+ "domain-port-convention": "Domain port mapped to the configured store (inferred)",
14
+ "ambiguous-binding": "Several implementations remain possible",
15
+ "generated-client-method": "Method declared by generated client",
16
+ "http-expression": "Outbound HTTP expression",
17
+ "analyzed-call-path": "Static analysis path; not a runtime trace",
18
+ "client-contract": "Client contract",
19
+ "catalog-contract": "Matching interface and method",
20
+ "event-declaration": "Declared event",
21
+ "repository-sql-call": "SQL operation inside repository",
22
+ "full-path": "Recovered full HTTP path",
23
+ "exact-route": "Exact HTTP route",
24
+ "unique-suffix": "Unique route suffix (heuristic)",
25
+ "continuation-entrypoint": "Matched continuation entrypoint",
26
+ "response-to-request": "Response associated with request",
27
+ "unresolved-target": "No unique target established",
28
+ "sql-alter-table": "ALTER TABLE in migration",
29
+ "sql-view-definition": "SQL view definition",
30
+ "sql-create-table": "CREATE TABLE in migration",
31
+ "domain-root-and-repository-layout": "Domain root and repository convention (inferred)",
32
+ "migration-aggregate-annotation": "Explicit aggregate annotation in migration",
33
+ };
34
+
35
+ /** One explanation surface for all flow kinds and schema relationships. */
36
+ export function RelationEvidencePanel({ items, renderSource }: {
37
+ items: readonly RelationEvidence[];
38
+ renderSource?: (source: string) => ReactNode;
39
+ }) {
40
+ return <section aria-label="Why this relation exists" className="my-3 rounded-card border border-line bg-surface p-3">
41
+ <h3 className="label mb-2">Why this relation exists</h3>
42
+ {items.length === 0 ? <p className="text-muted">No source evidence recorded in this fragment.</p> :
43
+ <ol className="space-y-3">
44
+ {items.map((item, i) => <li key={`${item.kind}-${i}`} className="min-w-0 border-l-2 border-line pl-3">
45
+ <div className={item.kind === "unresolved" ? "text-[var(--status-unresolved)]" : "text-ink"}>{labels[item.kind]}</div>
46
+ <div className="text-muted">{rules[item.rule] ?? item.rule}</div>
47
+ {item.symbol ? <div className="mono break-all">{item.symbol}</div> : null}
48
+ {item.source ? <div className="mono break-all text-muted">{renderSource ? renderSource(item.source) : item.source}</div> : null}
49
+ {item.candidates?.length ? <div className="mt-1"><span className="text-muted">Candidates</span><ul>{item.candidates.map((candidate) => <li className="mono break-all" key={candidate}>{candidate}</li>)}</ul></div> : null}
50
+ </li>)}
51
+ </ol>}
52
+ </section>;
53
+ }
@@ -0,0 +1,22 @@
1
+ // What the source says a field's value must satisfy, as marks beside the
2
+ // type: "required", "≥ 1 chars", "one of USD, EUR". One component so the
3
+ // schema table, the field tree and the inline shape of a request say it the
4
+ // same way; the words come from lib/rules.
5
+
6
+ import type { Field } from "../catalog";
7
+ import { ruleMarks } from "../lib/rules";
8
+
9
+ export function RuleMarks({ field }: { field: Field }) {
10
+ const marks = ruleMarks(field);
11
+ if (marks.length === 0) return null;
12
+
13
+ return (
14
+ <>
15
+ {marks.map((mark) => (
16
+ <span key={mark.title} className="type-mark rule-mark" title={mark.title}>
17
+ {mark.text}
18
+ </span>
19
+ ))}
20
+ </>
21
+ );
22
+ }
@@ -10,6 +10,7 @@ import { Fragment, useState } from "react";
10
10
  import { ChevronDown, ChevronRight } from "lucide-react";
11
11
  import type { Field, RpcEnum } from "../catalog";
12
12
  import { parseType } from "../lib/shape";
13
+ import { RuleMarks } from "./RuleMarks";
13
14
 
14
15
  /**
15
16
  * The enum a field's type names, among the ones the interface declares. By
@@ -91,26 +92,29 @@ export function ShapeRows({
91
92
  primitive, so a reader can tell which names are worth following
92
93
  without the row becoming a link it is not. */}
93
94
  <td className="mono py-0.5 pr-3 whitespace-nowrap text-muted">
94
- {set ? (
95
- <button
96
- type="button"
97
- onClick={() => toggle(field.name)}
98
- aria-expanded={shown}
99
- className="inline-flex items-center gap-1 rounded-control hover:text-ink"
100
- title={`${set.name}: ${set.values.length} values click to ${shown ? "hide" : "show"} them`}
101
- >
102
- {shown ? (
103
- <ChevronDown size={11} aria-hidden />
104
- ) : (
105
- <ChevronRight size={11} aria-hidden />
106
- )}
107
- {field.type}
108
- </button>
109
- ) : field.ref ? (
110
- `${field.type} →`
111
- ) : (
112
- field.type
113
- )}
95
+ <span className="inline-flex items-center gap-1.5">
96
+ {set ? (
97
+ <button
98
+ type="button"
99
+ onClick={() => toggle(field.name)}
100
+ aria-expanded={shown}
101
+ className="inline-flex items-center gap-1 rounded-control hover:text-ink"
102
+ title={`${set.name}: ${set.values.length} values — click to ${shown ? "hide" : "show"} them`}
103
+ >
104
+ {shown ? (
105
+ <ChevronDown size={11} aria-hidden />
106
+ ) : (
107
+ <ChevronRight size={11} aria-hidden />
108
+ )}
109
+ {field.type}
110
+ </button>
111
+ ) : field.ref ? (
112
+ `${field.type} →`
113
+ ) : (
114
+ field.type
115
+ )}
116
+ <RuleMarks field={field} />
117
+ </span>
114
118
  </td>
115
119
  <td className="py-0.5 text-muted">{field.doc}</td>
116
120
  </tr>