@shortlink-org/portolan 0.1.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 (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,1441 @@
1
+ package main
2
+
3
+ import (
4
+ "go/ast"
5
+ "go/types"
6
+ "path"
7
+ "sort"
8
+ "strconv"
9
+ "strings"
10
+
11
+ "github.com/shortlink-org/portolan/catalog"
12
+ "github.com/shortlink-org/portolan/plugin"
13
+ )
14
+
15
+ // Flows are read out of the same layout as everything else here, and the
16
+ // layout is again the claim.
17
+ //
18
+ // Two things start one. An endpoint is somebody calling in, and the transport
19
+ // layer already says which use cases it runs and in what order. A policy is an
20
+ // event arriving, and the type it asserts on says which. Everything after that
21
+ // is the use case's own body: a field of UseCase is a port, a call on that port
22
+ // is a hop, and a domain call whose signature returns an event is what puts the
23
+ // event on the bus.
24
+ //
25
+ // What this cannot do it does not pretend to. An `if` or a `switch` becomes an
26
+ // alt only when something happens inside it - a hop, a publish - and its
27
+ // branch is terminal when the block ends in a return; an `if err != nil { return err }` with
28
+ // nothing in it is not an alternative path, it is the end of this one, and
29
+ // forty empty frames would say less than none. A step inside a loop carries a
30
+ // note saying so rather than a frame the reader would have to trust. Nothing
31
+ // is observed running, so every step is declared: this reads code, and code is
32
+ // a claim about behaviour, not a record of it. The one exception is a call
33
+ // whose far end the manifest does not name, which is unresolved.
34
+
35
+ const (
36
+ laneClient = "client"
37
+ laneBus = "bus"
38
+ )
39
+
40
+ // maxInline is how far a flow follows a use case into another one. Two is the
41
+ // whole of this estate - an endpoint into a use case into the use case behind
42
+ // its port - and a limit that is never reached is a limit that never has to be
43
+ // explained.
44
+ const maxInline = 2
45
+
46
+ type flowOptions struct {
47
+ context string
48
+ svcID string
49
+ service string
50
+ // store is the slug the manifest gives the service's database. Without it
51
+ // there is no lane to put persistence on, and the calls stay on the
52
+ // service's own.
53
+ store string
54
+ // peers maps a proto package to the service that answers to it.
55
+ peers map[string]string
56
+ // externals maps an api id to the system outside the estate that answers
57
+ // to it, when the manifest names one rather than leaving it to the
58
+ // document beside the client.
59
+ externals map[string]string
60
+ // events maps the import path a foreign event is vendored under to the
61
+ // aggregate that raised it.
62
+ events map[string]string
63
+ }
64
+
65
+ type flowReader struct {
66
+ root string
67
+ opts flowOptions
68
+ layout sourceLayout
69
+ b *plugin.Builder
70
+ bindings map[string]string
71
+ useCases map[string]*pkg
72
+ domains map[string]*pkg
73
+ // referenced records the events some step named, so that an event nothing
74
+ // here could follow can be reported rather than silently left out.
75
+ referenced map[string]bool
76
+ warnedStore bool
77
+ // module is the module path of go.mod, which is how an import path is
78
+ // read back to a directory in the tree.
79
+ module string
80
+ // adapters are the ports assembly fills with something other than a use
81
+ // case; clients are the generated clients already read, by import path.
82
+ adapters map[string]adapterDecl
83
+ wireBoundPorts map[string]bool
84
+ clients map[string]map[string]client
85
+ // calls are the rpcs some step made, by id, for the service's consumes.
86
+ calls map[string]catalog.RpcCall
87
+ warnedPeer map[string]bool
88
+ }
89
+
90
+ // extractFlows reads every sequence the service runs, in a fixed order:
91
+ // endpoints by operation id, then policies by type name.
92
+ func extractFlows(root string, opts flowOptions, layout sourceLayout, endpoints []endpointDecl, events []string, b *plugin.Builder) ([]catalog.Flow, []catalog.RpcCall) {
93
+ r := newFlowReader(root, opts, b, layout)
94
+
95
+ out := []catalog.Flow{}
96
+ for _, endpoint := range endpoints {
97
+ if flow, ok := r.endpointFlow(endpoint); ok {
98
+ out = append(out, flow)
99
+ }
100
+ }
101
+ out = append(out, r.policyFlows()...)
102
+
103
+ for _, event := range events {
104
+ if !r.referenced[event] {
105
+ b.Warn(event, "no flow reaches this event: nothing this extractor could follow publishes it")
106
+ }
107
+ }
108
+
109
+ return out, r.consumes()
110
+ }
111
+
112
+ func newFlowReader(root string, opts flowOptions, b *plugin.Builder, layouts ...sourceLayout) *flowReader {
113
+ layout := discoverLayout(root)
114
+ if len(layouts) > 0 {
115
+ layout = layouts[0]
116
+ }
117
+
118
+ return &flowReader{
119
+ root: root,
120
+ opts: opts,
121
+ layout: layout,
122
+ b: b,
123
+ bindings: portBindings(root, layout),
124
+ adapters: adapterBindings(root, layout),
125
+ wireBoundPorts: wireBoundPorts(root, layout),
126
+ module: modulePath(root),
127
+ useCases: map[string]*pkg{},
128
+ domains: map[string]*pkg{},
129
+ clients: map[string]map[string]client{},
130
+ calls: map[string]catalog.RpcCall{},
131
+ referenced: map[string]bool{},
132
+ warnedPeer: map[string]bool{},
133
+ }
134
+ }
135
+
136
+ // consumes is every rpc the flows made, in id order, so the fragment is the
137
+ // same however the use cases happened to be read.
138
+ func (r *flowReader) consumes() []catalog.RpcCall {
139
+ out := make([]catalog.RpcCall, 0, len(r.calls))
140
+ for _, call := range r.calls {
141
+ out = append(out, call)
142
+ }
143
+ sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
144
+
145
+ return out
146
+ }
147
+
148
+ // endpointFlow opens with the call from outside and continues into whatever the
149
+ // handler runs.
150
+ //
151
+ // The flow is named after the endpoint rather than after a use case because the
152
+ // endpoint is the thing that happened; where two use cases run behind one, the
153
+ // prose comes from the last of them, which is the one the endpoint is FOR - the
154
+ // others authorized it.
155
+ func (r *flowReader) endpointFlow(endpoint endpointDecl) (catalog.Flow, bool) {
156
+ d := newDraft()
157
+ d.lane(catalog.Participant{ID: laneClient, Kind: catalog.ParticipantActor})
158
+ d.lane(r.serviceLane())
159
+
160
+ d.add(catalog.Step{
161
+ From: laneClient,
162
+ To: r.opts.svcID,
163
+ Kind: catalog.StepRPC,
164
+ Label: endpoint.id,
165
+ Line: at(endpoint.source, endpoint.line),
166
+ })
167
+
168
+ for _, useCase := range endpoint.useCases {
169
+ r.walkUseCase(d, useCase, 0)
170
+ }
171
+
172
+ last := endpoint.useCases[len(endpoint.useCases)-1]
173
+ name := slug(endpoint.id)
174
+ id := r.opts.service + "-" + name
175
+
176
+ return catalog.Flow{
177
+ ID: "flow." + id,
178
+ Slug: id,
179
+ Name: sentence(name),
180
+ Summary: r.useCaseDoc(last),
181
+ Source: endpoint.source,
182
+ Owner: r.opts.context,
183
+ Participants: d.lanes,
184
+ Steps: d.steps,
185
+ }, true
186
+ }
187
+
188
+ // policyFlows reads every discovered policy package: a rule of the form "when
189
+ // X has happened, do Y", which is a flow that opens on the bus.
190
+ //
191
+ // It opens with the event rather than with the policy, and that is what lets
192
+ // the two halves meet: a flow whose FIRST step names the same event another
193
+ // flow publishes is that flow's continuation, and nothing has to write the seam
194
+ // down for a reader to follow it.
195
+ func (r *flowReader) policyFlows() []catalog.Flow {
196
+ out := []catalog.Flow{}
197
+
198
+ for _, dir := range r.layout.policies {
199
+ out = append(out, r.policyFlowsIn(dir)...)
200
+ }
201
+
202
+ return out
203
+ }
204
+
205
+ func (r *flowReader) policyFlowsIn(dir string) []catalog.Flow {
206
+ out := []catalog.Flow{}
207
+
208
+ pkg, err := parsePkg(r.root, dir)
209
+ if err != nil {
210
+ return out
211
+ }
212
+
213
+ imports := importsOf(pkg)
214
+
215
+ names := []string{}
216
+ byName := map[string]structDecl{}
217
+ for _, decl := range pkg.structs() {
218
+ if !exported(decl.name) {
219
+ continue
220
+ }
221
+ names = append(names, decl.name)
222
+ byName[decl.name] = decl
223
+ }
224
+ sort.Strings(names)
225
+
226
+ for _, name := range names {
227
+ decl := byName[name]
228
+
229
+ handle := pkg.methods(name)["Handle"]
230
+ if handle == nil {
231
+ continue
232
+ }
233
+
234
+ event, ok := r.assertedEvent(handle, imports)
235
+ if !ok {
236
+ r.b.Warn(name, pkg.dir+": "+name+".Handle asserts on no event type; the policy is not paired with what triggers it")
237
+
238
+ continue
239
+ }
240
+
241
+ d := newDraft()
242
+ d.lane(catalog.Participant{ID: laneBus, Kind: catalog.ParticipantBroker})
243
+ d.lane(r.serviceLane())
244
+
245
+ source, line := pkg.position(handle.Pos())
246
+ if event.foreign != "" {
247
+ // Another repository's event. Its id is <service>.<aggregate>.<Name>
248
+ // and this tree knows neither half, so the step names what it
249
+ // asserts on and says it resolves to nothing here; the merge is
250
+ // where the other side may turn up.
251
+ r.b.Warn(name, pkg.dir+": "+name+".Handle asserts on "+event.foreign+"."+event.name+", an event this repository does not declare; the step is unresolved")
252
+ d.add(catalog.Step{
253
+ From: laneBus,
254
+ To: r.opts.svcID,
255
+ Kind: catalog.StepEvent,
256
+ Label: event.name,
257
+ Status: catalog.StatusUnresolved,
258
+ Note: "Reacts to `" + event.name + "` from `" + event.foreign + "`, which is not an event this repository declares.",
259
+ Line: at(source, line),
260
+ })
261
+ } else {
262
+ d.add(catalog.Step{
263
+ From: laneBus,
264
+ To: r.opts.svcID,
265
+ Kind: catalog.StepEvent,
266
+ Ref: event.id,
267
+ Label: event.name,
268
+ Line: at(source, line),
269
+ })
270
+ r.referenced[event.id] = true
271
+ }
272
+
273
+ r.walkBody(d, &scope{
274
+ pkg: pkg,
275
+ fields: structFields(decl.fields),
276
+ imports: imports,
277
+ vars: map[string]domainRef{},
278
+ recv: receiverIdent(handle),
279
+ recvType: name,
280
+ }, handle, 0)
281
+
282
+ id := r.opts.service + "-" + slug(name)
283
+ out = append(out, catalog.Flow{
284
+ ID: "flow." + id,
285
+ Slug: id,
286
+ Name: sentence(slug(name)),
287
+ Summary: withoutLeading(name+" ", decl.doc),
288
+ Source: decl.source,
289
+ Owner: r.opts.context,
290
+ Participants: d.lanes,
291
+ Steps: d.steps,
292
+ })
293
+ }
294
+
295
+ return out
296
+ }
297
+
298
+ // assertedEvent reads `changed, ok := e.(userevent.PasswordChanged)`: the type
299
+ // a policy asserts on is the fact it reacts to. A type from outside this
300
+ // module is somebody else's event; it is returned with `foreign` set to where
301
+ // it came from, because the policy exists whether or not its trigger does.
302
+ func (r *flowReader) assertedEvent(fn *ast.FuncDecl, imports map[string]string) (eventRef, bool) {
303
+ var out eventRef
304
+ found := false
305
+
306
+ ast.Inspect(fn.Body, func(node ast.Node) bool {
307
+ assert, ok := node.(*ast.TypeAssertExpr)
308
+ if !ok || assert.Type == nil || found {
309
+ return true
310
+ }
311
+
312
+ selector, name, cut := strings.Cut(types.ExprString(assert.Type), ".")
313
+ if !cut {
314
+ return true
315
+ }
316
+ importPath := imports[selector]
317
+ if aggregate, isEvent := eventPackage(importPath); isEvent {
318
+ out = eventRef{id: eventID(aggregateID(r.opts.svcID, aggregate), name), name: name}
319
+ found = true
320
+
321
+ return false
322
+ }
323
+ // Somebody else's event, vendored into this tree as a shape of its own.
324
+ // The manifest is the only place that knows whose aggregate raised it.
325
+ if aggregate, placed := r.opts.events[importPath]; placed {
326
+ out = eventRef{id: eventID(aggregate, name), name: name}
327
+ found = true
328
+
329
+ return false
330
+ }
331
+ // The same thing with nobody to place it: named, and left to the merge.
332
+ if importPath != "" && (strings.Contains(importPath, "/internal/infrastructure/") || strings.Contains(importPath, "/internal/integration/")) {
333
+ out = eventRef{name: name, foreign: importPath}
334
+ found = true
335
+
336
+ return false
337
+ }
338
+ if importPath != "" && r.module != "" && !strings.HasPrefix(importPath, r.module+"/") && !isStandard(importPath) {
339
+ out = eventRef{name: name, foreign: importPath}
340
+ found = true
341
+
342
+ return false
343
+ }
344
+
345
+ return true
346
+ })
347
+
348
+ return out, found
349
+ }
350
+
351
+ // isStandard is the one thing an import path says about itself: a standard
352
+ // library path has no dot in its first segment.
353
+ func isStandard(importPath string) bool {
354
+ first, _, _ := strings.Cut(importPath, "/")
355
+
356
+ return !strings.Contains(first, ".")
357
+ }
358
+
359
+ type eventRef struct {
360
+ // foreign is the import path of an event another repository declares.
361
+ foreign string
362
+ id string
363
+ name string
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // reading one use case
368
+ // ---------------------------------------------------------------------------
369
+
370
+ // scope is one function body being read: what the receiver is called, what
371
+ // ports its type holds, and what each local name has turned out to hold.
372
+ type scope struct {
373
+ pkg *pkg
374
+ key string
375
+ fields map[string]string
376
+ imports map[string]string
377
+ vars map[string]domainRef
378
+ recv string
379
+ // recvType is what the receiver's methods hang off: UseCase for a use
380
+ // case, its own name for a policy.
381
+ recvType string
382
+ }
383
+
384
+ // domainRef is a value whose type belongs to the service's own domain. Nothing
385
+ // else is tracked: a string is a string, and the only reason to follow a value
386
+ // at all is to notice when it is an event on its way to the bus.
387
+ type domainRef struct {
388
+ aggregate string
389
+ name string
390
+ event bool
391
+ }
392
+
393
+ func (r *flowReader) walkUseCase(d *flowDraft, key string, depth int) {
394
+ if depth > maxInline || d.seen[key] {
395
+ return
396
+ }
397
+ d.seen[key] = true
398
+
399
+ pkg := r.useCasePkg(key)
400
+ if pkg == nil {
401
+ return
402
+ }
403
+
404
+ handle := pkg.methods("UseCase")["Handle"]
405
+ if handle == nil {
406
+ r.b.Warn(key, pkg.dir+": no Handle on UseCase; the use case contributes no steps")
407
+
408
+ return
409
+ }
410
+
411
+ r.walkBody(d, &scope{
412
+ pkg: pkg,
413
+ key: key,
414
+ fields: useCaseFields(pkg),
415
+ imports: importsOf(pkg),
416
+ vars: map[string]domainRef{},
417
+ recv: receiverIdent(handle),
418
+ recvType: "UseCase",
419
+ }, handle, depth)
420
+ }
421
+
422
+ func (r *flowReader) walkBody(d *flowDraft, s *scope, fn *ast.FuncDecl, depth int) {
423
+ if fn == nil || fn.Body == nil {
424
+ return
425
+ }
426
+ r.walkStmts(d, s, fn.Body.List, depth)
427
+ }
428
+
429
+ func (r *flowReader) walkStmts(d *flowDraft, s *scope, list []ast.Stmt, depth int) {
430
+ for _, stmt := range list {
431
+ r.walkStmt(d, s, stmt, depth)
432
+ }
433
+ }
434
+
435
+ // walkStmt reads one statement in source order. An `if` may become a frame, a
436
+ // loop becomes a note on what it encloses, and everything else contributes
437
+ // its calls, in the order they are written, and no frame: a switch is a
438
+ // choice too, but its arms are values rather than conditions in words, and
439
+ // reading it as an alt is a later question.
440
+ func (r *flowReader) walkStmt(d *flowDraft, s *scope, stmt ast.Stmt, depth int) {
441
+ switch x := stmt.(type) {
442
+ case *ast.IfStmt:
443
+ r.walkIf(d, s, x, depth)
444
+ case *ast.ForStmt:
445
+ if x.Init != nil {
446
+ r.walkStmt(d, s, x.Init, depth)
447
+ }
448
+ if x.Cond != nil {
449
+ r.callsIn(d, s, x.Cond, depth)
450
+ }
451
+ d.enter(loopTitle(x))
452
+ r.walkStmts(d, s, x.Body.List, depth)
453
+ d.leave()
454
+ case *ast.RangeStmt:
455
+ r.callsIn(d, s, x.X, depth)
456
+ // `for _, s := range sessions`: the element is what the list holds.
457
+ // A list came back from a port as `[]*Session`, so its element is a
458
+ // Session, and what is called on it can be read the same way.
459
+ if list, ok := x.X.(*ast.Ident); ok {
460
+ if ref, tracked := s.vars[list.Name]; tracked {
461
+ if value, ok := x.Value.(*ast.Ident); ok && value.Name != "_" {
462
+ s.vars[value.Name] = ref
463
+ }
464
+ }
465
+ }
466
+ d.enter(loopTitle(x))
467
+ r.walkStmts(d, s, x.Body.List, depth)
468
+ d.leave()
469
+ case *ast.BlockStmt:
470
+ r.walkStmts(d, s, x.List, depth)
471
+ case *ast.LabeledStmt:
472
+ r.walkStmt(d, s, x.Stmt, depth)
473
+ case *ast.SwitchStmt:
474
+ if x.Init != nil {
475
+ r.walkStmt(d, s, x.Init, depth)
476
+ }
477
+ if x.Tag != nil {
478
+ r.callsIn(d, s, x.Tag, depth)
479
+ }
480
+ r.walkCases(d, s, x.Body.List, switchTitle(x.Tag), depth)
481
+ case *ast.TypeSwitchStmt:
482
+ if x.Init != nil {
483
+ r.walkStmt(d, s, x.Init, depth)
484
+ }
485
+ r.walkStmt(d, s, x.Assign, depth)
486
+ r.walkCases(d, s, x.Body.List, typeSwitchTitle(x.Assign), depth)
487
+ case *ast.SelectStmt:
488
+ r.walkStmts(d, s, x.Body.List, depth)
489
+ case *ast.CaseClause:
490
+ for _, expr := range x.List {
491
+ r.callsIn(d, s, expr, depth)
492
+ }
493
+ r.walkStmts(d, s, x.Body, depth)
494
+ case *ast.CommClause:
495
+ if x.Comm != nil {
496
+ r.walkStmt(d, s, x.Comm, depth)
497
+ }
498
+ r.walkStmts(d, s, x.Body, depth)
499
+ default:
500
+ r.callsIn(d, s, stmt, depth)
501
+ }
502
+ }
503
+
504
+ // walkCases reads a switch as one choice, an arm per case. The title of an
505
+ // arm is what the case says, with the subject in front of it - `resp.Code is
506
+ // 200, 201`, `err is ErrNotFound` - and `default` is otherwise. The rest of
507
+ // the rule is the `if` rule: an arm is terminal when it returns, the choice is
508
+ // drawn only if some arm has a hop in it, and a choice every arm of which
509
+ // leaves loses the marks.
510
+ func (r *flowReader) walkCases(d *flowDraft, s *scope, clauses []ast.Stmt, title func([]ast.Expr) string, depth int) {
511
+ var branches []catalog.AltBranch
512
+ titles := map[string]bool{}
513
+ drew := false
514
+ sawDefault := false
515
+
516
+ for _, stmt := range clauses {
517
+ clause, ok := stmt.(*ast.CaseClause)
518
+ if !ok {
519
+ continue
520
+ }
521
+ for _, expr := range clause.List {
522
+ r.callsIn(d, s, expr, depth)
523
+ }
524
+ d.push()
525
+ r.walkStmts(d, s, clause.Body, depth)
526
+ steps := d.pop()
527
+ drew = drew || len(steps) > 0
528
+
529
+ name := "otherwise"
530
+ if clause.List != nil {
531
+ name = title(clause.List)
532
+ } else {
533
+ sawDefault = true
534
+ }
535
+ terminal := len(clause.Body) > 0 && isReturn(clause.Body[len(clause.Body)-1])
536
+ branches = append(branches, catalog.AltBranch{Title: uniqueTitle(name, titles), Steps: steps, Terminal: terminal})
537
+ }
538
+ if !drew {
539
+ return
540
+ }
541
+ if !sawDefault {
542
+ branches = append(branches, catalog.AltBranch{Title: uniqueTitle("otherwise", titles), Steps: catalog.FlowNodes{}})
543
+ }
544
+
545
+ all := true
546
+ for _, branch := range branches {
547
+ if !branch.Terminal {
548
+ all = false
549
+ }
550
+ }
551
+ if all {
552
+ for i := range branches {
553
+ branches[i].Terminal = false
554
+ }
555
+ }
556
+
557
+ d.addAlt(branches)
558
+ }
559
+
560
+ func isReturn(stmt ast.Stmt) bool {
561
+ _, ok := stmt.(*ast.ReturnStmt)
562
+
563
+ return ok
564
+ }
565
+
566
+ // switchTitle names an arm of a switch: with a tag, `tag is a, b`; without
567
+ // one the cases are conditions in their own right, joined by or.
568
+ func switchTitle(tag ast.Expr) func([]ast.Expr) string {
569
+ return func(list []ast.Expr) string {
570
+ parts := make([]string, 0, len(list))
571
+ for _, expr := range list {
572
+ parts = append(parts, types.ExprString(expr))
573
+ }
574
+ if tag == nil {
575
+ return strings.Join(parts, " or ")
576
+ }
577
+
578
+ return types.ExprString(tag) + " is " + strings.Join(parts, ", ")
579
+ }
580
+ }
581
+
582
+ // typeSwitchTitle names an arm of a type switch by what is being asked
583
+ // about: `e is *OrderPlaced, *OrderCancelled`.
584
+ func typeSwitchTitle(assign ast.Stmt) func([]ast.Expr) string {
585
+ subject := ""
586
+ var find func(ast.Node) bool
587
+ find = func(node ast.Node) bool {
588
+ if assert, ok := node.(*ast.TypeAssertExpr); ok && assert.Type == nil {
589
+ subject = types.ExprString(assert.X)
590
+
591
+ return false
592
+ }
593
+
594
+ return true
595
+ }
596
+ ast.Inspect(assign, find)
597
+
598
+ return func(list []ast.Expr) string {
599
+ parts := make([]string, 0, len(list))
600
+ for _, expr := range list {
601
+ parts = append(parts, types.ExprString(expr))
602
+ }
603
+ if subject == "" {
604
+ return strings.Join(parts, ", ")
605
+ }
606
+
607
+ return subject + " is " + strings.Join(parts, ", ")
608
+ }
609
+ }
610
+
611
+ // walkIf reads an `if`, and its `else if` chain, as one choice. Each arm is
612
+ // read into its own list; the choice is kept only if some arm produced a
613
+ // step, because a frame around nothing tells the reader nothing. An arm that
614
+ // ends in a return is terminal - the flow does not continue past the alt on
615
+ // that path - and a chain with no final else gets an empty "otherwise" arm,
616
+ // which is the arm the steps after the alt follow.
617
+ func (r *flowReader) walkIf(d *flowDraft, s *scope, stmt *ast.IfStmt, depth int) {
618
+ var branches []catalog.AltBranch
619
+ titles := map[string]bool{}
620
+ drew := false
621
+
622
+ current := stmt
623
+ for {
624
+ if current.Init != nil {
625
+ r.walkStmt(d, s, current.Init, depth)
626
+ }
627
+ r.callsIn(d, s, current.Cond, depth)
628
+
629
+ d.push()
630
+ r.walkStmts(d, s, current.Body.List, depth)
631
+ steps := d.pop()
632
+ drew = drew || len(steps) > 0
633
+ branches = append(branches, catalog.AltBranch{
634
+ Title: uniqueTitle(types.ExprString(current.Cond), titles),
635
+ Steps: steps,
636
+ Terminal: endsWithReturn(current.Body),
637
+ })
638
+
639
+ if current.Else == nil {
640
+ branches = append(branches, catalog.AltBranch{Title: uniqueTitle("otherwise", titles), Steps: catalog.FlowNodes{}})
641
+
642
+ break
643
+ }
644
+ if next, ok := current.Else.(*ast.IfStmt); ok {
645
+ current = next
646
+
647
+ continue
648
+ }
649
+ block, ok := current.Else.(*ast.BlockStmt)
650
+ if !ok {
651
+ break
652
+ }
653
+ d.push()
654
+ r.walkStmts(d, s, block.List, depth)
655
+ steps = d.pop()
656
+ drew = drew || len(steps) > 0
657
+ branches = append(branches, catalog.AltBranch{
658
+ Title: uniqueTitle("otherwise", titles),
659
+ Steps: steps,
660
+ Terminal: endsWithReturn(block),
661
+ })
662
+
663
+ break
664
+ }
665
+
666
+ if !drew {
667
+ return
668
+ }
669
+
670
+ // A choice every arm of which ends the flow is not a choice about what
671
+ // comes next: nothing does, and the catalog refuses to draw an alt whose
672
+ // branches all leave. The arms stay; the mark comes off.
673
+ all := true
674
+ for _, branch := range branches {
675
+ if !branch.Terminal {
676
+ all = false
677
+ }
678
+ }
679
+ if all {
680
+ for i := range branches {
681
+ branches[i].Terminal = false
682
+ }
683
+ }
684
+
685
+ d.addAlt(branches)
686
+ }
687
+
688
+ // callsIn contributes the calls of one node, in the order they are written.
689
+ func (r *flowReader) callsIn(d *flowDraft, s *scope, node ast.Node, depth int) {
690
+ if node == nil {
691
+ return
692
+ }
693
+ for _, site := range callSitesIn(node) {
694
+ r.call(d, s, site, depth)
695
+ }
696
+ }
697
+
698
+ func uniqueTitle(title string, seen map[string]bool) string {
699
+ out := title
700
+ for n := 2; seen[out]; n++ {
701
+ out = title + " (" + strconv.Itoa(n) + ")"
702
+ }
703
+ seen[out] = true
704
+
705
+ return out
706
+ }
707
+
708
+ // endsWithReturn is what makes a branch terminal: control leaves the function
709
+ // at the end of the block, so nothing written after the `if` runs on it.
710
+ func endsWithReturn(block *ast.BlockStmt) bool {
711
+ if block == nil || len(block.List) == 0 {
712
+ return false
713
+ }
714
+ _, ok := block.List[len(block.List)-1].(*ast.ReturnStmt)
715
+
716
+ return ok
717
+ }
718
+
719
+ func (r *flowReader) call(d *flowDraft, s *scope, site callSite, depth int) {
720
+ if ident, ok := site.call.Fun.(*ast.Ident); ok && ident.Name == "append" {
721
+ // Preserve an event while it is collected into a variadic slice before
722
+ // Save(ctx, aggregate, events...). The slice is still carrying that
723
+ // domain fact even though its static type is []event.Event.
724
+ for _, arg := range site.call.Args[1:] {
725
+ name, ok := arg.(*ast.Ident)
726
+ if !ok {
727
+ continue
728
+ }
729
+ ref, tracked := s.vars[name.Name]
730
+ if tracked && ref.event {
731
+ bind(s, site, []domainRef{ref})
732
+ break
733
+ }
734
+ }
735
+ return
736
+ }
737
+
738
+ selector, ok := site.call.Fun.(*ast.SelectorExpr)
739
+ if !ok {
740
+ return
741
+ }
742
+ method := selector.Sel.Name
743
+
744
+ switch x := selector.X.(type) {
745
+ case *ast.SelectorExpr:
746
+ // uc.<port>.<Method>(...): the only shape that is a hop.
747
+ receiver, ok := x.X.(*ast.Ident)
748
+ if !ok || receiver.Name != s.recv {
749
+ return
750
+ }
751
+ r.portCall(d, s, x.Sel.Name, method, site, depth)
752
+
753
+ case *ast.Ident:
754
+ switch {
755
+ case x.Name == s.recv:
756
+ // uc.<helper>(...): still this use case, in another method. The
757
+ // writing a use case does in a helper is writing it does.
758
+ if helper := s.pkg.methods(s.recvType)[method]; helper != nil {
759
+ r.walkBody(d, &scope{
760
+ pkg: s.pkg,
761
+ key: s.key,
762
+ fields: s.fields,
763
+ imports: s.imports,
764
+ vars: map[string]domainRef{},
765
+ recv: receiverIdent(helper),
766
+ recvType: s.recvType,
767
+ }, helper, depth)
768
+ }
769
+
770
+ case s.imports[x.Name] != "":
771
+ // session.Start(...): a domain constructor. Not a hop - nothing
772
+ // left the service - but its signature says what came back.
773
+ bind(s, site, r.resultsOfFunc(s.imports[x.Name], method))
774
+
775
+ default:
776
+ // sess.Revoke(...): a method on something the domain handed over.
777
+ if ref, ok := s.vars[x.Name]; ok {
778
+ bind(s, site, r.resultsOfMethod(ref, method))
779
+ }
780
+ }
781
+ }
782
+ }
783
+
784
+ // portCall turns a call on a port into a hop, and decides whose lane it lands
785
+ // in. Everything a use case reaches, it reaches through a field of UseCase.
786
+ func (r *flowReader) portCall(d *flowDraft, s *scope, field, method string, site callSite, depth int) {
787
+ declared := s.fields[field]
788
+ if declared == "" || strings.HasPrefix(declared, "func") {
789
+ // A clock and an id generator are ports too, but nothing is at the
790
+ // other end of them.
791
+ return
792
+ }
793
+
794
+ source, line := s.pkg.position(site.call.Pos())
795
+
796
+ // A port that is, or is adapted over, another service's generated client.
797
+ if hops := r.clientCalls(s, declared, method); len(hops) > 0 {
798
+ for _, hop := range hops {
799
+ r.rpcHop(d, hop, at(source, line))
800
+ }
801
+
802
+ return
803
+ }
804
+
805
+ // A port this use case declares, bound in assembly to another use case:
806
+ // by method when the adapter reaches several, by port otherwise.
807
+ target, ok := r.bindings[s.key+"."+declared+"."+method]
808
+ if !ok {
809
+ target, ok = r.bindings[s.key+"."+declared]
810
+ }
811
+ if ok {
812
+ r.useCaseHop(d, target, "Port `"+declared+"`, bound at assembly to the "+operationName(target)+" use case.", at(source, line), depth)
813
+
814
+ return
815
+ }
816
+
817
+ // A field holding another use case outright, which is what a policy does.
818
+ if target, ok := useCaseSelector(declared, s.imports); ok {
819
+ r.useCaseHop(d, target, "", at(source, line), depth)
820
+
821
+ return
822
+ }
823
+
824
+ // A consumer-owned interface bound directly to a concrete adapter by Wire
825
+ // is local work. There is no architectural lane to draw for password
826
+ // hashing, encoding, or another in-process mechanism.
827
+ if r.wireBoundPorts[s.key+"."+declared] {
828
+ return
829
+ }
830
+
831
+ // A port of the domain: the store is at the other end of it.
832
+ aggregate, name, ok := domainSelector(declared, s.imports)
833
+ if !ok {
834
+ selector, _, _ := strings.Cut(strings.TrimPrefix(declared, "*"), ".")
835
+ if applicationSupportImport(s.imports[selector]) {
836
+ // Application services such as password hashing are local work,
837
+ // not a hop to another participant in the architecture flow.
838
+ return
839
+ }
840
+ r.b.Warn(s.key, s.pkg.dir+": port `"+field+" "+declared+"` is neither a domain port nor a use case; its calls are left out of the flow")
841
+
842
+ return
843
+ }
844
+
845
+ d.add(catalog.Step{
846
+ From: r.opts.svcID,
847
+ To: r.storeLane(d),
848
+ Kind: catalog.StepCall,
849
+ Label: method,
850
+ Line: at(source, line),
851
+ })
852
+
853
+ // The events a change produced are handed to the repository along with the
854
+ // aggregate, so a port call carrying one is where the fact leaves for the
855
+ // bus. What carries it there - an outbox, a relay - is the adapter's
856
+ // business and is not a step the source can show.
857
+ for _, arg := range site.call.Args {
858
+ ident, ok := arg.(*ast.Ident)
859
+ if !ok {
860
+ continue
861
+ }
862
+ ref, tracked := s.vars[ident.Name]
863
+ if !tracked || !ref.event {
864
+ continue
865
+ }
866
+
867
+ id := eventID(aggregateID(r.opts.svcID, ref.aggregate), ref.name)
868
+ d.add(catalog.Step{
869
+ From: r.opts.svcID,
870
+ To: d.laneID(r.busLane()),
871
+ Kind: catalog.StepEvent,
872
+ Ref: id,
873
+ Label: ref.name,
874
+ Line: at(source, line),
875
+ })
876
+ r.referenced[id] = true
877
+ }
878
+
879
+ bind(s, site, r.resultsOfPortMethod(aggregate, name, method))
880
+ }
881
+
882
+ // rpcHop is a call to another service, on the lane the manifest gives its
883
+ // package. The call is also recorded for the service's consumes, sourced from
884
+ // the generated client it was read off, so the step's ref resolves without
885
+ // anybody vendoring the proto twice.
886
+ func (r *flowReader) rpcHop(d *flowDraft, hop rpcHop, line string) {
887
+ id := hop.client.methods[hop.method]
888
+ lane, peer, status := r.peerLane(d, hop.client)
889
+
890
+ // The label is the operation as the contract names it - the rpc, or the
891
+ // operationId - rather than the Go method the client offers it under:
892
+ // GetUserWithResponse is how the client is called, getUser is what runs.
893
+ d.add(catalog.Step{
894
+ From: r.opts.svcID,
895
+ To: lane,
896
+ Kind: catalog.StepRPC,
897
+ Ref: id,
898
+ Label: id[strings.LastIndex(id, "/")+1:],
899
+ Status: status,
900
+ Line: line,
901
+ })
902
+
903
+ if _, seen := r.calls[id]; !seen {
904
+ r.calls[id] = catalog.RpcCall{ID: id, Peer: peer, Status: status, Source: hop.client.source}
905
+ }
906
+ }
907
+
908
+ // useCaseHop is a call into another use case of the same service: a message to
909
+ // itself, and then that use case's own steps.
910
+ func (r *flowReader) useCaseHop(d *flowDraft, target, note, line string, depth int) {
911
+ d.add(catalog.Step{
912
+ From: r.opts.svcID,
913
+ To: r.opts.svcID,
914
+ Kind: catalog.StepCall,
915
+ Label: operationName(target),
916
+ Note: note,
917
+ Line: line,
918
+ })
919
+
920
+ r.walkUseCase(d, target, depth+1)
921
+ }
922
+
923
+ // ---------------------------------------------------------------------------
924
+ // following a value back to its type
925
+ // ---------------------------------------------------------------------------
926
+
927
+ // resultsOfFunc reads the results of a package-level function of a domain
928
+ // package: `func Start(...) (*Session, event.SessionStarted, error)`.
929
+ func (r *flowReader) resultsOfFunc(importPath, name string) []domainRef {
930
+ aggregate, ok := domainPackage(importPath)
931
+ if !ok {
932
+ return nil
933
+ }
934
+
935
+ pkg := r.domainPkg(aggregate)
936
+ if pkg == nil {
937
+ return nil
938
+ }
939
+
940
+ for _, file := range pkg.files {
941
+ for _, decl := range file.Decls {
942
+ fn, ok := decl.(*ast.FuncDecl)
943
+ if !ok || fn.Recv != nil || fn.Name.Name != name {
944
+ continue
945
+ }
946
+
947
+ return r.resultRefs(pkg, aggregate, fn.Type)
948
+ }
949
+ }
950
+
951
+ return nil
952
+ }
953
+
954
+ // resultsOfMethod reads the results of a method on a domain type: what
955
+ // `sess.Revoke(...)` gives back.
956
+ func (r *flowReader) resultsOfMethod(ref domainRef, name string) []domainRef {
957
+ pkg := r.domainPkg(ref.aggregate)
958
+ if pkg == nil {
959
+ return nil
960
+ }
961
+
962
+ fn := pkg.methods(ref.name)[name]
963
+ if fn == nil {
964
+ return nil
965
+ }
966
+
967
+ return r.resultRefs(pkg, ref.aggregate, fn.Type)
968
+ }
969
+
970
+ // resultsOfPortMethod reads the results of one method of a domain port:
971
+ // `ByID(ctx, id) (*Session, error)` is how a use case comes to hold a Session.
972
+ func (r *flowReader) resultsOfPortMethod(aggregate, port, name string) []domainRef {
973
+ pkg := r.domainPkg(aggregate)
974
+ if pkg == nil {
975
+ return nil
976
+ }
977
+
978
+ for _, file := range pkg.files {
979
+ for _, decl := range file.Decls {
980
+ gen, ok := decl.(*ast.GenDecl)
981
+ if !ok {
982
+ continue
983
+ }
984
+
985
+ for _, spec := range gen.Specs {
986
+ typeSpec, ok := spec.(*ast.TypeSpec)
987
+ if !ok || typeSpec.Name.Name != port {
988
+ continue
989
+ }
990
+ iface, ok := typeSpec.Type.(*ast.InterfaceType)
991
+ if !ok || iface.Methods == nil {
992
+ continue
993
+ }
994
+
995
+ for _, method := range iface.Methods.List {
996
+ fn, ok := method.Type.(*ast.FuncType)
997
+ if !ok || len(method.Names) == 0 || method.Names[0].Name != name {
998
+ continue
999
+ }
1000
+
1001
+ return r.resultRefs(pkg, aggregate, fn)
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ return nil
1008
+ }
1009
+
1010
+ // resultRefs turns a result list into what each position holds, by position, so
1011
+ // that the names on the left of an assignment can be paired with it. A result
1012
+ // this cannot place - a string, an error - leaves a hole rather than shifting
1013
+ // everything after it along.
1014
+ func (r *flowReader) resultRefs(pkg *pkg, aggregate string, fn *ast.FuncType) []domainRef {
1015
+ if fn == nil || fn.Results == nil {
1016
+ return nil
1017
+ }
1018
+
1019
+ imports := importsOf(pkg)
1020
+
1021
+ var out []domainRef
1022
+ for _, result := range fn.Results.List {
1023
+ ref := domainRef{}
1024
+
1025
+ name := strings.TrimPrefix(strings.TrimPrefix(types.ExprString(result.Type), "[]"), "*")
1026
+ name = strings.TrimPrefix(name, "[]")
1027
+
1028
+ if selector, typeName, cut := strings.Cut(name, "."); cut {
1029
+ if agg, isEvent := eventPackage(imports[selector]); isEvent {
1030
+ ref = domainRef{aggregate: agg, name: typeName, event: true}
1031
+ }
1032
+ } else if exported(name) {
1033
+ ref = domainRef{aggregate: aggregate, name: name}
1034
+ }
1035
+
1036
+ // One entry per name, because `a, b Session` is two results.
1037
+ count := len(result.Names)
1038
+ if count == 0 {
1039
+ count = 1
1040
+ }
1041
+ for range count {
1042
+ out = append(out, ref)
1043
+ }
1044
+ }
1045
+
1046
+ return out
1047
+ }
1048
+
1049
+ func bind(s *scope, site callSite, results []domainRef) {
1050
+ for i, expr := range site.lhs {
1051
+ if i >= len(results) || results[i].name == "" {
1052
+ continue
1053
+ }
1054
+ if ident, ok := expr.(*ast.Ident); ok && ident.Name != "_" {
1055
+ s.vars[ident.Name] = results[i]
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ // ---------------------------------------------------------------------------
1061
+ // the draft a flow is built up in
1062
+ // ---------------------------------------------------------------------------
1063
+
1064
+ type flowDraft struct {
1065
+ lanes []catalog.Participant
1066
+ steps catalog.FlowNodes
1067
+ // sinks is where the next node goes: the arm of an alt being read, or,
1068
+ // with nothing pushed, the flow's own list.
1069
+ sinks []*catalog.FlowNodes
1070
+ // n numbers every node, step or frame, so an id is unique wherever the
1071
+ // node sits.
1072
+ n int
1073
+ seen map[string]bool
1074
+ // loops is the loops enclosing whatever is being read right now, outermost
1075
+ // first. A use case called once per session is read once, and every step it
1076
+ // contributes happens once per session; the fact belongs to the steps, not
1077
+ // to the call that got there.
1078
+ loops []string
1079
+ }
1080
+
1081
+ func (d *flowDraft) enter(loop string) { d.loops = append(d.loops, loop) }
1082
+ func (d *flowDraft) leave() { d.loops = d.loops[:len(d.loops)-1] }
1083
+
1084
+ // note puts the loops in front of whatever the step had to say for itself.
1085
+ func (d *flowDraft) note(own string) string {
1086
+ var loops []string
1087
+ for _, loop := range d.loops {
1088
+ if loop == "" || contains(loops, loop) {
1089
+ continue
1090
+ }
1091
+ loops = append(loops, loop)
1092
+ }
1093
+
1094
+ out := ""
1095
+ if len(loops) > 0 {
1096
+ out = strings.Join(loops, ", ") + "."
1097
+ }
1098
+
1099
+ return strings.TrimSpace(out + " " + own)
1100
+ }
1101
+
1102
+ func contains(list []string, value string) bool {
1103
+ for _, existing := range list {
1104
+ if existing == value {
1105
+ return true
1106
+ }
1107
+ }
1108
+
1109
+ return false
1110
+ }
1111
+
1112
+ // lane declares a participant once, in the order it was first needed. That
1113
+ // order is the order of the lanes in every picture drawn from this.
1114
+ func (d *flowDraft) lane(p catalog.Participant) string {
1115
+ for _, existing := range d.lanes {
1116
+ if existing.ID == p.ID {
1117
+ return p.ID
1118
+ }
1119
+ }
1120
+ d.lanes = append(d.lanes, p)
1121
+
1122
+ return p.ID
1123
+ }
1124
+
1125
+ func (d *flowDraft) laneID(p catalog.Participant) string { return d.lane(p) }
1126
+
1127
+ func newDraft() *flowDraft { return &flowDraft{seen: map[string]bool{}} }
1128
+
1129
+ func (d *flowDraft) sink() *catalog.FlowNodes {
1130
+ if len(d.sinks) == 0 {
1131
+ return &d.steps
1132
+ }
1133
+
1134
+ return d.sinks[len(d.sinks)-1]
1135
+ }
1136
+
1137
+ func (d *flowDraft) push() { d.sinks = append(d.sinks, &catalog.FlowNodes{}) }
1138
+
1139
+ func (d *flowDraft) pop() catalog.FlowNodes {
1140
+ top := d.sinks[len(d.sinks)-1]
1141
+ d.sinks = d.sinks[:len(d.sinks)-1]
1142
+
1143
+ return *top
1144
+ }
1145
+
1146
+ func (d *flowDraft) add(step catalog.Step) {
1147
+ d.n++
1148
+ step.Type = "step"
1149
+ step.ID = "s" + strconv.Itoa(d.n)
1150
+ step.Note = d.note(step.Note)
1151
+ // Nothing here has been watched running. `declared` is the whole of what
1152
+ // reading source can claim - unless the step already says less.
1153
+ if step.Status == "" {
1154
+ step.Status = catalog.StatusDeclared
1155
+ }
1156
+ sink := d.sink()
1157
+ *sink = append(*sink, &step)
1158
+ }
1159
+
1160
+ func (d *flowDraft) addAlt(branches []catalog.AltBranch) {
1161
+ d.n++
1162
+ sink := d.sink()
1163
+ *sink = append(*sink, &catalog.Alt{Type: "alt", ID: "alt" + strconv.Itoa(d.n), Branches: branches})
1164
+ }
1165
+
1166
+ func (r *flowReader) serviceLane() catalog.Participant {
1167
+ context := r.opts.context
1168
+
1169
+ return catalog.Participant{ID: r.opts.svcID, Kind: catalog.ParticipantService, Context: &context}
1170
+ }
1171
+
1172
+ func (r *flowReader) busLane() catalog.Participant {
1173
+ return catalog.Participant{ID: laneBus, Kind: catalog.ParticipantBroker}
1174
+ }
1175
+
1176
+ // storeLane is the lane persistence lands in, and the service's own when the
1177
+ // manifest does not name a store. A call on a repository happened either way;
1178
+ // what is unknown is only where it landed, and saying so once is better than
1179
+ // inventing a database nobody named.
1180
+ //
1181
+ // The id is `<service>-<store>` rather than the store's own `<service>.<store>`
1182
+ // because a participant that is not a service has to be a bare name: a dot is
1183
+ // how the diagram model spells containment, and a lane called `auth.auth.pg`
1184
+ // is read there as something inside the service rather than beside it.
1185
+ func (r *flowReader) storeLane(d *flowDraft) string {
1186
+ if r.opts.store == "" {
1187
+ if !r.warnedStore {
1188
+ r.b.Warn(r.opts.svcID, "no store named in the options, so repository calls stay on the service's own lane")
1189
+ r.warnedStore = true
1190
+ }
1191
+
1192
+ return r.opts.svcID
1193
+ }
1194
+
1195
+ context := r.opts.context
1196
+
1197
+ return d.lane(catalog.Participant{
1198
+ ID: r.opts.service + "-" + r.opts.store,
1199
+ Kind: catalog.ParticipantStore,
1200
+ Context: &context,
1201
+ })
1202
+ }
1203
+
1204
+ // ---------------------------------------------------------------------------
1205
+ // syntax helpers
1206
+ // ---------------------------------------------------------------------------
1207
+
1208
+ // callSite is one call and the names it was assigned to.
1209
+ type callSite struct {
1210
+ call *ast.CallExpr
1211
+ lhs []ast.Expr
1212
+ }
1213
+
1214
+ // callSites lists the calls of a body in source order.
1215
+ //
1216
+ // The order is the flow. ast.Inspect walks children in the order they are
1217
+ // written, so a call in an `if` initialiser comes before the block it guards,
1218
+ // which is what a reader of the code sees too.
1219
+ func callSites(fn *ast.FuncDecl) []callSite {
1220
+ if fn == nil || fn.Body == nil {
1221
+ return nil
1222
+ }
1223
+
1224
+ return callSitesIn(fn.Body)
1225
+ }
1226
+
1227
+ // callSitesIn lists the calls under one node, in source order.
1228
+ func callSitesIn(node ast.Node) []callSite {
1229
+ assigned := map[*ast.CallExpr][]ast.Expr{}
1230
+ ast.Inspect(node, func(n ast.Node) bool {
1231
+ assign, ok := n.(*ast.AssignStmt)
1232
+ if !ok || len(assign.Rhs) != 1 {
1233
+ return true
1234
+ }
1235
+ if call, ok := assign.Rhs[0].(*ast.CallExpr); ok {
1236
+ assigned[call] = assign.Lhs
1237
+ }
1238
+
1239
+ return true
1240
+ })
1241
+
1242
+ var out []callSite
1243
+ ast.Inspect(node, func(n ast.Node) bool {
1244
+ if call, ok := n.(*ast.CallExpr); ok {
1245
+ out = append(out, callSite{call: call, lhs: assigned[call]})
1246
+ }
1247
+
1248
+ return true
1249
+ })
1250
+
1251
+ return out
1252
+ }
1253
+
1254
+ // loopTitle says that a step repeats, for the loop it sits in.
1255
+ //
1256
+ // A loop is a frame the catalog can hold, and this deliberately does not build
1257
+ // one: the frame would have to say what it repeats until, and the condition is
1258
+ // written for a compiler rather than for a reader. A note says the true part.
1259
+ func loopTitle(stmt ast.Stmt) string {
1260
+ switch node := stmt.(type) {
1261
+ case *ast.RangeStmt:
1262
+ return "inside a loop over `" + types.ExprString(node.X) + "`"
1263
+ case *ast.ForStmt:
1264
+ if node.Cond != nil {
1265
+ return "inside a loop, while `" + types.ExprString(node.Cond) + "`"
1266
+ }
1267
+
1268
+ return "inside a loop"
1269
+ }
1270
+
1271
+ return ""
1272
+ }
1273
+
1274
+ // useCaseFields reads the ports off `type UseCase struct`.
1275
+ func useCaseFields(pkg *pkg) map[string]string {
1276
+ for _, decl := range pkg.structs() {
1277
+ if decl.name == "UseCase" {
1278
+ return structFields(decl.fields)
1279
+ }
1280
+ }
1281
+
1282
+ return map[string]string{}
1283
+ }
1284
+
1285
+ func structFields(st *ast.StructType) map[string]string {
1286
+ out := map[string]string{}
1287
+ if st == nil || st.Fields == nil {
1288
+ return out
1289
+ }
1290
+
1291
+ for _, field := range st.Fields.List {
1292
+ declared := types.ExprString(field.Type)
1293
+ for _, name := range field.Names {
1294
+ out[name.Name] = declared
1295
+ }
1296
+ }
1297
+
1298
+ return out
1299
+ }
1300
+
1301
+ // importsOf maps the name a package's files refer to an import by - its alias,
1302
+ // or the last segment of its path - to the path itself.
1303
+ func importsOf(pkg *pkg) map[string]string {
1304
+ out := map[string]string{}
1305
+
1306
+ for _, file := range pkg.files {
1307
+ for _, spec := range file.Imports {
1308
+ importPath := strings.Trim(spec.Path.Value, `"`)
1309
+
1310
+ name := path.Base(importPath)
1311
+ if spec.Name != nil {
1312
+ name = spec.Name.Name
1313
+ }
1314
+ out[name] = importPath
1315
+
1316
+ // A feature-sliced domain commonly lives in .../user/domain while
1317
+ // declaring `package user`. Go binds the declared package name, not
1318
+ // the final path segment, so retain that logical name as well.
1319
+ if spec.Name == nil {
1320
+ if aggregate, ok := domainImport(importPath); ok {
1321
+ out[aggregate] = importPath
1322
+ }
1323
+ }
1324
+ }
1325
+ }
1326
+
1327
+ return out
1328
+ }
1329
+
1330
+ // domainPackage reads an import of the service's own domain back to the
1331
+ // aggregate it belongs to, and says no to everything else.
1332
+ func domainPackage(importPath string) (string, bool) {
1333
+ return domainImport(importPath)
1334
+ }
1335
+
1336
+ // eventPackage recognizes the event package below either supported domain
1337
+ // layout, plus a feature's integration-event package.
1338
+ func eventPackage(importPath string) (string, bool) {
1339
+ return eventImport(importPath)
1340
+ }
1341
+
1342
+ // domainSelector reads a field type like `session.Repository` back to the
1343
+ // aggregate and the port name.
1344
+ func domainSelector(declared string, imports map[string]string) (string, string, bool) {
1345
+ selector, name, found := strings.Cut(strings.TrimPrefix(declared, "*"), ".")
1346
+ if !found {
1347
+ return "", "", false
1348
+ }
1349
+
1350
+ aggregate, ok := domainPackage(imports[selector])
1351
+
1352
+ return aggregate, name, ok
1353
+ }
1354
+
1355
+ // useCaseSelector reads `*end_after_credential_change.UseCase` back to the use
1356
+ // case key.
1357
+ func useCaseSelector(declared string, imports map[string]string) (string, bool) {
1358
+ selector, name, found := strings.Cut(strings.TrimPrefix(declared, "*"), ".")
1359
+ if !found || name != "UseCase" {
1360
+ return "", false
1361
+ }
1362
+
1363
+ aggregate, useCaseName, found := useCaseImport(imports[selector])
1364
+ if !found {
1365
+ return "", false
1366
+ }
1367
+
1368
+ return aggregate + "/" + useCaseName, true
1369
+ }
1370
+
1371
+ func (r *flowReader) useCasePkg(key string) *pkg {
1372
+ if cached, ok := r.useCases[key]; ok {
1373
+ return cached
1374
+ }
1375
+
1376
+ dir := r.layout.useCases[key]
1377
+ pkg, err := parsePkg(r.root, dir)
1378
+ if err != nil {
1379
+ r.b.Warn(key, dir+" could not be parsed; its steps are missing from every flow that runs it")
1380
+ pkg = nil
1381
+ }
1382
+ r.useCases[key] = pkg
1383
+
1384
+ return pkg
1385
+ }
1386
+
1387
+ func (r *flowReader) domainPkg(aggregate string) *pkg {
1388
+ if cached, ok := r.domains[aggregate]; ok {
1389
+ return cached
1390
+ }
1391
+
1392
+ pkg, err := parsePkg(r.root, r.layout.domains[aggregate])
1393
+ if err != nil {
1394
+ pkg = nil
1395
+ }
1396
+ r.domains[aggregate] = pkg
1397
+
1398
+ return pkg
1399
+ }
1400
+
1401
+ func (r *flowReader) useCaseDoc(key string) string {
1402
+ pkg := r.useCasePkg(key)
1403
+ if pkg == nil {
1404
+ return ""
1405
+ }
1406
+
1407
+ return operationDoc(r.root, r.layout.useCases[key], pkg)
1408
+ }
1409
+
1410
+ func operationName(key string) string {
1411
+ _, name, _ := strings.Cut(key, "/")
1412
+
1413
+ return camel(name)
1414
+ }
1415
+
1416
+ // at is the `file:line` a reader can open. Written here rather than at every
1417
+ // call site so that a step without a position says nothing instead of ":0".
1418
+ func at(source string, line int) string {
1419
+ if source == "" || line == 0 {
1420
+ return ""
1421
+ }
1422
+
1423
+ return source + ":" + strconv.Itoa(line)
1424
+ }
1425
+
1426
+ // sentence turns a slug into a name written the way a person would: one capital
1427
+ // at the front, and nothing else touched.
1428
+ func sentence(slug string) string {
1429
+ words := strings.Split(slug, "-")
1430
+ if len(words) == 0 || words[0] == "" {
1431
+ return slug
1432
+ }
1433
+
1434
+ runes := []rune(words[0])
1435
+ if runes[0] >= 'a' && runes[0] <= 'z' {
1436
+ runes[0] = runes[0] - 'a' + 'A'
1437
+ }
1438
+ words[0] = string(runes)
1439
+
1440
+ return strings.Join(words, " ")
1441
+ }