@openprd/cli 0.1.1 → 0.1.8

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 (137) hide show
  1. package/.openprd/README.md +43 -69
  2. package/.openprd/README_EN.md +84 -0
  3. package/.openprd/benchmarks/index.md +7 -0
  4. package/.openprd/benchmarks/sources.yaml +25 -3
  5. package/.openprd/discovery/config.json +16 -2
  6. package/.openprd/engagements/active/flows.md +19 -14
  7. package/.openprd/engagements/active/handoff.md +11 -4
  8. package/.openprd/engagements/active/prd.md +99 -71
  9. package/.openprd/engagements/active/review.html +4 -4
  10. package/.openprd/engagements/active/roles.md +9 -8
  11. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
  12. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
  13. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
  14. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
  15. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
  16. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
  17. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
  18. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
  19. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
  20. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
  21. package/.openprd/knowledge/index.json +44 -4
  22. package/.openprd/reviews/v0001.html +195 -129
  23. package/.openprd/reviews/v0002.html +1150 -0
  24. package/.openprd/reviews/v0003.html +1150 -0
  25. package/.openprd/reviews/v0004.html +1150 -0
  26. package/.openprd/reviews/v0005.html +1150 -0
  27. package/.openprd/standards/config.json +12 -9
  28. package/.openprd/state/changes.json +17 -2
  29. package/.openprd/state/current.json +399 -63
  30. package/.openprd/state/release-ledger.json +344 -0
  31. package/.openprd/state/version-index.json +52 -0
  32. package/.openprd/state/versions/v0002.json +264 -0
  33. package/.openprd/state/versions/v0002.md +183 -0
  34. package/.openprd/state/versions/v0003.json +269 -0
  35. package/.openprd/state/versions/v0003.md +188 -0
  36. package/.openprd/state/versions/v0004.json +274 -0
  37. package/.openprd/state/versions/v0004.md +193 -0
  38. package/.openprd/state/versions/v0005.json +299 -0
  39. package/.openprd/state/versions/v0005.md +189 -0
  40. package/.openprd/templates/agent/intake.md +5 -4
  41. package/.openprd/templates/b2b/intake.md +5 -4
  42. package/.openprd/templates/base/intake.md +10 -4
  43. package/.openprd/templates/company/README.md +9 -7
  44. package/.openprd/templates/company/README_EN.md +12 -0
  45. package/.openprd/templates/consumer/intake.md +5 -4
  46. package/.openprd/templates/industry/README.md +12 -10
  47. package/.openprd/templates/industry/README_EN.md +18 -0
  48. package/.openprd/templates/project/README.md +11 -9
  49. package/.openprd/templates/project/README_EN.md +16 -0
  50. package/.openprd/templates/session/README.md +11 -9
  51. package/.openprd/templates/session/README_EN.md +16 -0
  52. package/AGENTS.md +12 -8
  53. package/README.md +399 -438
  54. package/README_CN.md +4 -578
  55. package/README_EN.md +850 -0
  56. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  57. package/docs/assets/openprd-requirement-routing-en.svg +102 -0
  58. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  59. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  60. package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
  61. package/package.json +6 -2
  62. package/scripts/dev-check-wrapup-copy.mjs +110 -0
  63. package/scripts/openprd-github-release-notes.mjs +99 -0
  64. package/scripts/quality-perf-check.mjs +203 -0
  65. package/skills/openprd-benchmark-router/SKILL.md +1 -0
  66. package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
  67. package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
  68. package/skills/openprd-discovery-loop/SKILL.md +2 -2
  69. package/skills/openprd-harness/SKILL.md +46 -24
  70. package/skills/openprd-harness/references/workflow-gates.md +15 -0
  71. package/skills/openprd-quality/SKILL.md +10 -4
  72. package/skills/openprd-requirement-intake/SKILL.md +31 -20
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
  75. package/skills/openprd-router/SKILL.md +2 -2
  76. package/skills/openprd-shared/SKILL.md +51 -23
  77. package/skills/openprd-standards/SKILL.md +2 -1
  78. package/src/agent-integration.js +265 -65
  79. package/src/benchmark/constants.js +107 -0
  80. package/src/benchmark/operations.js +235 -0
  81. package/src/benchmark/registry.js +64 -0
  82. package/src/benchmark/render.js +115 -0
  83. package/src/benchmark/source.js +617 -0
  84. package/src/benchmark/storage.js +121 -0
  85. package/src/benchmark/verify.js +235 -0
  86. package/src/benchmark.js +50 -851
  87. package/src/change-summary.js +339 -0
  88. package/src/cli/args.js +67 -6
  89. package/src/cli/basic-print.js +365 -0
  90. package/src/cli/benchmark-print.js +91 -0
  91. package/src/cli/change-print.js +221 -0
  92. package/src/cli/doctor-print.js +268 -0
  93. package/src/cli/growth-print.js +176 -0
  94. package/src/cli/print.js +73 -1384
  95. package/src/cli/quality-print.js +284 -0
  96. package/src/cli/run-print.js +297 -0
  97. package/src/cli/shared-print.js +127 -0
  98. package/src/cli/workflow-print.js +195 -0
  99. package/src/codex-hook-runner-template.mjs +639 -117
  100. package/src/codex-runtime.js +324 -0
  101. package/src/dev-standards.js +178 -5
  102. package/src/diagram-core.js +5 -5
  103. package/src/discovery.js +2 -1
  104. package/src/execution-strategy.js +369 -0
  105. package/src/fleet.js +4 -0
  106. package/src/github-release.js +156 -0
  107. package/src/growth.js +311 -13
  108. package/src/html-artifact-utils.js +25 -0
  109. package/src/html-artifacts.js +157 -1596
  110. package/src/knowledge.js +1176 -75
  111. package/src/language-policy.js +2 -112
  112. package/src/learning-html-artifact.js +1031 -0
  113. package/src/learning-review.js +3 -2
  114. package/src/loop.js +280 -9
  115. package/src/openprd.js +341 -38
  116. package/src/openspec/change-validate.js +0 -9
  117. package/src/openspec/execute.js +79 -3
  118. package/src/openspec/generate.js +33 -20
  119. package/src/openspec/tasks.js +33 -2
  120. package/src/prd-core.js +10 -9
  121. package/src/product-type-copy.js +69 -0
  122. package/src/quality-html-artifact.js +108 -9
  123. package/src/quality-learning.js +30 -0
  124. package/src/quality-visual-review.js +237 -0
  125. package/src/quality.js +329 -43
  126. package/src/registry-hygiene.js +54 -0
  127. package/src/release-ledger.js +413 -0
  128. package/src/review-presentation.js +12 -6
  129. package/src/run-harness.js +722 -48
  130. package/src/session-binding.js +40 -3
  131. package/src/session-registry.js +159 -0
  132. package/src/standards.js +5 -3
  133. package/src/test-strategy.js +386 -0
  134. package/src/visual-compare.js +915 -34
  135. package/src/work-unit-migration.js +5 -1
  136. package/src/workspace-core.js +343 -19
  137. package/src/workspace-workflow.js +538 -134
