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,333 @@
1
+ /**
2
+ * Hooks Manager
3
+ *
4
+ * Manages Git hooks for document governance
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+
10
+ class HooksManager {
11
+ constructor(projectPath) {
12
+ this.projectPath = projectPath;
13
+ this.gitDir = path.join(projectPath, '.git');
14
+ this.hooksDir = path.join(this.gitDir, 'hooks');
15
+ this.preCommitPath = path.join(this.hooksDir, 'pre-commit');
16
+ this.backupPath = path.join(this.hooksDir, 'pre-commit.backup');
17
+
18
+ // Marker to identify our hook content
19
+ this.hookMarkerStart = '# BEGIN kiro-spec-engine document governance';
20
+ this.hookMarkerEnd = '# END kiro-spec-engine document governance';
21
+ }
22
+
23
+ /**
24
+ * Check if Git hooks are installed
25
+ *
26
+ * @returns {Promise<Object>}
27
+ */
28
+ async checkHooksInstalled() {
29
+ try {
30
+ // Check if .git directory exists
31
+ if (!await fs.pathExists(this.gitDir)) {
32
+ return {
33
+ installed: false,
34
+ reason: 'not_git_repo',
35
+ message: 'Not a Git repository'
36
+ };
37
+ }
38
+
39
+ // Check if hooks directory exists
40
+ if (!await fs.pathExists(this.hooksDir)) {
41
+ return {
42
+ installed: false,
43
+ reason: 'no_hooks_dir',
44
+ message: 'Git hooks directory does not exist'
45
+ };
46
+ }
47
+
48
+ // Check if pre-commit hook exists
49
+ if (!await fs.pathExists(this.preCommitPath)) {
50
+ return {
51
+ installed: false,
52
+ reason: 'no_hook',
53
+ message: 'Pre-commit hook not installed'
54
+ };
55
+ }
56
+
57
+ // Check if our hook content is present
58
+ const content = await fs.readFile(this.preCommitPath, 'utf8');
59
+ const hasOurHook = content.includes(this.hookMarkerStart);
60
+
61
+ return {
62
+ installed: hasOurHook,
63
+ reason: hasOurHook ? 'installed' : 'other_hook',
64
+ message: hasOurHook
65
+ ? 'Document governance hook is installed'
66
+ : 'Pre-commit hook exists but is not ours',
67
+ hasExistingHook: !hasOurHook
68
+ };
69
+ } catch (error) {
70
+ return {
71
+ installed: false,
72
+ reason: 'error',
73
+ message: `Error checking hooks: ${error.message}`,
74
+ error: error.message
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Install pre-commit hook
81
+ *
82
+ * @returns {Promise<Object>}
83
+ */
84
+ async installHooks() {
85
+ try {
86
+ // Check if .git directory exists
87
+ if (!await fs.pathExists(this.gitDir)) {
88
+ return {
89
+ success: false,
90
+ reason: 'not_git_repo',
91
+ message: 'Not a Git repository. Initialize Git first with: git init'
92
+ };
93
+ }
94
+
95
+ // Create hooks directory if it doesn't exist
96
+ if (!await fs.pathExists(this.hooksDir)) {
97
+ await fs.ensureDir(this.hooksDir);
98
+ }
99
+
100
+ // Check if pre-commit hook already exists
101
+ let existingContent = '';
102
+ let hasExistingHook = false;
103
+
104
+ if (await fs.pathExists(this.preCommitPath)) {
105
+ existingContent = await fs.readFile(this.preCommitPath, 'utf8');
106
+
107
+ // Check if our hook is already installed
108
+ if (existingContent.includes(this.hookMarkerStart)) {
109
+ return {
110
+ success: true,
111
+ reason: 'already_installed',
112
+ message: 'Document governance hook is already installed'
113
+ };
114
+ }
115
+
116
+ hasExistingHook = true;
117
+
118
+ // Backup existing hook
119
+ await fs.writeFile(this.backupPath, existingContent);
120
+ }
121
+
122
+ // Generate our hook content
123
+ const ourHookContent = this.generateHookContent();
124
+
125
+ // Combine with existing hook if present
126
+ let finalContent;
127
+ if (hasExistingHook) {
128
+ // Preserve existing hook and add ours
129
+ finalContent = this.combineHooks(existingContent, ourHookContent);
130
+ } else {
131
+ // Just use our hook with shebang
132
+ finalContent = `#!/bin/sh\n\n${ourHookContent}`;
133
+ }
134
+
135
+ // Write the hook file
136
+ await fs.writeFile(this.preCommitPath, finalContent);
137
+
138
+ // Make it executable (Unix-like systems)
139
+ if (process.platform !== 'win32') {
140
+ await fs.chmod(this.preCommitPath, 0o755);
141
+ }
142
+
143
+ return {
144
+ success: true,
145
+ reason: hasExistingHook ? 'installed_with_preservation' : 'installed',
146
+ message: hasExistingHook
147
+ ? 'Document governance hook installed. Existing hook preserved and backed up.'
148
+ : 'Document governance hook installed successfully.',
149
+ backupCreated: hasExistingHook
150
+ };
151
+ } catch (error) {
152
+ return {
153
+ success: false,
154
+ reason: 'error',
155
+ message: `Failed to install hooks: ${error.message}`,
156
+ error: error.message
157
+ };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Uninstall pre-commit hook
163
+ *
164
+ * @returns {Promise<Object>}
165
+ */
166
+ async uninstallHooks() {
167
+ try {
168
+ // Check if hook exists
169
+ if (!await fs.pathExists(this.preCommitPath)) {
170
+ return {
171
+ success: true,
172
+ reason: 'not_installed',
173
+ message: 'Document governance hook is not installed'
174
+ };
175
+ }
176
+
177
+ // Read current hook content
178
+ const content = await fs.readFile(this.preCommitPath, 'utf8');
179
+
180
+ // Check if our hook is present
181
+ if (!content.includes(this.hookMarkerStart)) {
182
+ return {
183
+ success: false,
184
+ reason: 'not_our_hook',
185
+ message: 'Pre-commit hook exists but is not ours. Manual removal required.'
186
+ };
187
+ }
188
+
189
+ // Remove our hook content
190
+ const newContent = this.removeOurHook(content);
191
+
192
+ // If nothing left (or just shebang), remove the file
193
+ const trimmedContent = newContent.trim();
194
+ if (trimmedContent === '' || trimmedContent === '#!/bin/sh') {
195
+ await fs.remove(this.preCommitPath);
196
+
197
+ // Restore backup if it exists
198
+ if (await fs.pathExists(this.backupPath)) {
199
+ await fs.remove(this.backupPath);
200
+ }
201
+
202
+ return {
203
+ success: true,
204
+ reason: 'removed',
205
+ message: 'Document governance hook removed successfully.'
206
+ };
207
+ } else {
208
+ // Write back the remaining content
209
+ await fs.writeFile(this.preCommitPath, newContent);
210
+
211
+ return {
212
+ success: true,
213
+ reason: 'removed_preserved',
214
+ message: 'Document governance hook removed. Other hooks preserved.'
215
+ };
216
+ }
217
+ } catch (error) {
218
+ return {
219
+ success: false,
220
+ reason: 'error',
221
+ message: `Failed to uninstall hooks: ${error.message}`,
222
+ error: error.message
223
+ };
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Generate hook content
229
+ *
230
+ * @returns {string}
231
+ */
232
+ generateHookContent() {
233
+ // Use Node.js to run validation
234
+ // This works on both Windows and Unix-like systems
235
+ return `${this.hookMarkerStart}
236
+
237
+ # Run document governance validation
238
+ node -e "
239
+ const path = require('path');
240
+ const ConfigManager = require('./lib/governance/config-manager');
241
+ const ValidationEngine = require('./lib/governance/validation-engine');
242
+
243
+ (async () => {
244
+ try {
245
+ const projectPath = process.cwd();
246
+ const configManager = new ConfigManager(projectPath);
247
+ const config = await configManager.load();
248
+ const validator = new ValidationEngine(projectPath, config);
249
+
250
+ // Validate root directory and all specs
251
+ const report = await validator.validate({ all: true });
252
+
253
+ if (!report.valid) {
254
+ console.error('\\nāŒ Document governance validation failed!\\n');
255
+ console.error('Found ' + report.errors.length + ' error(s):\\n');
256
+
257
+ report.errors.forEach(err => {
258
+ console.error(' • ' + err.path);
259
+ console.error(' ' + err.message);
260
+ console.error(' → ' + err.recommendation + '\\n');
261
+ });
262
+
263
+ console.error('Fix these issues before committing:');
264
+ console.error(' kse doctor --docs # Diagnose issues');
265
+ console.error(' kse cleanup # Remove temporary files');
266
+ console.error(' kse validate --all # Validate structure\\n');
267
+
268
+ process.exit(1);
269
+ }
270
+ } catch (error) {
271
+ console.error('Error running document validation:', error.message);
272
+ // Don't block commit on validation errors
273
+ process.exit(0);
274
+ }
275
+ })();
276
+ "
277
+
278
+ ${this.hookMarkerEnd}
279
+ `;
280
+ }
281
+
282
+ /**
283
+ * Combine existing hook with our hook
284
+ *
285
+ * @param {string} existingContent - Existing hook content
286
+ * @param {string} ourContent - Our hook content
287
+ * @returns {string}
288
+ */
289
+ combineHooks(existingContent, ourContent) {
290
+ // Ensure shebang is present
291
+ let combined = existingContent;
292
+
293
+ if (!combined.startsWith('#!/')) {
294
+ combined = '#!/bin/sh\n\n' + combined;
295
+ }
296
+
297
+ // Add our hook at the end
298
+ combined += '\n\n' + ourContent;
299
+
300
+ return combined;
301
+ }
302
+
303
+ /**
304
+ * Remove our hook from combined content
305
+ *
306
+ * @param {string} content - Hook content
307
+ * @returns {string}
308
+ */
309
+ removeOurHook(content) {
310
+ const startIndex = content.indexOf(this.hookMarkerStart);
311
+ const endIndex = content.indexOf(this.hookMarkerEnd);
312
+
313
+ if (startIndex === -1 || endIndex === -1) {
314
+ return content;
315
+ }
316
+
317
+ // Remove our hook section (including markers and newlines)
318
+ const before = content.substring(0, startIndex).trimEnd();
319
+ const after = content.substring(endIndex + this.hookMarkerEnd.length).trimStart();
320
+
321
+ if (before && after) {
322
+ return before + '\n\n' + after;
323
+ } else if (before) {
324
+ return before;
325
+ } else if (after) {
326
+ return after;
327
+ } else {
328
+ return '';
329
+ }
330
+ }
331
+ }
332
+
333
+ module.exports = HooksManager;
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Reporter
3
+ *
4
+ * Formats and displays governance operation results
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const path = require('path');
9
+
10
+ class Reporter {
11
+ constructor() {
12
+ this.chalk = chalk;
13
+ }
14
+
15
+ /**
16
+ * Display diagnostic report
17
+ *
18
+ * @param {DiagnosticReport} report - Diagnostic report
19
+ */
20
+ displayDiagnostic(report) {
21
+ console.log(this.chalk.bold.cyan('\nšŸ“‹ Document Governance Diagnostic\n'));
22
+
23
+ if (report.compliant) {
24
+ console.log(this.chalk.green('āœ… Project is compliant'));
25
+ console.log('All documents follow the lifecycle management rules.\n');
26
+ return;
27
+ }
28
+
29
+ console.log(this.chalk.yellow(`āš ļø Found ${report.violations.length} violation(s)\n`));
30
+
31
+ // Group violations by type
32
+ const byType = this.groupBy(report.violations, 'type');
33
+
34
+ for (const [type, violations] of Object.entries(byType)) {
35
+ console.log(this.chalk.bold.blue(`${this.formatType(type)} (${violations.length})`));
36
+
37
+ for (const violation of violations) {
38
+ const icon = violation.severity === 'error' ? 'āŒ' : 'āš ļø';
39
+ console.log(` ${icon} ${this.chalk.gray(violation.path)}`);
40
+ console.log(` ${violation.description}`);
41
+ console.log(` ${this.chalk.cyan('→ ' + violation.recommendation)}`);
42
+ }
43
+
44
+ console.log();
45
+ }
46
+
47
+ // Display recommendations
48
+ if (report.recommendations && report.recommendations.length > 0) {
49
+ console.log(this.chalk.bold.blue('šŸ’” Recommended Actions'));
50
+ report.recommendations.forEach(rec => {
51
+ console.log(` • ${rec}`);
52
+ });
53
+ console.log();
54
+ }
55
+
56
+ // Display summary
57
+ if (report.summary) {
58
+ console.log(this.chalk.bold('Summary:'));
59
+ console.log(` Total violations: ${report.summary.totalViolations}`);
60
+ if (report.summary.bySeverity) {
61
+ console.log(` Errors: ${report.summary.bySeverity.error || 0}`);
62
+ console.log(` Warnings: ${report.summary.bySeverity.warning || 0}`);
63
+ console.log(` Info: ${report.summary.bySeverity.info || 0}`);
64
+ }
65
+ console.log();
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Display cleanup report
71
+ *
72
+ * @param {CleanupReport} report - Cleanup report
73
+ * @param {boolean} dryRun - Whether this was a dry run
74
+ */
75
+ displayCleanup(report, dryRun = false) {
76
+ const title = dryRun ? 'Cleanup Preview (Dry Run)' : 'Cleanup Complete';
77
+ console.log(this.chalk.bold.cyan(`\n🧹 ${title}\n`));
78
+
79
+ if (report.deletedFiles.length === 0) {
80
+ console.log(this.chalk.green('āœ… No files to clean\n'));
81
+ return;
82
+ }
83
+
84
+ const verb = dryRun ? 'Would delete' : 'Deleted';
85
+ console.log(this.chalk.yellow(`${verb} ${report.deletedFiles.length} file(s):\n`));
86
+
87
+ report.deletedFiles.forEach(file => {
88
+ console.log(` šŸ—‘ļø ${this.chalk.gray(file)}`);
89
+ });
90
+
91
+ if (report.errors && report.errors.length > 0) {
92
+ console.log();
93
+ console.log(this.chalk.red(`āŒ ${report.errors.length} error(s):`));
94
+ report.errors.forEach(err => {
95
+ console.log(` • ${this.chalk.gray(err.path)}: ${err.error}`);
96
+ });
97
+ }
98
+
99
+ console.log();
100
+
101
+ if (dryRun) {
102
+ console.log(this.chalk.cyan('šŸ’” Run without --dry-run to actually delete these files\n'));
103
+ } else if (report.success) {
104
+ console.log(this.chalk.green('āœ… Cleanup completed successfully\n'));
105
+ } else {
106
+ console.log(this.chalk.yellow('āš ļø Cleanup completed with errors\n'));
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Display validation report
112
+ *
113
+ * @param {ValidationReport} report - Validation report
114
+ */
115
+ displayValidation(report) {
116
+ console.log(this.chalk.bold.cyan('\nāœ“ Document Structure Validation\n'));
117
+
118
+ // Check if there are any errors or warnings
119
+ const hasErrors = report.errors && report.errors.length > 0;
120
+ const hasWarnings = report.warnings && report.warnings.length > 0;
121
+
122
+ if (report.valid && !hasWarnings) {
123
+ console.log(this.chalk.green('āœ… Validation passed'));
124
+ console.log('All document structures are compliant.\n');
125
+ return;
126
+ }
127
+
128
+ if (hasErrors) {
129
+ console.log(this.chalk.red(`āŒ ${report.errors.length} error(s):\n`));
130
+
131
+ report.errors.forEach(err => {
132
+ console.log(` āŒ ${this.chalk.gray(err.path)}`);
133
+ console.log(` ${err.message}`);
134
+ console.log(` ${this.chalk.cyan('→ ' + err.recommendation)}`);
135
+ console.log();
136
+ });
137
+ }
138
+
139
+ if (hasWarnings) {
140
+ console.log(this.chalk.yellow(`āš ļø ${report.warnings.length} warning(s):\n`));
141
+
142
+ report.warnings.forEach(warn => {
143
+ console.log(` āš ļø ${this.chalk.gray(warn.path)}`);
144
+ console.log(` ${warn.message}`);
145
+ console.log(` ${this.chalk.cyan('→ ' + warn.recommendation)}`);
146
+ console.log();
147
+ });
148
+ }
149
+
150
+ // Display summary
151
+ if (report.summary) {
152
+ console.log(this.chalk.bold('Summary:'));
153
+ console.log(` Errors: ${report.summary.totalErrors}`);
154
+ console.log(` Warnings: ${report.summary.totalWarnings}`);
155
+ console.log();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Display archive report
161
+ *
162
+ * @param {ArchiveReport} report - Archive report
163
+ * @param {boolean} dryRun - Whether this was a dry run
164
+ */
165
+ displayArchive(report, dryRun = false) {
166
+ const title = dryRun ? 'Archive Preview (Dry Run)' : 'Archive Complete';
167
+ console.log(this.chalk.bold.cyan(`\nšŸ“¦ ${title}\n`));
168
+
169
+ // Check if there are errors even with no moved files
170
+ const hasErrors = report.errors && report.errors.length > 0;
171
+
172
+ if (report.movedFiles.length === 0 && !hasErrors) {
173
+ console.log(this.chalk.green('āœ… No files to archive\n'));
174
+ return;
175
+ }
176
+
177
+ if (report.movedFiles.length > 0) {
178
+ const verb = dryRun ? 'Would move' : 'Moved';
179
+ console.log(this.chalk.yellow(`${verb} ${report.movedFiles.length} file(s):\n`));
180
+
181
+ report.movedFiles.forEach(move => {
182
+ const fromBasename = path.basename(move.from);
183
+ const toRelative = this.chalk.gray(move.to);
184
+ console.log(` šŸ“¦ ${fromBasename}`);
185
+ console.log(` ${this.chalk.gray('→')} ${toRelative}`);
186
+ });
187
+
188
+ console.log();
189
+ }
190
+
191
+ if (hasErrors) {
192
+ console.log(this.chalk.red(`āŒ ${report.errors.length} error(s):`));
193
+ report.errors.forEach(err => {
194
+ console.log(` • ${this.chalk.gray(err.path)}: ${err.error}`);
195
+ });
196
+ console.log();
197
+ }
198
+
199
+ if (dryRun && report.movedFiles.length > 0) {
200
+ console.log(this.chalk.cyan('šŸ’” Run without --dry-run to actually move these files\n'));
201
+ } else if (report.success && report.movedFiles.length > 0) {
202
+ console.log(this.chalk.green('āœ… Archive completed successfully\n'));
203
+ } else if (!report.success) {
204
+ console.log(this.chalk.yellow('āš ļø Archive completed with errors\n'));
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Display error message
210
+ *
211
+ * @param {string} message - Error message
212
+ */
213
+ displayError(message) {
214
+ console.log(this.chalk.red(`\nāŒ Error: ${message}\n`));
215
+ }
216
+
217
+ /**
218
+ * Display statistics
219
+ *
220
+ * @param {Object} stats - Statistics object
221
+ */
222
+ displayStats(stats) {
223
+ console.log(this.chalk.bold.cyan('\nšŸ“Š Document Compliance Statistics\n'));
224
+
225
+ // Overall Summary
226
+ console.log(this.chalk.bold('Overall Summary:'));
227
+ console.log(` Total Executions: ${stats.totalExecutions}`);
228
+ console.log(` Total Violations Found: ${stats.totalViolations}`);
229
+ console.log(` Total Cleanup Actions: ${stats.totalCleanupActions}`);
230
+ console.log(` Total Archive Actions: ${stats.totalArchiveActions}`);
231
+ console.log(` Total Errors: ${stats.totalErrors}`);
232
+ console.log();
233
+
234
+ // Time Range
235
+ if (stats.firstExecution && stats.lastExecution) {
236
+ console.log(this.chalk.bold('Time Range:'));
237
+ console.log(` First Execution: ${new Date(stats.firstExecution).toLocaleString()}`);
238
+ console.log(` Last Execution: ${new Date(stats.lastExecution).toLocaleString()}`);
239
+ console.log();
240
+ }
241
+
242
+ // Executions by Tool
243
+ if (Object.keys(stats.executionsByTool).length > 0) {
244
+ console.log(this.chalk.bold('Executions by Tool:'));
245
+ Object.entries(stats.executionsByTool)
246
+ .sort((a, b) => b[1] - a[1])
247
+ .forEach(([tool, count]) => {
248
+ console.log(` ${tool}: ${count}`);
249
+ });
250
+ console.log();
251
+ }
252
+
253
+ // Violations by Type
254
+ if (Object.keys(stats.violationsByType).length > 0) {
255
+ console.log(this.chalk.bold('Violations by Type:'));
256
+ Object.entries(stats.violationsByType)
257
+ .sort((a, b) => b[1] - a[1])
258
+ .forEach(([type, count]) => {
259
+ console.log(` ${this.formatType(type)}: ${count}`);
260
+ });
261
+ console.log();
262
+ }
263
+
264
+ // Violations Over Time (last 5)
265
+ if (stats.violationsOverTime.length > 0) {
266
+ console.log(this.chalk.bold('Recent Violations:'));
267
+ const recent = stats.violationsOverTime.slice(-5);
268
+ recent.forEach(entry => {
269
+ const date = new Date(entry.timestamp).toLocaleString();
270
+ console.log(` ${date}: ${entry.count} violation(s)`);
271
+ });
272
+ console.log();
273
+ }
274
+
275
+ // Cleanup Actions Over Time (last 5)
276
+ if (stats.cleanupActionsOverTime.length > 0) {
277
+ console.log(this.chalk.bold('Recent Cleanup Actions:'));
278
+ const recent = stats.cleanupActionsOverTime.slice(-5);
279
+ recent.forEach(entry => {
280
+ const date = new Date(entry.timestamp).toLocaleString();
281
+ console.log(` ${date}: ${entry.count} file(s) deleted`);
282
+ });
283
+ console.log();
284
+ }
285
+
286
+ // Recommendations
287
+ console.log(this.chalk.cyan('šŸ’” Run "kse docs report" to generate a detailed compliance report\n'));
288
+ }
289
+
290
+ /**
291
+ * Display success message
292
+ *
293
+ * @param {string} message - Success message
294
+ */
295
+ displaySuccess(message) {
296
+ console.log(this.chalk.green(`\nāœ… ${message}\n`));
297
+ }
298
+
299
+ /**
300
+ * Display info message
301
+ *
302
+ * @param {string} message - Info message
303
+ */
304
+ displayInfo(message) {
305
+ console.log(this.chalk.cyan(`\nā„¹ļø ${message}\n`));
306
+ }
307
+
308
+ /**
309
+ * Helper: Group array by property
310
+ *
311
+ * @param {Array} array - Array to group
312
+ * @param {string} property - Property to group by
313
+ * @returns {Object} - Grouped object
314
+ */
315
+ groupBy(array, property) {
316
+ return array.reduce((acc, item) => {
317
+ const key = item[property];
318
+ if (!acc[key]) acc[key] = [];
319
+ acc[key].push(item);
320
+ return acc;
321
+ }, {});
322
+ }
323
+
324
+ /**
325
+ * Helper: Format violation type
326
+ *
327
+ * @param {string} type - Violation type
328
+ * @returns {string} - Formatted type
329
+ */
330
+ formatType(type) {
331
+ return type.split('_').map(word =>
332
+ word.charAt(0).toUpperCase() + word.slice(1)
333
+ ).join(' ');
334
+ }
335
+ }
336
+
337
+ module.exports = Reporter;