@polymorphism-tech/morph-spec 2.1.2 → 2.3.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.
@@ -0,0 +1,350 @@
1
+ /**
2
+ * UI Feature Detector
3
+ *
4
+ * Analyzes spec.md to detect UI needs automatically.
5
+ * Auto-triggers FASE 1.5: UI/UX when entities + CRUD are detected.
6
+ *
7
+ * MORPH-SPEC 3.0
8
+ */
9
+
10
+ /**
11
+ * Detect UI needs from spec content
12
+ * @param {string} specContent - Content of spec.md
13
+ * @returns {Object} UI needs analysis
14
+ */
15
+ export function detectUINeeds(specContent) {
16
+ const entities = parseEntities(specContent);
17
+ const crudOperations = detectCRUD(specContent);
18
+ const uiKeywords = detectUIKeywords(specContent);
19
+
20
+ const uiNeeds = [];
21
+
22
+ // Auto-detect CRUD UI needs
23
+ for (const entity of entities) {
24
+ if (crudOperations.create) {
25
+ uiNeeds.push({
26
+ type: 'form',
27
+ entity: entity.name,
28
+ operation: 'create',
29
+ fields: entity.properties,
30
+ priority: 'high'
31
+ });
32
+ }
33
+
34
+ if (crudOperations.read || crudOperations.list) {
35
+ uiNeeds.push({
36
+ type: 'list',
37
+ entity: entity.name,
38
+ fields: entity.properties.filter(p => p.displayInList !== false),
39
+ features: detectListFeatures(specContent),
40
+ priority: 'high'
41
+ });
42
+ }
43
+
44
+ if (crudOperations.update) {
45
+ uiNeeds.push({
46
+ type: 'form',
47
+ entity: entity.name,
48
+ operation: 'edit',
49
+ fields: entity.properties,
50
+ priority: 'medium'
51
+ });
52
+ }
53
+
54
+ if (crudOperations.delete) {
55
+ uiNeeds.push({
56
+ type: 'confirm-dialog',
57
+ entity: entity.name,
58
+ operation: 'delete',
59
+ priority: 'medium'
60
+ });
61
+ }
62
+ }
63
+
64
+ // Detect special UI components from keywords
65
+ if (uiKeywords.wizard) {
66
+ uiNeeds.push({
67
+ type: 'wizard',
68
+ steps: extractWizardSteps(specContent),
69
+ priority: 'high'
70
+ });
71
+ }
72
+
73
+ if (uiKeywords.dashboard) {
74
+ uiNeeds.push({
75
+ type: 'dashboard',
76
+ widgets: extractDashboardWidgets(specContent),
77
+ priority: 'high'
78
+ });
79
+ }
80
+
81
+ if (uiKeywords.upload) {
82
+ uiNeeds.push({
83
+ type: 'file-upload',
84
+ maxSize: extractMaxFileSize(specContent),
85
+ allowedTypes: extractAllowedFileTypes(specContent),
86
+ priority: 'high'
87
+ });
88
+ }
89
+
90
+ const shouldTriggerUIPhase = uiNeeds.length > 0 ||
91
+ entities.length > 0 ||
92
+ uiKeywords.hasUIKeywords;
93
+
94
+ return {
95
+ shouldTrigger: shouldTriggerUIPhase,
96
+ entities,
97
+ crudOperations,
98
+ uiNeeds,
99
+ keywords: uiKeywords,
100
+ confidence: calculateConfidence(entities, crudOperations, uiKeywords)
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Parse entities from spec
106
+ */
107
+ function parseEntities(specContent) {
108
+ const entities = [];
109
+
110
+ // Match pseudo-code entity definitions
111
+ const entityPattern = /(?:class|entity|table)\s+(\w+)\s*{([^}]+)}/gi;
112
+ let match;
113
+
114
+ while ((match = entityPattern.exec(specContent)) !== null) {
115
+ const entityName = match[1];
116
+ const body = match[2];
117
+
118
+ const properties = parseProperties(body);
119
+
120
+ entities.push({
121
+ name: entityName,
122
+ properties,
123
+ hasId: properties.some(p => p.name.toLowerCase() === 'id'),
124
+ hasTimestamps: properties.some(p =>
125
+ p.name.toLowerCase().includes('createdat') ||
126
+ p.name.toLowerCase().includes('updatedat')
127
+ )
128
+ });
129
+ }
130
+
131
+ return entities;
132
+ }
133
+
134
+ /**
135
+ * Parse properties from entity body
136
+ */
137
+ function parseProperties(body) {
138
+ const properties = [];
139
+ const lines = body.split('\n').map(l => l.trim()).filter(l => l);
140
+
141
+ for (const line of lines) {
142
+ // Match: Type PropertyName or PropertyName: Type
143
+ const match = line.match(/(?:(\w+)\s+(\w+)|(\w+)\s*:\s*(\w+))/);
144
+ if (!match) continue;
145
+
146
+ const type = match[1] || match[4];
147
+ const name = match[2] || match[3];
148
+
149
+ if (!type || !name) continue;
150
+
151
+ properties.push({
152
+ name,
153
+ type,
154
+ isRequired: !line.includes('?') && !line.includes('optional'),
155
+ isNullable: line.includes('?'),
156
+ displayInList: !isInternalProperty(name),
157
+ isEnum: type.endsWith('Status') || type.endsWith('Type') || type.endsWith('Kind')
158
+ });
159
+ }
160
+
161
+ return properties;
162
+ }
163
+
164
+ /**
165
+ * Check if property is internal (not for display)
166
+ */
167
+ function isInternalProperty(name) {
168
+ const internalNames = ['id', 'createdat', 'updatedat', 'deletedat', 'version', 'rowversion'];
169
+ return internalNames.includes(name.toLowerCase());
170
+ }
171
+
172
+ /**
173
+ * Detect CRUD operations from spec
174
+ */
175
+ function detectCRUD(specContent) {
176
+ const lower = specContent.toLowerCase();
177
+
178
+ return {
179
+ create: /\b(create|add|new|insert|register|upload)\b/i.test(lower),
180
+ read: /\b(read|get|view|show|display|retrieve|fetch)\b/i.test(lower),
181
+ update: /\b(update|edit|modify|change)\b/i.test(lower),
182
+ delete: /\b(delete|remove|destroy)\b/i.test(lower),
183
+ list: /\b(list|index|all|search|filter|query)\b/i.test(lower)
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Detect UI-specific keywords
189
+ */
190
+ function detectUIKeywords(specContent) {
191
+ const lower = specContent.toLowerCase();
192
+
193
+ return {
194
+ wizard: /\b(wizard|step|multi-?step|flow)\b/i.test(lower),
195
+ dashboard: /\b(dashboard|overview|summary|stats|metrics)\b/i.test(lower),
196
+ chart: /\b(chart|graph|visualization|plot)\b/i.test(lower),
197
+ upload: /\b(upload|file|photo|image|document|attachment)\b/i.test(lower),
198
+ modal: /\b(modal|dialog|popup|overlay)\b/i.test(lower),
199
+ form: /\b(form|input|field|validation)\b/i.test(lower),
200
+ table: /\b(table|grid|list|datagrid)\b/i.test(lower),
201
+ hasUIKeywords: /\b(wizard|dashboard|chart|upload|modal|form|table|ui|ux|screen|page|component)\b/i.test(lower)
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Detect list features (pagination, filters, etc.)
207
+ */
208
+ function detectListFeatures(specContent) {
209
+ const lower = specContent.toLowerCase();
210
+
211
+ return {
212
+ pagination: /\b(paginat|page|per.?page|offset|limit)\b/i.test(lower),
213
+ sorting: /\b(sort|order|asc|desc)\b/i.test(lower),
214
+ filtering: /\b(filter|search|query|where)\b/i.test(lower),
215
+ selection: /\b(select|check|multi.?select|bulk)\b/i.test(lower),
216
+ actions: /\b(action|edit|delete|view|export)\b/i.test(lower)
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Extract wizard steps from spec
222
+ */
223
+ function extractWizardSteps(specContent) {
224
+ const steps = [];
225
+ const stepPattern = /step\s+(\d+):\s*([^\n]+)/gi;
226
+ let match;
227
+
228
+ while ((match = stepPattern.exec(specContent)) !== null) {
229
+ steps.push({
230
+ number: parseInt(match[1]),
231
+ title: match[2].trim()
232
+ });
233
+ }
234
+
235
+ return steps.length > 0 ? steps : [
236
+ { number: 1, title: 'Information' },
237
+ { number: 2, title: 'Confirmation' }
238
+ ];
239
+ }
240
+
241
+ /**
242
+ * Extract dashboard widgets from spec
243
+ */
244
+ function extractDashboardWidgets(specContent) {
245
+ const widgets = [];
246
+
247
+ if (/\b(stat|metric|kpi|count)\b/i.test(specContent)) {
248
+ widgets.push({ type: 'stat-card', count: 3 });
249
+ }
250
+
251
+ if (/\b(chart|graph|visualization)\b/i.test(specContent)) {
252
+ widgets.push({ type: 'chart', chartType: 'line' });
253
+ }
254
+
255
+ if (/\b(table|grid|list|recent)\b/i.test(specContent)) {
256
+ widgets.push({ type: 'data-table', showActions: true });
257
+ }
258
+
259
+ return widgets;
260
+ }
261
+
262
+ /**
263
+ * Extract max file size from spec
264
+ */
265
+ function extractMaxFileSize(specContent) {
266
+ const match = specContent.match(/(\d+)\s*(mb|gb|kb)/i);
267
+ if (!match) return '10MB';
268
+
269
+ const size = parseInt(match[1]);
270
+ const unit = match[2].toLowerCase();
271
+
272
+ return `${size}${unit.toUpperCase()}`;
273
+ }
274
+
275
+ /**
276
+ * Extract allowed file types from spec
277
+ */
278
+ function extractAllowedFileTypes(specContent) {
279
+ const match = specContent.match(/\.(jpg|jpeg|png|pdf|doc|docx|xls|xlsx|csv|txt|zip)(?:\s*,\s*\.|\s+)/gi);
280
+ if (!match) return ['.jpg', '.jpeg', '.png'];
281
+
282
+ return match.map(m => m.trim().replace(/\s*,\s*$/, ''));
283
+ }
284
+
285
+ /**
286
+ * Calculate confidence score
287
+ */
288
+ function calculateConfidence(entities, crudOperations, uiKeywords) {
289
+ let score = 0;
290
+
291
+ // Entities detected
292
+ if (entities.length > 0) score += 30;
293
+ if (entities.length > 2) score += 10;
294
+
295
+ // CRUD operations
296
+ const crudCount = Object.values(crudOperations).filter(v => v).length;
297
+ score += crudCount * 10;
298
+
299
+ // UI keywords
300
+ if (uiKeywords.hasUIKeywords) score += 20;
301
+ if (uiKeywords.wizard) score += 15;
302
+ if (uiKeywords.dashboard) score += 15;
303
+
304
+ return Math.min(100, score);
305
+ }
306
+
307
+ /**
308
+ * Generate UI phase trigger message
309
+ */
310
+ export function generateUITriggerMessage(analysis) {
311
+ if (!analysis.shouldTrigger) {
312
+ return null;
313
+ }
314
+
315
+ const { entities, crudOperations, uiNeeds, confidence } = analysis;
316
+
317
+ const parts = [];
318
+
319
+ parts.push(`🎨 **UI/UX Auto-Detection** (${confidence}% confidence)`);
320
+ parts.push('');
321
+
322
+ if (entities.length > 0) {
323
+ parts.push(`**Entities Detected:** ${entities.map(e => e.name).join(', ')}`);
324
+ }
325
+
326
+ if (Object.values(crudOperations).some(v => v)) {
327
+ const ops = Object.entries(crudOperations)
328
+ .filter(([k, v]) => v)
329
+ .map(([k]) => k.toUpperCase());
330
+ parts.push(`**CRUD Operations:** ${ops.join(', ')}`);
331
+ }
332
+
333
+ if (uiNeeds.length > 0) {
334
+ parts.push('');
335
+ parts.push('**UI Needs Detected:**');
336
+ uiNeeds.forEach((need, i) => {
337
+ const icon = need.type === 'form' ? '📝' :
338
+ need.type === 'list' ? '📋' :
339
+ need.type === 'wizard' ? '🧙' :
340
+ need.type === 'dashboard' ? '📊' : '🔧';
341
+ parts.push(`${i + 1}. ${icon} ${need.type.toUpperCase()}: ${need.entity || need.operation || 'Generic'}`);
342
+ });
343
+ }
344
+
345
+ parts.push('');
346
+ parts.push('**➡️ Auto-triggering FASE 1.5: UI/UX Design**');
347
+ parts.push('Generating wireframes, component specs, and design system...');
348
+
349
+ return parts.join('\n');
350
+ }