ctxman 3.0.0 → 3.1.0
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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +683 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +675 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +113 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/sources/FileDataSource.js +6 -6
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +243 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +255 -0
- package/lib/wizards/project-detector.js +571 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate Code Detector
|
|
3
|
+
* Detects duplicate and near-duplicate code blocks
|
|
4
|
+
* Part of FEAT-005: AI-Powered Context Suggestions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getLogger } from '../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
const logger = getLogger('DuplicateDetector');
|
|
10
|
+
|
|
11
|
+
export class DuplicateDetector {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
minLines: options.minLines || 5,
|
|
15
|
+
similarityThreshold: options.similarityThreshold || 0.8,
|
|
16
|
+
maxBlocks: options.maxBlocks || 100,
|
|
17
|
+
...options,
|
|
18
|
+
};
|
|
19
|
+
this.duplicates = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect duplicate code blocks in files
|
|
24
|
+
* @param {Array} files - Array of file objects with content
|
|
25
|
+
* @returns {Array} Array of duplicate block groups
|
|
26
|
+
*/
|
|
27
|
+
detect(files) {
|
|
28
|
+
logger.debug(`Analyzing ${files.length} files for duplicates`);
|
|
29
|
+
|
|
30
|
+
const blocks = this.extractBlocks(files);
|
|
31
|
+
const duplicateGroups = this.findDuplicates(blocks);
|
|
32
|
+
|
|
33
|
+
this.duplicates = duplicateGroups;
|
|
34
|
+
logger.info(`Found ${duplicateGroups.length} duplicate code groups`);
|
|
35
|
+
|
|
36
|
+
return duplicateGroups;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract code blocks from files
|
|
41
|
+
* @param {Array} files - Array of file objects
|
|
42
|
+
* @returns {Array} Array of code blocks
|
|
43
|
+
*/
|
|
44
|
+
extractBlocks(files) {
|
|
45
|
+
const blocks = [];
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
if (!file.content || !this.isCodeFile(file.path)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines = file.content.split('\n');
|
|
53
|
+
const minLines = this.options.minLines;
|
|
54
|
+
|
|
55
|
+
// Extract sliding windows of code blocks
|
|
56
|
+
for (let i = 0; i <= lines.length - minLines; i++) {
|
|
57
|
+
const blockLines = lines.slice(i, i + minLines);
|
|
58
|
+
const content = blockLines.join('\n');
|
|
59
|
+
|
|
60
|
+
// Skip empty or comment-only blocks
|
|
61
|
+
if (this.isSignificantBlock(content)) {
|
|
62
|
+
blocks.push({
|
|
63
|
+
file: file.path,
|
|
64
|
+
relativePath: file.relativePath || file.path,
|
|
65
|
+
startLine: i + 1,
|
|
66
|
+
endLine: i + minLines,
|
|
67
|
+
content,
|
|
68
|
+
hash: this.hashBlock(content),
|
|
69
|
+
normalizedHash: this.hashBlock(this.normalizeBlock(content)),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return blocks;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find duplicate blocks by comparing hashes
|
|
80
|
+
* @param {Array} blocks - Array of code blocks
|
|
81
|
+
* @returns {Array} Array of duplicate groups
|
|
82
|
+
*/
|
|
83
|
+
findDuplicates(blocks) {
|
|
84
|
+
const hashMap = new Map();
|
|
85
|
+
const normalizedHashMap = new Map();
|
|
86
|
+
|
|
87
|
+
// Group by exact hash
|
|
88
|
+
for (const block of blocks) {
|
|
89
|
+
// Exact duplicates
|
|
90
|
+
if (!hashMap.has(block.hash)) {
|
|
91
|
+
hashMap.set(block.hash, []);
|
|
92
|
+
}
|
|
93
|
+
hashMap.get(block.hash).push(block);
|
|
94
|
+
|
|
95
|
+
// Near duplicates (normalized)
|
|
96
|
+
if (!normalizedHashMap.has(block.normalizedHash)) {
|
|
97
|
+
normalizedHashMap.set(block.normalizedHash, []);
|
|
98
|
+
}
|
|
99
|
+
normalizedHashMap.get(block.normalizedHash).push(block);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Find groups with more than one occurrence
|
|
103
|
+
const duplicateGroups = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
|
|
106
|
+
// Process exact duplicates
|
|
107
|
+
for (const [hash, group] of hashMap) {
|
|
108
|
+
if (group.length > 1) {
|
|
109
|
+
const key = `exact:${hash}`;
|
|
110
|
+
if (!seen.has(key)) {
|
|
111
|
+
seen.add(key);
|
|
112
|
+
duplicateGroups.push({
|
|
113
|
+
type: 'exact',
|
|
114
|
+
count: group.length,
|
|
115
|
+
files: [...new Set(group.map((b) => b.relativePath))],
|
|
116
|
+
locations: group.map((b) => ({
|
|
117
|
+
file: b.relativePath,
|
|
118
|
+
line: b.startLine,
|
|
119
|
+
})),
|
|
120
|
+
tokens: this.estimateTokens(group[0].content),
|
|
121
|
+
preview: this.getPreview(group[0].content),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Process near duplicates (normalized)
|
|
128
|
+
for (const [hash, group] of normalizedHashMap) {
|
|
129
|
+
if (group.length > 2) {
|
|
130
|
+
// Higher threshold for normalized
|
|
131
|
+
const key = `normalized:${hash}`;
|
|
132
|
+
if (!seen.has(key)) {
|
|
133
|
+
seen.add(key);
|
|
134
|
+
|
|
135
|
+
// Calculate similarity within group
|
|
136
|
+
const uniqueFiles = [...new Set(group.map((b) => b.relativePath))];
|
|
137
|
+
if (uniqueFiles.length > 1) {
|
|
138
|
+
duplicateGroups.push({
|
|
139
|
+
type: 'similar',
|
|
140
|
+
count: group.length,
|
|
141
|
+
files: uniqueFiles,
|
|
142
|
+
locations: group.slice(0, 5).map((b) => ({
|
|
143
|
+
file: b.relativePath,
|
|
144
|
+
line: b.startLine,
|
|
145
|
+
})),
|
|
146
|
+
tokens: this.estimateTokens(group[0].content),
|
|
147
|
+
preview: this.getPreview(group[0].content),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Sort by impact (count * tokens)
|
|
155
|
+
duplicateGroups.sort((a, b) => b.count * b.tokens - a.count * a.tokens);
|
|
156
|
+
|
|
157
|
+
return duplicateGroups.slice(0, this.options.maxBlocks);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if file is a code file
|
|
162
|
+
*/
|
|
163
|
+
isCodeFile(filePath) {
|
|
164
|
+
const codeExtensions = [
|
|
165
|
+
'.js',
|
|
166
|
+
'.ts',
|
|
167
|
+
'.jsx',
|
|
168
|
+
'.tsx',
|
|
169
|
+
'.py',
|
|
170
|
+
'.java',
|
|
171
|
+
'.go',
|
|
172
|
+
'.rs',
|
|
173
|
+
'.cs',
|
|
174
|
+
'.php',
|
|
175
|
+
'.rb',
|
|
176
|
+
];
|
|
177
|
+
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
|
178
|
+
return codeExtensions.includes(ext);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if block has significant code (not just comments/whitespace)
|
|
183
|
+
*/
|
|
184
|
+
isSignificantBlock(content) {
|
|
185
|
+
const lines = content.split('\n');
|
|
186
|
+
let significantLines = 0;
|
|
187
|
+
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
const trimmed = line.trim();
|
|
190
|
+
// Skip empty lines and single-line comments
|
|
191
|
+
if (
|
|
192
|
+
trimmed &&
|
|
193
|
+
!trimmed.startsWith('//') &&
|
|
194
|
+
!trimmed.startsWith('#') &&
|
|
195
|
+
!trimmed.startsWith('/*')
|
|
196
|
+
) {
|
|
197
|
+
significantLines++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return significantLines >= 3;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a hash of a code block
|
|
206
|
+
*/
|
|
207
|
+
hashBlock(content) {
|
|
208
|
+
let hash = 0;
|
|
209
|
+
const str = content;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < str.length; i++) {
|
|
212
|
+
const char = str.charCodeAt(i);
|
|
213
|
+
hash = (hash << 5) - hash + char;
|
|
214
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return hash.toString(16);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Normalize block for near-duplicate detection
|
|
222
|
+
*/
|
|
223
|
+
normalizeBlock(content) {
|
|
224
|
+
return content
|
|
225
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
226
|
+
.replace(/[a-zA-Z_][a-zA-Z0-9_]*/g, 'VAR') // Replace identifiers
|
|
227
|
+
.replace(/"[^"]*"/g, 'STR') // Replace strings
|
|
228
|
+
.replace(/'[^']*'/g, 'STR')
|
|
229
|
+
.replace(/\d+/g, 'NUM') // Replace numbers
|
|
230
|
+
.trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Estimate tokens for content
|
|
235
|
+
*/
|
|
236
|
+
estimateTokens(content) {
|
|
237
|
+
return Math.ceil(content.length / 3.5);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get preview of code block
|
|
242
|
+
*/
|
|
243
|
+
getPreview(content, maxLength = 60) {
|
|
244
|
+
const firstLine = content.split('\n')[0].trim();
|
|
245
|
+
if (firstLine.length <= maxLength) {
|
|
246
|
+
return firstLine;
|
|
247
|
+
}
|
|
248
|
+
return firstLine.substring(0, maxLength - 3) + '...';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get statistics about duplicates
|
|
253
|
+
*/
|
|
254
|
+
getStats() {
|
|
255
|
+
return {
|
|
256
|
+
totalGroups: this.duplicates.length,
|
|
257
|
+
exactDuplicates: this.duplicates.filter((d) => d.type === 'exact').length,
|
|
258
|
+
similarCode: this.duplicates.filter((d) => d.type === 'similar').length,
|
|
259
|
+
wastedTokens: this.duplicates.reduce((sum, d) => sum + d.tokens * (d.count - 1), 0),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default DuplicateDetector;
|
|
@@ -6,121 +6,162 @@
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
|
|
8
8
|
class GoMethodAnalyzer {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return Array.from(methodsMap.values());
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
getLineNumber(content, index) {
|
|
58
|
-
return content.substring(0, index).split('\n').length;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
isInComment(content, index) {
|
|
62
|
-
// Check if position is inside a comment
|
|
63
|
-
const beforeContent = content.substring(0, index);
|
|
64
|
-
const lines = beforeContent.split('\n');
|
|
65
|
-
const currentLine = lines[lines.length - 1];
|
|
66
|
-
|
|
67
|
-
// Single-line comment check
|
|
68
|
-
if (currentLine.includes('//')) {
|
|
69
|
-
const commentPos = currentLine.indexOf('//');
|
|
70
|
-
const matchPos = currentLine.length;
|
|
71
|
-
if (commentPos < matchPos) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
9
|
+
extractMethods(content, filePath) {
|
|
10
|
+
const patterns = [
|
|
11
|
+
// Regular function: func FunctionName(params) returnType { ... }
|
|
12
|
+
{ regex: /func\s+([A-Z_][a-zA-Z0-9_]*)\s*\(/g, type: 'function' },
|
|
13
|
+
|
|
14
|
+
// Method (receiver function): func (r *Receiver) MethodName(params) returnType { ... }
|
|
15
|
+
{ regex: /func\s+\([^)]+\)\s+([A-Z_][a-zA-Z0-9_]*)\s*\(/g, type: 'method' },
|
|
16
|
+
|
|
17
|
+
// Interface method declarations: MethodName(params) returnType
|
|
18
|
+
{
|
|
19
|
+
regex: /^\s+([A-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*(?:[a-zA-Z0-9_*\[\]]+)?/gm,
|
|
20
|
+
type: 'interface',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const methodsMap = new Map();
|
|
25
|
+
const processedLines = new Map();
|
|
26
|
+
|
|
27
|
+
patterns.forEach(({ regex, type }) => {
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = regex.exec(content)) !== null) {
|
|
30
|
+
const line = this.getLineNumber(content, match.index);
|
|
31
|
+
const methodName = match[1];
|
|
32
|
+
|
|
33
|
+
if (methodName && !this.isKeyword(methodName)) {
|
|
34
|
+
const key = `${methodName}:${line}`;
|
|
35
|
+
|
|
36
|
+
// Skip if this line was already processed
|
|
37
|
+
if (processedLines.has(key)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Validate it's not inside a comment
|
|
42
|
+
if (this.isInComment(content, match.index)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
methodsMap.set(key, {
|
|
47
|
+
name: methodName,
|
|
48
|
+
line: line,
|
|
49
|
+
file: path.relative(process.cwd(), filePath),
|
|
50
|
+
type: type,
|
|
51
|
+
});
|
|
52
|
+
processedLines.set(key, true);
|
|
74
53
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return Array.from(methodsMap.values());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getLineNumber(content, index) {
|
|
61
|
+
return content.substring(0, index).split('\n').length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isInComment(content, index) {
|
|
65
|
+
// Check if position is inside a comment
|
|
66
|
+
const beforeContent = content.substring(0, index);
|
|
67
|
+
const lines = beforeContent.split('\n');
|
|
68
|
+
const currentLine = lines[lines.length - 1];
|
|
69
|
+
|
|
70
|
+
// Single-line comment check
|
|
71
|
+
if (currentLine.includes('//')) {
|
|
72
|
+
const commentPos = currentLine.indexOf('//');
|
|
73
|
+
const matchPos = currentLine.length;
|
|
74
|
+
if (commentPos < matchPos) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
97
77
|
}
|
|
98
78
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
79
|
+
// Multi-line comment check (simple heuristic)
|
|
80
|
+
const beforeText = content.substring(0, index);
|
|
81
|
+
const openComments = (beforeText.match(/\/\*/g) || []).length;
|
|
82
|
+
const closeComments = (beforeText.match(/\*\//g) || []).length;
|
|
83
|
+
|
|
84
|
+
return openComments > closeComments;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isKeyword(name) {
|
|
88
|
+
// Go keywords and common types
|
|
89
|
+
const keywords = new Set([
|
|
90
|
+
'break',
|
|
91
|
+
'case',
|
|
92
|
+
'chan',
|
|
93
|
+
'const',
|
|
94
|
+
'continue',
|
|
95
|
+
'default',
|
|
96
|
+
'defer',
|
|
97
|
+
'else',
|
|
98
|
+
'fallthrough',
|
|
99
|
+
'for',
|
|
100
|
+
'func',
|
|
101
|
+
'go',
|
|
102
|
+
'goto',
|
|
103
|
+
'if',
|
|
104
|
+
'import',
|
|
105
|
+
'interface',
|
|
106
|
+
'map',
|
|
107
|
+
'package',
|
|
108
|
+
'range',
|
|
109
|
+
'return',
|
|
110
|
+
'select',
|
|
111
|
+
'struct',
|
|
112
|
+
'switch',
|
|
113
|
+
'type',
|
|
114
|
+
'var',
|
|
115
|
+
// Common types
|
|
116
|
+
'bool',
|
|
117
|
+
'byte',
|
|
118
|
+
'complex64',
|
|
119
|
+
'complex128',
|
|
120
|
+
'error',
|
|
121
|
+
'float32',
|
|
122
|
+
'float64',
|
|
123
|
+
'int',
|
|
124
|
+
'int8',
|
|
125
|
+
'int16',
|
|
126
|
+
'int32',
|
|
127
|
+
'int64',
|
|
128
|
+
'rune',
|
|
129
|
+
'string',
|
|
130
|
+
'uint',
|
|
131
|
+
'uint8',
|
|
132
|
+
'uint16',
|
|
133
|
+
'uint32',
|
|
134
|
+
'uint64',
|
|
135
|
+
'uintptr',
|
|
136
|
+
]);
|
|
137
|
+
return keywords.has(name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extractMethodContent(content, methodName) {
|
|
141
|
+
// Extract function/method body
|
|
142
|
+
const patterns = [
|
|
143
|
+
new RegExp(`(func\\s+${methodName}\\s*\\([^)]*\\)[^{]*\\{)`, 'g'),
|
|
144
|
+
new RegExp(`(func\\s+\\([^)]+\\)\\s+${methodName}\\s*\\([^)]*\\)[^{]*\\{)`, 'g'),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
for (const pattern of patterns) {
|
|
148
|
+
const match = pattern.exec(content);
|
|
149
|
+
if (match) {
|
|
150
|
+
// Find the matching closing brace
|
|
151
|
+
let braceCount = 1;
|
|
152
|
+
let pos = match.index + match[0].length;
|
|
153
|
+
|
|
154
|
+
while (braceCount > 0 && pos < content.length) {
|
|
155
|
+
if (content[pos] === '{') braceCount++;
|
|
156
|
+
if (content[pos] === '}') braceCount--;
|
|
157
|
+
pos++;
|
|
121
158
|
}
|
|
122
|
-
|
|
159
|
+
|
|
160
|
+
return content.substring(match.index, pos);
|
|
161
|
+
}
|
|
123
162
|
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
export default GoMethodAnalyzer;
|