@openprd/cli 0.1.9 → 0.1.11

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 (138) hide show
  1. package/.openprd/benchmarks/index.md +16 -0
  2. package/.openprd/benchmarks/sources.yaml +53 -1
  3. package/.openprd/config.yaml +10 -0
  4. package/.openprd/design/README.md +47 -0
  5. package/.openprd/design/README_EN.md +25 -0
  6. package/.openprd/design/active/asset-spec.md +15 -0
  7. package/.openprd/design/active/direction-plan.md +7 -0
  8. package/.openprd/design/active/facts-sheet.md +13 -0
  9. package/.openprd/design/active/image-preflight.md +9 -0
  10. package/.openprd/design/active/selected-direction.md +11 -0
  11. package/.openprd/design/anti-slop.md +16 -0
  12. package/.openprd/design/assets/dark-mesh.svg +14 -0
  13. package/.openprd/design/assets/paper-grid.svg +14 -0
  14. package/.openprd/design/assets/surface-presets.md +21 -0
  15. package/.openprd/design/assets/workbench-grid.svg +16 -0
  16. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  17. package/.openprd/design/components/component-catalog.md +42 -0
  18. package/.openprd/design/layouts/layout-catalog.json +45 -0
  19. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  20. package/.openprd/design/recipes/content-experience.md +26 -0
  21. package/.openprd/design/recipes/operational-tool.md +25 -0
  22. package/.openprd/design/recipes/product-launch.md +26 -0
  23. package/.openprd/design/templates/README.md +36 -0
  24. package/.openprd/design/templates/README_EN.md +31 -0
  25. package/.openprd/design/templates/content-home.html +415 -0
  26. package/.openprd/design/templates/ops-dashboard.html +340 -0
  27. package/.openprd/design/templates/product-launch.html +360 -0
  28. package/.openprd/design/themes/theme-catalog.json +77 -0
  29. package/.openprd/engagements/active/prd.md +111 -100
  30. package/.openprd/schema/prd.schema.yaml +25 -0
  31. package/.openprd/templates/base/intake.md +17 -0
  32. package/.openprd/templates/base/prd.md +31 -6
  33. package/AGENTS.md +10 -7
  34. package/README.md +26 -28
  35. package/README_EN.md +3 -3
  36. package/package.json +41 -2
  37. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  38. package/skills/openprd-benchmark-router/SKILL.md +5 -0
  39. package/skills/openprd-frontend-design/SKILL.md +161 -0
  40. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  41. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  42. package/skills/openprd-harness/SKILL.md +37 -20
  43. package/skills/openprd-learning-review/SKILL.md +2 -1
  44. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  45. package/skills/openprd-quality/SKILL.md +5 -3
  46. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  47. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  48. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  49. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +108 -0
  50. package/skills/openprd-router/SKILL.md +13 -5
  51. package/skills/openprd-shared/SKILL.md +38 -25
  52. package/src/agent-integration.js +120 -69
  53. package/src/brainstorm-artifacts.js +1805 -0
  54. package/src/brainstorm-presentation.js +233 -0
  55. package/src/brainstorm.js +766 -0
  56. package/src/cli/args.js +51 -3
  57. package/src/cli/doctor-print.js +34 -8
  58. package/src/cli/print.js +6 -0
  59. package/src/cli/quality-print.js +79 -3
  60. package/src/cli/run-print.js +43 -1
  61. package/src/cli/shared-print.js +33 -2
  62. package/src/cli/workflow-print.js +21 -0
  63. package/src/codex-hook-runner-template.mjs +838 -58
  64. package/src/design-starter-support.js +462 -0
  65. package/src/design-starter.js +1207 -0
  66. package/src/diagram-core.js +384 -151
  67. package/src/html-artifacts.js +403 -50
  68. package/src/knowledge.js +291 -32
  69. package/src/learning-review.js +3 -3
  70. package/src/loop.js +839 -101
  71. package/src/openprd.js +86 -1
  72. package/src/openspec/change-validate.js +1 -0
  73. package/src/prd-core.js +119 -2
  74. package/src/quality-html-artifact.js +30 -5
  75. package/src/quality-learning.js +50 -6
  76. package/src/quality.js +22 -12
  77. package/src/run-harness.js +191 -29
  78. package/src/self-update.js +301 -4
  79. package/src/standards.js +46 -5
  80. package/src/visual-prepare.js +940 -0
  81. package/src/workspace-core.js +175 -0
  82. package/src/workspace-workflow.js +307 -7
  83. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  84. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  85. package/.openprd/discovery/config.json +0 -35
  86. package/.openprd/engagements/active/flows.md +0 -35
  87. package/.openprd/engagements/active/handoff.md +0 -16
  88. package/.openprd/engagements/active/review.html +0 -61
  89. package/.openprd/engagements/active/roles.md +0 -22
  90. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  91. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  92. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  93. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  94. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  95. package/.openprd/exports/.gitkeep +0 -0
  96. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  97. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  98. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  99. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  100. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  101. package/.openprd/knowledge/index.json +0 -47
  102. package/.openprd/reviews/v0001.html +0 -1322
  103. package/.openprd/reviews/v0002.html +0 -1150
  104. package/.openprd/reviews/v0003.html +0 -1150
  105. package/.openprd/reviews/v0004.html +0 -1150
  106. package/.openprd/reviews/v0005.html +0 -1150
  107. package/.openprd/sessions/.gitkeep +0 -0
  108. package/.openprd/state/changes.json +0 -27
  109. package/.openprd/state/current.json +0 -505
  110. package/.openprd/state/release-ledger.json +0 -387
  111. package/.openprd/state/version-index.json +0 -67
  112. package/.openprd/state/versions/.gitkeep +0 -0
  113. package/.openprd/state/versions/v0001.json +0 -121
  114. package/.openprd/state/versions/v0001.md +0 -161
  115. package/.openprd/state/versions/v0002.json +0 -264
  116. package/.openprd/state/versions/v0002.md +0 -183
  117. package/.openprd/state/versions/v0003.json +0 -269
  118. package/.openprd/state/versions/v0003.md +0 -188
  119. package/.openprd/state/versions/v0004.json +0 -274
  120. package/.openprd/state/versions/v0004.md +0 -193
  121. package/.openprd/state/versions/v0005.json +0 -299
  122. package/.openprd/state/versions/v0005.md +0 -189
  123. package/docs/assets/openprd-capability-overview-en.png +0 -0
  124. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  125. package/docs/assets/openprd-learning-html.png +0 -0
  126. package/docs/assets/openprd-quality-html.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  128. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  129. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  130. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  131. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  132. package/docs/assets/openprd-review-html.png +0 -0
  133. package/docs/assets/openprd-scenario-overview.png +0 -0
  134. package/docs/assets/openprd-scenario-overview.svg +0 -114
  135. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  136. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  137. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  138. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -0,0 +1,766 @@
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
+ listOfStrings(currentState.communityFit).length > 0 ? 'community-fit' : null,
184
+ normalizedText(currentState.currentAlternative || currentState.asIs) ? 'current-alternative' : null,
185
+ listOfStrings(currentState.painEvidence).length > 0 ? 'pain-evidence' : null,
186
+ listOfStrings(currentState.manualPath).length > 0 ? 'manual-path' : null,
187
+ listOfStrings(currentState.manualPlaybook).length > 0 ? 'manual-playbook' : null,
188
+ listOfStrings(currentState.commitmentSignals).length > 0 ? 'commitment' : null,
189
+ normalizedText(currentState.firstValidationStep || currentState.nextStep) ? 'validation-step' : null,
190
+ listOfStrings(currentState.paymentProof).length > 0 ? 'payment-proof' : null,
191
+ normalizedText(currentState.mvpSlice) ? 'mvp-slice' : null,
192
+ normalizedText(currentState.weekendTest) ? 'weekend-test' : null,
193
+ listOfStrings(currentState.smallestExecution).length > 0 ? 'smallest-execution' : null,
194
+ listOfStrings(currentState.productizeGate).length > 0 ? 'productize-gate' : null,
195
+ listOfStrings(currentState.firstCustomerPath).length > 0 ? 'first-customer-path' : null,
196
+ normalizedText(currentState.pricingHypothesis) ? 'pricing' : null,
197
+ normalizedText(currentState.customerOneProfitability) ? 'customer-one-profitability' : null,
198
+ listOfStrings(currentState.growthDiscipline).length > 0 ? 'growth-discipline' : null,
199
+ normalizedText(currentState.reversibility) ? 'reversibility' : null,
200
+ normalizedText(currentState.customerTruth) ? 'customer-truth' : null,
201
+ normalizedText(currentState.valuesFit) ? 'values-fit' : null,
202
+ listOfStrings(currentState.goals).length > 0 ? 'goals' : null,
203
+ listOfStrings(currentState.assumptions).length > 0 ? 'assumptions' : null,
204
+ benchmark.counts.approved > 0 ? 'benchmark' : null,
205
+ knowledge.counts.skills > 0 ? 'knowledge' : null,
206
+ ].filter(Boolean).length;
207
+ if (signals >= 8) return '已经有较完整基础,可以开始整理 PRD';
208
+ if (signals >= 5) return '方向基本有了,但还差 1 轮确认';
209
+ return '现在信息还不够,先继续把问题聊透';
210
+ }
211
+
212
+ function buildFollowUpPrompts(currentState, workspaceScan) {
213
+ const prompts = [];
214
+ if (listOfStrings(currentState.primaryUsers).length === 0) {
215
+ prompts.push('先补一轮目标用户和真实场景。');
216
+ }
217
+ if (listOfStrings(currentState.community).length === 0) {
218
+ prompts.push('先说清第一批最容易触达、最可能给真实反馈的人群或社区。');
219
+ }
220
+ if (!normalizedText(currentState.currentAlternative || currentState.asIs)) {
221
+ prompts.push('先说清楚现在是靠什么办法在解决这件事。');
222
+ }
223
+ if (listOfStrings(currentState.manualPath).length === 0) {
224
+ prompts.push('先补一条不做完整产品时也能手工交付价值的路径。');
225
+ }
226
+ if (listOfStrings(currentState.manualPlaybook).length === 0) {
227
+ prompts.push('先把手工作战卡写出来,至少补齐触发条件、步骤、工具、耗时和交接点。');
228
+ }
229
+ if (listOfStrings(currentState.communityFit).length === 0) {
230
+ prompts.push('先说清你为什么算这个社区里的自己人、他们在哪里聚集、为什么这个切口足够小,以及你现在是否已经在真实参与或贡献。');
231
+ }
232
+ if (listOfStrings(currentState.painEvidence).length === 0) {
233
+ prompts.push('先证明这个问题真的痛,用户现在已经在为更差的办法花时间或花钱。');
234
+ }
235
+ if (listOfStrings(currentState.commitmentSignals).length === 0) {
236
+ prompts.push('先定义什么真实承诺能证明这不是口头兴趣。');
237
+ }
238
+ if (!normalizedText(currentState.firstValidationStep || currentState.nextStep)) {
239
+ prompts.push('先定一个最低成本验证动作,不要一上来写全套方案。');
240
+ }
241
+ if (listOfStrings(currentState.defaultAlivePlan).length === 0) {
242
+ prompts.push('先说清验证阶段怎样控制成本、时间和交付方式,确保这件事先活下来。');
243
+ }
244
+ if (listOfStrings(currentState.paymentProof).length === 0) {
245
+ prompts.push('先补有没有 10 个样本、3/10 付费意愿或更强交易信号。');
246
+ }
247
+ if (!normalizedText(currentState.mvpSlice)) {
248
+ prompts.push('先收敛第一版到底只做哪一件事。');
249
+ }
250
+ if (!normalizedText(currentState.weekendTest)) {
251
+ prompts.push('先判断这件事能不能压成周末级 MVP 或更轻试跑。');
252
+ }
253
+ if (listOfStrings(currentState.smallestExecution).length === 0) {
254
+ prompts.push('先说清能不能用 spreadsheet、表单或 no-code 工具把第一版跑起来;如果必须开始做产品,也只自动化最重复的一步,先压成 forms / lists / CRUD 骨架,不要先为假想中的未来客户造复杂能力。');
255
+ }
256
+ if (listOfStrings(currentState.productizeGate).length === 0) {
257
+ prompts.push('先写清达到什么条件才允许继续产品化或加功能,先服务今天已经存在的重复需求,不要提前为未来规模开壳。');
258
+ }
259
+ if (listOfStrings(currentState.firstCustomerPath).length === 0) {
260
+ prompts.push('先说清第一批客户最现实的触达顺序。');
261
+ }
262
+ if (!normalizedText(currentState.pricingHypothesis)) {
263
+ prompts.push('先给出一个从第一个客户开始就成立的收费假设。');
264
+ }
265
+ if (!normalizedText(currentState.customerOneProfitability)) {
266
+ prompts.push('先判断客户 1 怎么覆盖时间和交付成本。');
267
+ }
268
+ if (listOfStrings(currentState.growthDiscipline).length === 0) {
269
+ prompts.push('先约束增长纪律:先熟人还是先社区、什么时候不该 launch、什么时候别急着花钱。');
270
+ }
271
+ if (!normalizedText(currentState.reversibility)) {
272
+ prompts.push('先说清如果验证结果一般,这条路有多容易回退,会不会逼你做重招聘、长期绑定或重平台化这类不可逆决策。');
273
+ }
274
+ if (!normalizedText(currentState.customerTruth)) {
275
+ prompts.push('先判断这更像客户真问题,还是团队自己的技术冲动。');
276
+ }
277
+ if (!normalizedText(currentState.valuesFit)) {
278
+ prompts.push('先确认这条路是否符合团队现在想坚持的价值观和长期经营方式;如果连续这样做 3 到 5 年,你还愿意住在这套业务里吗?');
279
+ }
280
+ if (listOfStrings(currentState.goals).length === 0) {
281
+ prompts.push('先把想达到的目标和判断标准说清楚。');
282
+ }
283
+ if (listOfStrings(currentState.assumptions).length === 0) {
284
+ prompts.push('把这件事要成立必须为真的前提先列出来。');
285
+ }
286
+ if (listOfStrings(currentState.openQuestions).length === 0) {
287
+ prompts.push('把最影响判断的几个问题先列出来。');
288
+ }
289
+ if (workspaceScan.docs.length === 0) {
290
+ prompts.push('如果有现成方案或旧文档,可以一起作为参考。');
291
+ }
292
+ prompts.push('如果以前做过类似产品,优先看看有没有现成做法能直接借。');
293
+ return prompts;
294
+ }
295
+
296
+ function humanizeBenchmarkHint(item) {
297
+ const note = normalizedText(item.note);
298
+ if (note) return note;
299
+ const scenarios = listOfStrings(item.scenarios);
300
+ if (scenarios.length > 0) {
301
+ return `外部类似案例更常出现在 ${scenarios.slice(0, 2).join('、')} 这些场景。`;
302
+ }
303
+ const triggerWhen = listOfStrings(item.triggerWhen);
304
+ if (triggerWhen.length > 0) {
305
+ return `这类参考通常适合用在 ${triggerWhen.slice(0, 2).join('、')} 这些情况。`;
306
+ }
307
+ return '外部类似案例普遍强调先缩小第一版范围,再进入开发。';
308
+ }
309
+
310
+ function buildBrainstormReport({ currentState, benchmark, knowledge, workspaceScan, topic }) {
311
+ const primaryUsers = listOfStrings(currentState.primaryUsers);
312
+ const stakeholders = listOfStrings(currentState.stakeholders);
313
+ const goals = listOfStrings(currentState.goals);
314
+ const successMetrics = listOfStrings(currentState.successMetrics);
315
+ const inScope = listOfStrings(currentState.inScope);
316
+ const outOfScope = listOfStrings(currentState.outOfScope);
317
+ const primaryFlows = listOfStrings(currentState.primaryFlows);
318
+ const openQuestions = listOfStrings(currentState.openQuestions);
319
+ const assumptions = listOfStrings(currentState.assumptions);
320
+ const stopLossActions = listOfStrings(currentState.stopLossActions);
321
+ const community = listOfStrings(currentState.community);
322
+ const seedUsers = listOfStrings(currentState.seedUsers);
323
+ const currentAlternative = normalizedText(currentState.currentAlternative || currentState.asIs);
324
+ const communityFit = listOfStrings(currentState.communityFit);
325
+ const painEvidence = listOfStrings(currentState.painEvidence);
326
+ const manualPath = listOfStrings(currentState.manualPath);
327
+ const manualPlaybook = listOfStrings(currentState.manualPlaybook);
328
+ const commitmentSignals = listOfStrings(currentState.commitmentSignals);
329
+ const firstValidationStep = normalizedText(currentState.firstValidationStep || currentState.nextStep);
330
+ const defaultAlivePlan = listOfStrings(currentState.defaultAlivePlan);
331
+ const paymentProof = listOfStrings(currentState.paymentProof);
332
+ const mvpSlice = normalizedText(currentState.mvpSlice);
333
+ const weekendTest = normalizedText(currentState.weekendTest);
334
+ const smallestExecution = listOfStrings(currentState.smallestExecution);
335
+ const productizeGate = listOfStrings(currentState.productizeGate);
336
+ const firstCustomerPath = listOfStrings(currentState.firstCustomerPath);
337
+ const pricingHypothesis = normalizedText(currentState.pricingHypothesis);
338
+ const customerOneProfitability = normalizedText(currentState.customerOneProfitability);
339
+ const growthDiscipline = listOfStrings(currentState.growthDiscipline);
340
+ const reversibility = normalizedText(currentState.reversibility);
341
+ const customerTruth = normalizedText(currentState.customerTruth);
342
+ const valuesFit = normalizedText(currentState.valuesFit);
343
+ const toBe = normalizedText(currentState.toBe);
344
+ const whyNow = normalizedText(currentState.whyNow);
345
+ const nextStep = normalizedText(currentState.nextStep);
346
+ const benchmarkSignals = benchmark.approved.slice(0, 4).map((item) => humanizeBenchmarkHint(item));
347
+ const knowledgeSignals = knowledge.skills.slice(0, 4).map((item) => {
348
+ const hint = normalizedText(item.description);
349
+ return hint
350
+ ? `当前项目里已经有一些可借鉴的经验:${hint}`
351
+ : '当前项目里已经有类似经验,优先确认能不能直接借用。';
352
+ });
353
+ const currentSituation = [
354
+ ...(primaryUsers.length > 0
355
+ ? [`这次先重点看 ${primaryUsers.slice(0, 2).join('、')} 的真实问题,不要一开始同时服务太多角色。`]
356
+ : ['当前还没锁定最核心的人群,建议先补最近一次真实场景。']),
357
+ ...(primaryFlows.length > 0
358
+ ? [`最近最值得先看的场景是:${primaryFlows[0]}。`]
359
+ : ['当前主流程还比较空,建议先补“最近一次真实发生的案例”。']),
360
+ ...(community.length > 0
361
+ ? [`第一批最容易先验证的人群或社区是:${community.slice(0, 2).join('、')}。`]
362
+ : ['当前还没锁定第一批最容易触达的人群或社区,建议先补最现实的反馈入口。']),
363
+ ...(communityFit.length > 0
364
+ ? [`你现在更适合先从这里切入:${communityFit.slice(0, 2).join('、')}。`]
365
+ : ['当前还没证明你为什么算这个社区里的自己人,建议先补“为什么现在就能触达”。']),
366
+ ...(currentAlternative
367
+ ? [`现在主要还是靠“${currentAlternative}”这类办法在撑着,先判断值不值得替换。`]
368
+ : ['当前还没明确现在是怎么解决的,建议先补现有替代方案。']),
369
+ ...(painEvidence.length > 0
370
+ ? [`这个问题之所以值得做,主要证据是:${painEvidence.slice(0, 2).join(';')}。`]
371
+ : ['当前还没证明这个问题到底有多痛,建议先补真实痛点和现有成本。']),
372
+ ...(whyNow
373
+ ? [`现在提这件事,主要因为:${whyNow}`]
374
+ : ['为什么现在要做还不够具体,建议先说清触发这件事的变化。']),
375
+ ...(mvpSlice
376
+ ? [`如果现在就动手,第一版最好只做“${mvpSlice}”这一件事。`]
377
+ : ['当前还没把第一版压成“一件事 MVP”,建议继续收范围。']),
378
+ ];
379
+ const directionOptions = [
380
+ `推荐方向:${goalsDirection(currentState, topic)}。`,
381
+ ...(toBe
382
+ ? [`更理想的结果是先做到:${toBe}。`]
383
+ : goals.length > 0
384
+ ? [`第一版先围绕“${goals[0]}”收范围,不急着把所有问题一次做完。`]
385
+ : [`第一版先把“${topic}”收敛成可落地的小切口。`]),
386
+ ...(manualPath.length > 0
387
+ ? [`不等完整产品也可以先这样交付价值:${manualPath.slice(0, 2).join('、')}。`]
388
+ : ['如果还没准备好完整产品,建议先补一条手工服务或半自动交付路径。']),
389
+ ...(weekendTest
390
+ ? [`如果要压轻一点,可以先跑这样一个周末级验证:${weekendTest}。`]
391
+ : ['建议再压一层,先想清楚有没有周末级 MVP 或更轻的试跑方式。']),
392
+ ...(smallestExecution.length > 0
393
+ ? [`更小的第一版可以先这样跑:${smallestExecution.slice(0, 2).join('、')}。`]
394
+ : ['建议优先想清楚能不能先用 spreadsheet、表单或 no-code 跑起来;如果必须开始做产品,也只自动化最重复的一步,先压成 forms / lists / CRUD 骨架,不要先为假想中的未来客户造复杂能力。']),
395
+ ...(inScope.length > 0
396
+ ? [`这轮先聚焦:${inScope.slice(0, 2).join('、')}。`]
397
+ : ['范围边界还不够清楚,建议先补“这轮先做什么、先不做什么”。']),
398
+ ...(benchmarkSignals.length > 0
399
+ ? [`外部类似做法大多也是先缩小第一版,再继续扩展。`]
400
+ : []),
401
+ ...(workspaceScan.reuseOpportunities.length > 0
402
+ ? ['备选思路:先盘点现有基础,能直接借的就不从零再做一遍。']
403
+ : []),
404
+ ];
405
+ const validationPlan = [
406
+ ...(communityFit.length > 0
407
+ ? communityFit.slice(0, 2).map((item) => `社区契合:${item}`)
408
+ : ['建议先证明你为什么算这个社区里的自己人、他们在哪里聚集、为什么这个切口够小,以及你现在是否已经在真实参与或贡献。']),
409
+ ...(manualPlaybook.length > 0
410
+ ? manualPlaybook.slice(0, 2).map((item) => `手工作战卡:${item}`)
411
+ : ['建议先把手工作战卡写出来,至少补齐触发条件、步骤、工具、耗时和交接点。']),
412
+ ...(commitmentSignals.length > 0
413
+ ? commitmentSignals.slice(0, 2).map((item) => `先看真实承诺:${item}`)
414
+ : ['建议先定义什么真实承诺能证明这不是口头兴趣。']),
415
+ ...(assumptions.length > 0
416
+ ? assumptions.slice(0, 2).map((item) => `关键前提:${item}`)
417
+ : ['当前还没把“这件事要成立,哪些前提必须为真”说清楚。']),
418
+ ...(successMetrics.length > 0
419
+ ? successMetrics.slice(0, 2).map((item) => `验证通过可以先看:${item}`)
420
+ : ['建议先补 1 到 2 条可验证标准,方便判断第一版值不值得继续。']),
421
+ ...(firstValidationStep
422
+ ? [`低成本先这样验证:${firstValidationStep}`]
423
+ : openQuestions.length > 0
424
+ ? [`低成本先验证最不确定的问题:${openQuestions[0]}`]
425
+ : ['建议先定一个最低成本的验证动作,再决定是否进入完整 PRD。']),
426
+ ...(paymentProof.length > 0
427
+ ? paymentProof.slice(0, 2).map((item) => `付费验证:${item}`)
428
+ : ['建议先补有没有 10 个样本、3/10 付费意愿或更强交易信号。']),
429
+ ...(weekendTest
430
+ ? [`再压一层可以先做:${weekendTest}`]
431
+ : ['如果还太重,建议继续把验证动作压到周末级别。']),
432
+ ...(productizeGate.length > 0
433
+ ? productizeGate.slice(0, 2).map((item) => `产品化门槛:${item}`)
434
+ : ['建议先写清达到什么条件才允许继续产品化,而不是一有想法就继续扩。']),
435
+ ...(stopLossActions.length > 0
436
+ ? stopLossActions.slice(0, 2).map((item) => `如果验证不顺,先这样止损:${item}`)
437
+ : ['如果验证结果不达预期,最好提前定义什么情况下先停。']),
438
+ ];
439
+ const validationLoop = [
440
+ ...(community.length > 0
441
+ ? [`先去这里找真实反馈:${community.slice(0, 2).join('、')}。`]
442
+ : ['先补第一批最容易触达的社区、渠道或人群。']),
443
+ ...(communityFit.length > 0
444
+ ? [`你更像这个圈子的自己人,因为:${communityFit.slice(0, 2).join('、')}。`]
445
+ : ['先补你为什么算这个社区里的自己人、他们为什么愿意先相信你。']),
446
+ ...(seedUsers.length > 0
447
+ ? [`第一批优先先找:${seedUsers.slice(0, 2).join('、')}。`]
448
+ : ['先补第一批最值得先聊、先服务的人。']),
449
+ ...(currentAlternative
450
+ ? [`当前主要替代方案是:${currentAlternative}。`]
451
+ : ['先补用户现在主要靠什么办法在解决。']),
452
+ ...(painEvidence.length > 0
453
+ ? [`痛点证据更像这样:${painEvidence.slice(0, 2).join(';')}。`]
454
+ : ['先补这个问题到底有多痛,用户现在已经在付出什么时间或金钱。']),
455
+ ...(manualPath.length > 0
456
+ ? [`先不做完整产品时,也可以这样手工交付:${manualPath.slice(0, 2).join('、')}。`]
457
+ : ['先补一条不做完整产品也能交付价值的手工路径。']),
458
+ ...(manualPlaybook.length > 0
459
+ ? manualPlaybook.slice(0, 2).map((item) => `手工作战卡先这样写:${item}`)
460
+ : ['先补一版手工作战卡,把触发条件、步骤、工具、耗时和交接点写清楚。']),
461
+ ...(firstCustomerPath.length > 0
462
+ ? [`第一批客户更现实的顺序是:${firstCustomerPath.slice(0, 3).join(' -> ')}。`]
463
+ : ['先补第一批客户最现实的触达顺序,比如熟人、社区和陌生线索怎么排。']),
464
+ ];
465
+ const businessViability = [
466
+ ...(commitmentSignals.length > 0
467
+ ? commitmentSignals.slice(0, 2).map((item) => `先用这种承诺证明值得继续:${item}`)
468
+ : ['先定义什么真实承诺最能证明值得继续。']),
469
+ ...(paymentProof.length > 0
470
+ ? paymentProof.slice(0, 2).map((item) => `付费证明:${item}`)
471
+ : ['先补 10 个样本里有没有 3/10 愿意付费、预付或给出更强承诺。']),
472
+ ...(pricingHypothesis
473
+ ? [`从第一个客户开始,先按这个方式收费:${pricingHypothesis}`]
474
+ : ['先给一个从第一个客户开始就成立的收费假设,不要无限期免费。']),
475
+ ...(customerOneProfitability
476
+ ? [`客户 1 的盈利或打平路径:${customerOneProfitability}`]
477
+ : ['先判断第一个客户怎么覆盖时间和交付成本。']),
478
+ ...(defaultAlivePlan.length > 0
479
+ ? defaultAlivePlan.slice(0, 2).map((item) => `先活下来要守住:${item}`)
480
+ : ['先补验证阶段怎样控制成本、时间和交付方式,确保这件事先活下来。']),
481
+ ...(firstValidationStep
482
+ ? [`下一步最便宜的验证动作是:${firstValidationStep}`]
483
+ : ['先定一个最低成本验证动作,再决定要不要继续做大。']),
484
+ ...(growthDiscipline.length > 0
485
+ ? growthDiscipline.slice(0, 2).map((item) => `增长纪律:${item}`)
486
+ : ['先约束增长纪律:先卖再 launch,先花时间别先花钱。']),
487
+ ];
488
+ const minimalistReview = [
489
+ ...(smallestExecution.length > 0
490
+ ? smallestExecution.slice(0, 2).map((item) => `更小的执行方式:${item}`)
491
+ : ['先判断能不能继续保持小:先用 spreadsheet、表单或 no-code 跑;如果必须开始做产品,也只自动化最重复的一步,先压成 forms / lists / CRUD 骨架,不要先为假想中的未来客户造复杂能力。']),
492
+ ...(productizeGate.length > 0
493
+ ? productizeGate.slice(0, 2).map((item) => `产品化条件:${item}`)
494
+ : ['先写清达到什么条件才允许产品化,别让功能冲动替代真实门槛。']),
495
+ ...(reversibility
496
+ ? [`如果结果一般,这条路的可逆性是:${reversibility}`]
497
+ : ['先判断如果验证结果一般,这条路是不是容易回退,避免先做重招聘、长期绑定或重平台化。']),
498
+ ...(customerTruth
499
+ ? [`客户真问题校验:${customerTruth}`]
500
+ : ['先判断这更像在解决客户真问题,还是团队自己的技术冲动。']),
501
+ ...(valuesFit
502
+ ? [`价值观一致性:${valuesFit}`]
503
+ : ['先确认这条路是否符合团队现在想坚持的价值观和长期经营方式;如果连续这样做 3 到 5 年,你还愿意住在这套业务里吗?']),
504
+ ];
505
+ const reuseFoundation = [
506
+ ...(inScope.length > 0
507
+ ? inScope.slice(0, 2).map((item) => `这轮范围里可以先围绕“${item}”判断现有能力能不能直接借。`)
508
+ : []),
509
+ ...(workspaceScan.docs.length > 0
510
+ ? ['当前项目里已经有现成文档或历史方案,可以先借现有边界和思路。']
511
+ : []),
512
+ ...(workspaceScan.activeArtifacts.length > 0
513
+ ? ['当前项目里已经有相关评审或方案产物,可以先沿用现成结构。']
514
+ : []),
515
+ ...(workspaceScan.reuseOpportunities.length > 0
516
+ ? [`当前工作区里已经识别到 ${Math.min(workspaceScan.reuseOpportunities.length, 8)} 类可能复用的能力,建议先看能不能少做一遍。`]
517
+ : []),
518
+ ...(stakeholders.length > 0
519
+ ? [`这件事还需要 ${stakeholders.slice(0, 3).join('、')} 一起参与或拍板。`]
520
+ : ['相关参与方和拍板人还没补齐,后面容易在范围或节奏上来回返工。']),
521
+ ];
522
+ const externalReferences = [...benchmarkSignals, ...knowledgeSignals].slice(0, 8);
523
+ const unresolved = [
524
+ ...(outOfScope.length > 0 ? outOfScope.map((item) => `这轮先不展开:${item}`) : []),
525
+ ...(openQuestions.length > 0 ? openQuestions.map((item) => `还要确认:${item}`) : ['还需要补 1 轮高价值确认,避免直接进入实现。']),
526
+ ];
527
+ const nextSteps = [
528
+ `如果方向基本认可,下一步就把这次结论整理成 PRD。`,
529
+ `如果还没把握,先补“当前替代方案、关键前提、低成本验证”这三类信息。`,
530
+ ];
531
+
532
+ return {
533
+ executiveSummary: normalizedText(currentState.problemStatement)
534
+ ? `围绕“${topic}”,这次更像是先把问题、目标和第一版思路想清楚,再决定怎么整理成 PRD。`
535
+ : `围绕“${topic}”,现在还在想清楚方向,建议先把目标、用户和第一版思路收拢,再进入 PRD。`,
536
+ currentSituation: currentSituation.slice(0, 8),
537
+ directionOptions: directionOptions.slice(0, 8),
538
+ validationPlan: validationPlan.slice(0, 8),
539
+ reuseFoundation: reuseFoundation.slice(0, 8),
540
+ externalReferences,
541
+ userSignals: currentSituation.slice(0, 8),
542
+ marketSignals: directionOptions.slice(0, 8),
543
+ validationLoop: validationLoop.slice(0, 8),
544
+ businessViability: businessViability.slice(0, 8),
545
+ minimalistReview: minimalistReview.slice(0, 8),
546
+ risks: validationPlan.slice(0, 8),
547
+ reuseOpportunities: reuseFoundation.slice(0, 8),
548
+ openQuestions: openQuestions.slice(0, 8),
549
+ unresolved: unresolved.slice(0, 8),
550
+ nextSteps,
551
+ };
552
+ }
553
+
554
+ export async function loadBrainstormState(projectRoot) {
555
+ const ws = await loadWorkspace(projectRoot);
556
+ if (!(await exists(ws.paths.activeBrainstormState))) {
557
+ throw new Error(`未找到脑暴状态文件: ${ws.paths.activeBrainstormState}。先运行 openprd brainstorm .`);
558
+ }
559
+ const record = await readJson(ws.paths.activeBrainstormState);
560
+ return { ws, record };
561
+ }
562
+
563
+ export async function renderBrainstormWorkspaceArtifacts(projectRoot, record, options = {}) {
564
+ const ws = await loadWorkspace(projectRoot);
565
+ const artifactId = normalizedText(record?.artifactId) || 'brainstorm-active';
566
+ const bundle = artifactBundlePaths(ws, artifactId);
567
+ const patch = renderBrainstormPatch({ record });
568
+ const markdown = renderBrainstormMarkdown({ record });
569
+
570
+ const nextRecord = {
571
+ ...record,
572
+ artifacts: {
573
+ bundleDir: bundle.dir,
574
+ htmlPath: ws.paths.activeBrainstormHtml,
575
+ bundleHtmlPath: bundle.html,
576
+ markdownPath: bundle.markdown,
577
+ patchPath: bundle.patch,
578
+ statePath: ws.paths.activeBrainstormState,
579
+ },
580
+ };
581
+
582
+ await writeText(bundle.markdown, markdown);
583
+ await writeJson(bundle.patch, patch);
584
+ await writeJson(ws.paths.activeBrainstormState, nextRecord);
585
+
586
+ const html = renderBrainstormArtifact({
587
+ record: nextRecord,
588
+ markdownPath: bundle.markdown,
589
+ patchPath: bundle.patch,
590
+ statePath: ws.paths.activeBrainstormState,
591
+ });
592
+ await writeHtmlArtifact(bundle.html, html);
593
+ await writeHtmlArtifact(ws.paths.activeBrainstormHtml, html);
594
+
595
+ if (options.open) {
596
+ await openArtifactInBrowser(ws.paths.activeBrainstormHtml);
597
+ }
598
+
599
+ return {
600
+ ws,
601
+ record: nextRecord,
602
+ htmlPath: ws.paths.activeBrainstormHtml,
603
+ bundleHtmlPath: bundle.html,
604
+ markdownPath: bundle.markdown,
605
+ patchPath: bundle.patch,
606
+ statePath: ws.paths.activeBrainstormState,
607
+ opened: Boolean(options.open),
608
+ };
609
+ }
610
+
611
+ export async function brainstormWorkspace(projectRoot, options = {}) {
612
+ const ws = await loadWorkspace(projectRoot);
613
+ if (!(await exists(ws.workspaceRoot))) {
614
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
615
+ }
616
+
617
+ const currentState = {
618
+ ...(ws.data.currentState ?? {}),
619
+ captureMeta: {
620
+ ...((ws.data.currentState ?? {}).captureMeta ?? {}),
621
+ },
622
+ };
623
+ const topic = normalizedText(options.topic)
624
+ || normalizedText(currentState.title)
625
+ || normalizedText(currentState.problemStatement)
626
+ || '当前需求方向脑暴';
627
+ const benchmarkResult = await listBenchmarkWorkspace(projectRoot).catch(() => ({
628
+ approved: [],
629
+ candidates: [],
630
+ recommendations: [],
631
+ counts: { approved: 0, candidates: 0 },
632
+ }));
633
+ const knowledgeIndex = await readJson(ws.paths.knowledgeIndex).catch(() => ({}));
634
+ const previousRecord = await readJson(ws.paths.activeBrainstormState).catch(() => null);
635
+ const workspaceScan = {
636
+ docs: await listDocsBasic(projectRoot),
637
+ reuseOpportunities: await listTopLevelReusableAreas(projectRoot),
638
+ activeArtifacts: await listActiveArtifacts(ws),
639
+ };
640
+ workspaceScan.followUpPrompts = buildFollowUpPrompts(currentState, workspaceScan);
641
+
642
+ const benchmark = summarizeBenchmarks(benchmarkResult);
643
+ const knowledge = summarizeKnowledge(knowledgeIndex);
644
+ const report = buildBrainstormReport({
645
+ currentState,
646
+ benchmark,
647
+ knowledge,
648
+ workspaceScan,
649
+ topic,
650
+ });
651
+ const artifactId = `brainstorm-${slugify(topic, 'active')}`;
652
+ const generatedAt = timestamp();
653
+ const recordBase = {
654
+ schema: 'openprd.brainstorm.v3',
655
+ artifactId,
656
+ topic,
657
+ title: `${topic} / 方向梳理`,
658
+ generatedAt,
659
+ currentStatus: currentState.status ?? 'initialized',
660
+ productType: currentState.productType ?? null,
661
+ productTypeLabel: currentState.productType ?? '待确认',
662
+ summary: {
663
+ recommendedDirection: goalsDirection(currentState, topic),
664
+ currentAlternative: summarizeCurrentAlternative(currentState),
665
+ confidenceLabel: buildConfidenceLabel(currentState, benchmark, knowledge),
666
+ sourceCoverageLabel: buildSourceCoverageLabel({ benchmark, knowledge, workspaceScan }),
667
+ },
668
+ captureState: {
669
+ problemStatement: currentState.problemStatement ?? '',
670
+ whyNow: currentState.whyNow ?? '',
671
+ primaryUsers: listOfStrings(currentState.primaryUsers),
672
+ stakeholders: listOfStrings(currentState.stakeholders),
673
+ community: listOfStrings(currentState.community),
674
+ seedUsers: listOfStrings(currentState.seedUsers),
675
+ communityFit: listOfStrings(currentState.communityFit),
676
+ painEvidence: listOfStrings(currentState.painEvidence),
677
+ goals: listOfStrings(currentState.goals),
678
+ successMetrics: listOfStrings(currentState.successMetrics),
679
+ inScope: listOfStrings(currentState.inScope),
680
+ outOfScope: listOfStrings(currentState.outOfScope),
681
+ primaryFlows: listOfStrings(currentState.primaryFlows),
682
+ openQuestions: listOfStrings(currentState.openQuestions),
683
+ assumptions: listOfStrings(currentState.assumptions),
684
+ stopLossActions: listOfStrings(currentState.stopLossActions),
685
+ currentAlternative: currentState.currentAlternative ?? '',
686
+ manualPath: listOfStrings(currentState.manualPath),
687
+ manualPlaybook: listOfStrings(currentState.manualPlaybook),
688
+ commitmentSignals: listOfStrings(currentState.commitmentSignals),
689
+ firstValidationStep: currentState.firstValidationStep ?? '',
690
+ defaultAlivePlan: listOfStrings(currentState.defaultAlivePlan),
691
+ paymentProof: listOfStrings(currentState.paymentProof),
692
+ mvpSlice: currentState.mvpSlice ?? '',
693
+ weekendTest: currentState.weekendTest ?? '',
694
+ smallestExecution: listOfStrings(currentState.smallestExecution),
695
+ productizeGate: listOfStrings(currentState.productizeGate),
696
+ firstCustomerPath: listOfStrings(currentState.firstCustomerPath),
697
+ pricingHypothesis: currentState.pricingHypothesis ?? '',
698
+ customerOneProfitability: currentState.customerOneProfitability ?? '',
699
+ growthDiscipline: listOfStrings(currentState.growthDiscipline),
700
+ reversibility: currentState.reversibility ?? '',
701
+ customerTruth: currentState.customerTruth ?? '',
702
+ valuesFit: currentState.valuesFit ?? '',
703
+ asIs: currentState.asIs ?? '',
704
+ toBe: currentState.toBe ?? '',
705
+ nextStep: currentState.nextStep ?? '',
706
+ },
707
+ report,
708
+ benchmark,
709
+ knowledge,
710
+ workspaceScan,
711
+ brainstormPresentation: previousRecord?.brainstormPresentation ?? null,
712
+ brainstormPresentationMeta: previousRecord?.brainstormPresentationMeta ?? null,
713
+ };
714
+ const digest = hashStableJson(recordBase);
715
+ const record = {
716
+ ...recordBase,
717
+ digest,
718
+ };
719
+
720
+ const rendered = await renderBrainstormWorkspaceArtifacts(projectRoot, record, { open: options.open || !options.json });
721
+ currentState.brainstormMeta = {
722
+ generatedAt,
723
+ artifactId,
724
+ digest,
725
+ topic,
726
+ htmlPath: toRelativeProjectPath(projectRoot, rendered.htmlPath),
727
+ markdownPath: toRelativeProjectPath(projectRoot, rendered.markdownPath),
728
+ patchPath: toRelativeProjectPath(projectRoot, rendered.patchPath),
729
+ statePath: toRelativeProjectPath(projectRoot, rendered.statePath),
730
+ recommendedDirection: record.summary.recommendedDirection,
731
+ };
732
+ const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
733
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
734
+ await appendWorkflowEvent(ws, 'brainstorm_generated', {
735
+ artifactId,
736
+ digest,
737
+ htmlPath: rendered.htmlPath,
738
+ markdownPath: rendered.markdownPath,
739
+ patchPath: rendered.patchPath,
740
+ });
741
+ await appendProgress(ws, [
742
+ `已生成脑暴工作台: ${toRelativeProjectPath(projectRoot, rendered.htmlPath)}。`,
743
+ `当前建议方向: ${record.summary.recommendedDirection}。`,
744
+ ]);
745
+
746
+ return {
747
+ ...rendered,
748
+ currentState: storedCurrentState,
749
+ };
750
+ }
751
+
752
+ function goalsDirection(currentState, topic) {
753
+ const firstGoal = listOfStrings(currentState.goals)[0];
754
+ if (firstGoal) {
755
+ return `先围绕“${firstGoal}”收敛第一版做法`;
756
+ }
757
+ return `先把“${topic}”收敛成可落地的第一版`;
758
+ }
759
+
760
+ function summarizeCurrentAlternative(currentState) {
761
+ const currentAlternative = normalizedText(currentState.currentAlternative || currentState.asIs);
762
+ if (currentAlternative) {
763
+ return `现在主要还是靠“${currentAlternative}”在解决`;
764
+ }
765
+ return '还没明确现在主要靠什么方式在解决';
766
+ }