kld-sdd 2.6.16 → 2.6.21

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 (70) hide show
  1. package/bin/kld-sdd-init.js +10 -0
  2. package/kld-sdd-guide.html +22 -0
  3. package/lib/device-auth-cli.js +130 -0
  4. package/lib/device-auth.js +345 -0
  5. package/lib/init-account-binding.js +108 -0
  6. package/lib/init.js +238 -55
  7. package/lib/skills-bundle.js +20 -1
  8. package/lib/tool-profiles.js +8 -0
  9. package/package.json +7 -3
  10. package/skywalk-sdd/apply-worktree-finish.cjs +2 -23
  11. package/skywalk-sdd/context-client.cjs +38 -87
  12. package/skywalk-sdd/index.cjs +860 -132
  13. package/skywalk-sdd/kb-sync-identity.cjs +780 -0
  14. package/skywalk-sdd/kb-upload.cjs +505 -0
  15. package/skywalk-sdd/lib/shared.cjs +811 -0
  16. package/skywalk-sdd/lib/usage-contract.cjs +276 -0
  17. package/skywalk-sdd/lib/usage-reporter.cjs +354 -0
  18. package/skywalk-sdd/lib/user-config.cjs +157 -0
  19. package/skywalk-sdd/metrics-v3.cjs +138 -8
  20. package/skywalk-sdd/ontology/archive-package.cjs +19 -34
  21. package/skywalk-sdd/ontology/change-lock.cjs +3 -7
  22. package/skywalk-sdd/ontology/external-key.cjs +18 -4
  23. package/skywalk-sdd/ontology/id.cjs +26 -5
  24. package/skywalk-sdd/ontology/identity-index.cjs +3 -7
  25. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -6
  26. package/skywalk-sdd/ontology/runtime.cjs +16 -12
  27. package/skywalk-sdd/ontology/traceability-validator.cjs +7 -4
  28. package/skywalk-sdd/reporting/change-report-markdown.cjs +137 -19
  29. package/skywalk-sdd/reporting/change-report-model.cjs +993 -14
  30. package/skywalk-sdd/reporting/change-report-renderer.cjs +106 -41
  31. package/skywalk-sdd/reporting/change-report-view-model.cjs +272 -43
  32. package/skywalk-sdd/reporting/core-metric-definitions.cjs +192 -0
  33. package/skywalk-sdd/spec-root.cjs +31 -0
  34. package/templates/git-hooks/pre-commit-consistency-check.cjs +271 -116
  35. package/templates/git-hooks/pre-push-consistency-check.cjs +252 -123
  36. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +327 -0
  37. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +54 -9
  38. package/templates/hooks/codebuddy/hooks/sdd-mid-checkpoint.cjs +63 -6
  39. package/templates/hooks/codebuddy/hooks/sdd-tdd-rhythm-gate.cjs +113 -65
  40. package/templates/openspec/proposal.md +7 -3
  41. package/templates/openspec/spec.md +3 -3
  42. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +8 -6
  43. package/templates/skills/kld-sdd/opsx-apply/checklist.md +2 -0
  44. package/templates/skills/kld-sdd/opsx-apply/reference.md +29 -7
  45. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +6 -5
  46. package/templates/skills/kld-sdd/opsx-check/SKILL.md +49 -17
  47. package/templates/skills/kld-sdd/opsx-check/checklist.md +4 -2
  48. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +187 -257
  49. package/templates/skills/kld-sdd/opsx-consistency-check/reference.md +129 -0
  50. package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -2
  51. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +2 -2
  52. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +185 -0
  53. package/templates/skills/kld-sdd/opsx-kb-config/reference.md +127 -0
  54. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +218 -53
  55. package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +51 -9
  56. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +12 -50
  57. package/templates/skills/kld-sdd/opsx-ontology-query/phase-3-postchange.md +2 -2
  58. package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +1 -1
  59. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +35 -23
  60. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  61. package/templates/skills/kld-sdd/opsx-propose/reference.md +22 -17
  62. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +2 -2
  63. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +19 -15
  64. package/templates/skills/kld-sdd/opsx-spec/checklist.md +2 -0
  65. package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -4
  66. package/templates/skills/kld-sdd/opsx-test/SKILL.md +2 -2
  67. package/templates/skills/kld-sdd/tdd-core/reference.md +1 -1
  68. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +1 -1
  69. package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +0 -7
  70. package/templates/skills/kld-sdd/opsx-ontology-query/state.example.json +0 -7
