@openprd/cli 0.1.9 → 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.
Files changed (138) hide show
  1. package/.openprd/benchmarks/index.md +16 -0
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/config.yaml +10 -0
  4. package/.openprd/design/README.md +47 -0
  5. package/.openprd/design/README_EN.md +25 -0
  6. package/.openprd/design/active/asset-spec.md +15 -0
  7. package/.openprd/design/active/direction-plan.md +7 -0
  8. package/.openprd/design/active/facts-sheet.md +13 -0
  9. package/.openprd/design/active/image-preflight.md +9 -0
  10. package/.openprd/design/active/selected-direction.md +11 -0
  11. package/.openprd/design/anti-slop.md +16 -0
  12. package/.openprd/design/assets/dark-mesh.svg +14 -0
  13. package/.openprd/design/assets/paper-grid.svg +14 -0
  14. package/.openprd/design/assets/surface-presets.md +21 -0
  15. package/.openprd/design/assets/workbench-grid.svg +16 -0
  16. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  17. package/.openprd/design/components/component-catalog.md +42 -0
  18. package/.openprd/design/layouts/layout-catalog.json +45 -0
  19. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  20. package/.openprd/design/recipes/content-experience.md +26 -0
  21. package/.openprd/design/recipes/operational-tool.md +25 -0
  22. package/.openprd/design/recipes/product-launch.md +26 -0
  23. package/.openprd/design/templates/README.md +36 -0
  24. package/.openprd/design/templates/README_EN.md +31 -0
  25. package/.openprd/design/templates/content-home.html +415 -0
  26. package/.openprd/design/templates/ops-dashboard.html +340 -0
  27. package/.openprd/design/templates/product-launch.html +360 -0
  28. package/.openprd/design/themes/theme-catalog.json +77 -0
  29. package/.openprd/engagements/active/prd.md +111 -100
  30. package/.openprd/schema/prd.schema.yaml +25 -0
  31. package/.openprd/templates/base/intake.md +17 -0
  32. package/.openprd/templates/base/prd.md +31 -6
  33. package/AGENTS.md +10 -7
  34. package/README.md +26 -28
  35. package/README_EN.md +3 -3
  36. package/package.json +41 -2
  37. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  38. package/skills/openprd-benchmark-router/SKILL.md +5 -0
  39. package/skills/openprd-frontend-design/SKILL.md +161 -0
  40. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  41. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  42. package/skills/openprd-harness/SKILL.md +37 -20
  43. package/skills/openprd-learning-review/SKILL.md +2 -1
  44. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  45. package/skills/openprd-quality/SKILL.md +5 -3
  46. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  47. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  48. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  49. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +108 -0
  50. package/skills/openprd-router/SKILL.md +13 -5
  51. package/skills/openprd-shared/SKILL.md +38 -25
  52. package/src/agent-integration.js +120 -69
  53. package/src/brainstorm-artifacts.js +1805 -0
  54. package/src/brainstorm-presentation.js +233 -0
  55. package/src/brainstorm.js +766 -0
  56. package/src/cli/args.js +51 -3
  57. package/src/cli/doctor-print.js +34 -8
  58. package/src/cli/print.js +6 -0
  59. package/src/cli/quality-print.js +79 -3
  60. package/src/cli/run-print.js +43 -1
  61. package/src/cli/shared-print.js +33 -2
  62. package/src/cli/workflow-print.js +21 -0
  63. package/src/codex-hook-runner-template.mjs +838 -58
  64. package/src/design-starter-support.js +462 -0
  65. package/src/design-starter.js +1207 -0
  66. package/src/diagram-core.js +384 -151
  67. package/src/html-artifacts.js +403 -50
  68. package/src/knowledge.js +291 -32
  69. package/src/learning-review.js +3 -3
  70. package/src/loop.js +839 -101
  71. package/src/openprd.js +86 -1
  72. package/src/openspec/change-validate.js +1 -0
  73. package/src/prd-core.js +119 -2
  74. package/src/quality-html-artifact.js +30 -5
  75. package/src/quality-learning.js +50 -6
  76. package/src/quality.js +22 -12
  77. package/src/run-harness.js +191 -29
  78. package/src/self-update.js +301 -4
  79. package/src/standards.js +46 -5
  80. package/src/visual-prepare.js +940 -0
  81. package/src/workspace-core.js +175 -0
  82. package/src/workspace-workflow.js +307 -7
  83. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  84. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  85. package/.openprd/discovery/config.json +0 -35
  86. package/.openprd/engagements/active/flows.md +0 -35
  87. package/.openprd/engagements/active/handoff.md +0 -16
  88. package/.openprd/engagements/active/review.html +0 -61
  89. package/.openprd/engagements/active/roles.md +0 -22
  90. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  91. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  92. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  93. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  94. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  95. package/.openprd/exports/.gitkeep +0 -0
  96. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  97. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  98. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  99. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  100. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  101. package/.openprd/knowledge/index.json +0 -47
  102. package/.openprd/reviews/v0001.html +0 -1322
  103. package/.openprd/reviews/v0002.html +0 -1150
  104. package/.openprd/reviews/v0003.html +0 -1150
  105. package/.openprd/reviews/v0004.html +0 -1150
  106. package/.openprd/reviews/v0005.html +0 -1150
  107. package/.openprd/sessions/.gitkeep +0 -0
  108. package/.openprd/state/changes.json +0 -27
  109. package/.openprd/state/current.json +0 -505
  110. package/.openprd/state/release-ledger.json +0 -387
  111. package/.openprd/state/version-index.json +0 -67
  112. package/.openprd/state/versions/.gitkeep +0 -0
  113. package/.openprd/state/versions/v0001.json +0 -121
  114. package/.openprd/state/versions/v0001.md +0 -161
  115. package/.openprd/state/versions/v0002.json +0 -264
  116. package/.openprd/state/versions/v0002.md +0 -183
  117. package/.openprd/state/versions/v0003.json +0 -269
  118. package/.openprd/state/versions/v0003.md +0 -188
  119. package/.openprd/state/versions/v0004.json +0 -274
  120. package/.openprd/state/versions/v0004.md +0 -193
  121. package/.openprd/state/versions/v0005.json +0 -299
  122. package/.openprd/state/versions/v0005.md +0 -189
  123. package/docs/assets/openprd-capability-overview-en.png +0 -0
  124. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  125. package/docs/assets/openprd-learning-html.png +0 -0
  126. package/docs/assets/openprd-quality-html.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  128. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  129. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  130. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  131. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  132. package/docs/assets/openprd-review-html.png +0 -0
  133. package/docs/assets/openprd-scenario-overview.png +0 -0
  134. package/docs/assets/openprd-scenario-overview.svg +0 -114
  135. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  136. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  137. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  138. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -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: #f7f4ed;
