@openprd/cli 0.1.8 → 0.1.10

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 (134) hide show
  1. package/.openprd/benchmarks/index.md +15 -1
  2. package/.openprd/config.yaml +10 -0
  3. package/.openprd/design/README.md +47 -0
  4. package/.openprd/design/README_EN.md +25 -0
  5. package/.openprd/design/active/asset-spec.md +15 -0
  6. package/.openprd/design/active/direction-plan.md +7 -0
  7. package/.openprd/design/active/facts-sheet.md +13 -0
  8. package/.openprd/design/active/image-preflight.md +9 -0
  9. package/.openprd/design/active/selected-direction.md +11 -0
  10. package/.openprd/design/anti-slop.md +16 -0
  11. package/.openprd/design/assets/dark-mesh.svg +14 -0
  12. package/.openprd/design/assets/paper-grid.svg +14 -0
  13. package/.openprd/design/assets/surface-presets.md +21 -0
  14. package/.openprd/design/assets/workbench-grid.svg +16 -0
  15. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  16. package/.openprd/design/components/component-catalog.md +42 -0
  17. package/.openprd/design/layouts/layout-catalog.json +45 -0
  18. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  19. package/.openprd/design/recipes/content-experience.md +26 -0
  20. package/.openprd/design/recipes/operational-tool.md +25 -0
  21. package/.openprd/design/recipes/product-launch.md +26 -0
  22. package/.openprd/design/templates/README.md +36 -0
  23. package/.openprd/design/templates/README_EN.md +31 -0
  24. package/.openprd/design/templates/content-home.html +415 -0
  25. package/.openprd/design/templates/ops-dashboard.html +340 -0
  26. package/.openprd/design/templates/product-launch.html +360 -0
  27. package/.openprd/design/themes/theme-catalog.json +77 -0
  28. package/.openprd/engagements/active/prd.md +111 -100
  29. package/.openprd/schema/prd.schema.yaml +10 -0
  30. package/.openprd/templates/base/intake.md +4 -0
  31. package/.openprd/templates/base/prd.md +16 -6
  32. package/AGENTS.md +10 -7
  33. package/README.md +28 -10
  34. package/README_EN.md +23 -3
  35. package/package.json +41 -2
  36. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  37. package/skills/openprd-benchmark-router/SKILL.md +4 -0
  38. package/skills/openprd-frontend-design/SKILL.md +161 -0
  39. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  40. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  41. package/skills/openprd-harness/SKILL.md +38 -21
  42. package/skills/openprd-learning-review/SKILL.md +2 -1
  43. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  44. package/skills/openprd-quality/SKILL.md +6 -4
  45. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  46. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  47. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  48. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
  49. package/skills/openprd-router/SKILL.md +13 -5
  50. package/skills/openprd-shared/SKILL.md +38 -25
  51. package/src/agent-integration.js +126 -75
  52. package/src/brainstorm-artifacts.js +1657 -0
  53. package/src/brainstorm-presentation.js +223 -0
  54. package/src/brainstorm.js +603 -0
  55. package/src/cli/args.js +51 -3
  56. package/src/cli/doctor-print.js +34 -8
  57. package/src/cli/print.js +6 -0
  58. package/src/cli/quality-print.js +70 -0
  59. package/src/cli/run-print.js +43 -1
  60. package/src/cli/shared-print.js +33 -2
  61. package/src/cli/workflow-print.js +21 -0
  62. package/src/codex-hook-runner-template.mjs +858 -65
  63. package/src/design-starter-support.js +462 -0
  64. package/src/design-starter.js +1207 -0
  65. package/src/knowledge.js +377 -21
  66. package/src/learning-review.js +3 -3
  67. package/src/loop.js +839 -101
  68. package/src/openprd.js +86 -1
  69. package/src/openspec/change-validate.js +1 -0
  70. package/src/prd-core.js +18 -0
  71. package/src/quality-learning.js +50 -6
  72. package/src/quality.js +2 -2
  73. package/src/run-harness.js +191 -29
  74. package/src/self-update.js +301 -4
  75. package/src/standards.js +46 -5
  76. package/src/visual-prepare.js +940 -0
  77. package/src/workspace-core.js +137 -0
  78. package/src/workspace-workflow.js +190 -7
  79. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  80. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  81. package/.openprd/discovery/config.json +0 -35
  82. package/.openprd/engagements/active/flows.md +0 -35
  83. package/.openprd/engagements/active/handoff.md +0 -16
  84. package/.openprd/engagements/active/review.html +0 -61
  85. package/.openprd/engagements/active/roles.md +0 -22
  86. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  87. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  88. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  89. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  90. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  91. package/.openprd/exports/.gitkeep +0 -0
  92. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  93. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  94. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  95. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  96. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  97. package/.openprd/knowledge/index.json +0 -47
  98. package/.openprd/reviews/v0001.html +0 -1322
  99. package/.openprd/reviews/v0002.html +0 -1150
  100. package/.openprd/reviews/v0003.html +0 -1150
  101. package/.openprd/reviews/v0004.html +0 -1150
  102. package/.openprd/reviews/v0005.html +0 -1150
  103. package/.openprd/sessions/.gitkeep +0 -0
  104. package/.openprd/state/changes.json +0 -27
  105. package/.openprd/state/current.json +0 -505
  106. package/.openprd/state/release-ledger.json +0 -344
  107. package/.openprd/state/version-index.json +0 -67
  108. package/.openprd/state/versions/.gitkeep +0 -0
  109. package/.openprd/state/versions/v0001.json +0 -121
  110. package/.openprd/state/versions/v0001.md +0 -161
  111. package/.openprd/state/versions/v0002.json +0 -264
  112. package/.openprd/state/versions/v0002.md +0 -183
  113. package/.openprd/state/versions/v0003.json +0 -269
  114. package/.openprd/state/versions/v0003.md +0 -188
  115. package/.openprd/state/versions/v0004.json +0 -274
  116. package/.openprd/state/versions/v0004.md +0 -193
  117. package/.openprd/state/versions/v0005.json +0 -299
  118. package/.openprd/state/versions/v0005.md +0 -189
  119. package/docs/assets/openprd-capability-overview-en.png +0 -0
  120. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  121. package/docs/assets/openprd-learning-html.png +0 -0
  122. package/docs/assets/openprd-quality-html.png +0 -0
  123. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  124. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  125. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  126. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  128. package/docs/assets/openprd-review-html.png +0 -0
  129. package/docs/assets/openprd-scenario-overview.png +0 -0
  130. package/docs/assets/openprd-scenario-overview.svg +0 -114
  131. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  132. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  133. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  134. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -0,0 +1,603 @@