@@ -0,0 +1,324 @@
1
+ /*
2
+ * 核心功能
3
+ * 封装 Codex CLI runtime 健康检查、可识别诊断和显式修复流程。
4
+ *
5
+ * 输入
6
+ * 接收 Codex 命令、package manager、cwd/env、是否 repair 以及可注入的 runCommand。
7
+ *
8
+ * 输出
9
+ * 导出 checkCodexCliHealth、repairCodexCli 和 ensureCodexCliReady 等结构化结果。
10
+ *
11
+ * 定位
12
+ * 位于 OpenPrd CLI 的 Agent runtime 边界,只处理本机 Codex 命令健康,不管理 PRD/task 状态。
13
+ *
14
+ * 依赖
15
+ * 使用 node:child_process 启动本地命令;被 doctor 与 loop run 复用。
16
+ *
17
+ * 维护规则
18
+ * 默认路径不得修改全局 Codex 安装;新增修复行为必须保持显式 opt-in 且可测试注入。
19
+ */
20
+ import { spawn } from 'node:child_process';
21
+
22
+ export const CODEX_REPAIR_PACKAGE = '@openai/codex@latest';
23
+
24
+ function shellQuote(value) {
25
+ const text = String(value);
26
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(text)) {
27
+ return text;
28
+ }
29
+ return `'${text.replace(/'/g, `'\\''`)}'`;
30
+ }
31
+
32
+ export function formatCommand(command, args = []) {
33
+ return [command, ...args].map(shellQuote).join(' ');
34
+ }
35
+
36
+ function windowsShellQuote(value) {
37
+ const text = String(value);
38
+ if (!text) {
39
+ return '""';
40
+ }
41
+ if (!/[\s"&<>|^]/.test(text)) {
42
+ return text;
43
+ }
44
+ return `"${text.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, '$1$1')}"`;
45
+ }
46
+
47
+ function formatWindowsCommand(command, args = []) {
48
+ return [command, ...args].map(windowsShellQuote).join(' ');
49
+ }
50
+
51
+ function hasExplicitPath(command) {
52
+ return /[\\/]/.test(command) || /^[A-Za-z]:/.test(command);
53
+ }
54
+
55
+ export function buildProcessInvocation(command, args = [], options = {}) {
56
+ const env = options.env ?? process.env;
57
+ const platform = options.platform ?? process.platform;
58
+ const useWindowsShellWrapper = (
59
+ platform === 'win32'
60
+ && options.shell === undefined
61
+ && !hasExplicitPath(command)
62
+ );
63
+
64
+ if (useWindowsShellWrapper) {
65
+ return {
66
+ command: env.ComSpec || 'cmd.exe',
67
+ args: ['/d', '/s', '/c', formatWindowsCommand(command, args)],
68
+ display: formatCommand(command, args),
69
+ shell: false,
70
+ };
71
+ }
72
+
73
+ return {
74
+ command,
75
+ args,
76
+ display: formatCommand(command, args),
77
+ shell: Boolean(options.shell),
78
+ };
79
+ }
80
+
81
+ function runProcess(command, args = [], options = {}) {
82
+ return new Promise((resolve) => {
83
+ const invocation = buildProcessInvocation(command, args, options);
84
+ const child = spawn(invocation.command, invocation.args, {
85
+ cwd: options.cwd ?? process.cwd(),
86
+ env: options.env ?? process.env,
87
+ shell: invocation.shell,
88
+ stdio: ['ignore', 'pipe', 'pipe'],
89
+ });
90
+ let stdout = '';
91
+ let stderr = '';
92
+ child.stdout.on('data', (chunk) => {
93
+ stdout = `${stdout}${chunk}`.slice(-64000);
94
+ });
95
+ child.stderr.on('data', (chunk) => {
96
+ stderr = `${stderr}${chunk}`.slice(-64000);
97
+ });
98
+ child.on('error', (error) => {
99
+ resolve({
100
+ ok: false,
101
+ command,
102
+ args,
103
+ display: invocation.display,
104
+ exitCode: null,
105
+ stdout,
106
+ stderr,
107
+ error: error.message,
108
+ });
109
+ });
110
+ child.on('close', (exitCode) => {
111
+ resolve({
112
+ ok: exitCode === 0,
113
+ command,
114
+ args,
115
+ display: invocation.display,
116
+ exitCode,
117
+ stdout,
118
+ stderr,
119
+ error: null,
120
+ });
121
+ });
122
+ });
123
+ }
124
+
125
+ function normalizeRunResult(command, args, result = {}) {
126
+ return {
127
+ ok: Boolean(result.ok),
128
+ command: result.command ?? command,
129
+ args: result.args ?? args,
130
+ display: result.display ?? formatCommand(command, args),
131
+ exitCode: result.exitCode ?? result.status ?? null,
132
+ stdout: String(result.stdout ?? ''),
133
+ stderr: String(result.stderr ?? ''),
134
+ error: result.error ? String(result.error) : null,
135
+ };
136
+ }
137
+
138
+ function runOutput(result) {
139
+ return [
140
+ result.stderr,
141
+ result.stdout,
142
+ result.error,
143
+ ].filter(Boolean).join('\n').trim();
144
+ }
145
+
146
+ export function buildCodexRepairCommand(options = {}) {
147
+ const command = options.packageManager ?? 'npm';
148
+ const args = ['install', '-g', options.package ?? CODEX_REPAIR_PACKAGE];
149
+ return {
150
+ command,
151
+ args,
152
+ display: formatCommand(command, args),
153
+ };
154
+ }
155
+
156
+ function missingOptionalDependency(output) {
157
+ const direct = output.match(/Missing optional dependency\s+([@A-Za-z0-9/._-]+)/i);
158
+ if (direct) return direct[1].replace(/[.。::,,;;]+$/, '');
159
+ const moduleMissing = output.match(/Cannot find module ['"](@openai\/codex-[^'"]+)['"]/i);
160
+ if (moduleMissing) return moduleMissing[1];
161
+ const packageLike = output.match(/(@openai\/codex-[A-Za-z0-9._-]+)/i);
162
+ if (/optional dependenc/i.test(output) && packageLike) return packageLike[1];
163
+ return null;
164
+ }
165
+
166
+ export function diagnoseCodexVersionFailure(result, options = {}) {
167
+ const output = runOutput(result);
168
+ const repairCommand = buildCodexRepairCommand(options);
169
+ const missingPackage = missingOptionalDependency(output);
170
+ if (missingPackage) {
171
+ return {
172
+ type: 'missing-optional-dependency',
173
+ summary: `Codex CLI 启动失败:缺少平台原生可选依赖 ${missingPackage}。`,
174
+ missingPackage,
175
+ repairCommand,
176
+ manualCommand: repairCommand.display,
177
+ output,
178
+ };
179
+ }
180
+
181
+ if (
182
+ result.exitCode === null
183
+ && /(ENOENT|not found|command not found|no such file|spawn .*enoent)/i.test(output)
184
+ ) {
185
+ return {
186
+ type: 'command-not-found',
187
+ summary: 'Codex CLI 不在 PATH 中,OpenPrd 无法启动 Codex 代理子会话。',
188
+ missingPackage: null,
189
+ repairCommand,
190
+ manualCommand: repairCommand.display,
191
+ output,
192
+ };
193
+ }
194
+
195
+ return {
196
+ type: 'version-check-failed',
197
+ summary: `Codex CLI 健康检查失败:codex --version 退出码 ${result.exitCode ?? 'unknown'}。`,
198
+ missingPackage: null,
199
+ repairCommand,
200
+ manualCommand: repairCommand.display,
201
+ output,
202
+ };
203
+ }
204
+
205
+ export async function checkCodexCliHealth(options = {}) {
206
+ const command = options.codexCommand ?? 'codex';
207
+ const args = options.versionArgs ?? ['--version'];
208
+ const runner = options.runCommand ?? runProcess;
209
+ const result = normalizeRunResult(
210
+ command,
211
+ args,
212
+ await runner(command, args, {
213
+ cwd: options.cwd,
214
+ env: options.env ?? process.env,
215
+ }),
216
+ );
217
+ const repairCommand = buildCodexRepairCommand(options);
218
+ const commandInfo = {
219
+ command,
220
+ args,
221
+ display: formatCommand(command, args),
222
+ };
223
+
224
+ if (result.ok) {
225
+ return {
226
+ ok: true,
227
+ command: commandInfo,
228
+ result,
229
+ version: (result.stdout || result.stderr).trim() || null,
230
+ diagnostic: null,
231
+ repairCommand,
232
+ errors: [],
233
+ };
234
+ }
235
+
236
+ const diagnostic = diagnoseCodexVersionFailure(result, options);
237
+ return {
238
+ ok: false,
239
+ command: commandInfo,
240
+ result,
241
+ version: null,
242
+ diagnostic,
243
+ repairCommand,
244
+ errors: [
245
+ diagnostic.summary,
246
+ `修复命令: ${repairCommand.display}`,
247
+ ],
248
+ };
249
+ }
250
+
251
+ export async function repairCodexCli(options = {}) {
252
+ const runner = options.runCommand ?? runProcess;
253
+ const commandInfo = buildCodexRepairCommand(options);
254
+ const result = normalizeRunResult(
255
+ commandInfo.command,
256
+ commandInfo.args,
257
+ await runner(commandInfo.command, commandInfo.args, {
258
+ cwd: options.cwd,
259
+ env: options.env ?? process.env,
260
+ }),
261
+ );
262
+ const recheck = result.ok ? await checkCodexCliHealth(options) : null;
263
+ const errors = [];
264
+ if (!result.ok) {
265
+ errors.push(result.stderr.trim() || result.error || `Codex 修复命令失败,退出码 ${result.exitCode ?? 'unknown'}。`);
266
+ }
267
+ if (result.ok && !recheck?.ok) {
268
+ errors.push(...(recheck?.errors ?? ['Codex 修复后仍未通过健康检查。']));
269
+ }
270
+
271
+ return {
272
+ attempted: true,
273
+ ok: result.ok && recheck?.ok === true,
274
+ command: commandInfo,
275
+ result,
276
+ recheck,
277
+ errors,
278
+ };
279
+ }
280
+
281
+ export async function ensureCodexCliReady(options = {}) {
282
+ const preflight = await checkCodexCliHealth(options);
283
+ if (preflight.ok) {
284
+ return {
285
+ ok: true,
286
+ preflight,
287
+ repair: null,
288
+ repairAttempted: false,
289
+ repairCommand: preflight.repairCommand,
290
+ errors: [],
291
+ };
292
+ }
293
+
294
+ if (!options.repair) {
295
+ return {
296
+ ok: false,
297
+ preflight,
298
+ repair: null,
299
+ repairAttempted: false,
300
+ repairCommand: preflight.repairCommand,
301
+ errors: [
302
+ 'Codex CLI 健康检查未通过,OpenPrd 已停止启动 Codex 子会话。',
303
+ ...preflight.errors,
304
+ '如需让 OpenPrd 执行修复,请显式运行 openprd doctor . --tools codex --fix 或 openprd loop . --run --agent codex --repair-agent。',
305
+ ],
306
+ };
307
+ }
308
+
309
+ const repair = await repairCodexCli(options);
310
+ return {
311
+ ok: repair.ok,
312
+ preflight,
313
+ repair,
314
+ repairAttempted: true,
315
+ repairCommand: preflight.repairCommand,
316
+ errors: repair.ok
317
+ ? []
318
+ : [
319
+ 'Codex CLI 显式修复后仍未通过健康检查。',
320
+ ...repair.errors,
321
+ `可手动执行: ${preflight.repairCommand.display}`,
322
+ ],
323
+ };
324
+ }
@@ -1,10 +1,14 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { cjoin, exists, readJson } from './fs-utils.js';
4
6
  import { buildCodeExtensionCandidate, observeGrowthWorkspace } from './growth.js';
5
7
  import { recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowledge.js';
6
8
 
7
9
  const DEVELOPMENT_STANDARDS_CONFIG = cjoin('.openprd', 'standards', 'config.json');
10
+ const DEV_CHECK_WRAPUP_COPY_SCRIPT = fileURLToPath(new URL('../scripts/dev-check-wrapup-copy.mjs', import.meta.url));
11
+ const DEV_CHECK_WRAPUP_FIELDS = ['规模信号', '预警原因', '本次处理结果', '后续建议'];
8
12
  const CODE_FILE_EXTENSIONS = new Set([
9
13
  '.c',
10
14
  '.cc',
@@ -209,23 +213,183 @@ function fileStatus(lineCount, lineConfig) {
209
213
 
210
214
  function nextActionForStatus(status, lineConfig) {
211
215
  if (status === 'ok') {
212
- return `结构状态正常;最终回复中可简要说明 dev-check 已回顾 touched files。`;
216
+ return `本轮没有显著维护风险;最终回复可简要说明已回顾本次改动文件。`;
213
217
  }
214
218
  if (status === 'attention') {
215
- return `最终回复说明本轮只触碰的局部职责和影响范围,避免继续新增无关职责。`;
219
+ return `本轮只做当前目标相关的小范围改动,并说明没有继续扩展该文件职责。`;
216
220
  }
217
221
  if (status === 'warning') {
218
- return `判断本轮是否继续扩大职责或堆叠逻辑;若扩大了,先重构、拆分或解耦后复查;若只是窄 bugfix 或小修且暂不拆,说明原因并留下后续拆分建议。`;
222
+ return `先判断这次是否新增职责;如果新增了,优先拆分或解耦后再收尾;如果只是小修,说明暂不拆的原因和后续拆分建议。`;
219
223
  }
220
224
  if (status === 'exempt') {
221
- return `豁免治理;只记录行数,不要求拆分。`;
225
+ return `不纳入本次维护风险判断;只记录行数,不要求拆分。`;
222
226
  }
223
227
  if (status === 'not-code') {
224
- return `不适用;研发期行数规则只约束代码文件。`;
228
+ return `不适用;维护风险检查只面向代码文件。`;
225
229
  }
226
230
  return `无法检查;请确认文件路径。`;
227
231
  }
228
232
 
233
+ function devCheckStatusLabel(status) {
234
+ if (status === 'ok') return '已检查,无需关注';
235
+ if (status === 'attention') return '🟡 低风险|建议留意';
236
+ if (status === 'warning') return '🟠 中风险|建议优先关注';
237
+ if (status === 'exempt') return '不纳入本次判断';
238
+ if (status === 'not-code') return '不适用';
239
+ if (status === 'error') return '🔴 高风险|需要先处理';
240
+ return String(status || '未知');
241
+ }
242
+
243
+ function devCheckConcernRank(file) {
244
+ if (file.status === 'error') return 4;
245
+ if (file.status === 'warning') return 3;
246
+ if (file.status === 'attention') return 2;
247
+ if (file.status === 'ok') return 1;
248
+ return 0;
249
+ }
250
+
251
+ function devCheckThresholdText(file) {
252
+ const thresholds = file.thresholds ?? {};
253
+ const okMax = thresholds.okMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.okMax;
254
+ const attentionMax = thresholds.attentionMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.attentionMax;
255
+ const lineText = file.lineCount === null || file.lineCount === undefined ? '未知行数' : `${file.lineCount} 行`;
256
+ if (file.status === 'warning') {
257
+ return `${lineText};已超过高维护风险线(>${attentionMax} 行)`;
258
+ }
259
+ if (file.status === 'attention') {
260
+ return `${lineText};已超过建议的单文件舒适区(≤${okMax} 行)`;
261
+ }
262
+ return `${lineText};建议单文件舒适区 ≤${okMax} 行,高维护风险线 >${attentionMax} 行`;
263
+ }
264
+
265
+ function devCheckReason(file) {
266
+ const thresholds = file.thresholds ?? {};
267
+ const okMax = thresholds.okMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.okMax;
268
+ const attentionMax = thresholds.attentionMax ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines.attentionMax;
269
+ if (file.status === 'attention') {
270
+ return `文件已经偏大,继续叠加新职责会提高评审、回归和交接成本;本轮需要说明只改了哪一小块。`;
271
+ }
272
+ if (file.status === 'warning') {
273
+ return `文件已经进入高维护风险区,继续加逻辑容易放大改动范围和回归成本;需要判断是否应先拆分。`;
274
+ }
275
+ if (file.status === 'ok') {
276
+ return `文件规模在建议范围内。`;
277
+ }
278
+ if (file.status === 'exempt') {
279
+ return '命中生成物、依赖、快照或项目配置豁免。';
280
+ }
281
+ if (file.status === 'not-code') {
282
+ return '未识别为代码文件。';
283
+ }
284
+ return file.nextAction || '无法完成检查。';
285
+ }
286
+
287
+ function devCheckSplitIdea(file) {
288
+ if (file.status === 'attention') {
289
+ return '后续如果还要继续改这个文件,按入口、状态、渲染或数据处理边界拆出更小模块。';
290
+ }
291
+ if (file.status === 'warning') {
292
+ return '优先把独立职责、测试夹具或输出渲染拆出,降低下一次需求的评审和回归成本。';
293
+ }
294
+ if (file.status === 'ok') {
295
+ return '无需拆分。';
296
+ }
297
+ if (file.status === 'exempt') {
298
+ return '无需拆分,保持豁免原因可追踪。';
299
+ }
300
+ if (file.status === 'not-code') {
301
+ return '不适用研发期代码拆分判断。';
302
+ }
303
+ return '先修复检查错误,再重新运行 dev-check。';
304
+ }
305
+
306
+ function markdownCell(value) {
307
+ return String(value ?? '')
308
+ .replace(/\r?\n/g, '<br>')
309
+ .replace(/\|/g, '\\|');
310
+ }
311
+
312
+ function renderMarkdownTable(columns, rows) {
313
+ if (!rows.length) return '';
314
+ const header = `| ${columns.map(markdownCell).join(' | ')} |`;
315
+ const divider = `| ${columns.map(() => '---').join(' | ')} |`;
316
+ const body = rows.map((row) => `| ${columns.map((column) => markdownCell(row[column])).join(' | ')} |`);
317
+ return [header, divider, ...body].join('\n');
318
+ }
319
+
320
+ function countDisplayChars(value) {
321
+ return Array.from(String(value ?? '')).length;
322
+ }
323
+
324
+ function buildCompactWrapUpRows(files) {
325
+ const result = spawnSync(process.execPath, [DEV_CHECK_WRAPUP_COPY_SCRIPT], {
326
+ input: JSON.stringify({
327
+ files: files.map((file) => ({
328
+ status: file.status,
329
+ lineCount: file.lineCount,
330
+ thresholds: file.thresholds,
331
+ nextAction: file.nextAction,
332
+ })),
333
+ }),
334
+ encoding: 'utf8',
335
+ maxBuffer: 1024 * 1024,
336
+ });
337
+ if (result.error) {
338
+ throw result.error;
339
+ }
340
+ if (result.status !== 0) {
341
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || 'Failed to generate compact dev-check wrap-up copy.');
342
+ }
343
+ let payload = null;
344
+ try {
345
+ payload = JSON.parse(result.stdout || '{}');
346
+ } catch (error) {
347
+ throw new Error(`Failed to parse compact dev-check wrap-up copy: ${error instanceof Error ? error.message : String(error)}`);
348
+ }
349
+ const rows = Array.isArray(payload?.rows) ? payload.rows : null;
350
+ const limit = Number.isInteger(payload?.limit) ? payload.limit : 20;
351
+ if (!rows || rows.length !== files.length) {
352
+ throw new Error('Compact dev-check wrap-up copy returned an unexpected row count.');
353
+ }
354
+ for (const row of rows) {
355
+ for (const field of DEV_CHECK_WRAPUP_FIELDS) {
356
+ if (typeof row[field] !== 'string') {
357
+ throw new Error(`Compact dev-check wrap-up copy is missing field: ${field}`);
358
+ }
359
+ if (countDisplayChars(row[field]) > limit) {
360
+ throw new Error(`Compact dev-check wrap-up copy exceeded ${limit} chars for field: ${field}`);
361
+ }
362
+ }
363
+ }
364
+ return rows;
365
+ }
366
+
367
+ function buildDevCheckWrapUp(files) {
368
+ const title = '后续建议';
369
+ const columns = ['影响对象', '关注程度', '规模信号', '预警原因', '本次处理结果', '后续建议'];
370
+ const attentionFiles = files
371
+ .filter((file) => ['attention', 'warning', 'error'].includes(file.status))
372
+ .sort((left, right) => devCheckConcernRank(right) - devCheckConcernRank(left));
373
+ const compactRows = attentionFiles.length > 0 ? buildCompactWrapUpRows(attentionFiles) : [];
374
+ const rows = attentionFiles.map((file, index) => ({
375
+ 影响对象: file.path,
376
+ 关注程度: devCheckStatusLabel(file.status),
377
+ ...compactRows[index],
378
+ }));
379
+ const markdownTable = renderMarkdownTable(columns, rows);
380
+ return {
381
+ required: rows.length > 0,
382
+ reason: rows.length > 0
383
+ ? '存在需要用户关注的影响对象;最终回复需要用“后续建议”说明本次处理结果和后续建议。'
384
+ : '本轮改动文件未触发影响范围提醒;最终回复可简要说明已完成改动对象回顾。',
385
+ title,
386
+ columns,
387
+ rows,
388
+ markdownTable,
389
+ markdownBlock: markdownTable ? `**${title}**\n\n${markdownTable}` : '',
390
+ };
391
+ }
392
+
229
393
  async function analyzeDevelopmentFile(projectRoot, targetPath, lineConfig) {
230
394
  const absolutePath = path.isAbsolute(targetPath)
231
395
  ? path.resolve(targetPath)
@@ -295,6 +459,7 @@ async function analyzeDevelopmentFile(projectRoot, targetPath, lineConfig) {
295
459
  path: relativePath,
296
460
  absolutePath,
297
461
  status,
462
+ statusLabel: devCheckStatusLabel(status),
298
463
  fileKind: exempt ? 'exempt' : (codeFile ? 'code' : (candidateCode ? 'candidate-code' : 'non-code')),
299
464
  lineCount,
300
465
  sizeBytes: stat.size,
@@ -305,6 +470,13 @@ async function analyzeDevelopmentFile(projectRoot, targetPath, lineConfig) {
305
470
  growthCandidate,
306
471
  growthObservation,
307
472
  nextAction,
473
+ wrapUp: ['attention', 'warning'].includes(status)
474
+ ? {
475
+ threshold: devCheckThresholdText({ status, lineCount, thresholds: { okMax: lineConfig.okMax, attentionMax: lineConfig.attentionMax } }),
476
+ reason: devCheckReason({ status, lineCount, thresholds: { okMax: lineConfig.okMax, attentionMax: lineConfig.attentionMax }, nextAction }),
477
+ splitIdea: devCheckSplitIdea({ status }),
478
+ }
479
+ : null,
308
480
  };
309
481
  }
310
482
 
@@ -366,6 +538,7 @@ export async function checkDevelopmentStandardsWorkspace(projectRoot, options =
366
538
  warning: statusCounts.warning ?? 0,
367
539
  },
368
540
  files,
541
+ wrapUp: buildDevCheckWrapUp(files),
369
542
  knowledgeReview,
370
543
  errors,
371
544
  };
@@ -355,9 +355,9 @@ function renderArrow(def) {
355
355
  }
356
356
 
357
357
  function resolveProductLayerTitle(productType) {
358
- if (productType === 'consumer') return '消费端体验层';
359
- if (productType === 'b2b') return 'B2B 工作流层';
360
- if (productType === 'agent') return 'Agent 运行层';
358
+ if (productType === 'consumer') return '个人消费者场景层';
359
+ if (productType === 'b2b') return '企业服务场景层';
360
+ if (productType === 'agent') return 'Agent 使用场景层';
361
361
  return '产品体验层';
362
362
  }
363
363
 
@@ -556,7 +556,7 @@ export function renderArchitectureDiagramHtml(model) {
556
556
  svgMarkup,
557
557
  summaryCards: model.summaryCards,
558
558
  sidePanels: model.sidePanels,
559
- footer: `负责人: ${model.metadata.owner} · 版本: ${model.metadata.versionId} · 目标: ${model.metadata.targetSystem} · 生成时间: ${model.generatedAt}`,
559
+ footer: `负责人: ${model.metadata.owner} · 交接去向: ${model.metadata.targetSystem} · 最近生成: ${model.generatedAt}`,
560
560
  });
561
561
  }
562
562
 
@@ -752,7 +752,7 @@ export function renderProductFlowDiagramHtml(model) {
752
752
  svgMarkup,
753
753
  summaryCards: model.summaryCards,
754
754
  sidePanels: model.sidePanels,
755
- footer: `负责人: ${model.metadata.owner} · 版本: ${model.metadata.versionId} · 目标: ${model.metadata.targetSystem} · 生成时间: ${model.generatedAt}`,
755
+ footer: `负责人: ${model.metadata.owner} · 交接去向: ${model.metadata.targetSystem} · 最近生成: ${model.generatedAt}`,
756
756
  });
757
757
  }
758
758
 
package/src/discovery.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId } from './prd-core.js';
4
+ import { formatProductTypeDisplay } from './product-type-copy.js';
4
5
  import { analyzeOpenSpecTaskVolumes } from './openspec/tasks.js';
5
6
  import { legacyOpenSpecDiscoveryDir, openPrdDiscoveryDir, readDiscoveryConfig } from './openspec/paths.js';
6
7
  import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
@@ -194,7 +195,7 @@ function renderDiscoveryContext({ mode, projectRoot, sourceRoot, snapshot, analy
194
195
  `- 项目根目录: ${projectRoot}`,
195
196
  `- 来源根目录: ${sourceRoot}`,
196
197
  `- PRD 版本: ${snapshot.versionId}`,
197
- `- 产品类型: ${snapshot.productType ?? '未分类'}`,
198
+ `- 产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}`,
198
199
  `- 必填字段完成度: ${analysis.completedRequiredFields}/${analysis.totalRequiredFields}`,
199
200
  `- 覆盖项: ${coverageMatrix.summary.pending}/${coverageMatrix.summary.total} 待处理`,
200
201
  next ? `- 下一项: ${next.title}` : '- 下一项: 无',