@diagrammo/dgmo 0.46.0 → 0.48.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 (534) hide show
  1. package/dist/advanced.d.ts +3 -855
  2. package/dist/advanced.js +541 -70107
  3. package/dist/analyzer-UYKBXAKR.js +15 -0
  4. package/dist/auto.js +1 -1
  5. package/dist/auto.mjs +1 -1
  6. package/dist/block.js +83 -69417
  7. package/dist/calculator-LDYQCDDY.js +11 -0
  8. package/dist/charts-d3-QOZHKTCT.js +25 -0
  9. package/dist/chunk-2DXMZRWJ.js +800 -0
  10. package/dist/chunk-2LACINWN.js +786 -0
  11. package/dist/chunk-33LCUH4H.js +893 -0
  12. package/dist/chunk-3CIOJ5YJ.js +35 -0
  13. package/dist/chunk-3HDBAXF5.js +86 -0
  14. package/dist/chunk-3JLKYGBQ.js +290 -0
  15. package/dist/chunk-3MQQNXPD.js +415 -0
  16. package/dist/chunk-3R5POCTZ.js +967 -0
  17. package/dist/chunk-3SMGIJDD.js +112 -0
  18. package/dist/chunk-44CM72SQ.js +409 -0
  19. package/dist/chunk-4GRI7MEB.js +1604 -0
  20. package/dist/chunk-4OHACBGX.js +606 -0
  21. package/dist/chunk-5DH3KYFH.js +21 -0
  22. package/dist/chunk-5MLYVTYM.js +76 -0
  23. package/dist/chunk-5QXL4NYS.js +553 -0
  24. package/dist/chunk-5WRI5ZAA.js +31 -0
  25. package/dist/chunk-6EK55ART.js +117 -0
  26. package/dist/chunk-6FZYCKXB.js +2065 -0
  27. package/dist/chunk-6HO5ZTXK.js +277 -0
  28. package/dist/chunk-6KFWC4CO.js +7 -0
  29. package/dist/chunk-6MTWIQVO.js +121 -0
  30. package/dist/chunk-6NYWYNBI.js +329 -0
  31. package/dist/chunk-6SM5NJQH.js +620 -0
  32. package/dist/chunk-725LDASD.js +422 -0
  33. package/dist/chunk-76OWZ3BA.js +130 -0
  34. package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
  35. package/dist/chunk-A44USUV7.js +923 -0
  36. package/dist/chunk-A4GOY3PE.js +25 -0
  37. package/dist/chunk-A4VFQH5G.js +251 -0
  38. package/dist/chunk-AELOBFW5.js +375 -0
  39. package/dist/chunk-AN2MOO5T.js +522 -0
  40. package/dist/chunk-BMR4CIEO.js +41 -0
  41. package/dist/chunk-BS7SV7C3.js +51 -0
  42. package/dist/chunk-BUPDIHML.js +613 -0
  43. package/dist/chunk-BUVJFRPY.js +887 -0
  44. package/dist/chunk-C3Z4DX3V.js +112 -0
  45. package/dist/chunk-CI2G3OEB.js +1818 -0
  46. package/dist/chunk-CKNVO24D.js +54 -0
  47. package/dist/chunk-CUVHQHZ7.js +391 -0
  48. package/dist/chunk-CXK4DZH3.js +114 -0
  49. package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
  50. package/dist/chunk-DU7W62N5.js +487 -0
  51. package/dist/chunk-DZAQBW35.js +751 -0
  52. package/dist/chunk-EB6FPQUP.js +316 -0
  53. package/dist/chunk-EMS2SK26.js +376 -0
  54. package/dist/chunk-ERLBPDGU.js +510 -0
  55. package/dist/chunk-EVCVDTXE.js +732 -0
  56. package/dist/chunk-FCIIHT24.js +384 -0
  57. package/dist/chunk-FFUN5D66.js +97 -0
  58. package/dist/chunk-FITOLXSX.js +714 -0
  59. package/dist/chunk-FMDSF3OV.js +68 -0
  60. package/dist/chunk-FNB3PNGF.js +410 -0
  61. package/dist/chunk-G2H74XJZ.js +2104 -0
  62. package/dist/chunk-GMR5MFHJ.js +339 -0
  63. package/dist/chunk-H6PI3KJT.js +305 -0
  64. package/dist/chunk-I46X5VUA.js +59 -0
  65. package/dist/chunk-I5IGB56C.js +255 -0
  66. package/dist/chunk-IH6ZMVQ5.js +3950 -0
  67. package/dist/chunk-IJZFN3HM.js +463 -0
  68. package/dist/chunk-IPWPWM6R.js +3079 -0
  69. package/dist/chunk-IYULMRBE.js +23 -0
  70. package/dist/chunk-IZMRON6C.js +167 -0
  71. package/dist/chunk-J5FIXQRT.js +813 -0
  72. package/dist/chunk-JED6ZDEE.js +73 -0
  73. package/dist/chunk-JEUJOX5X.js +136 -0
  74. package/dist/chunk-JM4LEPY3.js +436 -0
  75. package/dist/chunk-L3UHP6TD.js +101 -0
  76. package/dist/chunk-L66GEFB6.js +524 -0
  77. package/dist/chunk-LIOHW36Z.js +469 -0
  78. package/dist/chunk-LP7EC4MI.js +16 -0
  79. package/dist/chunk-MITI6NOL.js +548 -0
  80. package/dist/chunk-MO5YHHMQ.js +168 -0
  81. package/dist/chunk-MZSBFAPJ.js +885 -0
  82. package/dist/chunk-NNAAJVBU.js +9 -0
  83. package/dist/chunk-NOTA6TEI.js +47 -0
  84. package/dist/chunk-NURGB5Z4.js +335 -0
  85. package/dist/chunk-NX4EQS3U.js +1062 -0
  86. package/dist/chunk-NXMESY4J.js +1 -0
  87. package/dist/chunk-OJR3MQYV.js +1458 -0
  88. package/dist/chunk-OURHMODR.js +454 -0
  89. package/dist/chunk-P235SVAH.js +1057 -0
  90. package/dist/chunk-P4RH4OSP.js +1038 -0
  91. package/dist/chunk-P6T66CCZ.js +226 -0
  92. package/dist/chunk-PBAHEPLQ.js +223 -0
  93. package/dist/chunk-PBHE5UG6.js +1530 -0
  94. package/dist/chunk-PDKUANDZ.js +35 -0
  95. package/dist/chunk-PWIHZ6NJ.js +296 -0
  96. package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
  97. package/dist/chunk-QLCCJEJI.js +253 -0
  98. package/dist/chunk-QOOUC2J5.js +52 -0
  99. package/dist/chunk-QYTPPDAO.js +402 -0
  100. package/dist/chunk-R3XI2VC7.js +801 -0
  101. package/dist/chunk-RCERWOJR.js +154 -0
  102. package/dist/chunk-RJE5JTVK.js +362 -0
  103. package/dist/chunk-RTLBSQ3G.js +1155 -0
  104. package/dist/chunk-RUNNI6VE.js +448 -0
  105. package/dist/chunk-RVMRZAPB.js +354 -0
  106. package/dist/chunk-RYEGFMUO.js +955 -0
  107. package/dist/chunk-RZCF2VVM.js +528 -0
  108. package/dist/chunk-SML67VPV.js +1025 -0
  109. package/dist/chunk-SQ7IY53Q.js +44 -0
  110. package/dist/chunk-SQAG3UXH.js +161 -0
  111. package/dist/chunk-SRTU6HMQ.js +1851 -0
  112. package/dist/chunk-T2OE3YQB.js +550 -0
  113. package/dist/chunk-T3UBLACL.js +299 -0
  114. package/dist/chunk-TLVIHFRO.js +296 -0
  115. package/dist/chunk-TYS6CSS5.js +52 -0
  116. package/dist/chunk-U6EQI3U6.js +130 -0
  117. package/dist/chunk-UJ4FJLBF.js +86 -0
  118. package/dist/chunk-ULOEATBZ.js +347 -0
  119. package/dist/chunk-UNCCZ3NU.js +127 -0
  120. package/dist/chunk-USZW2HHU.js +197 -0
  121. package/dist/chunk-UWN33VNX.js +1610 -0
  122. package/dist/chunk-UZ7EZW3B.js +40 -0
  123. package/dist/chunk-VOKGH627.js +37 -0
  124. package/dist/chunk-WBRZJOEC.js +204 -0
  125. package/dist/chunk-WCSOJFU3.js +380 -0
  126. package/dist/chunk-WCT3NYCV.js +160 -0
  127. package/dist/chunk-WVAJFAIA.js +529 -0
  128. package/dist/chunk-X5LCYCL5.js +394 -0
  129. package/dist/chunk-XAKEUMEJ.js +1256 -0
  130. package/dist/chunk-Y2ZE3MSG.js +590 -0
  131. package/dist/chunk-Y3J7VR52.js +646 -0
  132. package/dist/chunk-Y5QGVTVC.js +33 -0
  133. package/dist/chunk-YAA6IID3.js +548 -0
  134. package/dist/chunk-YGJTDXVR.js +692 -0
  135. package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
  136. package/dist/chunk-YYVTBZZC.js +61 -0
  137. package/dist/chunk-Z5CGSFJB.js +647 -0
  138. package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
  139. package/dist/chunk-ZGR7ULFJ.js +978 -0
  140. package/dist/chunk-ZJEL4H2R.js +851 -0
  141. package/dist/chunk-ZO27FGJS.js +554 -0
  142. package/dist/collapse-CLPJD2RL.js +11 -0
  143. package/dist/collapse-SLINJQW6.js +9 -0
  144. package/dist/collapse-ZWUFFCK2.js +9 -0
  145. package/dist/compute-G55RG53N.js +8 -0
  146. package/dist/dgmo-router-DZCIDUO5.js +73 -0
  147. package/dist/dimensions-J3FHIMJ6.js +22 -0
  148. package/dist/element.js +1 -1
  149. package/dist/element.mjs +1 -1
  150. package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
  151. package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
  152. package/dist/index.d.ts +3 -469
  153. package/dist/index.js +137 -69685
  154. package/dist/layout-46N4JJYK.js +17 -0
  155. package/dist/layout-5RA6QWR6.js +26 -0
  156. package/dist/layout-5T6RH5M6.js +14 -0
  157. package/dist/layout-7QXCNOVS.js +13 -0
  158. package/dist/layout-EE6GE46Y.js +17 -0
  159. package/dist/layout-G23F7LDH.js +9 -0
  160. package/dist/layout-HRBZPFCN.js +23 -0
  161. package/dist/layout-HWDSRX5E.js +20 -0
  162. package/dist/layout-HYZPWAK5.js +18 -0
  163. package/dist/layout-N76NR4ZO.js +19 -0
  164. package/dist/layout-OB3RI63S.js +14 -0
  165. package/dist/layout-S2QBYSK2.js +15 -0
  166. package/dist/layout-UM5F4DJB.js +15 -0
  167. package/dist/layout-search-OYV77HHS.js +1655 -0
  168. package/dist/load-data-EHDDZ5ZS.js +8 -0
  169. package/dist/palettes-PRJ2RI6D.js +56 -0
  170. package/dist/parser-2MFP2H2E.js +11 -0
  171. package/dist/parser-3STE62GB.js +21 -0
  172. package/dist/parser-4FL2CMDG.js +18 -0
  173. package/dist/parser-4OGOBJJ4.js +13 -0
  174. package/dist/parser-6BTRLKRZ.js +15 -0
  175. package/dist/parser-AQETEVRW.js +21 -0
  176. package/dist/parser-CFZD5IBL.js +12 -0
  177. package/dist/parser-FUSKFGZN.js +16 -0
  178. package/dist/parser-I6KXEYFK.js +13 -0
  179. package/dist/parser-IXSZZQHY.js +18 -0
  180. package/dist/parser-JAL2HD4I.js +13 -0
  181. package/dist/parser-KNSQSKQO.js +20 -0
  182. package/dist/parser-N5O4IHUN.js +13 -0
  183. package/dist/parser-NGU3ETO6.js +16 -0
  184. package/dist/parser-NRAI6O25.js +19 -0
  185. package/dist/parser-O5J4VBII.js +11 -0
  186. package/dist/parser-OV7NOBW6.js +21 -0
  187. package/dist/parser-PQ3POX45.js +23 -0
  188. package/dist/parser-TFZFBVWN.js +11 -0
  189. package/dist/parser-TRTHDFPP.js +18 -0
  190. package/dist/parser-VQP2UJ6W.js +11 -0
  191. package/dist/parser-XDVELHRK.js +13 -0
  192. package/dist/parser-XKRP7QKP.js +11 -0
  193. package/dist/parser-Y6WTBU76.js +14 -0
  194. package/dist/parser-ZSDPZYQP.js +12 -0
  195. package/dist/renderer-2XQQQ6QR.js +28 -0
  196. package/dist/renderer-5NDVNDAZ.js +26 -0
  197. package/dist/renderer-6EEZHMH4.js +26 -0
  198. package/dist/renderer-6W5462RB.js +43 -0
  199. package/dist/renderer-7MVLWWQD.js +22 -0
  200. package/dist/renderer-AWWAF6XD.js +33 -0
  201. package/dist/renderer-BYCNMBF7.js +34 -0
  202. package/dist/renderer-CLJHQPBJ.js +29 -0
  203. package/dist/renderer-GHPZMFU2.js +15 -0
  204. package/dist/renderer-JGBKUCZD.js +16 -0
  205. package/dist/renderer-KQB7UGDS.js +19 -0
  206. package/dist/renderer-PGX2TKVS.js +24 -0
  207. package/dist/renderer-QIWVHWZO.js +15 -0
  208. package/dist/renderer-RPOA5YPZ.js +35 -0
  209. package/dist/renderer-SMIBCRVB.js +24 -0
  210. package/dist/renderer-T7YKC5VP.js +28 -0
  211. package/dist/renderer-TOCOVLHS.js +37 -0
  212. package/dist/renderer-UWA5XS74.js +39 -0
  213. package/dist/renderer-VACVQYML.js +25 -0
  214. package/dist/renderer-W4M6QLIJ.js +37 -0
  215. package/dist/renderer-WZRREQ2P.js +23 -0
  216. package/dist/renderer-YY2OSF6E.js +27 -0
  217. package/dist/renderer-Z7I3W2PP.js +24 -0
  218. package/dist/renderer-ZGNTUWC4.js +22 -0
  219. package/dist/renderer-ZTR3DER3.js +19 -0
  220. package/dist/renderer-radial-C32GPJQ7.js +21 -0
  221. package/dist/resolver-NOAA7ID7.js +13 -0
  222. package/dist/state-collapse-HCGD6IVJ.js +8 -0
  223. package/dist/state-parser-Z2V23ATR.js +18 -0
  224. package/dist/state-renderer-RFYHSP4K.js +34 -0
  225. package/dist/themes-1-CuKpeH.d.ts +908 -0
  226. package/package.json +19 -70
  227. package/dist/advanced.cjs +0 -71648
  228. package/dist/advanced.d.cts +0 -6571
  229. package/dist/auto.cjs +0 -70648
  230. package/dist/auto.d.cts +0 -54
  231. package/dist/block.cjs +0 -70208
  232. package/dist/block.d.cts +0 -115
  233. package/dist/editor.d.cts +0 -26
  234. package/dist/element.cjs +0 -70530
  235. package/dist/element.d.cts +0 -62
  236. package/dist/highlight.cjs +0 -852
  237. package/dist/highlight.d.cts +0 -49
  238. package/dist/index.cjs +0 -70491
  239. package/dist/index.d.cts +0 -626
  240. package/dist/pert.cjs +0 -325
  241. package/dist/pert.d.cts +0 -559
  242. package/src/advanced.ts +0 -873
  243. package/src/arc/parser.ts +0 -15
  244. package/src/arc/renderer.ts +0 -633
  245. package/src/auto/index.ts +0 -621
  246. package/src/auto/shared.ts +0 -370
  247. package/src/auto/styles.ts +0 -73
  248. package/src/block/layout.ts +0 -198
  249. package/src/block/renderer.ts +0 -484
  250. package/src/block/types.ts +0 -67
  251. package/src/boxes-and-lines/collapse.ts +0 -96
  252. package/src/boxes-and-lines/focus.ts +0 -257
  253. package/src/boxes-and-lines/label-placement.ts +0 -339
  254. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  255. package/src/boxes-and-lines/layout-layered.ts +0 -722
  256. package/src/boxes-and-lines/layout-search.ts +0 -1678
  257. package/src/boxes-and-lines/layout.ts +0 -398
  258. package/src/boxes-and-lines/parser.ts +0 -1181
  259. package/src/boxes-and-lines/renderer.ts +0 -1465
  260. package/src/boxes-and-lines/types.ts +0 -66
  261. package/src/c4/parser.ts +0 -1158
  262. package/src/c4/renderer.ts +0 -2008
  263. package/src/c4/types.ts +0 -89
  264. package/src/chart-type-registry.ts +0 -529
  265. package/src/chart-types.ts +0 -225
  266. package/src/chart.ts +0 -685
  267. package/src/charts-d3/bar.ts +0 -338
  268. package/src/charts-d3/chord.ts +0 -140
  269. package/src/charts-d3/function.ts +0 -147
  270. package/src/charts-d3/funnel.ts +0 -104
  271. package/src/charts-d3/heatmap.ts +0 -145
  272. package/src/charts-d3/index.ts +0 -337
  273. package/src/charts-d3/interactions.ts +0 -477
  274. package/src/charts-d3/line.ts +0 -344
  275. package/src/charts-d3/mount.ts +0 -151
  276. package/src/charts-d3/pie.ts +0 -117
  277. package/src/charts-d3/polar.ts +0 -89
  278. package/src/charts-d3/radar.ts +0 -0
  279. package/src/charts-d3/sankey.ts +0 -0
  280. package/src/charts-d3/scatter.ts +0 -185
  281. package/src/charts-d3/shared.ts +0 -208
  282. package/src/class/layout.ts +0 -326
  283. package/src/class/parser.ts +0 -635
  284. package/src/class/renderer.ts +0 -793
  285. package/src/class/types.ts +0 -58
  286. package/src/cli-banner.ts +0 -106
  287. package/src/cli.ts +0 -1080
  288. package/src/colors-diagnostics.ts +0 -35
  289. package/src/colors.ts +0 -379
  290. package/src/completion-types.ts +0 -27
  291. package/src/completion.ts +0 -2668
  292. package/src/cycle/layout.ts +0 -1026
  293. package/src/cycle/parser.ts +0 -387
  294. package/src/cycle/renderer.ts +0 -653
  295. package/src/cycle/types.ts +0 -101
  296. package/src/d3.ts +0 -1434
  297. package/src/data-chart-parser.ts +0 -1007
  298. package/src/dgmo-router.ts +0 -296
  299. package/src/diagnostics-registry.ts +0 -108
  300. package/src/diagnostics.ts +0 -0
  301. package/src/dimensions.ts +0 -20
  302. package/src/directives-registry.ts +0 -396
  303. package/src/editor/dgmo.grammar +0 -76
  304. package/src/editor/dgmo.grammar.d.ts +0 -2
  305. package/src/editor/dgmo.grammar.js +0 -18
  306. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  307. package/src/editor/dgmo.grammar.terms.js +0 -35
  308. package/src/editor/highlight-api.ts +0 -596
  309. package/src/editor/highlight.ts +0 -36
  310. package/src/editor/index.ts +0 -27
  311. package/src/editor/keywords.ts +0 -128
  312. package/src/editor/tokens.ts +0 -30
  313. package/src/element/index.ts +0 -432
  314. package/src/embed/css.ts +0 -312
  315. package/src/embed/escape.ts +0 -15
  316. package/src/embed/index.ts +0 -280
  317. package/src/er/classify.ts +0 -228
  318. package/src/er/layout.ts +0 -530
  319. package/src/er/parser.ts +0 -750
  320. package/src/er/renderer.ts +0 -725
  321. package/src/er/types.ts +0 -52
  322. package/src/error-card.ts +0 -269
  323. package/src/event-line/diagnostics.ts +0 -117
  324. package/src/event-line/parser.ts +0 -643
  325. package/src/event-line/renderer.ts +0 -1742
  326. package/src/event-line/types.ts +0 -74
  327. package/src/fonts.ts +0 -3
  328. package/src/gantt/parser.ts +0 -1641
  329. package/src/gantt/renderer.ts +0 -3787
  330. package/src/gantt/resolver.ts +0 -172
  331. package/src/gantt/types.ts +0 -206
  332. package/src/graph/diagnostics.ts +0 -64
  333. package/src/graph/edge-spline.ts +0 -29
  334. package/src/graph/flowchart-parser.ts +0 -759
  335. package/src/graph/flowchart-renderer.ts +0 -817
  336. package/src/graph/layout.ts +0 -472
  337. package/src/graph/notes.ts +0 -21
  338. package/src/graph/state-collapse.ts +0 -78
  339. package/src/graph/state-parser.ts +0 -574
  340. package/src/graph/state-renderer.ts +0 -653
  341. package/src/graph/types.ts +0 -65
  342. package/src/index.ts +0 -261
  343. package/src/infra/compute.ts +0 -1385
  344. package/src/infra/layout.ts +0 -888
  345. package/src/infra/parser.ts +0 -1234
  346. package/src/infra/renderer.ts +0 -2534
  347. package/src/infra/roles.ts +0 -75
  348. package/src/infra/types.ts +0 -216
  349. package/src/infra/validation.ts +0 -201
  350. package/src/journey-map/layout.ts +0 -490
  351. package/src/journey-map/parser.ts +0 -557
  352. package/src/journey-map/renderer.ts +0 -1580
  353. package/src/journey-map/types.ts +0 -47
  354. package/src/kanban/mutations.ts +0 -175
  355. package/src/kanban/parser.ts +0 -509
  356. package/src/kanban/renderer.ts +0 -1449
  357. package/src/kanban/types.ts +0 -39
  358. package/src/label-layout.ts +0 -368
  359. package/src/map/colorize.ts +0 -54
  360. package/src/map/completion.ts +0 -316
  361. package/src/map/context-labels.ts +0 -609
  362. package/src/map/data/PROVENANCE.json +0 -1
  363. package/src/map/data/README.md +0 -40
  364. package/src/map/data/airport-collisions.json +0 -1
  365. package/src/map/data/airports.json +0 -1
  366. package/src/map/data/gazetteer.json +0 -1
  367. package/src/map/data/lakes.json +0 -1
  368. package/src/map/data/mountain-ranges.json +0 -1
  369. package/src/map/data/na-lakes.json +0 -1
  370. package/src/map/data/na-land.json +0 -1
  371. package/src/map/data/region-names.json +0 -1
  372. package/src/map/data/rivers.json +0 -1
  373. package/src/map/data/types.ts +0 -121
  374. package/src/map/data/us-states.json +0 -1
  375. package/src/map/data/water-bodies.json +0 -1
  376. package/src/map/data/world-coarse.json +0 -1
  377. package/src/map/data/world-detail.json +0 -1
  378. package/src/map/diagnostics.ts +0 -134
  379. package/src/map/dimensions.ts +0 -150
  380. package/src/map/geo-query.ts +0 -295
  381. package/src/map/geo.ts +0 -438
  382. package/src/map/invert.ts +0 -111
  383. package/src/map/layout.ts +0 -5069
  384. package/src/map/legend-band.ts +0 -127
  385. package/src/map/load-data.ts +0 -186
  386. package/src/map/parser.ts +0 -783
  387. package/src/map/renderer.ts +0 -1445
  388. package/src/map/resolved-types.ts +0 -160
  389. package/src/map/resolver.ts +0 -1146
  390. package/src/map/types.ts +0 -194
  391. package/src/mindmap/collapse.ts +0 -67
  392. package/src/mindmap/layout.ts +0 -649
  393. package/src/mindmap/parser.ts +0 -430
  394. package/src/mindmap/renderer.ts +0 -637
  395. package/src/mindmap/text-wrap.ts +0 -218
  396. package/src/mindmap/types.ts +0 -56
  397. package/src/org/collapse.ts +0 -151
  398. package/src/org/layout.ts +0 -1262
  399. package/src/org/parser.ts +0 -519
  400. package/src/org/renderer.ts +0 -866
  401. package/src/org/resolver.ts +0 -636
  402. package/src/palettes/atlas.ts +0 -77
  403. package/src/palettes/blueprint.ts +0 -73
  404. package/src/palettes/catppuccin.ts +0 -84
  405. package/src/palettes/color-utils.ts +0 -443
  406. package/src/palettes/index.ts +0 -67
  407. package/src/palettes/nord.ts +0 -67
  408. package/src/palettes/registry.ts +0 -114
  409. package/src/palettes/slate.ts +0 -73
  410. package/src/palettes/tidewater.ts +0 -73
  411. package/src/palettes/tokyo-night.ts +0 -86
  412. package/src/palettes/types.ts +0 -88
  413. package/src/pert/analyzer.ts +0 -1183
  414. package/src/pert/diagnostics.ts +0 -110
  415. package/src/pert/index.ts +0 -43
  416. package/src/pert/internal.ts +0 -252
  417. package/src/pert/layout.ts +0 -971
  418. package/src/pert/monte-carlo.ts +0 -446
  419. package/src/pert/parser.ts +0 -1606
  420. package/src/pert/renderer.ts +0 -4009
  421. package/src/pert/share-normalize.ts +0 -62
  422. package/src/pert/types.ts +0 -604
  423. package/src/pyramid/parser.ts +0 -185
  424. package/src/pyramid/renderer.ts +0 -768
  425. package/src/pyramid/types.ts +0 -28
  426. package/src/quadrant/parser.ts +0 -18
  427. package/src/quadrant/renderer.ts +0 -638
  428. package/src/raci/diagnostics.ts +0 -213
  429. package/src/raci/index.ts +0 -23
  430. package/src/raci/mutations.ts +0 -321
  431. package/src/raci/parser.ts +0 -699
  432. package/src/raci/renderer.ts +0 -1627
  433. package/src/raci/types.ts +0 -82
  434. package/src/raci/variants.ts +0 -320
  435. package/src/render.ts +0 -268
  436. package/src/ring/parser.ts +0 -212
  437. package/src/ring/renderer.ts +0 -490
  438. package/src/ring/types.ts +0 -27
  439. package/src/sequence/collapse.ts +0 -174
  440. package/src/sequence/parser.ts +0 -1596
  441. package/src/sequence/participant-inference.ts +0 -152
  442. package/src/sequence/renderer.ts +0 -3168
  443. package/src/sequence/tag-resolution.ts +0 -164
  444. package/src/sharing.ts +0 -211
  445. package/src/sitemap/collapse.ts +0 -100
  446. package/src/sitemap/layout.ts +0 -925
  447. package/src/sitemap/parser.ts +0 -755
  448. package/src/sitemap/renderer.ts +0 -828
  449. package/src/sitemap/types.ts +0 -42
  450. package/src/slope/parser.ts +0 -18
  451. package/src/slope/renderer.ts +0 -378
  452. package/src/swimlane/diagnostics.ts +0 -94
  453. package/src/swimlane/layout.ts +0 -597
  454. package/src/swimlane/parser.ts +0 -613
  455. package/src/swimlane/renderer.ts +0 -681
  456. package/src/swimlane/types.ts +0 -151
  457. package/src/tech-radar/interactive.ts +0 -1129
  458. package/src/tech-radar/layout.ts +0 -197
  459. package/src/tech-radar/parser.ts +0 -436
  460. package/src/tech-radar/renderer.ts +0 -1239
  461. package/src/tech-radar/shared.ts +0 -151
  462. package/src/tech-radar/types.ts +0 -86
  463. package/src/themes.ts +0 -22
  464. package/src/timeline/parser.ts +0 -472
  465. package/src/timeline/renderer.ts +0 -3024
  466. package/src/timeline/types.ts +0 -33
  467. package/src/timeline/viz-parser.ts +0 -21
  468. package/src/treemap/diagnostics.ts +0 -62
  469. package/src/treemap/layout-radial.ts +0 -233
  470. package/src/treemap/layout.ts +0 -189
  471. package/src/treemap/parser.ts +0 -463
  472. package/src/treemap/renderer-radial.ts +0 -438
  473. package/src/treemap/renderer.ts +0 -572
  474. package/src/treemap/treemap-shared.ts +0 -309
  475. package/src/treemap/types.ts +0 -60
  476. package/src/utils/arrow-markers.ts +0 -51
  477. package/src/utils/arrows-diagnostics.ts +0 -62
  478. package/src/utils/arrows.ts +0 -227
  479. package/src/utils/brand.ts +0 -61
  480. package/src/utils/card.ts +0 -183
  481. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  482. package/src/utils/collapse-engine/tree.ts +0 -117
  483. package/src/utils/d3-helpers.ts +0 -219
  484. package/src/utils/d3-types.ts +0 -9
  485. package/src/utils/description-helpers.ts +0 -38
  486. package/src/utils/duration.ts +0 -312
  487. package/src/utils/export-container.ts +0 -41
  488. package/src/utils/expr-eval.ts +0 -27
  489. package/src/utils/fit-canvas.ts +0 -64
  490. package/src/utils/hover-styles.ts +0 -673
  491. package/src/utils/inline-markdown.ts +0 -96
  492. package/src/utils/legend-constants.ts +0 -48
  493. package/src/utils/legend-d3.ts +0 -618
  494. package/src/utils/legend-integration.ts +0 -107
  495. package/src/utils/legend-layout.ts +0 -776
  496. package/src/utils/legend-svg.ts +0 -214
  497. package/src/utils/legend-types.ts +0 -272
  498. package/src/utils/name-normalize.ts +0 -163
  499. package/src/utils/note-box/constants.ts +0 -25
  500. package/src/utils/note-box/index.ts +0 -11
  501. package/src/utils/note-box/metrics.ts +0 -90
  502. package/src/utils/note-box/svg.ts +0 -331
  503. package/src/utils/notes/bounds.ts +0 -30
  504. package/src/utils/notes/build.ts +0 -131
  505. package/src/utils/notes/index.ts +0 -18
  506. package/src/utils/notes/model.ts +0 -19
  507. package/src/utils/notes/parse.ts +0 -131
  508. package/src/utils/notes/place.ts +0 -177
  509. package/src/utils/notes/resolve.ts +0 -88
  510. package/src/utils/number-format.ts +0 -36
  511. package/src/utils/parsing.ts +0 -953
  512. package/src/utils/reserved-key-registry.ts +0 -239
  513. package/src/utils/safe-href.ts +0 -38
  514. package/src/utils/scaling.ts +0 -97
  515. package/src/utils/svg-embed.ts +0 -251
  516. package/src/utils/tag-groups.ts +0 -607
  517. package/src/utils/text-measure.ts +0 -122
  518. package/src/utils/title-constants.ts +0 -24
  519. package/src/utils/visual-conventions.ts +0 -61
  520. package/src/utils/wrapped-desc.ts +0 -87
  521. package/src/venn/parser.ts +0 -18
  522. package/src/venn/renderer.ts +0 -1047
  523. package/src/version-control/diagnostics.ts +0 -67
  524. package/src/version-control/parser.ts +0 -544
  525. package/src/version-control/renderer.ts +0 -724
  526. package/src/version-control/types.ts +0 -100
  527. package/src/visualizations/parse.ts +0 -1560
  528. package/src/visualizations/types.ts +0 -262
  529. package/src/wireframe/layout.ts +0 -479
  530. package/src/wireframe/parser.ts +0 -1114
  531. package/src/wireframe/renderer.ts +0 -1318
  532. package/src/wireframe/types.ts +0 -110
  533. package/src/wordcloud/parser.ts +0 -18
  534. package/src/wordcloud/renderer.ts +0 -357
