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
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared change-report view model for Markdown / HTML / parity digests.
|
|
7
|
+
* Four desktop tabs: 概览 / 指标与阶段 / 问题与证据 / 产物与追溯
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SECTION_ORDER = [
|
|
11
|
+
'overview',
|
|
12
|
+
'metrics-stages',
|
|
13
|
+
'issues-evidence',
|
|
14
|
+
'artifacts-trace',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SECTION_TITLES = {
|
|
18
|
+
overview: '概览',
|
|
19
|
+
'metrics-stages': '指标与阶段',
|
|
20
|
+
'issues-evidence': '问题与证据',
|
|
21
|
+
'artifacts-trace': '产物与追溯',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function fractionText(verified, total) {
|
|
25
|
+
if (verified == null || total == null) return '暂无数据';
|
|
26
|
+
return `${verified}/${total}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function percentText(value, digits = 2) {
|
|
30
|
+
return Number.isFinite(Number(value))
|
|
31
|
+
? `${(Number(value) * 100).toFixed(digits)}%`
|
|
32
|
+
: '暂无数据';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function durationText(value) {
|
|
36
|
+
if (!Number.isFinite(Number(value))) return '暂无数据';
|
|
37
|
+
const totalSeconds = Math.round(Number(value) / 1000);
|
|
38
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
39
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
40
|
+
const seconds = totalSeconds % 60;
|
|
41
|
+
return [hours ? `${hours}小时` : '', minutes ? `${minutes}分` : '', `${seconds}秒`]
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function localTimeText(value) {
|
|
47
|
+
if (!value) return '—';
|
|
48
|
+
const parsed = new Date(value);
|
|
49
|
+
return Number.isNaN(parsed.getTime())
|
|
50
|
+
? '—'
|
|
51
|
+
: parsed.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildSemanticDigestInput(report, sections) {
|
|
55
|
+
const audit = report.task_evidence_audit || {};
|
|
56
|
+
const tdd = report.tdd_pair_audit || {};
|
|
57
|
+
const changed = report.changed_files || {};
|
|
58
|
+
const evidence = report.change_summary?.evidence || {};
|
|
59
|
+
const artifacts = report.artifacts?.files || {};
|
|
60
|
+
return {
|
|
61
|
+
schema_version: report.schema_version || null,
|
|
62
|
+
metrics_contract: report.metrics_contract || null,
|
|
63
|
+
change: report.change || null,
|
|
64
|
+
section_ids: SECTION_ORDER.slice(),
|
|
65
|
+
conclusion_status: report.conclusion?.status || null,
|
|
66
|
+
tasks: {
|
|
67
|
+
document: report.change_summary?.tasks?.document || null,
|
|
68
|
+
direct_test_backlink: audit.direct_test_backlink || null,
|
|
69
|
+
strict_test_coverage: audit.strict_test_coverage || null,
|
|
70
|
+
traceability: report.change_summary?.tasks?.traceability || null,
|
|
71
|
+
},
|
|
72
|
+
tdd_pairs: {
|
|
73
|
+
required: tdd.required ?? null,
|
|
74
|
+
complete: tdd.complete ?? null,
|
|
75
|
+
},
|
|
76
|
+
tests: report.change_summary?.tests || null,
|
|
77
|
+
evidence: {
|
|
78
|
+
category_count: evidence.category_count ?? null,
|
|
79
|
+
occurrence_count: evidence.occurrence_count ?? null,
|
|
80
|
+
issues: (evidence.issues || []).map((issue) => ({
|
|
81
|
+
issue_id: issue.issue_id,
|
|
82
|
+
code: issue.code,
|
|
83
|
+
status: issue.status,
|
|
84
|
+
verdict: issue.verdict,
|
|
85
|
+
subject: issue.subject,
|
|
86
|
+
expected: issue.expected,
|
|
87
|
+
actual: issue.actual,
|
|
88
|
+
root_cause: issue.root_cause,
|
|
89
|
+
remediation: issue.remediation,
|
|
90
|
+
evidence_event_ids: issue.evidence_event_ids,
|
|
91
|
+
disposition: issue.disposition,
|
|
92
|
+
})),
|
|
93
|
+
},
|
|
94
|
+
changed_files: {
|
|
95
|
+
task_event_files_changed: changed.task_event_files_changed ?? null,
|
|
96
|
+
final_output_files_changed: changed.final_output_files_changed ?? null,
|
|
97
|
+
added_lines: changed.added_lines ?? null,
|
|
98
|
+
deleted_lines: changed.deleted_lines ?? null,
|
|
99
|
+
diff_source: changed.diff_source ?? null,
|
|
100
|
+
reconciliation_status: changed.reconciliation_status ?? null,
|
|
101
|
+
},
|
|
102
|
+
attribution: report.attribution_audit || null,
|
|
103
|
+
artifact_sizes: Object.fromEntries(
|
|
104
|
+
Object.entries(artifacts).map(([name, item]) => [name, item?.size_bytes ?? null]),
|
|
105
|
+
),
|
|
106
|
+
section_summaries: Object.fromEntries(
|
|
107
|
+
SECTION_ORDER.map((id) => [id, sections[id]?.summary || null]),
|
|
108
|
+
),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildChangeReportViewModel(report = {}) {
|
|
113
|
+
const summary = report.change_summary || {};
|
|
114
|
+
const audit = report.task_evidence_audit || {};
|
|
115
|
+
const tdd = report.tdd_pair_audit || {};
|
|
116
|
+
const changed = report.changed_files || {};
|
|
117
|
+
const evidence = summary.evidence || { categories: [], category_count: 0, occurrence_count: 0 };
|
|
118
|
+
const artifacts = report.artifacts?.files || {};
|
|
119
|
+
const metrics = report.metrics || {};
|
|
120
|
+
const efficiency = metrics.efficiency || {};
|
|
121
|
+
const quality = metrics.quality || {};
|
|
122
|
+
const process = metrics.process || {};
|
|
123
|
+
const telemetry = metrics.telemetry_health || {};
|
|
124
|
+
const minSet = report.minimum_metric_set || {};
|
|
125
|
+
const openIssueCount = (report.evidence_alerts?.open || []).length;
|
|
126
|
+
const totalDurationMs = efficiency.total_duration_ms
|
|
127
|
+
?? efficiency.effective_stage_duration_including_rework_ms
|
|
128
|
+
?? efficiency.e1_lead_time_ms
|
|
129
|
+
?? null;
|
|
130
|
+
const e1 = efficiency.e1_scenario_delivery_efficiency_ms ?? null;
|
|
131
|
+
const e2 = efficiency.e2_coding_time_ratio_including_rework
|
|
132
|
+
?? efficiency.e2_coding_time_ratio
|
|
133
|
+
?? null;
|
|
134
|
+
const e3 = efficiency.e3_spec_time_ratio_including_rework
|
|
135
|
+
?? efficiency.e3_spec_time_ratio
|
|
136
|
+
?? null;
|
|
137
|
+
const metricCards = [
|
|
138
|
+
{
|
|
139
|
+
name: '每个场景平均用时',
|
|
140
|
+
code: 'E1',
|
|
141
|
+
value: Number.isFinite(e1) ? `${durationText(e1)} / 场景` : '暂无数据',
|
|
142
|
+
state: (
|
|
143
|
+
minSet.evidence_status?.e1 === 'trusted'
|
|
144
|
+
|| minSet.trusted_metrics?.includes?.('e1')
|
|
145
|
+
|| minSet.trusted?.includes?.('e1')
|
|
146
|
+
) ? '可信' : '需补证',
|
|
147
|
+
explanation: '七个标准阶段总耗时(含返工)÷ 验收场景数。',
|
|
148
|
+
source: `公式:标准阶段总耗时 ÷ 场景数。场景数:${efficiency.scenario_count ?? '未知'};当前值:${e1 ?? '不可计算'} ms。`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: '规约检查得分',
|
|
152
|
+
code: 'Q3',
|
|
153
|
+
value: quality.q3_spec_quality_score == null ? '暂无数据' : `${quality.q3_spec_quality_score} / 100`,
|
|
154
|
+
state: quality.q3_score_status === 'verified' ? '已验证' : '临时',
|
|
155
|
+
explanation: '最新规约检查结果;独立复核后才标记为已验证。',
|
|
156
|
+
source: `评审独立性:${quality.q3_reviewer_independence || '未知'};证据状态:${quality.q3_score_status || '未知'}。`,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: '编码前是否先检查',
|
|
160
|
+
code: 'P4',
|
|
161
|
+
value: percentText(process.p4_quality_gate_enforcement_rate),
|
|
162
|
+
state: Number.isFinite(process.p4_quality_gate_enforcement_rate) ? '可计算' : '需补证',
|
|
163
|
+
explanation: '首次进入编码前,是否存在已配对且成功或部分通过的检查。',
|
|
164
|
+
source: '只接受能与 check 开始事件配对、且发生在首次 apply 之前的成功或部分通过结果。',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: '阶段重复次数',
|
|
168
|
+
code: 'P-R',
|
|
169
|
+
value: process.rework_summary?.total_rework_attempts == null
|
|
170
|
+
? '暂无数据'
|
|
171
|
+
: `${process.rework_summary.total_rework_attempts} 次`,
|
|
172
|
+
state: process.rework_summary ? '可计算' : '需补证',
|
|
173
|
+
explanation: '七个标准阶段中,首次执行之后的重复尝试次数。',
|
|
174
|
+
source: '辅助 test / explore 阶段不计入标准阶段重复总数。',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: '执行记录是否完整',
|
|
178
|
+
code: 'P-H',
|
|
179
|
+
value: telemetry.p_h_telemetry_health_score == null
|
|
180
|
+
? '暂无数据'
|
|
181
|
+
: `${telemetry.p_h_telemetry_health_score} / 100`,
|
|
182
|
+
state: telemetry.p_h_telemetry_health_score == null ? '需补证' : '可计算',
|
|
183
|
+
explanation: '标准阶段的身份、开始结束配对和数据质量综合评分。',
|
|
184
|
+
source: '沿用既有权重,但只以 propose、spec、design、task、check、apply、archive 七阶段为计算范围。',
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const sections = {
|
|
189
|
+
overview: {
|
|
190
|
+
id: 'overview',
|
|
191
|
+
title: SECTION_TITLES.overview,
|
|
192
|
+
summary: report.conclusion?.text || '当前证据不足,暂不能形成可信结论。',
|
|
193
|
+
scope_duration: durationText(totalDurationMs),
|
|
194
|
+
cards: [
|
|
195
|
+
{
|
|
196
|
+
key: 'final_tests',
|
|
197
|
+
label: '最终测试',
|
|
198
|
+
value: fractionText(summary.tests?.passed, summary.tests?.total),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
key: 'document_tasks',
|
|
202
|
+
label: '文档任务完成',
|
|
203
|
+
value: fractionText(summary.tasks?.document?.completed, summary.tasks?.document?.total),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: 'direct_backlink',
|
|
207
|
+
label: '直接测试回链',
|
|
208
|
+
value: fractionText(
|
|
209
|
+
audit.direct_test_backlink?.verified,
|
|
210
|
+
audit.direct_test_backlink?.total,
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
key: 'strict_coverage',
|
|
215
|
+
label: '严格测试覆盖',
|
|
216
|
+
value: fractionText(
|
|
217
|
+
audit.strict_test_coverage?.verified ?? summary.tasks?.traceability?.verified,
|
|
218
|
+
audit.strict_test_coverage?.total ?? summary.tasks?.traceability?.total,
|
|
219
|
+
),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
key: 'open_issues',
|
|
223
|
+
label: '开放证据问题',
|
|
224
|
+
value: String(openIssueCount),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
metric_cards: metricCards,
|
|
228
|
+
},
|
|
229
|
+
'metrics-stages': {
|
|
230
|
+
id: 'metrics-stages',
|
|
231
|
+
title: SECTION_TITLES['metrics-stages'],
|
|
232
|
+
summary: '本次变更效率、质量、过程与阶段执行',
|
|
233
|
+
metrics: {
|
|
234
|
+
total_duration_ms: totalDurationMs,
|
|
235
|
+
e1,
|
|
236
|
+
e2,
|
|
237
|
+
e3,
|
|
238
|
+
q3: quality.q3_spec_quality_score ?? null,
|
|
239
|
+
p4: process.p4_quality_gate_enforcement_rate ?? null,
|
|
240
|
+
p_r: process.rework_summary?.total_rework_attempts ?? null,
|
|
241
|
+
p_h: telemetry.p_h_telemetry_health_score ?? null,
|
|
242
|
+
},
|
|
243
|
+
metric_cards: metricCards,
|
|
244
|
+
metric_rows: [
|
|
245
|
+
{ label: '七阶段总耗时', value: durationText(totalDurationMs) },
|
|
246
|
+
{ label: '每个场景平均用时', code: 'E1', value: metricCards[0].value },
|
|
247
|
+
{ label: '编码时间占比', code: 'E2', value: percentText(e2) },
|
|
248
|
+
{ label: '规划与规约时间占比', code: 'E3', value: percentText(e3) },
|
|
249
|
+
{ label: '阶段重复执行', code: 'P-R', value: metricCards[3].value },
|
|
250
|
+
],
|
|
251
|
+
stages: (report.stage_timeline?.nodes || []).map((node) => ({
|
|
252
|
+
...node,
|
|
253
|
+
display_stage: node.stage || node.command || '未知阶段',
|
|
254
|
+
display_round: node.round || 1,
|
|
255
|
+
display_start: localTimeText(node.start_ts),
|
|
256
|
+
display_end: localTimeText(node.end_ts),
|
|
257
|
+
display_duration: durationText(node.duration_ms),
|
|
258
|
+
display_result: node.result || '未知',
|
|
259
|
+
})),
|
|
260
|
+
tdd_pairs: {
|
|
261
|
+
required: tdd.required ?? null,
|
|
262
|
+
complete: tdd.complete ?? null,
|
|
263
|
+
pairs: tdd.pairs || [],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
'issues-evidence': {
|
|
267
|
+
id: 'issues-evidence',
|
|
268
|
+
title: SECTION_TITLES['issues-evidence'],
|
|
269
|
+
summary: evidence.note
|
|
270
|
+
|| `${evidence.category_count || 0} 类问题,${evidence.occurrence_count || 0} 次规则命中`,
|
|
271
|
+
categories: evidence.categories || [],
|
|
272
|
+
issues: evidence.issues || [],
|
|
273
|
+
open_alerts: report.evidence_alerts?.open || [],
|
|
274
|
+
task_evidence_audit: audit,
|
|
275
|
+
apply_findings: report.apply_evidence_audit?.findings || [],
|
|
276
|
+
warning_dispositions: report.warning_dispositions || [],
|
|
277
|
+
},
|
|
278
|
+
'artifacts-trace': {
|
|
279
|
+
id: 'artifacts-trace',
|
|
280
|
+
title: SECTION_TITLES['artifacts-trace'],
|
|
281
|
+
summary: changed.reconciliation_note || '产物与文件对账',
|
|
282
|
+
changed_files: {
|
|
283
|
+
task_event_files_changed: changed.task_event_files_changed ?? null,
|
|
284
|
+
final_output_files_changed: changed.final_output_files_changed ?? null,
|
|
285
|
+
added_lines: changed.added_lines ?? null,
|
|
286
|
+
deleted_lines: changed.deleted_lines ?? null,
|
|
287
|
+
diff_source: changed.diff_source || null,
|
|
288
|
+
reconciliation_status: changed.reconciliation_status ?? null,
|
|
289
|
+
reconciliation_note: changed.reconciliation_note || null,
|
|
290
|
+
final_output_files: changed.final_output_files || changed.files || null,
|
|
291
|
+
only_in_task_events: changed.only_in_task_events || [],
|
|
292
|
+
only_in_final_output: changed.only_in_final_output || [],
|
|
293
|
+
},
|
|
294
|
+
artifacts: Object.entries(artifacts).map(([name, item]) => ({
|
|
295
|
+
name,
|
|
296
|
+
path: item?.path || null,
|
|
297
|
+
exists: Boolean(item?.exists),
|
|
298
|
+
size_bytes: item?.size_bytes ?? null,
|
|
299
|
+
size_display: item?.size_display || null,
|
|
300
|
+
sha256: item?.sha256 ?? null,
|
|
301
|
+
integrity_note: item?.integrity_note || null,
|
|
302
|
+
})),
|
|
303
|
+
attribution: report.attribution_audit || null,
|
|
304
|
+
attribution_summary: {
|
|
305
|
+
linked_source_event_count: report.attribution_audit?.linked_source_event_count ?? 0,
|
|
306
|
+
active_link_count: report.attribution_audit?.active_links?.length ?? 0,
|
|
307
|
+
unassigned_candidate_count: report.attribution_audit?.unassigned_candidate_event_ids?.length ?? 0,
|
|
308
|
+
rejected_collision_count: report.attribution_audit?.rejected_direct_collision_event_ids?.length ?? 0,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
change: report.change || null,
|
|
315
|
+
schema_version: report.schema_version || null,
|
|
316
|
+
metrics_contract: report.metrics_contract || null,
|
|
317
|
+
generated_at: report.generated_at || null,
|
|
318
|
+
conclusion: report.conclusion || null,
|
|
319
|
+
minimum_metric_set: report.minimum_metric_set || null,
|
|
320
|
+
scheme_compliance: report.scheme_compliance || null,
|
|
321
|
+
known_risks: report.known_risks || null,
|
|
322
|
+
process_notes: report.process_notes || null,
|
|
323
|
+
telemetry_warnings: report.telemetry_warnings || null,
|
|
324
|
+
archive_result: report.archive_result || null,
|
|
325
|
+
report_scope: report.report_scope || null,
|
|
326
|
+
section_order: SECTION_ORDER.slice(),
|
|
327
|
+
sections,
|
|
328
|
+
semantic_digest_input: buildSemanticDigestInput(report, sections),
|
|
329
|
+
semantic_digest: crypto.createHash('sha256')
|
|
330
|
+
.update(JSON.stringify(buildSemanticDigestInput(report, sections)))
|
|
331
|
+
.digest('hex'),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
SECTION_ORDER,
|
|
337
|
+
SECTION_TITLES,
|
|
338
|
+
buildChangeReportViewModel,
|
|
339
|
+
buildSemanticDigestInput,
|
|
340
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# KLD SDD Commit-MSG Hook
|
|
3
|
+
# 自动查找 commit-msg-sdd-trailer.cjs,兼容单仓库/多仓库/单仓mono布局
|
|
4
|
+
# marker: KLD SDD quality gate
|
|
5
|
+
|
|
6
|
+
# 从 hook 所在目录推算 git 根目录
|
|
7
|
+
hook_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
git_dir="$(dirname "$hook_dir")"
|
|
9
|
+
git_root="$(dirname "$git_dir")"
|
|
10
|
+
|
|
11
|
+
script=""
|
|
12
|
+
|
|
13
|
+
# 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
|
|
14
|
+
spec_path=$(git config --local sdd.specPath 2>/dev/null)
|
|
15
|
+
if [ -n "$spec_path" ] && [ -f "$spec_path/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
|
|
16
|
+
script="$spec_path/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
|
|
20
|
+
if [ -z "$script" ] && [ -f "$git_root/.sdd-spec-root" ]; then
|
|
21
|
+
spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
|
|
22
|
+
if [ -n "$spec_rel" ] && [ -f "$git_root/$spec_rel/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
|
|
23
|
+
script="$git_root/$spec_rel/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
|
|
28
|
+
if [ -z "$script" ]; then
|
|
29
|
+
search_dir="$git_root"
|
|
30
|
+
while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
|
|
31
|
+
if [ -f "$search_dir/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs" ]; then
|
|
32
|
+
script="$search_dir/skywalk-sdd/git-hooks/commit-msg-sdd-trailer.cjs"
|
|
33
|
+
break
|
|
34
|
+
fi
|
|
35
|
+
search_dir="$(dirname "$search_dir")"
|
|
36
|
+
done
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [ -z "$script" ]; then
|
|
40
|
+
echo "[SDD] 未找到 commit-msg-sdd-trailer.cjs,跳过 commit-msg trailer"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# $1 = Git 传入的 commit message 文件路径
|
|
45
|
+
# --project 告诉 .cjs 脚本代码仓根目录(用于解析 spec 布局)
|
|
46
|
+
node "$script" "$1" --project="$git_root"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# KLD SDD Pre-Commit Hook
|
|
3
|
+
# 远程引用 spec 仓 skywalk-sdd/git-hooks/ 下的 .cjs 脚本
|
|
4
|
+
# 兼容单仓库/多仓库/单仓mono布局
|
|
5
|
+
# marker: KLD SDD quality gate
|
|
6
|
+
|
|
7
|
+
# 从 hook 所在目录推算 git 根目录
|
|
8
|
+
hook_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
git_dir="$(dirname "$hook_dir")"
|
|
10
|
+
git_root="$(dirname "$git_dir")"
|
|
11
|
+
|
|
12
|
+
hooks_dir=""
|
|
13
|
+
|
|
14
|
+
# 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
|
|
15
|
+
spec_path=$(git config --local sdd.specPath 2>/dev/null)
|
|
16
|
+
if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
|
|
17
|
+
hooks_dir="$spec_path/skywalk-sdd/git-hooks"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
|
|
21
|
+
if [ -z "$hooks_dir" ] && [ -f "$git_root/.sdd-spec-root" ]; then
|
|
22
|
+
spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
|
|
23
|
+
if [ -n "$spec_rel" ] && [ -d "$git_root/$spec_rel/skywalk-sdd/git-hooks" ]; then
|
|
24
|
+
hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks"
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
|
|
29
|
+
if [ -z "$hooks_dir" ]; then
|
|
30
|
+
search_dir="$git_root"
|
|
31
|
+
while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
|
|
32
|
+
if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
|
|
33
|
+
hooks_dir="$search_dir/skywalk-sdd/git-hooks"
|
|
34
|
+
break
|
|
35
|
+
fi
|
|
36
|
+
search_dir="$(dirname "$search_dir")"
|
|
37
|
+
done
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ -z "$hooks_dir" ]; then
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-commit 检查"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "[SDD] 执行提交前检查..."
|
|
46
|
+
|
|
47
|
+
# 1. tasks.md 完成度检查
|
|
48
|
+
if [ -f "$hooks_dir/pre-commit-sdd-check.cjs" ]; then
|
|
49
|
+
node "$hooks_dir/pre-commit-sdd-check.cjs" --project="$git_root" "$@" || exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# 2. 代码-spec 一致性检查
|
|
53
|
+
if [ -f "$hooks_dir/pre-commit-consistency-check.cjs" ]; then
|
|
54
|
+
node "$hooks_dir/pre-commit-consistency-check.cjs" --project="$git_root" "$@" || exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "[SDD] ✅ 所有 pre-commit 检查通过"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-commit-consistency-check.cjs
|
|
4
|
+
*
|
|
5
|
+
* Pre-commit 门禁:检查活跃变更的 Spec 一致性校验报告。
|
|
6
|
+
* 若 confidence=low 则阻止提交;缺少报告则警告但放行(向后兼容)。
|
|
7
|
+
*
|
|
8
|
+
* 用法: node pre-commit-consistency-check.cjs --project=<project-root> [--change=<change-name>]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
function readArg(name) {
|
|
17
|
+
const prefix = `--${name}=`;
|
|
18
|
+
const found = process.argv.find(arg => arg.startsWith(prefix));
|
|
19
|
+
return found ? found.slice(prefix.length) : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasFlag(name) {
|
|
23
|
+
return process.argv.includes(`--${name}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从 .sdd-spec-root 文件读取 spec 包裹包相对路径(单仓 mono 布局)
|
|
28
|
+
*/
|
|
29
|
+
function resolveSpecRoot(projectRoot) {
|
|
30
|
+
// 1. 检查 .sdd-spec-root(单仓 mono 模式)
|
|
31
|
+
const hintPath = path.join(projectRoot, '.sdd-spec-root');
|
|
32
|
+
if (fs.existsSync(hintPath)) {
|
|
33
|
+
const rel = fs.readFileSync(hintPath, 'utf8').trim();
|
|
34
|
+
if (rel) {
|
|
35
|
+
const specAbs = path.resolve(projectRoot, rel);
|
|
36
|
+
if (fs.existsSync(specAbs)) {
|
|
37
|
+
return specAbs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. 查找 *-sdd-specs 子目录(多仓工作区模式)
|
|
43
|
+
const entries = fs.existsSync(projectRoot) ? fs.readdirSync(projectRoot) : [];
|
|
44
|
+
for (const name of entries) {
|
|
45
|
+
if (/-sdd-specs$/i.test(name)) {
|
|
46
|
+
const candidate = path.join(projectRoot, name);
|
|
47
|
+
if (fs.statSync(candidate).isDirectory() && fs.existsSync(path.join(candidate, 'openspec'))) {
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. 项目根本身就是 spec 仓
|
|
54
|
+
if (fs.existsSync(path.join(projectRoot, 'openspec', 'changes'))) {
|
|
55
|
+
return projectRoot;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 发现活跃变更列表
|
|
63
|
+
*/
|
|
64
|
+
function discoverActiveChanges(specRoot, explicitChange) {
|
|
65
|
+
if (explicitChange) {
|
|
66
|
+
return [explicitChange];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const changesDir = path.join(specRoot, 'openspec', 'changes');
|
|
70
|
+
if (!fs.existsSync(changesDir)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return fs.readdirSync(changesDir)
|
|
75
|
+
.filter(name => {
|
|
76
|
+
const fullPath = path.join(changesDir, name);
|
|
77
|
+
return fs.statSync(fullPath).isDirectory()
|
|
78
|
+
&& !name.startsWith('.')
|
|
79
|
+
&& name !== 'archive';
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 查找变更的一致性报告 JSON
|
|
85
|
+
*/
|
|
86
|
+
function findConsistencyReport(specRoot, changeName) {
|
|
87
|
+
const patterns = [
|
|
88
|
+
'consistency-report-result.json',
|
|
89
|
+
'consistency-report-self-review-result.json',
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const pattern of patterns) {
|
|
93
|
+
const reportPath = path.join(specRoot, 'openspec', 'changes', changeName, pattern);
|
|
94
|
+
if (fs.existsSync(reportPath)) {
|
|
95
|
+
return reportPath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 解析一致性报告,返回置信度
|
|
104
|
+
*/
|
|
105
|
+
function parseReport(reportPath) {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(reportPath, 'utf8');
|
|
108
|
+
const data = JSON.parse(content);
|
|
109
|
+
return {
|
|
110
|
+
confidence: (data.confidence || '').toLowerCase(),
|
|
111
|
+
overallResult: (data.overallResult || '').toLowerCase(),
|
|
112
|
+
changeName: data.changeName || '',
|
|
113
|
+
reportPath,
|
|
114
|
+
};
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { confidence: '', overallResult: '', changeName: '', reportPath, parseError: err.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function main() {
|
|
121
|
+
const projectRoot = path.resolve(readArg('project') || process.env.SDD_PROJECT || process.cwd());
|
|
122
|
+
const explicitChange = readArg('change') || process.env.SDD_CHANGE || process.env.OPENSPEC_CHANGE || '';
|
|
123
|
+
const strictMode = hasFlag('strict') || process.env.SDD_STRICT_CONSISTENCY === '1';
|
|
124
|
+
|
|
125
|
+
const specRoot = resolveSpecRoot(projectRoot);
|
|
126
|
+
if (!specRoot) {
|
|
127
|
+
console.log('SDD consistency-check: 未找到 spec 仓库,跳过一致性校验');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const changes = discoverActiveChanges(specRoot, explicitChange);
|
|
132
|
+
if (changes.length === 0) {
|
|
133
|
+
console.log('SDD consistency-check: 无活跃变更,跳过一致性校验');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const blockers = [];
|
|
138
|
+
const missing = [];
|
|
139
|
+
const passed = [];
|
|
140
|
+
|
|
141
|
+
for (const changeName of changes) {
|
|
142
|
+
const reportPath = findConsistencyReport(specRoot, changeName);
|
|
143
|
+
if (!reportPath) {
|
|
144
|
+
missing.push(changeName);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const report = parseReport(reportPath);
|
|
149
|
+
if (report.parseError) {
|
|
150
|
+
console.warn(`SDD consistency-check: ⚠️ ${changeName} 报告解析失败: ${report.parseError}`);
|
|
151
|
+
missing.push(changeName);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (report.confidence === 'low' || report.overallResult === 'fail') {
|
|
156
|
+
blockers.push({ changeName, report });
|
|
157
|
+
} else {
|
|
158
|
+
passed.push({ changeName, report });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 输出摘要
|
|
163
|
+
if (passed.length > 0) {
|
|
164
|
+
for (const item of passed) {
|
|
165
|
+
console.log(`SDD consistency-check: ✅ ${item.changeName} 置信度=${item.report.confidence}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (missing.length > 0) {
|
|
170
|
+
console.warn(`SDD consistency-check: ⚠️ 以下变更缺少一致性校验报告(允许提交,建议执行 opsx-consistency-check):`);
|
|
171
|
+
for (const name of missing) {
|
|
172
|
+
console.warn(` - ${name}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (blockers.length > 0) {
|
|
177
|
+
console.error(`SDD consistency-check: ❌ 以下变更的一致性校验未通过(confidence=low),阻止提交:`);
|
|
178
|
+
for (const item of blockers) {
|
|
179
|
+
console.error(` - ${item.changeName} (confidence=${item.report.confidence}, result=${item.report.overallResult})`);
|
|
180
|
+
console.error(` 报告路径: ${item.report.reportPath}`);
|
|
181
|
+
}
|
|
182
|
+
console.error('');
|
|
183
|
+
console.error('请修复不一致项后重新执行 opsx-consistency-check,或使用 --no-verify 跳过(不推荐)。');
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (strictMode && missing.length > 0) {
|
|
188
|
+
console.error('SDD consistency-check: ❌ strict 模式已启用,缺少报告的变更不允许提交。');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# KLD SDD Pre-Push Hook
|
|
3
|
+
# 远程引用 spec 仓 skywalk-sdd/git-hooks/ 下的 .cjs 脚本
|
|
4
|
+
# 兼容单仓库/多仓库/单仓mono布局
|
|
5
|
+
# marker: KLD SDD quality gate
|
|
6
|
+
|
|
7
|
+
# 从 hook 所在目录推算 git 根目录
|
|
8
|
+
hook_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
git_dir="$(dirname "$hook_dir")"
|
|
10
|
+
git_root="$(dirname "$git_dir")"
|
|
11
|
+
|
|
12
|
+
hooks_dir=""
|
|
13
|
+
|
|
14
|
+
# 方法1: git config sdd.specPath(多仓布局:spec 是独立 clone)
|
|
15
|
+
spec_path=$(git config --local sdd.specPath 2>/dev/null)
|
|
16
|
+
if [ -n "$spec_path" ] && [ -d "$spec_path/skywalk-sdd/git-hooks" ]; then
|
|
17
|
+
hooks_dir="$spec_path/skywalk-sdd/git-hooks"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# 方法2: .sdd-spec-root(单仓 mono 布局:skywalk-sdd 在 spec 包裹包子目录内)
|
|
21
|
+
if [ -z "$hooks_dir" ] && [ -f "$git_root/.sdd-spec-root" ]; then
|
|
22
|
+
spec_rel=$(cat "$git_root/.sdd-spec-root" | head -1 | tr -d '[:space:]')
|
|
23
|
+
if [ -n "$spec_rel" ] && [ -d "$git_root/$spec_rel/skywalk-sdd/git-hooks" ]; then
|
|
24
|
+
hooks_dir="$git_root/$spec_rel/skywalk-sdd/git-hooks"
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# 方法3: 从 git 根向上搜索 skywalk-sdd/git-hooks(spec 仓本身或父级)
|
|
29
|
+
if [ -z "$hooks_dir" ]; then
|
|
30
|
+
search_dir="$git_root"
|
|
31
|
+
while [ "$search_dir" != "/" ] && [ "$search_dir" != "" ]; do
|
|
32
|
+
if [ -d "$search_dir/skywalk-sdd/git-hooks" ]; then
|
|
33
|
+
hooks_dir="$search_dir/skywalk-sdd/git-hooks"
|
|
34
|
+
break
|
|
35
|
+
fi
|
|
36
|
+
search_dir="$(dirname "$search_dir")"
|
|
37
|
+
done
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if [ -z "$hooks_dir" ]; then
|
|
41
|
+
echo "[SDD] 未找到 spec 仓的 skywalk-sdd/git-hooks 目录,跳过 pre-push 检查"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "[SDD] 执行推送前检查..."
|
|
46
|
+
|
|
47
|
+
# 1. doctor 质量门禁检查
|
|
48
|
+
if [ -f "$hooks_dir/pre-push-sdd-check.cjs" ]; then
|
|
49
|
+
node "$hooks_dir/pre-push-sdd-check.cjs" --project="$git_root" "$@" || exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# 2. 代码-spec 一致性检查(推送前更严格)
|
|
53
|
+
if [ -f "$hooks_dir/pre-push-consistency-check.cjs" ]; then
|
|
54
|
+
node "$hooks_dir/pre-push-consistency-check.cjs" --project="$git_root" "$@" || exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
echo "[SDD] ✅ 所有 pre-push 检查通过"
|