@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,1407 @@
1
+ // The right rail. One panel, one selection, every page that draws a diagram.
2
+ //
3
+ // It renders by kind rather than by page, so an event opened from a flow reads
4
+ // exactly the same as an event opened from the dependency graph. Nothing here
5
+ // navigates on its own: every route change is a link the reader chose.
6
+
7
+ import { Link } from "react-router";
8
+ import { X } from "lucide-react";
9
+ import { useEffect, type ReactNode } from "react";
10
+ import {
11
+ Panel,
12
+ ResizeHandle,
13
+ SavedGroup,
14
+ useCanvasResize,
15
+ usePanelRef,
16
+ } from "../app/panels";
17
+ import { SidePanel } from "../components/Overlay";
18
+ import { useNarrow } from "../app/responsive";
19
+ import { useUiStore } from "../app/ui-store";
20
+ import { catalog, index } from "../data";
21
+ import {
22
+ blockFields,
23
+ columnId,
24
+ columnNameOfId,
25
+ mapsBlockId,
26
+ mapsFieldPath,
27
+ relationOfColumnId,
28
+ storeViews,
29
+ viewReads,
30
+ } from "../catalog";
31
+ import { flowsForService, usesOfDef } from "../lib/derive";
32
+ import { KIND_LABEL } from "../lib/kinds";
33
+ import { eventScope, resolveShape } from "../lib/shape";
34
+ import { stepsInto } from "../lib/backlinks";
35
+ import { walkSteps } from "../catalog";
36
+ import { methodCount } from "../lib/api";
37
+ import {
38
+ consumersOf,
39
+ countsOf,
40
+ dependenciesOf,
41
+ registryUrl,
42
+ } from "../lib/registry";
43
+ import { typesDisagree } from "../lib/data-model";
44
+ import { STORE_KIND_LABEL, StoreKindMark } from "../er/StoreHeader";
45
+ import { upstreamOf } from "../er/lineage";
46
+ import type { LineageMaps } from "../er/lineage";
47
+ import { ctxStyle } from "../lib/context-color";
48
+ import { Ident } from "../components/Ident";
49
+ import { StatusChip } from "../components/primitives";
50
+ import { StepDetailBody } from "../flow/StepDetail";
51
+ import {
52
+ AGGREGATE_ANCHOR,
53
+ EVENT_ANCHOR,
54
+ SERVICE_ANCHOR,
55
+ eventPath as eventPathOf,
56
+ blockPath,
57
+ modulePath,
58
+ paths,
59
+ } from "../routes";
60
+ import { PinButton } from "../app/pins";
61
+ import type { PinKind } from "../lib/pins";
62
+ import type { Resolved, Selection } from "./model";
63
+ import { resolveSelection } from "./model";
64
+ import { selectionPath } from "./pages";
65
+ import { useSelectionStore } from "./store";
66
+
67
+ /** The catalog's lineage graph, read whenever a column is open. */
68
+ const LINEAGE: LineageMaps = {
69
+ from: index.lineageFrom,
70
+ into: index.lineageInto,
71
+ };
72
+
73
+ function Label({ children }: { children: ReactNode }) {
74
+ return <div className="label mt-3 mb-1">{children}</div>;
75
+ }
76
+
77
+ function Row({ children }: { children: ReactNode }) {
78
+ return <div className="flex items-center gap-2 py-0.5">{children}</div>;
79
+ }
80
+
81
+ /** The panel's own way of moving the selection, without leaving the page. */
82
+ function SelectLink({ id, children }: { id: string; children: ReactNode }) {
83
+ const select = useSelectionStore((s) => s.select);
84
+ return (
85
+ <button
86
+ type="button"
87
+ onClick={() => select(id, "panel")}
88
+ title={id}
89
+ data-peek={id}
90
+ className="mono trunc rounded-control text-left text-accent hover:underline"
91
+ >
92
+ {children}
93
+ </button>
94
+ );
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Bodies
99
+ // ---------------------------------------------------------------------------
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Persistence bodies. A store, a table and a column are three zoom levels on
103
+ // one question — where does this live — so each one names the level above it
104
+ // and lists the level below.
105
+ // ---------------------------------------------------------------------------
106
+
107
+ function StoreBody({
108
+ resolved,
109
+ }: {
110
+ resolved: Extract<Resolved, { kind: "store" }>;
111
+ }) {
112
+ const { store, service } = resolved;
113
+ return (
114
+ <>
115
+ <Label>Kind</Label>
116
+ <div className="mono flex items-center gap-1.5 text-muted">
117
+ <StoreKindMark kind={store.kind} />
118
+ {STORE_KIND_LABEL[store.kind]}
119
+ </div>
120
+
121
+ <Label>Owner</Label>
122
+ <SelectLink id={service.id}>{service.id}</SelectLink>
123
+
124
+ <Label>Tables</Label>
125
+ {store.tables.length === 0 && (store.keyspaces ?? []).length === 0 ? (
126
+ <div className="mono text-muted">no schema extracted</div>
127
+ ) : null}
128
+ {store.tables.map((table) => (
129
+ <Row key={table.id}>
130
+ <SelectLink id={table.id}>{table.name}</SelectLink>
131
+ <span className="mono ml-auto shrink-0 text-muted">
132
+ {table.columns.length}
133
+ </span>
134
+ </Row>
135
+ ))}
136
+
137
+ {(store.keyspaces ?? []).length > 0 ? (
138
+ <>
139
+ <Label>Key patterns</Label>
140
+ {(store.keyspaces ?? []).map((keyspace) => (
141
+ <Row key={keyspace.pattern}>
142
+ <span className="mono min-w-0 break-all">{keyspace.pattern}</span>
143
+ <span className="mono ml-auto shrink-0 text-muted">
144
+ {keyspace.operations.join("/")}
145
+ </span>
146
+ </Row>
147
+ ))}
148
+ </>
149
+ ) : null}
150
+
151
+ {storeViews(store).length > 0 ? (
152
+ <>
153
+ <Label>Views</Label>
154
+ {storeViews(store).map((view) => (
155
+ <Row key={view.id}>
156
+ <SelectLink id={view.id}>{view.name}</SelectLink>
157
+ <span className="mono ml-auto shrink-0 text-muted">
158
+ {view.materialized ? "matview" : "view"}
159
+ </span>
160
+ </Row>
161
+ ))}
162
+ </>
163
+ ) : null}
164
+
165
+ {store.source ? (
166
+ <>
167
+ <Label>Source</Label>
168
+ <Ident block value={store.source} className="text-muted" />
169
+ </>
170
+ ) : null}
171
+ </>
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Where a value goes and where it came from, as two lists of column links.
177
+ *
178
+ * Written once and used from both the table body and the column body, because
179
+ * "who reads this" is the same question at either zoom level and a reader who
180
+ * has learned to read it on a column should not have to learn it again.
181
+ */
182
+ function LineageRows({ ids }: { ids: readonly string[] }) {
183
+ return (
184
+ <>
185
+ {ids.map((id) => (
186
+ <Row key={id}>
187
+ <SelectLink id={id}>
188
+ {relationOfColumnId(id).split(".").at(-1)}.{columnNameOfId(id)}
189
+ </SelectLink>
190
+ </Row>
191
+ ))}
192
+ </>
193
+ );
194
+ }
195
+
196
+ function ViewBody({
197
+ resolved,
198
+ }: {
199
+ resolved: Extract<Resolved, { kind: "view" }>;
200
+ }) {
201
+ const { view, store } = resolved;
202
+ const aggregateId = view.persists?.aggregate;
203
+ const reads = viewReads(view);
204
+ // Everything computed from this view's columns, wherever it lives: a view
205
+ // read by another view is the case the canvas cannot draw in one hop.
206
+ const feeds = [
207
+ ...new Set(
208
+ view.columns.flatMap(
209
+ (c) => index.lineageInto.get(columnId(view.id, c.name)) ?? [],
210
+ ),
211
+ ),
212
+ ];
213
+
214
+ return (
215
+ <>
216
+ {view.doc ? <p className="mt-2 text-muted">{view.doc}</p> : null}
217
+
218
+ <Label>Store</Label>
219
+ <SelectLink id={store.id}>{store.id}</SelectLink>
220
+
221
+ <Label>Kind</Label>
222
+ <div className="mono text-muted">
223
+ {view.materialized
224
+ ? "materialized view — the rows are kept, and can be stale"
225
+ : "view — the rows are computed on every read"}
226
+ </div>
227
+
228
+ {aggregateId ? (
229
+ <>
230
+ <Label>Presents</Label>
231
+ <SelectLink id={aggregateId}>{aggregateId}</SelectLink>
232
+ </>
233
+ ) : null}
234
+
235
+ <Label>Reads</Label>
236
+ {reads.length === 0 ? (
237
+ <div className="mono text-muted">
238
+ nothing says what this view is computed from
239
+ </div>
240
+ ) : null}
241
+ {reads.map((id) => (
242
+ <Row key={id}>
243
+ <SelectLink id={id}>{id.split(".").at(-1)}</SelectLink>
244
+ <span className="mono ml-auto shrink-0 text-muted">{id}</span>
245
+ </Row>
246
+ ))}
247
+
248
+ <Label>Columns</Label>
249
+ <table className="w-full">
250
+ <tbody>
251
+ {view.columns.map((column) => (
252
+ <tr key={column.name} className="align-top">
253
+ <td className="mono py-0.5 pr-2 whitespace-nowrap">
254
+ <SelectLink id={columnId(view.id, column.name)}>
255
+ {column.name}
256
+ </SelectLink>
257
+ </td>
258
+ <td className="mono py-0.5 pr-2 text-muted">
259
+ {column.type}
260
+ {column.nullable ? "?" : ""}
261
+ </td>
262
+ <td className="mono py-0.5 text-muted">
263
+ {(column.from ?? []).length > 0 ? (
264
+ <span className="trunc" title={column.from?.join("\n")}>
265
+ ← {columnNameOfId(column.from?.[0] ?? "")}
266
+ {(column.from?.length ?? 0) > 1
267
+ ? ` +${(column.from?.length ?? 1) - 1}`
268
+ : ""}
269
+ </span>
270
+ ) : null}
271
+ </td>
272
+ </tr>
273
+ ))}
274
+ </tbody>
275
+ </table>
276
+
277
+ {feeds.length > 0 ? (
278
+ <>
279
+ <Label>Feeds</Label>
280
+ <LineageRows ids={feeds} />
281
+ </>
282
+ ) : null}
283
+
284
+ {view.definition ? (
285
+ <>
286
+ <Label>Definition</Label>
287
+ <pre className="mono overflow-x-auto rounded-card border p-2 border-line bg-surface text-muted">
288
+ {view.definition}
289
+ </pre>
290
+ </>
291
+ ) : null}
292
+
293
+ {view.source ? (
294
+ <>
295
+ <Label>Source</Label>
296
+ <Ident block value={view.source} className="text-muted" />
297
+ </>
298
+ ) : null}
299
+ </>
300
+ );
301
+ }
302
+
303
+ function TableBody({
304
+ resolved,
305
+ }: {
306
+ resolved: Extract<Resolved, { kind: "table" }>;
307
+ }) {
308
+ const { table, store } = resolved;
309
+ const aggregateId = table.persists?.aggregate;
310
+ const into = index.fkIntoTable.get(table.id) ?? [];
311
+ const readers = index.viewsReading.get(table.id) ?? [];
312
+ const feeds = [
313
+ ...new Set(
314
+ table.columns.flatMap(
315
+ (c) => index.lineageInto.get(columnId(table.id, c.name)) ?? [],
316
+ ),
317
+ ),
318
+ ];
319
+
320
+ return (
321
+ <>
322
+ {table.doc ? <p className="mt-2 text-muted">{table.doc}</p> : null}
323
+
324
+ <Label>Store</Label>
325
+ <SelectLink id={store.id}>{store.id}</SelectLink>
326
+
327
+ {table.role ? (
328
+ <>
329
+ <Label>Role</Label>
330
+ <div className="mono text-muted">{table.role}</div>
331
+ </>
332
+ ) : null}
333
+
334
+ {aggregateId ? (
335
+ <>
336
+ <Label>Persists</Label>
337
+ <SelectLink id={aggregateId}>{aggregateId}</SelectLink>
338
+ </>
339
+ ) : null}
340
+
341
+ <Label>Columns</Label>
342
+ <table className="w-full">
343
+ <tbody>
344
+ {table.columns.map((column) => {
345
+ // A column carrying a domain field links to the block that
346
+ // declares it: that is the whole point of the `maps` metadata.
347
+ const blockId = mapsBlockId(
348
+ aggregateId ? index.aggregateById.get(aggregateId) : undefined,
349
+ column.maps,
350
+ );
351
+ const to = blockId ? blockPath(blockId) : null;
352
+ return (
353
+ <tr key={column.name} className="align-top">
354
+ <td className="mono py-0.5 pr-2 whitespace-nowrap">
355
+ <SelectLink id={columnId(table.id, column.name)}>
356
+ {column.pk ? "· " : ""}
357
+ {column.name}
358
+ </SelectLink>
359
+ </td>
360
+ <td className="mono py-0.5 pr-2 text-muted">
361
+ {column.type}
362
+ {column.nullable ? "?" : ""}
363
+ </td>
364
+ <td className="mono py-0.5 text-muted">
365
+ {column.maps ? (
366
+ to ? (
367
+ <Link
368
+ to={to}
369
+ className="trunc text-accent hover:underline"
370
+ >
371
+ {column.maps}
372
+ </Link>
373
+ ) : (
374
+ column.maps
375
+ )
376
+ ) : null}
377
+ </td>
378
+ </tr>
379
+ );
380
+ })}
381
+ </tbody>
382
+ </table>
383
+
384
+ {(table.indexes ?? []).length > 0 ? (
385
+ <>
386
+ <Label>Indexes</Label>
387
+ {(table.indexes ?? []).map((ix) => (
388
+ <Row key={ix.name}>
389
+ <span className="mono trunc" title={ix.name}>
390
+ {ix.columns.join(", ")}
391
+ </span>
392
+ {ix.unique ? (
393
+ <span className="chip ml-auto shrink-0">unique</span>
394
+ ) : null}
395
+ </Row>
396
+ ))}
397
+ </>
398
+ ) : null}
399
+
400
+ <Label>Referenced by</Label>
401
+ {into.length === 0 ? (
402
+ <div className="mono text-muted">nothing points at this table</div>
403
+ ) : null}
404
+ {into.map((owner) => (
405
+ <Row key={`${owner.table.id}.${owner.column.name}`}>
406
+ <SelectLink id={columnId(owner.table.id, owner.column.name)}>
407
+ {owner.table.name}.{owner.column.name}
408
+ </SelectLink>
409
+ </Row>
410
+ ))}
411
+
412
+ {/* A view over this table is not a reference — nothing constrains
413
+ anything — but it is the same worry in a different shape: rename a
414
+ column here and the view breaks with no error until it is read. */}
415
+ {readers.length > 0 ? (
416
+ <>
417
+ <Label>Read by</Label>
418
+ {readers.map((view) => (
419
+ <Row key={view.id}>
420
+ <SelectLink id={view.id}>{view.name}</SelectLink>
421
+ <span className="mono ml-auto shrink-0 text-muted">
422
+ {view.materialized ? "matview" : "view"}
423
+ </span>
424
+ </Row>
425
+ ))}
426
+ </>
427
+ ) : null}
428
+
429
+ {feeds.length > 0 ? (
430
+ <>
431
+ <Label>Feeds</Label>
432
+ <LineageRows ids={feeds} />
433
+ </>
434
+ ) : null}
435
+ </>
436
+ );
437
+ }
438
+
439
+ function ColumnBody({
440
+ resolved,
441
+ }: {
442
+ resolved: Extract<Resolved, { kind: "column" }>;
443
+ }) {
444
+ const { column, table, view } = resolved;
445
+ const aggregateId = (table ?? view)?.persists?.aggregate;
446
+ const aggregate = aggregateId
447
+ ? index.aggregateById.get(aggregateId)
448
+ : undefined;
449
+ const from = index.lineageFrom.get(resolved.id) ?? [];
450
+ const into = index.lineageInto.get(resolved.id) ?? [];
451
+ // The far ends of the chain, not the next hop: a column three copies
452
+ // downstream of the truth is what a reader is trying to find out about.
453
+ const origins = [...upstreamOf(LINEAGE, resolved.id)].filter(
454
+ (id) => (index.lineageFrom.get(id)?.length ?? 0) === 0,
455
+ );
456
+ const blockId = mapsBlockId(aggregate, column.maps);
457
+ const block = blockId ? index.blockById.get(blockId) : undefined;
458
+ const to = blockId ? blockPath(blockId) : null;
459
+ const field = block
460
+ ? blockFields(catalog, block.block).find(
461
+ (f) =>
462
+ f.name === (mapsFieldPath(column.maps ?? "").split(".")[0] ?? ""),
463
+ )
464
+ : undefined;
465
+
466
+ return (
467
+ <>
468
+ {column.doc ? <p className="mt-2 text-muted">{column.doc}</p> : null}
469
+
470
+ <Label>{view ? "View" : "Table"}</Label>
471
+ {view ? (
472
+ <SelectLink id={view.id}>{view.name}</SelectLink>
473
+ ) : table ? (
474
+ <SelectLink id={table.id}>{table.name}</SelectLink>
475
+ ) : null}
476
+
477
+ <Label>Type</Label>
478
+ <div className="mono text-muted">
479
+ {column.type}
480
+ {column.nullable ? " · nullable" : " · not null"}
481
+ {column.pk ? " · primary key" : ""}
482
+ </div>
483
+
484
+ {column.fk ? (
485
+ <>
486
+ <Label>References</Label>
487
+ <SelectLink id={column.fk.table}>
488
+ {column.fk.table}.{column.fk.column}
489
+ </SelectLink>
490
+ {column.fk.onDelete ? (
491
+ <div className="mono mt-1 text-muted">
492
+ on delete {column.fk.onDelete}
493
+ </div>
494
+ ) : null}
495
+ </>
496
+ ) : null}
497
+
498
+ {from.length > 0 ? (
499
+ <>
500
+ <Label>Computed from</Label>
501
+ <LineageRows ids={from} />
502
+ {/* Only worth saying when the chain is longer than one hop: with a
503
+ single source, the origin IS the source and printing it twice
504
+ says nothing. */}
505
+ {origins.length > 0 && !origins.every((id) => from.includes(id)) ? (
506
+ <>
507
+ <div className="mono mt-2 text-muted">originally</div>
508
+ <LineageRows ids={origins} />
509
+ </>
510
+ ) : null}
511
+ </>
512
+ ) : null}
513
+
514
+ {into.length > 0 ? (
515
+ <>
516
+ <Label>Feeds</Label>
517
+ <LineageRows ids={into} />
518
+ </>
519
+ ) : null}
520
+
521
+ {column.maps ? (
522
+ <>
523
+ <Label>Carries</Label>
524
+ {to ? (
525
+ <Link
526
+ to={to}
527
+ className="mono rounded-control text-accent hover:underline"
528
+ >
529
+ {column.maps}
530
+ </Link>
531
+ ) : (
532
+ <div className="mono text-muted">{column.maps}</div>
533
+ )}
534
+ {field ? (
535
+ <div className="mono mt-1 text-muted">
536
+ {field.type}
537
+ {typesDisagree(column.type, field.type) ? (
538
+ <span className="ml-2 text-declared">
539
+ · disagrees with {column.type}
540
+ </span>
541
+ ) : null}
542
+ </div>
543
+ ) : (
544
+ <div className="mono mt-1 text-unresolved">
545
+ no field of that name is declared
546
+ </div>
547
+ )}
548
+ </>
549
+ ) : null}
550
+ </>
551
+ );
552
+ }
553
+
554
+ function EventBody({
555
+ resolved,
556
+ }: {
557
+ resolved: Extract<Resolved, { kind: "event" }>;
558
+ }) {
559
+ const { event, service } = resolved;
560
+ const latest = event.versions[event.versions.length - 1];
561
+ const scope = eventScope(index, event);
562
+ // The steps that carry this event, not just the flows: a panel that says
563
+ // "checkout" sends the reader to the top of a forty-step rail, and one that
564
+ // says "checkout · step 14" opens on the step.
565
+ const steps = stepsInto(catalog, new Set([event.id]));
566
+ const decisions = index.adrsByEvent.get(event.id) ?? [];
567
+
568
+ return (
569
+ <>
570
+ <div className="mt-1.5 flex flex-wrap gap-1">
571
+ {event.versions.map((v) => (
572
+ <span
573
+ key={v.version}
574
+ className="mono rounded-control border px-1.5 py-px"
575
+ style={{
576
+ borderColor: v === latest ? "var(--accent)" : "var(--border)",
577
+ color: v === latest ? "var(--accent)" : "var(--fg-muted)",
578
+ }}
579
+ >
580
+ {v.version}
581
+ </span>
582
+ ))}
583
+ </div>
584
+
585
+ <Label>Producer</Label>
586
+ <SelectLink id={service.id}>{service.id}</SelectLink>
587
+
588
+ <Label>Schema · {latest?.version ?? "—"}</Label>
589
+ <table className="w-full">
590
+ <tbody>
591
+ {(latest?.fields ?? []).map((f) => {
592
+ // A shared def moves the selection; a block of the aggregate has
593
+ // a page and no selection, so it is a link.
594
+ const shape = f.ref ? null : resolveShape(catalog, f, scope);
595
+ const page =
596
+ shape && shape.kind !== "def" ? blockPath(shape.id) : null;
597
+ return (
598
+ <tr key={f.name} className="align-top">
599
+ <td className="mono py-0.5 pr-2 whitespace-nowrap">{f.name}</td>
600
+ <td className="mono py-0.5 text-muted">
601
+ {f.ref ? (
602
+ <SelectLink id={f.ref}>{f.type}</SelectLink>
603
+ ) : page && shape ? (
604
+ <Link
605
+ to={page}
606
+ className="rounded-control text-accent hover:underline"
607
+ title={`open ${KIND_LABEL[shape.kind as "vo" | "entity"]} ${shape.name}`}
608
+ >
609
+ {f.type}
610
+ </Link>
611
+ ) : (
612
+ f.type
613
+ )}
614
+ </td>
615
+ </tr>
616
+ );
617
+ })}
618
+ </tbody>
619
+ </table>
620
+
621
+ <Label>Consumers</Label>
622
+ {event.consumers.length === 0 ? (
623
+ <div className="mono text-muted">nothing consumes this event</div>
624
+ ) : null}
625
+ {event.consumers.map((c) => (
626
+ <Row key={c.service}>
627
+ <SelectLink id={c.service}>{c.service}</SelectLink>
628
+ {c.via ? (
629
+ <Link
630
+ to={paths.flowStep(c.via.flow, c.via.step)}
631
+ className="mono shrink-0 text-muted hover:text-ink hover:underline"
632
+ title={`read from flow ${c.via.flow}, step ${c.via.step}; no source declares this consumer`}
633
+ >
634
+ via flow
635
+ </Link>
636
+ ) : null}
637
+ <span className="ml-auto shrink-0">
638
+ <StatusChip status={c.status} title={c.note} />
639
+ </span>
640
+ </Row>
641
+ ))}
642
+
643
+ <Label>Appears in flows</Label>
644
+ {steps.length === 0 ? (
645
+ <div className="mono text-muted">no flow references this event</div>
646
+ ) : (
647
+ <div className="flex flex-col gap-1">
648
+ {steps.map((s) => (
649
+ <Link
650
+ key={`${s.flow.slug}:${s.stepId}`}
651
+ to={paths.flowStep(s.flow.slug, s.stepId)}
652
+ className="mono text-accent"
653
+ >
654
+ {s.flow.slug} · step {s.number} →
655
+ </Link>
656
+ ))}
657
+ </div>
658
+ )}
659
+
660
+ {decisions.length > 0 ? (
661
+ <>
662
+ <Label>Decisions</Label>
663
+ <div className="flex flex-col gap-1">
664
+ {decisions.map((adr) => (
665
+ <Link
666
+ key={adr.id}
667
+ to={paths.adr(adr.slug)}
668
+ className="mono truncate text-accent"
669
+ title={adr.title}
670
+ >
671
+ {adr.id} · {adr.title}
672
+ </Link>
673
+ ))}
674
+ </div>
675
+ </>
676
+ ) : null}
677
+
678
+ <Label>Source</Label>
679
+ {latest?.source ? (
680
+ <Ident block value={latest.source} className="text-muted" />
681
+ ) : (
682
+ <div className="mono text-muted">not recorded</div>
683
+ )}
684
+ </>
685
+ );
686
+ }
687
+
688
+ /**
689
+ * What the panel calls the thing it is showing.
690
+ *
691
+ * Most kinds are their id, which the line underneath already prints. These are
692
+ * the ones with a name a reader would say out loud instead - and it is a
693
+ * function rather than a ternary chain because it was nine levels deep before
694
+ * a tenth kind existed.
695
+ */
696
+ function titleOf(resolved: Resolved | null, selection: Selection): string {
697
+ if (!resolved) return selection.id;
698
+
699
+ switch (resolved.kind) {
700
+ case "event":
701
+ return resolved.event.name;
702
+ case "table":
703
+ return resolved.table.name;
704
+ case "view":
705
+ return resolved.view.name;
706
+ case "column":
707
+ return `${resolved.view?.name ?? resolved.table?.name ?? resolved.store.slug}.${resolved.column.name}`;
708
+ case "flow-step":
709
+ return resolved.step.label ?? resolved.step.ref ?? resolved.step.kind;
710
+ case "bundle":
711
+ return `${resolved.bundle.from} → ${resolved.bundle.to}`;
712
+ // `acme/shop`, not the registry host as well: the host is the same for
713
+ // every module in almost every estate.
714
+ case "module":
715
+ return resolved.module.name;
716
+ default:
717
+ return selection.id;
718
+ }
719
+ }
720
+
721
+ function ModuleBody({
722
+ resolved,
723
+ }: {
724
+ resolved: Extract<Resolved, { kind: "module" }>;
725
+ }) {
726
+ const { module } = resolved;
727
+ const counts = countsOf(index, module);
728
+ const consumers = consumersOf(index, module);
729
+ const deps = dependenciesOf(index, module);
730
+ const owner = module.owner ? index.serviceById.get(module.owner) : undefined;
731
+ const url = registryUrl(module);
732
+ const to = modulePath(module.id);
733
+
734
+ return (
735
+ <>
736
+ {module.registry ? <Label>{module.registry}</Label> : null}
737
+ <Label>commit</Label>
738
+ <Row>
739
+ {module.commit ? (
740
+ <Ident value={module.commit.slice(0, 12)} />
741
+ ) : (
742
+ /* Not pinned is a fact worth saying: it means two builds a day apart
743
+ can describe two different modules under one name. */
744
+ <span className="text-muted">not pinned</span>
745
+ )}
746
+ </Row>
747
+
748
+ <Label>holds</Label>
749
+ <div className="rows">
750
+ <div className="row">
751
+ <span className="flex-1">packages</span>
752
+ <span className="tnum text-muted">{counts.packages}</span>
753
+ </div>
754
+ <div className="row">
755
+ <span className="flex-1">interfaces</span>
756
+ <span className="tnum text-muted">{counts.interfaces}</span>
757
+ </div>
758
+ <div className="row">
759
+ <span className="flex-1">methods</span>
760
+ <span className="tnum text-muted">{counts.methods}</span>
761
+ </div>
762
+ <div className="row">
763
+ <span className="flex-1">messages</span>
764
+ <span className="tnum text-muted">{counts.messages}</span>
765
+ </div>
766
+ </div>
767
+
768
+ {/* Who publishes it, and - the more interesting half - who else reads it. */}
769
+ <Label>published by</Label>
770
+ {owner ? (
771
+ <SelectLink id={owner.id}>{owner.id}</SelectLink>
772
+ ) : (
773
+ <p className="text-muted">
774
+ nobody in this catalog — the module is published elsewhere
775
+ </p>
776
+ )}
777
+
778
+ {consumers.length > 0 ? (
779
+ <>
780
+ <Label>read by</Label>
781
+ <div className="rows">
782
+ {consumers.map((service) => (
783
+ <SelectLink key={service.id} id={service.id}>
784
+ {service.id}
785
+ </SelectLink>
786
+ ))}
787
+ </div>
788
+ </>
789
+ ) : null}
790
+
791
+ {deps.length > 0 ? (
792
+ <>
793
+ <Label>depends on</Label>
794
+ <div className="rows">
795
+ {deps.map((dep) =>
796
+ dep.module ? (
797
+ <Link
798
+ key={dep.id}
799
+ to={paths.module(dep.module.slug)}
800
+ className="row mono hover:text-ink"
801
+ >
802
+ {dep.module.name}
803
+ </Link>
804
+ ) : (
805
+ /* A module may depend on one the estate never vendored. Naming
806
+ it and saying so beats a link into nothing. */
807
+ <div key={dep.id} className="row mono text-muted">
808
+ {dep.id}
809
+ <span className="ml-auto">not in this catalog</span>
810
+ </div>
811
+ ),
812
+ )}
813
+ </div>
814
+ </>
815
+ ) : null}
816
+
817
+ {to ? (
818
+ <Link to={to} className="mt-3 block text-muted hover:text-ink">
819
+ open the module →
820
+ </Link>
821
+ ) : null}
822
+ {url ? (
823
+ <a
824
+ href={url}
825
+ target="_blank"
826
+ rel="noreferrer"
827
+ className="mt-1 block text-muted hover:text-ink"
828
+ >
829
+ {module.name} on {module.registry} ↗
830
+ </a>
831
+ ) : null}
832
+ </>
833
+ );
834
+ }
835
+
836
+ function ServiceBody({
837
+ resolved,
838
+ }: {
839
+ resolved: Extract<Resolved, { kind: "service" }>;
840
+ }) {
841
+ const { service, context } = resolved;
842
+ const methods = methodCount(service);
843
+ const events = service.aggregates.flatMap((a) => a.events);
844
+ const unresolved = service.consumes.filter((c) => c.status === "unresolved");
845
+ // Each flow this service takes part in, opened on the first step it is
846
+ // on either end of - where it enters the story, not the top of the rail.
847
+ const appearances = flowsForService(catalog, service.id).map((flow) => {
848
+ const steps = walkSteps(flow.steps);
849
+ const at = steps.findIndex((s) => s.from === service.id || s.to === service.id);
850
+ return { flow, step: at >= 0 ? steps[at] : undefined, number: at + 1 };
851
+ });
852
+
853
+ return (
854
+ <>
855
+ <Ident
856
+ block
857
+ value={`${service.repo}/${service.path}`}
858
+ className="mt-1.5 text-muted"
859
+ />
860
+
861
+ <Label>Provides</Label>
862
+ {/* The count opens the tab that lists the methods it counted. */}
863
+ <Link
864
+ to={`${paths.service(context.id, service.slug)}?tab=provides`}
865
+ className="mono rounded-control text-muted hover:text-ink"
866
+ >
867
+ <span className="tnum">{methods}</span> method
868
+ {methods === 1 ? "" : "s"} over{" "}
869
+ <span className="tnum">{service.provides.length}</span> service
870
+ {service.provides.length === 1 ? "" : "s"}
871
+ </Link>
872
+ {service.provides.map((p) => (
873
+ <Ident block key={p.id} value={p.id} className="text-muted" />
874
+ ))}
875
+
876
+ <Label>Consumes</Label>
877
+ {service.consumes.length === 0 ? (
878
+ <div className="mono text-muted">this service calls nobody</div>
879
+ ) : null}
880
+ {service.consumes.map((call) => (
881
+ <Row key={call.id}>
882
+ <Ident value={call.id} />
883
+ <span className="ml-auto shrink-0">
884
+ <StatusChip status={call.status} title={call.note} />
885
+ </span>
886
+ </Row>
887
+ ))}
888
+ {unresolved.length > 0 ? (
889
+ <Link
890
+ to={paths.problems()}
891
+ className="mono mt-1 block rounded-control text-unresolved hover:underline"
892
+ >
893
+ <span className="tnum">{unresolved.length}</span> call
894
+ {unresolved.length === 1 ? "" : "s"} resolve to nothing in the catalog
895
+
896
+ </Link>
897
+ ) : null}
898
+
899
+ <Label>Publishes</Label>
900
+ {events.length === 0 ? (
901
+ <div className="mono text-muted">no events</div>
902
+ ) : null}
903
+ {events.map((e) => (
904
+ <Row key={e.id}>
905
+ <SelectLink id={e.id}>{e.name}</SelectLink>
906
+ <Link
907
+ to={`${eventPathOf(e.id) ?? paths.service(context.id, service.slug)}#${EVENT_ANCHOR.consumers}`}
908
+ title={`${e.consumers.length} consumers of ${e.name}`}
909
+ className="mono tnum ml-auto shrink-0 rounded-control text-muted hover:text-ink"
910
+ >
911
+ {e.consumers.length}
912
+ </Link>
913
+ </Row>
914
+ ))}
915
+
916
+ <Label>Appears in flows</Label>
917
+ {appearances.length === 0 ? (
918
+ <div className="mono text-muted">appears in no flow</div>
919
+ ) : (
920
+ <div className="flex flex-col gap-1">
921
+ {appearances.map(({ flow, step, number }) => (
922
+ <Link
923
+ key={flow.slug}
924
+ to={step ? paths.flowStep(flow.slug, step.id) : paths.flow(flow.slug)}
925
+ className="mono text-accent"
926
+ title={step ? `${flow.name}, from step ${number}` : flow.name}
927
+ >
928
+ {flow.slug}
929
+ {step ? ` · step ${number}` : ""} →
930
+ </Link>
931
+ ))}
932
+ </div>
933
+ )}
934
+
935
+ <Label>Context</Label>
936
+ <SelectLink id={context.id}>{context.id}</SelectLink>
937
+ </>
938
+ );
939
+ }
940
+
941
+ function AggregateBody({
942
+ resolved,
943
+ }: {
944
+ resolved: Extract<Resolved, { kind: "aggregate" }>;
945
+ }) {
946
+ const { aggregate, service, context } = resolved;
947
+ const commands = aggregate.operations.filter((o) => o.kind === "command");
948
+ const queries = aggregate.operations.filter((o) => o.kind === "query");
949
+ const aggregatePath = paths.aggregate(
950
+ context.id,
951
+ service.slug,
952
+ aggregate.slug,
953
+ );
954
+
955
+ return (
956
+ <>
957
+ <Label>Owner</Label>
958
+ <SelectLink id={service.id}>{service.id}</SelectLink>
959
+
960
+ <Label>Operations</Label>
961
+ <div className="mono flex gap-3 text-muted">
962
+ <Link
963
+ to={`${aggregatePath}#${AGGREGATE_ANCHOR.commands}`}
964
+ className="rounded-control hover:text-ink"
965
+ >
966
+ <span className="tnum">{commands.length}</span> command
967
+ {commands.length === 1 ? "" : "s"}
968
+ </Link>
969
+ <Link
970
+ to={`${aggregatePath}#${AGGREGATE_ANCHOR.queries}`}
971
+ className="rounded-control hover:text-ink"
972
+ >
973
+ <span className="tnum">{queries.length}</span> quer
974
+ {queries.length === 1 ? "y" : "ies"}
975
+ </Link>
976
+ </div>
977
+
978
+ <Label>Events</Label>
979
+ {aggregate.events.length === 0 ? (
980
+ <div className="mono text-muted">this aggregate publishes nothing</div>
981
+ ) : null}
982
+ {aggregate.events.map((e) => (
983
+ <Row key={e.id}>
984
+ <SelectLink id={e.id}>{e.name}</SelectLink>
985
+ </Row>
986
+ ))}
987
+ </>
988
+ );
989
+ }
990
+
991
+ function ContextBody({
992
+ resolved,
993
+ }: {
994
+ resolved: Extract<Resolved, { kind: "context" }>;
995
+ }) {
996
+ const { context } = resolved;
997
+ return (
998
+ <>
999
+ <p className="mt-1.5 text-muted">{context.summary}</p>
1000
+ <Label>Services</Label>
1001
+ {context.services.map((s) => (
1002
+ <Row key={s.id}>
1003
+ <SelectLink id={s.id}>{s.slug}</SelectLink>
1004
+ <Link
1005
+ to={`${paths.service(context.id, s.slug)}#${SERVICE_ANCHOR.events}`}
1006
+ className="mono ml-auto shrink-0 rounded-control text-muted hover:text-ink"
1007
+ >
1008
+ <span className="tnum">
1009
+ {s.aggregates.reduce((n, a) => n + a.events.length, 0)}
1010
+ </span>{" "}
1011
+ events
1012
+ </Link>
1013
+ </Row>
1014
+ ))}
1015
+ </>
1016
+ );
1017
+ }
1018
+
1019
+ function ValueObjectBody({
1020
+ resolved,
1021
+ }: {
1022
+ resolved: Extract<Resolved, { kind: "value-object" }>;
1023
+ }) {
1024
+ const uses = usesOfDef(catalog, resolved.id);
1025
+ return (
1026
+ <>
1027
+ <Label>Fields</Label>
1028
+ <table className="w-full">
1029
+ <tbody>
1030
+ {resolved.def.fields.map((f) => (
1031
+ <tr key={f.name} className="align-top">
1032
+ <td className="mono py-0.5 pr-2 whitespace-nowrap">{f.name}</td>
1033
+ <td className="mono py-0.5 pr-2 text-muted">
1034
+ {f.ref ? <SelectLink id={f.ref}>{f.type}</SelectLink> : f.type}
1035
+ </td>
1036
+ <td className="py-0.5 text-muted">{f.doc}</td>
1037
+ </tr>
1038
+ ))}
1039
+ </tbody>
1040
+ </table>
1041
+
1042
+ <Label>Used in</Label>
1043
+ {uses.events.length === 0 && uses.defs.length === 0 ? (
1044
+ <div className="mono text-muted">nothing carries this type</div>
1045
+ ) : null}
1046
+ {uses.events.map((use) => (
1047
+ <Row key={use.eventId}>
1048
+ <SelectLink id={use.eventId}>{use.eventId}</SelectLink>
1049
+ <span className="mono ml-auto shrink-0 text-muted">
1050
+ {use.versions.join(" ")}
1051
+ </span>
1052
+ </Row>
1053
+ ))}
1054
+ {uses.defs.map((id) => (
1055
+ <Row key={id}>
1056
+ <SelectLink id={id}>{id}</SelectLink>
1057
+ <span className="mono ml-auto shrink-0 text-muted">type</span>
1058
+ </Row>
1059
+ ))}
1060
+ </>
1061
+ );
1062
+ }
1063
+
1064
+ function FlowStepBody({
1065
+ resolved,
1066
+ }: {
1067
+ resolved: Extract<Resolved, { kind: "flow-step" }>;
1068
+ }) {
1069
+ return (
1070
+ <>
1071
+ <div className="mono mt-1 mb-2 flex items-center gap-2 text-muted">
1072
+ <Link to={paths.flow(resolved.flow.slug)} className="text-accent">
1073
+ {resolved.flow.slug}
1074
+ </Link>
1075
+ </div>
1076
+ <StepDetailBody step={resolved.step} flow={resolved.flow} />
1077
+ </>
1078
+ );
1079
+ }
1080
+
1081
+ /**
1082
+ * One bundled edge, opened.
1083
+ *
1084
+ * Compact mode trades every event label for a number, and this is where the
1085
+ * number is spent: the count on the line is the length of this list, and the
1086
+ * list is the only place the reader can find out which events it stood for.
1087
+ */
1088
+ function BundleBody({
1089
+ resolved,
1090
+ }: {
1091
+ resolved: Extract<Resolved, { kind: "bundle" }>;
1092
+ }) {
1093
+ const { bundle } = resolved;
1094
+ return (
1095
+ <>
1096
+ <Label>Publisher</Label>
1097
+ <SelectLink id={bundle.from}>{bundle.from}</SelectLink>
1098
+
1099
+ <Label>Consumer</Label>
1100
+ {resolved.to ? (
1101
+ <SelectLink id={bundle.to}>{bundle.to}</SelectLink>
1102
+ ) : (
1103
+ <div className="mono text-muted" title="not in the catalog">
1104
+ {bundle.to} — not in catalog
1105
+ </div>
1106
+ )}
1107
+
1108
+ <Label>
1109
+ {bundle.events.length} {bundle.events.length === 1 ? "event" : "events"}
1110
+ </Label>
1111
+ {bundle.events.map((event) => (
1112
+ <Row key={event.id}>
1113
+ <SelectLink id={event.id}>{event.name}</SelectLink>
1114
+ <span className="ml-auto shrink-0">
1115
+ <StatusChip status={event.status} />
1116
+ </span>
1117
+ </Row>
1118
+ ))}
1119
+ </>
1120
+ );
1121
+ }
1122
+
1123
+ /**
1124
+ * A node the model draws but the catalog has never heard of — an external
1125
+ * participant, most often. Saying so beats saying nothing.
1126
+ */
1127
+ function UnknownBody({ selection }: { selection: Selection }) {
1128
+ const consumers = catalog.contexts.flatMap((c) =>
1129
+ c.services.flatMap((s) =>
1130
+ s.aggregates.flatMap((a) =>
1131
+ a.events
1132
+ .filter((e) => e.consumers.some((x) => x.service === selection.id))
1133
+ .map((e) => e),
1134
+ ),
1135
+ ),
1136
+ );
1137
+ const flows = catalog.flows.filter((f) =>
1138
+ f.participants.some((p) => p.id === selection.id),
1139
+ );
1140
+
1141
+ return (
1142
+ <>
1143
+ <p className="meta mt-1.5 text-unresolved">
1144
+ nothing in the catalog owns this id
1145
+ </p>
1146
+ {consumers.length > 0 ? (
1147
+ <>
1148
+ <Label>Named as a consumer of</Label>
1149
+ {consumers.map((e) => (
1150
+ <Row key={e.id}>
1151
+ <SelectLink id={e.id}>{e.name}</SelectLink>
1152
+ </Row>
1153
+ ))}
1154
+ </>
1155
+ ) : null}
1156
+ {flows.length > 0 ? (
1157
+ <>
1158
+ <Label>Appears in flows</Label>
1159
+ {flows.map((f) => (
1160
+ <Link
1161
+ key={f.slug}
1162
+ to={paths.flow(f.slug)}
1163
+ className="mono block text-accent"
1164
+ >
1165
+ {f.slug} →
1166
+ </Link>
1167
+ ))}
1168
+ </>
1169
+ ) : null}
1170
+ {consumers.length === 0 && flows.length === 0 ? (
1171
+ <p className="meta mt-3">it is not referenced anywhere else either</p>
1172
+ ) : null}
1173
+ </>
1174
+ );
1175
+ }
1176
+
1177
+ // ---------------------------------------------------------------------------
1178
+ // Frame
1179
+ // ---------------------------------------------------------------------------
1180
+
1181
+ function kindLabel(selection: Selection, resolved: Resolved | null): string {
1182
+ if (!resolved) return "unknown";
1183
+ if (resolved.kind === "flow-step") return `step ${resolved.number}`;
1184
+ return selection.kind;
1185
+ }
1186
+
1187
+ /**
1188
+ * What pinning the open selection would pin, or null when the selection is not
1189
+ * a thing a reader can come back to. A column, a view and a store are read
1190
+ * inside the canvas that holds them; a step is read inside its flow. Pinning
1191
+ * one of those would bookmark a scroll position rather than an entity.
1192
+ */
1193
+ function pinFor(
1194
+ resolved: Resolved | null,
1195
+ ): { kind: PinKind; id: string } | null {
1196
+ if (!resolved) return null;
1197
+ switch (resolved.kind) {
1198
+ case "event":
1199
+ return { kind: "event", id: resolved.event.id };
1200
+ case "service":
1201
+ return { kind: "service", id: resolved.service.id };
1202
+ case "aggregate":
1203
+ return { kind: "aggregate", id: resolved.aggregate.id };
1204
+ case "table":
1205
+ return { kind: "table", id: resolved.table.id };
1206
+ default:
1207
+ return null;
1208
+ }
1209
+ }
1210
+
1211
+ export function DetailPanel() {
1212
+ const selection = useSelectionStore((s) => s.selection);
1213
+ const clear = useSelectionStore((s) => s.clear);
1214
+ if (!selection) return null;
1215
+
1216
+ const resolved = resolveSelection(selection.id);
1217
+ const page = selectionPath(selection);
1218
+ const pin = pinFor(resolved);
1219
+
1220
+ return (
1221
+ <aside
1222
+ /* Slides 16px and fades in when the panel appears - not on every change
1223
+ of selection, which would set the whole rail moving each time the
1224
+ reader clicked a step. There is no exit: it unmounts on clear. */
1225
+ className="panel-in pane flex h-full w-full flex-col border-l border-line bg-canvas"
1226
+ aria-label="Selection detail"
1227
+ >
1228
+ <div className="sticky-bar flex shrink-0 items-center gap-2 border-b px-4 py-2.5 border-line">
1229
+ <span className="label">{kindLabel(selection, resolved)}</span>
1230
+ {resolved?.kind === "flow-step" ? (
1231
+ <StatusChip status={resolved.step.status} />
1232
+ ) : null}
1233
+ {/* Right-aligned beside the close: both are about the panel rather
1234
+ than about what is in it. */}
1235
+ {pin ? (
1236
+ <span className="ml-auto flex items-center">
1237
+ <PinButton kind={pin.kind} id={pin.id} size={14} />
1238
+ </span>
1239
+ ) : null}
1240
+ <button
1241
+ type="button"
1242
+ onClick={() => clear("panel")}
1243
+ aria-label="Close selection detail (Esc)"
1244
+ title="Esc"
1245
+ className="rounded-control p-1 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
1246
+ >
1247
+ <X size={16} aria-hidden />
1248
+ </button>
1249
+ </div>
1250
+
1251
+ <div className="flex-1 overflow-y-auto p-4">
1252
+ <div className="flex flex-wrap items-baseline gap-x-2">
1253
+ <span className="mono break-all text-sm text-ink">
1254
+ {titleOf(resolved, selection)}
1255
+ </span>
1256
+ {resolved?.kind === "event" ||
1257
+ resolved?.kind === "service" ||
1258
+ resolved?.kind === "aggregate" ? (
1259
+ <span
1260
+ className="mono ctx"
1261
+ style={ctxStyle(
1262
+ "context" in resolved ? resolved.context.id : null,
1263
+ )}
1264
+ >
1265
+ {resolved.context.id}
1266
+ </span>
1267
+ ) : null}
1268
+ </div>
1269
+ {/* A synthetic id is not something a reader can paste anywhere, so a
1270
+ step and a bundle keep theirs to themselves. */}
1271
+ {resolved !== null &&
1272
+ resolved.kind !== "flow-step" &&
1273
+ resolved.kind !== "bundle" ? (
1274
+ <Ident block value={selection.id} className="mt-0.5 text-muted" />
1275
+ ) : null}
1276
+
1277
+ {page ? (
1278
+ <Link
1279
+ to={page}
1280
+ className="mono mt-3 inline-block rounded-control text-accent hover:underline"
1281
+ >
1282
+ open page →
1283
+ </Link>
1284
+ ) : null}
1285
+
1286
+ {resolved === null ? (
1287
+ <UnknownBody selection={selection} />
1288
+ ) : resolved.kind === "event" ? (
1289
+ <EventBody resolved={resolved} />
1290
+ ) : resolved.kind === "service" ? (
1291
+ <ServiceBody resolved={resolved} />
1292
+ ) : resolved.kind === "aggregate" ? (
1293
+ <AggregateBody resolved={resolved} />
1294
+ ) : resolved.kind === "context" ? (
1295
+ <ContextBody resolved={resolved} />
1296
+ ) : resolved.kind === "value-object" ? (
1297
+ <ValueObjectBody resolved={resolved} />
1298
+ ) : resolved.kind === "store" ? (
1299
+ <StoreBody resolved={resolved} />
1300
+ ) : resolved.kind === "table" ? (
1301
+ <TableBody resolved={resolved} />
1302
+ ) : resolved.kind === "view" ? (
1303
+ <ViewBody resolved={resolved} />
1304
+ ) : resolved.kind === "column" ? (
1305
+ <ColumnBody resolved={resolved} />
1306
+ ) : resolved.kind === "bundle" ? (
1307
+ <BundleBody resolved={resolved} />
1308
+ ) : resolved.kind === "module" ? (
1309
+ <ModuleBody resolved={resolved} />
1310
+ ) : (
1311
+ <FlowStepBody resolved={resolved} />
1312
+ )}
1313
+ </div>
1314
+ </aside>
1315
+ );
1316
+ }
1317
+
1318
+ /**
1319
+ * Page shell for the routes that embed a diagram: the page on the left, the
1320
+ * selection detail on the right.
1321
+ *
1322
+ * The detail panel is not opened by dragging. It is collapsed to nothing while
1323
+ * there is no selection and expanded the moment there is one, driven from the
1324
+ * store rather than from the handle - the handle only decides how wide it is
1325
+ * once open, and that width is what gets remembered.
1326
+ */
1327
+ export function WithDetail({
1328
+ id,
1329
+ children,
1330
+ }: {
1331
+ /** Page name for the persisted layout: "portolan:<page>". */
1332
+ id: string;
1333
+ children: ReactNode;
1334
+ }) {
1335
+ const selection = useSelectionStore((s) => s.selection);
1336
+ const clear = useSelectionStore((s) => s.clear);
1337
+ const hidden = useUiStore((s) => s.detailHidden);
1338
+ const setHidden = useUiStore((s) => s.setDetailHidden);
1339
+ const detailRef = usePanelRef();
1340
+ const settle = useCanvasResize();
1341
+ const narrow = useNarrow();
1342
+ const open = selection !== null && !hidden;
1343
+
1344
+ // Picking something new is a request to see it. "]" means "not now", not
1345
+ // "never again", so the next selection brings the rail back rather than
1346
+ // landing silently behind a panel the reader forgot they folded away.
1347
+ const selectedId = selection?.id ?? null;
1348
+ useEffect(() => {
1349
+ if (selectedId !== null) setHidden(false);
1350
+ }, [selectedId, setHidden]);
1351
+
1352
+ useEffect(() => {
1353
+ const panel = detailRef.current;
1354
+ if (!panel) return;
1355
+ if (open) panel.expand();
1356
+ else panel.collapse();
1357
+ // `narrow` is in the list because the Group unmounts across the
1358
+ // breakpoint: the panel that comes back has to be told again.
1359
+ }, [open, narrow, detailRef]);
1360
+
1361
+ // Below the breakpoint there is no room for a third pane, so the rail becomes
1362
+ // a sheet over the page. Esc is already the app's "clear the selection", and
1363
+ // clearing the selection is exactly what closes this.
1364
+ if (narrow) {
1365
+ return (
1366
+ <div className="relative h-full min-h-0">
1367
+ {children}
1368
+ <SidePanel
1369
+ open={open}
1370
+ onClose={() => clear("panel")}
1371
+ side="right"
1372
+ label="Selection detail"
1373
+ width="85%"
1374
+ >
1375
+ <DetailPanel />
1376
+ </SidePanel>
1377
+ </div>
1378
+ );
1379
+ }
1380
+
1381
+ return (
1382
+ <SavedGroup
1383
+ id={`portolan:${id}`}
1384
+ orientation="horizontal"
1385
+ className="h-full min-h-0"
1386
+ >
1387
+ <Panel id="content" className="h-full min-w-0" onResize={settle}>
1388
+ {children}
1389
+ </Panel>
1390
+
1391
+ <ResizeHandle id="detail" />
1392
+
1393
+ <Panel
1394
+ id="detail"
1395
+ defaultSize="24"
1396
+ minSize="16"
1397
+ collapsible
1398
+ collapsedSize="0"
1399
+ panelRef={detailRef}
1400
+ className="h-full"
1401
+ onResize={settle}
1402
+ >
1403
+ <DetailPanel />
1404
+ </Panel>
1405
+ </SavedGroup>
1406
+ );
1407
+ }