@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,225 +0,0 @@
1
- package main
2
-
3
- import (
4
- "io/fs"
5
- "path"
6
- "path/filepath"
7
- "sort"
8
- "strings"
9
- )
10
-
11
- // sourceLayout is the small amount of filesystem knowledge the extractor
12
- // needs. Discovery is deliberately separate from reading behaviour: the rest
13
- // of the extractor deals in aggregate/use-case keys and concrete package
14
- // directories, not in one repository-wide directory convention.
15
- type sourceLayout struct {
16
- packages []string
17
- domains map[string]string
18
- useCases map[string]string
19
- integrationEvents map[string]string
20
- policies []string
21
- http []string
22
- grpc []string
23
- }
24
-
25
- func discoverLayout(root string) sourceLayout {
26
- layout := sourceLayout{
27
- packages: goPackageDirs(root, "internal"),
28
- domains: map[string]string{},
29
- useCases: map[string]string{},
30
- integrationEvents: map[string]string{},
31
- }
32
-
33
- for _, dir := range layout.packages {
34
- parts := strings.Split(filepath.ToSlash(dir), "/")
35
-
36
- if aggregate, ok := domainDir(parts); ok {
37
- layout.domains[aggregate] = dir
38
- }
39
- if aggregate, ok := integrationEventDir(parts); ok {
40
- layout.integrationEvents[aggregate] = dir
41
- }
42
- if aggregate, name, ok := useCaseDir(parts); ok {
43
- pkg, err := parsePkg(root, dir)
44
- if err == nil && hasStructNamed(pkg, "UseCase") {
45
- layout.useCases[aggregate+"/"+name] = dir
46
- }
47
- }
48
-
49
- if path.Base(dir) == "policy" {
50
- layout.policies = append(layout.policies, dir)
51
- }
52
- if isTransportPackage(parts, "http") {
53
- layout.http = append(layout.http, dir)
54
- }
55
- if isTransportPackage(parts, "grpc") {
56
- layout.grpc = append(layout.grpc, dir)
57
- }
58
- }
59
-
60
- sort.Strings(layout.policies)
61
- sort.Strings(layout.http)
62
- sort.Strings(layout.grpc)
63
-
64
- return layout
65
- }
66
-
67
- func hasStructNamed(pkg *pkg, name string) bool {
68
- for _, decl := range pkg.structs() {
69
- if decl.name == name {
70
- return true
71
- }
72
- }
73
- return false
74
- }
75
-
76
- // domainDir accepts both layouts:
77
- //
78
- // internal/domain/user
79
- // internal/user/domain
80
- func domainDir(parts []string) (string, bool) {
81
- if len(parts) == 3 && parts[0] == "internal" && parts[1] == "domain" {
82
- return parts[2], true
83
- }
84
- if len(parts) == 3 && parts[0] == "internal" && parts[2] == "domain" {
85
- return parts[1], true
86
- }
87
- return "", false
88
- }
89
-
90
- // useCaseDir accepts the horizontal convention, feature slices with use cases
91
- // directly below application, and feature slices that keep a usecases level.
92
- func useCaseDir(parts []string) (aggregate, name string, ok bool) {
93
- if len(parts) == 5 && parts[0] == "internal" && parts[1] == "application" && parts[3] == "usecases" {
94
- return parts[2], parts[4], true
95
- }
96
- if len(parts) == 4 && parts[0] == "internal" && parts[2] == "application" {
97
- return parts[1], parts[3], true
98
- }
99
- if len(parts) == 5 && parts[0] == "internal" && parts[2] == "application" && parts[3] == "usecases" {
100
- return parts[1], parts[4], true
101
- }
102
- return "", "", false
103
- }
104
-
105
- func integrationEventDir(parts []string) (string, bool) {
106
- if len(parts) == 4 && parts[0] == "internal" && parts[2] == "integration" && parts[3] == "event" {
107
- return parts[1], true
108
- }
109
- // Legacy event DTOs lived beside repositories.
110
- if len(parts) == 5 && parts[0] == "internal" && parts[1] == "infrastructure" && parts[2] == "repository" && parts[4] == "dto" {
111
- return parts[3], true
112
- }
113
- return "", false
114
- }
115
-
116
- func isTransportPackage(parts []string, protocol string) bool {
117
- for _, part := range parts {
118
- if part == "gen" {
119
- return false
120
- }
121
- }
122
- for i, part := range parts {
123
- if part != protocol || i == 0 {
124
- continue
125
- }
126
- // Horizontal: infrastructure/transport/http[/handler].
127
- // Feature: user/infrastructure/http[/handler].
128
- // Shared: internal/transport/http.
129
- if parts[i-1] == "transport" || parts[i-1] == "infrastructure" {
130
- return true
131
- }
132
- }
133
- return false
134
- }
135
-
136
- func goPackageDirs(root, rel string) []string {
137
- base := filepath.Join(root, filepath.FromSlash(rel))
138
- seen := map[string]bool{}
139
-
140
- _ = filepath.WalkDir(base, func(filename string, entry fs.DirEntry, err error) error {
141
- if err != nil {
142
- return nil
143
- }
144
- if entry.IsDir() {
145
- if filename != base && strings.HasPrefix(entry.Name(), ".") {
146
- return filepath.SkipDir
147
- }
148
- return nil
149
- }
150
-
151
- name := entry.Name()
152
- if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
153
- return nil
154
- }
155
- dir, err := filepath.Rel(root, filepath.Dir(filename))
156
- if err == nil {
157
- seen[filepath.ToSlash(dir)] = true
158
- }
159
- return nil
160
- })
161
-
162
- out := make([]string, 0, len(seen))
163
- for dir := range seen {
164
- out = append(out, dir)
165
- }
166
- sort.Strings(out)
167
- return out
168
- }
169
-
170
- func internalParts(importPath string) ([]string, bool) {
171
- _, after, found := strings.Cut(importPath, "/internal/")
172
- if !found || after == "" {
173
- return nil, false
174
- }
175
- return append([]string{"internal"}, strings.Split(after, "/")...), true
176
- }
177
-
178
- func domainImport(importPath string) (string, bool) {
179
- parts, ok := internalParts(importPath)
180
- if !ok {
181
- return "", false
182
- }
183
- return domainDir(parts)
184
- }
185
-
186
- func eventImport(importPath string) (string, bool) {
187
- parts, ok := internalParts(importPath)
188
- if !ok {
189
- return "", false
190
- }
191
- if len(parts) == 4 && parts[0] == "internal" && parts[1] == "domain" && parts[3] == "event" {
192
- return parts[2], true
193
- }
194
- if len(parts) == 4 && parts[0] == "internal" && parts[2] == "domain" && parts[3] == "event" {
195
- return parts[1], true
196
- }
197
- return integrationEventDir(parts)
198
- }
199
-
200
- func useCaseImport(importPath string) (aggregate, name string, ok bool) {
201
- parts, ok := internalParts(importPath)
202
- if !ok {
203
- return "", "", false
204
- }
205
- return useCaseDir(parts)
206
- }
207
-
208
- func applicationSupportImport(importPath string) bool {
209
- parts, ok := internalParts(importPath)
210
- if !ok {
211
- return false
212
- }
213
-
214
- return (len(parts) >= 3 && parts[1] == "application") ||
215
- (len(parts) >= 4 && parts[2] == "application")
216
- }
217
-
218
- func sortedKeys[V any](values map[string]V) []string {
219
- keys := make([]string, 0, len(values))
220
- for key := range values {
221
- keys = append(keys, key)
222
- }
223
- sort.Strings(keys)
224
- return keys
225
- }
@@ -1,125 +0,0 @@
1
- package main
2
-
3
- import (
4
- "encoding/json"
5
- "os"
6
- "path/filepath"
7
- "strings"
8
- "testing"
9
-
10
- "github.com/shortlink-org/portolan/catalog"
11
- "github.com/shortlink-org/portolan/plugin"
12
- )
13
-
14
- func TestLayoutClassifiersAcceptHorizontalAndFeatureSlices(t *testing.T) {
15
- tests := []struct {
16
- path string
17
- aggregate string
18
- useCase string
19
- }{
20
- {"github.com/example/auth/internal/domain/user", "user", ""},
21
- {"github.com/example/auth/internal/user/domain", "user", ""},
22
- {"github.com/example/auth/internal/application/user/usecases/register", "user", "register"},
23
- {"github.com/example/auth/internal/user/application/register", "user", "register"},
24
- {"github.com/example/auth/internal/user/application/usecases/register", "user", "register"},
25
- }
26
-
27
- for _, tt := range tests {
28
- if tt.useCase == "" {
29
- if got, ok := domainImport(tt.path); !ok || got != tt.aggregate {
30
- t.Errorf("domainImport(%q) = %q, %v", tt.path, got, ok)
31
- }
32
- continue
33
- }
34
- aggregate, name, ok := useCaseImport(tt.path)
35
- if !ok || aggregate != tt.aggregate || name != tt.useCase {
36
- t.Errorf("useCaseImport(%q) = %q, %q, %v", tt.path, aggregate, name, ok)
37
- }
38
- }
39
- }
40
-
41
- func TestDiscoverLayoutFindsFeaturePackages(t *testing.T) {
42
- root := t.TempDir()
43
- write := func(name, contents string) {
44
- t.Helper()
45
- filename := filepath.Join(root, filepath.FromSlash(name))
46
- if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
47
- t.Fatal(err)
48
- }
49
- if err := os.WriteFile(filename, []byte(contents), 0o644); err != nil {
50
- t.Fatal(err)
51
- }
52
- }
53
-
54
- write("internal/user/domain/user.go", "package user\ntype User struct{}\n")
55
- write("internal/user/application/register/usecase.go", "package register\ntype UseCase struct{}\n")
56
- write("internal/user/application/credentials/port.go", "package credentials\ntype PasswordHasher interface{}\n")
57
- write("internal/user/integration/event/event.go", "package event\nconst Topic = \"users\"\n")
58
- write("internal/user/infrastructure/http/handler.go", "package http\ntype Handler struct{}\n")
59
- write("internal/session/infrastructure/messaging/policy/revoke.go", "package policy\ntype Revoke struct{}\n")
60
- write("internal/transport/http/gen/server.gen.go", "package gen\ntype ServerInterface interface{}\n")
61
-
62
- layout := discoverLayout(root)
63
- if got := layout.domains["user"]; got != "internal/user/domain" {
64
- t.Errorf("user domain = %q", got)
65
- }
66
- if got := layout.useCases["user/register"]; got != "internal/user/application/register" {
67
- t.Errorf("register use case = %q", got)
68
- }
69
- if _, found := layout.useCases["user/credentials"]; found {
70
- t.Error("an application support package was classified as a use case")
71
- }
72
- if got := layout.integrationEvents["user"]; got != "internal/user/integration/event" {
73
- t.Errorf("integration events = %q", got)
74
- }
75
- if len(layout.http) != 1 || layout.http[0] != "internal/user/infrastructure/http" {
76
- t.Errorf("http packages = %v", layout.http)
77
- }
78
- if len(layout.policies) != 1 || layout.policies[0] != "internal/session/infrastructure/messaging/policy" {
79
- t.Errorf("policy packages = %v", layout.policies)
80
- }
81
- }
82
-
83
- func TestExtractsFeatureSlicedAuthExample(t *testing.T) {
84
- root := filepath.Join("..", "..", "examples", "auth")
85
- resp, err := extract(plugin.Input{Root: root, Commit: "test", GeneratedAt: "2026-01-01T00:00:00Z"}, Options{
86
- Context: "auth", Service: "auth", Store: "pg",
87
- })
88
- if err != nil {
89
- t.Fatal(err)
90
- }
91
-
92
- var got catalog.Catalog
93
- if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
94
- t.Fatal(err)
95
- }
96
- service := got.Contexts[0].Services[0]
97
- if len(service.Aggregates) != 3 {
98
- t.Fatalf("aggregates = %d, want user, session and lockout", len(service.Aggregates))
99
- }
100
-
101
- wantOperations := map[string]int{"user": 4, "session": 4, "lockout": 3}
102
- for _, aggregate := range service.Aggregates {
103
- if len(aggregate.Operations) != wantOperations[aggregate.Slug] {
104
- t.Errorf("%s operations = %d, want %d", aggregate.Slug, len(aggregate.Operations), wantOperations[aggregate.Slug])
105
- }
106
- for _, event := range aggregate.Events {
107
- if event.Wire == nil || event.Wire.Channel == "" {
108
- t.Errorf("%s.%s has no integration-event channel", aggregate.Slug, event.Name)
109
- }
110
- }
111
- }
112
-
113
- foundPolicy := false
114
- for _, flow := range got.Flows {
115
- if flow.ID == "flow.auth-revoke-sessions-on-password-change" {
116
- foundPolicy = true
117
- }
118
- }
119
- if !foundPolicy {
120
- t.Error("the feature-local password-change policy was not extracted")
121
- }
122
- if !strings.Contains(resp.Files[0].Contents, `"ref": "auth.auth.lockout.AccountLocked"`) {
123
- t.Error("AccountLocked collected into an event slice was not followed to the repository")
124
- }
125
- }
@@ -1,324 +0,0 @@
1
- package main
2
-
3
- import (
4
- "go/ast"
5
- "go/token"
6
- "sort"
7
- "strconv"
8
- "strings"
9
-
10
- "github.com/shortlink-org/portolan/catalog"
11
- "github.com/shortlink-org/portolan/plugin"
12
- )
13
-
14
- // The root's lifecycle, read off a go-sdk/fsm rule set and the one method
15
- // that runs events through it.
16
- //
17
- // var Rules = fsm.TransitionRuleSet{
18
- // StateLive: {EventRevoke: StateRevoked},
19
- // StateRevoked: {},
20
- // }
21
- // func (s *Session) trigger(ev fsm.Event, now time.Time) bool { … machine.TriggerEvent(ctx, ev) … }
22
- // func (s *Session) Revoke(reason event.Reason, now time.Time) (event.SessionEnded, bool) { s.trigger(EventRevoke, now) … }
23
- //
24
- // The rule set gives the states, in the order the literal lists them, and the
25
- // edges, each named by the fsm event that takes it. The method whose body
26
- // calls TriggerEvent is the mover; every exported method that hands the mover
27
- // a constant makes the edges that constant names. The event type a method
28
- // returns is what its last move publishes: a Go method puts its guards and
29
- // its housekeeping first and the change it announces at the end, so a method
30
- // that lapses a lock and then locks again hands AccountLocked back for the
31
- // lock, not for the lapse. A rule no method takes, a constant no rule knows,
32
- // and a mover handed something that is not a constant are each reported,
33
- // because the table is a claim and the claim should be kept.
34
- func readLifecycle(p *pkg, root string, events []catalog.Event, aggID string, b *plugin.Builder) *catalog.Lifecycle {
35
- consts := stringConsts(p)
36
- states, rules := ruleSet(p, consts)
37
- if len(states) == 0 {
38
- return nil
39
- }
40
-
41
- mover := ""
42
- methods := p.methods(root)
43
- for name, fn := range methods {
44
- if calls(fn, "TriggerEvent") {
45
- mover = name
46
-
47
- break
48
- }
49
- }
50
- if mover == "" {
51
- b.Warn(aggID, p.dir+" declares a fsm.TransitionRuleSet but no method of "+root+" calls TriggerEvent, so nothing is read as moving along it")
52
-
53
- return &catalog.Lifecycle{States: states, Transitions: []catalog.Transition{}}
54
- }
55
-
56
- byName := map[string]string{}
57
- for _, ev := range events {
58
- byName[ev.Name] = ev.ID
59
- }
60
-
61
- var transitions []catalog.Transition
62
- taken := map[string]bool{}
63
- for name, fn := range methods {
64
- if name == mover || !exported(name) || fn.Body == nil {
65
- continue
66
- }
67
- emits := emittedEvent(fn, byName)
68
- var made []catalog.Transition
69
- ast.Inspect(fn.Body, func(node ast.Node) bool {
70
- call, ok := node.(*ast.CallExpr)
71
- if !ok {
72
- return true
73
- }
74
- sel, ok := call.Fun.(*ast.SelectorExpr)
75
- if !ok || (sel.Sel.Name != mover && sel.Sel.Name != "TriggerEvent") {
76
- return true
77
- }
78
- event, literal := eventArg(call, consts)
79
- if !literal {
80
- b.Warn(aggID, p.at(call.Pos())+": "+name+" hands "+sel.Sel.Name+" something that is not a constant; the move is not in the lifecycle")
81
-
82
- return true
83
- }
84
- known := false
85
- for _, from := range states {
86
- to, ok := rules[from][event]
87
- if !ok {
88
- continue
89
- }
90
- known = true
91
- taken[from+"→"+to] = true
92
- made = append(made, catalog.Transition{From: from, To: to, On: name, Source: p.at(call.Pos())})
93
- }
94
- if !known {
95
- b.Warn(aggID, p.at(call.Pos())+": "+name+" triggers "+strconv.Quote(event)+", which no rule in the table knows")
96
- }
97
-
98
- return true
99
- })
100
- // The event goes with the last move the method makes: see above.
101
- if len(made) > 0 && emits != "" {
102
- last := made[len(made)-1].Source
103
- for i := range made {
104
- if made[i].Source == last {
105
- made[i].Emits = emits
106
- }
107
- }
108
- }
109
- transitions = append(transitions, made...)
110
- }
111
- for _, from := range states {
112
- for event, to := range rules[from] {
113
- if !taken[from+"→"+to] {
114
- b.Warn(aggID, "the table allows "+from+" → "+to+" on "+strconv.Quote(event)+", and no method of "+root+" makes that move")
115
- }
116
- }
117
- }
118
-
119
- order := map[string]int{}
120
- for i, s := range states {
121
- order[s] = i
122
- }
123
- sort.SliceStable(transitions, func(i, j int) bool {
124
- a, c := transitions[i], transitions[j]
125
- if order[a.From] != order[c.From] {
126
- return order[a.From] < order[c.From]
127
- }
128
- if order[a.To] != order[c.To] {
129
- return order[a.To] < order[c.To]
130
- }
131
-
132
- return a.On < c.On
133
- })
134
- if transitions == nil {
135
- transitions = []catalog.Transition{}
136
- }
137
-
138
- return &catalog.Lifecycle{States: states, Transitions: transitions}
139
- }
140
-
141
- // ruleSet finds a package-level `fsm.TransitionRuleSet{…}` literal and reads
142
- // it as states, in literal order, and from → event → to. A state that only
143
- // ever appears as a target is appended, so the table can leave a terminal
144
- // state out and the page still draws it.
145
- func ruleSet(p *pkg, consts map[string]string) ([]string, map[string]map[string]string) {
146
- rules := map[string]map[string]string{}
147
- var states []string
148
- seen := map[string]bool{}
149
- add := func(state string) {
150
- if !seen[state] {
151
- seen[state] = true
152
- states = append(states, state)
153
- }
154
- }
155
-
156
- for _, file := range p.files {
157
- for _, decl := range file.Decls {
158
- gen, ok := decl.(*ast.GenDecl)
159
- if !ok || gen.Tok != token.VAR {
160
- continue
161
- }
162
- for _, spec := range gen.Specs {
163
- value, ok := spec.(*ast.ValueSpec)
164
- if !ok {
165
- continue
166
- }
167
- for _, v := range value.Values {
168
- lit, ok := v.(*ast.CompositeLit)
169
- if !ok || !isRuleSetType(lit.Type) {
170
- continue
171
- }
172
- for _, elt := range lit.Elts {
173
- kv, ok := elt.(*ast.KeyValueExpr)
174
- if !ok {
175
- continue
176
- }
177
- from, ok := constString(kv.Key, consts)
178
- if !ok {
179
- continue
180
- }
181
- add(from)
182
- if rules[from] == nil {
183
- rules[from] = map[string]string{}
184
- }
185
- inner, ok := kv.Value.(*ast.CompositeLit)
186
- if !ok {
187
- continue
188
- }
189
- for _, e := range inner.Elts {
190
- pair, ok := e.(*ast.KeyValueExpr)
191
- if !ok {
192
- continue
193
- }
194
- event, ok1 := constString(pair.Key, consts)
195
- to, ok2 := constString(pair.Value, consts)
196
- if ok1 && ok2 {
197
- rules[from][event] = to
198
- }
199
- }
200
- }
201
- }
202
- }
203
- }
204
- }
205
- for _, from := range append([]string(nil), states...) {
206
- for _, to := range rules[from] {
207
- add(to)
208
- }
209
- }
210
-
211
- return states, rules
212
- }
213
-
214
- func isRuleSetType(expr ast.Expr) bool {
215
- sel, ok := expr.(*ast.SelectorExpr)
216
- if !ok {
217
- return false
218
- }
219
- pkg, ok := sel.X.(*ast.Ident)
220
-
221
- return ok && pkg.Name == "fsm" && sel.Sel.Name == "TransitionRuleSet"
222
- }
223
-
224
- // eventArg finds the fsm event a call hands over: the constant among its
225
- // arguments, whichever position the mover keeps it in.
226
- func eventArg(call *ast.CallExpr, consts map[string]string) (string, bool) {
227
- for _, arg := range call.Args {
228
- if s, ok := constString(arg, consts); ok {
229
- return s, true
230
- }
231
- }
232
-
233
- return "", false
234
- }
235
-
236
- // constString reads an expression as a string it can be sure of: a literal,
237
- // or a name the package declares as one.
238
- func constString(expr ast.Expr, consts map[string]string) (string, bool) {
239
- switch e := expr.(type) {
240
- case *ast.BasicLit:
241
- if e.Kind == token.STRING {
242
- s, err := strconv.Unquote(e.Value)
243
-
244
- return s, err == nil
245
- }
246
- case *ast.Ident:
247
- s, ok := consts[e.Name]
248
-
249
- return s, ok
250
- case *ast.ParenExpr:
251
- return constString(e.X, consts)
252
- }
253
-
254
- return "", false
255
- }
256
-
257
- // stringConsts collects the package's string constants by name, typed or not:
258
- // `StateLive fsm.State = "live"` is "live".
259
- func stringConsts(p *pkg) map[string]string {
260
- out := map[string]string{}
261
- for _, file := range p.files {
262
- for _, decl := range file.Decls {
263
- gen, ok := decl.(*ast.GenDecl)
264
- if !ok || gen.Tok != token.CONST {
265
- continue
266
- }
267
- for _, spec := range gen.Specs {
268
- value, ok := spec.(*ast.ValueSpec)
269
- if !ok {
270
- continue
271
- }
272
- for i, name := range value.Names {
273
- if i >= len(value.Values) {
274
- break
275
- }
276
- if s, ok := constString(value.Values[i], nil); ok {
277
- out[name.Name] = s
278
- }
279
- }
280
- }
281
- }
282
- }
283
-
284
- return out
285
- }
286
-
287
- // emittedEvent is the event type among a method's results, when one of the
288
- // aggregate's events is there: `(event.SessionEnded, bool)` emits SessionEnded.
289
- func emittedEvent(fn *ast.FuncDecl, byName map[string]string) string {
290
- if fn.Type.Results == nil {
291
- return ""
292
- }
293
- for _, field := range fn.Type.Results.List {
294
- expr := field.Type
295
- if star, ok := expr.(*ast.StarExpr); ok {
296
- expr = star.X
297
- }
298
- name := ""
299
- switch t := expr.(type) {
300
- case *ast.SelectorExpr:
301
- name = t.Sel.Name
302
- case *ast.Ident:
303
- name = t.Name
304
- }
305
- if id, ok := byName[name]; ok {
306
- return id
307
- }
308
- }
309
-
310
- return ""
311
- }
312
-
313
- // at is a position as a reader would open it: the package-relative path and
314
- // the line.
315
- func (p *pkg) at(pos token.Pos) string {
316
- for file, rel := range p.paths {
317
- if file.Pos() <= pos && pos <= file.End() {
318
- return rel + ":" + strconv.Itoa(p.fset.Position(pos).Line)
319
- }
320
- }
321
- name, line := p.position(pos)
322
-
323
- return strings.TrimPrefix(name, p.dir+"/") + ":" + strconv.Itoa(line)
324
- }