1
+ /*
2
+ * 核心功能
3
+ * 生成脑暴模式的 workspace 数据、资源池摘要和稳定 artifact。
4
+ *
5
+ * 输入
6
+ * 接收项目根目录、可选 topic 和打开页面参数,并读取 benchmark、knowledge 与当前工作区状态。
7
+ *
8
+ * 输出
9
+ * 写入 brainstorm.html、brainstorm.json、data.md、capture-patch.json,并返回结构化结果。
10
+ *
11
+ * 定位
12
+ * 位于脑暴模式应用层,负责把 OpenPrd 现有资源组织成可评审的脑暴工作台。
13
+ *
14
+ * 依赖
15
+ * 依赖 benchmark、knowledge、workspace-core 和 brainstorm-artifacts;由 openprd CLI 作为命令入口调用。
16
+ *
17
+ * 维护规则
18
+ * 修改脑暴状态结构或 artifact 路径时,必须同步维护 CLI、presentation 工具、测试和 docs/basic。
19
+ */
20
+ import crypto from 'node:crypto';
21
+ import fs from 'node:fs/promises';
22
+ import path from 'node:path';
23
+ import { listBenchmarkWorkspace } from './benchmark.js';
24
+ import { renderBrainstormArtifact, renderBrainstormMarkdown, renderBrainstormPatch } from './brainstorm-artifacts.js';
25
+ import { openArtifactInBrowser, artifactBundlePaths, writeHtmlArtifact } from './html-artifacts.js';
26
+ import { exists, readJson, writeJson, writeText } from './fs-utils.js';
27
+ import { timestamp } from './time.js';
28
+ import { appendProgress, appendWorkflowEvent, buildWorkflowTaskGraph, loadWorkspace, persistWorkspaceCurrentState } from './workspace-core.js';
29
+
30
+ const WORKSPACE_SCAN_IGNORE = new Set(['.git', '.openprd', 'node_modules', '.DS_Store']);
31
+
32
+ function normalizedText(value) {
33
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
34
+ }
35
+
36
+ function listOfStrings(value) {
37
+ if (!Array.isArray(value)) return [];
38
+ return value
39
+ .map((item) => normalizedText(item))
40
+ .filter(Boolean);
41
+ }
42
+
43
+ function firstText(value, fallback = '') {
44
+ return listOfStrings(value)[0] || fallback;
45
+ }
46
+
47
+ function hashStableJson(value) {
48
+ return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
49
+ }
50
+
51
+ function stableStringify(value) {
52
+ if (Array.isArray(value)) {
53
+ return `[${value.map(stableStringify).join(',')}]`;
54
+ }
55
+ if (value && typeof value === 'object') {
56
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
57
+ }
58
+ return JSON.stringify(value);
59
+ }
60
+
61
+ function slugify(value, fallback = 'brainstorm') {
62
+ const slug = String(value ?? '')
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
65
+ .replace(/^-+|-+$/g, '')
66
+ .slice(0, 48);
67
+ return slug || fallback;
68
+ }
69
+
70
+ function toRelativeProjectPath(projectRoot, filePath) {
71
+ const relative = path.relative(projectRoot, filePath).split(path.sep).join('/');
72
+ return relative && !relative.startsWith('..') ? relative : filePath;
73
+ }
74
+
75
+ async function listDocsBasic(projectRoot) {
76
+ const docsRoot = path.join(projectRoot, 'docs', 'basic');
77
+ if (!(await exists(docsRoot))) return [];
78
+ const files = await fs.readdir(docsRoot, { withFileTypes: true });
79
+ return files
80
+ .filter((entry) => entry.isFile())
81
+ .map((entry) => path.join('docs', 'basic', entry.name).split(path.sep).join('/'))
82
+ .sort();
83
+ }
84
+
85
+ async function listTopLevelReusableAreas(projectRoot) {
86
+ const entries = await fs.readdir(projectRoot, { withFileTypes: true }).catch(() => []);
87
+ return entries
88
+ .filter((entry) => !WORKSPACE_SCAN_IGNORE.has(entry.name))
89
+ .filter((entry) => entry.isDirectory() || entry.isFile())
90
+ .slice(0, 24)
91
+ .map((entry) => {
92
+ if (entry.isDirectory()) {
93
+ return `目录 ${entry.name} 可能包含可复用能力`;
94
+ }
95
+ return `文件 ${entry.name} 可能包含可复用配置`;
96
+ });
97
+ }
98
+
99
+ async function listActiveArtifacts(ws) {
100
+ const targets = [
101
+ ws.paths.activePrd,
102
+ ws.paths.activeReviewHtml,
103
+ ws.paths.activeBrainstormHtml,
104
+ ws.paths.activeArchitectureDiagramHtml,
105
+ ws.paths.activeProductFlowDiagramHtml,
106
+ ];
107
+ const items = [];
108
+ for (const target of targets) {
109
+ if (!(await exists(target))) continue;
110
+ items.push(toRelativeProjectPath(ws.projectRoot, target));
111
+ }
112
+ return items;
113
+ }
114
+
115
+ function summarizeBenchmarks(result) {
116
+ const approved = Array.isArray(result?.approved) ? result.approved : [];
117
+ const candidates = Array.isArray(result?.candidates) ? result.candidates : [];
118
+ const recommendations = Array.isArray(result?.recommendations) ? result.recommendations : [];
119
+ return {
120
+ approved: approved.map((item) => ({
121
+ id: item.id,
122
+ title: item.title,
123
+ repo: item.repo ?? null,
124
+ url: item.url ?? null,
125
+ scenarios: listOfStrings(item.scenarios),
126
+ triggerWhen: listOfStrings(item.triggerWhen),
127
+ note: normalizedText(item.note),
128
+ researchMethod: item.researchMethod ?? null,
129
+ })),
130
+ recommendations: recommendations.map((item) => ({
131
+ id: item.id,
132
+ title: item.title,
133
+ adoptedCount: Number(item.adoptedCount ?? item.recentAdoptedCount ?? 0),
134
+ threshold: Number(item.threshold ?? item.promotion?.threshold ?? 0),
135
+ approveCommand: item.approveCommand ?? item.promotion?.approveCommand ?? null,
136
+ })),
137
+ counts: {
138
+ approved: approved.length,
139
+ candidates: candidates.length,
140
+ },
141
+ };
142
+ }
143
+
144
+ function summarizeKnowledge(index) {
145
+ const skills = Array.isArray(index?.skills) ? index.skills : [];
146
+ const candidates = Array.isArray(index?.candidates) ? index.candidates : [];
147
+ return {
148
+ skills: skills.slice(0, 8).map((item) => ({
149
+ skillName: item.skillName,
150
+ description: normalizedText(item.description ?? item.summary ?? ''),
151
+ categories: listOfStrings(item.categories),
152
+ adoption: item.adoption ?? {},
153
+ touchedFiles: listOfStrings(item.touchedFiles).slice(0, 6),
154
+ })),
155
+ candidates: candidates.slice(0, 6).map((item) => ({
156
+ candidateId: item.candidateId,
157
+ title: item.title ?? item.candidateId,
158
+ status: item.status ?? 'pending-review',
159
+ })),
160
+ counts: {
161
+ incidents: Array.isArray(index?.incidents) ? index.incidents.length : 0,
162
+ patterns: Array.isArray(index?.patterns) ? index.patterns.length : 0,
163
+ skills: skills.length,
164
+ candidates: candidates.length,
165
+ drafts: Array.isArray(index?.drafts) ? index.drafts.length : 0,
166
+ },
167
+ };
168
+ }
169
+
170
+ function buildSourceCoverageLabel({ benchmark, knowledge, workspaceScan }) {
171
+ return [
172
+ `benchmark ${benchmark.counts.approved}/${benchmark.counts.candidates}`,
173
+ `knowledge ${knowledge.counts.skills}/${knowledge.counts.candidates}`,
174
+ `docs ${workspaceScan.docs.length}`,
175
+ ].join(' · ');
176
+ }
177
+
178
+ function buildConfidenceLabel(currentState, benchmark, knowledge) {
179
+ const signals = [
180
+ normalizedText(currentState.problemStatement),
181
+ listOfStrings(currentState.primaryUsers).length > 0 ? 'users' : null,
182
+ listOfStrings(currentState.community).length > 0 ? 'community' : null,
183
+ normalizedText(currentState.currentAlternative || currentState.asIs) ? 'current-alternative' : null,
184
+ listOfStrings(currentState.commitmentSignals).length > 0 ? 'commitment' : null,
185
+ normalizedText(currentState.firstValidationStep || currentState.nextStep) ? 'validation-step' : null,
186
+ listOfStrings(currentState.goals).length > 0 ? 'goals' : null,
187
+ listOfStrings(currentState.assumptions).length > 0 ? 'assumptions' : null,
188
+ benchmark.counts.approved > 0 ? 'benchmark' : null,
189
+ knowledge.counts.skills > 0 ? 'knowledge' : null,
190
+ ].filter(Boolean).length;
191
+ if (signals >= 6) return '已经有较完整基础,可以开始整理 PRD';
192
+ if (signals >= 4) return '方向基本有了,但还差 1 轮确认';
193
+ return '现在信息还不够,先继续把问题聊透';
194
+ }
195
+
196
+ function buildFollowUpPrompts(currentState, workspaceScan) {
197
+ const prompts = [];
198
+ if (listOfStrings(currentState.primaryUsers).length === 0) {
199
+ prompts.push('先补一轮目标用户和真实场景。');
200
+ }
201
+ if (listOfStrings(currentState.community).length === 0) {
202
+ prompts.push('先说清第一批最容易触达、最可能给真实反馈的人群或社区。');
203
+ }
204
+ if (!normalizedText(currentState.currentAlternative || currentState.asIs)) {
205
+ prompts.push('先说清楚现在是靠什么办法在解决这件事。');
206
+ }
207
+ if (listOfStrings(currentState.manualPath).length === 0) {
208
+ prompts.push('先补一条不做完整产品时也能手工交付价值的路径。');
209
+ }
210
+ if (listOfStrings(currentState.commitmentSignals).length === 0) {
211
+ prompts.push('先定义什么真实承诺能证明这不是口头兴趣。');
212
+ }
213
+ if (!normalizedText(currentState.firstValidationStep || currentState.nextStep)) {
214
+ prompts.push('先定一个最低成本验证动作,不要一上来写全套方案。');
215
+ }
216
+ if (listOfStrings(currentState.defaultAlivePlan).length === 0) {
217
+ prompts.push('先说清验证阶段怎样控制成本、时间和交付方式,确保这件事先活下来。');
218
+ }
219
+ if (listOfStrings(currentState.goals).length === 0) {
220
+ prompts.push('先把想达到的目标和判断标准说清楚。');
221
+ }
222
+ if (listOfStrings(currentState.assumptions).length === 0) {
223
+ prompts.push('把这件事要成立必须为真的前提先列出来。');
224
+ }
225
+ if (listOfStrings(currentState.openQuestions).length === 0) {
226
+ prompts.push('把最影响判断的几个问题先列出来。');
227
+ }
228
+ if (workspaceScan.docs.length === 0) {
229
+ prompts.push('如果有现成方案或旧文档,可以一起作为参考。');
230
+ }
231
+ prompts.push('如果以前做过类似产品,优先看看有没有现成做法能直接借。');
232
+ return prompts;
233
+ }
234
+
235
+ function humanizeBenchmarkHint(item) {
236
+ const note = normalizedText(item.note);
237
+ if (note) return note;
238
+ const scenarios = listOfStrings(item.scenarios);
239
+ if (scenarios.length > 0) {
240
+ return `外部类似案例更常出现在 ${scenarios.slice(0, 2).join('、')} 这些场景。`;
241
+ }
242
+ const triggerWhen = listOfStrings(item.triggerWhen);
243
+ if (triggerWhen.length > 0) {
244
+ return `这类参考通常适合用在 ${triggerWhen.slice(0, 2).join('、')} 这些情况。`;
245
+ }
246
+ return '外部类似案例普遍强调先缩小第一版范围,再进入开发。';
247
+ }
248
+
249
+ function buildBrainstormReport({ currentState, benchmark, knowledge, workspaceScan, topic }) {
250
+ const primaryUsers = listOfStrings(currentState.primaryUsers);
251
+ const stakeholders = listOfStrings(currentState.stakeholders);
252
+ const goals = listOfStrings(currentState.goals);
253
+ const successMetrics = listOfStrings(currentState.successMetrics);
254
+ const inScope = listOfStrings(currentState.inScope);
255
+ const outOfScope = listOfStrings(currentState.outOfScope);
256
+ const primaryFlows = listOfStrings(currentState.primaryFlows);
257
+ const openQuestions = listOfStrings(currentState.openQuestions);
258
+ const assumptions = listOfStrings(currentState.assumptions);
259
+ const stopLossActions = listOfStrings(currentState.stopLossActions);
260
+ const community = listOfStrings(currentState.community);
261
+ const seedUsers = listOfStrings(currentState.seedUsers);
262
+ const currentAlternative = normalizedText(currentState.currentAlternative || currentState.asIs);
263
+ const manualPath = listOfStrings(currentState.manualPath);
264
+ const commitmentSignals = listOfStrings(currentState.commitmentSignals);
265
+ const firstValidationStep = normalizedText(currentState.firstValidationStep || currentState.nextStep);
266
+ const defaultAlivePlan = listOfStrings(currentState.defaultAlivePlan);
267
+ const toBe = normalizedText(currentState.toBe);
268
+ const whyNow = normalizedText(currentState.whyNow);
269
+ const nextStep = normalizedText(currentState.nextStep);
270
+ const benchmarkSignals = benchmark.approved.slice(0, 4).map((item) => humanizeBenchmarkHint(item));
271
+ const knowledgeSignals = knowledge.skills.slice(0, 4).map((item) => {
272
+ const hint = normalizedText(item.description);
273
+ return hint
274
+ ? `当前项目里已经有一些可借鉴的经验:${hint}`
275
+ : '当前项目里已经有类似经验,优先确认能不能直接借用。';
276
+ });
277
+ const currentSituation = [
278
+ ...(primaryUsers.length > 0
279
+ ? [`这次先重点看 ${primaryUsers.slice(0, 2).join('、')} 的真实问题,不要一开始同时服务太多角色。`]
280
+ : ['当前还没锁定最核心的人群,建议先补最近一次真实场景。']),
281
+ ...(primaryFlows.length > 0
282
+ ? [`最近最值得先看的场景是:${primaryFlows[0]}。`]
283
+ : ['当前主流程还比较空,建议先补“最近一次真实发生的案例”。']),
284
+ ...(community.length > 0
285
+ ? [`第一批最容易先验证的人群或社区是:${community.slice(0, 2).join('、')}。`]
286
+ : ['当前还没锁定第一批最容易触达的人群或社区,建议先补最现实的反馈入口。']),
287
+ ...(currentAlternative
288
+ ? [`现在主要还是靠“${currentAlternative}”这类办法在撑着,先判断值不值得替换。`]
289
+ : ['当前还没明确现在是怎么解决的,建议先补现有替代方案。']),
290
+ ...(whyNow
291
+ ? [`现在提这件事,主要因为:${whyNow}`]
292
+ : ['为什么现在要做还不够具体,建议先说清触发这件事的变化。']),
293
+ ];
294
+ const directionOptions = [
295
+ `推荐方向:${goalsDirection(currentState, topic)}。`,
296
+ ...(toBe
297
+ ? [`更理想的结果是先做到:${toBe}。`]
298
+ : goals.length > 0
299
+ ? [`第一版先围绕“${goals[0]}”收范围,不急着把所有问题一次做完。`]
300
+ : [`第一版先把“${topic}”收敛成可落地的小切口。`]),
301
+ ...(manualPath.length > 0
302
+ ? [`不等完整产品也可以先这样交付价值:${manualPath.slice(0, 2).join('、')}。`]
303
+ : ['如果还没准备好完整产品,建议先补一条手工服务或半自动交付路径。']),
304
+ ...(inScope.length > 0
305
+ ? [`这轮先聚焦:${inScope.slice(0, 2).join('、')}。`]
306
+ : ['范围边界还不够清楚,建议先补“这轮先做什么、先不做什么”。']),
307
+ ...(benchmarkSignals.length > 0
308
+ ? [`外部类似做法大多也是先缩小第一版,再继续扩展。`]
309
+ : []),
310
+ ...(workspaceScan.reuseOpportunities.length > 0
311
+ ? ['备选思路:先盘点现有基础,能直接借的就不从零再做一遍。']
312
+ : []),
313
+ ];
314
+ const validationPlan = [
315
+ ...(commitmentSignals.length > 0
316
+ ? commitmentSignals.slice(0, 2).map((item) => `先看真实承诺:${item}`)
317
+ : ['建议先定义什么真实承诺能证明这不是口头兴趣。']),
318
+ ...(assumptions.length > 0
319
+ ? assumptions.slice(0, 2).map((item) => `关键前提:${item}`)
320
+ : ['当前还没把“这件事要成立,哪些前提必须为真”说清楚。']),
321
+ ...(successMetrics.length > 0
322
+ ? successMetrics.slice(0, 2).map((item) => `验证通过可以先看:${item}`)
323
+ : ['建议先补 1 到 2 条可验证标准,方便判断第一版值不值得继续。']),
324
+ ...(firstValidationStep
325
+ ? [`低成本先这样验证:${firstValidationStep}`]
326
+ : openQuestions.length > 0
327
+ ? [`低成本先验证最不确定的问题:${openQuestions[0]}`]
328
+ : ['建议先定一个最低成本的验证动作,再决定是否进入完整 PRD。']),
329
+ ...(stopLossActions.length > 0
330
+ ? stopLossActions.slice(0, 2).map((item) => `如果验证不顺,先这样止损:${item}`)
331
+ : ['如果验证结果不达预期,最好提前定义什么情况下先停。']),
332
+ ];
333
+ const validationLoop = [
334
+ ...(community.length > 0
335
+ ? [`先去这里找真实反馈:${community.slice(0, 2).join('、')}。`]
336
+ : ['先补第一批最容易触达的社区、渠道或人群。']),
337
+ ...(seedUsers.length > 0
338
+ ? [`第一批优先先找:${seedUsers.slice(0, 2).join('、')}。`]
339
+ : ['先补第一批最值得先聊、先服务的人。']),
340
+ ...(currentAlternative
341
+ ? [`当前主要替代方案是:${currentAlternative}。`]
342
+ : ['先补用户现在主要靠什么办法在解决。']),
343
+ ...(manualPath.length > 0
344
+ ? [`先不做完整产品时,也可以这样手工交付:${manualPath.slice(0, 2).join('、')}。`]
345
+ : ['先补一条不做完整产品也能交付价值的手工路径。']),
346
+ ];
347
+ const businessViability = [
348
+ ...(commitmentSignals.length > 0
349
+ ? commitmentSignals.slice(0, 2).map((item) => `先用这种承诺证明值得继续:${item}`)
350
+ : ['先定义什么真实承诺最能证明值得继续。']),
351
+ ...(defaultAlivePlan.length > 0
352
+ ? defaultAlivePlan.slice(0, 2).map((item) => `先活下来要守住:${item}`)
353
+ : ['先补验证阶段怎样控制成本、时间和交付方式,确保这件事先活下来。']),
354
+ ...(firstValidationStep
355
+ ? [`下一步最便宜的验证动作是:${firstValidationStep}`]
356
+ : ['先定一个最低成本验证动作,再决定要不要继续做大。']),
357
+ ];
358
+ const reuseFoundation = [
359
+ ...(inScope.length > 0
360
+ ? inScope.slice(0, 2).map((item) => `这轮范围里可以先围绕“${item}”判断现有能力能不能直接借。`)
361
+ : []),
362
+ ...(workspaceScan.docs.length > 0
363
+ ? ['当前项目里已经有现成文档或历史方案,可以先借现有边界和思路。']
364
+ : []),
365
+ ...(workspaceScan.activeArtifacts.length > 0
366
+ ? ['当前项目里已经有相关评审或方案产物,可以先沿用现成结构。']
367
+ : []),
368
+ ...(workspaceScan.reuseOpportunities.length > 0
369
+ ? [`当前工作区里已经识别到 ${Math.min(workspaceScan.reuseOpportunities.length, 8)} 类可能复用的能力,建议先看能不能少做一遍。`]
370
+ : []),
371
+ ...(stakeholders.length > 0
372
+ ? [`这件事还需要 ${stakeholders.slice(0, 3).join('、')} 一起参与或拍板。`]
373
+ : ['相关参与方和拍板人还没补齐,后面容易在范围或节奏上来回返工。']),
374
+ ];
375
+ const externalReferences = [...benchmarkSignals, ...knowledgeSignals].slice(0, 8);
376
+ const unresolved = [
377
+ ...(outOfScope.length > 0 ? outOfScope.map((item) => `这轮先不展开:${item}`) : []),
378
+ ...(openQuestions.length > 0 ? openQuestions.map((item) => `还要确认:${item}`) : ['还需要补 1 轮高价值确认,避免直接进入实现。']),
379
+ ];
380
+ const nextSteps = [
381
+ `如果方向基本认可,下一步就把这次结论整理成 PRD。`,
382
+ `如果还没把握,先补“当前替代方案、关键前提、低成本验证”这三类信息。`,
383
+ ];
384
+
385
+ return {
386
+ executiveSummary: normalizedText(currentState.problemStatement)
387
+ ? `围绕“${topic}”,这次更像是先把问题、目标和第一版思路想清楚,再决定怎么整理成 PRD。`
388
+ : `围绕“${topic}”,现在还在想清楚方向,建议先把目标、用户和第一版思路收拢,再进入 PRD。`,
389
+ currentSituation: currentSituation.slice(0, 8),
390
+ directionOptions: directionOptions.slice(0, 8),
391
+ validationPlan: validationPlan.slice(0, 8),
392
+ reuseFoundation: reuseFoundation.slice(0, 8),
393
+ externalReferences,
394
+ userSignals: currentSituation.slice(0, 8),
395
+ marketSignals: directionOptions.slice(0, 8),
396
+ validationLoop: validationLoop.slice(0, 8),
397
+ businessViability: businessViability.slice(0, 8),
398
+ risks: validationPlan.slice(0, 8),
399
+ reuseOpportunities: reuseFoundation.slice(0, 8),
400
+ openQuestions: openQuestions.slice(0, 8),
401
+ unresolved: unresolved.slice(0, 8),
402
+ nextSteps,
403
+ };
404
+ }
405
+
406
+ export async function loadBrainstormState(projectRoot) {
407
+ const ws = await loadWorkspace(projectRoot);
408
+ if (!(await exists(ws.paths.activeBrainstormState))) {
409
+ throw new Error(`未找到脑暴状态文件: ${ws.paths.activeBrainstormState}。先运行 openprd brainstorm .`);
410
+ }
411
+ const record = await readJson(ws.paths.activeBrainstormState);
412
+ return { ws, record };
413
+ }
414
+
415
+ export async function renderBrainstormWorkspaceArtifacts(projectRoot, record, options = {}) {
416
+ const ws = await loadWorkspace(projectRoot);
417
+ const artifactId = normalizedText(record?.artifactId) || 'brainstorm-active';
418
+ const bundle = artifactBundlePaths(ws, artifactId);
419
+ const patch = renderBrainstormPatch({ record });
420
+ const markdown = renderBrainstormMarkdown({ record });
421
+
422
+ const nextRecord = {
423
+ ...record,
424
+ artifacts: {
425
+ bundleDir: bundle.dir,
426
+ htmlPath: ws.paths.activeBrainstormHtml,
427
+ bundleHtmlPath: bundle.html,
428
+ markdownPath: bundle.markdown,
429
+ patchPath: bundle.patch,
430
+ statePath: ws.paths.activeBrainstormState,
431
+ },
432
+ };
433
+
434
+ await writeText(bundle.markdown, markdown);
435
+ await writeJson(bundle.patch, patch);
436
+ await writeJson(ws.paths.activeBrainstormState, nextRecord);
437
+
438
+ const html = renderBrainstormArtifact({
439
+ record: nextRecord,
440
+ markdownPath: bundle.markdown,
441
+ patchPath: bundle.patch,
442
+ statePath: ws.paths.activeBrainstormState,
443
+ });
444
+ await writeHtmlArtifact(bundle.html, html);
445
+ await writeHtmlArtifact(ws.paths.activeBrainstormHtml, html);
446
+
447
+ if (options.open) {
448
+ await openArtifactInBrowser(ws.paths.activeBrainstormHtml);
449
+ }
450
+
451
+ return {
452
+ ws,
453
+ record: nextRecord,
454
+ htmlPath: ws.paths.activeBrainstormHtml,
455
+ bundleHtmlPath: bundle.html,
456
+ markdownPath: bundle.markdown,
457
+ patchPath: bundle.patch,
458
+ statePath: ws.paths.activeBrainstormState,
459
+ opened: Boolean(options.open),
460
+ };
461
+ }
462
+
463
+ export async function brainstormWorkspace(projectRoot, options = {}) {
464
+ const ws = await loadWorkspace(projectRoot);
465
+ if (!(await exists(ws.workspaceRoot))) {
466
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
467
+ }
468
+
469
+ const currentState = {
470
+ ...(ws.data.currentState ?? {}),
471
+ captureMeta: {
472
+ ...((ws.data.currentState ?? {}).captureMeta ?? {}),
473
+ },
474
+ };
475
+ const topic = normalizedText(options.topic)
476
+ || normalizedText(currentState.title)
477
+ || normalizedText(currentState.problemStatement)
478
+ || '当前需求方向脑暴';
479
+ const benchmarkResult = await listBenchmarkWorkspace(projectRoot).catch(() => ({
480
+ approved: [],
481
+ candidates: [],
482
+ recommendations: [],
483
+ counts: { approved: 0, candidates: 0 },
484
+ }));
485
+ const knowledgeIndex = await readJson(ws.paths.knowledgeIndex).catch(() => ({}));
486
+ const previousRecord = await readJson(ws.paths.activeBrainstormState).catch(() => null);
487
+ const workspaceScan = {
488
+ docs: await listDocsBasic(projectRoot),
489
+ reuseOpportunities: await listTopLevelReusableAreas(projectRoot),
490
+ activeArtifacts: await listActiveArtifacts(ws),
491
+ };
492
+ workspaceScan.followUpPrompts = buildFollowUpPrompts(currentState, workspaceScan);
493
+
494
+ const benchmark = summarizeBenchmarks(benchmarkResult);
495
+ const knowledge = summarizeKnowledge(knowledgeIndex);
496
+ const report = buildBrainstormReport({
497
+ currentState,
498
+ benchmark,
499
+ knowledge,
500
+ workspaceScan,
501
+ topic,
502
+ });
503
+ const artifactId = `brainstorm-${slugify(topic, 'active')}`;
504
+ const generatedAt = timestamp();
505
+ const recordBase = {
506
+ schema: 'openprd.brainstorm.v3',
507
+ artifactId,
508
+ topic,
509
+ title: `${topic} / 方向梳理`,
510
+ generatedAt,
511
+ currentStatus: currentState.status ?? 'initialized',
512
+ productType: currentState.productType ?? null,
513
+ productTypeLabel: currentState.productType ?? '待确认',
514
+ summary: {
515
+ recommendedDirection: goalsDirection(currentState, topic),
516
+ currentAlternative: summarizeCurrentAlternative(currentState),
517
+ confidenceLabel: buildConfidenceLabel(currentState, benchmark, knowledge),
518
+ sourceCoverageLabel: buildSourceCoverageLabel({ benchmark, knowledge, workspaceScan }),
519
+ },
520
+ captureState: {
521
+ problemStatement: currentState.problemStatement ?? '',
522
+ whyNow: currentState.whyNow ?? '',
523
+ primaryUsers: listOfStrings(currentState.primaryUsers),
524
+ stakeholders: listOfStrings(currentState.stakeholders),
525
+ community: listOfStrings(currentState.community),
526
+ seedUsers: listOfStrings(currentState.seedUsers),
527
+ goals: listOfStrings(currentState.goals),
528
+ successMetrics: listOfStrings(currentState.successMetrics),
529
+ inScope: listOfStrings(currentState.inScope),
530
+ outOfScope: listOfStrings(currentState.outOfScope),
531
+ primaryFlows: listOfStrings(currentState.primaryFlows),
532
+ openQuestions: listOfStrings(currentState.openQuestions),
533
+ assumptions: listOfStrings(currentState.assumptions),
534
+ stopLossActions: listOfStrings(currentState.stopLossActions),
535
+ currentAlternative: currentState.currentAlternative ?? '',
536
+ manualPath: listOfStrings(currentState.manualPath),
537
+ commitmentSignals: listOfStrings(currentState.commitmentSignals),
538
+ firstValidationStep: currentState.firstValidationStep ?? '',
539
+ defaultAlivePlan: listOfStrings(currentState.defaultAlivePlan),
540
+ asIs: currentState.asIs ?? '',
541
+ toBe: currentState.toBe ?? '',
542
+ nextStep: currentState.nextStep ?? '',
543
+ },
544
+ report,
545
+ benchmark,
546
+ knowledge,
547
+ workspaceScan,
548
+ brainstormPresentation: previousRecord?.brainstormPresentation ?? null,
549
+ brainstormPresentationMeta: previousRecord?.brainstormPresentationMeta ?? null,
550
+ };
551
+ const digest = hashStableJson(recordBase);
552
+ const record = {
553
+ ...recordBase,
554
+ digest,
555
+ };
556
+
557
+ const rendered = await renderBrainstormWorkspaceArtifacts(projectRoot, record, { open: options.open || !options.json });
558
+ currentState.brainstormMeta = {
559
+ generatedAt,
560
+ artifactId,
561
+ digest,
562
+ topic,
563
+ htmlPath: toRelativeProjectPath(projectRoot, rendered.htmlPath),
564
+ markdownPath: toRelativeProjectPath(projectRoot, rendered.markdownPath),
565
+ patchPath: toRelativeProjectPath(projectRoot, rendered.patchPath),
566
+ statePath: toRelativeProjectPath(projectRoot, rendered.statePath),
567
+ recommendedDirection: record.summary.recommendedDirection,
568
+ };
569
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
570
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
571
+ await appendWorkflowEvent(ws, 'brainstorm_generated', {
572
+ artifactId,
573
+ digest,
574
+ htmlPath: rendered.htmlPath,
575
+ markdownPath: rendered.markdownPath,
576
+ patchPath: rendered.patchPath,
577
+ });
578
+ await appendProgress(ws, [
579
+ `已生成脑暴工作台: ${toRelativeProjectPath(projectRoot, rendered.htmlPath)}。`,
580
+ `当前建议方向: ${record.summary.recommendedDirection}。`,
581
+ ]);
582
+
583
+ return {
584
+ ...rendered,
585
+ currentState: storedCurrentState,
586
+ };
587
+ }
588
+
589
+ function goalsDirection(currentState, topic) {
590
+ const firstGoal = listOfStrings(currentState.goals)[0];
591
+ if (firstGoal) {
592
+ return `先围绕“${firstGoal}”收敛第一版做法`;
593
+ }
594
+ return `先把“${topic}”收敛成可落地的第一版`;
595
+ }
596
+
597
+ function summarizeCurrentAlternative(currentState) {
598
+ const currentAlternative = normalizedText(currentState.currentAlternative || currentState.asIs);
599
+ if (currentAlternative) {
600
+ return `现在主要还是靠“${currentAlternative}”在解决`;
601
+ }
602
+ return '还没明确现在主要靠什么方式在解决';
603
+ }