kld-sdd 2.6.12 → 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.
Files changed (91) hide show
  1. package/README.md +29 -7
  2. package/kld-sdd-guide.html +428 -159
  3. package/lib/init.js +37 -7
  4. package/package.json +5 -2
  5. package/skywalk-sdd/index.cjs +2668 -415
  6. package/skywalk-sdd/metrics-v3.cjs +1153 -0
  7. package/skywalk-sdd/ontology/archive-package.cjs +120 -3
  8. package/skywalk-sdd/ontology/identity-index.cjs +9 -2
  9. package/skywalk-sdd/ontology/ontology-paths.cjs +73 -0
  10. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -80
  11. package/skywalk-sdd/ontology/runtime.cjs +32 -22
  12. package/skywalk-sdd/ontology/structural-identity.cjs +11 -2
  13. package/skywalk-sdd/ontology/traceability-validator.cjs +16 -8
  14. package/skywalk-sdd/ontology/working-artifacts.cjs +4 -2
  15. package/skywalk-sdd/reporting/change-report-model.cjs +309 -0
  16. package/skywalk-sdd/reporting/change-report-renderer.cjs +425 -0
  17. package/skywalk-sdd/runtime-metadata.cjs +21 -0
  18. package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +39 -9
  19. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +87 -8
  20. package/templates/hooks/claude/hooks/sdd-prompt.cjs +3 -1
  21. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +39 -9
  22. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +87 -9
  23. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +3 -1
  24. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +14 -13
  25. package/templates/skills/kld-sdd/opsx-apply/checklist.md +20 -20
  26. package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +12 -12
  27. package/templates/skills/kld-sdd/opsx-apply/reference.md +32 -31
  28. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +44 -6
  29. package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -2
  30. package/templates/skills/kld-sdd/opsx-check/SKILL.md +21 -13
  31. package/templates/skills/kld-sdd/opsx-check/checklist.md +8 -7
  32. package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
  33. package/templates/skills/kld-sdd/opsx-design/checklist.md +1 -1
  34. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +14 -0
  35. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +31 -0
  36. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +12 -4
  37. package/templates/skills/kld-sdd/opsx-propose/reference.md +29 -3
  38. package/templates/skills/kld-sdd/opsx-rules/reference.md +3 -1
  39. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +2 -2
  40. package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -1
  41. package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -10
  42. package/templates/skills/kld-sdd/opsx-task/checklist.md +3 -3
  43. package/templates/skills/kld-sdd/opsx-task/reference.md +3 -3
  44. package/templates/skills/kld-sdd/opsx-test/SKILL.md +7 -5
  45. package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/SKILL.md +4 -4
  46. package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/reference.md +4 -4
  47. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/SKILL.md +13 -13
  48. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/checklist.md +5 -5
  49. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/reference.md +12 -12
  50. package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/SKILL.md +3 -3
  51. package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/checklist.md +2 -2
  52. package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/SKILL.md +2 -2
  53. package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/parameterized-testing.md +1 -1
  54. package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/SKILL.md +5 -5
  55. package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/checklist.md +5 -5
  56. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/SKILL.md +3 -3
  57. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/non-tdd-modules.md +1 -1
  58. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/refactor-checklist.md +1 -1
  59. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +19 -0
  60. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +0 -19
  61. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/cause-effect-clarity.md +0 -0
  62. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/clean-test-data.md +0 -0
  63. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/existing-test-awareness.md +0 -0
  64. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/given-when-then.md +0 -0
  65. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/good-test-qualities.md +0 -0
  66. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/mock-boundary.md +0 -0
  67. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/naming-conventions.md +0 -0
  68. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/no-logic-in-tests.md +0 -0
  69. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/one-test-one-scenario.md +0 -0
  70. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/prefer-public-apis.md +0 -0
  71. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/test-behaviors-not-methods.md +0 -0
  72. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/argument-matching.md +0 -0
  73. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/controller-test-rules.md +0 -0
  74. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/domain-service-rules.md +0 -0
  75. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/java-test-template.md +0 -0
  76. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/json-serialization.md +0 -0
  77. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/logging-rules.md +0 -0
  78. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/compilation-verification.md +0 -0
  79. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/execution-verification.md +0 -0
  80. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/python/py-test-template.md +0 -0
  81. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/typescript/ts-test-template.md +0 -0
  82. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/controller-strategy.md +0 -0
  83. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/dag-generation-rules.md +0 -0
  84. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/des-step-annotation.md +0 -0
  85. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/exception-path-coverage.md +0 -0
  86. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-scope-declaration.md +0 -0
  87. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-yagni-fence.md +0 -0
  88. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/multi-validation-split.md +0 -0
  89. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/task-type-definitions.md +0 -0
  90. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/tdd-strategy-selection.md +0 -0
  91. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/test-execution-gate.md +0 -0
