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
@@ -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 检查通过"