thuban 0.3.2 → 0.3.3

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.
@@ -1,415 +1,415 @@
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;
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;