thuban 0.3.3 → 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 (66) hide show
  1. package/dist/LICENSE +21 -0
  2. package/dist/README.md +185 -0
  3. package/dist/cli.js +2 -0
  4. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  5. package/dist/packages/scanner/alert-manager.js +1 -0
  6. package/dist/packages/scanner/baseline-manager.js +1 -0
  7. package/dist/packages/scanner/code-scanner.js +1 -0
  8. package/dist/packages/scanner/codebase-passport.js +1 -0
  9. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  10. package/dist/packages/scanner/dependency-graph.js +1 -0
  11. package/dist/packages/scanner/drift-detector.js +1 -0
  12. package/dist/packages/scanner/executive-report.js +1 -0
  13. package/dist/packages/scanner/file-collector.js +1 -0
  14. package/dist/packages/scanner/file-watcher.js +1 -0
  15. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  16. package/dist/packages/scanner/hallucination-detector.js +1 -0
  17. package/dist/packages/scanner/health-checker.js +1 -0
  18. package/dist/packages/scanner/html-report.js +1 -0
  19. package/dist/packages/scanner/index.js +1 -0
  20. package/dist/packages/scanner/license-manager.js +1 -0
  21. package/dist/packages/scanner/master-health-checker.js +1 -0
  22. package/dist/packages/scanner/monitor-notifier.js +1 -0
  23. package/dist/packages/scanner/monitor-service.js +1 -0
  24. package/dist/packages/scanner/monitor-store.js +1 -0
  25. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  26. package/dist/packages/scanner/scan-diff.js +1 -0
  27. package/dist/packages/scanner/scan-runner.js +1 -0
  28. package/dist/packages/scanner/secret-scanner.js +1 -0
  29. package/dist/packages/scanner/sentinel-core.js +1 -0
  30. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  31. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  32. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  33. package/dist/packages/scanner/widget-generator.js +1 -0
  34. package/package.json +12 -7
  35. package/cli.js +0 -2627
  36. package/packages/scanner/ai-confidence-scorer.js +0 -260
  37. package/packages/scanner/alert-manager.js +0 -398
  38. package/packages/scanner/baseline-manager.js +0 -109
  39. package/packages/scanner/code-scanner.js +0 -758
  40. package/packages/scanner/codebase-passport.js +0 -299
  41. package/packages/scanner/copy-paste-detector.js +0 -276
  42. package/packages/scanner/dependency-graph.js +0 -541
  43. package/packages/scanner/drift-detector.js +0 -423
  44. package/packages/scanner/executive-report.js +0 -774
  45. package/packages/scanner/file-collector.js +0 -64
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -301
  48. package/packages/scanner/hallucination-detector.js +0 -822
  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 -100
  52. package/packages/scanner/license-manager.js +0 -331
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/monitor-notifier.js +0 -64
  55. package/packages/scanner/monitor-service.js +0 -103
  56. package/packages/scanner/monitor-store.js +0 -117
  57. package/packages/scanner/pre-commit-gate.js +0 -216
  58. package/packages/scanner/scan-diff.js +0 -50
  59. package/packages/scanner/scan-runner.js +0 -172
  60. package/packages/scanner/secret-scanner.js +0 -612
  61. package/packages/scanner/secret-scanner.test.js +0 -103
  62. package/packages/scanner/sentinel-core.js +0 -616
  63. package/packages/scanner/sentinel-knowledge.js +0 -322
  64. package/packages/scanner/tech-debt-analyzer.js +0 -583
  65. package/packages/scanner/tech-debt-cost.js +0 -194
  66. package/packages/scanner/widget-generator.js +0 -415
