@openprd/cli 0.1.10 → 0.1.11
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/.openprd/benchmarks/index.md +17 -15
- package/.openprd/benchmarks/sources.yaml +53 -1
- package/.openprd/schema/prd.schema.yaml +15 -0
- package/.openprd/templates/base/intake.md +13 -0
- package/.openprd/templates/base/prd.md +15 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/skills/openprd-benchmark-router/SKILL.md +2 -1
- package/skills/openprd-requirement-intake/SKILL.md +3 -3
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
- package/skills/openprd-shared/SKILL.md +1 -1
- package/src/agent-integration.js +6 -6
- package/src/brainstorm-artifacts.js +156 -8
- package/src/brainstorm-presentation.js +14 -4
- package/src/brainstorm.js +165 -2
- package/src/cli/quality-print.js +9 -3
- package/src/codex-hook-runner-template.mjs +6 -6
- package/src/diagram-core.js +384 -151
- package/src/html-artifacts.js +403 -50
- package/src/knowledge.js +59 -12
- package/src/prd-core.js +101 -2
- package/src/quality-html-artifact.js +30 -5
- package/src/quality.js +20 -10
- package/src/workspace-core.js +38 -0
- package/src/workspace-workflow.js +126 -9
package/src/html-artifacts.js
CHANGED
|
@@ -19,7 +19,7 @@ function card(title, body) {
|
|
|
19
19
|
`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [], footer = '', statusBadge = null, topMeta = [] }) {
|
|
22
|
+
function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [], footer = '', statusBadge = null, topMeta = [], bottomActions = [] }) {
|
|
23
23
|
return `<!DOCTYPE html>
|
|
24
24
|
<html lang="zh-CN">
|
|
25
25
|
<head>
|
|
@@ -29,26 +29,23 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
29
29
|
<style>
|
|
30
30
|
:root {
|
|
31
31
|
color-scheme: light;
|
|
32
|
-
--bg: #
|
|
33
|
-
--panel:
|
|
34
|
-
--text: #
|
|
35
|
-
--muted: #
|
|
36
|
-
--line:
|
|
37
|
-
--accent: #
|
|
38
|
-
--accent-soft: rgba(
|
|
32
|
+
--bg: #f6f8fb;
|
|
33
|
+
--panel: #ffffff;
|
|
34
|
+
--text: #172033;
|
|
35
|
+
--muted: #667085;
|
|
36
|
+
--line: #d8dee8;
|
|
37
|
+
--accent: #2563eb;
|
|
38
|
+
--accent-soft: rgba(37,99,235,0.1);
|
|
39
39
|
--danger: #dc2626;
|
|
40
40
|
--danger-soft: rgba(220,38,38,0.08);
|
|
41
41
|
--ok: #15803d;
|
|
42
42
|
--ok-soft: rgba(21,128,61,0.08);
|
|
43
43
|
--mono: "JetBrains Mono","SFMono-Regular",Menlo,monospace;
|
|
44
|
-
--serif: "Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,serif;
|
|
45
44
|
}
|
|
46
45
|
* { box-sizing: border-box; }
|
|
47
46
|
body {
|
|
48
47
|
margin: 0;
|
|
49
|
-
background:
|
|
50
|
-
radial-gradient(circle at top left, rgba(217,119,6,0.08), transparent 25%),
|
|
51
|
-
linear-gradient(180deg, #faf8f2 0%, var(--bg) 100%);
|
|
48
|
+
background: var(--bg);
|
|
52
49
|
color: var(--text);
|
|
53
50
|
font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
|
54
51
|
}
|
|
@@ -68,7 +65,7 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
68
65
|
padding: 6px 12px;
|
|
69
66
|
border-radius: 999px;
|
|
70
67
|
border: 1px solid var(--line);
|
|
71
|
-
background:
|
|
68
|
+
background: #ffffff;
|
|
72
69
|
color: var(--muted);
|
|
73
70
|
font-size: 12px;
|
|
74
71
|
letter-spacing: 0.08em;
|
|
@@ -129,10 +126,10 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
129
126
|
}
|
|
130
127
|
h1 {
|
|
131
128
|
margin: 0;
|
|
132
|
-
font-size: clamp(
|
|
133
|
-
line-height: 1;
|
|
134
|
-
font-
|
|
135
|
-
|
|
129
|
+
font-size: clamp(30px, 4.4vw, 46px);
|
|
130
|
+
line-height: 1.15;
|
|
131
|
+
font-weight: 700;
|
|
132
|
+
letter-spacing: -0.01em;
|
|
136
133
|
}
|
|
137
134
|
.subtitle {
|
|
138
135
|
max-width: 880px;
|
|
@@ -161,10 +158,9 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
161
158
|
}
|
|
162
159
|
.card {
|
|
163
160
|
border: 1px solid var(--line);
|
|
164
|
-
border-radius:
|
|
161
|
+
border-radius: 16px;
|
|
165
162
|
background: var(--panel);
|
|
166
|
-
|
|
167
|
-
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
|
|
163
|
+
box-shadow: 0 10px 24px rgba(23,32,51,0.06);
|
|
168
164
|
overflow: hidden;
|
|
169
165
|
}
|
|
170
166
|
.card-header {
|
|
@@ -178,10 +174,9 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
178
174
|
padding: 12px 18px 18px;
|
|
179
175
|
}
|
|
180
176
|
.metric {
|
|
181
|
-
font-size:
|
|
182
|
-
line-height: 1.
|
|
183
|
-
font-
|
|
184
|
-
font-weight: 600;
|
|
177
|
+
font-size: 28px;
|
|
178
|
+
line-height: 1.15;
|
|
179
|
+
font-weight: 700;
|
|
185
180
|
}
|
|
186
181
|
.metric-sub {
|
|
187
182
|
margin-top: 8px;
|
|
@@ -193,7 +188,7 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
193
188
|
padding: 10px 12px;
|
|
194
189
|
border-radius: 12px;
|
|
195
190
|
border: 1px solid var(--line);
|
|
196
|
-
background:
|
|
191
|
+
background: #fbfcfe;
|
|
197
192
|
}
|
|
198
193
|
.mini-metric-value {
|
|
199
194
|
font-size: 18px;
|
|
@@ -227,7 +222,7 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
227
222
|
padding: 12px 14px;
|
|
228
223
|
border-radius: 14px;
|
|
229
224
|
border: 1px solid var(--line);
|
|
230
|
-
background:
|
|
225
|
+
background: #fbfcfe;
|
|
231
226
|
}
|
|
232
227
|
.qa-label,
|
|
233
228
|
.option-title,
|
|
@@ -277,8 +272,9 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
277
272
|
overflow-x: auto;
|
|
278
273
|
padding: 14px;
|
|
279
274
|
border-radius: 14px;
|
|
280
|
-
|
|
281
|
-
|
|
275
|
+
border: 1px solid var(--line);
|
|
276
|
+
background: #f9fafb;
|
|
277
|
+
color: var(--text);
|
|
282
278
|
font-family: var(--mono);
|
|
283
279
|
font-size: 13px;
|
|
284
280
|
line-height: 1.6;
|
|
@@ -288,6 +284,46 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
288
284
|
color: var(--muted);
|
|
289
285
|
font-size: 13px;
|
|
290
286
|
}
|
|
287
|
+
.page-bottom-bar {
|
|
288
|
+
position: fixed;
|
|
289
|
+
inset-inline: 0;
|
|
290
|
+
bottom: 0;
|
|
291
|
+
z-index: 30;
|
|
292
|
+
padding: 14px 24px calc(14px + env(safe-area-inset-bottom));
|
|
293
|
+
background: rgba(246,248,251,0.92);
|
|
294
|
+
backdrop-filter: blur(8px);
|
|
295
|
+
border-top: 1px solid var(--line);
|
|
296
|
+
}
|
|
297
|
+
.page-bottom-bar-inner {
|
|
298
|
+
max-width: 980px;
|
|
299
|
+
margin: 0 auto;
|
|
300
|
+
display: flex;
|
|
301
|
+
justify-content: flex-end;
|
|
302
|
+
gap: 12px;
|
|
303
|
+
}
|
|
304
|
+
.page-bottom-action {
|
|
305
|
+
appearance: none;
|
|
306
|
+
border-radius: 12px;
|
|
307
|
+
padding: 10px 22px;
|
|
308
|
+
font-size: 15px;
|
|
309
|
+
font-weight: 600;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
border: 1px solid var(--line);
|
|
312
|
+
background: #ffffff;
|
|
313
|
+
color: var(--text);
|
|
314
|
+
}
|
|
315
|
+
.page-bottom-action.primary {
|
|
316
|
+
border-color: rgba(21,128,61,0.35);
|
|
317
|
+
background: var(--ok-soft);
|
|
318
|
+
color: var(--ok);
|
|
319
|
+
}
|
|
320
|
+
.page-bottom-action.danger {
|
|
321
|
+
border-color: rgba(220,38,38,0.35);
|
|
322
|
+
background: var(--danger-soft);
|
|
323
|
+
color: var(--danger);
|
|
324
|
+
}
|
|
325
|
+
.page-bottom-action:hover { filter: brightness(0.97); }
|
|
326
|
+
.has-bottom-bar { padding-bottom: 96px; }
|
|
291
327
|
@media (max-width: 700px) {
|
|
292
328
|
.page { padding: 20px 14px 40px; }
|
|
293
329
|
.subtitle { font-size: 16px; }
|
|
@@ -295,7 +331,7 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
295
331
|
</style>
|
|
296
332
|
</head>
|
|
297
333
|
<body>
|
|
298
|
-
<main class="page">
|
|
334
|
+
<main class="page${bottomActions.length ? ' has-bottom-bar' : ''}">
|
|
299
335
|
<header class="hero">
|
|
300
336
|
<div class="hero-topline">
|
|
301
337
|
<div class="eyebrow">${escapeHtml(eyebrow)}</div>
|
|
@@ -308,6 +344,15 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
308
344
|
<section class="summary-grid">${summaryCards.join('\n')}</section>
|
|
309
345
|
<section class="section-grid">${sections.join('\n')}</section>
|
|
310
346
|
${footer ? `<div class="footer">${escapeHtml(footer)}</div>` : ''}
|
|
347
|
+
${bottomActions.length ? `
|
|
348
|
+
<nav class="page-bottom-bar" aria-label="下一步反馈">
|
|
349
|
+
<div class="page-bottom-bar-inner">
|
|
350
|
+
${bottomActions.map((action) => `
|
|
351
|
+
<button type="button" class="page-bottom-action ${escapeHtml(action.tone ?? 'primary')}" data-copy-value="${escapeHtml(action.copyText)}">
|
|
352
|
+
${escapeHtml(action.label)}
|
|
353
|
+
</button>`).join('')}
|
|
354
|
+
</div>
|
|
355
|
+
</nav>` : ''}
|
|
311
356
|
<script>
|
|
312
357
|
document.querySelectorAll('[data-copy-target]').forEach((button) => {
|
|
313
358
|
button.addEventListener('click', async () => {
|
|
@@ -319,6 +364,14 @@ function pageShell({ title, subtitle, eyebrow, summaryCards = [], sections = [],
|
|
|
319
364
|
setTimeout(() => { button.textContent = old; }, 1200);
|
|
320
365
|
});
|
|
321
366
|
});
|
|
367
|
+
document.querySelectorAll('[data-copy-value]').forEach((button) => {
|
|
368
|
+
button.addEventListener('click', async () => {
|
|
369
|
+
await navigator.clipboard.writeText(button.dataset.copyValue || '');
|
|
370
|
+
const old = button.textContent;
|
|
371
|
+
button.textContent = '✓ 已复制,回到对话里粘贴给我';
|
|
372
|
+
setTimeout(() => { button.textContent = old; }, 1600);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
322
375
|
</script>
|
|
323
376
|
</main>
|
|
324
377
|
</body>
|
|
@@ -816,15 +869,99 @@ function renderReviewProjectVersion(projectRelease) {
|
|
|
816
869
|
`;
|
|
817
870
|
}
|
|
818
871
|
|
|
819
|
-
function renderReviewOverview(snapshot, sectionsData) {
|
|
872
|
+
function renderReviewOverview(snapshot, sectionsData, agenda = []) {
|
|
820
873
|
const problem = sectionsData.problem?.problemStatement || '尚未形成明确问题定义';
|
|
874
|
+
const goal = reviewList(sectionsData.goals?.goals)[0] ?? null;
|
|
875
|
+
const agendaMarkup = agenda.length === 0 ? '' : `
|
|
876
|
+
<aside class="review-agenda" aria-label="评审清单">
|
|
877
|
+
<p class="review-agenda-title">这次评审要确认 ${agenda.length} 块内容</p>
|
|
878
|
+
<ol class="review-agenda-list">
|
|
879
|
+
${agenda.map((item) => `
|
|
880
|
+
<li>
|
|
881
|
+
<a href="#review-panel-${escapeHtml(item.kind)}">
|
|
882
|
+
<span class="review-agenda-name">${escapeHtml(item.title)}</span>
|
|
883
|
+
<span class="review-agenda-count${item.count === 0 ? ' empty' : ''}">${item.count === 0 ? '暂无内容' : `${item.count} 条`}</span>
|
|
884
|
+
</a>
|
|
885
|
+
</li>`).join('')}
|
|
886
|
+
</ol>
|
|
887
|
+
<p class="review-agenda-foot">逐块看完后,用页面底部的按钮告诉我结论就可以。</p>
|
|
888
|
+
</aside>
|
|
889
|
+
`;
|
|
821
890
|
return `
|
|
822
891
|
<section class="review-overview" aria-labelledby="reviewOverviewTitle">
|
|
823
892
|
<div class="review-overview-copy">
|
|
824
893
|
<p class="review-kicker">需求概览</p>
|
|
825
894
|
<h1 id="reviewOverviewTitle">${escapeHtml(snapshot.title || 'PRD 评审')}</h1>
|
|
826
895
|
<p class="review-problem">${escapeHtml(problem)}</p>
|
|
896
|
+
${goal ? `<p class="review-goal"><span class="review-goal-label">这次的目标</span>${escapeHtml(goal)}</p>` : ''}
|
|
827
897
|
</div>
|
|
898
|
+
${agendaMarkup}
|
|
899
|
+
</section>
|
|
900
|
+
`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const REVIEW_GAP_PLACEHOLDER = /^待(补充|澄清|确认)/;
|
|
904
|
+
|
|
905
|
+
const REVIEW_GAP_FIELDS = [
|
|
906
|
+
{ path: ['problem', 'problemStatement'], label: '问题定义', hint: '还没说清楚这次要解决什么问题', priority: 1 },
|
|
907
|
+
{ path: ['goals', 'goals'], label: '目标', hint: '还没说清楚这次想达成什么', priority: 1 },
|
|
908
|
+
{ path: ['scope', 'inScope'], label: '范围内', hint: '还没列出这次要做的内容', priority: 1 },
|
|
909
|
+
{ path: ['scenarios', 'primaryFlows'], label: '主流程', hint: '还没把用户怎么走完整个流程讲清楚', priority: 1 },
|
|
910
|
+
{ path: ['goals', 'successMetrics'], label: '成功指标', hint: '还没定义怎么算做成了', priority: 2 },
|
|
911
|
+
{ path: ['scope', 'outOfScope'], label: '范围外', hint: '还没说清楚这次明确不做什么', priority: 2 },
|
|
912
|
+
{ path: ['requirements', 'functional'], label: '功能需求', hint: '还没列出必须交付的功能', priority: 2 },
|
|
913
|
+
{ path: ['scenarios', 'edgeCases'], label: '边界情况', hint: '还没考虑特殊情况怎么处理', priority: 3 },
|
|
914
|
+
{ path: ['scenarios', 'failureModes'], label: '失败路径', hint: '还没说清楚出错后怎么恢复', priority: 3 },
|
|
915
|
+
];
|
|
916
|
+
|
|
917
|
+
function reviewGapFieldMissing(sectionsData, field) {
|
|
918
|
+
const raw = sectionsData?.[field.path[0]]?.[field.path[1]];
|
|
919
|
+
if (Array.isArray(raw)) {
|
|
920
|
+
const items = reviewList(raw);
|
|
921
|
+
return items.length === 0 || items.every((item) => REVIEW_GAP_PLACEHOLDER.test(item));
|
|
922
|
+
}
|
|
923
|
+
const text = String(raw ?? '').trim();
|
|
924
|
+
return !text || REVIEW_GAP_PLACEHOLDER.test(text);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function collectReviewGaps(sectionsData) {
|
|
928
|
+
return REVIEW_GAP_FIELDS
|
|
929
|
+
.filter((field) => reviewGapFieldMissing(sectionsData ?? {}, field))
|
|
930
|
+
.sort((a, b) => a.priority - b.priority);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function renderReviewGapPanel(sectionsData) {
|
|
934
|
+
const gaps = collectReviewGaps(sectionsData);
|
|
935
|
+
if (gaps.length === 0) {
|
|
936
|
+
return `
|
|
937
|
+
<section class="review-gap review-gap-clear" aria-label="信息完整度">
|
|
938
|
+
<div class="review-gap-headline">
|
|
939
|
+
<span class="review-gap-dot" aria-hidden="true"></span>
|
|
940
|
+
<strong>核心信息已补齐</strong>
|
|
941
|
+
<span class="review-gap-sub">问题、目标、范围和主流程都已经写清楚,可以放心往下确认。</span>
|
|
942
|
+
</div>
|
|
943
|
+
</section>
|
|
944
|
+
`;
|
|
945
|
+
}
|
|
946
|
+
const blocking = gaps.filter((gap) => gap.priority === 1).length;
|
|
947
|
+
const subText = blocking > 0
|
|
948
|
+
? '其中带「关键」标记的内容建议先补齐,再做确认。'
|
|
949
|
+
: '都不是拦路的问题,但确认前补上会更稳。';
|
|
950
|
+
return `
|
|
951
|
+
<section class="review-gap review-gap-pending" aria-label="信息完整度">
|
|
952
|
+
<div class="review-gap-headline">
|
|
953
|
+
<span class="review-gap-dot" aria-hidden="true"></span>
|
|
954
|
+
<strong>确认前还差 ${gaps.length} 件事</strong>
|
|
955
|
+
<span class="review-gap-sub">${escapeHtml(subText)}</span>
|
|
956
|
+
</div>
|
|
957
|
+
<ol class="review-gap-list">
|
|
958
|
+
${gaps.map((gap) => `
|
|
959
|
+
<li class="review-gap-item">
|
|
960
|
+
<span class="review-gap-tag${gap.priority === 1 ? ' review-gap-tag-key' : ''}">${gap.priority === 1 ? '关键' : '建议'}</span>
|
|
961
|
+
<strong>${escapeHtml(gap.label)}</strong>
|
|
962
|
+
<span class="review-gap-hint">${escapeHtml(gap.hint)}</span>
|
|
963
|
+
</li>`).join('')}
|
|
964
|
+
</ol>
|
|
828
965
|
</section>
|
|
829
966
|
`;
|
|
830
967
|
}
|
|
@@ -1121,14 +1258,16 @@ function reviewSubtitleText(value) {
|
|
|
1121
1258
|
}
|
|
1122
1259
|
|
|
1123
1260
|
function renderReviewPanel({ kind, title, description, items, emptyText, visual = '' }) {
|
|
1261
|
+
const itemCount = Array.isArray(items) ? items.filter(Boolean).length : 0;
|
|
1124
1262
|
return `
|
|
1125
|
-
<section class="review-panel review-panel-${escapeHtml(kind)}">
|
|
1263
|
+
<section class="review-panel review-panel-${escapeHtml(kind)}" id="review-panel-${escapeHtml(kind)}">
|
|
1126
1264
|
<header class="review-panel-head">
|
|
1127
1265
|
${reviewIcon(kind)}
|
|
1128
1266
|
<div>
|
|
1129
1267
|
<h3>${escapeHtml(title)}</h3>
|
|
1130
1268
|
<p>${escapeHtml(reviewSubtitleText(description))}</p>
|
|
1131
1269
|
</div>
|
|
1270
|
+
<span class="review-panel-count${itemCount === 0 ? ' empty' : ''}" aria-label="条目数">${itemCount === 0 ? '暂无' : `${itemCount} 条`}</span>
|
|
1132
1271
|
</header>
|
|
1133
1272
|
<div class="review-chip-row" aria-label="${escapeHtml(title)}重点摘要">
|
|
1134
1273
|
${reviewHighlightChips(items, emptyText)}
|
|
@@ -1267,37 +1406,43 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1267
1406
|
...reviewList(sectionsData.risks?.risks),
|
|
1268
1407
|
...reviewList(sectionsData.risks?.openQuestions),
|
|
1269
1408
|
]);
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1409
|
+
const panelSpecs = [
|
|
1410
|
+
{
|
|
1272
1411
|
kind: 'flow',
|
|
1273
1412
|
title: '主流程与边界情况',
|
|
1274
1413
|
description: '确认用户旅程、关键步骤和恢复路径是否已经讲清楚,能否进入实现前确认',
|
|
1275
1414
|
emptyText: '暂无主流程、边界情况或失败路径。',
|
|
1276
1415
|
visual: renderReviewJourneySvg({ primaryFlows, edgeCases, failureModes }),
|
|
1277
1416
|
items: flowPanelItems,
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1280
1419
|
kind: 'function',
|
|
1281
1420
|
title: '功能与约束',
|
|
1282
1421
|
description: '区分必须交付、非功能要求和当前依赖假设',
|
|
1283
1422
|
emptyText: '暂无功能、非功能要求或依赖约束。',
|
|
1284
1423
|
items: functionPanelItems,
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1287
1426
|
kind: 'guardrail',
|
|
1288
1427
|
title: '业务成本与滥用护栏',
|
|
1289
1428
|
description: '涉及免费额度、消耗型成本或第三方调用时,先确认限制、报警和止损动作',
|
|
1290
1429
|
emptyText: '暂无业务成本或滥用护栏。',
|
|
1291
1430
|
items: guardrailPanelItems,
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1294
1433
|
kind: 'risk',
|
|
1295
1434
|
title: '开放问题与风险',
|
|
1296
1435
|
description: '需求定稿前还没关掉的问题要留在这里,不要默默假定解决',
|
|
1297
1436
|
emptyText: '暂无假设、风险或开放问题。',
|
|
1298
1437
|
items: riskPanelItems,
|
|
1299
|
-
}
|
|
1438
|
+
},
|
|
1300
1439
|
];
|
|
1440
|
+
const panelAgenda = panelSpecs.map((spec) => ({
|
|
1441
|
+
kind: spec.kind,
|
|
1442
|
+
title: spec.title,
|
|
1443
|
+
count: Array.isArray(spec.items) ? spec.items.filter(Boolean).length : 0,
|
|
1444
|
+
}));
|
|
1445
|
+
const panels = panelSpecs.map((spec) => renderReviewPanel(spec));
|
|
1301
1446
|
return `<!DOCTYPE html>
|
|
1302
1447
|
<html lang="zh-CN">
|
|
1303
1448
|
<head>
|
|
@@ -1399,9 +1544,116 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1399
1544
|
box-shadow: 0 16px 34px rgba(15, 23, 42, 0.06);
|
|
1400
1545
|
}
|
|
1401
1546
|
.review-overview {
|
|
1402
|
-
display:
|
|
1547
|
+
display: grid;
|
|
1548
|
+
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 1fr);
|
|
1549
|
+
gap: 24px;
|
|
1550
|
+
align-items: start;
|
|
1403
1551
|
padding: 24px;
|
|
1404
1552
|
}
|
|
1553
|
+
.review-goal {
|
|
1554
|
+
display: flex;
|
|
1555
|
+
flex-wrap: wrap;
|
|
1556
|
+
align-items: baseline;
|
|
1557
|
+
gap: 8px;
|
|
1558
|
+
max-width: 760px;
|
|
1559
|
+
margin: 14px 0 0;
|
|
1560
|
+
color: var(--review-text);
|
|
1561
|
+
font-size: 15px;
|
|
1562
|
+
line-height: 1.7;
|
|
1563
|
+
}
|
|
1564
|
+
.review-goal-label {
|
|
1565
|
+
flex-shrink: 0;
|
|
1566
|
+
padding: 2px 10px;
|
|
1567
|
+
border-radius: 999px;
|
|
1568
|
+
border: 1px solid rgba(37,99,235,0.3);
|
|
1569
|
+
background: rgba(37,99,235,0.06);
|
|
1570
|
+
color: var(--review-blue);
|
|
1571
|
+
font-size: 12px;
|
|
1572
|
+
font-weight: 600;
|
|
1573
|
+
}
|
|
1574
|
+
.review-agenda {
|
|
1575
|
+
padding: 16px 18px;
|
|
1576
|
+
border-radius: 14px;
|
|
1577
|
+
border: 1px solid var(--review-line);
|
|
1578
|
+
background: var(--review-panel-soft);
|
|
1579
|
+
}
|
|
1580
|
+
.review-agenda-title {
|
|
1581
|
+
margin: 0;
|
|
1582
|
+
color: var(--review-text);
|
|
1583
|
+
font-size: 14px;
|
|
1584
|
+
font-weight: 700;
|
|
1585
|
+
}
|
|
1586
|
+
.review-agenda-list {
|
|
1587
|
+
margin: 12px 0 0;
|
|
1588
|
+
padding: 0;
|
|
1589
|
+
list-style: none;
|
|
1590
|
+
display: grid;
|
|
1591
|
+
gap: 6px;
|
|
1592
|
+
counter-reset: agenda;
|
|
1593
|
+
}
|
|
1594
|
+
.review-agenda-list li { counter-increment: agenda; }
|
|
1595
|
+
.review-agenda-list a {
|
|
1596
|
+
display: flex;
|
|
1597
|
+
align-items: center;
|
|
1598
|
+
gap: 10px;
|
|
1599
|
+
padding: 8px 10px;
|
|
1600
|
+
border-radius: 10px;
|
|
1601
|
+
border: 1px solid transparent;
|
|
1602
|
+
color: var(--review-text);
|
|
1603
|
+
text-decoration: none;
|
|
1604
|
+
font-size: 14px;
|
|
1605
|
+
line-height: 1.5;
|
|
1606
|
+
}
|
|
1607
|
+
.review-agenda-list a::before {
|
|
1608
|
+
content: counter(agenda);
|
|
1609
|
+
flex-shrink: 0;
|
|
1610
|
+
width: 20px;
|
|
1611
|
+
height: 20px;
|
|
1612
|
+
display: inline-flex;
|
|
1613
|
+
align-items: center;
|
|
1614
|
+
justify-content: center;
|
|
1615
|
+
border-radius: 999px;
|
|
1616
|
+
background: rgba(37,99,235,0.1);
|
|
1617
|
+
color: var(--review-blue);
|
|
1618
|
+
font-size: 12px;
|
|
1619
|
+
font-weight: 700;
|
|
1620
|
+
}
|
|
1621
|
+
.review-agenda-list a:hover {
|
|
1622
|
+
border-color: var(--review-line);
|
|
1623
|
+
background: #ffffff;
|
|
1624
|
+
}
|
|
1625
|
+
.review-agenda-name { flex: 1; min-width: 0; }
|
|
1626
|
+
.review-agenda-count {
|
|
1627
|
+
flex-shrink: 0;
|
|
1628
|
+
color: var(--review-blue);
|
|
1629
|
+
font-size: 12px;
|
|
1630
|
+
font-weight: 600;
|
|
1631
|
+
}
|
|
1632
|
+
.review-agenda-count.empty { color: var(--review-muted); font-weight: 400; }
|
|
1633
|
+
.review-agenda-foot {
|
|
1634
|
+
margin: 12px 0 0;
|
|
1635
|
+
color: var(--review-muted);
|
|
1636
|
+
font-size: 13px;
|
|
1637
|
+
line-height: 1.6;
|
|
1638
|
+
}
|
|
1639
|
+
.review-panel-count {
|
|
1640
|
+
flex-shrink: 0;
|
|
1641
|
+
margin-left: auto;
|
|
1642
|
+
padding: 3px 10px;
|
|
1643
|
+
border-radius: 999px;
|
|
1644
|
+
border: 1px solid rgba(37,99,235,0.25);
|
|
1645
|
+
background: rgba(37,99,235,0.06);
|
|
1646
|
+
color: var(--review-blue);
|
|
1647
|
+
font-size: 12px;
|
|
1648
|
+
font-weight: 600;
|
|
1649
|
+
white-space: nowrap;
|
|
1650
|
+
}
|
|
1651
|
+
.review-panel-count.empty {
|
|
1652
|
+
border-color: var(--review-line);
|
|
1653
|
+
background: var(--review-panel-soft);
|
|
1654
|
+
color: var(--review-muted);
|
|
1655
|
+
font-weight: 400;
|
|
1656
|
+
}
|
|
1405
1657
|
.review-overview-copy,
|
|
1406
1658
|
.review-panel {
|
|
1407
1659
|
min-width: 0;
|
|
@@ -1427,6 +1679,83 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1427
1679
|
line-height: 1.75;
|
|
1428
1680
|
overflow-wrap: anywhere;
|
|
1429
1681
|
}
|
|
1682
|
+
.review-gap {
|
|
1683
|
+
margin-top: 18px;
|
|
1684
|
+
padding: 18px 20px;
|
|
1685
|
+
border-radius: 16px;
|
|
1686
|
+
border: 1px solid var(--review-line);
|
|
1687
|
+
background: var(--review-panel);
|
|
1688
|
+
box-shadow: 0 10px 24px rgba(23,32,51,0.05);
|
|
1689
|
+
}
|
|
1690
|
+
.review-gap-clear {
|
|
1691
|
+
border-color: rgba(21,128,61,0.3);
|
|
1692
|
+
background: #f2fbf5;
|
|
1693
|
+
}
|
|
1694
|
+
.review-gap-pending {
|
|
1695
|
+
border-color: rgba(180,83,9,0.3);
|
|
1696
|
+
background: #fffaf2;
|
|
1697
|
+
}
|
|
1698
|
+
.review-gap-headline {
|
|
1699
|
+
display: flex;
|
|
1700
|
+
flex-wrap: wrap;
|
|
1701
|
+
align-items: baseline;
|
|
1702
|
+
gap: 10px;
|
|
1703
|
+
font-size: 16px;
|
|
1704
|
+
}
|
|
1705
|
+
.review-gap-headline strong {
|
|
1706
|
+
color: var(--review-text);
|
|
1707
|
+
font-size: 17px;
|
|
1708
|
+
}
|
|
1709
|
+
.review-gap-dot {
|
|
1710
|
+
width: 10px;
|
|
1711
|
+
height: 10px;
|
|
1712
|
+
border-radius: 999px;
|
|
1713
|
+
align-self: center;
|
|
1714
|
+
}
|
|
1715
|
+
.review-gap-clear .review-gap-dot { background: var(--review-green); }
|
|
1716
|
+
.review-gap-pending .review-gap-dot { background: var(--review-amber); }
|
|
1717
|
+
.review-gap-sub {
|
|
1718
|
+
color: var(--review-muted);
|
|
1719
|
+
font-size: 14px;
|
|
1720
|
+
line-height: 1.6;
|
|
1721
|
+
}
|
|
1722
|
+
.review-gap-list {
|
|
1723
|
+
margin: 14px 0 0;
|
|
1724
|
+
padding: 0;
|
|
1725
|
+
list-style: none;
|
|
1726
|
+
display: grid;
|
|
1727
|
+
gap: 8px;
|
|
1728
|
+
}
|
|
1729
|
+
.review-gap-item {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
flex-wrap: wrap;
|
|
1732
|
+
align-items: baseline;
|
|
1733
|
+
gap: 8px;
|
|
1734
|
+
padding: 10px 12px;
|
|
1735
|
+
border-radius: 12px;
|
|
1736
|
+
border: 1px solid rgba(180,83,9,0.16);
|
|
1737
|
+
background: rgba(255,255,255,0.75);
|
|
1738
|
+
font-size: 14px;
|
|
1739
|
+
line-height: 1.6;
|
|
1740
|
+
}
|
|
1741
|
+
.review-gap-item strong { color: var(--review-text); }
|
|
1742
|
+
.review-gap-hint { color: var(--review-muted); }
|
|
1743
|
+
.review-gap-tag {
|
|
1744
|
+
display: inline-flex;
|
|
1745
|
+
align-items: center;
|
|
1746
|
+
padding: 2px 8px;
|
|
1747
|
+
border-radius: 999px;
|
|
1748
|
+
border: 1px solid var(--review-line);
|
|
1749
|
+
background: #ffffff;
|
|
1750
|
+
color: var(--review-muted);
|
|
1751
|
+
font-size: 12px;
|
|
1752
|
+
font-weight: 600;
|
|
1753
|
+
}
|
|
1754
|
+
.review-gap-tag-key {
|
|
1755
|
+
border-color: rgba(220,38,38,0.35);
|
|
1756
|
+
color: var(--review-red);
|
|
1757
|
+
background: rgba(220,38,38,0.06);
|
|
1758
|
+
}
|
|
1430
1759
|
.review-map {
|
|
1431
1760
|
margin-top: 18px;
|
|
1432
1761
|
padding: 20px;
|
|
@@ -1797,7 +2126,8 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1797
2126
|
<div class="review-brand">OpenPrd / 评审面板</div>
|
|
1798
2127
|
${renderReviewProjectVersion(visibleProjectRelease)}
|
|
1799
2128
|
</header>
|
|
1800
|
-
${renderReviewOverview(snapshot, sectionsData)}
|
|
2129
|
+
${renderReviewOverview(snapshot, sectionsData, panelAgenda)}
|
|
2130
|
+
${renderReviewGapPanel(sectionsData)}
|
|
1801
2131
|
${renderReviewFlowSvg(snapshot, sectionsData)}
|
|
1802
2132
|
<section class="review-panel-grid" aria-label="固定评审项">
|
|
1803
2133
|
${panels.join('\n')}
|
|
@@ -1945,12 +2275,12 @@ export function renderPlaygroundArtifact({ snapshot, state, markdownPath, patchP
|
|
|
1945
2275
|
<style>
|
|
1946
2276
|
:root {
|
|
1947
2277
|
color-scheme: light;
|
|
1948
|
-
--bg: #
|
|
2278
|
+
--bg: #f6f8fb;
|
|
1949
2279
|
--panel: #ffffff;
|
|
1950
|
-
--line:
|
|
1951
|
-
--text: #
|
|
1952
|
-
--muted: #
|
|
1953
|
-
--accent: #
|
|
2280
|
+
--line: #d8dee8;
|
|
2281
|
+
--text: #172033;
|
|
2282
|
+
--muted: #667085;
|
|
2283
|
+
--accent: #2563eb;
|
|
1954
2284
|
}
|
|
1955
2285
|
* { box-sizing: border-box; }
|
|
1956
2286
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
|
@@ -1967,8 +2297,8 @@ export function renderPlaygroundArtifact({ snapshot, state, markdownPath, patchP
|
|
|
1967
2297
|
textarea { width: 100%; border: 1px solid var(--line); border-radius: 12px; padding: 12px; font: inherit; line-height: 1.6; resize: vertical; }
|
|
1968
2298
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
1969
2299
|
button { border: 1px solid var(--line); border-radius: 10px; padding: 10px 14px; background: #fff; cursor: pointer; }
|
|
1970
|
-
.primary { background:
|
|
1971
|
-
pre { margin: 0; border-radius: 14px; background: #
|
|
2300
|
+
.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
2301
|
+
pre { margin: 0; border-radius: 14px; border: 1px solid var(--line); background: #f9fafb; color: var(--text); padding: 14px; overflow: auto; white-space: pre-wrap; line-height: 1.6; font-size: 13px; }
|
|
1972
2302
|
.hint { color: var(--muted); font-size: 13px; line-height: 1.6; }
|
|
1973
2303
|
.top-meta {
|
|
1974
2304
|
display: flex;
|
|
@@ -1982,7 +2312,7 @@ export function renderPlaygroundArtifact({ snapshot, state, markdownPath, patchP
|
|
|
1982
2312
|
padding: 6px 10px;
|
|
1983
2313
|
border-radius: 999px;
|
|
1984
2314
|
border: 1px solid var(--line);
|
|
1985
|
-
background:
|
|
2315
|
+
background: #ffffff;
|
|
1986
2316
|
color: var(--muted);
|
|
1987
2317
|
font-size: 12px;
|
|
1988
2318
|
}
|
|
@@ -2180,14 +2510,29 @@ export function renderReviewArtifact({ snapshot, projectRelease = null }) {
|
|
|
2180
2510
|
|
|
2181
2511
|
export function renderRegressionArtifact({ task, report }) {
|
|
2182
2512
|
const passed = report.summary.failed === 0;
|
|
2513
|
+
const failedCases = (report.cases ?? []).filter((item) => !item.passed);
|
|
2183
2514
|
const summaryCards = [
|
|
2184
2515
|
metricCard('任务', task.id, task.title),
|
|
2185
2516
|
metricCard('验证方式', report.kind || 'command', report.verifyCommand || '未指定'),
|
|
2186
2517
|
metricCard('通过用例', `${report.summary.passed}/${report.summary.total}`, '本次回归通过的测试用例数量'),
|
|
2187
2518
|
metricCard('失败用例', `${report.summary.failed}`, '需要继续修复或补证据的测试用例数量'),
|
|
2188
2519
|
];
|
|
2520
|
+
const verdictCard = card('这次验证的结论', passed
|
|
2521
|
+
? `
|
|
2522
|
+
<div class="qa-item success">
|
|
2523
|
+
<div class="qa-label">全部 ${report.summary.total} 个用例都通过了</div>
|
|
2524
|
+
<div class="qa-meta">这部分功能按预期工作,可以放心进入下一步。想继续推进就用底部按钮告诉我。</div>
|
|
2525
|
+
</div>
|
|
2526
|
+
`
|
|
2527
|
+
: `
|
|
2528
|
+
<div class="qa-item warning">
|
|
2529
|
+
<div class="qa-label">还有 ${report.summary.failed} 个用例没有通过</div>
|
|
2530
|
+
<div class="qa-meta">没过的是:${failedCases.map((item) => escapeHtml(item.title || item.id)).join('、') || '见下方清单'}。建议先让我继续修复,再重新跑一次验证。</div>
|
|
2531
|
+
</div>
|
|
2532
|
+
`);
|
|
2189
2533
|
|
|
2190
2534
|
const sections = [
|
|
2535
|
+
verdictCard,
|
|
2191
2536
|
card('回归用例清单', report.cases.map((item) => `
|
|
2192
2537
|
<div class="qa-item ${item.passed ? 'success' : 'warning'}">
|
|
2193
2538
|
<div class="qa-label">${escapeHtml(item.id)} · ${escapeHtml(item.title)}</div>
|
|
@@ -2226,6 +2571,14 @@ export function renderRegressionArtifact({ task, report }) {
|
|
|
2226
2571
|
summaryCards,
|
|
2227
2572
|
sections,
|
|
2228
2573
|
footer: '',
|
|
2574
|
+
bottomActions: passed
|
|
2575
|
+
? [
|
|
2576
|
+
{ label: '确认通过,继续下一步', tone: 'primary', copyText: `回归报告 ${task.id} 我看过了,全部用例通过,确认继续推进下一步。` },
|
|
2577
|
+
]
|
|
2578
|
+
: [
|
|
2579
|
+
{ label: '先修复未通过的用例', tone: 'danger', copyText: `回归报告 ${task.id} 里还有 ${report.summary.failed} 个用例没通过(${failedCases.map((item) => item.id).join('、')}),请继续修复后重新跑一次验证。` },
|
|
2580
|
+
{ label: '我接受现状,继续下一步', tone: 'neutral', copyText: `回归报告 ${task.id} 的未通过用例我已知晓,先按现状继续推进,后续再处理。` },
|
|
2581
|
+
],
|
|
2229
2582
|
});
|
|
2230
2583
|
}
|
|
2231
2584
|
|