@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,1038 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { BoundedContext, Catalog } from "./catalog";
3
+ import { mergeCatalogs } from "./merge";
4
+ import type { CatalogSource, SourceCatalog } from "./merge";
5
+
6
+ // `Partial<SourceCatalog>`, so a case can hand in a source with no stamp at
7
+ // all - which is what an authored file is, and what the merge has to survive.
8
+ function source(path: string, catalog: Partial<SourceCatalog>): CatalogSource {
9
+ return {
10
+ path,
11
+ catalog: {
12
+ generatedAt: "2026-01-01T00:00:00Z",
13
+ commit: "aaa1111",
14
+ contexts: [],
15
+ defs: {},
16
+ flows: [],
17
+ adrs: [],
18
+ ...catalog,
19
+ },
20
+ };
21
+ }
22
+
23
+ function context(
24
+ id: string,
25
+ services: string[],
26
+ overrides: Partial<BoundedContext> = {},
27
+ ): BoundedContext {
28
+ return {
29
+ id,
30
+ slug: id,
31
+ name: id,
32
+ summary: "",
33
+ services: services.map((serviceId) => ({
34
+ id: serviceId,
35
+ slug: serviceId.split(".")[1] ?? serviceId,
36
+ name: serviceId,
37
+ repo: "",
38
+ path: "",
39
+ readme: "",
40
+ provides: [],
41
+ consumes: [],
42
+ aggregates: [],
43
+ })),
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ describe("mergeCatalogs", () => {
49
+ it("puts services from two sources into the context they share", () => {
50
+ const merged = mergeCatalogs([
51
+ source("a.json", { contexts: [context("shop", ["shop.oms"])] }),
52
+ source("b.json", { contexts: [context("shop", ["shop.pricing"])] }),
53
+ ]);
54
+
55
+ expect(merged.conflicts).toEqual([]);
56
+ expect(merged.catalog.contexts).toHaveLength(1);
57
+ expect(merged.catalog.contexts[0]?.services.map((s) => s.id)).toEqual([
58
+ "shop.oms",
59
+ "shop.pricing",
60
+ ]);
61
+ });
62
+
63
+ // The set of sources comes from a glob, and a glob's order is not a fact
64
+ // about the estate. Two runs that disagree would make every generated file
65
+ // churn for no reason.
66
+ it("does not depend on the order the sources arrive in", () => {
67
+ const a = source("a.json", { contexts: [context("shop", ["shop.oms"])] });
68
+ const b = source("b.json", {
69
+ contexts: [context("shop", ["shop.pricing"])],
70
+ });
71
+
72
+ expect(JSON.stringify(mergeCatalogs([a, b]).catalog)).toEqual(
73
+ JSON.stringify(mergeCatalogs([b, a]).catalog),
74
+ );
75
+ });
76
+
77
+ // A fragment that knows only one aspect leaves the rest empty, and sorts
78
+ // wherever its filename puts it. An empty value must never win.
79
+ it("lets a later source fill in what an earlier one left empty", () => {
80
+ const bare = context("auth", ["auth.auth"], { name: "", summary: "" });
81
+ bare.services = bare.services.map((s) => ({ ...s, name: "", repo: "" }));
82
+
83
+ const full = context("auth", ["auth.auth"], { name: "Authentication" });
84
+ full.services = full.services.map((s) => ({
85
+ ...s,
86
+ name: "Authentication & Sessions",
87
+ repo: "github.com/example/auth",
88
+ }));
89
+
90
+ const merged = mergeCatalogs([
91
+ source("a-api.json", { contexts: [bare] }),
92
+ source("b-domain.json", { contexts: [full] }),
93
+ ]);
94
+
95
+ expect(merged.conflicts).toEqual([]);
96
+ expect(merged.catalog.contexts[0]?.name).toBe("Authentication");
97
+ expect(merged.catalog.contexts[0]?.services[0]?.name).toBe(
98
+ "Authentication & Sessions",
99
+ );
100
+ });
101
+
102
+ it("merges neutral kinds and reports incompatible claims", () => {
103
+ const base = context("platform", ["platform.tools"]);
104
+ const typed = context("platform", ["platform.tools"], { kind: "system" });
105
+ typed.services[0]!.kind = "application";
106
+ const conflicting = context("platform", ["platform.tools"], { kind: "team" });
107
+ conflicting.services[0]!.kind = "cli";
108
+
109
+ const merged = mergeCatalogs([
110
+ source("a.json", { contexts: [base] }),
111
+ source("b.json", { contexts: [typed] }),
112
+ source("c.json", { contexts: [conflicting] }),
113
+ ]);
114
+
115
+ expect(merged.catalog.contexts[0]!.kind).toBe("system");
116
+ expect(merged.catalog.contexts[0]!.services[0]!.kind).toBe("application");
117
+ expect(merged.conflicts.map((conflict) => conflict.message)).toEqual([
118
+ expect.stringContaining("has kind team"),
119
+ expect.stringContaining("has kind cli"),
120
+ ]);
121
+ });
122
+
123
+ it("records two sources disagreeing about a context's name, and keeps the first", () => {
124
+ const merged = mergeCatalogs([
125
+ source("a.json", { contexts: [context("shop", [], { name: "Shop" })] }),
126
+ source("b.json", {
127
+ contexts: [context("shop", [], { name: "Storefront" })],
128
+ }),
129
+ ]);
130
+
131
+ expect(merged.catalog.contexts[0]?.name).toBe("Shop");
132
+ expect(merged.conflicts).toHaveLength(1);
133
+ expect(merged.conflicts[0]?.path).toBe("b.json");
134
+ expect(merged.conflicts[0]?.message).toContain("Storefront");
135
+ });
136
+
137
+ // The reason each aspect of a service gets its own extractor: one source
138
+ // knows the aggregates, another knows what the service answers over HTTP, and
139
+ // neither has to know the other exists.
140
+ it("merges one service described by two sources", () => {
141
+ const domain = context("auth", ["auth.auth"]);
142
+ domain.services = domain.services.map((s) => ({
143
+ ...s,
144
+ aggregates: [
145
+ {
146
+ id: "auth.auth.user",
147
+ slug: "user",
148
+ name: "User",
149
+ readme: "",
150
+ root: "User",
151
+ entities: [],
152
+ valueObjects: [],
153
+ operations: [],
154
+ events: [],
155
+ },
156
+ ],
157
+ }));
158
+
159
+ const api = context("auth", ["auth.auth"]);
160
+ api.services = api.services.map((s) => ({
161
+ ...s,
162
+ provides: [
163
+ {
164
+ id: "auth.v1.Users",
165
+ methods: [{ name: "registerUser" }],
166
+ source: "openapi.yaml",
167
+ },
168
+ ],
169
+ }));
170
+
171
+ const merged = mergeCatalogs([
172
+ source("a-domain.json", { contexts: [domain] }),
173
+ source("b-api.json", { contexts: [api] }),
174
+ ]);
175
+
176
+ expect(merged.conflicts).toEqual([]);
177
+ expect(merged.catalog.contexts[0]?.services).toHaveLength(1);
178
+
179
+ const service = merged.catalog.contexts[0]?.services[0];
180
+ expect(service?.aggregates.map((a) => a.id)).toEqual(["auth.auth.user"]);
181
+ expect(service?.provides.map((p) => p.id)).toEqual(["auth.v1.Users"]);
182
+ });
183
+
184
+ it("unions a service's commands by the line to type", () => {
185
+ const domain = context("shop", ["shop.cart"]);
186
+ domain.services = domain.services.map((s) => ({
187
+ ...s,
188
+ commands: [
189
+ { runner: "npm", name: "test", run: "npm test", body: "vitest run" },
190
+ ],
191
+ }));
192
+
193
+ const commands = context("shop", ["shop.cart"]);
194
+ commands.services = commands.services.map((s) => ({
195
+ ...s,
196
+ commands: [
197
+ { runner: "npm", name: "test", run: "npm test", body: "vitest" },
198
+ { runner: "make", name: "gen", run: "make gen", doc: "Regenerate stubs" },
199
+ ],
200
+ }));
201
+
202
+ const merged = mergeCatalogs([
203
+ source("a-domain.json", { contexts: [domain] }),
204
+ source("b-commands.json", { contexts: [commands] }),
205
+ ]);
206
+
207
+ expect(merged.conflicts).toEqual([]);
208
+ const service = merged.catalog.contexts[0]?.services[0];
209
+ expect(service?.commands?.map((c) => c.run)).toEqual([
210
+ "npm test",
211
+ "make gen",
212
+ ]);
213
+ // The first source's reading of a shared line is the one kept.
214
+ expect(service?.commands?.[0]?.body).toBe("vitest run");
215
+ });
216
+
217
+ it("lets a second source add an event, and a consumer, to an aggregate it did not declare", () => {
218
+ const aggregate = (events: Catalog["contexts"][0]["services"][0]["aggregates"][0]["events"]) => ({
219
+ id: "shop.oms.order",
220
+ slug: "order",
221
+ name: "Order",
222
+ readme: "",
223
+ root: "Order",
224
+ entities: [],
225
+ valueObjects: [],
226
+ operations: [],
227
+ events,
228
+ });
229
+ const event = (name: string, consumers: { service: string; status: "declared" | "verified"; note?: string }[]) => ({
230
+ id: `shop.oms.order.${name}`,
231
+ slug: name.toLowerCase(),
232
+ name,
233
+ versions: [{ version: "v1", doc: "", source: "x", fields: [] }],
234
+ consumers,
235
+ });
236
+
237
+ const producer = context("shop", ["shop.oms"]);
238
+ producer.services[0]!.aggregates = [
239
+ aggregate([event("OrderPlaced", [{ service: "payments.ledger", status: "declared", note: "first" }])]),
240
+ ];
241
+ const overlay = context("shop", ["shop.oms"]);
242
+ overlay.services[0]!.aggregates = [
243
+ aggregate([
244
+ event("OrderPlaced", [
245
+ { service: "payments.ledger", status: "verified", note: "second" },
246
+ { service: "delivery.core", status: "declared" },
247
+ ]),
248
+ event("OrderCancelled", []),
249
+ ]),
250
+ ];
251
+
252
+ const before = JSON.stringify([producer, overlay]);
253
+ const merged = mergeCatalogs([
254
+ source("a.json", { contexts: [producer] }),
255
+ source("b.json", { contexts: [overlay] }),
256
+ ]);
257
+
258
+ expect(merged.conflicts).toEqual([]);
259
+ const events = merged.catalog.contexts[0]?.services[0]?.aggregates[0]?.events;
260
+ expect(events?.map((e) => e.id)).toEqual([
261
+ "shop.oms.order.OrderPlaced",
262
+ "shop.oms.order.OrderCancelled",
263
+ ]);
264
+ // The second source has seen the ledger consume it, and that is the one
265
+ // thing a later source may say about an edge somebody else declared.
266
+ expect(events?.[0]?.consumers).toEqual([
267
+ { service: "payments.ledger", status: "verified", note: "first" },
268
+ { service: "delivery.core", status: "declared" },
269
+ ]);
270
+ // The union never writes into a source.
271
+ expect(JSON.stringify([producer, overlay])).toBe(before);
272
+ });
273
+
274
+ it("reports two sources disagreeing about a service's repo", () => {
275
+ const a = context("auth", ["auth.auth"]);
276
+ a.services = a.services.map((s) => ({ ...s, repo: "github.com/one" }));
277
+ const b = context("auth", ["auth.auth"]);
278
+ b.services = b.services.map((s) => ({ ...s, repo: "github.com/two" }));
279
+
280
+ const merged = mergeCatalogs([
281
+ source("a.json", { contexts: [a] }),
282
+ source("b.json", { contexts: [b] }),
283
+ ]);
284
+
285
+ expect(merged.catalog.contexts[0]?.services[0]?.repo).toBe(
286
+ "github.com/one",
287
+ );
288
+ expect(merged.conflicts[0]?.message).toContain("different repo");
289
+ });
290
+
291
+ it("accepts a shared type defined identically twice", () => {
292
+ const money = { fields: [{ name: "amount", type: "int64", doc: "" }] };
293
+ const merged = mergeCatalogs([
294
+ source("a.json", { defs: { Money: money } }),
295
+ source("b.json", { defs: { Money: money } }),
296
+ ]);
297
+
298
+ expect(merged.conflicts).toEqual([]);
299
+ });
300
+
301
+ // `ref` is a bare key into one namespace, so two sources meaning different
302
+ // things by "Money" is a real problem rather than a naming accident.
303
+ it("reports a shared type defined differently in two sources", () => {
304
+ const merged = mergeCatalogs([
305
+ source("a.json", {
306
+ defs: {
307
+ Money: { fields: [{ name: "amount", type: "int64", doc: "" }] },
308
+ },
309
+ }),
310
+ source("b.json", {
311
+ defs: {
312
+ Money: { fields: [{ name: "amount", type: "string", doc: "" }] },
313
+ },
314
+ }),
315
+ ]);
316
+
317
+ expect(merged.conflicts).toHaveLength(1);
318
+ expect(merged.conflicts[0]?.where).toBe("defs.Money");
319
+ expect(merged.catalog.defs.Money?.fields[0]?.type).toBe("int64");
320
+ });
321
+
322
+ it("keeps one flow when two sources declare the same id, and says so when they disagree", () => {
323
+ const flow = {
324
+ id: "flow.checkout",
325
+ slug: "checkout",
326
+ name: "Checkout",
327
+ summary: "",
328
+ owner: "shop",
329
+ participants: [],
330
+ steps: [],
331
+ };
332
+ const hop = {
333
+ type: "step" as const,
334
+ id: "s1",
335
+ from: "a",
336
+ to: "b",
337
+ kind: "call" as const,
338
+ label: "x",
339
+ status: "declared" as const,
340
+ };
341
+
342
+ const merged = mergeCatalogs([
343
+ source("a.json", { flows: [flow] }),
344
+ source("b.json", { flows: [{ ...flow, name: "Checkout again", steps: [hop] }] }),
345
+ ]);
346
+
347
+ expect(merged.catalog.flows).toHaveLength(1);
348
+ expect(merged.catalog.flows[0]?.name).toBe("Checkout");
349
+ expect(merged.catalog.flows[0]?.steps).toEqual([]);
350
+ expect(merged.conflicts[0]?.message).toContain(
351
+ "already declared in a.json",
352
+ );
353
+ });
354
+
355
+ // A merged catalog is exactly as fresh as its stalest part, and the parts
356
+ // keep their own stamps so anything that wants to be precise still can be.
357
+ it("stamps the merge with the oldest source and keeps each source's own", () => {
358
+ const merged = mergeCatalogs([
359
+ source("a.json", { generatedAt: "2026-03-01T00:00:00Z", commit: "aaa" }),
360
+ source("b.json", { generatedAt: "2026-01-01T00:00:00Z", commit: "bbb" }),
361
+ ]);
362
+
363
+ expect(merged.catalog.generatedAt).toBe("2026-01-01T00:00:00Z");
364
+ expect(merged.catalog.commit).toBe("2 sources");
365
+ expect(merged.sources).toEqual([
366
+ { path: "a.json", generatedAt: "2026-03-01T00:00:00Z", commit: "aaa" },
367
+ { path: "b.json", generatedAt: "2026-01-01T00:00:00Z", commit: "bbb" },
368
+ ]);
369
+ });
370
+
371
+ // The corpus mixes offsets, because the host stamps a fragment with the
372
+ // commit that touched it and a commit is dated where it was made. Sorting
373
+ // the text picks the wrong one whenever the two disagree, and this is the
374
+ // pair where they do: 05:57 in Bangkok is 22:57 the previous day in UTC.
375
+ it("takes the earliest instant, not the earliest string", () => {
376
+ const merged = mergeCatalogs([
377
+ source("a.json", { generatedAt: "2026-09-05T00:00:00Z", commit: "aaa" }),
378
+ source("b.json", {
379
+ generatedAt: "2026-09-05T05:57:24+07:00",
380
+ commit: "bbb",
381
+ }),
382
+ ]);
383
+
384
+ expect(merged.catalog.generatedAt).toBe("2026-09-05T05:57:24+07:00");
385
+ });
386
+
387
+ // Not evidence of being old, evidence of being broken - and the catalog is
388
+ // dated from the stamps that can be read rather than from the one that
389
+ // cannot.
390
+ it("does not let an unreadable stamp win", () => {
391
+ const merged = mergeCatalogs([
392
+ source("a.json", {
393
+ generatedAt: "1970-13-45T99:99:99Z",
394
+ commit: "aaa",
395
+ }),
396
+ source("b.json", { generatedAt: "2026-09-05T00:00:00Z", commit: "bbb" }),
397
+ ]);
398
+
399
+ expect(merged.catalog.generatedAt).toBe("2026-09-05T00:00:00Z");
400
+ });
401
+
402
+ // A file no plugin produces is stamped by nobody. It still merges - it is
403
+ // where the estate's authored facts live - and it dates and attributes
404
+ // nothing, which is the whole difference between a provenance and a guess.
405
+ it("ignores a source that carries no stamp", () => {
406
+ const merged = mergeCatalogs([
407
+ source("authored.json", { generatedAt: undefined, commit: undefined }),
408
+ source("b.json", { generatedAt: "2026-09-05T00:00:00Z", commit: "bbb" }),
409
+ ]);
410
+
411
+ expect(merged.catalog.generatedAt).toBe("2026-09-05T00:00:00Z");
412
+ expect(merged.catalog.commit).toBe("bbb");
413
+ expect(merged.sources[0]).toEqual({
414
+ path: "authored.json",
415
+ generatedAt: "",
416
+ commit: "",
417
+ });
418
+ });
419
+
420
+ it("keeps a single commit when every source agrees", () => {
421
+ const merged = mergeCatalogs([
422
+ source("a.json", { commit: "same" }),
423
+ source("b.json", { commit: "same" }),
424
+ ]);
425
+
426
+ expect(merged.catalog.commit).toBe("same");
427
+ });
428
+ });
429
+
430
+ describe("mergeCatalogs: glossary terms", () => {
431
+ const term = (id: string, context: string, slug: string) => ({
432
+ id,
433
+ slug,
434
+ context,
435
+ name: slug,
436
+ definition: "one sentence.",
437
+ source: "GLOSSARY.md:1",
438
+ });
439
+
440
+ // THE TEST THAT JUSTIFIES THE ID SCHEME.
441
+ //
442
+ // "One meaning per word inside this context" is a claim about a boundary,
443
+ // not about the estate: `User` in auth and `User` in shop are two different
444
+ // things, correctly, and the id is what keeps them from merging into one.
445
+ it("keeps one word in two contexts as two terms", () => {
446
+ const merged = mergeCatalogs([
447
+ source("auth.json", { terms: [term("auth.user", "auth", "user")] }),
448
+ source("shop.json", { terms: [term("shop.user", "shop", "user")] }),
449
+ ]);
450
+
451
+ expect(merged.catalog.terms).toHaveLength(2);
452
+ expect(merged.conflicts).toEqual([]);
453
+ });
454
+
455
+ // And the other half of it: two services of ONE context keeping glossaries
456
+ // that both define the word. Settling that by file order would hide exactly
457
+ // the disagreement the glossary exists to surface.
458
+ it("reports one word defined twice in one context", () => {
459
+ const merged = mergeCatalogs([
460
+ source("a.json", { terms: [term("shop.order", "shop", "order")] }),
461
+ source("b.json", { terms: [term("shop.order", "shop", "order")] }),
462
+ ]);
463
+
464
+ expect(merged.catalog.terms).toHaveLength(1);
465
+ expect(merged.conflicts).toHaveLength(1);
466
+ expect(merged.conflicts[0]?.message).toContain("term");
467
+ expect(merged.conflicts[0]?.message).toContain("a.json");
468
+ });
469
+
470
+ // Optional in the file and absent downstream, exactly like `stores`.
471
+ it("leaves terms off a catalog whose sources carried none", () => {
472
+ const merged = mergeCatalogs([source("a.json", {})]);
473
+
474
+ expect(merged.catalog.terms).toBeUndefined();
475
+ });
476
+ });
477
+
478
+ describe("mergeCatalogs: owners", () => {
479
+ const owned = (id: string, owners: string[]) => {
480
+ const c = context("shop", [id]);
481
+ c.services[0]!.owners = owners;
482
+
483
+ return c;
484
+ };
485
+
486
+ // Two rules that both matched are two facts, not two answers: a service
487
+ // owned by the team that wrote it and by the platform team is owned by both,
488
+ // which is what a reviewer sees on the pull request.
489
+ it("unions owners from two sources", () => {
490
+ const merged = mergeCatalogs([
491
+ source("a.json", { contexts: [owned("shop.oms", ["@acme/oms"])] }),
492
+ source("b.json", { contexts: [owned("shop.oms", ["@acme/platform"])] }),
493
+ ]);
494
+
495
+ const service = merged.catalog.contexts[0]?.services[0];
496
+ expect(service?.owners).toEqual(["@acme/oms", "@acme/platform"]);
497
+ expect(merged.conflicts).toEqual([]);
498
+ });
499
+
500
+ it("says a handle once when both sources name it", () => {
501
+ const merged = mergeCatalogs([
502
+ source("a.json", { contexts: [owned("shop.oms", ["@acme/oms"])] }),
503
+ source("b.json", { contexts: [owned("shop.oms", ["@acme/oms"])] }),
504
+ ]);
505
+
506
+ expect(merged.catalog.contexts[0]?.services[0]?.owners).toEqual(["@acme/oms"]);
507
+ });
508
+
509
+ it("leaves owners off a service nobody claimed", () => {
510
+ const merged = mergeCatalogs([
511
+ source("a.json", { contexts: [context("shop", ["shop.oms"])] }),
512
+ ]);
513
+
514
+ expect(merged.catalog.contexts[0]?.services[0]?.owners).toBeUndefined();
515
+ });
516
+ });
517
+
518
+ describe("mergeCatalogs: repo pins", () => {
519
+ const shop = { repo: "github.com/acme/shop", commit: "c1d2e3f" };
520
+
521
+ // The case that made the pins a union rather than a claim: one fetch step
522
+ // vendors a repository once, and every extractor that reads a service out of
523
+ // that copy answers with the same pin. Two sources agreeing is not a
524
+ // collision, and reporting it would put a conflict on the Problems page for
525
+ // every service the estate vendors.
526
+ it("takes one pin from two sources that agree", () => {
527
+ const merged = mergeCatalogs([
528
+ source("a.json", { repos: [shop] }),
529
+ source("b.json", { repos: [shop] }),
530
+ ]);
531
+
532
+ expect(merged.catalog.repos).toEqual([shop]);
533
+ expect(merged.conflicts).toEqual([]);
534
+ });
535
+
536
+ // Two commits for one repository means one of the fragments was written
537
+ // against a pin that has since moved. Both cannot be true - the copy on disk
538
+ // is at one commit - and a link built from the stale one points at code the
539
+ // reader never saw, so the reader is told rather than the newer one winning
540
+ // silently on file order.
541
+ it("reports one repository pinned to two commits", () => {
542
+ const merged = mergeCatalogs([
543
+ source("a.json", { repos: [shop] }),
544
+ source("b.json", { repos: [{ repo: "github.com/acme/shop", commit: "9999999" }] }),
545
+ ]);
546
+
547
+ expect(merged.catalog.repos).toEqual([shop]);
548
+ expect(merged.conflicts).toHaveLength(1);
549
+ expect(merged.conflicts[0]?.message).toContain("9999999");
550
+ expect(merged.conflicts[0]?.message).toContain("a.json");
551
+ });
552
+
553
+ it("unions pins for different repositories", () => {
554
+ const merged = mergeCatalogs([
555
+ source("a.json", { repos: [shop] }),
556
+ source("b.json", { repos: [{ repo: "github.com/acme/pay", commit: "abc0000" }] }),
557
+ ]);
558
+
559
+ expect(merged.catalog.repos).toHaveLength(2);
560
+ expect(merged.conflicts).toEqual([]);
561
+ });
562
+
563
+ // Optional in the file and absent downstream, exactly like `stores`.
564
+ it("leaves repos off a catalog whose sources carried none", () => {
565
+ const merged = mergeCatalogs([source("a.json", {})]);
566
+
567
+ expect(merged.catalog.repos).toBeUndefined();
568
+ });
569
+ });
570
+
571
+ describe("mergeCatalogs: schema modules", () => {
572
+ const shop = () => ({
573
+ id: "buf.build/acme/shop",
574
+ slug: "acme-shop",
575
+ name: "acme/shop",
576
+ registry: "buf.build",
577
+ packages: ["shop.v1"],
578
+ files: ["shop/v1/orders.proto"],
579
+ source: "proto",
580
+ });
581
+
582
+ // THE TEST THAT JUSTIFIES THE ID SCHEME.
583
+ //
584
+ // The producer and each consumer describe the same module from different
585
+ // repositories, and neither knows what the other calls it. Only a
586
+ // registry-global id makes them one entry; an id derived from an owner would
587
+ // grow one module per reader.
588
+ it("unions a module described by a producer and by a consumer into one", () => {
589
+ const merged = mergeCatalogs([
590
+ source("a-producer.json", {
591
+ modules: [{ ...shop(), owner: "shop.oms" }],
592
+ }),
593
+ source("b-consumer.json", {
594
+ modules: [{ ...shop(), source: "internal/infrastructure/shop" }],
595
+ }),
596
+ ]);
597
+
598
+ expect(merged.catalog.modules).toHaveLength(1);
599
+ // First source by path wins, as everywhere else at this level.
600
+ expect(merged.catalog.modules?.[0]?.owner).toBe("shop.oms");
601
+ expect(merged.conflicts).toHaveLength(1);
602
+ expect(merged.conflicts[0]?.message).toContain("module");
603
+ });
604
+
605
+ it("keeps two genuinely different modules apart", () => {
606
+ const merged = mergeCatalogs([
607
+ source("a.json", { modules: [shop()] }),
608
+ source("b.json", {
609
+ modules: [
610
+ {
611
+ ...shop(),
612
+ id: "buf.build/acme/pricing",
613
+ slug: "acme-pricing",
614
+ name: "acme/pricing",
615
+ },
616
+ ],
617
+ }),
618
+ ]);
619
+
620
+ expect(merged.catalog.modules).toHaveLength(2);
621
+ expect(merged.conflicts).toEqual([]);
622
+ });
623
+
624
+ // Optional in the file and absent downstream, exactly like `stores`.
625
+ it("leaves modules off a catalog whose sources carried none", () => {
626
+ const merged = mergeCatalogs([source("a.json", {})]);
627
+
628
+ expect(merged.catalog.modules).toBeUndefined();
629
+ });
630
+
631
+ // Two extractors reading two halves of one service - the protos it publishes
632
+ // and the copies it vendors - each name a module, and the service ends up
633
+ // listing both.
634
+ it("unions the modules a service names, across sources", () => {
635
+ const publishes = context("shop", ["shop.oms"]);
636
+ publishes.services = publishes.services.map((s) => ({
637
+ ...s,
638
+ modules: ["buf.build/acme/shop"],
639
+ }));
640
+
641
+ const vendors = context("shop", ["shop.oms"]);
642
+ vendors.services = vendors.services.map((s) => ({
643
+ ...s,
644
+ modules: ["buf.build/acme/shop", "buf.build/acme/pricing"],
645
+ }));
646
+
647
+ const merged = mergeCatalogs([
648
+ source("a-publishes.json", { contexts: [publishes] }),
649
+ source("b-vendors.json", { contexts: [vendors] }),
650
+ ]);
651
+
652
+ expect(merged.catalog.contexts[0]?.services[0]?.modules).toEqual([
653
+ "buf.build/acme/shop",
654
+ "buf.build/acme/pricing",
655
+ ]);
656
+ });
657
+
658
+ // A service may split its bus across several documents, so two sources
659
+ // describing one channel is not a conflict: the address is the identity, and
660
+ // the messages are the union.
661
+ it("unions the messages two documents put on one channel", () => {
662
+ const outgoing = context("shop", ["shop.cart"]);
663
+ outgoing.services = outgoing.services.map((s) => ({
664
+ ...s,
665
+ channels: [
666
+ {
667
+ address: "shop.cart.basket",
668
+ source: "bus/asyncapi.yaml",
669
+ messages: [
670
+ { name: "cart.BasketCreated", direction: "send" as const },
671
+ ],
672
+ },
673
+ ],
674
+ }));
675
+
676
+ const incoming = context("shop", ["shop.cart"]);
677
+ incoming.services = incoming.services.map((s) => ({
678
+ ...s,
679
+ channels: [
680
+ {
681
+ address: "shop.cart.basket",
682
+ source: "policies/asyncapi.yaml",
683
+ messages: [
684
+ { name: "cart.BasketCreated", direction: "send" as const },
685
+ { name: "cart.BasketCheckedOut", direction: "send" as const },
686
+ ],
687
+ },
688
+ {
689
+ address: "auth_session",
690
+ source: "policies/asyncapi.yaml",
691
+ messages: [{ name: "auth.SessionEnded", direction: "receive" as const }],
692
+ },
693
+ ],
694
+ }));
695
+
696
+ const merged = mergeCatalogs([
697
+ source("a-outgoing.json", { contexts: [outgoing] }),
698
+ source("b-incoming.json", { contexts: [incoming] }),
699
+ ]);
700
+
701
+ const channels = merged.catalog.contexts[0]?.services[0]?.channels;
702
+ expect(channels?.map((c) => c.address)).toEqual([
703
+ "shop.cart.basket",
704
+ "auth_session",
705
+ ]);
706
+ expect(channels?.[0]?.messages.map((m) => m.name)).toEqual([
707
+ "cart.BasketCreated",
708
+ "cart.BasketCheckedOut",
709
+ ]);
710
+ // The channel that was here keeps its own source; it is where its prose
711
+ // came from.
712
+ expect(channels?.[0]?.source).toBe("bus/asyncapi.yaml");
713
+ });
714
+ });
715
+
716
+ describe("a second source that has seen the flow run", () => {
717
+ const step = (id: string, status: "declared" | "verified", ref?: string) => ({
718
+ type: "step" as const,
719
+ id,
720
+ from: "client",
721
+ to: "auth.auth",
722
+ kind: "rpc" as const,
723
+ label: id,
724
+ status,
725
+ ...(ref ? { ref } : {}),
726
+ });
727
+ const flow = (steps: Catalog["flows"][number]["steps"]) => ({
728
+ id: "flow.login",
729
+ slug: "login",
730
+ name: "Login",
731
+ summary: "",
732
+ owner: "auth",
733
+ participants: [
734
+ { id: "client", kind: "actor" as const, context: null },
735
+ { id: "auth.auth", kind: "service" as const, context: "auth" },
736
+ ],
737
+ steps,
738
+ });
739
+
740
+ it("raises the steps it vouches for and leaves the rest as declared", () => {
741
+ const merged = mergeCatalogs([
742
+ source("a.json", { flows: [flow([step("s1", "declared"), step("s2", "declared")])] }),
743
+ source("b.json", { flows: [flow([step("s1", "verified"), step("s2", "declared")])] }),
744
+ ]);
745
+
746
+ expect(merged.conflicts).toEqual([]);
747
+ const steps = merged.catalog.flows[0]?.steps ?? [];
748
+ expect(steps.map((s) => (s.type === "step" ? s.status : s.type))).toEqual(["verified", "declared"]);
749
+ });
750
+
751
+ it("raises steps inside frames too", () => {
752
+ const framed = (status: "declared" | "verified") => [
753
+ {
754
+ type: "alt" as const,
755
+ id: "alt1",
756
+ branches: [
757
+ { title: "blocked", terminal: true, steps: [step("s2", status)] },
758
+ { title: "otherwise", steps: [] },
759
+ ],
760
+ },
761
+ ];
762
+ const merged = mergeCatalogs([
763
+ source("a.json", { flows: [flow(framed("declared"))] }),
764
+ source("b.json", { flows: [flow(framed("verified"))] }),
765
+ ]);
766
+
767
+ expect(merged.conflicts).toEqual([]);
768
+ const alt = merged.catalog.flows[0]?.steps[0];
769
+ expect(alt?.type === "alt" && alt.branches[0]?.steps[0]?.type === "step" ? alt.branches[0].steps[0].status : "").toBe("verified");
770
+ });
771
+
772
+ it("is a conflict when it disagrees about anything but status", () => {
773
+ const merged = mergeCatalogs([
774
+ source("a.json", { flows: [flow([step("s1", "declared")])] }),
775
+ source("b.json", { flows: [flow([step("s1", "verified", "auth.v1.Users/login")])] }),
776
+ ]);
777
+
778
+ expect(merged.conflicts).toHaveLength(1);
779
+ expect(merged.conflicts[0]?.message).toContain("already declared");
780
+ const first = merged.catalog.flows[0]?.steps[0];
781
+ expect(first?.type === "step" ? first.status : "").toBe("declared");
782
+ });
783
+
784
+ it("does not touch the source it was handed", () => {
785
+ const a = source("a.json", { flows: [flow([step("s1", "declared")])] });
786
+ mergeCatalogs([a, source("b.json", { flows: [flow([step("s1", "verified")])] })]);
787
+
788
+ const first = a.catalog.flows[0]?.steps[0];
789
+ expect(first?.type === "step" ? first.status : "").toBe("declared");
790
+ });
791
+
792
+ it("lets verified win for a consumer and a call declared twice", () => {
793
+ const withEdges = (status: "declared" | "verified") => {
794
+ const ctx = context("shop", ["shop.oms"]);
795
+ ctx.services[0]!.consumes = [{ id: "psp.v2.Charges/Create", peer: "psp", status, source: "x" }];
796
+ ctx.services[0]!.aggregates = [
797
+ {
798
+ id: "shop.oms.order",
799
+ slug: "order",
800
+ name: "Order",
801
+ readme: "",
802
+ root: "Order",
803
+ entities: [],
804
+ valueObjects: [],
805
+ operations: [],
806
+ events: [
807
+ {
808
+ id: "shop.oms.order.OrderPlaced",
809
+ slug: "order-placed",
810
+ name: "OrderPlaced",
811
+ versions: [{ version: "v1", doc: "", source: "", fields: [] }],
812
+ consumers: [{ service: "payments.ledger", status, note: status === "verified" ? "seen in traces" : "" }],
813
+ },
814
+ ],
815
+ },
816
+ ];
817
+ return ctx;
818
+ };
819
+ const merged = mergeCatalogs([
820
+ source("a.json", { contexts: [withEdges("declared")] }),
821
+ source("b.json", { contexts: [withEdges("verified")] }),
822
+ ]);
823
+
824
+ expect(merged.conflicts).toEqual([]);
825
+ const service = merged.catalog.contexts[0]?.services[0];
826
+ expect(service?.consumes[0]?.status).toBe("verified");
827
+ const consumer = service?.aggregates[0]?.events[0]?.consumers[0];
828
+ expect(consumer?.status).toBe("verified");
829
+ expect(consumer?.note).toBe("seen in traces");
830
+ });
831
+ });
832
+
833
+ // An external is described the way a service is: by more than one source,
834
+ // none of which knows the others exist.
835
+ describe("externals", () => {
836
+ const charges = { id: "psp.v1.Charges", methods: [{ name: "Create" }], source: "psp/openapi.yaml" };
837
+ const refunds = { id: "psp.v1.Refunds", methods: [{ name: "Create" }], source: "psp/openapi.yaml" };
838
+
839
+ it("unions the interfaces two sources read, and keeps the first name", () => {
840
+ const merged = mergeCatalogs([
841
+ source("a.json", { externals: [{ id: "psp", slug: "psp", name: "", summary: "", provides: [charges] }] }),
842
+ source("b.json", { externals: [{ id: "psp", slug: "psp", name: "PSP", summary: "the network", url: "https://psp.example", provides: [refunds, charges] }] }),
843
+ ]);
844
+
845
+ expect(merged.conflicts).toEqual([]);
846
+ expect(merged.catalog.externals).toHaveLength(1);
847
+ const psp = merged.catalog.externals?.[0];
848
+ expect(psp?.name).toBe("PSP");
849
+ expect(psp?.url).toBe("https://psp.example");
850
+ expect(psp?.provides.map((p) => p.id)).toEqual(["psp.v1.Charges", "psp.v1.Refunds"]);
851
+ });
852
+
853
+ it("records a second name as a conflict rather than renaming", () => {
854
+ const merged = mergeCatalogs([
855
+ source("a.json", { externals: [{ id: "psp", slug: "psp", name: "PSP", summary: "", provides: [] }] }),
856
+ source("b.json", { externals: [{ id: "psp", slug: "psp", name: "Stripe", summary: "", provides: [] }] }),
857
+ ]);
858
+
859
+ expect(merged.catalog.externals?.[0]?.name).toBe("PSP");
860
+ expect(merged.conflicts.map((c) => c.where)).toEqual(["psp"]);
861
+ });
862
+
863
+ it("enriches a called SOAP method with the full WSDL contract regardless of file order", () => {
864
+ const id = "booking.soap.BookingPort";
865
+ const merged = mergeCatalogs([
866
+ source("http-clients.json", {
867
+ externals: [{
868
+ id: "booking", slug: "booking", name: "Booking", summary: "", provides: [{
869
+ id, source: "contracts/booking.wsdl", methods: [{ name: "Cancel" }],
870
+ }],
871
+ }],
872
+ }),
873
+ source("wsdl.json", {
874
+ externals: [{
875
+ id: "booking", slug: "booking", name: "Booking", summary: "", provides: [{
876
+ id, source: "contracts/booking.wsdl",
877
+ methods: [{
878
+ name: "Cancel", request: "CancelRequest", response: "CancelResponse",
879
+ soap: { version: "1.2", action: "urn:booking:cancel", faults: ["BookingFault"] },
880
+ }],
881
+ messages: [
882
+ { name: "CancelRequest", fields: [{ name: "bookingId", type: "string", doc: "" }] },
883
+ { name: "CancelResponse", fields: [] },
884
+ ],
885
+ }],
886
+ }],
887
+ }),
888
+ ]);
889
+
890
+ const provided = merged.catalog.externals?.[0]?.provides[0];
891
+ expect(provided?.methods).toEqual([{
892
+ name: "Cancel", request: "CancelRequest", response: "CancelResponse",
893
+ soap: { version: "1.2", action: "urn:booking:cancel", faults: ["BookingFault"] },
894
+ }]);
895
+ expect(provided?.messages?.map((message) => message.name)).toEqual([
896
+ "CancelRequest",
897
+ "CancelResponse",
898
+ ]);
899
+ });
900
+
901
+ it("leaves the field out when no source names one", () => {
902
+ const merged = mergeCatalogs([source("a.json", { contexts: [context("shop", ["shop.oms"])] })]);
903
+ expect(merged.catalog.externals).toBeUndefined();
904
+ });
905
+ });
906
+
907
+ describe("mergeCatalogs: maps", () => {
908
+ // The store extractor read `q.ID()` and `q.BasketID()` off the repository;
909
+ // the domain extractor read `id` and `basketID` off the struct. Once both
910
+ // are in one catalog the column can be spelled the way the field is, which
911
+ // is the only spelling every reader looks a field up by.
912
+ const domain = () => {
913
+ const c = context("shop", ["shop.pricing"]);
914
+ c.services[0]!.aggregates = [
915
+ {
916
+ id: "shop.pricing.quote",
917
+ slug: "quote",
918
+ name: "Quote",
919
+ readme: "",
920
+ root: "Quote",
921
+ entities: [
922
+ {
923
+ id: "shop.pricing.quote.quote",
924
+ slug: "quote",
925
+ name: "Quote",
926
+ doc: "",
927
+ fields: [
928
+ { name: "id", type: "string", doc: "" },
929
+ { name: "basketID", type: "string", doc: "" },
930
+ { name: "total", type: "Money", doc: "" },
931
+ { name: "lines", type: "[]line.Line", doc: "" },
932
+ ],
933
+ },
934
+ ],
935
+ valueObjects: [
936
+ {
937
+ id: "shop.pricing.quote.line",
938
+ slug: "line",
939
+ name: "Line",
940
+ doc: "",
941
+ fields: [
942
+ { name: "sku", type: "string", doc: "" },
943
+ { name: "unitPrice", type: "money.Money", doc: "" },
944
+ ],
945
+ },
946
+ {
947
+ id: "shop.pricing.quote.money",
948
+ slug: "money",
949
+ name: "Money",
950
+ doc: "",
951
+ fields: [{ name: "amountMinor", type: "int64", doc: "" }],
952
+ },
953
+ ],
954
+ operations: [],
955
+ events: [],
956
+ },
957
+ ];
958
+
959
+ return c;
960
+ };
961
+ const store = (maps: Record<string, string>) => ({
962
+ id: "shop.pricing.pg",
963
+ slug: "pg",
964
+ name: "pg",
965
+ kind: "postgres" as const,
966
+ owner: "shop.pricing",
967
+ tables: [
968
+ {
969
+ id: "shop.pricing.pg.quotes",
970
+ name: "quotes",
971
+ persists: { aggregate: "shop.pricing.quote" },
972
+ columns: Object.entries(maps).map(([name, m]) => ({
973
+ name,
974
+ type: "text",
975
+ nullable: false,
976
+ maps: m,
977
+ })),
978
+ },
979
+ ],
980
+ });
981
+
982
+ it("spells a column's field the way the aggregate declares it", () => {
983
+ const merged = mergeCatalogs([
984
+ source("domain.json", { contexts: [domain()] }),
985
+ source("stores.json", {
986
+ stores: [
987
+ store({
988
+ id: "Quote.ID",
989
+ basket_id: "Quote.BasketID",
990
+ total_minor: "Quote.Total.AmountMinor",
991
+ currency: "Quote.total",
992
+ state: "Quote.State",
993
+ sku: "Quote.Lines.SKU",
994
+ unit_price_minor: "Quote.Lines.UnitPrice.AmountMinor",
995
+ weight: "Quote.Lines.Weight.Grams",
996
+ }),
997
+ ],
998
+ }),
999
+ ]);
1000
+
1001
+ const columns = merged.catalog.stores?.[0]?.tables[0]?.columns ?? [];
1002
+ const maps = Object.fromEntries(columns.map((c) => [c.name, c.maps]));
1003
+ expect(maps).toEqual({
1004
+ id: "Quote.id",
1005
+ basket_id: "Quote.basketID",
1006
+ total_minor: "Quote.total.amountMinor",
1007
+ currency: "Quote.total",
1008
+ // No field of any case is called State: the claim stays as the code
1009
+ // made it, for a reader to see rather than for the merge to hide.
1010
+ state: "Quote.State",
1011
+ // Through the elements of a field, block by block, as far as the
1012
+ // declarations reach; what lies beyond them stays as read.
1013
+ sku: "Quote.lines.sku",
1014
+ unit_price_minor: "Quote.lines.unitPrice.amountMinor",
1015
+ weight: "Quote.lines.Weight.Grams",
1016
+ });
1017
+ });
1018
+
1019
+ it("leaves the sources' own objects alone", () => {
1020
+ const stores = [store({ id: "Quote.ID" })];
1021
+ mergeCatalogs([
1022
+ source("domain.json", { contexts: [domain()] }),
1023
+ source("stores.json", { stores }),
1024
+ ]);
1025
+
1026
+ expect(stores[0]!.tables[0]!.columns[0]!.maps).toBe("Quote.ID");
1027
+ });
1028
+
1029
+ it("does nothing for a table that persists no aggregate this catalog knows", () => {
1030
+ const merged = mergeCatalogs([
1031
+ source("stores.json", { stores: [store({ id: "Quote.ID" })] }),
1032
+ ]);
1033
+
1034
+ expect(merged.catalog.stores?.[0]?.tables[0]?.columns[0]?.maps).toBe(
1035
+ "Quote.ID",
1036
+ );
1037
+ });
1038
+ });