@claudetools/tools 0.9.0 → 0.9.2

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 (85) hide show
  1. package/dist/cli.js +9 -1
  2. package/dist/codedna/__tests__/examples/mongoose-example.d.ts +6 -0
  3. package/dist/codedna/__tests__/examples/mongoose-example.js +163 -0
  4. package/dist/codedna/__tests__/fixtures/typeorm-production-test.d.ts +1 -0
  5. package/dist/codedna/__tests__/fixtures/typeorm-production-test.js +231 -0
  6. package/dist/codedna/__tests__/fixtures/typeorm-test.d.ts +1 -0
  7. package/dist/codedna/__tests__/fixtures/typeorm-test.js +124 -0
  8. package/dist/codedna/__tests__/laravel-output-review.d.ts +1 -0
  9. package/dist/codedna/__tests__/laravel-output-review.js +249 -0
  10. package/dist/codedna/__tests__/mongoose-output-test.d.ts +1 -0
  11. package/dist/codedna/__tests__/mongoose-output-test.js +178 -0
  12. package/dist/codedna/examples/radix-example.d.ts +2 -0
  13. package/dist/codedna/examples/radix-example.js +259 -0
  14. package/dist/codedna/index.d.ts +5 -3
  15. package/dist/codedna/index.js +6 -3
  16. package/dist/codedna/kappa-ast.d.ts +143 -5
  17. package/dist/codedna/kappa-drizzle-generator.js +8 -5
  18. package/dist/codedna/kappa-gofiber-generator.d.ts +65 -0
  19. package/dist/codedna/kappa-gofiber-generator.js +587 -0
  20. package/dist/codedna/kappa-laravel-generator.d.ts +68 -0
  21. package/dist/codedna/kappa-laravel-generator.js +741 -0
  22. package/dist/codedna/kappa-lexer.d.ts +44 -0
  23. package/dist/codedna/kappa-lexer.js +124 -0
  24. package/dist/codedna/kappa-mantine-generator.d.ts +65 -0
  25. package/dist/codedna/kappa-mantine-generator.js +518 -0
  26. package/dist/codedna/kappa-mongoose-generator.d.ts +44 -0
  27. package/dist/codedna/kappa-mongoose-generator.js +442 -0
  28. package/dist/codedna/kappa-parser.d.ts +43 -1
  29. package/dist/codedna/kappa-parser.js +601 -0
  30. package/dist/codedna/kappa-radix-generator.d.ts +61 -0
  31. package/dist/codedna/kappa-radix-generator.js +566 -0
  32. package/dist/codedna/kappa-typeorm-generator.d.ts +59 -0
  33. package/dist/codedna/kappa-typeorm-generator.js +723 -0
  34. package/dist/codedna/kappa-vitest-generator.d.ts +85 -0
  35. package/dist/codedna/kappa-vitest-generator.js +739 -0
  36. package/dist/codedna/parser.js +26 -1
  37. package/dist/codegen/cloud-client.d.ts +160 -0
  38. package/dist/codegen/cloud-client.js +195 -0
  39. package/dist/codegen/codegen-tool.d.ts +35 -0
  40. package/dist/codegen/codegen-tool.js +312 -0
  41. package/dist/codegen/field-inference.d.ts +24 -0
  42. package/dist/codegen/field-inference.js +101 -0
  43. package/dist/codegen/form-parser.d.ts +13 -0
  44. package/dist/codegen/form-parser.js +186 -0
  45. package/dist/codegen/index.d.ts +2 -0
  46. package/dist/codegen/index.js +4 -0
  47. package/dist/codegen/natural-parser.d.ts +50 -0
  48. package/dist/codegen/natural-parser.js +769 -0
  49. package/dist/handlers/codedna-handlers.d.ts +1 -1
  50. package/dist/handlers/codegen-handlers.d.ts +20 -0
  51. package/dist/handlers/codegen-handlers.js +60 -0
  52. package/dist/handlers/kappa-handlers.d.ts +97 -0
  53. package/dist/handlers/kappa-handlers.js +408 -0
  54. package/dist/handlers/tool-handlers.js +124 -221
  55. package/dist/helpers/api-client.js +48 -3
  56. package/dist/helpers/compact-formatter.d.ts +9 -2
  57. package/dist/helpers/compact-formatter.js +26 -2
  58. package/dist/helpers/config.d.ts +7 -2
  59. package/dist/helpers/config.js +25 -10
  60. package/dist/helpers/session-validation.d.ts +1 -1
  61. package/dist/helpers/session-validation.js +2 -4
  62. package/dist/helpers/tasks.d.ts +21 -0
  63. package/dist/helpers/tasks.js +52 -0
  64. package/dist/helpers/workers.d.ts +1 -1
  65. package/dist/helpers/workers.js +19 -19
  66. package/dist/setup.d.ts +1 -0
  67. package/dist/setup.js +228 -3
  68. package/dist/templates/claude-md.d.ts +1 -1
  69. package/dist/templates/claude-md.js +37 -152
  70. package/dist/templates/orchestrator-prompt.d.ts +2 -2
  71. package/dist/templates/orchestrator-prompt.js +31 -38
  72. package/dist/templates/self-critique.d.ts +50 -0
  73. package/dist/templates/self-critique.js +209 -0
  74. package/dist/templates/worker-prompt.d.ts +3 -3
  75. package/dist/templates/worker-prompt.js +18 -18
  76. package/dist/tools.js +77 -413
  77. package/docs/codedna/generator-testing-summary.md +205 -0
  78. package/docs/codedna/radix-ui-generator.md +478 -0
  79. package/docs/kappa-gofiber-generator.md +274 -0
  80. package/docs/kappa-laravel-fixes.md +172 -0
  81. package/docs/kappa-mongoose-generator.md +322 -0
  82. package/docs/kappa-vitest-generator.md +337 -0
  83. package/package.json +1 -1
  84. package/dist/context/deduplication.test.d.ts +0 -6
  85. package/dist/context/deduplication.test.js +0 -84
