@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
package/src/merge.ts ADDED
@@ -0,0 +1,1004 @@
1
+ // Merging sources into one catalog.
2
+ //
3
+ // The estate is not written down in one place. A service publishes facts about
4
+ // itself next to its own code, an authored file carries the prose nobody can
5
+ // generate, and what a reader sees is the union. There is no master file, and
6
+ // deliberately so: a single catalog.json would have to be written by whoever
7
+ // owns the repository it lives in, which is exactly the bottleneck this avoids.
8
+ //
9
+ // Two things follow from that, and both are the point rather than the cost:
10
+ //
11
+ // - Merging happens BEFORE validation. Referential integrity is a property of
12
+ // the union, not of any one source, so a source that names a peer it does
13
+ // not own is normal rather than broken.
14
+ // - A conflict is a value, not an exception. Two sources disagreeing about a
15
+ // context's name is a fact about the estate that belongs on the Problems
16
+ // page, next to the other things that are true and unfortunate.
17
+
18
+ import type {
19
+ Aggregate,
20
+ Alt,
21
+ AltBranch,
22
+ Block,
23
+ BoundedContext,
24
+ Catalog,
25
+ Channel,
26
+ External,
27
+ Field,
28
+ Flow,
29
+ FlowNode,
30
+ Loop,
31
+ Parallel,
32
+ RpcMethod,
33
+ RpcService,
34
+ Service,
35
+ Status,
36
+ Step,
37
+ Store,
38
+ TypeDef,
39
+ } from "./catalog";
40
+
41
+ /**
42
+ * A catalog as a FILE may carry it, which is not quite a catalog as a reader
43
+ * gets one: the two stamps are optional here and required downstream.
44
+ *
45
+ * The host stamps what it generates, from the last commit that touched the
46
+ * input the fragment was read from. A file nobody generates - the estate's
47
+ * hand-written facts - is stamped by nobody, and a provenance typed in by hand
48
+ * is a claim no process keeps true: it stays where it was typed while the
49
+ * estate moves on, and being the oldest stamp in the corpus it then dates the
50
+ * whole merged catalog. So a source is allowed to carry no stamp at all, and
51
+ * saying nothing is treated as what it is - an absence of evidence, which
52
+ * drops out of the merge rather than voting in it.
53
+ */
54
+ export type SourceCatalog = Omit<Catalog, "generatedAt" | "commit"> &
55
+ Partial<Pick<Catalog, "generatedAt" | "commit">>;
56
+
57
+ /** One file, parsed. */
58
+ export interface CatalogSource {
59
+ /** Where it was read from, as a reader would type it. */
60
+ path: string;
61
+ catalog: SourceCatalog;
62
+ }
63
+
64
+ /**
65
+ * When a source was generated and from what. This is per-source and never
66
+ * merged into one number: a catalog whose domain facts are a day old and whose
67
+ * schema facts are a month old is not "three weeks stale", it is two claims of
68
+ * different ages, and averaging them hides the one that matters.
69
+ *
70
+ * Both fields are empty for a source that carries no stamp, so the shape of a
71
+ * stamp does not depend on whether there was one to read.
72
+ */
73
+ export interface SourceStamp {
74
+ path: string;
75
+ generatedAt: string;
76
+ commit: string;
77
+ }
78
+
79
+ export interface MergeConflict {
80
+ /** The source that lost - the one whose value was not taken. */
81
+ path: string;
82
+ /** What the conflict is about, as a catalog id. */
83
+ where: string;
84
+ message: string;
85
+ }
86
+
87
+ export interface MergeResult {
88
+ catalog: Catalog;
89
+ sources: SourceStamp[];
90
+ conflicts: MergeConflict[];
91
+ }
92
+
93
+ /**
94
+ * Merges sources in path order, so the result never depends on the order a
95
+ * glob happened to return.
96
+ *
97
+ * First writer wins for anything scalar, and the loser is recorded rather than
98
+ * dropped silently. Lists are unioned by id.
99
+ */
100
+ export function mergeCatalogs(sources: CatalogSource[]): MergeResult {
101
+ const ordered = [...sources].sort((a, b) => a.path.localeCompare(b.path));
102
+ const conflicts: MergeConflict[] = [];
103
+
104
+ const contexts = new Map<string, BoundedContext>();
105
+ const contextOrigin = new Map<string, string>();
106
+ const services = new Map<string, Service>();
107
+ const serviceOrigin = new Map<string, string>();
108
+ const defs: Record<string, TypeDef> = {};
109
+ const defOrigin = new Map<string, string>();
110
+ const flows: Catalog["flows"] = [];
111
+ const adrs: Catalog["adrs"] = [];
112
+ const stores: NonNullable<Catalog["stores"]> = [];
113
+ const modules: NonNullable<Catalog["modules"]> = [];
114
+ const terms: NonNullable<Catalog["terms"]> = [];
115
+ const repos: NonNullable<Catalog["repos"]> = [];
116
+ const repoOrigin = new Map<string, string>();
117
+ const externals = new Map<string, External>();
118
+ const externalOrigin = new Map<string, string>();
119
+ const seen = new Map<string, string>(); // flow/adr/store/module/term id -> source path
120
+
121
+ const stamps: SourceStamp[] = ordered.map((source) => ({
122
+ path: source.path,
123
+ generatedAt: source.catalog.generatedAt ?? "",
124
+ commit: source.catalog.commit ?? "",
125
+ }));
126
+
127
+ for (const { path, catalog } of ordered) {
128
+ for (const context of catalog.contexts) {
129
+ const existing = contexts.get(context.id);
130
+ if (!existing) {
131
+ // The services list is filled from the merge below, not copied: a
132
+ // service can be described by several sources at once, and the context
133
+ // has to end up holding the merged one rather than the first arrival.
134
+ contexts.set(context.id, { ...context, services: [] });
135
+ contextOrigin.set(context.id, path);
136
+ } else {
137
+ // A second source may add services to a context somebody else named.
138
+ // What it may not do is quietly rename it.
139
+ const owner = contextOrigin.get(context.id) ?? "";
140
+ // First NON-EMPTY wins, not first. A fragment describing one aspect of
141
+ // a service leaves the fields it knows nothing about empty, and it can
142
+ // easily sort before the one that fills them in - so an empty value
143
+ // yields rather than winning and calling the real one a conflict.
144
+ for (const field of ["name", "summary"] as const) {
145
+ const theirs = context[field];
146
+ if (!theirs) continue;
147
+ if (!existing[field]) {
148
+ existing[field] = theirs;
149
+
150
+ continue;
151
+ }
152
+ if (theirs !== existing[field]) {
153
+ conflicts.push({
154
+ path,
155
+ where: context.id,
156
+ message: `context "${context.id}" has ${field} "${theirs}" here and "${existing[field]}" in ${owner}; the first one is used`,
157
+ });
158
+ }
159
+ }
160
+
161
+ // Classification is its own line only because it is not a plain string:
162
+ // the same rule, spelled where the type can see it.
163
+ if (context.classification) {
164
+ if (!existing.classification) {
165
+ existing.classification = context.classification;
166
+ } else if (existing.classification !== context.classification) {
167
+ conflicts.push({
168
+ path,
169
+ where: context.id,
170
+ message: `context "${context.id}" is classified ${context.classification} here and ${existing.classification} in ${owner}; the first one is used`,
171
+ });
172
+ }
173
+ }
174
+ if (context.kind) {
175
+ if (!existing.kind) {
176
+ existing.kind = context.kind;
177
+ } else if (existing.kind !== context.kind) {
178
+ conflicts.push({
179
+ path,
180
+ where: context.id,
181
+ message: `context "${context.id}" has kind ${context.kind} here and ${existing.kind} in ${owner}; the first one is used`,
182
+ });
183
+ }
184
+ }
185
+ }
186
+
187
+ for (const service of context.services) {
188
+ mergeService(services, serviceOrigin, service, path, conflicts);
189
+ }
190
+ }
191
+
192
+ for (const [key, def] of Object.entries(catalog.defs)) {
193
+ const existing = defs[key];
194
+ if (existing === undefined) {
195
+ defs[key] = def;
196
+ defOrigin.set(key, path);
197
+
198
+ continue;
199
+ }
200
+ // Shared types live in one namespace on purpose: `ref` is a bare key, so
201
+ // two sources meaning different things by "Money" is a real problem and
202
+ // not one a prefix would solve - it would only make it invisible.
203
+ if (JSON.stringify(existing) !== JSON.stringify(def)) {
204
+ conflicts.push({
205
+ path,
206
+ where: `defs.${key}`,
207
+ message: `shared type "${key}" is defined differently here than in ${defOrigin.get(key)}; the first one is used`,
208
+ });
209
+ }
210
+ }
211
+
212
+ for (const flow of catalog.flows) {
213
+ const owner = seen.get(flow.id);
214
+ if (owner === undefined) {
215
+ seen.set(flow.id, path);
216
+ flows.push(flow);
217
+
218
+ continue;
219
+ }
220
+ // A second declaration of the same flow is allowed to say one thing:
221
+ // that some of its steps have been seen running. That is what a source
222
+ // built from traces knows and a source built from code cannot, and the
223
+ // two meet here, on the step. Anything else about the flow - a lane, a
224
+ // hop, a branch - has to agree, or the second one is the conflict it
225
+ // always was.
226
+ const at = flows.findIndex((f) => f.id === flow.id);
227
+ const raised = overlayFlow(flows[at]!, flow);
228
+ if (raised) {
229
+ flows[at] = raised;
230
+
231
+ continue;
232
+ }
233
+ conflicts.push({
234
+ path,
235
+ where: flow.id,
236
+ message: `flow "${flow.id}" is already declared in ${owner}; the one here is ignored`,
237
+ });
238
+ }
239
+ for (const adr of catalog.adrs) {
240
+ if (claim(seen, adr.id, path, conflicts, "ADR")) adrs.push(adr);
241
+ }
242
+ for (const store of catalog.stores ?? []) {
243
+ if (claim(seen, store.id, path, conflicts, "store")) stores.push(store);
244
+ }
245
+ // Claimed by id like everything else at this level, and that is exactly why
246
+ // a module's id is its registry-global name rather than one derived from an
247
+ // owner: the producer and each consumer describe the same module from
248
+ // different repositories, and only a shared id makes them one entry here.
249
+ for (const module of catalog.modules ?? []) {
250
+ if (claim(seen, module.id, path, conflicts, "module"))
251
+ modules.push(module);
252
+ }
253
+ // A term's id carries its context, so the same word in two contexts is two
254
+ // entries and merges as two. What collides here is one word defined twice
255
+ // inside ONE context - two services of it keeping glossaries that disagree
256
+ // - which is the failure a glossary is written to prevent, reported where
257
+ // the reader can see both sources rather than settled by file order.
258
+ for (const term of catalog.terms ?? []) {
259
+ if (claim(seen, term.id, path, conflicts, "term")) terms.push(term);
260
+ }
261
+ // Not claimed by id like the rest, because agreeing is the normal case
262
+ // rather than a collision: one fetch step writes one fragment per
263
+ // repository, and two extractors reading two services out of that one copy
264
+ // are describing the same pin. Saying the same thing twice is silence.
265
+ // Saying two different things is the conflict - the copy on disk is at one
266
+ // commit, so a second answer means a fragment left over from a pin that
267
+ // moved, and a link built from it would point at code the reader never saw.
268
+ for (const pin of catalog.repos ?? []) {
269
+ const owner = repoOrigin.get(pin.repo);
270
+ if (owner === undefined) {
271
+ repoOrigin.set(pin.repo, path);
272
+ repos.push(pin);
273
+
274
+ continue;
275
+ }
276
+ const held = repos.find((r) => r.repo === pin.repo)!;
277
+ if (held.commit === pin.commit) continue;
278
+ conflicts.push({
279
+ path,
280
+ where: pin.repo,
281
+ message: `repository "${pin.repo}" is pinned to ${pin.commit} here and to ${held.commit} in ${owner}; the first one is used`,
282
+ });
283
+ }
284
+ // Merged like a service rather than claimed like a store, and for the
285
+ // same reason a service is: one source reads the document a third party
286
+ // publishes and knows what it answers on, another names it and says what
287
+ // it is for, and neither has to know the other exists.
288
+ for (const external of catalog.externals ?? []) {
289
+ mergeExternal(externals, externalOrigin, external, path, conflicts);
290
+ }
291
+ }
292
+
293
+ // Services go back into their contexts once every source has been read, in
294
+ // the order they were first declared.
295
+ for (const service of services.values()) {
296
+ const contextID = service.id.slice(0, service.id.indexOf("."));
297
+ contexts.get(contextID)?.services.push(service);
298
+ }
299
+
300
+ const merged: Catalog = {
301
+ // The oldest stamp, because a merged catalog is exactly as fresh as its
302
+ // stalest part. Per-source stamps travel alongside for anything that wants
303
+ // to be precise about which part that is - the Overview stamp opens onto
304
+ // them, which is what makes "6 sources" a number a reader can go and read.
305
+ //
306
+ // Only stamped sources are counted. An unstamped one is not the stalest
307
+ // part of anything; it is a part with no age, and a merge that let it vote
308
+ // would report the age of the file nobody generates.
309
+ generatedAt: oldest(stamps.map((s) => s.generatedAt)),
310
+ commit: describeCommits(stamps),
311
+ contexts: [...contexts.values()],
312
+ defs,
313
+ flows,
314
+ adrs,
315
+ };
316
+ if (stores.length > 0) merged.stores = foldMaps(merged, stores);
317
+ if (modules.length > 0) merged.modules = modules;
318
+ if (terms.length > 0) merged.terms = terms;
319
+ if (repos.length > 0) merged.repos = repos;
320
+ if (externals.size > 0) merged.externals = [...externals.values()];
321
+
322
+ return { catalog: merged, sources: stamps, conflicts };
323
+ }
324
+
325
+ /**
326
+ * Spells every column's `maps` path the way the aggregate declares the field.
327
+ *
328
+ * An extractor reads the path off the code that writes the row, and what the
329
+ * code says there is the accessor - `q.ID()`, `q.BasketID()` - while the field
330
+ * behind it is `id` or `basketID`. One name in two cases, and the extractor
331
+ * that read the store never saw the domain to know which. Here both have
332
+ * arrived, so the store's spelling is folded onto the domain's when exactly
333
+ * one declared field matches ignoring case. A path that already names a field,
334
+ * or names none, is left as read: the first needs nothing, and the second is
335
+ * a claim the reader should see as the code made it.
336
+ *
337
+ * Returns new stores rather than rewriting the sources' own objects.
338
+ */
339
+ function foldMaps(catalog: Catalog, stores: Store[]): Store[] {
340
+ const aggregates = new Map<string, Aggregate>();
341
+ for (const context of catalog.contexts) {
342
+ for (const service of context.services) {
343
+ for (const aggregate of service.aggregates) {
344
+ aggregates.set(aggregate.id, aggregate);
345
+ }
346
+ }
347
+ }
348
+
349
+ // The same reading `blockFields` gives a page; repeated rather than
350
+ // imported because this file is loaded by node as it is, without a bundler
351
+ // to resolve a value import of the catalog module.
352
+ const fieldsOf = (block: Block): Field[] =>
353
+ block.fields ?? (block.ref ? catalog.defs[block.ref]?.fields : []) ?? [];
354
+ const blockNamed = (
355
+ aggregate: Aggregate,
356
+ name: string,
357
+ ): Block | undefined => {
358
+ const blocks = [...aggregate.valueObjects, ...aggregate.entities];
359
+ return (
360
+ blocks.find((b) => b.name === name) ??
361
+ blocks.find((b) => b.name.toLowerCase() === name.toLowerCase())
362
+ );
363
+ };
364
+ // The block a field's type names, when it names one of this aggregate's:
365
+ // `Scan[]`, `[]line.Line` and `Money | undefined` all point at a block by
366
+ // the last word of the type, whatever the language wrapped it in.
367
+ const blockOf = (aggregate: Aggregate, field: Field): Block | undefined => {
368
+ const word = field.type
369
+ .replace(/\s*\|\s*(undefined|null)\s*$/g, "")
370
+ .replace(/^\[\]|\[\]$/g, "")
371
+ .replace(/^readonly\s+/, "")
372
+ .split(".")
373
+ .pop();
374
+ return word ? blockNamed(aggregate, word) : undefined;
375
+ };
376
+ const fieldNamed = (fields: Field[], name: string): Field | undefined => {
377
+ const exact = fields.find((f) => f.name === name);
378
+ if (exact) return exact;
379
+ const same = fields.filter(
380
+ (f) => f.name.toLowerCase() === name.toLowerCase(),
381
+ );
382
+ return same.length === 1 ? same[0] : undefined;
383
+ };
384
+
385
+ const fold = (aggregate: Aggregate, maps: string): string => {
386
+ const [head, ...path] = maps.split(".");
387
+ if (!head || path.length === 0) return maps;
388
+ let block = blockNamed(aggregate, head);
389
+ if (!block) return maps;
390
+ const spelled: string[] = [block.name];
391
+ for (const segment of path) {
392
+ const field = block ? fieldNamed(fieldsOf(block), segment) : undefined;
393
+ if (!field) {
394
+ // Nothing declared matches from here on: the rest stays as read,
395
+ // a claim for the reader to see rather than for the merge to hide.
396
+ spelled.push(segment);
397
+ block = undefined;
398
+ continue;
399
+ }
400
+ spelled.push(field.name);
401
+ block = blockOf(aggregate, field);
402
+ }
403
+ return spelled.join(".");
404
+ };
405
+
406
+ return stores.map((store) => ({
407
+ ...store,
408
+ tables: store.tables.map((table) => {
409
+ const aggregate = table.persists?.aggregate
410
+ ? aggregates.get(table.persists.aggregate)
411
+ : undefined;
412
+ if (!aggregate) return table;
413
+ return {
414
+ ...table,
415
+ columns: table.columns.map((column) => {
416
+ if (!column.maps) return column;
417
+ const maps = fold(aggregate, column.maps);
418
+ return maps === column.maps ? column : { ...column, maps };
419
+ }),
420
+ };
421
+ }),
422
+ }));
423
+ }
424
+
425
+ /**
426
+ * Merges one service into the set.
427
+ *
428
+ * This is what lets a service be described by more than one source at a time,
429
+ * which is the whole point of extracting each aspect separately: one generator
430
+ * reads the domain and knows the aggregates, another reads the OpenAPI spec and
431
+ * knows what the service answers, and neither has to know about the other.
432
+ *
433
+ * Lists are unioned by id; scalars keep the first value and record the loser.
434
+ */
435
+ function mergeService(
436
+ services: Map<string, Service>,
437
+ origin: Map<string, string>,
438
+ incoming: Service,
439
+ path: string,
440
+ conflicts: MergeConflict[],
441
+ ): void {
442
+ const existing = services.get(incoming.id);
443
+ if (!existing) {
444
+ services.set(incoming.id, {
445
+ ...incoming,
446
+ provides: [...incoming.provides],
447
+ consumes: [...incoming.consumes],
448
+ aggregates: incoming.aggregates.map(copyAggregate),
449
+ ...(incoming.stores ? { stores: [...incoming.stores] } : {}),
450
+ ...(incoming.modules ? { modules: [...incoming.modules] } : {}),
451
+ ...(incoming.channels
452
+ ? { channels: incoming.channels.map(copyChannel) }
453
+ : {}),
454
+ ...(incoming.owners ? { owners: [...incoming.owners] } : {}),
455
+ ...(incoming.technologies
456
+ ? { technologies: [...incoming.technologies] }
457
+ : {}),
458
+ ...(incoming.commands
459
+ ? { commands: incoming.commands.map((c) => ({ ...c })) }
460
+ : {}),
461
+ });
462
+ origin.set(incoming.id, path);
463
+
464
+ return;
465
+ }
466
+
467
+ const owner = origin.get(incoming.id) ?? "";
468
+ for (const field of ["name", "repo", "path", "readme"] as const) {
469
+ const theirs = incoming[field];
470
+ if (!theirs) continue;
471
+ if (!existing[field]) {
472
+ existing[field] = theirs;
473
+
474
+ continue;
475
+ }
476
+ if (theirs !== existing[field]) {
477
+ conflicts.push({
478
+ path,
479
+ where: incoming.id,
480
+ message: `service "${incoming.id}" has a different ${field} here than in ${owner}; the first one is used`,
481
+ });
482
+ }
483
+ }
484
+
485
+ if (incoming.kind) {
486
+ if (!existing.kind) {
487
+ existing.kind = incoming.kind;
488
+ } else if (existing.kind !== incoming.kind) {
489
+ conflicts.push({
490
+ path,
491
+ where: incoming.id,
492
+ message: `service "${incoming.id}" has kind ${incoming.kind} here and ${existing.kind} in ${owner}; the first one is used`,
493
+ });
494
+ }
495
+ }
496
+
497
+ if (incoming.technologies?.length) {
498
+ const technologies = existing.technologies ?? [];
499
+ for (const technology of incoming.technologies) {
500
+ if (!technologies.includes(technology)) technologies.push(technology);
501
+ }
502
+ existing.technologies = technologies;
503
+ }
504
+
505
+ mergeInterfaces(existing.provides, incoming.provides);
506
+ appendNew(existing.consumes, incoming.consumes, (c) => c.id, raise);
507
+ mergeAggregates(existing.aggregates, incoming.aggregates);
508
+
509
+ if (incoming.stores?.length) {
510
+ const stores = existing.stores ?? [];
511
+ for (const id of incoming.stores) if (!stores.includes(id)) stores.push(id);
512
+ existing.stores = stores;
513
+ }
514
+
515
+ if (incoming.modules?.length) {
516
+ const modules = existing.modules ?? [];
517
+ for (const id of incoming.modules)
518
+ if (!modules.includes(id)) modules.push(id);
519
+ existing.modules = modules;
520
+ }
521
+
522
+ // Unioned like stores and modules, and for the same reason: two sources
523
+ // naming owners are two rules that both matched, not two answers to one
524
+ // question. A service owned by the platform team and by the team that wrote
525
+ // it is owned by both, which is what the file says and what a reviewer sees.
526
+ if (incoming.owners?.length) {
527
+ const owners = existing.owners ?? [];
528
+ for (const handle of incoming.owners) {
529
+ if (!owners.includes(handle)) owners.push(handle);
530
+ }
531
+ existing.owners = owners;
532
+ }
533
+
534
+ // Keyed by the line a reader types, which is the whole of a command's
535
+ // identity: two fragments listing `make test` read one Makefile, and a
536
+ // second row would be the same command offered twice. Not a conflict
537
+ // worth reporting when the doc or body differ, either - the first one
538
+ // read is kept, the way scalars are.
539
+ if (incoming.commands?.length) {
540
+ const commands = existing.commands ?? [];
541
+ for (const command of incoming.commands) {
542
+ if (!commands.some((c) => c.run === command.run)) {
543
+ commands.push({ ...command });
544
+ }
545
+ }
546
+ existing.commands = commands;
547
+ }
548
+
549
+ if (incoming.channels?.length) {
550
+ // Keyed by address, because that is the whole of a channel's identity. Two
551
+ // documents describing one channel is not a conflict worth reporting - a
552
+ // service may split its bus across several - so the first one keeps the
553
+ // messages it declared and the second adds the ones it knows about.
554
+ const channels = existing.channels ?? [];
555
+ appendNew(
556
+ channels,
557
+ incoming.channels.map(copyChannel),
558
+ (c) => c.address,
559
+ (mine, theirs) => {
560
+ if (!mine.kind && theirs.kind) {
561
+ mine.kind = theirs.kind;
562
+ } else if (mine.kind && theirs.kind && mine.kind !== theirs.kind) {
563
+ conflicts.push({
564
+ path,
565
+ where: incoming.id,
566
+ message: `channel "${mine.address}" of service "${incoming.id}" has kind ${theirs.kind} here and ${mine.kind} in ${owner}; the first one is used`,
567
+ });
568
+ }
569
+ appendNew(
570
+ mine.messages,
571
+ theirs.messages,
572
+ (m) => `${m.direction} ${m.name}`,
573
+ );
574
+
575
+ // Mutated in place: the channel that was here keeps its prose and its
576
+ // source, and gains the messages the other document named.
577
+ return undefined;
578
+ },
579
+ );
580
+ existing.channels = channels;
581
+ }
582
+ }
583
+
584
+ function copyChannel(channel: Channel): Channel {
585
+ return { ...channel, messages: [...channel.messages] };
586
+ }
587
+
588
+ /**
589
+ * Merges one external into the set, the way a service is merged: interfaces
590
+ * unioned by id, scalars keeping the first non-empty value with the loser
591
+ * recorded. There is less to merge - no aggregates, no consumes, no stores -
592
+ * because there is less the catalog may claim about a system it does not own.
593
+ */
594
+ function mergeExternal(
595
+ externals: Map<string, External>,
596
+ origin: Map<string, string>,
597
+ incoming: External,
598
+ path: string,
599
+ conflicts: MergeConflict[],
600
+ ): void {
601
+ const existing = externals.get(incoming.id);
602
+ if (!existing) {
603
+ externals.set(incoming.id, {
604
+ ...incoming,
605
+ provides: [...incoming.provides],
606
+ });
607
+ origin.set(incoming.id, path);
608
+
609
+ return;
610
+ }
611
+
612
+ const owner = origin.get(incoming.id) ?? "";
613
+ for (const field of ["name", "summary", "url"] as const) {
614
+ const theirs = incoming[field];
615
+ if (!theirs) continue;
616
+ if (!existing[field]) {
617
+ existing[field] = theirs;
618
+
619
+ continue;
620
+ }
621
+ if (theirs !== existing[field]) {
622
+ conflicts.push({
623
+ path,
624
+ where: incoming.id,
625
+ message: `external "${incoming.id}" has a different ${field} here than in ${owner}; the first one is used`,
626
+ });
627
+ }
628
+ }
629
+
630
+ mergeInterfaces(existing.provides, incoming.provides);
631
+ }
632
+
633
+ /**
634
+ * An interface is commonly known by two independent sources: a contract
635
+ * extractor carries every method and message shape, while a call-site
636
+ * extractor carries only the methods this repository actually invokes. They
637
+ * share an id on purpose, so the second source enriches the first regardless
638
+ * of filename order instead of losing a whole WSDL/OpenAPI shape.
639
+ */
640
+ function mergeInterfaces(target: RpcService[], incoming: RpcService[]): void {
641
+ for (const theirs of incoming) {
642
+ const mine = target.find((provided) => provided.id === theirs.id);
643
+ if (!mine) {
644
+ target.push(copyInterface(theirs));
645
+ continue;
646
+ }
647
+
648
+ if (!mine.source && theirs.source) mine.source = theirs.source;
649
+ if (!mine.module && theirs.module) mine.module = theirs.module;
650
+ for (const method of theirs.methods) {
651
+ const existing = mine.methods.find((candidate) => candidate.name === method.name);
652
+ if (!existing) {
653
+ mine.methods.push(copyMethod(method));
654
+ } else {
655
+ enrichMethod(existing, method);
656
+ }
657
+ }
658
+
659
+ const messages = mine.messages ?? [];
660
+ for (const message of theirs.messages ?? []) {
661
+ const existing = messages.find((candidate) => candidate.name === message.name);
662
+ if (!existing) {
663
+ messages.push({
664
+ ...message,
665
+ fields: message.fields.map((field) => ({ ...field })),
666
+ ...(message.discriminator
667
+ ? {
668
+ discriminator: {
669
+ ...message.discriminator,
670
+ variants: message.discriminator.variants.map((variant) => ({ ...variant })),
671
+ },
672
+ }
673
+ : {}),
674
+ });
675
+ continue;
676
+ }
677
+ appendNew(existing.fields, message.fields.map((field) => ({ ...field })), (field) => field.name);
678
+ if (!existing.discriminator && message.discriminator) {
679
+ existing.discriminator = {
680
+ ...message.discriminator,
681
+ variants: message.discriminator.variants.map((variant) => ({ ...variant })),
682
+ };
683
+ }
684
+ }
685
+ if (messages.length > 0) mine.messages = messages;
686
+ }
687
+ }
688
+
689
+ function copyInterface(provided: RpcService): RpcService {
690
+ return {
691
+ ...provided,
692
+ methods: provided.methods.map(copyMethod),
693
+ ...(provided.messages
694
+ ? {
695
+ messages: provided.messages.map((message) => ({
696
+ ...message,
697
+ fields: message.fields.map((field) => ({ ...field })),
698
+ ...(message.discriminator
699
+ ? {
700
+ discriminator: {
701
+ ...message.discriminator,
702
+ variants: message.discriminator.variants.map((variant) => ({ ...variant })),
703
+ },
704
+ }
705
+ : {}),
706
+ })),
707
+ }
708
+ : {}),
709
+ };
710
+ }
711
+
712
+ function copyMethod(method: RpcMethod): RpcMethod {
713
+ return {
714
+ ...method,
715
+ ...(method.http ? { http: { ...method.http } } : {}),
716
+ ...(method.soap
717
+ ? {
718
+ soap: {
719
+ ...method.soap,
720
+ ...(method.soap.faults ? { faults: [...method.soap.faults] } : {}),
721
+ ...(method.soap.headers ? { headers: [...method.soap.headers] } : {}),
722
+ },
723
+ }
724
+ : {}),
725
+ };
726
+ }
727
+
728
+ function enrichMethod(mine: RpcMethod, theirs: RpcMethod): void {
729
+ for (const field of ["doc", "request", "requestRef", "response", "responseRef", "streaming"] as const) {
730
+ if (!mine[field] && theirs[field]) mine[field] = theirs[field] as never;
731
+ }
732
+ if (!mine.deprecated && theirs.deprecated) mine.deprecated = true;
733
+ if (!mine.http && theirs.http) mine.http = { ...theirs.http };
734
+ if (!mine.soap && theirs.soap) {
735
+ mine.soap = copyMethod(theirs).soap;
736
+ } else if (mine.soap && theirs.soap) {
737
+ for (const field of ["action", "version", "style", "endpoint", "binding"] as const) {
738
+ if (!mine.soap[field] && theirs.soap[field]) mine.soap[field] = theirs.soap[field] as never;
739
+ }
740
+ mine.soap.faults = [...new Set([...(mine.soap.faults ?? []), ...(theirs.soap.faults ?? [])])];
741
+ mine.soap.headers = [...new Set([...(mine.soap.headers ?? []), ...(theirs.soap.headers ?? [])])];
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Unions aggregates by id, and inside a shared aggregate unions events by id,
747
+ * and inside a shared event unions consumers by service.
748
+ *
749
+ * Union stops one level short of the scalars on purpose. Two sources both
750
+ * describing `shop.oms.order` are the domain extractor and something that
751
+ * knows one more thing about it - typically who listens to its events, which
752
+ * is a fact the producer's repository cannot hold. The fields they both fill
753
+ * in keep the first value like everything else here; what the second one adds
754
+ * is kept rather than thrown away with the whole aggregate.
755
+ */
756
+ function mergeAggregates(
757
+ into: Aggregate[],
758
+ from: Aggregate[],
759
+ ): void {
760
+ const byId = new Map(into.map((a) => [a.id, a]));
761
+ for (const incoming of from) {
762
+ const existing = byId.get(incoming.id);
763
+ if (!existing) {
764
+ const copy = copyAggregate(incoming);
765
+ into.push(copy);
766
+ byId.set(copy.id, copy);
767
+
768
+ continue;
769
+ }
770
+ const events = new Map(existing.events.map((e) => [e.id, e]));
771
+ for (const event of incoming.events) {
772
+ const known = events.get(event.id);
773
+ if (!known) {
774
+ const copy = { ...event, consumers: [...event.consumers] };
775
+ existing.events.push(copy);
776
+ events.set(copy.id, copy);
777
+
778
+ continue;
779
+ }
780
+ appendNew(known.consumers, event.consumers, (c) => c.service, raise);
781
+ }
782
+ }
783
+ }
784
+
785
+ /** A copy deep enough that unioning into it never writes into a source. */
786
+ function copyAggregate(aggregate: Aggregate): Aggregate {
787
+ return {
788
+ ...aggregate,
789
+ events: aggregate.events.map((e) => ({ ...e, consumers: [...e.consumers] })),
790
+ ...(aggregate.enums ? { enums: [...aggregate.enums] } : {}),
791
+ };
792
+ }
793
+
794
+ /**
795
+ * Appends the entries whose id is not already present. An entry that IS
796
+ * present is left alone, unless `meet` is given: then the two are handed to
797
+ * it, because for a few kinds of entry a second source can add to the first
798
+ * without contradicting it.
799
+ */
800
+ function appendNew<T>(
801
+ into: T[],
802
+ from: T[],
803
+ id: (item: T) => string,
804
+ meet?: (existing: T, incoming: T) => T | undefined,
805
+ ): void {
806
+ const at = new Map(into.map((item, i) => [id(item), i]));
807
+ for (const item of from) {
808
+ const i = at.get(id(item));
809
+ if (i !== undefined) {
810
+ const replaced = meet?.(into[i]!, item);
811
+ if (replaced !== undefined) into[i] = replaced;
812
+
813
+ continue;
814
+ }
815
+ at.set(id(item), into.length);
816
+ into.push(item);
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Where two sources describe the same edge, `verified` wins. A code reader
822
+ * says a call is declared; a trace says it happened; the second does not
823
+ * contradict the first, it knows more. Nothing else about the edge moves:
824
+ * the first note stays unless there was none.
825
+ */
826
+ function raise<T extends { status: Status; note?: string }>(
827
+ existing: T,
828
+ incoming: T,
829
+ ): T | undefined {
830
+ if (incoming.status !== "verified" || existing.status === "verified") return undefined;
831
+ const raised = { ...existing, status: "verified" as Status };
832
+ if (!raised.note && incoming.note) raised.note = incoming.note;
833
+
834
+ return raised;
835
+ }
836
+
837
+ /**
838
+ * The same flow, declared twice, where the second declaration differs only in
839
+ * what it can vouch for. Returns a copy of the first with every step the
840
+ * second marks `verified` raised, or undefined when the two disagree about
841
+ * anything but status - a different lane, hop, branch or ref - in which case
842
+ * the second is the ordinary conflict.
843
+ *
844
+ * Only `declared` is raised. `unresolved` means the far end is not in the
845
+ * catalog, and a trace showing the hop does not put it there.
846
+ */
847
+ export function overlayFlow(existing: Flow, incoming: Flow): Flow | undefined {
848
+ const lanes = (flow: Flow) => flow.participants.map((p) => p.id).join(" ");
849
+ if (lanes(existing) !== lanes(incoming)) return undefined;
850
+
851
+ const steps = overlayNodes(existing.steps, incoming.steps);
852
+ if (!steps) return undefined;
853
+
854
+ return { ...existing, steps };
855
+ }
856
+
857
+ function overlayNodes(
858
+ existing: FlowNode[],
859
+ incoming: FlowNode[],
860
+ ): FlowNode[] | undefined {
861
+ if (existing.length !== incoming.length) return undefined;
862
+ const out: FlowNode[] = [];
863
+ for (let i = 0; i < existing.length; i++) {
864
+ const a = existing[i]!;
865
+ const b = incoming[i]!;
866
+ if (a.type !== b.type || a.id !== b.id) return undefined;
867
+ switch (a.type) {
868
+ case "step": {
869
+ const step = b as Step;
870
+ if (
871
+ a.from !== step.from ||
872
+ a.to !== step.to ||
873
+ a.kind !== step.kind ||
874
+ (a.ref ?? "") !== (step.ref ?? "")
875
+ )
876
+ return undefined;
877
+ if (step.status === "verified" && a.status === "declared") {
878
+ out.push({ ...a, status: "verified", note: a.note || step.note });
879
+ } else {
880
+ out.push(a);
881
+ }
882
+ break;
883
+ }
884
+ case "parallel": {
885
+ const other = b as Parallel;
886
+ if (a.branches.length !== other.branches.length) return undefined;
887
+ const branches: FlowNode[][] = [];
888
+ for (let j = 0; j < a.branches.length; j++) {
889
+ const branch = overlayNodes(a.branches[j]!, other.branches[j]!);
890
+ if (!branch) return undefined;
891
+ branches.push(branch);
892
+ }
893
+ out.push({ ...a, branches });
894
+ break;
895
+ }
896
+ case "alt": {
897
+ const other = b as Alt;
898
+ if (a.branches.length !== other.branches.length) return undefined;
899
+ const branches: AltBranch[] = [];
900
+ for (let j = 0; j < a.branches.length; j++) {
901
+ const mine = a.branches[j]!;
902
+ const theirs = other.branches[j]!;
903
+ if (mine.title !== theirs.title) return undefined;
904
+ const steps = overlayNodes(mine.steps, theirs.steps);
905
+ if (!steps) return undefined;
906
+ branches.push({ ...mine, steps });
907
+ }
908
+ out.push({ ...a, branches });
909
+ break;
910
+ }
911
+ case "loop": {
912
+ const other = b as Loop;
913
+ if (a.title !== other.title) return undefined;
914
+ const steps = overlayNodes(a.steps, other.steps);
915
+ if (!steps) return undefined;
916
+ out.push({ ...a, steps });
917
+ break;
918
+ }
919
+ }
920
+ }
921
+
922
+ return out;
923
+ }
924
+
925
+ /**
926
+ * Records who owns an id. Returns false when somebody already did, which is
927
+ * how a duplicate is skipped rather than appended twice.
928
+ */
929
+ function claim(
930
+ owners: Map<string, string>,
931
+ id: string,
932
+ path: string,
933
+ conflicts: MergeConflict[],
934
+ kind: string,
935
+ ): boolean {
936
+ const owner = owners.get(id);
937
+ if (owner === undefined) {
938
+ owners.set(id, path);
939
+
940
+ return true;
941
+ }
942
+
943
+ conflicts.push({
944
+ path,
945
+ where: id,
946
+ message: `${kind} "${id}" is already declared in ${owner}; the one here is ignored`,
947
+ });
948
+
949
+ return false;
950
+ }
951
+
952
+ /**
953
+ * The earliest instant among the stamps, returned as the string the source
954
+ * itself wrote.
955
+ *
956
+ * Instants, not text. The corpus mixes offsets - the host writes `%cI`, so a
957
+ * fragment stamped in Bangkok says `+07:00` and one stamped on a runner says
958
+ * `Z` - and sorting ISO strings compares wall clocks while ignoring the offset
959
+ * that says which wall. `2026-09-05T05:57:24+07:00` sorts after
960
+ * `2026-09-05T00:00:00Z` and happened five hours before it.
961
+ *
962
+ * The winner keeps its own text rather than a normalised rendering of it,
963
+ * because this string is displayed and a reader comparing the header against
964
+ * the fragment on disk should find the same characters in both.
965
+ *
966
+ * A stamp nobody can parse does not win. It is not evidence of being old - it
967
+ * is evidence of being broken - and the alternative, letting it sort first,
968
+ * would date the whole estate from one corrupt file. It is still returned when
969
+ * NOTHING parses, because then it is all there is to show, and a garbled date
970
+ * on the page says "look at the stamps" where an empty one would fail the load
971
+ * with "generatedAt is missing" about a stamp that is plainly present.
972
+ */
973
+ function oldest(stamps: string[]): string {
974
+ const known = stamps.filter(Boolean);
975
+ const dated = known
976
+ .map((iso) => ({ iso, at: Date.parse(iso) }))
977
+ .filter((s) => !Number.isNaN(s.at))
978
+ .sort((a, b) => a.at - b.at);
979
+
980
+ return dated[0]?.iso ?? known[0] ?? "";
981
+ }
982
+
983
+ /**
984
+ * What to show where a single commit used to be. One source keeps its sha; more
985
+ * than one, all agreeing, keeps it too - and a genuine spread says so rather
986
+ * than picking a winner and looking precise.
987
+ *
988
+ * The count is of distinct commits, not of files: twenty-eight fragments
989
+ * written by one commit are one source of facts, and saying "28" would make a
990
+ * reader think the estate came from twenty-eight places. A source nobody
991
+ * stamped is not one of them.
992
+ */
993
+ function describeCommits(stamps: SourceStamp[]): string {
994
+ const commits = [...new Set(stamps.map((s) => s.commit).filter(Boolean))];
995
+ if (commits.length === 1) return commits[0] ?? "";
996
+ if (commits.length === 0) return "";
997
+
998
+ return `${commits.length} sources`;
999
+ }
1000
+
1001
+ /** Every service in the merged catalog, flattened. Callers want this constantly. */
1002
+ export function services(catalog: Catalog): Service[] {
1003
+ return catalog.contexts.flatMap((context) => context.services);
1004
+ }