@mmapp/react-compiler 0.1.0-alpha.1 → 0.1.0-alpha.4

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 (346) hide show
  1. package/ATOM-PIPELINE.md +144 -0
  2. package/README.md +88 -40
  3. package/dist/auth-3UK75242.mjs +17 -0
  4. package/dist/babel/index.d.mts +2 -2
  5. package/dist/babel/index.d.ts +2 -2
  6. package/dist/babel/index.js +2816 -279
  7. package/dist/babel/index.mjs +2 -2
  8. package/dist/chunk-3USIFFE4.mjs +2190 -0
  9. package/dist/chunk-45YMGEVT.mjs +186 -0
  10. package/dist/chunk-4FN2AISW.mjs +148 -0
  11. package/dist/chunk-4OPI5L7G.mjs +2593 -0
  12. package/dist/chunk-4RYTKOOJ.mjs +186 -0
  13. package/dist/chunk-52XHYD2V.mjs +214 -0
  14. package/dist/chunk-5FTDWKHH.mjs +244 -0
  15. package/dist/chunk-5GUFFFGL.mjs +148 -0
  16. package/dist/chunk-5RKTOVR5.mjs +244 -0
  17. package/dist/chunk-5YDMOO4X.mjs +214 -0
  18. package/dist/chunk-64ZWEMLJ.mjs +148 -0
  19. package/dist/chunk-6XP4KSWQ.mjs +2190 -0
  20. package/dist/chunk-72QWL54I.mjs +175 -0
  21. package/dist/chunk-7B4TRI7C.mjs +4835 -0
  22. package/dist/chunk-7JRAEFRB.mjs +7510 -0
  23. package/dist/chunk-7T6Q5KAA.mjs +7506 -0
  24. package/dist/chunk-7ZKGHTNB.mjs +4952 -0
  25. package/dist/chunk-ABYPKRSB.mjs +215 -0
  26. package/dist/chunk-BZEXUPDH.mjs +175 -0
  27. package/dist/chunk-CIESM3BP.mjs +33 -0
  28. package/dist/chunk-DE3ZGQAC.mjs +148 -0
  29. package/dist/chunk-DMCY3BBG.mjs +1933 -0
  30. package/dist/chunk-DPIK3PJS.mjs +244 -0
  31. package/dist/chunk-E5IVH4RE.mjs +186 -0
  32. package/dist/chunk-E6FZNUR5.mjs +4953 -0
  33. package/dist/chunk-EJRBDQDP.mjs +2607 -0
  34. package/dist/chunk-ELO4TXJL.mjs +186 -0
  35. package/dist/chunk-EO6SYNCG.mjs +175 -0
  36. package/dist/chunk-FKRO52XH.mjs +3446 -0
  37. package/dist/chunk-FL4YAKU6.mjs +4941 -0
  38. package/dist/chunk-FYT47UBU.mjs +5076 -0
  39. package/dist/chunk-GCLGPOJZ.mjs +148 -0
  40. package/dist/chunk-GXB4JOP7.mjs +5072 -0
  41. package/dist/chunk-HFXOUMTD.mjs +175 -0
  42. package/dist/chunk-HRYR54PT.mjs +175 -0
  43. package/dist/chunk-HWIZ47US.mjs +214 -0
  44. package/dist/chunk-IB7MNPQL.mjs +4953 -0
  45. package/dist/chunk-ICSIHQCG.mjs +148 -0
  46. package/dist/chunk-J3M4GUS7.mjs +161 -0
  47. package/dist/chunk-J7JUAHS4.mjs +186 -0
  48. package/dist/chunk-JLA5VNQ3.mjs +186 -0
  49. package/dist/chunk-JQLWFCTM.mjs +214 -0
  50. package/dist/chunk-JRGFBWTN.mjs +2918 -0
  51. package/dist/chunk-KFJJCQAL.mjs +148 -0
  52. package/dist/chunk-KJUIIEQE.mjs +186 -0
  53. package/dist/chunk-KNWTHRVQ.mjs +175 -0
  54. package/dist/chunk-KSG4XSZF.mjs +175 -0
  55. package/dist/chunk-LF5N6DOU.mjs +175 -0
  56. package/dist/chunk-LJQCM2IM.mjs +214 -0
  57. package/dist/chunk-NTB7OEX2.mjs +2918 -0
  58. package/dist/chunk-NW6555WJ.mjs +186 -0
  59. package/dist/chunk-O4AUS7EU.mjs +148 -0
  60. package/dist/chunk-OMZE6VLQ.mjs +214 -0
  61. package/dist/chunk-OPJKP747.mjs +7506 -0
  62. package/dist/chunk-P4BR7WVO.mjs +2190 -0
  63. package/dist/chunk-QQHVYH2X.mjs +244 -0
  64. package/dist/chunk-R2DD5GTY.mjs +186 -0
  65. package/dist/chunk-S5QLWLLT.mjs +186 -0
  66. package/dist/chunk-SCWGT2FY.mjs +2190 -0
  67. package/dist/chunk-SMKJUSB3.mjs +2190 -0
  68. package/dist/chunk-THFYE5ZX.mjs +244 -0
  69. package/dist/chunk-UDDTWG5J.mjs +734 -0
  70. package/dist/chunk-VCAY2KGM.mjs +175 -0
  71. package/dist/chunk-VLTKQDJ3.mjs +244 -0
  72. package/dist/chunk-WBYMW4NQ.mjs +3450 -0
  73. package/dist/chunk-WECAV6QB.mjs +148 -0
  74. package/dist/chunk-WMKBXUCE.mjs +3228 -0
  75. package/dist/chunk-WVYY32LD.mjs +939 -0
  76. package/dist/chunk-XAJ5BKKL.mjs +4947 -0
  77. package/dist/chunk-XDVM4YHX.mjs +3450 -0
  78. package/dist/chunk-XG2X7AEA.mjs +175 -0
  79. package/dist/chunk-XG7Z23NQ.mjs +148 -0
  80. package/dist/chunk-XWZAOCQ7.mjs +2607 -0
  81. package/dist/chunk-Y6MA7ULW.mjs +148 -0
  82. package/dist/chunk-YMS7Q7LG.mjs +214 -0
  83. package/dist/chunk-Z2G5RZ4H.mjs +186 -0
  84. package/dist/chunk-ZA37XTGA.mjs +175 -0
  85. package/dist/chunk-ZE3KCHBM.mjs +2918 -0
  86. package/dist/cli/index.js +14720 -7199
  87. package/dist/cli/index.mjs +224 -183
  88. package/dist/codemod/cli.js +1 -1
  89. package/dist/codemod/cli.mjs +2 -2
  90. package/dist/codemod/index.d.mts +3 -3
  91. package/dist/codemod/index.d.ts +3 -3
  92. package/dist/codemod/index.js +1 -1
  93. package/dist/codemod/index.mjs +2 -2
  94. package/dist/config-PL24KEWL.mjs +219 -0
  95. package/dist/deploy-YAJGW6II.mjs +9 -0
  96. package/dist/dev-server-CrQ041KP.d.mts +79 -0
  97. package/dist/dev-server-CrQ041KP.d.ts +79 -0
  98. package/dist/dev-server-RmGHIntF.d.mts +113 -0
  99. package/dist/dev-server-RmGHIntF.d.ts +113 -0
  100. package/dist/dev-server.d.mts +2 -2
  101. package/dist/dev-server.d.ts +2 -2
  102. package/dist/dev-server.js +6424 -1597
  103. package/dist/dev-server.mjs +5 -5
  104. package/dist/envelope-ChEkuHij.d.mts +265 -0
  105. package/dist/envelope-ChEkuHij.d.ts +265 -0
  106. package/dist/envelope.d.mts +2 -2
  107. package/dist/envelope.d.ts +2 -2
  108. package/dist/envelope.js +2814 -277
  109. package/dist/envelope.mjs +3 -3
  110. package/dist/index-CEKyyazf.d.mts +104 -0
  111. package/dist/index-CEKyyazf.d.ts +104 -0
  112. package/dist/index.d.mts +168 -9
  113. package/dist/index.d.ts +168 -9
  114. package/dist/index.js +5606 -681
  115. package/dist/index.mjs +217 -9
  116. package/dist/init-7FJENUDK.mjs +407 -0
  117. package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
  118. package/dist/init-DQDX3QK6.mjs +369 -0
  119. package/dist/init-EHO4VQ22.mjs +369 -0
  120. package/dist/init-UC3FWPIW.mjs +367 -0
  121. package/dist/init-UNSMVKIK.mjs +366 -0
  122. package/dist/init-UNV5XIDE.mjs +367 -0
  123. package/dist/project-compiler-2P4N4DR7.mjs +10 -0
  124. package/dist/project-compiler-D2LCC27O.mjs +10 -0
  125. package/dist/project-compiler-EJ3GANJE.mjs +10 -0
  126. package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
  127. package/dist/project-compiler-NNK32MPG.mjs +10 -0
  128. package/dist/project-compiler-OP2VVGJQ.mjs +10 -0
  129. package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
  130. package/dist/project-compiler-VWNNCHGO.mjs +10 -0
  131. package/dist/project-compiler-XVAAU4C5.mjs +10 -0
  132. package/dist/project-compiler-YES5FGMD.mjs +10 -0
  133. package/dist/project-compiler-ZB4RUYVL.mjs +10 -0
  134. package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
  135. package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
  136. package/dist/project-decompiler-U55HQUHW.mjs +7 -0
  137. package/dist/project-decompiler-US7GAVIC.mjs +7 -0
  138. package/dist/project-decompiler-VLPR22QF.mjs +7 -0
  139. package/dist/pull-FUS5QYZS.mjs +109 -0
  140. package/dist/pull-KOL2QAYQ.mjs +109 -0
  141. package/dist/pull-LD5ENLGY.mjs +109 -0
  142. package/dist/pull-P44LDRWB.mjs +109 -0
  143. package/dist/seed-KOGEPGOJ.mjs +154 -0
  144. package/dist/server-VW6UPCHO.mjs +277 -0
  145. package/dist/testing/index.d.mts +8 -8
  146. package/dist/testing/index.d.ts +8 -8
  147. package/dist/testing/index.js +2824 -287
  148. package/dist/testing/index.mjs +2 -2
  149. package/dist/verify-BYHUKARQ.mjs +1833 -0
  150. package/dist/verify-OQDEQYMS.mjs +1833 -0
  151. package/dist/verify-SEIXUGN4.mjs +1833 -0
  152. package/dist/vite/index.d.mts +1 -1
  153. package/dist/vite/index.d.ts +1 -1
  154. package/dist/vite/index.js +2817 -280
  155. package/dist/vite/index.mjs +3 -3
  156. package/examples/authentication/main.workflow.tsx +1 -1
  157. package/examples/authentication/mm.config.ts +1 -1
  158. package/examples/authentication/pages/LoginPage.tsx +2 -2
  159. package/examples/authentication/pages/SignupPage.tsx +2 -2
  160. package/examples/counter.workflow.tsx +1 -1
  161. package/examples/dashboard.workflow.tsx +1 -1
  162. package/examples/invoice-approval/actions/invoice.server.ts +1 -1
  163. package/examples/invoice-approval/main.workflow.tsx +1 -1
  164. package/examples/invoice-approval/mm.config.ts +1 -1
  165. package/examples/invoice-approval/pages/InvoiceDetailPage.tsx +1 -1
  166. package/examples/invoice-approval/pages/InvoiceFormPage.tsx +1 -1
  167. package/examples/invoice-approval/pages/InvoiceListPage.tsx +1 -1
  168. package/examples/todo-app.workflow.tsx +1 -1
  169. package/examples/uber-app/actions/matching.server.ts +1 -1
  170. package/examples/uber-app/actions/notifications.server.ts +1 -1
  171. package/examples/uber-app/actions/payments.server.ts +1 -1
  172. package/examples/uber-app/actions/pricing.server.ts +1 -1
  173. package/examples/uber-app/app/admin/analytics.tsx +2 -2
  174. package/examples/uber-app/app/admin/fleet.tsx +21 -21
  175. package/examples/uber-app/app/admin/surge-pricing.tsx +2 -2
  176. package/examples/uber-app/app/driver/dashboard.tsx +2 -2
  177. package/examples/uber-app/app/driver/earnings.tsx +2 -2
  178. package/examples/uber-app/app/driver/navigation.tsx +2 -2
  179. package/examples/uber-app/app/driver/ride-acceptance.tsx +2 -2
  180. package/examples/uber-app/app/rider/home.tsx +2 -2
  181. package/examples/uber-app/app/rider/payment-methods.tsx +2 -2
  182. package/examples/uber-app/app/rider/ride-history.tsx +2 -2
  183. package/examples/uber-app/app/rider/ride-tracking.tsx +2 -2
  184. package/examples/uber-app/components/DriverCard.tsx +1 -1
  185. package/examples/uber-app/components/MapView.tsx +3 -3
  186. package/examples/uber-app/components/RatingStars.tsx +2 -2
  187. package/examples/uber-app/components/RideCard.tsx +1 -1
  188. package/examples/uber-app/mm.config.ts +1 -1
  189. package/examples/uber-app/workflows/dispute-resolution.workflow.tsx +2 -2
  190. package/examples/uber-app/workflows/driver-onboarding.workflow.tsx +2 -2
  191. package/examples/uber-app/workflows/payment-processing.workflow.tsx +2 -2
  192. package/examples/uber-app/workflows/ride-request.workflow.tsx +2 -2
  193. package/package.json +10 -4
  194. package/compile-blueprint-chat.mjs +0 -99
  195. package/compile-blueprint-glass-console.mjs +0 -98
  196. package/compile-chat-defs.mjs +0 -92
  197. package/examples/uber-app/tests/payment.test.tsx +0 -129
  198. package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
  199. package/package.json.backup +0 -86
  200. package/scripts/decompile.ts +0 -226
  201. package/scripts/seed-auth.ts +0 -267
  202. package/scripts/seed-uber.ts +0 -248
  203. package/scripts/validate-uber.ts +0 -119
  204. package/seed-blueprint-chat.mjs +0 -444
  205. package/seed-blueprint-glass-console.mjs +0 -445
  206. package/seed-compiled.mjs +0 -318
  207. package/src/RoundTripValidator.ts +0 -400
  208. package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
  209. package/src/__tests__/auth-module-compilation.test.ts +0 -247
  210. package/src/__tests__/auth-template-compilation.test.ts +0 -589
  211. package/src/__tests__/change-extractor.test.ts +0 -142
  212. package/src/__tests__/cli-pull.test.ts +0 -73
  213. package/src/__tests__/cli-test.test.ts +0 -72
  214. package/src/__tests__/component-extractor.test.ts +0 -331
  215. package/src/__tests__/context-extractor.test.ts +0 -145
  216. package/src/__tests__/decompiler.test.ts +0 -718
  217. package/src/__tests__/define-blueprint.test.ts +0 -133
  218. package/src/__tests__/definition-validator.test.ts +0 -519
  219. package/src/__tests__/during-extractor.test.ts +0 -152
  220. package/src/__tests__/effect-extractor.test.ts +0 -107
  221. package/src/__tests__/event-emission.test.ts +0 -127
  222. package/src/__tests__/examples.test.ts +0 -236
  223. package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
  224. package/src/__tests__/golden-suite.test.ts +0 -403
  225. package/src/__tests__/grammar-island-extractor.test.ts +0 -289
  226. package/src/__tests__/instance-key.test.ts +0 -82
  227. package/src/__tests__/ir-migration.test.ts +0 -255
  228. package/src/__tests__/lock-file.test.ts +0 -117
  229. package/src/__tests__/model-extractor.test.ts +0 -195
  230. package/src/__tests__/model-field-acl.test.ts +0 -237
  231. package/src/__tests__/model-hooks.test.ts +0 -130
  232. package/src/__tests__/model-ref-resolution.test.ts +0 -268
  233. package/src/__tests__/model-roundtrip.test.ts +0 -502
  234. package/src/__tests__/model-runtime.test.ts +0 -112
  235. package/src/__tests__/model-transitions.test.ts +0 -183
  236. package/src/__tests__/nrt-action-trace.test.ts +0 -391
  237. package/src/__tests__/pipeline-hardening.test.ts +0 -413
  238. package/src/__tests__/project-compiler.test.ts +0 -546
  239. package/src/__tests__/project-decompiler.test.ts +0 -343
  240. package/src/__tests__/query-compilation.test.ts +0 -145
  241. package/src/__tests__/round-trip/PLAN.md +0 -158
  242. package/src/__tests__/round-trip/README.md +0 -52
  243. package/src/__tests__/round-trip/RESULTS.md +0 -86
  244. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
  245. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
  246. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
  247. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
  248. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
  249. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
  250. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
  251. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
  252. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
  253. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
  254. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
  255. package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
  256. package/src/__tests__/round-trip-ir.test.ts +0 -300
  257. package/src/__tests__/round-trip.test.ts +0 -1212
  258. package/src/__tests__/route-merging.test.ts +0 -372
  259. package/src/__tests__/router-composition.test.ts +0 -489
  260. package/src/__tests__/router-extractor.test.ts +0 -176
  261. package/src/__tests__/server-action-extractor.test.ts +0 -128
  262. package/src/__tests__/smart-type-inference.test.ts +0 -365
  263. package/src/__tests__/source-envelope.test.ts +0 -284
  264. package/src/__tests__/source-fidelity.test.ts +0 -516
  265. package/src/__tests__/state-extractor.test.ts +0 -115
  266. package/src/__tests__/strict-mode.test.ts +0 -227
  267. package/src/__tests__/transition-effect-extractor.test.ts +0 -119
  268. package/src/__tests__/transition-extractor.test.ts +0 -68
  269. package/src/__tests__/ts-to-expression.test.ts +0 -462
  270. package/src/__tests__/type-generator.test.ts +0 -201
  271. package/src/__tests__/uber-validation.test.ts +0 -502
  272. package/src/action-compiler.ts +0 -361
  273. package/src/babel/emitters/experience-transform.ts +0 -199
  274. package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
  275. package/src/babel/emitters/pure-form-emitter.ts +0 -1023
  276. package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
  277. package/src/babel/extractors/change-extractor.ts +0 -199
  278. package/src/babel/extractors/component-extractor.ts +0 -907
  279. package/src/babel/extractors/computed-extractor.ts +0 -262
  280. package/src/babel/extractors/context-extractor.ts +0 -277
  281. package/src/babel/extractors/during-extractor.ts +0 -295
  282. package/src/babel/extractors/effect-extractor.ts +0 -340
  283. package/src/babel/extractors/event-extractor.ts +0 -235
  284. package/src/babel/extractors/grammar-island-extractor.ts +0 -302
  285. package/src/babel/extractors/model-extractor.ts +0 -1018
  286. package/src/babel/extractors/router-extractor.ts +0 -303
  287. package/src/babel/extractors/server-action-extractor.ts +0 -173
  288. package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
  289. package/src/babel/extractors/server-state-extractor.ts +0 -88
  290. package/src/babel/extractors/state-extractor.ts +0 -214
  291. package/src/babel/extractors/transition-effect-extractor.ts +0 -176
  292. package/src/babel/extractors/transition-extractor.ts +0 -143
  293. package/src/babel/index.ts +0 -24
  294. package/src/babel/transpilers/ts-to-expression.ts +0 -674
  295. package/src/babel/visitor.ts +0 -807
  296. package/src/cli/auth.ts +0 -255
  297. package/src/cli/build.ts +0 -288
  298. package/src/cli/deploy.ts +0 -206
  299. package/src/cli/index.ts +0 -328
  300. package/src/cli/installer.ts +0 -261
  301. package/src/cli/lock-file.ts +0 -94
  302. package/src/cli/mmrc.ts +0 -22
  303. package/src/cli/pull.ts +0 -172
  304. package/src/cli/registry-client.ts +0 -175
  305. package/src/cli/test.ts +0 -397
  306. package/src/cli/type-generator.ts +0 -243
  307. package/src/codemod/__tests__/forward.test.ts +0 -239
  308. package/src/codemod/__tests__/reverse.test.ts +0 -145
  309. package/src/codemod/__tests__/round-trip.test.ts +0 -137
  310. package/src/codemod/annotation.ts +0 -97
  311. package/src/codemod/classify.ts +0 -197
  312. package/src/codemod/cli.ts +0 -207
  313. package/src/codemod/control-flow.ts +0 -409
  314. package/src/codemod/forward.ts +0 -244
  315. package/src/codemod/import-manager.ts +0 -171
  316. package/src/codemod/index.ts +0 -120
  317. package/src/codemod/reverse.ts +0 -197
  318. package/src/codemod/rules.ts +0 -174
  319. package/src/codemod/state-transform.ts +0 -126
  320. package/src/decompiler/ast-builder.ts +0 -538
  321. package/src/decompiler/config-generator.ts +0 -151
  322. package/src/decompiler/index.ts +0 -315
  323. package/src/decompiler/project-decompiler.ts +0 -1776
  324. package/src/decompiler/project.ts +0 -862
  325. package/src/decompiler/split-strategy.ts +0 -140
  326. package/src/decompiler/state-emitter.ts +0 -1053
  327. package/src/decompiler/sx-emitter.ts +0 -318
  328. package/src/decompiler/workspace-hydrator.ts +0 -189
  329. package/src/dev-server.ts +0 -238
  330. package/src/envelope/fs-tree.ts +0 -217
  331. package/src/envelope/source-envelope.ts +0 -264
  332. package/src/envelope.ts +0 -315
  333. package/src/incremental-compiler.ts +0 -401
  334. package/src/index.ts +0 -99
  335. package/src/model-compiler.ts +0 -277
  336. package/src/project-compiler.ts +0 -1629
  337. package/src/route-extractor.ts +0 -333
  338. package/src/testing/index.ts +0 -32
  339. package/src/testing/snapshot.ts +0 -252
  340. package/src/testing/test-utils.ts +0 -226
  341. package/src/types.ts +0 -68
  342. package/src/vite/index.ts +0 -288
  343. package/test-compile.mjs +0 -142
  344. package/tsconfig.json +0 -25
  345. package/tsup.config.ts +0 -23
  346. package/vitest.config.ts +0 -9
