@openprd/cli 0.1.1 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +344 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +399 -438
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +850 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +46 -24
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +10 -4
  72. package/skills/openprd-requirement-intake/SKILL.md +31 -20
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +265 -65
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +639 -117
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1176 -75
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/session-binding.js +40 -3
  131. package/src/session-registry.js +159 -0
  132. package/src/standards.js +5 -3
  133. package/src/test-strategy.js +386 -0
  134. package/src/visual-compare.js +915 -34
  135. package/src/work-unit-migration.js +5 -1
  136. package/src/workspace-core.js +343 -19
  137. package/src/workspace-workflow.js +538 -134
@@ -0,0 +1,121 @@
1
+ /*
2
+ * 核心功能
3
+ * 管理 benchmark 工作区目录、approved/candidate 文件读取和索引落盘。
4
+ *
5
+ * 输入
6
+ * 接收项目根目录以及来源列表,用于创建、读取和刷新 `.openprd/benchmarks`。
7
+ *
8
+ * 输出
9
+ * 导出 benchmark workspace 初始化、source 文件路径和持久化读写能力。
10
+ *
11
+ * 定位
12
+ * 位于 benchmark IO 边界,负责文件系统交互,不承载信源推断或 verify 规则。
13
+ *
14
+ * 依赖
15
+ * 依赖 node:fs/promises、fs-utils、time,以及 render/source 提供的纯逻辑能力。
16
+ *
17
+ * 维护规则
18
+ * 变更文件布局或落盘格式时必须兼容现有 `.openprd/benchmarks` 目录和已生成 yaml/md 文件。
19
+ */
20
+ import fs from 'node:fs/promises';
21
+ import { cjoin, exists, readYaml, writeText, writeYaml } from '../fs-utils.js';
22
+ import { timestamp } from '../time.js';
23
+ import {
24
+ BENCHMARK_EVIDENCE_DIR,
25
+ BENCHMARK_INDEX_FILE,
26
+ BENCHMARK_INBOX_DIR,
27
+ BENCHMARK_SOURCES_FILE,
28
+ benchmarkPath,
29
+ defaultIndex,
30
+ defaultSourcesFile,
31
+ } from './constants.js';
32
+ import { renderBenchmarkIndex } from './render.js';
33
+ import { normalizeSourceRecord } from './source.js';
34
+
35
+ async function ensureOpenPrdWorkspace(projectRoot) {
36
+ const workspaceRoot = cjoin(projectRoot, '.openprd');
37
+ if (!(await exists(workspaceRoot))) {
38
+ throw new Error('Project is not initialized with OpenPrd. Run `openprd init .` first.');
39
+ }
40
+ }
41
+
42
+ async function ensureBenchmarkWorkspace(projectRoot) {
43
+ await ensureOpenPrdWorkspace(projectRoot);
44
+ await fs.mkdir(benchmarkPath(projectRoot, BENCHMARK_INBOX_DIR), { recursive: true });
45
+ await fs.mkdir(benchmarkPath(projectRoot, BENCHMARK_EVIDENCE_DIR), { recursive: true });
46
+ if (!(await exists(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE)))) {
47
+ await writeYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE), defaultSourcesFile());
48
+ }
49
+ if (!(await exists(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE)))) {
50
+ await writeText(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE), `${defaultIndex()}\n`);
51
+ }
52
+ }
53
+
54
+ async function loadApprovedSources(projectRoot) {
55
+ const payload = await readYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE)).catch(() => defaultSourcesFile());
56
+ return Array.isArray(payload.sources) ? payload.sources.map(normalizeSourceRecord) : [];
57
+ }
58
+
59
+ async function loadCandidateSources(projectRoot) {
60
+ const inboxDir = benchmarkPath(projectRoot, BENCHMARK_INBOX_DIR);
61
+ const entries = await fs.readdir(inboxDir, { withFileTypes: true }).catch(() => []);
62
+ const candidates = [];
63
+ for (const entry of entries) {
64
+ if (!entry.isFile() || !/\.(yaml|yml)$/i.test(entry.name)) {
65
+ continue;
66
+ }
67
+ const filePath = cjoin(inboxDir, entry.name);
68
+ const payload = await readYaml(filePath).catch(() => null);
69
+ if (payload && typeof payload === 'object') {
70
+ candidates.push(normalizeSourceRecord(payload));
71
+ }
72
+ }
73
+ return candidates.sort((left, right) => left.id.localeCompare(right.id));
74
+ }
75
+
76
+ function sourceFilePath(projectRoot, id) {
77
+ return benchmarkPath(projectRoot, cjoin(BENCHMARK_INBOX_DIR, `${id}.yaml`));
78
+ }
79
+
80
+ function evidenceFilePath(projectRoot, id) {
81
+ return benchmarkPath(projectRoot, cjoin(BENCHMARK_EVIDENCE_DIR, `${id}.md`));
82
+ }
83
+
84
+ async function writeApprovedSources(projectRoot, sources) {
85
+ const next = {
86
+ ...defaultSourcesFile(),
87
+ updatedAt: timestamp(),
88
+ sources: sources
89
+ .map(normalizeSourceRecord)
90
+ .sort((left, right) => left.id.localeCompare(right.id)),
91
+ };
92
+ await writeYaml(benchmarkPath(projectRoot, BENCHMARK_SOURCES_FILE), next);
93
+ }
94
+
95
+ async function refreshBenchmarkIndex(projectRoot) {
96
+ const approved = await loadApprovedSources(projectRoot);
97
+ const candidates = await loadCandidateSources(projectRoot);
98
+ await writeText(benchmarkPath(projectRoot, BENCHMARK_INDEX_FILE), renderBenchmarkIndex(approved, candidates));
99
+ return { approved, candidates };
100
+ }
101
+
102
+ async function readCandidateById(projectRoot, id) {
103
+ const filePath = sourceFilePath(projectRoot, id);
104
+ if (!(await exists(filePath))) {
105
+ return null;
106
+ }
107
+ const payload = await readYaml(filePath);
108
+ return normalizeSourceRecord(payload);
109
+ }
110
+
111
+ export {
112
+ ensureBenchmarkWorkspace,
113
+ ensureOpenPrdWorkspace,
114
+ evidenceFilePath,
115
+ loadApprovedSources,
116
+ loadCandidateSources,
117
+ readCandidateById,
118
+ refreshBenchmarkIndex,
119
+ sourceFilePath,
120
+ writeApprovedSources,
121
+ };
@@ -0,0 +1,235 @@
1
+ /*
2
+ * 核心功能
3
+ * 执行 benchmark verify,包括远程探活、重复检测和 promotion 控制校验。
4
+ *
5
+ * 输入
6
+ * 接收项目根目录和 benchmark workspace 内已存在的 approved/candidate source 数据。
7
+ *
8
+ * 输出
9
+ * 导出 benchmark verify 结果,并在校验通过时回写 lastVerified 等字段。
10
+ *
11
+ * 定位
12
+ * 位于 benchmark 质量门禁层,负责验证与探测,不直接承载 add/observe/approve 入口。
13
+ *
14
+ * 依赖
15
+ * 依赖 node:child_process、fs-utils、time,以及 source/storage 提供的领域逻辑与持久化能力。
16
+ *
17
+ * 维护规则
18
+ * 新增 verify 规则时必须区分 error 与 warning,避免把 advisory 检查升级成破坏性门禁。
19
+ */
20
+ import path from 'node:path';
21
+ import { spawn } from 'node:child_process';
22
+ import { exists, writeYaml } from '../fs-utils.js';
23
+ import { timestamp } from '../time.js';
24
+ import { benchmarkRecommendations, hasOverbroadTrigger, normalizeCheckedSource, sourceIdentity, validatePromotionControl } from './source.js';
25
+ import {
26
+ ensureBenchmarkWorkspace,
27
+ loadApprovedSources,
28
+ loadCandidateSources,
29
+ refreshBenchmarkIndex,
30
+ sourceFilePath,
31
+ writeApprovedSources,
32
+ } from './storage.js';
33
+
34
+ async function fetchWithTimeout(urlString, method, timeoutMs = 4000) {
35
+ const controller = new AbortController();
36
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
37
+ try {
38
+ return await fetch(urlString, {
39
+ method,
40
+ redirect: 'follow',
41
+ signal: controller.signal,
42
+ });
43
+ } finally {
44
+ clearTimeout(timeout);
45
+ }
46
+ }
47
+
48
+ function isReachableProbeStatus(status) {
49
+ return (status >= 200 && status < 400) || status === 401 || status === 403 || status === 405;
50
+ }
51
+
52
+ async function probeRemoteSourceWithCurl(urlString, timeoutMs = 6000) {
53
+ return await new Promise((resolve) => {
54
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
55
+ const child = spawn(
56
+ 'curl',
57
+ [
58
+ '-L',
59
+ '-o',
60
+ '/dev/null',
61
+ '-s',
62
+ '-w',
63
+ '%{http_code}',
64
+ '--max-time',
65
+ String(timeoutSeconds),
66
+ urlString,
67
+ ],
68
+ {
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ },
71
+ );
72
+
73
+ let stdout = '';
74
+ let stderr = '';
75
+ let settled = false;
76
+ const settle = (value) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ resolve(value);
80
+ };
81
+
82
+ const timeout = setTimeout(() => {
83
+ child.kill('SIGKILL');
84
+ settle({ ok: false, reason: 'curl probe timeout' });
85
+ }, timeoutMs + 250);
86
+
87
+ child.stdout.on('data', (chunk) => {
88
+ stdout += String(chunk);
89
+ });
90
+ child.stderr.on('data', (chunk) => {
91
+ stderr += String(chunk);
92
+ });
93
+ child.on('error', (error) => {
94
+ clearTimeout(timeout);
95
+ settle({ ok: false, reason: error instanceof Error ? error.message : String(error) });
96
+ });
97
+ child.on('close', (code) => {
98
+ clearTimeout(timeout);
99
+ const status = Number.parseInt(stdout.trim(), 10);
100
+ if (Number.isInteger(status) && isReachableProbeStatus(status)) {
101
+ settle({ ok: true, status, via: 'curl' });
102
+ return;
103
+ }
104
+ if (Number.isInteger(status) && status > 0) {
105
+ settle({ ok: false, status, reason: `HTTP ${status}` });
106
+ return;
107
+ }
108
+ const detail = stderr.trim() || `curl exit ${code ?? 'unknown'}`;
109
+ settle({ ok: false, reason: detail });
110
+ });
111
+ });
112
+ }
113
+
114
+ async function probeRemoteSource(urlString) {
115
+ try {
116
+ const headResponse = await fetchWithTimeout(urlString, 'HEAD');
117
+ if (isReachableProbeStatus(headResponse.status)) {
118
+ return { ok: true, status: headResponse.status };
119
+ }
120
+ return { ok: false, status: headResponse.status, reason: `HTTP ${headResponse.status}` };
121
+ } catch {
122
+ try {
123
+ const getResponse = await fetchWithTimeout(urlString, 'GET', 5000);
124
+ if (isReachableProbeStatus(getResponse.status)) {
125
+ return { ok: true, status: getResponse.status };
126
+ }
127
+ return { ok: false, status: getResponse.status, reason: `HTTP ${getResponse.status}` };
128
+ } catch (error) {
129
+ const fallback = await probeRemoteSourceWithCurl(urlString);
130
+ if (fallback.ok) {
131
+ return fallback;
132
+ }
133
+ return { ok: false, reason: fallback.reason ?? (error instanceof Error ? error.message : String(error)) };
134
+ }
135
+ }
136
+ }
137
+
138
+ async function verifyBenchmarkWorkspace(projectRoot) {
139
+ await ensureBenchmarkWorkspace(projectRoot);
140
+ const approved = await loadApprovedSources(projectRoot);
141
+ const candidates = await loadCandidateSources(projectRoot);
142
+ const allSources = [...approved, ...candidates];
143
+ const checks = [];
144
+ const seenIds = new Map();
145
+ const seenLocations = new Map();
146
+ const approvedUpdates = new Map();
147
+ const candidateUpdates = new Map();
148
+
149
+ for (const source of allSources) {
150
+ const issues = [];
151
+ if (seenIds.has(source.id)) {
152
+ issues.push({ level: 'error', code: 'duplicate-id', message: `Duplicate benchmark id with ${seenIds.get(source.id)}` });
153
+ } else {
154
+ seenIds.set(source.id, source.id);
155
+ }
156
+
157
+ const identity = sourceIdentity(source);
158
+ if (seenLocations.has(identity)) {
159
+ issues.push({ level: 'error', code: 'duplicate-source', message: `Duplicate benchmark source with ${seenLocations.get(identity)}` });
160
+ } else {
161
+ seenLocations.set(identity, source.id);
162
+ }
163
+
164
+ if (source.url) {
165
+ try {
166
+ new URL(source.url);
167
+ } catch {
168
+ issues.push({ level: 'error', code: 'invalid-url', message: `Invalid URL: ${source.url}` });
169
+ }
170
+ if (!issues.some((issue) => issue.code === 'invalid-url')) {
171
+ const probe = await probeRemoteSource(source.url);
172
+ if (!probe.ok) {
173
+ issues.push({ level: 'error', code: 'unreachable-source', message: `Unreachable source: ${source.url} (${probe.reason ?? 'unknown'})` });
174
+ }
175
+ }
176
+ }
177
+
178
+ if (source.path) {
179
+ const absolutePath = path.resolve(projectRoot, source.path);
180
+ if (!(await exists(absolutePath))) {
181
+ issues.push({ level: 'error', code: 'missing-local-source', message: `Missing local source: ${source.path}` });
182
+ }
183
+ }
184
+
185
+ if (!Array.isArray(source.scenarios) || source.scenarios.length === 0) {
186
+ issues.push({ level: 'warning', code: 'missing-scenarios', message: 'Missing benchmark scenarios.' });
187
+ }
188
+ if (hasOverbroadTrigger(source)) {
189
+ issues.push({ level: 'warning', code: 'overbroad-trigger', message: 'Trigger rules are too broad or missing.' });
190
+ }
191
+
192
+ issues.push(...validatePromotionControl(source));
193
+
194
+ const ok = !issues.some((issue) => issue.level === 'error');
195
+ const nextSource = ok ? normalizeCheckedSource(source) : source;
196
+ if (source.status === 'approved') {
197
+ approvedUpdates.set(source.id, nextSource);
198
+ } else {
199
+ candidateUpdates.set(source.id, nextSource);
200
+ }
201
+ checks.push({
202
+ id: source.id,
203
+ title: source.title,
204
+ status: source.status,
205
+ ok,
206
+ issues,
207
+ });
208
+ }
209
+
210
+ const approvedNext = approved.map((source) => approvedUpdates.get(source.id) ?? source);
211
+ const candidateNext = candidates.map((source) => candidateUpdates.get(source.id) ?? source);
212
+ await writeApprovedSources(projectRoot, approvedNext);
213
+ for (const source of candidateNext) {
214
+ await writeYaml(sourceFilePath(projectRoot, source.id), source);
215
+ }
216
+ await refreshBenchmarkIndex(projectRoot);
217
+
218
+ const errors = checks.flatMap((check) => check.issues.filter((issue) => issue.level === 'error').map((issue) => `${check.id}: ${issue.message}`));
219
+ const warnings = checks.flatMap((check) => check.issues.filter((issue) => issue.level !== 'error').map((issue) => `${check.id}: ${issue.message}`));
220
+
221
+ return {
222
+ ok: errors.length === 0,
223
+ action: 'benchmark-verify',
224
+ projectRoot,
225
+ checkedAt: timestamp(),
226
+ checks,
227
+ errors,
228
+ warnings,
229
+ recommendations: benchmarkRecommendations(candidateNext),
230
+ };
231
+ }
232
+
233
+ export {
234
+ verifyBenchmarkWorkspace,
235
+ };