@shortlink-org/portolan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (826) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +387 -0
  3. package/catalog/enum_test.go +46 -0
  4. package/catalog/model.go +932 -0
  5. package/catalog/roundtrip_test.go +185 -0
  6. package/catalog/via_test.go +38 -0
  7. package/cli/portolan.mjs +368 -0
  8. package/go.mod +14 -0
  9. package/go.sum +14 -0
  10. package/index.html +13 -0
  11. package/internal/commands/cargo.go +154 -0
  12. package/internal/commands/commands.go +143 -0
  13. package/internal/commands/commands_test.go +323 -0
  14. package/internal/commands/gradle.go +77 -0
  15. package/internal/commands/justfile.go +82 -0
  16. package/internal/commands/makefile.go +106 -0
  17. package/internal/commands/maven.go +93 -0
  18. package/internal/commands/packagejson.go +159 -0
  19. package/internal/commands/pyproject.go +169 -0
  20. package/internal/commands/taskfile.go +101 -0
  21. package/internal/commands/testdata/estate/.cargo/config.toml +7 -0
  22. package/internal/commands/testdata/estate/Makefile +33 -0
  23. package/internal/commands/testdata/estate/Taskfile.yml +21 -0
  24. package/internal/commands/testdata/estate/build.gradle.kts +21 -0
  25. package/internal/commands/testdata/estate/justfile +20 -0
  26. package/internal/commands/testdata/estate/package.json +13 -0
  27. package/internal/commands/testdata/estate/pom.xml +29 -0
  28. package/internal/commands/testdata/estate/pyproject.toml +27 -0
  29. package/internal/commands/testdata/estate/xtask/src/main.rs +27 -0
  30. package/internal/commands/testdata/golden/commands.json +305 -0
  31. package/internal/gohttp/analyze.go +2500 -0
  32. package/internal/gohttp/endpoints.go +1067 -0
  33. package/internal/gohttp/roots.go +320 -0
  34. package/internal/gohttp/typed.go +143 -0
  35. package/internal/goscan/constants.go +85 -0
  36. package/internal/goscan/goscan_test.go +227 -0
  37. package/internal/goscan/names.go +52 -0
  38. package/internal/goscan/parse_test.go +11 -0
  39. package/internal/goscan/source.go +36 -0
  40. package/internal/goscan/tree.go +155 -0
  41. package/internal/goscan/types.go +99 -0
  42. package/internal/wsdl/ids.go +127 -0
  43. package/internal/wsdl/ids_test.go +21 -0
  44. package/internal/wsdl/model.go +62 -0
  45. package/internal/wsdl/parse.go +920 -0
  46. package/internal/wsdl/parse_test.go +133 -0
  47. package/package.json +117 -0
  48. package/plugin/describe.go +107 -0
  49. package/plugin/describe_test.go +114 -0
  50. package/plugin/protocol.go +117 -0
  51. package/plugin/schematest/schematest.go +126 -0
  52. package/plugins/README.md +1160 -0
  53. package/plugins/extract-adr/describe.go +19 -0
  54. package/plugins/extract-adr/describe_test.go +11 -0
  55. package/plugins/extract-adr/extract.go +153 -0
  56. package/plugins/extract-adr/extract_test.go +350 -0
  57. package/plugins/extract-adr/history.go +99 -0
  58. package/plugins/extract-adr/main.go +65 -0
  59. package/plugins/extract-adr/options.schema.json +26 -0
  60. package/plugins/extract-adr/parse.go +642 -0
  61. package/plugins/extract-adr/parse_test.go +405 -0
  62. package/plugins/extract-asyncapi/describe.go +19 -0
  63. package/plugins/extract-asyncapi/describe_test.go +11 -0
  64. package/plugins/extract-asyncapi/extract.go +315 -0
  65. package/plugins/extract-asyncapi/extract_test.go +197 -0
  66. package/plugins/extract-asyncapi/main.go +48 -0
  67. package/plugins/extract-asyncapi/options.schema.json +26 -0
  68. package/plugins/extract-asyncapi/spec.go +151 -0
  69. package/plugins/extract-celery/README.md +169 -0
  70. package/plugins/extract-celery/extract.py +346 -0
  71. package/plugins/extract-celery/extract_test.py +192 -0
  72. package/plugins/extract-celery/main.py +66 -0
  73. package/plugins/extract-celery/options.py +31 -0
  74. package/plugins/extract-celery/options.schema.json +31 -0
  75. package/plugins/extract-celery/producers.py +156 -0
  76. package/plugins/extract-celery/routes_test.py +106 -0
  77. package/plugins/extract-commands/describe.go +19 -0
  78. package/plugins/extract-commands/describe_test.go +11 -0
  79. package/plugins/extract-commands/extract.go +79 -0
  80. package/plugins/extract-commands/extract_test.go +92 -0
  81. package/plugins/extract-commands/main.go +55 -0
  82. package/plugins/extract-commands/options.schema.json +20 -0
  83. package/plugins/extract-csr/avro.go +258 -0
  84. package/plugins/extract-csr/describe.go +19 -0
  85. package/plugins/extract-csr/describe_test.go +11 -0
  86. package/plugins/extract-csr/extract.go +338 -0
  87. package/plugins/extract-csr/extract_test.go +374 -0
  88. package/plugins/extract-csr/jsonschema.go +343 -0
  89. package/plugins/extract-csr/lock.go +25 -0
  90. package/plugins/extract-csr/main.go +84 -0
  91. package/plugins/extract-csr/options.schema.json +43 -0
  92. package/plugins/extract-csr/subject.go +102 -0
  93. package/plugins/extract-django/README.md +386 -0
  94. package/plugins/extract-django/apps.py +100 -0
  95. package/plugins/extract-django/auth.py +302 -0
  96. package/plugins/extract-django/auth_test.py +188 -0
  97. package/plugins/extract-django/clients.py +121 -0
  98. package/plugins/extract-django/contracts.py +458 -0
  99. package/plugins/extract-django/database.py +119 -0
  100. package/plugins/extract-django/domain.py +318 -0
  101. package/plugins/extract-django/events.py +86 -0
  102. package/plugins/extract-django/extract.py +601 -0
  103. package/plugins/extract-django/extract_test.py +397 -0
  104. package/plugins/extract-django/flows.py +1278 -0
  105. package/plugins/extract-django/flows_test.py +338 -0
  106. package/plugins/extract-django/ids.py +42 -0
  107. package/plugins/extract-django/ids_test.py +30 -0
  108. package/plugins/extract-django/lifecycle.py +222 -0
  109. package/plugins/extract-django/lifecycle_test.py +76 -0
  110. package/plugins/extract-django/main.py +66 -0
  111. package/plugins/extract-django/openapi.py +229 -0
  112. package/plugins/extract-django/openapi_test.py +60 -0
  113. package/plugins/extract-django/operations.py +96 -0
  114. package/plugins/extract-django/options.py +61 -0
  115. package/plugins/extract-django/options.schema.json +126 -0
  116. package/plugins/extract-django/routing.py +214 -0
  117. package/plugins/extract-django/routing_test.py +198 -0
  118. package/plugins/extract-django/serializers.py +558 -0
  119. package/plugins/extract-django/store.py +434 -0
  120. package/plugins/extract-django/transport.py +242 -0
  121. package/plugins/extract-flows/describe.go +19 -0
  122. package/plugins/extract-flows/describe_test.go +11 -0
  123. package/plugins/extract-flows/extract.go +91 -0
  124. package/plugins/extract-flows/main.go +45 -0
  125. package/plugins/extract-flows/options.schema.json +16 -0
  126. package/plugins/extract-flows/parse.go +593 -0
  127. package/plugins/extract-flows/parse_test.go +204 -0
  128. package/plugins/extract-glossary/describe.go +19 -0
  129. package/plugins/extract-glossary/describe_test.go +11 -0
  130. package/plugins/extract-glossary/extract.go +115 -0
  131. package/plugins/extract-glossary/extract_test.go +220 -0
  132. package/plugins/extract-glossary/main.go +59 -0
  133. package/plugins/extract-glossary/options.schema.json +21 -0
  134. package/plugins/extract-glossary/parse.go +214 -0
  135. package/plugins/extract-glossary/parse_test.go +203 -0
  136. package/plugins/extract-go/aggregate.go +214 -0
  137. package/plugins/extract-go/client.go +409 -0
  138. package/plugins/extract-go/client_test.go +266 -0
  139. package/plugins/extract-go/describe.go +19 -0
  140. package/plugins/extract-go/describe_test.go +11 -0
  141. package/plugins/extract-go/enum.go +195 -0
  142. package/plugins/extract-go/enum_test.go +82 -0
  143. package/plugins/extract-go/event.go +99 -0
  144. package/plugins/extract-go/extract.go +191 -0
  145. package/plugins/extract-go/extract_test.go +261 -0
  146. package/plugins/extract-go/flow.go +1441 -0
  147. package/plugins/extract-go/flow_test.go +609 -0
  148. package/plugins/extract-go/httpclient.go +174 -0
  149. package/plugins/extract-go/httpclient_test.go +296 -0
  150. package/plugins/extract-go/ids.go +92 -0
  151. package/plugins/extract-go/layout.go +225 -0
  152. package/plugins/extract-go/layout_test.go +125 -0
  153. package/plugins/extract-go/lifecycle.go +324 -0
  154. package/plugins/extract-go/lifecycle_test.go +108 -0
  155. package/plugins/extract-go/main.go +79 -0
  156. package/plugins/extract-go/operation.go +157 -0
  157. package/plugins/extract-go/options.schema.json +66 -0
  158. package/plugins/extract-go/source.go +316 -0
  159. package/plugins/extract-go/transport.go +321 -0
  160. package/plugins/extract-go/transport_test.go +145 -0
  161. package/plugins/extract-go/wiring.go +434 -0
  162. package/plugins/extract-go-nats/describe.go +19 -0
  163. package/plugins/extract-go-nats/describe_test.go +11 -0
  164. package/plugins/extract-go-nats/extract.go +177 -0
  165. package/plugins/extract-go-nats/extract_test.go +389 -0
  166. package/plugins/extract-go-nats/index.go +379 -0
  167. package/plugins/extract-go-nats/main.go +42 -0
  168. package/plugins/extract-go-nats/options.schema.json +21 -0
  169. package/plugins/extract-go-nats/resolve.go +161 -0
  170. package/plugins/extract-go-nats/sites.go +224 -0
  171. package/plugins/extract-graphql/describe.go +19 -0
  172. package/plugins/extract-graphql/describe_test.go +11 -0
  173. package/plugins/extract-graphql/extract.go +433 -0
  174. package/plugins/extract-graphql/extract_test.go +256 -0
  175. package/plugins/extract-graphql/ids.go +49 -0
  176. package/plugins/extract-graphql/lex.go +237 -0
  177. package/plugins/extract-graphql/main.go +51 -0
  178. package/plugins/extract-graphql/options.schema.json +31 -0
  179. package/plugins/extract-graphql/parse.go +621 -0
  180. package/plugins/extract-graphql/parse_test.go +122 -0
  181. package/plugins/extract-http-clients/describe.go +19 -0
  182. package/plugins/extract-http-clients/describe_test.go +11 -0
  183. package/plugins/extract-http-clients/extract.go +705 -0
  184. package/plugins/extract-http-clients/extract_test.go +1263 -0
  185. package/plugins/extract-http-clients/main.go +42 -0
  186. package/plugins/extract-http-clients/options.schema.json +33 -0
  187. package/plugins/extract-java/README.md +190 -0
  188. package/plugins/extract-java/build/org/portolan/extract/Catalog.class +0 -0
  189. package/plugins/extract-java/build/org/portolan/extract/Clients$1.class +0 -0
  190. package/plugins/extract-java/build/org/portolan/extract/Clients$2.class +0 -0
  191. package/plugins/extract-java/build/org/portolan/extract/Clients$Call.class +0 -0
  192. package/plugins/extract-java/build/org/portolan/extract/Clients$Client.class +0 -0
  193. package/plugins/extract-java/build/org/portolan/extract/Clients$Route.class +0 -0
  194. package/plugins/extract-java/build/org/portolan/extract/Clients.class +0 -0
  195. package/plugins/extract-java/build/org/portolan/extract/Domain$Aggregate.class +0 -0
  196. package/plugins/extract-java/build/org/portolan/extract/Domain.class +0 -0
  197. package/plugins/extract-java/build/org/portolan/extract/Events$Found.class +0 -0
  198. package/plugins/extract-java/build/org/portolan/extract/Events.class +0 -0
  199. package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
  200. package/plugins/extract-java/build/org/portolan/extract/Flows$1.class +0 -0
  201. package/plugins/extract-java/build/org/portolan/extract/Flows$Binding.class +0 -0
  202. package/plugins/extract-java/build/org/portolan/extract/Flows$Draft.class +0 -0
  203. package/plugins/extract-java/build/org/portolan/extract/Flows$Frame.class +0 -0
  204. package/plugins/extract-java/build/org/portolan/extract/Flows$Options.class +0 -0
  205. package/plugins/extract-java/build/org/portolan/extract/Flows.class +0 -0
  206. package/plugins/extract-java/build/org/portolan/extract/Ids.class +0 -0
  207. package/plugins/extract-java/build/org/portolan/extract/Json$Reader.class +0 -0
  208. package/plugins/extract-java/build/org/portolan/extract/Json.class +0 -0
  209. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$1.class +0 -0
  210. package/plugins/extract-java/build/org/portolan/extract/Lifecycle$Move.class +0 -0
  211. package/plugins/extract-java/build/org/portolan/extract/Lifecycle.class +0 -0
  212. package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
  213. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Operation.class +0 -0
  214. package/plugins/extract-java/build/org/portolan/extract/OpenApi$Spec.class +0 -0
  215. package/plugins/extract-java/build/org/portolan/extract/OpenApi.class +0 -0
  216. package/plugins/extract-java/build/org/portolan/extract/Operations$1.class +0 -0
  217. package/plugins/extract-java/build/org/portolan/extract/Operations$UseCase.class +0 -0
  218. package/plugins/extract-java/build/org/portolan/extract/Operations.class +0 -0
  219. package/plugins/extract-java/build/org/portolan/extract/Proto$Rpc.class +0 -0
  220. package/plugins/extract-java/build/org/portolan/extract/Proto$Service.class +0 -0
  221. package/plugins/extract-java/build/org/portolan/extract/Proto.class +0 -0
  222. package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
  223. package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
  224. package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
  225. package/plugins/extract-java/build/org/portolan/extract/Protocol.class +0 -0
  226. package/plugins/extract-java/build/org/portolan/extract/Source$Project.class +0 -0
  227. package/plugins/extract-java/build/org/portolan/extract/Source$Unit.class +0 -0
  228. package/plugins/extract-java/build/org/portolan/extract/Source.class +0 -0
  229. package/plugins/extract-java/build/org/portolan/extract/Transport$Endpoint.class +0 -0
  230. package/plugins/extract-java/build/org/portolan/extract/Transport.class +0 -0
  231. package/plugins/extract-java/build/org/portolan/extract/Yaml$Line.class +0 -0
  232. package/plugins/extract-java/build/org/portolan/extract/Yaml$Reader.class +0 -0
  233. package/plugins/extract-java/build/org/portolan/extract/Yaml.class +0 -0
  234. package/plugins/extract-java/options.schema.json +70 -0
  235. package/plugins/extract-openapi/describe.go +19 -0
  236. package/plugins/extract-openapi/describe_test.go +11 -0
  237. package/plugins/extract-openapi/discover.go +243 -0
  238. package/plugins/extract-openapi/extract.go +526 -0
  239. package/plugins/extract-openapi/extract_test.go +545 -0
  240. package/plugins/extract-openapi/main.go +75 -0
  241. package/plugins/extract-openapi/options.schema.json +62 -0
  242. package/plugins/extract-openapi/spec.go +350 -0
  243. package/plugins/extract-project/README.md +22 -0
  244. package/plugins/extract-project/describe.go +19 -0
  245. package/plugins/extract-project/describe_test.go +11 -0
  246. package/plugins/extract-project/extract.go +221 -0
  247. package/plugins/extract-project/extract_test.go +109 -0
  248. package/plugins/extract-project/main.go +41 -0
  249. package/plugins/extract-project/options.schema.json +48 -0
  250. package/plugins/extract-proto/ast.go +125 -0
  251. package/plugins/extract-proto/consumes.go +77 -0
  252. package/plugins/extract-proto/describe.go +19 -0
  253. package/plugins/extract-proto/extract.go +293 -0
  254. package/plugins/extract-proto/extract_test.go +459 -0
  255. package/plugins/extract-proto/ids.go +89 -0
  256. package/plugins/extract-proto/ids_test.go +57 -0
  257. package/plugins/extract-proto/lex.go +285 -0
  258. package/plugins/extract-proto/main.go +100 -0
  259. package/plugins/extract-proto/module.go +120 -0
  260. package/plugins/extract-proto/options.schema.json +51 -0
  261. package/plugins/extract-proto/parse.go +720 -0
  262. package/plugins/extract-proto/parse_test.go +307 -0
  263. package/plugins/extract-proto/provides.go +236 -0
  264. package/plugins/extract-proto/resolve.go +222 -0
  265. package/plugins/extract-proto/resolve_test.go +119 -0
  266. package/plugins/extract-python-kafka/README.md +36 -0
  267. package/plugins/extract-python-kafka/extract.py +211 -0
  268. package/plugins/extract-python-kafka/extract_test.py +68 -0
  269. package/plugins/extract-python-kafka/main.py +59 -0
  270. package/plugins/extract-python-kafka/options.py +29 -0
  271. package/plugins/extract-python-kafka/options.schema.json +31 -0
  272. package/plugins/extract-redis/README.md +11 -0
  273. package/plugins/extract-redis/describe.go +19 -0
  274. package/plugins/extract-redis/describe_test.go +11 -0
  275. package/plugins/extract-redis/extract.go +183 -0
  276. package/plugins/extract-redis/extract_test.go +168 -0
  277. package/plugins/extract-redis/keyspaces.go +469 -0
  278. package/plugins/extract-redis/main.go +44 -0
  279. package/plugins/extract-redis/options.schema.json +30 -0
  280. package/plugins/extract-river/describe.go +19 -0
  281. package/plugins/extract-river/describe_test.go +11 -0
  282. package/plugins/extract-river/extract.go +507 -0
  283. package/plugins/extract-river/extract_test.go +132 -0
  284. package/plugins/extract-river/main.go +42 -0
  285. package/plugins/extract-river/options.schema.json +21 -0
  286. package/plugins/extract-rust/Cargo.lock +133 -0
  287. package/plugins/extract-rust/Cargo.toml +23 -0
  288. package/plugins/extract-rust/README.md +204 -0
  289. package/plugins/extract-rust/options.schema.json +65 -0
  290. package/plugins/extract-rust/rustfmt.toml +1 -0
  291. package/plugins/extract-sql/ddl.go +893 -0
  292. package/plugins/extract-sql/ddl_test.go +401 -0
  293. package/plugins/extract-sql/describe.go +19 -0
  294. package/plugins/extract-sql/describe_test.go +11 -0
  295. package/plugins/extract-sql/layout.go +221 -0
  296. package/plugins/extract-sql/layout_test.go +55 -0
  297. package/plugins/extract-sql/lineage.go +107 -0
  298. package/plugins/extract-sql/main.go +141 -0
  299. package/plugins/extract-sql/maps.go +564 -0
  300. package/plugins/extract-sql/maps_java.go +117 -0
  301. package/plugins/extract-sql/maps_rust.go +333 -0
  302. package/plugins/extract-sql/maps_rust_test.go +70 -0
  303. package/plugins/extract-sql/maps_test.go +204 -0
  304. package/plugins/extract-sql/maps_ts.go +398 -0
  305. package/plugins/extract-sql/maps_ts_test.go +136 -0
  306. package/plugins/extract-sql/options.schema.json +53 -0
  307. package/plugins/extract-sql/projection.go +70 -0
  308. package/plugins/extract-sql/projection_test.go +50 -0
  309. package/plugins/extract-sql/store.go +420 -0
  310. package/plugins/extract-sql/store_test.go +233 -0
  311. package/plugins/extract-sql/view.go +295 -0
  312. package/plugins/extract-ts/README.md +306 -0
  313. package/plugins/extract-ts/ast.test.ts +178 -0
  314. package/plugins/extract-ts/ast.ts +648 -0
  315. package/plugins/extract-ts/clients.ts +185 -0
  316. package/plugins/extract-ts/domain.ts +164 -0
  317. package/plugins/extract-ts/extract.test.ts +208 -0
  318. package/plugins/extract-ts/extract.ts +200 -0
  319. package/plugins/extract-ts/flows.ts +783 -0
  320. package/plugins/extract-ts/graphql.test.ts +73 -0
  321. package/plugins/extract-ts/graphql.ts +181 -0
  322. package/plugins/extract-ts/ids.test.ts +21 -0
  323. package/plugins/extract-ts/ids.ts +52 -0
  324. package/plugins/extract-ts/lifecycle.ts +138 -0
  325. package/plugins/extract-ts/main.ts +48 -0
  326. package/plugins/extract-ts/openapi.test.ts +64 -0
  327. package/plugins/extract-ts/openapi.ts +99 -0
  328. package/plugins/extract-ts/operations.ts +93 -0
  329. package/plugins/extract-ts/options.schema.json +96 -0
  330. package/plugins/extract-ts/source.test.ts +174 -0
  331. package/plugins/extract-ts/source.ts +465 -0
  332. package/plugins/extract-ts/transport.ts +147 -0
  333. package/plugins/extract-ts/tsconfig.json +13 -0
  334. package/plugins/extract-ts/wiring.ts +190 -0
  335. package/plugins/extract-watermill/describe.go +19 -0
  336. package/plugins/extract-watermill/describe_test.go +11 -0
  337. package/plugins/extract-watermill/extract.go +1228 -0
  338. package/plugins/extract-watermill/extract_test.go +234 -0
  339. package/plugins/extract-watermill/main.go +41 -0
  340. package/plugins/extract-watermill/options.schema.json +21 -0
  341. package/plugins/extract-wsdl/describe.go +19 -0
  342. package/plugins/extract-wsdl/describe_test.go +11 -0
  343. package/plugins/extract-wsdl/extract.go +148 -0
  344. package/plugins/extract-wsdl/extract_test.go +45 -0
  345. package/plugins/extract-wsdl/main.go +53 -0
  346. package/plugins/extract-wsdl/options.schema.json +59 -0
  347. package/plugins/fetch-bsr/auth.go +114 -0
  348. package/plugins/fetch-bsr/bsr.go +240 -0
  349. package/plugins/fetch-bsr/cache.go +66 -0
  350. package/plugins/fetch-bsr/describe.go +19 -0
  351. package/plugins/fetch-bsr/fetch.go +192 -0
  352. package/plugins/fetch-bsr/fetch_test.go +465 -0
  353. package/plugins/fetch-bsr/lock.go +71 -0
  354. package/plugins/fetch-bsr/main.go +117 -0
  355. package/plugins/fetch-bsr/options.schema.json +42 -0
  356. package/plugins/fetch-csr/auth.go +95 -0
  357. package/plugins/fetch-csr/cache.go +69 -0
  358. package/plugins/fetch-csr/describe.go +19 -0
  359. package/plugins/fetch-csr/describe_test.go +11 -0
  360. package/plugins/fetch-csr/fetch.go +254 -0
  361. package/plugins/fetch-csr/fetch_test.go +494 -0
  362. package/plugins/fetch-csr/lock.go +95 -0
  363. package/plugins/fetch-csr/main.go +122 -0
  364. package/plugins/fetch-csr/options.schema.json +42 -0
  365. package/plugins/fetch-csr/registry.go +209 -0
  366. package/plugins/fetch-git/cache.go +65 -0
  367. package/plugins/fetch-git/describe.go +19 -0
  368. package/plugins/fetch-git/describe_test.go +11 -0
  369. package/plugins/fetch-git/fetch.go +181 -0
  370. package/plugins/fetch-git/fetch_test.go +332 -0
  371. package/plugins/fetch-git/git.go +169 -0
  372. package/plugins/fetch-git/lock.go +72 -0
  373. package/plugins/fetch-git/main.go +127 -0
  374. package/plugins/fetch-git/offline.go +39 -0
  375. package/plugins/fetch-git/options.schema.json +42 -0
  376. package/plugins/fetch-git/pin.go +90 -0
  377. package/plugins/fetch-git/pin_test.go +74 -0
  378. package/plugins/gen-backstage/describe.go +17 -0
  379. package/plugins/gen-backstage/main.go +22 -0
  380. package/plugins/gen-backstage/options.schema.json +30 -0
  381. package/plugins/gen-backstage/plugin.go +473 -0
  382. package/plugins/gen-backstage/plugin_test.go +145 -0
  383. package/plugins/gen-backstage.wasm +0 -0
  384. package/plugins/gen-markdown/adr.go +162 -0
  385. package/plugins/gen-markdown/aggregate.go +362 -0
  386. package/plugins/gen-markdown/canonical.go +93 -0
  387. package/plugins/gen-markdown/context.go +90 -0
  388. package/plugins/gen-markdown/coverage_test.go +89 -0
  389. package/plugins/gen-markdown/describe.go +19 -0
  390. package/plugins/gen-markdown/describe_test.go +11 -0
  391. package/plugins/gen-markdown/external.go +71 -0
  392. package/plugins/gen-markdown/flow.go +277 -0
  393. package/plugins/gen-markdown/glossary.go +76 -0
  394. package/plugins/gen-markdown/glossary_test.go +148 -0
  395. package/plugins/gen-markdown/llms.go +302 -0
  396. package/plugins/gen-markdown/main.go +22 -0
  397. package/plugins/gen-markdown/markdown.go +254 -0
  398. package/plugins/gen-markdown/markdown_test.go +100 -0
  399. package/plugins/gen-markdown/module.go +107 -0
  400. package/plugins/gen-markdown/options.schema.json +16 -0
  401. package/plugins/gen-markdown/plugin.go +39 -0
  402. package/plugins/gen-markdown/quality_test.go +176 -0
  403. package/plugins/gen-markdown/redis_test.go +38 -0
  404. package/plugins/gen-markdown/render.go +344 -0
  405. package/plugins/gen-markdown/render_test.go +186 -0
  406. package/plugins/gen-markdown/service.go +456 -0
  407. package/plugins/gen-markdown/source.go +177 -0
  408. package/plugins/gen-markdown/store.go +201 -0
  409. package/plugins/gen-markdown.wasm +0 -0
  410. package/plugins/gen-mermaid/describe.go +17 -0
  411. package/plugins/gen-mermaid/main.go +22 -0
  412. package/plugins/gen-mermaid/options.schema.json +10 -0
  413. package/plugins/gen-mermaid/plugin.go +104 -0
  414. package/plugins/gen-mermaid/plugin_test.go +33 -0
  415. package/plugins/gen-mermaid.wasm +0 -0
  416. package/plugins/openapi/ids.go +261 -0
  417. package/plugins/openapi/ids_test.go +98 -0
  418. package/plugins/pyplugin/catalog.py +209 -0
  419. package/plugins/pyplugin/celery_conf.py +436 -0
  420. package/plugins/pyplugin/celery_tasks.py +76 -0
  421. package/plugins/pyplugin/kafka.py +570 -0
  422. package/plugins/pyplugin/names.py +38 -0
  423. package/plugins/pyplugin/protocol.py +79 -0
  424. package/plugins/pyplugin/source.py +285 -0
  425. package/plugins/verify-codeowners/describe.go +19 -0
  426. package/plugins/verify-codeowners/describe_test.go +11 -0
  427. package/plugins/verify-codeowners/main.go +75 -0
  428. package/plugins/verify-codeowners/match.go +85 -0
  429. package/plugins/verify-codeowners/match_test.go +47 -0
  430. package/plugins/verify-codeowners/options.schema.json +15 -0
  431. package/plugins/verify-codeowners/owners.go +164 -0
  432. package/plugins/verify-codeowners/owners_test.go +225 -0
  433. package/plugins/verify-codeowners/parse.go +90 -0
  434. package/plugins/verify-codeowners/parse_test.go +62 -0
  435. package/plugins/verify-otel/describe.go +19 -0
  436. package/plugins/verify-otel/describe_test.go +11 -0
  437. package/plugins/verify-otel/main.go +53 -0
  438. package/plugins/verify-otel/match.go +336 -0
  439. package/plugins/verify-otel/options.schema.json +30 -0
  440. package/plugins/verify-otel/otlp.go +200 -0
  441. package/plugins/verify-otel/verify.go +734 -0
  442. package/plugins/verify-otel/verify_test.go +460 -0
  443. package/portolan.json +1073 -0
  444. package/public/favicon.svg +12 -0
  445. package/schema/portolan.schema.json +2558 -0
  446. package/scripts/README.md +53 -0
  447. package/scripts/build-report.mjs +60 -0
  448. package/scripts/build-report.test.mjs +44 -0
  449. package/scripts/builtin-plugins.mjs +49 -0
  450. package/scripts/catalog-sources.mjs +67 -0
  451. package/scripts/diff.mjs +315 -0
  452. package/scripts/diff.test.mjs +151 -0
  453. package/scripts/forge-comment.mjs +144 -0
  454. package/scripts/forge-comment.test.mjs +151 -0
  455. package/scripts/forge-release.mjs +138 -0
  456. package/scripts/forge-release.test.mjs +106 -0
  457. package/scripts/forge.mjs +61 -0
  458. package/scripts/gen-likec4.mjs +854 -0
  459. package/scripts/gen-likec4.test.mjs +258 -0
  460. package/scripts/gen.mjs +603 -0
  461. package/scripts/generated-docs.test.mjs +66 -0
  462. package/scripts/local-api.mjs +1192 -0
  463. package/scripts/local-api.test.mjs +387 -0
  464. package/scripts/manifest.mjs +214 -0
  465. package/scripts/manifest.test.mjs +148 -0
  466. package/scripts/output-path.mjs +79 -0
  467. package/scripts/output-path.test.mjs +45 -0
  468. package/scripts/package-smoke.mjs +37 -0
  469. package/scripts/plugin-host.mjs +419 -0
  470. package/scripts/plugin-host.test.mjs +109 -0
  471. package/scripts/plugin-wasm-worker.mjs +34 -0
  472. package/scripts/run-builtin.mjs +81 -0
  473. package/scripts/schema.mjs +357 -0
  474. package/scripts/site-docs.mjs +295 -0
  475. package/scripts/site-docs.test.mjs +181 -0
  476. package/scripts/vendor-lock.mjs +58 -0
  477. package/scripts/vendor-lock.test.mjs +69 -0
  478. package/src/README.md +38 -0
  479. package/src/app/App.tsx +448 -0
  480. package/src/app/BranchPicker.test.ts +46 -0
  481. package/src/app/BranchPicker.tsx +153 -0
  482. package/src/app/Breadcrumbs.test.ts +38 -0
  483. package/src/app/Breadcrumbs.tsx +223 -0
  484. package/src/app/BuildStamp.tsx +146 -0
  485. package/src/app/CommandPalette.tsx +292 -0
  486. package/src/app/HashScroll.tsx +45 -0
  487. package/src/app/Sidebar.tsx +1742 -0
  488. package/src/app/TopBar.tsx +393 -0
  489. package/src/app/density.tsx +64 -0
  490. package/src/app/forge-access.test.ts +27 -0
  491. package/src/app/forge-access.tsx +80 -0
  492. package/src/app/panels.tsx +141 -0
  493. package/src/app/pins.tsx +242 -0
  494. package/src/app/query-client.ts +24 -0
  495. package/src/app/responsive.ts +55 -0
  496. package/src/app/shortcuts.test.ts +39 -0
  497. package/src/app/shortcuts.tsx +282 -0
  498. package/src/app/theme.tsx +51 -0
  499. package/src/app/title.test.ts +14 -0
  500. package/src/app/title.ts +30 -0
  501. package/src/app/toast.tsx +59 -0
  502. package/src/app/tree-keys.test.ts +80 -0
  503. package/src/app/tree-keys.ts +87 -0
  504. package/src/app/ui-store.ts +52 -0
  505. package/src/catalog-error.test.ts +105 -0
  506. package/src/catalog-profile.test.ts +59 -0
  507. package/src/catalog-profile.ts +101 -0
  508. package/src/catalog-stores.test.ts +431 -0
  509. package/src/catalog.test.ts +1215 -0
  510. package/src/catalog.ts +3125 -0
  511. package/src/chat/Cards.tsx +408 -0
  512. package/src/chat/ChatPanel.tsx +79 -0
  513. package/src/chat/ChatSettings.tsx +219 -0
  514. package/src/chat/Composer.tsx +99 -0
  515. package/src/chat/Conversation.tsx +281 -0
  516. package/src/chat/Header.tsx +46 -0
  517. package/src/chat/Message.tsx +49 -0
  518. package/src/chat/Notice.tsx +195 -0
  519. package/src/chat/Starter.tsx +67 -0
  520. package/src/chat/Steps.tsx +45 -0
  521. package/src/chat/Waiting.tsx +87 -0
  522. package/src/chat/flags.test.ts +59 -0
  523. package/src/chat/flags.ts +155 -0
  524. package/src/chat/linkify.test.ts +30 -0
  525. package/src/chat/linkify.ts +45 -0
  526. package/src/chat/prefs.ts +56 -0
  527. package/src/chat/prompt.test.ts +43 -0
  528. package/src/chat/prompt.ts +157 -0
  529. package/src/chat/store.ts +35 -0
  530. package/src/chat/tools.ts +58 -0
  531. package/src/chat/transport.ts +70 -0
  532. package/src/components/AdrRow.tsx +29 -0
  533. package/src/components/AnchorLink.tsx +67 -0
  534. package/src/components/ApiReference.tsx +134 -0
  535. package/src/components/AsyncApiReference.tsx +120 -0
  536. package/src/components/CatalogStamp.tsx +155 -0
  537. package/src/components/ChannelRows.tsx +132 -0
  538. package/src/components/CommandRows.tsx +139 -0
  539. package/src/components/DiagramSkeleton.tsx +71 -0
  540. package/src/components/EditorLink.tsx +85 -0
  541. package/src/components/FieldTree.tsx +305 -0
  542. package/src/components/FlowTrigger.tsx +34 -0
  543. package/src/components/Ident.tsx +89 -0
  544. package/src/components/Integrations.tsx +243 -0
  545. package/src/components/LifecycleDiagram.tsx +110 -0
  546. package/src/components/MachineDocs.tsx +42 -0
  547. package/src/components/Markdown.tsx +173 -0
  548. package/src/components/MarkdownTable.tsx +206 -0
  549. package/src/components/Mermaid.tsx +211 -0
  550. package/src/components/MethodRows.tsx +309 -0
  551. package/src/components/Overlay.tsx +125 -0
  552. package/src/components/PageHeader.tsx +189 -0
  553. package/src/components/RowActions.tsx +97 -0
  554. package/src/components/SchemaDocument.tsx +89 -0
  555. package/src/components/Select.tsx +126 -0
  556. package/src/components/ShapeRows.tsx +118 -0
  557. package/src/components/SourceDoc.tsx +82 -0
  558. package/src/components/SourcePreview.tsx +441 -0
  559. package/src/components/TabRow.tsx +182 -0
  560. package/src/components/TechIcon.tsx +34 -0
  561. package/src/components/Toc.tsx +132 -0
  562. package/src/components/WhatLinksHere.tsx +273 -0
  563. package/src/components/WsdlReference.tsx +84 -0
  564. package/src/components/ddd-icons.tsx +107 -0
  565. package/src/components/kind.tsx +157 -0
  566. package/src/components/logo.tsx +61 -0
  567. package/src/components/primitives.tsx +264 -0
  568. package/src/data.test.ts +18 -0
  569. package/src/data.ts +202 -0
  570. package/src/enrich.test.ts +1150 -0
  571. package/src/enrich.ts +672 -0
  572. package/src/er/ErCanvas.tsx +533 -0
  573. package/src/er/RedisSchema.tsx +74 -0
  574. package/src/er/RelationCard.tsx +267 -0
  575. package/src/er/StoreHeader.tsx +155 -0
  576. package/src/er/TableNode.tsx +84 -0
  577. package/src/er/ViewNode.tsx +83 -0
  578. package/src/er/layout.test.ts +115 -0
  579. package/src/er/layout.ts +77 -0
  580. package/src/er/lineage.test.ts +140 -0
  581. package/src/er/lineage.ts +107 -0
  582. package/src/er/markers.tsx +79 -0
  583. package/src/er/spec.test.ts +344 -0
  584. package/src/er/spec.ts +426 -0
  585. package/src/flow/ChainList.tsx +162 -0
  586. package/src/flow/FlowTable.tsx +123 -0
  587. package/src/flow/FlowToolbar.tsx +372 -0
  588. package/src/flow/StepDetail.tsx +380 -0
  589. package/src/flow/StepRail.tsx +428 -0
  590. package/src/flow/answers.test.ts +50 -0
  591. package/src/flow/answers.ts +58 -0
  592. package/src/flow/chain.test.ts +383 -0
  593. package/src/flow/chain.ts +353 -0
  594. package/src/flow/chapters.test.ts +185 -0
  595. package/src/flow/chapters.ts +225 -0
  596. package/src/flow/continues.test.ts +102 -0
  597. package/src/flow/continues.ts +65 -0
  598. package/src/flow/cross-context.test.ts +97 -0
  599. package/src/flow/cross-context.ts +35 -0
  600. package/src/flow/mermaid.test.ts +22 -0
  601. package/src/flow/mermaid.ts +141 -0
  602. package/src/flow/outline.test.ts +212 -0
  603. package/src/flow/outline.ts +170 -0
  604. package/src/flow/paths.test.ts +129 -0
  605. package/src/flow/paths.ts +147 -0
  606. package/src/flow/prefs.test.ts +80 -0
  607. package/src/flow/prefs.ts +104 -0
  608. package/src/graph/DependencyGraph.tsx +283 -0
  609. package/src/graph/DependencyNodes.tsx +162 -0
  610. package/src/graph/EdgeLabel.tsx +61 -0
  611. package/src/graph/FocusedEventGraph.tsx +211 -0
  612. package/src/graph/GraphToolbar.tsx +187 -0
  613. package/src/graph/RoutedEdge.tsx +80 -0
  614. package/src/graph/dependency-layout.test.ts +360 -0
  615. package/src/graph/dependency-layout.ts +444 -0
  616. package/src/graph/elk.test.ts +70 -0
  617. package/src/graph/elk.ts +287 -0
  618. package/src/graph/nodes.tsx +97 -0
  619. package/src/graph/theme.ts +81 -0
  620. package/src/graph/useElkFlow.ts +125 -0
  621. package/src/index.css +1547 -0
  622. package/src/language/InLanguage.tsx +40 -0
  623. package/src/language/TermCard.tsx +132 -0
  624. package/src/language/cards.test.ts +114 -0
  625. package/src/language/cards.ts +111 -0
  626. package/src/lib/admonition.test.ts +49 -0
  627. package/src/lib/admonition.ts +48 -0
  628. package/src/lib/adr.test.ts +131 -0
  629. package/src/lib/adr.ts +68 -0
  630. package/src/lib/api.test.ts +122 -0
  631. package/src/lib/api.ts +135 -0
  632. package/src/lib/backlinks.test.ts +330 -0
  633. package/src/lib/backlinks.ts +567 -0
  634. package/src/lib/branch-compare.ts +16 -0
  635. package/src/lib/build-info.test.ts +76 -0
  636. package/src/lib/build-info.ts +80 -0
  637. package/src/lib/catalog-diff.test.ts +275 -0
  638. package/src/lib/catalog-diff.ts +655 -0
  639. package/src/lib/clipboard.ts +43 -0
  640. package/src/lib/comparison-memory.test.ts +40 -0
  641. package/src/lib/comparison-memory.ts +39 -0
  642. package/src/lib/context-color.ts +27 -0
  643. package/src/lib/context-map.test.ts +148 -0
  644. package/src/lib/context-map.ts +399 -0
  645. package/src/lib/data-model.test.ts +245 -0
  646. package/src/lib/data-model.ts +399 -0
  647. package/src/lib/data-problems.test.ts +178 -0
  648. package/src/lib/data-problems.ts +314 -0
  649. package/src/lib/derive.test.ts +251 -0
  650. package/src/lib/derive.ts +515 -0
  651. package/src/lib/editor-link.test.ts +125 -0
  652. package/src/lib/editor-link.ts +96 -0
  653. package/src/lib/editor-prefs.ts +27 -0
  654. package/src/lib/event-graph.test.ts +237 -0
  655. package/src/lib/event-graph.ts +327 -0
  656. package/src/lib/export-canvas.ts +37 -0
  657. package/src/lib/export-file.test.ts +30 -0
  658. package/src/lib/export-file.ts +20 -0
  659. package/src/lib/flow-tree.test.ts +177 -0
  660. package/src/lib/flow-tree.ts +160 -0
  661. package/src/lib/forge-refs.test.ts +50 -0
  662. package/src/lib/forge-refs.ts +60 -0
  663. package/src/lib/format.test.ts +35 -0
  664. package/src/lib/format.ts +64 -0
  665. package/src/lib/github-catalog.test.ts +301 -0
  666. package/src/lib/github-catalog.ts +610 -0
  667. package/src/lib/integrations.test.ts +119 -0
  668. package/src/lib/integrations.ts +133 -0
  669. package/src/lib/kinds.test.ts +91 -0
  670. package/src/lib/kinds.ts +204 -0
  671. package/src/lib/lifecycle.test.ts +93 -0
  672. package/src/lib/lifecycle.ts +165 -0
  673. package/src/lib/local-api.ts +194 -0
  674. package/src/lib/motion-features.ts +11 -0
  675. package/src/lib/motion.test.ts +120 -0
  676. package/src/lib/motion.tsx +269 -0
  677. package/src/lib/palette-recent.test.ts +80 -0
  678. package/src/lib/palette-recent.ts +90 -0
  679. package/src/lib/palette.test.ts +365 -0
  680. package/src/lib/palette.ts +593 -0
  681. package/src/lib/pins.test.ts +139 -0
  682. package/src/lib/pins.ts +130 -0
  683. package/src/lib/problems.test.ts +112 -0
  684. package/src/lib/project-preview.ts +5 -0
  685. package/src/lib/proto-problems.test.ts +121 -0
  686. package/src/lib/proto-problems.ts +65 -0
  687. package/src/lib/queries.test.ts +113 -0
  688. package/src/lib/queries.ts +131 -0
  689. package/src/lib/registry.test.ts +219 -0
  690. package/src/lib/registry.ts +208 -0
  691. package/src/lib/scenarios.ts +547 -0
  692. package/src/lib/setup-info.test.ts +199 -0
  693. package/src/lib/setup-info.ts +415 -0
  694. package/src/lib/shape.test.ts +289 -0
  695. package/src/lib/shape.ts +454 -0
  696. package/src/lib/sidebar-prefs.test.ts +32 -0
  697. package/src/lib/sidebar-prefs.ts +75 -0
  698. package/src/lib/source-code.test.ts +159 -0
  699. package/src/lib/source-code.ts +286 -0
  700. package/src/lib/source-doc.test.ts +131 -0
  701. package/src/lib/source-doc.ts +103 -0
  702. package/src/lib/source-highlight.test.ts +29 -0
  703. package/src/lib/source-highlight.ts +24 -0
  704. package/src/lib/source-link.test.ts +153 -0
  705. package/src/lib/source-link.ts +201 -0
  706. package/src/lib/spec-files.test.ts +24 -0
  707. package/src/lib/spec-files.ts +23 -0
  708. package/src/lib/table-directive.test.ts +139 -0
  709. package/src/lib/table-directive.ts +106 -0
  710. package/src/lib/tech.test.ts +120 -0
  711. package/src/lib/tech.ts +421 -0
  712. package/src/lib/terms.test.ts +67 -0
  713. package/src/lib/terms.ts +179 -0
  714. package/src/lib/wire-problems.test.ts +263 -0
  715. package/src/lib/wire-problems.ts +281 -0
  716. package/src/likec4/C4View.tsx +55 -0
  717. package/src/likec4/CanvasBridge.tsx +187 -0
  718. package/src/likec4/FlowView.tsx +235 -0
  719. package/src/likec4/InteractiveView.tsx +144 -0
  720. package/src/likec4/canvas-viewport.test.ts +149 -0
  721. package/src/likec4/canvas-viewport.ts +135 -0
  722. package/src/likec4/flow-edges.test.ts +51 -0
  723. package/src/likec4/flow-edges.ts +89 -0
  724. package/src/likec4/frame-theme.test.ts +76 -0
  725. package/src/likec4/frame-theme.ts +101 -0
  726. package/src/likec4/highlight-css.test.ts +49 -0
  727. package/src/likec4/highlight-css.ts +52 -0
  728. package/src/likec4/ids.test.ts +151 -0
  729. package/src/likec4/ids.ts +115 -0
  730. package/src/likec4/levels.tsx +70 -0
  731. package/src/likec4/mapping.test.ts +66 -0
  732. package/src/likec4/mapping.ts +54 -0
  733. package/src/likec4/reserved.json +141 -0
  734. package/src/likec4/view-index.ts +78 -0
  735. package/src/main.tsx +13 -0
  736. package/src/map/ContextMapGraph.tsx +274 -0
  737. package/src/map/ContextNode.tsx +70 -0
  738. package/src/map/FloatingEdge.tsx +86 -0
  739. package/src/merge.test.ts +1038 -0
  740. package/src/merge.ts +1004 -0
  741. package/src/pages/AdrDetail.tsx +242 -0
  742. package/src/pages/AdrIndex.tsx +114 -0
  743. package/src/pages/AggregatePage.tsx +727 -0
  744. package/src/pages/BlockPage.tsx +478 -0
  745. package/src/pages/CatalogFailure.tsx +111 -0
  746. package/src/pages/Changes.tsx +313 -0
  747. package/src/pages/ContextMap.tsx +534 -0
  748. package/src/pages/ContextPage.tsx +346 -0
  749. package/src/pages/EnumPage.tsx +282 -0
  750. package/src/pages/EventPage.tsx +686 -0
  751. package/src/pages/ExternalPage.tsx +204 -0
  752. package/src/pages/FlowDetail.tsx +663 -0
  753. package/src/pages/FlowIndex.tsx +303 -0
  754. package/src/pages/GraphPage.tsx +217 -0
  755. package/src/pages/Language.tsx +257 -0
  756. package/src/pages/ModulePage.tsx +366 -0
  757. package/src/pages/NotFound.tsx +54 -0
  758. package/src/pages/Overview.tsx +344 -0
  759. package/src/pages/Problems.tsx +360 -0
  760. package/src/pages/RegistryIndex.tsx +118 -0
  761. package/src/pages/ServicePage.tsx +746 -0
  762. package/src/pages/Settings.tsx +975 -0
  763. package/src/pages/StorePage.tsx +114 -0
  764. package/src/peek/PeekCard.test.ts +35 -0
  765. package/src/peek/PeekCard.tsx +118 -0
  766. package/src/peek/PeekLayer.tsx +147 -0
  767. package/src/peek/model.test.ts +156 -0
  768. package/src/peek/model.ts +253 -0
  769. package/src/routes.test.ts +222 -0
  770. package/src/routes.ts +449 -0
  771. package/src/selection/DetailPanel.tsx +1407 -0
  772. package/src/selection/hash.test.ts +73 -0
  773. package/src/selection/hash.ts +58 -0
  774. package/src/selection/model.test.ts +199 -0
  775. package/src/selection/model.ts +473 -0
  776. package/src/selection/pages.test.ts +115 -0
  777. package/src/selection/pages.ts +268 -0
  778. package/src/selection/store.test.ts +62 -0
  779. package/src/selection/store.ts +46 -0
  780. package/src/selection/sync.tsx +99 -0
  781. package/src/table/ColumnsMenu.tsx +72 -0
  782. package/src/table/DataTable.tsx +780 -0
  783. package/src/table/Facets.tsx +54 -0
  784. package/src/table/GroupMenu.tsx +69 -0
  785. package/src/table/Toolbar.tsx +190 -0
  786. package/src/table/cells.tsx +117 -0
  787. package/src/table/compare.test.ts +117 -0
  788. package/src/table/compare.ts +196 -0
  789. package/src/table/export.test.ts +88 -0
  790. package/src/table/export.ts +66 -0
  791. package/src/table/facet-groups.test.ts +74 -0
  792. package/src/table/facet-groups.ts +56 -0
  793. package/src/table/grouping.test.ts +84 -0
  794. package/src/table/grouping.ts +82 -0
  795. package/src/table/infer.test.ts +57 -0
  796. package/src/table/infer.ts +50 -0
  797. package/src/table/persist.test.ts +76 -0
  798. package/src/table/persist.ts +112 -0
  799. package/src/table/sort-url.test.ts +75 -0
  800. package/src/table/sort-url.ts +52 -0
  801. package/src/table/types.ts +80 -0
  802. package/src/table/useTableState.ts +220 -0
  803. package/src/test-catalog.ts +7 -0
  804. package/src/testing/estate/data__catalog.json +2847 -0
  805. package/src/testing/estate/data__flows.json +1724 -0
  806. package/src/testing/estate/examples__auth__portolan__api.json +212 -0
  807. package/src/testing/estate/examples__auth__portolan__domain.json +1204 -0
  808. package/src/testing/estate/examples__auth__portolan__glossary.json +226 -0
  809. package/src/testing/estate/examples__auth__portolan__observed.json +757 -0
  810. package/src/testing/estate/examples__auth__portolan__stores.json +195 -0
  811. package/src/testing/estate/examples__shop__cart__portolan__api.json +234 -0
  812. package/src/testing/estate/examples__shop__cart__portolan__commands.json +86 -0
  813. package/src/testing/estate/examples__shop__cart__portolan__domain.json +966 -0
  814. package/src/testing/estate/examples__shop__cart__portolan__observed.json +498 -0
  815. package/src/testing/estate/examples__shop__cart__portolan__stores.json +208 -0
  816. package/src/testing/estate/examples__shop__oms__portolan__glossary.json +98 -0
  817. package/src/testing/estate/examples__shop__oms__portolan__proto.json +186 -0
  818. package/src/testing/estate.ts +49 -0
  819. package/src/trail/Trail.tsx +81 -0
  820. package/src/trail/model.test.ts +157 -0
  821. package/src/trail/model.ts +189 -0
  822. package/src/trail/record.tsx +32 -0
  823. package/src/trail/store.test.ts +66 -0
  824. package/src/trail/store.ts +102 -0
  825. package/tsconfig.json +35 -0
  826. package/vite.config.ts +171 -0