@@ -1,1776 +0,0 @@
1
- /**
2
- * project-decompiler.ts — Enhanced multi-file project decompiler.
3
- *
4
- * Takes a full WorkflowDefinition (with states, transitions, fields,
5
- * experience, roles) and produces a multi-file project structure:
6
- *
7
- * - States/Transitions → models/{slug}.ts (exported arrays + interface)
8
- * - Experience tree → app/**\/*.tsx pages based on route structure
9
- * - Roles → auth guard helper in app/layout.tsx
10
- * - Server actions → actions/*.server.ts files
11
- * - Config → mm.config.ts
12
- *
13
- * Uses the SplitStrategy to determine output complexity and the
14
- * ConfigGenerator for mm.config.ts emission.
15
- */
16
-
17
- import type {
18
- IRExperienceNode,
19
- IRFieldDefinition,
20
- IRStateDefinition,
21
- IRTransitionDefinition,
22
- } from '@mindmatrix/player-core';
23
- import { decompile } from './index';
24
- import type { DecompilerInput } from './index';
25
- import { determineSplitStrategy } from './split-strategy';
26
- import type { SplitDecision } from './split-strategy';
27
- import { extractConfigData, generateMmConfig } from './config-generator';
28
- import type { FileRole } from '../envelope/fs-tree';
29
-
30
- // =============================================================================
31
- // Public Types
32
- // =============================================================================
33
-
34
- /** A single file produced by project decompilation. */
35
- export interface EnhancedProjectFile {
36
- /** Relative path from project root. */
37
- path: string;
38
- /** File role per envelope fs-tree. */
39
- role: FileRole;
40
- /** Generated source code. */
41
- content: string;
42
- /** SHA-256 hash of content (for workspace hydration). */
43
- hash?: string;
44
- }
45
-
46
- /** Result of the enhanced project decompiler. */
47
- export interface EnhancedDecompileResult {
48
- files: EnhancedProjectFile[];
49
- entryFile: string;
50
- slug: string;
51
- splitDecision: SplitDecision;
52
- }
53
-
54
- // =============================================================================
55
- // Name Helpers
56
- // =============================================================================
57
-
58
- function pascalCase(slug: string): string {
59
- return slug
60
- .split(/[-_]/)
61
- .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
62
- .join('');
63
- }
64
-
65
- function camelCase(str: string): string {
66
- return str.replace(/[-_]([a-z])/g, (_, c: string) => c.toUpperCase());
67
- }
68
-
69
- /**
70
- * Corrects the "all same from" compiler bug in transitions.
71
- * When all transitions share the same `from` value AND every transition
72
- * name is an independently-known state name (from `from` + `to` fields only,
73
- * NOT from the transition names themselves), the transition name is used as
74
- * the actual source state.
75
- *
76
- * This avoids false positives where transitions like "increment" and "reset"
77
- * share `from: 'idle'` but are NOT state names.
78
- */
79
- function correctTransitionFromFields(
80
- transitions: IRTransitionDefinition[],
81
- ): IRTransitionDefinition[] {
82
- if (transitions.length <= 1) return transitions;
83
-
84
- const fromValues = new Set<string>();
85
- for (const t of transitions) {
86
- const arr = Array.isArray(t.from) ? t.from : [t.from as unknown as string];
87
- for (const f of arr) fromValues.add(f);
88
- }
89
- if (fromValues.size !== 1) return transitions;
90
-
91
- // Build state set from from/to values ONLY (not from transition names)
92
- const stateNames = new Set<string>([...fromValues]);
93
- for (const t of transitions) {
94
- stateNames.add(t.to);
95
- }
96
-
97
- // Only apply correction if EVERY transition name is already a known state
98
- const allNamesAreStates = transitions.every(t => stateNames.has(t.name));
99
- if (!allNamesAreStates) return transitions;
100
-
101
- return transitions.map(t => ({ ...t, from: [t.name] }));
102
- }
103
-
104
- function fieldTypeToTS(fieldType: string, field?: IRFieldDefinition): string {
105
- // Check for options → emit union type
106
- const options = getFieldOptions(field);
107
- if (options && options.length > 0 && (fieldType === 'select' || fieldType === 'text')) {
108
- return options.map(o => `'${esc(String(o))}'`).join(' | ');
109
- }
110
-
111
- switch (fieldType) {
112
- case 'text': case 'rich_text': case 'email': case 'url':
113
- case 'phone': case 'color': case 'select':
114
- return 'string';
115
- case 'number': case 'currency': case 'percentage':
116
- case 'rating': case 'duration': case 'auto_number':
117
- return 'number';
118
- case 'boolean':
119
- return 'boolean';
120
- case 'date': case 'datetime': case 'created_at': case 'updated_at':
121
- return 'Date';
122
- case 'multi_select':
123
- return 'string[]';
124
- case 'json': case 'object':
125
- return 'Record<string, unknown>';
126
- case 'array':
127
- return 'string[]';
128
- case 'file': case 'image':
129
- return 'string';
130
- case 'relation': case 'lookup':
131
- return 'string';
132
- default:
133
- return 'unknown';
134
- }
135
- }
136
-
137
- /**
138
- * Extracts options from a field's validation or metadata.
139
- * Options may be stored as:
140
- * - field.validation.options (primary — DB schema)
141
- * - field.metadata.options (fallback)
142
- * - field.options (legacy)
143
- */
144
- function getFieldOptions(field?: IRFieldDefinition): string[] | null {
145
- if (!field) return null;
146
- const f = field as unknown as Record<string, unknown>;
147
-
148
- // Primary: validation.options
149
- const validation = f.validation as Record<string, unknown> | undefined;
150
- if (validation?.options && Array.isArray(validation.options)) {
151
- return validation.options as string[];
152
- }
153
-
154
- // Fallback: metadata.options
155
- const meta = f.metadata as Record<string, unknown> | undefined;
156
- if (meta?.options && Array.isArray(meta.options)) {
157
- return meta.options as string[];
158
- }
159
-
160
- // Legacy: field.options
161
- if (f.options && Array.isArray(f.options)) {
162
- return f.options as string[];
163
- }
164
-
165
- return null;
166
- }
167
-
168
- // =============================================================================
169
- // Model File: defineModel() + companion interface
170
- // =============================================================================
171
-
172
- /**
173
- * Returns a valid JS object key — quoted if the name isn't a valid identifier.
174
- */
175
- function safeKey(name: string): string {
176
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${esc(name)}'`;
177
- }
178
-
179
- /**
180
- * Maps an IR field type to a defineModel field type string.
181
- */
182
- function fieldTypeToModelType(fieldType: string): string {
183
- switch (fieldType) {
184
- case 'text': case 'rich_text': case 'select':
185
- return 'string';
186
- case 'email': return 'email';
187
- case 'url': return 'url';
188
- case 'phone': return 'phone';
189
- case 'color': return 'color';
190
- case 'number': case 'currency': case 'percentage':
191
- case 'rating': case 'duration': case 'auto_number':
192
- return 'number';
193
- case 'boolean':
194
- return 'boolean';
195
- case 'date': case 'datetime': case 'created_at': case 'updated_at':
196
- return 'date';
197
- case 'multi_select':
198
- return 'array';
199
- case 'json': case 'object':
200
- return 'json';
201
- case 'array':
202
- return 'array';
203
- case 'file': case 'image':
204
- return 'file';
205
- case 'relation': case 'lookup':
206
- return 'relation';
207
- default:
208
- return fieldType;
209
- }
210
- }
211
-
212
- /**
213
- * Serializes a default value for defineModel field declarations.
214
- */
215
- function serializeDefault(value: unknown): string {
216
- if (value === undefined || value === null) return 'null';
217
- if (typeof value === 'string') return `'${esc(value)}'`;
218
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
219
- if (Array.isArray(value)) {
220
- if (value.length === 0) return '[]';
221
- return `[${value.map(v => serializeDefault(v)).join(', ')}]`;
222
- }
223
- return JSON.stringify(value);
224
- }
225
-
226
- /**
227
- * Generates a complete model file with:
228
- * - import { defineModel } from "@mindmatrix/react"
229
- * - export default defineModel({ slug, version, category, fields, states, transitions })
230
- * - companion TypeScript interface for IntelliSense
231
- */
232
- function generateModelFile(
233
- slug: string,
234
- fields: IRFieldDefinition[],
235
- states: IRStateDefinition[],
236
- transitions: IRTransitionDefinition[],
237
- meta?: { version?: string; category?: string; description?: string },
238
- ): string {
239
- const typeName = pascalCase(slug);
240
- const interfaceName = `${typeName}Fields`;
241
- const version = meta?.version || '0.1.0';
242
- const category = meta?.category || 'data';
243
- const lines: string[] = [];
244
-
245
- // Import
246
- lines.push(`import { defineModel } from '@mindmatrix/react';`);
247
- lines.push(``);
248
-
249
- // Companion TypeScript interface for IntelliSense
250
- // NOTE: Field labels are deliberately excluded to ensure round-trip stability.
251
- // Fields sorted alphabetically for deterministic round-trip output.
252
- const sortedFields = [...fields].sort((a, b) => a.name.localeCompare(b.name));
253
- if (sortedFields.length > 0) {
254
- lines.push(`export interface ${interfaceName} {`);
255
- for (const field of sortedFields) {
256
- const tsType = fieldTypeToTS(field.type, field);
257
- const optional = field.required ? '' : '?';
258
- lines.push(` ${camelCase(field.name)}${optional}: ${tsType};`);
259
- }
260
- lines.push(`}`);
261
- lines.push(``);
262
- }
263
-
264
- // defineModel() call
265
- lines.push(`export default defineModel({`);
266
- lines.push(` slug: '${esc(slug)}',`);
267
- lines.push(` version: '${esc(version)}',`);
268
- lines.push(` category: '${esc(category)}',`);
269
- if (meta?.description) {
270
- lines.push(` description: '${esc(meta.description)}',`);
271
- }
272
-
273
- // Fields (sorted alphabetically for determinism)
274
- if (sortedFields.length > 0) {
275
- lines.push(` fields: {`);
276
- for (const field of sortedFields) {
277
- const props: string[] = [];
278
- props.push(`type: '${fieldTypeToModelType(field.type)}'`);
279
- if (field.required) {
280
- props.push(`required: true`);
281
- }
282
- // Default value — emit when present. Null is emitted explicitly to
283
- // distinguish "no default" (undefined) from "default is null".
284
- // Skip trivial type-inferred defaults (empty string, 0, false) to keep
285
- // model files clean, but always emit null for round-trip stability.
286
- if (field.default_value === null) {
287
- props.push(`default: null`);
288
- } else if (field.default_value !== undefined
289
- && field.default_value !== '' && field.default_value !== 0
290
- && field.default_value !== false) {
291
- props.push(`default: ${serializeDefault(field.default_value)}`);
292
- }
293
- // Enum/options for select fields
294
- const options = getFieldOptions(field);
295
- if (options && options.length > 0) {
296
- const enumStr = options.map(o => `'${esc(String(o))}'`).join(', ');
297
- props.push(`enum: [${enumStr}]`);
298
- }
299
- lines.push(` ${camelCase(field.name)}: { ${props.join(', ')} },`);
300
- }
301
- lines.push(` },`);
302
- }
303
-
304
- // States (sorted alphabetically for determinism, skip empty-named states)
305
- const validStates = states.filter(s => s.name !== '');
306
- if (validStates.length > 0) {
307
- const sortedStates = [...validStates].sort((a, b) => a.name.localeCompare(b.name));
308
- lines.push(` states: {`);
309
- for (const state of sortedStates) {
310
- const props: string[] = [];
311
- if (state.type === 'START') {
312
- props.push(`type: 'initial'`);
313
- } else if (state.type === 'END') {
314
- props.push(`type: 'final'`);
315
- }
316
- if (state.description) {
317
- props.push(`description: '${esc(state.description)}'`);
318
- }
319
- const body = props.length > 0 ? ` ${props.join(', ')} ` : '';
320
- lines.push(` ${safeKey(state.name)}: {${body}},`);
321
- }
322
- lines.push(` },`);
323
- }
324
-
325
- // Transitions (sorted alphabetically for determinism, with from-field correction)
326
- if (transitions.length > 0) {
327
- const correctedTrans = correctTransitionFromFields(transitions);
328
- const sortedTrans = [...correctedTrans].sort((a, b) => a.name.localeCompare(b.name));
329
- lines.push(` transitions: {`);
330
- for (const trans of sortedTrans) {
331
- const fromArr = Array.isArray(trans.from) ? trans.from : [trans.from as unknown as string];
332
- const parts: string[] = [];
333
- if (fromArr.length === 1) {
334
- parts.push(`from: '${esc(fromArr[0])}'`);
335
- } else {
336
- parts.push(`from: [${fromArr.map(f => `'${esc(f)}'`).join(', ')}]`);
337
- }
338
- parts.push(`to: '${esc(trans.to)}'`);
339
- if (trans.roles && trans.roles.length > 0) {
340
- parts.push(`roles: [${trans.roles.map(r => `'${esc(r)}'`).join(', ')}]`);
341
- }
342
- if (trans.auto) {
343
- parts.push(`auto: true`);
344
- }
345
- if (trans.required_fields && trans.required_fields.length > 0) {
346
- parts.push(`required_fields: [${trans.required_fields.map(f => `'${esc(f)}'`).join(', ')}]`);
347
- }
348
- lines.push(` ${safeKey(trans.name)}: { ${parts.join(', ')} },`);
349
- }
350
- lines.push(` },`);
351
- }
352
-
353
- lines.push(`});`);
354
- lines.push(``);
355
-
356
- return lines.join('\n');
357
- }
358
-
359
- // =============================================================================
360
- // Layout File (with auth guards)
361
- // =============================================================================
362
-
363
- /**
364
- * Generates app/layout.tsx with role-based auth guards.
365
- */
366
- function generateLayoutFile(
367
- slug: string,
368
- roles: Array<{ name: string; permissions: string[] }>,
369
- ): string {
370
- const componentName = `${pascalCase(slug)}Layout`;
371
- const lines: string[] = [
372
- `/**`,
373
- ` * ${componentName} — root layout with auth guards.`,
374
- ` */`,
375
- ``,
376
- `import { useRole, Stack } from '@mindmatrix/react';`,
377
- ``,
378
- `interface LayoutProps {`,
379
- ` children: React.ReactNode;`,
380
- `}`,
381
- ``,
382
- `export default function ${componentName}({ children }: LayoutProps) {`,
383
- ];
384
-
385
- // Role guard declarations
386
- for (const role of roles) {
387
- const varName = `is${pascalCase(role.name)}`;
388
- lines.push(` const ${varName} = useRole('${esc(role.name)}');`);
389
- }
390
-
391
- if (roles.length > 0) {
392
- lines.push(``);
393
- // Generate a simple authorized check
394
- const roleChecks = roles.map(r => `is${pascalCase(r.name)}`).join(' || ');
395
- lines.push(` const isAuthorized = ${roleChecks};`);
396
- lines.push(``);
397
- lines.push(` if (!isAuthorized) {`);
398
- lines.push(` return <Stack sx={{ p: 32, align: 'center' }}>Access denied</Stack>;`);
399
- lines.push(` }`);
400
- }
401
-
402
- lines.push(``);
403
- lines.push(` return (`);
404
- lines.push(` <Stack sx={{ minH: '100vh' }}>`);
405
- lines.push(` {children}`);
406
- lines.push(` </Stack>`);
407
- lines.push(` );`);
408
- lines.push(`}`);
409
- lines.push(``);
410
-
411
- return lines.join('\n');
412
- }
413
-
414
- // =============================================================================
415
- // Route Table & Page Classification
416
- // =============================================================================
417
-
418
- interface RouteEntry {
419
- stateName: string;
420
- role: string;
421
- page: string;
422
- path: string;
423
- }
424
-
425
- interface PageSection {
426
- route: string;
427
- role: string;
428
- slug: string;
429
- title: string;
430
- componentName: string;
431
- tree: IRExperienceNode;
432
- filePath: string;
433
- localDefaults: Record<string, unknown>;
434
- roleGuard?: string;
435
- dataSources?: unknown[];
436
- }
437
-
438
- /**
439
- * Extracts route table from a router child definition.
440
- * Parses state descriptions like "Route: /admin/analytics" into structured entries.
441
- */
442
- function extractRouteTable(childDefinitions?: DecompilerInput[]): RouteEntry[] {
443
- if (!childDefinitions) return [];
444
- const router = childDefinitions.find(
445
- c => c.slug.endsWith('-router') || c.category === 'router',
446
- );
447
- if (!router?.states) return [];
448
-
449
- return router.states
450
- .filter(s => s.description?.startsWith('Route:'))
451
- .map(s => {
452
- const route = s.description!.replace(/^Route:\s*/, '').replace(/^\//, '');
453
- const parts = route.split('/');
454
- return {
455
- stateName: s.name,
456
- role: parts[0] || 'shared',
457
- page: parts.slice(1).join('/') || parts[0],
458
- path: route,
459
- };
460
- });
461
- }
462
-
463
- /**
464
- * BFS search for the first h2 heading text in a subtree.
465
- */
466
- function findPageTitle(node: IRExperienceNode, maxDepth = 5): string | null {
467
- const queue: Array<{ n: IRExperienceNode; d: number }> = [{ n: node, d: 0 }];
468
-
469
- while (queue.length > 0) {
470
- const { n, d } = queue.shift()!;
471
- if (d > maxDepth) continue;
472
-
473
- if (
474
- (n.component === 'Text' || n.component === 'Heading') &&
475
- (n.config?.level === 2 || n.config?.variant === 'h2')
476
- ) {
477
- if (n.config?.text) return n.config.text as string;
478
- // Composite title from children (e.g., "Payment #" + "{id}")
479
- if (n.children?.length) {
480
- const parts = n.children
481
- .filter(c => c.config?.value)
482
- .map(c => c.config!.value as string);
483
- if (parts.length > 0) return parts.join('');
484
- }
485
- }
486
-
487
- if (n.children) {
488
- for (const child of n.children) {
489
- queue.push({ n: child, d: d + 1 });
490
- }
491
- }
492
- }
493
-
494
- return null;
495
- }
496
-
497
- /**
498
- * Detects the role for a page from bindings, localDefaults keys, and title hints.
499
- */
500
- function detectPageRole(node: IRExperienceNode): string {
501
- // Show when binding
502
- const when = node.bindings?.when || '';
503
- if (/isAdmin|is_admin/i.test(when)) return 'admin';
504
- if (/isDriver|is_driver/i.test(when)) return 'driver';
505
- if (/isRider|is_rider/i.test(when)) return 'rider';
506
-
507
- // localDefaults key prefixes
508
- const ld = (node.config?.localDefaults || {}) as Record<string, unknown>;
509
- const keys = Object.keys(ld);
510
- if (keys.some(k => k.startsWith('driver.') || k === 'is_online')) return 'driver';
511
- if (keys.some(k => k.startsWith('earnings.') || k.startsWith('matching.'))) return 'driver';
512
- if (keys.some(k => k.startsWith('rider.'))) return 'rider';
513
-
514
- // localDefaults content-based hints
515
- if (keys.includes('ride.current_fare') || keys.includes('time_left')) return 'driver';
516
- if (keys.includes('selected_vehicle') || keys.includes('driver_location')) return 'rider';
517
- if (keys.includes('date_from') || keys.includes('date_to')) return 'rider';
518
- if (keys.includes('cvv') || keys.includes('card_number')) return 'rider';
519
-
520
- // Title-based
521
- const title = findPageTitle(node);
522
- if (title) {
523
- const lower = title.toLowerCase();
524
- if (lower.includes('driver') || lower.includes('earning') || lower.includes('navigation')) return 'driver';
525
- if (lower.includes('ride request') || lower.includes('ride acceptance')) return 'driver';
526
- if (lower.includes('payment method') || lower.includes('ride history')) return 'rider';
527
- if (lower.includes('your ride') || lower.includes('where to') || lower.includes('ride tracking')) return 'rider';
528
- if (lower.includes('analytics') || lower.includes('surge') || lower.includes('fleet')) return 'admin';
529
- }
530
-
531
- return 'shared';
532
- }
533
-
534
- /**
535
- * Checks if a node represents a child workflow view (not a router page).
536
- * Detected by title containing "#" (instance refs) or EXACT slug match with child defs.
537
- * Uses strict matching to avoid false positives (e.g., "Incoming Ride Request" ≠ "ride-request").
538
- */
539
- function isChildWorkflowView(
540
- node: IRExperienceNode,
541
- childSlugs: Set<string>,
542
- ): boolean {
543
- const title = findPageTitle(node);
544
- if (!title) return false;
545
-
546
- // Title contains "#" → references specific instance (e.g., "Payment #abc123")
547
- if (title.includes('#')) return true;
548
-
549
- // Exact slug match only (no substring matching to avoid false positives)
550
- const titleSlug = title
551
- .toLowerCase()
552
- .replace(/[^a-z0-9\s]/g, '')
553
- .trim()
554
- .replace(/\s+/g, '-');
555
-
556
- return childSlugs.has(titleSlug);
557
- }
558
-
559
- /** Converts a page title to a URL-friendly slug. */
560
- function titleToSlug(title: string): string {
561
- const MAP: Record<string, string> = {
562
- 'where to?': 'home',
563
- 'where to': 'home',
564
- 'your ride': 'ride-tracking',
565
- 'incoming ride request': 'ride-acceptance',
566
- };
567
- const lower = title.toLowerCase();
568
- if (MAP[lower]) return MAP[lower];
569
-
570
- return lower
571
- .replace(/[^a-z0-9\s]/g, '')
572
- .trim()
573
- .replace(/\s+/g, '-');
574
- }
575
-
576
- /** Scores how well a page title matches a route entry. */
577
- function scoreRouteMatch(title: string, route: RouteEntry): number {
578
- const titleSlug = title
579
- .toLowerCase()
580
- .replace(/[^a-z0-9\s]/g, '')
581
- .trim()
582
- .replace(/\s+/g, '-');
583
- const titleWords = new Set(titleSlug.split('-').filter(w => w.length > 2));
584
-
585
- let score = 0;
586
- for (const word of route.page.split('-')) {
587
- if (titleWords.has(word)) score += 2;
588
- }
589
- if (titleSlug.includes(route.page) || route.page.includes(titleSlug)) {
590
- score += 3;
591
- }
592
- return score;
593
- }
594
-
595
- /** Extracts role guard binding from a Show node (e.g., "isAdmin"). */
596
- function extractRoleGuard(node: IRExperienceNode): string | undefined {
597
- const when = node.bindings?.when || '';
598
- const match = when.match(/\$instance\.(is\w+)/);
599
- return match ? match[1] : undefined;
600
- }
601
-
602
- /** Collects dataSources from a node and its immediate children. */
603
- function collectDataSources(node: IRExperienceNode): unknown[] {
604
- const sources: unknown[] = [];
605
- const nodeDS = (node as unknown as Record<string, unknown>).dataSources;
606
- if (Array.isArray(nodeDS)) sources.push(...nodeDS);
607
-
608
- if (node.children) {
609
- for (const child of node.children) {
610
- const childDS = (child as unknown as Record<string, unknown>).dataSources;
611
- if (Array.isArray(childDS)) sources.push(...childDS);
612
- }
613
- }
614
- return sources;
615
- }
616
-
617
- /** Strips localDefaults from a node's config (shallow copy). */
618
- function stripLocalDefaults(node: IRExperienceNode): IRExperienceNode {
619
- if (!node.config?.localDefaults) return node;
620
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
621
- const { localDefaults, ...rest } = node.config;
622
- return { ...node, config: Object.keys(rest).length > 0 ? rest : undefined };
623
- }
624
-
625
- /** Gets page content tree: unwraps Show wrappers, removes localDefaults. */
626
- function getPageContentTree(node: IRExperienceNode): IRExperienceNode {
627
- if (node.component === 'Show' && node.children?.length === 1) {
628
- return stripLocalDefaults(node.children[0]);
629
- }
630
- return stripLocalDefaults(node);
631
- }
632
-
633
- /** Infers a field type from a default value. */
634
- function inferFieldType(value: unknown): string {
635
- if (typeof value === 'boolean') return 'boolean';
636
- if (typeof value === 'number') return 'number';
637
- return 'text';
638
- }
639
-
640
- /**
641
- * Main page extraction: walks root experience children, classifies them as
642
- * route pages or child workflow views, and matches pages to router states.
643
- *
644
- * Two-pass route matching:
645
- * Pass 1: High-confidence keyword overlap (score >= 3)
646
- * Pass 2: Assign remaining by elimination within each role group
647
- */
648
- function extractPageSections(
649
- experience: IRExperienceNode | undefined,
650
- childDefinitions?: DecompilerInput[],
651
- ): { pages: PageSection[]; childViews: IRExperienceNode[] } {
652
- if (!experience?.children) return { pages: [], childViews: [] };
653
-
654
- // ── Fast path: Router/Route experience (already structured by compiler) ──
655
- // When the experience already contains a Router with Route children (e.g. from
656
- // a previous compile→decompile cycle), extract pages deterministically from the
657
- // Route nodes. This ensures source stabilization (pass N+1 = pass N).
658
- const routerExtracted = extractFromRouterExperience(experience);
659
- if (routerExtracted) return routerExtracted;
660
-
661
- // ── Heuristic path: flat experience (original source) ──
662
- const routeTable = extractRouteTable(childDefinitions);
663
- const childSlugs = new Set(
664
- (childDefinitions || [])
665
- .filter(c => !c.slug.endsWith('-router') && c.category !== 'router')
666
- .map(c => c.slug),
667
- );
668
-
669
- const pages: PageSection[] = [];
670
- const childViews: IRExperienceNode[] = [];
671
- const candidates: Array<{
672
- title: string | null;
673
- role: string;
674
- index: number;
675
- node: IRExperienceNode;
676
- localDefaults: Record<string, unknown>;
677
- roleGuard?: string;
678
- dataSources?: unknown[];
679
- }> = [];
680
-
681
- // Phase 1: classify each child
682
- for (let i = 0; i < experience.children.length; i++) {
683
- const child = experience.children[i];
684
-
685
- if (isChildWorkflowView(child, childSlugs)) {
686
- childViews.push(child);
687
- continue;
688
- }
689
-
690
- const title = findPageTitle(child);
691
- const role = detectPageRole(child);
692
- const localDefaults = (child.config?.localDefaults || {}) as Record<string, unknown>;
693
- const roleGuard = extractRoleGuard(child);
694
- const dataSources = collectDataSources(child);
695
-
696
- candidates.push({
697
- title, role, index: i, node: child,
698
- localDefaults, roleGuard,
699
- dataSources: dataSources.length > 0 ? dataSources : undefined,
700
- });
701
- }
702
-
703
- // Phase 2: match to routes (two-pass)
704
- const assignments = new Map<number, RouteEntry>();
705
- const usedRoutes = new Set<string>();
706
-
707
- // Pass 1: high-confidence keyword matches
708
- for (const cand of candidates) {
709
- if (!cand.title) continue;
710
- const roleRoutes = routeTable.filter(
711
- r => r.role === cand.role && !usedRoutes.has(r.stateName),
712
- );
713
- let bestRoute: RouteEntry | null = null;
714
- let bestScore = 0;
715
- for (const route of roleRoutes) {
716
- const score = scoreRouteMatch(cand.title, route);
717
- if (score > bestScore) { bestScore = score; bestRoute = route; }
718
- }
719
- if (bestRoute && bestScore >= 3) {
720
- assignments.set(cand.index, bestRoute);
721
- usedRoutes.add(bestRoute.stateName);
722
- }
723
- }
724
-
725
- // Pass 2: iterative elimination — loop until no more assignments made.
726
- // This ensures that after a titled candidate consumes a route, title-less
727
- // candidates that now have only 1 remaining route in their role group get assigned.
728
- let changed = true;
729
- while (changed) {
730
- changed = false;
731
- for (const cand of candidates) {
732
- if (assignments.has(cand.index)) continue;
733
- const remaining = routeTable.filter(
734
- r => r.role === cand.role && !usedRoutes.has(r.stateName),
735
- );
736
- if (remaining.length === 1) {
737
- assignments.set(cand.index, remaining[0]);
738
- usedRoutes.add(remaining[0].stateName);
739
- changed = true;
740
- } else if (remaining.length > 1 && cand.title) {
741
- let bestRoute: RouteEntry | null = null;
742
- let bestScore = 0;
743
- for (const route of remaining) {
744
- const score = scoreRouteMatch(cand.title, route);
745
- if (score > bestScore) { bestScore = score; bestRoute = route; }
746
- }
747
- if (bestRoute && bestScore > 0) {
748
- assignments.set(cand.index, bestRoute);
749
- usedRoutes.add(bestRoute.stateName);
750
- changed = true;
751
- }
752
- }
753
- }
754
- }
755
-
756
- // Phase 3: build PageSection array
757
- for (const cand of candidates) {
758
- const route = assignments.get(cand.index);
759
- const slug = route?.page || titleToSlug(cand.title || `page-${cand.index}`);
760
- const filePath = `app/${cand.role}/${slug}.tsx`;
761
- const componentName = pascalCase(slug) + 'Page';
762
-
763
- pages.push({
764
- route: route?.path || `${cand.role}/${slug}`,
765
- role: cand.role,
766
- slug,
767
- title: cand.title || slug,
768
- componentName,
769
- tree: cand.node,
770
- filePath,
771
- localDefaults: cand.localDefaults,
772
- roleGuard: cand.roleGuard,
773
- dataSources: cand.dataSources,
774
- });
775
- }
776
-
777
- return { pages, childViews };
778
- }
779
-
780
- /**
781
- * Extracts pages from a Router/Route experience tree produced by the compiler.
782
- * Returns null if the experience is not a Router/Route structure.
783
- *
784
- * Router experience layout:
785
- * Stack(blueprint-root)
786
- * Row(nav-bar) → NavLink children
787
- * Router(page-router) → Route children, each wrapping a page tree
788
- */
789
- function extractFromRouterExperience(
790
- experience: IRExperienceNode,
791
- ): { pages: PageSection[]; childViews: IRExperienceNode[] } | null {
792
- if (!experience.children) return null;
793
-
794
- // Find the Router node among experience children
795
- const routerNode = experience.children.find(c => c.component === 'Router');
796
- if (!routerNode?.children) return null;
797
-
798
- // Collect Route children (skip the index/exact duplicate)
799
- const routes = routerNode.children.filter(c => c.component === 'Route');
800
- if (routes.length === 0) return null;
801
-
802
- const pages: PageSection[] = [];
803
- const seenPaths = new Set<string>();
804
-
805
- for (const route of routes) {
806
- const routePath = (route.config?.path as string) || '';
807
- // Skip index route (exact: true, path: '/') — it's a duplicate of the first named route
808
- if (route.config?.exact && routePath === '/') continue;
809
- if (seenPaths.has(routePath)) continue;
810
- seenPaths.add(routePath);
811
-
812
- // Derive slug and role from route path: /setup/review → role=setup, slug=review
813
- // Single-segment routes (e.g. /page) go directly under app/ to avoid the
814
- // compiler's /page suffix stripping creating a path mismatch on round-trip.
815
- const segments = routePath.replace(/^\//, '').split('/').filter(Boolean);
816
- const role = segments.length > 1 ? segments[0] : undefined;
817
- const slug = segments.length > 1 ? segments.slice(1).join('-') : (segments[0] || 'page0');
818
- const filePath = role ? `app/${role}/${slug}.tsx` : `app/${slug}.tsx`;
819
- const componentName = pascalCase(slug) + 'Page';
820
-
821
- // The page content is the Route's child (unwrap the Route wrapper).
822
- // Strip compiler-added gap/padding config that the project-compiler injects
823
- // on Route children (config: { ...child.config, gap: 4, padding: 4 }).
824
- const rawPageTree = route.children?.[0] || route;
825
- const pageTree = { ...rawPageTree };
826
- if (pageTree.config) {
827
- const { gap, padding, ...restConfig } = pageTree.config as Record<string, unknown>;
828
- pageTree.config = Object.keys(restConfig).length > 0 ? restConfig : undefined;
829
- }
830
- const localDefaults = (pageTree.config?.localDefaults || {}) as Record<string, unknown>;
831
-
832
- pages.push({
833
- route: routePath,
834
- role: role || '',
835
- slug,
836
- title: findPageTitle(pageTree) || slug,
837
- componentName,
838
- tree: pageTree,
839
- filePath,
840
- localDefaults,
841
- });
842
- }
843
-
844
- return pages.length > 0 ? { pages, childViews: [] } : null;
845
- }
846
-
847
- /**
848
- * Generates a standalone page component file from a PageSection.
849
- * Converts localDefaults to useState, unwraps the tree, includes data sources.
850
- * When model slugs are referenced via dataSources, emits typed model imports.
851
- */
852
- function generatePageFileFromSection(
853
- page: PageSection,
854
- modelSlugToPath?: Map<string, string>,
855
- ): string {
856
- const fields: IRFieldDefinition[] = Object.entries(page.localDefaults)
857
- .filter(([key]) => !key.includes('.'))
858
- .map(([key, value]) => ({
859
- name: key,
860
- type: inferFieldType(value),
861
- required: false,
862
- default_value: value ?? undefined,
863
- }));
864
-
865
- const contentTree = getPageContentTree(page.tree);
866
-
867
- const metadata: Record<string, unknown> = {};
868
- if (page.dataSources && page.dataSources.length > 0) {
869
- metadata.dataSources = page.dataSources;
870
-
871
- // Build model imports map: slug → relative import path from this page file
872
- if (modelSlugToPath) {
873
- const modelImports: Record<string, string> = {};
874
- for (const ds of page.dataSources) {
875
- const dsObj = ds as Record<string, unknown>;
876
- if (dsObj.type !== 'workflow') continue;
877
- const slug = (dsObj.slug || dsObj.name) as string;
878
- if (!slug) continue;
879
- const modelPath = modelSlugToPath.get(slug);
880
- if (modelPath) {
881
- // Compute relative path from page file to model file
882
- const pageDir = page.filePath.split('/').slice(0, -1).join('/');
883
- const rel = computeRelativeImport(pageDir, modelPath);
884
- modelImports[slug] = rel;
885
- }
886
- }
887
- if (Object.keys(modelImports).length > 0) {
888
- metadata.modelImports = modelImports;
889
- }
890
- }
891
- }
892
-
893
- const pageInput: DecompilerInput = {
894
- slug: page.slug,
895
- name: page.componentName,
896
- version: '1.0.0',
897
- category: 'page',
898
- states: [],
899
- transitions: [],
900
- fields,
901
- roles: [],
902
- experience: contentTree,
903
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
904
- };
905
-
906
- return decompile(pageInput, {
907
- componentName: page.componentName,
908
- includeAnnotation: false,
909
- }).code;
910
- }
911
-
912
- /**
913
- * Computes a relative import path from a source directory to a target file.
914
- * E.g. from "app/shared" to "models/task.ts" → "../../models/task"
915
- */
916
- function computeRelativeImport(fromDir: string, toFile: string): string {
917
- const fromParts = fromDir.split('/').filter(Boolean);
918
- const toParts = toFile.replace(/\.(ts|tsx)$/, '').split('/').filter(Boolean);
919
-
920
- // Find common prefix length
921
- let common = 0;
922
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
923
- common++;
924
- }
925
-
926
- const ups = fromParts.length - common;
927
- const downs = toParts.slice(common);
928
-
929
- if (ups === 0 && downs.length === 0) return './index';
930
- const prefix = ups > 0 ? '../'.repeat(ups) : './';
931
- return prefix + downs.join('/');
932
- }
933
-
934
- /**
935
- * Generates a main.workflow.tsx that imports page components and composes them.
936
- * Keeps global hooks from the definition and child workflow views inline.
937
- */
938
- function generateMainWithPages(
939
- definition: DecompilerInput,
940
- pages: PageSection[],
941
- childViews: IRExperienceNode[],
942
- ): string {
943
- // Build modified experience tree with page component refs
944
- const mainChildren: IRExperienceNode[] = [];
945
-
946
- for (const page of pages) {
947
- if (page.roleGuard) {
948
- mainChildren.push({
949
- id: `page-${page.slug}`,
950
- component: 'Show',
951
- bindings: { when: `$instance.${page.roleGuard}` },
952
- children: [{ id: `${page.slug}-ref`, component: page.componentName }],
953
- });
954
- } else {
955
- mainChildren.push({
956
- id: `page-${page.slug}`,
957
- component: page.componentName,
958
- });
959
- }
960
- }
961
-
962
- for (const cv of childViews) {
963
- mainChildren.push(cv);
964
- }
965
-
966
- const mainTree: IRExperienceNode = {
967
- id: 'root',
968
- component: 'Stack',
969
- children: mainChildren,
970
- };
971
-
972
- // Strip dataSources from metadata for the main file — they belong to
973
- // individual page files, not the top-level orchestrator. Including them here
974
- // causes import instability (useMutation/useQuery present on pass 1 but not pass 2).
975
- const strippedMeta = { ...(definition.metadata || {}) };
976
- delete (strippedMeta as any).dataSources;
977
- delete (strippedMeta as any).queries;
978
- delete (strippedMeta as any).mutations;
979
- delete (strippedMeta as any).mutationTargets;
980
- const modifiedDef: DecompilerInput = {
981
- ...definition,
982
- experience: mainTree,
983
- metadata: strippedMeta,
984
- };
985
- const result = decompile(modifiedDef, { includeAnnotation: true });
986
-
987
- // Build import lines for page components
988
- const pageImports = pages
989
- .map(p => `import ${p.componentName} from './${p.filePath.replace(/\.tsx$/, '')}';`)
990
- .join('\n');
991
-
992
- // Insert after last existing import line
993
- const code = result.code;
994
- const importRegex = /^import .+$/gm;
995
- let lastImportEnd = -1;
996
- let m: RegExpExecArray | null;
997
- while ((m = importRegex.exec(code)) !== null) {
998
- lastImportEnd = m.index + m[0].length;
999
- }
1000
-
1001
- if (lastImportEnd > 0) {
1002
- return code.slice(0, lastImportEnd) + '\n' + pageImports + code.slice(lastImportEnd);
1003
- }
1004
- return pageImports + '\n' + code;
1005
- }
1006
-
1007
- // =============================================================================
1008
- // Server Action Extraction
1009
- // =============================================================================
1010
-
1011
- interface ServerAction {
1012
- name: string;
1013
- type: string;
1014
- config: Record<string, unknown>;
1015
- }
1016
-
1017
- const SERVER_ACTION_TYPES = new Set([
1018
- 'http_request', 'webhook', 'call_webhook',
1019
- 'notify', 'send_notification',
1020
- 'call_workflow', 'spawn_instance', 'spawn_subworkflow',
1021
- 'emit_event',
1022
- 'custom',
1023
- ]);
1024
-
1025
- function extractServerActions(
1026
- states: IRStateDefinition[],
1027
- transitions: IRTransitionDefinition[],
1028
- ): ServerAction[] {
1029
- const seen = new Set<string>();
1030
- const actions: ServerAction[] = [];
1031
-
1032
- function collect(defs: Array<{ id: string; type: string; config: Record<string, unknown> }>) {
1033
- for (const action of defs) {
1034
- if (SERVER_ACTION_TYPES.has(action.type) && !seen.has(action.id)) {
1035
- seen.add(action.id);
1036
- const slug = String(action.config.name || action.config.event || action.config.slug || action.type);
1037
- const name = camelCase(slug.replace(/[^a-zA-Z0-9_-]/g, '-'));
1038
- actions.push({ name, type: action.type, config: action.config });
1039
- }
1040
- }
1041
- }
1042
-
1043
- for (const state of states) {
1044
- collect(state.on_enter);
1045
- collect(state.on_exit);
1046
- for (const during of state.during) {
1047
- collect(during.actions);
1048
- }
1049
- }
1050
- for (const trans of transitions) {
1051
- collect(trans.actions);
1052
- }
1053
-
1054
- return actions;
1055
- }
1056
-
1057
- function generateServerActionFile(
1058
- slug: string,
1059
- actions: ServerAction[],
1060
- ): string {
1061
- const lines: string[] = [
1062
- `/**`,
1063
- ` * Server actions for "${slug}".`,
1064
- ` *`,
1065
- ` * These run server-side during state transitions.`,
1066
- ` */`,
1067
- ``,
1068
- `import type { TransitionContext } from '@mindmatrix/react';`,
1069
- ``,
1070
- ];
1071
-
1072
- for (const action of actions) {
1073
- lines.push(`/** ${actionComment(action)} */`);
1074
- lines.push(`export async function ${action.name}(ctx: TransitionContext): Promise<void> {`);
1075
- lines.push(` const { instance, env } = ctx;`);
1076
-
1077
- switch (action.type) {
1078
- case 'http_request': case 'webhook': case 'call_webhook': {
1079
- const url = String(action.config.url || 'https://api.example.com/webhook');
1080
- const method = String(action.config.method || 'POST');
1081
- lines.push(` await fetch('${esc(url)}', {`);
1082
- lines.push(` method: '${method}',`);
1083
- lines.push(` headers: { 'Content-Type': 'application/json' },`);
1084
- lines.push(` body: JSON.stringify({ instanceId: instance.id }),`);
1085
- lines.push(` });`);
1086
- break;
1087
- }
1088
- case 'notify': case 'send_notification': {
1089
- const msg = String(action.config.message || action.config.body || 'Notification');
1090
- lines.push(` await env.notify({ message: '${esc(msg)}', instanceId: instance.id });`);
1091
- break;
1092
- }
1093
- case 'call_workflow': case 'spawn_instance': case 'spawn_subworkflow': {
1094
- const target = String(action.config.slug || action.config.workflow || 'child-workflow');
1095
- lines.push(` await env.spawn('${esc(target)}', { parentId: instance.id });`);
1096
- break;
1097
- }
1098
- case 'emit_event': {
1099
- const event = String(action.config.event || action.config.name || 'custom-event');
1100
- lines.push(` await env.emit('${esc(event)}', { instanceId: instance.id });`);
1101
- break;
1102
- }
1103
- case 'custom': {
1104
- const expr = action.config.expression || action.config.body;
1105
- if (expr) {
1106
- lines.push(` // Original expression: ${String(expr).replace(/\n/g, '\\n').slice(0, 200)}`);
1107
- }
1108
- lines.push(` // TODO: Implement custom action logic`);
1109
- lines.push(` throw new Error('Not implemented — decompiled stub');`);
1110
- break;
1111
- }
1112
- default:
1113
- lines.push(` // TODO: Implement ${action.type} logic`);
1114
- }
1115
-
1116
- lines.push(`}`);
1117
- lines.push(``);
1118
- }
1119
-
1120
- return lines.join('\n');
1121
- }
1122
-
1123
- function actionComment(action: ServerAction): string {
1124
- switch (action.type) {
1125
- case 'http_request': case 'webhook': case 'call_webhook':
1126
- return `HTTP ${action.config.method || 'POST'} to ${action.config.url || 'webhook'}`;
1127
- case 'notify': case 'send_notification':
1128
- return `Send notification: ${action.config.message || ''}`;
1129
- case 'call_workflow': case 'spawn_instance': case 'spawn_subworkflow':
1130
- return `Spawn workflow: ${action.config.slug || action.config.workflow || ''}`;
1131
- case 'emit_event':
1132
- return `Emit event: ${action.config.event || action.config.name || ''}`;
1133
- case 'custom':
1134
- return `Custom action: ${action.name}`;
1135
- default:
1136
- return `Server action: ${action.type}`;
1137
- }
1138
- }
1139
-
1140
- // =============================================================================
1141
- // Component Definition Emission (from metadata.componentDefinitions)
1142
- // =============================================================================
1143
-
1144
- /**
1145
- * Emits component files from metadata.componentDefinitions.
1146
- * Each entry maps a component name to its experience tree and props.
1147
- */
1148
- function emitComponentDefinitions(
1149
- definition: DecompilerInput,
1150
- files: EnhancedProjectFile[],
1151
- ): void {
1152
- const meta = definition.metadata as Record<string, unknown> | undefined;
1153
- const componentDefs = meta?.componentDefinitions as
1154
- Record<string, { experience: IRExperienceNode; props: string[] }> | undefined;
1155
- if (!componentDefs) return;
1156
-
1157
- for (const [name, def] of Object.entries(componentDefs)) {
1158
- const content = generateComponentFromDefinition(name, def.experience, def.props);
1159
- files.push({
1160
- path: `components/${name}.tsx`,
1161
- role: 'component',
1162
- content,
1163
- });
1164
- }
1165
- }
1166
-
1167
- /**
1168
- * Infers a TypeScript type for a component prop by analyzing how it's used
1169
- * in the experience tree (bindings, visible_when, event handlers).
1170
- */
1171
- function inferPropType(propName: string, experience: IRExperienceNode): string {
1172
- // Walk the tree to find usages
1173
- const usages: string[] = [];
1174
- collectPropUsages(propName, experience, usages);
1175
-
1176
- // Event handlers → callback types
1177
- if (propName.startsWith('on') && propName.length > 2 && propName[2] === propName[2].toUpperCase()) {
1178
- return '(() => void)';
1179
- }
1180
-
1181
- // Boolean-like names
1182
- const boolNames = ['show', 'is', 'has', 'enable', 'disable', 'compact', 'track', 'follow'];
1183
- if (boolNames.some(b => propName.toLowerCase().startsWith(b))) return 'boolean';
1184
-
1185
- // Known patterns from bindings
1186
- for (const usage of usages) {
1187
- if (usage.includes('.address') || usage.includes('Address')) return '{ address: string; latitude: number; longitude: number }';
1188
- if (usage.includes('.toFixed')) return 'number';
1189
- if (usage.includes('.toLocaleString')) return 'number';
1190
- if (usage.includes('.heading')) return '{ latitude: number; longitude: number; heading: number }';
1191
- if (usage.includes('.length')) return 'unknown[]';
1192
- }
1193
-
1194
- // Name-based heuristics
1195
- if (/url|src|image|avatar|icon/i.test(propName)) return 'string';
1196
- if (/count|amount|total|rating|eta|minutes|radius|zoom/i.test(propName)) return 'number';
1197
- if (/style|sx/i.test(propName)) return 'React.CSSProperties';
1198
- if (/items|list|zones|options/i.test(propName)) return 'unknown[]';
1199
- if (/location|position|point/i.test(propName)) return '{ latitude: number; longitude: number }';
1200
-
1201
- return 'unknown';
1202
- }
1203
-
1204
- /** Collects binding expressions that reference a prop name. */
1205
- function collectPropUsages(propName: string, node: IRExperienceNode, out: string[]): void {
1206
- if (node.bindings) {
1207
- for (const expr of Object.values(node.bindings)) {
1208
- if (typeof expr === 'string' && expr.includes(propName)) {
1209
- out.push(expr);
1210
- }
1211
- }
1212
- }
1213
- if (node.visible_when && typeof node.visible_when === 'string' && node.visible_when.includes(propName)) {
1214
- out.push(node.visible_when);
1215
- }
1216
- if (node.children) {
1217
- for (const child of node.children) {
1218
- collectPropUsages(propName, child, out);
1219
- }
1220
- }
1221
- }
1222
-
1223
- /**
1224
- * Generates a component file from an experience tree and prop list.
1225
- * Produces proper TypeScript interfaces with inferred prop types and
1226
- * destructured props in the function signature.
1227
- */
1228
- function generateComponentFromDefinition(
1229
- name: string,
1230
- experience: IRExperienceNode,
1231
- props: string[],
1232
- ): string {
1233
- // Decompile the experience tree into JSX
1234
- const pageInput: DecompilerInput = {
1235
- slug: name.toLowerCase(),
1236
- name,
1237
- version: '1.0.0',
1238
- category: 'component',
1239
- states: [],
1240
- transitions: [],
1241
- fields: [],
1242
- roles: [],
1243
- experience,
1244
- };
1245
-
1246
- const result = decompile(pageInput, {
1247
- componentName: name,
1248
- includeAnnotation: false,
1249
- });
1250
-
1251
- // If the component has props, inject a typed interface and destructured params
1252
- if (props.length > 0) {
1253
- const typedProps = props.map(p => {
1254
- const type = inferPropType(p, experience);
1255
- const isCallback = p.startsWith('on') && p.length > 2 && p[2] === p[2].toUpperCase();
1256
- const optional = isCallback ? '?' : '';
1257
- return ` ${p}${optional}: ${type};`;
1258
- });
1259
-
1260
- const propsInterface = `interface ${name}Props {\n${typedProps.join('\n')}\n}\n\n`;
1261
- const code = result.code
1262
- .replace(
1263
- `export default function ${name}()`,
1264
- `${propsInterface}export default function ${name}({ ${props.join(', ')} }: ${name}Props)`,
1265
- );
1266
- return code;
1267
- }
1268
-
1269
- return result.code;
1270
- }
1271
-
1272
- // =============================================================================
1273
- // Server Action Emission from Metadata (preserved bodies)
1274
- // =============================================================================
1275
-
1276
- /**
1277
- * Emits server action files from metadata.serverActions when they have
1278
- * preserved function bodies. Returns the list of emitted file paths.
1279
- */
1280
- function emitServerActionsFromMetadata(
1281
- definition: DecompilerInput,
1282
- files: EnhancedProjectFile[],
1283
- ): string[] {
1284
- const meta = definition.metadata as Record<string, unknown> | undefined;
1285
- const serverActions = meta?.serverActions as Array<{
1286
- name: string;
1287
- sourceFile?: string;
1288
- body?: string;
1289
- async?: boolean;
1290
- params?: string[];
1291
- description?: string;
1292
- returnType?: string;
1293
- }> | undefined;
1294
-
1295
- if (!serverActions || serverActions.length === 0) return [];
1296
-
1297
- // Only emit if at least one action has a preserved body
1298
- const actionsWithBodies = serverActions.filter(a => a.body);
1299
- if (actionsWithBodies.length === 0) return [];
1300
-
1301
- // Group actions by source file
1302
- const byFile = new Map<string, typeof actionsWithBodies>();
1303
- for (const action of actionsWithBodies) {
1304
- const file = action.sourceFile || `actions/${action.name}.server.ts`;
1305
- if (!byFile.has(file)) byFile.set(file, []);
1306
- byFile.get(file)!.push(action);
1307
- }
1308
-
1309
- const emittedPaths: string[] = [];
1310
- for (const [filePath, actions] of byFile) {
1311
- const content = generateServerActionFileFromBodies(filePath, actions);
1312
- // Check if a file at this path was already emitted by extractServerActions
1313
- const existing = files.findIndex(f => f.path === filePath);
1314
- if (existing !== -1) {
1315
- // Replace the generated stub with the preserved bodies
1316
- files[existing].content = content;
1317
- } else {
1318
- files.push({ path: filePath, role: 'server-action', content });
1319
- }
1320
- emittedPaths.push(filePath);
1321
- }
1322
-
1323
- return emittedPaths;
1324
- }
1325
-
1326
- /**
1327
- * Generates a server action file from preserved function bodies.
1328
- * Includes the standard import header and each function's full source.
1329
- */
1330
- function generateServerActionFileFromBodies(
1331
- filePath: string,
1332
- actions: Array<{
1333
- name: string;
1334
- body?: string;
1335
- description?: string;
1336
- }>,
1337
- ): string {
1338
- const lines: string[] = [
1339
- `/**`,
1340
- ` * Server actions — ${filePath}`,
1341
- ` *`,
1342
- ` * Auto-generated from preserved function bodies.`,
1343
- ` */`,
1344
- ``,
1345
- `import type { TransitionContext, ActionResult } from '@mindmatrix/react';`,
1346
- ``,
1347
- ];
1348
-
1349
- for (const action of actions) {
1350
- if (action.body) {
1351
- lines.push(action.body);
1352
- lines.push(``);
1353
- }
1354
- }
1355
-
1356
- return lines.join('\n');
1357
- }
1358
-
1359
- // =============================================================================
1360
- // Router Transition Reduction
1361
- // =============================================================================
1362
-
1363
- /**
1364
- * Reduces O(n²) router transitions to only meaningful navigation paths:
1365
- * 1. Intra-role transitions (RIDER_HOME → RIDER_HISTORY — same role prefix)
1366
- * 2. Cross-role home transitions (RIDER_HOME → DRIVER_DASHBOARD — between "home" states)
1367
- *
1368
- * This preserves realistic navigation while removing the every-to-every explosion.
1369
- */
1370
- function reduceRouterTransitions(
1371
- transitions: IRTransitionDefinition[],
1372
- states: IRStateDefinition[],
1373
- ): IRTransitionDefinition[] {
1374
- if (transitions.length === 0) return transitions;
1375
-
1376
- // Extract role prefixes from state names (e.g., RIDER, DRIVER, ADMIN)
1377
- const stateNames = new Set(states.map(s => s.name));
1378
- const roleGroups = new Map<string, string[]>();
1379
-
1380
- for (const state of states) {
1381
- const parts = state.name.split('_');
1382
- // Role prefix is everything before the last segment (e.g., RIDER from RIDER_HOME)
1383
- // For states like ADMIN_SURGE_PRICING, group by first segment
1384
- const role = parts[0];
1385
- if (!roleGroups.has(role)) roleGroups.set(role, []);
1386
- roleGroups.get(role)!.push(state.name);
1387
- }
1388
-
1389
- // Identify "home" states per role (first state in each role group, or ones with HOME/DASHBOARD)
1390
- const homeStates = new Set<string>();
1391
- for (const [, group] of roleGroups) {
1392
- const home = group.find(s =>
1393
- s.includes('HOME') || s.includes('DASHBOARD') || s.includes('ANALYTICS')
1394
- ) || group[0];
1395
- homeStates.add(home);
1396
- }
1397
-
1398
- // Keep transitions that are:
1399
- // 1. Intra-role (same role prefix)
1400
- // 2. Between home states of different roles
1401
- const kept: IRTransitionDefinition[] = [];
1402
- const seenKeys = new Set<string>();
1403
-
1404
- for (const t of transitions) {
1405
- const fromArr = Array.isArray(t.from) ? t.from : [t.from as unknown as string];
1406
- const fromState = fromArr[0];
1407
- const toState = t.to;
1408
-
1409
- if (!fromState || !stateNames.has(fromState) || !stateNames.has(toState)) continue;
1410
-
1411
- const fromRole = fromState.split('_')[0];
1412
- const toRole = toState.split('_')[0];
1413
- const key = `${fromState}→${toState}`;
1414
-
1415
- if (seenKeys.has(key)) continue;
1416
-
1417
- // Rule 1: same role group
1418
- const sameRole = fromRole === toRole;
1419
-
1420
- // Rule 2: from a home state to another home state
1421
- const crossRoleHome = homeStates.has(fromState) && homeStates.has(toState);
1422
-
1423
- if (sameRole || crossRoleHome) {
1424
- seenKeys.add(key);
1425
- kept.push(t);
1426
- }
1427
- }
1428
-
1429
- return kept;
1430
- }
1431
-
1432
- // =============================================================================
1433
- // Main: decompileProjectEnhanced
1434
- // =============================================================================
1435
-
1436
- /**
1437
- * Enhanced project decompiler that uses SplitStrategy to determine output
1438
- * complexity and generates a complete multi-file project.
1439
- *
1440
- * @param definition - The full workflow definition to decompile.
1441
- * @returns Files, entry path, slug, and the split decision used.
1442
- */
1443
- export function decompileProjectEnhanced(
1444
- definition: DecompilerInput,
1445
- ): EnhancedDecompileResult {
1446
- const slug = definition.slug;
1447
- const decision = determineSplitStrategy(definition);
1448
- const files: EnhancedProjectFile[] = [];
1449
- const modelPaths: string[] = [];
1450
- const entryPaths: string[] = [];
1451
- const actionPaths: string[] = [];
1452
-
1453
- // --- Single file mode ---
1454
- if (decision.tier === 'single') {
1455
- const result = decompile(definition);
1456
- files.push({
1457
- path: 'main.workflow.tsx',
1458
- role: 'view-entry',
1459
- content: result.code,
1460
- });
1461
- // Even in single-file mode, emit components and actions if present in metadata
1462
- emitComponentDefinitions(definition, files);
1463
- emitServerActionsFromMetadata(definition, files);
1464
- return { files, entryFile: 'main.workflow.tsx', slug, splitDecision: decision };
1465
- }
1466
-
1467
- // --- Model file ---
1468
- if (decision.emitModels && definition.fields.length > 0) {
1469
- const modelPath = `models/${slug}.ts`;
1470
- const modelContent = generateModelFile(
1471
- slug,
1472
- definition.fields,
1473
- definition.states,
1474
- definition.transitions,
1475
- { version: definition.version, category: Array.isArray(definition.category) ? definition.category[0] : definition.category, description: definition.description },
1476
- );
1477
- files.push({ path: modelPath, role: 'model', content: modelContent });
1478
- modelPaths.push(modelPath);
1479
- }
1480
-
1481
- // --- Server actions ---
1482
- if (decision.emitActions) {
1483
- const serverActions = extractServerActions(definition.states, definition.transitions);
1484
- if (serverActions.length > 0) {
1485
- const actionPath = `actions/${slug}.server.ts`;
1486
- const actionContent = generateServerActionFile(slug, serverActions);
1487
- files.push({ path: actionPath, role: 'server-action', content: actionContent });
1488
- actionPaths.push(actionPath);
1489
- }
1490
- }
1491
-
1492
- // --- Pages from experience tree (role-based split) ---
1493
- let pagesExtracted = false;
1494
- let extractedPages: PageSection[] = [];
1495
- let extractedChildViews: IRExperienceNode[] = [];
1496
- // Track whether pages were extracted from an existing Router/Route structure.
1497
- // If so, the router child definition is compiler-generated and should be skipped.
1498
- let routerIsCompilerGenerated = false;
1499
- if (decision.emitPages && definition.experience) {
1500
- // Check if Router/Route structure exists (indicates compiler-generated router)
1501
- const routerNode = definition.experience.children?.find(
1502
- (c: IRExperienceNode) => c.component === 'Router',
1503
- );
1504
- routerIsCompilerGenerated = !!routerNode;
1505
-
1506
- const { pages, childViews } = extractPageSections(
1507
- definition.experience,
1508
- definition.childDefinitions,
1509
- );
1510
- if (pages.length > 0) {
1511
- pagesExtracted = true;
1512
- extractedPages = pages;
1513
- extractedChildViews = childViews;
1514
-
1515
- // Build slug → model path map for type-safe imports
1516
- const modelSlugToPath = new Map<string, string>();
1517
- modelSlugToPath.set(slug, `models/${slug}.ts`);
1518
- if (definition.childDefinitions) {
1519
- for (const child of definition.childDefinitions) {
1520
- const cs = child.slug;
1521
- if (!cs.endsWith('-router') && child.category !== 'router') {
1522
- modelSlugToPath.set(cs, `models/${cs}.ts`);
1523
- }
1524
- }
1525
- }
1526
-
1527
- for (const page of pages) {
1528
- const pageContent = generatePageFileFromSection(page, modelSlugToPath);
1529
- files.push({ path: page.filePath, role: 'page', content: pageContent });
1530
- }
1531
- }
1532
- }
1533
-
1534
- // --- Layout with auth guards ---
1535
- if (definition.roles && definition.roles.length > 0 && decision.tier === 'large') {
1536
- const layoutContent = generateLayoutFile(slug, definition.roles);
1537
- files.push({ path: 'app/layout.tsx', role: 'layout', content: layoutContent });
1538
- }
1539
-
1540
- // --- Child definitions (blueprint support) ---
1541
- // Merge child definitions by slug (compiler may produce duplicates when
1542
- // both .workflow.tsx and models/*.ts share the same slug). Merge fields,
1543
- // states, transitions — required=true wins, non-empty default_value wins.
1544
- // Also skip: router definitions generated from page files, and children whose
1545
- // slug matches the parent (they are the parent, not a child).
1546
- const emittedModelSlugs = new Set(modelPaths.map(p => p.replace('models/', '').replace('.ts', '')));
1547
- const mergedChildren = new Map<string, DecompilerInput>();
1548
- if (definition.childDefinitions && definition.childDefinitions.length > 0) {
1549
- for (const child of definition.childDefinitions) {
1550
- const childSlug = child.slug;
1551
- if (childSlug === slug) continue; // Skip parent-slug children
1552
- const isRouter = childSlug.endsWith('-router') || child.category === 'router';
1553
- if (isRouter && routerIsCompilerGenerated) continue;
1554
-
1555
- if (!mergedChildren.has(childSlug)) {
1556
- mergedChildren.set(childSlug, { ...child });
1557
- } else {
1558
- // Merge: fields (required + default_value wins), states, transitions
1559
- const existing = mergedChildren.get(childSlug)!;
1560
- // Merge fields: merge metadata for existing fields, add new ones
1561
- const fieldMap = new Map(existing.fields.map(f => [f.name, f]));
1562
- for (const f of child.fields) {
1563
- if (fieldMap.has(f.name)) {
1564
- const ef = fieldMap.get(f.name)!;
1565
- if (f.required && !ef.required) ef.required = true;
1566
- if (f.default_value != null && (ef.default_value == null || ef.default_value === '' || ef.default_value === 0 || ef.default_value === false)) {
1567
- ef.default_value = f.default_value;
1568
- }
1569
- } else {
1570
- existing.fields.push(f);
1571
- fieldMap.set(f.name, f);
1572
- }
1573
- }
1574
- // Merge states: if a later child has MORE states, prefer its complete set
1575
- // (model IRs have explicit states arrays, workflow IRs only have inferred states)
1576
- if (child.states.length > existing.states.length) {
1577
- // Later child has more states — use its set as the base
1578
- const existingByName = new Map(existing.states.map(s => [s.name, s]));
1579
- existing.states = child.states.map(s => {
1580
- // Preserve actions from existing state if any
1581
- const ex = existingByName.get(s.name);
1582
- if (ex && (ex.on_enter.length || ex.on_exit.length || ex.during.length)) {
1583
- return { ...s, on_enter: ex.on_enter, on_exit: ex.on_exit, during: ex.during };
1584
- }
1585
- return s;
1586
- });
1587
- // Add non-default states from existing that aren't in the child.
1588
- // Skip 'draft' if the child already has a START state (it's a compiler default).
1589
- const childNames = new Set(child.states.map(s => s.name));
1590
- const hasStart = child.states.some(s => s.type === 'START');
1591
- for (const s of existingByName.values()) {
1592
- if (!childNames.has(s.name)) {
1593
- if (s.name === 'draft' && s.type === 'START' && hasStart) continue;
1594
- existing.states.push(s);
1595
- }
1596
- }
1597
- } else {
1598
- // Just add missing states
1599
- const stateNames = new Set(existing.states.map(s => s.name));
1600
- for (const s of child.states) {
1601
- if (!stateNames.has(s.name)) {
1602
- existing.states.push(s);
1603
- stateNames.add(s.name);
1604
- }
1605
- }
1606
- }
1607
- // Merge transitions (add missing)
1608
- const transNames = new Set(existing.transitions.map(t => t.name));
1609
- for (const t of child.transitions) {
1610
- if (!transNames.has(t.name)) {
1611
- existing.transitions.push(t);
1612
- transNames.add(t.name);
1613
- }
1614
- }
1615
- // Prefer non-default version/category/description
1616
- if (child.version && child.version !== '0.1.0' && (!existing.version || existing.version === '0.1.0')) {
1617
- existing.version = child.version;
1618
- }
1619
- if (child.category && child.category !== 'data' && (!existing.category || existing.category === 'data')) {
1620
- existing.category = child.category;
1621
- }
1622
- if (child.description && !existing.description) {
1623
- existing.description = child.description;
1624
- }
1625
- }
1626
- }
1627
- }
1628
-
1629
- for (const [childSlug, child] of mergedChildren) {
1630
- const isRouter = childSlug.endsWith('-router') || child.category === 'router';
1631
-
1632
- // For routers, reduce O(n²) transitions to adjacent/related routes
1633
- const childTransitions = isRouter
1634
- ? reduceRouterTransitions(child.transitions, child.states)
1635
- : child.transitions;
1636
-
1637
- // Child model — skip if parent already emitted a model for this slug
1638
- if (child.fields.length > 0 && !emittedModelSlugs.has(childSlug)) {
1639
- const childModelPath = `models/${childSlug}.ts`;
1640
- files.push({
1641
- path: childModelPath,
1642
- role: 'model',
1643
- content: generateModelFile(childSlug, child.fields, child.states, childTransitions,
1644
- { version: child.version, category: Array.isArray(child.category) ? child.category[0] : child.category, description: child.description }),
1645
- });
1646
- modelPaths.push(childModelPath);
1647
- emittedModelSlugs.add(childSlug);
1648
- }
1649
-
1650
- // Child workflow file — preserve parent version/category if child uses defaults
1651
- const childInput: DecompilerInput = {
1652
- ...child,
1653
- version: child.version || definition.version,
1654
- category: child.category || definition.category,
1655
- transitions: childTransitions,
1656
- experience: ((child as unknown as Record<string, unknown>).views as Record<string, unknown> | undefined)?.default as IRExperienceNode | undefined,
1657
- };
1658
- const childResult = decompile(childInput, {
1659
- componentName: pascalCase(childSlug),
1660
- includeAnnotation: true,
1661
- });
1662
- const childPath = `${childSlug}.workflow.tsx`;
1663
- files.push({ path: childPath, role: 'view-entry', content: childResult.code });
1664
- entryPaths.push(childPath);
1665
- }
1666
-
1667
- // --- Component definitions from metadata ---
1668
- emitComponentDefinitions(definition, files);
1669
-
1670
- // --- Server actions with preserved bodies from metadata ---
1671
- const emittedActionPaths = emitServerActionsFromMetadata(definition, files);
1672
- actionPaths.push(...emittedActionPaths);
1673
-
1674
- // --- Main entry file ---
1675
- const mainPath = 'main.workflow.tsx';
1676
- if (pagesExtracted) {
1677
- const mainContent = generateMainWithPages(definition, extractedPages, extractedChildViews);
1678
- files.push({ path: mainPath, role: 'view-entry', content: mainContent });
1679
- } else {
1680
- const mainResult = decompile(definition);
1681
- files.push({ path: mainPath, role: 'view-entry', content: mainResult.code });
1682
- }
1683
- entryPaths.push(mainPath);
1684
-
1685
- // --- mm.config.ts ---
1686
- if (decision.emitConfig) {
1687
- // Deduplicate paths before config generation (second pass may add duplicates)
1688
- const configData = extractConfigData(
1689
- definition,
1690
- [...new Set(modelPaths)],
1691
- [...new Set(entryPaths)],
1692
- [...new Set(actionPaths)],
1693
- );
1694
- const configContent = generateMmConfig(configData);
1695
- files.push({ path: 'mm.config.ts', role: 'config', content: configContent });
1696
- }
1697
-
1698
- // --- package.json ---
1699
- files.push({
1700
- path: 'package.json',
1701
- role: 'config',
1702
- content: generatePackageJson(definition),
1703
- });
1704
-
1705
- // --- tsconfig.json ---
1706
- files.push({
1707
- path: 'tsconfig.json',
1708
- role: 'config',
1709
- content: TSCONFIG_TEMPLATE,
1710
- });
1711
-
1712
- return {
1713
- files,
1714
- entryFile: mainPath,
1715
- slug,
1716
- splitDecision: decision,
1717
- };
1718
- }
1719
-
1720
- // =============================================================================
1721
- // Package scaffolding
1722
- // =============================================================================
1723
-
1724
- const TSCONFIG_TEMPLATE = JSON.stringify({
1725
- compilerOptions: {
1726
- target: 'ES2022',
1727
- module: 'ESNext',
1728
- moduleResolution: 'bundler',
1729
- jsx: 'react-jsx',
1730
- strict: true,
1731
- esModuleInterop: true,
1732
- skipLibCheck: true,
1733
- forceConsistentCasingInFileNames: true,
1734
- declaration: true,
1735
- sourceMap: true,
1736
- outDir: 'dist',
1737
- rootDir: '.',
1738
- baseUrl: '.',
1739
- paths: { '@/*': ['./*'] },
1740
- },
1741
- include: ['**/*.ts', '**/*.tsx'],
1742
- exclude: ['node_modules', 'dist'],
1743
- }, null, 2) + '\n';
1744
-
1745
- function generatePackageJson(def: DecompilerInput): string {
1746
- const pkg = {
1747
- name: `@mindmatrix/blueprint-${def.slug}`,
1748
- version: def.version || '1.0.0',
1749
- description: def.description || '',
1750
- type: 'module',
1751
- main: 'main.workflow.tsx',
1752
- scripts: {
1753
- 'type-check': 'tsc --noEmit',
1754
- build: 'mmrc build --src .',
1755
- deploy: 'mmrc deploy --build --src .',
1756
- },
1757
- peerDependencies: {
1758
- react: '>=18.0.0',
1759
- '@mindmatrix/react': 'workspace:*',
1760
- },
1761
- devDependencies: {
1762
- '@mindmatrix/react-compiler': 'workspace:*',
1763
- '@types/react': '^19.0.0',
1764
- typescript: '^5.4.0',
1765
- },
1766
- };
1767
- return JSON.stringify(pkg, null, 2) + '\n';
1768
- }
1769
-
1770
- // =============================================================================
1771
- // Helpers
1772
- // =============================================================================
1773
-
1774
- function esc(s: string): string {
1775
- return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1776
- }