@dsdenes/devboard 0.1.559 → 0.1.562

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 (323) hide show
  1. package/AGENTS.md +59 -0
  2. package/README.md +2497 -26
  3. package/bin/devboard.mjs +352 -118
  4. package/client/App.svelte +523 -0
  5. package/client/FeedbackOverlay.svelte +1125 -0
  6. package/client/FlowMode.svelte +1015 -0
  7. package/client/app.css +3922 -0
  8. package/client/env.d.ts +23 -0
  9. package/client/lib/commands.ts +836 -0
  10. package/client/lib/components/ActivityBar.svelte +295 -0
  11. package/client/lib/components/AdrTable.svelte +241 -0
  12. package/client/lib/components/CommandPalette.svelte +198 -0
  13. package/client/lib/components/ConceptKind.svelte +66 -0
  14. package/client/lib/components/ConfirmDialog.svelte +62 -0
  15. package/client/lib/components/DataTable.svelte +243 -0
  16. package/client/lib/components/DetailSplit.svelte +68 -0
  17. package/client/lib/components/DiscoverOverview.svelte +898 -0
  18. package/client/lib/components/DiscoverWorkspace.svelte +437 -0
  19. package/client/lib/components/DraftsPage.svelte +186 -0
  20. package/client/lib/components/GoalWorkspace.svelte +1042 -0
  21. package/client/lib/components/IssuesAgentFocus.svelte +117 -0
  22. package/client/lib/components/IssuesAgentLog.svelte +50 -0
  23. package/client/lib/components/IssuesQuestion.svelte +95 -0
  24. package/client/lib/components/IssuesWorkspace.svelte +577 -0
  25. package/client/lib/components/MermaidDiagram.svelte +89 -0
  26. package/client/lib/components/MonacoEditor.svelte +190 -0
  27. package/client/lib/components/NotificationRegion.svelte +195 -0
  28. package/client/lib/components/PaneHandle.svelte +25 -0
  29. package/client/lib/components/ScoutIssues.svelte +215 -0
  30. package/client/lib/components/ScoutWorkspace.svelte +180 -0
  31. package/client/lib/components/Sidebar.svelte +622 -0
  32. package/client/lib/components/StatusBar.svelte +118 -0
  33. package/client/lib/components/Terminal.svelte +193 -0
  34. package/client/lib/components/WatchdogWorkspace.svelte +537 -0
  35. package/client/lib/components/ui/button.svelte +56 -0
  36. package/client/lib/components/ui/icon-button.svelte +56 -0
  37. package/client/lib/components/ui/input.svelte +19 -0
  38. package/client/lib/components/ui/page-header.svelte +120 -0
  39. package/client/lib/components/ui/stage-track.svelte +141 -0
  40. package/client/lib/components/watchdog-indicator.svelte +189 -0
  41. package/client/lib/connection.svelte.ts +210 -0
  42. package/client/lib/dependency-diagram.test.ts +90 -0
  43. package/client/lib/dependency-diagram.ts +75 -0
  44. package/client/lib/discover-format.test.ts +265 -0
  45. package/client/lib/discover-format.ts +394 -0
  46. package/client/lib/discover.svelte.ts +412 -0
  47. package/client/lib/draft-format.test.ts +48 -0
  48. package/client/lib/draft-format.ts +13 -0
  49. package/client/lib/flow-save.test.ts +148 -0
  50. package/client/lib/flow.svelte.ts +445 -0
  51. package/client/lib/goal-reconnect.test.ts +282 -0
  52. package/client/lib/goal.svelte.ts +652 -0
  53. package/client/lib/issues.svelte.ts +439 -0
  54. package/client/lib/layout.svelte.ts +234 -0
  55. package/client/lib/notification-issue.test.ts +53 -0
  56. package/client/lib/notification-issue.ts +27 -0
  57. package/client/lib/notifications.svelte.ts +62 -0
  58. package/client/lib/poll-status.test.ts +141 -0
  59. package/client/lib/poll-status.ts +88 -0
  60. package/client/lib/record-status.test.ts +19 -0
  61. package/client/lib/record-status.ts +25 -0
  62. package/client/lib/scout.svelte.ts +164 -0
  63. package/client/lib/skill-document.test.ts +77 -0
  64. package/client/lib/skill-document.ts +57 -0
  65. package/client/lib/status-tone.test.ts +40 -0
  66. package/client/lib/status-tone.ts +33 -0
  67. package/client/lib/types.ts +578 -0
  68. package/client/lib/utils.ts +6 -0
  69. package/client/lib/viewport.svelte.ts +20 -0
  70. package/client/lib/watchdog.svelte.ts +69 -0
  71. package/client/main.ts +7 -0
  72. package/dist/apple-touch-icon.png +0 -0
  73. package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
  74. package/dist/assets/arc-BZgCSX2H.js +1 -0
  75. package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
  76. package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
  77. package/dist/assets/array-BifhSqXX.js +1 -0
  78. package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
  79. package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
  80. package/dist/assets/channel-CgM4lcUW.js +1 -0
  81. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
  83. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  84. package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
  85. package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
  86. package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
  87. package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
  88. package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
  89. package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
  90. package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
  91. package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  92. package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
  93. package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
  94. package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
  95. package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
  96. package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
  97. package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
  98. package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
  99. package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
  100. package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
  101. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  102. package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
  103. package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
  104. package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
  105. package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
  106. package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
  107. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  108. package/dist/assets/dagre-D7JM2Yhv.js +1 -0
  109. package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
  110. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  111. package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
  112. package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
  113. package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
  114. package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
  115. package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
  116. package/dist/assets/dist-Dce-2YbA.js +1 -0
  117. package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
  118. package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
  119. package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
  120. package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
  121. package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
  122. package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
  123. package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
  124. package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
  125. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  126. package/dist/assets/index-ByQ-XX38.css +1 -0
  127. package/dist/assets/index-CIf5TpfY.js +214 -0
  128. package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
  129. package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
  130. package/dist/assets/init-C-OQMol4.js +1 -0
  131. package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
  132. package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
  133. package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
  134. package/dist/assets/katex-DJhrJSbw.js +257 -0
  135. package/dist/assets/line-N9XiCREw.js +1 -0
  136. package/dist/assets/linear-C9hnqmJv.js +1 -0
  137. package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
  138. package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
  139. package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
  140. package/dist/assets/monaco-DjpsE8to.css +1 -0
  141. package/dist/assets/monaco-DqzJaNIe.js +908 -0
  142. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  143. package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
  144. package/dist/assets/path-fybaL0A-.js +1 -0
  145. package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
  146. package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
  147. package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
  148. package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
  149. package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
  150. package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
  151. package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
  152. package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
  153. package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
  154. package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
  155. package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
  156. package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  157. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  158. package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
  159. package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
  160. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  161. package/dist/assets/src-CGp3bfio.js +1 -0
  162. package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
  163. package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
  164. package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
  165. package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
  166. package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
  167. package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
  168. package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
  169. package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
  170. package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
  171. package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
  172. package/dist/assets/xterm-BrP-ENHg.css +1 -0
  173. package/dist/assets/xterm-C8zLTNGk.js +16 -0
  174. package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
  175. package/dist/favicon.ico +0 -0
  176. package/dist/favicon.svg +6 -0
  177. package/dist/index.html +34 -0
  178. package/extensions/implement-goal-issue.ts +368 -0
  179. package/extensions/implement-next-epic.ts +835 -0
  180. package/index.html +28 -0
  181. package/install.sh +111 -0
  182. package/issues/control.ts +91 -0
  183. package/issues/guards.ts +12 -0
  184. package/issues/ledger.ts +122 -0
  185. package/issues/legacy.ts +68 -0
  186. package/issues/lock.ts +305 -0
  187. package/issues/paths.ts +76 -0
  188. package/issues/preflight.ts +189 -0
  189. package/issues/repository.ts +203 -0
  190. package/issues/scout/actions.ts +255 -0
  191. package/issues/scout/control.ts +80 -0
  192. package/issues/scout/github.ts +358 -0
  193. package/issues/scout/paths.ts +129 -0
  194. package/issues/scout/preflight.ts +277 -0
  195. package/issues/scout/recommendation.ts +400 -0
  196. package/issues/scout/scan.ts +143 -0
  197. package/issues/scout/state.ts +448 -0
  198. package/issues/scout/worker.ts +1282 -0
  199. package/issues/state.ts +584 -0
  200. package/issues/tracker.ts +271 -0
  201. package/issues/worker.ts +1065 -0
  202. package/lib/dependencies.ts +61 -0
  203. package/lib/forge-auth.ts +953 -0
  204. package/lib/forge.ts +141 -0
  205. package/lib/gitea.ts +709 -0
  206. package/lib/github-release.ts +318 -0
  207. package/lib/inventory.test.ts +79 -0
  208. package/lib/inventory.ts +53 -0
  209. package/lib/release.test.ts +269 -0
  210. package/lib/release.ts +801 -0
  211. package/omp/commands/adr-resolve.md +8 -0
  212. package/omp/commands/discover.md +6 -0
  213. package/omp/commands/epic-next.md +8 -0
  214. package/omp/commands/flow-change.md +8 -0
  215. package/omp/commands/flow-goal.md +8 -0
  216. package/omp/commands/flow-implement.md +8 -0
  217. package/omp/commands/flow-widget.md +8 -0
  218. package/omp/commands/goal-draft.md +8 -0
  219. package/omp/commands/goal-outcome.md +8 -0
  220. package/omp/commands/implement-issue.md +8 -0
  221. package/omp/commands/prepare-skills.md +6 -0
  222. package/omp/commands/scout-recommendations.md +8 -0
  223. package/omp/commands/setup-devboard.md +24 -0
  224. package/omp/config.yml +12 -0
  225. package/omp/extensions/implement-goal-issue.ts +2 -0
  226. package/omp/extensions/implement-next-epic.ts +2 -0
  227. package/omp/mcp.json +9 -0
  228. package/package.json +86 -7
  229. package/prompts/adr-resolve.md +130 -0
  230. package/prompts/discover.md +541 -0
  231. package/prompts/epic-next.md +533 -0
  232. package/prompts/flow-change.md +76 -0
  233. package/prompts/flow-goal.md +126 -0
  234. package/prompts/flow-implement.md +103 -0
  235. package/prompts/flow-widget.md +54 -0
  236. package/prompts/goal-draft.md +147 -0
  237. package/prompts/goal-outcome.md +87 -0
  238. package/prompts/implement-issue.md +66 -0
  239. package/prompts/prepare-skills.md +142 -0
  240. package/prompts/scout-recommendations.md +143 -0
  241. package/public/apple-touch-icon.png +0 -0
  242. package/public/favicon.ico +0 -0
  243. package/public/favicon.svg +6 -0
  244. package/server/acp.ts +540 -0
  245. package/server/activity.test.ts +96 -0
  246. package/server/activity.ts +48 -0
  247. package/server/app.test.ts +882 -0
  248. package/server/app.ts +6179 -0
  249. package/server/ask.test.ts +543 -0
  250. package/server/ask.ts +589 -0
  251. package/server/dependencies.test.ts +118 -0
  252. package/server/discover-remote.test.ts +1602 -0
  253. package/server/discover-sheet.test.ts +176 -0
  254. package/server/discover.test.ts +1510 -0
  255. package/server/discover.ts +3198 -0
  256. package/server/feedback.test.ts +458 -0
  257. package/server/feedback.ts +369 -0
  258. package/server/fixtures/adr-source.ts +71 -0
  259. package/server/fixtures/hang-download.ts +14 -0
  260. package/server/fixtures/stub-agent.ts +1870 -0
  261. package/server/fixtures/stub-forge.ts +141 -0
  262. package/server/fixtures/stub-gh.ts +57 -0
  263. package/server/flow-pr.test.ts +976 -0
  264. package/server/flow.test.ts +1632 -0
  265. package/server/flow.ts +2655 -0
  266. package/server/forge-pr.ts +387 -0
  267. package/server/goal-creation.test.ts +957 -0
  268. package/server/goal-draft.test.ts +348 -0
  269. package/server/goal-drafts.ts +449 -0
  270. package/server/goal-proposal.test.ts +479 -0
  271. package/server/goal-proposal.ts +232 -0
  272. package/server/goal-revision.test.ts +613 -0
  273. package/server/goal-slug.test.ts +56 -0
  274. package/server/goal-slug.ts +96 -0
  275. package/server/goal.ts +20 -0
  276. package/server/index.ts +147 -0
  277. package/server/issue-close.test.ts +100 -0
  278. package/server/issue-close.ts +178 -0
  279. package/server/issues.test.ts +167 -0
  280. package/server/issues.ts +479 -0
  281. package/server/journal.test.ts +350 -0
  282. package/server/journal.ts +329 -0
  283. package/server/lifecycle-pause.test.ts +1795 -0
  284. package/server/lifecycle.test.ts +1879 -0
  285. package/server/parallel-lifecycle.test.ts +1029 -0
  286. package/server/persistence-restart.test.ts +723 -0
  287. package/server/persistence.test.ts +1935 -0
  288. package/server/records.test.ts +180 -0
  289. package/server/records.ts +99 -0
  290. package/server/reset-implementation.test.ts +997 -0
  291. package/server/run.test.ts +611 -0
  292. package/server/scout.test.ts +877 -0
  293. package/server/scout.ts +459 -0
  294. package/server/skills-preparation.test.ts +1073 -0
  295. package/server/state.ts +249 -0
  296. package/server/tracker.ts +369 -0
  297. package/server/updates.test.ts +444 -0
  298. package/server/updates.ts +198 -0
  299. package/server/version.test.ts +140 -0
  300. package/server/version.ts +151 -0
  301. package/server/watchdog.ts +389 -0
  302. package/svelte.config.js +5 -0
  303. package/templates/host-AGENTS.md +235 -0
  304. package/tsconfig.json +21 -0
  305. package/vite.config.ts +21 -0
  306. package/watchdog/cleanup-main.ts +98 -0
  307. package/watchdog/cleanup.ts +301 -0
  308. package/watchdog/collect.ts +311 -0
  309. package/watchdog/coordinator.ts +397 -0
  310. package/watchdog/delivery.ts +107 -0
  311. package/watchdog/discovery.ts +209 -0
  312. package/watchdog/findings.ts +211 -0
  313. package/watchdog/github.ts +189 -0
  314. package/watchdog/history.ts +138 -0
  315. package/watchdog/install.ts +103 -0
  316. package/watchdog/ledger.ts +188 -0
  317. package/watchdog/lock.ts +63 -0
  318. package/watchdog/main.ts +106 -0
  319. package/watchdog/status.ts +71 -0
  320. package/watchdog/systemd.ts +96 -0
  321. package/watchdog/uninstall.ts +30 -0
  322. package/watchdog/units/devboard-watchdog.service +9 -0
  323. package/watchdog/units/devboard-watchdog.timer +10 -0
