@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,96 @@
1
+ // A source path, as a link the reader's own editor opens.
2
+ //
3
+ // The forge link answers "show me this file at the built commit". This one
4
+ // answers "let me edit it", which is the question a reader whose own catalog
5
+ // is on screen is actually asking - and going through GitHub to reach a file
6
+ // that is on the disk under the browser is the trip this saves.
7
+ //
8
+ // It needs an absolute path, which only the local server knows: the catalog
9
+ // spells sources relative to the repository, and the browser has no idea
10
+ // where that repository is. So a link exists only in local mode, and the
11
+ // workspace the server reports is joined with the catalog's own path.
12
+
13
+ import { buildInfo } from "./build-info";
14
+ import type { BuildInfo } from "./build-info";
15
+ import { bare, looksLikePath, splitLine } from "./source-link";
16
+ import type { SourceLocation } from "./source-link";
17
+
18
+ /** The editors with a URL scheme that opens a file at a line. */
19
+ export const EDITORS = [
20
+ { id: "vscode", name: "VS Code" },
21
+ { id: "vscode-insiders", name: "VS Code Insiders" },
22
+ { id: "cursor", name: "Cursor" },
23
+ { id: "zed", name: "Zed" },
24
+ { id: "idea", name: "JetBrains" },
25
+ ] as const;
26
+
27
+ export type EditorId = (typeof EDITORS)[number]["id"];
28
+
29
+ export const DEFAULT_EDITOR: EditorId = "vscode";
30
+
31
+ export function isEditorId(value: unknown): value is EditorId {
32
+ return EDITORS.some((e) => e.id === value);
33
+ }
34
+
35
+ export function editorName(id: EditorId): string {
36
+ return EDITORS.find((e) => e.id === id)?.name ?? id;
37
+ }
38
+
39
+ /**
40
+ * Join a workspace and a catalog path into one absolute path, or null when
41
+ * the path is not one the workspace can hold: absolute already, or climbing
42
+ * out of it. The server refuses those too; refusing here means no dead link.
43
+ */
44
+ export function absolutePath(workspace: string, path: string): string | null {
45
+ const root = workspace.replace(/\/+$/, "");
46
+ if (!root) return null;
47
+ const clean = path.trim().replaceAll("\\", "/").replace(/^\.\//, "");
48
+ if (!clean || clean.startsWith("/") || clean.split("/").includes("..")) {
49
+ return null;
50
+ }
51
+ return `${root}/${clean}`;
52
+ }
53
+
54
+ /**
55
+ * The link that opens `where` - a path, or `path:line` - in `editor`, or null
56
+ * when there is nothing to open. The path segments are percent-encoded as a
57
+ * URL's would be, so a space in a directory name survives the scheme.
58
+ */
59
+ export function editorHref(
60
+ editor: EditorId,
61
+ workspace: string,
62
+ where: string,
63
+ ): string | null {
64
+ const { path, line } = splitLine(where);
65
+ if (!looksLikePath(path)) return null;
66
+ const absolute = absolutePath(workspace, path);
67
+ if (!absolute) return null;
68
+
69
+ if (editor === "idea") {
70
+ const query = new URLSearchParams({ file: absolute });
71
+ if (line) query.set("line", String(line));
72
+ return `idea://open?${query.toString()}`;
73
+ }
74
+
75
+ const encoded = absolute.split("/").map(encodeURIComponent).join("/");
76
+ return `${editor}://file${encoded}${line ? `:${line}` : ""}`;
77
+ }
78
+
79
+ /**
80
+ * The catalog path a resolved source can be edited at, or null when the file
81
+ * is not in this workspace: a service fetched from another repository lives
82
+ * on the forge, not on the disk under the browser. A path with no forge
83
+ * behind it is local by definition, and a forge path counts when the forge
84
+ * is the repository this was built from.
85
+ */
86
+ export function editorWhere(
87
+ location: SourceLocation,
88
+ info: BuildInfo = buildInfo,
89
+ ): string | null {
90
+ if (location.kind === "remote") {
91
+ if (!info.repoUrl || bare(location.repositoryUrl) !== bare(info.repoUrl)) {
92
+ return null;
93
+ }
94
+ }
95
+ return location.line ? `${location.path}:${location.line}` : location.path;
96
+ }
@@ -0,0 +1,27 @@
1
+ // Which editor the "open in editor" link opens. One key, one value, and the
2
+ // default when nothing is stored or storage is unavailable.
3
+
4
+ import { DEFAULT_EDITOR, isEditorId } from "./editor-link";
5
+ import type { EditorId } from "./editor-link";
6
+
7
+ export const EDITOR_KEY = "portolan.editor";
8
+
9
+ export function parseEditor(raw: string | null | undefined): EditorId {
10
+ return isEditorId(raw) ? raw : DEFAULT_EDITOR;
11
+ }
12
+
13
+ export function readEditor(): EditorId {
14
+ try {
15
+ return parseEditor(localStorage.getItem(EDITOR_KEY));
16
+ } catch {
17
+ return DEFAULT_EDITOR;
18
+ }
19
+ }
20
+
21
+ export function writeEditor(editor: EditorId): void {
22
+ try {
23
+ localStorage.setItem(EDITOR_KEY, editor);
24
+ } catch {
25
+ // A private window keeps the choice for the session in memory only.
26
+ }
27
+ }
@@ -0,0 +1,237 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { catalog } from "../testing/estate";
3
+ import { thinCatalog, wideCatalog } from "./scenarios";
4
+ import {
5
+ bundleById,
6
+ bundleId,
7
+ bundles,
8
+ edgeCount,
9
+ eventGraph,
10
+ filterEventGraph,
11
+ parseBundleId,
12
+ } from "./event-graph";
13
+
14
+ const sample = eventGraph(catalog);
15
+
16
+ describe("eventGraph", () => {
17
+ it("makes a node of every service and every event", () => {
18
+ expect(sample.services.filter((s) => !s.ghost).map((s) => s.id)).toEqual([
19
+ "shop.oms",
20
+ "shop.pricing",
21
+ // Merge order is source path order, so what the examples publish comes
22
+ // after the hand-written estate: cart joins shop after its two hand-
23
+ // written services, and auth's context comes last.
24
+ "shop.cart",
25
+ "payments.ledger",
26
+ "delivery.core",
27
+ "auth.auth",
28
+ ]);
29
+ expect(sample.events.map((e) => e.name)).toContain("PaymentCaptured");
30
+ // One node per event, whatever the consumer count: PaymentCaptured has two
31
+ // consumers and is still one pill.
32
+ const captured = sample.events.filter((e) => e.name === "PaymentCaptured");
33
+ expect(captured).toHaveLength(1);
34
+ expect(captured[0]?.consumers).toHaveLength(2);
35
+ });
36
+
37
+ it("keeps consumers with no service of their own as ghosts", () => {
38
+ expect(sample.services.filter((s) => s.ghost).map((s) => s.id)).toEqual([
39
+ "analytics-sink",
40
+ ]);
41
+ });
42
+
43
+ it("tints an event by its publisher's context", () => {
44
+ const captured = sample.events.find((e) => e.name === "PaymentCaptured");
45
+ expect(captured?.publisher).toBe("payments.ledger");
46
+ expect(captured?.context).toBe("payments");
47
+ });
48
+
49
+ it("counts what each service publishes and consumes", () => {
50
+ const ledger = sample.services.find((s) => s.id === "payments.ledger");
51
+ // PaymentAuthorized, PaymentCaptured, PaymentDeclined, RefundIssued.
52
+ expect(ledger?.publishes).toBe(4);
53
+ // OrderPlaced and OrderCancelled.
54
+ expect(ledger?.consumes).toBe(2);
55
+ });
56
+
57
+ it("carries the consumer the traces showed: auth hears its own PasswordChanged", () => {
58
+ // The code declares it only by implication - the revoke-sessions flow
59
+ // opens with the event arriving at auth.auth - and the recording in
60
+ // examples/auth/telemetry shows it happening, which is what makes the
61
+ // edge verified rather than derived.
62
+ const auth = sample.services.find((s) => s.id === "auth.auth");
63
+ expect(auth?.consumes).toBe(1);
64
+ const changed = sample.events.find((e) => e.name === "PasswordChanged");
65
+ expect(changed?.consumers).toMatchObject([
66
+ { service: "auth.auth", status: "verified", self: true },
67
+ ]);
68
+ });
69
+
70
+ it("marks self-consumption on the event rather than as an edge", () => {
71
+ const selfConsumed = eventGraph({
72
+ ...catalog,
73
+ contexts: catalog.contexts.map((c) => ({
74
+ ...c,
75
+ services: c.services.map((s) => ({
76
+ ...s,
77
+ aggregates: s.aggregates.map((a) => ({
78
+ ...a,
79
+ events: a.events.map((e) => ({
80
+ ...e,
81
+ consumers: [{ service: s.id, status: "verified" as const }],
82
+ })),
83
+ })),
84
+ })),
85
+ })),
86
+ });
87
+ expect(selfConsumed.events.every((e) => e.consumers[0]?.self)).toBe(true);
88
+ // Never a loop: a self consumption is not a bundle either.
89
+ expect(bundles(selfConsumed)).toEqual([]);
90
+ });
91
+ });
92
+
93
+ describe("filterEventGraph", () => {
94
+ it("returns the graph untouched when no chip is on", () => {
95
+ expect(
96
+ filterEventGraph(sample, { contexts: new Set(), statuses: new Set() }),
97
+ ).toBe(sample);
98
+ });
99
+
100
+ it("filters services by context and keeps events with one end left", () => {
101
+ const only = filterEventGraph(sample, {
102
+ contexts: new Set(["delivery"]),
103
+ statuses: new Set(),
104
+ });
105
+ expect(only.services.map((s) => s.id)).toContain("delivery.core");
106
+ expect(only.services.map((s) => s.id)).not.toContain("shop.oms");
107
+ // Everything delivery.core emits survives, and so does everything it is
108
+ // named as a consumer of - the pills that reach it from outside.
109
+ const names = only.events.map((e) => e.name);
110
+ expect(names).toContain("ShipmentDispatched");
111
+ expect(names).toContain("PaymentCaptured");
112
+ // But not traffic it has nothing to do with.
113
+ expect(names).not.toContain("QuoteIssued");
114
+ });
115
+
116
+ it("keeps a ghost alive as long as an event still names it", () => {
117
+ const only = filterEventGraph(sample, {
118
+ contexts: new Set(["delivery"]),
119
+ statuses: new Set(),
120
+ });
121
+ expect(only.services.map((s) => s.id)).toContain("analytics-sink");
122
+ });
123
+
124
+ it("hides edges of an unwanted status", () => {
125
+ const verified = filterEventGraph(sample, {
126
+ contexts: new Set(),
127
+ statuses: new Set(["verified"] as const),
128
+ });
129
+ const statuses = verified.events.flatMap((e) =>
130
+ e.consumers.map((c) => c.status),
131
+ );
132
+ expect(new Set(statuses)).toEqual(new Set(["verified"]));
133
+ });
134
+
135
+ it("hides an event whose every consumption was filtered out", () => {
136
+ const verified = filterEventGraph(sample, {
137
+ contexts: new Set(),
138
+ statuses: new Set(["verified"] as const),
139
+ });
140
+ // QuoteExpired is consumed once, and only as "declared".
141
+ expect(verified.events.map((e) => e.name)).not.toContain("QuoteExpired");
142
+ });
143
+
144
+ it("keeps an event that never had a consumer to filter", () => {
145
+ const thin = eventGraph(thinCatalog());
146
+ const verified = filterEventGraph(thin, {
147
+ contexts: new Set(),
148
+ statuses: new Set(["verified"] as const),
149
+ });
150
+ expect(verified.events).toHaveLength(3);
151
+ });
152
+
153
+ it("reports how many edges a filter is hiding", () => {
154
+ const before = edgeCount(sample);
155
+ const after = edgeCount(
156
+ filterEventGraph(sample, {
157
+ contexts: new Set(["delivery"]),
158
+ statuses: new Set(),
159
+ }),
160
+ );
161
+ expect(after).toBeLessThan(before);
162
+ expect(before - after).toBeGreaterThan(0);
163
+ });
164
+ });
165
+
166
+ describe("bundles", () => {
167
+ it("collapses every event of a pair into one edge with a count", () => {
168
+ const pair = bundles(sample).find(
169
+ (b) => b.from === "payments.ledger" && b.to === "delivery.core",
170
+ );
171
+ expect(pair?.events.map((e) => e.name).sort()).toEqual([
172
+ "PaymentAuthorized",
173
+ "PaymentCaptured",
174
+ "RefundIssued",
175
+ ]);
176
+ // Worst wins: two of the three are only declared.
177
+ expect(pair?.status).toBe("declared");
178
+ });
179
+
180
+ it("draws one edge per ordered pair, never one per event", () => {
181
+ const all = bundles(sample);
182
+ const pairs = new Set(all.map((b) => `${b.from}->${b.to}`));
183
+ expect(pairs.size).toBe(all.length);
184
+ });
185
+
186
+ it("flags a pair that publishes both ways so the canvas can offset it", () => {
187
+ const there = bundles(sample).find(
188
+ (b) => b.from === "shop.oms" && b.to === "payments.ledger",
189
+ );
190
+ const back = bundles(sample).find(
191
+ (b) => b.from === "payments.ledger" && b.to === "shop.oms",
192
+ );
193
+ expect(there?.back).toBe(true);
194
+ expect(back?.back).toBe(true);
195
+ });
196
+
197
+ it("round-trips its id", () => {
198
+ const id = bundleId("a.b", "c.d");
199
+ expect(parseBundleId(id)).toEqual({ from: "a.b", to: "c.d" });
200
+ expect(parseBundleId("shop.oms")).toBeNull();
201
+ expect(bundleById(sample, id)).toBeNull();
202
+ expect(
203
+ bundleById(sample, bundleId("payments.ledger", "delivery.core"))?.events,
204
+ ).toHaveLength(3);
205
+ });
206
+ });
207
+
208
+ describe("the thin estate", () => {
209
+ const thin = eventGraph(thinCatalog());
210
+
211
+ it("is one service and three events with nothing listening", () => {
212
+ expect(thin.services).toHaveLength(1);
213
+ expect(thin.events).toHaveLength(3);
214
+ expect(thin.events.every((e) => e.consumers.length === 0)).toBe(true);
215
+ });
216
+
217
+ it("still draws a line from the service to each of its events", () => {
218
+ expect(edgeCount(thin)).toBe(3);
219
+ });
220
+
221
+ it("has no bundle at all, because nothing reaches anything", () => {
222
+ expect(bundles(thin)).toEqual([]);
223
+ });
224
+ });
225
+
226
+ describe("the wide estate", () => {
227
+ const wide = eventGraph(wideCatalog());
228
+
229
+ it("is forty services and eighty events", () => {
230
+ expect(wide.services.filter((s) => !s.ghost)).toHaveLength(40);
231
+ expect(wide.events).toHaveLength(80);
232
+ });
233
+
234
+ it("still has ghosts in it", () => {
235
+ expect(wide.services.filter((s) => s.ghost).length).toBeGreaterThan(0);
236
+ });
237
+ });
@@ -0,0 +1,327 @@
1
+ // The dependency graph's model: services AND events are nodes.
2
+ //
3
+ // The old shape drew one labelled arrow per (event, consumer) pair, so an
4
+ // event with four consumers was four lines carrying the same word four times.
5
+ // The word was the densest thing on the canvas and it was also the most
6
+ // duplicated. Promoting the event to a node fixes both at once: it is written
7
+ // once, it is a thing a reader can click, and every line around it can then be
8
+ // bare - the node IS the label.
9
+ //
10
+ // Two derivations live here. `eventGraph` is the bipartite one; `bundles`
11
+ // collapses it back to services only, one edge per ordered pair with a count.
12
+ // Both are pure, so the acceptance criteria are assertions rather than
13
+ // eyeballing.
14
+
15
+ import type { Catalog, Status } from "../catalog";
16
+
17
+ /** Worst wins: a bundle is known no better than its least-known event. */
18
+ export function worstStatus(statuses: readonly Status[]): Status {
19
+ if (statuses.includes("unresolved")) return "unresolved";
20
+ if (statuses.includes("declared")) return "declared";
21
+ return "verified";
22
+ }
23
+
24
+ export interface GraphService {
25
+ id: string;
26
+ label: string;
27
+ context: string | null;
28
+ /** named as a consumer but absent from the catalog */
29
+ ghost: boolean;
30
+ /** events it publishes */
31
+ publishes: number;
32
+ /** distinct events it consumes, its own included */
33
+ consumes: number;
34
+ }
35
+
36
+ export interface EventConsumer {
37
+ service: string;
38
+ status: Status;
39
+ /**
40
+ * The consumer is the publisher. It is held here rather than dropped so the
41
+ * status filter treats it like any other consumption; the canvas renders it
42
+ * as a chip on the pill, never as a loop edge.
43
+ */
44
+ self: boolean;
45
+ }
46
+
47
+ export interface GraphEvent {
48
+ id: string;
49
+ name: string;
50
+ /** service id; the pill is tinted by this service's context */
51
+ publisher: string;
52
+ context: string | null;
53
+ consumers: EventConsumer[];
54
+ }
55
+
56
+ export interface EventGraph {
57
+ services: GraphService[];
58
+ events: GraphEvent[];
59
+ }
60
+
61
+ /** Services in catalog order, events grouped by publisher in the same order. */
62
+ export function eventGraph(catalog: Catalog): EventGraph {
63
+ const services = new Map<string, GraphService>();
64
+ const events: GraphEvent[] = [];
65
+
66
+ const blank = (id: string, context: string | null, ghost: boolean) => ({
67
+ id,
68
+ label: id,
69
+ context,
70
+ ghost,
71
+ publishes: 0,
72
+ consumes: 0,
73
+ });
74
+
75
+ for (const context of catalog.contexts) {
76
+ for (const service of context.services) {
77
+ services.set(service.id, blank(service.id, context.id, false));
78
+ }
79
+ }
80
+
81
+ for (const context of catalog.contexts) {
82
+ for (const service of context.services) {
83
+ for (const aggregate of service.aggregates) {
84
+ for (const event of aggregate.events) {
85
+ const consumers: EventConsumer[] = event.consumers.map((c) => ({
86
+ service: c.service,
87
+ status: c.status,
88
+ self: c.service === service.id,
89
+ }));
90
+ for (const consumer of consumers) {
91
+ // A consumer with no service of its own is still a real
92
+ // dependency. Drawing it as a ghost keeps the gap visible instead
93
+ // of hiding it.
94
+ if (!services.has(consumer.service)) {
95
+ services.set(consumer.service, blank(consumer.service, null, true));
96
+ }
97
+ }
98
+ events.push({
99
+ id: event.id,
100
+ name: event.name,
101
+ publisher: service.id,
102
+ context: context.id,
103
+ consumers,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ return { services: [...services.values()], events: counted(events, services) };
111
+ }
112
+
113
+ /** Fills in each service's two footer numbers from the events around it. */
114
+ function counted(
115
+ events: GraphEvent[],
116
+ services: Map<string, GraphService>,
117
+ ): GraphEvent[] {
118
+ for (const event of events) {
119
+ const publisher = services.get(event.publisher);
120
+ if (publisher) publisher.publishes += 1;
121
+ // Distinct, because one event consumed twice by the same service is still
122
+ // one thing that service listens to.
123
+ const seen = new Set<string>();
124
+ for (const consumer of event.consumers) {
125
+ if (seen.has(consumer.service)) continue;
126
+ seen.add(consumer.service);
127
+ const service = services.get(consumer.service);
128
+ if (service) service.consumes += 1;
129
+ }
130
+ }
131
+ return events;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Filters
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export interface GraphFilter {
139
+ /** Active context chips. Empty means every context. */
140
+ contexts: ReadonlySet<string>;
141
+ /** Active status chips. Empty means every status. */
142
+ statuses: ReadonlySet<Status>;
143
+ }
144
+
145
+ export const NO_FILTER: GraphFilter = {
146
+ contexts: new Set(),
147
+ statuses: new Set(),
148
+ };
149
+
150
+ /**
151
+ * How many lines the canvas would draw for a graph: one per event that has a
152
+ * publisher to hang off, plus one per consumption that is not the publisher
153
+ * listening to itself.
154
+ *
155
+ * The header subtracts a filtered count from an unfiltered one to say how many
156
+ * edges a chip is hiding, so "edge" has to mean exactly one drawn line.
157
+ */
158
+ export function edgeCount(graph: EventGraph): number {
159
+ const present = new Set(graph.services.map((s) => s.id));
160
+ let n = 0;
161
+ for (const event of graph.events) {
162
+ if (present.has(event.publisher)) n += 1;
163
+ for (const consumer of event.consumers) {
164
+ if (!consumer.self && present.has(consumer.service)) n += 1;
165
+ }
166
+ }
167
+ return n;
168
+ }
169
+
170
+ /**
171
+ * The graph a set of chips leaves standing.
172
+ *
173
+ * Contexts filter SERVICES, and only services. An event survives if either end
174
+ * of it does, so filtering to one context still shows what that context emits
175
+ * into the dark and what reaches it from outside - the pill keeps its
176
+ * publisher's colour, which is what says the traffic comes from somewhere no
177
+ * longer on screen.
178
+ *
179
+ * Statuses filter CONSUMPTIONS, and only consumptions. An event that had
180
+ * consumers and has none left is hidden: an empty pill under a "verified only"
181
+ * filter would read as an event nobody consumes, which is a different fact and
182
+ * a false one. An event that never had consumers is not hidden by a status
183
+ * filter, because it never held a status to disagree with.
184
+ *
185
+ * A ghost is neither: it has no context to be filtered by and exists only
186
+ * because an event named it, so it lives and dies with the events that do.
187
+ */
188
+ export function filterEventGraph(
189
+ graph: EventGraph,
190
+ filter: GraphFilter,
191
+ ): EventGraph {
192
+ const byContext = filter.contexts.size > 0;
193
+ const byStatus = filter.statuses.size > 0;
194
+ if (!byContext && !byStatus) return graph;
195
+
196
+ const ghosts = new Set(
197
+ graph.services.filter((s) => s.ghost).map((s) => s.id),
198
+ );
199
+ const kept = new Set(
200
+ graph.services
201
+ .filter(
202
+ (s) =>
203
+ !s.ghost &&
204
+ (!byContext ||
205
+ (s.context !== null && filter.contexts.has(s.context))),
206
+ )
207
+ .map((s) => s.id),
208
+ );
209
+
210
+ const passes = (c: EventConsumer): boolean =>
211
+ !byStatus || filter.statuses.has(c.status);
212
+
213
+ const events: GraphEvent[] = [];
214
+ const liveGhosts = new Set<string>();
215
+ for (const event of graph.events) {
216
+ const consumers = event.consumers.filter(passes);
217
+ if (byStatus && event.consumers.length > 0 && consumers.length === 0) {
218
+ continue;
219
+ }
220
+ // Only a real service can justify an event: a ghost is the far end of a
221
+ // dependency, never a reason to draw one.
222
+ const anchored =
223
+ kept.has(event.publisher) ||
224
+ consumers.some((c) => kept.has(c.service));
225
+ if (!anchored) continue;
226
+
227
+ const surviving = consumers.filter(
228
+ (c) => kept.has(c.service) || ghosts.has(c.service),
229
+ );
230
+ for (const consumer of surviving) {
231
+ if (ghosts.has(consumer.service)) liveGhosts.add(consumer.service);
232
+ }
233
+ events.push({ ...event, consumers: surviving });
234
+ }
235
+
236
+ return {
237
+ services: graph.services.filter(
238
+ (s) => kept.has(s.id) || liveGhosts.has(s.id),
239
+ ),
240
+ events,
241
+ };
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Compact mode: services only, one bundled edge per ordered pair.
246
+ // ---------------------------------------------------------------------------
247
+
248
+ export interface BundleEvent {
249
+ id: string;
250
+ name: string;
251
+ status: Status;
252
+ }
253
+
254
+ export interface Bundle {
255
+ /** "bundle:<from>><to>" - a selection id, so the rail can resolve it */
256
+ id: string;
257
+ from: string;
258
+ to: string;
259
+ events: BundleEvent[];
260
+ /** worst status among the events it carries */
261
+ status: Status;
262
+ /** the reverse pair also exists, so the canvas has to offset both */
263
+ back: boolean;
264
+ }
265
+
266
+ export function bundleId(from: string, to: string): string {
267
+ return `bundle:${from}>${to}`;
268
+ }
269
+
270
+ export function parseBundleId(id: string): { from: string; to: string } | null {
271
+ if (!id.startsWith("bundle:")) return null;
272
+ const body = id.slice(7);
273
+ const at = body.indexOf(">");
274
+ if (at <= 0 || at === body.length - 1) return null;
275
+ return { from: body.slice(0, at), to: body.slice(at + 1) };
276
+ }
277
+
278
+ /**
279
+ * One entry per ordered pair, in the order the pairs first appear.
280
+ *
281
+ * Self-consumption never becomes a bundle: an arrow from a box back to itself
282
+ * is a loop, and a loop is the one shape a layered router cannot place without
283
+ * drawing a circle around the node it belongs to.
284
+ */
285
+ export function bundles(graph: EventGraph): Bundle[] {
286
+ const byPair = new Map<string, Bundle>();
287
+ const present = new Set(graph.services.map((s) => s.id));
288
+
289
+ for (const event of graph.events) {
290
+ if (!present.has(event.publisher)) continue;
291
+ for (const consumer of event.consumers) {
292
+ if (consumer.self || !present.has(consumer.service)) continue;
293
+ const id = bundleId(event.publisher, consumer.service);
294
+ const found = byPair.get(id);
295
+ const carried: BundleEvent = {
296
+ id: event.id,
297
+ name: event.name,
298
+ status: consumer.status,
299
+ };
300
+ if (found) {
301
+ found.events.push(carried);
302
+ found.status = worstStatus(found.events.map((e) => e.status));
303
+ } else {
304
+ byPair.set(id, {
305
+ id,
306
+ from: event.publisher,
307
+ to: consumer.service,
308
+ events: [carried],
309
+ status: consumer.status,
310
+ back: false,
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ for (const bundle of byPair.values()) {
317
+ bundle.back = byPair.has(bundleId(bundle.to, bundle.from));
318
+ }
319
+ return [...byPair.values()];
320
+ }
321
+
322
+ /** The bundle a selection id names, or null. Used by the detail rail. */
323
+ export function bundleById(graph: EventGraph, id: string): Bundle | null {
324
+ const parsed = parseBundleId(id);
325
+ if (!parsed) return null;
326
+ return bundles(graph).find((b) => b.id === id) ?? null;
327
+ }