@openprd/cli 0.1.10 → 0.1.18

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 (146) hide show
  1. package/.openprd/benchmarks/index.md +17 -15
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
  4. package/.openprd/changes/adaptive-project-framing/design.md +47 -0
  5. package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
  6. package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
  7. package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
  8. package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
  9. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
  10. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
  11. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
  12. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
  13. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
  14. package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
  15. package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
  16. package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
  17. package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
  18. package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
  19. package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
  20. package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
  21. package/.openprd/changes/openprd/.openprd.yaml +2 -0
  22. package/.openprd/changes/openprd/design.md +46 -0
  23. package/.openprd/changes/openprd/proposal.md +36 -0
  24. package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
  25. package/.openprd/changes/openprd/task-events.jsonl +23 -0
  26. package/.openprd/changes/openprd/tasks.md +242 -0
  27. package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
  28. package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
  29. package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
  30. package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
  31. package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
  32. package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
  33. package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
  34. package/.openprd/changes/test-strategy-router/design.md +65 -0
  35. package/.openprd/changes/test-strategy-router/proposal.md +45 -0
  36. package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
  37. package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
  38. package/.openprd/changes/test-strategy-router/tasks.md +132 -0
  39. package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
  40. package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
  41. package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
  42. package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
  43. package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
  44. package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
  45. package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
  46. package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
  47. package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
  48. package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
  49. package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
  50. package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
  51. package/.openprd/design/README.md +11 -6
  52. package/.openprd/design/README_EN.md +4 -1
  53. package/.openprd/design/active/asset-spec.md +6 -0
  54. package/.openprd/design/active/direction-plan.md +5 -5
  55. package/.openprd/design/active/selected-direction.md +2 -0
  56. package/.openprd/design/anti-slop.md +26 -1
  57. package/.openprd/design/assets/surface-presets.md +6 -0
  58. package/.openprd/design/checklists/ui-quality-gate.md +7 -0
  59. package/.openprd/design/lenses/frontend-lenses.md +28 -0
  60. package/.openprd/design/recipes/content-experience.md +8 -1
  61. package/.openprd/design/recipes/operational-tool.md +7 -0
  62. package/.openprd/design/recipes/product-launch.md +8 -1
  63. package/.openprd/design/templates/README.md +6 -3
  64. package/.openprd/design/templates/README_EN.md +6 -3
  65. package/.openprd/design/templates/ops-dashboard.html +8 -1
  66. package/.openprd/design/templates/product-launch.html +2 -1
  67. package/.openprd/design/themes/theme-catalog.json +23 -5
  68. package/.openprd/engagements/active/prd.md +103 -100
  69. package/.openprd/schema/prd.schema.yaml +15 -0
  70. package/.openprd/standards/config.json +7 -1
  71. package/.openprd/templates/base/intake.md +13 -0
  72. package/.openprd/templates/base/prd.md +15 -0
  73. package/AGENTS.md +14 -4
  74. package/README.md +107 -14
  75. package/README_EN.md +131 -13
  76. package/package.json +1 -1
  77. package/scripts/dev-check-wrapup-copy.mjs +46 -21
  78. package/skills/openprd-benchmark-router/SKILL.md +2 -1
  79. package/skills/openprd-diagram-review/SKILL.md +27 -2
  80. package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  82. package/skills/openprd-frontend-design/SKILL.md +44 -20
  83. package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
  84. package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
  85. package/skills/openprd-harness/SKILL.md +22 -16
  86. package/skills/openprd-quality/SKILL.md +10 -8
  87. package/skills/openprd-requirement-intake/SKILL.md +5 -3
  88. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
  89. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
  90. package/skills/openprd-router/SKILL.md +2 -1
  91. package/skills/openprd-shared/SKILL.md +27 -17
  92. package/skills/openprd-standards/SKILL.md +4 -2
  93. package/src/agent-canonical-content.js +808 -0
  94. package/src/agent-integration.js +259 -778
  95. package/src/brainstorm-artifacts.js +156 -8
  96. package/src/brainstorm-presentation.js +14 -4
  97. package/src/brainstorm.js +165 -2
  98. package/src/canvas-app.html.js +2910 -0
  99. package/src/canvas-i18n.js +274 -0
  100. package/src/canvas-workspace.js +2271 -0
  101. package/src/cli/args.js +72 -4
  102. package/src/cli/basic-print.js +3 -0
  103. package/src/cli/canvas-print.js +106 -0
  104. package/src/cli/doctor-print.js +20 -0
  105. package/src/cli/print.js +2 -0
  106. package/src/cli/quality-commands.js +222 -0
  107. package/src/cli/quality-print.js +37 -5
  108. package/src/cli/run-print.js +5 -0
  109. package/src/cli/workflow-print.js +3 -0
  110. package/src/codex-app-server-relay.js +251 -0
  111. package/src/codex-hook-runner-template.mjs +963 -65
  112. package/src/design-starter-support.js +2 -2
  113. package/src/design-starter.js +39 -3
  114. package/src/dev-standards.js +125 -10
  115. package/src/diagram-core.js +390 -171
  116. package/src/discovery.js +2 -8
  117. package/src/docs-compact.js +125 -0
  118. package/src/execution-strategy.js +1 -1
  119. package/src/fleet.js +9 -3
  120. package/src/guide.js +227 -0
  121. package/src/html-artifacts.js +825 -51
  122. package/src/knowledge.js +101 -20
  123. package/src/language-policy.js +63 -3
  124. package/src/learning-review.js +1 -1
  125. package/src/loop.js +1 -1
  126. package/src/openprd.js +68 -131
  127. package/src/openspec/change-validate.js +2 -2
  128. package/src/openspec/constants.js +6 -7
  129. package/src/openspec/execute.js +2 -2
  130. package/src/openspec/generate.js +318 -102
  131. package/src/openspec/migration.js +168 -0
  132. package/src/openspec/paths.js +1 -22
  133. package/src/prd-core.js +102 -3
  134. package/src/quality-html-artifact.js +30 -5
  135. package/src/quality-visual-review.js +257 -14
  136. package/src/quality.js +66 -18
  137. package/src/review-presentation.js +60 -0
  138. package/src/run-harness.js +41 -3
  139. package/src/session-evidence.js +363 -0
  140. package/src/standards.js +64 -0
  141. package/src/test-strategy.js +7 -4
  142. package/src/visual-compare-centering.js +622 -0
  143. package/src/visual-compare-core.js +1123 -0
  144. package/src/visual-compare.js +620 -698
  145. package/src/workspace-core.js +132 -4
  146. package/src/workspace-workflow.js +172 -10
