@paths.design/caws-cli 9.3.2 → 10.0.1
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 +58 -27
- 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 +27 -15
- package/dist/commands/gates.js +122 -0
- package/dist/commands/init.js +143 -15
- package/dist/commands/iterate.js +77 -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/sidecar.js +71 -0
- package/dist/commands/specs.js +233 -44
- package/dist/commands/status.js +113 -9
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +49 -6
- package/dist/commands/verify-acs.js +35 -78
- package/dist/commands/waivers.js +69 -12
- package/dist/commands/worktree.js +50 -25
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +116 -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 +102 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +130 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +42 -10
- 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 +71 -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 +50 -0
- package/dist/templates/.caws/schemas/waivers.schema.json +30 -24
- package/dist/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +7 -3
- 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/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +34 -32
- 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 +1 -1
- package/dist/templates/.claude/settings.json +26 -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 +43 -8
- package/dist/templates/agents.md +29 -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/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 +42 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +505 -0
- package/dist/validation/spec-validation.js +92 -22
- package/dist/waivers-manager.js +60 -6
- package/dist/worktree/worktree-manager.js +390 -93
- package/package.json +6 -6
- package/templates/.caws/schemas/policy.schema.json +50 -0
- package/templates/.caws/schemas/waivers.schema.json +30 -24
- package/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +7 -3
- 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/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +34 -32
- 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 +1 -1
- package/templates/.claude/settings.json +26 -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 +43 -8
- package/templates/{AGENTS.md → agents.md} +29 -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
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Working-State Layer
|
|
3
|
+
*
|
|
4
|
+
* Runtime companion to specs that tracks what an agent is currently doing.
|
|
5
|
+
* Persists current phase, touched files, gate results, blockers, and
|
|
6
|
+
* next actions to `.caws/state/<spec-id>.json`.
|
|
7
|
+
*
|
|
8
|
+
* All writes are non-fatal — if state cannot be persisted the calling
|
|
9
|
+
* command continues normally.
|
|
10
|
+
*
|
|
11
|
+
* @author @darianrosebrook
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { lifecycle, EVENTS } = require('./lifecycle-events');
|
|
17
|
+
|
|
18
|
+
const STATE_DIR = '.caws/state';
|
|
19
|
+
const STATE_SCHEMA_VERSION = 'caws.state.v1';
|
|
20
|
+
const MAX_HISTORY = 20;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Find the project root by walking up to the nearest .caws/ directory.
|
|
28
|
+
* Falls back to cwd if nothing found.
|
|
29
|
+
* @param {string} [startDir]
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function findRoot(startDir) {
|
|
33
|
+
let dir = startDir || process.cwd();
|
|
34
|
+
while (dir !== path.dirname(dir)) {
|
|
35
|
+
if (fs.existsSync(path.join(dir, '.caws'))) return dir;
|
|
36
|
+
dir = path.dirname(dir);
|
|
37
|
+
}
|
|
38
|
+
return startDir || process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the absolute path for a spec's state file.
|
|
43
|
+
* @param {string} specId
|
|
44
|
+
* @param {string} [projectRoot]
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function getStatePath(specId, projectRoot) {
|
|
48
|
+
const root = projectRoot || findRoot();
|
|
49
|
+
return path.join(root, STATE_DIR, `${specId}.json`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Core CRUD
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build an empty default state for a new spec.
|
|
58
|
+
* @param {string} specId
|
|
59
|
+
* @returns {object}
|
|
60
|
+
*/
|
|
61
|
+
function initializeState(specId) {
|
|
62
|
+
return {
|
|
63
|
+
schema: STATE_SCHEMA_VERSION,
|
|
64
|
+
spec_id: specId,
|
|
65
|
+
updated_at: new Date().toISOString(),
|
|
66
|
+
phase: 'not-started',
|
|
67
|
+
files_touched: [],
|
|
68
|
+
validation: null,
|
|
69
|
+
evaluation: null,
|
|
70
|
+
gates: null,
|
|
71
|
+
acceptance_criteria: null,
|
|
72
|
+
blockers: [],
|
|
73
|
+
next_actions: [],
|
|
74
|
+
history: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Load state from disk. Returns null if file does not exist.
|
|
80
|
+
* @param {string} specId
|
|
81
|
+
* @param {string} [projectRoot]
|
|
82
|
+
* @returns {object|null}
|
|
83
|
+
*/
|
|
84
|
+
function loadState(specId, projectRoot) {
|
|
85
|
+
const filePath = getStatePath(specId, projectRoot);
|
|
86
|
+
if (!fs.existsSync(filePath)) return null;
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Save state atomically (write-then-rename).
|
|
96
|
+
* @param {string} specId
|
|
97
|
+
* @param {object} state
|
|
98
|
+
* @param {string} [projectRoot]
|
|
99
|
+
*/
|
|
100
|
+
function saveState(specId, state, projectRoot) {
|
|
101
|
+
const filePath = getStatePath(specId, projectRoot);
|
|
102
|
+
const dir = path.dirname(filePath);
|
|
103
|
+
if (!fs.existsSync(dir)) {
|
|
104
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
107
|
+
fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2));
|
|
108
|
+
fs.renameSync(tmpPath, filePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Delete state file for a spec.
|
|
113
|
+
* @param {string} specId
|
|
114
|
+
* @param {string} [projectRoot]
|
|
115
|
+
*/
|
|
116
|
+
function deleteState(specId, projectRoot) {
|
|
117
|
+
const filePath = getStatePath(specId, projectRoot);
|
|
118
|
+
if (fs.existsSync(filePath)) {
|
|
119
|
+
fs.unlinkSync(filePath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load → patch → recompute derived fields → save.
|
|
125
|
+
* @param {string} specId
|
|
126
|
+
* @param {object} patch - Partial state to merge
|
|
127
|
+
* @param {object} [options]
|
|
128
|
+
* @param {string} [options.projectRoot]
|
|
129
|
+
* @param {object} [options.spec] - Spec object for derived-field computation
|
|
130
|
+
* @param {string} [options.command] - Command name for history entry
|
|
131
|
+
* @param {string} [options.summary] - Summary for history entry
|
|
132
|
+
* @returns {object} Updated state
|
|
133
|
+
*/
|
|
134
|
+
function updateState(specId, patch, options = {}) {
|
|
135
|
+
const { projectRoot, spec, command, summary } = options;
|
|
136
|
+
let state = loadState(specId, projectRoot) || initializeState(specId);
|
|
137
|
+
|
|
138
|
+
// Merge top-level sections (replace, not deep-merge)
|
|
139
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
140
|
+
if (key === 'files_touched' && Array.isArray(value)) {
|
|
141
|
+
// Merge file lists with dedup
|
|
142
|
+
const merged = new Set([...(state.files_touched || []), ...value]);
|
|
143
|
+
state.files_touched = [...merged];
|
|
144
|
+
} else {
|
|
145
|
+
state[key] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Append history
|
|
150
|
+
if (command) {
|
|
151
|
+
state.history = state.history || [];
|
|
152
|
+
state.history.push({
|
|
153
|
+
timestamp: new Date().toISOString(),
|
|
154
|
+
command,
|
|
155
|
+
summary: summary || '',
|
|
156
|
+
});
|
|
157
|
+
// Cap at MAX_HISTORY
|
|
158
|
+
if (state.history.length > MAX_HISTORY) {
|
|
159
|
+
state.history = state.history.slice(-MAX_HISTORY);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Recompute derived fields
|
|
164
|
+
state.blockers = computeBlockers(state);
|
|
165
|
+
state.next_actions = computeNextActions(state, spec);
|
|
166
|
+
const oldPhase = state.phase;
|
|
167
|
+
state.phase = computePhase(state, spec);
|
|
168
|
+
state.updated_at = new Date().toISOString();
|
|
169
|
+
|
|
170
|
+
// Emit phase transition event
|
|
171
|
+
if (oldPhase && oldPhase !== state.phase) {
|
|
172
|
+
try {
|
|
173
|
+
lifecycle.emit(EVENTS.PHASE_TRANSITION, {
|
|
174
|
+
specId, oldPhase, newPhase: state.phase,
|
|
175
|
+
timestamp: state.updated_at,
|
|
176
|
+
});
|
|
177
|
+
} catch { /* non-fatal */ }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
saveState(specId, state, projectRoot);
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Record helpers — called by individual commands
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Record validation result.
|
|
190
|
+
* @param {string} specId
|
|
191
|
+
* @param {object} result
|
|
192
|
+
* @param {boolean} result.passed
|
|
193
|
+
* @param {number} [result.compliance_score]
|
|
194
|
+
* @param {string} [result.grade]
|
|
195
|
+
* @param {number} [result.error_count]
|
|
196
|
+
* @param {number} [result.warning_count]
|
|
197
|
+
* @param {string} [projectRoot]
|
|
198
|
+
*/
|
|
199
|
+
function recordValidation(specId, result, projectRoot) {
|
|
200
|
+
const validation = {
|
|
201
|
+
last_run: new Date().toISOString(),
|
|
202
|
+
passed: result.passed,
|
|
203
|
+
compliance_score: result.compliance_score ?? null,
|
|
204
|
+
grade: result.grade ?? null,
|
|
205
|
+
error_count: result.error_count ?? 0,
|
|
206
|
+
warning_count: result.warning_count ?? 0,
|
|
207
|
+
};
|
|
208
|
+
const summaryText = result.passed
|
|
209
|
+
? `Passed (Grade ${validation.grade || '?'})`
|
|
210
|
+
: `Failed — ${validation.error_count} error(s)`;
|
|
211
|
+
updateState(specId, { validation }, {
|
|
212
|
+
projectRoot,
|
|
213
|
+
command: 'validate',
|
|
214
|
+
summary: summaryText,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Record evaluation result.
|
|
220
|
+
* @param {string} specId
|
|
221
|
+
* @param {object} result
|
|
222
|
+
* @param {number} result.score
|
|
223
|
+
* @param {number} result.max_score
|
|
224
|
+
* @param {number} result.percentage
|
|
225
|
+
* @param {string} result.grade
|
|
226
|
+
* @param {number} result.checks_passed
|
|
227
|
+
* @param {number} result.checks_total
|
|
228
|
+
* @param {string} [projectRoot]
|
|
229
|
+
*/
|
|
230
|
+
function recordEvaluation(specId, result, projectRoot) {
|
|
231
|
+
const evaluation = {
|
|
232
|
+
last_run: new Date().toISOString(),
|
|
233
|
+
score: result.score,
|
|
234
|
+
max_score: result.max_score,
|
|
235
|
+
percentage: result.percentage,
|
|
236
|
+
grade: result.grade,
|
|
237
|
+
checks_passed: result.checks_passed,
|
|
238
|
+
checks_total: result.checks_total,
|
|
239
|
+
};
|
|
240
|
+
updateState(specId, { evaluation }, {
|
|
241
|
+
projectRoot,
|
|
242
|
+
command: 'evaluate',
|
|
243
|
+
summary: `${result.score}/${result.max_score} (${result.percentage}%) Grade ${result.grade}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Record gate evaluation results.
|
|
249
|
+
* @param {string} specId
|
|
250
|
+
* @param {object} report - Report from evaluateGates()
|
|
251
|
+
* @param {boolean} report.passed
|
|
252
|
+
* @param {object} report.summary
|
|
253
|
+
* @param {object[]} report.gates - Individual gate results
|
|
254
|
+
* @param {string} [context] - Execution context (cli, commit, edit)
|
|
255
|
+
* @param {string} [projectRoot]
|
|
256
|
+
*/
|
|
257
|
+
function recordGates(specId, report, context, projectRoot) {
|
|
258
|
+
const gates = {
|
|
259
|
+
last_run: new Date().toISOString(),
|
|
260
|
+
context: context || 'cli',
|
|
261
|
+
passed: report.passed,
|
|
262
|
+
summary: report.summary,
|
|
263
|
+
results: (report.gates || []).map(g => ({
|
|
264
|
+
name: g.name,
|
|
265
|
+
status: g.status,
|
|
266
|
+
mode: g.mode,
|
|
267
|
+
})),
|
|
268
|
+
};
|
|
269
|
+
const { blocked, warned, passed } = report.summary || {};
|
|
270
|
+
const summaryText = `${passed || 0} passed, ${blocked || 0} blocked, ${warned || 0} warned`;
|
|
271
|
+
updateState(specId, { gates }, {
|
|
272
|
+
projectRoot,
|
|
273
|
+
command: 'gates',
|
|
274
|
+
summary: summaryText,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Record acceptance-criteria verification results.
|
|
280
|
+
* @param {string} specId
|
|
281
|
+
* @param {object} result
|
|
282
|
+
* @param {number} result.total
|
|
283
|
+
* @param {number} result.pass
|
|
284
|
+
* @param {number} result.fail
|
|
285
|
+
* @param {number} result.unchecked
|
|
286
|
+
* @param {object[]} [result.results] - Per-AC results
|
|
287
|
+
* @param {string} [projectRoot]
|
|
288
|
+
*/
|
|
289
|
+
function recordACVerification(specId, result, projectRoot) {
|
|
290
|
+
const acceptance_criteria = {
|
|
291
|
+
last_run: new Date().toISOString(),
|
|
292
|
+
total: result.total,
|
|
293
|
+
pass: result.pass,
|
|
294
|
+
fail: result.fail,
|
|
295
|
+
unchecked: result.unchecked,
|
|
296
|
+
results: (result.results || []).map(r => ({
|
|
297
|
+
id: r.id,
|
|
298
|
+
status: r.status,
|
|
299
|
+
})),
|
|
300
|
+
};
|
|
301
|
+
const summaryText = `${result.pass}/${result.total} pass, ${result.fail} fail, ${result.unchecked} unchecked`;
|
|
302
|
+
updateState(specId, { acceptance_criteria }, {
|
|
303
|
+
projectRoot,
|
|
304
|
+
command: 'verify-acs',
|
|
305
|
+
summary: summaryText,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Merge touched files into state (additive, deduped).
|
|
311
|
+
* @param {string} specId
|
|
312
|
+
* @param {string[]} files
|
|
313
|
+
* @param {string} [projectRoot]
|
|
314
|
+
*/
|
|
315
|
+
function mergeFilesTouched(specId, files, projectRoot) {
|
|
316
|
+
if (!files || files.length === 0) return;
|
|
317
|
+
updateState(specId, { files_touched: files }, {
|
|
318
|
+
projectRoot,
|
|
319
|
+
command: 'session',
|
|
320
|
+
summary: `+${files.length} file(s) touched`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Derived-field computation
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Derive the current workflow phase from state evidence.
|
|
330
|
+
* @param {object} state
|
|
331
|
+
* @param {object} [spec] - Spec object (for AC count)
|
|
332
|
+
* @returns {string}
|
|
333
|
+
*/
|
|
334
|
+
function computePhase(state, _spec) {
|
|
335
|
+
const v = state.validation;
|
|
336
|
+
const e = state.evaluation;
|
|
337
|
+
const g = state.gates;
|
|
338
|
+
const ac = state.acceptance_criteria;
|
|
339
|
+
|
|
340
|
+
// Nothing has run yet
|
|
341
|
+
if (!v && !e && !g && !ac) return 'not-started';
|
|
342
|
+
|
|
343
|
+
// Validation failed or evaluation below 70% → still authoring the spec
|
|
344
|
+
if (v && !v.passed) return 'spec-authoring';
|
|
345
|
+
if (e && e.percentage < 70) return 'spec-authoring';
|
|
346
|
+
|
|
347
|
+
// All ACs pass, all gates pass, evaluation >= 90% → complete
|
|
348
|
+
if (ac && ac.total > 0 && ac.fail === 0 && ac.unchecked === 0
|
|
349
|
+
&& g && g.passed
|
|
350
|
+
&& e && e.percentage >= 90) {
|
|
351
|
+
return 'complete';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// All ACs pass, gates have been run → verification phase
|
|
355
|
+
if (ac && ac.total > 0 && ac.fail === 0 && ac.unchecked === 0 && g) {
|
|
356
|
+
return 'verification';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Otherwise: implementation
|
|
360
|
+
return 'implementation';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Extract active blockers from state.
|
|
365
|
+
* @param {object} state
|
|
366
|
+
* @returns {object[]}
|
|
367
|
+
*/
|
|
368
|
+
function computeBlockers(state) {
|
|
369
|
+
const blockers = [];
|
|
370
|
+
const now = new Date().toISOString();
|
|
371
|
+
|
|
372
|
+
// Validation failure
|
|
373
|
+
if (state.validation && !state.validation.passed) {
|
|
374
|
+
blockers.push({
|
|
375
|
+
type: 'validation_failure',
|
|
376
|
+
message: `Validation failed with ${state.validation.error_count} error(s)`,
|
|
377
|
+
since: state.validation.last_run || now,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Gate failures (block-mode only)
|
|
382
|
+
if (state.gates && state.gates.results) {
|
|
383
|
+
for (const g of state.gates.results) {
|
|
384
|
+
if (g.status === 'fail' && g.mode === 'block') {
|
|
385
|
+
blockers.push({
|
|
386
|
+
type: 'gate_failure',
|
|
387
|
+
gate: g.name,
|
|
388
|
+
message: `Gate "${g.name}" is blocking`,
|
|
389
|
+
since: state.gates.last_run || now,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// AC failures
|
|
396
|
+
if (state.acceptance_criteria && state.acceptance_criteria.fail > 0) {
|
|
397
|
+
const failingIds = (state.acceptance_criteria.results || [])
|
|
398
|
+
.filter(r => r.status === 'FAIL')
|
|
399
|
+
.map(r => r.id);
|
|
400
|
+
blockers.push({
|
|
401
|
+
type: 'ac_failure',
|
|
402
|
+
message: `${state.acceptance_criteria.fail} acceptance criteria failing${failingIds.length ? ': ' + failingIds.join(', ') : ''}`,
|
|
403
|
+
since: state.acceptance_criteria.last_run || new Date().toISOString(),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return blockers;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Compute ordered next actions based on current state.
|
|
412
|
+
* @param {object} state
|
|
413
|
+
* @param {object} [spec]
|
|
414
|
+
* @returns {string[]}
|
|
415
|
+
*/
|
|
416
|
+
function computeNextActions(state, _spec) {
|
|
417
|
+
const actions = [];
|
|
418
|
+
|
|
419
|
+
// Validation failed → fix first
|
|
420
|
+
if (state.validation && !state.validation.passed) {
|
|
421
|
+
actions.push('Fix validation errors, then run: caws validate');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Gate blockers
|
|
425
|
+
if (state.gates && state.gates.results) {
|
|
426
|
+
for (const g of state.gates.results) {
|
|
427
|
+
if (g.status === 'fail' && g.mode === 'block') {
|
|
428
|
+
actions.push(`Fix gate violation: ${g.name}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Failing ACs
|
|
434
|
+
if (state.acceptance_criteria) {
|
|
435
|
+
const failing = (state.acceptance_criteria.results || [])
|
|
436
|
+
.filter(r => r.status === 'FAIL')
|
|
437
|
+
.map(r => r.id);
|
|
438
|
+
if (failing.length > 0) {
|
|
439
|
+
actions.push(`Fix failing acceptance criteria: ${failing.join(', ')}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const unchecked = state.acceptance_criteria.unchecked || 0;
|
|
443
|
+
if (unchecked > 0) {
|
|
444
|
+
actions.push(`Add tests for ${unchecked} unchecked acceptance criteria`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Low evaluation
|
|
449
|
+
if (state.evaluation && state.evaluation.percentage < 80) {
|
|
450
|
+
actions.push(`Improve spec quality (currently ${state.evaluation.percentage}%), run: caws evaluate`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// No validation yet
|
|
454
|
+
if (!state.validation) {
|
|
455
|
+
actions.push('Run: caws validate');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// No evaluation yet
|
|
459
|
+
if (!state.evaluation) {
|
|
460
|
+
actions.push('Run: caws evaluate');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// No AC verification yet
|
|
464
|
+
if (!state.acceptance_criteria) {
|
|
465
|
+
actions.push('Run: caws verify-acs');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Everything green
|
|
469
|
+
if (actions.length === 0) {
|
|
470
|
+
actions.push('All checks passing. Ready for merge. Run: caws verify-acs --run for final verification.');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return actions;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Exports
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
module.exports = {
|
|
481
|
+
// Core
|
|
482
|
+
loadState,
|
|
483
|
+
saveState,
|
|
484
|
+
deleteState,
|
|
485
|
+
updateState,
|
|
486
|
+
initializeState,
|
|
487
|
+
getStatePath,
|
|
488
|
+
|
|
489
|
+
// Recorders
|
|
490
|
+
recordValidation,
|
|
491
|
+
recordEvaluation,
|
|
492
|
+
recordGates,
|
|
493
|
+
recordACVerification,
|
|
494
|
+
mergeFilesTouched,
|
|
495
|
+
|
|
496
|
+
// Derived fields
|
|
497
|
+
computePhase,
|
|
498
|
+
computeBlockers,
|
|
499
|
+
computeNextActions,
|
|
500
|
+
|
|
501
|
+
// Constants
|
|
502
|
+
STATE_DIR,
|
|
503
|
+
STATE_SCHEMA_VERSION,
|
|
504
|
+
MAX_HISTORY,
|
|
505
|
+
};
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* @author @darianrosebrook
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
7
9
|
const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
|
|
8
10
|
const { execSync } = require('child_process');
|
|
11
|
+
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Get actual budget statistics from git history
|
|
@@ -21,27 +24,31 @@ function getActualBudgetStats(specDir) {
|
|
|
21
24
|
try {
|
|
22
25
|
baseRef = execSync('git describe --tags --abbrev=0 2>/dev/null', {
|
|
23
26
|
cwd,
|
|
24
|
-
encoding: 'utf8'
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
25
29
|
}).trim();
|
|
26
30
|
} catch {
|
|
27
31
|
// No tags found, use initial commit
|
|
28
32
|
baseRef = execSync('git rev-list --max-parents=0 HEAD', {
|
|
29
33
|
cwd,
|
|
30
|
-
encoding: 'utf8'
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
36
|
}).trim();
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
// Count files changed since base ref
|
|
35
40
|
const filesOutput = execSync(`git diff --name-only ${baseRef}..HEAD`, {
|
|
36
41
|
cwd,
|
|
37
|
-
encoding: 'utf8'
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
44
|
});
|
|
39
45
|
const files_changed = filesOutput.trim().split('\n').filter(Boolean).length;
|
|
40
46
|
|
|
41
47
|
// Count lines changed (added + removed)
|
|
42
48
|
const numstatOutput = execSync(`git diff --numstat ${baseRef}..HEAD`, {
|
|
43
49
|
cwd,
|
|
44
|
-
encoding: 'utf8'
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
45
52
|
});
|
|
46
53
|
let lines_changed = 0;
|
|
47
54
|
for (const line of numstatOutput.trim().split('\n').filter(Boolean)) {
|
|
@@ -67,7 +74,25 @@ function getActualBudgetStats(specDir) {
|
|
|
67
74
|
*/
|
|
68
75
|
const validateWorkingSpec = (spec, _options = {}) => {
|
|
69
76
|
try {
|
|
70
|
-
//
|
|
77
|
+
// First pass: AJV schema validation (non-blocking — results collected as warnings)
|
|
78
|
+
let schemaWarnings = [];
|
|
79
|
+
try {
|
|
80
|
+
const schemaPath = getSchemaPath('working-spec.schema.json', process.cwd());
|
|
81
|
+
const validate = createValidator(schemaPath);
|
|
82
|
+
const schemaResult = validate(spec);
|
|
83
|
+
if (!schemaResult.valid) {
|
|
84
|
+
schemaWarnings = schemaResult.errors.map(e => ({
|
|
85
|
+
instancePath: e.path,
|
|
86
|
+
message: e.message,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
} catch (schemaErr) {
|
|
90
|
+
// Schema not available — fall through to semantic validation
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Second pass: semantic checks (authoritative — always runs as fallback)
|
|
94
|
+
|
|
95
|
+
// Check required fields (schema may not be available)
|
|
71
96
|
const requiredFields = [
|
|
72
97
|
'id',
|
|
73
98
|
'title',
|
|
@@ -82,17 +107,6 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
82
107
|
'contracts',
|
|
83
108
|
];
|
|
84
109
|
|
|
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
110
|
for (const field of requiredFields) {
|
|
97
111
|
if (!spec[field]) {
|
|
98
112
|
return {
|
|
@@ -120,6 +134,14 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
120
134
|
};
|
|
121
135
|
}
|
|
122
136
|
|
|
137
|
+
// Normalize risk_tier: accept "T1"/"T2"/"T3" strings and convert to numeric
|
|
138
|
+
if (spec.risk_tier !== undefined && typeof spec.risk_tier === 'string') {
|
|
139
|
+
const match = spec.risk_tier.match(/^T?(\d)$/i);
|
|
140
|
+
if (match) {
|
|
141
|
+
spec.risk_tier = parseInt(match[1], 10);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
// Validate status field if present
|
|
124
146
|
if (spec.status) {
|
|
125
147
|
const { SPEC_STATUSES } = require('../constants/spec-types');
|
|
@@ -203,7 +225,10 @@ const validateWorkingSpec = (spec, _options = {}) => {
|
|
|
203
225
|
};
|
|
204
226
|
}
|
|
205
227
|
|
|
206
|
-
return {
|
|
228
|
+
return {
|
|
229
|
+
valid: true,
|
|
230
|
+
schemaWarnings: schemaWarnings.length > 0 ? schemaWarnings : undefined,
|
|
231
|
+
};
|
|
207
232
|
} catch (error) {
|
|
208
233
|
return {
|
|
209
234
|
valid: false,
|
|
@@ -227,7 +252,30 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
227
252
|
const { autoFix = false, checkBudget = false, projectRoot } = options;
|
|
228
253
|
|
|
229
254
|
try {
|
|
230
|
-
|
|
255
|
+
let errors = [];
|
|
256
|
+
let warnings = [];
|
|
257
|
+
let fixes = [];
|
|
258
|
+
|
|
259
|
+
// First pass: AJV schema validation (non-blocking — results collected as warnings)
|
|
260
|
+
try {
|
|
261
|
+
const schemaPath = getSchemaPath('working-spec.schema.json', projectRoot || process.cwd());
|
|
262
|
+
const validate = createValidator(schemaPath);
|
|
263
|
+
const schemaResult = validate(spec);
|
|
264
|
+
if (!schemaResult.valid) {
|
|
265
|
+
for (const e of schemaResult.errors) {
|
|
266
|
+
const fieldName = e.path ? e.path.replace(/^\//, '').split('/')[0] : '';
|
|
267
|
+
warnings.push({
|
|
268
|
+
instancePath: e.path,
|
|
269
|
+
message: `Schema: ${e.message}`,
|
|
270
|
+
suggestion: fieldName ? getFieldSuggestion(fieldName, spec) : undefined,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch (schemaErr) {
|
|
275
|
+
// Schema not available — non-fatal
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Required fields check (authoritative — always runs regardless of schema)
|
|
231
279
|
const requiredFields = [
|
|
232
280
|
'id',
|
|
233
281
|
'title',
|
|
@@ -242,10 +290,6 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
242
290
|
'contracts',
|
|
243
291
|
];
|
|
244
292
|
|
|
245
|
-
let errors = [];
|
|
246
|
-
let warnings = [];
|
|
247
|
-
let fixes = [];
|
|
248
|
-
|
|
249
293
|
for (const field of requiredFields) {
|
|
250
294
|
if (!spec[field]) {
|
|
251
295
|
errors.push({
|
|
@@ -257,6 +301,8 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
257
301
|
}
|
|
258
302
|
}
|
|
259
303
|
|
|
304
|
+
// Semantic checks that AJV can't express
|
|
305
|
+
|
|
260
306
|
// Validate specific field formats
|
|
261
307
|
if (spec.id && !/^[A-Z]+-\d+$/.test(spec.id)) {
|
|
262
308
|
errors.push({
|
|
@@ -575,6 +621,30 @@ function validateWorkingSpecWithSuggestions(spec, options = {}) {
|
|
|
575
621
|
// Budget enforcement is derived from policy.yaml risk_tier + waivers.
|
|
576
622
|
// No warning emitted — the field is valid and expected.
|
|
577
623
|
|
|
624
|
+
// Validate scope.json against scope.schema.json if it exists
|
|
625
|
+
if (projectRoot) {
|
|
626
|
+
const scopeJsonPath = path.join(projectRoot, '.caws', 'scope.json');
|
|
627
|
+
if (fs.existsSync(scopeJsonPath)) {
|
|
628
|
+
try {
|
|
629
|
+
const schemaPath = getSchemaPath('scope.schema.json', projectRoot);
|
|
630
|
+
const validate = createValidator(schemaPath);
|
|
631
|
+
const scopeData = JSON.parse(fs.readFileSync(scopeJsonPath, 'utf8'));
|
|
632
|
+
const scopeResult = validate(scopeData);
|
|
633
|
+
if (!scopeResult.valid) {
|
|
634
|
+
for (const err of scopeResult.errors) {
|
|
635
|
+
warnings.push({
|
|
636
|
+
instancePath: `/scope.json${err.path}`,
|
|
637
|
+
message: `scope.json schema violation: ${err.message}`,
|
|
638
|
+
suggestion: 'Fix .caws/scope.json to match scope.schema.json',
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
} catch (schemaErr) {
|
|
643
|
+
// Non-fatal — don't block validation on schema issues
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
578
648
|
// Derive and check budget if requested
|
|
579
649
|
let budgetCheck = null;
|
|
580
650
|
if (checkBudget && projectRoot) {
|