@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,302 +0,0 @@
1
- package main
2
-
3
- import (
4
- "strings"
5
-
6
- "github.com/shortlink-org/portolan/catalog"
7
- "github.com/shortlink-org/portolan/plugin"
8
- )
9
-
10
- // The site for a language model, in the shape llmstxt.org asks for.
11
- //
12
- // llms.txt is the index page again as flat lists: one H1, one blockquote that
13
- // says what the estate is, then a link and a line of context per entity. A
14
- // reader that fetches pages on demand starts here.
15
- //
16
- // llms-full.txt is every page of the site in one file, for a reader that has a
17
- // context window and not a filesystem. It is assembled from the pages already
18
- // rendered, not rendered again, so it cannot disagree with them.
19
- //
20
- // Neither takes the estate's public URL: the links are the same relative
21
- // paths every other page uses, correct wherever the docs directory is mounted.
22
- const (
23
- llmsPage = "llms.txt"
24
- llmsFullPage = "llms-full.txt"
25
- )
26
-
27
- // renderLlms runs last. Everything the builder holds at that point is the
28
- // site, and llms-full.txt is that, in that order.
29
- func (s *site) renderLlms() {
30
- pages := append([]plugin.File(nil), s.b.Files...)
31
-
32
- s.b.file(llmsPage, s.llmsIndex())
33
- s.b.file(llmsFullPage, s.llmsFull(pages))
34
- }
35
-
36
- func (s *site) llmsIndex() string {
37
- const self = llmsPage
38
-
39
- var b strings.Builder
40
- b.WriteString("# " + s.title() + "\n\n")
41
- b.WriteString("> " + s.llmsSummary() + "\n\n")
42
- b.WriteString(s.stamp() + "\n")
43
- b.WriteString("Every link below is a markdown page beside this file. " +
44
- "[" + llmsFullPage + "](" + llmsFullPage + ") is all of those pages in one file, " +
45
- "in the order the site lists them.\n")
46
-
47
- var contexts, services []string
48
- for i := range s.cat.Contexts {
49
- ctx := &s.cat.Contexts[i]
50
- contexts = append(contexts, entry(s.ref(self, ctx.ID, ctx.Name), string(ctx.Classification), firstLine(ctx.Summary)))
51
-
52
- for j := range ctx.Services {
53
- svc := &ctx.Services[j]
54
- names := make([]string, 0, len(svc.Aggregates))
55
- for k := range svc.Aggregates {
56
- names = append(names, svc.Aggregates[k].Name)
57
- }
58
- aggregates := ""
59
- if len(names) > 0 {
60
- aggregates = "Aggregates: " + strings.Join(names, ", ")
61
- }
62
- services = append(services, entry(s.ref(self, svc.ID, svc.Name), "in "+ctx.Name, aggregates))
63
- }
64
- }
65
- section(&b, "Contexts", list(contexts))
66
-
67
- // Straight after the contexts and before anything that uses the words. A
68
- // model reading this estate cold has the same problem a person has: every
69
- // page below is written in a vocabulary it has not been told yet.
70
- var glossaries []string
71
- for i := range s.cat.Contexts {
72
- ctx := &s.cat.Contexts[i]
73
- terms := s.termsOf[ctx.ID]
74
- if len(terms) == 0 {
75
- continue
76
- }
77
- names := make([]string, 0, len(terms))
78
- for _, term := range terms {
79
- names = append(names, term.Name)
80
- }
81
- glossaries = append(glossaries, entry(
82
- link("Glossary of "+ctx.Name, self, s.glossaryPath(ctx)),
83
- plural(len(terms), "term"),
84
- strings.Join(names, ", "),
85
- ))
86
- }
87
- section(&b, "Glossaries", list(glossaries))
88
-
89
- section(&b, "Services", list(services))
90
-
91
- // The pages a model has most to learn from: the shape of the data, the
92
- // commands and the events of the estate all live here, and nowhere else
93
- // on the site is the readme of an aggregate printed in full.
94
- var aggregates []string
95
- for i := range s.cat.Contexts {
96
- ctx := &s.cat.Contexts[i]
97
- for j := range ctx.Services {
98
- svc := &ctx.Services[j]
99
- for k := range svc.Aggregates {
100
- agg := &svc.Aggregates[k]
101
- commands, queries := 0, 0
102
- for _, op := range agg.Operations {
103
- if op.Kind == catalog.OperationQuery {
104
- queries++
105
- } else {
106
- commands++
107
- }
108
- }
109
- shape := []string{plural(commands, "command")}
110
- if queries > 0 {
111
- shape = append(shape, plural(queries, "query", "queries"))
112
- }
113
- shape = append(shape, plural(len(agg.Events), "event"))
114
- aggregates = append(aggregates, entry(
115
- s.ref(self, agg.ID, agg.Name),
116
- "in "+s.ref(self, svc.ID, svc.Name),
117
- "root "+code(agg.Root),
118
- strings.Join(shape, ", "),
119
- firstSentence(body(agg.Readme, agg.Name)),
120
- ))
121
- }
122
- }
123
- }
124
- section(&b, "Aggregates", list(aggregates))
125
-
126
- externals := make([]string, 0, len(s.cat.Externals))
127
- for i := range s.cat.Externals {
128
- ext := &s.cat.Externals[i]
129
- interfaces := make([]string, 0, len(ext.Provides))
130
- for j := range ext.Provides {
131
- interfaces = append(interfaces, ext.Provides[j].ID)
132
- }
133
- externals = append(externals, entry(s.ref(self, ext.ID, orDefault(ext.Name, ext.ID)),
134
- "outside the estate", "Answers on "+strings.Join(interfaces, ", "), firstLine(ext.Summary)))
135
- }
136
- section(&b, "Outside the estate", list(externals))
137
-
138
- modules := make([]string, 0, len(s.cat.Modules))
139
- for i := range s.cat.Modules {
140
- module := &s.cat.Modules[i]
141
- modules = append(modules, entry(
142
- s.ref(self, module.ID, orDefault(module.Name, module.ID)),
143
- "schema module", "published by "+s.ref(self, module.Owner, module.Owner),
144
- strings.Join(module.Packages, ", "),
145
- ))
146
- }
147
- section(&b, "Schema modules", list(modules))
148
-
149
- flows := make([]string, 0, len(s.cat.Flows))
150
- for i := range s.cat.Flows {
151
- flow := &s.cat.Flows[i]
152
- owner := ""
153
- if flow.Owner != "" {
154
- owner = "owned by " + s.ref(self, flow.Owner, flow.Owner)
155
- }
156
- flows = append(flows, entry(s.ref(self, flow.ID, flow.Name), owner, firstLine(flow.Summary)))
157
- }
158
- section(&b, "Flows", list(flows))
159
-
160
- adrs := make([]string, 0, len(s.cat.Adrs))
161
- for i := range s.cat.Adrs {
162
- adr := &s.cat.Adrs[i]
163
- var when []string
164
- if adr.Status != "" {
165
- when = append(when, string(adr.Status))
166
- }
167
- if adr.Date != "" {
168
- when = append(when, adr.Date)
169
- }
170
- title := adr.Title
171
- if len(when) > 0 {
172
- title += " (" + strings.Join(when, ", ") + ")"
173
- }
174
- adrs = append(adrs, entry(s.ref(self, adr.ID, adr.ID), title))
175
- }
176
- section(&b, "Decisions", list(adrs))
177
-
178
- // What llmstxt.org calls Optional: pages a reader short on context can
179
- // skip. The shape of a shared type or a table matters once a page above
180
- // has named it, and not before.
181
- var optional []string
182
- if len(s.cat.Defs) > 0 {
183
- optional = append(optional, entry(link("Shared types", self, typesPage),
184
- "types named by more than one aggregate, event or message"))
185
- }
186
- for i := range s.cat.Stores {
187
- store := &s.cat.Stores[i]
188
- note := string(store.Kind) + " store"
189
- if owner, ok := s.services[store.Owner]; ok {
190
- note += " of " + s.ref(self, owner.ID, owner.Name)
191
- }
192
- schema := plural(len(store.Tables), "table")
193
- if len(store.Keyspaces) > 0 {
194
- schema = plural(len(store.Keyspaces), "key pattern")
195
- }
196
- optional = append(optional, entry(s.ref(self, store.ID, store.Name), note, schema))
197
- }
198
- section(&b, "Optional", list(optional))
199
-
200
- return b.String()
201
- }
202
-
203
- // llmsSummary is the blockquote: what kind of thing this is, and how much of
204
- // it there is.
205
- func (s *site) llmsSummary() string {
206
- services, aggregates := 0, 0
207
- for i := range s.cat.Contexts {
208
- services += len(s.cat.Contexts[i].Services)
209
- for j := range s.cat.Contexts[i].Services {
210
- aggregates += len(s.cat.Contexts[i].Services[j].Aggregates)
211
- }
212
- }
213
-
214
- parts := []string{
215
- plural(len(s.cat.Contexts), "bounded context"),
216
- plural(services, "service"),
217
- plural(aggregates, "aggregate"),
218
- plural(len(s.cat.Stores), "store"),
219
- }
220
- if len(s.cat.Modules) > 0 {
221
- parts = append(parts, plural(len(s.cat.Modules), "schema module"))
222
- }
223
- // Named only when there is one: an estate that calls nobody outside has
224
- // nothing to count, and "0 systems outside" reads as a claim about the
225
- // world rather than about the catalog.
226
- if len(s.cat.Externals) > 0 {
227
- parts = append(parts, plural(len(s.cat.Externals), "system outside the estate", "systems outside the estate"))
228
- }
229
- parts = append(parts,
230
- plural(len(s.cat.Flows), "flow"),
231
- plural(len(s.cat.Adrs), "decision record"),
232
- )
233
-
234
- return "Architecture catalog of " + s.title() + ": " + strings.Join(parts[:len(parts)-1], ", ") +
235
- " and " + parts[len(parts)-1] + ", read from the code, the specs and the traces of the estate."
236
- }
237
-
238
- func (s *site) llmsFull(pages []plugin.File) string {
239
- var b strings.Builder
240
- b.WriteString("*" + llmsFullPage + ": every page of " + s.title() + ", one after another. " +
241
- "A page begins with a comment naming its file, and the links inside it are " +
242
- "relative to that file's directory.*\n\n")
243
- b.WriteString(s.stamp())
244
-
245
- for _, page := range pages {
246
- b.WriteString("\n<!-- " + page.Name + " -->\n\n")
247
- b.WriteString(strings.TrimRight(page.Contents, "\n") + "\n")
248
- }
249
-
250
- return b.String()
251
- }
252
-
253
- // entry is one line of an llms.txt list: the link, then whatever notes are
254
- // not empty, sentence by sentence.
255
- func entry(link string, notes ...string) string {
256
- kept := make([]string, 0, len(notes))
257
- for _, note := range notes {
258
- note = strings.TrimSpace(note)
259
- if note == "" {
260
- continue
261
- }
262
- kept = append(kept, strings.TrimSuffix(note, "."))
263
- }
264
- if len(kept) == 0 {
265
- return "- " + link
266
- }
267
-
268
- return "- " + link + ": " + strings.Join(kept, ". ")
269
- }
270
-
271
- // firstSentence is the opening of a readme as one line: the first paragraph
272
- // unwrapped, cut at the end of its first sentence. A readme is wrapped at
273
- // some column by whoever wrote it, so the first line alone stops mid-phrase.
274
- func firstSentence(md string) string {
275
- md = strings.TrimSpace(md)
276
- if md == "" {
277
- return ""
278
- }
279
- para := md
280
- if i := strings.Index(md, "\n\n"); i >= 0 {
281
- para = md[:i]
282
- }
283
- if strings.HasPrefix(para, "#") {
284
- return ""
285
- }
286
- para = strings.Join(strings.Fields(para), " ")
287
- for i := 0; i < len(para); i++ {
288
- if para[i] == '.' && (i+1 == len(para) || para[i+1] == ' ') {
289
- return para[:i]
290
- }
291
- }
292
-
293
- return para
294
- }
295
-
296
- func list(items []string) string {
297
- if len(items) == 0 {
298
- return ""
299
- }
300
-
301
- return strings.Join(items, "\n") + "\n"
302
- }
@@ -1,22 +0,0 @@
1
- package main
2
-
3
- import (
4
- "fmt"
5
- "io"
6
- "os"
7
-
8
- "github.com/shortlink-org/portolan/plugin"
9
- )
10
-
11
- func main() {
12
- if err := run(os.Stdin, os.Stdout); err != nil {
13
- fmt.Fprintln(os.Stderr, "portolan-gen-markdown:", err)
14
- os.Exit(1)
15
- }
16
- }
17
-
18
- func run(stdin io.Reader, stdout io.Writer) error {
19
- return plugin.Serve(stdin, stdout, descriptor(), func(req plugin.Request, opts Options) (plugin.Response, error) {
20
- return render(req, opts), nil
21
- })
22
- }
@@ -1,254 +0,0 @@
1
- package main
2
-
3
- import (
4
- "path"
5
- "strings"
6
- )
7
-
8
- // Markdown primitives. Small on purpose: a template engine would put the shape
9
- // of a page somewhere other than the code that decides what goes on it.
10
-
11
- // table renders a GFM table, or nothing at all when there are no rows. An
12
- // empty table with a header is a heading that promises content and delivers a
13
- // horizontal rule.
14
- func table(headers []string, rows [][]string) string {
15
- if len(rows) == 0 {
16
- return ""
17
- }
18
-
19
- // A column nobody filled in is dropped. The same renderer draws a table of
20
- // columns with lineage and a table of columns without any, and a column of
21
- // nine dashes is a question the reader keeps re-asking.
22
- keep := make([]int, 0, len(headers))
23
- for i := range headers {
24
- // The first column identifies the row, so it stays even when empty.
25
- if i == 0 || anyFilled(rows, i) {
26
- keep = append(keep, i)
27
- }
28
- }
29
-
30
- var b strings.Builder
31
- head := make([]string, len(keep))
32
- for i, column := range keep {
33
- head[i] = headers[column]
34
- }
35
- b.WriteString("| " + strings.Join(head, " | ") + " |\n")
36
- b.WriteString("|" + strings.Repeat(" --- |", len(keep)) + "\n")
37
-
38
- for _, row := range rows {
39
- cells := make([]string, len(keep))
40
- for i, column := range keep {
41
- if column < len(row) {
42
- cells[i] = cell(row[column])
43
- } else {
44
- cells[i] = cell("")
45
- }
46
- }
47
- b.WriteString("| " + strings.Join(cells, " | ") + " |\n")
48
- }
49
-
50
- return b.String()
51
- }
52
-
53
- func anyFilled(rows [][]string, column int) bool {
54
- for _, row := range rows {
55
- if column < len(row) && strings.TrimSpace(row[column]) != "" {
56
- return true
57
- }
58
- }
59
-
60
- return false
61
- }
62
-
63
- // cell makes a string safe to sit between two pipes. A doc comment routinely
64
- // contains both a newline and a pipe, and either one silently ends the row.
65
- func cell(s string) string {
66
- s = strings.ReplaceAll(s, "|", "\\|")
67
- s = strings.ReplaceAll(s, "\r\n", "\n")
68
- s = strings.ReplaceAll(s, "\n", " ")
69
- s = strings.TrimSpace(s)
70
- if s == "" {
71
- return "—"
72
- }
73
-
74
- return s
75
- }
76
-
77
- func code(s string) string {
78
- if s == "" {
79
- return ""
80
- }
81
- longest, run := 0, 0
82
- for _, r := range s {
83
- if r == '`' {
84
- run++
85
- if run > longest {
86
- longest = run
87
- }
88
- } else {
89
- run = 0
90
- }
91
- }
92
- marks := strings.Repeat("`", longest+1)
93
- if longest > 0 || strings.HasPrefix(s, " ") || strings.HasSuffix(s, " ") {
94
- return marks + " " + s + " " + marks
95
- }
96
- return marks + s + marks
97
- }
98
-
99
- func fence(lang, body string) string {
100
- return "```" + lang + "\n" + strings.TrimRight(body, "\n") + "\n```\n"
101
- }
102
-
103
- // link is a relative markdown link from one generated page to another. The
104
- // paths are the ones the host will write, so the link is correct wherever the
105
- // docs directory is mounted.
106
- func link(text, from, to string) string {
107
- return "[" + text + "](" + rel(from, to) + ")"
108
- }
109
-
110
- // rel is filepath.Rel for slash paths, between two FILES: the result is
111
- // relative to the directory the first one sits in.
112
- //
113
- // It is written out rather than taken from path/filepath because a plugin
114
- // compiled for wasip1 and one compiled for the host must agree, and
115
- // filepath.Rel agrees with whatever separator it was built for.
116
- func rel(from, to string) string {
117
- fromDir := strings.Split(path.Dir(from), "/")
118
- if path.Dir(from) == "." {
119
- fromDir = nil
120
- }
121
-
122
- toParts := strings.Split(to, "/")
123
-
124
- common := 0
125
- for common < len(fromDir) && common < len(toParts)-1 && fromDir[common] == toParts[common] {
126
- common++
127
- }
128
-
129
- var out []string
130
- for range fromDir[common:] {
131
- out = append(out, "..")
132
- }
133
- out = append(out, toParts[common:]...)
134
-
135
- result := strings.Join(out, "/")
136
- if result == "" {
137
- return path.Base(to)
138
- }
139
-
140
- return result
141
- }
142
-
143
- // shiftHeadings pushes every ATX heading down by `by` levels, leaving fenced
144
- // code alone.
145
- //
146
- // Readmes and ADR bodies are whole documents that start at `#`. Pasting one
147
- // under a generated `#` gives a page with two top-level headings, which every
148
- // table-of-contents generator then reads as two documents.
149
- func shiftHeadings(md string, by int) string {
150
- if md == "" {
151
- return ""
152
- }
153
-
154
- lines := strings.Split(strings.ReplaceAll(md, "\r\n", "\n"), "\n")
155
- fenced := false
156
- for i, line := range lines {
157
- trimmed := strings.TrimSpace(line)
158
- if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
159
- fenced = !fenced
160
-
161
- continue
162
- }
163
- if fenced || !strings.HasPrefix(line, "#") {
164
- continue
165
- }
166
-
167
- level := 0
168
- for level < len(line) && line[level] == '#' {
169
- level++
170
- }
171
- // A run of hashes with no space after it is not a heading.
172
- if level >= len(line) || line[level] != ' ' {
173
- continue
174
- }
175
- // Markdown has no h7. Anything that would go past six stays where it is,
176
- // because a heading rendered as literal hashes is worse than a deep one.
177
- if level+by > 6 {
178
- continue
179
- }
180
-
181
- lines[i] = strings.Repeat("#", by) + line
182
- }
183
-
184
- return strings.Join(lines, "\n")
185
- }
186
-
187
- // section appends a titled block, and nothing at all when the block is empty.
188
- // Every page here is a list of optional sections, so this is what keeps a
189
- // service with no consumers from growing a "Consumes" heading over a blank.
190
- func section(b *strings.Builder, title, body string) {
191
- if strings.TrimSpace(body) == "" {
192
- return
193
- }
194
-
195
- b.WriteString("\n## " + title + "\n\n")
196
- b.WriteString(strings.TrimRight(body, "\n") + "\n")
197
- }
198
-
199
- func subsection(b *strings.Builder, title, body string) {
200
- if strings.TrimSpace(body) == "" {
201
- return
202
- }
203
-
204
- b.WriteString("\n### " + title + "\n\n")
205
- b.WriteString(strings.TrimRight(body, "\n") + "\n")
206
- }
207
-
208
- // defList renders a page's metadata.
209
- //
210
- // It is a bullet list rather than a two-column table because a table needs a
211
- // header row, and the header over an id and a path is two empty cells that
212
- // every renderer draws a box around.
213
- func defList(rows [][]string) string {
214
- var b strings.Builder
215
- for _, row := range rows {
216
- if len(row) < 2 || strings.TrimSpace(row[1]) == "" {
217
- continue
218
- }
219
- b.WriteString("- **" + row[0] + ":** " + strings.TrimSpace(row[1]) + "\n")
220
- }
221
-
222
- return b.String()
223
- }
224
-
225
- // body prepares a readme or an ADR body to sit under a generated `# Title`.
226
- //
227
- // A document that opens with its own title repeating the page's gets that line
228
- // dropped, and everything below it is already at the right depth. One that
229
- // starts lower is pushed down a level so its sections nest under the page
230
- // instead of beside it.
231
- func body(md, title string) string {
232
- md = strings.TrimSpace(md)
233
- if md == "" {
234
- return ""
235
- }
236
-
237
- lines := strings.Split(strings.ReplaceAll(md, "\r\n", "\n"), "\n")
238
- for i, line := range lines {
239
- if strings.TrimSpace(line) == "" {
240
- continue
241
- }
242
- if heading, ok := strings.CutPrefix(line, "# "); ok && sameTitle(heading, title) {
243
- return strings.TrimSpace(strings.Join(lines[i+1:], "\n"))
244
- }
245
-
246
- break
247
- }
248
-
249
- return shiftHeadings(md, 1)
250
- }
251
-
252
- func sameTitle(a, b string) bool {
253
- return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b))
254
- }
@@ -1,100 +0,0 @@
1
- package main
2
-
3
- import (
4
- "strings"
5
- "testing"
6
- )
7
-
8
- func TestRel(t *testing.T) {
9
- cases := []struct{ from, to, want string }{
10
- {"README.md", "types.md", "types.md"},
11
- {"README.md", "shop/oms/README.md", "shop/oms/README.md"},
12
- {"shop/oms/README.md", "shop/oms/aggregates/order.md", "aggregates/order.md"},
13
- {"shop/oms/aggregates/order.md", "types.md", "../../../types.md"},
14
- {"shop/oms/aggregates/order.md", "shop/pricing/README.md", "../../pricing/README.md"},
15
- {"flows/checkout.md", "shop/oms/README.md", "../shop/oms/README.md"},
16
- {"adr/org.0001.md", "adr/org.0002.md", "org.0002.md"},
17
- }
18
-
19
- for _, c := range cases {
20
- if got := rel(c.from, c.to); got != c.want {
21
- t.Errorf("rel(%q, %q) = %q, want %q", c.from, c.to, got, c.want)
22
- }
23
- }
24
- }
25
-
26
- func TestShiftHeadingsLeavesCodeAlone(t *testing.T) {
27
- in := "# Title\n\n```sh\n# not a heading\n```\n\n## Section\n"
28
- want := "## Title\n\n```sh\n# not a heading\n```\n\n### Section\n"
29
-
30
- if got := shiftHeadings(in, 1); got != want {
31
- t.Errorf("shiftHeadings:\n got: %q\nwant: %q", got, want)
32
- }
33
- }
34
-
35
- func TestShiftHeadingsStopsAtSix(t *testing.T) {
36
- if got := shiftHeadings("###### deep", 1); got != "###### deep" {
37
- t.Errorf("a sixth-level heading must stay put, got %q", got)
38
- }
39
- if got := shiftHeadings("#hashtag", 1); got != "#hashtag" {
40
- t.Errorf("a hash with no space is not a heading, got %q", got)
41
- }
42
- }
43
-
44
- // A readme opening with the page's own title would give the page two h1s, so
45
- // that line is dropped and the rest keeps its depth.
46
- func TestBodyDropsRepeatedTitle(t *testing.T) {
47
- got := body("# Basket\n\n## Lifetime\n\nShort.", "Basket")
48
- if got != "## Lifetime\n\nShort." {
49
- t.Errorf("body kept the repeated title: %q", got)
50
- }
51
-
52
- got = body("Just prose.\n\n# Later heading", "Basket")
53
- if got != "Just prose.\n\n## Later heading" {
54
- t.Errorf("body should shift a document that does not open with the title: %q", got)
55
- }
56
- }
57
-
58
- func TestCellSurvivesPipesAndNewlines(t *testing.T) {
59
- if got := cell("a | b\nc"); got != "a \\| b c" {
60
- t.Errorf("cell = %q", got)
61
- }
62
- if got := cell(" "); got != "—" {
63
- t.Errorf("an empty cell should be a dash, got %q", got)
64
- }
65
- }
66
-
67
- func TestMermaidTextEscapesHashBeforeSemicolons(t *testing.T) {
68
- // The escape for # ends in a semicolon, so the order of the two
69
- // replacements is the whole of this function's correctness.
70
- if got := mermaidText("markPaid #1; twice"); got != "markPaid #35;1, twice" {
71
- t.Errorf("mermaidText = %q", got)
72
- }
73
- }
74
-
75
- func TestAnchor(t *testing.T) {
76
- if got := anchor("Gateway Ref"); got != "gateway-ref" {
77
- t.Errorf("anchor = %q", got)
78
- }
79
- }
80
-
81
- func TestCodeUsesALongerFenceAroundBackticks(t *testing.T) {
82
- if got := code("run `gen`"); got != "`` run `gen` ``" {
83
- t.Fatalf("code() = %q", got)
84
- }
85
- }
86
-
87
- func FuzzMarkdownPrimitives(f *testing.F) {
88
- f.Add("a | b\nnext `tick`")
89
- f.Add("обычный текст")
90
- f.Fuzz(func(t *testing.T, value string) {
91
- got := cell(value)
92
- if strings.ContainsAny(got, "\r\n") {
93
- t.Fatalf("cell kept a line break: %q", got)
94
- }
95
- wrapped := code(value)
96
- if value != "" && !strings.HasPrefix(wrapped, "`") {
97
- t.Fatalf("code is not fenced: %q", wrapped)
98
- }
99
- })
100
- }