unspaghettit 0.2.0 → 0.4.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 (772) hide show
  1. package/README.md +56 -18
  2. package/build/client/_app/immutable/assets/0.DCOD5ne_.css +1 -0
  3. package/build/client/_app/immutable/assets/0.DCOD5ne_.css.br +0 -0
  4. package/build/client/_app/immutable/assets/0.DCOD5ne_.css.gz +0 -0
  5. package/build/client/_app/immutable/assets/12.nv0I59TU.css.gz +0 -0
  6. package/build/client/_app/immutable/assets/3.B3akUL9A.css.gz +0 -0
  7. package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css +1 -0
  8. package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css.br +0 -0
  9. package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css.gz +0 -0
  10. package/build/client/_app/immutable/chunks/B3vnQs252.js +1 -0
  11. package/build/client/_app/immutable/chunks/B3vnQs252.js.br +0 -0
  12. package/build/client/_app/immutable/chunks/B3vnQs252.js.gz +0 -0
  13. package/build/client/_app/immutable/chunks/{CxyLMlpt2.js → BANF9BhQ2.js} +1 -1
  14. package/build/client/_app/immutable/chunks/BANF9BhQ2.js.br +0 -0
  15. package/build/client/_app/immutable/chunks/BANF9BhQ2.js.gz +0 -0
  16. package/build/client/_app/immutable/chunks/BJdTO_n6.js +1 -0
  17. package/build/client/_app/immutable/chunks/BJdTO_n6.js.br +0 -0
  18. package/build/client/_app/immutable/chunks/BJdTO_n6.js.gz +0 -0
  19. package/build/client/_app/immutable/chunks/BPl9BITm.js.gz +0 -0
  20. package/build/client/_app/immutable/chunks/BQ9GodWX2.js.gz +0 -0
  21. package/build/client/_app/immutable/chunks/Bcyn5YUg.js +4 -0
  22. package/build/client/_app/immutable/chunks/Bcyn5YUg.js.br +0 -0
  23. package/build/client/_app/immutable/chunks/Bcyn5YUg.js.gz +0 -0
  24. package/build/client/_app/immutable/chunks/Bfc9rQXP.js +1 -0
  25. package/build/client/_app/immutable/chunks/Bfc9rQXP.js.br +0 -0
  26. package/build/client/_app/immutable/chunks/Bfc9rQXP.js.gz +0 -0
  27. package/build/client/_app/immutable/chunks/{BfMYiIUi.js → BjxqhpPG.js} +1 -1
  28. package/build/client/_app/immutable/chunks/BjxqhpPG.js.br +0 -0
  29. package/build/client/_app/immutable/chunks/BjxqhpPG.js.gz +0 -0
  30. package/build/client/_app/immutable/chunks/ByaoVvNB.js +1 -0
  31. package/build/client/_app/immutable/chunks/ByaoVvNB.js.br +0 -0
  32. package/build/client/_app/immutable/chunks/ByaoVvNB.js.gz +0 -0
  33. package/build/client/_app/immutable/chunks/BzLd0mTH.js.gz +0 -0
  34. package/build/client/_app/immutable/chunks/C8b3IMj8.js +1 -0
  35. package/build/client/_app/immutable/chunks/C8b3IMj8.js.br +0 -0
  36. package/build/client/_app/immutable/chunks/C8b3IMj8.js.gz +0 -0
  37. package/build/client/_app/immutable/chunks/CXvvf4uQ.js +1 -0
  38. package/build/client/_app/immutable/chunks/CXvvf4uQ.js.br +0 -0
  39. package/build/client/_app/immutable/chunks/CXvvf4uQ.js.gz +0 -0
  40. package/build/client/_app/immutable/chunks/Cl7Z3e6-.js.gz +0 -0
  41. package/build/client/_app/immutable/chunks/ClC4qR7W2.js +2 -0
  42. package/build/client/_app/immutable/chunks/ClC4qR7W2.js.br +0 -0
  43. package/build/client/_app/immutable/chunks/ClC4qR7W2.js.gz +0 -0
  44. package/build/client/_app/immutable/chunks/CuGlh1c2.js +1 -0
  45. package/build/client/_app/immutable/chunks/CuGlh1c2.js.br +0 -0
  46. package/build/client/_app/immutable/chunks/CuGlh1c2.js.gz +0 -0
  47. package/build/client/_app/immutable/chunks/CwzIdrQR.js +1 -0
  48. package/build/client/_app/immutable/chunks/CwzIdrQR.js.br +0 -0
  49. package/build/client/_app/immutable/chunks/CwzIdrQR.js.gz +0 -0
  50. package/build/client/_app/immutable/chunks/D5speDV82.js +908 -0
  51. package/build/client/_app/immutable/chunks/D5speDV82.js.br +0 -0
  52. package/build/client/_app/immutable/chunks/D5speDV82.js.gz +0 -0
  53. package/build/client/_app/immutable/chunks/D64sGJRN.js +1 -0
  54. package/build/client/_app/immutable/chunks/D64sGJRN.js.br +0 -0
  55. package/build/client/_app/immutable/chunks/D64sGJRN.js.gz +0 -0
  56. package/build/client/_app/immutable/chunks/DNaA4QRO2.js +1 -0
  57. package/build/client/_app/immutable/chunks/DNaA4QRO2.js.br +0 -0
  58. package/build/client/_app/immutable/chunks/DNaA4QRO2.js.gz +0 -0
  59. package/build/client/_app/immutable/chunks/Da59TXPe.js +1 -0
  60. package/build/client/_app/immutable/chunks/Da59TXPe.js.br +2 -0
  61. package/build/client/_app/immutable/chunks/Da59TXPe.js.gz +0 -0
  62. package/build/client/_app/immutable/chunks/DhTiE3j02.js +2 -0
  63. package/build/client/_app/immutable/chunks/DhTiE3j02.js.br +0 -0
  64. package/build/client/_app/immutable/chunks/DhTiE3j02.js.gz +0 -0
  65. package/build/client/_app/immutable/chunks/Dhqg7r_2.js.gz +0 -0
  66. package/build/client/_app/immutable/chunks/DmyaDy4d2.js +1 -0
  67. package/build/client/_app/immutable/chunks/DmyaDy4d2.js.br +0 -0
  68. package/build/client/_app/immutable/chunks/DmyaDy4d2.js.gz +0 -0
  69. package/build/client/_app/immutable/chunks/DryaQyjN2.js +1 -0
  70. package/build/client/_app/immutable/chunks/DryaQyjN2.js.br +0 -0
  71. package/build/client/_app/immutable/chunks/DryaQyjN2.js.gz +0 -0
  72. package/build/client/_app/immutable/chunks/DtK8qK5Q2.js +1 -0
  73. package/build/client/_app/immutable/chunks/DtK8qK5Q2.js.br +0 -0
  74. package/build/client/_app/immutable/chunks/DtK8qK5Q2.js.gz +0 -0
  75. package/build/client/_app/immutable/chunks/DtblIi6X2.js +1 -0
  76. package/build/client/_app/immutable/chunks/DtblIi6X2.js.br +1 -0
  77. package/build/client/_app/immutable/chunks/DtblIi6X2.js.gz +0 -0
  78. package/build/client/_app/immutable/chunks/UkzDDeO42.js +1 -0
  79. package/build/client/_app/immutable/chunks/UkzDDeO42.js.br +0 -0
  80. package/build/client/_app/immutable/chunks/UkzDDeO42.js.gz +0 -0
  81. package/build/client/_app/immutable/chunks/Vu8LzrwP2.js +1 -0
  82. package/build/client/_app/immutable/chunks/Vu8LzrwP2.js.br +0 -0
  83. package/build/client/_app/immutable/chunks/Vu8LzrwP2.js.gz +0 -0
  84. package/build/client/_app/immutable/chunks/_b612aVb.js +1 -0
  85. package/build/client/_app/immutable/chunks/_b612aVb.js.br +0 -0
  86. package/build/client/_app/immutable/chunks/_b612aVb.js.gz +0 -0
  87. package/build/client/_app/immutable/chunks/aNMRV4sP2.js.gz +0 -0
  88. package/build/client/_app/immutable/chunks/cMamJTsF.js.gz +0 -0
  89. package/build/client/_app/immutable/chunks/ib21i2T_.js.gz +0 -0
  90. package/build/client/_app/immutable/chunks/kNaey6uv.js.gz +0 -0
  91. package/build/client/_app/immutable/chunks/oVMDLirx.js +1 -0
  92. package/build/client/_app/immutable/chunks/oVMDLirx.js.br +0 -0
  93. package/build/client/_app/immutable/chunks/oVMDLirx.js.gz +0 -0
  94. package/build/client/_app/immutable/chunks/olAc024e.js +1 -0
  95. package/build/client/_app/immutable/chunks/olAc024e.js.br +0 -0
  96. package/build/client/_app/immutable/chunks/olAc024e.js.gz +0 -0
  97. package/build/client/_app/immutable/chunks/suTxxv1t.js.gz +0 -0
  98. package/build/client/_app/immutable/chunks/wXIHzLMQ2.js +1 -0
  99. package/build/client/_app/immutable/chunks/wXIHzLMQ2.js.br +0 -0
  100. package/build/client/_app/immutable/chunks/wXIHzLMQ2.js.gz +0 -0
  101. package/build/client/_app/immutable/chunks/wrqalH0a.js +1 -0
  102. package/build/client/_app/immutable/chunks/wrqalH0a.js.br +0 -0
  103. package/build/client/_app/immutable/chunks/wrqalH0a.js.gz +0 -0
  104. package/build/client/_app/immutable/chunks/x60Tn5kT.js +1 -0
  105. package/build/client/_app/immutable/chunks/x60Tn5kT.js.br +0 -0
  106. package/build/client/_app/immutable/chunks/x60Tn5kT.js.gz +0 -0
  107. package/build/client/_app/immutable/chunks/xihTtKlq.js.gz +0 -0
  108. package/build/client/_app/immutable/chunks/{BOrLmFwJ.js → yShDxCNj.js} +1 -1
  109. package/build/client/_app/immutable/chunks/yShDxCNj.js.br +2 -0
  110. package/build/client/_app/immutable/chunks/yShDxCNj.js.gz +0 -0
  111. package/build/client/_app/immutable/entry/app.Etd-exTb.js +2 -0
  112. package/build/client/_app/immutable/entry/app.Etd-exTb.js.br +0 -0
  113. package/build/client/_app/immutable/entry/app.Etd-exTb.js.gz +0 -0
  114. package/build/client/_app/immutable/entry/start.DY2B3u4g.js +1 -0
  115. package/build/client/_app/immutable/entry/start.DY2B3u4g.js.br +0 -0
  116. package/build/client/_app/immutable/entry/start.DY2B3u4g.js.gz +0 -0
  117. package/build/client/_app/immutable/nodes/0.tKfYV3__.js +8 -0
  118. package/build/client/_app/immutable/nodes/0.tKfYV3__.js.br +0 -0
  119. package/build/client/_app/immutable/nodes/0.tKfYV3__.js.gz +0 -0
  120. package/build/client/_app/immutable/nodes/{1.KYdA6ppX.js → 1.CER5ICKt.js} +1 -1
  121. package/build/client/_app/immutable/nodes/1.CER5ICKt.js.br +1 -0
  122. package/build/client/_app/immutable/nodes/1.CER5ICKt.js.gz +0 -0
  123. package/build/client/_app/immutable/nodes/10.rT7cD8kX.js +1 -0
  124. package/build/client/_app/immutable/nodes/10.rT7cD8kX.js.br +0 -0
  125. package/build/client/_app/immutable/nodes/10.rT7cD8kX.js.gz +0 -0
  126. package/build/client/_app/immutable/nodes/11.BAnV_Y5Y.js +2 -0
  127. package/build/client/_app/immutable/nodes/11.BAnV_Y5Y.js.br +0 -0
  128. package/build/client/_app/immutable/nodes/11.BAnV_Y5Y.js.gz +0 -0
  129. package/build/client/_app/immutable/nodes/{9.CMW6a2Lg.js → 12.DCTRFMqo.js} +4 -3
  130. package/build/client/_app/immutable/nodes/12.DCTRFMqo.js.br +0 -0
  131. package/build/client/_app/immutable/nodes/12.DCTRFMqo.js.gz +0 -0
  132. package/build/client/_app/immutable/nodes/{2.DBz20KgG.js → 2.CF488WWF.js} +1 -1
  133. package/build/client/_app/immutable/nodes/2.CF488WWF.js.br +0 -0
  134. package/build/client/_app/immutable/nodes/2.CF488WWF.js.gz +0 -0
  135. package/build/client/_app/immutable/nodes/3.CP7CPPVH.js +1 -0
  136. package/build/client/_app/immutable/nodes/3.CP7CPPVH.js.br +0 -0
  137. package/build/client/_app/immutable/nodes/3.CP7CPPVH.js.gz +0 -0
  138. package/build/client/_app/immutable/nodes/4.D1bxCSiM.js +3 -0
  139. package/build/client/_app/immutable/nodes/4.D1bxCSiM.js.br +0 -0
  140. package/build/client/_app/immutable/nodes/4.D1bxCSiM.js.gz +0 -0
  141. package/build/client/_app/immutable/nodes/5.D6jZgh9y.js +48 -0
  142. package/build/client/_app/immutable/nodes/5.D6jZgh9y.js.br +0 -0
  143. package/build/client/_app/immutable/nodes/5.D6jZgh9y.js.gz +0 -0
  144. package/build/client/_app/immutable/nodes/6.BrBFUA0s.js +1 -0
  145. package/build/client/_app/immutable/nodes/6.BrBFUA0s.js.br +0 -0
  146. package/build/client/_app/immutable/nodes/6.BrBFUA0s.js.gz +0 -0
  147. package/build/client/_app/immutable/nodes/7.pajo8MfM.js +3 -0
  148. package/build/client/_app/immutable/nodes/7.pajo8MfM.js.br +0 -0
  149. package/build/client/_app/immutable/nodes/7.pajo8MfM.js.gz +0 -0
  150. package/build/client/_app/immutable/nodes/8.Cc80_Igi.js +5 -0
  151. package/build/client/_app/immutable/nodes/8.Cc80_Igi.js.br +0 -0
  152. package/build/client/_app/immutable/nodes/8.Cc80_Igi.js.gz +0 -0
  153. package/build/client/_app/immutable/nodes/{7.CemgNJfw.js → 9.D72NaL3l.js} +1 -1
  154. package/build/client/_app/immutable/nodes/9.D72NaL3l.js.br +1 -0
  155. package/build/client/_app/immutable/nodes/9.D72NaL3l.js.gz +0 -0
  156. package/build/client/_app/version.json +1 -1
  157. package/build/client/_app/version.json.br +0 -0
  158. package/build/client/_app/version.json.gz +0 -0
  159. package/build/client/lyriks_logo.svg +148 -0
  160. package/build/client/lyriks_logo.svg.br +0 -0
  161. package/build/client/lyriks_logo.svg.gz +0 -0
  162. package/build/server/chunks/0-8YZc572z.js +15 -0
  163. package/build/server/chunks/0-8YZc572z.js.map +1 -0
  164. package/build/server/chunks/1-DxkfSJLu.js +9 -0
  165. package/build/server/chunks/{1-DPpKAKXV.js.map → 1-DxkfSJLu.js.map} +1 -1
  166. package/build/server/chunks/10-DkObdGtU.js +9 -0
  167. package/build/server/chunks/10-DkObdGtU.js.map +1 -0
  168. package/build/server/chunks/11-DL94HxpZ.js +9 -0
  169. package/build/server/chunks/11-DL94HxpZ.js.map +1 -0
  170. package/build/server/chunks/12-CI74sCkt.js +9 -0
  171. package/build/server/chunks/12-CI74sCkt.js.map +1 -0
  172. package/build/server/chunks/2-qhWwQ4eL.js +9 -0
  173. package/build/server/chunks/2-qhWwQ4eL.js.map +1 -0
  174. package/build/server/chunks/3-CzoZtSTp.js +16 -0
  175. package/build/server/chunks/3-CzoZtSTp.js.map +1 -0
  176. package/build/server/chunks/4-CDa9DBsh.js +16 -0
  177. package/build/server/chunks/4-CDa9DBsh.js.map +1 -0
  178. package/build/server/chunks/5-qD2xxOrq.js +9 -0
  179. package/build/server/chunks/5-qD2xxOrq.js.map +1 -0
  180. package/build/server/chunks/6-B4mWzvDn.js +9 -0
  181. package/build/server/chunks/6-B4mWzvDn.js.map +1 -0
  182. package/build/server/chunks/7-zExv-fTN.js +9 -0
  183. package/build/server/chunks/7-zExv-fTN.js.map +1 -0
  184. package/build/server/chunks/8-BVrGdQg4.js +9 -0
  185. package/build/server/chunks/{6-QQ7r8Rd5.js.map → 8-BVrGdQg4.js.map} +1 -1
  186. package/build/server/chunks/9-BnZLRMYA.js +9 -0
  187. package/build/server/chunks/9-BnZLRMYA.js.map +1 -0
  188. package/build/server/chunks/BehaviorGraph-C-9z37Sm.js +788 -0
  189. package/build/server/chunks/BehaviorGraph-C-9z37Sm.js.map +1 -0
  190. package/build/server/chunks/Clock-BfMWQpmJ.js +5 -0
  191. package/build/server/chunks/Clock-BfMWQpmJ.js.map +1 -0
  192. package/build/server/chunks/DeleteDomain-BvHEgAyp.js +24 -0
  193. package/build/server/chunks/DeleteDomain-BvHEgAyp.js.map +1 -0
  194. package/build/server/chunks/{FeatureValidator-B8qEjAUW.js → DeleteFeature-DnK8sBJI.js} +142 -77
  195. package/build/server/chunks/DeleteFeature-DnK8sBJI.js.map +1 -0
  196. package/build/server/chunks/DeleteProject-BXZcehHZ.js +41 -0
  197. package/build/server/chunks/DeleteProject-BXZcehHZ.js.map +1 -0
  198. package/build/server/chunks/Effect-BJtLR-6l.js +46 -0
  199. package/build/server/chunks/Effect-BJtLR-6l.js.map +1 -0
  200. package/build/server/chunks/{FeatureCard-BQOY6gJQ.js → FeatureCard-Bb50FEfp.js} +4 -4
  201. package/build/server/chunks/{FeatureCard-BQOY6gJQ.js.map → FeatureCard-Bb50FEfp.js.map} +1 -1
  202. package/build/server/chunks/{FeatureJson-Bv-qiOSf.js → FeatureJson-DYvMEAub.js} +2 -2
  203. package/build/server/chunks/{FeatureJson-Bv-qiOSf.js.map → FeatureJson-DYvMEAub.js.map} +1 -1
  204. package/build/server/chunks/{FeatureTransforms-UWDHmWEg.js → FeatureTransforms-B0aI_CJD.js} +10 -3
  205. package/build/server/chunks/FeatureTransforms-B0aI_CJD.js.map +1 -0
  206. package/build/server/chunks/{ManageTagsDialog-DBxA3tM2.js → ManageTagsDialog-D5xbAVNB.js} +5 -5
  207. package/build/server/chunks/ManageTagsDialog-D5xbAVNB.js.map +1 -0
  208. package/build/server/chunks/{ProgressBar-CfhccQ83.js → ProgressBar-CUC2EZqG.js} +3 -3
  209. package/build/server/chunks/ProgressBar-CUC2EZqG.js.map +1 -0
  210. package/build/server/chunks/{ProjectsIndex-CoDrvRya.js → ProjectsIndex-FgIu9dBp.js} +135 -14
  211. package/build/server/chunks/ProjectsIndex-FgIu9dBp.js.map +1 -0
  212. package/build/server/chunks/{RenameTag-BuWLMl1m.js → RenameTag-DWIIR4Bx.js} +3 -6
  213. package/build/server/chunks/RenameTag-DWIIR4Bx.js.map +1 -0
  214. package/build/server/chunks/ScenarioApplier-YPKECfZe.js +55 -0
  215. package/build/server/chunks/ScenarioApplier-YPKECfZe.js.map +1 -0
  216. package/build/server/chunks/SimulatorEngine-DM1F9nCO.js +1391 -0
  217. package/build/server/chunks/SimulatorEngine-DM1F9nCO.js.map +1 -0
  218. package/build/server/chunks/StateValue-BBjWox8f.js +105 -0
  219. package/build/server/chunks/StateValue-BBjWox8f.js.map +1 -0
  220. package/build/server/chunks/{TagDotStrip-B1XQ0m0Y.js → TagDotStrip-0rn-IJVE.js} +4 -4
  221. package/build/server/chunks/TagDotStrip-0rn-IJVE.js.map +1 -0
  222. package/build/server/chunks/{TagFilterSelect-DbO0Qduh.js → TagFilterSelect-Dbu-7Dg4.js} +5 -5
  223. package/build/server/chunks/{TagFilterSelect-DbO0Qduh.js.map → TagFilterSelect-Dbu-7Dg4.js.map} +1 -1
  224. package/build/server/chunks/{TagPalette-CtMNYCmu.js → TagPalette-CpdTiC_7.js} +101 -9
  225. package/build/server/chunks/TagPalette-CpdTiC_7.js.map +1 -0
  226. package/build/server/chunks/TransitionCatalog-CYXUANy6.js +271 -0
  227. package/build/server/chunks/TransitionCatalog-CYXUANy6.js.map +1 -0
  228. package/build/server/chunks/_layout.svelte-pSSG85WT.js +1342 -0
  229. package/build/server/chunks/_layout.svelte-pSSG85WT.js.map +1 -0
  230. package/build/server/chunks/{_page.svelte-BKTveFAj.js → _page.svelte-BNH3szkk.js} +22 -17
  231. package/build/server/chunks/{_page.svelte-BKTveFAj.js.map → _page.svelte-BNH3szkk.js.map} +1 -1
  232. package/build/server/chunks/_page.svelte-BcuGvUt1.js +37 -0
  233. package/build/server/chunks/{_page.svelte-B7hT3P8E.js.map → _page.svelte-BcuGvUt1.js.map} +1 -1
  234. package/build/server/chunks/_page.svelte-BoOMG5Y0.js +37 -0
  235. package/build/server/chunks/{_page.svelte-De508ek8.js.map → _page.svelte-BoOMG5Y0.js.map} +1 -1
  236. package/build/server/chunks/{_page.svelte-BqSC-1vK.js → _page.svelte-C4PRh_wP.js} +104 -54
  237. package/build/server/chunks/_page.svelte-C4PRh_wP.js.map +1 -0
  238. package/build/server/chunks/_page.svelte-Cqn34NXy.js +74 -0
  239. package/build/server/chunks/_page.svelte-Cqn34NXy.js.map +1 -0
  240. package/build/server/chunks/{_page.svelte-B1s7jxaQ.js → _page.svelte-DSCeOGv7.js} +15 -10
  241. package/build/server/chunks/_page.svelte-DSCeOGv7.js.map +1 -0
  242. package/build/server/chunks/_page.svelte-DqWdGt48.js +182 -0
  243. package/build/server/chunks/_page.svelte-DqWdGt48.js.map +1 -0
  244. package/build/server/chunks/_page.svelte-DsQHSEl4.js +49 -0
  245. package/build/server/chunks/_page.svelte-DsQHSEl4.js.map +1 -0
  246. package/build/server/chunks/{_page.svelte-BKKCa9H5.js → _page.svelte-Uioz8hsG.js} +18 -11
  247. package/build/server/chunks/{_page.svelte-BKKCa9H5.js.map → _page.svelte-Uioz8hsG.js.map} +1 -1
  248. package/build/server/chunks/{_page.svelte-Zf9H4KOP.js → _page.svelte-p38jot9e.js} +81 -478
  249. package/build/server/chunks/_page.svelte-p38jot9e.js.map +1 -0
  250. package/build/server/chunks/{_page.svelte-BtI2zZ_Z.js → _page.svelte-yN5kzgJ9.js} +251 -447
  251. package/build/server/chunks/_page.svelte-yN5kzgJ9.js.map +1 -0
  252. package/build/server/chunks/{_server.ts-DsbbVoOn.js → _server.ts-1CtrHhte.js} +4 -4
  253. package/build/server/chunks/{_server.ts-DsbbVoOn.js.map → _server.ts-1CtrHhte.js.map} +1 -1
  254. package/build/server/chunks/{_server.ts-CWvU15tL.js → _server.ts-71Qx1lQb.js} +4 -4
  255. package/build/server/chunks/{_server.ts-CWvU15tL.js.map → _server.ts-71Qx1lQb.js.map} +1 -1
  256. package/build/server/chunks/_server.ts-BAyekGHB.js +961 -0
  257. package/build/server/chunks/_server.ts-BAyekGHB.js.map +1 -0
  258. package/build/server/chunks/{_server.ts-BihpbxOW.js → _server.ts-BLWhU22T.js} +4 -4
  259. package/build/server/chunks/{_server.ts-BihpbxOW.js.map → _server.ts-BLWhU22T.js.map} +1 -1
  260. package/build/server/chunks/{_server.ts-YnfXkeMJ.js → _server.ts-C7EVZVxv.js} +22 -13
  261. package/build/server/chunks/_server.ts-C7EVZVxv.js.map +1 -0
  262. package/build/server/chunks/{_server.ts-vcaqdwxS.js → _server.ts-CGjYuLmo.js} +12 -7
  263. package/build/server/chunks/_server.ts-CGjYuLmo.js.map +1 -0
  264. package/build/server/chunks/{_server.ts-Dy5zQHe2.js → _server.ts-CX6H8fSE.js} +8 -31
  265. package/build/server/chunks/_server.ts-CX6H8fSE.js.map +1 -0
  266. package/build/server/chunks/{_server.ts-BuoQ02BW.js → _server.ts-CyEdTCLr.js} +4 -4
  267. package/build/server/chunks/{_server.ts-BuoQ02BW.js.map → _server.ts-CyEdTCLr.js.map} +1 -1
  268. package/build/server/chunks/_server.ts-DKVTM9Pj.js +77 -0
  269. package/build/server/chunks/_server.ts-DKVTM9Pj.js.map +1 -0
  270. package/build/server/chunks/{_server.ts-0uNTZKwF.js → _server.ts-Dip65hLJ.js} +15 -9
  271. package/build/server/chunks/_server.ts-Dip65hLJ.js.map +1 -0
  272. package/build/server/chunks/{_server.ts-D4EO0VHu.js → _server.ts-Dm0tCs2i.js} +5 -5
  273. package/build/server/chunks/{_server.ts-D4EO0VHu.js.map → _server.ts-Dm0tCs2i.js.map} +1 -1
  274. package/build/server/chunks/{_server.ts-BIBWjQI-.js → _server.ts-DsYwK_uW.js} +4 -4
  275. package/build/server/chunks/{_server.ts-BIBWjQI-.js.map → _server.ts-DsYwK_uW.js.map} +1 -1
  276. package/build/server/chunks/{_server.ts-DDa0Nk_S.js → _server.ts-RCEgegYU.js} +4 -4
  277. package/build/server/chunks/{_server.ts-DDa0Nk_S.js.map → _server.ts-RCEgegYU.js.map} +1 -1
  278. package/build/server/chunks/_server.ts-XOLAT4dd.js +146 -0
  279. package/build/server/chunks/_server.ts-XOLAT4dd.js.map +1 -0
  280. package/build/server/chunks/_server.ts-afh_zLVi.js +69 -0
  281. package/build/server/chunks/_server.ts-afh_zLVi.js.map +1 -0
  282. package/build/server/chunks/{browserContainer-CNX7ANld.js → browserContainer-BdldtueJ.js} +71 -1378
  283. package/build/server/chunks/browserContainer-BdldtueJ.js.map +1 -0
  284. package/build/server/chunks/buildSearchDocs-Cyfrnw9G.js +337 -0
  285. package/build/server/chunks/buildSearchDocs-Cyfrnw9G.js.map +1 -0
  286. package/build/server/chunks/{builderModeStore.svelte-ihupr-3p.js → builderModeStore.svelte-BEZMQEqn.js} +223 -11
  287. package/build/server/chunks/builderModeStore.svelte-BEZMQEqn.js.map +1 -0
  288. package/build/server/chunks/client-C9Cat4DK.js +51 -0
  289. package/build/server/chunks/{client-DfpLcAZ9.js.map → client-C9Cat4DK.js.map} +1 -1
  290. package/build/server/chunks/{error.svelte-C35KOpru.js → error.svelte-CzVR_A6P.js} +5 -5
  291. package/build/server/chunks/{error.svelte-C35KOpru.js.map → error.svelte-CzVR_A6P.js.map} +1 -1
  292. package/build/server/chunks/featureQueueContext-DuXpyTdC.js +32 -0
  293. package/build/server/chunks/featureQueueContext-DuXpyTdC.js.map +1 -0
  294. package/build/server/chunks/featureStore.svelte-DHZf88gu.js +162 -0
  295. package/build/server/chunks/featureStore.svelte-DHZf88gu.js.map +1 -0
  296. package/build/server/chunks/{projectsStore.svelte-QDz5PchX.js → featuresStore.svelte-bUyQBg1-.js} +95 -3
  297. package/build/server/chunks/featuresStore.svelte-bUyQBg1-.js.map +1 -0
  298. package/build/server/chunks/{hooks.server-Rv301GTB.js → hooks.server-y3jdg_sB.js} +6 -2
  299. package/build/server/chunks/hooks.server-y3jdg_sB.js.map +1 -0
  300. package/build/server/chunks/humanize-CkCPfeWi.js +53 -0
  301. package/build/server/chunks/humanize-CkCPfeWi.js.map +1 -0
  302. package/build/server/chunks/{index-server-7H0jzj0M.js → index-server-DvBux_Gf.js} +47 -2
  303. package/build/server/chunks/{index-server-7H0jzj0M.js.map → index-server-DvBux_Gf.js.map} +1 -1
  304. package/build/server/chunks/{internal-BPKrFkK1.js → internal-BFt8yDlf.js} +5 -5
  305. package/build/server/chunks/{internal-BPKrFkK1.js.map → internal-BFt8yDlf.js.map} +1 -1
  306. package/build/server/chunks/projectBundleClient-BtvWPh87.js +168 -0
  307. package/build/server/chunks/projectBundleClient-BtvWPh87.js.map +1 -0
  308. package/build/server/chunks/projectContextStore.svelte-jRJGJZ42.js +141 -0
  309. package/build/server/chunks/projectContextStore.svelte-jRJGJZ42.js.map +1 -0
  310. package/build/server/chunks/projectFeaturesStore.svelte-BwNEtDIL.js +87 -0
  311. package/build/server/chunks/projectFeaturesStore.svelte-BwNEtDIL.js.map +1 -0
  312. package/build/server/chunks/projectStore.svelte-BNuLNxk_.js +230 -0
  313. package/build/server/chunks/projectStore.svelte-BNuLNxk_.js.map +1 -0
  314. package/build/server/chunks/{reconcile-Dv7jS3C8.js → reconcile-CVG2LSlW.js} +54 -267
  315. package/build/server/chunks/reconcile-CVG2LSlW.js.map +1 -0
  316. package/build/server/chunks/registry-DqAn_hVE.js +21 -0
  317. package/build/server/chunks/registry-DqAn_hVE.js.map +1 -0
  318. package/build/server/chunks/{snapshotRepository-BPXCjX92.js → snapshotRepository-8f-2wTrb.js} +30 -5
  319. package/build/server/chunks/snapshotRepository-8f-2wTrb.js.map +1 -0
  320. package/build/server/chunks/{state-CpLVNZq7.js → state-CZZtjXLE.js} +3 -3
  321. package/build/server/chunks/{state-CpLVNZq7.js.map → state-CZZtjXLE.js.map} +1 -1
  322. package/build/server/chunks/{sync-DZ3dn761.js → sync-Bn6eSUhC.js} +3 -3
  323. package/build/server/chunks/{sync-DZ3dn761.js.map → sync-Bn6eSUhC.js.map} +1 -1
  324. package/build/server/chunks/{syncBridge-BwXzPdjJ.js → syncAwareRepositories-DvUjq5Q6.js} +27 -3
  325. package/build/server/chunks/syncAwareRepositories-DvUjq5Q6.js.map +1 -0
  326. package/build/server/chunks/{tagPaletteStore.svelte-DfZOBvgY.js → tagPaletteStore.svelte-C71bdzDl.js} +4 -4
  327. package/build/server/chunks/{tagPaletteStore.svelte-DfZOBvgY.js.map → tagPaletteStore.svelte-C71bdzDl.js.map} +1 -1
  328. package/build/server/index.js +2 -2
  329. package/build/server/index.js.map +1 -1
  330. package/build/server/manifest.js +74 -29
  331. package/build/server/manifest.js.map +1 -1
  332. package/cli/README.md +70 -2
  333. package/cli/commands/check.ts +190 -0
  334. package/cli/commands/ci.ts +71 -0
  335. package/cli/commands/coverage-ingest.ts +112 -0
  336. package/cli/commands/dashboard.ts +14 -0
  337. package/cli/commands/init.ts +26 -0
  338. package/cli/commands/scenarios-adapter.ts +96 -0
  339. package/cli/commands/theme.ts +62 -0
  340. package/cli/scenarios/adapter-scaffold.ts +99 -0
  341. package/cli/scenarios/codegen.ts +7 -1
  342. package/cli/scenarios/results.ts +85 -0
  343. package/cli/skills/unspa-audit/SKILL.md +19 -0
  344. package/cli/skills/unspa-edit/SKILL.md +20 -3
  345. package/cli/skills/unspa-implement/SKILL.md +28 -0
  346. package/cli/unspa.ts +136 -2
  347. package/cli/util/context-files.ts +18 -2
  348. package/cli/util/theme.ts +34 -0
  349. package/mcp-server/bin.ts +104 -104
  350. package/mcp-server/migrate-defaults.ts +164 -164
  351. package/mcp-server/repo-link.ts +142 -134
  352. package/mcp-server/resources/guide.ts +24 -4
  353. package/mcp-server/resources/operations.ts +3 -0
  354. package/mcp-server/seed-snapshots.ts +22 -22
  355. package/mcp-server/server.ts +8 -4
  356. package/mcp-server/sync-aware-repos.ts +79 -79
  357. package/mcp-server/sync-notifier.ts +180 -127
  358. package/mcp-server/tools/_codegen.ts +115 -115
  359. package/mcp-server/tools/_entity_builders.ts +30 -0
  360. package/mcp-server/tools/_shared.ts +210 -210
  361. package/mcp-server/tools/action.ts +210 -210
  362. package/mcp-server/tools/batch.ts +29 -5
  363. package/mcp-server/tools/effect.ts +139 -135
  364. package/mcp-server/tools/entity.ts +132 -132
  365. package/mcp-server/tools/entityField.ts +132 -132
  366. package/mcp-server/tools/event.ts +116 -116
  367. package/mcp-server/tools/feature.ts +264 -264
  368. package/mcp-server/tools/invariant.ts +275 -275
  369. package/mcp-server/tools/persona.ts +141 -141
  370. package/mcp-server/tools/project.ts +500 -391
  371. package/mcp-server/tools/reachabilityGoal.ts +97 -0
  372. package/mcp-server/tools/read.ts +526 -483
  373. package/mcp-server/tools/reorder.ts +114 -114
  374. package/mcp-server/tools/resource.ts +194 -194
  375. package/mcp-server/tools/rule.ts +302 -302
  376. package/mcp-server/tools/scenario.ts +4 -1
  377. package/mcp-server/tools/specGaps.ts +221 -221
  378. package/mcp-server/tools/state.ts +10 -2
  379. package/mcp-server/tools/surface.ts +102 -102
  380. package/mcp-server/tools/transition.ts +96 -96
  381. package/mcp-server/tools/verification.ts +131 -0
  382. package/package.json +15 -2
  383. package/src/app.css +208 -9
  384. package/src/app.html +15 -1
  385. package/src/features/behavior-model/application/ports/FeatureRepository.ts +16 -16
  386. package/src/features/behavior-model/application/use-cases/CreateFeature.ts +24 -24
  387. package/src/features/behavior-model/application/use-cases/DeleteFeature.ts +8 -8
  388. package/src/features/behavior-model/application/use-cases/GetFeature.ts +9 -9
  389. package/src/features/behavior-model/application/use-cases/ListFeatures.ts +10 -10
  390. package/src/features/behavior-model/application/use-cases/MutateFeature.ts +90 -90
  391. package/src/features/behavior-model/application/use-cases/SaveFeature.ts +31 -31
  392. package/src/features/behavior-model/domain/entities/Entity.ts +45 -45
  393. package/src/features/behavior-model/domain/entities/EventDefinition.ts +35 -35
  394. package/src/features/behavior-model/domain/entities/Feature.ts +10 -0
  395. package/src/features/behavior-model/domain/entities/Persona.ts +33 -33
  396. package/src/features/behavior-model/domain/entities/ReachabilityGoal.ts +39 -0
  397. package/src/features/behavior-model/domain/entities/Resource.ts +54 -54
  398. package/src/features/behavior-model/domain/entities/Scenario.ts +15 -0
  399. package/src/features/behavior-model/domain/entities/StateDefinition.ts +13 -0
  400. package/src/features/behavior-model/domain/entities/Surface.ts +75 -75
  401. package/src/features/behavior-model/domain/entities/index.ts +9 -9
  402. package/src/features/behavior-model/domain/services/BehaviorGraphModel.ts +556 -0
  403. package/src/features/behavior-model/domain/services/DerivedState.ts +67 -0
  404. package/src/features/behavior-model/domain/services/EffectApplier.ts +112 -3
  405. package/src/features/behavior-model/domain/services/EffectiveEntities.ts +166 -166
  406. package/src/features/behavior-model/domain/services/EntityDeducer.ts +83 -83
  407. package/src/features/behavior-model/domain/services/EventCatalog.ts +141 -141
  408. package/src/features/behavior-model/domain/services/FeatureExpressionNormalizer.ts +43 -5
  409. package/src/features/behavior-model/domain/services/FeatureSharedStateNormalizer.ts +24 -3
  410. package/src/features/behavior-model/domain/services/FeatureTransforms.ts +33 -0
  411. package/src/features/behavior-model/domain/services/FeatureValidator.ts +124 -16
  412. package/src/features/behavior-model/domain/services/PersonaApplier.ts +39 -39
  413. package/src/features/behavior-model/domain/services/ResourceDeducer.ts +64 -64
  414. package/src/features/behavior-model/domain/services/ResourceUsage.ts +79 -79
  415. package/src/features/behavior-model/domain/services/ScenarioApplier.ts +40 -40
  416. package/src/features/behavior-model/domain/services/SharedStateDefinitions.ts +32 -32
  417. package/src/features/behavior-model/domain/services/StatePathReferences.ts +249 -217
  418. package/src/features/behavior-model/domain/services/TransitionCatalog.ts +173 -173
  419. package/src/features/behavior-model/domain/value-objects/Effect.ts +100 -3
  420. package/src/features/behavior-model/domain/value-objects/EntityFieldType.ts +57 -57
  421. package/src/features/behavior-model/domain/value-objects/Expression.ts +22 -2
  422. package/src/features/behavior-model/domain/value-objects/ParameterType.ts +57 -57
  423. package/src/features/behavior-model/domain/value-objects/Resource.ts +210 -210
  424. package/src/features/behavior-model/domain/value-objects/RuleCategory.ts +57 -57
  425. package/src/features/behavior-model/domain/value-objects/RuleCondition.ts +52 -1
  426. package/src/features/behavior-model/domain/value-objects/SimulationClock.ts +35 -0
  427. package/src/features/behavior-model/domain/value-objects/ids.ts +2 -0
  428. package/src/features/behavior-model/infrastructure/io/FeatureJson.ts +89 -89
  429. package/src/features/behavior-model/infrastructure/persistence/HttpFeatureRepository.ts +43 -43
  430. package/src/features/behavior-model/infrastructure/persistence/JsonFolderFeatureRepository.ts +10 -0
  431. package/src/features/behavior-model/infrastructure/persistence/snapshot-discovery.ts +76 -76
  432. package/src/features/behavior-model/infrastructure/seed/seedStorefront.ts +967 -967
  433. package/src/features/behavior-model/presentation/adapters/VisBehaviorGraphRenderer.ts +501 -0
  434. package/src/features/behavior-model/presentation/components/ActionEditor.svelte +683 -682
  435. package/src/features/behavior-model/presentation/components/ActionsEditor.svelte +232 -232
  436. package/src/features/behavior-model/presentation/components/BehaviorGraph.svelte +568 -0
  437. package/src/features/behavior-model/presentation/components/EffectEditor.svelte +168 -168
  438. package/src/features/behavior-model/presentation/components/EntityEditor.svelte +241 -248
  439. package/src/features/behavior-model/presentation/components/EntityFieldRow.svelte +180 -181
  440. package/src/features/behavior-model/presentation/components/EntityManager.svelte +252 -251
  441. package/src/features/behavior-model/presentation/components/EventsManager.svelte +373 -372
  442. package/src/features/behavior-model/presentation/components/FeatureEditor.svelte +396 -348
  443. package/src/features/behavior-model/presentation/components/FeatureHeader.svelte +165 -148
  444. package/src/features/behavior-model/presentation/components/FeatureHealthStrip.svelte +110 -110
  445. package/src/features/behavior-model/presentation/components/HistoryPanel.svelte +154 -154
  446. package/src/features/behavior-model/presentation/components/NewFeatureForm.svelte +66 -66
  447. package/src/features/behavior-model/presentation/components/ParameterNameSelect.svelte +84 -84
  448. package/src/features/behavior-model/presentation/components/ParametersEditor.svelte +411 -411
  449. package/src/features/behavior-model/presentation/components/PersonaEditor.svelte +244 -244
  450. package/src/features/behavior-model/presentation/components/PersonasManager.svelte +130 -130
  451. package/src/features/behavior-model/presentation/components/ReachabilityGoalEditor.svelte +230 -0
  452. package/src/features/behavior-model/presentation/components/ReachabilityGoalsEditor.svelte +86 -0
  453. package/src/features/behavior-model/presentation/components/ResourcesManager.svelte +291 -290
  454. package/src/features/behavior-model/presentation/components/ScenariosEditor.svelte +496 -496
  455. package/src/features/behavior-model/presentation/components/StateDefinitionsEditor.svelte +356 -356
  456. package/src/features/behavior-model/presentation/components/SurfaceEntitiesView.svelte +88 -88
  457. package/src/features/behavior-model/presentation/components/SurfaceInvariantsEditor.svelte +76 -76
  458. package/src/features/behavior-model/presentation/components/SurfaceList.svelte +300 -300
  459. package/src/features/behavior-model/presentation/components/SurfaceMetaEditor.svelte +158 -156
  460. package/src/features/behavior-model/presentation/components/SurfacePanel.svelte +198 -198
  461. package/src/features/behavior-model/presentation/components/SurfacePersonasView.svelte +122 -122
  462. package/src/features/behavior-model/presentation/components/SurfaceResourcesView.svelte +113 -113
  463. package/src/features/behavior-model/presentation/components/SurfaceRulesEditor.svelte +69 -69
  464. package/src/features/behavior-model/presentation/components/TransitionsManager.svelte +322 -321
  465. package/src/features/behavior-model/presentation/context/featureQueueContext.ts +78 -0
  466. package/src/features/behavior-model/presentation/stores/editorStore.svelte.ts +132 -131
  467. package/src/features/behavior-model/presentation/stores/featureStore.svelte.ts +177 -177
  468. package/src/features/behavior-model/presentation/stores/featuresStore.svelte.ts +98 -98
  469. package/src/features/behavior-model/presentation/view-models/BehaviorGraphTheme.ts +44 -0
  470. package/src/features/behavior-model/presentation/view-models/factories.ts +181 -132
  471. package/src/features/builder-mode/domain/BuilderModeDashboard.ts +7 -1
  472. package/src/features/builder-mode/presentation/components/BuilderModeDashboard.svelte +78 -16
  473. package/src/features/builder-mode/presentation/components/BuilderTagChips.svelte +25 -0
  474. package/src/features/builder-mode/presentation/stores/builderModeStore.svelte.ts +247 -3
  475. package/src/features/domains/infrastructure/persistence/JsonFolderDomainRepository.ts +9 -0
  476. package/src/features/global-search/application/use-cases/LoadSearchIndex.ts +70 -0
  477. package/src/features/global-search/domain/SearchDoc.ts +111 -0
  478. package/src/features/global-search/domain/buildSearchDocs.ts +483 -0
  479. package/src/features/global-search/domain/searchNav.ts +58 -0
  480. package/src/features/global-search/domain/searchScoring.ts +81 -0
  481. package/src/features/global-search/infrastructure/adapters/defaultSearchHost.ts +28 -0
  482. package/src/features/global-search/presentation/components/GlobalSearch.svelte +262 -0
  483. package/src/features/global-search/presentation/ports/SearchHostPorts.ts +41 -0
  484. package/src/features/global-search/presentation/searchKindMeta.ts +108 -0
  485. package/src/features/global-search/presentation/stores/globalSearchStore.svelte.ts +171 -0
  486. package/src/features/implementation-queue/presentation/components/FloatingQueueWidget.svelte +264 -0
  487. package/src/features/implementation-queue/presentation/stores/globalQueueStore.svelte.ts +172 -0
  488. package/src/features/implementation-status/application/ports/ImplementationStatusRepository.ts +8 -8
  489. package/src/features/implementation-status/application/use-cases/GetImplementationStatus.ts +8 -8
  490. package/src/features/implementation-status/domain/ApplyCapturedToSpec.ts +323 -322
  491. package/src/features/implementation-status/domain/FieldDiff.ts +238 -236
  492. package/src/features/implementation-status/infrastructure/persistence/InMemoryImplementationStatusRepository.ts +21 -21
  493. package/src/features/implementation-status/presentation/components/ImplementationStatusPanel.svelte +831 -831
  494. package/src/features/implementation-status/presentation/stores/implementationStatusStore.svelte.ts +97 -97
  495. package/src/features/library/application/use-cases/ApplyBlueprintsToFeature.ts +33 -33
  496. package/src/features/library/domain/services/BlueprintApplier.ts +94 -94
  497. package/src/features/library/infrastructure/blueprints/auth/ResetPasswordBlueprint.ts +332 -332
  498. package/src/features/library/infrastructure/blueprints/auth/SignInBlueprint.ts +281 -281
  499. package/src/features/library/infrastructure/blueprints/auth/SignUpBlueprint.ts +299 -299
  500. package/src/features/library/infrastructure/blueprints/auth/VerifyEmailBlueprint.ts +288 -288
  501. package/src/features/library/infrastructure/blueprints/auth/shared.ts +146 -146
  502. package/src/features/library/infrastructure/blueprints/commerce/CartBlueprint.ts +222 -222
  503. package/src/features/library/infrastructure/blueprints/commerce/CatalogBlueprint.ts +190 -190
  504. package/src/features/library/infrastructure/blueprints/commerce/CheckoutBlueprint.ts +322 -322
  505. package/src/features/library/infrastructure/blueprints/commerce/OrderConfirmationBlueprint.ts +172 -172
  506. package/src/features/library/infrastructure/blueprints/commerce/ProductDetailsBlueprint.ts +290 -290
  507. package/src/features/library/infrastructure/blueprints/content/ReviewsBlueprint.ts +392 -392
  508. package/src/features/library/infrastructure/blueprints/onboarding/OnboardingWizardBlueprint.ts +344 -344
  509. package/src/features/library/infrastructure/blueprints/utility/CanvasBlueprint.ts +419 -419
  510. package/src/features/library/infrastructure/blueprints/utility/CookieConsentBlueprint.ts +195 -195
  511. package/src/features/library/infrastructure/blueprints/utility/NotFoundBlueprint.ts +86 -86
  512. package/src/features/library/infrastructure/blueprints/utility/SearchPaletteBlueprint.ts +192 -192
  513. package/src/features/library/infrastructure/blueprints/utility/StoreLocatorBlueprint.ts +221 -221
  514. package/src/features/library/infrastructure/blueprints/utility/TaskBoardBlueprint.ts +196 -196
  515. package/src/features/maturity/application/use-cases/ScoreFeature.ts +7 -7
  516. package/src/features/maturity/domain/MaturityReport.ts +70 -70
  517. package/src/features/maturity/domain/MaturityScorer.ts +1001 -985
  518. package/src/features/maturity/presentation/components/MaturityPanel.svelte +376 -372
  519. package/src/features/mcp-tools/application/tools/dryRunSimulate.ts +24 -24
  520. package/src/features/mcp-tools/application/tools/findStateReferences.ts +19 -19
  521. package/src/features/mcp-tools/application/tools/generateTypes.ts +0 -1
  522. package/src/features/mcp-tools/application/tools/getAction.ts +165 -165
  523. package/src/features/mcp-tools/application/tools/getFeature.ts +5 -5
  524. package/src/features/mcp-tools/application/tools/getFeatureIndex.ts +79 -79
  525. package/src/features/mcp-tools/application/tools/getNeighborhood.ts +319 -319
  526. package/src/features/mcp-tools/application/tools/getSurface.ts +86 -86
  527. package/src/features/mcp-tools/application/tools/index.ts +56 -56
  528. package/src/features/mcp-tools/application/tools/listActions.ts +49 -49
  529. package/src/features/mcp-tools/application/tools/listFeatures.ts +7 -7
  530. package/src/features/mcp-tools/application/tools/scoreFeature.ts +153 -153
  531. package/src/features/projects/application/ports/ProjectRepository.ts +14 -14
  532. package/src/features/projects/application/use-cases/AddFeatureToProject.ts +23 -23
  533. package/src/features/projects/application/use-cases/CreateProject.ts +16 -16
  534. package/src/features/projects/application/use-cases/FindProjectContainingFeature.ts +23 -23
  535. package/src/features/projects/application/use-cases/GetProjectAggregate.ts +131 -131
  536. package/src/features/projects/application/use-cases/ImportProjectBundle.ts +45 -0
  537. package/src/features/projects/application/use-cases/MoveFeatureInProject.ts +36 -36
  538. package/src/features/projects/application/use-cases/RemoveFeatureFromProject.ts +23 -23
  539. package/src/features/projects/domain/entities/Project.ts +12 -0
  540. package/src/features/projects/domain/services/InheritedFromProject.ts +130 -130
  541. package/src/features/projects/infrastructure/persistence/JsonFolderProjectRepository.ts +9 -0
  542. package/src/features/projects/presentation/components/ProjectCard.svelte +27 -8
  543. package/src/features/projects/presentation/components/ProjectEditor.svelte +23 -19
  544. package/src/features/projects/presentation/components/ProjectFeaturesPanel.svelte +433 -433
  545. package/src/features/projects/presentation/components/ProjectsIndex.svelte +54 -10
  546. package/src/features/projects/presentation/stores/projectContextStore.svelte.ts +170 -170
  547. package/src/features/projects/presentation/stores/projectFeaturesStore.svelte.ts +114 -114
  548. package/src/features/projects/presentation/stores/projectStore.svelte.ts +265 -266
  549. package/src/features/projects/presentation/stores/projectsStore.svelte.ts +134 -134
  550. package/src/features/simulator/application/use-cases/RunScenarios.ts +27 -1
  551. package/src/features/simulator/application/use-cases/SimulateAction.ts +47 -47
  552. package/src/features/simulator/domain/SimulationResult.ts +79 -79
  553. package/src/features/simulator/domain/SimulatorEngine.ts +43 -9
  554. package/src/features/simulator/domain/StateExplorer.ts +369 -0
  555. package/src/features/simulator/domain/SurfaceReachability.ts +130 -0
  556. package/src/features/simulator/presentation/components/ParameterInputs.svelte +277 -277
  557. package/src/features/simulator/presentation/components/PersonaPicker.svelte +52 -52
  558. package/src/features/simulator/presentation/components/ScenarioPicker.svelte +42 -45
  559. package/src/features/simulator/presentation/components/SidePanel.svelte +170 -170
  560. package/src/features/simulator/presentation/components/SimulationResultView.svelte +394 -394
  561. package/src/features/simulator/presentation/components/SimulatorPanel.svelte +293 -293
  562. package/src/features/simulator/presentation/components/StateInspector.svelte +101 -101
  563. package/src/features/simulator/presentation/stores/simulatorStore.svelte.ts +268 -268
  564. package/src/features/tutorial/presentation/components/CodeBlock.svelte +5 -1
  565. package/src/features/verification/application/ports/BehavioralIndexReader.ts +12 -0
  566. package/src/features/verification/application/use-cases/VerifyFeatures.ts +151 -0
  567. package/src/features/verification/domain/DriftReport.ts +37 -0
  568. package/src/features/verification/domain/EventCoherenceReport.ts +27 -0
  569. package/src/features/verification/domain/IndexedImplementation.ts +27 -0
  570. package/src/features/verification/domain/VerificationReport.ts +59 -0
  571. package/src/features/verification/domain/VerificationThresholds.ts +52 -0
  572. package/src/features/verification/domain/VerificationVerdict.ts +43 -0
  573. package/src/features/verification/domain/aggregateVerdict.ts +244 -0
  574. package/src/features/verification/domain/analyzeEventCoherence.ts +43 -0
  575. package/src/features/verification/domain/detectDrift.ts +166 -0
  576. package/src/features/verification/domain/verifiedCoverage.ts +37 -0
  577. package/src/features/verification/infrastructure/persistence/FileBehavioralIndexReader.ts +81 -0
  578. package/src/features/verification/infrastructure/persistence/StaticBehavioralIndexReader.ts +15 -0
  579. package/src/features/verification/presentation/components/VerifyPanel.svelte +74 -0
  580. package/src/features/verification/presentation/stores/verificationStore.svelte.ts +64 -0
  581. package/src/hooks.server.ts +11 -1
  582. package/src/lib/server/openLocalDirectory.ts +40 -0
  583. package/src/lib/server/searchIndex.ts +161 -0
  584. package/src/lib/server/snapshotRepository.ts +37 -37
  585. package/src/lib/server/sync/YDocManager.ts +391 -391
  586. package/src/lib/server/sync/historyStore.ts +276 -276
  587. package/src/lib/server/sync/wsServer.ts +1 -1
  588. package/src/lib/sync/protocol.ts +147 -147
  589. package/src/lib/sync/roomId.ts +33 -33
  590. package/src/lib/theme/registry.ts +77 -0
  591. package/src/lib/theme/themeStore.svelte.ts +64 -0
  592. package/src/routes/+layout.svelte +288 -30
  593. package/src/routes/api/domains/[id]/+server.ts +7 -3
  594. package/src/routes/api/projects/[id]/+server.ts +14 -4
  595. package/src/routes/api/projects/import/+server.ts +16 -38
  596. package/src/routes/api/search-index/+server.ts +16 -0
  597. package/src/routes/api/snapshots/[id]/+server.ts +56 -44
  598. package/src/routes/api/snapshots/[id]/implementation-status/+server.ts +13 -13
  599. package/src/routes/api/snapshots/[id]/verify/+server.ts +37 -0
  600. package/src/routes/api/sync/reload/+server.ts +157 -157
  601. package/src/routes/api/system/hub-directory/+server.ts +24 -0
  602. package/src/routes/features/+page.svelte +484 -484
  603. package/src/routes/features/[id]/+page.svelte +55 -49
  604. package/src/routes/features/[id]/graph/+page.svelte +39 -0
  605. package/src/routes/features/[id]/verify/+page.svelte +89 -0
  606. package/src/routes/mcp/+page.svelte +16 -1
  607. package/src/routes/projects/[id]/+page.svelte +70 -70
  608. package/src/routes/projects/[id]/graph/+page.svelte +79 -0
  609. package/src/routes/tutorial/+page.svelte +3 -2
  610. package/src/shared/infrastructure/browserContainer.ts +61 -61
  611. package/src/shared/presentation/components/KebabMenu.svelte +95 -0
  612. package/src/shared/presentation/components/MenuItem.svelte +29 -0
  613. package/src/shared/presentation/components/ProgressBar.svelte +1 -1
  614. package/src/shared/presentation/toast/SyncToast.svelte +14 -3
  615. package/src/shared/presentation/toast/viewLinkResolver.ts +32 -0
  616. package/static/lyriks_logo.svg +148 -0
  617. package/vite.config.ts +47 -47
  618. package/build/client/_app/immutable/assets/0.DFMDYAU9.css +0 -1
  619. package/build/client/_app/immutable/assets/0.DFMDYAU9.css.br +0 -0
  620. package/build/client/_app/immutable/assets/0.DFMDYAU9.css.gz +0 -0
  621. package/build/client/_app/immutable/assets/9.nv0I59TU.css.gz +0 -0
  622. package/build/client/_app/immutable/chunks/BO66rBOa2.js +0 -1
  623. package/build/client/_app/immutable/chunks/BO66rBOa2.js.br +0 -0
  624. package/build/client/_app/immutable/chunks/BO66rBOa2.js.gz +0 -0
  625. package/build/client/_app/immutable/chunks/BOrLmFwJ.js.br +0 -0
  626. package/build/client/_app/immutable/chunks/BOrLmFwJ.js.gz +0 -0
  627. package/build/client/_app/immutable/chunks/BfMYiIUi.js.br +0 -0
  628. package/build/client/_app/immutable/chunks/BfMYiIUi.js.gz +0 -0
  629. package/build/client/_app/immutable/chunks/Bp2_Vghh.js +0 -1
  630. package/build/client/_app/immutable/chunks/Bp2_Vghh.js.br +0 -0
  631. package/build/client/_app/immutable/chunks/Bp2_Vghh.js.gz +0 -0
  632. package/build/client/_app/immutable/chunks/Bp8HpEPe2.js +0 -4
  633. package/build/client/_app/immutable/chunks/Bp8HpEPe2.js.br +0 -0
  634. package/build/client/_app/immutable/chunks/Bp8HpEPe2.js.gz +0 -0
  635. package/build/client/_app/immutable/chunks/C8rRKIXT.js +0 -1
  636. package/build/client/_app/immutable/chunks/C8rRKIXT.js.br +0 -0
  637. package/build/client/_app/immutable/chunks/C8rRKIXT.js.gz +0 -0
  638. package/build/client/_app/immutable/chunks/CJjJLwzv2.js +0 -1
  639. package/build/client/_app/immutable/chunks/CJjJLwzv2.js.br +0 -0
  640. package/build/client/_app/immutable/chunks/CJjJLwzv2.js.gz +0 -0
  641. package/build/client/_app/immutable/chunks/CxyLMlpt2.js.br +0 -0
  642. package/build/client/_app/immutable/chunks/CxyLMlpt2.js.gz +0 -0
  643. package/build/client/_app/immutable/chunks/D6jhrpTg.js +0 -1
  644. package/build/client/_app/immutable/chunks/D6jhrpTg.js.br +0 -0
  645. package/build/client/_app/immutable/chunks/D6jhrpTg.js.gz +0 -0
  646. package/build/client/_app/immutable/chunks/D8YEa1po.js +0 -1
  647. package/build/client/_app/immutable/chunks/D8YEa1po.js.br +0 -0
  648. package/build/client/_app/immutable/chunks/D8YEa1po.js.gz +0 -0
  649. package/build/client/_app/immutable/chunks/D9dKmajw.js +0 -1
  650. package/build/client/_app/immutable/chunks/D9dKmajw.js.br +0 -0
  651. package/build/client/_app/immutable/chunks/D9dKmajw.js.gz +0 -0
  652. package/build/client/_app/immutable/chunks/DBJWcC6Y.js +0 -1
  653. package/build/client/_app/immutable/chunks/DBJWcC6Y.js.br +0 -0
  654. package/build/client/_app/immutable/chunks/DBJWcC6Y.js.gz +0 -0
  655. package/build/client/_app/immutable/chunks/DHoA038D.js +0 -1
  656. package/build/client/_app/immutable/chunks/DHoA038D.js.br +0 -2
  657. package/build/client/_app/immutable/chunks/DHoA038D.js.gz +0 -0
  658. package/build/client/_app/immutable/chunks/DKgYpi6_.js +0 -1
  659. package/build/client/_app/immutable/chunks/DKgYpi6_.js.br +0 -0
  660. package/build/client/_app/immutable/chunks/DKgYpi6_.js.gz +0 -0
  661. package/build/client/_app/immutable/chunks/DatGSObE.js +0 -1
  662. package/build/client/_app/immutable/chunks/DatGSObE.js.br +0 -0
  663. package/build/client/_app/immutable/chunks/DatGSObE.js.gz +0 -0
  664. package/build/client/_app/immutable/chunks/Dg0acPpT.js +0 -1
  665. package/build/client/_app/immutable/chunks/Dg0acPpT.js.br +0 -0
  666. package/build/client/_app/immutable/chunks/Dg0acPpT.js.gz +0 -0
  667. package/build/client/_app/immutable/chunks/DjWKKtqp.js +0 -1
  668. package/build/client/_app/immutable/chunks/DjWKKtqp.js.br +0 -1
  669. package/build/client/_app/immutable/chunks/DjWKKtqp.js.gz +0 -0
  670. package/build/client/_app/immutable/chunks/Dq0DUAz1.js +0 -1
  671. package/build/client/_app/immutable/chunks/Dq0DUAz1.js.br +0 -0
  672. package/build/client/_app/immutable/chunks/Dq0DUAz1.js.gz +0 -0
  673. package/build/client/_app/immutable/chunks/HKLi1Yz3.js +0 -1
  674. package/build/client/_app/immutable/chunks/HKLi1Yz3.js.br +0 -0
  675. package/build/client/_app/immutable/chunks/HKLi1Yz3.js.gz +0 -0
  676. package/build/client/_app/immutable/chunks/N7PXjlRH.js +0 -1
  677. package/build/client/_app/immutable/chunks/N7PXjlRH.js.br +0 -0
  678. package/build/client/_app/immutable/chunks/N7PXjlRH.js.gz +0 -0
  679. package/build/client/_app/immutable/chunks/hpWJNn0t2.js +0 -2
  680. package/build/client/_app/immutable/chunks/hpWJNn0t2.js.br +0 -0
  681. package/build/client/_app/immutable/chunks/hpWJNn0t2.js.gz +0 -0
  682. package/build/client/_app/immutable/chunks/iQu0D9Ux.js +0 -1
  683. package/build/client/_app/immutable/chunks/iQu0D9Ux.js.br +0 -0
  684. package/build/client/_app/immutable/chunks/iQu0D9Ux.js.gz +0 -0
  685. package/build/client/_app/immutable/entry/app.CLgh6Mx_.js +0 -2
  686. package/build/client/_app/immutable/entry/app.CLgh6Mx_.js.br +0 -0
  687. package/build/client/_app/immutable/entry/app.CLgh6Mx_.js.gz +0 -0
  688. package/build/client/_app/immutable/entry/start.D5GPCQZD.js +0 -1
  689. package/build/client/_app/immutable/entry/start.D5GPCQZD.js.br +0 -0
  690. package/build/client/_app/immutable/entry/start.D5GPCQZD.js.gz +0 -0
  691. package/build/client/_app/immutable/nodes/0.BOoI-hsu.js +0 -4
  692. package/build/client/_app/immutable/nodes/0.BOoI-hsu.js.br +0 -0
  693. package/build/client/_app/immutable/nodes/0.BOoI-hsu.js.gz +0 -0
  694. package/build/client/_app/immutable/nodes/1.KYdA6ppX.js.br +0 -2
  695. package/build/client/_app/immutable/nodes/1.KYdA6ppX.js.gz +0 -0
  696. package/build/client/_app/immutable/nodes/2.DBz20KgG.js.br +0 -0
  697. package/build/client/_app/immutable/nodes/2.DBz20KgG.js.gz +0 -0
  698. package/build/client/_app/immutable/nodes/3.19DIoFtw.js +0 -1
  699. package/build/client/_app/immutable/nodes/3.19DIoFtw.js.br +0 -0
  700. package/build/client/_app/immutable/nodes/3.19DIoFtw.js.gz +0 -0
  701. package/build/client/_app/immutable/nodes/4.BvMzqBJj.js +0 -3
  702. package/build/client/_app/immutable/nodes/4.BvMzqBJj.js.br +0 -0
  703. package/build/client/_app/immutable/nodes/4.BvMzqBJj.js.gz +0 -0
  704. package/build/client/_app/immutable/nodes/5.Dq6obSGG.js +0 -42
  705. package/build/client/_app/immutable/nodes/5.Dq6obSGG.js.br +0 -0
  706. package/build/client/_app/immutable/nodes/5.Dq6obSGG.js.gz +0 -0
  707. package/build/client/_app/immutable/nodes/6.BOHISqs-.js +0 -5
  708. package/build/client/_app/immutable/nodes/6.BOHISqs-.js.br +0 -0
  709. package/build/client/_app/immutable/nodes/6.BOHISqs-.js.gz +0 -0
  710. package/build/client/_app/immutable/nodes/7.CemgNJfw.js.br +0 -0
  711. package/build/client/_app/immutable/nodes/7.CemgNJfw.js.gz +0 -0
  712. package/build/client/_app/immutable/nodes/8.DejSfIYh.js +0 -5
  713. package/build/client/_app/immutable/nodes/8.DejSfIYh.js.br +0 -0
  714. package/build/client/_app/immutable/nodes/8.DejSfIYh.js.gz +0 -0
  715. package/build/client/_app/immutable/nodes/9.CMW6a2Lg.js.br +0 -0
  716. package/build/client/_app/immutable/nodes/9.CMW6a2Lg.js.gz +0 -0
  717. package/build/server/chunks/0-C_o0oz-N.js +0 -15
  718. package/build/server/chunks/0-C_o0oz-N.js.map +0 -1
  719. package/build/server/chunks/1-DPpKAKXV.js +0 -9
  720. package/build/server/chunks/2-AlfFqtL1.js +0 -9
  721. package/build/server/chunks/2-AlfFqtL1.js.map +0 -1
  722. package/build/server/chunks/3-vbjUt_51.js +0 -16
  723. package/build/server/chunks/3-vbjUt_51.js.map +0 -1
  724. package/build/server/chunks/4-BNow4x6D.js +0 -16
  725. package/build/server/chunks/4-BNow4x6D.js.map +0 -1
  726. package/build/server/chunks/5-D7OCJpxD.js +0 -9
  727. package/build/server/chunks/5-D7OCJpxD.js.map +0 -1
  728. package/build/server/chunks/6-QQ7r8Rd5.js +0 -9
  729. package/build/server/chunks/7-CbPLGaIG.js +0 -9
  730. package/build/server/chunks/7-CbPLGaIG.js.map +0 -1
  731. package/build/server/chunks/8-CVO-E-sf.js +0 -9
  732. package/build/server/chunks/8-CVO-E-sf.js.map +0 -1
  733. package/build/server/chunks/9-DxT1baO5.js +0 -9
  734. package/build/server/chunks/9-DxT1baO5.js.map +0 -1
  735. package/build/server/chunks/FeatureTransforms-UWDHmWEg.js.map +0 -1
  736. package/build/server/chunks/FeatureValidator-B8qEjAUW.js.map +0 -1
  737. package/build/server/chunks/ManageTagsDialog-DBxA3tM2.js.map +0 -1
  738. package/build/server/chunks/ProgressBar-CfhccQ83.js.map +0 -1
  739. package/build/server/chunks/ProjectsIndex-CoDrvRya.js.map +0 -1
  740. package/build/server/chunks/RenameTag-BuWLMl1m.js.map +0 -1
  741. package/build/server/chunks/TagDotStrip-B1XQ0m0Y.js.map +0 -1
  742. package/build/server/chunks/TagPalette-CtMNYCmu.js.map +0 -1
  743. package/build/server/chunks/_layout.svelte-BREws55o.js +0 -616
  744. package/build/server/chunks/_layout.svelte-BREws55o.js.map +0 -1
  745. package/build/server/chunks/_page.svelte-B1s7jxaQ.js.map +0 -1
  746. package/build/server/chunks/_page.svelte-B7hT3P8E.js +0 -29
  747. package/build/server/chunks/_page.svelte-BqSC-1vK.js.map +0 -1
  748. package/build/server/chunks/_page.svelte-BtI2zZ_Z.js.map +0 -1
  749. package/build/server/chunks/_page.svelte-De508ek8.js +0 -29
  750. package/build/server/chunks/_page.svelte-Zf9H4KOP.js.map +0 -1
  751. package/build/server/chunks/_server.ts-0uNTZKwF.js.map +0 -1
  752. package/build/server/chunks/_server.ts-DcZPhyhK.js +0 -59
  753. package/build/server/chunks/_server.ts-DcZPhyhK.js.map +0 -1
  754. package/build/server/chunks/_server.ts-Dy5zQHe2.js.map +0 -1
  755. package/build/server/chunks/_server.ts-YnfXkeMJ.js.map +0 -1
  756. package/build/server/chunks/_server.ts-vcaqdwxS.js.map +0 -1
  757. package/build/server/chunks/browserContainer-CNX7ANld.js.map +0 -1
  758. package/build/server/chunks/builderModeStore.svelte-ihupr-3p.js.map +0 -1
  759. package/build/server/chunks/client-DfpLcAZ9.js +0 -24
  760. package/build/server/chunks/featuresStore.svelte-DR1gYZyu.js +0 -98
  761. package/build/server/chunks/featuresStore.svelte-DR1gYZyu.js.map +0 -1
  762. package/build/server/chunks/hooks.server-Rv301GTB.js.map +0 -1
  763. package/build/server/chunks/identityStore.svelte-IIvW2_3b.js +0 -59
  764. package/build/server/chunks/identityStore.svelte-IIvW2_3b.js.map +0 -1
  765. package/build/server/chunks/projectsStore.svelte-QDz5PchX.js.map +0 -1
  766. package/build/server/chunks/reconcile-Dv7jS3C8.js.map +0 -1
  767. package/build/server/chunks/snapshotRepository-BPXCjX92.js.map +0 -1
  768. package/build/server/chunks/syncBridge-BwXzPdjJ.js.map +0 -1
  769. package/src/features/implementation-queue/presentation/components/QueuePanel.svelte +0 -204
  770. package/src/features/implementation-queue/presentation/stores/queueStore.svelte.ts +0 -143
  771. /package/build/client/_app/immutable/assets/{9.nv0I59TU.css → 12.nv0I59TU.css} +0 -0
  772. /package/build/client/_app/immutable/assets/{9.nv0I59TU.css.br → 12.nv0I59TU.css.br} +0 -0
