tlc-claude-code 1.2.28 → 1.3.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.
Files changed (87) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  3. package/dashboard/dist/components/UsagePane.js +51 -0
  4. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  5. package/dashboard/dist/components/UsagePane.test.js +142 -0
  6. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  7. package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
  8. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  10. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  11. package/dashboard/dist/components/WorkspacePane.js +17 -0
  12. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  13. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  14. package/package.json +15 -4
  15. package/scripts/capture-screenshots.js +170 -0
  16. package/scripts/docs-update.js +253 -0
  17. package/scripts/generate-screenshots.js +321 -0
  18. package/scripts/project-docs.js +377 -0
  19. package/scripts/vps-setup.sh +477 -0
  20. package/server/lib/architecture-command.js +450 -0
  21. package/server/lib/architecture-command.test.js +754 -0
  22. package/server/lib/ast-analyzer.js +324 -0
  23. package/server/lib/ast-analyzer.test.js +437 -0
  24. package/server/lib/auth-system.test.js +4 -1
  25. package/server/lib/boundary-detector.js +427 -0
  26. package/server/lib/boundary-detector.test.js +320 -0
  27. package/server/lib/budget-alerts.js +138 -0
  28. package/server/lib/budget-alerts.test.js +235 -0
  29. package/server/lib/candidates-tracker.js +210 -0
  30. package/server/lib/candidates-tracker.test.js +300 -0
  31. package/server/lib/checkpoint-manager.js +251 -0
  32. package/server/lib/checkpoint-manager.test.js +474 -0
  33. package/server/lib/circular-detector.js +337 -0
  34. package/server/lib/circular-detector.test.js +353 -0
  35. package/server/lib/cohesion-analyzer.js +310 -0
  36. package/server/lib/cohesion-analyzer.test.js +447 -0
  37. package/server/lib/contract-testing.js +625 -0
  38. package/server/lib/contract-testing.test.js +342 -0
  39. package/server/lib/conversion-planner.js +469 -0
  40. package/server/lib/conversion-planner.test.js +361 -0
  41. package/server/lib/convert-command.js +351 -0
  42. package/server/lib/convert-command.test.js +608 -0
  43. package/server/lib/coupling-calculator.js +189 -0
  44. package/server/lib/coupling-calculator.test.js +509 -0
  45. package/server/lib/dependency-graph.js +367 -0
  46. package/server/lib/dependency-graph.test.js +516 -0
  47. package/server/lib/duplication-detector.js +349 -0
  48. package/server/lib/duplication-detector.test.js +401 -0
  49. package/server/lib/example-service.js +616 -0
  50. package/server/lib/example-service.test.js +397 -0
  51. package/server/lib/impact-scorer.js +184 -0
  52. package/server/lib/impact-scorer.test.js +211 -0
  53. package/server/lib/mermaid-generator.js +358 -0
  54. package/server/lib/mermaid-generator.test.js +301 -0
  55. package/server/lib/messaging-patterns.js +750 -0
  56. package/server/lib/messaging-patterns.test.js +213 -0
  57. package/server/lib/microservice-template.js +386 -0
  58. package/server/lib/microservice-template.test.js +325 -0
  59. package/server/lib/new-project-microservice.js +450 -0
  60. package/server/lib/new-project-microservice.test.js +600 -0
  61. package/server/lib/refactor-command.js +326 -0
  62. package/server/lib/refactor-command.test.js +528 -0
  63. package/server/lib/refactor-executor.js +254 -0
  64. package/server/lib/refactor-executor.test.js +305 -0
  65. package/server/lib/refactor-observer.js +292 -0
  66. package/server/lib/refactor-observer.test.js +422 -0
  67. package/server/lib/refactor-progress.js +193 -0
  68. package/server/lib/refactor-progress.test.js +251 -0
  69. package/server/lib/refactor-reporter.js +237 -0
  70. package/server/lib/refactor-reporter.test.js +247 -0
  71. package/server/lib/semantic-analyzer.js +198 -0
  72. package/server/lib/semantic-analyzer.test.js +474 -0
  73. package/server/lib/service-scaffold.js +486 -0
  74. package/server/lib/service-scaffold.test.js +373 -0
  75. package/server/lib/shared-kernel.js +578 -0
  76. package/server/lib/shared-kernel.test.js +255 -0
  77. package/server/lib/traefik-config.js +282 -0
  78. package/server/lib/traefik-config.test.js +312 -0
  79. package/server/lib/usage-command.js +218 -0
  80. package/server/lib/usage-command.test.js +391 -0
  81. package/server/lib/usage-formatter.js +192 -0
  82. package/server/lib/usage-formatter.test.js +267 -0
  83. package/server/lib/usage-history.js +122 -0
  84. package/server/lib/usage-history.test.js +206 -0
  85. package/server/package-lock.json +14 -0
  86. package/server/package.json +1 -0
  87. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * Duplication Detector