@@ -18,6 +18,15 @@ function inferVisualReviewMode(value) {
18
18
  if (normalized.includes('visual-parallel-board') || normalized.includes('parallel-board') || normalized.includes('experiment-board')) {
19
19
  return 'parallel-board';
20
20
  }
21
+ if (normalized.includes('visual-verification-board') || normalized.includes('verification-board') || normalized.includes('screenshot-evidence-board') || normalized.includes('screenshot-board')) {
22
+ return 'verification-board';
23
+ }
24
+ if (normalized.includes('visual-alignment-board') || normalized.includes('alignment-board') || normalized.includes('grid-alignment')) {
25
+ return 'alignment-board';
26
+ }
27
+ if (normalized.includes('visual-centering-board') || normalized.includes('centering-board') || normalized.includes('center-board')) {
28
+ return 'centering-board';
29
+ }
21
30
  if (normalized.includes('visual-before-after') || normalized.includes('before-after')) {
22
31
  return 'before-after';
23
32
  }
@@ -73,6 +82,9 @@ export async function listVisualReviewArtifacts(projectRoot) {
73
82
  metadataPath: entry.relativePath,
74
83
  mode: inferredMode,
75
84
  labels: payload.labels ?? existing.labels ?? null,
85
+ alignmentScope: payload.alignmentScope ?? existing.alignmentScope ?? null,
86
+ centering: payload.centering ?? existing.centering ?? null,
87
+ groups: Array.isArray(payload.groups) ? payload.groups : (existing.groups ?? null),
76
88
  generatedAt: payload.generatedAt ?? existing.generatedAt ?? null,
77
89
  mtimeMs: stat?.mtimeMs ?? existing.mtimeMs ?? 0,
78
90
  });
@@ -138,29 +150,186 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
138
150
  'parallel board',
139
151
  'parallel experiment',
140
152
  'experiment board',
141
- 'evidence board',
142
153
  '并行实验',
143
154
  '并行方向',
144
155
  '多方向实验',
145
156
  '方案对比板',