@@ -0,0 +1,898 @@
1
+ <script lang="ts">
2
+ import {
3
+ COMMAND_LABELS,
4
+ HIDDEN_LEAVES,
5
+ humanize,
6
+ implementedTiers,
7
+ isLeaf,
8
+ leafLines,
9
+ pyramidHappyPathCount,
10
+ pyramidTiers,
11
+ pyramidTestCount,
12
+ recordItems,
13
+ relativeTime,
14
+ scalarText,
15
+ sectionEntries,
16
+ sectionLeaf,
17
+ unknownText,
18
+ } from "../discover-format";
19
+ import { onMount } from "svelte";
20
+ import { connection } from "../connection.svelte";
21
+ import { dependencyChart, dependencyEdges } from "../dependency-diagram";
22
+ import { discover } from "../discover.svelte";
23
+ import type { Leaf } from "../types";
24
+ import DataTable from "./DataTable.svelte";
25
+ import MermaidDiagram from "./MermaidDiagram.svelte";
26
+ import Terminal from "./Terminal.svelte";
27
+ import Button from "./ui/button.svelte";
28
+ import Input from "./ui/input.svelte";
29
+
30
+ const COMMAND_TOTAL = Object.keys(COMMAND_LABELS).length;
31
+ const COMMAND_COLUMNS = [
32
+ { id: "label", header: "Task", accessorKey: "label" },
33
+ { id: "value", header: "Command", accessorKey: "value" },
34
+ ];
35
+ const COMMAND_WIDTHS = { label: "96px" };
36
+ const LEVEL_COLUMNS = [
37
+ { id: "level", header: "Level", accessorKey: "level" },
38
+ { id: "tests", header: "Tests", accessorKey: "tests" },
39
+ { id: "happyPath", header: "Happy path", accessorKey: "happyPath" },
40
+ { id: "command", header: "Command", accessorKey: "command" },
41
+ ];
42
+ const QUALITY_GATE_COLUMNS = [
43
+ { id: "gate", header: "Quality gate", accessorKey: "gate" },
44
+ { id: "checks", header: "What it checks", accessorKey: "checks" },
45
+ ];
46
+ const QUALITY_GATE_WIDTHS = { gate: "220px" };
47
+ const BOUNDARY_GROUPS = [
48
+ { key: "sharedKernels", label: "Shared kernels", kind: "chips" },
49
+ { key: "externalBoundaries", label: "External boundaries", kind: "chips" },
50
+ { key: "dependencyDirection", label: "Dependency direction", kind: "sentence" },
51
+ ] as const;
52
+
53
+ type BoundaryGroup = (typeof BOUNDARY_GROUPS)[number];
54
+
55
+ interface ContextRow {
56
+ name: string;
57
+ path: string;
58
+ responsibility: string;
59
+ entryPoints: string[];
60
+ }
61
+
62
+ interface BoundaryRow {
63
+ key: BoundaryGroup["key"];
64
+ label: string;
65
+ kind: BoundaryGroup["kind"];
66
+ values: string[];
67
+ }
68
+
69
+ const MONO_FIELDS = new Set(["artifact", "defaultBranch", "manifests"]);
70
+
71
+ const sheet = $derived(discover.sheet);
72
+ const freshness = $derived(discover.freshness);
73
+ const definition = $derived(sectionLeaf(sheet, "definition"));
74
+ const definitionText = $derived(
75
+ definition === undefined ? "" : definition.confidence === "unknown"
76
+ ? unknownText(definition)
77
+ : leafText(definition),
78
+ );
79
+ const projectPath = $derived(
80
+ discover.remoteTarget?.url ??
81
+ (freshness?.project ?? (connection.projectPath.trim().length > 0 ? connection.projectPath : "")),
82
+ );
83
+ const projectName = $derived(discover.repository ?? (baseName(projectPath) || "Discover"));
84
+
85
+ const commandsSection = $derived(
86
+ typeof sheet?.commands === "object" && sheet.commands !== null
87
+ ? (sheet.commands as Record<string, unknown>)
88
+ : null,
89
+ );
90
+ let commandsOpen = $state(false);
91
+ /** The retained dropdown; a host target leaves it with no option chosen. */
92
+ let remoteSelect = $state<HTMLSelectElement | null>(null);
93
+ $effect(() => {
94
+ const select = remoteSelect;
95
+ if (select === null) return;
96
+ const active = discover.remoteTarget?.url ?? "";
97
+ select.selectedIndex = discover.retained.findIndex((project) => project.url === active);
98
+ });
99
+ const foundCommandRows = $derived(
100
+ commandRowsOf(commandsSection, discover.testCommand, { includeMissing: false }),
101
+ );
102
+ const commandRows = $derived(commandsOpen
103
+ ? commandRowsOf(commandsSection, discover.testCommand, { includeMissing: true })
104
+ : foundCommandRows);
105
+ const recordedTestCommand = $derived.by(() => {
106
+ const leaf = sectionLeaf(commandsSection, "test");
107
+ return leaf === undefined || leaf.confidence === "unknown" ? "" : scalarText(leaf.value);
108
+ });
109
+
110
+ const testingSection = $derived(
111
+ typeof sheet?.testing === "object" && sheet.testing !== null ? sheet.testing : null,
112
+ );
113
+ const levels = $derived(sectionLeaf(testingSection, "levels"));
114
+ const recordedTiers = $derived(pyramidTiers(levels));
115
+ const tiers = $derived(implementedTiers(recordedTiers));
116
+ const testTotal = $derived(pyramidTestCount(tiers));
117
+ const happyPath = $derived(pyramidHappyPathCount(tiers));
118
+ const levelRows = $derived(
119
+ tiers.map((tier) => {
120
+ const parsed = Number.parseInt(tier.tests, 10);
121
+ const tests = Number.isFinite(parsed) ? parsed : 0;
122
+ const parsedHappy = Number.parseInt(tier.happyPath, 10);
123
+ const happyLabel = Number.isFinite(parsedHappy) ? `${parsedHappy}` : "—";
124
+ return {
125
+ level: humanize(tier.level),
126
+ tests: `${tests} ${tests === 1 ? "test" : "tests"}`,
127
+ happyPath: happyLabel,
128
+ command: tier.command.length > 0 ? discover.aliasFor(tier.command) : "",
129
+ rawLevel: tier.level,
130
+ };
131
+ }),
132
+ );
133
+
134
+ const qualityGatesLeaf = $derived(sectionLeaf(sheet?.ci, "qualityGates"));
135
+ const qualityGateRows = $derived(
136
+ recordItems(qualityGatesLeaf)
137
+ .map((record) => ({
138
+ gate: scalarText(record.name).trim(),
139
+ checks: scalarText(record.checks).trim(),
140
+ }))
141
+ .filter((row) => row.gate.length > 0),
142
+ );
143
+ const hasQualityGates = $derived(
144
+ qualityGatesLeaf !== undefined &&
145
+ (qualityGatesLeaf.confidence === "unknown" || qualityGateRows.length > 0),
146
+ );
147
+
148
+ const runtimeSection = $derived(visibleSection(sheet?.runtime, HIDDEN_LEAVES.runtime ?? []));
149
+ let now = $state(Date.now());
150
+ const startedLabel = $derived(
151
+ discover.startedAt === null ? "" : relativeTime(discover.startedAt, now),
152
+ );
153
+
154
+ onMount(() => {
155
+ const timer = setInterval(() => (now = Date.now()), 30_000);
156
+ return () => clearInterval(timer);
157
+ });
158
+
159
+ const deploymentSection = $derived(
160
+ visibleSection(sheet?.deployment, HIDDEN_LEAVES.deployment ?? []),
161
+ );
162
+ const ciSection = $derived(visibleSection(sheet?.ci, HIDDEN_LEAVES.ci ?? []));
163
+ const branchingSection = $derived(visibleSection(sheet?.branching, []));
164
+ const hasDelivery =
165
+ $derived(
166
+ Object.keys(deploymentSection).length > 0 ||
167
+ Object.keys(ciSection).length > 0 ||
168
+ Object.keys(branchingSection).length > 0,
169
+ );
170
+
171
+ const stackLeaf = $derived(sectionLeaf(sheet, "stack"));
172
+ const stackLines = $derived(
173
+ stackLeaf === undefined || stackLeaf.confidence === "unknown" ? [] : leafLines(stackLeaf.value),
174
+ );
175
+
176
+ const architectureSection = $derived(sheet?.architecture);
177
+ const contextsLeaf = $derived(sectionLeaf(architectureSection, "contexts"));
178
+ const contextRows: ContextRow[] = $derived(
179
+ recordItems(contextsLeaf)
180
+ .map((record) => {
181
+ const path = scalarText(record.path);
182
+ const entryPoints = Array.isArray(record.entryPoints)
183
+ ? record.entryPoints.map((entry) => scalarText(entry).trim()).filter(Boolean)
184
+ : [];
185
+ return {
186
+ name: scalarText(record.name).trim(),
187
+ path,
188
+ responsibility: scalarText(record.responsibility).trim(),
189
+ entryPoints: [...new Set(entryPoints)].filter((entry) => entry !== path),
190
+ };
191
+ })
192
+ .filter((row) => row.name.length > 0 || row.path.length > 0),
193
+ );
194
+ const dependencyDiagram = $derived.by(() => {
195
+ const leaf = sectionLeaf(architectureSection, "dependencies");
196
+ if (leaf === undefined || leaf.confidence === "unknown") return null;
197
+ return dependencyChart(
198
+ contextRows.map((row) => row.name),
199
+ dependencyEdges(leaf.value),
200
+ );
201
+ });
202
+ const boundaryRows: BoundaryRow[] = $derived.by(() => {
203
+ const rows: BoundaryRow[] = [];
204
+ for (const group of BOUNDARY_GROUPS) {
205
+ const leaf = sectionLeaf(architectureSection, group.key);
206
+ if (leaf === undefined || leaf.confidence === "unknown") continue;
207
+ const values = boundaryValues(leaf, group.kind);
208
+ if (values.length === 0) continue;
209
+ rows.push({ key: group.key, label: group.label, kind: group.kind, values });
210
+ }
211
+ return rows;
212
+ });
213
+ const unknownBoundaryGroups = $derived.by(() => {
214
+ const labels: string[] = [];
215
+ const reasons = new Set<string>();
216
+ for (const group of BOUNDARY_GROUPS) {
217
+ const leaf = sectionLeaf(architectureSection, group.key);
218
+ if (leaf === undefined || leaf.confidence !== "unknown") continue;
219
+ labels.push(group.label.toLowerCase());
220
+ if (typeof leaf.reason === "string" && leaf.reason.length > 0) reasons.add(leaf.reason);
221
+ }
222
+ return { labels, reasons: [...reasons] };
223
+ });
224
+
225
+ function baseName(value: string): string {
226
+ const trimmed = value.replace(/[\\/]+$/, "");
227
+ const parts = trimmed.split(/[\\/]+/);
228
+ return parts[parts.length - 1] ?? trimmed;
229
+ }
230
+
231
+ function leafText(leaf: Leaf): string {
232
+ return leafLines(leaf.value).join(", ");
233
+ }
234
+
235
+ function boundaryValues(leaf: Leaf, kind: BoundaryGroup["kind"]): string[] {
236
+ if (kind === "sentence") {
237
+ const text = scalarText(leaf.value).trim();
238
+ return text.length > 0 && text.toLowerCase() !== "none detected" ? [text] : [];
239
+ }
240
+ if (!Array.isArray(leaf.value)) return [];
241
+ if (leaf.value.length === 0) return ["None detected."];
242
+ const values = leaf.value.map((value) => scalarText(value).trim()).filter(Boolean);
243
+ return [...new Set(values)].filter((value) => value.toLowerCase() !== "none detected");
244
+ }
245
+
246
+ function visibleSection(section: unknown, hidden: string[]): Record<string, unknown> {
247
+ if (typeof section !== "object" || section === null) return {};
248
+ return Object.fromEntries(
249
+ Object.entries(section).filter(([key, entry]) => !hidden.includes(key) && isLeaf(entry)),
250
+ );
251
+ }
252
+
253
+ function commandRowsOf(
254
+ section: Record<string, unknown> | null,
255
+ testCommand: string,
256
+ options: { includeMissing: boolean },
257
+ ): Array<Record<string, unknown>> {
258
+ if (section === null) return [];
259
+ const rows: Array<Record<string, unknown>> = [];
260
+ for (const [key, label] of Object.entries(COMMAND_LABELS)) {
261
+ const leaf = section[key];
262
+ const known = isLeaf(leaf) && leaf.confidence !== "unknown";
263
+ if (known) {
264
+ const command = discover.aliasFor(scalarText(leaf.value));
265
+ // A try-out that only aliases the suite is the same command, not a gap.
266
+ if (key === "tryOut" && testCommand.length > 0 && command === testCommand) continue;
267
+ rows.push({ key, label, value: command, missing: false });
268
+ continue;
269
+ }
270
+ if (!options.includeMissing) continue;
271
+ rows.push({
272
+ key,
273
+ label,
274
+ value: "",
275
+ missing: true,
276
+ reason: isLeaf(leaf) && leaf.confidence === "unknown"
277
+ ? unknownText(leaf)
278
+ : "No command was recorded.",
279
+ });
280
+ }
281
+ return rows;
282
+ }
283
+
284
+ function discoveredLabel(value: string): string {
285
+ const date = new Date(value);
286
+ if (Number.isNaN(date.getTime())) return value;
287
+ return new Intl.DateTimeFormat(undefined, {
288
+ dateStyle: "medium",
289
+ timeStyle: "short",
290
+ }).format(date);
291
+ }
292
+
293
+ function knownFacts(section: Record<string, unknown>): Array<[string, Leaf]> {
294
+ return sectionEntries(section, []).filter(([, leaf]) => leaf.confidence !== "unknown");
295
+ }
296
+ </script>
297
+
298
+ {#snippet startControl()}
299
+ <Button
300
+ variant="default"
301
+ data-command="discover.start"
302
+ data-testid="discover-start"
303
+ disabled={discover.startDisabled}
304
+ title={discover.startTitle}
305
+ onclick={() => discover.start()}
306
+ >
307
+ {discover.hasArtifacts ? "Re-discover" : "Discover project"}
308
+ </Button>
309
+ {/snippet}
310
+
311
+ {#snippet questionPanel()}
312
+ {#if discover.question !== null}
313
+ <section class="panel discover-question" data-testid="discover-question">
314
+ <div class="panel__header">
315
+ <h2 class="panel__title">The crawl needs a decision</h2>
316
+ <span class="panel__subtitle">Answered once</span>
317
+ </div>
318
+ <div class="panel__body stack-sm">
319
+ <p class="discover-question__message">{discover.question.message}</p>
320
+ {#if discover.question.options.length > 0}
321
+ <div class="choice-list">
322
+ {#each discover.question.options as option (option.label)}
323
+ <button
324
+ type="button"
325
+ class="choice"
326
+ data-testid="discover-choice"
327
+ data-command="discover.answer.submit"
328
+ onclick={() => discover.answer(option.label)}
329
+ >
330
+ <span class="choice__label">
331
+ {option.label}
332
+ {#if option.recommended}
333
+ <span class="choice__recommended">Recommended</span>
334
+ {/if}
335
+ </span>
336
+ {#if option.description}
337
+ <span class="choice__description">{option.description}</span>
338
+ {/if}
339
+ </button>
340
+ {/each}
341
+ </div>
342
+ {/if}
343
+ <form
344
+ class="discover-answer"
345
+ onsubmit={(event) => {
346
+ event.preventDefault();
347
+ discover.answer(discover.answerDraft);
348
+ }}
349
+ >
350
+ <Input
351
+ id="discover-answer-input"
352
+ type="text"
353
+ value={discover.answerDraft}
354
+ oninput={(event) => (discover.answerDraft = event.currentTarget.value)}
355
+ placeholder="Your answer"
356
+ aria-label="Your answer"
357
+ data-testid="discover-answer-input"
358
+ />
359
+ <Button
360
+ variant="default"
361
+ type="submit"
362
+ data-command="discover.answer.submit"
363
+ data-testid="discover-answer"
364
+ disabled={discover.answerDraft.trim().length === 0}
365
+ >
366
+ Answer
367
+ </Button>
368
+ </form>
369
+ </div>
370
+ </section>
371
+ {/if}
372
+ {/snippet}
373
+
374
+ {#snippet factList(section: Record<string, unknown>)}
375
+ {@const facts = knownFacts(section)}
376
+ {#if facts.length === 0}
377
+ <p class="empty">Not observed.</p>
378
+ {:else}
379
+ <dl class="discover-facts">
380
+ {#each facts as [key, leaf] (key)}
381
+ <div class="discover-fact">
382
+ <dt>{humanize(key)}</dt>
383
+ <dd class:discover-mono={MONO_FIELDS.has(key)}>{leafText(leaf)}</dd>
384
+ </div>
385
+ {/each}
386
+ </dl>
387
+ {/if}
388
+ {/snippet}
389
+
390
+ {#snippet chipList(lines: string[])}
391
+ <div class="discover-chips">
392
+ {#each lines as line, index (index)}
393
+ {@const href = discover.repositoryLink(line)}
394
+ {#if href === null}
395
+ <code class="code-chip">{line}</code>
396
+ {:else}
397
+ <a
398
+ class="code-chip code-chip--link"
399
+ href={href}
400
+ target="_blank"
401
+ rel="noreferrer noopener"
402
+ title="Open the repository"
403
+ >
404
+ {line}
405
+ </a>
406
+ {/if}
407
+ {/each}
408
+ </div>
409
+ {/snippet}
410
+
411
+ {#snippet plainChips(lines: string[])}
412
+ <div class="discover-chips">
413
+ {#each lines as line (line)}
414
+ <code class="code-chip">{line}</code>
415
+ {/each}
416
+ </div>
417
+ {/snippet}
418
+
419
+ {#snippet levelCell(row: Record<string, unknown>, columnId: string)}
420
+ {#if columnId === "command"}
421
+ {#if String(row.command ?? "").length > 0}
422
+ <code class="code-chip">$ {String(row.command)}</code>
423
+ {:else}
424
+ <span class="muted">—</span>
425
+ {/if}
426
+ {:else}
427
+ {String(row[columnId] ?? "")}
428
+ {/if}
429
+ {/snippet}
430
+
431
+ {#snippet qualityGateCell(row: Record<string, unknown>, columnId: string)}
432
+ {#if columnId === "gate"}
433
+ <span class="discover-quality-gate__name">{String(row.gate ?? "")}</span>
434
+ {:else if String(row.checks ?? "").length > 0}
435
+ <span class="discover-quality-gate__checks">{String(row.checks)}</span>
436
+ {:else}
437
+ <span class="muted">No check was recorded.</span>
438
+ {/if}
439
+ {/snippet}
440
+
441
+ {#snippet commandCell(row: Record<string, unknown>, columnId: string)}
442
+ {#if columnId === "value"}
443
+ {#if row.missing === true}
444
+ <span class="muted discover-command-missing" title={String(row.reason ?? "")}>not found</span>
445
+ {:else}
446
+ <code class="code-chip">$ {String(row.value ?? "")}</code>
447
+ {/if}
448
+ {:else}
449
+ <span class="discover-command-label" class:muted={row.missing === true}>
450
+ {String(row.label ?? "")}
451
+ </span>
452
+ {/if}
453
+ {/snippet}
454
+
455
+ {#snippet sheetRail()}
456
+ <div class="discover-rail">
457
+ <section class="discover-block" data-testid="discover-card" data-field="runtime">
458
+ <div class="discover-block__head">
459
+ <h2 class="discover-block__title">Platform</h2>
460
+ </div>
461
+ {@render factList(runtimeSection)}
462
+ </section>
463
+
464
+ {#if hasDelivery}
465
+ <section class="discover-block" data-testid="discover-card" data-field="delivery">
466
+ <div class="discover-block__head">
467
+ <h2 class="discover-block__title">Delivery</h2>
468
+ </div>
469
+ {#if Object.keys(deploymentSection).length > 0}
470
+ <div class="discover-subblock">
471
+ <h3 class="discover-subblock__title">Deployment</h3>
472
+ {@render factList(deploymentSection)}
473
+ </div>
474
+ {/if}
475
+ {#if Object.keys(ciSection).length > 0}
476
+ <div class="discover-subblock">
477
+ <h3 class="discover-subblock__title">CI</h3>
478
+ {@render factList(ciSection)}
479
+ </div>
480
+ {/if}
481
+ {#if Object.keys(branchingSection).length > 0}
482
+ <div class="discover-subblock">
483
+ <h3 class="discover-subblock__title">Branching</h3>
484
+ {@render factList(branchingSection)}
485
+ </div>
486
+ {/if}
487
+ </section>
488
+ {/if}
489
+
490
+ {#if stackLines.length > 0}
491
+ <section class="discover-block" data-testid="discover-card" data-field="stack">
492
+ <div class="discover-block__head">
493
+ <h2 class="discover-block__title">Stack</h2>
494
+ <span class="discover-block__meta">{stackLines.length} entries</span>
495
+ </div>
496
+ {@render chipList(stackLines)}
497
+ </section>
498
+ {/if}
499
+ </div>
500
+ {/snippet}
501
+
502
+ <div
503
+ class="scroll-column discover-page"
504
+ data-testid="discover-panel"
505
+ data-tab="overview"
506
+ aria-label="Discover overview"
507
+ >
508
+ {#snippet remotePanel()}
509
+ {#if discover.remoteAvailable}
510
+ <section class="panel discover-remote" data-testid="discover-remote" aria-label="Discover remote target">
511
+ <div class="panel__header">
512
+ <h2 class="panel__title">Manual testing target</h2>
513
+ <span class="panel__subtitle">Source checkout only</span>
514
+ </div>
515
+ <div class="panel__body stack-sm">
516
+ {#if discover.remoteTarget !== null}
517
+ <p class="row-sm" data-testid="discover-remote-target">
518
+ <span class="badge" data-tone="warning">Manual testing</span>
519
+ <span>Discover reads <code>{discover.remoteTarget.url}</code>.</span>
520
+ </p>
521
+ {/if}
522
+ {#if discover.retained.length > 0}
523
+ <div class="discover-remote__retained">
524
+ <label class="discover-remote__label" for="discover-remote-select">Retained project</label>
525
+ <select
526
+ id="discover-remote-select"
527
+ class="discover-remote__select"
528
+ data-testid="discover-remote-select"
529
+ aria-label="Retained remote project"
530
+ bind:this={remoteSelect}
531
+ disabled={discover.remoteChangeDisabled}
532
+ title={discover.remoteChangeTitle}
533
+ onchange={(event) => discover.switchRemote(event.currentTarget.value)}
534
+ >
535
+ {#each discover.retained as project (project.url)}
536
+ <option value={project.url}>{project.url}</option>
537
+ {/each}
538
+ </select>
539
+ </div>
540
+ <ul class="discover-remote__list" aria-label="Retained projects">
541
+ {#each discover.retained as project (project.url)}
542
+ <li
543
+ class="discover-remote__item"
544
+ class:discover-remote__item--active={project.url === discover.remoteTarget?.url}
545
+ >
546
+ <span class="discover-remote__url">{project.url}</span>
547
+ <Button
548
+ variant="ghost"
549
+ size="sm"
550
+ data-testid="discover-remote-remove"
551
+ data-url={project.url}
552
+ disabled={discover.remoteChangeDisabled}
553
+ title={discover.remoteChangeTitle}
554
+ onclick={() => discover.removeRemote(project.url)}
555
+ >
556
+ Remove
557
+ </Button>
558
+ </li>
559
+ {/each}
560
+ </ul>
561
+ {/if}
562
+ <form
563
+ class="discover-remote__form"
564
+ onsubmit={(event) => {
565
+ event.preventDefault();
566
+ discover.startRemote();
567
+ }}
568
+ >
569
+ <Input
570
+ type="url"
571
+ aria-label="GitHub repository URL"
572
+ placeholder="https://github.com/owner/repo"
573
+ autocomplete="off"
574
+ spellcheck="false"
575
+ value={discover.remoteUrl}
576
+ oninput={(event) => (discover.remoteUrl = event.currentTarget.value)}
577
+ data-testid="discover-remote-url"
578
+ />
579
+ <Button
580
+ variant="outline"
581
+ type="submit"
582
+ data-testid="discover-remote-start"
583
+ disabled={discover.remoteStartDisabled}
584
+ title={discover.remoteStartTitle}
585
+ >
586
+ Discover from URL
587
+ </Button>
588
+ {#if discover.remoteTarget !== null}
589
+ <Button
590
+ variant="ghost"
591
+ data-testid="discover-remote-clear"
592
+ disabled={discover.remoteChangeDisabled}
593
+ title={discover.remoteChangeTitle}
594
+ onclick={() => discover.clearRemote()}
595
+ >
596
+ Clear remote target
597
+ </Button>
598
+ {/if}
599
+ </form>
600
+ {#if discover.cloning !== null}
601
+ <p class="muted" data-testid="discover-remote-cloning">
602
+ Cloning <code>{discover.cloning.url}</code>…
603
+ </p>
604
+ {/if}
605
+ </div>
606
+ </section>
607
+ {/if}
608
+ {/snippet}
609
+
610
+ {@render remotePanel()}
611
+
612
+ <header class="discover-head" data-testid="discover-card" data-field="definition">
613
+ <div class="discover-head__top">
614
+ <div class="discover-head__identity">
615
+ <h1 class="discover-head__title" title={projectPath}>{projectName}</h1>
616
+ </div>
617
+ <div class="discover-head__actions">
618
+ {#if discover.busy !== null}
619
+ <span class="muted">{discover.busy}</span>
620
+ {/if}
621
+ {@render startControl()}
622
+ </div>
623
+ </div>
624
+
625
+ {#if discover.hasArtifacts}
626
+ <p class="discover-head__definition">{definitionText}</p>
627
+ {#if freshness !== null}
628
+ <div class="discover-head__meta" data-testid="discover-freshness">
629
+ <span>{discoveredLabel(freshness.discoveredAt)}</span>
630
+ </div>
631
+ {/if}
632
+ {:else}
633
+ <p class="discover-head__definition">
634
+ Inventory this project from repository evidence: stack, commands, testing, architecture, and
635
+ the conventions an agent should follow here.
636
+ </p>
637
+ {/if}
638
+ </header>
639
+
640
+ {#if discover.active}
641
+ <section class="panel discover-run" data-testid="discover-active">
642
+ <div class="panel__header discover-run__header">
643
+ <div class="discover-run__heading">
644
+ <span class="discover-run__dot" aria-hidden="true"></span>
645
+ <h2 class="panel__title">Run in progress</h2>
646
+ </div>
647
+ <div class="discover-run__meta">
648
+ {#if discover.startedAt !== null}
649
+ <span class="muted discover-run__started" title={discover.startedAt}>
650
+ Started {startedLabel}
651
+ </span>
652
+ {/if}
653
+ <button
654
+ type="button"
655
+ class="discover-run__stop"
656
+ title="Stop this Discover run"
657
+ data-command="discover.stop"
658
+ data-testid="discover-stop"
659
+ onclick={() => discover.stop()}
660
+ >
661
+ Stop
662
+ </button>
663
+ </div>
664
+ </div>
665
+ <div class="discover-run__body">
666
+ <div class="discover-run__terminal">
667
+ <Terminal lines={discover.activity} data-testid="discover-activity" label="Discover activity" />
668
+ </div>
669
+ </div>
670
+ </section>
671
+ {/if}
672
+
673
+ {@render questionPanel()}
674
+
675
+ {#if discover.hasArtifacts && sheet !== null}
676
+ <div class="discover-sheet" data-testid="discover-sheet">
677
+ {@render sheetRail()}
678
+ <div class="discover-col">
679
+ {#if commandsSection !== null}
680
+ <section class="panel discover-card" data-testid="discover-card" data-field="commands">
681
+ <div class="panel__header">
682
+ <h2 class="panel__title">Commands</h2>
683
+ <button
684
+ type="button"
685
+ class="discover-coverage"
686
+ data-testid="discover-command-coverage"
687
+ aria-expanded={commandsOpen}
688
+ aria-controls="discover-command-table"
689
+ title={commandsOpen
690
+ ? "Hide the commands that were not found"
691
+ : "Show every command slot, including the ones that were not found"}
692
+ onclick={() => (commandsOpen = !commandsOpen)}
693
+ >
694
+ {foundCommandRows.length}
695
+ of {COMMAND_TOTAL} found
696
+ </button>
697
+ </div>
698
+ <div class="panel__body">
699
+ {#if isLeaf(commandsSection)}
700
+ <p class="empty">{unknownText(commandsSection)}</p>
701
+ {:else}
702
+ <div id="discover-command-table">
703
+ <DataTable
704
+ data-testid="discover-command-list"
705
+ columns={COMMAND_COLUMNS}
706
+ rows={commandRows}
707
+ rowAttrs={(row) => ({
708
+ "data-command-key": String(row.key ?? ""),
709
+ "data-found": row.missing === true ? "false" : "true",
710
+ })}
711
+ cell={commandCell}
712
+ hideHeader
713
+ columnWidths={COMMAND_WIDTHS}
714
+ full
715
+ wrap
716
+ />
717
+ </div>
718
+ {/if}
719
+ </div>
720
+ </section>
721
+ {/if}
722
+
723
+ {#if testingSection !== null}
724
+ <section class="panel discover-card" data-testid="discover-card" data-field="testing">
725
+ <div class="panel__header">
726
+ <h2 class="panel__title">Testing</h2>
727
+ <span class="panel__subtitle">
728
+ {tiers.length} {tiers.length === 1 ? "level" : "levels"} · {testTotal ?? 0}
729
+ {(testTotal ?? 0) === 1 ? "test" : "tests"}
730
+ </span>
731
+ </div>
732
+ <div class="panel__body">
733
+ {#if isLeaf(testingSection)}
734
+ <p class="empty">{unknownText(testingSection)}</p>
735
+ {:else if levels === undefined}
736
+ <p class="empty">No test levels were detected.</p>
737
+ {:else if levels.confidence === "unknown"}
738
+ <p class="empty">{unknownText(levels)}</p>
739
+ {:else if tiers.length === 0}
740
+ <p class="empty">
741
+ {recordedTiers.length === 0
742
+ ? "No test level was recorded."
743
+ : "No recorded level carries a test."}
744
+ </p>
745
+ {:else}
746
+ <div class="discover-test-summary">
747
+ <div class="discover-stats" data-testid="discover-test-summary">
748
+ <div class="discover-stat">
749
+ <span class="discover-stat__value">{testTotal ?? 0}</span>
750
+ <span class="discover-stat__label">
751
+ {(testTotal ?? 0) === 1 ? "test" : "tests"}
752
+ </span>
753
+ </div>
754
+ <div class="discover-stat">
755
+ <span class="discover-stat__value">{tiers.length}</span>
756
+ <span class="discover-stat__label">
757
+ {tiers.length === 1 ? "level" : "levels"}
758
+ </span>
759
+ </div>
760
+ {#if happyPath !== null}
761
+ <div class="discover-stat">
762
+ <span class="discover-stat__value">
763
+ {Math.round((happyPath.happy / happyPath.tests) * 100)}%
764
+ </span>
765
+ <span class="discover-stat__label">happy path</span>
766
+ </div>
767
+ {/if}
768
+ </div>
769
+ {#if discover.testCommand.length > 0}
770
+ <div class="discover-command-line" data-testid="discover-suite-command">
771
+ <span class="muted">Whole suite</span>
772
+ <code class="code-chip" title={recordedTestCommand === discover.testCommand
773
+ ? undefined
774
+ : recordedTestCommand}>$ {discover.testCommand}</code>
775
+ </div>
776
+ {/if}
777
+ </div>
778
+ <DataTable
779
+ data-testid="discover-pyramid"
780
+ columns={LEVEL_COLUMNS}
781
+ rows={levelRows}
782
+ rowAttrs={(row) => ({ "data-level": String(row.rawLevel ?? "") })}
783
+ cell={levelCell}
784
+ full
785
+ wrap
786
+ />
787
+ {/if}
788
+ </div>
789
+ </section>
790
+ {/if}
791
+
792
+ {#if hasQualityGates}
793
+ <section
794
+ class="panel discover-card"
795
+ data-testid="discover-card"
796
+ data-field="quality-gates"
797
+ >
798
+ <div class="panel__header">
799
+ <h2 class="panel__title">Quality gates</h2>
800
+ {#if qualityGatesLeaf !== undefined && qualityGatesLeaf.confidence !== "unknown"}
801
+ <span class="panel__subtitle">
802
+ {qualityGateRows.length}
803
+ {qualityGateRows.length === 1 ? "gate" : "gates"}
804
+ </span>
805
+ {/if}
806
+ </div>
807
+ <div class="panel__body">
808
+ {#if qualityGatesLeaf === undefined}
809
+ <p class="empty">No quality gate was recorded.</p>
810
+ {:else if qualityGatesLeaf.confidence === "unknown"}
811
+ <p class="empty">{unknownText(qualityGatesLeaf)}</p>
812
+ {:else}
813
+ <DataTable
814
+ data-testid="discover-quality-gates"
815
+ columns={QUALITY_GATE_COLUMNS}
816
+ rows={qualityGateRows}
817
+ rowAttrs={(row) => ({ "data-gate": String(row.gate ?? "") })}
818
+ cell={qualityGateCell}
819
+ columnWidths={QUALITY_GATE_WIDTHS}
820
+ full
821
+ wrap
822
+ />
823
+ {/if}
824
+ </div>
825
+ </section>
826
+ {/if}
827
+
828
+ {#if architectureSection !== undefined && typeof architectureSection === "object" && architectureSection !== null}
829
+ <section class="panel discover-card" data-testid="discover-card" data-field="architecture">
830
+ <div class="panel__header">
831
+ <h2 class="panel__title">Bounded contexts</h2>
832
+ </div>
833
+ <div class="panel__body">
834
+ {#if contextRows.length > 0}
835
+ <ul class="discover-contexts" data-testid="discover-contexts">
836
+ {#each contextRows as context, index (index)}
837
+ <li class="discover-context" data-context={context.name}>
838
+ <div class="discover-context__head">
839
+ <span class="discover-context__name">{context.name}</span>
840
+ </div>
841
+ {#if context.responsibility.length > 0}
842
+ <p class="discover-context__responsibility">{context.responsibility}</p>
843
+ {/if}
844
+ {#if context.entryPoints.length > 0}
845
+ <div class="discover-context__entries">
846
+ <span class="discover-context__label">Entry points</span>
847
+ {@render plainChips(context.entryPoints)}
848
+ </div>
849
+ {/if}
850
+ </li>
851
+ {/each}
852
+ </ul>
853
+ {:else if contextsLeaf !== undefined && contextsLeaf.confidence === "unknown"}
854
+ <p class="empty">{unknownText(contextsLeaf)}</p>
855
+ {:else}
856
+ <p class="empty">No bounded context was detected.</p>
857
+ {/if}
858
+
859
+ {#if boundaryRows.length > 0}
860
+ <dl class="discover-boundaries">
861
+ {#each boundaryRows as row (row.key)}
862
+ <div class="discover-boundary" data-role={row.key}>
863
+ <dt>{row.label}</dt>
864
+ <dd>
865
+ {#if row.kind === "chips"}
866
+ {@render plainChips(row.values)}
867
+ {:else}
868
+ {row.values[0]}
869
+ {/if}
870
+ </dd>
871
+ </div>
872
+ {/each}
873
+ </dl>
874
+ {/if}
875
+
876
+ {#if dependencyDiagram !== null}
877
+ <div class="discover-subblock" data-testid="discover-dependency-graph">
878
+ <h3 class="discover-subblock__title">Dependency graph</h3>
879
+ <MermaidDiagram
880
+ chart={dependencyDiagram}
881
+ label={`Dependency graph for ${projectName}`}
882
+ />
883
+ </div>
884
+ {/if}
885
+
886
+ {#if unknownBoundaryGroups.labels.length > 0}
887
+ <p class="discover-boundary-note">
888
+ Not observed: {unknownBoundaryGroups.labels.join(", ")}.
889
+ {unknownBoundaryGroups.reasons.join(" ")}
890
+ </p>
891
+ {/if}
892
+ </div>
893
+ </section>
894
+ {/if}
895
+ </div>
896
+ </div>
897
+ {/if}
898
+ </div>