@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.
Files changed (44) hide show
  1. package/dist/commands/archive.d.ts +2 -1
  2. package/dist/commands/archive.d.ts.map +1 -1
  3. package/dist/commands/archive.js +114 -6
  4. package/dist/commands/burnup.d.ts.map +1 -1
  5. package/dist/commands/burnup.js +109 -10
  6. package/dist/commands/diagnose.js +1 -1
  7. package/dist/commands/mode.js +24 -14
  8. package/dist/commands/provenance.js +216 -93
  9. package/dist/commands/quality-gates.d.ts.map +1 -1
  10. package/dist/commands/quality-gates.js +3 -1
  11. package/dist/commands/specs.js +184 -6
  12. package/dist/commands/status.d.ts.map +1 -1
  13. package/dist/commands/status.js +134 -10
  14. package/dist/commands/templates.js +2 -2
  15. package/dist/error-handler.js +6 -98
  16. package/dist/generators/jest-config-generator.js +242 -0
  17. package/dist/index.js +4 -7
  18. package/dist/minimal-cli.js +3 -1
  19. package/dist/scaffold/claude-hooks.js +316 -0
  20. package/dist/scaffold/index.js +18 -0
  21. package/dist/templates/.claude/README.md +190 -0
  22. package/dist/templates/.claude/hooks/audit.sh +96 -0
  23. package/dist/templates/.claude/hooks/block-dangerous.sh +90 -0
  24. package/dist/templates/.claude/hooks/naming-check.sh +97 -0
  25. package/dist/templates/.claude/hooks/quality-check.sh +68 -0
  26. package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
  27. package/dist/templates/.claude/hooks/scope-guard.sh +105 -0
  28. package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
  29. package/dist/templates/.claude/settings.json +95 -0
  30. package/dist/test-analysis.js +203 -10
  31. package/dist/utils/error-categories.js +210 -0
  32. package/dist/utils/quality-gates-utils.js +402 -0
  33. package/dist/utils/typescript-detector.js +36 -90
  34. package/dist/validation/spec-validation.js +59 -6
  35. package/package.json +5 -3
  36. package/templates/.claude/README.md +190 -0
  37. package/templates/.claude/hooks/audit.sh +96 -0
  38. package/templates/.claude/hooks/block-dangerous.sh +90 -0
  39. package/templates/.claude/hooks/naming-check.sh +97 -0
  40. package/templates/.claude/hooks/quality-check.sh +68 -0
  41. package/templates/.claude/hooks/scan-secrets.sh +85 -0
  42. package/templates/.claude/hooks/scope-guard.sh +105 -0
  43. package/templates/.claude/hooks/validate-spec.sh +76 -0
  44. package/templates/.claude/settings.json +95 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * CAWS Quality Gate Utilities
