kld-sdd 2.4.7

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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "kld-sdd",
3
+ "version": "2.4.7",
4
+ "description": "KLD SDD OpenSpec 项目初始化工具 - 内置模版一键初始化",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "kld-sdd": "bin/kld-sdd-init.js",
8
+ "kld-sdd-init": "bin/kld-sdd-init.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "kld",
15
+ "sdd",
16
+ "openspec",
17
+ "init",
18
+ "cursor",
19
+ "claude",
20
+ "codebuddy",
21
+ "codex",
22
+ "template"
23
+ ],
24
+ "author": "KLD Team",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=14.0.0"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "lib/",
32
+ "templates/",
33
+ "skywalk-sdd/index.js",
34
+ "README.md"
35
+ ]
36
+ }
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SDD Telemetry CLI
4
+ *
5
+ * 本地 CLI 工具,为 SDD 工作流提供精确的流程度量采集。
6
+ * AI Agent 通过终端命令调用,将事件写入项目本地 skywalk-sdd/ 目录。
7
+ *
8
+ * 用法:
9
+ * node skywalk-sdd/log.js start --command=propose --project=/path --change=xxx [--agent=cursor]
10
+ * node skywalk-sdd/log.js end --event-id=evt_xxx --result=success --summary="..."
11
+ * node skywalk-sdd/log.js metrics --project=/path [--change=xxx]
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+
18
+ // ── 配置 ──────────────────────────────────────────────
19
+
20
+ /** 获取项目的 skywalk-sdd 数据目录 */
21
+ function getDataDir(projectRoot) {
22
+ return path.join(projectRoot, 'skywalk-sdd');
23
+ }
24
+
25
+ // SDD 标准阶段顺序
26
+ const STAGE_ORDER = ['propose', 'spec', 'design', 'task', 'check', 'apply', 'test', 'archive', 'explore'];
27
+ const CORE_STAGES = ['propose', 'spec', 'design', 'apply', 'test'];
28
+
29
+ // ── 工具函数 ──────────────────────────────────────────
30
+ function ensureDir(dirPath) {
31
+ if (!fs.existsSync(dirPath)) {
32
+ fs.mkdirSync(dirPath, { recursive: true });
33
+ }
34
+ }
35
+
36
+ function generateEventId() {
37
+ return 'evt_' + crypto.randomBytes(6).toString('hex');
38
+ }
39
+
40
+ function today() {
41
+ return new Date().toISOString().slice(0, 10);
42
+ }
43
+
44
+ function nowISO() {
45
+ return new Date().toISOString();
46
+ }
47
+
48
+ /** 安全化 change name,防止路径穿越 */
49
+ function safeChangeName(name) {
50
+ if (!name) return 'general';
51
+ return name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^-+|-+$/g, '') || 'general';
52
+ }
53
+
54
+ /** 追加一行 JSONL 到事件文件(写入失败时抛出异常) */
55
+ function appendEvent(dataDir, changeName, event) {
56
+ const dir = path.join(dataDir, 'events', safeChangeName(changeName));
57
+ ensureDir(dir);
58
+ const file = path.join(dir, `${today()}.jsonl`);
59
+ const line = JSON.stringify(event) + '\n';
60
+ fs.appendFileSync(file, line, 'utf8');
61
+ if (!fs.existsSync(file) || fs.statSync(file).size === 0) {
62
+ throw new Error(`事件文件写入验证失败: ${file}`);
63
+ }
64
+ }
65
+
66
+ /** 读取指定 change 的所有事件 */
67
+ function readEvents(dataDir, changeName) {
68
+ try {
69
+ const dir = path.join(dataDir, 'events', safeChangeName(changeName));
70
+ if (!fs.existsSync(dir)) return [];
71
+
72
+ const events = [];
73
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort();
74
+ for (const file of files) {
75
+ const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
76
+ for (const line of lines) {
77
+ try { events.push(JSON.parse(line)); } catch {}
78
+ }
79
+ }
80
+ return events;
81
+ } catch (err) {
82
+ console.error('[sdd-telemetry] 事件读取失败:', err.message);
83
+ return [];
84
+ }
85
+ }
86
+
87
+ /** 读取所有 change 的事件 */
88
+ function readAllEvents(dataDir) {
89
+ const eventsDir = path.join(dataDir, 'events');
90
+ if (!fs.existsSync(eventsDir)) return [];
91
+
92
+ const events = [];
93
+ const changeDirs = fs.readdirSync(eventsDir).filter(d => {
94
+ return fs.statSync(path.join(eventsDir, d)).isDirectory();
95
+ });
96
+
97
+ for (const changeDir of changeDirs) {
98
+ const changeEvents = readEvents(dataDir, changeDir);
99
+ events.push(...changeEvents);
100
+ }
101
+ return events;
102
+ }
103
+
104
+ // ── 四维度量指标计算 ──────────────────────────────────
105
+
106
+ /**
107
+ * 计算单个 change 的四维指标
108
+ */
109
+ function computeChangeMetrics(changeName, events) {
110
+ const changeEvents = events.filter(e => e.change === changeName);
111
+ if (changeEvents.length === 0) return null;
112
+
113
+ const starts = changeEvents.filter(e => e.type === 'stage_start');
114
+ const ends = changeEvents.filter(e => e.type === 'stage_end');
115
+
116
+ // 已执行的阶段(去重)
117
+ const executedStages = [...new Set(starts.map(e => e.command))];
118
+ const completedStages = [...new Set(ends.map(e => e.command))];
119
+
120
+ // ── 维度一:流程健康度 ──
121
+ const coveredCoreStages = CORE_STAGES.filter(s => executedStages.includes(s));
122
+ const criticalPathCoverage = coveredCoreStages.length / CORE_STAGES.length;
123
+
124
+ // 跳阶段检测
125
+ const stageSequence = starts.map(e => e.command);
126
+ const skippedStages = detectSkippedStages(stageSequence);
127
+
128
+ // 阶段回退检测
129
+ const regressions = detectRegressions(stageSequence);
130
+ const regressionRate = stageSequence.length > 1
131
+ ? regressions.length / (stageSequence.length - 1)
132
+ : 0;
133
+
134
+ // Spec 存在率
135
+ const hasSpec = executedStages.includes('spec');
136
+
137
+ // ── 维度二:交付效率 ──
138
+ const proposeStart = starts.find(e => e.command === 'propose');
139
+ const archiveEnd = ends.find(e => e.command === 'archive');
140
+ const changeLeadTime = (proposeStart && archiveEnd)
141
+ ? new Date(archiveEnd.timestamp).getTime() - new Date(proposeStart.timestamp).getTime()
142
+ : null;
143
+
144
+ const designEnd = ends.find(e => e.command === 'design');
145
+ const applyStart = starts.find(e => e.command === 'apply');
146
+ const designToCodeTime = (designEnd && applyStart)
147
+ ? new Date(applyStart.timestamp).getTime() - new Date(designEnd.timestamp).getTime()
148
+ : null;
149
+
150
+ // 首次 Apply 成功率
151
+ const testEnds = ends.filter(e => e.command === 'test');
152
+ const firstTestSuccess = testEnds.length > 0 ? testEnds[0].result === 'success' : null;
153
+
154
+ // 阶段平均耗时
155
+ const stageDurations = {};
156
+ for (const end of ends) {
157
+ if (end.duration_ms) {
158
+ if (!stageDurations[end.command]) stageDurations[end.command] = [];
159
+ stageDurations[end.command].push(end.duration_ms);
160
+ }
161
+ }
162
+ const avgStageDurations = {};
163
+ for (const [stage, durations] of Object.entries(stageDurations)) {
164
+ avgStageDurations[stage] = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
165
+ }
166
+
167
+ // ── 维度三:质量信号 ──
168
+ const firstTestResult = testEnds.length > 0 ? testEnds[0] : null;
169
+ const firstTestPassRate = firstTestResult?.details?.test_results
170
+ ? firstTestResult.details.test_results.passed /
171
+ (firstTestResult.details.test_results.passed + firstTestResult.details.test_results.failed + (firstTestResult.details.test_results.skipped || 0))
172
+ : null;
173
+
174
+ const checkEnds = ends.filter(e => e.command === 'check');
175
+ const firstCheckPass = checkEnds.length > 0
176
+ ? (checkEnds[0].details?.check_results?.errors || 0) === 0
177
+ : null;
178
+
179
+ // Apply-Test-Fix 循环次数
180
+ const applyCount = starts.filter(e => e.command === 'apply').length;
181
+
182
+ // 测试覆盖率趋势
183
+ const coverageTrend = testEnds
184
+ .filter(e => e.details?.test_results?.coverage != null)
185
+ .map(e => ({ timestamp: e.timestamp, coverage: e.details.test_results.coverage }));
186
+
187
+ // ── 维度四:团队洞察 ──
188
+ const agentTypes = [...new Set(changeEvents.filter(e => e.agent_type).map(e => e.agent_type))];
189
+ const isCompleted = !!archiveEnd;
190
+
191
+ return {
192
+ change: changeName,
193
+ process_health: {
194
+ critical_path_coverage: criticalPathCoverage,
195
+ covered_core_stages: coveredCoreStages,
196
+ regression_rate: regressionRate,
197
+ regressions,
198
+ skipped_stages: skippedStages,
199
+ has_spec: hasSpec,
200
+ },
201
+ delivery_efficiency: {
202
+ change_lead_time_ms: changeLeadTime,
203
+ design_to_code_time_ms: designToCodeTime,
204
+ first_apply_success: firstTestSuccess,
205
+ avg_stage_durations_ms: avgStageDurations,
206
+ },
207
+ quality_signals: {
208
+ first_test_pass_rate: firstTestPassRate,
209
+ first_check_pass: firstCheckPass,
210
+ apply_test_fix_cycles: applyCount,
211
+ coverage_trend: coverageTrend,
212
+ },
213
+ team_insights: {
214
+ agent_types: agentTypes,
215
+ is_completed: isCompleted,
216
+ total_stages_executed: stageSequence.length,
217
+ executed_stages: executedStages,
218
+ },
219
+ };
220
+ }
221
+
222
+ /** 检测跳阶段 */
223
+ function detectSkippedStages(sequence) {
224
+ const skipped = [];
225
+ for (let i = 1; i < sequence.length; i++) {
226
+ const prevIdx = STAGE_ORDER.indexOf(sequence[i - 1]);
227
+ const currIdx = STAGE_ORDER.indexOf(sequence[i]);
228
+ if (prevIdx >= 0 && currIdx >= 0 && currIdx > prevIdx + 1) {
229
+ const missed = STAGE_ORDER.slice(prevIdx + 1, currIdx);
230
+ skipped.push({ from: sequence[i - 1], to: sequence[i], missed });
231
+ }
232
+ }
233
+ return skipped;
234
+ }
235
+
236
+ /** 检测阶段回退 */
237
+ function detectRegressions(sequence) {
238
+ const regressions = [];
239
+ for (let i = 1; i < sequence.length; i++) {
240
+ const prevIdx = STAGE_ORDER.indexOf(sequence[i - 1]);
241
+ const currIdx = STAGE_ORDER.indexOf(sequence[i]);
242
+ if (prevIdx >= 0 && currIdx >= 0 && currIdx < prevIdx) {
243
+ regressions.push({ from: sequence[i - 1], back_to: sequence[i] });
244
+ }
245
+ }
246
+ return regressions;
247
+ }
248
+
249
+ /**
250
+ * 计算全局概览指标
251
+ */
252
+ function computeOverviewMetrics(events) {
253
+ const changeNames = [...new Set(events.map(e => e.change).filter(Boolean))];
254
+
255
+ const changeMetrics = changeNames
256
+ .map(name => computeChangeMetrics(name, events))
257
+ .filter(Boolean);
258
+
259
+ const totalChanges = changeMetrics.length;
260
+ if (totalChanges === 0) {
261
+ return { total_changes: 0, message: '暂无事件数据' };
262
+ }
263
+
264
+ // 维度一汇总
265
+ const avgCriticalPath = changeMetrics.reduce((s, m) => s + m.process_health.critical_path_coverage, 0) / totalChanges;
266
+ const specRate = changeMetrics.filter(m => m.process_health.has_spec).length / totalChanges;
267
+ const avgRegressionRate = changeMetrics.reduce((s, m) => s + m.process_health.regression_rate, 0) / totalChanges;
268
+
269
+ // 维度二汇总
270
+ const completedChanges = changeMetrics.filter(m => m.delivery_efficiency.change_lead_time_ms != null);
271
+ const avgLeadTime = completedChanges.length > 0
272
+ ? completedChanges.reduce((s, m) => s + m.delivery_efficiency.change_lead_time_ms, 0) / completedChanges.length
273
+ : null;
274
+ const firstApplySuccessRate = (() => {
275
+ const withTest = changeMetrics.filter(m => m.delivery_efficiency.first_apply_success != null);
276
+ return withTest.length > 0
277
+ ? withTest.filter(m => m.delivery_efficiency.first_apply_success).length / withTest.length
278
+ : null;
279
+ })();
280
+
281
+ // 维度三汇总
282
+ const firstCheckPassRate = (() => {
283
+ const withCheck = changeMetrics.filter(m => m.quality_signals.first_check_pass != null);
284
+ return withCheck.length > 0
285
+ ? withCheck.filter(m => m.quality_signals.first_check_pass).length / withCheck.length
286
+ : null;
287
+ })();
288
+ const avgApplyTestCycles = changeMetrics.reduce((s, m) => s + m.quality_signals.apply_test_fix_cycles, 0) / totalChanges;
289
+
290
+ // 维度四汇总
291
+ const completedCount = changeMetrics.filter(m => m.team_insights.is_completed).length;
292
+ const changeCompletionRate = completedCount / totalChanges;
293
+ const activeChanges = totalChanges - completedCount;
294
+ const allAgents = [...new Set(changeMetrics.flatMap(m => m.team_insights.agent_types))];
295
+
296
+ return {
297
+ total_changes: totalChanges,
298
+ process_health: {
299
+ avg_critical_path_coverage: Math.round(avgCriticalPath * 100) / 100,
300
+ spec_existence_rate: Math.round(specRate * 100) / 100,
301
+ avg_regression_rate: Math.round(avgRegressionRate * 100) / 100,
302
+ },
303
+ delivery_efficiency: {
304
+ avg_lead_time_ms: avgLeadTime != null ? Math.round(avgLeadTime) : null,
305
+ first_apply_success_rate: firstApplySuccessRate != null ? Math.round(firstApplySuccessRate * 100) / 100 : null,
306
+ },
307
+ quality_signals: {
308
+ first_check_pass_rate: firstCheckPassRate != null ? Math.round(firstCheckPassRate * 100) / 100 : null,
309
+ avg_apply_test_fix_cycles: Math.round(avgApplyTestCycles * 10) / 10,
310
+ },
311
+ team_insights: {
312
+ change_completion_rate: Math.round(changeCompletionRate * 100) / 100,
313
+ active_changes: activeChanges,
314
+ agent_distribution: allAgents,
315
+ },
316
+ };
317
+ }
318
+
319
+ // ── CLI 参数解析 ──────────────────────────────────────
320
+
321
+ /** 解析 --key=value 格式的参数 */
322
+ function parseArgs(argv) {
323
+ const args = {};
324
+ for (const arg of argv) {
325
+ const match = arg.match(/^--([a-zA-Z_-]+)=(.*)$/);
326
+ if (match) {
327
+ args[match[1]] = match[2];
328
+ } else if (arg.match(/^--([a-zA-Z_-]+)$/)) {
329
+ args[arg.slice(2)] = true;
330
+ }
331
+ }
332
+ return args;
333
+ }
334
+
335
+ // ── CLI 命令实现 ─────────────────────────────────────
336
+
337
+ /**
338
+ * log start: 记录阶段开始
339
+ */
340
+ function cmdStart(args) {
341
+ const command = args.command;
342
+ const projectRoot = args.project || args['project-root'] || process.cwd();
343
+ const changeName = args.change || args['change-name'] || '';
344
+ const agentType = args.agent || args['agent-type'] || 'unknown';
345
+
346
+ if (!command) {
347
+ console.error('错误: 缺少 --command 参数');
348
+ console.error('用法: node skywalk-sdd/log.js start --command=propose --project=/path');
349
+ process.exit(1);
350
+ }
351
+
352
+ const validCommands = ['propose', 'spec', 'design', 'task', 'check', 'apply', 'test', 'archive', 'explore'];
353
+ if (!validCommands.includes(command)) {
354
+ console.error(`错误: 无效的 command "${command}",有效值: ${validCommands.join(', ')}`);
355
+ process.exit(1);
356
+ }
357
+
358
+ const dataDir = getDataDir(projectRoot);
359
+ const eventId = generateEventId();
360
+ const timestamp = nowISO();
361
+
362
+ const event = {
363
+ event_id: eventId,
364
+ type: 'stage_start',
365
+ command,
366
+ change: changeName || 'general',
367
+ agent_type: agentType,
368
+ project_root: projectRoot,
369
+ timestamp,
370
+ context: {},
371
+ };
372
+
373
+ appendEvent(dataDir, event.change, event);
374
+
375
+ // 输出 JSON,方便 AI Agent 解析 event_id
376
+ const output = {
377
+ event_id: eventId,
378
+ started_at: timestamp,
379
+ message: `SDD ${command} 阶段开始记录(change: ${event.change})`,
380
+ };
381
+ console.log(JSON.stringify(output, null, 2));
382
+ }
383
+
384
+ /**
385
+ * log end: 记录阶段结束
386
+ */
387
+ function cmdEnd(args) {
388
+ const eventId = args['event-id'] || args.event_id;
389
+ const result = args.result;
390
+ const summary = args.summary || '';
391
+ const projectRoot = args.project || args['project-root'] || process.cwd();
392
+
393
+ if (!eventId) {
394
+ console.error('错误: 缺少 --event-id 参数');
395
+ console.error('用法: node skywalk-sdd/log.js end --event-id=evt_xxx --result=success --summary="..." --project=/path');
396
+ process.exit(1);
397
+ }
398
+ if (!result) {
399
+ console.error('错误: 缺少 --result 参数(success/failure/partial)');
400
+ process.exit(1);
401
+ }
402
+
403
+ // 从事件文件中查找对应的 start 事件
404
+ const dataDir = getDataDir(projectRoot);
405
+ const startEvent = searchEventInDataDir(dataDir, eventId);
406
+ const timestamp = nowISO();
407
+
408
+ const durationMs = startEvent
409
+ ? new Date(timestamp).getTime() - new Date(startEvent.timestamp).getTime()
410
+ : null;
411
+
412
+ const event = {
413
+ event_id: eventId,
414
+ type: 'stage_end',
415
+ command: startEvent?.command || 'unknown',
416
+ change: startEvent?.change || 'general',
417
+ agent_type: startEvent?.agent_type || 'unknown',
418
+ project_root: projectRoot,
419
+ timestamp,
420
+ duration_ms: durationMs,
421
+ result,
422
+ summary,
423
+ details: {},
424
+ };
425
+
426
+ appendEvent(dataDir, event.change, event);
427
+
428
+ const output = {
429
+ event_id: eventId,
430
+ duration_ms: durationMs,
431
+ recorded_at: timestamp,
432
+ message: `SDD ${event.command} 阶段结束(${result},耗时 ${durationMs ? (durationMs / 1000).toFixed(1) + 's' : '未知'})`,
433
+ };
434
+ console.log(JSON.stringify(output, null, 2));
435
+ }
436
+
437
+ /**
438
+ * 从事件文件中查找 start 事件(因为 CLI 无状态,需要从文件回溯)
439
+ */
440
+ function findStartEvent(eventId) {
441
+ // 尝试从当前目录的 skywalk-sdd 查找
442
+ const cwdDataDir = getDataDir(process.cwd());
443
+ const found = searchEventInDataDir(cwdDataDir, eventId);
444
+ if (found) return found;
445
+ return null;
446
+ }
447
+
448
+ function searchEventInDataDir(dataDir, eventId) {
449
+ const eventsDir = path.join(dataDir, 'events');
450
+ if (!fs.existsSync(eventsDir)) return null;
451
+
452
+ try {
453
+ const changeDirs = fs.readdirSync(eventsDir).filter(d => {
454
+ return fs.statSync(path.join(eventsDir, d)).isDirectory();
455
+ });
456
+
457
+ for (const changeDir of changeDirs) {
458
+ const dir = path.join(eventsDir, changeDir);
459
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')).sort().reverse();
460
+ for (const file of files) {
461
+ const lines = fs.readFileSync(path.join(dir, file), 'utf-8').split('\n').filter(Boolean);
462
+ for (const line of lines) {
463
+ try {
464
+ const event = JSON.parse(line);
465
+ if (event.event_id === eventId && event.type === 'stage_start') {
466
+ return event;
467
+ }
468
+ } catch {}
469
+ }
470
+ }
471
+ }
472
+ } catch {}
473
+ return null;
474
+ }
475
+
476
+ /**
477
+ * log metrics: 查询度量指标
478
+ */
479
+ function cmdMetrics(args) {
480
+ const projectRoot = args.project || args['project-root'] || process.cwd();
481
+ const changeName = args.change || args['change-name'];
482
+ const dateFrom = args['date-from'];
483
+ const dateTo = args['date-to'];
484
+
485
+ const dataDir = getDataDir(projectRoot);
486
+ let events = changeName ? readEvents(dataDir, changeName) : readAllEvents(dataDir);
487
+
488
+ // 日期过滤
489
+ if (dateFrom || dateTo) {
490
+ events = events.filter(e => {
491
+ const d = e.timestamp?.slice(0, 10);
492
+ if (dateFrom && d < dateFrom) return false;
493
+ if (dateTo && d > dateTo) return false;
494
+ return true;
495
+ });
496
+ }
497
+
498
+ let metrics;
499
+ if (changeName) {
500
+ metrics = computeChangeMetrics(changeName, events);
501
+ if (!metrics) {
502
+ console.log(JSON.stringify({ error: `未找到变更 "${changeName}" 的事件数据` }));
503
+ return;
504
+ }
505
+ } else {
506
+ metrics = computeOverviewMetrics(events);
507
+ }
508
+
509
+ console.log(JSON.stringify(metrics, null, 2));
510
+ }
511
+
512
+ // ── CLI 入口 ─────────────────────────────────────────
513
+
514
+ function main() {
515
+ // 支持两种调用方式:
516
+ // 1) npx kld-sdd log start ... → argv 含 'log'
517
+ // 2) node skywalk-sdd/log.js start ... → argv 直接以子命令开头
518
+ const argv = process.argv.slice(2);
519
+ const logIdx = argv.indexOf('log');
520
+ const subArgs = logIdx === -1 ? argv : argv.slice(logIdx + 1);
521
+
522
+ if (!subArgs.length || subArgs[0] === '-h' || subArgs[0] === '--help') {
523
+ showHelp();
524
+ process.exit(subArgs.length ? 0 : 1);
525
+ }
526
+ const subCommand = subArgs[0];
527
+ const flags = parseArgs(subArgs.slice(1));
528
+
529
+ switch (subCommand) {
530
+ case 'start':
531
+ cmdStart(flags);
532
+ break;
533
+ case 'end':
534
+ cmdEnd(flags);
535
+ break;
536
+ case 'metrics':
537
+ cmdMetrics(flags);
538
+ break;
539
+ default:
540
+ showHelp();
541
+ process.exit(1);
542
+ }
543
+ }
544
+
545
+ function showHelp() {
546
+ console.log(`
547
+ SDD Telemetry CLI - 流程度量采集工具
548
+
549
+ 用法:
550
+ node skywalk-sdd/log.js start --command=<cmd> --project=<path> [--change=<name>] [--agent=<type>]
551
+ node skywalk-sdd/log.js end --event-id=<id> --result=<success|failure|partial> --summary="..."
552
+ node skywalk-sdd/log.js metrics --project=<path> [--change=<name>]
553
+
554
+ 子命令:
555
+ start 记录 SDD 阶段开始,返回 event_id
556
+ end 记录 SDD 阶段结束,关联 event_id
557
+ metrics 查询度量指标(四维分析)
558
+
559
+ 示例:
560
+ node skywalk-sdd/log.js start --command=propose --project=/my/project --change=user-auth --agent=cursor
561
+ node skywalk-sdd/log.js end --event-id=evt_abc123 --result=success --summary="创建 proposal.md"
562
+ node skywalk-sdd/log.js metrics --project=/my/project --change=user-auth
563
+ `);
564
+ }
565
+
566
+ // 导出供测试使用
567
+ module.exports = {
568
+ main,
569
+ parseArgs,
570
+ cmdStart,
571
+ cmdEnd,
572
+ cmdMetrics,
573
+ computeChangeMetrics,
574
+ computeOverviewMetrics,
575
+ readEvents,
576
+ readAllEvents,
577
+ appendEvent,
578
+ getDataDir,
579
+ safeChangeName,
580
+ generateEventId,
581
+ findStartEvent,
582
+ };
583
+
584
+ // 直接运行时执行
585
+ if (require.main === module) {
586
+ main();
587
+ }