@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
package/src/benchmark.js
CHANGED
|
@@ -1,817 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return slug || fallback;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function defaultSourcesFile() {
|
|
42
|
-
return {
|
|
43
|
-
version: 1,
|
|
44
|
-
schema: 'openprd.benchmarks.v1',
|
|
45
|
-
updatedAt: timestamp(),
|
|
46
|
-
sources: [],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function defaultIndex() {
|
|
51
|
-
return [
|
|
52
|
-
'# OpenPrd Benchmark Registry',
|
|
53
|
-
'',
|
|
54
|
-
'## 规则',
|
|
55
|
-
'',
|
|
56
|
-
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。',
|
|
57
|
-
'- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。',
|
|
58
|
-
'- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。',
|
|
59
|
-
'',
|
|
60
|
-
'## Approved Sources',
|
|
61
|
-
'',
|
|
62
|
-
'- 暂无已批准来源。',
|
|
63
|
-
'',
|
|
64
|
-
'## Candidate Sources',
|
|
65
|
-
'',
|
|
66
|
-
'- 暂无待确认来源。',
|
|
67
|
-
'',
|
|
68
|
-
].join('\n');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isHttpUrl(value) {
|
|
72
|
-
return /^https?:\/\//i.test(String(value ?? '').trim());
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isGitHubShorthand(value) {
|
|
76
|
-
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(String(value ?? '').trim());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function normalizeRemoteUrl(value) {
|
|
80
|
-
if (isGitHubShorthand(value)) {
|
|
81
|
-
return `https://github.com/${String(value).trim()}`;
|
|
82
|
-
}
|
|
83
|
-
return String(value ?? '').trim();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function toRepoSlug(urlString) {
|
|
87
|
-
try {
|
|
88
|
-
const url = new URL(urlString);
|
|
89
|
-
if (!/github\.com$/i.test(url.hostname)) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
const segments = url.pathname.split('/').filter(Boolean);
|
|
93
|
-
if (segments.length < 2) {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
return `${segments[0]}/${segments[1]}`.replace(/\.git$/i, '');
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function inferSourceType(urlString, sourceValue) {
|
|
103
|
-
if (sourceValue?.kind === 'local-file') {
|
|
104
|
-
return 'local-file';
|
|
105
|
-
}
|
|
106
|
-
const normalized = String(urlString ?? '').toLowerCase();
|
|
107
|
-
if (normalized.includes('github.com/')) {
|
|
108
|
-
return 'github';
|
|
109
|
-
}
|
|
110
|
-
if (
|
|
111
|
-
normalized.includes('/docs')
|
|
112
|
-
|| normalized.includes('developers.openai.com')
|
|
113
|
-
|| normalized.includes('platform.claude.com')
|
|
114
|
-
|| normalized.includes('code.claude.com')
|
|
115
|
-
|| normalized.includes('ai.google.dev')
|
|
116
|
-
) {
|
|
117
|
-
return 'official-docs';
|
|
118
|
-
}
|
|
119
|
-
if (
|
|
120
|
-
normalized.includes('/blog/')
|
|
121
|
-
|| normalized.includes('/engineering/')
|
|
122
|
-
|| normalized.includes('openai.com/index/')
|
|
123
|
-
|| normalized.includes('anthropic.com/engineering/')
|
|
124
|
-
|| normalized.includes('langchain.com/blog/')
|
|
125
|
-
|| normalized.includes('manus.im/blog/')
|
|
126
|
-
) {
|
|
127
|
-
return 'engineering-article';
|
|
128
|
-
}
|
|
129
|
-
return 'web';
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function inferResearchMethod(sourceType) {
|
|
133
|
-
if (sourceType === 'github') {
|
|
134
|
-
return 'deepwiki_then_github';
|
|
135
|
-
}
|
|
136
|
-
if (sourceType === 'official-docs') {
|
|
137
|
-
return 'context7_then_official';
|
|
138
|
-
}
|
|
139
|
-
if (sourceType === 'local-file') {
|
|
140
|
-
return 'local_read_first';
|
|
141
|
-
}
|
|
142
|
-
return 'official_page_first';
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function dedupe(values) {
|
|
146
|
-
return [...new Set((values ?? []).filter(Boolean))];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function inferScenarios(text) {
|
|
150
|
-
const normalized = String(text ?? '').toLowerCase();
|
|
151
|
-
const scenarios = [];
|
|
152
|
-
const add = (value) => {
|
|
153
|
-
if (!scenarios.includes(value)) {
|
|
154
|
-
scenarios.push(value);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
if (/(openprd|openspec|superpowers|prd|product requirements?)/i.test(normalized)) {
|
|
159
|
-
add('openprd-product');
|
|
160
|
-
}
|
|
161
|
-
if (/(cli|doctor|dry-run|command discoverability|developer experience|dx)/i.test(normalized)) {
|
|
162
|
-
add('cli-tooling');
|
|
163
|
-
add('developer-experience');
|
|
164
|
-
}
|
|
165
|
-
if (/(skill|skills|skill discovery|skill install|skill router)/i.test(normalized)) {
|
|
166
|
-
add('skill-design');
|
|
167
|
-
}
|
|
168
|
-
if (/(harness|agent|long-running|workflow loop|managed agents)/i.test(normalized)) {
|
|
169
|
-
add('agent-harness');
|
|
170
|
-
}
|
|
171
|
-
if (/(code review|pr review|pull request review|review lane|reviewer agreement|false positive|hallucination filter|merge recommendation|critical\/high\/medium\/low|deep review|independent reviewers|交叉验证|误报过滤|合并建议|深度代码审查|并行审查|审查分级|独立审查|reviewer agreement)/i.test(normalized)) {
|
|
172
|
-
add('pr-review-harness');
|
|
173
|
-
add('agent-harness');
|
|
174
|
-
}
|
|
175
|
-
if (/(context engineering|context window|context registry|retrieval)/i.test(normalized)) {
|
|
176
|
-
add('context-engineering');
|
|
177
|
-
}
|
|
178
|
-
if (/(prompt engineering|prompting|system prompt|prompt guidance)/i.test(normalized)) {
|
|
179
|
-
add('prompt-engineering');
|
|
180
|
-
}
|
|
181
|
-
if (/(icon|icons|iconfont|lucide|tabler|react icons|phosphor|lobehub|techicons|thiings|图标|图标站|图标库|视觉资产)/i.test(normalized)) {
|
|
182
|
-
add('icon-resources');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return scenarios;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function inferTriggerWhen(scenarios) {
|
|
189
|
-
const lines = [];
|
|
190
|
-
for (const scenario of scenarios) {
|
|
191
|
-
if (scenario === 'openprd-product') {
|
|
192
|
-
lines.push('设计 OpenPrd / PRD 工作流、需求入口、状态承接或生成规则');
|
|
193
|
-
}
|
|
194
|
-
if (scenario === 'cli-tooling') {
|
|
195
|
-
lines.push('设计 CLI 命令、doctor、dry-run、错误提示、确认流程或可发现性');
|
|
196
|
-
}
|
|
197
|
-
if (scenario === 'skill-design') {
|
|
198
|
-
lines.push('设计 skill 触发、metadata、安装方式、自动识别或项目级覆盖规则');
|
|
199
|
-
}
|
|
200
|
-
if (scenario === 'agent-harness') {
|
|
201
|
-
lines.push('设计 Agent harness、长程任务、状态持久化、验证门禁或人工接管');
|
|
202
|
-
}
|
|
203
|
-
if (scenario === 'pr-review-harness') {
|
|
204
|
-
lines.push('设计 merge 前高风险复核、独立 reviewer 交叉验证、误报过滤、reviewer agreement 或 merge recommendation');
|
|
205
|
-
}
|
|
206
|
-
if (scenario === 'context-engineering') {
|
|
207
|
-
lines.push('设计上下文常驻、按需检索、registry/索引或证据优先级');
|
|
208
|
-
}
|
|
209
|
-
if (scenario === 'prompt-engineering') {
|
|
210
|
-
lines.push('设计系统提示、skill 提示、任务提示或 structured prompting');
|
|
211
|
-
}
|
|
212
|
-
if (scenario === 'developer-experience') {
|
|
213
|
-
lines.push('设计开发者体验、命令组合方式、输出结构或错误恢复路径');
|
|
214
|
-
}
|
|
215
|
-
if (scenario === 'icon-resources') {
|
|
216
|
-
lines.push('选择 UI、AI、技术栈、3D 或功能图标资源站,或选择 Lucide、Tabler、React Icons 等实现库');
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return dedupe(lines).slice(0, 3);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function inferNotFor(scenarios) {
|
|
223
|
-
const exclusions = [];
|
|
224
|
-
if (!scenarios.includes('openprd-product')) {
|
|
225
|
-
exclusions.push('普通 PRD / 产品流程设计');
|
|
226
|
-
}
|
|
227
|
-
if (!scenarios.includes('cli-tooling')) {
|
|
228
|
-
exclusions.push('与 CLI 无关的一次性 UI 视觉问题');
|
|
229
|
-
}
|
|
230
|
-
if (!scenarios.includes('agent-harness')) {
|
|
231
|
-
exclusions.push('单次脚本报错或纯环境权限问题');
|
|
232
|
-
}
|
|
233
|
-
if (!scenarios.includes('pr-review-harness')) {
|
|
234
|
-
exclusions.push('与 PR 审查 lane 无关的普通实现任务');
|
|
235
|
-
} else {
|
|
236
|
-
exclusions.push('默认给每个低风险 PR 拉起多 reviewer 并行审查');
|
|
237
|
-
}
|
|
238
|
-
if (!scenarios.includes('prompt-engineering')) {
|
|
239
|
-
exclusions.push('不涉及提示词或上下文工程的纯实现细节');
|
|
240
|
-
}
|
|
241
|
-
if (scenarios.includes('icon-resources')) {
|
|
242
|
-
exclusions.push('不涉及图标、视觉资产或图标实现库选型的任务');
|
|
243
|
-
}
|
|
244
|
-
return dedupe(exclusions).slice(0, 3);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function titleFromSource(sourceValue, normalizedUrl, sourceType) {
|
|
248
|
-
if (sourceValue.kind === 'local-file') {
|
|
249
|
-
return path.basename(sourceValue.absolutePath);
|
|
250
|
-
}
|
|
251
|
-
if (sourceType === 'github') {
|
|
252
|
-
return toRepoSlug(normalizedUrl) ?? normalizedUrl;
|
|
253
|
-
}
|
|
254
|
-
try {
|
|
255
|
-
const url = new URL(normalizedUrl);
|
|
256
|
-
const lastSegment = url.pathname.split('/').filter(Boolean).at(-1);
|
|
257
|
-
return lastSegment ? `${url.hostname}/${lastSegment}` : url.hostname;
|
|
258
|
-
} catch {
|
|
259
|
-
return normalizedUrl;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function normalizeSourceRecord(record) {
|
|
264
|
-
return {
|
|
265
|
-
id: record.id,
|
|
266
|
-
title: record.title,
|
|
267
|
-
scope: record.scope ?? 'project',
|
|
268
|
-
status: record.status,
|
|
269
|
-
sourceType: record.sourceType,
|
|
270
|
-
url: record.url ?? null,
|
|
271
|
-
path: record.path ?? null,
|
|
272
|
-
repo: record.repo ?? null,
|
|
273
|
-
researchMethod: record.researchMethod,
|
|
274
|
-
scenarios: dedupe(record.scenarios ?? []),
|
|
275
|
-
triggerWhen: dedupe(record.triggerWhen ?? []),
|
|
276
|
-
notFor: dedupe(record.notFor ?? []),
|
|
277
|
-
note: record.note ?? null,
|
|
278
|
-
value: record.value ?? null,
|
|
279
|
-
addedAt: record.addedAt ?? timestamp(),
|
|
280
|
-
approvedAt: record.approvedAt ?? null,
|
|
281
|
-
lastVerified: record.lastVerified ?? null,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function ensureOpenPrdWorkspace(projectRoot) {
|
|
286
|
-
const workspaceRoot = cjoin(projectRoot, '.openprd');
|
|
287
|
-
if (!(await exists(workspaceRoot))) {
|
|
288
|
-
throw new Error('Project is not initialized with OpenPrd. Run `openprd init .` first.');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
async function ensureBenchmarkWorkspace(projectRoot) {
|
|
293
|
-
await ensureOpenPrdWorkspace(projectRoot);
|
|
294
|
-
await fs.mkdir(benchmarkPath(projectRoot, BENCHMARK_INBOX_DIR), { recursive: true });
|
|
295
|
-
await fs.mkdir(benchmarkPath(projectRoot, BENCHMARK_EVIDENCE_DIR), { recursive: true });
|
|
296
|
-
if (!(await exists(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE)))) {
|
|
297
|
-
await writeYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE), defaultSourcesFile());
|
|
298
|
-
}
|
|
299
|
-
if (!(await exists(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE)))) {
|
|
300
|
-
await writeText(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE), `${defaultIndex()}\n`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function loadApprovedSources(projectRoot) {
|
|
305
|
-
const payload = await readYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE)).catch(() => defaultSourcesFile());
|
|
306
|
-
return Array.isArray(payload.sources) ? payload.sources.map(normalizeSourceRecord) : [];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function loadCandidateSources(projectRoot) {
|
|
310
|
-
const inboxDir = benchmarkPath(projectRoot, BENCHMARK_INBOX_DIR);
|
|
311
|
-
const entries = await fs.readdir(inboxDir, { withFileTypes: true }).catch(() => []);
|
|
312
|
-
const candidates = [];
|
|
313
|
-
for (const entry of entries) {
|
|
314
|
-
if (!entry.isFile() || !/\.(yaml|yml)$/i.test(entry.name)) {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
const filePath = cjoin(inboxDir, entry.name);
|
|
318
|
-
const payload = await readYaml(filePath).catch(() => null);
|
|
319
|
-
if (payload && typeof payload === 'object') {
|
|
320
|
-
candidates.push(normalizeSourceRecord(payload));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
return candidates.sort((left, right) => left.id.localeCompare(right.id));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function renderSourceCard(source) {
|
|
327
|
-
const location = source.url ?? source.path ?? 'unknown';
|
|
328
|
-
const scenarios = source.scenarios.length > 0 ? source.scenarios.join(', ') : '未分类';
|
|
329
|
-
const triggerWhen = source.triggerWhen.length > 0 ? source.triggerWhen.join(';') : '待补充';
|
|
330
|
-
const notFor = source.notFor.length > 0 ? source.notFor.join(';') : '待补充';
|
|
331
|
-
const lines = [
|
|
332
|
-
`### ${source.title} \`${source.id}\``,
|
|
333
|
-
'',
|
|
334
|
-
`- 状态: ${source.status}`,
|
|
335
|
-
`- 来源类型: ${source.sourceType}`,
|
|
336
|
-
`- 场景: ${scenarios}`,
|
|
337
|
-
`- 触发: ${triggerWhen}`,
|
|
338
|
-
`- 不适用: ${notFor}`,
|
|
339
|
-
`- 研究方式: ${source.researchMethod}`,
|
|
340
|
-
`- 来源: ${location}`,
|
|
341
|
-
];
|
|
342
|
-
if (source.note) {
|
|
343
|
-
lines.push(`- 备注: ${source.note}`);
|
|
344
|
-
}
|
|
345
|
-
if (source.value) {
|
|
346
|
-
lines.push(`- 价值: ${source.value}`);
|
|
347
|
-
}
|
|
348
|
-
lines.push('');
|
|
349
|
-
return lines.join('\n');
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function renderBenchmarkIndex(approved, candidates) {
|
|
353
|
-
const lines = [
|
|
354
|
-
'# OpenPrd Benchmark Registry',
|
|
355
|
-
'',
|
|
356
|
-
'## 规则',
|
|
357
|
-
'',
|
|
358
|
-
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map。',
|
|
359
|
-
'- `inbox/` 里的 candidate 只表示待确认线索,不表示长期最佳实践。',
|
|
360
|
-
'- 每次只挑 1-3 个高相关来源;来源目录不是事实来源。',
|
|
361
|
-
'',
|
|
362
|
-
'## Approved Sources',
|
|
363
|
-
'',
|
|
364
|
-
];
|
|
365
|
-
if (approved.length === 0) {
|
|
366
|
-
lines.push('- 暂无已批准来源。', '');
|
|
367
|
-
} else {
|
|
368
|
-
for (const source of approved) {
|
|
369
|
-
lines.push(renderSourceCard(source));
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
lines.push('## Candidate Sources', '');
|
|
373
|
-
if (candidates.length === 0) {
|
|
374
|
-
lines.push('- 暂无待确认来源。', '');
|
|
375
|
-
} else {
|
|
376
|
-
for (const source of candidates) {
|
|
377
|
-
lines.push(renderSourceCard(source));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return `${lines.join('\n').trimEnd()}\n`;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async function writeApprovedSources(projectRoot, sources) {
|
|
384
|
-
const next = {
|
|
385
|
-
...defaultSourcesFile(),
|
|
386
|
-
updatedAt: timestamp(),
|
|
387
|
-
sources: sources
|
|
388
|
-
.map(normalizeSourceRecord)
|
|
389
|
-
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
390
|
-
};
|
|
391
|
-
await writeYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE), next);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async function refreshBenchmarkIndex(projectRoot) {
|
|
395
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
396
|
-
const candidates = await loadCandidateSources(projectRoot);
|
|
397
|
-
await writeText(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE), renderBenchmarkIndex(approved, candidates));
|
|
398
|
-
return { approved, candidates };
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async function resolveSourceInput(projectRoot, source) {
|
|
402
|
-
const raw = String(source ?? '').trim();
|
|
403
|
-
if (!raw) {
|
|
404
|
-
throw new Error('Benchmark source is required.');
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (isGitHubShorthand(raw) || isHttpUrl(raw)) {
|
|
408
|
-
const url = normalizeRemoteUrl(raw);
|
|
409
|
-
return { kind: 'remote-url', raw, url };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const absolutePath = path.isAbsolute(raw) ? raw : path.resolve(projectRoot, raw);
|
|
413
|
-
if (await exists(absolutePath)) {
|
|
414
|
-
return {
|
|
415
|
-
kind: 'local-file',
|
|
416
|
-
raw,
|
|
417
|
-
absolutePath,
|
|
418
|
-
relativePath: path.relative(projectRoot, absolutePath) || path.basename(absolutePath),
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
throw new Error(`Cannot resolve benchmark source: ${raw}`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function buildSourceValue(sourceValue, note) {
|
|
426
|
-
const normalizedUrl = sourceValue.kind === 'remote-url' ? sourceValue.url : null;
|
|
427
|
-
const sourceType = inferSourceType(normalizedUrl, sourceValue);
|
|
428
|
-
const combinedText = [sourceValue.raw, normalizedUrl, sourceValue.relativePath, note].filter(Boolean).join(' ');
|
|
429
|
-
const scenarios = inferScenarios(combinedText);
|
|
430
|
-
const title = titleFromSource(sourceValue, normalizedUrl, sourceType);
|
|
431
|
-
const repo = normalizedUrl ? toRepoSlug(normalizedUrl) : null;
|
|
432
|
-
const idSeed = repo ?? sourceValue.relativePath ?? title;
|
|
433
|
-
const id = slugify(idSeed, 'benchmark-source');
|
|
434
|
-
|
|
435
|
-
return normalizeSourceRecord({
|
|
436
|
-
id,
|
|
437
|
-
title,
|
|
438
|
-
scope: 'project',
|
|
439
|
-
status: 'candidate',
|
|
440
|
-
sourceType,
|
|
441
|
-
url: normalizedUrl,
|
|
442
|
-
path: sourceValue.kind === 'local-file' ? sourceValue.relativePath : null,
|
|
443
|
-
repo,
|
|
444
|
-
researchMethod: inferResearchMethod(sourceType),
|
|
445
|
-
scenarios,
|
|
446
|
-
triggerWhen: inferTriggerWhen(scenarios),
|
|
447
|
-
notFor: inferNotFor(scenarios),
|
|
448
|
-
note: note ?? null,
|
|
449
|
-
value: note ?? null,
|
|
450
|
-
addedAt: timestamp(),
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function sourceIdentity(source) {
|
|
455
|
-
if (source.url) {
|
|
456
|
-
return `url:${source.url.toLowerCase()}`;
|
|
457
|
-
}
|
|
458
|
-
if (source.path) {
|
|
459
|
-
return `path:${source.path}`;
|
|
460
|
-
}
|
|
461
|
-
return `id:${source.id}`;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function duplicateSource(existingSources, candidate) {
|
|
465
|
-
const wanted = sourceIdentity(candidate);
|
|
466
|
-
return existingSources.find((source) => source.id === candidate.id || sourceIdentity(source) === wanted) ?? null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function renderEvidence(source) {
|
|
470
|
-
return [
|
|
471
|
-
`# ${source.title}`,
|
|
472
|
-
'',
|
|
473
|
-
`- ID: ${source.id}`,
|
|
474
|
-
`- 状态: ${source.status}`,
|
|
475
|
-
`- 场景: ${source.scenarios.join(', ') || '未分类'}`,
|
|
476
|
-
`- 触发: ${source.triggerWhen.join(';') || '待补充'}`,
|
|
477
|
-
`- 不适用: ${source.notFor.join(';') || '待补充'}`,
|
|
478
|
-
`- 研究方式: ${source.researchMethod}`,
|
|
479
|
-
`- 来源: ${source.url ?? source.path ?? 'unknown'}`,
|
|
480
|
-
'',
|
|
481
|
-
'## 备注',
|
|
482
|
-
'',
|
|
483
|
-
source.note ?? '待补充',
|
|
484
|
-
'',
|
|
485
|
-
].join('\n');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function sourceFilePath(projectRoot, id) {
|
|
489
|
-
return benchmarkPath(projectRoot, cjoin(BENCHMARK_INBOX_DIR, `${id}.yaml`));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function evidenceFilePath(projectRoot, id) {
|
|
493
|
-
return benchmarkPath(projectRoot, cjoin(BENCHMARK_EVIDENCE_DIR, `${id}.md`));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export async function addBenchmarkWorkspace(projectRoot, options = {}) {
|
|
497
|
-
await ensureBenchmarkWorkspace(projectRoot);
|
|
498
|
-
const sourceValue = await resolveSourceInput(projectRoot, options.source ?? options.target ?? options.reference ?? null);
|
|
499
|
-
const source = buildSourceValue(sourceValue, options.notes ?? null);
|
|
500
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
501
|
-
const candidates = await loadCandidateSources(projectRoot);
|
|
502
|
-
const duplicate = duplicateSource([...approved, ...candidates], source);
|
|
503
|
-
if (duplicate) {
|
|
504
|
-
return {
|
|
505
|
-
ok: false,
|
|
506
|
-
action: 'benchmark-add',
|
|
507
|
-
projectRoot,
|
|
508
|
-
error: `Benchmark source already exists: ${duplicate.id}`,
|
|
509
|
-
duplicate,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const candidatePath = sourceFilePath(projectRoot, source.id);
|
|
514
|
-
const evidencePath = evidenceFilePath(projectRoot, source.id);
|
|
515
|
-
await writeYaml(candidatePath, source);
|
|
516
|
-
await writeText(evidencePath, `${renderEvidence(source)}\n`);
|
|
517
|
-
const refreshed = await refreshBenchmarkIndex(projectRoot);
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
ok: true,
|
|
521
|
-
action: 'benchmark-add',
|
|
522
|
-
projectRoot,
|
|
523
|
-
source,
|
|
524
|
-
files: {
|
|
525
|
-
candidate: candidatePath,
|
|
526
|
-
evidence: evidencePath,
|
|
527
|
-
index: benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE),
|
|
528
|
-
sources: benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE),
|
|
529
|
-
},
|
|
530
|
-
summary: {
|
|
531
|
-
approved: refreshed.approved.length,
|
|
532
|
-
candidates: refreshed.candidates.length,
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export async function listBenchmarkWorkspace(projectRoot) {
|
|
538
|
-
await ensureBenchmarkWorkspace(projectRoot);
|
|
539
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
540
|
-
const candidates = await loadCandidateSources(projectRoot);
|
|
541
|
-
return {
|
|
542
|
-
ok: true,
|
|
543
|
-
action: 'benchmark-list',
|
|
544
|
-
projectRoot,
|
|
545
|
-
approved,
|
|
546
|
-
candidates,
|
|
547
|
-
counts: {
|
|
548
|
-
approved: approved.length,
|
|
549
|
-
candidates: candidates.length,
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async function readCandidateById(projectRoot, id) {
|
|
555
|
-
const filePath = sourceFilePath(projectRoot, id);
|
|
556
|
-
if (!(await exists(filePath))) {
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
const payload = await readYaml(filePath);
|
|
560
|
-
return normalizeSourceRecord(payload);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
export async function approveBenchmarkWorkspace(projectRoot, options = {}) {
|
|
564
|
-
await ensureBenchmarkWorkspace(projectRoot);
|
|
565
|
-
const id = String(options.id ?? '').trim();
|
|
566
|
-
if (!id) {
|
|
567
|
-
throw new Error('Benchmark id is required for approve.');
|
|
568
|
-
}
|
|
569
|
-
const candidate = await readCandidateById(projectRoot, id);
|
|
570
|
-
if (!candidate) {
|
|
571
|
-
throw new Error(`Benchmark candidate not found: ${id}`);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
575
|
-
const approvedSource = normalizeSourceRecord({
|
|
576
|
-
...candidate,
|
|
577
|
-
status: 'approved',
|
|
578
|
-
approvedAt: timestamp(),
|
|
579
|
-
});
|
|
580
|
-
const nextApproved = approved.filter((source) => source.id !== id);
|
|
581
|
-
nextApproved.push(approvedSource);
|
|
582
|
-
await writeApprovedSources(projectRoot, nextApproved);
|
|
583
|
-
await fs.rm(sourceFilePath(projectRoot, id), { force: true });
|
|
584
|
-
await writeText(evidenceFilePath(projectRoot, id), `${renderEvidence(approvedSource)}\n`);
|
|
585
|
-
const refreshed = await refreshBenchmarkIndex(projectRoot);
|
|
586
|
-
|
|
587
|
-
return {
|
|
588
|
-
ok: true,
|
|
589
|
-
action: 'benchmark-approve',
|
|
590
|
-
projectRoot,
|
|
591
|
-
source: approvedSource,
|
|
592
|
-
counts: {
|
|
593
|
-
approved: refreshed.approved.length,
|
|
594
|
-
candidates: refreshed.candidates.length,
|
|
595
|
-
},
|
|
596
|
-
files: {
|
|
597
|
-
sources: benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE),
|
|
598
|
-
index: benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE),
|
|
599
|
-
},
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function fetchWithTimeout(urlString, method, timeoutMs = 4000) {
|
|
604
|
-
const controller = new AbortController();
|
|
605
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
606
|
-
try {
|
|
607
|
-
return await fetch(urlString, {
|
|
608
|
-
method,
|
|
609
|
-
redirect: 'follow',
|
|
610
|
-
signal: controller.signal,
|
|
611
|
-
});
|
|
612
|
-
} finally {
|
|
613
|
-
clearTimeout(timeout);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function isReachableProbeStatus(status) {
|
|
618
|
-
return (status >= 200 && status < 400) || status === 401 || status === 403 || status === 405;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
async function probeRemoteSourceWithCurl(urlString, timeoutMs = 6000) {
|
|
622
|
-
return await new Promise((resolve) => {
|
|
623
|
-
const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
624
|
-
const child = spawn(
|
|
625
|
-
'curl',
|
|
626
|
-
[
|
|
627
|
-
'-L',
|
|
628
|
-
'-o',
|
|
629
|
-
'/dev/null',
|
|
630
|
-
'-s',
|
|
631
|
-
'-w',
|
|
632
|
-
'%{http_code}',
|
|
633
|
-
'--max-time',
|
|
634
|
-
String(timeoutSeconds),
|
|
635
|
-
urlString,
|
|
636
|
-
],
|
|
637
|
-
{
|
|
638
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
639
|
-
},
|
|
640
|
-
);
|
|
641
|
-
|
|
642
|
-
let stdout = '';
|
|
643
|
-
let stderr = '';
|
|
644
|
-
let settled = false;
|
|
645
|
-
const settle = (value) => {
|
|
646
|
-
if (settled) return;
|
|
647
|
-
settled = true;
|
|
648
|
-
resolve(value);
|
|
649
|
-
};
|
|
650
|
-
|
|
651
|
-
const timeout = setTimeout(() => {
|
|
652
|
-
child.kill('SIGKILL');
|
|
653
|
-
settle({ ok: false, reason: 'curl probe timeout' });
|
|
654
|
-
}, timeoutMs + 250);
|
|
655
|
-
|
|
656
|
-
child.stdout.on('data', (chunk) => {
|
|
657
|
-
stdout += String(chunk);
|
|
658
|
-
});
|
|
659
|
-
child.stderr.on('data', (chunk) => {
|
|
660
|
-
stderr += String(chunk);
|
|
661
|
-
});
|
|
662
|
-
child.on('error', (error) => {
|
|
663
|
-
clearTimeout(timeout);
|
|
664
|
-
settle({ ok: false, reason: error instanceof Error ? error.message : String(error) });
|
|
665
|
-
});
|
|
666
|
-
child.on('close', (code) => {
|
|
667
|
-
clearTimeout(timeout);
|
|
668
|
-
const status = Number.parseInt(stdout.trim(), 10);
|
|
669
|
-
if (Number.isInteger(status) && isReachableProbeStatus(status)) {
|
|
670
|
-
settle({ ok: true, status, via: 'curl' });
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
if (Number.isInteger(status) && status > 0) {
|
|
674
|
-
settle({ ok: false, status, reason: `HTTP ${status}` });
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const detail = stderr.trim() || `curl exit ${code ?? 'unknown'}`;
|
|
678
|
-
settle({ ok: false, reason: detail });
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
async function probeRemoteSource(urlString) {
|
|
684
|
-
try {
|
|
685
|
-
const headResponse = await fetchWithTimeout(urlString, 'HEAD');
|
|
686
|
-
if (isReachableProbeStatus(headResponse.status)) {
|
|
687
|
-
return { ok: true, status: headResponse.status };
|
|
688
|
-
}
|
|
689
|
-
return { ok: false, status: headResponse.status, reason: `HTTP ${headResponse.status}` };
|
|
690
|
-
} catch {
|
|
691
|
-
try {
|
|
692
|
-
const getResponse = await fetchWithTimeout(urlString, 'GET', 5000);
|
|
693
|
-
if (isReachableProbeStatus(getResponse.status)) {
|
|
694
|
-
return { ok: true, status: getResponse.status };
|
|
695
|
-
}
|
|
696
|
-
return { ok: false, status: getResponse.status, reason: `HTTP ${getResponse.status}` };
|
|
697
|
-
} catch (error) {
|
|
698
|
-
const fallback = await probeRemoteSourceWithCurl(urlString);
|
|
699
|
-
if (fallback.ok) {
|
|
700
|
-
return fallback;
|
|
701
|
-
}
|
|
702
|
-
return { ok: false, reason: fallback.reason ?? (error instanceof Error ? error.message : String(error)) };
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function hasOverbroadTrigger(source) {
|
|
708
|
-
if (!Array.isArray(source.triggerWhen) || source.triggerWhen.length === 0) {
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
const combined = source.triggerWhen.join(' ').toLowerCase();
|
|
712
|
-
return OVERBROAD_TRIGGER_TOKENS.some((token) => combined.includes(token.toLowerCase()));
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function normalizeCheckedSource(source) {
|
|
716
|
-
return normalizeSourceRecord({
|
|
717
|
-
...source,
|
|
718
|
-
lastVerified: timestamp(),
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
export async function verifyBenchmarkWorkspace(projectRoot) {
|
|
723
|
-
await ensureBenchmarkWorkspace(projectRoot);
|
|
724
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
725
|
-
const candidates = await loadCandidateSources(projectRoot);
|
|
726
|
-
const allSources = [...approved, ...candidates];
|
|
727
|
-
const checks = [];
|
|
728
|
-
const seenIds = new Map();
|
|
729
|
-
const seenLocations = new Map();
|
|
730
|
-
const approvedUpdates = new Map();
|
|
731
|
-
const candidateUpdates = new Map();
|
|
732
|
-
|
|
733
|
-
for (const source of allSources) {
|
|
734
|
-
const issues = [];
|
|
735
|
-
if (seenIds.has(source.id)) {
|
|
736
|
-
issues.push({ level: 'error', code: 'duplicate-id', message: `Duplicate benchmark id with ${seenIds.get(source.id)}` });
|
|
737
|
-
} else {
|
|
738
|
-
seenIds.set(source.id, source.id);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const identity = sourceIdentity(source);
|
|
742
|
-
if (seenLocations.has(identity)) {
|
|
743
|
-
issues.push({ level: 'error', code: 'duplicate-source', message: `Duplicate benchmark source with ${seenLocations.get(identity)}` });
|
|
744
|
-
} else {
|
|
745
|
-
seenLocations.set(identity, source.id);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
if (source.url) {
|
|
749
|
-
try {
|
|
750
|
-
new URL(source.url);
|
|
751
|
-
} catch {
|
|
752
|
-
issues.push({ level: 'error', code: 'invalid-url', message: `Invalid URL: ${source.url}` });
|
|
753
|
-
}
|
|
754
|
-
if (!issues.some((issue) => issue.code === 'invalid-url')) {
|
|
755
|
-
const probe = await probeRemoteSource(source.url);
|
|
756
|
-
if (!probe.ok) {
|
|
757
|
-
issues.push({ level: 'error', code: 'unreachable-source', message: `Unreachable source: ${source.url} (${probe.reason ?? 'unknown'})` });
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
if (source.path) {
|
|
763
|
-
const absolutePath = path.resolve(projectRoot, source.path);
|
|
764
|
-
if (!(await exists(absolutePath))) {
|
|
765
|
-
issues.push({ level: 'error', code: 'missing-local-source', message: `Missing local source: ${source.path}` });
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
if (!Array.isArray(source.scenarios) || source.scenarios.length === 0) {
|
|
770
|
-
issues.push({ level: 'warning', code: 'missing-scenarios', message: 'Missing benchmark scenarios.' });
|
|
771
|
-
}
|
|
772
|
-
if (hasOverbroadTrigger(source)) {
|
|
773
|
-
issues.push({ level: 'warning', code: 'overbroad-trigger', message: 'Trigger rules are too broad or missing.' });
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const ok = !issues.some((issue) => issue.level === 'error');
|
|
777
|
-
const nextSource = ok ? normalizeCheckedSource(source) : source;
|
|
778
|
-
if (source.status === 'approved') {
|
|
779
|
-
approvedUpdates.set(source.id, nextSource);
|
|
780
|
-
} else {
|
|
781
|
-
candidateUpdates.set(source.id, nextSource);
|
|
782
|
-
}
|
|
783
|
-
checks.push({
|
|
784
|
-
id: source.id,
|
|
785
|
-
title: source.title,
|
|
786
|
-
status: source.status,
|
|
787
|
-
ok,
|
|
788
|
-
issues,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const approvedNext = approved.map((source) => approvedUpdates.get(source.id) ?? source);
|
|
793
|
-
const candidateNext = candidates.map((source) => candidateUpdates.get(source.id) ?? source);
|
|
794
|
-
await writeApprovedSources(projectRoot, approvedNext);
|
|
795
|
-
for (const source of candidateNext) {
|
|
796
|
-
await writeYaml(sourceFilePath(projectRoot, source.id), source);
|
|
797
|
-
}
|
|
798
|
-
await refreshBenchmarkIndex(projectRoot);
|
|
799
|
-
|
|
800
|
-
const errors = checks.flatMap((check) => check.issues.filter((issue) => issue.level === 'error').map((issue) => `${check.id}: ${issue.message}`));
|
|
801
|
-
const warnings = checks.flatMap((check) => check.issues.filter((issue) => issue.level !== 'error').map((issue) => `${check.id}: ${issue.message}`));
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
ok: errors.length === 0,
|
|
805
|
-
action: 'benchmark-verify',
|
|
806
|
-
projectRoot,
|
|
807
|
-
checkedAt: timestamp(),
|
|
808
|
-
checks,
|
|
809
|
-
errors,
|
|
810
|
-
warnings,
|
|
811
|
-
};
|
|
812
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 作为 benchmark 子系统的对外入口,组装 add/observe/list/approve/verify 与摘要渲染能力。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收项目根目录和 benchmark action/options,并分发到对应职责模块。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 导出保持兼容的 benchmark workspace API、registry 渲染函数和共享常量。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于 OpenPrd CLI 应用边界,只负责模块拼装与 action 路由,不再承载具体 benchmark 细节实现。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 依赖 benchmark 子目录中的 operations、verify、registry、storage 和 constants 模块。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 对外导出名称和返回契约必须保持稳定;新增职责优先放入子模块,再由这里做薄组装。
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
addBenchmarkWorkspace,
|
|
22
|
+
approveBenchmarkWorkspace,
|
|
23
|
+
listBenchmarkRecommendationsWorkspace,
|
|
24
|
+
listBenchmarkWorkspace,
|
|
25
|
+
observeBenchmarkSourceWorkspace,
|
|
26
|
+
} from './benchmark/operations.js';
|
|
27
|
+
import { renderApprovedBenchmarkRegistrySection } from './benchmark/registry.js';
|
|
28
|
+
import { verifyBenchmarkWorkspace } from './benchmark/verify.js';
|
|
29
|
+
import { ensureBenchmarkWorkspace } from './benchmark/storage.js';
|
|
30
|
+
import {
|
|
31
|
+
BENCHMARK_DIR,
|
|
32
|
+
BENCHMARK_EVIDENCE_DIR,
|
|
33
|
+
BENCHMARK_INDEX_FILE,
|
|
34
|
+
BENCHMARK_INBOX_DIR,
|
|
35
|
+
BENCHMARK_SOURCES_FILE,
|
|
36
|
+
DEFAULT_ADOPTION_THRESHOLD,
|
|
37
|
+
} from './benchmark/constants.js';
|
|
813
38
|
|
|
814
|
-
|
|
39
|
+
async function benchmarkWorkspace(projectRoot, options = {}) {
|
|
815
40
|
const action = options.action ?? 'list';
|
|
816
41
|
if (action === 'add') {
|
|
817
42
|
return addBenchmarkWorkspace(projectRoot, options);
|
|
@@ -819,55 +44,29 @@ export async function benchmarkWorkspace(projectRoot, options = {}) {
|
|
|
819
44
|
if (action === 'approve') {
|
|
820
45
|
return approveBenchmarkWorkspace(projectRoot, options);
|
|
821
46
|
}
|
|
47
|
+
if (action === 'observe') {
|
|
48
|
+
return observeBenchmarkSourceWorkspace(projectRoot, options);
|
|
49
|
+
}
|
|
822
50
|
if (action === 'verify') {
|
|
823
51
|
return verifyBenchmarkWorkspace(projectRoot, options);
|
|
824
52
|
}
|
|
825
53
|
return listBenchmarkWorkspace(projectRoot, options);
|
|
826
54
|
}
|
|
827
55
|
|
|
828
|
-
export async function renderApprovedBenchmarkRegistrySection(projectRoot) {
|
|
829
|
-
await ensureBenchmarkWorkspace(projectRoot);
|
|
830
|
-
const approved = await loadApprovedSources(projectRoot);
|
|
831
|
-
if (approved.length === 0) {
|
|
832
|
-
return [
|
|
833
|
-
'## Project Benchmark Registry',
|
|
834
|
-
'',
|
|
835
|
-
'- 当前项目还没有 approved benchmark source。',
|
|
836
|
-
'- 如需补充,用 `openprd benchmark add <url|repo|file>` 添加 candidate,再用 `openprd benchmark approve <id>` 纳入项目级 registry。',
|
|
837
|
-
'- Agent 仍应先读取 `.openprd/benchmarks/index.md` 和 `.openprd/benchmarks/sources.yaml`,但 candidate inbox 不能当成长期事实来源。',
|
|
838
|
-
'',
|
|
839
|
-
].join('\n');
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
const lines = [
|
|
843
|
-
'## Project Benchmark Registry',
|
|
844
|
-
'',
|
|
845
|
-
'- 先读取 `.openprd/benchmarks/index.md` 和 `.openprd/benchmarks/sources.yaml`。',
|
|
846
|
-
'- 项目级 approved benchmark 优先于 OpenPrd 内置 Source Map;`inbox/` candidate 只能作为待确认线索。',
|
|
847
|
-
'- 每次最多优先挑 1-3 个与当前任务最相关的 approved source。',
|
|
848
|
-
'',
|
|
849
|
-
'### Approved Sources',
|
|
850
|
-
'',
|
|
851
|
-
];
|
|
852
|
-
|
|
853
|
-
for (const source of approved.slice(0, 20)) {
|
|
854
|
-
const location = source.repo ? `${source.repo} (${source.url})` : (source.url ?? source.path ?? 'unknown');
|
|
855
|
-
lines.push(`- \`${source.id}\` ${source.title}`);
|
|
856
|
-
lines.push(` - 场景: ${source.scenarios.join(', ') || '未分类'}`);
|
|
857
|
-
lines.push(` - 触发: ${source.triggerWhen.join(';') || '待补充'}`);
|
|
858
|
-
lines.push(` - 不适用: ${source.notFor.join(';') || '待补充'}`);
|
|
859
|
-
lines.push(` - 研究方式: ${source.researchMethod}`);
|
|
860
|
-
lines.push(` - 来源: ${location}`);
|
|
861
|
-
}
|
|
862
|
-
lines.push('');
|
|
863
|
-
return lines.join('\n');
|
|
864
|
-
}
|
|
865
|
-
|
|
866
56
|
export {
|
|
57
|
+
addBenchmarkWorkspace,
|
|
58
|
+
approveBenchmarkWorkspace,
|
|
867
59
|
BENCHMARK_DIR,
|
|
868
60
|
BENCHMARK_EVIDENCE_DIR,
|
|
869
61
|
BENCHMARK_INDEX_FILE,
|
|
870
62
|
BENCHMARK_INBOX_DIR,
|
|
871
63
|
BENCHMARK_SOURCES_FILE,
|
|
64
|
+
benchmarkWorkspace,
|
|
65
|
+
DEFAULT_ADOPTION_THRESHOLD,
|
|
872
66
|
ensureBenchmarkWorkspace,
|
|
67
|
+
listBenchmarkRecommendationsWorkspace,
|
|
68
|
+
listBenchmarkWorkspace,
|
|
69
|
+
observeBenchmarkSourceWorkspace,
|
|
70
|
+
renderApprovedBenchmarkRegistrySection,
|
|
71
|
+
verifyBenchmarkWorkspace,
|
|
873
72
|
};
|