@paths.design/caws-cli 9.3.2 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -32
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +41 -15
- package/dist/commands/gates.js +149 -0
- package/dist/commands/init.js +150 -19
- package/dist/commands/iterate.js +81 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +74 -0
- package/dist/commands/specs.js +381 -45
- package/dist/commands/status.js +117 -9
- package/dist/commands/templates.js +0 -8
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +70 -6
- package/dist/commands/verify-acs.js +48 -76
- package/dist/commands/waivers.js +212 -13
- package/dist/commands/worktree.js +131 -26
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +121 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +109 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +157 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +51 -17
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +105 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +112 -0
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
- package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/dist/templates/.claude/settings.json +31 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +77 -8
- package/dist/templates/agents.md +50 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +50 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +530 -0
- package/dist/validation/spec-validation.js +191 -31
- package/dist/waivers-manager.js +144 -6
- package/dist/worktree/worktree-manager.js +598 -95
- package/package.json +9 -8
- package/templates/.caws/schemas/policy.schema.json +112 -0
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +96 -20
- package/templates/.caws/schemas/working-spec.schema.json +264 -57
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +10 -4
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +136 -55
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
- package/templates/.claude/settings.json +31 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +77 -8
- package/templates/{AGENTS.md → agents.md} +50 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -4,8 +4,43 @@
|
|
|
4
4
|
* @author @darianrosebrook
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { deriveBudgetSync, checkBudgetCompliance } = require('../budget-derivation');
|
|
8
10
|
const { execSync } = require('child_process');
|
|
11
|
+
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CAWSFIX-10: Canonical regex for valid spec IDs.
|
|
15
|
+
*
|
|
16
|
+
* Accepts:
|
|
17
|
+
* - Single-segment: FEAT-001, EVLOG-002, CAWSFIX-06 (legacy shape)
|
|
18
|
+
* - Multi-segment: P03-IMPL-01, ALG-001A-HARDEN-01, CAWS-FIX-03
|
|
19
|
+
*
|
|
20
|
+
* Rejects:
|
|
21
|
+
* - lowercase (feat-001)
|
|
22
|
+
* - leading digit (01-FEAT)
|
|
23
|
+
* - missing number suffix (FEAT-)
|
|
24
|
+
* - trailing hyphen (FEAT-01-)
|
|
25
|
+
* - leading/double hyphen (--FEAT-01, FEAT--001)
|
|
26
|
+
* - empty string
|
|
27
|
+
*
|
|
28
|
+
* Grammar: [PREFIX](-[SEGMENT])*-NUMBER
|
|
29
|
+
* - PREFIX = [A-Z] followed by zero+ [A-Z0-9]
|
|
30
|
+
* - SEGMENT = one+ [A-Z0-9] (alphanumeric, uppercase only)
|
|
31
|
+
* - NUMBER = one+ digits
|
|
32
|
+
*
|
|
33
|
+
* Defined once per A4 invariant; referenced by both the basic validator
|
|
34
|
+
* (line ~125 pre-fix) and the enhanced validator (line ~307 pre-fix).
|
|
35
|
+
*/
|
|
36
|
+
const SPEC_ID_PATTERN = /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*-\d+$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* User-facing error message for bad spec IDs (CAWSFIX-10 A5).
|
|
40
|
+
* Kept as a module constant so the message stays in sync with the pattern.
|
|
41
|
+
*/
|
|
42
|
+
const SPEC_ID_ERROR_MESSAGE =
|
|
43
|
+
'Project ID should be in format: PREFIX-NUMBER or PREFIX-SEGMENT-NUMBER (e.g., FEAT-001, P03-IMPL-01)';
|
|
9
44
|
|
|
10
45
|
/**
|
|
11
46
|
* Get actual budget statistics from git history
|
|
@@ -21,27 +56,31 @@ function getActualBudgetStats(specDir) {
|
|
|
21
56
|
try {
|
|
22
57
|
baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
|
|
23
58
|
cwd,
|
|
24
|
-
encoding: 'utf8'
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
25
61
|
}).trim();
|
|
26
62
|
} catch {
|
|
27
63
|
// No tags found, use initial commit
|
|
28
64
|
baseRef = execSync('git rev-list --max-parents=0 HEAD', {
|
|
29
65
|
cwd,
|
|
30
|
-
encoding: 'utf8'
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
68
|
}).trim();
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
// Count files changed since base ref
|
|
35
72
|
const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
|
|
36
73
|
cwd,
|
|
37
|
-
encoding: 'utf8'
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
76
|
});
|
|
39
77
|
const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
|
|
40
78
|
|
|
41
79
|
// Count lines changed (added + removed)
|
|
42
80
|
const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
|
|
43
81
|
cwd,
|
|
44
|
-
encoding: 'utf8'
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
45
84
|
});
|
|
46
85
|
let lines_changed = 0;
|
|
47
86
|
for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
|
|
@@ -59,6 +98,42 @@ function getActualBudgetStats(specDir) {
|
|
|
59
98
|
}
|
|
60
99
|
}
|
|
61
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Alias the modern `acceptance_criteria` key into `acceptance` so the semantic
|
|
103
|
+
* validator (which historically keys off `acceptance`) accepts both shapes.
|
|
104
|
+
*
|
|
105
|
+
* Precedence (per CAWSFIX-09 A3 invariant):
|
|
106
|
+
* - If `acceptance` is present (legacy shape: {id,given,when,then}), it wins.
|
|
107
|
+
* - Otherwise `acceptance_criteria` (modern shape: {id,description,test_nodeids,status})
|
|
108
|
+
* is copied into `acceptance`.
|
|
109
|
+
*
|
|
110
|
+
* IMPORTANT: this function mutates the spec in place. The existing validator
|
|
111
|
+
* also mutates in place (risk_tier string→number coercion at line ~141; auto-fix
|
|
112
|
+
* writes via `current[pathParts[...]] = fix.value`). Callers of
|
|
113
|
+
* `validateWorkingSpecWithSuggestions({...}, {autoFix:true})` observe those
|
|
114
|
+
* mutations on the object they passed in — see `Multiple Auto-Fixes` tests.
|
|
115
|
+
* Returning a clone here would silently break that contract.
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} spec - Raw spec object (mutated in place)
|
|
118
|
+
* @returns {Object} Same spec reference
|
|
119
|
+
*/
|
|
120
|
+
function aliasAcceptanceCriteria(spec) {
|
|
121
|
+
if (!spec || typeof spec !== 'object') return spec;
|
|
122
|
+
|
|
123
|
+
const hasLegacy = Array.isArray(spec.acceptance) && spec.acceptance.length > 0;
|
|
124
|
+
const hasModern =
|
|
125
|
+
Array.isArray(spec.acceptance_criteria) && spec.acceptance_criteria.length > 0;
|
|
126
|
+
|
|
127
|
+
// Only alias when: legacy is absent AND modern has content.
|
|
128
|
+
// (Legacy wins when both present; empty modern arrays do not satisfy the
|
|
129
|
+
// required-field check — see edge-case tests in acceptance-criteria-alias.test.js.)
|
|
130
|
+
if (!hasLegacy && hasModern) {
|
|
131
|
+
spec.acceptance = spec.acceptance_criteria;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return spec;
|
|
135
|
+
}
|
|
136
|
+
|
|
62
137
|
/**
|
|
63
138
|
* Basic validation of working spec
|
|
64
139
|
* @param {Object} spec - Working spec object
|
|
@@ -67,7 +142,30 @@ function getActualBudgetStats(specDir) {
|
|
|
67
142
|
*/
|
|
68
143
|
const validateWorkingSpec = (spec, _options = {}) => {
|
|
69
144
|
try {
|
|
70
|
-
//
|
|
145
|
+
// CAWSFIX-09: Alias `acceptance_criteria` -> `acceptance` before any
|
|
146
|
+
// semantic checks so specs using the modern shape don't trigger
|
|
147
|
+
// "Missing required field: acceptance" false negatives.
|
|
148
|
+
aliasAcceptanceCriteria(spec);
|
|
149
|
+
|
|
150
|
+
// First pass: AJV schema validation (non-blocking — results collected as warnings)
|
|
151
|
+
let schemaWarnings = [];
|
|
152
|
+
try {
|
|
153
|
+
const schemaPath = getSchemaPath('working-spec.schema.json', process.cwd());
|
|
154
|
+
const validate = createValidator(schemaPath);
|
|
155
|
+
const schemaResult = validate(spec);
|
|
156
|
+
if (!schemaResult.valid) {
|
|
157
|
+
schemaWarnings = schemaResult.errors.map(e => ({
|
|
158
|
+
instancePath: e.path,
|
|
159
|
+
message: e.message,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
} catch (schemaErr) {
|
|
163
|
+
// Schema not available — fall through to semantic validation
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Second pass: semantic checks (authoritative — always runs as fallback)
|
|
167
|
+
|
|
168
|
+
// Check required fields (schema may not be available)
|
|
71
169
|
const requiredFields = [
|
|
72
170
|
'id',
|
|
73
171
|
'title',
|
|
@@ -82,17 +180,6 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
82
180
|
'contracts',
|
|
83
181
|
];
|
|
84
182
|
|
|
85
|
-
// For new policy-based specs, change_budget is not required
|
|
86
|
-
// It's derived from policy.yaml + waivers
|
|
87
|
-
|
|
88
|
-
// Normalize risk_tier: accept "T1"/"T2"/"T3" strings and convert to numeric
|
|
89
|
-
if (spec.risk_tier !== undefined && typeof spec.risk_tier === 'string') {
|
|
90
|
-
const match = spec.risk_tier.match(/^T?(\d)$/i);
|
|
91
|
-
if (match) {
|
|
92
|
-
spec.risk_tier = parseInt(match[1], 10);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
183
|
for (const field of requiredFields) {
|
|
97
184
|
if (!spec[field]) {
|
|
98
185
|
return {
|
|
@@ -107,19 +194,27 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
107
194
|
}
|
|
108
195
|
}
|
|
109
196
|
|
|
110
|
-
// Validate specific field formats
|
|
111
|
-
if (
|
|
197
|
+
// Validate specific field formats (CAWSFIX-10: DRY regex via SPEC_ID_PATTERN)
|
|
198
|
+
if (!SPEC_ID_PATTERN.test(spec.id)) {
|
|
112
199
|
return {
|
|
113
200
|
valid: false,
|
|
114
201
|
errors: [
|
|
115
202
|
{
|
|
116
203
|
instancePath: '/id',
|
|
117
|
-
message:
|
|
204
|
+
message: SPEC_ID_ERROR_MESSAGE,
|
|
118
205
|
},
|
|
119
206
|
],
|
|
120
207
|
};
|
|
121
208
|
}
|
|
122
209
|
|
|
210
|
+
// Normalize risk_tier: accept "T1"/"T2"/"T3" strings and convert to numeric
|
|
211
|
+
if (spec.risk_tier !== undefined && typeof spec.risk_tier === 'string') {
|
|
212
|
+
const match = spec.risk_tier.match(/^T?(\d)$/i);
|
|
213
|
+
if (match) {
|
|
214
|
+
spec.risk_tier = parseInt(match[1], 10);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
123
218
|
// Validate status field if present
|
|
124
219
|
if (spec.status) {
|
|
125
220
|
const { SPEC_STATUSES } = require('../constants/spec-types');
|
|
@@ -203,7 +298,10 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
203
298
|
};
|
|
204
299
|
}
|
|
205
300
|
|
|
206
|
-
return {
|
|
301
|
+
return {
|
|
302
|
+
valid: true,
|
|
303
|
+
schemaWarnings: schemaWarnings.length > 0 ? schemaWarnings : undefined,
|
|
304
|
+
};
|
|
207
305
|
} catch (error) {
|
|
208
306
|
return {
|
|
209
307
|
valid: false,
|
|
@@ -227,7 +325,36 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
227
325
|
const { autoFix = false, checkBudget = false, projectRoot } = options;
|
|
228
326
|
|
|
229
327
|
try {
|
|
230
|
-
//
|
|
328
|
+
// CAWSFIX-09: Alias `acceptance_criteria` -> `acceptance` so the
|
|
329
|
+
// required-field check and the "No acceptance criteria defined" warning
|
|
330
|
+
// recognize the modern shape as valid. Mutates in place to preserve the
|
|
331
|
+
// existing auto-fix contract (callers observe fixes on their object).
|
|
332
|
+
aliasAcceptanceCriteria(spec);
|
|
333
|
+
|
|
334
|
+
let errors = [];
|
|
335
|
+
let warnings = [];
|
|
336
|
+
let fixes = [];
|
|
337
|
+
|
|
338
|
+
// First pass: AJV schema validation (non-blocking — results collected as warnings)
|
|
339
|
+
try {
|
|
340
|
+
const schemaPath = getSchemaPath('working-spec.schema.json', projectRoot || process.cwd());
|
|
341
|
+
const validate = createValidator(schemaPath);
|
|
342
|
+
const schemaResult = validate(spec);
|
|
343
|
+
if (!schemaResult.valid) {
|
|
344
|
+
for (const e of schemaResult.errors) {
|
|
345
|
+
const fieldName = e.path ? e.path.replace(/^\//, '').split('/')[0] : '';
|
|
346
|
+
warnings.push({
|
|
347
|
+
instancePath: e.path,
|
|
348
|
+
message: `Schema: ${e.message}`,
|
|
349
|
+
suggestion: fieldName ? getFieldSuggestion(fieldName, spec) : undefined,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (schemaErr) {
|
|
354
|
+
// Schema not available — non-fatal
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Required fields check (authoritative — always runs regardless of schema)
|
|
231
358
|
const requiredFields = [
|
|
232
359
|
'id',
|
|
233
360
|
'title',
|
|
@@ -242,10 +369,6 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
242
369
|
'contracts',
|
|
243
370
|
];
|
|
244
371
|
|
|
245
|
-
let errors = [];
|
|
246
|
-
let warnings = [];
|
|
247
|
-
let fixes = [];
|
|
248
|
-
|
|
249
372
|
for (const field of requiredFields) {
|
|
250
373
|
if (!spec[field]) {
|
|
251
374
|
errors.push({
|
|
@@ -257,12 +380,14 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
257
380
|
}
|
|
258
381
|
}
|
|
259
382
|
|
|
260
|
-
//
|
|
261
|
-
|
|
383
|
+
// Semantic checks that AJV can't express
|
|
384
|
+
|
|
385
|
+
// Validate specific field formats (CAWSFIX-10: DRY regex via SPEC_ID_PATTERN)
|
|
386
|
+
if (spec.id && !SPEC_ID_PATTERN.test(spec.id)) {
|
|
262
387
|
errors.push({
|
|
263
388
|
instancePath: '/id',
|
|
264
|
-
message:
|
|
265
|
-
suggestion: 'Use format like: PROJ-001, FEAT-002,
|
|
389
|
+
message: SPEC_ID_ERROR_MESSAGE,
|
|
390
|
+
suggestion: 'Use format like: PROJ-001, FEAT-002, P03-IMPL-01, ALG-001A-HARDEN-01',
|
|
266
391
|
canAutoFix: false,
|
|
267
392
|
});
|
|
268
393
|
}
|
|
@@ -575,11 +700,43 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
575
700
|
// Budget enforcement is derived from policy.yaml risk_tier + waivers.
|
|
576
701
|
// No warning emitted — the field is valid and expected.
|
|
577
702
|
|
|
703
|
+
// Validate scope.json against scope.schema.json if it exists
|
|
704
|
+
if (projectRoot) {
|
|
705
|
+
const scopeJsonPath = path.join(projectRoot, '.caws', 'scope.json');
|
|
706
|
+
if (fs.existsSync(scopeJsonPath)) {
|
|
707
|
+
try {
|
|
708
|
+
const schemaPath = getSchemaPath('scope.schema.json', projectRoot);
|
|
709
|
+
const validate = createValidator(schemaPath);
|
|
710
|
+
const scopeData = JSON.parse(fs.readFileSync(scopeJsonPath, 'utf8'));
|
|
711
|
+
const scopeResult = validate(scopeData);
|
|
712
|
+
if (!scopeResult.valid) {
|
|
713
|
+
for (const err of scopeResult.errors) {
|
|
714
|
+
warnings.push({
|
|
715
|
+
instancePath: `/scope.json${err.path}`,
|
|
716
|
+
message: `scope.json schema violation: ${err.message}`,
|
|
717
|
+
suggestion: 'Fix .caws/scope.json to match scope.schema.json',
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch (schemaErr) {
|
|
722
|
+
// Non-fatal — don't block validation on schema issues
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
578
727
|
// Derive and check budget if requested
|
|
728
|
+
//
|
|
729
|
+
// CAWSFIX-07: use `deriveBudgetSync` here. The async `deriveBudget`
|
|
730
|
+
// returns a Promise; this synchronous function previously passed the
|
|
731
|
+
// Promise straight into `checkBudgetCompliance`, which then read
|
|
732
|
+
// `derivedBudget.effective.max_files` on an undefined `.effective` and
|
|
733
|
+
// threw "Cannot read properties of undefined (reading 'max_files')" —
|
|
734
|
+
// surfaced as the "Budget derivation failed" warning on every
|
|
735
|
+
// schema-compliant spec.
|
|
579
736
|
let budgetCheck = null;
|
|
580
737
|
if (checkBudget && projectRoot) {
|
|
581
738
|
try {
|
|
582
|
-
const derivedBudget =
|
|
739
|
+
const derivedBudget = deriveBudgetSync(spec, projectRoot);
|
|
583
740
|
|
|
584
741
|
// Get actual stats from git history
|
|
585
742
|
const actualStats = getActualBudgetStats(projectRoot) || {
|
|
@@ -758,4 +915,7 @@ module.exports = {
|
|
|
758
915
|
canAutoFixField,
|
|
759
916
|
calculateComplianceScore,
|
|
760
917
|
getComplianceGrade,
|
|
918
|
+
// CAWSFIX-10: exported so init.js and tests reference the same regex
|
|
919
|
+
SPEC_ID_PATTERN,
|
|
920
|
+
SPEC_ID_ERROR_MESSAGE,
|
|
761
921
|
};
|
package/dist/waivers-manager.js
CHANGED
|
@@ -195,6 +195,29 @@ class WaiversManager {
|
|
|
195
195
|
return waiver;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Find an active, non-expired waiver that covers a specific gate.
|
|
200
|
+
* Used by the gate evaluation pipeline to skip gates with active waivers.
|
|
201
|
+
* @param {string} gateName - Gate identifier (e.g. 'budget_limit', 'god_object')
|
|
202
|
+
* @returns {Promise<{waiverId: string, reason: string}|null>}
|
|
203
|
+
*/
|
|
204
|
+
async getActiveWaiverForGate(gateName) {
|
|
205
|
+
const activeWaivers = await this.loadActiveWaivers();
|
|
206
|
+
const now = new Date();
|
|
207
|
+
|
|
208
|
+
for (const waiver of activeWaivers) {
|
|
209
|
+
const gates = Array.isArray(waiver.gates) ? waiver.gates : [waiver.gate];
|
|
210
|
+
const expiresAt = new Date(waiver.expires_at || waiver.expiry);
|
|
211
|
+
|
|
212
|
+
if (gates.includes(gateName) || gates.includes('*')) {
|
|
213
|
+
if ((!waiver.status || waiver.status === 'active') && expiresAt > now) {
|
|
214
|
+
return { waiverId: waiver.id, reason: waiver.reason };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
198
221
|
/**
|
|
199
222
|
* Check if waiver applies to specific gates
|
|
200
223
|
*/
|
|
@@ -252,6 +275,90 @@ class WaiversManager {
|
|
|
252
275
|
return activeWaivers;
|
|
253
276
|
}
|
|
254
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Enumerate individual waiver files (WV-XXXX.yaml) on disk and return
|
|
280
|
+
* their parsed contents. These files are the source of truth per the
|
|
281
|
+
* CAWSFIX-04 invariants; active-waivers.yaml is an aggregate index.
|
|
282
|
+
*
|
|
283
|
+
* @returns {Array<{id: string, path: string, data: object}>}
|
|
284
|
+
*/
|
|
285
|
+
enumerateWaiverFiles() {
|
|
286
|
+
const out = [];
|
|
287
|
+
if (!fs.existsSync(this.waiversDir)) return out;
|
|
288
|
+
|
|
289
|
+
const files = fs.readdirSync(this.waiversDir);
|
|
290
|
+
for (const file of files) {
|
|
291
|
+
const match = file.match(/^(WV-\d{4})\.yaml$/);
|
|
292
|
+
if (!match) continue;
|
|
293
|
+
|
|
294
|
+
const filePath = path.join(this.waiversDir, file);
|
|
295
|
+
let data;
|
|
296
|
+
try {
|
|
297
|
+
data = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
298
|
+
} catch (err) {
|
|
299
|
+
// Skip unparseable files; do not swallow — warn the caller.
|
|
300
|
+
console.warn(`Warning: could not parse ${file}: ${err.message}`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (data && typeof data === 'object') {
|
|
304
|
+
out.push({ id: match[1], path: filePath, data });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Identify waivers that are candidates for expiry-based pruning.
|
|
312
|
+
* A waiver is prunable iff `status === 'active'` AND
|
|
313
|
+
* `expires_at < now`. Already-expired or revoked waivers are skipped
|
|
314
|
+
* (their status is correct; pruning wouldn't change anything).
|
|
315
|
+
*
|
|
316
|
+
* @param {Date} [nowOverride] — inject clock for tests
|
|
317
|
+
* @returns {Array<{id: string, path: string, expires_at: string}>}
|
|
318
|
+
*/
|
|
319
|
+
findExpiredWaivers(nowOverride) {
|
|
320
|
+
const now = nowOverride instanceof Date ? nowOverride : new Date();
|
|
321
|
+
const records = this.enumerateWaiverFiles();
|
|
322
|
+
const candidates = [];
|
|
323
|
+
|
|
324
|
+
for (const rec of records) {
|
|
325
|
+
const w = rec.data;
|
|
326
|
+
const status = w.status;
|
|
327
|
+
// Only active waivers are prunable. Waivers with no status field are
|
|
328
|
+
// treated as active (matches existing loadActiveWaivers() assumption).
|
|
329
|
+
if (status && status !== 'active') continue;
|
|
330
|
+
if (!w.expires_at) continue;
|
|
331
|
+
|
|
332
|
+
const expiresAt = new Date(w.expires_at);
|
|
333
|
+
if (!Number.isFinite(expiresAt.getTime())) continue; // malformed date
|
|
334
|
+
if (expiresAt < now) {
|
|
335
|
+
candidates.push({
|
|
336
|
+
id: rec.id,
|
|
337
|
+
path: rec.path,
|
|
338
|
+
expires_at: w.expires_at,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return candidates;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Transition a single waiver file from `status: active` to
|
|
347
|
+
* `status: expired` in place. The file is rewritten with its existing
|
|
348
|
+
* field order where possible; a `status` field is added or replaced.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} filePath
|
|
351
|
+
* @returns {object} the updated waiver object
|
|
352
|
+
*/
|
|
353
|
+
markWaiverExpired(filePath) {
|
|
354
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
355
|
+
const data = yaml.load(raw) || {};
|
|
356
|
+
data.status = 'expired';
|
|
357
|
+
data.expired_at = new Date().toISOString();
|
|
358
|
+
fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8');
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
|
|
255
362
|
/**
|
|
256
363
|
* Revoke a waiver
|
|
257
364
|
*/
|
|
@@ -367,15 +474,46 @@ class WaiversManager {
|
|
|
367
474
|
// Private helper methods
|
|
368
475
|
|
|
369
476
|
async generateWaiverId() {
|
|
370
|
-
|
|
371
|
-
|
|
477
|
+
// Scan all waiver files in the directory (not just active-waivers.yaml)
|
|
478
|
+
// to avoid recycling IDs from expired/revoked waivers
|
|
479
|
+
const usedIds = new Set();
|
|
480
|
+
|
|
481
|
+
// Collect IDs from active-waivers.yaml
|
|
482
|
+
const activeWaivers = await this.loadActiveWaivers();
|
|
483
|
+
for (const w of activeWaivers) {
|
|
484
|
+
if (w.id) usedIds.add(w.id);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Collect IDs from individual waiver files (WV-XXXX.yaml)
|
|
488
|
+
try {
|
|
489
|
+
const files = fs.readdirSync(this.waiversDir);
|
|
490
|
+
for (const file of files) {
|
|
491
|
+
const match = file.match(/^(WV-\d{4})\.yaml$/);
|
|
492
|
+
if (match) usedIds.add(match[1]);
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
// Directory may not exist yet
|
|
496
|
+
}
|
|
372
497
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
498
|
+
// Generate a random 4-digit ID that doesn't collide
|
|
499
|
+
const maxAttempts = 100;
|
|
500
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
501
|
+
const num = Math.floor(Math.random() * 10000);
|
|
502
|
+
const candidate = `WV-${num.toString().padStart(4, '0')}`;
|
|
503
|
+
if (!usedIds.has(candidate)) {
|
|
504
|
+
return candidate;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Fallback: sequential scan if random keeps colliding (>100 attempts)
|
|
509
|
+
for (let n = 1; n <= 9999; n++) {
|
|
510
|
+
const candidate = `WV-${n.toString().padStart(4, '0')}`;
|
|
511
|
+
if (!usedIds.has(candidate)) {
|
|
512
|
+
return candidate;
|
|
513
|
+
}
|
|
376
514
|
}
|
|
377
515
|
|
|
378
|
-
|
|
516
|
+
throw new Error('No available waiver IDs (all 9999 slots used)');
|
|
379
517
|
}
|
|
380
518
|
|
|
381
519
|
validateWaiver(waiver) {
|