@polymorphism-tech/morph-spec 2.1.2 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +389 -40
- package/bin/morph-spec.js +121 -0
- package/bin/task-manager.js +368 -0
- package/bin/validate-agents-skills.js +17 -5
- package/bin/validate.js +268 -0
- package/content/.claude/skills/specialists/ef-modeler.md +11 -0
- package/content/.claude/skills/specialists/hangfire-orchestrator.md +10 -0
- package/content/.claude/skills/specialists/ui-ux-designer.md +40 -0
- package/content/.claude/skills/stacks/dotnet-blazor.md +18 -0
- package/content/.morph/examples/state-v3.json +188 -0
- package/detectors/structure-detector.js +32 -3
- package/package.json +1 -1
- package/src/commands/create-story.js +68 -0
- package/src/commands/init.js +59 -5
- package/src/commands/state.js +1 -1
- package/src/commands/task.js +75 -0
- package/src/lib/continuous-validator.js +440 -0
- package/src/lib/learning-system.js +520 -0
- package/src/lib/mockup-generator.js +366 -0
- package/src/lib/ui-detector.js +350 -0
- package/src/lib/validators/architecture-validator.js +387 -0
- package/src/lib/validators/package-validator.js +360 -0
- package/src/lib/validators/ui-contrast-validator.js +422 -0
- package/src/utils/file-copier.js +26 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning System
|
|
3
|
+
*
|
|
4
|
+
* Tracks user decisions and learns preferences over time.
|
|
5
|
+
* Provides contextual suggestions based on historical patterns.
|
|
6
|
+
*
|
|
7
|
+
* Learns from:
|
|
8
|
+
* - decisions.md files (ADRs from each feature)
|
|
9
|
+
* - .morph/project/standards/*.md (project standards)
|
|
10
|
+
* - state.json (feature outcomes and complexity)
|
|
11
|
+
*
|
|
12
|
+
* MORPH-SPEC 3.0 - Sprint 4
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
16
|
+
import { glob } from 'glob';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decision Categories
|
|
22
|
+
*/
|
|
23
|
+
const DECISION_CATEGORIES = {
|
|
24
|
+
uiLibrary: {
|
|
25
|
+
keywords: ['fluent', 'mudblazor', 'radzen', 'ui library', 'component library'],
|
|
26
|
+
options: ['Fluent UI', 'MudBlazor', 'Radzen', 'Custom']
|
|
27
|
+
},
|
|
28
|
+
architecture: {
|
|
29
|
+
keywords: ['cqrs', 'repository', 'mediator', 'clean architecture', 'pattern'],
|
|
30
|
+
options: ['CQRS', 'Repository Pattern', 'Service Layer', 'Direct EF', 'DDD']
|
|
31
|
+
},
|
|
32
|
+
infrastructure: {
|
|
33
|
+
keywords: ['azure', 'sql', 'cosmos', 'storage', 'container apps', 'app service'],
|
|
34
|
+
options: ['Azure SQL', 'Cosmos DB', 'Container Apps', 'App Service', 'Functions', 'Service Bus', 'Storage Queue']
|
|
35
|
+
},
|
|
36
|
+
authentication: {
|
|
37
|
+
keywords: ['auth', 'login', 'identity', 'clerk', 'entra'],
|
|
38
|
+
options: ['Clerk', 'Azure AD/Entra', 'Identity', 'Custom']
|
|
39
|
+
},
|
|
40
|
+
stateManagement: {
|
|
41
|
+
keywords: ['state', 'fluxor', 'redux', 'signal'],
|
|
42
|
+
options: ['Fluxor', 'Blazor State', 'In-Memory', 'API-driven']
|
|
43
|
+
},
|
|
44
|
+
testing: {
|
|
45
|
+
keywords: ['test', 'xunit', 'nunit', 'bunit'],
|
|
46
|
+
options: ['xUnit', 'NUnit', 'bUnit', 'Playwright']
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Learning System Class
|
|
52
|
+
*/
|
|
53
|
+
export class LearningSystem {
|
|
54
|
+
constructor(projectPath = '.') {
|
|
55
|
+
this.projectPath = projectPath;
|
|
56
|
+
this.knowledgeBasePath = join(projectPath, '.morph', 'knowledge-base.json');
|
|
57
|
+
this.knowledgeBase = this.loadKnowledgeBase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load knowledge base from disk
|
|
62
|
+
*/
|
|
63
|
+
loadKnowledgeBase() {
|
|
64
|
+
if (existsSync(this.knowledgeBasePath)) {
|
|
65
|
+
try {
|
|
66
|
+
const content = readFileSync(this.knowledgeBasePath, 'utf-8');
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(chalk.yellow('⚠️ Failed to load knowledge base, creating new one'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return this.createEmptyKnowledgeBase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create empty knowledge base
|
|
78
|
+
*/
|
|
79
|
+
createEmptyKnowledgeBase() {
|
|
80
|
+
return {
|
|
81
|
+
version: '3.0.0',
|
|
82
|
+
lastUpdated: new Date().toISOString(),
|
|
83
|
+
totalDecisions: 0,
|
|
84
|
+
categories: Object.keys(DECISION_CATEGORIES).reduce((acc, category) => {
|
|
85
|
+
acc[category] = {
|
|
86
|
+
total: 0,
|
|
87
|
+
choices: {},
|
|
88
|
+
patterns: []
|
|
89
|
+
};
|
|
90
|
+
return acc;
|
|
91
|
+
}, {}),
|
|
92
|
+
features: [],
|
|
93
|
+
insights: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save knowledge base to disk
|
|
99
|
+
*/
|
|
100
|
+
saveKnowledgeBase() {
|
|
101
|
+
this.knowledgeBase.lastUpdated = new Date().toISOString();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
writeFileSync(
|
|
105
|
+
this.knowledgeBasePath,
|
|
106
|
+
JSON.stringify(this.knowledgeBase, null, 2),
|
|
107
|
+
'utf-8'
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(chalk.red('❌ Failed to save knowledge base:', error.message));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Learn from all features in project
|
|
116
|
+
*/
|
|
117
|
+
async learnFromProject() {
|
|
118
|
+
console.log(chalk.cyan('🧠 Learning from project history...\n'));
|
|
119
|
+
|
|
120
|
+
// Find all decisions.md files
|
|
121
|
+
const decisionFiles = await glob('.morph/project/outputs/*/decisions.md', {
|
|
122
|
+
cwd: this.projectPath
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (decisionFiles.length === 0) {
|
|
126
|
+
console.log(chalk.yellow('⚠️ No decisions.md files found'));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let newDecisions = 0;
|
|
131
|
+
|
|
132
|
+
for (const file of decisionFiles) {
|
|
133
|
+
const featureName = file.split('/')[3]; // Extract feature name from path
|
|
134
|
+
const learned = await this.learnFromDecisionFile(file, featureName);
|
|
135
|
+
newDecisions += learned;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Generate insights
|
|
139
|
+
this.generateInsights();
|
|
140
|
+
|
|
141
|
+
// Save knowledge base
|
|
142
|
+
this.saveKnowledgeBase();
|
|
143
|
+
|
|
144
|
+
console.log(chalk.green(`\n✅ Learned ${newDecisions} new decisions from ${decisionFiles.length} features`));
|
|
145
|
+
console.log(chalk.gray(` Total knowledge: ${this.knowledgeBase.totalDecisions} decisions`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Learn from single decisions.md file
|
|
150
|
+
*/
|
|
151
|
+
async learnFromDecisionFile(filePath, featureName) {
|
|
152
|
+
const content = readFileSync(join(this.projectPath, filePath), 'utf-8');
|
|
153
|
+
|
|
154
|
+
// Check if already processed
|
|
155
|
+
if (this.knowledgeBase.features.includes(featureName)) {
|
|
156
|
+
return 0; // Already learned from this feature
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let decisionsLearned = 0;
|
|
160
|
+
|
|
161
|
+
// Parse ADRs (Architectural Decision Records)
|
|
162
|
+
const adrs = this.parseADRs(content);
|
|
163
|
+
|
|
164
|
+
for (const adr of adrs) {
|
|
165
|
+
const category = this.categorizeDecision(adr);
|
|
166
|
+
|
|
167
|
+
if (category) {
|
|
168
|
+
this.recordDecision(category, adr, featureName);
|
|
169
|
+
decisionsLearned++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Mark feature as processed
|
|
174
|
+
this.knowledgeBase.features.push(featureName);
|
|
175
|
+
|
|
176
|
+
return decisionsLearned;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse ADRs from decisions.md content
|
|
181
|
+
*/
|
|
182
|
+
parseADRs(content) {
|
|
183
|
+
const adrs = [];
|
|
184
|
+
const adrPattern = /##\s+ADR-\d+:\s+(.+?)\n([\s\S]*?)(?=##\s+ADR-\d+:|$)/g;
|
|
185
|
+
let match;
|
|
186
|
+
|
|
187
|
+
while ((match = adrPattern.exec(content)) !== null) {
|
|
188
|
+
const title = match[1].trim();
|
|
189
|
+
const body = match[2].trim();
|
|
190
|
+
|
|
191
|
+
// Extract decision, alternatives, rationale
|
|
192
|
+
const decision = this.extractSection(body, 'Decision|Choice|Selected');
|
|
193
|
+
const alternatives = this.extractSection(body, 'Alternatives|Options|Considered');
|
|
194
|
+
const rationale = this.extractSection(body, 'Rationale|Justification|Why');
|
|
195
|
+
|
|
196
|
+
adrs.push({
|
|
197
|
+
title,
|
|
198
|
+
decision,
|
|
199
|
+
alternatives,
|
|
200
|
+
rationale,
|
|
201
|
+
fullText: body
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return adrs;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract section from ADR body
|
|
210
|
+
*/
|
|
211
|
+
extractSection(body, sectionPattern) {
|
|
212
|
+
const regex = new RegExp(`\\*\\*${sectionPattern}\\*\\*:?\\s*([^*]+)`, 'i');
|
|
213
|
+
const match = body.match(regex);
|
|
214
|
+
return match ? match[1].trim() : '';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Categorize decision into known categories
|
|
219
|
+
*/
|
|
220
|
+
categorizeDecision(adr) {
|
|
221
|
+
const text = `${adr.title} ${adr.decision} ${adr.fullText}`.toLowerCase();
|
|
222
|
+
|
|
223
|
+
for (const [category, config] of Object.entries(DECISION_CATEGORIES)) {
|
|
224
|
+
for (const keyword of config.keywords) {
|
|
225
|
+
if (text.includes(keyword.toLowerCase())) {
|
|
226
|
+
return category;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null; // Uncategorized
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Record decision in knowledge base
|
|
236
|
+
*/
|
|
237
|
+
recordDecision(category, adr, featureName) {
|
|
238
|
+
const categoryData = this.knowledgeBase.categories[category];
|
|
239
|
+
|
|
240
|
+
// Extract chosen option
|
|
241
|
+
const chosen = this.extractChosenOption(adr, category);
|
|
242
|
+
|
|
243
|
+
if (chosen) {
|
|
244
|
+
// Increment choice count
|
|
245
|
+
if (!categoryData.choices[chosen]) {
|
|
246
|
+
categoryData.choices[chosen] = { count: 0, features: [], rationales: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
categoryData.choices[chosen].count++;
|
|
250
|
+
categoryData.choices[chosen].features.push(featureName);
|
|
251
|
+
|
|
252
|
+
if (adr.rationale) {
|
|
253
|
+
categoryData.choices[chosen].rationales.push(adr.rationale);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Store pattern
|
|
258
|
+
categoryData.patterns.push({
|
|
259
|
+
feature: featureName,
|
|
260
|
+
decision: adr.decision,
|
|
261
|
+
rationale: adr.rationale
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
categoryData.total++;
|
|
265
|
+
this.knowledgeBase.totalDecisions++;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract chosen option from ADR
|
|
270
|
+
*/
|
|
271
|
+
extractChosenOption(adr, category) {
|
|
272
|
+
const config = DECISION_CATEGORIES[category];
|
|
273
|
+
const text = `${adr.title} ${adr.decision}`.toLowerCase();
|
|
274
|
+
|
|
275
|
+
for (const option of config.options) {
|
|
276
|
+
if (text.includes(option.toLowerCase())) {
|
|
277
|
+
return option;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Generate insights from learned patterns
|
|
286
|
+
*/
|
|
287
|
+
generateInsights() {
|
|
288
|
+
const insights = [];
|
|
289
|
+
|
|
290
|
+
for (const [category, data] of Object.entries(this.knowledgeBase.categories)) {
|
|
291
|
+
if (data.total === 0) continue;
|
|
292
|
+
|
|
293
|
+
// Find most common choice
|
|
294
|
+
const sortedChoices = Object.entries(data.choices)
|
|
295
|
+
.sort(([, a], [, b]) => b.count - a.count);
|
|
296
|
+
|
|
297
|
+
if (sortedChoices.length > 0) {
|
|
298
|
+
const [topChoice, topData] = sortedChoices[0];
|
|
299
|
+
const percentage = ((topData.count / data.total) * 100).toFixed(0);
|
|
300
|
+
|
|
301
|
+
insights.push({
|
|
302
|
+
category,
|
|
303
|
+
type: 'preference',
|
|
304
|
+
insight: `User prefers ${topChoice} for ${category} (${percentage}% of decisions)`,
|
|
305
|
+
confidence: percentage >= 70 ? 'high' : percentage >= 50 ? 'medium' : 'low',
|
|
306
|
+
suggestion: `Suggest ${topChoice} by default for ${category} decisions`,
|
|
307
|
+
evidence: {
|
|
308
|
+
totalDecisions: data.total,
|
|
309
|
+
choiceCount: topData.count,
|
|
310
|
+
features: topData.features
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Detect patterns (e.g., always use CQRS with complex features)
|
|
316
|
+
if (category === 'architecture' && data.patterns.length > 3) {
|
|
317
|
+
const cqrsPattern = data.patterns.filter(p =>
|
|
318
|
+
p.decision.toLowerCase().includes('cqrs')
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (cqrsPattern.length > 0) {
|
|
322
|
+
insights.push({
|
|
323
|
+
category,
|
|
324
|
+
type: 'pattern',
|
|
325
|
+
insight: 'User tends to use CQRS for complex features',
|
|
326
|
+
confidence: 'medium',
|
|
327
|
+
suggestion: 'Suggest CQRS when feature complexity is high',
|
|
328
|
+
evidence: {
|
|
329
|
+
occurrences: cqrsPattern.length,
|
|
330
|
+
features: cqrsPattern.map(p => p.feature)
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.knowledgeBase.insights = insights;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get suggestion for a category
|
|
342
|
+
*/
|
|
343
|
+
getSuggestion(category, context = {}) {
|
|
344
|
+
const categoryData = this.knowledgeBase.categories[category];
|
|
345
|
+
|
|
346
|
+
if (!categoryData || categoryData.total === 0) {
|
|
347
|
+
return {
|
|
348
|
+
category,
|
|
349
|
+
suggestion: null,
|
|
350
|
+
confidence: 'none',
|
|
351
|
+
reason: 'No historical data for this category'
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Find most common choice
|
|
356
|
+
const sortedChoices = Object.entries(categoryData.choices)
|
|
357
|
+
.sort(([, a], [, b]) => b.count - a.count);
|
|
358
|
+
|
|
359
|
+
if (sortedChoices.length === 0) {
|
|
360
|
+
return {
|
|
361
|
+
category,
|
|
362
|
+
suggestion: null,
|
|
363
|
+
confidence: 'none',
|
|
364
|
+
reason: 'No choices recorded'
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const [topChoice, topData] = sortedChoices[0];
|
|
369
|
+
const percentage = (topData.count / categoryData.total) * 100;
|
|
370
|
+
|
|
371
|
+
const confidence = percentage >= 70 ? 'high' : percentage >= 50 ? 'medium' : 'low';
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
category,
|
|
375
|
+
suggestion: topChoice,
|
|
376
|
+
confidence,
|
|
377
|
+
percentage: percentage.toFixed(0),
|
|
378
|
+
reason: `You chose ${topChoice} in ${topData.count} of ${categoryData.total} previous ${category} decisions`,
|
|
379
|
+
alternatives: sortedChoices.slice(1, 3).map(([choice, data]) => ({
|
|
380
|
+
name: choice,
|
|
381
|
+
count: data.count,
|
|
382
|
+
percentage: ((data.count / categoryData.total) * 100).toFixed(0)
|
|
383
|
+
})),
|
|
384
|
+
recentFeatures: topData.features.slice(-3)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get all suggestions for current context
|
|
390
|
+
*/
|
|
391
|
+
getAllSuggestions(context = {}) {
|
|
392
|
+
const suggestions = {};
|
|
393
|
+
|
|
394
|
+
for (const category of Object.keys(DECISION_CATEGORIES)) {
|
|
395
|
+
suggestions[category] = this.getSuggestion(category, context);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return suggestions;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Format suggestions for console output
|
|
403
|
+
*/
|
|
404
|
+
formatSuggestions(suggestions) {
|
|
405
|
+
console.log(chalk.cyan('\n💡 AI Suggestions based on your preferences:\n'));
|
|
406
|
+
|
|
407
|
+
for (const [category, suggestion] of Object.entries(suggestions)) {
|
|
408
|
+
if (suggestion.confidence === 'none') continue;
|
|
409
|
+
|
|
410
|
+
const confidenceColor = {
|
|
411
|
+
high: chalk.green,
|
|
412
|
+
medium: chalk.yellow,
|
|
413
|
+
low: chalk.gray
|
|
414
|
+
}[suggestion.confidence];
|
|
415
|
+
|
|
416
|
+
console.log(confidenceColor(` ${category}:`));
|
|
417
|
+
console.log(chalk.white(` → ${suggestion.suggestion} (${suggestion.percentage}% confidence)`));
|
|
418
|
+
console.log(chalk.gray(` ${suggestion.reason}`));
|
|
419
|
+
|
|
420
|
+
if (suggestion.alternatives && suggestion.alternatives.length > 0) {
|
|
421
|
+
console.log(chalk.gray(` Alternatives: ${suggestion.alternatives.map(a => `${a.name} (${a.percentage}%)`).join(', ')}`));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log('');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get insights summary
|
|
430
|
+
*/
|
|
431
|
+
getInsightsSummary() {
|
|
432
|
+
const insights = this.knowledgeBase.insights;
|
|
433
|
+
|
|
434
|
+
if (insights.length === 0) {
|
|
435
|
+
return {
|
|
436
|
+
totalInsights: 0,
|
|
437
|
+
message: 'No insights generated yet. Build more features to learn preferences.'
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const highConfidence = insights.filter(i => i.confidence === 'high');
|
|
442
|
+
const mediumConfidence = insights.filter(i => i.confidence === 'medium');
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
totalInsights: insights.length,
|
|
446
|
+
highConfidence: highConfidence.length,
|
|
447
|
+
mediumConfidence: mediumConfidence.length,
|
|
448
|
+
insights: insights.map(i => ({
|
|
449
|
+
category: i.category,
|
|
450
|
+
insight: i.insight,
|
|
451
|
+
confidence: i.confidence,
|
|
452
|
+
suggestion: i.suggestion
|
|
453
|
+
}))
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Format insights for console output
|
|
459
|
+
*/
|
|
460
|
+
formatInsights() {
|
|
461
|
+
const summary = this.getInsightsSummary();
|
|
462
|
+
|
|
463
|
+
if (summary.totalInsights === 0) {
|
|
464
|
+
console.log(chalk.yellow(`\n⚠️ ${summary.message}\n`));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
console.log(chalk.cyan(`\n🧠 Learning Insights (${summary.totalInsights} patterns detected):\n`));
|
|
469
|
+
|
|
470
|
+
for (const insight of summary.insights) {
|
|
471
|
+
const confidenceEmoji = {
|
|
472
|
+
high: '🟢',
|
|
473
|
+
medium: '🟡',
|
|
474
|
+
low: '🔵'
|
|
475
|
+
}[insight.confidence];
|
|
476
|
+
|
|
477
|
+
console.log(`${confidenceEmoji} ${chalk.bold(insight.category)}: ${insight.insight}`);
|
|
478
|
+
console.log(chalk.gray(` Suggestion: ${insight.suggestion}\n`));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Reset knowledge base (for testing or fresh start)
|
|
484
|
+
*/
|
|
485
|
+
reset() {
|
|
486
|
+
this.knowledgeBase = this.createEmptyKnowledgeBase();
|
|
487
|
+
this.saveKnowledgeBase();
|
|
488
|
+
console.log(chalk.yellow('⚠️ Knowledge base reset'));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Export knowledge base for analysis
|
|
493
|
+
*/
|
|
494
|
+
export() {
|
|
495
|
+
return {
|
|
496
|
+
...this.knowledgeBase,
|
|
497
|
+
summary: this.getInsightsSummary()
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Quick functions for imports
|
|
504
|
+
*/
|
|
505
|
+
export async function learnFromProject(projectPath = '.') {
|
|
506
|
+
const learner = new LearningSystem(projectPath);
|
|
507
|
+
await learner.learnFromProject();
|
|
508
|
+
return learner;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function getSuggestions(projectPath = '.', context = {}) {
|
|
512
|
+
const learner = new LearningSystem(projectPath);
|
|
513
|
+
return learner.getAllSuggestions(context);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function showInsights(projectPath = '.') {
|
|
517
|
+
const learner = new LearningSystem(projectPath);
|
|
518
|
+
learner.formatInsights();
|
|
519
|
+
return learner.getInsightsSummary();
|
|
520
|
+
}
|