@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.
Files changed (286) hide show
  1. package/README.md +71 -32
  2. package/dist/budget-derivation.js +221 -74
  3. package/dist/commands/archive.js +67 -28
  4. package/dist/commands/burnup.js +20 -11
  5. package/dist/commands/diagnose.js +34 -22
  6. package/dist/commands/evaluate.js +41 -15
  7. package/dist/commands/gates.js +149 -0
  8. package/dist/commands/init.js +150 -19
  9. package/dist/commands/iterate.js +81 -4
  10. package/dist/commands/parallel.js +4 -0
  11. package/dist/commands/plan.js +9 -19
  12. package/dist/commands/provenance.js +53 -17
  13. package/dist/commands/quality-monitor.js +64 -45
  14. package/dist/commands/scope.js +264 -0
  15. package/dist/commands/sidecar.js +74 -0
  16. package/dist/commands/specs.js +381 -45
  17. package/dist/commands/status.js +117 -9
  18. package/dist/commands/templates.js +0 -8
  19. package/dist/commands/tutorial.js +10 -9
  20. package/dist/commands/validate.js +70 -6
  21. package/dist/commands/verify-acs.js +48 -76
  22. package/dist/commands/waivers.js +212 -13
  23. package/dist/commands/worktree.js +131 -26
  24. package/dist/error-handler.js +2 -13
  25. package/dist/gates/budget-limit.js +121 -0
  26. package/dist/gates/feedback.js +260 -0
  27. package/dist/gates/format.js +179 -0
  28. package/dist/gates/god-object.js +117 -0
  29. package/dist/gates/pipeline.js +167 -0
  30. package/dist/gates/scope-boundary.js +93 -0
  31. package/dist/gates/spec-completeness.js +109 -0
  32. package/dist/gates/todo-detection.js +205 -0
  33. package/dist/index.js +157 -151
  34. package/dist/parallel/parallel-manager.js +3 -3
  35. package/dist/policy/PolicyManager.js +51 -17
  36. package/dist/scaffold/claude-hooks.js +24 -1
  37. package/dist/scaffold/git-hooks.js +45 -102
  38. package/dist/scaffold/index.js +4 -3
  39. package/dist/session/session-manager.js +105 -14
  40. package/dist/sidecars/index.js +33 -0
  41. package/dist/sidecars/listeners.js +40 -0
  42. package/dist/sidecars/provenance-summary.js +238 -0
  43. package/dist/sidecars/quality-gaps.js +258 -0
  44. package/dist/sidecars/schema.js +149 -0
  45. package/dist/sidecars/spec-drift.js +151 -0
  46. package/dist/sidecars/waiver-draft.js +176 -0
  47. package/dist/templates/.caws/schemas/policy.schema.json +112 -0
  48. package/dist/templates/.caws/schemas/scope.schema.json +3 -3
  49. package/dist/templates/.caws/schemas/waivers.schema.json +96 -20
  50. package/dist/templates/.caws/schemas/working-spec.schema.json +264 -57
  51. package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
  52. package/dist/templates/.caws/templates/working-spec.template.yml +10 -4
  53. package/dist/templates/.caws/tools/scope-guard.js +66 -15
  54. package/dist/templates/.claude/README.md +1 -1
  55. package/dist/templates/.claude/hooks/audit.sh +0 -0
  56. package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
  57. package/dist/templates/.claude/hooks/classify_command.py +592 -0
  58. package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  59. package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
  60. package/dist/templates/.claude/hooks/quality-check.sh +23 -10
  61. package/dist/templates/.claude/hooks/scope-guard.sh +136 -55
  62. package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
  63. package/dist/templates/.claude/hooks/session-log.sh +76 -3
  64. package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  65. package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
  66. package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  67. package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
  68. package/dist/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  69. package/dist/templates/.claude/settings.json +31 -0
  70. package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  71. package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  72. package/dist/templates/.cursor/hooks/session-log.sh +924 -0
  73. package/dist/templates/.cursor/hooks.json +25 -0
  74. package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  75. package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  76. package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  77. package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  78. package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  79. package/dist/templates/.github/copilot-instructions.md +5 -5
  80. package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  81. package/dist/templates/.junie/guidelines.md +2 -2
  82. package/dist/templates/.vscode/settings.json +3 -1
  83. package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  84. package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  85. package/dist/templates/CLAUDE.md +77 -8
  86. package/dist/templates/agents.md +50 -9
  87. package/dist/templates/docs/README.md +8 -7
  88. package/dist/templates/scripts/new_feature.sh +80 -0
  89. package/dist/test-analysis.js +43 -30
  90. package/dist/tool-loader.js +1 -1
  91. package/dist/utils/agent-session.js +202 -0
  92. package/dist/utils/detection.js +8 -2
  93. package/dist/utils/event-log.js +584 -0
  94. package/dist/utils/event-renderer.js +521 -0
  95. package/dist/utils/finalization.js +7 -6
  96. package/dist/utils/gitignore-updater.js +3 -0
  97. package/dist/utils/lifecycle-events.js +94 -0
  98. package/dist/utils/quality-gates-utils.js +29 -44
  99. package/dist/utils/schema-validator.js +50 -0
  100. package/dist/utils/spec-resolver.js +93 -21
  101. package/dist/utils/working-state.js +530 -0
  102. package/dist/validation/spec-validation.js +191 -31
  103. package/dist/waivers-manager.js +144 -6
  104. package/dist/worktree/worktree-manager.js +598 -95
  105. package/package.json +9 -8
  106. package/templates/.caws/schemas/policy.schema.json +112 -0
  107. package/templates/.caws/schemas/scope.schema.json +3 -3
  108. package/templates/.caws/schemas/waivers.schema.json +96 -20
  109. package/templates/.caws/schemas/working-spec.schema.json +264 -57
  110. package/templates/.caws/schemas/worktrees.schema.json +3 -1
  111. package/templates/.caws/templates/working-spec.template.yml +10 -4
  112. package/templates/.caws/tools/scope-guard.js +66 -15
  113. package/templates/.claude/README.md +1 -1
  114. package/templates/.claude/hooks/block-dangerous.sh +52 -11
  115. package/templates/.claude/hooks/classify_command.py +592 -0
  116. package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
  117. package/templates/.claude/hooks/protected-paths.sh +39 -0
  118. package/templates/.claude/hooks/quality-check.sh +23 -10
  119. package/templates/.claude/hooks/scope-guard.sh +136 -55
  120. package/templates/.claude/hooks/session-caws-status.sh +2 -2
  121. package/templates/.claude/hooks/session-log.sh +76 -3
  122. package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
  123. package/templates/.claude/hooks/test_classify_command.py +370 -0
  124. package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
  125. package/templates/.claude/hooks/worktree-guard.sh +2 -2
  126. package/templates/.claude/hooks/worktree-write-guard.sh +97 -4
  127. package/templates/.claude/settings.json +31 -0
  128. package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
  129. package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
  130. package/templates/.cursor/hooks/session-log.sh +924 -0
  131. package/templates/.cursor/hooks.json +25 -0
  132. package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
  133. package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
  134. package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
  135. package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
  136. package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
  137. package/templates/.github/copilot-instructions.md +5 -5
  138. package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
  139. package/templates/.junie/guidelines.md +2 -2
  140. package/templates/.vscode/settings.json +3 -1
  141. package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
  142. package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
  143. package/templates/CLAUDE.md +77 -8
  144. package/templates/{AGENTS.md → agents.md} +50 -9
  145. package/templates/docs/README.md +8 -7
  146. package/templates/scripts/new_feature.sh +80 -0
  147. package/dist/budget-derivation.d.ts +0 -74
  148. package/dist/budget-derivation.d.ts.map +0 -1
  149. package/dist/cicd-optimizer.d.ts +0 -142
  150. package/dist/cicd-optimizer.d.ts.map +0 -1
  151. package/dist/commands/archive.d.ts +0 -51
  152. package/dist/commands/archive.d.ts.map +0 -1
  153. package/dist/commands/burnup.d.ts +0 -6
  154. package/dist/commands/burnup.d.ts.map +0 -1
  155. package/dist/commands/diagnose.d.ts +0 -52
  156. package/dist/commands/diagnose.d.ts.map +0 -1
  157. package/dist/commands/evaluate.d.ts +0 -8
  158. package/dist/commands/evaluate.d.ts.map +0 -1
  159. package/dist/commands/init.d.ts +0 -5
  160. package/dist/commands/init.d.ts.map +0 -1
  161. package/dist/commands/iterate.d.ts +0 -8
  162. package/dist/commands/iterate.d.ts.map +0 -1
  163. package/dist/commands/mode.d.ts +0 -25
  164. package/dist/commands/mode.d.ts.map +0 -1
  165. package/dist/commands/parallel.d.ts +0 -7
  166. package/dist/commands/parallel.d.ts.map +0 -1
  167. package/dist/commands/plan.d.ts +0 -49
  168. package/dist/commands/plan.d.ts.map +0 -1
  169. package/dist/commands/provenance.d.ts +0 -32
  170. package/dist/commands/provenance.d.ts.map +0 -1
  171. package/dist/commands/quality-gates.d.ts +0 -6
  172. package/dist/commands/quality-gates.d.ts.map +0 -1
  173. package/dist/commands/quality-gates.js +0 -444
  174. package/dist/commands/quality-monitor.d.ts +0 -17
  175. package/dist/commands/quality-monitor.d.ts.map +0 -1
  176. package/dist/commands/session.d.ts +0 -7
  177. package/dist/commands/session.d.ts.map +0 -1
  178. package/dist/commands/specs.d.ts +0 -77
  179. package/dist/commands/specs.d.ts.map +0 -1
  180. package/dist/commands/status.d.ts +0 -44
  181. package/dist/commands/status.d.ts.map +0 -1
  182. package/dist/commands/templates.d.ts +0 -74
  183. package/dist/commands/templates.d.ts.map +0 -1
  184. package/dist/commands/tool.d.ts +0 -13
  185. package/dist/commands/tool.d.ts.map +0 -1
  186. package/dist/commands/troubleshoot.d.ts +0 -8
  187. package/dist/commands/troubleshoot.d.ts.map +0 -1
  188. package/dist/commands/troubleshoot.js +0 -104
  189. package/dist/commands/tutorial.d.ts +0 -55
  190. package/dist/commands/tutorial.d.ts.map +0 -1
  191. package/dist/commands/validate.d.ts +0 -15
  192. package/dist/commands/validate.d.ts.map +0 -1
  193. package/dist/commands/waivers.d.ts +0 -8
  194. package/dist/commands/waivers.d.ts.map +0 -1
  195. package/dist/commands/workflow.d.ts +0 -85
  196. package/dist/commands/workflow.d.ts.map +0 -1
  197. package/dist/commands/worktree.d.ts +0 -7
  198. package/dist/commands/worktree.d.ts.map +0 -1
  199. package/dist/config/index.d.ts +0 -29
  200. package/dist/config/index.d.ts.map +0 -1
  201. package/dist/config/lite-scope.d.ts +0 -33
  202. package/dist/config/lite-scope.d.ts.map +0 -1
  203. package/dist/config/modes.d.ts +0 -264
  204. package/dist/config/modes.d.ts.map +0 -1
  205. package/dist/constants/spec-types.d.ts +0 -93
  206. package/dist/constants/spec-types.d.ts.map +0 -1
  207. package/dist/error-handler.d.ts +0 -151
  208. package/dist/error-handler.d.ts.map +0 -1
  209. package/dist/generators/jest-config-generator.d.ts +0 -32
  210. package/dist/generators/jest-config-generator.d.ts.map +0 -1
  211. package/dist/generators/jest-config.d.ts +0 -32
  212. package/dist/generators/jest-config.d.ts.map +0 -1
  213. package/dist/generators/jest-config.js +0 -242
  214. package/dist/generators/working-spec.d.ts +0 -13
  215. package/dist/generators/working-spec.d.ts.map +0 -1
  216. package/dist/index-new.d.ts +0 -5
  217. package/dist/index-new.d.ts.map +0 -1
  218. package/dist/index-new.js +0 -317
  219. package/dist/index.d.ts +0 -5
  220. package/dist/index.d.ts.map +0 -1
  221. package/dist/index.js.backup +0 -4711
  222. package/dist/minimal-cli.d.ts +0 -3
  223. package/dist/minimal-cli.d.ts.map +0 -1
  224. package/dist/parallel/parallel-manager.d.ts +0 -67
  225. package/dist/parallel/parallel-manager.d.ts.map +0 -1
  226. package/dist/policy/PolicyManager.d.ts +0 -104
  227. package/dist/policy/PolicyManager.d.ts.map +0 -1
  228. package/dist/scaffold/claude-hooks.d.ts +0 -28
  229. package/dist/scaffold/claude-hooks.d.ts.map +0 -1
  230. package/dist/scaffold/cursor-hooks.d.ts +0 -7
  231. package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
  232. package/dist/scaffold/git-hooks.d.ts +0 -38
  233. package/dist/scaffold/git-hooks.d.ts.map +0 -1
  234. package/dist/scaffold/index.d.ts +0 -17
  235. package/dist/scaffold/index.d.ts.map +0 -1
  236. package/dist/session/session-manager.d.ts +0 -94
  237. package/dist/session/session-manager.d.ts.map +0 -1
  238. package/dist/spec/SpecFileManager.d.ts +0 -146
  239. package/dist/spec/SpecFileManager.d.ts.map +0 -1
  240. package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
  241. package/dist/templates/.github/copilot/instructions.md +0 -311
  242. package/dist/test-analysis.d.ts +0 -231
  243. package/dist/test-analysis.d.ts.map +0 -1
  244. package/dist/tool-interface.d.ts +0 -236
  245. package/dist/tool-interface.d.ts.map +0 -1
  246. package/dist/tool-loader.d.ts +0 -77
  247. package/dist/tool-loader.d.ts.map +0 -1
  248. package/dist/tool-validator.d.ts +0 -72
  249. package/dist/tool-validator.d.ts.map +0 -1
  250. package/dist/utils/async-utils.d.ts +0 -73
  251. package/dist/utils/async-utils.d.ts.map +0 -1
  252. package/dist/utils/command-wrapper.d.ts +0 -66
  253. package/dist/utils/command-wrapper.d.ts.map +0 -1
  254. package/dist/utils/detection.d.ts +0 -14
  255. package/dist/utils/detection.d.ts.map +0 -1
  256. package/dist/utils/error-categories.d.ts +0 -52
  257. package/dist/utils/error-categories.d.ts.map +0 -1
  258. package/dist/utils/finalization.d.ts +0 -17
  259. package/dist/utils/finalization.d.ts.map +0 -1
  260. package/dist/utils/git-lock.d.ts +0 -13
  261. package/dist/utils/git-lock.d.ts.map +0 -1
  262. package/dist/utils/gitignore-updater.d.ts +0 -39
  263. package/dist/utils/gitignore-updater.d.ts.map +0 -1
  264. package/dist/utils/ide-detection.d.ts +0 -89
  265. package/dist/utils/ide-detection.d.ts.map +0 -1
  266. package/dist/utils/project-analysis.d.ts +0 -34
  267. package/dist/utils/project-analysis.d.ts.map +0 -1
  268. package/dist/utils/promise-utils.d.ts +0 -30
  269. package/dist/utils/promise-utils.d.ts.map +0 -1
  270. package/dist/utils/quality-gates-utils.d.ts +0 -49
  271. package/dist/utils/quality-gates-utils.d.ts.map +0 -1
  272. package/dist/utils/quality-gates.d.ts +0 -49
  273. package/dist/utils/quality-gates.d.ts.map +0 -1
  274. package/dist/utils/quality-gates.js +0 -402
  275. package/dist/utils/spec-resolver.d.ts +0 -80
  276. package/dist/utils/spec-resolver.d.ts.map +0 -1
  277. package/dist/utils/typescript-detector.d.ts +0 -66
  278. package/dist/utils/typescript-detector.d.ts.map +0 -1
  279. package/dist/utils/yaml-validation.d.ts +0 -32
  280. package/dist/utils/yaml-validation.d.ts.map +0 -1
  281. package/dist/validation/spec-validation.d.ts +0 -43
  282. package/dist/validation/spec-validation.d.ts.map +0 -1
  283. package/dist/waivers-manager.d.ts +0 -167
  284. package/dist/waivers-manager.d.ts.map +0 -1
  285. package/dist/worktree/worktree-manager.d.ts +0 -54
  286. package/dist/worktree/worktree-manager.d.ts.map +0 -1
