@paths.design/caws-cli 8.0.1 → 8.1.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/commands/archive.d.ts +2 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +114 -6
- package/dist/commands/burnup.d.ts.map +1 -1
- package/dist/commands/burnup.js +109 -10
- package/dist/commands/diagnose.js +1 -1
- package/dist/commands/mode.js +24 -14
- package/dist/commands/provenance.js +216 -93
- package/dist/commands/quality-gates.d.ts.map +1 -1
- package/dist/commands/quality-gates.js +3 -1
- package/dist/commands/specs.js +184 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +134 -10
- package/dist/commands/templates.js +2 -2
- package/dist/error-handler.js +6 -98
- package/dist/generators/jest-config-generator.js +242 -0
- package/dist/index.js +4 -7
- package/dist/minimal-cli.js +3 -1
- package/dist/scaffold/claude-hooks.js +316 -0
- package/dist/scaffold/index.js +18 -0
- package/dist/templates/.claude/README.md +190 -0
- package/dist/templates/.claude/hooks/audit.sh +96 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/dist/templates/.claude/hooks/naming-check.sh +97 -0
- package/dist/templates/.claude/hooks/quality-check.sh +68 -0
- package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +105 -0
- package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
- package/dist/templates/.claude/settings.json +95 -0
- package/dist/test-analysis.js +203 -10
- package/dist/utils/error-categories.js +210 -0
- package/dist/utils/quality-gates-utils.js +402 -0
- package/dist/utils/typescript-detector.js +36 -90
- package/dist/validation/spec-validation.js +59 -6
- package/package.json +5 -3
- package/templates/.claude/README.md +190 -0
- package/templates/.claude/hooks/audit.sh +96 -0
- package/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/templates/.claude/hooks/naming-check.sh +97 -0
- package/templates/.claude/hooks/quality-check.sh +68 -0
- package/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/templates/.claude/hooks/scope-guard.sh +105 -0
- package/templates/.claude/hooks/validate-spec.sh +76 -0
- package/templates/.claude/settings.json +95 -0
|
@@ -18,10 +18,11 @@ export function loadChange(changeId: string): Promise<any | null>;
|
|
|
18
18
|
export function validateAcceptanceCriteria(workingSpec: any): Promise<any>;
|
|
19
19
|
/**
|
|
20
20
|
* Validate change meets quality gates
|
|
21
|
+
* Runs the actual quality gates runner and checks for violations
|
|
21
22
|
* @param {string} changeId - Change identifier
|
|
22
23
|
* @returns {Promise<Object>} Quality gate result
|
|
23
24
|
*/
|
|
24
|
-
export function validateQualityGates(
|
|
25
|
+
export function validateQualityGates(changeId: string): Promise<any>;
|
|
25
26
|
/**
|
|
26
27
|
* Generate change summary for archival
|
|
27
28
|
* @param {Object} change - Change data
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/commands/archive.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/commands/archive.js"],"names":[],"mappings":"AA0VA;;;;GAIG;AACH,yCAHW,MAAM,+BAqGhB;AAjbD;;;;GAIG;AACH,qCAHW,MAAM,GACJ,OAAO,CAAC,MAAO,IAAI,CAAC,CAgChC;AAED;;;;GAIG;AACH,8DAFa,OAAO,KAAQ,CA8B3B;AAED;;;;;GAKG;AACH,+CAHW,MAAM,GACJ,OAAO,KAAQ,CAmH3B;AAED;;;;GAIG;AACH,oDAFa,OAAO,CAAC,MAAM,CAAC,CAgC3B;AAED;;;;GAIG;AACH,4CAFa,OAAO,CAAC,IAAI,CAAC,CAazB;AAED;;;;GAIG;AACH,+CAFa,OAAO,CAAC,IAAI,CAAC,CAoCzB;AAED;;;;;GAKG;AACH,6FAiCC"}
|
package/dist/commands/archive.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs = require('fs-extra');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const yaml = require('js-yaml');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
11
12
|
const { safeAsync, outputResult } = require('../error-handler');
|
|
12
13
|
|
|
13
14
|
// Import spec resolution system
|
|
@@ -87,16 +88,123 @@ async function validateAcceptanceCriteria(workingSpec) {
|
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
90
|
* Validate change meets quality gates
|
|
91
|
+
* Runs the actual quality gates runner and checks for violations
|
|
90
92
|
* @param {string} changeId - Change identifier
|
|
91
93
|
* @returns {Promise<Object>} Quality gate result
|
|
92
94
|
*/
|
|
93
95
|
async function validateQualityGates(_changeId) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
const gates = [];
|
|
97
|
+
const violations = [];
|
|
98
|
+
const warnings = [];
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// Try to run the quality gates runner
|
|
102
|
+
const qualityGatesPath = path.join(
|
|
103
|
+
__dirname,
|
|
104
|
+
'..',
|
|
105
|
+
'..',
|
|
106
|
+
'..',
|
|
107
|
+
'quality-gates',
|
|
108
|
+
'run-quality-gates.mjs'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Check if quality gates runner exists
|
|
112
|
+
if (await fs.pathExists(qualityGatesPath)) {
|
|
113
|
+
try {
|
|
114
|
+
// Run quality gates in CI mode (checks all files, not just staged)
|
|
115
|
+
const result = execSync(`node "${qualityGatesPath}" --context=ci --json 2>&1`, {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
timeout: 60000, // 60 second timeout
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Try to parse JSON output
|
|
122
|
+
try {
|
|
123
|
+
const jsonMatch = result.match(/\{[\s\S]*\}$/);
|
|
124
|
+
if (jsonMatch) {
|
|
125
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
126
|
+
if (parsed.violations) {
|
|
127
|
+
violations.push(...parsed.violations);
|
|
128
|
+
}
|
|
129
|
+
if (parsed.warnings) {
|
|
130
|
+
warnings.push(...parsed.warnings);
|
|
131
|
+
}
|
|
132
|
+
gates.push(...(parsed.gates || []));
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// JSON parsing failed, check for error indicators in output
|
|
136
|
+
if (result.includes('❌') || result.includes('FAIL')) {
|
|
137
|
+
violations.push({ message: 'Quality gates reported failures', output: result });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (execError) {
|
|
141
|
+
// Command failed - check exit code and output
|
|
142
|
+
if (execError.status !== 0) {
|
|
143
|
+
const output = execError.stdout || execError.message;
|
|
144
|
+
if (output.includes('violations') || output.includes('❌')) {
|
|
145
|
+
violations.push({ message: 'Quality gates failed', output: output.substring(0, 500) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Also check for active waivers that might cover violations
|
|
152
|
+
const waiversPath = path.join(process.cwd(), '.caws', 'waivers', 'active-waivers.yaml');
|
|
153
|
+
let hasActiveWaivers = false;
|
|
154
|
+
if (await fs.pathExists(waiversPath)) {
|
|
155
|
+
const waiversContent = await fs.readFile(waiversPath, 'utf8');
|
|
156
|
+
const waivers = yaml.load(waiversContent);
|
|
157
|
+
if (waivers && waivers.waivers) {
|
|
158
|
+
const activeWaiverCount = Object.keys(waivers.waivers).length;
|
|
159
|
+
if (activeWaiverCount > 0) {
|
|
160
|
+
hasActiveWaivers = true;
|
|
161
|
+
gates.push(`${activeWaiverCount} active waiver(s)`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Determine overall validity
|
|
167
|
+
const hasBlockingViolations = violations.some(
|
|
168
|
+
(v) => v.severity === 'block' || v.severity === 'fail'
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (violations.length === 0) {
|
|
172
|
+
return {
|
|
173
|
+
valid: true,
|
|
174
|
+
message: 'All quality gates passed',
|
|
175
|
+
gates: gates.length > 0 ? gates : ['naming', 'duplication', 'god-objects', 'hidden-todo'],
|
|
176
|
+
violations: [],
|
|
177
|
+
warnings,
|
|
178
|
+
};
|
|
179
|
+
} else if (hasActiveWaivers && !hasBlockingViolations) {
|
|
180
|
+
return {
|
|
181
|
+
valid: true,
|
|
182
|
+
message: `Quality gates passed with ${violations.length} waived violation(s)`,
|
|
183
|
+
gates,
|
|
184
|
+
violations,
|
|
185
|
+
warnings,
|
|
186
|
+
waived: true,
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
return {
|
|
190
|
+
valid: false,
|
|
191
|
+
message: `${violations.length} quality gate violation(s) found`,
|
|
192
|
+
gates,
|
|
193
|
+
violations,
|
|
194
|
+
warnings,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// If quality gates can't be run, warn but don't block
|
|
199
|
+
return {
|
|
200
|
+
valid: true,
|
|
201
|
+
message: `Quality gates check skipped: ${error.message}`,
|
|
202
|
+
gates: [],
|
|
203
|
+
violations: [],
|
|
204
|
+
warnings: [{ message: `Could not run quality gates: ${error.message}` }],
|
|
205
|
+
skipped: true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
100
208
|
}
|
|
101
209
|
|
|
102
210
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"burnup.d.ts","sourceRoot":"","sources":["../../src/commands/burnup.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"burnup.d.ts","sourceRoot":"","sources":["../../src/commands/burnup.js"],"names":[],"mappings":"AAsFA;;;GAGG;AACH,wCAFW,MAAM,iBAgGhB"}
|
package/dist/commands/burnup.js
CHANGED
|
@@ -8,9 +8,82 @@ const fs = require('fs-extra');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const yaml = require('js-yaml');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
11
12
|
|
|
12
13
|
const { deriveBudget, generateBurnupReport } = require('../budget-derivation');
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Get actual git change statistics from the repository
|
|
17
|
+
* Analyzes changes since the last tag or initial commit
|
|
18
|
+
* @param {string} specDir - Directory containing the spec file
|
|
19
|
+
* @returns {Object} Stats with files_changed, lines_added, lines_removed, lines_changed
|
|
20
|
+
*/
|
|
21
|
+
function getGitChangeStats(specDir) {
|
|
22
|
+
try {
|
|
23
|
+
const cwd = specDir || process.cwd();
|
|
24
|
+
|
|
25
|
+
// Find the base reference - prefer last tag, fall back to first commit
|
|
26
|
+
let baseRef;
|
|
27
|
+
try {
|
|
28
|
+
baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
|
|
29
|
+
cwd,
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
}).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
// No tags, use first commit
|
|
34
|
+
try {
|
|
35
|
+
baseRef = execSync('git rev-list --max-parents=0 HEAD', {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
}).trim();
|
|
39
|
+
} catch {
|
|
40
|
+
// Not a git repo or no commits
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get file change count
|
|
46
|
+
const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
|
|
47
|
+
cwd,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
});
|
|
50
|
+
const filesChanged = filesOutput.trim().split('\n').filter(Boolean).length;
|
|
51
|
+
|
|
52
|
+
// Get line statistics using --numstat
|
|
53
|
+
const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
|
|
54
|
+
cwd,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let linesAdded = 0;
|
|
59
|
+
let linesRemoved = 0;
|
|
60
|
+
|
|
61
|
+
numstatOutput
|
|
62
|
+
.trim()
|
|
63
|
+
.split('\n')
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.forEach((line) => {
|
|
66
|
+
const [added, removed] = line.split('\t');
|
|
67
|
+
// Skip binary files (shown as '-')
|
|
68
|
+
if (added !== '-' && removed !== '-') {
|
|
69
|
+
linesAdded += parseInt(added, 10) || 0;
|
|
70
|
+
linesRemoved += parseInt(removed, 10) || 0;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
files_changed: filesChanged,
|
|
76
|
+
lines_added: linesAdded,
|
|
77
|
+
lines_removed: linesRemoved,
|
|
78
|
+
lines_changed: linesAdded + linesRemoved,
|
|
79
|
+
base_ref: baseRef,
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Return null if git analysis fails
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
14
87
|
/**
|
|
15
88
|
* Burn-up command handler
|
|
16
89
|
* @param {string} specFile - Path to spec file
|
|
@@ -32,15 +105,34 @@ async function burnupCommand(specFile) {
|
|
|
32
105
|
// Derive budget
|
|
33
106
|
const derivedBudget = deriveBudget(spec, path.dirname(specPath));
|
|
34
107
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
108
|
+
// Get actual git change statistics
|
|
109
|
+
const gitStats = getGitChangeStats(path.dirname(specPath));
|
|
110
|
+
|
|
111
|
+
let currentStats;
|
|
112
|
+
if (gitStats) {
|
|
113
|
+
currentStats = {
|
|
114
|
+
files_changed: gitStats.files_changed,
|
|
115
|
+
lines_changed: gitStats.lines_changed,
|
|
116
|
+
lines_added: gitStats.lines_added,
|
|
117
|
+
lines_removed: gitStats.lines_removed,
|
|
118
|
+
risk_tier: spec.risk_tier,
|
|
119
|
+
base_ref: gitStats.base_ref,
|
|
120
|
+
};
|
|
121
|
+
console.log(chalk.gray(` Analyzing changes since: ${gitStats.base_ref}`));
|
|
122
|
+
} else {
|
|
123
|
+
// Fallback if git analysis fails (not in a repo or no commits)
|
|
124
|
+
console.log(chalk.yellow(' ⚠️ Could not analyze git history, using zero values'));
|
|
125
|
+
currentStats = {
|
|
126
|
+
files_changed: 0,
|
|
127
|
+
lines_changed: 0,
|
|
128
|
+
lines_added: 0,
|
|
129
|
+
lines_removed: 0,
|
|
130
|
+
risk_tier: spec.risk_tier,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
41
133
|
|
|
42
134
|
// Generate report
|
|
43
|
-
const report = generateBurnupReport(derivedBudget,
|
|
135
|
+
const report = generateBurnupReport(derivedBudget, currentStats);
|
|
44
136
|
|
|
45
137
|
console.log(report);
|
|
46
138
|
|
|
@@ -63,15 +155,22 @@ async function burnupCommand(specFile) {
|
|
|
63
155
|
|
|
64
156
|
console.log(
|
|
65
157
|
chalk.gray(
|
|
66
|
-
` Current Usage: ${
|
|
158
|
+
` Current Usage: ${currentStats.files_changed} files, ${currentStats.lines_changed} LOC`
|
|
67
159
|
)
|
|
68
160
|
);
|
|
161
|
+
if (currentStats.lines_added !== undefined) {
|
|
162
|
+
console.log(
|
|
163
|
+
chalk.gray(
|
|
164
|
+
` Breakdown: +${currentStats.lines_added} added, -${currentStats.lines_removed} removed`
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
69
168
|
|
|
70
169
|
const filePercent = Math.round(
|
|
71
|
-
(
|
|
170
|
+
(currentStats.files_changed / derivedBudget.effective.max_files) * 100
|
|
72
171
|
);
|
|
73
172
|
const locPercent = Math.round(
|
|
74
|
-
(
|
|
173
|
+
(currentStats.lines_changed / derivedBudget.effective.max_loc) * 100
|
|
75
174
|
);
|
|
76
175
|
|
|
77
176
|
if (filePercent > 90 || locPercent > 90) {
|
|
@@ -11,7 +11,7 @@ const chalk = require('chalk');
|
|
|
11
11
|
|
|
12
12
|
// Import utilities
|
|
13
13
|
const { checkTypeScriptTestConfig } = require('../utils/typescript-detector');
|
|
14
|
-
const { configureJestForTypeScript } = require('../generators/jest-config');
|
|
14
|
+
const { configureJestForTypeScript } = require('../generators/jest-config-generator');
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Health check: Working spec validity
|
package/dist/commands/mode.js
CHANGED
|
@@ -18,13 +18,32 @@ const {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Display current mode status
|
|
21
|
+
* @param {string} currentMode - The current mode to display
|
|
21
22
|
*/
|
|
22
|
-
function displayCurrentMode() {
|
|
23
|
+
function displayCurrentMode(currentMode) {
|
|
24
|
+
const tier = getTier(currentMode);
|
|
25
|
+
|
|
23
26
|
console.log(chalk.bold.cyan('\n🔧 CAWS Current Mode'));
|
|
24
27
|
console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
console.log(chalk.
|
|
29
|
+
console.log(chalk.bold(`Active Mode: ${tier.icon} ${tier.color(currentMode)}`));
|
|
30
|
+
console.log(chalk.gray(`Description: ${tier.description}`));
|
|
31
|
+
console.log('');
|
|
32
|
+
|
|
33
|
+
// Quality requirements
|
|
34
|
+
console.log(chalk.bold('Quality Requirements:'));
|
|
35
|
+
console.log(chalk.gray(` Test Coverage: ≥${tier.qualityRequirements.testCoverage}%`));
|
|
36
|
+
console.log(chalk.gray(` Mutation Score: ≥${tier.qualityRequirements.mutationScore}%`));
|
|
37
|
+
console.log(chalk.gray(` Contracts: ${tier.qualityRequirements.contracts}`));
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
// Risk tiers supported
|
|
41
|
+
console.log(chalk.bold('Supported Risk Tiers:'));
|
|
42
|
+
tier.riskTiers.forEach((riskTier) => {
|
|
43
|
+
const riskColor =
|
|
44
|
+
riskTier === 'T1' ? chalk.red : riskTier === 'T2' ? chalk.yellow : chalk.green;
|
|
45
|
+
console.log(chalk.gray(` ${riskColor(riskTier)}`));
|
|
46
|
+
});
|
|
28
47
|
console.log('');
|
|
29
48
|
}
|
|
30
49
|
|
|
@@ -128,21 +147,12 @@ async function modeCommand(action, options = {}) {
|
|
|
128
147
|
switch (action) {
|
|
129
148
|
case 'current': {
|
|
130
149
|
const currentMode = await getCurrentMode();
|
|
131
|
-
displayCurrentMode();
|
|
132
|
-
|
|
133
|
-
const tier = getTier(currentMode);
|
|
134
|
-
console.log(chalk.bold(`Current Mode: ${tier.icon} ${tier.color(currentMode)}`));
|
|
135
|
-
console.log(chalk.gray(`Description: ${tier.description}`));
|
|
136
|
-
console.log(
|
|
137
|
-
chalk.gray(
|
|
138
|
-
`Quality: ${tier.qualityRequirements.testCoverage}% coverage, ${tier.qualityRequirements.mutationScore}% mutation`
|
|
139
|
-
)
|
|
140
|
-
);
|
|
150
|
+
displayCurrentMode(currentMode);
|
|
141
151
|
|
|
142
152
|
return outputResult({
|
|
143
153
|
command: 'mode current',
|
|
144
154
|
mode: currentMode,
|
|
145
|
-
tier:
|
|
155
|
+
tier: getTier(currentMode),
|
|
146
156
|
});
|
|
147
157
|
}
|
|
148
158
|
|