@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.
- package/CLAUDE.md +389 -40
- package/bin/morph-spec.js +121 -0
- package/bin/task-manager.js +368 -0
- package/bin/validate-agents-skills.js +17 -5
- package/bin/validate.js +268 -0
- package/content/.claude/skills/specialists/ef-modeler.md +11 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +10 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +40 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +18 -0
- package/content/.morph/examples/state-v3.json +188 -0
- package/detectors/structure-detector.js +32 -3
- package/package.json +1 -1
- package/src/commands/create-story.js +68 -0
- package/src/commands/init.js +59 -5
- package/src/commands/state.js +1 -1
- package/src/commands/task.js +75 -0
- package/src/lib/continuous-validator.js +440 -0
- package/src/lib/learning-system.js +520 -0
- package/src/lib/mockup-generator.js +366 -0
- package/src/lib/ui-detector.js +350 -0
- package/src/lib/validators/architecture-validator.js +387 -0
- package/src/lib/validators/package-validator.js +360 -0
- package/src/lib/validators/ui-contrast-validator.js +422 -0
- package/src/utils/file-copier.js +26 -0
|
@@ -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
|
+
}
|