@@ -0,0 +1,425 @@
1
+ 'use strict';
2
+
3
+ const { buildChangeReportModel } = require('./change-report-model.cjs');
4
+
5
+ function escapeHtml(value) {
6
+ return String(value ?? '').replace(/[&<>"']/g, (char) => ({
7
+ '&': '&amp;',
8
+ '<': '&lt;',
9
+ '>': '&gt;',
10
+ '"': '&quot;',
11
+ "'": '&#39;',
12
+ }[char]));
13
+ }
14
+
15
+ function percent(value, digits = 2) {
16
+ return Number.isFinite(value) ? `${(value * 100).toFixed(digits)}%` : '暂无数据';
17
+ }
18
+
19
+ function duration(value) {
20
+ if (!Number.isFinite(value)) return '暂无数据';
21
+ const totalSeconds = Math.round(value / 1000);
22
+ const hours = Math.floor(totalSeconds / 3600);
23
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
24
+ const seconds = totalSeconds % 60;
25
+ return [hours ? `${hours}小时` : '', minutes ? `${minutes}分` : '', `${seconds}秒`].filter(Boolean).join('');
26
+ }
27
+
28
+ function fraction(value, total) {
29
+ return value == null || total == null ? '暂无数据' : `${value} / ${total}`;
30
+ }
31
+
32
+ function durationMetricHtml(value) {
33
+ if (!Number.isFinite(value)) return '<span class="muted-null">null</span>';
34
+ return `${Math.round(value / 60000)}<span class="unit">分钟</span>`;
35
+ }
36
+
37
+ function statusTone(status) {
38
+ if (['trusted', 'complete', 'passed', 'verified', 'implemented'].includes(status)) return 'good';
39
+ if (['attention', 'incomplete', 'provisional', 'needs_input', 'open'].includes(status)) return 'warn';
40
+ if (['failed', 'blocked', 'error'].includes(status)) return 'bad';
41
+ return 'neutral';
42
+ }
43
+
44
+ function metricCard({ name, code, value, state, explanation, source }) {
45
+ return `<article class="metric-card">
46
+ <div class="metric-top"><span>${escapeHtml(name)}</span><span class="metric-code">${escapeHtml(code)}</span></div>
47
+ <strong>${escapeHtml(value)}</strong>
48
+ <div class="metric-state ${statusTone(state)}">${escapeHtml(state || '暂定')}</div>
49
+ <p>${escapeHtml(explanation)}</p>
50
+ <button class="info-btn" type="button" data-info="${escapeHtml(source || explanation)}" data-drawer-title="${escapeHtml(`${name} · ${code}`)}" data-drawer-body="${escapeHtml(source || explanation)}">查看计算口径与证据</button>
51
+ </article>`;
52
+ }
53
+
54
+ function renderChangeReportHtml(report) {
55
+ report = buildChangeReportModel(report);
56
+ const metrics = report.metrics || {};
57
+ const efficiency = metrics.efficiency || {};
58
+ const quality = metrics.quality || {};
59
+ const processMetrics = metrics.process || {};
60
+ const telemetry = metrics.telemetry_health || {};
61
+ const summary = report.change_summary || {};
62
+ const tests = summary.tests || {};
63
+ const tasks = summary.tasks || {};
64
+ const evidence = summary.evidence || { categories: [] };
65
+ const changed = report.changed_files || {};
66
+ const artifacts = report.artifacts?.files || {};
67
+ const minSet = report.minimum_metric_set || {};
68
+ const totalDuration = efficiency.total_duration_ms
69
+ ?? efficiency.effective_stage_duration_including_rework_ms
70
+ ?? efficiency.e1_lead_time_ms;
71
+ const e1 = efficiency.e1_scenario_delivery_efficiency_ms;
72
+ const e2 = efficiency.e2_coding_time_ratio_including_rework ?? efficiency.e2_coding_time_ratio;
73
+ const e3 = efficiency.e3_spec_time_ratio_including_rework ?? efficiency.e3_spec_time_ratio;
74
+ const conclusionTone = statusTone(report.conclusion?.status);
75
+ const evidenceCategories = Array.isArray(evidence.categories) ? evidence.categories : [];
76
+ const stageNodes = report.stage_timeline?._available && Array.isArray(report.stage_timeline.nodes)
77
+ ? report.stage_timeline.nodes
78
+ : [];
79
+ const activeLinks = report.attribution_audit?.active_links || [];
80
+ const candidateIds = report.attribution_audit?.unassigned_candidate_event_ids || [];
81
+ const knownWarnings = report.known_risks?.warnings || [];
82
+ const warningDetails = knownWarnings.flatMap((warning) => (
83
+ Array.isArray(warning.items) ? warning.items : []
84
+ ));
85
+ const telemetryWarnings = report.telemetry_warnings?.by_type || {};
86
+ const artifactRows = Object.entries(artifacts).map(([name, item]) => `
87
+ <tr>
88
+ <td>${escapeHtml({
89
+ markdown_report: 'Markdown 报告',
90
+ html_report: 'HTML 报告',
91
+ json_report: 'JSON 报告',
92
+ execution_log: '执行日志',
93
+ manifest: '归档清单',
94
+ archive_zip: '归档包',
95
+ }[name] || name)}</td>
96
+ <td><span class="state ${item?.exists ? 'good' : 'neutral'}">${item?.exists ? '已生成' : '未生成'}</span></td>
97
+ <td class="path">${escapeHtml(item?.path || '—')}</td>
98
+ <td>${item?.size_bytes == null ? '—' : escapeHtml(`${item.size_bytes} B`)}</td>
99
+ </tr>`).join('');
100
+ const evidenceRows = evidenceCategories.map((group) => `
101
+ <article class="issue-row">
102
+ <div><span class="issue-count">${escapeHtml(group.occurrence_count)}</span></div>
103
+ <div>
104
+ <h3>${escapeHtml(group.label)}</h3>
105
+ <p>${escapeHtml(group.affected_tasks?.length || 0)} 个关联任务 · ${escapeHtml(group.event_ids?.length || 0)} 个可定位事件</p>
106
+ </div>
107
+ <button class="text-button drawer-trigger" type="button"
108
+ data-drawer-title="${escapeHtml(group.label)}"
109
+ data-drawer-body="${escapeHtml(`规则编码:${group.code}\n关联任务:${(group.affected_tasks || []).join('、') || '无'}\n事件:${(group.event_ids || []).join('、') || '无'}\n处置:${JSON.stringify(group.dispositions || {})}`)}">查看详情</button>
110
+ </article>`).join('');
111
+ const stageRows = stageNodes.map((node) => {
112
+ const stage = node.stage || node.command || '未知阶段';
113
+ const round = node.round || 1;
114
+ const start = node.start_ts ? new Date(node.start_ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : '—';
115
+ const end = node.end_ts ? new Date(node.end_ts).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) : '—';
116
+ return `
117
+ <tr class="tl-node">
118
+ <td>${escapeHtml(stage)}${round > 1 ? '²' : ''}${round > 1 ? '<span class="tl-repeat">R</span>' : ''}</td>
119
+ <td>${escapeHtml(node.round || 1)}</td>
120
+ <td><span class="tl-time">${escapeHtml(start)}→${escapeHtml(end)}</span><br>${escapeHtml(duration(node.duration_ms))}</td>
121
+ <td><span class="state ${statusTone(node.result === 'success' ? 'passed' : node.result)}">${escapeHtml(node.result || '未知')}</span></td>
122
+ </tr>`;
123
+ }).join('');
124
+
125
+ const metricCards = [
126
+ {
127
+ name: '每个场景平均用时',
128
+ code: 'E1',
129
+ value: Number.isFinite(e1) ? `${duration(e1)} / 场景` : '暂无数据',
130
+ state: (
131
+ minSet.evidence_status?.e1 === 'trusted'
132
+ || minSet.trusted_metrics?.includes?.('e1')
133
+ || minSet.trusted?.includes?.('e1')
134
+ ) ? '可信' : '需补证',
135
+ explanation: '七个标准阶段总耗时(含返工)÷ 验收场景数。',
136
+ source: `公式:标准阶段总耗时 ÷ 场景数。场景数:${efficiency.scenario_count ?? '未知'};当前值:${e1 ?? '不可计算'} ms。`,
137
+ },
138
+ {
139
+ name: '规约检查得分',
140
+ code: 'Q3',
141
+ value: quality.q3_spec_quality_score == null ? '暂无数据' : `${quality.q3_spec_quality_score} / 100`,
142
+ state: quality.q3_score_status === 'verified' ? '已验证' : '临时',
143
+ explanation: '最新规约检查结果;独立复核后才标记为已验证。',
144
+ source: `评审独立性:${quality.q3_reviewer_independence || '未知'};证据状态:${quality.q3_score_status || '未知'}。`,
145
+ },
146
+ {
147
+ name: '编码前已完成检查',
148
+ code: 'P4',
149
+ value: percent(processMetrics.p4_quality_gate_enforcement_rate),
150
+ state: Number.isFinite(processMetrics.p4_quality_gate_enforcement_rate) ? '可计算' : '需补证',
151
+ explanation: '编码前是否先检查:首次进入编码前,是否存在已配对且成功或部分通过的检查。',
152
+ source: '只接受能与 check 开始事件配对、且发生在首次 apply 之前的成功或部分通过结果。',
153
+ },
154
+ {
155
+ name: '阶段重复次数',
156
+ code: 'P-R',
157
+ value: processMetrics.rework_summary?.total_rework_attempts == null
158
+ ? '暂无数据'
159
+ : `${processMetrics.rework_summary.total_rework_attempts} 次`,
160
+ state: processMetrics.rework_summary ? '可计算' : '需补证',
161
+ explanation: '七个标准阶段中,首次执行之后的重复尝试次数。',
162
+ source: '辅助 test / explore 阶段不计入标准阶段返工总数。',
163
+ },
164
+ {
165
+ name: '执行记录完整度',
166
+ code: 'P-H',
167
+ value: telemetry.p_h_telemetry_health_score == null
168
+ ? '暂无数据'
169
+ : `${telemetry.p_h_telemetry_health_score} / 100`,
170
+ state: telemetry.p_h_telemetry_health_score == null ? '需补证' : '可计算',
171
+ explanation: '执行记录是否完整:标准阶段的身份、开始结束配对和数据质量综合评分。',
172
+ source: '沿用既有权重,但只以 propose、spec、design、task、check、apply、archive 七阶段为计算范围。',
173
+ },
174
+ ].map(metricCard).join('');
175
+
176
+ return `<!doctype html>
177
+ <html lang="zh-CN" data-theme="light">
178
+ <head>
179
+ <meta charset="utf-8">
180
+ <meta name="viewport" content="width=device-width,initial-scale=1">
181
+ <title>SDD 效果度量报告 · ${escapeHtml(report.change)}</title>
182
+ <style>
183
+ :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)}
184
+ *{box-sizing:border-box}
185
+ [hidden]{display:none!important}
186
+ body{margin:0;min-width:1040px;background:var(--canvas);color:var(--ink);font:14px/1.55 "Segoe UI","Microsoft YaHei",sans-serif}
187
+ button{font:inherit}
188
+ .shell{width:min(1280px,calc(100% - 56px));margin:26px auto 46px}
189
+ .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)}
190
+ .identity{display:flex;align-items:center;justify-content:space-between;padding:22px 26px 16px;gap:24px}
191
+ .eyebrow{color:var(--blue);font-weight:750;font-size:12px;letter-spacing:.08em}
192
+ h1{font-size:26px;line-height:1.2;margin:3px 0 7px;letter-spacing:-.025em}
193
+ .meta,.micro{color:var(--muted);font-size:12px}.meta code{color:#35425a}
194
+ .freshness{text-align:right}.freshness strong{display:block;color:var(--good);font-size:13px}
195
+ .tabs{display:flex;padding:0 18px;border-top:1px solid #eef1f5}
196
+ .tabs button{border:0;border-bottom:3px solid transparent;background:none;color:#56647b;padding:14px 18px 12px;cursor:pointer;font-weight:650}
197
+ .tabs button:hover{color:var(--blue)}.tabs button:focus-visible{outline:3px solid #b2ccff;outline-offset:-3px}
198
+ .tabs button[aria-selected="true"]{color:var(--blue);border-color:var(--blue)}
199
+ .panel{background:#f8fafc;border:1px solid var(--line);border-top:0;border-radius:0 0 18px 18px;padding:22px;min-height:560px}
200
+ .hero{display:grid;grid-template-columns:1.55fr .45fr;gap:16px;margin-bottom:16px}
201
+ .card{background:var(--paper);border:1px solid var(--line);border-radius:14px;padding:18px}
202
+ .conclusion{background:linear-gradient(135deg,#f7faff,#eef5ff);border-color:#c8d9f5}
203
+ .conclusion h2,.section-head h2{margin:0}.conclusion p{font-size:18px;font-weight:680;color:#203e73;margin:10px 0 0}
204
+ .scope{display:flex;flex-direction:column;justify-content:center}.scope strong{font-size:19px}.scope span{color:var(--muted);margin-top:4px}
205
+ .summary-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
206
+ .summary-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px;min-height:112px}
207
+ .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}
208
+ .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}
209
+ .metric-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}
210
+ .metric-card{background:#fff;border:1px solid var(--line);border-radius:14px;padding:15px;min-height:210px;display:flex;flex-direction:column}
211
+ .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}
212
+ .metric-card>strong{font-size:21px;margin:15px 0 5px}.metric-card p{color:var(--muted);font-size:12px;margin:10px 0;flex:1}
213
+ .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}
214
+ .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}
215
+ .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}
216
+ .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)}
217
+ .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}
218
+ 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}
219
+ .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}
220
+ .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}
221
+ .callout{border-left:4px solid var(--blue);background:var(--blue-soft);padding:12px 14px;border-radius:0 10px 10px 0;color:#344054}
222
+ .drawer-backdrop{position:fixed;inset:0;z-index:40;background:rgba(15,23,42,.42);display:none}.drawer-backdrop.open{display:block}
223
+ .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}
224
+ .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}
225
+ .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}
226
+ @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}}
227
+ </style>
228
+ </head>
229
+ <body>
230
+ <main class="shell">
231
+ <header class="report-head">
232
+ <div class="identity">
233
+ <div>
234
+ <div class="eyebrow">SDD · 单次变更报告</div>
235
+ <h1>${escapeHtml(report.change)}</h1>
236
+ <div class="meta">仅展示本次变更 · Schema ${escapeHtml(report.schema_version)} · 指标契约 ${escapeHtml(report.metrics_contract)}</div>
237
+ </div>
238
+ <div class="freshness"><strong>数据范围已锁定</strong><span class="micro">生成于 ${escapeHtml(report.generated_at || '未知')}</span></div>
239
+ </div>
240
+ <nav class="tabs" role="tablist" aria-label="报告章节">
241
+ <button role="tab" id="tab-overview" aria-controls="panel-overview" aria-selected="true" tabindex="0">概览</button>
242
+ <button role="tab" id="tab-metrics" aria-controls="panel-metrics" aria-selected="false" tabindex="-1">指标与阶段</button>
243
+ <button role="tab" id="tab-evidence" aria-controls="panel-evidence" aria-selected="false" tabindex="-1">问题与证据</button>
244
+ <button role="tab" id="tab-artifacts" aria-controls="panel-artifacts" aria-selected="false" tabindex="-1">产物与追溯</button>
245
+ </nav>
246
+ </header>
247
+
248
+ <section class="panel" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
249
+ <div class="hero">
250
+ <article class="card conclusion">
251
+ <span class="state ${conclusionTone}">${escapeHtml(report.conclusion?.status === 'trusted' ? '结论可信' : '需要关注')}</span>
252
+ <h2>一句话结论</h2>
253
+ <p>${escapeHtml(report.conclusion?.text || '当前证据不足,暂不能形成可信结论。')}</p>
254
+ </article>
255
+ <article class="card scope"><span>七个标准阶段总耗时</span><strong>${escapeHtml(duration(totalDuration))}</strong><span>辅助阶段不进入效率分母</span></article>
256
+ </div>
257
+ <div class="summary-grid">
258
+ <article class="summary-card"><span>最终测试</span><strong>${escapeHtml(fraction(tests.passed, tests.total))}</strong><p>${tests.status === 'passed' ? '最终权威运行全部通过' : '暂无可验证的最终运行'}</p></article>
259
+ <article class="summary-card"><span>文档任务完成</span><strong>${escapeHtml(fraction(tasks.document?.completed, tasks.document?.total))}</strong><p>来自 tasks 文档状态</p></article>
260
+ <article class="summary-card"><span>事件可追溯</span><strong>${escapeHtml(fraction(tasks.traceability?.verified, tasks.traceability?.total))}</strong><p>来自严格 task/test 事件对账</p></article>
261
+ <article class="summary-card"><span>证据问题</span><strong>${escapeHtml(evidence.category_count ?? 0)} 类</strong><p>${escapeHtml(evidence.occurrence_count ?? 0)} 次规则命中,不等于独立缺陷数</p></article>
262
+ </div>
263
+ <div class="section">
264
+ <div class="section-head"><h2>核心指标</h2><p>中文业务名称为主,编码仅作定位标签</p></div>
265
+ <div class="metric-grid">${metricCards}</div>
266
+ </div>
267
+ <div class="section card">
268
+ <div class="section-head"><h2>需要处理</h2><p>证据问题已归并到“问题与证据”页签</p></div>
269
+ <p class="${evidence.category_count ? 'warn' : 'good'} state">${evidence.category_count ? `${escapeHtml(evidence.category_count)} 类问题待处置` : '当前无需处理'}</p>
270
+ ${telemetryWarnings.task_update_reuse > 0 ? '<div class="quality-alert">检测到 task_update 复用;相关可信度已降级。</div>' : ''}
271
+ </div>
272
+ </section>
273
+
274
+ <section class="panel" role="tabpanel" id="panel-metrics" aria-labelledby="tab-metrics" hidden>
275
+ <div class="section-head"><div><h2>指标与阶段</h2><p>本页只解释本次变更的计算口径和阶段执行。</p></div></div>
276
+ <div class="two-col">
277
+ <article class="wide-card">
278
+ <h3>效率拆分</h3>
279
+ <table><tbody>
280
+ <tr><td>七阶段总耗时</td><td><strong>${escapeHtml(duration(totalDuration))}</strong></td></tr>
281
+ <tr><td>每个场景平均用时 <span class="metric-code">E1</span></td><td>${escapeHtml(Number.isFinite(e1) ? `${duration(e1)} / 场景` : '暂无数据')}</td></tr>
282
+ <tr><td>编码时间占比 <span class="metric-code">E2</span></td><td>${escapeHtml(percent(e2))}</td></tr>
283
+ <tr><td>规划与规约时间占比 <span class="metric-code">E3</span></td><td>${escapeHtml(percent(e3))}</td></tr>
284
+ </tbody></table>
285
+ </article>
286
+ <article class="wide-card">
287
+ <h3>时间线与阶段执行明细</h3>
288
+ <table><thead><tr><th>阶段</th><th>轮次</th><th>耗时</th><th>结果</th></tr></thead><tbody>${stageRows || '<tr><td colspan="4">暂无阶段明细</td></tr>'}</tbody></table>
289
+ </article>
290
+ </div>
291
+ <p class="callout">标准阶段固定为:提案、规格、设计、任务拆解、检查、实现、归档。测试和探索属于辅助阶段,不计入效率分母。</p>
292
+ <details class="legacy-appendix">
293
+ <summary>历史兼容数据(默认折叠,仅供旧版消费者对照)</summary>
294
+ <div class="two-col wide-card section">
295
+ <div>
296
+ <h3>Legacy 效率指标</h3>
297
+ <p>一次成码率:${escapeHtml(percent(efficiency.e4_ai_code_first_pass_rate))}</p>
298
+ <h3>Legacy 质量指标</h3>
299
+ <p>规约符合度:${escapeHtml(quality.q1_spec_conformance_score ?? '暂无数据')}</p>
300
+ <h3>Legacy 过程指标</h3>
301
+ <p>阶段重复执行:${escapeHtml(processMetrics.rework_summary?.total_rework_attempts ?? '暂无数据')}</p>
302
+ <p>阶段间等待:${durationMetricHtml(efficiency.idle_time_ms)}</p>
303
+ <p>Q1 规约符合度:${escapeHtml(quality.q1_spec_conformance_score ?? '暂无数据')}${quality.q1_human_status === 'unverified' ? '(自评·待人工确认)' : ''}</p>
304
+ <p>E4 一次成码率:${escapeHtml(percent(efficiency.e4_ai_code_first_pass_rate))}(基于 task_update 信号判定,非代码 diff)${telemetryWarnings.task_update_reuse > 0 ? ';受 task_update 复用影响,已扣分' : ''}</p>
305
+ <p>过程记录事件:${escapeHtml(report.process_notes?.total ?? 0)} 条</p>
306
+ ${changed._available ? '' : '<p><span class="muted-null">null</span>:旧版文件统计不可用</p>'}
307
+ </div>
308
+ <div>
309
+ <h3>重复执行原因</h3>
310
+ <p>${escapeHtml((processMetrics.rework_summary?.reasons?.details || []).map((item) => item.reason === 'pre-archive-recheck' ? '归档前强制复检' : item.reason).join('、') || '无')}</p>
311
+ </div>
312
+ </div>
313
+ </details>
314
+ </section>
315
+
316
+ <section class="panel" role="tabpanel" id="panel-evidence" aria-labelledby="tab-evidence" hidden>
317
+ <div class="section-head"><div><h2>问题与证据</h2><p>按问题类别聚合;展开后再查看任务和事件,不把规则命中误称为独立缺陷。</p></div></div>
318
+ <div class="issue-summary"><strong>${escapeHtml(evidence.category_count ?? 0)} 类问题</strong><p>${escapeHtml(evidence.occurrence_count ?? 0)} 次规则命中</p></div>
319
+ <article class="wide-card">${evidenceRows || '<p class="micro">当前没有证据问题。</p>'}</article>
320
+ ${warningDetails.length > 0 ? `<article class="wide-card section"><h3>已知风险明细</h3>${warningDetails.map((item) => `<p>${escapeHtml(item.description || '待确认项')}${item.target ? `(${escapeHtml(item.target)})` : ''}</p>`).join('')}</article>` : ''}
321
+ <p class="callout">${escapeHtml(evidence.note || '同一任务可能命中多条规则,命中次数不等于独立缺陷数。')}</p>
322
+ </section>
323
+
324
+ <section class="panel" role="tabpanel" id="panel-artifacts" aria-labelledby="tab-artifacts" hidden>
325
+ <div class="section-head"><div><h2>产物与追溯</h2><p>文件口径、变更归因和实际落盘产物均独立展示。</p></div></div>
326
+ <div class="two-col">
327
+ <article class="wide-card">
328
+ <h3>变更文件 · 双口径</h3>
329
+ <table><tbody>
330
+ <tr><td>任务事件累计文件</td><td><strong>${escapeHtml(changed.task_event_files_changed ?? '暂无数据')}</strong></td></tr>
331
+ <tr><td>最终输出快照文件</td><td><strong>${escapeHtml(changed.final_output_files_changed ?? '暂无数据')}</strong></td></tr>
332
+ <tr><td>新增 / 删除行</td><td>${escapeHtml(changed.added_lines ?? '暂无数据')} / ${escapeHtml(changed.deleted_lines ?? '暂无数据')}</td></tr>
333
+ <tr><td>数据来源</td><td>${escapeHtml(changed.diff_source || '未知')}</td></tr>
334
+ </tbody></table>
335
+ <p class="micro">${escapeHtml(changed.reconciliation_note || '暂无口径说明。')}</p>
336
+ </article>
337
+ <article class="wide-card">
338
+ <h3>归因审计</h3>
339
+ <table><tbody>
340
+ <tr><td>显式关联历史事件</td><td>${escapeHtml(report.attribution_audit?.linked_source_event_count ?? activeLinks.length)}</td></tr>
341
+ <tr><td>有效关联</td><td>${escapeHtml(activeLinks.length)}</td></tr>
342
+ <tr><td>未归属候选</td><td>${escapeHtml(candidateIds.length)}</td></tr>
343
+ </tbody></table>
344
+ <button class="text-button drawer-trigger" type="button" data-drawer-title="归因审计详情" data-drawer-body="${escapeHtml(`有效关联:${JSON.stringify(activeLinks)}\n未归属候选:${candidateIds.join('、') || '无'}`)}">查看归因事件</button>
345
+ </article>
346
+ </div>
347
+ <article class="wide-card section">
348
+ <h3>产物清单 · 实际产物 · 归档结果</h3>
349
+ <table><thead><tr><th>产物</th><th>状态</th><th>路径</th><th>大小</th></tr></thead><tbody>${artifactRows}</tbody></table>
350
+ </article>
351
+ <article class="wide-card section"><h3>已知风险</h3><p>${knownWarnings.length ? `${escapeHtml(knownWarnings.length)} 组风险记录,详情见“问题与证据”。` : '严重问题:0 · 无需修复。'}</p><p class="micro">${escapeHtml(report.archive_result?.archive_path || report.artifacts?.archive_path || '归档目录暂不可用')}</p></article>
352
+ </section>
353
+ </main>
354
+
355
+ <div class="drawer-backdrop" id="drawerBackdrop" aria-hidden="true">
356
+ <aside class="drawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" aria-describedby="drawerBody" tabindex="-1">
357
+ <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>
358
+ <pre id="drawerBody"></pre>
359
+ </aside>
360
+ </div>
361
+ <noscript><p class="callout">当前浏览器未启用脚本;请打印本报告或查看 Markdown / JSON 产物。查看证据时可使用“问题与证据”章节。</p></noscript>
362
+ <script>
363
+ (function(){
364
+ var tabs=Array.prototype.slice.call(document.querySelectorAll('[role="tab"]'));
365
+ var panels=Array.prototype.slice.call(document.querySelectorAll('[role="tabpanel"]'));
366
+ function activate(tab,focus){
367
+ tabs.forEach(function(item){
368
+ var selected=item===tab;
369
+ item.setAttribute('aria-selected',selected?'true':'false');
370
+ item.tabIndex=selected?0:-1;
371
+ document.getElementById(item.getAttribute('aria-controls')).hidden=!selected;
372
+ });
373
+ if(focus)tab.focus();
374
+ }
375
+ tabs.forEach(function(tab,index){
376
+ tab.addEventListener('click',function(){activate(tab,false);});
377
+ tab.addEventListener('keydown',function(event){
378
+ var next=index;
379
+ if(event.key === 'ArrowRight')next=(index+1)%tabs.length;
380
+ else if(event.key==='ArrowLeft')next=(index-1+tabs.length)%tabs.length;
381
+ else if(event.key==='Home')next=0;
382
+ else if(event.key==='End')next=tabs.length-1;
383
+ else return;
384
+ event.preventDefault();activate(tabs[next],true);
385
+ });
386
+ });
387
+ var backdrop=document.getElementById('drawerBackdrop');
388
+ var drawer=backdrop.querySelector('.drawer');
389
+ var drawerTitle=document.getElementById('drawerTitle');
390
+ var drawerBody=document.getElementById('drawerBody');
391
+ var lastTrigger=null;
392
+ function closeDrawer(){
393
+ if(!backdrop.classList.contains('open'))return;
394
+ backdrop.classList.remove('open');backdrop.setAttribute('aria-hidden','true');
395
+ if(lastTrigger)lastTrigger.focus();
396
+ }
397
+ document.querySelectorAll('.drawer-trigger,.info-btn').forEach(function(button){
398
+ button.addEventListener('click',function(){
399
+ lastTrigger=button;drawerTitle.textContent=button.dataset.drawerTitle||'详情';
400
+ drawerBody.textContent=button.dataset.drawerBody||'暂无详情';
401
+ backdrop.classList.add('open');backdrop.setAttribute('aria-hidden','false');drawer.focus();
402
+ });
403
+ });
404
+ document.getElementById('drawerClose').addEventListener('click',closeDrawer);
405
+ backdrop.addEventListener('click',function(event){if(event.target===backdrop)closeDrawer();});
406
+ document.addEventListener('keydown',function(event){
407
+ if(event.key === 'Escape')closeDrawer();
408
+ var e=event;
409
+ if(e.key==='Tab'&&backdrop.classList.contains('open')){
410
+ var items=drawer.querySelectorAll('button,[href],[tabindex]:not([tabindex="-1"])');
411
+ var first=items[0],last=items[items.length-1];
412
+ if(event.shiftKey&&document.activeElement===first){event.preventDefault();last.focus();}
413
+ else if(!event.shiftKey&&document.activeElement===last){event.preventDefault();first.focus();}
414
+ }
415
+ });
416
+ })();
417
+ </script>
418
+ </body>
419
+ </html>`;
420
+ }
421
+
422
+ module.exports = {
423
+ escapeHtml,
424
+ renderChangeReportHtml,
425
+ };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * 解析运行时元数据,返回 producer 身份信息。
7
+ *
8
+ * 开发/测试模式:直接读取上级 package.json 的 version 字段。
9
+ * 部署模式:init.js 会生成一个版本号写死的 runtime-metadata.cjs 覆盖此文件。
10
+ *
11
+ * @returns {{ version: string, producer: string }}
12
+ */
13
+ function resolveRuntimeMetadata() {
14
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
15
+ return {
16
+ version: pkg.version,
17
+ producer: `kld-sdd@${pkg.version}`,
18
+ };
19
+ }
20
+
21
+ module.exports = { resolveRuntimeMetadata };
@@ -144,6 +144,26 @@ function isRealTestDetails(testResults) {
144
144
  return false;
145
145
  }
