@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
@@ -0,0 +1,158 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { afterEach, beforeAll, describe, expect, it } from "vitest";
6
+
7
+ import { InitError, defaultAnswers, init } from "./init.mjs";
8
+
9
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ const created = [];
11
+
12
+ beforeAll(() => {
13
+ process.env.PORTOLAN_SCHEMA = resolve(root, "schema/portolan.schema.json");
14
+ process.env.PORTOLAN_INSTALL_ROOT = root;
15
+ });
16
+
17
+ afterEach(() => {
18
+ for (const path of created.splice(0)) rmSync(path, { recursive: true, force: true });
19
+ });
20
+
21
+ function workspace(files) {
22
+ const dir = mkdtempSync(join(tmpdir(), "portolan-init-"));
23
+ created.push(dir);
24
+ for (const [name, content] of Object.entries(files)) {
25
+ mkdirSync(dirname(join(dir, name)), { recursive: true });
26
+ writeFileSync(join(dir, name), content);
27
+ }
28
+ return dir;
29
+ }
30
+
31
+ const quiet = { version: "0.0.0-test", log: () => {}, ask: { ...defaultAnswers, warn: () => {} } };
32
+ const manifestOf = (dir) => JSON.parse(readFileSync(join(dir, "portolan.json"), "utf8"));
33
+ const goService = {
34
+ "go.mod": "module example.com/orders\n\ngo 1.24\n",
35
+ "internal/domain/order/order.go": "package order\n\ntype Order struct{ ID string }\n",
36
+ };
37
+
38
+ describe("init with defaults", () => {
39
+ it("reads what the repository contains into one project at the root", async () => {
40
+ const dir = workspace({
41
+ "package.json": '{"name":"@acme/order-service","scripts":{"test":"vitest"}}\n',
42
+ ...goService,
43
+ "api/openapi.yaml": "openapi: 3.0.0\ninfo:\n title: Orders\n version: '1'\npaths: {}\n",
44
+ "docs/adr/0001-free-form.md": "# Not an ADR Portolan understands\n",
45
+ });
46
+ const result = await init(dir, quiet);
47
+ const manifest = manifestOf(dir);
48
+
49
+ expect(manifest.projects).toEqual([{ id: "order-service", name: "Order Service", root: ".", group: "order-service", component: "order-service" }]);
50
+ expect(manifest.sources).toEqual(["portolan/*.json"]);
51
+ expect(manifest.extract.map((step) => step.plugin)).toEqual(["project", "go-domain", "openapi"]);
52
+ expect(manifest.extract.every((step) => step.in === "." && step.out === "portolan")).toBe(true);
53
+ expect(manifest.extract[0].options).toMatchObject({ groupKind: "bounded-context", componentKind: "service" });
54
+ expect(manifest.extract[2].options.spec).toBe("api/openapi.yaml");
55
+ expect(manifest.generate.map((step) => step.plugin)).toEqual(["markdown", "mermaid"]);
56
+ expect(result.generate).toBe(false);
57
+
58
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
59
+ expect(pkg.scripts).toEqual({ test: "vitest", "architecture": "portolan dev", "architecture:gen": "portolan generate", "architecture:check": "portolan check", "architecture:build": "portolan build" });
60
+ expect(readFileSync(join(dir, ".gitignore"), "utf8")).toContain(".portolan/\n");
61
+ });
62
+
63
+ it("turns nested build files into one project each when the root has none", async () => {
64
+ const dir = workspace({
65
+ "README.md": "# Estate\n",
66
+ "services/orders/go.mod": goService["go.mod"],
67
+ "services/orders/internal/domain/order/order.go": goService["internal/domain/order/order.go"],
68
+ "services/web/package.json": '{"name":"web"}\n',
69
+ });
70
+ await init(dir, quiet);
71
+ const manifest = manifestOf(dir);
72
+ const repository = manifest.projects[0].group;
73
+
74
+ expect(manifest.projects.map((project) => [project.id, project.root, project.component])).toEqual([
75
+ ["orders", "services/orders", "orders"],
76
+ ["web", "services/web", "web"],
77
+ ]);
78
+ expect(manifest.projects.every((project) => project.group === repository)).toBe(true);
79
+ expect(manifest.sources).toEqual(["services/orders/portolan/*.json", "services/web/portolan/*.json"]);
80
+ expect(manifest.extract.map((step) => [step.plugin, step.in])).toEqual([
81
+ ["project", "services/orders"],
82
+ ["go-domain", "services/orders"],
83
+ ["project", "services/web"],
84
+ ]);
85
+ });
86
+
87
+ it("keeps the root as the project when it has a build file of its own", async () => {
88
+ const dir = workspace({
89
+ "package.json": '{"name":"tool"}\n',
90
+ "examples/demo/go.mod": goService["go.mod"],
91
+ });
92
+ await init(dir, quiet);
93
+ expect(manifestOf(dir).projects.map((project) => project.root)).toEqual(["."]);
94
+ });
95
+
96
+ it("still writes a manifest for an empty directory", async () => {
97
+ const dir = workspace({});
98
+ await init(dir, quiet);
99
+ const manifest = manifestOf(dir);
100
+ expect(manifest.extract.map((step) => step.plugin)).toEqual(["project"]);
101
+ expect(manifest.projects[0].root).toBe(".");
102
+ });
103
+
104
+ it("refuses to touch an existing manifest", async () => {
105
+ const dir = workspace({ "portolan.json": "{}\n" });
106
+ await expect(init(dir, quiet)).rejects.toThrow(InitError);
107
+ expect(readFileSync(join(dir, "portolan.json"), "utf8")).toBe("{}\n");
108
+ });
109
+ });
110
+
111
+ describe("init with answers", () => {
112
+ it("writes only the roots, plugins and identity that were chosen", async () => {
113
+ const dir = workspace({
114
+ "README.md": "# Estate\n",
115
+ "services/orders/go.mod": goService["go.mod"],
116
+ "services/orders/internal/domain/order/order.go": goService["internal/domain/order/order.go"],
117
+ "services/web/package.json": '{"name":"web"}\n',
118
+ });
119
+ const asked = [];
120
+ const result = await init(dir, {
121
+ ...quiet,
122
+ ask: {
123
+ ...quiet.ask,
124
+ roots: (choices) => { asked.push(["roots", choices.map((choice) => [choice.path, choice.selected])]); return ["services/orders"]; },
125
+ plugins: (root, detections) => { asked.push(["plugins", root, detections.map((detection) => detection.plugin)]); return ["project"]; },
126
+ scripts: () => false,
127
+ generate: () => true,
128
+ },
129
+ });
130
+ const manifest = manifestOf(dir);
131
+
132
+ expect(asked).toEqual([
133
+ ["roots", [[".", false], ["services/orders", true], ["services/web", true]]],
134
+ ["plugins", "services/orders", ["project", "go-domain"]],
135
+ ]);
136
+ expect(manifest.projects.map((project) => project.id)).toEqual(["orders"]);
137
+ expect(manifest.extract.map((step) => step.plugin)).toEqual(["project"]);
138
+ expect(result.generate).toBe(true);
139
+ });
140
+
141
+ it("uses the id the user typed for a single project", async () => {
142
+ const dir = workspace({ "package.json": '{"name":"@acme/whatever"}\n' });
143
+ await init(dir, {
144
+ ...quiet,
145
+ ask: { ...quiet.ask, identity: (defaults) => ({ ...defaults, id: "orders", name: "Orders", group: "orders", component: "orders" }) },
146
+ });
147
+ const manifest = manifestOf(dir);
148
+ expect(manifest.projects[0]).toMatchObject({ id: "orders", name: "Orders" });
149
+ expect(manifest.extract[0].options).toMatchObject({ group: "orders", component: "orders", componentName: "Orders" });
150
+ expect(manifest.generate[0].options.title).toBe("Orders");
151
+ });
152
+
153
+ it("writes nothing when the user declines", async () => {
154
+ const dir = workspace({ "package.json": '{"name":"x"}\n' });
155
+ await expect(init(dir, { ...quiet, ask: { ...quiet.ask, write: () => false } })).rejects.toThrow(/cancelled/);
156
+ expect(() => readFileSync(join(dir, "portolan.json"))).toThrow();
157
+ });
158
+ });
package/cli/portolan.mjs CHANGED
@@ -18,6 +18,8 @@ import { glob } from "node:fs/promises";
18
18
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
 
