@specverse/engines 4.2.2 → 4.3.1

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 (63) hide show
  1. package/dist/inference/core/specly-converter.d.ts.map +1 -1
  2. package/dist/inference/core/specly-converter.js +43 -22
  3. package/dist/inference/core/specly-converter.js.map +1 -1
  4. package/dist/inference/index.d.ts.map +1 -1
  5. package/dist/inference/index.js +29 -0
  6. package/dist/inference/index.js.map +1 -1
  7. package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts +8 -5
  8. package/dist/inference/ui-contracts/rules/action-buttons-present.d.ts.map +1 -1
  9. package/dist/inference/ui-contracts/rules/action-buttons-present.js +85 -19
  10. package/dist/inference/ui-contracts/rules/action-buttons-present.js.map +1 -1
  11. package/dist/inference/ui-contracts/test-case-types.d.ts +3 -0
  12. package/dist/inference/ui-contracts/test-case-types.d.ts.map +1 -1
  13. package/dist/inference/ui-contracts/translator.d.ts.map +1 -1
  14. package/dist/inference/ui-contracts/translator.js +4 -0
  15. package/dist/inference/ui-contracts/translator.js.map +1 -1
  16. package/dist/libs/instance-factories/applications/templates/react/index-html-generator.js +2 -2
  17. package/dist/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.js +291 -59
  18. package/dist/libs/instance-factories/applications/templates/react-starter/belongs-to.js +37 -0
  19. package/dist/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.js +9 -124
  20. package/dist/libs/instance-factories/applications/templates/react-starter/detail-body-composer.js +9 -75
  21. package/dist/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.js +129 -0
  22. package/dist/libs/instance-factories/applications/templates/react-starter/form-body-composer.js +9 -212
  23. package/dist/libs/instance-factories/applications/templates/react-starter/helpers-emitter.js +260 -5
  24. package/dist/libs/instance-factories/applications/templates/react-starter/list-body-composer.js +8 -50
  25. package/dist/libs/instance-factories/applications/templates/react-starter/operation-emitters.js +470 -0
  26. package/dist/libs/instance-factories/applications/templates/react-starter/operation-view-generator.js +136 -0
  27. package/dist/libs/instance-factories/applications/templates/react-starter/package-json-generator.js +2 -1
  28. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
  29. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
  30. package/dist/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
  31. package/dist/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.js +49 -10
  32. package/dist/libs/instance-factories/applications/templates/react-starter/view-emitter.js +223 -10
  33. package/dist/libs/instance-factories/applications/templates/react-starter/views-generator.js +14 -1
  34. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +13 -1
  35. package/dist/libs/instance-factories/controllers/templates/fastify/server-generator.js +18 -0
  36. package/libs/instance-factories/applications/templates/react/index-html-generator.ts +2 -2
  37. package/libs/instance-factories/applications/templates/react-starter/__tests__/dashboard-body-composer.test.ts +3 -1
  38. package/libs/instance-factories/applications/templates/react-starter/__tests__/detail-body-composer.test.ts +24 -25
  39. package/libs/instance-factories/applications/templates/react-starter/__tests__/list-body-composer.test.ts +3 -3
  40. package/libs/instance-factories/applications/templates/react-starter/__tests__/orchestrator.test.ts +2 -0
  41. package/libs/instance-factories/applications/templates/react-starter/__tests__/parity-p3-rendered-output.test.ts +1 -1
  42. package/libs/instance-factories/applications/templates/react-starter/__tests__/starter-generators.test.ts +5 -4
  43. package/libs/instance-factories/applications/templates/react-starter/__tests__/views-generator.test.ts +11 -4
  44. package/libs/instance-factories/applications/templates/react-starter/app-tsx-generator.ts +377 -71
  45. package/libs/instance-factories/applications/templates/react-starter/belongs-to.ts +66 -0
  46. package/libs/instance-factories/applications/templates/react-starter/dashboard-body-composer.ts +15 -182
  47. package/libs/instance-factories/applications/templates/react-starter/detail-body-composer.ts +16 -128
  48. package/libs/instance-factories/applications/templates/react-starter/emit-jsx-source.ts +233 -0
  49. package/libs/instance-factories/applications/templates/react-starter/form-body-composer.ts +16 -376
  50. package/libs/instance-factories/applications/templates/react-starter/helpers-emitter.ts +282 -4
  51. package/libs/instance-factories/applications/templates/react-starter/list-body-composer.ts +26 -135
  52. package/libs/instance-factories/applications/templates/react-starter/operation-emitters.ts +497 -0
  53. package/libs/instance-factories/applications/templates/react-starter/operation-view-generator.ts +209 -0
  54. package/libs/instance-factories/applications/templates/react-starter/package-json-generator.ts +1 -0
  55. package/libs/instance-factories/applications/templates/react-starter/skeletons/detail.tsx.template +54 -61
  56. package/libs/instance-factories/applications/templates/react-starter/skeletons/form.tsx.template +115 -27
  57. package/libs/instance-factories/applications/templates/react-starter/skeletons/list.tsx.template +17 -9
  58. package/libs/instance-factories/applications/templates/react-starter/use-api-hooks-starter-generator.ts +71 -10
  59. package/libs/instance-factories/applications/templates/react-starter/view-emitter.ts +359 -18
  60. package/libs/instance-factories/applications/templates/react-starter/views-generator.ts +28 -1
  61. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +13 -1
  62. package/libs/instance-factories/controllers/templates/fastify/server-generator.ts +18 -0
  63. package/package.json +2 -2
