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,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildChangeReportModel } = require('./change-report-model.cjs');
|
|
4
|
+
const { buildChangeReportViewModel } = require('./change-report-view-model.cjs');
|
|
5
|
+
|
|
6
|
+
function escapeHtml(value) {
|
|
7
|
+
return String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
|
8
|
+
'&': '&',
|
|
9
|
+
'<': '<',
|
|
10
|
+
'>': '>',
|
|
11
|
+
'"': '"',
|
|
12
|
+
"'": ''',
|
|
13
|
+
}[char]));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function statusTone(status) {
|
|
17
|
+
if (['trusted', 'complete', 'passed', 'verified', 'implemented'].includes(status)) return 'good';
|
|
18
|
+
if (['attention', 'incomplete', 'provisional', 'needs_input', 'open'].includes(status)) return 'warn';
|
|
19
|
+
if (['failed', 'blocked', 'error'].includes(status)) return 'bad';
|
|
20
|
+
return 'neutral';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function metricCard({ name, code, value, state, explanation, source }) {
|
|
24
|
+
return `<article class="metric-card">
|
|
25
|
+
<div class="metric-top"><span>${escapeHtml(name)}</span><span class="metric-code">${escapeHtml(code)}</span></div>
|
|
26
|
+
<strong>${escapeHtml(value)}</strong>
|
|
27
|
+
<div class="metric-state ${statusTone(state)}">${escapeHtml(state || '暂定')}</div>
|
|
28
|
+
<p>${escapeHtml(explanation)}</p>
|
|
29
|
+
<button class="info-btn" type="button" data-info="${escapeHtml(source || explanation)}" data-drawer-title="${escapeHtml(`${name} · ${code}`)}" data-drawer-body="${escapeHtml(source || explanation)}">查看计算口径与证据</button>
|
|
30
|
+
</article>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderChangeReportHtml(report) {
|
|
34
|
+
if (!report.change_summary || !report.artifacts?.files) {
|
|
35
|
+
report = buildChangeReportModel(report);
|
|
36
|
+
}
|
|
37
|
+
const view = buildChangeReportViewModel(report);
|
|
38
|
+
const overviewSection = view.sections.overview;
|
|
39
|
+
const metricsSection = view.sections['metrics-stages'];
|
|
40
|
+
const issueSection = view.sections['issues-evidence'];
|
|
41
|
+
const artifactSection = view.sections['artifacts-trace'];
|
|
42
|
+
const summary = report.change_summary || {};
|
|
43
|
+
const evidence = {
|
|
44
|
+
...(summary.evidence || {}),
|
|
45
|
+
categories: issueSection.categories || [],
|
|
46
|
+
issues: issueSection.issues || [],
|
|
47
|
+
};
|
|
48
|
+
const changed = artifactSection.changed_files || {};
|
|
49
|
+
const artifacts = artifactSection.artifacts || [];
|
|
50
|
+
const conclusionTone = statusTone(view.conclusion?.status);
|
|
51
|
+
const evidenceCategories = evidence.categories;
|
|
52
|
+
const stageNodes = metricsSection.stages || [];
|
|
53
|
+
const activeLinks = artifactSection.attribution?.active_links || [];
|
|
54
|
+
const candidateIds = artifactSection.attribution?.unassigned_candidate_event_ids || [];
|
|
55
|
+
const knownWarnings = view.known_risks?.warnings || [];
|
|
56
|
+
const warningDetails = knownWarnings.flatMap((warning) => (
|
|
57
|
+
Array.isArray(warning.items) ? warning.items : []
|
|
58
|
+
));
|
|
59
|
+
const telemetryWarnings = view.telemetry_warnings?.by_type || {};
|
|
60
|
+
const artifactRows = artifacts.map((item) => `
|
|
61
|
+
<tr>
|
|
62
|
+
<td>${escapeHtml({
|
|
63
|
+
markdown_report: 'Markdown 报告',
|
|
64
|
+
html_report: 'HTML 报告',
|
|
65
|
+
json_report: 'JSON 报告',
|
|
66
|
+
execution_log: '执行日志',
|
|
67
|
+
manifest: '归档清单',
|
|
68
|
+
archive_zip: '归档包',
|
|
69
|
+
}[item.name] || item.name)}</td>
|
|
70
|
+
<td><span class="state ${item?.exists ? 'good' : 'neutral'}">${item?.exists ? '已生成' : '未生成'}</span></td>
|
|
71
|
+
<td class="path">${escapeHtml(item?.path || '—')}</td>
|
|
72
|
+
<td>${item?.size_display
|
|
73
|
+
? escapeHtml(item.size_display)
|
|
74
|
+
: (item?.size_bytes == null ? '—' : escapeHtml(item.size_bytes))}</td>
|
|
75
|
+
</tr>`).join('');
|
|
76
|
+
const evidenceRows = evidenceCategories.map((group) => {
|
|
77
|
+
const issueDetails = (group.issues || []).map((issue) => [
|
|
78
|
+
`问题:${issue.issue_id}`,
|
|
79
|
+
`对象:${issue.subject || '本次变更'}`,
|
|
80
|
+
`状态:${issue.status}`,
|
|
81
|
+
`判定:${issue.verdict}`,
|
|
82
|
+
`根因:${issue.root_cause}`,
|
|
83
|
+
`预期:${issue.expected}`,
|
|
84
|
+
`实际:${issue.actual}`,
|
|
85
|
+
`影响:${issue.impact}`,
|
|
86
|
+
`修复建议:${issue.remediation}`,
|
|
87
|
+
`证据事件:${(issue.evidence_event_ids || []).join('、') || '暂无可定位事件'}`,
|
|
88
|
+
].join('\n')).join('\n\n');
|
|
89
|
+
return `
|
|
90
|
+
<article class="issue-row">
|
|
91
|
+
<div><span class="issue-count">${escapeHtml(group.occurrence_count)}</span></div>
|
|
92
|
+
<div>
|
|
93
|
+
<h3>${escapeHtml(group.label)}</h3>
|
|
94
|
+
<p>${escapeHtml(group.affected_tasks?.length || 0)} 个关联任务 · ${escapeHtml(group.event_ids?.length || 0)} 个可定位事件</p>
|
|
95
|
+
</div>
|
|
96
|
+
<button class="text-button drawer-trigger" type="button"
|
|
97
|
+
data-drawer-title="${escapeHtml(group.label)}"
|
|
98
|
+
data-drawer-body="${escapeHtml(`${issueDetails || `规则编码:${group.code}\n状态:open\n根因:当前证据未满足规则\n预期:规则证据完整\n实际:证据不足\n修复建议:补充真实证据`}\n\n规则编码:${group.code}\n关联任务:${(group.affected_tasks || []).join('、') || '无'}\n事件:${(group.event_ids || []).join('、') || '无'}\n处置:${JSON.stringify(group.dispositions || {})}`)}">查看详情</button>
|
|
99
|
+
</article>`;
|
|
100
|
+
}).join('');
|
|
101
|
+
const stageRows = stageNodes.map((node) => {
|
|
102
|
+
const stage = node.display_stage;
|
|
103
|
+
const round = node.display_round;
|
|
104
|
+
return `
|
|
105
|
+
<tr class="tl-node">
|
|
106
|
+
<td>${escapeHtml(stage)}${round > 1 ? '²' : ''}${round > 1 ? '<span class="tl-repeat">R</span>' : ''}</td>
|
|
107
|
+
<td>${escapeHtml(round)}</td>
|
|
108
|
+
<td><span class="tl-time">${escapeHtml(node.display_start)}→${escapeHtml(node.display_end)}</span><br>${escapeHtml(node.display_duration)}</td>
|
|
109
|
+
<td><span class="state ${statusTone(node.display_result === 'success' ? 'passed' : node.display_result)}">${escapeHtml(node.display_result)}</span></td>
|
|
110
|
+
</tr>`;
|
|
111
|
+
}).join('');
|
|
112
|
+
|
|
113
|
+
const metricCards = (metricsSection.metric_cards || []).map(metricCard).join('');
|
|
114
|
+
const summaryCards = (overviewSection.cards || []).map((card) => `
|
|
115
|
+
<article class="summary-card"><span>${escapeHtml(card.label)}</span><strong>${escapeHtml(card.value)}</strong></article>`).join('');
|
|
116
|
+
const metricRows = (metricsSection.metric_rows || []).map((row) => `
|
|
117
|
+
<tr><td>${escapeHtml(row.label)}${row.code ? ` <span class="metric-code">${escapeHtml(row.code)}</span>` : ''}</td><td>${escapeHtml(row.value)}</td></tr>`).join('');
|
|
118
|
+
|
|
119
|
+
return `<!doctype html>
|
|
120
|
+
<html lang="zh-CN" data-theme="light">
|
|
121
|
+
<head>
|
|
122
|
+
<meta charset="utf-8">
|
|
123
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
124
|
+
<meta name="sdd-semantic-digest" content="${view.semantic_digest}">
|
|
125
|
+
<title>SDD 效果度量报告 · ${escapeHtml(report.change)}</title>
|
|
126
|
+
<style>
|
|
127
|
+
:root{--ink:#16213a;--muted:#68758c;--line:#dfe5ef;--paper:#fff;--canvas:#eef2f7;--blue:#175cd3;--blue-soft:#edf4ff;--good:#067647;--good-soft:#ecfdf3;--warn:#b54708;--warn-soft:#fffaeb;--bad:#b42318;--bad-soft:#fef3f2;--shadow:0 18px 54px rgba(31,43,66,.1)}
|
|
128
|
+
*{box-sizing:border-box}
|
|
129
|
+
[hidden]{display:none!important}
|
|
130
|
+
body{margin:0;min-width:1040px;background:var(--canvas);color:var(--ink);font:14px/1.55 "Segoe UI","Microsoft YaHei",sans-serif}
|
|
131
|
+
button{font:inherit}
|
|
132
|
+
.shell{width:min(1280px,calc(100% - 56px));margin:26px auto 46px}
|
|
133
|
+
.report-head{position:sticky;top:0;z-index:20;background:rgba(255,255,255,.96);border:1px solid var(--line);border-radius:18px 18px 0 0;box-shadow:var(--shadow);backdrop-filter:blur(12px)}
|
|
134
|
+
.identity{display:flex;align-items:center;justify-content:space-between;padding:22px 26px 16px;gap:24px}
|
|
135
|
+
.eyebrow{color:var(--blue);font-weight:750;font-size:12px;letter-spacing:.08em}
|
|
136
|
+
h1{font-size:26px;line-height:1.2;margin:3px 0 7px;letter-spacing:-.025em}
|
|
137
|
+
.meta,.micro{color:var(--muted);font-size:12px}.meta code{color:#35425a}
|
|
138
|
+
.freshness{text-align:right}.freshness strong{display:block;color:var(--good);font-size:13px}
|
|
139
|
+
.tabs{display:flex;padding:0 18px;border-top:1px solid #eef1f5}
|
|
140
|
+
.tabs button{border:0;border-bottom:3px solid transparent;background:none;color:#56647b;padding:14px 18px 12px;cursor:pointer;font-weight:650}
|
|
141
|
+
.tabs button:hover{color:var(--blue)}.tabs button:focus-visible{outline:3px solid #b2ccff;outline-offset:-3px}
|
|
142
|
+
.tabs button[aria-selected="true"]{color:var(--blue);border-color:var(--blue)}
|
|
143
|
+
.panel{background:#f8fafc;border:1px solid var(--line);border-top:0;border-radius:0 0 18px 18px;padding:22px;min-height:560px}
|
|
144
|
+
.hero{display:grid;grid-template-columns:1.55fr .45fr;gap:16px;margin-bottom:16px}
|
|
145
|
+
.card{background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:18px}
|
|
146
|
+
.conclusion{background:linear-gradient(135deg,#f7faff,#eef5ff);border-color:#c8d9f5}
|
|
147
|
+
.conclusion h2,.section-head h2{margin:0}.conclusion p{font-size:18px;font-weight:680;color:#203e73;margin:10px 0 0}
|
|
148
|
+
.scope{display:flex;flex-direction:column;justify-content:center}.scope strong{font-size:19px}.scope span{color:var(--muted);margin-top:4px}
|
|
149
|
+
.summary-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
|
150
|
+
.summary-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px;min-height:112px}
|
|
151
|
+
.summary-card span{color:var(--muted);font-size:12px}.summary-card strong{display:block;font-size:24px;margin:7px 0 2px;font-variant-numeric:tabular-nums}.summary-card p{margin:0;color:var(--muted);font-size:11px}
|
|
152
|
+
.section{margin-top:16px}.section-head{display:flex;align-items:end;justify-content:space-between;margin-bottom:11px}.section-head p{margin:0;color:var(--muted);font-size:12px}
|
|
153
|
+
.metric-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}
|
|
154
|
+
.metric-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:15px;min-height:210px;display:flex;flex-direction:column}
|
|
155
|
+
.metric-top{display:flex;justify-content:space-between;gap:8px;color:#344054;font-weight:700}.metric-code{color:var(--blue);background:var(--blue-soft);border-radius:999px;padding:1px 7px;font-size:11px;white-space:nowrap}
|
|
156
|
+
.metric-card>strong{font-size:21px;margin:15px 0 5px}.metric-card p{color:var(--muted);font-size:12px;margin:10px 0;flex:1}
|
|
157
|
+
.metric-state,.state{display:inline-flex;width:max-content;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700;background:#f2f4f7;color:#475467}
|
|
158
|
+
.good{background:var(--good-soft)!important;color:var(--good)!important}.warn{background:var(--warn-soft)!important;color:var(--warn)!important}.bad{background:var(--bad-soft)!important;color:var(--bad)!important}.neutral{background:#f2f4f7!important;color:#475467!important}
|
|
159
|
+
.text-button{border:0;background:none;color:var(--blue);padding:0;cursor:pointer;text-align:left;font-weight:650;font-size:12px}.text-button:hover{text-decoration:underline}.text-button:focus-visible{outline:3px solid #b2ccff;outline-offset:3px}
|
|
160
|
+
.info-btn{border:0;background:none;color:var(--blue);padding:0;cursor:pointer;text-align:left;font-weight:650;font-size:12px}.muted-null{color:var(--muted)}
|
|
161
|
+
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:16px}.wide-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:18px}
|
|
162
|
+
table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:1px solid #edf0f4;text-align:left;vertical-align:top}th{font-size:12px;color:var(--muted)}tr:last-child td{border-bottom:0}.path{max-width:420px;overflow-wrap:anywhere;color:#475467;font-family:Consolas,monospace;font-size:11px}
|
|
163
|
+
.issue-summary{display:flex;align-items:center;gap:12px;background:var(--warn-soft);border:1px solid #fedf89;border-radius:14px;padding:15px 18px;margin-bottom:12px}.issue-summary strong{font-size:18px}.issue-summary p{margin:0;color:#7a2e0e}
|
|
164
|
+
.issue-row{display:grid;grid-template-columns:52px 1fr auto;align-items:center;gap:14px;padding:15px 4px;border-bottom:1px solid #edf0f4}.issue-row:last-child{border-bottom:0}.issue-count{display:grid;place-items:center;width:42px;height:42px;border-radius:12px;background:var(--warn-soft);color:var(--warn);font-weight:800}.issue-row h3{margin:0 0 2px}.issue-row p{margin:0;color:var(--muted);font-size:12px}
|
|
165
|
+
.callout{border-left:4px solid var(--blue);background:var(--blue-soft);padding:12px 14px;border-radius:0 10px 10px 0;color:#344054}
|
|
166
|
+
.drawer-backdrop{position:fixed;inset:0;z-index:40;background:rgba(15,23,42,.42);display:none}.drawer-backdrop.open{display:block}
|
|
167
|
+
.drawer{position:absolute;right:0;top:0;height:100%;width:520px;background:#fff;padding:26px;box-shadow:-20px 0 70px rgba(15,23,42,.22);overflow:auto}
|
|
168
|
+
.drawer-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.drawer h2{margin:0}.drawer pre{white-space:pre-wrap;word-break:break-word;background:#f8fafc;border:1px solid var(--line);border-radius:12px;padding:15px;color:#344054}
|
|
169
|
+
.icon-button{border:1px solid var(--line);background:#fff;border-radius:9px;width:34px;height:34px;cursor:pointer}.icon-button:focus-visible{outline:3px solid #b2ccff}
|
|
170
|
+
@media print{body{min-width:0;background:#fff}.shell{width:100%;margin:0}.report-head{position:static;box-shadow:none}.tabs{display:none}.panel{display:block!important;border:0;min-height:0;break-before:page}.drawer-backdrop{display:none!important}}
|
|
171
|
+
</style>
|
|
172
|
+
</head>
|
|
173
|
+
<body>
|
|
174
|
+
<main class="shell">
|
|
175
|
+
<header class="report-head">
|
|
176
|
+
<div class="identity">
|
|
177
|
+
<div>
|
|
178
|
+
<div class="eyebrow">SDD · 单次变更报告</div>
|
|
179
|
+
<h1>${escapeHtml(report.change)}</h1>
|
|
180
|
+
<div class="meta">仅展示本次变更 · Schema ${escapeHtml(report.schema_version)} · 指标契约 ${escapeHtml(report.metrics_contract)}</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="freshness"><strong>数据范围已锁定</strong><span class="micro">生成于 ${escapeHtml(report.generated_at || '未知')}</span></div>
|
|
183
|
+
</div>
|
|
184
|
+
<nav class="tabs" role="tablist" aria-label="报告章节">
|
|
185
|
+
<button role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0">概览</button>
|
|
186
|
+
<button role="tab" id="tab-metrics" aria-controls="panel-metrics" aria-selected="false" tabindex="-1">指标与阶段</button>
|
|
187
|
+
<button role="tab" id="tab-evidence" aria-controls="panel-evidence" aria-selected="false" tabindex="-1">问题与证据</button>
|
|
188
|
+
<button role="tab" id="tab-artifacts" aria-controls="panel-artifacts" aria-selected="false" tabindex="-1">产物与追溯</button>
|
|
189
|
+
</nav>
|
|
190
|
+
</header>
|
|
191
|
+
|
|
192
|
+
<section class="panel" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
|
|
193
|
+
<div class="hero">
|
|
194
|
+
<article class="card conclusion">
|
|
195
|
+
<span class="state ${conclusionTone}">${escapeHtml(view.conclusion?.status === 'trusted' ? '结论可信' : '需要关注')}</span>
|
|
196
|
+
<h2>一句话结论</h2>
|
|
197
|
+
<p>${escapeHtml(view.conclusion?.text || '当前证据不足,暂不能形成可信结论。')}</p>
|
|
198
|
+
</article>
|
|
199
|
+
<article class="card scope"><span>七个标准阶段总耗时</span><strong>${escapeHtml(overviewSection.scope_duration)}</strong><span>辅助阶段不进入效率分母</span></article>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="summary-grid">${summaryCards}</div>
|
|
202
|
+
<div class="section">
|
|
203
|
+
<div class="section-head"><h2>核心指标</h2><p>中文业务名称为主,编码仅作定位标签</p></div>
|
|
204
|
+
<div class="metric-grid">${metricCards}</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="section card">
|
|
207
|
+
<div class="section-head"><h2>需要处理</h2><p>证据问题已归并到“问题与证据”页签</p></div>
|
|
208
|
+
<p class="${evidence.category_count ? 'warn' : 'good'} state">${evidence.category_count ? `${escapeHtml(evidence.category_count)} 类问题待处置` : '当前无需处理'}</p>
|
|
209
|
+
${telemetryWarnings.task_update_reuse > 0 ? '<div class="quality-alert">检测到 task_update 复用;相关可信度已降级。</div>' : ''}
|
|
210
|
+
</div>
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
<section class="panel" role="tabpanel" id="panel-metrics" aria-labelledby="tab-metrics" hidden>
|
|
214
|
+
<div class="section-head"><div><h2>指标与阶段</h2><p>本页只解释本次变更的计算口径和阶段执行。</p></div></div>
|
|
215
|
+
<div class="two-col">
|
|
216
|
+
<article class="wide-card">
|
|
217
|
+
<h3>效率拆分</h3>
|
|
218
|
+
<table><tbody>${metricRows}</tbody></table>
|
|
219
|
+
</article>
|
|
220
|
+
<article class="wide-card">
|
|
221
|
+
<h3>时间线与阶段执行明细</h3>
|
|
222
|
+
<table><thead><tr><th>阶段</th><th>轮次</th><th>耗时</th><th>结果</th></tr></thead><tbody>${stageRows || '<tr><td colspan="4">暂无阶段明细</td></tr>'}</tbody></table>
|
|
223
|
+
</article>
|
|
224
|
+
</div>
|
|
225
|
+
<p class="callout">标准阶段固定为:提案、规格、设计、任务拆解、检查、实现、归档。测试和探索属于辅助阶段,不计入效率分母。</p>
|
|
226
|
+
</section>
|
|
227
|
+
|
|
228
|
+
<section class="panel" role="tabpanel" id="panel-evidence" aria-labelledby="tab-evidence" hidden>
|
|
229
|
+
<div class="section-head"><div><h2>问题与证据</h2><p>按问题类别聚合;展开后再查看任务和事件,不把规则命中误称为独立缺陷。</p></div></div>
|
|
230
|
+
<div class="issue-summary"><strong>${escapeHtml(evidence.category_count ?? 0)} 类问题</strong><p>${escapeHtml(evidence.occurrence_count ?? 0)} 次规则命中</p></div>
|
|
231
|
+
<article class="wide-card">${evidenceRows || '<p class="micro">当前没有证据问题。</p>'}</article>
|
|
232
|
+
<article class="wide-card section">
|
|
233
|
+
<h3>已知风险</h3>
|
|
234
|
+
<p>严重问题:${escapeHtml(view.known_risks?._available ? (view.known_risks.severe_issues?.length ?? 0) : '无数据')}</p>
|
|
235
|
+
${warningDetails.length > 0
|
|
236
|
+
? warningDetails.map((item) => `<p>${escapeHtml(item.description || '待确认项')}${item.target ? `(${escapeHtml(item.target)})` : ''}</p>`).join('')
|
|
237
|
+
: '<p class="micro">无需修复</p>'}
|
|
238
|
+
</article>
|
|
239
|
+
${Array.isArray(view.process_notes?.notes) && view.process_notes.notes.length > 0 ? `
|
|
240
|
+
<article class="wide-card section">
|
|
241
|
+
<h3>过程记录事件</h3>
|
|
242
|
+
<p>过程事件总数:${escapeHtml(view.process_notes.total ?? 0)}</p>
|
|
243
|
+
${view.process_notes.notes.slice(0, 40).map((note) => `<p>[${escapeHtml(note.stage || 'unknown')}] ${escapeHtml(note.kind || 'note')}${note.round ? ` R${escapeHtml(note.round)}` : ''}:${escapeHtml(note.summary || '')}</p>`).join('')}
|
|
244
|
+
</article>` : ''}
|
|
245
|
+
<p class="callout">${escapeHtml(evidence.note || '同一任务可能命中多条规则,命中次数不等于独立缺陷数。')}</p>
|
|
246
|
+
</section>
|
|
247
|
+
|
|
248
|
+
<section class="panel" role="tabpanel" id="panel-artifacts" aria-labelledby="tab-artifacts" hidden>
|
|
249
|
+
<div class="section-head"><div><h2>产物与追溯</h2><p>文件口径、变更归因和实际落盘产物均独立展示。</p></div></div>
|
|
250
|
+
<div class="two-col">
|
|
251
|
+
<article class="wide-card">
|
|
252
|
+
<h3>变更文件 · 双口径</h3>
|
|
253
|
+
<table><tbody>
|
|
254
|
+
<tr><td>任务事件累计文件</td><td><strong>${escapeHtml(changed.task_event_files_changed ?? '暂无数据')}</strong></td></tr>
|
|
255
|
+
<tr><td>最终输出快照文件</td><td><strong>${escapeHtml(changed.final_output_files_changed ?? '暂无数据')}</strong></td></tr>
|
|
256
|
+
<tr><td>新增 / 删除行</td><td>${escapeHtml(changed.added_lines ?? '暂无数据')} / ${escapeHtml(changed.deleted_lines ?? '暂无数据')}</td></tr>
|
|
257
|
+
<tr><td>数据来源</td><td>${escapeHtml(changed.diff_source || '未知')}</td></tr>
|
|
258
|
+
</tbody></table>
|
|
259
|
+
<p class="micro">${escapeHtml(changed.reconciliation_note || '暂无口径说明。')}</p>
|
|
260
|
+
</article>
|
|
261
|
+
<article class="wide-card">
|
|
262
|
+
<h3>归因审计</h3>
|
|
263
|
+
<table><tbody>
|
|
264
|
+
<tr><td>显式关联历史事件</td><td>${escapeHtml(artifactSection.attribution_summary.linked_source_event_count)}</td></tr>
|
|
265
|
+
<tr><td>有效关联</td><td>${escapeHtml(artifactSection.attribution_summary.active_link_count)}</td></tr>
|
|
266
|
+
<tr><td>未归属候选</td><td>${escapeHtml(artifactSection.attribution_summary.unassigned_candidate_count)}</td></tr>
|
|
267
|
+
</tbody></table>
|
|
268
|
+
<button class="text-button drawer-trigger" type="button" data-drawer-title="归因审计详情" data-drawer-body="${escapeHtml(`有效关联:${JSON.stringify(activeLinks)}\n未归属候选:${candidateIds.join('、') || '无'}`)}">查看归因事件</button>
|
|
269
|
+
</article>
|
|
270
|
+
</div>
|
|
271
|
+
<article class="wide-card section">
|
|
272
|
+
<h3>产物清单 · 实际产物 · 归档结果</h3>
|
|
273
|
+
<table><thead><tr><th>产物</th><th>状态</th><th>路径</th><th>大小</th></tr></thead><tbody>${artifactRows}</tbody></table>
|
|
274
|
+
</article>
|
|
275
|
+
<article class="wide-card section"><h3>产物备注</h3><p class="micro">归档目录:${escapeHtml(report.archive_result?.archive_path || report.artifacts?.archive_path || '暂不可用')}</p><p class="micro">已知风险明细仅在“问题与证据”页展示,本页不重复列表。</p></article>
|
|
276
|
+
</section>
|
|
277
|
+
</main>
|
|
278
|
+
|
|
279
|
+
<div class="drawer-backdrop" id="drawerBackdrop" aria-hidden="true">
|
|
280
|
+
<aside class="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" aria-describedby="drawerBody" tabindex="-1">
|
|
281
|
+
<div class="drawer-head"><div><div class="eyebrow">报告详情</div><h2 id="drawerTitle">详情</h2></div><button class="icon-button" id="drawerClose" type="button" aria-label="关闭详情">×</button></div>
|
|
282
|
+
<pre id="drawerBody"></pre>
|
|
283
|
+
</aside>
|
|
284
|
+
</div>
|
|
285
|
+
<noscript><p class="callout">当前浏览器未启用脚本;请打印本报告或查看 Markdown / JSON 产物。查看证据时可使用“问题与证据”章节。</p></noscript>
|
|
286
|
+
<script>
|
|
287
|
+
(function(){
|
|
288
|
+
var tabs=Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
|
|
289
|
+
var panels=Array.prototype.slice.call(document.querySelectorAll('[role="tabpanel"]'));
|
|
290
|
+
function activate(tab,focus){
|
|
291
|
+
tabs.forEach(function(item){
|
|
292
|
+
var selected=item===tab;
|
|
293
|
+
item.setAttribute('aria-selected',selected?'true':'false');
|
|
294
|
+
item.tabIndex=selected?0:-1;
|
|
295
|
+
document.getElementById(item.getAttribute('aria-controls')).hidden=!selected;
|
|
296
|
+
});
|
|
297
|
+
if(focus)tab.focus();
|
|
298
|
+
}
|
|
299
|
+
tabs.forEach(function(tab,index){
|
|
300
|
+
tab.addEventListener('click',function(){activate(tab,false);});
|
|
301
|
+
tab.addEventListener('keydown',function(event){
|
|
302
|
+
var next=index;
|
|
303
|
+
if(event.key === 'ArrowRight')next=(index+1)%tabs.length;
|
|
304
|
+
else if(event.key==='ArrowLeft')next=(index-1+tabs.length)%tabs.length;
|
|
305
|
+
else if(event.key==='Home')next=0;
|
|
306
|
+
else if(event.key==='End')next=tabs.length-1;
|
|
307
|
+
else return;
|
|
308
|
+
event.preventDefault();activate(tabs[next],true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
var backdrop=document.getElementById('drawerBackdrop');
|
|
312
|
+
var drawer=backdrop.querySelector('.drawer');
|
|
313
|
+
var drawerTitle=document.getElementById('drawerTitle');
|
|
314
|
+
var drawerBody=document.getElementById('drawerBody');
|
|
315
|
+
var lastTrigger=null;
|
|
316
|
+
function closeDrawer(){
|
|
317
|
+
if(!backdrop.classList.contains('open'))return;
|
|
318
|
+
backdrop.classList.remove('open');backdrop.setAttribute('aria-hidden','true');
|
|
319
|
+
if(lastTrigger)lastTrigger.focus();
|
|
320
|
+
}
|
|
321
|
+
document.querySelectorAll('.drawer-trigger,.info-btn').forEach(function(button){
|
|
322
|
+
button.addEventListener('click',function(){
|
|
323
|
+
lastTrigger=button;drawerTitle.textContent=button.dataset.drawerTitle||'详情';
|
|
324
|
+
drawerBody.textContent=button.dataset.drawerBody||'暂无详情';
|
|
325
|
+
backdrop.classList.add('open');backdrop.setAttribute('aria-hidden','false');drawer.focus();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
document.getElementById('drawerClose').addEventListener('click',closeDrawer);
|
|
329
|
+
backdrop.addEventListener('click',function(event){if(event.target===backdrop)closeDrawer();});
|
|
330
|
+
document.addEventListener('keydown',function(event){
|
|
331
|
+
if(event.key === 'Escape')closeDrawer();
|
|
332
|
+
var e=event;
|
|
333
|
+
if(e.key==='Tab'&&backdrop.classList.contains('open')){
|
|
334
|
+
var items=drawer.querySelectorAll('button,[href],[tabindex]:not([tabindex="-1"])');
|
|
335
|
+
var first=items[0],last=items[items.length-1];
|
|
336
|
+
if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
|
|
337
|
+
else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
})();
|
|
341
|
+
</script>
|
|
342
|
+
</body>
|
|
343
|
+
</html>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
escapeHtml,
|
|
348
|
+
renderChangeReportHtml,
|
|
349
|
+
};
|