@openprd/cli 0.1.0

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 (154) hide show
  1. package/.openprd/README.md +82 -0
  2. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
  3. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
  4. package/.openprd/benchmarks/index.md +37 -0
  5. package/.openprd/benchmarks/sources.yaml +56 -0
  6. package/.openprd/config.yaml +50 -0
  7. package/.openprd/discovery/config.json +21 -0
  8. package/.openprd/engagements/active/flows.md +30 -0
  9. package/.openprd/engagements/active/handoff.md +9 -0
  10. package/.openprd/engagements/active/intake.md +15 -0
  11. package/.openprd/engagements/active/prd.md +161 -0
  12. package/.openprd/engagements/active/review.html +61 -0
  13. package/.openprd/engagements/active/roles.md +21 -0
  14. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
  15. package/.openprd/exports/.gitkeep +0 -0
  16. package/.openprd/knowledge/index.json +7 -0
  17. package/.openprd/quality/config.json +229 -0
  18. package/.openprd/reviews/v0001.html +1256 -0
  19. package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
  20. package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
  21. package/.openprd/schema/prd.schema.yaml +121 -0
  22. package/.openprd/sessions/.gitkeep +0 -0
  23. package/.openprd/standards/config.json +88 -0
  24. package/.openprd/standards/file-manual-template.md +28 -0
  25. package/.openprd/standards/folder-readme-template.md +28 -0
  26. package/.openprd/state/.gitkeep +0 -0
  27. package/.openprd/state/changes.json +12 -0
  28. package/.openprd/state/current.json +169 -0
  29. package/.openprd/state/version-index.json +15 -0
  30. package/.openprd/state/versions/.gitkeep +0 -0
  31. package/.openprd/state/versions/v0001.json +121 -0
  32. package/.openprd/state/versions/v0001.md +161 -0
  33. package/.openprd/templates/agent/intake.md +6 -0
  34. package/.openprd/templates/agent/prd.md +21 -0
  35. package/.openprd/templates/b2b/intake.md +6 -0
  36. package/.openprd/templates/b2b/prd.md +24 -0
  37. package/.openprd/templates/base/intake.md +18 -0
  38. package/.openprd/templates/base/prd.md +67 -0
  39. package/.openprd/templates/company/README.md +10 -0
  40. package/.openprd/templates/consumer/intake.md +6 -0
  41. package/.openprd/templates/consumer/prd.md +19 -0
  42. package/.openprd/templates/diagram/architecture.contract.json +53 -0
  43. package/.openprd/templates/diagram/product-flow.contract.json +76 -0
  44. package/.openprd/templates/industry/README.md +16 -0
  45. package/.openprd/templates/manifest.yaml +27 -0
  46. package/.openprd/templates/project/README.md +14 -0
  47. package/.openprd/templates/session/README.md +14 -0
  48. package/AGENTS.md +44 -0
  49. package/CONTRIBUTING.md +30 -0
  50. package/LICENSE +21 -0
  51. package/README.md +727 -0
  52. package/README_CN.md +583 -0
  53. package/SECURITY.md +23 -0
  54. package/bin/openprd.js +5 -0
  55. package/docs/assets/openprd-capability-overview-en.png +0 -0
  56. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  57. package/docs/assets/openprd-learning-html.png +0 -0
  58. package/docs/assets/openprd-quality-html.png +0 -0
  59. package/docs/assets/openprd-review-html.png +0 -0
  60. package/docs/assets/openprd-scenario-overview.png +0 -0
  61. package/docs/assets/openprd-scenario-overview.svg +114 -0
  62. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  63. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  64. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  65. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
  66. package/package.json +59 -0
  67. package/scripts/openprd-dev-check.mjs +5 -0
  68. package/scripts/openprd-review-presentation.mjs +82 -0
  69. package/skills/openprd-benchmark-router/SKILL.md +92 -0
  70. package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
  71. package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
  72. package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
  73. package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
  74. package/skills/openprd-diagram-review/SKILL.md +91 -0
  75. package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
  76. package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
  77. package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
  78. package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
  79. package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
  80. package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
  81. package/skills/openprd-discovery-loop/SKILL.md +196 -0
  82. package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
  83. package/skills/openprd-harness/SKILL.md +179 -0
  84. package/skills/openprd-harness/agents/openai.yaml +4 -0
  85. package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
  86. package/skills/openprd-harness/references/command-map.md +71 -0
  87. package/skills/openprd-harness/references/examples.md +26 -0
  88. package/skills/openprd-harness/references/usage-guide.md +335 -0
  89. package/skills/openprd-harness/references/workflow-gates.md +51 -0
  90. package/skills/openprd-learning-review/SKILL.md +75 -0
  91. package/skills/openprd-learning-review/agents/openai.yaml +4 -0
  92. package/skills/openprd-learning-review/references/content-contract.md +125 -0
  93. package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
  94. package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
  95. package/skills/openprd-learning-review/references/genre-library.md +43 -0
  96. package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
  97. package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
  98. package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
  99. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
  100. package/skills/openprd-quality/SKILL.md +101 -0
  101. package/skills/openprd-requirement-intake/SKILL.md +76 -0
  102. package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
  103. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
  104. package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
  105. package/skills/openprd-router/SKILL.md +40 -0
  106. package/skills/openprd-shared/SKILL.md +142 -0
  107. package/skills/openprd-shared/agents/openai.yaml +4 -0
  108. package/skills/openprd-shared/references/language-and-review.md +50 -0
  109. package/skills/openprd-shared/references/operating-rules.md +65 -0
  110. package/skills/openprd-shared/references/skill-architecture.md +70 -0
  111. package/skills/openprd-standards/SKILL.md +79 -0
  112. package/skills/openprd-standards/agents/openai.yaml +4 -0
  113. package/src/agent-integration.js +1717 -0
  114. package/src/benchmark.js +873 -0
  115. package/src/cli/args.js +460 -0
  116. package/src/cli/print.js +1423 -0
  117. package/src/codex-hook-runner-template.mjs +2422 -0
  118. package/src/dev-standards.js +372 -0
  119. package/src/diagram-core.js +1047 -0
  120. package/src/diagram-workspace.js +262 -0
  121. package/src/discovery.js +709 -0
  122. package/src/fleet.js +531 -0
  123. package/src/fs-utils.js +83 -0
  124. package/src/growth.js +545 -0
  125. package/src/html-artifacts.js +3803 -0
  126. package/src/knowledge.js +668 -0
  127. package/src/language-policy.js +142 -0
  128. package/src/learning-review.js +1655 -0
  129. package/src/loop.js +1290 -0
  130. package/src/openprd.js +1136 -0
  131. package/src/openspec/change-lifecycle.js +359 -0
  132. package/src/openspec/change-validate.js +248 -0
  133. package/src/openspec/constants.js +12 -0
  134. package/src/openspec/execute.js +300 -0
  135. package/src/openspec/generate.js +692 -0
  136. package/src/openspec/paths.js +111 -0
  137. package/src/openspec/tasks.js +352 -0
  138. package/src/prd-core.js +656 -0
  139. package/src/quality-html-artifact.js +1414 -0
  140. package/src/quality-learning.js +658 -0
  141. package/src/quality.js +1262 -0
  142. package/src/review-presentation.js +240 -0
  143. package/src/run-harness.js +1470 -0
  144. package/src/self-update.js +329 -0
  145. package/src/session-binding.js +140 -0
  146. package/src/source-inventory.js +224 -0
  147. package/src/standards.js +914 -0
  148. package/src/time.js +33 -0
  149. package/src/visual-compare.js +216 -0
  150. package/src/work-unit-migration.js +232 -0
  151. package/src/work-unit.js +88 -0
  152. package/src/workspace-core.js +1706 -0
  153. package/src/workspace-registry.js +162 -0
  154. package/src/workspace-workflow.js +1797 -0