@@ -1,1234 +0,0 @@
1
- // ============================================================
2
- // Infra Chart Parser
3
- // ============================================================
4
- //
5
- // Parses `infra [Title]` syntax into a structured InfraModel.
6
- // Handles: chart metadata, component blocks with indented properties
7
- // and connections, [Group] containers, tag groups, pipe metadata.
8
-
9
- import { tagAttrKey } from '../utils/tag-groups';
10
- import {
11
- emit,
12
- formatDgmoError,
13
- makeDgmoError,
14
- NAME_DIAGNOSTIC_CODES,
15
- nameMergedMessage,
16
- suggest,
17
- } from '../diagnostics';
18
- import { GRAPH_DX } from '../graph/diagnostics';
19
- import { tryStripDescriptionKeyword } from '../utils/description-helpers';
20
- import { parseInArrowLabel } from '../utils/arrows';
21
- import {
22
- measureIndent,
23
- parseFirstLine,
24
- OPTION_NOCOLON_RE,
25
- tryParseSharedOption,
26
- warnUnknownMetaKeys,
27
- } from '../utils/parsing';
28
- import { isRecognizedColorName } from '../colors';
29
- import { normalizeName, displayName } from '../utils/name-normalize';
30
- import {
31
- matchTagBlockHeading,
32
- stripDefaultModifier,
33
- validateTagGroupNames,
34
- } from '../utils/tag-groups';
35
- import { INFRA_REGISTRY, withTagAliases } from '../utils/reserved-key-registry';
36
- import type {
37
- ParsedInfra,
38
- InfraNode,
39
- InfraGroup,
40
- InfraTagGroup,
41
- } from './types';
42
- import { INFRA_BEHAVIOR_KEYS, EDGE_ONLY_KEYS } from './types';
43
- import { INFRA_TOP_LEVEL_OPTION_SET } from '../directives-registry';
44
- import type { Writable } from '../utils/brand';
45
-
46
- // ============================================================
47
- // Regex patterns
48
- // ============================================================
49
-
50
- // Connection: -label-> Target or -> Target (pipe metadata handled by extractPipeMetadata)
51
- const CONNECTION_RE = /^-(?:([^-].*?))?->\s*(.+?)\s*$/;
52
-
53
- // Simple connection shorthand: -> Target (no label, no dash prefix needed for edge)
54
- const SIMPLE_CONNECTION_RE = /^->\s*(.+?)\s*$/;
55
-
56
- // Async connection: ~label~> Target or ~> Target
57
- const ASYNC_CONNECTION_RE = /^~(?:([^~].*?))?~>\s*(.+?)\s*$/;
58
-
59
- // Async simple connection shorthand: ~> Target
60
- const ASYNC_SIMPLE_CONNECTION_RE = /^~>\s*(.+?)\s*$/;
61
-
62
- // Deprecated xN fanout suffix (e.g. "x5" at end of line)
63
- const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
64
-
65
- // Group declaration: [Group Name] with optional `as <alias>` and optional
66
- // metadata (legacy `| ...` or new same-line `key: value` per §1.4).
67
- // Capture order: 1=label, 2=alias, 3=metadata tail.
68
- const GROUP_RE =
69
- /^\[([^\]]+)\]\s*(?:as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*)?((?:\|.*)|(?:[a-zA-Z_]\w*\s*:.*))?$/;
70
-
71
- // Tag value: `Name` or `Name color` (trailing-token color form). Color is
72
- // extracted via the shared `extractColor` helper at use-site (see
73
- // `dgmo/src/utils/parsing.ts:extractColor`), not via this regex. This regex
74
- // just confirms the line shape is a valid tag value (no reserved sigils).
75
- const TAG_VALUE_RE = /^(\w[\w\s]+?)\s*$/;
76
-
77
- // Component line. Accepts either a quoted name ("name with | : reserved chars")
78
- // or a bare name (multi-word allowed; must start with letter/underscore so digit-
79
- // only or sigil-led lines fall through to other branches). Metadata follows
80
- // after a `|` separator (legacy) or as same-line `key: value` (§1.4).
81
- //
82
- // Named captures: qname (quoted content), uname (bare name), alias (`as <x>`
83
- // postfix), meta (the metadata tail). qname/uname are mutually exclusive.
84
- //
85
- // A component declaration: a quoted display name OR a bare name, an OPTIONAL
86
- // `as <alias>` postfix (alias mirrors peelAlias: letter-led, ≤12 chars), then
87
- // optional same-line metadata. The `as` capture is what makes a quoted name +
88
- // alias parse — `"Web App" as web` — which the bare-name path got for free by
89
- // sweeping the alias into the lazy name group and peeling it later, but the
90
- // quoted path could not reach past the closing quote.
91
- const COMPONENT_RE =
92
- /^(?:"(?<qname>[^"]+)"|(?<uname>[a-zA-Z_][\w -]*?))(?:\s+as\s+(?<alias>[A-Za-z][A-Za-z0-9_]{0,11}))?\s*(?<meta>(?:\|.*)|(?:[a-zA-Z_]\w*\s*:.*))?$/;
93
-
94
- // Legacy pipe metadata: | key: value or | k1: v1, k2: v2 (comma-separated).
95
- // Kept for transitional back-compat extraction; new same-line metadata
96
- // (§1.4) is preferred and extracted via SAMELINE_META_RE below.
97
- const PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
98
- // Same-line metadata (post-0.18.0 §1.4): `k: v, k2: v2` after any name
99
- // region — matches a colon-bearing key-value, comma-separated.
100
- const SAMELINE_META_RE = /(?:^|[\s,])(\w+)\s*:\s*([^,]+?)(?=\s*,|\s*$)/g;
101
-
102
- // Property: key: value (colon-separated)
103
- const PROPERTY_RE = /^([\w-]+):\s*(.+)$/;
104
-
105
- // Percentage value: 80% or 99.99%
106
- const PERCENT_RE = /^([\d.]+)%$/;
107
-
108
- // Range value: N-M (for instances)
109
- const RANGE_RE = /^(\d+)-(\d+)$/;
110
-
111
- // Node names that act as the traffic entry point (edge node)
112
- const EDGE_NODE_NAMES = new Set(['edge', 'internet']);
113
-
114
- // Known top-level option keys (space-separated, no colon). Derived from the
115
- // single-source directives registry; the anti-drift guard
116
- // (tests/highlight-coverage.test.ts) cross-checks it against the editor sets.
117
- export const INFRA_TOP_LEVEL_OPTIONS: ReadonlySet<string> =
118
- INFRA_TOP_LEVEL_OPTION_SET;
119
- const TOP_LEVEL_OPTIONS = INFRA_TOP_LEVEL_OPTIONS;
120
-
121
- // ============================================================
122
- // Helpers
123
- // ============================================================
124
-
125
- function nodeId(name: string): string {
126
- return normalizeName(name);
127
- }
128
-
129
- function groupId(name: string): string {
130
- return `[${normalizeName(name)}]`;
131
- }
132
-
133
- function parsePropertyValue(raw: string): string | number {
134
- const pct = raw.match(PERCENT_RE);
135
- // Capture group 1 in-bounds after successful match.
136
- if (pct) return parseFloat(pct[1]!);
137
-
138
- const num = parseFloat(raw);
139
- if (!isNaN(num) && String(num) === raw.trim()) return num;
140
-
141
- return raw.trim();
142
- }
143
-
144
- function extractPipeMetadata(rest: string): {
145
- tags: Record<string, string>;
146
- clean: string;
147
- hadPipe: boolean;
148
- } {
149
- const tags: Record<string, string> = {};
150
- let clean = rest;
151
- let match: RegExpExecArray | null;
152
- const hadPipe = rest.includes('|');
153
- if (hadPipe) {
154
- PIPE_META_RE.lastIndex = 0;
155
- while ((match = PIPE_META_RE.exec(rest)) !== null) {
156
- tags[match[1]!.trim()] = match[2]!.trim();
157
- clean = clean.replace(match[0], '');
158
- }
159
- return { tags, clean: clean.trim(), hadPipe: true };
160
- }
161
- // §1.4 same-line metadata: no pipe, but `key: value, key2: value2`
162
- // may follow the name. Extract from right-to-left so the leftmost
163
- // name region is preserved as `clean`.
164
- SAMELINE_META_RE.lastIndex = 0;
165
- let firstMatchIdx = -1;
166
- while ((match = SAMELINE_META_RE.exec(rest)) !== null) {
167
- tags[match[1]!.trim()] = match[2]!.trim();
168
- if (firstMatchIdx === -1) firstMatchIdx = match.index;
169
- }
170
- if (firstMatchIdx >= 0) {
171
- clean = rest.substring(0, firstMatchIdx).trim();
172
- }
173
- return { tags, clean: clean.trim(), hadPipe: false };
174
- }
175
-
176
- // Detect unparsed pipe metadata left in a target name after extractPipeMetadata.
177
- // Common case: `split 100%` without a colon isn't picked up by PIPE_META_RE.
178
- const UNPARSED_SPLIT_RE = /\bsplit\s+(\d+)%/;
179
-
180
- function warnUnparsedPipeMeta(
181
- targetName: string,
182
- lineNumber: number,
183
- warnFn: (line: number, message: string) => void
184
- ): void {
185
- if (!targetName.includes('|')) return;
186
- const splitMatch = targetName.match(UNPARSED_SPLIT_RE);
187
- if (splitMatch) {
188
- warnFn(
189
- lineNumber,
190
- `'split ${splitMatch[1]}%' needs a colon — use 'split: ${splitMatch[1]}%'`
191
- );
192
- } else {
193
- warnFn(
194
- lineNumber,
195
- `Unparsed pipe metadata in target — pipe values use 'key: value' syntax`
196
- );
197
- }
198
- }
199
-
200
- // ============================================================
201
- // Parser
202
- // ============================================================
203
-
204
- export function parseInfra(content: string): ParsedInfra {
205
- const lines = content.split('\n');
206
- const options: Record<string, string> = {};
207
- const result: Writable<ParsedInfra> = {
208
- type: 'infra',
209
- title: null,
210
- titleLineNumber: null,
211
- direction: 'LR',
212
- nodes: [],
213
- edges: [],
214
- groups: [],
215
- tagGroups: [],
216
- options,
217
- diagnostics: [],
218
- error: null,
219
- };
220
-
221
- const nodeMap = new Map<string, Writable<InfraNode>>();
222
- // Sidecar mutable tag-records, keyed by node id. The same Record object is
223
- // also referenced from `node.tags`; mutating it here updates both views.
224
- const nodeMutableTags = new Map<string, Record<string, string>>();
225
-
226
- // Push a description line onto a node. Manages the underlying mutable
227
- // array and assigns it back so `node.description` stays in sync.
228
- function pushDescription(node: Writable<InfraNode>, text: string): void {
229
- const existing = (node.description as string[] | undefined) ?? [];
230
- existing.push(text);
231
- node.description = existing;
232
- }
233
-
234
- // Tag-alias set grows as `tag Name as <x>` groups are parsed.
235
- const tagAliasSet = new Set<string>();
236
- const EDGE_PIPE_REGISTRY = {
237
- keys: new Set(['split', 'fanout']),
238
- tagAliases: new Set<string>(),
239
- } as const;
240
-
241
- // Per-parse alias literal → canonical node id (TD-18). Per C8.
242
- const nameAliasMap = new Map<string, string>();
243
- function peelAlias(label: string): { label: string; alias?: string } {
244
- const trimmed = label.trim();
245
- const m = trimmed.match(/^(.*?)\s+as\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/);
246
- if (!m) return { label: trimmed };
247
- // Capture groups 1 & 2 in-bounds after successful match.
248
- return { label: m[1]!.trim(), ...(m[2] !== undefined && { alias: m[2] }) };
249
- }
250
-
251
- // Infra nodes have no "is a <type>" declaration — capability comes from
252
- // properties (cache-hit, buffer, drain-rate, …). Strip the suffix if a
253
- // user wrote it (likely copying from sequence/c4 syntax) and warn.
254
- const IS_A_SUFFIX = /^(.*?)\s+is\s+an?\s+[A-Za-z][\w-]*\s*$/i;
255
- function peelInfraDecorations(
256
- rawName: string,
257
- lineNumber: number
258
- ): { label: string; alias?: string } {
259
- const peeled = peelAlias(rawName);
260
- const m = peeled.label.match(IS_A_SUFFIX);
261
- if (!m) return peeled;
262
- warn(
263
- lineNumber,
264
- `Infra nodes don't use 'is a <type>' — types are inferred from properties (cache-hit, buffer, drain-rate, …). Drop the 'is a' suffix.`
265
- );
266
- // Capture group 1 in-bounds after successful match.
267
- return {
268
- label: m[1]!.trim(),
269
- ...(peeled.alias !== undefined && { alias: peeled.alias }),
270
- };
271
- }
272
- /**
273
- * Resolve a connection-target token. If the token exactly matches a
274
- * declared alias, return the bound canonical id. Otherwise fall
275
- * through to the normal `nodeId()` hash.
276
- */
277
- function resolveTargetId(rawName: string): string {
278
- const aliasResolved = nameAliasMap.get(rawName.trim());
279
- if (aliasResolved !== undefined) return aliasResolved;
280
- return nodeId(rawName);
281
- }
282
-
283
- // Tracks first-seen display form for each normalized edge-target id.
284
- // Used to give forward-reference stubs a readable label (e.g. 'CDN'
285
- // instead of the lowercased internal 'cdn').
286
- const targetDisplays = new Map<string, string>();
287
- const rememberTargetDisplay = (key: string, display: string): void => {
288
- if (!targetDisplays.has(key)) targetDisplays.set(key, display.trim());
289
- };
290
-
291
- const setError = (line: number, message: string) => {
292
- const diag = makeDgmoError(line, message);
293
- result.diagnostics.push(diag);
294
- if (!result.error) result.error = formatDgmoError(diag);
295
- };
296
-
297
- const warn = (line: number, message: string) => {
298
- result.diagnostics.push(makeDgmoError(line, message, 'warning'));
299
- };
300
-
301
- // Track parser state
302
- let currentNode: Writable<InfraNode> | null = null;
303
- let currentGroup: Writable<InfraGroup> | null = null;
304
- let currentTagGroup: Writable<InfraTagGroup> | null = null;
305
- let baseIndent = 0; // indent of the current component line
306
- // The `infra [Title]` declaration is only legal on the first content line.
307
- // parseFirstLine() must run ONCE, there — otherwise a later bare node line
308
- // whose name is a chart-type keyword (e.g. an edge-block header `timeline`)
309
- // is mis-read as a nested chart declaration and the parse aborts.
310
- let firstLineConsumed = false;
311
-
312
- function finishCurrentNode() {
313
- if (!currentNode) return;
314
- const key = currentNode.id;
315
- const existing = nodeMap.get(key);
316
- if (existing) {
317
- const incomingDisplay = displayName(currentNode.label);
318
- const existingDisplay = displayName(existing.label);
319
- // A bare reference line that is a declared alias of `existing` (e.g. an
320
- // edge-block header `web` for a node `"Web App" as web`) is an intentional
321
- // pointer, not a near-duplicate name — don't flag it as a case/whitespace
322
- // typo. Genuine collisions (no matching alias) still warn.
323
- const isAliasRef = nameAliasMap.get(currentNode.label) === key;
324
- if (incomingDisplay !== existingDisplay && !isAliasRef) {
325
- result.diagnostics.push(
326
- makeDgmoError(
327
- currentNode.lineNumber,
328
- nameMergedMessage({
329
- incomingDisplay,
330
- incomingLine: currentNode.lineNumber,
331
- existingDisplay,
332
- existingLine: existing.lineNumber,
333
- }),
334
- 'warning',
335
- NAME_DIAGNOSTIC_CODES.NAME_MERGED
336
- )
337
- );
338
- }
339
- currentNode = null;
340
- return;
341
- }
342
- // Validate mutual exclusion: concurrency vs instances/max-rps
343
- const keys = new Set(currentNode.properties.map((p) => p.key));
344
- if (
345
- keys.has('concurrency') &&
346
- (keys.has('instances') || keys.has('max-rps'))
347
- ) {
348
- const conflicting = [
349
- keys.has('instances') ? 'instances' : '',
350
- keys.has('max-rps') ? 'max-rps' : '',
351
- ]
352
- .filter(Boolean)
353
- .join(', ');
354
- warn(
355
- currentNode.lineNumber,
356
- `'concurrency' (serverless) is mutually exclusive with ${conflicting}. Serverless nodes scale via concurrency, not instances.`
357
- );
358
- }
359
- // Validate mutual exclusion: buffer (queue) vs max-rps (service)
360
- if (keys.has('buffer') && keys.has('max-rps')) {
361
- warn(
362
- currentNode.lineNumber,
363
- `'buffer' (queue) and 'max-rps' (service) represent different capacity models. A queue buffers messages; a service processes them.`
364
- );
365
- }
366
- nodeMap.set(key, currentNode);
367
- result.nodes.push(currentNode);
368
- currentNode = null;
369
- }
370
-
371
- function finishCurrentTagGroup() {
372
- if (currentTagGroup) {
373
- result.tagGroups.push(currentTagGroup);
374
- }
375
- currentTagGroup = null;
376
- }
377
-
378
- for (let i = 0; i < lines.length; i++) {
379
- // In-bounds by loop guard.
380
- const raw = lines[i]!;
381
- const lineNumber = i + 1;
382
- const trimmed = raw.trim();
383
- const indent = measureIndent(raw);
384
-
385
- // Skip empty lines and comments
386
- if (!trimmed || trimmed.startsWith('//')) continue;
387
-
388
- // Skip markdown section headers
389
- if (/^#{2,}\s+/.test(trimmed)) continue;
390
-
391
- // ---- Top-level metadata (no indent) ----
392
- if (indent === 0) {
393
- // Close any open blocks
394
- if (indent === 0 && currentNode && !trimmed.startsWith('-')) {
395
- finishCurrentNode();
396
- }
397
-
398
- // First line: `infra [Title]` or legacy `chart: infra` — only ever
399
- // attempted on the first content line (see firstLineConsumed). A node or
400
- // alias named like a chart type elsewhere is a normal node, not a redecl.
401
- if (!firstLineConsumed) {
402
- firstLineConsumed = true;
403
- const firstLineResult = parseFirstLine(trimmed);
404
- if (firstLineResult) {
405
- if (firstLineResult.chartType !== 'infra') {
406
- setError(
407
- lineNumber,
408
- `Expected chart type 'infra', got '${firstLineResult.chartType}'`
409
- );
410
- }
411
- if (firstLineResult.title) {
412
- result.title = firstLineResult.title;
413
- result.titleLineNumber = lineNumber;
414
- }
415
- continue;
416
- }
417
- }
418
-
419
- // direction-tb — bare boolean to switch to top-to-bottom (default is LR)
420
- if (/^direction-tb$/i.test(trimmed)) {
421
- result.direction = 'TB';
422
- continue;
423
- }
424
-
425
- if (trimmed === 'animate' || trimmed === 'no-animate') {
426
- warn(
427
- lineNumber,
428
- '"animate" / "no-animate" have been removed — animation is controlled by the rendering context.'
429
- );
430
- continue;
431
- }
432
-
433
- if (tryParseSharedOption(trimmed, options)) {
434
- continue;
435
- }
436
-
437
- // Top-level options: `key value` (space-separated, no colon)
438
- const optMatch = trimmed.match(OPTION_NOCOLON_RE);
439
- // Capture groups 1 & 2 in-bounds after successful match.
440
- if (optMatch && TOP_LEVEL_OPTIONS.has(optMatch[1]!.toLowerCase())) {
441
- options[optMatch[1]!.toLowerCase()] = optMatch[2]!.trim();
442
- continue;
443
- }
444
-
445
- // Tag group: `tag Name as <alias>` (via shared matchTagBlockHeading)
446
- const tagMatch = matchTagBlockHeading(trimmed);
447
- if (tagMatch) {
448
- finishCurrentNode();
449
- finishCurrentTagGroup();
450
- if (tagMatch.alias) tagAliasSet.add(normalizeName(tagMatch.alias));
451
- currentTagGroup = {
452
- name: tagMatch.name,
453
- alias: tagMatch.alias ?? null,
454
- values: [],
455
- lineNumber,
456
- };
457
- continue;
458
- }
459
-
460
- // [Group Name] [as <alias>] [| t: Engineering] — TD-18 native `as` postfix
461
- const groupMatch = trimmed.match(GROUP_RE);
462
- if (groupMatch) {
463
- finishCurrentNode();
464
- finishCurrentTagGroup();
465
- // Capture group 1 in-bounds after successful match.
466
- const gLabel = groupMatch[1]!.trim();
467
- const groupAlias = groupMatch[2];
468
- const gId = groupId(gLabel);
469
- if (groupAlias) nameAliasMap.set(groupAlias, gId);
470
- const groupMeta = groupMatch[3]
471
- ? extractPipeMetadata(groupMatch[3]).tags
472
- : undefined;
473
- if (groupMeta) {
474
- warnUnknownMetaKeys(
475
- groupMeta,
476
- withTagAliases(INFRA_REGISTRY, tagAliasSet),
477
- (msg) => warn(lineNumber, msg)
478
- );
479
- }
480
- const hasMeta = groupMeta && Object.keys(groupMeta).length > 0;
481
- const newGroup: Writable<InfraGroup> = {
482
- id: gId,
483
- label: gLabel,
484
- ...(hasMeta && { metadata: groupMeta }),
485
- lineNumber,
486
- };
487
- currentGroup = newGroup;
488
- result.groups.push(newGroup);
489
- continue;
490
- }
491
-
492
- // Component at top level (no indent) — supports `as` postfix
493
- const compMatch = trimmed.match(COMPONENT_RE);
494
- if (compMatch) {
495
- finishCurrentNode();
496
- finishCurrentTagGroup();
497
-
498
- const { qname, uname, alias: asAlias, meta } = compMatch.groups!;
499
- const rawName = (qname ?? uname ?? '').trim();
500
- const peeled = peelInfraDecorations(rawName, lineNumber);
501
- const name = peeled.label;
502
- // Explicit `as` capture wins; fall back to an alias the peeler split out
503
- // of a bare name (and the quoted-literal `"Foo as bar"` legacy case).
504
- const alias = asAlias ?? peeled.alias;
505
- const rest = meta ?? '';
506
- const { tags } = extractPipeMetadata(rest);
507
- warnUnknownMetaKeys(
508
- tags,
509
- withTagAliases(INFRA_REGISTRY, tagAliasSet),
510
- (msg) => warn(lineNumber, msg)
511
- );
512
- // A bare reference line may be an alias for an already-declared node
513
- // (its label-slug differs from the alias, e.g. "Web App" as web). Resolve
514
- // through the alias map first so an edge-source/reference merges into
515
- // that node instead of spawning a duplicate stub.
516
- const id = nameAliasMap.get(name) ?? nodeId(name);
517
- if (alias) nameAliasMap.set(alias, id);
518
- const isEdge = EDGE_NODE_NAMES.has(id.toLowerCase());
519
-
520
- currentNode = {
521
- id,
522
- label: name,
523
- properties: [],
524
- groupId: null,
525
- tags,
526
- isEdge,
527
- lineNumber,
528
- };
529
- nodeMutableTags.set(id, tags);
530
- currentGroup = null;
531
- baseIndent = 0;
532
- continue;
533
- }
534
- }
535
-
536
- // ---- Indented lines ----
537
-
538
- // Tag value inside tag group — first value is the default unless another is marked `default`
539
- if (currentTagGroup && indent > 0) {
540
- const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
541
- // Trailing-token color (universal rule, §1.5): peel off a lowercase
542
- // recognized color word from the end of the line. Downstream stores
543
- // the raw color NAME (not the palette hex) so the renderer can resolve
544
- // against whichever theme/palette is active at render time.
545
- const lastSpaceIdx = cleanEntry.lastIndexOf(' ');
546
- let valueName = cleanEntry;
547
- let rawColor: string | undefined;
548
- if (lastSpaceIdx > 0) {
549
- const trailing = cleanEntry.substring(lastSpaceIdx + 1);
550
- if (isRecognizedColorName(trailing)) {
551
- rawColor = trailing;
552
- valueName = cleanEntry.substring(0, lastSpaceIdx).trimEnd();
553
- }
554
- }
555
- const tvMatch = valueName.match(TAG_VALUE_RE);
556
- if (tvMatch || /^\w+$/.test(valueName)) {
557
- currentTagGroup.values.push({
558
- name: valueName,
559
- ...(rawColor !== undefined && { color: rawColor }),
560
- });
561
- if (isDefault) {
562
- currentTagGroup.defaultValue = valueName;
563
- } else if (currentTagGroup.values.length === 1) {
564
- currentTagGroup.defaultValue = valueName;
565
- }
566
- continue;
567
- }
568
- warn(
569
- lineNumber,
570
- `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}' — a value is a bare word with an optional trailing color (e.g. 'Production red'); no ':' or '|'.`
571
- );
572
- continue;
573
- }
574
-
575
- // Inside a [Group] but no current node — group properties or component declaration
576
- if (currentGroup && !currentNode && indent > 0) {
577
- // Group-level properties (instances, collapsed)
578
- const propMatch = trimmed.match(PROPERTY_RE);
579
- if (propMatch) {
580
- // Capture groups 1 & 2 in-bounds after successful match.
581
- const key = propMatch[1]!.toLowerCase();
582
- const val = propMatch[2]!.trim();
583
- if (key === 'instances') {
584
- const rangeM = val.match(RANGE_RE);
585
- if (rangeM) {
586
- currentGroup.instances = val;
587
- } else {
588
- const num = parseInt(val, 10);
589
- if (!isNaN(num)) currentGroup.instances = num;
590
- }
591
- continue;
592
- }
593
- if (key === 'collapsed') {
594
- currentGroup.collapsed = val.toLowerCase() === 'true';
595
- continue;
596
- }
597
- // Fall through to component matching — could be a component name
598
- // that happens to match PROPERTY_RE (e.g., "MyService: v2")
599
- }
600
-
601
- const compMatch = trimmed.match(COMPONENT_RE);
602
- if (compMatch) {
603
- finishCurrentTagGroup();
604
- const { qname, uname, alias: asAlias, meta } = compMatch.groups!;
605
- const rawName = (qname ?? uname ?? '').trim();
606
- const peeled = peelInfraDecorations(rawName, lineNumber);
607
- const name = peeled.label;
608
- const alias = asAlias ?? peeled.alias;
609
- const rest = meta ?? '';
610
- const { tags: nodeTags } = extractPipeMetadata(rest);
611
- warnUnknownMetaKeys(
612
- nodeTags,
613
- withTagAliases(INFRA_REGISTRY, tagAliasSet),
614
- (msg) => warn(lineNumber, msg)
615
- );
616
- // A bare reference line may be an alias for an already-declared node
617
- // (its label-slug differs from the alias, e.g. "Web App" as web). Resolve
618
- // through the alias map first so an edge-source/reference merges into
619
- // that node instead of spawning a duplicate stub.
620
- const id = nameAliasMap.get(name) ?? nodeId(name);
621
- if (alias) nameAliasMap.set(alias, id);
622
- // Cascade group metadata into node tags; node-level metadata overrides
623
- const tags: Record<string, string> = currentGroup.metadata
624
- ? { ...currentGroup.metadata, ...nodeTags }
625
- : nodeTags;
626
-
627
- currentNode = {
628
- id,
629
- label: name,
630
- properties: [],
631
- groupId: currentGroup.id,
632
- tags,
633
- isEdge: false,
634
- lineNumber,
635
- };
636
- nodeMutableTags.set(id, tags);
637
- baseIndent = indent;
638
- continue;
639
- }
640
- }
641
-
642
- // Inside a component block — properties and connections
643
- if (currentNode && indent > baseIndent) {
644
- // Detect deprecated xN fanout syntax
645
- const deprecatedFanout = trimmed.match(DEPRECATED_FANOUT_RE);
646
- if (
647
- deprecatedFanout &&
648
- (trimmed.startsWith('->') ||
649
- trimmed.startsWith('-') ||
650
- trimmed.startsWith('~'))
651
- ) {
652
- const n = deprecatedFanout[1];
653
- setError(
654
- lineNumber,
655
- `'x${n}' fanout syntax is no longer supported — use 'fanout: ${n}' instead`
656
- );
657
- continue;
658
- }
659
-
660
- // Async simple connection: ~> Target
661
- const asyncSimpleConn = trimmed.match(ASYNC_SIMPLE_CONNECTION_RE);
662
- if (asyncSimpleConn) {
663
- // Capture group 1 in-bounds after successful match.
664
- const targetRaw = asyncSimpleConn[1]!.trim();
665
- const pipeMeta = extractPipeMetadata(targetRaw);
666
- const targetName = pipeMeta.clean || targetRaw;
667
- warnUnparsedPipeMeta(targetName, lineNumber, warn);
668
- warnUnknownMetaKeys(pipeMeta.tags, EDGE_PIPE_REGISTRY, (msg) =>
669
- warn(lineNumber, msg)
670
- );
671
- const split = pipeMeta.tags['split']
672
- ? parseFloat(pipeMeta.tags['split'])
673
- : null;
674
- const fanoutRaw = pipeMeta.tags['fanout']
675
- ? parseInt(pipeMeta.tags['fanout'], 10)
676
- : null;
677
- if (fanoutRaw !== null && fanoutRaw < 1) {
678
- warn(
679
- lineNumber,
680
- `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
681
- );
682
- }
683
- const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
684
- const targetIdAsync = resolveTargetId(targetName);
685
- rememberTargetDisplay(targetIdAsync, targetName);
686
- result.edges.push({
687
- sourceId: currentNode.id,
688
- targetId: targetIdAsync,
689
- label: '',
690
- async: true,
691
- split,
692
- fanout,
693
- lineNumber,
694
- });
695
- continue;
696
- }
697
-
698
- // Async labeled connection: ~label~> Target
699
- const asyncConnMatch = trimmed.match(ASYNC_CONNECTION_RE);
700
- if (asyncConnMatch) {
701
- const rawLabel = asyncConnMatch[1] ?? '';
702
- const labelResult = parseInArrowLabel(rawLabel, lineNumber);
703
- result.diagnostics.push(...labelResult.diagnostics);
704
- const label = labelResult.label ?? '';
705
- // Capture group 2 in-bounds after successful match.
706
- const targetRaw = asyncConnMatch[2]!.trim();
707
- const pipeMeta = extractPipeMetadata(targetRaw);
708
- const targetName = pipeMeta.clean || targetRaw;
709
- warnUnparsedPipeMeta(targetName, lineNumber, warn);
710
- warnUnknownMetaKeys(pipeMeta.tags, EDGE_PIPE_REGISTRY, (msg) =>
711
- warn(lineNumber, msg)
712
- );
713
- const split = pipeMeta.tags['split']
714
- ? parseFloat(pipeMeta.tags['split'])
715
- : null;
716
- const fanoutRaw = pipeMeta.tags['fanout']
717
- ? parseInt(pipeMeta.tags['fanout'], 10)
718
- : null;
719
- if (fanoutRaw !== null && fanoutRaw < 1) {
720
- warn(
721
- lineNumber,
722
- `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
723
- );
724
- }
725
- const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
726
-
727
- let targetId: string;
728
- const targetGroupMatch = targetName.match(/^\[([^\]]+)\]/);
729
- if (targetGroupMatch) {
730
- // Capture group 1 in-bounds after successful match.
731
- targetId = groupId(targetGroupMatch[1]!);
732
- rememberTargetDisplay(targetId, `[${targetGroupMatch[1]}]`);
733
- } else {
734
- targetId = resolveTargetId(targetName);
735
- rememberTargetDisplay(targetId, targetName);
736
- }
737
-
738
- result.edges.push({
739
- sourceId: currentNode.id,
740
- targetId,
741
- label,
742
- async: true,
743
- split,
744
- fanout,
745
- lineNumber,
746
- });
747
- continue;
748
- }
749
-
750
- // Simple connection: -> Target or -> Target | fanout: 5
751
- const simpleConn = trimmed.match(SIMPLE_CONNECTION_RE);
752
- if (simpleConn) {
753
- // Capture group 1 in-bounds after successful match.
754
- const targetRaw = simpleConn[1]!.trim();
755
- const pipeMeta = extractPipeMetadata(targetRaw);
756
- const targetName = pipeMeta.clean || targetRaw;
757
- warnUnparsedPipeMeta(targetName, lineNumber, warn);
758
- warnUnknownMetaKeys(pipeMeta.tags, EDGE_PIPE_REGISTRY, (msg) =>
759
- warn(lineNumber, msg)
760
- );
761
- const split = pipeMeta.tags['split']
762
- ? parseFloat(pipeMeta.tags['split'])
763
- : null;
764
- const fanoutRaw = pipeMeta.tags['fanout']
765
- ? parseInt(pipeMeta.tags['fanout'], 10)
766
- : null;
767
- if (fanoutRaw !== null && fanoutRaw < 1) {
768
- warn(
769
- lineNumber,
770
- `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
771
- );
772
- }
773
- const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
774
- const targetIdSimple = resolveTargetId(targetName);
775
- rememberTargetDisplay(targetIdSimple, targetName);
776
- result.edges.push({
777
- sourceId: currentNode.id,
778
- targetId: targetIdSimple,
779
- label: '',
780
- async: false,
781
- split,
782
- fanout,
783
- lineNumber,
784
- });
785
- continue;
786
- }
787
-
788
- // Labeled connection: -label-> Target | split: N%, fanout: 3
789
- const connMatch = trimmed.match(CONNECTION_RE);
790
- if (connMatch) {
791
- const rawLabel = connMatch[1] ?? '';
792
- const labelResult = parseInArrowLabel(rawLabel, lineNumber);
793
- result.diagnostics.push(...labelResult.diagnostics);
794
- const label = labelResult.label ?? '';
795
- // Capture group 2 in-bounds after successful match.
796
- const targetRaw = connMatch[2]!.trim();
797
- const pipeMeta = extractPipeMetadata(targetRaw);
798
- const targetName = pipeMeta.clean || targetRaw;
799
- warnUnparsedPipeMeta(targetName, lineNumber, warn);
800
- warnUnknownMetaKeys(pipeMeta.tags, EDGE_PIPE_REGISTRY, (msg) =>
801
- warn(lineNumber, msg)
802
- );
803
- const split = pipeMeta.tags['split']
804
- ? parseFloat(pipeMeta.tags['split'])
805
- : null;
806
- const fanoutRaw = pipeMeta.tags['fanout']
807
- ? parseInt(pipeMeta.tags['fanout'], 10)
808
- : null;
809
- if (fanoutRaw !== null && fanoutRaw < 1) {
810
- warn(
811
- lineNumber,
812
- `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
813
- );
814
- }
815
- const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
816
-
817
- // Target might be a group ref like [API Pods] or an alias literal.
818
- let targetId: string;
819
- const targetGroupMatch = targetName.match(/^\[([^\]]+)\]/);
820
- if (targetGroupMatch) {
821
- // Capture group 1 in-bounds after successful match.
822
- targetId = groupId(targetGroupMatch[1]!);
823
- rememberTargetDisplay(targetId, `[${targetGroupMatch[1]}]`);
824
- } else {
825
- targetId = resolveTargetId(targetName);
826
- rememberTargetDisplay(targetId, targetName);
827
- }
828
-
829
- result.edges.push({
830
- sourceId: currentNode.id,
831
- targetId,
832
- label,
833
- async: false,
834
- split,
835
- fanout,
836
- lineNumber,
837
- });
838
- continue;
839
- }
840
-
841
- // Empty description (no value) — silently skip rather than emitting "Unexpected line"
842
- if (/^description\s*:?\s*$/i.test(trimmed)) continue;
843
-
844
- // Property: key: value
845
- const propMatch = trimmed.match(PROPERTY_RE);
846
- if (propMatch) {
847
- // Capture groups 1 & 2 in-bounds after successful match.
848
- const key = propMatch[1]!.toLowerCase();
849
- const rawVal = propMatch[2]!.trim();
850
-
851
- // description is display metadata, not a behavior key; silently ignored on edge nodes.
852
- // Single-line only — no length enforcement, but keep it short for legibility.
853
- if (key === 'description' && currentNode) {
854
- if (!currentNode.isEdge) {
855
- pushDescription(currentNode, rawVal);
856
- }
857
- continue;
858
- }
859
-
860
- // Indented tag assignment — `t: Edge` on its own line, the indented twin
861
- // of same-line `Name t: Edge`. A tag alias is neither a behavior property
862
- // nor 'description', so without this it falls through to the unknown-key
863
- // path and is mis-collected as description text, silently dropping the
864
- // node's tier. currentNode.tags is the same object held in
865
- // nodeMutableTags, so a direct assign is enough.
866
- if (currentNode && tagAliasSet.has(normalizeName(key))) {
867
- const mut = nodeMutableTags.get(currentNode.id);
868
- if (mut) mut[tagAttrKey(key)] = rawVal;
869
- continue;
870
- }
871
-
872
- // Unknown keys: decide between property typo warning vs description collection.
873
- // Heuristic: if the key looks like a plausible property (alphanumeric-hyphen, close
874
- // match to a known key, or the value looks numeric/percentage), warn as typo.
875
- // Otherwise treat the whole line as description text.
876
- if (!INFRA_BEHAVIOR_KEYS.has(key) && !EDGE_ONLY_KEYS.has(key)) {
877
- const allKeys = [...INFRA_BEHAVIOR_KEYS, ...EDGE_ONLY_KEYS];
878
- const hint = suggest(key, allKeys);
879
- const valueLooksNumeric = /^[\d.]+%?$/.test(rawVal);
880
- if (hint || valueLooksNumeric) {
881
- // Likely a typo — warn
882
- let msg = `Unknown infra property '${key}'.`;
883
- if (hint) msg += ` ${hint}`;
884
- else
885
- msg +=
886
- ` Properties are colon-keyed 'key: value' from the known schema` +
887
- ` (e.g. latency-ms, max-rps, cache-hit, instances, concurrency).`;
888
- warn(lineNumber, msg);
889
- } else if (!currentNode.isEdge) {
890
- // Likely prose — collect as description
891
- pushDescription(currentNode, trimmed);
892
- continue;
893
- }
894
- continue;
895
- }
896
-
897
- // Validate edge-only keys
898
- if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
899
- warn(
900
- lineNumber,
901
- `Property '${key}' belongs on the entry point — declare it indented under an 'internet' or 'edge' node, not on '${currentNode.label}'.`
902
- );
903
- }
904
-
905
- const value = parsePropertyValue(rawVal);
906
- currentNode.properties.push({ key, value, lineNumber });
907
- continue;
908
- }
909
-
910
- // Indented line inside a component: only the `description` keyword
911
- // attaches as a description; a bare prose line is no longer auto-promoted.
912
- const descResult = tryStripDescriptionKeyword(trimmed);
913
- if (descResult.isKeyword && !descResult.needsColon) {
914
- if (currentNode.isEdge) {
915
- // description on edge nodes is silently ignored
916
- continue;
917
- }
918
- pushDescription(currentNode, descResult.text);
919
- continue;
920
- }
921
- warn(
922
- lineNumber,
923
- `Unexpected line inside component '${currentNode.label}'. Expected a property (key: value), connection (-> Target), or a description (description: text).`
924
- );
925
- continue;
926
- }
927
-
928
- // Component inside group (same indent as group children)
929
- if (currentGroup && indent > 0) {
930
- finishCurrentNode();
931
-
932
- const compMatch = trimmed.match(COMPONENT_RE);
933
- if (compMatch) {
934
- const { qname, uname, alias: asAlias, meta } = compMatch.groups!;
935
- const rawName = (qname ?? uname ?? '').trim();
936
- const peeled = peelInfraDecorations(rawName, lineNumber);
937
- const name = peeled.label;
938
- const alias = asAlias ?? peeled.alias;
939
- const rest = meta ?? '';
940
- const { tags: nodeTags } = extractPipeMetadata(rest);
941
- warnUnknownMetaKeys(
942
- nodeTags,
943
- withTagAliases(INFRA_REGISTRY, tagAliasSet),
944
- (msg) => warn(lineNumber, msg)
945
- );
946
- // A bare reference line may be an alias for an already-declared node
947
- // (its label-slug differs from the alias, e.g. "Web App" as web). Resolve
948
- // through the alias map first so an edge-source/reference merges into
949
- // that node instead of spawning a duplicate stub.
950
- const id = nameAliasMap.get(name) ?? nodeId(name);
951
- if (alias) nameAliasMap.set(alias, id);
952
- const tags: Record<string, string> = currentGroup.metadata
953
- ? { ...currentGroup.metadata, ...nodeTags }
954
- : nodeTags;
955
-
956
- currentNode = {
957
- id,
958
- label: name,
959
- properties: [],
960
- groupId: currentGroup.id,
961
- tags,
962
- isEdge: false,
963
- lineNumber,
964
- };
965
- nodeMutableTags.set(id, tags);
966
- baseIndent = indent;
967
- continue;
968
- }
969
- }
970
-
971
- // If we reach here and indent is 0, try as a top-level component
972
- if (indent === 0) {
973
- const compMatch = trimmed.match(COMPONENT_RE);
974
- if (compMatch) {
975
- finishCurrentNode();
976
- finishCurrentTagGroup();
977
- currentGroup = null;
978
-
979
- const { qname, uname, alias: asAlias, meta } = compMatch.groups!;
980
- const rawName = (qname ?? uname ?? '').trim();
981
- const peeled = peelInfraDecorations(rawName, lineNumber);
982
- const name = peeled.label;
983
- const alias = asAlias ?? peeled.alias;
984
- const rest = meta ?? '';
985
- const { tags } = extractPipeMetadata(rest);
986
- warnUnknownMetaKeys(
987
- tags,
988
- withTagAliases(INFRA_REGISTRY, tagAliasSet),
989
- (msg) => warn(lineNumber, msg)
990
- );
991
- // A bare reference line may be an alias for an already-declared node
992
- // (its label-slug differs from the alias, e.g. "Web App" as web). Resolve
993
- // through the alias map first so an edge-source/reference merges into
994
- // that node instead of spawning a duplicate stub.
995
- const id = nameAliasMap.get(name) ?? nodeId(name);
996
- if (alias) nameAliasMap.set(alias, id);
997
-
998
- currentNode = {
999
- id,
1000
- label: name,
1001
- properties: [],
1002
- groupId: null,
1003
- tags,
1004
- isEdge: EDGE_NODE_NAMES.has(id.toLowerCase()),
1005
- lineNumber,
1006
- };
1007
- nodeMutableTags.set(id, tags);
1008
- baseIndent = 0;
1009
- continue;
1010
- }
1011
- }
1012
-
1013
- // Catch-all: nothing matched this line
1014
- warn(
1015
- lineNumber,
1016
- currentGroup
1017
- ? `Unexpected line: '${trimmed}'. Inside a group, indent a component name under the '[Group]'.`
1018
- : `Unexpected line: '${trimmed}'. Expected a node ('Api Gateway'), a group ('[Backend]'), a tag group ('tag Tier as t'), or an option ('default-rps 100'); node properties go indented under a node as 'key: value'.`
1019
- );
1020
- }
1021
-
1022
- // Flush last open blocks
1023
- finishCurrentNode();
1024
- finishCurrentTagGroup();
1025
-
1026
- // Ensure referenced targets exist (create stub nodes for forward references)
1027
- for (const edge of result.edges) {
1028
- if (!nodeMap.has(edge.targetId)) {
1029
- // Check if target is a group
1030
- const isGroup = result.groups.some((g) => g.id === edge.targetId);
1031
- if (!isGroup) {
1032
- // Create a stub node for forward-referenced targets. Display label
1033
- // recovers the user's first-seen casing so the rendered SVG shows
1034
- // 'CDN' instead of the lowercased internal id.
1035
- const stubDisplay = targetDisplays.get(edge.targetId) ?? edge.targetId;
1036
- const stubTags: Record<string, string> = {};
1037
- const stub: Writable<InfraNode> = {
1038
- id: edge.targetId,
1039
- label: stubDisplay,
1040
- properties: [],
1041
- groupId: null,
1042
- tags: stubTags,
1043
- isEdge: false,
1044
- lineNumber: edge.lineNumber,
1045
- };
1046
- // stub.id is edge.targetId, already-normalized via nodeId() at edge construction
1047
- // eslint-disable-next-line name-normalize/required-at-insertion
1048
- nodeMap.set(stub.id, stub);
1049
- result.nodes.push(stub);
1050
- nodeMutableTags.set(stub.id, stubTags);
1051
- }
1052
- }
1053
- }
1054
-
1055
- // Inject default tag values into nodes that don't have one
1056
- for (const tg of result.tagGroups) {
1057
- if (!tg.defaultValue) continue;
1058
- const key = tagAttrKey(tg.alias ?? tg.name);
1059
- for (const node of result.nodes) {
1060
- if (node.isEdge) continue;
1061
- if (!(key in node.tags)) {
1062
- const tags = nodeMutableTags.get(node.id);
1063
- if (tags) tags[key] = tg.defaultValue;
1064
- }
1065
- }
1066
- }
1067
-
1068
- validateTagGroupNames(result.tagGroups, warn, setError);
1069
-
1070
- checkReachability(result);
1071
-
1072
- return result;
1073
- }
1074
-
1075
- /**
1076
- * Reachability lint: an infra diagram traces request traffic flowing inward
1077
- * from an `internet`/`edge` entry, so every node must have a directed path
1078
- * back to an entry. A node nothing routes to carries no traffic — it's dead
1079
- * and doesn't belong. Emits `warning`-severity diagnostics (never blocks
1080
- * rendering). Mirrors the compute model: an edge targeting a `[Group]`
1081
- * delivers to all of that group's children.
1082
- */
1083
- function checkReachability(result: Writable<ParsedInfra>): void {
1084
- if (result.nodes.length === 0) return;
1085
-
1086
- const entries = result.nodes.filter((n) => n.isEdge);
1087
-
1088
- // No entry at all: the rule can't be satisfied. One diagnostic, not N.
1089
- if (entries.length === 0) {
1090
- const line = result.titleLineNumber ?? result.nodes[0]?.lineNumber ?? 1;
1091
- result.diagnostics.push(emit(GRAPH_DX.INFRA_NO_ENTRY, line));
1092
- return;
1093
- }
1094
-
1095
- // groupId -> child node ids (for expanding edges that target a [Group]).
1096
- const groupChildMap = new Map<string, string[]>();
1097
- for (const node of result.nodes) {
1098
- if (node.groupId) {
1099
- const list = groupChildMap.get(node.groupId) ?? [];
1100
- list.push(node.id);
1101
- // node.groupId is already a normalized id from parse time.
1102
- // eslint-disable-next-line name-normalize/required-at-insertion
1103
- groupChildMap.set(node.groupId, list);
1104
- }
1105
- }
1106
-
1107
- // Outbound adjacency: an edge to a group reaches every child of that group.
1108
- const outbound = new Map<string, string[]>();
1109
- for (const edge of result.edges) {
1110
- const targets = groupChildMap.get(edge.targetId) ?? [edge.targetId];
1111
- const list = outbound.get(edge.sourceId) ?? [];
1112
- list.push(...targets);
1113
- outbound.set(edge.sourceId, list);
1114
- }
1115
-
1116
- // BFS from every entry node.
1117
- const reachable = new Set<string>();
1118
- const queue: string[] = [];
1119
- for (const entry of entries) {
1120
- reachable.add(entry.id);
1121
- queue.push(entry.id);
1122
- }
1123
- while (queue.length > 0) {
1124
- const current = queue.shift()!;
1125
- for (const next of outbound.get(current) ?? []) {
1126
- if (!reachable.has(next)) {
1127
- reachable.add(next);
1128
- queue.push(next);
1129
- }
1130
- }
1131
- }
1132
-
1133
- for (const node of result.nodes) {
1134
- if (node.isEdge || reachable.has(node.id)) continue;
1135
- result.diagnostics.push(
1136
- emit(GRAPH_DX.INFRA_UNREACHABLE, node.lineNumber, { label: node.label })
1137
- );
1138
- }
1139
- }
1140
-
1141
- // ============================================================
1142
- // Symbol extraction (for completion API)
1143
- // ============================================================
1144
-
1145
- import type { DiagramSymbols } from '../completion-types';
1146
-
1147
- /**
1148
- * Extract component names (entities) from infra document text.
1149
- * Used by the dgmo completion API for ghost hints and popup completions.
1150
- */
1151
- /** Strip ` as <alias>` and ` is a/an <type>` decorations from a node name
1152
- * so completion suggests the bare identifier the user will reference. */
1153
- function stripNodeDecorations(name: string): string {
1154
- let s = name.trim();
1155
- const aliasMatch = s.match(/^(.*?)\s+as\s+[A-Za-z][A-Za-z0-9_]{0,11}\s*$/);
1156
- // Capture group 1 in-bounds after successful match.
1157
- if (aliasMatch) s = aliasMatch[1]!.trim();
1158
- const isAMatch = s.match(/^(.*?)\s+is\s+an?\s+[A-Za-z][\w-]*\s*$/i);
1159
- // Capture group 1 in-bounds after successful match.
1160
- if (isAMatch) s = isAMatch[1]!.trim();
1161
- return s;
1162
- }
1163
-
1164
- export function extractSymbols(docText: string): DiagramSymbols {
1165
- const entities: string[] = [];
1166
- let inMetadata = true;
1167
- let inTagGroup = false;
1168
- for (const rawLine of docText.split('\n')) {
1169
- const line = rawLine.trim();
1170
- if (line.length === 0) continue;
1171
- const indented = /^\s/.test(rawLine);
1172
-
1173
- // Metadata phase: skip until first non-metadata root-level line.
1174
- // Metadata includes: `infra [Title]`, `chart: type`, `direction X`, `slo-*`, etc.
1175
- if (inMetadata) {
1176
- if (!indented) {
1177
- // Recognize new-style bare options (`key value`) and old-style (`key: value`)
1178
- const firstLine = parseFirstLine(line);
1179
- if (firstLine) continue; // chart type line
1180
- if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line))
1181
- continue;
1182
- if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
1183
- inMetadata = false;
1184
- } else {
1185
- continue;
1186
- }
1187
- }
1188
-
1189
- if (!indented) {
1190
- // Root-level: tag group declaration, group header, or component
1191
- if (/^tag\s/i.test(line)) {
1192
- inTagGroup = true;
1193
- continue;
1194
- }
1195
- if (/^tag\s*:/i.test(line)) {
1196
- inTagGroup = true;
1197
- continue;
1198
- } // legacy
1199
- inTagGroup = false;
1200
- if (/^\[/.test(line)) continue; // [Group] header
1201
- const m = COMPONENT_RE.exec(line);
1202
- if (m) {
1203
- const name = stripNodeDecorations((m[1] ?? m[2] ?? '').trim());
1204
- if (name && !entities.includes(name)) entities.push(name);
1205
- }
1206
- } else {
1207
- // Indented: skip tag values, connections, and properties; extract grouped components
1208
- if (inTagGroup) continue;
1209
- if (/^->/.test(line)) continue; // simple connection
1210
- if (/^~>/.test(line)) continue; // async simple connection
1211
- if (/^-[^>]+-?>/.test(line)) continue; // labeled connection
1212
- if (/^~[^~]+~>/.test(line)) continue; // async labeled connection
1213
- if (/^\w[\w-]*\s*:/.test(line)) continue; // property (key: value) legacy
1214
- // New-style property: first token is a known behavior/property key
1215
- // String.split always returns at least one element; line.length > 0 here.
1216
- const firstToken = line.split(/\s/)[0]!.toLowerCase();
1217
- if (
1218
- (INFRA_BEHAVIOR_KEYS.has(firstToken) ||
1219
- EDGE_ONLY_KEYS.has(firstToken) ||
1220
- firstToken === 'description' ||
1221
- firstToken === 'instances' ||
1222
- firstToken === 'collapsed') &&
1223
- /\s/.test(line)
1224
- )
1225
- continue;
1226
- const m = COMPONENT_RE.exec(line);
1227
- if (m) {
1228
- const name = stripNodeDecorations((m[1] ?? m[2] ?? '').trim());
1229
- if (name && !entities.includes(name)) entities.push(name);
1230
- }
1231
- }
1232
- }
1233
- return { kind: 'infra', entities };
1234
- }