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.
@@ -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;