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.
- package/README.md +9 -4
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +146 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/package.json +15 -4
- package/scripts/capture-screenshots.js +170 -0
- package/scripts/docs-update.js +253 -0
- package/scripts/generate-screenshots.js +321 -0
- package/scripts/project-docs.js +377 -0
- package/scripts/vps-setup.sh +477 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/package-lock.json +14 -0
- package/server/package.json +1 -0
- 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 };
|