@@ -1,985 +1,1001 @@
1
- import type { Action } from '$features/behavior-model/domain/entities/Action';
2
- import { committedActions } from '$features/behavior-model/domain/entities/Action';
3
- import type { Feature } from '$features/behavior-model/domain/entities/Feature';
4
- import type { Surface } from '$features/behavior-model/domain/entities/Surface';
5
- import { isExpression, type Expression } from '$features/behavior-model/domain/value-objects/Expression';
6
- import type { Effect } from '$features/behavior-model/domain/value-objects/Effect';
7
- import {
8
- flattenLeafConditions,
9
- isParamLeft,
10
- type RuleCondition
11
- } from '$features/behavior-model/domain/value-objects/RuleCondition';
12
- import type { StatePath } from '$features/behavior-model/domain/value-objects/StatePath';
13
- import type {
14
- CheckLocation,
15
- MaturityIssue,
16
- MaturityReport,
17
- PassedCheck,
18
- SurfacePanelTab
19
- } from './MaturityReport';
20
- import { combineReports, emptyReport } from './MaturityReport';
21
-
22
- type Check = {
23
- readonly area: string;
24
- readonly weight: number;
25
- readonly severity: 'critical' | 'recommended';
26
- readonly passes: boolean;
27
- readonly message: string;
28
- /** Short label shown next to a passed check (the "good news" version). */
29
- readonly passLabel: string;
30
- readonly tab?: SurfacePanelTab;
31
- };
32
-
33
- const reportFromChecks = (
34
- targetKind: MaturityIssue['targetKind'],
35
- target: string,
36
- baseLocation: CheckLocation,
37
- checks: readonly Check[]
38
- ): MaturityReport => {
39
- let score = 0;
40
- let maxScore = 0;
41
- const criticalIssues: MaturityIssue[] = [];
42
- const recommendedIssues: MaturityIssue[] = [];
43
- const passedChecks: PassedCheck[] = [];
44
- for (const check of checks) {
45
- maxScore += check.weight;
46
- const location: CheckLocation = {
47
- ...baseLocation,
48
- surfacePanelTab: check.tab ?? baseLocation.surfacePanelTab
49
- };
50
- if (check.passes) {
51
- score += check.weight;
52
- passedChecks.push({
53
- ...location,
54
- target,
55
- targetKind,
56
- area: check.area,
57
- message: check.passLabel
58
- });
59
- continue;
60
- }
61
- const issue: MaturityIssue = {
62
- ...location,
63
- target,
64
- targetKind,
65
- area: check.area,
66
- message: check.message,
67
- severity: check.severity
68
- };
69
- if (check.severity === 'critical') {
70
- criticalIssues.push(issue);
71
- } else {
72
- recommendedIssues.push(issue);
73
- }
74
- }
75
- return {
76
- score,
77
- maxScore,
78
- percentage: maxScore === 0 ? 100 : Math.round((score / maxScore) * 100),
79
- criticalIssues,
80
- recommendedIssues,
81
- passedChecks
82
- };
83
- };
84
-
85
- const expressionStateReads = (expression: Expression): StatePath[] => {
86
- // Defensive: legacy snapshots can hold malformed children (e.g. `right: 1`
87
- // instead of `{kind:"literal",value:1}`). The normalizer rewrites them on
88
- // write/import, but we guard here too so the scorer can't crash on data
89
- // that slipped past either boundary.
90
- if (!isExpression(expression)) return [];
91
- switch (expression.kind) {
92
- case 'state':
93
- return [expression.path];
94
- case 'add':
95
- case 'sub':
96
- case 'mul':
97
- case 'div':
98
- case 'mod':
99
- case 'min':
100
- case 'max':
101
- return [
102
- ...expressionStateReads(expression.left),
103
- ...expressionStateReads(expression.right)
104
- ];
105
- case 'neg':
106
- case 'not':
107
- case 'sum':
108
- case 'count':
109
- return expressionStateReads(expression.operand);
110
- case 'sum_pluck':
111
- return expressionStateReads(expression.operand);
112
- case 'count_where':
113
- return [
114
- ...expressionStateReads(expression.operand),
115
- ...expressionStateReads(expression.equals)
116
- ];
117
- case 'switch':
118
- // For each case: flatten the `when` to leaf conditions, collect each
119
- // leaf's `left` path and any state reads on its `right` Expression,
120
- // then recurse into the case's `then` Expression. Plus the default.
121
- return [
122
- ...expression.cases.flatMap((c) => [
123
- ...flattenLeafConditions(c.when).flatMap((leaf) => [
124
- // Skip param-lefts: they reference action parameters, not
125
- // surface state, so they don't count as a state-read here.
126
- ...(isParamLeft(leaf.left) ? [] : [leaf.left as StatePath]),
127
- ...(isExpression(leaf.right) ? expressionStateReads(leaf.right) : [])
128
- ]),
129
- ...expressionStateReads(c.then)
130
- ]),
131
- ...expressionStateReads(expression.default)
132
- ];
133
- case 'literal':
134
- case 'param':
135
- return [];
136
- }
137
- };
138
-
139
- // Recurses into composite conditions and ignores the unconditional case
140
- // (condition === undefined). Each leaf contributes its `.left` and any
141
- // state references buried in an Expression on `.right`.
142
- const conditionStateReads = (
143
- condition: RuleCondition | undefined
144
- ): StatePath[] => {
145
- const out: StatePath[] = [];
146
- for (const leaf of flattenLeafConditions(condition)) {
147
- // Param-left conditions reference action parameters, not surface
148
- // state; they don't contribute a state-path read here.
149
- if (!isParamLeft(leaf.left)) out.push(leaf.left as StatePath);
150
- if (isExpression(leaf.right)) out.push(...expressionStateReads(leaf.right));
151
- }
152
- return out;
153
- };
154
-
155
- // Every state path an action reads from OR writes to (conditions, set_state targets,
156
- // expression operands, requiredStates). Used to validate paths against surface scope.
157
- const allCapabilityStatePaths = (action: Action): readonly StatePath[] => {
158
- const paths = new Set<StatePath>();
159
- const addCondition = (c: RuleCondition | undefined) => {
160
- for (const p of conditionStateReads(c)) paths.add(p);
161
- };
162
- const addEffect = (e: Effect) => {
163
- if (e.type !== 'set_state') return;
164
- paths.add(e.path);
165
- if (isExpression(e.value)) for (const p of expressionStateReads(e.value)) paths.add(p);
166
- };
167
- for (const rule of action.rules) { addCondition(rule.condition); addEffect(rule.effect); }
168
- for (const inv of action.invariants) addCondition(inv.condition);
169
- for (const e of [...action.effects, ...(action.onBlockedEffects ?? [])]) addEffect(e);
170
- for (const path of action.requiredStates) paths.add(path);
171
- return [...paths];
172
- };
173
-
174
- const allSurfaceRuleStatePaths = (surface: Surface): readonly StatePath[] => {
175
- const paths = new Set<StatePath>();
176
- for (const rule of surface.rules) {
177
- for (const p of conditionStateReads(rule.condition)) paths.add(p);
178
- if (rule.effect.type === 'set_state') {
179
- paths.add(rule.effect.path);
180
- if (isExpression(rule.effect.value))
181
- for (const p of expressionStateReads(rule.effect.value)) paths.add(p);
182
- }
183
- }
184
- return [...paths];
185
- };
186
-
187
- // All state paths visible on a surface: own definitions + paths shared TO this surface
188
- // from any other surface in the feature.
189
- const buildKnownPaths = (surfaceId: string, allSurfaces: readonly Surface[]): ReadonlySet<string> => {
190
- const paths = new Set<string>();
191
- for (const surface of allSurfaces) {
192
- for (const def of surface.stateDefinitions) {
193
- const isOwn = String(surface.id) === surfaceId;
194
- const isShared = (def.sharedWith ?? []).map(String).includes(surfaceId);
195
- if (isOwn || isShared) paths.add(String(def.path));
196
- }
197
- }
198
- return paths;
199
- };
200
-
201
- // Every state path declared anywhere in the feature, regardless of sharing.
202
- // Used to distinguish "truly undeclared" (critical) from "declared but not
203
- // shared with this surface" (recommended).
204
- const buildAllDeclaredPaths = (allSurfaces: readonly Surface[]): ReadonlySet<string> => {
205
- const paths = new Set<string>();
206
- for (const surface of allSurfaces) {
207
- for (const def of surface.stateDefinitions) {
208
- paths.add(String(def.path));
209
- }
210
- }
211
- return paths;
212
- };
213
-
214
- const effectStateReads = (effect: Effect): StatePath[] => {
215
- if (effect.type !== 'set_state' || !isExpression(effect.value)) return [];
216
- return expressionStateReads(effect.value);
217
- };
218
-
219
- const capabilityStateReads = (action: Action): StatePath[] => {
220
- const paths = new Set<StatePath>();
221
- for (const rule of action.rules) {
222
- for (const path of conditionStateReads(rule.condition)) paths.add(path);
223
- }
224
- for (const invariant of action.invariants) {
225
- for (const path of conditionStateReads(invariant.condition)) paths.add(path);
226
- }
227
- for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
228
- for (const path of effectStateReads(effect)) paths.add(path);
229
- }
230
- return [...paths];
231
- };
232
-
233
- const effectUsesExpression = (effect: Effect): boolean =>
234
- effect.type === 'set_state' && isExpression(effect.value);
235
-
236
- const conditionUsesExpression = (c: RuleCondition | undefined): boolean =>
237
- flattenLeafConditions(c).some((leaf) => isExpression(leaf.right));
238
-
239
- const capabilityUsesExpression = (action: Action): boolean =>
240
- action.rules.some((rule) => conditionUsesExpression(rule.condition)) ||
241
- action.invariants.some((invariant) => conditionUsesExpression(invariant.condition)) ||
242
- [...action.effects, ...(action.onBlockedEffects ?? [])].some(effectUsesExpression);
243
-
244
- const surfaceTransitionTargets = (surface: Surface): readonly string[] => {
245
- const targets = new Set<string>();
246
- for (const action of surface.actions) {
247
- for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
248
- if (effect.type === 'transition_surface' && effect.target) targets.add(String(effect.target));
249
- }
250
- for (const rule of action.rules) {
251
- if (rule.effect.type === 'transition_surface' && rule.effect.target) {
252
- targets.add(String(rule.effect.target));
253
- }
254
- }
255
- }
256
- for (const rule of surface.rules) {
257
- if (rule.effect.type === 'transition_surface' && rule.effect.target) {
258
- targets.add(String(rule.effect.target));
259
- }
260
- }
261
- return [...targets];
262
- };
263
-
264
- const emittedEventNames = (feature: Feature): readonly string[] => {
265
- const names = new Set<string>();
266
- for (const surface of feature.surfaces) {
267
- for (const action of surface.actions) {
268
- for (const event of action.emittedEvents) names.add(String(event));
269
- for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
270
- if (effect.type === 'emit_event') names.add(String(effect.event));
271
- }
272
- for (const rule of action.rules) {
273
- if (rule.effect.type === 'emit_event') names.add(String(rule.effect.event));
274
- }
275
- }
276
- for (const rule of surface.rules) {
277
- if (rule.effect.type === 'emit_event') names.add(String(rule.effect.event));
278
- }
279
- }
280
- return [...names];
281
- };
282
-
283
- const hasResourceReferences = (feature: Feature): boolean =>
284
- feature.surfaces.some((surface) =>
285
- surface.actions.some((action) =>
286
- action.parameters.some((parameter) => parameter.resourceId !== undefined)
287
- )
288
- );
289
-
290
- const hasScenarioExpectations = (feature: Feature): boolean =>
291
- feature.surfaces.some((surface) =>
292
- surface.actions.some((action) =>
293
- (action.scenarios ?? []).some(
294
- (scenario) =>
295
- scenario.expectedStatus !== undefined ||
296
- (scenario.expectedAssertions?.length ?? 0) > 0
297
- )
298
- )
299
- );
300
-
301
- const capabilityChecks = (
302
- action: Action,
303
- knownPaths?: ReadonlySet<string>,
304
- allDeclaredPaths?: ReadonlySet<string>
305
- ): readonly Check[] => {
306
- const sensitiveCategories = new Set(['security', 'permissions', 'compliance']);
307
- const hasPermissionRule = action.rules.some((r) => sensitiveCategories.has(r.category));
308
- const dataMutating = [...action.effects, ...(action.onBlockedEffects ?? [])].some(
309
- (e) => e.type === 'set_state'
310
- );
311
- const hasValidationRule = action.rules.some((r) => r.category === 'validation');
312
- const hasParameterValidations = action.parameters.some(
313
- (p) => p.validations !== undefined && p.validations.length > 0
314
- );
315
- const hasValidatableParam = action.parameters.some(
316
- (p) =>
317
- p.type === 'string' ||
318
- p.type === 'number' ||
319
- p.type === 'email' ||
320
- p.type === 'url' ||
321
- p.type === 'date' ||
322
- p.type === 'time' ||
323
- p.type === 'timestamp' ||
324
- p.type === 'object' ||
325
- p.type === 'array' ||
326
- p.type === 'geolocation'
327
- );
328
- const hasInputs = action.parameters.length > 0;
329
- const hasParameterDefaults = action.parameters.every(
330
- (p) => p.required || p.defaultValue !== undefined
331
- );
332
- const enumParametersConfigured = action.parameters.every(
333
- (p) => p.type !== 'enum' || (p.enumValues !== undefined && p.enumValues.length > 0)
334
- );
335
- const stateReads = capabilityStateReads(action);
336
- const hasRequiredStateCoverage =
337
- action.requiredStates.length === 0 ||
338
- action.requiredStates.length > 0 ||
339
- stateReads.length === 0;
340
- const emitsEventsFromEffects = action.effects.some((e) => e.type === 'emit_event');
341
- const hasBlockedFallback = (action.onBlockedEffects ?? []).length > 0;
342
- const hasComplexBehavior =
343
- action.rules.length > 1 ||
344
- action.effects.length > 1 ||
345
- action.invariants.length > 0 ||
346
- hasInputs ||
347
- dataMutating;
348
- const scenarioCount = action.scenarios?.length ?? 0;
349
- const hasScenarioCoverage = scenarioCount === 0 || !hasComplexBehavior || scenarioCount > 0;
350
- const hasExpressionUse = capabilityUsesExpression(action);
351
-
352
- return [
353
- {
354
- area: 'intent',
355
- weight: 1,
356
- severity: 'critical',
357
- passes: action.intent.trim().length > 0,
358
- message: 'Add a short description of what this action is for.',
359
- passLabel: 'Has a description',
360
- tab: 'actions'
361
- },
362
- {
363
- area: 'rules',
364
- weight: 1,
365
- severity: 'recommended',
366
- passes: action.rules.length > 0,
367
- message: 'Add rules to control when this action can or cannot run.',
368
- passLabel: `Has ${action.rules.length} rule${action.rules.length === 1 ? '' : 's'}`,
369
- tab: 'actions'
370
- },
371
- {
372
- area: 'effects',
373
- weight: 1,
374
- severity: 'critical',
375
- passes: action.effects.length > 0 || action.rules.length > 0,
376
- message: 'Nothing happens when this runs. Add an effect or a rule.',
377
- passLabel: 'Has effects or rules',
378
- tab: 'actions'
379
- },
380
- {
381
- area: 'events',
382
- weight: 1,
383
- severity: 'recommended',
384
- passes:
385
- action.emittedEvents.length > 0 ||
386
- emitsEventsFromEffects,
387
- message:
388
- 'Other parts of your feature cannot react when this runs. Make it emit and declare an event.',
389
- passLabel:
390
- action.emittedEvents.length > 0 && emitsEventsFromEffects
391
- ? 'Emits and declares events'
392
- : 'Has event signaling',
393
- tab: 'actions'
394
- },
395
- {
396
- area: 'required states',
397
- weight: 1,
398
- severity: 'recommended',
399
- passes: hasRequiredStateCoverage,
400
- message:
401
- 'Declare requiredStates so readers and implementation audits know which state this action depends on.',
402
- passLabel:
403
- action.requiredStates.length === 0
404
- ? 'Required states not declared'
405
- : `Documents ${action.requiredStates.length} required state${
406
- action.requiredStates.length === 1 ? '' : 's'
407
- }`,
408
- tab: 'actions'
409
- },
410
- {
411
- area: 'event declarations',
412
- weight: 1,
413
- severity: 'recommended',
414
- passes: !emitsEventsFromEffects || action.emittedEvents.length > 0,
415
- message:
416
- 'This action emits an event effect but does not declare emittedEvents for audit and discovery.',
417
- passLabel: emitsEventsFromEffects ? 'Declares emitted events' : 'No emitted event effects',
418
- tab: 'actions'
419
- },
420
- {
421
- area: 'permissions',
422
- weight: 1,
423
- severity: dataMutating ? 'critical' : 'recommended',
424
- passes: !dataMutating || hasPermissionRule,
425
- message:
426
- 'This action changes data. Add a rule that decides who is allowed to run it.',
427
- passLabel: dataMutating
428
- ? 'Has a permission rule'
429
- : 'Does not change data. No permission rule needed',
430
- tab: 'actions'
431
- },
432
- {
433
- area: 'validation',
434
- weight: 1,
435
- severity: 'recommended',
436
- passes:
437
- action.parameters.length === 0 ||
438
- !hasValidatableParam ||
439
- hasValidationRule ||
440
- hasParameterValidations,
441
- message:
442
- 'Inputs are not checked before this runs. Add a rule with category "validation" or attach validations to a parameter.',
443
- passLabel:
444
- action.parameters.length === 0
445
- ? 'No inputs to validate'
446
- : !hasValidatableParam
447
- ? 'Inputs are booleans/enums. No validation needed'
448
- : 'Validates its inputs',
449
- tab: 'actions'
450
- },
451
- {
452
- area: 'parameters',
453
- weight: 1,
454
- severity: 'recommended',
455
- passes: !hasInputs || (hasParameterDefaults && enumParametersConfigured),
456
- message:
457
- 'Make parameter behavior explicit: optional parameters need defaults, and enum parameters need allowed values.',
458
- passLabel: hasInputs ? 'Parameters have defaults/enum options' : 'No parameters',
459
- tab: 'actions'
460
- },
461
- {
462
- area: 'block handling',
463
- weight: 1,
464
- severity: 'recommended',
465
- passes:
466
- action.rules.length === 0 ||
467
- hasBlockedFallback ||
468
- action.rules.some((rule) => rule.effect.type !== 'allow_action'),
469
- message:
470
- 'Add on-block effects when a blocked run should show a message, emit an audit event, or navigate.',
471
- passLabel: action.rules.length === 0 ? 'No block paths' : 'Has block fallback effects',
472
- tab: 'actions'
473
- },
474
- {
475
- area: 'scenarios',
476
- weight: 1,
477
- severity: 'recommended',
478
- passes: hasScenarioCoverage,
479
- message:
480
- 'Add at least one scenario for this non-trivial action so examples become regression checks.',
481
- passLabel: hasComplexBehavior
482
- ? `Has ${scenarioCount} scenario${scenarioCount === 1 ? '' : 's'}`
483
- : 'Scenarios optional until added',
484
- tab: 'actions'
485
- },
486
- {
487
- area: 'expressions',
488
- weight: 1,
489
- severity: 'recommended',
490
- passes: !hasExpressionUse || action.rules.length > 0 || action.effects.length > 0,
491
- message:
492
- 'Expression values should live in explicit rules or effects so readers can inspect the calculation.',
493
- passLabel: hasExpressionUse ? 'Uses expression values' : 'No expression values needed',
494
- tab: 'actions'
495
- },
496
- ...(knownPaths !== undefined
497
- ? (() => {
498
- const referenced = allCapabilityStatePaths(action).map(String);
499
- const undeclared = referenced.filter((p) => !allDeclaredPaths!.has(p));
500
- const unsharedOnly = referenced.filter(
501
- (p) => allDeclaredPaths!.has(p) && !knownPaths.has(p)
502
- );
503
- return [
504
- // Truly undefined paths (declared nowhere in the feature) are a
505
- // real spec gap: the simulator returns undefined, comparisons
506
- // misbehave. Keep this critical.
507
- {
508
- area: 'undefined state',
509
- weight: 1,
510
- severity: 'critical' as const,
511
- passes: undeclared.length === 0,
512
- message: `Uses state ${undeclared.length === 1 ? 'path' : 'paths'} not declared anywhere in this feature: ${undeclared.slice(0, 3).join(', ')}${undeclared.length > 3 ? '…' : ''}. Declare ${undeclared.length === 1 ? 'it' : 'them'} on a surface.`,
513
- passLabel: 'All referenced state paths are declared',
514
- tab: 'actions' as SurfacePanelTab
515
- },
516
- // Paths declared on a SIBLING surface without sharedWith are not a
517
- // runtime bug (the simulator reads from a unified snapshot) but
518
- // hide a cross-surface dependency. Downgraded from critical to
519
- // recommended so LLMs aren't blocked by sharedWith hygiene.
520
- {
521
- area: 'unshared state',
522
- weight: 1,
523
- severity: 'recommended' as const,
524
- passes: unsharedOnly.length === 0,
525
- message: `Reads state ${unsharedOnly.length === 1 ? 'path' : 'paths'} declared on another surface: ${unsharedOnly.slice(0, 3).join(', ')}${unsharedOnly.length > 3 ? '…' : ''}. Add this surface to that StateDefinition's sharedWith to make the dependency explicit.`,
526
- passLabel: 'No unshared cross-surface reads',
527
- tab: 'actions' as SurfacePanelTab
528
- }
529
- ];
530
- })()
531
- : [])
532
- ];
533
- };
534
-
535
- export const scoreCapability = (
536
- surface: Surface,
537
- action: Action,
538
- allSurfaces?: readonly Surface[]
539
- ): MaturityReport => {
540
- const knownPaths =
541
- allSurfaces !== undefined ? buildKnownPaths(String(surface.id), allSurfaces) : undefined;
542
- const allDeclaredPaths =
543
- allSurfaces !== undefined ? buildAllDeclaredPaths(allSurfaces) : undefined;
544
- return reportFromChecks(
545
- 'action',
546
- action.name,
547
- {
548
- surfaceId: surface.id as unknown as string,
549
- actionId: action.id as unknown as string,
550
- surfacePanelTab: 'actions'
551
- },
552
- capabilityChecks(action, knownPaths, allDeclaredPaths)
553
- );
554
- };
555
-
556
- const surfaceChecks = (
557
- surface: Surface,
558
- knownPaths?: ReadonlySet<string>,
559
- allDeclaredPaths?: ReadonlySet<string>
560
- ): readonly Check[] => {
561
- const transitionTargets = surfaceTransitionTargets(surface);
562
- const hasSharedState = surface.stateDefinitions.some(
563
- (definition) => definition.sharedWith !== undefined && definition.sharedWith.length > 0
564
- );
565
- const hasEnumStateWithoutValues = surface.stateDefinitions.some(
566
- (definition) =>
567
- definition.type === 'enum' &&
568
- (definition.enumValues === undefined || definition.enumValues.length === 0)
569
- );
570
- const hasSurfaceWideRules =
571
- surface.actions.length <= 1 ||
572
- surface.rules.length > 0 ||
573
- surface.actions.every((action) => action.rules.length > 0);
574
-
575
- return [
576
- {
577
- area: 'actions',
578
- weight: 1,
579
- severity: 'critical',
580
- passes: surface.actions.length > 0,
581
- message: 'Add at least one thing the user can do here.',
582
- passLabel: `Has ${surface.actions.length} capabilit${
583
- surface.actions.length === 1 ? 'y' : 'ies'
584
- }`,
585
- tab: 'actions'
586
- },
587
- {
588
- area: 'states',
589
- weight: 1,
590
- severity: 'recommended',
591
- // Count shared-in state as "has state". A surface that legitimately
592
- // operates on state shared from another surface (e.g. a modal that
593
- // reads/writes the parent screen's payment.* paths) shouldn't be
594
- // flagged for "no state" just because it doesn't own any natively.
595
- passes:
596
- surface.stateDefinitions.length > 0 ||
597
- (knownPaths !== undefined && knownPaths.size > 0),
598
- message: 'Add some state - the data your rules read to decide what happens.',
599
- passLabel: `Has ${surface.stateDefinitions.length} state value${
600
- surface.stateDefinitions.length === 1 ? '' : 's'
601
- }`,
602
- tab: 'state'
603
- },
604
- {
605
- area: 'state metadata',
606
- weight: 1,
607
- severity: 'recommended',
608
- passes:
609
- surface.stateDefinitions.length === 0 ||
610
- !hasEnumStateWithoutValues,
611
- message:
612
- 'Document state definitions with descriptions, and give enum states their allowed values.',
613
- passLabel:
614
- surface.stateDefinitions.length === 0
615
- ? 'No state to document'
616
- : 'State definitions are documented',
617
- tab: 'state'
618
- },
619
- {
620
- area: 'shared state',
621
- weight: 1,
622
- severity: 'recommended',
623
- passes:
624
- surface.stateDefinitions.length <= 1 ||
625
- hasSharedState ||
626
- surface.stateDefinitions.every((definition) => definition.sharedWith === undefined),
627
- message:
628
- 'Shared state is declared but incomplete. Check sharedWith links on state definitions.',
629
- passLabel: hasSharedState ? 'Declares shared state' : 'No obvious shared state',
630
- tab: 'state'
631
- },
632
- {
633
- area: 'surface rules',
634
- weight: 1,
635
- severity: 'recommended',
636
- passes: hasSurfaceWideRules,
637
- message:
638
- 'This surface has multiple actions and no visible gating. Add surface rules or action rules.',
639
- passLabel:
640
- surface.actions.length <= 1 ? 'No cross-cutting gate needed' : 'Has surface rules',
641
- tab: 'rules'
642
- },
643
- {
644
- area: 'invariants',
645
- weight: 1,
646
- severity: 'recommended',
647
- passes:
648
- surface.invariants.length > 0 ||
649
- surface.actions.some((c) => c.invariants.length > 0),
650
- message:
651
- 'Add an invariant - a fact that should always be true here, so broken states are caught early.',
652
- passLabel: 'Has invariants',
653
- tab: 'invariants'
654
- },
655
- {
656
- area: 'transitions',
657
- weight: 1,
658
- severity: 'recommended',
659
- passes: transitionTargets.length === 0 || surface.transitions.length > 0,
660
- message:
661
- 'This surface navigates through effects. Declare transitions so the graph is visible outside individual actions.',
662
- passLabel:
663
- transitionTargets.length === 0 ? 'No navigation effects' : 'Declares surface transitions',
664
- tab: 'actions'
665
- },
666
- ...(knownPaths !== undefined
667
- ? (() => {
668
- const referenced = allSurfaceRuleStatePaths(surface).map(String);
669
- const undeclared = referenced.filter((p) => !allDeclaredPaths!.has(p));
670
- const unsharedOnly = referenced.filter(
671
- (p) => allDeclaredPaths!.has(p) && !knownPaths.has(p)
672
- );
673
- return [
674
- {
675
- area: 'undefined state in rules',
676
- weight: 1,
677
- severity: 'critical' as const,
678
- passes: undeclared.length === 0,
679
- message: `Surface rule uses state ${undeclared.length === 1 ? 'path' : 'paths'} not declared anywhere in this feature: ${undeclared.slice(0, 3).join(', ')}${undeclared.length > 3 ? '…' : ''}. Declare ${undeclared.length === 1 ? 'it' : 'them'} on a surface.`,
680
- passLabel: 'All surface rule state paths are declared',
681
- tab: 'rules' as SurfacePanelTab
682
- },
683
- {
684
- area: 'unshared state in rules',
685
- weight: 1,
686
- severity: 'recommended' as const,
687
- passes: unsharedOnly.length === 0,
688
- message: `Surface rule reads state ${unsharedOnly.length === 1 ? 'path' : 'paths'} declared on another surface: ${unsharedOnly.slice(0, 3).join(', ')}${unsharedOnly.length > 3 ? '…' : ''}. Add this surface to that StateDefinition's sharedWith.`,
689
- passLabel: 'No unshared cross-surface reads in surface rules',
690
- tab: 'rules' as SurfacePanelTab
691
- }
692
- ];
693
- })()
694
- : [])
695
- ];
696
- };
697
-
698
- export const scoreSurface = (surface: Surface, allSurfaces?: readonly Surface[]): MaturityReport => {
699
- const knownPaths = allSurfaces !== undefined
700
- ? buildKnownPaths(String(surface.id), allSurfaces)
701
- : undefined;
702
- const allDeclaredPaths = allSurfaces !== undefined
703
- ? buildAllDeclaredPaths(allSurfaces)
704
- : undefined;
705
- // Evolution actions are dashed proposals, not committed behavior: scoring
706
- // their empty bodies would tank the surface. Exclude them from BOTH the
707
- // surface-level checks (so an evolution-only surface still asks for a real
708
- // action) and the per-action rollup.
709
- const committed = committedActions(surface.actions);
710
- const baseReport = reportFromChecks(
711
- 'surface',
712
- surface.name,
713
- { surfaceId: surface.id as unknown as string },
714
- surfaceChecks({ ...surface, actions: committed }, knownPaths, allDeclaredPaths)
715
- );
716
- const childrenReport = committed.reduce<MaturityReport>(
717
- (acc, c) => combineReports(acc, scoreCapability(surface, c, allSurfaces)),
718
- emptyReport()
719
- );
720
- return combineReports(baseReport, childrenReport);
721
- };
722
-
723
- const featureChecks = (feature: Feature): readonly Check[] => {
724
- const nameCheck: Check = {
725
- area: 'name',
726
- weight: 1,
727
- severity: 'critical',
728
- passes: feature.name.trim().length > 0,
729
- message: 'Give this feature a name.',
730
- passLabel: 'Has a name'
731
- };
732
- const surfacesCheck: Check = {
733
- area: 'surfaces',
734
- weight: 1,
735
- severity: 'critical',
736
- passes: feature.surfaces.length > 0,
737
- message: 'Add a surface - a place where users see and do things.',
738
- passLabel: `Has ${feature.surfaces.length} surface${
739
- feature.surfaces.length === 1 ? '' : 's'
740
- }`
741
- };
742
-
743
- // Without surfaces, the remaining feature-level checks would all pass
744
- // vacuously (no emitted events to register, no resources to reference, no
745
- // scenarios to assert against, ...) and the score would read ~90% for a
746
- // feature with nothing in it. Gate them behind the "has a surface"
747
- // prerequisite so the maturity card honestly says "0 / 1: add a surface"
748
- // on a fresh feature instead of misleading the user into thinking they're
749
- // nearly done.
750
- if (feature.surfaces.length === 0) {
751
- return [nameCheck, surfacesCheck];
752
- }
753
-
754
- const emittedEvents = emittedEventNames(feature);
755
- const registeredEvents = new Set((feature.events ?? []).map((event) => String(event.name)));
756
- const allEmittedEventsRegistered = emittedEvents.every((event) => registeredEvents.has(event));
757
- const registeredEventsHavePayloads = (feature.events ?? []).every(
758
- (event) =>
759
- (event.payloadSchema?.length ?? 0) === 0 ||
760
- event.payloadSchema?.every((field) => field.name.trim().length > 0)
761
- );
762
- const resourceReferences = hasResourceReferences(feature);
763
- const scenarios = feature.surfaces.flatMap((surface) =>
764
- surface.actions.flatMap((action) => action.scenarios ?? [])
765
- );
766
-
767
- return [
768
- nameCheck,
769
- {
770
- area: 'description',
771
- weight: 1,
772
- severity: 'recommended',
773
- passes: true,
774
- message: 'Add an feature description so agents and humans know the product boundary.',
775
- passLabel:
776
- (feature.description?.trim().length ?? 0) > 0
777
- ? 'Has a description'
778
- : 'Description optional'
779
- },
780
- surfacesCheck,
781
- {
782
- area: 'events registry',
783
- weight: 1,
784
- severity: 'recommended',
785
- passes:
786
- emittedEvents.length === 0 ||
787
- (feature.events ?? []).length === 0 ||
788
- allEmittedEventsRegistered,
789
- message:
790
- 'Register emitted events in the Events tab so payloads and producers are discoverable.',
791
- passLabel:
792
- emittedEvents.length === 0
793
- ? 'No emitted events to register'
794
- : (feature.events ?? []).length === 0
795
- ? 'Event registry optional until used'
796
- : 'Emitted events are registered'
797
- },
798
- {
799
- area: 'event payloads',
800
- weight: 1,
801
- severity: 'recommended',
802
- passes: registeredEventsHavePayloads,
803
- message: 'Event payload schema fields need names when payloads are declared.',
804
- passLabel:
805
- (feature.events ?? []).length === 0 ? 'No event payloads declared' : 'Payload schemas are valid'
806
- },
807
- {
808
- area: 'resources',
809
- weight: 1,
810
- severity: 'recommended',
811
- passes: !resourceReferences || feature.resources.length > 0,
812
- message: 'Parameters reference resources, but no Resources are declared at the feature level.',
813
- passLabel: resourceReferences ? 'Declares referenced resources' : 'No resource references'
814
- },
815
- {
816
- area: 'data',
817
- weight: 1,
818
- severity: 'recommended',
819
- passes: true,
820
- message:
821
- 'Promote repeated or domain-shaped state into Entity definitions so the model has a clear schema.',
822
- passLabel: feature.entities.length > 0 ? 'Has data definitions' : 'Entity definitions optional'
823
- },
824
- {
825
- area: 'personas',
826
- weight: 1,
827
- severity: 'recommended',
828
- passes: true,
829
- message: 'Add personas to exercise important roles, regions, plans, or auth states.',
830
- passLabel: feature.personas.length > 0 ? 'Has personas' : 'Personas optional'
831
- },
832
- {
833
- area: 'scenario assertions',
834
- weight: 1,
835
- severity: 'recommended',
836
- passes: scenarios.length === 0 || hasScenarioExpectations(feature) || scenarios.length > 0,
837
- message: 'Scenarios are more useful when they declare expectedStatus or expectedAssertions.',
838
- passLabel:
839
- scenarios.length === 0
840
- ? 'No scenarios yet'
841
- : hasScenarioExpectations(feature)
842
- ? 'Scenarios have expectations'
843
- : 'Scenario expectations optional'
844
- },
845
- {
846
- area: 'implementation context',
847
- weight: 1,
848
- severity: 'recommended',
849
- passes: true,
850
- message:
851
- 'Add devContext so MCP implementation audits can reflect the stack and coding conventions.',
852
- passLabel: feature.devContext ? 'Has implementation context' : 'Implementation context optional'
853
- }
854
- ];
855
- };
856
-
857
- /**
858
- * A feature with zero surfaces hasn't been built yet. Reporting anything
859
- * above 0% (the old behaviour gave ~50% when only the "has a name" check
860
- * passed) misled users into thinking they were halfway done. Force 0%
861
- * and clear the passedChecks list the only thing a user can act on at
862
- * this stage is "add a surface", which is already surfaced as the lone
863
- * critical issue from featureChecks's early-return path.
864
- */
865
- const emptyFeatureReport = (baseReport: MaturityReport): MaturityReport => ({
866
- ...baseReport,
867
- score: 0,
868
- percentage: 0,
869
- passedChecks: []
870
- });
871
-
872
- export const scoreFeature = (feature: Feature): MaturityReport => {
873
- const baseReport = reportFromChecks(
874
- 'feature',
875
- feature.name,
876
- {},
877
- featureChecks(feature)
878
- );
879
- if (feature.surfaces.length === 0) return emptyFeatureReport(baseReport);
880
- const childrenReport = feature.surfaces.reduce<MaturityReport>(
881
- (acc, s) => combineReports(acc, scoreSurface(s, feature.surfaces)),
882
- emptyReport()
883
- );
884
- return combineReports(baseReport, childrenReport);
885
- };
886
-
887
- /**
888
- * Per-action breakdown for a single surface. The aggregate is the surface
889
- * score (surface-level checks + every action), exposing both halves
890
- * separately so the UI can render the rollup action → surface explicitly.
891
- */
892
- export type SurfaceMaturityBreakdown = {
893
- readonly surface: Surface;
894
- readonly aggregate: MaturityReport;
895
- readonly surfaceLevel: MaturityReport;
896
- readonly perAction: readonly {
897
- readonly action: Action;
898
- readonly report: MaturityReport;
899
- }[];
900
- };
901
-
902
- export const scoreSurfaceBreakdown = (
903
- surface: Surface,
904
- allSurfaces?: readonly Surface[]
905
- ): SurfaceMaturityBreakdown => {
906
- const knownPaths =
907
- allSurfaces !== undefined ? buildKnownPaths(String(surface.id), allSurfaces) : undefined;
908
- const allDeclaredPaths =
909
- allSurfaces !== undefined ? buildAllDeclaredPaths(allSurfaces) : undefined;
910
- const committed = committedActions(surface.actions);
911
- const surfaceLevel = reportFromChecks(
912
- 'surface',
913
- surface.name,
914
- { surfaceId: surface.id as unknown as string },
915
- surfaceChecks({ ...surface, actions: committed }, knownPaths, allDeclaredPaths)
916
- );
917
- // Skip evolution proposals: the maturity breakdown lists committed actions
918
- // only, so a dashed placeholder never appears as a "0/14" red row.
919
- const perAction = committed.map((action) => ({
920
- action,
921
- report: scoreCapability(surface, action, allSurfaces)
922
- }));
923
- const aggregate = perAction.reduce<MaturityReport>(
924
- (acc, entry) => combineReports(acc, entry.report),
925
- surfaceLevel
926
- );
927
- return { surface, aggregate, surfaceLevel, perAction };
928
- };
929
-
930
- /**
931
- * Per-surface breakdown plus the global aggregate. Roll-up flow is
932
- * action → surface → feature: action checks form the leaves,
933
- * surface-level checks (state, invariants, presence of actions) sit
934
- * alongside, and the feature adds its own (name, has surfaces).
935
- */
936
- export type FeatureMaturityBreakdown = {
937
- readonly featureLevel: MaturityReport;
938
- readonly perSurface: readonly {
939
- readonly surface: Surface;
940
- /** Aggregate surface score = surfaceLevel + every action. */
941
- readonly report: MaturityReport;
942
- readonly surfaceLevel: MaturityReport;
943
- readonly perAction: readonly {
944
- readonly action: Action;
945
- readonly report: MaturityReport;
946
- }[];
947
- }[];
948
- readonly global: MaturityReport;
949
- };
950
-
951
- export const scoreFeatureBreakdown = (
952
- feature: Feature
953
- ): FeatureMaturityBreakdown => {
954
- const featureLevel = reportFromChecks(
955
- 'feature',
956
- feature.name,
957
- {},
958
- featureChecks(feature)
959
- );
960
- // Same empty-feature short-circuit as scoreFeature — keep the global
961
- // bar at 0% and drop "passed" noise when no surfaces exist. The
962
- // featureLevel report still carries the "add a surface" critical
963
- // issue so the user can act on it.
964
- if (feature.surfaces.length === 0) {
965
- return {
966
- featureLevel,
967
- perSurface: [],
968
- global: emptyFeatureReport(featureLevel)
969
- };
970
- }
971
- const perSurface = feature.surfaces.map((surface) => {
972
- const breakdown = scoreSurfaceBreakdown(surface, feature.surfaces);
973
- return {
974
- surface: breakdown.surface,
975
- report: breakdown.aggregate,
976
- surfaceLevel: breakdown.surfaceLevel,
977
- perAction: breakdown.perAction
978
- };
979
- });
980
- const global = perSurface.reduce<MaturityReport>(
981
- (acc, entry) => combineReports(acc, entry.report),
982
- featureLevel
983
- );
984
- return { featureLevel, perSurface, global };
985
- };
1
+ import type { Action } from '$features/behavior-model/domain/entities/Action';
2
+ import { committedActions } from '$features/behavior-model/domain/entities/Action';
3
+ import type { Feature } from '$features/behavior-model/domain/entities/Feature';
4
+ import type { Surface } from '$features/behavior-model/domain/entities/Surface';
5
+ import { isExpression, type Expression } from '$features/behavior-model/domain/value-objects/Expression';
6
+ import type { Effect } from '$features/behavior-model/domain/value-objects/Effect';
7
+ import {
8
+ flattenLeafConditions,
9
+ isParamLeft,
10
+ type RuleCondition
11
+ } from '$features/behavior-model/domain/value-objects/RuleCondition';
12
+ import type { StatePath } from '$features/behavior-model/domain/value-objects/StatePath';
13
+ import type {
14
+ CheckLocation,
15
+ MaturityIssue,
16
+ MaturityReport,
17
+ PassedCheck,
18
+ SurfacePanelTab
19
+ } from './MaturityReport';
20
+ import { combineReports, emptyReport } from './MaturityReport';
21
+
22
+ type Check = {
23
+ readonly area: string;
24
+ readonly weight: number;
25
+ readonly severity: 'critical' | 'recommended';
26
+ readonly passes: boolean;
27
+ readonly message: string;
28
+ /** Short label shown next to a passed check (the "good news" version). */
29
+ readonly passLabel: string;
30
+ readonly tab?: SurfacePanelTab;
31
+ };
32
+
33
+ const reportFromChecks = (
34
+ targetKind: MaturityIssue['targetKind'],
35
+ target: string,
36
+ baseLocation: CheckLocation,
37
+ checks: readonly Check[]
38
+ ): MaturityReport => {
39
+ let score = 0;
40
+ let maxScore = 0;
41
+ const criticalIssues: MaturityIssue[] = [];
42
+ const recommendedIssues: MaturityIssue[] = [];
43
+ const passedChecks: PassedCheck[] = [];
44
+ for (const check of checks) {
45
+ maxScore += check.weight;
46
+ const location: CheckLocation = {
47
+ ...baseLocation,
48
+ surfacePanelTab: check.tab ?? baseLocation.surfacePanelTab
49
+ };
50
+ if (check.passes) {
51
+ score += check.weight;
52
+ passedChecks.push({
53
+ ...location,
54
+ target,
55
+ targetKind,
56
+ area: check.area,
57
+ message: check.passLabel
58
+ });
59
+ continue;
60
+ }
61
+ const issue: MaturityIssue = {
62
+ ...location,
63
+ target,
64
+ targetKind,
65
+ area: check.area,
66
+ message: check.message,
67
+ severity: check.severity
68
+ };
69
+ if (check.severity === 'critical') {
70
+ criticalIssues.push(issue);
71
+ } else {
72
+ recommendedIssues.push(issue);
73
+ }
74
+ }
75
+ return {
76
+ score,
77
+ maxScore,
78
+ percentage: maxScore === 0 ? 100 : Math.round((score / maxScore) * 100),
79
+ criticalIssues,
80
+ recommendedIssues,
81
+ passedChecks
82
+ };
83
+ };
84
+
85
+ const expressionStateReads = (expression: Expression): StatePath[] => {
86
+ // Defensive: legacy snapshots can hold malformed children (e.g. `right: 1`
87
+ // instead of `{kind:"literal",value:1}`). The normalizer rewrites them on
88
+ // write/import, but we guard here too so the scorer can't crash on data
89
+ // that slipped past either boundary.
90
+ if (!isExpression(expression)) return [];
91
+ switch (expression.kind) {
92
+ case 'state':
93
+ return [expression.path];
94
+ case 'add':
95
+ case 'sub':
96
+ case 'mul':
97
+ case 'div':
98
+ case 'mod':
99
+ case 'min':
100
+ case 'max':
101
+ return [
102
+ ...expressionStateReads(expression.left),
103
+ ...expressionStateReads(expression.right)
104
+ ];
105
+ case 'neg':
106
+ case 'not':
107
+ case 'sum':
108
+ case 'count':
109
+ return expressionStateReads(expression.operand);
110
+ case 'sum_pluck':
111
+ return expressionStateReads(expression.operand);
112
+ case 'count_where':
113
+ return [
114
+ ...expressionStateReads(expression.operand),
115
+ ...expressionStateReads(expression.equals)
116
+ ];
117
+ case 'switch':
118
+ // For each case: flatten the `when` to leaf conditions, collect each
119
+ // leaf's `left` path and any state reads on its `right` Expression,
120
+ // then recurse into the case's `then` Expression. Plus the default.
121
+ return [
122
+ ...expression.cases.flatMap((c) => [
123
+ ...flattenLeafConditions(c.when).flatMap((leaf) => [
124
+ // Skip param-lefts: they reference action parameters, not
125
+ // surface state, so they don't count as a state-read here.
126
+ ...(isParamLeft(leaf.left) ? [] : [leaf.left as StatePath]),
127
+ ...(isExpression(leaf.right) ? expressionStateReads(leaf.right) : [])
128
+ ]),
129
+ ...expressionStateReads(c.then)
130
+ ]),
131
+ ...expressionStateReads(expression.default)
132
+ ];
133
+ case 'literal':
134
+ case 'param':
135
+ return [];
136
+ }
137
+ };
138
+
139
+ // Recurses into composite conditions and ignores the unconditional case
140
+ // (condition === undefined). Each leaf contributes its `.left` and any
141
+ // state references buried in an Expression on `.right`.
142
+ const conditionStateReads = (
143
+ condition: RuleCondition | undefined
144
+ ): StatePath[] => {
145
+ const out: StatePath[] = [];
146
+ for (const leaf of flattenLeafConditions(condition)) {
147
+ // Param-left conditions reference action parameters, not surface
148
+ // state; they don't contribute a state-path read here.
149
+ if (!isParamLeft(leaf.left)) out.push(leaf.left as StatePath);
150
+ if (isExpression(leaf.right)) out.push(...expressionStateReads(leaf.right));
151
+ }
152
+ return out;
153
+ };
154
+
155
+ // Every state path an action reads from OR writes to (conditions, set_state targets,
156
+ // expression operands, requiredStates). Used to validate paths against surface scope.
157
+ const allCapabilityStatePaths = (action: Action): readonly StatePath[] => {
158
+ const paths = new Set<StatePath>();
159
+ const addCondition = (c: RuleCondition | undefined) => {
160
+ for (const p of conditionStateReads(c)) paths.add(p);
161
+ };
162
+ const addEffect = (e: Effect) => {
163
+ if (e.type !== 'set_state') return;
164
+ paths.add(e.path);
165
+ if (isExpression(e.value)) for (const p of expressionStateReads(e.value)) paths.add(p);
166
+ };
167
+ for (const rule of action.rules) { addCondition(rule.condition); addEffect(rule.effect); }
168
+ for (const inv of action.invariants) addCondition(inv.condition);
169
+ for (const e of [...action.effects, ...(action.onBlockedEffects ?? [])]) addEffect(e);
170
+ for (const path of action.requiredStates) paths.add(path);
171
+ return [...paths];
172
+ };
173
+
174
+ const allSurfaceRuleStatePaths = (surface: Surface): readonly StatePath[] => {
175
+ const paths = new Set<StatePath>();
176
+ for (const rule of surface.rules) {
177
+ for (const p of conditionStateReads(rule.condition)) paths.add(p);
178
+ if (rule.effect.type === 'set_state') {
179
+ paths.add(rule.effect.path);
180
+ if (isExpression(rule.effect.value))
181
+ for (const p of expressionStateReads(rule.effect.value)) paths.add(p);
182
+ }
183
+ }
184
+ return [...paths];
185
+ };
186
+
187
+ // All state paths visible on a surface: own definitions + paths shared TO this surface
188
+ // from any other surface in the feature.
189
+ const buildKnownPaths = (surfaceId: string, allSurfaces: readonly Surface[]): ReadonlySet<string> => {
190
+ const paths = new Set<string>();
191
+ for (const surface of allSurfaces) {
192
+ for (const def of surface.stateDefinitions) {
193
+ const isOwn = String(surface.id) === surfaceId;
194
+ const isShared = (def.sharedWith ?? []).map(String).includes(surfaceId);
195
+ if (isOwn || isShared) paths.add(String(def.path));
196
+ }
197
+ }
198
+ return paths;
199
+ };
200
+
201
+ // Every state path declared anywhere in the feature, regardless of sharing.
202
+ // Used to distinguish "truly undeclared" (critical) from "declared but not
203
+ // shared with this surface" (recommended).
204
+ const buildAllDeclaredPaths = (allSurfaces: readonly Surface[]): ReadonlySet<string> => {
205
+ const paths = new Set<string>();
206
+ for (const surface of allSurfaces) {
207
+ for (const def of surface.stateDefinitions) {
208
+ paths.add(String(def.path));
209
+ }
210
+ }
211
+ return paths;
212
+ };
213
+
214
+ const effectStateReads = (effect: Effect): StatePath[] => {
215
+ if (effect.type !== 'set_state' || !isExpression(effect.value)) return [];
216
+ return expressionStateReads(effect.value);
217
+ };
218
+
219
+ const capabilityStateReads = (action: Action): StatePath[] => {
220
+ const paths = new Set<StatePath>();
221
+ for (const rule of action.rules) {
222
+ for (const path of conditionStateReads(rule.condition)) paths.add(path);
223
+ }
224
+ for (const invariant of action.invariants) {
225
+ for (const path of conditionStateReads(invariant.condition)) paths.add(path);
226
+ }
227
+ for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
228
+ for (const path of effectStateReads(effect)) paths.add(path);
229
+ }
230
+ return [...paths];
231
+ };
232
+
233
+ const effectUsesExpression = (effect: Effect): boolean =>
234
+ effect.type === 'set_state' && isExpression(effect.value);
235
+
236
+ const conditionUsesExpression = (c: RuleCondition | undefined): boolean =>
237
+ flattenLeafConditions(c).some((leaf) => isExpression(leaf.right));
238
+
239
+ const capabilityUsesExpression = (action: Action): boolean =>
240
+ action.rules.some((rule) => conditionUsesExpression(rule.condition)) ||
241
+ action.invariants.some((invariant) => conditionUsesExpression(invariant.condition)) ||
242
+ [...action.effects, ...(action.onBlockedEffects ?? [])].some(effectUsesExpression);
243
+
244
+ const surfaceTransitionTargets = (surface: Surface): readonly string[] => {
245
+ const targets = new Set<string>();
246
+ for (const action of surface.actions) {
247
+ for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
248
+ if (effect.type === 'transition_surface' && effect.target) targets.add(String(effect.target));
249
+ }
250
+ for (const rule of action.rules) {
251
+ if (rule.effect.type === 'transition_surface' && rule.effect.target) {
252
+ targets.add(String(rule.effect.target));
253
+ }
254
+ }
255
+ }
256
+ for (const rule of surface.rules) {
257
+ if (rule.effect.type === 'transition_surface' && rule.effect.target) {
258
+ targets.add(String(rule.effect.target));
259
+ }
260
+ }
261
+ return [...targets];
262
+ };
263
+
264
+ const emittedEventNames = (feature: Feature): readonly string[] => {
265
+ const names = new Set<string>();
266
+ for (const surface of feature.surfaces) {
267
+ for (const action of surface.actions) {
268
+ for (const event of action.emittedEvents) names.add(String(event));
269
+ for (const effect of [...action.effects, ...(action.onBlockedEffects ?? [])]) {
270
+ if (effect.type === 'emit_event') names.add(String(effect.event));
271
+ }
272
+ for (const rule of action.rules) {
273
+ if (rule.effect.type === 'emit_event') names.add(String(rule.effect.event));
274
+ }
275
+ }
276
+ for (const rule of surface.rules) {
277
+ if (rule.effect.type === 'emit_event') names.add(String(rule.effect.event));
278
+ }
279
+ }
280
+ return [...names];
281
+ };
282
+
283
+ const hasResourceReferences = (feature: Feature): boolean =>
284
+ feature.surfaces.some((surface) =>
285
+ surface.actions.some((action) =>
286
+ action.parameters.some((parameter) => parameter.resourceId !== undefined)
287
+ )
288
+ );
289
+
290
+ const hasScenarioExpectations = (feature: Feature): boolean =>
291
+ feature.surfaces.some((surface) =>
292
+ surface.actions.some((action) =>
293
+ (action.scenarios ?? []).some(
294
+ (scenario) =>
295
+ scenario.expectedStatus !== undefined ||
296
+ (scenario.expectedAssertions?.length ?? 0) > 0
297
+ )
298
+ )
299
+ );
300
+
301
+ const capabilityChecks = (
302
+ action: Action,
303
+ knownPaths?: ReadonlySet<string>,
304
+ allDeclaredPaths?: ReadonlySet<string>
305
+ ): readonly Check[] => {
306
+ const sensitiveCategories = new Set(['security', 'permissions', 'compliance']);
307
+ const hasPermissionRule = action.rules.some((r) => sensitiveCategories.has(r.category));
308
+ const dataMutating = [...action.effects, ...(action.onBlockedEffects ?? [])].some(
309
+ (e) => e.type === 'set_state'
310
+ );
311
+ const hasValidationRule = action.rules.some((r) => r.category === 'validation');
312
+ const hasParameterValidations = action.parameters.some(
313
+ (p) => p.validations !== undefined && p.validations.length > 0
314
+ );
315
+ const hasValidatableParam = action.parameters.some(
316
+ (p) =>
317
+ p.type === 'string' ||
318
+ p.type === 'number' ||
319
+ p.type === 'email' ||
320
+ p.type === 'url' ||
321
+ p.type === 'date' ||
322
+ p.type === 'time' ||
323
+ p.type === 'timestamp' ||
324
+ p.type === 'object' ||
325
+ p.type === 'array' ||
326
+ p.type === 'geolocation'
327
+ );
328
+ const hasInputs = action.parameters.length > 0;
329
+ const hasParameterDefaults = action.parameters.every(
330
+ (p) => p.required || p.defaultValue !== undefined
331
+ );
332
+ const enumParametersConfigured = action.parameters.every(
333
+ (p) => p.type !== 'enum' || (p.enumValues !== undefined && p.enumValues.length > 0)
334
+ );
335
+ const stateReads = capabilityStateReads(action);
336
+ const hasRequiredStateCoverage =
337
+ action.requiredStates.length === 0 ||
338
+ action.requiredStates.length > 0 ||
339
+ stateReads.length === 0;
340
+ const emitsEventsFromEffects = action.effects.some((e) => e.type === 'emit_event');
341
+ const hasBlockedFallback = (action.onBlockedEffects ?? []).length > 0;
342
+ const hasComplexBehavior =
343
+ action.rules.length > 1 ||
344
+ action.effects.length > 1 ||
345
+ action.invariants.length > 0 ||
346
+ hasInputs ||
347
+ dataMutating;
348
+ const scenarioCount = action.scenarios?.length ?? 0;
349
+ const hasScenarioCoverage = scenarioCount === 0 || !hasComplexBehavior || scenarioCount > 0;
350
+ const hasExpressionUse = capabilityUsesExpression(action);
351
+
352
+ return [
353
+ {
354
+ area: 'intent',
355
+ weight: 1,
356
+ severity: 'critical',
357
+ passes: action.intent.trim().length > 0,
358
+ message: 'Add a short description of what this action is for.',
359
+ passLabel: 'Has a description',
360
+ tab: 'actions'
361
+ },
362
+ {
363
+ area: 'rules',
364
+ weight: 1,
365
+ severity: 'recommended',
366
+ passes: action.rules.length > 0,
367
+ message: 'Add rules to control when this action can or cannot run.',
368
+ passLabel: `Has ${action.rules.length} rule${action.rules.length === 1 ? '' : 's'}`,
369
+ tab: 'actions'
370
+ },
371
+ {
372
+ area: 'effects',
373
+ weight: 1,
374
+ severity: 'critical',
375
+ passes: action.effects.length > 0 || action.rules.length > 0,
376
+ message: 'Nothing happens when this runs. Add an effect or a rule.',
377
+ passLabel: 'Has effects or rules',
378
+ tab: 'actions'
379
+ },
380
+ {
381
+ area: 'events',
382
+ weight: 1,
383
+ severity: 'recommended',
384
+ passes:
385
+ action.emittedEvents.length > 0 ||
386
+ emitsEventsFromEffects,
387
+ message:
388
+ 'Other parts of your feature cannot react when this runs. Make it emit and declare an event.',
389
+ passLabel:
390
+ action.emittedEvents.length > 0 && emitsEventsFromEffects
391
+ ? 'Emits and declares events'
392
+ : 'Has event signaling',
393
+ tab: 'actions'
394
+ },
395
+ {
396
+ area: 'required states',
397
+ weight: 1,
398
+ severity: 'recommended',
399
+ passes: hasRequiredStateCoverage,
400
+ message:
401
+ 'Declare requiredStates so readers and implementation audits know which state this action depends on.',
402
+ passLabel:
403
+ action.requiredStates.length === 0
404
+ ? 'Required states not declared'
405
+ : `Documents ${action.requiredStates.length} required state${
406
+ action.requiredStates.length === 1 ? '' : 's'
407
+ }`,
408
+ tab: 'actions'
409
+ },
410
+ {
411
+ area: 'event declarations',
412
+ weight: 1,
413
+ severity: 'recommended',
414
+ passes: !emitsEventsFromEffects || action.emittedEvents.length > 0,
415
+ message:
416
+ 'This action emits an event effect but does not declare emittedEvents for audit and discovery.',
417
+ passLabel: emitsEventsFromEffects ? 'Declares emitted events' : 'No emitted event effects',
418
+ tab: 'actions'
419
+ },
420
+ {
421
+ area: 'permissions',
422
+ weight: 1,
423
+ severity: dataMutating ? 'critical' : 'recommended',
424
+ passes: !dataMutating || hasPermissionRule,
425
+ message:
426
+ 'This action changes data. Add a rule that decides who is allowed to run it.',
427
+ passLabel: dataMutating
428
+ ? 'Has a permission rule'
429
+ : 'Does not change data. No permission rule needed',
430
+ tab: 'actions'
431
+ },
432
+ {
433
+ area: 'validation',
434
+ weight: 1,
435
+ severity: 'recommended',
436
+ passes:
437
+ action.parameters.length === 0 ||
438
+ !hasValidatableParam ||
439
+ hasValidationRule ||
440
+ hasParameterValidations,
441
+ message:
442
+ 'Inputs are not checked before this runs. Add a rule with category "validation" or attach validations to a parameter.',
443
+ passLabel:
444
+ action.parameters.length === 0
445
+ ? 'No inputs to validate'
446
+ : !hasValidatableParam
447
+ ? 'Inputs are booleans/enums. No validation needed'
448
+ : 'Validates its inputs',
449
+ tab: 'actions'
450
+ },
451
+ {
452
+ area: 'parameters',
453
+ weight: 1,
454
+ severity: 'recommended',
455
+ passes: !hasInputs || (hasParameterDefaults && enumParametersConfigured),
456
+ message:
457
+ 'Make parameter behavior explicit: optional parameters need defaults, and enum parameters need allowed values.',
458
+ passLabel: hasInputs ? 'Parameters have defaults/enum options' : 'No parameters',
459
+ tab: 'actions'
460
+ },
461
+ {
462
+ area: 'block handling',
463
+ weight: 1,
464
+ severity: 'recommended',
465
+ passes:
466
+ action.rules.length === 0 ||
467
+ hasBlockedFallback ||
468
+ action.rules.some((rule) => rule.effect.type !== 'allow_action'),
469
+ message:
470
+ 'Add on-block effects when a blocked run should show a message, emit an audit event, or navigate.',
471
+ passLabel: action.rules.length === 0 ? 'No block paths' : 'Has block fallback effects',
472
+ tab: 'actions'
473
+ },
474
+ {
475
+ area: 'scenarios',
476
+ weight: 1,
477
+ severity: 'recommended',
478
+ passes: hasScenarioCoverage,
479
+ message:
480
+ 'Add at least one scenario for this non-trivial action so examples become regression checks.',
481
+ passLabel: hasComplexBehavior
482
+ ? `Has ${scenarioCount} scenario${scenarioCount === 1 ? '' : 's'}`
483
+ : 'Scenarios optional until added',
484
+ tab: 'actions'
485
+ },
486
+ {
487
+ area: 'expressions',
488
+ weight: 1,
489
+ severity: 'recommended',
490
+ passes: !hasExpressionUse || action.rules.length > 0 || action.effects.length > 0,
491
+ message:
492
+ 'Expression values should live in explicit rules or effects so readers can inspect the calculation.',
493
+ passLabel: hasExpressionUse ? 'Uses expression values' : 'No expression values needed',
494
+ tab: 'actions'
495
+ },
496
+ ...(knownPaths !== undefined
497
+ ? (() => {
498
+ const referenced = allCapabilityStatePaths(action).map(String);
499
+ const undeclared = referenced.filter((p) => !allDeclaredPaths!.has(p));
500
+ const unsharedOnly = referenced.filter(
501
+ (p) => allDeclaredPaths!.has(p) && !knownPaths.has(p)
502
+ );
503
+ return [
504
+ // Truly undefined paths (declared nowhere in the feature) are a
505
+ // real spec gap: the simulator returns undefined, comparisons
506
+ // misbehave. Keep this critical.
507
+ {
508
+ area: 'undefined state',
509
+ weight: 1,
510
+ severity: 'critical' as const,
511
+ passes: undeclared.length === 0,
512
+ message: `Uses state ${undeclared.length === 1 ? 'path' : 'paths'} not declared anywhere in this feature: ${undeclared.slice(0, 3).join(', ')}${undeclared.length > 3 ? '…' : ''}. Declare ${undeclared.length === 1 ? 'it' : 'them'} on a surface.`,
513
+ passLabel: 'All referenced state paths are declared',
514
+ tab: 'actions' as SurfacePanelTab
515
+ },
516
+ // Paths declared on a SIBLING surface without sharedWith are not a
517
+ // runtime bug (the simulator reads from a unified snapshot) but
518
+ // hide a cross-surface dependency. Downgraded from critical to
519
+ // recommended so LLMs aren't blocked by sharedWith hygiene.
520
+ {
521
+ area: 'unshared state',
522
+ weight: 1,
523
+ severity: 'recommended' as const,
524
+ passes: unsharedOnly.length === 0,
525
+ message: `Reads state ${unsharedOnly.length === 1 ? 'path' : 'paths'} declared on another surface: ${unsharedOnly.slice(0, 3).join(', ')}${unsharedOnly.length > 3 ? '…' : ''}. Add this surface to that StateDefinition's sharedWith to make the dependency explicit.`,
526
+ passLabel: 'No unshared cross-surface reads',
527
+ tab: 'actions' as SurfacePanelTab
528
+ }
529
+ ];
530
+ })()
531
+ : [])
532
+ ];
533
+ };
534
+
535
+ export const scoreCapability = (
536
+ surface: Surface,
537
+ action: Action,
538
+ allSurfaces?: readonly Surface[]
539
+ ): MaturityReport => {
540
+ const knownPaths =
541
+ allSurfaces !== undefined ? buildKnownPaths(String(surface.id), allSurfaces) : undefined;
542
+ const allDeclaredPaths =
543
+ allSurfaces !== undefined ? buildAllDeclaredPaths(allSurfaces) : undefined;
544
+ return reportFromChecks(
545
+ 'action',
546
+ action.name,
547
+ {
548
+ surfaceId: surface.id as unknown as string,
549
+ actionId: action.id as unknown as string,
550
+ surfacePanelTab: 'actions'
551
+ },
552
+ capabilityChecks(action, knownPaths, allDeclaredPaths)
553
+ );
554
+ };
555
+
556
+ const surfaceChecks = (
557
+ surface: Surface,
558
+ knownPaths?: ReadonlySet<string>,
559
+ allDeclaredPaths?: ReadonlySet<string>
560
+ ): readonly Check[] => {
561
+ const transitionTargets = surfaceTransitionTargets(surface);
562
+ const hasSharedState = surface.stateDefinitions.some(
563
+ (definition) => definition.sharedWith !== undefined && definition.sharedWith.length > 0
564
+ );
565
+ const hasEnumStateWithoutValues = surface.stateDefinitions.some(
566
+ (definition) =>
567
+ definition.type === 'enum' &&
568
+ (definition.enumValues === undefined || definition.enumValues.length === 0)
569
+ );
570
+ const hasSurfaceWideRules =
571
+ surface.actions.length <= 1 ||
572
+ surface.rules.length > 0 ||
573
+ surface.actions.every((action) => action.rules.length > 0);
574
+
575
+ return [
576
+ {
577
+ area: 'actions',
578
+ weight: 1,
579
+ severity: 'critical',
580
+ passes: surface.actions.length > 0,
581
+ message: 'Add at least one thing the user can do here.',
582
+ passLabel: `Has ${surface.actions.length} capabilit${
583
+ surface.actions.length === 1 ? 'y' : 'ies'
584
+ }`,
585
+ tab: 'actions'
586
+ },
587
+ {
588
+ area: 'states',
589
+ weight: 1,
590
+ severity: 'recommended',
591
+ // Count shared-in state as "has state". A surface that legitimately
592
+ // operates on state shared from another surface (e.g. a modal that
593
+ // reads/writes the parent screen's payment.* paths) shouldn't be
594
+ // flagged for "no state" just because it doesn't own any natively.
595
+ passes:
596
+ surface.stateDefinitions.length > 0 ||
597
+ (knownPaths !== undefined && knownPaths.size > 0),
598
+ message: 'Add some state - the data your rules read to decide what happens.',
599
+ passLabel: `Has ${surface.stateDefinitions.length} state value${
600
+ surface.stateDefinitions.length === 1 ? '' : 's'
601
+ }`,
602
+ tab: 'state'
603
+ },
604
+ {
605
+ area: 'state metadata',
606
+ weight: 1,
607
+ severity: 'recommended',
608
+ passes:
609
+ surface.stateDefinitions.length === 0 ||
610
+ !hasEnumStateWithoutValues,
611
+ message:
612
+ 'Document state definitions with descriptions, and give enum states their allowed values.',
613
+ passLabel:
614
+ surface.stateDefinitions.length === 0
615
+ ? 'No state to document'
616
+ : 'State definitions are documented',
617
+ tab: 'state'
618
+ },
619
+ {
620
+ area: 'shared state',
621
+ weight: 1,
622
+ severity: 'recommended',
623
+ passes:
624
+ surface.stateDefinitions.length <= 1 ||
625
+ hasSharedState ||
626
+ surface.stateDefinitions.every((definition) => definition.sharedWith === undefined),
627
+ message:
628
+ 'Shared state is declared but incomplete. Check sharedWith links on state definitions.',
629
+ passLabel: hasSharedState ? 'Declares shared state' : 'No obvious shared state',
630
+ tab: 'state'
631
+ },
632
+ {
633
+ area: 'surface rules',
634
+ weight: 1,
635
+ severity: 'recommended',
636
+ passes: hasSurfaceWideRules,
637
+ message:
638
+ 'This surface has multiple actions and no visible gating. Add surface rules or action rules.',
639
+ passLabel:
640
+ surface.actions.length <= 1 ? 'No cross-cutting gate needed' : 'Has surface rules',
641
+ tab: 'rules'
642
+ },
643
+ {
644
+ area: 'invariants',
645
+ weight: 1,
646
+ severity: 'recommended',
647
+ passes:
648
+ surface.invariants.length > 0 ||
649
+ surface.actions.some((c) => c.invariants.length > 0),
650
+ message:
651
+ 'Add an invariant - a fact that should always be true here, so broken states are caught early.',
652
+ passLabel: 'Has invariants',
653
+ tab: 'invariants'
654
+ },
655
+ {
656
+ area: 'transitions',
657
+ weight: 1,
658
+ severity: 'recommended',
659
+ passes: transitionTargets.length === 0 || surface.transitions.length > 0,
660
+ message:
661
+ 'This surface navigates through effects. Declare transitions so the graph is visible outside individual actions.',
662
+ passLabel:
663
+ transitionTargets.length === 0 ? 'No navigation effects' : 'Declares surface transitions',
664
+ tab: 'actions'
665
+ },
666
+ ...(knownPaths !== undefined
667
+ ? (() => {
668
+ const referenced = allSurfaceRuleStatePaths(surface).map(String);
669
+ const undeclared = referenced.filter((p) => !allDeclaredPaths!.has(p));
670
+ const unsharedOnly = referenced.filter(
671
+ (p) => allDeclaredPaths!.has(p) && !knownPaths.has(p)
672
+ );
673
+ return [
674
+ {
675
+ area: 'undefined state in rules',
676
+ weight: 1,
677
+ severity: 'critical' as const,
678
+ passes: undeclared.length === 0,
679
+ message: `Surface rule uses state ${undeclared.length === 1 ? 'path' : 'paths'} not declared anywhere in this feature: ${undeclared.slice(0, 3).join(', ')}${undeclared.length > 3 ? '…' : ''}. Declare ${undeclared.length === 1 ? 'it' : 'them'} on a surface.`,
680
+ passLabel: 'All surface rule state paths are declared',
681
+ tab: 'rules' as SurfacePanelTab
682
+ },
683
+ {
684
+ area: 'unshared state in rules',
685
+ weight: 1,
686
+ severity: 'recommended' as const,
687
+ passes: unsharedOnly.length === 0,
688
+ message: `Surface rule reads state ${unsharedOnly.length === 1 ? 'path' : 'paths'} declared on another surface: ${unsharedOnly.slice(0, 3).join(', ')}${unsharedOnly.length > 3 ? '…' : ''}. Add this surface to that StateDefinition's sharedWith.`,
689
+ passLabel: 'No unshared cross-surface reads in surface rules',
690
+ tab: 'rules' as SurfacePanelTab
691
+ }
692
+ ];
693
+ })()
694
+ : [])
695
+ ];
696
+ };
697
+
698
+ export const scoreSurface = (surface: Surface, allSurfaces?: readonly Surface[]): MaturityReport => {
699
+ const knownPaths = allSurfaces !== undefined
700
+ ? buildKnownPaths(String(surface.id), allSurfaces)
701
+ : undefined;
702
+ const allDeclaredPaths = allSurfaces !== undefined
703
+ ? buildAllDeclaredPaths(allSurfaces)
704
+ : undefined;
705
+ // Evolution actions are dashed proposals, not committed behavior: scoring
706
+ // their empty bodies would tank the surface. Exclude them from BOTH the
707
+ // surface-level checks (so an evolution-only surface still asks for a real
708
+ // action) and the per-action rollup.
709
+ const committed = committedActions(surface.actions);
710
+ const baseReport = reportFromChecks(
711
+ 'surface',
712
+ surface.name,
713
+ { surfaceId: surface.id as unknown as string },
714
+ surfaceChecks({ ...surface, actions: committed }, knownPaths, allDeclaredPaths)
715
+ );
716
+ const childrenReport = committed.reduce<MaturityReport>(
717
+ (acc, c) => combineReports(acc, scoreCapability(surface, c, allSurfaces)),
718
+ emptyReport()
719
+ );
720
+ return combineReports(baseReport, childrenReport);
721
+ };
722
+
723
+ const featureChecks = (feature: Feature): readonly Check[] => {
724
+ const nameCheck: Check = {
725
+ area: 'name',
726
+ weight: 1,
727
+ severity: 'critical',
728
+ passes: feature.name.trim().length > 0,
729
+ message: 'Give this feature a name.',
730
+ passLabel: 'Has a name'
731
+ };
732
+ const surfacesCheck: Check = {
733
+ area: 'surfaces',
734
+ weight: 1,
735
+ severity: 'critical',
736
+ passes: feature.surfaces.length > 0,
737
+ message: 'Add a surface - a place where users see and do things.',
738
+ passLabel: `Has ${feature.surfaces.length} surface${
739
+ feature.surfaces.length === 1 ? '' : 's'
740
+ }`
741
+ };
742
+
743
+ // Without surfaces, the remaining feature-level checks would all pass
744
+ // vacuously (no emitted events to register, no resources to reference, no
745
+ // scenarios to assert against, ...) and the score would read ~90% for a
746
+ // feature with nothing in it. Gate them behind the "has a surface"
747
+ // prerequisite so the maturity card honestly says "0 / 1: add a surface"
748
+ // on a fresh feature instead of misleading the user into thinking they're
749
+ // nearly done.
750
+ if (feature.surfaces.length === 0) {
751
+ return [nameCheck, surfacesCheck];
752
+ }
753
+
754
+ const emittedEvents = emittedEventNames(feature);
755
+ const registeredEvents = new Set((feature.events ?? []).map((event) => String(event.name)));
756
+ const allEmittedEventsRegistered = emittedEvents.every((event) => registeredEvents.has(event));
757
+ const registeredEventsHavePayloads = (feature.events ?? []).every(
758
+ (event) =>
759
+ (event.payloadSchema?.length ?? 0) === 0 ||
760
+ event.payloadSchema?.every((field) => field.name.trim().length > 0)
761
+ );
762
+ const resourceReferences = hasResourceReferences(feature);
763
+ const scenarios = feature.surfaces.flatMap((surface) =>
764
+ surface.actions.flatMap((action) => action.scenarios ?? [])
765
+ );
766
+
767
+ return [
768
+ nameCheck,
769
+ {
770
+ area: 'description',
771
+ weight: 1,
772
+ severity: 'recommended',
773
+ passes: true,
774
+ message: 'Add an feature description so agents and humans know the product boundary.',
775
+ passLabel:
776
+ (feature.description?.trim().length ?? 0) > 0
777
+ ? 'Has a description'
778
+ : 'Description optional'
779
+ },
780
+ surfacesCheck,
781
+ {
782
+ area: 'events registry',
783
+ weight: 1,
784
+ severity: 'recommended',
785
+ passes:
786
+ emittedEvents.length === 0 ||
787
+ (feature.events ?? []).length === 0 ||
788
+ allEmittedEventsRegistered,
789
+ message:
790
+ 'Register emitted events in the Events tab so payloads and producers are discoverable.',
791
+ passLabel:
792
+ emittedEvents.length === 0
793
+ ? 'No emitted events to register'
794
+ : (feature.events ?? []).length === 0
795
+ ? 'Event registry optional until used'
796
+ : 'Emitted events are registered'
797
+ },
798
+ {
799
+ area: 'event payloads',
800
+ weight: 1,
801
+ severity: 'recommended',
802
+ passes: registeredEventsHavePayloads,
803
+ message: 'Event payload schema fields need names when payloads are declared.',
804
+ passLabel:
805
+ (feature.events ?? []).length === 0 ? 'No event payloads declared' : 'Payload schemas are valid'
806
+ },
807
+ {
808
+ area: 'resources',
809
+ weight: 1,
810
+ severity: 'recommended',
811
+ passes: !resourceReferences || feature.resources.length > 0,
812
+ message: 'Parameters reference resources, but no Resources are declared at the feature level.',
813
+ passLabel: resourceReferences ? 'Declares referenced resources' : 'No resource references'
814
+ },
815
+ {
816
+ area: 'data',
817
+ weight: 1,
818
+ severity: 'recommended',
819
+ passes: true,
820
+ message:
821
+ 'Promote repeated or domain-shaped state into Entity definitions so the model has a clear schema.',
822
+ passLabel: feature.entities.length > 0 ? 'Has data definitions' : 'Entity definitions optional'
823
+ },
824
+ {
825
+ area: 'personas',
826
+ weight: 1,
827
+ severity: 'recommended',
828
+ passes: true,
829
+ message: 'Add personas to exercise important roles, regions, plans, or auth states.',
830
+ passLabel: feature.personas.length > 0 ? 'Has personas' : 'Personas optional'
831
+ },
832
+ {
833
+ area: 'scenario assertions',
834
+ weight: 1,
835
+ severity: 'recommended',
836
+ passes: scenarios.length === 0 || hasScenarioExpectations(feature) || scenarios.length > 0,
837
+ message: 'Scenarios are more useful when they declare expectedStatus or expectedAssertions.',
838
+ passLabel:
839
+ scenarios.length === 0
840
+ ? 'No scenarios yet'
841
+ : hasScenarioExpectations(feature)
842
+ ? 'Scenarios have expectations'
843
+ : 'Scenario expectations optional'
844
+ },
845
+ {
846
+ area: 'implementation context',
847
+ weight: 1,
848
+ severity: 'recommended',
849
+ passes: true,
850
+ message:
851
+ 'Add devContext so MCP implementation audits can reflect the stack and coding conventions.',
852
+ passLabel: feature.devContext ? 'Has implementation context' : 'Implementation context optional'
853
+ },
854
+ {
855
+ area: 'liveness',
856
+ weight: 1,
857
+ severity: 'recommended',
858
+ // Liveness goals are optional (not every feature has a "this must stay
859
+ // reachable" property), so this never fails it credits the modeler for
860
+ // declaring goals the model checker can verify, the complement to the
861
+ // invariant (safety) coverage the surface/action checks already reward.
862
+ passes: true,
863
+ message:
864
+ 'Add reachability goals so the model checker can prove key states stay reachable (liveness), not just that invariants hold (safety).',
865
+ passLabel:
866
+ (feature.reachabilityGoals?.length ?? 0) > 0
867
+ ? `Declares ${feature.reachabilityGoals!.length} liveness goal${feature.reachabilityGoals!.length === 1 ? '' : 's'}`
868
+ : 'Liveness goals optional'
869
+ }
870
+ ];
871
+ };
872
+
873
+ /**
874
+ * A feature with zero surfaces hasn't been built yet. Reporting anything
875
+ * above 0% (the old behaviour gave ~50% when only the "has a name" check
876
+ * passed) misled users into thinking they were halfway done. Force 0%
877
+ * and clear the passedChecks list — the only thing a user can act on at
878
+ * this stage is "add a surface", which is already surfaced as the lone
879
+ * critical issue from featureChecks's early-return path.
880
+ */
881
+ const emptyFeatureReport = (baseReport: MaturityReport): MaturityReport => ({
882
+ ...baseReport,
883
+ score: 0,
884
+ percentage: 0,
885
+ passedChecks: []
886
+ });
887
+
888
+ export const scoreFeature = (feature: Feature): MaturityReport => {
889
+ const baseReport = reportFromChecks(
890
+ 'feature',
891
+ feature.name,
892
+ {},
893
+ featureChecks(feature)
894
+ );
895
+ if (feature.surfaces.length === 0) return emptyFeatureReport(baseReport);
896
+ const childrenReport = feature.surfaces.reduce<MaturityReport>(
897
+ (acc, s) => combineReports(acc, scoreSurface(s, feature.surfaces)),
898
+ emptyReport()
899
+ );
900
+ return combineReports(baseReport, childrenReport);
901
+ };
902
+
903
+ /**
904
+ * Per-action breakdown for a single surface. The aggregate is the surface
905
+ * score (surface-level checks + every action), exposing both halves
906
+ * separately so the UI can render the rollup action → surface explicitly.
907
+ */
908
+ export type SurfaceMaturityBreakdown = {
909
+ readonly surface: Surface;
910
+ readonly aggregate: MaturityReport;
911
+ readonly surfaceLevel: MaturityReport;
912
+ readonly perAction: readonly {
913
+ readonly action: Action;
914
+ readonly report: MaturityReport;
915
+ }[];
916
+ };
917
+
918
+ export const scoreSurfaceBreakdown = (
919
+ surface: Surface,
920
+ allSurfaces?: readonly Surface[]
921
+ ): SurfaceMaturityBreakdown => {
922
+ const knownPaths =
923
+ allSurfaces !== undefined ? buildKnownPaths(String(surface.id), allSurfaces) : undefined;
924
+ const allDeclaredPaths =
925
+ allSurfaces !== undefined ? buildAllDeclaredPaths(allSurfaces) : undefined;
926
+ const committed = committedActions(surface.actions);
927
+ const surfaceLevel = reportFromChecks(
928
+ 'surface',
929
+ surface.name,
930
+ { surfaceId: surface.id as unknown as string },
931
+ surfaceChecks({ ...surface, actions: committed }, knownPaths, allDeclaredPaths)
932
+ );
933
+ // Skip evolution proposals: the maturity breakdown lists committed actions
934
+ // only, so a dashed placeholder never appears as a "0/14" red row.
935
+ const perAction = committed.map((action) => ({
936
+ action,
937
+ report: scoreCapability(surface, action, allSurfaces)
938
+ }));
939
+ const aggregate = perAction.reduce<MaturityReport>(
940
+ (acc, entry) => combineReports(acc, entry.report),
941
+ surfaceLevel
942
+ );
943
+ return { surface, aggregate, surfaceLevel, perAction };
944
+ };
945
+
946
+ /**
947
+ * Per-surface breakdown plus the global aggregate. Roll-up flow is
948
+ * action → surface → feature: action checks form the leaves,
949
+ * surface-level checks (state, invariants, presence of actions) sit
950
+ * alongside, and the feature adds its own (name, has surfaces).
951
+ */
952
+ export type FeatureMaturityBreakdown = {
953
+ readonly featureLevel: MaturityReport;
954
+ readonly perSurface: readonly {
955
+ readonly surface: Surface;
956
+ /** Aggregate surface score = surfaceLevel + every action. */
957
+ readonly report: MaturityReport;
958
+ readonly surfaceLevel: MaturityReport;
959
+ readonly perAction: readonly {
960
+ readonly action: Action;
961
+ readonly report: MaturityReport;
962
+ }[];
963
+ }[];
964
+ readonly global: MaturityReport;
965
+ };
966
+
967
+ export const scoreFeatureBreakdown = (
968
+ feature: Feature
969
+ ): FeatureMaturityBreakdown => {
970
+ const featureLevel = reportFromChecks(
971
+ 'feature',
972
+ feature.name,
973
+ {},
974
+ featureChecks(feature)
975
+ );
976
+ // Same empty-feature short-circuit as scoreFeature — keep the global
977
+ // bar at 0% and drop "passed" noise when no surfaces exist. The
978
+ // featureLevel report still carries the "add a surface" critical
979
+ // issue so the user can act on it.
980
+ if (feature.surfaces.length === 0) {
981
+ return {
982
+ featureLevel,
983
+ perSurface: [],
984
+ global: emptyFeatureReport(featureLevel)
985
+ };
986
+ }
987
+ const perSurface = feature.surfaces.map((surface) => {
988
+ const breakdown = scoreSurfaceBreakdown(surface, feature.surfaces);
989
+ return {
990
+ surface: breakdown.surface,
991
+ report: breakdown.aggregate,
992
+ surfaceLevel: breakdown.surfaceLevel,
993
+ perAction: breakdown.perAction
994
+ };
995
+ });
996
+ const global = perSurface.reduce<MaturityReport>(
997
+ (acc, entry) => combineReports(acc, entry.report),
998
+ featureLevel
999
+ );
1000
+ return { featureLevel, perSurface, global };
1001
+ };