kiro-spec-engine 1.4.3 ā 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 +86 -4
- 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 +7 -7
|
@@ -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;
|