kld-sdd 2.6.13 → 2.6.15
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 +16 -12
- package/kld-sdd-guide.html +4 -5
- package/lib/init.js +14 -11
- package/package.json +2 -2
- package/skywalk-sdd/index.cjs +936 -109
- package/skywalk-sdd/metrics-v3.cjs +103 -15
- package/skywalk-sdd/ontology/archive-package.cjs +6 -0
- package/skywalk-sdd/ontology/identity-index.cjs +9 -2
- package/skywalk-sdd/ontology/ontology-paths.cjs +73 -0
- package/skywalk-sdd/ontology/runtime.cjs +32 -22
- package/skywalk-sdd/ontology/structural-identity.cjs +11 -2
- package/skywalk-sdd/ontology/traceability-validator.cjs +16 -8
- package/skywalk-sdd/ontology/working-artifacts.cjs +2 -1
- package/skywalk-sdd/reporting/change-report-markdown.cjs +294 -0
- package/skywalk-sdd/reporting/change-report-model.cjs +452 -0
- package/skywalk-sdd/reporting/change-report-renderer.cjs +349 -0
- package/skywalk-sdd/reporting/change-report-view-model.cjs +340 -0
- package/skywalk-sdd/runtime-metadata.cjs +21 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +4 -2
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +2 -2
- package/templates/skills/kld-sdd/opsx-check/checklist.md +1 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +14 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +31 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +6 -6
- package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -7
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
- package/templates/skills/kld-sdd/tdd-core/reference.md +3 -1
- package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
let formatArtifactSize;
|
|
8
|
+
try {
|
|
9
|
+
({ formatArtifactSize } = require('../metrics-v3.cjs'));
|
|
10
|
+
} catch {
|
|
11
|
+
formatArtifactSize = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatArtifactSizeDisplay(bytes) {
|
|
15
|
+
if (typeof formatArtifactSize === 'function') return formatArtifactSize(bytes);
|
|
16
|
+
const n = Number(bytes);
|
|
17
|
+
if (!Number.isFinite(n) || n < 0) return '0.00 KB';
|
|
18
|
+
const mib = 1024 * 1024;
|
|
19
|
+
if (n < mib) return `${(n / 1024).toFixed(2)} KB`;
|
|
20
|
+
return `${(n / mib).toFixed(2)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EVIDENCE_LABELS = {
|
|
24
|
+
TASK_UPDATE_MISSING: '任务状态事件缺失',
|
|
25
|
+
TASK_TEST_EVIDENCE_MISSING: '任务验证证据缺失',
|
|
26
|
+
TASK_TEST_LINKAGE_DEFECT: '任务测试链接缺陷',
|
|
27
|
+
TDD_RED_GREEN_INCOMPLETE: 'TDD 红绿链路不完整',
|
|
28
|
+
TDD_PAIR_RED_DID_NOT_FAIL: 'TDD RED 未有效失败',
|
|
29
|
+
TDD_PAIR_RED_MISSING: 'TDD RED 证据缺失',
|
|
30
|
+
TDD_PAIR_GREEN_MISSING: 'TDD GREEN 证据缺失',
|
|
31
|
+
TDD_PAIR_UNCLASSIFIED: 'TDD 对证据不可判定',
|
|
32
|
+
LEGACY_TASK_PARSER: '任务文档仍使用旧格式',
|
|
33
|
+
apply_test_missing: '实现阶段缺少测试证据',
|
|
34
|
+
tdd_red_missing: 'TDD 缺少有效失败测试',
|
|
35
|
+
process_note_missing: '过程说明缺失',
|
|
36
|
+
task_update_reuse: '测试结果被重复引用',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function eventTimestamp(event) {
|
|
40
|
+
const raw = event?.timestamp || event?.ended_at || event?.started_at;
|
|
41
|
+
const value = Date.parse(raw || '');
|
|
42
|
+
return Number.isFinite(value) ? value : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scopedEventsForChange(events = [], change = null) {
|
|
46
|
+
if (!change) return events;
|
|
47
|
+
return events.filter((event) => (
|
|
48
|
+
event?.change === change
|
|
49
|
+
|| event?.attribution?.target_change === change
|
|
50
|
+
));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractFinalTestResult(events = [], fallback = null, change = null) {
|
|
54
|
+
const testEvents = scopedEventsForChange(events, change)
|
|
55
|
+
.filter((event) => event?.type === 'test_result')
|
|
56
|
+
.sort((a, b) => eventTimestamp(b) - eventTimestamp(a));
|
|
57
|
+
const candidates = testEvents
|
|
58
|
+
.filter((event) => {
|
|
59
|
+
if (event?.type !== 'test_result') return false;
|
|
60
|
+
const result = event.details?.test_results || {};
|
|
61
|
+
return result.evidence_tier === 'strict'
|
|
62
|
+
&& ['green', 'refactor', 'regression'].includes(result.tdd_phase)
|
|
63
|
+
&& result.counts_known === true;
|
|
64
|
+
});
|
|
65
|
+
const event = candidates.find(item => item.details?.test_results?.snapshot_kind === 'final')
|
|
66
|
+
|| candidates[0];
|
|
67
|
+
const history = testEvents
|
|
68
|
+
.filter(item => item !== event)
|
|
69
|
+
.map(item => ({
|
|
70
|
+
event_id: item.event_id || null,
|
|
71
|
+
timestamp: item.timestamp || null,
|
|
72
|
+
passed: Number.isFinite(Number(item.details?.test_results?.passed))
|
|
73
|
+
? Number(item.details.test_results.passed)
|
|
74
|
+
: null,
|
|
75
|
+
failed: Number.isFinite(Number(item.details?.test_results?.failed))
|
|
76
|
+
? Number(item.details.test_results.failed)
|
|
77
|
+
: null,
|
|
78
|
+
snapshot_kind: item.details?.test_results?.snapshot_kind || 'unspecified',
|
|
79
|
+
evidence_tier: item.details?.test_results?.evidence_tier || 'legacy',
|
|
80
|
+
}));
|
|
81
|
+
if (!event) {
|
|
82
|
+
return fallback || {
|
|
83
|
+
passed: null,
|
|
84
|
+
failed: null,
|
|
85
|
+
total: null,
|
|
86
|
+
status: 'unavailable',
|
|
87
|
+
source_event_id: null,
|
|
88
|
+
source: 'unavailable',
|
|
89
|
+
history,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const result = event.details.test_results;
|
|
93
|
+
const passed = Number(result.passed);
|
|
94
|
+
const failed = Number(result.failed);
|
|
95
|
+
const skipped = Number(result.skipped || 0);
|
|
96
|
+
return {
|
|
97
|
+
passed,
|
|
98
|
+
failed,
|
|
99
|
+
skipped,
|
|
100
|
+
total: passed + failed + skipped,
|
|
101
|
+
status: result.result === 'success' && failed === 0 ? 'passed' : 'failed',
|
|
102
|
+
source_event_id: event.event_id || null,
|
|
103
|
+
source: 'strict-test-result',
|
|
104
|
+
snapshot_kind: result.snapshot_kind || 'unspecified',
|
|
105
|
+
history,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeTaskSummary(report) {
|
|
110
|
+
const completion = report.archive_result?.task_completion || {};
|
|
111
|
+
const primary = completion.primary_tasks || {};
|
|
112
|
+
const taskEvidence = report.task_evidence_audit || {};
|
|
113
|
+
const documentTotal = taskEvidence.document_completion?.total
|
|
114
|
+
?? primary.total
|
|
115
|
+
?? completion.total
|
|
116
|
+
?? null;
|
|
117
|
+
const documentCompleted = taskEvidence.document_completion?.completed
|
|
118
|
+
?? primary.completed
|
|
119
|
+
?? completion.completed
|
|
120
|
+
?? null;
|
|
121
|
+
const audit = report.apply_evidence_audit || {};
|
|
122
|
+
const auditTasks = audit.tasks || audit.summary || {};
|
|
123
|
+
const traceabilityVerified = taskEvidence.strict_test_coverage?.verified
|
|
124
|
+
?? auditTasks.with_test_evidence
|
|
125
|
+
?? auditTasks.verified
|
|
126
|
+
?? null;
|
|
127
|
+
const traceabilityTotal = taskEvidence.strict_test_coverage?.total
|
|
128
|
+
?? auditTasks.completed
|
|
129
|
+
?? auditTasks.total
|
|
130
|
+
?? documentTotal;
|
|
131
|
+
return {
|
|
132
|
+
document: {
|
|
133
|
+
completed: documentCompleted,
|
|
134
|
+
total: documentTotal,
|
|
135
|
+
status: documentTotal == null
|
|
136
|
+
? 'unavailable'
|
|
137
|
+
: (documentCompleted === documentTotal ? 'complete' : 'incomplete'),
|
|
138
|
+
source: 'tasks-document',
|
|
139
|
+
},
|
|
140
|
+
direct_test_backlink: taskEvidence.direct_test_backlink || null,
|
|
141
|
+
strict_test_coverage: taskEvidence.strict_test_coverage || null,
|
|
142
|
+
traceability: {
|
|
143
|
+
verified: traceabilityVerified,
|
|
144
|
+
total: traceabilityTotal,
|
|
145
|
+
status: traceabilityTotal == null || traceabilityVerified == null
|
|
146
|
+
? 'unavailable'
|
|
147
|
+
: (traceabilityVerified === traceabilityTotal ? 'complete' : 'incomplete'),
|
|
148
|
+
source: 'strict-event-audit',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function issueGuidance(code) {
|
|
154
|
+
return ({
|
|
155
|
+
TASK_UPDATE_MISSING: {
|
|
156
|
+
expected: '已完成任务应有对应的 completed task_update 事件。',
|
|
157
|
+
root_cause: '任务文档状态与结构化事件没有同步。',
|
|
158
|
+
impact: '任务完成状态不能被事件链直接验证。',
|
|
159
|
+
remediation: '补录真实 task_update;若历史证据不存在,明确标记不可追溯。',
|
|
160
|
+
},
|
|
161
|
+
TASK_TEST_LINKAGE_DEFECT: {
|
|
162
|
+
expected: 'task_update 应直接引用覆盖当前任务的 strict 成功测试。',
|
|
163
|
+
root_cause: 'test_event_id 缺失、无效或没有覆盖当前任务。',
|
|
164
|
+
impact: '测试存在,但任务到测试的直接审计链不完整。',
|
|
165
|
+
remediation: '将 task_update 精确关联到覆盖当前任务的 strict 测试事件。',
|
|
166
|
+
},
|
|
167
|
+
TASK_TEST_EVIDENCE_MISSING: {
|
|
168
|
+
expected: '任务应有严格成功测试,或有可审计的不适用理由。',
|
|
169
|
+
root_cause: '没有找到直接引用、间接严格覆盖或有效豁免。',
|
|
170
|
+
impact: '无法验证任务完成状态是否经过测试。',
|
|
171
|
+
remediation: '执行并记录覆盖测试;确实不适用时填写具体原因。',
|
|
172
|
+
},
|
|
173
|
+
TDD_RED_GREEN_INCOMPLETE: {
|
|
174
|
+
expected: 'TDD 证据应按 pair 形成有效 RED→GREEN 顺序。',
|
|
175
|
+
root_cause: '旧版逐任务算法无法正确表达成对 TDD 证据。',
|
|
176
|
+
impact: '可能把已有配对证据误报为缺失。',
|
|
177
|
+
remediation: '使用 pair 级审计结果,不再生成逐任务 TDD 告警。',
|
|
178
|
+
},
|
|
179
|
+
}[code] || {
|
|
180
|
+
expected: '相关规则所要求的证据应完整且可定位。',
|
|
181
|
+
root_cause: '当前事件或文档证据未满足该规则。',
|
|
182
|
+
impact: '对应结论的可信度会降低。',
|
|
183
|
+
remediation: '根据规则编码定位证据源,补充或纠正真实记录。',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizeIssue(alert, index) {
|
|
188
|
+
const code = alert.warning || alert.code || 'UNKNOWN';
|
|
189
|
+
const guidance = issueGuidance(code);
|
|
190
|
+
const eventIds = [...new Set([
|
|
191
|
+
alert.opened_by_event_id,
|
|
192
|
+
alert.last_seen_event_id,
|
|
193
|
+
alert.resolved_by_event_id,
|
|
194
|
+
alert.evidence_event_id,
|
|
195
|
+
...(Array.isArray(alert.evidence_event_ids) ? alert.evidence_event_ids : []),
|
|
196
|
+
].filter(Boolean))];
|
|
197
|
+
const subject = alert.subject || alert.task_id || alert.pair_id || null;
|
|
198
|
+
const status = alert.status
|
|
199
|
+
|| (['fixed', 'accepted', 'waived'].includes(alert.disposition) ? 'resolved' : 'open');
|
|
200
|
+
return {
|
|
201
|
+
issue_id: alert.issue_id || alert.key || `${code}:${subject || 'change'}:${index + 1}`,
|
|
202
|
+
category: alert.category || 'evidence',
|
|
203
|
+
code,
|
|
204
|
+
label: EVIDENCE_LABELS[code] || code,
|
|
205
|
+
severity: alert.severity || 'warning',
|
|
206
|
+
verdict: alert.verdict || (status === 'resolved' ? 'resolved' : 'evidence-gap'),
|
|
207
|
+
status,
|
|
208
|
+
subject,
|
|
209
|
+
task_id: alert.task_id || null,
|
|
210
|
+
pair_id: alert.pair_id || null,
|
|
211
|
+
expected: alert.expected || guidance.expected,
|
|
212
|
+
actual: alert.actual || alert.message || alert.reason || '当前证据未满足规则要求。',
|
|
213
|
+
root_cause: alert.root_cause || guidance.root_cause,
|
|
214
|
+
impact: alert.impact || guidance.impact,
|
|
215
|
+
remediation: alert.remediation || guidance.remediation,
|
|
216
|
+
evidence_event_ids: eventIds,
|
|
217
|
+
related_event_ids: Array.isArray(alert.related_event_ids) ? alert.related_event_ids : [],
|
|
218
|
+
disposition: alert.disposition || (status === 'resolved' ? 'fixed' : 'open'),
|
|
219
|
+
occurrence_count: Math.max(1, Number(alert.occurrences) || 1),
|
|
220
|
+
history: {
|
|
221
|
+
first_seen_at: alert.first_seen_at || null,
|
|
222
|
+
last_seen_at: alert.last_seen_at || null,
|
|
223
|
+
resolved_at: alert.resolved_at || null,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeEvidenceSummary(report) {
|
|
229
|
+
const alerts = report.evidence_alerts || {};
|
|
230
|
+
const alertHistory = Array.isArray(alerts.history) && alerts.history.length > 0
|
|
231
|
+
? alerts.history
|
|
232
|
+
: [...(alerts.open || []), ...(alerts.resolved || [])];
|
|
233
|
+
const history = [
|
|
234
|
+
...alertHistory,
|
|
235
|
+
...(report.check_warning_dispositions || []).map(item => ({
|
|
236
|
+
...item,
|
|
237
|
+
code: item.warning,
|
|
238
|
+
occurrences: 1,
|
|
239
|
+
status: ['fixed', 'accepted', 'waived'].includes(item.disposition) ? 'resolved' : 'open',
|
|
240
|
+
last_seen_event_id: item.evidence_event_id,
|
|
241
|
+
})),
|
|
242
|
+
];
|
|
243
|
+
const issues = history.map(normalizeIssue);
|
|
244
|
+
const groups = new Map();
|
|
245
|
+
for (const issue of issues) {
|
|
246
|
+
const code = issue.code;
|
|
247
|
+
if (!groups.has(code)) {
|
|
248
|
+
groups.set(code, {
|
|
249
|
+
code,
|
|
250
|
+
label: EVIDENCE_LABELS[code] || code,
|
|
251
|
+
occurrence_count: 0,
|
|
252
|
+
affected_tasks: [],
|
|
253
|
+
event_ids: [],
|
|
254
|
+
dispositions: {},
|
|
255
|
+
issues: [],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const group = groups.get(code);
|
|
259
|
+
group.occurrence_count += issue.occurrence_count;
|
|
260
|
+
group.issues.push(issue);
|
|
261
|
+
const task = issue.task_id || issue.subject;
|
|
262
|
+
if (task && !group.affected_tasks.includes(task)) group.affected_tasks.push(task);
|
|
263
|
+
for (const eventId of issue.evidence_event_ids) {
|
|
264
|
+
if (eventId && !group.event_ids.includes(eventId)) group.event_ids.push(eventId);
|
|
265
|
+
}
|
|
266
|
+
const disposition = issue.disposition;
|
|
267
|
+
group.dispositions[disposition] = (group.dispositions[disposition] || 0) + 1;
|
|
268
|
+
}
|
|
269
|
+
const categories = [...groups.values()];
|
|
270
|
+
return {
|
|
271
|
+
category_count: categories.length,
|
|
272
|
+
occurrence_count: categories.reduce((sum, item) => sum + item.occurrence_count, 0),
|
|
273
|
+
categories,
|
|
274
|
+
issues,
|
|
275
|
+
note: '一次任务或事件可能同时命中多条规则;命中次数不等于独立缺陷数。',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeChangedFiles(changedFiles = {}) {
|
|
280
|
+
const taskEventFiles = Array.isArray(changedFiles.task_event_files)
|
|
281
|
+
? [...new Set(changedFiles.task_event_files)]
|
|
282
|
+
: null;
|
|
283
|
+
const finalOutputFiles = Array.isArray(changedFiles.final_output_files)
|
|
284
|
+
? [...new Set(changedFiles.final_output_files)]
|
|
285
|
+
: (Array.isArray(changedFiles.files) ? [...new Set(changedFiles.files)] : null);
|
|
286
|
+
const taskCount = changedFiles.task_event_files_changed
|
|
287
|
+
?? changedFiles.files_changed
|
|
288
|
+
?? taskEventFiles?.length
|
|
289
|
+
?? null;
|
|
290
|
+
const finalCount = changedFiles.final_output_files_changed
|
|
291
|
+
?? finalOutputFiles?.length
|
|
292
|
+
?? null;
|
|
293
|
+
const countsDiffer = taskCount != null && finalCount != null && taskCount !== finalCount;
|
|
294
|
+
const normalized = {
|
|
295
|
+
...changedFiles,
|
|
296
|
+
task_event_files: taskEventFiles,
|
|
297
|
+
task_event_files_changed: taskCount,
|
|
298
|
+
final_output_files: finalOutputFiles,
|
|
299
|
+
final_output_files_changed: finalCount,
|
|
300
|
+
diff_source: changedFiles.diff_source || 'dual-source',
|
|
301
|
+
line_count_estimate: changedFiles.line_count_estimate ?? null,
|
|
302
|
+
reconciliation_status: changedFiles.reconciliation_status
|
|
303
|
+
|| (countsDiffer ? 'count-mismatch' : 'consistent'),
|
|
304
|
+
reconciliation_note: changedFiles.reconciliation_note
|
|
305
|
+
|| (countsDiffer
|
|
306
|
+
? `任务事件累计涉及 ${taskCount} 个文件;最终输出快照包含 ${finalCount} 个文件。两者用途不同,不应合并为一个口径。`
|
|
307
|
+
: '任务事件与最终输出文件口径一致。'),
|
|
308
|
+
};
|
|
309
|
+
// v4:不再对外输出 legacy_fields;若调用方传入则显式剔除
|
|
310
|
+
delete normalized.legacy_fields;
|
|
311
|
+
return normalized;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function resolveArtifact(projectRoot, artifactPath, options = {}) {
|
|
315
|
+
if (!artifactPath) return { path: null, exists: false, size_bytes: null, size_display: null, sha256: null };
|
|
316
|
+
const absolute = path.isAbsolute(artifactPath)
|
|
317
|
+
? artifactPath
|
|
318
|
+
: path.resolve(projectRoot || process.cwd(), artifactPath);
|
|
319
|
+
try {
|
|
320
|
+
const stat = fs.statSync(absolute);
|
|
321
|
+
if (!stat.isFile()) {
|
|
322
|
+
return { path: artifactPath, exists: false, size_bytes: null, size_display: null, sha256: null };
|
|
323
|
+
}
|
|
324
|
+
const sizeBytes = stat.size;
|
|
325
|
+
const sizeDisplay = formatArtifactSizeDisplay(sizeBytes);
|
|
326
|
+
if (options.selfReferential) {
|
|
327
|
+
return {
|
|
328
|
+
path: artifactPath,
|
|
329
|
+
exists: true,
|
|
330
|
+
size_bytes: sizeBytes,
|
|
331
|
+
size_display: sizeDisplay,
|
|
332
|
+
sha256: null,
|
|
333
|
+
integrity_note: '报告可展示自身大小;哈希不内嵌自身,请以归档 Manifest 为准',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const sha256 = crypto.createHash('sha256').update(fs.readFileSync(absolute)).digest('hex');
|
|
337
|
+
return {
|
|
338
|
+
path: artifactPath,
|
|
339
|
+
exists: true,
|
|
340
|
+
size_bytes: sizeBytes,
|
|
341
|
+
size_display: sizeDisplay,
|
|
342
|
+
sha256: `sha256:${sha256}`,
|
|
343
|
+
};
|
|
344
|
+
} catch {
|
|
345
|
+
return { path: artifactPath, exists: false, size_bytes: null, size_display: null, sha256: null };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function normalizeArtifacts(report) {
|
|
350
|
+
const archive = report.archive_result || {};
|
|
351
|
+
const existing = report.artifacts || {};
|
|
352
|
+
const projectRoot = report.project_root;
|
|
353
|
+
const paths = {
|
|
354
|
+
markdown_report: archive.report_path,
|
|
355
|
+
html_report: archive.report_html_path,
|
|
356
|
+
json_report: archive.report_json_path,
|
|
357
|
+
execution_log: archive.execution_log_path
|
|
358
|
+
|| existing.execution_log_path
|
|
359
|
+
|| (archive.archive_path ? path.join(archive.archive_path, 'logs', 'execution-log.md') : null),
|
|
360
|
+
manifest: archive.manifest_path || existing.manifest_path,
|
|
361
|
+
archive_zip: archive.archive_zip_path
|
|
362
|
+
|| archive.zip_path
|
|
363
|
+
|| archive.package_path
|
|
364
|
+
|| existing.archive_zip_path
|
|
365
|
+
|| existing.package_path,
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
...existing,
|
|
369
|
+
files: Object.fromEntries(
|
|
370
|
+
Object.entries(paths).map(([name, artifactPath]) => [
|
|
371
|
+
name,
|
|
372
|
+
resolveArtifact(projectRoot, artifactPath, {
|
|
373
|
+
selfReferential: [
|
|
374
|
+
'markdown_report',
|
|
375
|
+
'html_report',
|
|
376
|
+
'json_report',
|
|
377
|
+
'manifest',
|
|
378
|
+
'archive_zip',
|
|
379
|
+
].includes(name),
|
|
380
|
+
}),
|
|
381
|
+
]),
|
|
382
|
+
),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildWarningDispositions(report) {
|
|
387
|
+
const alerts = [
|
|
388
|
+
...(report.evidence_alerts?.open || []),
|
|
389
|
+
...(report.evidence_alerts?.resolved || []),
|
|
390
|
+
...(report.check_warning_dispositions || []),
|
|
391
|
+
];
|
|
392
|
+
const allowed = new Set(['fixed', 'accepted', 'waived', 'needs_input', 'open']);
|
|
393
|
+
const checkWarnings = new Set((report.check_warning_dispositions || []).map(item => item.warning));
|
|
394
|
+
const byWarning = new Map();
|
|
395
|
+
for (const alert of alerts) {
|
|
396
|
+
const inferred = alert.status === 'resolved' ? 'fixed' : 'open';
|
|
397
|
+
const disposition = allowed.has(alert.disposition) ? alert.disposition : inferred;
|
|
398
|
+
const normalized = {
|
|
399
|
+
warning: alert.warning || alert.code || 'UNKNOWN',
|
|
400
|
+
subject: alert.subject || alert.task_id || null,
|
|
401
|
+
disposition,
|
|
402
|
+
reason: alert.disposition_reason || alert.reason || alert.message || null,
|
|
403
|
+
evidence_event_id: alert.evidence_event_id || alert.resolved_by_event_id || alert.last_seen_event_id || null,
|
|
404
|
+
};
|
|
405
|
+
if (!byWarning.has(normalized.warning) || checkWarnings.has(normalized.warning)) {
|
|
406
|
+
byWarning.set(normalized.warning, normalized);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return [...byWarning.values()];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function buildChangeReportModel(report, events = []) {
|
|
413
|
+
if (!report || report.level !== 'change' || !report.change) return report;
|
|
414
|
+
const scopedEvents = scopedEventsForChange(events, report.change);
|
|
415
|
+
const inputEventCount = Array.isArray(events) ? events.length : 0;
|
|
416
|
+
const changedFiles = normalizeChangedFiles(report.changed_files);
|
|
417
|
+
const existingSummary = report.change_summary || {};
|
|
418
|
+
const changeSummary = {
|
|
419
|
+
tests: scopedEvents.length > 0
|
|
420
|
+
? extractFinalTestResult(scopedEvents, report.final_test_result || existingSummary.tests || null, report.change)
|
|
421
|
+
: (existingSummary.tests || extractFinalTestResult(scopedEvents, report.final_test_result || null, report.change)),
|
|
422
|
+
tasks: existingSummary.tasks || normalizeTaskSummary(report),
|
|
423
|
+
evidence: existingSummary.evidence || normalizeEvidenceSummary(report),
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
...report,
|
|
427
|
+
report_scope: {
|
|
428
|
+
...(report.report_scope || {}),
|
|
429
|
+
kind: 'single-change',
|
|
430
|
+
change: report.change,
|
|
431
|
+
input_event_count: report.report_scope?.input_event_count ?? inputEventCount,
|
|
432
|
+
included_event_count: report.report_scope?.included_event_count ?? scopedEvents.length,
|
|
433
|
+
excluded_event_count: report.report_scope?.excluded_event_count
|
|
434
|
+
?? Math.max(0, inputEventCount - scopedEvents.length),
|
|
435
|
+
cross_change_data_included: false,
|
|
436
|
+
},
|
|
437
|
+
metric_provenance: report.metric_provenance || report.metric_evidence || null,
|
|
438
|
+
change_summary: changeSummary,
|
|
439
|
+
changed_files: changedFiles,
|
|
440
|
+
warning_dispositions: buildWarningDispositions(report),
|
|
441
|
+
artifacts: normalizeArtifacts(report),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
module.exports = {
|
|
446
|
+
EVIDENCE_LABELS,
|
|
447
|
+
buildChangeReportModel,
|
|
448
|
+
extractFinalTestResult,
|
|
449
|
+
normalizeEvidenceSummary,
|
|
450
|
+
normalizeChangedFiles,
|
|
451
|
+
formatArtifactSizeDisplay,
|
|
452
|
+
};
|