@@ -4,8 +4,43 @@
4
4
  * @author @darianrosebrook
5
5
  */
6
6
 
7
- const { deriveBudget, checkBudgetCompliance } = require('../budget-derivation');
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
- // Basic structural validation for essential fields
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 (!/^[A-Z]+-\d+$/.test(spec.id)) {
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: 'Project ID should be in format: PREFIX-NUMBER (e.g., FEAT-1234)',
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 { valid: true };
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
- // Basic structural validation for essential fields
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
- // Validate specific field formats
261
- if (spec.id && !/^[A-Z]+-\d+$/.test(spec.id)) {
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: 'Project ID should be in format: PREFIX-NUMBER (e.g., FEAT-1234)',
265
- suggestion: 'Use format like: PROJ-001, FEAT-002, FIX-003',
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 = deriveBudget(spec, projectRoot);
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
  };
@@ -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
- const existingWaivers = await this.loadActiveWaivers();
371
- const usedIds = new Set(existingWaivers.map((w) => parseInt(w.id.split('-')[1])));
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
- let counter = 1;
374
- while (usedIds.has(counter)) {
375
- counter++;
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
- return `WV-${counter.toString().padStart(4, '0')}`;
516
+ throw new Error('No available waiver IDs (all 9999 slots used)');
379
517
  }
380
518
 
381
519
  validateWaiver(waiver) {