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,323 +0,0 @@
1
- /**
2
- * File Watcher
3
- *
4
- * @purpose Event-driven FileWatcher component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system reads, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel File Watcher
15
- *
16
- * Real-time file system monitoring for the Orion codebase.
17
- * Detects file changes, additions, and deletions to trigger scans.
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const EventEmitter = require('events');
23
-
24
- class FileWatcher extends EventEmitter {
25
- constructor(config = {}) {
26
- super();
27
-
28
- this.config = {
29
- rootPath: config.rootPath || process.cwd(),
30
- watchPatterns: config.watchPatterns || ['**/*.js', '**/*.ts', '**/*.json'],
31
- ignorePatterns: config.ignorePatterns || [
32
- 'node_modules/**',
33
- '.git/**',
34
- 'dist/**',
35
- 'build/**',
36
- '.orion/snapshots/**',
37
- 'coverage/**',
38
- '*.log'
39
- ],
40
- debounceMs: config.debounceMs || 300,
41
- ...config
42
- };
43
-
44
- this.watchers = [];
45
- this.watchedFiles = new Set();
46
- this.debounceTimers = new Map();
47
- this.isRunning = false;
48
- }
49
-
50
- /**
51
- * Start watching files
52
- */
53
- async start() {
54
- if (this.isRunning) {
55
- return { success: true, message: 'Already watching' };
56
- }
57
-
58
- console.log('[FILE-WATCHER] Starting file watcher...');
59
- console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);
60
-
61
- try {
62
- // Discover files to watch
63
- await this._discoverFiles(this.config.rootPath);
64
-
65
- // Set up watchers
66
- this._setupWatchers();
67
-
68
- this.isRunning = true;
69
- console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`);
70
-
71
- return {
72
- success: true,
73
- filesWatched: this.watchedFiles.size
74
- };
75
- } catch (error) {
76
- console.error('[FILE-WATCHER] Start failed:', error.message);
77
- return { success: false, error: error.message };
78
- }
79
- }
80
-
81
- /**
82
- * Stop watching files
83
- */
84
- async stop() {
85
- console.log('[FILE-WATCHER] Stopping file watcher...');
86
-
87
- // Close all watchers
88
- for (const watcher of this.watchers) {
89
- try {
90
- watcher.close();
91
- } catch (e) {
92
- // Ignore close errors
93
- }
94
- }
95
-
96
- // Clear debounce timers
97
- for (const timer of this.debounceTimers.values()) {
98
- clearTimeout(timer);
99
- }
100
-
101
- this.watchers = [];
102
- this.debounceTimers.clear();
103
- this.isRunning = false;
104
-
105
- console.log('[FILE-WATCHER] Stopped');
106
- return { success: true };
107
- }
108
-
109
- /**
110
- * Get count of watched files
111
- */
112
- getWatchedCount() {
113
- return this.watchedFiles.size;
114
- }
115
-
116
- /**
117
- * Get list of watched files
118
- */
119
- getWatchedFiles() {
120
- return Array.from(this.watchedFiles);
121
- }
122
-
123
- /**
124
- * Check if a file path should be ignored
125
- */
126
- shouldIgnore(filePath) {
127
- const relativePath = path.relative(this.config.rootPath, filePath);
128
-
129
- for (const pattern of this.config.ignorePatterns) {
130
- if (this._matchPattern(relativePath, pattern)) {
131
- return true;
132
- }
133
- }
134
-
135
- return false;
136
- }
137
-
138
- /**
139
- * Check if a file path matches watch patterns
140
- */
141
- shouldWatch(filePath) {
142
- const relativePath = path.relative(this.config.rootPath, filePath);
143
-
144
- // First check if ignored
145
- if (this.shouldIgnore(filePath)) {
146
- return false;
147
- }
148
-
149
- // Then check if matches watch patterns
150
- for (const pattern of this.config.watchPatterns) {
151
- if (this._matchPattern(relativePath, pattern)) {
152
- return true;
153
- }
154
- }
155
-
156
- return false;
157
- }
158
-
159
- // Private methods
160
-
161
- async _discoverFiles(dir) {
162
- try {
163
- const entries = fs.readdirSync(dir, { withFileTypes: true });
164
-
165
- for (const entry of entries) {
166
- const fullPath = path.join(dir, entry.name);
167
-
168
- // Skip ignored directories early
169
- if (entry.isDirectory()) {
170
- if (!this.shouldIgnore(fullPath)) {
171
- await this._discoverFiles(fullPath);
172
- }
173
- } else if (entry.isFile()) {
174
- if (this.shouldWatch(fullPath)) {
175
- this.watchedFiles.add(fullPath);
176
- }
177
- }
178
- }
179
- } catch (error) {
180
- // Directory might not exist or be inaccessible
181
- if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
182
- console.warn(`[FILE-WATCHER] Error scanning ${dir}:`, error.message);
183
- }
184
- }
185
- }
186
-
187
- _setupWatchers() {
188
- // Group files by directory for efficient watching
189
- const directories = new Set();
190
- for (const file of this.watchedFiles) {
191
- directories.add(path.dirname(file));
192
- }
193
-
194
- // Watch each directory
195
- for (const dir of directories) {
196
- try {
197
- const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
198
- if (filename) {
199
- const fullPath = path.join(dir, filename);
200
- this._handleFileEvent(eventType, fullPath);
201
- }
202
- });
203
-
204
- watcher.on('error', (error) => {
205
- console.warn(`[FILE-WATCHER] Watcher error for ${dir}:`, error.message);
206
- });
207
-
208
- this.watchers.push(watcher);
209
- } catch (error) {
210
- console.warn(`[FILE-WATCHER] Could not watch ${dir}:`, error.message);
211
- }
212
- }
213
-
214
- // Also set up a recursive watcher for the root (if supported)
215
- try {
216
- const rootWatcher = fs.watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
217
- if (filename) {
218
- const fullPath = path.join(this.config.rootPath, filename);
219
- this._handleFileEvent(eventType, fullPath);
220
- }
221
- });
222
-
223
- rootWatcher.on('error', (error) => {
224
- // Recursive watch might not be supported on all platforms
225
- if (error.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
226
- console.warn('[FILE-WATCHER] Root watcher error:', error.message);
227
- }
228
- });
229
-
230
- this.watchers.push(rootWatcher);
231
- } catch (error) {
232
- // Recursive watching might not be available
233
- }
234
- }
235
-
236
- _handleFileEvent(eventType, filePath) {
237
- // Skip if ignored
238
- if (this.shouldIgnore(filePath)) {
239
- return;
240
- }
241
-
242
- // Debounce rapid events
243
- if (this.debounceTimers.has(filePath)) {
244
- clearTimeout(this.debounceTimers.get(filePath));
245
- }
246
-
247
- this.debounceTimers.set(filePath, setTimeout(() => {
248
- this.debounceTimers.delete(filePath);
249
- this._processFileEvent(eventType, filePath);
250
- }, this.config.debounceMs));
251
- }
252
-
253
- _processFileEvent(eventType, filePath) {
254
- // Check if file exists
255
- const exists = fs.existsSync(filePath);
256
- const isFile = exists && fs.statSync(filePath).isFile();
257
-
258
- let event;
259
-
260
- if (!exists) {
261
- // File was deleted
262
- if (this.watchedFiles.has(filePath)) {
263
- this.watchedFiles.delete(filePath);
264
- event = {
265
- type: 'delete',
266
- filePath,
267
- timestamp: new Date().toISOString()
268
- };
269
- }
270
- } else if (isFile && this.shouldWatch(filePath)) {
271
- if (this.watchedFiles.has(filePath)) {
272
- // File was modified
273
- event = {
274
- type: 'change',
275
- filePath,
276
- timestamp: new Date().toISOString()
277
- };
278
- } else {
279
- // New file added
280
- this.watchedFiles.add(filePath);
281
- event = {
282
- type: 'add',
283
- filePath,
284
- timestamp: new Date().toISOString()
285
- };
286
- }
287
- }
288
-
289
- if (event) {
290
- this.emit(event.type, event);
291
- this.emit('any', event);
292
- }
293
- }
294
-
295
- _matchPattern(filePath, pattern) {
296
- // Simple glob-like pattern matching
297
- // Supports: *, **, ?
298
-
299
- // Normalize separators
300
- const normalizedPath = filePath.replace(/\\/g, '/');
301
- const normalizedPattern = pattern.replace(/\\/g, '/');
302
-
303
- // Convert glob pattern to regex
304
- let regexPattern = normalizedPattern
305
- .replace(/\./g, '\\.') // Escape dots
306
- .replace(/\*\*/g, '<<<GLOBSTAR>>>') // Temporarily replace **
307
- .replace(/\*/g, '[^/]*') // * matches anything except /
308
- .replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
309
- .replace(/\?/g, '.'); // ? matches single char
310
-
311
- // Anchor the pattern
312
- regexPattern = '^' + regexPattern + '$';
313
-
314
- try {
315
- const regex = new RegExp(regexPattern);
316
- return regex.test(normalizedPath);
317
- } catch (e) {
318
- return false;
319
- }
320
- }
321
- }
322
-
323
- module.exports = FileWatcher;
@@ -1,226 +0,0 @@
1
- /**
2
- * Thuban Ghost Code Detector
3
- *
4
- * @purpose Find AI-generated functions that were pasted but never called
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for Unique feature — no other tool targets "pasted but unwired" code
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- class GhostCodeDetector {
15
- constructor(opts = {}) {
16
- this.rootPath = opts.rootPath || process.cwd();
17
- }
18
-
19
- /**
20
- * Scan files for ghost (uncalled) functions
21
- */
22
- scan(files) {
23
- const functions = []; // All function definitions
24
- const references = {}; // Function name → reference count
25
-
26
- // Pass 1: Find all function definitions
27
- for (const file of files) {
28
- let content;
29
- try {
30
- const stat = fs.statSync(file);
31
- if (stat.size > 512 * 1024) continue;
32
- content = fs.readFileSync(file, 'utf-8');
33
- } catch (e) {
34
- continue;
35
- }
36
-
37
- const relPath = path.relative(this.rootPath, file);
38
-
39
- // Skip test files, config files, and entry points
40
- if (this._isSkippable(relPath)) continue;
41
-
42
- const lines = content.split('\n');
43
-
44
- for (let i = 0; i < lines.length; i++) {
45
- const line = lines[i];
46
- const fn = this._extractFunction(line, i, lines);
47
- if (fn) {
48
- fn.file = relPath;
49
- fn.line = i + 1;
50
- fn.lineCount = this._countFunctionLines(lines, i);
51
- functions.push(fn);
52
-
53
- // Initialize reference counter
54
- if (!references[fn.name]) references[fn.name] = 0;
55
- }
56
- }
57
- }
58
-
59
- // Pass 2: Count references to each function
60
- for (const file of files) {
61
- let content;
62
- try {
63
- content = fs.readFileSync(file, 'utf-8');
64
- } catch (e) {
65
- continue;
66
- }
67
-
68
- for (const fn of functions) {
69
- if (fn.name.length < 3) continue; // Skip very short names (likely false positives)
70
-
71
- // Count how many times this function name appears (excluding its definition)
72
- const regex = new RegExp(`\\b${this._escapeRegex(fn.name)}\\b`, 'g');
73
- const matches = content.match(regex);
74
- if (matches) {
75
- const relPath = path.relative(this.rootPath, file);
76
- // Subtract 1 if this is the file where it's defined (the definition itself)
77
- const isDefFile = relPath === fn.file;
78
- references[fn.name] += matches.length - (isDefFile ? 1 : 0);
79
- }
80
- }
81
- }
82
-
83
- // Pass 3: Find ghosts — defined but never referenced elsewhere
84
- const ghosts = functions
85
- .filter(fn => {
86
- const refs = references[fn.name] || 0;
87
- // It's a ghost if:
88
- // - Referenced 0 times outside its definition
89
- // - Not an export (exports are meant to be used externally)
90
- // - Not a constructor, lifecycle method, or callback pattern
91
- return refs <= 0 && !fn.isExport && !fn.isLifecycle && fn.lineCount >= 5;
92
- })
93
- .map(fn => ({
94
- ...fn,
95
- references: references[fn.name] || 0,
96
- wastedLines: fn.lineCount,
97
- category: 'ghost',
98
- severity: fn.lineCount > 30 ? 'high' : 'medium',
99
- message: `${fn.name}() — ${fn.lineCount} lines, never called`,
100
- }))
101
- .sort((a, b) => b.wastedLines - a.wastedLines);
102
-
103
- const totalWastedLines = ghosts.reduce((sum, g) => sum + g.wastedLines, 0);
104
- const totalLines = files.reduce((sum, f) => {
105
- try { return sum + fs.readFileSync(f, 'utf-8').split('\n').length; } catch (e) { return sum; }
106
- }, 0);
107
-
108
- return {
109
- ghosts,
110
- totalGhosts: ghosts.length,
111
- totalWastedLines,
112
- wastedPercent: totalLines > 0 ? Math.round((totalWastedLines / totalLines) * 1000) / 10 : 0,
113
- totalLines,
114
- };
115
- }
116
-
117
- // ─── Private ─────────────────────────────────────────────
118
-
119
- _extractFunction(line, lineIndex, lines) {
120
- const trimmed = line.trim();
121
-
122
- // function name(
123
- let match = trimmed.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
124
- if (match) {
125
- return {
126
- name: match[1],
127
- type: 'function',
128
- isExport: this._isExported(line, lineIndex, lines),
129
- isLifecycle: this._isLifecycle(match[1]),
130
- };
131
- }
132
-
133
- // const name = (async) function
134
- match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/);
135
- if (match) {
136
- return {
137
- name: match[1],
138
- type: 'const-function',
139
- isExport: this._isExported(line, lineIndex, lines),
140
- isLifecycle: this._isLifecycle(match[1]),
141
- };
142
- }
143
-
144
- // const name = (...) =>
145
- match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/);
146
- if (match && (trimmed.includes('=>') || (lineIndex + 1 < lines.length && lines[lineIndex + 1].includes('=>')))) {
147
- return {
148
- name: match[1],
149
- type: 'arrow',
150
- isExport: this._isExported(line, lineIndex, lines),
151
- isLifecycle: this._isLifecycle(match[1]),
152
- };
153
- }
154
-
155
- // class method: name(
156
- match = trimmed.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/);
157
- if (match && !['if', 'for', 'while', 'switch', 'catch', 'constructor', 'render', 'toString'].includes(match[1])) {
158
- return {
159
- name: match[1],
160
- type: 'method',
161
- isExport: false,
162
- isLifecycle: this._isLifecycle(match[1]),
163
- };
164
- }
165
-
166
- return null;
167
- }
168
-
169
- _isExported(line, lineIndex, lines) {
170
- if (/^export\s/.test(line.trim())) return true;
171
- if (/module\.exports/.test(line)) return true;
172
-
173
- // Check if it's exported later in the file
174
- // (simplified — check last 20 lines for module.exports containing the function name)
175
- return false;
176
- }
177
-
178
- _isLifecycle(name) {
179
- const lifecycle = [
180
- 'constructor', 'render', 'componentDidMount', 'componentWillUnmount',
181
- 'componentDidUpdate', 'shouldComponentUpdate', 'getSnapshotBeforeUpdate',
182
- 'getDerivedStateFromProps', 'useEffect', 'useState', 'useMemo',
183
- 'toString', 'valueOf', 'toJSON', 'inspect',
184
- 'setup', 'teardown', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
185
- 'init', 'destroy', 'configure', 'bootstrap', 'main',
186
- 'get', 'set', 'post', 'put', 'delete', 'patch', 'handle',
187
- ];
188
- return lifecycle.includes(name) || name.startsWith('_') || name.startsWith('on');
189
- }
190
-
191
- _countFunctionLines(lines, startLine) {
192
- let depth = 0;
193
- let started = false;
194
- let count = 0;
195
-
196
- for (let i = startLine; i < lines.length && i < startLine + 200; i++) {
197
- const line = lines[i];
198
- count++;
199
-
200
- for (const ch of line) {
201
- if (ch === '{') { depth++; started = true; }
202
- if (ch === '}') depth--;
203
- }
204
-
205
- if (started && depth <= 0) break;
206
- }
207
-
208
- return count;
209
- }
210
-
211
- _isSkippable(relPath) {
212
- const skip = [
213
- /\.test\./i, /\.spec\./i, /test[\/\\]/i, /spec[\/\\]/i,
214
- /__test__/i, /__mocks__/i, /\.config\./i, /\.d\.ts$/,
215
- /index\.(js|ts)$/, /server\.(js|ts)$/, /app\.(js|ts)$/,
216
- /routes?\.(js|ts)$/, /middleware/i,
217
- ];
218
- return skip.some(r => r.test(relPath));
219
- }
220
-
221
- _escapeRegex(str) {
222
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
223
- }
224
- }
225
-
226
- module.exports = GhostCodeDetector;