@@ -0,0 +1,975 @@
1
+ import { useDocumentTitle } from "../app/title";
2
+ import { createContext, useCallback, useContext, useEffect, useState } from "react";
3
+ import { Link } from "react-router";
4
+ import {
5
+ ArrowLeft,
6
+ Box,
7
+ Check,
8
+ ChevronDown,
9
+ CircleAlert,
10
+ FolderGit2,
11
+ KeyRound,
12
+ LoaderCircle,
13
+ Moon,
14
+ Play,
15
+ Plus,
16
+ Rows2,
17
+ Rows4,
18
+ ShieldCheck,
19
+ Sun,
20
+ Terminal,
21
+ Trash2,
22
+ X,
23
+ } from "lucide-react";
24
+ import { catalog, catalogSources } from "../data";
25
+ import { useDensity } from "../app/density";
26
+ import { useEditorStore } from "../components/EditorLink";
27
+ import { Select } from "../components/Select";
28
+ import { EDITORS } from "../lib/editor-link";
29
+ import { parseEditor } from "../lib/editor-prefs";
30
+ import { useTheme } from "../app/theme";
31
+ import { useToastStore } from "../app/toast";
32
+ import { absoluteTime, plural, relativeTime } from "../lib/format";
33
+ import { setupInfo as staticSetupInfo } from "../lib/setup-info";
34
+ import type {
35
+ SetupInfo,
36
+ SetupPhase,
37
+ SetupPlugin,
38
+ SetupProject,
39
+ SetupRunStep,
40
+ SetupRunStepStatus,
41
+ } from "../lib/setup-info";
42
+ import {
43
+ applyProjectTrial,
44
+ cancelGeneration,
45
+ discover,
46
+ disposeProjectTrial,
47
+ forgetRepositoryCredential,
48
+ inspectRepository,
49
+ LocalApiError,
50
+ saveRepositoryCredential,
51
+ startGeneration,
52
+ startProjectTrial,
53
+ subscribeToRun,
54
+ } from "../lib/local-api";
55
+ import type { Discovery, ProjectDraft, ProjectPlan, RunEvent } from "../lib/local-api";
56
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
57
+ import { localKeys, localStatusQuery } from "../lib/queries";
58
+ import { sourceHref, treeHref } from "../lib/source-link";
59
+ import { paths } from "../routes";
60
+ import { Empty, SectionTitle } from "../components/PageHeader";
61
+ import { Modal } from "../components/Overlay";
62
+ import { MachineDocs } from "../components/MachineDocs";
63
+ import { ChatSection } from "../chat/ChatSettings";
64
+ import { BUILD as CHAT_BUILD } from "../chat/flags";
65
+
66
+ type Health = "healthy" | "changed" | "failed" | "unchecked";
67
+
68
+ const SetupContext = createContext<SetupInfo>(staticSetupInfo);
69
+ const useSetup = () => useContext(SetupContext);
70
+
71
+ function Metric({ value, label }: { value: number; label: string }) {
72
+ return (
73
+ <div className="min-w-0 rounded-card border border-line bg-canvas px-3 py-2 shadow-xs">
74
+ <span className="tnum text-lg font-semibold text-ink">{value}</span>
75
+ <span className="mono ml-2 text-muted">{plural(value, label)}</span>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function duration(milliseconds: number): string {
81
+ if (milliseconds < 1000) return `${Math.round(milliseconds)} ms`;
82
+ if (milliseconds < 60_000) return `${(milliseconds / 1000).toFixed(1)} s`;
83
+ return `${Math.floor(milliseconds / 60_000)}m ${Math.round((milliseconds % 60_000) / 1000)}s`;
84
+ }
85
+
86
+ function healthFor(steps: SetupRunStep[], expected: number, setupInfo: SetupInfo): Health {
87
+ if (
88
+ expected === 0 ||
89
+ setupInfo.reportStale ||
90
+ !setupInfo.run ||
91
+ setupInfo.run.status === "running" ||
92
+ steps.length < expected
93
+ ) {
94
+ return "unchecked";
95
+ }
96
+ if (steps.some((step) => step.status === "failed")) return "failed";
97
+ if (steps.some((step) => step.status === "drifted")) return "changed";
98
+ return "healthy";
99
+ }
100
+
101
+ const HEALTH_LABEL: Record<Health, string> = {
102
+ healthy: "healthy",
103
+ changed: "out of date",
104
+ failed: "failed",
105
+ unchecked: "not checked",
106
+ };
107
+
108
+ function HealthBadge({ health }: { health: Health }) {
109
+ const style =
110
+ health === "healthy"
111
+ ? "status-verified"
112
+ : health === "unchecked"
113
+ ? "text-muted"
114
+ : health === "changed"
115
+ ? "status-declared"
116
+ : "status-unresolved";
117
+ return <span className={`chip ${style}`}>{HEALTH_LABEL[health]}</span>;
118
+ }
119
+
120
+ function StepStatus({ status }: { status: SetupRunStepStatus }) {
121
+ const health: Health =
122
+ status === "failed"
123
+ ? "failed"
124
+ : status === "drifted"
125
+ ? "changed"
126
+ : "healthy";
127
+ return (
128
+ <span className={`chip ${health === "healthy" ? "status-verified" : health === "changed" ? "status-declared" : "status-unresolved"}`}>
129
+ {status}
130
+ </span>
131
+ );
132
+ }
133
+
134
+ function BuildHealth() {
135
+ const setupInfo = useSetup();
136
+ const run = setupInfo.run;
137
+ const stale = setupInfo.reportStale;
138
+ const health: Health =
139
+ stale || !run || run.status === "running"
140
+ ? "unchecked"
141
+ : run.status === "failed"
142
+ ? "failed"
143
+ : run.steps.length !== setupInfo.steps.length
144
+ ? "unchecked"
145
+ : run.status === "drifted"
146
+ ? "changed"
147
+ : "healthy";
148
+ const finished = run?.finishedAt || run?.startedAt || "";
149
+ const completed = run?.steps.filter((step) => step.status !== "failed").length ?? 0;
150
+
151
+ return (
152
+ <section className="mt-4 rounded-card border border-line bg-surface px-4 py-3 shadow-xs">
153
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center">
154
+ <div className="flex min-w-0 flex-1 items-start gap-3">
155
+ {health === "failed" ? (
156
+ <CircleAlert size={18} aria-hidden className="mt-0.5 shrink-0 text-unresolved" />
157
+ ) : (
158
+ <Check
159
+ size={18}
160
+ aria-hidden
161
+ className={`mt-0.5 shrink-0 ${health === "healthy" ? "text-verified" : "text-muted"}`}
162
+ />
163
+ )}
164
+ <div className="min-w-0">
165
+ <div className="flex flex-wrap items-center gap-2">
166
+ <span className="font-semibold text-ink">Build health</span>
167
+ <HealthBadge health={health} />
168
+ </div>
169
+ <p className="mt-0.5 text-muted">
170
+ {stale
171
+ ? "Configuration changed since the last generator run."
172
+ : !run
173
+ ? "No local generator result is available for this build."
174
+ : health === "unchecked"
175
+ ? "The recorded generator run did not complete every declared step."
176
+ : health === "healthy"
177
+ ? "Every recorded pipeline step completed and generated output is current."
178
+ : health === "changed"
179
+ ? "The check found generated output that needs to be refreshed."
180
+ : "The generator stopped before the pipeline completed."}
181
+ </p>
182
+ </div>
183
+ </div>
184
+ {/* Three readings of one run, set as a strip rather than a pair of
185
+ right-aligned columns: the labels are short and the values are not,
186
+ and ranging both right left a ragged channel down the middle that
187
+ read as a mistake. Label over value, ranged left, is how the metrics
188
+ under this block are already set. */}
189
+ {run ? (
190
+ <dl className="flex shrink-0 gap-x-6 gap-y-2 max-sm:flex-wrap">
191
+ <div>
192
+ <dt className="label">steps</dt>
193
+ <dd className="mono tnum mt-0.5 text-ink">{completed}/{setupInfo.steps.length}</dd>
194
+ </div>
195
+ <div>
196
+ <dt className="label">run</dt>
197
+ <dd className="mono mt-0.5 text-ink">{run.mode} · {duration(run.durationMs)}</dd>
198
+ </div>
199
+ {finished ? (
200
+ <div>
201
+ <dt className="label">finished</dt>
202
+ <dd className="mono mt-0.5 text-ink" title={absoluteTime(finished)}>{relativeTime(finished)}</dd>
203
+ </div>
204
+ ) : null}
205
+ </dl>
206
+ ) : (
207
+ <code className="mono shrink-0 rounded-control border border-line bg-canvas px-2 py-1 text-ink">
208
+ npm run gen:check
209
+ </code>
210
+ )}
211
+ </div>
212
+ </section>
213
+ );
214
+ }
215
+
216
+ function projectHref(project: SetupProject): string | null {
217
+ const group = project.group ?? project.context;
218
+ const component = project.component ?? project.service;
219
+ const context = catalog.contexts.find((item) => item.id === group);
220
+ if (!context) return null;
221
+ const service = context.services.find(
222
+ (item) =>
223
+ item.slug === component ||
224
+ item.id === component ||
225
+ item.id === `${context.id}.${component}`,
226
+ );
227
+ return service ? paths.service(context.id, service.slug) : paths.context(context.id);
228
+ }
229
+
230
+ function forge(project: SetupProject): { href: string; title: string } | null {
231
+ if (project.repository) {
232
+ return {
233
+ href: treeHref(project.root, { repo: project.repository }, catalog.repos) ?? project.repository,
234
+ title: "Open the project's source at the fetched commit",
235
+ };
236
+ }
237
+ const tree = treeHref(project.root, null);
238
+ return tree
239
+ ? { href: tree, title: "Open the project's directory at the built commit" }
240
+ : null;
241
+ }
242
+
243
+ function FileLink({ path }: { path: string }) {
244
+ const href = sourceHref(path, null);
245
+ return href ? (
246
+ <a href={href} target="_blank" rel="noreferrer" className="rounded-control text-accent hover:underline">
247
+ {path} ↗
248
+ </a>
249
+ ) : (
250
+ <span title={path}>{path}</span>
251
+ );
252
+ }
253
+
254
+ function PipelineSteps({ steps }: { steps: SetupRunStep[] }) {
255
+ if (steps.length === 0) {
256
+ return <p className="mono text-muted">No result recorded for these steps.</p>;
257
+ }
258
+ return (
259
+ <div className="divide-y divide-line rounded-control border border-line">
260
+ {steps.map((step) => (
261
+ <div key={step.ordinal} className="px-3 py-2">
262
+ <div className="grid gap-2 sm:grid-cols-[5rem_minmax(7rem,1fr)_auto_auto] sm:items-center">
263
+ <span className="mono text-muted">{step.phase}</span>
264
+ <span className="mono truncate text-ink" title={step.plugin}>{step.plugin}</span>
265
+ <span className="mono text-muted">{step.fileCount} {plural(step.fileCount, "file")}</span>
266
+ <div className="flex items-center justify-between gap-2 sm:justify-end">
267
+ <span className="mono text-muted">{duration(step.durationMs)}</span>
268
+ <StepStatus status={step.status} />
269
+ </div>
270
+ </div>
271
+ <StepWarnings warnings={step.warnings} />
272
+ </div>
273
+ ))}
274
+ </div>
275
+ );
276
+ }
277
+
278
+ // What a plugin could not read, in its own words. For a tree the extractor
279
+ // only half understood this is the list of what to fix, so it is shown where
280
+ // the step is rather than left on the terminal it scrolled off.
281
+ function StepWarnings({ warnings }: { warnings: string[] }) {
282
+ if (warnings.length === 0) return null;
283
+ return (
284
+ <details className="mt-2" open={warnings.length <= 5}>
285
+ <summary className="mono cursor-pointer text-muted">
286
+ {warnings.length} {plural(warnings.length, "warning")}
287
+ </summary>
288
+ <ul className="mono mt-1 space-y-1 text-muted">
289
+ {warnings.map((warning, index) => (
290
+ <li key={`${index}:${warning}`} className="break-words border-l-2 border-line pl-2">
291
+ {warning}
292
+ </li>
293
+ ))}
294
+ </ul>
295
+ </details>
296
+ );
297
+ }
298
+
299
+ function ProjectCard({ project }: { project: SetupProject }) {
300
+ const setupInfo = useSetup();
301
+ const declared = setupInfo.steps.filter((step) => step.projectId === project.id);
302
+ const runSteps = setupInfo.run?.steps.filter((step) => step.projectId === project.id) ?? [];
303
+ const pluginNames = [...new Set(declared.map((step) => step.plugin))];
304
+ const sources = catalogSources.filter(
305
+ (source) => source.path === project.root || source.path.startsWith(`${project.root}/`),
306
+ );
307
+ const outputs = [...new Set(runSteps.flatMap((step) => step.files))];
308
+ const commits = [...new Set(sources.map((source) => source.commit).filter(Boolean))];
309
+ const href = projectHref(project);
310
+ const sourceLink = forge(project);
311
+ const health = healthFor(runSteps, declared.length, setupInfo);
312
+ const title = <span className="font-semibold text-ink">{project.name}</span>;
313
+
314
+ return (
315
+ <article id={`project-${project.id}`} className="scroll-mt-4 rounded-card border border-line bg-canvas p-card shadow-xs">
316
+ <div className="flex items-start gap-3">
317
+ <FolderGit2 size={18} aria-hidden className="mt-0.5 shrink-0 text-muted" />
318
+ <div className="min-w-0 flex-1">
319
+ <div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
320
+ {href ? <Link to={href} className="rounded-control hover:underline">{title}</Link> : title}
321
+ <HealthBadge health={health} />
322
+ </div>
323
+ <div className="mono mt-0.5 flex items-center gap-2 text-muted">
324
+ <span className="truncate" title={project.root}>{project.root}</span>
325
+ {sourceLink ? (
326
+ <a href={sourceLink.href} target="_blank" rel="noreferrer" className="shrink-0 rounded-control text-accent hover:underline" title={sourceLink.title}>
327
+ source ↗
328
+ </a>
329
+ ) : null}
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <dl className="mono mt-4 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1 text-muted">
335
+ <dt>scope</dt><dd className="truncate text-ink">{[project.group ?? project.context, project.component ?? project.service].filter(Boolean).join(" · ") || "estate"}</dd>
336
+ <dt>pipeline</dt><dd className="text-ink">{declared.length} {plural(declared.length, "step")}</dd>
337
+ <dt>fragments</dt><dd className="text-ink">{sources.length}</dd>
338
+ <dt>commit</dt><dd className="truncate text-ink" title={commits.join(", ")}>{commits.length === 0 ? "not stamped" : commits.length === 1 ? commits[0] : `${commits.length} source commits`}</dd>
339
+ </dl>
340
+
341
+ <div className="mt-4 flex flex-wrap gap-1.5" aria-label="Active plugins">
342
+ {pluginNames.map((name) => <a key={name} href={`#plugin-${name}`} className="chip border-line-strong hover:border-accent hover:text-accent">{name}</a>)}
343
+ </div>
344
+
345
+ <details className="group mt-4 border-t border-line pt-3">
346
+ {/* Clickable, so it answers the pointer. The plugin rows below tint
347
+ their whole row; a disclosure inside a card has no edges to tint, so
348
+ it says the same thing in the colour a link uses. */}
349
+ <summary className="flex cursor-pointer list-none items-center justify-between gap-2 rounded-control font-medium text-ink transition-colors group-hover:text-accent">
350
+ Pipeline and sources
351
+ <ChevronDown size={16} aria-hidden className="shrink-0 text-muted transition-transform group-hover:text-accent group-open:rotate-180" />
352
+ </summary>
353
+ <div className="mt-3 space-y-4">
354
+ <PipelineSteps steps={runSteps} />
355
+ <div>
356
+ <div className="label mb-2">catalog sources</div>
357
+ {sources.length > 0 ? <ul className="mono space-y-1 text-muted">{sources.map((source) => <li key={source.path} className="truncate"><FileLink path={source.path} /></li>)}</ul> : <p className="mono text-muted">No catalog fragments found under this root.</p>}
358
+ </div>
359
+ {outputs.length > 0 ? (
360
+ <div>
361
+ <div className="label mb-2">generated outputs</div>
362
+ <ul className="mono space-y-1 text-muted">{outputs.map((output) => <li key={output} className="truncate"><FileLink path={output} /></li>)}</ul>
363
+ </div>
364
+ ) : null}
365
+ </div>
366
+ </details>
367
+ </article>
368
+ );
369
+ }
370
+
371
+ const PHASE_LABEL: Record<SetupPhase, string> = { extract: "extract", verify: "verify", generate: "generate" };
372
+
373
+ /* Fifteen of sixteen plugins run as a host process, so spelling it out on
374
+ every row was fifteen repetitions of the word to make the one that differs
375
+ findable. The icon carries it on the row and the legend under the list says
376
+ what the two mean; the name stays for the reader who opens a row. */
377
+ function Runtime({ plugin, icon = false }: { plugin: SetupPlugin; icon?: boolean }) {
378
+ const [Icon, tone, name] =
379
+ plugin.runtime === "wasm"
380
+ ? ([ShieldCheck, "text-verified", "WASM sandbox"] as const)
381
+ : ([Terminal, "text-declared", "host process"] as const);
382
+ return icon ? (
383
+ <span className={tone} title={name}>
384
+ <Icon size={14} aria-hidden />
385
+ <span className="sr-only">{name}</span>
386
+ </span>
387
+ ) : (
388
+ <span className={`inline-flex items-center gap-1.5 ${tone}`}><Icon size={14} aria-hidden /> {name}</span>
389
+ );
390
+ }
391
+
392
+ /* Health is the same word on nearly every row too. A dot states it without
393
+ spending a column on it, and the badge comes back the moment it is not
394
+ "healthy" - which is the only time anyone is reading this column. */
395
+ function HealthDot({ health }: { health: Health }) {
396
+ const tone =
397
+ health === "healthy"
398
+ ? "bg-verified"
399
+ : health === "changed"
400
+ ? "bg-declared"
401
+ : health === "failed"
402
+ ? "bg-unresolved"
403
+ : "bg-line-strong";
404
+ return (
405
+ <span className={`size-1.5 shrink-0 rounded-full ${tone}`} title={HEALTH_LABEL[health]}>
406
+ <span className="sr-only">{HEALTH_LABEL[health]}</span>
407
+ </span>
408
+ );
409
+ }
410
+
411
+ const PHASE_ORDER: SetupPhase[] = ["extract", "verify", "generate"];
412
+
413
+ /**
414
+ * The sixteen plugins, grouped by the phase they run in.
415
+ *
416
+ * It was a five-column table, and four of the columns said the same thing on
417
+ * nearly every row: the phase (thirteen say "extract"), the runtime (fifteen
418
+ * say "host process"), the status (sixteen say "healthy"). A table is for
419
+ * columns that differ. The phase became the group it sorts into, the runtime
420
+ * and the status became a mark, and what is left on the row is the name and
421
+ * the one number that varies. The detail every row could open is unchanged.
422
+ */
423
+ function PluginsList() {
424
+ const setupInfo = useSetup();
425
+ if (setupInfo.plugins.length === 0) return <Empty>this build ran no plugins</Empty>;
426
+ const projectNames = new Map(setupInfo.projects.map((project) => [project.id, project.name]));
427
+ const groups = [
428
+ ...PHASE_ORDER.map((phase) => ({
429
+ key: phase as string,
430
+ label: PHASE_LABEL[phase],
431
+ plugins: setupInfo.plugins.filter((plugin) => plugin.phases[0] === phase),
432
+ })),
433
+ // A plugin the manifest declares and no step uses. There are none today,
434
+ // and the row that says so is the only place anyone would find out.
435
+ { key: "unused", label: "declared, unused", plugins: setupInfo.plugins.filter((plugin) => plugin.phases.length === 0) },
436
+ ].filter((group) => group.plugins.length > 0);
437
+
438
+ return (
439
+ <div className="overflow-hidden rounded-card border border-line shadow-xs">
440
+ {groups.map((group) => (
441
+ <section key={group.key} className="border-t border-line first:border-t-0">
442
+ <h3 className="label flex items-center gap-2 bg-surface px-3 py-1.5">
443
+ {group.label}
444
+ <span className="text-muted/70">{group.plugins.length}</span>
445
+ </h3>
446
+ {group.plugins.map((plugin) => {
447
+ const declared = setupInfo.steps.filter((step) => step.plugin === plugin.name);
448
+ const runSteps = setupInfo.run?.steps.filter((step) => step.plugin === plugin.name) ?? [];
449
+ const health = plugin.stepCount === 0 ? "unchecked" : healthFor(runSteps, declared.length, setupInfo);
450
+ const outputs = [...new Set(runSteps.flatMap((step) => step.files))];
451
+ return (
452
+ <details key={plugin.name} id={`plugin-${plugin.name}`} className="group scroll-mt-4 border-t border-line">
453
+ <summary className="flex cursor-pointer list-none items-center gap-2.5 px-3 py-1.5 hover:bg-surface/60">
454
+ {health === "healthy" ? <HealthDot health={health} /> : null}
455
+ <span className="mono truncate text-ink" title={plugin.name}>{plugin.name}</span>
456
+ <Runtime plugin={plugin} icon />
457
+ {health === "healthy" ? null : <HealthBadge health={health} />}
458
+ <span className="mono ml-auto shrink-0 text-muted">
459
+ {plugin.projectIds.length > 0
460
+ ? `${plugin.projectIds.length} ${plural(plugin.projectIds.length, "project")}`
461
+ : plugin.stepCount > 0
462
+ ? "estate"
463
+ : "—"}
464
+ </span>
465
+ <ChevronDown size={15} aria-hidden className="shrink-0 text-muted transition-transform group-open:rotate-180" />
466
+ </summary>
467
+ <div className="border-t border-line bg-surface/50 px-4 py-4">
468
+ <div className="grid gap-5 lg:grid-cols-[minmax(0,1.4fr)_minmax(14rem,1fr)]">
469
+ <div><div className="label mb-2">last run</div><PipelineSteps steps={runSteps} /></div>
470
+ <div className="space-y-4">
471
+ <div><div className="label mb-2">runtime</div><p className="mono"><Runtime plugin={plugin} /></p></div>
472
+ <div><div className="label mb-2">used by</div>{plugin.projectIds.length > 0 ? <div className="flex flex-wrap gap-1.5">{plugin.projectIds.map((id) => <a key={id} href={`#project-${id}`} className="chip border-line-strong hover:border-accent hover:text-accent">{projectNames.get(id) ?? id}</a>)}</div> : <p className="mono text-muted">{plugin.stepCount > 0 ? "Estate-wide catalog" : "No pipeline step uses this plugin."}</p>}</div>
473
+ {outputs.length > 0 ? <div><div className="label mb-2">outputs</div><ul className="mono space-y-1 text-muted">{outputs.map((output) => <li key={output} className="truncate"><FileLink path={output} /></li>)}</ul></div> : null}
474
+ </div>
475
+ </div>
476
+ </div>
477
+ </details>
478
+ );
479
+ })}
480
+ </section>
481
+ ))}
482
+ </div>
483
+ );
484
+ }
485
+
486
+ /**
487
+ * Which editor the "edit ↗" links hand a file to. The links exist only in
488
+ * local mode; the choice is offered everywhere, because a reader sets it once
489
+ * and the static build they open tomorrow is the same browser.
490
+ */
491
+ function EditorChoice() {
492
+ const editor = useEditorStore((s) => s.editor);
493
+ const setEditor = useEditorStore((s) => s.set);
494
+ const options = EDITORS.map((e) => ({ value: e.id, label: e.name }));
495
+ return (
496
+ <div className="rounded-card border border-line p-card shadow-xs">
497
+ <div className="label mb-3">open source in</div>
498
+ <Select
499
+ value={editor}
500
+ options={options}
501
+ onChange={(value) => setEditor(parseEditor(value))}
502
+ label="Editor for source links"
503
+ menuWidth={200}
504
+ />
505
+ <p className="mono mt-2 text-muted">Used by the edit links in local mode.</p>
506
+ </div>
507
+ );
508
+ }
509
+
510
+ function Appearance() {
511
+ const { theme, toggle: toggleTheme } = useTheme();
512
+ const { density, toggle: toggleDensity } = useDensity();
513
+ return (
514
+ <div className="grid gap-grid sm:grid-cols-2 xl:grid-cols-3">
515
+ <div className="rounded-card border border-line p-card shadow-xs">
516
+ <div className="label mb-3">theme</div>
517
+ <div className="seg inline-flex" role="group" aria-label="Theme">
518
+ <button type="button" aria-pressed={theme === "dark"} onClick={() => theme !== "dark" && toggleTheme()} className={`flex items-center gap-1.5 ${theme === "dark" ? "is-on" : ""}`}><Moon size={15} aria-hidden /> dark</button>
519
+ <button type="button" aria-pressed={theme === "light"} onClick={() => theme !== "light" && toggleTheme()} className={`flex items-center gap-1.5 ${theme === "light" ? "is-on" : ""}`}><Sun size={15} aria-hidden /> light</button>
520
+ </div>
521
+ </div>
522
+ <div className="rounded-card border border-line p-card shadow-xs">
523
+ <div className="label mb-3">row density</div>
524
+ <div className="seg inline-flex" role="group" aria-label="Row density">
525
+ <button type="button" aria-pressed={density === "comfortable"} onClick={() => density !== "comfortable" && toggleDensity()} className={`flex items-center gap-1.5 ${density === "comfortable" ? "is-on" : ""}`}><Rows4 size={15} aria-hidden /> comfortable</button>
526
+ <button type="button" aria-pressed={density === "compact"} onClick={() => density !== "compact" && toggleDensity()} className={`flex items-center gap-1.5 ${density === "compact" ? "is-on" : ""}`}><Rows2 size={15} aria-hidden /> compact</button>
527
+ </div>
528
+ </div>
529
+ <EditorChoice />
530
+ </div>
531
+ );
532
+ }
533
+
534
+ const FIELD = "mono w-full rounded-control border border-line bg-canvas px-3 py-2 text-ink outline-none focus:border-accent";
535
+
536
+ function Field({ label, value, onChange, placeholder, required = false }: { label: string; value: string; onChange: (value: string) => void; placeholder?: string; required?: boolean }) {
537
+ return (
538
+ <label className="block">
539
+ <span className="label mb-1.5 block">{label}</span>
540
+ <input className={FIELD} value={value} onChange={(event) => onChange(event.target.value)} placeholder={placeholder} required={required} />
541
+ </label>
542
+ );
543
+ }
544
+
545
+ function AddProjectCard({ onClick }: { onClick: () => void }) {
546
+ return (
547
+ <button type="button" onClick={onClick} className="group flex min-h-52 items-center justify-center rounded-card border border-dashed border-line-strong bg-canvas p-card text-left shadow-xs transition-colors hover:border-accent hover:bg-surface">
548
+ <span className="flex max-w-72 flex-col items-center text-center">
549
+ <span className="flex size-9 items-center justify-center rounded-full border border-line-strong text-muted group-hover:border-accent group-hover:text-accent"><Plus size={18} aria-hidden /></span>
550
+ <span className="mt-3 font-semibold text-ink">Add a project</span>
551
+ <span className="mt-1 text-muted">Point Portolan at a local component or repository and it will suggest the extractors to use.</span>
552
+ </span>
553
+ </button>
554
+ );
555
+ }
556
+
557
+ const CAPABILITIES: Record<string, { title: string; summary: string }> = {
558
+ project: { title: "Component metadata", summary: "Name, ownership, repository and runnable commands." },
559
+ "go-domain": { title: "Go domain model", summary: "Aggregates, entities, value objects and domain events." },
560
+ "ts-domain": { title: "TypeScript domain model", summary: "Aggregates, entities, value objects and domain events." },
561
+ "rust-domain": { title: "Rust domain model", summary: "Aggregates, entities, value objects and domain events." },
562
+ "java-domain": { title: "Java domain model", summary: "Aggregates, entities, value objects and domain events." },
563
+ "django-domain": { title: "Django data model", summary: "Models and their relationships." },
564
+ openapi: { title: "HTTP API contract", summary: "Operations and messages declared by OpenAPI or Swagger." },
565
+ wsdl: { title: "SOAP contract", summary: "Services, operations and messages declared by WSDL." },
566
+ "http-clients": { title: "Outbound integrations", summary: "HTTP and SOAP calls, provider branches and code flows." },
567
+ redis: { title: "Redis data model", summary: "Clients, key patterns and stored value hints." },
568
+ river: { title: "River jobs", summary: "Job producers, workers and their code flows." },
569
+ watermill: { title: "Watermill messaging", summary: "Publishers, handlers, topics and their code flows." },
570
+ asyncapi: { title: "Messaging contract", summary: "Channels and messages declared by AsyncAPI." },
571
+ celery: { title: "Celery jobs", summary: "Tasks, producers and worker flows." },
572
+ graphql: { title: "GraphQL contract", summary: "Queries, mutations and schema types." },
573
+ proto: { title: "Protobuf contract", summary: "gRPC services, methods and messages." },
574
+ sql: { title: "SQL data model", summary: "Stores, tables, columns and relationships." },
575
+ adr: { title: "Architecture decisions", summary: "ADRs and their catalog relationships." },
576
+ glossary: { title: "Glossary", summary: "Project language and shared definitions." },
577
+ };
578
+
579
+ function RepositoryFailure({ failure, message, token, onTokenChange, onForget, busy }: { failure: LocalApiError; message: string; token: string; onTokenChange: (value: string) => void; onForget: () => void; busy: boolean }) {
580
+ const authFailure = failure.code === "repository_auth_required" || failure.code === "repository_forbidden";
581
+ const title = failure.code === "repository_auth_required" ? "Authentication required" : failure.code === "repository_forbidden" ? "Repository access denied" : failure.code === "repository_timeout" ? "Repository timed out" : "Repository unavailable";
582
+ const guidance = failure.code === "repository_auth_required"
583
+ ? "Authenticate with your Git credential helper or provide a session token below."
584
+ : failure.code === "repository_forbidden"
585
+ ? "Confirm read access and, where required, approve the credential for organization SSO."
586
+ : failure.code === "repository_timeout"
587
+ ? "Check connectivity, VPN and proxy settings. The retry stays on this step."
588
+ : "Verify the repository URL and ref before retrying.";
589
+ const scope = failure.provider === "GitHub" ? "Use a fine-grained token with Contents: Read, or a classic token with repo access." : "Use a token with the read_repository scope.";
590
+ return (
591
+ <div role="alert" className="mt-4 rounded-control border border-unresolved bg-surface px-3 py-3">
592
+ <div className="flex items-start gap-3">
593
+ <CircleAlert size={18} className="mt-0.5 shrink-0 text-unresolved" />
594
+ <div className="min-w-0 flex-1">
595
+ <div className="flex flex-wrap items-center gap-2"><span className="font-medium text-ink">{title}</span><span className="chip status-unresolved">{failure.status}</span>{failure.provider ? <span className="chip status-declared">{failure.provider}</span> : null}</div>
596
+ <p className="mt-1 text-muted">{message}</p>
597
+ <p className="mt-2 text-muted">{guidance}</p>
598
+ {authFailure && failure.credentialSupported ? <div className="mt-3 rounded-control border border-line bg-canvas p-3"><div className="flex items-center gap-2 font-medium text-ink"><KeyRound size={15} /> {failure.credentialPresent ? "Replace session token" : "Use an access token"}</div><p className="mt-1 text-muted">{scope} It stays only in this local process and is forgotten when the server stops.</p><label className="mt-3 block"><span className="label mb-1.5 block">{failure.provider} access token</span><input className={FIELD} type="password" autoComplete="off" spellCheck={false} value={token} onChange={(event) => onTokenChange(event.target.value)} placeholder="token is never written to the repository" /></label>{failure.credentialPresent ? <button type="button" className="tbtn mt-3" onClick={onForget} disabled={busy}><Trash2 size={14} /> Forget saved token</button> : null}</div> : null}
599
+ </div>
600
+ </div>
601
+ </div>
602
+ );
603
+ }
604
+
605
+ function Wizard({ open, onClose, onAdded, onRunStarted }: { open: boolean; onClose: () => void; onAdded: (setup: SetupInfo) => void; onRunStarted: (runId: string) => void }) {
606
+ const [source, setSource] = useState<"local" | "external">("local");
607
+ const [stage, setStage] = useState<"source" | "scope" | "configure" | "trial">("source");
608
+ const [path, setPath] = useState("");
609
+ const [repository, setRepository] = useState("");
610
+ const [ref, setRef] = useState("main");
611
+ const [sourcePath, setSourcePath] = useState("");
612
+ const [discovery, setDiscovery] = useState<Discovery | null>(null);
613
+ const [scopeDiscovery, setScopeDiscovery] = useState<Discovery | null>(null);
614
+ const [inspectionCommit, setInspectionCommit] = useState("");
615
+ const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
616
+ const [pendingComponents, setPendingComponents] = useState<string[]>([]);
617
+ const [batchPosition, setBatchPosition] = useState<{ current: number; total: number } | null>(null);
618
+ const [draft, setDraft] = useState<ProjectDraft | null>(null);
619
+ const [plan, setPlan] = useState<ProjectPlan | null>(null);
620
+ const [trialRunId, setTrialRunId] = useState<string | null>(null);
621
+ const [trialEvents, setTrialEvents] = useState<RunEvent[]>([]);
622
+ const [busy, setBusy] = useState(false);
623
+ const [error, setError] = useState("");
624
+ const [repositoryError, setRepositoryError] = useState<LocalApiError | null>(null);
625
+ const [retryComponent, setRetryComponent] = useState<{ path: string; batch: boolean } | null>(null);
626
+ const [credentialToken, setCredentialToken] = useState("");
627
+
628
+ useEffect(() => {
629
+ if (!open) {
630
+ setStage("source"); setSource("local"); setPath(""); setRepository(""); setRef("main"); setSourcePath(""); setDiscovery(null); setScopeDiscovery(null); setInspectionCommit(""); setSelectedComponents([]); setPendingComponents([]); setBatchPosition(null); setDraft(null); setPlan(null); setTrialRunId(null); setTrialEvents([]); setError(""); setRepositoryError(null); setRetryComponent(null); setCredentialToken(""); setBusy(false);
631
+ }
632
+ }, [open]);
633
+
634
+ useEffect(() => {
635
+ if (!trialRunId) return;
636
+ return subscribeToRun(
637
+ trialRunId,
638
+ (event) => setTrialEvents((current) => [...current, event]),
639
+ () => {},
640
+ );
641
+ }, [trialRunId]);
642
+
643
+ function clearRepositoryFailure() {
644
+ setError(""); setRepositoryError(null); setRetryComponent(null); setCredentialToken("");
645
+ }
646
+
647
+ function selectSource(next: "local" | "external") {
648
+ setSource(next);
649
+ clearRepositoryFailure();
650
+ }
651
+
652
+ async function detect() {
653
+ setBusy(true); setError("");
654
+ setRepositoryError(null); setRetryComponent(null);
655
+ setScopeDiscovery(null);
656
+ setSelectedComponents([]); setPendingComponents([]); setBatchPosition(null);
657
+ try {
658
+ const inspection = source === "external" ? await inspectRepository(repository, ref, sourcePath) : null;
659
+ const found = inspection?.discovery ?? await discover(path);
660
+ const commit = inspection?.commit ?? "";
661
+ setInspectionCommit(commit);
662
+ if (!sourcePath.trim() && found.components.some((component) => component.path !== ".")) {
663
+ const onlyComponent = found.components.length === 1 ? found.components[0]?.path : undefined;
664
+ setScopeDiscovery(found); setSelectedComponents(onlyComponent ? [onlyComponent] : []); setStage("scope");
665
+ } else configure(found, commit, sourcePath);
666
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); setRepositoryError(cause instanceof LocalApiError && cause.code?.startsWith("repository_") ? cause : null); }
667
+ finally { setBusy(false); }
668
+ }
669
+
670
+ function configure(found: Discovery, commit: string, selectedSourcePath: string, projectId?: string) {
671
+ setDiscovery(found);
672
+ setDraft({ source, root: found.root, repository, ref, commit, sourcePath: selectedSourcePath, ...found.defaults, ...(projectId ? { id: projectId } : {}), plugins: found.detections.filter((item) => item.selected).map((item) => item.plugin) });
673
+ setStage("configure");
674
+ }
675
+
676
+ async function chooseComponent(componentPath: string, batch = false) {
677
+ if (!scopeDiscovery) return;
678
+ setBusy(true); setError("");
679
+ setRepositoryError(null); setRetryComponent(null);
680
+ try {
681
+ const projectId = batch && componentPath !== "." ? componentPath.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") : undefined;
682
+ if (source === "external") {
683
+ const selected = componentPath === "." ? "" : componentPath;
684
+ const inspection = await inspectRepository(repository, inspectionCommit || ref, selected);
685
+ configure(inspection.discovery, inspection.commit, selected, projectId);
686
+ } else {
687
+ const base = path.replace(/\/$/, "");
688
+ const selected = componentPath === "." ? base : [base === "." ? "" : base, componentPath].filter(Boolean).join("/");
689
+ configure(componentPath === "." ? scopeDiscovery : await discover(selected), "", "", projectId);
690
+ }
691
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); setRepositoryError(cause instanceof LocalApiError && cause.code?.startsWith("repository_") ? cause : null); setRetryComponent({ path: componentPath, batch }); }
692
+ finally { setBusy(false); }
693
+ }
694
+
695
+ async function reviewSelectedComponents() {
696
+ if (!scopeDiscovery || selectedComponents.length === 0) return;
697
+ const ordered = scopeDiscovery.components.map((component) => component.path).filter((componentPath) => selectedComponents.includes(componentPath));
698
+ const first = ordered[0];
699
+ if (!first) return;
700
+ setPendingComponents(ordered.slice(1));
701
+ setBatchPosition(ordered.length > 1 ? { current: 1, total: ordered.length } : null);
702
+ await chooseComponent(first, ordered.length > 1);
703
+ }
704
+
705
+ function retryInspection() {
706
+ return retryComponent ? chooseComponent(retryComponent.path, retryComponent.batch) : detect();
707
+ }
708
+
709
+ async function authenticateRepository() {
710
+ if (!credentialToken.trim()) return;
711
+ setBusy(true);
712
+ try {
713
+ await saveRepositoryCredential(repository, credentialToken);
714
+ setCredentialToken("");
715
+ } catch (cause) {
716
+ setError(cause instanceof Error ? cause.message : String(cause));
717
+ setBusy(false);
718
+ return;
719
+ }
720
+ setBusy(false);
721
+ await retryInspection();
722
+ }
723
+
724
+ async function forgetCredential() {
725
+ if (!repositoryError) return;
726
+ setBusy(true);
727
+ try {
728
+ await forgetRepositoryCredential(repository);
729
+ setCredentialToken("");
730
+ setRepositoryError(new LocalApiError(repositoryError.message, {
731
+ status: repositoryError.status, code: repositoryError.code, retryable: repositoryError.retryable, provider: repositoryError.provider, host: repositoryError.host, credentialSupported: repositoryError.credentialSupported, credentialPresent: false,
732
+ }));
733
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
734
+ finally { setBusy(false); }
735
+ }
736
+
737
+ async function runTrial() {
738
+ if (!draft) return;
739
+ setBusy(true); setError("");
740
+ setRepositoryError(null); setRetryComponent(null);
741
+ try {
742
+ const result = await startProjectTrial(draft);
743
+ setPlan(result.plan); setTrialEvents([]); setTrialRunId(result.runId); setStage("trial");
744
+ }
745
+ catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); }
746
+ finally { setBusy(false); }
747
+ }
748
+
749
+ async function applyTrial(generate: boolean) {
750
+ if (!trialRunId) return;
751
+ setBusy(true); setError("");
752
+ try {
753
+ const result = await applyProjectTrial(trialRunId, generate);
754
+ onAdded(result.setup);
755
+ if (pendingComponents.length > 0) {
756
+ const [next, ...remaining] = pendingComponents;
757
+ if (!next) throw new Error("The next selected component is missing.");
758
+ setPendingComponents(remaining); setBatchPosition((position) => position ? { ...position, current: position.current + 1 } : null);
759
+ setTrialRunId(null); setTrialEvents([]); setPlan(null);
760
+ await chooseComponent(next, true);
761
+ } else {
762
+ onClose();
763
+ if (result.run) onRunStarted(result.run.runId);
764
+ }
765
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); setBusy(false); }
766
+ }
767
+
768
+ const finished = trialEvents.slice().reverse().find((event): event is Extract<RunEvent, { type: "process-finished" }> => event.type === "process-finished");
769
+ const pipeline = trialEvents.find((event): event is Extract<RunEvent, { type: "pipeline-ready" }> => event.type === "pipeline-ready");
770
+ const completedSteps = trialEvents.filter((event): event is Extract<RunEvent, { type: "step-finished" }> => event.type === "step-finished");
771
+ const activeStep = trialEvents.slice().reverse().find((event): event is Extract<RunEvent, { type: "step-started" }> => event.type === "step-started");
772
+ const trial = trialEvents.find((event): event is Extract<RunEvent, { type: "project-trial-ready" }> => event.type === "project-trial-ready");
773
+ const logs = trialEvents.filter((event): event is Extract<RunEvent, { type: "log" }> => event.type === "log");
774
+ const trialRunning = stage === "trial" && !finished;
775
+ const totalSteps = pipeline?.stepCount ?? plan?.steps.length ?? 0;
776
+ const percent = totalSteps ? Math.round((completedSteps.length / totalSteps) * 100) : 0;
777
+ const heading = stage === "source" ? "Add a project" : stage === "scope" ? "Choose a component" : stage === "configure" ? "Review discovery" : "Trial extraction";
778
+ function back() {
779
+ if (stage === "trial") { if (trialRunId) void disposeProjectTrial(trialRunId); setStage("configure"); setTrialRunId(null); setTrialEvents([]); setPlan(null); }
780
+ else if (stage === "configure" && scopeDiscovery) { setStage("scope"); setPendingComponents([]); setBatchPosition(null); }
781
+ else setStage("source");
782
+ setError(""); setRepositoryError(null); setRetryComponent(null); setCredentialToken("");
783
+ }
784
+ return (
785
+ <Modal open={open} onClose={busy || trialRunning ? () => {} : onClose} label={heading} width="min(800px,94vw)">
786
+ <div className="flex items-center gap-3 border-b border-line px-5 py-4">
787
+ {stage !== "source" ? <button type="button" className="tbtn p-1.5" onClick={back} disabled={trialRunning || busy} aria-label="Back"><ArrowLeft size={16} /></button> : null}
788
+ <div className="min-w-0 flex-1"><div className="font-semibold text-ink">{heading}</div><div className="mono mt-0.5 text-muted">{stage === "source" ? "1 / 4 · source" : stage === "scope" ? "2 / 4 · scope" : stage === "configure" ? `3 / 4 · capabilities${batchPosition ? ` · ${batchPosition.current} / ${batchPosition.total}` : ""}` : `4 / 4 · proof${batchPosition ? ` · ${batchPosition.current} / ${batchPosition.total}` : ""}`}</div></div>
789
+ <button type="button" className="tbtn p-1.5" onClick={onClose} disabled={busy || trialRunning} aria-label="Close"><X size={16} /></button>
790
+ </div>
791
+ <div className="min-h-0 flex-1 overflow-y-auto p-5">
792
+ {stage === "source" ? (
793
+ repositoryError ? <div className="flex flex-wrap items-center justify-between gap-3 rounded-control border border-line bg-surface px-3 py-2"><div className="min-w-0"><div className="mono truncate text-ink">{repository}</div><div className="mono mt-0.5 text-muted">{ref || "HEAD"}{sourcePath ? ` · ${sourcePath}` : ""}</div></div><button type="button" className="tbtn" onClick={clearRepositoryFailure}>Edit source</button></div> : <div className="space-y-5">
794
+ <div className="seg inline-flex" role="group" aria-label="Project source"><button type="button" className={source === "local" ? "is-on" : ""} aria-pressed={source === "local"} onClick={() => selectSource("local")}>local directory</button><button type="button" className={source === "external" ? "is-on" : ""} aria-pressed={source === "external"} onClick={() => selectSource("external")}>external repository</button></div>
795
+ <p className="text-muted">Detection reads project files but does not execute project code. External repositories are pinned and vendored through the built-in git fetcher.</p>
796
+ {source === "local" ? <><Field label="local path" value={path} onChange={setPath} placeholder="services/billing" required /><Field label="repository URL · optional" value={repository} onChange={(value) => { setRepository(value); clearRepositoryFailure(); }} placeholder="https://github.com/acme/billing" /></> : <><Field label="repository URL" value={repository} onChange={(value) => { setRepository(value); clearRepositoryFailure(); }} placeholder="https://github.com/acme/platform" required /><div className="grid gap-4 sm:grid-cols-2"><Field label="branch, tag or commit" value={ref} onChange={(value) => { setRef(value); clearRepositoryFailure(); }} placeholder="main" /><Field label="component path · optional" value={sourcePath} onChange={(value) => { setSourcePath(value); clearRepositoryFailure(); }} placeholder="services/billing" /></div></>}
797
+ </div>
798
+ ) : stage === "scope" && scopeDiscovery ? (
799
+ <div className="space-y-4">
800
+ <div className="flex flex-wrap items-start justify-between gap-2"><div><p className="text-ink">Portolan found {scopeDiscovery.components.length} possible {plural(scopeDiscovery.components.length, "component")}.</p><p className="mt-1 text-muted">Select one or several modules. Each selected component gets its own scoped discovery and trial before it is added. In a batch, confirmed components are applied one at a time, so you can stop between them.</p></div><div className="flex gap-2"><button type="button" className="tbtn" onClick={() => setSelectedComponents(scopeDiscovery.components.map((component) => component.path))}>Select all</button><button type="button" className="tbtn" onClick={() => setSelectedComponents([])} disabled={!selectedComponents.length}>Clear</button></div></div>
801
+ <div className="grid gap-2">{scopeDiscovery.components.map((component) => { const selected = selectedComponents.includes(component.path); return <button type="button" key={component.path} aria-pressed={selected} onClick={() => setSelectedComponents((current) => selected ? current.filter((item) => item !== component.path) : [...current, component.path])} disabled={busy} className={`flex items-center gap-3 rounded-control border px-3 py-3 text-left transition-colors hover:border-accent hover:bg-surface ${selected ? "border-accent bg-surface" : "border-line"}`}><span className={`flex size-9 shrink-0 items-center justify-center rounded-control ${selected ? "bg-accent text-canvas" : "bg-surface text-muted"}`}>{selected ? <Check size={17} /> : <FolderGit2 size={17} />}</span><span className="min-w-0 flex-1"><span className="font-medium text-ink">{component.name}</span><span className="mono mt-0.5 block truncate text-muted">{component.path === "." ? "repository root" : component.path}</span></span><span className="flex flex-wrap justify-end gap-1">{component.technologies.map((technology) => <span key={technology} className="chip status-declared">{technology}</span>)}</span></button>; })}</div>
802
+ {scopeDiscovery.componentsTruncated ? <p className="text-declared">Showing the first {scopeDiscovery.components.length} component roots. Narrow the source path to inspect the rest.</p> : null}
803
+ </div>
804
+ ) : stage === "configure" && discovery && draft ? (
805
+ <div className="space-y-5">
806
+ <div className="rounded-control border border-line bg-surface px-3 py-2 text-muted"><span className="mono text-ink">{draft.source === "external" ? `${draft.repository}@${draft.commit.slice(0, 7)}${draft.sourcePath ? `/${draft.sourcePath}` : ""}` : discovery.root}</span> · scanned {discovery.filesScanned} files{discovery.truncated ? " (limit reached)" : ""}</div>
807
+ <div className="grid gap-4 sm:grid-cols-2">
808
+ <Field label="project name" value={draft.name} onChange={(name) => setDraft({ ...draft, name })} required />
809
+ <Field label="project id" value={draft.id} onChange={(id) => setDraft({ ...draft, id })} required />
810
+ <Field label="group" value={draft.group} onChange={(group) => setDraft({ ...draft, group })} />
811
+ <Field label="component slug" value={draft.component} onChange={(component) => setDraft({ ...draft, component })} />
812
+ </div>
813
+ <div><div className="label mb-2">detected capabilities</div>
814
+ {discovery.detections.length ? <div className="grid gap-2">{discovery.detections.map((item) => {
815
+ const checked = draft.plugins.includes(item.plugin);
816
+ const capability = CAPABILITIES[item.plugin] ?? { title: item.plugin, summary: "Catalog facts extracted from project files." };
817
+ return <label key={item.plugin} className={`flex cursor-pointer items-start gap-3 rounded-control border px-3 py-3 transition-colors ${checked ? "border-accent bg-surface" : "border-line hover:bg-surface"}`}><input className="mt-1" type="checkbox" checked={checked} onChange={() => setDraft({ ...draft, plugins: checked ? draft.plugins.filter((name) => name !== item.plugin) : [...draft.plugins, item.plugin] })} /><span className="min-w-0 flex-1"><span className="flex flex-wrap items-center gap-2"><span className="font-medium text-ink">{capability.title}</span><span className={`chip ${item.confidence === "high" ? "status-verified" : "status-declared"}`}>{item.confidence} confidence</span>{item.candidates.length > 1 ? <span className="chip status-declared">{item.candidates.length} candidates</span> : null}</span><span className="mt-0.5 block text-muted">{capability.summary}</span><span className="mono mt-1 block truncate text-faint" title={item.evidence}>evidence · {item.evidence}</span><span className="mono mt-0.5 block text-faint">extractor · {item.plugin}</span></span></label>;
818
+ })}</div> : <Empty>no supported project signals found</Empty>}
819
+ </div>
820
+ </div>
821
+ ) : stage === "trial" && plan ? (
822
+ <div className="space-y-5">
823
+ <div className={`rounded-control border px-3 py-3 ${trial ? "border-verified bg-surface" : finished?.status === "failed" ? "border-unresolved" : "border-line bg-surface"}`}>
824
+ <div className="flex items-start gap-3">{trial ? <Check size={18} className="mt-0.5 shrink-0 text-verified" /> : finished?.status === "failed" ? <CircleAlert size={18} className="mt-0.5 shrink-0 text-unresolved" /> : <LoaderCircle size={18} className="mt-0.5 shrink-0 animate-spin text-accent" />}<div><div className="font-medium text-ink">{trial ? "Extraction succeeded — repository unchanged" : finished?.status === "failed" ? "Trial extraction failed" : activeStep ? `Running ${activeStep.plugin}` : "Creating an isolated workspace…"}</div><p className="mt-0.5 text-muted">{trial ? "These results came from real catalog fragments. Apply is now safe to continue." : "Portolan is running the selected extractors without writing to portolan.json or your project."}</p></div></div>
825
+ {trial?.previewUrl ? <a className="btn-accent mt-3 inline-flex" href={trial.previewUrl} target="_blank" rel="noreferrer">Open catalog preview ↗</a> : null}
826
+ {trial?.previewError ? <p className="mt-2 text-declared">The extraction is valid, but the temporary site could not start: {trial.previewError}</p> : null}
827
+ {!finished ? <><div className="mt-3 h-1.5 overflow-hidden rounded-full bg-canvas"><div className="h-full bg-accent transition-[width]" style={{ width: `${percent}%` }} /></div><div className="mono mt-1 text-right text-muted">{completedSteps.length} / {totalSteps || "?"} steps</div></> : null}
828
+ </div>
829
+ {trial?.facts.length ? <section><div className="label mb-2">catalog facts found</div><div className="grid grid-cols-2 gap-2 sm:grid-cols-3">{trial.facts.map((fact) => <div key={fact.key} className="rounded-control border border-line bg-canvas px-3 py-2"><div className="tnum text-lg font-semibold text-ink">{fact.count}</div><div className="text-muted">{fact.label}</div></div>)}</div></section> : null}
830
+ <section><div className="label mb-2">extractor results</div><div className="divide-y divide-line rounded-control border border-line">{(trial?.steps ?? completedSteps).map((step) => <div key={`${step.plugin}:${"ordinal" in step ? step.ordinal : "trial"}`} className="grid gap-1 px-3 py-2 sm:grid-cols-[1fr_auto_auto]"><span><span className="font-medium text-ink">{CAPABILITIES[step.plugin]?.title ?? step.plugin}</span><span className="mono ml-2 text-faint">{step.plugin}</span>{step.message ? <span className="mt-1 block text-unresolved">{step.message}</span> : null}</span><span className="mono text-muted">{step.fileCount} {plural(step.fileCount, "file")}</span><span className={`chip ${step.status === "failed" ? "status-unresolved" : "status-verified"}`}>{step.status === "failed" ? "failed" : "read"}</span></div>)}</div></section>
831
+ {trial?.warnings.length ? <section><div className="label mb-2">warnings · {trial.warnings.length}</div><div className="space-y-2">{trial.warnings.map((warning, index) => <div key={`${warning.plugin}:${index}`} className="flex gap-2 rounded-control border border-declared px-3 py-2 text-muted"><CircleAlert size={15} className="mt-0.5 shrink-0 text-declared" /><span><span className="mono text-ink">{warning.plugin}</span> · {warning.message}</span></div>)}</div></section> : null}
832
+ {finished?.status === "failed" && logs.length ? <details><summary className="cursor-pointer text-muted">Generator log · {logs.length} lines</summary><pre className="mono mt-2 max-h-48 overflow-auto whitespace-pre-wrap rounded-control bg-surface p-3 text-muted">{logs.map((event) => event.message).join("\n")}</pre></details> : null}
833
+ {trial ? <div><div className="label mb-2">changes after apply</div><dl className="mono grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-2 text-muted"><dt>project</dt><dd className="text-ink">{plan.project.id}</dd>{plan.fetch ? <><dt>pin</dt><dd className="truncate text-ink">{plan.fetch.commit.slice(0, 12)}</dd></> : null}<dt>source</dt><dd className="truncate text-ink">{plan.source}</dd><dt>pipeline</dt><dd className="text-ink">{plan.steps.length + (plan.fetch ? 1 : 0)} {plural(plan.steps.length + (plan.fetch ? 1 : 0), "extract step")}</dd><dt>fragments</dt><dd className="text-ink">{trial.generatedFiles} generated</dd></dl></div> : null}
834
+ </div>
835
+ ) : null}
836
+ {error ? repositoryError ? <RepositoryFailure failure={repositoryError} message={error} token={credentialToken} onTokenChange={setCredentialToken} onForget={() => void forgetCredential()} busy={busy} /> : <div role="alert" className="mt-4 rounded-control border border-unresolved px-3 py-2 text-unresolved">{error}</div> : null}
837
+ </div>
838
+ <div className="flex flex-wrap justify-end gap-2 border-t border-line px-5 py-4">
839
+ <button type="button" className="tbtn" onClick={onClose} disabled={busy || trialRunning}>Cancel</button>
840
+ {stage === "source" ? <button key={repositoryError?.retryable ? "retry-inspection" : source} type="button" className="btn-accent" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : detect())} disabled={busy || (source === "local" ? !path.trim() : !repository.trim())}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : null} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : source === "external" ? "Inspect repository" : "Detect project"}</button> : null}
841
+ {stage === "scope" ? <button key={repositoryError?.retryable ? "retry-component" : "review-components"} type="button" className="btn-accent" aria-label={repositoryError?.retryable ? credentialToken.trim() ? "Save token and retry" : "Retry inspection" : `Review ${selectedComponents.length || "selected"} ${selectedComponents.length === 1 ? "component" : "components"}`} onClick={() => void (repositoryError?.retryable ? credentialToken.trim() ? authenticateRepository() : retryInspection() : reviewSelectedComponents())} disabled={busy || (!repositoryError?.retryable && !selectedComponents.length)}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} {repositoryError?.retryable ? credentialToken.trim() ? "Save token & retry" : "Retry inspection" : <>Review {selectedComponents.length || "selected"} {selectedComponents.length === 1 ? "component" : "components"}</>}</button> : null}
842
+ {stage === "configure" ? <button type="button" className="btn-accent" onClick={() => void runTrial()} disabled={busy || !draft?.plugins.length}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Run trial extraction</button> : null}
843
+ {stage === "trial" && trialRunning ? <button type="button" className="tbtn" onClick={() => trialRunId && void cancelGeneration(trialRunId)}>Cancel trial</button> : null}
844
+ {stage === "trial" && finished && !trial ? <button type="button" className="btn-accent" onClick={back}>Back to configuration</button> : null}
845
+ {stage === "trial" && trial ? pendingComponents.length > 0 ? <button type="button" className="btn-accent" onClick={() => void applyTrial(false)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & review next</button> : <><button type="button" className="tbtn" onClick={() => void applyTrial(false)} disabled={busy}>Add without generating</button><button type="button" className="btn-accent" onClick={() => void applyTrial(true)} disabled={busy}>{busy ? <LoaderCircle size={15} className="animate-spin" /> : <Play size={15} />} Add & generate</button></> : null}
846
+ </div>
847
+ </Modal>
848
+ );
849
+ }
850
+
851
+ function RunDialog({ runId, open, onClose, onFinished, onApply }: { runId: string | null; open: boolean; onClose: () => void; onFinished: () => void; onApply: (previewRunId: string) => void }) {
852
+ const [events, setEvents] = useState<RunEvent[]>([]);
853
+ const finished = events.slice().reverse().find(
854
+ (event): event is Extract<RunEvent, { type: "process-finished" }> => event.type === "process-finished",
855
+ );
856
+ const pipeline = events.find((event) => event.type === "pipeline-ready");
857
+ const steps = events.filter((event) => event.type === "step-finished");
858
+ const active = events.slice().reverse().find(
859
+ (event): event is Extract<RunEvent, { type: "step-started" }> => event.type === "step-started",
860
+ );
861
+ const logs = events.filter((event) => event.type === "log");
862
+ const run = events.find((event): event is Extract<RunEvent, { type: "run-started" }> => event.type === "run-started");
863
+ const preview = events.find((event): event is Extract<RunEvent, { type: "preview-ready" }> => event.type === "preview-ready");
864
+ useEffect(() => {
865
+ if (!runId) return;
866
+ setEvents([]);
867
+ return subscribeToRun(runId, (event) => { setEvents((current) => [...current, event]); if (event.type === "process-finished") onFinished(); }, onFinished);
868
+ }, [runId, onFinished]);
869
+ const total = pipeline?.type === "pipeline-ready" ? pipeline.stepCount : 0;
870
+ const percent = total ? Math.round((steps.length / total) * 100) : 0;
871
+ return (
872
+ <Modal open={open} onClose={finished ? onClose : () => {}} label="Generate documentation" width="min(720px,94vw)">
873
+ <div className="flex items-center gap-3 border-b border-line px-5 py-4"><div className="flex-1"><div className="font-semibold text-ink">{run?.mode === "preview" ? "Preview generated changes" : "Generate documentation"}</div><div className="mono mt-0.5 text-muted">{finished ? `finished · ${finished.status}` : active?.type === "step-started" ? `${active.phase} · ${active.plugin}` : run?.mode === "preview" ? "creating an isolated workspace…" : "starting generator…"}</div></div>{finished ? <button className="tbtn p-1.5" onClick={onClose} aria-label="Close"><X size={16} /></button> : <LoaderCircle size={18} className="animate-spin text-accent" />}</div>
874
+ <div className="min-h-0 flex-1 overflow-y-auto p-5">
875
+ <div className="h-1.5 overflow-hidden rounded-full bg-surface"><div className="h-full bg-accent transition-[width]" style={{ width: `${percent}%` }} /></div>
876
+ <div className="mono mt-2 flex justify-between text-muted"><span>{steps.length} / {total || "?"} steps</span><span>{percent}%</span></div>
877
+ <div className="mt-4 divide-y divide-line rounded-control border border-line">{steps.map((event) => event.type === "step-finished" ? <div key={`${event.ordinal}:${event.plugin}`} className="grid gap-1 px-3 py-2 sm:grid-cols-[8rem_1fr_auto]"><span className="mono text-muted">{event.phase}</span><span className="mono text-ink">{event.plugin}</span><span className={`chip ${event.status === "failed" ? "status-unresolved" : "status-verified"}`}>{event.status}</span></div> : null)}</div>
878
+ {preview ? <section className="mt-5"><div className="label mb-2">generated diff · {preview.totalFiles} {plural(preview.totalFiles, "file")}</div>{preview.files.length ? <div className="divide-y divide-line overflow-hidden rounded-control border border-line">{preview.files.map((file) => <details key={file.path} className="group"><summary className="flex cursor-pointer items-center gap-2 px-3 py-2 hover:bg-surface"><span className={`chip ${file.status === "removed" ? "status-unresolved" : file.status === "added" ? "status-verified" : "status-declared"}`}>{file.status}</span><span className="mono truncate text-ink">{file.path}</span><ChevronDown size={15} className="ml-auto shrink-0 text-muted transition-transform group-open:rotate-180" /></summary><pre className="mono max-h-80 overflow-auto whitespace-pre p-3 text-muted bg-surface">{file.diff || "Binary file changed"}</pre></details>)}</div> : <div className="rounded-control border border-line bg-surface px-3 py-3 text-muted">Generated documentation is already up to date.</div>}{preview.truncated ? <p className="mt-2 text-muted">Showing the first {preview.files.length} changed files.</p> : null}</section> : null}
879
+ {logs.length ? <details className="mt-4"><summary className="cursor-pointer text-muted">Generator log · {logs.length} lines</summary><pre className="mono mt-2 max-h-48 overflow-auto whitespace-pre-wrap rounded-control bg-surface p-3 text-muted">{logs.map((event) => event.type === "log" ? event.message : "").join("\n")}</pre></details> : null}
880
+ </div>
881
+ <div className="flex justify-end gap-2 border-t border-line px-5 py-4">{finished ? <><button type="button" className="tbtn" onClick={onClose}>{preview?.files.length ? "Cancel" : "Done"}</button>{preview?.files.length && finished.status === "ok" && runId ? <button type="button" className="btn-accent" onClick={() => onApply(runId)}><Play size={15} /> Apply changes</button> : null}</> : <button type="button" className="tbtn" onClick={() => runId && void cancelGeneration(runId)}>Cancel generation</button>}</div>
882
+ </Modal>
883
+ );
884
+ }
885
+
886
+ function SettingsContent({ local, onAdd, onGenerate }: { local: boolean; onAdd: () => void; onGenerate: () => void }) {
887
+ const setupInfo = useSetup();
888
+ const active = setupInfo.plugins.filter((plugin) => plugin.stepCount > 0);
889
+ return (
890
+ <div className="h-full overflow-y-auto p-gutter">
891
+ <div className="max-w-table">
892
+ <div className="flex flex-wrap items-start justify-between gap-3"><div><div className="flex items-center gap-2"><h1 className="text-lg font-semibold">Settings</h1>{local ? <span className="chip status-verified">local mode</span> : null}</div><p className="mt-1 max-w-prose text-muted">The projects, plugins and local preferences used by this catalog. {local ? "This local session can update portolan.json and preview generator output." : "Build configuration is read-only here and comes from portolan.json."}</p></div>{local ? <button type="button" className="btn-accent" onClick={onGenerate}><Play size={15} /> Preview generated diff</button> : null}</div>
893
+ <BuildHealth />
894
+
895
+ <div className="mt-4 grid grid-cols-2 gap-grid lg:grid-cols-4">
896
+ <Metric value={setupInfo.projects.length} label="project" />
897
+ <Metric value={active.length} label="active plugin" />
898
+ <Metric value={setupInfo.steps.length} label="pipeline step" />
899
+ <Metric value={catalogSources.length} label="catalog source" />
900
+ </div>
901
+
902
+ <section className="mt-section">
903
+ <SectionTitle right={local ? "editable in local mode" : "declared in portolan.json"}>Projects</SectionTitle>
904
+ {setupInfo.projects.length === 0 && !local ? <Empty>portolan.json names no projects — every input here is the estate's own</Empty> : <div className="grid gap-grid xl:grid-cols-2">{setupInfo.projects.map((project) => <ProjectCard key={project.id} project={project} />)}{local ? <AddProjectCard onClick={onAdd} /> : null}</div>}
905
+ </section>
906
+
907
+ <section className="mt-section">
908
+ <SectionTitle right={`${active.length} of ${setupInfo.plugins.length} active`}>Plugins</SectionTitle>
909
+ <PluginsList />
910
+ <p className="mono mt-2 text-muted">WASM runs without filesystem, network or environment access. A host process runs with the permissions of the build.</p>
911
+ </section>
912
+
913
+ <section className="mt-section"><SectionTitle right="stored in this browser">Appearance</SectionTitle><Appearance /></section>
914
+
915
+ {CHAT_BUILD.built ? <section className="mt-section"><SectionTitle right="stored in this browser">Ask the catalog</SectionTitle><ChatSection /></section> : null}
916
+
917
+ <details className="mt-section rounded-card border border-line shadow-xs">
918
+ <summary className="cursor-pointer select-none px-4 py-3 font-semibold text-ink">Advanced build inputs</summary>
919
+ <div className="border-t border-line p-4">
920
+ <div className="label mb-2">catalog source patterns</div>
921
+ {setupInfo.sources.length === 0 ? <Empty>no source patterns declared</Empty> : <ul className="mono space-y-1 text-muted">{setupInfo.sources.map((source) => <li key={source}>{source}</li>)}</ul>}
922
+ <div className="label mt-5 mb-2">pipeline</div>
923
+ {setupInfo.steps.length === 0 ? <Empty>no pipeline steps declared</Empty> : <div className="space-y-1">{setupInfo.steps.map((step, index) => <div key={`${step.phase}:${step.plugin}:${step.input ?? "catalog"}:${index}`} className="mono grid gap-x-3 text-muted sm:grid-cols-[5rem_9rem_1fr]"><span>{step.phase}</span><span className="text-ink">{step.plugin}</span><span className="truncate" title={step.input ?? "merged catalog"}>{step.input ?? "merged catalog"} → {step.output}</span></div>)}</div>}
924
+ <div className="label mt-5 mb-2">generated documentation</div>
925
+ <MachineDocs />
926
+ </div>
927
+ </details>
928
+
929
+ <div className="mono mt-section flex items-center gap-2 pb-section text-muted"><Box size={14} aria-hidden />{local ? "Changes are written to portolan.json; generated files remain reviewable in git." : "Configuration is embedded at build time; changing it requires a new catalog build."}</div>
930
+ </div>
931
+ </div>
932
+ );
933
+ }
934
+
935
+ export function Settings() {
936
+ useDocumentTitle("Settings");
937
+ const queryClient = useQueryClient();
938
+ const status = useQuery(localStatusQuery());
939
+ // No local server means no local mode; the static build-time setup stands in.
940
+ const local = status.isSuccess;
941
+ const setup = status.data?.setup ?? staticSetupInfo;
942
+ const [wizard, setWizard] = useState(false);
943
+ const [runId, setRunId] = useState<string | null>(null);
944
+ const [runOpen, setRunOpen] = useState(false);
945
+ const say = useToastStore((state) => state.say);
946
+ const activeRunId = status.data?.activeRun?.id ?? null;
947
+ useEffect(() => {
948
+ if (!activeRunId) return;
949
+ setRunId(activeRunId);
950
+ setRunOpen(true);
951
+ }, [activeRunId]);
952
+ const refresh = useCallback(() => {
953
+ void queryClient.invalidateQueries({ queryKey: localKeys.status });
954
+ }, [queryClient]);
955
+ // The wizard hands back the setup the server now has; keep the status in
956
+ // step without a round trip.
957
+ const setSetup = useCallback((next: SetupInfo) => {
958
+ queryClient.setQueryData(localKeys.status, (current) => current ? { ...current, setup: next } : current);
959
+ }, [queryClient]);
960
+ async function generate(previewRunId?: string) {
961
+ try {
962
+ const run = await startGeneration(previewRunId ? "write" : "preview", previewRunId);
963
+ setRunId(run.runId); setRunOpen(true);
964
+ } catch (cause) {
965
+ say(cause instanceof Error ? cause.message : String(cause));
966
+ }
967
+ }
968
+ return (
969
+ <SetupContext.Provider value={setup}>
970
+ <SettingsContent local={local} onAdd={() => setWizard(true)} onGenerate={() => void generate()} />
971
+ <Wizard open={wizard} onClose={() => setWizard(false)} onAdded={setSetup} onRunStarted={(id) => { setRunId(id); setRunOpen(true); }} />
972
+ <RunDialog runId={runId} open={runOpen} onClose={() => setRunOpen(false)} onFinished={refresh} onApply={(preview) => void generate(preview)} />
973
+ </SetupContext.Provider>
974
+ );
975
+ }