@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,183 @@
1
+ /**
2
+ * P3 — Rendered-output equivalence
3
+ *
4
+ * Architectural invariant: the same (spec, view, model) must produce
5
+ * semantically equivalent output in app-demo (runtime React adapter)
6
+ * and in ReactAppStarter (Factory B emitter). They differ only in
7
+ * execution model — one renders in the browser, the other emits
8
+ * static JSX — but the visible structure must match.
9
+ *
10
+ * This file tests the LIST VIEW path, where parity is most
11
+ * tractable because both consumers call the same Tailwind adapter
12
+ * for the table shell. The test covers:
13
+ *
14
+ * 1. Column selection — both use `inferFieldsFromSchema` from
15
+ * the canonical pattern engine. Drift in either composer's
16
+ * column-filter logic would diverge here.
17
+ * 2. Table shell HTML — the Tailwind adapter renders the same
18
+ * shell for both paths; ensured by Factory B importing
19
+ * `createUniversalTailwindAdapter` directly.
20
+ * 3. Field humanisation — `camelCase` → `Title Case` for headers.
21
+ *
22
+ * Not yet covered (future extensions):
23
+ * - Detail / form / dashboard views — would need a reference
24
+ * React-adapter render path captured as an HTML string, then
25
+ * diffed against the Factory B emitted JSX modulo structural
26
+ * differences. Doable but bigger test machinery.
27
+ * - FK display-name resolution — blocked on Factory B's TODO to
28
+ * replace plain-FK inputs with dropdowns.
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
33
+ import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
34
+ import { composeListBody } from '../list-body-composer.js';
35
+ import type { EmitContext, ModelSpec } from '../view-emitter.js';
36
+
37
+ // ───────────────────────────────────────────────────────────────────────
38
+ // Reference model + view-spec setup
39
+ // ───────────────────────────────────────────────────────────────────────
40
+
41
+ function makeReferenceModel(): ModelSpec {
42
+ return {
43
+ name: 'Article',
44
+ attributes: {
45
+ id: { type: 'UUID', required: true, auto: 'uuid4' }, // excluded
46
+ title: { type: 'String', required: true }, // included
47
+ slug: { type: 'String', required: true, unique: true }, // included
48
+ content: { type: 'Text', required: false }, // included
49
+ status: { type: 'String', values: ['draft', 'published'] },// included
50
+ authorId: { type: 'UUID', required: true }, // included (FK)
51
+ createdAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
52
+ updatedAt: { type: 'DateTime', auto: 'now' }, // excluded (auto)
53
+ },
54
+ };
55
+ }
56
+
57
+ function makeContext(): EmitContext {
58
+ const model = makeReferenceModel();
59
+ return {
60
+ view: { type: 'list', model: 'Article' },
61
+ viewName: 'ArticleListView',
62
+ model,
63
+ modelSchemas: { Article: model },
64
+ renderBody: composeListBody,
65
+ };
66
+ }
67
+
68
+ function humanize(s: string): string {
69
+ return s.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
70
+ }
71
+
72
+ // ───────────────────────────────────────────────────────────────────────
73
+ // Parity assertions
74
+ // ───────────────────────────────────────────────────────────────────────
75
+
76
+ describe('P3 — list view column inference parity', () => {
77
+ it('runtime inferFieldsFromSchema and Factory B composer see the same columns', () => {
78
+ const ctx = makeContext();
79
+ const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
80
+
81
+ // We can extract Factory B's column choices by looking at what
82
+ // headers it emits into the rendered output.
83
+ const body = composeListBody(ctx);
84
+
85
+ for (const col of runtimeColumns) {
86
+ const label = humanize(col);
87
+ expect(body, `runtime column "${col}" (label: "${label}") must appear in Factory B output`)
88
+ .toContain(`>${label}<`);
89
+ }
90
+
91
+ // And nothing EXTRA from the model that runtime excludes:
92
+ const runtimeSet = new Set(runtimeColumns);
93
+ const allAttrs = Object.keys(ctx.model.attributes);
94
+ const runtimeExcluded = allAttrs.filter(a => !runtimeSet.has(a));
95
+ for (const excluded of runtimeExcluded) {
96
+ const label = humanize(excluded);
97
+ // Factory B must NOT emit a <th> for this field.
98
+ const headerPattern = new RegExp(`<th\\b[^>]*>${label}</th>`);
99
+ expect(body).not.toMatch(headerPattern);
100
+ }
101
+ });
102
+
103
+ it('matches the expected column set for the reference model', () => {
104
+ const cols = inferFieldsFromSchema(makeContext().modelSchemas, 'Article');
105
+ // Canonical expectation for this reference spec.
106
+ expect(cols).toEqual(['title', 'slug', 'content', 'status', 'authorId']);
107
+ });
108
+ });
109
+
110
+ describe('P3 — list view table shell parity', () => {
111
+ it('emits a table shell byte-identical to a direct Tailwind adapter call', () => {
112
+ // The composer calls createUniversalTailwindAdapter() + table.render()
113
+ // with a known columns list and a sentinel in children. If we call
114
+ // the adapter directly with the same args, we should get the same
115
+ // shell HTML (modulo the sentinel vs the Factory B JSX injection).
116
+ const ctx = makeContext();
117
+ const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
118
+ const headers = runtimeColumns.map(humanize);
119
+
120
+ const adapter = createUniversalTailwindAdapter({ darkMode: true });
121
+ const SENTINEL = 'SENTINEL';
122
+ const shellHtml = adapter.components.table.render({
123
+ properties: { columns: headers },
124
+ children: SENTINEL,
125
+ });
126
+
127
+ // Every column header appears in the shell, in order.
128
+ let lastIdx = -1;
129
+ for (const header of headers) {
130
+ const idx = shellHtml.indexOf(`>${header}</th>`);
131
+ expect(idx, `header "${header}" must appear in Tailwind adapter output`).toBeGreaterThan(-1);
132
+ expect(idx, `header "${header}" must appear AFTER the previous header`).toBeGreaterThan(lastIdx);
133
+ lastIdx = idx;
134
+ }
135
+
136
+ // Factory B's composer output must preserve the same header order.
137
+ const body = composeListBody(ctx);
138
+ let lastBodyIdx = -1;
139
+ for (const header of headers) {
140
+ const idx = body.indexOf(`>${header}</th>`);
141
+ expect(idx).toBeGreaterThan(-1);
142
+ expect(idx).toBeGreaterThan(lastBodyIdx);
143
+ lastBodyIdx = idx;
144
+ }
145
+ });
146
+
147
+ it('Factory B output carries Tailwind classes from the canonical adapter shell', () => {
148
+ const body = composeListBody(makeContext());
149
+ // A sampling of classes that come from the adapter's table template.
150
+ // If the adapter changes its classes, both consumers update together.
151
+ expect(body).toContain('overflow-x-auto');
152
+ expect(body).toContain('min-w-full');
153
+ expect(body).toContain('divide-y divide-gray-200');
154
+ expect(body).toContain('bg-gray-50 dark:bg-gray-800');
155
+ });
156
+
157
+ it('rows are synthesized by Factory B (the adapter emits placeholder for react mount)', () => {
158
+ const body = composeListBody(makeContext());
159
+ // The body should contain a real JSX .map() — Factory B's
160
+ // contribution to the shared shell.
161
+ expect(body).toContain('filtered.map((item, idx) =>');
162
+ expect(body).toContain('onClick={() => onSelect?.(item)}');
163
+ });
164
+ });
165
+
166
+ describe('P3 — field humanisation parity', () => {
167
+ it('both paths humanise column names the same way (camelCase → Title Case)', () => {
168
+ // Factory B's humanize is inlined in the composer. The runtime
169
+ // adapter humanises via the same-shaped regex in pattern-engine /
170
+ // react-pattern-adapter. This test documents the contract by
171
+ // comparing against a fixture.
172
+ const cases: [string, string][] = [
173
+ ['title', 'Title'],
174
+ ['authorId', 'Author Id'],
175
+ ['createdAt', 'Created At'],
176
+ ['slugifiedURL', 'Slugified U R L'],
177
+ ['name', 'Name'],
178
+ ];
179
+ for (const [input, expected] of cases) {
180
+ expect(humanize(input), `humanize("${input}")`).toBe(expected);
181
+ }
182
+ });
183
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import {
6
+ loadHashManifest,
7
+ saveHashManifest,
8
+ reconcileWrites,
9
+ sha256,
10
+ HASHES_DIR,
11
+ HASHES_FILE,
12
+ } from '../regen-safety.js';
13
+
14
+ let projectRoot: string;
15
+
16
+ beforeEach(() => {
17
+ projectRoot = mkdtempSync(join(tmpdir(), 'specverse-regen-safety-'));
18
+ });
19
+
20
+ afterEach(() => {
21
+ rmSync(projectRoot, { recursive: true, force: true });
22
+ });
23
+
24
+ describe('hash manifest persistence', () => {
25
+ it('loadHashManifest returns {} when the file is missing', () => {
26
+ expect(loadHashManifest(projectRoot)).toEqual({});
27
+ });
28
+
29
+ it('round-trips through save → load', () => {
30
+ saveHashManifest(projectRoot, { 'src/a.ts': 'abc', 'src/b.ts': 'def' });
31
+ const loaded = loadHashManifest(projectRoot);
32
+ expect(loaded).toEqual({ 'src/a.ts': 'abc', 'src/b.ts': 'def' });
33
+ expect(existsSync(join(projectRoot, HASHES_DIR, HASHES_FILE))).toBe(true);
34
+ });
35
+
36
+ it('tolerates a malformed manifest file by returning {}', () => {
37
+ const dir = join(projectRoot, HASHES_DIR);
38
+ mkdirSync(dir, { recursive: true });
39
+ writeFileSync(join(dir, HASHES_FILE), '{ not valid JSON', 'utf8');
40
+ expect(loadHashManifest(projectRoot)).toEqual({});
41
+ });
42
+ });
43
+
44
+ describe('reconcileWrites — pure planning (no I/O writes)', () => {
45
+ it('approves brand-new files and records their hashes', () => {
46
+ const proposed = { 'src/views/PostListView.tsx': 'content-a' };
47
+ const result = reconcileWrites(projectRoot, proposed, {});
48
+ expect(result.approvedWrites).toEqual(proposed);
49
+ expect(result.skipped).toEqual([]);
50
+ expect(existsSync(join(projectRoot, 'src/views/PostListView.tsx'))).toBe(false);
51
+ expect(result.manifest['src/views/PostListView.tsx']).toBe(sha256('content-a'));
52
+ });
53
+
54
+ it('approves overwriting a pristine file and updates its hash', () => {
55
+ const path = 'src/a.ts';
56
+ const absPath = join(projectRoot, path);
57
+ const originalContent = 'v1';
58
+ const newContent = 'v2';
59
+
60
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
61
+ writeFileSync(absPath, originalContent, 'utf8');
62
+ const prevManifest = { [path]: sha256(originalContent) };
63
+
64
+ const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
65
+
66
+ expect(result.approvedWrites[path]).toBe(newContent);
67
+ expect(result.skipped).toEqual([]);
68
+ // reconcileWrites is pure planning — filesystem unchanged
69
+ expect(readFileSync(absPath, 'utf8')).toBe(originalContent);
70
+ expect(result.manifest[path]).toBe(sha256(newContent));
71
+ });
72
+
73
+ it('skips a user-edited file and keeps the old hash record', () => {
74
+ const path = 'src/edited.ts';
75
+ const absPath = join(projectRoot, path);
76
+ const originalContent = 'v1';
77
+ const editedContent = 'v1 + user edits';
78
+ const newContent = 'v2';
79
+
80
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
81
+ writeFileSync(absPath, editedContent, 'utf8');
82
+ const prevManifest = { [path]: sha256(originalContent) };
83
+
84
+ const result = reconcileWrites(projectRoot, { [path]: newContent }, prevManifest);
85
+
86
+ expect(result.approvedWrites[path]).toBeUndefined();
87
+ expect(result.skipped).toHaveLength(1);
88
+ expect(result.skipped[0]).toMatchObject({
89
+ path,
90
+ reason: expect.stringContaining('edited'),
91
+ });
92
+ expect(readFileSync(absPath, 'utf8')).toBe(editedContent);
93
+ expect(result.manifest[path]).toBe(prevManifest[path]);
94
+ });
95
+
96
+ it('skips files that exist but have no prior hash record', () => {
97
+ const path = 'src/unknown.ts';
98
+ const absPath = join(projectRoot, path);
99
+
100
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
101
+ writeFileSync(absPath, 'manually placed', 'utf8');
102
+
103
+ const result = reconcileWrites(projectRoot, { [path]: 'would-overwrite' }, {});
104
+
105
+ expect(result.approvedWrites[path]).toBeUndefined();
106
+ expect(result.skipped).toHaveLength(1);
107
+ expect(result.skipped[0].reason).toContain('no prior hash recorded');
108
+ expect(readFileSync(absPath, 'utf8')).toBe('manually placed');
109
+ });
110
+
111
+ it('does not mutate the input prevManifest', () => {
112
+ const prev = { 'a.ts': sha256('old') };
113
+ const prevSnapshot = { ...prev };
114
+ reconcileWrites(projectRoot, { 'a.ts': 'new', 'b.ts': 'also new' }, prev);
115
+ expect(prev).toEqual(prevSnapshot);
116
+ });
117
+
118
+ it('handles a batch of mixed outcomes correctly', () => {
119
+ mkdirSync(join(projectRoot, 'src'), { recursive: true });
120
+ writeFileSync(join(projectRoot, 'src/pristine.ts'), 'v1', 'utf8');
121
+ writeFileSync(join(projectRoot, 'src/edited.ts'), 'v1 + user edit', 'utf8');
122
+ const prev = {
123
+ 'src/pristine.ts': sha256('v1'),
124
+ 'src/edited.ts': sha256('v1'),
125
+ };
126
+
127
+ const proposed = {
128
+ 'src/pristine.ts': 'v2',
129
+ 'src/edited.ts': 'v2',
130
+ 'src/new.ts': 'brand new',
131
+ };
132
+
133
+ const result = reconcileWrites(projectRoot, proposed, prev);
134
+
135
+ expect(Object.keys(result.approvedWrites).sort()).toEqual(['src/new.ts', 'src/pristine.ts']);
136
+ expect(result.skipped.map(s => s.path)).toEqual(['src/edited.ts']);
137
+ // No writes happened
138
+ expect(readFileSync(join(projectRoot, 'src/pristine.ts'), 'utf8')).toBe('v1');
139
+ expect(readFileSync(join(projectRoot, 'src/edited.ts'), 'utf8')).toBe('v1 + user edit');
140
+ expect(result.manifest['src/pristine.ts']).toBe(sha256('v2'));
141
+ expect(result.manifest['src/edited.ts']).toBe(prev['src/edited.ts']);
142
+ expect(result.manifest['src/new.ts']).toBe(sha256('brand new'));
143
+ });
144
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { generate as generateAppTsx } from '../app-tsx-generator.js';
4
+ import { generate as generatePackageJson } from '../package-json-generator.js';
5
+
6
+ function assertValidTsx(source: string, label: string): void {
7
+ const result = ts.transpileModule(source, {
8
+ compilerOptions: {
9
+ jsx: ts.JsxEmit.Preserve,
10
+ target: ts.ScriptTarget.ES2022,
11
+ module: ts.ModuleKind.ESNext,
12
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
13
+ strict: false,
14
+ },
15
+ reportDiagnostics: true,
16
+ });
17
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
18
+ if (errors.length > 0) {
19
+ const message = errors
20
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
21
+ .join('\n');
22
+ throw new Error(`${label} failed to parse:\n${message}\n\n--- source ---\n${source}`);
23
+ }
24
+ }
25
+
26
+ describe('app-tsx-generator', () => {
27
+ it('produces valid TSX with one branch per model and view type', async () => {
28
+ const source = await generateAppTsx({
29
+ spec: {
30
+ models: {
31
+ Post: { name: 'Post', attributes: {} },
32
+ Author: { name: 'Author', attributes: {} },
33
+ },
34
+ },
35
+ });
36
+ assertValidTsx(source, 'App.tsx');
37
+ // One nav section per model
38
+ expect(source).toContain('Post');
39
+ expect(source).toContain('Author');
40
+ // One import per (model, view) pair — 2 models × 4 view types = 8 imports
41
+ const importLines = source.match(/^import \{ \w+View \} from '\.\/views\//gm) ?? [];
42
+ expect(importLines.length).toBe(8);
43
+ // Navigation wiring
44
+ expect(source).toContain("select('Post', 'list')");
45
+ expect(source).toContain("select('Author', 'dashboard')");
46
+ });
47
+
48
+ it('handles an empty spec gracefully', async () => {
49
+ const source = await generateAppTsx({ spec: { models: {}, views: {} } });
50
+ assertValidTsx(source, 'App.tsx (empty spec)');
51
+ expect(source).toContain('No models in this spec.');
52
+ // Still renders a QueryClientProvider
53
+ expect(source).toContain('QueryClientProvider');
54
+ });
55
+
56
+ it('uses @tanstack/react-query, not @specverse/runtime', async () => {
57
+ const source = await generateAppTsx({
58
+ spec: { models: { Post: { name: 'Post', attributes: {} } } },
59
+ });
60
+ expect(source).not.toContain('@specverse/runtime');
61
+ expect(source).toContain('@tanstack/react-query');
62
+ });
63
+ });
64
+
65
+ describe('package-json-generator', () => {
66
+ it('omits @specverse/runtime from dependencies', async () => {
67
+ const json = await generatePackageJson({ spec: { name: 'test' } });
68
+ const pkg = JSON.parse(json);
69
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
70
+ expect(allDeps['@specverse/runtime']).toBeUndefined();
71
+ expect(allDeps['@specverse/engines']).toBeUndefined();
72
+ expect(allDeps['@specverse/types']).toBeUndefined();
73
+ });
74
+
75
+ it('includes the deps the emitted views actually need', async () => {
76
+ const json = await generatePackageJson({ spec: { name: 'test' } });
77
+ const pkg = JSON.parse(json);
78
+ expect(pkg.dependencies).toMatchObject({
79
+ react: expect.any(String),
80
+ 'react-dom': expect.any(String),
81
+ '@tanstack/react-query': expect.any(String),
82
+ });
83
+ expect(pkg.devDependencies).toMatchObject({
84
+ typescript: expect.any(String),
85
+ vite: expect.any(String),
86
+ tailwindcss: expect.any(String),
87
+ });
88
+ });
89
+
90
+ it('slugifies the app name', async () => {
91
+ const json = await generatePackageJson({
92
+ spec: { metadata: { name: 'My Fancy App!' } },
93
+ });
94
+ const pkg = JSON.parse(json);
95
+ expect(pkg.name).toBe('my-fancy-app');
96
+ });
97
+
98
+ it('falls back to a default name when none is provided', async () => {
99
+ const json = await generatePackageJson({ spec: {} });
100
+ const pkg = JSON.parse(json);
101
+ expect(pkg.name).toBe('specverse-starter-app');
102
+ });
103
+
104
+ it('parses as valid JSON with the expected shape', async () => {
105
+ const json = await generatePackageJson({ spec: { name: 'test' } });
106
+ const pkg = JSON.parse(json);
107
+ expect(pkg.type).toBe('module');
108
+ expect(pkg.scripts).toMatchObject({
109
+ dev: 'vite',
110
+ build: 'tsc && vite build',
111
+ preview: 'vite preview',
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { emitView, type EmitContext, type ModelSpec, type ViewSpec } from '../view-emitter.js';
4
+
5
+ /**
6
+ * Build a minimal context. The `renderBody` is a stub that returns a
7
+ * fixed Tailwind table — the point of these tests is the emitter
8
+ * orchestration + substitutions + JSX validity, not the body's content.
9
+ * Parity with the real Tailwind adapter is covered by Phase 3 (P3).
10
+ */
11
+ function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
12
+ const defaultModel: ModelSpec = {
13
+ name: 'Post',
14
+ attributes: {
15
+ id: { type: 'UUID', required: true },
16
+ title: { type: 'String', required: true },
17
+ body: { type: 'Text', required: false },
18
+ },
19
+ };
20
+
21
+ const defaultView: ViewSpec = {
22
+ type: 'list',
23
+ model: 'Post',
24
+ };
25
+
26
+ return {
27
+ view: defaultView,
28
+ viewName: 'PostListView',
29
+ model: defaultModel,
30
+ modelSchemas: { Post: defaultModel },
31
+ // renderBody returns JSX-ready source that will be injected verbatim
32
+ // into {{BODY}}. NOT raw HTML — that's the composer's job.
33
+ renderBody: () =>
34
+ '<table className="w-full"><thead><tr><th className="px-4 py-2">Title</th></tr></thead>' +
35
+ '<tbody><tr><td className="px-4 py-2">Example</td></tr></tbody></table>',
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ function assertValidTsx(source: string, label: string): void {
41
+ const result = ts.transpileModule(source, {
42
+ compilerOptions: {
43
+ jsx: ts.JsxEmit.Preserve,
44
+ target: ts.ScriptTarget.ES2022,
45
+ module: ts.ModuleKind.ESNext,
46
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
47
+ strict: false,
48
+ },
49
+ reportDiagnostics: true,
50
+ });
51
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
52
+ if (errors.length > 0) {
53
+ const message = errors
54
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
55
+ .join('\n');
56
+ throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
57
+ }
58
+ }
59
+
60
+ describe('emitView — list', () => {
61
+ it('produces TSX that parses cleanly', () => {
62
+ const context = makeContext();
63
+ const source = emitView(context);
64
+ assertValidTsx(source, 'emitted list view');
65
+ });
66
+
67
+ it('substitutes model names into the skeleton', () => {
68
+ const source = emitView(makeContext());
69
+ expect(source).toContain('PostListView'); // component name
70
+ expect(source).toContain('usePostsQuery'); // generated hook name
71
+ expect(source).toContain('useDeletePostMutation'); // generated hook name
72
+ expect(source).toContain("Search posts…"); // pluralized lowercase
73
+ expect(source).toContain('+ New Post'); // action button label
74
+ expect(source).toContain('No posts yet.'); // empty state
75
+ });
76
+
77
+ it('substitutes a non-trivial plural correctly', () => {
78
+ const m: ModelSpec = { name: 'Category', attributes: {} };
79
+ const source = emitView(makeContext({
80
+ model: m,
81
+ view: { type: 'list', model: 'Category' },
82
+ viewName: 'CategoryListView',
83
+ modelSchemas: { Category: m },
84
+ }));
85
+ expect(source).toContain('CategoryListView');
86
+ expect(source).toContain('useCategoriesQuery'); // "Category" → "Categories"
87
+ expect(source).toContain('Search categories…');
88
+ });
89
+
90
+ it('injects the rendered body at {{BODY}}', () => {
91
+ const source = emitView(makeContext({
92
+ renderBody: () => '<table className="my-table"><tbody><tr><td>Custom</td></tr></tbody></table>',
93
+ }));
94
+ expect(source).toContain('<table className="my-table">');
95
+ expect(source).toContain('Custom');
96
+ // The placeholder itself should be gone.
97
+ expect(source).not.toContain('{{BODY}}');
98
+ // All other placeholders should be gone too.
99
+ expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
100
+ });
101
+
102
+ it('throws for an unknown view type', () => {
103
+ expect(() =>
104
+ emitView(makeContext({ view: { type: 'unknown-type-xyz' } }))
105
+ ).toThrow(/No skeleton registered/);
106
+ });
107
+ });