kld-sdd 2.6.14 → 2.6.16

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.
Files changed (33) hide show
  1. package/README.md +7 -5
  2. package/lib/init.js +118 -73
  3. package/lib/skills-bundle.js +3 -0
  4. package/package.json +3 -2
  5. package/skywalk-sdd/index.cjs +936 -109
  6. package/skywalk-sdd/metrics-v3.cjs +103 -15
  7. package/skywalk-sdd/openspec-shim.cjs +48 -0
  8. package/skywalk-sdd/reporting/change-report-markdown.cjs +294 -0
  9. package/skywalk-sdd/reporting/change-report-model.cjs +170 -27
  10. package/skywalk-sdd/reporting/change-report-renderer.cjs +78 -154
  11. package/skywalk-sdd/reporting/change-report-view-model.cjs +340 -0
  12. package/templates/git-hooks/commit-msg +46 -0
  13. package/templates/git-hooks/pre-commit +57 -0
  14. package/templates/git-hooks/pre-commit-consistency-check.cjs +193 -0
  15. package/templates/git-hooks/pre-push +57 -0
  16. package/templates/git-hooks/pre-push-consistency-check.cjs +197 -0
  17. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +369 -0
  18. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +41 -0
  19. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +108 -0
  20. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +36 -1
  21. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +248 -0
  22. package/templates/hooks/codebuddy/settings.json +8 -0
  23. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +13 -0
  24. package/templates/skills/kld-sdd/opsx-apply/checklist.md +15 -0
  25. package/templates/skills/kld-sdd/opsx-apply/reference.md +45 -2
  26. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +1 -1
  27. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +592 -0
  28. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +3 -3
  29. package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -7
  30. package/templates/skills/kld-sdd/tdd-core/reference.md +3 -1
  31. package/templates/skills/kld-sdd/tdd-rules/SKILL.md +1 -0
  32. package/templates/skills/kld-sdd/tdd-rules/rules/tdd-rhythm-enforcement.md +101 -0
  33. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
@@ -4,10 +4,31 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const crypto = require('crypto');
6
6
 