@@ -0,0 +1,769 @@
1
+ // =============================================================================
2
+ // Natural Language → Kappa AST Parser
3
+ // =============================================================================
4
+ //
5
+ // Converts natural language entity descriptions into Kappa AST.
6
+ // No DSL syntax required - just describe what you want.
7
+ //
8
+ // Examples:
9
+ // "User with email, password, role (admin/user)"
10
+ // "Post with title, content, published, belongs to User as author"
11
+ // "Comment with body, approved, belongs to User, belongs to Post"
12
+ //
13
+ import { inferFieldType, parseInlineEnum, parseExplicitType } from './field-inference.js';
14
+ const PRIMITIVE_TYPES = [
15
+ 'string', 'int', 'float', 'bool', 'email', 'url', 'uuid',
16
+ 'phone', 'slug', 'markdown', 'json', 'timestamp', 'date', 'time', 'duration'
17
+ ];
18
+ /**
19
+ * Parse natural language description into Kappa entities
20
+ */
21
+ export function parseNaturalLanguage(description) {
22
+ const errors = [];
23
+ const entities = [];
24
+ // Split by entity boundaries (look for "Entity with" pattern)
25
+ const entityBlocks = splitIntoEntities(description);
26
+ for (const block of entityBlocks) {
27
+ try {
28
+ const entity = parseEntityBlock(block);
29
+ entities.push(entity);
30
+ }
31
+ catch (e) {
32
+ errors.push(e instanceof Error ? e.message : String(e));
33
+ }
34
+ }
35
+ return {
36
+ success: errors.length === 0 && entities.length > 0,
37
+ entities,
38
+ components: [], // TODO: Will be populated when component parser is added
39
+ errors,
40
+ };
41
+ }
42
+ /**
43
+ * Split description into entity blocks
44
+ */
45
+ function splitIntoEntities(description) {
46
+ // Pattern: EntityName with/has ...
47
+ const pattern = /([A-Z][a-zA-Z]*)\s+(?:with|has|having)\s+/gi;
48
+ const matches = [...description.matchAll(pattern)];
49
+ if (matches.length === 0) {
50
+ // Try simpler pattern: just EntityName followed by fields
51
+ const simpleMatch = description.match(/^([A-Z][a-zA-Z]*)\s*[:\-]?\s*(.+)$/);
52
+ if (simpleMatch) {
53
+ return [description];
54
+ }
55
+ return [];
56
+ }
57
+ const blocks = [];
58
+ for (let i = 0; i < matches.length; i++) {
59
+ const start = matches[i].index;
60
+ const end = i < matches.length - 1 ? matches[i + 1].index : description.length;
61
+ blocks.push(description.slice(start, end).trim());
62
+ }
63
+ return blocks;
64
+ }
65
+ /**
66
+ * Parse a single entity block
67
+ */
68
+ function parseEntityBlock(block) {
69
+ // Extract entity name
70
+ const nameMatch = block.match(/^([A-Z][a-zA-Z]*)/);
71
+ if (!nameMatch) {
72
+ throw new Error(`Could not find entity name in: ${block}`);
73
+ }
74
+ const entityName = nameMatch[1];
75
+ // Extract fields part (after "with" or "has" or ":")
76
+ const fieldsMatch = block.match(/(?:with|has|having|:)\s*(.+)$/i);
77
+ if (!fieldsMatch) {
78
+ throw new Error(`Could not find fields in: ${block}`);
79
+ }
80
+ const fieldsStr = fieldsMatch[1];
81
+ const { fields, relationships } = parseFieldsList(fieldsStr, entityName);
82
+ // Auto-add id if not present
83
+ const hasId = fields.some(f => f.name === 'id');
84
+ if (!hasId) {
85
+ fields.unshift(createField('id', 'uuid', ['primary', 'auto']));
86
+ }
87
+ // Auto-add timestamps if entity has content fields
88
+ const hasContent = fields.some(f => ['content', 'body', 'title', 'headline'].includes(f.name));
89
+ const hasCreatedAt = fields.some(f => f.name === 'createdAt');
90
+ if (hasContent && !hasCreatedAt) {
91
+ fields.push(createField('createdAt', 'timestamp', ['auto']));
92
+ fields.push(createField('updatedAt', 'timestamp', ['auto']));
93
+ }
94
+ return {
95
+ kind: 'EntityBlock',
96
+ name: entityName,
97
+ fields,
98
+ relationships,
99
+ capabilities: [],
100
+ lifecycleHooks: [],
101
+ indexes: [],
102
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
103
+ };
104
+ }
105
+ /**
106
+ * Parse comma-separated fields list
107
+ */
108
+ function parseFieldsList(fieldsStr, entityName) {
109
+ const fields = [];
110
+ const relationships = [];
111
+ // Split by comma, but not inside parentheses
112
+ const parts = smartSplit(fieldsStr, ',');
113
+ for (const part of parts) {
114
+ const trimmed = part.trim();
115
+ if (!trimmed)
116
+ continue;
117
+ // Check for relationship
118
+ const rel = parseRelationship(trimmed);
119
+ if (rel) {
120
+ relationships.push(rel);
121
+ continue;
122
+ }
123
+ // Check for enum notation: "role (admin, user, guest)"
124
+ const enumMatch = parseInlineEnum(trimmed);
125
+ if (enumMatch) {
126
+ fields.push(createEnumField(enumMatch.name, enumMatch.values));
127
+ continue;
128
+ }
129
+ // Check for explicit type: "age:int" or "name:string:optional"
130
+ const explicit = parseExplicitType(trimmed);
131
+ if (explicit) {
132
+ fields.push(createFieldFromExplicit(explicit.name, explicit.type));
133
+ continue;
134
+ }
135
+ // Infer from field name - strip any trailing type-like suffixes that weren't parsed
136
+ const cleanedField = trimmed.replace(/[.,;!?]+$/, '').trim();
137
+ // If there's a colon that wasn't parsed as explicit type, take only the name part
138
+ const fieldName = cleanedField.includes(':')
139
+ ? cleanedField.split(':')[0].replace(/[^a-zA-Z0-9]/g, '')
140
+ : cleanedField.replace(/[^a-zA-Z0-9]/g, '');
141
+ if (fieldName) {
142
+ const inferred = inferFieldType(fieldName);
143
+ fields.push(createField(fieldName, inferred.type, inferred.modifiers));
144
+ }
145
+ }
146
+ return { fields, relationships };
147
+ }
148
+ /**
149
+ * Parse relationship notation
150
+ * Examples: "belongs to User", "has many Posts", "belongs to User as author"
151
+ */
152
+ function parseRelationship(text) {
153
+ // belongs_to: "belongs to Entity (as alias)"
154
+ const belongsMatch = text.match(/^belongs\s+to\s+([A-Z][a-zA-Z]*)(?:\s+as\s+(\w+))?$/i);
155
+ if (belongsMatch) {
156
+ return {
157
+ kind: 'EntityRelationship',
158
+ type: 'belongs_to',
159
+ entity: belongsMatch[1],
160
+ as: belongsMatch[2],
161
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
162
+ };
163
+ }
164
+ // has_many: "has many Entities"
165
+ const hasManyMatch = text.match(/^has\s+many\s+([A-Z][a-zA-Z]*)$/i);
166
+ if (hasManyMatch) {
167
+ return {
168
+ kind: 'EntityRelationship',
169
+ type: 'has_many',
170
+ entity: hasManyMatch[1],
171
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
172
+ };
173
+ }
174
+ // has_one: "has one Profile"
175
+ const hasOneMatch = text.match(/^has\s+one\s+([A-Z][a-zA-Z]*)$/i);
176
+ if (hasOneMatch) {
177
+ return {
178
+ kind: 'EntityRelationship',
179
+ type: 'has_one',
180
+ entity: hasOneMatch[1],
181
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
182
+ };
183
+ }
184
+ return null;
185
+ }
186
+ /**
187
+ * Create a field with primitive type
188
+ */
189
+ function createField(name, typeName, modifiers) {
190
+ // Normalize type name
191
+ const normalizedType = normalizeTypeName(typeName);
192
+ const fieldType = {
193
+ kind: 'primitive',
194
+ type: normalizedType,
195
+ };
196
+ return {
197
+ kind: 'EntityField',
198
+ name,
199
+ type: fieldType,
200
+ modifiers: modifiers,
201
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
202
+ };
203
+ }
204
+ /**
205
+ * Create an enum field
206
+ */
207
+ function createEnumField(name, values) {
208
+ return {
209
+ kind: 'EntityField',
210
+ name,
211
+ type: {
212
+ kind: 'enum',
213
+ values,
214
+ },
215
+ modifiers: [],
216
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
217
+ };
218
+ }
219
+ /**
220
+ * Create field from explicit type specification
221
+ */
222
+ function createFieldFromExplicit(name, typeInfo) {
223
+ const { type, modifiers } = typeInfo;
224
+ // Check if it's an enum
225
+ if (type.startsWith('enum:')) {
226
+ const values = type.slice(5).split(',').map(v => v.trim());
227
+ return createEnumField(name, values);
228
+ }
229
+ return createField(name, type, modifiers);
230
+ }
231
+ /**
232
+ * Normalize type name to Kappa primitive
233
+ */
234
+ function normalizeTypeName(type) {
235
+ const normalized = type.toLowerCase();
236
+ const typeMap = {
237
+ 'integer': 'int',
238
+ 'number': 'int',
239
+ 'decimal': 'float',
240
+ 'double': 'float',
241
+ 'boolean': 'bool',
242
+ 'datetime': 'timestamp',
243
+ 'text': 'markdown',
244
+ 'varchar': 'string',
245
+ 'char': 'string',
246
+ };
247
+ return typeMap[normalized] || normalized;
248
+ }
249
+ /**
250
+ * Smart split that respects parentheses
251
+ */
252
+ function smartSplit(str, delimiter) {
253
+ const result = [];
254
+ let current = '';
255
+ let depth = 0;
256
+ for (const char of str) {
257
+ if (char === '(')
258
+ depth++;
259
+ if (char === ')')
260
+ depth--;
261
+ if (char === delimiter && depth === 0) {
262
+ result.push(current);
263
+ current = '';
264
+ }
265
+ else {
266
+ current += char;
267
+ }
268
+ }
269
+ if (current)
270
+ result.push(current);
271
+ return result;
272
+ }
273
+ // =============================================================================
274
+ // Page Parsing (Natural Language → PageBlock)
275
+ // =============================================================================
276
+ /**
277
+ * Parse natural language page descriptions into PageBlock AST
278
+ *
279
+ * Examples:
280
+ * "Dashboard page at /dashboard"
281
+ * "User profile at /users/:id with auth guard"
282
+ * "Login page at /login for guests only"
283
+ */
284
+ export function parsePageDescription(description) {
285
+ const pages = [];
286
+ // Try to parse as a single page description first
287
+ const page = parsePageBlock(description.trim());
288
+ if (page) {
289
+ pages.push(page);
290
+ }
291
+ return pages;
292
+ }
293
+ function parsePageBlock(block) {
294
+ // Extract page name and route
295
+ // Pattern: "PageName page at /route"
296
+ const routeMatch = block.match(/^([A-Z][a-zA-Z]*)\s+page\s+at\s+(\/[\w/:]*)/i);
297
+ if (!routeMatch) {
298
+ return null;
299
+ }
300
+ const pageName = routeMatch[1];
301
+ const route = routeMatch[2];
302
+ // Extract guard (authenticated, guest_only, admin, etc.)
303
+ let guard;
304
+ if (/with\s+auth(?:\s+guard)?|authenticated|requires?\s+auth/i.test(block)) {
305
+ guard = 'authenticated';
306
+ }
307
+ else if (/for\s+guests?(?:\s+only)?|guest[_\s]only/i.test(block)) {
308
+ guard = 'guest_only';
309
+ }
310
+ else if (/admin\s+only|requires?\s+admin/i.test(block)) {
311
+ guard = 'admin';
312
+ }
313
+ // Extract title
314
+ const titleMatch = block.match(/title[:\s]+["']([^"']+)["']/i);
315
+ const title = titleMatch ? titleMatch[1] : pageName;
316
+ // Extract loaders from "loads Entity data" or "loads Entity"
317
+ const loaders = [];
318
+ const loadsPattern = /loads?\s+([A-Z][a-zA-Z]*)\s*(?:data)?/gi;
319
+ const loadsMatches = [...block.matchAll(loadsPattern)];
320
+ for (const match of loadsMatches) {
321
+ const entityName = match[1];
322
+ const loaderName = entityName.toLowerCase();
323
+ // Determine source based on route parameters
324
+ let source;
325
+ if (route.includes(':id')) {
326
+ source = `api.get_${loaderName}($params.id)`;
327
+ }
328
+ else if (route.includes(`:${loaderName}Id`)) {
329
+ source = `api.get_${loaderName}($params.${loaderName}Id)`;
330
+ }
331
+ else {
332
+ // Default to list
333
+ source = `api.list_${loaderName}s`;
334
+ }
335
+ loaders.push({
336
+ kind: 'PageLoader',
337
+ name: loaderName,
338
+ source,
339
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
340
+ });
341
+ }
342
+ const actions = [];
343
+ return {
344
+ kind: 'PageBlock',
345
+ name: pageName,
346
+ route,
347
+ guard,
348
+ loaders,
349
+ actions,
350
+ title,
351
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
352
+ };
353
+ }
354
+ /**
355
+ * UI component keywords for detection
356
+ */
357
+ const UI_COMPONENT_KEYWORDS = [
358
+ 'button', 'input', 'card', 'modal', 'dialog', 'form', 'table',
359
+ 'dropdown', 'select', 'checkbox', 'radio', 'switch', 'slider',
360
+ 'tabs', 'accordion', 'tooltip', 'popover', 'menu', 'navbar',
361
+ 'sidebar', 'footer', 'header', 'badge', 'chip', 'avatar',
362
+ 'alert', 'notification', 'toast', 'skeleton', 'spinner', 'loader',
363
+ 'progress', 'pagination', 'breadcrumb', 'stepper', 'calendar',
364
+ 'datepicker', 'timepicker', 'textarea', 'search', 'combobox'
365
+ ];
366
+ /**
367
+ * Detect if a name represents a UI component
368
+ */
369
+ function isUIComponent(name) {
370
+ const lowerName = name.toLowerCase();
371
+ return UI_COMPONENT_KEYWORDS.some(keyword => lowerName.includes(keyword));
372
+ }
373
+ /**
374
+ * Parse component description into ComponentBlock
375
+ *
376
+ * Examples:
377
+ * "Button with label, onClick, variant (primary/secondary)"
378
+ * "Card with title, content, footer, variant (default/outlined/elevated)"
379
+ * "Input with value, onChange, placeholder, disabled"
380
+ */
381
+ export function parseComponentDescription(description) {
382
+ const errors = [];
383
+ const components = [];
384
+ // Split by component boundaries
385
+ const componentBlocks = splitIntoComponents(description);
386
+ for (const block of componentBlocks) {
387
+ try {
388
+ const component = parseComponentBlock(block);
389
+ components.push(component);
390
+ }
391
+ catch (e) {
392
+ errors.push(e instanceof Error ? e.message : String(e));
393
+ }
394
+ }
395
+ return {
396
+ success: errors.length === 0 && components.length > 0,
397
+ components,
398
+ errors,
399
+ };
400
+ }
401
+ /**
402
+ * Split description into component blocks
403
+ */
404
+ function splitIntoComponents(description) {
405
+ // Pattern: ComponentName with/has ...
406
+ const pattern = /([A-Z][a-zA-Z]*)\s+(?:with|has|having)\s+/gi;
407
+ const matches = [...description.matchAll(pattern)];
408
+ if (matches.length === 0) {
409
+ // Try simpler pattern
410
+ const simpleMatch = description.match(/^([A-Z][a-zA-Z]*)\s*[:\-]?\s*(.+)$/);
411
+ if (simpleMatch) {
412
+ return [description];
413
+ }
414
+ return [];
415
+ }
416
+ const blocks = [];
417
+ for (let i = 0; i < matches.length; i++) {
418
+ const start = matches[i].index;
419
+ const end = i < matches.length - 1 ? matches[i + 1].index : description.length;
420
+ blocks.push(description.slice(start, end).trim());
421
+ }
422
+ return blocks;
423
+ }
424
+ /**
425
+ * Parse a single component block
426
+ */
427
+ function parseComponentBlock(block) {
428
+ // Extract component name
429
+ const nameMatch = block.match(/^([A-Z][a-zA-Z]*)/);
430
+ if (!nameMatch) {
431
+ throw new Error(`Could not find component name in: ${block}`);
432
+ }
433
+ const componentName = nameMatch[1];
434
+ // Verify it's a UI component
435
+ if (!isUIComponent(componentName)) {
436
+ throw new Error(`"${componentName}" does not appear to be a UI component. Use entity parser for data models.`);
437
+ }
438
+ // Extract props part (after "with" or "has" or ":")
439
+ const propsMatch = block.match(/(?:with|has|having|:)\s*(.+)$/i);
440
+ if (!propsMatch) {
441
+ throw new Error(`Could not find props in: ${block}`);
442
+ }
443
+ const propsStr = propsMatch[1];
444
+ const { props, variants } = parseComponentPropsList(propsStr);
445
+ return {
446
+ kind: 'ComponentBlock',
447
+ name: componentName,
448
+ props,
449
+ variants,
450
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
451
+ };
452
+ }
453
+ /**
454
+ * Parse comma-separated props list
455
+ */
456
+ function parseComponentPropsList(propsStr) {
457
+ const props = [];
458
+ const variants = [];
459
+ // Split by comma, but not inside parentheses
460
+ const parts = smartSplit(propsStr, ',');
461
+ for (const part of parts) {
462
+ const trimmed = part.trim();
463
+ if (!trimmed)
464
+ continue;
465
+ // Check for variant notation: "variant (primary/secondary/tertiary)"
466
+ const variantMatch = trimmed.match(/^variant\s*\(([^)]+)\)$/i);
467
+ if (variantMatch) {
468
+ const variantNames = variantMatch[1]
469
+ .split('/')
470
+ .map(v => v.trim())
471
+ .filter(v => v.length > 0);
472
+ for (const variantName of variantNames) {
473
+ variants.push({
474
+ kind: 'ComponentVariant',
475
+ name: variantName,
476
+ modifications: [],
477
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
478
+ });
479
+ }
480
+ continue;
481
+ }
482
+ // Check for enum notation: "size (sm/md/lg)"
483
+ const enumMatch = parseInlineEnum(trimmed);
484
+ if (enumMatch) {
485
+ // For components, enums become union type props
486
+ const unionType = enumMatch.values.map(v => `'${v}'`).join(' | ');
487
+ props.push(createComponentProp(enumMatch.name, unionType, false));
488
+ continue;
489
+ }
490
+ // Check for explicit type: "onClick:function" or "disabled:boolean"
491
+ const explicit = parseExplicitType(trimmed);
492
+ if (explicit) {
493
+ const optional = explicit.type.modifiers.includes('optional');
494
+ const propType = inferComponentPropType(explicit.name, explicit.type.type);
495
+ props.push(createComponentProp(explicit.name, propType, optional));
496
+ continue;
497
+ }
498
+ // Infer from prop name
499
+ const propName = trimmed.replace(/[^a-zA-Z0-9]/g, '');
500
+ if (propName) {
501
+ const propType = inferComponentPropType(propName);
502
+ const optional = isOptionalProp(propName);
503
+ props.push(createComponentProp(propName, propType, optional));
504
+ }
505
+ }
506
+ return { props, variants };
507
+ }
508
+ /**
509
+ * Create a component prop
510
+ */
511
+ function createComponentProp(name, type, optional) {
512
+ return {
513
+ kind: 'ComponentProp',
514
+ name,
515
+ type,
516
+ optional,
517
+ loc: { startLine: 1, startColumn: 1, endLine: 1, endColumn: 1, startOffset: 0, endOffset: 0 },
518
+ };
519
+ }
520
+ /**
521
+ * Infer component prop type from name
522
+ */
523
+ function inferComponentPropType(name, explicitType) {
524
+ if (explicitType) {
525
+ const typeMap = {
526
+ 'string': 'string',
527
+ 'int': 'number',
528
+ 'integer': 'number',
529
+ 'float': 'number',
530
+ 'number': 'number',
531
+ 'bool': 'boolean',
532
+ 'boolean': 'boolean',
533
+ 'function': '() => void',
534
+ 'callback': '() => void',
535
+ };
536
+ return typeMap[explicitType.toLowerCase()] || explicitType;
537
+ }
538
+ const lowerName = name.toLowerCase();
539
+ // Event handlers
540
+ if (lowerName.startsWith('on')) {
541
+ return '() => void';
542
+ }
543
+ // Boolean flags
544
+ if (lowerName.startsWith('is') || lowerName.startsWith('has') || lowerName.startsWith('show') ||
545
+ ['disabled', 'readonly', 'required', 'loading', 'active', 'open', 'checked'].includes(lowerName)) {
546
+ return 'boolean';
547
+ }
548
+ // Children/content
549
+ if (['children', 'content', 'body'].includes(lowerName)) {
550
+ return 'React.ReactNode';
551
+ }
552
+ // Style/className
553
+ if (['classname', 'style'].includes(lowerName)) {
554
+ return lowerName === 'classname' ? 'string' : 'React.CSSProperties';
555
+ }
556
+ // Numeric values
557
+ if (['value', 'count', 'max', 'min', 'step', 'width', 'height'].includes(lowerName)) {
558
+ return 'number';
559
+ }
560
+ // Default to string
561
+ return 'string';
562
+ }
563
+ /**
564
+ * Determine if prop is optional based on common patterns
565
+ */
566
+ function isOptionalProp(name) {
567
+ const lowerName = name.toLowerCase();
568
+ // Required props
569
+ const requiredProps = ['children', 'value', 'onchange', 'onclick', 'label', 'title'];
570
+ if (requiredProps.includes(lowerName)) {
571
+ return false;
572
+ }
573
+ // Optional props
574
+ const optionalProps = ['placeholder', 'disabled', 'readonly', 'className', 'style',
575
+ 'variant', 'size', 'icon', 'tooltip', 'ariaLabel'];
576
+ if (optionalProps.includes(lowerName)) {
577
+ return true;
578
+ }
579
+ // Event handlers are usually optional except onChange/onClick
580
+ if (lowerName.startsWith('on') && !['onchange', 'onclick'].includes(lowerName)) {
581
+ return true;
582
+ }
583
+ // Default to optional
584
+ return true;
585
+ }
586
+ // =============================================================================
587
+ // Form Parsing (Natural Language → FormBlock)
588
+ // =============================================================================
589
+ /**
590
+ * Parse natural language form description into FormBlock
591
+ *
592
+ * Examples:
593
+ * "Login form with email, password, submit to /auth/login"
594
+ * "Create Project form with name, description, submit to /api/projects"
595
+ * "Contact form with name, email, message:textarea, submit to /contact"
596
+ */
597
+ export function parseFormDescription(description) {
598
+ // Check if this is a form description
599
+ const formPattern = /^([A-Z][a-zA-Z\s]*?)\s+form\s+with\s+(.+?)(?:,\s*submit\s+to\s+([^\s,]+))?\s*$/i;
600
+ const match = description.match(formPattern);
601
+ if (!match) {
602
+ return null;
603
+ }
604
+ const [, formName, fieldsStr, submitEndpoint] = match;
605
+ // Parse fields
606
+ const fieldParts = smartSplit(fieldsStr, ',').map(s => s.trim()).filter(Boolean);
607
+ const fields = [];
608
+ for (const fieldPart of fieldParts) {
609
+ // Skip if this is the submit directive
610
+ if (fieldPart.toLowerCase().startsWith('submit to')) {
611
+ continue;
612
+ }
613
+ const field = parseFormField(fieldPart);
614
+ if (field) {
615
+ fields.push(field);
616
+ }
617
+ }
618
+ // Create submit action
619
+ const submit = {
620
+ kind: 'FormSubmit',
621
+ action: submitEndpoint || '/submit',
622
+ button: 'Submit',
623
+ onSuccess: 'redirect',
624
+ onError: 'show_error',
625
+ loc: createSourceLocation(),
626
+ };
627
+ // Create simple single-column layout
628
+ const layout = fields.map(field => ({
629
+ kind: 'FormLayoutRow',
630
+ fields: [field.name],
631
+ columns: 1,
632
+ loc: createSourceLocation(),
633
+ }));
634
+ return {
635
+ kind: 'FormBlock',
636
+ name: formName.trim().replace(/\s+/g, ''),
637
+ fields,
638
+ submit,
639
+ layout,
640
+ loc: createSourceLocation(),
641
+ };
642
+ }
643
+ /**
644
+ * Parse a single form field from natural language
645
+ * Examples:
646
+ * "email" -> email field with email validation
647
+ * "password" -> password field
648
+ * "message:textarea" -> textarea field
649
+ * "role:select(admin,user)" -> select field with options
650
+ */
651
+ function parseFormField(fieldStr) {
652
+ // Check for explicit type: "fieldname:type"
653
+ const typeMatch = fieldStr.match(/^(\w+):(\w+)(?:\(([^)]+)\))?$/);
654
+ let name;
655
+ let type;
656
+ let options;
657
+ if (typeMatch) {
658
+ name = typeMatch[1];
659
+ const explicitType = typeMatch[2].toLowerCase();
660
+ const optionsStr = typeMatch[3];
661
+ // Map explicit types to form field types
662
+ type = mapToFormFieldType(explicitType);
663
+ // Parse options if present
664
+ if (optionsStr) {
665
+ options = optionsStr.split(',').map(s => s.trim());
666
+ }
667
+ }
668
+ else {
669
+ // Infer from field name
670
+ name = fieldStr.trim();
671
+ type = inferFormFieldType(name);
672
+ }
673
+ const label = capitalizeWords(name);
674
+ const required = !name.toLowerCase().includes('optional');
675
+ const field = {
676
+ kind: 'FormField',
677
+ name,
678
+ type,
679
+ label,
680
+ required,
681
+ loc: createSourceLocation(),
682
+ };
683
+ // Add options for select/multiselect
684
+ if (options && (type === 'select' || type === 'multiselect' || type === 'radio')) {
685
+ field.options = options.map(value => ({
686
+ kind: 'FormFieldOption',
687
+ value,
688
+ label: capitalizeWords(value),
689
+ loc: createSourceLocation(),
690
+ }));
691
+ }
692
+ // Add placeholder for text fields
693
+ if (['text', 'email', 'password', 'textarea'].includes(type)) {
694
+ field.placeholder = 'Enter ' + label.toLowerCase();
695
+ }
696
+ return field;
697
+ }
698
+ /**
699
+ * Map explicit type string to FormFieldType
700
+ */
701
+ function mapToFormFieldType(typeStr) {
702
+ const typeMap = {
703
+ 'text': 'text',
704
+ 'textarea': 'textarea',
705
+ 'email': 'email',
706
+ 'password': 'password',
707
+ 'number': 'number',
708
+ 'select': 'select',
709
+ 'multiselect': 'multiselect',
710
+ 'checkbox': 'checkbox',
711
+ 'radio': 'radio',
712
+ 'date': 'date',
713
+ 'time': 'time',
714
+ 'datetime': 'datetime',
715
+ 'file': 'file',
716
+ };
717
+ return typeMap[typeStr] || 'text';
718
+ }
719
+ /**
720
+ * Infer form field type from field name
721
+ */
722
+ function inferFormFieldType(name) {
723
+ const lowerName = name.toLowerCase();
724
+ if (lowerName.includes('email'))
725
+ return 'email';
726
+ if (lowerName.includes('password'))
727
+ return 'password';
728
+ if (lowerName.includes('phone') || lowerName.includes('tel'))
729
+ return 'text';
730
+ if (lowerName.includes('message') || lowerName.includes('description') || lowerName.includes('bio'))
731
+ return 'textarea';
732
+ if (lowerName.includes('date') && lowerName.includes('time'))
733
+ return 'datetime';
734
+ if (lowerName.includes('date'))
735
+ return 'date';
736
+ if (lowerName.includes('time'))
737
+ return 'time';
738
+ if (lowerName.includes('age') || lowerName.includes('count') || lowerName.includes('number'))
739
+ return 'number';
740
+ if (lowerName.includes('agree') || lowerName.includes('accept') || lowerName.includes('confirm'))
741
+ return 'checkbox';
742
+ if (lowerName.includes('file') || lowerName.includes('upload') || lowerName.includes('attachment'))
743
+ return 'file';
744
+ return 'text';
745
+ }
746
+ /**
747
+ * Capitalize words in a string
748
+ */
749
+ function capitalizeWords(str) {
750
+ return str
751
+ .replace(/([A-Z])/g, ' $1') // Add space before caps
752
+ .trim()
753
+ .split(/\s+/)
754
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
755
+ .join(' ');
756
+ }
757
+ /**
758
+ * Create a default source location
759
+ */
760
+ function createSourceLocation() {
761
+ return {
762
+ startLine: 1,
763
+ startColumn: 1,
764
+ endLine: 1,
765
+ endColumn: 1,
766
+ startOffset: 0,
767
+ endOffset: 0,
768
+ };
769
+ }