@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,692 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { preferSimplifiedChinese } from '../language-policy.js';
4
+ import { needsBusinessGuardrails } from '../prd-core.js';
5
+ import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './constants.js';
6
+ import { openPrdChangeRoot, openPrdDiscoveryConfigPath, readDiscoveryConfig } from './paths.js';
7
+
8
+ function cjoin(...parts) {
9
+ return path.join(...parts);
10
+ }
11
+
12
+ function exists(filePath) {
13
+ return fs.access(filePath).then(() => true).catch(() => false);
14
+ }
15
+
16
+ async function readJson(filePath) {
17
+ const text = await fs.readFile(filePath, 'utf8');
18
+ return JSON.parse(text);
19
+ }
20
+
21
+ async function writeText(filePath, text) {
22
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
23
+ await fs.writeFile(filePath, text, 'utf8');
24
+ }
25
+
26
+ async function writeJson(filePath, value) {
27
+ await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
28
+ }
29
+
30
+ function slugify(value, fallback = 'item') {
31
+ const slug = String(value ?? '')
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, '-')
34
+ .replace(/^-+|-+$/g, '')
35
+ .slice(0, 80);
36
+ return slug || fallback;
37
+ }
38
+
39
+ function scalar(value, fallback = '待补充') {
40
+ if (value === null || value === undefined) {
41
+ return fallback;
42
+ }
43
+ const text = String(value).trim();
44
+ return text || fallback;
45
+ }
46
+
47
+ function arrayValue(value) {
48
+ if (Array.isArray(value)) {
49
+ return value.map((item) => scalar(item, '')).filter(Boolean);
50
+ }
51
+ if (value === null || value === undefined || String(value).trim() === '') {
52
+ return [];
53
+ }
54
+ return [String(value).trim()];
55
+ }
56
+
57
+ function bullets(items, fallback = ['待补充']) {
58
+ const values = arrayValue(items);
59
+ return (values.length > 0 ? values : fallback).map((item) => `- ${item}`).join('\n');
60
+ }
61
+
62
+ function safeRequirementTitle(value, fallback) {
63
+ return scalar(value, fallback).replace(/\s+/g, ' ').slice(0, 120);
64
+ }
65
+
66
+ function trimTaskText(value) {
67
+ return scalar(value, '')
68
+ .replace(/\s+/g, ' ')
69
+ .replace(/[。;;,,]+$/g, '')
70
+ .trim();
71
+ }
72
+
73
+ function normalizeTaskSemanticKey(value) {
74
+ return trimTaskText(value)
75
+ .toLowerCase()
76
+ .replace(/[。,“”"'`~!@#$%^&*()_+=\-[\]{}|\\;:,.<>/?、]/g, ' ')
77
+ .replace(/\b(users?|system|current|primary|flow|requirement|acceptance|non functional|non-functional)\b/g, ' ')
78
+ .replace(/(用户|系统|当前|主工作树|至少|已经|完成|提供|新增|展示|支持|查看|看到|可以|能够|需要|实现|验证|回归|打通|落地|接通|补齐|对齐|并且|以及|中的|基础|首版|可见|目标|要求)/g, ' ')
79
+ .replace(/\s+/g, ' ')
80
+ .trim();
81
+ }
82
+
83
+ function summarizeTaskItems(items, limit = 2, maxLength = 56) {
84
+ const values = arrayValue(items).map((item) => trimTaskText(item)).filter(Boolean);
85
+ if (values.length === 0) {
86
+ return '当前需求闭环';
87
+ }
88
+ const shown = [];
89
+ for (const item of values) {
90
+ const candidate = [...shown, item].join(' / ');
91
+ if (shown.length >= limit || candidate.length > maxLength) {
92
+ break;
93
+ }
94
+ shown.push(item);
95
+ }
96
+ if (shown.length === 0) {
97
+ shown.push(values[0].slice(0, maxLength));
98
+ }
99
+ return values.length > shown.length ? `${shown.join(' / ')} 等 ${values.length} 项` : shown.join(' / ');
100
+ }
101
+
102
+ function buildDoneText(prefix, item) {
103
+ return `${prefix}${trimTaskText(item)}`;
104
+ }
105
+
106
+ function cleanImplementationTitle(item) {
107
+ const text = trimTaskText(item)
108
+ .replace(/^系统需要/g, '')
109
+ .replace(/^系统应/g, '')
110
+ .replace(/^系统会/g, '')
111
+ .replace(/^系统/g, '')
112
+ .replace(/^需要/g, '')
113
+ .replace(/^当前主工作树中/g, '')
114
+ .trim();
115
+ if (!text) {
116
+ return '补齐当前需求实现';
117
+ }
118
+ if (/^(在|提供|保留|展示|支持|完成|补齐|同步|避免|接入|接通|收口|更新|修复|统一)/.test(text)) {
119
+ return text;
120
+ }
121
+ if (/^(Add|Apply|Build|Collect|Create|Deny|Display|Expose|Fetch|Handle|Implement|Keep|Load|Manage|Persist|Port|Prepare|Prevent|Protect|Record|Refresh|Render|Reuse|Route|Return|Save|Show|Submit|Support|Sync|Track|Update|Validate|Wire)/i.test(text)) {
122
+ return text;
123
+ }
124
+ return `实现${text}`;
125
+ }
126
+
127
+ function buildVerificationTitle(item) {
128
+ const text = trimTaskText(item);
129
+ if (!text) {
130
+ return '验证当前需求闭环';
131
+ }
132
+ if (/^(验证|回归|确认|检查|Compare|Verify|Test|Review)/i.test(text)) {
133
+ return text;
134
+ }
135
+ return `验证${text}`;
136
+ }
137
+
138
+ function chunkItems(items, maxItemsPerChunk = 2) {
139
+ const chunks = [];
140
+ for (let index = 0; index < items.length; index += maxItemsPerChunk) {
141
+ chunks.push(items.slice(index, index + maxItemsPerChunk));
142
+ }
143
+ return chunks;
144
+ }
145
+
146
+ const DEFAULT_EXECUTION_VERIFY_COMMAND = 'openprd run . --verify';
147
+
148
+ const ARCHITECTURE_TASK_DEFINITIONS = [
149
+ {
150
+ key: 'shared-contracts',
151
+ pattern: /(shared contracts?|host methods?|host api|ipc|preload|schema|contract|entitlement|接口约定|共享契约|契约|协议|类型)/i,
152
+ title: '对齐共享契约与 Host API 调用边界',
153
+ done: '共享契约、Host API 和上下游调用边界已经对齐,后续功能可以直接接线。',
154
+ },
155
+ {
156
+ key: 'domain-data',
157
+ pattern: /(domain|service|gateway|adapter|repository|storage|snapshot|backend|billing|orders?|entitlement|sync|cache|状态快照|领域服务|网关|适配器|仓储|后端|订单|会员|额度|数据)/i,
158
+ title: '补齐领域服务、数据读取与状态同步',
159
+ done: '领域服务、数据读取和状态同步已经接通,界面不会停留在假可见状态。',
160
+ },
161
+ {
162
+ key: 'main-runtime',
163
+ pattern: /(main process|electron main|main\b|window|daemon|tray|主进程|窗口|守护|后台)/i,
164
+ title: '补齐主进程、窗口与后台接线',
165
+ done: '主进程事件、窗口入口或后台能力的接线已经补齐,不会因为运行时边界遗漏而失效。',
166
+ },
167
+ {
168
+ key: 'renderer-surface',
169
+ pattern: /(renderer|view|ui|route|navigation|page|dialog|modal|surface|sidebar|entry|renderer 入口|界面|页面|弹窗|弹层|入口|导航|菜单|路由|侧边栏)/i,
170
+ title: '接通界面入口、导航与页面挂载',
171
+ done: '用户可以从正确入口进入对应界面,页面挂载与状态收尾已经接通。',
172
+ },
173
+ {
174
+ key: 'diagnostics',
175
+ pattern: /(diagnostic|log|logging|trace|telemetry|monitor|诊断|日志|追踪|监控|埋点)/i,
176
+ title: '补齐日志、诊断与可观测性信号',
177
+ done: '关键阶段日志和诊断信号已经保留,后续排查可以直接定位到断点。',
178
+ },
179
+ ];
180
+
181
+ function resolveCapability(snapshot) {
182
+ const productType = slugify(snapshot.productType ?? snapshot.templatePack ?? 'product', 'product');
183
+ return `${productType}-requirements`;
184
+ }
185
+
186
+ function buildProposal({ changeId, capability, snapshot }) {
187
+ const sections = snapshot.sections ?? {};
188
+ return [
189
+ `# ${scalar(snapshot.title, changeId)}`,
190
+ '',
191
+ '## 背景与原因',
192
+ '',
193
+ scalar(sections.problem?.problemStatement, '需要把当前产品需求转化为可执行的规格变更。'),
194
+ '',
195
+ '## 变更内容',
196
+ '',
197
+ bullets([
198
+ ...arrayValue(sections.scope?.inScope),
199
+ ...arrayValue(sections.requirements?.functional),
200
+ ...arrayValue(sections.goals?.acceptanceGoals),
201
+ ], ['根据当前 PRD 生成 OpenPrd 管理的规格增量。']),
202
+ '',
203
+ '## 能力范围',
204
+ '',
205
+ `- \`${capability}\`: ${scalar(snapshot.title, '产品行为')} 需求。`,
206
+ '',
207
+ '## 影响范围',
208
+ '',
209
+ bullets([
210
+ ...arrayValue(sections.users?.primaryUsers).map((item) => `主要用户: ${item}`),
211
+ ...arrayValue(sections.businessGuardrails?.costDrivers).map((item) => `成本来源: ${item}`),
212
+ ...arrayValue(sections.businessGuardrails?.usageLimits).map((item) => `额度限制: ${item}`),
213
+ ...arrayValue(sections.constraints?.dependencies).map((item) => `依赖: ${item}`),
214
+ ...arrayValue(sections.risks?.risks).map((item) => `风险: ${item}`),
215
+ ], ['Agent 可以通过 OpenPrd 从 PRD 继续推进到 specs、tasks、validation 和 execution。']),
216
+ '',
217
+ ].join('\n');
218
+ }
219
+
220
+ function buildDesign({ snapshot }) {
221
+ const sections = snapshot.sections ?? {};
222
+ return [
223
+ '# 设计',
224
+ '',
225
+ '## 背景',
226
+ '',
227
+ scalar(sections.problem?.whyNow, '根据最新 OpenPrd 快照生成。'),
228
+ '',
229
+ '## 目标',
230
+ '',
231
+ bullets(sections.goals?.goals),
232
+ '',
233
+ '## 范围',
234
+ '',
235
+ bullets(sections.scope?.inScope),
236
+ '',
237
+ '## 约束',
238
+ '',
239
+ bullets([
240
+ ...arrayValue(sections.constraints?.technical),
241
+ ...arrayValue(sections.constraints?.compliance),
242
+ ...arrayValue(sections.constraints?.dependencies),
243
+ ]),
244
+ '',
245
+ '## 业务护栏',
246
+ '',
247
+ bullets([
248
+ ...arrayValue(sections.businessGuardrails?.costDrivers).map((item) => `成本来源: ${item}`),
249
+ ...arrayValue(sections.businessGuardrails?.usageLimits).map((item) => `额度限制: ${item}`),
250
+ ...arrayValue(sections.businessGuardrails?.abusePrevention).map((item) => `滥用防护: ${item}`),
251
+ ...arrayValue(sections.businessGuardrails?.monitoringSignals).map((item) => `监控信号: ${item}`),
252
+ ...arrayValue(sections.businessGuardrails?.alertThresholds).map((item) => `报警阈值: ${item}`),
253
+ ...arrayValue(sections.businessGuardrails?.stopLossActions).map((item) => `止损动作: ${item}`),
254
+ ]),
255
+ '',
256
+ '## 风险与开放问题',
257
+ '',
258
+ bullets([
259
+ ...arrayValue(sections.risks?.assumptions).map((item) => `假设: ${item}`),
260
+ ...arrayValue(sections.risks?.risks).map((item) => `风险: ${item}`),
261
+ ...arrayValue(sections.risks?.openQuestions).map((item) => `问题: ${item}`),
262
+ ]),
263
+ '',
264
+ ].join('\n');
265
+ }
266
+
267
+ export function buildSpec({ snapshot }) {
268
+ const sections = snapshot.sections ?? {};
269
+ const title = safeRequirementTitle(
270
+ preferSimplifiedChinese(snapshot.title, '当前 PRD 描述的产品行为'),
271
+ '当前 PRD 描述的产品行为',
272
+ );
273
+ const acceptanceSource = arrayValue(sections.goals?.acceptanceGoals)[0]
274
+ ?? arrayValue(sections.requirements?.functional)[0];
275
+ const flow = preferSimplifiedChinese(arrayValue(sections.scenarios?.primaryFlows)[0], '主要用户完成主流程');
276
+ const acceptance = preferSimplifiedChinese(acceptanceSource, '预期产品结果得到满足');
277
+ const edgeCase = preferSimplifiedChinese(arrayValue(sections.scenarios?.edgeCases)[0], '出现边界情况');
278
+ const failureMode = preferSimplifiedChinese(arrayValue(sections.scenarios?.failureModes)[0], '出现失败模式');
279
+
280
+ return [
281
+ '## 新增需求',
282
+ '',
283
+ `### 需求:${title}`,
284
+ preferSimplifiedChinese(sections.problem?.problemStatement, '生成的能力应保持最新 OpenPrd 快照描述的行为。'),
285
+ '',
286
+ '#### 场景:主流程成功',
287
+ `- **当** ${flow}`,
288
+ `- **则** ${acceptance}`,
289
+ '',
290
+ '#### 场景:边界情况保持可见',
291
+ `- **当** ${edgeCase}`,
292
+ '- **则** 产品应保持该情况明确可见,以支持实现和验证',
293
+ '',
294
+ '#### 场景:失败模式得到处理',
295
+ `- **当** ${failureMode}`,
296
+ '- **则** 产品应提供有边界且可评审的结果',
297
+ '',
298
+ ].join('\n');
299
+ }
300
+
301
+ function inferArchitectureTasks(snapshot) {
302
+ const sections = snapshot.sections ?? {};
303
+ const lines = [
304
+ ...arrayValue(sections.constraints?.technical),
305
+ ...arrayValue(sections.constraints?.dependencies),
306
+ ];
307
+ if (lines.length === 0) {
308
+ return [];
309
+ }
310
+
311
+ return ARCHITECTURE_TASK_DEFINITIONS
312
+ .map((definition, index) => ({
313
+ definition,
314
+ index,
315
+ matches: lines.filter((line) => definition.pattern.test(line)),
316
+ }))
317
+ .filter((item) => item.matches.length > 0)
318
+ .sort((left, right) => right.matches.length - left.matches.length || left.index - right.index)
319
+ .map(({ definition, matches }) => ({
320
+ key: definition.key,
321
+ type: 'implementation',
322
+ title: definition.title,
323
+ done: `${definition.done} 涉及: ${summarizeTaskItems(matches, 2, 72)}。`,
324
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
325
+ phase: 'architecture',
326
+ }));
327
+ }
328
+
329
+ function buildRequirementImplementationTasks(snapshot) {
330
+ const sections = snapshot.sections ?? {};
331
+ const sourceItems = arrayValue(sections.requirements?.functional);
332
+ const fallbackScopeItems = arrayValue(sections.scope?.inScope);
333
+ const implementationItems = sourceItems.length > 0 ? sourceItems : fallbackScopeItems;
334
+ const tasks = [];
335
+ const seen = new Set();
336
+
337
+ for (const item of implementationItems) {
338
+ const key = normalizeTaskSemanticKey(item);
339
+ if (!key || seen.has(key)) {
340
+ continue;
341
+ }
342
+ seen.add(key);
343
+ tasks.push({
344
+ key,
345
+ type: 'implementation',
346
+ title: cleanImplementationTitle(item),
347
+ done: buildDoneText('已完成:', item),
348
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
349
+ phase: 'implementation',
350
+ });
351
+ }
352
+
353
+ return tasks;
354
+ }
355
+
356
+ function buildFlowIntegrationTasks(snapshot) {
357
+ const sections = snapshot.sections ?? {};
358
+ const flows = arrayValue(sections.scenarios?.primaryFlows);
359
+ if (flows.length === 0) {
360
+ return [];
361
+ }
362
+ return [{
363
+ key: `integration:${summarizeTaskItems(flows, 2, 48)}`,
364
+ type: 'implementation',
365
+ title: `打通主流程闭环:${summarizeTaskItems(flows, 2, 56)}`,
366
+ done: `主流程关键节点已经打通,用户可以按预期从入口走到结果收尾。涉及: ${summarizeTaskItems(flows, 2, 72)}。`,
367
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
368
+ phase: 'integration',
369
+ }];
370
+ }
371
+
372
+ function buildAcceptanceVerificationTasks(snapshot) {
373
+ const sections = snapshot.sections ?? {};
374
+ return arrayValue(sections.goals?.acceptanceGoals).map((item) => ({
375
+ key: `acceptance:${normalizeTaskSemanticKey(item)}`,
376
+ type: 'verification',
377
+ title: buildVerificationTitle(item),
378
+ done: buildDoneText('已验证:', item),
379
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
380
+ phase: 'verification',
381
+ }));
382
+ }
383
+
384
+ function buildNonFunctionalVerificationTasks(snapshot) {
385
+ const sections = snapshot.sections ?? {};
386
+ const nonFunctionalChunks = chunkItems(arrayValue(sections.requirements?.nonFunctional), 2).map((items, index) => ({
387
+ key: `non-functional:${index}:${summarizeTaskItems(items, 2, 36)}`,
388
+ type: 'verification',
389
+ title: `回归非功能约束:${summarizeTaskItems(items, 2, 56)}`,
390
+ done: `非功能约束已经回归确认。涉及: ${summarizeTaskItems(items, 2, 72)}。`,
391
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
392
+ phase: 'verification',
393
+ }));
394
+ const edgeAndFailure = [
395
+ ...arrayValue(sections.scenarios?.edgeCases).map((item) => `边界情况:${trimTaskText(item)}`),
396
+ ...arrayValue(sections.scenarios?.failureModes).map((item) => `失败处理:${trimTaskText(item)}`),
397
+ ];
398
+ const edgeTasks = edgeAndFailure.length > 0
399
+ ? [{
400
+ key: `edge:${summarizeTaskItems(edgeAndFailure, 2, 36)}`,
401
+ type: 'verification',
402
+ title: `回归边界条件与失败处理:${summarizeTaskItems(edgeAndFailure, 2, 56)}`,
403
+ done: `边界条件与失败处理已经回归确认。涉及: ${summarizeTaskItems(edgeAndFailure, 2, 72)}。`,
404
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
405
+ phase: 'verification',
406
+ }]
407
+ : [];
408
+ return [...nonFunctionalChunks, ...edgeTasks];
409
+ }
410
+
411
+ function buildTaskItems({ changeId, snapshot, capability }) {
412
+ const candidates = [
413
+ {
414
+ key: 'governance:spec-review',
415
+ type: 'governance',
416
+ title: '评审生成的 spec 覆盖',
417
+ done: `生成的 ${capability} spec 符合 PRD 意图`,
418
+ verify: `openprd change . --validate --change ${changeId}`,
419
+ phase: 'governance-start',
420
+ },
421
+ ...inferArchitectureTasks(snapshot),
422
+ ...buildRequirementImplementationTasks(snapshot),
423
+ ...buildFlowIntegrationTasks(snapshot),
424
+ ...buildAcceptanceVerificationTasks(snapshot),
425
+ ...buildNonFunctionalVerificationTasks(snapshot),
426
+ ...(needsBusinessGuardrails(snapshot)
427
+ ? [
428
+ {
429
+ key: 'verification:guardrail-usage',
430
+ type: 'verification',
431
+ title: '验证成本与额度护栏',
432
+ done: '已验证免费、试用或低权限用户不能绕过额度、并发、频率或总量限制',
433
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
434
+ phase: 'verification',
435
+ },
436
+ {
437
+ key: 'verification:guardrail-abuse',
438
+ type: 'verification',
439
+ title: '验证滥用与越权路径',
440
+ done: '已覆盖重复请求、并发请求、越权身份和异常恢复等负向场景',
441
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
442
+ phase: 'verification',
443
+ },
444
+ {
445
+ key: 'verification:guardrail-monitoring',
446
+ type: 'verification',
447
+ title: '验证成本监控、报警和止损',
448
+ done: '已确认用量或成本信号、报警阈值和人工/自动止损动作可执行',
449
+ verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
450
+ phase: 'verification',
451
+ },
452
+ ]
453
+ : []),
454
+ {
455
+ key: 'documentation:docs-basic',
456
+ type: 'documentation',
457
+ title: '维护 docs/basic 项目基础文档',
458
+ done: '已检查 docs/basic 是否缺失或因本次需求、流程、结构、依赖、产品行为变化而过期;若涉及后端、脚本、Agent 或工具链变更,已同步评估 CLI 与 API 接入面,并在 backend-structure.md 中记录事实或不适用原因;需要更新的基础文档已同步',
459
+ verify: 'openprd standards . --verify',
460
+ phase: 'documentation',
461
+ },
462
+ {
463
+ key: 'documentation:manuals',
464
+ type: 'documentation',
465
+ title: '更新文件说明书和文件夹 README',
466
+ done: '本次变更涉及的文件说明书和文件夹 README 已检查;缺失的已补齐,过期的已更新',
467
+ verify: 'openprd standards . --verify',
468
+ phase: 'documentation',
469
+ },
470
+ {
471
+ key: 'governance:final-validate',
472
+ type: 'governance',
473
+ title: '运行 OpenPrd spec 校验',
474
+ done: '生成的 change 通过 OpenPrd 校验',
475
+ verify: `openprd change . --validate --change ${changeId}`,
476
+ phase: 'governance-end',
477
+ },
478
+ ];
479
+
480
+ const deduped = [];
481
+ const seen = new Set();
482
+ for (const item of candidates) {
483
+ const { key, title } = item;
484
+ const dedupeKey = key ?? `${item.phase}:${normalizeTaskSemanticKey(title)}`;
485
+ if (!dedupeKey) {
486
+ continue;
487
+ }
488
+ if (seen.has(dedupeKey)) {
489
+ continue;
490
+ }
491
+ seen.add(dedupeKey);
492
+ deduped.push(item);
493
+ }
494
+
495
+ const tasks = deduped.map((item, index) => ({
496
+ id: `T001.${String(index + 1).padStart(2, '0')}`,
497
+ title: item.title,
498
+ type: item.type,
499
+ phase: item.phase,
500
+ done: item.done,
501
+ verify: item.verify ?? (item.type === 'documentation' ? 'openprd standards . --verify' : DEFAULT_EXECUTION_VERIFY_COMMAND),
502
+ deps: [],
503
+ }));
504
+
505
+ const phaseTasks = {
506
+ governanceStart: tasks.filter((task) => task.phase === 'governance-start'),
507
+ architecture: tasks.filter((task) => task.phase === 'architecture'),
508
+ implementation: tasks.filter((task) => task.phase === 'implementation'),
509
+ integration: tasks.filter((task) => task.phase === 'integration'),
510
+ verification: tasks.filter((task) => task.phase === 'verification'),
511
+ documentation: tasks.filter((task) => task.phase === 'documentation'),
512
+ governanceEnd: tasks.filter((task) => task.phase === 'governance-end'),
513
+ };
514
+
515
+ const anchorTask = phaseTasks.governanceStart.at(-1) ?? null;
516
+ let previousId = anchorTask?.id ?? null;
517
+ for (const task of phaseTasks.architecture) {
518
+ task.deps = previousId ? [previousId] : [];
519
+ previousId = task.id;
520
+ }
521
+
522
+ const implementationAnchor = previousId ?? anchorTask?.id ?? null;
523
+ previousId = implementationAnchor;
524
+ for (const task of phaseTasks.implementation) {
525
+ task.deps = previousId ? [previousId] : [];
526
+ previousId = task.id;
527
+ }
528
+
529
+ const integrationAnchor = previousId ?? implementationAnchor ?? anchorTask?.id ?? null;
530
+ previousId = integrationAnchor;
531
+ for (const task of phaseTasks.integration) {
532
+ task.deps = previousId ? [previousId] : [];
533
+ previousId = task.id;
534
+ }
535
+
536
+ const verificationAnchor = previousId ?? integrationAnchor ?? implementationAnchor ?? anchorTask?.id ?? null;
537
+ previousId = verificationAnchor;
538
+ for (const task of phaseTasks.verification) {
539
+ task.deps = previousId ? [previousId] : [];
540
+ previousId = task.id;
541
+ }
542
+
543
+ const documentationAnchor = previousId ?? verificationAnchor ?? integrationAnchor ?? implementationAnchor ?? anchorTask?.id ?? null;
544
+ previousId = documentationAnchor;
545
+ for (const task of phaseTasks.documentation) {
546
+ task.deps = previousId ? [previousId] : [];
547
+ previousId = task.id;
548
+ }
549
+
550
+ const governanceEndAnchor = previousId ?? documentationAnchor ?? verificationAnchor ?? integrationAnchor ?? implementationAnchor ?? anchorTask?.id ?? null;
551
+ previousId = governanceEndAnchor;
552
+ for (const task of phaseTasks.governanceEnd) {
553
+ task.deps = previousId ? [previousId] : [];
554
+ previousId = task.id;
555
+ }
556
+
557
+ return tasks.map(({ phase, ...task }) => task);
558
+ }
559
+
560
+ function taskFileName(index) {
561
+ return index === 0 ? 'tasks.md' : `tasks-${String(index + 1).padStart(3, '0')}.md`;
562
+ }
563
+
564
+ function renderTaskFiles(tasks, maxItemsPerFile) {
565
+ const chunks = [];
566
+ for (let index = 0; index < tasks.length; index += maxItemsPerFile) {
567
+ chunks.push(tasks.slice(index, index + maxItemsPerFile));
568
+ }
569
+
570
+ return chunks.map((chunk, chunkIndex) => {
571
+ const nextFileName = chunkIndex < chunks.length - 1 ? taskFileName(chunkIndex + 1) : null;
572
+ const lines = [
573
+ '# 任务',
574
+ '',
575
+ ];
576
+
577
+ for (const task of chunk) {
578
+ lines.push(`- [ ] ${task.id} ${task.title}`);
579
+ lines.push(` - type: ${task.type}`);
580
+ if (task.deps.length > 0) {
581
+ lines.push(` - deps: ${task.deps.join(', ')}`);
582
+ }
583
+ lines.push(` - done: ${task.done}`);
584
+ lines.push(` - verify: ${task.verify}`);
585
+ lines.push('');
586
+ }
587
+
588
+ if (nextFileName) {
589
+ lines.push(`- [ ] Continue with \`${nextFileName}\` after completing this file.`);
590
+ lines.push('');
591
+ }
592
+
593
+ return {
594
+ fileName: taskFileName(chunkIndex),
595
+ text: lines.join('\n'),
596
+ };
597
+ });
598
+ }
599
+
600
+ async function readTaskMax(projectRoot) {
601
+ const discoveryConfig = await readDiscoveryConfig(projectRoot, readJson);
602
+ const maxItemsPerFile = Number(discoveryConfig?.taskSharding?.maxItemsPerFile ?? OPENSPEC_TASK_MAX_ITEMS_PER_FILE);
603
+ return Number.isInteger(maxItemsPerFile) && maxItemsPerFile > 0 ? maxItemsPerFile : OPENSPEC_TASK_MAX_ITEMS_PER_FILE;
604
+ }
605
+
606
+ async function writeDiscoveryConfig(projectRoot, changeId) {
607
+ const configPath = openPrdDiscoveryConfigPath(projectRoot);
608
+ const current = await readJson(configPath).catch(() => ({}));
609
+ await writeJson(configPath, {
610
+ ...current,
611
+ activeChange: changeId,
612
+ taskSharding: {
613
+ maxItemsPerFile: current?.taskSharding?.maxItemsPerFile ?? OPENSPEC_TASK_MAX_ITEMS_PER_FILE,
614
+ handoffRequired: current?.taskSharding?.handoffRequired ?? true,
615
+ firstFile: current?.taskSharding?.firstFile ?? 'tasks.md',
616
+ nextFilePattern: current?.taskSharding?.nextFilePattern ?? 'tasks-###.md',
617
+ },
618
+ taskMetadata: {
619
+ stableIdPattern: current?.taskMetadata?.stableIdPattern ?? 'T###.##',
620
+ required: current?.taskMetadata?.required ?? ['done', 'verify'],
621
+ optional: current?.taskMetadata?.optional ?? ['deps', 'type'],
622
+ dependencyOrder: current?.taskMetadata?.dependencyOrder ?? 'dependencies must appear before dependents',
623
+ },
624
+ });
625
+ }
626
+
627
+ export async function generateOpenSpecChangeWorkspace(projectRoot, options = {}) {
628
+ const snapshot = options.snapshot;
629
+ if (!snapshot) {
630
+ throw new Error('生成 OpenPrd change 需要 PRD 快照。');
631
+ }
632
+
633
+ const changeId = slugify(options.change ?? snapshot.title, 'openprd-generated-change');
634
+ const capability = resolveCapability(snapshot);
635
+ const changeDir = cjoin(openPrdChangeRoot(projectRoot), changeId);
636
+ const files = [
637
+ {
638
+ path: cjoin(changeDir, '.openprd.yaml'),
639
+ text: [
640
+ 'schema: openprd.change.v1',
641
+ `generatedFrom: ${snapshot.versionId}`,
642
+ '',
643
+ ].join('\n'),
644
+ },
645
+ {
646
+ path: cjoin(changeDir, 'proposal.md'),
647
+ text: buildProposal({ changeId, capability, snapshot }),
648
+ },
649
+ {
650
+ path: cjoin(changeDir, 'design.md'),
651
+ text: buildDesign({ snapshot }),
652
+ },
653
+ {
654
+ path: cjoin(changeDir, 'specs', capability, 'spec.md'),
655
+ text: buildSpec({ snapshot }),
656
+ },
657
+ ];
658
+ const tasks = buildTaskItems({ changeId, snapshot, capability });
659
+ const taskFiles = renderTaskFiles(tasks, await readTaskMax(projectRoot));
660
+
661
+ for (const file of taskFiles) {
662
+ files.push({
663
+ path: cjoin(changeDir, file.fileName),
664
+ text: file.text,
665
+ });
666
+ }
667
+
668
+ const existing = [];
669
+ for (const file of files) {
670
+ if (await exists(file.path)) {
671
+ existing.push(path.relative(projectRoot, file.path));
672
+ }
673
+ }
674
+ if (existing.length > 0 && !options.force) {
675
+ throw new Error(`OpenPrd change 已存在生成文件;请使用 --force 覆盖: ${existing.join(', ')}`);
676
+ }
677
+
678
+ for (const file of files) {
679
+ await writeText(file.path, file.text);
680
+ }
681
+ await writeDiscoveryConfig(projectRoot, changeId);
682
+
683
+ return {
684
+ ok: true,
685
+ projectRoot,
686
+ changeId,
687
+ changeDir,
688
+ capability,
689
+ files: files.map((file) => path.relative(projectRoot, file.path)),
690
+ taskCount: tasks.length,
691
+ };
692
+ }