21
+ import { InitError, commandWorks, init as runInit, isInteractive, promptAnswers, toolchainFor } from "./init.mjs";
22
+
21
23
  const installRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
22
24
  const packageJson = JSON.parse(readFileSync(resolve(installRoot, "package.json"), "utf8"));
23
25
 
@@ -84,7 +86,7 @@ function help() {
84
86
  Usage: portolan <command> [options]
85
87
 
86
88
  Commands:
87
- init create a minimal portolan.json for this repository
89
+ init inspect this repository and create portolan.json
88
90
  dev open the local architecture site
89
91
  generate update catalog fragments, docs, and exports
90
92
  check fail when committed generated files are out of date
@@ -99,73 +101,48 @@ Options:
99
101
  --base PATH deployed URL base (default: /)
100
102
  --host HOST dev server host (default: 127.0.0.1)
101
103
  --port PORT dev server port
102
- --yes, -y accept non-destructive init defaults`);
104
+ --yes, -y init without questions: take every detected default`);
103
105
  }
104
106
 
105
- function init(workspace) {
106
- const manifestPath = resolve(workspace, "portolan.json");
107
- if (existsSync(manifestPath)) fail("portolan.json already exists; init did not change it");
108
-
109
- const projectPackage = readJson(resolve(workspace, "package.json"));
110
- const rawName = String(projectPackage?.name ?? basename(workspace)).replace(/^@[^/]+\//, "");
111
- const id = slug(rawName) || "project";
112
- const title = titleOf(id);
113
- const manifest = {
114
- $schema: `https://raw.githubusercontent.com/shortlink-org/portolan/${VERSION}/schema/portolan.schema.json`,
115
- sources: ["portolan/*.json"],
116
- projects: [{ id, name: title, root: ".", group: id, component: id }],
117
- extract: [{
118
- plugin: "project",
119
- in: ".",
120
- out: "portolan",
121
- options: { group: id, groupName: title, component: id, componentName: title },
122
- }],
123
- generate: [
124
- { plugin: "markdown", out: "docs", options: { title } },
125
- { plugin: "mermaid", out: "exports/mermaid", options: { title: `${title} flows` } },
126
- ],
127
- };
128
- writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
129
-
130
- if (projectPackage) {
131
- const scripts = { ...(projectPackage.scripts ?? {}) };
132
- const defaults = {
133
- "architecture": "portolan dev",
134
- "architecture:gen": "portolan generate",
135
- "architecture:check": "portolan check",
136
- "architecture:build": "portolan build",
137
- };
138
- for (const [name, command] of Object.entries(defaults)) {
139
- if (!(name in scripts)) scripts[name] = command;
140
- }
141
- writeFileSync(resolve(workspace, "package.json"), `${JSON.stringify({ ...projectPackage, scripts }, null, 2)}\n`);
142
- }
143
-
144
- const ignorePath = resolve(workspace, ".gitignore");
145
- const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
146
- if (!ignore.split(/\r?\n/).includes(".portolan/")) {
147
- writeFileSync(ignorePath, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}\n# Portolan local build state\n.portolan/\n`);
107
+ async function init(workspace, options) {
108
+ const interactive = !options.yes && isInteractive();
109
+ let result;
110
+ try {
111
+ result = await runInit(workspace, { version: VERSION, ask: interactive ? promptAnswers(VERSION) : undefined });
112
+ } catch (error) {
113
+ if (error instanceof InitError) fail(error.message);
114
+ throw error;
148
115
  }
149
-
150
- console.log("created portolan.json");
151
- if (projectPackage) console.log("added architecture scripts to package.json");
152
- console.log("next: portolan generate && portolan dev");
116
+ if (result.generate) runScript("scripts/gen.mjs", [], workspace);
153
117
  }
154
118
 
155
119
  function doctor(workspace) {
120
+ const manifestPath = resolve(workspace, "portolan.json");
121
+ const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : null;
156
122
  const checks = [
157
123
  ["Node.js >= 24", Number(process.versions.node.split(".")[0]) >= 24, process.version],
158
- ["portolan.json", existsSync(resolve(workspace, "portolan.json")), "required"],
124
+ ["portolan.json", Boolean(manifest), "required"],
159
125
  ["Git", commandWorks("git", ["--version"]), "used for deterministic source stamps"],
160
- ["Go", commandWorks("go", ["version"]), "required by built-in Go extractors"],
161
- ["Python 3", commandWorks("python3", ["--version"]), "required by Python extractors"],
162
- ["Java", commandWorks("java", ["-version"]), "required by the Java extractor"],
163
- ["Cargo", commandWorks("cargo", ["--version"]), "required by the Rust extractor"],
164
126
  ];
127
+
128
+ // A toolchain is required only when a step in this manifest names a plugin
129
+ // that runs in it; the built-in Go plugins run as wasm and ask for nothing.
130
+ const needed = new Map();
131
+ const steps = manifest ? ["extract", "verify", "generate"].flatMap((phase) => manifest[phase] ?? []) : [];
132
+ for (const name of new Set(steps.map((step) => step.plugin))) {
133
+ const need = toolchainFor(name, (manifest.plugins ?? []).find((plugin) => plugin.name === name));
134
+ if (need) needed.set(need.label, [...(needed.get(need.label) ?? []), name]);
135
+ }
136
+ for (const [label, command, args] of [["Go", "go", ["version"]], ["Python 3", "python3", ["--version"]], ["Java", "java", ["-version"]], ["Cargo", "cargo", ["--version"]]]) {
137
+ const plugins = needed.get(label);
138
+ const present = commandWorks(command, args);
139
+ checks.push([label, present || !plugins, plugins ? `needed by ${plugins.join(", ")}${present ? "" : "; not on PATH"}` : `${present ? "present" : "absent"}; nothing in portolan.json needs it`]);
140
+ }
141
+
165
142
  for (const [label, ok, note] of checks) {
166
143
  console.log(`${ok ? "ok" : "--"} ${label}${note ? ` — ${note}` : ""}`);
167
144
  }
168
- if (!checks[0][1] || !checks[1][1]) process.exitCode = 1;
145
+ if (checks.some(([, ok]) => !ok)) process.exitCode = 1;
169
146
  }
170
147
 
171
148
  async function build(workspace, options) {
@@ -194,6 +171,7 @@ async function dev(workspace, options) {
194
171
  runNode(packageBin("vite", "bin/vite.js"), args, workspace, {
195
172
  ...process.env,
196
173
  PORTOLAN_WORKSPACE: workspace,
174
+ BASE_PATH: options.base ?? process.env.BASE_PATH ?? "/",
197
175
  });
198
176
  }
199
177
 
@@ -248,7 +226,10 @@ async function prepareSite(workspace) {
248
226
 
249
227
  function generateLikeC4(stage) {
250
228
  runNode(resolve(stage, "scripts/gen-likec4.mjs"), [], stage);
251
- runNode(packageBin("likec4", "bin/likec4.mjs"), ["gen", "react", "likec4", "-o", "src/likec4/generated.jsx"], stage);
229
+ // Inside a container likec4 switches to a graphviz binary by default and,
230
+ // finding none, reports "no views found". The wasm engine it uses
231
+ // everywhere else is the one wanted, so it is asked for by name.
232
+ runNode(packageBin("likec4", "bin/likec4.mjs"), ["gen", "react", "likec4", "-o", "src/likec4/generated.jsx", "--no-use-dot"], stage);
252
233
  }
253
234
 
254
235
  async function matchedFiles(workspace, patterns) {
@@ -341,23 +322,6 @@ function inside(root, target) {
341
322
  return path === "" || (!path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path));
342
323
  }
343
324
 
344
- function commandWorks(command, args) {
345
- const result = spawnSync(command, args, { stdio: "ignore" });
346
- return !result.error && result.status === 0;
347
- }
348
-
349
- function readJson(path) {
350
- try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
351
- }
352
-
353
- function slug(value) {
354
- return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
355
- }
356
-
357
- function titleOf(value) {
358
- return value.split("-").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
359
- }
360
-
361
325
  function fail(message) {
362
326
  console.error(`portolan: ${message}`);
363
327
  process.exit(1);
package/index.html CHANGED
@@ -4,6 +4,25 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <meta
8
+ name="description"
9
+ content="Portolan turns code, specifications, schemas, traces and ADRs into a static, navigable architecture catalog."
10
+ />
11
+ <meta property="og:title" content="Portolan — architecture from code" />
12
+ <meta
13
+ property="og:description"
14
+ content="A navigable map of your software estate, generated from code and evidence."
15
+ />
16
+ <meta property="og:type" content="website" />
17
+ <meta
18
+ property="og:image"
19
+ content="https://shortlink-org.github.io/portolan/og.png"
20
+ />
21
+ <meta name="twitter:card" content="summary_large_image" />
22
+ <meta
23
+ name="twitter:image"
24
+ content="https://shortlink-org.github.io/portolan/og.png"
25
+ />
7
26
  <title>portolan</title>
8
27
  </head>
9
28
  <body>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@shortlink-org/portolan",
3
3
  "description": "Generate a navigable architecture catalog from code and specifications.",
4
4
  "private": false,
5
- "version": "0.1.0",
5
+ "version": "0.2.0",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "homepage": "https://github.com/shortlink-org/portolan#readme",
@@ -26,7 +26,6 @@
26
26
  "public",
27
27
  "schema",
28
28
  "plugins/*.wasm",
29
- "plugins/*/*.go",
30
29
  "plugins/*/*.py",
31
30
  "plugins/*/*.ts",
32
31
  "plugins/*/*.json",
@@ -35,15 +34,10 @@
35
34
  "plugins/*/*.lock",
36
35
  "plugins/*/src",
37
36
  "plugins/extract-java/build",
38
- "plugin",
39
- "catalog",
40
- "internal",
41
37
  "index.html",
42
38
  "vite.config.ts",
43
39
  "tsconfig.json",
44
40
  "portolan.json",
45
- "go.mod",
46
- "go.sum",
47
41
  "README.md",
48
42
  "LICENSE"
49
43
  ],
@@ -61,10 +55,10 @@
61
55
  "test:package": "node scripts/package-smoke.mjs",
62
56
  "test:watch": "vitest",
63
57
  "typecheck": "tsc --noEmit && tsc -p plugins/extract-ts",
64
- "likec4:gen": "node scripts/gen-likec4.mjs && likec4 gen react likec4 -o src/likec4/generated.jsx",
58
+ "likec4:gen": "node scripts/gen-likec4.mjs && likec4 gen react likec4 -o src/likec4/generated.jsx --no-use-dot",
65
59
  "likec4:validate": "likec4 validate likec4",
66
60
  "predev": "npm run likec4:gen",
67
- "plugins:build": "GOOS=wasip1 GOARCH=wasm go build -o plugins/gen-markdown.wasm ./plugins/gen-markdown && GOOS=wasip1 GOARCH=wasm go build -o plugins/gen-mermaid.wasm ./plugins/gen-mermaid && GOOS=wasip1 GOARCH=wasm go build -o plugins/gen-backstage.wasm ./plugins/gen-backstage && javac --release 21 -d plugins/extract-java/build plugins/extract-java/src/org/portolan/extract/*.java",
61
+ "plugins:build": "GOOS=wasip1 GOARCH=wasm go build -trimpath -ldflags=\"-s -w\" -o plugins/portolan-go.wasm ./plugins/cmd/portolan-go && javac --release 21 -d plugins/extract-java/build plugins/extract-java/src/org/portolan/extract/*.java",
68
62
  "schema": "npm run plugins:build && node scripts/schema.mjs",
69
63
  "schema:check": "npm run plugins:build && node scripts/schema.mjs --check",
70
64
  "gen": "npm run plugins:build && node scripts/gen.mjs",
@@ -78,6 +72,7 @@
78
72
  "@ai-sdk/openai-compatible": "3.0.44",
79
73
  "@ai-sdk/react": "4.0.96",
80
74
  "@asyncapi/react-component": "^3.1.8",
75
+ "@clack/prompts": "^1.8.0",
81
76
  "@headlessui/react": "^2.2.10",
82
77
  "@scalar/api-reference-react": "^0.9.66",
83
78
  "@tanstack/react-query": "^5.102.8",
@@ -108,7 +103,7 @@
108
103
  "zustand": "^5.0.15"
109
104
  },
110
105
  "devDependencies": {
111
- "@types/node": "^26.4.0",
106
+ "@types/node": "^26.5.0",
112
107
  "@types/react": "^19.0.0",
113
108
  "@types/react-dom": "^19.0.0",
114
109
  "typescript": "^7.0.2",
package/plugins/README.md CHANGED
@@ -31,6 +31,13 @@ is what makes that worth having: `encoding/json` drops a field it does not
31
31
  recognise, so before this a misspelled option was no option at all and nothing
32
32
  said so.
33
33
 
34
+ A descriptor may also carry `needs`: what the host must put in the request
35
+ beyond the tree, because a sandboxed module cannot reach it. The one need so
36
+ far is `history` (portolan.0007) - when each file under the root was first
37
+ committed and last changed, read by the host from one `git log` and handed
38
+ over as `input.history`, keyed by the path the plugin would open. `extract-adr`
39
+ asks for it; a plugin that does not ask is handed nothing.
40
+
34
41
  A generator **names** files; it never writes them. `scripts/gen.mjs` writes what
35
42
  comes back, refuses a name that points outside the output directory, and deletes
36
43
  pages that stopped being generated. That is what lets a generator run as a wasm
@@ -56,7 +63,7 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
56
63
 
57
64
  ## Adding one
58
65
 
59
- 1. Write it. In Go, a new directory here with a `main` that hands its options
66
+ 1. Write it. In Go, a new package here whose `run` hands its options
60
67
  type to `plugin.Serve`, which reads the request, answers a describe and calls
61
68
  the work; `catalog.Catalog` from `github.com/shortlink-org/portolan/catalog`
62
69
  is the mirror of the schema, and `internal/goscan` is the tree as syntax -
@@ -71,8 +78,12 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
71
78
  `go:embed` and returned in the descriptor. `schematest.Check` in a test keeps
72
79
  it from drifting from the options struct: a field renamed on one side and not
73
80
  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`.
81
+ 3. Build it. A built-in Go plugin is a library package with
82
+ `Serve(io.Reader, io.Writer) error`; add it to the map in
83
+ `plugins/cmd/portolan-go/main.go`, and `plugins:build` in `package.json`
84
+ puts it in `plugins/portolan-go.wasm` with the rest (a test keeps the map
85
+ and `portolan.json` in step). A plugin of your own is its own module:
86
+ `GOOS=wasip1 GOARCH=wasm go build`.
76
87
  4. Declare it in `portolan.json`, under `plugins` (how to run it) and
77
88
  `generate` (what to run it on), then run `npm run schema` so the manifest
78
89
  schema learns its options.
@@ -80,7 +91,7 @@ either be rendered or be explicitly acknowledged by the relevant exporter.
80
91
  ```json
81
92
  {
82
93
  "plugins": [
83
- { "name": "markdown", "wasm": { "url": "file://plugins/gen-markdown.wasm" } }
94
+ { "name": "markdown", "wasm": { "url": "file://plugins/portolan-go.wasm" } }
84
95
  ],
85
96
  "generate": [
86
97
  { "plugin": "markdown", "out": "docs", "options": { "title": "Example estate" } }
@@ -449,7 +460,7 @@ only left out of its own stamp when the output is *inside* the input root, and
449
460
 
450
461
  ```json
451
462
  {
452
- "plugins": [{ "name": "adr", "process": { "cmd": "go run ./plugins/extract-adr" } }],
463
+ "plugins": [{ "name": "adr", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
453
464
  "extract": [
454
465
  {
455
466
  "plugin": "adr",
@@ -514,7 +525,7 @@ is the point of the id; the same word twice in one context is an error.
514
525
 
515
526
  ```json
516
527
  {
517
- "plugins": [{ "name": "glossary", "process": { "cmd": "go run ./plugins/extract-glossary" } }],
528
+ "plugins": [{ "name": "glossary", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
518
529
  "extract": [
519
530
  {
520
531
  "plugin": "glossary",
@@ -588,7 +599,7 @@ runner files sit beside the service, so the step is told which one it is.
588
599
 
589
600
  ```json
590
601
  {
591
- "plugins": [{ "name": "commands", "process": { "cmd": "go run ./plugins/extract-commands" } }],
602
+ "plugins": [{ "name": "commands", "wasm": { "url": "file://plugins/portolan-go.wasm" } }],
592
603
  "extract": [
593
604
  {
594
605
  "plugin": "commands",
@@ -808,15 +819,31 @@ the forge looks.
808
819
 
809
820
  ## wasm or process
810
821
 
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.
822
+ `wasm` is the default and should stay that way. The module gets no network, no
823
+ environment and no way to start a process. A generator gets no filesystem
824
+ either. An extract or verify step gets the workspace preopened as `/`
825
+ (portolan.0006), which is how the built-in Go extractors read a tree without a
826
+ Go toolchain on the machine: every one of them, and the three generators, is
827
+ the single module `plugins/portolan-go.wasm`, which answers to the plugin name
828
+ the host passes as `argv[0]`. WASI preopens read-write, so an extractor is
829
+ trusted not to write the tree it reads, the same trust a process plugin has
830
+ today; a `sha256` pins that trust to a build.
831
+
832
+ `process` is the escape hatch for a plugin that needs a toolchain: the Rust,
833
+ Java, Python and TypeScript extractors run in their own runtimes, and
834
+ `fetch-bsr` still talks to its registry from Go. It gets the same protocol
835
+ and none of the sandbox, which is the trade being made and the reason it is
836
+ not the default. It declares `command` and an `args` array; the host never
837
+ feeds a command string through a shell. A built-in Go plugin that still runs
838
+ as a process is the same code reached as
839
+ `go run ./plugins/cmd/portolan-go <name>`.
840
+
841
+ `host` is for Portolan's own code that needs what only the host has - a git
842
+ binary, a socket - and so runs inside the host process (portolan.0008):
843
+ `{ "name": "git", "host": "fetch-git" }`. The name is resolved against the
844
+ modules shipped in `scripts/host-plugins/` and nothing else, so a manifest
845
+ cannot point the host at arbitrary code; the contract is the same as any
846
+ plugin's, files named and never written.
820
847
 
821
848
  A plugin fetched over `https://` must declare its `sha256`; the host verifies it
822
849
  and caches by digest. A `file://` plugin may declare one, but a checksum
@@ -838,10 +865,15 @@ paths inside the copy are the repository's own, which is the point: the
838
865
  extract step that follows points its `in` at the vendored service and reads
839
866
  it exactly as it would read that service's checkout.
840
867
 
868
+ It runs inside the host (`scripts/host-plugins/fetch-git.mjs`,
869
+ portolan.0008) rather than as a module, because it needs a git binary and a
870
+ socket, and a manifest names it with `host` rather than `wasm` or `process`.
871
+ The contract is the same: it names files, the host writes them.
872
+
841
873
  ```json
842
874
  {
843
875
  "sources": ["data/*.json", "vendor/repos/*/*/git.repo.json"],
844
- "plugins": [{ "name": "git", "process": { "command": "go", "args": ["run", "./plugins/fetch-git"] } }],
876
+ "plugins": [{ "name": "git", "host": "fetch-git" }],
845
877
  "extract": [
846
878
  {
847
879
  "plugin": "git",
@@ -928,8 +960,8 @@ its protos and locks are on disk by the time the parser reads them.
928
960
  ```json
929
961
  {
930
962
  "plugins": [
931
- { "name": "bsr", "process": { "command": "go", "args": ["run", "./plugins/fetch-bsr"] } },
932
- { "name": "proto", "process": { "command": "go", "args": ["run", "./plugins/extract-proto"] } }
963
+ { "name": "bsr", "host": "fetch-bsr" },
964
+ { "name": "proto", "wasm": { "url": "file://plugins/portolan-go.wasm" } }
933
965
  ],
934
966
  "extract": [
935
967
  {
@@ -966,12 +998,14 @@ decides what to do with them — and not an oversight to work around.
966
998
 
967
999
  ### Why fetch-bsr can never be wasm
968
1000
 
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.
1001
+ It needs a socket and a credential, which is why it runs inside the host
1002
+ (`scripts/host-plugins/fetch-bsr.mjs`, portolan.0008) and is declared with
1003
+ `host`. The protocol's "no ambient state" rule is about *facts*: nothing about
1004
+ the estate may come from anywhere but the request. A credential is not a fact
1005
+ about the estate — it decides whether the fetch succeeds, never what the fetch
1006
+ says — and a test asserts the output is byte-identical with and without a
1007
+ token. The token comes from `BUF_TOKEN` or the netrc `buf registry login`
1008
+ wrote, never from the manifest.
975
1009
 
976
1010
  ### Pinning, and the offline rule
977
1011
 
@@ -1037,6 +1071,11 @@ the reading over a tree it can verify without a registry existing at all.
1037
1071
  | output | one schema and a `csr.lock.json` per subject | one catalog fragment |
1038
1072
  | deterministic | only because it is pinned and cached | absolutely |
1039
1073
 
1074
+ Declared as `{ "name": "csr", "host": "fetch-csr" }` and
1075
+ `{ "name": "csr-schemas", "wasm": { "url": "file://plugins/portolan-go.wasm" } }`:
1076
+ the fetcher runs inside the host (`scripts/host-plugins/fetch-csr.mjs`,
1077
+ portolan.0008), the reader in the shared module.
1078
+
1040
1079
  A registered version is immutable: subject `orders-value` at version 3 is the
1041
1080
  same bytes today and next year, and re-registering a changed schema makes
1042
1081
  version 4. That is the promise a BSR commit makes, so the same four rules govern
@@ -1050,8 +1089,8 @@ Each lands in its own directory, and the referring subject's lock is what an
1050
1089
  offline run follows to find them.
1051
1090
 
1052
1091
  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
1092
+ indented — re-spaced token by token, never re-parsed, so the file still says
1093
+ what the registry said in the order it said it — because a version
1055
1094
  bump that is one unreadable line is a review nobody can do. The digest is over
1056
1095
  the bytes as written, so verifying needs no reformatting of anything.
1057
1096
 
Binary file