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.
Files changed (59) hide show
  1. package/README.md +137 -102
  2. package/dist/LICENSE +21 -0
  3. package/dist/README.md +185 -0
  4. package/dist/cli.js +2 -0
  5. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  6. package/dist/packages/scanner/alert-manager.js +1 -0
  7. package/dist/packages/scanner/baseline-manager.js +1 -0
  8. package/dist/packages/scanner/code-scanner.js +1 -0
  9. package/dist/packages/scanner/codebase-passport.js +1 -0
  10. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  11. package/dist/packages/scanner/dependency-graph.js +1 -0
  12. package/dist/packages/scanner/drift-detector.js +1 -0
  13. package/dist/packages/scanner/executive-report.js +1 -0
  14. package/dist/packages/scanner/file-collector.js +1 -0
  15. package/dist/packages/scanner/file-watcher.js +1 -0
  16. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  17. package/dist/packages/scanner/hallucination-detector.js +1 -0
  18. package/dist/packages/scanner/health-checker.js +1 -0
  19. package/dist/packages/scanner/html-report.js +1 -0
  20. package/dist/packages/scanner/index.js +1 -0
  21. package/dist/packages/scanner/license-manager.js +1 -0
  22. package/dist/packages/scanner/master-health-checker.js +1 -0
  23. package/dist/packages/scanner/monitor-notifier.js +1 -0
  24. package/dist/packages/scanner/monitor-service.js +1 -0
  25. package/dist/packages/scanner/monitor-store.js +1 -0
  26. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  27. package/dist/packages/scanner/scan-diff.js +1 -0
  28. package/dist/packages/scanner/scan-runner.js +1 -0
  29. package/dist/packages/scanner/secret-scanner.js +1 -0
  30. package/dist/packages/scanner/sentinel-core.js +1 -0
  31. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  32. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  33. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  34. package/dist/packages/scanner/widget-generator.js +1 -0
  35. package/package.json +16 -8
  36. package/cli.js +0 -2412
  37. package/packages/scanner/ai-confidence-scorer.js +0 -260
  38. package/packages/scanner/alert-manager.js +0 -398
  39. package/packages/scanner/baseline-manager.js +0 -109
  40. package/packages/scanner/code-scanner.js +0 -713
  41. package/packages/scanner/codebase-passport.js +0 -299
  42. package/packages/scanner/copy-paste-detector.js +0 -250
  43. package/packages/scanner/dependency-graph.js +0 -536
  44. package/packages/scanner/drift-detector.js +0 -423
  45. package/packages/scanner/executive-report.js +0 -774
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -226
  48. package/packages/scanner/hallucination-detector.js +0 -652
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -92
  52. package/packages/scanner/license-manager.js +0 -318
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/pre-commit-gate.js +0 -216
  55. package/packages/scanner/sentinel-core.js +0 -608
  56. package/packages/scanner/sentinel-knowledge.js +0 -322
  57. package/packages/scanner/tech-debt-analyzer.js +0 -565
  58. package/packages/scanner/tech-debt-cost.js +0 -194
  59. 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;