thuban 0.3.2 → 0.3.4
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 +137 -102
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +16 -8
- package/cli.js +0 -2412
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -713
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -250
- package/packages/scanner/dependency-graph.js +0 -536
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -226
- package/packages/scanner/hallucination-detector.js +0 -652
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -92
- package/packages/scanner/license-manager.js +0 -318
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/sentinel-core.js +0 -608
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -565
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thuban Tech Debt Cost Calculator
|
|
3
|
-
*
|
|
4
|
-
* @purpose Translate tech debt into £/hours so CTOs can see the business cost
|
|
5
|
-
* @module thuban
|
|
6
|
-
* @layer scanner
|
|
7
|
-
* @exports-to cli
|
|
8
|
-
* @critical-for Sales — CTOs buy when they see the number
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
// Cost model — conservative estimates
|
|
15
|
-
const COST_MODEL = {
|
|
16
|
-
hourlyRate: 80, // £/hr average UK developer
|
|
17
|
-
weeklyDriftHours: 3, // Hours of new debt accumulated per week per 1000 files
|
|
18
|
-
|
|
19
|
-
// Minutes to fix each issue type manually (without Thuban)
|
|
20
|
-
manualFixMinutes: {
|
|
21
|
-
hallucination: 30, // Finding + understanding + replacing a phantom API
|
|
22
|
-
deprecated: 15, // Researching replacement + updating
|
|
23
|
-
security: 45, // Security issues need careful review
|
|
24
|
-
circular_dep: 60, // Untangling circular dependencies
|
|
25
|
-
orphan_file: 5, // Reviewing and deleting
|
|
26
|
-
missing_annotation: 2, // Adding Mother Code DNA
|
|
27
|
-
copy_paste: 20, // Extracting to shared utility
|
|
28
|
-
ghost_code: 10, // Reviewing and removing dead functions
|
|
29
|
-
code_smell: 10, // General refactoring
|
|
30
|
-
drift: 15, // Re-aligning annotation with reality
|
|
31
|
-
},
|
|
32
|
-
|
|
33
|
-
// Minutes with Thuban auto-fix
|
|
34
|
-
thubanFixMinutes: {
|
|
35
|
-
hallucination: 0.5, // Auto-suggests replacement
|
|
36
|
-
deprecated: 0.5, // Auto-replaces
|
|
37
|
-
security: 5, // Flags + suggests but needs human review
|
|
38
|
-
circular_dep: 5, // Shows the cycle, suggests break point
|
|
39
|
-
orphan_file: 0.5, // Auto-flags for deletion
|
|
40
|
-
missing_annotation: 0.1, // Auto-injects
|
|
41
|
-
copy_paste: 2, // Shows cluster, suggests extraction
|
|
42
|
-
ghost_code: 0.5, // Auto-flags for removal
|
|
43
|
-
code_smell: 1, // Auto-fixes common patterns
|
|
44
|
-
drift: 0.5, // Auto-updates annotation
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
class TechDebtCostCalculator {
|
|
49
|
-
constructor(opts = {}) {
|
|
50
|
-
this.hourlyRate = opts.hourlyRate || COST_MODEL.hourlyRate;
|
|
51
|
-
this.currency = opts.currency || '£';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Calculate cost from a list of issues
|
|
56
|
-
*/
|
|
57
|
-
calculate(issues, fileCount = 0) {
|
|
58
|
-
const breakdown = {};
|
|
59
|
-
let totalManualMinutes = 0;
|
|
60
|
-
let totalThubanMinutes = 0;
|
|
61
|
-
|
|
62
|
-
for (const issue of issues) {
|
|
63
|
-
const category = this._categorize(issue);
|
|
64
|
-
if (!breakdown[category]) {
|
|
65
|
-
breakdown[category] = { count: 0, manualMinutes: 0, thubanMinutes: 0 };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const manualMin = COST_MODEL.manualFixMinutes[category] || 10;
|
|
69
|
-
const thubanMin = COST_MODEL.thubanFixMinutes[category] || 1;
|
|
70
|
-
|
|
71
|
-
breakdown[category].count++;
|
|
72
|
-
breakdown[category].manualMinutes += manualMin;
|
|
73
|
-
breakdown[category].thubanMinutes += thubanMin;
|
|
74
|
-
totalManualMinutes += manualMin;
|
|
75
|
-
totalThubanMinutes += thubanMin;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const manualHours = totalManualMinutes / 60;
|
|
79
|
-
const thubanHours = totalThubanMinutes / 60;
|
|
80
|
-
const manualCost = manualHours * this.hourlyRate;
|
|
81
|
-
const thubanCost = thubanHours * this.hourlyRate;
|
|
82
|
-
const savings = manualCost - thubanCost;
|
|
83
|
-
const savingsPercent = manualCost > 0 ? Math.round((savings / manualCost) * 100) : 0;
|
|
84
|
-
|
|
85
|
-
// Weekly debt accumulation estimate
|
|
86
|
-
const weeklyNewDebt = (fileCount / 1000) * COST_MODEL.weeklyDriftHours;
|
|
87
|
-
const monthlyNewDebtCost = weeklyNewDebt * 4 * this.hourlyRate;
|
|
88
|
-
|
|
89
|
-
// ROI calculation
|
|
90
|
-
const thubanMonthlyCost = 19; // Pro tier
|
|
91
|
-
const monthlyROI = savings > 0 ? Math.round(savings / thubanMonthlyCost) : 0;
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
summary: {
|
|
95
|
-
totalIssues: issues.length,
|
|
96
|
-
manualHours: Math.round(manualHours * 10) / 10,
|
|
97
|
-
manualCost: Math.round(manualCost),
|
|
98
|
-
thubanHours: Math.round(thubanHours * 10) / 10,
|
|
99
|
-
thubanCost: Math.round(thubanCost),
|
|
100
|
-
savings: Math.round(savings),
|
|
101
|
-
savingsPercent,
|
|
102
|
-
timeSaved: this._formatTime(totalManualMinutes - totalThubanMinutes),
|
|
103
|
-
},
|
|
104
|
-
breakdown: Object.entries(breakdown).map(([category, data]) => ({
|
|
105
|
-
category,
|
|
106
|
-
count: data.count,
|
|
107
|
-
manualTime: this._formatTime(data.manualMinutes),
|
|
108
|
-
thubanTime: this._formatTime(data.thubanMinutes),
|
|
109
|
-
manualCost: Math.round((data.manualMinutes / 60) * this.hourlyRate),
|
|
110
|
-
thubanCost: Math.round((data.thubanMinutes / 60) * this.hourlyRate),
|
|
111
|
-
})).sort((a, b) => b.manualCost - a.manualCost),
|
|
112
|
-
projection: {
|
|
113
|
-
weeklyNewDebtHours: Math.round(weeklyNewDebt * 10) / 10,
|
|
114
|
-
monthlyNewDebtCost: Math.round(monthlyNewDebtCost),
|
|
115
|
-
monthlyThubanCost: thubanMonthlyCost,
|
|
116
|
-
monthlyROI: `${monthlyROI}x`,
|
|
117
|
-
annualSavings: Math.round(savings + (monthlyNewDebtCost * 12)),
|
|
118
|
-
verdict: monthlyROI >= 5
|
|
119
|
-
? 'Thuban pays for itself within the first scan.'
|
|
120
|
-
: monthlyROI >= 2
|
|
121
|
-
? 'Thuban pays for itself within the first week.'
|
|
122
|
-
: 'Thuban will save you money within the first month.',
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Format for CLI output
|
|
129
|
-
*/
|
|
130
|
-
formatCLI(result) {
|
|
131
|
-
const lines = [];
|
|
132
|
-
const s = result.summary;
|
|
133
|
-
const p = result.projection;
|
|
134
|
-
const c = this.currency;
|
|
135
|
-
|
|
136
|
-
lines.push('');
|
|
137
|
-
lines.push(' TECH DEBT COST ESTIMATE');
|
|
138
|
-
lines.push('');
|
|
139
|
-
lines.push(` Current debt: ${s.manualHours} developer-hours to fix manually`);
|
|
140
|
-
lines.push(` At ${c}${this.hourlyRate}/hr: ${c}${s.manualCost.toLocaleString()} if you fix it yourself`);
|
|
141
|
-
lines.push(` With Thuban: ${s.thubanHours} hours (${s.timeSaved} saved)`);
|
|
142
|
-
lines.push(` You save: ${c}${s.savings.toLocaleString()} (${s.savingsPercent}%)`);
|
|
143
|
-
lines.push('');
|
|
144
|
-
|
|
145
|
-
// Breakdown table
|
|
146
|
-
lines.push(' BREAKDOWN');
|
|
147
|
-
lines.push(` ${'Category'.padEnd(20)} ${'Count'.padEnd(8)} ${'Manual'.padEnd(12)} ${'Thuban'.padEnd(12)} ${'Saving'.padEnd(10)}`);
|
|
148
|
-
lines.push(' ' + '-'.repeat(62));
|
|
149
|
-
for (const b of result.breakdown) {
|
|
150
|
-
const saving = `${c}${b.manualCost - b.thubanCost}`;
|
|
151
|
-
lines.push(` ${b.category.padEnd(20)} ${String(b.count).padEnd(8)} ${(c + b.manualCost).padEnd(12)} ${(c + b.thubanCost).padEnd(12)} ${saving.padEnd(10)}`);
|
|
152
|
-
}
|
|
153
|
-
lines.push('');
|
|
154
|
-
|
|
155
|
-
// Projection
|
|
156
|
-
lines.push(' PROJECTION');
|
|
157
|
-
lines.push(` Every week you wait, ${p.weeklyNewDebtHours} hours of new debt accumulates.`);
|
|
158
|
-
lines.push(` That's ${c}${p.monthlyNewDebtCost}/month in growing technical debt.`);
|
|
159
|
-
lines.push(` Thuban Pro costs ${c}${p.monthlyThubanCost}/month. ROI: ${p.monthlyROI}.`);
|
|
160
|
-
lines.push('');
|
|
161
|
-
lines.push(` ${p.verdict}`);
|
|
162
|
-
lines.push('');
|
|
163
|
-
|
|
164
|
-
return lines.join('\n');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ─── Private ─────────────────────────────────────────────
|
|
168
|
-
|
|
169
|
-
_categorize(issue) {
|
|
170
|
-
const cat = (issue.category || '').toLowerCase();
|
|
171
|
-
const id = (issue.id || '').toLowerCase();
|
|
172
|
-
|
|
173
|
-
if (cat === 'hallucination' || id.startsWith('hall')) return 'hallucination';
|
|
174
|
-
if (cat === 'deprecated' || id.startsWith('depr')) return 'deprecated';
|
|
175
|
-
if (cat === 'security') return 'security';
|
|
176
|
-
if (cat === 'circular' || id.includes('circular')) return 'circular_dep';
|
|
177
|
-
if (cat === 'orphan' || id.includes('orphan')) return 'orphan_file';
|
|
178
|
-
if (cat === 'drift') return 'drift';
|
|
179
|
-
if (cat === 'ghost' || id.includes('ghost')) return 'ghost_code';
|
|
180
|
-
if (cat === 'copy_paste' || id.includes('duplicate')) return 'copy_paste';
|
|
181
|
-
if (cat === 'annotation' || id.includes('mother')) return 'missing_annotation';
|
|
182
|
-
return 'code_smell';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
_formatTime(minutes) {
|
|
186
|
-
if (minutes < 60) return `${Math.round(minutes)} mins`;
|
|
187
|
-
const hours = Math.floor(minutes / 60);
|
|
188
|
-
const mins = Math.round(minutes % 60);
|
|
189
|
-
if (mins === 0) return `${hours}h`;
|
|
190
|
-
return `${hours}h ${mins}m`;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = TechDebtCostCalculator;
|
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sentinel Widget Generator
|
|
3
|
-
*
|
|
4
|
-
* Automatically generates widget contracts for files that don't have them.
|
|
5
|
-
* Uses dependency graph, code analysis, and file path heuristics to
|
|
6
|
-
* create meaningful @purpose, @module, @layer, etc. annotations.
|
|
7
|
-
*
|
|
8
|
-
* @purpose Generate widget contracts for undocumented files
|
|
9
|
-
* @module sentinel
|
|
10
|
-
* @layer infrastructure
|
|
11
|
-
* @imports-from dependency-graph, fs, path
|
|
12
|
-
* @exports-to sentinel-core, CLI tools
|
|
13
|
-
* @side-effects File system reads; optionally writes widgets to files
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const DependencyGraph = require('./dependency-graph');
|
|
19
|
-
|
|
20
|
-
class WidgetGenerator {
|
|
21
|
-
constructor(config = {}) {
|
|
22
|
-
this.config = {
|
|
23
|
-
rootPath: config.rootPath || process.cwd(),
|
|
24
|
-
dryRun: config.dryRun !== false, // Default to dry run (don't write)
|
|
25
|
-
...config
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
this.dependencyGraph = config.dependencyGraph || null;
|
|
29
|
-
|
|
30
|
-
// Module detection patterns
|
|
31
|
-
this.modulePatterns = {
|
|
32
|
-
'sentinel': /^sentinel[\\\/]/,
|
|
33
|
-
'citadel': /^citadel[\\\/]/,
|
|
34
|
-
'forge': /^forge[\\\/]/,
|
|
35
|
-
'server': /^server[\\\/]/,
|
|
36
|
-
'build-engine': /^build-engine[\\\/]/,
|
|
37
|
-
'autopilot': /^autopilot[\\\/]/,
|
|
38
|
-
'silverwings': /^silverwings[\\\/]/,
|
|
39
|
-
'output': /^output[\\\/]/
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Layer detection patterns
|
|
43
|
-
this.layerPatterns = {
|
|
44
|
-
'presentation': /routes|api|handlers|controllers|views/i,
|
|
45
|
-
'application': /service|manager|orchestrat|core|engine/i,
|
|
46
|
-
'domain': /model|entity|domain|types/i,
|
|
47
|
-
'infrastructure': /utils|helpers|lib|data|store|memory/i
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Side effect patterns
|
|
51
|
-
this.sideEffectPatterns = [
|
|
52
|
-
{ pattern: /fs\.(write|append|unlink|mkdir|rmdir|rename)/i, effect: 'File system writes' },
|
|
53
|
-
{ pattern: /fs\.read/i, effect: 'File system reads' },
|
|
54
|
-
{ pattern: /fetch\(|axios\.|http\.|https\./i, effect: 'HTTP requests' },
|
|
55
|
-
{ pattern: /process\.exit/i, effect: 'Process termination' },
|
|
56
|
-
{ pattern: /child_process|spawn|exec\(/i, effect: 'Spawns child processes' },
|
|
57
|
-
{ pattern: /console\.(log|error|warn)/i, effect: 'Console output' },
|
|
58
|
-
{ pattern: /\.emit\(|EventEmitter/i, effect: 'Event emission' },
|
|
59
|
-
{ pattern: /supabase|createClient/i, effect: 'Database operations' },
|
|
60
|
-
{ pattern: /twilio|sendgrid|resend/i, effect: 'External API calls' }
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
// Purpose detection from common patterns
|
|
64
|
-
this.purposePatterns = [
|
|
65
|
-
{ pattern: /class\s+(\w+).*extends.*Router/i, purpose: (m) => `Express router for ${m[1]}` },
|
|
66
|
-
{ pattern: /class\s+(\w+).*extends.*EventEmitter/i, purpose: (m) => `Event-driven ${m[1]} component` },
|
|
67
|
-
{ pattern: /class\s+(\w+)/i, purpose: (m) => `${m[1]} class implementation` },
|
|
68
|
-
{ pattern: /module\.exports\s*=\s*\{([^}]+)\}/i, purpose: (m) => `Exports: ${m[1].match(/\w+/g)?.slice(0, 5).join(', ')}` },
|
|
69
|
-
{ pattern: /router\.(get|post|put|delete)/i, purpose: () => 'API route handlers' },
|
|
70
|
-
{ pattern: /async function\s+(\w+)/i, purpose: (m) => `Provides ${m[1]} functionality` },
|
|
71
|
-
{ pattern: /function\s+(\w+)/i, purpose: (m) => `Provides ${m[1]} functionality` }
|
|
72
|
-
];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generate widgets for all files missing them
|
|
77
|
-
*/
|
|
78
|
-
async generateAll() {
|
|
79
|
-
console.log('[WIDGET-GENERATOR] Starting widget generation...');
|
|
80
|
-
|
|
81
|
-
// Build dependency graph if not provided
|
|
82
|
-
if (!this.dependencyGraph) {
|
|
83
|
-
this.dependencyGraph = new DependencyGraph({ rootPath: this.config.rootPath });
|
|
84
|
-
await this.dependencyGraph.build();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Find files without widgets
|
|
88
|
-
const files = await this._discoverFiles(this.config.rootPath);
|
|
89
|
-
const results = {
|
|
90
|
-
total: files.length,
|
|
91
|
-
generated: 0,
|
|
92
|
-
skipped: 0,
|
|
93
|
-
alreadyHasWidget: 0,
|
|
94
|
-
widgets: []
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
for (const file of files) {
|
|
98
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
99
|
-
|
|
100
|
-
// Skip if already has widget
|
|
101
|
-
if (this._hasWidget(content)) {
|
|
102
|
-
results.alreadyHasWidget++;
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const widget = await this.generateWidget(file, content);
|
|
107
|
-
results.generated++;
|
|
108
|
-
results.widgets.push({
|
|
109
|
-
file,
|
|
110
|
-
relativePath: path.relative(this.config.rootPath, file),
|
|
111
|
-
widget
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Apply widget if not dry run
|
|
115
|
-
if (!this.config.dryRun) {
|
|
116
|
-
await this._applyWidget(file, content, widget);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
console.log(`[WIDGET-GENERATOR] Generated ${results.generated} widgets (${results.alreadyHasWidget} already had widgets)`);
|
|
121
|
-
return results;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Generate widget for a single file
|
|
126
|
-
*/
|
|
127
|
-
async generateWidget(filePath, content = null) {
|
|
128
|
-
if (!content) {
|
|
129
|
-
content = fs.readFileSync(filePath, 'utf8');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const relativePath = path.relative(this.config.rootPath, filePath);
|
|
133
|
-
const fileName = path.basename(filePath, path.extname(filePath));
|
|
134
|
-
|
|
135
|
-
// Analyze file
|
|
136
|
-
const analysis = {
|
|
137
|
-
purpose: this._detectPurpose(content, fileName, relativePath),
|
|
138
|
-
module: this._detectModule(relativePath),
|
|
139
|
-
layer: this._detectLayer(relativePath, content),
|
|
140
|
-
importsFrom: this._getImportsFrom(filePath),
|
|
141
|
-
exportsTo: this._getExportsTo(filePath),
|
|
142
|
-
sideEffects: this._detectSideEffects(content),
|
|
143
|
-
criticalFor: this._detectCriticalFor(filePath)
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Build widget string
|
|
147
|
-
const widget = this._buildWidgetString(analysis, fileName);
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
analysis,
|
|
151
|
-
widgetString: widget,
|
|
152
|
-
fileName,
|
|
153
|
-
relativePath
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Preview widgets for all files (dry run report)
|
|
159
|
-
*/
|
|
160
|
-
async preview(limit = 20) {
|
|
161
|
-
const results = await this.generateAll();
|
|
162
|
-
|
|
163
|
-
console.log('\n' + '='.repeat(70));
|
|
164
|
-
console.log('WIDGET GENERATION PREVIEW');
|
|
165
|
-
console.log('='.repeat(70));
|
|
166
|
-
console.log(`Total files: ${results.total}`);
|
|
167
|
-
console.log(`Already have widgets: ${results.alreadyHasWidget}`);
|
|
168
|
-
console.log(`Will generate: ${results.generated}`);
|
|
169
|
-
console.log('');
|
|
170
|
-
|
|
171
|
-
for (const item of results.widgets.slice(0, limit)) {
|
|
172
|
-
console.log('-'.repeat(70));
|
|
173
|
-
console.log(`FILE: ${item.relativePath}`);
|
|
174
|
-
console.log('-'.repeat(70));
|
|
175
|
-
console.log(item.widget.widgetString);
|
|
176
|
-
console.log('');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (results.widgets.length > limit) {
|
|
180
|
-
console.log(`... and ${results.widgets.length - limit} more files`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return results;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Apply widgets to files (actually writes them)
|
|
188
|
-
*/
|
|
189
|
-
async apply() {
|
|
190
|
-
this.config.dryRun = false;
|
|
191
|
-
const results = await this.generateAll();
|
|
192
|
-
console.log(`[WIDGET-GENERATOR] Applied ${results.generated} widgets to files`);
|
|
193
|
-
return results;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Private methods
|
|
197
|
-
|
|
198
|
-
_hasWidget(content) {
|
|
199
|
-
// Check for existing widget with @purpose, @module, or @layer
|
|
200
|
-
const widgetPattern = /\/\*\*[\s\S]*?@(purpose|module|layer)[\s\S]*?\*\//;
|
|
201
|
-
return widgetPattern.test(content);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
_detectPurpose(content, fileName, relativePath) {
|
|
205
|
-
// Try pattern matching first
|
|
206
|
-
for (const { pattern, purpose } of this.purposePatterns) {
|
|
207
|
-
const match = content.match(pattern);
|
|
208
|
-
if (match) {
|
|
209
|
-
return typeof purpose === 'function' ? purpose(match) : purpose;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Fall back to file name analysis
|
|
214
|
-
const nameWords = fileName.replace(/[-_]/g, ' ').replace(/([A-Z])/g, ' $1').trim();
|
|
215
|
-
|
|
216
|
-
// Common suffixes
|
|
217
|
-
if (fileName.includes('route')) return `Route handlers for ${nameWords}`;
|
|
218
|
-
if (fileName.includes('api')) return `API endpoints for ${nameWords}`;
|
|
219
|
-
if (fileName.includes('service')) return `Service layer for ${nameWords}`;
|
|
220
|
-
if (fileName.includes('manager')) return `Manager for ${nameWords}`;
|
|
221
|
-
if (fileName.includes('store')) return `State store for ${nameWords}`;
|
|
222
|
-
if (fileName.includes('util')) return `Utility functions for ${nameWords}`;
|
|
223
|
-
if (fileName.includes('helper')) return `Helper functions for ${nameWords}`;
|
|
224
|
-
if (fileName.includes('test')) return `Tests for ${nameWords}`;
|
|
225
|
-
if (fileName.includes('config')) return `Configuration for ${nameWords}`;
|
|
226
|
-
if (fileName.includes('index')) return `Entry point for ${path.dirname(relativePath).split(path.sep).pop()}`;
|
|
227
|
-
|
|
228
|
-
return `Provides ${nameWords} functionality`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
_detectModule(relativePath) {
|
|
232
|
-
const normalized = relativePath.replace(/\\/g, '/');
|
|
233
|
-
|
|
234
|
-
for (const [module, pattern] of Object.entries(this.modulePatterns)) {
|
|
235
|
-
if (pattern.test(normalized)) {
|
|
236
|
-
return module;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Use first directory as module
|
|
241
|
-
const parts = normalized.split('/');
|
|
242
|
-
return parts[0] || 'root';
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
_detectLayer(relativePath, content) {
|
|
246
|
-
// Check path patterns
|
|
247
|
-
for (const [layer, pattern] of Object.entries(this.layerPatterns)) {
|
|
248
|
-
if (pattern.test(relativePath)) {
|
|
249
|
-
return layer;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check content patterns
|
|
254
|
-
if (/router\.(get|post|put|delete)|app\.(get|post)/i.test(content)) {
|
|
255
|
-
return 'presentation';
|
|
256
|
-
}
|
|
257
|
-
if (/class.*Service|class.*Manager/i.test(content)) {
|
|
258
|
-
return 'application';
|
|
259
|
-
}
|
|
260
|
-
if (/class.*Model|schema|entity/i.test(content)) {
|
|
261
|
-
return 'domain';
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
return 'application'; // Default
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
_getImportsFrom(filePath) {
|
|
268
|
-
if (!this.dependencyGraph) return [];
|
|
269
|
-
|
|
270
|
-
const deps = this.dependencyGraph.getDependencies(filePath);
|
|
271
|
-
const imports = [];
|
|
272
|
-
|
|
273
|
-
for (const dep of deps.slice(0, 10)) { // Limit to 10
|
|
274
|
-
if (dep.resolved) {
|
|
275
|
-
const relPath = path.relative(this.config.rootPath, dep.resolved);
|
|
276
|
-
const moduleParts = relPath.split(path.sep);
|
|
277
|
-
const module = moduleParts[0];
|
|
278
|
-
const file = moduleParts[moduleParts.length - 1];
|
|
279
|
-
imports.push(`${module}/${file}`);
|
|
280
|
-
} else if (!dep.module.startsWith('.')) {
|
|
281
|
-
// External module
|
|
282
|
-
imports.push(dep.module);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return imports;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
_getExportsTo(filePath) {
|
|
290
|
-
if (!this.dependencyGraph) return [];
|
|
291
|
-
|
|
292
|
-
const dependents = this.dependencyGraph.getDependents(filePath);
|
|
293
|
-
const exports = [];
|
|
294
|
-
|
|
295
|
-
for (const dep of dependents.slice(0, 10)) { // Limit to 10
|
|
296
|
-
const relPath = path.relative(this.config.rootPath, dep);
|
|
297
|
-
const moduleParts = relPath.split(path.sep);
|
|
298
|
-
const module = moduleParts[0];
|
|
299
|
-
exports.push(module);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Deduplicate
|
|
303
|
-
return [...new Set(exports)];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
_detectSideEffects(content) {
|
|
307
|
-
const effects = [];
|
|
308
|
-
|
|
309
|
-
for (const { pattern, effect } of this.sideEffectPatterns) {
|
|
310
|
-
if (pattern.test(content)) {
|
|
311
|
-
effects.push(effect);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return effects.length > 0 ? effects : ['None detected'];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
_detectCriticalFor(filePath) {
|
|
319
|
-
if (!this.dependencyGraph) return [];
|
|
320
|
-
|
|
321
|
-
const impact = this.dependencyGraph.getImpactAnalysis(filePath);
|
|
322
|
-
|
|
323
|
-
if (impact.riskLevel === 'critical') {
|
|
324
|
-
return [`CRITICAL: ${impact.totalImpact} files depend on this`];
|
|
325
|
-
}
|
|
326
|
-
if (impact.riskLevel === 'high') {
|
|
327
|
-
return [`High impact: ${impact.totalImpact} files depend on this`];
|
|
328
|
-
}
|
|
329
|
-
if (impact.affectedModules.length > 0) {
|
|
330
|
-
return impact.affectedModules.slice(0, 5);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return [];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
_buildWidgetString(analysis, fileName) {
|
|
337
|
-
const lines = [
|
|
338
|
-
'/**',
|
|
339
|
-
` * ${this._toTitleCase(fileName)}`,
|
|
340
|
-
' *',
|
|
341
|
-
` * @purpose ${analysis.purpose}`,
|
|
342
|
-
` * @module ${analysis.module}`,
|
|
343
|
-
` * @layer ${analysis.layer}`
|
|
344
|
-
];
|
|
345
|
-
|
|
346
|
-
if (analysis.importsFrom.length > 0) {
|
|
347
|
-
lines.push(` * @imports-from ${analysis.importsFrom.join(', ')}`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (analysis.exportsTo.length > 0) {
|
|
351
|
-
lines.push(` * @exports-to ${analysis.exportsTo.join(', ')}`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
lines.push(` * @side-effects ${analysis.sideEffects.join(', ')}`);
|
|
355
|
-
|
|
356
|
-
if (analysis.criticalFor.length > 0) {
|
|
357
|
-
lines.push(` * @critical-for ${analysis.criticalFor.join(', ')}`);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
lines.push(' */');
|
|
361
|
-
|
|
362
|
-
return lines.join('\n');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
_toTitleCase(str) {
|
|
366
|
-
return str
|
|
367
|
-
.replace(/[-_]/g, ' ')
|
|
368
|
-
.replace(/([A-Z])/g, ' $1')
|
|
369
|
-
.trim()
|
|
370
|
-
.split(' ')
|
|
371
|
-
.map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
372
|
-
.join(' ');
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async _applyWidget(filePath, content, widget) {
|
|
376
|
-
// Prepend widget to file content
|
|
377
|
-
const newContent = widget.widgetString + '\n\n' + content;
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
381
|
-
return true;
|
|
382
|
-
} catch (error) {
|
|
383
|
-
console.error(`[WIDGET-GENERATOR] Failed to write widget to ${filePath}:`, error.message);
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async _discoverFiles(dir, files = []) {
|
|
389
|
-
const ignorePatterns = ['node_modules', '.git', 'dist', '.vite', '.orion'];
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
393
|
-
|
|
394
|
-
for (const entry of entries) {
|
|
395
|
-
if (ignorePatterns.some(p => entry.name === p || entry.name.startsWith(p))) {
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const fullPath = path.join(dir, entry.name);
|
|
400
|
-
|
|
401
|
-
if (entry.isDirectory()) {
|
|
402
|
-
await this._discoverFiles(fullPath, files);
|
|
403
|
-
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
404
|
-
files.push(fullPath);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
} catch (error) {
|
|
408
|
-
// Skip inaccessible directories
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return files;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
module.exports = WidgetGenerator;
|