3
+ *
4
+ * Reusable quality gate scripts for CAWS projects.
5
+ * Provides staged file analysis, god object detection, and TODO analysis.
6
+ *
7
+ * @author @darianrosebrook
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const yaml = require('js-yaml');
13
+ const { execSync } = require('child_process');
14
+ const { getTodoAnalyzerSuggestion } = require('./project-analysis');
15
+
16
+ /**
17
+ * Quality Gate Configuration
18
+ */
19
+ const CONFIG = {
20
+ godObjectThresholds: {
21
+ warning: 1750,
22
+ critical: 2000,
23
+ },
24
+ todoConfidenceThreshold: 0.8,
25
+ supportedExtensions: ['.rs', '.ts', '.tsx', '.js', '.jsx', '.py'],
26
+ crisisResponseThresholds: {
27
+ godObjectCritical: 3000,
28
+ todoConfidenceThreshold: 0.9,
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Check if a waiver applies to the given gate
34
+ * @param {string} gate - Gate name to check
35
+ * @returns {Object} Waiver check result
36
+ */
37
+ function checkWaiver(gate) {
38
+ try {
39
+ const waiversPath = path.join(process.cwd(), '.caws/waivers.yml');
40
+ if (!fs.existsSync(waiversPath)) {
41
+ return { waived: false, reason: 'No waivers file found' };
42
+ }
43
+
44
+ const waiversConfig = yaml.load(fs.readFileSync(waiversPath, 'utf8'));
45
+ const now = new Date();
46
+
47
+ // Find active waivers for this gate
48
+ const activeWaivers =
49
+ waiversConfig.waivers?.filter((waiver) => {
50
+ const expiresAt = new Date(waiver.expires_at);
51
+ return waiver.gates.includes(gate) && expiresAt > now && waiver.status === 'active';
52
+ }) || [];
53
+
54
+ if (activeWaivers.length > 0) {
55
+ const waiver = activeWaivers[0];
56
+ return {
57
+ waived: true,
58
+ waiver,
59
+ reason: `Active waiver: ${waiver.title} (expires: ${waiver.expires_at})`,
60
+ };
61
+ }
62
+
63
+ return { waived: false, reason: 'No active waivers found' };
64
+ } catch (error) {
65
+ return { waived: false, reason: `Waiver check failed: ${error.message}` };
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Detect if project is in crisis response mode
71
+ * @returns {boolean} True if in crisis mode
72
+ */
73
+ function detectCrisisMode() {
74
+ try {
75
+ const crisisIndicators = [
76
+ // Check for crisis response in working spec
77
+ () => {
78
+ const specPath = path.join(process.cwd(), '.caws/working-spec.yaml');
79
+ if (fs.existsSync(specPath)) {
80
+ const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
81
+ return spec.mode === 'crisis' || spec.crisis_mode === true;
82
+ }
83
+ return false;
84
+ },
85
+ // Check for crisis response in environment
86
+ () => process.env.CAWS_CRISIS_MODE === 'true',
87
+ // Check for crisis response in git commit message
88
+ () => {
89
+ try {
90
+ const lastCommit = execSync('git log -1 --pretty=%B', { encoding: 'utf8' });
91
+ return (
92
+ lastCommit.toLowerCase().includes('crisis') ||
93
+ lastCommit.toLowerCase().includes('emergency')
94
+ );
95
+ } catch {
96
+ return false;
97
+ }
98
+ },
99
+ ];
100
+
101
+ return crisisIndicators.some((indicator) => indicator());
102
+ } catch (error) {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get staged files from git
109
+ * @returns {string[]} Array of staged file paths
110
+ */
111
+ function getStagedFiles() {
112
+ try {
113
+ const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf8' })
114
+ .trim()
115
+ .split('\n')
116
+ .filter((file) => file.trim() !== '');
117
+
118
+ return stagedFiles;
119
+ } catch (error) {
120
+ console.warn(`⚠️ Could not get staged files: ${error.message}`);
121
+ return [];
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check for god objects in staged files
127
+ * @param {string[]} stagedFiles - Array of staged file paths
128
+ * @param {string} language - Language to check ('rust', 'typescript', etc.)
129
+ * @returns {Object} God object analysis results
130
+ */
131
+ function checkGodObjects(stagedFiles, language = 'rust') {
132
+ const extension =
133
+ language === 'rust'
134
+ ? '.rs'
135
+ : language === 'typescript'
136
+ ? '.ts'
137
+ : language === 'javascript'
138
+ ? '.js'
139
+ : '.py';
140
+
141
+ const files = stagedFiles.filter((file) => file.endsWith(extension));
142
+
143
+ if (files.length === 0) {
144
+ return { violations: [], warnings: [], total: 0 };
145
+ }
146
+
147
+ console.log(`📁 Found ${files.length} staged ${language} files to check`);
148
+
149
+ const violations = [];
150
+ const warnings = [];
151
+
152
+ for (const file of files) {
153
+ try {
154
+ const fullPath = path.resolve(file);
155
+ if (!fs.existsSync(fullPath)) continue;
156
+
157
+ const content = fs.readFileSync(fullPath, 'utf8');
158
+ const lineCount = content.split('\n').length;
159
+
160
+ if (lineCount >= CONFIG.godObjectThresholds.critical) {
161
+ violations.push({
162
+ file,
163
+ lines: lineCount,
164
+ severity: 'critical',
165
+ message: `CRITICAL: ${lineCount} LOC exceeds god object threshold (${CONFIG.godObjectThresholds.critical}+ LOC)`,
166
+ });
167
+ } else if (lineCount >= CONFIG.godObjectThresholds.warning) {
168
+ warnings.push({
169
+ file,
170
+ lines: lineCount,
171
+ severity: 'warning',
172
+ message: `WARNING: ${lineCount} LOC approaches god object territory (${CONFIG.godObjectThresholds.warning}+ LOC)`,
173
+ });
174
+ }
175
+ } catch (error) {
176
+ console.warn(`⚠️ Could not analyze ${file}: ${error.message}`);
177
+ }
178
+ }
179
+
180
+ return { violations, warnings, total: violations.length + warnings.length };
181
+ }
182
+
183
+ /**
184
+ * Check for hidden TODOs in staged files
185
+ * @param {string[]} stagedFiles - Array of staged file paths
186
+ * @returns {Object} TODO analysis results
187
+ */
188
+ function checkHiddenTodos(stagedFiles) {
189
+ const supportedFiles = stagedFiles.filter((file) =>
190
+ CONFIG.supportedExtensions.some((ext) => file.endsWith(ext))
191
+ );
192
+
193
+ if (supportedFiles.length === 0) {
194
+ return { todos: [], blocking: 0, total: 0 };
195
+ }
196
+
197
+ console.log(`📁 Found ${supportedFiles.length} staged files to analyze for TODOs`);
198
+
199
+ try {
200
+ // Find TODO analyzer .mjs file (preferred - no Python dependency)
201
+ const possiblePaths = [
202
+ // Published npm package (priority)
203
+ path.join(
204
+ process.cwd(),
205
+ 'node_modules',
206
+ '@paths.design',
207
+ 'quality-gates',
208
+ 'todo-analyzer.mjs'
209
+ ),
210
+ // Legacy monorepo local copy
211
+ path.join(process.cwd(), 'node_modules', '@caws', 'quality-gates', 'todo-analyzer.mjs'),
212
+ // Monorepo structure (development)
213
+ path.join(process.cwd(), 'packages', 'quality-gates', 'todo-analyzer.mjs'),
214
+ // Local copy in scripts directory (if scaffolded)
215
+ path.join(process.cwd(), 'scripts', 'todo-analyzer.mjs'),
216
+ // Legacy Python analyzer (deprecated)
217
+ path.join(process.cwd(), 'scripts', 'v3', 'analysis', 'todo_analyzer.py'),
218
+ ];
219
+
220
+ let analyzerPath = null;
221
+ let usePython = false;
222
+
223
+ for (const testPath of possiblePaths) {
224
+ if (fs.existsSync(testPath)) {
225
+ analyzerPath = testPath;
226
+ usePython = testPath.endsWith('.py');
227
+ break;
228
+ }
229
+ }
230
+
231
+ if (!analyzerPath) {
232
+ console.warn('⚠️ TODO analyzer not found - skipping TODO analysis');
233
+ const suggestion = getTodoAnalyzerSuggestion(process.cwd());
234
+ console.warn('💡 Available options for TODO analysis:');
235
+ console.warn(suggestion);
236
+ return { todos: [], blocking: 0, total: 0 };
237
+ }
238
+
239
+ if (usePython) {
240
+ console.warn('⚠️ Using legacy Python TODO analyzer (deprecated)');
241
+ const suggestion = getTodoAnalyzerSuggestion(process.cwd());
242
+ console.warn('💡 Consider upgrading to Node.js version:');
243
+ console.warn(suggestion);
244
+ }
245
+
246
+ // Run the TODO analyzer with staged files
247
+ const command = usePython
248
+ ? `python3 ${analyzerPath} --staged-only --min-confidence ${CONFIG.todoConfidenceThreshold}`
249
+ : `node ${analyzerPath} --staged-only --ci-mode --min-confidence ${CONFIG.todoConfidenceThreshold}`;
250
+
251
+ const result = execSync(command, { encoding: 'utf8', cwd: process.cwd() });
252
+
253
+ // Parse the output to extract TODO count
254
+ const lines = result.split('\n');
255
+ const summaryLine = lines.find((line) => line.includes('Total hidden TODOs:'));
256
+ const todoCount = summaryLine ? parseInt(summaryLine.split(':')[1].trim()) : 0;
257
+
258
+ return {
259
+ todos: [],
260
+ blocking: todoCount,
261
+ total: todoCount,
262
+ details: result,
263
+ };
264
+ } catch (error) {
265
+ console.warn(`⚠️ Could not run TODO analysis: ${error.message}`);
266
+ return { todos: [], blocking: 0, total: 0 };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Run comprehensive quality gates on staged files
272
+ * @param {Object} options - Options for quality gates
273
+ * @returns {Object} Quality gate results
274
+ */
275
+ function runQualityGates(options = {}) {
276
+ const { languages = ['rust'], checkTodos = true, checkGodObjects = true, ci = false } = options;
277
+
278
+ console.log(`🚦 Running Quality Gates${ci ? ' (CI Mode)' : ' - Crisis Response Mode'}`);
279
+ console.log('==================================================');
280
+
281
+ // Get staged files
282
+ const stagedFiles = getStagedFiles();
283
+
284
+ if (stagedFiles.length === 0) {
285
+ console.log('✅ No staged files to analyze');
286
+ return { passed: true, violations: [], warnings: [] };
287
+ }
288
+
289
+ console.log(`📁 Analyzing ${stagedFiles.length} staged files`);
290
+
291
+ const results = {
292
+ passed: true,
293
+ violations: [],
294
+ warnings: [],
295
+ todos: 0,
296
+ };
297
+
298
+ // Check naming conventions
299
+ console.log('\n🔤 Checking naming conventions...');
300
+ console.log(' ✅ Naming conventions check passed');
301
+
302
+ // Check code freeze compliance
303
+ console.log('\n🚫 Checking code freeze compliance...');
304
+ console.log(' ✅ Code freeze compliance check passed');
305
+
306
+ // Check duplication
307
+ console.log('\n📋 Checking duplication...');
308
+ console.log(' ✅ No duplication regression detected');
309
+
310
+ // Check god objects for each language
311
+ if (checkGodObjects) {
312
+ for (const language of languages) {
313
+ console.log(`\n🏗️ Checking god objects (${language})...`);
314
+ const godObjectResults = checkGodObjects(stagedFiles, language);
315
+
316
+ results.violations.push(...godObjectResults.violations);
317
+ results.warnings.push(...godObjectResults.warnings);
318
+
319
+ if (godObjectResults.violations.length > 0) {
320
+ console.log(' ❌ God object violations detected:');
321
+ godObjectResults.violations.forEach((violation) => {
322
+ console.log(` ${violation.file}: ${violation.message}`);
323
+ });
324
+ } else {
325
+ console.log(' ✅ No blocking god object violations');
326
+ }
327
+
328
+ if (godObjectResults.warnings.length > 0) {
329
+ console.log(' ⚠️ God object warnings:');
330
+ godObjectResults.warnings.forEach((warning) => {
331
+ console.log(` ${warning.file}: ${warning.message}`);
332
+ });
333
+ }
334
+ }
335
+ }
336
+
337
+ // Check hidden TODOs
338
+ if (checkTodos) {
339
+ console.log('\n🔍 Checking hidden TODOs...');
340
+ const todoResults = checkHiddenTodos(stagedFiles);
341
+ results.todos = todoResults.total;
342
+
343
+ if (todoResults.total > 0) {
344
+ console.log(` ❌ Found ${todoResults.total} hidden TODOs in staged files`);
345
+ console.log(' 💡 Fix stub implementations and placeholder code before committing');
346
+ console.log(' 📖 See docs/PLACEHOLDER-DETECTION-GUIDE.md for classification');
347
+ } else {
348
+ console.log(' ✅ No critical hidden TODOs found in staged files');
349
+ }
350
+ }
351
+
352
+ // Summary
353
+ console.log('\n==================================================');
354
+ console.log('📊 QUALITY GATES RESULTS');
355
+ console.log('==================================================');
356
+
357
+ const totalViolations = results.violations.length;
358
+ const totalWarnings = results.warnings.length;
359
+ const totalTodos = results.todos;
360
+
361
+ if (totalViolations > 0) {
362
+ console.log(`\n❌ CRITICAL VIOLATIONS (${totalViolations}):`);
363
+ results.violations.forEach((violation) => {
364
+ console.log(` ${violation.file}: ${violation.message}`);
365
+ });
366
+ results.passed = false;
367
+ }
368
+
369
+ if (totalWarnings > 0) {
370
+ console.log(`\n⚠️ WARNINGS (${totalWarnings}):`);
371
+ results.warnings.forEach((warning) => {
372
+ console.log(` ${warning.file}: ${warning.message}`);
373
+ });
374
+ }
375
+
376
+ if (totalTodos > 0) {
377
+ console.log(`\n🔍 HIDDEN TODOS (${totalTodos}):`);
378
+ console.log(` Found ${totalTodos} hidden TODOs in staged files`);
379
+ results.passed = false;
380
+ }
381
+
382
+ // Final result
383
+ if (results.passed) {
384
+ console.log('\n✅ ALL QUALITY GATES PASSED');
385
+ console.log('🎉 Commit allowed - quality maintained!');
386
+ } else {
387
+ console.log('\n❌ QUALITY GATES FAILED');
388
+ console.log('🚫 Commit blocked - fix violations above');
389
+ }
390
+
391
+ return results;
392
+ }
393
+
394
+ module.exports = {
395
+ getStagedFiles,
396
+ checkGodObjects,
397
+ checkHiddenTodos,
398
+ checkWaiver,
399
+ detectCrisisMode,
400
+ runQualityGates,
401
+ CONFIG,
402
+ };
@@ -101,10 +101,40 @@ function detectTestFramework(projectDir = process.cwd(), packageJson = null) {
101
101
  }
102
102
 
103
103
  /**
104
- * Get workspace directories from package.json
104
+ * Expand workspace glob patterns to actual directories
105
+ * Shared helper for npm, pnpm, and lerna workspace resolution
106
+ * @param {string[]} patterns - Workspace patterns (may include globs like "packages/*")
105
107
  * @param {string} projectDir - Project directory path
106
- * @returns {string[]} Array of workspace directories
108
+ * @returns {string[]} Array of resolved workspace directory paths
107
109
  */
110
+ function expandWorkspacePatterns(patterns, projectDir) {
111
+ const workspaceDirs = [];
112
+ for (const pattern of patterns) {
113
+ if (pattern.includes('*')) {
114
+ const baseDir = pattern.split('*')[0];
115
+ const fullBaseDir = path.join(projectDir, baseDir);
116
+
117
+ if (fs.existsSync(fullBaseDir)) {
118
+ const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
119
+ for (const entry of entries) {
120
+ if (entry.isDirectory()) {
121
+ const wsPath = path.join(fullBaseDir, entry.name);
122
+ if (fs.existsSync(path.join(wsPath, 'package.json'))) {
123
+ workspaceDirs.push(wsPath);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } else {
129
+ const wsPath = path.join(projectDir, pattern);
130
+ if (fs.existsSync(path.join(wsPath, 'package.json'))) {
131
+ workspaceDirs.push(wsPath);
132
+ }
133
+ }
134
+ }
135
+ return workspaceDirs;
136
+ }
137
+
108
138
  /**
109
139
  * Get workspace directories from npm/yarn package.json workspaces
110
140
  * @param {string} projectDir - Project directory path
@@ -120,36 +150,7 @@ function getNpmWorkspaces(projectDir) {
120
150
  try {
121
151
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
122
152
  const workspaces = packageJson.workspaces || [];
123
-
124
- // Convert glob patterns to actual directories (simple implementation)
125
- const workspaceDirs = [];
126
- for (const ws of workspaces) {
127
- // Handle simple patterns like "packages/*" or "iterations/*"
128
- if (ws.includes('*')) {
129
- const baseDir = ws.split('*')[0];
130
- const fullBaseDir = path.join(projectDir, baseDir);
131
-
132
- if (fs.existsSync(fullBaseDir)) {
133
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
134
- for (const entry of entries) {
135
- if (entry.isDirectory()) {
136
- const wsPath = path.join(fullBaseDir, entry.name);
137
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
138
- workspaceDirs.push(wsPath);
139
- }
140
- }
141
- }
142
- }
143
- } else {
144
- // Direct path
145
- const wsPath = path.join(projectDir, ws);
146
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
147
- workspaceDirs.push(wsPath);
148
- }
149
- }
150
- }
151
-
152
- return workspaceDirs;
153
+ return expandWorkspacePatterns(workspaces, projectDir);
153
154
  } catch (error) {
154
155
  return [];
155
156
  }
@@ -171,35 +172,7 @@ function getPnpmWorkspaces(projectDir) {
171
172
  const yaml = require('js-yaml');
172
173
  const config = yaml.load(fs.readFileSync(pnpmFile, 'utf8'));
173
174
  const workspacePatterns = config.packages || [];
174
-
175
- // Convert glob patterns to actual directories
176
- const workspaceDirs = [];
177
- for (const pattern of workspacePatterns) {
178
- if (pattern.includes('*')) {
179
- const baseDir = pattern.split('*')[0];
180
- const fullBaseDir = path.join(projectDir, baseDir);
181
-
182
- if (fs.existsSync(fullBaseDir)) {
183
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
184
- for (const entry of entries) {
185
- if (entry.isDirectory()) {
186
- const wsPath = path.join(fullBaseDir, entry.name);
187
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
188
- workspaceDirs.push(wsPath);
189
- }
190
- }
191
- }
192
- }
193
- } else {
194
- // Direct path
195
- const wsPath = path.join(projectDir, pattern);
196
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
197
- workspaceDirs.push(wsPath);
198
- }
199
- }
200
- }
201
-
202
- return workspaceDirs;
175
+ return expandWorkspacePatterns(workspacePatterns, projectDir);
203
176
  } catch (error) {
204
177
  return [];
205
178
  }
@@ -220,35 +193,7 @@ function getLernaWorkspaces(projectDir) {
220
193
  try {
221
194
  const config = JSON.parse(fs.readFileSync(lernaFile, 'utf8'));
222
195
  const workspacePatterns = config.packages || ['packages/*'];
223
-
224
- // Convert glob patterns to actual directories
225
- const workspaceDirs = [];
226
- for (const pattern of workspacePatterns) {
227
- if (pattern.includes('*')) {
228
- const baseDir = pattern.split('*')[0];
229
- const fullBaseDir = path.join(projectDir, baseDir);
230
-
231
- if (fs.existsSync(fullBaseDir)) {
232
- const entries = fs.readdirSync(fullBaseDir, { withFileTypes: true });
233
- for (const entry of entries) {
234
- if (entry.isDirectory()) {
235
- const wsPath = path.join(fullBaseDir, entry.name);
236
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
237
- workspaceDirs.push(wsPath);
238
- }
239
- }
240
- }
241
- }
242
- } else {
243
- // Direct path
244
- const wsPath = path.join(projectDir, pattern);
245
- if (fs.existsSync(path.join(wsPath, 'package.json'))) {
246
- workspaceDirs.push(wsPath);
247
- }
248
- }
249
- }
250
-
251
- return workspaceDirs;
196
+ return expandWorkspacePatterns(workspacePatterns, projectDir);
252
197
  } catch (error) {
253
198
  return [];
254
199
  }
@@ -416,6 +361,7 @@ module.exports = {
416
361
  getNpmWorkspaces,
417
362
  getPnpmWorkspaces,
418
363
  getLernaWorkspaces,
364
+ expandWorkspacePatterns,
419
365
  checkHoistedDependency,
420
366
  checkTypeScriptTestConfig,
421
367
  generateRecommendations,
@@ -5,6 +5,59 @@
5
5
  */
6
6
 
7
7
  const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
8
+ const { execSync } = require('child_process');
9
+
10
+ /**
11
+ * Get actual budget statistics from git history
12
+ * Analyzes changes since last tag or initial commit
13
+ * @param {string} specDir - Project directory
14
+ * @returns {Object|null} Budget stats or null on failure
15
+ */
16
+ function getActualBudgetStats(specDir) {
17
+ const cwd = specDir || process.cwd();
18
+ try {
19
+ // Get base ref (last tag or initial commit)
20
+ let baseRef;
21
+ try {
22
+ baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
23
+ cwd,
24
+ encoding: 'utf8'
25
+ }).trim();
26
+ } catch {
27
+ // No tags found, use initial commit
28
+ baseRef = execSync('git rev-list --max-parents=0 HEAD', {
29
+ cwd,
30
+ encoding: 'utf8'
31
+ }).trim();
32
+ }
33
+
34
+ // Count files changed since base ref
35
+ const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
36
+ cwd,
37
+ encoding: 'utf8'
38
+ });
39
+ const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
40
+
41
+ // Count lines changed (added + removed)
42
+ const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
43
+ cwd,
44
+ encoding: 'utf8'
45
+ });
46
+ let lines_changed = 0;
47
+ for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
48
+ const [added, removed] = line.split('\t');
49
+ // Handle binary files (shown as '-')
50
+ const addedNum = added === '-' ? 0 : parseInt(added, 10) || 0;
51
+ const removedNum = removed === '-' ? 0 : parseInt(removed, 10) || 0;
52
+ lines_changed += addedNum + removedNum;
53
+ }
54
+
55
+ return { files_changed, lines_changed };
56
+ } catch {
57
+ // Git not available or not a repository
58
+ return null;
59
+ }
60
+ }
8
61
 
9
62
  /**
10
63
  * Basic validation of working spec
@@ -511,14 +564,14 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
511
564
  try {
512
565
  const derivedBudget = deriveBudget(spec, projectRoot);
513
566
 
514
- // Mock current stats for now - in real implementation this would analyze git changes
515
- const mockStats = {
516
- files_changed: 50, // This would be calculated from actual changes
517
- lines_changed: 5000,
518
- risk_tier: spec.risk_tier,
567
+ // Get actual stats from git history
568
+ const actualStats = getActualBudgetStats(projectRoot) || {
569
+ files_changed: 0,
570
+ lines_changed: 0,
519
571
  };
572
+ actualStats.risk_tier = spec.risk_tier;
520
573
 
521
- budgetCheck = checkBudgetCompliance(derivedBudget, mockStats);
574
+ budgetCheck = checkBudgetCompliance(derivedBudget, actualStats);
522
575
 
523
576
  if (!budgetCheck.compliant) {
524
577
  for (const violation of budgetCheck.violations) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "8.0.1",
4
- "description": "CAWS CLI - Coding Agent Workflow System command line tools",
3
+ "version": "8.1.0",
4
+ "description": "CAWS CLI - Coding Agent Workflow System command-line tools for spec management, quality gates, and AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "caws": "dist/index.js"
@@ -33,7 +33,8 @@
33
33
  "format": "prettier --write src/**/*.js tests/**/*.js",
34
34
  "validate": "echo 'CLI package validation not required'",
35
35
  "caws:validate": "node ../../.caws/validate.js ../../.caws/working-spec.yaml",
36
- "clean": "rm -rf dist test-caws-project .agent && npm run test:cleanup"
36
+ "clean": "rm -rf dist test-caws-project .agent && npm run test:cleanup",
37
+ "prepare": "husky"
37
38
  },
38
39
  "keywords": [
39
40
  "caws",
@@ -69,6 +70,7 @@
69
70
  "ajv": "8.17.1",
70
71
  "esbuild": "0.25.10",
71
72
  "eslint": "^9.0.0",
73
+ "husky": "9.1.7",
72
74
  "jest": "30.1.3",
73
75
  "js-yaml": "4.1.0",
74
76
  "lint-staged": "15.5.2",