146
- '证据板',
157
+ ];
158
+ const verificationTokens = [
159
+ 'verification board',
160
+ 'verification-board',
161
+ 'screenshot board',
162
+ 'screenshot-evidence-board',
163
+ 'visual-verification-board',
164
+ 'computer screenshot',
165
+ 'computer use screenshot',
166
+ 'browser screenshot',
167
+ 'playwright screenshot',
168
+ 'screencapture',
169
+ '普通截图',
170
+ '实测截图',
171
+ '截图实测',
172
+ '截图证据',
173
+ '截图验收',
174
+ 'Computer 实测',
175
+ 'Browser 实测',
176
+ '运行态截图',
177
+ ];
178
+ const alignmentTokens = [
179
+ 'alignment board',
180
+ 'alignment-board',
181
+ 'grid alignment',
182
+ 'grid-alignment',
183
+ 'baseline',
184
+ 'baseline spread',
185
+ 'same slot',
186
+ 'repeated card',
187
+ 'homogeneous card',
188
+ 'list card',
189
+ 'card grid',
190
+ 'table layout',
191
+ '对齐辅助线',
192
+ '辅助线',
193
+ '网格对齐',
194
+ '基线',
195
+ '坐标偏差',
196
+ '同构',
197
+ '重复单元',
198
+ '相同槽位',
199
+ '相同文案类型',
200
+ '列表卡片',
201
+ '卡片列表',
202
+ '卡片网格',
203
+ '网格卡片',
204
+ '网格列表',
205
+ '列表网格',
206
+ '表格',
207
+ '排版对齐',
208
+ '排版漂移',
209
+ '布局漂移',
210
+ '对齐有问题',
211
+ '对齐问题',
212
+ '对齐不准',
213
+ '对不齐',
214
+ '不对齐',
215
+ '没有对齐',
216
+ '没对齐',
217
+ '左右偏差',
218
+ '上下偏差',
219
+ '横向偏差',
220
+ '竖向偏差',
221
+ ];
222
+ const centeringTokens = [
223
+ 'centering board',
224
+ 'centering-board',
225
+ 'center board',
226
+ 'center-board',
227
+ 'visual-centering-board',
228
+ 'canvas center',
229
+ 'visual centroid',
230
+ 'bbox center',
231
+ 'bounding box center',
232
+ 'active bounds',
233
+ 'single element',
234
+ 'inside centered',
235
+ 'internal centering',
236
+ 'logo center',
237
+ 'logo centered',
238
+ 'logo internal',
239
+ 'icon center',
240
+ 'icon centered',
241
+ 'icon internal',
242
+ 'avatar center',
243
+ 'badge center',
244
+ 'button graphic',
245
+ '内部居中',
246
+ '内部不居中',
247
+ '居中判定',
248
+ '居中识别',
249
+ '居中评估',
250
+ '视觉重心',
251
+ '主体外接框',
252
+ '外接框中心',
253
+ '画布中心',
254
+ '单元素',
255
+ '单个元素',
256
+ '单一元素',
257
+ '图标内部',
258
+ '图片内部',
259
+ '素材内部',
260
+ '按钮图形',
261
+ '偏心',
262
+ '不居中',
263
+ '没居中',
264
+ '左偏',
265
+ '右偏',
266
+ '上飘',
267
+ '下沉',
268
+ ];
269
+ const contentSlotAlignmentTokens = [
270
+ 'content slot',
271
+ 'internal slot',
272
+ 'same slot',
273
+ 'title slot',
274
+ 'price slot',
275
+ 'button slot',
276
+ 'card content',
277
+ 'repeated card',
278
+ 'homogeneous card',
279
+ 'list card',
280
+ 'card grid',
281
+ '标题',
282
+ '副标题',
283
+ '描述',
284
+ '正文',
285
+ '标签',
286
+ '状态',
287
+ '价格',
288
+ '按钮',
289
+ '操作区',
290
+ '图标',
291
+ '内容槽位',
292
+ '内部槽位',
293
+ '相同槽位',
294
+ '相同文案类型',
295
+ '相同组件槽位',
296
+ '列表卡片',
297
+ '卡片列表',
298
+ '卡片网格',
299
+ '网格卡片',
300
+ '网格列表',
301
+ '列表网格',
147
302
  ];
148
303
  const expectsReferenceCompare = includesAny(haystack, referenceTokens);
149
304
  const expectsFocusBoard = includesAny(haystack, focusTokens);
150
305
  const expectsParallelBoard = includesAny(haystack, parallelTokens);
306
+ const expectsVerificationBoard = includesAny(haystack, verificationTokens);
307
+ const expectsAlignmentBoard = includesAny(haystack, alignmentTokens);
308
+ const expectsCenteringBoard = includesAny(haystack, centeringTokens);
309
+ const expectsContentSlotAlignment = expectsAlignmentBoard && includesAny(haystack, contentSlotAlignmentTokens);
151
310
  const referenceArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'reference-actual');
152
311
  const beforeAfterArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'before-after');
153
312
  const focusArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'focus-board');
154
313
  const parallelArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'parallel-board');
314
+ const verificationArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'verification-board');
315
+ const alignmentArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'alignment-board');
316
+ const centeringArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'centering-board');
317
+ const contentSlotAlignmentArtifacts = alignmentArtifacts.filter(hasContentSlotAlignmentEvidence);
155
318
  let matchingArtifacts;
156
- if (expectsParallelBoard) {
319
+ if (expectsCenteringBoard) {
320
+ matchingArtifacts = centeringArtifacts;
321
+ } else if (expectsAlignmentBoard) {
322
+ matchingArtifacts = alignmentArtifacts;
323
+ } else if (expectsVerificationBoard) {
324
+ matchingArtifacts = verificationArtifacts;
325
+ } else if (expectsParallelBoard) {
157
326
  matchingArtifacts = parallelArtifacts;
158
327
  } else if (expectsFocusBoard) {
159
328
  matchingArtifacts = focusArtifacts;
160
329
  } else if (expectsReferenceCompare) {
161
330
  matchingArtifacts = referenceArtifacts;
162
331
  } else {
163
- matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts];
332
+ matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts, ...verificationArtifacts, ...alignmentArtifacts, ...centeringArtifacts];
164
333
  }
