@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,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
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * {{MODEL_NAME}}ListView — 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 { useState, useMemo } from 'react';
9
+ import { use{{PLURAL_MODEL}}Query, useDelete{{MODEL_NAME}}Mutation } from '../hooks/useApi';
10
+ import type { {{MODEL_NAME}} } from '../types/api';
11
+
12
+ interface {{MODEL_NAME}}ListViewProps {
13
+ onSelect?: (item: {{MODEL_NAME}}) => void;
14
+ onCreate?: () => void;
15
+ }
16
+
17
+ export function {{MODEL_NAME}}ListView({ onSelect, onCreate }: {{MODEL_NAME}}ListViewProps) {
18
+ const { data: items = [], isLoading, error } = use{{PLURAL_MODEL}}Query();
19
+ const deleteItem = useDelete{{MODEL_NAME}}Mutation();
20
+ const [searchTerm, setSearchTerm] = useState('');
21
+
22
+ const filtered = useMemo(
23
+ () =>
24
+ items.filter((item: {{MODEL_NAME}}) =>
25
+ // Cast to `any` inside JSX context — `Record<string, unknown>`
26
+ // looks like a JSX opening tag to the TSX parser when this
27
+ // expression ends up inside JSX. `any` is safe here: we only
28
+ // read values for substring-matching against the search term.
29
+ Object.values(item as any).some(v =>
30
+ String(v ?? '').toLowerCase().includes(searchTerm.toLowerCase())
31
+ )
32
+ ),
33
+ [items, searchTerm]
34
+ );
35
+
36
+ if (isLoading) return <div className="p-4 text-gray-500">Loading {{PLURAL_LOWER}}…</div>;
37
+ if (error) return <div className="p-4 text-red-600">Error loading {{PLURAL_LOWER}}: {String(error)}</div>;
38
+
39
+ return (
40
+ <div className="p-6 space-y-4">
41
+ <div className="flex items-center justify-between">
42
+ <input
43
+ type="search"
44
+ placeholder="Search {{PLURAL_LOWER}}…"
45
+ value={searchTerm}
46
+ onChange={e => setSearchTerm(e.target.value)}
47
+ className="w-64 rounded border border-gray-300 px-3 py-2 text-sm"
48
+ />
49
+ <button
50
+ type="button"
51
+ onClick={onCreate}
52
+ className="rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
53
+ >
54
+ + New {{MODEL_NAME}}
55
+ </button>
56
+ </div>
57
+
58
+ {/* Pattern-rendered list body (table + rows). Edit freely. */}
59
+ {{BODY}}
60
+
61
+ {filtered.length === 0 && (
62
+ <div className="p-8 text-center text-gray-400">No {{PLURAL_LOWER}} yet.</div>
63
+ )}
64
+
65
+ {deleteItem.isError && (
66
+ <div className="p-2 text-sm text-red-600">
67
+ Delete failed: {String(deleteItem.error)}
68
+ </div>
69
+ )}
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * ReactAppStarter view emitter
3
+ *
4
+ * Given a view spec + model spec + expanded spec, emits a complete
5
+ * idiomatic React component source file as a string.
6
+ *
7
+ * Strategy 3 ("skeleton + rendered interior"): a hand-written
8
+ * skeleton template provides the idiomatic React outer structure
9
+ * (imports, hooks, props, layout). The canonical Tailwind adapter
10
+ * renders the pattern's interior as HTML. The html-to-jsx transformer
11
+ * converts the HTML to JSX. The emitter composes these.
12
+ *
13
+ * See README.md in this directory for the architecture.
14
+ */
15
+
16
+ import { readFileSync } from 'fs';
17
+ import { join, dirname } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ /**
21
+ * The minimal shape of a view spec this emitter needs. Matches the
22
+ * inferred spec format — `view.type` drives skeleton selection,
23
+ * `view.model` identifies the primary model, `view.uiComponents` (if
24
+ * present) overrides inference defaults.
25
+ */
26
+ export interface ViewSpec {
27
+ type: string;
28
+ model?: string;
29
+ uiComponents?: Record<string, unknown>;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ /**
34
+ * The minimal shape of a model spec this emitter needs. Field details
35
+ * flow through to the Tailwind adapter which decides column selection.
36
+ */
37
+ export interface ModelSpec {
38
+ name: string;
39
+ attributes: Record<string, unknown>;
40
+ relationships?: Record<string, unknown>;
41
+ lifecycles?: Record<string, unknown>;
42
+ }
43
+
44
+ export interface EmitContext {
45
+ view: ViewSpec;
46
+ viewName: string; // e.g. "PostListView"
47
+ model: ModelSpec;
48
+ modelSchemas: Record<string, ModelSpec>;
49
+ /** Pluggable so we can stub the renderer in unit tests. */
50
+ renderBody: RenderBodyFn;
51
+ }
52
+
53
+ /**
54
+ * Render the body of a view as JSX-safe source — the exact string
55
+ * that is dropped at `{{BODY}}` in the skeleton.
56
+ *
57
+ * The renderBody implementation is responsible for the whole body
58
+ * pipeline: invoke the canonical Tailwind adapter for static shell
59
+ * rendering, run that HTML through `htmlToJsx`, and inject any JSX
60
+ * expressions (`.map()`, event handlers). Keeping it here means
61
+ * view-emitter stays a pure orchestrator and can't accidentally
62
+ * double-transform JSX that's already JSX.
63
+ */
64
+ export type RenderBodyFn = (context: EmitContext) => string;
65
+
66
+ /**
67
+ * Emit a complete .tsx file for a single view. Pure function — all
68
+ * inputs come in via the context argument so it's easy to test.
69
+ */
70
+ export function emitView(context: EmitContext): string {
71
+ const skeleton = loadSkeleton(context.view.type);
72
+ const bodyJsx = context.renderBody(context);
73
+
74
+ const substitutions = buildSubstitutions(context, bodyJsx);
75
+ return applySubstitutions(skeleton, substitutions);
76
+ }
77
+
78
+ // ──────────────────────────────────────────────────────────────────────
79
+ // Skeleton loading
80
+ // ──────────────────────────────────────────────────────────────────────
81
+
82
+ const SKELETON_BY_VIEW_TYPE: Record<string, string> = {
83
+ list: 'list.tsx.template',
84
+ detail: 'detail.tsx.template',
85
+ form: 'form.tsx.template',
86
+ dashboard: 'dashboard.tsx.template',
87
+ // Specialist types (board, timeline, calendar, analytics, workflow,
88
+ // wizard, comparison, settings, map, feed, profile) come online in
89
+ // Phase 2e.
90
+ };
91
+
92
+ function loadSkeleton(viewType: string): string {
93
+ const filename = SKELETON_BY_VIEW_TYPE[viewType.toLowerCase()];
94
+ if (!filename) {
95
+ throw new Error(
96
+ `No skeleton registered for view type "${viewType}". ` +
97
+ `Known types: ${Object.keys(SKELETON_BY_VIEW_TYPE).join(', ')}.`
98
+ );
99
+ }
100
+
101
+ const here = dirname(fileURLToPath(import.meta.url));
102
+ const path = join(here, 'skeletons', filename);
103
+ return readFileSync(path, 'utf8');
104
+ }
105
+
106
+ // ──────────────────────────────────────────────────────────────────────
107
+ // Substitution
108
+ // ──────────────────────────────────────────────────────────────────────
109
+
110
+ interface Substitutions {
111
+ MODEL_NAME: string;
112
+ PLURAL_MODEL: string;
113
+ PLURAL_LOWER: string;
114
+ SINGULAR_LOWER: string;
115
+ BODY: string;
116
+ }
117
+
118
+ function buildSubstitutions(context: EmitContext, body: string): Substitutions {
119
+ const modelName = context.model.name;
120
+ const pluralModel = pluralize(modelName);
121
+ return {
122
+ MODEL_NAME: modelName,
123
+ PLURAL_MODEL: pluralModel,
124
+ PLURAL_LOWER: pluralModel.toLowerCase(),
125
+ SINGULAR_LOWER: modelName.toLowerCase(),
126
+ BODY: body,
127
+ };
128
+ }
129
+
130
+ function applySubstitutions(template: string, subs: Substitutions): string {
131
+ let out = template;
132
+ for (const [key, value] of Object.entries(subs)) {
133
+ // Replace every occurrence of {{KEY}} — plain string substitution,
134
+ // no expression evaluation. Escape regex special chars in the key
135
+ // (defensive; keys are ASCII constants).
136
+ const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
137
+ out = out.replace(pattern, value);
138
+ }
139
+ return out;
140
+ }
141
+
142
+ /**
143
+ * Minimal English pluralizer matching the adapter's conventions.
144
+ * Good enough for generated code — users can edit the output if they
145
+ * want a different pluralization.
146
+ */
147
+ function pluralize(s: string): string {
148
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
149
+ if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
150
+ return s + 's';
151
+ }