kld-sdd 2.6.13 → 2.6.14

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.
@@ -0,0 +1,309 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ const EVIDENCE_LABELS = {
8
+ TASK_UPDATE_MISSING: '任务状态事件缺失',
9
+ TASK_TEST_EVIDENCE_MISSING: '任务验证证据缺失',
10
+ TDD_RED_GREEN_INCOMPLETE: 'TDD 红绿链路不完整',
11
+ LEGACY_TASK_PARSER: '任务文档仍使用旧格式',
12
+ apply_test_missing: '实现阶段缺少测试证据',
13
+ tdd_red_missing: 'TDD 缺少有效失败测试',
14
+ process_note_missing: '过程说明缺失',
15
+ task_update_reuse: '测试结果被重复引用',
16
+ };
17
+
18
+ function eventTimestamp(event) {
19
+ const raw = event?.timestamp || event?.ended_at || event?.started_at;
20
+ const value = Date.parse(raw || '');
21
+ return Number.isFinite(value) ? value : 0;
22
+ }
23
+
24
+ function extractFinalTestResult(events = [], fallback = null) {
25
+ const testEvents = events
26
+ .filter((event) => event?.type === 'test_result')
27
+ .sort((a, b) => eventTimestamp(b) - eventTimestamp(a));
28
+ const candidates = testEvents
29
+ .filter((event) => {
30
+ if (event?.type !== 'test_result') return false;
31
+ const result = event.details?.test_results || {};
32
+ return result.evidence_tier === 'strict'
33
+ && ['green', 'refactor', 'regression'].includes(result.tdd_phase)
34
+ && result.counts_known === true;
35
+ });
36
+ const event = candidates.find(item => item.details?.test_results?.snapshot_kind === 'final')
37
+ || candidates[0];
38
+ const history = testEvents
39
+ .filter(item => item !== event)
40
+ .map(item => ({
41
+ event_id: item.event_id || null,
42
+ timestamp: item.timestamp || null,
43
+ passed: Number.isFinite(Number(item.details?.test_results?.passed))
44
+ ? Number(item.details.test_results.passed)
45
+ : null,
46
+ failed: Number.isFinite(Number(item.details?.test_results?.failed))
47
+ ? Number(item.details.test_results.failed)
48
+ : null,
49
+ snapshot_kind: item.details?.test_results?.snapshot_kind || 'unspecified',
50
+ evidence_tier: item.details?.test_results?.evidence_tier || 'legacy',
51
+ }));
52
+ if (!event) {
53
+ return fallback || {
54
+ passed: null,
55
+ failed: null,
56
+ total: null,
57
+ status: 'unavailable',
58
+ source_event_id: null,
59
+ source: 'unavailable',
60
+ history,
61
+ };
62
+ }
63
+ const result = event.details.test_results;
64
+ const passed = Number(result.passed);
65
+ const failed = Number(result.failed);
66
+ const skipped = Number(result.skipped || 0);
67
+ return {
68
+ passed,
69
+ failed,
70
+ skipped,
71
+ total: passed + failed + skipped,
72
+ status: result.result === 'success' && failed === 0 ? 'passed' : 'failed',
73
+ source_event_id: event.event_id || null,
74
+ source: 'strict-test-result',
75
+ snapshot_kind: result.snapshot_kind || 'unspecified',
76
+ history,
77
+ };
78
+ }
79
+
80
+ function normalizeTaskSummary(report) {
81
+ const completion = report.archive_result?.task_completion || {};
82
+ const primary = completion.primary_tasks || {};
83
+ const documentTotal = primary.total ?? completion.total ?? null;
84
+ const documentCompleted = primary.completed ?? completion.completed ?? null;
85
+ const audit = report.apply_evidence_audit || {};
86
+ const auditTasks = audit.tasks || audit.summary || {};
87
+ const traceabilityVerified = auditTasks.with_test_evidence ?? auditTasks.verified ?? null;
88
+ const traceabilityTotal = auditTasks.completed ?? auditTasks.total ?? documentTotal;
89
+ return {
90
+ document: {
91
+ completed: documentCompleted,
92
+ total: documentTotal,
93
+ status: documentTotal == null
94
+ ? 'unavailable'
95
+ : (documentCompleted === documentTotal ? 'complete' : 'incomplete'),
96
+ source: 'tasks-document',
97
+ },
98
+ traceability: {
99
+ verified: traceabilityVerified,
100
+ total: traceabilityTotal,
101
+ status: traceabilityTotal == null || traceabilityVerified == null
102
+ ? 'unavailable'
103
+ : (traceabilityVerified === traceabilityTotal ? 'complete' : 'incomplete'),
104
+ source: 'strict-event-audit',
105
+ },
106
+ };
107
+ }
108
+
109
+ function normalizeEvidenceSummary(report) {
110
+ const alerts = report.evidence_alerts || {};
111
+ const alertHistory = Array.isArray(alerts.history) && alerts.history.length > 0
112
+ ? alerts.history
113
+ : [...(alerts.open || []), ...(alerts.resolved || [])];
114
+ const history = [
115
+ ...alertHistory,
116
+ ...(report.check_warning_dispositions || []).map(item => ({
117
+ ...item,
118
+ code: item.warning,
119
+ occurrences: 1,
120
+ status: ['fixed', 'accepted', 'waived'].includes(item.disposition) ? 'resolved' : 'open',
121
+ last_seen_event_id: item.evidence_event_id,
122
+ })),
123
+ ];
124
+ const groups = new Map();
125
+ for (const alert of history) {
126
+ const code = alert.warning || alert.code || 'UNKNOWN';
127
+ if (!groups.has(code)) {
128
+ groups.set(code, {
129
+ code,
130
+ label: EVIDENCE_LABELS[code] || code,
131
+ occurrence_count: 0,
132
+ affected_tasks: [],
133
+ event_ids: [],
134
+ dispositions: {},
135
+ });
136
+ }
137
+ const group = groups.get(code);
138
+ group.occurrence_count += Math.max(1, Number(alert.occurrences) || 1);
139
+ const task = alert.task_id || alert.subject;
140
+ if (task && !group.affected_tasks.includes(task)) group.affected_tasks.push(task);
141
+ for (const eventId of [alert.opened_by_event_id, alert.last_seen_event_id, alert.resolved_by_event_id]) {
142
+ if (eventId && !group.event_ids.includes(eventId)) group.event_ids.push(eventId);
143
+ }
144
+ const disposition = alert.disposition || (alert.status === 'resolved' ? 'fixed' : 'open');
145
+ group.dispositions[disposition] = (group.dispositions[disposition] || 0) + 1;
146
+ }
147
+ const categories = [...groups.values()];
148
+ return {
149
+ category_count: categories.length,
150
+ occurrence_count: categories.reduce((sum, item) => sum + item.occurrence_count, 0),
151
+ categories,
152
+ note: '一次任务或事件可能同时命中多条规则;命中次数不等于独立缺陷数。',
153
+ };
154
+ }
155
+
156
+ function normalizeChangedFiles(changedFiles = {}) {
157
+ const taskEventFiles = Array.isArray(changedFiles.task_event_files)
158
+ ? [...new Set(changedFiles.task_event_files)]
159
+ : null;
160
+ const finalOutputFiles = Array.isArray(changedFiles.final_output_files)
161
+ ? [...new Set(changedFiles.final_output_files)]
162
+ : (Array.isArray(changedFiles.files) ? [...new Set(changedFiles.files)] : null);
163
+ const taskCount = changedFiles.task_event_files_changed
164
+ ?? changedFiles.files_changed
165
+ ?? taskEventFiles?.length
166
+ ?? null;
167
+ const finalCount = changedFiles.final_output_files_changed
168
+ ?? finalOutputFiles?.length
169
+ ?? null;
170
+ const countsDiffer = taskCount != null && finalCount != null && taskCount !== finalCount;
171
+ return {
172
+ ...changedFiles,
173
+ task_event_files: taskEventFiles,
174
+ task_event_files_changed: taskCount,
175
+ final_output_files: finalOutputFiles,
176
+ final_output_files_changed: finalCount,
177
+ diff_source: changedFiles.diff_source || 'legacy-fields',
178
+ line_count_estimate: changedFiles.line_count_estimate ?? null,
179
+ reconciliation_note: changedFiles.reconciliation_note
180
+ || (countsDiffer
181
+ ? `任务事件累计涉及 ${taskCount} 个文件;最终输出快照包含 ${finalCount} 个文件。两者用途不同,不应合并为一个口径。`
182
+ : '任务事件与最终输出文件口径一致。'),
183
+ legacy_fields: {
184
+ files: { deprecated: true, ambiguous: true },
185
+ files_changed: { deprecated: true, ambiguous: true },
186
+ },
187
+ };
188
+ }
189
+
190
+ function resolveArtifact(projectRoot, artifactPath, options = {}) {
191
+ if (!artifactPath) return { path: null, exists: false, size_bytes: null, sha256: null };
192
+ const absolute = path.isAbsolute(artifactPath)
193
+ ? artifactPath
194
+ : path.resolve(projectRoot || process.cwd(), artifactPath);
195
+ try {
196
+ const stat = fs.statSync(absolute);
197
+ if (!stat.isFile()) return { path: artifactPath, exists: false, size_bytes: null, sha256: null };
198
+ if (options.selfReferential) {
199
+ return {
200
+ path: artifactPath,
201
+ exists: true,
202
+ size_bytes: null,
203
+ sha256: null,
204
+ integrity_note: '报告不能内嵌自身最终大小与哈希;请以归档 Manifest 为准',
205
+ };
206
+ }
207
+ const sha256 = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
208
+ return { path: artifactPath, exists: true, size_bytes: stat.size, sha256: `sha256:${sha256}` };
209
+ } catch {
210
+ return { path: artifactPath, exists: false, size_bytes: null, sha256: null };
211
+ }
212
+ }
213
+
214
+ function normalizeArtifacts(report) {
215
+ const archive = report.archive_result || {};
216
+ const existing = report.artifacts || {};
217
+ const projectRoot = report.project_root;
218
+ const paths = {
219
+ markdown_report: archive.report_path,
220
+ html_report: archive.report_html_path,
221
+ json_report: archive.report_json_path,
222
+ execution_log: archive.execution_log_path
223
+ || existing.execution_log_path
224
+ || (archive.archive_path ? path.join(archive.archive_path, 'logs', 'execution-log.md') : null),
225
+ manifest: archive.manifest_path || existing.manifest_path,
226
+ archive_zip: archive.archive_zip_path
227
+ || archive.zip_path
228
+ || archive.package_path
229
+ || existing.archive_zip_path
230
+ || existing.package_path,
231
+ };
232
+ return {
233
+ ...existing,
234
+ files: Object.fromEntries(
235
+ Object.entries(paths).map(([name, artifactPath]) => [
236
+ name,
237
+ resolveArtifact(projectRoot, artifactPath, {
238
+ selfReferential: [
239
+ 'markdown_report',
240
+ 'html_report',
241
+ 'json_report',
242
+ 'manifest',
243
+ 'archive_zip',
244
+ ].includes(name),
245
+ }),
246
+ ]),
247
+ ),
248
+ };
249
+ }
250
+
251
+ function buildWarningDispositions(report) {
252
+ const alerts = [
253
+ ...(report.evidence_alerts?.open || []),
254
+ ...(report.evidence_alerts?.resolved || []),
255
+ ...(report.check_warning_dispositions || []),
256
+ ];
257
+ const allowed = new Set(['fixed', 'accepted', 'waived', 'needs_input', 'open']);
258
+ const checkWarnings = new Set((report.check_warning_dispositions || []).map(item => item.warning));
259
+ const byWarning = new Map();
260
+ for (const alert of alerts) {
261
+ const inferred = alert.status === 'resolved' ? 'fixed' : 'open';
262
+ const disposition = allowed.has(alert.disposition) ? alert.disposition : inferred;
263
+ const normalized = {
264
+ warning: alert.warning || alert.code || 'UNKNOWN',
265
+ subject: alert.subject || alert.task_id || null,
266
+ disposition,
267
+ reason: alert.disposition_reason || alert.reason || alert.message || null,
268
+ evidence_event_id: alert.evidence_event_id || alert.resolved_by_event_id || alert.last_seen_event_id || null,
269
+ };
270
+ if (!byWarning.has(normalized.warning) || checkWarnings.has(normalized.warning)) {
271
+ byWarning.set(normalized.warning, normalized);
272
+ }
273
+ }
274
+ return [...byWarning.values()];
275
+ }
276
+
277
+ function buildChangeReportModel(report, events = []) {
278
+ if (!report || report.level !== 'change' || !report.change) return report;
279
+ const changedFiles = normalizeChangedFiles(report.changed_files);
280
+ const existingSummary = report.change_summary || {};
281
+ const changeSummary = {
282
+ tests: events.length > 0
283
+ ? extractFinalTestResult(events, report.final_test_result || existingSummary.tests || null)
284
+ : (existingSummary.tests || extractFinalTestResult(events, report.final_test_result || null)),
285
+ tasks: existingSummary.tasks || normalizeTaskSummary(report),
286
+ evidence: existingSummary.evidence || normalizeEvidenceSummary(report),
287
+ };
288
+ return {
289
+ ...report,
290
+ report_scope: {
291
+ kind: 'single-change',
292
+ change: report.change,
293
+ cross_change_data_included: false,
294
+ },
295
+ metric_provenance: report.metric_provenance || report.metric_evidence || null,
296
+ change_summary: changeSummary,
297
+ changed_files: changedFiles,
298
+ warning_dispositions: buildWarningDispositions(report),
299
+ artifacts: normalizeArtifacts(report),
300
+ };
301
+ }
302
+
303
+ module.exports = {
304
+ EVIDENCE_LABELS,
305
+ buildChangeReportModel,
306
+ extractFinalTestResult,
307
+ normalizeEvidenceSummary,
308
+ normalizeChangedFiles,
309
+ };