@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.
- package/.openprd/benchmarks/index.md +17 -15
- package/.openprd/benchmarks/sources.yaml +53 -1
- package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
- package/.openprd/changes/adaptive-project-framing/design.md +47 -0
- package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
- package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
- package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
- package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
- package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
- package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
- package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
- package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
- package/.openprd/changes/openprd/.openprd.yaml +2 -0
- package/.openprd/changes/openprd/design.md +46 -0
- package/.openprd/changes/openprd/proposal.md +36 -0
- package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd/task-events.jsonl +23 -0
- package/.openprd/changes/openprd/tasks.md +242 -0
- package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
- package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
- package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
- package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
- package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
- package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
- package/.openprd/changes/test-strategy-router/design.md +65 -0
- package/.openprd/changes/test-strategy-router/proposal.md +45 -0
- package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
- package/.openprd/changes/test-strategy-router/tasks.md +132 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
- package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
- package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
- package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
- package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
- package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
- package/.openprd/design/README.md +11 -6
- package/.openprd/design/README_EN.md +4 -1
- package/.openprd/design/active/asset-spec.md +6 -0
- package/.openprd/design/active/direction-plan.md +5 -5
- package/.openprd/design/active/selected-direction.md +2 -0
- package/.openprd/design/anti-slop.md +26 -1
- package/.openprd/design/assets/surface-presets.md +6 -0
- package/.openprd/design/checklists/ui-quality-gate.md +7 -0
- package/.openprd/design/lenses/frontend-lenses.md +28 -0
- package/.openprd/design/recipes/content-experience.md +8 -1
- package/.openprd/design/recipes/operational-tool.md +7 -0
- package/.openprd/design/recipes/product-launch.md +8 -1
- package/.openprd/design/templates/README.md +6 -3
- package/.openprd/design/templates/README_EN.md +6 -3
- package/.openprd/design/templates/ops-dashboard.html +8 -1
- package/.openprd/design/templates/product-launch.html +2 -1
- package/.openprd/design/themes/theme-catalog.json +23 -5
- package/.openprd/engagements/active/prd.md +103 -100
- package/.openprd/schema/prd.schema.yaml +15 -0
- package/.openprd/standards/config.json +7 -1
- package/.openprd/templates/base/intake.md +13 -0
- package/.openprd/templates/base/prd.md +15 -0
- package/AGENTS.md +14 -4
- package/README.md +107 -14
- package/README_EN.md +131 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-benchmark-router/SKILL.md +2 -1
- package/skills/openprd-diagram-review/SKILL.md +27 -2
- package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-frontend-design/SKILL.md +44 -20
- package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
- package/skills/openprd-harness/SKILL.md +22 -16
- package/skills/openprd-quality/SKILL.md +10 -8
- package/skills/openprd-requirement-intake/SKILL.md +5 -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-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +27 -17
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +808 -0
- package/src/agent-integration.js +259 -778
- package/src/brainstorm-artifacts.js +156 -8
- package/src/brainstorm-presentation.js +14 -4
- package/src/brainstorm.js +165 -2
- package/src/canvas-app.html.js +2910 -0
- package/src/canvas-i18n.js +274 -0
- package/src/canvas-workspace.js +2271 -0
- package/src/cli/args.js +72 -4
- package/src/cli/basic-print.js +3 -0
- package/src/cli/canvas-print.js +106 -0
- package/src/cli/doctor-print.js +20 -0
- package/src/cli/print.js +2 -0
- package/src/cli/quality-commands.js +222 -0
- package/src/cli/quality-print.js +37 -5
- package/src/cli/run-print.js +5 -0
- package/src/cli/workflow-print.js +3 -0
- package/src/codex-app-server-relay.js +251 -0
- package/src/codex-hook-runner-template.mjs +963 -65
- package/src/design-starter-support.js +2 -2
- package/src/design-starter.js +39 -3
- package/src/dev-standards.js +125 -10
- package/src/diagram-core.js +390 -171
- package/src/discovery.js +2 -8
- package/src/docs-compact.js +125 -0
- package/src/execution-strategy.js +1 -1
- package/src/fleet.js +9 -3
- package/src/guide.js +227 -0
- package/src/html-artifacts.js +825 -51
- package/src/knowledge.js +101 -20
- package/src/language-policy.js +63 -3
- package/src/learning-review.js +1 -1
- package/src/loop.js +1 -1
- package/src/openprd.js +68 -131
- package/src/openspec/change-validate.js +2 -2
- package/src/openspec/constants.js +6 -7
- package/src/openspec/execute.js +2 -2
- package/src/openspec/generate.js +318 -102
- package/src/openspec/migration.js +168 -0
- package/src/openspec/paths.js +1 -22
- package/src/prd-core.js +102 -3
- package/src/quality-html-artifact.js +30 -5
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +66 -18
- package/src/review-presentation.js +60 -0
- package/src/run-harness.js +41 -3
- package/src/session-evidence.js +363 -0
- package/src/standards.js +64 -0
- package/src/test-strategy.js +7 -4
- package/src/visual-compare-centering.js +622 -0
- package/src/visual-compare-core.js +1123 -0
- package/src/visual-compare.js +620 -698
- package/src/workspace-core.js +132 -4
- 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 (
|
|
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
|
-
: '
|
|
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
|
-
|
|
358
|
+
expectsCenteringBoard
|
|
359
|
+
? '检测到 logo、图标、头像、徽标、图片或按钮图形等单个元素的内部居中/视觉重心语义,但未看到本次内部居中证据板。单张截图或主观“看起来居中”不能替代画布中心、主体外接框和视觉重心偏移量测。'
|
|
360
|
+
: expectsAlignmentBoard
|
|
361
|
+
? '检测到同构列表、卡片、网格、表格或对齐反馈语义,但未看到本次对齐辅助线证据板。新功能开发中只要有重复同构单元,也应同时量测容器轨道和内部内容槽位的坐标偏差。'
|
|
362
|
+
: expectsParallelBoard
|
|
180
363
|
? '检测到界面视觉改动且用户在比较多方向实验,但未看到本次并行实验证据板。'
|
|
181
|
-
:
|
|
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
|
-
|
|
391
|
+
? (
|
|
392
|
+
missingContentSlotAlignment
|
|
393
|
+
? '已找到对齐辅助线证据板,但缺少内部内容槽位量测'
|
|
394
|
+
: expectsCenteringBoard
|
|
395
|
+
? `已找到 ${matchingArtifacts.length} 份内部居中证据板`
|
|
396
|
+
: expectsAlignmentBoard
|
|
397
|
+
? `已找到 ${matchingArtifacts.length} 份对齐辅助线证据板`
|
|
398
|
+
: expectsParallelBoard
|
|
200
399
|
? `已找到 ${matchingArtifacts.length} 份并行实验证据板`
|
|
201
|
-
:
|
|
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
|
-
|
|
409
|
+
expectsCenteringBoard
|
|
410
|
+
? '未找到本次内部居中证据板'
|
|
411
|
+
: expectsAlignmentBoard
|
|
412
|
+
? '未找到本次对齐辅助线证据板'
|
|
413
|
+
: expectsParallelBoard
|
|
209
414
|
? '未找到本次并行实验证据板'
|
|
210
|
-
:
|
|
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
|
|
610
|
-
|
|
611
|
-
|
|
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: '
|
|
635
|
+
{ path: '项目内的日志与追踪配置', source: signalSummary ? `检测到${signalSummary}` : '日志追踪检查' },
|
|
619
636
|
].slice(0, 12),
|
|
620
|
-
summary:
|
|
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
|
-
?
|
|
645
|
-
:
|
|
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}
|
|
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:
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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);
|