@shortlink-org/portolan 0.1.0 → 0.2.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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,261 +0,0 @@
1
- // Package openapi is what two extractors agree on about an OpenAPI document:
2
- // how its interfaces are named in the catalog, and which operation answers on
3
- // which route. The server side reads the document to say what a service
4
- // provides; the client side reads the copy vendored beside a generated client
5
- // to say what a service calls. Both have to spell `auth.v1.Sessions/login` the
6
- // same way, or the call would never resolve to the method, and one place is
7
- // how they do.
8
- package openapi
9
-
10
- import (
11
- "fmt"
12
- "os"
13
- "strings"
14
-
15
- "gopkg.in/yaml.v3"
16
- )
17
-
18
- // APIID is the document's title and major version: `auth` 1.0.0 gives
19
- // `auth.v1`. It prefixes every interface the document declares.
20
- func APIID(title, version string) string {
21
- name := title
22
- if name == "" {
23
- name = "api"
24
- }
25
- name = strings.ReplaceAll(strings.ToLower(name), " ", "-")
26
-
27
- if major, _, ok := strings.Cut(version, "."); ok && major != "" {
28
- return name + ".v" + major
29
- }
30
-
31
- return name
32
- }
33
-
34
- // DocumentAPIID is the id a document says it goes by in the estate, or the one
35
- // built from its title and version when it says nothing.
36
- //
37
- // `x-portolan-api` in `info` is for a copy vendored from outside the estate:
38
- // Stripe's document is titled "Stripe API" and versioned "2026-08-26.dahlia",
39
- // and `stripe-api.v2026-08-26` on every arrow would be the document's words
40
- // where the estate wants its own. The copy is the consumer's translation
41
- // boundary already, so it is the one place the estate's name may be written,
42
- // and both sides of a call read it from there rather than from two manifests.
43
- func DocumentAPIID(declared, title, version string) string {
44
- if declared = strings.TrimSpace(declared); declared != "" {
45
- return declared
46
- }
47
-
48
- return APIID(title, version)
49
- }
50
-
51
- // ExternalID is the id a system outside the estate goes by when nothing but
52
- // its own document names it: the document's title as a slug, with a trailing
53
- // "API" dropped because the system is the thing, not its interface. "Gordian
54
- // Flights & Ancillaries API" is gordian-flights-ancillaries; "Stripe API" is
55
- // stripe. Both sides of a call - the extractor that reads the vendored copy
56
- // for what the system answers on, and the one that reads the client beside it
57
- // for what this service calls - derive it here, so neither needs the manifest
58
- // to say it.
59
- func ExternalID(title string) string {
60
- var b strings.Builder
61
- dash := false
62
- for _, r := range strings.ToLower(strings.TrimSpace(title)) {
63
- switch {
64
- case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
65
- b.WriteRune(r)
66
- dash = false
67
- case !dash && b.Len() > 0:
68
- b.WriteByte('-')
69
- dash = true
70
- }
71
- }
72
- id := strings.TrimRight(b.String(), "-")
73
- if trimmed := strings.TrimSuffix(id, "-api"); trimmed != "" {
74
- id = trimmed
75
- }
76
-
77
- return id
78
- }
79
-
80
- // Title is the human form of a tag: users becomes Users, price_list becomes
81
- // PriceList, because it sits in an id beside a proto-shaped service name.
82
- func Title(name string) string {
83
- var b strings.Builder
84
- for _, word := range strings.FieldsFunc(name, func(r rune) bool { return r == '_' || r == '-' || r == ' ' }) {
85
- runes := []rune(word)
86
- if runes[0] >= 'a' && runes[0] <= 'z' {
87
- runes[0] = runes[0] - 'a' + 'A'
88
- }
89
- b.WriteString(string(runes))
90
- }
91
-
92
- return b.String()
93
- }
94
-
95
- // InterfaceID is the contract declared by the OpenAPI document. Tags organise
96
- // operations inside that contract; they do not create separate interfaces.
97
- func InterfaceID(api, _ string) string { return api }
98
-
99
- // Verbs, in the order the paths object lists them by convention.
100
- var Verbs = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace"}
101
-
102
- // Operation is one route of the document and the name it goes by.
103
- type Operation struct {
104
- // ID is the operationId, or `VERB /path` when the document has none.
105
- ID string
106
- // Tag is the first tag, retained as document metadata. It does not change
107
- // the identity of the contract.
108
- Tag string
109
- // Verb is upper case: POST.
110
- Verb string
111
- // Path is the template as written: /v1/users/{userId}.
112
- Path string
113
- }
114
-
115
- // Interface is the id of the interface the operation belongs to.
116
- func (o Operation) Interface(api string) string { return InterfaceID(api, o.Tag) }
117
-
118
- // CallID is what a call to this operation is known by in the catalog.
119
- func (o Operation) CallID(api string) string { return o.Interface(api) + "/" + o.ID }
120
-
121
- // Spec is the part of a document these extractors read.
122
- type Spec struct {
123
- API string
124
- Operations []Operation
125
-
126
- // Title, Description and DocsURL are what the document says about the
127
- // system it describes, for naming that system when the manifest does not.
128
- Title string
129
- Description string
130
- DocsURL string
131
- }
132
-
133
- // Read loads a document and lists its operations. The api id may be
134
- // overridden by whoever calls, since the manifest may name one.
135
- func Read(path string) (*Spec, error) {
136
- contents, err := os.ReadFile(path)
137
- if err != nil {
138
- return nil, err
139
- }
140
- var root yaml.Node
141
- if err := yaml.Unmarshal(contents, &root); err != nil {
142
- return nil, fmt.Errorf("%s: %w", path, err)
143
- }
144
- node := &root
145
- if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
146
- node = node.Content[0]
147
- }
148
-
149
- info := child(node, "info")
150
- spec := &Spec{
151
- API: DocumentAPIID(text(child(info, "x-portolan-api")), text(child(info, "title")), text(child(info, "version"))),
152
- Title: text(child(info, "title")),
153
- Description: text(child(info, "description")),
154
- DocsURL: text(child(node, "externalDocs", "url")),
155
- }
156
- for _, p := range entries(child(node, "paths")) {
157
- for _, verb := range Verbs {
158
- operation := child(p.value, verb)
159
- if operation == nil {
160
- continue
161
- }
162
- id := text(child(operation, "operationId"))
163
- if id == "" {
164
- id = strings.ToUpper(verb) + " " + p.key
165
- }
166
- tags := list(child(operation, "tags"))
167
- tag := ""
168
- if len(tags) > 0 {
169
- tag = tags[0]
170
- }
171
- spec.Operations = append(spec.Operations, Operation{ID: id, Tag: tag, Verb: strings.ToUpper(verb), Path: p.key})
172
- }
173
- }
174
-
175
- return spec, nil
176
- }
177
-
178
- // Find looks up an operation by verb and route. Both sides may spell a path
179
- // parameter differently - `{userId}` in the document, `%s` in a generated
180
- // client - so parameters are compared by position, not by name.
181
- func (s *Spec) Find(verb, path string) (Operation, bool) {
182
- want := strings.ToUpper(verb) + " " + shape(path)
183
- for _, op := range s.Operations {
184
- if op.Verb+" "+shape(op.Path) == want {
185
- return op, true
186
- }
187
- }
188
-
189
- return Operation{}, false
190
- }
191
-
192
- // shape replaces every parameter, however spelled, with one marker.
193
- func shape(path string) string {
194
- var b strings.Builder
195
- for _, segment := range strings.Split(strings.TrimSuffix(path, "/"), "/") {
196
- if strings.HasPrefix(segment, "{") || segment == "%s" || segment == "%v" || segment == "%d" {
197
- b.WriteString("/*")
198
-
199
- continue
200
- }
201
- b.WriteString("/" + segment)
202
- }
203
-
204
- return b.String()
205
- }
206
-
207
- type entry struct {
208
- key string
209
- value *yaml.Node
210
- }
211
-
212
- func entries(node *yaml.Node) []entry {
213
- if node == nil || node.Kind != yaml.MappingNode {
214
- return nil
215
- }
216
- var out []entry
217
- for i := 0; i+1 < len(node.Content); i += 2 {
218
- out = append(out, entry{key: node.Content[i].Value, value: node.Content[i+1]})
219
- }
220
-
221
- return out
222
- }
223
-
224
- func child(node *yaml.Node, keys ...string) *yaml.Node {
225
- for _, key := range keys {
226
- if node == nil || node.Kind != yaml.MappingNode {
227
- return nil
228
- }
229
- var next *yaml.Node
230
- for i := 0; i+1 < len(node.Content); i += 2 {
231
- if node.Content[i].Value == key {
232
- next = node.Content[i+1]
233
-
234
- break
235
- }
236
- }
237
- node = next
238
- }
239
-
240
- return node
241
- }
242
-
243
- func text(node *yaml.Node) string {
244
- if node == nil || node.Kind != yaml.ScalarNode {
245
- return ""
246
- }
247
-
248
- return strings.TrimSpace(node.Value)
249
- }
250
-
251
- func list(node *yaml.Node) []string {
252
- if node == nil || node.Kind != yaml.SequenceNode {
253
- return nil
254
- }
255
- var out []string
256
- for _, item := range node.Content {
257
- out = append(out, text(item))
258
- }
259
-
260
- return out
261
- }
@@ -1,98 +0,0 @@
1
- package openapi
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "testing"
7
- )
8
-
9
- func TestIDs(t *testing.T) {
10
- if got := APIID("Auth", "1.2.3"); got != "auth.v1" {
11
- t.Errorf("APIID = %q", got)
12
- }
13
- if got := APIID("", ""); got != "api" {
14
- t.Errorf("APIID of nothing = %q", got)
15
- }
16
- if got := Title("price_list"); got != "PriceList" {
17
- t.Errorf("Title = %q", got)
18
- }
19
- if got := InterfaceID("auth.v1", "sessions"); got != "auth.v1" {
20
- t.Errorf("InterfaceID = %q", got)
21
- }
22
- if got := InterfaceID("auth.v1", ""); got != "auth.v1" {
23
- t.Errorf("InterfaceID without a tag = %q", got)
24
- }
25
- // A vendored copy may say what the estate calls it; a document that says
26
- // nothing is named the way it always was.
27
- if got := DocumentAPIID(" stripe.v1 ", "Stripe API", "2026-08-26.dahlia"); got != "stripe.v1" {
28
- t.Errorf("DocumentAPIID with x-portolan-api = %q", got)
29
- }
30
- if got := DocumentAPIID("", "Stripe API", "2026-08-26.dahlia"); got != "stripe-api.v2026-08-26" {
31
- t.Errorf("DocumentAPIID without = %q", got)
32
- }
33
- }
34
-
35
- // A system outside the estate is named by its document's title, and the
36
- // title's "API" is the interface, not the system.
37
- func TestExternalID(t *testing.T) {
38
- for title, want := range map[string]string{
39
- "Gordian Flights & Ancillaries API": "gordian-flights-ancillaries",
40
- "Stripe API": "stripe",
41
- " Partner REST API ": "partner-rest",
42
- "API": "api",
43
- "": "",
44
- "Amadeus (Self-Service) v2": "amadeus-self-service-v2",
45
- } {
46
- if got := ExternalID(title); got != want {
47
- t.Errorf("ExternalID(%q) = %q, want %q", title, got, want)
48
- }
49
- }
50
- }
51
-
52
- const doc = `openapi: 3.0.3
53
- info:
54
- title: auth
55
- version: 1.0.0
56
- paths:
57
- /v1/sessions:
58
- post:
59
- operationId: login
60
- tags: [sessions]
61
- /v1/users/{userId}:
62
- get:
63
- operationId: getUser
64
- tags: [users]
65
- /v1/health:
66
- get: {}
67
- `
68
-
69
- func TestReadAndFind(t *testing.T) {
70
- path := filepath.Join(t.TempDir(), "openapi.yaml")
71
- if err := os.WriteFile(path, []byte(doc), 0o644); err != nil {
72
- t.Fatal(err)
73
- }
74
- spec, err := Read(path)
75
- if err != nil {
76
- t.Fatal(err)
77
- }
78
- if spec.API != "auth.v1" || len(spec.Operations) != 3 {
79
- t.Fatalf("spec = %+v", spec)
80
- }
81
-
82
- login, ok := spec.Find("post", "/v1/sessions")
83
- if !ok || login.CallID(spec.API) != "auth.v1/login" {
84
- t.Errorf("login = %+v %v", login, ok)
85
- }
86
- // A generated client spells the parameter as a format verb.
87
- user, ok := spec.Find("GET", "/v1/users/%s")
88
- if !ok || user.CallID(spec.API) != "auth.v1/getUser" {
89
- t.Errorf("getUser = %+v %v", user, ok)
90
- }
91
- health, ok := spec.Find("GET", "/v1/health")
92
- if !ok || health.CallID(spec.API) != "auth.v1/GET /v1/health" {
93
- t.Errorf("an operation without an id or tag = %+v %v", health, ok)
94
- }
95
- if _, ok := spec.Find("DELETE", "/v1/sessions"); ok {
96
- t.Error("a verb the document does not declare was found")
97
- }
98
- }
@@ -1,19 +0,0 @@
1
- package main
2
-
3
- import (
4
- _ "embed"
5
-
6
- "github.com/shortlink-org/portolan/plugin"
7
- )
8
-
9
- //go:embed options.schema.json
10
- var optionsSchema []byte
11
-
12
- func descriptor() plugin.Descriptor {
13
- return plugin.Descriptor{
14
- Name: "verify-codeowners",
15
- Summary: "Reads the repository's CODEOWNERS against the catalog: who to ask about each service, which services nobody owns, and which rules own nothing.",
16
- Phases: []string{plugin.PhaseVerify},
17
- Options: optionsSchema,
18
- }
19
- }
@@ -1,11 +0,0 @@
1
- package main
2
-
3
- import (
4
- "testing"
5
-
6
- "github.com/shortlink-org/portolan/plugin/schematest"
7
- )
8
-
9
- func TestOptionsSchemaMatchesStruct(t *testing.T) {
10
- schematest.Check(t, optionsSchema, Options{})
11
- }
@@ -1,75 +0,0 @@
1
- // Command verify-codeowners says who to ask about each service, by reading the
2
- // CODEOWNERS file the repository already keeps.
3
- //
4
- // Ownership is the question a catalog is opened for and the one it answered
5
- // worst: `owner` on a flow or a store means the bounded context, not a team,
6
- // so "who do I ask about shop.oms" had no answer anywhere on the page. The
7
- // answer was already written down - every repository with review rules has it,
8
- // in a file the forge itself enforces - and typing it a second time into the
9
- // manifest would make the copy the thing that goes stale.
10
- //
11
- // So it is read, never written, and never resolved. A handle is what the file
12
- // says and what a reviewer types; turning `@acme/oms-team` into the people in
13
- // it is a call to a forge's API, which needs a credential this deliberately
14
- // does not have and would answer differently tomorrow.
15
- //
16
- // It is a verifier rather than an extractor because a rule is a path and only
17
- // the merged catalog knows where each service is. It earns the name twice
18
- // over: a service no rule matches, and a rule that matches no service, are
19
- // both reported - the second being the failure a CODEOWNERS file has and can
20
- // never report about itself, a team believing it owns something it does not.
21
- //
22
- // Nothing here is forge-specific. GitHub, GitLab and Gitea read the same file
23
- // from the same three places; where GitLab's sections change whose rule wins,
24
- // the file is read the flatter way and said so out loud.
25
- //
26
- // {
27
- // "plugins": [
28
- // { "name": "codeowners", "process": { "command": "go", "args": ["run", "./plugins/verify-codeowners"] } }
29
- // ],
30
- // "verify": [
31
- // { "plugin": "codeowners", "in": ".github", "out": "data", "options": { "out": "owners.json" } }
32
- // ]
33
- // }
34
- //
35
- // Point `in` at the directory the file is in rather than at the repository
36
- // root. The host dates a fragment from the last commit to touch the step's
37
- // input, and the subject of this one is the CODEOWNERS file: rooted at the
38
- // repository, it would be restamped by every commit ever made.
39
- package main
40
-
41
- import (
42
- "fmt"
43
- "io"
44
- "os"
45
-
46
- "github.com/shortlink-org/portolan/plugin"
47
- )
48
-
49
- // Options are what the manifest tells the verifier.
50
- type Options struct {
51
- // File is the CODEOWNERS to read, relative to the input root. Left out,
52
- // the three places a forge looks: CODEOWNERS, .github/CODEOWNERS and
53
- // docs/CODEOWNERS, in that order.
54
- File string `json:"file,omitempty"`
55
-
56
- // Out names the fragment file.
57
- Out string `json:"out,omitempty"`
58
- }
59
-
60
- func main() {
61
- if err := run(os.Stdin, os.Stdout); err != nil {
62
- fmt.Fprintln(os.Stderr, "portolan-verify-codeowners:", err)
63
- os.Exit(1)
64
- }
65
- }
66
-
67
- func run(stdin io.Reader, stdout io.Writer) error {
68
- return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
69
- if req.Input.Root == "" {
70
- return plugin.Response{}, fmt.Errorf("no input root: there is nowhere to look for a CODEOWNERS")
71
- }
72
-
73
- return verify(req, opts)
74
- })
75
- }
@@ -1,85 +0,0 @@
1
- package main
2
-
3
- // Whether a rule owns a directory.
4
- //
5
- // CODEOWNERS patterns are gitignore patterns, and the question asked here is
6
- // narrower than the one gitignore answers: not "does this pattern match this
7
- // file" but "does this pattern own this SERVICE", where a service is a
8
- // directory. A rule owns a directory when it matches the directory itself or
9
- // anything above it - `services/` owns `services/oms` - and does not when it
10
- // matches only something inside it: `services/oms/internal` is a rule about
11
- // part of the service, and reading it as ownership of the whole would hand a
12
- // team a page they never asked for.
13
- //
14
- // The two shapes a pattern comes in are the whole of the rule:
15
- //
16
- // - anchored, when it has a `/` anywhere but the end - `services/oms`,
17
- // `/docs` - and is matched against the path from the repository root;
18
- // - floating, when it is a single segment - `*.go`, `internal` - and matches
19
- // that segment at any depth, which is how gitignore has always read one.
20
- //
21
- // `*` and `?` stay inside a segment, `**` spans any number of them.
22
-
23
- import (
24
- "path"
25
- "strings"
26
- )
27
-
28
- // owns reports whether pattern owns the directory at target, both written
29
- // repository-relative and with forward slashes.
30
- func owns(pattern, target string) bool {
31
- pattern = strings.TrimSpace(pattern)
32
- target = strings.Trim(target, "/")
33
- if pattern == "" || target == "" {
34
- return false
35
- }
36
-
37
- // A pattern that is only a `/*` or `*` owns everything at that level.
38
- anchored := strings.HasPrefix(pattern, "/") || strings.Contains(strings.TrimSuffix(pattern, "/"), "/")
39
- pattern = strings.Trim(pattern, "/")
40
- if pattern == "" {
41
- return true // "/" - the whole repository.
42
- }
43
-
44
- segments := strings.Split(target, "/")
45
- if !anchored {
46
- // Any segment matching means the directory it names is owned, and so
47
- // is everything under it - including this one.
48
- for _, segment := range segments {
49
- if ok, _ := path.Match(pattern, segment); ok {
50
- return true
51
- }
52
- }
53
-
54
- return false
55
- }
56
-
57
- return matchPrefix(strings.Split(pattern, "/"), segments)
58
- }
59
-
60
- // matchPrefix reports whether the pattern matches the target or a prefix of
61
- // it. A pattern longer than the target names something inside the directory
62
- // and owns no more of it than that.
63
- func matchPrefix(pattern, target []string) bool {
64
- if len(pattern) == 0 {
65
- return true // Every segment of the pattern matched; the rest is inside it.
66
- }
67
- if pattern[0] == "**" {
68
- // Zero or more segments: try here, then one deeper.
69
- for i := 0; i <= len(target); i++ {
70
- if matchPrefix(pattern[1:], target[i:]) {
71
- return true
72
- }
73
- }
74
-
75
- return false
76
- }
77
- if len(target) == 0 {
78
- return false
79
- }
80
- if ok, _ := path.Match(pattern[0], target[0]); !ok {
81
- return false
82
- }
83
-
84
- return matchPrefix(pattern[1:], target[1:])
85
- }
@@ -1,47 +0,0 @@
1
- package main
2
-
3
- import "testing"
4
-
5
- // The rule the whole plugin turns on: a pattern owns a directory when it names
6
- // the directory or anything above it, and does not when it names only
7
- // something inside it.
8
- func TestOwns(t *testing.T) {
9
- cases := []struct {
10
- pattern string
11
- target string
12
- want bool
13
- why string
14
- }{
15
- {"examples/shop/oms", "examples/shop/oms", true, "the directory itself"},
16
- {"examples/shop", "examples/shop/oms", true, "a directory above it"},
17
- {"examples/", "examples/shop/oms", true, "a trailing slash is still a directory"},
18
- {"/examples", "examples/shop/oms", true, "anchored at the root"},
19
- {"*", "examples/shop/oms", true, "everything"},
20
- {"/", "examples/shop/oms", true, "the whole repository"},
21
-
22
- {"examples/shop/oms/internal", "examples/shop/oms", false, "a rule about part of the service is not a rule about the service"},
23
- {"examples/auth", "examples/shop/oms", false, "a sibling"},
24
- {"docs", "examples/shop/oms", false, "somewhere else entirely"},
25
-
26
- // Floating patterns match at any depth, which is what gitignore has
27
- // always meant by a pattern with no slash in it.
28
- {"oms", "examples/shop/oms", true, "a single segment matches at any depth"},
29
- {"shop", "examples/shop/oms", true, "including a segment above"},
30
- {"om", "examples/shop/oms", false, "and matches whole segments, not prefixes"},
31
-
32
- {"examples/*/oms", "examples/shop/oms", true, "a star stays inside its segment"},
33
- {"examples/*", "examples/shop/oms", true, "and matches the segment above"},
34
- {"examples/*/oms", "examples/shop/cart/oms", false, "so it does not cross a slash"},
35
- {"examples/**/oms", "examples/shop/cart/oms", true, "two stars do"},
36
- {"examples/**", "examples/shop/oms", true, "two stars also match nothing at all"},
37
-
38
- {"", "examples/shop/oms", false, "an empty pattern owns nothing"},
39
- {"examples", "", false, "and nothing is owned by no pattern"},
40
- }
41
-
42
- for _, c := range cases {
43
- if got := owns(c.pattern, c.target); got != c.want {
44
- t.Errorf("owns(%q, %q) = %v, want %v - %s", c.pattern, c.target, got, c.want, c.why)
45
- }
46
- }
47
- }