@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,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
+ });
@@ -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
+ });