@specverse/engines 4.1.30 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) 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/dist/inference/comprehensive-engine.d.ts.map +1 -1
  5. package/dist/inference/comprehensive-engine.js +3 -19
  6. package/dist/inference/comprehensive-engine.js.map +1 -1
  7. package/dist/inference/core/rule-engine.d.ts +31 -0
  8. package/dist/inference/core/rule-engine.d.ts.map +1 -1
  9. package/dist/inference/core/rule-engine.js +117 -33
  10. package/dist/inference/core/rule-engine.js.map +1 -1
  11. package/dist/inference/core/rule-file-types.d.ts +0 -2
  12. package/dist/inference/core/rule-file-types.d.ts.map +1 -1
  13. package/dist/inference/core/rule-file-types.js +3 -6
  14. package/dist/inference/core/rule-file-types.js.map +1 -1
  15. package/dist/inference/core/rule-loader.d.ts +5 -15
  16. package/dist/inference/core/rule-loader.d.ts.map +1 -1
  17. package/dist/inference/core/rule-loader.js +43 -132
  18. package/dist/inference/core/rule-loader.js.map +1 -1
  19. package/dist/inference/core/types.d.ts +0 -6
  20. package/dist/inference/core/types.d.ts.map +1 -1
  21. package/dist/inference/core/types.js +0 -4
  22. package/dist/inference/core/types.js.map +1 -1
  23. package/dist/inference/logical/generators/component-type-resolver.d.ts +0 -26
  24. package/dist/inference/logical/generators/component-type-resolver.d.ts.map +1 -1
  25. package/dist/inference/logical/generators/component-type-resolver.js +0 -19
  26. package/dist/inference/logical/generators/component-type-resolver.js.map +1 -1
  27. package/dist/inference/logical/generators/specialist-view-expander.d.ts +1 -17
  28. package/dist/inference/logical/generators/specialist-view-expander.d.ts.map +1 -1
  29. package/dist/inference/logical/generators/specialist-view-expander.js +0 -15
  30. package/dist/inference/logical/generators/specialist-view-expander.js.map +1 -1
  31. package/dist/inference/logical/generators/view-generator.d.ts +4 -14
  32. package/dist/inference/logical/generators/view-generator.d.ts.map +1 -1
  33. package/dist/inference/logical/generators/view-generator.js +6 -26
  34. package/dist/inference/logical/generators/view-generator.js.map +1 -1
  35. package/dist/inference/logical/index.d.ts +2 -2
  36. package/dist/inference/logical/index.d.ts.map +1 -1
  37. package/dist/inference/logical/logical-engine.d.ts.map +1 -1
  38. package/dist/inference/logical/logical-engine.js +17 -80
  39. package/dist/inference/logical/logical-engine.js.map +1 -1
  40. package/dist/inference/quint-transpiler.d.ts +5 -3
  41. package/dist/inference/quint-transpiler.d.ts.map +1 -1
  42. package/dist/inference/quint-transpiler.js +11 -6
  43. package/dist/inference/quint-transpiler.js.map +1 -1
  44. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
  45. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
  46. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
  47. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
  48. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
  49. package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
  50. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
  51. package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
  52. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
  53. package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
  54. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
  55. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
  56. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
  57. package/dist/realize/index.d.ts.map +1 -1
  58. package/dist/realize/index.js +15 -22
  59. package/dist/realize/index.js.map +1 -1
  60. package/dist/registry/utils/manifest-adapter.d.ts +8 -1
  61. package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
  62. package/dist/registry/utils/manifest-adapter.js +8 -1
  63. package/dist/registry/utils/manifest-adapter.js.map +1 -1
  64. package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
  65. package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  66. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
  67. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
  68. package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
  69. package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
  70. package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
  71. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
  72. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
  73. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
  74. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
  75. package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
  76. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
  77. package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
  78. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
  79. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
  80. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
  81. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
  82. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
  83. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
  84. package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
  85. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
  86. package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
  87. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
  88. package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
  89. package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
  90. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
  91. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
  92. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
  93. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
  94. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
  95. package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
  96. package/package.json +3 -3
  97. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
  98. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
  99. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
  100. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
  101. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
  102. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
  103. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
  104. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
  105. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
  106. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
  107. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
  108. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
  109. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
  110. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
  111. package/dist/libs/instance-factories/views/index.js +0 -48
  112. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
  113. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
  114. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
  115. package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
  116. package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
  117. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
  118. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
  119. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
  120. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
  121. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
  122. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
  123. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
  124. package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
  125. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
  126. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
  127. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
  128. package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
  129. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
  130. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
  131. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
  132. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
  133. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
  134. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
  135. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
  136. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  137. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
  138. package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
  139. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
  140. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
  141. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
  142. package/dist/realize/index.js.bak +0 -758
  143. package/libs/instance-factories/applications/react-app.yaml +0 -186
  144. package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
  145. package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
  146. package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
  147. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
  148. package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
  149. package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
  150. package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
  151. package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
  152. package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
  153. package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
  154. package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
  155. package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
  156. package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
  157. package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
  158. package/libs/instance-factories/views/README.md +0 -62
  159. package/libs/instance-factories/views/index.d.ts +0 -13
  160. package/libs/instance-factories/views/index.d.ts.map +0 -1
  161. package/libs/instance-factories/views/index.js +0 -18
  162. package/libs/instance-factories/views/index.js.map +0 -1
  163. package/libs/instance-factories/views/index.ts +0 -45
  164. package/libs/instance-factories/views/react-components.yaml +0 -129
  165. package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
  166. package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
  167. package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
  168. package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
  169. package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
  170. package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
  171. package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
  172. package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
  173. package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
  174. package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
  175. package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
  176. package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
  177. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
  178. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
  179. package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
  180. package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
  181. package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
  182. package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
  183. package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
  184. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
  185. package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
  186. package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
  187. package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
  188. package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
  189. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
  190. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
  191. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
  192. package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
  193. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
  194. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
  195. package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
  196. package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
  197. package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
  198. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
  199. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
  200. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
  201. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
  202. package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
  203. package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
  204. package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
  205. package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
  206. package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
  207. package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
  208. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
  209. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
  210. package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
  211. package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
  212. package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
  213. package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
  214. package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
  215. package/libs/instance-factories/views/templates/shared/index.ts +0 -68
  216. package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
  217. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
  218. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
  219. package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
  220. package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
  221. package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
  222. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
  223. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
  224. package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
  225. package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
  226. package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
