@openprd/cli 0.1.0 → 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 (138) 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 +402 -441
  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 +39 -23
  73. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
  74. package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
  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/self-update.js +1 -1
  131. package/src/session-binding.js +40 -3
  132. package/src/session-registry.js +159 -0
  133. package/src/standards.js +5 -3
  134. package/src/test-strategy.js +386 -0
  135. package/src/visual-compare.js +915 -34
  136. package/src/work-unit-migration.js +5 -1
  137. package/src/workspace-core.js +343 -19
  138. package/src/workspace-workflow.js +538 -134
@@ -115,7 +115,57 @@ function assertTaskReady(task, state) {
115
115
  return dependencyState;
116
116
  }
117
117
 
118
- async function runVerifyCommand(command, cwd) {
118
+ function isTaskEvidenceRequiredCommand(command) {
119
+ return /^openprd\s+tasks\s+\./i.test(String(command ?? ''))
120
+ && /\s--evidence-required\b/i.test(String(command ?? ''));
121
+ }
122
+
123
+ function isLegacyPerTaskFullVerifyCommand(command) {
124
+ return /^openprd\s+run\s+\.\s+--verify\s*$/i.test(String(command ?? '').replace(/\s+/g, ' ').trim());
125
+ }
126
+
127
+ function taskEvidenceState(task, options = {}) {
128
+ const metadata = task?.metadata ?? {};
129
+ const evidence = String(options.evidence ?? metadata.evidence ?? '').trim();
130
+ const waiver = String(metadata.waiver ?? metadata['waiver-reason'] ?? '').trim();
131
+ return {
132
+ evidence,
133
+ waiver,
134
+ ok: Boolean(evidence || waiver),
135
+ };
136
+ }
137
+
138
+ function buildTaskEvidenceVerification(command, task, options = {}) {
139
+ const evidenceState = taskEvidenceState(task, options);
140
+ const legacyHint = isLegacyPerTaskFullVerifyCommand(command)
141
+ ? '旧任务里的 per-task openprd run . --verify 已保留给阶段或最终门禁,不会在任务推进时触发全局 quality。'
142
+ : null;
143
+ const hint = `${legacyHint ? `${legacyHint} ` : ''}先运行本任务最小足够测试或审查,再通过 --evidence <路径或摘要> 传入证据,或在 tasks.md 写入 evidence:/waiver-reason:。`;
144
+ return {
145
+ ok: evidenceState.ok,
146
+ command,
147
+ exitCode: evidenceState.ok ? 0 : 1,
148
+ stdout: evidenceState.ok
149
+ ? [
150
+ 'OpenPrd task evidence: passed',
151
+ evidenceState.evidence ? `evidence: ${evidenceState.evidence}` : null,
152
+ evidenceState.waiver ? `waiver: ${evidenceState.waiver}` : null,
153
+ legacyHint,
154
+ 'scope: task-only; workspace quality is reserved for phase/final gates',
155
+ '',
156
+ ].filter(Boolean).join('\n')
157
+ : '',
158
+ stderr: evidenceState.ok
159
+ ? ''
160
+ : `OpenPrd task evidence: missing evidence for ${task?.id ?? 'task'}. ${hint}\n`,
161
+ };
162
+ }
163
+
164
+ async function runVerifyCommand(command, cwd, context = {}) {
165
+ if ((isTaskEvidenceRequiredCommand(command) || isLegacyPerTaskFullVerifyCommand(command)) && context.task) {
166
+ return buildTaskEvidenceVerification(command, context.task, context);
167
+ }
168
+
119
169
  return new Promise((resolve) => {
120
170
  const child = spawn(command, {
121
171
  cwd,
@@ -152,6 +202,22 @@ async function runVerifyCommand(command, cwd) {
152
202
  });
153
203
  }
154
204
 
205
+ export async function checkOpenSpecTaskEvidenceWorkspace(projectRoot, options = {}) {
206
+ const state = await loadTaskState(projectRoot, options);
207
+ const task = resolveTaskSelection(state, options);
208
+ assertTaskReady(task, state);
209
+ const command = `openprd tasks . --change ${state.changeId} --item ${task.id} --evidence-required`;
210
+ const verification = buildTaskEvidenceVerification(command, task, options);
211
+ return {
212
+ ok: verification.ok,
213
+ action: 'evidence-check',
214
+ projectRoot,
215
+ changeId: state.changeId,
216
+ task,
217
+ verification,
218
+ };
219
+ }
220
+
155
221
  async function markTaskComplete(task) {
156
222
  const text = await readText(task.absolutePath);
157
223
  const lines = text.split(/\r?\n/);
@@ -208,7 +274,12 @@ export async function verifyOpenSpecTaskWorkspace(projectRoot, options = {}) {
208
274
  if (!task.metadata.verify) {
209
275
  throw new Error(`${task.id} is missing verify command.`);
210
276
  }
211
- const verification = await runVerifyCommand(task.metadata.verify, projectRoot);
277
+ const verification = await runVerifyCommand(task.metadata.verify, projectRoot, {
278
+ task,
279
+ state,
280
+ evidence: options.evidence,
281
+ notes: options.notes,
282
+ });
212
283
  await appendTaskEvent(state, {
213
284
  action: 'verify',
214
285
  taskId: task.id,
@@ -239,7 +310,12 @@ export async function advanceOpenSpecTaskWorkspace(projectRoot, options = {}) {
239
310
  if (!task.metadata.verify) {
240
311
  throw new Error(`${task.id} is missing verify command.`);
241
312
  }
242
- verification = await runVerifyCommand(task.metadata.verify, projectRoot);
313
+ verification = await runVerifyCommand(task.metadata.verify, projectRoot, {
314
+ task,
315
+ state,
316
+ evidence: options.evidence,
317
+ notes: options.notes,
318
+ });
243
319
  if (!verification.ok) {
244
320
  await appendTaskEvent(state, {
245
321
  action: 'advance_failed',
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { preferSimplifiedChinese } from '../language-policy.js';
4
4
  import { needsBusinessGuardrails } from '../prd-core.js';
5
+ import { EXECUTION_STRATEGY_METADATA_KEYS, formatTaskExecutionStrategyMetadata } from '../execution-strategy.js';
6
+ import { TEST_STRATEGY_METADATA_KEYS, formatTaskTestStrategyMetadata } from '../test-strategy.js';
5
7
  import { OPENSPEC_TASK_MAX_ITEMS_PER_FILE } from './constants.js';
6
8
  import { openPrdChangeRoot, openPrdDiscoveryConfigPath, readDiscoveryConfig } from './paths.js';
7
9
 
@@ -143,7 +145,12 @@ function chunkItems(items, maxItemsPerChunk = 2) {
143
145
  return chunks;
144
146
  }
145
147
 
146
- const DEFAULT_EXECUTION_VERIFY_COMMAND = 'openprd run . --verify';
148
+ function defaultTaskVerifyCommand(changeId, task) {
149
+ if (task.type === 'documentation') {
150
+ return 'openprd standards . --verify';
151
+ }
152
+ return `openprd tasks . --change ${changeId} --item ${task.id} --evidence-required`;
153
+ }
147
154
 
148
155
  const ARCHITECTURE_TASK_DEFINITIONS = [
149
156
  {
@@ -321,7 +328,6 @@ function inferArchitectureTasks(snapshot) {
321
328
  type: 'implementation',
322
329
  title: definition.title,
323
330
  done: `${definition.done} 涉及: ${summarizeTaskItems(matches, 2, 72)}。`,
324
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
325
331
  phase: 'architecture',
326
332
  }));
327
333
  }
@@ -345,7 +351,6 @@ function buildRequirementImplementationTasks(snapshot) {
345
351
  type: 'implementation',
346
352
  title: cleanImplementationTitle(item),
347
353
  done: buildDoneText('已完成:', item),
348
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
349
354
  phase: 'implementation',
350
355
  });
351
356
  }
@@ -364,7 +369,6 @@ function buildFlowIntegrationTasks(snapshot) {
364
369
  type: 'implementation',
365
370
  title: `打通主流程闭环:${summarizeTaskItems(flows, 2, 56)}`,
366
371
  done: `主流程关键节点已经打通,用户可以按预期从入口走到结果收尾。涉及: ${summarizeTaskItems(flows, 2, 72)}。`,
367
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
368
372
  phase: 'integration',
369
373
  }];
370
374
  }
@@ -376,7 +380,6 @@ function buildAcceptanceVerificationTasks(snapshot) {
376
380
  type: 'verification',
377
381
  title: buildVerificationTitle(item),
378
382
  done: buildDoneText('已验证:', item),
379
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
380
383
  phase: 'verification',
381
384
  }));
382
385
  }
@@ -388,7 +391,6 @@ function buildNonFunctionalVerificationTasks(snapshot) {
388
391
  type: 'verification',
389
392
  title: `回归非功能约束:${summarizeTaskItems(items, 2, 56)}`,
390
393
  done: `非功能约束已经回归确认。涉及: ${summarizeTaskItems(items, 2, 72)}。`,
391
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
392
394
  phase: 'verification',
393
395
  }));
394
396
  const edgeAndFailure = [
@@ -401,7 +403,6 @@ function buildNonFunctionalVerificationTasks(snapshot) {
401
403
  type: 'verification',
402
404
  title: `回归边界条件与失败处理:${summarizeTaskItems(edgeAndFailure, 2, 56)}`,
403
405
  done: `边界条件与失败处理已经回归确认。涉及: ${summarizeTaskItems(edgeAndFailure, 2, 72)}。`,
404
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
405
406
  phase: 'verification',
406
407
  }]
407
408
  : [];
@@ -430,7 +431,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
430
431
  type: 'verification',
431
432
  title: '验证成本与额度护栏',
432
433
  done: '已验证免费、试用或低权限用户不能绕过额度、并发、频率或总量限制',
433
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
434
434
  phase: 'verification',
435
435
  },
436
436
  {
@@ -438,7 +438,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
438
438
  type: 'verification',
439
439
  title: '验证滥用与越权路径',
440
440
  done: '已覆盖重复请求、并发请求、越权身份和异常恢复等负向场景',
441
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
442
441
  phase: 'verification',
443
442
  },
444
443
  {
@@ -446,7 +445,6 @@ function buildTaskItems({ changeId, snapshot, capability }) {
446
445
  type: 'verification',
447
446
  title: '验证成本监控、报警和止损',
448
447
  done: '已确认用量或成本信号、报警阈值和人工/自动止损动作可执行',
449
- verify: DEFAULT_EXECUTION_VERIFY_COMMAND,
450
448
  phase: 'verification',
451
449
  },
452
450
  ]
@@ -492,15 +490,18 @@ function buildTaskItems({ changeId, snapshot, capability }) {
492
490
  deduped.push(item);
493
491
  }
494
492
 
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
- }));
493
+ const tasks = deduped.map((item, index) => {
494
+ const task = {
495
+ id: `T001.${String(index + 1).padStart(2, '0')}`,
496
+ title: item.title,
497
+ type: item.type,
498
+ phase: item.phase,
499
+ done: item.done,
500
+ deps: [],
501
+ };
502
+ task.verify = item.verify ?? defaultTaskVerifyCommand(changeId, task);
503
+ return task;
504
+ });
504
505
 