@@ -14,24 +14,62 @@ function escapeHtml(value) {
14
14
  }
15
15
 
16
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';
17
+ if (['trusted', 'complete', 'passed', 'verified', 'implemented', 'success', '可信', '已独立验证', '可计算'].includes(status)) return 'good';
18
+ if (['attention', 'incomplete', 'provisional', 'inconsistent', 'needs_input', 'open', '需补证', '尚未独立验证', '计算不一致'].includes(status)) return 'warn';
19
+ if (['failed', 'failure', 'blocked', 'error'].includes(status)) return 'bad';
20
20
  return 'neutral';
21
21
  }
22
22
 
23
- function metricCard({ name, code, value, state, explanation, source }) {
23
+ function metricDrawerBody(detail, fallback) {
24
+ if (!detail) return fallback;
25
+ const inputs = (detail.raw_inputs || []).map((item) => {
26
+ const value = item.display_value ?? item.value ?? '暂无数据';
27
+ const exclusion = item.included === false
28
+ ? `;不计入公式:${item.exclusion_reason || '仅作辅助说明'}`
29
+ : '';
30
+ return `- ${item.label}:${value}\n 来源:${item.source || '未记录'}${exclusion}`;
31
+ });
32
+ const evidence = (detail.evidence || []).map(item => (
33
+ [
34
+ `- ${item.path || item.event_id || item.type || '证据'}${item.count == null ? '' : `(${item.count} 条)`}`,
35
+ ...(item.scenarios || []).map(scenario => ` - 场景:${scenario.title || '未命名'}(第 ${scenario.line ?? '未知'} 行)`),
36
+ ].join('\n')
37
+ ));
38
+ return [
39
+ `指标定义\n${detail.definition}`,
40
+ `指标用途\n${detail.purpose}`,
41
+ `计算公式\n${detail.formula}`,
42
+ `数据来源\n${detail.source_description}`,
43
+ `对应方案\n${detail.scheme_reference}`,
44
+ `本次原始数据\n${inputs.join('\n') || '- 暂无可展示的原始数据'}`,
45
+ `计算步骤\n${(detail.calculation_steps || []).map(step => `- ${step}`).join('\n') || '- 暂无可复算步骤'}`,
46
+ `本次结果\n${detail.result?.display_value || '暂无数据'}\n可信度:${detail.trust?.explanation || '未记录'}`,
47
+ `证据与限制\n${[...evidence, ...(detail.caveats || []).map(item => `- ${item}`)].join('\n') || '- 暂无可定位证据'}`,
48
+ ].join('\n\n');
49
+ }
50
+
51
+ function metricCard({
52
+ name,
53
+ code,
54
+ value,
55
+ state,
56
+ state_code: stateCode,
57
+ explanation,
58
+ source,
59
+ detail,
60
+ }) {
61
+ const drawerBody = metricDrawerBody(detail, source || explanation);
24
62
  return `<article class="metric-card">
25
63
  <div class="metric-top"><span>${escapeHtml(name)}</span><span class="metric-code">${escapeHtml(code)}</span></div>
26
64
  <strong>${escapeHtml(value)}</strong>
27
- <div class="metric-state ${statusTone(state)}">${escapeHtml(state || '暂定')}</div>
65
+ <div class="metric-state ${statusTone(stateCode || state)}">${escapeHtml(state || '暂定')}</div>
28
66
  <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>
67
+ <button class="info-btn" type="button" data-info="${escapeHtml(source || explanation)}" data-drawer-title="${escapeHtml(`${detail?.name || name} · ${code}`)}" data-drawer-body="${escapeHtml(drawerBody)}">查看计算口径与证据</button>
30
68
  </article>`;
31
69
  }
32
70
 
33
71
  function renderChangeReportHtml(report) {
34
- if (!report.change_summary || !report.artifacts?.files) {
72
+ if (!report.change_summary || !report.artifacts?.files || !report.metric_details) {
35
73
  report = buildChangeReportModel(report);
36
74
  }
37
75
  const view = buildChangeReportViewModel(report);
@@ -50,12 +88,23 @@ function renderChangeReportHtml(report) {
50
88
  const conclusionTone = statusTone(view.conclusion?.status);
51
89
  const evidenceCategories = evidence.categories;
52
90
  const stageNodes = metricsSection.stages || [];
53
- const activeLinks = artifactSection.attribution?.active_links || [];
54
- const candidateIds = artifactSection.attribution?.unassigned_candidate_event_ids || [];
55
91
  const knownWarnings = view.known_risks?.warnings || [];
56
- const warningDetails = knownWarnings.flatMap((warning) => (
57
- Array.isArray(warning.items) ? warning.items : []
58
- ));
92
+ const riskRows = knownWarnings.flatMap((warning) => {
93
+ const items = Array.isArray(warning.items) && warning.items.length > 0
94
+ ? warning.items
95
+ : [{ description: `${warning.command || '检查'} 阶段记录了 ${warning.warnings || 0} 个提醒` }];
96
+ return items.map(item => ({
97
+ ...item,
98
+ status_label: ({
99
+ fixed: '已解决',
100
+ accepted: '已接受,保留观察',
101
+ waived: '已豁免,不阻断',
102
+ ignored: '不重要,已标记',
103
+ needs_input: '等待补充信息',
104
+ open: '待处理',
105
+ }[item.disposition] || (warning.resolved === true ? '已解决' : '待确认')),
106
+ }));
107
+ });
59
108
  const telemetryWarnings = view.telemetry_warnings?.by_type || {};
60
109
  const artifactRows = artifacts.map((item) => `
61
110
  <tr>
@@ -74,18 +123,20 @@ function renderChangeReportHtml(report) {
74
123
  : (item?.size_bytes == null ? '—' : escapeHtml(item.size_bytes))}</td>
75
124
  </tr>`).join('');
76
125
  const evidenceRows = evidenceCategories.map((group) => {
77
- const issueDetails = (group.issues || []).map((issue) => [
126
+ const issueDetails = (group.issues || []).map((issue) => {
127
+ const displayIssue = issueSection.issues.find(item => item.issue_id === issue.issue_id) || issue;
128
+ return [
78
129
  `问题:${issue.issue_id}`,
79
130
  `对象:${issue.subject || '本次变更'}`,
80
- `状态:${issue.status}`,
81
- `判定:${issue.verdict}`,
131
+ `状态:${displayIssue.status_label || issue.status}`,
82
132
  `根因:${issue.root_cause}`,
83
133
  `预期:${issue.expected}`,
84
134
  `实际:${issue.actual}`,
85
135
  `影响:${issue.impact}`,
86
136
  `修复建议:${issue.remediation}`,
87
137
  `证据事件:${(issue.evidence_event_ids || []).join('、') || '暂无可定位事件'}`,
88
- ].join('\n')).join('\n\n');
138
+ ].join('\n');
139
+ }).join('\n\n');
89
140
  return `
90
141
  <article class="issue-row">
91
142
  <div><span class="issue-count">${escapeHtml(group.occurrence_count)}</span></div>
@@ -95,7 +146,7 @@ function renderChangeReportHtml(report) {
95
146
  </div>
96
147
  <button class="text-button drawer-trigger" type="button"
97
148
  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>
149
+ data-drawer-body="${escapeHtml(`${issueDetails || `规则编码:${group.code}\n状态:待处理\n根因:当前证据未满足规则\n预期:规则证据完整\n实际:证据不足\n修复建议:补充真实证据`}\n\n规则编码:${group.code}\n关联任务:${(group.affected_tasks || []).join('、') || '无'}\n事件:${(group.event_ids || []).join('、') || '无'}`)}">查看详情</button>
99
150
  </article>`;
100
151
  }).join('');
101
152
  const stageRows = stageNodes.map((node) => {
@@ -106,7 +157,7 @@ function renderChangeReportHtml(report) {
106
157
  <td>${escapeHtml(stage)}${round > 1 ? '²' : ''}${round > 1 ? '<span class="tl-repeat">R</span>' : ''}</td>
107
158
  <td>${escapeHtml(round)}</td>
108
159
  <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>
160
+ <td><span class="state ${statusTone(node.result)}">${escapeHtml(node.display_result)}</span></td>
110
161
  </tr>`;
111
162
  }).join('');
112
163
 
@@ -164,9 +215,11 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
164
215
  .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
216
  .callout{border-left:4px solid var(--blue);background:var(--blue-soft);padding:12px 14px;border-radius:0 10px 10px 0;color:#344054}
166
217
  .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}
218
+ .drawer{position:absolute;right:0;top:0;height:100%;width:min(720px,92vw);background:#fff;padding:26px;box-shadow:-20px 0 70px rgba(15,23,42,.22);overflow:auto}
168
219
  .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
220
  .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}
221
+ @media screen and (max-width:1040px){body{min-width:0}.shell{width:100%;margin:0}.report-head{border-width:0 0 1px;border-radius:0}.identity{align-items:flex-start;flex-direction:column;padding:16px;gap:8px}.freshness{text-align:left}.tabs{overflow-x:auto;padding:0 8px}.tabs button{flex:0 0 auto;padding-left:12px;padding-right:12px}.panel{border-width:0;padding:12px;min-height:0;border-radius:0}.hero,.two-col{grid-template-columns:minmax(0,1fr)}.summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.metric-grid{grid-template-columns:minmax(0,1fr)}.section,.card,.wide-card{min-width:0}.wide-card{overflow-x:auto}.wide-card table{min-width:640px}.issue-row{grid-template-columns:44px minmax(0,1fr)}.issue-row .text-button{grid-column:2}.section-head{align-items:flex-start}.drawer{width:100%;max-width:100vw;padding:18px}}
222
+ @media screen and (max-width:520px){.summary-grid{grid-template-columns:minmax(0,1fr)}}
170
223
  @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
224
  </style>
172
225
  </head>
@@ -196,7 +249,7 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
196
249
  <h2>一句话结论</h2>
197
250
  <p>${escapeHtml(view.conclusion?.text || '当前证据不足,暂不能形成可信结论。')}</p>
198
251
  </article>
199
- <article class="card scope"><span>七个标准阶段总耗时</span><strong>${escapeHtml(overviewSection.scope_duration)}</strong><span>辅助阶段不进入效率分母</span></article>
252
+ <article class="card scope"><span>标准阶段有效耗时(含返工)</span><strong>${escapeHtml(overviewSection.scope_duration)}</strong><span>辅助阶段和阶段间空隙不进入 E1 分子</span></article>
200
253
  </div>
201
254
  <div class="summary-grid">${summaryCards}</div>
202
255
  <div class="section">
@@ -227,13 +280,24 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
227
280
 
228
281
  <section class="panel" role="tabpanel" id="panel-evidence" aria-labelledby="tab-evidence" hidden>
229
282
  <div class="section-head"><div><h2>问题与证据</h2><p>按问题类别聚合;展开后再查看任务和事件,不把规则命中误称为独立缺陷。</p></div></div>
283
+ <article class="wide-card">
284
+ <h3>这些内容是怎么收集的</h3>
285
+ <p>${escapeHtml(issueSection.collection_method)}</p>
286
+ <p><strong>功能验证:</strong>${escapeHtml(issueSection.functional_result?.summary)} ${escapeHtml(issueSection.functional_result?.explanation)}</p>
287
+ <p><strong>证据链判断:</strong>${escapeHtml(issueSection.traceability_result?.summary)} ${escapeHtml(issueSection.traceability_result?.explanation)}</p>
288
+ </article>
230
289
  <div class="issue-summary"><strong>${escapeHtml(evidence.category_count ?? 0)} 类问题</strong><p>${escapeHtml(evidence.occurrence_count ?? 0)} 次规则命中</p></div>
231
290
  <article class="wide-card">${evidenceRows || '<p class="micro">当前没有证据问题。</p>'}</article>
291
+ ${(issueSection.warning_dispositions || []).length > 0 ? `
292
+ <article class="wide-card section">
293
+ <h3>告警处置</h3>
294
+ ${issueSection.warning_dispositions.map(item => `<p>${escapeHtml(item.warning || '未标识告警')}:${escapeHtml(item.disposition_label || '待处理')}${item.reason ? `;${escapeHtml(item.reason)}` : ''}</p>`).join('')}
295
+ </article>` : ''}
232
296
  <article class="wide-card section">
233
297
  <h3>已知风险</h3>
234
298
  <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('')
299
+ ${riskRows.length > 0
300
+ ? riskRows.map((item) => `<p><span class="state ${item.status_label === '待处理' || item.status_label === '待确认' ? 'warn' : 'neutral'}">${escapeHtml(item.status_label)}</span> ${escapeHtml(item.description || '待确认项')}${item.target ? `(${escapeHtml(item.target)})` : ''}</p>`).join('')
237
301
  : '<p class="micro">无需修复</p>'}
238
302
  </article>
239
303
  ${Array.isArray(view.process_notes?.notes) && view.process_notes.notes.length > 0 ? `
@@ -246,26 +310,25 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
246
310
  </section>
247
311
 
248
312
  <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>
313
+ <div class="section-head"><div><h2>产物与追溯</h2><p>分别说明任务过程中涉及的文件和最终实际交付的文件。</p></div></div>
250
314
  <div class="two-col">
251
315
  <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>
316
+ <h3>${escapeHtml(changed.task_scope?.label || '任务过程中涉及的文件')}</h3>
317
+ <p>${escapeHtml(changed.task_scope?.explanation || '')}</p>
318
+ <p><strong>${escapeHtml(changed.task_scope?.count ?? '暂无数据')} 个</strong></p>
319
+ <p class="path">${escapeHtml((changed.task_scope?.files || []).join('') || '暂无文件清单')}</p>
320
+ <p class="micro">来源:${escapeHtml(changed.task_scope?.source || '未记录')};任务:${escapeHtml((changed.task_scope?.task_ids || []).join('、') || '未记录')};最新记录:${escapeHtml(changed.task_scope?.latest_at || '未记录')}</p>
321
+ ${(changed.task_scope?.no_file_change_reasons || []).map(item => `<p class="micro">未改文件说明:${escapeHtml(item.task_id || '未标识任务')}:${escapeHtml(item.reason || item)}</p>`).join('')}
260
322
  </article>
261
323
  <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>
324
+ <h3>${escapeHtml(changed.final_scope?.label || '最终实际交付的文件')}</h3>
325
+ <p>${escapeHtml(changed.final_scope?.explanation || '')}</p>
326
+ <p><strong>${escapeHtml(changed.final_scope?.count ?? '暂无数据')} 个</strong></p>
327
+ <p class="path">${escapeHtml((changed.final_scope?.files || []).join('、') || '暂无文件清单')}</p>
328
+ <p class="micro">来源:${escapeHtml(changed.final_scope?.source || '未记录')};事件:${escapeHtml(changed.final_scope?.event_id || '未记录')};快照时间:${escapeHtml(changed.final_scope?.captured_at || '未记录')}</p>
329
+ <p class="micro">未产生交付文件的原因:${escapeHtml(changed.final_scope?.no_output_reason || '不适用')}</p>
330
+ <p class="micro">行数统计说明:${escapeHtml(changed.final_scope?.line_statistics_explanation || '未记录')}</p>
331
+ <p class="micro">${escapeHtml(changed.reconciliation_note || '暂无口径说明。')}</p>
269
332
  </article>
270
333
  </div>
271
334
  <article class="wide-card section">
@@ -312,6 +375,7 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
312
375
  var drawer=backdrop.querySelector('.drawer');
313
376
  var drawerTitle=document.getElementById('drawerTitle');
314
377
  var drawerBody=document.getElementById('drawerBody');
378
+ var drawerClose=document.getElementById('drawerClose');
315
379
  var lastTrigger=null;
316
380
  function closeDrawer(){
317
381
  if(!backdrop.classList.contains('open'))return;
@@ -322,10 +386,10 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
322
386
  button.addEventListener('click',function(){
323
387
  lastTrigger=button;drawerTitle.textContent=button.dataset.drawerTitle||'详情';
324
388
  drawerBody.textContent=button.dataset.drawerBody||'暂无详情';
325
- backdrop.classList.add('open');backdrop.setAttribute('aria-hidden','false');drawer.focus();
389
+ backdrop.classList.add('open');backdrop.setAttribute('aria-hidden','false');drawerClose.focus();
326
390
  });
327
391
  });
328
- document.getElementById('drawerClose').addEventListener('click',closeDrawer);
392
+ drawerClose.addEventListener('click',closeDrawer);
329
393
  backdrop.addEventListener('click',function(event){if(event.target===backdrop)closeDrawer();});
330
394
  document.addEventListener('keydown',function(event){
331
395
  if(event.key === 'Escape')closeDrawer();
@@ -333,7 +397,8 @@ table{width:100%;border-collapse:collapse}th,td{padding:11px 10px;border-bottom:
333
397
  if(e.key==='Tab'&&backdrop.classList.contains('open')){
334
398
  var items=drawer.querySelectorAll('button,[href],[tabindex]:not([tabindex="-1"])');
335
399
  var first=items[0],last=items[items.length-1];
336
- if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
400
+ if(!drawer.contains(document.activeElement)){event.preventDefault();(event.shiftKey?last:first).focus();}
401
+ else if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
337
402
  else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}
338
403
  }
339
404
  });