@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,120 @@
1
+ /**
2
+ * List-view body composer for ReactAppStarter
3
+ *
4
+ * Implements the `renderBody` contract of view-emitter.ts for list
5
+ * views. Uses the canonical Tailwind adapter from
6
+ * `@specverse/runtime/views/tailwind` to render the TABLE SHELL, then
7
+ * injects a JSX `.map()` expression into the tbody so the generated
8
+ * component renders rows from the `filtered` array defined by the
9
+ * skeleton.
10
+ *
11
+ * Why split it this way: the adapter produces static HTML (the same
12
+ * HTML app-demo uses). For Factory B we want React that maps over
13
+ * runtime data — so we take the adapter's shell and replace a sentinel
14
+ * inside the tbody with a `{filtered.map(...)}` expression. The shell
15
+ * stays canonical; only the row-iteration JSX is synthesized here.
16
+ *
17
+ * See README.md for the full architecture.
18
+ */
19
+
20
+ import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
21
+ import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
22
+ import { htmlToJsx } from './html-to-jsx.js';
23
+ import type { EmitContext, ModelSpec } from './view-emitter.js';
24
+
25
+ /**
26
+ * Sentinel token. Must be ASCII-safe (no curly braces) so it survives
27
+ * the html-to-jsx transform unchanged, and must be unique enough not
28
+ * to appear in real HTML output.
29
+ */
30
+ const TBODY_SENTINEL = '__SPECVERSE_TBODY_ROWS__';
31
+
32
+ /**
33
+ * Compose the list view's body as JSX-safe source. Returned string is
34
+ * meant to be dropped at `{{BODY}}` inside `skeletons/list.tsx.template`.
35
+ */
36
+ export function composeListBody(context: EmitContext): string {
37
+ const columns = inferColumns(context);
38
+ const headers = columns.map(humanize);
39
+
40
+ const adapter = createUniversalTailwindAdapter({ darkMode: true });
41
+ const shellHtml = adapter.components.table.render({
42
+ properties: { columns: headers },
43
+ children: TBODY_SENTINEL,
44
+ });
45
+
46
+ // Transform the static HTML shell to JSX-safe source. The sentinel
47
+ // lands in a text position inside the <tbody> and survives the
48
+ // transform untouched.
49
+ const shellJsx = htmlToJsx(shellHtml);
50
+
51
+ // Build the row-iteration JSX that replaces the sentinel. The
52
+ // skeleton's useMemo produces `filtered: Model[]`, so the map
53
+ // expression binds over that. onSelect is a prop the skeleton
54
+ // declares. Delete action wires to the deleteItem mutation.
55
+ const rowMap = buildRowMap(columns);
56
+
57
+ if (!shellJsx.includes(TBODY_SENTINEL)) {
58
+ // Defensive: catches adapter output changes that no longer flow
59
+ // `children` into the tbody. Caught at composer time, not runtime.
60
+ throw new Error(
61
+ 'composeListBody: tbody sentinel not present in adapter output. ' +
62
+ 'The canonical Tailwind adapter may have changed its table rendering.'
63
+ );
64
+ }
65
+
66
+ return shellJsx.replace(TBODY_SENTINEL, rowMap);
67
+ }
68
+
69
+ /**
70
+ * Select the non-metadata attributes of a model. Delegates to the
71
+ * canonical pattern library so the column set is identical to what
72
+ * the runtime React adapter picks for the same model — parity test P3
73
+ * relies on this being one-and-the-same function.
74
+ */
75
+ function inferColumns(context: EmitContext): string[] {
76
+ return inferFieldsFromSchema(context.modelSchemas, context.model.name);
77
+ }
78
+
79
+ /**
80
+ * Convert a camelCase attribute name to Title-Case words for display
81
+ * as a column header. Matches the convention used by runtime's
82
+ * pattern adapter (see `humanize` in runtime react-pattern-adapter).
83
+ */
84
+ function humanize(name: string): string {
85
+ return name
86
+ .replace(/([A-Z])/g, ' $1')
87
+ .replace(/^./, c => c.toUpperCase())
88
+ .trim();
89
+ }
90
+
91
+ /**
92
+ * Emit the `{filtered.map(...)}` JSX expression that renders each
93
+ * row. Keep the output readable so users inspecting the generated
94
+ * file can follow what's happening.
95
+ */
96
+ function buildRowMap(columns: string[]): string {
97
+ // Type-cast to `any` to avoid TS generic syntax (`<string, unknown>`)
98
+ // inside JSX expressions, which the TSX parser can't always
99
+ // disambiguate from a JSX opening tag. `any` is the right choice for
100
+ // starter-kit output anyway — the user will often reshape the row
101
+ // type after editing.
102
+ const cells = columns
103
+ .map(col =>
104
+ ` <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">` +
105
+ `{String((item as any).${col} ?? '')}</td>`
106
+ )
107
+ .join('\n');
108
+
109
+ return [
110
+ '{filtered.map((item, idx) => (',
111
+ ' <tr',
112
+ ' key={idx}',
113
+ ' onClick={() => onSelect?.(item)}',
114
+ ' className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"',
115
+ ' >',
116
+ cells,
117
+ ' </tr>',
118
+ '))}',
119
+ ].join('\n');
120
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Factory B top-level orchestrator
3
+ *
4
+ * Single entry point the realize engine calls. Returns a map of
5
+ * `relativePath → content` for every file the factory decides to
6
+ * write, after applying regeneration-safety triage. Realize handles
7
+ * the actual filesystem writes via its normal multi-file generator
8
+ * pipeline.
9
+ *
10
+ * Skipped (user-edited) files are NOT in the returned map — realize
11
+ * never sees them, so user edits are safe. The hash manifest is
12
+ * emitted as one of the returned files (`.specverse-gen/hashes.json`)
13
+ * so it rolls through the same write pipeline.
14
+ */
15
+
16
+ import { generate as generateViews } from './views-generator.js';
17
+ import { generate as generateAppTsx } from './app-tsx-generator.js';
18
+ import { generate as generatePackageJson } from './package-json-generator.js';
19
+ import {
20
+ loadHashManifest,
21
+ reconcileWrites,
22
+ summarize,
23
+ HASHES_DIR,
24
+ HASHES_FILE,
25
+ } from './regen-safety.js';
26
+ import type { ExpandedSpec } from './views-generator.js';
27
+
28
+ export interface OrchestratorContext {
29
+ /** Expanded spec (post-inference). */
30
+ spec: ExpandedSpec & { metadata?: { name?: string }; name?: string };
31
+ /** Resolved manifest configuration. */
32
+ manifest?: unknown;
33
+ /** Absolute path to the project root (the frontend directory for this factory). */
34
+ projectRoot: string;
35
+ }
36
+
37
+ /**
38
+ * The shape realize expects from a multi-file generator: a map of
39
+ * relative paths to their contents. Realize creates parent dirs and
40
+ * writes the files verbatim.
41
+ *
42
+ * Skipped (user-edited) files are intentionally absent — realize
43
+ * never sees them.
44
+ */
45
+ export type OrchestratorOutput = Record<string, string>;
46
+
47
+ /**
48
+ * Produce the complete file set for a Factory B run. Realize writes
49
+ * whatever comes back; the orchestrator is the sole source of truth
50
+ * for "what ends up on disk."
51
+ */
52
+ export async function generate(context: OrchestratorContext): Promise<OrchestratorOutput> {
53
+ const { spec, projectRoot } = context;
54
+
55
+ // 1. Produce all file contents (pure composition — no I/O yet).
56
+ const proposed: Record<string, string> = {};
57
+
58
+ const viewFiles = await generateViews({ spec });
59
+ Object.assign(proposed, viewFiles);
60
+
61
+ proposed['src/App.tsx'] = await generateAppTsx({ spec });
62
+ proposed['package.json'] = await generatePackageJson({ spec });
63
+
64
+ // 2. Load prior hash manifest + triage. Read-only.
65
+ const prevManifest = loadHashManifest(projectRoot);
66
+ const result = reconcileWrites(projectRoot, proposed, prevManifest);
67
+
68
+ // 3. Log the summary for operator visibility.
69
+ console.log(summarize(result, projectRoot));
70
+
71
+ // 4. Emit the hash manifest as one of the written files so it flows
72
+ // through realize's normal write pipeline.
73
+ const manifestPath = `${HASHES_DIR}/${HASHES_FILE}`;
74
+ const out: OrchestratorOutput = {
75
+ ...result.approvedWrites,
76
+ [manifestPath]: JSON.stringify(result.manifest, null, 2) + '\n',
77
+ };
78
+
79
+ return out;
80
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * package.json generator for ReactAppStarter
3
+ *
4
+ * Deliberately does NOT include @specverse/runtime. The promise of
5
+ * Factory B is that the generated project is standalone — everything
6
+ * needed to render is emitted as local source.
7
+ */
8
+
9
+ export interface PackageJsonGeneratorContext {
10
+ spec: {
11
+ metadata?: { name?: string };
12
+ name?: string;
13
+ };
14
+ manifest?: unknown;
15
+ }
16
+
17
+ export async function generate(context: PackageJsonGeneratorContext): Promise<string> {
18
+ const appName = context.spec?.metadata?.name ?? context.spec?.name ?? 'specverse-starter-app';
19
+ const packageName = slugify(appName);
20
+
21
+ const pkg = {
22
+ name: packageName,
23
+ private: true,
24
+ version: '0.1.0',
25
+ type: 'module',
26
+ scripts: {
27
+ dev: 'vite',
28
+ build: 'tsc && vite build',
29
+ preview: 'vite preview',
30
+ typecheck: 'tsc --noEmit',
31
+ },
32
+ dependencies: {
33
+ react: '^18.2.0',
34
+ 'react-dom': '^18.2.0',
35
+ '@tanstack/react-query': '^5.0.0',
36
+ },
37
+ devDependencies: {
38
+ '@types/react': '^18.2.0',
39
+ '@types/react-dom': '^18.2.0',
40
+ '@vitejs/plugin-react': '^4.2.0',
41
+ autoprefixer: '^10.4.20',
42
+ postcss: '^8.4.47',
43
+ tailwindcss: '^3.4.13',
44
+ typescript: '^5.2.0',
45
+ vite: '^6.0.0',
46
+ },
47
+ };
48
+
49
+ return JSON.stringify(pkg, null, 2) + '\n';
50
+ }
51
+
52
+ function slugify(s: string): string {
53
+ return s
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, '-')
56
+ .replace(/^-+|-+$/g, '') || 'specverse-starter-app';
57
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Regeneration safety for ReactAppStarter
3
+ *
4
+ * When realize runs Factory B, generated files can end up in two states:
5
+ *
6
+ * 1. Pristine — the file on disk is byte-identical to what we last
7
+ * wrote. Safe to overwrite with a new version.
8
+ *
9
+ * 2. User-edited — the user modified the file. We must NOT clobber
10
+ * their work.
11
+ *
12
+ * The mechanism is an SHA-256 hash recorded per file in
13
+ * `.specverse-gen/hashes.json` at write time. At regeneration, we
14
+ * compare the on-disk hash against the recorded one. Mismatch → skip
15
+ * with a warning. Match → overwrite and update the hash.
16
+ *
17
+ * The factory orchestrator wraps realize's write calls through
18
+ * `reconcileWrites` below, which handles all three outcomes:
19
+ * - brand-new file → write, record hash
20
+ * - pristine existing → overwrite, update hash
21
+ * - user-edited existing → skip, leave hash unchanged
22
+ */
23
+
24
+ import { createHash } from 'crypto';
25
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
26
+ import { join, relative } from 'path';
27
+
28
+ /** Relative path (from project root) → SHA-256 hex digest. */
29
+ export type HashManifest = Record<string, string>;
30
+
31
+ export const HASHES_DIR = '.specverse-gen';
32
+ export const HASHES_FILE = 'hashes.json';
33
+
34
+ export function sha256(s: string): string {
35
+ return createHash('sha256').update(s, 'utf8').digest('hex');
36
+ }
37
+
38
+ /**
39
+ * Load a `.specverse-gen/hashes.json` from the project root. Returns
40
+ * an empty manifest if the file is missing or malformed — the
41
+ * calling convention is that a missing manifest means "no record of
42
+ * prior generation, treat everything as new."
43
+ */
44
+ export function loadHashManifest(projectRoot: string): HashManifest {
45
+ const path = join(projectRoot, HASHES_DIR, HASHES_FILE);
46
+ if (!existsSync(path)) return {};
47
+ try {
48
+ const raw = readFileSync(path, 'utf8');
49
+ const parsed = JSON.parse(raw);
50
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
51
+ const out: HashManifest = {};
52
+ for (const [k, v] of Object.entries(parsed)) {
53
+ if (typeof v === 'string') out[k] = v;
54
+ }
55
+ return out;
56
+ }
57
+ } catch {
58
+ // Malformed — treat as missing. Next generate pass will overwrite.
59
+ }
60
+ return {};
61
+ }
62
+
63
+ /** Write the manifest back to `.specverse-gen/hashes.json`. */
64
+ export function saveHashManifest(projectRoot: string, manifest: HashManifest): void {
65
+ const dir = join(projectRoot, HASHES_DIR);
66
+ mkdirSync(dir, { recursive: true });
67
+ const path = join(dir, HASHES_FILE);
68
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
69
+ }
70
+
71
+ export interface ReconcileResult {
72
+ /** Relative path → content for every file approved for writing. */
73
+ approvedWrites: Record<string, string>;
74
+ /** Files we skipped because the user edited them (or couldn't confirm origin). */
75
+ skipped: { path: string; reason: string }[];
76
+ /** The updated hash manifest reflecting all approved writes. */
77
+ manifest: HashManifest;
78
+ }
79
+
80
+ /**
81
+ * Pure-planning triage. For each proposed file, decides whether the
82
+ * write is safe:
83
+ * - Path doesn't exist → APPROVE, record hash.
84
+ * - Path exists + on-disk hash matches recorded hash → APPROVE
85
+ * (pristine overwrite), update hash.
86
+ * - Path exists + hash mismatch → SKIP (user-edited), preserve old
87
+ * hash record.
88
+ * - Path exists + no recorded hash → SKIP (cautious default).
89
+ *
90
+ * Reads the filesystem but doesn't write. Returns the approved writes
91
+ * as a map the caller can pass to the realize write pipeline.
92
+ */
93
+ export function reconcileWrites(
94
+ projectRoot: string,
95
+ proposed: Record<string, string>,
96
+ prevManifest: HashManifest
97
+ ): ReconcileResult {
98
+ const manifest: HashManifest = { ...prevManifest };
99
+ const approvedWrites: Record<string, string> = {};
100
+ const skipped: { path: string; reason: string }[] = [];
101
+
102
+ for (const [relPath, content] of Object.entries(proposed)) {
103
+ const abs = join(projectRoot, relPath);
104
+ const newHash = sha256(content);
105
+
106
+ if (!existsSync(abs)) {
107
+ approvedWrites[relPath] = content;
108
+ manifest[relPath] = newHash;
109
+ continue;
110
+ }
111
+
112
+ const currentContent = readFileSync(abs, 'utf8');
113
+ const currentHash = sha256(currentContent);
114
+ const recordedHash = prevManifest[relPath];
115
+
116
+ if (recordedHash == null) {
117
+ skipped.push({
118
+ path: relPath,
119
+ reason: 'no prior hash recorded — cannot confirm this file was generated by us',
120
+ });
121
+ continue;
122
+ }
123
+
124
+ if (currentHash === recordedHash) {
125
+ approvedWrites[relPath] = content;
126
+ manifest[relPath] = newHash;
127
+ continue;
128
+ }
129
+
130
+ skipped.push({
131
+ path: relPath,
132
+ reason: 'file has been edited since last generation',
133
+ });
134
+ }
135
+
136
+ return { approvedWrites, skipped, manifest };
137
+ }
138
+
139
+ /**
140
+ * Human-readable summary of a ReconcileResult. Used by the factory
141
+ * orchestrator to log after a run.
142
+ */
143
+ export function summarize(result: ReconcileResult, projectRoot: string): string {
144
+ const lines: string[] = [];
145
+ const writeCount = Object.keys(result.approvedWrites).length;
146
+ lines.push(`[ReactAppStarter] Approved ${writeCount} file(s) for writing.`);
147
+ if (result.skipped.length > 0) {
148
+ lines.push(`[ReactAppStarter] Skipped ${result.skipped.length} user-edited file(s):`);
149
+ for (const { path, reason } of result.skipped) {
150
+ lines.push(` - ${path} (${reason})`);
151
+ }
152
+ lines.push(
153
+ `To accept upstream regeneration for a skipped file, delete it (\`rm ${relative(process.cwd(), projectRoot)}/PATH\`) and re-run \`spv realize\`.`
154
+ );
155
+ }
156
+ return lines.join('\n');
157
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * {{MODEL_NAME}}DashboardView — generated by @specverse/realize (ReactAppStarter)
3
+ *
4
+ * Safe to edit. Edits are preserved across regeneration via content
5
+ * hashing (see .specverse-gen/hashes.json). To accept an upstream
6
+ * regeneration of this file, delete it first, then run `spv realize`.
7
+ *
8
+ * A minimal dashboard: summary counts derived from the list query,
9
+ * plus a compact preview of recent records. Charts and aggregation
10
+ * metrics are deferred — add them as the backend grows suitable
11
+ * endpoints.
12
+ */
13
+ import { useMemo } from 'react';
14
+ import { use{{PLURAL_MODEL}}Query } from '../hooks/useApi';
15
+ import type { {{MODEL_NAME}} } from '../types/api';
16
+
17
+ interface {{MODEL_NAME}}DashboardViewProps {
18
+ /** Number of recent records to preview. Default 5. */
19
+ previewLimit?: number;
20
+ onSelect?: (item: {{MODEL_NAME}}) => void;
21
+ }
22
+
23
+ export function {{MODEL_NAME}}DashboardView({
24
+ previewLimit = 5,
25
+ onSelect,
26
+ }: {{MODEL_NAME}}DashboardViewProps) {
27
+ const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
28
+
29
+ const preview = useMemo(
30
+ () => items.slice(0, previewLimit),
31
+ [items, previewLimit]
32
+ );
33
+
34
+ if (isLoading) return <div className="p-4 text-gray-500">Loading dashboard…</div>;
35
+ if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
36
+
37
+ return (
38
+ <div className="p-6 space-y-6">
39
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
40
+ {{MODEL_NAME}} dashboard
41
+ </h2>
42
+
43
+ {/* Pattern-rendered dashboard body (metrics + preview). Edit freely. */}
44
+ {{BODY}}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * {{MODEL_NAME}}DetailView — generated by @specverse/realize (ReactAppStarter)
3
+ *
4
+ * Safe to edit. Edits are preserved across regeneration via content
5
+ * hashing (see .specverse-gen/hashes.json). To accept an upstream
6
+ * regeneration of this file, delete it first, then run `spv realize`.
7
+ */
8
+ import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
9
+ import type { {{MODEL_NAME}} } from '../types/api';
10
+
11
+ interface {{MODEL_NAME}}DetailViewProps {
12
+ entityId: string | number;
13
+ onEdit?: (item: {{MODEL_NAME}}) => void;
14
+ onBack?: () => void;
15
+ /** Called after the delete mutation succeeds. */
16
+ onDeleted?: () => void;
17
+ }
18
+
19
+ export function {{MODEL_NAME}}DetailView({
20
+ entityId,
21
+ onEdit,
22
+ onBack,
23
+ onDeleted,
24
+ }: {{MODEL_NAME}}DetailViewProps) {
25
+ const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
26
+ const deleteItem = useDelete{{MODEL_NAME}}Mutation();
27
+
28
+ const item = items.find(
29
+ (x: {{MODEL_NAME}}) => (x as any).id === entityId
30
+ );
31
+
32
+ if (isLoading) return <div className="p-4 text-gray-500">Loading…</div>;
33
+ if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
34
+ if (!item) return <div className="p-4 text-gray-400">No {{SINGULAR_LOWER}} matching id {String(entityId)}.</div>;
35
+
36
+ const handleDelete = async () => {
37
+ if (!confirm('Delete this {{SINGULAR_LOWER}}?')) return;
38
+ try {
39
+ await deleteItem.mutateAsync((item as any).id);
40
+ onDeleted?.();
41
+ } catch {
42
+ // deleteItem.error is surfaced below
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="p-6 space-y-4">
48
+ <div className="flex items-center justify-between">
49
+ <div className="flex items-center gap-3">
50
+ {onBack && (
51
+ <button
52
+ type="button"
53
+ onClick={onBack}
54
+ className="text-sm text-gray-500 hover:text-gray-700"
55
+ >
56
+ ← Back
57
+ </button>
58
+ )}
59
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
60
+ {{MODEL_NAME}} detail
61
+ </h2>
62
+ </div>
63
+ <div className="flex gap-2">
64
+ {onEdit && (
65
+ <button
66
+ type="button"
67
+ onClick={() => onEdit(item)}
68
+ className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
69
+ >
70
+ Edit
71
+ </button>
72
+ )}
73
+ <button
74
+ type="button"
75
+ onClick={handleDelete}
76
+ disabled={deleteItem.isPending}
77
+ className="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-60"
78
+ >
79
+ {deleteItem.isPending ? 'Deleting…' : 'Delete'}
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Pattern-rendered detail body (fields). Edit freely. */}
85
+ {{BODY}}
86
+
87
+ {deleteItem.isError && (
88
+ <div className="p-2 text-sm text-red-600">
89
+ Delete failed: {String(deleteItem.error)}
90
+ </div>
91
+ )}
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * {{MODEL_NAME}}FormView — generated by @specverse/realize (ReactAppStarter)
3
+ *
4
+ * Safe to edit. Edits are preserved across regeneration via content
5
+ * hashing (see .specverse-gen/hashes.json). To accept an upstream
6
+ * regeneration of this file, delete it first, then run `spv realize`.
7
+ *
8
+ * Controlled form. One field per non-auto-generated attribute.
9
+ * Mode: 'create' (default) or 'update' — passed as a prop.
10
+ */
11
+ import { useEffect, useState } from 'react';
12
+ import {
13
+ use{{PLURAL_MODEL}}Query,
14
+ useCreate{{MODEL_NAME}}Mutation,
15
+ useUpdate{{MODEL_NAME}}Mutation,
16
+ } from '../hooks/useApi';
17
+ import type { {{MODEL_NAME}} } from '../types/api';
18
+
19
+ type FormMode = 'create' | 'update';
20
+
21
+ interface {{MODEL_NAME}}FormViewProps {
22
+ mode?: FormMode;
23
+ /** Required in update mode. */
24
+ entityId?: string | number;
25
+ onSuccess?: (item: {{MODEL_NAME}}) => void;
26
+ onCancel?: () => void;
27
+ }
28
+
29
+ export function {{MODEL_NAME}}FormView({
30
+ mode = 'create',
31
+ entityId,
32
+ onSuccess,
33
+ onCancel,
34
+ }: {{MODEL_NAME}}FormViewProps) {
35
+ const { data: items = [] } = use{{PLURAL_MODEL}}Query();
36
+ const createItem = useCreate{{MODEL_NAME}}Mutation();
37
+ const updateItem = useUpdate{{MODEL_NAME}}Mutation();
38
+
39
+ const existing =
40
+ mode === 'update'
41
+ ? items.find((x: {{MODEL_NAME}}) => (x as any).id === entityId)
42
+ : undefined;
43
+
44
+ const [formData, setFormData] = useState<Partial<{{MODEL_NAME}}>>(existing ?? {});
45
+
46
+ // When the fetched list lands after initial render (update mode),
47
+ // hydrate the form with the loaded entity's data.
48
+ useEffect(() => {
49
+ if (existing) setFormData(existing);
50
+ }, [existing]);
51
+
52
+ const handleChange = (field: string, value: unknown) => {
53
+ setFormData(prev => ({ ...prev, [field]: value }) as Partial<{{MODEL_NAME}}>);
54
+ };
55
+
56
+ const handleSubmit = async (event: React.FormEvent) => {
57
+ event.preventDefault();
58
+ try {
59
+ const result =
60
+ mode === 'create'
61
+ ? await createItem.mutateAsync(formData as {{MODEL_NAME}})
62
+ : await updateItem.mutateAsync({
63
+ id: entityId as any,
64
+ data: formData as Partial<{{MODEL_NAME}}>,
65
+ });
66
+ onSuccess?.(result as {{MODEL_NAME}});
67
+ } catch {
68
+ // Mutation errors are surfaced below via createItem / updateItem.
69
+ }
70
+ };
71
+
72
+ const mutation = mode === 'create' ? createItem : updateItem;
73
+ const submitLabel =
74
+ mode === 'create'
75
+ ? mutation.isPending ? 'Creating…' : 'Create {{MODEL_NAME}}'
76
+ : mutation.isPending ? 'Updating…' : 'Update {{MODEL_NAME}}';
77
+
78
+ return (
79
+ <form onSubmit={handleSubmit} className="p-6 space-y-6">
80
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
81
+ {mode === 'create' ? 'New {{MODEL_NAME}}' : 'Edit {{MODEL_NAME}}'}
82
+ </h2>
83
+
84
+ {/* Pattern-rendered form fields. Edit freely. */}
85
+ {{BODY}}
86
+
87
+ <div className="flex gap-2 border-t border-gray-200 dark:border-gray-700 pt-4">
88
+ <button
89
+ type="submit"
90
+ disabled={mutation.isPending}
91
+ className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
92
+ >
93
+ {submitLabel}
94
+ </button>
95
+ {onCancel && (
96
+ <button
97
+ type="button"
98
+ onClick={onCancel}
99
+ className="rounded border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
100
+ >
101
+ Cancel
102
+ </button>
103
+ )}
104
+ </div>
105
+
106
+ {mutation.isError && (
107
+ <div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
108
+ {mode === 'create' ? 'Create failed: ' : 'Update failed: '}
109
+ {String(mutation.error)}
110
+ </div>
111
+ )}
112
+ </form>
113
+ );
114
+ }