7
+ let formatArtifactSize;
8
+ try {
9
+ ({ formatArtifactSize } = require('../metrics-v3.cjs'));
10
+ } catch {
11
+ formatArtifactSize = null;
12
+ }
13
+
14
+ function formatArtifactSizeDisplay(bytes) {
15
+ if (typeof formatArtifactSize === 'function') return formatArtifactSize(bytes);
16
+ const n = Number(bytes);
17
+ if (!Number.isFinite(n) || n < 0) return '0.00 KB';
18
+ const mib = 1024 * 1024;
19
+ if (n < mib) return `${(n / 1024).toFixed(2)} KB`;
20
+ return `${(n / mib).toFixed(2)} MB`;
21
+ }
22
+
7
23
  const EVIDENCE_LABELS = {
8
24
  TASK_UPDATE_MISSING: '任务状态事件缺失',
9
25
  TASK_TEST_EVIDENCE_MISSING: '任务验证证据缺失',
26
+ TASK_TEST_LINKAGE_DEFECT: '任务测试链接缺陷',
10
27
  TDD_RED_GREEN_INCOMPLETE: 'TDD 红绿链路不完整',
28
+ TDD_PAIR_RED_DID_NOT_FAIL: 'TDD RED 未有效失败',
29
+ TDD_PAIR_RED_MISSING: 'TDD RED 证据缺失',
30
+ TDD_PAIR_GREEN_MISSING: 'TDD GREEN 证据缺失',
31
+ TDD_PAIR_UNCLASSIFIED: 'TDD 对证据不可判定',
11
32
  LEGACY_TASK_PARSER: '任务文档仍使用旧格式',
12
33
  apply_test_missing: '实现阶段缺少测试证据',
13
34
  tdd_red_missing: 'TDD 缺少有效失败测试',
@@ -21,8 +42,16 @@ function eventTimestamp(event) {
21
42
  return Number.isFinite(value) ? value : 0;
22
43
  }
23
44
 
24
- function extractFinalTestResult(events = [], fallback = null) {
25
- const testEvents = events
45
+ function scopedEventsForChange(events = [], change = null) {
46
+ if (!change) return events;
47
+ return events.filter((event) => (
48
+ event?.change === change
49
+ || event?.attribution?.target_change === change
50
+ ));
51
+ }
52
+
53
+ function extractFinalTestResult(events = [], fallback = null, change = null) {
54
+ const testEvents = scopedEventsForChange(events, change)
26
55
  .filter((event) => event?.type === 'test_result')
27
56
  .sort((a, b) => eventTimestamp(b) - eventTimestamp(a));
28
57
  const candidates = testEvents
@@ -80,12 +109,25 @@ function extractFinalTestResult(events = [], fallback = null) {
80
109
  function normalizeTaskSummary(report) {
81
110
  const completion = report.archive_result?.task_completion || {};
82
111
  const primary = completion.primary_tasks || {};
83
- const documentTotal = primary.total ?? completion.total ?? null;
84
- const documentCompleted = primary.completed ?? completion.completed ?? null;
112
+ const taskEvidence = report.task_evidence_audit || {};
113
+ const documentTotal = taskEvidence.document_completion?.total
114
+ ?? primary.total
115
+ ?? completion.total
116
+ ?? null;
117
+ const documentCompleted = taskEvidence.document_completion?.completed
118
+ ?? primary.completed
119
+ ?? completion.completed
120
+ ?? null;
85
121
  const audit = report.apply_evidence_audit || {};
86
122
  const auditTasks = audit.tasks || audit.summary || {};
87
- const traceabilityVerified = auditTasks.with_test_evidence ?? auditTasks.verified ?? null;
88
- const traceabilityTotal = auditTasks.completed ?? auditTasks.total ?? documentTotal;
123
+ const traceabilityVerified = taskEvidence.strict_test_coverage?.verified
124
+ ?? auditTasks.with_test_evidence
125
+ ?? auditTasks.verified
126
+ ?? null;
127
+ const traceabilityTotal = taskEvidence.strict_test_coverage?.total
128
+ ?? auditTasks.completed
129
+ ?? auditTasks.total
130
+ ?? documentTotal;
89
131
  return {
90
132
  document: {
91
133
  completed: documentCompleted,
@@ -95,6 +137,8 @@ function normalizeTaskSummary(report) {
95
137
  : (documentCompleted === documentTotal ? 'complete' : 'incomplete'),
96
138
  source: 'tasks-document',
97
139
  },
140
+ direct_test_backlink: taskEvidence.direct_test_backlink || null,
141
+ strict_test_coverage: taskEvidence.strict_test_coverage || null,
98
142
  traceability: {
99
143
  verified: traceabilityVerified,
100
144
  total: traceabilityTotal,
@@ -106,6 +150,81 @@ function normalizeTaskSummary(report) {
106
150
  };
107
151
  }
108
152
 
153
+ function issueGuidance(code) {
154
+ return ({
155
+ TASK_UPDATE_MISSING: {
156
+ expected: '已完成任务应有对应的 completed task_update 事件。',
157
+ root_cause: '任务文档状态与结构化事件没有同步。',
158
+ impact: '任务完成状态不能被事件链直接验证。',
159
+ remediation: '补录真实 task_update;若历史证据不存在,明确标记不可追溯。',
160
+ },
161
+ TASK_TEST_LINKAGE_DEFECT: {
162
+ expected: 'task_update 应直接引用覆盖当前任务的 strict 成功测试。',
163
+ root_cause: 'test_event_id 缺失、无效或没有覆盖当前任务。',
164
+ impact: '测试存在,但任务到测试的直接审计链不完整。',
165
+ remediation: '将 task_update 精确关联到覆盖当前任务的 strict 测试事件。',
166
+ },
167
+ TASK_TEST_EVIDENCE_MISSING: {
168
+ expected: '任务应有严格成功测试,或有可审计的不适用理由。',
169
+ root_cause: '没有找到直接引用、间接严格覆盖或有效豁免。',
170
+ impact: '无法验证任务完成状态是否经过测试。',
171
+ remediation: '执行并记录覆盖测试;确实不适用时填写具体原因。',
172
+ },
173
+ TDD_RED_GREEN_INCOMPLETE: {
174
+ expected: 'TDD 证据应按 pair 形成有效 RED→GREEN 顺序。',
175
+ root_cause: '旧版逐任务算法无法正确表达成对 TDD 证据。',
176
+ impact: '可能把已有配对证据误报为缺失。',
177
+ remediation: '使用 pair 级审计结果,不再生成逐任务 TDD 告警。',
178
+ },
179
+ }[code] || {
180
+ expected: '相关规则所要求的证据应完整且可定位。',
181
+ root_cause: '当前事件或文档证据未满足该规则。',
182
+ impact: '对应结论的可信度会降低。',
183
+ remediation: '根据规则编码定位证据源,补充或纠正真实记录。',
184
+ });
185
+ }
186
+
187
+ function normalizeIssue(alert, index) {
188
+ const code = alert.warning || alert.code || 'UNKNOWN';
189
+ const guidance = issueGuidance(code);
190
+ const eventIds = [...new Set([
191
+ alert.opened_by_event_id,
192
+ alert.last_seen_event_id,
193
+ alert.resolved_by_event_id,
194
+ alert.evidence_event_id,
195
+ ...(Array.isArray(alert.evidence_event_ids) ? alert.evidence_event_ids : []),
196
+ ].filter(Boolean))];
197
+ const subject = alert.subject || alert.task_id || alert.pair_id || null;
198
+ const status = alert.status
199
+ || (['fixed', 'accepted', 'waived'].includes(alert.disposition) ? 'resolved' : 'open');
200
+ return {
201
+ issue_id: alert.issue_id || alert.key || `${code}:${subject || 'change'}:${index + 1}`,
202
+ category: alert.category || 'evidence',
203
+ code,
204
+ label: EVIDENCE_LABELS[code] || code,
205
+ severity: alert.severity || 'warning',
206
+ verdict: alert.verdict || (status === 'resolved' ? 'resolved' : 'evidence-gap'),
207
+ status,
208
+ subject,
209
+ task_id: alert.task_id || null,
210
+ pair_id: alert.pair_id || null,
211
+ expected: alert.expected || guidance.expected,
212
+ actual: alert.actual || alert.message || alert.reason || '当前证据未满足规则要求。',
213
+ root_cause: alert.root_cause || guidance.root_cause,
214
+ impact: alert.impact || guidance.impact,
215
+ remediation: alert.remediation || guidance.remediation,
216
+ evidence_event_ids: eventIds,
217
+ related_event_ids: Array.isArray(alert.related_event_ids) ? alert.related_event_ids : [],
218
+ disposition: alert.disposition || (status === 'resolved' ? 'fixed' : 'open'),
219
+ occurrence_count: Math.max(1, Number(alert.occurrences) || 1),
220
+ history: {
221
+ first_seen_at: alert.first_seen_at || null,
222
+ last_seen_at: alert.last_seen_at || null,
223
+ resolved_at: alert.resolved_at || null,
224
+ },
225
+ };
226
+ }
227
+
109
228
  function normalizeEvidenceSummary(report) {
110
229
  const alerts = report.evidence_alerts || {};
111
230
  const alertHistory = Array.isArray(alerts.history) && alerts.history.length > 0
@@ -121,9 +240,10 @@ function normalizeEvidenceSummary(report) {
121
240
  last_seen_event_id: item.evidence_event_id,
122
241
  })),
123
242
  ];
243
+ const issues = history.map(normalizeIssue);
124
244
  const groups = new Map();
125
- for (const alert of history) {
126
- const code = alert.warning || alert.code || 'UNKNOWN';
245
+ for (const issue of issues) {
246
+ const code = issue.code;
127
247
  if (!groups.has(code)) {
128
248
  groups.set(code, {
129
249
  code,
@@ -132,16 +252,18 @@ function normalizeEvidenceSummary(report) {
132
252
  affected_tasks: [],
133
253
  event_ids: [],
134
254
  dispositions: {},
255
+ issues: [],
135
256
  });
136
257
  }
137
258
  const group = groups.get(code);
138
- group.occurrence_count += Math.max(1, Number(alert.occurrences) || 1);
139
- const task = alert.task_id || alert.subject;
259
+ group.occurrence_count += issue.occurrence_count;
260
+ group.issues.push(issue);
261
+ const task = issue.task_id || issue.subject;
140
262
  if (task && !group.affected_tasks.includes(task)) group.affected_tasks.push(task);
141
- for (const eventId of [alert.opened_by_event_id, alert.last_seen_event_id, alert.resolved_by_event_id]) {
263
+ for (const eventId of issue.evidence_event_ids) {
142
264
  if (eventId && !group.event_ids.includes(eventId)) group.event_ids.push(eventId);
143
265
  }
144
- const disposition = alert.disposition || (alert.status === 'resolved' ? 'fixed' : 'open');
266
+ const disposition = issue.disposition;
145
267
  group.dispositions[disposition] = (group.dispositions[disposition] || 0) + 1;
146
268
  }
147
269
  const categories = [...groups.values()];
@@ -149,6 +271,7 @@ function normalizeEvidenceSummary(report) {
149
271
  category_count: categories.length,
150
272
  occurrence_count: categories.reduce((sum, item) => sum + item.occurrence_count, 0),
151
273
  categories,
274
+ issues,
152
275
  note: '一次任务或事件可能同时命中多条规则;命中次数不等于独立缺陷数。',
153
276
  };
154
277
  }
@@ -168,46 +291,58 @@ function normalizeChangedFiles(changedFiles = {}) {
168
291
  ?? finalOutputFiles?.length
169
292
  ?? null;
170
293
  const countsDiffer = taskCount != null && finalCount != null && taskCount !== finalCount;
171
- return {
294
+ const normalized = {
172
295
  ...changedFiles,
173
296
  task_event_files: taskEventFiles,
174
297
  task_event_files_changed: taskCount,
175
298
  final_output_files: finalOutputFiles,
176
299
  final_output_files_changed: finalCount,
177
- diff_source: changedFiles.diff_source || 'legacy-fields',
300
+ diff_source: changedFiles.diff_source || 'dual-source',
178
301
  line_count_estimate: changedFiles.line_count_estimate ?? null,
302
+ reconciliation_status: changedFiles.reconciliation_status
303
+ || (countsDiffer ? 'count-mismatch' : 'consistent'),
179
304
  reconciliation_note: changedFiles.reconciliation_note
180
305
  || (countsDiffer
181
306
  ? `任务事件累计涉及 ${taskCount} 个文件;最终输出快照包含 ${finalCount} 个文件。两者用途不同,不应合并为一个口径。`
182
307
  : '任务事件与最终输出文件口径一致。'),
183
- legacy_fields: {
184
- files: { deprecated: true, ambiguous: true },
185
- files_changed: { deprecated: true, ambiguous: true },
186
- },
187
308
  };
309
+ // v4:不再对外输出 legacy_fields;若调用方传入则显式剔除
310
+ delete normalized.legacy_fields;
311
+ return normalized;
188
312
  }
189
313
 
190
314
  function resolveArtifact(projectRoot, artifactPath, options = {}) {
191
- if (!artifactPath) return { path: null, exists: false, size_bytes: null, sha256: null };
315
+ if (!artifactPath) return { path: null, exists: false, size_bytes: null, size_display: null, sha256: null };
192
316
  const absolute = path.isAbsolute(artifactPath)
193
317
  ? artifactPath
194
318
  : path.resolve(projectRoot || process.cwd(), artifactPath);
195
319
  try {
196
320
  const stat = fs.statSync(absolute);
197
- if (!stat.isFile()) return { path: artifactPath, exists: false, size_bytes: null, sha256: null };
321
+ if (!stat.isFile()) {
322
+ return { path: artifactPath, exists: false, size_bytes: null, size_display: null, sha256: null };
323
+ }
324
+ const sizeBytes = stat.size;
325
+ const sizeDisplay = formatArtifactSizeDisplay(sizeBytes);
198
326
  if (options.selfReferential) {
199
327
  return {
200
328
  path: artifactPath,
201
329
  exists: true,
202
- size_bytes: null,
330
+ size_bytes: sizeBytes,
331
+ size_display: sizeDisplay,
203
332
  sha256: null,
204
- integrity_note: '报告不能内嵌自身最终大小与哈希;请以归档 Manifest 为准',
333
+ integrity_note: '报告可展示自身大小;哈希不内嵌自身,请以归档 Manifest 为准',
205
334
  };
206
335
  }
207
336
  const sha256 = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
208
- return { path: artifactPath, exists: true, size_bytes: stat.size, sha256: `sha256:${sha256}` };
337
+ return {
338
+ path: artifactPath,
339
+ exists: true,
340
+ size_bytes: sizeBytes,
341
+ size_display: sizeDisplay,
342
+ sha256: `sha256:${sha256}`,
343
+ };
209
344
  } catch {
210
- return { path: artifactPath, exists: false, size_bytes: null, sha256: null };
345
+ return { path: artifactPath, exists: false, size_bytes: null, size_display: null, sha256: null };
211
346
  }
212
347
  }
213
348
 
@@ -276,20 +411,27 @@ function buildWarningDispositions(report) {
276
411
 
277
412
  function buildChangeReportModel(report, events = []) {
278
413
  if (!report || report.level !== 'change' || !report.change) return report;
414
+ const scopedEvents = scopedEventsForChange(events, report.change);
415
+ const inputEventCount = Array.isArray(events) ? events.length : 0;
279
416
  const changedFiles = normalizeChangedFiles(report.changed_files);
280
417
  const existingSummary = report.change_summary || {};
281
418
  const changeSummary = {
282
- tests: events.length > 0
283
- ? extractFinalTestResult(events, report.final_test_result || existingSummary.tests || null)
284
- : (existingSummary.tests || extractFinalTestResult(events, report.final_test_result || null)),
419
+ tests: scopedEvents.length > 0
420
+ ? extractFinalTestResult(scopedEvents, report.final_test_result || existingSummary.tests || null, report.change)
421
+ : (existingSummary.tests || extractFinalTestResult(scopedEvents, report.final_test_result || null, report.change)),
285
422
  tasks: existingSummary.tasks || normalizeTaskSummary(report),
286
423
  evidence: existingSummary.evidence || normalizeEvidenceSummary(report),
287
424
  };
288
425
  return {
289
426
  ...report,
290
427
  report_scope: {
428
+ ...(report.report_scope || {}),
291
429
  kind: 'single-change',
292
430
  change: report.change,
431
+ input_event_count: report.report_scope?.input_event_count ?? inputEventCount,
432
+ included_event_count: report.report_scope?.included_event_count ?? scopedEvents.length,
433
+ excluded_event_count: report.report_scope?.excluded_event_count
434
+ ?? Math.max(0, inputEventCount - scopedEvents.length),
293
435
  cross_change_data_included: false,
294
436
  },
295
437
  metric_provenance: report.metric_provenance || report.metric_evidence || null,
@@ -306,4 +448,5 @@ module.exports = {
306
448
  extractFinalTestResult,
307
449
  normalizeEvidenceSummary,
308
450
  normalizeChangedFiles,
451
+ formatArtifactSizeDisplay,
309
452
  };