@specverse/engines 4.1.30 → 4.2.1

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 (285) hide show
  1. package/assets/examples/manifests/frontend-only.yaml +3 -6
  2. package/assets/examples/manifests/fullstack-app.yaml +5 -7
  3. package/assets/examples/manifests/fullstack-monorepo.yaml +3 -6
  4. package/assets/templates/default/specs/main.specly +65 -0
  5. package/dist/inference/comprehensive-engine.d.ts.map +1 -1
  6. package/dist/inference/comprehensive-engine.js +3 -19
  7. package/dist/inference/comprehensive-engine.js.map +1 -1
  8. package/dist/inference/core/rule-engine.d.ts +31 -0
  9. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  10. package/dist/inference/core/rule-engine.js +117 -33
  11. package/dist/inference/core/rule-engine.js.map +1 -1
  12. package/dist/inference/core/rule-file-types.d.ts +0 -2
  13. package/dist/inference/core/rule-file-types.d.ts.map +1 -1
  14. package/dist/inference/core/rule-file-types.js +3 -6
  15. package/dist/inference/core/rule-file-types.js.map +1 -1
  16. package/dist/inference/core/rule-loader.d.ts +5 -15
  17. package/dist/inference/core/rule-loader.d.ts.map +1 -1
  18. package/dist/inference/core/rule-loader.js +43 -132
  19. package/dist/inference/core/rule-loader.js.map +1 -1
  20. package/dist/inference/core/types.d.ts +0 -6
  21. package/dist/inference/core/types.d.ts.map +1 -1
  22. package/dist/inference/core/types.js +0 -4
  23. package/dist/inference/core/types.js.map +1 -1
  24. package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
  25. package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
  26. package/dist/inference/logical/generators/component-type-resolver.js +0 -19
  27. package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
  28. package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
  29. package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
  30. package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
  31. package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
  32. package/dist/inference/logical/generators/view-generator.d.ts +4 -14
  33. package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
  34. package/dist/inference/logical/generators/view-generator.js +6 -26
  35. package/dist/inference/logical/generators/view-generator.js.map +1 -1
  36. package/dist/inference/logical/index.d.ts +2 -2
  37. package/dist/inference/logical/index.d.ts.map +1 -1
  38. package/dist/inference/logical/logical-engine.d.ts.map +1 -1
  39. package/dist/inference/logical/logical-engine.js +17 -80
  40. package/dist/inference/logical/logical-engine.js.map +1 -1
  41. package/dist/inference/quint-transpiler.d.ts +5 -3
  42. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  43. package/dist/inference/quint-transpiler.js +11 -6
  44. package/dist/inference/quint-transpiler.js.map +1 -1
  45. package/dist/libs/instance-factories/CURVED-INTERFACE.md +278 -0
  46. package/dist/libs/instance-factories/README.md +73 -0
  47. package/dist/libs/instance-factories/applications/README.md +51 -0
  48. package/dist/libs/instance-factories/applications/generic-app.yaml +52 -0
  49. package/{libs/instance-factories/applications/react-app.yaml → dist/libs/instance-factories/applications/react-app-runtime.yaml} +30 -77
  50. package/dist/libs/instance-factories/applications/react-app-starter.yaml +143 -0
  51. package/dist/libs/instance-factories/applications/templates/react/env-example-generator.js +24 -2
  52. package/dist/libs/instance-factories/applications/templates/react/vite-config-generator.js +54 -33
  53. package/dist/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  54. package/dist/libs/instance-factories/applications/templates/react-starter/api-types-starter-generator.js +69 -0
  55. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
  56. package/dist/libs/instance-factories/applications/templates/react-starter/belongs-to.js +40 -0
  57. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +129 -0
  58. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +80 -0
  59. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +217 -0
  60. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +51 -0
  61. package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
  62. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +56 -0
  63. package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +41 -0
  64. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
  65. package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
  66. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +49 -0
  67. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +96 -0
  68. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +116 -0
  69. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +74 -0
  70. package/dist/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.js +95 -0
  71. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +81 -0
  72. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
  73. package/dist/libs/instance-factories/archived/fastify-prisma.yaml +104 -0
  74. package/dist/libs/instance-factories/cli/README.md +43 -0
  75. package/dist/libs/instance-factories/cli/commander-js.yaml +55 -0
  76. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +63 -12
  77. package/dist/libs/instance-factories/communication/README.md +47 -0
  78. package/dist/libs/instance-factories/communication/event-emitter.yaml +60 -0
  79. package/dist/libs/instance-factories/communication/rabbitmq-events.yaml +87 -0
  80. package/dist/libs/instance-factories/controllers/README.md +42 -0
  81. package/dist/libs/instance-factories/controllers/fastify.yaml +139 -0
  82. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +29 -2
  83. package/dist/libs/instance-factories/infrastructure/README.md +29 -0
  84. package/dist/libs/instance-factories/infrastructure/docker-k8s.yaml +61 -0
  85. package/dist/libs/instance-factories/orms/README.md +54 -0
  86. package/dist/libs/instance-factories/orms/prisma.yaml +89 -0
  87. package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +2 -2
  88. package/dist/libs/instance-factories/scaffolding/README.md +49 -0
  89. package/dist/libs/instance-factories/scaffolding/generic-scaffold.yaml +65 -0
  90. package/dist/libs/instance-factories/sdks/README.md +28 -0
  91. package/dist/libs/instance-factories/sdks/python-sdk.yaml +66 -0
  92. package/dist/libs/instance-factories/sdks/typescript-sdk.yaml +59 -0
  93. package/dist/libs/instance-factories/services/README.md +55 -0
  94. package/dist/libs/instance-factories/services/prisma-services.yaml +71 -0
  95. package/dist/libs/instance-factories/storage/README.md +34 -0
  96. package/dist/libs/instance-factories/storage/mongodb.yaml +79 -0
  97. package/dist/libs/instance-factories/storage/postgresql.yaml +75 -0
  98. package/dist/libs/instance-factories/storage/redis.yaml +79 -0
  99. package/dist/libs/instance-factories/testing/README.md +40 -0
  100. package/dist/libs/instance-factories/testing/vitest-tests.yaml +63 -0
  101. package/dist/libs/instance-factories/tools/README.md +70 -0
  102. package/dist/libs/instance-factories/tools/mcp.yaml +36 -0
  103. package/dist/libs/instance-factories/tools/vscode.yaml +35 -0
  104. package/dist/libs/instance-factories/validation/README.md +38 -0
  105. package/dist/libs/instance-factories/validation/zod.yaml +56 -0
  106. package/dist/realize/engines/code-generator.d.ts.map +1 -1
  107. package/dist/realize/engines/code-generator.js +3 -0
  108. package/dist/realize/engines/code-generator.js.map +1 -1
  109. package/dist/realize/index.d.ts.map +1 -1
  110. package/dist/realize/index.js +15 -22
  111. package/dist/realize/index.js.map +1 -1
  112. package/dist/registry/utils/manifest-adapter.d.ts +8 -1
  113. package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
  114. package/dist/registry/utils/manifest-adapter.js +8 -1
  115. package/dist/registry/utils/manifest-adapter.js.map +1 -1
  116. package/libs/instance-factories/applications/react-app-starter.yaml +143 -0
  117. package/libs/instance-factories/applications/templates/react/env-example-generator.ts +24 -2
  118. package/libs/instance-factories/applications/templates/react/vite-config-generator.ts +54 -33
  119. package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  120. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
  121. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +146 -0
  122. package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +188 -0
  123. package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
  124. package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
  125. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
  126. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +184 -0
  127. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
  128. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
  129. package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
  130. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
  131. package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
  132. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
  133. package/libs/instance-factories/applications/templates/react-starter/api-types-starter-generator.ts +98 -0
  134. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
  135. package/libs/instance-factories/applications/templates/react-starter/belongs-to.ts +82 -0
  136. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +189 -0
  137. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
  138. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +383 -0
  139. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +66 -0
  140. package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
  141. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +146 -0
  142. package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +95 -0
  143. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
  144. package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
  145. package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +49 -0
  146. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +96 -0
  147. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +116 -0
  148. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +74 -0
  149. package/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.ts +124 -0
  150. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +209 -0
  151. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
  152. package/libs/instance-factories/cli/templates/commander/command-generator.ts +63 -12
  153. package/libs/instance-factories/controllers/fastify.yaml +7 -0
  154. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +36 -2
  155. package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +11 -4
  156. package/package.json +3 -3
  157. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
  158. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
  159. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
  160. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
  161. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
  162. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
  163. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
  164. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
  165. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
  166. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
  167. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
  168. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
  169. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
  170. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
  171. package/dist/libs/instance-factories/views/index.js +0 -48
  172. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
  173. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
  174. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
  175. package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
  176. package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
  177. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
  178. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
  179. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
  180. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
  181. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
  182. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
  183. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
  184. package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
  185. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
  186. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
  187. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
  188. package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
  189. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
  190. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
  191. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
  192. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
  193. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
  194. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
  195. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
  196. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  197. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
  198. package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
  199. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
  200. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
  201. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
  202. package/dist/realize/index.js.bak +0 -758
  203. package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
  204. package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
  205. package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
  206. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
  207. package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
  208. package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
  209. package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
  210. package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
  211. package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
  212. package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
  213. package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
  214. package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
  215. package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
  216. package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
  217. package/libs/instance-factories/views/README.md +0 -62
  218. package/libs/instance-factories/views/index.d.ts +0 -13
  219. package/libs/instance-factories/views/index.d.ts.map +0 -1
  220. package/libs/instance-factories/views/index.js +0 -18
  221. package/libs/instance-factories/views/index.js.map +0 -1
  222. package/libs/instance-factories/views/index.ts +0 -45
  223. package/libs/instance-factories/views/react-components.yaml +0 -129
  224. package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
  225. package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
  226. package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
  227. package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
  228. package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
  229. package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
  230. package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
  231. package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
  232. package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
  233. package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
  234. package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
  235. package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
  236. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
  237. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
  238. package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
  239. package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
  240. package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
  241. package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
  242. package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
  243. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
  244. package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
  245. package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
  246. package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
  247. package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
  248. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
  249. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
  250. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
  251. package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
  252. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
  253. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
  254. package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
  255. package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
  256. package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
  257. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
  258. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
  259. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
  260. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
  261. package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
  262. package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
  263. package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
  264. package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
  265. package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
  266. package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
  267. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
  268. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
  269. package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
  270. package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
  271. package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
  272. package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
  273. package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
  274. package/libs/instance-factories/views/templates/shared/index.ts +0 -68
  275. package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
  276. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
  277. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
  278. package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
  279. package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
  280. package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
  281. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
  282. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
  283. package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
  284. package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
  285. package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import * as ts from 'typescript';