146
146
 
147
+ function isPassingTestDetails(testResults) {
148
+ if (!isRealTestDetails(testResults)) return false;
149
+ const failed = Number(testResults.failed);
150
+ const exitCode = Number(testResults.exit_code);
151
+ return (!Number.isFinite(failed) || failed === 0)
152
+ && (!Number.isFinite(exitCode) || exitCode === 0);
153
+ }
154
+
155
+ function isStrictTestEvidence(event, changeName) {
156
+ const testResults = event?.details?.test_results;
157
+ if (event?.type !== 'test_result' || event.change !== changeName) return false;
158
+ if (!event.run_id || !event.source || testResults?.evidence_tier !== 'strict') return false;
159
+ if (!['green', 'refactor', 'regression'].includes(testResults.tdd_phase)) return false;
160
+ if (!Number.isInteger(testResults.exit_code) || testResults.counts_known !== true) return false;
161
+ return event.result === 'success'
162
+ && testResults.exit_code === 0
163
+ && Number(testResults.failed) === 0
164
+ && isPassingTestDetails(testResults);
165
+ }
166
+
147
167
  function loadChangeEvents(projectRoot, changeName) {
148
168
  const safe = safeChangeName(changeName);
149
169
  const dir = path.join(projectRoot, 'skywalk-sdd', 'events', safe);
@@ -170,33 +190,36 @@ function loadChangeEvents(projectRoot, changeName) {
170
190
  function hasRealTestExecution(projectRoot, changeName, capability, sinceTimestamp) {
171
191
  const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
172
192
  const events = loadChangeEvents(projectRoot, changeName);
193
+ let legacyEvidence = null;
173
194
 
174
195
  for (const event of events) {
175
196
  const ts = new Date(event.timestamp || 0).getTime();
176
197
  if (since && ts < since) continue;
177
198
 
178
- if (event.type === 'stage_end' && event.command === 'test' &&
179
- (event.result === 'success' || event.result === 'failure')) {
180
- return { kind: 'opsx-test', event };
199
+ if (event.type === 'stage_end' && event.command === 'test' && event.result === 'success') {
200
+ legacyEvidence ||= { kind: 'legacy-opsx-test', event };
181
201
  }
182
202
 
183
203
  if (event.type === 'test_result') {
184
204
  const tr = event.details && event.details.test_results;
185
- if (isRealTestDetails(tr)) {
186
- return { kind: 'test_result', event };
205
+ if (isStrictTestEvidence(event, changeName)) {
206
+ return { kind: 'strict-test-result', event };
207
+ }
208
+ if (event.result === 'success' && isPassingTestDetails(tr)) {
209
+ legacyEvidence ||= { kind: 'legacy-test-result', event };
187
210
  }
188
211
  }
189
212
 
190
213
  if (event.type === 'task_update' && (event.command === 'apply' || !event.command)) {
191
214
  const tr = event.details && event.details.test_results;
192
- if (isRealTestDetails(tr)) {
215
+ if (event.result !== 'failure' && isPassingTestDetails(tr)) {
193
216
  if (!capability || !event.capability || event.capability === capability) {
194
- return { kind: 'task_update', event };
217
+ legacyEvidence ||= { kind: 'legacy-task-update', event };
195
218
  }
196
219
  }
197
220
  }
198
221
  }
199
- return null;
222
+ return legacyEvidence;
200
223
  }
201
224
 
202
225
  function isApplyCompletionBash(command) {
@@ -281,9 +304,10 @@ function shouldGate(projectRoot, applyEvent) {
281
304
  applyEvent.capability,
282
305
  applyEvent.timestamp
283
306
  );
284
- if (evidence) {
307
+ if (evidence?.kind === 'strict-test-result') {
285
308
  return null;
286
309
  }
310
+ if (evidence) return { strategy, changeName: applyEvent.change, capability: applyEvent.capability, legacyEvidence: evidence };
287
311
  return { strategy, changeName: applyEvent.change, capability: applyEvent.capability };
288
312
  }
289
313
 
@@ -441,6 +465,11 @@ if (require.main === module) {
441
465
  if (!gate) {
442
466
  process.exit(0);
443
467
  }
468
+ if (gate.legacyEvidence) {
469
+ recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'legacy_test_evidence',
470
+ '仅检测到旧版测试证据;本次兼容放行,请补录包含 run-id、exit code、TDD phase 和严格身份字段的 test_result');
471
+ process.exit(0);
472
+ }
444
473
 
445
474
  const reason = buildReason(gate.strategy, gate.changeName, gate.capability);
446
475
  recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'apply_test_missing', 'Apply test gate blocked: no unit test evidence');
@@ -458,5 +487,6 @@ module.exports = {
458
487
  detectMissingTddRed,
459
488
  hasExistingWarning,
460
489
  isRealTestDetails,
490
+ isStrictTestEvidence,
461
491
  requiresUnitTests,
462
492
  };