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.
- package/README.md +7 -5
- package/lib/init.js +118 -73
- package/lib/skills-bundle.js +3 -0
- package/package.json +3 -2
- package/skywalk-sdd/index.cjs +936 -109
- package/skywalk-sdd/metrics-v3.cjs +103 -15
- package/skywalk-sdd/openspec-shim.cjs +48 -0
- package/skywalk-sdd/reporting/change-report-markdown.cjs +294 -0
- package/skywalk-sdd/reporting/change-report-model.cjs +170 -27
- package/skywalk-sdd/reporting/change-report-renderer.cjs +78 -154
- package/skywalk-sdd/reporting/change-report-view-model.cjs +340 -0
- package/templates/git-hooks/commit-msg +46 -0
- package/templates/git-hooks/pre-commit +57 -0
- package/templates/git-hooks/pre-commit-consistency-check.cjs +193 -0
- package/templates/git-hooks/pre-push +57 -0
- package/templates/git-hooks/pre-push-consistency-check.cjs +197 -0
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +369 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +41 -0
- package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +108 -0
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +36 -1
- package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +248 -0
- package/templates/hooks/codebuddy/settings.json +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +13 -0
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +15 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +45 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +592 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +3 -3
- package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -7
- package/templates/skills/kld-sdd/tdd-core/reference.md +3 -1
- package/templates/skills/kld-sdd/tdd-rules/SKILL.md +1 -0
- package/templates/skills/kld-sdd/tdd-rules/rules/tdd-rhythm-enforcement.md +101 -0
- 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
|
|
25
|
-
|
|
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
|
|
84
|
-
const
|
|
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 =
|
|
88
|
-
|
|
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
|
|
126
|
-
const code =
|
|
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 +=
|
|
139
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 || '
|
|
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())
|
|
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:
|
|
330
|
+
size_bytes: sizeBytes,
|
|
331
|
+
size_display: sizeDisplay,
|
|
203
332
|
sha256: null,
|
|
204
|
-
integrity_note: '
|
|
333
|
+
integrity_note: '报告可展示自身大小;哈希不内嵌自身,请以归档 Manifest 为准',
|
|
205
334
|
};
|
|
206
335
|
}
|
|
207
336
|
const sha256 = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
208
|
-
return {
|
|
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:
|
|
283
|
-
? extractFinalTestResult(
|
|
284
|
-
: (existingSummary.tests || extractFinalTestResult(
|
|
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
|
};
|