prd-workflow-cli 1.4.1 → 2.0.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.
@@ -0,0 +1,424 @@
1
+ /**
2
+ * freeze-checks.js
3
+ *
4
+ * freeze 命令的前置检查模块
5
+ * 将 R1/R2 审视集成到 freeze 流程中
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const chalk = require('chalk');
11
+
12
+ /**
13
+ * 执行 B3 冻结前的所有检查
14
+ * @param {string} iterationDir - 迭代目录路径
15
+ * @returns {Object} { pass: boolean, results: Array, summary: string }
16
+ */
17
+ async function runPlanFreezeChecks(iterationDir) {
18
+ const results = [];
19
+
20
+ console.log(chalk.bold('\n📋 B3 规划冻结前置检查\n'));
21
+ console.log(chalk.gray('─'.repeat(50)));
22
+
23
+ // ===== 阶段 1: 文档存在性检查 =====
24
+ console.log(chalk.bold('\n📁 文档存在性检查\n'));
25
+
26
+ const bPlanPath = path.join(iterationDir, 'B_规划文档.md');
27
+ const bPlanExists = await fs.pathExists(bPlanPath);
28
+
29
+ results.push({
30
+ category: '文档存在性',
31
+ item: 'B_规划文档.md',
32
+ pass: bPlanExists,
33
+ message: bPlanExists ? '文件存在' : '文件不存在,请运行 prd plan create B'
34
+ });
35
+
36
+ printCheckResult('B_规划文档.md', bPlanExists);
37
+
38
+ // 如果文档不存在,提前返回
39
+ if (!bPlanExists) {
40
+ return {
41
+ pass: false,
42
+ results,
43
+ summary: '文档不完整,无法继续检查'
44
+ };
45
+ }
46
+
47
+ // ===== 阶段 2: 必填项检查 =====
48
+ console.log(chalk.bold('\n📝 必填项检查\n'));
49
+
50
+ const bPlanContent = await fs.readFile(bPlanPath, 'utf-8');
51
+
52
+ // B_规划文档 必填项检查
53
+ const bPlanChecks = [
54
+ { field: '启动检查', pattern: /\[x\].*问题真实存在/i },
55
+ { field: '核心问题', pattern: /要解决的问题[\s\S]*?(?=\n##|\n---|$)/i },
56
+ { field: '需求拆解', pattern: /REQ-\d{3}/i },
57
+ { field: 'PM 确认', pattern: /\[x\].*核心问题已明确/i }
58
+ ];
59
+
60
+ for (const check of bPlanChecks) {
61
+ const match = bPlanContent.match(check.pattern);
62
+ const hasContent = match && (check.pattern.toString().includes('[x]') ? true : match[0].length > 30);
63
+ results.push({
64
+ category: '必填项',
65
+ item: `B - ${check.field}`,
66
+ pass: hasContent,
67
+ message: hasContent ? '已填写' : `请在 B_规划文档 中完成「${check.field}」`
68
+ });
69
+ printCheckResult(`B - ${check.field}`, hasContent);
70
+ }
71
+
72
+ // ===== 阶段 3: R1 审视(5 维度) =====
73
+ console.log(chalk.bold('\n📊 R1 规划审视(5 维度)\n'));
74
+
75
+ const r1Checks = await runR1Review(b1Content, b2Content, iterationDir);
76
+ results.push(...r1Checks);
77
+
78
+ for (const check of r1Checks) {
79
+ printCheckResult(check.item, check.pass, check.message);
80
+ }
81
+
82
+ // ===== 汇总结果 =====
83
+ console.log(chalk.gray('\n' + '─'.repeat(50)));
84
+
85
+ const failures = results.filter(r => !r.pass);
86
+ const pass = failures.length === 0;
87
+
88
+ let summary;
89
+ if (pass) {
90
+ summary = '所有检查通过,可以执行冻结';
91
+ console.log(chalk.bold.green('\n✅ ' + summary + '\n'));
92
+ } else {
93
+ summary = `${failures.length} 项检查未通过`;
94
+ console.log(chalk.bold.red(`\n❌ ${summary}\n`));
95
+ console.log(chalk.yellow('未通过的检查项:\n'));
96
+ failures.forEach(f => {
97
+ console.log(` ⚠️ ${f.item}`);
98
+ console.log(chalk.gray(` ${f.message}\n`));
99
+ });
100
+ }
101
+
102
+ return { pass, results, summary };
103
+ }
104
+
105
+ /**
106
+ * 执行 R1 审视(5 维度)
107
+ */
108
+ async function runR1Review(b1Content, b2Content, iterationDir) {
109
+ const results = [];
110
+
111
+ // 读取 A 类文档用于对比
112
+ const baselineDir = path.join(path.dirname(iterationDir), '..', '01_产品基线');
113
+ let a2Content = '';
114
+ try {
115
+ const a2Path = path.join(baselineDir, 'A2_存量反馈与数据汇总.md');
116
+ if (await fs.pathExists(a2Path)) {
117
+ a2Content = await fs.readFile(a2Path, 'utf-8');
118
+ }
119
+ } catch (e) {
120
+ // 忽略读取错误
121
+ }
122
+
123
+ // 1. 目标清晰性
124
+ const hasGoal = /要解决的核心问题[\s\S]{20,}/.test(b1Content);
125
+ const hasMeasurable = /成功标准[\s\S]*?(提升|降低|达到|\d+%)/.test(b1Content);
126
+ results.push({
127
+ category: 'R1审视',
128
+ item: '1. 目标清晰性',
129
+ pass: hasGoal,
130
+ message: hasGoal
131
+ ? (hasMeasurable ? '目标明确且可衡量' : '目标明确,建议补充可衡量指标')
132
+ : '请在 B1 中明确描述核心问题'
133
+ });
134
+
135
+ // 2. 场景真实性
136
+ const hasScenario = /场景\d|触发条件|用户目标/.test(b1Content);
137
+ const hasA2Reference = /A2|用户反馈|真实反馈/.test(b1Content);
138
+ results.push({
139
+ category: 'R1审视',
140
+ item: '2. 场景真实性',
141
+ pass: hasScenario,
142
+ message: hasScenario
143
+ ? (hasA2Reference ? '场景真实,有用户反馈支撑' : '场景已描述,建议关联 A2 用户反馈')
144
+ : '请在 B1 中描述具体使用场景'
145
+ });
146
+
147
+ // 3. 现状一致性
148
+ const hasA1Reference = /A1|A0|现有功能|已上线/.test(b1Content);
149
+ results.push({
150
+ category: 'R1审视',
151
+ item: '3. 现状一致性',
152
+ pass: hasA1Reference,
153
+ message: hasA1Reference
154
+ ? '规划与现状文档一致'
155
+ : '建议在 B1 中引用 A0/A1 说明现状依据'
156
+ });
157
+
158
+ // 4. 范围收敛性
159
+ const hasNotDo = /不包含|明确不做|不做/.test(b1Content);
160
+ const hasScope = /首版包含|进入首版/.test(b2Content);
161
+ const hasPriority = /P0|P1|P2/.test(b2Content);
162
+ const scopePass = hasNotDo && hasScope && hasPriority;
163
+ results.push({
164
+ category: 'R1审视',
165
+ item: '4. 范围收敛性',
166
+ pass: scopePass,
167
+ message: scopePass
168
+ ? '范围边界清晰'
169
+ : `请确保:${!hasNotDo ? '说明不做什么、' : ''}${!hasScope ? '明确首版范围、' : ''}${!hasPriority ? '标注优先级' : ''}`
170
+ });
171
+
172
+ // 5. 版本化准备度
173
+ const hasRequirements = (b2Content.match(/需求项 #\d/g) || []).length;
174
+ const canVersion = hasRequirements >= 1 && hasScope;
175
+ results.push({
176
+ category: 'R1审视',
177
+ item: '5. 版本化准备度',
178
+ pass: canVersion,
179
+ message: canVersion
180
+ ? `可拆分为版本,共 ${hasRequirements} 个需求项`
181
+ : '请在 B2 中拆分需求项并标注首版范围'
182
+ });
183
+
184
+ return results;
185
+ }
186
+
187
+ /**
188
+ * 执行 C3 冻结前的所有检查
189
+ */
190
+ /**
191
+ * 执行 C3 冻结前的所有检查
192
+ */
193
+ async function runVersionFreezeChecks(iterationDir) {
194
+ const results = [];
195
+
196
+ console.log(chalk.bold('\n📋 C3 版本冻结前置检查 (自动 R2 审视)\n'));
197
+ console.log(chalk.gray('─'.repeat(50)));
198
+
199
+ // ===== 阶段 1: IT 完整性检查 =====
200
+ console.log(chalk.bold('\n📁 IT 文档检查\n'));
201
+
202
+ const b3Path = path.join(iterationDir, 'B3_规划冻结归档.md');
203
+ const itDir = path.join(iterationDir, 'IT');
204
+
205
+ const b3Exists = await fs.pathExists(b3Path);
206
+ let itExists = await fs.pathExists(itDir);
207
+ let itFolders = [];
208
+
209
+ if (itExists) {
210
+ const items = await fs.readdir(itDir);
211
+ itFolders = items.filter(name => name.startsWith('IT-'));
212
+ if (itFolders.length === 0) {
213
+ itExists = false;
214
+ }
215
+ }
216
+
217
+ results.push({
218
+ category: '文档准备',
219
+ item: 'B3_规划冻结归档.md',
220
+ pass: b3Exists,
221
+ message: b3Exists ? '规划已冻结' : '请先执行 prd plan freeze'
222
+ });
223
+
224
+ results.push({
225
+ category: '文档准备',
226
+ item: 'IT 用户故事',
227
+ pass: itExists,
228
+ message: itExists ? `发现 ${itFolders.length} 个 IT 故事` : '请先运行 prd it create 创建用户故事'
229
+ });
230
+
231
+ printCheckResult('B3_规划冻结归档.md', b3Exists);
232
+ printCheckResult('IT 用户故事', itExists, itExists ? `共 ${itFolders.length} 个` : '目录为空或不存在');
233
+
234
+ if (!b3Exists || !itExists) {
235
+ return {
236
+ pass: false,
237
+ results,
238
+ summary: '文档不完整,无法继续检查'
239
+ };
240
+ }
241
+
242
+ // 检查每个 IT 的文件完整性
243
+ let allFilesCompleted = true;
244
+ for (const folder of itFolders) {
245
+ const itPath = path.join(itDir, folder);
246
+ const itId = folder.split('-').slice(0, 2).join('-');
247
+ const bizPath = path.join(itPath, `${itId}-BIZ.md`);
248
+ const devPath = path.join(itPath, `${itId}-DEV.md`);
249
+
250
+ // 检查 BIZ
251
+ if (await fs.pathExists(bizPath)) {
252
+ const content = await fs.readFile(bizPath, 'utf-8');
253
+ const isDefault = content.includes('[用户角色]');
254
+ if (isDefault) {
255
+ allFilesCompleted = false;
256
+ results.push({ category: 'IT完整性', item: `${itId}-BIZ`, pass: false, message: '文件待填写' });
257
+ printCheckResult(`${itId}-BIZ.md`, false, '文件包含默认模板内容');
258
+ }
259
+ } else {
260
+ allFilesCompleted = false;
261
+ results.push({ category: 'IT完整性', item: `${itId}-BIZ`, pass: false, message: '文件缺失' });
262
+ printCheckResult(`${itId}-BIZ.md`, false, '文件不存在');
263
+ }
264
+
265
+ // 检查 DEV
266
+ if (await fs.pathExists(devPath)) {
267
+ const content = await fs.readFile(devPath, 'utf-8');
268
+ const isDefault = content.includes('<!-- 从 BIZ 复制 -->');
269
+ if (isDefault) {
270
+ allFilesCompleted = false;
271
+ results.push({ category: 'IT完整性', item: `${itId}-DEV`, pass: false, message: '文件待填写' });
272
+ printCheckResult(`${itId}-DEV.md`, false, '文件包含默认模板内容');
273
+ }
274
+ } else {
275
+ allFilesCompleted = false;
276
+ results.push({ category: 'IT完整性', item: `${itId}-DEV`, pass: false, message: '文件缺失' });
277
+ printCheckResult(`${itId}-DEV.md`, false, '文件不存在');
278
+ }
279
+ }
280
+
281
+ if (!allFilesCompleted) {
282
+ return {
283
+ pass: false,
284
+ results,
285
+ summary: 'IT 文档未填写完整'
286
+ };
287
+ }
288
+
289
+ // ===== 阶段 2: R2 审视(5 维度) =====
290
+ console.log(chalk.bold('\n📊 R2 版本审视(5 维度)\n'));
291
+
292
+ // 读取所有 IT 内容汇总
293
+ let allBizContent = '';
294
+ let allDevContent = '';
295
+ let hasUI = false;
296
+
297
+ for (const folder of itFolders) {
298
+ const itPath = path.join(itDir, folder);
299
+ const itId = folder.split('-').slice(0, 2).join('-');
300
+
301
+ allBizContent += await fs.readFile(path.join(itPath, `${itId}-BIZ.md`), 'utf-8') + '\n';
302
+ allDevContent += await fs.readFile(path.join(itPath, `${itId}-DEV.md`), 'utf-8') + '\n';
303
+
304
+ // 检查是否有 UI 原型文件
305
+ const uiDir = path.join(itPath, 'UI原型');
306
+ if (await fs.pathExists(uiDir)) {
307
+ const uis = await fs.readdir(uiDir);
308
+ if (uis.some(f => f.endsWith('.json') || f.endsWith('.html'))) {
309
+ hasUI = true;
310
+ }
311
+ }
312
+ }
313
+
314
+ const b3Content = await fs.readFile(b3Path, 'utf-8');
315
+ const r2Checks = await runR2Review(b3Content, allBizContent, allDevContent, hasUI);
316
+ results.push(...r2Checks);
317
+
318
+ for (const check of r2Checks) {
319
+ printCheckResult(check.item, check.pass, check.message);
320
+ }
321
+
322
+ // ===== 汇总结果 =====
323
+ console.log(chalk.gray('\n' + '─'.repeat(50)));
324
+
325
+ const failures = results.filter(r => !r.pass);
326
+ const pass = failures.length === 0;
327
+
328
+ let summary;
329
+ if (pass) {
330
+ summary = '所有检查通过,可以执行冻结';
331
+ console.log(chalk.bold.green('\n✅ ' + summary + '\n'));
332
+ } else {
333
+ summary = `${failures.length} 项检查未通过`;
334
+ console.log(chalk.bold.red(`\n❌ ${summary}\n`));
335
+ console.log(chalk.yellow('未通过的检查项:\n'));
336
+ failures.forEach(f => {
337
+ console.log(` ⚠️ ${f.item}`);
338
+ console.log(chalk.gray(` ${f.message}\n`));
339
+ });
340
+ }
341
+
342
+ return { pass, results, summary };
343
+ }
344
+
345
+ /**
346
+ * 执行 R2 审视(5 维度)
347
+ * 针对 IT 架构
348
+ */
349
+ async function runR2Review(b3Content, allBizContent, allDevContent, hasUI) {
350
+ const results = [];
351
+
352
+ // 1. 版本目标一致性
353
+ // 检查 BIZ 中是否包含场景描述
354
+ const hasScenario = /### 场景|触发条件/i.test(allBizContent);
355
+ results.push({
356
+ category: 'R2审视',
357
+ item: '1. 业务场景闭环',
358
+ pass: hasScenario,
359
+ message: hasScenario ? '已定义业务场景' : '请在 BIZ 文档中描述具体应用场景'
360
+ });
361
+
362
+ // 2. 范围偏移检查
363
+ // 检查是否有关联 B3 的痕迹
364
+ const hasTrace = /关联 BIZ|来源追溯/i.test(allDevContent) || /来源/i.test(allBizContent);
365
+ results.push({
366
+ category: 'R2审视',
367
+ item: '2. 规划范围一致性',
368
+ pass: hasTrace,
369
+ message: hasTrace ? '已包含来源追溯' : '建议在文档中明确与 B3 的关联'
370
+ });
371
+
372
+ // 3. 规划覆盖完整性
373
+ const hasAcceptance = /验收标准|### 4\. 验收/i.test(allBizContent);
374
+ results.push({
375
+ category: 'R2审视',
376
+ item: '3. 验收标准完整性',
377
+ pass: hasAcceptance,
378
+ message: hasAcceptance ? '已定义验收标准' : '请在 BIZ 文档中完善验收标准'
379
+ });
380
+
381
+ // 4. 需求粒度成熟度
382
+ const hasDetail = /功能描述|交互规则|状态变化/i.test(allDevContent);
383
+ const hasBoundary = /边界|异常|特殊情况/i.test(allBizContent + allDevContent);
384
+ results.push({
385
+ category: 'R2审视',
386
+ item: '4. 细节与边界',
387
+ pass: hasDetail,
388
+ message: hasDetail
389
+ ? (hasBoundary ? '细节与边界定义完整' : '有功能描述,建议补充边界/异常情况')
390
+ : '请在 DEV 文档中完善功能细节'
391
+ });
392
+
393
+ // 5. 进入执行准备度
394
+ // IT 架构下,UI 原型是加分项,但 DEV 必须有
395
+ const isReady = hasDetail && hasAcceptance;
396
+ results.push({
397
+ category: 'R2审视',
398
+ item: '5. 开发就绪状态',
399
+ pass: isReady,
400
+ // message: `${hasUI ? '包含 UI 原型,' : ''}技术规格已就绪`
401
+ message: isReady ? '技术规格已就绪' : '请确保完善验收标准和功能细节'
402
+ });
403
+
404
+ return results;
405
+ }
406
+
407
+ /**
408
+ * 打印检查结果
409
+ */
410
+ function printCheckResult(item, pass, detail = '') {
411
+ const icon = pass ? chalk.green('✓') : chalk.red('✗');
412
+ const status = pass ? chalk.green('通过') : chalk.red('未通过');
413
+ console.log(` ${icon} ${item}: ${status}`);
414
+ if (detail && !pass) {
415
+ console.log(chalk.gray(` → ${detail}`));
416
+ }
417
+ }
418
+
419
+ module.exports = {
420
+ runPlanFreezeChecks,
421
+ runVersionFreezeChecks,
422
+ runR1Review,
423
+ runR2Review
424
+ };