@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.
- package/.openprd/README.md +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +344 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +402 -441
- package/README_CN.md +4 -578
- package/README_EN.md +850 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- package/skills/openprd-requirement-intake/SKILL.md +39 -23
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +22 -8
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +265 -65
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +639 -117
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1176 -75
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/self-update.js +1 -1
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- 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
|
|
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
|
-
|
|
809
|
-
|
|
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
|
|
823
|
-
blocked:
|
|
824
|
-
tasks
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
868
|
-
warnings.push(`当前有 ${
|
|
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 ||
|
|
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
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
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:
|
|
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
|
|
1105
|
-
|
|
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
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
|
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
|
|