@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,1742 @@
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
2
+ import type { KeyboardEvent as ReactKeyboardEvent } from "react";
3
+ import { TREE_KEYS, treeKey } from "./tree-keys";
4
+ import type { TreeRow } from "./tree-keys";
5
+ import { Link, NavLink, useLocation, useMatch } from "react-router";
6
+ import {
7
+ DURATION,
8
+ LayoutGroup,
9
+ Unfold,
10
+ m,
11
+ transitions,
12
+ } from "../lib/motion";
13
+ import {
14
+ Check,
15
+ ChevronRight,
16
+ GripVertical,
17
+ PanelLeftOpen,
18
+ PinOff,
19
+ Settings2,
20
+ TriangleAlert,
21
+ } from "lucide-react";
22
+ import { catalog, index } from "../data";
23
+ import type {
24
+ Aggregate,
25
+ Block,
26
+ BoundedContext,
27
+ Classification,
28
+ Event,
29
+ Operation,
30
+ RpcMethod,
31
+ RpcService,
32
+ Service,
33
+ Store,
34
+ Table,
35
+ View,
36
+ Enum,
37
+ } from "../catalog";
38
+ import { allModules, allTerms, enumsOf, storeViews } from "../catalog";
39
+ import { adrNumber, newestAccepted } from "../lib/adr";
40
+ import { contextName, ctxStyle } from "../lib/context-color";
41
+ import { contextStats, problems } from "../lib/derive";
42
+ import { dataProblems } from "../lib/data-problems";
43
+ import { protoProblems } from "../lib/proto-problems";
44
+ import { vocabularies } from "../language/cards";
45
+ import {
46
+ FLOW_HEALTH_NOTE,
47
+ groupFlowsByOwner,
48
+ reachDots,
49
+ visibleEntries,
50
+ } from "../lib/flow-tree";
51
+ import type { FlowEntry, FlowHealth, FlowGroup } from "../lib/flow-tree";
52
+ import type { Kind, LeafKind } from "../lib/kinds";
53
+ import { plural } from "../lib/format";
54
+ import {
55
+ FLOW_GROUPS_KEY,
56
+ SECTIONS_KEY,
57
+ readFlags,
58
+ writeFlags,
59
+ } from "../lib/sidebar-prefs";
60
+ import type { Flags } from "../lib/sidebar-prefs";
61
+ import { KindIcon } from "../components/kind";
62
+ import { CompassRose, Wordmark } from "../components/logo";
63
+ import { isStruck } from "../components/primitives";
64
+ import { packageAnchor, paths } from "../routes";
65
+ import { selectionHash } from "../selection/hash";
66
+ import { STORE_KIND_LABEL, StoreKindMark } from "../er/StoreHeader";
67
+ import { resolvePin, usePinsStore } from "./pins";
68
+ import { resolveSelection, selectionFor } from "../selection/model";
69
+ import { selectsInPlace } from "../selection/pages";
70
+ import { useSelectionStore } from "../selection/store";
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // The rows under a service, grouped the way the tree draws them. The tree
74
+ // shows everything: searching the catalog is ⌘K's job, and a second box that
75
+ // narrowed this pane was the same question asked twice on one screen.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ interface AggregateRows {
79
+ aggregate: Aggregate;
80
+ valueObjects: Block[];
81
+ entities: Block[];
82
+ enums: Enum[];
83
+ events: Event[];
84
+ commands: Operation[];
85
+ queries: Operation[];
86
+ }
87
+
88
+ interface StoreRows {
89
+ store: Store;
90
+ tables: Table[];
91
+ views: View[];
92
+ }
93
+
94
+ interface ServiceRows {
95
+ service: Service;
96
+ aggregates: AggregateRows[];
97
+ endpoints: EndpointRows[];
98
+ stores: StoreRows[];
99
+ }
100
+
101
+ interface EndpointRows {
102
+ provided: RpcService;
103
+ methods: RpcMethod[];
104
+ }
105
+
106
+ /** What the service answers, one entry per interface it declares. */
107
+ function endpointRows(service: Service): EndpointRows[] {
108
+ return service.provides.map((provided) => ({
109
+ provided,
110
+ methods: provided.methods,
111
+ }));
112
+ }
113
+
114
+ /**
115
+ * The stores a service owns. Read-only stores are left out: the tree is a map
116
+ * of what each service is responsible for, and a store it borrows belongs
117
+ * under its owner.
118
+ */
119
+ function storeRows(service: Service): StoreRows[] {
120
+ return (catalog.stores ?? [])
121
+ .filter((store) => store.owner === service.id)
122
+ .map((store) => ({
123
+ store,
124
+ tables: store.tables,
125
+ views: storeViews(store),
126
+ }));
127
+ }
128
+
129
+ function aggregateRows(aggregate: Aggregate): AggregateRows {
130
+ const ops = (kind: "command" | "query"): Operation[] =>
131
+ aggregate.operations.filter((o) => o.kind === kind);
132
+ return {
133
+ aggregate,
134
+ valueObjects: aggregate.valueObjects,
135
+ entities: aggregate.entities,
136
+ enums: enumsOf(aggregate),
137
+ events: aggregate.events,
138
+ commands: ops("command"),
139
+ queries: ops("query"),
140
+ };
141
+ }
142
+
143
+ function contextRows(context: BoundedContext): {
144
+ context: BoundedContext;
145
+ services: ServiceRows[];
146
+ } {
147
+ return {
148
+ context,
149
+ services: context.services.map((service) => ({
150
+ service,
151
+ aggregates: service.aggregates.map(aggregateRows),
152
+ endpoints: endpointRows(service),
153
+ stores: storeRows(service),
154
+ })),
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Contexts, in the order the reader should meet them: where the estate
160
+ * competes first, what holds it up next, what it bought last, and what it has
161
+ * not yet rated at the end. This is the ONE place classification orders
162
+ * anything - everywhere else it is a badge and nothing more.
163
+ */
164
+ const CLASSIFICATION_ORDER: Record<Classification, number> = {
165
+ core: 0,
166
+ supporting: 1,
167
+ generic: 2,
168
+ };
169
+
170
+ function classificationRank(c: Classification | undefined): number {
171
+ return c === undefined ? 3 : CLASSIFICATION_ORDER[c];
172
+ }
173
+
174
+ const CLASSIFICATION_NOTE: Record<Classification, string> = {
175
+ core: "core domain - where this estate competes",
176
+ supporting: "supporting domain - needed here, but not a differentiator",
177
+ generic: "generic domain - a solved problem, bought or borrowed",
178
+ };
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Rows
182
+ // ---------------------------------------------------------------------------
183
+
184
+ const indent = (depth: number) => 8 + depth * 12;
185
+
186
+ /**
187
+ * The selection's light: the raised background and the accent edge of the
188
+ * row that is selected. One element for the whole tree - a new selection is
189
+ * the same light arriving on another row, and it travels there on the settle
190
+ * spring rather than going out here and coming on over there. `-left-0.5`
191
+ * puts its edge over the row's own transparent 2px border, so the text does
192
+ * not move.
193
+ */
194
+ function SelectionLight() {
195
+ return (
196
+ <m.span
197
+ layoutId="selection"
198
+ aria-hidden
199
+ className="pointer-events-none absolute inset-y-0 right-0 -left-0.5 -z-10 border-l-2 border-accent"
200
+ style={{ background: "var(--surface-2)" }}
201
+ transition={transitions.settle}
202
+ />
203
+ );
204
+ }
205
+
206
+ function Chevron({ open }: { open: boolean }) {
207
+ return (
208
+ <ChevronRight
209
+ size={14}
210
+ aria-hidden
211
+ className="block shrink-0 t-micro transition-transform"
212
+ style={{
213
+ transform: open ? "rotate(90deg)" : "none",
214
+ color: "var(--fg-muted)",
215
+ }}
216
+ />
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Clicking a row always selects. Whether it also navigates is the question the
222
+ * page on screen answers: on a flow page, picking something that flow already
223
+ * draws is a question about that sequence, so the reader stays put.
224
+ *
225
+ * Only the in-place case writes the store here. A row that navigates carries
226
+ * its selection in the link's hash instead (see `rowTarget`): writing the
227
+ * store first and then letting the link go would race SelectionSync, whose
228
+ * `replace` lands with the pathname it rendered with - the page being left -
229
+ * and undoes the navigation. A selection in the URL wins on arrival, so one
230
+ * write does both jobs. The palette works the same way.
231
+ */
232
+ function useRowClick(selId: string | undefined) {
233
+ const select = useSelectionStore((s) => s.select);
234
+ const { pathname } = useLocation();
235
+ return (e: React.MouseEvent) => {
236
+ if (selId === undefined) return;
237
+ if (!selectsInPlace(pathname, selectionFor(selId))) return;
238
+ e.preventDefault();
239
+ select(selId, "sidebar");
240
+ };
241
+ }
242
+
243
+ /** Where a row's link goes: its page, with the row's selection in the hash. */
244
+ function rowTarget(to: string, selId: string | undefined): string {
245
+ if (selId === undefined || to.includes("#")) return to;
246
+ return `${to}${selectionHash(selectionFor(selId))}`;
247
+ }
248
+
249
+ /**
250
+ * A leaf: the whole row navigates. Rows that stand for a selectable entity also
251
+ * carry `selId`, which makes them both a writer of the selection and a reader
252
+ * of it - the row lights up whether it was clicked here, on a diagram, or in
253
+ * the palette.
254
+ */
255
+ function Leaf({
256
+ to,
257
+ depth,
258
+ children,
259
+ title,
260
+ selId,
261
+ }: {
262
+ to: string;
263
+ depth: number;
264
+ children: React.ReactNode;
265
+ title?: string;
266
+ selId?: string;
267
+ }) {
268
+ const selected = useSelectionStore(
269
+ (s) => selId !== undefined && s.selection?.id === selId,
270
+ );
271
+ const onClick = useRowClick(selId);
272
+ return (
273
+ <NavLink
274
+ to={rowTarget(to, selId)}
275
+ end
276
+ title={title}
277
+ data-sel={selId}
278
+ data-tree-row
279
+ data-tree-depth={depth}
280
+ onClick={onClick}
281
+ style={({ isActive }) => ({
282
+ paddingLeft: indent(depth),
283
+ background: isActive && !selected ? "var(--surface-2)" : undefined,
284
+ borderLeftWidth: 2,
285
+ borderLeftStyle: "solid",
286
+ borderLeftColor:
287
+ isActive && !selected ? "var(--accent)" : "transparent",
288
+ })}
289
+ /* The edge is always 2px, transparent when idle: lighting a row must not
290
+ shift the text beside it. The route's own row paints its light in
291
+ place; the SELECTION's light is one element that slides from the row
292
+ it was on to this one. */
293
+ data-nav-item
294
+ className="tree-row relative isolate flex items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
295
+ >
296
+ {selected ? <SelectionLight /> : null}
297
+ {children}
298
+ </NavLink>
299
+ );
300
+ }
301
+
302
+ /**
303
+ * A branch: a disclosure triangle that only expands, next to a link that only
304
+ * navigates. Keeping them separate means neither is nested inside the other.
305
+ *
306
+ * `end` is for controls that must be their own target - the unresolved-edge
307
+ * count on a context, which goes somewhere else entirely. It sits OUTSIDE the
308
+ * link, because an anchor may not contain another one, and the link gives up
309
+ * its claim on the rest of the row to make room for it.
310
+ */
311
+ function Branch({
312
+ to,
313
+ depth,
314
+ open,
315
+ onToggle,
316
+ label,
317
+ children,
318
+ right,
319
+ end,
320
+ under,
321
+ selId,
322
+ }: {
323
+ to: string;
324
+ depth: number;
325
+ open: boolean;
326
+ onToggle: () => void;
327
+ label: string;
328
+ children: React.ReactNode;
329
+ right?: React.ReactNode;
330
+ end?: React.ReactNode;
331
+ under?: React.ReactNode;
332
+ selId?: string;
333
+ }) {
334
+ const selected = useSelectionStore(
335
+ (s) => selId !== undefined && s.selection?.id === selId,
336
+ );
337
+ const onClick = useRowClick(selId);
338
+ const active = useMatch({ path: to, end: true }) !== null && !selected;
339
+ return (
340
+ <>
341
+ <div
342
+ className="tree-row group relative isolate flex items-stretch t-micro transition-colors hover:bg-surface"
343
+ style={{
344
+ paddingLeft: indent(depth),
345
+ background: active ? "var(--surface-2)" : undefined,
346
+ borderLeftWidth: 2,
347
+ borderLeftStyle: "solid",
348
+ borderLeftColor: active ? "var(--accent)" : "transparent",
349
+ }}
350
+ >
351
+ {selected ? <SelectionLight /> : null}
352
+ <button
353
+ type="button"
354
+ onClick={onToggle}
355
+ aria-expanded={open}
356
+ aria-label={`${open ? "Collapse" : "Expand"} ${label}`}
357
+ data-tree-toggle
358
+ className="flex shrink-0 items-center pr-1"
359
+ >
360
+ <Chevron open={open} />
361
+ </button>
362
+ <NavLink
363
+ to={rowTarget(to, selId)}
364
+ end
365
+ data-sel={selId}
366
+ data-nav-item
367
+ data-tree-row
368
+ data-tree-depth={depth}
369
+ data-tree-open={open}
370
+ onClick={onClick}
371
+ className={`flex min-w-0 items-center gap-1.5 py-[3px] pr-2 ${
372
+ end ? "shrink" : "flex-1"
373
+ }`}
374
+ >
375
+ {children}
376
+ {right && !end ? (
377
+ <span className="ml-auto shrink-0">{right}</span>
378
+ ) : null}
379
+ </NavLink>
380
+ {end}
381
+ {right && end ? (
382
+ <span className="flex shrink-0 items-center pr-2">{right}</span>
383
+ ) : null}
384
+ </div>
385
+ {under}
386
+ </>
387
+ );
388
+ }
389
+
390
+ /**
391
+ * A group header inside an aggregate - "value objects (3)". It names a kind
392
+ * rather than a thing, so it has no page and does nothing but open and close.
393
+ */
394
+ function Group({
395
+ kind,
396
+ label,
397
+ count,
398
+ open,
399
+ onToggle,
400
+ depth,
401
+ children,
402
+ }: {
403
+ kind: LeafKind;
404
+ /** Overrides the kind's own plural — the stores group is "data", not "tables". */
405
+ label?: string;
406
+ count: number;
407
+ open: boolean;
408
+ onToggle: () => void;
409
+ depth: number;
410
+ children: React.ReactNode;
411
+ }) {
412
+ if (count === 0) return null;
413
+ return (
414
+ <>
415
+ <button
416
+ type="button"
417
+ onClick={onToggle}
418
+ aria-expanded={open}
419
+ data-tree-row
420
+ data-tree-depth={depth}
421
+ data-tree-open={open}
422
+ style={{ paddingLeft: indent(depth) }}
423
+ className="tree-row group-row"
424
+ >
425
+ <Chevron open={open} />
426
+ <KindIcon kind={kind} />
427
+ <span>
428
+ {label ?? KIND_GROUP_LABEL[kind]} ({count})
429
+ </span>
430
+ </button>
431
+ <Unfold open={open}>{children}</Unfold>
432
+ </>
433
+ );
434
+ }
435
+
436
+ const KIND_GROUP_LABEL: Record<LeafKind, string> = {
437
+ vo: "value objects",
438
+ entity: "entities",
439
+ enum: "enums",
440
+ event: "events",
441
+ command: "commands",
442
+ query: "queries",
443
+ endpoint: "api",
444
+ table: "tables",
445
+ view: "views",
446
+ };
447
+
448
+ // ---------------------------------------------------------------------------
449
+ // Sections
450
+ // ---------------------------------------------------------------------------
451
+
452
+ /**
453
+ * A top-level band of the tree. Its header folds it and always states its size:
454
+ * a folded section that also hid its count would be indistinguishable from an
455
+ * empty one, and the reader would have to open it to find out which.
456
+ *
457
+ * 12px above the header and nothing else between sections. The tree is one
458
+ * continuous list; boxing each band would make a reader count boxes.
459
+ */
460
+ function Section({
461
+ title,
462
+ count,
463
+ open,
464
+ onToggle,
465
+ first = false,
466
+ children,
467
+ }: {
468
+ title: string;
469
+ count: number;
470
+ open: boolean;
471
+ onToggle: () => void;
472
+ /** The first section needs no air above it - the header is already there. */
473
+ first?: boolean;
474
+ children: React.ReactNode;
475
+ }) {
476
+ return (
477
+ <div className={first ? "" : "mt-3"}>
478
+ <button
479
+ type="button"
480
+ onClick={onToggle}
481
+ aria-expanded={open}
482
+ className="sb-section"
483
+ >
484
+ <Chevron open={open} />
485
+ {title}
486
+ <span className="sb-count">{count}</span>
487
+ </button>
488
+ <Unfold open={open}>{children}</Unfold>
489
+ </div>
490
+ );
491
+ }
492
+
493
+ /** A section of the tree with nothing under it, and why. */
494
+ // ---------------------------------------------------------------------------
495
+ // The rows as the arrow keys see them. Every leaf, branch link and group
496
+ // header carries `data-tree-row`; a closed branch's children are not in the
497
+ // DOM at all, and a row mid-fold has no box yet, so what is left is exactly
498
+ // what is on screen.
499
+ // ---------------------------------------------------------------------------
500
+
501
+ function treeRowsOf(scroller: HTMLElement): HTMLElement[] {
502
+ return [...scroller.querySelectorAll<HTMLElement>("[data-tree-row]")].filter(
503
+ (el) => el.offsetParent !== null,
504
+ );
505
+ }
506
+
507
+ function describeRow(el: HTMLElement): TreeRow {
508
+ const open = el.dataset.treeOpen;
509
+ return {
510
+ depth: Number(el.dataset.treeDepth ?? 0),
511
+ open: open === "true" ? true : open === "false" ? false : null,
512
+ };
513
+ }
514
+
515
+ /** The control that opens or closes a row: itself for a group, the chevron beside a branch. */
516
+ function toggleOf(row: HTMLElement): HTMLElement | null {
517
+ if (row.hasAttribute("aria-expanded")) return row;
518
+ return (
519
+ row.parentElement?.querySelector<HTMLElement>(":scope > [data-tree-toggle]") ??
520
+ null
521
+ );
522
+ }
523
+
524
+ function TreeNote({ children }: { children: React.ReactNode }) {
525
+ return <div className="px-3 py-1 text-muted">{children}</div>;
526
+ }
527
+
528
+ // ---------------------------------------------------------------------------
529
+ // Flows
530
+ // ---------------------------------------------------------------------------
531
+
532
+ const HEALTH_COLOR: Record<FlowHealth, string> = {
533
+ unresolved: "var(--status-unresolved)",
534
+ declared: "var(--fg-faint)",
535
+ verified: "var(--status-verified)",
536
+ };
537
+
538
+ /**
539
+ * One flow. Its name is what the row is for; the two marks at the end answer
540
+ * the two questions a reader has before opening it - how far does it travel,
541
+ * and can I believe it.
542
+ */
543
+ function FlowRow({ entry }: { entry: FlowEntry }) {
544
+ const { flow, health, reach } = entry;
545
+ const { dots, more } = reachDots(reach);
546
+ const crosses =
547
+ reach.length === 0
548
+ ? "stays inside its own context"
549
+ : `crosses into ${reach.join(", ")}`;
550
+ return (
551
+ <Leaf
552
+ to={paths.flow(flow.slug)}
553
+ depth={1}
554
+ title={`${flow.slug} — ${crosses}`}
555
+ >
556
+ <KindIcon kind="flow" />
557
+ <span className="truncate">{flow.name}</span>
558
+ <span className="ml-auto flex shrink-0 items-center gap-2 pl-2">
559
+ <span className="flex items-center gap-0.5" aria-hidden>
560
+ {dots.map((c) => (
561
+ <span key={c} className="sb-reach" style={ctxStyle(c)} />
562
+ ))}
563
+ {more > 0 ? (
564
+ <span className="mono" style={{ color: "var(--fg-faint)" }}>
565
+ +{more}
566
+ </span>
567
+ ) : null}
568
+ </span>
569
+ <span
570
+ className="sb-health"
571
+ title={FLOW_HEALTH_NOTE[health]}
572
+ style={{ background: HEALTH_COLOR[health] }}
573
+ />
574
+ </span>
575
+ </Leaf>
576
+ );
577
+ }
578
+
579
+ /**
580
+ * The flows one context owns. Grouping them by owner rather than listing them
581
+ * flat is what makes the section answer "whose flows are these" - and the
582
+ * unowned group is a defect report: with a valid catalog it is never drawn.
583
+ */
584
+ function FlowGroupNode({
585
+ group,
586
+ open,
587
+ onToggle,
588
+ }: {
589
+ group: FlowGroup;
590
+ open: boolean;
591
+ onToggle: () => void;
592
+ }) {
593
+ const { shown, hidden } = visibleEntries(group.entries);
594
+ const owner = group.owner;
595
+ const label = owner === null ? "unowned" : contextName(owner);
596
+ return (
597
+ <div>
598
+ <button
599
+ type="button"
600
+ onClick={onToggle}
601
+ aria-expanded={open}
602
+ style={{ paddingLeft: indent(0) }}
603
+ title={
604
+ owner === null
605
+ ? "these flows name no owner — a valid catalog has none of these"
606
+ : `flows owned by ${owner}`
607
+ }
608
+ className="tree-row flex w-full items-center gap-1.5 py-[3px] pr-2 text-left t-micro transition-colors hover:bg-surface"
609
+ >
610
+ <Chevron open={open} />
611
+ {owner === null ? (
612
+ <TriangleAlert
613
+ size={13}
614
+ aria-hidden
615
+ className="block shrink-0 text-unresolved"
616
+ />
617
+ ) : (
618
+ <KindIcon kind="context" contextId={owner} />
619
+ )}
620
+ <span
621
+ className="truncate"
622
+ style={owner === null ? { color: "var(--status-unresolved)" } : {}}
623
+ >
624
+ {label}
625
+ </span>
626
+ <span className="sb-count">{group.entries.length}</span>
627
+ </button>
628
+ <Unfold open={open}>
629
+ <>
630
+ {shown.map((entry) => (
631
+ <FlowRow key={entry.flow.slug} entry={entry} />
632
+ ))}
633
+ {hidden > 0 && owner !== null ? (
634
+ <NavLink
635
+ to={`${paths.flows()}?owner=${encodeURIComponent(owner)}`}
636
+ data-nav-item
637
+ style={{ paddingLeft: indent(1) }}
638
+ className="tree-row mono flex items-center py-[3px] pr-2 text-accent hover:bg-surface"
639
+ >
640
+ view all {group.entries.length} →
641
+ </NavLink>
642
+ ) : null}
643
+ </>
644
+ </Unfold>
645
+ </div>
646
+ );
647
+ }
648
+
649
+ // ---------------------------------------------------------------------------
650
+ // Pins
651
+ // ---------------------------------------------------------------------------
652
+
653
+ /**
654
+ * The reader's own shortlist, above everything the catalog decided. It only
655
+ * exists while there is something on it: an empty band with a header would
656
+ * teach the feature by taking up room, which is the one thing the pane cannot
657
+ * spare.
658
+ */
659
+ function PinnedSection({
660
+ open,
661
+ onToggle,
662
+ }: {
663
+ open: boolean;
664
+ onToggle: () => void;
665
+ }) {
666
+ const pins = usePinsStore((s) => s.pins);
667
+ const toggle = usePinsStore((s) => s.toggle);
668
+ const reorder = usePinsStore((s) => s.reorder);
669
+ // Which row is under the cursor is a ref, not state: a drop fires in the same
670
+ // gesture that started the drag, and a state write scheduled by `dragstart`
671
+ // is not guaranteed to have landed by the time `drop` reads it. The state
672
+ // beside it only dims the row, so it is allowed to arrive late.
673
+ const dragFrom = useRef<number | null>(null);
674
+ const [dragging, setDragging] = useState<number | null>(null);
675
+
676
+ // Resolved at render, so a pin taken against a build that no longer has the
677
+ // event simply stops being drawn rather than pointing into nothing.
678
+ const rows = useMemo(
679
+ () =>
680
+ pins
681
+ .map((pin, at) => ({ at, resolved: resolvePin(pin) }))
682
+ .filter(
683
+ (r): r is { at: number; resolved: NonNullable<typeof r.resolved> } =>
684
+ r.resolved !== null,
685
+ ),
686
+ [pins],
687
+ );
688
+ if (rows.length === 0) return null;
689
+
690
+ return (
691
+ <Section
692
+ title="Pinned"
693
+ count={rows.length}
694
+ open={open}
695
+ onToggle={onToggle}
696
+ first
697
+ >
698
+ {rows.map(({ at, resolved }) => (
699
+ <div
700
+ key={`${resolved.pin.kind}:${resolved.pin.id}`}
701
+ draggable
702
+ onDragStart={(e) => {
703
+ dragFrom.current = at;
704
+ e.dataTransfer.effectAllowed = "move";
705
+ setDragging(at);
706
+ }}
707
+ onDragEnd={() => {
708
+ dragFrom.current = null;
709
+ setDragging(null);
710
+ }}
711
+ onDragOver={(e) => {
712
+ e.preventDefault();
713
+ e.dataTransfer.dropEffect = "move";
714
+ }}
715
+ onDrop={(e) => {
716
+ e.preventDefault();
717
+ const from = dragFrom.current;
718
+ if (from !== null) reorder(from, at);
719
+ dragFrom.current = null;
720
+ setDragging(null);
721
+ }}
722
+ className={`group relative flex items-stretch ${
723
+ dragging === at ? "opacity-50" : ""
724
+ }`}
725
+ >
726
+ <NavLink
727
+ to={resolved.path}
728
+ end
729
+ draggable={false}
730
+ data-nav-item
731
+ title={resolved.title}
732
+ style={({ isActive }) => ({
733
+ paddingLeft: indent(0),
734
+ background: isActive ? "var(--surface-2)" : undefined,
735
+ borderLeftWidth: 2,
736
+ borderLeftStyle: "solid",
737
+ borderLeftColor: isActive ? "var(--accent)" : "transparent",
738
+ })}
739
+ className="tree-row flex min-w-0 flex-1 items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
740
+ >
741
+ <GripVertical
742
+ size={12}
743
+ aria-hidden
744
+ className="block shrink-0 opacity-0 group-hover:opacity-100"
745
+ style={{ color: "var(--fg-faint)" }}
746
+ />
747
+ <KindIcon
748
+ kind={resolved.kind}
749
+ {...(resolved.contextId ? { contextId: resolved.contextId } : {})}
750
+ />
751
+ <span className="truncate">{resolved.name}</span>
752
+ </NavLink>
753
+ <button
754
+ type="button"
755
+ onClick={() => toggle(resolved.pin)}
756
+ aria-label={`Unpin ${resolved.name}`}
757
+ title={`Unpin ${resolved.name}`}
758
+ className="flex shrink-0 items-center px-2 text-muted opacity-0 t-micro transition-opacity hover:text-ink group-hover:opacity-100 focus-visible:opacity-100"
759
+ >
760
+ <PinOff size={13} aria-hidden />
761
+ </button>
762
+ </div>
763
+ ))}
764
+ </Section>
765
+ );
766
+ }
767
+
768
+ // ---------------------------------------------------------------------------
769
+ // The bottom group. A sibling of the scroller rather than a row inside it, so
770
+ // "pinned to the bottom" is a fact about the layout and not about how far the
771
+ // tree happens to have been scrolled.
772
+ // ---------------------------------------------------------------------------
773
+
774
+ function BottomGroup() {
775
+ // Both halves of the Problems page, because that is what the row opens. The
776
+ // badge is red while anything on it is an error and amber when only the
777
+ // schema disagrees with itself - a page of warnings is not a clean estate,
778
+ // and a green tick over it would be the one lie the row can tell.
779
+ const found = useMemo(
780
+ () => [
781
+ ...problems(catalog),
782
+ ...protoProblems(catalog, index),
783
+ ...dataProblems(catalog, index),
784
+ ],
785
+ [],
786
+ );
787
+ const errors = found.filter((p) => p.severity === "error").length;
788
+ const colour =
789
+ errors > 0 ? "var(--status-unresolved)" : "var(--status-declared)";
790
+
791
+ return (
792
+ <div className="shrink-0 border-t bg-canvas border-line py-0.5">
793
+ <NavLink
794
+ to={paths.problems()}
795
+ end
796
+ data-nav-item
797
+ aria-keyshortcuts="g p"
798
+ title={
799
+ found.length === 0
800
+ ? "Problems — every edge in the catalog lands somewhere"
801
+ : `Problems — ${found.length} to look at`
802
+ }
803
+ style={({ isActive }) => ({
804
+ paddingLeft: indent(0),
805
+ background: isActive ? "var(--surface-2)" : undefined,
806
+ borderLeftWidth: 2,
807
+ borderLeftStyle: "solid",
808
+ borderLeftColor: isActive ? "var(--accent)" : "transparent",
809
+ })}
810
+ className="tree-row flex items-center gap-1.5 py-[3px] pr-2 t-micro transition-colors hover:bg-surface"
811
+ >
812
+ <TriangleAlert
813
+ size={14}
814
+ aria-hidden
815
+ className="block shrink-0"
816
+ style={{ color: found.length === 0 ? "var(--fg-muted)" : colour }}
817
+ />
818
+ <span className="truncate">Problems</span>
819
+ {found.length === 0 ? (
820
+ <Check
821
+ size={13}
822
+ aria-hidden
823
+ className="ml-auto block shrink-0 text-muted"
824
+ />
825
+ ) : (
826
+ <span
827
+ className="mono tnum ml-auto shrink-0"
828
+ style={{ color: colour }}
829
+ >
830
+ {found.length}
831
+ </span>
832
+ )}
833
+ </NavLink>
834
+ <NavLink
835
+ to={paths.settings()}
836
+ end
837
+ data-nav-item
838
+ title="Settings — projects, plugins and appearance"
839
+ style={({ isActive }) => ({
840
+ paddingLeft: indent(0),
841
+ background: isActive ? "var(--surface-2)" : undefined,
842
+ borderLeftWidth: 2,
843
+ borderLeftStyle: "solid",
844
+ borderLeftColor: isActive ? "var(--accent)" : "transparent",
845
+ })}
846
+ className="tree-row flex items-center gap-1.5 py-[3px] pr-2 text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
847
+ >
848
+ <Settings2 size={14} aria-hidden className="block shrink-0" />
849
+ <span className="truncate">Settings</span>
850
+ </NavLink>
851
+ </div>
852
+ );
853
+ }
854
+
855
+ /**
856
+ * What is left of the tree at 48px. Not a menu: every button here does the one
857
+ * thing the rail can honestly offer, which is to give the tree its width back
858
+ * and land the reader on the section they pointed at.
859
+ */
860
+ function IconRail({ onExpand }: { onExpand: () => void }) {
861
+ const sections: { key: string; kind: Kind; label: string }[] = [
862
+ { key: "flows", kind: "flow", label: "Flows" },
863
+ { key: "domains", kind: "context", label: "Contexts" },
864
+ // Conditional for the same reason the band is: at 48px a button that opens
865
+ // an empty section is worse than no button.
866
+ ...(allModules(catalog).length > 0
867
+ ? [{ key: "registry", kind: "module" as Kind, label: "Registry" }]
868
+ : []),
869
+ { key: "adrs", kind: "adr", label: "Decisions" },
870
+ ];
871
+ return (
872
+ <nav
873
+ className="flex h-full flex-col items-center gap-1 border-r py-3 border-line bg-canvas"
874
+ aria-label="Catalog (collapsed)"
875
+ >
876
+ {/* The mark survives the collapse; the wordmark does not fit and is not
877
+ missed - at 48px the rose is the whole identity. */}
878
+ <CompassRose size={18} className="mb-1 text-ink" />
879
+ <button
880
+ type="button"
881
+ onClick={onExpand}
882
+ title="Expand the catalog"
883
+ aria-label="Expand the catalog"
884
+ aria-expanded={false}
885
+ className="flex size-8 items-center justify-center rounded-control text-muted t-micro transition-colors hover:bg-surface hover:text-ink"
886
+ >
887
+ <PanelLeftOpen size={16} aria-hidden />
888
+ </button>
889
+ <span aria-hidden className="my-1 h-px w-6 bg-line" />
890
+ {sections.map(({ key, kind, label }) => (
891
+ <button
892
+ key={key}
893
+ type="button"
894
+ onClick={onExpand}
895
+ title={`${label} — expand the catalog`}
896
+ aria-label={`${label} — expand the catalog`}
897
+ className="flex size-8 items-center justify-center rounded-control t-micro transition-colors hover:bg-surface"
898
+ >
899
+ <KindIcon kind={kind} size={16} />
900
+ </button>
901
+ ))}
902
+ <NavLink
903
+ to={paths.settings()}
904
+ title="Settings"
905
+ aria-label="Settings"
906
+ className={({ isActive }) =>
907
+ `mt-auto flex size-8 items-center justify-center rounded-control t-micro transition-colors hover:bg-surface ${
908
+ isActive ? "bg-surface text-accent" : "text-muted hover:text-ink"
909
+ }`
910
+ }
911
+ >
912
+ <Settings2 size={16} aria-hidden />
913
+ </NavLink>
914
+ </nav>
915
+ );
916
+ }
917
+
918
+ // ---------------------------------------------------------------------------
919
+
920
+ export function Sidebar({
921
+ railed = false,
922
+ onExpand,
923
+ }: {
924
+ /** True once the shell panel has been collapsed to the 48px rail. */
925
+ railed?: boolean;
926
+ onExpand?: () => void;
927
+ }) {
928
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
929
+
930
+ // Which bands and which owner groups are folded. Both outlive the session:
931
+ // they are a reader's standing answer to "I do not work on that", unlike the
932
+ // branch state below, which follows whatever is selected.
933
+ const [sections, setSections] = useState<Flags>(() =>
934
+ readFlags(SECTIONS_KEY),
935
+ );
936
+ const [groups, setGroups] = useState<Flags>(() => readFlags(FLOW_GROUPS_KEY));
937
+
938
+ const sectionOpen = (key: string): boolean => sections[key] ?? true;
939
+ const toggleSection = (key: string) =>
940
+ setSections((prev) => {
941
+ const next = { ...prev, [key]: !(prev[key] ?? true) };
942
+ writeFlags(SECTIONS_KEY, next);
943
+ return next;
944
+ });
945
+ const groupOpen = (key: string): boolean => groups[key] ?? true;
946
+ const toggleGroup = (key: string) =>
947
+ setGroups((prev) => {
948
+ const next = { ...prev, [key]: !(prev[key] ?? true) };
949
+ writeFlags(FLOW_GROUPS_KEY, next);
950
+ return next;
951
+ });
952
+
953
+ const isOpen = (key: string, def: boolean): boolean => collapsed[key] ?? def;
954
+ const toggle = (key: string, def: boolean) =>
955
+ setCollapsed((c) => ({ ...c, [key]: !(c[key] ?? def) }));
956
+ const flowGroups = useMemo(() => groupFlowsByOwner(catalog.flows), []);
957
+ const flowCount = flowGroups.reduce((n, g) => n + g.entries.length, 0);
958
+
959
+ // One row per vocabulary, not per word: thirty-eight terms in the tree would
960
+ // be a dictionary nobody scrolls past to reach the model. The row opens the
961
+ // page already filtered to that context.
962
+ const vocabs = useMemo(() => vocabularies(catalog), []);
963
+ const termCount = vocabs.reduce((n, v) => n + v.terms.length, 0);
964
+
965
+ // Core first, then supporting, then generic, then the ones nobody has rated;
966
+ // alphabetical inside each. This is the only ordering classification does.
967
+ const contexts = useMemo(
968
+ () =>
969
+ [...catalog.contexts]
970
+ .sort(
971
+ (a, b) =>
972
+ classificationRank(a.classification) -
973
+ classificationRank(b.classification) ||
974
+ a.name.localeCompare(b.name),
975
+ )
976
+ .map(contextRows),
977
+ [],
978
+ );
979
+
980
+ // The schema modules. Nothing at all when the estate has never published a
981
+ // proto, which is what keeps the band from appearing empty.
982
+ const modules = useMemo(() => allModules(catalog), []);
983
+
984
+ // A standing list of what currently holds; every decision is on its page.
985
+ const adrs = useMemo(() => newestAccepted(catalog, 5), []);
986
+
987
+ // --- following the selection --------------------------------------------
988
+
989
+ const selection = useSelectionStore((s) => s.selection);
990
+ const scrollerRef = useRef<HTMLDivElement | null>(null);
991
+
992
+ // The arrow keys, on whatever row has focus. The rows describe themselves
993
+ // - depth, and open or closed for a branch - and `treeKey` says what the
994
+ // key does; this only reads the rows off the DOM and carries it out. A
995
+ // branch's toggle is the chevron beside its link; a group toggles itself.
996
+ const onTreeKey = useCallback((e: ReactKeyboardEvent<HTMLDivElement>) => {
997
+ if (!TREE_KEYS.has(e.key)) return;
998
+ if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
999
+ const scroller = scrollerRef.current;
1000
+ if (!scroller) return;
1001
+ const rows = treeRowsOf(scroller);
1002
+ const at = rows.indexOf(document.activeElement as HTMLElement);
1003
+ const move = treeKey(rows.map(describeRow), at, e.key);
1004
+ if (!move) return;
1005
+ e.preventDefault();
1006
+ const row = rows[move.index];
1007
+ if (!row) return;
1008
+ if (move.type === "focus") {
1009
+ row.focus();
1010
+ row.scrollIntoView({ block: "nearest" });
1011
+ } else {
1012
+ toggleOf(row)?.click();
1013
+ }
1014
+ }, []);
1015
+
1016
+ // Opening the ancestors is not optional. A selection made on a diagram or in
1017
+ // the palette has to be findable in the tree, and a tree row inside three
1018
+ // collapsed parents may as well not exist.
1019
+ useEffect(() => {
1020
+ if (!selection) return;
1021
+ const resolved = resolveSelection(selection.id);
1022
+ if (!resolved) return;
1023
+
1024
+ const keys: string[] = [];
1025
+ if ("context" in resolved) keys.push(`c:${resolved.context.id}`);
1026
+ if ("service" in resolved) keys.push(`s:${resolved.service.id}`);
1027
+ if ("aggregate" in resolved) keys.push(`a:${resolved.aggregate.id}`);
1028
+ if (resolved.kind === "event") {
1029
+ keys.push(`a:${resolved.aggregate.id}:event`);
1030
+ }
1031
+ // A store sits behind two closed groups by default, and a table behind
1032
+ // three; a row nobody can see is a selection nobody can follow.
1033
+ if ("store" in resolved) {
1034
+ keys.push(`s:${resolved.service.id}:data`, `st:${resolved.store.id}`);
1035
+ }
1036
+ // A module has no ancestor to open - it sits at the top of its own band -
1037
+ // so only the band itself has to be unfolded for the row to be findable.
1038
+ const band = resolved.kind === "module" ? "registry" : "contexts";
1039
+
1040
+ if (keys.length === 0 && band === "contexts") return;
1041
+
1042
+ // The band itself can be folded, and folding it hides every ancestor at
1043
+ // once - so the section is opened first, then the branches inside it.
1044
+ setSections((prev) => {
1045
+ if (prev[band] !== false) return prev;
1046
+ const next = { ...prev, [band]: true };
1047
+ writeFlags(SECTIONS_KEY, next);
1048
+ return next;
1049
+ });
1050
+
1051
+ if (keys.length === 0) return;
1052
+
1053
+ setCollapsed((c) => {
1054
+ // Only skip when every ancestor is *explicitly* open. An unset key means
1055
+ // "still on its default", which for an aggregate is closed.
1056
+ if (keys.every((k) => c[k] === true)) return c;
1057
+ const next = { ...c };
1058
+ for (const k of keys) next[k] = true;
1059
+ return next;
1060
+ });
1061
+ }, [selection]);
1062
+
1063
+ // Scroll only when the row is actually out of sight: yanking the tree under
1064
+ // a reader who can already see the row is worse than doing nothing.
1065
+ //
1066
+ // Measured one panel duration later, not now: "reveal" opens the row's
1067
+ // ancestors in the same update, and a branch takes that long to unfold to
1068
+ // its height. Measured at once, the row is inside a box still growing, and
1069
+ // where it is is not where it will be.
1070
+ useEffect(() => {
1071
+ const timer = setTimeout(() => {
1072
+ const scroller = scrollerRef.current;
1073
+ if (!selection || !scroller) return;
1074
+ const row = scroller.querySelector<HTMLElement>(
1075
+ `[data-sel="${CSS.escape(selection.id)}"]`,
1076
+ );
1077
+ if (!row) return;
1078
+ const a = row.getBoundingClientRect();
1079
+ const b = scroller.getBoundingClientRect();
1080
+ if (a.top >= b.top && a.bottom <= b.bottom) return;
1081
+ row.scrollIntoView({ block: "nearest", behavior: "smooth" });
1082
+ }, DURATION.panel * 1000);
1083
+ return () => clearTimeout(timer);
1084
+ }, [selection, collapsed, sections, groups]);
1085
+
1086
+ if (railed) return <IconRail onExpand={() => onExpand?.()} />;
1087
+
1088
+ // Namespaces the selection light's layoutId to this tree.
1089
+ const treeId = useId();
1090
+
1091
+ return (
1092
+ <nav
1093
+ className="flex h-full flex-col border-r border-line bg-canvas"
1094
+ aria-label="Catalog"
1095
+ >
1096
+ <LayoutGroup id={treeId}>
1097
+ {/* One row: the mark and the name. Searching lives in ⌘K, so nothing
1098
+ else competes for the one pane that is always on screen. */}
1099
+ <div className="shrink-0 border-b px-3 pt-2.5 pb-2 border-line">
1100
+ <Wordmark />
1101
+ </div>
1102
+
1103
+ <div
1104
+ ref={scrollerRef}
1105
+ data-nav-list
1106
+ onKeyDown={onTreeKey}
1107
+ className="pane flex-1 overflow-y-auto pb-3"
1108
+ >
1109
+ <PinnedSection
1110
+ open={sectionOpen("pinned")}
1111
+ onToggle={() => toggleSection("pinned")}
1112
+ />
1113
+
1114
+ <Section
1115
+ title="Flows"
1116
+ count={flowCount}
1117
+ open={sectionOpen("flows")}
1118
+ onToggle={() => toggleSection("flows")}
1119
+ >
1120
+ {flowGroups.length === 0 ? (
1121
+ <TreeNote>none charted yet</TreeNote>
1122
+ ) : null}
1123
+ {flowGroups.map((group) => {
1124
+ const key = group.owner ?? "unowned";
1125
+ return (
1126
+ <FlowGroupNode
1127
+ key={key}
1128
+ group={group}
1129
+ open={groupOpen(key)}
1130
+ onToggle={() => toggleGroup(key)}
1131
+ />
1132
+ );
1133
+ })}
1134
+ </Section>
1135
+
1136
+ <Section
1137
+ title="Contexts"
1138
+ count={contexts.length}
1139
+ open={sectionOpen("contexts")}
1140
+ onToggle={() => toggleSection("contexts")}
1141
+ >
1142
+ {contexts.length === 0 ? (
1143
+ <TreeNote>
1144
+ nothing extracted yet
1145
+ </TreeNote>
1146
+ ) : null}
1147
+ {contexts.map(({ context, services }) => {
1148
+ const ckey = `c:${context.id}`;
1149
+ const copen = isOpen(ckey, true);
1150
+ const unresolved = contextStats(context).unresolved;
1151
+ return (
1152
+ <div key={context.id}>
1153
+ <Branch
1154
+ to={paths.context(context.id)}
1155
+ depth={0}
1156
+ open={copen}
1157
+ onToggle={() => toggle(ckey, true)}
1158
+ label={`context ${context.id}`}
1159
+ selId={context.id}
1160
+ end={
1161
+ <>
1162
+ {/* Its own target, because it goes somewhere else: the
1163
+ reader clicked the count, and the count is a list. */}
1164
+ {unresolved > 0 ? (
1165
+ <Link
1166
+ to={`${paths.problems()}?context=${encodeURIComponent(context.id)}`}
1167
+ title={`${unresolved} unresolved ${plural(unresolved, "edge")} — open Problems`}
1168
+ className="mono tnum flex shrink-0 items-center px-1.5 text-unresolved hover:underline"
1169
+ >
1170
+ {unresolved}
1171
+ </Link>
1172
+ ) : null}
1173
+ <span className="ml-auto" />
1174
+ {/* Only worth saying while the services are not on
1175
+ screen; once the branch is open the reader can count
1176
+ them. */}
1177
+ {!copen ? (
1178
+ <span className="mono flex shrink-0 items-center pr-1.5 text-muted opacity-0 t-micro transition-opacity group-hover:opacity-100">
1179
+ {context.services.length}{" "}
1180
+ {plural(context.services.length, "service")}
1181
+ </span>
1182
+ ) : null}
1183
+ </>
1184
+ }
1185
+ right={
1186
+ context.classification ? (
1187
+ <span
1188
+ className="sb-class"
1189
+ title={CLASSIFICATION_NOTE[context.classification]}
1190
+ >
1191
+ {context.classification}
1192
+ </span>
1193
+ ) : null
1194
+ }
1195
+ >
1196
+ <KindIcon kind="context" contextId={context.id} />
1197
+ <span className="truncate" title={context.id}>
1198
+ {context.name}
1199
+ </span>
1200
+ </Branch>
1201
+ <Unfold open={copen}>
1202
+ {services.map(
1203
+ ({ service, aggregates, endpoints, stores }) => {
1204
+ const skey = `s:${service.id}`;
1205
+ const sopen = isOpen(skey, true);
1206
+ return (
1207
+ <div key={service.id}>
1208
+ <Branch
1209
+ to={paths.service(context.id, service.slug)}
1210
+ depth={1}
1211
+ open={sopen}
1212
+ onToggle={() => toggle(skey, true)}
1213
+ label={`service ${service.id}`}
1214
+ selId={service.id}
1215
+ >
1216
+ <KindIcon kind="service" />
1217
+ <span className="truncate" title={service.id}>
1218
+ {service.name}
1219
+ </span>
1220
+ </Branch>
1221
+ <Unfold open={sopen}>
1222
+ {aggregates.map((rows) => (
1223
+ <AggregateNode
1224
+ key={rows.aggregate.id}
1225
+ rows={rows}
1226
+ contextId={context.id}
1227
+ serviceSlug={service.slug}
1228
+ isOpen={isOpen}
1229
+ toggle={toggle}
1230
+ />
1231
+ ))}
1232
+ {/* Between the model and where it is kept: an
1233
+ endpoint is how the outside reaches the first and
1234
+ eventually moves the second. Methods are listed
1235
+ flat rather than nested under their interface -
1236
+ an operationId is unique across a document, so
1237
+ the extra level would carry no information and
1238
+ cost a line of indent in a narrow tree. */}
1239
+ {endpoints.length > 0 ? (
1240
+ <Group
1241
+ kind="endpoint"
1242
+ label="api"
1243
+ count={endpoints.reduce(
1244
+ (n, e) => n + e.methods.length,
1245
+ 0,
1246
+ )}
1247
+ depth={2}
1248
+ open={isOpen(`${skey}:api`, false)}
1249
+ onToggle={() => toggle(`${skey}:api`, false)}
1250
+ >
1251
+ {endpoints.flatMap(({ provided, methods }) =>
1252
+ methods.map((method) => (
1253
+ <Leaf
1254
+ key={`${provided.id}/${method.name}`}
1255
+ to={`${paths.service(context.id, service.slug)}?tab=provides`}
1256
+ depth={3}
1257
+ title={`${provided.id}/${method.name} — ${provided.source}`}
1258
+ >
1259
+ <KindIcon kind="endpoint" />
1260
+ <span className="mono truncate">
1261
+ {method.name}
1262
+ </span>
1263
+ </Leaf>
1264
+ )),
1265
+ )}
1266
+ </Group>
1267
+ ) : null}
1268
+
1269
+ {/* After the aggregates, because the model comes
1270
+ before where it is kept. Closed by default: this
1271
+ is the answer to a question about deployment, not
1272
+ the one the tree is usually open for. */}
1273
+ <Group
1274
+ kind="table"
1275
+ label="data"
1276
+ count={stores.length}
1277
+ depth={2}
1278
+ open={isOpen(`${skey}:data`, false)}
1279
+ onToggle={() => toggle(`${skey}:data`, false)}
1280
+ >
1281
+ {stores.map(({ store, tables, views }) => (
1282
+ <StoreNode
1283
+ key={store.id}
1284
+ store={store}
1285
+ tables={tables}
1286
+ views={views}
1287
+ contextId={context.id}
1288
+ serviceSlug={service.slug}
1289
+ isOpen={isOpen}
1290
+ toggle={toggle}
1291
+ />
1292
+ ))}
1293
+ </Group>
1294
+ </Unfold>
1295
+ </div>
1296
+ );
1297
+ },
1298
+ )}
1299
+ </Unfold>
1300
+ </div>
1301
+ );
1302
+ })}
1303
+ </Section>
1304
+
1305
+ {/* Guarded on the count rather than drawn empty: `Section` renders its
1306
+ header and a TreeNote at zero, so an estate that has never published
1307
+ a proto would grow a permanent dead row. */}
1308
+ {modules.length > 0 ? (
1309
+ <Section
1310
+ title="Registry"
1311
+ count={modules.length}
1312
+ open={sectionOpen("registry")}
1313
+ onToggle={() => toggleSection("registry")}
1314
+ >
1315
+ {modules.map((module) => {
1316
+ const open = isOpen(`mod:${module.id}`, false);
1317
+
1318
+ return (
1319
+ <div key={module.id}>
1320
+ <Branch
1321
+ to={paths.module(module.slug)}
1322
+ depth={0}
1323
+ selId={module.id}
1324
+ label={`module ${module.name}`}
1325
+ open={open}
1326
+ onToggle={() => toggle(`mod:${module.id}`, false)}
1327
+ right={
1328
+ module.commit ? null : (
1329
+ <span
1330
+ className="mono text-muted"
1331
+ title="tracked by label rather than pinned to a commit"
1332
+ >
1333
+ ~
1334
+ </span>
1335
+ )
1336
+ }
1337
+ >
1338
+ <KindIcon kind="module" />
1339
+ <span className="mono truncate" title={module.id}>
1340
+ {module.name}
1341
+ </span>
1342
+ </Branch>
1343
+
1344
+ {/* Two levels only. The interfaces inside a module are
1345
+ already in the tree under the service that answers on
1346
+ them, and listing them again would say the estate has
1347
+ twice as many. */}
1348
+ {open
1349
+ ? module.packages.map((name) => (
1350
+ <Leaf
1351
+ key={name}
1352
+ to={`${paths.module(module.slug)}?tab=interfaces#${packageAnchor(name)}`}
1353
+ depth={1}
1354
+ title={`${name} — ${module.id}`}
1355
+ >
1356
+ <KindIcon kind="endpoint" />
1357
+ <span className="mono truncate">{name}</span>
1358
+ </Leaf>
1359
+ ))
1360
+ : null}
1361
+ </div>
1362
+ );
1363
+ })}
1364
+ <NavLink
1365
+ to={paths.registry()}
1366
+ data-nav-item
1367
+ className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
1368
+ >
1369
+ view all {allModules(catalog).length} →
1370
+ </NavLink>
1371
+ </Section>
1372
+ ) : null}
1373
+
1374
+ <Section
1375
+ title="Decisions"
1376
+ count={adrs.length}
1377
+ open={sectionOpen("decisions")}
1378
+ onToggle={() => toggleSection("decisions")}
1379
+ >
1380
+ {adrs.length === 0 ? (
1381
+ <TreeNote>
1382
+ nothing on the record yet
1383
+ </TreeNote>
1384
+ ) : null}
1385
+ {adrs.map((adr) => (
1386
+ <Leaf
1387
+ key={adr.id}
1388
+ to={paths.adr(adr.slug)}
1389
+ depth={0}
1390
+ title={`${adr.id} — ${adr.title}`}
1391
+ >
1392
+ <KindIcon kind="adr" />
1393
+ {/* The number without its prefix: the header already said
1394
+ DECISIONS, and "ADR-" repeated down a column is a word the
1395
+ reader reads five times to get to the digits. */}
1396
+ <span
1397
+ className={`mono tnum shrink-0 ${isStruck(adr.status) ? "line-through text-muted" : ""}`}
1398
+ >
1399
+ {adrNumber(adr).replace(/^ADR-/, "")}
1400
+ </span>
1401
+ <span className="truncate">{adr.title}</span>
1402
+ </Leaf>
1403
+ ))}
1404
+ {catalog.adrs.length > 0 ? (
1405
+ <NavLink
1406
+ to={paths.adrs()}
1407
+ data-nav-item
1408
+ className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
1409
+ >
1410
+ view all {catalog.adrs.length} →
1411
+ </NavLink>
1412
+ ) : null}
1413
+ </Section>
1414
+
1415
+ {/* Guarded on the count, like Registry: an estate that has written no
1416
+ glossary would otherwise grow a permanent empty band teaching a
1417
+ feature by taking up room. */}
1418
+ {allTerms(catalog).length > 0 ? (
1419
+ <Section
1420
+ title="Language"
1421
+ count={termCount}
1422
+ open={sectionOpen("language")}
1423
+ onToggle={() => toggleSection("language")}
1424
+ >
1425
+ {vocabs.map((vocabulary) => (
1426
+ <Leaf
1427
+ key={vocabulary.contextId}
1428
+ to={paths.contextLanguage(vocabulary.contextId)}
1429
+ depth={0}
1430
+ title={`${vocabulary.context?.name ?? vocabulary.contextId} — one meaning per word inside this context`}
1431
+ >
1432
+ <KindIcon kind="context" contextId={vocabulary.contextId} />
1433
+ <span className="truncate">{vocabulary.contextId}</span>
1434
+ <span className="sb-count">{vocabulary.terms.length}</span>
1435
+ </Leaf>
1436
+ ))}
1437
+ <NavLink
1438
+ to={paths.language()}
1439
+ data-nav-item
1440
+ className="tree-row mono flex items-center py-[3px] pr-2 pl-[8px] text-accent hover:bg-surface"
1441
+ >
1442
+ view all {allTerms(catalog).length} →
1443
+ </NavLink>
1444
+ </Section>
1445
+ ) : null}
1446
+ </div>
1447
+
1448
+ <BottomGroup />
1449
+ </LayoutGroup>
1450
+ </nav>
1451
+ );
1452
+ }
1453
+
1454
+ /**
1455
+ * A store and the tables in it. The store row navigates to its own ER page; the
1456
+ * table rows land on that page with the table already selected, because a table
1457
+ * read outside the picture of what points at it is a list of column names.
1458
+ */
1459
+ function StoreNode({
1460
+ store,
1461
+ tables,
1462
+ views,
1463
+ contextId,
1464
+ serviceSlug,
1465
+ isOpen,
1466
+ toggle,
1467
+ }: {
1468
+ store: Store;
1469
+ tables: Table[];
1470
+ views: View[];
1471
+ contextId: string;
1472
+ serviceSlug: string;
1473
+ isOpen: (key: string, def: boolean) => boolean;
1474
+ toggle: (key: string, def: boolean) => void;
1475
+ }) {
1476
+ const key = `st:${store.id}`;
1477
+ const open = isOpen(key, false);
1478
+ const to = paths.store(contextId, serviceSlug, store.slug);
1479
+
1480
+ return (
1481
+ <div>
1482
+ <Branch
1483
+ to={to}
1484
+ depth={3}
1485
+ open={open}
1486
+ onToggle={() => toggle(key, false)}
1487
+ label={`store ${store.id}`}
1488
+ selId={store.id}
1489
+ right={
1490
+ <span
1491
+ className="mono flex items-center gap-1 text-muted"
1492
+ title={STORE_KIND_LABEL[store.kind]}
1493
+ >
1494
+ <StoreKindMark kind={store.kind} size={12} />
1495
+ {store.kind}
1496
+ </span>
1497
+ }
1498
+ >
1499
+ <KindIcon kind="store" />
1500
+ <span className="truncate" title={store.id}>
1501
+ {store.name}
1502
+ </span>
1503
+ </Branch>
1504
+ {/* Tables and views keep the monospace: unlike everything above them
1505
+ they are not human names but the identifiers a migration writes and a
1506
+ query names, and reading one in prose type invites a typo. */}
1507
+ {open
1508
+ ? tables.map((table) => (
1509
+ <Leaf
1510
+ key={table.id}
1511
+ to={to}
1512
+ depth={4}
1513
+ title={table.doc ?? table.id}
1514
+ selId={table.id}
1515
+ >
1516
+ <KindIcon kind="table" />
1517
+ <span className="mono truncate">{table.name}</span>
1518
+ {table.role && table.role !== "other" ? (
1519
+ <span className="mono ml-auto shrink-0 text-muted">
1520
+ {table.role === "aggregate-root" ? "root" : table.role}
1521
+ </span>
1522
+ ) : null}
1523
+ </Leaf>
1524
+ ))
1525
+ : null}
1526
+ {/* Views after the tables, always: they are computed from them, and a
1527
+ tree that interleaved the two would put a reading of the rows above
1528
+ the place the rows live. */}
1529
+ {open
1530
+ ? views.map((view) => (
1531
+ <Leaf
1532
+ key={view.id}
1533
+ to={to}
1534
+ depth={4}
1535
+ title={view.doc ?? view.id}
1536
+ selId={view.id}
1537
+ >
1538
+ <KindIcon kind="view" />
1539
+ <span className="mono truncate">{view.name}</span>
1540
+ <span className="mono ml-auto shrink-0 text-muted">
1541
+ {view.materialized ? "matview" : "view"}
1542
+ </span>
1543
+ </Leaf>
1544
+ ))
1545
+ : null}
1546
+ </div>
1547
+ );
1548
+ }
1549
+
1550
+ function AggregateNode({
1551
+ rows,
1552
+ contextId,
1553
+ serviceSlug,
1554
+ isOpen,
1555
+ toggle,
1556
+ }: {
1557
+ rows: AggregateRows;
1558
+ contextId: string;
1559
+ serviceSlug: string;
1560
+ isOpen: (key: string, def: boolean) => boolean;
1561
+ toggle: (key: string, def: boolean) => void;
1562
+ }) {
1563
+ const { aggregate } = rows;
1564
+ const akey = `a:${aggregate.id}`;
1565
+ const aopen = isOpen(akey, false);
1566
+ const to = paths.aggregate(contextId, serviceSlug, aggregate.slug);
1567
+
1568
+ // Events are the group that opens by itself: they are what other contexts
1569
+ // actually depend on. The structural groups stay shut until asked for.
1570
+ const group = (kind: LeafKind, def: boolean) => ({
1571
+ kind,
1572
+ open: isOpen(`${akey}:${kind}`, def),
1573
+ onToggle: () => toggle(`${akey}:${kind}`, def),
1574
+ });
1575
+
1576
+ const blockLeaf = (kind: "vo" | "entity", block: Block) => {
1577
+ const path =
1578
+ kind === "vo"
1579
+ ? paths.valueObject(contextId, serviceSlug, aggregate.slug, block.slug)
1580
+ : paths.entity(contextId, serviceSlug, aggregate.slug, block.slug);
1581
+ return (
1582
+ <Leaf key={block.id} to={path} depth={4} title={block.doc ?? block.id}>
1583
+ <KindIcon kind={kind} />
1584
+ <span className="mono truncate">{block.name}</span>
1585
+ {kind === "entity" && block.name === aggregate.root ? (
1586
+ <span className="mono ml-auto shrink-0 text-muted">root</span>
1587
+ ) : null}
1588
+ </Leaf>
1589
+ );
1590
+ };
1591
+
1592
+ return (
1593
+ <div>
1594
+ <Branch
1595
+ to={to}
1596
+ depth={2}
1597
+ open={aopen}
1598
+ onToggle={() => toggle(akey, false)}
1599
+ label={`aggregate ${aggregate.name}`}
1600
+ selId={aggregate.id}
1601
+ under={
1602
+ <div
1603
+ className="mono truncate pr-2 text-muted"
1604
+ style={{ paddingLeft: indent(3), fontSize: 11 }}
1605
+ title={`aggregate root: ${aggregate.root}`}
1606
+ >
1607
+ root: {aggregate.root}
1608
+ </div>
1609
+ }
1610
+ right={
1611
+ aggregate.events.length === 0 ? (
1612
+ <span
1613
+ className="mono text-muted"
1614
+ title="this aggregate publishes no events"
1615
+ >
1616
+ 0
1617
+ </span>
1618
+ ) : null
1619
+ }
1620
+ >
1621
+ <KindIcon kind="aggregate" />
1622
+ <span className="truncate font-medium" title={aggregate.id}>
1623
+ {aggregate.name}
1624
+ </span>
1625
+ </Branch>
1626
+
1627
+ <Unfold open={aopen}>
1628
+ <>
1629
+ <Group
1630
+ {...group("vo", false)}
1631
+ count={rows.valueObjects.length}
1632
+ depth={3}
1633
+ >
1634
+ {rows.valueObjects.map((b) => blockLeaf("vo", b))}
1635
+ </Group>
1636
+
1637
+ <Group
1638
+ {...group("entity", false)}
1639
+ count={rows.entities.length}
1640
+ depth={3}
1641
+ >
1642
+ {rows.entities.map((b) => blockLeaf("entity", b))}
1643
+ </Group>
1644
+
1645
+ <Group
1646
+ {...group("enum", false)}
1647
+ count={rows.enums.length}
1648
+ depth={3}
1649
+ >
1650
+ {rows.enums.map((item) => (
1651
+ <Leaf
1652
+ key={item.id}
1653
+ to={paths.enum(contextId, serviceSlug, aggregate.slug, item.slug)}
1654
+ depth={4}
1655
+ title={item.doc || item.id}
1656
+ >
1657
+ <KindIcon kind="enum" />
1658
+ <span className="mono truncate">{item.name}</span>
1659
+ <span className="mono ml-auto shrink-0 text-muted">
1660
+ {item.values.length}
1661
+ </span>
1662
+ </Leaf>
1663
+ ))}
1664
+ </Group>
1665
+
1666
+ <Group
1667
+ {...group("event", true)}
1668
+ count={rows.events.length}
1669
+ depth={3}
1670
+ >
1671
+ {rows.events.map((event) => {
1672
+ const latest = event.versions[event.versions.length - 1];
1673
+ return (
1674
+ <Leaf
1675
+ key={event.id}
1676
+ to={paths.event(
1677
+ contextId,
1678
+ serviceSlug,
1679
+ aggregate.slug,
1680
+ event.slug,
1681
+ )}
1682
+ depth={4}
1683
+ title={event.id}
1684
+ selId={event.id}
1685
+ >
1686
+ <KindIcon kind="event" />
1687
+ <span
1688
+ className="mono truncate"
1689
+ style={{ color: "var(--kind-event)" }}
1690
+ >
1691
+ {event.name}
1692
+ </span>
1693
+ {latest ? (
1694
+ <span className="mono ml-auto shrink-0 border px-1 border-line text-muted">
1695
+ {latest.version}
1696
+ </span>
1697
+ ) : null}
1698
+ </Leaf>
1699
+ );
1700
+ })}
1701
+ </Group>
1702
+
1703
+ <Group
1704
+ {...group("command", false)}
1705
+ count={rows.commands.length}
1706
+ depth={3}
1707
+ >
1708
+ {rows.commands.map((op) => (
1709
+ <Leaf
1710
+ key={op.id}
1711
+ to={`${to}#bb-commands`}
1712
+ depth={4}
1713
+ title={op.id}
1714
+ >
1715
+ <KindIcon kind="command" />
1716
+ <span className="mono truncate text-muted">{op.id}</span>
1717
+ </Leaf>
1718
+ ))}
1719
+ </Group>
1720
+
1721
+ <Group
1722
+ {...group("query", false)}
1723
+ count={rows.queries.length}
1724
+ depth={3}
1725
+ >
1726
+ {rows.queries.map((op) => (
1727
+ <Leaf
1728
+ key={op.id}
1729
+ to={`${to}#bb-queries`}
1730
+ depth={4}
1731
+ title={op.id}
1732
+ >
1733
+ <KindIcon kind="query" />
1734
+ <span className="mono truncate text-muted">{op.id}</span>
1735
+ </Leaf>
1736
+ ))}
1737
+ </Group>
1738
+ </>
1739
+ </Unfold>
1740
+ </div>
1741
+ );
1742
+ }