@paths.design/caws-cli 3.5.0 → 4.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/budget-derivation.d.ts +41 -2
- package/dist/budget-derivation.d.ts.map +1 -1
- package/dist/budget-derivation.js +417 -30
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +105 -28
- package/dist/index.js +2 -0
- package/dist/policy/PolicyManager.d.ts +104 -0
- package/dist/policy/PolicyManager.d.ts.map +1 -0
- package/dist/policy/PolicyManager.js +399 -0
- package/dist/scaffold/cursor-hooks.d.ts.map +1 -1
- package/dist/scaffold/cursor-hooks.js +15 -0
- package/dist/spec/SpecFileManager.d.ts +146 -0
- package/dist/spec/SpecFileManager.d.ts.map +1 -0
- package/dist/spec/SpecFileManager.js +419 -0
- package/dist/validation/spec-validation.d.ts +14 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +225 -13
- package/package.json +1 -1
- package/templates/.cursor/rules/01-claims-verification.mdc +144 -0
- package/templates/.cursor/rules/02-testing-standards.mdc +315 -0
- package/templates/.cursor/rules/03-infrastructure-standards.mdc +251 -0
- package/templates/.cursor/rules/04-documentation-integrity.mdc +291 -0
- package/templates/.cursor/rules/05-production-readiness-checklist.mdc +214 -0
- package/templates/.cursor/rules/README.md +64 -0
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Derive budget for a working spec based on policy and waivers
|
|
3
|
+
* Enhanced to use PolicyManager for caching
|
|
3
4
|
* @param {Object} spec - Working spec object
|
|
4
5
|
* @param {string} projectRoot - Project root directory
|
|
6
|
+
* @param {Object} options - Derivation options
|
|
7
|
+
* @param {boolean} options.useCache - Use cached policy (default: true)
|
|
5
8
|
* @returns {Object} Derived budget with baseline and effective limits
|
|
6
9
|
*/
|
|
7
|
-
export function deriveBudget(spec: any, projectRoot?: string
|
|
10
|
+
export function deriveBudget(spec: any, projectRoot?: string, options?: {
|
|
11
|
+
useCache: boolean;
|
|
12
|
+
}): any;
|
|
8
13
|
/**
|
|
9
14
|
* Load a waiver by ID
|
|
15
|
+
* Enhanced with structure validation and detailed error reporting
|
|
10
16
|
* @param {string} waiverId - Waiver ID (e.g., WV-0001)
|
|
11
17
|
* @param {string} projectRoot - Project root directory
|
|
12
18
|
* @returns {Object|null} Waiver object or null if not found
|
|
@@ -14,10 +20,12 @@ export function deriveBudget(spec: any, projectRoot?: string): any;
|
|
|
14
20
|
export function loadWaiver(waiverId: string, projectRoot: string): any | null;
|
|
15
21
|
/**
|
|
16
22
|
* Check if a waiver is currently valid
|
|
23
|
+
* Enhanced with proper expiry and approval validation
|
|
17
24
|
* @param {Object} waiver - Waiver object
|
|
25
|
+
* @param {Object} policy - Policy configuration (optional)
|
|
18
26
|
* @returns {boolean} Whether waiver is valid and active
|
|
19
27
|
*/
|
|
20
|
-
export function isWaiverValid(waiver: any): boolean;
|
|
28
|
+
export function isWaiverValid(waiver: any, policy?: any): boolean;
|
|
21
29
|
/**
|
|
22
30
|
* Check if current changes exceed derived budget
|
|
23
31
|
* @param {Object} derivedBudget - Budget from deriveBudget()
|
|
@@ -27,9 +35,40 @@ export function isWaiverValid(waiver: any): boolean;
|
|
|
27
35
|
export function checkBudgetCompliance(derivedBudget: any, currentStats: any): any;
|
|
28
36
|
/**
|
|
29
37
|
* Generate burn-up report for scope visibility
|
|
38
|
+
* Enhanced with utilization metrics and warnings
|
|
30
39
|
* @param {Object} derivedBudget - Budget from deriveBudget()
|
|
31
40
|
* @param {Object} currentStats - Current change statistics
|
|
32
41
|
* @returns {string} Human-readable burn-up report
|
|
33
42
|
*/
|
|
34
43
|
export function generateBurnupReport(derivedBudget: any, currentStats: any): string;
|
|
44
|
+
/**
|
|
45
|
+
* Calculate budget utilization percentages
|
|
46
|
+
* @param {Object} budgetCompliance - Budget compliance result
|
|
47
|
+
* @returns {Object} Utilization percentages
|
|
48
|
+
*/
|
|
49
|
+
export function calculateBudgetUtilization(budgetCompliance: any): any;
|
|
50
|
+
/**
|
|
51
|
+
* Check if changes are approaching budget limit
|
|
52
|
+
* @param {Object} budgetCompliance - Budget compliance result
|
|
53
|
+
* @param {number} threshold - Warning threshold (default 80)
|
|
54
|
+
* @returns {boolean} Whether approaching limit
|
|
55
|
+
*/
|
|
56
|
+
export function isApproachingBudgetLimit(budgetCompliance: any, threshold?: number): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Validate policy structure and content
|
|
59
|
+
* @param {Object} policy - Policy object from policy.yaml
|
|
60
|
+
* @throws {Error} If policy is invalid
|
|
61
|
+
*/
|
|
62
|
+
export function validatePolicy(policy: any): void;
|
|
63
|
+
/**
|
|
64
|
+
* Get default policy as fallback
|
|
65
|
+
* @returns {Object} Default CAWS policy
|
|
66
|
+
*/
|
|
67
|
+
export function getDefaultPolicy(): any;
|
|
68
|
+
/**
|
|
69
|
+
* Validate waiver document structure
|
|
70
|
+
* @param {Object} waiver - Waiver document to validate
|
|
71
|
+
* @throws {Error} If waiver structure is invalid
|
|
72
|
+
*/
|
|
73
|
+
export function validateWaiverStructure(waiver: any): void;
|
|
35
74
|
//# sourceMappingURL=budget-derivation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budget-derivation.d.ts","sourceRoot":"","sources":["../src/budget-derivation.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"budget-derivation.d.ts","sourceRoot":"","sources":["../src/budget-derivation.js"],"names":[],"mappings":"AA8JA;;;;;;;;GAQG;AACH,sDALW,MAAM,YAEd;IAAyB,QAAQ,EAAzB,OAAO;CACf,OA+FF;AA4FD;;;;;;GAMG;AACH,qCAJW,MAAM,eACN,MAAM,GACJ,MAAO,IAAI,CAuCvB;AAED;;;;;;GAMG;AACH,0DAFa,OAAO,CAgDnB;AAED;;;;;GAKG;AACH,kFA8BC;AAqCD;;;;;;GAMG;AACH,6EAFa,MAAM,CA8ClB;AAtFD;;;;GAIG;AACH,uEAiBC;AAED;;;;;GAKG;AACH,4EAHW,MAAM,GACJ,OAAO,CAKnB;AAhgBD;;;;GAIG;AACH,kDA+FC;AAED;;;GAGG;AACH,wCAsCC;AA0GD;;;;GAIG;AACH,2DAmFC"}
|
|
@@ -1,38 +1,221 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Budget Derivation Logic
|
|
3
3
|
* Derives budgets from policy.yaml and applies waivers
|
|
4
|
+
* Enhanced with PolicyManager for caching and improved performance
|
|
4
5
|
* @author @darianrosebrook
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const fs = require('fs-extra');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const yaml = require('js-yaml');
|
|
11
|
+
const { defaultPolicyManager } = require('./policy/PolicyManager');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Validate policy structure and content
|
|
15
|
+
* @param {Object} policy - Policy object from policy.yaml
|
|
16
|
+
* @throws {Error} If policy is invalid
|
|
17
|
+
*/
|
|
18
|
+
function validatePolicy(policy) {
|
|
19
|
+
// Validate version
|
|
20
|
+
if (!policy.version) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'Policy missing version field\n' +
|
|
23
|
+
'Add "version: 1" to .caws/policy.yaml\n' +
|
|
24
|
+
'Run "caws init" to regenerate policy.yaml'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Validate risk_tiers exists
|
|
29
|
+
if (!policy.risk_tiers) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'Policy missing risk_tiers configuration\n' +
|
|
32
|
+
'Policy must define risk tiers 1, 2, and 3\n' +
|
|
33
|
+
'Run "caws init" to regenerate policy.yaml'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Validate each required tier
|
|
38
|
+
for (const tier of [1, 2, 3]) {
|
|
39
|
+
if (!policy.risk_tiers[tier]) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Policy missing configuration for risk tier ${tier}\n` +
|
|
42
|
+
`Add risk_tiers.${tier} with max_files and max_loc to .caws/policy.yaml\n` +
|
|
43
|
+
'Run "caws init" to regenerate policy.yaml'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tierConfig = policy.risk_tiers[tier];
|
|
48
|
+
|
|
49
|
+
// Validate max_files
|
|
50
|
+
if (!tierConfig.max_files || tierConfig.max_files <= 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Invalid max_files for tier ${tier}: ${tierConfig.max_files}\n` +
|
|
53
|
+
`max_files must be a positive integer\n` +
|
|
54
|
+
`Fix in .caws/policy.yaml under risk_tiers.${tier}.max_files`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate max_loc
|
|
59
|
+
if (!tierConfig.max_loc || tierConfig.max_loc <= 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid max_loc for tier ${tier}: ${tierConfig.max_loc}\n` +
|
|
62
|
+
`max_loc must be a positive integer\n` +
|
|
63
|
+
`Fix in .caws/policy.yaml under risk_tiers.${tier}.max_loc`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate thresholds if present
|
|
68
|
+
if (
|
|
69
|
+
tierConfig.coverage_threshold !== undefined &&
|
|
70
|
+
(tierConfig.coverage_threshold < 0 || tierConfig.coverage_threshold > 100)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Invalid coverage_threshold for tier ${tier}: ${tierConfig.coverage_threshold}\n` +
|
|
74
|
+
`coverage_threshold must be between 0 and 100\n` +
|
|
75
|
+
`Fix in .caws/policy.yaml under risk_tiers.${tier}.coverage_threshold`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
tierConfig.mutation_threshold !== undefined &&
|
|
81
|
+
(tierConfig.mutation_threshold < 0 || tierConfig.mutation_threshold > 100)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid mutation_threshold for tier ${tier}: ${tierConfig.mutation_threshold}\n` +
|
|
85
|
+
`mutation_threshold must be between 0 and 100\n` +
|
|
86
|
+
`Fix in .caws/policy.yaml under risk_tiers.${tier}.mutation_threshold`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Validate waiver_approval if present
|
|
92
|
+
if (policy.waiver_approval) {
|
|
93
|
+
if (
|
|
94
|
+
policy.waiver_approval.required_approvers !== undefined &&
|
|
95
|
+
policy.waiver_approval.required_approvers < 0
|
|
96
|
+
) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Invalid waiver_approval.required_approvers: ${policy.waiver_approval.required_approvers}\n` +
|
|
99
|
+
'required_approvers must be a non-negative integer'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
policy.waiver_approval.max_duration_days !== undefined &&
|
|
105
|
+
policy.waiver_approval.max_duration_days <= 0
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid waiver_approval.max_duration_days: ${policy.waiver_approval.max_duration_days}\n` +
|
|
109
|
+
'max_duration_days must be a positive integer'
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get default policy as fallback
|
|
117
|
+
* @returns {Object} Default CAWS policy
|
|
118
|
+
*/
|
|
119
|
+
function getDefaultPolicy() {
|
|
120
|
+
return {
|
|
121
|
+
version: 1,
|
|
122
|
+
risk_tiers: {
|
|
123
|
+
1: {
|
|
124
|
+
max_files: 25,
|
|
125
|
+
max_loc: 1000,
|
|
126
|
+
coverage_threshold: 90,
|
|
127
|
+
mutation_threshold: 70,
|
|
128
|
+
contracts_required: true,
|
|
129
|
+
manual_review_required: true,
|
|
130
|
+
description: 'Critical changes requiring manual review',
|
|
131
|
+
},
|
|
132
|
+
2: {
|
|
133
|
+
max_files: 50,
|
|
134
|
+
max_loc: 2000,
|
|
135
|
+
coverage_threshold: 80,
|
|
136
|
+
mutation_threshold: 50,
|
|
137
|
+
contracts_required: true,
|
|
138
|
+
manual_review_required: false,
|
|
139
|
+
description: 'Standard features with automated gates',
|
|
140
|
+
},
|
|
141
|
+
3: {
|
|
142
|
+
max_files: 100,
|
|
143
|
+
max_loc: 5000,
|
|
144
|
+
coverage_threshold: 70,
|
|
145
|
+
mutation_threshold: 30,
|
|
146
|
+
contracts_required: false,
|
|
147
|
+
manual_review_required: false,
|
|
148
|
+
description: 'Low-risk changes with minimal oversight',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
waiver_approval: {
|
|
152
|
+
required_approvers: 1,
|
|
153
|
+
max_duration_days: 90,
|
|
154
|
+
auto_revoke_expired: true,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
10
158
|
|
|
11
159
|
/**
|
|
12
160
|
* Derive budget for a working spec based on policy and waivers
|
|
161
|
+
* Enhanced to use PolicyManager for caching
|
|
13
162
|
* @param {Object} spec - Working spec object
|
|
14
163
|
* @param {string} projectRoot - Project root directory
|
|
164
|
+
* @param {Object} options - Derivation options
|
|
165
|
+
* @param {boolean} options.useCache - Use cached policy (default: true)
|
|
15
166
|
* @returns {Object} Derived budget with baseline and effective limits
|
|
16
167
|
*/
|
|
17
|
-
function deriveBudget(spec, projectRoot = process.cwd()) {
|
|
168
|
+
async function deriveBudget(spec, projectRoot = process.cwd(), options = {}) {
|
|
18
169
|
try {
|
|
19
|
-
// Load policy
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
170
|
+
// Load policy using PolicyManager (with caching)
|
|
171
|
+
const policyResult = await defaultPolicyManager.loadPolicy(projectRoot, {
|
|
172
|
+
useCache: options.useCache !== false,
|
|
173
|
+
});
|
|
24
174
|
|
|
25
|
-
const policy =
|
|
175
|
+
const policy = policyResult;
|
|
26
176
|
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
29
|
-
|
|
177
|
+
// Check if using default policy
|
|
178
|
+
if (policy._isDefault) {
|
|
179
|
+
const expectedPath = path.join(projectRoot, '.caws', 'policy.yaml');
|
|
180
|
+
const policyExists = fs.existsSync(expectedPath);
|
|
181
|
+
|
|
182
|
+
if (policyExists) {
|
|
183
|
+
console.error(
|
|
184
|
+
'⚠️ Policy file exists but not loaded: ' +
|
|
185
|
+
expectedPath +
|
|
186
|
+
'\n' +
|
|
187
|
+
' Current working directory: ' +
|
|
188
|
+
process.cwd() +
|
|
189
|
+
'\n' +
|
|
190
|
+
' Project root: ' +
|
|
191
|
+
projectRoot +
|
|
192
|
+
'\n' +
|
|
193
|
+
' Cache status: ' +
|
|
194
|
+
(policy._cacheHit ? 'HIT (may be stale)' : 'MISS') +
|
|
195
|
+
'\n' +
|
|
196
|
+
' This may be a path resolution or caching issue\n'
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
console.warn(
|
|
200
|
+
'⚠️ Policy file not found: .caws/policy.yaml\n' +
|
|
201
|
+
' Using default policy. Run "caws init" to create policy.yaml'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if risk tier exists in policy
|
|
207
|
+
if (!policy.risk_tiers[spec.risk_tier]) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Risk tier ${spec.risk_tier} not defined in policy.yaml\n` +
|
|
210
|
+
`Policy only defines tiers: ${Object.keys(policy.risk_tiers).join(', ')}\n` +
|
|
211
|
+
`Valid tiers are: 1 (critical), 2 (standard), 3 (low-risk)`
|
|
212
|
+
);
|
|
30
213
|
}
|
|
31
214
|
|
|
32
215
|
const tierBudget = policy.risk_tiers[spec.risk_tier];
|
|
33
216
|
const baseline = {
|
|
34
217
|
max_files: tierBudget.max_files,
|
|
35
|
-
max_loc: tierBudget.max_loc
|
|
218
|
+
max_loc: tierBudget.max_loc,
|
|
36
219
|
};
|
|
37
220
|
|
|
38
221
|
// Start with baseline budget
|
|
@@ -43,6 +226,16 @@ function deriveBudget(spec, projectRoot = process.cwd()) {
|
|
|
43
226
|
for (const waiverId of spec.waiver_ids) {
|
|
44
227
|
const waiver = loadWaiver(waiverId, projectRoot);
|
|
45
228
|
if (waiver && waiver.status === 'active' && isWaiverValid(waiver)) {
|
|
229
|
+
// Validate waiver covers budget_limit gate
|
|
230
|
+
if (!waiver.gates || !waiver.gates.includes('budget_limit')) {
|
|
231
|
+
console.warn(
|
|
232
|
+
`\n⚠️ Waiver ${waiverId} does not cover 'budget_limit' gate\n` +
|
|
233
|
+
` Current gates: [${waiver.gates ? waiver.gates.join(', ') : 'none'}]\n` +
|
|
234
|
+
` Add 'budget_limit' to gates array to apply to budget violations\n`
|
|
235
|
+
);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
46
239
|
// Apply additive deltas
|
|
47
240
|
if (waiver.delta) {
|
|
48
241
|
if (waiver.delta.max_files) {
|
|
@@ -60,59 +253,194 @@ function deriveBudget(spec, projectRoot = process.cwd()) {
|
|
|
60
253
|
baseline,
|
|
61
254
|
effective: effectiveBudget,
|
|
62
255
|
waivers_applied: spec.waiver_ids || [],
|
|
63
|
-
derived_at: new Date().toISOString()
|
|
256
|
+
derived_at: new Date().toISOString(),
|
|
64
257
|
};
|
|
65
|
-
|
|
66
258
|
} catch (error) {
|
|
67
259
|
throw new Error(`Budget derivation failed: ${error.message}`);
|
|
68
260
|
}
|
|
69
261
|
}
|
|
70
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Validate waiver document structure
|
|
265
|
+
* @param {Object} waiver - Waiver document to validate
|
|
266
|
+
* @throws {Error} If waiver structure is invalid
|
|
267
|
+
*/
|
|
268
|
+
function validateWaiverStructure(waiver) {
|
|
269
|
+
const requiredFields = ['id', 'title', 'reason', 'status', 'gates', 'expires_at', 'approvers'];
|
|
270
|
+
|
|
271
|
+
// Check all required fields present
|
|
272
|
+
for (const field of requiredFields) {
|
|
273
|
+
if (!(field in waiver)) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Waiver missing required field: ${field}\n` +
|
|
276
|
+
`Required fields: ${requiredFields.join(', ')}\n` +
|
|
277
|
+
`Fix the waiver file at .caws/waivers/${waiver.id || 'unknown'}.yaml`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Validate ID format (WV-XXXX)
|
|
283
|
+
if (!/^WV-\d{4}$/.test(waiver.id)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Invalid waiver ID format: ${waiver.id}\n` +
|
|
286
|
+
'Waiver IDs must follow the format: WV-XXXX (e.g., WV-0001)\n' +
|
|
287
|
+
'Where XXXX is a 4-digit number\n' +
|
|
288
|
+
`Fix the id field in .caws/waivers/${waiver.id}.yaml`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Validate status
|
|
293
|
+
const validStatuses = ['active', 'expired', 'revoked'];
|
|
294
|
+
if (!validStatuses.includes(waiver.status)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Invalid waiver status: ${waiver.status}\n` +
|
|
297
|
+
`Status must be one of: ${validStatuses.join(', ')}\n` +
|
|
298
|
+
`Fix the status field in .caws/waivers/${waiver.id}.yaml`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate gates is array
|
|
303
|
+
if (!Array.isArray(waiver.gates) || waiver.gates.length === 0) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`Invalid waiver gates: ${JSON.stringify(waiver.gates)}\n` +
|
|
306
|
+
'gates must be a non-empty array of gate names\n' +
|
|
307
|
+
`Example: gates: ["budget_limit", "coverage_threshold"]\n` +
|
|
308
|
+
`Fix the gates field in .caws/waivers/${waiver.id}.yaml`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate approvers is array
|
|
313
|
+
if (!Array.isArray(waiver.approvers) || waiver.approvers.length === 0) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Invalid waiver approvers: ${JSON.stringify(waiver.approvers)}\n` +
|
|
316
|
+
'approvers must be a non-empty array of approver names/emails\n' +
|
|
317
|
+
'Example: approvers: ["tech-lead@company.com"]\n' +
|
|
318
|
+
`Fix the approvers field in .caws/waivers/${waiver.id}.yaml`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Validate expires_at is valid date string
|
|
323
|
+
const expiryDate = new Date(waiver.expires_at);
|
|
324
|
+
if (isNaN(expiryDate.getTime())) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Invalid waiver expiry date: ${waiver.expires_at}\n` +
|
|
327
|
+
'expires_at must be a valid ISO 8601 date string\n' +
|
|
328
|
+
'Example: expires_at: "2025-12-31T23:59:59Z"\n' +
|
|
329
|
+
`Fix the expires_at field in .caws/waivers/${waiver.id}.yaml`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Validate delta if present
|
|
334
|
+
if (waiver.delta) {
|
|
335
|
+
if (waiver.delta.max_files !== undefined && waiver.delta.max_files < 0) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Invalid waiver delta.max_files: ${waiver.delta.max_files}\n` +
|
|
338
|
+
'delta.max_files must be a non-negative integer\n' +
|
|
339
|
+
`Fix the delta field in .caws/waivers/${waiver.id}.yaml`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (waiver.delta.max_loc !== undefined && waiver.delta.max_loc < 0) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`Invalid waiver delta.max_loc: ${waiver.delta.max_loc}\n` +
|
|
346
|
+
'delta.max_loc must be a non-negative integer\n' +
|
|
347
|
+
`Fix the delta field in .caws/waivers/${waiver.id}.yaml`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
71
353
|
/**
|
|
72
354
|
* Load a waiver by ID
|
|
355
|
+
* Enhanced with structure validation and detailed error reporting
|
|
73
356
|
* @param {string} waiverId - Waiver ID (e.g., WV-0001)
|
|
74
357
|
* @param {string} projectRoot - Project root directory
|
|
75
358
|
* @returns {Object|null} Waiver object or null if not found
|
|
76
359
|
*/
|
|
77
360
|
function loadWaiver(waiverId, projectRoot) {
|
|
78
361
|
try {
|
|
362
|
+
// Validate ID format before attempting to load
|
|
363
|
+
if (!/^WV-\d{4}$/.test(waiverId)) {
|
|
364
|
+
console.error(
|
|
365
|
+
`\n❌ Invalid waiver ID format: ${waiverId}\n` +
|
|
366
|
+
` Waiver IDs must be exactly 4 digits: WV-0001 through WV-9999\n` +
|
|
367
|
+
` Fix waiver_ids in .caws/working-spec.yaml\n`
|
|
368
|
+
);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
|
|
79
372
|
const waiverPath = path.join(projectRoot, '.caws', 'waivers', `${waiverId}.yaml`);
|
|
80
373
|
if (!fs.existsSync(waiverPath)) {
|
|
81
|
-
console.
|
|
374
|
+
console.error(
|
|
375
|
+
`\n❌ Waiver file not found: ${waiverId}\n` +
|
|
376
|
+
` Expected location: ${waiverPath}\n` +
|
|
377
|
+
` Create waiver with: caws waiver create\n`
|
|
378
|
+
);
|
|
82
379
|
return null;
|
|
83
380
|
}
|
|
84
381
|
|
|
85
382
|
const waiver = yaml.load(fs.readFileSync(waiverPath, 'utf8'));
|
|
383
|
+
|
|
384
|
+
// Validate waiver structure
|
|
385
|
+
try {
|
|
386
|
+
validateWaiverStructure(waiver);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
console.error(`\n❌ Invalid waiver ${waiverId}: ${error.message}\n`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
86
392
|
return waiver;
|
|
87
393
|
} catch (error) {
|
|
88
|
-
console.
|
|
394
|
+
console.error(`\n❌ Failed to load waiver ${waiverId}: ${error.message}\n`);
|
|
89
395
|
return null;
|
|
90
396
|
}
|
|
91
397
|
}
|
|
92
398
|
|
|
93
399
|
/**
|
|
94
400
|
* Check if a waiver is currently valid
|
|
401
|
+
* Enhanced with proper expiry and approval validation
|
|
95
402
|
* @param {Object} waiver - Waiver object
|
|
403
|
+
* @param {Object} policy - Policy configuration (optional)
|
|
96
404
|
* @returns {boolean} Whether waiver is valid and active
|
|
97
405
|
*/
|
|
98
|
-
function isWaiverValid(waiver) {
|
|
406
|
+
function isWaiverValid(waiver, policy = null) {
|
|
99
407
|
try {
|
|
408
|
+
// Check status first
|
|
409
|
+
if (waiver.status !== 'active') {
|
|
410
|
+
console.warn(`Waiver ${waiver.id} has status: ${waiver.status}`);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
100
414
|
// Check if expired
|
|
101
415
|
if (waiver.expires_at) {
|
|
102
416
|
const expiryDate = new Date(waiver.expires_at);
|
|
103
417
|
const now = new Date();
|
|
104
418
|
if (now > expiryDate) {
|
|
419
|
+
console.warn(`Waiver ${waiver.id} expired on ${waiver.expires_at}`);
|
|
105
420
|
return false;
|
|
106
421
|
}
|
|
107
422
|
}
|
|
108
423
|
|
|
109
|
-
// Check
|
|
110
|
-
if (waiver.
|
|
424
|
+
// Check required approvals
|
|
425
|
+
if (!waiver.approvers || waiver.approvers.length === 0) {
|
|
426
|
+
console.warn(`Waiver ${waiver.id} has no approvers`);
|
|
111
427
|
return false;
|
|
112
428
|
}
|
|
113
429
|
|
|
114
|
-
//
|
|
115
|
-
if (
|
|
430
|
+
// Validate minimum approvers if policy provided
|
|
431
|
+
if (policy && policy.waiver_approval && policy.waiver_approval.required_approvers) {
|
|
432
|
+
const minApprovers = policy.waiver_approval.required_approvers;
|
|
433
|
+
if (waiver.approvers.length < minApprovers) {
|
|
434
|
+
console.warn(
|
|
435
|
+
`Waiver ${waiver.id} has ${waiver.approvers.length} approvers, needs ${minApprovers}`
|
|
436
|
+
);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Check required fields
|
|
442
|
+
if (!waiver.id || !waiver.title || !waiver.gates) {
|
|
443
|
+
console.warn(`Waiver ${waiver.id || 'unknown'} missing required fields`);
|
|
116
444
|
return false;
|
|
117
445
|
}
|
|
118
446
|
|
|
@@ -139,7 +467,7 @@ function checkBudgetCompliance(derivedBudget, currentStats) {
|
|
|
139
467
|
current: currentStats.files_changed,
|
|
140
468
|
limit: derivedBudget.effective.max_files,
|
|
141
469
|
baseline: derivedBudget.baseline.max_files,
|
|
142
|
-
message: `File count (${currentStats.files_changed}) exceeds budget (${derivedBudget.effective.max_files})
|
|
470
|
+
message: `File count (${currentStats.files_changed}) exceeds budget (${derivedBudget.effective.max_files})`,
|
|
143
471
|
});
|
|
144
472
|
}
|
|
145
473
|
|
|
@@ -150,19 +478,55 @@ function checkBudgetCompliance(derivedBudget, currentStats) {
|
|
|
150
478
|
current: currentStats.lines_changed,
|
|
151
479
|
limit: derivedBudget.effective.max_loc,
|
|
152
480
|
baseline: derivedBudget.baseline.max_loc,
|
|
153
|
-
message: `Lines of code (${currentStats.lines_changed}) exceed budget (${derivedBudget.effective.max_loc})
|
|
481
|
+
message: `Lines of code (${currentStats.lines_changed}) exceed budget (${derivedBudget.effective.max_loc})`,
|
|
154
482
|
});
|
|
155
483
|
}
|
|
156
484
|
|
|
157
485
|
return {
|
|
158
486
|
compliant: violations.length === 0,
|
|
159
487
|
violations,
|
|
160
|
-
budget: derivedBudget
|
|
488
|
+
budget: derivedBudget,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Calculate budget utilization percentages
|
|
494
|
+
* @param {Object} budgetCompliance - Budget compliance result
|
|
495
|
+
* @returns {Object} Utilization percentages
|
|
496
|
+
*/
|
|
497
|
+
function calculateBudgetUtilization(budgetCompliance) {
|
|
498
|
+
const filesPercent =
|
|
499
|
+
budgetCompliance.budget.effective.max_files > 0
|
|
500
|
+
? (budgetCompliance.budget.baseline.max_files / budgetCompliance.budget.effective.max_files) *
|
|
501
|
+
100
|
|
502
|
+
: 0;
|
|
503
|
+
|
|
504
|
+
const locPercent =
|
|
505
|
+
budgetCompliance.budget.effective.max_loc > 0
|
|
506
|
+
? (budgetCompliance.budget.baseline.max_loc / budgetCompliance.budget.effective.max_loc) * 100
|
|
507
|
+
: 0;
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
files: Math.round(filesPercent),
|
|
511
|
+
loc: Math.round(locPercent),
|
|
512
|
+
overall: Math.round(Math.max(filesPercent, locPercent)),
|
|
161
513
|
};
|
|
162
514
|
}
|
|
163
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Check if changes are approaching budget limit
|
|
518
|
+
* @param {Object} budgetCompliance - Budget compliance result
|
|
519
|
+
* @param {number} threshold - Warning threshold (default 80)
|
|
520
|
+
* @returns {boolean} Whether approaching limit
|
|
521
|
+
*/
|
|
522
|
+
function isApproachingBudgetLimit(budgetCompliance, threshold = 80) {
|
|
523
|
+
const utilization = calculateBudgetUtilization(budgetCompliance);
|
|
524
|
+
return utilization.overall >= threshold;
|
|
525
|
+
}
|
|
526
|
+
|
|
164
527
|
/**
|
|
165
528
|
* Generate burn-up report for scope visibility
|
|
529
|
+
* Enhanced with utilization metrics and warnings
|
|
166
530
|
* @param {Object} derivedBudget - Budget from deriveBudget()
|
|
167
531
|
* @param {Object} currentStats - Current change statistics
|
|
168
532
|
* @returns {string} Human-readable burn-up report
|
|
@@ -178,18 +542,36 @@ function generateBurnupReport(derivedBudget, currentStats) {
|
|
|
178
542
|
];
|
|
179
543
|
|
|
180
544
|
if (derivedBudget.waivers_applied.length > 0) {
|
|
545
|
+
report.push('');
|
|
181
546
|
report.push(`Waivers Applied: ${derivedBudget.waivers_applied.join(', ')}`);
|
|
182
|
-
report.push(
|
|
547
|
+
report.push(
|
|
548
|
+
`Effective Budget: ${derivedBudget.effective.max_files} files, ${derivedBudget.effective.max_loc} LOC`
|
|
549
|
+
);
|
|
183
550
|
}
|
|
184
551
|
|
|
185
|
-
const filePercent = Math.round(
|
|
186
|
-
|
|
552
|
+
const filePercent = Math.round(
|
|
553
|
+
(currentStats.files_changed / derivedBudget.effective.max_files) * 100
|
|
554
|
+
);
|
|
555
|
+
const locPercent = Math.round(
|
|
556
|
+
(currentStats.lines_changed / derivedBudget.effective.max_loc) * 100
|
|
557
|
+
);
|
|
187
558
|
|
|
188
|
-
report.push(
|
|
189
|
-
report.push(
|
|
559
|
+
report.push('');
|
|
560
|
+
report.push(
|
|
561
|
+
`File Usage: ${filePercent}% (${currentStats.files_changed}/${derivedBudget.effective.max_files})`
|
|
562
|
+
);
|
|
563
|
+
report.push(
|
|
564
|
+
`LOC Usage: ${locPercent}% (${currentStats.lines_changed}/${derivedBudget.effective.max_loc})`
|
|
565
|
+
);
|
|
190
566
|
|
|
191
|
-
|
|
567
|
+
// Add warnings at different thresholds
|
|
568
|
+
const overall = Math.max(filePercent, locPercent);
|
|
569
|
+
if (overall >= 95) {
|
|
570
|
+
report.push('', '🚫 CRITICAL: Budget nearly exhausted!');
|
|
571
|
+
} else if (overall >= 90) {
|
|
192
572
|
report.push('', '⚠️ WARNING: Approaching budget limits');
|
|
573
|
+
} else if (overall >= 80) {
|
|
574
|
+
report.push('', '⚠️ Notice: 80% of budget used');
|
|
193
575
|
}
|
|
194
576
|
|
|
195
577
|
return report.join('\n');
|
|
@@ -200,5 +582,10 @@ module.exports = {
|
|
|
200
582
|
loadWaiver,
|
|
201
583
|
isWaiverValid,
|
|
202
584
|
checkBudgetCompliance,
|
|
203
|
-
generateBurnupReport
|
|
585
|
+
generateBurnupReport,
|
|
586
|
+
calculateBudgetUtilization,
|
|
587
|
+
isApproachingBudgetLimit,
|
|
588
|
+
validatePolicy,
|
|
589
|
+
getDefaultPolicy,
|
|
590
|
+
validateWaiverStructure,
|
|
204
591
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.js"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,0CAHW,MAAM,+BAoIhB"}
|