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,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Governance Command Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides CLI interface for document governance operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const ConfigManager = require('../governance/config-manager');
|
|
9
|
+
const DiagnosticEngine = require('../governance/diagnostic-engine');
|
|
10
|
+
const CleanupTool = require('../governance/cleanup-tool');
|
|
11
|
+
const ValidationEngine = require('../governance/validation-engine');
|
|
12
|
+
const ArchiveTool = require('../governance/archive-tool');
|
|
13
|
+
const HooksManager = require('../governance/hooks-manager');
|
|
14
|
+
const Reporter = require('../governance/reporter');
|
|
15
|
+
const ExecutionLogger = require('../governance/execution-logger');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute document governance command
|
|
19
|
+
*
|
|
20
|
+
* @param {string} subcommand - The subcommand (cleanup, validate, archive, etc.)
|
|
21
|
+
* @param {Object} options - Command options
|
|
22
|
+
* @returns {Promise<number>} - Exit code (0 for success, non-zero for error)
|
|
23
|
+
*/
|
|
24
|
+
async function docsCommand(subcommand, options = {}) {
|
|
25
|
+
const projectPath = process.cwd();
|
|
26
|
+
const reporter = new Reporter();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// Load configuration
|
|
30
|
+
const configManager = new ConfigManager(projectPath);
|
|
31
|
+
const config = await configManager.load();
|
|
32
|
+
|
|
33
|
+
// Route to appropriate handler
|
|
34
|
+
switch (subcommand) {
|
|
35
|
+
case 'diagnose':
|
|
36
|
+
case 'diagnostic':
|
|
37
|
+
return await handleDiagnostic(projectPath, config, reporter);
|
|
38
|
+
|
|
39
|
+
case 'cleanup':
|
|
40
|
+
return await handleCleanup(projectPath, config, reporter, options);
|
|
41
|
+
|
|
42
|
+
case 'validate':
|
|
43
|
+
return await handleValidate(projectPath, config, reporter, options);
|
|
44
|
+
|
|
45
|
+
case 'archive':
|
|
46
|
+
return await handleArchive(projectPath, config, reporter, options);
|
|
47
|
+
|
|
48
|
+
case 'hooks':
|
|
49
|
+
return await handleHooks(projectPath, reporter, options);
|
|
50
|
+
|
|
51
|
+
case 'config':
|
|
52
|
+
return await handleConfig(projectPath, configManager, reporter, options);
|
|
53
|
+
|
|
54
|
+
case 'stats':
|
|
55
|
+
return await handleStats(projectPath, reporter, options);
|
|
56
|
+
|
|
57
|
+
case 'report':
|
|
58
|
+
return await handleReport(projectPath, reporter, options);
|
|
59
|
+
|
|
60
|
+
case 'help':
|
|
61
|
+
default:
|
|
62
|
+
showHelp();
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
reporter.displayError(error.message);
|
|
67
|
+
if (options.verbose) {
|
|
68
|
+
console.error(error.stack);
|
|
69
|
+
}
|
|
70
|
+
return 2; // Fatal error
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle diagnostic command
|
|
76
|
+
*/
|
|
77
|
+
async function handleDiagnostic(projectPath, config, reporter) {
|
|
78
|
+
const engine = new DiagnosticEngine(projectPath, config);
|
|
79
|
+
const report = await engine.scan();
|
|
80
|
+
|
|
81
|
+
reporter.displayDiagnostic(report);
|
|
82
|
+
|
|
83
|
+
// Log execution
|
|
84
|
+
const logger = new ExecutionLogger(projectPath);
|
|
85
|
+
await logger.logExecution('diagnostic', 'scan', report);
|
|
86
|
+
|
|
87
|
+
return report.compliant ? 0 : 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Handle cleanup command
|
|
92
|
+
*/
|
|
93
|
+
async function handleCleanup(projectPath, config, reporter, options) {
|
|
94
|
+
const tool = new CleanupTool(projectPath, config);
|
|
95
|
+
|
|
96
|
+
const cleanupOptions = {
|
|
97
|
+
dryRun: options.dryRun || options.dry || false,
|
|
98
|
+
interactive: options.interactive || options.i || false,
|
|
99
|
+
spec: options.spec || null
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const report = await tool.cleanup(cleanupOptions);
|
|
103
|
+
|
|
104
|
+
reporter.displayCleanup(report, cleanupOptions.dryRun);
|
|
105
|
+
|
|
106
|
+
// Log execution (only if not dry run)
|
|
107
|
+
if (!cleanupOptions.dryRun) {
|
|
108
|
+
const logger = new ExecutionLogger(projectPath);
|
|
109
|
+
await logger.logExecution('cleanup', 'delete', report);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return report.success ? 0 : 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handle validate command
|
|
117
|
+
*/
|
|
118
|
+
async function handleValidate(projectPath, config, reporter, options) {
|
|
119
|
+
const engine = new ValidationEngine(projectPath, config);
|
|
120
|
+
|
|
121
|
+
const validateOptions = {
|
|
122
|
+
spec: options.spec || null,
|
|
123
|
+
all: options.all || false
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const report = await engine.validate(validateOptions);
|
|
127
|
+
|
|
128
|
+
reporter.displayValidation(report);
|
|
129
|
+
|
|
130
|
+
// Log execution
|
|
131
|
+
const logger = new ExecutionLogger(projectPath);
|
|
132
|
+
await logger.logExecution('validation', 'validate', report);
|
|
133
|
+
|
|
134
|
+
return report.valid ? 0 : 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle archive command
|
|
139
|
+
*/
|
|
140
|
+
async function handleArchive(projectPath, config, reporter, options) {
|
|
141
|
+
if (!options.spec) {
|
|
142
|
+
reporter.displayError('--spec option is required for archive command');
|
|
143
|
+
console.log('Usage: kse docs archive --spec <spec-name> [--dry-run]');
|
|
144
|
+
return 2;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tool = new ArchiveTool(projectPath, config);
|
|
148
|
+
|
|
149
|
+
const archiveOptions = {
|
|
150
|
+
dryRun: options.dryRun || options.dry || false
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const report = await tool.archive(options.spec, archiveOptions);
|
|
154
|
+
|
|
155
|
+
reporter.displayArchive(report, archiveOptions.dryRun);
|
|
156
|
+
|
|
157
|
+
// Log execution (only if not dry run)
|
|
158
|
+
if (!archiveOptions.dryRun) {
|
|
159
|
+
const logger = new ExecutionLogger(projectPath);
|
|
160
|
+
await logger.logExecution('archive', 'move', report);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return report.success ? 0 : 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handle hooks command
|
|
168
|
+
*/
|
|
169
|
+
async function handleHooks(projectPath, reporter, options) {
|
|
170
|
+
const manager = new HooksManager(projectPath);
|
|
171
|
+
|
|
172
|
+
// Determine subcommand (install, uninstall, status)
|
|
173
|
+
const hookSubcommand = options._?.[0] || 'status';
|
|
174
|
+
|
|
175
|
+
switch (hookSubcommand) {
|
|
176
|
+
case 'install':
|
|
177
|
+
return await handleHooksInstall(manager, reporter);
|
|
178
|
+
|
|
179
|
+
case 'uninstall':
|
|
180
|
+
return await handleHooksUninstall(manager, reporter);
|
|
181
|
+
|
|
182
|
+
case 'status':
|
|
183
|
+
default:
|
|
184
|
+
return await handleHooksStatus(manager, reporter);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handle hooks install
|
|
190
|
+
*/
|
|
191
|
+
async function handleHooksInstall(manager, reporter) {
|
|
192
|
+
console.log(chalk.cyan('š§ Installing document governance hooks...\n'));
|
|
193
|
+
|
|
194
|
+
const result = await manager.installHooks();
|
|
195
|
+
|
|
196
|
+
if (result.success) {
|
|
197
|
+
console.log(chalk.green('ā
' + result.message));
|
|
198
|
+
|
|
199
|
+
if (result.backupCreated) {
|
|
200
|
+
console.log(chalk.gray(' Backup created at: .git/hooks/pre-commit.backup'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(chalk.cyan('\nThe pre-commit hook will now validate documents before each commit.'));
|
|
204
|
+
console.log(chalk.gray('To bypass validation, use: git commit --no-verify\n'));
|
|
205
|
+
|
|
206
|
+
return 0;
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.red('ā ' + result.message));
|
|
209
|
+
|
|
210
|
+
if (result.reason === 'not_git_repo') {
|
|
211
|
+
console.log(chalk.yellow('\nThis is not a Git repository.'));
|
|
212
|
+
console.log(chalk.gray('Initialize Git first: git init\n'));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Handle hooks uninstall
|
|
221
|
+
*/
|
|
222
|
+
async function handleHooksUninstall(manager, reporter) {
|
|
223
|
+
console.log(chalk.cyan('š§ Uninstalling document governance hooks...\n'));
|
|
224
|
+
|
|
225
|
+
const result = await manager.uninstallHooks();
|
|
226
|
+
|
|
227
|
+
if (result.success) {
|
|
228
|
+
console.log(chalk.green('ā
' + result.message + '\n'));
|
|
229
|
+
return 0;
|
|
230
|
+
} else {
|
|
231
|
+
console.log(chalk.red('ā ' + result.message));
|
|
232
|
+
|
|
233
|
+
if (result.reason === 'not_our_hook') {
|
|
234
|
+
console.log(chalk.yellow('\nThe pre-commit hook was not installed by kiro-spec-engine.'));
|
|
235
|
+
console.log(chalk.gray('You may need to manually edit: .git/hooks/pre-commit\n'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Handle hooks status
|
|
244
|
+
*/
|
|
245
|
+
async function handleHooksStatus(manager, reporter) {
|
|
246
|
+
console.log(chalk.cyan('š Checking Git hooks status...\n'));
|
|
247
|
+
|
|
248
|
+
const status = await manager.checkHooksInstalled();
|
|
249
|
+
|
|
250
|
+
if (status.installed) {
|
|
251
|
+
console.log(chalk.green('ā
Document governance hooks are installed'));
|
|
252
|
+
console.log(chalk.gray(' Pre-commit validation is active\n'));
|
|
253
|
+
return 0;
|
|
254
|
+
} else {
|
|
255
|
+
console.log(chalk.yellow('ā ļø Document governance hooks are not installed'));
|
|
256
|
+
console.log(chalk.gray(' Reason: ' + status.message));
|
|
257
|
+
console.log(chalk.cyan('\nTo install hooks: kse docs hooks install\n'));
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Handle config command
|
|
264
|
+
*/
|
|
265
|
+
async function handleConfig(projectPath, configManager, reporter, options) {
|
|
266
|
+
// Check if --set flag is provided
|
|
267
|
+
if (options.set) {
|
|
268
|
+
return await handleConfigSet(configManager, reporter, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check if --reset flag is provided
|
|
272
|
+
if (options.reset) {
|
|
273
|
+
return await handleConfigReset(configManager, reporter);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default: display current configuration
|
|
277
|
+
return await handleConfigDisplay(configManager, reporter);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Handle config display
|
|
282
|
+
*/
|
|
283
|
+
async function handleConfigDisplay(configManager, reporter) {
|
|
284
|
+
console.log(chalk.bold.cyan('\nāļø Document Governance Configuration\n'));
|
|
285
|
+
|
|
286
|
+
const config = configManager.getAll();
|
|
287
|
+
|
|
288
|
+
console.log(chalk.bold('Root Allowed Files:'));
|
|
289
|
+
config.rootAllowedFiles.forEach(file => {
|
|
290
|
+
console.log(` ⢠${file}`);
|
|
291
|
+
});
|
|
292
|
+
console.log();
|
|
293
|
+
|
|
294
|
+
console.log(chalk.bold('Spec Subdirectories:'));
|
|
295
|
+
config.specSubdirs.forEach(dir => {
|
|
296
|
+
console.log(` ⢠${dir}`);
|
|
297
|
+
});
|
|
298
|
+
console.log();
|
|
299
|
+
|
|
300
|
+
console.log(chalk.bold('Temporary Patterns:'));
|
|
301
|
+
config.temporaryPatterns.forEach(pattern => {
|
|
302
|
+
console.log(` ⢠${pattern}`);
|
|
303
|
+
});
|
|
304
|
+
console.log();
|
|
305
|
+
|
|
306
|
+
console.log(chalk.gray('Configuration file: .kiro/config/docs.json'));
|
|
307
|
+
console.log(chalk.gray('To modify: kse docs config --set <key> <value>'));
|
|
308
|
+
console.log(chalk.gray('To reset: kse docs config --reset\n'));
|
|
309
|
+
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Handle config set
|
|
315
|
+
*/
|
|
316
|
+
async function handleConfigSet(configManager, reporter, options) {
|
|
317
|
+
// Parse the key and value from options
|
|
318
|
+
// Expected format: --set key value or --set key "value1,value2"
|
|
319
|
+
const args = options._ || [];
|
|
320
|
+
|
|
321
|
+
// Find the index of the key after 'config'
|
|
322
|
+
const configIndex = args.indexOf('config');
|
|
323
|
+
const keyIndex = configIndex + 1;
|
|
324
|
+
|
|
325
|
+
if (keyIndex >= args.length) {
|
|
326
|
+
reporter.displayError('Missing configuration key');
|
|
327
|
+
console.log(chalk.gray('Usage: kse docs config --set <key> <value>'));
|
|
328
|
+
console.log(chalk.gray('Example: kse docs config --set root-allowed-files "README.md,CUSTOM.md"\n'));
|
|
329
|
+
return 2;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const key = args[keyIndex];
|
|
333
|
+
const valueIndex = keyIndex + 1;
|
|
334
|
+
|
|
335
|
+
if (valueIndex >= args.length) {
|
|
336
|
+
reporter.displayError('Missing configuration value');
|
|
337
|
+
console.log(chalk.gray('Usage: kse docs config --set <key> <value>'));
|
|
338
|
+
console.log(chalk.gray('Example: kse docs config --set root-allowed-files "README.md,CUSTOM.md"\n'));
|
|
339
|
+
return 2;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const valueStr = args[valueIndex];
|
|
343
|
+
|
|
344
|
+
// Convert kebab-case to camelCase
|
|
345
|
+
const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
346
|
+
|
|
347
|
+
// Validate key
|
|
348
|
+
const validKeys = ['rootAllowedFiles', 'specSubdirs', 'temporaryPatterns'];
|
|
349
|
+
if (!validKeys.includes(camelKey)) {
|
|
350
|
+
reporter.displayError(`Invalid configuration key: ${key}`);
|
|
351
|
+
console.log(chalk.gray('Valid keys: root-allowed-files, spec-subdirs, temporary-patterns\n'));
|
|
352
|
+
return 2;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Parse value (comma-separated list)
|
|
356
|
+
const value = valueStr.split(',').map(v => v.trim()).filter(v => v.length > 0);
|
|
357
|
+
|
|
358
|
+
if (value.length === 0) {
|
|
359
|
+
reporter.displayError('Value cannot be empty');
|
|
360
|
+
return 2;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Set the value
|
|
364
|
+
try {
|
|
365
|
+
await configManager.set(camelKey, value);
|
|
366
|
+
|
|
367
|
+
console.log(chalk.green(`ā
Configuration updated: ${key}`));
|
|
368
|
+
console.log(chalk.gray(` New value: ${value.join(', ')}\n`));
|
|
369
|
+
|
|
370
|
+
return 0;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
reporter.displayError(`Failed to update configuration: ${error.message}`);
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Handle config reset
|
|
379
|
+
*/
|
|
380
|
+
async function handleConfigReset(configManager, reporter) {
|
|
381
|
+
console.log(chalk.yellow('ā ļø Resetting configuration to defaults...\n'));
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
await configManager.reset();
|
|
385
|
+
|
|
386
|
+
console.log(chalk.green('ā
Configuration reset to defaults'));
|
|
387
|
+
console.log(chalk.gray(' Run "kse docs config" to view current configuration\n'));
|
|
388
|
+
|
|
389
|
+
return 0;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
reporter.displayError(`Failed to reset configuration: ${error.message}`);
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Handle stats command
|
|
398
|
+
*/
|
|
399
|
+
async function handleStats(projectPath, reporter, options) {
|
|
400
|
+
const logger = new ExecutionLogger(projectPath);
|
|
401
|
+
|
|
402
|
+
// Get execution history
|
|
403
|
+
const history = await logger.getHistory();
|
|
404
|
+
|
|
405
|
+
if (history.length === 0) {
|
|
406
|
+
console.log(chalk.yellow('ā ļø No execution history found'));
|
|
407
|
+
console.log(chalk.gray(' Run document governance commands to generate statistics\n'));
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Calculate statistics
|
|
412
|
+
const stats = calculateStatistics(history);
|
|
413
|
+
|
|
414
|
+
// Display statistics
|
|
415
|
+
reporter.displayStats(stats);
|
|
416
|
+
|
|
417
|
+
return 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handle report command
|
|
422
|
+
*/
|
|
423
|
+
async function handleReport(projectPath, reporter, options) {
|
|
424
|
+
const logger = new ExecutionLogger(projectPath);
|
|
425
|
+
const fs = require('fs-extra');
|
|
426
|
+
const path = require('path');
|
|
427
|
+
|
|
428
|
+
// Get execution history
|
|
429
|
+
const history = await logger.getHistory();
|
|
430
|
+
|
|
431
|
+
if (history.length === 0) {
|
|
432
|
+
console.log(chalk.yellow('ā ļø No execution history found'));
|
|
433
|
+
console.log(chalk.gray(' Run document governance commands to generate a report\n'));
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Calculate statistics
|
|
438
|
+
const stats = calculateStatistics(history);
|
|
439
|
+
|
|
440
|
+
// Generate markdown report
|
|
441
|
+
const report = generateMarkdownReport(stats, history);
|
|
442
|
+
|
|
443
|
+
// Ensure reports directory exists
|
|
444
|
+
const reportsDir = path.join(projectPath, '.kiro', 'reports');
|
|
445
|
+
await fs.ensureDir(reportsDir);
|
|
446
|
+
|
|
447
|
+
// Generate filename with timestamp
|
|
448
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T')[0];
|
|
449
|
+
const filename = `document-compliance-${timestamp}.md`;
|
|
450
|
+
const reportPath = path.join(reportsDir, filename);
|
|
451
|
+
|
|
452
|
+
// Save report
|
|
453
|
+
await fs.writeFile(reportPath, report, 'utf8');
|
|
454
|
+
|
|
455
|
+
console.log(chalk.green('ā
Compliance report generated'));
|
|
456
|
+
console.log(chalk.gray(` Saved to: ${reportPath}\n`));
|
|
457
|
+
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Calculate statistics from execution history
|
|
463
|
+
*
|
|
464
|
+
* @param {Array} history - Execution history entries
|
|
465
|
+
* @returns {Object} - Statistics object
|
|
466
|
+
*/
|
|
467
|
+
function calculateStatistics(history) {
|
|
468
|
+
const stats = {
|
|
469
|
+
totalExecutions: history.length,
|
|
470
|
+
executionsByTool: {},
|
|
471
|
+
totalViolations: 0,
|
|
472
|
+
violationsByType: {},
|
|
473
|
+
totalCleanupActions: 0,
|
|
474
|
+
totalArchiveActions: 0,
|
|
475
|
+
totalErrors: 0,
|
|
476
|
+
firstExecution: null,
|
|
477
|
+
lastExecution: null,
|
|
478
|
+
violationsOverTime: [],
|
|
479
|
+
cleanupActionsOverTime: []
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Process each history entry
|
|
483
|
+
history.forEach(entry => {
|
|
484
|
+
// Track executions by tool
|
|
485
|
+
if (!stats.executionsByTool[entry.tool]) {
|
|
486
|
+
stats.executionsByTool[entry.tool] = 0;
|
|
487
|
+
}
|
|
488
|
+
stats.executionsByTool[entry.tool]++;
|
|
489
|
+
|
|
490
|
+
// Track timestamps
|
|
491
|
+
if (!stats.firstExecution || entry.timestamp < stats.firstExecution) {
|
|
492
|
+
stats.firstExecution = entry.timestamp;
|
|
493
|
+
}
|
|
494
|
+
if (!stats.lastExecution || entry.timestamp > stats.lastExecution) {
|
|
495
|
+
stats.lastExecution = entry.timestamp;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Process diagnostic results
|
|
499
|
+
if (entry.tool === 'diagnostic' && entry.results) {
|
|
500
|
+
if (entry.results.violations) {
|
|
501
|
+
const violationCount = entry.results.violations.length;
|
|
502
|
+
stats.totalViolations += violationCount;
|
|
503
|
+
|
|
504
|
+
// Track violations over time
|
|
505
|
+
stats.violationsOverTime.push({
|
|
506
|
+
timestamp: entry.timestamp,
|
|
507
|
+
count: violationCount
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Track violations by type
|
|
511
|
+
entry.results.violations.forEach(violation => {
|
|
512
|
+
if (!stats.violationsByType[violation.type]) {
|
|
513
|
+
stats.violationsByType[violation.type] = 0;
|
|
514
|
+
}
|
|
515
|
+
stats.violationsByType[violation.type]++;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Process cleanup results
|
|
521
|
+
if (entry.tool === 'cleanup' && entry.results) {
|
|
522
|
+
if (entry.results.deletedFiles) {
|
|
523
|
+
const cleanupCount = entry.results.deletedFiles.length;
|
|
524
|
+
stats.totalCleanupActions += cleanupCount;
|
|
525
|
+
|
|
526
|
+
// Track cleanup actions over time
|
|
527
|
+
stats.cleanupActionsOverTime.push({
|
|
528
|
+
timestamp: entry.timestamp,
|
|
529
|
+
count: cleanupCount
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (entry.results.errors) {
|
|
534
|
+
stats.totalErrors += entry.results.errors.length;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Process archive results
|
|
539
|
+
if (entry.tool === 'archive' && entry.results) {
|
|
540
|
+
if (entry.results.movedFiles) {
|
|
541
|
+
stats.totalArchiveActions += entry.results.movedFiles.length;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (entry.results.errors) {
|
|
545
|
+
stats.totalErrors += entry.results.errors.length;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Process validation results
|
|
550
|
+
if (entry.tool === 'validation' && entry.results) {
|
|
551
|
+
if (entry.results.errors) {
|
|
552
|
+
stats.totalErrors += entry.results.errors.length;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return stats;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Generate markdown compliance report
|
|
562
|
+
*
|
|
563
|
+
* @param {Object} stats - Statistics object
|
|
564
|
+
* @param {Array} history - Execution history
|
|
565
|
+
* @returns {string} - Markdown report
|
|
566
|
+
*/
|
|
567
|
+
function generateMarkdownReport(stats, history) {
|
|
568
|
+
const lines = [];
|
|
569
|
+
|
|
570
|
+
// Header
|
|
571
|
+
lines.push('# Document Compliance Report');
|
|
572
|
+
lines.push('');
|
|
573
|
+
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
574
|
+
lines.push('');
|
|
575
|
+
|
|
576
|
+
// Summary
|
|
577
|
+
lines.push('## Summary');
|
|
578
|
+
lines.push('');
|
|
579
|
+
lines.push(`- **Total Executions:** ${stats.totalExecutions}`);
|
|
580
|
+
lines.push(`- **Total Violations Found:** ${stats.totalViolations}`);
|
|
581
|
+
lines.push(`- **Total Cleanup Actions:** ${stats.totalCleanupActions}`);
|
|
582
|
+
lines.push(`- **Total Archive Actions:** ${stats.totalArchiveActions}`);
|
|
583
|
+
lines.push(`- **Total Errors:** ${stats.totalErrors}`);
|
|
584
|
+
lines.push(`- **First Execution:** ${stats.firstExecution || 'N/A'}`);
|
|
585
|
+
lines.push(`- **Last Execution:** ${stats.lastExecution || 'N/A'}`);
|
|
586
|
+
lines.push('');
|
|
587
|
+
|
|
588
|
+
// Executions by Tool
|
|
589
|
+
lines.push('## Executions by Tool');
|
|
590
|
+
lines.push('');
|
|
591
|
+
lines.push('| Tool | Count |');
|
|
592
|
+
lines.push('|------|-------|');
|
|
593
|
+
Object.entries(stats.executionsByTool).forEach(([tool, count]) => {
|
|
594
|
+
lines.push(`| ${tool} | ${count} |`);
|
|
595
|
+
});
|
|
596
|
+
lines.push('');
|
|
597
|
+
|
|
598
|
+
// Violations by Type
|
|
599
|
+
if (Object.keys(stats.violationsByType).length > 0) {
|
|
600
|
+
lines.push('## Violations by Type');
|
|
601
|
+
lines.push('');
|
|
602
|
+
lines.push('| Type | Count |');
|
|
603
|
+
lines.push('|------|-------|');
|
|
604
|
+
Object.entries(stats.violationsByType)
|
|
605
|
+
.sort((a, b) => b[1] - a[1])
|
|
606
|
+
.forEach(([type, count]) => {
|
|
607
|
+
lines.push(`| ${type} | ${count} |`);
|
|
608
|
+
});
|
|
609
|
+
lines.push('');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Violations Over Time
|
|
613
|
+
if (stats.violationsOverTime.length > 0) {
|
|
614
|
+
lines.push('## Violations Over Time');
|
|
615
|
+
lines.push('');
|
|
616
|
+
lines.push('| Timestamp | Count |');
|
|
617
|
+
lines.push('|-----------|-------|');
|
|
618
|
+
stats.violationsOverTime.forEach(entry => {
|
|
619
|
+
lines.push(`| ${entry.timestamp} | ${entry.count} |`);
|
|
620
|
+
});
|
|
621
|
+
lines.push('');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Cleanup Actions Over Time
|
|
625
|
+
if (stats.cleanupActionsOverTime.length > 0) {
|
|
626
|
+
lines.push('## Cleanup Actions Over Time');
|
|
627
|
+
lines.push('');
|
|
628
|
+
lines.push('| Timestamp | Files Deleted |');
|
|
629
|
+
lines.push('|-----------|---------------|');
|
|
630
|
+
stats.cleanupActionsOverTime.forEach(entry => {
|
|
631
|
+
lines.push(`| ${entry.timestamp} | ${entry.count} |`);
|
|
632
|
+
});
|
|
633
|
+
lines.push('');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Recent Executions
|
|
637
|
+
lines.push('## Recent Executions');
|
|
638
|
+
lines.push('');
|
|
639
|
+
const recentHistory = history.slice(-10).reverse();
|
|
640
|
+
recentHistory.forEach(entry => {
|
|
641
|
+
lines.push(`### ${entry.tool} - ${entry.operation}`);
|
|
642
|
+
lines.push('');
|
|
643
|
+
lines.push(`**Timestamp:** ${entry.timestamp}`);
|
|
644
|
+
lines.push('');
|
|
645
|
+
|
|
646
|
+
if (entry.results) {
|
|
647
|
+
if (entry.results.violations) {
|
|
648
|
+
lines.push(`**Violations Found:** ${entry.results.violations.length}`);
|
|
649
|
+
}
|
|
650
|
+
if (entry.results.deletedFiles) {
|
|
651
|
+
lines.push(`**Files Deleted:** ${entry.results.deletedFiles.length}`);
|
|
652
|
+
}
|
|
653
|
+
if (entry.results.movedFiles) {
|
|
654
|
+
lines.push(`**Files Moved:** ${entry.results.movedFiles.length}`);
|
|
655
|
+
}
|
|
656
|
+
if (entry.results.errors && entry.results.errors.length > 0) {
|
|
657
|
+
lines.push(`**Errors:** ${entry.results.errors.length}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
lines.push('');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Footer
|
|
665
|
+
lines.push('---');
|
|
666
|
+
lines.push('');
|
|
667
|
+
lines.push('*Generated by kiro-spec-engine document governance system*');
|
|
668
|
+
lines.push('');
|
|
669
|
+
|
|
670
|
+
return lines.join('\n');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Display help information
|
|
675
|
+
*/
|
|
676
|
+
function showHelp() {
|
|
677
|
+
console.log(chalk.bold.cyan('\nš Document Governance Commands\n'));
|
|
678
|
+
|
|
679
|
+
console.log(chalk.bold('Usage:'));
|
|
680
|
+
console.log(' kse docs <subcommand> [options]\n');
|
|
681
|
+
|
|
682
|
+
console.log(chalk.bold('Subcommands:'));
|
|
683
|
+
console.log(' diagnose Scan project for document violations');
|
|
684
|
+
console.log(' cleanup Remove temporary documents');
|
|
685
|
+
console.log(' validate Validate document structure');
|
|
686
|
+
console.log(' archive Organize Spec artifacts into subdirectories');
|
|
687
|
+
console.log(' hooks <action> Manage Git hooks (install, uninstall, status)');
|
|
688
|
+
console.log(' config Display or modify configuration');
|
|
689
|
+
console.log(' stats Display compliance statistics');
|
|
690
|
+
console.log(' report Generate compliance report');
|
|
691
|
+
console.log(' help Show this help message\n');
|
|
692
|
+
|
|
693
|
+
console.log(chalk.bold('Options:'));
|
|
694
|
+
console.log(' --dry-run, --dry Preview changes without applying them');
|
|
695
|
+
console.log(' --interactive, -i Prompt for confirmation (cleanup only)');
|
|
696
|
+
console.log(' --spec <name> Target specific Spec directory');
|
|
697
|
+
console.log(' --all Validate all Specs (validate only)');
|
|
698
|
+
console.log(' --set <key> <value> Set configuration value (config only)');
|
|
699
|
+
console.log(' --reset Reset configuration to defaults (config only)');
|
|
700
|
+
console.log(' --verbose Show detailed error information\n');
|
|
701
|
+
|
|
702
|
+
console.log(chalk.bold('Examples:'));
|
|
703
|
+
console.log(' kse docs diagnose');
|
|
704
|
+
console.log(' kse docs cleanup --dry-run');
|
|
705
|
+
console.log(' kse docs cleanup --spec my-spec');
|
|
706
|
+
console.log(' kse docs validate --all');
|
|
707
|
+
console.log(' kse docs archive --spec my-spec --dry-run');
|
|
708
|
+
console.log(' kse docs hooks install');
|
|
709
|
+
console.log(' kse docs hooks status');
|
|
710
|
+
console.log(' kse docs config');
|
|
711
|
+
console.log(' kse docs config --set root-allowed-files "README.md,CUSTOM.md"');
|
|
712
|
+
console.log(' kse docs config --reset');
|
|
713
|
+
console.log(' kse docs stats');
|
|
714
|
+
console.log(' kse docs report\n');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
module.exports = docsCommand;
|