kiro-spec-engine 1.4.4 → 1.5.2
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/CHANGELOG.md +62 -1
- package/README.md +16 -0
- package/README.zh.md +380 -0
- package/bin/kiro-spec-engine.js +102 -44
- package/docs/adoption-guide.md +53 -0
- package/docs/document-governance.md +864 -0
- package/docs/spec-numbering-guide.md +348 -0
- package/docs/spec-workflow.md +65 -0
- package/docs/troubleshooting.md +339 -0
- package/docs/zh/spec-numbering-guide.md +348 -0
- package/lib/adoption/adoption-strategy.js +22 -6
- package/lib/adoption/conflict-resolver.js +239 -0
- package/lib/adoption/diff-viewer.js +226 -0
- package/lib/backup/selective-backup.js +207 -0
- package/lib/commands/adopt.js +95 -10
- package/lib/commands/docs.js +717 -0
- package/lib/commands/doctor.js +141 -3
- package/lib/commands/status.js +77 -5
- package/lib/governance/archive-tool.js +231 -0
- package/lib/governance/cleanup-tool.js +237 -0
- package/lib/governance/config-manager.js +186 -0
- package/lib/governance/diagnostic-engine.js +271 -0
- package/lib/governance/execution-logger.js +243 -0
- package/lib/governance/file-scanner.js +285 -0
- package/lib/governance/hooks-manager.js +333 -0
- package/lib/governance/reporter.js +337 -0
- package/lib/governance/validation-engine.js +181 -0
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup Tool
|
|
3
|
+
*
|
|
4
|
+
* Removes temporary and non-compliant documents
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const FileScanner = require('./file-scanner');
|
|
11
|
+
|
|
12
|
+
class CleanupTool {
|
|
13
|
+
constructor(projectPath, config) {
|
|
14
|
+
this.projectPath = projectPath;
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.scanner = new FileScanner(projectPath);
|
|
17
|
+
this.deletedFiles = [];
|
|
18
|
+
this.errors = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute cleanup operation
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} options - Cleanup options
|
|
25
|
+
* @param {boolean} options.dryRun - Preview without deleting
|
|
26
|
+
* @param {boolean} options.interactive - Prompt for each file
|
|
27
|
+
* @param {string} options.spec - Specific Spec to clean
|
|
28
|
+
* @returns {Promise<CleanupReport>}
|
|
29
|
+
*/
|
|
30
|
+
async cleanup(options = {}) {
|
|
31
|
+
const filesToDelete = await this.identifyFilesToDelete(options.spec);
|
|
32
|
+
|
|
33
|
+
if (options.dryRun) {
|
|
34
|
+
return this.generateDryRunReport(filesToDelete);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const file of filesToDelete) {
|
|
38
|
+
if (options.interactive) {
|
|
39
|
+
const shouldDelete = await this.promptForConfirmation(file);
|
|
40
|
+
if (!shouldDelete) continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await this.deleteFile(file);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return this.generateReport();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Identify files to delete
|
|
51
|
+
*
|
|
52
|
+
* @param {string} specName - Optional specific Spec
|
|
53
|
+
* @returns {Promise<string[]>}
|
|
54
|
+
*/
|
|
55
|
+
async identifyFilesToDelete(specName = null) {
|
|
56
|
+
const files = [];
|
|
57
|
+
|
|
58
|
+
// Scan root directory
|
|
59
|
+
files.push(...await this.scanRootForTemporary());
|
|
60
|
+
|
|
61
|
+
// Scan Spec directories
|
|
62
|
+
if (specName) {
|
|
63
|
+
files.push(...await this.scanSpecForTemporary(specName));
|
|
64
|
+
} else {
|
|
65
|
+
files.push(...await this.scanAllSpecsForTemporary());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Scan root directory for temporary files
|
|
73
|
+
*
|
|
74
|
+
* @returns {Promise<string[]>}
|
|
75
|
+
*/
|
|
76
|
+
async scanRootForTemporary() {
|
|
77
|
+
const temporaryFiles = [];
|
|
78
|
+
const mdFiles = await this.scanner.findMarkdownFiles(this.projectPath);
|
|
79
|
+
const allowedFiles = this.config.rootAllowedFiles || [];
|
|
80
|
+
const temporaryPatterns = this.config.temporaryPatterns || [];
|
|
81
|
+
|
|
82
|
+
for (const filePath of mdFiles) {
|
|
83
|
+
const basename = path.basename(filePath);
|
|
84
|
+
|
|
85
|
+
// If not in allowed list, check if it matches temporary patterns
|
|
86
|
+
if (!allowedFiles.includes(basename)) {
|
|
87
|
+
// Check if it matches any temporary pattern
|
|
88
|
+
if (this.scanner.matchesPattern(filePath, temporaryPatterns)) {
|
|
89
|
+
temporaryFiles.push(filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return temporaryFiles;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Scan a specific Spec directory for temporary files
|
|
99
|
+
*
|
|
100
|
+
* @param {string} specName - Spec name
|
|
101
|
+
* @returns {Promise<string[]>}
|
|
102
|
+
*/
|
|
103
|
+
async scanSpecForTemporary(specName) {
|
|
104
|
+
const temporaryFiles = [];
|
|
105
|
+
const specPath = this.scanner.getSpecDirectory(specName);
|
|
106
|
+
|
|
107
|
+
// Check if Spec directory exists
|
|
108
|
+
if (!await this.scanner.exists(specPath)) {
|
|
109
|
+
return temporaryFiles;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const mdFiles = await this.scanner.findMarkdownFiles(specPath);
|
|
113
|
+
const temporaryPatterns = this.config.temporaryPatterns || [];
|
|
114
|
+
const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
|
|
115
|
+
|
|
116
|
+
for (const filePath of mdFiles) {
|
|
117
|
+
const basename = path.basename(filePath);
|
|
118
|
+
|
|
119
|
+
// Don't delete required files even if they match patterns
|
|
120
|
+
if (requiredFiles.includes(basename)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check if it matches any temporary pattern
|
|
125
|
+
if (this.scanner.matchesPattern(filePath, temporaryPatterns)) {
|
|
126
|
+
temporaryFiles.push(filePath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return temporaryFiles;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Scan all Spec directories for temporary files
|
|
135
|
+
*
|
|
136
|
+
* @returns {Promise<string[]>}
|
|
137
|
+
*/
|
|
138
|
+
async scanAllSpecsForTemporary() {
|
|
139
|
+
const temporaryFiles = [];
|
|
140
|
+
const specDirs = await this.scanner.findSpecDirectories();
|
|
141
|
+
|
|
142
|
+
for (const specDir of specDirs) {
|
|
143
|
+
const specName = path.basename(specDir);
|
|
144
|
+
const specTemporaryFiles = await this.scanSpecForTemporary(specName);
|
|
145
|
+
temporaryFiles.push(...specTemporaryFiles);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return temporaryFiles;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Prompt user for confirmation to delete a file
|
|
153
|
+
*
|
|
154
|
+
* @param {string} filePath - Path to file
|
|
155
|
+
* @returns {Promise<boolean>}
|
|
156
|
+
*/
|
|
157
|
+
async promptForConfirmation(filePath) {
|
|
158
|
+
const relativePath = this.scanner.getRelativePath(filePath);
|
|
159
|
+
|
|
160
|
+
const answer = await inquirer.prompt([
|
|
161
|
+
{
|
|
162
|
+
type: 'confirm',
|
|
163
|
+
name: 'shouldDelete',
|
|
164
|
+
message: `Delete ${relativePath}?`,
|
|
165
|
+
default: false
|
|
166
|
+
}
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
return answer.shouldDelete;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete a file safely
|
|
174
|
+
*
|
|
175
|
+
* @param {string} filePath - Path to file
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
async deleteFile(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
await fs.remove(filePath);
|
|
181
|
+
this.deletedFiles.push(filePath);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
this.errors.push({
|
|
184
|
+
path: filePath,
|
|
185
|
+
error: error.message
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate dry run report
|
|
192
|
+
*
|
|
193
|
+
* @param {string[]} filesToDelete - Files that would be deleted
|
|
194
|
+
* @returns {CleanupReport}
|
|
195
|
+
*/
|
|
196
|
+
generateDryRunReport(filesToDelete) {
|
|
197
|
+
return {
|
|
198
|
+
success: true,
|
|
199
|
+
deletedFiles: filesToDelete,
|
|
200
|
+
errors: [],
|
|
201
|
+
summary: {
|
|
202
|
+
totalDeleted: filesToDelete.length,
|
|
203
|
+
totalErrors: 0
|
|
204
|
+
},
|
|
205
|
+
dryRun: true
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Generate cleanup report
|
|
211
|
+
*
|
|
212
|
+
* @returns {CleanupReport}
|
|
213
|
+
*/
|
|
214
|
+
generateReport() {
|
|
215
|
+
return {
|
|
216
|
+
success: this.errors.length === 0,
|
|
217
|
+
deletedFiles: this.deletedFiles,
|
|
218
|
+
errors: this.errors,
|
|
219
|
+
summary: {
|
|
220
|
+
totalDeleted: this.deletedFiles.length,
|
|
221
|
+
totalErrors: this.errors.length
|
|
222
|
+
},
|
|
223
|
+
dryRun: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @typedef {Object} CleanupReport
|
|
230
|
+
* @property {boolean} success - Whether cleanup succeeded
|
|
231
|
+
* @property {string[]} deletedFiles - Files that were deleted
|
|
232
|
+
* @property {Object[]} errors - Errors encountered
|
|
233
|
+
* @property {Object} summary - Summary statistics
|
|
234
|
+
* @property {boolean} dryRun - Whether this was a dry run
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
module.exports = CleanupTool;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages document governance configuration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class ConfigManager {
|
|
11
|
+
constructor(projectPath) {
|
|
12
|
+
this.projectPath = projectPath;
|
|
13
|
+
this.configPath = path.join(projectPath, '.kiro/config/docs.json');
|
|
14
|
+
this.config = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load configuration
|
|
19
|
+
*
|
|
20
|
+
* @returns {Promise<Object>}
|
|
21
|
+
*/
|
|
22
|
+
async load() {
|
|
23
|
+
if (await fs.pathExists(this.configPath)) {
|
|
24
|
+
try {
|
|
25
|
+
this.config = await fs.readJson(this.configPath);
|
|
26
|
+
|
|
27
|
+
// Validate and merge with defaults to ensure all required fields exist
|
|
28
|
+
const defaults = this.getDefaults();
|
|
29
|
+
this.config = { ...defaults, ...this.config };
|
|
30
|
+
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn('Config file corrupted, using defaults');
|
|
33
|
+
this.config = this.getDefaults();
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
this.config = this.getDefaults();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get default configuration
|
|
44
|
+
*
|
|
45
|
+
* @returns {Object}
|
|
46
|
+
*/
|
|
47
|
+
getDefaults() {
|
|
48
|
+
return {
|
|
49
|
+
rootAllowedFiles: [
|
|
50
|
+
'README.md',
|
|
51
|
+
'README.zh.md',
|
|
52
|
+
'CHANGELOG.md',
|
|
53
|
+
'CONTRIBUTING.md'
|
|
54
|
+
],
|
|
55
|
+
specSubdirs: [
|
|
56
|
+
'reports',
|
|
57
|
+
'scripts',
|
|
58
|
+
'tests',
|
|
59
|
+
'results',
|
|
60
|
+
'docs'
|
|
61
|
+
],
|
|
62
|
+
temporaryPatterns: [
|
|
63
|
+
'*-SUMMARY.md',
|
|
64
|
+
'SESSION-*.md',
|
|
65
|
+
'*-COMPLETE.md',
|
|
66
|
+
'TEMP-*.md',
|
|
67
|
+
'WIP-*.md',
|
|
68
|
+
'MVP-*.md'
|
|
69
|
+
]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Save configuration
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} config - Configuration to save
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async save(config) {
|
|
80
|
+
await fs.ensureDir(path.dirname(this.configPath));
|
|
81
|
+
await fs.writeJson(this.configPath, config, { spaces: 2 });
|
|
82
|
+
this.config = config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Update a configuration value
|
|
87
|
+
*
|
|
88
|
+
* @param {string} key - Configuration key
|
|
89
|
+
* @param {any} value - New value
|
|
90
|
+
* @returns {Promise<void>}
|
|
91
|
+
*/
|
|
92
|
+
async set(key, value) {
|
|
93
|
+
if (!this.config) {
|
|
94
|
+
await this.load();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.config[key] = value;
|
|
98
|
+
await this.save(this.config);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get a configuration value
|
|
103
|
+
*
|
|
104
|
+
* @param {string} key - Configuration key
|
|
105
|
+
* @returns {any} - Configuration value
|
|
106
|
+
*/
|
|
107
|
+
get(key) {
|
|
108
|
+
if (!this.config) {
|
|
109
|
+
throw new Error('Configuration not loaded. Call load() first.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.config[key];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all configuration
|
|
117
|
+
*
|
|
118
|
+
* @returns {Object} - Complete configuration object
|
|
119
|
+
*/
|
|
120
|
+
getAll() {
|
|
121
|
+
if (!this.config) {
|
|
122
|
+
throw new Error('Configuration not loaded. Call load() first.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { ...this.config };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reset to defaults
|
|
130
|
+
*
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async reset() {
|
|
134
|
+
this.config = this.getDefaults();
|
|
135
|
+
await this.save(this.config);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Validate configuration structure
|
|
140
|
+
*
|
|
141
|
+
* @param {Object} config - Configuration to validate
|
|
142
|
+
* @returns {Object} - Validation result { valid: boolean, errors: string[] }
|
|
143
|
+
*/
|
|
144
|
+
validate(config) {
|
|
145
|
+
const errors = [];
|
|
146
|
+
|
|
147
|
+
// Check required fields
|
|
148
|
+
if (!config.rootAllowedFiles || !Array.isArray(config.rootAllowedFiles)) {
|
|
149
|
+
errors.push('rootAllowedFiles must be an array');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!config.specSubdirs || !Array.isArray(config.specSubdirs)) {
|
|
153
|
+
errors.push('specSubdirs must be an array');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!config.temporaryPatterns || !Array.isArray(config.temporaryPatterns)) {
|
|
157
|
+
errors.push('temporaryPatterns must be an array');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check array contents
|
|
161
|
+
if (config.rootAllowedFiles && Array.isArray(config.rootAllowedFiles)) {
|
|
162
|
+
if (config.rootAllowedFiles.some(f => typeof f !== 'string')) {
|
|
163
|
+
errors.push('rootAllowedFiles must contain only strings');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (config.specSubdirs && Array.isArray(config.specSubdirs)) {
|
|
168
|
+
if (config.specSubdirs.some(d => typeof d !== 'string')) {
|
|
169
|
+
errors.push('specSubdirs must contain only strings');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (config.temporaryPatterns && Array.isArray(config.temporaryPatterns)) {
|
|
174
|
+
if (config.temporaryPatterns.some(p => typeof p !== 'string')) {
|
|
175
|
+
errors.push('temporaryPatterns must contain only strings');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
valid: errors.length === 0,
|
|
181
|
+
errors
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = ConfigManager;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic Engine
|
|
3
|
+
*
|
|
4
|
+
* Scans and analyzes project documentation structure
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const FileScanner = require('./file-scanner');
|
|
9
|
+
const ConfigManager = require('./config-manager');
|
|
10
|
+
|
|
11
|
+
class DiagnosticEngine {
|
|
12
|
+
constructor(projectPath, config) {
|
|
13
|
+
this.projectPath = projectPath;
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.scanner = new FileScanner(projectPath);
|
|
16
|
+
this.violations = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run full diagnostic scan
|
|
21
|
+
*
|
|
22
|
+
* @returns {Promise<DiagnosticReport>}
|
|
23
|
+
*/
|
|
24
|
+
async scan() {
|
|
25
|
+
this.violations = []; // Reset violations
|
|
26
|
+
|
|
27
|
+
await this.scanRootDirectory();
|
|
28
|
+
await this.scanSpecDirectories();
|
|
29
|
+
|
|
30
|
+
return this.generateReport();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scan root directory for violations
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
async scanRootDirectory() {
|
|
39
|
+
const mdFiles = await this.scanner.findMarkdownFiles(this.projectPath);
|
|
40
|
+
const allowedFiles = this.config.rootAllowedFiles || [];
|
|
41
|
+
|
|
42
|
+
for (const filePath of mdFiles) {
|
|
43
|
+
const basename = path.basename(filePath);
|
|
44
|
+
|
|
45
|
+
if (!allowedFiles.includes(basename)) {
|
|
46
|
+
// Check if it's a temporary document
|
|
47
|
+
const isTemporary = this.scanner.matchesPattern(filePath, this.config.temporaryPatterns || []);
|
|
48
|
+
|
|
49
|
+
this.violations.push({
|
|
50
|
+
type: 'root_violation',
|
|
51
|
+
path: filePath,
|
|
52
|
+
description: `Unexpected markdown file in root directory: ${basename}`,
|
|
53
|
+
severity: isTemporary ? 'warning' : 'error',
|
|
54
|
+
recommendation: isTemporary
|
|
55
|
+
? `Delete temporary file: ${basename}`
|
|
56
|
+
: `Move ${basename} to appropriate location or delete if temporary`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Scan all Spec directories
|
|
64
|
+
*
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async scanSpecDirectories() {
|
|
68
|
+
const specDirs = await this.scanner.findSpecDirectories();
|
|
69
|
+
|
|
70
|
+
for (const specDir of specDirs) {
|
|
71
|
+
await this.scanSpecDirectory(specDir);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Scan a single Spec directory
|
|
77
|
+
*
|
|
78
|
+
* @param {string} specPath - Path to Spec directory
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
async scanSpecDirectory(specPath) {
|
|
82
|
+
const specName = path.basename(specPath);
|
|
83
|
+
const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
|
|
84
|
+
|
|
85
|
+
// Check for missing required files
|
|
86
|
+
for (const requiredFile of requiredFiles) {
|
|
87
|
+
const filePath = path.join(specPath, requiredFile);
|
|
88
|
+
const exists = await this.scanner.exists(filePath);
|
|
89
|
+
|
|
90
|
+
if (!exists) {
|
|
91
|
+
this.violations.push({
|
|
92
|
+
type: 'missing_required_file',
|
|
93
|
+
path: filePath,
|
|
94
|
+
description: `Missing required file in Spec ${specName}: ${requiredFile}`,
|
|
95
|
+
severity: 'error',
|
|
96
|
+
recommendation: `Create ${requiredFile} in ${specName}`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check for temporary documents in Spec directory
|
|
102
|
+
const mdFiles = await this.scanner.findMarkdownFiles(specPath);
|
|
103
|
+
const temporaryFiles = this.scanner.matchPatterns(mdFiles, this.config.temporaryPatterns || []);
|
|
104
|
+
|
|
105
|
+
for (const tempFile of temporaryFiles) {
|
|
106
|
+
const basename = path.basename(tempFile);
|
|
107
|
+
|
|
108
|
+
// Don't flag required files as temporary even if they match patterns
|
|
109
|
+
if (!requiredFiles.includes(basename)) {
|
|
110
|
+
this.violations.push({
|
|
111
|
+
type: 'temporary_document',
|
|
112
|
+
path: tempFile,
|
|
113
|
+
description: `Temporary document should be deleted: ${basename}`,
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
recommendation: `Delete temporary file: ${basename} from ${specName}`
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for misplaced artifacts (files not in subdirectories)
|
|
121
|
+
const allFiles = await this.scanner.getFiles(specPath);
|
|
122
|
+
|
|
123
|
+
for (const filePath of allFiles) {
|
|
124
|
+
const basename = path.basename(filePath);
|
|
125
|
+
|
|
126
|
+
// Skip required files
|
|
127
|
+
if (requiredFiles.includes(basename)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Skip temporary files (already flagged as temporary_document)
|
|
132
|
+
if (this.scanner.matchesPattern(filePath, this.config.temporaryPatterns || [])) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// This is a misplaced artifact
|
|
137
|
+
this.violations.push({
|
|
138
|
+
type: 'misplaced_artifact',
|
|
139
|
+
path: filePath,
|
|
140
|
+
description: `Artifact not in subdirectory: ${basename}`,
|
|
141
|
+
severity: 'warning',
|
|
142
|
+
recommendation: `Move ${basename} to appropriate subdirectory (${this.config.specSubdirs.join(', ')})`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check subdirectory naming
|
|
147
|
+
const subdirs = await this.scanner.getSubdirectories(specPath);
|
|
148
|
+
const allowedSubdirs = this.config.specSubdirs || [];
|
|
149
|
+
|
|
150
|
+
for (const subdirPath of subdirs) {
|
|
151
|
+
const subdirName = path.basename(subdirPath);
|
|
152
|
+
|
|
153
|
+
// Skip hidden directories
|
|
154
|
+
if (subdirName.startsWith('.')) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!allowedSubdirs.includes(subdirName)) {
|
|
159
|
+
this.violations.push({
|
|
160
|
+
type: 'invalid_subdirectory',
|
|
161
|
+
path: subdirPath,
|
|
162
|
+
description: `Non-standard subdirectory in Spec ${specName}: ${subdirName}`,
|
|
163
|
+
severity: 'info',
|
|
164
|
+
recommendation: `Rename to one of: ${allowedSubdirs.join(', ')}, or remove if not needed`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Generate diagnostic report
|
|
172
|
+
*
|
|
173
|
+
* @returns {DiagnosticReport}
|
|
174
|
+
*/
|
|
175
|
+
generateReport() {
|
|
176
|
+
const compliant = this.violations.length === 0;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
compliant,
|
|
180
|
+
violations: this.violations,
|
|
181
|
+
summary: this.generateSummary(),
|
|
182
|
+
recommendations: this.generateRecommendations()
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Generate summary statistics
|
|
188
|
+
*
|
|
189
|
+
* @returns {Object}
|
|
190
|
+
*/
|
|
191
|
+
generateSummary() {
|
|
192
|
+
const summary = {
|
|
193
|
+
totalViolations: this.violations.length,
|
|
194
|
+
byType: {},
|
|
195
|
+
bySeverity: {
|
|
196
|
+
error: 0,
|
|
197
|
+
warning: 0,
|
|
198
|
+
info: 0
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Count by type
|
|
203
|
+
for (const violation of this.violations) {
|
|
204
|
+
summary.byType[violation.type] = (summary.byType[violation.type] || 0) + 1;
|
|
205
|
+
summary.bySeverity[violation.severity] = (summary.bySeverity[violation.severity] || 0) + 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return summary;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Generate actionable recommendations
|
|
213
|
+
*
|
|
214
|
+
* @returns {string[]}
|
|
215
|
+
*/
|
|
216
|
+
generateRecommendations() {
|
|
217
|
+
const recommendations = [];
|
|
218
|
+
const summary = this.generateSummary();
|
|
219
|
+
|
|
220
|
+
// Root violations
|
|
221
|
+
if (summary.byType.root_violation > 0) {
|
|
222
|
+
recommendations.push('Run `kse cleanup` to remove temporary files from root directory');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Temporary documents
|
|
226
|
+
if (summary.byType.temporary_document > 0) {
|
|
227
|
+
recommendations.push('Run `kse cleanup` to remove temporary documents from Spec directories');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Misplaced artifacts
|
|
231
|
+
if (summary.byType.misplaced_artifact > 0) {
|
|
232
|
+
recommendations.push('Run `kse archive --spec <spec-name>` to organize artifacts into subdirectories');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Missing required files
|
|
236
|
+
if (summary.byType.missing_required_file > 0) {
|
|
237
|
+
recommendations.push('Create missing required files (requirements.md, design.md, tasks.md) in affected Specs');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Invalid subdirectories
|
|
241
|
+
if (summary.byType.invalid_subdirectory > 0) {
|
|
242
|
+
recommendations.push('Rename non-standard subdirectories to match allowed names');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// General recommendation
|
|
246
|
+
if (recommendations.length > 0) {
|
|
247
|
+
recommendations.push('Run `kse validate --all` after fixes to confirm compliance');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return recommendations;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @typedef {Object} DiagnosticReport
|
|
256
|
+
* @property {boolean} compliant - Whether project is compliant
|
|
257
|
+
* @property {Violation[]} violations - List of violations found
|
|
258
|
+
* @property {Object} summary - Summary statistics
|
|
259
|
+
* @property {string[]} recommendations - Actionable recommendations
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @typedef {Object} Violation
|
|
264
|
+
* @property {string} type - Violation type (root_violation, spec_violation, etc.)
|
|
265
|
+
* @property {string} path - File or directory path
|
|
266
|
+
* @property {string} description - Human-readable description
|
|
267
|
+
* @property {string} severity - Severity level (error, warning, info)
|
|
268
|
+
* @property {string} recommendation - How to fix
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
module.exports = DiagnosticEngine;
|