@openprd/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openprd/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
package/src/knowledge.js
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cjoin, exists, readJson, writeJson, writeText } from './fs-utils.js';
|
|
4
|
+
import { resolveQualityLearningSource } from './quality-learning.js';
|
|
5
|
+
import { timestamp } from './time.js';
|
|
6
|
+
|
|
7
|
+
const KNOWLEDGE_DIR = cjoin('.openprd', 'knowledge');
|
|
8
|
+
const KNOWLEDGE_INDEX = cjoin(KNOWLEDGE_DIR, 'index.json');
|
|
9
|
+
const KNOWLEDGE_CANDIDATES_DIR = cjoin(KNOWLEDGE_DIR, 'candidates');
|
|
10
|
+
const KNOWLEDGE_DRAFTS_DIR = cjoin(KNOWLEDGE_DIR, 'drafts');
|
|
11
|
+
const OPENPRD_HARNESS_TURN_STATE = cjoin('.openprd', 'harness', 'turn-state.json');
|
|
12
|
+
|
|
13
|
+
const CODE_EXTENSIONS = new Set([
|
|
14
|
+
'.c',
|
|
15
|
+
'.cc',
|
|
16
|
+
'.cjs',
|
|
17
|
+
'.cpp',
|
|
18
|
+
'.cs',
|
|
19
|
+
'.css',
|
|
20
|
+
'.go',
|
|
21
|
+
'.h',
|
|
22
|
+
'.hpp',
|
|
23
|
+
'.html',
|
|
24
|
+
'.java',
|
|
25
|
+
'.js',
|
|
26
|
+
'.json',
|
|
27
|
+
'.jsx',
|
|
28
|
+
'.kt',
|
|
29
|
+
'.md',
|
|
30
|
+
'.mjs',
|
|
31
|
+
'.py',
|
|
32
|
+
'.rb',
|
|
33
|
+
'.rs',
|
|
34
|
+
'.scss',
|
|
35
|
+
'.sh',
|
|
36
|
+
'.swift',
|
|
37
|
+
'.toml',
|
|
38
|
+
'.ts',
|
|
39
|
+
'.tsx',
|
|
40
|
+
'.vue',
|
|
41
|
+
'.yaml',
|
|
42
|
+
'.yml',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
function knowledgePath(projectRoot, relativePath) {
|
|
46
|
+
return cjoin(projectRoot, relativePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultKnowledgeIndex() {
|
|
50
|
+
return {
|
|
51
|
+
version: 1,
|
|
52
|
+
updatedAt: timestamp(),
|
|
53
|
+
incidents: [],
|
|
54
|
+
patterns: [],
|
|
55
|
+
skills: [],
|
|
56
|
+
candidates: [],
|
|
57
|
+
drafts: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeStringList(value) {
|
|
62
|
+
if (!Array.isArray(value)) return [];
|
|
63
|
+
return value
|
|
64
|
+
.map((item) => String(item ?? '').trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function uniq(items) {
|
|
69
|
+
return [...new Set(items.filter(Boolean))];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function slugify(value, fallback = 'knowledge') {
|
|
73
|
+
const slug = String(value ?? '')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
76
|
+
.replace(/^-+|-+$/g, '')
|
|
77
|
+
.slice(0, 80);
|
|
78
|
+
return slug || fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function firstString(...values) {
|
|
82
|
+
for (const value of values) {
|
|
83
|
+
if (typeof value === 'string' && value.trim()) {
|
|
84
|
+
return value.trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toRelativeProjectPath(projectRoot, filePath) {
|
|
91
|
+
if (!filePath) return null;
|
|
92
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
93
|
+
? path.resolve(filePath)
|
|
94
|
+
: knowledgePath(projectRoot, filePath);
|
|
95
|
+
const relativePath = path.relative(projectRoot, absolutePath).split(path.sep).join('/');
|
|
96
|
+
return relativePath && !relativePath.startsWith('..') ? relativePath : String(filePath).split(path.sep).join('/');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readJsonObject(value) {
|
|
100
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function ensureKnowledgeWorkspace(projectRoot) {
|
|
104
|
+
await fs.mkdir(knowledgePath(projectRoot, cjoin(KNOWLEDGE_DIR, 'incidents')), { recursive: true });
|
|
105
|
+
await fs.mkdir(knowledgePath(projectRoot, cjoin(KNOWLEDGE_DIR, 'patterns')), { recursive: true });
|
|
106
|
+
await fs.mkdir(knowledgePath(projectRoot, cjoin(KNOWLEDGE_DIR, 'skills')), { recursive: true });
|
|
107
|
+
await fs.mkdir(knowledgePath(projectRoot, KNOWLEDGE_CANDIDATES_DIR), { recursive: true });
|
|
108
|
+
await fs.mkdir(knowledgePath(projectRoot, KNOWLEDGE_DRAFTS_DIR), { recursive: true });
|
|
109
|
+
const indexPath = knowledgePath(projectRoot, KNOWLEDGE_INDEX);
|
|
110
|
+
if (!(await exists(indexPath))) {
|
|
111
|
+
await writeJson(indexPath, defaultKnowledgeIndex());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readKnowledgeIndex(projectRoot) {
|
|
116
|
+
await ensureKnowledgeWorkspace(projectRoot);
|
|
117
|
+
const current = await readJson(knowledgePath(projectRoot, KNOWLEDGE_INDEX)).catch(() => defaultKnowledgeIndex());
|
|
118
|
+
return {
|
|
119
|
+
...defaultKnowledgeIndex(),
|
|
120
|
+
...current,
|
|
121
|
+
incidents: Array.isArray(current?.incidents) ? current.incidents : [],
|
|
122
|
+
patterns: Array.isArray(current?.patterns) ? current.patterns : [],
|
|
123
|
+
skills: Array.isArray(current?.skills) ? current.skills : [],
|
|
124
|
+
candidates: Array.isArray(current?.candidates) ? current.candidates : [],
|
|
125
|
+
drafts: Array.isArray(current?.drafts) ? current.drafts : [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function writeKnowledgeIndex(projectRoot, index) {
|
|
130
|
+
await writeJson(knowledgePath(projectRoot, KNOWLEDGE_INDEX), {
|
|
131
|
+
...defaultKnowledgeIndex(),
|
|
132
|
+
...index,
|
|
133
|
+
updatedAt: timestamp(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function upsertBy(items, key, value, max = 200) {
|
|
138
|
+
return [value, ...items.filter((item) => item?.[key] !== value[key])].slice(0, max);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function signalSummary(signal) {
|
|
142
|
+
if (!signal) return null;
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (signal.summary) parts.push(signal.summary);
|
|
145
|
+
if (Array.isArray(signal.attentionGates) && signal.attentionGates.length > 0) {
|
|
146
|
+
parts.push(`attention gates: ${signal.attentionGates.join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(signal.touchedFiles) && signal.touchedFiles.length > 0) {
|
|
149
|
+
parts.push(`touched: ${signal.touchedFiles.slice(0, 6).join(', ')}`);
|
|
150
|
+
}
|
|
151
|
+
return parts.join(' | ') || null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeReviewSignal(projectRoot, signal = {}) {
|
|
155
|
+
const touchedFiles = uniq(normalizeStringList(signal.touchedFiles).map((file) => toRelativeProjectPath(projectRoot, file)));
|
|
156
|
+
return {
|
|
157
|
+
id: firstString(signal.id, signal.kind, signal.source, signal.title, timestamp()) ?? timestamp(),
|
|
158
|
+
kind: firstString(signal.kind, signal.source, 'signal') ?? 'signal',
|
|
159
|
+
at: signal.at ?? timestamp(),
|
|
160
|
+
ok: typeof signal.ok === 'boolean' ? signal.ok : null,
|
|
161
|
+
productionReady: typeof signal.productionReady === 'boolean' ? signal.productionReady : null,
|
|
162
|
+
attentionGates: normalizeStringList(signal.attentionGates),
|
|
163
|
+
summary: firstString(signal.summary, signal.message, signal.reason),
|
|
164
|
+
touchedFiles,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isSubstantiveTouchedFile(filePath) {
|
|
169
|
+
const normalized = String(filePath ?? '').split(path.sep).join('/');
|
|
170
|
+
if (!normalized) return false;
|
|
171
|
+
if (/^docs\/basic\//.test(normalized)) return true;
|
|
172
|
+
if (/^skills\/.+\/SKILL\.md$/.test(normalized)) return true;
|
|
173
|
+
if (/^AGENTS\.md$/.test(normalized)) return true;
|
|
174
|
+
const extension = path.extname(normalized).toLowerCase();
|
|
175
|
+
if (!CODE_EXTENSIONS.has(extension)) return false;
|
|
176
|
+
if (/README/i.test(path.basename(normalized)) && !/^docs\/basic\//.test(normalized)) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildKnowledgeCategories({ source, touchedFiles, reviewSignals }) {
|
|
183
|
+
const categories = [];
|
|
184
|
+
const signalKinds = reviewSignals.map((signal) => signal.kind);
|
|
185
|
+
const touchesAgentInfra = touchedFiles.some((file) => /(agent|harness|hook|workflow|skill|prompt|quality|run-harness|loop|growth|standards)/i.test(file));
|
|
186
|
+
const hasRuntimePattern = source.rootCauseCandidates.length > 0 || source.eventNames.length > 0 || source.symptoms.length > 1;
|
|
187
|
+
const hasVerifiedOutcome = reviewSignals.some((signal) => (
|
|
188
|
+
signal.ok === true || signal.productionReady === true
|
|
189
|
+
) && ['quality-verify', 'run-verify', 'loop-finish'].includes(signal.kind));
|
|
190
|
+
const hasAttentionOutcome = reviewSignals.some((signal) => signal.ok === false || signal.productionReady === false || signal.attentionGates.length > 0);
|
|
191
|
+
|
|
192
|
+
if (hasRuntimePattern || hasAttentionOutcome) {
|
|
193
|
+
categories.push('hidden-debug-knowledge');
|
|
194
|
+
}
|
|
195
|
+
if (touchesAgentInfra) {
|
|
196
|
+
categories.push('agent-misjudgment');
|
|
197
|
+
}
|
|
198
|
+
if (hasVerifiedOutcome || signalKinds.includes('loop-finish') || signalKinds.includes('run-verify')) {
|
|
199
|
+
categories.push('high-impact-fix');
|
|
200
|
+
}
|
|
201
|
+
return uniq(categories);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function categoryReason(category) {
|
|
205
|
+
if (category === 'hidden-debug-knowledge') {
|
|
206
|
+
return '本轮结果里已经出现可复用的症状、排查线索或根因模式,不应该只留在当前对话里。';
|
|
207
|
+
}
|
|
208
|
+
if (category === 'agent-misjudgment') {
|
|
209
|
+
return '这次改动直接影响 Agent / harness / hook / skill 行为,后续很容易再次踩到同类判断问题。';
|
|
210
|
+
}
|
|
211
|
+
if (category === 'high-impact-fix') {
|
|
212
|
+
return '这次修复已经带有验证或收尾证据,适合尽快抽象成项目级研发经验。';
|
|
213
|
+
}
|
|
214
|
+
return '这次实现已经具备沉淀项目经验的价值。';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function deriveKnowledgeNames(source) {
|
|
218
|
+
const sourceRef = source?.sourceId ?? source?.title ?? source?.status ?? 'diagnostic';
|
|
219
|
+
const sourceKind = source?.kind === 'quality-report' ? 'quality' : 'diagnostic';
|
|
220
|
+
const incidentId = source?.kind === 'quality-report'
|
|
221
|
+
? `incident-${sourceRef}`
|
|
222
|
+
: `incident-${slugify(sourceRef, 'diagnostic')}`;
|
|
223
|
+
const patternId = `${sourceKind}-${slugify(sourceRef, sourceKind)}`;
|
|
224
|
+
const skillName = `openprd-experience-${slugify(patternId)}`;
|
|
225
|
+
return { incidentId, patternId, skillName };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildTurnReviewTitle(raw, source) {
|
|
229
|
+
return firstString(
|
|
230
|
+
raw?.title,
|
|
231
|
+
raw?.summary?.title,
|
|
232
|
+
raw?.promptPreview,
|
|
233
|
+
raw?.prompt,
|
|
234
|
+
source.title,
|
|
235
|
+
source.sourceId,
|
|
236
|
+
'项目经验草案',
|
|
237
|
+
) ?? '项目经验草案';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function loadRawReviewInput(projectRoot, from) {
|
|
241
|
+
if (!from) return { sourcePath: null, raw: null };
|
|
242
|
+
const resolved = path.isAbsolute(from) ? path.resolve(from) : knowledgePath(projectRoot, from);
|
|
243
|
+
const stat = await fs.stat(resolved).catch(() => null);
|
|
244
|
+
if (!stat) {
|
|
245
|
+
return { sourcePath: resolved, raw: null };
|
|
246
|
+
}
|
|
247
|
+
if (stat.isDirectory()) {
|
|
248
|
+
const diagnosticPath = path.join(resolved, 'diagnostic-report.json');
|
|
249
|
+
const diagnostic = await readJson(diagnosticPath).catch(() => null);
|
|
250
|
+
return { sourcePath: resolved, raw: readJsonObject(diagnostic) };
|
|
251
|
+
}
|
|
252
|
+
const parsed = await readJson(resolved).catch(() => null);
|
|
253
|
+
return { sourcePath: resolved, raw: readJsonObject(parsed) };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderList(items, fallback) {
|
|
257
|
+
const list = items.filter(Boolean);
|
|
258
|
+
if (list.length === 0) {
|
|
259
|
+
return `- ${fallback}`;
|
|
260
|
+
}
|
|
261
|
+
return list.map((item) => `- ${item}`).join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderKnowledgeDraftSkill({ skillName, candidate, source, relativeCandidateDir }) {
|
|
265
|
+
const triggerItems = uniq([
|
|
266
|
+
...candidate.reasons,
|
|
267
|
+
...source.symptoms.map((item) => `症状: ${item}`),
|
|
268
|
+
...candidate.reviewSignals.map((signal) => {
|
|
269
|
+
const summary = signalSummary(signal);
|
|
270
|
+
return summary ? `${signal.kind}: ${summary}` : signal.kind;
|
|
271
|
+
}),
|
|
272
|
+
]);
|
|
273
|
+
const inspectItems = uniq([
|
|
274
|
+
...candidate.touchedFiles.map((file) => `\`${file}\``),
|
|
275
|
+
...source.evidenceSources.map((item) => `\`${item.path}\``),
|
|
276
|
+
]);
|
|
277
|
+
const verificationItems = uniq([
|
|
278
|
+
...candidate.reviewSignals.map((signal) => signal.summary).filter(Boolean),
|
|
279
|
+
...source.verificationSteps,
|
|
280
|
+
]);
|
|
281
|
+
return `---
|
|
282
|
+
name: ${skillName}
|
|
283
|
+
description: OpenPrd 在本轮回顾时自动生成的待确认项目经验草案。
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
# ${skillName}
|
|
287
|
+
|
|
288
|
+
> 状态:draft
|
|
289
|
+
> 候选目录:\`${relativeCandidateDir}\`
|
|
290
|
+
> Promote:\`openprd quality . --learn --from ${relativeCandidateDir}\`
|
|
291
|
+
|
|
292
|
+
## 为什么值得沉淀
|
|
293
|
+
|
|
294
|
+
${renderList(triggerItems, '本轮实现已经出现值得复用的排查或修复模式。')}
|
|
295
|
+
|
|
296
|
+
## 下次触发时先看什么
|
|
297
|
+
|
|
298
|
+
${renderList(inspectItems, '先看本轮 touched files 和已有诊断证据。')}
|
|
299
|
+
|
|
300
|
+
## 可复用模式
|
|
301
|
+
|
|
302
|
+
${renderList(source.rootCauseCandidates.map((candidateItem) => candidateItem.title), '先按本轮诊断线索复走一次,再补最小必要证据。')}
|
|
303
|
+
|
|
304
|
+
## 验证方式
|
|
305
|
+
|
|
306
|
+
${renderList(verificationItems, '修复后重新走一遍本轮验证链路,确认问题不再复现。')}
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildCandidateDiagnosticReport({ candidateId, title, summary, source, touchedFiles, reviewSignals }) {
|
|
311
|
+
return {
|
|
312
|
+
id: candidateId,
|
|
313
|
+
knowledgeCandidateId: candidateId,
|
|
314
|
+
title,
|
|
315
|
+
status: reviewSignals.some((signal) => signal.ok === false || signal.productionReady === false) ? 'needs-attention' : 'pass',
|
|
316
|
+
summary: {
|
|
317
|
+
title,
|
|
318
|
+
status: reviewSignals.some((signal) => signal.ok === false || signal.productionReady === false) ? 'needs-attention' : 'pass',
|
|
319
|
+
message: summary,
|
|
320
|
+
},
|
|
321
|
+
problem: summary,
|
|
322
|
+
message: summary,
|
|
323
|
+
touchedFiles,
|
|
324
|
+
reviewSignals,
|
|
325
|
+
runtimeEvents: reviewSignals.map((signal) => ({
|
|
326
|
+
eventName: signal.kind,
|
|
327
|
+
status: signal.ok === false || signal.productionReady === false ? 'needs-attention' : 'pass',
|
|
328
|
+
message: signal.summary ?? signal.kind,
|
|
329
|
+
touchedFiles: signal.touchedFiles,
|
|
330
|
+
at: signal.at,
|
|
331
|
+
})),
|
|
332
|
+
timeline: reviewSignals.map((signal) => ({
|
|
333
|
+
event: signal.kind,
|
|
334
|
+
message: signal.summary ?? signal.kind,
|
|
335
|
+
status: signal.ok === false || signal.productionReady === false ? 'needs-attention' : 'pass',
|
|
336
|
+
line: null,
|
|
337
|
+
at: signal.at,
|
|
338
|
+
})),
|
|
339
|
+
rootCauseCandidates: source.rootCauseCandidates.length > 0
|
|
340
|
+
? source.rootCauseCandidates
|
|
341
|
+
: source.symptoms.map((symptom) => ({ title: symptom })),
|
|
342
|
+
verificationSteps: uniq([
|
|
343
|
+
...reviewSignals.map((signal) => signal.summary).filter(Boolean),
|
|
344
|
+
...source.verificationSteps,
|
|
345
|
+
]),
|
|
346
|
+
prevention: uniq([
|
|
347
|
+
'把本轮修复抽象成项目级 skill,而不是只保留一次性聊天上下文。',
|
|
348
|
+
...source.prevention,
|
|
349
|
+
]),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildKnowledgeCandidateMeta({
|
|
354
|
+
projectRoot,
|
|
355
|
+
candidateId,
|
|
356
|
+
candidatePath,
|
|
357
|
+
draftSkillPath,
|
|
358
|
+
candidateDir,
|
|
359
|
+
source,
|
|
360
|
+
title,
|
|
361
|
+
summary,
|
|
362
|
+
categories,
|
|
363
|
+
reasons,
|
|
364
|
+
touchedFiles,
|
|
365
|
+
reviewSignals,
|
|
366
|
+
existingCandidate,
|
|
367
|
+
}) {
|
|
368
|
+
return {
|
|
369
|
+
version: 1,
|
|
370
|
+
candidateId,
|
|
371
|
+
status: existingCandidate?.status === 'promoted' ? 'promoted' : 'pending-review',
|
|
372
|
+
createdAt: existingCandidate?.createdAt ?? timestamp(),
|
|
373
|
+
updatedAt: timestamp(),
|
|
374
|
+
sourceKind: source.kind,
|
|
375
|
+
sourceRef: source.sourceId,
|
|
376
|
+
title,
|
|
377
|
+
summary,
|
|
378
|
+
categories,
|
|
379
|
+
reasons,
|
|
380
|
+
touchedFiles,
|
|
381
|
+
reviewSignals,
|
|
382
|
+
files: {
|
|
383
|
+
candidate: candidatePath,
|
|
384
|
+
candidateDir,
|
|
385
|
+
draftSkill: draftSkillPath,
|
|
386
|
+
},
|
|
387
|
+
suggestedLearnCommand: `openprd quality . --learn --from ${path.relative(projectRoot, candidateDir) || '.'}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function recordKnowledgeReviewSignal(projectRoot, signal = {}) {
|
|
392
|
+
const statePath = knowledgePath(projectRoot, OPENPRD_HARNESS_TURN_STATE);
|
|
393
|
+
if (!(await exists(statePath))) {
|
|
394
|
+
return { ok: true, recorded: false, reason: 'turn-state-missing', turnStatePath: statePath };
|
|
395
|
+
}
|
|
396
|
+
const state = await readJson(statePath).catch(() => null);
|
|
397
|
+
const current = readJsonObject(state);
|
|
398
|
+
if (!current) {
|
|
399
|
+
return { ok: true, recorded: false, reason: 'turn-state-invalid', turnStatePath: statePath };
|
|
400
|
+
}
|
|
401
|
+
const normalized = normalizeReviewSignal(projectRoot, signal);
|
|
402
|
+
const existingSignals = Array.isArray(current.reviewSignals) ? current.reviewSignals : [];
|
|
403
|
+
const reviewSignals = [normalized, ...existingSignals.filter((item) => item?.id !== normalized.id)].slice(0, 24);
|
|
404
|
+
const touchedFiles = uniq([
|
|
405
|
+
...normalizeStringList(current.touchedFiles).map((file) => toRelativeProjectPath(projectRoot, file)),
|
|
406
|
+
...normalized.touchedFiles,
|
|
407
|
+
]);
|
|
408
|
+
const runtimeEvents = Array.isArray(current.runtimeEvents) ? current.runtimeEvents : [];
|
|
409
|
+
const timeline = Array.isArray(current.timeline) ? current.timeline : [];
|
|
410
|
+
await writeJson(statePath, {
|
|
411
|
+
...current,
|
|
412
|
+
touchedFiles,
|
|
413
|
+
reviewSignals,
|
|
414
|
+
runtimeEvents: [
|
|
415
|
+
{
|
|
416
|
+
eventName: normalized.kind,
|
|
417
|
+
status: normalized.ok === false || normalized.productionReady === false ? 'needs-attention' : 'pass',
|
|
418
|
+
message: normalized.summary ?? normalized.kind,
|
|
419
|
+
at: normalized.at,
|
|
420
|
+
},
|
|
421
|
+
...runtimeEvents,
|
|
422
|
+
].slice(0, 32),
|
|
423
|
+
timeline: [
|
|
424
|
+
{
|
|
425
|
+
event: normalized.kind,
|
|
426
|
+
message: normalized.summary ?? normalized.kind,
|
|
427
|
+
status: normalized.ok === false || normalized.productionReady === false ? 'needs-attention' : 'pass',
|
|
428
|
+
at: normalized.at,
|
|
429
|
+
},
|
|
430
|
+
...timeline,
|
|
431
|
+
].slice(0, 32),
|
|
432
|
+
updatedAt: timestamp(),
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
ok: true,
|
|
436
|
+
recorded: true,
|
|
437
|
+
turnStatePath: statePath,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
442
|
+
await ensureKnowledgeWorkspace(projectRoot);
|
|
443
|
+
const from = options.from ?? ((await exists(knowledgePath(projectRoot, OPENPRD_HARNESS_TURN_STATE))) ? OPENPRD_HARNESS_TURN_STATE : null);
|
|
444
|
+
if (!from) {
|
|
445
|
+
return {
|
|
446
|
+
ok: true,
|
|
447
|
+
action: 'quality-knowledge-review',
|
|
448
|
+
skipped: true,
|
|
449
|
+
reason: 'no-review-source',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const rawInput = await loadRawReviewInput(projectRoot, from);
|
|
454
|
+
const resolved = await resolveQualityLearningSource(projectRoot, {
|
|
455
|
+
from,
|
|
456
|
+
latestReportPath: options.latestReportPath ?? null,
|
|
457
|
+
requiredCorrelationFields: Array.isArray(options.requiredCorrelationFields) ? options.requiredCorrelationFields : [],
|
|
458
|
+
});
|
|
459
|
+
if (!resolved.ok) {
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
action: 'quality-knowledge-review',
|
|
463
|
+
skipped: true,
|
|
464
|
+
reason: resolved.error,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const source = resolved.source;
|
|
469
|
+
const raw = readJsonObject(rawInput.raw) ?? {};
|
|
470
|
+
const touchedFiles = uniq([
|
|
471
|
+
...normalizeStringList(raw.touchedFiles).map((file) => toRelativeProjectPath(projectRoot, file)),
|
|
472
|
+
...normalizeStringList(options.touchedFiles).map((file) => toRelativeProjectPath(projectRoot, file)),
|
|
473
|
+
]).filter(Boolean);
|
|
474
|
+
const substantiveTouchedFiles = touchedFiles.filter(isSubstantiveTouchedFile);
|
|
475
|
+
const embeddedSignals = Array.isArray(raw.reviewSignals) ? raw.reviewSignals : [];
|
|
476
|
+
const reviewSignals = uniq([
|
|
477
|
+
...embeddedSignals.map((signal) => JSON.stringify(normalizeReviewSignal(projectRoot, signal))),
|
|
478
|
+
...(options.signal ? [JSON.stringify(normalizeReviewSignal(projectRoot, options.signal))] : []),
|
|
479
|
+
]).map((entry) => JSON.parse(entry));
|
|
480
|
+
const categories = buildKnowledgeCategories({ source, touchedFiles: substantiveTouchedFiles, reviewSignals });
|
|
481
|
+
const reasons = categories.map(categoryReason);
|
|
482
|
+
const hasStrongSignal = categories.length > 0
|
|
483
|
+
|| source.rootCauseCandidates.length > 0
|
|
484
|
+
|| source.symptoms.length > 1
|
|
485
|
+
|| reviewSignals.some((signal) => signal.ok === true || signal.productionReady === true);
|
|
486
|
+
|
|
487
|
+
if (substantiveTouchedFiles.length === 0 || !hasStrongSignal) {
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
action: 'quality-knowledge-review',
|
|
491
|
+
skipped: true,
|
|
492
|
+
reason: substantiveTouchedFiles.length === 0 ? 'no-substantive-touched-files' : 'no-knowledge-signal',
|
|
493
|
+
sourceKind: source.kind,
|
|
494
|
+
sourcePath: source.sourcePath,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const title = buildTurnReviewTitle(raw, source);
|
|
499
|
+
const rawCandidateRef = firstString(raw.knowledgeCandidateId, raw.id);
|
|
500
|
+
const candidateId = rawCandidateRef
|
|
501
|
+
? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
|
|
502
|
+
: `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
|
|
503
|
+
const promotedSource = { ...source, sourceId: candidateId };
|
|
504
|
+
const names = deriveKnowledgeNames(promotedSource);
|
|
505
|
+
const candidateDir = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId));
|
|
506
|
+
const candidatePath = path.join(candidateDir, 'candidate.json');
|
|
507
|
+
const diagnosticReportPath = path.join(candidateDir, 'diagnostic-report.json');
|
|
508
|
+
const rootCausePath = path.join(candidateDir, 'root-cause-candidates.json');
|
|
509
|
+
const timelinePath = path.join(candidateDir, 'timeline.json');
|
|
510
|
+
const draftSkillPath = knowledgePath(projectRoot, cjoin(KNOWLEDGE_DRAFTS_DIR, names.skillName, 'SKILL.md'));
|
|
511
|
+
const existingCandidate = await readJson(candidatePath).catch(() => null);
|
|
512
|
+
const reviewSummary = [
|
|
513
|
+
`本轮修改了 ${substantiveTouchedFiles.length} 个可沉淀文件。`,
|
|
514
|
+
reasons[0] ?? '这次实现已经具备项目级经验抽象价值。',
|
|
515
|
+
reviewSignals.length > 0 ? `已记录 ${reviewSignals.length} 条回顾信号。` : null,
|
|
516
|
+
].filter(Boolean).join(' ');
|
|
517
|
+
const candidate = buildKnowledgeCandidateMeta({
|
|
518
|
+
projectRoot,
|
|
519
|
+
candidateId,
|
|
520
|
+
candidatePath,
|
|
521
|
+
draftSkillPath,
|
|
522
|
+
candidateDir,
|
|
523
|
+
source: promotedSource,
|
|
524
|
+
title,
|
|
525
|
+
summary: reviewSummary,
|
|
526
|
+
categories,
|
|
527
|
+
reasons,
|
|
528
|
+
touchedFiles: substantiveTouchedFiles,
|
|
529
|
+
reviewSignals,
|
|
530
|
+
existingCandidate: readJsonObject(existingCandidate) ?? null,
|
|
531
|
+
});
|
|
532
|
+
const relativeCandidateDir = path.relative(projectRoot, candidateDir).split(path.sep).join('/');
|
|
533
|
+
await writeJson(candidatePath, candidate);
|
|
534
|
+
await writeJson(diagnosticReportPath, buildCandidateDiagnosticReport({
|
|
535
|
+
candidateId,
|
|
536
|
+
title,
|
|
537
|
+
summary: reviewSummary,
|
|
538
|
+
source,
|
|
539
|
+
touchedFiles: substantiveTouchedFiles,
|
|
540
|
+
reviewSignals,
|
|
541
|
+
}));
|
|
542
|
+
await writeJson(rootCausePath, source.rootCauseCandidates.length > 0 ? source.rootCauseCandidates : substantiveTouchedFiles.map((file) => ({ title: `Inspect ${file}` })));
|
|
543
|
+
await writeJson(timelinePath, reviewSignals.map((signal) => ({
|
|
544
|
+
event: signal.kind,
|
|
545
|
+
message: signal.summary ?? signal.kind,
|
|
546
|
+
status: signal.ok === false || signal.productionReady === false ? 'needs-attention' : 'pass',
|
|
547
|
+
at: signal.at,
|
|
548
|
+
})));
|
|
549
|
+
await writeText(draftSkillPath, renderKnowledgeDraftSkill({
|
|
550
|
+
skillName: names.skillName,
|
|
551
|
+
candidate,
|
|
552
|
+
source,
|
|
553
|
+
relativeCandidateDir,
|
|
554
|
+
}));
|
|
555
|
+
|
|
556
|
+
const index = await readKnowledgeIndex(projectRoot);
|
|
557
|
+
await writeKnowledgeIndex(projectRoot, {
|
|
558
|
+
...index,
|
|
559
|
+
candidates: upsertBy(index.candidates, 'candidateId', {
|
|
560
|
+
candidateId,
|
|
561
|
+
status: candidate.status,
|
|
562
|
+
path: candidatePath,
|
|
563
|
+
sourceKind: promotedSource.kind,
|
|
564
|
+
sourceRef: promotedSource.sourceId,
|
|
565
|
+
title,
|
|
566
|
+
draftSkillPath,
|
|
567
|
+
}),
|
|
568
|
+
drafts: upsertBy(index.drafts, 'skillName', {
|
|
569
|
+
skillName: names.skillName,
|
|
570
|
+
path: draftSkillPath,
|
|
571
|
+
candidateId,
|
|
572
|
+
status: candidate.status,
|
|
573
|
+
}),
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
ok: true,
|
|
578
|
+
action: 'quality-knowledge-review',
|
|
579
|
+
skipped: false,
|
|
580
|
+
projectRoot,
|
|
581
|
+
sourceKind: source.kind,
|
|
582
|
+
sourcePath: source.sourcePath,
|
|
583
|
+
candidateId,
|
|
584
|
+
skillName: names.skillName,
|
|
585
|
+
categories,
|
|
586
|
+
reasons,
|
|
587
|
+
summary: reviewSummary,
|
|
588
|
+
suggestedLearnCommand: candidate.suggestedLearnCommand,
|
|
589
|
+
files: {
|
|
590
|
+
candidate: candidatePath,
|
|
591
|
+
candidateDir,
|
|
592
|
+
diagnosticReport: diagnosticReportPath,
|
|
593
|
+
rootCauseCandidates: rootCausePath,
|
|
594
|
+
timeline: timelinePath,
|
|
595
|
+
draftSkill: draftSkillPath,
|
|
596
|
+
index: knowledgePath(projectRoot, KNOWLEDGE_INDEX),
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function candidateIdFromSourcePath(projectRoot, sourcePath) {
|
|
602
|
+
if (!sourcePath) return null;
|
|
603
|
+
const relative = toRelativeProjectPath(projectRoot, sourcePath);
|
|
604
|
+
const match = relative.match(/^\.openprd\/knowledge\/candidates\/([^/]+)/);
|
|
605
|
+
return match ? match[1] : null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export async function markKnowledgeCandidatePromoted(projectRoot, options = {}) {
|
|
609
|
+
await ensureKnowledgeWorkspace(projectRoot);
|
|
610
|
+
const candidateId = candidateIdFromSourcePath(projectRoot, options.sourcePath)
|
|
611
|
+
?? (Array.isArray(options.sourcePaths)
|
|
612
|
+
? options.sourcePaths.map((entry) => candidateIdFromSourcePath(projectRoot, entry)).find(Boolean)
|
|
613
|
+
: null);
|
|
614
|
+
if (!candidateId) {
|
|
615
|
+
return { ok: true, updated: false };
|
|
616
|
+
}
|
|
617
|
+
const candidatePath = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId, 'candidate.json'));
|
|
618
|
+
const candidate = await readJson(candidatePath).catch(() => null);
|
|
619
|
+
if (!candidate) {
|
|
620
|
+
return { ok: true, updated: false };
|
|
621
|
+
}
|
|
622
|
+
await writeJson(candidatePath, {
|
|
623
|
+
...candidate,
|
|
624
|
+
status: 'promoted',
|
|
625
|
+
promotedAt: timestamp(),
|
|
626
|
+
promotedSkillPath: options.skillPath ?? null,
|
|
627
|
+
promotedIncidentPath: options.incidentPath ?? null,
|
|
628
|
+
promotedPatternPath: options.patternPath ?? null,
|
|
629
|
+
updatedAt: timestamp(),
|
|
630
|
+
});
|
|
631
|
+
const index = await readKnowledgeIndex(projectRoot);
|
|
632
|
+
await writeKnowledgeIndex(projectRoot, {
|
|
633
|
+
...index,
|
|
634
|
+
candidates: upsertBy(index.candidates, 'candidateId', {
|
|
635
|
+
...(index.candidates.find((item) => item.candidateId === candidateId) ?? {}),
|
|
636
|
+
candidateId,
|
|
637
|
+
status: 'promoted',
|
|
638
|
+
path: candidatePath,
|
|
639
|
+
draftSkillPath: candidate.files?.draftSkill ?? null,
|
|
640
|
+
sourceKind: candidate.sourceKind ?? null,
|
|
641
|
+
sourceRef: candidate.sourceRef ?? null,
|
|
642
|
+
title: candidate.title ?? candidateId,
|
|
643
|
+
}),
|
|
644
|
+
drafts: candidate.files?.draftSkill
|
|
645
|
+
? upsertBy(index.drafts, 'skillName', {
|
|
646
|
+
skillName: path.basename(path.dirname(candidate.files.draftSkill)),
|
|
647
|
+
path: candidate.files.draftSkill,
|
|
648
|
+
candidateId,
|
|
649
|
+
status: 'promoted',
|
|
650
|
+
})
|
|
651
|
+
: index.drafts,
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
ok: true,
|
|
655
|
+
updated: true,
|
|
656
|
+
candidateId,
|
|
657
|
+
candidatePath,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export {
|
|
662
|
+
deriveKnowledgeNames,
|
|
663
|
+
ensureKnowledgeWorkspace,
|
|
664
|
+
KNOWLEDGE_CANDIDATES_DIR,
|
|
665
|
+
KNOWLEDGE_DRAFTS_DIR,
|
|
666
|
+
KNOWLEDGE_INDEX,
|
|
667
|
+
OPENPRD_HARNESS_TURN_STATE,
|
|
668
|
+
};
|