@paths.design/caws-cli 3.5.0 → 4.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/dist/budget-derivation.d.ts +41 -2
- package/dist/budget-derivation.d.ts.map +1 -1
- package/dist/budget-derivation.js +417 -30
- package/dist/commands/archive.d.ts +50 -0
- package/dist/commands/archive.d.ts.map +1 -0
- package/dist/commands/archive.js +353 -0
- package/dist/commands/iterate.d.ts.map +1 -1
- package/dist/commands/iterate.js +12 -13
- package/dist/commands/mode.d.ts +24 -0
- package/dist/commands/mode.d.ts.map +1 -0
- package/dist/commands/mode.js +259 -0
- package/dist/commands/plan.d.ts +49 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +448 -0
- package/dist/commands/quality-gates.d.ts +52 -0
- package/dist/commands/quality-gates.d.ts.map +1 -0
- package/dist/commands/quality-gates.js +490 -0
- package/dist/commands/specs.d.ts +71 -0
- package/dist/commands/specs.d.ts.map +1 -0
- package/dist/commands/specs.js +735 -0
- package/dist/commands/status.d.ts +4 -3
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +552 -22
- package/dist/commands/tutorial.d.ts +55 -0
- package/dist/commands/tutorial.d.ts.map +1 -0
- package/dist/commands/tutorial.js +481 -0
- package/dist/commands/validate.d.ts +10 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +199 -39
- package/dist/config/modes.d.ts +225 -0
- package/dist/config/modes.d.ts.map +1 -0
- package/dist/config/modes.js +321 -0
- package/dist/constants/spec-types.d.ts +41 -0
- package/dist/constants/spec-types.d.ts.map +1 -0
- package/dist/constants/spec-types.js +42 -0
- package/dist/index-new.d.ts +5 -0
- package/dist/index-new.d.ts.map +1 -0
- package/dist/index-new.js +317 -0
- package/dist/index.js +227 -10
- package/dist/index.js.backup +4711 -0
- package/dist/policy/PolicyManager.d.ts +104 -0
- package/dist/policy/PolicyManager.d.ts.map +1 -0
- package/dist/policy/PolicyManager.js +399 -0
- package/dist/scaffold/cursor-hooks.d.ts.map +1 -1
- package/dist/scaffold/cursor-hooks.js +15 -0
- package/dist/scaffold/git-hooks.d.ts.map +1 -1
- package/dist/scaffold/git-hooks.js +32 -44
- package/dist/scaffold/index.d.ts.map +1 -1
- package/dist/scaffold/index.js +19 -0
- package/dist/spec/SpecFileManager.d.ts +146 -0
- package/dist/spec/SpecFileManager.d.ts.map +1 -0
- package/dist/spec/SpecFileManager.js +419 -0
- package/dist/utils/quality-gates-errors.js +520 -0
- package/dist/utils/quality-gates.d.ts +49 -0
- package/dist/utils/quality-gates.d.ts.map +1 -0
- package/dist/utils/quality-gates.js +361 -0
- package/dist/utils/spec-resolver.d.ts +88 -0
- package/dist/utils/spec-resolver.d.ts.map +1 -0
- package/dist/utils/spec-resolver.js +602 -0
- package/dist/validation/spec-validation.d.ts +14 -0
- package/dist/validation/spec-validation.d.ts.map +1 -1
- package/dist/validation/spec-validation.js +225 -13
- package/package.json +6 -5
- package/templates/.cursor/hooks/caws-scope-guard.sh +64 -8
- package/templates/.cursor/hooks/validate-spec.sh +22 -12
- package/templates/.cursor/rules/00-claims-verification.mdc +144 -0
- package/templates/.cursor/rules/01-working-style.mdc +50 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +370 -0
- package/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
- package/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
- package/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
- package/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
- package/templates/.cursor/rules/07-process-ops.mdc +20 -0
- package/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
- package/templates/.cursor/rules/09-docstrings.mdc +89 -0
- package/templates/.cursor/rules/10-authorship-and-attribution.mdc +15 -0
- package/templates/.cursor/rules/11-documentation-quality-standards.mdc +390 -0
- package/templates/.cursor/rules/12-scope-management-waivers.mdc +385 -0
- package/templates/.cursor/rules/13-implementation-completeness.mdc +516 -0
- package/templates/.cursor/rules/14-language-agnostic-standards.mdc +588 -0
- package/templates/.cursor/rules/15-sophisticated-todo-detection.mdc +425 -0
- package/templates/.cursor/rules/README.md +150 -0
- package/templates/apps/tools/caws/prompt-lint.js.backup +274 -0
- package/templates/apps/tools/caws/provenance.js.backup +73 -0
- package/templates/scripts/quality-gates/check-god-objects.js +146 -0
- package/templates/scripts/quality-gates/run-quality-gates.js +50 -0
- package/templates/scripts/v3/analysis/todo_analyzer.py +1950 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Spec Resolution System
|
|
3
|
+
* Resolves spec files with priority: feature-specific > working-spec.yaml
|
|
4
|
+
* Enables multi-agent workflows where each agent works on their own spec
|
|
5
|
+
* @author @darianrosebrook
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const yaml = require('js-yaml');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
|
|
13
|
+
// Import SPEC_TYPES from constants for consistent display
|
|
14
|
+
const { SPEC_TYPES } = require('../constants/spec-types');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spec resolution priority:
|
|
18
|
+
* 1. .caws/specs/<spec-id>.yaml (feature-specific, multi-agent safe)
|
|
19
|
+
* 2. .caws/working-spec.yaml (legacy, single-agent only)
|
|
20
|
+
*/
|
|
21
|
+
const SPECS_DIR = '.caws/specs';
|
|
22
|
+
const LEGACY_SPEC = '.caws/working-spec.yaml';
|
|
23
|
+
const SPECS_REGISTRY = '.caws/specs/registry.json';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve spec file path based on priority
|
|
27
|
+
* @param {Object} options - Resolution options
|
|
28
|
+
* @param {string} [options.specId] - Feature-specific spec ID (e.g., 'user-auth', 'FEAT-001')
|
|
29
|
+
* @param {string} [options.specFile] - Explicit file path override
|
|
30
|
+
* @param {boolean} [options.warnLegacy=true] - Warn when falling back to legacy spec
|
|
31
|
+
* @param {boolean} [options.interactive=false] - Use interactive spec selection for multiple specs
|
|
32
|
+
* @returns {Promise<{path: string, type: 'feature' | 'legacy', spec: Object}>}
|
|
33
|
+
*/
|
|
34
|
+
async function resolveSpec(options = {}) {
|
|
35
|
+
const { specId, specFile, warnLegacy = true, interactive = false } = options;
|
|
36
|
+
|
|
37
|
+
// 1. Explicit file path takes highest priority
|
|
38
|
+
if (specFile) {
|
|
39
|
+
const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(process.cwd(), specFile);
|
|
40
|
+
|
|
41
|
+
if (await fs.pathExists(explicitPath)) {
|
|
42
|
+
const yaml = require('js-yaml');
|
|
43
|
+
const content = await fs.readFile(explicitPath, 'utf8');
|
|
44
|
+
const spec = yaml.load(content);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
path: explicitPath,
|
|
48
|
+
type: explicitPath.includes('/specs/') ? 'feature' : 'legacy',
|
|
49
|
+
spec,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new Error(`Spec file not found: ${explicitPath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Feature-specific spec (preferred for multi-agent)
|
|
57
|
+
if (specId) {
|
|
58
|
+
const featurePath = path.join(process.cwd(), SPECS_DIR, `${specId}.yaml`);
|
|
59
|
+
|
|
60
|
+
if (await fs.pathExists(featurePath)) {
|
|
61
|
+
const yaml = require('js-yaml');
|
|
62
|
+
const content = await fs.readFile(featurePath, 'utf8');
|
|
63
|
+
const spec = yaml.load(content);
|
|
64
|
+
|
|
65
|
+
console.log(chalk.green(`✅ Using feature-specific spec: ${specId}`));
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
path: featurePath,
|
|
69
|
+
type: 'feature',
|
|
70
|
+
spec,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Feature spec '${specId}' not found. Create it with: caws specs create ${specId}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Auto-detect from registry or list specs
|
|
80
|
+
const registry = await loadSpecsRegistry();
|
|
81
|
+
const specIds = Object.keys(registry.specs ?? {});
|
|
82
|
+
|
|
83
|
+
if (specIds.length === 1) {
|
|
84
|
+
// Single spec - use it automatically
|
|
85
|
+
const singleSpecId = specIds[0];
|
|
86
|
+
const singleSpecPath = path.join(process.cwd(), SPECS_DIR, registry.specs[singleSpecId].path);
|
|
87
|
+
|
|
88
|
+
if (await fs.pathExists(singleSpecPath)) {
|
|
89
|
+
const yaml = require('js-yaml');
|
|
90
|
+
const content = await fs.readFile(singleSpecPath, 'utf8');
|
|
91
|
+
const spec = yaml.load(content);
|
|
92
|
+
|
|
93
|
+
console.log(chalk.blue(`📋 Auto-detected single spec: ${singleSpecId}`));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
path: singleSpecPath,
|
|
97
|
+
type: 'feature',
|
|
98
|
+
spec,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
} else if (specIds.length > 1) {
|
|
102
|
+
// Multiple specs - require explicit selection with enhanced guidance
|
|
103
|
+
console.error(chalk.red('❌ Multiple specs detected. Please specify which one:'));
|
|
104
|
+
|
|
105
|
+
// Show specs with details
|
|
106
|
+
const specsInfo = [];
|
|
107
|
+
for (const id of specIds) {
|
|
108
|
+
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
109
|
+
try {
|
|
110
|
+
const content = await fs.readFile(specPath, 'utf8');
|
|
111
|
+
const spec = yaml.load(content);
|
|
112
|
+
const status = spec.status || 'draft';
|
|
113
|
+
const type = spec.type || 'feature';
|
|
114
|
+
const statusColor =
|
|
115
|
+
status === 'active' ? chalk.green : status === 'completed' ? chalk.blue : chalk.yellow;
|
|
116
|
+
const typeColor = SPEC_TYPES[type] ? SPEC_TYPES[type].color : chalk.white;
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.yellow(
|
|
120
|
+
` - ${id} ${typeColor(`(${type})`)} ${statusColor(`[${status}]`)} - ${spec.title || 'Untitled'}`
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
specsInfo.push({ id, type, status, title: spec.title || 'Untitled' });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.log(chalk.yellow(` - ${id} (error loading details)`));
|
|
126
|
+
specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'Error loading' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Interactive mode
|
|
131
|
+
if (interactive) {
|
|
132
|
+
try {
|
|
133
|
+
const selectedSpecId = await interactiveSpecSelection(specIds);
|
|
134
|
+
|
|
135
|
+
// Recursively resolve with the selected spec ID
|
|
136
|
+
return await resolveSpec({
|
|
137
|
+
specId: selectedSpecId,
|
|
138
|
+
warnLegacy,
|
|
139
|
+
interactive: false, // Prevent infinite recursion
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`Interactive selection failed: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(chalk.blue('\n Usage: caws <command> --spec-id <spec-id>'));
|
|
147
|
+
console.log(chalk.gray(` Example: caws validate --spec-id ${specIds[0]}`));
|
|
148
|
+
|
|
149
|
+
// Suggest most likely spec (active first, then by type priority)
|
|
150
|
+
const priorityOrder = { active: 0, draft: 1, completed: 2 };
|
|
151
|
+
const sortedSpecs = specIds.sort((a, b) => {
|
|
152
|
+
const aSpec = specsInfo.find((s) => s.id === a);
|
|
153
|
+
const bSpec = specsInfo.find((s) => s.id === b);
|
|
154
|
+
const aPriority = priorityOrder[aSpec?.status] || 999;
|
|
155
|
+
const bPriority = priorityOrder[bSpec?.status] || 999;
|
|
156
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
157
|
+
|
|
158
|
+
// Then by type (feature > fix > refactor > etc.)
|
|
159
|
+
const typePriority = { feature: 0, fix: 1, refactor: 2, chore: 3, docs: 4 };
|
|
160
|
+
const aTypePriority = typePriority[aSpec?.type] || 999;
|
|
161
|
+
const bTypePriority = typePriority[bSpec?.type] || 999;
|
|
162
|
+
return aTypePriority - bTypePriority;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log(chalk.green('\n💡 Quick suggestion:'));
|
|
166
|
+
console.log(chalk.gray(` Try: caws <command> --spec-id ${sortedSpecs[0]}`));
|
|
167
|
+
|
|
168
|
+
// Interactive mode suggestion
|
|
169
|
+
console.log(chalk.blue('\n Interactive mode: caws <command> --interactive-spec-selection'));
|
|
170
|
+
|
|
171
|
+
throw new Error('Spec ID required when multiple specs exist');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. Fall back to legacy working-spec.yaml (with warning)
|
|
175
|
+
const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
|
|
176
|
+
|
|
177
|
+
if (await fs.pathExists(legacyPath)) {
|
|
178
|
+
const yaml = require('js-yaml');
|
|
179
|
+
const content = await fs.readFile(legacyPath, 'utf8');
|
|
180
|
+
const spec = yaml.load(content);
|
|
181
|
+
|
|
182
|
+
if (warnLegacy) {
|
|
183
|
+
console.log(chalk.yellow('⚠️ Using legacy working-spec.yaml'));
|
|
184
|
+
console.log(chalk.gray(' For multi-agent workflows, use feature-specific specs:'));
|
|
185
|
+
console.log(chalk.blue(' caws specs create <feature-id>'));
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
path: legacyPath,
|
|
191
|
+
type: 'legacy',
|
|
192
|
+
spec,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 5. No specs found
|
|
197
|
+
throw new Error(
|
|
198
|
+
'No CAWS spec found. Initialize with: caws init or create a feature spec: caws specs create <id>'
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Load specs registry
|
|
204
|
+
* @returns {Promise<Object>} Registry data
|
|
205
|
+
*/
|
|
206
|
+
async function loadSpecsRegistry() {
|
|
207
|
+
const registryPath = path.join(process.cwd(), SPECS_REGISTRY);
|
|
208
|
+
|
|
209
|
+
if (!(await fs.pathExists(registryPath))) {
|
|
210
|
+
return {
|
|
211
|
+
version: '1.0.0',
|
|
212
|
+
specs: {},
|
|
213
|
+
lastUpdated: new Date().toISOString(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const registry = await fs.readJson(registryPath);
|
|
219
|
+
return registry;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
version: '1.0.0',
|
|
223
|
+
specs: {},
|
|
224
|
+
lastUpdated: new Date().toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* List all available specs
|
|
231
|
+
* @returns {Promise<Array<{id: string, path: string, type: string}>>}
|
|
232
|
+
*/
|
|
233
|
+
async function listAvailableSpecs() {
|
|
234
|
+
const specs = [];
|
|
235
|
+
|
|
236
|
+
// Check feature-specific specs
|
|
237
|
+
const specsDir = path.join(process.cwd(), SPECS_DIR);
|
|
238
|
+
if (await fs.pathExists(specsDir)) {
|
|
239
|
+
const files = await fs.readdir(specsDir);
|
|
240
|
+
const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
241
|
+
|
|
242
|
+
for (const file of yamlFiles) {
|
|
243
|
+
if (file === 'registry.json') continue;
|
|
244
|
+
|
|
245
|
+
const specPath = path.join(specsDir, file);
|
|
246
|
+
try {
|
|
247
|
+
const yaml = require('js-yaml');
|
|
248
|
+
const content = await fs.readFile(specPath, 'utf8');
|
|
249
|
+
const spec = yaml.load(content);
|
|
250
|
+
|
|
251
|
+
specs.push({
|
|
252
|
+
id: spec.id || path.basename(file, path.extname(file)),
|
|
253
|
+
path: path.relative(process.cwd(), specPath),
|
|
254
|
+
type: 'feature',
|
|
255
|
+
title: spec.title || 'Untitled',
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Skip invalid specs
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check legacy working-spec.yaml
|
|
264
|
+
const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
|
|
265
|
+
if (await fs.pathExists(legacyPath)) {
|
|
266
|
+
try {
|
|
267
|
+
const yaml = require('js-yaml');
|
|
268
|
+
const content = await fs.readFile(legacyPath, 'utf8');
|
|
269
|
+
const spec = yaml.load(content);
|
|
270
|
+
|
|
271
|
+
specs.push({
|
|
272
|
+
id: spec.id || 'working-spec',
|
|
273
|
+
path: LEGACY_SPEC,
|
|
274
|
+
type: 'legacy',
|
|
275
|
+
title: spec.title || 'Legacy Working Spec',
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
// Skip invalid spec
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return specs;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Interactive spec selection using readline
|
|
287
|
+
* @param {string[]} specIds - Available spec IDs
|
|
288
|
+
* @returns {Promise<string>} Selected spec ID
|
|
289
|
+
*/
|
|
290
|
+
async function interactiveSpecSelection(specIds) {
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const readline = require('readline');
|
|
293
|
+
|
|
294
|
+
console.log(chalk.blue('\n📋 Interactive Spec Selection'));
|
|
295
|
+
console.log(chalk.gray('Select which spec to use:\n'));
|
|
296
|
+
|
|
297
|
+
specIds.forEach((id, index) => {
|
|
298
|
+
console.log(chalk.yellow(`${index + 1}. ${id}`));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
console.log(chalk.gray('\nEnter number (1-' + specIds.length + ') or spec ID directly: '));
|
|
302
|
+
|
|
303
|
+
const rl = readline.createInterface({
|
|
304
|
+
input: process.stdin,
|
|
305
|
+
output: process.stdout,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
rl.question('> ', (answer) => {
|
|
309
|
+
rl.close();
|
|
310
|
+
|
|
311
|
+
const trimmed = answer.trim();
|
|
312
|
+
|
|
313
|
+
// Check if it's a number
|
|
314
|
+
const num = parseInt(trimmed);
|
|
315
|
+
if (num >= 1 && num <= specIds.length) {
|
|
316
|
+
resolve(specIds[num - 1]);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if it's a direct spec ID
|
|
321
|
+
if (specIds.includes(trimmed)) {
|
|
322
|
+
resolve(trimmed);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
reject(new Error(`Invalid selection: ${trimmed}. Please choose a valid spec ID.`));
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if project is using multi-spec architecture
|
|
333
|
+
* @returns {Promise<{isMultiSpec: boolean, specCount: number, needsMigration: boolean}>}
|
|
334
|
+
*/
|
|
335
|
+
async function checkMultiSpecStatus() {
|
|
336
|
+
const registry = await loadSpecsRegistry();
|
|
337
|
+
const hasFeatureSpecs = Object.keys(registry.specs ?? {}).length > 0;
|
|
338
|
+
const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
|
|
339
|
+
const hasLegacySpec = await fs.pathExists(legacyPath);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
isMultiSpec: hasFeatureSpecs,
|
|
343
|
+
specCount: Object.keys(registry.specs ?? {}).length,
|
|
344
|
+
needsMigration: hasLegacySpec && !hasFeatureSpecs,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check for scope conflicts between specs
|
|
350
|
+
* @param {string[]} specIds - Array of spec IDs to check
|
|
351
|
+
* @returns {Promise<Array<{spec1: string, spec2: string, conflicts: string[]}>>} Array of conflicts
|
|
352
|
+
*/
|
|
353
|
+
async function checkScopeConflicts(specIds) {
|
|
354
|
+
const conflicts = [];
|
|
355
|
+
const specScopes = [];
|
|
356
|
+
|
|
357
|
+
// Load registry once
|
|
358
|
+
const registry = await loadSpecsRegistry();
|
|
359
|
+
|
|
360
|
+
// Load all specs and their scopes
|
|
361
|
+
for (const id of specIds) {
|
|
362
|
+
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const content = await fs.readFile(specPath, 'utf8');
|
|
366
|
+
const spec = yaml.load(content);
|
|
367
|
+
|
|
368
|
+
specScopes.push({
|
|
369
|
+
id,
|
|
370
|
+
scope: spec.scope || { in: [], out: [] },
|
|
371
|
+
title: spec.title || id,
|
|
372
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// Skip specs that can't be loaded
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check for conflicts between each pair of specs
|
|
380
|
+
for (let i = 0; i < specScopes.length; i++) {
|
|
381
|
+
for (let j = i + 1; j < specScopes.length; j++) {
|
|
382
|
+
const spec1 = specScopes[i];
|
|
383
|
+
const spec2 = specScopes[j];
|
|
384
|
+
|
|
385
|
+
const spec1Paths = new Set(spec1.scope.in || []);
|
|
386
|
+
const spec2Paths = new Set(spec2.scope.in || []);
|
|
387
|
+
|
|
388
|
+
// Find overlapping paths
|
|
389
|
+
const overlappingPaths = [];
|
|
390
|
+
for (const path1 of spec1Paths) {
|
|
391
|
+
for (const path2 of spec2Paths) {
|
|
392
|
+
if (pathsOverlap(path1, path2)) {
|
|
393
|
+
overlappingPaths.push(`${path1} ↔ ${path2}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (overlappingPaths.length > 0) {
|
|
399
|
+
conflicts.push({
|
|
400
|
+
spec1: spec1.id,
|
|
401
|
+
spec2: spec2.id,
|
|
402
|
+
conflicts: overlappingPaths,
|
|
403
|
+
severity: 'warning', // Could be 'error' for stricter enforcement
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return conflicts;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check if two paths overlap (simplified implementation)
|
|
414
|
+
* @param {string} path1 - First path
|
|
415
|
+
* @param {string} path2 - Second path
|
|
416
|
+
* @returns {boolean} True if paths overlap
|
|
417
|
+
*/
|
|
418
|
+
function pathsOverlap(path1, path2) {
|
|
419
|
+
// Normalize paths (remove leading/trailing slashes)
|
|
420
|
+
const normalizePath = (p) => p.replace(/^\/+|\/+$/g, '');
|
|
421
|
+
|
|
422
|
+
const normalized1 = normalizePath(path1);
|
|
423
|
+
const normalized2 = normalizePath(path2);
|
|
424
|
+
|
|
425
|
+
// Check for exact match
|
|
426
|
+
if (normalized1 === normalized2) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Handle wildcard patterns
|
|
431
|
+
const hasWildcard = (p) => p.includes('*');
|
|
432
|
+
|
|
433
|
+
if (hasWildcard(normalized1) || hasWildcard(normalized2)) {
|
|
434
|
+
// Convert wildcards to regex patterns
|
|
435
|
+
const toRegex = (p) => {
|
|
436
|
+
// Escape dots first
|
|
437
|
+
let result = p.replace(/\./g, '\\.');
|
|
438
|
+
|
|
439
|
+
// Handle ** patterns (match any path including zero segments)
|
|
440
|
+
result = result.replace(/\*\*/g, '(?:.*/)?');
|
|
441
|
+
|
|
442
|
+
// Handle single * patterns (match any non-slash characters)
|
|
443
|
+
result = result.replace(/\*/g, '[^/]*');
|
|
444
|
+
|
|
445
|
+
// Fix patterns like src/auth/**/*.js to match src/auth/login.js
|
|
446
|
+
// The pattern (?:.*/)?[^/]* should become .*[^/]* for direct filename matching
|
|
447
|
+
result = result.replace(/(\?:.*\/)?[^/]*/g, '.*[^/]*');
|
|
448
|
+
|
|
449
|
+
// Also fix patterns like (?:.[^/]*/)?/[^/]* to match direct filenames
|
|
450
|
+
result = result.replace(/(?:\..*\/)?[^/]*/g, '.*[^/]*');
|
|
451
|
+
|
|
452
|
+
return result;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Check if either path matches the other's pattern
|
|
456
|
+
if (hasWildcard(normalized1)) {
|
|
457
|
+
const regex1 = new RegExp('^' + toRegex(normalized1) + '$');
|
|
458
|
+
if (regex1.test(normalized2)) return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (hasWildcard(normalized2)) {
|
|
462
|
+
const regex2 = new RegExp('^' + toRegex(normalized2) + '$');
|
|
463
|
+
if (regex2.test(normalized1)) return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Simple substring check for non-wildcard paths
|
|
470
|
+
return normalized1.includes(normalized2) || normalized2.includes(normalized1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Suggest migration from legacy to multi-spec
|
|
475
|
+
* @returns {Promise<void>}
|
|
476
|
+
*/
|
|
477
|
+
async function suggestMigration() {
|
|
478
|
+
const status = await checkMultiSpecStatus();
|
|
479
|
+
|
|
480
|
+
if (status.needsMigration) {
|
|
481
|
+
console.log(chalk.yellow('\n⚠️ Migration Recommended: Single-Spec → Multi-Spec'));
|
|
482
|
+
console.log(chalk.gray(' Your project uses the legacy working-spec.yaml'));
|
|
483
|
+
console.log(chalk.gray(' For multi-agent workflows, migrate to feature-specific specs:\n'));
|
|
484
|
+
console.log(chalk.blue(' 1. caws specs create <feature-id>'));
|
|
485
|
+
console.log(chalk.blue(' 2. Copy relevant content from working-spec.yaml'));
|
|
486
|
+
console.log(chalk.blue(' 3. Update agents to use --spec-id <feature-id>'));
|
|
487
|
+
console.log(chalk.gray('\n See: docs/guides/multi-agent-migration.md\n'));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Feature breakdown logic (moved from specs.js to avoid circular dependency)
|
|
492
|
+
function suggestFeatureBreakdown(legacySpec) {
|
|
493
|
+
const features = [];
|
|
494
|
+
|
|
495
|
+
if (legacySpec.acceptance && legacySpec.acceptance.length > 0) {
|
|
496
|
+
// Group acceptance criteria by logical features
|
|
497
|
+
const criteriaByFeature = {};
|
|
498
|
+
|
|
499
|
+
legacySpec.acceptance.forEach((criterion, index) => {
|
|
500
|
+
// Simple heuristic: extract feature from criterion description (check all fields)
|
|
501
|
+
const fullDescription = [
|
|
502
|
+
criterion.given || '',
|
|
503
|
+
criterion.when || '',
|
|
504
|
+
criterion.then || '',
|
|
505
|
+
criterion.description || '',
|
|
506
|
+
criterion.title || `A${index + 1}`,
|
|
507
|
+
].join(' ');
|
|
508
|
+
const words = fullDescription.toLowerCase().split(' ');
|
|
509
|
+
|
|
510
|
+
// Look for common feature keywords
|
|
511
|
+
const featureKeywords = {
|
|
512
|
+
auth: 'Authentication',
|
|
513
|
+
login: 'Authentication',
|
|
514
|
+
payment: 'Payment System',
|
|
515
|
+
billing: 'Billing',
|
|
516
|
+
dashboard: 'Dashboard',
|
|
517
|
+
admin: 'Admin Panel',
|
|
518
|
+
api: 'API',
|
|
519
|
+
database: 'Data Layer',
|
|
520
|
+
ui: 'User Interface',
|
|
521
|
+
email: 'Email System',
|
|
522
|
+
notification: 'Notifications',
|
|
523
|
+
report: 'Reporting',
|
|
524
|
+
search: 'Search',
|
|
525
|
+
filter: 'Filtering',
|
|
526
|
+
user: 'User Management',
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
let featureKey = 'general';
|
|
530
|
+
let featureTitle = 'General Features';
|
|
531
|
+
|
|
532
|
+
for (const [keyword, title] of Object.entries(featureKeywords)) {
|
|
533
|
+
if (words.some((word) => word.includes(keyword))) {
|
|
534
|
+
featureKey = keyword;
|
|
535
|
+
featureTitle = title;
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!criteriaByFeature[featureKey]) {
|
|
541
|
+
criteriaByFeature[featureKey] = {
|
|
542
|
+
id: featureKey,
|
|
543
|
+
title: featureTitle,
|
|
544
|
+
criteria: [],
|
|
545
|
+
scope: { in: [], out: [] },
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
criteriaByFeature[featureKey].criteria.push(criterion);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// Convert to feature objects
|
|
553
|
+
Object.values(criteriaByFeature).forEach((feature) => {
|
|
554
|
+
// Suggest scope based on feature type
|
|
555
|
+
const scopeSuggestions = {
|
|
556
|
+
user: { in: ['src/users/', 'tests/users/'], out: ['src/payments/', 'src/admin/'] },
|
|
557
|
+
auth: { in: ['src/auth/', 'tests/auth/'], out: ['src/payments/', 'src/admin/'] },
|
|
558
|
+
payment: { in: ['src/payments/', 'tests/payments/'], out: ['src/users/', 'src/admin/'] },
|
|
559
|
+
dashboard: {
|
|
560
|
+
in: ['src/dashboard/', 'tests/dashboard/'],
|
|
561
|
+
out: ['src/payments/', 'src/users/'],
|
|
562
|
+
},
|
|
563
|
+
admin: { in: ['src/admin/', 'tests/admin/'], out: ['src/payments/', 'src/users/'] },
|
|
564
|
+
api: { in: ['src/api/', 'tests/api/'], out: ['src/dashboard/', 'src/admin/'] },
|
|
565
|
+
general: { in: ['src/', 'tests/'], out: [] },
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const suggestion = scopeSuggestions[feature.id] || scopeSuggestions.general;
|
|
569
|
+
feature.scope = suggestion;
|
|
570
|
+
|
|
571
|
+
features.push(feature);
|
|
572
|
+
});
|
|
573
|
+
} else {
|
|
574
|
+
// Fallback: create a single feature
|
|
575
|
+
features.push({
|
|
576
|
+
id: 'main-feature',
|
|
577
|
+
title: legacySpec.title || 'Main Feature',
|
|
578
|
+
criteria: legacySpec.acceptance || [],
|
|
579
|
+
scope: {
|
|
580
|
+
in: ['src/', 'tests/'],
|
|
581
|
+
out: [],
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return features;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
resolveSpec,
|
|
591
|
+
listAvailableSpecs,
|
|
592
|
+
checkMultiSpecStatus,
|
|
593
|
+
checkScopeConflicts,
|
|
594
|
+
suggestMigration,
|
|
595
|
+
interactiveSpecSelection,
|
|
596
|
+
loadSpecsRegistry,
|
|
597
|
+
suggestFeatureBreakdown,
|
|
598
|
+
pathsOverlap,
|
|
599
|
+
SPECS_DIR,
|
|
600
|
+
LEGACY_SPEC,
|
|
601
|
+
SPECS_REGISTRY,
|
|
602
|
+
};
|
|
@@ -26,4 +26,18 @@ export function getFieldSuggestion(field: string, _spec: any): string;
|
|
|
26
26
|
* @returns {boolean} Whether field can be auto-fixed
|
|
27
27
|
*/
|
|
28
28
|
export function canAutoFixField(field: string, _spec: any): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Calculate compliance score based on errors and warnings
|
|
31
|
+
* Score ranges from 0 (many issues) to 1 (perfect)
|
|
32
|
+
* @param {Array} errors - Validation errors
|
|
33
|
+
* @param {Array} warnings - Validation warnings
|
|
34
|
+
* @returns {number} Compliance score (0-1)
|
|
35
|
+
*/
|
|
36
|
+
export function calculateComplianceScore(errors: any[], warnings: any[]): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get compliance grade from score
|
|
39
|
+
* @param {number} score - Compliance score (0-1)
|
|
40
|
+
* @returns {string} Grade (A, B, C, D, F)
|
|
41
|
+
*/
|
|
42
|
+
export function getComplianceGrade(score: number): string;
|
|
29
43
|
//# sourceMappingURL=spec-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,mEA8HC;AAED;;;;;GAKG;AACH,
|
|
1
|
+
{"version":3,"file":"spec-validation.d.ts","sourceRoot":"","sources":["../../src/validation/spec-validation.js"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,mEA8HC;AAED;;;;;GAKG;AACH,kFAyWC;AAoCD;;;;;GAKG;AACH,0CAJW,MAAM,eAEJ,MAAM,CAkBlB;AAED;;;;;GAKG;AACH,uCAJW,MAAM,eAEJ,OAAO,CAKnB;AAnED;;;;;;GAMG;AACH,0EAFa,MAAM,CAclB;AAED;;;;GAIG;AACH,0CAHW,MAAM,GACJ,MAAM,CAQlB"}
|