@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,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { emitEntityDisplay } from '../helpers-emitter.js';
4
+
5
+ function assertValidTsx(source: string, label: string): void {
6
+ const result = ts.transpileModule(source, {
7
+ compilerOptions: {
8
+ jsx: ts.JsxEmit.Preserve,
9
+ target: ts.ScriptTarget.ES2022,
10
+ module: ts.ModuleKind.ESNext,
11
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
12
+ strict: true,
13
+ },
14
+ reportDiagnostics: true,
15
+ });
16
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
17
+ if (errors.length > 0) {
18
+ const message = errors
19
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
20
+ .join('\n');
21
+ throw new Error(`${label} failed to parse as TypeScript:\n${message}`);
22
+ }
23
+ }
24
+
25
+ describe('emitEntityDisplay', () => {
26
+ it('produces a module that parses under strict TypeScript', () => {
27
+ assertValidTsx(emitEntityDisplay(), 'entity-display.ts');
28
+ });
29
+
30
+ it('exports both getEntityDisplayName and resolveEntityDisplayName', () => {
31
+ const src = emitEntityDisplay();
32
+ expect(src).toMatch(/export function getEntityDisplayName/);
33
+ expect(src).toMatch(/export function resolveEntityDisplayName/);
34
+ });
35
+
36
+ it('uses the canonical candidate list in documented order', () => {
37
+ const src = emitEntityDisplay();
38
+ // Order matters: name → title → displayName → label → username → email.
39
+ // The order mirrors @specverse/runtime/views/core/entity-display.
40
+ const candidatesLine = src.match(/const candidates = \[([^\]]+)\]/)?.[1];
41
+ expect(candidatesLine).toBeDefined();
42
+ expect(candidatesLine).toContain("'name'");
43
+ expect(candidatesLine).toContain("'title'");
44
+ expect(candidatesLine).toContain("'displayName'");
45
+ expect(candidatesLine).toContain("'email'");
46
+ // Check relative order
47
+ const order = ['name', 'title', 'displayName', 'label', 'username', 'email'];
48
+ let lastIdx = -1;
49
+ for (const n of order) {
50
+ const idx = candidatesLine!.indexOf(`'${n}'`);
51
+ expect(idx).toBeGreaterThan(lastIdx);
52
+ lastIdx = idx;
53
+ }
54
+ });
55
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { htmlToJsx } from '../html-to-jsx.js';
3
+
4
+ describe('htmlToJsx — attribute renames', () => {
5
+ it('renames class → className', () => {
6
+ expect(htmlToJsx('<div class="foo">x</div>')).toBe('<div className="foo">x</div>');
7
+ });
8
+
9
+ it('renames class inside nested elements', () => {
10
+ const input = '<div class="a"><span class="b">hi</span></div>';
11
+ const expected = '<div className="a"><span className="b">hi</span></div>';
12
+ expect(htmlToJsx(input)).toBe(expected);
13
+ });
14
+
15
+ it('renames for → htmlFor on labels', () => {
16
+ expect(htmlToJsx('<label for="id">X</label>')).toBe('<label htmlFor="id">X</label>');
17
+ });
18
+
19
+ it('renames tabindex → tabIndex', () => {
20
+ expect(htmlToJsx('<div tabindex="0">x</div>')).toBe('<div tabIndex="0">x</div>');
21
+ });
22
+
23
+ it('renames multiple attributes in a single tag', () => {
24
+ const input = '<input class="c" readonly maxlength="10" />';
25
+ const out = htmlToJsx(input);
26
+ expect(out).toContain('className="c"');
27
+ expect(out).toContain('readOnly');
28
+ expect(out).toContain('maxLength="10"');
29
+ });
30
+
31
+ it('does not rewrite attribute values that contain "class"', () => {
32
+ // The substring "class" in an attribute value should not be renamed.
33
+ const input = '<div class="has class word" data-x="my class name">y</div>';
34
+ const out = htmlToJsx(input);
35
+ // Expected: only the class ATTRIBUTE is renamed; values unchanged.
36
+ expect(out).toBe('<div className="has class word" data-x="my class name">y</div>');
37
+ });
38
+ });
39
+
40
+ describe('htmlToJsx — void elements', () => {
41
+ it('self-closes img', () => {
42
+ expect(htmlToJsx('<img src="a.png" alt="A">')).toBe('<img src="a.png" alt="A" />');
43
+ });
44
+
45
+ it('self-closes br', () => {
46
+ expect(htmlToJsx('line<br>break')).toBe('line<br />break');
47
+ });
48
+
49
+ it('self-closes input', () => {
50
+ expect(htmlToJsx('<input type="text" class="f">')).toBe('<input type="text" className="f" />');
51
+ });
52
+
53
+ it('self-closes hr', () => {
54
+ expect(htmlToJsx('<hr>')).toBe('<hr />');
55
+ });
56
+
57
+ it('leaves already-self-closed void elements alone', () => {
58
+ expect(htmlToJsx('<br />')).toBe('<br />');
59
+ expect(htmlToJsx('<img src="a" />')).toBe('<img src="a" />');
60
+ });
61
+
62
+ it('handles void elements in the middle of content', () => {
63
+ const input = '<p>hello<br>world<br>!</p>';
64
+ expect(htmlToJsx(input)).toBe('<p>hello<br />world<br />!</p>');
65
+ });
66
+ });
67
+
68
+ describe('htmlToJsx — inline style conversion', () => {
69
+ it('converts a single-declaration style', () => {
70
+ const input = '<div style="color: red">x</div>';
71
+ const out = htmlToJsx(input);
72
+ expect(out).toBe("<div style={{ color: 'red' }}>x</div>");
73
+ });
74
+
75
+ it('converts multiple declarations and camelCases property names', () => {
76
+ const input = '<div style="color: red; font-size: 14px; background-color: blue">x</div>';
77
+ const out = htmlToJsx(input);
78
+ expect(out).toBe("<div style={{ color: 'red', fontSize: '14px', backgroundColor: 'blue' }}>x</div>");
79
+ });
80
+
81
+ it('handles empty style', () => {
82
+ expect(htmlToJsx('<div style="">x</div>')).toBe('<div style={{}}>x</div>');
83
+ });
84
+
85
+ it('escapes single quotes inside values', () => {
86
+ const input = `<div style="font-family: 'Helvetica Neue'">x</div>`;
87
+ const out = htmlToJsx(input);
88
+ expect(out).toBe("<div style={{ fontFamily: '\\'Helvetica Neue\\'' }}>x</div>");
89
+ });
90
+
91
+ it('strips trailing semicolons and extra spacing', () => {
92
+ const input = '<div style="color:red; font-size: 14px ;">x</div>';
93
+ const out = htmlToJsx(input);
94
+ expect(out).toBe("<div style={{ color: 'red', fontSize: '14px' }}>x</div>");
95
+ });
96
+ });
97
+
98
+ describe('htmlToJsx — idempotence', () => {
99
+ it('is a no-op on already-transformed input', () => {
100
+ const jsxLike = '<div className="a"><input type="text" /><br /></div>';
101
+ expect(htmlToJsx(jsxLike)).toBe(jsxLike);
102
+ });
103
+ });
104
+
105
+ describe('htmlToJsx — empty / degenerate input', () => {
106
+ it('returns empty string for empty input', () => {
107
+ expect(htmlToJsx('')).toBe('');
108
+ });
109
+
110
+ it('returns text-only input unchanged', () => {
111
+ expect(htmlToJsx('Loading...')).toBe('Loading...');
112
+ });
113
+ });
114
+
115
+ describe('htmlToJsx — realistic Tailwind-adapter output', () => {
116
+ it('transforms a typical table produced by the adapter', () => {
117
+ const input =
118
+ '<table class="w-full text-left border-collapse">' +
119
+ '<thead><tr><th class="px-4 py-2 font-semibold">Title</th></tr></thead>' +
120
+ '<tbody><tr class="hover:bg-gray-50"><td class="px-4 py-2">Hello</td></tr></tbody>' +
121
+ '</table>';
122
+ const out = htmlToJsx(input);
123
+ expect(out).toContain('className="w-full text-left border-collapse"');
124
+ expect(out).toContain('className="px-4 py-2 font-semibold"');
125
+ expect(out).toContain('className="hover:bg-gray-50"');
126
+ expect(out).not.toContain('class=');
127
+ });
128
+
129
+ it('transforms a form-group with inputs', () => {
130
+ const input =
131
+ '<div class="space-y-4">' +
132
+ '<label for="title" class="text-sm">Title</label>' +
133
+ '<input type="text" class="border rounded px-2 py-1">' +
134
+ '</div>';
135
+ const out = htmlToJsx(input);
136
+ expect(out).toContain('htmlFor="title"');
137
+ expect(out).toContain('className="text-sm"');
138
+ expect(out).toContain('<input type="text" className="border rounded px-2 py-1" />');
139
+ });
140
+ });
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as ts from 'typescript';
3
+ import { composeListBody } from '../list-body-composer.js';
4
+ import { emitView, type EmitContext, type ModelSpec } from '../view-emitter.js';
5
+
6
+ /**
7
+ * End-to-end: the canonical Tailwind adapter renders the table shell,
8
+ * the composer injects a JSX row-map, the emitter substitutes into
9
+ * the skeleton. The full output must parse as valid TSX.
10
+ */
11
+
12
+ function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
13
+ const post: ModelSpec = {
14
+ name: 'Post',
15
+ attributes: {
16
+ // `id` is excluded by runtime's inferFieldsFromSchema directly.
17
+ id: { type: 'UUID', required: true },
18
+ title: { type: 'String', required: true },
19
+ body: { type: 'Text', required: false },
20
+ // `auto=now` is what marks publishedAt as excluded from column
21
+ // inference — the canonical rule from runtime pattern-engine.
22
+ publishedAt: { type: 'DateTime', required: false, auto: 'now' },
23
+ authorId: { type: 'UUID', required: true },
24
+ },
25
+ };
26
+
27
+ return {
28
+ view: { type: 'list', model: 'Post' },
29
+ viewName: 'PostListView',
30
+ model: post,
31
+ modelSchemas: { Post: post },
32
+ renderBody: composeListBody,
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ function assertValidTsx(source: string, label: string): void {
38
+ const result = ts.transpileModule(source, {
39
+ compilerOptions: {
40
+ jsx: ts.JsxEmit.Preserve,
41
+ target: ts.ScriptTarget.ES2022,
42
+ module: ts.ModuleKind.ESNext,
43
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
44
+ strict: false,
45
+ },
46
+ reportDiagnostics: true,
47
+ });
48
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
49
+ if (errors.length > 0) {
50
+ const message = errors
51
+ .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
52
+ .join('\n');
53
+ throw new Error(`${label} failed to parse as TSX:\n${message}\n\n--- source ---\n${source}`);
54
+ }
55
+ }
56
+
57
+ describe('composeListBody — direct output', () => {
58
+ it('produces JSX containing the canonical table shell', () => {
59
+ const body = composeListBody(makeContext());
60
+ // Comes from the Tailwind adapter's table template
61
+ expect(body).toContain('overflow-x-auto');
62
+ expect(body).toContain('min-w-full');
63
+ expect(body).toContain('<thead');
64
+ expect(body).toContain('<tbody');
65
+ // After html-to-jsx: class → className
66
+ expect(body).toContain('className=');
67
+ expect(body).not.toMatch(/\bclass=/);
68
+ });
69
+
70
+ it('renders one header cell per non-metadata attribute, humanized', () => {
71
+ const body = composeListBody(makeContext());
72
+ expect(body).toContain('>Title<');
73
+ expect(body).toContain('>Body<');
74
+ expect(body).toContain('>Author Id<'); // authorId humanized
75
+ expect(body).not.toContain('>Id<'); // id filtered as metadata
76
+ expect(body).not.toContain('>Published At<'); // publishedAt filtered as metadata
77
+ });
78
+
79
+ it('emits a filtered.map expression with one cell per column', () => {
80
+ const body = composeListBody(makeContext());
81
+ expect(body).toContain('filtered.map((item, idx) =>');
82
+ expect(body).toContain('onClick={() => onSelect?.(item)}');
83
+
84
+ // Count how many times each field name appears in cell-context (inside
85
+ // the `<td>...item.FIELD ??` substring). Metadata should appear 0 times;
86
+ // business fields exactly once each.
87
+ const cellsRegion = body.slice(body.indexOf('<tbody'));
88
+ const cellAppearances = (field: string) =>
89
+ (cellsRegion.match(new RegExp(`item as any\\)\\.${field} \\?\\?`, 'g')) ?? []).length;
90
+
91
+ expect(cellAppearances('title')).toBe(1);
92
+ expect(cellAppearances('body')).toBe(1);
93
+ expect(cellAppearances('authorId')).toBe(1);
94
+ expect(cellAppearances('id')).toBe(0); // metadata
95
+ expect(cellAppearances('publishedAt')).toBe(0); // metadata
96
+ });
97
+
98
+ it('handles a model with zero non-metadata attributes', () => {
99
+ const empty: ModelSpec = {
100
+ name: 'Pulse',
101
+ attributes: { id: { type: 'UUID', required: true } },
102
+ };
103
+ const body = composeListBody(makeContext({
104
+ model: empty,
105
+ view: { type: 'list', model: 'Pulse' },
106
+ viewName: 'PulseListView',
107
+ modelSchemas: { Pulse: empty },
108
+ }));
109
+ // Table renders with empty columns array (no <th> cells inside <tr>)
110
+ expect(body).toContain('<thead');
111
+ // Row map still present — but cells block empty
112
+ expect(body).toContain('filtered.map');
113
+ });
114
+ });
115
+
116
+ describe('emitView wired to composeListBody — end-to-end', () => {
117
+ it('produces a complete PostListView.tsx that parses', () => {
118
+ const source = emitView(makeContext());
119
+ assertValidTsx(source, 'PostListView');
120
+ });
121
+
122
+ it('integrates skeleton + composer correctly', () => {
123
+ const source = emitView(makeContext());
124
+ // From skeleton
125
+ expect(source).toContain('export function PostListView');
126
+ expect(source).toContain('usePostsQuery');
127
+ expect(source).toContain('useDeletePostMutation');
128
+ // From composer (canonical adapter shell + row map)
129
+ expect(source).toContain('filtered.map');
130
+ expect(source).toContain('onSelect?.(item)');
131
+ expect(source).toContain('>Title<');
132
+ // No stray sentinels or placeholders
133
+ expect(source).not.toContain('__SPECVERSE_TBODY_ROWS__');
134
+ expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
135
+ });
136
+
137
+ it('produces readable, line-broken output (no wall of HTML)', () => {
138
+ const source = emitView(makeContext());
139
+ // Sanity: the emitted file should be multi-line idiomatic React,
140
+ // not one giant collapsed line.
141
+ const lines = source.split('\n');
142
+ expect(lines.length).toBeGreaterThan(30);
143
+ // And no obviously-HTML escape leakage
144
+ expect(source).not.toContain('dangerouslySetInnerHTML');
145
+ });
146
+ });
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import * as ts from 'typescript';
6
+ import { generate } from '../orchestrator.js';
7
+ import { sha256, HASHES_DIR, HASHES_FILE } from '../regen-safety.js';
8
+
9
+ let projectRoot: string;
10
+
11
+ beforeEach(() => {
12
+ projectRoot = mkdtempSync(join(tmpdir(), 'react-starter-orch-'));
13
+ // Suppress the orchestrator's console.log summary during tests.
14
+ vi.spyOn(console, 'log').mockImplementation(() => {});
15
+ });
16
+
17
+ afterEach(() => {
18
+ rmSync(projectRoot, { recursive: true, force: true });
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ function makeSpec() {
23
+ return {
24
+ metadata: { name: 'My App' },
25
+ models: {
26
+ Post: {
27
+ name: 'Post',
28
+ attributes: {
29
+ id: { type: 'UUID', auto: 'uuid4' },
30
+ title: { type: 'String', required: true },
31
+ body: { type: 'Text' },
32
+ },
33
+ },
34
+ },
35
+ views: {},
36
+ };
37
+ }
38
+
39
+ function assertValidTsx(source: string, label: string): void {
40
+ const result = ts.transpileModule(source, {
41
+ compilerOptions: {
42
+ jsx: ts.JsxEmit.Preserve,
43
+ target: ts.ScriptTarget.ES2022,
44
+ module: ts.ModuleKind.ESNext,
45
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
46
+ strict: false,
47
+ },
48
+ reportDiagnostics: true,
49
+ });
50
+ const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
51
+ if (errors.length > 0) {
52
+ const msg = errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('\n');
53
+ throw new Error(`${label} failed to parse:\n${msg}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Apply an orchestrator output to disk — simulates what realize does
59
+ * when it receives the multi-file generator result.
60
+ */
61
+ function applyOutput(output: Record<string, string>, root: string): void {
62
+ for (const [relPath, content] of Object.entries(output)) {
63
+ const abs = join(root, relPath);
64
+ mkdirSync(join(abs, '..'), { recursive: true });
65
+ writeFileSync(abs, content, 'utf8');
66
+ }
67
+ }
68
+
69
+ describe('orchestrator — first-run (empty project)', () => {
70
+ it('returns the full set of files: views + helpers + App.tsx + package.json + hash manifest', async () => {
71
+ const output = await generate({ spec: makeSpec(), projectRoot });
72
+ const paths = Object.keys(output).sort();
73
+ expect(paths).toEqual([
74
+ `${HASHES_DIR}/${HASHES_FILE}`,
75
+ 'package.json',
76
+ 'src/App.tsx',
77
+ 'src/lib/entity-display.ts',
78
+ 'src/views/PostDashboardView.tsx',
79
+ 'src/views/PostDetailView.tsx',
80
+ 'src/views/PostFormView.tsx',
81
+ 'src/views/PostListView.tsx',
82
+ ]);
83
+ });
84
+
85
+ it('does not write to disk (that is realize\'s job)', async () => {
86
+ await generate({ spec: makeSpec(), projectRoot });
87
+ // Orchestrator is pure — nothing on disk yet.
88
+ expect(existsSync(join(projectRoot, 'package.json'))).toBe(false);
89
+ expect(existsSync(join(projectRoot, 'src/App.tsx'))).toBe(false);
90
+ });
91
+
92
+ it('every emitted .tsx parses as valid TSX', async () => {
93
+ const output = await generate({ spec: makeSpec(), projectRoot });
94
+ for (const [path, source] of Object.entries(output)) {
95
+ if (path.endsWith('.tsx')) assertValidTsx(source, path);
96
+ }
97
+ });
98
+
99
+ it('package.json has no @specverse/runtime dependency', async () => {
100
+ const output = await generate({ spec: makeSpec(), projectRoot });
101
+ const pkg = JSON.parse(output['package.json']);
102
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
103
+ expect(deps['@specverse/runtime']).toBeUndefined();
104
+ });
105
+
106
+ it('the emitted hash manifest records every approved file', async () => {
107
+ const output = await generate({ spec: makeSpec(), projectRoot });
108
+ const manifestKey = `${HASHES_DIR}/${HASHES_FILE}`;
109
+ const manifest = JSON.parse(output[manifestKey]);
110
+ // Every returned file (except the manifest itself) should have a hash.
111
+ for (const path of Object.keys(output)) {
112
+ if (path === manifestKey) continue;
113
+ expect(manifest[path]).toBe(sha256(output[path]));
114
+ }
115
+ });
116
+ });
117
+
118
+ describe('orchestrator — regeneration safety', () => {
119
+ it('re-approves pristine files on a second run', async () => {
120
+ // First run — write outputs to disk to simulate realize.
121
+ const firstOutput = await generate({ spec: makeSpec(), projectRoot });
122
+ applyOutput(firstOutput, projectRoot);
123
+
124
+ // Second run — everything is pristine, should be re-approved.
125
+ const secondOutput = await generate({ spec: makeSpec(), projectRoot });
126
+ // The view files etc. should reappear in the output (realize will overwrite).
127
+ expect(secondOutput['src/views/PostListView.tsx']).toBeDefined();
128
+ });
129
+
130
+ it('omits a user-edited file from the output', async () => {
131
+ const firstOutput = await generate({ spec: makeSpec(), projectRoot });
132
+ applyOutput(firstOutput, projectRoot);
133
+
134
+ // User edits one of the files
135
+ const editedPath = 'src/views/PostListView.tsx';
136
+ writeFileSync(join(projectRoot, editedPath), '/* edited */', 'utf8');
137
+
138
+ const secondOutput = await generate({ spec: makeSpec(), projectRoot });
139
+
140
+ // User-edited file not in the output → realize won't overwrite it
141
+ expect(secondOutput[editedPath]).toBeUndefined();
142
+ // Other files still present
143
+ expect(secondOutput['src/views/PostDetailView.tsx']).toBeDefined();
144
+ // Manifest preserves the OLD hash for the skipped file
145
+ const manifest = JSON.parse(secondOutput[`${HASHES_DIR}/${HASHES_FILE}`]);
146
+ const oldManifest = JSON.parse(firstOutput[`${HASHES_DIR}/${HASHES_FILE}`]);
147
+ expect(manifest[editedPath]).toBe(oldManifest[editedPath]);
148
+ });
149
+
150
+ it('is cautious when the user deletes the hash manifest', async () => {
151
+ const firstOutput = await generate({ spec: makeSpec(), projectRoot });
152
+ applyOutput(firstOutput, projectRoot);
153
+
154
+ // User deletes the hash manifest
155
+ rmSync(join(projectRoot, HASHES_DIR), { recursive: true, force: true });
156
+
157
+ const secondOutput = await generate({ spec: makeSpec(), projectRoot });
158
+
159
+ // All existing files are now "unknown origin" → skipped.
160
+ // Output should only contain the fresh hash manifest.
161
+ expect(Object.keys(secondOutput)).toEqual([`${HASHES_DIR}/${HASHES_FILE}`]);
162
+ });
163
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * P2 — Factory generators consume the pattern library, not forks
3
+ *
4
+ * Architectural invariant I5 from VIEW-RENDERING-ARCHITECTURE.md:
5
+ * "Factory generators are consumers of the pattern library, not
6
+ * reimplementations of it."
7
+ *
8
+ * Every composer / body-generator under react-starter must import
9
+ * from `@specverse/runtime/views/core` or `@specverse/runtime/views/tailwind`.
10
+ * A generator that doesn't is a reimplementation — it's drift-prone
11
+ * and violates the "one pattern library, three consumers" contract.
12
+ *
13
+ * Scope: files matching *-composer.ts or view-emitter.ts / generator
14
+ * files that actually produce view-layout code. Skeletons (.tsx.template)
15
+ * are content-only and exempt. helpers-emitter / html-to-jsx are
16
+ * transformation utilities, exempt — they don't consume pattern data.
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { readdirSync, readFileSync } from 'fs';
21
+ import { join, resolve } from 'path';
22
+ import { fileURLToPath } from 'url';
23
+
24
+ const HERE = fileURLToPath(import.meta.url);
25
+ const STARTER_DIR = resolve(HERE, '..', '..');
26
+
27
+ const RUNTIME_IMPORT_PATTERN =
28
+ /from\s+['"]@specverse\/runtime\/views\/(core|tailwind)['"]/;
29
+
30
+ /**
31
+ * Files required to import from @specverse/runtime/views/core or
32
+ * /views/tailwind. The filenames match the convention of any generator
33
+ * that emits view layout or composes pattern data.
34
+ */
35
+ const MUST_IMPORT_FROM_RUNTIME = (filename: string): boolean => {
36
+ // Every body composer (list / detail / form / dashboard).
37
+ if (/-body-composer\.ts$/.test(filename)) return true;
38
+ return false;
39
+ };
40
+
41
+ /**
42
+ * Files explicitly exempt from P2 — orchestrators, transformers, and
43
+ * utilities that don't consume pattern data. Each exemption carries
44
+ * a short justification as an inline comment near the import list.
45
+ */
46
+ const EXEMPT_FILES = new Set([
47
+ // Orchestrator: dispatches to composers (which do import runtime).
48
+ 'views-generator.ts',
49
+ 'view-emitter.ts',
50
+ 'orchestrator.ts',
51
+ // Pure string transformer, no pattern data.
52
+ 'html-to-jsx.ts',
53
+ // Emits inline source code; no adapter calls needed.
54
+ 'helpers-emitter.ts',
55
+ 'app-tsx-generator.ts',
56
+ 'package-json-generator.ts',
57
+ // Pure utility for content-hashing, no pattern consumption.
58
+ 'regen-safety.ts',
59
+ ]);
60
+
61
+ function collectTypeScriptFiles(dir: string, prefix = ''): string[] {
62
+ const out: string[] = [];
63
+ const entries = readdirSync(dir, { withFileTypes: true });
64
+ for (const entry of entries) {
65
+ if (entry.name === '__tests__' || entry.name === 'skeletons') continue;
66
+ const full = join(dir, entry.name);
67
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
68
+ if (entry.isDirectory()) {
69
+ out.push(...collectTypeScriptFiles(full, rel));
70
+ } else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
71
+ out.push(rel);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ describe('P2 — factory generators consume the pattern library', () => {
78
+ const files = collectTypeScriptFiles(STARTER_DIR);
79
+
80
+ it('produces a non-empty file list (sanity)', () => {
81
+ expect(files.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ for (const relPath of files) {
85
+ const filename = relPath.split('/').pop()!;
86
+
87
+ if (EXEMPT_FILES.has(filename)) continue;
88
+ if (!MUST_IMPORT_FROM_RUNTIME(filename)) continue;
89
+
90
+ it(`${relPath} imports from @specverse/runtime/views/{core|tailwind}`, () => {
91
+ const content = readFileSync(join(STARTER_DIR, relPath), 'utf8');
92
+ const imports = RUNTIME_IMPORT_PATTERN.test(content);
93
+ if (!imports) {
94
+ throw new Error(
95
+ `${relPath} must import from @specverse/runtime/views/core or ` +
96
+ `@specverse/runtime/views/tailwind to satisfy invariant I5. ` +
97
+ `If it genuinely doesn't need pattern data, add it to the ` +
98
+ `EXEMPT_FILES set with a justification. Generators that emit ` +
99
+ `view-layout code inevitably use pattern constants (field ` +
100
+ `classification, atomic components) — sourcing those from ` +
101
+ `runtime is what keeps the three consumers in sync.`
102
+ );
103
+ }
104
+ });
105
+ }
106
+
107
+ it('every composer is covered by an explicit rule (no silent skips)', () => {
108
+ const composers = files.filter(f => f.endsWith('-body-composer.ts'));
109
+ expect(composers.length).toBeGreaterThanOrEqual(4); // list/detail/form/dashboard at minimum
110
+ for (const c of composers) {
111
+ const filename = c.split('/').pop()!;
112
+ expect(MUST_IMPORT_FROM_RUNTIME(filename)).toBe(true);
113
+ expect(EXEMPT_FILES.has(filename)).toBe(false);
114
+ }
115
+ });
116
+ });