@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,1160 @@
1
+ # Generators
2
+
3
+ A generator turns the catalog into something else. The built-in generators
4
+ produce Markdown and LLM indexes, standalone Mermaid diagrams, and a Backstage
5
+ Software Catalog bundle; each lives in its own directory beside the plugin.
6
+
7
+ The contract is one JSON message in and one JSON message out:
8
+
9
+ ```
10
+ → { "portolanVersion": "0.1.0", "catalog": { ... }, "options": { ... } }
11
+ ← { "files": [{ "name": "shop/oms/README.md", "contents": "..." }] }
12
+ ```
13
+
14
+ There is a second question, asked with `"kind": "describe"`, and the answer is
15
+ the plugin itself rather than its work:
16
+
17
+ ```
18
+ → { "portolanVersion": "0.1.0", "kind": "describe" }
19
+ ← { "files": [],
20
+ "describe": { "name": "extract-go", "summary": "...",
21
+ "phases": ["extract"],
22
+ "options": { "type": "object", "additionalProperties": false,
23
+ "properties": { "context": { ... } } } } }
24
+ ```
25
+
26
+ The options a plugin takes are facts the source does not carry, so only the
27
+ plugin knows what it can be told. `npm run schema` asks all of them and composes
28
+ `schema/portolan.schema.json`, which an editor reads while the manifest is being
29
+ written and `gen` checks before it runs anything. `additionalProperties: false`
30
+ is what makes that worth having: `encoding/json` drops a field it does not
31
+ recognise, so before this a misspelled option was no option at all and nothing
32
+ said so.
33
+
34
+ A generator **names** files; it never writes them. `scripts/gen.mjs` writes what
35
+ comes back, refuses a name that points outside the output directory, and deletes
36
+ pages that stopped being generated. That is what lets a generator run as a wasm
37
+ module with no directory preopened at all — the sandbox is not a restriction
38
+ worked around, it is the reason the protocol has this shape.
39
+
40
+ Three obligations, and they are the whole of it:
41
+
42
+ 1. **Valid output.** Every file a generator names is committed and read.
43
+ 2. **Determinism.** The same catalog produces byte-identical output, so
44
+ reviewing generated documentation is reviewing a diff. Sort anything that
45
+ comes out of a map; never read a clock.
46
+ 3. **One decisive response.** A malformed request, incompatible protocol,
47
+ unsafe filename or invalid response fails the run. Non-fatal extraction
48
+ notes go to stderr; there is no advisory response property a caller may
49
+ accidentally ignore. A note that opens with `warning: ` is kept beside the
50
+ step in `.portolan/build-report.json` and listed on the Settings page.
51
+
52
+ The repository enforces those obligations with schema/field coverage tests,
53
+ byte-for-byte permutation tests, generated-link and anchor checks, Mermaid
54
+ parser checks, and Backstage relationship validation. A new catalog field must
55
+ either be rendered or be explicitly acknowledged by the relevant exporter.
56
+
57
+ ## Adding one
58
+
59
+ 1. Write it. In Go, a new directory here with a `main` that hands its options
60
+ type to `plugin.Serve`, which reads the request, answers a describe and calls
61
+ the work; `catalog.Catalog` from `github.com/shortlink-org/portolan/catalog`
62
+ is the mirror of the schema, and `internal/goscan` is the tree as syntax -
63
+ the files parsed once, the import path of each package, the string
64
+ constants followed to their literals - which River and Watermill share and
65
+ the next Go extractor should not copy. In Python, the same three things live in
66
+ `pyplugin/` - `protocol.py`, `source.py` for the tree as syntax, and
67
+ `catalog.py` for the fragment shapes - and `extract-django` and
68
+ `extract-celery` are what using them looks like. In any other language,
69
+ anything that speaks the protocol above.
70
+ 2. Describe it. An `options.schema.json` beside the source, embedded with
71
+ `go:embed` and returned in the descriptor. `schematest.Check` in a test keeps
72
+ it from drifting from the options struct: a field renamed on one side and not
73
+ the other fails, and so does an option with no description.
74
+ 3. Build it. For a wasm plugin, `GOOS=wasip1 GOARCH=wasm go build`. Add the
75
+ line to `plugins:build` in `package.json`.
76
+ 4. Declare it in `portolan.json`, under `plugins` (how to run it) and
77
+ `generate` (what to run it on), then run `npm run schema` so the manifest
78
+ schema learns its options.
79
+
80
+ ```json
81
+ {
82
+ "plugins": [
83
+ { "name": "markdown", "wasm": { "url": "file://plugins/gen-markdown.wasm" } }
84
+ ],
85
+ "generate": [
86
+ { "plugin": "markdown", "out": "docs", "options": { "title": "Example estate" } }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ## Lifecycles
92
+
93
+ An aggregate whose root has a status gets a `lifecycle` on the catalog: the
94
+ states, the first being where a new root starts, and one move per edge - the
95
+ method that makes it and the event it hands back. All three extractors read it
96
+ off a table the code keeps, never off the branches of the methods, because the
97
+ table is the claim and the methods are held to it: an edge in the table no
98
+ method makes, a move into a state the table lacks, and a status changed
99
+ outside the one way through the table are each reported.
100
+
101
+ In Go the table is a `go-sdk/fsm` rule set, `var Rules = fsm.TransitionRuleSet{
102
+ StateLive: {EventRevoke: StateRevoked}, …}`, with the states and events as
103
+ string constants; the method whose body calls `TriggerEvent` is the mover, and
104
+ every exported method that hands it a constant makes the edges that constant
105
+ names. The event type a method returns is what its last move publishes - a
106
+ method lapsing a lock and then locking again hands `AccountLocked` back for
107
+ the lock. In TypeScript it is `export const TRANSITIONS = { open: ["checked-out"],
108
+ … }` and the method that assigns `this.status`; see `extract-ts/README.md`. In
109
+ Rust it is `pub const TRANSITIONS: &[(&str, &[&str])] = &[("placed", &["confirmed"]),
110
+ …]` and the method that assigns `self.status`, handed a string or a variant of
111
+ the status enum; see `extract-rust/README.md`. In Python it is that mapping on
112
+ the Django model, beside the `TextChoices` that names the states, and the
113
+ method assigning `self.status` - or django-fsm's `@transition(field=status,
114
+ source=…, target=…)`, which is the same table written one edge at a time; see
115
+ `extract-django/README.md`. In Java it is `TRANSITIONS` beside the status enum
116
+ and the method assigning `this.status`; Java is also the one language here with
117
+ a vocabulary for the model, so the rest of `extract-java` reads what jMolecules
118
+ declares rather than what the layout implies.
119
+ Terminal states are derived on the page - nothing leads out - and never
120
+ written down. A move the clock makes, a session expiring, a lock running out,
121
+ is not a move: nothing runs when it happens, so it is not in the table.
122
+
123
+ ## Enums
124
+
125
+ An aggregate's fields take some of their values from closed sets - a reason,
126
+ a status, a code - and a consumer of its events switches on them. Those sets
127
+ are the aggregate's `enums`: one entry per set, id `<aggregate>.<slug>`,
128
+ the values in declaration order, each with its doc and a `deprecated` mark
129
+ when the source carries one. A status enum is read here as well as by the
130
+ lifecycle: the lifecycle keeps the moves, the enum keeps the doc on each
131
+ value, and a page may draw both.
132
+
133
+ What counts as one is a convention per language, and each extractor's README
134
+ says which. In Go, which has no enum, it is a named type over a basic one -
135
+ `type Reason string` - and a const block whose constants are of that type,
136
+ looked for in the aggregate's package, under `vo/`, and under `event/`;
137
+ a value's name is the constant's literal, because that is what the wire
138
+ carries, and the constant's own name only for an iota. A `Deprecated:`
139
+ paragraph in the doc marks the value. In Rust it is a `pub enum` whose every
140
+ variant is a bare name, the literal an `as_str` answers standing in for the
141
+ variant. In Java it is a top-level enum in the aggregate's package. In proto,
142
+ the enums the messages reach through their fields sit on the interface as
143
+ `enums`, with the numbers the wire uses.
144
+
145
+ ## A repository without a domain model
146
+
147
+ `extract-project` is the baseline extractor for any repository. It reads only
148
+ README and build/deployment manifests, emits a neutral group and component,
149
+ and leaves `aggregates` empty. This is intentional: a package called `domain`
150
+ is not evidence that the project models aggregates. OpenAPI, AsyncAPI,
151
+ GraphQL, proto and SQL extractors merge their facts into the same component.
152
+
153
+ The language-specific domain extractors are optional enrichments. The local
154
+ setup wizard offers one only when it finds the structure that extractor
155
+ requires; for Go this means an aggregate package in either
156
+ `internal/domain/<aggregate>` or `internal/<aggregate>/domain`, containing the
157
+ root struct named after that aggregate. The extractor follows the same choice
158
+ for application use cases, transport adapters, policies, integration-event
159
+ DTOs, and assembly bindings, so horizontal layers and feature slices can
160
+ coexist while a service is being migrated.
161
+
162
+ The SQL extractor follows the same migration path. With no `repositories` or
163
+ `projectors` option it discovers both
164
+ `internal/infrastructure/repository/<aggregate>/migrations` and
165
+ `internal/<aggregate>/infrastructure/repository/migrations` (and the matching
166
+ projector forms), merging every package into one store. An explicit root keeps
167
+ the original collection layout for TypeScript, Rust, Java, or custom trees and
168
+ may also point directly at one feature repository.
169
+
170
+ `extract-river` is another independent enrichment for Go repositories. It
171
+ joins a job argument's `Kind()` to `Client.Insert`/`InsertTx`, the selected
172
+ queue, `Worker[Args].Work`, and `river.AddWorker`. The result is a work-queue
173
+ channel plus a two-hop enqueue/dispatch flow, with payload fields and source
174
+ lines. It does not need aggregates and does not treat a job as a domain event.
175
+
176
+ `extract-redis` finds runtime construction of go-redis, rueidis and redigo
177
+ clients in non-test Go source. That source evidence adds a service-owned Redis
178
+ store to the catalog even when the repository has no SQL migrations or domain
179
+ model. It follows literals, constants, concatenation, `fmt.Sprintf`, helper
180
+ functions and conditional suffixes into common Redis operations, producing
181
+ key patterns with their read/write/delete use, TTL, value type and source. The
182
+ patterns remain Redis keyspaces rather than being presented as SQL tables.
183
+
184
+ `extract-watermill` reads Watermill `Router.AddHandler` and
185
+ `AddNoPublisherHandler` registrations. It resolves literal and constant topics,
186
+ plus defaults on env-config structs, follows direct `Publisher.Publish` calls
187
+ and one-hop publishing helpers, and traces JSON marshal/unmarshal values back to
188
+ their Go payload structs. It carries enclosing `if`/`else` conditions and early
189
+ returns through the handler's control flow: proven alternatives become one
190
+ catalog `alt`; publications whose relationship cannot be proven remain separate
191
+ possible routes. The channels merge normally with AsyncAPI declarations by address. Generic
192
+ `NewEventHandler[T]` and `NewCommandHandler[T]` registrations on Watermill CQRS
193
+ processors are also extracted; fixed topic generators and the standard
194
+ event/command-name generator form are resolved from source.
195
+
196
+ `extract-go-nats` reads nats.go and JetStream calls into the subjects a service
197
+ listens on and publishes to. A call is known by the type it is made on -
198
+ `*nats.Conn`, `nats.JetStreamContext`, `jetstream.JetStream`, `jetstream.Stream` -
199
+ and not by its name, because a service's own bus port has a `Subscribe` too.
200
+ The subject is followed to a literal, a constant, a config default, or a
201
+ parameter; a parameter is followed up to two hops through the callers,
202
+ including calls through an interface the adapter satisfies, which is how a
203
+ port `Subscribe(ctx, topic, name, handler)` reads as the assembly's
204
+ `Subscribe(ctx, cart.Topic, cart.BasketCheckedOut{}.Name(), …)`. When the port
205
+ takes exactly one other string beside the subject, that string is the
206
+ message's name; a direct call names no message, and its direction is in the
207
+ channel's doc. A subject read off a database row is a warning at the call,
208
+ not a channel. Consumer configs give the filter subject and the durable name;
209
+ streams, wildcard subjects and work-queue retention are not read yet.
210
+
211
+ `extract-python-kafka` is the framework-independent Kafka enrichment for
212
+ Python. It recognizes confluent-kafka, kafka-python and aiokafka by their
213
+ imported client types, follows literal topics through constants, environment
214
+ defaults, settings and local factories, and emits generic message streams with
215
+ producer and consumer flows. Only client-side configuration proven in source
216
+ is retained; authentication values are omitted, while broker-side partitions,
217
+ replication and retention remain explicitly unknown. See
218
+ `extract-python-kafka/README.md` for the supported call shapes.
219
+
220
+ `extract-wsdl` reads WSDL 1.1 contracts as structured SOAP APIs. It follows
221
+ local WSDL imports and XSD imports/includes without network access, keeps
222
+ distinct services, ports and SOAP 1.1/1.2 bindings, and records operation
223
+ actions, request/response messages, faults, headers and reachable XSD shapes.
224
+ It can describe a contract implemented by the component or a vendored copy for
225
+ an external system (`mode: external`). Remote imports are reported as missing
226
+ evidence rather than fetched during generation.
227
+
228
+ `extract-http-clients` is the outbound counterpart and does not require a
229
+ domain layout. It reads `net/http` request construction, calls through an
230
+ `oapi-codegen` client, and SOAP `Call`/`CallContext` sites. A generated client
231
+ is joined to the OpenAPI document beside it, so the call uses the document's
232
+ operation id and lands on an external with the contract the document declares.
233
+ SOAP actions are joined to WSDL bindings when the action matches. Generated and
234
+ hand-written wrapper signatures are learned from their call into the SOAP
235
+ transport or their `SOAPAction`/SOAP 1.2 content-type header, so the action,
236
+ request and response positions are taken from code rather than assumed. A
237
+ raw request whose peer or contract cannot be proved is still useful evidence:
238
+ it is emitted as `unresolved`, with its method, path and source line, rather
239
+ than being assigned to a guessed system. Conditions guarding a call and the
240
+ opposite path after an early return are carried into the flow note.
241
+ Calls through local wrappers retain their argument values, including closure
242
+ arguments, so a path and HTTP method declared by a business operation survive
243
+ the trip into the transport. URL-shaped configuration is followed through a
244
+ constructor and client field to the request, and the resulting evidence chain
245
+ is included in the flow instead of presenting a receiver field as an endpoint.
246
+ Inbound composition also follows fixed factories, string-keyed constructor
247
+ maps, capability type assertions, and interface fields wired by composite
248
+ literals, direct assignments, or setters. A standalone flow says whether no
249
+ source caller exists or callers exist but no inbound/asynchronous root was
250
+ proved, so the UI exposes the missing evidence instead of implying a complete
251
+ business path.
252
+ When a provider branch still ends before its transport, the extractor loads
253
+ the module with `go/packages`, builds SSA, and uses `x/tools` VTA to resolve
254
+ calls through interface parameters, function values, return values, and
255
+ interface-typed struct fields. Typed edges are followed only after a concrete
256
+ factory branch is selected: applying context-insensitive VTA to a shared
257
+ dispatcher would otherwise attach every request implementation to every
258
+ endpoint. Factory conditions and HTTP/SOAP meaning continue to come from the
259
+ source extractor. Module loading is read-only and bounded; unavailable private
260
+ dependencies, type errors, or a timeout produce a warning and retain the
261
+ syntax-only result rather than failing generation.
262
+ Routes without a provider factory are also joined to their outbound calls,
263
+ including handlers invoked from closures and methods on locally constructed
264
+ values. A Swagger `@Router` annotation is medium-confidence root evidence for
265
+ handler factories behind custom registries. Direct calls reached through a
266
+ `main → Run`/`Start`/`Bootstrap` assembly path become high-confidence startup
267
+ flows. `AddFunc`, `AfterFunc`, and `Schedule` registrations create scheduled
268
+ roots when their handler reaches an outbound call. Extracted HTTP, callback,
269
+ startup, scheduled, River-job and Watermill-event flows publish that trigger
270
+ provenance; unmatched transport fragments are explicitly `unproven` with low
271
+ confidence.
272
+
273
+ After fragments merge, Portolan composes them into root-oriented,
274
+ cross-protocol flows. The seam is machine evidence rather than a display-name
275
+ match: an exact source function reached by another extractor, a River job's
276
+ queue plus `Kind()`, or a Watermill transport plus topic address.
277
+ Composition is recursive, so one request can continue through provider
278
+ HTTP/SOAP calls, enqueue a River job, enter its worker, and make further
279
+ outbound calls. Ambiguous handoffs remain separate. Each composed flow records
280
+ the source fragment slugs it includes, and the UI exposes that provenance as a
281
+ `cross-protocol` badge. Trigger-bearing job and event flows stay available as
282
+ standalone entry views; transport-only fragments consumed by a proven root are
283
+ removed from the top-level flow list.
284
+
285
+ ## Flows written by hand
286
+
287
+ Some flows will always be written by people: the design doc for something not
288
+ built yet, the reconstruction after an incident, the path no test pins. The
289
+ catalog's JSON is the wrong place to write one - a tree of nodes, a unique id
290
+ per step, every lane declared twice - so `extract-flows` reads a text form
291
+ that reads like the sequence diagram it becomes: one file per flow, one line
292
+ per hop, frames closed by `end`. The estate here declares no such step: every
293
+ flow it shows is read out of code that runs, and one written by hand over files
294
+ nobody wrote would be the single page in the catalog nothing holds to account.
295
+ A repository that has a real one points an `extract-flows` step at the directory
296
+ the files live in.
297
+
298
+ ```markdown
299
+ # Order accepted
300
+ owner: shop
301
+ source: services/oms/test/integration/order_accepted_test.go
302
+
303
+ The narrow slice one integration test pins end to end.
304
+
305
+ ## Participants
306
+ - oms-db: store in shop "oms-db (postgres)"
307
+ - psp-gateway: external "psp-gateway (external)"
308
+
309
+ ## Steps
310
+ shop.oms -> oms-db: insertOrderAndOutboxRow [verified] @internal/oms/adapter/postgres/order_repo.go:141 #a1
311
+ > The order row and the outbox row commit in one transaction.
312
+ shop.oms -> bus: event shop.oms.order.OrderPlaced [verified]
313
+ shop.oms -> shop.pricing: rpc shop.v1.Pricing/GetQuote as "GetQuote (250 ms)"
314
+
315
+ alt score below 40 #alt-risk
316
+ bus -> payments.ledger: event shop.oms.order.OrderPlaced
317
+ else score at or above 40
318
+ shop.oms -> bus: event shop.oms.order.OrderCancelled
319
+ stop
320
+ else
321
+ end
322
+
323
+ par OrderPlaced fan-out
324
+ bus -> payments.ledger: event shop.oms.order.OrderPlaced
325
+ and
326
+ bus -> delivery.core: event shop.oms.order.OrderPlaced
327
+ end
328
+
329
+ loop outbox relay, every 200 ms until the batch is empty
330
+ shop.oms -> oms-db: SELECT ... FOR UPDATE SKIP LOCKED
331
+ end
332
+ ```
333
+
334
+ The head is the name, `owner:` (the context the flow belongs to), an optional
335
+ `source:` (where it was read from; the file itself when left out) and an
336
+ optional `slug:` (the file's name when left out), then the summary. A hop is
337
+ `from -> to: [call|rpc|event] label-or-ref` - `call` when no kind is written -
338
+ followed in any order by `as "label"`, `[status]`, `@where` and `#id`. An
339
+ event names its ref, `shop.oms.order.OrderPlaced`; an rpc names its call id,
340
+ `shop.v1.Pricing/GetQuote`, or, for a call no interface declares - a webhook
341
+ arriving on a route - just a label. The label of an event or rpc is the last
342
+ segment of its ref unless `as` says otherwise; the status is `declared`
343
+ unless written; `@` is a file and line, or wherever the hop was seen. A note is
344
+ the `>` lines under the hop. Ids are numbered unless given, and giving them
345
+ is what keeps deep links to a step stable across edits.
346
+
347
+ `alt <when> … else <when> … end` is a choice; `else` alone is "otherwise", a
348
+ branch with nothing in it is allowed, and `stop` as the last line of a branch
349
+ says the flow ends there rather than rejoining. `par [title] … and … end`
350
+ runs its branches side by side; `loop <until> … end` repeats. Frames nest.
351
+
352
+ Services are known by their `context.service` id, `bus` and `client` by name;
353
+ any other lane is declared under Participants, in the order the lanes should
354
+ be drawn, as `- <id>: <kind> [in <context>] ["label"]`. Lines starting with
355
+ `//` are comments. A mistake fails the run with its file and line, the way a
356
+ compiler would: a flow silently left out is the kind of missing nobody
357
+ notices. A ref that resolves to nothing is caught later, by the validator,
358
+ because only the merged catalog can say.
359
+
360
+ ## Decisions written by hand
361
+
362
+ Every decision worth keeping is already written down, in a file beside the code
363
+ it constrains, in the MADR shape a person reads. Typing it a second time into
364
+ the catalog's JSON - an id, a slug, a number, a scope and a body with every
365
+ newline escaped - makes the JSON the source and the markdown a copy, and the
366
+ copy is the one that goes stale: the three records that lived in
367
+ `data/catalog.json` before `extract-adr` existed all named files that were
368
+ never in the repository at all.
369
+
370
+ So the markdown is the source and the fragment is the output. `extract-adr`
371
+ reads `docs/adr/*.md` under a service's root - a `README.md` among them is the
372
+ directory's index, not a decision, and is skipped - and answers with one
373
+ fragment holding the records.
374
+
375
+ ```markdown
376
+ # auth.0003 — Session expiry publishes no event
377
+
378
+ - **Status:** accepted
379
+ - **Date:** 2026-08-22
380
+ - **Scope:** auth.auth
381
+ - **Superseded by:** auth.0007
382
+ - **Supersedes:** auth.0001, auth.0002
383
+ - **Relates:** auth.auth.session.SessionEnded, shop.cart, checkout
384
+ - **Note:** how a revocation is kept out of the cache was decided again in
385
+ auth.0010; the drop described below turned out not to be enough.
386
+
387
+ ## Context and Problem Statement
388
+
389
+ ## Decision Outcome
390
+
391
+ ```
392
+
393
+ The title carries the record's id, an em dash, and the title. The id is a
394
+ prefix and four padded digits, and the prefix is whatever the record is about -
395
+ a service (`auth`, `cart`, `oms`), a context (`payments`) or the organisation
396
+ (`org`). The file is named `NNNN-kebab-slug.md` with the same number, because
397
+ the slug the catalog keeps is built from both: the id with its dots opened out,
398
+ then the file's kebab, as in `auth-0003-expiry-publishes-nothing`. A file
399
+ renamed away from its record would silently change the address of a decision
400
+ somebody linked to, so the two are held against each other.
401
+
402
+ `Status` and `Date` are required, and so is `Scope` unless the step's `scope`
403
+ option names it for the whole tree; the rest are written when there is
404
+ something to write, and a bullet the format does not have fails the record
405
+ rather than being dropped. A bullet may wrap onto the next line, indented under
406
+ itself - the break is the author's line width and closes up into a space.
407
+ `Scope` says what the record is about by how many segments it has: none, or
408
+ `org`, for the organisation, one for a context, two for `<context>.<service>`.
409
+ `Relates` names events, services and flows in one list and they are told apart
410
+ by their shape - a flow by its slug, which has no dots, a service by
411
+ `<context>.<service>`, an event by the aggregate and `Name` after that -
412
+ because an author should not have to remember which of three lists a name
413
+ belongs in. `Note` is prose no other field holds, most often that part of a
414
+ record was decided again somewhere else without the whole of it being
415
+ superseded; it sits in the page's header, above the frozen body.
416
+
417
+ Everything from the first `##` onward is the record. It goes into the catalog
418
+ exactly as written and comes back out onto the page the same way, headings and
419
+ all: an ADR is frozen history, and nothing on its page is redrawn from the
420
+ model as it stands now. Prose above that first `##` is a mistake - a paragraph
421
+ that drifted up there would be read by a person and dropped by the extractor.
422
+
423
+ The [adr-tools](https://github.com/npryce/adr-tools) shape is read too: number
424
+ and title on the title line, `Date:` above the record, status as the first
425
+ section with its `Superseded by` / `Supersedes` link. It has no prefix or
426
+ scope, so both come from the step's `scope` option.
427
+
428
+ `src/catalog.ts` fails the whole app on load if a record breaks any of its
429
+ rules, so the extractor checks first, where the file that caused it can be
430
+ named: ids and slugs unique, an id ending in its own zero-padded number, a date
431
+ that parses, a status from the five, and both halves of a supersession. A file
432
+ that breaks one of them is left out with a warning naming the file and line,
433
+ and the rest of the tree is read; only a supersession with one half recorded
434
+ refuses the whole run. That is why `Superseded by` and `Supersedes` are two
435
+ bullets rather than one derived from the other - supersession is a two-way
436
+ fact, and half of it recorded is a bug. The halves that live in one step's
437
+ tree are held against each other there; a record superseded by one in another
438
+ service's tree is a claim only the merged catalog can check, and the validator
439
+ checks it. The same goes for `Scope` and `Relates`: an extractor sees one root,
440
+ so it validates the shape of a name and leaves whether the thing exists to the
441
+ far side.
442
+
443
+ The demo estate's org-wide and context-wide records live in `data/adr`, and are
444
+ read by a step that points at that directory with a glob of its own. Root
445
+ `docs/` is where `gen-markdown` writes, so nothing hand-written can live there.
446
+ `in` is the directory of records rather than `data` itself: a step's fragment is
447
+ only left out of its own stamp when the output is *inside* the input root, and
448
+ `in: data` with `out: data` would be stamped from the file it writes.
449
+
450
+ ```json
451
+ {
452
+ "plugins": [{ "name": "adr", "process": { "cmd": "go run ./plugins/extract-adr" } }],
453
+ "extract": [
454
+ {
455
+ "plugin": "adr",
456
+ "in": "data/adr",
457
+ "out": "data",
458
+ "options": { "files": ["*.md"], "out": "adr.json" }
459
+ },
460
+ {
461
+ "plugin": "adr",
462
+ "in": "examples/auth",
463
+ "out": "examples/auth/portolan",
464
+ "options": { "out": "adr.json" }
465
+ }
466
+ ]
467
+ }
468
+ ```
469
+
470
+ ## Vocabulary written by hand
471
+
472
+ A context's glossary is the one file in a service written for a person and read
473
+ by everyone: what a word means inside the boundary it is spoken in. Nothing
474
+ generates it and nothing should - a definition is a decision about language,
475
+ not a fact about a type - so `extract-glossary` only reads. It takes
476
+ `GLOSSARY.md` at a service's root and answers with a fragment of terms.
477
+
478
+ ```markdown
479
+ # Glossary — auth
480
+
481
+ One meaning per word inside this context.
482
+
483
+ **Session.** Proof that a user logged in, how long that proof is good for, and
484
+ whether it has been taken away.
485
+ ```
486
+
487
+ A title, an optional line or two saying what the vocabulary covers, then one
488
+ paragraph per term in alphabetical order. The paragraph opens with the term in
489
+ bold and the full stop inside the bold, so `**Email address.**` names a
490
+ two-word term and nothing has to guess where the name ends. Everything after it
491
+ is the definition, carried through as written. Hard wrapping is the author's
492
+ business: a soft break inside a paragraph is a space, here as in every markdown
493
+ renderer.
494
+
495
+ Nothing reads the definition for structure. A glossary is a person explaining a
496
+ word to another person, and a parser that went looking for shapes inside the
497
+ explanation would be a parser telling an estate how to phrase itself.
498
+
499
+ The shapes a glossary is otherwise written in are refused by name: a table, a
500
+ bullet list, a heading per term. Also refused: a file that does not open with
501
+ `# Glossary`, an entry that defines nothing, and one word defined twice -
502
+ inside a file or across the files of one step - because a word with two
503
+ meanings in one context is the failure the glossary exists to prevent.
504
+
505
+ What is merely untidy comes back as a warning and the fragment is still
506
+ written: a file that has drifted out of alphabetical order, a root with no
507
+ glossary at all.
508
+
509
+ The term's id is `<context>.<slug>` - `auth.session`, `shop.order` - so the
510
+ context has to be told to the step rather than derived from the directory: a
511
+ glossary sits beside a SERVICE, and `examples/shop/oms/GLOSSARY.md` holds
512
+ words that belong to `shop`. The same word in two contexts is two terms, which
513
+ is the point of the id; the same word twice in one context is an error.
514
+
515
+ ```json
516
+ {
517
+ "plugins": [{ "name": "glossary", "process": { "cmd": "go run ./plugins/extract-glossary" } }],
518
+ "extract": [
519
+ {
520
+ "plugin": "glossary",
521
+ "in": "examples/auth",
522
+ "out": "examples/auth/portolan",
523
+ "options": { "context": "auth", "out": "glossary.json" }
524
+ }
525
+ ]
526
+ }
527
+ ```
528
+
529
+ ## Commands: what to type
530
+
531
+ The first thing a reader new to a checkout wants is not the aggregate list but
532
+ `how do I build this, test it, run it`. The README answers that sometimes; the
533
+ runner files answer it always, and they are already in the tree. `extract-commands`
534
+ reads them and puts the answer on the service as a list of commands - the line
535
+ to type, what the file says it is for, what the runner would execute, and where
536
+ it was read.
537
+
538
+ | File | Runner | Line to type | Description read from |
539
+ | --- | --- | --- | --- |
540
+ | `Makefile` | make | `make <target>` | `## comment` on the target's line, else the `#` block over it |
541
+ | `justfile` | just | `just <recipe>` | `[doc("...")]`, else the `#` block over it |
542
+ | `Taskfile.yml` | task | `task <name>` | `desc`, else `summary` |
543
+ | `package.json` `scripts` | npm, or pnpm/yarn/bun by the lockfile | `npm run <name>`; `npm test`, `npm start` | nothing - the script line is the body |
544
+ | `pyproject.toml` | poe (`[tool.poe.tasks]`), pdm (`[tool.pdm.scripts]`) | `poe <name>`, `pdm run <name>` | `help` |
545
+ | `pom.xml` | mvn, or `./mvnw` when the wrapper is there | `mvn test`, `mvn package`, and the goals of declared plugins: `spring-boot:run`, `quarkus:dev`, `flyway:migrate`, … | the goal's own purpose |
546
+ | `build.gradle(.kts)` | gradle, or `./gradlew` | `gradle build`, `gradle test`; `run` with the application plugin, `bootRun` with Spring Boot; every `tasks.register("...")` | the task's `description` |
547
+ | `.cargo/config.toml` | cargo | `cargo <alias>` for each `[alias]`; when one runs the `xtask` package, `cargo xtask <sub>` for each subcommand of its `main.rs` | `///` over a clap variant, `//` over a match arm |
548
+
549
+ Left out, because the runner leaves them out too: `_`-prefixed and `[private]`
550
+ entries, `internal` tasks, make's special and pattern targets (`.PHONY`, `%.o`),
551
+ targets spelled through a variable, and `pre`/`post` hooks of a script that is
552
+ itself listed. `[project.scripts]` in a pyproject is not read: those are
553
+ programs the package installs, not tasks a developer runs. `uv` has no task
554
+ section and Go has no runner of its own, so a repository with only those
555
+ declares no commands.
556
+
557
+ Maven and Gradle are the other way round: the build is the runner, and what
558
+ a project declares is which plugins extend it. So a pom lists the two
559
+ lifecycle phases every pom answers to, `test` and `package`, and then the
560
+ goals a person types that its `<build><plugins>` add - `spring-boot:run` for
561
+ the Spring Boot plugin, `flyway:migrate` for Flyway - from a short table of
562
+ the plugins an estate meets. A plugin bound to a phase, like protobuf
563
+ generation, is run by the phase and is not listed; neither is one under
564
+ `<pluginManagement>`, which pins a version and runs nothing. A Gradle script
565
+ lists `build` and `test`, `run` when it applies the application plugin,
566
+ `bootRun` for Spring Boot, and every task it registers itself, with the
567
+ description it sets. The script is read as text, not run: a task registered
568
+ in a loop or by an unnamed plugin is not here.
569
+
570
+ Cargo declares commands in one place, `[alias]` in `.cargo/config.toml`, and
571
+ the xtask convention is an alias that runs a package: `xtask = "run --package
572
+ xtask --"`. Each alias is a command with its expansion as the body, and when
573
+ the expansion runs a package and leaves the subcommand to the caller, that
574
+ package's `main.rs` is read for the subcommands - the variants of a clap enum
575
+ deriving `Subcommand`, in kebab-case, with their `///` comment as the doc, and
576
+ the string literals of a `match` on the first argument, with a `//` comment
577
+ over the arm. An alias that names its subcommand already, like `gen = "run -p
578
+ xtask -- gen"`, is listed as the one command it is.
579
+
580
+ Nothing is evaluated. A target inside an `ifeq` is listed; a name that depends
581
+ on a variable's value is not, because the name it would have is not in the
582
+ file. The pyproject reader is a line scanner that follows the table headers
583
+ and the shapes both runners document, not a TOML parser.
584
+
585
+ The fragment names the service and lists its commands, and claims nothing
586
+ else: merging puts the list on the service a domain extractor described. The
587
+ runner files sit beside the service, so the step is told which one it is.
588
+
589
+ ```json
590
+ {
591
+ "plugins": [{ "name": "commands", "process": { "cmd": "go run ./plugins/extract-commands" } }],
592
+ "extract": [
593
+ {
594
+ "plugin": "commands",
595
+ "in": "examples/shop/pricing",
596
+ "out": "examples/shop/pricing/portolan",
597
+ "options": { "context": "shop", "service": "pricing", "out": "commands.json" }
598
+ }
599
+ ]
600
+ }
601
+ ```
602
+
603
+ `extract-project` reads the same files for the component it describes, so a
604
+ repository read by that plugin does not need this one.
605
+
606
+ Downstream, `gen-markdown` draws the list as a Commands table on the service
607
+ page, and `gen-backstage` puts it on the Component twice, because Backstage
608
+ has no field for it: as the `portolan.io/commands` annotation, one command a
609
+ line with its description, and as entity links of type `command`, each
610
+ leading to the line of the runner file it was read from, when `sourceBaseUrl`
611
+ says where the repository is.
612
+
613
+ ## Outside the estate: an external with a contract
614
+
615
+ A service calls things nobody here builds - a card network, a tax API, a
616
+ carrier. The first answer the catalog gave was `unknown`: the call was
617
+ recorded and left unresolved, the lane drawn dashed and red, and the Problems
618
+ page listed it beside real defects. True, and unhelpful, once the far end is
619
+ Stripe and Stripe publishes a document.
620
+
621
+ An **external** is what the catalog may claim about such a system, and no
622
+ more: what it answers on, read from the copy of its document vendored beside
623
+ the adapter that calls it, and what the manifest says it is called and is
624
+ for. It sits at the root beside the contexts (`catalog.externals`) with a bare
625
+ id, no context, no aggregates and no repository, and the estate's picture
626
+ draws it outside, muted, exactly where it is.
627
+
628
+ Two steps describe one, and neither knows the other exists:
629
+
630
+ ```json
631
+ { "plugin": "openapi", "in": "examples/payments/ledger",
632
+ "out": "examples/payments/ledger/portolan",
633
+ "options": { "external": "stripe", "externalName": "Stripe",
634
+ "externalUrl": "https://docs.stripe.com/api",
635
+ "spec": "src/main/java/.../infrastructure/stripe/openapi/openapi.yaml",
636
+ "out": "stripe.json" } },
637
+ { "plugin": "java-domain", "in": "examples/payments/ledger", "...": "...",
638
+ "options": { "externals": { "stripe.v1": "stripe" } } }
639
+ ```
640
+
641
+ The first reads the copy and says what `stripe` answers on. The second reads
642
+ the adapter, finds the verb and the route in each call, looks the operation up
643
+ in the same copy, and - told by `externals` that the copy's api id belongs to
644
+ `stripe` - records the call as `stripe.v1/PostPaymentIntents`, declared, on a
645
+ lane of kind `external`. The merge joins the two by the id; a call to an
646
+ operation the copy declares resolves, one it does not is reported by the
647
+ extractor and left out.
648
+
649
+ Neither line is needed when the tree can say it itself. An `openapi` step
650
+ with no `spec` walks the tree for every document and reads what sits beside
651
+ each one: a server generated from it - oapi-codegen's `ServerInterface`,
652
+ swag's `docs` package, a handler or controller written against it - means the
653
+ service implements it, and the document is what the service provides; a
654
+ client generated from it - `ClientInterface`, a `client.gen.go`, an adapter
655
+ named after a client - means the service calls it, and the document names a
656
+ system this tree does not implement. That system is an external, with the
657
+ id, the name and the summary the document gives itself: a copy titled
658
+ "Gordian Flights & Ancillaries API" becomes `gordian-flights-ancillaries`,
659
+ the trailing "API" dropped because the system is the thing and not its
660
+ interface. `peers` on the same step says which called documents are ours -
661
+ `{"auth.v1": "auth.auth"}` - and those are skipped, because the service that
662
+ implements them describes them; `externals` names a system when the title
663
+ would not, `{"stripe.v1": "stripe"}`. A document with neither a server nor a
664
+ client beside it is reported and left alone, since reading it as either would
665
+ be a guess.
666
+
667
+ The domain extractor keeps the same rule from its side: a generated HTTP
668
+ client whose api no `peers` line claims is read as calling the system the
669
+ document beside it is titled after, and the call lands on an `external` lane,
670
+ declared, under the id both sides derive from that title. `externals` on the
671
+ domain step overrides the name; a proto client, whose contract names no
672
+ system, stays unresolved until `peers` says who answers.
673
+
674
+ The copy is **narrow** - the operations the service calls and the schemas they
675
+ answer with, every field verbatim - for the reason org.0001 gives for a proto:
676
+ what is vendored has to be reviewable, and Stripe's whole document is not. It
677
+ carries one line the original does not, `x-portolan-api: stripe.v1` in
678
+ `info`: the copy is already the consumer's translation boundary, so it is the
679
+ one place the estate's name for the document is written, and every reader of
680
+ the copy takes the id from there rather than from Stripe's title and version
681
+ - or from two manifests that would have to agree.
682
+
683
+ ## Verifiers: the third phase
684
+
685
+ An extractor reads source and runs before there is a catalog; a generator
686
+ reads the catalog and writes pages. A **verifier** sits between them. It reads
687
+ something observed - traces today, a test's record tomorrow - and answers with
688
+ a fragment like an extractor's, but one that only makes sense against the
689
+ merged catalog: "this hop was seen running" names a hop somebody else declared.
690
+ So a `verify` step is handed both `input` and `catalog`, and the catalog it is
691
+ handed leaves out the step's own last output. Without that, what it wrote last
692
+ time would count as evidence this time, and the fragment could never be checked
693
+ against a clean run.
694
+
695
+ ```json
696
+ {
697
+ "plugins": [{ "name": "otel", "process": { "command": "go", "args": ["run", "./plugins/verify-otel"] } }],
698
+ "verify": [
699
+ {
700
+ "plugin": "otel",
701
+ "in": "examples/auth",
702
+ "out": "examples/auth/portolan",
703
+ "options": { "traces": ["telemetry/traces.jsonl"], "out": "observed.json" }
704
+ }
705
+ ]
706
+ }
707
+ ```
708
+
709
+ What comes back is merged like any other source, under two rules that exist
710
+ for it. A flow declared twice is accepted when the second declaration differs
711
+ only in status: `declared` steps become `verified`, and anything else that
712
+ differs - a lane, a hop, a branch - is the conflict it always was. A consumer
713
+ or a call declared twice keeps the first note and takes `verified` if either
714
+ side has it.
715
+
716
+ ### verify-otel
717
+
718
+ Reads OTLP JSON - one batch per file or one per line, as a collector's file
719
+ exporter writes it - and turns each trace into hops between lanes:
720
+
721
+ | span | hop |
722
+ | --- | --- |
723
+ | kind server, `http.route` | `client → service`, an rpc; matched to the operation whose `http` verb and path the OpenAPI extractor recorded, which is what opens an endpoint flow |
724
+ | kind client, `rpc.service` + `rpc.method` | `service → provider`, an rpc; `unknown` lane and `unresolved` when nothing provides it, however often it ran |
725
+ | `db.system.name`, `db.operation.name` | `service → its store`, a call; the statement nested under a query is not a second call |
726
+ | kind producer, `event.name` | `service → bus`, the event whose name that is among the service's own; a producer span under another for the same name is the relay's and the same publish |
727
+ | kind consumer, `event.name` | `bus → service`, and a `verified` consumer on the event |
728
+
729
+ A trace whose root opens a declared flow raises the steps it shows: the call
730
+ in, the events out, the rpcs with a ref. A `call` step is never raised - a
731
+ `SELECT` ran, which is not the same claim as "the repository's `ByEmail` was
732
+ called" - and `unresolved` is never raised, because a trace does not put the
733
+ far end in the catalog. A consumer span inside a trace opens a flow of its own
734
+ and is matched the same way, so one password change verifies both the
735
+ request's flow and the policy's. A root no flow opens is written down as
736
+ `observed-<service>-<route>`, once per shape, with a summary saying how many
737
+ traces showed it.
738
+
739
+ `service.name` is matched to the one service whose slug it is, `event.name` to
740
+ the one event whose `wire.name` it is, or failing that to the one event of the
741
+ publisher's with that last segment; `services` and `events` in the options say
742
+ otherwise where an estate's names differ. A publish span whose
743
+ `messaging.destination.name` is not the event's `wire.channel` is a warning:
744
+ the event went out, but not where the code says it does.
745
+
746
+ ### verify-codeowners
747
+
748
+ Reads the `CODEOWNERS` a repository already keeps and says who to ask about
749
+ each service.
750
+
751
+ "Who do I ask about `shop.oms`" was the question the estate answered worst.
752
+ `owner` on a flow, a store or a module means the bounded context that holds it
753
+ - a grouping, not a team - and there was nothing on any page a reader could act
754
+ on. The answer was already written down, in the one file the forge itself
755
+ enforces: a team that owns a directory is a team that gets the pull request.
756
+
757
+ ```json
758
+ {
759
+ "plugins": [{ "name": "codeowners", "process": { "command": "go", "args": ["run", "./plugins/verify-codeowners"] } }],
760
+ "verify": [
761
+ { "plugin": "codeowners", "in": ".github", "out": "data", "options": { "out": "owners.json" } }
762
+ ]
763
+ }
764
+ ```
765
+
766
+ Point `in` at the directory the file is in, not at the repository root: the
767
+ host dates a fragment from the last commit to touch the step's input, and the
768
+ subject of this one is the `CODEOWNERS` file. Rooted at the repository, it
769
+ would be restamped by every commit ever made. Left with no `file`, the three
770
+ places a forge looks are tried in order - `CODEOWNERS`, `.github/CODEOWNERS`,
771
+ `docs/CODEOWNERS` - and a `file` that names something absent fails the run,
772
+ because answering "nobody owns anything" to a typo is only noticed a month
773
+ later.
774
+
775
+ It is a verifier and not an extractor because a rule is a path and only the
776
+ merged catalog knows where each service is; an extractor would have to be told,
777
+ service by service, in the manifest, what the catalog already says. It earns
778
+ the name twice over. A service no rule matches is reported, and so is a rule
779
+ that matches no service - a team believing it owns something the estate does
780
+ not have, which is the one failure a `CODEOWNERS` file can never report about
781
+ itself. A rule that matches and never wins is reported differently, because the
782
+ fix is different: nothing is wrong with the path, everything it covers is just
783
+ claimed by a rule below it.
784
+
785
+ What lands on the catalog is `owners` on the service: handles exactly as the
786
+ file spells them, `@acme/oms-team`, `@someone`, `dev@acme.io`. Deliberately
787
+ nothing more - resolving a handle to the people currently in it is a call to a
788
+ forge's API, which needs a credential this does not have and answers
789
+ differently tomorrow, and a handle is what a reviewer types anyway. Two sources
790
+ naming owners are unioned, because two rules that both matched are two facts
791
+ and not two answers.
792
+
793
+ The grammar is gitignore's, minus the parts CODEOWNERS does not have. A
794
+ pattern owns a directory when it names the directory or anything above it, and
795
+ does not when it names only something inside it: `services/oms/internal` is a
796
+ rule about part of a service, and reading it as ownership of the whole would
797
+ hand a team a page it never asked for. Later rules win. A pattern with no
798
+ owners after it wins too - taking ownership back is the only reason anybody
799
+ writes one. GitLab's sections change which rule wins and are read the flatter
800
+ way GitHub means, with a warning saying so, because the difference only ever
801
+ shows up as an owner quietly missing from a page.
802
+
803
+ The demo estate's file is `data/codeowners/CODEOWNERS`, which is deliberately
804
+ not one of the three places a forge reads: this repository is both the tool and
805
+ the estate it describes, and a real `CODEOWNERS` here would ask GitHub to
806
+ request reviews from teams that do not exist. A real repository puts it where
807
+ the forge looks.
808
+
809
+ ## wasm or process
810
+
811
+ `wasm` is the default and should stay that way. The module gets no filesystem,
812
+ no network and no environment; a plugin from somebody else's repository can be
813
+ run over your source tree without reading it.
814
+
815
+ `process` is the escape hatch for a generator that needs a toolchain — one
816
+ reading Go source has to run `go list`, and no wasm module can spawn anything.
817
+ It gets the same protocol and none of the sandbox, which is the trade being made
818
+ and the reason it is not the default. It declares `command` and an `args` array;
819
+ the host never feeds a command string through a shell.
820
+
821
+ A plugin fetched over `https://` must declare its `sha256`; the host verifies it
822
+ and caches by digest. A `file://` plugin may declare one, but a checksum
823
+ protects a download, not a module built from the source next to it.
824
+ Downloads do not follow redirects and are bounded in time and size. Every run
825
+ has a deadline and bounded stdout/stderr; wasm runs in a worker so even a module
826
+ stuck in a loop can be terminated. Responses reject unknown properties,
827
+ duplicate or unsafe filenames, and non-string contents before anything is
828
+ written.
829
+
830
+ ## Services in other repositories: fetch-git
831
+
832
+ `fetch-git` is `fetch-bsr` for a repository rather than a registry, and it
833
+ lives by the same four rules. A pin is a repository, a commit and the paths
834
+ actually read; the step fetches exactly those directories at exactly that
835
+ commit and hands them back as files, so the host writes them into the tree
836
+ beside a `git.lock.json` naming the commit and the digest of every file. The
837
+ paths inside the copy are the repository's own, which is the point: the
838
+ extract step that follows points its `in` at the vendored service and reads
839
+ it exactly as it would read that service's checkout.
840
+
841
+ ```json
842
+ {
843
+ "sources": ["data/*.json", "vendor/repos/*/*/git.repo.json"],
844
+ "plugins": [{ "name": "git", "process": { "command": "go", "args": ["run", "./plugins/fetch-git"] } }],
845
+ "extract": [
846
+ {
847
+ "plugin": "git",
848
+ "in": "vendor",
849
+ "out": "vendor/repos",
850
+ "options": {
851
+ "cache": "vendor/repos",
852
+ "repos": [
853
+ { "repo": "github.com/acme/shop", "commit": "c1d2e3f4…", "paths": ["services/oms", "proto"] }
854
+ ]
855
+ }
856
+ },
857
+ {
858
+ "plugin": "go-domain",
859
+ "in": "vendor/repos/acme/shop/services/oms",
860
+ "out": "data/shop",
861
+ "options": { "context": "shop", "service": "oms", "store": "pg" }
862
+ }
863
+ ]
864
+ }
865
+ ```
866
+
867
+ It runs the `git` the host already needs for stamps: a fetch of the one
868
+ commit into a directory that exists for one call, and an archive of the paths
869
+ wanted, read straight into memory. Whatever git is configured to do about
870
+ credentials and hosts - a helper, a netrc entry, an ssh agent - it does here
871
+ too, and the plugin reads none of it. `PORTOLAN_OFFLINE` (or any truthy `CI`)
872
+ replays the committed copies against their locks; a commit the manifest does
873
+ not pin is resolved online with a warning and refused offline; a fetch that
874
+ fails falls back to the committed copy when there is one, and is a red build
875
+ when there is not; a vendored file edited by hand is reported by path.
876
+
877
+ ### What the copy says about itself
878
+
879
+ Two files land beside every copy, and the difference between them is who reads
880
+ them. `git.lock.json` is for the next run of this step: the commit, and the
881
+ digest of every file, which is what makes replaying the copy equivalent to
882
+ fetching it again. `git.repo.json` is for the estate - a catalog fragment
883
+ holding one line, the repository and the commit it is a copy of - which is why
884
+ it is in `sources` above.
885
+
886
+ Nothing else can say it. A service says which repository it lives in, and an
887
+ extractor reads a directory as a pure function of what is on disk; neither has
888
+ any idea which commit somebody fetched. Without that line, every source path
889
+ of every vendored service is dead text on the page - the file and the line are
890
+ known, and there is nowhere to send a reader - and every fragment read out of
891
+ the copy is stamped with the commit that VENDORED it, so the service looks
892
+ fresh whenever the fetch is re-run and unchanged when its own repository
893
+ moves. With it, `sourceHref` links the line at the commit it was read at and
894
+ `stampFor` dates the fragment from the code rather than the vendoring.
895
+
896
+ One more line is needed for the app itself: `SOURCE_GLOBS` in `src/data.ts`,
897
+ where the same patterns are written out a second time because
898
+ `import.meta.glob` resolves at build time and needs literals.
899
+
900
+ ## Schema modules: fetch and parse, kept apart
901
+
902
+ `fetch-bsr` and `extract-proto` are two plugins on purpose, and the split is
903
+ the whole design.
904
+
905
+ | | `fetch-bsr` | `extract-proto` |
906
+ | --- | --- | --- |
907
+ | job | registry wire → `.proto` bytes | `.proto` bytes → catalog fragment |
908
+ | network | yes | never |
909
+ | environment | reads `BUF_TOKEN` | never |
910
+ | output | `.proto` files and a `bsr.lock.json` per module | one catalog fragment |
911
+ | deterministic | only because it is pinned and cached | absolutely |
912
+
913
+ Extraction stays a pure function of the tree. Fetching is the step that can
914
+ fail, need a credential, or come back with something different than it did
915
+ yesterday, and confining that to its own step is what lets everything after it
916
+ be replayed byte-for-byte from a checkout.
917
+
918
+ **The fetched protos are the plugin's `Response.Files`, not a side effect.** The
919
+ host writes them like any other generated file, so they get a manifest entry,
920
+ are compared by `gen:check`, and are removed when the step stops naming them.
921
+ The cache is not a second copy of anything — it *is* the tree. Refreshing a pin
922
+ produces one pull request holding the pin bump, the proto diff, the lock diff
923
+ and the fragment diff, which is the review worth having.
924
+
925
+ Declare the fetch step **before** the extract step: steps run in list order, so
926
+ its protos and locks are on disk by the time the parser reads them.
927
+
928
+ ```json
929
+ {
930
+ "plugins": [
931
+ { "name": "bsr", "process": { "command": "go", "args": ["run", "./plugins/fetch-bsr"] } },
932
+ { "name": "proto", "process": { "command": "go", "args": ["run", "./plugins/extract-proto"] } }
933
+ ],
934
+ "extract": [
935
+ {
936
+ "plugin": "bsr",
937
+ "in": "examples/shop",
938
+ "out": "examples/shop/vendor/proto",
939
+ "options": {
940
+ "cache": "examples/shop/vendor/proto",
941
+ "modules": [
942
+ { "module": "buf.build/acme/shop", "commit": "c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6" }
943
+ ]
944
+ }
945
+ },
946
+ {
947
+ "plugin": "proto",
948
+ "in": "examples/shop",
949
+ "out": "examples/shop/portolan",
950
+ "options": {
951
+ "context": "shop",
952
+ "service": "oms",
953
+ "paths": ["vendor/proto/acme/shop"],
954
+ "vendored": ["internal/infrastructure/pricing"],
955
+ "peers": { "pricing.v1": "shop.pricing" },
956
+ "out": "proto.json"
957
+ }
958
+ }
959
+ ]
960
+ }
961
+ ```
962
+
963
+ `cache` repeats the step's `out` because a plugin is never told where its output
964
+ goes. That is the shape of the protocol — a plugin returns files and the host
965
+ decides what to do with them — and not an oversight to work around.
966
+
967
+ ### Why fetch-bsr can never be wasm
968
+
969
+ It needs a socket and a credential. `process` exists for exactly that trade, and
970
+ `auth.go` is the only file in either plugin that reads the environment. The
971
+ protocol's "no ambient state" rule is about *facts*: nothing about the estate may
972
+ come from anywhere but the request. A credential is not a fact about the estate —
973
+ it decides whether the fetch succeeds, never what the fetch says — and a test
974
+ asserts the output is byte-identical with and without a token.
975
+
976
+ ### Pinning, and the offline rule
977
+
978
+ Pin every module to a commit. A BSR commit is immutable, so a pinned download is
979
+ byte-reproducible, which is the only reason replaying from disk is equivalent to
980
+ fetching again. An unpinned module is resolved and warned about when online, and
981
+ refused when offline — there is nothing to replay against.
982
+
983
+ `PORTOLAN_OFFLINE=1` (or any truthy `CI`) turns the fetch off. The step then
984
+ re-emits the committed copies, checked against their locks. Set it in CI; the
985
+ workflow already does.
986
+
987
+ Four rules govern what happens when a fetch does not:
988
+
989
+ 1. Fetch succeeded → the fetched files and a regenerated lock.
990
+ 2. Skipped or failed, cache complete and matching its digests → the cached files
991
+ byte-identically, plus a warning. Output unchanged, so `--check` stays clean.
992
+ 3. Failed **and** no usable cache → a non-zero exit, never a short file list. The
993
+ host deletes files a step stops naming, and dropping a repository's vendored
994
+ protos because a laptop went offline is worse than a red build.
995
+ 4. A cached file whose digest no longer matches is reported by path — someone
996
+ edited a vendored copy, which is the drift `docs/adr/org.0001.md` wants seen.
997
+
998
+ ### Why the proto parser is hand-written
999
+
1000
+ `docs/adr/org.0001.md` has consumers keeping *narrowed* vendored copies, and a
1001
+ narrowed copy routinely imports a file nobody vendored beside it. A compiler —
1002
+ `protocompile`, `protoc` — refuses to produce anything for that input. The whole
1003
+ point of reading vendored copies is to describe files that do not build
1004
+ standalone, so the parser is tolerant: every construct it declines to model
1005
+ (`extend`, a proto2 `group`, an aggregate option body) is named in a diagnostic
1006
+ rather than dropped, and only a file that cannot be tokenised is fatal.
1007
+
1008
+ It also keeps types **as written** — `[]LineItem`, `map[string]Money`, the
1009
+ `optional` keyword, the author's declaration order — the same stance the catalog
1010
+ already takes for schemas, and one a descriptor has already thrown away.
1011
+
1012
+ ### What extract-proto will not claim
1013
+
1014
+ `status` is only ever `declared` or `unresolved`, **never `verified`**. Reading a
1015
+ `.proto` proves a call was written down. `verified` in the shipped catalog means
1016
+ a test exercises it end to end, which is a property of the merged catalog — and
1017
+ every extractor runs before one exists.
1018
+
1019
+ A message named `OrderPlaced` in an `events.proto` stays an `RpcMessage` and does
1020
+ not become a catalog `Event`. `Event.id` is `<service>.<aggregate>.<Name>` and
1021
+ this extractor knows the package, not the aggregate; a guess would collide with
1022
+ the event `extract-go` already emits, or invent a ghost aggregate that would sit
1023
+ beside the real one forever.
1024
+
1025
+ ## Schema registry subjects: the same split, one topic further
1026
+
1027
+ `fetch-csr` and `extract-csr` are the Confluent Schema Registry half of the same
1028
+ argument, and they are shaped like `fetch-bsr` and `extract-proto` on purpose:
1029
+ one owns the socket and the credential, the other owns the reading, and CI runs
1030
+ the reading over a tree it can verify without a registry existing at all.
1031
+
1032
+ | | `fetch-csr` | `extract-csr` |
1033
+ | --- | --- | --- |
1034
+ | job | registry wire → schema files | schema files → catalog fragment |
1035
+ | network | yes | never |
1036
+ | environment | reads `CSR_API_KEY`/`CSR_API_SECRET`, or `CSR_TOKEN` | never |
1037
+ | output | one schema and a `csr.lock.json` per subject | one catalog fragment |
1038
+ | deterministic | only because it is pinned and cached | absolutely |
1039
+
1040
+ A registered version is immutable: subject `orders-value` at version 3 is the
1041
+ same bytes today and next year, and re-registering a changed schema makes
1042
+ version 4. That is the promise a BSR commit makes, so the same four rules govern
1043
+ a fetch that does not happen, and `PORTOLAN_OFFLINE=1` replays the committed
1044
+ copies against their locks exactly as it does there.
1045
+
1046
+ A subject a fetched schema **references** is fetched too, pinned by the
1047
+ reference rather than by the manifest, and needs no entry of its own — the
1048
+ version is part of the bytes we already have, so following one adds no lottery.
1049
+ Each lands in its own directory, and the referring subject's lock is what an
1050
+ offline run follows to find them.
1051
+
1052
+ Avro and JSON schemas arrive minified onto one line. They are written out
1053
+ indented — with `json.Indent`, which reformats without reordering, so the file
1054
+ still says what the registry said in the order it said it — because a version
1055
+ bump that is one unreadable line is a review nobody can do. The digest is over
1056
+ the bytes as written, so verifying needs no reformatting of anything.
1057
+
1058
+ ### The strategy the registry does not record
1059
+
1060
+ A subject name is whatever the producer's serializer decided to call the
1061
+ registration, and the rule it used — the `SubjectNameStrategy` — is nowhere in
1062
+ the registry's answer. `shop.oms.order-value` is a topic plus a suffix under
1063
+ `TopicNameStrategy` and a record's full name under `RecordNameStrategy`, and
1064
+ nothing but the manifest can say which. So `strategy` is an option, and
1065
+ everything `extract-csr` does with a name follows from being told it.
1066
+
1067
+ Under `topic-record` the separator is a hyphen and both halves may contain one,
1068
+ so the split is made by matching the **schema's own full name** as the suffix
1069
+ rather than by searching for a delimiter.
1070
+
1071
+ A `-key` subject is kept as a shape and put on no channel: a key is part of
1072
+ every message on the topic, not a message on it. Under `record` there is no
1073
+ topic at all, and a fragment with no channels is the right answer rather than a
1074
+ gap — that strategy exists so a record can be reused across many.
1075
+
1076
+ ### What extract-csr will not claim
1077
+
1078
+ **It emits no events.** An `Event` in the catalog belongs to an aggregate, and a
1079
+ registry holds schemas, not domains — it has no idea which aggregate raises
1080
+ what. So the shapes land in `defs`, where a shared shape belongs, and the topics
1081
+ land in the service's channels beside the ones an AsyncAPI document declares.
1082
+ The domain extractor says an aggregate raises `OrderPlaced` and calls it
1083
+ `shop.oms.OrderPlaced` on the wire; this says a schema by that name is
1084
+ registered against topic `shop.oms.order` and has these fields. Neither knows
1085
+ the other exists, and the pages hold the two against each other.
1086
+
1087
+ **It does not say who produces.** A registry records no producer and no
1088
+ consumer. `direction` is told, per step and per subject, or it would be invented.
1089
+
1090
+ **It does not parse protobuf.** That is `extract-proto`'s whole job, and a
1091
+ second, worse parser here would be a second answer to one question. A `PROTOBUF`
1092
+ subject still names its topic — which is the one thing a `.proto` file cannot
1093
+ say — and a diagnostic points at `extract-proto` for the fields.
1094
+
1095
+ A field referencing a shape nothing in the estate vendored keeps its **name**
1096
+ and loses its **ref**. The catalog validates that every ref resolves, and
1097
+ failing a run over a reference that is genuinely true — the shape really does
1098
+ live in another estate — would be the wrong end of the trade.
1099
+
1100
+ ## The bus: a channel is a claim, not an event
1101
+
1102
+ `extract-asyncapi` reads an AsyncAPI document and answers with the channels a
1103
+ service declares — the address the broker knows, and each message on it with the
1104
+ direction it travels. What it does **not** answer with is events.
1105
+
1106
+ That looks like a gap and is a boundary. `Event.id` is
1107
+ `<service>.<aggregate>.<Name>`, and an AsyncAPI document knows the message on the
1108
+ wire, not the aggregate that raised it. An extractor that guessed would either
1109
+ collide with the event `extract-go` and `extract-ts` already emit or invent a
1110
+ ghost aggregate that would sit beside the real one forever — the same rule
1111
+ `extract-proto` keeps about a message called `OrderPlaced`.
1112
+
1113
+ So the two sources meet in the merge instead, and the pages hold them against
1114
+ each other. The domain says an aggregate raises `BasketCreated` and how it leaves,
1115
+ in `wire`; the document says the service sends `cart.BasketCreated` on
1116
+ `shop.cart.basket`. Where they agree the catalog says the same thing twice, which
1117
+ is worth nothing. Where they disagree it is worth a row on the Problems page,
1118
+ because one of the two is stale:
1119
+
1120
+ - an event whose channel the document does not declare — a subscriber reading
1121
+ the document does not know the message exists;
1122
+ - a channel the document declares and no event names — a promise nothing keeps;
1123
+ - a message the document listens for that nothing in the estate publishes.
1124
+
1125
+ That last one is the only edge in the catalog that runs from the subscriber
1126
+ outwards. Everywhere else a publisher names its consumers; here the subscriber
1127
+ names a message and the estate is searched for whoever puts it on the wire. A
1128
+ subscription that resolves is how two repositories that never mention each other
1129
+ are found to be joined — and a channel that two services both declare a send on
1130
+ is a second publisher, which is an error for the reason a second writer in a
1131
+ database is.
1132
+
1133
+ ### 2.x says publish and subscribe backwards
1134
+
1135
+ In AsyncAPI 3.x an operation carries `action: send` or `action: receive`, from
1136
+ the application's side, and there is nothing to get wrong. In 2.x a channel has
1137
+ `publish` and `subscribe`, and both are written from the **client's** side:
1138
+ `publish` is what somebody else publishes *to* the application, so the
1139
+ application receives it, and `subscribe` is what the application produces for
1140
+ somebody else to subscribe to.
1141
+
1142
+ Reading 2.x the obvious way puts every arrow in the estate the wrong way round.
1143
+ The extractor reads both versions and answers in 3.x's vocabulary, which is the
1144
+ one the catalog keeps.
1145
+
1146
+ ### A work queue is a channel of kind `job`
1147
+
1148
+ A task queue is a channel too - an address the broker knows, messages that
1149
+ travel on it - with one difference the catalog has to be told: many callers
1150
+ put the same job on it by design, and no domain event stands behind a job.
1151
+ `kind: "job"` on the channel says so. The merge does not call two senders on a
1152
+ job queue rival publishers, and the Problems page does not look for an event
1153
+ with the job's wire name. `extract-celery` answers with these for a Python tree:
1154
+ one channel per queue, a `send` per task the tree enqueues and a `receive` per
1155
+ task it declares, and one flow per task that is both - the call that enqueues
1156
+ it, then the worker that runs it. It reads the queue the way Celery decides
1157
+ it, the call before the decorator before `task_routes` before the default,
1158
+ and `transaction.on_commit(...)` around an enqueue is a note on the step,
1159
+ which is the one fact about *when* a message leaves that the code states
1160
+ plainly.