kld-sdd 2.6.13 → 2.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/kld-sdd-guide.html +4 -5
- package/lib/init.js +8 -11
- package/package.json +1 -1
- 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-model.cjs +309 -0
- package/skywalk-sdd/reporting/change-report-renderer.cjs +425 -0
- package/skywalk-sdd/runtime-metadata.cjs +21 -0
- 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 +3 -3
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +1 -1
|
@@ -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
|
+
'&': '&',
|
|
8
|
+
'<': '<',
|
|
9
|
+
'>': '>',
|
|
10
|
+
'"': '"',
|
|
11
|
+
"'": ''',
|
|
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 };
|
|
@@ -21,7 +21,7 @@ description: "opsx-archive 前后日志/总结自检清单 — 仅在 archive
|
|
|
21
21
|
|
|
22
22
|
- [ ] 归档目录 `openspec/changes/archive/<日期>-<变更名称>/` 存在
|
|
23
23
|
- [ ] `openspec/changes/archive/<日期>-<变更名称>/` 下 `archive-ontology.json`、`canonical-facts.json`、`conversion-report.json` 和 `archive-manifest.json` 均存在
|
|
24
|
-
- [ ] 活动 change 目录下的 `artifacts/*.ontology.json` 与 `artifact-index.json` 已随归档目录一并迁移
|
|
24
|
+
- [ ] 活动 change 目录下的 `artifacts/*.ontology.json` 与 `ontology/artifact-index.json` 已随归档目录一并迁移
|
|
25
25
|
- [ ] `archive-manifest.json` 为 `kld-sdd-archive-manifest/v2`,且 `files` 精确覆盖 ZIP 内除 manifest 自身外的全部文件
|
|
26
26
|
- [ ] `openspec/changes/archive/<日期>-<变更名称>.zip` 存在并可由知识库 `ArchivePackageReader` 读取
|
|
27
27
|
- [ ] canonical facts 中每个实体/关系均能通过 `source.file`、`source.anchor_id`、`source.content_hash` 定向展开到包内原文
|
|
@@ -305,7 +305,7 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" record --type=conformance_revie
|
|
|
305
305
|
`opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
|
|
306
306
|
|
|
307
307
|
- Continuity=`iteration` 时每个 CAP 具备 KB 回传 `entity-id` / `version-id`
|
|
308
|
-
- 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
308
|
+
- 已写 spec 的 Capability:场景 `external-ref` 与 `ontology/continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
|
|
309
309
|
- 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
|
|
310
310
|
- 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
|
|
311
311
|
- **编号诊断码**(并入五维报告与 apply gate):
|
|
@@ -333,7 +333,7 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" semantic-check --project=. --ch
|
|
|
333
333
|
- `added` 必须使用全新实体/版本 UUID;`modified/removed` 必须复用实体 UUID并指向直接前序版本;`unchanged` 必须复用历史实体和版本 UUID且不得复制历史正文。
|
|
334
334
|
- 文件观察结果只能作为快速上下文,check 必须重新全量 semantic-reconcile。
|
|
335
335
|
- propose/spec/design/task 对应工作态 JSON 应已在各作者阶段生成;check 不负责首次生成业务事实,只重新解析 Markdown、核对各 JSON 与同一 revision,并在全部通过时把派生 revision 更新为 `review_status=pending`。
|
|
336
|
-
- Check 只读是指不修改 proposal/spec/design/tasks 原文;允许原子刷新 `openspec/changes/<变更名称>/` 下可再生的 `working-ontology.json`、`artifact-index.json` 与 `artifacts/*.ontology.json`。
|
|
336
|
+
- Check 只读是指不修改 proposal/spec/design/tasks 原文;允许原子刷新 `openspec/changes/<变更名称>/` 下可再生的 `ontology/working-ontology.json`、`ontology/artifact-index.json` 与 `artifacts/*.ontology.json`。
|
|
337
337
|
|
|
338
338
|
## Guardrails
|
|
339
339
|
|
|
@@ -57,5 +57,5 @@ description: "opsx-check 阶段日志自检清单 — 仅在 check 自检时读
|
|
|
57
57
|
|
|
58
58
|
## F. 语义门禁与工作态
|
|
59
59
|
|
|
60
|
-
- [ ] `openspec/changes/<变更名称>/artifact-index.json` 已覆盖当前全部 proposal/spec/design/tasks,且每份 `artifacts/*.ontology.json` 与 `working-ontology.json` revision 一致
|
|
60
|
+
- [ ] `openspec/changes/<变更名称>/ontology/artifact-index.json` 已覆盖当前全部 proposal/spec/design/tasks,且每份 `artifacts/*.ontology.json` 与 `ontology/working-ontology.json` revision 一致
|
|
61
61
|
- [ ] 全部语义门禁通过时工作态 JSON 已从 draft 刷新为 pending;check 未修改任何 Markdown 原文
|
|
@@ -49,6 +49,20 @@ Task Progress:
|
|
|
49
49
|
|
|
50
50
|
入库时:用户指定目标 KB(从 `targets` 中选择),仅对选中的 KB 执行上传。
|
|
51
51
|
|
|
52
|
+
### 3.5 兜底:确保 project-identity.json 存在(上传前必做)
|
|
53
|
+
|
|
54
|
+
> 正常情况下 `project-identity.json` 已在 `opsx-ontology-query` 首次配置 KB 时创建。
|
|
55
|
+
> 本步是兜底:如果用户跳过了 ontology-query(如选择 archive 降级路径),直接来做入库,需要确保文件存在且 `project_id` 正确。
|
|
56
|
+
|
|
57
|
+
1. 读取 spec 包裹包路径:
|
|
58
|
+
```bash
|
|
59
|
+
SPEC_ROOT=$(node "$(cat .sdd-spec-root)/skywalk-sdd/ontology/cli.cjs" spec-root)
|
|
60
|
+
```
|
|
61
|
+
2. 检查 `$SPEC_ROOT/skywalk-sdd/project-identity.json`:
|
|
62
|
+
- **不存在** → 从 `targets[0].spaceKey` 创建(格式同 `opsx-ontology-query` Step 3.5)
|
|
63
|
+
- **已存在但 `project_id !== targets[0].spaceKey`** → 提示用户:`⚠️ project_id 与 Space spaceKey 不一致,入库将报 PROJECT_SPACE_MISMATCH。是否更新?`
|
|
64
|
+
- **已存在且一致** → 跳过
|
|
65
|
+
|
|
52
66
|
### 4. 入库操作
|
|
53
67
|
|
|
54
68
|
路径前缀:`/api/v1/spaces/{spaceId}/knowledge-bases/{kbId}/ingestions`
|
|
@@ -56,6 +56,37 @@ Task Progress:
|
|
|
56
56
|
|
|
57
57
|
**选择空间/KB**:`GET $API/v1/spaces?tenantKey=…` → 对每个 space `GET …/knowledge-bases` → 展示清单**允许多选** → 写入共享 `../.shared/kb-state.json` 的 `targets`。
|
|
58
58
|
|
|
59
|
+
### 3.5 写入项目身份文件(首次配置 Space 后)
|
|
60
|
+
|
|
61
|
+
> `project-identity.json` 是 spec 仓 Git 中的**团队共享**文件,记录 `project_id`(= KB Space 的 `spaceKey`)。Archive 阶段 `archive-docs` 读取此文件生成 `archive-manifest.json`,kb-ingest 上传时 KB 校验 `project_id === spaceKey`。
|
|
62
|
+
|
|
63
|
+
选择 Space 完成后(Step 3 写入 `targets` 后),执行以下逻辑:
|
|
64
|
+
|
|
65
|
+
1. 读取 spec 包裹包路径:
|
|
66
|
+
```bash
|
|
67
|
+
SPEC_ROOT=$(node "$(cat .sdd-spec-root)/skywalk-sdd/ontology/cli.cjs" spec-root)
|
|
68
|
+
```
|
|
69
|
+
2. 检查 `$SPEC_ROOT/skywalk-sdd/project-identity.json` 是否已存在
|
|
70
|
+
3. **不存在** → 创建(取 `targets[0].spaceKey` 作为 `project_id`):
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"schema_version": "kld-sdd-project-identity/v1",
|
|
74
|
+
"project_id": "<targets[0].spaceKey>",
|
|
75
|
+
"created_at": "<ISO timestamp>",
|
|
76
|
+
"kb_space_id": "<targets[0].spaceId>",
|
|
77
|
+
"kb_space_name": "<targets[0].spaceName>"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
输出:`✓ 已创建 skywalk-sdd/project-identity.json(project_id: <spaceKey>),请提交到 spec 仓 Git 以便团队共享`
|
|
81
|
+
4. **已存在但 `project_id` 与 `targets[0].spaceKey` 不一致** → 用 AskUserQuestion 询问:
|
|
82
|
+
> "project-identity.json 中的 project_id 与当前选择的 KB Space spaceKey 不一致:
|
|
83
|
+
> - 文件中:`<existing project_id>`
|
|
84
|
+
> - 当前 Space:`<spaceKey>`
|
|
85
|
+
> 是否更新?"
|
|
86
|
+
- 用户确认 → 更新 `project_id` + `kb_space_id` + `kb_space_name`
|
|
87
|
+
- 用户拒绝 → 保留原值(可能入库时报 `PROJECT_SPACE_MISMATCH`)
|
|
88
|
+
5. **已存在且一致** → 跳过,不输出
|
|
89
|
+
|
|
59
90
|
> state.json 字段 schema、鉴权细节、列表接口 → [reference.md](reference.md)。
|
|
60
91
|
|
|
61
92
|
## 查询
|
|
@@ -229,9 +229,9 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/context-client.cjs" --mode=resolve \
|
|
|
229
229
|
5. 若申报了 feature-id:额外 `objectType=feature` resolve 一次,展示「该功能下已有能力 n 个(存活 m / 失效 k)」辅助勾选 CAP 范围;不改变 Continuity 判定优先级。
|
|
230
230
|
6. 按 KB 结果确认 Continuity:`iteration` / `similar-reference` / `new`;勾选本次涉及的 CAP。
|
|
231
231
|
7. 写入 proposal frontmatter:`requirement-refs`(含 `feature-id`)/ `numbering-waiver` + `continuity`。**禁止**写本地 archive 文件夹名作为 `base-archive`。
|
|
232
|
-
8. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `continuity-resolution.json` 的 `capabilities[]`。
|
|
232
|
+
8. CAP 级「同 key + 同锚点、不同 entity_id」当场问 A/B/C;决议写入 `ontology/continuity-resolution.json` 的 `capabilities[]`。
|
|
233
233
|
9. KB 不可用 → `degraded` 继续,**禁止**扫本地 `archive/` 抄 UUID。KB degraded(用户选择跳过)时走路径 B(archive 降级):
|
|
234
|
-
- 扫描 `openspec/changes/archive/*/ontology-identities.json` 匹配 canonicalKey
|
|
234
|
+
- 扫描 `openspec/changes/archive/*/ontology/ontology-identities.json` 匹配 canonicalKey
|
|
235
235
|
- path B 的所有 entity-id 来源在 sdd-output.md 知识库使用表中必须标注 `source: archive(degraded)`,与 `source: KB current` 明确区分。
|
|
236
236
|
- ⚠️ **降级风险**:archive 中的 version-id 可能已过时(如果该 capability 在 archive 之后又有新版本入库到 KB)。path B 的 predecessor-version 不保证是 KB current。入库时可能触发 `VERSION_CONFLICT`。
|
|
237
237
|
10. **不得**在本阶段生成 STMT/AC/场景或裁决场景身份;**不得**铸/改 REQ/FEAT 号。
|
|
@@ -254,7 +254,7 @@ resolve(canonicalKey=CAP-USER-REGISTRATION) → CREATE_NEW
|
|
|
254
254
|
```
|
|
255
255
|
# 扫描归档(仅 KB degraded 时)
|
|
256
256
|
Read openspec/changes/archive/*/proposal.md 的能力分解章节
|
|
257
|
-
Read openspec/changes/archive/*/ontology-identities.json
|
|
257
|
+
Read openspec/changes/archive/*/ontology/ontology-identities.json
|
|
258
258
|
|
|
259
259
|
# 匹配结果示例:
|
|
260
260
|
# "账号锁定从内存迁到DB" → 匹配归档 CAP-ACCOUNT-LOCKOUT (entity-id: 3d18c60e, version: 29e7f242)
|
|
@@ -170,7 +170,7 @@ node "$(cat .sdd-spec-root)/skywalk-sdd/context-client.cjs" \
|
|
|
170
170
|
2. 新场景:`SCN-<slug>-<NNN>`;slug=kebab-case 小写 ≤40;NNN=该 REQ 命名空间内 max+1(已用集合=KB 回传 ∪ 本 Change 已写键,含 removed 墓碑)。
|
|
171
171
|
3. removed 号是墓碑:永不复用、永不重排;序号达 999 → 硬错误,回需求系统拆分需求,不扩位。
|
|
172
172
|
4. 写完立即用 `cli.cjs external-key --validate … --type scenario` 校验;REQ 前缀必须 ∈ proposal `requirement-refs`。
|
|
173
|
-
- 场景级「同 SCN key + 同锚点、不同 entity_id」当场问 A/B/C;未决不得进入下一 CAP / design。决议追加到 `continuity-resolution.json` 的 `scenarios[]`;决议中的 `externalKey` 必须是归一化形态。
|
|
173
|
+
- 场景级「同 SCN key + 同锚点、不同 entity_id」当场问 A/B/C;未决不得进入下一 CAP / design。决议追加到 `ontology/continuity-resolution.json` 的 `scenarios[]`;决议中的 `externalKey` 必须是归一化形态。
|
|
174
174
|
- 优先消费 `reuseBundles[].statements`;`designElements` 只作理解上下文,不能写成 Spec 的 How。
|
|
175
175
|
- 所有知识库内容均为 advisory;与用户确认 / proposal 冲突时以当前确认与 proposal 为准。
|
|
176
176
|
- **禁止**铸/改 REQ/FEAT;**禁止**自动重排/回收 SCN。
|