tlc-claude-code 1.2.29 → 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/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 +1 -1
- 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
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example Service Template Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
|
|
7
|
+
describe('ExampleService', () => {
|
|
8
|
+
describe('generate', () => {
|
|
9
|
+
it('generates service directory structure', async () => {
|
|
10
|
+
const { ExampleService } = await import('./example-service.js');
|
|
11
|
+
const service = new ExampleService();
|
|
12
|
+
|
|
13
|
+
const config = {
|
|
14
|
+
name: 'user',
|
|
15
|
+
port: 3001,
|
|
16
|
+
database: 'postgres',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = service.generate(config);
|
|
20
|
+
|
|
21
|
+
expect(result.directories).toContain('user');
|
|
22
|
+
expect(result.directories).toContain('user/src');
|
|
23
|
+
expect(result.directories).toContain('user/src/routes');
|
|
24
|
+
expect(result.directories).toContain('user/migrations');
|
|
25
|
+
expect(result.directories).toContain('user/tests');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('generates all required files', async () => {
|
|
29
|
+
const { ExampleService } = await import('./example-service.js');
|
|
30
|
+
const service = new ExampleService();
|
|
31
|
+
|
|
32
|
+
const config = {
|
|
33
|
+
name: 'order',
|
|
34
|
+
port: 3002,
|
|
35
|
+
database: 'postgres',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const result = service.generate(config);
|
|
39
|
+
const filePaths = result.files.map(f => f.path);
|
|
40
|
+
|
|
41
|
+
expect(filePaths).toContain('order/package.json');
|
|
42
|
+
expect(filePaths).toContain('order/src/index.js');
|
|
43
|
+
expect(filePaths).toContain('order/src/routes/index.js');
|
|
44
|
+
expect(filePaths).toContain('order/Dockerfile');
|
|
45
|
+
expect(filePaths).toContain('order/docker-compose.yml');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('generatePackageJson', () => {
|
|
50
|
+
it('has correct name from config', async () => {
|
|
51
|
+
const { ExampleService } = await import('./example-service.js');
|
|
52
|
+
const service = new ExampleService();
|
|
53
|
+
|
|
54
|
+
const config = { name: 'inventory', port: 3003 };
|
|
55
|
+
|
|
56
|
+
const result = service.generatePackageJson(config);
|
|
57
|
+
const pkg = JSON.parse(result);
|
|
58
|
+
|
|
59
|
+
expect(pkg.name).toBe('inventory');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('includes express dependency', async () => {
|
|
63
|
+
const { ExampleService } = await import('./example-service.js');
|
|
64
|
+
const service = new ExampleService();
|
|
65
|
+
|
|
66
|
+
const config = { name: 'catalog', port: 3004 };
|
|
67
|
+
|
|
68
|
+
const result = service.generatePackageJson(config);
|
|
69
|
+
const pkg = JSON.parse(result);
|
|
70
|
+
|
|
71
|
+
expect(pkg.dependencies.express).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('includes pg when database is postgres', async () => {
|
|
75
|
+
const { ExampleService } = await import('./example-service.js');
|
|
76
|
+
const service = new ExampleService();
|
|
77
|
+
|
|
78
|
+
const config = { name: 'billing', port: 3005, database: 'postgres' };
|
|
79
|
+
|
|
80
|
+
const result = service.generatePackageJson(config);
|
|
81
|
+
const pkg = JSON.parse(result);
|
|
82
|
+
|
|
83
|
+
expect(pkg.dependencies.pg).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not include pg when no database specified', async () => {
|
|
87
|
+
const { ExampleService } = await import('./example-service.js');
|
|
88
|
+
const service = new ExampleService();
|
|
89
|
+
|
|
90
|
+
const config = { name: 'simple', port: 3006 };
|
|
91
|
+
|
|
92
|
+
const result = service.generatePackageJson(config);
|
|
93
|
+
const pkg = JSON.parse(result);
|
|
94
|
+
|
|
95
|
+
expect(pkg.dependencies.pg).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('includes required scripts', async () => {
|
|
99
|
+
const { ExampleService } = await import('./example-service.js');
|
|
100
|
+
const service = new ExampleService();
|
|
101
|
+
|
|
102
|
+
const config = { name: 'worker', port: 3007 };
|
|
103
|
+
|
|
104
|
+
const result = service.generatePackageJson(config);
|
|
105
|
+
const pkg = JSON.parse(result);
|
|
106
|
+
|
|
107
|
+
expect(pkg.scripts.start).toBeDefined();
|
|
108
|
+
expect(pkg.scripts.dev).toBeDefined();
|
|
109
|
+
expect(pkg.scripts.test).toBeDefined();
|
|
110
|
+
expect(pkg.scripts.migrate).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('generateIndex', () => {
|
|
115
|
+
it('creates express app', async () => {
|
|
116
|
+
const { ExampleService } = await import('./example-service.js');
|
|
117
|
+
const service = new ExampleService();
|
|
118
|
+
|
|
119
|
+
const config = { name: 'api', port: 3008 };
|
|
120
|
+
|
|
121
|
+
const result = service.generateIndex(config);
|
|
122
|
+
|
|
123
|
+
expect(result).toContain("require('express')");
|
|
124
|
+
expect(result).toContain('express()');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('has health endpoint', async () => {
|
|
128
|
+
const { ExampleService } = await import('./example-service.js');
|
|
129
|
+
const service = new ExampleService();
|
|
130
|
+
|
|
131
|
+
const config = { name: 'health-check', port: 3009 };
|
|
132
|
+
|
|
133
|
+
const result = service.generateIndex(config);
|
|
134
|
+
|
|
135
|
+
expect(result).toContain('/health');
|
|
136
|
+
expect(result).toContain('healthy');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('imports routes', async () => {
|
|
140
|
+
const { ExampleService } = await import('./example-service.js');
|
|
141
|
+
const service = new ExampleService();
|
|
142
|
+
|
|
143
|
+
const config = { name: 'router', port: 3010 };
|
|
144
|
+
|
|
145
|
+
const result = service.generateIndex(config);
|
|
146
|
+
|
|
147
|
+
expect(result).toContain("require('./routes')");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('has error middleware', async () => {
|
|
151
|
+
const { ExampleService } = await import('./example-service.js');
|
|
152
|
+
const service = new ExampleService();
|
|
153
|
+
|
|
154
|
+
const config = { name: 'errors', port: 3011 };
|
|
155
|
+
|
|
156
|
+
const result = service.generateIndex(config);
|
|
157
|
+
|
|
158
|
+
expect(result).toContain('err');
|
|
159
|
+
expect(result).toContain('500');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('generateRoutes', () => {
|
|
164
|
+
it('includes GET list endpoint', async () => {
|
|
165
|
+
const { ExampleService } = await import('./example-service.js');
|
|
166
|
+
const service = new ExampleService();
|
|
167
|
+
|
|
168
|
+
const config = { name: 'items', port: 3012 };
|
|
169
|
+
|
|
170
|
+
const result = service.generateRoutes(config);
|
|
171
|
+
|
|
172
|
+
expect(result).toContain("router.get('/'");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('includes GET by id endpoint', async () => {
|
|
176
|
+
const { ExampleService } = await import('./example-service.js');
|
|
177
|
+
const service = new ExampleService();
|
|
178
|
+
|
|
179
|
+
const config = { name: 'products', port: 3013 };
|
|
180
|
+
|
|
181
|
+
const result = service.generateRoutes(config);
|
|
182
|
+
|
|
183
|
+
expect(result).toContain("router.get('/:id'");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('includes POST create endpoint', async () => {
|
|
187
|
+
const { ExampleService } = await import('./example-service.js');
|
|
188
|
+
const service = new ExampleService();
|
|
189
|
+
|
|
190
|
+
const config = { name: 'users', port: 3014 };
|
|
191
|
+
|
|
192
|
+
const result = service.generateRoutes(config);
|
|
193
|
+
|
|
194
|
+
expect(result).toContain("router.post('/'");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('includes PUT update endpoint', async () => {
|
|
198
|
+
const { ExampleService } = await import('./example-service.js');
|
|
199
|
+
const service = new ExampleService();
|
|
200
|
+
|
|
201
|
+
const config = { name: 'accounts', port: 3015 };
|
|
202
|
+
|
|
203
|
+
const result = service.generateRoutes(config);
|
|
204
|
+
|
|
205
|
+
expect(result).toContain("router.put('/:id'");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('includes DELETE endpoint', async () => {
|
|
209
|
+
const { ExampleService } = await import('./example-service.js');
|
|
210
|
+
const service = new ExampleService();
|
|
211
|
+
|
|
212
|
+
const config = { name: 'tasks', port: 3016 };
|
|
213
|
+
|
|
214
|
+
const result = service.generateRoutes(config);
|
|
215
|
+
|
|
216
|
+
expect(result).toContain("router.delete('/:id'");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('generateMigrations', () => {
|
|
221
|
+
it('has up function', async () => {
|
|
222
|
+
const { ExampleService } = await import('./example-service.js');
|
|
223
|
+
const service = new ExampleService();
|
|
224
|
+
|
|
225
|
+
const config = { name: 'migrate', port: 3017, database: 'postgres' };
|
|
226
|
+
|
|
227
|
+
const result = service.generateMigrations(config);
|
|
228
|
+
|
|
229
|
+
expect(result.content).toContain('async function up');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('has down function', async () => {
|
|
233
|
+
const { ExampleService } = await import('./example-service.js');
|
|
234
|
+
const service = new ExampleService();
|
|
235
|
+
|
|
236
|
+
const config = { name: 'rollback', port: 3018, database: 'postgres' };
|
|
237
|
+
|
|
238
|
+
const result = service.generateMigrations(config);
|
|
239
|
+
|
|
240
|
+
expect(result.content).toContain('async function down');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('has timestamp naming', async () => {
|
|
244
|
+
const { ExampleService } = await import('./example-service.js');
|
|
245
|
+
const service = new ExampleService();
|
|
246
|
+
|
|
247
|
+
const config = { name: 'schema', port: 3019, database: 'postgres' };
|
|
248
|
+
|
|
249
|
+
const result = service.generateMigrations(config);
|
|
250
|
+
|
|
251
|
+
// Should have timestamp format like 001_initial_schema.js or YYYYMMDD format
|
|
252
|
+
expect(result.path).toMatch(/\d+.*\.js$/);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('generateTests', () => {
|
|
257
|
+
it('covers CRUD operations', async () => {
|
|
258
|
+
const { ExampleService } = await import('./example-service.js');
|
|
259
|
+
const service = new ExampleService();
|
|
260
|
+
|
|
261
|
+
const config = { name: 'crud', port: 3020 };
|
|
262
|
+
|
|
263
|
+
const result = service.generateTests(config);
|
|
264
|
+
|
|
265
|
+
expect(result.unit).toContain('GET');
|
|
266
|
+
expect(result.unit).toContain('POST');
|
|
267
|
+
expect(result.unit).toContain('PUT');
|
|
268
|
+
expect(result.unit).toContain('DELETE');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('has integration test examples', async () => {
|
|
272
|
+
const { ExampleService } = await import('./example-service.js');
|
|
273
|
+
const service = new ExampleService();
|
|
274
|
+
|
|
275
|
+
const config = { name: 'integration', port: 3021 };
|
|
276
|
+
|
|
277
|
+
const result = service.generateTests(config);
|
|
278
|
+
|
|
279
|
+
expect(result.integration).toContain('describe');
|
|
280
|
+
expect(result.integration).toContain('it(');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('generateDockerfile', () => {
|
|
285
|
+
it('has multi-stage build', async () => {
|
|
286
|
+
const { ExampleService } = await import('./example-service.js');
|
|
287
|
+
const service = new ExampleService();
|
|
288
|
+
|
|
289
|
+
const config = { name: 'docker', port: 3022 };
|
|
290
|
+
|
|
291
|
+
const result = service.generateDockerfile(config);
|
|
292
|
+
|
|
293
|
+
expect(result).toContain('FROM');
|
|
294
|
+
expect(result).toContain('AS');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('has health check', async () => {
|
|
298
|
+
const { ExampleService } = await import('./example-service.js');
|
|
299
|
+
const service = new ExampleService();
|
|
300
|
+
|
|
301
|
+
const config = { name: 'healthcheck', port: 3023 };
|
|
302
|
+
|
|
303
|
+
const result = service.generateDockerfile(config);
|
|
304
|
+
|
|
305
|
+
expect(result).toContain('HEALTHCHECK');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('has non-root user', async () => {
|
|
309
|
+
const { ExampleService } = await import('./example-service.js');
|
|
310
|
+
const service = new ExampleService();
|
|
311
|
+
|
|
312
|
+
const config = { name: 'secure', port: 3024 };
|
|
313
|
+
|
|
314
|
+
const result = service.generateDockerfile(config);
|
|
315
|
+
|
|
316
|
+
expect(result).toContain('USER');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('generateDockerCompose', () => {
|
|
321
|
+
it('includes service container', async () => {
|
|
322
|
+
const { ExampleService } = await import('./example-service.js');
|
|
323
|
+
const service = new ExampleService();
|
|
324
|
+
|
|
325
|
+
const config = { name: 'compose', port: 3025 };
|
|
326
|
+
|
|
327
|
+
const result = service.generateDockerCompose(config);
|
|
328
|
+
|
|
329
|
+
expect(result).toContain('compose');
|
|
330
|
+
expect(result).toContain('services:');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('includes database when configured', async () => {
|
|
334
|
+
const { ExampleService } = await import('./example-service.js');
|
|
335
|
+
const service = new ExampleService();
|
|
336
|
+
|
|
337
|
+
const config = { name: 'db-compose', port: 3026, database: 'postgres' };
|
|
338
|
+
|
|
339
|
+
const result = service.generateDockerCompose(config);
|
|
340
|
+
|
|
341
|
+
expect(result).toContain('postgres');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('has network configuration', async () => {
|
|
345
|
+
const { ExampleService } = await import('./example-service.js');
|
|
346
|
+
const service = new ExampleService();
|
|
347
|
+
|
|
348
|
+
const config = { name: 'network', port: 3027 };
|
|
349
|
+
|
|
350
|
+
const result = service.generateDockerCompose(config);
|
|
351
|
+
|
|
352
|
+
expect(result).toContain('networks:');
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('edge cases', () => {
|
|
357
|
+
it('handles service without database', async () => {
|
|
358
|
+
const { ExampleService } = await import('./example-service.js');
|
|
359
|
+
const service = new ExampleService();
|
|
360
|
+
|
|
361
|
+
const config = { name: 'no-db', port: 3028 };
|
|
362
|
+
|
|
363
|
+
const result = service.generate(config);
|
|
364
|
+
|
|
365
|
+
// Should still generate basic structure
|
|
366
|
+
expect(result.directories).toContain('no-db');
|
|
367
|
+
expect(result.files.length).toBeGreaterThan(0);
|
|
368
|
+
|
|
369
|
+
// Docker compose should NOT have postgres
|
|
370
|
+
const composeFile = result.files.find(f => f.path.endsWith('docker-compose.yml'));
|
|
371
|
+
expect(composeFile.content).not.toContain('postgres');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('service starts without errors (structure check)', async () => {
|
|
375
|
+
const { ExampleService } = await import('./example-service.js');
|
|
376
|
+
const service = new ExampleService();
|
|
377
|
+
|
|
378
|
+
const config = { name: 'valid', port: 3029, database: 'postgres' };
|
|
379
|
+
|
|
380
|
+
const result = service.generate(config);
|
|
381
|
+
|
|
382
|
+
// Verify complete structure
|
|
383
|
+
expect(result.directories).toBeDefined();
|
|
384
|
+
expect(result.files).toBeDefined();
|
|
385
|
+
expect(Array.isArray(result.directories)).toBe(true);
|
|
386
|
+
expect(Array.isArray(result.files)).toBe(true);
|
|
387
|
+
|
|
388
|
+
// Each file should have path and content
|
|
389
|
+
for (const file of result.files) {
|
|
390
|
+
expect(file.path).toBeDefined();
|
|
391
|
+
expect(file.content).toBeDefined();
|
|
392
|
+
expect(typeof file.path).toBe('string');
|
|
393
|
+
expect(typeof file.content).toBe('string');
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Scorer
|
|
3
|
+
* Calculate priority score for refactoring opportunities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
class ImpactScorer {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.weights = {
|
|
11
|
+
complexityReduction: options.complexityWeight || 0.30,
|
|
12
|
+
blastRadius: options.blastRadiusWeight || 0.25,
|
|
13
|
+
changeFrequency: options.frequencyWeight || 0.25,
|
|
14
|
+
risk: options.riskWeight || 0.20,
|
|
15
|
+
};
|
|
16
|
+
this.exec = options.exec || this.defaultExec.bind(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
defaultExec(command) {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(command, { encoding: 'utf-8' });
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate impact score for a refactoring opportunity
|
|
29
|
+
* @param {Object} opportunity - Refactoring opportunity
|
|
30
|
+
* @returns {Object} Score breakdown and total
|
|
31
|
+
*/
|
|
32
|
+
score(opportunity) {
|
|
33
|
+
const complexityScore = this.scoreComplexityReduction(opportunity);
|
|
34
|
+
const blastRadiusScore = this.scoreBlastRadius(opportunity);
|
|
35
|
+
const frequencyScore = this.scoreChangeFrequency(opportunity);
|
|
36
|
+
const riskScore = this.scoreRisk(opportunity);
|
|
37
|
+
|
|
38
|
+
const total = Math.round(
|
|
39
|
+
complexityScore * this.weights.complexityReduction +
|
|
40
|
+
blastRadiusScore * this.weights.blastRadius +
|
|
41
|
+
frequencyScore * this.weights.changeFrequency +
|
|
42
|
+
riskScore * this.weights.risk
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
total: Math.min(100, Math.max(0, total)),
|
|
47
|
+
breakdown: {
|
|
48
|
+
complexityReduction: complexityScore,
|
|
49
|
+
blastRadius: blastRadiusScore,
|
|
50
|
+
changeFrequency: frequencyScore,
|
|
51
|
+
risk: riskScore,
|
|
52
|
+
},
|
|
53
|
+
weights: this.weights,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Score based on complexity reduction potential
|
|
59
|
+
* Higher complexity = higher score (more to gain)
|
|
60
|
+
*/
|
|
61
|
+
scoreComplexityReduction(opportunity) {
|
|
62
|
+
const { complexity, targetComplexity } = opportunity;
|
|
63
|
+
|
|
64
|
+
if (!complexity) return 50; // Default middle score
|
|
65
|
+
|
|
66
|
+
const reduction = complexity - (targetComplexity || 1);
|
|
67
|
+
|
|
68
|
+
// Scale: 0-5 reduction = 0-50, 5-15 = 50-80, 15+ = 80-100
|
|
69
|
+
if (reduction <= 0) return 20;
|
|
70
|
+
if (reduction <= 5) return 20 + (reduction * 10);
|
|
71
|
+
if (reduction <= 15) return 70 + ((reduction - 5) * 2);
|
|
72
|
+
return 90 + Math.min(10, (reduction - 15));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Score based on blast radius (files affected)
|
|
77
|
+
*/
|
|
78
|
+
scoreBlastRadius(opportunity) {
|
|
79
|
+
const { filesAffected, linesAffected } = opportunity;
|
|
80
|
+
|
|
81
|
+
// More files affected = higher impact
|
|
82
|
+
let score = 30; // base
|
|
83
|
+
|
|
84
|
+
if (filesAffected) {
|
|
85
|
+
if (filesAffected === 1) score = 40;
|
|
86
|
+
else if (filesAffected <= 3) score = 60;
|
|
87
|
+
else if (filesAffected <= 10) score = 80;
|
|
88
|
+
else score = 95;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Adjust for lines affected
|
|
92
|
+
if (linesAffected) {
|
|
93
|
+
if (linesAffected > 100) score = Math.min(100, score + 10);
|
|
94
|
+
if (linesAffected > 500) score = Math.min(100, score + 10);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return score;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Score based on change frequency from git history
|
|
102
|
+
*/
|
|
103
|
+
scoreChangeFrequency(opportunity) {
|
|
104
|
+
const { filePath, changeCount } = opportunity;
|
|
105
|
+
|
|
106
|
+
// If changeCount provided, use it
|
|
107
|
+
if (changeCount !== undefined) {
|
|
108
|
+
if (changeCount === 0) return 30;
|
|
109
|
+
if (changeCount <= 5) return 50;
|
|
110
|
+
if (changeCount <= 20) return 70;
|
|
111
|
+
if (changeCount <= 50) return 85;
|
|
112
|
+
return 95;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Otherwise try to get from git
|
|
116
|
+
if (filePath) {
|
|
117
|
+
try {
|
|
118
|
+
const output = this.exec(`git log --oneline "${filePath}" 2>/dev/null | wc -l`);
|
|
119
|
+
const commits = parseInt(output.trim(), 10) || 0;
|
|
120
|
+
|
|
121
|
+
if (commits === 0) return 30;
|
|
122
|
+
if (commits <= 5) return 50;
|
|
123
|
+
if (commits <= 20) return 70;
|
|
124
|
+
if (commits <= 50) return 85;
|
|
125
|
+
return 95;
|
|
126
|
+
} catch {
|
|
127
|
+
return 50; // Default if git fails
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 50;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Score based on risk (test coverage, criticality)
|
|
136
|
+
* Lower coverage = higher risk = higher priority to refactor safely
|
|
137
|
+
*/
|
|
138
|
+
scoreRisk(opportunity) {
|
|
139
|
+
const { testCoverage, isCritical } = opportunity;
|
|
140
|
+
|
|
141
|
+
let score = 50;
|
|
142
|
+
|
|
143
|
+
// Lower test coverage = higher risk score
|
|
144
|
+
if (testCoverage !== undefined) {
|
|
145
|
+
if (testCoverage >= 80) score = 30;
|
|
146
|
+
else if (testCoverage >= 60) score = 50;
|
|
147
|
+
else if (testCoverage >= 40) score = 70;
|
|
148
|
+
else if (testCoverage >= 20) score = 85;
|
|
149
|
+
else score = 95;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Critical paths get higher score
|
|
153
|
+
if (isCritical) {
|
|
154
|
+
score = Math.min(100, score + 15);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return score;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Score multiple opportunities and sort by impact
|
|
162
|
+
* @param {Array} opportunities - Array of opportunities
|
|
163
|
+
* @returns {Array} Sorted opportunities with scores
|
|
164
|
+
*/
|
|
165
|
+
scoreAll(opportunities) {
|
|
166
|
+
return opportunities
|
|
167
|
+
.map(opp => ({
|
|
168
|
+
...opp,
|
|
169
|
+
impact: this.score(opp),
|
|
170
|
+
}))
|
|
171
|
+
.sort((a, b) => b.impact.total - a.impact.total);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get priority tier from score
|
|
176
|
+
*/
|
|
177
|
+
static getTier(score) {
|
|
178
|
+
if (score >= 80) return 'high';
|
|
179
|
+
if (score >= 50) return 'medium';
|
|
180
|
+
return 'low';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { ImpactScorer };
|