165
334
  const evidenceSources = matchingArtifacts.slice(0, 12).map((artifact) => ({
166
335
  path: artifact.path,
@@ -170,15 +339,31 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
170
339
  ? 'visual-review/before-after'
171
340
  : artifact.mode === 'focus-board'
172
341
  ? 'visual-review/focus-board'
173
- : 'visual-review/parallel-board',
342
+ : artifact.mode === 'parallel-board'
343
+ ? 'visual-review/parallel-board'
344
+ : artifact.mode === 'verification-board'
345
+ ? 'visual-review/verification-board'
346
+ : artifact.mode === 'alignment-board'
347
+ ? 'visual-review/alignment-board'
348
+ : 'visual-review/centering-board',
174
349
  }));
175
350
  const warnings = [];
351
+ const missingContentSlotAlignment = relevant
352
+ && expectsContentSlotAlignment
353
+ && alignmentArtifacts.length > 0
354
+ && contentSlotAlignmentArtifacts.length === 0;
176
355
 
177
356
  if (relevant && matchingArtifacts.length === 0) {
178
357
  warnings.push(
179
- expectsParallelBoard
358
+ expectsCenteringBoard
359
+ ? '检测到 logo、图标、头像、徽标、图片或按钮图形等单个元素的内部居中/视觉重心语义,但未看到本次内部居中证据板。单张截图或主观“看起来居中”不能替代画布中心、主体外接框和视觉重心偏移量测。'
360
+ : expectsAlignmentBoard
361
+ ? '检测到同构列表、卡片、网格、表格或对齐反馈语义,但未看到本次对齐辅助线证据板。新功能开发中只要有重复同构单元,也应同时量测容器轨道和内部内容槽位的坐标偏差。'
362
+ : expectsParallelBoard
180
363
  ? '检测到界面视觉改动且用户在比较多方向实验,但未看到本次并行实验证据板。'
181
- : expectsFocusBoard
364
+ : expectsVerificationBoard
365
+ ? '检测到普通截图、Computer/Browser 实测或运行态截图语义,但未看到本次截图实测证据板。普通截图只能作为原始素材,不能单独替代视觉收口拼图。'
366
+ : expectsFocusBoard
182
367
  ? '检测到界面视觉改动且用户在关注局部细节,但未看到本次局部焦点证据板。'
183
368
  : expectsReferenceCompare
184
369
  ? '检测到界面视觉改动且已有参考图/设计稿语义,但未看到本次“效果图 / 实现截图”对比证据。'
@@ -190,24 +375,46 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
190
375
  warnings.push('当前有视觉证据,但局部细节仍建议补一份局部焦点证据板,方便围绕编号区域复核。');
191
376
  } else if (relevant && expectsParallelBoard && parallelArtifacts.length === 0 && matchingArtifacts.length > 0) {
192
377
  warnings.push('当前有视觉证据,但多方向实验仍建议补一份并行实验证据板,把方案和指标放到同一板里审查。');
378
+ } else if (relevant && expectsVerificationBoard && verificationArtifacts.length === 0 && matchingArtifacts.length > 0) {
379
+ warnings.push('当前有视觉证据,但普通截图或 Computer 实测仍建议补一份截图实测证据板,把截图、实测路径和检查点拼到同一张图里。');
380
+ } else if (relevant && expectsAlignmentBoard && alignmentArtifacts.length === 0 && matchingArtifacts.length > 0) {
381
+ warnings.push('当前有视觉证据,但同构列表、卡片、网格或表格仍建议补一份对齐辅助线证据板,把相同槽位的 spread 放到同一张图里。');
382
+ } else if (relevant && expectsCenteringBoard && centeringArtifacts.length === 0 && matchingArtifacts.length > 0) {
383
+ warnings.push('当前有视觉证据,但单元素内部居中仍建议补一份内部居中证据板,把画布中心、主体外接框和视觉重心偏移放到同一张图里。');
384
+ } else if (missingContentSlotAlignment) {
385
+ warnings.push('当前只看到对齐辅助线证据板,但没有看到标题、副标题、描述、标签、状态、价格、按钮或图标等内部内容槽位量测。卡片/列表/网格不能只量外框、列宽或行顶。');
193
386
  }
194
387
 
195
388
  const summary = !relevant
196
389
  ? '当前场景未要求视觉评审证据'
197
390
  : matchingArtifacts.length > 0
198
- ? (
199
- expectsParallelBoard
391
+ ? (
392
+ missingContentSlotAlignment
393
+ ? '已找到对齐辅助线证据板,但缺少内部内容槽位量测'
394
+ : expectsCenteringBoard
395
+ ? `已找到 ${matchingArtifacts.length} 份内部居中证据板`
396
+ : expectsAlignmentBoard
397
+ ? `已找到 ${matchingArtifacts.length} 份对齐辅助线证据板`
398
+ : expectsParallelBoard
200
399
  ? `已找到 ${matchingArtifacts.length} 份并行实验证据板`
201
- : expectsFocusBoard
400
+ : expectsVerificationBoard
401
+ ? `已找到 ${matchingArtifacts.length} 份截图实测证据板`
402
+ : expectsFocusBoard
202
403
  ? `已找到 ${matchingArtifacts.length} 份局部焦点证据板`
203
404
  : expectsReferenceCompare
204
405
  ? `已找到 ${matchingArtifacts.length} 份效果图 / 实现截图对比证据`
205
- : `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点或修改前后自检证据`
406
+ : `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点、截图实测或修改前后自检证据`
206
407
  )
207
408
  : (
208
- expectsParallelBoard
409
+ expectsCenteringBoard
410
+ ? '未找到本次内部居中证据板'
411
+ : expectsAlignmentBoard
412
+ ? '未找到本次对齐辅助线证据板'
413
+ : expectsParallelBoard
209
414
  ? '未找到本次并行实验证据板'
210
- : expectsFocusBoard
415
+ : expectsVerificationBoard
416
+ ? '未找到本次截图实测证据板'
417
+ : expectsFocusBoard
211
418
  ? '未找到本次局部焦点证据板'
212
419
  : expectsReferenceCompare
213
420
  ? '未找到本次效果图 / 实现截图对比证据'
@@ -215,13 +422,19 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
215
422
  );
216
423
 
217
424
  return {
218
- status: !relevant || matchingArtifacts.length > 0 ? 'pass' : 'needs-evidence',
425
+ status: !relevant || (matchingArtifacts.length > 0 && !missingContentSlotAlignment) ? 'pass' : 'needs-evidence',
219
426
  relevant,
220
427
  expectsReferenceCompare,
221
428
  expectsFocusBoard,
222
429
  expectsParallelBoard,
430
+ expectsVerificationBoard,
431
+ expectsAlignmentBoard,
432
+ expectsCenteringBoard,
433
+ expectsContentSlotAlignment,
223
434
  artifacts: visualArtifacts,
224
435
  matchingArtifacts,
436
+ contentSlotAlignmentArtifacts,
437
+ centeringArtifacts,
225
438
  warnings,
226
439
  evidence: {
227
440
  present: matchingArtifacts.length > 0,
@@ -231,6 +444,36 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
231
444
  };
232
445
  }
233
446
 
447
+ function hasContentSlotAlignmentEvidence(artifact) {
448
+ const haystack = JSON.stringify([
449
+ artifact.path,
450
+ artifact.metadataPath,
451
+ artifact.labels,
452
+ artifact.alignmentScope,
453
+ artifact.groups,
454
+ ]).toLowerCase();
455
+ return [
456
+ 'content-slot',
457
+ 'content slot',
458
+ 'internal slot',
459
+ '内容槽位',
460
+ '内部槽位',
461
+ '相同槽位',
462
+ '相同文案类型',
463
+ '相同组件槽位',
464
+ '标题',
465
+ '副标题',
466
+ '描述',
467
+ '正文',
468
+ '标签',
469
+ '状态',
470
+ '价格',
471
+ '按钮',
472
+ '操作区',
473
+ '图标',
474
+ ].some((token) => haystack.includes(String(token).toLowerCase()));
475
+ }
476
+
234
477
  export {
235
478
  VISUAL_REVIEW_DIR,
236
479
  VISUAL_REVIEW_SCHEMA,
package/src/quality.js CHANGED
@@ -108,13 +108,20 @@ const EVIDENCE_TOKENS = {
108
108
  'test-strategy': ['test-layer', 'test-size', 'test-scope', 'evidence-plan', 'testing pyramid', '测试策略', '测试分流', '单元测试', '集成测试', '端到端'],
109
109
  smoke: ['smoke', 'e2e', 'playwright', 'cypress', 'main flow', 'happy path', '冒烟', '主流程'],
110
110
  'feature-coverage': ['feature coverage', 'acceptance', 'tasks done', 'openprd tasks', '验收', '功能覆盖', '任务完成'],
111
- 'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板'],
111
+ 'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'visual-verification-board', 'visual-alignment-board', 'visual-centering-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', 'verification-board', 'alignment-board', 'centering-board', 'center-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板', '截图实测证据板', '对齐辅助线证据板', '内部居中证据板', '内容槽位', '内部槽位', '视觉重心', '主体外接框', '画布中心', '偏心'],
112
112
  'normal-performance': ['performance', 'perf', 'benchmark', 'latency', 'p95', 'lighthouse', 'k6', '性能', '耗时'],
113
113
  'extreme-performance': ['extreme', 'stress', 'load test', 'large-data', 'pressure', 'k6', '压力', '极端', '大数据'],
114
114
  knowledge: ['quality learn', 'incident', 'pattern', 'skill', '复盘', '经验', '沉淀'],
115
115
  growth: ['growth ledger', 'completion checkpoint', 'openprd grow', 'workflow-gotcha', 'code-extension', '自我成长', '账本', '候选'],
116
116
  };
117
117
 
118
+ const DIAGNOSTIC_SURFACE_LABELS = {
119
+ 'runtime-events': '运行事件记录',
120
+ timeline: '问题时间线',
121
+ 'root-cause-candidates': '根因排查线索',
122
+ 'diagnostic-report': '诊断报告',
123
+ };
124
+
118
125
  function qualityPath(projectRoot, relativePath) {
119
126
  return cjoin(projectRoot, relativePath);
120
127
  }
@@ -246,6 +253,10 @@ function defaultQualityConfig() {
246
253
  ledgerPath: OPENPRD_GROWTH_LEDGER,
247
254
  completionCheckpointRequired: true,
248
255
  },
256
+ reports: {
257
+ retentionMaxReports: 60,
258
+ retentionEnabled: true,
259
+ },
249
260
  };
250
261
  }