@@ -0,0 +1,470 @@
1
+ function emitFieldInput() {
2
+ return `/**
3
+ * FieldInput \u2014 type-aware input component used by OperationExecutor.
4
+ *
5
+ * Inlined into this project by @specverse/realize (ReactAppStarter).
6
+ * Edit freely. Ported from app-demo's FieldInput.
7
+ *
8
+ * Renders the right input for a parameter type:
9
+ * - UUID ending in Id \u2192 entity dropdown via EntitySelect
10
+ * - Boolean \u2192 checkbox (or select in compact mode)
11
+ * - Integer/Number/Decimal \u2192 number input
12
+ * - DateTime/Date \u2192 native picker
13
+ * - Email \u2192 email input
14
+ * - Text (multiline) \u2192 textarea
15
+ * - default \u2192 text input
16
+ */
17
+ import { useState, useEffect } from 'react';
18
+ import { getEntityDisplayName } from './entity-display';
19
+
20
+ const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
21
+
22
+ function pluralize(s: string): string {
23
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
24
+ if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
25
+ return s + 's';
26
+ }
27
+
28
+ function humanizeFieldName(name: string): string {
29
+ return name
30
+ .replace(/([A-Z])/g, ' $1')
31
+ .replace(/^./, c => c.toUpperCase())
32
+ .trim();
33
+ }
34
+
35
+ interface FieldInputProps {
36
+ name: string;
37
+ typeStr: string;
38
+ value: any;
39
+ onChange: (value: any) => void;
40
+ required?: boolean;
41
+ disabled?: boolean;
42
+ /** Compact mode for inline use (smaller padding, no label). */
43
+ compact?: boolean;
44
+ }
45
+
46
+ const inputClass = (compact: boolean) => compact
47
+ ? 'w-full px-2 py-1 text-xs bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-1 focus:ring-blue-500'
48
+ : 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-800 disabled:cursor-not-allowed';
49
+
50
+ export function FieldInput({ name, typeStr, value, onChange, required, disabled, compact = false }: FieldInputProps) {
51
+ const type = typeStr.toLowerCase();
52
+
53
+ if (type.includes('bool')) {
54
+ return compact ? (
55
+ <select
56
+ value={value ?? ''}
57
+ onChange={e => onChange(e.target.value === 'true')}
58
+ disabled={disabled}
59
+ className={inputClass(compact)}
60
+ >
61
+ <option value="">Select...</option>
62
+ <option value="true">true</option>
63
+ <option value="false">false</option>
64
+ </select>
65
+ ) : (
66
+ <div className="flex items-center pt-2">
67
+ <input
68
+ type="checkbox"
69
+ checked={!!value}
70
+ onChange={e => onChange(e.target.checked)}
71
+ disabled={disabled}
72
+ className="h-4 w-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"
73
+ />
74
+ <span className="ml-2 text-sm text-gray-600 dark:text-gray-300">
75
+ {value ? 'Yes' : 'No'}
76
+ </span>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (type.includes('int') || type.includes('number') || type.includes('decimal')) {
82
+ return (
83
+ <input
84
+ type="number"
85
+ step={type.includes('decimal') ? '0.01' : '1'}
86
+ value={value ?? ''}
87
+ onChange={e => onChange(type.includes('int') ? parseInt(e.target.value) || 0 : parseFloat(e.target.value) || 0)}
88
+ disabled={disabled}
89
+ required={required}
90
+ className={inputClass(compact)}
91
+ />
92
+ );
93
+ }
94
+
95
+ if (type.includes('datetime')) {
96
+ return (
97
+ <input
98
+ type="datetime-local"
99
+ value={value ? new Date(value).toISOString().slice(0, 16) : ''}
100
+ onChange={e => onChange(e.target.value ? new Date(e.target.value).toISOString() : '')}
101
+ disabled={disabled}
102
+ required={required}
103
+ className={inputClass(compact)}
104
+ />
105
+ );
106
+ }
107
+
108
+ if (type.includes('date')) {
109
+ return (
110
+ <input
111
+ type="date"
112
+ value={value ? new Date(value).toISOString().slice(0, 10) : ''}
113
+ onChange={e => onChange(e.target.value ? new Date(e.target.value).toISOString() : '')}
114
+ disabled={disabled}
115
+ required={required}
116
+ className={inputClass(compact)}
117
+ />
118
+ );
119
+ }
120
+
121
+ if (type.includes('email')) {
122
+ return (
123
+ <input
124
+ type="email"
125
+ value={value ?? ''}
126
+ onChange={e => onChange(e.target.value)}
127
+ disabled={disabled}
128
+ required={required}
129
+ placeholder={humanizeFieldName(name)}
130
+ className={inputClass(compact)}
131
+ />
132
+ );
133
+ }
134
+
135
+ if (type === 'text') {
136
+ return (
137
+ <textarea
138
+ value={value ?? ''}
139
+ onChange={e => onChange(e.target.value)}
140
+ disabled={disabled}
141
+ required={required}
142
+ rows={compact ? 2 : 3}
143
+ placeholder={humanizeFieldName(name)}
144
+ className={\`\${inputClass(compact)} resize-none\`}
145
+ />
146
+ );
147
+ }
148
+
149
+ return (
150
+ <input
151
+ type="text"
152
+ value={value ?? ''}
153
+ onChange={e => onChange(e.target.value)}
154
+ disabled={disabled}
155
+ required={required}
156
+ placeholder={\`\${humanizeFieldName(name)}\${typeStr ? \` (\${typeStr})\` : ''}\`}
157
+ className={inputClass(compact)}
158
+ />
159
+ );
160
+ }
161
+
162
+ /**
163
+ * EntitySelect \u2014 dropdown of existing entities, for UUID parameters
164
+ * that reference a model. Fetches \`GET /api/{resource}\` (static REST
165
+ * convention) and renders each entity by its display name.
166
+ */
167
+ interface EntitySelectProps {
168
+ modelName: string;
169
+ value: string;
170
+ onChange: (value: string) => void;
171
+ compact?: boolean;
172
+ /** Filter entities by a foreign key value, e.g. { categoryId: 'abc-123' }.
173
+ * Used for cascading pickers where a second param depends on the first. */
174
+ filterBy?: Record<string, string>;
175
+ }
176
+
177
+ export function EntitySelect({ modelName, value, onChange, compact = false, filterBy }: EntitySelectProps) {
178
+ const [allEntities, setAllEntities] = useState<any[]>([]);
179
+ const resource = pluralize(modelName).toLowerCase();
180
+
181
+ useEffect(() => {
182
+ fetch(\`\${API_BASE}/api/\${resource}\`)
183
+ .then(r => r.json())
184
+ .then(data => setAllEntities(Array.isArray(data) ? data : []))
185
+ .catch(() => setAllEntities([]));
186
+ }, [resource]);
187
+
188
+ const entities = filterBy
189
+ ? allEntities.filter(e => {
190
+ for (const [fk, fkValue] of Object.entries(filterBy)) {
191
+ if (!fkValue) continue;
192
+ if (!(fk in e)) continue;
193
+ if (e[fk] !== fkValue) return false;
194
+ }
195
+ return true;
196
+ })
197
+ : allEntities;
198
+
199
+ return (
200
+ <select
201
+ value={value || ''}
202
+ onChange={e => onChange(e.target.value)}
203
+ className={inputClass(compact)}
204
+ >
205
+ <option value="">
206
+ Select {modelName}...{filterBy && Object.values(filterBy).some(v => v) ? \` (\${entities.length} matching)\` : ''}
207
+ </option>
208
+ {entities.map(entity => {
209
+ const id = entity.id;
210
+ return (
211
+ <option key={id} value={id}>
212
+ {getEntityDisplayName(entity)}{id ? \` (\${String(id).slice(0, 8)}...)\` : ''}
213
+ </option>
214
+ );
215
+ })}
216
+ </select>
217
+ );
218
+ }
219
+ `;
220
+ }
221
+ function emitOperationExecutor() {
222
+ return `/**
223
+ * OperationExecutor \u2014 run a controller command or service operation
224
+ * against the backend. Renders a parameter form, an Execute button,
225
+ * and the result/error beneath.
226
+ *
227
+ * Inlined into this project by @specverse/realize (ReactAppStarter).
228
+ * Edit freely. Ported from app-demo's OperationExecutor.
229
+ *
230
+ * For source='service' ops: POST /api/services/{serviceName}/{operationName}
231
+ * For source='controller' ops: POST /api/{resource}/{operationName}, where
232
+ * resource is the pluralized-lowercase form of the controller's managed
233
+ * model (ControllerName minus 'Controller' suffix).
234
+ */
235
+ import { useState, useMemo } from 'react';
236
+ import { FieldInput, EntitySelect } from './FieldInput';
237
+ import { OperationResultView } from './OperationResultView';
238
+ import { Play } from 'lucide-react';
239
+
240
+ const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
241
+
242
+ function pluralize(s: string): string {
243
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
244
+ if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
245
+ return s + 's';
246
+ }
247
+
248
+ interface OperationExecutorProps {
249
+ /** The service (source='service') or controller (source='controller') name. */
250
+ serviceName: string;
251
+ operationName: string;
252
+ /** Parameter declarations from the spec. Supports both
253
+ * \`{ name: { type: 'String' } }\` and \`{ name: 'String' }\` shapes. */
254
+ parameters?: Record<string, any>;
255
+ /** Optional; currently unused but kept for forward compat with
256
+ * app-demo where requires gates the run. */
257
+ requires?: string[];
258
+ source?: 'service' | 'controller';
259
+ }
260
+
261
+ export function OperationExecutor({
262
+ serviceName,
263
+ operationName,
264
+ parameters,
265
+ requires: _requires,
266
+ source = 'service',
267
+ }: OperationExecutorProps) {
268
+ const [params, setParams] = useState<Record<string, any>>({});
269
+ const [loading, setLoading] = useState(false);
270
+ const [result, setResult] = useState<any>(null);
271
+ const [error, setError] = useState<string | null>(null);
272
+
273
+ const paramDefs = useMemo(() => {
274
+ if (!parameters) return [];
275
+ const entries: Array<[string, any]> = Array.isArray(parameters)
276
+ ? parameters.map((p: any) => [p.name, p])
277
+ : Object.entries(parameters);
278
+ if (entries.length === 0) return [];
279
+
280
+ return entries.map(([name, def]) => {
281
+ const typeStr = typeof def === 'string' ? def : def?.type || '';
282
+ const isUuid = typeStr.toLowerCase().includes('uuid');
283
+ let refModel: string | null = null;
284
+ if (isUuid && name.endsWith('Id')) {
285
+ const base = name.slice(0, -2);
286
+ refModel = base.charAt(0).toUpperCase() + base.slice(1);
287
+ }
288
+ return { name, type: typeStr, isUuid, refModel };
289
+ });
290
+ }, [parameters]);
291
+
292
+ const execute = async () => {
293
+ setLoading(true);
294
+ setError(null);
295
+ setResult(null);
296
+ try {
297
+ let url: string;
298
+ if (source === 'controller') {
299
+ // ControllerName \u2192 resource-plural-lowercase (PostController \u2192 posts)
300
+ const modelName = serviceName.replace(/Controller$/, '');
301
+ const resource = pluralize(modelName).toLowerCase();
302
+ url = \`\${API_BASE}/api/\${resource}/\${operationName}\`;
303
+ } else {
304
+ url = \`\${API_BASE}/api/services/\${serviceName}/\${operationName}\`;
305
+ }
306
+ const res = await fetch(url, {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify(params),
310
+ });
311
+ const data = await res.json();
312
+ setResult(data);
313
+ if (!res.ok) {
314
+ setError(data?.message || data?.error || \`\${res.status} \${res.statusText}\`);
315
+ } else if (data && data.success === false) {
316
+ setError(data.error?.message || 'Operation failed');
317
+ }
318
+ } catch (err: any) {
319
+ setError(err?.message ?? String(err));
320
+ } finally {
321
+ setLoading(false);
322
+ }
323
+ };
324
+
325
+ return (
326
+ <div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 space-y-2">
327
+ {paramDefs.length > 0 ? (
328
+ paramDefs.map(param => {
329
+ // Cascading filter: if this param's model has a belongsTo to another
330
+ // already-filled param's model, filter by it.
331
+ let filterBy: Record<string, string> | undefined;
332
+ if (param.refModel) {
333
+ for (const other of paramDefs) {
334
+ if (other === param || !other.refModel) continue;
335
+ const fk = other.refModel.charAt(0).toLowerCase() + other.refModel.slice(1) + 'Id';
336
+ if (params[other.name]) {
337
+ filterBy = { ...filterBy, [fk]: params[other.name] };
338
+ }
339
+ }
340
+ }
341
+ return (
342
+ <div key={param.name} className="flex items-center gap-2">
343
+ <label
344
+ className="text-xs text-gray-500 dark:text-gray-400 w-24 flex-shrink-0 truncate"
345
+ title={param.name}
346
+ >
347
+ {param.name}
348
+ </label>
349
+ {param.refModel ? (
350
+ <EntitySelect
351
+ modelName={param.refModel}
352
+ value={params[param.name] || ''}
353
+ onChange={v => setParams(prev => ({ ...prev, [param.name]: v }))}
354
+ filterBy={filterBy}
355
+ compact
356
+ />
357
+ ) : (
358
+ <FieldInput
359
+ name={param.name}
360
+ typeStr={param.type}
361
+ value={params[param.name]}
362
+ onChange={v => setParams(prev => ({ ...prev, [param.name]: v }))}
363
+ compact
364
+ />
365
+ )}
366
+ </div>
367
+ );
368
+ })
369
+ ) : (
370
+ <p className="text-xs text-gray-400 italic">No parameters defined</p>
371
+ )}
372
+
373
+ <button
374
+ onClick={execute}
375
+ disabled={loading}
376
+ className="w-full px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded flex items-center justify-center gap-1 disabled:opacity-50"
377
+ >
378
+ <Play className="w-3 h-3" />
379
+ {loading ? 'Executing...' : \`Execute \${operationName}\`}
380
+ </button>
381
+
382
+ {error && !result && (
383
+ <pre className="p-2 text-xs text-red-400 bg-red-50 dark:bg-red-900/20 rounded overflow-x-auto">
384
+ {error}
385
+ </pre>
386
+ )}
387
+ {result && <OperationResultView result={result} />}
388
+ </div>
389
+ );
390
+ }
391
+ `;
392
+ }
393
+ function emitOperationResultView() {
394
+ return `/**
395
+ * OperationResultView \u2014 formatted display of an operation's result.
396
+ *
397
+ * Inlined into this project by @specverse/realize (ReactAppStarter).
398
+ * Edit freely. Ported from app-demo's OperationResultView.
399
+ *
400
+ * Handles three shapes:
401
+ * - Success with data \u2192 JSON pretty-print
402
+ * - Success without data \u2192 "Operation succeeded" banner
403
+ * - Error { success: false, error: ... } \u2192 error banner + details
404
+ * - Unexpected \u2192 raw JSON
405
+ */
406
+ import { CheckCircle, XCircle } from 'lucide-react';
407
+
408
+ interface OperationResultViewProps {
409
+ result: any;
410
+ }
411
+
412
+ function JsonDisplay({ value }: { value: any }) {
413
+ return (
414
+ <pre className="p-3 text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded overflow-x-auto text-gray-900 dark:text-gray-100">
415
+ {JSON.stringify(value, null, 2)}
416
+ </pre>
417
+ );
418
+ }
419
+
420
+ export function OperationResultView({ result }: OperationResultViewProps) {
421
+ // Explicit error envelope: { success: false, error: ... }
422
+ if (result && typeof result === 'object' && result.success === false) {
423
+ const errMsg =
424
+ (typeof result.error === 'object' ? result.error?.message : result.error) ||
425
+ 'Operation failed';
426
+ return (
427
+ <div className="space-y-2">
428
+ <div className="flex items-start gap-2 p-2 rounded bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-xs">
429
+ <XCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
430
+ <div>
431
+ <div className="font-medium">Operation failed</div>
432
+ <div className="mt-1">{errMsg}</div>
433
+ </div>
434
+ </div>
435
+ {result.error && typeof result.error === 'object' && <JsonDisplay value={result.error} />}
436
+ </div>
437
+ );
438
+ }
439
+
440
+ // Explicit success envelope: { success: true, data?: ... }
441
+ if (result && typeof result === 'object' && result.success === true) {
442
+ return (
443
+ <div className="space-y-2">
444
+ <div className="flex items-center gap-2 p-2 rounded bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 text-xs">
445
+ <CheckCircle className="w-4 h-4 flex-shrink-0" />
446
+ <span className="font-medium">Operation succeeded</span>
447
+ </div>
448
+ {result.data !== undefined && <JsonDisplay value={result.data} />}
449
+ </div>
450
+ );
451
+ }
452
+
453
+ // No envelope \u2014 just show whatever came back (most REST endpoints).
454
+ return (
455
+ <div className="space-y-2">
456
+ <div className="flex items-center gap-2 p-2 rounded bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 text-xs">
457
+ <CheckCircle className="w-4 h-4 flex-shrink-0" />
458
+ <span className="font-medium">Operation completed</span>
459
+ </div>
460
+ {result !== undefined && result !== null && <JsonDisplay value={result} />}
461
+ </div>
462
+ );
463
+ }
464
+ `;
465
+ }
466
+ export {
467
+ emitFieldInput,
468
+ emitOperationExecutor,
469
+ emitOperationResultView
470
+ };
@@ -0,0 +1,136 @@
1
+ const STANDARD_OPS = /* @__PURE__ */ new Set([
2
+ "create",
3
+ "retrieve",
4
+ "retrieve_many",
5
+ "update",
6
+ "delete",
7
+ "validate",
8
+ "evolve",
9
+ "list",
10
+ "query",
11
+ "get",
12
+ "attachProfile",
13
+ "detachProfile",
14
+ "hasProfile",
15
+ "handleChildAdded",
16
+ "handleChildRemoved",
17
+ "validateRelationshipIntegrity",
18
+ "repairRelationshipIntegrity"
19
+ ]);
20
+ function normalizeParameters(raw) {
21
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
22
+ const out = {};
23
+ for (const [name, def] of Object.entries(raw)) {
24
+ let type;
25
+ if (typeof def === "string") {
26
+ type = def;
27
+ } else if (def && typeof def === "object" && "type" in def && typeof def.type === "string") {
28
+ type = def.type;
29
+ } else {
30
+ type = "String";
31
+ }
32
+ out[name] = type.split(/\s+/)[0] || "String";
33
+ }
34
+ return Object.keys(out).length > 0 ? out : void 0;
35
+ }
36
+ function collectOperationViews(spec) {
37
+ const out = [];
38
+ const seen = /* @__PURE__ */ new Set();
39
+ const controllers = spec.controllers ?? {};
40
+ for (const [ctlName, ctl] of Object.entries(controllers)) {
41
+ const ops = ctl?.operations ?? ctl?.commands ?? {};
42
+ for (const [opName, op] of Object.entries(ops)) {
43
+ if (STANDARD_OPS.has(opName)) continue;
44
+ const viewName = capitalize(opName) + "View";
45
+ if (seen.has(viewName)) continue;
46
+ seen.add(viewName);
47
+ out.push({
48
+ viewName,
49
+ label: humanize(opName),
50
+ serviceName: ctlName,
51
+ operationName: opName,
52
+ parameters: normalizeParameters(op?.parameters ?? op?.params),
53
+ requires: op?.requires,
54
+ returns: op?.returns,
55
+ source: "controller"
56
+ });
57
+ }
58
+ }
59
+ const services = spec.services ?? {};
60
+ for (const [svcName, svc] of Object.entries(services)) {
61
+ const rawOps = svc?.operations;
62
+ const opEntries = Array.isArray(rawOps) ? rawOps.map((o) => [o.name, o]) : Object.entries(rawOps ?? {});
63
+ for (const [opName, op] of opEntries) {
64
+ if (!opName) continue;
65
+ if (STANDARD_OPS.has(opName)) continue;
66
+ const viewName = capitalize(opName) + "View";
67
+ if (seen.has(viewName)) continue;
68
+ seen.add(viewName);
69
+ out.push({
70
+ viewName,
71
+ label: humanize(opName),
72
+ serviceName: svcName,
73
+ operationName: opName,
74
+ parameters: normalizeParameters(op?.parameters ?? op?.params),
75
+ requires: op?.requires,
76
+ returns: op?.returns,
77
+ source: "service"
78
+ });
79
+ }
80
+ }
81
+ return out;
82
+ }
83
+ function emitOperationView(desc) {
84
+ const paramsJson = desc.parameters ? JSON.stringify(desc.parameters, null, 2).replace(/\n/g, "\n ") : "undefined";
85
+ const requiresJson = desc.requires ? JSON.stringify(desc.requires) : "undefined";
86
+ const returnsPill = desc.returns ? `
87
+ <span className="text-xs px-2 py-0.5 bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded font-mono">
88
+ \u2192 ${escapeJsx(desc.returns)}
89
+ </span>` : "";
90
+ return `/**
91
+ * ${desc.viewName} \u2014 generated by @specverse/realize (ReactAppStarter)
92
+ *
93
+ * Auto-generated operation view for ${desc.source} ${desc.serviceName}.${desc.operationName}.
94
+ * Safe to edit; the generator will not overwrite a hand-edited file
95
+ * (content-hashing in .specverse-gen/hashes.json).
96
+ */
97
+ import { OperationExecutor } from '../lib/OperationExecutor';
98
+
99
+ export function ${desc.viewName}() {
100
+ return (
101
+ <div className="p-6 space-y-4">
102
+ <div className="flex items-center gap-2">
103
+ <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
104
+ ${escapeJsx(desc.label)}
105
+ </h2>${returnsPill}
106
+ </div>
107
+ <p className="text-sm text-gray-500 dark:text-gray-400">
108
+ Execute ${escapeJsx(desc.serviceName)}.${escapeJsx(desc.operationName)}
109
+ </p>
110
+ <div className="max-w-lg">
111
+ <OperationExecutor
112
+ serviceName="${desc.serviceName}"
113
+ operationName="${desc.operationName}"
114
+ parameters={${paramsJson}}
115
+ requires={${requiresJson}}
116
+ source="${desc.source}"
117
+ />
118
+ </div>
119
+ </div>
120
+ );
121
+ }
122
+ `;
123
+ }
124
+ function capitalize(s) {
125
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
126
+ }
127
+ function humanize(s) {
128
+ return s.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()).trim();
129
+ }
130
+ function escapeJsx(s) {
131
+ return s.replace(/[<>&]/g, (ch) => ({ "<": "&lt;", ">": "&gt;", "&": "&amp;" })[ch]);
132
+ }
133
+ export {
134
+ collectOperationViews,
135
+ emitOperationView
136
+ };
@@ -15,7 +15,8 @@ async function generate(context) {
15
15
  dependencies: {
16
16
  react: "^18.2.0",
17
17
  "react-dom": "^18.2.0",
18
- "@tanstack/react-query": "^5.0.0"
18
+ "@tanstack/react-query": "^5.0.0",
19
+ "lucide-react": "^0.400.0"
19
20
  },
20
21
  devDependencies: {
21
22
  "@types/react": "^18.2.0",