@openprd/cli 0.1.1 → 0.1.9
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/README.md +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +387 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +419 -438
- package/README_CN.md +4 -578
- package/README_EN.md +870 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +47 -25
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +11 -5
- package/skills/openprd-requirement-intake/SKILL.md +31 -20
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +271 -71
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +659 -124
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1321 -76
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
|
@@ -48,6 +48,7 @@ function humanText(value) {
|
|
|
48
48
|
|
|
49
49
|
function gateDisplay(gate) {
|
|
50
50
|
if (gate?.id === 'knowledge') return '经验沉淀';
|
|
51
|
+
if (gate?.id === 'growth') return '成长账本';
|
|
51
52
|
return humanText(gate?.label ?? gate?.id ?? '测试块');
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -98,20 +99,43 @@ function gateDescription(gate) {
|
|
|
98
99
|
const descriptions = {
|
|
99
100
|
smoke: '核心路径能否跑通,至少覆盖主流程和关键失败路径',
|
|
100
101
|
'feature-coverage': '需求拆解项是否全部完成,验收点是否有对应回归',
|
|
102
|
+
'visual-review': '界面改动是否留下效果图对比、局部焦点证据板、并行实验证据板或修改前后自检证据',
|
|
101
103
|
'business-guardrails': '成本、额度、滥用、报警和止损是否讲清楚',
|
|
104
|
+
'test-strategy': '本次需求该用哪几层测试、证据怎么留下、是否有豁免',
|
|
102
105
|
traceability: '出问题时是否能追到用户动作、请求、任务和错误',
|
|
103
106
|
redaction: '报告、日志和错误信息是否会暴露敏感信息',
|
|
104
107
|
'normal-performance': '普通规模下是否可用、不卡顿、不超时',
|
|
105
108
|
'extreme-performance': '大数据、并发、异常输入或边界规模是否有兜底',
|
|
106
109
|
knowledge: '本次问题是否需要沉淀经验,避免下次重复漏测',
|
|
110
|
+
growth: '本次收工是否留下成长账本事件,确保自动补齐和规则演进可观察',
|
|
107
111
|
};
|
|
108
112
|
return descriptions[gate.id] ?? '确认这项测试是否和本次需求相关,证据是否来自本次执行';
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
function
|
|
115
|
+
function featureCoverageLedgerMessage(report) {
|
|
116
|
+
const tasks = activeTasks(report);
|
|
117
|
+
const pending = Number(tasks.pending ?? 0);
|
|
118
|
+
if (pending <= 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const total = Number(tasks.total ?? 0);
|
|
122
|
+
const done = Number(tasks.done ?? 0);
|
|
123
|
+
const blocked = Number(tasks.blocked ?? 0);
|
|
124
|
+
const progress = total > 0 ? `${done}/${total}` : `${done}`;
|
|
125
|
+
const changeLabel = tasks.activeChange ? `当前变更 ${tasks.activeChange}` : '当前任务账本';
|
|
126
|
+
return `${changeLabel} 还有 ${pending} 个未完成任务(已完成 ${progress}${blocked > 0 ? `,其中 ${blocked} 个因依赖阻塞` : ''}),这是账本未收口,不等于当前实现失败`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function gateTreatment(gate, report) {
|
|
112
130
|
if (gate.id === 'feature-coverage' && gate.evidence?.summary === '当前没有激活任务清单') {
|
|
113
131
|
return '全项目检查可继续;具体需求交付时要补任务拆解';
|
|
114
132
|
}
|
|
133
|
+
if (gate.id === 'feature-coverage') {
|
|
134
|
+
const ledgerMessage = featureCoverageLedgerMessage(report);
|
|
135
|
+
if (ledgerMessage) {
|
|
136
|
+
return ledgerMessage;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
115
139
|
if (gate.required && gate.status === 'pass') return '保留证据即可继续';
|
|
116
140
|
if (gate.required) return '现在修复或补证据,完成后重新生成报告';
|
|
117
141
|
if (gate.status === 'pass') return '已覆盖,可作为辅助证据保留';
|
|
@@ -173,7 +197,7 @@ function actionItems(report) {
|
|
|
173
197
|
const failing = required.filter((gate) => !['pass', 'waived'].includes(gate.status));
|
|
174
198
|
const advisory = (report.gates ?? []).filter((gate) => !gate.required && gate.status !== 'pass');
|
|
175
199
|
if (failing.length > 0) {
|
|
176
|
-
return failing.map((gate) => `${gateDisplay(gate)}:${humanText(gate.warnings?.[0] ?? gate.evidence?.summary ?? '补齐证据后再继续')}`);
|
|
200
|
+
return failing.map((gate) => `${gateDisplay(gate)}:${humanText(gate.warnings?.[0] ?? gateTreatment(gate, report) ?? gate.evidence?.summary ?? '补齐证据后再继续')}`);
|
|
177
201
|
}
|
|
178
202
|
if (advisory.length > 0) {
|
|
179
203
|
return advisory.map((gate) => `${gateDisplay(gate)}:判断是否属于本期,属于就补测,不属于就说明延期原因`);
|
|
@@ -477,10 +501,10 @@ function detailList(items, emptyText) {
|
|
|
477
501
|
`;
|
|
478
502
|
}
|
|
479
503
|
|
|
480
|
-
function requiredItems(required) {
|
|
504
|
+
function requiredItems(report, required) {
|
|
481
505
|
return required.map((gate) => ({
|
|
482
506
|
summary: gateSummary(gate),
|
|
483
|
-
detail: `${gate.required ? '本期必测' : '按风险确认'},${gate.evidence?.summary ?? '等待补充本次证据'}。${gateTreatment(gate)}`,
|
|
507
|
+
detail: `${gate.required ? '本期必测' : '按风险确认'},${gate.evidence?.summary ?? '等待补充本次证据'}。${gateTreatment(gate, report)}`,
|
|
484
508
|
}));
|
|
485
509
|
}
|
|
486
510
|
|
|
@@ -489,7 +513,7 @@ function exceptionItems(report) {
|
|
|
489
513
|
.filter((gate) => gate.status !== 'pass' && gate.status !== 'waived')
|
|
490
514
|
.map((gate) => ({
|
|
491
515
|
summary: gateSummary(gate),
|
|
492
|
-
detail: humanText(gate.warnings?.[0] ?? gateTreatment(gate)).replace(/[。.]$/u, ''),
|
|
516
|
+
detail: humanText(gate.warnings?.[0] ?? gateTreatment(gate, report)).replace(/[。.]$/u, ''),
|
|
493
517
|
}));
|
|
494
518
|
}
|
|
495
519
|
|
|
@@ -505,17 +529,32 @@ function evidenceItems(report) {
|
|
|
505
529
|
|
|
506
530
|
function environmentItems(report) {
|
|
507
531
|
const evalHarness = report.evalHarness;
|
|
532
|
+
const strategy = evalHarness.testStrategy ?? {};
|
|
508
533
|
const obs = report.observability;
|
|
509
534
|
const knowledge = report.knowledge;
|
|
535
|
+
const growth = report.growth ?? { summary: { eventCount: 0, completionCheckpoints: 0 } };
|
|
510
536
|
const businessGuardrails = report.businessGuardrails;
|
|
537
|
+
const visualReview = report.visualReview ?? { relevant: false, evidence: { present: false, summary: '当前场景未要求视觉评审证据' } };
|
|
511
538
|
return [
|
|
539
|
+
{
|
|
540
|
+
summary: '分层测试策略',
|
|
541
|
+
detail: strategy.total > 0
|
|
542
|
+
? `显式 ${strategy.explicit ?? 0} 项,推导 ${strategy.inferred ?? 0} 项,已规划证据 ${strategy.evidencePlanned ?? 0} 项`
|
|
543
|
+
: '当前没有可分析的任务级测试策略',
|
|
544
|
+
},
|
|
512
545
|
{
|
|
513
546
|
summary: evalHarness.smoke.present ? '主流程验证可用' : '主流程验证缺失',
|
|
514
547
|
detail: evalHarness.smoke.commands.join(' / ') || '还没有发现可直接复跑的验证入口',
|
|
515
548
|
},
|
|
516
549
|
{
|
|
517
550
|
summary: '任务覆盖',
|
|
518
|
-
detail: `已完成 ${evalHarness.featureCoverage.activeTasks.done}/${evalHarness.featureCoverage.activeTasks.total},待处理 ${evalHarness.featureCoverage.activeTasks.pending}`,
|
|
551
|
+
detail: `已完成 ${evalHarness.featureCoverage.activeTasks.done}/${evalHarness.featureCoverage.activeTasks.total},待处理 ${evalHarness.featureCoverage.activeTasks.pending}${evalHarness.featureCoverage.activeTasks.blocked > 0 ? `,阻塞 ${evalHarness.featureCoverage.activeTasks.blocked}` : ''}`,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
summary: '视觉证据',
|
|
555
|
+
detail: visualReview.relevant
|
|
556
|
+
? (visualReview.evidence?.summary ?? '需要补充本次视觉对比证据')
|
|
557
|
+
: '当前场景未要求视觉评审证据',
|
|
519
558
|
},
|
|
520
559
|
{
|
|
521
560
|
summary: '问题追踪',
|
|
@@ -527,11 +566,58 @@ function environmentItems(report) {
|
|
|
527
566
|
},
|
|
528
567
|
{
|
|
529
568
|
summary: '经验沉淀',
|
|
530
|
-
detail: knowledge.skills.length > 0
|
|
569
|
+
detail: knowledge.skills.length > 0
|
|
570
|
+
? `已有 ${knowledge.skills.length} 个项目经验${knowledge.candidates.length > 0 ? `,另有 ${knowledge.candidates.length} 个待确认草案` : ''}`
|
|
571
|
+
: (knowledge.candidates.length > 0 ? `已有 ${knowledge.candidates.length} 个待确认草案` : '首次稳定问题修复后应沉淀经验'),
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
summary: '成长账本',
|
|
575
|
+
detail: Number(growth.summary?.eventCount ?? 0) > 0
|
|
576
|
+
? `已记录 ${growth.summary.eventCount} 条事件,completion checkpoint ${growth.summary.completionCheckpoints ?? 0} 条`
|
|
577
|
+
: '当前还没有成长账本事件;完成态至少应留下 checkpoint 或候选',
|
|
531
578
|
},
|
|
532
579
|
];
|
|
533
580
|
}
|
|
534
581
|
|
|
582
|
+
function layerLabel(layer) {
|
|
583
|
+
const labels = {
|
|
584
|
+
unit: '单元',
|
|
585
|
+
integration: '集成',
|
|
586
|
+
e2e: '端到端',
|
|
587
|
+
manual: '人工',
|
|
588
|
+
smoke: '冒烟',
|
|
589
|
+
visual: '视觉',
|
|
590
|
+
performance: '性能',
|
|
591
|
+
security: '安全',
|
|
592
|
+
weapp: '小程序',
|
|
593
|
+
none: '无',
|
|
594
|
+
};
|
|
595
|
+
return labels[layer] ?? layer;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function strategyItems(report) {
|
|
599
|
+
const strategy = report.evalHarness?.testStrategy ?? {};
|
|
600
|
+
const layerCounts = strategy.layerCounts ?? {};
|
|
601
|
+
const layerSummary = Object.entries(layerCounts)
|
|
602
|
+
.filter(([, count]) => Number(count) > 0)
|
|
603
|
+
.map(([layer, count]) => `${layerLabel(layer)} ${count}`)
|
|
604
|
+
.join(',');
|
|
605
|
+
const warnings = strategy.warnings ?? [];
|
|
606
|
+
return [
|
|
607
|
+
{
|
|
608
|
+
summary: strategy.total > 0 ? `已分析 ${strategy.total} 个任务` : '暂无任务策略',
|
|
609
|
+
detail: layerSummary || '没有检测到任务级测试层级',
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
summary: `证据计划 ${strategy.evidencePlanned ?? 0}/${strategy.total ?? 0}`,
|
|
613
|
+
detail: strategy.evidencePresent > 0 ? `已有 ${strategy.evidencePresent} 项证据记录` : '当前重点是确认该测哪一层,再留下本次执行证据',
|
|
614
|
+
},
|
|
615
|
+
...(warnings.length > 0
|
|
616
|
+
? warnings.slice(0, 3).map((warning) => ({ summary: '需关注', detail: humanText(warning) }))
|
|
617
|
+
: [{ summary: '比例规则', detail: '70/20/10 只作健康形状参考,不作为硬阻断' }]),
|
|
618
|
+
];
|
|
619
|
+
}
|
|
620
|
+
|
|
535
621
|
function panel({ kind, title, description, chips, items, emptyText }) {
|
|
536
622
|
return `
|
|
537
623
|
<section class="quality-panel quality-panel-${escapeHtml(kind)}">
|
|
@@ -597,7 +683,7 @@ function tableRowsForGates(report) {
|
|
|
597
683
|
<strong>${gate.evidence?.present ? `${gate.evidence.sources.length} 条` : '缺证据'}</strong>
|
|
598
684
|
<span>${escapeHtml(gate.evidence?.summary ?? '未找到本次执行证据')}</span>
|
|
599
685
|
</td>
|
|
600
|
-
<td>${escapeHtml(gateTreatment(gate))}</td>
|
|
686
|
+
<td>${escapeHtml(gateTreatment(gate, report))}</td>
|
|
601
687
|
</tr>
|
|
602
688
|
`).join('\n');
|
|
603
689
|
}
|
|
@@ -1260,7 +1346,7 @@ export function renderQualityEvalArtifact({ report }) {
|
|
|
1260
1346
|
title: '本期必测结果',
|
|
1261
1347
|
description: '先看必须覆盖的测试是否通过,没通过就不要继续',
|
|
1262
1348
|
chips: required.map((gate) => chip(gateSummary(gate), toneForGate(gate))),
|
|
1263
|
-
items: requiredItems(required),
|
|
1349
|
+
items: requiredItems(report, required),
|
|
1264
1350
|
emptyText: '当前没有被判定为本期必测的测试块。',
|
|
1265
1351
|
}),
|
|
1266
1352
|
panel({
|
|
@@ -1290,6 +1376,7 @@ export function renderQualityEvalArtifact({ report }) {
|
|
|
1290
1376
|
title: '执行环境与覆盖',
|
|
1291
1377
|
description: '区分项目具备测试能力,和这次是否真的留下证据',
|
|
1292
1378
|
chips: [
|
|
1379
|
+
chip(`测试策略 ${report.evalHarness.testStrategy?.total ?? 0} 项`, (report.evalHarness.testStrategy?.total ?? 0) > 0 ? 'note' : 'warn'),
|
|
1293
1380
|
chip(report.evalHarness.smoke.present ? '主流程验证可用' : '缺主流程验证', report.evalHarness.smoke.present ? 'pass' : 'warn'),
|
|
1294
1381
|
chip(report.observability.correlationFields.length > 0 ? '问题可追踪' : '追踪线索不足', report.observability.correlationFields.length > 0 ? 'pass' : 'warn'),
|
|
1295
1382
|
chip(report.businessGuardrails.missingEvidence.length > 0 ? '成本护栏待补' : '成本护栏完整', report.businessGuardrails.missingEvidence.length > 0 ? 'warn' : 'pass'),
|
|
@@ -1297,6 +1384,18 @@ export function renderQualityEvalArtifact({ report }) {
|
|
|
1297
1384
|
items: environmentItems(report),
|
|
1298
1385
|
emptyText: '还没有检测到执行环境信息。',
|
|
1299
1386
|
}),
|
|
1387
|
+
panel({
|
|
1388
|
+
kind: 'check',
|
|
1389
|
+
title: '分层测试策略',
|
|
1390
|
+
description: '按需求风险选择最小足够证据,不把固定比例当硬指标',
|
|
1391
|
+
chips: [
|
|
1392
|
+
chip(`显式 ${report.evalHarness.testStrategy?.explicit ?? 0}`, 'note'),
|
|
1393
|
+
chip(`推导 ${report.evalHarness.testStrategy?.inferred ?? 0}`, 'note'),
|
|
1394
|
+
chip(`证据计划 ${report.evalHarness.testStrategy?.evidencePlanned ?? 0}`, (report.evalHarness.testStrategy?.evidencePlanned ?? 0) > 0 ? 'pass' : 'warn'),
|
|
1395
|
+
],
|
|
1396
|
+
items: strategyItems(report),
|
|
1397
|
+
emptyText: '还没有任务级测试策略。',
|
|
1398
|
+
}),
|
|
1300
1399
|
].join('\n');
|
|
1301
1400
|
|
|
1302
1401
|
return `<!DOCTYPE html>
|
package/src/quality-learning.js
CHANGED
|
@@ -619,6 +619,24 @@ export function renderExperienceSkill({ skillName, source }) {
|
|
|
619
619
|
source.eventNames.length > 0 ? `运行态再次出现事件:${source.eventNames.join(', ')}。` : null,
|
|
620
620
|
source.symptoms.length > 0 ? `本次症状包括:${source.symptoms.join(';')}。` : null,
|
|
621
621
|
]
|
|
622
|
+
const applicability = [
|
|
623
|
+
source.abstractPattern ? `优先适用于:${source.abstractPattern}` : null,
|
|
624
|
+
source.kind === 'quality-report'
|
|
625
|
+
? '适用于已经完成一轮实现,需要把质量闭环、证据顺序和防复发要求固定下来的任务。'
|
|
626
|
+
: '适用于已经拿到 runtime-events、timeline、root-cause-candidates 或 diagnostic-report 的排查任务。',
|
|
627
|
+
]
|
|
628
|
+
const typicalInputs = [
|
|
629
|
+
source.title ? `问题摘要: ${source.title}` : null,
|
|
630
|
+
source.evidenceSources.length > 0
|
|
631
|
+
? `可直接使用的证据: ${source.evidenceSources.slice(0, 4).map((item) => `${item.kind}:${item.path}`).join(';')}`
|
|
632
|
+
: null,
|
|
633
|
+
source.eventNames.length > 0 ? `关键事件: ${source.eventNames.join(' -> ')}` : null,
|
|
634
|
+
]
|
|
635
|
+
const typicalOutputs = [
|
|
636
|
+
'结论性诊断说明,明确问题症状、根因模式和下一步动作。',
|
|
637
|
+
source.verificationSteps.length > 0 ? `最小验证链路: ${source.verificationSteps[0]}` : null,
|
|
638
|
+
'可复用的项目级 knowledge skill 或后续 candidate / incident / pattern 记录。',
|
|
639
|
+
]
|
|
622
640
|
|
|
623
641
|
return `---
|
|
624
642
|
name: ${skillName}
|
|
@@ -631,6 +649,18 @@ description: 由 OpenPrd 从 ${source.kind} 自动沉淀的项目级排查经验
|
|
|
631
649
|
|
|
632
650
|
${renderList(triggers, '当同类问题再次出现,先复用这套排查路径。')}
|
|
633
651
|
|
|
652
|
+
## 适用范围
|
|
653
|
+
|
|
654
|
+
${renderList(applicability, '适用于相似问题再次出现时,先复用已有排查路径和验证证据。')}
|
|
655
|
+
|
|
656
|
+
## 典型输入
|
|
657
|
+
|
|
658
|
+
${renderList(typicalInputs, '至少带上问题摘要、现有证据和关键事件。')}
|
|
659
|
+
|
|
660
|
+
## 典型输出
|
|
661
|
+
|
|
662
|
+
${renderList(typicalOutputs, '至少产出可复用结论、验证链路和后续知识沉淀。')}
|
|
663
|
+
|
|
634
664
|
## 先看哪些证据
|
|
635
665
|
|
|
636
666
|
${renderEvidenceSources(source.evidenceSources)}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cjoin, readJson } from './fs-utils.js';
|
|
4
|
+
|
|
5
|
+
const VISUAL_REVIEW_DIR = cjoin('.openprd', 'harness', 'visual-reviews');
|
|
6
|
+
const VISUAL_REVIEW_SCHEMA = 'openprd.visual-review.v1';
|
|
7
|
+
const VISUAL_REVIEW_IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
8
|
+
|
|
9
|
+
function normalizeWorkspacePath(value) {
|
|
10
|
+
return String(value ?? '').split(path.sep).join('/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function inferVisualReviewMode(value) {
|
|
14
|
+
const normalized = normalizeWorkspacePath(value).toLowerCase();
|
|
15
|
+
if (normalized.includes('visual-focus-board') || normalized.includes('focus-board') || normalized.includes('focus-region')) {
|
|
16
|
+
return 'focus-board';
|
|
17
|
+
}
|
|
18
|
+
if (normalized.includes('visual-parallel-board') || normalized.includes('parallel-board') || normalized.includes('experiment-board')) {
|
|
19
|
+
return 'parallel-board';
|
|
20
|
+
}
|
|
21
|
+
if (normalized.includes('visual-before-after') || normalized.includes('before-after')) {
|
|
22
|
+
return 'before-after';
|
|
23
|
+
}
|
|
24
|
+
if (normalized.includes('visual-compare') || normalized.includes('reference-actual')) {
|
|
25
|
+
return 'reference-actual';
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function walkVisualReviewDir(projectRoot, dir, collected) {
|
|
31
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const fullPath = cjoin(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
await walkVisualReviewDir(projectRoot, fullPath, collected);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!entry.isFile()) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
42
|
+
if (!VISUAL_REVIEW_IMAGE_EXTENSIONS.has(ext) && ext !== '.json') {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const relativePath = normalizeWorkspacePath(path.relative(projectRoot, fullPath));
|
|
46
|
+
collected.push({ fullPath, relativePath, ext });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function listVisualReviewArtifacts(projectRoot) {
|
|
51
|
+
const root = cjoin(projectRoot, VISUAL_REVIEW_DIR);
|
|
52
|
+
const entries = [];
|
|
53
|
+
await walkVisualReviewDir(projectRoot, root, entries);
|
|
54
|
+
const artifactsByKey = new Map();
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (entry.ext === '.json') {
|
|
58
|
+
const payload = await readJson(entry.fullPath).catch(() => null);
|
|
59
|
+
if (payload?.schema !== VISUAL_REVIEW_SCHEMA) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = normalizeWorkspacePath(String(payload.outputPath ?? entry.relativePath).replace(/\.[^.]+$/u, ''));
|
|
63
|
+
const existing = artifactsByKey.get(key) ?? {};
|
|
64
|
+
const outputPath = normalizeWorkspacePath(payload.outputPath ?? '');
|
|
65
|
+
const inferredMode = payload.mode ?? inferVisualReviewMode(outputPath) ?? inferVisualReviewMode(entry.relativePath);
|
|
66
|
+
if (!inferredMode) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const stat = await fs.stat(entry.fullPath).catch(() => null);
|
|
70
|
+
artifactsByKey.set(key, {
|
|
71
|
+
...existing,
|
|
72
|
+
path: outputPath || existing.path || entry.relativePath,
|
|
73
|
+
metadataPath: entry.relativePath,
|
|
74
|
+
mode: inferredMode,
|
|
75
|
+
labels: payload.labels ?? existing.labels ?? null,
|
|
76
|
+
generatedAt: payload.generatedAt ?? existing.generatedAt ?? null,
|
|
77
|
+
mtimeMs: stat?.mtimeMs ?? existing.mtimeMs ?? 0,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const key = entry.relativePath.replace(/\.[^.]+$/u, '');
|
|
83
|
+
const existing = artifactsByKey.get(key) ?? {};
|
|
84
|
+
const inferredMode = inferVisualReviewMode(entry.relativePath);
|
|
85
|
+
if (!inferredMode) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const stat = await fs.stat(entry.fullPath).catch(() => null);
|
|
89
|
+
artifactsByKey.set(key, {
|
|
90
|
+
...existing,
|
|
91
|
+
path: entry.relativePath,
|
|
92
|
+
mode: existing.mode ?? inferredMode,
|
|
93
|
+
mtimeMs: stat?.mtimeMs ?? existing.mtimeMs ?? 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return [...artifactsByKey.values()]
|
|
98
|
+
.filter((artifact) => artifact.path && artifact.mode)
|
|
99
|
+
.sort((a, b) => (b.mtimeMs ?? 0) - (a.mtimeMs ?? 0))
|
|
100
|
+
.slice(0, 24);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function detectVisualReview({ policy, activeChangeContext, activeTasks, visualArtifacts, includesAny }) {
|
|
104
|
+
const relevant = policy.requiredGates.includes('visual-review');
|
|
105
|
+
const haystack = [
|
|
106
|
+
activeChangeContext.text,
|
|
107
|
+
activeTasks.tasks.map((task) => [
|
|
108
|
+
task.title,
|
|
109
|
+
...Object.entries(task.metadata ?? {}).map(([key, value]) => `${key}: ${value}`),
|
|
110
|
+
].join('\n')).join('\n'),
|
|
111
|
+
].join('\n');
|
|
112
|
+
const referenceTokens = [
|
|
113
|
+
'reference image',
|
|
114
|
+
'reference design',
|
|
115
|
+
'design reference',
|
|
116
|
+
'effect image',
|
|
117
|
+
'mockup',
|
|
118
|
+
'figma',
|
|
119
|
+
'效果图',
|
|
120
|
+
'设计稿',
|
|
121
|
+
'视觉稿',
|
|
122
|
+
'参考图',
|
|
123
|
+
'用户给图',
|
|
124
|
+
'图片资产',
|
|
125
|
+
];
|
|
126
|
+
const focusTokens = [
|
|
127
|
+
'focus board',
|
|
128
|
+
'focus region',
|
|
129
|
+
'focus-region',
|
|
130
|
+
'local compare',
|
|
131
|
+
'zoom compare',
|
|
132
|
+
'局部对比',
|
|
133
|
+
'局部放大',
|
|
134
|
+
'焦点区域',
|
|
135
|
+
'focus',
|
|
136
|
+
];
|
|
137
|
+
const parallelTokens = [
|
|
138
|
+
'parallel board',
|
|
139
|
+
'parallel experiment',
|
|
140
|
+
'experiment board',
|
|
141
|
+
'evidence board',
|
|
142
|
+
'并行实验',
|
|
143
|
+
'并行方向',
|
|
144
|
+
'多方向实验',
|
|
145
|
+
'方案对比板',
|
|
146
|
+
'证据板',
|
|
147
|
+
];
|
|
148
|
+
const expectsReferenceCompare = includesAny(haystack, referenceTokens);
|
|
149
|
+
const expectsFocusBoard = includesAny(haystack, focusTokens);
|
|
150
|
+
const expectsParallelBoard = includesAny(haystack, parallelTokens);
|
|
151
|
+
const referenceArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'reference-actual');
|
|
152
|
+
const beforeAfterArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'before-after');
|
|
153
|
+
const focusArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'focus-board');
|
|
154
|
+
const parallelArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'parallel-board');
|
|
155
|
+
let matchingArtifacts;
|
|
156
|
+
if (expectsParallelBoard) {
|
|
157
|
+
matchingArtifacts = parallelArtifacts;
|
|
158
|
+
} else if (expectsFocusBoard) {
|
|
159
|
+
matchingArtifacts = focusArtifacts;
|
|
160
|
+
} else if (expectsReferenceCompare) {
|
|
161
|
+
matchingArtifacts = referenceArtifacts;
|
|
162
|
+
} else {
|
|
163
|
+
matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts];
|
|
164
|
+
}
|
|
165
|
+
const evidenceSources = matchingArtifacts.slice(0, 12).map((artifact) => ({
|
|
166
|
+
path: artifact.path,
|
|
167
|
+
source: artifact.mode === 'reference-actual'
|
|
168
|
+
? 'visual-review/reference-actual'
|
|
169
|
+
: artifact.mode === 'before-after'
|
|
170
|
+
? 'visual-review/before-after'
|
|
171
|
+
: artifact.mode === 'focus-board'
|
|
172
|
+
? 'visual-review/focus-board'
|
|
173
|
+
: 'visual-review/parallel-board',
|
|
174
|
+
}));
|
|
175
|
+
const warnings = [];
|
|
176
|
+
|
|
177
|
+
if (relevant && matchingArtifacts.length === 0) {
|
|
178
|
+
warnings.push(
|
|
179
|
+
expectsParallelBoard
|
|
180
|
+
? '检测到界面视觉改动且用户在比较多方向实验,但未看到本次并行实验证据板。'
|
|
181
|
+
: expectsFocusBoard
|
|
182
|
+
? '检测到界面视觉改动且用户在关注局部细节,但未看到本次局部焦点证据板。'
|
|
183
|
+
: expectsReferenceCompare
|
|
184
|
+
? '检测到界面视觉改动且已有参考图/设计稿语义,但未看到本次“效果图 / 实现截图”对比证据。'
|
|
185
|
+
: '检测到界面视觉改动,但未看到本次 visual-compare 产出的视觉对比或修改前后自检证据。'
|
|
186
|
+
);
|
|
187
|
+
} else if (relevant && expectsReferenceCompare && referenceArtifacts.length === 0 && beforeAfterArtifacts.length > 0) {
|
|
188
|
+
warnings.push('当前只发现修改前后自检图;如果已有参考图或设计稿,请补一份“效果图 / 实现截图”对比图。');
|
|
189
|
+
} else if (relevant && expectsFocusBoard && focusArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
190
|
+
warnings.push('当前有视觉证据,但局部细节仍建议补一份局部焦点证据板,方便围绕编号区域复核。');
|
|
191
|
+
} else if (relevant && expectsParallelBoard && parallelArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
192
|
+
warnings.push('当前有视觉证据,但多方向实验仍建议补一份并行实验证据板,把方案和指标放到同一板里审查。');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const summary = !relevant
|
|
196
|
+
? '当前场景未要求视觉评审证据'
|
|
197
|
+
: matchingArtifacts.length > 0
|
|
198
|
+
? (
|
|
199
|
+
expectsParallelBoard
|
|
200
|
+
? `已找到 ${matchingArtifacts.length} 份并行实验证据板`
|
|
201
|
+
: expectsFocusBoard
|
|
202
|
+
? `已找到 ${matchingArtifacts.length} 份局部焦点证据板`
|
|
203
|
+
: expectsReferenceCompare
|
|
204
|
+
? `已找到 ${matchingArtifacts.length} 份效果图 / 实现截图对比证据`
|
|
205
|
+
: `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点或修改前后自检证据`
|
|
206
|
+
)
|
|
207
|
+
: (
|
|
208
|
+
expectsParallelBoard
|
|
209
|
+
? '未找到本次并行实验证据板'
|
|
210
|
+
: expectsFocusBoard
|
|
211
|
+
? '未找到本次局部焦点证据板'
|
|
212
|
+
: expectsReferenceCompare
|
|
213
|
+
? '未找到本次效果图 / 实现截图对比证据'
|
|
214
|
+
: '未找到本次 visual-compare 视觉证据'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
status: !relevant || matchingArtifacts.length > 0 ? 'pass' : 'needs-evidence',
|
|
219
|
+
relevant,
|
|
220
|
+
expectsReferenceCompare,
|
|
221
|
+
expectsFocusBoard,
|
|
222
|
+
expectsParallelBoard,
|
|
223
|
+
artifacts: visualArtifacts,
|
|
224
|
+
matchingArtifacts,
|
|
225
|
+
warnings,
|
|
226
|
+
evidence: {
|
|
227
|
+
present: matchingArtifacts.length > 0,
|
|
228
|
+
sources: evidenceSources,
|
|
229
|
+
summary,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
VISUAL_REVIEW_DIR,
|
|
236
|
+
VISUAL_REVIEW_SCHEMA,
|
|
237
|
+
};
|