kiro-spec-engine 1.4.4 → 1.5.5

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.
@@ -1,16 +1,19 @@
1
1
  const chalk = require('chalk');
2
2
  const pythonChecker = require('../python-checker');
3
3
  const { getI18n } = require('../i18n');
4
+ const DiagnosticEngine = require('../governance/diagnostic-engine');
5
+ const ConfigManager = require('../governance/config-manager');
6
+ const path = require('path');
4
7
 
5
8
  /**
6
9
  * CLI Doctor Command Component
7
10
  *
8
11
  * Verifies system requirements and provides diagnostics.
9
- * Checks Node.js version and Python availability.
12
+ * Checks Node.js version, Python availability, and document compliance.
10
13
  *
11
- * Requirements: 7.5
14
+ * Requirements: 7.5, 10.2
12
15
  */
13
- function doctorCommand() {
16
+ async function doctorCommand(options = {}) {
14
17
  const i18n = getI18n();
15
18
 
16
19
  console.log(chalk.red('🔥') + ' ' + i18n.t('cli.commands.doctor.title'));
@@ -43,6 +46,13 @@ function doctorCommand() {
43
46
  console.log(chalk.blue('─'.repeat(60)));
44
47
  console.log();
45
48
 
49
+ // Check document compliance
50
+ await checkDocumentCompliance(options);
51
+
52
+ console.log();
53
+ console.log(chalk.blue('─'.repeat(60)));
54
+ console.log();
55
+
46
56
  // Summary
47
57
  if (pythonStatus.available) {
48
58
  console.log(chalk.green('✅ ' + i18n.t('cli.commands.doctor.all_good')));
@@ -56,4 +66,132 @@ function doctorCommand() {
56
66
  console.log();
57
67
  }
58
68
 
69
+ /**
70
+ * Check document compliance
71
+ *
72
+ * @param {Object} options - Command options
73
+ * @param {boolean} options.docs - Whether to show detailed diagnostics
74
+ */
75
+ async function checkDocumentCompliance(options = {}) {
76
+ try {
77
+ const projectPath = process.cwd();
78
+
79
+ // Load configuration
80
+ const configManager = new ConfigManager(projectPath);
81
+ const config = await configManager.load();
82
+
83
+ // Run diagnostic scan
84
+ const diagnosticEngine = new DiagnosticEngine(projectPath, config);
85
+ const report = await diagnosticEngine.scan();
86
+
87
+ // Display results based on options
88
+ if (options.docs) {
89
+ // Detailed diagnostics mode
90
+ displayDetailedDiagnostics(report);
91
+ } else {
92
+ // Brief compliance status
93
+ displayBriefCompliance(report);
94
+ }
95
+ } catch (error) {
96
+ console.log(chalk.yellow('⚠️ Document compliance check failed:'), error.message);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Display brief document compliance status
102
+ *
103
+ * @param {Object} report - Diagnostic report
104
+ */
105
+ function displayBriefCompliance(report) {
106
+ console.log(chalk.bold('Document Compliance:'));
107
+
108
+ if (report.compliant) {
109
+ console.log(chalk.green(' ✓ All documents follow lifecycle management rules'));
110
+ } else {
111
+ const errorCount = report.summary.bySeverity.error || 0;
112
+ const warningCount = report.summary.bySeverity.warning || 0;
113
+
114
+ if (errorCount > 0) {
115
+ console.log(chalk.red(` ✗ ${errorCount} error(s) found`));
116
+ }
117
+ if (warningCount > 0) {
118
+ console.log(chalk.yellow(` ⚠ ${warningCount} warning(s) found`));
119
+ }
120
+
121
+ console.log(chalk.cyan(` → Run 'kse doctor --docs' for detailed diagnostics`));
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Display detailed document diagnostics
127
+ *
128
+ * @param {Object} report - Diagnostic report
129
+ */
130
+ function displayDetailedDiagnostics(report) {
131
+ console.log(chalk.bold.cyan('Document Governance Diagnostic:'));
132
+ console.log();
133
+
134
+ if (report.compliant) {
135
+ console.log(chalk.green(' ✅ Project is compliant'));
136
+ console.log(' All documents follow the lifecycle management rules.');
137
+ return;
138
+ }
139
+
140
+ console.log(chalk.yellow(` ⚠️ Found ${report.violations.length} violation(s)`));
141
+ console.log();
142
+
143
+ // Group violations by type
144
+ const byType = groupBy(report.violations, 'type');
145
+
146
+ for (const [type, violations] of Object.entries(byType)) {
147
+ console.log(chalk.blue(` ${formatType(type)} (${violations.length})`));
148
+
149
+ for (const violation of violations) {
150
+ const icon = violation.severity === 'error' ? '❌' : '⚠️';
151
+ const relativePath = path.relative(process.cwd(), violation.path);
152
+ console.log(` ${icon} ${chalk.gray(relativePath)}`);
153
+ console.log(` ${violation.description}`);
154
+ console.log(` ${chalk.cyan('→ ' + violation.recommendation)}`);
155
+ }
156
+
157
+ console.log();
158
+ }
159
+
160
+ // Display recommendations
161
+ if (report.recommendations && report.recommendations.length > 0) {
162
+ console.log(chalk.blue(' 💡 Recommended Actions:'));
163
+ report.recommendations.forEach(rec => {
164
+ console.log(` • ${rec}`);
165
+ });
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Helper: Group array by property
171
+ *
172
+ * @param {Array} array - Array to group
173
+ * @param {string} property - Property to group by
174
+ * @returns {Object} - Grouped object
175
+ */
176
+ function groupBy(array, property) {
177
+ return array.reduce((acc, item) => {
178
+ const key = item[property];
179
+ if (!acc[key]) acc[key] = [];
180
+ acc[key].push(item);
181
+ return acc;
182
+ }, {});
183
+ }
184
+
185
+ /**
186
+ * Helper: Format violation type
187
+ *
188
+ * @param {string} type - Violation type
189
+ * @returns {string} - Formatted type
190
+ */
191
+ function formatType(type) {
192
+ return type.split('_').map(word =>
193
+ word.charAt(0).toUpperCase() + word.slice(1)
194
+ ).join(' ');
195
+ }
196
+
59
197
  module.exports = doctorCommand;
@@ -9,6 +9,8 @@ const path = require('path');
9
9
  const fs = require('fs-extra');
10
10
  const TaskClaimer = require('../task/task-claimer');
11
11
  const WorkspaceManager = require('../workspace/workspace-manager');
12
+ const DiagnosticEngine = require('../governance/diagnostic-engine');
13
+ const ConfigManager = require('../governance/config-manager');
12
14
 
13
15
  /**
14
16
  * Executes the status command
@@ -57,7 +59,10 @@ async function statusCommand(options = {}) {
57
59
 
58
60
  console.log();
59
61
 
60
- // 3. List specs
62
+ // 3. Document compliance status
63
+ await displayDocumentCompliance(projectPath);
64
+
65
+ // 4. List specs
61
66
  const specsPath = path.join(projectPath, '.kiro/specs');
62
67
  const specsExist = await fs.pathExists(specsPath);
63
68
 
@@ -83,7 +88,7 @@ async function statusCommand(options = {}) {
83
88
  console.log(chalk.blue(`📁 Specs (${specDirs.length})`));
84
89
  console.log();
85
90
 
86
- // 4. Analyze each spec
91
+ // 5. Analyze each spec
87
92
  const taskClaimer = new TaskClaimer();
88
93
  const allClaimedTasks = [];
89
94
 
@@ -145,7 +150,7 @@ async function statusCommand(options = {}) {
145
150
  console.log();
146
151
  }
147
152
 
148
- // 5. Team activity view
153
+ // 6. Team activity view
149
154
  if (team && allClaimedTasks.length > 0) {
150
155
  console.log(chalk.blue('👥 Team Activity'));
151
156
  console.log();
@@ -186,7 +191,7 @@ async function statusCommand(options = {}) {
186
191
  });
187
192
  }
188
193
 
189
- // 6. Summary
194
+ // 7. Summary
190
195
  if (allClaimedTasks.length > 0) {
191
196
  const staleClaims = allClaimedTasks.filter(t => t.isStale);
192
197
 
@@ -202,7 +207,7 @@ async function statusCommand(options = {}) {
202
207
  }
203
208
  }
204
209
 
205
- // 7. Next steps
210
+ // 8. Next steps
206
211
  console.log(chalk.blue('💡 Commands'));
207
212
  console.log(' View team activity: ' + chalk.cyan('kse status --team'));
208
213
  console.log(' Detailed view: ' + chalk.cyan('kse status --verbose'));
@@ -222,4 +227,71 @@ async function statusCommand(options = {}) {
222
227
  }
223
228
  }
224
229
 
230
+ /**
231
+ * Display document compliance status
232
+ *
233
+ * @param {string} projectPath - Project root path
234
+ * @returns {Promise<void>}
235
+ */
236
+ async function displayDocumentCompliance(projectPath) {
237
+ try {
238
+ // Load configuration
239
+ const configManager = new ConfigManager(projectPath);
240
+ await configManager.load();
241
+
242
+ // Run diagnostic scan
243
+ const diagnosticEngine = new DiagnosticEngine(projectPath, configManager.config);
244
+ const report = await diagnosticEngine.scan();
245
+
246
+ // Display compliance status
247
+ console.log(chalk.blue('📄 Document Compliance'));
248
+
249
+ if (report.compliant) {
250
+ console.log(` Status: ${chalk.green('✅ Compliant')}`);
251
+ console.log(` ${chalk.gray('All documents follow lifecycle management rules')}`);
252
+ } else {
253
+ const errorCount = report.violations.filter(v => v.severity === 'error').length;
254
+ const warningCount = report.violations.filter(v => v.severity === 'warning').length;
255
+
256
+ console.log(` Status: ${chalk.red('❌ Non-Compliant')}`);
257
+ console.log(` Violations: ${chalk.red(errorCount)} error(s), ${chalk.yellow(warningCount)} warning(s)`);
258
+
259
+ // Show violation breakdown by type
260
+ const byType = {};
261
+ report.violations.forEach(v => {
262
+ byType[v.type] = (byType[v.type] || 0) + 1;
263
+ });
264
+
265
+ const typeNames = {
266
+ 'root_violation': 'Root directory',
267
+ 'spec_violation': 'Spec structure',
268
+ 'missing_file': 'Missing files',
269
+ 'misplaced_artifact': 'Misplaced artifacts',
270
+ 'temporary_document': 'Temporary documents'
271
+ };
272
+
273
+ Object.entries(byType).forEach(([type, count]) => {
274
+ const name = typeNames[type] || type;
275
+ console.log(` • ${name}: ${count}`);
276
+ });
277
+
278
+ console.log();
279
+ console.log(chalk.cyan(' Quick Fix Commands:'));
280
+ console.log(` ${chalk.gray('•')} Run diagnostics: ${chalk.cyan('kse doctor --docs')}`);
281
+ console.log(` ${chalk.gray('•')} Clean temporary files: ${chalk.cyan('kse cleanup')}`);
282
+ console.log(` ${chalk.gray('•')} Validate structure: ${chalk.cyan('kse validate --all')}`);
283
+ console.log(` ${chalk.gray('•')} Archive artifacts: ${chalk.cyan('kse archive --spec <name>')}`);
284
+ }
285
+
286
+ console.log();
287
+ } catch (error) {
288
+ // Silently skip if governance components not available
289
+ // This allows status to work even if governance is not fully set up
290
+ if (error.code !== 'MODULE_NOT_FOUND') {
291
+ console.log(chalk.gray(' (Document compliance check skipped)'));
292
+ console.log();
293
+ }
294
+ }
295
+ }
296
+
225
297
  module.exports = statusCommand;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Archive Tool
3
+ *
4
+ * Organizes Spec artifacts into proper subdirectories
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+ const FileScanner = require('./file-scanner');
10
+
11
+ class ArchiveTool {
12
+ constructor(projectPath, config) {
13
+ this.projectPath = projectPath;
14
+ this.config = config;
15
+ this.scanner = new FileScanner(projectPath);
16
+ this.movedFiles = [];
17
+ this.errors = [];
18
+ }
19
+
20
+ /**
21
+ * Archive artifacts in a Spec directory
22
+ *
23
+ * @param {string} specName - Spec name
24
+ * @param {Object} options - Archive options
25
+ * @param {boolean} options.dryRun - Preview without moving
26
+ * @returns {Promise<ArchiveReport>}
27
+ */
28
+ async archive(specName, options = {}) {
29
+ const specPath = this.scanner.getSpecDirectory(specName);
30
+
31
+ // Check if Spec directory exists
32
+ if (!await this.scanner.exists(specPath)) {
33
+ this.errors.push({
34
+ path: specPath,
35
+ error: `Spec directory does not exist: ${specName}`
36
+ });
37
+ return this.generateReport();
38
+ }
39
+
40
+ const artifacts = await this.identifyArtifacts(specPath);
41
+
42
+ if (options.dryRun) {
43
+ return this.generateDryRunReport(artifacts);
44
+ }
45
+
46
+ for (const artifact of artifacts) {
47
+ await this.moveArtifact(artifact);
48
+ }
49
+
50
+ return this.generateReport();
51
+ }
52
+
53
+ /**
54
+ * Identify artifacts to archive
55
+ *
56
+ * @param {string} specPath - Path to Spec directory
57
+ * @returns {Promise<Artifact[]>}
58
+ */
59
+ async identifyArtifacts(specPath) {
60
+ const artifacts = [];
61
+ const files = await this.scanner.getFiles(specPath);
62
+ const requiredFiles = ['requirements.md', 'design.md', 'tasks.md'];
63
+
64
+ for (const filePath of files) {
65
+ const basename = path.basename(filePath);
66
+
67
+ // Skip required files
68
+ if (requiredFiles.includes(basename)) {
69
+ continue;
70
+ }
71
+
72
+ const targetSubdir = this.determineTargetSubdir(basename);
73
+ artifacts.push({
74
+ sourcePath: filePath,
75
+ targetSubdir: targetSubdir,
76
+ filename: basename
77
+ });
78
+ }
79
+
80
+ return artifacts;
81
+ }
82
+
83
+ /**
84
+ * Determine target subdirectory for a file
85
+ *
86
+ * @param {string} filename - File name
87
+ * @returns {string}
88
+ */
89
+ determineTargetSubdir(filename) {
90
+ const lower = filename.toLowerCase();
91
+
92
+ // Results - check for result keywords FIRST (before test patterns)
93
+ // This ensures "test-results.json" goes to results, not tests
94
+ if (lower.includes('result') || lower.includes('output')) {
95
+ return 'results';
96
+ }
97
+
98
+ // Tests - check for test patterns (test files, not result files)
99
+ if (lower.endsWith('.test.js') || lower.endsWith('.spec.js') ||
100
+ lower.includes('.test.') || lower.includes('.spec.') ||
101
+ (lower.includes('test') && (lower.endsWith('.js') || lower.endsWith('.py')))) {
102
+ return 'tests';
103
+ }
104
+
105
+ // Scripts - check for script extensions and keywords
106
+ if (lower.endsWith('.js') || lower.endsWith('.py') ||
107
+ lower.endsWith('.sh') || lower.endsWith('.bat') ||
108
+ lower.includes('script')) {
109
+ return 'scripts';
110
+ }
111
+
112
+ // Reports - check for report keywords
113
+ if (lower.includes('report') || lower.includes('analysis') ||
114
+ lower.includes('summary') || lower.includes('diagnostic')) {
115
+ return 'reports';
116
+ }
117
+
118
+ // Data files
119
+ if (lower.includes('data')) {
120
+ return 'results';
121
+ }
122
+
123
+ // Default to docs for markdown and other documentation files
124
+ return 'docs';
125
+ }
126
+
127
+ /**
128
+ * Move an artifact to its target subdirectory
129
+ *
130
+ * @param {Artifact} artifact - Artifact to move
131
+ * @returns {Promise<void>}
132
+ */
133
+ async moveArtifact(artifact) {
134
+ try {
135
+ const targetDir = path.join(
136
+ path.dirname(artifact.sourcePath),
137
+ artifact.targetSubdir
138
+ );
139
+
140
+ // Ensure target directory exists
141
+ await fs.ensureDir(targetDir);
142
+
143
+ const targetPath = path.join(targetDir, artifact.filename);
144
+
145
+ // Check if target file already exists
146
+ if (await fs.pathExists(targetPath)) {
147
+ this.errors.push({
148
+ path: artifact.sourcePath,
149
+ error: `Target file already exists: ${targetPath}`
150
+ });
151
+ return;
152
+ }
153
+
154
+ await fs.move(artifact.sourcePath, targetPath);
155
+
156
+ this.movedFiles.push({
157
+ from: artifact.sourcePath,
158
+ to: targetPath
159
+ });
160
+ } catch (error) {
161
+ this.errors.push({
162
+ path: artifact.sourcePath,
163
+ error: error.message
164
+ });
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Generate dry run report
170
+ *
171
+ * @param {Artifact[]} artifacts - Artifacts that would be moved
172
+ * @returns {ArchiveReport}
173
+ */
174
+ generateDryRunReport(artifacts) {
175
+ const movedFiles = artifacts.map(artifact => ({
176
+ from: artifact.sourcePath,
177
+ to: path.join(
178
+ path.dirname(artifact.sourcePath),
179
+ artifact.targetSubdir,
180
+ artifact.filename
181
+ )
182
+ }));
183
+
184
+ return {
185
+ success: true,
186
+ movedFiles: movedFiles,
187
+ errors: [],
188
+ summary: {
189
+ totalMoved: movedFiles.length,
190
+ totalErrors: 0
191
+ },
192
+ dryRun: true
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Generate archive report
198
+ *
199
+ * @returns {ArchiveReport}
200
+ */
201
+ generateReport() {
202
+ return {
203
+ success: this.errors.length === 0,
204
+ movedFiles: this.movedFiles,
205
+ errors: this.errors,
206
+ summary: {
207
+ totalMoved: this.movedFiles.length,
208
+ totalErrors: this.errors.length
209
+ },
210
+ dryRun: false
211
+ };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @typedef {Object} Artifact
217
+ * @property {string} sourcePath - Current file path
218
+ * @property {string} targetSubdir - Target subdirectory name
219
+ * @property {string} filename - File name
220
+ */
221
+
222
+ /**
223
+ * @typedef {Object} ArchiveReport
224
+ * @property {boolean} success - Whether archiving succeeded
225
+ * @property {Object[]} movedFiles - Files that were moved
226
+ * @property {Object[]} errors - Errors encountered
227
+ * @property {Object} summary - Summary statistics
228
+ * @property {boolean} dryRun - Whether this was a dry run
229
+ */
230
+
231
+ module.exports = ArchiveTool;