@@ -0,0 +1,211 @@
1
+ # ReactAppStarter — Emitter Architecture
2
+
3
+ **Audience**: anyone touching the view emitter for Factory B (the "starter-kit" React factory).
4
+
5
+ **Companion docs**:
6
+ - `specverse-self/docs/guides/VIEW-RENDERING-ARCHITECTURE.md` (the "one pattern library, three consumers" design)
7
+ - `specverse-self/docs/plans/2026-04-17-UNIFIED-VIEW-RENDERING.md` (the migration plan)
8
+
9
+ ---
10
+
11
+ ## The problem this solves
12
+
13
+ Factory B emits a standalone React codebase. For every view in the inferred spec (dev defaults + user-defined views), it writes a `.tsx` file the user can read, fork, and edit. The generated code:
14
+
15
+ - Has no `@specverse/runtime` dependency (fully standalone).
16
+ - Is idiomatic React with typed props, hooks, event handlers — not a big static HTML blob.
17
+ - Renders the **same** visual output as `@specverse/runtime` renders for the same spec (verified by Phase 3 parity test P3).
18
+
19
+ The tension: "idiomatic React structure" wants hand-crafted JSX. "Same visual output as runtime" wants a single rendering authority. Strategy 3 resolves this by splitting *structure* (hand-written skeleton templates) from *interior* (canonical Tailwind adapter rendering).
20
+
21
+ ## The three pieces
22
+
23
+ ```
24
+ ┌─────────────────────────────────────────────────────────────┐
25
+ │ 1. view-emitter.ts — orchestrator │
26
+ │ Takes (viewSpec, modelSpec, expandedSpec) → .tsx string │
27
+ └──────────────┬───────────────────────────────┬──────────────┘
28
+ │ │
29
+ ▼ ▼
30
+ ┌──────────────────────────────┐ ┌─────────────────────────────────┐
31
+ │ 2. skeletons/*.tsx.template │ │ 3. html-to-jsx.ts — transformer│
32
+ │ One per view type: │ │ │
33
+ │ list.tsx.template │ │ Converts Tailwind-HTML │
34
+ │ detail.tsx.template │ │ strings from the runtime │
35
+ │ form.tsx.template │ │ adapter into JSX-safe source │
36
+ │ dashboard.tsx.template │ │ (class→className, self-close,│
37
+ │ + specialist types │ │ brace escape, attribute map) │
38
+ │ │ │ │
39
+ │ Each has a {{BODY}} │ │ ~50-150 LoC, bounded scope │
40
+ │ placeholder the emitter │ │ │
41
+ │ fills with transformed │ │ │
42
+ │ JSX. │ │ │
43
+ └──────────────────────────────┘ └─────────────────────────────────┘
44
+
45
+
46
+ │ renders body as HTML
47
+
48
+ ┌─────────────────────┴──────────────────────┐
49
+ │ @specverse/runtime/views/tailwind │
50
+ │ createUniversalTailwindAdapter() │
51
+ │ (the canonical build-time renderer — │
52
+ │ same code app-demo and Factory A │
53
+ │ transitively rely on at runtime) │
54
+ └────────────────────────────────────────────┘
55
+ ```
56
+
57
+ ## Data flow for ONE view
58
+
59
+ Given an inferred `PostListView` in the expanded spec:
60
+
61
+ 1. **view-emitter.ts** receives `(view, model, expandedSpec)`.
62
+ 2. Picks the correct skeleton based on `view.type`: `skeletons/list.tsx.template`.
63
+ 3. Builds a render context for the pattern: `{ primaryModel: 'Post', modelSchemas: {...}, viewSpec: view, modelData: [] }` (empty data — structure only).
64
+ 4. Calls `createUniversalTailwindAdapter()` and renders the composite pattern's interior → HTML string.
65
+ Example output: `<table class="w-full ..."><thead><tr><th class="...">Title</th>...`.
66
+ 5. Passes that HTML through **html-to-jsx.ts** → JSX-safe source.
67
+ Example: `<table className="w-full ..."><thead><tr><th className="...">{/* data.map */}Title</th>...`.
68
+ 6. Substitutes `{{BODY}}` in the skeleton with the transformed JSX.
69
+ 7. Substitutes skeleton placeholders like `{{MODEL_NAME}}`, `{{PLURAL}}`, `{{PROPS_INTERFACE}}` with model-specific values.
70
+ 8. Returns the complete `.tsx` source string.
71
+
72
+ The factory's realize pass calls this once per view, writes each to `frontend/src/views/{ViewName}.tsx`, updates `.specverse-gen/hashes.json`.
73
+
74
+ ## Why this split works
75
+
76
+ - **Idiomatic outer structure** — skeletons are hand-written `.tsx.template` files, tweaked by the humans who maintain the factory. They define imports, props interfaces, hook placement, event wiring, loading states, empty states. Exactly what a human React author would write.
77
+ - **Pattern-driven interior** — the actual markup (table header cells, row templates, form fields, badge styling) comes from the canonical Tailwind adapter. When runtime improves the pattern rendering, Factory B inherits the fix automatically — because the adapter is the single source.
78
+ - **No full re-implementation** — we don't build a second renderer. The adapter already works for runtime; we reuse it at build time.
79
+
80
+ ## The html-to-jsx transformer — scope
81
+
82
+ Input: HTML string with Tailwind classes. Single root element (always — the adapter always produces a containing element).
83
+
84
+ Output: A JSX-safe source string that can be dropped between JSX tags.
85
+
86
+ What it does:
87
+ 1. `class="..."` → `className="..."`
88
+ 2. Self-closing tags: `<img src="...">` → `<img src="..." />`
89
+ 3. Escape `{` and `}` in text content by wrapping in `{'{'}` / `{'}'}` when they appear outside attribute values.
90
+ 4. Convert `style="color: red"` to `style={{ color: 'red' }}`.
91
+ 5. Convert `for="..."` to `htmlFor="..."`.
92
+ 6. Strip any attributes that aren't valid in JSX.
93
+
94
+ What it explicitly does NOT do:
95
+ - Parse JavaScript inside the HTML (the adapter produces static HTML — no JS expressions).
96
+ - Insert React hooks or event handlers — those live in the skeleton, not the interior.
97
+ - Handle SVG / MathML nuances — only HTML+Tailwind.
98
+
99
+ Test coverage target: feed the transformer 50 pre-captured HTML snippets from the canonical adapter output, assert the JSX output parses as valid TSX via `typescript.transpileModule`.
100
+
101
+ ## Skeleton template format
102
+
103
+ A skeleton is a `.tsx.template` file — syntactically valid TypeScript with substitution markers. Example (sketch for `list.tsx.template`):
104
+
105
+ ```tsx
106
+ /**
107
+ * {{MODEL_NAME}}ListView — generated by @specverse/realize (ReactAppStarter)
108
+ *
109
+ * Safe to edit. Edits are preserved across regeneration (content-hashed).
110
+ * Regenerate with `spv realize` or, if this file was edited, delete the
111
+ * file first to opt back into regeneration.
112
+ */
113
+ import { useState, useMemo } from 'react';
114
+ import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
115
+ import type { {{MODEL_NAME}} } from '../types/api';
116
+
117
+ interface {{MODEL_NAME}}ListViewProps {
118
+ onSelect?: (item: {{MODEL_NAME}}) => void;
119
+ onCreate?: () => void;
120
+ }
121
+
122
+ export function {{MODEL_NAME}}ListView({ onSelect, onCreate }: {{MODEL_NAME}}ListViewProps) {
123
+ const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
124
+ const deleteItem = useDelete{{MODEL_NAME}}Mutation();
125
+ const [searchTerm, setSearchTerm] = useState('');
126
+
127
+ const filtered = useMemo(
128
+ () => items.filter(item =>
129
+ Object.values(item).some(v => String(v).toLowerCase().includes(searchTerm.toLowerCase()))
130
+ ),
131
+ [items, searchTerm]
132
+ );
133
+
134
+ if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
135
+ if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
136
+
137
+ return (
138
+ <div className="p-6 space-y-4">
139
+ <div className="flex justify-between items-center">
140
+ <input
141
+ type="search"
142
+ placeholder="Search {{PLURAL_LOWER}}…"
143
+ value={searchTerm}
144
+ onChange={e => setSearchTerm(e.target.value)}
145
+ className="rounded border px-3 py-2 w-64"
146
+ />
147
+ <button
148
+ onClick={onCreate}
149
+ className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
150
+ >
151
+ + New {{MODEL_NAME}}
152
+ </button>
153
+ </div>
154
+
155
+ {/* {{BODY}} — pattern-rendered table interior, JSX-transformed */}
156
+ {{BODY}}
157
+
158
+ {filtered.length === 0 && (
159
+ <div className="p-8 text-center text-gray-400">No {{PLURAL_LOWER}} yet.</div>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+ ```
165
+
166
+ Substitution contract:
167
+ - `{{MODEL_NAME}}` — PascalCase model name (e.g., `Post`).
168
+ - `{{PLURAL_MODEL}}` — PascalCase plural (e.g., `Posts`).
169
+ - `{{PLURAL_LOWER}}` — lowercase plural (e.g., `posts`).
170
+ - `{{BODY}}` — exactly one substitution per skeleton, where the pattern-rendered interior goes.
171
+
172
+ Substitution is plain string replacement — no template expression evaluation.
173
+
174
+ ## File placement
175
+
176
+ ```
177
+ engines/libs/instance-factories/applications/templates/react-starter/
178
+ ├── README.md # this file
179
+ ├── view-emitter.ts # orchestrator (step 1)
180
+ ├── html-to-jsx.ts # transformer (step 3)
181
+ ├── skeletons/
182
+ │ ├── list.tsx.template # list view
183
+ │ ├── detail.tsx.template # detail view
184
+ │ ├── form.tsx.template # form view (Phase 2e)
185
+ │ ├── dashboard.tsx.template # dashboard (Phase 2e)
186
+ │ └── specialist-*.tsx.template # board/timeline/calendar (Phase 2e)
187
+ └── __tests__/
188
+ ├── html-to-jsx.test.ts # transformer unit tests
189
+ └── view-emitter.test.ts # end-to-end: (spec, view) → valid TSX
190
+ ```
191
+
192
+ ## Build order (Phase 2a in the migration plan)
193
+
194
+ 1. Implement `html-to-jsx.ts` first (smallest, most testable, standalone).
195
+ 2. Write `list.tsx.template` (one view type; production-quality).
196
+ 3. Implement `view-emitter.ts` — orchestrator that wires html-to-jsx + canonical Tailwind adapter + skeleton.
197
+ 4. Integration test: take a reference spec with one model + its list view, emit, assert the `.tsx` source parses and its JSX matches the expected shape.
198
+ 5. Only after list works end-to-end: template out to detail + form + dashboard.
199
+
200
+ ## Open questions (inside Phase 2a)
201
+
202
+ 1. **Where do per-model hooks come from?** `use{{PLURAL_MODEL}}Query` and `useDelete{{MODEL_NAME}}Mutation` in the skeleton presume a generated hooks file (`src/hooks/useApi.ts`) that provides them. `ReactAppRuntime` already generates this file via `use-api-hooks-generator.ts` — Factory B should reuse that generator (shared between both factories as called out in the migration plan).
203
+ 2. **How does the skeleton address auto-generated fields (id, createdAt, etc.)?** The view should skip them in the table/form. `isAutoGeneratedField()` logic needs to be available in the generated output. Plan: emit a `src/lib/field-helpers.ts` alongside the views (inlined from the pattern library's logic, same way the runtime has it).
204
+ 3. **What happens when the view spec has `uiComponents` that the pattern library doesn't recognize?** Runtime gracefully falls back to a default table. Factory B should do the same — same fallback logic, same output.
205
+ 4. **Should the skeleton expose a slot for user-added React nodes (e.g. `<Children />` or a `prepend` prop)?** Useful for extensibility without forcing users to edit the generated file. Defer to v2 of the emitter — start with fixed skeletons.
206
+
207
+ ## Non-goals
208
+
209
+ - No JSX AST parsing or manipulation. `html-to-jsx.ts` is a string transformer, not a compiler.
210
+ - No server-side rendering of the React components. The emitter outputs source files; the user's Vite build bundles them.
211
+ - No runtime customization of skeletons. A project that wants different skeletons forks the factory or the generated code.
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { composeDashboardBody } from '../dashboard-body-composer.js';
4
+ import { emitView, type EmitContext, type ModelSpec } from '../view-emitter.js';
5
+
6
+ function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
7
+ const post: ModelSpec = {
8
+ name: 'Post',
9
+ attributes: {
10
+ id: { type: 'UUID', required: true },
11
+ title: { type: 'String', required: true },
12
+ body: { type: 'Text', required: false },
13
+ status: { type: 'String', required: false, values: ['draft', 'live', 'archived'] },
14
+ createdAt: { type: 'DateTime', required: false },
15
+ },
16
+ };
17
+
18
+ return {
19
+ view: { type: 'dashboard', model: 'Post' },
20
+ viewName: 'PostDashboardView',
21
+ model: post,
22
+ modelSchemas: { Post: post },
23
+ renderBody: composeDashboardBody,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function assertValidTsx(source: string, label: string): void {
29
+ const result = ts.transpileModule(source, {
30
+ compilerOptions: {
31
+ jsx: ts.JsxEmit.Preserve,
32
+ target: ts.ScriptTarget.ES2022,
33
+ module: ts.ModuleKind.ESNext,
34
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
35
+ strict: false,
36
+ },
37
+ reportDiagnostics: true,
38
+ });
39
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
40
+ if (errors.length > 0) {
41
+ const message = errors
42
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
43
+ .join('\n');
44
+ throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
45
+ }
46
+ }
47
+
48
+ describe('composeDashboardBody — metrics row', () => {
49
+ it('emits a total-count card', () => {
50
+ const body = composeDashboardBody(makeContext());
51
+ expect(body).toContain('Total posts');
52
+ expect(body).toContain('{items.length}');
53
+ });
54
+
55
+ it('emits a per-value breakdown card for each enum value when a small enum exists', () => {
56
+ const body = composeDashboardBody(makeContext());
57
+ // The `status` attribute has 3 values. Each gets a card.
58
+ expect(body).toContain('Status: Draft');
59
+ expect(body).toContain('Status: Live');
60
+ expect(body).toContain('Status: Archived');
61
+ // Filter expressions for counts
62
+ expect(body).toContain(`i.status === "draft"`);
63
+ expect(body).toContain(`i.status === "live"`);
64
+ expect(body).toContain(`i.status === "archived"`);
65
+ });
66
+
67
+ it('falls back to a placeholder card when no enum fields exist', () => {
68
+ const plain: ModelSpec = {
69
+ name: 'Plain',
70
+ attributes: {
71
+ id: { type: 'UUID' },
72
+ name: { type: 'String' },
73
+ },
74
+ };
75
+ const body = composeDashboardBody(makeContext({
76
+ model: plain,
77
+ view: { type: 'dashboard', model: 'Plain' },
78
+ viewName: 'PlainDashboardView',
79
+ modelSchemas: { Plain: plain },
80
+ }));
81
+ expect(body).toContain('Add a metric here');
82
+ });
83
+
84
+ it('skips large enums (> 6 values) to avoid card sprawl', () => {
85
+ const huge: ModelSpec = {
86
+ name: 'Huge',
87
+ attributes: {
88
+ id: { type: 'UUID' },
89
+ tier: {
90
+ type: 'String',
91
+ values: ['s', 'a', 'b', 'c', 'd', 'e', 'f'], // 7 → should be skipped
92
+ },
93
+ },
94
+ };
95
+ const body = composeDashboardBody(makeContext({
96
+ model: huge,
97
+ view: { type: 'dashboard', model: 'Huge' },
98
+ viewName: 'HugeDashboardView',
99
+ modelSchemas: { Huge: huge },
100
+ }));
101
+ expect(body).not.toContain('Tier: S');
102
+ expect(body).toContain('Add a metric here'); // placeholder kicked in
103
+ });
104
+ });
105
+
106
+ describe('composeDashboardBody — recent preview', () => {
107
+ it('emits a table with business-field columns (metadata excluded)', () => {
108
+ const body = composeDashboardBody(makeContext());
109
+ expect(body).toContain('Recent Posts');
110
+ expect(body).toContain('>Title<');
111
+ expect(body).toContain('>Body<');
112
+ expect(body).toContain('>Status<');
113
+ // metadata excluded
114
+ expect(body).not.toContain('>Id<');
115
+ expect(body).not.toContain('>Created At<');
116
+ });
117
+
118
+ it('maps over `preview` (from the skeleton), not `items`', () => {
119
+ const body = composeDashboardBody(makeContext());
120
+ expect(body).toContain('{preview.map((item, idx) =>');
121
+ });
122
+
123
+ it('handles empty-list state with a spanning row', () => {
124
+ const body = composeDashboardBody(makeContext());
125
+ expect(body).toContain('{preview.length === 0 && (');
126
+ expect(body).toContain('No records yet.');
127
+ });
128
+ });
129
+
130
+ describe('composeDashboardBody — TODO comments', () => {
131
+ it('flags the extension point for aggregation metrics', () => {
132
+ const body = composeDashboardBody(makeContext());
133
+ expect(body).toContain('TODO: add aggregation metrics');
134
+ });
135
+ });
136
+
137
+ describe('emitView wired to composeDashboardBody — end-to-end', () => {
138
+ it('produces a complete PostDashboardView.tsx that parses', () => {
139
+ const source = emitView(makeContext());
140
+ assertValidTsx(source, 'PostDashboardView');
141
+ });
142
+
143
+ it('wires the skeleton: component, hooks, preview memo, body', () => {
144
+ const source = emitView(makeContext());
145
+ expect(source).toContain('export function PostDashboardView');
146
+ expect(source).toContain('usePostsQuery');
147
+ expect(source).toContain('Post dashboard');
148
+ expect(source).toContain('items.slice(0, previewLimit)');
149
+ expect(source).toContain('Total posts');
150
+ expect(source).toContain('Recent Posts');
151
+ expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
152
+ });
153
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { composeDetailBody } from '../detail-body-composer.js';
4
+ import { emitView, type EmitContext, type ModelSpec } from '../view-emitter.js';
5
+
6
+ function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
7
+ const post: ModelSpec = {
8
+ name: 'Post',
9
+ attributes: {
10
+ id: { type: 'UUID', required: true },
11
+ title: { type: 'String', required: true },
12
+ body: { type: 'Text', required: false },
13
+ authorId: { type: 'UUID', required: true },
14
+ createdAt: { type: 'DateTime', required: false },
15
+ publishedAt: { type: 'DateTime', required: false },
16
+ },
17
+ relationships: {
18
+ author: { type: 'belongsTo', targetModel: 'Author' },
19
+ },
20
+ };
21
+
22
+ return {
23
+ view: { type: 'detail', model: 'Post' },
24
+ viewName: 'PostDetailView',
25
+ model: post,
26
+ modelSchemas: { Post: post },
27
+ renderBody: composeDetailBody,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function assertValidTsx(source: string, label: string): void {
33
+ const result = ts.transpileModule(source, {
34
+ compilerOptions: {
35
+ jsx: ts.JsxEmit.Preserve,
36
+ target: ts.ScriptTarget.ES2022,
37
+ module: ts.ModuleKind.ESNext,
38
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
39
+ strict: false,
40
+ },
41
+ reportDiagnostics: true,
42
+ });
43
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
44
+ if (errors.length > 0) {
45
+ const message = errors
46
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
47
+ .join('\n');
48
+ throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
49
+ }
50
+ }
51
+
52
+ describe('composeDetailBody — direct output', () => {
53
+ it('groups fields into business / belongsTo / metadata sections', () => {
54
+ const body = composeDetailBody(makeContext());
55
+
56
+ // Business fields (first <dl>)
57
+ expect(body).toContain('>Title<');
58
+ expect(body).toContain('>Body<');
59
+
60
+ // belongsTo section — surfaces authorId and notes the FK→name gap
61
+ expect(body).toContain('>Author<');
62
+ expect(body).toContain('.authorId ??');
63
+ expect(body).toContain('TODO: resolve FK ids');
64
+
65
+ // Metadata section (id / createdAt / publishedAt) — muted styling
66
+ expect(body).toContain('>Id<');
67
+ expect(body).toContain('>Created At<');
68
+ expect(body).toContain('>Published At<');
69
+ expect(body).toContain('text-xs text-gray-500');
70
+
71
+ // Business section is separate from metadata (no mixing)
72
+ const businessIdx = body.indexOf('>Title<');
73
+ const metadataIdx = body.indexOf('>Id<');
74
+ expect(businessIdx).toBeLessThan(metadataIdx);
75
+ });
76
+
77
+ it('emits one <dt>/<dd> per field with (item as any).FIELD access', () => {
78
+ const body = composeDetailBody(makeContext());
79
+ expect(body).toContain('(item as any).title ??');
80
+ expect(body).toContain('(item as any).body ??');
81
+ expect(body).toContain('(item as any).id ??');
82
+ expect(body).toContain('(item as any).createdAt ??');
83
+ });
84
+
85
+ it('falls back to a "no business fields" message when all attrs are metadata', () => {
86
+ const mostlyMetadata: ModelSpec = {
87
+ name: 'Stub',
88
+ attributes: {
89
+ id: { type: 'UUID' },
90
+ createdAt: { type: 'DateTime' },
91
+ },
92
+ };
93
+ const body = composeDetailBody(makeContext({
94
+ model: mostlyMetadata,
95
+ view: { type: 'detail', model: 'Stub' },
96
+ viewName: 'StubDetailView',
97
+ modelSchemas: { Stub: mostlyMetadata },
98
+ }));
99
+ expect(body).toContain('No business fields defined');
100
+ });
101
+
102
+ it('skips the belongsTo section when no belongsTo relationships exist', () => {
103
+ const solo: ModelSpec = {
104
+ name: 'Solo',
105
+ attributes: { id: {}, name: {} },
106
+ relationships: {
107
+ tags: { type: 'hasMany', targetModel: 'Tag' }, // hasMany, not belongsTo
108
+ },
109
+ };
110
+ const body = composeDetailBody(makeContext({
111
+ model: solo,
112
+ view: { type: 'detail', model: 'Solo' },
113
+ viewName: 'SoloDetailView',
114
+ modelSchemas: { Solo: solo },
115
+ }));
116
+ expect(body).not.toContain('TODO: resolve FK');
117
+ expect(body).toContain('>Name<');
118
+ });
119
+ });
120
+
121
+ describe('emitView wired to composeDetailBody — end-to-end', () => {
122
+ it('produces a complete PostDetailView.tsx that parses', () => {
123
+ const source = emitView(makeContext());
124
+ assertValidTsx(source, 'PostDetailView');
125
+ });
126
+
127
+ it('wires the skeleton: component, hooks, actions, body', () => {
128
+ const source = emitView(makeContext());
129
+ expect(source).toContain('export function PostDetailView');
130
+ expect(source).toContain('usePostsQuery');
131
+ expect(source).toContain('useDeletePostMutation');
132
+ expect(source).toContain('Post detail');
133
+ expect(source).toContain('onEdit(item)');
134
+ expect(source).toContain("confirm('Delete this post?')");
135
+ expect(source).toContain('(item as any).title ??');
136
+ expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
137
+ });
138
+
139
+ it('handles plural singular correctly for the loading / not-found copy', () => {
140
+ const source = emitView(makeContext());
141
+ expect(source).toContain('Loading…');
142
+ // SINGULAR_LOWER substitution: "No post matching id ..."
143
+ expect(source).toContain('No post matching id');
144
+ });
145
+ });
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { composeFormBody } from '../form-body-composer.js';
4
+ import { emitView, type EmitContext, type ModelSpec } from '../view-emitter.js';
5
+
6
+ function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
7
+ const post: ModelSpec = {
8
+ name: 'Post',
9
+ attributes: {
10
+ id: { type: 'UUID', required: true, auto: 'uuid4' },
11
+ title: { type: 'String', required: true },
12
+ body: { type: 'Text', required: false },
13
+ published: { type: 'Boolean', required: false },
14
+ score: { type: 'Integer', required: false },
15
+ email: { type: 'Email', required: false },
16
+ status: { type: 'String', required: false, values: ['draft', 'live', 'archived'] },
17
+ authorId: { type: 'UUID', required: true }, // belongsTo FK
18
+ createdAt: { type: 'DateTime', required: false }, // metadata
19
+ },
20
+ relationships: {
21
+ author: { type: 'belongsTo', targetModel: 'Author' },
22
+ },
23
+ };
24
+
25
+ return {
26
+ view: { type: 'form', model: 'Post' },
27
+ viewName: 'PostFormView',
28
+ model: post,
29
+ modelSchemas: { Post: post },
30
+ renderBody: composeFormBody,
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ function assertValidTsx(source: string, label: string): void {
36
+ const result = ts.transpileModule(source, {
37
+ compilerOptions: {
38
+ jsx: ts.JsxEmit.Preserve,
39
+ target: ts.ScriptTarget.ES2022,
40
+ module: ts.ModuleKind.ESNext,
41
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
42
+ strict: false,
43
+ },
44
+ reportDiagnostics: true,
45
+ });
46
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
47
+ if (errors.length > 0) {
48
+ const message = errors
49
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
50
+ .join('\n');
51
+ throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
52
+ }
53
+ }
54
+
55
+ describe('composeFormBody — field selection', () => {
56
+ it('omits auto-generated fields (id, createdAt) and attrs with auto markers', () => {
57
+ const body = composeFormBody(makeContext());
58
+ expect(body).not.toMatch(/htmlFor="id"/);
59
+ expect(body).not.toMatch(/htmlFor="createdAt"/);
60
+ });
61
+
62
+ it('includes business fields in order', () => {
63
+ const body = composeFormBody(makeContext());
64
+ expect(body).toContain('htmlFor="title"');
65
+ expect(body).toContain('htmlFor="body"');
66
+ expect(body).toContain('htmlFor="score"');
67
+ });
68
+
69
+ it('renders a message when there are no editable fields', () => {
70
+ const emptyish: ModelSpec = {
71
+ name: 'Bare',
72
+ attributes: {
73
+ id: { type: 'UUID', auto: 'uuid4' },
74
+ createdAt: { type: 'DateTime' },
75
+ },
76
+ };
77
+ const body = composeFormBody(makeContext({
78
+ model: emptyish,
79
+ view: { type: 'form', model: 'Bare' },
80
+ viewName: 'BareFormView',
81
+ modelSchemas: { Bare: emptyish },
82
+ }));
83
+ expect(body).toContain('No editable fields for this model.');
84
+ });
85
+ });
86
+
87
+ describe('composeFormBody — input type per attribute', () => {
88
+ it('String with values=[...] → select', () => {
89
+ const body = composeFormBody(makeContext());
90
+ expect(body).toMatch(/<select\s[^>]*id="status"/);
91
+ expect(body).toContain('<option value="draft">draft</option>');
92
+ expect(body).toContain('<option value="live">live</option>');
93
+ });
94
+
95
+ it('Text → textarea', () => {
96
+ const body = composeFormBody(makeContext());
97
+ expect(body).toMatch(/<textarea\s[^>]*id="body"/);
98
+ });
99
+
100
+ it('Boolean → checkbox', () => {
101
+ const body = composeFormBody(makeContext());
102
+ expect(body).toContain('type="checkbox"');
103
+ expect(body).toContain('id="published"');
104
+ expect(body).toContain("checked={Boolean((formData as any).published)}");
105
+ });
106
+
107
+ it('Integer → number input with Number() coercion', () => {
108
+ const body = composeFormBody(makeContext());
109
+ expect(body).toMatch(/id="score"[\s\S]*type="number"/);
110
+ expect(body).toContain("Number(e.target.value)");
111
+ });
112
+
113
+ it('Email → email input', () => {
114
+ const body = composeFormBody(makeContext());
115
+ expect(body).toMatch(/id="email"[\s\S]*type="email"/);
116
+ });
117
+
118
+ it('String default → text input', () => {
119
+ const body = composeFormBody(makeContext());
120
+ expect(body).toMatch(/id="title"[\s\S]*type="text"/);
121
+ });
122
+
123
+ it('UUID → text input (treated like string)', () => {
124
+ const body = composeFormBody(makeContext());
125
+ expect(body).toMatch(/id="authorId"[\s\S]*type="text"/);
126
+ });
127
+ });
128
+
129
+ describe('composeFormBody — required markers', () => {
130
+ it('adds required HTML attribute to required inputs; omits it on optional ones', () => {
131
+ const body = composeFormBody(makeContext());
132
+ // The bareword ` required` attribute, emitted at end of an onChange
133
+ // line. Required fields in the fixture: title, authorId (belongsTo FK
134
+ // emitted as required). Optional: body, published, score, email, status.
135
+ const occurrences = (body.match(/ required$/gm) ?? []).length;
136
+ expect(occurrences).toBe(2);
137
+ });
138
+
139
+ it("appends ' *' to the label of required fields", () => {
140
+ const body = composeFormBody(makeContext());
141
+ expect(body).toContain('Title *');
142
+ // body is not required
143
+ expect(body).not.toContain('Body *');
144
+ });
145
+ });
146
+
147
+ describe('composeFormBody — belongsTo section', () => {
148
+ it('surfaces belongsTo FK inputs with a TODO comment', () => {
149
+ const body = composeFormBody(makeContext());
150
+ expect(body).toContain('TODO: swap these text inputs for <select>s');
151
+ // authorId is emitted as a text input
152
+ expect(body).toMatch(/id="authorId"[\s\S]*?type="text"/);
153
+ });
154
+ });
155
+
156
+ describe('emitView wired to composeFormBody — end-to-end', () => {
157
+ it('produces a complete PostFormView.tsx that parses', () => {
158
+ const source = emitView(makeContext());
159
+ assertValidTsx(source, 'PostFormView');
160
+ });
161
+
162
+ it('wires the skeleton: component, hooks, modes, body', () => {
163
+ const source = emitView(makeContext());
164
+ expect(source).toContain('export function PostFormView');
165
+ expect(source).toContain('useCreatePostMutation');
166
+ expect(source).toContain('useUpdatePostMutation');
167
+ expect(source).toContain('mode === \'create\'');
168
+ expect(source).toContain('Create Post');
169
+ expect(source).toContain('Update Post');
170
+ // Body injected
171
+ expect(source).toContain('htmlFor="title"');
172
+ expect(source).toContain('<textarea');
173
+ expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
174
+ });
175
+ });