@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
package/src/quality.js CHANGED
@@ -2,10 +2,21 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { cjoin, exists, readJson, readText, writeJson, writeText } from './fs-utils.js';
4
4
  import { renderQualityEvalArtifact } from './html-artifacts.js';
5
+ import { parseOpenSpecTaskDeps } from './openspec/tasks.js';
5
6
  import { renderExperienceSkill, resolveQualityLearningSource } from './quality-learning.js';
6
7
  import {
8
+ detectTestStrategyCapabilities,
9
+ summarizeTaskTestStrategies,
10
+ } from './test-strategy.js';
11
+ import {
12
+ detectVisualReview,
13
+ listVisualReviewArtifacts,
14
+ } from './quality-visual-review.js';
15
+ import {
16
+ buildKnowledgeAdoptionSummary,
7
17
  deriveKnowledgeNames,
8
18
  ensureKnowledgeWorkspace,
19
+ listKnowledgeCandidates,
9
20
  KNOWLEDGE_CANDIDATES_DIR,
10
21
  KNOWLEDGE_DRAFTS_DIR,
11
22
  markKnowledgeCandidatePromoted,
@@ -13,6 +24,10 @@ import {
13
24
  recordKnowledgeReviewSignal,
14
25
  reviewKnowledgeWorkspace,
15
26
  } from './knowledge.js';
27
+ import {
28
+ OPENPRD_GROWTH_LEDGER,
29
+ recordGrowthCheckpointWorkspace,
30
+ } from './growth.js';
16
31
  import { timestamp } from './time.js';
17
32
 
18
33
  const QUALITY_DIR = cjoin('.openprd', 'quality');
@@ -74,11 +89,14 @@ const QUALITY_GATE_IDS = [
74
89
  'traceability',
75
90
  'redaction',
76
91
  'business-guardrails',
92
+ 'test-strategy',
77
93
  'smoke',
78
94
  'feature-coverage',
95
+ 'visual-review',
79
96
  'normal-performance',
80
97
  'extreme-performance',
81
98
  'knowledge',
99
+ 'growth',
82
100
  ];
83
101
 
84
102
  const EVIDENCE_EXTENSIONS = new Set(['.json', '.md', '.txt', '.log', '.xml', '.html', '.csv']);
@@ -87,11 +105,14 @@ const EVIDENCE_TOKENS = {
87
105
  traceability: ['trace_id', 'span_id', 'request_id', 'task_id', 'error_id', 'trace verified', '链路', '追踪'],
88
106
  redaction: ['redaction', 'redact', 'mask', 'masked', 'pii', 'secret', 'token redacted', '脱敏', '敏感字段'],
89
107
  'business-guardrails': ['quota', 'rate limit', 'abuse', 'budget', 'kill switch', 'cost_usd', '额度', '限流', '滥用', '止损'],
108
+ 'test-strategy': ['test-layer', 'test-size', 'test-scope', 'evidence-plan', 'testing pyramid', '测试策略', '测试分流', '单元测试', '集成测试', '端到端'],
90
109
  smoke: ['smoke', 'e2e', 'playwright', 'cypress', 'main flow', 'happy path', '冒烟', '主流程'],
91
110
  'feature-coverage': ['feature coverage', 'acceptance', 'tasks done', 'openprd tasks', '验收', '功能覆盖', '任务完成'],
111
+ 'visual-review': ['visual-compare', 'visual-before-after', 'visual-focus-board', 'visual-parallel-board', 'reference-actual', 'before-after', 'focus-board', 'parallel-board', '效果图', '实现截图', '修改前', '修改后', '视觉对比', '局部焦点证据板', '并行实验证据板'],
92
112
  'normal-performance': ['performance', 'perf', 'benchmark', 'latency', 'p95', 'lighthouse', 'k6', '性能', '耗时'],
93
113
  'extreme-performance': ['extreme', 'stress', 'load test', 'large-data', 'pressure', 'k6', '压力', '极端', '大数据'],
94
114
  knowledge: ['quality learn', 'incident', 'pattern', 'skill', '复盘', '经验', '沉淀'],
115
+ growth: ['growth ledger', 'completion checkpoint', 'openprd grow', 'workflow-gotcha', 'code-extension', '自我成长', '账本', '候选'],
95
116
  };
96
117
 
97
118
  function qualityPath(projectRoot, relativePath) {
@@ -126,6 +147,7 @@ function defaultQualityConfig() {
126
147
  currentEvidenceRequired: true,
127
148
  evidenceSources: [
128
149
  '.openprd/harness/test-reports',
150
+ '.openprd/harness/visual-reviews',
129
151
  '.openprd/quality/evidence',
130
152
  'test-results',
131
153
  'tests/reports',
@@ -133,8 +155,8 @@ function defaultQualityConfig() {
133
155
  ],
134
156
  scenarioProfiles: {
135
157
  core: ['smoke', 'feature-coverage'],
136
- frontend: ['smoke'],
137
- desktop: ['smoke'],
158
+ frontend: ['smoke', 'visual-review'],
159
+ desktop: ['smoke', 'visual-review'],
138
160
  backend: ['smoke', 'traceability'],
139
161
  businessCost: ['business-guardrails'],
140
162
  security: ['redaction'],
@@ -219,6 +241,11 @@ function defaultQualityConfig() {
219
241
  draftDir: '.openprd/knowledge/drafts',
220
242
  abstractionRequired: true,
221
243
  },
244
+ growth: {
245
+ enabled: true,
246
+ ledgerPath: OPENPRD_GROWTH_LEDGER,
247
+ completionCheckpointRequired: true,
248
+ },
222
249
  };
223
250
  }
224
251
 
@@ -263,6 +290,10 @@ function normalizeQualityConfig(config = {}) {
263
290
  ...defaults.knowledge,
264
291
  ...(config.knowledge ?? {}),
265
292
  },
293
+ growth: {
294
+ ...defaults.growth,
295
+ ...(config.growth ?? {}),
296
+ },
266
297
  };
267
298
  }
268
299
 
@@ -481,7 +512,32 @@ function detectScenarioTags({ activeChangeContext, activeTasks, businessGuardrai
481
512
  return [...tags];
482
513
  }
483
514
 
484
- function buildQualityPolicy({ config, activeChangeContext, activeTasks, businessGuardrails }) {
515
+ function isSubstantiveCompletionFile(relativePath) {
516
+ const normalized = String(relativePath ?? '').split(path.sep).join('/');
517
+ if (!normalized) {
518
+ return false;
519
+ }
520
+ if (/^(src|app|lib|server|scripts|test|tests|templates)\//.test(normalized)) {
521
+ return true;
522
+ }
523
+ return ['package.json', 'README.md', 'AGENTS.md'].includes(normalized);
524
+ }
525
+
526
+ function detectCompletionState({ files, activeTasks, evidenceFiles }) {
527
+ const substantiveFiles = files.filter((file) => isSubstantiveCompletionFile(file.path));
528
+ const hasEvidence = evidenceFiles.length > 0;
529
+ const activeTaskLedgerSettled = !activeTasks.activeChange || Number(activeTasks.pending ?? 0) === 0;
530
+ const postCompletionRequired = substantiveFiles.length > 0 && hasEvidence && activeTaskLedgerSettled;
531
+ return {
532
+ postCompletionRequired,
533
+ substantiveFiles: substantiveFiles.slice(0, 12).map((file) => file.path),
534
+ substantiveFileCount: substantiveFiles.length,
535
+ activeTaskLedgerSettled,
536
+ hasEvidence,
537
+ };
538
+ }
539
+
540
+ function buildQualityPolicy({ config, activeChangeContext, activeTasks, businessGuardrails, completionState }) {
485
541
  const scenarioTags = detectScenarioTags({ activeChangeContext, activeTasks, businessGuardrails });
486
542
  const profiles = config.evalHarness.scenarioProfiles ?? defaultQualityConfig().evalHarness.scenarioProfiles;
487
543
  const required = new Set();
@@ -508,6 +564,14 @@ function buildQualityPolicy({ config, activeChangeContext, activeTasks, business
508
564
  if (!config.knowledge.enabled) {
509
565
  required.delete('knowledge');
510
566
  }
567
+ if (completionState?.postCompletionRequired) {
568
+ if (config.knowledge.enabled) {
569
+ required.add('knowledge');
570
+ }
571
+ if (config.growth?.enabled && config.growth?.completionCheckpointRequired !== false) {
572
+ required.add('growth');
573
+ }
574
+ }
511
575
  return {
512
576
  scenarioTags,
513
577
  requiredGates: QUALITY_GATE_IDS.filter((gate) => required.has(gate)),
@@ -516,7 +580,7 @@ function buildQualityPolicy({ config, activeChangeContext, activeTasks, business
516
580
  };
517
581
  }
518
582
 
519
- function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, businessGuardrails, knowledge }) {
583
+ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, businessGuardrails, knowledge, growth, visualReview }) {
520
584
  const ledger = Object.fromEntries(QUALITY_GATE_IDS.map((gate) => {
521
585
  const tokens = EVIDENCE_TOKENS[gate] ?? [];
522
586
  const matches = evidenceFiles
@@ -577,8 +641,8 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
577
641
  ...knowledge.candidates.slice(0, 3).map((candidate) => ({ path: candidate, source: 'openprd-knowledge-candidate' })),
578
642
  ].slice(0, 12),
579
643
  summary: knowledge.candidates.length > 0
580
- ? `已有 ${knowledge.skills.length} 个项目经验 Skill,另有 ${knowledge.candidates.length} 个待确认 candidate`
581
- : `已有 ${knowledge.skills.length} 个项目经验 Skill`,
644
+ ? `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0},另有 ${knowledge.candidates.length} 个待确认 candidate`
645
+ : `已有 ${knowledge.skills.length} 个项目经验 Skill,命中 ${knowledge.adoption?.totals?.hit ?? 0} / 引用 ${knowledge.adoption?.totals?.referenced ?? 0} / 注入 ${knowledge.adoption?.totals?.injected ?? 0}`,
582
646
  };
583
647
  } else if (knowledge.candidates.length > 0) {
584
648
  ledger.knowledge = {
@@ -591,9 +655,44 @@ function buildEvidenceLedger({ evidenceFiles, activeTasks, observability, busine
591
655
  summary: `已有 ${knowledge.candidates.length} 个待确认 knowledge candidate`,
592
656
  };
593
657
  }
658
+ if (growth?.summary) {
659
+ const lifecycleCount = Number(growth.summary.lifecycleCount ?? 0);
660
+ const completionCheckpoints = Number(growth.summary.completionCheckpoints ?? 0);
661
+ if (lifecycleCount > 0 || completionCheckpoints > 0) {
662
+ ledger.growth = {
663
+ ...ledger.growth,
664
+ present: true,
665
+ sources: [
666
+ ...ledger.growth.sources,
667
+ { path: growth.ledgerPath ?? OPENPRD_GROWTH_LEDGER, source: 'openprd-growth-ledger' },
668
+ ].slice(0, 12),
669
+ summary: `growth 账本已有 ${growth.summary.eventCount ?? 0} 条事件,其中 completion checkpoint ${completionCheckpoints} 条`,
670
+ };
671
+ }
672
+ }
673
+ if (visualReview?.evidence) {
674
+ ledger['visual-review'] = {
675
+ ...ledger['visual-review'],
676
+ ...visualReview.evidence,
677
+ };
678
+ }
594
679
  return ledger;
595
680
  }
596
681
 
682
+ function describeFeatureCoverageLedger(activeTasks = {}) {
683
+ const total = Number(activeTasks.total ?? 0);
684
+ const done = Number(activeTasks.done ?? 0);
685
+ const pending = Number(activeTasks.pending ?? 0);
686
+ const blocked = Number(activeTasks.blocked ?? 0);
687
+ if (pending <= 0) {
688
+ return null;
689
+ }
690
+ const progress = total > 0 ? `${done}/${total}` : `${done}`;
691
+ const changeLabel = activeTasks.activeChange ? `当前变更 ${activeTasks.activeChange}` : '当前任务账本';
692
+ const blockedText = blocked > 0 ? `,其中 ${blocked} 个因依赖阻塞` : '';
693
+ return `${changeLabel} 仍有 ${pending} 个未完成任务(已完成 ${progress}${blockedText})。这通常表示任务账本尚未收口或覆盖证据未补齐,不等于当前实现失败。`;
694
+ }
695
+
597
696
  function detectObservability({ config, files, texts, packageJson }) {
598
697
  const { dependencyNames } = packageSignals(packageJson);
599
698
  const haystack = [
@@ -667,6 +766,8 @@ function detectEvalHarness({ config, files, texts, packageJson, activeTasks }) {
667
766
  const scriptEntries = Object.entries(scripts);
668
767
  const commandText = scriptEntries.map(([name, command]) => `${name}: ${command}`).join('\n');
669
768
  const hasTest = scriptEntries.some(([name]) => /(^|:)(test|check)$/.test(name)) || includesAny(commandText, ['node --test', 'vitest', 'jest', 'pytest']);
769
+ const testStrategy = summarizeTaskTestStrategies(activeTasks.tasks ?? []);
770
+ const testCapabilities = detectTestStrategyCapabilities({ scripts, files, dependencyNames });
670
771
  const smokeCommands = scriptEntries
671
772
  .filter(([name, command]) => /smoke|e2e|playwright|cypress|test:ui/i.test(`${name} ${command}`))
672
773
  .map(([name, command]) => `${name}: ${command}`);
@@ -693,11 +794,17 @@ function detectEvalHarness({ config, files, texts, packageJson, activeTasks }) {
693
794
  warnings.push('未检测到极端数据 fixtures 或压力场景数据。');
694
795
  }
695
796
  if (activeTasks.total > 0 && activeTasks.pending > 0) {
696
- warnings.push(`当前任务清单仍有 ${activeTasks.pending} 个未完成条目,功能覆盖不能判定为完整。`);
797
+ warnings.push(describeFeatureCoverageLedger(activeTasks) ?? `当前任务清单仍有 ${activeTasks.pending} 个未完成条目,功能覆盖不能判定为完整。`);
697
798
  }
799
+ warnings.push(...testStrategy.warnings);
698
800
  return {
699
801
  status: hasSmoke && hasPerf && hasExtremeFixtures && activeTasks.pending === 0 ? 'pass' : 'needs-attention',
700
802
  hasUnitOrCommandTests: hasTest,
803
+ testStrategy: {
804
+ status: testStrategy.total === 0 || testStrategy.warnings.length > 0 ? 'needs-attention' : 'pass',
805
+ ...testStrategy,
806
+ capabilities: testCapabilities,
807
+ },
701
808
  smoke: {
702
809
  present: hasSmoke,
703
810
  commands: smokeCommands,
@@ -800,28 +907,65 @@ async function readActiveTasks(projectRoot) {
800
907
  for (const relativePath of taskFiles) {
801
908
  const text = await readText(cjoin(projectRoot, relativePath)).catch(() => '');
802
909
  const lines = text.split(/\r?\n/);
910
+ let currentTask = null;
803
911
  for (let index = 0; index < lines.length; index += 1) {
804
912
  const match = lines[index].match(/^\s*-\s+\[([ xX~-])\]\s+(.+)$/);
805
913
  if (match) {
806
914
  const done = /x/i.test(match[1]);
807
915
  const blocked = match[1] === '~' || /blocked|阻塞/i.test(match[2]);
808
- tasks.push({
809
- title: match[2].trim(),
916
+ const title = match[2].trim();
917
+ const structured = title.match(/^(T\d{3}\.\d+)\s+(.+)$/);
918
+ currentTask = {
919
+ id: structured?.[1] ?? null,
920
+ title: structured?.[2]?.trim() ?? title,
810
921
  done,
811
922
  blocked,
812
923
  source: relativePath,
813
924
  line: index + 1,
814
- });
925
+ metadata: {},
926
+ };
927
+ tasks.push(currentTask);
928
+ continue;
929
+ }
930
+ const metadataMatch = lines[index].match(/^\s{2,}-\s+([a-z0-9_-]+):\s*(.*)$/i);
931
+ if (currentTask && metadataMatch) {
932
+ currentTask.metadata[metadataMatch[1].toLowerCase()] = metadataMatch[2].trim();
815
933
  }
816
934
  }
817
935
  }
936
+ const taskById = new Map(tasks.filter((task) => task.id).map((task) => [task.id, task]));
937
+ const blockedTasks = tasks
938
+ .filter((task) => !task.done)
939
+ .map((task) => {
940
+ const deps = parseOpenSpecTaskDeps(task.metadata?.deps);
941
+ const missing = [];
942
+ const incomplete = [];
943
+ for (const depId of deps) {
944
+ const dependency = taskById.get(depId);
945
+ if (!dependency) {
946
+ missing.push(depId);
947
+ continue;
948
+ }
949
+ if (!dependency.done) {
950
+ incomplete.push(depId);
951
+ }
952
+ }
953
+ return {
954
+ task,
955
+ deps,
956
+ missing,
957
+ incomplete,
958
+ ready: missing.length === 0 && incomplete.length === 0,
959
+ };
960
+ })
961
+ .filter((item) => !item.ready);
818
962
  return {
819
963
  activeChange,
820
964
  total: tasks.length,
821
965
  done: tasks.filter((task) => task.done).length,
822
- pending: tasks.filter((task) => !task.done && !task.blocked).length,
823
- blocked: tasks.filter((task) => task.blocked).length,
824
- tasks: tasks.slice(0, 50),
966
+ pending: tasks.filter((task) => !task.done).length,
967
+ blocked: blockedTasks.length,
968
+ tasks,
825
969
  };
826
970
  }
827
971
 
@@ -843,7 +987,7 @@ async function listKnowledgeFiles(projectRoot) {
843
987
  return collected;
844
988
  }
845
989
 
846
- function detectKnowledge({ config, knowledgeFiles }) {
990
+ function detectKnowledge({ config, knowledgeFiles, candidateState, knowledgeIndex, completionState }) {
847
991
  const skillDir = config.knowledge.skillDir ?? '.openprd/knowledge/skills';
848
992
  const candidateDir = config.knowledge.candidateDir ?? KNOWLEDGE_CANDIDATES_DIR;
849
993
  const draftDir = config.knowledge.draftDir ?? KNOWLEDGE_DRAFTS_DIR;
@@ -851,30 +995,53 @@ function detectKnowledge({ config, knowledgeFiles }) {
851
995
  .filter((file) => file.path.startsWith(skillDir.replace(/\//g, path.sep)) || file.path.startsWith(skillDir))
852
996
  .filter((file) => file.path.endsWith('SKILL.md'))
853
997
  .map((file) => file.path);
854
- const candidates = knowledgeFiles
998
+ const candidateFiles = knowledgeFiles
855
999
  .filter((file) => file.path.startsWith(candidateDir.replace(/\//g, path.sep)) || file.path.startsWith(candidateDir))
856
1000
  .filter((file) => file.path.endsWith('candidate.json'))
857
1001
  .map((file) => file.path);
1002
+ const pendingCandidates = (candidateState?.pending ?? []).map((candidate) => candidate.path).filter(Boolean);
1003
+ const reviewedCandidates = (candidateState?.reviewed ?? []).map((candidate) => candidate.path).filter(Boolean);
858
1004
  const drafts = knowledgeFiles
859
1005
  .filter((file) => file.path.startsWith(draftDir.replace(/\//g, path.sep)) || file.path.startsWith(draftDir))
860
1006
  .filter((file) => file.path.endsWith('SKILL.md'))
861
1007
  .map((file) => file.path);
862
1008
  const incidents = knowledgeFiles.filter((file) => /\.openprd[\\/]knowledge[\\/]incidents[\\/].+\.json$/.test(file.path));
1009
+ const adoption = buildKnowledgeAdoptionSummary(Array.isArray(knowledgeIndex?.skills) ? knowledgeIndex.skills : []);
863
1010
  const warnings = [];
864
- if (config.knowledge.enabled && skills.length === 0) {
1011
+ const hasReusableArtifact = skills.length > 0 || pendingCandidates.length > 0 || Number(candidateState?.counts?.total ?? 0) > 0;
1012
+ if (config.knowledge.enabled && skills.length === 0 && !hasReusableArtifact) {
865
1013
  warnings.push('项目级经验 skill 库尚为空;首次问题修复后应沉淀抽象经验。');
866
1014
  }
867
- if (config.knowledge.enabled && candidates.length > 0) {
868
- warnings.push(`当前有 ${candidates.length} 个待确认 knowledge candidate;本轮收工前应决定是否 promote 为正式项目经验。`);
1015
+ if (config.knowledge.enabled && pendingCandidates.length > 0) {
1016
+ warnings.push(`当前有 ${pendingCandidates.length} 个待确认 knowledge candidate;本轮收工前应决定 promote、reject 或 archive。`);
1017
+ }
1018
+ if (config.knowledge.enabled && completionState?.postCompletionRequired && !hasReusableArtifact) {
1019
+ warnings.push('本次已经达到可交付状态,但还没有自动生成 knowledge candidate;收工前至少保留一条可审查的项目经验草案。');
1020
+ }
1021
+ if (config.knowledge.enabled && skills.length > 0 && adoption.totals.referenced === 0) {
1022
+ warnings.push('项目级经验 skill 已产出,但还没有任何 run-context 引用记录;优先接入自动命中与注入链路。');
869
1023
  }
870
1024
  return {
871
- status: !config.knowledge.enabled || skills.length > 0 ? 'pass' : 'needs-attention',
1025
+ status: !config.knowledge.enabled || hasReusableArtifact ? 'pass' : 'needs-attention',
872
1026
  enabled: config.knowledge.enabled,
873
1027
  skillDir,
874
1028
  candidateDir,
875
1029
  draftDir,
876
1030
  skills,
877
- candidates,
1031
+ candidates: pendingCandidates,
1032
+ candidateFiles,
1033
+ candidateCounts: candidateState?.counts ?? {
1034
+ total: candidateFiles.length,
1035
+ pending: pendingCandidates.length,
1036
+ promoted: 0,
1037
+ rejected: 0,
1038
+ archived: 0,
1039
+ reviewed: reviewedCandidates.length,
1040
+ byStatus: {},
1041
+ },
1042
+ reviewedCandidates,
1043
+ candidateDetails: candidateState?.candidates ?? [],
1044
+ adoption,
878
1045
  drafts,
879
1046
  incidents: incidents.map((file) => file.path),
880
1047
  recommendations: [
@@ -886,6 +1053,39 @@ function detectKnowledge({ config, knowledgeFiles }) {
886
1053
  };
887
1054
  }
888
1055
 
1056
+ function detectGrowth({ config, growthLedger, completionState }) {
1057
+ const summary = growthLedger?.summary ?? {};
1058
+ const lifecycleCount = Number(summary.observed ?? 0)
1059
+ + Number(summary.manualApplied ?? 0)
1060
+ + Number(summary.autoApplied ?? 0)
1061
+ + Number(summary.reconciledAutoApplied ?? 0)
1062
+ + Number(summary.rejected ?? 0)
1063
+ + Number(summary.skipped ?? 0);
1064
+ const completionCheckpoints = Number(summary.completionCheckpoints ?? 0);
1065
+ const eventCount = Number(summary.eventCount ?? 0);
1066
+ const warnings = [];
1067
+ if (config.growth?.enabled && completionState?.postCompletionRequired && completionCheckpoints === 0 && lifecycleCount === 0) {
1068
+ warnings.push('本次已经达到可交付状态,但 .openprd/growth 还没有任何收工账本记录;至少补一条 completion checkpoint 或 growth 观察事件。');
1069
+ }
1070
+ if (config.growth?.enabled && completionState?.postCompletionRequired && completionCheckpoints > 0 && lifecycleCount === 0) {
1071
+ warnings.push('已记录完成检查点,但还没有新增 growth candidate;如果本轮形成了新的偏好、规则或工作流经验,收工前补一条 observe。');
1072
+ }
1073
+ return {
1074
+ status: !config.growth?.enabled || !completionState?.postCompletionRequired || completionCheckpoints > 0 || lifecycleCount > 0
1075
+ ? 'pass'
1076
+ : 'needs-attention',
1077
+ enabled: config.growth?.enabled !== false,
1078
+ ledgerPath: config.growth?.ledgerPath ?? OPENPRD_GROWTH_LEDGER,
1079
+ summary: {
1080
+ eventCount,
1081
+ lifecycleCount,
1082
+ completionCheckpoints,
1083
+ current: summary.current ?? { total: 0, pending: 0, applied: 0, rejected: 0 },
1084
+ },
1085
+ warnings,
1086
+ };
1087
+ }
1088
+
889
1089
  function buildGate({ id, label, baseStatus, baseWarnings, policy, evidenceLedger }) {
890
1090
  const required = policy.requiredGates.includes(id);
891
1091
  const evidence = evidenceLedger[id] ?? { present: false, sources: [], summary: '未找到本次执行证据' };
@@ -909,7 +1109,7 @@ function buildGate({ id, label, baseStatus, baseWarnings, policy, evidenceLedger
909
1109
  };
910
1110
  }
911
1111
 
912
- function buildGates({ observability, evalHarness, businessGuardrails, knowledge, policy, evidenceLedger }) {
1112
+ function buildGates({ observability, evalHarness, businessGuardrails, knowledge, growth, visualReview, policy, evidenceLedger }) {
913
1113
  return [
914
1114
  buildGate({
915
1115
  id: 'traceability',
@@ -935,6 +1135,14 @@ function buildGates({ observability, evalHarness, businessGuardrails, knowledge,
935
1135
  policy,
936
1136
  evidenceLedger,
937
1137
  }),
1138
+ buildGate({
1139
+ id: 'test-strategy',
1140
+ label: '分层测试策略',
1141
+ baseStatus: evalHarness.testStrategy.status,
1142
+ baseWarnings: evalHarness.testStrategy.warnings,
1143
+ policy,
1144
+ evidenceLedger,
1145
+ }),
938
1146
  buildGate({
939
1147
  id: 'smoke',
940
1148
  label: '冒烟测试体系',
@@ -947,7 +1155,17 @@ function buildGates({ observability, evalHarness, businessGuardrails, knowledge,
947
1155
  id: 'feature-coverage',
948
1156
  label: '任务与功能覆盖',
949
1157
  baseStatus: evalHarness.featureCoverage.activeTasks.pending === 0 ? 'pass' : 'needs-attention',
950
- baseWarnings: evalHarness.featureCoverage.activeTasks.pending === 0 ? [] : ['仍有未完成任务或缺少任务覆盖证据。'],
1158
+ baseWarnings: evalHarness.featureCoverage.activeTasks.pending === 0
1159
+ ? []
1160
+ : [describeFeatureCoverageLedger(evalHarness.featureCoverage.activeTasks) ?? '仍有未完成任务或缺少任务覆盖证据。'],
1161
+ policy,
1162
+ evidenceLedger,
1163
+ }),
1164
+ buildGate({
1165
+ id: 'visual-review',
1166
+ label: '视觉对比与自检',
1167
+ baseStatus: visualReview.status,
1168
+ baseWarnings: visualReview.warnings,
951
1169
  policy,
952
1170
  evidenceLedger,
953
1171
  }),
@@ -975,6 +1193,14 @@ function buildGates({ observability, evalHarness, businessGuardrails, knowledge,
975
1193
  policy,
976
1194
  evidenceLedger,
977
1195
  }),
1196
+ buildGate({
1197
+ id: 'growth',
1198
+ label: '自我成长账本',
1199
+ baseStatus: growth.status,
1200
+ baseWarnings: growth.warnings,
1201
+ policy,
1202
+ evidenceLedger,
1203
+ }),
978
1204
  ];
979
1205
  }
980
1206
 
@@ -982,8 +1208,8 @@ async function loadPackageJson(projectRoot) {
982
1208
  return readJson(cjoin(projectRoot, 'package.json')).catch(() => null);
983
1209
  }
984
1210
 
985
- async function buildQualityReport(projectRoot, config) {
986
- const id = reportId();
1211
+ async function buildQualityReport(projectRoot, config, options = {}) {
1212
+ const id = options.reportId ?? reportId();
987
1213
  const normalizedConfig = normalizeQualityConfig(config);
988
1214
  const files = await walkProject(projectRoot);
989
1215
  const texts = await readProjectTexts(projectRoot, files);
@@ -991,26 +1217,33 @@ async function buildQualityReport(projectRoot, config) {
991
1217
  const activeTasks = await readActiveTasks(projectRoot);
992
1218
  const activeChangeContext = await readActiveChangeContext(projectRoot, activeTasks.activeChange);
993
1219
  const evidenceFiles = await readEvidenceFiles(projectRoot, normalizedConfig);
1220
+ const visualArtifacts = await listVisualReviewArtifacts(projectRoot);
994
1221
  const knowledgeFiles = await listKnowledgeFiles(projectRoot);
1222
+ const knowledgeIndex = await readJson(qualityPath(projectRoot, KNOWLEDGE_INDEX)).catch(() => ({ version: 1, skills: [] }));
1223
+ const growthLedger = await readJson(qualityPath(projectRoot, normalizedConfig.growth?.ledgerPath ?? OPENPRD_GROWTH_LEDGER)).catch(() => null);
1224
+ const completionState = detectCompletionState({ files, activeTasks, evidenceFiles });
995
1225
  const observability = detectObservability({ config: normalizedConfig, files, texts, packageJson });
996
1226
  const evalHarness = detectEvalHarness({ config: normalizedConfig, files, texts, packageJson, activeTasks });
997
1227
  const businessGuardrails = detectBusinessGuardrails({ config: normalizedConfig, files, texts, packageJson });
998
- const knowledge = detectKnowledge({ config: normalizedConfig, knowledgeFiles });
999
- const policy = buildQualityPolicy({ config: normalizedConfig, activeChangeContext, activeTasks, businessGuardrails });
1000
- const evidenceLedger = buildEvidenceLedger({ evidenceFiles, activeTasks, observability, businessGuardrails, knowledge });
1001
- const gates = buildGates({ observability, evalHarness, businessGuardrails, knowledge, policy, evidenceLedger });
1228
+ const candidateState = await listKnowledgeCandidates(projectRoot, { status: 'all' }).catch(() => null);
1229
+ const knowledge = detectKnowledge({ config: normalizedConfig, knowledgeFiles, candidateState, knowledgeIndex, completionState });
1230
+ const growth = detectGrowth({ config: normalizedConfig, growthLedger, completionState });
1231
+ const policy = buildQualityPolicy({ config: normalizedConfig, activeChangeContext, activeTasks, businessGuardrails, completionState });
1232
+ const visualReview = detectVisualReview({ policy, activeChangeContext, activeTasks, visualArtifacts, includesAny });
1233
+ const evidenceLedger = buildEvidenceLedger({ evidenceFiles, activeTasks, observability, businessGuardrails, knowledge, growth, visualReview });
1234
+ const gates = buildGates({ observability, evalHarness, businessGuardrails, knowledge, growth, visualReview, policy, evidenceLedger });
1002
1235
  const blockingStatuses = new Set(['fail']);
1003
1236
  const attentionStatuses = new Set(['needs-attention', 'needs-evidence']);
1004
1237
  const readiness = {
1005
1238
  ok: !gates.some((gate) => blockingStatuses.has(gate.status)),
1006
1239
  productionReady: !gates.some((gate) => attentionStatuses.has(gate.status) || blockingStatuses.has(gate.status)),
1007
- enforcement: config.enforcement,
1240
+ enforcement: normalizedConfig.enforcement,
1008
1241
  failingGates: gates.filter((gate) => blockingStatuses.has(gate.status)).map((gate) => gate.id),
1009
1242
  attentionGates: gates.filter((gate) => attentionStatuses.has(gate.status)).map((gate) => gate.id),
1010
1243
  };
1011
1244
  evalHarness.executionEvidence = {
1012
1245
  sources: evidenceFiles.map((file) => ({ path: file.path, source: file.source, size: file.size })).slice(0, 120),
1013
- ledger: Object.fromEntries(['smoke', 'feature-coverage', 'normal-performance', 'extreme-performance'].map((gate) => [gate, evidenceLedger[gate]])),
1246
+ ledger: Object.fromEntries(['test-strategy', 'smoke', 'feature-coverage', 'visual-review', 'normal-performance', 'extreme-performance'].map((gate) => [gate, evidenceLedger[gate]])),
1014
1247
  };
1015
1248
  return {
1016
1249
  version: 1,
@@ -1032,7 +1265,10 @@ async function buildQualityReport(projectRoot, config) {
1032
1265
  observability,
1033
1266
  evalHarness,
1034
1267
  businessGuardrails,
1268
+ visualReview,
1035
1269
  knowledge,
1270
+ growth,
1271
+ completionState,
1036
1272
  configSnapshot: normalizedConfig,
1037
1273
  };
1038
1274
  }
@@ -1101,31 +1337,73 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1101
1337
  }
1102
1338
  const config = normalizeQualityConfig(await readJson(configPath));
1103
1339
  await ensureQualityDirs(projectRoot);
1104
- const report = await buildQualityReport(projectRoot, config);
1105
- const paths = await writeReport(projectRoot, report);
1340
+ const initialReport = await buildQualityReport(projectRoot, config);
1341
+ let report = initialReport;
1342
+ let paths = await writeReport(projectRoot, report);
1106
1343
  const knowledgeSignal = {
1107
1344
  kind: 'quality-verify',
1108
1345
  ok: report.readiness.productionReady,
1109
1346
  productionReady: report.readiness.productionReady,
1110
1347
  attentionGates: report.readiness.attentionGates,
1348
+ touchedFiles: report.completionState?.substantiveFiles ?? [],
1111
1349
  summary: `quality ${report.summary.status}`,
1112
1350
  };
1113
1351
  await recordKnowledgeReviewSignal(projectRoot, knowledgeSignal).catch(() => null);
1352
+ const growthCheckpoint = report.completionState?.postCompletionRequired && config.growth?.enabled !== false
1353
+ ? await recordGrowthCheckpointWorkspace(projectRoot, {
1354
+ outcome: 'quality-verify',
1355
+ reason: report.evalHarness?.featureCoverage?.activeTasks?.activeChange
1356
+ ?? report.completionState?.substantiveFiles?.slice(0, 4).join('|')
1357
+ ?? 'quality-post-completion',
1358
+ changed: report.completionState?.substantiveFiles ?? [],
1359
+ }).catch((error) => ({
1360
+ ok: false,
1361
+ action: 'growth-checkpoint',
1362
+ projectRoot,
1363
+ recorded: false,
1364
+ errors: [error instanceof Error ? error.message : String(error)],
1365
+ }))
1366
+ : {
1367
+ ok: true,
1368
+ action: 'growth-checkpoint',
1369
+ projectRoot,
1370
+ recorded: false,
1371
+ skipped: true,
1372
+ reason: 'completion-checkpoint-not-required',
1373
+ };
1114
1374
  const reviewSource = (await exists(qualityPath(projectRoot, OPENPRD_HARNESS_TURN_STATE)))
1115
1375
  ? OPENPRD_HARNESS_TURN_STATE
1116
1376
  : paths.jsonPath;
1117
- const knowledgeReview = await reviewKnowledgeWorkspace(projectRoot, {
1118
- from: reviewSource,
1119
- signal: knowledgeSignal,
1120
- requiredCorrelationFields: config.observability.requiredCorrelationFields,
1121
- }).catch((error) => ({
1122
- ok: false,
1123
- action: 'quality-knowledge-review',
1124
- skipped: false,
1125
- errors: [error instanceof Error ? error.message : String(error)],
1126
- }));
1377
+ const shouldAutoReviewKnowledge = Boolean(
1378
+ report.completionState?.postCompletionRequired
1379
+ && (report.knowledge?.skills?.length ?? 0) === 0
1380
+ && Number(report.knowledge?.candidateCounts?.total ?? 0) === 0
1381
+ );
1382
+ const knowledgeReview = shouldAutoReviewKnowledge
1383
+ ? await reviewKnowledgeWorkspace(projectRoot, {
1384
+ from: reviewSource,
1385
+ signal: knowledgeSignal,
1386
+ touchedFiles: report.completionState?.substantiveFiles ?? [],
1387
+ requiredCorrelationFields: config.observability.requiredCorrelationFields,
1388
+ }).catch((error) => ({
1389
+ ok: false,
1390
+ action: 'quality-knowledge-review',
1391
+ skipped: false,
1392
+ errors: [error instanceof Error ? error.message : String(error)],
1393
+ }))
1394
+ : {
1395
+ ok: true,
1396
+ action: 'quality-knowledge-review',
1397
+ skipped: true,
1398
+ reason: 'reusable-knowledge-artifact-already-exists',
1399
+ };
1400
+ report = await buildQualityReport(projectRoot, config, { reportId: initialReport.id });
1401
+ paths = await writeReport(projectRoot, report);
1127
1402
  const strict = options.strict === true;
1128
1403
  const blocking = (strict || config.enforcement === 'blocking') && !report.readiness.productionReady;
1404
+ const featureCoverageOnly = report.readiness.failingGates.length === 0
1405
+ && report.readiness.attentionGates.length === 1
1406
+ && report.readiness.attentionGates[0] === 'feature-coverage';
1129
1407
  return {
1130
1408
  ok: !blocking,
1131
1409
  action: 'quality-verify',
@@ -1134,8 +1412,16 @@ export async function verifyQualityWorkspace(projectRoot, options = {}) {
1134
1412
  reportPath: paths.jsonPath,
1135
1413
  htmlPath: paths.htmlPath,
1136
1414
  indexPath: paths.indexPath,
1415
+ growthCheckpoint,
1137
1416
  knowledgeReview,
1138
- errors: blocking ? ['Quality readiness is not production-ready; one or more required gates need evidence or attention.'] : [],
1417
+ errors: blocking
1418
+ ? [
1419
+ featureCoverageOnly
1420
+ ? (describeFeatureCoverageLedger(report.evalHarness?.featureCoverage?.activeTasks ?? null)
1421
+ ?? '当前 feature-coverage 账本尚未收口,不等于当前实现失败。')
1422
+ : 'Quality readiness is not production-ready; one or more required gates need evidence or attention.',
1423
+ ]
1424
+ : [],
1139
1425
  };
1140
1426
  }
1141
1427