@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,1797 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { analyzePrdSnapshot, buildPrdSnapshot, diffSnapshots, formatVersionId, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
5
+ import { getDiagramReviewState } from './diagram-workspace.js';
6
+ import { exists, parseYamlText, readJson, readText, writeJson, writeText } from './fs-utils.js';
7
+ import { artifactBundlePaths, canonicalReviewPath, defaultReviewArtifactPath, openArtifactInBrowser, renderPlaygroundArtifact, renderPlaygroundMarkdown, renderPlaygroundPatch, renderReviewArtifact, renderReviewEntryHtml, writeHtmlArtifact } from './html-artifacts.js';
8
+ import { findOpenPrdSpecLanguageViolations } from './language-policy.js';
9
+ import { buildSpec as buildOpenSpecSpec } from './openspec/generate.js';
10
+ import { assertReviewPresentationReady, getReviewPresentationGate } from './review-presentation.js';
11
+ import { syncSessionBindingFromReview, syncSessionBindingFromSnapshot } from './session-binding.js';
12
+ import { timestamp } from './time.js';
13
+ import { generateWorkUnitId, normalizeWorkUnitId, readWorkUnitBinding, resolveTargetRoot, writeWorkUnitBinding } from './work-unit.js';
14
+ import { appendDecision, appendOpenQuestions, appendProgress, appendWorkflowEvent, buildClarificationPlan, buildClarificationState, buildWorkflowTaskGraph, CAPTURE_SOURCES, coerceCapturedValue, deriveGateLabels, detectWorkspaceScenario, extractMarkdownSection, FIELD_PATH_TO_STATE_KEY, isSupportedProductType, loadLatestVersionSnapshot, loadWorkspace, normalizeVersionId, readVersionIndex, readVersionSnapshot, renderFlowDoc, renderHandoffDoc, renderRolesDoc, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
15
+
16
+ function requirementGatePath(projectRoot) {
17
+ return path.join(projectRoot, '.openprd', 'harness', 'requirement-gate.json');
18
+ }
19
+
20
+ const PRD_REVIEW_STATUSES = ['pending-confirmation', 'confirmed', 'needs-revision'];
21
+ const CURRENT_SNAPSHOT_CACHE_KEYS = [
22
+ 'versionId',
23
+ 'versionNumber',
24
+ 'workUnitId',
25
+ 'sections',
26
+ 'content',
27
+ 'digest',
28
+ 'reviewPresentationMeta',
29
+ ];
30
+ const REVIEW_PRESENTATION_RELEVANT_FIELD_PREFIXES = [
31
+ 'problem.',
32
+ 'users.',
33
+ 'goals.',
34
+ 'scope.',
35
+ 'scenarios.',
36
+ 'requirements.',
37
+ 'businessGuardrails.',
38
+ 'constraints.',
39
+ 'risks.',
40
+ 'typeSpecific.',
41
+ ];
42
+ const REVIEW_PRESENTATION_RELEVANT_FIELDS = new Set([
43
+ 'meta.title',
44
+ 'meta.productType',
45
+ ]);
46
+ const NON_SEMANTIC_CAPTURE_SOURCES = new Set(['agent-normalized']);
47
+ const REVIEW_SAFE_CAPTURE_FIELDS = new Set([
48
+ 'meta.status',
49
+ 'reviewPresentation',
50
+ ]);
51
+ const REVIEW_PRESENTATION_RELEVANT_OVERRIDE_KEYS = new Set([
52
+ 'title',
53
+ 'problemStatement',
54
+ 'whyNow',
55
+ 'evidence',
56
+ 'primaryUsers',
57
+ 'secondaryUsers',
58
+ 'stakeholders',
59
+ 'goals',
60
+ 'successMetrics',
61
+ 'acceptanceGoals',
62
+ 'inScope',
63
+ 'outOfScope',
64
+ 'primaryFlows',
65
+ 'edgeCases',
66
+ 'failureModes',
67
+ 'functional',
68
+ 'nonFunctional',
69
+ 'businessRules',
70
+ 'costDrivers',
71
+ 'usageLimits',
72
+ 'abusePrevention',
73
+ 'monitoringSignals',
74
+ 'alertThresholds',
75
+ 'stopLossActions',
76
+ 'technical',
77
+ 'compliance',
78
+ 'dependencies',
79
+ 'assumptions',
80
+ 'risks',
81
+ 'openQuestions',
82
+ 'persona',
83
+ 'segment',
84
+ 'journey',
85
+ 'activationMetric',
86
+ 'retentionMetric',
87
+ 'buyer',
88
+ 'user',
89
+ 'admin',
90
+ 'operator',
91
+ 'roles',
92
+ 'asIs',
93
+ 'toBe',
94
+ 'permissionMatrix',
95
+ 'approvalFlow',
96
+ 'humanAgentContract',
97
+ 'autonomyBoundary',
98
+ 'toolBoundary',
99
+ 'stateModel',
100
+ 'evalPlan',
101
+ ]);
102
+ const SYNTHESIZE_CONTENT_OVERRIDE_KEYS = new Set([
103
+ 'title',
104
+ 'owner',
105
+ 'productType',
106
+ 'problemStatement',
107
+ 'whyNow',
108
+ 'evidence',
109
+ 'primaryUsers',
110
+ 'secondaryUsers',
111
+ 'stakeholders',
112
+ 'goals',
113
+ 'successMetrics',
114
+ 'acceptanceGoals',
115
+ 'inScope',
116
+ 'outOfScope',
117
+ 'primaryFlows',
118
+ 'edgeCases',
119
+ 'failureModes',
120
+ 'functional',
121
+ 'nonFunctional',
122
+ 'businessRules',
123
+ 'costDrivers',
124
+ 'usageLimits',
125
+ 'abusePrevention',
126
+ 'monitoringSignals',
127
+ 'alertThresholds',
128
+ 'stopLossActions',
129
+ 'technical',
130
+ 'compliance',
131
+ 'dependencies',
132
+ 'assumptions',
133
+ 'risks',
134
+ 'openQuestions',
135
+ 'handoffOwner',
136
+ 'nextStep',
137
+ 'targetSystem',
138
+ 'reviewPresentation',
139
+ 'persona',
140
+ 'segment',
141
+ 'journey',
142
+ 'activationMetric',
143
+ 'retentionMetric',
144
+ 'buyer',
145
+ 'user',
146
+ 'admin',
147
+ 'operator',
148
+ 'roles',
149
+ 'asIs',
150
+ 'toBe',
151
+ 'permissionMatrix',
152
+ 'approvalFlow',
153
+ 'humanAgentContract',
154
+ 'autonomyBoundary',
155
+ 'toolBoundary',
156
+ 'stateModel',
157
+ 'evalPlan',
158
+ ]);
159
+
160
+ function normalizePrdReviewStatus(status) {
161
+ return PRD_REVIEW_STATUSES.includes(status) ? status : 'pending-confirmation';
162
+ }
163
+
164
+ async function readActiveRequirementGate(projectRoot) {
165
+ const gate = await readJson(requirementGatePath(projectRoot)).catch(() => null);
166
+ return gate?.active ? gate : null;
167
+ }
168
+
169
+ function meaningfulOverrideValue(value) {
170
+ if (value === null || value === undefined) return false;
171
+ if (typeof value === 'string') return value.trim().length > 0;
172
+ if (Array.isArray(value)) return value.length > 0;
173
+ if (typeof value === 'object') return Object.keys(value).length > 0;
174
+ return value !== false;
175
+ }
176
+
177
+ function hasSynthesizeContentOverrides(overrides) {
178
+ return Object.entries(overrides).some(([key, value]) => (
179
+ SYNTHESIZE_CONTENT_OVERRIDE_KEYS.has(key) && meaningfulOverrideValue(value)
180
+ ));
181
+ }
182
+
183
+ function latestCaptureTimestamp(currentState) {
184
+ const timestamps = [
185
+ currentState?.lastCapturedAt,
186
+ ...Object.values(currentState?.captureMeta ?? {}).map((entry) => entry?.capturedAt),
187
+ ].filter(Boolean).map(String);
188
+ return timestamps.length > 0 ? timestamps.sort().at(-1) : null;
189
+ }
190
+
191
+ function requirementGateReferenceTimestamp(gate) {
192
+ return gate?.confirmedAt ?? gate?.updatedAt ?? gate?.openedAt ?? null;
193
+ }
194
+
195
+ function gateQuestionLimit(gate, fallback) {
196
+ const raw = Number(gate?.approvalPolicy?.maxClarificationQuestions);
197
+ if (!Number.isFinite(raw) || raw <= 0) {
198
+ return fallback;
199
+ }
200
+ return Math.min(fallback, Math.max(1, Math.floor(raw)));
201
+ }
202
+
203
+ function ensureFreshRequirementStateForSynthesize({ gate, currentState, overrides }) {
204
+ if (!gate) {
205
+ return;
206
+ }
207
+ const gateAt = requirementGateReferenceTimestamp(gate);
208
+ if (!gateAt) {
209
+ return;
210
+ }
211
+ const capturedAt = latestCaptureTimestamp(currentState);
212
+ if (capturedAt && String(capturedAt) >= String(gateAt)) {
213
+ return;
214
+ }
215
+ throw new Error([
216
+ 'OpenPrd 已阻止 synthesize:当前有新的需求入口,但 current.json 还没有记录本轮确认答案。',
217
+ hasSynthesizeContentOverrides(overrides)
218
+ ? '当前 requirement gate 处于进行中,partial override 不能替代 fresh capture;请先用 openprd capture 写入本轮目标、问题、范围和验收信息。'
219
+ : '请先用 openprd capture 写入本轮目标、问题、范围和验收信息。',
220
+ ].join(' '));
221
+ }
222
+
223
+ function resolveReviewPaths(ws, snapshot) {
224
+ const canonicalReview = canonicalReviewPath(ws, snapshot.versionId);
225
+ const activeReviewEntry = defaultReviewArtifactPath(ws);
226
+ return {
227
+ canonicalReview,
228
+ activeReviewEntry,
229
+ };
230
+ }
231
+
232
+ async function writeReviewFiles(ws, snapshot, { writeEntry = true } = {}) {
233
+ assertReviewPresentationReady(snapshot);
234
+ const reviewHtml = renderReviewArtifact({ snapshot });
235
+ const { canonicalReview, activeReviewEntry } = resolveReviewPaths(ws, snapshot);
236
+ await writeHtmlArtifact(canonicalReview, reviewHtml);
237
+ if (writeEntry) {
238
+ await writeHtmlArtifact(activeReviewEntry, renderReviewEntryHtml({
239
+ entryPath: activeReviewEntry,
240
+ reviewPath: canonicalReview,
241
+ title: `${snapshot.title} / 评审入口`,
242
+ }));
243
+ }
244
+ return {
245
+ canonicalReview,
246
+ activeReviewEntry: writeEntry ? activeReviewEntry : null,
247
+ };
248
+ }
249
+
250
+ async function removeReviewFiles(reviewFiles) {
251
+ await Promise.all([
252
+ reviewFiles.canonicalReview ? fs.rm(reviewFiles.canonicalReview, { force: true }) : null,
253
+ reviewFiles.activeReviewEntry ? fs.rm(reviewFiles.activeReviewEntry, { force: true }) : null,
254
+ ].filter(Boolean));
255
+ }
256
+
257
+ function shouldUseCurrentDraftForGuidance(currentState) {
258
+ return Boolean(
259
+ currentState?.reviewStatus?.stale
260
+ || (currentState?.lastCapturedAt && !['synthesized', 'frozen', 'handed_off'].includes(currentState?.status))
261
+ );
262
+ }
263
+
264
+ function clearCurrentSnapshotCache(currentState) {
265
+ for (const key of CURRENT_SNAPSHOT_CACHE_KEYS) {
266
+ delete currentState[key];
267
+ }
268
+ return currentState;
269
+ }
270
+
271
+ function isReviewPresentationRelevantField(field) {
272
+ if (!field) return false;
273
+ return REVIEW_PRESENTATION_RELEVANT_FIELDS.has(field)
274
+ || REVIEW_PRESENTATION_RELEVANT_FIELD_PREFIXES.some((prefix) => field.startsWith(prefix));
275
+ }
276
+
277
+ function shouldDropInheritedReviewPresentationFromCapture(applied) {
278
+ const fields = applied.map((item) => item.field).filter(Boolean);
279
+ if (fields.includes('reviewPresentation')) {
280
+ return false;
281
+ }
282
+ return fields.some((field) => isReviewPresentationRelevantField(field));
283
+ }
284
+
285
+ function shouldDropInheritedReviewPresentationFromOverrides(overrides) {
286
+ if (Object.prototype.hasOwnProperty.call(overrides, 'reviewPresentation')) {
287
+ return false;
288
+ }
289
+ return Object.keys(overrides).some((key) => REVIEW_PRESENTATION_RELEVANT_OVERRIDE_KEYS.has(key));
290
+ }
291
+
292
+ function dropInheritedReviewPresentation(currentState) {
293
+ delete currentState.reviewPresentation;
294
+ delete currentState.reviewPresentationMeta;
295
+ if (currentState.captureMeta && typeof currentState.captureMeta === 'object' && !Array.isArray(currentState.captureMeta)) {
296
+ delete currentState.captureMeta.reviewPresentation;
297
+ delete currentState.captureMeta.reviewPresentationMeta;
298
+ }
299
+ return currentState;
300
+ }
301
+
302
+ function syncCurrentSnapshotCache(currentState, snapshot) {
303
+ clearCurrentSnapshotCache(currentState);
304
+ currentState.versionId = snapshot.versionId;
305
+ currentState.versionNumber = snapshot.versionNumber;
306
+ currentState.workUnitId = snapshot.workUnitId ?? null;
307
+ currentState.sections = snapshot.sections;
308
+ currentState.content = snapshot.content;
309
+ currentState.digest = snapshot.digest;
310
+ currentState.reviewPresentationMeta = snapshot.reviewPresentationMeta ?? null;
311
+ return currentState;
312
+ }
313
+
314
+ function markReviewStateStaleAfterCapture(currentState, applied, capturedAt) {
315
+ const dropInheritedPresentation = shouldDropInheritedReviewPresentationFromCapture(applied);
316
+ if (dropInheritedPresentation) {
317
+ dropInheritedReviewPresentation(currentState);
318
+ }
319
+ const staleFields = applied
320
+ .filter((item) => item.field && !REVIEW_SAFE_CAPTURE_FIELDS.has(item.field) && !NON_SEMANTIC_CAPTURE_SOURCES.has(item.source))
321
+ .map((item) => item.field);
322
+ const previousReview = currentState.reviewStatus ?? null;
323
+ const staleVersionId = currentState.latestVersionId ?? currentState.versionId ?? previousReview?.versionId ?? null;
324
+ if (staleFields.length === 0) {
325
+ return false;
326
+ }
327
+ const staleWorkUnitId = currentState.activeWorkUnitId ?? currentState.workUnitId ?? previousReview?.workUnitId ?? null;
328
+ currentState.previousLatestVersionId = staleVersionId;
329
+ currentState.previousLatestVersionDigest = currentState.latestVersionDigest ?? currentState.digest ?? null;
330
+ currentState.previousActiveWorkUnitId = staleWorkUnitId;
331
+ delete currentState.latestVersionId;
332
+ delete currentState.latestVersionDigest;
333
+ delete currentState.activeWorkUnitId;
334
+ clearCurrentSnapshotCache(currentState);
335
+ currentState.status = 'clarifying';
336
+ if (!staleVersionId) {
337
+ return false;
338
+ }
339
+ currentState.reviewStatus = {
340
+ versionId: null,
341
+ workUnitId: null,
342
+ status: 'needs-revision',
343
+ stale: true,
344
+ staleReason: 'captured-fields-updated',
345
+ staleFields,
346
+ staleVersionId,
347
+ staleVersionDigest: currentState.previousLatestVersionDigest,
348
+ staleWorkUnitId,
349
+ staleArtifact: previousReview?.reviewPath ?? previousReview?.stableArtifact ?? previousReview?.artifact ?? null,
350
+ updatedAt: capturedAt,
351
+ };
352
+ return true;
353
+ }
354
+
355
+ function requirementLooksLikeInterfaceWork(gate) {
356
+ const text = `${gate?.promptPreview ?? ''} ${JSON.stringify(gate?.intent ?? {})}`;
357
+ return /界面|页面|菜单|入口|按钮|表单|弹窗|导航|布局|看板|列表|配置页|模块|组件|UI|tab/i.test(text);
358
+ }
359
+
360
+ function assertOpenSpecPreflightReady(snapshot) {
361
+ const specText = buildOpenSpecSpec({ snapshot });
362
+ const violations = findOpenPrdSpecLanguageViolations(specText);
363
+ if (violations.length === 0) {
364
+ return;
365
+ }
366
+ const examples = violations
367
+ .slice(0, 3)
368
+ .map((violation) => `第 ${violation.line} 行:${violation.reason}(${violation.text})`)
369
+ .join(';');
370
+ throw new Error([
371
+ 'OpenPrd 已阻止 synthesize:按当前 PRD 生成的 spec.md 仍会触发简体中文预检,review.html 还不能进入确认。',
372
+ '请先把标题、问题陈述和场景文案整理成可直接产出 spec 的简体中文表达。',
373
+ '如果只是内部措辞规范化,请先用 openprd capture . --source agent-normalized 写回,再重新 synthesize。',
374
+ examples ? `示例:${examples}。` : null,
375
+ ].filter(Boolean).join(' '));
376
+ }
377
+
378
+ function requirementPrompt(gate) {
379
+ return String(gate?.promptPreview ?? '').trim();
380
+ }
381
+
382
+ function includesAny(text, patterns) {
383
+ return patterns.some((pattern) => pattern.test(text));
384
+ }
385
+
386
+ function detectRequirementIntakeComplexity(gate) {
387
+ const text = requirementPrompt(gate);
388
+ const complexPatterns = [
389
+ /新增|新建|增加/,
390
+ /模块|流程|编排|一站式|信息架构|工作流|workflow|wizard/i,
391
+ /多角色|权限|审批|协作|团队|客户|后台|管理/,
392
+ /AI|agent|模型|生成|自动化|集成|第三方/i,
393
+ /免费|额度|计费|成本|滥用|安全|合规/,
394
+ ];
395
+ const vaguePatterns = [
396
+ /体验|优化|提升|更好|智能|自动|高效|完整|体系|平台/,
397
+ /我希望|用户反馈|考虑不全|模糊|大概|可能/,
398
+ ];
399
+ const simpleConcretePatterns = [
400
+ /按钮|文案|颜色|圆角|位置|间距|字号|图标|标题|空格|标点|错别字|拼写|label|copy/i,
401
+ /红圈|描边|边框|卡片|平铺|去掉|去除|移除|隐藏|对齐|留白|背景/,
402
+ /从.+(改到|移到|移动到|换到|变成|改成|改为).+/,
403
+ ];
404
+ const reasons = [];
405
+ if (text.length >= 80) {
406
+ reasons.push('输入较长,包含多个意图或约束');
407
+ }
408
+ if (includesAny(text, complexPatterns)) {
409
+ reasons.push('涉及新能力、模块、流程、权限、成本或集成');
410
+ }
411
+ if (includesAny(text, vaguePatterns)) {
412
+ reasons.push('表达仍偏目标或体验,需要先收敛用户场景');
413
+ }
414
+
415
+ const simpleConcrete = text.length <= 80
416
+ && includesAny(text, simpleConcretePatterns)
417
+ && !includesAny(text, [/新增|新建|模块|流程|编排|一站式|权限|审批|agent|AI/i]);
418
+
419
+ if (simpleConcrete) {
420
+ return {
421
+ mode: 'focused',
422
+ label: '轻量项目映射',
423
+ minimumDepth: 1,
424
+ questionLimit: 3,
425
+ reasons: ['输入看起来是明确的局部调整,只需要确认影响位置和验收方式'],
426
+ };
427
+ }
428
+
429
+ if (reasons.length > 0) {
430
+ return {
431
+ mode: 'deep',
432
+ label: '三轮需求自省',
433
+ minimumDepth: 3,
434
+ questionLimit: 6,
435
+ reasons,
436
+ };
437
+ }
438
+
439
+ return {
440
+ mode: 'focused',
441
+ label: '轻量需求自省',
442
+ minimumDepth: 1,
443
+ questionLimit: 4,
444
+ reasons: ['需求目标相对聚焦,但仍需要结合当前项目确认范围和验收方式'],
445
+ };
446
+ }
447
+
448
+ function shortList(items, fallback = '待补充') {
449
+ const list = (Array.isArray(items) ? items : [items]).map((item) => String(item || '').trim()).filter(Boolean);
450
+ return list.length > 0 ? list.slice(0, 3).join(';') : fallback;
451
+ }
452
+
453
+ function normalizeClarifyMode(mode) {
454
+ if (mode === 'artifact') {
455
+ return 'inline-with-checklist';
456
+ }
457
+ return ['auto', 'inline', 'inline-with-checklist'].includes(mode) ? mode : 'auto';
458
+ }
459
+
460
+ function estimateInlineClarificationLines(clarification, reflection) {
461
+ const activeChangeLines = reflection?.projectContext?.activeChange ? 1 : 0;
462
+ return 4
463
+ + clarification.mustAskUser.length
464
+ + Math.min(clarification.canInferLater.length, 2)
465
+ + activeChangeLines;
466
+ }
467
+
468
+ function isLightweightClarifyQuestion(item) {
469
+ const id = String(item?.id ?? '');
470
+ return /^(meta|users|goals|scope|scenarios|requirements)\./.test(id);
471
+ }
472
+
473
+ function chooseClarifyPresentation({ requirementGate, clarification, reflection, requestedMode = 'auto' }) {
474
+ const normalizedMode = normalizeClarifyMode(requestedMode);
475
+ const estimatedLineCount = estimateInlineClarificationLines(clarification, reflection);
476
+ const questionCount = clarification.mustAskUser.length;
477
+ const substantialQuestionCount = clarification.mustAskUser.filter((item) => !isLightweightClarifyQuestion(item)).length;
478
+ const defaultMode = !requirementGate?.active || substantialQuestionCount > 2 || questionCount > 2 || reflection?.mode === 'deep' || estimatedLineCount > 8
479
+ ? 'inline-with-checklist'
480
+ : 'inline';
481
+ const mode = normalizedMode === 'auto' ? defaultMode : normalizedMode;
482
+ const reason = mode === 'inline-with-checklist'
483
+ ? '澄清阶段只在对话内呈现,当前需求用摘要和简短清单确认;正式 HTML 评审留给后续 review。'
484
+ : '当前需求可以用十句话以内讲清楚,直接在对话内确认,降低用户跳转成本。';
485
+ return {
486
+ mode,
487
+ label: mode === 'inline-with-checklist' ? '对话内澄清 + 简短清单' : '对话内澄清',
488
+ estimatedLineCount,
489
+ questionCount,
490
+ substantialQuestionCount,
491
+ reason,
492
+ };
493
+ }
494
+
495
+ function buildInlineClarification({ clarification, reflection, presentation }) {
496
+ if (!presentation?.mode?.startsWith('inline')) {
497
+ return null;
498
+ }
499
+ const prompt = reflection?.promptPreview || '本轮需求';
500
+ const projectContext = reflection?.projectContext ?? {};
501
+ const lines = [
502
+ `我理解的目标:${prompt}`,
503
+ `落点:${projectContext.productName ?? '当前项目'};按${projectContext.scenario ?? '当前工作区'}处理。`,
504
+ '范围边界:只处理本轮需求,不自动合并历史 active change 或未提到的扩展。',
505
+ '验收方式:确认用户能看到或完成的结果,以及哪些既有行为不能被改变。',
506
+ ];
507
+ if (projectContext.activeChange) {
508
+ lines.push(`历史提醒:当前还有 ${projectContext.activeChange.activeChange},本轮先分开处理。`);
509
+ }
510
+ const questions = clarification.mustAskUser.slice(0, presentation.mode === 'inline' ? 3 : 5);
511
+ if (questions.length > 0) {
512
+ lines.push('建议确认:');
513
+ for (const item of questions) {
514
+ lines.push(`- ${item.prompt}`);
515
+ }
516
+ } else {
517
+ lines.push('建议确认:如果以上理解正确,用户回复“可以”或“确认执行”后再继续。');
518
+ }
519
+ return {
520
+ mode: presentation.mode,
521
+ title: presentation.label,
522
+ estimatedLineCount: presentation.estimatedLineCount,
523
+ lines,
524
+ };
525
+ }
526
+
527
+ async function readActiveChangeHint(projectRoot) {
528
+ const state = await readJson(path.join(projectRoot, '.openprd', 'state', 'changes.json')).catch(() => null);
529
+ const activeChange = state?.activeChange ?? null;
530
+ if (!activeChange) {
531
+ return null;
532
+ }
533
+ return {
534
+ activeChange,
535
+ status: state?.changes?.[activeChange]?.status ?? 'active',
536
+ };
537
+ }
538
+
539
+ function reflectionQuestion(id, label, prompt) {
540
+ return {
541
+ id: `requirement-intake.${id}`,
542
+ title: label,
543
+ label,
544
+ question: prompt,
545
+ prompt,
546
+ reason: 'requirement-intake-reflection',
547
+ };
548
+ }
549
+
550
+ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, analysis, scenario, gate }) {
551
+ if (!gate?.active) {
552
+ return null;
553
+ }
554
+
555
+ const text = requirementPrompt(gate);
556
+ const complexity = detectRequirementIntakeComplexity(gate);
557
+ const activeChange = await readActiveChangeHint(projectRoot);
558
+ const sections = snapshot.sections ?? {};
559
+ const productName = snapshot.title || sections.meta?.title || '当前项目';
560
+ const productType = snapshot.productType ?? resolveCurrentProductType(ws) ?? '未分类';
561
+ const currentProblem = sections.problem?.problemStatement || '待补充';
562
+ const currentScope = shortList(sections.scope?.inScope, '当前范围还没有稳定记录');
563
+ const missing = analysis.missingFields.slice(0, 4).map((field) => field.label);
564
+ const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
565
+ const mustConfirm = complexity.mode === 'deep'
566
+ ? [
567
+ reflectionQuestion('intent', '意图与目标', '请确认我对需求目标的理解:目标用户是谁、在哪个场景下,需要完成什么结果?'),
568
+ reflectionQuestion('project-context', '项目影响范围', '结合当前项目,哪些已有模块、入口、流程或历史需求必须复用,哪些可以调整?'),
569
+ reflectionQuestion('scope-quality', '范围与验收', '这个需求的范围内、范围外、成功标准和失败路径分别是什么?'),
570
+ needsInterfaceSketch
571
+ ? reflectionQuestion('interface-sketch', '界面或流程草图', '需求涉及界面或流程,请先确认主要区域、操作入口、预览/确认点和风险提示。')
572
+ : reflectionQuestion('details-boundary', '细节与边界', '请确认关键字段、状态变化、数据来源、权限边界和可验收细节。'),
573
+ ]
574
+ : [
575
+ reflectionQuestion('project-context', '项目映射', '请确认这个调整具体落在哪个页面、模块、入口或流程,以及哪些已有行为不能被改变。'),
576
+ reflectionQuestion('acceptance', '验收方式', '请确认完成后用户能看到或做到什么,以及最小验收标准是什么。'),
577
+ ];
578
+
579
+ return {
580
+ version: 1,
581
+ active: true,
582
+ mode: complexity.mode,
583
+ label: complexity.label,
584
+ minimumDepth: complexity.minimumDepth,
585
+ questionLimit: gateQuestionLimit(gate, complexity.questionLimit),
586
+ promptPreview: text,
587
+ reasons: complexity.reasons,
588
+ needsInterfaceSketch,
589
+ projectContext: {
590
+ scenario: scenario.label,
591
+ scenarioReason: scenario.reason,
592
+ productName,
593
+ productType,
594
+ currentProblem,
595
+ currentScope,
596
+ activeChange,
597
+ missingFields: missing,
598
+ },
599
+ rounds: [
600
+ {
601
+ id: 'intent-normalization',
602
+ title: '第 1 轮:意图归一化',
603
+ findings: [
604
+ `用户原始输入:${text || '待补充'}`,
605
+ `初步判断:${complexity.label}`,
606
+ `需要先把表达收敛成用户、场景、目标、动作和期望结果。`,
607
+ ],
608
+ },
609
+ {
610
+ id: 'project-context',
611
+ title: '第 2 轮:项目上下文映射',
612
+ findings: [
613
+ `工作区场景:${scenario.label},${scenario.reason}`,
614
+ `当前产品:${productName}(${productType}),已记录问题:${currentProblem}`,
615
+ `当前范围线索:${currentScope}`,
616
+ activeChange ? `仍有 active change:${activeChange.activeChange}(${activeChange.status}),需要和本轮需求分开评估。` : '当前没有检测到 active change 冲突。',
617
+ ],
618
+ },
619
+ {
620
+ id: 'product-quality',
621
+ title: '第 3 轮:产品质量自检',
622
+ findings: [
623
+ `仍需确认的信息:${shortList(missing, '暂无明显缺口')}`,
624
+ needsInterfaceSketch ? '需求看起来涉及界面或流程,需要先给用户确认草图或关键操作路径。' : '需求暂未明显命中界面,但仍要确认状态、边界和验收方式。',
625
+ '进入实现前必须保留范围、非目标、异常路径和验收证据。',
626
+ ],
627
+ },
628
+ ],
629
+ mustConfirm,
630
+ };
631
+ }
632
+
633
+ function renderRequirementIntakeReflection(reflection) {
634
+ if (!reflection?.active) {
635
+ return '# 需求入口自省\n\n- 当前没有 active requirement intake。\n';
636
+ }
637
+ const lines = [
638
+ '# 需求入口自省',
639
+ '',
640
+ `- 模式: ${reflection.label}`,
641
+ `- 用户输入: ${reflection.promptPreview || '待补充'}`,
642
+ `- 复杂度依据: ${shortList(reflection.reasons, '未命中复杂度提示')}`,
643
+ '',
644
+ '## 项目上下文',
645
+ '',
646
+ `- 工作区场景: ${reflection.projectContext.scenario}`,
647
+ `- 当前产品: ${reflection.projectContext.productName} (${reflection.projectContext.productType})`,
648
+ `- 当前问题: ${reflection.projectContext.currentProblem}`,
649
+ `- 当前范围: ${reflection.projectContext.currentScope}`,
650
+ reflection.projectContext.activeChange ? `- 历史 active change: ${reflection.projectContext.activeChange.activeChange}` : '- 历史 active change: 无',
651
+ '',
652
+ ];
653
+ for (const round of reflection.rounds) {
654
+ lines.push(`## ${round.title}`, '');
655
+ for (const finding of round.findings) {
656
+ lines.push(`- ${finding}`);
657
+ }
658
+ lines.push('');
659
+ }
660
+ lines.push('## 必须确认的问题', '');
661
+ for (const question of reflection.mustConfirm) {
662
+ lines.push(`- ${question.label}: ${question.prompt}`);
663
+ }
664
+ lines.push('');
665
+ return lines.join('\n');
666
+ }
667
+
668
+ async function writeRequirementIntakeReflection(ws, reflection) {
669
+ if (!reflection?.active) {
670
+ return null;
671
+ }
672
+ const reflectionPath = path.join(ws.workspaceRoot, 'engagements', 'active', 'intake-reflection.md');
673
+ await writeText(reflectionPath, renderRequirementIntakeReflection(reflection));
674
+ return reflectionPath;
675
+ }
676
+
677
+ function buildRequirementIntakeDepth(gate, reflection = null) {
678
+ const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
679
+ const fallbackLayers = [
680
+ reflectionQuestion('product-context', '用户 / 场景 / 问题', '先确认:什么用户,在什么场景下,遇到什么问题?为什么现在值得解决?'),
681
+ reflectionQuestion('product-outcome', '目标 / 影响 / 成功标准', '解决后用户能完成什么?减少什么成本或风险?用什么成功指标或验收标准判断有效?'),
682
+ reflectionQuestion('product-flow', '现状流程 / 目标流程 / 异常路径', '请拆出当前流程、目标流程、关键决策点、失败路径,以及哪些动作必须由用户确认。'),
683
+ reflectionQuestion(
684
+ 'product-detail',
685
+ needsInterfaceSketch ? '界面草图 / 字段 / 状态' : '细节 / 状态 / 边界',
686
+ needsInterfaceSketch
687
+ ? '这个需求涉及界面,请先给用户一版 ASCII 线框草图,标出主要区域、操作入口、预览/确认点和风险提示,让用户确认后再 synthesize。'
688
+ : '请补齐关键字段、状态变化、数据来源、权限边界和可验收细节;如果后续发现涉及界面,也要先补 ASCII 线框草图。'
689
+ ),
690
+ ];
691
+ const layers = reflection?.mustConfirm?.length > 0 ? reflection.mustConfirm : fallbackLayers;
692
+ return {
693
+ active: true,
694
+ mode: reflection?.mode ?? 'deep',
695
+ label: reflection?.label ?? '需求入口深挖',
696
+ minimumDepth: reflection?.minimumDepth ?? 3,
697
+ questionLimit: gateQuestionLimit(gate, reflection?.questionLimit ?? 6),
698
+ needsInterfaceSketch,
699
+ promptPreview: gate?.promptPreview ?? '',
700
+ reflection,
701
+ layers,
702
+ };
703
+ }
704
+
705
+ function applyRequirementIntakeDepth(clarification, gate, reflection = null, options = {}) {
706
+ if (!gate?.active) {
707
+ return clarification;
708
+ }
709
+
710
+ const requirementIntake = buildRequirementIntakeDepth(gate, reflection);
711
+ if (options.satisfied) {
712
+ return {
713
+ ...clarification,
714
+ requirementIntake: {
715
+ ...requirementIntake,
716
+ satisfied: true,
717
+ },
718
+ shouldAskUser: false,
719
+ };
720
+ }
721
+ const existingIds = new Set(clarification.mustAskUser.map((item) => item.id));
722
+ const depthQuestions = requirementIntake.layers
723
+ .filter((item) => !existingIds.has(item.id));
724
+
725
+ if (!clarification.shouldAskUser && clarification.mustAskUser.length === 0 && depthQuestions.length === 0) {
726
+ return {
727
+ ...clarification,
728
+ requirementIntake: {
729
+ ...requirementIntake,
730
+ satisfied: true,
731
+ },
732
+ };
733
+ }
734
+
735
+ const combined = [...depthQuestions, ...clarification.mustAskUser];
736
+ const mustAskUser = combined.slice(0, requirementIntake.questionLimit);
737
+ const deferred = combined.slice(requirementIntake.questionLimit).map((item) => ({
738
+ id: item.id,
739
+ label: item.label,
740
+ prompt: item.prompt,
741
+ }));
742
+
743
+ return {
744
+ ...clarification,
745
+ requirementIntake,
746
+ mustAskUser,
747
+ canInferLater: [...deferred, ...clarification.canInferLater],
748
+ shouldAskUser: true,
749
+ };
750
+ }
751
+
752
+ function parseArtifactFrontmatter(text) {
753
+ if (!text.startsWith('---\n')) {
754
+ throw new Error('Artifact markdown is missing frontmatter.');
755
+ }
756
+ const end = text.indexOf('\n---', 4);
757
+ if (end < 0) {
758
+ throw new Error('Artifact markdown frontmatter is not closed.');
759
+ }
760
+ return parseYamlText(text.slice(4, end));
761
+ }
762
+
763
+ function buildPlaygroundState(snapshot) {
764
+ const sections = snapshot.sections ?? {};
765
+ return {
766
+ problemStatement: sections.problem?.problemStatement ?? '',
767
+ goals: [...(sections.goals?.goals ?? [])],
768
+ successMetrics: [...(sections.goals?.successMetrics ?? [])],
769
+ inScope: [...(sections.scope?.inScope ?? [])],
770
+ outOfScope: [...(sections.scope?.outOfScope ?? [])],
771
+ primaryFlows: [...(sections.scenarios?.primaryFlows ?? [])],
772
+ openQuestions: [...(sections.risks?.openQuestions ?? [])],
773
+ };
774
+ }
775
+
776
+ async function getPrdReviewState(ws, latestSnapshot = null) {
777
+ const currentState = ws.data.currentState ?? {};
778
+ const latestVersionId = latestSnapshot?.versionId ?? currentState.latestVersionId ?? null;
779
+ const stored = currentState.reviewStatus ?? null;
780
+ const reviewPath = stored?.reviewPath
781
+ ?? stored?.stableArtifact
782
+ ?? (latestVersionId ? canonicalReviewPath(ws, latestVersionId) : null);
783
+ const entryPath = stored?.entryPath ?? stored?.artifact ?? defaultReviewArtifactPath(ws);
784
+ const artifactExists = reviewPath ? await exists(reviewPath) : false;
785
+ const status = stored?.versionId === latestVersionId
786
+ ? normalizePrdReviewStatus(stored.status)
787
+ : (artifactExists ? 'pending-confirmation' : 'missing');
788
+ let reason = '最新 PRD 评审产物已确认。';
789
+ if (!artifactExists) {
790
+ const presentationGate = latestSnapshot ? getReviewPresentationGate(latestSnapshot) : null;
791
+ reason = presentationGate && !presentationGate.ok
792
+ ? '缺少已通过脚本校验的评审展示文案,先运行 openprd review-presentation 写入后再生成可确认评审页。'
793
+ : '缺少最新 PRD 评审文件,freeze 前需要重新生成可评审产物。';
794
+ } else if (status === 'pending-confirmation') {
795
+ reason = '最新 PRD 评审文件尚未标记为用户已确认。';
796
+ } else if (status === 'needs-revision') {
797
+ reason = '最新 PRD 评审文件已标记为需要修改,不能直接 freeze。';
798
+ }
799
+ return {
800
+ versionId: latestVersionId,
801
+ status,
802
+ artifactExists,
803
+ artifact: reviewPath,
804
+ entryArtifact: entryPath,
805
+ shouldGateFreeze: Boolean(latestVersionId) && (!artifactExists || status !== 'confirmed'),
806
+ reason,
807
+ updatedAt: stored?.updatedAt ?? null,
808
+ notes: stored?.notes ?? null,
809
+ };
810
+ }
811
+
812
+ async function synthesizeWorkspace(projectRoot, overrides = {}) {
813
+ const ws = await loadWorkspace(projectRoot);
814
+ if (!(await exists(ws.workspaceRoot))) {
815
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
816
+ }
817
+ ensureFreshRequirementStateForSynthesize({
818
+ gate: await readActiveRequirementGate(projectRoot),
819
+ currentState: ws.data.currentState ?? {},
820
+ overrides,
821
+ });
822
+
823
+ const versionIndex = await readVersionIndex(ws);
824
+ const nextVersionNumber = overrides.versionNumber ?? (versionIndex.length > 0
825
+ ? Math.max(...versionIndex.map((entry) => Number(entry.versionNumber) || 0)) + 1
826
+ : 1);
827
+ const versionId = overrides.versionId ?? formatVersionId(nextVersionNumber);
828
+ const createdAt = overrides.createdAt ?? timestamp();
829
+ const workUnitId = normalizeWorkUnitId(overrides.workUnit ?? overrides.workUnitId) ?? generateWorkUnitId();
830
+ const targetRoot = resolveTargetRoot(ws, overrides.targetRoot);
831
+ const baseCurrentState = {
832
+ ...(ws.data.currentState ?? {}),
833
+ captureMeta: {
834
+ ...((ws.data.currentState ?? {}).captureMeta ?? {}),
835
+ },
836
+ };
837
+ if (shouldDropInheritedReviewPresentationFromOverrides(overrides)) {
838
+ dropInheritedReviewPresentation(baseCurrentState);
839
+ }
840
+ const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState: baseCurrentState } }, {
841
+ ...overrides,
842
+ versionNumber: nextVersionNumber,
843
+ versionId,
844
+ createdAt,
845
+ workUnitId,
846
+ targetRoot,
847
+ productType: overrides.productType ?? resolveCurrentProductType(ws),
848
+ templatePack: overrides.templatePack ?? resolveActiveTemplatePack(ws),
849
+ });
850
+
851
+ snapshot.content = renderPrdMarkdown(snapshot);
852
+ snapshot.digest = crypto.createHash('sha256').update(snapshot.content).digest('hex');
853
+ assertOpenSpecPreflightReady(snapshot);
854
+
855
+ await writeVersionSnapshot(ws, snapshot);
856
+
857
+ const indexEntry = summarizeSnapshot(snapshot);
858
+ await writeVersionIndex(ws, [...versionIndex, indexEntry]);
859
+
860
+ await writeText(ws.paths.activePrd, snapshot.content);
861
+ await writeText(ws.paths.activeFlows, renderFlowDoc(snapshot));
862
+ await writeText(ws.paths.activeRoles, renderRolesDoc(snapshot));
863
+ await writeText(ws.paths.activeHandoff, renderHandoffDoc(snapshot));
864
+ const presentationGate = getReviewPresentationGate(snapshot);
865
+ const reviewFiles = presentationGate.ok
866
+ ? await writeReviewFiles(ws, snapshot)
867
+ : {
868
+ canonicalReview: canonicalReviewPath(ws, snapshot.versionId),
869
+ activeReviewEntry: defaultReviewArtifactPath(ws),
870
+ };
871
+ if (!presentationGate.ok) {
872
+ await removeReviewFiles(reviewFiles);
873
+ reviewFiles.activeReviewEntry = null;
874
+ }
875
+ const workUnit = await writeWorkUnitBinding(ws, {
876
+ snapshot,
877
+ reviewPath: reviewFiles.canonicalReview,
878
+ activeReviewPath: reviewFiles.activeReviewEntry,
879
+ targetRoot,
880
+ });
881
+ if (overrides.open && presentationGate.ok) {
882
+ await openArtifactInBrowser(reviewFiles.canonicalReview);
883
+ }
884
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(snapshot));
885
+ await appendWorkflowEvent(ws, 'synthesized', {
886
+ versionId: snapshot.versionId,
887
+ versionNumber: snapshot.versionNumber,
888
+ productType: snapshot.productType,
889
+ reviewArtifact: presentationGate.ok ? reviewFiles.canonicalReview : null,
890
+ reviewPresentationRequired: !presentationGate.ok,
891
+ });
892
+ await appendDecision(ws, [
893
+ `已生成版本 ${snapshot.versionId}。`,
894
+ `产品类型: ${snapshot.productType ?? '未分类'}。`,
895
+ `模板包: ${snapshot.templatePack}。`,
896
+ `Digest: ${snapshot.digest}.`,
897
+ ]);
898
+ await appendProgress(ws, [
899
+ `已生成 PRD 快照 ${snapshot.versionId}。`,
900
+ `已更新当前 PRD、流程、角色和交接文档。`,
901
+ presentationGate.ok
902
+ ? `已生成可确认评审面板: ${reviewFiles.canonicalReview}。`
903
+ : '评审面板暂未生成:需要先通过 openprd review-presentation 写入展示文案。',
904
+ ]);
905
+
906
+ const currentState = syncCurrentSnapshotCache({
907
+ ...baseCurrentState,
908
+ captureMeta: {
909
+ ...baseCurrentState.captureMeta,
910
+ ...(overrides.title ? { 'meta.title': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
911
+ ...(overrides.owner ? { 'meta.owner': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
912
+ ...(overrides.problemStatement ? { 'problem.problemStatement': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
913
+ ...(overrides.whyNow ? { 'problem.whyNow': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
914
+ ...(overrides.evidence ? { 'problem.evidence': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
915
+ ...(overrides.productType ? { 'meta.productType': { source: 'user-confirmed', capturedAt: timestamp() } } : {}),
916
+ },
917
+ status: 'synthesized',
918
+ prdVersion: snapshot.versionNumber,
919
+ latestVersionId: snapshot.versionId,
920
+ latestVersionDigest: snapshot.digest,
921
+ activeWorkUnitId: snapshot.workUnitId,
922
+ targetRoot,
923
+ reviewStatus: {
924
+ versionId: snapshot.versionId,
925
+ workUnitId: snapshot.workUnitId,
926
+ status: 'pending-confirmation',
927
+ reviewPath: reviewFiles.canonicalReview,
928
+ entryPath: reviewFiles.activeReviewEntry,
929
+ artifact: reviewFiles.activeReviewEntry,
930
+ stableArtifact: reviewFiles.canonicalReview,
931
+ updatedAt: snapshot.createdAt,
932
+ },
933
+ title: snapshot.title,
934
+ owner: snapshot.owner,
935
+ productType: snapshot.productType,
936
+ templatePack: snapshot.templatePack,
937
+ synthesizedAt: snapshot.createdAt,
938
+ }, snapshot);
939
+ await writeJson(ws.paths.currentState, currentState);
940
+ const nextWs = { ...ws, data: { ...ws.data, currentState } };
941
+ await syncSessionBindingFromSnapshot(projectRoot, snapshot, {
942
+ reviewStatus: 'pending-confirmation',
943
+ reviewPath: reviewFiles.canonicalReview,
944
+ activeReviewPath: reviewFiles.activeReviewEntry,
945
+ targetRoot,
946
+ });
947
+
948
+ return {
949
+ ws: nextWs,
950
+ snapshot,
951
+ currentState,
952
+ indexEntry,
953
+ versionIndex: [...versionIndex, indexEntry],
954
+ reviewArtifact: reviewFiles.activeReviewEntry,
955
+ stableReviewArtifact: reviewFiles.canonicalReview,
956
+ reviewPath: reviewFiles.canonicalReview,
957
+ reviewEntryPath: reviewFiles.activeReviewEntry,
958
+ reviewPresentationRequired: !presentationGate.ok,
959
+ reviewPresentationGate: presentationGate,
960
+ workUnitId: snapshot.workUnitId,
961
+ workUnit,
962
+ opened: Boolean(overrides.open && presentationGate.ok),
963
+ };
964
+ }
965
+
966
+ async function diffWorkspace(projectRoot, options = {}) {
967
+ const ws = await loadWorkspace(projectRoot);
968
+ if (!(await exists(ws.workspaceRoot))) {
969
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
970
+ }
971
+
972
+ const index = await readVersionIndex(ws);
973
+ if (index.length === 0) {
974
+ throw new Error('No synthesized PRD versions exist yet. Run openprd synthesize first.');
975
+ }
976
+
977
+ const requestedFrom = normalizeVersionId(options.from);
978
+ const requestedTo = normalizeVersionId(options.to);
979
+
980
+ const fromEntry = requestedFrom
981
+ ? index.find((entry) => normalizeVersionId(entry.versionId) === requestedFrom)
982
+ : index[index.length - 2] ?? null;
983
+ const toEntry = requestedTo
984
+ ? index.find((entry) => normalizeVersionId(entry.versionId) === requestedTo)
985
+ : index[index.length - 1] ?? null;
986
+
987
+ if (!fromEntry || !toEntry) {
988
+ throw new Error('Need at least two PRD versions to diff.');
989
+ }
990
+
991
+ const before = await readVersionSnapshot(ws, fromEntry.versionId);
992
+ const after = await readVersionSnapshot(ws, toEntry.versionId);
993
+ if (!before || !after) {
994
+ throw new Error('Unable to read one or both PRD version snapshots.');
995
+ }
996
+
997
+ const diff = diffSnapshots(before, after);
998
+ return { ws, before, after, diff };
999
+ }
1000
+
1001
+ async function reviewWorkspace(projectRoot, options = {}) {
1002
+ const ws = await loadWorkspace(projectRoot);
1003
+ if (!(await exists(ws.workspaceRoot))) {
1004
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1005
+ }
1006
+ const latest = await loadLatestVersionSnapshot(ws);
1007
+ if (!latest?.snapshot) {
1008
+ return {
1009
+ ok: false,
1010
+ action: 'review',
1011
+ projectRoot,
1012
+ errors: ['No synthesized PRD version exists yet. Run openprd synthesize first.'],
1013
+ };
1014
+ }
1015
+
1016
+ const requestedVersion = normalizeVersionId(options.version);
1017
+ const snapshot = requestedVersion
1018
+ ? await readVersionSnapshot(ws, requestedVersion)
1019
+ : latest.snapshot;
1020
+ if (!snapshot) {
1021
+ return {
1022
+ ok: false,
1023
+ action: 'review',
1024
+ projectRoot,
1025
+ errors: [`No synthesized PRD version found for ${options.version}.`],
1026
+ };
1027
+ }
1028
+
1029
+ let requestedWorkUnitId = null;
1030
+ try {
1031
+ requestedWorkUnitId = normalizeWorkUnitId(options.workUnit ?? options.workUnitId);
1032
+ } catch (error) {
1033
+ return {
1034
+ ok: false,
1035
+ action: 'review',
1036
+ projectRoot,
1037
+ versionId: snapshot.versionId,
1038
+ errors: [error.message],
1039
+ };
1040
+ }
1041
+
1042
+ const validationErrors = [];
1043
+ if (options.digest && options.digest !== snapshot.digest) {
1044
+ validationErrors.push(`Digest mismatch for ${snapshot.versionId}: expected ${snapshot.digest}, got ${options.digest}.`);
1045
+ }
1046
+ if (requestedWorkUnitId && snapshot.workUnitId !== requestedWorkUnitId) {
1047
+ validationErrors.push(`Work unit mismatch for ${snapshot.versionId}: expected ${snapshot.workUnitId ?? 'none'}, got ${requestedWorkUnitId}.`);
1048
+ }
1049
+ if (validationErrors.length > 0) {
1050
+ return {
1051
+ ok: false,
1052
+ action: 'review',
1053
+ projectRoot,
1054
+ versionId: snapshot.versionId,
1055
+ workUnitId: snapshot.workUnitId ?? null,
1056
+ status: 'blocked',
1057
+ errors: validationErrors,
1058
+ };
1059
+ }
1060
+
1061
+ const isLatest = normalizeVersionId(snapshot.versionId) === normalizeVersionId(latest.snapshot.versionId);
1062
+ const presentationGate = getReviewPresentationGate(snapshot);
1063
+ if (!presentationGate.ok) {
1064
+ return {
1065
+ ok: false,
1066
+ action: 'review',
1067
+ projectRoot,
1068
+ versionId: snapshot.versionId,
1069
+ workUnitId: snapshot.workUnitId ?? null,
1070
+ status: 'blocked',
1071
+ errors: presentationGate.errors,
1072
+ presentationFeedback: presentationGate.violations,
1073
+ requiredCommand: presentationGate.requiredCommand,
1074
+ };
1075
+ }
1076
+ const reviewFiles = await writeReviewFiles(ws, snapshot, { writeEntry: isLatest });
1077
+ const bindingBefore = await readWorkUnitBinding(ws, snapshot.workUnitId);
1078
+ const before = isLatest
1079
+ ? await getPrdReviewState(ws, snapshot)
1080
+ : {
1081
+ status: normalizePrdReviewStatus(bindingBefore?.status ?? 'pending-confirmation'),
1082
+ artifact: reviewFiles.canonicalReview,
1083
+ };
1084
+ let marked = false;
1085
+ let status = before.status;
1086
+ let workUnit = bindingBefore;
1087
+ if (options.mark) {
1088
+ status = normalizePrdReviewStatus(options.mark);
1089
+ if (status !== options.mark) {
1090
+ throw new Error(`Unsupported review status: ${options.mark}`);
1091
+ }
1092
+ if (isLatest) {
1093
+ const currentState = {
1094
+ ...(ws.data.currentState ?? {}),
1095
+ activeWorkUnitId: snapshot.workUnitId ?? (ws.data.currentState ?? {}).activeWorkUnitId,
1096
+ targetRoot: snapshot.targetRoot ?? (ws.data.currentState ?? {}).targetRoot,
1097
+ reviewStatus: {
1098
+ versionId: snapshot.versionId,
1099
+ workUnitId: snapshot.workUnitId ?? null,
1100
+ status,
1101
+ reviewPath: reviewFiles.canonicalReview,
1102
+ entryPath: reviewFiles.activeReviewEntry,
1103
+ artifact: reviewFiles.activeReviewEntry,
1104
+ stableArtifact: reviewFiles.canonicalReview,
1105
+ updatedAt: timestamp(),
1106
+ notes: options.notes ?? null,
1107
+ },
1108
+ };
1109
+ await writeJson(ws.paths.currentState, currentState);
1110
+ }
1111
+ workUnit = await writeWorkUnitBinding(ws, {
1112
+ snapshot,
1113
+ reviewPath: reviewFiles.canonicalReview,
1114
+ activeReviewPath: reviewFiles.activeReviewEntry,
1115
+ targetRoot: snapshot.targetRoot,
1116
+ status,
1117
+ });
1118
+ await appendWorkflowEvent(ws, 'review_marked', {
1119
+ versionId: snapshot.versionId,
1120
+ workUnitId: snapshot.workUnitId ?? null,
1121
+ status,
1122
+ });
1123
+ await appendProgress(ws, [
1124
+ `PRD 评审状态: ${status}。`,
1125
+ `版本: ${snapshot.versionId}。`,
1126
+ snapshot.workUnitId ? `工作单元: ${snapshot.workUnitId}。` : null,
1127
+ ]);
1128
+ await syncSessionBindingFromReview(projectRoot, snapshot, {
1129
+ reviewStatus: status,
1130
+ reviewPath: reviewFiles.canonicalReview,
1131
+ activeReviewPath: reviewFiles.activeReviewEntry,
1132
+ targetRoot: snapshot.targetRoot,
1133
+ });
1134
+ marked = true;
1135
+ }
1136
+
1137
+ const reloaded = await loadWorkspace(projectRoot);
1138
+ const after = isLatest
1139
+ ? await getPrdReviewState(reloaded, snapshot)
1140
+ : {
1141
+ status,
1142
+ artifactExists: await exists(reviewFiles.canonicalReview),
1143
+ artifact: reviewFiles.canonicalReview,
1144
+ entryArtifact: null,
1145
+ };
1146
+ if (options.open && (await exists(reviewFiles.canonicalReview))) {
1147
+ await openArtifactInBrowser(reviewFiles.canonicalReview);
1148
+ }
1149
+
1150
+ return {
1151
+ ok: after.artifactExists,
1152
+ action: 'review',
1153
+ projectRoot,
1154
+ versionId: snapshot.versionId,
1155
+ workUnitId: snapshot.workUnitId ?? null,
1156
+ status: after.status,
1157
+ previousStatus: before.status,
1158
+ marked,
1159
+ reviewArtifact: after.entryArtifact ?? reviewFiles.activeReviewEntry,
1160
+ stableReviewArtifact: after.artifact,
1161
+ reviewPath: after.artifact,
1162
+ reviewEntryPath: after.entryArtifact ?? reviewFiles.activeReviewEntry,
1163
+ workUnit,
1164
+ opened: Boolean(options.open && after.artifactExists),
1165
+ errors: after.artifactExists ? [] : ['Missing review file. Run openprd synthesize . --open'],
1166
+ };
1167
+ }
1168
+
1169
+ async function clarifyWorkspace(projectRoot, options = {}) {
1170
+ const ws = await loadWorkspace(projectRoot);
1171
+ if (!(await exists(ws.workspaceRoot))) {
1172
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1173
+ }
1174
+
1175
+ const versionIndex = await readVersionIndex(ws);
1176
+ const currentState = ws.data.currentState ?? {};
1177
+ const snapshot = (await loadLatestVersionSnapshot(ws))?.snapshot ?? buildPrdSnapshot(ws, {
1178
+ ...currentState,
1179
+ versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1180
+ versionId: currentState.prdVersion > 0
1181
+ ? formatVersionId(currentState.prdVersion)
1182
+ : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1183
+ productType: resolveCurrentProductType(ws),
1184
+ templatePack: resolveActiveTemplatePack(ws),
1185
+ status: currentState.status ?? 'draft',
1186
+ });
1187
+
1188
+ const analysis = analyzePrdSnapshot(snapshot);
1189
+ const basePlan = buildClarificationPlan(snapshot, analysis);
1190
+ const scenario = await detectWorkspaceScenario(projectRoot, ws, versionIndex);
1191
+ const requirementGate = await readActiveRequirementGate(projectRoot);
1192
+ const intakeReflection = await buildRequirementIntakeReflection({
1193
+ projectRoot,
1194
+ ws,
1195
+ snapshot,
1196
+ analysis,
1197
+ scenario,
1198
+ gate: requirementGate,
1199
+ });
1200
+ const intakeReflectionPath = await writeRequirementIntakeReflection(ws, intakeReflection);
1201
+ const prdReviewState = await getPrdReviewState(ws, snapshot);
1202
+ const clarification = applyRequirementIntakeDepth(buildClarificationState({
1203
+ snapshot,
1204
+ analysis,
1205
+ basePlan,
1206
+ scenario,
1207
+ captureMeta: ws.data.currentState?.captureMeta ?? {},
1208
+ prdReviewState,
1209
+ limit: Number(options.limit ?? 8),
1210
+ }), requirementGate, intakeReflection);
1211
+ const clarifyPresentation = chooseClarifyPresentation({
1212
+ requirementGate,
1213
+ clarification,
1214
+ reflection: intakeReflection,
1215
+ requestedMode: options.mode ?? 'auto',
1216
+ });
1217
+ const inlineClarification = buildInlineClarification({
1218
+ clarification,
1219
+ reflection: intakeReflection,
1220
+ presentation: clarifyPresentation,
1221
+ });
1222
+
1223
+ await appendWorkflowEvent(ws, 'clarify', {
1224
+ missingRequiredFields: clarification.missingRequiredFields,
1225
+ mustAskUser: clarification.mustAskUser.map((item) => item.id),
1226
+ scenario: clarification.scenario.id,
1227
+ intakeReflection: intakeReflectionPath ? path.relative(ws.workspaceRoot, intakeReflectionPath) : null,
1228
+ presentationMode: clarifyPresentation.mode,
1229
+ });
1230
+ await appendOpenQuestions(ws, clarification.mustAskUser.map((item) => item.prompt));
1231
+ let clarifyHtmlPath = null;
1232
+ let clarifyBundle = null;
1233
+ if (options.open && clarifyHtmlPath) {
1234
+ await openArtifactInBrowser(clarifyHtmlPath);
1235
+ }
1236
+
1237
+ return {
1238
+ ws,
1239
+ snapshot,
1240
+ analysis,
1241
+ clarification,
1242
+ clarifyPresentation,
1243
+ inlineClarification,
1244
+ clarifyArtifact: clarifyHtmlPath,
1245
+ clarifyArtifactBundle: clarifyBundle,
1246
+ intakeReflection,
1247
+ intakeReflectionPath,
1248
+ opened: Boolean(options.open && clarifyHtmlPath),
1249
+ };
1250
+ }
1251
+
1252
+ async function playgroundWorkspace(projectRoot, options = {}) {
1253
+ const ws = await loadWorkspace(projectRoot);
1254
+ if (!(await exists(ws.workspaceRoot))) {
1255
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1256
+ }
1257
+
1258
+ const versionIndex = await readVersionIndex(ws);
1259
+ const currentState = ws.data.currentState ?? {};
1260
+ const snapshot = (await loadLatestVersionSnapshot(ws))?.snapshot ?? buildPrdSnapshot(ws, {
1261
+ ...currentState,
1262
+ versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1263
+ versionId: currentState.prdVersion > 0
1264
+ ? formatVersionId(currentState.prdVersion)
1265
+ : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1266
+ productType: resolveCurrentProductType(ws),
1267
+ templatePack: resolveActiveTemplatePack(ws),
1268
+ status: currentState.status ?? 'draft',
1269
+ });
1270
+
1271
+ const state = buildPlaygroundState(snapshot);
1272
+ const bundle = artifactBundlePaths(ws, `${snapshot.versionId}-playground`);
1273
+ const markdown = renderPlaygroundMarkdown({ snapshot, state });
1274
+ const patch = renderPlaygroundPatch({ state });
1275
+ await writeText(bundle.markdown, markdown);
1276
+ await writeJson(bundle.patch, patch);
1277
+ await writeHtmlArtifact(bundle.html, renderPlaygroundArtifact({
1278
+ snapshot,
1279
+ state,
1280
+ markdownPath: bundle.markdown,
1281
+ patchPath: bundle.patch,
1282
+ }));
1283
+ await appendWorkflowEvent(ws, 'playground_generated', {
1284
+ versionId: snapshot.versionId,
1285
+ htmlPath: bundle.html,
1286
+ markdownPath: bundle.markdown,
1287
+ patchPath: bundle.patch,
1288
+ });
1289
+ await appendProgress(ws, [
1290
+ `已生成 playground artifact bundle: ${path.relative(ws.workspaceRoot, bundle.dir)}。`,
1291
+ ]);
1292
+ if (options.open) {
1293
+ await openArtifactInBrowser(bundle.html);
1294
+ }
1295
+
1296
+ return {
1297
+ ws,
1298
+ snapshot,
1299
+ state,
1300
+ htmlPath: bundle.html,
1301
+ markdownPath: bundle.markdown,
1302
+ patchPath: bundle.patch,
1303
+ opened: Boolean(options.open),
1304
+ };
1305
+ }
1306
+
1307
+ async function captureWorkspace(projectRoot, options = {}) {
1308
+ const ws = await loadWorkspace(projectRoot);
1309
+ if (!(await exists(ws.workspaceRoot))) {
1310
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1311
+ }
1312
+
1313
+ const currentState = {
1314
+ ...(ws.data.currentState ?? {}),
1315
+ };
1316
+ currentState.captureMeta = {
1317
+ ...(currentState.captureMeta ?? {}),
1318
+ };
1319
+
1320
+ const updates = [];
1321
+
1322
+ if (options.artifactMarkdown) {
1323
+ const artifactText = await readText(path.resolve(options.artifactMarkdown));
1324
+ const artifact = parseArtifactFrontmatter(artifactText);
1325
+ const payload = artifact.capturePatch;
1326
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1327
+ throw new Error('Artifact markdown frontmatter is missing capturePatch.');
1328
+ }
1329
+
1330
+ for (const [field, rawEntry] of Object.entries(payload)) {
1331
+ const stateKey = FIELD_PATH_TO_STATE_KEY[field];
1332
+ if (!stateKey) {
1333
+ throw new Error(`Unsupported capture field in artifact markdown: ${field}`);
1334
+ }
1335
+ const value = rawEntry?.value ?? rawEntry;
1336
+ const source = rawEntry?.source ?? options.source;
1337
+ const append = rawEntry?.append ?? options.append;
1338
+ if (value === null || value === undefined) {
1339
+ throw new Error(`Missing capture value in artifact markdown for field: ${field}`);
1340
+ }
1341
+ updates.push({
1342
+ field,
1343
+ stateKey,
1344
+ value,
1345
+ source: CAPTURE_SOURCES.includes(source) ? source : 'user-confirmed',
1346
+ append: Boolean(append),
1347
+ });
1348
+ }
1349
+ } else if (options.jsonFile) {
1350
+ const payload = await readJson(path.resolve(options.jsonFile));
1351
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1352
+ throw new Error('Capture JSON file must contain an object at the root');
1353
+ }
1354
+
1355
+ for (const [field, rawEntry] of Object.entries(payload)) {
1356
+ const stateKey = FIELD_PATH_TO_STATE_KEY[field];
1357
+ if (!stateKey) {
1358
+ throw new Error(`Unsupported capture field in json file: ${field}`);
1359
+ }
1360
+
1361
+ let value = rawEntry;
1362
+ let source = options.source;
1363
+ let append = Boolean(options.append);
1364
+
1365
+ if (rawEntry && typeof rawEntry === 'object' && !Array.isArray(rawEntry) && ('value' in rawEntry || 'source' in rawEntry || 'append' in rawEntry)) {
1366
+ value = rawEntry.value;
1367
+ source = rawEntry.source ?? source;
1368
+ append = rawEntry.append ?? append;
1369
+ }
1370
+
1371
+ if (value === null || value === undefined) {
1372
+ throw new Error(`Missing capture value in json file for field: ${field}`);
1373
+ }
1374
+
1375
+ updates.push({
1376
+ field,
1377
+ stateKey,
1378
+ value,
1379
+ source: CAPTURE_SOURCES.includes(source) ? source : 'user-confirmed',
1380
+ append: Boolean(append),
1381
+ });
1382
+ }
1383
+ } else {
1384
+ const field = options.field?.trim();
1385
+ if (!field) {
1386
+ throw new Error('Missing required option: --field');
1387
+ }
1388
+ const stateKey = FIELD_PATH_TO_STATE_KEY[field];
1389
+ if (!stateKey) {
1390
+ throw new Error(`Unsupported capture field: ${field}`);
1391
+ }
1392
+ if (options.value === null || options.value === undefined) {
1393
+ throw new Error('Missing required option: --value');
1394
+ }
1395
+ updates.push({
1396
+ field,
1397
+ stateKey,
1398
+ value: options.value,
1399
+ source: CAPTURE_SOURCES.includes(options.source) ? options.source : 'user-confirmed',
1400
+ append: Boolean(options.append),
1401
+ });
1402
+ }
1403
+
1404
+ const applied = [];
1405
+ for (const update of updates) {
1406
+ const nextValue = coerceCapturedValue(update.field, update.value, update.append);
1407
+
1408
+ if (update.append) {
1409
+ const prev = currentState[update.stateKey];
1410
+ const prevArray = Array.isArray(prev)
1411
+ ? prev
1412
+ : (prev ? coerceCapturedValue(update.field, prev, true) : []);
1413
+ const nextArray = Array.isArray(nextValue) ? nextValue : [nextValue];
1414
+ currentState[update.stateKey] = [...prevArray, ...nextArray];
1415
+ } else {
1416
+ currentState[update.stateKey] = nextValue;
1417
+ }
1418
+
1419
+ applied.push({
1420
+ field: update.field,
1421
+ stateKey: update.stateKey,
1422
+ source: update.source,
1423
+ value: currentState[update.stateKey],
1424
+ });
1425
+ }
1426
+
1427
+ currentState.lastCapturedAt = timestamp();
1428
+ currentState.status = currentState.status === 'initialized' ? 'clarifying' : (currentState.status ?? 'clarifying');
1429
+ for (const update of applied) {
1430
+ currentState.captureMeta[update.field] = {
1431
+ source: update.source,
1432
+ capturedAt: currentState.lastCapturedAt,
1433
+ };
1434
+ }
1435
+ const staleReview = markReviewStateStaleAfterCapture(currentState, applied, currentState.lastCapturedAt);
1436
+ await writeJson(ws.paths.currentState, currentState);
1437
+
1438
+ const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState } }, {
1439
+ ...currentState,
1440
+ versionNumber: currentState.prdVersion ?? 0,
1441
+ versionId: currentState.prdVersion > 0 ? formatVersionId(currentState.prdVersion) : 'v0000',
1442
+ productType: currentState.productType ?? resolveCurrentProductType(ws),
1443
+ templatePack: currentState.templatePack ?? resolveActiveTemplatePack(ws),
1444
+ });
1445
+ const analysis = analyzePrdSnapshot(snapshot);
1446
+ const diagramState = await getDiagramReviewState({ ...ws, data: { ...ws.data, currentState } }, snapshot);
1447
+ const updatedWs = { ...ws, data: { ...ws.data, currentState } };
1448
+ const scenario = await detectWorkspaceScenario(projectRoot, updatedWs, await readVersionIndex(ws));
1449
+ const requirementGate = await readActiveRequirementGate(projectRoot);
1450
+ const intakeReflection = await buildRequirementIntakeReflection({
1451
+ projectRoot,
1452
+ ws: updatedWs,
1453
+ snapshot,
1454
+ analysis,
1455
+ scenario,
1456
+ gate: requirementGate,
1457
+ });
1458
+ await writeRequirementIntakeReflection(updatedWs, intakeReflection);
1459
+ const prdReviewState = await getPrdReviewState(updatedWs, snapshot);
1460
+ const clarification = applyRequirementIntakeDepth(buildClarificationState({
1461
+ snapshot,
1462
+ analysis,
1463
+ basePlan: buildClarificationPlan(snapshot, analysis),
1464
+ scenario,
1465
+ captureMeta: currentState.captureMeta,
1466
+ prdReviewState,
1467
+ limit: 8,
1468
+ }), requirementGate, intakeReflection);
1469
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(snapshot, analysis, { diagramState, clarificationState: clarification }));
1470
+ await appendWorkflowEvent(ws, 'capture', {
1471
+ fields: applied.map((item) => item.field),
1472
+ sources: applied.map((item) => item.source),
1473
+ staleReview,
1474
+ });
1475
+ await appendDecision(ws, [
1476
+ `Captured clarification for ${applied.map((item) => item.field).join(', ')}.`,
1477
+ ]);
1478
+ await appendProgress(ws, [
1479
+ `已更新 ${applied.length} 个字段到当前工作区状态。`,
1480
+ ]);
1481
+
1482
+ return {
1483
+ ws: { ...ws, data: { ...ws.data, currentState } },
1484
+ applied,
1485
+ artifactMarkdown: options.artifactMarkdown ?? null,
1486
+ field: applied[0]?.field ?? null,
1487
+ stateKey: applied[0]?.stateKey ?? null,
1488
+ value: applied[0]?.value ?? null,
1489
+ source: applied[0]?.source ?? null,
1490
+ analysis,
1491
+ };
1492
+ }
1493
+
1494
+ async function computeWorkspaceGuidance(ws, options = {}) {
1495
+ const versionIndex = await readVersionIndex(ws);
1496
+ const currentState = ws.data.currentState ?? {};
1497
+ const currentProductType = resolveCurrentProductType(ws);
1498
+ const currentStatus = currentState.status ?? 'unknown';
1499
+ const latestVersion = versionIndex.length > 0 ? await loadLatestVersionSnapshot(ws) : null;
1500
+ const currentDraftSnapshot = buildPrdSnapshot(ws, {
1501
+ ...currentState,
1502
+ versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
1503
+ versionId: currentState.prdVersion > 0
1504
+ ? formatVersionId(currentState.prdVersion)
1505
+ : (versionIndex.at(-1)?.versionId ?? 'v0000'),
1506
+ productType: currentProductType,
1507
+ templatePack: resolveActiveTemplatePack(ws),
1508
+ });
1509
+ const analysisSnapshot = shouldUseCurrentDraftForGuidance(currentState)
1510
+ ? currentDraftSnapshot
1511
+ : (latestVersion?.snapshot ?? currentDraftSnapshot);
1512
+ const analysis = analyzePrdSnapshot(analysisSnapshot);
1513
+ const hasProductType = isSupportedProductType(currentProductType ?? analysis.productType);
1514
+ const diagramState = await getDiagramReviewState(ws, analysisSnapshot);
1515
+ const prdReviewState = await getPrdReviewState(ws, analysisSnapshot);
1516
+ const scenario = await detectWorkspaceScenario(ws.projectRoot, ws, versionIndex);
1517
+ const requirementGate = await readActiveRequirementGate(ws.projectRoot);
1518
+ const intakeSatisfiedByReview = prdReviewState.status === 'confirmed' && analysis.missingRequiredFields === 0;
1519
+ const intakeReflection = await buildRequirementIntakeReflection({
1520
+ projectRoot: ws.projectRoot,
1521
+ ws,
1522
+ snapshot: analysisSnapshot,
1523
+ analysis,
1524
+ scenario,
1525
+ gate: requirementGate,
1526
+ });
1527
+ const clarification = applyRequirementIntakeDepth(buildClarificationState({
1528
+ snapshot: analysisSnapshot,
1529
+ analysis,
1530
+ basePlan: buildClarificationPlan(analysisSnapshot, analysis),
1531
+ scenario,
1532
+ captureMeta: currentState.captureMeta ?? {},
1533
+ prdReviewState,
1534
+ limit: Number(options.questionLimit ?? 5),
1535
+ }), requirementGate, intakeReflection, { satisfied: intakeSatisfiedByReview });
1536
+
1537
+ let nextAction = 'synthesize';
1538
+ let reason = 'PRD 可以合成为第一个版本。';
1539
+ let suggestedCommand = 'openprd synthesize .';
1540
+ let suggestedQuestions = analysis.suggestedQuestions;
1541
+
1542
+ if (clarification.shouldAskUser) {
1543
+ nextAction = 'clarify-user';
1544
+ reason = '工作区缺少用户确认的关键信息,需要先澄清再继续合成。';
1545
+ suggestedCommand = 'openprd clarify .';
1546
+ suggestedQuestions = clarification.mustAskUser.map((item) => item.prompt);
1547
+ } else if (!hasProductType) {
1548
+ nextAction = 'classify';
1549
+ reason = '产品类型尚未锁定。';
1550
+ suggestedCommand = 'openprd classify . <consumer|b2b|agent>';
1551
+ suggestedQuestions = ['这是 consumer、b2b 还是 agent 产品?'];
1552
+ } else if (analysis.missingRequiredFields > 0) {
1553
+ nextAction = 'interview';
1554
+ reason = `仍缺少 ${analysis.missingRequiredFields} 个必填字段。`;
1555
+ suggestedCommand = `openprd interview . --product-type ${currentProductType}`;
1556
+ } else if (currentStatus === 'frozen') {
1557
+ nextAction = 'handoff';
1558
+ reason = '最新 PRD 已 freeze,可以交接。';
1559
+ suggestedCommand = 'openprd handoff . --target openprd';
1560
+ suggestedQuestions = [];
1561
+ } else if (currentStatus === 'handed_off') {
1562
+ nextAction = versionIndex.length > 1 ? 'diff' : 'history';
1563
+ reason = '该工作区已经完成交接。';
1564
+ suggestedCommand = nextAction === 'diff' ? 'openprd diff .' : 'openprd history .';
1565
+ suggestedQuestions = [];
1566
+ } else if (diagramState.shouldGateFreeze && (currentStatus === 'synthesized' || currentState.prdVersion > 0)) {
1567
+ nextAction = 'diagram';
1568
+ reason = diagramState.reason;
1569
+ suggestedCommand = `openprd diagram . --type ${diagramState.preferredType} --open`;
1570
+ suggestedQuestions = [
1571
+ `这张 ${diagramState.preferredType} 图是否符合预期设计?`,
1572
+ '当前可视化表达中还缺少什么,或哪里不准确?',
1573
+ ];
1574
+ } else if (prdReviewState.shouldGateFreeze && (currentStatus === 'synthesized' || currentState.prdVersion > 0)) {
1575
+ nextAction = 'review';
1576
+ reason = prdReviewState.reason;
1577
+ suggestedCommand = prdReviewState.artifactExists
1578
+ ? 'openprd review . --open'
1579
+ : (prdReviewState.reason.includes('review-presentation')
1580
+ ? 'openprd review-presentation . --template'
1581
+ : 'openprd synthesize . --open');
1582
+ suggestedQuestions = [
1583
+ '这份 PRD 的问题、目标、范围、主流程、失败路径和风险是否符合你的理解?',
1584
+ '如果已经确认,请运行 openprd review . --mark confirmed;如果需要修改,请运行 openprd review . --mark needs-revision。',
1585
+ ];
1586
+ } else if (currentStatus === 'synthesized' || currentState.prdVersion > 0) {
1587
+ nextAction = 'freeze';
1588
+ reason = '已有版本化 PRD,交接前应先 freeze。';
1589
+ suggestedCommand = 'openprd freeze .';
1590
+ suggestedQuestions = [];
1591
+ }
1592
+
1593
+ const taskGraph = buildWorkflowTaskGraph(analysisSnapshot, analysis, { diagramState, prdReviewState, clarificationState: clarification });
1594
+ const gates = deriveGateLabels({ nextAction, diagramState, clarification });
1595
+
1596
+ return {
1597
+ versionIndex,
1598
+ currentState,
1599
+ analysisSnapshot,
1600
+ analysis,
1601
+ diagramState,
1602
+ prdReviewState,
1603
+ clarification,
1604
+ taskGraph,
1605
+ nextAction,
1606
+ reason,
1607
+ suggestedCommand,
1608
+ suggestedQuestions,
1609
+ gates,
1610
+ };
1611
+ }
1612
+
1613
+
1614
+ async function nextWorkspace(projectRoot) {
1615
+ const ws = await loadWorkspace(projectRoot);
1616
+ if (!(await exists(ws.workspaceRoot))) {
1617
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1618
+ }
1619
+
1620
+ const guidance = await computeWorkspaceGuidance(ws, { questionLimit: 5 });
1621
+ const {
1622
+ versionIndex,
1623
+ currentState,
1624
+ analysisSnapshot,
1625
+ analysis,
1626
+ diagramState,
1627
+ prdReviewState,
1628
+ clarification,
1629
+ taskGraph,
1630
+ nextAction,
1631
+ reason,
1632
+ suggestedCommand,
1633
+ suggestedQuestions,
1634
+ gates,
1635
+ } = guidance;
1636
+
1637
+ await writeJson(ws.paths.taskGraph, taskGraph);
1638
+ await appendWorkflowEvent(ws, 'next', {
1639
+ nextAction,
1640
+ reason,
1641
+ missingRequiredFields: analysis.missingRequiredFields,
1642
+ });
1643
+ if (analysis.missingRequiredFields > 0) {
1644
+ await appendOpenQuestions(ws, [
1645
+ `还有 ${analysis.missingRequiredFields} 个关键信息需要确认。`,
1646
+ ...analysis.suggestedQuestions,
1647
+ ]);
1648
+ }
1649
+ await appendProgress(ws, [
1650
+ `建议下一步: ${nextAction}。`,
1651
+ `原因: ${reason}`,
1652
+ ]);
1653
+
1654
+ return {
1655
+ ws,
1656
+ currentState,
1657
+ versionIndex,
1658
+ analysisSnapshot,
1659
+ analysis,
1660
+ diagramState,
1661
+ prdReviewState,
1662
+ clarification,
1663
+ taskGraph,
1664
+ gates,
1665
+ recommendation: {
1666
+ nextAction,
1667
+ reason,
1668
+ suggestedCommand,
1669
+ suggestedQuestions,
1670
+ currentGate: gates.currentGate,
1671
+ upcomingGate: gates.upcomingGate,
1672
+ },
1673
+ workflow: ['clarify', 'classify', 'interview', 'synthesize', 'diagram', 'review', 'freeze', 'handoff'],
1674
+ };
1675
+ }
1676
+
1677
+ async function historyWorkspace(projectRoot) {
1678
+ const ws = await loadWorkspace(projectRoot);
1679
+ if (!(await exists(ws.workspaceRoot))) {
1680
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1681
+ }
1682
+
1683
+ const index = await readVersionIndex(ws);
1684
+ return { ws, versions: index };
1685
+ }
1686
+
1687
+ async function classifyWorkspace(projectRoot, productType) {
1688
+ if (!isSupportedProductType(productType)) {
1689
+ throw new Error(`Unsupported product type: ${productType}`);
1690
+ }
1691
+
1692
+ const ws = await loadWorkspace(projectRoot);
1693
+ if (!(await exists(ws.workspaceRoot))) {
1694
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1695
+ }
1696
+
1697
+ const currentState = {
1698
+ ...(ws.data.currentState ?? {}),
1699
+ captureMeta: {
1700
+ ...((ws.data.currentState ?? {}).captureMeta ?? {}),
1701
+ 'meta.productType': {
1702
+ source: 'user-confirmed',
1703
+ capturedAt: timestamp(),
1704
+ },
1705
+ },
1706
+ status: 'classified',
1707
+ productType,
1708
+ templatePack: productType,
1709
+ classifiedAt: timestamp(),
1710
+ };
1711
+ await writeJson(ws.paths.currentState, currentState);
1712
+ await appendWorkflowEvent(ws, 'classified', { productType });
1713
+ await appendDecision(ws, [
1714
+ `已锁定产品类型为 ${productType}。`,
1715
+ `模板包已设置为 ${productType}。`,
1716
+ ]);
1717
+ await appendProgress(ws, [
1718
+ `已将工作区分类为 ${productType}。`,
1719
+ ]);
1720
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(currentState));
1721
+
1722
+ return { ws, currentState };
1723
+ }
1724
+
1725
+ async function interviewWorkspace(projectRoot, requestedType = null) {
1726
+ const ws = await loadWorkspace(projectRoot);
1727
+ if (!(await exists(ws.workspaceRoot))) {
1728
+ throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
1729
+ }
1730
+
1731
+ if (requestedType && !isSupportedProductType(requestedType)) {
1732
+ throw new Error(`Unsupported product type: ${requestedType}`);
1733
+ }
1734
+
1735
+ const productType = requestedType ?? resolveCurrentProductType(ws);
1736
+ const sourceFiles = [ws.paths.baseIntake];
1737
+ if (productType === 'consumer') sourceFiles.push(ws.paths.consumerIntake);
1738
+ if (productType === 'b2b') sourceFiles.push(ws.paths.b2bIntake);
1739
+ if (productType === 'agent') sourceFiles.push(ws.paths.agentIntake);
1740
+
1741
+ const sourceContent = [];
1742
+ for (const sourceFile of sourceFiles) {
1743
+ const rel = path.relative(ws.workspaceRoot, sourceFile);
1744
+ const content = await readText(sourceFile);
1745
+ sourceContent.push(`## ${rel}
1746
+
1747
+ ${content}`);
1748
+ }
1749
+
1750
+ const currentState = {
1751
+ ...(ws.data.currentState ?? {}),
1752
+ status: 'interviewing',
1753
+ productType: productType ?? ws.data.currentState?.productType ?? null,
1754
+ templatePack: productType ?? resolveActiveTemplatePack(ws),
1755
+ interviewStartedAt: timestamp(),
1756
+ };
1757
+ await writeJson(ws.paths.currentState, currentState);
1758
+ await appendWorkflowEvent(ws, 'interview_started', {
1759
+ productType: currentState.productType,
1760
+ sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
1761
+ });
1762
+ await appendProgress(ws, [
1763
+ `已加载 ${productType ?? '未分类'} 的访谈问题。`,
1764
+ `来源文件: ${sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)).join(', ')}`,
1765
+ ]);
1766
+ await appendOpenQuestions(ws, [
1767
+ '我们要解决什么问题?',
1768
+ '主要用户是谁?',
1769
+ '成功是什么样?',
1770
+ '哪些内容明确不在范围内?',
1771
+ '我们希望 freeze 的第一个里程碑是什么?',
1772
+ ]);
1773
+ await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(currentState));
1774
+
1775
+ return {
1776
+ ws,
1777
+ productType,
1778
+ sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
1779
+ transcript: sourceContent.join('\n\n---\n\n'),
1780
+ currentState,
1781
+ };
1782
+ }
1783
+
1784
+
1785
+ export {
1786
+ captureWorkspace,
1787
+ clarifyWorkspace,
1788
+ classifyWorkspace,
1789
+ computeWorkspaceGuidance,
1790
+ diffWorkspace,
1791
+ historyWorkspace,
1792
+ interviewWorkspace,
1793
+ nextWorkspace,
1794
+ playgroundWorkspace,
1795
+ reviewWorkspace,
1796
+ synthesizeWorkspace
1797
+ };