6
+ import { generate } from '../orchestrator.js';
7
+ import { sha256, HASHES_DIR, HASHES_FILE } from '../regen-safety.js';
8
+
9
+ let projectRoot: string;
10
+
11
+ beforeEach(() => {
12
+ projectRoot = mkdtempSync(join(tmpdir(), 'react-starter-orch-'));
13
+ // Suppress the orchestrator's console.log summary during tests.
14
+ vi.spyOn(console, 'log').mockImplementation(() => {});
15
+ });
16
+
17
+ afterEach(() => {
18
+ rmSync(projectRoot, { recursive: true, force: true });
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ function makeSpec() {
23
+ return {
24
+ metadata: { name: 'My App' },
25
+ models: {
26
+ Post: {
27
+ name: 'Post',
28
+ attributes: {
29
+ id: { type: 'UUID', auto: 'uuid4' },
30
+ title: { type: 'String', required: true },
31
+ body: { type: 'Text' },
32
+ },
33
+ },
34
+ },
35
+ views: {},
36
+ };
37
+ }
38
+
39
+ function assertValidTsx(source: string, label: string): void {
40
+ const result = ts.transpileModule(source, {
41
+ compilerOptions: {
42
+ jsx: ts.JsxEmit.Preserve,
43
+ target: ts.ScriptTarget.ES2022,
44
+ module: ts.ModuleKind.ESNext,
45
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
46
+ strict: false,
47
+ },
48
+ reportDiagnostics: true,
49
+ });
50
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
51
+ if (errors.length > 0) {
52
+ const msg = errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('\n');
53
+ throw new Error(`${label} failed to parse:\n${msg}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Drive the orchestrator with the per-test tmp dir as the effective
59
+ * frontend root. `frontendDir='.'` collapses the
60
+ * `${outputDir}/${frontendDir}` join so files land directly under
61
+ * `projectRoot`, which keeps test assertions terse.
62
+ */
63
+ async function runGenerate(spec = makeSpec()) {
64
+ return generate({ spec, outputDir: projectRoot, frontendDir: '.' });
65
+ }
66
+
67
+ /** List every file the orchestrator wrote under projectRoot. */
68
+ function walk(dir: string, prefix = ''): string[] {
69
+ const { readdirSync, statSync } = require('fs');
70
+ const out: string[] = [];
71
+ for (const entry of readdirSync(dir)) {
72
+ const abs = join(dir, entry);
73
+ const rel = prefix ? `${prefix}/${entry}` : entry;
74
+ if (statSync(abs).isDirectory()) out.push(...walk(abs, rel));
75
+ else out.push(rel);
76
+ }
77
+ return out.sort();
78
+ }
79
+
80
+ describe('orchestrator — first-run (empty project)', () => {
81
+ it('writes the full set of files: views + helpers + App.tsx + package.json + hash manifest', async () => {
82
+ const ret = await runGenerate();
83
+ // Contract: returns '' (realize's single-file writeOutput skips it).
84
+ expect(ret).toBe('');
85
+
86
+ const written = walk(projectRoot);
87
+ expect(written).toEqual([
88
+ `${HASHES_DIR}/${HASHES_FILE}`,
89
+ 'package.json',
90
+ 'src/App.tsx',
91
+ 'src/lib/entity-display.ts',
92
+ 'src/views/PostDashboardView.tsx',
93
+ 'src/views/PostDetailView.tsx',
94
+ 'src/views/PostFormView.tsx',
95
+ 'src/views/PostListView.tsx',
96
+ ]);
97
+ });
98
+
99
+ it('every emitted .tsx parses as valid TSX', async () => {
100
+ await runGenerate();
101
+ for (const path of walk(projectRoot)) {
102
+ if (path.endsWith('.tsx')) {
103
+ const source = readFileSync(join(projectRoot, path), 'utf8');
104
+ assertValidTsx(source, path);
105
+ }
106
+ }
107
+ });
108
+
109
+ it('package.json has no @specverse/runtime dependency', async () => {
110
+ await runGenerate();
111
+ const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
112
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
113
+ expect(deps['@specverse/runtime']).toBeUndefined();
114
+ });
115
+
116
+ it('the emitted hash manifest records every approved file', async () => {
117
+ await runGenerate();
118
+ const manifestPath = join(projectRoot, HASHES_DIR, HASHES_FILE);
119
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
120
+ for (const path of walk(projectRoot)) {
121
+ if (path === `${HASHES_DIR}/${HASHES_FILE}`) continue;
122
+ const content = readFileSync(join(projectRoot, path), 'utf8');
123
+ expect(manifest[path]).toBe(sha256(content));
124
+ }
125
+ });
126
+ });
127
+
128
+ describe('orchestrator — regeneration safety', () => {
129
+ it('re-approves pristine files on a second run', async () => {
130
+ await runGenerate();
131
+
132
+ // Record the first-run hash of one of the view files.
133
+ const listPath = 'src/views/PostListView.tsx';
134
+ const listAbs = join(projectRoot, listPath);
135
+ const firstContent = readFileSync(listAbs, 'utf8');
136
+
137
+ // Second run. Pristine files should be rewritten (the orchestrator
138
+ // writes them, possibly with identical bytes).
139
+ await runGenerate();
140
+ expect(existsSync(listAbs)).toBe(true);
141
+ expect(readFileSync(listAbs, 'utf8')).toBe(firstContent);
142
+ });
143
+
144
+ it('preserves a user-edited file across regeneration', async () => {
145
+ await runGenerate();
146
+
147
+ const editedPath = 'src/views/PostListView.tsx';
148
+ const editedAbs = join(projectRoot, editedPath);
149
+ const userEdit = '/* user edit */';
150
+ writeFileSync(editedAbs, userEdit, 'utf8');
151
+
152
+ await runGenerate();
153
+
154
+ // File on disk is unchanged — user edit survived.
155
+ expect(readFileSync(editedAbs, 'utf8')).toBe(userEdit);
156
+ // Manifest still records the ORIGINAL (pre-edit) hash so a future
157
+ // un-edit can re-sync cleanly.
158
+ const manifest = JSON.parse(
159
+ readFileSync(join(projectRoot, HASHES_DIR, HASHES_FILE), 'utf8')
160
+ );
161
+ expect(manifest[editedPath]).toBeDefined();
162
+ expect(manifest[editedPath]).not.toBe(sha256(userEdit));
163
+ });
164
+
165
+ it('is cautious when the user deletes the hash manifest', async () => {
166
+ await runGenerate();
167
+
168
+ // User deletes the hash manifest but keeps the view files.
169
+ rmSync(join(projectRoot, HASHES_DIR), { recursive: true, force: true });
170
+
171
+ // Record the existing file contents (may be identical to what the
172
+ // orchestrator would emit, but we're treating them as unknown origin).
173
+ const listPath = join(projectRoot, 'src/views/PostListView.tsx');
174
+ const before = readFileSync(listPath, 'utf8');
175
+
176
+ await runGenerate();
177
+
178
+ // No hash manifest means we can't tell user edits from originals, so
179
+ // existing files are skipped. Content is unchanged.
180
+ expect(readFileSync(listPath, 'utf8')).toBe(before);
181
+ // A fresh hash manifest was written.
182
+ expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
183
+ });
184
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * P2 — Factory generators consume the pattern library, not forks
3
+ *
4
+ * Architectural invariant I5 from VIEW-RENDERING-ARCHITECTURE.md:
5
+ * "Factory generators are consumers of the pattern library, not
6
+ * reimplementations of it."
7
+ *
8
+ * Every composer / body-generator under react-starter must import
9
+ * from `@specverse/runtime/views/core` or `@specverse/runtime/views/tailwind`.
10
+ * A generator that doesn't is a reimplementation — it's drift-prone
11
+ * and violates the "one pattern library, three consumers" contract.
12
+ *
13
+ * Scope: files matching *-composer.ts or view-emitter.ts / generator
14
+ * files that actually produce view-layout code. Skeletons (.tsx.template)
15
+ * are content-only and exempt. helpers-emitter / html-to-jsx are
16
+ * transformation utilities, exempt — they don't consume pattern data.
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { readdirSync, readFileSync } from 'fs';
21
+ import { join, resolve } from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const HERE = fileURLToPath(import.meta.url);
25
+ const STARTER_DIR = resolve(HERE, '..', '..');
26
+
27
+ const RUNTIME_IMPORT_PATTERN =
28
+ /from\s+['"]@specverse\/runtime\/views\/(core|tailwind)['"]/;
29
+
30
+ /**
31
+ * Files required to import from @specverse/runtime/views/core or
32
+ * /views/tailwind. The filenames match the convention of any generator
33
+ * that emits view layout or composes pattern data.
34
+ */
35
+ const MUST_IMPORT_FROM_RUNTIME = (filename: string): boolean => {
36
+ // Every body composer (list / detail / form / dashboard).
37
+ if (/-body-composer\.ts$/.test(filename)) return true;
38
+ return false;
39
+ };
40
+
41
+ /**
42
+ * Files explicitly exempt from P2 — orchestrators, transformers, and
43
+ * utilities that don't consume pattern data. Each exemption carries
44
+ * a short justification as an inline comment near the import list.
45
+ */
46
+ const EXEMPT_FILES = new Set([
47
+ // Orchestrator: dispatches to composers (which do import runtime).
48
+ 'views-generator.ts',
49
+ 'view-emitter.ts',
50
+ 'orchestrator.ts',
51
+ // Pure string transformer, no pattern data.
52
+ 'html-to-jsx.ts',
53
+ // Emits inline source code; no adapter calls needed.
54
+ 'helpers-emitter.ts',
55
+ 'app-tsx-generator.ts',
56
+ 'package-json-generator.ts',
57
+ // Pure utility for content-hashing, no pattern consumption.
58
+ 'regen-safety.ts',
59
+ ]);
60
+
61
+ function collectTypeScriptFiles(dir: string, prefix = ''): string[] {
62
+ const out: string[] = [];
63
+ const entries = readdirSync(dir, { withFileTypes: true });
64
+ for (const entry of entries) {
65
+ if (entry.name === '__tests__' || entry.name === 'skeletons') continue;
66
+ const full = join(dir, entry.name);
67
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
68
+ if (entry.isDirectory()) {
69
+ out.push(...collectTypeScriptFiles(full, rel));
70
+ } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
71
+ out.push(rel);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ describe('P2 — factory generators consume the pattern library', () => {
78
+ const files = collectTypeScriptFiles(STARTER_DIR);
79
+
80
+ it('produces a non-empty file list (sanity)', () => {
81
+ expect(files.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ for (const relPath of files) {
85
+ const filename = relPath.split('/').pop()!;
86
+
87
+ if (EXEMPT_FILES.has(filename)) continue;
88
+ if (!MUST_IMPORT_FROM_RUNTIME(filename)) continue;
89
+
90
+ it(`${relPath} imports from @specverse/runtime/views/{core|tailwind}`, () => {
91
+ const content = readFileSync(join(STARTER_DIR, relPath), 'utf8');
92
+ const imports = RUNTIME_IMPORT_PATTERN.test(content);
93
+ if (!imports) {
94
+ throw new Error(
95
+ `${relPath} must import from @specverse/runtime/views/core or ` +
96
+ `@specverse/runtime/views/tailwind to satisfy invariant I5. ` +
97
+ `If it genuinely doesn't need pattern data, add it to the ` +
98
+ `EXEMPT_FILES set with a justification. Generators that emit ` +
99
+ `view-layout code inevitably use pattern constants (field ` +
100
+ `classification, atomic components) — sourcing those from ` +
101
+ `runtime is what keeps the three consumers in sync.`
102
+ );
103
+ }
104
+ });
105
+ }
106
+
107
+ it('every composer is covered by an explicit rule (no silent skips)', () => {
108
+ const composers = files.filter(f => f.endsWith('-body-composer.ts'));
109
+ expect(composers.length).toBeGreaterThanOrEqual(4); // list/detail/form/dashboard at minimum
110
+ for (const c of composers) {
111
+ const filename = c.split('/').pop()!;
112
+ expect(MUST_IMPORT_FROM_RUNTIME(filename)).toBe(true);
113
+ expect(EXEMPT_FILES.has(filename)).toBe(false);
114
+ }
115
+ });
116
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * P3 — Rendered-output equivalence
3
+ *
4
+ * Architectural invariant: the same (spec, view, model) must produce
5
+ * semantically equivalent output in app-demo (runtime React adapter)
6
+ * and in ReactAppStarter (Factory B emitter). They differ only in
7
+ * execution model — one renders in the browser, the other emits
8
+ * static JSX — but the visible structure must match.
9
+ *
10
+ * This file tests the LIST VIEW path, where parity is most
11
+ * tractable because both consumers call the same Tailwind adapter
12
+ * for the table shell. The test covers:
13
+ *
14
+ * 1. Column selection — both use `inferFieldsFromSchema` from
15
+ * the canonical pattern engine. Drift in either composer's
16
+ * column-filter logic would diverge here.
17
+ * 2. Table shell HTML — the Tailwind adapter renders the same
18
+ * shell for both paths; ensured by Factory B importing
19
+ * `createUniversalTailwindAdapter` directly.
20
+ * 3. Field humanisation — `camelCase` → `Title Case` for headers.
21
+ *
22
+ * Not yet covered (future extensions):
23
+ * - Detail / form / dashboard views — would need a reference
24
+ * React-adapter render path captured as an HTML string, then
25
+ * diffed against the Factory B emitted JSX modulo structural
26
+ * differences. Doable but bigger test machinery.
27
+ * - FK display-name resolution — blocked on Factory B's TODO to
28
+ * replace plain-FK inputs with dropdowns.
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
33
+ import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
34
+ import { composeListBody } from '../list-body-composer.js';
35
+ import type { EmitContext, ModelSpec } from '../view-emitter.js';
36
+
37
+ // ───────────────────────────────────────────────────────────────────────
38
+ // Reference model + view-spec setup
39
+ // ───────────────────────────────────────────────────────────────────────
40
+
41
+ function makeReferenceModel(): ModelSpec {
42
+ return {
43
+ name: 'Article',
44
+ attributes: {
45
+ id: { type: 'UUID', required: true, auto: 'uuid4' }, // excluded
46
+ title: { type: 'String', required: true }, // included
47
+ slug: { type: 'String', required: true, unique: true }, // included
48
+ content: { type: 'Text', required: false }, // included
49
+ status: { type: 'String', values: ['draft', 'published'] },// included
50
+ authorId: { type: 'UUID', required: true }, // included (FK)
51
+ createdAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
52
+ updatedAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
53
+ },
54
+ };
55
+ }
56
+
57
+ function makeContext(): EmitContext {
58
+ const model = makeReferenceModel();
59
+ return {
60
+ view: { type: 'list', model: 'Article' },
61
+ viewName: 'ArticleListView',
62
+ model,
63
+ modelSchemas: { Article: model },
64
+ renderBody: composeListBody,
65
+ };
66
+ }
67
+
68
+ function humanize(s: string): string {
69
+ return s.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
70
+ }
71
+
72
+ // ───────────────────────────────────────────────────────────────────────
73
+ // Parity assertions
74
+ // ───────────────────────────────────────────────────────────────────────
75
+
76
+ describe('P3 — list view column inference parity', () => {
77
+ it('runtime inferFieldsFromSchema and Factory B composer see the same columns', () => {
78
+ const ctx = makeContext();
79
+ const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
80
+
81
+ // We can extract Factory B's column choices by looking at what
82
+ // headers it emits into the rendered output.
83
+ const body = composeListBody(ctx);
84
+
85
+ for (const col of runtimeColumns) {
86
+ const label = humanize(col);
87
+ expect(body, `runtime column "${col}" (label: "${label}") must appear in Factory B output`)
88
+ .toContain(`>${label}<`);
89
+ }
90
+
91
+ // And nothing EXTRA from the model that runtime excludes:
92
+ const runtimeSet = new Set(runtimeColumns);
93
+ const allAttrs = Object.keys(ctx.model.attributes);
94
+ const runtimeExcluded = allAttrs.filter(a => !runtimeSet.has(a));
95
+ for (const excluded of runtimeExcluded) {
96
+ const label = humanize(excluded);
97
+ // Factory B must NOT emit a <th> for this field.
98
+ const headerPattern = new RegExp(`<th\\b[^>]*>${label}</th>`);
99
+ expect(body).not.toMatch(headerPattern);
100
+ }
101
+ });
102
+
103
+ it('matches the expected column set for the reference model', () => {
104
+ const cols = inferFieldsFromSchema(makeContext().modelSchemas, 'Article');
105
+ // Canonical expectation for this reference spec.
106
+ expect(cols).toEqual(['title', 'slug', 'content', 'status', 'authorId']);
107
+ });
108
+ });
109
+
110
+ describe('P3 — list view table shell parity', () => {
111
+ it('emits a table shell byte-identical to a direct Tailwind adapter call', () => {
112
+ // The composer calls createUniversalTailwindAdapter() + table.render()
113
+ // with a known columns list and a sentinel in children. If we call
114
+ // the adapter directly with the same args, we should get the same
115
+ // shell HTML (modulo the sentinel vs the Factory B JSX injection).
116
+ const ctx = makeContext();
117
+ const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
118
+ const headers = runtimeColumns.map(humanize);
119
+
120
+ const adapter = createUniversalTailwindAdapter({ darkMode: true });
121
+ const SENTINEL = 'SENTINEL';
122
+ const shellHtml = adapter.components.table.render({
123
+ properties: { columns: headers },
124
+ children: SENTINEL,
125
+ });
126
+
127
+ // Every column header appears in the shell, in order.
128
+ let lastIdx = -1;
129
+ for (const header of headers) {
130
+ const idx = shellHtml.indexOf(`>${header}</th>`);
131
+ expect(idx, `header "${header}" must appear in Tailwind adapter output`).toBeGreaterThan(-1);
132
+ expect(idx, `header "${header}" must appear AFTER the previous header`).toBeGreaterThan(lastIdx);
133
+ lastIdx = idx;
134
+ }
135
+
136
+ // Factory B's composer output must preserve the same header order.
137
+ const body = composeListBody(ctx);
138
+ let lastBodyIdx = -1;
139
+ for (const header of headers) {
140
+ const idx = body.indexOf(`>${header}</th>`);
141
+ expect(idx).toBeGreaterThan(-1);
142
+ expect(idx).toBeGreaterThan(lastBodyIdx);
143
+ lastBodyIdx = idx;
144
+ }
145
+ });
146
+
147
+ it('Factory B output carries Tailwind classes from the canonical adapter shell', () => {
148
+ const body = composeListBody(makeContext());
149
+ // A sampling of classes that come from the adapter's table template.
150
+ // If the adapter changes its classes, both consumers update together.
151
+ expect(body).toContain('overflow-x-auto');
152
+ expect(body).toContain('min-w-full');
153
+ expect(body).toContain('divide-y divide-gray-200');
154
+ expect(body).toContain('bg-gray-50 dark:bg-gray-800');
155
+ });
156
+
157
+ it('rows are synthesized by Factory B (the adapter emits placeholder for react mount)', () => {
158
+ const body = composeListBody(makeContext());
159
+ // The body should contain a real JSX .map() — Factory B's
160
+ // contribution to the shared shell.
161
+ expect(body).toContain('filtered.map((item, idx) =>');
162
+ expect(body).toContain('onClick={() => onSelect?.(item)}');
163
+ });
164
+ });
165
+
166
+ describe('P3 — field humanisation parity', () => {
167
+ it('both paths humanise column names the same way (camelCase → Title Case)', () => {
168
+ // Factory B's humanize is inlined in the composer. The runtime
169
+ // adapter humanises via the same-shaped regex in pattern-engine /
170
+ // react-pattern-adapter. This test documents the contract by
171
+ // comparing against a fixture.
172
+ const cases: [string, string][] = [
173
+ ['title', 'Title'],
174
+ ['authorId', 'Author Id'],
175
+ ['createdAt', 'Created At'],
176
+ ['slugifiedURL', 'Slugified U R L'],
177
+ ['name', 'Name'],
178
+ ];
179
+ for (const [input, expected] of cases) {
180
+ expect(humanize(input), `humanize("${input}")`).toBe(expected);
181
+ }
182
+ });
183
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import {
6
+ loadHashManifest,
7
+ saveHashManifest,
8
+ reconcileWrites,
9
+ sha256,
10
+ HASHES_DIR,
11
+ HASHES_FILE,
12
+ } from '../regen-safety.js';
13
+
14
+ let projectRoot: string;
15
+
16
+ beforeEach(() => {
17
+ projectRoot = mkdtempSync(join(tmpdir(), 'specverse-regen-safety-'));
18
+ });
19
+
20
+ afterEach(() => {
21
+ rmSync(projectRoot, { recursive: true, force: true });
22
+ });
23
+
24
+ describe('hash manifest persistence', () => {
25
+ it('loadHashManifest returns {} when the file is missing', () => {
26
+ expect(loadHashManifest(projectRoot)).toEqual({});
27
+ });
28
+
29
+ it('round-trips through save → load', () => {
30
+ saveHashManifest(projectRoot, { 'src/a.ts': 'abc', 'src/b.ts': 'def' });
31
+ const loaded = loadHashManifest(projectRoot);
32
+ expect(loaded).toEqual({ 'src/a.ts': 'abc', 'src/b.ts': 'def' });
33
+ expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
34
+ });
35
+
36
+ it('tolerates a malformed manifest file by returning {}', () => {
37
+ const dir = join(projectRoot, HASHES_DIR);
38
+ mkdirSync(dir, { recursive: true });
39
+ writeFileSync(join(dir, HASHES_FILE), '{ not valid JSON', 'utf8');
40
+ expect(loadHashManifest(projectRoot)).toEqual({});
41
+ });
42
+ });
43
+
44
+ describe('reconcileWrites — pure planning (no I/O writes)', () => {
45
+ it('approves brand-new files and records their hashes', () => {
46
+ const proposed = { 'src/views/PostListView.tsx': 'content-a' };
47
+ const result = reconcileWrites(projectRoot, proposed, {});
48
+ expect(result.approvedWrites).toEqual(proposed);
49
+ expect(result.skipped).toEqual([]);
50
+ expect(existsSync(join(projectRoot, 'src/views/PostListView.tsx'))).toBe(false);
51
+ expect(result.manifest['src/views/PostListView.tsx']).toBe(sha256('content-a'));
52
+ });
53
+
54
+ it('approves overwriting a pristine file and updates its hash', () => {
55
+ const path = 'src/a.ts';
56
+ const absPath = join(projectRoot, path);
57
+ const originalContent = 'v1';
58
+ const newContent = 'v2';
59
+
60
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
61
+ writeFileSync(absPath, originalContent, 'utf8');
62
+ const prevManifest = { [path]: sha256(originalContent) };
63
+
64
+ const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
65
+
66
+ expect(result.approvedWrites[path]).toBe(newContent);
67
+ expect(result.skipped).toEqual([]);
68
+ // reconcileWrites is pure planning — filesystem unchanged
69
+ expect(readFileSync(absPath, 'utf8')).toBe(originalContent);
70
+ expect(result.manifest[path]).toBe(sha256(newContent));
71
+ });
72
+
73
+ it('skips a user-edited file and keeps the old hash record', () => {
74
+ const path = 'src/edited.ts';
75
+ const absPath = join(projectRoot, path);
76
+ const originalContent = 'v1';
77
+ const editedContent = 'v1 + user edits';
78
+ const newContent = 'v2';
79
+
80
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
81
+ writeFileSync(absPath, editedContent, 'utf8');
82
+ const prevManifest = { [path]: sha256(originalContent) };
83
+
84
+ const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
85
+
86
+ expect(result.approvedWrites[path]).toBeUndefined();
87
+ expect(result.skipped).toHaveLength(1);
88
+ expect(result.skipped[0]).toMatchObject({
89
+ path,
90
+ reason: expect.stringContaining('edited'),
91
+ });
92
+ expect(readFileSync(absPath, 'utf8')).toBe(editedContent);
93
+ expect(result.manifest[path]).toBe(prevManifest[path]);
94
+ });
95
+
96
+ it('skips files that exist but have no prior hash record', () => {
97
+ const path = 'src/unknown.ts';
98
+ const absPath = join(projectRoot, path);
99
+
100
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
101
+ writeFileSync(absPath, 'manually placed', 'utf8');
102
+
103
+ const result = reconcileWrites(projectRoot, { [path]: 'would-overwrite' }, {});
104
+
105
+ expect(result.approvedWrites[path]).toBeUndefined();
106
+ expect(result.skipped).toHaveLength(1);
107
+ expect(result.skipped[0].reason).toContain('no prior hash recorded');
108
+ expect(readFileSync(absPath, 'utf8')).toBe('manually placed');
109
+ });
110
+
111
+ it('does not mutate the input prevManifest', () => {
112
+ const prev = { 'a.ts': sha256('old') };
113
+ const prevSnapshot = { ...prev };
114
+ reconcileWrites(projectRoot, { 'a.ts': 'new', 'b.ts': 'also new' }, prev);
115
+ expect(prev).toEqual(prevSnapshot);
116
+ });
117
+
118
+ it('handles a batch of mixed outcomes correctly', () => {
119
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
120
+ writeFileSync(join(projectRoot, 'src/pristine.ts'), 'v1', 'utf8');
121
+ writeFileSync(join(projectRoot, 'src/edited.ts'), 'v1 + user edit', 'utf8');
122
+ const prev = {
123
+ 'src/pristine.ts': sha256('v1'),
124
+ 'src/edited.ts': sha256('v1'),
125
+ };
126
+
127
+ const proposed = {
128
+ 'src/pristine.ts': 'v2',
129
+ 'src/edited.ts': 'v2',
130
+ 'src/new.ts': 'brand new',
131
+ };
132
+
133
+ const result = reconcileWrites(projectRoot, proposed, prev);
134
+
135
+ expect(Object.keys(result.approvedWrites).sort()).toEqual(['src/new.ts', 'src/pristine.ts']);
136
+ expect(result.skipped.map(s => s.path)).toEqual(['src/edited.ts']);
137
+ // No writes happened
138
+ expect(readFileSync(join(projectRoot, 'src/pristine.ts'), 'utf8')).toBe('v1');
139
+ expect(readFileSync(join(projectRoot, 'src/edited.ts'), 'utf8')).toBe('v1 + user edit');
140
+ expect(result.manifest['src/pristine.ts']).toBe(sha256('v2'));
141
+ expect(result.manifest['src/edited.ts']).toBe(prev['src/edited.ts']);
142
+ expect(result.manifest['src/new.ts']).toBe(sha256('brand new'));
143
+ });
144
+ });