@shortlink-org/portolan 0.1.0 → 0.2.1

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 +27 -10
  36. package/scripts/local-api.test.mjs +85 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +24 -3
  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 +17 -9
  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,214 +0,0 @@
1
- package main
2
-
3
- import (
4
- "fmt"
5
- "regexp"
6
- "strings"
7
-
8
- "github.com/shortlink-org/portolan/catalog"
9
- )
10
-
11
- // The format, in one place.
12
- //
13
- // # Glossary — auth
14
- //
15
- // One meaning per word inside this context.
16
- //
17
- // **Session.** Proof that a user logged in, how long that proof is good for,
18
- // and whether it has been taken away.
19
- //
20
- // A title, an optional line or two saying what the vocabulary covers, then one
21
- // paragraph per term in alphabetical order. The paragraph opens with the term
22
- // in bold and the full stop inside the bold, so `**Email address.**` names a
23
- // two-word term and nothing has to guess where the name ends. Everything after
24
- // it is the definition, carried through as written.
25
- //
26
- // That is the whole format. Nothing here reads the prose for structure: a
27
- // glossary is a person explaining a word to another person, and a parser that
28
- // went looking for shapes inside the explanation would be a parser telling an
29
- // estate how to phrase itself.
30
- var (
31
- titleLine = regexp.MustCompile(`^#\s+Glossary\b`)
32
- entryOpen = regexp.MustCompile(`^\*\*(.+?)\.\*\*\s*(.*)$`)
33
- listItem = regexp.MustCompile(`^([-*+]|\d+\.)\s`)
34
- )
35
-
36
- // A paragraph of the file, flattened to one line, and where it started.
37
- //
38
- // Flattening is what makes the rest of this file about words rather than about
39
- // line breaks: a glossary is hard-wrapped at whatever width its author likes,
40
- // and a soft break inside a paragraph is a space in every markdown renderer
41
- // there is. The line number survives because it is the only thing an error
42
- // message can point at.
43
- type paragraph struct {
44
- line int
45
- text string
46
- }
47
-
48
- // blocks splits a file into paragraphs on blank lines.
49
- func blocks(src string) []paragraph {
50
- var out []paragraph
51
-
52
- lines := strings.Split(strings.ReplaceAll(src, "\r\n", "\n"), "\n")
53
- start := 0
54
- var held []string
55
- flush := func() {
56
- if len(held) == 0 {
57
- return
58
- }
59
- out = append(out, paragraph{line: start, text: strings.Join(held, " ")})
60
- held = nil
61
- }
62
- for i, line := range lines {
63
- trimmed := strings.TrimSpace(line)
64
- if trimmed == "" {
65
- flush()
66
-
67
- continue
68
- }
69
- if len(held) == 0 {
70
- start = i + 1
71
- }
72
- held = append(held, trimmed)
73
- }
74
- flush()
75
-
76
- return out
77
- }
78
-
79
- // slug turns a term into the kebab-case form ids and urls use: "Email address"
80
- // becomes email-address, "Password policy" becomes password-policy.
81
- func slug(name string) string {
82
- var b strings.Builder
83
-
84
- dash := false
85
- for _, r := range strings.ToLower(name) {
86
- switch {
87
- case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
88
- b.WriteRune(r)
89
- dash = false
90
- default:
91
- if !dash && b.Len() > 0 {
92
- b.WriteRune('-')
93
- dash = true
94
- }
95
- }
96
- }
97
-
98
- return strings.Trim(b.String(), "-")
99
- }
100
-
101
- func where(file string, line int) string {
102
- return fmt.Sprintf("%s:%d: ", file, line)
103
- }
104
-
105
- // short quotes the head of a paragraph, for an error that has to say what it
106
- // found instead of what it wanted.
107
- func short(text string) string {
108
- runes := []rune(text)
109
- if len(runes) > 40 {
110
- return `"` + string(runes[:40]) + `…"`
111
- }
112
-
113
- return `"` + text + `"`
114
- }
115
-
116
- // refuse names what a paragraph is, when it is not an entry. A paragraph
117
- // before the first entry is the preamble and is allowed to be anything; after
118
- // one, everything is an entry, and the shapes worth naming are the shapes a
119
- // glossary is otherwise written in.
120
- func refuse(text string, started bool) string {
121
- switch {
122
- case strings.HasPrefix(text, "|"):
123
- return "a table: a glossary is prose, one paragraph per term, so that an entry has room to say what the term is not"
124
- case strings.HasPrefix(text, "#"):
125
- return "a heading under the title: a term is a paragraph, not a section of its own"
126
- case listItem.MatchString(text):
127
- return "a list: a term is a paragraph opening with **Term.**"
128
- case started:
129
- return "a paragraph naming no term; after the first entry every paragraph is one, and it opens with **Term.**"
130
- }
131
-
132
- return ""
133
- }
134
-
135
- // parseGlossary reads one file into terms, the notes worth making about it,
136
- // and the reasons it cannot be read at all.
137
- //
138
- // Errors and warnings are kept apart all the way up: an error is something the
139
- // catalog cannot hold, a warning is something a person should go and write.
140
- func parseGlossary(file, context, src string) (terms []catalog.Term, warns, errs []string) {
141
- paras := blocks(src)
142
- if len(paras) == 0 {
143
- return nil, nil, []string{file + ": the file is empty"}
144
- }
145
- if !titleLine.MatchString(paras[0].text) {
146
- return nil, nil, []string{where(file, paras[0].line) +
147
- "a glossary opens with `# Glossary — <context>`, and this file opens with " + short(paras[0].text)}
148
- }
149
-
150
- seen := map[string]int{}
151
- previous := ""
152
- started := false
153
- for _, p := range paras[1:] {
154
- if !strings.HasPrefix(p.text, "**") {
155
- if msg := refuse(p.text, started); msg != "" {
156
- errs = append(errs, where(file, p.line)+msg)
157
- }
158
-
159
- continue
160
- }
161
- started = true
162
-
163
- m := entryOpen.FindStringSubmatch(p.text)
164
- if m == nil {
165
- errs = append(errs, where(file, p.line)+
166
- "an entry opens with the term in bold and the full stop inside it, `**Session.**`")
167
-
168
- continue
169
- }
170
- name := strings.TrimSpace(m[1])
171
- body := strings.TrimSpace(m[2])
172
- id := slug(name)
173
- if id == "" {
174
- errs = append(errs, where(file, p.line)+"a term with no name")
175
-
176
- continue
177
- }
178
- if body == "" {
179
- errs = append(errs, where(file, p.line)+name+" says nothing")
180
-
181
- continue
182
- }
183
-
184
- if at, taken := seen[id]; taken {
185
- errs = append(errs, where(file, p.line)+name+fmt.Sprintf(" is already defined on line %d", at))
186
-
187
- continue
188
- }
189
- seen[id] = p.line
190
-
191
- // Order is a warning because it costs a reader a moment and costs the
192
- // catalog nothing: every page sorts what it draws anyway.
193
- lower := strings.ToLower(name)
194
- if previous != "" && lower < previous {
195
- warns = append(warns, where(file, p.line)+name+" sits after "+previous+"; the glossary is alphabetical")
196
- }
197
- previous = lower
198
-
199
- terms = append(terms, catalog.Term{
200
- ID: context + "." + id,
201
- Slug: id,
202
- Context: context,
203
- Name: name,
204
- Definition: body,
205
- Source: fmt.Sprintf("%s:%d", file, p.line),
206
- })
207
- }
208
-
209
- if !started && len(errs) == 0 {
210
- warns = append(warns, file+": a glossary with no terms in it")
211
- }
212
-
213
- return terms, warns, errs
214
- }
@@ -1,203 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "strings"
6
- "testing"
7
-
8
- "github.com/shortlink-org/portolan/catalog"
9
- )
10
-
11
- const sample = `# Glossary — auth
12
-
13
- One meaning per word inside this context. The code, the events and the API
14
- spell these the same way.
15
-
16
- **Email address.** The address a user logs in with, normalised on creation.
17
-
18
- **Session.** Proof that a user logged in, and how long that proof is good for,
19
- and whether it has been taken away.
20
-
21
- **Version.** The number the store compares before writing an aggregate. Zero
22
- means never stored.
23
- `
24
-
25
- const file = "examples/auth/GLOSSARY.md"
26
-
27
- func parsed(t *testing.T, src string) []catalog.Term {
28
- t.Helper()
29
-
30
- terms, _, errs := parseGlossary(file, "auth", src)
31
- if len(errs) > 0 {
32
- t.Fatalf("errors: %s", strings.Join(errs, "\n"))
33
- }
34
-
35
- return terms
36
- }
37
-
38
- func refused(t *testing.T, src string) string {
39
- t.Helper()
40
-
41
- _, _, errs := parseGlossary(file, "auth", src)
42
- if len(errs) == 0 {
43
- t.Fatal("parsed without complaint")
44
- }
45
-
46
- return strings.Join(errs, "\n")
47
- }
48
-
49
- func warned(t *testing.T, src string) string {
50
- t.Helper()
51
-
52
- _, warns, errs := parseGlossary(file, "auth", src)
53
- if len(errs) > 0 {
54
- t.Fatalf("errors: %s", strings.Join(errs, "\n"))
55
- }
56
-
57
- return strings.Join(warns, "\n")
58
- }
59
-
60
- func term(t *testing.T, terms []catalog.Term, name string) catalog.Term {
61
- t.Helper()
62
-
63
- for _, candidate := range terms {
64
- if candidate.Name == name {
65
- return candidate
66
- }
67
- }
68
- t.Fatalf("no term called %q", name)
69
-
70
- return catalog.Term{}
71
- }
72
-
73
- func TestTheDefinitionIsTheParagraph(t *testing.T) {
74
- session := term(t, parsed(t, sample), "Session")
75
-
76
- want := "Proof that a user logged in, and how long that proof is good for, and whether it has been taken away."
77
- if session.Definition != want {
78
- t.Errorf("definition is %q, want %q", session.Definition, want)
79
- }
80
- }
81
-
82
- // The hard wrap in the file is a soft break in markdown, and a definition that
83
- // carried it would be a definition nothing could put on a card.
84
- func TestParagraphIsOneLine(t *testing.T) {
85
- for _, term := range parsed(t, sample) {
86
- if strings.ContainsAny(term.Definition, "\n") {
87
- t.Errorf("%s carries a line break", term.Name)
88
- }
89
- }
90
- }
91
-
92
- func TestTwoWordTerm(t *testing.T) {
93
- address := term(t, parsed(t, sample), "Email address")
94
-
95
- if address.Slug != "email-address" {
96
- t.Errorf("slug is %q", address.Slug)
97
- }
98
- if address.ID != "auth.email-address" {
99
- t.Errorf("id is %q", address.ID)
100
- }
101
- if address.Context != "auth" {
102
- t.Errorf("context is %q", address.Context)
103
- }
104
- }
105
-
106
- func TestSourceIsTheLineTheEntryStartsOn(t *testing.T) {
107
- if got := term(t, parsed(t, sample), "Session").Source; got != file+":8" {
108
- t.Errorf("source is %q, want %s:8", got, file)
109
- }
110
- }
111
-
112
- func TestPreambleIsNotATerm(t *testing.T) {
113
- if terms := parsed(t, sample); len(terms) != 3 {
114
- names := []string{}
115
- for _, term := range terms {
116
- names = append(names, term.Name)
117
- }
118
- t.Errorf("read %d terms: %s", len(terms), strings.Join(names, ", "))
119
- }
120
- }
121
-
122
- func TestTableIsRefused(t *testing.T) {
123
- src := "# Glossary — auth\n\n| Term | Meaning |\n| --- | --- |\n| Session | Proof. |\n"
124
-
125
- if got := refused(t, src); !strings.Contains(got, "prose") {
126
- t.Errorf("refusal is %q", got)
127
- }
128
- }
129
-
130
- func TestHeadingPerTermIsRefused(t *testing.T) {
131
- src := "# Glossary — auth\n\n## Session\n\nProof that a user logged in.\n"
132
-
133
- if got := refused(t, src); !strings.Contains(got, "a term is a paragraph") {
134
- t.Errorf("refusal is %q", got)
135
- }
136
- }
137
-
138
- func TestListIsRefused(t *testing.T) {
139
- src := "# Glossary — auth\n\n- **Session.** Proof that a user logged in.\n"
140
-
141
- if got := refused(t, src); !strings.Contains(got, "a list") {
142
- t.Errorf("refusal is %q", got)
143
- }
144
- }
145
-
146
- func TestAFileThatIsNotAGlossaryIsRefused(t *testing.T) {
147
- src := "# Sessions\n\n**Session.** Proof that a user logged in.\n"
148
-
149
- if got := refused(t, src); !strings.Contains(got, "opens with `# Glossary") {
150
- t.Errorf("refusal is %q", got)
151
- }
152
- }
153
-
154
- // A word with two meanings inside one context is the failure the glossary is
155
- // written to prevent, so it is the one duplication that cannot be a warning.
156
- func TestTheSameWordTwiceIsRefused(t *testing.T) {
157
- src := sample + "\n**Session.** Something else entirely.\n"
158
-
159
- if got := refused(t, src); !strings.Contains(got, "already defined on line 8") {
160
- t.Errorf("refusal is %q", got)
161
- }
162
- }
163
-
164
- func TestOutOfOrderIsAWarning(t *testing.T) {
165
- src := "# Glossary — auth\n\n**Lockout.** An account refusing passwords.\n\n**Locked.** The state of a lockout while it refuses.\n"
166
-
167
- if got := warned(t, src); !strings.Contains(got, "alphabetical") {
168
- t.Errorf("warnings are %q", got)
169
- }
170
- }
171
-
172
- func TestAnEntryThatNamesNothing(t *testing.T) {
173
- src := "# Glossary — auth\n\n**Session** proof that a user logged in.\n"
174
-
175
- if got := refused(t, src); !strings.Contains(got, "the full stop inside it") {
176
- t.Errorf("refusal is %q", got)
177
- }
178
- }
179
-
180
- // The estate's own glossaries parse, and are held to the format they document.
181
- func TestTheEstatesOwnGlossaries(t *testing.T) {
182
- for _, tt := range []struct{ path, context string }{
183
- {"../../examples/auth/GLOSSARY.md", "auth"},
184
- {"../../examples/shop/oms/GLOSSARY.md", "shop"},
185
- } {
186
- t.Run(tt.path, func(t *testing.T) {
187
- src, err := os.ReadFile(tt.path)
188
- if err != nil {
189
- t.Fatal(err)
190
- }
191
- terms, warns, errs := parseGlossary(tt.path, tt.context, string(src))
192
- if len(errs) > 0 {
193
- t.Fatalf("errors: %s", strings.Join(errs, "\n"))
194
- }
195
- if len(warns) > 0 {
196
- t.Errorf("warnings: %s", strings.Join(warns, "\n"))
197
- }
198
- if len(terms) == 0 {
199
- t.Error("no terms read")
200
- }
201
- })
202
- }
203
- }
@@ -1,214 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path"
6
- "path/filepath"
7
- "strings"
8
-
9
- "github.com/shortlink-org/portolan/catalog"
10
- "github.com/shortlink-org/portolan/plugin"
11
- )
12
-
13
- // extractAggregate reads one discovered aggregate domain package.
14
- //
15
- // The layout is the claim: a directory there is an aggregate, the struct named
16
- // after it is the root, `vo/` holds its value objects and `event/` the facts it
17
- // publishes. Nothing is inferred from a comment or a marker interface, so a
18
- // package that does not follow the layout produces a diagnostic instead of a
19
- // half-right aggregate.
20
- func extractAggregate(root, aggregateName, domainPath string, layout sourceLayout, svcID string, b *plugin.Builder) (catalog.Aggregate, bool) {
21
- pkg, err := parsePkg(root, domainPath)
22
- if err != nil {
23
- if !os.IsNotExist(err) {
24
- b.Warn(aggregateID(svcID, aggregateName), domainPath+" could not be parsed: "+err.Error())
25
- }
26
-
27
- return catalog.Aggregate{}, false
28
- }
29
-
30
- id := aggregateID(svcID, slug(aggregateName))
31
- name := title(pkg.name)
32
-
33
- aggregate := catalog.Aggregate{
34
- ID: id,
35
- Slug: slug(aggregateName),
36
- Name: name,
37
- Readme: aggregateReadme(root, domainPath, pkg.name, name, pkg.doc()),
38
- Entities: []catalog.Block{},
39
- ValueObjects: []catalog.Block{},
40
- Operations: []catalog.Operation{},
41
- Events: []catalog.Event{},
42
- }
43
-
44
- for _, decl := range pkg.structs() {
45
- if !exported(decl.name) {
46
- continue
47
- }
48
-
49
- aggregate.Entities = append(aggregate.Entities, catalog.Block{
50
- ID: blockID(id, slug(decl.name)),
51
- Slug: slug(decl.name),
52
- Name: decl.name,
53
- Doc: decl.doc,
54
- Fields: fields(decl.fields),
55
- })
56
- }
57
-
58
- // The root is the entity named after its package: package user holds User,
59
- // and package price_list holds PriceList. It is a rule rather than a guess,
60
- // and a package that breaks it is not read as an aggregate at all. Taking
61
- // whichever struct came first instead would put a package of error types
62
- // on the page as a model with a root and seven fields, and a reader
63
- // believes a table; a warning and no page is the honest answer.
64
- aggregate.Root = pascal(pkg.name)
65
- if !hasBlock(aggregate.Entities, aggregate.Root) {
66
- if len(aggregate.Entities) == 0 {
67
- b.Warn(id, domainPath+" declares no exported struct, so the aggregate has no root")
68
-
69
- return catalog.Aggregate{}, false
70
- }
71
-
72
- b.Warn(id, domainPath+" has no struct called "+pascal(pkg.name)+", so it is not read as an aggregate; the root is the struct named after its package")
73
-
74
- return catalog.Aggregate{}, false
75
- }
76
-
77
- aggregate.ValueObjects = extractValueObjects(root, domainPath, id, b)
78
- aggregate.Enums = extractEnums(root, domainPath, id, pkg, b)
79
- aggregate.Events = extractEvents(root, aggregateName, domainPath, layout, id, b)
80
- aggregate.Lifecycle = readLifecycle(pkg, aggregate.Root, aggregate.Events, id, b)
81
-
82
- return aggregate, true
83
- }
84
-
85
- // extractValueObjects reads vo/* below a discovered aggregate domain package.
86
- //
87
- // Each directory there is one value object, and the exported struct inside it
88
- // is its shape. `rules/` is skipped: a validation specification is how the
89
- // value object refuses a value, not part of what it holds.
90
- func extractValueObjects(root, domainPath, aggID string, b *plugin.Builder) []catalog.Block {
91
- out := []catalog.Block{}
92
-
93
- for _, name := range subdirs(root, path.Join(domainPath, "vo")) {
94
- if name == "rules" {
95
- continue
96
- }
97
-
98
- pkg, err := parsePkg(root, path.Join(domainPath, "vo", name))
99
- if err != nil {
100
- continue
101
- }
102
-
103
- found := false
104
- for _, decl := range pkg.structs() {
105
- if !exported(decl.name) {
106
- continue
107
- }
108
-
109
- doc := decl.doc
110
- if doc == "" {
111
- doc = firstSentenceOrAll(pkg.doc())
112
- }
113
-
114
- // Qualified the way Go refers to it, so that the value object and
115
- // the field that holds it are visibly the same thing: User.Password
116
- // is a `password.Hash`, and a block called just `Hash` says nothing
117
- // about which one.
118
- label := qualified(pkg.name, decl.name)
119
-
120
- out = append(out, catalog.Block{
121
- ID: blockID(aggID, slug(label)),
122
- Slug: slug(label),
123
- Name: label,
124
- Doc: doc,
125
- Fields: fields(decl.fields),
126
- })
127
- found = true
128
- }
129
-
130
- if !found {
131
- b.Warn(aggID, path.Join(domainPath, "vo", name)+" declares no exported struct; skipped")
132
- }
133
- }
134
-
135
- return out
136
- }
137
-
138
- func hasBlock(blocks []catalog.Block, name string) bool {
139
- for i := range blocks {
140
- if blocks[i].Name == name {
141
- return true
142
- }
143
- }
144
-
145
- return false
146
- }
147
-
148
- // qualified is pkg.Name, unless the package is named after the type it holds -
149
- // token.Token adds nothing over Token.
150
- func qualified(pkg, name string) string {
151
- if strings.EqualFold(pkg, name) {
152
- return name
153
- }
154
-
155
- return pkg + "." + name
156
- }
157
-
158
- func exported(name string) bool {
159
- return name != "" && name[0] >= 'A' && name[0] <= 'Z'
160
- }
161
-
162
- // aggregateReadme prefers the package's README.md, which is written for a
163
- // reader and can draw the states, over the package comment, which is written
164
- // for whoever opens the file next. Same preference as operationDoc, for the
165
- // same reason. The comment is a fallback, not a second source: a package that
166
- // has both is described by its README alone.
167
- func aggregateReadme(root, domainPath, pkgName, name, doc string) string {
168
- if md := readFile(filepath.Join(root, filepath.FromSlash(domainPath), "README.md")); md != "" {
169
- return md
170
- }
171
-
172
- return readme(pkgName, name, doc)
173
- }
174
-
175
- // readme turns a package comment into the markdown the catalog carries. The
176
- // heading is added because every other readme in a catalog has one, and a page
177
- // that starts mid-paragraph reads as a fragment of something else.
178
- func readme(pkgName, name, doc string) string {
179
- doc = withoutPackagePrefix(pkgName, strings.TrimSpace(doc))
180
- if doc == "" {
181
- return ""
182
- }
183
-
184
- return "# " + name + "\n\n" + doc
185
- }
186
-
187
- // withoutPackagePrefix drops the "Package user " that Go doc convention puts at
188
- // the front of a package comment.
189
- //
190
- // It is a convention for godoc, where the sentence is read next to the package
191
- // clause. On a page headed "User" it reads as a leftover from somewhere else,
192
- // and the sentence works perfectly well without it.
193
- func withoutPackagePrefix(pkgName, doc string) string {
194
- return withoutLeading("Package "+pkgName+" ", doc)
195
- }
196
-
197
- // withoutLeading is the same for anything else Go doc convention names before
198
- // it says anything: a type comment opens with the type.
199
- func withoutLeading(prefix, doc string) string {
200
- rest, ok := strings.CutPrefix(doc, prefix)
201
- if !ok {
202
- return doc
203
- }
204
-
205
- runes := []rune(rest)
206
- if len(runes) == 0 {
207
- return doc
208
- }
209
- if runes[0] >= 'a' && runes[0] <= 'z' {
210
- runes[0] = runes[0] - 'a' + 'A'
211
- }
212
-
213
- return string(runes)
214
- }