251
262
 
@@ -294,6 +305,10 @@ function normalizeQualityConfig(config = {}) {
294
305
  ...defaults.growth,
295
306
  ...(config.growth ?? {}),
296
307
  },
308
+ reports: {
309
+ ...defaults.reports,
310
+ ...(config.reports ?? {}),
311
+ },
297
312
  };
298
313
  }
299
314
 
@@ -435,8 +450,7 @@ async function readActiveChangeContext(projectRoot, activeChange) {
435
450
  return { activeChange: null, files: [], text: '' };
436
451
  }
437
452
  const roots = [
438
- cjoin('openprd', 'changes', activeChange),
439
- cjoin('openspec', 'changes', activeChange),
453
+ cjoin('.openprd', 'changes', activeChange),
440
454
  ];
441
455
  const files = [];
442
456
  async function walk(root, dir) {
@@ -606,18 +620,21 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
606
620
  };
607
621
  }
608
622
  if (observability.status === 'pass') {
609
- const observabilitySignals = [
610
- ...observability.centralizedTools,
611
- ...(observability.diagnosticSurfaces ?? []),
612
- ];
623
+ const toolCount = observability.centralizedTools.length;
624
+ const surfaceLabels = (observability.diagnosticSurfaces ?? [])
625
+ .map((surface) => DIAGNOSTIC_SURFACE_LABELS[surface] ?? surface);
626
+ const signalSummary = [
627
+ toolCount > 0 ? `${toolCount} 类日志或追踪工具` : '',
628
+ surfaceLabels.length > 0 ? `${surfaceLabels.length} 类诊断记录` : '',
629
+ ].filter(Boolean).join('和');
613
630
  ledger.traceability = {
614
631
  ...ledger.traceability,
615
632
  present: true,
616
633
  sources: [
617
634
  ...ledger.traceability.sources,
618
- { path: 'project-observability-signals', source: observabilitySignals.join(', ') || 'observability' },
635
+ { path: '项目内的日志与追踪配置', source: signalSummary ? `检测到${signalSummary}` : '日志追踪检查' },
619
636
  ].slice(0, 12),
620
- summary: `检测到 ${observability.correlationFields.length} 个链路关联字段${observability.diagnosticSurfaces?.length ? `;诊断面: ${observability.diagnosticSurfaces.join(', ')}` : ''}`,
637
+ summary: `出问题时可以追查:检测到 ${observability.correlationFields.length} 个追踪线索${surfaceLabels.length > 0 ? `,并留有${surfaceLabels.join('')}` : ''}`,
621
638
  };
622
639
  }
