ctxman 3.0.0 → 3.1.1

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 (224) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +687 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +772 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +116 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. 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
- 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
- { regex: /^\s+([A-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*(?:[a-zA-Z0-9_*\[\]]+)?/gm, type: 'interface' },
19
- ];
20
-
21
- const methodsMap = new Map();
22
- const processedLines = new Map();
23
-
24
- patterns.forEach(({ regex, type }) => {
25
- let match;
26
- while ((match = regex.exec(content)) !== null) {
27
- const line = this.getLineNumber(content, match.index);
28
- const methodName = match[1];
29
-
30
- if (methodName && !this.isKeyword(methodName)) {
31
- const key = `${methodName}:${line}`;
32
-
33
- // Skip if this line was already processed
34
- if (processedLines.has(key)) {
35
- continue;
36
- }
37
-
38
- // Validate it's not inside a comment
39
- if (this.isInComment(content, match.index)) {
40
- continue;
41
- }
42
-
43
- methodsMap.set(key, {
44
- name: methodName,
45
- line: line,
46
- file: path.relative(process.cwd(), filePath),
47
- type: type
48
- });
49
- processedLines.set(key, true);
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
- // Multi-line comment check (simple heuristic)
77
- const beforeText = content.substring(0, index);
78
- const openComments = (beforeText.match(/\/\*/g) || []).length;
79
- const closeComments = (beforeText.match(/\*\//g) || []).length;
80
-
81
- return openComments > closeComments;
82
- }
83
-
84
- isKeyword(name) {
85
- // Go keywords and common types
86
- const keywords = new Set([
87
- 'break', 'case', 'chan', 'const', 'continue', 'default', 'defer',
88
- 'else', 'fallthrough', 'for', 'func', 'go', 'goto', 'if',
89
- 'import', 'interface', 'map', 'package', 'range', 'return',
90
- 'select', 'struct', 'switch', 'type', 'var',
91
- // Common types
92
- 'bool', 'byte', 'complex64', 'complex128', 'error', 'float32',
93
- 'float64', 'int', 'int8', 'int16', 'int32', 'int64', 'rune',
94
- 'string', 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr'
95
- ]);
96
- return keywords.has(name);
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
- extractMethodContent(content, methodName) {
100
- // Extract function/method body
101
- const patterns = [
102
- new RegExp(`(func\\s+${methodName}\\s*\\([^)]*\\)[^{]*\\{)`, 'g'),
103
- new RegExp(`(func\\s+\\([^)]+\\)\\s+${methodName}\\s*\\([^)]*\\)[^{]*\\{)`, 'g')
104
- ];
105
-
106
- for (const pattern of patterns) {
107
- const match = pattern.exec(content);
108
- if (match) {
109
- // Find the matching closing brace
110
- let braceCount = 1;
111
- let pos = match.index + match[0].length;
112
-
113
- while (braceCount > 0 && pos < content.length) {
114
- if (content[pos] === '{') braceCount++;
115
- if (content[pos] === '}') braceCount--;
116
- pos++;
117
- }
118
-
119
- return content.substring(match.index, pos);
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
- return null;
159
+
160
+ return content.substring(match.index, pos);
161
+ }
123
162
  }
163
+ return null;
164
+ }
124
165
  }
125
166
 
126
167
  export default GoMethodAnalyzer;