aios-core 3.0.0 → 3.2.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,1010 @@
1
+ /**
2
+ * Squad Designer
3
+ *
4
+ * Analyzes documentation and generates squad blueprints
5
+ * with intelligent agent and task recommendations.
6
+ *
7
+ * @module squad-designer
8
+ * @version 1.0.0
9
+ * @see Story SQS-9: Squad Designer
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+
16
+ /**
17
+ * Default output path for blueprints
18
+ * @constant {string}
19
+ */
20
+ const DEFAULT_DESIGNS_PATH = './squads/.designs';
21
+
22
+ /**
23
+ * Minimum confidence threshold for recommendations
24
+ * @constant {number}
25
+ */
26
+ const MIN_CONFIDENCE_THRESHOLD = 0.5;
27
+
28
+ /**
29
+ * Keywords that indicate workflow actions
30
+ * @constant {string[]}
31
+ */
32
+ const ACTION_KEYWORDS = [
33
+ 'create', 'add', 'new', 'generate', 'build',
34
+ 'update', 'edit', 'modify', 'change', 'patch',
35
+ 'delete', 'remove', 'cancel', 'archive',
36
+ 'get', 'fetch', 'retrieve', 'list', 'search', 'find', 'query',
37
+ 'process', 'handle', 'manage', 'execute', 'run',
38
+ 'validate', 'verify', 'check', 'approve', 'reject',
39
+ 'send', 'notify', 'alert', 'email', 'publish',
40
+ 'import', 'export', 'sync', 'migrate', 'transform',
41
+ 'login', 'logout', 'authenticate', 'authorize',
42
+ 'upload', 'download', 'save', 'load',
43
+ ];
44
+
45
+ /**
46
+ * Keywords that indicate integrations
47
+ * @constant {string[]}
48
+ */
49
+ const INTEGRATION_KEYWORDS = [
50
+ 'api', 'rest', 'graphql', 'webhook', 'endpoint',
51
+ 'database', 'db', 'sql', 'nosql', 'redis', 'postgres', 'mysql', 'mongodb',
52
+ 'aws', 'azure', 'gcp', 'cloud', 's3', 'lambda',
53
+ 'stripe', 'paypal', 'payment', 'gateway',
54
+ 'slack', 'discord', 'email', 'sms', 'twilio',
55
+ 'oauth', 'jwt', 'auth0', 'firebase',
56
+ 'github', 'gitlab', 'bitbucket',
57
+ 'docker', 'kubernetes', 'k8s',
58
+ ];
59
+
60
+ /**
61
+ * Keywords that indicate stakeholder roles
62
+ * @constant {string[]}
63
+ */
64
+ const ROLE_KEYWORDS = [
65
+ 'user', 'admin', 'administrator', 'manager', 'owner',
66
+ 'customer', 'client', 'buyer', 'seller', 'vendor',
67
+ 'developer', 'engineer', 'devops', 'qa', 'tester',
68
+ 'analyst', 'designer', 'architect',
69
+ 'operator', 'support', 'agent', 'representative',
70
+ ];
71
+
72
+ /**
73
+ * Error codes for SquadDesignerError
74
+ * @enum {string}
75
+ */
76
+ const DesignerErrorCodes = {
77
+ NO_DOCUMENTATION: 'NO_DOCUMENTATION',
78
+ PARSE_ERROR: 'PARSE_ERROR',
79
+ EMPTY_ANALYSIS: 'EMPTY_ANALYSIS',
80
+ BLUEPRINT_EXISTS: 'BLUEPRINT_EXISTS',
81
+ INVALID_BLUEPRINT: 'INVALID_BLUEPRINT',
82
+ SAVE_ERROR: 'SAVE_ERROR',
83
+ };
84
+
85
+ /**
86
+ * Custom error class for Squad Designer operations
87
+ * @extends Error
88
+ */
89
+ class SquadDesignerError extends Error {
90
+ /**
91
+ * Create a SquadDesignerError
92
+ * @param {string} code - Error code from DesignerErrorCodes enum
93
+ * @param {string} message - Human-readable error message
94
+ * @param {string} [suggestion] - Suggested fix for the error
95
+ */
96
+ constructor(code, message, suggestion) {
97
+ super(message);
98
+ this.name = 'SquadDesignerError';
99
+ this.code = code;
100
+ this.suggestion = suggestion || '';
101
+
102
+ if (Error.captureStackTrace) {
103
+ Error.captureStackTrace(this, SquadDesignerError);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Create error for no documentation provided
109
+ * @returns {SquadDesignerError}
110
+ */
111
+ static noDocumentation() {
112
+ return new SquadDesignerError(
113
+ DesignerErrorCodes.NO_DOCUMENTATION,
114
+ 'No documentation provided for analysis',
115
+ 'Provide documentation via --docs flag or paste text interactively',
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Create error for parse failure
121
+ * @param {string} filePath - Path that failed to parse
122
+ * @param {string} reason - Parse failure reason
123
+ * @returns {SquadDesignerError}
124
+ */
125
+ static parseError(filePath, reason) {
126
+ return new SquadDesignerError(
127
+ DesignerErrorCodes.PARSE_ERROR,
128
+ `Failed to parse documentation: ${filePath} - ${reason}`,
129
+ 'Check file format (supported: .md, .yaml, .yml, .json, .txt)',
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Create error for empty analysis result
135
+ * @returns {SquadDesignerError}
136
+ */
137
+ static emptyAnalysis() {
138
+ return new SquadDesignerError(
139
+ DesignerErrorCodes.EMPTY_ANALYSIS,
140
+ 'No domain concepts could be extracted from documentation',
141
+ 'Provide more detailed documentation with clear entities and workflows',
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Create error for existing blueprint
147
+ * @param {string} blueprintPath - Path where blueprint exists
148
+ * @returns {SquadDesignerError}
149
+ */
150
+ static blueprintExists(blueprintPath) {
151
+ return new SquadDesignerError(
152
+ DesignerErrorCodes.BLUEPRINT_EXISTS,
153
+ `Blueprint already exists at ${blueprintPath}`,
154
+ 'Use --force to overwrite or choose a different output path',
155
+ );
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Squad Designer class
161
+ * Analyzes documentation and generates squad blueprints
162
+ */
163
+ class SquadDesigner {
164
+ /**
165
+ * Create a SquadDesigner
166
+ * @param {Object} options - Designer options
167
+ * @param {string} [options.designsPath] - Path to designs directory
168
+ */
169
+ constructor(options = {}) {
170
+ this.designsPath = options.designsPath || DEFAULT_DESIGNS_PATH;
171
+ }
172
+
173
+ // ===========================================================================
174
+ // DOCUMENTATION COLLECTION
175
+ // ===========================================================================
176
+
177
+ /**
178
+ * Collect and normalize documentation from various sources
179
+ * @param {Object} options - Collection options
180
+ * @param {string|string[]} [options.docs] - File paths or text content
181
+ * @param {string} [options.text] - Direct text input
182
+ * @param {string} [options.domain] - Domain hint
183
+ * @returns {Promise<Object>} Normalized documentation
184
+ */
185
+ async collectDocumentation(options) {
186
+ const sources = [];
187
+
188
+ // Handle file paths
189
+ if (options.docs) {
190
+ const paths = Array.isArray(options.docs)
191
+ ? options.docs
192
+ : options.docs.split(',').map(p => p.trim());
193
+
194
+ for (const filePath of paths) {
195
+ try {
196
+ const content = await this.readDocumentationFile(filePath);
197
+ sources.push({
198
+ type: 'file',
199
+ path: filePath,
200
+ content,
201
+ });
202
+ } catch (error) {
203
+ throw SquadDesignerError.parseError(filePath, error.message);
204
+ }
205
+ }
206
+ }
207
+
208
+ // Handle direct text input
209
+ if (options.text) {
210
+ sources.push({
211
+ type: 'text',
212
+ path: null,
213
+ content: options.text,
214
+ });
215
+ }
216
+
217
+ if (sources.length === 0) {
218
+ throw SquadDesignerError.noDocumentation();
219
+ }
220
+
221
+ return {
222
+ sources,
223
+ domainHint: options.domain || null,
224
+ mergedContent: sources.map(s => s.content).join('\n\n---\n\n'),
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Read and parse a documentation file
230
+ * @param {string} filePath - Path to file
231
+ * @returns {Promise<string>} File content as text
232
+ */
233
+ async readDocumentationFile(filePath) {
234
+ const content = await fs.readFile(filePath, 'utf-8');
235
+ const ext = path.extname(filePath).toLowerCase();
236
+
237
+ switch (ext) {
238
+ case '.yaml':
239
+ case '.yml':
240
+ // Convert YAML to readable text
241
+ try {
242
+ const parsed = yaml.load(content);
243
+ return this.yamlToText(parsed);
244
+ } catch {
245
+ return content; // Return raw if parse fails
246
+ }
247
+
248
+ case '.json':
249
+ // Convert JSON to readable text
250
+ try {
251
+ const parsed = JSON.parse(content);
252
+ return this.jsonToText(parsed);
253
+ } catch {
254
+ return content;
255
+ }
256
+
257
+ case '.md':
258
+ case '.txt':
259
+ default:
260
+ return content;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Convert YAML object to readable text
266
+ * @param {Object} obj - Parsed YAML object
267
+ * @param {number} [depth=0] - Current depth for indentation
268
+ * @returns {string} Text representation
269
+ */
270
+ yamlToText(obj, depth = 0) {
271
+ if (typeof obj !== 'object' || obj === null) {
272
+ return String(obj);
273
+ }
274
+
275
+ const indent = ' '.repeat(depth);
276
+ const lines = [];
277
+
278
+ for (const [key, value] of Object.entries(obj)) {
279
+ if (Array.isArray(value)) {
280
+ lines.push(`${indent}${key}:`);
281
+ for (const item of value) {
282
+ if (typeof item === 'object') {
283
+ lines.push(`${indent} - ${this.yamlToText(item, depth + 2)}`);
284
+ } else {
285
+ lines.push(`${indent} - ${item}`);
286
+ }
287
+ }
288
+ } else if (typeof value === 'object' && value !== null) {
289
+ lines.push(`${indent}${key}:`);
290
+ lines.push(this.yamlToText(value, depth + 1));
291
+ } else {
292
+ lines.push(`${indent}${key}: ${value}`);
293
+ }
294
+ }
295
+
296
+ return lines.join('\n');
297
+ }
298
+
299
+ /**
300
+ * Convert JSON object to readable text
301
+ * @param {Object} obj - Parsed JSON object
302
+ * @returns {string} Text representation
303
+ */
304
+ jsonToText(obj) {
305
+ return this.yamlToText(obj); // Reuse YAML converter
306
+ }
307
+
308
+ // ===========================================================================
309
+ // DOMAIN ANALYSIS
310
+ // ===========================================================================
311
+
312
+ /**
313
+ * Analyze documentation and extract domain concepts
314
+ * @param {Object} documentation - Normalized documentation from collectDocumentation
315
+ * @returns {Object} Analysis result with entities, workflows, integrations, stakeholders
316
+ */
317
+ analyzeDomain(documentation) {
318
+ const content = documentation.mergedContent.toLowerCase();
319
+ const originalContent = documentation.mergedContent;
320
+
321
+ const analysis = {
322
+ domain: this.extractDomain(originalContent, documentation.domainHint),
323
+ entities: this.extractEntities(originalContent),
324
+ workflows: this.extractWorkflows(content, originalContent),
325
+ integrations: this.extractIntegrations(content),
326
+ stakeholders: this.extractStakeholders(content),
327
+ };
328
+
329
+ // Validate we extracted something useful
330
+ if (
331
+ analysis.entities.length === 0 &&
332
+ analysis.workflows.length === 0
333
+ ) {
334
+ throw SquadDesignerError.emptyAnalysis();
335
+ }
336
+
337
+ return analysis;
338
+ }
339
+
340
+ /**
341
+ * Extract domain name from content
342
+ * @param {string} content - Original content
343
+ * @param {string|null} hint - Domain hint if provided
344
+ * @returns {string} Domain name
345
+ */
346
+ extractDomain(content, hint) {
347
+ if (hint) {
348
+ return this.toDomainName(hint);
349
+ }
350
+
351
+ // Try to extract from title/heading
352
+ const titleMatch = content.match(/^#\s+(.+)$/m);
353
+ if (titleMatch) {
354
+ return this.toDomainName(titleMatch[1]);
355
+ }
356
+
357
+ // Try to extract from "name:" in YAML
358
+ const nameMatch = content.match(/name:\s*(.+)/i);
359
+ if (nameMatch) {
360
+ return this.toDomainName(nameMatch[1]);
361
+ }
362
+
363
+ return 'custom-domain';
364
+ }
365
+
366
+ /**
367
+ * Convert text to domain name format
368
+ * @param {string} text - Input text
369
+ * @returns {string} Kebab-case domain name
370
+ */
371
+ toDomainName(text) {
372
+ return text
373
+ .toLowerCase()
374
+ .replace(/[^a-z0-9\s-]/g, '')
375
+ .replace(/\s+/g, '-')
376
+ .replace(/-+/g, '-')
377
+ .replace(/^-|-$/g, '')
378
+ .substring(0, 50);
379
+ }
380
+
381
+ /**
382
+ * Extract entities (nouns, concepts) from content
383
+ * @param {string} content - Original content (preserves case)
384
+ * @returns {string[]} List of entities
385
+ */
386
+ extractEntities(content) {
387
+ const entities = new Set();
388
+
389
+ // Find capitalized words (likely entities)
390
+ const capitalizedPattern = /\b([A-Z][a-z]+(?:[A-Z][a-z]+)*)\b/g;
391
+ let match;
392
+ while ((match = capitalizedPattern.exec(content)) !== null) {
393
+ const word = match[1];
394
+ // Filter out common non-entity words
395
+ if (!this.isCommonWord(word) && word.length > 2) {
396
+ entities.add(word);
397
+ }
398
+ }
399
+
400
+ // Find words in backticks or quotes (often entities in docs)
401
+ const quotedPattern = /[`"']([A-Za-z][A-Za-z0-9_]+)[`"']/g;
402
+ while ((match = quotedPattern.exec(content)) !== null) {
403
+ const word = match[1];
404
+ if (!this.isCommonWord(word) && word.length > 2) {
405
+ entities.add(this.toTitleCase(word));
406
+ }
407
+ }
408
+
409
+ return Array.from(entities).slice(0, 20); // Limit to top 20
410
+ }
411
+
412
+ /**
413
+ * Check if word is a common non-entity word
414
+ * @param {string} word - Word to check
415
+ * @returns {boolean} True if common word
416
+ */
417
+ isCommonWord(word) {
418
+ const commonWords = new Set([
419
+ 'The', 'This', 'That', 'These', 'Those', 'What', 'When', 'Where', 'Which',
420
+ 'How', 'Why', 'Who', 'All', 'Any', 'Some', 'Each', 'Every', 'Both',
421
+ 'Few', 'More', 'Most', 'Other', 'Such', 'No', 'Not', 'Only', 'Same',
422
+ 'Than', 'Too', 'Very', 'Just', 'But', 'And', 'For', 'With', 'From',
423
+ 'About', 'Into', 'Through', 'During', 'Before', 'After', 'Above', 'Below',
424
+ 'Between', 'Under', 'Again', 'Further', 'Then', 'Once', 'Here', 'There',
425
+ 'True', 'False', 'Null', 'None', 'Yes', 'No', 'Example', 'Note', 'Warning',
426
+ 'Error', 'Success', 'Failure', 'Status', 'Type', 'Name', 'Value', 'Data',
427
+ 'File', 'Path', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Function',
428
+ 'Class', 'Method', 'Property', 'Parameter', 'Return', 'Input', 'Output',
429
+ 'Request', 'Response', 'Result', 'Config', 'Options', 'Settings',
430
+ ]);
431
+ return commonWords.has(word);
432
+ }
433
+
434
+ /**
435
+ * Convert string to title case
436
+ * @param {string} str - Input string
437
+ * @returns {string} Title case string
438
+ */
439
+ toTitleCase(str) {
440
+ return str.charAt(0).toUpperCase() + str.slice(1);
441
+ }
442
+
443
+ /**
444
+ * Extract workflows from content
445
+ * @param {string} lowerContent - Lowercase content
446
+ * @param {string} originalContent - Original content
447
+ * @returns {string[]} List of workflow names
448
+ */
449
+ extractWorkflows(lowerContent, originalContent) {
450
+ const workflows = new Set();
451
+
452
+ // Find action + noun patterns
453
+ for (const action of ACTION_KEYWORDS) {
454
+ const pattern = new RegExp(`\\b${action}[\\s-]+([a-z]+)`, 'gi');
455
+ let match;
456
+ while ((match = pattern.exec(lowerContent)) !== null) {
457
+ const noun = match[1];
458
+ if (noun.length > 2 && !this.isStopWord(noun)) {
459
+ workflows.add(`${action}-${noun}`);
460
+ }
461
+ }
462
+ }
463
+
464
+ // Find numbered steps or bullet points with actions
465
+ const stepPattern = /(?:^|\n)\s*(?:\d+\.|[-*])\s*([A-Za-z]+)\s+(?:the\s+)?([a-z]+)/gi;
466
+ let match;
467
+ while ((match = stepPattern.exec(originalContent)) !== null) {
468
+ const verb = match[1].toLowerCase();
469
+ const noun = match[2].toLowerCase();
470
+ if (ACTION_KEYWORDS.includes(verb) && !this.isStopWord(noun)) {
471
+ workflows.add(`${verb}-${noun}`);
472
+ }
473
+ }
474
+
475
+ return Array.from(workflows).slice(0, 15); // Limit to top 15
476
+ }
477
+
478
+ /**
479
+ * Check if word is a stop word
480
+ * @param {string} word - Word to check
481
+ * @returns {boolean} True if stop word
482
+ */
483
+ isStopWord(word) {
484
+ const stopWords = new Set([
485
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
486
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
487
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
488
+ 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
489
+ 'it', 'its', 'this', 'that', 'these', 'those', 'all', 'each', 'every',
490
+ ]);
491
+ return stopWords.has(word.toLowerCase());
492
+ }
493
+
494
+ /**
495
+ * Extract integrations from content
496
+ * @param {string} content - Lowercase content
497
+ * @returns {string[]} List of integrations
498
+ */
499
+ extractIntegrations(content) {
500
+ const integrations = new Set();
501
+
502
+ for (const keyword of INTEGRATION_KEYWORDS) {
503
+ if (content.includes(keyword)) {
504
+ integrations.add(this.toTitleCase(keyword));
505
+ }
506
+ }
507
+
508
+ // Find API endpoints mentioned
509
+ const apiPattern = /(?:api|endpoint)[:\s]+([a-z/_-]+)/gi;
510
+ let match;
511
+ while ((match = apiPattern.exec(content)) !== null) {
512
+ integrations.add(`API: ${match[1]}`);
513
+ }
514
+
515
+ return Array.from(integrations).slice(0, 10);
516
+ }
517
+
518
+ /**
519
+ * Extract stakeholders from content
520
+ * @param {string} content - Lowercase content
521
+ * @returns {string[]} List of stakeholders
522
+ */
523
+ extractStakeholders(content) {
524
+ const stakeholders = new Set();
525
+
526
+ for (const role of ROLE_KEYWORDS) {
527
+ const pattern = new RegExp(`\\b${role}s?\\b`, 'gi');
528
+ if (pattern.test(content)) {
529
+ stakeholders.add(this.toTitleCase(role));
530
+ }
531
+ }
532
+
533
+ return Array.from(stakeholders).slice(0, 10);
534
+ }
535
+
536
+ // ===========================================================================
537
+ // RECOMMENDATION GENERATION
538
+ // ===========================================================================
539
+
540
+ /**
541
+ * Generate agent recommendations based on analysis
542
+ * @param {Object} analysis - Domain analysis result
543
+ * @returns {Array} Recommended agents with confidence scores
544
+ */
545
+ generateAgentRecommendations(analysis) {
546
+ const agents = [];
547
+ const usedWorkflows = new Set();
548
+
549
+ // Group workflows by main action category
550
+ const workflowGroups = this.groupWorkflowsByCategory(analysis.workflows);
551
+
552
+ for (const [category, workflows] of Object.entries(workflowGroups)) {
553
+ if (workflows.length === 0) continue;
554
+
555
+ const agentId = `${analysis.domain}-${category}`;
556
+ const commands = workflows.map(w => w.replace(/-/g, '-'));
557
+
558
+ // Calculate confidence based on workflow clarity
559
+ const confidence = Math.min(
560
+ 0.95,
561
+ 0.6 + (workflows.length * 0.05) + (commands.length > 3 ? 0.1 : 0),
562
+ );
563
+
564
+ agents.push({
565
+ id: this.toKebabCase(agentId),
566
+ role: this.generateAgentRole(category, workflows, analysis.domain),
567
+ commands: commands.slice(0, 6), // Max 6 commands per agent
568
+ confidence: Math.round(confidence * 100) / 100,
569
+ user_added: false,
570
+ user_modified: false,
571
+ });
572
+
573
+ workflows.forEach(w => usedWorkflows.add(w));
574
+ }
575
+
576
+ // If we have entities but no clear workflows, create a generic manager
577
+ if (agents.length === 0 && analysis.entities.length > 0) {
578
+ const mainEntity = analysis.entities[0].toLowerCase();
579
+ agents.push({
580
+ id: `${mainEntity}-manager`,
581
+ role: `Manages ${mainEntity} lifecycle and operations`,
582
+ commands: [`create-${mainEntity}`, `update-${mainEntity}`, `delete-${mainEntity}`, `list-${mainEntity}s`],
583
+ confidence: 0.65,
584
+ user_added: false,
585
+ user_modified: false,
586
+ });
587
+ }
588
+
589
+ return agents;
590
+ }
591
+
592
+ /**
593
+ * Group workflows by category
594
+ * @param {string[]} workflows - List of workflows
595
+ * @returns {Object} Grouped workflows
596
+ */
597
+ groupWorkflowsByCategory(workflows) {
598
+ const groups = {
599
+ manager: [],
600
+ processor: [],
601
+ handler: [],
602
+ };
603
+
604
+ for (const workflow of workflows) {
605
+ const [action] = workflow.split('-');
606
+
607
+ if (['create', 'update', 'delete', 'add', 'remove', 'edit'].includes(action)) {
608
+ groups.manager.push(workflow);
609
+ } else if (['process', 'transform', 'migrate', 'sync', 'import', 'export'].includes(action)) {
610
+ groups.processor.push(workflow);
611
+ } else {
612
+ groups.handler.push(workflow);
613
+ }
614
+ }
615
+
616
+ // Remove empty groups
617
+ return Object.fromEntries(
618
+ Object.entries(groups).filter(([, v]) => v.length > 0),
619
+ );
620
+ }
621
+
622
+ /**
623
+ * Generate agent role description
624
+ * @param {string} category - Agent category
625
+ * @param {string[]} workflows - Agent workflows
626
+ * @param {string} domain - Domain name
627
+ * @returns {string} Role description
628
+ */
629
+ generateAgentRole(category, workflows, domain) {
630
+ const domainTitle = domain.split('-').map(w => this.toTitleCase(w)).join(' ');
631
+
632
+ switch (category) {
633
+ case 'manager':
634
+ return `Manages ${domainTitle} resources and lifecycle operations`;
635
+ case 'processor':
636
+ return `Processes and transforms ${domainTitle} data`;
637
+ case 'handler':
638
+ return `Handles ${domainTitle} events and operations`;
639
+ default:
640
+ return `Manages ${domainTitle} ${category} operations`;
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Convert string to kebab-case
646
+ * @param {string} str - Input string
647
+ * @returns {string} Kebab-case string
648
+ */
649
+ toKebabCase(str) {
650
+ return str
651
+ .toLowerCase()
652
+ .replace(/[^a-z0-9]+/g, '-')
653
+ .replace(/^-|-$/g, '');
654
+ }
655
+
656
+ /**
657
+ * Generate task recommendations based on analysis and agents
658
+ * @param {Object} analysis - Domain analysis result
659
+ * @param {Array} agents - Recommended agents
660
+ * @returns {Array} Recommended tasks with confidence scores
661
+ */
662
+ generateTaskRecommendations(analysis, agents) {
663
+ const tasks = [];
664
+
665
+ for (const agent of agents) {
666
+ for (const command of agent.commands) {
667
+ const taskName = `${command}.md`;
668
+ const entrada = this.generateTaskEntrada(command, analysis);
669
+ const saida = this.generateTaskSaida(command, analysis);
670
+
671
+ // Calculate confidence based on entrada/saida clarity
672
+ const confidence = Math.min(
673
+ 0.95,
674
+ 0.5 + (entrada.length * 0.1) + (saida.length * 0.1),
675
+ );
676
+
677
+ tasks.push({
678
+ name: taskName.replace('.md', ''),
679
+ agent: agent.id,
680
+ entrada,
681
+ saida,
682
+ confidence: Math.round(confidence * 100) / 100,
683
+ });
684
+ }
685
+ }
686
+
687
+ return tasks;
688
+ }
689
+
690
+ /**
691
+ * Generate task entrada (inputs)
692
+ * @param {string} command - Command name
693
+ * @param {Object} analysis - Domain analysis
694
+ * @returns {string[]} Input parameters
695
+ */
696
+ generateTaskEntrada(command, analysis) {
697
+ const inputs = [];
698
+ const [action, ...rest] = command.split('-');
699
+ const subject = rest.join('_');
700
+
701
+ switch (action) {
702
+ case 'create':
703
+ case 'add':
704
+ inputs.push(`${subject}_data`);
705
+ if (analysis.stakeholders.length > 0) {
706
+ inputs.push('created_by');
707
+ }
708
+ break;
709
+
710
+ case 'update':
711
+ case 'edit':
712
+ case 'modify':
713
+ inputs.push(`${subject}_id`);
714
+ inputs.push('updates');
715
+ break;
716
+
717
+ case 'delete':
718
+ case 'remove':
719
+ inputs.push(`${subject}_id`);
720
+ break;
721
+
722
+ case 'get':
723
+ case 'fetch':
724
+ case 'retrieve':
725
+ inputs.push(`${subject}_id`);
726
+ break;
727
+
728
+ case 'list':
729
+ case 'search':
730
+ case 'find':
731
+ inputs.push('filters');
732
+ inputs.push('pagination');
733
+ break;
734
+
735
+ case 'process':
736
+ case 'transform':
737
+ inputs.push('source_data');
738
+ inputs.push('options');
739
+ break;
740
+
741
+ default:
742
+ inputs.push(`${subject}_id`);
743
+ inputs.push('options');
744
+ }
745
+
746
+ return inputs;
747
+ }
748
+
749
+ /**
750
+ * Generate task saida (outputs)
751
+ * @param {string} command - Command name
752
+ * @param {Object} _analysis - Domain analysis (reserved for future use)
753
+ * @returns {string[]} Output parameters
754
+ */
755
+ generateTaskSaida(command, _analysis) {
756
+ const outputs = [];
757
+ const [action, ...rest] = command.split('-');
758
+ const subject = rest.join('_');
759
+
760
+ switch (action) {
761
+ case 'create':
762
+ case 'add':
763
+ outputs.push(`${subject}_id`);
764
+ outputs.push('status');
765
+ break;
766
+
767
+ case 'update':
768
+ case 'edit':
769
+ case 'modify':
770
+ outputs.push(`updated_${subject}`);
771
+ outputs.push('changelog');
772
+ break;
773
+
774
+ case 'delete':
775
+ case 'remove':
776
+ outputs.push('success');
777
+ outputs.push('deleted_at');
778
+ break;
779
+
780
+ case 'get':
781
+ case 'fetch':
782
+ case 'retrieve':
783
+ outputs.push(subject);
784
+ break;
785
+
786
+ case 'list':
787
+ case 'search':
788
+ case 'find':
789
+ outputs.push(`${subject}_list`);
790
+ outputs.push('total_count');
791
+ break;
792
+
793
+ case 'process':
794
+ case 'transform':
795
+ outputs.push('result_data');
796
+ outputs.push('metrics');
797
+ break;
798
+
799
+ default:
800
+ outputs.push('result');
801
+ outputs.push('status');
802
+ }
803
+
804
+ return outputs;
805
+ }
806
+
807
+ // ===========================================================================
808
+ // BLUEPRINT GENERATION
809
+ // ===========================================================================
810
+
811
+ /**
812
+ * Generate complete blueprint
813
+ * @param {Object} options - Blueprint options
814
+ * @param {Object} options.analysis - Domain analysis
815
+ * @param {Object} options.recommendations - Agent and task recommendations
816
+ * @param {Object} options.metadata - Blueprint metadata
817
+ * @param {Object} [options.userAdjustments] - User modifications
818
+ * @returns {Object} Complete squad blueprint
819
+ */
820
+ generateBlueprint(options) {
821
+ const { analysis, recommendations, metadata, userAdjustments } = options;
822
+
823
+ // Calculate overall confidence
824
+ const agentConfidences = recommendations.agents.map(a => a.confidence);
825
+ const taskConfidences = recommendations.tasks.map(t => t.confidence);
826
+ const allConfidences = [...agentConfidences, ...taskConfidences];
827
+ const overallConfidence = allConfidences.length > 0
828
+ ? allConfidences.reduce((a, b) => a + b, 0) / allConfidences.length
829
+ : 0.5;
830
+
831
+ // Determine template based on recommendations
832
+ const template = this.determineTemplate(recommendations);
833
+
834
+ return {
835
+ squad: {
836
+ name: `${analysis.domain}-squad`,
837
+ description: `Squad for ${analysis.domain.replace(/-/g, ' ')} management`,
838
+ domain: analysis.domain,
839
+ },
840
+ analysis: {
841
+ entities: analysis.entities,
842
+ workflows: analysis.workflows,
843
+ integrations: analysis.integrations,
844
+ stakeholders: analysis.stakeholders,
845
+ },
846
+ recommendations: {
847
+ agents: recommendations.agents,
848
+ tasks: recommendations.tasks,
849
+ template,
850
+ config_mode: 'extend',
851
+ },
852
+ metadata: {
853
+ created_at: metadata.created_at || new Date().toISOString(),
854
+ source_docs: metadata.source_docs || [],
855
+ user_adjustments: userAdjustments?.count || 0,
856
+ overall_confidence: Math.round(overallConfidence * 100) / 100,
857
+ },
858
+ };
859
+ }
860
+
861
+ /**
862
+ * Determine best template based on recommendations
863
+ * @param {Object} recommendations - Recommendations
864
+ * @returns {string} Template name
865
+ */
866
+ determineTemplate(recommendations) {
867
+ const hasDataProcessing = recommendations.tasks.some(t =>
868
+ t.name.includes('process') || t.name.includes('transform') ||
869
+ t.name.includes('import') || t.name.includes('export'),
870
+ );
871
+
872
+ if (hasDataProcessing) {
873
+ return 'etl';
874
+ }
875
+
876
+ if (recommendations.tasks.length === 0) {
877
+ return 'agent-only';
878
+ }
879
+
880
+ return 'basic';
881
+ }
882
+
883
+ // ===========================================================================
884
+ // BLUEPRINT PERSISTENCE
885
+ // ===========================================================================
886
+
887
+ /**
888
+ * Save blueprint to file
889
+ * @param {Object} blueprint - Squad blueprint
890
+ * @param {string} [outputPath] - Output path (optional)
891
+ * @param {Object} [options] - Save options
892
+ * @param {boolean} [options.force] - Overwrite existing
893
+ * @returns {Promise<string>} Path to saved file
894
+ */
895
+ async saveBlueprint(blueprint, outputPath, options = {}) {
896
+ const designsDir = outputPath || this.designsPath;
897
+
898
+ // Ensure designs directory exists
899
+ await fs.mkdir(designsDir, { recursive: true });
900
+
901
+ const filename = `${blueprint.squad.name}-design.yaml`;
902
+ const filePath = path.join(designsDir, filename);
903
+
904
+ // Check if file exists
905
+ if (!options.force) {
906
+ try {
907
+ await fs.access(filePath);
908
+ throw SquadDesignerError.blueprintExists(filePath);
909
+ } catch (error) {
910
+ if (error.code !== 'ENOENT') {
911
+ throw error;
912
+ }
913
+ }
914
+ }
915
+
916
+ // Generate YAML content
917
+ const yamlContent = this.blueprintToYaml(blueprint);
918
+
919
+ // Write file
920
+ await fs.writeFile(filePath, yamlContent, 'utf-8');
921
+
922
+ return filePath;
923
+ }
924
+
925
+ /**
926
+ * Convert blueprint to YAML string
927
+ * @param {Object} blueprint - Blueprint object
928
+ * @returns {string} YAML content
929
+ */
930
+ blueprintToYaml(blueprint) {
931
+ const header = `# Squad Design Blueprint
932
+ # Generated by *design-squad
933
+ # Source: ${blueprint.metadata.source_docs.join(', ') || 'Interactive input'}
934
+ # Created: ${blueprint.metadata.created_at}
935
+
936
+ `;
937
+ return header + yaml.dump(blueprint, {
938
+ indent: 2,
939
+ lineWidth: 100,
940
+ noRefs: true,
941
+ sortKeys: false,
942
+ });
943
+ }
944
+
945
+ /**
946
+ * Load blueprint from file
947
+ * @param {string} blueprintPath - Path to blueprint file
948
+ * @returns {Promise<Object>} Loaded blueprint
949
+ */
950
+ async loadBlueprint(blueprintPath) {
951
+ try {
952
+ const content = await fs.readFile(blueprintPath, 'utf-8');
953
+ return yaml.load(content);
954
+ } catch (error) {
955
+ throw new SquadDesignerError(
956
+ DesignerErrorCodes.INVALID_BLUEPRINT,
957
+ `Failed to load blueprint: ${error.message}`,
958
+ 'Check that the blueprint file exists and is valid YAML',
959
+ );
960
+ }
961
+ }
962
+
963
+ /**
964
+ * Validate blueprint structure
965
+ * @param {Object} blueprint - Blueprint to validate
966
+ * @returns {Object} Validation result { valid, errors }
967
+ */
968
+ validateBlueprint(blueprint) {
969
+ const errors = [];
970
+
971
+ // Check required top-level keys
972
+ if (!blueprint.squad) {
973
+ errors.push('Missing required key: squad');
974
+ } else {
975
+ if (!blueprint.squad.name) errors.push('Missing squad.name');
976
+ if (!blueprint.squad.domain) errors.push('Missing squad.domain');
977
+ }
978
+
979
+ if (!blueprint.recommendations) {
980
+ errors.push('Missing required key: recommendations');
981
+ } else {
982
+ if (!Array.isArray(blueprint.recommendations.agents)) {
983
+ errors.push('recommendations.agents must be an array');
984
+ }
985
+ if (!Array.isArray(blueprint.recommendations.tasks)) {
986
+ errors.push('recommendations.tasks must be an array');
987
+ }
988
+ }
989
+
990
+ if (!blueprint.metadata) {
991
+ errors.push('Missing required key: metadata');
992
+ }
993
+
994
+ return {
995
+ valid: errors.length === 0,
996
+ errors,
997
+ };
998
+ }
999
+ }
1000
+
1001
+ module.exports = {
1002
+ SquadDesigner,
1003
+ SquadDesignerError,
1004
+ DesignerErrorCodes,
1005
+ DEFAULT_DESIGNS_PATH,
1006
+ MIN_CONFIDENCE_THRESHOLD,
1007
+ ACTION_KEYWORDS,
1008
+ INTEGRATION_KEYWORDS,
1009
+ ROLE_KEYWORDS,
1010
+ };