623
640
  if (!businessGuardrails.riskDetected || businessGuardrails.status === 'pass') {
@@ -641,8 +658,8 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
641
658
  ...knowledge.candidates.slice(0, 3).map((candidate) => ({ path: candidate, source: 'openprd-knowledge-candidate' })),
642
659
  ].slice(0, 12),
643
660
  summary: knowledge.candidates.length > 0
644
- ? `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0},另有 ${knowledge.candidates.length} 个待确认 candidate`
645
- : `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0}`,
661
+ ? `已沉淀 ${knowledge.skills.length} 条项目经验,近期被实际复用 ${knowledge.adoption?.totals?.referenced ?? 0} 次,另有 ${knowledge.candidates.length} 条经验草案等确认`
662
+ : `已沉淀 ${knowledge.skills.length} 条项目经验,近期被实际复用 ${knowledge.adoption?.totals?.referenced ?? 0} 次`,
646
663
  };
647
664
  } else if (knowledge.candidates.length > 0) {
648
665
  ledger.knowledge = {
@@ -652,7 +669,7 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
652
669
  ...ledger.knowledge.sources,
653
670
  ...knowledge.candidates.slice(0, 6).map((candidate) => ({ path: candidate, source: 'openprd-knowledge-candidate' })),
654
671
  ].slice(0, 12),
655
- summary: `已有 ${knowledge.candidates.length} 个待确认 knowledge candidate`,
672
+ summary: `已有 ${knowledge.candidates.length} 条经验草案等确认,确认后会成为可复用的项目经验`,
656
673
  };
