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,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,250 +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
- return code
153
- // Remove comments
154
- .replace(/\/\/.*$/gm, '')
155
- .replace(/\/\*[\s\S]*?\*\//g, '')
156
- // Normalize whitespace
157
- .replace(/\s+/g, ' ')
158
- // Normalize variable names to placeholders
159
- .replace(/(?:const|let|var)\s+([a-zA-Z_$]\w*)/g, 'const VAR')
160
- // Normalize string literals
161
- .replace(/'[^']*'/g, 'STR')
162
- .replace(/"[^"]*"/g, 'STR')
163
- .replace(/`[^`]*`/g, 'STR')
164
- // Normalize numbers
165
- .replace(/\b\d+\.?\d*\b/g, 'NUM')
166
- .trim();
167
- }
168
-
169
- _tokenize(normalized) {
170
- return normalized
171
- .split(/[\s{}()\[\];,=+\-*/<>!&|?:.]+/)
172
- .filter(t => t.length > 0);
173
- }
174
-
175
- _tokenSimilarity(tokens1, tokens2) {
176
- if (tokens1.length === 0 && tokens2.length === 0) return 1;
177
- if (tokens1.length === 0 || tokens2.length === 0) return 0;
178
-
179
- const set1 = new Set(tokens1);
180
- const set2 = new Set(tokens2);
181
- const intersection = [...set1].filter(t => set2.has(t)).length;
182
- const union = new Set([...tokens1, ...tokens2]).size;
183
-
184
- // Jaccard similarity
185
- const jaccard = union > 0 ? intersection / union : 0;
186
-
187
- // Also consider length similarity
188
- const lengthRatio = Math.min(tokens1.length, tokens2.length) / Math.max(tokens1.length, tokens2.length);
189
-
190
- return (jaccard * 0.7) + (lengthRatio * 0.3);
191
- }
192
-
193
- _findDiffs(cluster) {
194
- if (cluster.length < 2) return [];
195
-
196
- const diffs = [];
197
- const base = cluster[0].normalized;
198
-
199
- for (let i = 1; i < cluster.length; i++) {
200
- const other = cluster[i].normalized;
201
- if (base !== other) {
202
- const baseTokens = new Set(this._tokenize(base));
203
- const otherTokens = new Set(this._tokenize(other));
204
- const onlyInOther = [...otherTokens].filter(t => !baseTokens.has(t));
205
- const onlyInBase = [...baseTokens].filter(t => !otherTokens.has(t));
206
- if (onlyInOther.length > 0 || onlyInBase.length > 0) {
207
- diffs.push({
208
- file: cluster[i].file,
209
- addedTokens: onlyInOther.slice(0, 5),
210
- removedTokens: onlyInBase.slice(0, 5),
211
- });
212
- }
213
- }
214
- }
215
-
216
- return diffs;
217
- }
218
-
219
- _detectFunction(line) {
220
- const trimmed = line.trim();
221
- let match = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
222
- if (match) return { name: match[1] };
223
- match = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/);
224
- if (match) return { name: match[1] };
225
- return null;
226
- }
227
-
228
- _findFunctionEnd(lines, startLine) {
229
- let depth = 0;
230
- let started = false;
231
- for (let i = startLine; i < lines.length && i < startLine + 200; i++) {
232
- for (const ch of lines[i]) {
233
- if (ch === '{') { depth++; started = true; }
234
- if (ch === '}') depth--;
235
- }
236
- if (started && depth <= 0) return i;
237
- }
238
- return Math.min(startLine + 20, lines.length - 1);
239
- }
240
-
241
- _isSkippable(relPath) {
242
- const skip = [
243
- /\.test\./i, /\.spec\./i, /test[\/\\]/i, /node_modules/i,
244
- /\.min\./i, /\.d\.ts$/, /\.config\./i,
245
- ];
246
- return skip.some(r => r.test(relPath));
247
- }
248
- }
249
-
250
- module.exports = CopyPasteDriftDetector;