505
506
  const phaseTasks = {
506
507
  governanceStart: tasks.filter((task) => task.phase === 'governance-start'),
@@ -582,6 +583,12 @@ function renderTaskFiles(tasks, maxItemsPerFile) {
582
583
  }
583
584
  lines.push(` - done: ${task.done}`);
584
585
  lines.push(` - verify: ${task.verify}`);
586
+ for (const metadata of formatTaskTestStrategyMetadata(task)) {
587
+ lines.push(` - ${metadata}`);
588
+ }
589
+ for (const metadata of formatTaskExecutionStrategyMetadata(task)) {
590
+ lines.push(` - ${metadata}`);
591
+ }
585
592
  lines.push('');
586
593
  }
587
594
 
@@ -606,6 +613,12 @@ async function readTaskMax(projectRoot) {
606
613
  async function writeDiscoveryConfig(projectRoot, changeId) {
607
614
  const configPath = openPrdDiscoveryConfigPath(projectRoot);
608
615
  const current = await readJson(configPath).catch(() => ({}));
616
+ const optionalMetadata = [
617
+ 'deps',
618
+ 'type',
619
+ ...TEST_STRATEGY_METADATA_KEYS,
620
+ ...EXECUTION_STRATEGY_METADATA_KEYS,
621
+ ];
609
622
  await writeJson(configPath, {
610
623
  ...current,
611
624
  activeChange: changeId,
@@ -618,7 +631,7 @@ async function writeDiscoveryConfig(projectRoot, changeId) {
618
631
  taskMetadata: {
619
632
  stableIdPattern: current?.taskMetadata?.stableIdPattern ?? 'T###.##',
620
633
  required: current?.taskMetadata?.required ?? ['done', 'verify'],
621
- optional: current?.taskMetadata?.optional ?? ['deps', 'type'],
634
+ optional: [...new Set([...(current?.taskMetadata?.optional ?? []), ...optionalMetadata])],
622
635
  dependencyOrder: current?.taskMetadata?.dependencyOrder ?? 'dependencies must appear before dependents',
623
636
  },
624
637
  });
@@ -6,6 +6,29 @@ import {
6
6
  OPENSPEC_TASK_MAX_ITEMS_PER_FILE,
7
7
  } from './constants.js';
8
8
  import { cjoin, exists, listChangeDirs, readDiscoveryConfig, resolveChangeDir } from './paths.js';
9
+ import {
10
+ TEST_STRATEGY_METADATA_KEYS,
11
+ summarizeTaskTestStrategies,
12
+ validateTaskTestStrategy,
13
+ } from '../test-strategy.js';
14
+ import {
15
+ EXECUTION_STRATEGY_METADATA_KEYS,
16
+ summarizeTaskExecutionStrategies,
17
+ validateTaskExecutionStrategy,
18
+ } from '../execution-strategy.js';
19
+
20
+ const OPENSPEC_TASK_METADATA_KEYS = [
21
+ 'deps',
22
+ 'done',
23
+ 'verify',
24
+ 'type',
25
+ 'category',
26
+ 'kind',
27
+ 'oracle',
28
+ ...TEST_STRATEGY_METADATA_KEYS,
29
+ ...EXECUTION_STRATEGY_METADATA_KEYS,
30
+ ];
31
+ const OPENSPEC_TASK_METADATA_PATTERN = new RegExp(`^\\s{2,}-\\s+(${OPENSPEC_TASK_METADATA_KEYS.join('|')}):\\s*(.*)$`, 'i');
9
32
 
10
33
  async function readText(filePath) {
11
34
  return fs.readFile(filePath, 'utf8');
@@ -58,7 +81,7 @@ export function parseOpenSpecTaskFile(text) {
58
81
  return;
59
82
  }
60
83
 
61
- const metadataMatch = line.match(/^\s{2,}-\s+(deps|done|verify|type|category|kind|oracle):\s*(.*)$/i);
84
+ const metadataMatch = line.match(OPENSPEC_TASK_METADATA_PATTERN);
62
85
  if (currentTask && metadataMatch) {
63
86
  currentTask.metadata[metadataMatch[1].toLowerCase()] = metadataMatch[2].trim();
64
87
  }
@@ -272,6 +295,12 @@ export function validateOpenSpecStructuredTasks(sortedFiles, errors, checks) {
272
295
  if (normalizedType !== 'governance' && isSpecOnlyValidateCommand(task.metadata.verify)) {
273
296
  errors.push(`${formatOpenSpecTaskLocation(task)} 的 verify 只做了 change 结构校验;${normalizedType} 任务必须提供能证明实际落地的验证命令或审查步骤。`);
274
297
  }
298
+ for (const strategyError of validateTaskTestStrategy(task)) {
299
+ errors.push(`${formatOpenSpecTaskLocation(task)} ${strategyError}`);
300
+ }
301
+ for (const strategyError of validateTaskExecutionStrategy(task)) {
302
+ errors.push(`${formatOpenSpecTaskLocation(task)} ${strategyError}`);
303
+ }
275
304
 
276
305
  for (const depId of parseOpenSpecTaskDeps(task.metadata.deps)) {
277
306
  if (!OPENSPEC_TASK_ID_PATTERN.test(depId)) {
@@ -290,7 +319,9 @@ export function validateOpenSpecStructuredTasks(sortedFiles, errors, checks) {
290
319
  }
291
320
  }
292
321
 
293
- checks.push(`结构化 OpenPrd 任务: ${tasks.length} 个任务,${dependencyCount} 条依赖。`);
322
+ const strategySummary = summarizeTaskTestStrategies(tasks);
323
+ const executionSummary = summarizeTaskExecutionStrategies(tasks);
324
+ checks.push(`结构化 OpenPrd 任务: ${tasks.length} 个任务,${dependencyCount} 条依赖;测试策略显式 ${strategySummary.explicit} 个、推导 ${strategySummary.inferred} 个;执行策略显式 ${executionSummary.explicit} 个、推导 ${executionSummary.inferred} 个。`);
294
325
  }
295
326
 
296
327
  export async function analyzeOpenSpecTaskVolumes(projectRoot, options = {}) {
package/src/prd-core.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { buildArchitectureDiagramModel, buildProductFlowDiagramModel, renderDiagramMermaidFromModel } from './diagram-core.js';
3
3
  import { TBD_ZH, languagePolicyLines } from './language-policy.js';
4
+ import { formatProductTypeDisplay, formatProductTypeQuestion, formatTemplatePackDisplay, getProductTypeSectionTitle } from './product-type-copy.js';
4
5
  import { timestamp } from './time.js';
5
6
 
6
7
  function isPlainObject(value) {
@@ -151,7 +152,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
151
152
  if (productType === 'consumer') {
152
153
  return {
153
154
  kind: 'consumer',
154
- title: '消费端专项',
155
+ title: getProductTypeSectionTitle('consumer'),
155
156
  fields: {
156
157
  persona: pickValue(overrides.persona, state.persona),
157
158
  segment: pickValue(overrides.segment, state.segment),
@@ -165,7 +166,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
165
166
  if (productType === 'b2b') {
166
167
  return {
167
168
  kind: 'b2b',
168
- title: 'B2B 专项',
169
+ title: getProductTypeSectionTitle('b2b'),
169
170
  fields: {
170
171
  buyer: pickValue(overrides.buyer, state.buyer),
171
172
  user: pickValue(overrides.user, state.user),
@@ -183,7 +184,7 @@ function buildTypeSpecificSection(productType, state, overrides) {
183
184
  if (productType === 'agent') {
184
185
  return {
185
186
  kind: 'agent',
186
- title: 'Agent 专项',
187
+ title: getProductTypeSectionTitle('agent'),
187
188
  fields: {
188
189
  humanAgentContract: pickValue(overrides.humanAgentContract, state.humanAgentContract),
189
190
  autonomyBoundary: pickValue(overrides.autonomyBoundary, state.autonomyBoundary),
@@ -196,9 +197,9 @@ function buildTypeSpecificSection(productType, state, overrides) {
196
197
 
197
198
  return {
198
199
  kind: 'base',
199
- title: '类型专项',
200
+ title: getProductTypeSectionTitle('base'),
200
201
  fields: {
201
- note: '请选择产品类型,以启用对应的专项 PRD 模块。',
202
+ note: '请选择产品场景,以启用对应的专项 PRD 模块。',
202
203
  },
203
204
  };
204
205
  }
@@ -338,8 +339,8 @@ export function renderPrdMarkdown(snapshot) {
338
339
  ...languagePolicyLines(),
339
340
  `- 版本: ${snapshot.versionId}`,
340
341
  `- 负责人: ${snapshot.owner}`,
341
- `- 产品类型: ${snapshot.productType ?? '未分类'}`,
342
- `- 模板包: ${snapshot.templatePack}`,
342
+ `- 产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}`,
343
+ `- 场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}`,
343
344
  `- 状态: ${snapshot.status}`,
344
345
  `- 生成时间: ${snapshot.createdAt}`,
345
346
  '',
@@ -348,7 +349,7 @@ export function renderPrdMarkdown(snapshot) {
348
349
  ['负责人', sections.meta.owner],
349
350
  ['状态', sections.meta.status],
350
351
  ['版本', sections.meta.version],
351
- ['产品类型', sections.meta.productType],
352
+ ['产品场景', formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })],
352
353
  ['日期', sections.meta.date],
353
354
  ]),
354
355
  renderSection('问题', [
@@ -421,7 +422,7 @@ const BASE_REQUIRED_FIELD_DESCRIPTORS = [
421
422
  { section: 'meta', path: 'meta.owner', label: '负责人', prompt: '谁负责这份 PRD?' },
422
423
  { section: 'meta', path: 'meta.version', label: '版本', prompt: '这份 PRD 从哪个版本开始?' },
423
424
  { section: 'meta', path: 'meta.status', label: '状态', prompt: '当前 PRD 状态是什么?' },
424
- { section: 'meta', path: 'meta.productType', label: '产品类型', prompt: '这是 consumer、b2b 还是 agent 产品?' },
425
+ { section: 'meta', path: 'meta.productType', label: '产品场景', prompt: formatProductTypeQuestion() },
425
426
  { section: 'problem', path: 'problem.problemStatement', label: '问题陈述', prompt: '我们要解决什么问题?' },
426
427
  { section: 'problem', path: 'problem.whyNow', label: '为什么是现在', prompt: '为什么现在是解决这个问题的合适时机?' },
427
428
  { section: 'problem', path: 'problem.evidence', label: '证据', prompt: '有哪些证据支持这个问题?' },
@@ -0,0 +1,69 @@
1
+ const PRODUCT_TYPE_LABELS = {
2
+ base: '通用产品或工程场景',
3
+ consumer: '面向个人消费者场景',
4
+ b2b: '面向企业服务场景',
5
+ agent: '以 Agent 为主要使用场景',
6
+ };
7
+
8
+ function withCode(label, code, includeCode) {
9
+ return includeCode ? `${label}(${code})` : label;
10
+ }
11
+
12
+ export function formatProductTypeDisplay(productType, options = {}) {
13
+ const { includeCode = false, fallback = '待确认' } = options;
14
+ const label = PRODUCT_TYPE_LABELS[productType];
15
+ return label ? withCode(label, productType, includeCode) : fallback;
16
+ }
17
+
18
+ export function formatTemplatePackDisplay(templatePack, options = {}) {
19
+ const { includeCode = false, fallback = '待确认' } = options;
20
+ const label = PRODUCT_TYPE_LABELS[templatePack];
21
+ if (label) return withCode(label, templatePack, includeCode);
22
+ if (templatePack === null || templatePack === undefined || `${templatePack}`.trim() === "") return fallback;
23
+ return includeCode ? `${templatePack}(custom)` : `${templatePack}`;
24
+ }
25
+
26
+ export function formatProductTypeSentence(productType, options = {}) {
27
+ const { inferred = false } = options;
28
+ switch (productType) {
29
+ case 'consumer':
30
+ return inferred ? '从当前描述看,更像面向个人消费者场景的产品。' : '当前更像面向个人消费者场景的产品。';
31
+ case 'b2b':
32
+ return inferred ? '从当前描述看,更像面向企业服务场景的产品。' : '当前更像面向企业服务场景的产品。';
33
+ case 'agent':
34
+ return inferred ? '从当前描述看,更像以 Agent 为主要使用场景的产品。' : '当前更像以 Agent 为主要使用场景的产品。';
35
+ case 'base':
36
+ return inferred ? '从当前描述看,更像通用产品或工程场景。' : '当前更像通用产品或工程场景。';
37
+ default:
38
+ return inferred ? '从当前描述看,产品场景仍待确认。' : '产品场景仍待确认。';
39
+ }
40
+ }
41
+
42
+ export function formatProductTypeOptions(options = {}) {
43
+ const { includeBase = false, includeCode = false } = options;
44
+ const values = [];
45
+ if (includeBase) values.push(withCode(PRODUCT_TYPE_LABELS.base, 'base', includeCode));
46
+ values.push(withCode(PRODUCT_TYPE_LABELS.consumer, 'consumer', includeCode));
47
+ values.push(withCode(PRODUCT_TYPE_LABELS.b2b, 'b2b', includeCode));
48
+ values.push(withCode(PRODUCT_TYPE_LABELS.agent, 'agent', includeCode));
49
+ return values.join(' / ');
50
+ }
51
+
52
+ export function formatProductTypeQuestion() {
53
+ return '这是更偏向面向个人消费者场景的产品、面向企业服务场景的产品,还是以 Agent 为主要使用场景的产品?';
54
+ }
55
+
56
+ export function getProductTypeSectionTitle(productType) {
57
+ switch (productType) {
58
+ case 'consumer':
59
+ return '个人消费者场景专项';
60
+ case 'b2b':
61
+ return '企业服务场景专项';
62
+ case 'agent':
63
+ return 'Agent 使用场景专项';
64
+ case 'base':
65
+ return '通用场景专项';
66
+ default:
67
+ return '产品场景专项';
68
+ }
69
+ }