@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.
@@ -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): any;
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":"AAUA;;;;;GAKG;AACH,sDAHW,MAAM,OAuDhB;AAED;;;;;GAKG;AACH,qCAJW,MAAM,eACN,MAAM,GACJ,MAAO,IAAI,CAgBvB;AAED;;;;GAIG;AACH,4CAFa,OAAO,CA4BnB;AAED;;;;;GAKG;AACH,kFA8BC;AAED;;;;;GAKG;AACH,6EAFa,MAAM,CA4BlB"}
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.yaml
20
- const policyPath = path.join(projectRoot, '.caws', 'policy.yaml');
21
- if (!fs.existsSync(policyPath)) {
22
- throw new Error('Policy file not found: .caws/policy.yaml');
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 = yaml.load(fs.readFileSync(policyPath, 'utf8'));
175
+ const policy = policyResult;
26
176
 
27
- // Validate policy structure
28
- if (!policy.risk_tiers || !policy.risk_tiers[spec.risk_tier]) {
29
- throw new Error(`Risk tier ${spec.risk_tier} not defined in policy.yaml`);
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.warn(`Waiver file not found: ${waiverPath}`);
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.warn(`Failed to load waiver ${waiverId}: ${error.message}`);
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 status
110
- if (waiver.status !== 'active') {
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
- // Check if it has required approvals (simplified check)
115
- if (!waiver.approvers || waiver.approvers.length === 0) {
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(`Effective Budget: ${derivedBudget.effective.max_files} files, ${derivedBudget.effective.max_loc} LOC`);
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((currentStats.files_changed / derivedBudget.effective.max_files) * 100);
186
- const locPercent = Math.round((currentStats.lines_changed / derivedBudget.effective.max_loc) * 100);
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(`File Usage: ${filePercent}% (${currentStats.files_changed}/${derivedBudget.effective.max_files})`);
189
- report.push(`LOC Usage: ${locPercent}% (${currentStats.lines_changed}/${derivedBudget.effective.max_loc})`);
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
- if (filePercent > 90 || locPercent > 90) {
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,5 +1,6 @@
1
1
  /**
2
2
  * Validate command handler
3
+ * Enhanced with JSON output format support
3
4
  * @param {string} specFile - Path to spec file
4
5
  * @param {Object} options - Command options
5
6
  */
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.js"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,0CAHW,MAAM,+BA2DhB"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.js"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,0CAHW,MAAM,+BAoIhB"}