33
- --panel: rgba(255,255,255,0.88);
34
- --text: #1f2937;
35
- --muted: #6b7280;
36
- --line: rgba(31,41,55,0.12);
37
- --accent: #d97706;
38
- --accent-soft: rgba(217,119,6,0.12);
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: rgba(255,255,255,0.72);
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(34px, 5vw, 56px);
133
- line-height: 1;
134
- font-family: var(--serif);
135
- font-weight: 600;
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: 20px;
161
+ border-radius: 16px;
165
162
  background: var(--panel);
166
- backdrop-filter: blur(8px);
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: 30px;
182
- line-height: 1.1;
183
- font-family: var(--serif);
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: rgba(255,255,255,0.72);
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: rgba(255,255,255,0.7);
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
- background: #161b22;
281
- color: #e5e7eb;
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 panels = [
1271
- renderReviewPanel({
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
- renderReviewPanel({
1417
+ },
1418
+ {
1280
1419
  kind: 'function',
1281
1420
  title: '功能与约束',
1282
1421
  description: '区分必须交付、非功能要求和当前依赖假设',
1283
1422
  emptyText: '暂无功能、非功能要求或依赖约束。',
1284
1423
  items: functionPanelItems,
1285
- }),
1286
- renderReviewPanel({
1424
+ },
1425
+ {
1287
1426
  kind: 'guardrail',
1288
1427
  title: '业务成本与滥用护栏',
1289
1428
  description: '涉及免费额度、消耗型成本或第三方调用时,先确认限制、报警和止损动作',
1290
1429
  emptyText: '暂无业务成本或滥用护栏。',
1291
1430
  items: guardrailPanelItems,
1292
- }),
1293
- renderReviewPanel({
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: block;
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: #fffaf0;
2278
+ --bg: #f6f8fb;
1949
2279
  --panel: #ffffff;
1950
- --line: rgba(15,23,42,0.12);
1951
- --text: #1f2937;
1952
- --muted: #6b7280;
1953
- --accent: #0f766e;
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: #0f766e; color: #fff; border-color: #0f766e; }
1971
- pre { margin: 0; border-radius: 14px; background: #111827; color: #e5e7eb; padding: 14px; overflow: auto; white-space: pre-wrap; line-height: 1.6; font-size: 13px; }
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: rgba(255,255,255,0.85);
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