657
674
  }
658
675
  if (growth?.summary) {
@@ -666,7 +683,7 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
666
683
  ...ledger.growth.sources,
667
684
  { path: growth.ledgerPath ?? OPENPRD_GROWTH_LEDGER, source: 'openprd-growth-ledger' },
668
685
  ].slice(0, 12),
669
- summary: `growth 账本已有 ${growth.summary.eventCount ?? 0} 条事件,其中 completion checkpoint ${completionCheckpoints} 条`,
686
+ summary: `成长记录已有 ${growth.summary.eventCount ?? 0} 条,其中收尾检查记录 ${completionCheckpoints} 条`,
670
687
  };
671
688
  }
672
689
  }
@@ -894,7 +911,9 @@ async function readActiveTasks(projectRoot) {
894
911
  return { activeChange: null, total: 0, done: 0, pending: 0, blocked: 0, tasks: [] };
895
912
  }
896
913
  const taskFiles = [];
897
- for (const root of [cjoin('openprd', 'changes', activeChange), cjoin('openspec', 'changes', activeChange)]) {
914
+ for (const root of [
915
+ cjoin('.openprd', 'changes', activeChange),
916
+ ]) {
898
917
  const dir = cjoin(projectRoot, root);
899
918
  const entries = await fs.readdir(dir).catch(() => []);
900
919
  for (const entry of entries) {
@@ -1273,7 +1292,35 @@ async function buildQualityReport(projectRoot, config, options = {}) {
1273
1292
  };
1274
1293
  }
1275
1294
 
1276
- async function writeReport(projectRoot, report) {
1295
+ export async function pruneQualityReports(projectRoot, retainedReports, config) {
1296
+ const retention = config?.reports ?? {};
1297
+ if (retention.retentionEnabled === false) {
1298
+ return { pruned: 0 };
1299
+ }
1300
+ const maxReports = Number.isInteger(retention.retentionMaxReports) && retention.retentionMaxReports > 0
1301
+ ? retention.retentionMaxReports
1302
+ : 60;
1303
+ const reportsDir = qualityPath(projectRoot, QUALITY_REPORTS_DIR);
1304
+ const entries = await fs.readdir(reportsDir).catch(() => []);
1305
+ const keepBases = new Set(
1306
+ retainedReports.slice(0, maxReports).map((item) => String(item.reportId ?? '').replace(/[^a-zA-Z0-9._-]/g, '_')),
1307
+ );
1308
+ let pruned = 0;
1309
+ for (const name of entries) {
1310
+ const match = name.match(/^(eval-[a-zA-Z0-9._-]+)\.(json|html)$/);
1311
+ if (!match) {
1312
+ continue;
1313
+ }
1314
+ if (keepBases.has(match[1])) {
1315
+ continue;
1316
+ }
1317
+ await fs.rm(cjoin(reportsDir, name), { force: true }).catch(() => {});
1318
+ pruned += 1;
1319
+ }
1320
+ return { pruned };
1321
+ }
1322
+
1323
+ async function writeReport(projectRoot, report, config = null) {
1277
1324
  await ensureQualityDirs(projectRoot);
1278
1325
  const reportBase = report.id.replace(/[^a-zA-Z0-9._-]/g, '_');
1279
1326
  const jsonPath = qualityPath(projectRoot, cjoin(QUALITY_REPORTS_DIR, `${reportBase}.json`));
@@ -1294,7 +1341,8 @@ async function writeReport(projectRoot, report) {
1294
1341
  ...(Array.isArray(index.reports) ? index.reports.filter((item) => item.reportId !== report.id) : []),
1295
1342
  ].slice(0, 100);
1296
1343
  await writeJson(indexPath, { version: 1, updatedAt: timestamp(), reports });
1297
- return { jsonPath, htmlPath, indexPath };
1344
+ const retention = await pruneQualityReports(projectRoot, reports, config ?? normalizeQualityConfig());
1345
+ return { jsonPath, htmlPath, indexPath, prunedReports: retention.pruned };
1298
1346
  }
1299
1347
 
1300
1348
  export async function initQualityWorkspace(projectRoot, options = {}) {
@@ -1339,7 +1387,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1339
1387
  await ensureQualityDirs(projectRoot);
1340
1388
  const initialReport = await buildQualityReport(projectRoot, config);
1341
1389
  let report = initialReport;
1342
- let paths = await writeReport(projectRoot, report);
1390
+ let paths = await writeReport(projectRoot, report, config);
1343
1391
  const knowledgeSignal = {
1344
1392
  kind: 'quality-verify',
1345
1393
  ok: report.readiness.productionReady,
@@ -1398,7 +1446,7 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1398
1446
  reason: 'reusable-knowledge-artifact-already-exists',
1399
1447
  };
1400
1448
  report = await buildQualityReport(projectRoot, config, { reportId: initialReport.id });
1401
- paths = await writeReport(projectRoot, report);
1449
+ paths = await writeReport(projectRoot, report, config);
1402
1450
  const strict = options.strict === true;
1403
1451
  const blocking = (strict || config.enforcement === 'blocking') && !report.readiness.productionReady;
1404
1452
  const featureCoverageOnly = report.readiness.failingGates.length === 0
@@ -48,6 +48,32 @@ export const REVIEW_PRESENTATION_TEMPLATE = {
48
48
  USER_CHANGE_SUMMARY_GUIDE.panelExamples.risk,
49
49
  ],
50
50
  },
51
+ designDirections: {
52
+ note: '可选;只有需求涉及界面/页面/交互时提供。先用 imagegen 生成 3 个差异足够大的交互稿,把图片放到 .openprd/design/active/ 后写入路径;不涉及界面时删除本字段。',
53
+ directions: [
54
+ {
55
+ id: 'direction-1',
56
+ title: '15 字以内方向名',
57
+ image: '.openprd/design/active/direction-1.png',
58
+ summary: '30 字以内一句话说明这个方向的核心体验',
59
+ tradeoff: '30 字以内说明取舍或适用场景',
60
+ },
61
+ {
62
+ id: 'direction-2',
63
+ title: '15 字以内方向名',
64
+ image: '.openprd/design/active/direction-2.png',
65
+ summary: '30 字以内一句话说明这个方向的核心体验',
66
+ tradeoff: '30 字以内说明取舍或适用场景',
67
+ },
68
+ {
69
+ id: 'direction-3',
70
+ title: '15 字以内方向名',
71
+ image: '.openprd/design/active/direction-3.png',
72
+ summary: '30 字以内一句话说明这个方向的核心体验',
73
+ tradeoff: '30 字以内说明取舍或适用场景',
74
+ },
75
+ ],
76
+ },
51
77
  };
52
78
 
53
79
  export function buildReviewPresentationTemplatePayload() {
@@ -182,6 +208,39 @@ function stableStringify(value) {
182
208
  return JSON.stringify(value);
183
209
  }
184
210
 
211
+ const ACTIVE_ARTIFACT_RETAIN_VERSIONS = 12;
212
+
213
+ export async function archiveStaleActiveArtifacts(projectRoot, options = {}) {
214
+ const retain = Number.isInteger(options.retain) && options.retain > 0
215
+ ? options.retain
216
+ : ACTIVE_ARTIFACT_RETAIN_VERSIONS;
217
+ const activeDir = path.join(projectRoot, '.openprd', 'artifacts', 'active');
218
+ const archiveDir = path.join(projectRoot, '.openprd', 'artifacts', 'archive');
219
+ const entries = await fs.readdir(activeDir, { withFileTypes: true }).catch(() => []);
220
+ const versioned = entries
221
+ .filter((entry) => entry.isDirectory() && /^v\d{4}-/.test(entry.name))
222
+ .map((entry) => entry.name)
223
+ .sort();
224
+ const stale = versioned.slice(0, Math.max(0, versioned.length - retain));
225
+ if (stale.length === 0) {
226
+ return { archived: [] };
227
+ }
228
+ await fs.mkdir(archiveDir, { recursive: true });
229
+ const archived = [];
230
+ for (const name of stale) {
231
+ const from = path.join(activeDir, name);
232
+ const to = path.join(archiveDir, name);
233
+ try {
234
+ await fs.rm(to, { recursive: true, force: true });
235
+ await fs.rename(from, to);
236
+ archived.push(name);
237
+ } catch {
238
+ // 归档失败不阻断评审生成
239
+ }
240
+ }
241
+ return { archived };
242
+ }
243
+
185
244
  async function renderValidatedReviewPresentation(projectRoot, snapshot) {
186
245
  const workspaceRoot = path.join(projectRoot, '.openprd');
187
246
  const canonicalReview = canonicalReviewPath({ workspaceRoot }, snapshot.versionId);
@@ -191,6 +250,7 @@ async function renderValidatedReviewPresentation(projectRoot, snapshot) {
191
250
  snapshot,
192
251
  projectRelease: buildReleaseLedgerSummary(releaseLedger.ledger),
193
252
  }));
253
+ await archiveStaleActiveArtifacts(projectRoot).catch(() => null);
194
254
 
195
255
  const versionIndexPath = path.join(workspaceRoot, 'state', 'version-index.json');
196
256
  const versionIndex = await readJson(versionIndexPath);