3
+ * Find copy-pasted and structurally similar code
4
+ */
5
+
6
+ class DuplicationDetector {
7
+ constructor(options = {}) {
8
+ this.options = {
9
+ minLines: options.minLines || 5,
10
+ minTokens: options.minTokens || 50,
11
+ similarityThreshold: options.similarityThreshold || 0.6,
12
+ ignoreImports: options.ignoreImports !== false,
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Detect duplications across files
18
+ * @param {Array} files - Array of { path, content } objects
19
+ * @returns {Object} Detection result
20
+ */
21
+ detect(files) {
22
+ if (!files || files.length === 0) {
23
+ return this.emptyResult();
24
+ }
25
+
26
+ // Preprocess files
27
+ const processed = files.map((f) => ({
28
+ path: f.path,
29
+ content: f.content,
30
+ lines: this.getSignificantLines(f.content),
31
+ normalized: this.normalizeCode(f.content),
32
+ }));
33
+
34
+ // Find exact duplicates
35
+ const duplicates = this.findExactDuplicates(processed);
36
+
37
+ // Find similar code
38
+ const similar = this.findSimilarCode(processed);
39
+
40
+ // Build file pairs
41
+ const pairs = this.buildFilePairs(duplicates, processed);
42
+
43
+ // Calculate per-file stats
44
+ const fileStats = this.calculateFileStats(processed, duplicates);
45
+
46
+ // Summary
47
+ const summary = this.buildSummary(processed, duplicates);
48
+
49
+ return {
50
+ duplicates,
51
+ similar,
52
+ pairs,
53
+ fileStats,
54
+ summary,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Get empty result structure
60
+ */
61
+ emptyResult() {
62
+ return {
63
+ duplicates: [],
64
+ similar: [],
65
+ pairs: [],
66
+ fileStats: {},
67
+ summary: {
68
+ totalFiles: 0,
69
+ filesWithDuplication: 0,
70
+ totalDuplicateBlocks: 0,
71
+ },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Get significant lines (non-empty, non-import)
77
+ */
78
+ getSignificantLines(content) {
79
+ if (!content) return [];
80
+
81
+ return content
82
+ .split('\n')
83
+ .map((line, index) => ({ line: line.trim(), lineNumber: index + 1 }))
84
+ .filter((item) => {
85
+ if (!item.line) return false;
86
+ if (this.options.ignoreImports && this.isImportLine(item.line)) return false;
87
+ return true;
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Check if line is an import/require statement
93
+ */
94
+ isImportLine(line) {
95
+ return (
96
+ line.startsWith('import ') ||
97
+ line.startsWith('const ') && line.includes('require(') ||
98
+ line.startsWith('let ') && line.includes('require(') ||
99
+ line.startsWith('var ') && line.includes('require(')
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Normalize code for comparison (remove variable names, literals)
105
+ */
106
+ normalizeCode(content) {
107
+ if (!content) return '';
108
+
109
+ return content
110
+ // Remove comments
111
+ .replace(/\/\/.*$/gm, '')
112
+ .replace(/\/\*[\s\S]*?\*\//g, '')
113
+ // Normalize string literals
114
+ .replace(/'[^']*'/g, "'STR'")
115
+ .replace(/"[^"]*"/g, '"STR"')
116
+ .replace(/`[^`]*`/g, '`STR`')
117
+ // Normalize numbers
118
+ .replace(/\b\d+\.?\d*\b/g, 'NUM')
119
+ // Normalize function names and parameters
120
+ .replace(/function\s+\w+\s*\(/g, 'function FUNC(')
121
+ .replace(/\((\w+)\)/g, '(PARAM)')
122
+ .replace(/\((\w+),/g, '(PARAM,')
123
+ .replace(/,\s*(\w+)\)/g, ', PARAM)')
124
+ .replace(/,\s*(\w+),/g, ', PARAM,')
125
+ // Normalize variable names (basic)
126
+ .replace(/\b(const|let|var)\s+(\w+)/g, '$1 VAR')
127
+ // Normalize property access (x.prop → IDENT.prop)
128
+ .replace(/\b(\w+)\./g, 'IDENT.')
129
+ // Normalize whitespace
130
+ .replace(/\s+/g, ' ')
131
+ .trim();
132
+ }
133
+
134
+ /**
135
+ * Find exact duplicate blocks
136
+ */
137
+ findExactDuplicates(processed) {
138
+ const duplicates = [];
139
+ const blockMap = new Map();
140
+
141
+ for (const file of processed) {
142
+ const blocks = this.extractBlocks(file.lines, file.path);
143
+
144
+ for (const block of blocks) {
145
+ const key = block.content;
146
+ if (!blockMap.has(key)) {
147
+ blockMap.set(key, []);
148
+ }
149
+ blockMap.get(key).push({
150
+ path: file.path,
151
+ startLine: block.startLine,
152
+ endLine: block.endLine,
153
+ });
154
+ }
155
+ }
156
+
157
+ // Find blocks that appear in multiple places
158
+ for (const [content, locations] of blockMap.entries()) {
159
+ if (locations.length > 1) {
160
+ duplicates.push({
161
+ content,
162
+ lineCount: content.split('\n').length,
163
+ files: [...new Set(locations.map((l) => l.path))],
164
+ locations,
165
+ });
166
+ }
167
+ }
168
+
169
+ return duplicates;
170
+ }
171
+
172
+ /**
173
+ * Extract code blocks of minimum size
174
+ */
175
+ extractBlocks(lines, path) {
176
+ const blocks = [];
177
+ const minLines = this.options.minLines;
178
+
179
+ if (lines.length < minLines) return blocks;
180
+
181
+ // Sliding window approach
182
+ for (let i = 0; i <= lines.length - minLines; i++) {
183
+ for (let length = minLines; length <= Math.min(lines.length - i, 50); length++) {
184
+ const blockLines = lines.slice(i, i + length);
185
+ const content = blockLines.map((l) => l.line).join('\n');
186
+
187
+ // Skip blocks that are too simple (mostly braces/whitespace)
188
+ if (this.isSignificantBlock(content)) {
189
+ blocks.push({
190
+ content,
191
+ startLine: blockLines[0].lineNumber,
192
+ endLine: blockLines[blockLines.length - 1].lineNumber,
193
+ });
194
+ }
195
+ }
196
+ }
197
+
198
+ return blocks;
199
+ }
200
+
201
+ /**
202
+ * Check if a block has significant content
203
+ */
204
+ isSignificantBlock(content) {
205
+ // Remove braces, semicolons, whitespace
206
+ const stripped = content.replace(/[{};()\s]/g, '');
207
+ return stripped.length > 20;
208
+ }
209
+
210
+ /**
211
+ * Find structurally similar code
212
+ */
213
+ findSimilarCode(processed) {
214
+ const similar = [];
215
+
216
+ for (let i = 0; i < processed.length; i++) {
217
+ for (let j = i + 1; j < processed.length; j++) {
218
+ const similarity = this.calculateSimilarity(
219
+ processed[i].normalized,
220
+ processed[j].normalized
221
+ );
222
+
223
+ if (similarity >= this.options.similarityThreshold) {
224
+ similar.push({
225
+ file1: processed[i].path,
226
+ file2: processed[j].path,
227
+ similarity,
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ return similar;
234
+ }
235
+
236
+ /**
237
+ * Calculate similarity between two normalized code strings
238
+ * Uses Jaccard similarity on token sets
239
+ */
240
+ calculateSimilarity(code1, code2) {
241
+ if (!code1 || !code2) return 0;
242
+
243
+ const tokens1 = new Set(code1.split(/\s+/));
244
+ const tokens2 = new Set(code2.split(/\s+/));
245
+
246
+ const intersection = new Set([...tokens1].filter((t) => tokens2.has(t)));
247
+ const union = new Set([...tokens1, ...tokens2]);
248
+
249
+ if (union.size === 0) return 0;
250
+
251
+ return intersection.size / union.size;
252
+ }
253
+
254
+ /**
255
+ * Build file pairs from duplicates
256
+ */
257
+ buildFilePairs(duplicates, processed) {
258
+ const pairMap = new Map();
259
+
260
+ for (const dup of duplicates) {
261
+ for (let i = 0; i < dup.locations.length; i++) {
262
+ for (let j = i + 1; j < dup.locations.length; j++) {
263
+ const loc1 = dup.locations[i];
264
+ const loc2 = dup.locations[j];
265
+ const key = [loc1.path, loc2.path].sort().join(':::');
266
+
267
+ if (!pairMap.has(key)) {
268
+ pairMap.set(key, {
269
+ file1: loc1.path < loc2.path ? loc1.path : loc2.path,
270
+ file2: loc1.path < loc2.path ? loc2.path : loc1.path,
271
+ lines1: { start: loc1.startLine, end: loc1.endLine },
272
+ lines2: { start: loc2.startLine, end: loc2.endLine },
273
+ duplicates: [],
274
+ totalDuplicatedLines: 0,
275
+ });
276
+ }
277
+
278
+ const pair = pairMap.get(key);
279
+ pair.duplicates.push({
280
+ lines1: { start: loc1.startLine, end: loc1.endLine },
281
+ lines2: { start: loc2.startLine, end: loc2.endLine },
282
+ lineCount: dup.lineCount,
283
+ });
284
+ pair.totalDuplicatedLines += dup.lineCount;
285
+ }
286
+ }
287
+ }
288
+
289
+ return Array.from(pairMap.values());
290
+ }
291
+
292
+ /**
293
+ * Calculate per-file duplication stats
294
+ */
295
+ calculateFileStats(processed, duplicates) {
296
+ const stats = {};
297
+
298
+ for (const file of processed) {
299
+ const totalLines = file.lines.length;
300
+
301
+ // Track which lines are duplicated (using Set to avoid counting overlaps)
302
+ const duplicatedLineNumbers = new Set();
303
+
304
+ for (const dup of duplicates) {
305
+ for (const loc of dup.locations) {
306
+ if (loc.path === file.path) {
307
+ for (let line = loc.startLine; line <= loc.endLine; line++) {
308
+ duplicatedLineNumbers.add(line);
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ const duplicatedLines = duplicatedLineNumbers.size;
315
+ const percentage = totalLines > 0
316
+ ? Math.round((duplicatedLines / totalLines) * 100)
317
+ : 0;
318
+
319
+ stats[file.path] = {
320
+ totalLines,
321
+ duplicatedLines,
322
+ duplicationPercentage: percentage,
323
+ };
324
+ }
325
+
326
+ return stats;
327
+ }
328
+
329
+ /**
330
+ * Build summary statistics
331
+ */
332
+ buildSummary(processed, duplicates) {
333
+ const filesWithDuplication = new Set();
334
+
335
+ for (const dup of duplicates) {
336
+ for (const loc of dup.locations) {
337
+ filesWithDuplication.add(loc.path);
338
+ }
339
+ }
340
+
341
+ return {
342
+ totalFiles: processed.length,
343
+ filesWithDuplication: filesWithDuplication.size,
344
+ totalDuplicateBlocks: duplicates.length,
345
+ };
346
+ }
347
+ }
348
+
349
+ module.exports = { DuplicationDetector };