@@ -0,0 +1,656 @@
1
+ import path from 'node:path';
2
+ import { buildArchitectureDiagramModel, buildProductFlowDiagramModel, renderDiagramMermaidFromModel } from './diagram-core.js';
3
+ import { TBD_ZH, languagePolicyLines } from './language-policy.js';
4
+ import { timestamp } from './time.js';
5
+
6
+ function isPlainObject(value) {
7
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+
10
+ function pickValue(...values) {
11
+ for (const value of values) {
12
+ if (value === null || value === undefined) continue;
13
+ if (typeof value === 'string' && value.trim() === '') continue;
14
+ if (Array.isArray(value) && value.length === 0) continue;
15
+ if (isPlainObject(value) && Object.keys(value).length === 0) continue;
16
+ return value;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function normalizeArray(value) {
22
+ if (Array.isArray(value)) {
23
+ return value.filter((item) => item !== null && item !== undefined && `${item}`.trim() !== '');
24
+ }
25
+ if (typeof value === 'string') {
26
+ return value
27
+ .split(/[\n,;]+/)
28
+ .map((item) => item.trim())
29
+ .filter(Boolean);
30
+ }
31
+ if (value === null || value === undefined) {
32
+ return [];
33
+ }
34
+ return [value];
35
+ }
36
+
37
+ const BUSINESS_GUARDRAIL_RISK_TOKENS = [
38
+ 'free user',
39
+ 'free tier',
40
+ 'trial',
41
+ 'quota',
42
+ 'usage limit',
43
+ 'rate limit',
44
+ 'credit',
45
+ 'metered',
46
+ 'cost',
47
+ 'spend',
48
+ 'third-party cost',
49
+ 'third-party api',
50
+ 'ai generation',
51
+ 'model call',
52
+ 'openai',
53
+ 'anthropic',
54
+ '免费用户',
55
+ '免费额度',
56
+ '试用',
57
+ '额度',
58
+ '用量',
59
+ '限流',
60
+ '积分',
61
+ '点数',
62
+ '成本',
63
+ '消耗',
64
+ '第三方成本',
65
+ '第三方调用',
66
+ '第三方 API',
67
+ '大模型',
68
+ '模型调用',
69
+ 'AI 生成',
70
+ '图像生成',
71
+ '内容生成',
72
+ '薅',
73
+ '滥用',
74
+ ];
75
+
76
+ function flattenForSearch(value) {
77
+ if (Array.isArray(value)) {
78
+ return value.map((item) => flattenForSearch(item)).join('\n');
79
+ }
80
+ if (isPlainObject(value)) {
81
+ return Object.entries(value)
82
+ .map(([key, entryValue]) => `${key}\n${flattenForSearch(entryValue)}`)
83
+ .join('\n');
84
+ }
85
+ return value === null || value === undefined ? '' : String(value);
86
+ }
87
+
88
+ function includesRiskToken(text) {
89
+ const normalized = String(text ?? '').toLowerCase();
90
+ return BUSINESS_GUARDRAIL_RISK_TOKENS.some((token) => normalized.includes(token.toLowerCase()));
91
+ }
92
+
93
+ function renderScalar(value) {
94
+ if (value === null || value === undefined || `${value}`.trim() === '') {
95
+ return TBD_ZH;
96
+ }
97
+ return `${value}`;
98
+ }
99
+
100
+ function renderChild(value, depth = 1) {
101
+ const indent = ' '.repeat(depth);
102
+
103
+ if (Array.isArray(value)) {
104
+ if (value.length === 0) {
105
+ return `${indent}- ${TBD_ZH}`;
106
+ }
107
+
108
+ return value
109
+ .map((item) => {
110
+ if (isPlainObject(item)) {
111
+ return Object.entries(item)
112
+ .map(([key, entryValue]) => `${indent}- ${key}: ${renderScalar(entryValue)}`)
113
+ .join('\n');
114
+ }
115
+ return `${indent}- ${renderScalar(item)}`;
116
+ })
117
+ .join('\n');
118
+ }
119
+
120
+ if (isPlainObject(value)) {
121
+ const entries = Object.entries(value);
122
+ if (entries.length === 0) {
123
+ return `${indent}- ${TBD_ZH}`;
124
+ }
125
+
126
+ return entries
127
+ .map(([key, entryValue]) => {
128
+ if (Array.isArray(entryValue) || isPlainObject(entryValue)) {
129
+ return `${indent}- ${key}:\n${renderChild(entryValue, depth + 1)}`;
130
+ }
131
+ return `${indent}- ${key}: ${renderScalar(entryValue)}`;
132
+ })
133
+ .join('\n');
134
+ }
135
+
136
+ return `${indent}- ${renderScalar(value)}`;
137
+ }
138
+
139
+ function renderField(label, value) {
140
+ if (Array.isArray(value) || isPlainObject(value)) {
141
+ return `- ${label}:\n${renderChild(value, 1)}`;
142
+ }
143
+ return `- ${label}: ${renderScalar(value)}`;
144
+ }
145
+
146
+ export function formatVersionId(versionNumber) {
147
+ return `v${String(versionNumber).padStart(4, '0')}`;
148
+ }
149
+
150
+ function buildTypeSpecificSection(productType, state, overrides) {
151
+ if (productType === 'consumer') {
152
+ return {
153
+ kind: 'consumer',
154
+ title: '消费端专项',
155
+ fields: {
156
+ persona: pickValue(overrides.persona, state.persona),
157
+ segment: pickValue(overrides.segment, state.segment),
158
+ journey: pickValue(overrides.journey, state.journey),
159
+ activationMetric: pickValue(overrides.activationMetric, state.activationMetric),
160
+ retentionMetric: pickValue(overrides.retentionMetric, state.retentionMetric),
161
+ },
162
+ };
163
+ }
164
+
165
+ if (productType === 'b2b') {
166
+ return {
167
+ kind: 'b2b',
168
+ title: 'B2B 专项',
169
+ fields: {
170
+ buyer: pickValue(overrides.buyer, state.buyer),
171
+ user: pickValue(overrides.user, state.user),
172
+ admin: pickValue(overrides.admin, state.admin),
173
+ operator: pickValue(overrides.operator, state.operator),
174
+ roles: pickValue(overrides.roles, state.roles),
175
+ asIs: pickValue(overrides.asIs, state.asIs),
176
+ toBe: pickValue(overrides.toBe, state.toBe),
177
+ permissionMatrix: pickValue(overrides.permissionMatrix, state.permissionMatrix),
178
+ approvalFlow: pickValue(overrides.approvalFlow, state.approvalFlow),
179
+ },
180
+ };
181
+ }
182
+
183
+ if (productType === 'agent') {
184
+ return {
185
+ kind: 'agent',
186
+ title: 'Agent 专项',
187
+ fields: {
188
+ humanAgentContract: pickValue(overrides.humanAgentContract, state.humanAgentContract),
189
+ autonomyBoundary: pickValue(overrides.autonomyBoundary, state.autonomyBoundary),
190
+ toolBoundary: pickValue(overrides.toolBoundary, state.toolBoundary),
191
+ stateModel: pickValue(overrides.stateModel, state.stateModel),
192
+ evalPlan: pickValue(overrides.evalPlan, state.evalPlan),
193
+ },
194
+ };
195
+ }
196
+
197
+ return {
198
+ kind: 'base',
199
+ title: '类型专项',
200
+ fields: {
201
+ note: '请选择产品类型,以启用对应的专项 PRD 模块。',
202
+ },
203
+ };
204
+ }
205
+
206
+ export function buildPrdSnapshot(ws, options = {}) {
207
+ const state = ws.data.currentState ?? {};
208
+ const versionNumber = options.versionNumber ?? state.prdVersion ?? 0;
209
+ const versionId = options.versionId ?? (versionNumber > 0 ? formatVersionId(versionNumber) : 'v0000');
210
+ const createdAt = options.createdAt ?? timestamp();
211
+ const productType = options.productType ?? state.productType ?? null;
212
+ const templatePack = options.templatePack ?? state.templatePack ?? ws.data.config?.activeTemplatePack ?? 'base';
213
+ const title = pickValue(options.title, state.title, path.basename(ws.projectRoot));
214
+ const owner = pickValue(options.owner, state.owner, TBD_ZH);
215
+ const status = pickValue(options.status, state.status, 'draft');
216
+
217
+ const sections = {
218
+ meta: {
219
+ title,
220
+ owner,
221
+ status,
222
+ version: versionId,
223
+ productType: productType ?? '未分类',
224
+ date: options.date ?? createdAt.slice(0, 10),
225
+ },
226
+ problem: {
227
+ problemStatement: pickValue(options.problemStatement, state.problemStatement),
228
+ whyNow: pickValue(options.whyNow, state.whyNow),
229
+ evidence: normalizeArray(pickValue(options.evidence, state.evidence)),
230
+ },
231
+ users: {
232
+ primaryUsers: normalizeArray(pickValue(options.primaryUsers, state.primaryUsers)),
233
+ secondaryUsers: normalizeArray(pickValue(options.secondaryUsers, state.secondaryUsers)),
234
+ stakeholders: normalizeArray(pickValue(options.stakeholders, state.stakeholders)),
235
+ },
236
+ goals: {
237
+ goals: normalizeArray(pickValue(options.goals, state.goals)),
238
+ successMetrics: normalizeArray(pickValue(options.successMetrics, state.successMetrics)),
239
+ acceptanceGoals: normalizeArray(pickValue(options.acceptanceGoals, state.acceptanceGoals)),
240
+ },
241
+ scope: {
242
+ inScope: normalizeArray(pickValue(options.inScope, state.inScope)),
243
+ outOfScope: normalizeArray(pickValue(options.outOfScope, state.outOfScope)),
244
+ },
245
+ scenarios: {
246
+ primaryFlows: normalizeArray(pickValue(options.primaryFlows, state.primaryFlows)),
247
+ edgeCases: normalizeArray(pickValue(options.edgeCases, state.edgeCases)),
248
+ failureModes: normalizeArray(pickValue(options.failureModes, state.failureModes)),
249
+ },
250
+ requirements: {
251
+ functional: normalizeArray(pickValue(options.functional, state.functional)),
252
+ nonFunctional: normalizeArray(pickValue(options.nonFunctional, state.nonFunctional)),
253
+ businessRules: normalizeArray(pickValue(options.businessRules, state.businessRules)),
254
+ },
255
+ businessGuardrails: {
256
+ costDrivers: normalizeArray(pickValue(options.costDrivers, state.costDrivers)),
257
+ usageLimits: normalizeArray(pickValue(options.usageLimits, state.usageLimits)),
258
+ abusePrevention: normalizeArray(pickValue(options.abusePrevention, state.abusePrevention)),
259
+ monitoringSignals: normalizeArray(pickValue(options.monitoringSignals, state.monitoringSignals)),
260
+ alertThresholds: normalizeArray(pickValue(options.alertThresholds, state.alertThresholds)),
261
+ stopLossActions: normalizeArray(pickValue(options.stopLossActions, state.stopLossActions)),
262
+ },
263
+ constraints: {
264
+ technical: normalizeArray(pickValue(options.technical, state.technical)),
265
+ compliance: normalizeArray(pickValue(options.compliance, state.compliance)),
266
+ dependencies: normalizeArray(pickValue(options.dependencies, state.dependencies)),
267
+ },
268
+ risks: {
269
+ assumptions: normalizeArray(pickValue(options.assumptions, state.assumptions)),
270
+ risks: normalizeArray(pickValue(options.risks, state.risks)),
271
+ openQuestions: normalizeArray(pickValue(options.openQuestions, state.openQuestions)),
272
+ },
273
+ handoff: {
274
+ owner: pickValue(options.handoffOwner, state.handoffOwner, owner),
275
+ nextStep: pickValue(options.nextStep, state.nextStep, '评审已生成的 PRD,并准备交接。'),
276
+ targetSystem: pickValue(options.targetSystem, state.targetSystem, 'OpenSpec'),
277
+ },
278
+ typeSpecific: buildTypeSpecificSection(productType, state, options),
279
+ };
280
+
281
+ return {
282
+ versionNumber,
283
+ versionId,
284
+ createdAt,
285
+ projectRoot: ws.projectRoot,
286
+ workspaceRoot: ws.workspaceRoot,
287
+ schema: ws.data.schema?.name ?? null,
288
+ templatePack,
289
+ productType,
290
+ title,
291
+ owner,
292
+ status: 'synthesized',
293
+ sections,
294
+ reviewPresentation: pickValue(options.reviewPresentation, state.reviewPresentation, null),
295
+ reviewPresentationMeta: pickValue(options.reviewPresentationMeta, state.reviewPresentationMeta, null),
296
+ workUnitId: pickValue(options.workUnitId, state.activeWorkUnitId, null),
297
+ targetRoot: pickValue(options.targetRoot, state.targetRoot, ws.projectRoot),
298
+ };
299
+ }
300
+
301
+ function renderSection(title, fields) {
302
+ return `## ${title}\n\n${fields.map(([label, value]) => renderField(label, value)).join('\n')}\n`;
303
+ }
304
+
305
+ function renderMermaidSection(snapshot) {
306
+ const productFlow = renderDiagramMermaidFromModel(
307
+ 'product-flow',
308
+ buildProductFlowDiagramModel(snapshot)
309
+ );
310
+ const architecture = renderDiagramMermaidFromModel(
311
+ 'architecture',
312
+ buildArchitectureDiagramModel(snapshot)
313
+ );
314
+
315
+ return [
316
+ '## 可视化图表',
317
+ '',
318
+ '### 产品流程',
319
+ '',
320
+ '```mermaid',
321
+ productFlow,
322
+ '```',
323
+ '',
324
+ '### 架构',
325
+ '',
326
+ '```mermaid',
327
+ architecture,
328
+ '```',
329
+ '',
330
+ ].join('\n');
331
+ }
332
+
333
+ export function renderPrdMarkdown(snapshot) {
334
+ const { sections } = snapshot;
335
+ const lines = [
336
+ `# ${snapshot.title}`,
337
+ '',
338
+ ...languagePolicyLines(),
339
+ `- 版本: ${snapshot.versionId}`,
340
+ `- 负责人: ${snapshot.owner}`,
341
+ `- 产品类型: ${snapshot.productType ?? '未分类'}`,
342
+ `- 模板包: ${snapshot.templatePack}`,
343
+ `- 状态: ${snapshot.status}`,
344
+ `- 生成时间: ${snapshot.createdAt}`,
345
+ '',
346
+ renderSection('元信息', [
347
+ ['标题', sections.meta.title],
348
+ ['负责人', sections.meta.owner],
349
+ ['状态', sections.meta.status],
350
+ ['版本', sections.meta.version],
351
+ ['产品类型', sections.meta.productType],
352
+ ['日期', sections.meta.date],
353
+ ]),
354
+ renderSection('问题', [
355
+ ['问题陈述', sections.problem.problemStatement],
356
+ ['为什么是现在', sections.problem.whyNow],
357
+ ['证据', sections.problem.evidence],
358
+ ]),
359
+ renderSection('用户与相关方', [
360
+ ['主要用户', sections.users.primaryUsers],
361
+ ['次要用户', sections.users.secondaryUsers],
362
+ ['相关方', sections.users.stakeholders],
363
+ ]),
364
+ renderSection('目标与成功标准', [
365
+ ['目标', sections.goals.goals],
366
+ ['成功指标', sections.goals.successMetrics],
367
+ ['验收目标', sections.goals.acceptanceGoals],
368
+ ]),
369
+ renderSection('范围与非目标', [
370
+ ['范围内', sections.scope.inScope],
371
+ ['范围外', sections.scope.outOfScope],
372
+ ]),
373
+ renderSection('场景与流程', [
374
+ ['主流程', sections.scenarios.primaryFlows],
375
+ ['边界情况', sections.scenarios.edgeCases],
376
+ ['失败模式', sections.scenarios.failureModes],
377
+ ]),
378
+ renderMermaidSection(snapshot),
379
+ renderSection('需求', [
380
+ ['功能需求', sections.requirements.functional],
381
+ ['非功能需求', sections.requirements.nonFunctional],
382
+ ['业务规则', sections.requirements.businessRules],
383
+ ]),
384
+ renderSection('业务护栏', [
385
+ ['成本来源', sections.businessGuardrails.costDrivers],
386
+ ['额度与限制', sections.businessGuardrails.usageLimits],
387
+ ['滥用防护', sections.businessGuardrails.abusePrevention],
388
+ ['监控信号', sections.businessGuardrails.monitoringSignals],
389
+ ['报警阈值', sections.businessGuardrails.alertThresholds],
390
+ ['止损动作', sections.businessGuardrails.stopLossActions],
391
+ ]),
392
+ renderSection('约束、依赖与风险', [
393
+ ['技术约束', sections.constraints.technical],
394
+ ['合规要求', sections.constraints.compliance],
395
+ ['依赖', sections.constraints.dependencies],
396
+ ['假设', sections.risks.assumptions],
397
+ ['风险', sections.risks.risks],
398
+ ['开放问题', sections.risks.openQuestions],
399
+ ]),
400
+ ];
401
+
402
+ const typeSpecific = sections.typeSpecific;
403
+ const typeSpecificFields = [
404
+ ['类型', typeSpecific.title ?? '类型专项'],
405
+ ...Object.entries(typeSpecific.fields),
406
+ ];
407
+ lines.push(renderSection('类型专项模块', typeSpecificFields));
408
+
409
+ lines.push(renderSection('交接', [
410
+ ['负责人', sections.handoff.owner],
411
+ ['下一步', sections.handoff.nextStep],
412
+ ['目标系统', sections.handoff.targetSystem],
413
+ ]));
414
+
415
+ return `${lines.filter(Boolean).join('\n')}`;
416
+ }
417
+
418
+
419
+ const BASE_REQUIRED_FIELD_DESCRIPTORS = [
420
+ { section: 'meta', path: 'meta.title', label: '标题', prompt: '这份 PRD 应该叫什么?' },
421
+ { section: 'meta', path: 'meta.owner', label: '负责人', prompt: '谁负责这份 PRD?' },
422
+ { section: 'meta', path: 'meta.version', label: '版本', prompt: '这份 PRD 从哪个版本开始?' },
423
+ { section: 'meta', path: 'meta.status', label: '状态', prompt: '当前 PRD 状态是什么?' },
424
+ { section: 'meta', path: 'meta.productType', label: '产品类型', prompt: '这是 consumer、b2b 还是 agent 产品?' },
425
+ { section: 'problem', path: 'problem.problemStatement', label: '问题陈述', prompt: '我们要解决什么问题?' },
426
+ { section: 'problem', path: 'problem.whyNow', label: '为什么是现在', prompt: '为什么现在是解决这个问题的合适时机?' },
427
+ { section: 'problem', path: 'problem.evidence', label: '证据', prompt: '有哪些证据支持这个问题?' },
428
+ { section: 'users', path: 'users.primaryUsers', label: '主要用户', prompt: '主要用户是谁?' },
429
+ { section: 'users', path: 'users.stakeholders', label: '相关方', prompt: '还有谁会参与或受到影响?' },
430
+ { section: 'goals', path: 'goals.goals', label: '目标', prompt: '我们希望达成什么结果?' },
431
+ { section: 'goals', path: 'goals.successMetrics', label: '成功指标', prompt: '如何衡量成功?' },
432
+ { section: 'goals', path: 'goals.acceptanceGoals', label: '验收目标', prompt: '满足什么条件才能认为完成?' },
433
+ { section: 'scope', path: 'scope.inScope', label: '范围内', prompt: '这个版本包含哪些范围?' },
434
+ { section: 'scope', path: 'scope.outOfScope', label: '范围外', prompt: '哪些内容明确不在范围内?' },
435
+ { section: 'scenarios', path: 'scenarios.primaryFlows', label: '主流程', prompt: '主要用户流程是什么?' },
436
+ { section: 'scenarios', path: 'scenarios.edgeCases', label: '边界情况', prompt: '哪些边界情况需要处理?' },
437
+ { section: 'scenarios', path: 'scenarios.failureModes', label: '失败模式', prompt: '需要处理哪些失败模式?' },
438
+ { section: 'requirements', path: 'requirements.functional', label: '功能需求', prompt: '产品必须做什么?' },
439
+ { section: 'requirements', path: 'requirements.nonFunctional', label: '非功能需求', prompt: '有哪些性能、可靠性或安全要求?' },
440
+ { section: 'requirements', path: 'requirements.businessRules', label: '业务规则', prompt: '需要遵守哪些业务规则?' },
441
+ { section: 'constraints', path: 'constraints.technical', label: '技术约束', prompt: '存在哪些技术约束?' },
442
+ { section: 'constraints', path: 'constraints.compliance', label: '合规要求', prompt: '是否存在合规或策略约束?' },
443
+ { section: 'constraints', path: 'constraints.dependencies', label: '依赖', prompt: '这个需求依赖什么?' },
444
+ { section: 'risks', path: 'risks.assumptions', label: '假设', prompt: '我们做了哪些假设?' },
445
+ { section: 'risks', path: 'risks.risks', label: '风险', prompt: '需要关注哪些风险?' },
446
+ { section: 'risks', path: 'risks.openQuestions', label: '开放问题', prompt: '还有哪些问题未解决?' },
447
+ { section: 'handoff', path: 'handoff.owner', label: '交接负责人', prompt: 'PRD freeze 后由谁负责下一步?' },
448
+ { section: 'handoff', path: 'handoff.nextStep', label: '下一步', prompt: 'PRD freeze 后马上做什么?' },
449
+ { section: 'handoff', path: 'handoff.targetSystem', label: '目标系统', prompt: '交接到哪里?' },
450
+ ];
451
+
452
+ const BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS = [
453
+ { section: 'businessGuardrails', path: 'businessGuardrails.costDrivers', label: '成本来源', prompt: '哪些用户行为、第三方服务或模型调用会产生成本?' },
454
+ { section: 'businessGuardrails', path: 'businessGuardrails.usageLimits', label: '额度与限制', prompt: '免费、试用或低权限用户的每日、月度、并发或总量限制是什么?' },
455
+ { section: 'businessGuardrails', path: 'businessGuardrails.abusePrevention', label: '滥用防护', prompt: '如何防止重复请求、并发绕过、越权使用或免费额度被刷?' },
456
+ { section: 'businessGuardrails', path: 'businessGuardrails.monitoringSignals', label: '监控信号', prompt: '需要监控哪些用量、成本、失败率或异常行为信号?' },
457
+ { section: 'businessGuardrails', path: 'businessGuardrails.alertThresholds', label: '报警阈值', prompt: '哪些金额、调用量、错误率或增长速度达到阈值后要报警?' },
458
+ { section: 'businessGuardrails', path: 'businessGuardrails.stopLossActions', label: '止损动作', prompt: '触发异常后,应该降级、暂停、关闭哪些能力,谁来处理?' },
459
+ ];
460
+
461
+ const TYPE_REQUIRED_FIELD_DESCRIPTORS = {
462
+ consumer: [
463
+ { section: 'consumer', path: 'typeSpecific.fields.persona', label: '用户画像', prompt: '目标用户画像是什么?' },
464
+ { section: 'consumer', path: 'typeSpecific.fields.segment', label: '用户分层', prompt: '目标用户分层是什么?' },
465
+ { section: 'consumer', path: 'typeSpecific.fields.journey', label: '用户旅程', prompt: '要优化哪段用户旅程?' },
466
+ { section: 'consumer', path: 'typeSpecific.fields.activationMetric', label: '激活指标', prompt: '哪个激活指标代表早期成功?' },
467
+ { section: 'consumer', path: 'typeSpecific.fields.retentionMetric', label: '留存指标', prompt: '哪个留存指标代表持续价值?' },
468
+ ],
469
+ b2b: [
470
+ { section: 'b2b', path: 'typeSpecific.fields.buyer', label: '采购方', prompt: '谁购买或审批这个产品?' },
471
+ { section: 'b2b', path: 'typeSpecific.fields.user', label: '使用者', prompt: '谁每天使用这个产品?' },
472
+ { section: 'b2b', path: 'typeSpecific.fields.admin', label: '管理员', prompt: '谁配置或管理这个产品?' },
473
+ { section: 'b2b', path: 'typeSpecific.fields.operator', label: '运营者', prompt: '谁端到端运营这个流程?' },
474
+ { section: 'b2b', path: 'typeSpecific.fields.roles', label: '角色', prompt: '流程中的关键角色有哪些?' },
475
+ { section: 'b2b', path: 'typeSpecific.fields.asIs', label: '现状流程', prompt: '当前流程是什么样?' },
476
+ { section: 'b2b', path: 'typeSpecific.fields.toBe', label: '目标流程', prompt: '未来流程应该是什么样?' },
477
+ { section: 'b2b', path: 'typeSpecific.fields.permissionMatrix', label: '权限矩阵', prompt: '需要哪些权限或访问规则?' },
478
+ { section: 'b2b', path: 'typeSpecific.fields.approvalFlow', label: '审批流程', prompt: '需要哪些审批或确认?' },
479
+ ],
480
+ agent: [
481
+ { section: 'agent', path: 'typeSpecific.fields.humanAgentContract', label: 'Human-Agent contract', prompt: '哪些事项必须由人确认,哪些可以由 Agent 自动完成?' },
482
+ { section: 'agent', path: 'typeSpecific.fields.autonomyBoundary', label: '自主边界', prompt: 'Agent 可以自主行动到什么程度?' },
483
+ { section: 'agent', path: 'typeSpecific.fields.toolBoundary', label: '工具边界', prompt: 'Agent 可以使用哪些工具?' },
484
+ { section: 'agent', path: 'typeSpecific.fields.stateModel', label: '状态模型', prompt: 'Agent 需要什么状态或记忆模型?' },
485
+ { section: 'agent', path: 'typeSpecific.fields.evalPlan', label: '评估计划', prompt: '如何评估这个 Agent?' },
486
+ ],
487
+ };
488
+
489
+ function getValueAtPath(root, pathString) {
490
+ if (!root || !pathString) {
491
+ return undefined;
492
+ }
493
+
494
+ return pathString.split('.').reduce((acc, key) => (acc === null || acc === undefined ? undefined : acc[key]), root);
495
+ }
496
+
497
+ function isMissingPrdValue(value) {
498
+ if (value === null || value === undefined) {
499
+ return true;
500
+ }
501
+
502
+ if (typeof value === 'string') {
503
+ const text = value.trim().toLowerCase();
504
+ return text === '' || text === 'tbd' || text === 'todo' || text === 'unknown' || text === 'unclassified';
505
+ }
506
+
507
+ if (Array.isArray(value)) {
508
+ return value.length === 0 || value.every((item) => isMissingPrdValue(item));
509
+ }
510
+
511
+ if (isPlainObject(value)) {
512
+ const entries = Object.values(value);
513
+ return entries.length === 0 || entries.every((item) => isMissingPrdValue(item));
514
+ }
515
+
516
+ return false;
517
+ }
518
+
519
+ export function getRequiredFieldDescriptors(productType) {
520
+ const descriptors = [...BASE_REQUIRED_FIELD_DESCRIPTORS];
521
+ if (TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]) {
522
+ descriptors.push(...TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]);
523
+ }
524
+ return descriptors;
525
+ }
526
+
527
+ export function needsBusinessGuardrails(snapshot) {
528
+ const sections = snapshot?.sections ?? {};
529
+ if (!isMissingPrdValue(sections.businessGuardrails)) {
530
+ return true;
531
+ }
532
+ const riskSource = {
533
+ problem: sections.problem,
534
+ users: sections.users,
535
+ goals: sections.goals,
536
+ scope: {
537
+ inScope: sections.scope?.inScope,
538
+ outOfScope: sections.scope?.outOfScope,
539
+ },
540
+ scenarios: sections.scenarios,
541
+ requirements: {
542
+ functional: sections.requirements?.functional,
543
+ businessRules: sections.requirements?.businessRules,
544
+ },
545
+ risks: sections.risks,
546
+ };
547
+ return includesRiskToken(flattenForSearch(riskSource));
548
+ }
549
+
550
+ export function analyzePrdSnapshot(snapshot) {
551
+ const productType = snapshot.productType ?? null;
552
+ const descriptors = getRequiredFieldDescriptors(productType);
553
+ if (needsBusinessGuardrails(snapshot)) {
554
+ descriptors.push(...BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS);
555
+ }
556
+ const missingFields = [];
557
+ const completeFields = [];
558
+
559
+ for (const descriptor of descriptors) {
560
+ const value = getValueAtPath(snapshot.sections, descriptor.path);
561
+ const missing = isMissingPrdValue(value);
562
+ const entry = {
563
+ ...descriptor,
564
+ value,
565
+ missing,
566
+ };
567
+ if (missing) {
568
+ missingFields.push(entry);
569
+ } else {
570
+ completeFields.push(entry);
571
+ }
572
+ }
573
+
574
+ const totalRequiredFields = descriptors.length;
575
+ const completedRequiredFields = completeFields.length;
576
+ const completionRatio = totalRequiredFields === 0 ? 1 : completedRequiredFields / totalRequiredFields;
577
+
578
+ return {
579
+ productType,
580
+ totalRequiredFields,
581
+ completedRequiredFields,
582
+ missingRequiredFields: missingFields.length,
583
+ completionRatio,
584
+ missingFields,
585
+ completeFields,
586
+ missingSections: [...new Set(missingFields.map((field) => field.section))],
587
+ suggestedQuestions: missingFields.slice(0, 5).map((field) => field.prompt),
588
+ };
589
+ }
590
+
591
+ function diffValues(before, after, prefix = '') {
592
+ if (Object.is(before, after)) {
593
+ return [];
594
+ }
595
+
596
+ const beforeJson = JSON.stringify(before);
597
+ const afterJson = JSON.stringify(after);
598
+ if (beforeJson === afterJson) {
599
+ return [];
600
+ }
601
+
602
+ const beforeIsObject = isPlainObject(before);
603
+ const afterIsObject = isPlainObject(after);
604
+
605
+ if (!beforeIsObject || !afterIsObject) {
606
+ return [{ path: prefix, before, after }];
607
+ }
608
+
609
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
610
+ const changes = [];
611
+
612
+ for (const key of keys) {
613
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
614
+ if (!(key in before)) {
615
+ changes.push({ path: nextPrefix, before: undefined, after: after[key] });
616
+ continue;
617
+ }
618
+ if (!(key in after)) {
619
+ changes.push({ path: nextPrefix, before: before[key], after: undefined });
620
+ continue;
621
+ }
622
+ changes.push(...diffValues(before[key], after[key], nextPrefix));
623
+ }
624
+
625
+ return changes;
626
+ }
627
+
628
+ export function diffSnapshots(beforeSnapshot, afterSnapshot) {
629
+ const changes = diffValues(beforeSnapshot.sections, afterSnapshot.sections);
630
+ const changedSections = [...new Set(changes.map((change) => change.path.split('.')[0]).filter(Boolean))];
631
+
632
+ return {
633
+ fromVersionId: beforeSnapshot.versionId,
634
+ toVersionId: afterSnapshot.versionId,
635
+ fromVersionNumber: beforeSnapshot.versionNumber,
636
+ toVersionNumber: afterSnapshot.versionNumber,
637
+ changedSections,
638
+ changes,
639
+ };
640
+ }
641
+
642
+ export function summarizeSnapshot(snapshot) {
643
+ return {
644
+ versionNumber: snapshot.versionNumber,
645
+ versionId: snapshot.versionId,
646
+ createdAt: snapshot.createdAt,
647
+ title: snapshot.title,
648
+ owner: snapshot.owner,
649
+ productType: snapshot.productType,
650
+ templatePack: snapshot.templatePack,
651
+ status: snapshot.status,
652
+ digest: snapshot.digest ?? null,
653
+ workUnitId: snapshot.workUnitId ?? null,
654
+ targetRoot: snapshot.targetRoot ?? null,
655
+ };
656
+ }