@specverse/engines 4.1.28 → 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 (237) 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/generic/main-generator.js +3 -3
  45. package/dist/libs/instance-factories/applications/templates/react/api-client-generator.js +16 -6
  46. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +110 -0
  47. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +121 -0
  48. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +78 -0
  49. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +190 -0
  50. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +45 -0
  51. package/dist/libs/instance-factories/applications/templates/react-starter/html-to-jsx.js +192 -0
  52. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +46 -0
  53. package/dist/libs/instance-factories/applications/templates/react-starter/orchestrator.js +30 -0
  54. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +38 -0
  55. package/dist/libs/instance-factories/applications/templates/react-starter/regen-safety.js +89 -0
  56. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +56 -0
  57. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +66 -0
  58. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +14 -11
  59. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +11 -3
  60. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +27 -17
  61. package/dist/libs/instance-factories/shared/path-resolver.js +1 -1
  62. package/dist/realize/index.d.ts.map +1 -1
  63. package/dist/realize/index.js +15 -22
  64. package/dist/realize/index.js.map +1 -1
  65. package/dist/registry/utils/manifest-adapter.d.ts +8 -1
  66. package/dist/registry/utils/manifest-adapter.d.ts.map +1 -1
  67. package/dist/registry/utils/manifest-adapter.js +8 -1
  68. package/dist/registry/utils/manifest-adapter.js.map +1 -1
  69. package/libs/instance-factories/applications/react-app-starter.yaml +150 -0
  70. package/libs/instance-factories/applications/templates/generic/main-generator.ts +3 -3
  71. package/libs/instance-factories/applications/templates/react/api-client-generator.ts +16 -6
  72. package/libs/instance-factories/applications/templates/react-starter/README.md +211 -0
  73. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +153 -0
  74. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +145 -0
  75. package/libs/instance-factories/applications/templates/react-starter/__tests__/form-body-composer.test.ts +175 -0
  76. package/libs/instance-factories/applications/templates/react-starter/__tests__/helpers-emitter.test.ts +55 -0
  77. package/libs/instance-factories/applications/templates/react-starter/__tests__/html-to-jsx.test.ts +140 -0
  78. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +146 -0
  79. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +163 -0
  80. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p2-factory-imports.test.ts +116 -0
  81. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +183 -0
  82. package/libs/instance-factories/applications/templates/react-starter/__tests__/regen-safety.test.ts +144 -0
  83. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +114 -0
  84. package/libs/instance-factories/applications/templates/react-starter/__tests__/view-emitter.test.ts +107 -0
  85. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +139 -0
  86. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +141 -0
  87. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +174 -0
  88. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +135 -0
  89. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +306 -0
  90. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +60 -0
  91. package/libs/instance-factories/applications/templates/react-starter/html-to-jsx.ts +334 -0
  92. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +120 -0
  93. package/libs/instance-factories/applications/templates/react-starter/orchestrator.ts +80 -0
  94. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +57 -0
  95. package/libs/instance-factories/applications/templates/react-starter/regen-safety.ts +157 -0
  96. package/libs/instance-factories/applications/templates/react-starter/skeletons/dashboard.tsx.template +47 -0
  97. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +94 -0
  98. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +114 -0
  99. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +72 -0
  100. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +151 -0
  101. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +137 -0
  102. package/libs/instance-factories/cli/templates/commander/command-generator.ts +14 -11
  103. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +11 -3
  104. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +27 -17
  105. package/libs/instance-factories/shared/path-resolver.ts +8 -2
  106. package/package.json +3 -3
  107. package/dist/libs/instance-factories/applications/templates/react/_view-components-source.js +0 -530
  108. package/dist/libs/instance-factories/applications/templates/react/app-tsx-generator.js +0 -73
  109. package/dist/libs/instance-factories/applications/templates/react/field-helpers-generator.js +0 -99
  110. package/dist/libs/instance-factories/applications/templates/react/package-json-generator.js +0 -49
  111. package/dist/libs/instance-factories/applications/templates/react/pattern-adapter-generator.js +0 -156
  112. package/dist/libs/instance-factories/applications/templates/react/react-pattern-adapter.js +0 -935
  113. package/dist/libs/instance-factories/applications/templates/react/relationship-field-generator.js +0 -143
  114. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.js +0 -646
  115. package/dist/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.js +0 -65
  116. package/dist/libs/instance-factories/applications/templates/react/view-dashboard-generator.js +0 -143
  117. package/dist/libs/instance-factories/applications/templates/react/view-detail-generator.js +0 -143
  118. package/dist/libs/instance-factories/applications/templates/react/view-form-generator.js +0 -355
  119. package/dist/libs/instance-factories/applications/templates/react/view-list-generator.js +0 -91
  120. package/dist/libs/instance-factories/applications/templates/react/view-router-generator.js +0 -79
  121. package/dist/libs/instance-factories/tools/templates/vscode/vscode-extension-generator.js.bak +0 -244
  122. package/dist/libs/instance-factories/views/index.js +0 -48
  123. package/dist/libs/instance-factories/views/templates/react/adapters/antd-adapter.js +0 -742
  124. package/dist/libs/instance-factories/views/templates/react/adapters/mui-adapter.js +0 -824
  125. package/dist/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.js +0 -719
  126. package/dist/libs/instance-factories/views/templates/react/app-generator.js +0 -45
  127. package/dist/libs/instance-factories/views/templates/react/components-generator.js +0 -820
  128. package/dist/libs/instance-factories/views/templates/react/forms-generator.js +0 -275
  129. package/dist/libs/instance-factories/views/templates/react/frontend-package-json-generator.js +0 -46
  130. package/dist/libs/instance-factories/views/templates/react/hooks-generator.js +0 -81
  131. package/dist/libs/instance-factories/views/templates/react/index-css-generator.js +0 -9
  132. package/dist/libs/instance-factories/views/templates/react/index-html-generator.js +0 -23
  133. package/dist/libs/instance-factories/views/templates/react/main-tsx-generator.js +0 -21
  134. package/dist/libs/instance-factories/views/templates/react/react-component-generator.js +0 -299
  135. package/dist/libs/instance-factories/views/templates/react/router-generator.js +0 -136
  136. package/dist/libs/instance-factories/views/templates/react/router-generic-generator.js +0 -107
  137. package/dist/libs/instance-factories/views/templates/react/shared-utils-generator.js +0 -187
  138. package/dist/libs/instance-factories/views/templates/react/spec-json-generator.js +0 -7
  139. package/dist/libs/instance-factories/views/templates/react/types-generator.js +0 -56
  140. package/dist/libs/instance-factories/views/templates/react/views-metadata-generator.js +0 -27
  141. package/dist/libs/instance-factories/views/templates/react/vite-config-generator.js +0 -29
  142. package/dist/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js +0 -261
  143. package/dist/libs/instance-factories/views/templates/shared/adapter-types.js +0 -34
  144. package/dist/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -800
  145. package/dist/libs/instance-factories/views/templates/shared/base-generator.js +0 -305
  146. package/dist/libs/instance-factories/views/templates/shared/component-metadata.js +0 -517
  147. package/dist/libs/instance-factories/views/templates/shared/composite-pattern-types.js +0 -0
  148. package/dist/libs/instance-factories/views/templates/shared/composite-patterns.js +0 -445
  149. package/dist/libs/instance-factories/views/templates/shared/index.js +0 -80
  150. package/dist/libs/instance-factories/views/templates/shared/pattern-validator.js +0 -210
  151. package/dist/libs/instance-factories/views/templates/shared/property-mapper.js +0 -492
  152. package/dist/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -321
  153. package/dist/realize/index.js.bak +0 -758
  154. package/libs/instance-factories/applications/react-app.yaml +0 -186
  155. package/libs/instance-factories/applications/templates/react/_view-components-source.ts +0 -555
  156. package/libs/instance-factories/applications/templates/react/app-tsx-generator.ts +0 -94
  157. package/libs/instance-factories/applications/templates/react/field-helpers-generator.ts +0 -106
  158. package/libs/instance-factories/applications/templates/react/package-json-generator.ts +0 -57
  159. package/libs/instance-factories/applications/templates/react/pattern-adapter-generator.ts +0 -179
  160. package/libs/instance-factories/applications/templates/react/react-pattern-adapter.tsx +0 -1347
  161. package/libs/instance-factories/applications/templates/react/relationship-field-generator.ts +0 -150
  162. package/libs/instance-factories/applications/templates/react/tailwind-adapter-generator.ts +0 -704
  163. package/libs/instance-factories/applications/templates/react/tailwind-adapter-wrapper-generator.ts +0 -84
  164. package/libs/instance-factories/applications/templates/react/view-dashboard-generator.ts +0 -150
  165. package/libs/instance-factories/applications/templates/react/view-detail-generator.ts +0 -150
  166. package/libs/instance-factories/applications/templates/react/view-form-generator.ts +0 -362
  167. package/libs/instance-factories/applications/templates/react/view-list-generator.ts +0 -98
  168. package/libs/instance-factories/applications/templates/react/view-router-generator.ts +0 -89
  169. package/libs/instance-factories/views/README.md +0 -62
  170. package/libs/instance-factories/views/index.d.ts +0 -13
  171. package/libs/instance-factories/views/index.d.ts.map +0 -1
  172. package/libs/instance-factories/views/index.js +0 -18
  173. package/libs/instance-factories/views/index.js.map +0 -1
  174. package/libs/instance-factories/views/index.ts +0 -45
  175. package/libs/instance-factories/views/react-components.yaml +0 -129
  176. package/libs/instance-factories/views/templates/ARCHITECTURE.md +0 -198
  177. package/libs/instance-factories/views/templates/react/adapters/antd-adapter.ts +0 -869
  178. package/libs/instance-factories/views/templates/react/adapters/mui-adapter.ts +0 -953
  179. package/libs/instance-factories/views/templates/react/adapters/shadcn-adapter.ts +0 -806
  180. package/libs/instance-factories/views/templates/react/app-generator.ts +0 -55
  181. package/libs/instance-factories/views/templates/react/components-generator.ts +0 -938
  182. package/libs/instance-factories/views/templates/react/forms-generator.ts +0 -325
  183. package/libs/instance-factories/views/templates/react/frontend-package-json-generator.ts +0 -57
  184. package/libs/instance-factories/views/templates/react/hooks-generator.ts +0 -106
  185. package/libs/instance-factories/views/templates/react/index-css-generator.ts +0 -14
  186. package/libs/instance-factories/views/templates/react/index-html-generator.ts +0 -34
  187. package/libs/instance-factories/views/templates/react/main-tsx-generator.ts +0 -29
  188. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts +0 -152
  189. package/libs/instance-factories/views/templates/react/react-component-generator.d.ts.map +0 -1
  190. package/libs/instance-factories/views/templates/react/react-component-generator.js +0 -398
  191. package/libs/instance-factories/views/templates/react/react-component-generator.js.map +0 -1
  192. package/libs/instance-factories/views/templates/react/react-component-generator.ts +0 -533
  193. package/libs/instance-factories/views/templates/react/router-generator.ts +0 -197
  194. package/libs/instance-factories/views/templates/react/router-generic-generator.ts +0 -132
  195. package/libs/instance-factories/views/templates/react/shared-utils-generator.ts +0 -196
  196. package/libs/instance-factories/views/templates/react/spec-json-generator.ts +0 -17
  197. package/libs/instance-factories/views/templates/react/types-generator.ts +0 -76
  198. package/libs/instance-factories/views/templates/react/views-metadata-generator.ts +0 -42
  199. package/libs/instance-factories/views/templates/react/vite-config-generator.ts +0 -38
  200. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.d.ts.map +0 -1
  201. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.js.map +0 -1
  202. package/libs/instance-factories/views/templates/runtime/runtime-view-renderer.ts +0 -474
  203. package/libs/instance-factories/views/templates/shared/__tests__/composite-patterns.test.ts +0 -242
  204. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts +0 -77
  205. package/libs/instance-factories/views/templates/shared/adapter-types.d.ts.map +0 -1
  206. package/libs/instance-factories/views/templates/shared/adapter-types.js +0 -47
  207. package/libs/instance-factories/views/templates/shared/adapter-types.js.map +0 -1
  208. package/libs/instance-factories/views/templates/shared/adapter-types.ts +0 -142
  209. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts +0 -63
  210. package/libs/instance-factories/views/templates/shared/atomic-components-registry.d.ts.map +0 -1
  211. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js +0 -822
  212. package/libs/instance-factories/views/templates/shared/atomic-components-registry.js.map +0 -1
  213. package/libs/instance-factories/views/templates/shared/atomic-components-registry.ts +0 -908
  214. package/libs/instance-factories/views/templates/shared/base-generator.d.ts +0 -247
  215. package/libs/instance-factories/views/templates/shared/base-generator.d.ts.map +0 -1
  216. package/libs/instance-factories/views/templates/shared/base-generator.js +0 -363
  217. package/libs/instance-factories/views/templates/shared/base-generator.js.map +0 -1
  218. package/libs/instance-factories/views/templates/shared/base-generator.ts +0 -608
  219. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts +0 -254
  220. package/libs/instance-factories/views/templates/shared/component-metadata.d.ts.map +0 -1
  221. package/libs/instance-factories/views/templates/shared/component-metadata.js +0 -602
  222. package/libs/instance-factories/views/templates/shared/component-metadata.js.map +0 -1
  223. package/libs/instance-factories/views/templates/shared/component-metadata.ts +0 -803
  224. package/libs/instance-factories/views/templates/shared/composite-pattern-types.ts +0 -250
  225. package/libs/instance-factories/views/templates/shared/composite-patterns.ts +0 -535
  226. package/libs/instance-factories/views/templates/shared/index.ts +0 -68
  227. package/libs/instance-factories/views/templates/shared/pattern-validator.ts +0 -279
  228. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts +0 -149
  229. package/libs/instance-factories/views/templates/shared/property-mapper.d.ts.map +0 -1
  230. package/libs/instance-factories/views/templates/shared/property-mapper.js +0 -580
  231. package/libs/instance-factories/views/templates/shared/property-mapper.js.map +0 -1
  232. package/libs/instance-factories/views/templates/shared/property-mapper.ts +0 -700
  233. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts +0 -143
  234. package/libs/instance-factories/views/templates/shared/syntax-mapper.d.ts.map +0 -1
  235. package/libs/instance-factories/views/templates/shared/syntax-mapper.js +0 -420
  236. package/libs/instance-factories/views/templates/shared/syntax-mapper.js.map +0 -1
  237. package/libs/instance-factories/views/templates/shared/syntax-mapper.ts +0 -539
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Detail-view body composer for ReactAppStarter
3
+ *
4
+ * Renders the interior of a detail view as a JSX-safe definition list:
5
+ * business fields first (prominent), metadata fields second (muted).
6
+ * The outer card + actions live in `skeletons/detail.tsx.template`.
7
+ *
8
+ * Unlike list view, the interior here is mostly label→value pairs, not
9
+ * a table. We use `<dl>` / `<dt>` / `<dd>` directly rather than routing
10
+ * through a specific atomic component — the field-list shape isn't one
11
+ * of the adapter's atomic components, and forcing it through a `card`
12
+ * adapter would add structure without clarifying anything in the
13
+ * generated code.
14
+ *
15
+ * BelongsTo relationships: today we emit the raw FK id. A future pass
16
+ * will resolve FK → entity display name (matches runtime's
17
+ * getEntityDisplayName). Marked with a TODO comment in the output so
18
+ * users inspecting the generated file see the hook.
19
+ */
20
+
21
+ import { METADATA_FIELDS } from '@specverse/runtime/views/core';
22
+ import type { EmitContext, ModelSpec } from './view-emitter.js';
23
+
24
+ /**
25
+ * Metadata attribute names rendered in a muted style. Sourced from
26
+ * the canonical pattern library so both this composer and the rest
27
+ * of the system stay in sync automatically.
28
+ */
29
+ const METADATA_FIELD_NAMES = new Set(METADATA_FIELDS);
30
+
31
+ export function composeDetailBody(context: EmitContext): string {
32
+ const { business, metadata } = partitionFields(context.model);
33
+ const belongsTo = extractBelongsTo(context.model);
34
+
35
+ const lines: string[] = [];
36
+ lines.push('<div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">');
37
+
38
+ if (business.length > 0) {
39
+ lines.push(' <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">');
40
+ for (const field of business) {
41
+ lines.push(...renderField(field, { muted: false }));
42
+ }
43
+ lines.push(' </dl>');
44
+ } else {
45
+ lines.push(' <p className="text-sm text-gray-400">No business fields defined for this model.</p>');
46
+ }
47
+
48
+ if (belongsTo.length > 0) {
49
+ lines.push('');
50
+ lines.push(' {/* TODO: resolve FK ids → related entity display names.');
51
+ lines.push(' See @specverse/runtime/views/core/entity-display for the');
52
+ lines.push(' canonical resolver, or load the related record in a hook. */}');
53
+ lines.push(' <dl className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4">');
54
+ for (const rel of belongsTo) {
55
+ lines.push(...renderField({ name: `${rel}Id`, label: humanize(rel) }, { muted: false }));
56
+ }
57
+ lines.push(' </dl>');
58
+ }
59
+
60
+ if (metadata.length > 0) {
61
+ lines.push('');
62
+ lines.push(' <dl className="mt-6 grid grid-cols-1 gap-2 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-4 text-xs text-gray-500 dark:text-gray-400">');
63
+ for (const field of metadata) {
64
+ lines.push(...renderField(field, { muted: true }));
65
+ }
66
+ lines.push(' </dl>');
67
+ }
68
+
69
+ lines.push('</div>');
70
+ return lines.join('\n');
71
+ }
72
+
73
+ interface Field {
74
+ name: string;
75
+ /** Display label; defaults to humanize(name). */
76
+ label?: string;
77
+ }
78
+
79
+ function partitionFields(model: ModelSpec): { business: Field[]; metadata: Field[] } {
80
+ const attrs = Object.keys(model.attributes ?? {});
81
+ const business: Field[] = [];
82
+ const metadata: Field[] = [];
83
+ for (const name of attrs) {
84
+ const field = { name, label: humanize(name) };
85
+ if (METADATA_FIELD_NAMES.has(name)) {
86
+ metadata.push(field);
87
+ } else {
88
+ business.push(field);
89
+ }
90
+ }
91
+ return { business, metadata };
92
+ }
93
+
94
+ /**
95
+ * Extract belongsTo relationship names from the model. Returns e.g.
96
+ * `['author']` for a Post that `belongsTo Author`. The skeleton's
97
+ * item already has `authorId` as a scalar field — we surface it
98
+ * explicitly in its own section so the generated UI separates "this
99
+ * entity's data" from "related entities."
100
+ */
101
+ function extractBelongsTo(model: ModelSpec): string[] {
102
+ const rels = model.relationships ?? {};
103
+ const out: string[] = [];
104
+ for (const [name, def] of Object.entries(rels)) {
105
+ const d = def as { type?: string };
106
+ if (d?.type === 'belongsTo') out.push(name);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function renderField(field: Field, opts: { muted: boolean }): string[] {
112
+ const label = field.label ?? humanize(field.name);
113
+ const labelCls = opts.muted
114
+ ? 'font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500'
115
+ : 'text-sm font-medium text-gray-500 dark:text-gray-400';
116
+ const valueCls = opts.muted
117
+ ? 'text-gray-500 dark:text-gray-400'
118
+ : 'mt-1 text-sm text-gray-900 dark:text-gray-100 break-words';
119
+
120
+ return [
121
+ ' <div>',
122
+ ` <dt className="${labelCls}">${label}</dt>`,
123
+ ` <dd className="${valueCls}">` +
124
+ `{String((item as any).${field.name} ?? '')}` +
125
+ '</dd>',
126
+ ' </div>',
127
+ ];
128
+ }
129
+
130
+ function humanize(name: string): string {
131
+ return name
132
+ .replace(/([A-Z])/g, ' $1')
133
+ .replace(/^./, c => c.toUpperCase())
134
+ .trim();
135
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Form-view body composer for ReactAppStarter
3
+ *
4
+ * Emits one <label>+<input> pair per editable attribute of the model.
5
+ * Input type is inferred from the attribute's type + constraints
6
+ * (mirrors the canonical mapping in
7
+ * `entities/src/core/views/inference/component-mappings.json`, minus
8
+ * the atomic-component indirection).
9
+ *
10
+ * Auto-generated fields (id, timestamps, `auto=...` markers) are
11
+ * omitted — the backend assigns them.
12
+ *
13
+ * Relationship fields (belongsTo) are emitted as plain-text inputs
14
+ * for the FK column with a TODO comment. Wiring them to a proper
15
+ * dropdown requires a hook that loads the related list at form render
16
+ * time; deferred to a later pass. The inline TODO is the hook for
17
+ * users who want to upgrade.
18
+ */
19
+
20
+ import { METADATA_FIELDS } from '@specverse/runtime/views/core';
21
+ import type { EmitContext, ModelSpec } from './view-emitter.js';
22
+
23
+ /**
24
+ * Attribute names the backend always generates — never render as
25
+ * inputs. Sourced from the pattern library's metadata set; any
26
+ * attribute with `auto=...` or category=metadata is additionally
27
+ * skipped via per-field checks in `selectFormFields` below.
28
+ */
29
+ const AUTO_GENERATED_FIELD_NAMES = new Set(METADATA_FIELDS);
30
+
31
+ /**
32
+ * Compose the form body as JSX-safe source. Dropped at `{{BODY}}`.
33
+ */
34
+ export function composeFormBody(context: EmitContext): string {
35
+ const belongsTo = extractBelongsTo(context.model);
36
+ // FK columns shadowed by a belongsTo relationship are emitted in the
37
+ // belongsTo section with a nicer label ("Author" instead of
38
+ // "Author Id") — skip them in the main loop so we don't render twice.
39
+ const shadowedFKs = new Set(belongsTo.map(rel => `${rel}Id`));
40
+ const fields = selectFormFields(context.model, shadowedFKs);
41
+
42
+ const lines: string[] = [];
43
+ lines.push('<div className="space-y-4">');
44
+
45
+ if (fields.length === 0 && belongsTo.length === 0) {
46
+ lines.push(' <p className="text-sm text-gray-400">No editable fields for this model.</p>');
47
+ lines.push('</div>');
48
+ return lines.join('\n');
49
+ }
50
+
51
+ for (const field of fields) {
52
+ lines.push(...renderInput(field));
53
+ }
54
+
55
+ if (belongsTo.length > 0) {
56
+ lines.push('');
57
+ lines.push(' {/* TODO: swap these text inputs for <select>s that load the related');
58
+ lines.push(' entities. See useEntitiesQuery(TargetModel) in the runtime hooks. */}');
59
+ for (const rel of belongsTo) {
60
+ lines.push(...renderInput({
61
+ name: `${rel}Id`,
62
+ label: humanize(rel),
63
+ type: 'String',
64
+ required: true,
65
+ }));
66
+ }
67
+ }
68
+
69
+ lines.push('</div>');
70
+ return lines.join('\n');
71
+ }
72
+
73
+ // ──────────────────────────────────────────────────────────────────────
74
+ // Field selection
75
+ // ──────────────────────────────────────────────────────────────────────
76
+
77
+ interface FieldInfo {
78
+ name: string;
79
+ label: string;
80
+ type: string;
81
+ required: boolean;
82
+ values?: string[]; // enum options if present
83
+ isLongString?: boolean;
84
+ auto?: boolean;
85
+ }
86
+
87
+ function selectFormFields(
88
+ model: ModelSpec,
89
+ skip: Set<string> = new Set()
90
+ ): FieldInfo[] {
91
+ const out: FieldInfo[] = [];
92
+ const attrs = model.attributes ?? {};
93
+ for (const [name, rawDef] of Object.entries(attrs)) {
94
+ if (AUTO_GENERATED_FIELD_NAMES.has(name)) continue;
95
+ if (skip.has(name)) continue;
96
+
97
+ const def = rawDef as AttributeShape;
98
+ if (def?.auto) continue;
99
+
100
+ // Heuristic: parse a type like "String required unique" to detect
101
+ // convention-string shapes. We prefer the structured fields if
102
+ // they're present.
103
+ const type = (def?.type ?? parseTypeFromConvention(def)) ?? 'String';
104
+ const required = def?.required === true || hasConventionFlag(def, 'required');
105
+ const values = def?.values as string[] | undefined;
106
+ const maxLength = def?.maxLength ?? def?.max as number | undefined;
107
+ const isLongString =
108
+ typeof maxLength === 'number' && maxLength > 100;
109
+
110
+ out.push({
111
+ name,
112
+ label: humanize(name),
113
+ type: normalizeType(type),
114
+ required,
115
+ values,
116
+ isLongString,
117
+ });
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /** Structured attribute object from the parsed spec. */
123
+ interface AttributeShape {
124
+ type?: string;
125
+ required?: boolean;
126
+ unique?: boolean;
127
+ auto?: string;
128
+ values?: string[];
129
+ maxLength?: number;
130
+ max?: number;
131
+ [k: string]: unknown;
132
+ }
133
+
134
+ function parseTypeFromConvention(def: unknown): string | undefined {
135
+ // Convention form: "String required unique"
136
+ if (typeof def === 'string') {
137
+ const first = def.trim().split(/\s+/)[0];
138
+ return first;
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ function hasConventionFlag(def: unknown, flag: string): boolean {
144
+ if (typeof def === 'string') {
145
+ return def.split(/\s+/).includes(flag);
146
+ }
147
+ return false;
148
+ }
149
+
150
+ function normalizeType(type: string): string {
151
+ // Strip any trailing modifiers from a compact convention-string form.
152
+ return type.replace(/[^A-Za-z].*$/, '');
153
+ }
154
+
155
+ function extractBelongsTo(model: ModelSpec): string[] {
156
+ const rels = model.relationships ?? {};
157
+ const out: string[] = [];
158
+ for (const [name, def] of Object.entries(rels)) {
159
+ if ((def as { type?: string })?.type === 'belongsTo') out.push(name);
160
+ }
161
+ return out;
162
+ }
163
+
164
+ // ──────────────────────────────────────────────────────────────────────
165
+ // Input rendering
166
+ // ──────────────────────────────────────────────────────────────────────
167
+
168
+ const INPUT_CLS =
169
+ 'w-full rounded border border-gray-300 px-3 py-2 text-sm ' +
170
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 ' +
171
+ 'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100';
172
+
173
+ const LABEL_CLS = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1';
174
+
175
+ function renderInput(field: FieldInfo): string[] {
176
+ const requiredMark = field.required ? ' *' : '';
177
+ const reqAttr = field.required ? ' required' : '';
178
+
179
+ // Enum → <select>
180
+ if (field.values && field.values.length > 0) {
181
+ const options = field.values
182
+ .map(v => ` <option value="${escapeAttr(v)}">${escapeText(v)}</option>`)
183
+ .join('\n');
184
+ return [
185
+ ' <div>',
186
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
187
+ ` <select`,
188
+ ` id="${field.name}"`,
189
+ ` className="${INPUT_CLS}"`,
190
+ ` value={String((formData as any).${field.name} ?? '')}`,
191
+ ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
192
+ ` >`,
193
+ ` <option value="">— choose —</option>`,
194
+ options,
195
+ ` </select>`,
196
+ ' </div>',
197
+ ];
198
+ }
199
+
200
+ // Long-string / Text → <textarea>
201
+ if (field.type === 'Text' || field.isLongString) {
202
+ return [
203
+ ' <div>',
204
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
205
+ ` <textarea`,
206
+ ` id="${field.name}"`,
207
+ ` className="${INPUT_CLS}"`,
208
+ ` rows={4}`,
209
+ ` value={String((formData as any).${field.name} ?? '')}`,
210
+ ` onChange={e => handleChange('${field.name}', e.target.value)}${reqAttr}`,
211
+ ` />`,
212
+ ' </div>',
213
+ ];
214
+ }
215
+
216
+ // Boolean → checkbox
217
+ if (field.type === 'Boolean') {
218
+ return [
219
+ ' <div>',
220
+ ` <label className="inline-flex items-center gap-2">`,
221
+ ` <input`,
222
+ ` id="${field.name}"`,
223
+ ` type="checkbox"`,
224
+ ` checked={Boolean((formData as any).${field.name})}`,
225
+ ` onChange={e => handleChange('${field.name}', e.target.checked)}`,
226
+ ` />`,
227
+ ` <span className="text-sm text-gray-700 dark:text-gray-300">${field.label}${requiredMark}</span>`,
228
+ ` </label>`,
229
+ ' </div>',
230
+ ];
231
+ }
232
+
233
+ const inputType = mapInputType(field.type);
234
+ return [
235
+ ' <div>',
236
+ ` <label className="${LABEL_CLS}" htmlFor="${field.name}">${field.label}${requiredMark}</label>`,
237
+ ` <input`,
238
+ ` id="${field.name}"`,
239
+ ` type="${inputType}"`,
240
+ ` className="${INPUT_CLS}"`,
241
+ ` value={String((formData as any).${field.name} ?? '')}`,
242
+ ` onChange={e => handleChange('${field.name}', ${coerceOnChange(field.type)})}${reqAttr}`,
243
+ ` />`,
244
+ ' </div>',
245
+ ];
246
+ }
247
+
248
+ /** Scalar SpecVerse type → HTML input type attribute. */
249
+ function mapInputType(type: string): string {
250
+ switch (type) {
251
+ case 'Integer':
252
+ case 'Float':
253
+ case 'Number':
254
+ case 'Money':
255
+ return 'number';
256
+ case 'DateTime':
257
+ return 'datetime-local';
258
+ case 'Date':
259
+ return 'date';
260
+ case 'Email':
261
+ return 'email';
262
+ case 'URL':
263
+ return 'url';
264
+ case 'String':
265
+ case 'UUID':
266
+ default:
267
+ return 'text';
268
+ }
269
+ }
270
+
271
+ /**
272
+ * JSX snippet for the onChange handler's second argument — coerces
273
+ * the string value to the right shape for the field. The user can
274
+ * edit this; defaults are a reasonable starter.
275
+ */
276
+ function coerceOnChange(type: string): string {
277
+ switch (type) {
278
+ case 'Integer':
279
+ case 'Number':
280
+ return "e.target.value === '' ? undefined : Number(e.target.value)";
281
+ case 'Float':
282
+ case 'Money':
283
+ return "e.target.value === '' ? undefined : parseFloat(e.target.value)";
284
+ default:
285
+ return 'e.target.value';
286
+ }
287
+ }
288
+
289
+ // ──────────────────────────────────────────────────────────────────────
290
+ // Small helpers
291
+ // ──────────────────────────────────────────────────────────────────────
292
+
293
+ function humanize(name: string): string {
294
+ return name
295
+ .replace(/([A-Z])/g, ' $1')
296
+ .replace(/^./, c => c.toUpperCase())
297
+ .trim();
298
+ }
299
+
300
+ function escapeAttr(s: string): string {
301
+ return s.replace(/"/g, '&quot;');
302
+ }
303
+
304
+ function escapeText(s: string): string {
305
+ return s.replace(/</g, '&lt;').replace(/>/g, '&gt;');
306
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Helpers emitter for ReactAppStarter
3
+ *
4
+ * Returns the source of small utility files that generated projects
5
+ * use. Inlined into `src/lib/` at realize time rather than imported
6
+ * from `@specverse/runtime` — Factory B's defining trait is that the
7
+ * generated code has no @specverse/* runtime dependency.
8
+ */
9
+
10
+ /**
11
+ * Source of `src/lib/entity-display.ts`.
12
+ *
13
+ * Mirrors `getEntityDisplayName` from
14
+ * `@specverse/runtime/views/core/entity-display`. Used by:
15
+ * - The FK-resolution TODO in detail-view output (users who upgrade
16
+ * the plain-FK display to a resolved display name import this).
17
+ * - Any user-written code that wants to label a record.
18
+ */
19
+ export function emitEntityDisplay(): string {
20
+ return `/**
21
+ * Pick the best human-readable label for an entity.
22
+ *
23
+ * Walks a prioritized list of candidate fields. Falls back to a
24
+ * truncated id if none match. Inlined into this project by
25
+ * @specverse/realize (ReactAppStarter); edit freely — nothing in
26
+ * the factory's regeneration will clobber your edits.
27
+ */
28
+ export function getEntityDisplayName(entity: Record<string, unknown> | null | undefined): string {
29
+ if (!entity) return '';
30
+
31
+ const candidates = ['name', 'title', 'displayName', 'label', 'username', 'email'];
32
+ for (const key of candidates) {
33
+ const value = entity[key];
34
+ if (typeof value === 'string' && value.trim().length > 0) {
35
+ return value;
36
+ }
37
+ }
38
+
39
+ const id = entity.id;
40
+ if (typeof id === 'string') {
41
+ return id.length > 8 ? id.slice(0, 8) + '…' : id;
42
+ }
43
+ return id != null ? String(id) : '';
44
+ }
45
+
46
+ /**
47
+ * Map an entity id to the display name of the record with that id in
48
+ * a provided list. Handy for resolving belongsTo FK columns in
49
+ * list / detail views.
50
+ */
51
+ export function resolveEntityDisplayName(
52
+ id: unknown,
53
+ records: ReadonlyArray<Record<string, unknown>>
54
+ ): string {
55
+ if (id == null) return '';
56
+ const match = records.find(r => r.id === id);
57
+ return match ? getEntityDisplayName(match) : String(id);
58
+ }
59
+ `;
60
+ }