@tpitre/story-ui 3.6.2 → 3.8.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/README.md +36 -32
- package/dist/cli/index.js +0 -5
- package/dist/cli/setup.js +1 -1
- package/dist/mcp-server/routes/generateStory.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStory.js +178 -87
- package/dist/mcp-server/routes/generateStoryStream.d.ts.map +1 -1
- package/dist/mcp-server/routes/generateStoryStream.js +149 -31
- package/dist/story-generator/dynamicPackageDiscovery.d.ts +35 -2
- package/dist/story-generator/dynamicPackageDiscovery.d.ts.map +1 -1
- package/dist/story-generator/dynamicPackageDiscovery.js +332 -6
- package/dist/story-generator/enhancedComponentDiscovery.d.ts.map +1 -1
- package/dist/story-generator/enhancedComponentDiscovery.js +149 -2
- package/dist/story-generator/framework-adapters/base-adapter.d.ts +1 -0
- package/dist/story-generator/framework-adapters/base-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/base-adapter.js +12 -2
- package/dist/story-generator/framework-adapters/react-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/react-adapter.js +2 -0
- package/dist/story-generator/framework-adapters/svelte-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/svelte-adapter.js +53 -7
- package/dist/story-generator/framework-adapters/vue-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/vue-adapter.js +21 -1
- package/dist/story-generator/framework-adapters/web-components-adapter.d.ts.map +1 -1
- package/dist/story-generator/framework-adapters/web-components-adapter.js +4 -0
- package/dist/story-generator/llm-providers/openai-provider.js +2 -2
- package/dist/story-generator/promptGenerator.d.ts.map +1 -1
- package/dist/story-generator/promptGenerator.js +179 -26
- package/dist/story-generator/runtimeValidator.d.ts +64 -0
- package/dist/story-generator/runtimeValidator.d.ts.map +1 -0
- package/dist/story-generator/runtimeValidator.js +356 -0
- package/dist/story-generator/selfHealingLoop.d.ts +112 -0
- package/dist/story-generator/selfHealingLoop.d.ts.map +1 -0
- package/dist/story-generator/selfHealingLoop.js +202 -0
- package/dist/story-generator/validateStory.d.ts.map +1 -1
- package/dist/story-generator/validateStory.js +81 -12
- package/dist/story-ui.config.d.ts +2 -0
- package/dist/story-ui.config.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.d.ts +0 -5
- package/dist/templates/StoryUI/StoryUIPanel.d.ts.map +1 -1
- package/dist/templates/StoryUI/StoryUIPanel.js +411 -223
- package/package.json +4 -4
- package/templates/StoryUI/StoryUIPanel.mdx +84 -0
- package/templates/StoryUI/StoryUIPanel.tsx +493 -265
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts +0 -18
- package/dist/templates/StoryUI/StoryUIPanel.stories.d.ts.map +0 -1
- package/dist/templates/StoryUI/StoryUIPanel.stories.js +0 -37
- package/templates/StoryUI/StoryUIPanel.stories.tsx +0 -44
- package/templates/StoryUI/manager.tsx +0 -859
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Validator for Story UI
|
|
3
|
+
*
|
|
4
|
+
* Validates that generated stories actually load and render in Storybook
|
|
5
|
+
* by making HTTP requests to the running Storybook instance after HMR processes
|
|
6
|
+
* the new story file.
|
|
7
|
+
*
|
|
8
|
+
* This catches runtime errors that static validation cannot detect, such as:
|
|
9
|
+
* - "importers[path] is not a function" - Storybook CSF loader errors
|
|
10
|
+
* - Module resolution failures
|
|
11
|
+
* - Runtime component errors
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from './logger.js';
|
|
14
|
+
// Known Storybook runtime error patterns
|
|
15
|
+
const RUNTIME_ERROR_PATTERNS = [
|
|
16
|
+
{ pattern: /importers\[.*?\] is not a function/i, type: 'module_error', description: 'CSF module loader error' },
|
|
17
|
+
{ pattern: /Cannot read propert.*of undefined/i, type: 'render_error', description: 'Component render error' },
|
|
18
|
+
{ pattern: /is not defined/i, type: 'render_error', description: 'Undefined variable error' },
|
|
19
|
+
{ pattern: /Module not found/i, type: 'module_error', description: 'Module resolution error' },
|
|
20
|
+
{ pattern: /Failed to resolve import/i, type: 'module_error', description: 'Import resolution error' },
|
|
21
|
+
{ pattern: /SyntaxError/i, type: 'module_error', description: 'Runtime syntax error' },
|
|
22
|
+
{ pattern: /Unexpected token/i, type: 'module_error', description: 'Parse error' },
|
|
23
|
+
{ pattern: /ReferenceError/i, type: 'render_error', description: 'Reference error' },
|
|
24
|
+
{ pattern: /TypeError/i, type: 'render_error', description: 'Type error' },
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Get the Storybook URL based on environment configuration
|
|
28
|
+
*/
|
|
29
|
+
export function getStorybookUrl() {
|
|
30
|
+
// Priority 1: Explicit storybookUrl in environment
|
|
31
|
+
if (process.env.STORYBOOK_URL) {
|
|
32
|
+
return process.env.STORYBOOK_URL;
|
|
33
|
+
}
|
|
34
|
+
// Priority 2: Proxy mode - use internal Storybook port
|
|
35
|
+
if (process.env.STORYBOOK_PROXY_ENABLED === 'true') {
|
|
36
|
+
const proxyPort = process.env.STORYBOOK_PROXY_PORT || '6006';
|
|
37
|
+
return `http://localhost:${proxyPort}`;
|
|
38
|
+
}
|
|
39
|
+
// Priority 3: Explicit Storybook port
|
|
40
|
+
if (process.env.STORYBOOK_PORT) {
|
|
41
|
+
return `http://localhost:${process.env.STORYBOOK_PORT}`;
|
|
42
|
+
}
|
|
43
|
+
// Priority 4: Default local Storybook
|
|
44
|
+
return 'http://localhost:6006';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if runtime validation is enabled
|
|
48
|
+
*/
|
|
49
|
+
export function isRuntimeValidationEnabled() {
|
|
50
|
+
// Enabled by default if we can determine a Storybook URL
|
|
51
|
+
// Can be explicitly disabled with STORYBOOK_RUNTIME_VALIDATION=false
|
|
52
|
+
if (process.env.STORYBOOK_RUNTIME_VALIDATION === 'false') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// In proxy mode, always enable since we know Storybook is accessible
|
|
56
|
+
if (process.env.STORYBOOK_PROXY_ENABLED === 'true') {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Otherwise, enable if explicitly set to true
|
|
60
|
+
return process.env.STORYBOOK_RUNTIME_VALIDATION === 'true';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert a story title to the Storybook story ID prefix format
|
|
64
|
+
* e.g., "Simple Card" with prefix "Generated/" -> "generated-simple-card"
|
|
65
|
+
* Note: This returns the prefix only, without the story export name
|
|
66
|
+
*/
|
|
67
|
+
export function titleToStoryIdPrefix(title, storyPrefix = 'Generated/') {
|
|
68
|
+
// Remove prefix and convert to kebab case
|
|
69
|
+
const fullTitle = storyPrefix + title;
|
|
70
|
+
const kebabTitle = fullTitle
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
73
|
+
.replace(/^-|-$/g, '');
|
|
74
|
+
return kebabTitle;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Extract the actual title from generated story content
|
|
78
|
+
* Looks for: title: 'Generated/Something' or title: "Generated/Something"
|
|
79
|
+
*/
|
|
80
|
+
export function extractTitleFromStory(storyContent) {
|
|
81
|
+
const titleMatch = storyContent.match(/title:\s*['"]([^'"]+)['"]/);
|
|
82
|
+
if (titleMatch) {
|
|
83
|
+
// Return the full title (including prefix like "Generated/")
|
|
84
|
+
return titleMatch[1];
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Convert a full story title (like "Generated/Button Click Counter") to story ID prefix
|
|
90
|
+
*/
|
|
91
|
+
export function fullTitleToStoryIdPrefix(fullTitle) {
|
|
92
|
+
return fullTitle
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
95
|
+
.replace(/^-|-$/g, '');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sleep utility
|
|
99
|
+
*/
|
|
100
|
+
function sleep(ms) {
|
|
101
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Fetch with timeout
|
|
105
|
+
*/
|
|
106
|
+
async function fetchWithTimeout(url, timeoutMs) {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
111
|
+
clearTimeout(timeoutId);
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
if (error.name === 'AbortError') {
|
|
117
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if stories exist in Storybook's index that match the given title prefix
|
|
124
|
+
* Returns the first matching story ID for iframe validation
|
|
125
|
+
*/
|
|
126
|
+
async function checkStoryInIndex(storyIdPrefix, storybookUrl, config) {
|
|
127
|
+
const indexUrl = `${storybookUrl}/index.json`;
|
|
128
|
+
const timeout = config.fetchTimeoutMs || 5000;
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetchWithTimeout(indexUrl, timeout);
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
return { exists: false, error: `Index returned ${response.status}` };
|
|
133
|
+
}
|
|
134
|
+
const index = await response.json();
|
|
135
|
+
// Storybook 7+ uses 'entries', older versions use 'stories'
|
|
136
|
+
const stories = index.entries || index.stories || {};
|
|
137
|
+
// Find story IDs that match our prefix (not docs entries)
|
|
138
|
+
const matchingIds = Object.keys(stories).filter(id => {
|
|
139
|
+
// Skip docs entries - we want actual story entries
|
|
140
|
+
if (id.endsWith('--docs'))
|
|
141
|
+
return false;
|
|
142
|
+
// Check if the ID starts with our prefix
|
|
143
|
+
return id.startsWith(storyIdPrefix + '--');
|
|
144
|
+
});
|
|
145
|
+
if (matchingIds.length > 0) {
|
|
146
|
+
return { exists: true, matchingStoryId: matchingIds[0] };
|
|
147
|
+
}
|
|
148
|
+
return { exists: false };
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return { exists: false, error: error.message };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Check the story iframe for runtime errors
|
|
156
|
+
*/
|
|
157
|
+
async function checkStoryIframe(storyId, storybookUrl, config) {
|
|
158
|
+
const iframeUrl = `${storybookUrl}/iframe.html?id=${storyId}&viewMode=story`;
|
|
159
|
+
const timeout = config.fetchTimeoutMs || 5000;
|
|
160
|
+
try {
|
|
161
|
+
const response = await fetchWithTimeout(iframeUrl, timeout);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error: `Story iframe returned ${response.status}`,
|
|
166
|
+
errorType: 'not_found'
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const html = await response.text();
|
|
170
|
+
// Check for known error patterns in the HTML response
|
|
171
|
+
for (const { pattern, type, description } of RUNTIME_ERROR_PATTERNS) {
|
|
172
|
+
if (pattern.test(html)) {
|
|
173
|
+
// Extract the actual error message if possible
|
|
174
|
+
const match = html.match(/<pre[^>]*>([\s\S]*?)<\/pre>/i) ||
|
|
175
|
+
html.match(/Error:?\s*([^\n<]+)/i);
|
|
176
|
+
const errorDetail = match ? match[1].trim().substring(0, 200) : description;
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: errorDetail,
|
|
180
|
+
errorType: type
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Check for Storybook error boundary markers
|
|
185
|
+
// Note: We need to check for VISIBLE errors, not just the error display template
|
|
186
|
+
// The 'sb-show-errordisplay' class is added to body when an error is actually shown
|
|
187
|
+
// IMPORTANT: Must use regex to check for class attribute, not just includes()
|
|
188
|
+
// because ':not(.sb-show-errordisplay)' exists in CSS selectors
|
|
189
|
+
const hasVisibleError = /class="[^"]*sb-show-errordisplay[^"]*"/i.test(html);
|
|
190
|
+
// Check for actual error content in the error display elements (non-empty)
|
|
191
|
+
const hasErrorContent = /<h1[^>]*id="error-message"[^>]*>[^<]+<\/h1>/i.test(html) ||
|
|
192
|
+
/<code[^>]*id="error-stack"[^>]*>[^<]+<\/code>/i.test(html);
|
|
193
|
+
// Check for specific error text (not in CSS context)
|
|
194
|
+
const hasDocsError = />\s*DocsRenderer error/i.test(html);
|
|
195
|
+
const hasStoryError = /class="[^"]*story-error[^"]*"/i.test(html);
|
|
196
|
+
if (hasVisibleError || hasErrorContent || hasDocsError || hasStoryError) {
|
|
197
|
+
// Try to extract the actual error message
|
|
198
|
+
const errorMsgMatch = html.match(/<h1[^>]*id="error-message"[^>]*>([^<]+)<\/h1>/i);
|
|
199
|
+
const errorDetail = errorMsgMatch ? errorMsgMatch[1].trim() : 'Storybook error boundary triggered';
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: errorDetail,
|
|
203
|
+
errorType: 'render_error'
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { success: true };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (error.message.includes('timed out')) {
|
|
210
|
+
return { success: false, error: error.message, errorType: 'timeout' };
|
|
211
|
+
}
|
|
212
|
+
return { success: false, error: error.message, errorType: 'connection_error' };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validate that a story loads and renders correctly in Storybook
|
|
217
|
+
*
|
|
218
|
+
* @param storyContent - The generated story content (used to extract the actual title)
|
|
219
|
+
* @param fallbackTitle - Fallback title if extraction fails (e.g., "Simple Card")
|
|
220
|
+
* @param storyPrefix - The story prefix from config (e.g., "Generated/")
|
|
221
|
+
* @param customConfig - Runtime validator configuration
|
|
222
|
+
* @returns Validation result with success status and any errors
|
|
223
|
+
*/
|
|
224
|
+
export async function validateStoryRuntime(storyContent, fallbackTitle, storyPrefix = 'Generated/', customConfig) {
|
|
225
|
+
// Check if runtime validation is enabled
|
|
226
|
+
if (!isRuntimeValidationEnabled()) {
|
|
227
|
+
logger.debug('Runtime validation disabled, skipping');
|
|
228
|
+
return { success: true, storyExists: true };
|
|
229
|
+
}
|
|
230
|
+
const storybookUrl = getStorybookUrl();
|
|
231
|
+
if (!storybookUrl) {
|
|
232
|
+
logger.warn('Could not determine Storybook URL for runtime validation');
|
|
233
|
+
return { success: true, storyExists: true, details: 'Storybook URL not configured' };
|
|
234
|
+
}
|
|
235
|
+
const config = {
|
|
236
|
+
storybookUrl,
|
|
237
|
+
hmrWaitMs: 3000,
|
|
238
|
+
fetchTimeoutMs: 5000,
|
|
239
|
+
retryAttempts: 3,
|
|
240
|
+
retryDelayMs: 1000,
|
|
241
|
+
...customConfig
|
|
242
|
+
};
|
|
243
|
+
// Extract the actual title from the story content, or use fallback
|
|
244
|
+
const extractedTitle = extractTitleFromStory(storyContent);
|
|
245
|
+
let storyIdPrefix;
|
|
246
|
+
if (extractedTitle) {
|
|
247
|
+
// Use the exact title from the generated code
|
|
248
|
+
storyIdPrefix = fullTitleToStoryIdPrefix(extractedTitle);
|
|
249
|
+
logger.debug(`Extracted title from story: "${extractedTitle}" -> prefix: "${storyIdPrefix}"`);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Fall back to constructing from the provided title
|
|
253
|
+
storyIdPrefix = titleToStoryIdPrefix(fallbackTitle, storyPrefix);
|
|
254
|
+
logger.debug(`Using fallback title: "${fallbackTitle}" -> prefix: "${storyIdPrefix}"`);
|
|
255
|
+
}
|
|
256
|
+
logger.info(`Runtime validation: checking stories with prefix "${storyIdPrefix}" at ${storybookUrl}`);
|
|
257
|
+
// Wait for HMR to process the new file
|
|
258
|
+
logger.debug(`Waiting ${config.hmrWaitMs}ms for HMR to process...`);
|
|
259
|
+
await sleep(config.hmrWaitMs);
|
|
260
|
+
// Step 1: Check if story appears in the index (with retries for HMR timing)
|
|
261
|
+
let matchingStoryId;
|
|
262
|
+
let lastIndexError;
|
|
263
|
+
for (let attempt = 1; attempt <= config.retryAttempts; attempt++) {
|
|
264
|
+
const indexResult = await checkStoryInIndex(storyIdPrefix, storybookUrl, config);
|
|
265
|
+
if (indexResult.exists && indexResult.matchingStoryId) {
|
|
266
|
+
matchingStoryId = indexResult.matchingStoryId;
|
|
267
|
+
logger.debug(`Found matching story: "${matchingStoryId}"`);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
lastIndexError = indexResult.error;
|
|
271
|
+
if (attempt < config.retryAttempts) {
|
|
272
|
+
logger.debug(`Story not found in index (attempt ${attempt}/${config.retryAttempts}), waiting...`);
|
|
273
|
+
await sleep(config.retryDelayMs);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!matchingStoryId) {
|
|
277
|
+
logger.warn(`Stories with prefix "${storyIdPrefix}" not found in Storybook index after ${config.retryAttempts} attempts`);
|
|
278
|
+
return {
|
|
279
|
+
success: false,
|
|
280
|
+
storyExists: false,
|
|
281
|
+
errorType: 'not_found',
|
|
282
|
+
renderError: lastIndexError || 'Story not found in Storybook index - HMR may not have processed the file',
|
|
283
|
+
details: `Story ID prefix: ${storyIdPrefix}`
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Step 2: Load the story iframe and check for runtime errors
|
|
287
|
+
const iframeResult = await checkStoryIframe(matchingStoryId, storybookUrl, config);
|
|
288
|
+
if (!iframeResult.success) {
|
|
289
|
+
logger.error(`Runtime error detected in story "${matchingStoryId}": ${iframeResult.error}`);
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
storyExists: true,
|
|
293
|
+
renderError: iframeResult.error,
|
|
294
|
+
errorType: iframeResult.errorType,
|
|
295
|
+
details: `Story ID: ${matchingStoryId}, URL: ${storybookUrl}/iframe.html?id=${matchingStoryId}`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
logger.info(`Runtime validation passed for story "${matchingStoryId}"`);
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
storyExists: true
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Format runtime validation errors for the self-healing prompt
|
|
306
|
+
*/
|
|
307
|
+
export function formatRuntimeErrorForHealing(result) {
|
|
308
|
+
if (result.success)
|
|
309
|
+
return '';
|
|
310
|
+
const parts = [];
|
|
311
|
+
parts.push(`RUNTIME ERROR: The generated story failed to load in Storybook.`);
|
|
312
|
+
if (result.renderError) {
|
|
313
|
+
parts.push(`Error: ${result.renderError}`);
|
|
314
|
+
}
|
|
315
|
+
if (result.errorType === 'module_error') {
|
|
316
|
+
parts.push(`This is a module/import error. Common causes:`);
|
|
317
|
+
parts.push(`- Invalid CSF (Component Story Format) structure`);
|
|
318
|
+
parts.push(`- Missing or malformed default export (meta)`);
|
|
319
|
+
parts.push(`- Story exports that conflict with Storybook internals`);
|
|
320
|
+
parts.push(`- Invalid import statements`);
|
|
321
|
+
parts.push(`\nEnsure the story follows this exact structure:`);
|
|
322
|
+
parts.push(`\`\`\`tsx`);
|
|
323
|
+
parts.push(`import type { Meta, StoryObj } from '@storybook/react';`);
|
|
324
|
+
parts.push(`import { Component } from '@design-system/core';`);
|
|
325
|
+
parts.push(``);
|
|
326
|
+
parts.push(`const meta: Meta<typeof Component> = {`);
|
|
327
|
+
parts.push(` title: 'Generated/Story Title',`);
|
|
328
|
+
parts.push(` component: Component,`);
|
|
329
|
+
parts.push(`};`);
|
|
330
|
+
parts.push(``);
|
|
331
|
+
parts.push(`export default meta;`);
|
|
332
|
+
parts.push(`type Story = StoryObj<typeof meta>;`);
|
|
333
|
+
parts.push(``);
|
|
334
|
+
parts.push(`export const Default: Story = {`);
|
|
335
|
+
parts.push(` render: () => <Component />,`);
|
|
336
|
+
parts.push(`};`);
|
|
337
|
+
parts.push(`\`\`\``);
|
|
338
|
+
}
|
|
339
|
+
else if (result.errorType === 'render_error') {
|
|
340
|
+
parts.push(`This is a component render error. Common causes:`);
|
|
341
|
+
parts.push(`- Using undefined variables or components`);
|
|
342
|
+
parts.push(`- Invalid props passed to components`);
|
|
343
|
+
parts.push(`- Missing required props`);
|
|
344
|
+
parts.push(`- Incorrect component composition`);
|
|
345
|
+
}
|
|
346
|
+
else if (result.errorType === 'not_found') {
|
|
347
|
+
parts.push(`The story was not found in Storybook's index. This usually means:`);
|
|
348
|
+
parts.push(`- The file has syntax errors that prevent Storybook from parsing it`);
|
|
349
|
+
parts.push(`- The story title/path doesn't match expected format`);
|
|
350
|
+
parts.push(`- The default export is missing or invalid`);
|
|
351
|
+
}
|
|
352
|
+
if (result.details) {
|
|
353
|
+
parts.push(`\nDetails: ${result.details}`);
|
|
354
|
+
}
|
|
355
|
+
return parts.join('\n');
|
|
356
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Healing Loop for Story Generation
|
|
3
|
+
*
|
|
4
|
+
* Provides LLM-assisted error correction when validation fails
|
|
5
|
+
* and auto-fix cannot repair the code.
|
|
6
|
+
*
|
|
7
|
+
* Design-system agnostic - uses discovered components from the user's project.
|
|
8
|
+
*/
|
|
9
|
+
import { ValidationResult } from './validateStory.js';
|
|
10
|
+
import { ValidationError } from './storyValidator.js';
|
|
11
|
+
/**
|
|
12
|
+
* Aggregated validation errors from all validation systems
|
|
13
|
+
*/
|
|
14
|
+
export interface ValidationErrors {
|
|
15
|
+
/** TypeScript AST syntax errors */
|
|
16
|
+
syntaxErrors: string[];
|
|
17
|
+
/** Forbidden pattern violations (e.g., UNSAFE_style) */
|
|
18
|
+
patternErrors: string[];
|
|
19
|
+
/** Invalid component import errors */
|
|
20
|
+
importErrors: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Options for self-healing prompt generation
|
|
24
|
+
*/
|
|
25
|
+
export interface SelfHealingOptions {
|
|
26
|
+
/** Maximum retry attempts */
|
|
27
|
+
maxAttempts: number;
|
|
28
|
+
/** List of available component names from discovery */
|
|
29
|
+
availableComponents: string[];
|
|
30
|
+
/** Framework being used (react, vue, angular, etc.) */
|
|
31
|
+
framework: string;
|
|
32
|
+
/** Import path for the component library */
|
|
33
|
+
importPath: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Result of a self-healing attempt
|
|
37
|
+
*/
|
|
38
|
+
export interface SelfHealingResult {
|
|
39
|
+
/** Whether the code was successfully healed */
|
|
40
|
+
success: boolean;
|
|
41
|
+
/** The final code (healed or best attempt) */
|
|
42
|
+
code: string;
|
|
43
|
+
/** Number of attempts made */
|
|
44
|
+
attempts: number;
|
|
45
|
+
/** History of errors from each attempt */
|
|
46
|
+
errorHistory: ValidationErrors[];
|
|
47
|
+
/** Final remaining errors (if any) */
|
|
48
|
+
finalErrors: ValidationErrors;
|
|
49
|
+
/** Whether self-healing was actually used */
|
|
50
|
+
selfHealingUsed: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Metrics for generation response
|
|
54
|
+
*/
|
|
55
|
+
export interface GenerationMetrics {
|
|
56
|
+
attempts: number;
|
|
57
|
+
selfHealingUsed: boolean;
|
|
58
|
+
validationHistory: Array<{
|
|
59
|
+
attempt: number;
|
|
60
|
+
syntaxErrors: number;
|
|
61
|
+
patternErrors: number;
|
|
62
|
+
importErrors: number;
|
|
63
|
+
autoFixApplied: boolean;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if validation errors object is empty (no errors)
|
|
68
|
+
*/
|
|
69
|
+
export declare function hasNoErrors(errors: ValidationErrors): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Get total error count
|
|
72
|
+
*/
|
|
73
|
+
export declare function getTotalErrorCount(errors: ValidationErrors): number;
|
|
74
|
+
/**
|
|
75
|
+
* Create empty validation errors object
|
|
76
|
+
*/
|
|
77
|
+
export declare function createEmptyErrors(): ValidationErrors;
|
|
78
|
+
/**
|
|
79
|
+
* Aggregate validation errors from different validation systems
|
|
80
|
+
*/
|
|
81
|
+
export declare function aggregateValidationErrors(astResult: ValidationResult | null, patternErrors: ValidationError[] | null, importErrors: string[] | null): ValidationErrors;
|
|
82
|
+
/**
|
|
83
|
+
* Determine if we should continue retrying based on error history
|
|
84
|
+
*/
|
|
85
|
+
export declare function shouldContinueRetrying(attempts: number, maxAttempts: number, errorHistory: ValidationErrors[]): {
|
|
86
|
+
shouldRetry: boolean;
|
|
87
|
+
reason: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Build the self-healing prompt to send to the LLM
|
|
91
|
+
* Design-system agnostic - uses discovered components
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildSelfHealingPrompt(originalCode: string, errors: ValidationErrors, attempt: number, options: SelfHealingOptions): string;
|
|
94
|
+
/**
|
|
95
|
+
* Format errors for logging
|
|
96
|
+
*/
|
|
97
|
+
export declare function formatErrorsForLog(errors: ValidationErrors): string;
|
|
98
|
+
/**
|
|
99
|
+
* Create generation metrics from error history
|
|
100
|
+
*/
|
|
101
|
+
export declare function createGenerationMetrics(attempts: number, errorHistory: ValidationErrors[], autoFixApplied: boolean[]): GenerationMetrics;
|
|
102
|
+
/**
|
|
103
|
+
* Select the best code from multiple attempts based on error count
|
|
104
|
+
*/
|
|
105
|
+
export declare function selectBestAttempt(attempts: Array<{
|
|
106
|
+
code: string;
|
|
107
|
+
errors: ValidationErrors;
|
|
108
|
+
}>): {
|
|
109
|
+
code: string;
|
|
110
|
+
errors: ValidationErrors;
|
|
111
|
+
} | null;
|
|
112
|
+
//# sourceMappingURL=selfHealingLoop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selfHealingLoop.d.ts","sourceRoot":"","sources":["../../story-generator/selfHealingLoop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Healing Loop for Story Generation
|
|
3
|
+
*
|
|
4
|
+
* Provides LLM-assisted error correction when validation fails
|
|
5
|
+
* and auto-fix cannot repair the code.
|
|
6
|
+
*
|
|
7
|
+
* Design-system agnostic - uses discovered components from the user's project.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Check if validation errors object is empty (no errors)
|
|
11
|
+
*/
|
|
12
|
+
export function hasNoErrors(errors) {
|
|
13
|
+
return (errors.syntaxErrors.length === 0 &&
|
|
14
|
+
errors.patternErrors.length === 0 &&
|
|
15
|
+
errors.importErrors.length === 0);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get total error count
|
|
19
|
+
*/
|
|
20
|
+
export function getTotalErrorCount(errors) {
|
|
21
|
+
return (errors.syntaxErrors.length +
|
|
22
|
+
errors.patternErrors.length +
|
|
23
|
+
errors.importErrors.length);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create empty validation errors object
|
|
27
|
+
*/
|
|
28
|
+
export function createEmptyErrors() {
|
|
29
|
+
return {
|
|
30
|
+
syntaxErrors: [],
|
|
31
|
+
patternErrors: [],
|
|
32
|
+
importErrors: [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Aggregate validation errors from different validation systems
|
|
37
|
+
*/
|
|
38
|
+
export function aggregateValidationErrors(astResult, patternErrors, importErrors) {
|
|
39
|
+
const errors = createEmptyErrors();
|
|
40
|
+
// Add AST validation errors
|
|
41
|
+
if (astResult && !astResult.isValid) {
|
|
42
|
+
errors.syntaxErrors = [...astResult.errors];
|
|
43
|
+
}
|
|
44
|
+
// Add pattern validation errors
|
|
45
|
+
if (patternErrors && patternErrors.length > 0) {
|
|
46
|
+
errors.patternErrors = patternErrors.map((e) => `Line ${e.line}: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
// Add import validation errors
|
|
49
|
+
if (importErrors && importErrors.length > 0) {
|
|
50
|
+
errors.importErrors = [...importErrors];
|
|
51
|
+
}
|
|
52
|
+
return errors;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Determine if we should continue retrying based on error history
|
|
56
|
+
*/
|
|
57
|
+
export function shouldContinueRetrying(attempts, maxAttempts, errorHistory) {
|
|
58
|
+
// Don't exceed max attempts
|
|
59
|
+
if (attempts >= maxAttempts) {
|
|
60
|
+
return { shouldRetry: false, reason: 'Maximum retry attempts reached' };
|
|
61
|
+
}
|
|
62
|
+
// If we have at least 2 attempts, check if errors are repeating
|
|
63
|
+
if (errorHistory.length >= 2) {
|
|
64
|
+
const currentErrors = errorHistory[errorHistory.length - 1];
|
|
65
|
+
const previousErrors = errorHistory[errorHistory.length - 2];
|
|
66
|
+
// Convert to sets for comparison
|
|
67
|
+
const currentSet = new Set([
|
|
68
|
+
...currentErrors.syntaxErrors,
|
|
69
|
+
...currentErrors.patternErrors,
|
|
70
|
+
...currentErrors.importErrors,
|
|
71
|
+
]);
|
|
72
|
+
const previousSet = new Set([
|
|
73
|
+
...previousErrors.syntaxErrors,
|
|
74
|
+
...previousErrors.patternErrors,
|
|
75
|
+
...previousErrors.importErrors,
|
|
76
|
+
]);
|
|
77
|
+
// Check if same errors are repeating (LLM is stuck)
|
|
78
|
+
if (currentSet.size === previousSet.size) {
|
|
79
|
+
let allSame = true;
|
|
80
|
+
for (const error of currentSet) {
|
|
81
|
+
if (!previousSet.has(error)) {
|
|
82
|
+
allSame = false;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (allSame && currentSet.size > 0) {
|
|
87
|
+
return {
|
|
88
|
+
shouldRetry: false,
|
|
89
|
+
reason: 'Same errors repeating - LLM appears stuck',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { shouldRetry: true, reason: '' };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the self-healing prompt to send to the LLM
|
|
98
|
+
* Design-system agnostic - uses discovered components
|
|
99
|
+
*/
|
|
100
|
+
export function buildSelfHealingPrompt(originalCode, errors, attempt, options) {
|
|
101
|
+
const sections = [];
|
|
102
|
+
sections.push(`## CODE CORRECTION REQUIRED (Attempt ${attempt} of ${options.maxAttempts})`);
|
|
103
|
+
sections.push('');
|
|
104
|
+
sections.push('Your previous code contained errors. Please fix them while preserving the original intent.');
|
|
105
|
+
sections.push('');
|
|
106
|
+
// Syntax errors section
|
|
107
|
+
if (errors.syntaxErrors.length > 0) {
|
|
108
|
+
sections.push('### TypeScript Syntax Errors');
|
|
109
|
+
sections.push('These prevent the code from compiling:');
|
|
110
|
+
errors.syntaxErrors.forEach((e) => sections.push(`- ${e}`));
|
|
111
|
+
sections.push('');
|
|
112
|
+
}
|
|
113
|
+
// Pattern errors section
|
|
114
|
+
if (errors.patternErrors.length > 0) {
|
|
115
|
+
sections.push('### Forbidden Patterns');
|
|
116
|
+
sections.push('These patterns are not allowed in this codebase:');
|
|
117
|
+
errors.patternErrors.forEach((e) => sections.push(`- ${e}`));
|
|
118
|
+
sections.push('');
|
|
119
|
+
}
|
|
120
|
+
// Import errors section
|
|
121
|
+
if (errors.importErrors.length > 0) {
|
|
122
|
+
sections.push('### Import Errors');
|
|
123
|
+
sections.push(`These components do not exist in "${options.importPath}":`);
|
|
124
|
+
errors.importErrors.forEach((e) => sections.push(`- ${e}`));
|
|
125
|
+
sections.push('');
|
|
126
|
+
// Show available components (design-system agnostic)
|
|
127
|
+
if (options.availableComponents.length > 0) {
|
|
128
|
+
sections.push('**Available components include:**');
|
|
129
|
+
const displayComponents = options.availableComponents.slice(0, 20);
|
|
130
|
+
sections.push(displayComponents.join(', '));
|
|
131
|
+
if (options.availableComponents.length > 20) {
|
|
132
|
+
sections.push(`... and ${options.availableComponents.length - 20} more`);
|
|
133
|
+
}
|
|
134
|
+
sections.push('');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Original code section
|
|
138
|
+
sections.push('### Original Code (with errors)');
|
|
139
|
+
sections.push('```tsx');
|
|
140
|
+
sections.push(originalCode);
|
|
141
|
+
sections.push('```');
|
|
142
|
+
sections.push('');
|
|
143
|
+
// Correction instructions
|
|
144
|
+
sections.push('### Correction Instructions');
|
|
145
|
+
sections.push('1. Fix ALL errors listed above');
|
|
146
|
+
sections.push('2. Keep the same component structure and layout');
|
|
147
|
+
sections.push('3. Do NOT add new features - only fix the errors');
|
|
148
|
+
sections.push('4. Ensure all JSX elements are properly opened and closed');
|
|
149
|
+
sections.push(`5. Only import components that exist in "${options.importPath}"`);
|
|
150
|
+
sections.push('6. Return the COMPLETE corrected code in a ```tsx code block');
|
|
151
|
+
sections.push('7. Do NOT include any explanation - just the corrected code block');
|
|
152
|
+
return sections.join('\n');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Format errors for logging
|
|
156
|
+
*/
|
|
157
|
+
export function formatErrorsForLog(errors) {
|
|
158
|
+
const parts = [];
|
|
159
|
+
if (errors.syntaxErrors.length > 0) {
|
|
160
|
+
parts.push(`Syntax(${errors.syntaxErrors.length})`);
|
|
161
|
+
}
|
|
162
|
+
if (errors.patternErrors.length > 0) {
|
|
163
|
+
parts.push(`Pattern(${errors.patternErrors.length})`);
|
|
164
|
+
}
|
|
165
|
+
if (errors.importErrors.length > 0) {
|
|
166
|
+
parts.push(`Import(${errors.importErrors.length})`);
|
|
167
|
+
}
|
|
168
|
+
return parts.length > 0 ? parts.join(', ') : 'None';
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Create generation metrics from error history
|
|
172
|
+
*/
|
|
173
|
+
export function createGenerationMetrics(attempts, errorHistory, autoFixApplied) {
|
|
174
|
+
return {
|
|
175
|
+
attempts,
|
|
176
|
+
selfHealingUsed: attempts > 1,
|
|
177
|
+
validationHistory: errorHistory.map((errors, index) => ({
|
|
178
|
+
attempt: index + 1,
|
|
179
|
+
syntaxErrors: errors.syntaxErrors.length,
|
|
180
|
+
patternErrors: errors.patternErrors.length,
|
|
181
|
+
importErrors: errors.importErrors.length,
|
|
182
|
+
autoFixApplied: autoFixApplied[index] || false,
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Select the best code from multiple attempts based on error count
|
|
188
|
+
*/
|
|
189
|
+
export function selectBestAttempt(attempts) {
|
|
190
|
+
if (attempts.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
let best = attempts[0];
|
|
193
|
+
let bestErrorCount = getTotalErrorCount(best.errors);
|
|
194
|
+
for (const attempt of attempts) {
|
|
195
|
+
const errorCount = getTotalErrorCount(attempt.errors);
|
|
196
|
+
if (errorCount < bestErrorCount) {
|
|
197
|
+
best = attempt;
|
|
198
|
+
bestErrorCount = errorCount;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return best;
|
|
202
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateStory.d.ts","sourceRoot":"","sources":["../../story-generator/validateStory.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAoB,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"validateStory.d.ts","sourceRoot":"","sources":["../../story-generator/validateStory.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAoB,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,gBAAgB,CA+G9G;AAqcD;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,gBAAgB,CAqC9F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,MAAM,CA+BvE"}
|