@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,1215 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { rawCatalog as raw } from "./test-catalog";
3
+ import {
4
+ CatalogError,
5
+ allComponents,
6
+ allAggregates,
7
+ allEvents,
8
+ blockFields,
9
+ buildIndex,
10
+ componentKind,
11
+ groupKind,
12
+ rootEntity,
13
+ stepConditions,
14
+ stepFrames,
15
+ validateCatalog,
16
+ walkSteps,
17
+ } from "./catalog";
18
+ import type { Adr, Alt, Catalog, Classification, Flow } from "./catalog";
19
+
20
+ const catalog = raw as unknown as Catalog;
21
+
22
+ function clone(): Catalog {
23
+ return JSON.parse(JSON.stringify(catalog)) as Catalog;
24
+ }
25
+
26
+ describe("validateCatalog", () => {
27
+ it("accepts the shipped catalog", () => {
28
+ expect(() => validateCatalog(catalog)).not.toThrow();
29
+ });
30
+
31
+ it("keeps old catalogs compatible while accepting neutral group and component kinds", () => {
32
+ expect(groupKind(catalog.contexts[0]!)).toBe("bounded-context");
33
+ expect(componentKind(catalog.contexts[0]!.services[0]!)).toBe("service");
34
+ expect(allComponents(catalog)).toEqual(
35
+ catalog.contexts.flatMap((group) => group.services),
36
+ );
37
+
38
+ const neutral = clone();
39
+ neutral.contexts[0]!.kind = "system";
40
+ neutral.contexts[0]!.services[0]!.kind = "application";
41
+ expect(() => validateCatalog(neutral)).not.toThrow();
42
+ });
43
+
44
+ it("rejects unknown neutral kinds", () => {
45
+ const badGroup = clone();
46
+ badGroup.contexts[0]!.kind = "folder" as never;
47
+ expect(() => validateCatalog(badGroup)).toThrow(/has kind "folder"/);
48
+
49
+ const badComponent = clone();
50
+ badComponent.contexts[0]!.services[0]!.kind = "daemon" as never;
51
+ expect(() => validateCatalog(badComponent)).toThrow(/has kind "daemon"/);
52
+ });
53
+
54
+ it("rejects an operation exposed by a method no interface declares", () => {
55
+ const bad = clone();
56
+ const aggregate = bad.contexts[0]?.services[0]?.aggregates[0];
57
+ if (!aggregate) throw new Error("no aggregate to hang the operation on");
58
+ const operation = aggregate.operations[0];
59
+ if (!operation) throw new Error("no operation");
60
+ operation.exposedBy = ["notAMethod"];
61
+
62
+ expect(() => validateCatalog(bad)).toThrow(/exposed by "notAMethod"/);
63
+ });
64
+
65
+ // The pairing is the whole point of the field, so the shipped catalog is
66
+ // held to it rather than only the hand-made counterexample above.
67
+ it("accepts an operation exposed by a method its service really declares", () => {
68
+ const good = clone();
69
+ const service = good.contexts[0]?.services[0];
70
+ const method = service?.provides[0]?.methods[0];
71
+ const operation = service?.aggregates[0]?.operations[0];
72
+ if (!service || !method || !operation) throw new Error("nothing to pair");
73
+ operation.exposedBy = [method.name];
74
+
75
+ expect(() => validateCatalog(good)).not.toThrow();
76
+ });
77
+
78
+ it("rejects an unknown SOAP version", () => {
79
+ const bad = clone();
80
+ const method = bad.contexts
81
+ .flatMap((context) => context.services)
82
+ .flatMap((service) => service.provides)
83
+ .flatMap((provided) => provided.methods)[0];
84
+ if (!method) throw new Error("fixture has no rpc method");
85
+ method.soap = { version: "2.0" as never, action: "urn:pay" };
86
+ expect(() => validateCatalog(bad)).toThrow(/expected 1.1 or 1.2/);
87
+ });
88
+
89
+ it("rejects a step whose participant was never declared, naming flow and step", () => {
90
+ const bad = clone();
91
+ const checkout = bad.flows.find((f) => f.slug === "checkout") as Flow;
92
+ const step = walkSteps(checkout.steps)[0];
93
+ if (!step) throw new Error("fixture has no steps");
94
+ step.to = "warehouse";
95
+ expect(() => validateCatalog(bad)).toThrowError(CatalogError);
96
+ expect(() => validateCatalog(bad)).toThrowError(
97
+ /flow "checkout" step "s1".*warehouse/s,
98
+ );
99
+ });
100
+
101
+ it("rejects an unresolvable ref unless the step is unresolved", () => {
102
+ const bad = clone();
103
+ const flow = bad.flows[0] as Flow;
104
+ const step = walkSteps(flow.steps)[0];
105
+ if (!step) throw new Error("fixture has no steps");
106
+ step.ref = "shop.oms.order.NoSuchEvent";
107
+ expect(() => validateCatalog(bad)).toThrowError(
108
+ /resolves to neither an Event nor an RpcCall/,
109
+ );
110
+ step.status = "unresolved";
111
+ expect(() => validateCatalog(bad)).not.toThrow();
112
+ });
113
+
114
+ it("rejects a field ref that is not a defs key", () => {
115
+ const bad = clone();
116
+ const field =
117
+ bad.contexts[0]?.services[0]?.aggregates[0]?.events[0]?.versions[0]
118
+ ?.fields[0];
119
+ if (!field) throw new Error("fixture has no fields");
120
+ field.ref = "Nonexistent";
121
+ expect(() => validateCatalog(bad)).toThrowError(
122
+ /references unknown def "Nonexistent"/,
123
+ );
124
+ });
125
+
126
+ it("rejects a context whose slug is not its id", () => {
127
+ const bad = clone();
128
+ const context = bad.contexts[0];
129
+ if (!context) throw new Error("fixture has no contexts");
130
+ context.slug = "the-shop";
131
+ expect(() => validateCatalog(bad)).toThrowError(CatalogError);
132
+ expect(() => validateCatalog(bad)).toThrowError(
133
+ /context "shop" has slug "the-shop".*must equal its id/s,
134
+ );
135
+ });
136
+
137
+ it("rejects a classification outside the three it may take", () => {
138
+ const bad = clone();
139
+ const context = bad.contexts[0];
140
+ if (!context) throw new Error("fixture has no contexts");
141
+ context.classification = "strategic" as Classification;
142
+ expect(() => validateCatalog(bad)).toThrowError(
143
+ /classification "strategic"; expected one of core, supporting, generic/,
144
+ );
145
+ });
146
+
147
+ it("accepts a context that states no classification at all", () => {
148
+ const bad = clone();
149
+ const context = bad.contexts[0];
150
+ if (!context) throw new Error("fixture has no contexts");
151
+ delete context.classification;
152
+ expect(() => validateCatalog(bad)).not.toThrow();
153
+ });
154
+
155
+ it("rejects duplicate slugs within a parent", () => {
156
+ const bad = clone();
157
+ const services = bad.contexts[0]?.services;
158
+ if (!services || !services[0] || !services[1])
159
+ throw new Error("fixture too small");
160
+ services[1].slug = services[0].slug;
161
+ services[1].id = services[0].id;
162
+ expect(() => validateCatalog(bad)).toThrowError(
163
+ /service slug .* is not unique/,
164
+ );
165
+ });
166
+ });
167
+
168
+ describe("stepFrames", () => {
169
+ const checkout = catalog.flows.find((f) => f.slug === "checkout") as Flow;
170
+ const frames = stepFrames(checkout.steps);
171
+ const stacks = [...frames.values()];
172
+
173
+ it("covers every step of the flow", () => {
174
+ for (const step of walkSteps(checkout.steps)) {
175
+ expect(frames.has(step.id), step.id).toBe(true);
176
+ }
177
+ });
178
+
179
+ it("gives a step outside every frame an empty stack", () => {
180
+ expect(stacks.some((stack) => stack.length === 0)).toBe(true);
181
+ });
182
+
183
+ it("stacks the frames outermost first, matching the tree's own nesting", () => {
184
+ // A stack is a path down the tree, so every prefix of it must itself be a
185
+ // stack some other step has - otherwise a frame was skipped or reordered.
186
+ const seen = new Set(
187
+ stacks.map((stack) => stack.map((f) => f.id).join(">")),
188
+ );
189
+ for (const stack of stacks) {
190
+ for (let i = 1; i < stack.length; i += 1) {
191
+ const prefix = stack
192
+ .slice(0, i)
193
+ .map((f) => f.id)
194
+ .join(">");
195
+ expect(
196
+ [...seen].some(
197
+ (key) => key === prefix || key.startsWith(`${prefix}>`),
198
+ ),
199
+ prefix,
200
+ ).toBe(true);
201
+ }
202
+ }
203
+ });
204
+
205
+ it("names the branch on an alt frame and the title on a loop or parallel", () => {
206
+ for (const frame of stacks.flat()) {
207
+ if (frame.kind === "alt") expect(frame.branch, frame.id).toBeTruthy();
208
+ if (frame.kind === "loop") expect(frame.title, frame.id).toBeTruthy();
209
+ if (frame.kind === "parallel")
210
+ expect(frame.branch, frame.id).toBeTruthy();
211
+ }
212
+ });
213
+
214
+ it("carries the terminal flag onto every step of the branch that ends", () => {
215
+ const terminal = stacks.flat().filter((f) => f.terminal);
216
+ expect(terminal.length).toBeGreaterThan(0);
217
+ for (const frame of terminal) expect(frame.kind).toBe("alt");
218
+ });
219
+
220
+ it("reports a loop as a frame but never as a condition", () => {
221
+ const withLoop = stacks.find((stack) =>
222
+ stack.some((f) => f.kind === "loop"),
223
+ );
224
+ if (!withLoop) throw new Error("fixture has no loop");
225
+ expect(stepConditions(withLoop).every((f) => f.kind === "alt")).toBe(true);
226
+ expect(stepConditions(withLoop).length).toBeLessThan(withLoop.length);
227
+ });
228
+ });
229
+
230
+ describe("validateCatalog: flow frames", () => {
231
+ /** The first alt of the checkout flow, in a throwaway copy of the catalog. */
232
+ function alt(): { bad: Catalog; node: Alt } {
233
+ const bad = clone();
234
+ const checkout = bad.flows.find((f) => f.slug === "checkout") as Flow;
235
+ const node = checkout.steps.find((n) => n.type === "alt") as Alt;
236
+ if (!node) throw new Error("fixture has no top-level alt in checkout");
237
+ return { bad, node };
238
+ }
239
+
240
+ it("accepts the shipped catalog, terminal branches and all", () => {
241
+ expect(() => validateCatalog(clone())).not.toThrow();
242
+ });
243
+
244
+ it("validates optional flow trigger evidence", () => {
245
+ const good = clone();
246
+ good.flows[0]!.trigger = {
247
+ kind: "message",
248
+ label: "Kafka · records.changed",
249
+ confidence: "high",
250
+ };
251
+ expect(() => validateCatalog(good)).not.toThrow();
252
+
253
+ const badKind = clone();
254
+ badKind.flows[0]!.trigger = { kind: "timer" as never, confidence: "high" };
255
+ expect(() => validateCatalog(badKind)).toThrow(
256
+ /unknown trigger kind "timer"/,
257
+ );
258
+
259
+ const badConfidence = clone();
260
+ badConfidence.flows[0]!.trigger = {
261
+ kind: "startup",
262
+ confidence: "maybe" as never,
263
+ };
264
+ expect(() => validateCatalog(badConfidence)).toThrow(
265
+ /unknown trigger confidence "maybe"/,
266
+ );
267
+ });
268
+
269
+ it("validates cross-protocol composition evidence", () => {
270
+ const good = clone();
271
+ const flow = good.flows[0]!;
272
+ const first = walkSteps(flow.steps)[0]!;
273
+ flow.includes = ["downstream-worker"];
274
+ first.reaches = ["jobs/email:Worker.Work"];
275
+ first.handoff = {
276
+ kind: "message",
277
+ transport: "kafka",
278
+ channel: "email.requested",
279
+ direction: "send",
280
+ };
281
+ expect(() => validateCatalog(good)).not.toThrow();
282
+
283
+ const bad = clone();
284
+ walkSteps(bad.flows[0]!.steps)[0]!.handoff = {
285
+ kind: "job",
286
+ transport: "river",
287
+ channel: "jobs",
288
+ direction: "receive",
289
+ };
290
+ expect(() => validateCatalog(bad)).toThrow(/job handoff with no message/);
291
+ });
292
+
293
+ it("rejects an alt with a single branch", () => {
294
+ const { bad, node } = alt();
295
+ node.branches = node.branches.slice(0, 1);
296
+ expect(() => validateCatalog(bad)).toThrowError(
297
+ /has 1 branch\(es\); an alt states a choice/,
298
+ );
299
+ });
300
+
301
+ it("rejects a branch that states no condition", () => {
302
+ const { bad, node } = alt();
303
+ const branch = node.branches[0];
304
+ if (!branch) throw new Error("fixture has no branches");
305
+ branch.title = "";
306
+ expect(() => validateCatalog(bad)).toThrowError(
307
+ /has a branch with no title/,
308
+ );
309
+ });
310
+
311
+ it("rejects two branches stating the same condition", () => {
312
+ const { bad, node } = alt();
313
+ const [first, second] = node.branches;
314
+ if (!first || !second) throw new Error("fixture has too few branches");
315
+ second.title = first.title;
316
+ expect(() => validateCatalog(bad)).toThrowError(/two branches titled/);
317
+ });
318
+
319
+ it("rejects steps written after an alt whose every branch ends the flow", () => {
320
+ const { bad, node } = alt();
321
+ for (const branch of node.branches) branch.terminal = true;
322
+ expect(() => validateCatalog(bad)).toThrowError(
323
+ /every branch is terminal, so the \d+ node\(s\) after it can never run/,
324
+ );
325
+ });
326
+
327
+ it("rejects a loop that does not say what it repeats until", () => {
328
+ const bad = clone();
329
+ const flow = bad.flows.find((f) => f.slug === "checkout") as Flow;
330
+ const loop = flow.steps.find((n) => n.type === "loop");
331
+ if (!loop || loop.type !== "loop") throw new Error("fixture has no loop");
332
+ loop.title = "";
333
+ expect(() => validateCatalog(bad)).toThrowError(/has no title/);
334
+ });
335
+ });
336
+
337
+ describe("validateCatalog: building blocks", () => {
338
+ function firstAggregate(bad: Catalog) {
339
+ const aggregate = bad.contexts[0]?.services[0]?.aggregates[0];
340
+ if (!aggregate) throw new Error("fixture has no aggregates");
341
+ return aggregate;
342
+ }
343
+
344
+ it("rejects a root that is not one of the aggregate's entities", () => {
345
+ const bad = clone();
346
+ firstAggregate(bad).root = "Basket";
347
+ expect(() => validateCatalog(bad)).toThrowError(
348
+ /aggregate "shop.oms.order" names root "Basket", which is not one of its entities/,
349
+ );
350
+ });
351
+
352
+ it("accepts a lifecycle over its own states and events, and derives nothing else", () => {
353
+ const ok = clone();
354
+ const agg = firstAggregate(ok);
355
+ agg.lifecycle = {
356
+ states: ["open", "placed"],
357
+ transitions: [
358
+ { from: "open", to: "placed", on: "place", emits: agg.events[0]!.id },
359
+ ],
360
+ };
361
+ expect(() => validateCatalog(ok)).not.toThrow();
362
+ });
363
+
364
+ it("rejects a transition into a state the lifecycle does not list", () => {
365
+ const bad = clone();
366
+ firstAggregate(bad).lifecycle = {
367
+ states: ["open"],
368
+ transitions: [{ from: "open", to: "gone", on: "vanish" }],
369
+ };
370
+ expect(() => validateCatalog(bad)).toThrowError(
371
+ /"gone" is not one of its states/,
372
+ );
373
+ });
374
+
375
+ it("rejects a transition emitting an event of another aggregate", () => {
376
+ const bad = clone();
377
+ firstAggregate(bad).lifecycle = {
378
+ states: ["open", "placed"],
379
+ transitions: [
380
+ {
381
+ from: "open",
382
+ to: "placed",
383
+ on: "place",
384
+ emits: "shop.cart.basket.BasketCreated",
385
+ },
386
+ ],
387
+ };
388
+ expect(() => validateCatalog(bad)).toThrowError(
389
+ /which is not one of its events/,
390
+ );
391
+ });
392
+
393
+ it("rejects a block id that disagrees with its slug", () => {
394
+ const bad = clone();
395
+ const vo = firstAggregate(bad).valueObjects[0];
396
+ if (!vo) throw new Error("fixture has no value objects");
397
+ vo.slug = "moolah";
398
+ expect(() => validateCatalog(bad)).toThrowError(
399
+ /must have id "shop.oms.order.moolah"/,
400
+ );
401
+ });
402
+
403
+ it("rejects a block ref that is not a defs key", () => {
404
+ const bad = clone();
405
+ const vo = firstAggregate(bad).valueObjects[0];
406
+ if (!vo) throw new Error("fixture has no value objects");
407
+ vo.ref = "Doubloons";
408
+ expect(() => validateCatalog(bad)).toThrowError(
409
+ /value object "shop.oms.order.money" references unknown def "Doubloons"/,
410
+ );
411
+ });
412
+
413
+ it("rejects a block with neither a shared type nor a shape of its own", () => {
414
+ const bad = clone();
415
+ const vo = firstAggregate(bad).valueObjects[0];
416
+ if (!vo) throw new Error("fixture has no value objects");
417
+ delete vo.ref;
418
+ expect(() => validateCatalog(bad)).toThrowError(
419
+ /has neither a def ref nor any fields of its own/,
420
+ );
421
+ });
422
+
423
+ it("rejects two value objects sharing a slug", () => {
424
+ const bad = clone();
425
+ const vos = firstAggregate(bad).valueObjects;
426
+ if (!vos[0] || !vos[1]) throw new Error("fixture too small");
427
+ vos[1].slug = vos[0].slug;
428
+ vos[1].id = vos[0].id;
429
+ expect(() => validateCatalog(bad)).toThrowError(
430
+ /value object slug "money" is not unique/,
431
+ );
432
+ });
433
+
434
+ it("rejects an rpc message field pointing at no def", () => {
435
+ const bad = clone();
436
+ const message = bad.contexts[0]?.services[0]?.provides[0]?.messages?.[0];
437
+ if (!message) throw new Error("fixture has no rpc messages");
438
+ const field = message.fields[0];
439
+ if (!field) throw new Error("fixture has no rpc message fields");
440
+ field.ref = "Doubloons";
441
+ expect(() => validateCatalog(bad)).toThrowError(
442
+ /rpc message "shop.v1.Orders.PlaceOrderRequest" references unknown def "Doubloons"/,
443
+ );
444
+ });
445
+ });
446
+
447
+ describe("validateCatalog: decision records", () => {
448
+ function adr(bad: Catalog, id: string): Adr {
449
+ const found = bad.adrs.find((a) => a.id === id);
450
+ if (!found) throw new Error(`fixture has no adr ${id}`);
451
+ return found;
452
+ }
453
+
454
+ it("rejects a relates entry that resolves to nothing", () => {
455
+ const bad = clone();
456
+ adr(bad, "shop.oms.0007").relates.services = ["shop.nope"];
457
+ expect(() => validateCatalog(bad)).toThrowError(
458
+ /adr "shop.oms.0007" relates to unknown service "shop.nope"/,
459
+ );
460
+
461
+ const badEvent = clone();
462
+ adr(badEvent, "org.0002").relates.events = ["shop.oms.cart.ItemAdded"];
463
+ expect(() => validateCatalog(badEvent)).toThrowError(
464
+ /relates to unknown event "shop.oms.cart.ItemAdded"/,
465
+ );
466
+
467
+ const badFlow = clone();
468
+ adr(badFlow, "payments.0004").relates.flows = ["chekout"];
469
+ expect(() => validateCatalog(badFlow)).toThrowError(
470
+ /relates to unknown flow "chekout"/,
471
+ );
472
+ });
473
+
474
+ it("rejects a scope that names something outside the catalog", () => {
475
+ const bad = clone();
476
+ adr(bad, "payments.0004").scope = { kind: "context", context: "billing" };
477
+ expect(() => validateCatalog(bad)).toThrowError(
478
+ /scoped to unknown context "billing"/,
479
+ );
480
+ });
481
+
482
+ it("rejects a superseded record with no supersededBy", () => {
483
+ const bad = clone();
484
+ const superseded = adr(bad, "shop.oms.0003");
485
+ delete superseded.supersededBy;
486
+ adr(bad, "shop.oms.0007").supersedes = [];
487
+ expect(() => validateCatalog(bad)).toThrowError(
488
+ /adr "shop.oms.0003" is superseded but names no supersededBy/,
489
+ );
490
+ });
491
+
492
+ it("rejects a supersession recorded on one side only", () => {
493
+ const forward = clone();
494
+ adr(forward, "shop.oms.0007").supersedes = [];
495
+ expect(() => validateCatalog(forward)).toThrowError(
496
+ /does not list it in supersedes/,
497
+ );
498
+
499
+ const back = clone();
500
+ const predecessor = adr(back, "shop.oms.0003");
501
+ predecessor.supersededBy = "org.0001";
502
+ expect(() => validateCatalog(back)).toThrowError(
503
+ /is not marked superseded by it/,
504
+ );
505
+ });
506
+
507
+ it("rejects supersededBy on a record whose status is not superseded", () => {
508
+ const bad = clone();
509
+ adr(bad, "shop.oms.0003").status = "accepted";
510
+ expect(() => validateCatalog(bad)).toThrowError(
511
+ /its status is "accepted", not "superseded"/,
512
+ );
513
+ });
514
+
515
+ it("rejects an id that disagrees with its number", () => {
516
+ const bad = clone();
517
+ adr(bad, "org.0002").number = 3;
518
+ expect(() => validateCatalog(bad)).toThrowError(
519
+ /adr "org.0002" must end with its number, "0003"/,
520
+ );
521
+ });
522
+ });
523
+
524
+ describe("sample data shape", () => {
525
+ it("gives every aggregate a root entity it actually lists", () => {
526
+ for (const aggregate of allAggregates(catalog)) {
527
+ const root = rootEntity(aggregate);
528
+ expect(root, `${aggregate.id} root "${aggregate.root}"`).toBeDefined();
529
+ expect(aggregate.entities.length).toBeGreaterThan(0);
530
+ }
531
+ });
532
+
533
+ it("names Money in more than one aggregate, through the same shared def", () => {
534
+ const money = allAggregates(catalog)
535
+ .flatMap((a) => a.valueObjects)
536
+ .filter((v) => v.ref === "Money");
537
+ expect(money.length).toBeGreaterThan(1);
538
+ // Same def, so the same fields, wherever it is named.
539
+ for (const vo of money) {
540
+ expect(blockFields(catalog, vo).map((f) => f.name)).toEqual([
541
+ "amountMinor",
542
+ "currency",
543
+ ]);
544
+ }
545
+ });
546
+
547
+ it("resolves a block's shape from its def or from its own fields", () => {
548
+ const index = buildIndex(catalog);
549
+ const shared = index.blockById.get("shop.oms.order.money");
550
+ const inline = index.blockById.get("shop.pricing.quote.quoted-line");
551
+ if (!shared || !inline) throw new Error("fixture is missing blocks");
552
+ expect(shared.kind).toBe("vo");
553
+ expect(blockFields(catalog, shared.block)).toEqual(
554
+ catalog.defs["Money"]?.fields,
555
+ );
556
+ expect(inline.block.ref).toBeUndefined();
557
+ expect(blockFields(catalog, inline.block).map((f) => f.name)).toContain(
558
+ "discountMinor",
559
+ );
560
+ });
561
+
562
+ it("indexes blocks by the shared def they name", () => {
563
+ const index = buildIndex(catalog);
564
+ expect(index.blocksByDef.get("Money")?.length).toBeGreaterThan(1);
565
+ expect(index.blocksByDef.get("Money")).toContain("shop.oms.order.money");
566
+ expect(index.blocksByDef.has("Doubloons")).toBe(false);
567
+ });
568
+
569
+ it("has four contexts, six services and one aggregate that publishes nothing", () => {
570
+ // The hand-written estate first, in its own order; then what the
571
+ // examples publish, in path order. shop.cart sits under examples/ and so
572
+ // joins shop after the two services data/ wrote.
573
+ expect(catalog.contexts.map((c) => c.id)).toEqual([
574
+ "shop",
575
+ "payments",
576
+ "delivery",
577
+ "auth",
578
+ ]);
579
+ expect(catalog.contexts.map((c) => c.classification)).toEqual([
580
+ "core",
581
+ "core",
582
+ "supporting",
583
+ "core",
584
+ ]);
585
+ const services = catalog.contexts.flatMap((c) => c.services);
586
+ expect(services.map((s) => s.id)).toEqual([
587
+ "shop.oms",
588
+ "shop.pricing",
589
+ "shop.cart",
590
+ "payments.ledger",
591
+ "delivery.core",
592
+ "auth.auth",
593
+ ]);
594
+ const empty = services
595
+ .flatMap((s) => s.aggregates)
596
+ .filter((a) => a.events.length === 0)
597
+ .map((a) => a.id);
598
+ expect(empty).toEqual(["shop.pricing.price-list"]);
599
+ });
600
+
601
+ it("has around twelve events and one with two versions adding a field", () => {
602
+ const events = allEvents(catalog);
603
+ expect(events.length).toBeGreaterThanOrEqual(12);
604
+ const twoVersions = events.filter((e) => e.versions.length === 2);
605
+ expect(twoVersions).toHaveLength(1);
606
+ const event = twoVersions[0];
607
+ if (!event) throw new Error("no versioned event");
608
+ const [v1, v2] = event.versions;
609
+ if (!v1 || !v2) throw new Error("bad versions");
610
+ expect(v2.fields.length).toBe(v1.fields.length + 1);
611
+ });
612
+
613
+ it("leaves two peers unresolved by hand, and one more read out of auth", () => {
614
+ const unresolved = catalog.contexts
615
+ .flatMap((c) => c.services)
616
+ .flatMap((s) => s.consumes)
617
+ .filter((r) => r.status === "unresolved");
618
+ expect(unresolved.map((r) => r.id)).toEqual([
619
+ "fraud.v2.Scoring/Score",
620
+ "psp.v2.Charges/Create",
621
+ "psp.v2.Charges/Capture",
622
+ "psp.v2.Charges/Refund",
623
+ "psp.v2.Charges/Void",
624
+ "risk.v1.RiskService/Assess",
625
+ ]);
626
+ // The hand-written two are unresolved for the same reason and each says
627
+ // so on the record; the one the extractor read off auth has only the
628
+ // package it could not place.
629
+ const byHand = unresolved.filter((r) => !r.id.startsWith("risk."));
630
+ expect(new Set(byHand.map((r) => r.peer))).toEqual(
631
+ new Set(["fraud-scoring", "psp-gateway"]),
632
+ );
633
+ for (const call of byHand) expect(call.note, call.id).toBeTruthy();
634
+ });
635
+
636
+ it("gives every flow an owner, and covers both ends of step status", () => {
637
+ const flows = catalog.flows;
638
+ // In the order their files sort, which is the order the extract step
639
+ // reads data/flows/*.flow.md in; then each example's flows, in the order
640
+ // its extractor and its recording wrote them.
641
+ expect(flows.map((f) => f.slug)).toEqual([
642
+ "checkout",
643
+ "gateway-webhook",
644
+ "order-accepted",
645
+ "order-cancelled",
646
+ "quote-expired-on-checkout",
647
+ "refund-requested",
648
+ "shipment-tracking",
649
+ "auth-change-password",
650
+ "auth-get-user",
651
+ "auth-login",
652
+ "auth-logout",
653
+ "auth-register-user",
654
+ "auth-validate-session",
655
+ "auth-revoke-sessions-on-password-change",
656
+ "cart-add-item",
657
+ "cart-checkout",
658
+ "cart-create-basket",
659
+ "cart-get-basket",
660
+ "cart-merge-baskets",
661
+ "cart-remove-item",
662
+ ]);
663
+ // Nothing in the tree has to guess where a flow belongs.
664
+ for (const flow of flows) expect(flow.owner, flow.slug).toBeTruthy();
665
+
666
+ const a = flows.find((f) => f.slug === "order-accepted");
667
+ const c = flows.find((f) => f.slug === "refund-requested");
668
+ if (!a || !c) throw new Error("missing flows");
669
+ // One flow whose every hop lands on something the catalog has, and one
670
+ // with a hop that does not: both ends of the range are in the fixture.
671
+ expect(walkSteps(a.steps).some((s) => s.status === "unresolved")).toBe(
672
+ false,
673
+ );
674
+ expect(walkSteps(c.steps).every((s) => s.status === "declared")).toBe(true);
675
+ });
676
+
677
+ it("gives checkout mixed statuses, and a note on every unresolved hop", () => {
678
+ const checkout = catalog.flows.find((f) => f.slug === "checkout");
679
+ if (!checkout) throw new Error("no checkout flow");
680
+ const steps = walkSteps(checkout.steps);
681
+ const statuses = steps.map((s) => s.status);
682
+ expect(statuses).toContain("verified");
683
+ expect(statuses).toContain("declared");
684
+ // The risk scorer and the two gateway hops: three peers outside the model.
685
+ const unresolved = steps.filter((s) => s.status === "unresolved");
686
+ expect(unresolved.map((s) => s.id)).toEqual(["s7", "s22", "s32"]);
687
+ // An unresolved step that does not say why is just a red mark.
688
+ expect(unresolved.filter((s) => s.note).length).toBeGreaterThan(0);
689
+ expect(steps.find((s) => s.id === "s7")?.note).toBeTruthy();
690
+ });
691
+
692
+ it("has five decisions, one of them superseded by another", () => {
693
+ expect(catalog.adrs.map((a) => a.id).sort()).toEqual([
694
+ "org.0001",
695
+ "org.0002",
696
+ "payments.0004",
697
+ "shop.oms.0003",
698
+ "shop.oms.0007",
699
+ ]);
700
+ const superseded = catalog.adrs.filter((a) => a.status === "superseded");
701
+ expect(superseded.map((a) => a.id)).toEqual(["shop.oms.0003"]);
702
+ expect(superseded[0]?.supersededBy).toBe("shop.oms.0007");
703
+ // The superseded record keeps its own body, wrong diagram included.
704
+ expect(superseded[0]?.body).toContain("QueryWorkflow(cart_id");
705
+ });
706
+
707
+ it("indexes decisions by the events they name, newest first", () => {
708
+ const index = buildIndex(catalog);
709
+ expect(
710
+ index.adrsByEvent
711
+ .get("payments.ledger.payment.PaymentCaptured")
712
+ ?.map((a) => a.id),
713
+ ).toEqual(["payments.0004"]);
714
+ expect(
715
+ index.adrBySlug.get("org-0001-client-protos-in-consumer-infrastructure")
716
+ ?.id,
717
+ ).toBe("org.0001");
718
+ });
719
+
720
+ it("indexes flows by event", () => {
721
+ const index = buildIndex(catalog);
722
+ expect(index.flowsByEvent.get("shop.oms.order.OrderPlaced")).toEqual([
723
+ "checkout",
724
+ "order-accepted",
725
+ ]);
726
+ expect(index.rpcProviderByMethod.get("shop.v1.Pricing/GetQuote")?.id).toBe(
727
+ "shop.pricing",
728
+ );
729
+ expect(index.rpcProviderByMethod.has("fraud.v2.Scoring/Score")).toBe(false);
730
+ });
731
+ });
732
+
733
+ describe("glossary terms", () => {
734
+ function terms(bad: Catalog) {
735
+ const list = bad.terms;
736
+ if (!list || list.length === 0) throw new Error("fixture has no terms");
737
+ return list;
738
+ }
739
+
740
+ it("indexes a term by id and groups a context's vocabulary", () => {
741
+ const index = buildIndex(catalog);
742
+
743
+ expect(index.termById.get("auth.session")?.name).toBe("Session");
744
+ expect(index.termsByContext.get("auth")?.length).toBe(
745
+ (catalog.terms ?? []).filter((t) => t.context === "auth").length,
746
+ );
747
+ // The same word in two contexts is two terms, and neither shadows the
748
+ // other: the id carries the boundary the meaning belongs to.
749
+ expect(index.termById.get("auth.bus")?.definition).not.toBe(
750
+ index.termById.get("shop.bus")?.definition,
751
+ );
752
+ });
753
+
754
+ it("carries the glossary's own paragraph, whole", () => {
755
+ const session = buildIndex(catalog).termById.get("auth.session");
756
+
757
+ expect(session?.definition).toMatch(/^Proof that a user logged in/);
758
+ });
759
+
760
+ // The mistake this catches is one line of the manifest: a step that does not
761
+ // say which context the glossary belongs to falls back to the directory, and
762
+ // every term it writes then belongs to a context nothing else declares.
763
+ it("rejects a term whose context the catalog does not declare", () => {
764
+ const bad = clone();
765
+ const term = terms(bad)[0]!;
766
+ term.context = "oms";
767
+ term.id = `oms.${term.slug}`;
768
+
769
+ expect(() => validateCatalog(bad)).toThrowError(
770
+ /belongs to context "oms", which the catalog does not declare/,
771
+ );
772
+ });
773
+
774
+ it("rejects an id that is not its context and slug", () => {
775
+ const bad = clone();
776
+ terms(bad)[0]!.id = "auth.something-else";
777
+
778
+ expect(() => validateCatalog(bad)).toThrowError(/must have id/);
779
+ });
780
+
781
+ it("rejects a term that says nothing about what it is", () => {
782
+ const bad = clone();
783
+ terms(bad)[0]!.definition = "";
784
+
785
+ expect(() => validateCatalog(bad)).toThrowError(
786
+ /says nothing about what it is/,
787
+ );
788
+ });
789
+
790
+ it("indexes a term whose definition is one short sentence", () => {
791
+ const ok = clone();
792
+ terms(ok)[0]!.definition = "One sentence.";
793
+
794
+ expect(() => validateCatalog(ok)).not.toThrow();
795
+ });
796
+ });
797
+
798
+ describe("validateCatalog: interfaces and modules", () => {
799
+ it("rejects two methods of one interface sharing a name", () => {
800
+ const bad = clone();
801
+ const provided = bad.contexts[0]?.services[0]?.provides[0];
802
+ if (!provided) throw new Error("nothing to break");
803
+ provided.methods = [{ name: "Same" }, { name: "Same" }];
804
+
805
+ // `exposedBy` names a method by name alone and `rpcProviderByMethod` is
806
+ // keyed by it, so a duplicate makes one of the two unreachable.
807
+ expect(() => validateCatalog(bad)).toThrow(CatalogError);
808
+ });
809
+
810
+ it("rejects a method that streams in a way nothing means", () => {
811
+ const bad = clone();
812
+ const provided = bad.contexts[0]?.services[0]?.provides[0];
813
+ const method = provided?.methods[0];
814
+ if (!method) throw new Error("nothing to break");
815
+ method.streaming = "sideways" as never;
816
+
817
+ expect(() => validateCatalog(bad)).toThrow(/expected one of/);
818
+ });
819
+
820
+ it("accepts a method that carries shapes and streams", () => {
821
+ const good = clone();
822
+ const method = good.contexts[0]?.services[0]?.provides[0]?.methods[0];
823
+ if (!method) throw new Error("nothing to change");
824
+ method.request = "PlaceOrderRequest";
825
+ method.response = "PlaceOrderResponse";
826
+ method.streaming = "server";
827
+ method.deprecated = true;
828
+
829
+ expect(() => validateCatalog(good)).not.toThrow();
830
+ });
831
+
832
+ // The shipped catalog publishes, so "no modules" has to be made: nothing may
833
+ // name one either, or the validator is right to refuse.
834
+ it("accepts a catalog with no modules at all", () => {
835
+ const none = clone();
836
+ delete none.modules;
837
+ for (const context of none.contexts) {
838
+ for (const service of context.services) {
839
+ delete service.modules;
840
+ for (const provided of service.provides) delete provided.module;
841
+ }
842
+ }
843
+
844
+ expect(() => validateCatalog(none)).not.toThrow();
845
+ });
846
+
847
+ it("rejects a service naming a module that is not in the catalog", () => {
848
+ const bad = clone();
849
+ const service = bad.contexts[0]?.services[0];
850
+ if (!service) throw new Error("nothing to break");
851
+ service.modules = ["buf.build/acme/nowhere"];
852
+
853
+ expect(() => validateCatalog(bad)).toThrow(/not in this catalog/);
854
+ });
855
+
856
+ it("rejects an interface declaring itself part of a module nobody declared", () => {
857
+ const bad = clone();
858
+ const provided = bad.contexts[0]?.services[0]?.provides[0];
859
+ if (!provided) throw new Error("nothing to break");
860
+ provided.module = "buf.build/acme/nowhere";
861
+
862
+ expect(() => validateCatalog(bad)).toThrow(/not in this catalog/);
863
+ });
864
+
865
+ it("rejects a module owned by something that is not a service", () => {
866
+ const bad = withModule(clone());
867
+ const module = bad.modules?.[0];
868
+ if (!module) throw new Error("nothing to break");
869
+ module.owner = "shop.nobody";
870
+
871
+ expect(() => validateCatalog(bad)).toThrow(/not a service/);
872
+ });
873
+
874
+ // A module published by a team, or by a repository outside the estate, is
875
+ // the ordinary case - not a defect.
876
+ it("accepts a module nobody in the catalog owns", () => {
877
+ const good = withModule(clone());
878
+ delete good.modules?.[0]?.owner;
879
+
880
+ expect(() => validateCatalog(good)).not.toThrow();
881
+ });
882
+
883
+ it("rejects two modules sharing a slug, which is what the URL uses", () => {
884
+ const bad = withModule(clone());
885
+ bad.modules?.push({
886
+ ...bad.modules[0]!,
887
+ id: "buf.build/other/shop",
888
+ name: "other/shop",
889
+ });
890
+
891
+ expect(() => validateCatalog(bad)).toThrow(/slug/);
892
+ });
893
+
894
+ // A module's deps come from its own lock and routinely name modules this
895
+ // estate never vendored - the same kind of fact as a call to a peer outside
896
+ // the catalog. Requiring them to resolve would mean a module could only be
897
+ // recorded once everything it depends on had been vendored too.
898
+ it("accepts a module depending on one the estate never vendored", () => {
899
+ const good = withModule(clone());
900
+ if (!good.modules?.[0]) throw new Error("nothing to change");
901
+ good.modules[0].deps = ["buf.build/acme/never-vendored"];
902
+
903
+ expect(() => validateCatalog(good)).not.toThrow();
904
+ });
905
+
906
+ it("indexes a module by id and by slug, and finds who uses it", () => {
907
+ const withOne = withModule(clone());
908
+ const service = withOne.contexts[0]?.services[0];
909
+ const provided = service?.provides[0];
910
+ if (!service || !provided) throw new Error("nothing to link");
911
+ provided.module = "buf.build/acme/shop";
912
+
913
+ const index = buildIndex(validateCatalog(withOne));
914
+
915
+ expect(index.moduleById.get("buf.build/acme/shop")?.slug).toBe("acme-shop");
916
+ expect(index.moduleBySlug.get("acme-shop")?.id).toBe("buf.build/acme/shop");
917
+ expect(
918
+ index.interfacesByModule.get("buf.build/acme/shop")?.[0]?.provided.id,
919
+ ).toBe(provided.id);
920
+ expect(
921
+ index.servicesUsingModule.get("buf.build/acme/shop")?.map((s) => s.id),
922
+ ).toEqual([service.id]);
923
+ });
924
+ });
925
+
926
+ /**
927
+ * The shipped catalog plus one module, owned by its first service. Added
928
+ * beside what the estate has published, not in its place: a service already
929
+ * names its module, and taking that module away breaks the catalog before the
930
+ * test gets to say what it meant to.
931
+ */
932
+ function withModule(catalog: Catalog): Catalog {
933
+ const owner = catalog.contexts[0]?.services[0]?.id;
934
+ catalog.modules = [
935
+ ...(catalog.modules ?? []),
936
+ {
937
+ id: "buf.build/acme/shop",
938
+ slug: "acme-shop",
939
+ name: "acme/shop",
940
+ registry: "buf.build",
941
+ owner,
942
+ packages: ["shop.v1"],
943
+ files: ["shop/v1/orders.proto"],
944
+ source: "proto",
945
+ },
946
+ ];
947
+
948
+ return catalog;
949
+ }
950
+
951
+ describe("validateCatalog: owners", () => {
952
+ const withOwners = (owners: string[]) => {
953
+ const c = clone();
954
+ const service = c.contexts[0]?.services[0];
955
+ if (!service) throw new Error("no service to own");
956
+ service.owners = owners;
957
+
958
+ return c;
959
+ };
960
+
961
+ it("takes a handle as it is written, whatever shape it is", () => {
962
+ expect(() =>
963
+ validateCatalog(
964
+ withOwners(["@acme/oms-team", "@someone", "dev@acme.io"]),
965
+ ),
966
+ ).not.toThrow();
967
+ });
968
+
969
+ it("rejects an owner with no name", () => {
970
+ expect(() => validateCatalog(withOwners(["@acme/oms-team", " "]))).toThrow(
971
+ /owner with no name/,
972
+ );
973
+ });
974
+
975
+ it("rejects one owner named twice", () => {
976
+ expect(() =>
977
+ validateCatalog(withOwners(["@acme/oms", "@acme/oms"])),
978
+ ).toThrow(/twice/);
979
+ });
980
+ });
981
+
982
+ describe("validateCatalog: repo pins", () => {
983
+ it("accepts a pin for a repository no service claims to live in", () => {
984
+ // A repository fetched for its protos before anything reads its code is a
985
+ // normal intermediate state: the pin is simply never looked up. Failing
986
+ // here would refuse to render an estate mid-migration.
987
+ const good = clone();
988
+ good.repos = [{ repo: "github.com/acme/pay", commit: "c1d2e3f" }];
989
+
990
+ expect(() => validateCatalog(good)).not.toThrow();
991
+ });
992
+
993
+ it("rejects a pin with no commit", () => {
994
+ const bad = clone();
995
+ bad.repos = [{ repo: "github.com/acme/shop", commit: "" }];
996
+
997
+ expect(() => validateCatalog(bad)).toThrow(/pinned to nothing/);
998
+ });
999
+
1000
+ it("rejects one repository pinned twice in one catalog", () => {
1001
+ const bad = clone();
1002
+ bad.repos = [
1003
+ { repo: "github.com/acme/shop", commit: "c1d2e3f" },
1004
+ { repo: "github.com/acme/shop", commit: "c1d2e3f" },
1005
+ ];
1006
+
1007
+ expect(() => validateCatalog(bad)).toThrow(/pinned twice/);
1008
+ });
1009
+ });
1010
+
1011
+ describe("validateCatalog: channels", () => {
1012
+ function speaking(): Catalog {
1013
+ const good = clone();
1014
+ const service = good.contexts[0]?.services[0];
1015
+ if (!service) throw new Error("nothing to change");
1016
+ service.channels = [
1017
+ {
1018
+ address: "shop.cart.basket",
1019
+ source: "asyncapi.yaml",
1020
+ messages: [
1021
+ { name: "cart.BasketCreated", direction: "send" },
1022
+ { name: "auth.SessionEnded", direction: "receive" },
1023
+ ],
1024
+ },
1025
+ ];
1026
+
1027
+ return good;
1028
+ }
1029
+
1030
+ it("accepts a service that declares what it says on the bus", () => {
1031
+ expect(() => validateCatalog(speaking())).not.toThrow();
1032
+ });
1033
+
1034
+ // A service with no document is the normal case, and it is not a service
1035
+ // that speaks to nobody.
1036
+ it("accepts a service with no channels at all", () => {
1037
+ const none = clone();
1038
+ for (const context of none.contexts) {
1039
+ for (const service of context.services) delete service.channels;
1040
+ }
1041
+
1042
+ expect(() => validateCatalog(none)).not.toThrow();
1043
+ });
1044
+
1045
+ it("rejects a channel with no address", () => {
1046
+ const bad = speaking();
1047
+ const channel = bad.contexts[0]?.services[0]?.channels?.[0];
1048
+ if (!channel) throw new Error("nothing to break");
1049
+ channel.address = "";
1050
+
1051
+ expect(() => validateCatalog(bad)).toThrow(/no address/);
1052
+ });
1053
+
1054
+ // One channel says both directions, so a second entry for the same address
1055
+ // is two answers to one question.
1056
+ it("rejects one address declared twice", () => {
1057
+ const bad = speaking();
1058
+ const channels = bad.contexts[0]?.services[0]?.channels;
1059
+ if (!channels?.[0]) throw new Error("nothing to break");
1060
+ channels.push({ ...channels[0], messages: [] });
1061
+
1062
+ expect(() => validateCatalog(bad)).toThrow(/twice/);
1063
+ });
1064
+
1065
+ // The name is what an event's wire is compared against, and a blank one
1066
+ // matches everything.
1067
+ it("rejects a message with no name", () => {
1068
+ const bad = speaking();
1069
+ const message = bad.contexts[0]?.services[0]?.channels?.[0]?.messages[0];
1070
+ if (!message) throw new Error("nothing to break");
1071
+ message.name = "";
1072
+
1073
+ expect(() => validateCatalog(bad)).toThrow(/no name/);
1074
+ });
1075
+
1076
+ it("rejects a direction that is neither send nor receive", () => {
1077
+ const bad = speaking();
1078
+ const message = bad.contexts[0]?.services[0]?.channels?.[0]?.messages[0];
1079
+ if (!message) throw new Error("nothing to break");
1080
+ message.direction = "publish" as never;
1081
+
1082
+ expect(() => validateCatalog(bad)).toThrow(/neither send nor receive/);
1083
+ });
1084
+ });
1085
+
1086
+ // A system outside the estate with a contract sits at the root beside the
1087
+ // contexts, and is held to the same shape as anything else that does.
1088
+ describe("externals", () => {
1089
+ function calling(): Catalog {
1090
+ const with_ = clone();
1091
+ with_.externals = [
1092
+ {
1093
+ id: "psp",
1094
+ slug: "psp",
1095
+ name: "PSP",
1096
+ summary: "the card network",
1097
+ url: "https://psp.example/docs",
1098
+ provides: [
1099
+ {
1100
+ id: "psp.v1.Charges",
1101
+ methods: [
1102
+ { name: "Create", http: { method: "POST", path: "/v1/charges" } },
1103
+ ],
1104
+ source: "psp/openapi.yaml",
1105
+ },
1106
+ ],
1107
+ },
1108
+ ];
1109
+ return with_;
1110
+ }
1111
+
1112
+ it("accepts one, and indexes what it answers on", () => {
1113
+ const good = calling();
1114
+ expect(() => validateCatalog(good)).not.toThrow();
1115
+ const index = buildIndex(good);
1116
+ expect(index.externalById.get("psp")?.name).toBe("PSP");
1117
+ expect(
1118
+ index.externalProviderByMethod.get("psp.v1.Charges/Create")?.id,
1119
+ ).toBe("psp");
1120
+ // Never widened into the service map: every reader of that one opens a service page.
1121
+ expect(index.rpcProviderByMethod.has("psp.v1.Charges/Create")).toBe(false);
1122
+ });
1123
+
1124
+ it("rejects a slug that is not the id", () => {
1125
+ const bad = calling();
1126
+ bad.externals![0]!.slug = "gateway";
1127
+ expect(() => validateCatalog(bad)).toThrow(/slug must equal its id/);
1128
+ });
1129
+
1130
+ it("rejects a dotted id", () => {
1131
+ const bad = calling();
1132
+ bad.externals![0]!.id = "shop.psp";
1133
+ bad.externals![0]!.slug = "shop.psp";
1134
+ expect(() => validateCatalog(bad)).toThrow(/dot in its id/);
1135
+ });
1136
+
1137
+ it("rejects the id of a bounded context", () => {
1138
+ const bad = calling();
1139
+ const taken = bad.contexts[0]!.id;
1140
+ bad.externals![0]!.id = taken;
1141
+ bad.externals![0]!.slug = taken;
1142
+ expect(() => validateCatalog(bad)).toThrow(/id of a bounded context/);
1143
+ });
1144
+
1145
+ it("rejects two with one id, and one interface listing a method twice", () => {
1146
+ const twice = calling();
1147
+ twice.externals!.push({ ...twice.externals![0]! });
1148
+ expect(() => validateCatalog(twice)).toThrow(CatalogError);
1149
+
1150
+ const doubled = calling();
1151
+ doubled.externals![0]!.provides[0]!.methods.push({ name: "Create" });
1152
+ expect(() => validateCatalog(doubled)).toThrow(/method/);
1153
+ });
1154
+ });
1155
+
1156
+ describe("validateCatalog: enums", () => {
1157
+ function withEnum(): { bad: Catalog; aggregate: NonNullable<Catalog["contexts"][0]["services"][0]["aggregates"][0]> } {
1158
+ const bad = clone();
1159
+ const aggregate = bad.contexts[0]?.services[0]?.aggregates[0];
1160
+ if (!aggregate) throw new Error("fixture has no aggregate");
1161
+ aggregate.enums = [
1162
+ {
1163
+ id: `${aggregate.id}.reason`,
1164
+ slug: "reason",
1165
+ name: "Reason",
1166
+ doc: "",
1167
+ values: [
1168
+ { name: "logout", doc: "" },
1169
+ { name: "revoked", doc: "" },
1170
+ ],
1171
+ },
1172
+ ];
1173
+ return { bad, aggregate };
1174
+ }
1175
+
1176
+ it("accepts an aggregate that declares none, and one that declares a set", () => {
1177
+ expect(() => validateCatalog(clone())).not.toThrow();
1178
+ const { bad } = withEnum();
1179
+ expect(() => validateCatalog(bad)).not.toThrow();
1180
+ });
1181
+
1182
+ it("indexes an enum under its id with its owners", () => {
1183
+ const { bad, aggregate } = withEnum();
1184
+ const owner = buildIndex(bad).enumById.get(`${aggregate.id}.reason`);
1185
+ expect(owner?.enum.name).toBe("Reason");
1186
+ expect(owner?.aggregate.id).toBe(aggregate.id);
1187
+ });
1188
+
1189
+ it("rejects an enum whose id is not spelled from the aggregate's", () => {
1190
+ const { bad, aggregate } = withEnum();
1191
+ (aggregate.enums ?? [])[0]!.id = "elsewhere.reason";
1192
+ expect(() => validateCatalog(bad)).toThrowError(
1193
+ /enum "elsewhere.reason" .* must have id/,
1194
+ );
1195
+ });
1196
+
1197
+ it("rejects an enum with no values, and one naming a value twice", () => {
1198
+ const { bad, aggregate } = withEnum();
1199
+ const item = (aggregate.enums ?? [])[0]!;
1200
+ item.values = [];
1201
+ expect(() => validateCatalog(bad)).toThrowError(/has no values/);
1202
+ item.values = [
1203
+ { name: "logout", doc: "" },
1204
+ { name: "logout", doc: "" },
1205
+ ];
1206
+ expect(() => validateCatalog(bad)).toThrowError(/lists value "logout" twice/);
1207
+ });
1208
+
1209
+ it("rejects two enums with one slug", () => {
1210
+ const { bad, aggregate } = withEnum();
1211
+ const item = (aggregate.enums ?? [])[0]!;
1212
+ aggregate.enums = [item, { ...item }];
1213
+ expect(() => validateCatalog(bad)).toThrowError(/enum slug "reason" is not unique/);
1214
+ });
1215
+ });