@paths.design/caws-cli 2.0.1 ā 3.0.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/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -96
- package/package.json +3 -2
- package/templates/agents.md +820 -0
- package/templates/apps/tools/caws/COMPLETION_REPORT.md +331 -0
- package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +360 -0
- package/templates/apps/tools/caws/README.md +463 -0
- package/templates/apps/tools/caws/TEST_STATUS.md +365 -0
- package/templates/apps/tools/caws/attest.js +357 -0
- package/templates/apps/tools/caws/ci-optimizer.js +642 -0
- package/templates/apps/tools/caws/config.ts +245 -0
- package/templates/apps/tools/caws/cross-functional.js +876 -0
- package/templates/apps/tools/caws/dashboard.js +1112 -0
- package/templates/apps/tools/caws/flake-detector.ts +362 -0
- package/templates/apps/tools/caws/gates.js +198 -0
- package/templates/apps/tools/caws/gates.ts +237 -0
- package/templates/apps/tools/caws/language-adapters.ts +381 -0
- package/templates/apps/tools/caws/language-support.d.ts +367 -0
- package/templates/apps/tools/caws/language-support.d.ts.map +1 -0
- package/templates/apps/tools/caws/language-support.js +585 -0
- package/templates/apps/tools/caws/legacy-assessment.ts +408 -0
- package/templates/apps/tools/caws/legacy-assessor.js +764 -0
- package/templates/apps/tools/caws/mutant-analyzer.js +734 -0
- package/templates/apps/tools/caws/perf-budgets.ts +349 -0
- package/templates/apps/tools/caws/property-testing.js +707 -0
- package/templates/apps/tools/caws/provenance.d.ts +14 -0
- package/templates/apps/tools/caws/provenance.d.ts.map +1 -0
- package/templates/apps/tools/caws/provenance.js +132 -0
- package/templates/apps/tools/caws/provenance.ts +211 -0
- package/templates/apps/tools/caws/schemas/waivers.schema.json +30 -0
- package/templates/apps/tools/caws/schemas/working-spec.schema.json +115 -0
- package/templates/apps/tools/caws/scope-guard.js +208 -0
- package/templates/apps/tools/caws/security-provenance.ts +483 -0
- package/templates/apps/tools/caws/shared/base-tool.ts +281 -0
- package/templates/apps/tools/caws/shared/config-manager.ts +366 -0
- package/templates/apps/tools/caws/shared/gate-checker.ts +597 -0
- package/templates/apps/tools/caws/shared/types.ts +444 -0
- package/templates/apps/tools/caws/shared/validator.ts +305 -0
- package/templates/apps/tools/caws/shared/waivers-manager.ts +174 -0
- package/templates/apps/tools/caws/spec-test-mapper.ts +391 -0
- package/templates/apps/tools/caws/templates/working-spec.template.yml +60 -0
- package/templates/apps/tools/caws/test-quality.js +578 -0
- package/templates/apps/tools/caws/tools-allow.json +331 -0
- package/templates/apps/tools/caws/validate.js +76 -0
- package/templates/apps/tools/caws/validate.ts +228 -0
- package/templates/apps/tools/caws/waivers.js +344 -0
- package/templates/apps/tools/caws/waivers.yml +19 -0
- package/templates/codemod/README.md +1 -0
- package/templates/codemod/test.js +1 -0
- package/templates/docs/README.md +150 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CAWS Legacy Assessment Tool
|
|
5
|
+
* Assesses legacy code for CAWS migration and generates migration plans
|
|
6
|
+
*
|
|
7
|
+
* @author @darianrosebrook
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { CawsBaseTool } from './shared/base-tool.js';
|
|
13
|
+
|
|
14
|
+
interface LegacyAssessment {
|
|
15
|
+
module: string;
|
|
16
|
+
complexity: number;
|
|
17
|
+
coverage: number;
|
|
18
|
+
changeFrequency: number;
|
|
19
|
+
dependencies: number;
|
|
20
|
+
recommendedTier: number;
|
|
21
|
+
migrationPriority: 'high' | 'medium' | 'low';
|
|
22
|
+
quickWins: string[];
|
|
23
|
+
estimatedEffort: 'small' | 'medium' | 'large';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MigrationPlan {
|
|
27
|
+
phase: number;
|
|
28
|
+
modules: string[];
|
|
29
|
+
estimatedDays: number;
|
|
30
|
+
dependencies: string[];
|
|
31
|
+
risks: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class LegacyAssessmentTool extends CawsBaseTool {
|
|
35
|
+
/**
|
|
36
|
+
* Assess a legacy module for CAWS migration
|
|
37
|
+
*/
|
|
38
|
+
async assessModule(modulePath: string): Promise<LegacyAssessment> {
|
|
39
|
+
const complexity = await this.calculateComplexity(modulePath);
|
|
40
|
+
const coverage = await this.getCurrentCoverage(modulePath);
|
|
41
|
+
const changeFrequency = await this.analyzeChangeFrequency(modulePath);
|
|
42
|
+
const dependencies = await this.analyzeDependencies(modulePath);
|
|
43
|
+
|
|
44
|
+
const recommendedTier = this.inferTier(coverage, dependencies, changeFrequency);
|
|
45
|
+
|
|
46
|
+
const migrationPriority = this.calculatePriority(changeFrequency, coverage, complexity);
|
|
47
|
+
|
|
48
|
+
const quickWins = this.identifyQuickWins(modulePath, coverage);
|
|
49
|
+
|
|
50
|
+
const estimatedEffort = this.estimateEffort(complexity, coverage);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
module: modulePath,
|
|
54
|
+
complexity,
|
|
55
|
+
coverage,
|
|
56
|
+
changeFrequency,
|
|
57
|
+
dependencies,
|
|
58
|
+
recommendedTier,
|
|
59
|
+
migrationPriority,
|
|
60
|
+
quickWins,
|
|
61
|
+
estimatedEffort,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate migration plan for legacy codebase
|
|
67
|
+
*/
|
|
68
|
+
async generateMigrationPlan(projectDir: string): Promise<{
|
|
69
|
+
phases: MigrationPlan[];
|
|
70
|
+
totalEstimatedDays: number;
|
|
71
|
+
criticalPath: string[];
|
|
72
|
+
}> {
|
|
73
|
+
const modules = await this.findModules(projectDir);
|
|
74
|
+
const assessments = await Promise.all(modules.map((m) => this.assessModule(m)));
|
|
75
|
+
|
|
76
|
+
// Sort by priority and dependencies
|
|
77
|
+
const sortedModules = assessments.sort((a, b) => {
|
|
78
|
+
const priorityScore = { high: 3, medium: 2, low: 1 };
|
|
79
|
+
return priorityScore[b.migrationPriority] - priorityScore[a.migrationPriority];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const phases: MigrationPlan[] = [];
|
|
83
|
+
let currentPhase: string[] = [];
|
|
84
|
+
let phaseNumber = 1;
|
|
85
|
+
let totalDays = 0;
|
|
86
|
+
|
|
87
|
+
const effortDays = { small: 2, medium: 5, large: 10 };
|
|
88
|
+
|
|
89
|
+
for (const assessment of sortedModules) {
|
|
90
|
+
if (currentPhase.length >= 3) {
|
|
91
|
+
// Max 3 modules per phase
|
|
92
|
+
const phaseDays = currentPhase.reduce(
|
|
93
|
+
(sum, mod) =>
|
|
94
|
+
sum +
|
|
95
|
+
effortDays[sortedModules.find((a) => a.module === mod)?.estimatedEffort || 'medium'],
|
|
96
|
+
0
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
phases.push({
|
|
100
|
+
phase: phaseNumber++,
|
|
101
|
+
modules: [...currentPhase],
|
|
102
|
+
estimatedDays: phaseDays,
|
|
103
|
+
dependencies: [],
|
|
104
|
+
risks: ['Dependencies may require coordination'],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
totalDays += phaseDays;
|
|
108
|
+
currentPhase = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
currentPhase.push(assessment.module);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Add final phase
|
|
115
|
+
if (currentPhase.length > 0) {
|
|
116
|
+
const phaseDays = currentPhase.reduce(
|
|
117
|
+
(sum, mod) =>
|
|
118
|
+
sum +
|
|
119
|
+
effortDays[sortedModules.find((a) => a.module === mod)?.estimatedEffort || 'medium'],
|
|
120
|
+
0
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
phases.push({
|
|
124
|
+
phase: phaseNumber,
|
|
125
|
+
modules: currentPhase,
|
|
126
|
+
estimatedDays: phaseDays,
|
|
127
|
+
dependencies: [],
|
|
128
|
+
risks: [],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
totalDays += phaseDays;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const criticalPath = sortedModules
|
|
135
|
+
.filter((a) => a.migrationPriority === 'high')
|
|
136
|
+
.map((a) => a.module);
|
|
137
|
+
|
|
138
|
+
return { phases, totalEstimatedDays: totalDays, criticalPath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async findModules(projectDir: string): Promise<string[]> {
|
|
142
|
+
const modules: string[] = [];
|
|
143
|
+
const srcDir = path.join(projectDir, 'src');
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(srcDir)) {
|
|
146
|
+
return modules;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (entry.isDirectory()) {
|
|
152
|
+
const modulePath = path.join('src', entry.name);
|
|
153
|
+
modules.push(modulePath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return modules;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async calculateComplexity(modulePath: string): Promise<number> {
|
|
161
|
+
// Simplified complexity calculation
|
|
162
|
+
try {
|
|
163
|
+
const files = this.findFilesRecursive(modulePath);
|
|
164
|
+
let totalLines = 0;
|
|
165
|
+
let cyclomaticSum = 0;
|
|
166
|
+
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
169
|
+
const lines = content.split('\n').length;
|
|
170
|
+
totalLines += lines;
|
|
171
|
+
|
|
172
|
+
// Count control flow statements as proxy for cyclomatic complexity
|
|
173
|
+
const controlFlow = (
|
|
174
|
+
content.match(/\b(if|else|for|while|switch|case|catch|&&|\|\|)\b/g) || []
|
|
175
|
+
).length;
|
|
176
|
+
cyclomaticSum += controlFlow;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return files.length > 0 ? cyclomaticSum / files.length : 0;
|
|
180
|
+
} catch {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async getCurrentCoverage(modulePath: string): Promise<number> {
|
|
186
|
+
try {
|
|
187
|
+
const coveragePath = path.join(process.cwd(), 'coverage', 'coverage-final.json');
|
|
188
|
+
if (!fs.existsSync(coveragePath)) {
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const coverageData = JSON.parse(fs.readFileSync(coveragePath, 'utf-8'));
|
|
193
|
+
let totalStatements = 0;
|
|
194
|
+
let coveredStatements = 0;
|
|
195
|
+
|
|
196
|
+
for (const [file, data] of Object.entries(coverageData)) {
|
|
197
|
+
if (file.includes(modulePath)) {
|
|
198
|
+
const fileData = data as any;
|
|
199
|
+
if (fileData.s) {
|
|
200
|
+
totalStatements += Object.keys(fileData.s).length;
|
|
201
|
+
coveredStatements += Object.values(fileData.s).filter((s: any) => s > 0).length;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return totalStatements > 0 ? coveredStatements / totalStatements : 0;
|
|
207
|
+
} catch {
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async analyzeChangeFrequency(modulePath: string): Promise<number> {
|
|
213
|
+
// Simplified - in real implementation, use git log
|
|
214
|
+
try {
|
|
215
|
+
const files = this.findFilesRecursive(modulePath);
|
|
216
|
+
// Placeholder: return based on number of files as proxy
|
|
217
|
+
return Math.min(files.length / 10, 1);
|
|
218
|
+
} catch {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async analyzeDependencies(modulePath: string): Promise<number> {
|
|
224
|
+
try {
|
|
225
|
+
const files = this.findFilesRecursive(modulePath);
|
|
226
|
+
let importCount = 0;
|
|
227
|
+
|
|
228
|
+
for (const file of files) {
|
|
229
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
230
|
+
const imports = content.match(/^import .* from/gm) || [];
|
|
231
|
+
importCount += imports.length;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return files.length > 0 ? importCount / files.length : 0;
|
|
235
|
+
} catch {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private inferTier(coverage: number, dependencies: number, changeFrequency: number): number {
|
|
241
|
+
// High change frequency + low coverage = critical (Tier 1)
|
|
242
|
+
if (changeFrequency > 0.7 && coverage < 0.5) {
|
|
243
|
+
return 1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Medium activity
|
|
247
|
+
if (changeFrequency > 0.4 || dependencies > 5) {
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Low activity, isolated
|
|
252
|
+
return 3;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private calculatePriority(
|
|
256
|
+
changeFrequency: number,
|
|
257
|
+
coverage: number,
|
|
258
|
+
complexity: number
|
|
259
|
+
): 'high' | 'medium' | 'low' {
|
|
260
|
+
const score = changeFrequency * 0.4 + (1 - coverage) * 0.4 + complexity * 0.2;
|
|
261
|
+
|
|
262
|
+
if (score > 0.7) return 'high';
|
|
263
|
+
if (score > 0.4) return 'medium';
|
|
264
|
+
return 'low';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private identifyQuickWins(modulePath: string, coverage: number): string[] {
|
|
268
|
+
const wins: string[] = [];
|
|
269
|
+
|
|
270
|
+
if (coverage === 0) {
|
|
271
|
+
wins.push('Add basic smoke tests for main functions');
|
|
272
|
+
} else if (coverage < 0.3) {
|
|
273
|
+
wins.push('Increase coverage by testing happy paths');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const files = this.findFilesRecursive(modulePath);
|
|
277
|
+
if (files.some((f) => !f.includes('.test.') && !f.includes('.spec.'))) {
|
|
278
|
+
wins.push('Add test files for untested modules');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
wins.push('Extract pure functions for easier testing');
|
|
282
|
+
wins.push('Add type definitions if missing');
|
|
283
|
+
|
|
284
|
+
return wins;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private estimateEffort(complexity: number, coverage: number): 'small' | 'medium' | 'large' {
|
|
288
|
+
const effortScore = complexity * 0.6 + (1 - coverage) * 0.4;
|
|
289
|
+
|
|
290
|
+
if (effortScore > 0.7) return 'large';
|
|
291
|
+
if (effortScore > 0.4) return 'medium';
|
|
292
|
+
return 'small';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private findFilesRecursive(dir: string, files: string[] = []): string[] {
|
|
296
|
+
try {
|
|
297
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
298
|
+
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
const fullPath = path.join(dir, entry.name);
|
|
301
|
+
if (entry.isDirectory() && !entry.name.includes('node_modules')) {
|
|
302
|
+
this.findFilesRecursive(fullPath, files);
|
|
303
|
+
} else if (entry.isFile() && (entry.name.endsWith('.ts') || entry.name.endsWith('.js'))) {
|
|
304
|
+
files.push(fullPath);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
// Directory doesn't exist
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return files;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// CLI interface
|
|
316
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
317
|
+
(async () => {
|
|
318
|
+
const command = process.argv[2];
|
|
319
|
+
const tool = new LegacyAssessmentTool();
|
|
320
|
+
|
|
321
|
+
switch (command) {
|
|
322
|
+
case 'assess': {
|
|
323
|
+
const modulePath = process.argv[3];
|
|
324
|
+
if (!modulePath) {
|
|
325
|
+
console.error('Usage: legacy-assessment assess <module-path>');
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const assessment = await tool.assessModule(modulePath);
|
|
331
|
+
|
|
332
|
+
console.log('\nš Legacy Module Assessment');
|
|
333
|
+
console.log('='.repeat(50));
|
|
334
|
+
console.log(`Module: ${assessment.module}`);
|
|
335
|
+
console.log(`Complexity: ${assessment.complexity.toFixed(2)} (avg cyclomatic per file)`);
|
|
336
|
+
console.log(`Coverage: ${(assessment.coverage * 100).toFixed(1)}%`);
|
|
337
|
+
console.log(`Change Frequency: ${(assessment.changeFrequency * 100).toFixed(1)}%`);
|
|
338
|
+
console.log(`Avg Dependencies: ${assessment.dependencies.toFixed(1)}`);
|
|
339
|
+
console.log(`\nRecommended Tier: ${assessment.recommendedTier}`);
|
|
340
|
+
console.log(`Migration Priority: ${assessment.migrationPriority}`);
|
|
341
|
+
console.log(`Estimated Effort: ${assessment.estimatedEffort}`);
|
|
342
|
+
|
|
343
|
+
if (assessment.quickWins.length > 0) {
|
|
344
|
+
console.log(`\nšÆ Quick Wins:`);
|
|
345
|
+
assessment.quickWins.forEach((win) => {
|
|
346
|
+
console.log(` - ${win}`);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error(`ā Assessment failed: ${error}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
case 'plan': {
|
|
357
|
+
const projectDir = process.argv[3] || process.cwd();
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const plan = await tool.generateMigrationPlan(projectDir);
|
|
361
|
+
|
|
362
|
+
console.log('\nšļø Legacy Migration Plan');
|
|
363
|
+
console.log('='.repeat(50));
|
|
364
|
+
console.log(`Total Estimated Days: ${plan.totalEstimatedDays}`);
|
|
365
|
+
console.log(`Number of Phases: ${plan.phases.length}`);
|
|
366
|
+
|
|
367
|
+
if (plan.criticalPath.length > 0) {
|
|
368
|
+
console.log(`\nš“ Critical Path Modules:`);
|
|
369
|
+
plan.criticalPath.forEach((mod) => {
|
|
370
|
+
console.log(` - ${mod}`);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(`\nš Migration Phases:`);
|
|
375
|
+
plan.phases.forEach((phase) => {
|
|
376
|
+
console.log(`\nPhase ${phase.phase} (${phase.estimatedDays} days):`);
|
|
377
|
+
console.log(` Modules:`);
|
|
378
|
+
phase.modules.forEach((mod) => {
|
|
379
|
+
console.log(` - ${mod}`);
|
|
380
|
+
});
|
|
381
|
+
if (phase.risks.length > 0) {
|
|
382
|
+
console.log(` Risks:`);
|
|
383
|
+
phase.risks.forEach((risk) => {
|
|
384
|
+
console.log(` ā ļø ${risk}`);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error(`ā Plan generation failed: ${error}`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
default:
|
|
396
|
+
console.log('CAWS Legacy Assessment Tool');
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log('Usage:');
|
|
399
|
+
console.log(' legacy-assessment assess <module-path> - Assess legacy module');
|
|
400
|
+
console.log(' legacy-assessment plan [project-dir] - Generate migration plan');
|
|
401
|
+
console.log('');
|
|
402
|
+
console.log('Examples:');
|
|
403
|
+
console.log(' legacy-assessment assess src/auth');
|
|
404
|
+
console.log(' legacy-assessment plan .');
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
})();
|
|
408
|
+
}
|