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
@@ -1,10 +1,30 @@
1
1
  'use strict';
2
2
 
3
- const REPORT_SCHEMA_VERSION = 'sdd-efficiency-report/v3';
4
- const METRICS_CONTRACT_VERSION = '3.0';
5
- const CHANGE_TYPES = ['config', 'document', 'report', 'composite', 'other'];
3
+ const REPORT_SCHEMA_VERSION = 'sdd-efficiency-report/v4';
4
+ const METRICS_CONTRACT_VERSION = '4.0';
5
+ const SCHEME_VERSION = 'V3.0';
6
+ /** Writable change types (scheme 5.3). Legacy document/other read as unknown. */
7
+ const CHANGE_TYPES = ['config', 'transaction', 'report', 'composite'];
8
+ const LEGACY_CHANGE_TYPES = ['document', 'other'];
6
9
  const MINIMUM_METRICS = ['e1', 'q3', 'p4', 'p_r', 'p_h'];
7
10
 
11
+ /**
12
+ * Format artifact byte size: <1MiB → KB, else MB; two decimals; never bare B.
13
+ * @param {number} bytes
14
+ * @returns {string}
15
+ */
16
+ function formatArtifactSize(bytes) {
17
+ const n = Number(bytes);
18
+ if (!Number.isFinite(n) || n < 0) return '0.00 KB';
19
+ const kib = 1024;
20
+ const mib = 1024 * 1024;
21
+ if (n < mib) {
22
+ const kb = n / kib;
23
+ return `${kb.toFixed(2)} KB`;
24
+ }
25
+ return `${(n / mib).toFixed(2)} MB`;
26
+ }
27
+
8
28
  const ALERT_THRESHOLDS = {
9
29
  q3: { red: 70, yellow: 85, direction: 'higher_better' },
10
30
  p4: { red: 0.4, yellow: 0.6, direction: 'higher_better' },
@@ -126,7 +146,27 @@ function countScenariosInMarkdown(text) {
126
146
  function validateChangeType(value) {
127
147
  if (typeof value !== 'string') return 'unknown';
128
148
  const normalized = value.trim().toLowerCase();
129
- return CHANGE_TYPES.includes(normalized) ? normalized : 'unknown';
149
+ if (CHANGE_TYPES.includes(normalized)) return normalized;
150
+ if (LEGACY_CHANGE_TYPES.includes(normalized)) return 'unknown';
151
+ return 'unknown';
152
+ }
153
+
154
+ function diagnoseChangeType(value) {
155
+ if (typeof value !== 'string') {
156
+ return { change_type: 'unknown', legacy_change_type: null, warning: null };
157
+ }
158
+ const normalized = value.trim().toLowerCase();
159
+ if (CHANGE_TYPES.includes(normalized)) {
160
+ return { change_type: normalized, legacy_change_type: null, warning: null };
161
+ }
162
+ if (LEGACY_CHANGE_TYPES.includes(normalized)) {
163
+ return {
164
+ change_type: 'unknown',
165
+ legacy_change_type: normalized,
166
+ warning: `legacy_change_type:${normalized}`,
167
+ };
168
+ }
169
+ return { change_type: 'unknown', legacy_change_type: null, warning: 'invalid_change_type' };
130
170
  }
131
171
 
132
172
  function resolveReviewerIndependence(meta = {}) {
@@ -850,9 +890,41 @@ function buildSchemeCompliance(input = {}) {
850
890
  const minimumStatus = minimum.numeric_available === minimum.numeric_total
851
891
  ? (minimum.trusted_available === minimum.numeric_total ? 'implemented' : 'provisional')
852
892
  : 'provisional';
853
- const q1Status = quality.q1_status || quality.q1_spec_conformance?.status || 'pending-upstream';
854
- const q4Status = quality.q4_status || quality.q4_tdd_coverage?.status || 'pending-upstream';
855
- const q3Status = quality.q3_score_status === 'verified' ? 'implemented' : 'provisional';
893
+ const q1Raw = quality.q1_human_status
894
+ || quality.q1_status
895
+ || quality.q1_spec_conformance?.status
896
+ || 'pending-upstream';
897
+ const q1Status = (q1Raw === 'unverified' || q1Raw === 'self-review')
898
+ ? 'provisional'
899
+ : (q1Raw === 'pending-upstream' ? 'pending-upstream' : q1Raw);
900
+ const q4Raw = quality.q4_status || quality.q4_tdd_coverage?.status || 'pending-upstream';
901
+ const q4Source = quality.q4_source || quality.q4_tdd_coverage?.source || null;
902
+ const q4Status = q4Source === 'conformance-fallback' || q4Raw === 'conformance-fallback'
903
+ ? 'pending-upstream'
904
+ : q4Raw;
905
+ const q3Independence = quality.q3_reviewer_independence || 'unknown';
906
+ const q3Status = quality.q3_score_status === 'verified'
907
+ && q3Independence === 'independent-review'
908
+ ? 'implemented'
909
+ : 'provisional';
910
+ const semanticParity = input.semantic_parity_status || input.semantic_parity || null;
911
+ const multiFormatStatus = semanticParity === 'pass'
912
+ ? 'implemented'
913
+ : (['md', 'html', 'json'].every(format => formats.includes(format)) ? 'provisional' : 'provisional');
914
+ const alertLoopReady = Boolean(
915
+ input.alert_loop
916
+ && input.alert_loop.root_cause
917
+ && input.alert_loop.owner
918
+ && input.alert_loop.due
919
+ && input.alert_loop.expected_impact
920
+ && input.alert_loop.two_cycle_evidence,
921
+ );
922
+ const eventContractHealth = input.evidence_contract_health
923
+ || input.event_contract_health
924
+ || null;
925
+ const reproducibleStatus = eventContractHealth === 'healthy' && Number(input.event_count) > 0
926
+ ? 'implemented'
927
+ : (Number(input.event_count) > 0 ? 'provisional' : 'provisional');
856
928
  const statusLabel = status => ({
857
929
  implemented: '已实现',
858
930
  provisional: '部分可用',
@@ -864,6 +936,7 @@ function buildSchemeCompliance(input = {}) {
864
936
  unknown: '未知',
865
937
  'independent-review': '独立复核',
866
938
  'self-review': '自评',
939
+ unverified: '未人工验证',
867
940
  }[status] || status || '未知');
868
941
 
869
942
  const items = [
@@ -886,7 +959,7 @@ function buildSchemeCompliance(input = {}) {
886
959
  label: '条件指标(Q1、Q4)',
887
960
  status: q1Status === 'pending-upstream' || q4Status === 'pending-upstream'
888
961
  ? 'pending-upstream'
889
- : 'implemented',
962
+ : (q1Status === 'provisional' || q4Status === 'provisional' ? 'provisional' : 'implemented'),
890
963
  evidence: `Q1:${statusLabel(q1Status)};Q4:${statusLabel(q4Status)}`,
891
964
  },
892
965
  {
@@ -899,7 +972,7 @@ function buildSchemeCompliance(input = {}) {
899
972
  requirement: 'independent-reviewer',
900
973
  label: '独立复核',
901
974
  status: q3Status,
902
- evidence: `Q3:${statusLabel(quality.q3_score_status || 'provisional')};复核方式:${statusLabel(quality.q3_reviewer_independence || 'unknown')}`,
975
+ evidence: `Q3:${statusLabel(quality.q3_score_status || 'provisional')};复核方式:${statusLabel(q3Independence)}`,
903
976
  },
904
977
  {
905
978
  requirement: 'tracking-periods',
@@ -922,20 +995,30 @@ function buildSchemeCompliance(input = {}) {
922
995
  {
923
996
  requirement: 'alert-feedback-loop',
924
997
  label: '指标与证据告警闭环',
925
- status: input.evidence_alerts ? 'implemented' : 'provisional',
926
- evidence: input.evidence_alerts ? '稳定问题编号、未解决/已解决状态和历史记录' : '当前模型未提供告警生命周期',
998
+ status: alertLoopReady ? 'implemented' : 'provisional',
999
+ evidence: alertLoopReady
1000
+ ? '具备根因、负责人、期限、预期影响与两周期验证'
1001
+ : '仍缺根因/负责人/期限/预期影响/两周期验证中的完整字段',
927
1002
  },
928
1003
  {
929
1004
  requirement: 'multi-format-same-model',
930
1005
  label: 'Markdown、HTML、JSON 同源',
931
- status: ['md', 'html', 'json'].every(format => formats.includes(format)) ? 'implemented' : 'provisional',
932
- evidence: formats.length > 0 ? formats.map(format => ({ md: 'Markdown', html: 'HTML', json: 'JSON' }[format] || format)).join(' / ') : '格式信息未提供',
1006
+ status: multiFormatStatus,
1007
+ evidence: semanticParity === 'pass'
1008
+ ? '三格式语义对账通过'
1009
+ : (formats.length > 0
1010
+ ? `${formats.map(format => ({ md: 'Markdown', html: 'HTML', json: 'JSON' }[format] || format)).join(' / ')} 已产出,语义对账未确认`
1011
+ : '格式信息未提供'),
933
1012
  },
934
1013
  {
935
1014
  requirement: 'reproducible-auditable',
936
1015
  label: '可复算、可审计',
937
- status: input.event_count > 0 ? 'implemented' : 'provisional',
938
- evidence: input.event_count > 0 ? `${input.event_count} 条 JSONL 事件` : '未提供事件数量',
1016
+ status: reproducibleStatus,
1017
+ evidence: eventContractHealth === 'healthy'
1018
+ ? `${input.event_count} 条事件且证据契约健康`
1019
+ : (input.event_count > 0
1020
+ ? `${input.event_count} 条 JSONL 事件;证据契约健康度未达标`
1021
+ : '未提供事件数量'),
939
1022
  },
940
1023
  ];
941
1024
  const counts = {};
@@ -945,6 +1028,7 @@ function buildSchemeCompliance(input = {}) {
945
1028
  version: 'V3.0',
946
1029
  items,
947
1030
  counts,
1031
+ evidence_contract_health: eventContractHealth || null,
948
1032
  };
949
1033
  }
950
1034
 
@@ -1125,7 +1209,9 @@ function deriveSuccessfulReworkReason(attemptContext = {}) {
1125
1209
  module.exports = {
1126
1210
  REPORT_SCHEMA_VERSION,
1127
1211
  METRICS_CONTRACT_VERSION,
1212
+ SCHEME_VERSION,
1128
1213
  CHANGE_TYPES,
1214
+ LEGACY_CHANGE_TYPES,
1129
1215
  ALERT_THRESHOLDS,
1130
1216
  MINIMUM_METRICS,
1131
1217
  clamp,
@@ -1148,6 +1234,8 @@ module.exports = {
1148
1234
  findDevelopmentActivities,
1149
1235
  deriveSuccessfulReworkReason,
1150
1236
  validateChangeType,
1237
+ diagnoseChangeType,
1238
+ formatArtifactSize,
1151
1239
  createInvalidArgError,
1152
1240
  countScenariosInMarkdown,
1153
1241
  };
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * openspec-shim.cjs — 自动定位 spec 包并透传 openspec 命令。
6
+ *
7
+ * 用法: node skywalk-sdd/openspec-shim.cjs <openspec-args>
8
+ * 示例: node skywalk-sdd/openspec-shim.cjs list
9
+ * node skywalk-sdd/openspec-shim.cjs add change my-feature
10
+ *
11
+ * 自动通过 resolve-spec-root.cjs 定位 spec 包目录,
12
+ * 然后在该目录下执行 openspec 命令。
13
+ */
14
+
15
+ const { resolveSpecProjectRoot } = require('./ontology/resolve-spec-root.cjs');
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
20
+ console.log('用法: node skywalk-sdd/openspec-shim.cjs <openspec-args>');
21
+ console.log('示例: node skywalk-sdd/openspec-shim.cjs list');
22
+ console.log(' node skywalk-sdd/openspec-shim.cjs add change my-feature');
23
+ process.exit(0);
24
+ }
25
+
26
+ const projectRoot = process.cwd();
27
+ const specRoot = resolveSpecProjectRoot(projectRoot);
28
+
29
+ if (specRoot === projectRoot) {
30
+ console.error('⚠️ 未找到 spec 包目录(未配置 .sdd-spec-root,也未检测到 *-sdd-specs 仓库)');
31
+ console.error(' 配置方式: 在项目根目录创建 .sdd-spec-root 文件,写入 spec 包相对或绝对路径');
32
+ console.error(' 或使用: kld-sdd link-spec --path=<spec-clone>');
33
+ process.exit(1);
34
+ }
35
+
36
+ const { execSync } = require('child_process');
37
+ const openspecArgs = args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ');
38
+ const cmd = `openspec ${openspecArgs}`;
39
+
40
+ try {
41
+ execSync(cmd, { cwd: specRoot, stdio: 'inherit' });
42
+ } catch (err) {
43
+ if (err.status) {
44
+ process.exit(err.status);
45
+ }
46
+ console.error(`❌ openspec 执行失败: ${err.message}`);
47
+ process.exit(1);
48
+ }
@@ -0,0 +1,294 @@
1
+ 'use strict';
2
+
3
+ const { buildChangeReportViewModel } = require('./change-report-view-model.cjs');
4
+
5
+ function formatValue(value) {
6
+ if (value == null || value === '') return '暂无数据';
7
+ return String(value);
8
+ }
9
+
10
+ function formatRatioToPercent(ratio) {
11
+ if (ratio == null || !Number.isFinite(Number(ratio))) return '暂无数据';
12
+ return `${(Number(ratio) * 100).toFixed(1)}%`;
13
+ }
14
+
15
+ function renderSectionOverview(section, report) {
16
+ const minSet = report.minimum_metric_set || {};
17
+ const lines = [
18
+ '## 一句话结论',
19
+ '',
20
+ `> ${section.summary}`,
21
+ '',
22
+ `## ${section.title}`,
23
+ '',
24
+ '### 摘要卡片',
25
+ ];
26
+ for (const card of section.cards || []) {
27
+ lines.push(`- ${card.label}:${formatValue(card.value)}`);
28
+ }
29
+ if (report.scheme_compliance?.counts) {
30
+ lines.push(
31
+ `- 方案覆盖:${report.scheme_compliance.counts.implemented ?? 0}/${report.scheme_compliance.items?.length ?? 0} 项已完整实现`,
32
+ );
33
+ }
34
+ lines.push(
35
+ '',
36
+ '### 关键指标可信覆盖',
37
+ `- 数值覆盖:${formatRatioToPercent(minSet.numeric_coverage_rate)}(${minSet.numeric_available ?? 0}/${minSet.numeric_total ?? 5})`,
38
+ `- 可信覆盖:${formatRatioToPercent(minSet.trusted_coverage_rate)}(${minSet.trusted_available ?? 0}/${minSet.numeric_total ?? 5};临时 ${minSet.provisional ?? 0} 项)`,
39
+ '',
40
+ '## 核心指标',
41
+ `- 每个场景平均用时(E1):见「指标与阶段」`,
42
+ `- 规约检查得分(Q3):见「指标与阶段」`,
43
+ `- 编码前是否先检查(P4):见「指标与阶段」`,
44
+ );
45
+ return lines;
46
+ }
47
+
48
+ function renderSectionMetrics(section, report) {
49
+ const tdd = section.tdd_pairs || {};
50
+ const reasons = report.metrics?.process?.rework_summary?.reasons;
51
+ const lines = [
52
+ `## ${section.title}`,
53
+ '',
54
+ section.summary,
55
+ '',
56
+ '### 关键指标',
57
+ ];
58
+ for (const metric of section.metric_cards || []) {
59
+ lines.push(`- ${metric.name}(${metric.code}):${metric.value} · ${metric.state}`);
60
+ }
61
+ if (reasons?.by_category) {
62
+ const parts = Object.entries(reasons.by_category)
63
+ .map(([k, v]) => `${k}=${v}`)
64
+ .join(', ');
65
+ lines.push(`- 重复执行原因分布:${parts || '无'}`);
66
+ }
67
+ lines.push(
68
+ '',
69
+ '### TDD 对',
70
+ `- 完整度:${tdd.complete == null || tdd.required == null ? '暂无数据' : `${tdd.complete}/${tdd.required}`}`,
71
+ );
72
+ const incomplete = (tdd.pairs || []).filter(pair => pair.status && pair.status !== 'complete');
73
+ if (incomplete.length > 0) {
74
+ lines.push('- 缺口:');
75
+ for (const pair of incomplete) {
76
+ lines.push(` - ${pair.pair_id}:${pair.status}`);
77
+ }
78
+ }
79
+ const stages = section.stages || [];
80
+ lines.push('', '### 阶段时间线');
81
+ if (stages.length === 0) {
82
+ lines.push('- 无阶段节点');
83
+ } else {
84
+ for (const node of stages.slice(0, 40)) {
85
+ lines.push(
86
+ `- ${node.display_stage} R${node.display_round}:${node.display_result}`
87
+ + `(${node.display_duration})`,
88
+ );
89
+ }
90
+ }
91
+ return lines;
92
+ }
93
+
94
+ function renderSectionIssues(section, report) {
95
+ const lines = [
96
+ `## ${section.title}`,
97
+ '',
98
+ section.summary,
99
+ '',
100
+ ];
101
+ const audit = section.task_evidence_audit || {};
102
+ if (audit.direct_test_backlink || audit.strict_test_coverage) {
103
+ lines.push('### 任务证据四口径');
104
+ lines.push(
105
+ `- 文档完成:${formatValue(audit.document_completion?.completed)}/${formatValue(audit.document_completion?.total)}`,
106
+ );
107
+ lines.push(
108
+ `- 直接回链:${formatValue(audit.direct_test_backlink?.verified)}/${formatValue(audit.direct_test_backlink?.total)}`,
109
+ );
110
+ lines.push(
111
+ `- 严格覆盖:${formatValue(audit.strict_test_coverage?.verified)}/${formatValue(audit.strict_test_coverage?.total)}`,
112
+ );
113
+ lines.push('');
114
+ }
115
+ lines.push('### 问题类别');
116
+ const categories = section.categories || [];
117
+ if (categories.length === 0) {
118
+ lines.push('- 当前没有归并后的问题类别。');
119
+ } else {
120
+ for (const group of categories) {
121
+ lines.push(
122
+ `- ${group.label || group.code}:命中 ${group.occurrence_count || 0} 次,关联任务 ${(group.affected_tasks || []).length} 个`,
123
+ );
124
+ }
125
+ }
126
+ const findings = section.apply_findings || [];
127
+ const issues = section.issues || [];
128
+ if (issues.length > 0) {
129
+ lines.push('', '### 问题明细');
130
+ for (const issue of issues) {
131
+ lines.push(
132
+ `- [${issue.code}] ${issue.subject || '本次变更'} · 状态:${issue.status} · 判定:${issue.verdict}`,
133
+ ` - 根因:${issue.root_cause}`,
134
+ ` - 预期:${issue.expected}`,
135
+ ` - 实际:${issue.actual}`,
136
+ ` - 影响:${issue.impact}`,
137
+ ` - 修复建议:${issue.remediation}`,
138
+ ` - 证据事件:${(issue.evidence_event_ids || []).join(', ') || '暂无可定位事件'}`,
139
+ );
140
+ }
141
+ } else if (findings.length > 0) {
142
+ lines.push('', '### 审计发现');
143
+ for (const finding of findings) {
144
+ lines.push(`- [${finding.code}] ${finding.task_id || '变更'}:${finding.message || ''}`);
145
+ }
146
+ }
147
+ const dispositions = section.warning_dispositions
148
+ || section.report_warning_dispositions
149
+ || [];
150
+ if (dispositions.length > 0) {
151
+ lines.push('', '### 告警处置');
152
+ for (const item of dispositions) {
153
+ lines.push(`- [告警处置] ${item.warning || item.code || 'UNKNOWN'}:${item.disposition || 'open'}`);
154
+ }
155
+ }
156
+
157
+ const known = report.known_risks || {};
158
+ lines.push('', '### 已知风险');
159
+ lines.push(`- 数据可用:${known._available ? '是' : '否'}`);
160
+ lines.push(
161
+ `- 严重问题数:${known._available ? (known.severe_issues?.length ?? 0) : '无数据'}`,
162
+ );
163
+ if (Array.isArray(known.warnings) && known.warnings.length > 0) {
164
+ for (const warning of known.warnings) {
165
+ const items = Array.isArray(warning.items) ? warning.items : null;
166
+ const count = items ? items.length : (warning.warnings || 0);
167
+ const statusLabel = warning.resolved === true ? '✅已修复' : '⏳待确认';
168
+ lines.push(`- 警告:${warning.command || 'check'} 阶段 ${count} 个${statusLabel}`);
169
+ if (items && items.length > 0) {
170
+ for (const item of items) {
171
+ lines.push(
172
+ ` - ${item.description || '待确认项'}${item.target ? `(${item.target})` : ''}`,
173
+ );
174
+ }
175
+ }
176
+ }
177
+ } else {
178
+ lines.push('- 无警告(无需修复)');
179
+ }
180
+
181
+ const notes = report.process_notes || {};
182
+ lines.push('', '## 过程记录');
183
+ lines.push(`- 数据可用:${notes._available ? '是' : '否'}`);
184
+ lines.push(`- 过程事件总数:${notes.total ?? 'null'}`);
185
+ if (notes.by_kind) {
186
+ lines.push(`- 按 kind 分布:${JSON.stringify(notes.by_kind)}`);
187
+ }
188
+ if (Array.isArray(notes.notes) && notes.notes.length > 0) {
189
+ for (const note of notes.notes) {
190
+ lines.push(
191
+ `- [${note.stage || 'unknown'}] ${note.kind || 'note'}${note.round ? ` R${note.round}` : ''}:${note.summary || ''}${note.target ? ` → ${note.target}` : ''}`,
192
+ );
193
+ }
194
+ } else {
195
+ lines.push('- 无过程事件');
196
+ }
197
+ return lines;
198
+ }
199
+
200
+ function renderSectionArtifacts(section, report) {
201
+ const changed = section.changed_files || {};
202
+ const rawChanged = report.changed_files || {};
203
+ const archive = report.archive_result || null;
204
+ const taskCompletion = archive?.task_completion || null;
205
+ const lines = [
206
+ `## ${section.title}`,
207
+ '',
208
+ section.summary,
209
+ '',
210
+ '### 变更文件',
211
+ `- 数据可用:${rawChanged._available ? '是' : '否'}`,
212
+ `- 最终变更文件数:${formatValue(changed.files_changed)}`,
213
+ `- 新增行数:${formatValue(changed.added_lines)}`,
214
+ `- 对账状态:${formatValue(changed.reconciliation_status)}`,
215
+ ];
216
+ if ((changed.only_in_task_events || []).length > 0) {
217
+ lines.push(`- 仅任务事件:${changed.only_in_task_events.join(', ')}`);
218
+ }
219
+ if ((changed.only_in_final_output || []).length > 0) {
220
+ lines.push(`- 仅最终输出:${changed.only_in_final_output.join(', ')}`);
221
+ }
222
+ lines.push('', '### 产物清单');
223
+ const artifacts = section.artifacts || [];
224
+ if (artifacts.length === 0) {
225
+ lines.push('- 暂无产物条目');
226
+ } else {
227
+ for (const item of artifacts) {
228
+ const size = item.size_display
229
+ || '—';
230
+ lines.push(
231
+ `- ${item.name}:${item.exists ? '已生成' : '未生成'} · ${item.path || '—'} · ${size}`,
232
+ );
233
+ }
234
+ }
235
+ const copied = report.artifacts?.copied_specs;
236
+ lines.push(
237
+ `- 迁移文件数:${Array.isArray(copied) ? copied.length : 'null'}`,
238
+ );
239
+ lines.push(
240
+ '',
241
+ '### 归因审计',
242
+ `- 显式关联历史事件:${section.attribution_summary?.linked_source_event_count ?? 0}`,
243
+ `- 有效关联:${section.attribution_summary?.active_link_count ?? 0}`,
244
+ `- 未归属候选:${section.attribution_summary?.unassigned_candidate_count ?? 0}`,
245
+ `- 被拒绝的跨变更碰撞:${section.attribution_summary?.rejected_collision_count ?? 0}`,
246
+ '',
247
+ '### 归档结果',
248
+ `- 归档原因:${archive?.reason || 'null'}`,
249
+ `- 归档方式:${archive?.method || 'null'}`,
250
+ `- 归档目录:${archive?.archive_path || report.artifacts?.archive_path || 'null'}`,
251
+ `- 归档清单:${archive?.manifest_path || report.artifacts?.manifest_path || 'null'}`,
252
+ `- 最终报告:${archive?.report_path || 'null'}`,
253
+ `- 主任务完成:${taskCompletion?.primary_tasks
254
+ ? `${taskCompletion.primary_tasks.completed}/${taskCompletion.primary_tasks.total}`
255
+ : 'null'}`,
256
+ `- 全部 checkbox 完成:${taskCompletion?.completed ?? 'null'}/${taskCompletion?.total ?? 'null'}(含验收子项;分母≠主任务数)`,
257
+ `- 未勾选 checkbox:${taskCompletion?.incomplete ?? 'null'}`,
258
+ `- 未勾选任务项:${taskCompletion?.incomplete ?? 'null'}`,
259
+ );
260
+ return lines;
261
+ }
262
+
263
+ function renderChangeReportMarkdown(report) {
264
+ const view = buildChangeReportViewModel(report);
265
+ const projectLabel = report.project_root
266
+ ? String(report.project_root).replace(/[\\/]+$/, '').split(/[\\/]/).pop()
267
+ : null;
268
+ const lines = [
269
+ `# SDD 效果度量报告${view.change ? ` - ${view.change}` : ''}`,
270
+ '',
271
+ `- 生成时间:${report.generated_at || '未知'}`,
272
+ ...(projectLabel ? [`- 项目路径:${projectLabel}`] : []),
273
+ `- 统计范围:change/${view.change || 'unknown'}`,
274
+ `- 报告 Schema:${view.schema_version || 'unknown'}(指标契约 ${view.metrics_contract || 'unknown'})`,
275
+ `<!-- sdd-semantic-digest:${view.semantic_digest} -->`,
276
+ '',
277
+ ];
278
+
279
+ for (const sectionId of view.section_order) {
280
+ const section = view.sections[sectionId];
281
+ if (!section) continue;
282
+ if (sectionId === 'overview') lines.push(...renderSectionOverview(section, report));
283
+ else if (sectionId === 'metrics-stages') lines.push(...renderSectionMetrics(section, report));
284
+ else if (sectionId === 'issues-evidence') lines.push(...renderSectionIssues(section, report));
285
+ else if (sectionId === 'artifacts-trace') lines.push(...renderSectionArtifacts(section, report));
286
+ lines.push('');
287
+ }
288
+
289
+ return lines.join('\n');
290
+ }
291
+
292
+ module.exports = {
293
+ renderChangeReportMarkdown,
294
+ };