@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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 定义 benchmark 子系统共享常量、目录路径和默认 registry 模板。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收项目根目录、路径片段和用于生成默认 sources/index 的当前时间。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出 benchmark 工作区路径、采纳阈值常量、slugify 和默认文件内容生成器。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 benchmark 模块最底层,不处理业务判断,只提供稳定的共享基座。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖 fs-utils 的路径拼接和 time 的时间戳工具;被 source、storage、operations 复用。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 修改路径常量或默认模板时必须保持与已有 workspace 文件布局和 CLI 文案兼容。
|
|
19
|
+
*/
|
|
20
|
+
import { cjoin } from '../fs-utils.js';
|
|
21
|
+
import { timestamp } from '../time.js';
|
|
22
|
+
|
|
23
|
+
const BENCHMARK_DIR = cjoin('.openprd', 'benchmarks');
|
|
24
|
+
const BENCHMARK_INBOX_DIR = cjoin(BENCHMARK_DIR, 'inbox');
|
|
25
|
+
const BENCHMARK_EVIDENCE_DIR = cjoin(BENCHMARK_DIR, 'evidence');
|
|
26
|
+
const BENCHMARK_SOURCES_FILE = cjoin(BENCHMARK_DIR, 'sources.yaml');
|
|
27
|
+
const BENCHMARK_INDEX_FILE = cjoin(BENCHMARK_DIR, 'index.md');
|
|
28
|
+
const DEFAULT_ADOPTION_THRESHOLD = 3;
|
|
29
|
+
const DEFAULT_ADOPTION_WINDOW_DAYS = 7;
|
|
30
|
+
const MAX_ADOPTION_EVIDENCE = 20;
|
|
31
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
const OVERBROAD_TRIGGER_TOKENS = [
|
|
34
|
+
'all',
|
|
35
|
+
'any',
|
|
36
|
+
'everything',
|
|
37
|
+
'generic',
|
|
38
|
+
'general',
|
|
39
|
+
'所有',
|
|
40
|
+
'任何',
|
|
41
|
+
'全部',
|
|
42
|
+
'通用',
|
|
43
|
+
'任意任务',
|
|
44
|
+
'任何任务',
|
|
45
|
+
'所有任务',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function benchmarkPath(projectRoot, relativePath) {
|
|
49
|
+
return cjoin(projectRoot, relativePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function slugify(value, fallback = 'benchmark') {
|
|
53
|
+
const slug = String(value ?? '')
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
56
|
+
.replace(/^-+|-+$/g, '')
|
|
57
|
+
.slice(0, 80);
|
|
58
|
+
return slug || fallback;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultSourcesFile() {
|
|
62
|
+
return {
|
|
63
|
+
version: 1,
|
|
64
|
+
schema: 'openprd.benchmarks.v1',
|
|
65
|
+
updatedAt: timestamp(),
|
|
66
|
+
sources: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function defaultIndex() {
|
|
71
|
+
return [
|
|
72
|
+
'# OpenPrd Benchmark Registry',
|
|
73
|
+
'',
|
|
74
|
+
'## 规则',
|
|
75
|
+
'',
|
|
76
|
+
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。',
|
|
77
|
+
'- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。',
|
|
78
|
+
'- 被采纳信源先用 `openprd benchmark observe <url|repo|file>` 累计证据,达到阈值后只推荐 approve,不自动晋级。',
|
|
79
|
+
'- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。',
|
|
80
|
+
'',
|
|
81
|
+
'## Approved Sources',
|
|
82
|
+
'',
|
|
83
|
+
'- 暂无已批准来源。',
|
|
84
|
+
'',
|
|
85
|
+
'## Candidate Sources',
|
|
86
|
+
'',
|
|
87
|
+
'- 暂无待确认来源。',
|
|
88
|
+
'',
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
BENCHMARK_DIR,
|
|
94
|
+
BENCHMARK_EVIDENCE_DIR,
|
|
95
|
+
BENCHMARK_INDEX_FILE,
|
|
96
|
+
BENCHMARK_INBOX_DIR,
|
|
97
|
+
BENCHMARK_SOURCES_FILE,
|
|
98
|
+
DAY_MS,
|
|
99
|
+
DEFAULT_ADOPTION_THRESHOLD,
|
|
100
|
+
DEFAULT_ADOPTION_WINDOW_DAYS,
|
|
101
|
+
MAX_ADOPTION_EVIDENCE,
|
|
102
|
+
OVERBROAD_TRIGGER_TOKENS,
|
|
103
|
+
benchmarkPath,
|
|
104
|
+
defaultIndex,
|
|
105
|
+
defaultSourcesFile,
|
|
106
|
+
slugify,
|
|
107
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 实现 benchmark add/observe/list/approve 等 workspace 级命令动作。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收项目根目录和 CLI/options 透传的 source、notes、threshold、id 等参数。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出结构化 benchmark 操作结果,并在需要时写入 candidate、approved、evidence 和 index 文件。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 benchmark 应用层,负责编排 source 逻辑与 storage 落盘,不承担 verify 探测职责。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖 render、source、storage 和 fs-utils;由 benchmark.js 作为对外入口复用。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 修改返回 payload 时必须保持 CLI 与测试依赖的 action/source/files/counts 契约稳定。
|
|
19
|
+
*/
|
|
20
|
+
import fs from 'node:fs/promises';
|
|
21
|
+
import { writeText, writeYaml } from '../fs-utils.js';
|
|
22
|
+
import { timestamp } from '../time.js';
|
|
23
|
+
import {
|
|
24
|
+
BENCHMARK_INDEX_FILE,
|
|
25
|
+
BENCHMARK_SOURCES_FILE,
|
|
26
|
+
benchmarkPath,
|
|
27
|
+
} from './constants.js';
|
|
28
|
+
import { renderEvidence } from './render.js';
|
|
29
|
+
import {
|
|
30
|
+
benchmarkRecommendations,
|
|
31
|
+
buildObservationEvidence,
|
|
32
|
+
buildSourceValue,
|
|
33
|
+
duplicateSource,
|
|
34
|
+
normalizeAdoptionEvidence,
|
|
35
|
+
normalizeAdoptionThreshold,
|
|
36
|
+
normalizeSourceRecord,
|
|
37
|
+
resolveSourceInput,
|
|
38
|
+
withPromotion,
|
|
39
|
+
} from './source.js';
|
|
40
|
+
import {
|
|
41
|
+
ensureBenchmarkWorkspace,
|
|
42
|
+
evidenceFilePath,
|
|
43
|
+
loadApprovedSources,
|
|
44
|
+
loadCandidateSources,
|
|
45
|
+
readCandidateById,
|
|
46
|
+
refreshBenchmarkIndex,
|
|
47
|
+
sourceFilePath,
|
|
48
|
+
writeApprovedSources,
|
|
49
|
+
} from './storage.js';
|
|
50
|
+
|
|
51
|
+
async function addBenchmarkWorkspace(projectRoot, options = {}) {
|
|
52
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
53
|
+
const sourceValue = await resolveSourceInput(projectRoot, options.source ?? options.target ?? options.reference ?? null);
|
|
54
|
+
const source = buildSourceValue(sourceValue, options.notes ?? null);
|
|
55
|
+
const approved = await loadApprovedSources(projectRoot);
|
|
56
|
+
const candidates = await loadCandidateSources(projectRoot);
|
|
57
|
+
const duplicate = duplicateSource([...approved, ...candidates], source);
|
|
58
|
+
if (duplicate) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
action: 'benchmark-add',
|
|
62
|
+
projectRoot,
|
|
63
|
+
error: `Benchmark source already exists: ${duplicate.id}`,
|
|
64
|
+
duplicate,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const candidatePath = sourceFilePath(projectRoot, source.id);
|
|
69
|
+
const evidencePath = evidenceFilePath(projectRoot, source.id);
|
|
70
|
+
await writeYaml(candidatePath, source);
|
|
71
|
+
await writeText(evidencePath, `${renderEvidence(source)}\n`);
|
|
72
|
+
const refreshed = await refreshBenchmarkIndex(projectRoot);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
action: 'benchmark-add',
|
|
77
|
+
projectRoot,
|
|
78
|
+
source,
|
|
79
|
+
files: {
|
|
80
|
+
candidate: candidatePath,
|
|
81
|
+
evidence: evidencePath,
|
|
82
|
+
index: benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE),
|
|
83
|
+
sources: benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE),
|
|
84
|
+
},
|
|
85
|
+
summary: {
|
|
86
|
+
approved: refreshed.approved.length,
|
|
87
|
+
candidates: refreshed.candidates.length,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function observeBenchmarkSourceWorkspace(projectRoot, options = {}) {
|
|
93
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
94
|
+
const threshold = normalizeAdoptionThreshold(options.threshold);
|
|
95
|
+
const sourceValue = await resolveSourceInput(projectRoot, options.source ?? options.target ?? options.reference ?? null);
|
|
96
|
+
const observed = buildSourceValue(sourceValue, options.notes ?? null);
|
|
97
|
+
const approved = await loadApprovedSources(projectRoot);
|
|
98
|
+
const candidates = await loadCandidateSources(projectRoot);
|
|
99
|
+
const duplicate = duplicateSource([...approved, ...candidates], observed);
|
|
100
|
+
const baseSource = duplicate ?? observed;
|
|
101
|
+
const evidence = buildObservationEvidence(baseSource, options);
|
|
102
|
+
const adoptedCount = Math.max(0, Number(baseSource.adoptedCount ?? 0)) + 1;
|
|
103
|
+
const nextSource = withPromotion({
|
|
104
|
+
...baseSource,
|
|
105
|
+
id: baseSource.id,
|
|
106
|
+
status: baseSource.status ?? 'candidate',
|
|
107
|
+
note: baseSource.note ?? observed.note,
|
|
108
|
+
value: baseSource.value ?? observed.value,
|
|
109
|
+
adoptedCount,
|
|
110
|
+
lastUsedAt: evidence.observedAt,
|
|
111
|
+
evidence: [
|
|
112
|
+
...normalizeAdoptionEvidence(baseSource.evidence),
|
|
113
|
+
evidence,
|
|
114
|
+
],
|
|
115
|
+
}, threshold, baseSource.promotion?.windowDays);
|
|
116
|
+
|
|
117
|
+
if (nextSource.status === 'approved') {
|
|
118
|
+
const nextApproved = approved.filter((source) => source.id !== nextSource.id);
|
|
119
|
+
nextApproved.push(nextSource);
|
|
120
|
+
await writeApprovedSources(projectRoot, nextApproved);
|
|
121
|
+
} else {
|
|
122
|
+
await writeYaml(sourceFilePath(projectRoot, nextSource.id), nextSource);
|
|
123
|
+
}
|
|
124
|
+
await writeText(evidenceFilePath(projectRoot, nextSource.id), `${renderEvidence(nextSource)}\n`);
|
|
125
|
+
const refreshed = await refreshBenchmarkIndex(projectRoot);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
action: 'benchmark-observe',
|
|
130
|
+
projectRoot,
|
|
131
|
+
source: nextSource,
|
|
132
|
+
created: !duplicate,
|
|
133
|
+
evidence,
|
|
134
|
+
recommended: Boolean(nextSource.promotion?.recommended),
|
|
135
|
+
recommendation: nextSource.promotion?.recommended
|
|
136
|
+
? {
|
|
137
|
+
id: nextSource.id,
|
|
138
|
+
title: nextSource.title,
|
|
139
|
+
adoptedCount: nextSource.recentAdoptedCount,
|
|
140
|
+
totalAdoptedCount: nextSource.adoptedCount,
|
|
141
|
+
threshold: nextSource.promotion.threshold,
|
|
142
|
+
windowDays: nextSource.promotion.windowDays,
|
|
143
|
+
approveCommand: nextSource.promotion.approveCommand,
|
|
144
|
+
}
|
|
145
|
+
: null,
|
|
146
|
+
files: {
|
|
147
|
+
candidate: nextSource.status === 'candidate' ? sourceFilePath(projectRoot, nextSource.id) : null,
|
|
148
|
+
evidence: evidenceFilePath(projectRoot, nextSource.id),
|
|
149
|
+
index: benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE),
|
|
150
|
+
sources: benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE),
|
|
151
|
+
},
|
|
152
|
+
summary: {
|
|
153
|
+
approved: refreshed.approved.length,
|
|
154
|
+
candidates: refreshed.candidates.length,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function listBenchmarkWorkspace(projectRoot) {
|
|
160
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
161
|
+
const approved = await loadApprovedSources(projectRoot);
|
|
162
|
+
const candidates = await loadCandidateSources(projectRoot);
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
action: 'benchmark-list',
|
|
166
|
+
projectRoot,
|
|
167
|
+
approved,
|
|
168
|
+
candidates,
|
|
169
|
+
counts: {
|
|
170
|
+
approved: approved.length,
|
|
171
|
+
candidates: candidates.length,
|
|
172
|
+
},
|
|
173
|
+
recommendations: benchmarkRecommendations(candidates),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function listBenchmarkRecommendationsWorkspace(projectRoot) {
|
|
178
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
179
|
+
const candidates = await loadCandidateSources(projectRoot);
|
|
180
|
+
return benchmarkRecommendations(candidates);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function approveBenchmarkWorkspace(projectRoot, options = {}) {
|
|
184
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
185
|
+
const id = String(options.id ?? '').trim();
|
|
186
|
+
if (!id) {
|
|
187
|
+
throw new Error('Benchmark id is required for approve.');
|
|
188
|
+
}
|
|
189
|
+
const candidate = await readCandidateById(projectRoot, id);
|
|
190
|
+
if (!candidate) {
|
|
191
|
+
throw new Error(`Benchmark candidate not found: ${id}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const approved = await loadApprovedSources(projectRoot);
|
|
195
|
+
const approvedSource = normalizeSourceRecord({
|
|
196
|
+
...candidate,
|
|
197
|
+
status: 'approved',
|
|
198
|
+
approvedAt: timestamp(),
|
|
199
|
+
promotion: {
|
|
200
|
+
...(candidate.promotion ?? {}),
|
|
201
|
+
recommended: false,
|
|
202
|
+
recommendedAt: null,
|
|
203
|
+
approveCommand: null,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
const nextApproved = approved.filter((source) => source.id !== id);
|
|
207
|
+
nextApproved.push(approvedSource);
|
|
208
|
+
await writeApprovedSources(projectRoot, nextApproved);
|
|
209
|
+
await fs.rm(sourceFilePath(projectRoot, id), { force: true });
|
|
210
|
+
await writeText(evidenceFilePath(projectRoot, id), `${renderEvidence(approvedSource)}\n`);
|
|
211
|
+
const refreshed = await refreshBenchmarkIndex(projectRoot);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
action: 'benchmark-approve',
|
|
216
|
+
projectRoot,
|
|
217
|
+
source: approvedSource,
|
|
218
|
+
counts: {
|
|
219
|
+
approved: refreshed.approved.length,
|
|
220
|
+
candidates: refreshed.candidates.length,
|
|
221
|
+
},
|
|
222
|
+
files: {
|
|
223
|
+
sources: benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE),
|
|
224
|
+
index: benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
addBenchmarkWorkspace,
|
|
231
|
+
approveBenchmarkWorkspace,
|
|
232
|
+
listBenchmarkRecommendationsWorkspace,
|
|
233
|
+
listBenchmarkWorkspace,
|
|
234
|
+
observeBenchmarkSourceWorkspace,
|
|
235
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 生成提供给 Agent 集成层的 benchmark registry 摘要片段。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收项目根目录,并读取当前 approved benchmark sources。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出 Project Benchmark Registry Markdown section,供技能模板和上下文拼装复用。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 benchmark 展示适配层,连接 storage 读取结果与 agent-facing 文本输出。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖 storage 提供的 workspace 初始化与 approved source 读取能力。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 文案必须保持面向 Agent 使用场景,避免把 candidate 当成已确认事实来源写入摘要。
|
|
19
|
+
*/
|
|
20
|
+
import { ensureBenchmarkWorkspace, loadApprovedSources } from './storage.js';
|
|
21
|
+
|
|
22
|
+
async function renderApprovedBenchmarkRegistrySection(projectRoot) {
|
|
23
|
+
await ensureBenchmarkWorkspace(projectRoot);
|
|
24
|
+
const approved = await loadApprovedSources(projectRoot);
|
|
25
|
+
if (approved.length === 0) {
|
|
26
|
+
return [
|
|
27
|
+
'## Project Benchmark Registry',
|
|
28
|
+
'',
|
|
29
|
+
'- 当前项目还没有 approved benchmark source。',
|
|
30
|
+
'- 如需补充,用 `openprd benchmark add <url|repo|file>` 添加 candidate,再用 `openprd benchmark approve <id>` 纳入项目级 registry。',
|
|
31
|
+
'- 被用户采纳过的外部信源可用 `openprd benchmark observe <url|repo|file> --notes <text>` 累计证据;达到阈值后仍需要用户确认 approve。',
|
|
32
|
+
'- Agent 仍应先读取 `.openprd/benchmarks/index.md` 和 `.openprd/benchmarks/sources.yaml`,但 candidate inbox 不能当成长期事实来源。',
|
|
33
|
+
'',
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = [
|
|
38
|
+
'## Project Benchmark Registry',
|
|
39
|
+
'',
|
|
40
|
+
'- 先读取 `.openprd/benchmarks/index.md` 和 `.openprd/benchmarks/sources.yaml`。',
|
|
41
|
+
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map;`inbox/` candidate 只能作为待确认线索。',
|
|
42
|
+
'- `benchmark observe` 只累计被采纳信源的证据;达到阈值后仍需用户确认 approve。',
|
|
43
|
+
'- 每次最多优先挑 1-3 个与当前任务最相关的 approved source。',
|
|
44
|
+
'',
|
|
45
|
+
'### Approved Sources',
|
|
46
|
+
'',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const source of approved.slice(0, 20)) {
|
|
50
|
+
const location = source.repo ? `${source.repo} (${source.url})` : (source.url ?? source.path ?? 'unknown');
|
|
51
|
+
lines.push(`- \`${source.id}\` ${source.title}`);
|
|
52
|
+
lines.push(` - 场景: ${source.scenarios.join(', ') || '未分类'}`);
|
|
53
|
+
lines.push(` - 触发: ${source.triggerWhen.join(';') || '待补充'}`);
|
|
54
|
+
lines.push(` - 不适用: ${source.notFor.join(';') || '待补充'}`);
|
|
55
|
+
lines.push(` - 研究方式: ${source.researchMethod}`);
|
|
56
|
+
lines.push(` - 来源: ${location}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('');
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
renderApprovedBenchmarkRegistrySection,
|
|
64
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 负责 benchmark registry、source card 和 evidence 文本渲染。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收已经归一化的 approved/candidate source 数据。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出 index.md 和 evidence.md 所需的稳定 Markdown 片段。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 benchmark 展示层,只做字符串组装,不参与 IO、校验或业务状态变更。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 无外部业务依赖;由 storage、operations 和 registry 复用。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 修改渲染文案时必须保持现有字段含义不变,避免影响生成 skill、CLI 输出和历史 benchmark 文件阅读。
|
|
19
|
+
*/
|
|
20
|
+
function renderSourceCard(source) {
|
|
21
|
+
const location = source.url ?? source.path ?? 'unknown';
|
|
22
|
+
const scenarios = source.scenarios.length > 0 ? source.scenarios.join(', ') : '未分类';
|
|
23
|
+
const triggerWhen = source.triggerWhen.length > 0 ? source.triggerWhen.join(';') : '待补充';
|
|
24
|
+
const notFor = source.notFor.length > 0 ? source.notFor.join(';') : '待补充';
|
|
25
|
+
const lines = [
|
|
26
|
+
`### ${source.title} \`${source.id}\``,
|
|
27
|
+
'',
|
|
28
|
+
`- 状态: ${source.status}`,
|
|
29
|
+
`- 来源类型: ${source.sourceType}`,
|
|
30
|
+
`- 场景: ${scenarios}`,
|
|
31
|
+
`- 触发: ${triggerWhen}`,
|
|
32
|
+
`- 不适用: ${notFor}`,
|
|
33
|
+
`- 研究方式: ${source.researchMethod}`,
|
|
34
|
+
`- 来源: ${location}`,
|
|
35
|
+
`- 规范化信源: ${source.sourceKey ?? source.id}`,
|
|
36
|
+
`- 最近 ${source.promotion.windowDays} 天采纳: ${source.recentAdoptedCount}`,
|
|
37
|
+
`- 累计采纳: ${source.adoptedCount}`,
|
|
38
|
+
];
|
|
39
|
+
if (source.lastUsedAt) {
|
|
40
|
+
lines.push(`- 最近采纳时间: ${source.lastUsedAt}`);
|
|
41
|
+
}
|
|
42
|
+
if (source.promotion?.recommended) {
|
|
43
|
+
lines.push(`- 推荐: 最近 ${source.promotion.windowDays} 天已达到 ${source.recentAdoptedCount}/${source.promotion.threshold} 次采纳,建议确认后运行 \`${source.promotion.approveCommand}\``);
|
|
44
|
+
}
|
|
45
|
+
if (source.note) {
|
|
46
|
+
lines.push(`- 备注: ${source.note}`);
|
|
47
|
+
}
|
|
48
|
+
if (source.value) {
|
|
49
|
+
lines.push(`- 价值: ${source.value}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push('');
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderBenchmarkIndex(approved, candidates) {
|
|
56
|
+
const lines = [
|
|
57
|
+
'# OpenPrd Benchmark Registry',
|
|
58
|
+
'',
|
|
59
|
+
'## 规则',
|
|
60
|
+
'',
|
|
61
|
+
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。',
|
|
62
|
+
'- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。',
|
|
63
|
+
'- 被采纳信源先累计证据,达到阈值后只推荐 approve,不自动晋级。',
|
|
64
|
+
'- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。',
|
|
65
|
+
'',
|
|
66
|
+
'## Approved Sources',
|
|
67
|
+
'',
|
|
68
|
+
];
|
|
69
|
+
if (approved.length === 0) {
|
|
70
|
+
lines.push('- 暂无已批准来源。', '');
|
|
71
|
+
} else {
|
|
72
|
+
for (const source of approved) {
|
|
73
|
+
lines.push(renderSourceCard(source));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push('## Candidate Sources', '');
|
|
78
|
+
if (candidates.length === 0) {
|
|
79
|
+
lines.push('- 暂无待确认来源。', '');
|
|
80
|
+
} else {
|
|
81
|
+
for (const source of candidates) {
|
|
82
|
+
lines.push(renderSourceCard(source));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderEvidence(source) {
|
|
89
|
+
return [
|
|
90
|
+
`# ${source.title}`,
|
|
91
|
+
'',
|
|
92
|
+
`- ID: ${source.id}`,
|
|
93
|
+
`- 状态: ${source.status}`,
|
|
94
|
+
`- 场景: ${source.scenarios.join(', ') || '未分类'}`,
|
|
95
|
+
`- 触发: ${source.triggerWhen.join(';') || '待补充'}`,
|
|
96
|
+
`- 不适用: ${source.notFor.join(';') || '待补充'}`,
|
|
97
|
+
`- 研究方式: ${source.researchMethod}`,
|
|
98
|
+
`- 来源: ${source.url ?? source.path ?? 'unknown'}`,
|
|
99
|
+
`- 规范化信源: ${source.sourceKey ?? source.id}`,
|
|
100
|
+
`- 最近 ${source.promotion.windowDays} 天采纳: ${source.recentAdoptedCount}`,
|
|
101
|
+
`- 累计采纳: ${source.adoptedCount}`,
|
|
102
|
+
source.lastUsedAt ? `- 最近采纳时间: ${source.lastUsedAt}` : null,
|
|
103
|
+
source.promotion?.recommended ? `- 推荐命令: ${source.promotion.approveCommand}` : null,
|
|
104
|
+
'',
|
|
105
|
+
'## 备注',
|
|
106
|
+
'',
|
|
107
|
+
source.note ?? '待补充',
|
|
108
|
+
'',
|
|
109
|
+
].filter((line) => line !== null).join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
renderBenchmarkIndex,
|
|
114
|
+
renderEvidence,
|
|
115
|
+
};
|