@shortlink-org/portolan 0.1.0 → 0.2.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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,107 +0,0 @@
1
- package main
2
-
3
- import (
4
- "strings"
5
-
6
- "github.com/shortlink-org/portolan/catalog"
7
- )
8
-
9
- // renderModules gives schema registries a first-class place in the generated
10
- // documentation. A module is shared by its publisher and every vendored
11
- // consumer, so hiding it inside one service loses the relationship readers
12
- // usually need it for.
13
- func (s *site) renderModules() {
14
- if len(s.cat.Modules) == 0 {
15
- return
16
- }
17
-
18
- const self = "modules/README.md"
19
- var b strings.Builder
20
- b.WriteString("# Schema modules\n\n")
21
- b.WriteString(s.stamp() + "\n")
22
- rows := make([][]string, 0, len(s.cat.Modules))
23
- for i := range s.cat.Modules {
24
- module := &s.cat.Modules[i]
25
- rows = append(rows, []string{
26
- s.ref(self, module.ID, orDefault(module.Name, module.ID)),
27
- s.ref(self, module.Owner, module.Owner),
28
- module.Registry,
29
- module.Commit,
30
- plural(len(module.Packages), "package"),
31
- })
32
- }
33
- b.WriteString(table([]string{"Module", "Publisher", "Registry", "Commit", "Packages"}, rows))
34
- s.b.file(self, b.String())
35
-
36
- for i := range s.cat.Modules {
37
- s.renderModule(&s.cat.Modules[i])
38
- }
39
- }
40
-
41
- func (s *site) renderModule(module *catalog.ProtoModule) {
42
- self := s.pathOf[module.ID]
43
- owner := s.services[module.Owner]
44
- var b strings.Builder
45
- b.WriteString("# " + orDefault(module.Name, module.ID) + "\n\n")
46
- b.WriteString(s.stamp() + "\n")
47
- b.WriteString(defList([][]string{
48
- {"Id", code(module.ID)},
49
- {"Registry", module.Registry},
50
- {"Publisher", s.ref(self, module.Owner, module.Owner)},
51
- {"Commit", code(module.Commit)},
52
- {"Digest", code(module.Digest)},
53
- {"Source", s.source(self, module.Source, owner)},
54
- }))
55
-
56
- packages := make([][]string, 0, len(module.Packages))
57
- for _, pkg := range module.Packages {
58
- packages = append(packages, []string{code(pkg)})
59
- }
60
- section(&b, "Packages", table([]string{"Package"}, packages))
61
-
62
- files := make([][]string, 0, len(module.Files))
63
- for _, file := range module.Files {
64
- files = append(files, []string{s.source(self, file, owner)})
65
- }
66
- section(&b, "Files", table([]string{"File"}, files))
67
-
68
- deps := make([][]string, 0, len(module.Deps))
69
- for _, id := range module.Deps {
70
- deps = append(deps, []string{s.ref(self, id, id)})
71
- }
72
- section(&b, "Dependencies", table([]string{"Module"}, deps))
73
-
74
- users := make([][]string, 0)
75
- for i := range s.cat.Contexts {
76
- for j := range s.cat.Contexts[i].Services {
77
- svc := &s.cat.Contexts[i].Services[j]
78
- for _, id := range svc.Modules {
79
- if id != module.ID {
80
- continue
81
- }
82
- access := "reads"
83
- if svc.ID == module.Owner {
84
- access = "publishes"
85
- }
86
- users = append(users, []string{s.ref(self, svc.ID, svc.Name), access})
87
- }
88
- }
89
- }
90
- section(&b, "Used by", table([]string{"Service", "Access"}, users))
91
-
92
- var interfaces strings.Builder
93
- for i := range s.cat.Contexts {
94
- for j := range s.cat.Contexts[i].Services {
95
- svc := &s.cat.Contexts[i].Services[j]
96
- for k := range svc.Provides {
97
- provided := &svc.Provides[k]
98
- if provided.Module == module.ID {
99
- interfaces.WriteString(s.providesBlock(self, []catalog.RpcService{*provided}, svc))
100
- }
101
- }
102
- }
103
- }
104
- section(&b, "Interfaces", interfaces.String())
105
-
106
- s.b.file(self, b.String())
107
- }
@@ -1,39 +0,0 @@
1
- // Package main is portolan-gen-markdown: a catalog in, a directory of markdown
2
- // out.
3
- //
4
- // The plugin never touches the filesystem. It is handed a catalog on stdin and
5
- // answers with the files it would like written; the host writes them. That is
6
- // what lets the same binary run as a sandboxed wasm module with no directory
7
- // preopened at all, and it is what makes `--check` cheap: the host compares
8
- // what came back against what is on disk without the plugin knowing there is a
9
- // disk.
10
- package main
11
-
12
- import (
13
- "fmt"
14
-
15
- "github.com/shortlink-org/portolan/plugin"
16
- )
17
-
18
- // Options are this plugin's own settings, passed through from the manifest.
19
- type Options struct {
20
- // Title heads the index page. The catalog does not name the estate, so
21
- // this is the one thing the plugin cannot derive.
22
- Title string `json:"title,omitempty"`
23
- // SourceBaseURL is the stable tree URL for this repository, such as a
24
- // default branch. External repositories still use their immutable RepoPin.
25
- SourceBaseURL string `json:"sourceBaseUrl,omitempty"`
26
- }
27
-
28
- // builder is plugin.Builder with the formatting the render code wants. The
29
- // shared one deliberately takes a plain string: a protocol type that knows
30
- // about printf verbs is a protocol type doing somebody else's job.
31
- type builder struct {
32
- plugin.Builder
33
- }
34
-
35
- func (b *builder) file(name, contents string) { b.File(name, contents) }
36
-
37
- func (b *builder) warn(ref, format string, args ...any) {
38
- b.Warn(ref, fmt.Sprintf(format, args...))
39
- }
@@ -1,176 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "net/url"
6
- "os"
7
- "path"
8
- "regexp"
9
- "slices"
10
- "strings"
11
- "testing"
12
-
13
- "github.com/shortlink-org/portolan/catalog"
14
- "github.com/shortlink-org/portolan/plugin"
15
- )
16
-
17
- var localLink = regexp.MustCompile(`\]\(([^\s)]+)(?:\s+[^)]*)?\)`)
18
- var explicitAnchor = regexp.MustCompile(`<a id="([^"]+)"></a>`)
19
-
20
- func fixtureCatalog(t *testing.T) catalog.Catalog {
21
- t.Helper()
22
- raw, err := os.ReadFile("testdata/catalog.json")
23
- if err != nil {
24
- t.Fatal(err)
25
- }
26
- var cat catalog.Catalog
27
- if err := json.Unmarshal(raw, &cat); err != nil {
28
- t.Fatal(err)
29
- }
30
- return cat
31
- }
32
-
33
- func renderedFiles(resp plugin.Response) map[string]string {
34
- out := make(map[string]string, len(resp.Files))
35
- for _, file := range resp.Files {
36
- out[file.Name] = file.Contents
37
- }
38
- return out
39
- }
40
-
41
- func TestEveryGeneratedLinkAndAnchorResolves(t *testing.T) {
42
- files := renderedFiles(render(plugin.Request{Catalog: fixtureCatalog(t)}, Options{}))
43
- for name, contents := range files {
44
- // This concatenation keeps each page's original relative links and names
45
- // the page in an HTML comment; it is not itself a linkable directory.
46
- if name == llmsFullPage {
47
- continue
48
- }
49
- for _, match := range localLink.FindAllStringSubmatch(contents, -1) {
50
- target := match[1]
51
- if strings.HasPrefix(target, "#") {
52
- target = name + target
53
- }
54
- if parsed, err := url.Parse(target); err != nil || parsed.IsAbs() {
55
- continue
56
- }
57
- parts := strings.SplitN(target, "#", 2)
58
- file := path.Clean(path.Join(path.Dir(name), parts[0]))
59
- body, ok := files[file]
60
- if !ok {
61
- t.Errorf("%s links to missing %s", name, target)
62
- continue
63
- }
64
- if len(parts) == 2 && parts[1] != "" && !hasAnchor(body, parts[1]) {
65
- t.Errorf("%s links to missing anchor %s in %s", name, parts[1], file)
66
- }
67
- }
68
- }
69
- }
70
-
71
- func hasAnchor(markdown, wanted string) bool {
72
- for _, match := range explicitAnchor.FindAllStringSubmatch(markdown, -1) {
73
- if match[1] == wanted {
74
- return true
75
- }
76
- }
77
- for _, line := range strings.Split(markdown, "\n") {
78
- if heading := strings.TrimLeft(line, "#"); heading != line && strings.HasPrefix(heading, " ") && anchor(strings.TrimSpace(heading)) == wanted {
79
- return true
80
- }
81
- }
82
- return false
83
- }
84
-
85
- func TestRenderingIgnoresNonSemanticEntityOrder(t *testing.T) {
86
- cat := fixtureCatalog(t)
87
- want := renderedFiles(render(plugin.Request{Catalog: cat}, Options{}))
88
- slices.Reverse(cat.Contexts)
89
- for i := range cat.Contexts {
90
- slices.Reverse(cat.Contexts[i].Services)
91
- for j := range cat.Contexts[i].Services {
92
- slices.Reverse(cat.Contexts[i].Services[j].Aggregates)
93
- slices.Reverse(cat.Contexts[i].Services[j].Provides)
94
- slices.Reverse(cat.Contexts[i].Services[j].Consumes)
95
- }
96
- }
97
- slices.Reverse(cat.Stores)
98
- slices.Reverse(cat.Modules)
99
- slices.Reverse(cat.Flows)
100
- slices.Reverse(cat.Adrs)
101
- slices.Reverse(cat.Terms)
102
- slices.Reverse(cat.Externals)
103
- got := renderedFiles(render(plugin.Request{Catalog: cat}, Options{}))
104
- if !reflectFiles(want, got) {
105
- t.Fatal("permuting entity sets changed the generated bytes")
106
- }
107
- }
108
-
109
- func reflectFiles(a, b map[string]string) bool {
110
- if len(a) != len(b) {
111
- return false
112
- }
113
- for name, value := range a {
114
- if b[name] != value {
115
- return false
116
- }
117
- }
118
- return true
119
- }
120
-
121
- func TestSemanticFieldsAndImmutableSourcesAreRendered(t *testing.T) {
122
- repo := "https://github.com/example/billing/blob/main"
123
- cat := fixtureCatalog(t)
124
- cat.Flows[0].Trigger = &catalog.FlowTrigger{Kind: "callback", Label: "POST /callbacks/paid", Confidence: "high"}
125
- files := renderedFiles(render(plugin.Request{Catalog: cat}, Options{SourceBaseURL: repo}))
126
- service := files["billing/invoices/README.md"]
127
- for _, want := range []string{"Schema modules", "Channels", "GET", "RaiseRequest", "server", "deprecated", "flow.raise-invoice#s3", repo + "/examples/billing/api/invoices.proto"} {
128
- if !strings.Contains(service, want) {
129
- t.Errorf("service page omits %q", want)
130
- }
131
- }
132
- if !strings.Contains(files["modules/example-billing.md"], "Billing contracts") {
133
- t.Error("module page was not generated")
134
- }
135
- flow := files["flows/"+cat.Flows[0].Slug+".md"]
136
- if !strings.Contains(flow, "**Trigger:** `callback` · POST /callbacks/paid") || !strings.Contains(flow, "**Root confidence:** high") {
137
- t.Error("flow trigger evidence was not rendered")
138
- }
139
- }
140
-
141
- func TestNeutralComponentDoesNotPretendToHaveADomainModel(t *testing.T) {
142
- cat := catalog.Catalog{
143
- GeneratedAt: "2026-09-06T00:00:00Z",
144
- Commit: "abc",
145
- Contexts: []catalog.BoundedContext{{
146
- ID: "avia", Slug: "avia", Name: "Avia", Kind: catalog.GroupKindSystem,
147
- Services: []catalog.Service{{
148
- ID: "avia.support", Slug: "support", Name: "Support", Kind: catalog.ComponentKindApplication,
149
- Technologies: []string{"Go", "Kafka"}, Provides: []catalog.RpcService{}, Consumes: []catalog.RpcCall{}, Aggregates: []catalog.Aggregate{},
150
- }},
151
- }},
152
- Defs: map[string]catalog.TypeDef{}, Flows: []catalog.Flow{}, Adrs: []catalog.Adr{},
153
- }
154
- files := renderedFiles(render(plugin.Request{Catalog: cat}, Options{}))
155
- group := files["avia/README.md"]
156
- component := files["avia/support/README.md"]
157
- if !strings.Contains(group, "## Components") || !strings.Contains(group, "Technologies") {
158
- t.Fatal(group)
159
- }
160
- if !strings.Contains(component, "**Kind:** application") || !strings.Contains(component, "Go, Kafka") {
161
- t.Fatal(component)
162
- }
163
- if strings.Contains(component, "## Aggregates") {
164
- t.Fatal("neutral component gained an empty DDD section:\n" + component)
165
- }
166
- }
167
-
168
- func TestExternalSourceUsesItsImmutablePin(t *testing.T) {
169
- owner := &catalog.Service{Repo: "gitlab.example/acme/payments", Path: "vendor/repos/acme/payments"}
170
- s := &site{cat: catalog.Catalog{Repos: []catalog.RepoPin{{Repo: owner.Repo, Commit: strings.Repeat("b", 40)}}}, opts: Options{SourceBaseURL: "https://github.com/example/billing/blob/main"}}
171
- got := s.source("billing/README.md", "vendor/repos/acme/payments/api/openapi.yaml:12", owner)
172
- want := "https://gitlab.example/acme/payments/-/blob/" + strings.Repeat("b", 40) + "/api/openapi.yaml#L12"
173
- if !strings.Contains(got, want) {
174
- t.Fatalf("source link = %s", got)
175
- }
176
- }
@@ -1,38 +0,0 @@
1
- package main
2
-
3
- import (
4
- "strings"
5
- "testing"
6
-
7
- "github.com/shortlink-org/portolan/catalog"
8
- "github.com/shortlink-org/portolan/plugin"
9
- )
10
-
11
- func TestRendersRedisKeyPatterns(t *testing.T) {
12
- cat := catalog.Catalog{
13
- Contexts: []catalog.BoundedContext{{
14
- ID: "sales", Slug: "sales", Name: "Sales", Services: []catalog.Service{{
15
- ID: "sales.catalog", Slug: "catalog", Name: "Catalog", Provides: []catalog.RpcService{}, Consumes: []catalog.RpcCall{}, Aggregates: []catalog.Aggregate{}, Stores: []string{"sales.catalog.redis"},
16
- }},
17
- }},
18
- Defs: map[string]catalog.TypeDef{}, Flows: []catalog.Flow{}, Adrs: []catalog.Adr{},
19
- Stores: []catalog.Store{{
20
- ID: "sales.catalog.redis", Slug: "redis", Name: "Catalog Redis", Kind: catalog.StoreKindRedis, Owner: "sales.catalog", Tables: []catalog.Table{},
21
- Keyspaces: []catalog.RedisKeyspace{{Pattern: "session:{id}", Operations: []catalog.RedisOperation{catalog.RedisOperationRead, catalog.RedisOperationWrite}, TTL: "24h", Value: "Session", Source: "cache.go:20"}},
22
- }},
23
- }
24
-
25
- response := render(plugin.Request{Catalog: cat}, Options{})
26
- var page string
27
- for _, file := range response.Files {
28
- if file.Name == "sales/catalog/stores/redis.md" {
29
- page = file.Contents
30
- break
31
- }
32
- }
33
- for _, want := range []string{"## Redis key patterns", "`session:{id}`", "read, write", "`Session`", "`24h`", "cache.go"} {
34
- if !strings.Contains(page, want) {
35
- t.Fatalf("Redis store page does not contain %q:\n%s", want, page)
36
- }
37
- }
38
- }
@@ -1,344 +0,0 @@
1
- package main
2
-
3
- import (
4
- "sort"
5
- "strings"
6
-
7
- "github.com/shortlink-org/portolan/catalog"
8
- "github.com/shortlink-org/portolan/plugin"
9
- )
10
-
11
- // site is the whole render: the catalog, the place every entity's page will
12
- // live, and the lookups that turn an id in the data into a link on a page.
13
- //
14
- // The layout is computed BEFORE anything is written. A page that mentions an
15
- // aggregate has to know where the aggregate's page will be, and working that
16
- // out at the moment of writing is how two pages end up disagreeing.
17
- type site struct {
18
- cat catalog.Catalog
19
- opts Options
20
- b *builder
21
-
22
- // pathOf maps a catalog id to the file that documents it. An id absent
23
- // from this map belongs to another source, which is a fact worth saying
24
- // rather than a link worth guessing.
25
- pathOf map[string]string
26
-
27
- services map[string]*catalog.Service
28
- contextOf map[string]*catalog.BoundedContext
29
- stores map[string]*catalog.Store
30
- modules map[string]*catalog.ProtoModule
31
- // eventPage maps an event id to the aggregate that publishes it, which is
32
- // the page that actually documents it.
33
- eventPage map[string]string
34
- // wireEvent resolves the message name used by an AsyncAPI channel to the
35
- // event page that documents that message's versions.
36
- wireEvent map[string]string
37
- // relationStore and relationName resolve a table or view id, which is what
38
- // foreign keys and lineage are written in, to the store that holds it and
39
- // the heading that documents it.
40
- relationStore map[string]string
41
- relationName map[string]string
42
- // adrsFor is keyed by what an ADR is scoped to: "org", a context id, or a
43
- // service id.
44
- adrsFor map[string][]*catalog.Adr
45
- // termsOf is a context's vocabulary, alphabetical. Keyed by context and
46
- // not by service: a glossary belongs to the boundary, and the file it was
47
- // read from happens to sit beside one of the services inside it.
48
- termsOf map[string][]*catalog.Term
49
- // methodOf is every interface method by "<interface>/<method>", which is
50
- // how a flow step names the call it makes.
51
- methodOf map[string]catalog.RpcMethod
52
- }
53
-
54
- func render(req plugin.Request, opts Options) plugin.Response {
55
- s := &site{
56
- cat: canonicalCatalog(req.Catalog),
57
- opts: opts,
58
- b: &builder{},
59
- pathOf: map[string]string{},
60
- services: map[string]*catalog.Service{},
61
- contextOf: map[string]*catalog.BoundedContext{},
62
- stores: map[string]*catalog.Store{},
63
- modules: map[string]*catalog.ProtoModule{},
64
- eventPage: map[string]string{},
65
- wireEvent: map[string]string{},
66
- adrsFor: map[string][]*catalog.Adr{},
67
- termsOf: map[string][]*catalog.Term{},
68
- methodOf: map[string]catalog.RpcMethod{},
69
-
70
- relationStore: map[string]string{},
71
- relationName: map[string]string{},
72
- }
73
-
74
- s.layout()
75
- s.renderIndex()
76
- s.renderTypes()
77
- s.renderModules()
78
- for i := range s.cat.Contexts {
79
- s.renderContext(&s.cat.Contexts[i])
80
- }
81
- for i := range s.cat.Stores {
82
- s.renderStore(&s.cat.Stores[i])
83
- }
84
- s.renderExternals()
85
- s.renderFlows()
86
- s.renderAdrs()
87
- s.renderLlms()
88
-
89
- return s.b.Response()
90
- }
91
-
92
- // layout decides where everything goes and builds the lookups. Nothing is
93
- // rendered here.
94
- func (s *site) layout() {
95
- for i := range s.cat.Contexts {
96
- ctx := &s.cat.Contexts[i]
97
- s.pathOf[ctx.ID] = ctx.Slug + "/README.md"
98
-
99
- for j := range ctx.Services {
100
- svc := &ctx.Services[j]
101
- dir := ctx.Slug + "/" + svc.Slug
102
- s.pathOf[svc.ID] = dir + "/README.md"
103
- s.services[svc.ID] = svc
104
- for j := range svc.Provides {
105
- provided := &svc.Provides[j]
106
- for k := range provided.Methods {
107
- s.methodOf[provided.ID+"/"+provided.Methods[k].Name] = provided.Methods[k]
108
- }
109
- }
110
- s.contextOf[svc.ID] = ctx
111
-
112
- for k := range svc.Aggregates {
113
- agg := &svc.Aggregates[k]
114
- s.pathOf[agg.ID] = dir + "/aggregates/" + agg.Slug + ".md"
115
-
116
- for e := range agg.Events {
117
- s.eventPage[agg.Events[e].ID] = agg.ID
118
- if wire := agg.Events[e].Wire; wire != nil && wire.Name != "" {
119
- s.wireEvent[wire.Name] = agg.Events[e].ID
120
- }
121
- }
122
- }
123
- }
124
- }
125
-
126
- for i := range s.cat.Modules {
127
- module := &s.cat.Modules[i]
128
- s.modules[module.ID] = module
129
- s.pathOf[module.ID] = "modules/" + module.Slug + ".md"
130
- }
131
-
132
- for i := range s.cat.Stores {
133
- store := &s.cat.Stores[i]
134
- s.stores[store.ID] = store
135
-
136
- for j := range store.Tables {
137
- s.relationStore[store.Tables[j].ID] = store.ID
138
- s.relationName[store.Tables[j].ID] = store.Tables[j].Name
139
- }
140
- for j := range store.Views {
141
- s.relationStore[store.Views[j].ID] = store.ID
142
- s.relationName[store.Views[j].ID] = store.Views[j].Name
143
- }
144
-
145
- // A store lives under the service that owns it. When the owner is in
146
- // another source there is no such directory, so it sits at the root
147
- // and the service page that would have linked it says why.
148
- if ownerPath, ok := s.pathOf[store.Owner]; ok {
149
- s.pathOf[store.ID] = parent(ownerPath) + "/stores/" + store.Slug + ".md"
150
- } else {
151
- s.pathOf[store.ID] = "stores/" + store.ID + ".md"
152
- s.b.warn(store.ID, "store %q is owned by %q, which is not in this catalog; its page sits at the root", store.ID, store.Owner)
153
- }
154
- }
155
-
156
- // An external sits at the root of the tree as it sits at the root of the
157
- // catalog: no context owns it, so no context's directory holds it.
158
- for i := range s.cat.Externals {
159
- ext := &s.cat.Externals[i]
160
- s.pathOf[ext.ID] = "externals/" + ext.Slug + ".md"
161
- for j := range ext.Provides {
162
- provided := &ext.Provides[j]
163
- for k := range provided.Methods {
164
- s.methodOf[provided.ID+"/"+provided.Methods[k].Name] = provided.Methods[k]
165
- }
166
- }
167
- }
168
-
169
- for i := range s.cat.Flows {
170
- s.pathOf[s.cat.Flows[i].ID] = "flows/" + s.cat.Flows[i].Slug + ".md"
171
- }
172
-
173
- for i := range s.cat.Terms {
174
- term := &s.cat.Terms[i]
175
- s.termsOf[term.Context] = append(s.termsOf[term.Context], term)
176
- }
177
- for _, terms := range s.termsOf {
178
- sort.Slice(terms, func(a, b int) bool {
179
- return strings.ToLower(terms[a].Name) < strings.ToLower(terms[b].Name)
180
- })
181
- }
182
-
183
- for i := range s.cat.Adrs {
184
- adr := &s.cat.Adrs[i]
185
- s.pathOf[adr.ID] = "adr/" + adr.ID + ".md"
186
-
187
- switch adr.Scope.Kind {
188
- case "context":
189
- s.adrsFor[adr.Scope.Context] = append(s.adrsFor[adr.Scope.Context], adr)
190
- case "service":
191
- s.adrsFor[adr.Scope.Service] = append(s.adrsFor[adr.Scope.Service], adr)
192
- default:
193
- s.adrsFor["org"] = append(s.adrsFor["org"], adr)
194
- }
195
- }
196
- }
197
-
198
- // ref renders a link to another page, or plain code when the target is not in
199
- // this catalog. The second case is the normal one for a merged estate: an
200
- // event consumer, an rpc peer or a store owner routinely lives in a source
201
- // this render never saw.
202
- func (s *site) ref(from, id, text string) string {
203
- if text == "" {
204
- text = id
205
- }
206
- if to, ok := s.pathOf[id]; ok {
207
- return link(text, from, to)
208
- }
209
-
210
- return code(text)
211
- }
212
-
213
- func (s *site) renderIndex() {
214
- const self = "README.md"
215
-
216
- var b strings.Builder
217
- b.WriteString("# " + s.title() + "\n\n")
218
- b.WriteString(s.stamp() + "\n")
219
-
220
- rows := make([][]string, 0, len(s.cat.Contexts))
221
- for i := range s.cat.Contexts {
222
- ctx := &s.cat.Contexts[i]
223
- names := make([]string, 0, len(ctx.Services))
224
- for j := range ctx.Services {
225
- names = append(names, s.ref(self, ctx.Services[j].ID, ctx.Services[j].Name))
226
- }
227
- rows = append(rows, []string{
228
- s.ref(self, ctx.ID, ctx.Name),
229
- string(ctx.Classification),
230
- strings.Join(names, ", "),
231
- firstLine(ctx.Summary),
232
- })
233
- }
234
- section(&b, "Contexts", table([]string{"Context", "Class", "Services", "Summary"}, rows))
235
-
236
- // The systems the estate calls and does not build, beside the contexts
237
- // rather than inside one: the reader asking "what do we depend on out
238
- // there" should not have to open every service to find out.
239
- externals := make([][]string, 0, len(s.cat.Externals))
240
- for i := range s.cat.Externals {
241
- ext := &s.cat.Externals[i]
242
- interfaces := make([]string, 0, len(ext.Provides))
243
- for j := range ext.Provides {
244
- interfaces = append(interfaces, code(ext.Provides[j].ID))
245
- }
246
- externals = append(externals, []string{
247
- s.ref(self, ext.ID, orDefault(ext.Name, ext.ID)),
248
- strings.Join(interfaces, ", "),
249
- firstLine(ext.Summary),
250
- })
251
- }
252
- section(&b, "Outside the estate", table([]string{"System", "Interfaces", "Summary"}, externals))
253
-
254
- modules := make([][]string, 0, len(s.cat.Modules))
255
- for i := range s.cat.Modules {
256
- module := &s.cat.Modules[i]
257
- modules = append(modules, []string{
258
- s.ref(self, module.ID, orDefault(module.Name, module.ID)),
259
- s.ref(self, module.Owner, module.Owner),
260
- module.Registry,
261
- plural(len(module.Packages), "package"),
262
- })
263
- }
264
- section(&b, "Schema modules", table([]string{"Module", "Publisher", "Registry", "Packages"}, modules))
265
-
266
- flows := make([][]string, 0, len(s.cat.Flows))
267
- for i := range s.cat.Flows {
268
- flow := &s.cat.Flows[i]
269
- owner := "—"
270
- if flow.Owner != "" {
271
- owner = s.ref(self, flow.Owner, flow.Owner)
272
- }
273
- flows = append(flows, []string{
274
- s.ref(self, flow.ID, flow.Name),
275
- owner,
276
- firstLine(flow.Summary),
277
- })
278
- }
279
- section(&b, "Flows", table([]string{"Flow", "Owner", "Summary"}, flows))
280
-
281
- adrs := make([][]string, 0, len(s.cat.Adrs))
282
- for i := range s.cat.Adrs {
283
- adr := &s.cat.Adrs[i]
284
- adrs = append(adrs, []string{
285
- s.ref(self, adr.ID, adr.ID),
286
- adr.Title,
287
- string(adr.Status),
288
- adr.Date,
289
- })
290
- }
291
- section(&b, "Decisions", table([]string{"ADR", "Title", "Status", "Date"}, adrs))
292
-
293
- s.b.file(self, b.String())
294
- }
295
-
296
- func (s *site) title() string {
297
- if s.opts.Title != "" {
298
- return s.opts.Title
299
- }
300
-
301
- return "Architecture catalog"
302
- }
303
-
304
- // stamp is the provenance line every page carries. A generated page that does
305
- // not say what it was generated from is indistinguishable from a hand-written
306
- // one that has gone stale.
307
- func (s *site) stamp() string {
308
- parts := []string{"Generated from the portolan catalog"}
309
- if s.cat.Commit != "" {
310
- parts = append(parts, "commit "+code(s.cat.Commit))
311
- }
312
- if s.cat.GeneratedAt != "" {
313
- parts = append(parts, "at "+s.cat.GeneratedAt)
314
- }
315
-
316
- return "*" + strings.Join(parts, " · ") + ". Do not edit by hand.*\n"
317
- }
318
-
319
- func firstLine(s string) string {
320
- if i := strings.Index(s, "\n"); i >= 0 {
321
- return strings.TrimSpace(s[:i])
322
- }
323
-
324
- return strings.TrimSpace(s)
325
- }
326
-
327
- // parent is the directory a page sits in, as a slash path.
328
- func parent(page string) string {
329
- if i := strings.LastIndex(page, "/"); i >= 0 {
330
- return page[:i]
331
- }
332
-
333
- return "."
334
- }
335
-
336
- func sortedDefKeys(defs map[string]catalog.TypeDef) []string {
337
- keys := make([]string, 0, len(defs))
338
- for k := range defs {
339
- keys = append(keys, k)
340
- }
341
- sort.Strings(keys)
342
-
343
- return keys
344
- }