@@ -1,299 +0,0 @@
1
- /**
2
- * Thuban Codebase Passport Generator
3
- *
4
- * @purpose Generate a single JSON identity file for the entire codebase
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for Onboarding — new devs understand the codebase in minutes not weeks
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const crypto = require('crypto');
14
-
15
- class CodebasePassport {
16
- constructor(opts = {}) {
17
- this.rootPath = opts.rootPath || process.cwd();
18
- }
19
-
20
- /**
21
- * Generate the passport from scan results
22
- */
23
- generate(files, opts = {}) {
24
- const startTime = Date.now();
25
- const fileMap = [];
26
- const dependencies = {};
27
- const exports = {};
28
- const entryPoints = [];
29
- const sacredFiles = [];
30
- const riskAreas = [];
31
- let totalLines = 0;
32
- const languages = {};
33
- const frameworks = new Set();
34
-
35
- for (const file of files) {
36
- let content;
37
- try {
38
- const stat = fs.statSync(file);
39
- if (stat.size > 1024 * 1024) continue;
40
- content = fs.readFileSync(file, 'utf-8');
41
- } catch (e) {
42
- continue;
43
- }
44
-
45
- const relPath = path.relative(this.rootPath, file);
46
- const lines = content.split('\n');
47
- totalLines += lines.length;
48
-
49
- // Detect language
50
- const ext = path.extname(file);
51
- languages[ext] = (languages[ext] || 0) + lines.length;
52
-
53
- // Detect frameworks
54
- this._detectFrameworks(content, frameworks);
55
-
56
- // Map imports and exports
57
- const fileDeps = this._extractDependencies(content, relPath);
58
- const fileExports = this._extractExports(content, relPath);
59
-
60
- dependencies[relPath] = fileDeps;
61
- if (fileExports.length > 0) exports[relPath] = fileExports;
62
-
63
- // Detect entry points
64
- if (this._isEntryPoint(relPath, content)) {
65
- entryPoints.push(relPath);
66
- }
67
-
68
- // Detect sacred files (high dependency count = risky to change)
69
- const importedByCount = files.filter(f => {
70
- try {
71
- const c = fs.readFileSync(f, 'utf-8');
72
- return c.includes(relPath.replace(/\\/g, '/')) || c.includes(relPath.replace(/\//g, '\\'));
73
- } catch (e) { return false; }
74
- }).length;
75
-
76
- if (importedByCount > 5) {
77
- sacredFiles.push({
78
- file: relPath,
79
- importedBy: importedByCount,
80
- risk: importedByCount > 10 ? 'critical' : 'high',
81
- note: `Changing this file affects ${importedByCount} other files. Proceed with caution.`,
82
- });
83
- }
84
-
85
- // File purpose summary
86
- const purpose = this._detectPurpose(relPath, content);
87
- fileMap.push({
88
- path: relPath,
89
- lines: lines.length,
90
- purpose,
91
- imports: fileDeps.length,
92
- exports: fileExports.length,
93
- });
94
- }
95
-
96
- // Build dependency graph summary
97
- const depChains = this._findCriticalPaths(dependencies);
98
-
99
- // Detect architecture pattern
100
- const architecture = this._detectArchitecture(fileMap);
101
-
102
- const passport = {
103
- _version: '1.0.0',
104
- _generated: new Date().toISOString(),
105
- _generator: 'thuban',
106
-
107
- project: {
108
- name: path.basename(this.rootPath),
109
- rootPath: this.rootPath,
110
- totalFiles: files.length,
111
- totalLines,
112
- languages: Object.entries(languages)
113
- .sort((a, b) => b[1] - a[1])
114
- .map(([ext, lines]) => ({ extension: ext, lines, percentage: Math.round((lines / totalLines) * 100) })),
115
- frameworks: [...frameworks],
116
- architecture,
117
- entryPoints,
118
- },
119
-
120
- files: fileMap
121
- .sort((a, b) => b.imports - a.imports)
122
- .slice(0, 200), // Cap to prevent huge passports
123
-
124
- sacred: sacredFiles
125
- .sort((a, b) => b.importedBy - a.importedBy),
126
-
127
- dependencies: {
128
- totalChains: Object.keys(dependencies).length,
129
- criticalPaths: depChains.slice(0, 20),
130
- circularRisks: this._findCircularRisks(dependencies),
131
- },
132
-
133
- onboarding: {
134
- estimatedReadTimeMinutes: Math.ceil(totalLines / 200), // ~200 lines/min for scanning
135
- startHere: entryPoints.slice(0, 5),
136
- keyFiles: sacredFiles.slice(0, 10).map(s => s.file),
137
- doNotTouch: sacredFiles.filter(s => s.risk === 'critical').map(s => s.file),
138
- quickSummary: this._generateQuickSummary(fileMap, frameworks, architecture),
139
- },
140
-
141
- generatedIn: Date.now() - startTime + 'ms',
142
- };
143
-
144
- return passport;
145
- }
146
-
147
- /**
148
- * Save passport to file
149
- */
150
- save(passport, outputPath) {
151
- const filePath = outputPath || path.join(this.rootPath, '.thuban-passport.json');
152
- fs.writeFileSync(filePath, JSON.stringify(passport, null, 2), 'utf-8');
153
- return filePath;
154
- }
155
-
156
- // ─── Private ─────────────────────────────────────────────
157
-
158
- _extractDependencies(content, filePath) {
159
- const deps = [];
160
- const patterns = [
161
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
162
- /import\s+.*?from\s+['"]([^'"]+)['"]/g,
163
- /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
164
- ];
165
-
166
- for (const pattern of patterns) {
167
- let match;
168
- while ((match = pattern.exec(content)) !== null) {
169
- deps.push(match[1]);
170
- }
171
- }
172
-
173
- return [...new Set(deps)];
174
- }
175
-
176
- _extractExports(content, filePath) {
177
- const exports = [];
178
-
179
- // module.exports
180
- const moduleExports = content.match(/module\.exports\s*=\s*(\{[\s\S]*?\}|[a-zA-Z_$]\w*)/);
181
- if (moduleExports) exports.push('module.exports');
182
-
183
- // Named exports
184
- const namedExports = content.match(/exports\.([a-zA-Z_$]\w*)\s*=/g);
185
- if (namedExports) {
186
- namedExports.forEach(e => exports.push(e.replace(/\s*=\s*$/, '')));
187
- }
188
-
189
- // ES6 exports
190
- const es6Exports = content.match(/export\s+(?:default\s+)?(?:const|let|var|function|class)\s+([a-zA-Z_$]\w*)/g);
191
- if (es6Exports) {
192
- es6Exports.forEach(e => exports.push(e));
193
- }
194
-
195
- return exports;
196
- }
197
-
198
- _isEntryPoint(relPath, content) {
199
- const entryNames = ['index.', 'main.', 'app.', 'server.', 'cli.', 'bin/'];
200
- if (entryNames.some(n => relPath.includes(n))) return true;
201
- if (content.includes('#!/usr/bin/env')) return true;
202
- if (content.includes('createServer') || content.includes('.listen(')) return true;
203
- return false;
204
- }
205
-
206
- _detectPurpose(relPath, content) {
207
- const lower = relPath.toLowerCase();
208
- if (/route/i.test(lower)) return 'routing';
209
- if (/middleware/i.test(lower)) return 'middleware';
210
- if (/model/i.test(lower)) return 'data model';
211
- if (/controller/i.test(lower)) return 'controller';
212
- if (/service/i.test(lower)) return 'service';
213
- if (/util|helper|lib/i.test(lower)) return 'utility';
214
- if (/config/i.test(lower)) return 'configuration';
215
- if (/test|spec/i.test(lower)) return 'test';
216
- if (/component/i.test(lower)) return 'UI component';
217
- if (/hook/i.test(lower)) return 'React hook';
218
- if (/store|redux|state/i.test(lower)) return 'state management';
219
- if (/api/i.test(lower)) return 'API layer';
220
- if (/db|database|migration/i.test(lower)) return 'database';
221
- if (/auth/i.test(lower)) return 'authentication';
222
- if (/cli|command/i.test(lower)) return 'CLI';
223
-
224
- // Check content for clues
225
- if (content.includes('app.get(') || content.includes('router.')) return 'routing';
226
- if (content.includes('mongoose.model') || content.includes('Schema')) return 'data model';
227
- if (content.includes('React') || content.includes('jsx')) return 'React component';
228
-
229
- return 'application logic';
230
- }
231
-
232
- _detectFrameworks(content, frameworks) {
233
- if (content.includes('require(\'express\'') || content.includes('from \'express\'')) frameworks.add('Express');
234
- if (content.includes('require(\'react\'') || content.includes('from \'react\'')) frameworks.add('React');
235
- if (content.includes('require(\'next\'') || content.includes('from \'next\'')) frameworks.add('Next.js');
236
- if (content.includes('require(\'mongoose\'') || content.includes('from \'mongoose\'')) frameworks.add('Mongoose');
237
- if (content.includes('require(\'fastify\'') || content.includes('from \'fastify\'')) frameworks.add('Fastify');
238
- if (content.includes('require(\'vue\'') || content.includes('from \'vue\'')) frameworks.add('Vue');
239
- if (content.includes('require(\'stripe\'') || content.includes('from \'stripe\'')) frameworks.add('Stripe');
240
- if (content.includes('@angular')) frameworks.add('Angular');
241
- }
242
-
243
- _detectArchitecture(fileMap) {
244
- const paths = fileMap.map(f => f.path.toLowerCase());
245
- if (paths.some(p => p.includes('controller')) && paths.some(p => p.includes('model'))) return 'MVC';
246
- if (paths.some(p => p.includes('service')) && paths.some(p => p.includes('repository'))) return 'Service-Repository';
247
- if (paths.some(p => p.includes('component')) && paths.some(p => p.includes('store'))) return 'Component-Store (SPA)';
248
- if (paths.some(p => p.includes('pages/')) && paths.some(p => p.includes('api/'))) return 'Next.js (Pages Router)';
249
- if (paths.some(p => p.includes('app/')) && paths.some(p => p.includes('route'))) return 'App Router';
250
- if (paths.some(p => p.includes('packages/'))) return 'Monorepo';
251
- return 'Flat';
252
- }
253
-
254
- _findCriticalPaths(dependencies) {
255
- return Object.entries(dependencies)
256
- .filter(([, deps]) => deps.length > 3)
257
- .map(([file, deps]) => ({
258
- file,
259
- dependsOn: deps.length,
260
- topDeps: deps.slice(0, 5),
261
- }))
262
- .sort((a, b) => b.dependsOn - a.dependsOn)
263
- .slice(0, 20);
264
- }
265
-
266
- _findCircularRisks(dependencies) {
267
- const risks = [];
268
- for (const [fileA, depsA] of Object.entries(dependencies)) {
269
- for (const dep of depsA) {
270
- const resolved = this._resolveRelative(fileA, dep);
271
- if (resolved && dependencies[resolved]) {
272
- const depsB = dependencies[resolved];
273
- if (depsB.some(d => this._resolveRelative(resolved, d) === fileA)) {
274
- risks.push({ fileA, fileB: resolved, risk: 'circular dependency' });
275
- }
276
- }
277
- }
278
- }
279
- return risks.slice(0, 10);
280
- }
281
-
282
- _resolveRelative(from, dep) {
283
- if (dep.startsWith('.')) {
284
- const dir = path.dirname(from);
285
- let resolved = path.join(dir, dep).replace(/\\/g, '/');
286
- if (!resolved.match(/\.\w+$/)) resolved += '.js';
287
- return resolved;
288
- }
289
- return null;
290
- }
291
-
292
- _generateQuickSummary(fileMap, frameworks, architecture) {
293
- const totalFiles = fileMap.length;
294
- const fw = [...frameworks].join(', ') || 'vanilla';
295
- return `${totalFiles}-file ${architecture} project using ${fw}. Start with entry points listed above.`;
296
- }
297
- }
298
-
299
- module.exports = CodebasePassport;
@@ -1,276 +0,0 @@
1
- /**
2
- * Thuban Copy-Paste Drift Detector
3
- *
4
- * @purpose Find similar code blocks pasted in multiple files with slight variations
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for Code quality — AI generates similar solutions everywhere
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const crypto = require('crypto');
14
-
15
- class CopyPasteDriftDetector {
16
- constructor(opts = {}) {
17
- this.rootPath = opts.rootPath || process.cwd();
18
- this.minLines = opts.minLines || 5; // Minimum function size to consider
19
- this.similarityThreshold = opts.similarity || 0.7; // 70% similarity = likely copy-paste
20
- }
21
-
22
- /**
23
- * Scan files for copy-paste clusters
24
- */
25
- scan(files) {
26
- const functions = [];
27
-
28
- // Extract all functions with their normalized bodies
29
- for (const file of files) {
30
- let content;
31
- try {
32
- const stat = fs.statSync(file);
33
- if (stat.size > 512 * 1024) continue;
34
- content = fs.readFileSync(file, 'utf-8');
35
- } catch (e) {
36
- continue;
37
- }
38
-
39
- const relPath = path.relative(this.rootPath, file);
40
- if (this._isSkippable(relPath)) continue;
41
-
42
- const fileFunctions = this._extractFunctions(content, relPath);
43
- functions.push(...fileFunctions);
44
- }
45
-
46
- // Compare all functions to find clusters
47
- const clusters = this._findClusters(functions);
48
-
49
- const totalDuplicateLines = clusters.reduce((sum, c) =>
50
- sum + c.members.reduce((s, m) => s + m.lineCount, 0) - c.members[0].lineCount, 0);
51
-
52
- return {
53
- clusters,
54
- totalClusters: clusters.length,
55
- totalDuplicateInstances: clusters.reduce((sum, c) => sum + c.members.length, 0),
56
- totalDuplicateLines,
57
- recommendation: clusters.length > 0
58
- ? `Extract ${clusters.length} shared utilities to eliminate ${totalDuplicateLines} duplicate lines.`
59
- : 'No copy-paste drift detected.',
60
- };
61
- }
62
-
63
- // ─── Private ─────────────────────────────────────────────
64
-
65
- _extractFunctions(content, filePath) {
66
- const lines = content.split('\n');
67
- const functions = [];
68
-
69
- for (let i = 0; i < lines.length; i++) {
70
- const fn = this._detectFunction(lines[i]);
71
- if (!fn) continue;
72
-
73
- const endLine = this._findFunctionEnd(lines, i);
74
- const body = lines.slice(i, endLine + 1).join('\n');
75
- const lineCount = endLine - i + 1;
76
-
77
- if (lineCount < this.minLines) continue;
78
-
79
- const normalized = this._normalize(body);
80
- const hash = crypto.createHash('md5').update(normalized).digest('hex');
81
- const tokens = this._tokenize(normalized);
82
-
83
- functions.push({
84
- file: filePath,
85
- name: fn.name,
86
- line: i + 1,
87
- lineCount,
88
- hash,
89
- tokens,
90
- normalized,
91
- });
92
-
93
- i = endLine;
94
- }
95
-
96
- return functions;
97
- }
98
-
99
- _findClusters(functions) {
100
- const visited = new Set();
101
- const clusters = [];
102
-
103
- for (let i = 0; i < functions.length; i++) {
104
- if (visited.has(i)) continue;
105
-
106
- const cluster = [functions[i]];
107
- visited.add(i);
108
-
109
- for (let j = i + 1; j < functions.length; j++) {
110
- if (visited.has(j)) continue;
111
-
112
- // Quick hash check for exact duplicates
113
- if (functions[i].hash === functions[j].hash) {
114
- cluster.push(functions[j]);
115
- visited.add(j);
116
- continue;
117
- }
118
-
119
- // Token-level similarity check
120
- const similarity = this._tokenSimilarity(functions[i].tokens, functions[j].tokens);
121
- if (similarity >= this.similarityThreshold) {
122
- cluster.push({ ...functions[j], similarity: Math.round(similarity * 100) });
123
- visited.add(j);
124
- }
125
- }
126
-
127
- if (cluster.length >= 2) {
128
- // Find the differences between cluster members
129
- const diffs = this._findDiffs(cluster);
130
-
131
- clusters.push({
132
- pattern: cluster[0].name,
133
- members: cluster.map(m => ({
134
- file: m.file,
135
- name: m.name,
136
- line: m.line,
137
- lineCount: m.lineCount,
138
- similarity: m.similarity || 100,
139
- })),
140
- totalInstances: cluster.length,
141
- differences: diffs,
142
- severity: cluster.length >= 4 ? 'high' : cluster.length >= 3 ? 'medium' : 'low',
143
- recommendation: `Extract to shared utility. ${cluster.length} functions → 1.`,
144
- });
145
- }
146
- }
147
-
148
- return clusters.sort((a, b) => b.totalInstances - a.totalInstances);
149
- }
150
-
151
- _normalize(code) {
152
- // Two-pass normalization:
153
- // Pass 1: collect all declared variable names
154
- const declaredNames = [];
155
- const declPattern = /(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
156
- let m;
157
- while ((m = declPattern.exec(code)) !== null) {
158
- if (!declaredNames.includes(m[1])) declaredNames.push(m[1]);
159
- }
160
-
161
- let normalized = code
162
- // Remove comments
163
- .replace(/\/\/.*$/gm, '')
164
- .replace(/\/\*[\s\S]*?\*\//g, '')
165
- // Normalize string literals
166
- .replace(/'[^']*'/g, 'STR')
167
- .replace(/"[^"]*"/g, 'STR')
168
- .replace(/`[^`]*`/g, 'STR')
169
- // Normalize numbers
170
- .replace(/\b\d+\.?\d*\b/g, 'NUM');
171
-
172
- // Pass 2: replace ALL occurrences of each declared name with VAR_N
173
- declaredNames.forEach((name, idx) => {
174
- const nameRegex = new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
175
- normalized = normalized.replace(nameRegex, `VAR_${idx}`);
176
- });
177
-
178
- // Normalize whitespace
179
- return normalized.replace(/\s+/g, ' ').trim();
180
- }
181
-
182
- _tokenize(normalized) {
183
- return normalized
184
- .split(/[\s{}()\[\];,=+\-*/<>!&|?:.]+/)
185
- .filter(t => t.length > 0);
186
- }
187
-
188
- _tokenSimilarity(tokens1, tokens2) {
189
- if (tokens1.length === 0 && tokens2.length === 0) return 1;
190
- if (tokens1.length === 0 || tokens2.length === 0) return 0;
191
-
192
- const set1 = new Set(tokens1);
193
- const set2 = new Set(tokens2);
194
- const intersection = [...set1].filter(t => set2.has(t)).length;
195
- const union = new Set([...tokens1, ...tokens2]).size;
196
-
197
- // Jaccard similarity
198
- const jaccard = union > 0 ? intersection / union : 0;
199
-
200
- // Also consider length similarity
201
- const lengthRatio = Math.min(tokens1.length, tokens2.length) / Math.max(tokens1.length, tokens2.length);
202
-
203
- return (jaccard * 0.7) + (lengthRatio * 0.3);
204
- }
205
-
206
- _findDiffs(cluster) {
207
- if (cluster.length < 2) return [];
208
-
209
- const diffs = [];
210
- const base = cluster[0].normalized;
211
-
212
- for (let i = 1; i < cluster.length; i++) {
213
- const other = cluster[i].normalized;
214
- if (base !== other) {
215
- const baseTokens = new Set(this._tokenize(base));
216
- const otherTokens = new Set(this._tokenize(other));
217
- const onlyInOther = [...otherTokens].filter(t => !baseTokens.has(t));
218
- const onlyInBase = [...baseTokens].filter(t => !otherTokens.has(t));
219
- if (onlyInOther.length > 0 || onlyInBase.length > 0) {
220
- diffs.push({
221
- file: cluster[i].file,
222
- addedTokens: onlyInOther.slice(0, 5),
223
- removedTokens: onlyInBase.slice(0, 5),
224
- });
225
- }
226
- }
227
- }
228
-
229
- return diffs;
230
- }
231
-
232
- _detectFunction(line) {
233
- const trimmed = line.trim();
234
- // JavaScript/TypeScript: function/const/let/var
235
- let match = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
236
- if (match) return { name: match[1] };
237
- match = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/);
238
- if (match) return { name: match[1] };
239
- // Kotlin: fun
240
- match = trimmed.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
241
- if (match) return { name: match[1] };
242
- // Rust: fn
243
- match = trimmed.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
244
- if (match) return { name: match[1] };
245
- // PHP: function
246
- match = trimmed.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
247
- if (match) return { name: match[1] };
248
- // Ruby: def
249
- match = trimmed.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/);
250
- if (match) return { name: match[1] };
251
- return null;
252
- }
253
-
254
- _findFunctionEnd(lines, startLine) {
255
- let depth = 0;
256
- let started = false;
257
- for (let i = startLine; i < lines.length && i < startLine + 200; i++) {
258
- for (const ch of lines[i]) {
259
- if (ch === '{') { depth++; started = true; }
260
- if (ch === '}') depth--;
261
- }
262
- if (started && depth <= 0) return i;
263
- }
264
- return Math.min(startLine + 20, lines.length - 1);
265
- }
266
-
267
- _isSkippable(relPath) {
268
- const skip = [
269
- /\.test\./i, /\.spec\./i, /test[\/\\]/i, /node_modules/i,
270
- /\.min\./i, /\.d\.ts$/, /\.config\./i,
271
- ];
272
- return skip.some(r => r.test(relPath));
273
- }
274
- }
275
-
276
- module.exports = CopyPasteDriftDetector;