@paths.design/caws-cli 7.0.2 → 7.0.3
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.js +5 -4
- package/dist/commands/diagnose.js +24 -19
- package/dist/commands/init.js +51 -4
- package/dist/commands/specs.js +40 -1
- package/dist/commands/status.js +2 -2
- package/dist/commands/tool.js +2 -3
- package/dist/config/index.js +17 -8
- package/dist/generators/working-spec.js +19 -6
- package/dist/scaffold/git-hooks.js +127 -29
- package/dist/scaffold/index.js +53 -7
- package/dist/templates/.caws/tools/README.md +20 -0
- package/dist/templates/.cursor/README.md +311 -0
- package/dist/templates/.cursor/hooks/audit.sh +55 -0
- package/dist/templates/.cursor/hooks/block-dangerous.sh +83 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +52 -0
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +130 -0
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +121 -0
- package/dist/templates/.cursor/hooks/format.sh +38 -0
- package/dist/templates/.cursor/hooks/naming-check.sh +64 -0
- package/dist/templates/.cursor/hooks/scan-secrets.sh +46 -0
- package/dist/templates/.cursor/hooks/scope-guard.sh +52 -0
- package/dist/templates/.cursor/hooks/validate-spec.sh +83 -0
- package/dist/templates/.cursor/hooks.json +59 -0
- package/dist/templates/.cursor/rules/00-claims-verification.mdc +144 -0
- package/dist/templates/.cursor/rules/01-working-style.mdc +50 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +370 -0
- package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
- package/dist/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
- package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
- package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
- package/dist/templates/.cursor/rules/07-process-ops.mdc +20 -0
- package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
- package/dist/templates/.cursor/rules/09-docstrings.mdc +89 -0
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +390 -0
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +385 -0
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +516 -0
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +588 -0
- package/dist/templates/.cursor/rules/README.md +148 -0
- package/dist/templates/.github/copilot/instructions.md +311 -0
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +5 -0
- package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +5 -0
- package/dist/templates/.vscode/launch.json +56 -0
- package/dist/templates/.vscode/settings.json +93 -0
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +92 -0
- package/dist/templates/COMMIT_CONVENTIONS.md +86 -0
- package/dist/templates/OIDC_SETUP.md +300 -0
- package/dist/templates/agents.md +1047 -0
- package/dist/templates/codemod/README.md +1 -0
- package/dist/templates/codemod/test.js +93 -0
- package/dist/templates/docs/README.md +150 -0
- package/dist/templates/scripts/quality-gates/check-god-objects.js +146 -0
- package/dist/templates/scripts/quality-gates/run-quality-gates.js +50 -0
- package/dist/templates/scripts/v3/analysis/todo_analyzer.py +1997 -0
- package/dist/tool-loader.js +6 -1
- package/dist/tool-validator.js +8 -2
- package/dist/utils/detection.js +4 -3
- package/dist/utils/git-lock.js +118 -0
- package/dist/utils/gitignore-updater.js +148 -0
- package/dist/utils/quality-gates.js +47 -7
- package/dist/utils/spec-resolver.js +23 -3
- package/dist/utils/yaml-validation.js +155 -0
- package/dist/validation/spec-validation.js +81 -2
- package/package.json +2 -2
- package/templates/.caws/schemas/waivers.schema.json +30 -0
- package/templates/.caws/schemas/working-spec.schema.json +133 -0
- package/templates/.caws/templates/working-spec.template.yml +74 -0
- package/templates/.caws/tools/README.md +20 -0
- package/templates/.caws/tools/scope-guard.js +208 -0
- package/templates/.caws/tools-allow.json +331 -0
- package/templates/.caws/waivers.yml +19 -0
- package/templates/.cursor/hooks/scope-guard.sh +2 -2
- package/templates/.cursor/hooks/validate-spec.sh +42 -7
- package/templates/apps/tools/caws/COMPLETION_REPORT.md +0 -331
- package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +0 -360
- package/templates/apps/tools/caws/README.md +0 -463
- package/templates/apps/tools/caws/TEST_STATUS.md +0 -365
- package/templates/apps/tools/caws/attest.js +0 -357
- package/templates/apps/tools/caws/ci-optimizer.js +0 -642
- package/templates/apps/tools/caws/config.ts +0 -245
- package/templates/apps/tools/caws/cross-functional.js +0 -876
- package/templates/apps/tools/caws/dashboard.js +0 -1112
- package/templates/apps/tools/caws/flake-detector.ts +0 -362
- package/templates/apps/tools/caws/gates.js +0 -198
- package/templates/apps/tools/caws/gates.ts +0 -271
- package/templates/apps/tools/caws/language-adapters.ts +0 -381
- package/templates/apps/tools/caws/language-support.d.ts +0 -367
- package/templates/apps/tools/caws/language-support.d.ts.map +0 -1
- package/templates/apps/tools/caws/language-support.js +0 -585
- package/templates/apps/tools/caws/legacy-assessment.ts +0 -408
- package/templates/apps/tools/caws/legacy-assessor.js +0 -764
- package/templates/apps/tools/caws/mutant-analyzer.js +0 -734
- package/templates/apps/tools/caws/perf-budgets.ts +0 -349
- package/templates/apps/tools/caws/prompt-lint.js.backup +0 -274
- package/templates/apps/tools/caws/property-testing.js +0 -707
- package/templates/apps/tools/caws/provenance.d.ts +0 -14
- package/templates/apps/tools/caws/provenance.d.ts.map +0 -1
- package/templates/apps/tools/caws/provenance.js +0 -132
- package/templates/apps/tools/caws/provenance.js.backup +0 -73
- package/templates/apps/tools/caws/provenance.ts +0 -211
- package/templates/apps/tools/caws/security-provenance.ts +0 -483
- package/templates/apps/tools/caws/shared/base-tool.ts +0 -281
- package/templates/apps/tools/caws/shared/config-manager.ts +0 -366
- package/templates/apps/tools/caws/shared/gate-checker.ts +0 -849
- package/templates/apps/tools/caws/shared/types.ts +0 -444
- package/templates/apps/tools/caws/shared/validator.ts +0 -305
- package/templates/apps/tools/caws/shared/waivers-manager.ts +0 -174
- package/templates/apps/tools/caws/spec-test-mapper.ts +0 -391
- package/templates/apps/tools/caws/test-quality.js +0 -578
- package/templates/apps/tools/caws/validate.js +0 -76
- package/templates/apps/tools/caws/validate.ts +0 -228
- package/templates/apps/tools/caws/waivers.js +0 -344
- /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/waivers.schema.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/schemas/working-spec.schema.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/templates/working-spec.template.yml +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws/tools}/scope-guard.js +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/tools-allow.json +0 -0
- /package/{templates/apps/tools/caws → dist/templates/.caws}/waivers.yml +0 -0
package/dist/tool-loader.js
CHANGED
|
@@ -19,8 +19,13 @@ const { safeAsync } = require('./error-handler');
|
|
|
19
19
|
class ToolLoader extends EventEmitter {
|
|
20
20
|
constructor(options = {}) {
|
|
21
21
|
super();
|
|
22
|
+
// Check new location first, fall back to legacy location
|
|
23
|
+
const newToolsDir = path.join(process.cwd(), '.caws/tools');
|
|
24
|
+
const legacyToolsDir = path.join(process.cwd(), 'apps/tools/caws');
|
|
25
|
+
const defaultToolsDir = fs.existsSync(newToolsDir) ? newToolsDir : legacyToolsDir;
|
|
26
|
+
|
|
22
27
|
this.options = {
|
|
23
|
-
toolsDir: options.toolsDir ||
|
|
28
|
+
toolsDir: options.toolsDir || defaultToolsDir,
|
|
24
29
|
cacheEnabled: options.cacheEnabled !== false,
|
|
25
30
|
timeout: options.timeout || 10000,
|
|
26
31
|
maxTools: options.maxTools || 50,
|
package/dist/tool-validator.js
CHANGED
|
@@ -15,9 +15,15 @@ const crypto = require('crypto');
|
|
|
15
15
|
*/
|
|
16
16
|
class ToolValidator {
|
|
17
17
|
constructor(options = {}) {
|
|
18
|
+
// Check new location first, fall back to legacy location
|
|
19
|
+
const newAllowlistPath = path.join(process.cwd(), '.caws/tools-allow.json');
|
|
20
|
+
const legacyAllowlistPath = path.join(process.cwd(), 'apps/tools/caws/tools-allow.json');
|
|
21
|
+
const defaultAllowlistPath = fs.existsSync(newAllowlistPath)
|
|
22
|
+
? newAllowlistPath
|
|
23
|
+
: legacyAllowlistPath;
|
|
24
|
+
|
|
18
25
|
this.options = {
|
|
19
|
-
allowlistPath:
|
|
20
|
-
options.allowlistPath || path.join(process.cwd(), 'apps/tools/caws/tools-allow.json'),
|
|
26
|
+
allowlistPath: options.allowlistPath || defaultAllowlistPath,
|
|
21
27
|
strictMode: options.strictMode !== false,
|
|
22
28
|
maxFileSize: options.maxFileSize || 1024 * 1024, // 1MB
|
|
23
29
|
...options,
|
package/dist/utils/detection.js
CHANGED
|
@@ -73,9 +73,10 @@ function detectCAWSSetup(cwd = process.cwd()) {
|
|
|
73
73
|
const specFiles = files.filter((f) => f.endsWith('-spec.yaml'));
|
|
74
74
|
const hasMultipleSpecs = specFiles.length > 1;
|
|
75
75
|
|
|
76
|
-
// Check for tools directory (enhanced setup)
|
|
77
|
-
const toolsDir = path.join(cwd, '
|
|
78
|
-
const
|
|
76
|
+
// Check for tools directory (enhanced setup) - check new location first
|
|
77
|
+
const toolsDir = path.join(cwd, '.caws/tools');
|
|
78
|
+
const legacyToolsDir = path.join(cwd, 'apps/tools/caws');
|
|
79
|
+
const hasTools = fs.existsSync(toolsDir) || fs.existsSync(legacyToolsDir);
|
|
79
80
|
|
|
80
81
|
// Determine setup type
|
|
81
82
|
let setupType = 'basic';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Git Lock Detection Utilities
|
|
3
|
+
* Functions for detecting and handling git locks
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check for git lock files
|
|
12
|
+
* @param {string} projectRoot - Project root directory
|
|
13
|
+
* @returns {Object} Lock status information
|
|
14
|
+
*/
|
|
15
|
+
function checkGitLock(projectRoot) {
|
|
16
|
+
const lockFile = path.join(projectRoot, '.git', 'index.lock');
|
|
17
|
+
const headLockFile = path.join(projectRoot, '.git', 'HEAD.lock');
|
|
18
|
+
|
|
19
|
+
const result = {
|
|
20
|
+
locked: false,
|
|
21
|
+
stale: false,
|
|
22
|
+
lockFiles: [],
|
|
23
|
+
message: null,
|
|
24
|
+
suggestion: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Check index.lock
|
|
28
|
+
if (fs.existsSync(lockFile)) {
|
|
29
|
+
const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|
30
|
+
const lockAgeMinutes = Math.floor(lockAge / 60000);
|
|
31
|
+
|
|
32
|
+
result.locked = true;
|
|
33
|
+
result.lockFiles.push({
|
|
34
|
+
path: '.git/index.lock',
|
|
35
|
+
age: lockAgeMinutes,
|
|
36
|
+
stale: lockAgeMinutes > 5,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (lockAgeMinutes > 5) {
|
|
40
|
+
// Stale lock (older than 5 minutes)
|
|
41
|
+
result.stale = true;
|
|
42
|
+
result.message = `Stale git lock detected (${lockAgeMinutes} minutes old). This may indicate a crashed git process.`;
|
|
43
|
+
result.suggestion = 'Remove stale lock: rm .git/index.lock';
|
|
44
|
+
} else {
|
|
45
|
+
// Active lock
|
|
46
|
+
result.message =
|
|
47
|
+
'Git lock detected. Another git process may be running.';
|
|
48
|
+
result.suggestion =
|
|
49
|
+
'Wait for the other process to complete, or check for running git/editor processes';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check HEAD.lock
|
|
54
|
+
if (fs.existsSync(headLockFile)) {
|
|
55
|
+
const lockAge = Date.now() - fs.statSync(headLockFile).mtimeMs;
|
|
56
|
+
const lockAgeMinutes = Math.floor(lockAge / 60000);
|
|
57
|
+
|
|
58
|
+
result.locked = true;
|
|
59
|
+
result.lockFiles.push({
|
|
60
|
+
path: '.git/HEAD.lock',
|
|
61
|
+
age: lockAgeMinutes,
|
|
62
|
+
stale: lockAgeMinutes > 5,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (lockAgeMinutes > 5) {
|
|
66
|
+
result.stale = true;
|
|
67
|
+
if (!result.message) {
|
|
68
|
+
result.message = `Stale git lock detected (${lockAgeMinutes} minutes old).`;
|
|
69
|
+
result.suggestion = 'Remove stale lock: rm .git/HEAD.lock';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format git lock error message
|
|
79
|
+
* @param {Object} lockStatus - Lock status from checkGitLock
|
|
80
|
+
* @returns {string} Formatted error message
|
|
81
|
+
*/
|
|
82
|
+
function formatGitLockError(lockStatus) {
|
|
83
|
+
if (!lockStatus.locked) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let message = '⚠️ Git lock detected\n';
|
|
88
|
+
message += ` ${lockStatus.message}\n`;
|
|
89
|
+
|
|
90
|
+
if (lockStatus.lockFiles.length > 0) {
|
|
91
|
+
message += '\n Lock files:\n';
|
|
92
|
+
for (const lockFile of lockStatus.lockFiles) {
|
|
93
|
+
message += ` - ${lockFile.path} (${lockFile.age} minutes old)`;
|
|
94
|
+
if (lockFile.stale) {
|
|
95
|
+
message += ' [STALE]';
|
|
96
|
+
}
|
|
97
|
+
message += '\n';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (lockStatus.suggestion) {
|
|
102
|
+
message += `\n 💡 ${lockStatus.suggestion}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (lockStatus.stale) {
|
|
106
|
+
message +=
|
|
107
|
+
'\n ⚠️ Warning: Removing stale locks may cause data loss if another process is actually running.\n';
|
|
108
|
+
message += ' Check for running git/editor processes before removing locks.\n';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return message;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
checkGitLock,
|
|
116
|
+
formatGitLockError,
|
|
117
|
+
};
|
|
118
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Gitignore Updater Utility
|
|
3
|
+
* Updates .gitignore to properly handle CAWS runtime files vs source files
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* CAWS .gitignore entries
|
|
13
|
+
*
|
|
14
|
+
* Strategy: Track shared/collaborative files, ignore local-only runtime data
|
|
15
|
+
*
|
|
16
|
+
* TRACKED (shared with team):
|
|
17
|
+
* - .caws/working-spec.yaml (main spec)
|
|
18
|
+
* - .caws/specs/*.yaml (feature specs)
|
|
19
|
+
* - .caws/policy.yaml (team policy)
|
|
20
|
+
* - .caws/waivers/*.yaml (project-wide waivers)
|
|
21
|
+
* - .caws/provenance/ (audit trails for compliance)
|
|
22
|
+
* - .caws/changes/ (change tracking for team visibility)
|
|
23
|
+
* - .caws/archive/ (archived changes for history)
|
|
24
|
+
* - .caws/plans/*.md (implementation plans)
|
|
25
|
+
*
|
|
26
|
+
* IGNORED (local-only):
|
|
27
|
+
* - .agent/ (agent runtime tracking, local to each developer)
|
|
28
|
+
* - Temporary files (*.tmp, *.bak)
|
|
29
|
+
* - Logs (caws.log, debug logs)
|
|
30
|
+
* - Local overrides (caws.local.*)
|
|
31
|
+
*/
|
|
32
|
+
const CAWS_GITIGNORE_ENTRIES = `
|
|
33
|
+
# CAWS Local Runtime Data (developer-specific, should not be tracked)
|
|
34
|
+
# ====================================================================
|
|
35
|
+
# Note: Specs, policy, waivers, provenance, and plans ARE tracked for team collaboration
|
|
36
|
+
# Only local agent tracking, generated tools, and temporary files are ignored
|
|
37
|
+
|
|
38
|
+
# Agent runtime tracking (local to each developer)
|
|
39
|
+
.agent/
|
|
40
|
+
|
|
41
|
+
# CAWS tools (now in .caws/tools/)
|
|
42
|
+
.caws/tools/
|
|
43
|
+
# Legacy location (for backward compatibility)
|
|
44
|
+
apps/tools/caws/
|
|
45
|
+
|
|
46
|
+
# Temporary CAWS files
|
|
47
|
+
**/*.caws.tmp
|
|
48
|
+
**/*.working-spec.bak
|
|
49
|
+
.caws/*.tmp
|
|
50
|
+
.caws/*.bak
|
|
51
|
+
|
|
52
|
+
# CAWS logs (local debugging)
|
|
53
|
+
caws-debug.log*
|
|
54
|
+
**/caws.log
|
|
55
|
+
.caws/*.log
|
|
56
|
+
|
|
57
|
+
# Local development overrides (developer-specific)
|
|
58
|
+
caws.local.*
|
|
59
|
+
.caws/local.*
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Update .gitignore to include CAWS runtime file exclusions
|
|
64
|
+
* @param {string} projectRoot - Project root directory
|
|
65
|
+
* @param {Object} options - Options
|
|
66
|
+
* @param {boolean} options.force - Force update even if entries exist
|
|
67
|
+
* @returns {Promise<boolean>} Whether .gitignore was updated
|
|
68
|
+
*/
|
|
69
|
+
async function updateGitignore(projectRoot, options = {}) {
|
|
70
|
+
const { force = false } = options;
|
|
71
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Read existing .gitignore or create empty
|
|
75
|
+
let existingContent = '';
|
|
76
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
77
|
+
existingContent = await fs.readFile(gitignorePath, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if CAWS entries already exist (check for either old or new header)
|
|
81
|
+
const hasCawsEntries =
|
|
82
|
+
existingContent.includes('# CAWS Local Runtime Data') ||
|
|
83
|
+
existingContent.includes('# CAWS Runtime Data');
|
|
84
|
+
|
|
85
|
+
if (hasCawsEntries && !force) {
|
|
86
|
+
// Already has CAWS entries, skip
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If old entries exist, replace them with new ones
|
|
91
|
+
if (existingContent.includes('# CAWS Runtime Data') && force) {
|
|
92
|
+
// Remove old CAWS entries (between "# CAWS Runtime Data" and next major section)
|
|
93
|
+
const lines = existingContent.split('\n');
|
|
94
|
+
const startIndex = lines.findIndex((line) => line.includes('# CAWS Runtime Data'));
|
|
95
|
+
if (startIndex !== -1) {
|
|
96
|
+
// Find the end of CAWS section (next major section starting with #)
|
|
97
|
+
let endIndex = startIndex + 1;
|
|
98
|
+
while (
|
|
99
|
+
endIndex < lines.length &&
|
|
100
|
+
(lines[endIndex].trim() === '' ||
|
|
101
|
+
lines[endIndex].startsWith('#') ||
|
|
102
|
+
lines[endIndex].startsWith('.caws/') ||
|
|
103
|
+
lines[endIndex].startsWith('.agent/') ||
|
|
104
|
+
lines[endIndex].includes('caws') ||
|
|
105
|
+
lines[endIndex].includes('CAWS'))
|
|
106
|
+
) {
|
|
107
|
+
endIndex++;
|
|
108
|
+
}
|
|
109
|
+
// Remove old section and insert new one
|
|
110
|
+
const before = lines.slice(0, startIndex).join('\n');
|
|
111
|
+
const after = lines.slice(endIndex).join('\n');
|
|
112
|
+
existingContent = [before, after].filter(Boolean).join('\n');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Append CAWS entries
|
|
117
|
+
const updatedContent = existingContent.trim() + '\n' + CAWS_GITIGNORE_ENTRIES.trim() + '\n';
|
|
118
|
+
|
|
119
|
+
await fs.writeFile(gitignorePath, updatedContent, 'utf8');
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn(chalk.yellow(`⚠️ Could not update .gitignore: ${error.message}`));
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify .gitignore has proper CAWS entries
|
|
130
|
+
* @param {string} projectRoot - Project root directory
|
|
131
|
+
* @returns {Promise<boolean>} Whether .gitignore has CAWS entries
|
|
132
|
+
*/
|
|
133
|
+
async function verifyGitignore(projectRoot) {
|
|
134
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
135
|
+
|
|
136
|
+
if (!(await fs.pathExists(gitignorePath))) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const content = await fs.readFile(gitignorePath, 'utf8');
|
|
141
|
+
return content.includes('# CAWS Local Runtime Data') || content.includes('# CAWS Runtime Data');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
updateGitignore,
|
|
146
|
+
verifyGitignore,
|
|
147
|
+
CAWS_GITIGNORE_ENTRIES,
|
|
148
|
+
};
|
|
@@ -196,18 +196,58 @@ function checkHiddenTodos(stagedFiles) {
|
|
|
196
196
|
console.log(`📁 Found ${supportedFiles.length} staged files to analyze for TODOs`);
|
|
197
197
|
|
|
198
198
|
try {
|
|
199
|
-
//
|
|
200
|
-
const
|
|
201
|
-
|
|
199
|
+
// Find TODO analyzer .mjs file (preferred - no Python dependency)
|
|
200
|
+
const possiblePaths = [
|
|
201
|
+
// Published npm package (priority)
|
|
202
|
+
path.join(
|
|
203
|
+
process.cwd(),
|
|
204
|
+
'node_modules',
|
|
205
|
+
'@paths.design',
|
|
206
|
+
'quality-gates',
|
|
207
|
+
'todo-analyzer.mjs'
|
|
208
|
+
),
|
|
209
|
+
// Legacy monorepo local copy
|
|
210
|
+
path.join(process.cwd(), 'node_modules', '@caws', 'quality-gates', 'todo-analyzer.mjs'),
|
|
211
|
+
// Monorepo structure (development)
|
|
212
|
+
path.join(process.cwd(), 'packages', 'quality-gates', 'todo-analyzer.mjs'),
|
|
213
|
+
// Local copy in scripts directory (if scaffolded)
|
|
214
|
+
path.join(process.cwd(), 'scripts', 'todo-analyzer.mjs'),
|
|
215
|
+
// Legacy Python analyzer (deprecated)
|
|
216
|
+
path.join(process.cwd(), 'scripts', 'v3', 'analysis', 'todo_analyzer.py'),
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
let analyzerPath = null;
|
|
220
|
+
let usePython = false;
|
|
221
|
+
|
|
222
|
+
for (const testPath of possiblePaths) {
|
|
223
|
+
if (fs.existsSync(testPath)) {
|
|
224
|
+
analyzerPath = testPath;
|
|
225
|
+
usePython = testPath.endsWith('.py');
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!analyzerPath) {
|
|
202
231
|
console.warn('⚠️ TODO analyzer not found - skipping TODO analysis');
|
|
232
|
+
console.warn(
|
|
233
|
+
'💡 Install @paths.design/quality-gates: npm install --save-dev @paths.design/quality-gates'
|
|
234
|
+
);
|
|
203
235
|
return { todos: [], blocking: 0, total: 0 };
|
|
204
236
|
}
|
|
205
237
|
|
|
238
|
+
if (usePython) {
|
|
239
|
+
console.warn('⚠️ Using legacy Python TODO analyzer (deprecated)');
|
|
240
|
+
console.warn(
|
|
241
|
+
'💡 Install @paths.design/quality-gates for Node.js version: npm install --save-dev @paths.design/quality-gates'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
206
245
|
// Run the TODO analyzer with staged files
|
|
207
|
-
const
|
|
208
|
-
`python3 ${analyzerPath} --staged-only --min-confidence ${CONFIG.todoConfidenceThreshold}
|
|
209
|
-
{
|
|
210
|
-
|
|
246
|
+
const command = usePython
|
|
247
|
+
? `python3 ${analyzerPath} --staged-only --min-confidence ${CONFIG.todoConfidenceThreshold}`
|
|
248
|
+
: `node ${analyzerPath} --staged-only --ci-mode --min-confidence ${CONFIG.todoConfidenceThreshold}`;
|
|
249
|
+
|
|
250
|
+
const result = execSync(command, { encoding: 'utf8', cwd: process.cwd() });
|
|
211
251
|
|
|
212
252
|
// Parse the output to extract TODO count
|
|
213
253
|
const lines = result.split('\n');
|
|
@@ -108,7 +108,14 @@ async function resolveSpec(options = {}) {
|
|
|
108
108
|
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
109
109
|
try {
|
|
110
110
|
const content = await fs.readFile(specPath, 'utf8');
|
|
111
|
-
|
|
111
|
+
let spec;
|
|
112
|
+
try {
|
|
113
|
+
spec = yaml.load(content);
|
|
114
|
+
} catch (yamlError) {
|
|
115
|
+
console.log(chalk.yellow(` - ${id} (YAML syntax error: ${yamlError.message})`));
|
|
116
|
+
specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'YAML error' });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
112
119
|
const status = spec.status || 'draft';
|
|
113
120
|
const type = spec.type || 'feature';
|
|
114
121
|
const statusColor =
|
|
@@ -122,7 +129,7 @@ async function resolveSpec(options = {}) {
|
|
|
122
129
|
);
|
|
123
130
|
specsInfo.push({ id, type, status, title: spec.title || 'Untitled' });
|
|
124
131
|
} catch (error) {
|
|
125
|
-
console.log(chalk.yellow(` - ${id} (error loading details)`));
|
|
132
|
+
console.log(chalk.yellow(` - ${id} (error loading details: ${error.message})`));
|
|
126
133
|
specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'Error loading' });
|
|
127
134
|
}
|
|
128
135
|
}
|
|
@@ -363,7 +370,20 @@ async function checkScopeConflicts(specIds) {
|
|
|
363
370
|
|
|
364
371
|
try {
|
|
365
372
|
const content = await fs.readFile(specPath, 'utf8');
|
|
366
|
-
|
|
373
|
+
let spec;
|
|
374
|
+
try {
|
|
375
|
+
spec = yaml.load(content);
|
|
376
|
+
} catch (yamlError) {
|
|
377
|
+
const relativePath = path.relative(process.cwd(), specPath);
|
|
378
|
+
throw new Error(
|
|
379
|
+
`Invalid YAML syntax in ${relativePath}: ${yamlError.message}\n` +
|
|
380
|
+
(yamlError.mark
|
|
381
|
+
? ` Line ${yamlError.mark.line + 1}, Column ${yamlError.mark.column + 1}\n`
|
|
382
|
+
: '') +
|
|
383
|
+
(yamlError.mark?.snippet ? ` ${yamlError.mark.snippet}\n` : '') +
|
|
384
|
+
`💡 Fix YAML syntax errors or use 'caws specs create <id>' for proper structure`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
367
387
|
|
|
368
388
|
specScopes.push({
|
|
369
389
|
id,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview YAML Validation Utilities
|
|
3
|
+
* Functions for validating YAML syntax and structure
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const yaml = require('js-yaml');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate YAML syntax for a file
|
|
13
|
+
* @param {string} filePath - Path to YAML file
|
|
14
|
+
* @returns {Object} Validation result with valid flag and error details
|
|
15
|
+
*/
|
|
16
|
+
function validateYamlSyntax(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(filePath)) {
|
|
19
|
+
return {
|
|
20
|
+
valid: false,
|
|
21
|
+
error: `File not found: ${filePath}`,
|
|
22
|
+
line: null,
|
|
23
|
+
column: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
28
|
+
yaml.load(content); // Will throw if invalid
|
|
29
|
+
|
|
30
|
+
return { valid: true };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
error: error.message,
|
|
35
|
+
line: error.mark?.line ? error.mark.line + 1 : null, // Convert to 1-based
|
|
36
|
+
column: error.mark?.column ? error.mark.column + 1 : null, // Convert to 1-based
|
|
37
|
+
snippet: error.mark?.snippet || null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate YAML syntax for multiple files
|
|
44
|
+
* @param {string[]} filePaths - Array of file paths to validate
|
|
45
|
+
* @returns {Object} Validation results with summary
|
|
46
|
+
*/
|
|
47
|
+
function validateYamlFiles(filePaths) {
|
|
48
|
+
const results = {
|
|
49
|
+
valid: true,
|
|
50
|
+
files: [],
|
|
51
|
+
errors: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (const filePath of filePaths) {
|
|
55
|
+
const validation = validateYamlSyntax(filePath);
|
|
56
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
57
|
+
|
|
58
|
+
results.files.push({
|
|
59
|
+
path: relativePath,
|
|
60
|
+
...validation,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!validation.valid) {
|
|
64
|
+
results.valid = false;
|
|
65
|
+
results.errors.push({
|
|
66
|
+
file: relativePath,
|
|
67
|
+
error: validation.error,
|
|
68
|
+
line: validation.line,
|
|
69
|
+
column: validation.column,
|
|
70
|
+
snippet: validation.snippet,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find all YAML files in .caws directory
|
|
80
|
+
* @param {string} projectRoot - Project root directory
|
|
81
|
+
* @returns {string[]} Array of YAML file paths
|
|
82
|
+
*/
|
|
83
|
+
function findCawsYamlFiles(projectRoot) {
|
|
84
|
+
const cawsDir = path.join(projectRoot, '.caws');
|
|
85
|
+
const yamlFiles = [];
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(cawsDir)) {
|
|
88
|
+
return yamlFiles;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function walkDir(dir) {
|
|
92
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const fullPath = path.join(dir, entry.name);
|
|
96
|
+
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
walkDir(fullPath);
|
|
99
|
+
} else if (
|
|
100
|
+
entry.isFile() &&
|
|
101
|
+
(entry.name.endsWith('.yaml') || entry.name.endsWith('.yml'))
|
|
102
|
+
) {
|
|
103
|
+
yamlFiles.push(fullPath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
walkDir(cawsDir);
|
|
109
|
+
return yamlFiles;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate all CAWS YAML files in project
|
|
114
|
+
* @param {string} projectRoot - Project root directory
|
|
115
|
+
* @returns {Object} Validation results
|
|
116
|
+
*/
|
|
117
|
+
function validateAllCawsYamlFiles(projectRoot) {
|
|
118
|
+
const yamlFiles = findCawsYamlFiles(projectRoot);
|
|
119
|
+
return validateYamlFiles(yamlFiles);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format validation error for display
|
|
124
|
+
* @param {Object} error - Error object from validateYamlSyntax
|
|
125
|
+
* @param {string} filePath - File path
|
|
126
|
+
* @returns {string} Formatted error message
|
|
127
|
+
*/
|
|
128
|
+
function formatYamlError(error, filePath) {
|
|
129
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
130
|
+
let message = `❌ Invalid YAML in ${relativePath}\n`;
|
|
131
|
+
message += ` Error: ${error.error}\n`;
|
|
132
|
+
|
|
133
|
+
if (error.line !== null) {
|
|
134
|
+
message += ` Line: ${error.line}`;
|
|
135
|
+
if (error.column !== null) {
|
|
136
|
+
message += `, Column: ${error.column}`;
|
|
137
|
+
}
|
|
138
|
+
message += '\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (error.snippet) {
|
|
142
|
+
message += ` ${error.snippet}\n`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return message;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
validateYamlSyntax,
|
|
150
|
+
validateYamlFiles,
|
|
151
|
+
findCawsYamlFiles,
|
|
152
|
+
validateAllCawsYamlFiles,
|
|
153
|
+
formatYamlError,
|
|
154
|
+
};
|
|
155
|
+
|
|
@@ -238,6 +238,42 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// Validate scope.out doesn't contain glob patterns
|
|
242
|
+
if (spec.scope && spec.scope.out && Array.isArray(spec.scope.out)) {
|
|
243
|
+
const globPatterns = spec.scope.out.filter(
|
|
244
|
+
(pattern) => pattern.includes('*') || pattern.includes('?')
|
|
245
|
+
);
|
|
246
|
+
if (globPatterns.length > 0) {
|
|
247
|
+
errors.push({
|
|
248
|
+
instancePath: '/scope/out',
|
|
249
|
+
message: `Unsupported glob patterns in scope.out: ${globPatterns.join(', ')}`,
|
|
250
|
+
suggestion:
|
|
251
|
+
'Use directory paths only (e.g., __pycache__/ instead of *.pyc or **/*.pyc). Python cache files are already covered by __pycache__/',
|
|
252
|
+
canAutoFix: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Auto-fix: remove glob patterns and keep only directory paths
|
|
256
|
+
if (autoFix) {
|
|
257
|
+
const fixedOut = spec.scope.out
|
|
258
|
+
.filter((pattern) => !pattern.includes('*') && !pattern.includes('?'))
|
|
259
|
+
.map((pattern) => {
|
|
260
|
+
// Ensure directory paths end with /
|
|
261
|
+
if (!pattern.includes('.') && !pattern.endsWith('/')) {
|
|
262
|
+
return pattern + '/';
|
|
263
|
+
}
|
|
264
|
+
return pattern;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
fixes.push({
|
|
268
|
+
field: 'scope.out',
|
|
269
|
+
value: fixedOut,
|
|
270
|
+
description: `Removed glob patterns from scope.out: ${globPatterns.join(', ')}`,
|
|
271
|
+
reason: 'Glob patterns are not supported in scope.out',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
241
277
|
// Auto-fix missing scope.out
|
|
242
278
|
if (spec.scope && !spec.scope.out) {
|
|
243
279
|
fixes.push({
|
|
@@ -329,7 +365,7 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
329
365
|
const suggestion = isChoreMode
|
|
330
366
|
? 'For infrastructure/setup work, add a minimal project_setup contract or create a waiver'
|
|
331
367
|
: 'Add API contracts (OpenAPI, GraphQL, etc.) or change mode to "chore" for maintenance work';
|
|
332
|
-
|
|
368
|
+
|
|
333
369
|
errors.push({
|
|
334
370
|
instancePath: '/contracts',
|
|
335
371
|
message: `Contracts required for Tier ${spec.risk_tier} changes`,
|
|
@@ -341,7 +377,8 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
341
377
|
{
|
|
342
378
|
type: 'project_setup',
|
|
343
379
|
path: '.caws/working-spec.yaml',
|
|
344
|
-
description:
|
|
380
|
+
description:
|
|
381
|
+
'Project-level CAWS configuration. Feature-specific contracts will be added as features are developed.',
|
|
345
382
|
},
|
|
346
383
|
],
|
|
347
384
|
}
|
|
@@ -392,6 +429,48 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
392
429
|
}
|
|
393
430
|
}
|
|
394
431
|
|
|
432
|
+
// Validate rollback format if present (for all tiers)
|
|
433
|
+
if (spec.rollback !== undefined) {
|
|
434
|
+
if (!Array.isArray(spec.rollback)) {
|
|
435
|
+
errors.push({
|
|
436
|
+
instancePath: '/rollback',
|
|
437
|
+
message: 'rollback must be an array of strings',
|
|
438
|
+
suggestion: 'Use format: ["Step 1", "Step 2", "Step 3"]',
|
|
439
|
+
canAutoFix: false,
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
// Check for duplicates
|
|
443
|
+
const uniqueSteps = [...new Set(spec.rollback)];
|
|
444
|
+
if (uniqueSteps.length !== spec.rollback.length) {
|
|
445
|
+
warnings.push({
|
|
446
|
+
instancePath: '/rollback',
|
|
447
|
+
message: 'Duplicate entries found in rollback array',
|
|
448
|
+
suggestion: 'Remove duplicate entries',
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (autoFix) {
|
|
452
|
+
fixes.push({
|
|
453
|
+
field: 'rollback',
|
|
454
|
+
value: uniqueSteps,
|
|
455
|
+
description: 'Removed duplicate rollback entries',
|
|
456
|
+
reason: 'Duplicate entries detected',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Validate each entry is a string
|
|
462
|
+
const invalidEntries = spec.rollback.filter((entry) => typeof entry !== 'string');
|
|
463
|
+
if (invalidEntries.length > 0) {
|
|
464
|
+
errors.push({
|
|
465
|
+
instancePath: '/rollback',
|
|
466
|
+
message: `Invalid rollback entries (must be strings): ${invalidEntries.length}`,
|
|
467
|
+
suggestion: 'All rollback entries must be string descriptions',
|
|
468
|
+
canAutoFix: false,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
395
474
|
// Validate waiver_ids format if present
|
|
396
475
|
if (spec.waiver_ids) {
|
|
397
476
|
if (!Array.isArray(spec.waiver_ids)) {
|