@openprd/cli 0.1.8 → 0.1.10
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/benchmarks/index.md +15 -1
- package/.openprd/config.yaml +10 -0
- package/.openprd/design/README.md +47 -0
- package/.openprd/design/README_EN.md +25 -0
- package/.openprd/design/active/asset-spec.md +15 -0
- package/.openprd/design/active/direction-plan.md +7 -0
- package/.openprd/design/active/facts-sheet.md +13 -0
- package/.openprd/design/active/image-preflight.md +9 -0
- package/.openprd/design/active/selected-direction.md +11 -0
- package/.openprd/design/anti-slop.md +16 -0
- package/.openprd/design/assets/dark-mesh.svg +14 -0
- package/.openprd/design/assets/paper-grid.svg +14 -0
- package/.openprd/design/assets/surface-presets.md +21 -0
- package/.openprd/design/assets/workbench-grid.svg +16 -0
- package/.openprd/design/checklists/ui-quality-gate.md +22 -0
- package/.openprd/design/components/component-catalog.md +42 -0
- package/.openprd/design/layouts/layout-catalog.json +45 -0
- package/.openprd/design/lenses/frontend-lenses.md +31 -0
- package/.openprd/design/recipes/content-experience.md +26 -0
- package/.openprd/design/recipes/operational-tool.md +25 -0
- package/.openprd/design/recipes/product-launch.md +26 -0
- package/.openprd/design/templates/README.md +36 -0
- package/.openprd/design/templates/README_EN.md +31 -0
- package/.openprd/design/templates/content-home.html +415 -0
- package/.openprd/design/templates/ops-dashboard.html +340 -0
- package/.openprd/design/templates/product-launch.html +360 -0
- package/.openprd/design/themes/theme-catalog.json +77 -0
- package/.openprd/engagements/active/prd.md +111 -100
- package/.openprd/schema/prd.schema.yaml +10 -0
- package/.openprd/templates/base/intake.md +4 -0
- package/.openprd/templates/base/prd.md +16 -6
- package/AGENTS.md +10 -7
- package/README.md +28 -10
- package/README_EN.md +23 -3
- package/package.json +41 -2
- package/scripts/openprd-codex-isolated-worker.mjs +799 -0
- package/skills/openprd-benchmark-router/SKILL.md +4 -0
- package/skills/openprd-frontend-design/SKILL.md +161 -0
- package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
- package/skills/openprd-harness/SKILL.md +38 -21
- package/skills/openprd-learning-review/SKILL.md +2 -1
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
- package/skills/openprd-quality/SKILL.md +6 -4
- package/skills/openprd-requirement-intake/SKILL.md +27 -5
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
- package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
- package/skills/openprd-router/SKILL.md +13 -5
- package/skills/openprd-shared/SKILL.md +38 -25
- package/src/agent-integration.js +126 -75
- package/src/brainstorm-artifacts.js +1657 -0
- package/src/brainstorm-presentation.js +223 -0
- package/src/brainstorm.js +603 -0
- package/src/cli/args.js +51 -3
- package/src/cli/doctor-print.js +34 -8
- package/src/cli/print.js +6 -0
- package/src/cli/quality-print.js +70 -0
- package/src/cli/run-print.js +43 -1
- package/src/cli/shared-print.js +33 -2
- package/src/cli/workflow-print.js +21 -0
- package/src/codex-hook-runner-template.mjs +858 -65
- package/src/design-starter-support.js +462 -0
- package/src/design-starter.js +1207 -0
- package/src/knowledge.js +377 -21
- package/src/learning-review.js +3 -3
- package/src/loop.js +839 -101
- package/src/openprd.js +86 -1
- package/src/openspec/change-validate.js +1 -0
- package/src/prd-core.js +18 -0
- package/src/quality-learning.js +50 -6
- package/src/quality.js +2 -2
- package/src/run-harness.js +191 -29
- package/src/self-update.js +301 -4
- package/src/standards.js +46 -5
- package/src/visual-prepare.js +940 -0
- package/src/workspace-core.js +137 -0
- package/src/workspace-workflow.js +190 -7
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
- package/.openprd/discovery/config.json +0 -35
- package/.openprd/engagements/active/flows.md +0 -35
- package/.openprd/engagements/active/handoff.md +0 -16
- package/.openprd/engagements/active/review.html +0 -61
- package/.openprd/engagements/active/roles.md +0 -22
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
- package/.openprd/knowledge/index.json +0 -47
- package/.openprd/reviews/v0001.html +0 -1322
- package/.openprd/reviews/v0002.html +0 -1150
- package/.openprd/reviews/v0003.html +0 -1150
- package/.openprd/reviews/v0004.html +0 -1150
- package/.openprd/reviews/v0005.html +0 -1150
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/state/changes.json +0 -27
- package/.openprd/state/current.json +0 -505
- package/.openprd/state/release-ledger.json +0 -344
- package/.openprd/state/version-index.json +0 -67
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +0 -121
- package/.openprd/state/versions/v0001.md +0 -161
- package/.openprd/state/versions/v0002.json +0 -264
- package/.openprd/state/versions/v0002.md +0 -183
- package/.openprd/state/versions/v0003.json +0 -269
- package/.openprd/state/versions/v0003.md +0 -188
- package/.openprd/state/versions/v0004.json +0 -274
- package/.openprd/state/versions/v0004.md +0 -193
- package/.openprd/state/versions/v0005.json +0 -299
- package/.openprd/state/versions/v0005.md +0 -189
- 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-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +0 -102
- 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 +0 -102
- 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 +0 -114
- 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/src/knowledge.js
CHANGED
|
@@ -170,6 +170,10 @@ function normalizeSkillIndexEntry(entry = {}) {
|
|
|
170
170
|
]),
|
|
171
171
|
categories: normalizeStringList(skill.categories),
|
|
172
172
|
triggerHints: normalizeStringList(skill.triggerHints),
|
|
173
|
+
useWhen: firstString(skill.useWhen, skill.use_when),
|
|
174
|
+
applicability: normalizeStringList(skill.applicability),
|
|
175
|
+
antiPatterns: normalizeStringList(skill.antiPatterns),
|
|
176
|
+
reviewFirst: normalizeStringList(skill.reviewFirst),
|
|
173
177
|
touchedFiles: normalizeStringList(skill.touchedFiles),
|
|
174
178
|
evidencePaths: normalizeStringList(skill.evidencePaths),
|
|
175
179
|
rootCauseLabels: normalizeStringList(skill.rootCauseLabels),
|
|
@@ -213,6 +217,37 @@ function sortByLength(items = []) {
|
|
|
213
217
|
return [...items].sort((left, right) => String(right).length - String(left).length);
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
function isGenericKnowledgeDescription(value) {
|
|
221
|
+
const normalized = normalizeSearchText(value);
|
|
222
|
+
if (!normalized) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return [
|
|
226
|
+
'自动沉淀的项目级排查经验',
|
|
227
|
+
'待确认项目经验草案',
|
|
228
|
+
'自动生成的待确认项目经验草案',
|
|
229
|
+
'项目级排查经验',
|
|
230
|
+
'项目经验草案',
|
|
231
|
+
].some((needle) => normalized.includes(normalizeSearchText(needle)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function stripUseWhenPrefix(value) {
|
|
235
|
+
return String(value ?? '').trim().replace(/^use when\b[::]?\s*/i, '').trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function deriveSkillUseWhen({ useWhen, description, applicability = [], triggerHints = [] }) {
|
|
239
|
+
const explicit = firstString(useWhen);
|
|
240
|
+
if (explicit) {
|
|
241
|
+
return explicit;
|
|
242
|
+
}
|
|
243
|
+
const desc = firstString(description);
|
|
244
|
+
if (desc && !isGenericKnowledgeDescription(desc)) {
|
|
245
|
+
return desc;
|
|
246
|
+
}
|
|
247
|
+
const fallback = firstString(applicability[0], triggerHints[0], desc);
|
|
248
|
+
return fallback ? `Use when ${stripUseWhenPrefix(fallback)}` : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
216
251
|
function scoreQueryAgainstFields(queryText, queryTokens, fields = []) {
|
|
217
252
|
let score = 0;
|
|
218
253
|
const matchedOn = [];
|
|
@@ -264,22 +299,47 @@ function parseMarkdownSectionList(markdown, headings = []) {
|
|
|
264
299
|
return uniq(collected);
|
|
265
300
|
}
|
|
266
301
|
|
|
267
|
-
function
|
|
302
|
+
function parseFrontmatterValue(markdown, key) {
|
|
268
303
|
const text = String(markdown ?? '');
|
|
269
304
|
const frontmatter = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
305
|
+
if (!frontmatter) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const pattern = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.+?)\\s*$`);
|
|
309
|
+
const line = frontmatter[1]
|
|
310
|
+
.split(/\r?\n/)
|
|
311
|
+
.map((item) => item.trim())
|
|
312
|
+
.find((item) => pattern.test(item));
|
|
313
|
+
if (!line) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
const match = line.match(pattern);
|
|
317
|
+
return match?.[1]?.trim() ?? null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseSkillMetadataFromText(markdown) {
|
|
321
|
+
const text = String(markdown ?? '');
|
|
322
|
+
const description = parseFrontmatterValue(text, 'description');
|
|
323
|
+
const useWhen = firstString(
|
|
324
|
+
parseFrontmatterValue(text, 'use_when'),
|
|
325
|
+
parseFrontmatterValue(text, 'use-when'),
|
|
326
|
+
/^use when\b/i.test(String(description ?? '')) ? description : null,
|
|
327
|
+
);
|
|
328
|
+
const triggerHints = parseMarkdownSectionList(text, ['触发场景', '触发条件', '同步触发条件', '收尾顺序']);
|
|
329
|
+
const applicability = parseMarkdownSectionList(text, ['适用范围', '适用时机', 'When to Use', 'When to use']);
|
|
330
|
+
const antiPatterns = parseMarkdownSectionList(text, ['不要直接套用', '不要在这些情况使用', '不要在这些情况直接套用', 'When Not to Use', 'Do Not Use When', '反模式', '常见误判']);
|
|
331
|
+
const reviewFirst = parseMarkdownSectionList(text, ['下次触发时先看什么', '先看什么', '先看哪些证据', '判断前先看', '排查顺序']);
|
|
332
|
+
const rootCauseLabels = parseMarkdownSectionList(text, ['可复用模式', '常见根因']);
|
|
277
333
|
const evidencePaths = uniq((text.match(/`([^`]+)`/g) ?? [])
|
|
278
334
|
.map((entry) => entry.replace(/`/g, '').trim())
|
|
279
335
|
.filter((entry) => entry.includes('/') || entry.endsWith('.md') || entry.endsWith('.js') || entry.endsWith('.ts')));
|
|
280
336
|
return {
|
|
281
337
|
description,
|
|
338
|
+
useWhen,
|
|
282
339
|
triggerHints,
|
|
340
|
+
applicability,
|
|
341
|
+
antiPatterns,
|
|
342
|
+
reviewFirst,
|
|
283
343
|
rootCauseLabels,
|
|
284
344
|
evidencePaths,
|
|
285
345
|
};
|
|
@@ -377,6 +437,18 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
|
|
|
377
437
|
...parsedSkill.triggerHints,
|
|
378
438
|
...candidateBundles.flatMap((bundle) => bundle.triggerHints),
|
|
379
439
|
]).slice(0, 24),
|
|
440
|
+
applicability: uniq([
|
|
441
|
+
...current.applicability,
|
|
442
|
+
...parsedSkill.applicability,
|
|
443
|
+
]).slice(0, 24),
|
|
444
|
+
antiPatterns: uniq([
|
|
445
|
+
...current.antiPatterns,
|
|
446
|
+
...parsedSkill.antiPatterns,
|
|
447
|
+
]).slice(0, 24),
|
|
448
|
+
reviewFirst: uniq([
|
|
449
|
+
...current.reviewFirst,
|
|
450
|
+
...parsedSkill.reviewFirst,
|
|
451
|
+
]).slice(0, 24),
|
|
380
452
|
touchedFiles: uniq([
|
|
381
453
|
...current.touchedFiles,
|
|
382
454
|
...candidateBundles.flatMap((bundle) => bundle.touchedFiles),
|
|
@@ -391,6 +463,19 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
|
|
|
391
463
|
...parsedSkill.rootCauseLabels,
|
|
392
464
|
...candidateBundles.flatMap((bundle) => bundle.rootCauseLabels),
|
|
393
465
|
]).slice(0, 24),
|
|
466
|
+
useWhen: deriveSkillUseWhen({
|
|
467
|
+
useWhen: current.useWhen ?? parsedSkill.useWhen,
|
|
468
|
+
description: current.description ?? parsedSkill.description,
|
|
469
|
+
applicability: uniq([
|
|
470
|
+
...current.applicability,
|
|
471
|
+
...parsedSkill.applicability,
|
|
472
|
+
]),
|
|
473
|
+
triggerHints: uniq([
|
|
474
|
+
...current.triggerHints,
|
|
475
|
+
...parsedSkill.triggerHints,
|
|
476
|
+
...candidateBundles.flatMap((bundle) => bundle.triggerHints),
|
|
477
|
+
]),
|
|
478
|
+
}),
|
|
394
479
|
description: current.description ?? parsedSkill.description,
|
|
395
480
|
summary: current.summary ?? candidateBundles.map((bundle) => bundle.summary).find(Boolean) ?? null,
|
|
396
481
|
adoption: normalizeSkillAdoption(current.adoption),
|
|
@@ -644,6 +729,29 @@ function summarizeReviewSignalKinds(reviewSignals = []) {
|
|
|
644
729
|
return uniq(reviewSignals.map((signal) => signal.kind).filter(Boolean)).slice(0, 6);
|
|
645
730
|
}
|
|
646
731
|
|
|
732
|
+
function summarizeKnowledgeScenario(source = {}, candidate = null) {
|
|
733
|
+
if (source.kind === 'quality-report') {
|
|
734
|
+
return source.attentionGates.length > 0
|
|
735
|
+
? `任务场景: 质量门禁收口(${source.attentionGates.slice(0, 4).join('、')})`
|
|
736
|
+
: '任务场景: 质量门禁收口';
|
|
737
|
+
}
|
|
738
|
+
const topRootCause = source.rootCauseCandidates?.[0]?.title ?? null;
|
|
739
|
+
if (topRootCause) {
|
|
740
|
+
return `任务场景: ${topRootCause}`;
|
|
741
|
+
}
|
|
742
|
+
const topTrigger = normalizeStringList(source.triggers)[0] ?? null;
|
|
743
|
+
if (topTrigger) {
|
|
744
|
+
return `任务场景: ${topTrigger}`;
|
|
745
|
+
}
|
|
746
|
+
const fallbackTitle = firstString(candidate?.title, source.title);
|
|
747
|
+
return fallbackTitle ? `任务场景: ${fallbackTitle}` : null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function summarizeEvidenceKinds(evidenceSources = []) {
|
|
751
|
+
const kinds = uniq(evidenceSources.map((item) => firstString(item.kind)).filter(Boolean));
|
|
752
|
+
return kinds.length > 0 ? `已有证据类型: ${kinds.join('、')}` : null;
|
|
753
|
+
}
|
|
754
|
+
|
|
647
755
|
function buildKnowledgeAbstraction({
|
|
648
756
|
candidate,
|
|
649
757
|
source,
|
|
@@ -670,27 +778,35 @@ function buildKnowledgeAbstraction({
|
|
|
670
778
|
...source.verificationSteps,
|
|
671
779
|
]).slice(0, 8);
|
|
672
780
|
const typicalInputs = uniq([
|
|
673
|
-
|
|
781
|
+
summarizeKnowledgeScenario(source, candidate),
|
|
674
782
|
touchedFiles.length > 0 ? `相关文件: ${touchedFiles.slice(0, 6).join('、')}` : null,
|
|
675
|
-
source.evidenceSources
|
|
676
|
-
? `已有证据: ${source.evidenceSources.slice(0, 4).map((item) => `${item.kind}:${item.path}`).join(';')}`
|
|
677
|
-
: null,
|
|
783
|
+
summarizeEvidenceKinds(source.evidenceSources),
|
|
678
784
|
reviewSignals.length > 0
|
|
679
785
|
? `验证信号: ${summarizeReviewSignalKinds(reviewSignals).join('、')}`
|
|
680
786
|
: null,
|
|
681
787
|
]).slice(0, 6);
|
|
682
788
|
const typicalOutputs = uniq([
|
|
683
|
-
relativeCandidateDir ?
|
|
684
|
-
|
|
685
|
-
relativeDraftSkillPath ? `draft skill: ${relativeDraftSkillPath}` : null,
|
|
789
|
+
relativeCandidateDir ? '项目经验候选与诊断包' : null,
|
|
790
|
+
relativeDraftSkillPath ? '待确认的项目经验草案' : null,
|
|
686
791
|
verificationSteps[0] ? `验证结论: ${verificationSteps[0]}` : null,
|
|
792
|
+
'可复用的验证链路与收尾动作',
|
|
687
793
|
]).slice(0, 6);
|
|
794
|
+
const antiPatterns = uniq([
|
|
795
|
+
'如果只是文件名、路径或个别词相似,但当前目标和验证方式不同,不要直接套用。',
|
|
796
|
+
source.kind === 'quality-report'
|
|
797
|
+
? '如果当前任务还没进入验证或收尾阶段,不要把这条经验当成通用实现模板。'
|
|
798
|
+
: '如果当前问题没有出现相似症状、事件或证据入口,不要因为改到相似模块就直接照搬旧结论。',
|
|
799
|
+
source.evidenceSources.length > 0
|
|
800
|
+
? '如果本轮已经有更新的现场证据,先核对新证据,再决定是否复用旧经验。'
|
|
801
|
+
: null,
|
|
802
|
+
]).slice(0, 4);
|
|
688
803
|
return {
|
|
689
804
|
triggerConditions,
|
|
690
805
|
applicability,
|
|
691
806
|
verificationSteps,
|
|
692
807
|
typicalInputs,
|
|
693
808
|
typicalOutputs,
|
|
809
|
+
antiPatterns,
|
|
694
810
|
};
|
|
695
811
|
}
|
|
696
812
|
|
|
@@ -707,13 +823,37 @@ function categoryReason(category) {
|
|
|
707
823
|
return '这次实现已经具备沉淀项目经验的价值。';
|
|
708
824
|
}
|
|
709
825
|
|
|
710
|
-
function
|
|
826
|
+
function deriveStablePatternKey(source = {}) {
|
|
827
|
+
if (source.kind === 'quality-report') {
|
|
828
|
+
const gates = normalizeStringList(source.attentionGates);
|
|
829
|
+
if (gates.length > 0) {
|
|
830
|
+
return gates.slice(0, 4).join('-');
|
|
831
|
+
}
|
|
832
|
+
return 'quality-closeout';
|
|
833
|
+
}
|
|
834
|
+
const rootCauseLabels = normalizeStringList(source.rootCauseCandidates?.map((item) => item.title));
|
|
835
|
+
if (rootCauseLabels.length > 0) {
|
|
836
|
+
return rootCauseLabels.slice(0, 3).join('-');
|
|
837
|
+
}
|
|
838
|
+
const triggers = normalizeStringList(source.triggers);
|
|
839
|
+
if (triggers.length > 0) {
|
|
840
|
+
return triggers.slice(0, 4).join('-');
|
|
841
|
+
}
|
|
842
|
+
if (source.abstractPattern) {
|
|
843
|
+
return source.abstractPattern;
|
|
844
|
+
}
|
|
845
|
+
return firstString(source.title, source.sourceId, source.status, 'diagnostic') ?? 'diagnostic';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function deriveKnowledgeNames(source, options = {}) {
|
|
711
849
|
const sourceRef = source?.sourceId ?? source?.title ?? source?.status ?? 'diagnostic';
|
|
712
850
|
const sourceKind = source?.kind === 'quality-report' ? 'quality' : 'diagnostic';
|
|
851
|
+
const stablePattern = options.stablePattern !== false;
|
|
713
852
|
const incidentId = source?.kind === 'quality-report'
|
|
714
853
|
? `incident-${sourceRef}`
|
|
715
854
|
: `incident-${slugify(sourceRef, 'diagnostic')}`;
|
|
716
|
-
const
|
|
855
|
+
const patternSeed = stablePattern ? deriveStablePatternKey(source) : sourceRef;
|
|
856
|
+
const patternId = `${sourceKind}-${slugify(patternSeed, sourceKind)}`;
|
|
717
857
|
const skillName = `openprd-experience-${slugify(patternId)}`;
|
|
718
858
|
return { incidentId, patternId, skillName };
|
|
719
859
|
}
|
|
@@ -898,9 +1038,11 @@ function renderKnowledgeDraftSkill({ skillName, candidate, source, relativeCandi
|
|
|
898
1038
|
...candidate.touchedFiles.map((file) => `\`${file}\``),
|
|
899
1039
|
...source.evidenceSources.map((item) => `\`${item.path}\``),
|
|
900
1040
|
]);
|
|
1041
|
+
const useWhen = 'Use when the current task overlaps this draft project experience and you should verify fit before reusing it.';
|
|
901
1042
|
return `---
|
|
902
1043
|
name: ${skillName}
|
|
903
|
-
description:
|
|
1044
|
+
description: ${useWhen}
|
|
1045
|
+
use_when: ${useWhen}
|
|
904
1046
|
---
|
|
905
1047
|
|
|
906
1048
|
# ${skillName}
|
|
@@ -929,6 +1071,10 @@ ${renderList(abstraction.typicalOutputs ?? [], '至少产出 knowledge candidate
|
|
|
929
1071
|
|
|
930
1072
|
${renderList(inspectItems, '先看本轮 touched files 和已有诊断证据。')}
|
|
931
1073
|
|
|
1074
|
+
## 不要直接套用
|
|
1075
|
+
|
|
1076
|
+
${renderList(abstraction.antiPatterns ?? [], '如果只是文件名相似,但当前目标、阶段或验证方式不同,不要直接套用。')}
|
|
1077
|
+
|
|
932
1078
|
## 可复用模式
|
|
933
1079
|
|
|
934
1080
|
${renderList(source.rootCauseCandidates.map((candidateItem) => candidateItem.title), '先按本轮诊断线索复走一次,再补最小必要证据。')}
|
|
@@ -1000,10 +1146,11 @@ function buildKnowledgeCandidateMeta({
|
|
|
1000
1146
|
existingCandidate,
|
|
1001
1147
|
abstraction,
|
|
1002
1148
|
}) {
|
|
1149
|
+
const existingStatus = normalizeCandidateStatus(existingCandidate?.status);
|
|
1003
1150
|
return {
|
|
1004
1151
|
version: 1,
|
|
1005
1152
|
candidateId,
|
|
1006
|
-
status:
|
|
1153
|
+
status: isReviewedKnowledgeCandidateStatus(existingStatus) ? existingStatus : 'pending-review',
|
|
1007
1154
|
createdAt: existingCandidate?.createdAt ?? timestamp(),
|
|
1008
1155
|
updatedAt: timestamp(),
|
|
1009
1156
|
sourceKind: source.kind,
|
|
@@ -1025,6 +1172,140 @@ function buildKnowledgeCandidateMeta({
|
|
|
1025
1172
|
};
|
|
1026
1173
|
}
|
|
1027
1174
|
|
|
1175
|
+
function cleanUserFacingExperienceText(value, fallback = null, max = 160) {
|
|
1176
|
+
let text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
1177
|
+
if (!text) {
|
|
1178
|
+
return fallback;
|
|
1179
|
+
}
|
|
1180
|
+
const replacements = [
|
|
1181
|
+
[/\bknowledge candidate\b/gi, '候选经验'],
|
|
1182
|
+
[/\bdraft skill\b/gi, '项目经验'],
|
|
1183
|
+
[/\bskill\b/gi, '经验'],
|
|
1184
|
+
[/\bpromote\b/gi, '保留'],
|
|
1185
|
+
[/\breject\b/gi, '暂不保留'],
|
|
1186
|
+
[/\barchive\b/gi, '先归档'],
|
|
1187
|
+
[/\bhook\b/gi, '收尾流程'],
|
|
1188
|
+
[/\bharness\b/gi, '项目流程'],
|
|
1189
|
+
[/\bturn-state\b/gi, '本轮记录'],
|
|
1190
|
+
[/\brun-state\b/gi, '当前状态'],
|
|
1191
|
+
[/\bOpenPrd\b/gi, '当前流程'],
|
|
1192
|
+
[/\.openprd\/[^\s,。;]+/g, ''],
|
|
1193
|
+
[/[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+/g, ''],
|
|
1194
|
+
];
|
|
1195
|
+
for (const [pattern, replacement] of replacements) {
|
|
1196
|
+
text = text.replace(pattern, replacement);
|
|
1197
|
+
}
|
|
1198
|
+
text = text
|
|
1199
|
+
.replace(/[()]/g, '')
|
|
1200
|
+
.replace(/[;;]+/g, ',')
|
|
1201
|
+
.replace(/\s+,/g, ',')
|
|
1202
|
+
.replace(/,+/g, ',')
|
|
1203
|
+
.replace(/\s+/g, ' ')
|
|
1204
|
+
.replace(/^[,,。\s]+|[,,。\s]+$/g, '')
|
|
1205
|
+
.trim();
|
|
1206
|
+
if (!text) {
|
|
1207
|
+
return fallback;
|
|
1208
|
+
}
|
|
1209
|
+
return trimPreview(text, max) ?? fallback;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function observedSituationFromSource({ source, reviewSignals, touchedFiles }) {
|
|
1213
|
+
const direct = [
|
|
1214
|
+
source.symptoms[0],
|
|
1215
|
+
reviewSignals.find((signal) => signal.summary)?.summary,
|
|
1216
|
+
source.abstractPattern,
|
|
1217
|
+
source.title,
|
|
1218
|
+
]
|
|
1219
|
+
.map((item) => cleanUserFacingExperienceText(item))
|
|
1220
|
+
.find(Boolean);
|
|
1221
|
+
if (direct) {
|
|
1222
|
+
return direct;
|
|
1223
|
+
}
|
|
1224
|
+
if (touchedFiles.length > 0) {
|
|
1225
|
+
return `这次任务在 ${touchedFiles.slice(0, 3).join('、')} 这些位置形成了一套后续可能会重复用到的处理方式。`;
|
|
1226
|
+
}
|
|
1227
|
+
return '这次任务里已经形成了一种以后可能重复出现的处理方式。';
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function plannedExperienceFromContext({ source, categories, touchedFiles }) {
|
|
1231
|
+
const touchesDocs = touchedFiles.some((file) => String(file).startsWith('docs/basic/'));
|
|
1232
|
+
const touchesTests = touchedFiles.some((file) => /^(test|tests)\//.test(String(file)));
|
|
1233
|
+
if (categories.includes('agent-misjudgment')) {
|
|
1234
|
+
return '以后做类似任务收尾时,我会先把本次情况、准备保留的经验和适用场景整理清楚,再用人话确认是否保留为当前项目经验。';
|
|
1235
|
+
}
|
|
1236
|
+
if (categories.includes('hidden-debug-knowledge')) {
|
|
1237
|
+
return '以后再遇到类似问题时,我会优先复用这次已经验证过的判断依据、排查顺序和收尾检查方式,而不是从零开始重新摸索。';
|
|
1238
|
+
}
|
|
1239
|
+
if (touchesDocs && touchesTests) {
|
|
1240
|
+
return '以后再遇到类似任务时,我会把处理方式、相关说明和验证方式一起整理成可复用经验,减少重复补充。';
|
|
1241
|
+
}
|
|
1242
|
+
if (touchesTests) {
|
|
1243
|
+
return '以后再遇到类似任务时,我会连同这次已经跑通的验证方式一起复用,避免只记住改法、忘记检查方式。';
|
|
1244
|
+
}
|
|
1245
|
+
const prevention = cleanUserFacingExperienceText(source.prevention[0]);
|
|
1246
|
+
if (prevention) {
|
|
1247
|
+
return prevention;
|
|
1248
|
+
}
|
|
1249
|
+
return '以后再遇到类似任务时,我会把这次已经验证过的处理顺序和注意事项保留下来,作为当前项目的默认经验。';
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function futureHandlingFromContext({ categories, touchedFiles }) {
|
|
1253
|
+
const touchesDocs = touchedFiles.some((file) => String(file).startsWith('docs/basic/'));
|
|
1254
|
+
if (categories.includes('agent-misjudgment')) {
|
|
1255
|
+
return '以后如果再遇到类似任务,我会优先按这套说法和处理顺序来收尾,减少重复解释和重复判断。';
|
|
1256
|
+
}
|
|
1257
|
+
if (touchesDocs) {
|
|
1258
|
+
return '以后如果再遇到类似任务,我会优先把处理方式和相关说明一起整理,减少来回补充。';
|
|
1259
|
+
}
|
|
1260
|
+
return '以后如果再遇到类似任务,我会优先按这套经验来处理,减少重复解释和重复判断。';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function knowledgeReviewStateNote(status) {
|
|
1264
|
+
const normalized = normalizeCandidateStatus(status);
|
|
1265
|
+
if (normalized === 'rejected') {
|
|
1266
|
+
return '这条经验之前已经标记为暂不保留,本轮不会再次向用户追问。';
|
|
1267
|
+
}
|
|
1268
|
+
if (normalized === 'archived') {
|
|
1269
|
+
return '这条经验之前已经先归档,本轮不会再次向用户追问。';
|
|
1270
|
+
}
|
|
1271
|
+
if (normalized === 'promoted' || normalized === 'merged') {
|
|
1272
|
+
return '这条经验已经保留为当前项目经验,本轮不需要再次确认。';
|
|
1273
|
+
}
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function buildKnowledgeUserFacingExperience({ candidate, source, touchedFiles, reviewSignals, categories }) {
|
|
1278
|
+
const observedSituation = observedSituationFromSource({ source, reviewSignals, touchedFiles });
|
|
1279
|
+
const plannedExperience = plannedExperienceFromContext({ source, categories, touchedFiles });
|
|
1280
|
+
const futureHandling = futureHandlingFromContext({ categories, touchedFiles });
|
|
1281
|
+
const scopeNote = '这条经验只会保留在当前项目里。';
|
|
1282
|
+
const reviewStateNote = knowledgeReviewStateNote(candidate.status);
|
|
1283
|
+
const shouldAskUser = normalizeCandidateStatus(candidate.status) === 'pending-review';
|
|
1284
|
+
const question = shouldAskUser ? '要我把它一起保留下来吗?' : null;
|
|
1285
|
+
const messageLines = [
|
|
1286
|
+
'这次我观察到一个以后可能重复出现的情况:',
|
|
1287
|
+
observedSituation,
|
|
1288
|
+
'',
|
|
1289
|
+
'我计划保留一条项目经验:',
|
|
1290
|
+
plannedExperience,
|
|
1291
|
+
'',
|
|
1292
|
+
futureHandling,
|
|
1293
|
+
scopeNote,
|
|
1294
|
+
question ?? reviewStateNote,
|
|
1295
|
+
].filter(Boolean);
|
|
1296
|
+
return {
|
|
1297
|
+
observedSituation,
|
|
1298
|
+
plannedExperience,
|
|
1299
|
+
futureHandling,
|
|
1300
|
+
scopeNote,
|
|
1301
|
+
question,
|
|
1302
|
+
reviewStateNote,
|
|
1303
|
+
shouldAskUser,
|
|
1304
|
+
projectOnly: true,
|
|
1305
|
+
message: messageLines.join('\n'),
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1028
1309
|
export async function recordKnowledgeReviewSignal(projectRoot, signal = {}) {
|
|
1029
1310
|
const statePath = knowledgePath(projectRoot, OPENPRD_HARNESS_TURN_STATE);
|
|
1030
1311
|
const normalized = normalizeReviewSignal(projectRoot, signal);
|
|
@@ -1157,7 +1438,7 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1157
1438
|
? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
|
|
1158
1439
|
: `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
|
|
1159
1440
|
const promotedSource = { ...source, sourceId: candidateId };
|
|
1160
|
-
const names = deriveKnowledgeNames(promotedSource);
|
|
1441
|
+
const names = deriveKnowledgeNames(promotedSource, { stablePattern: false });
|
|
1161
1442
|
const candidateDir = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId));
|
|
1162
1443
|
const candidatePath = path.join(candidateDir, 'candidate.json');
|
|
1163
1444
|
const diagnosticReportPath = path.join(candidateDir, 'diagnostic-report.json');
|
|
@@ -1202,6 +1483,13 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1202
1483
|
...draftCandidate,
|
|
1203
1484
|
abstraction,
|
|
1204
1485
|
};
|
|
1486
|
+
const userFacingExperience = buildKnowledgeUserFacingExperience({
|
|
1487
|
+
candidate,
|
|
1488
|
+
source,
|
|
1489
|
+
touchedFiles: substantiveTouchedFiles,
|
|
1490
|
+
reviewSignals,
|
|
1491
|
+
categories,
|
|
1492
|
+
});
|
|
1205
1493
|
await writeJson(candidatePath, candidate);
|
|
1206
1494
|
await writeJson(diagnosticReportPath, buildCandidateDiagnosticReport({
|
|
1207
1495
|
candidateId,
|
|
@@ -1245,6 +1533,17 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1245
1533
|
status: candidate.status,
|
|
1246
1534
|
}),
|
|
1247
1535
|
});
|
|
1536
|
+
if (turnStateSource && await exists(knowledgePath(projectRoot, turnStateSource))) {
|
|
1537
|
+
const turnState = await readJson(knowledgePath(projectRoot, turnStateSource)).catch(() => null);
|
|
1538
|
+
const currentTurnState = readJsonObject(turnState);
|
|
1539
|
+
if (currentTurnState) {
|
|
1540
|
+
await writeJson(knowledgePath(projectRoot, turnStateSource), {
|
|
1541
|
+
...currentTurnState,
|
|
1542
|
+
knowledgeCandidateId: candidateId,
|
|
1543
|
+
updatedAt: timestamp(),
|
|
1544
|
+
}).catch(() => null);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1248
1547
|
|
|
1249
1548
|
return {
|
|
1250
1549
|
ok: true,
|
|
@@ -1257,7 +1556,9 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1257
1556
|
skillName: names.skillName,
|
|
1258
1557
|
categories,
|
|
1259
1558
|
reasons,
|
|
1559
|
+
status: candidate.status,
|
|
1260
1560
|
summary: reviewSummary,
|
|
1561
|
+
userFacingExperience,
|
|
1261
1562
|
suggestedLearnCommand: candidate.suggestedLearnCommand,
|
|
1262
1563
|
files: {
|
|
1263
1564
|
candidate: candidatePath,
|
|
@@ -1401,6 +1702,54 @@ function buildKnowledgeMatchQuery(options = {}) {
|
|
|
1401
1702
|
};
|
|
1402
1703
|
}
|
|
1403
1704
|
|
|
1705
|
+
function trimPromptList(items = [], limit = 3, max = 96) {
|
|
1706
|
+
return uniq(
|
|
1707
|
+
normalizeStringList(items)
|
|
1708
|
+
.map((item) => trimPreview(item, max))
|
|
1709
|
+
.filter(Boolean),
|
|
1710
|
+
).slice(0, limit);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function buildKnowledgeMatchCandidateCard(skill) {
|
|
1714
|
+
return {
|
|
1715
|
+
skillName: skill.skillName,
|
|
1716
|
+
useWhen: firstString(skill.useWhen, skill.description),
|
|
1717
|
+
applicability: trimPromptList(skill.applicability, 2, 96),
|
|
1718
|
+
reviewFirst: trimPromptList(skill.reviewFirst, 3, 96),
|
|
1719
|
+
antiPatterns: trimPromptList(skill.antiPatterns, 2, 96),
|
|
1720
|
+
touchedFiles: trimPromptList(skill.touchedFiles, 4, 80),
|
|
1721
|
+
matchedOn: trimPromptList(skill.matchedOn, 3, 96),
|
|
1722
|
+
matchSummary: trimPreview(skill.matchSummary, 160),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function buildKnowledgeMandatoryCheck(matches, options = {}) {
|
|
1727
|
+
if (!Array.isArray(matches) || matches.length === 0) {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
const focusSignals = trimPromptList([
|
|
1731
|
+
options.recommendationTitle ? `当前目标: ${options.recommendationTitle}` : null,
|
|
1732
|
+
options.recommendationReason ? `目标原因: ${options.recommendationReason}` : null,
|
|
1733
|
+
options.activeChange ? `当前变更: ${options.activeChange}` : null,
|
|
1734
|
+
options.nextTaskTitle ? `下一任务: ${options.nextTaskTitle}` : null,
|
|
1735
|
+
...normalizeStringList(options.relatedFiles).map((file) => `相关文件: ${file}`),
|
|
1736
|
+
], 6, 120);
|
|
1737
|
+
return {
|
|
1738
|
+
required: true,
|
|
1739
|
+
mode: 'prompt-rerank',
|
|
1740
|
+
title: '先做项目经验检查,再决定是否复用',
|
|
1741
|
+
summary: '这些项目经验只是候选,不代表都要复用;先按当前目标、阶段和验证方式判断。',
|
|
1742
|
+
instructions: [
|
|
1743
|
+
'先判断当前任务真正要解决什么,再看候选经验。',
|
|
1744
|
+
'只有当适用时机、范围或证据入口与当前任务一致时,才复用对应经验。',
|
|
1745
|
+
'如果只是文件名、路径或个别词重合,但任务目标、阶段或验证方式不一致,就不要套用。',
|
|
1746
|
+
'最多选择 0 到 3 条最相关经验;如果都不贴切,可以明确本轮不复用项目经验。',
|
|
1747
|
+
],
|
|
1748
|
+
focusSignals,
|
|
1749
|
+
candidates: matches.map((skill) => buildKnowledgeMatchCandidateCard(skill)).slice(0, Math.max(1, Number(options.limit ?? 3))),
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1404
1753
|
function scoreKnowledgeSkillMatch(skill, query) {
|
|
1405
1754
|
const fileHints = uniq([
|
|
1406
1755
|
...skill.touchedFiles,
|
|
@@ -1410,10 +1759,13 @@ function scoreKnowledgeSkillMatch(skill, query) {
|
|
|
1410
1759
|
]);
|
|
1411
1760
|
const fields = [
|
|
1412
1761
|
skill.skillName,
|
|
1413
|
-
skill.
|
|
1762
|
+
skill.useWhen,
|
|
1763
|
+
...(!isGenericKnowledgeDescription(skill.description) ? [skill.description] : []),
|
|
1414
1764
|
skill.summary,
|
|
1415
1765
|
...skill.categories,
|
|
1416
1766
|
...skill.triggerHints,
|
|
1767
|
+
...skill.applicability,
|
|
1768
|
+
...skill.reviewFirst,
|
|
1417
1769
|
...skill.rootCauseLabels,
|
|
1418
1770
|
...fileHints,
|
|
1419
1771
|
];
|
|
@@ -1458,14 +1810,18 @@ export async function resolveKnowledgeSkillMatches(projectRoot, options = {}) {
|
|
|
1458
1810
|
.filter(Boolean)
|
|
1459
1811
|
.sort((left, right) => right.score - left.score || left.skillName.localeCompare(right.skillName))
|
|
1460
1812
|
.slice(0, Math.max(1, Number(options.limit ?? 3)));
|
|
1813
|
+
const mandatoryCheck = buildKnowledgeMandatoryCheck(matches, options);
|
|
1461
1814
|
return {
|
|
1462
1815
|
ok: true,
|
|
1463
1816
|
action: 'knowledge-match',
|
|
1464
1817
|
projectRoot,
|
|
1465
1818
|
query: trimPreview(query.text, 320),
|
|
1466
1819
|
matched: matches,
|
|
1820
|
+
mandatoryCheck,
|
|
1467
1821
|
summary: {
|
|
1468
1822
|
matched: matches.length,
|
|
1823
|
+
reviewRequired: Boolean(mandatoryCheck?.required),
|
|
1824
|
+
reviewMode: mandatoryCheck?.mode ?? null,
|
|
1469
1825
|
},
|
|
1470
1826
|
};
|
|
1471
1827
|
}
|
package/src/learning-review.js
CHANGED
|
@@ -161,10 +161,10 @@ const STYLE_PROMPT_PACKS = {
|
|
|
161
161
|
titlePatterns: [
|
|
162
162
|
'《{topic}》修行札记',
|
|
163
163
|
'《{topic}》证道小卷',
|
|
164
|
-
'《{topic}
|
|
164
|
+
'《{topic}》藏录篇',
|
|
165
165
|
],
|
|
166
166
|
outlineArc: ['筑基立卷', '观脉识图', '破雾辨源', '传功成谱', '归元再启'],
|
|
167
|
-
imageryBank: ['灵根', '经脉', '法门', '玉简', '破境', '
|
|
167
|
+
imageryBank: ['灵根', '经脉', '法门', '玉简', '破境', '藏录', '心法', '炉火'],
|
|
168
168
|
sentenceRhythm: '长短句交错;每段先给意象,再落回事实、路径或证据。',
|
|
169
169
|
taboo: [
|
|
170
170
|
'不要把证据不存在的内容写成神迹或事实。',
|
|
@@ -179,7 +179,7 @@ const STYLE_PROMPT_PACKS = {
|
|
|
179
179
|
titlePrompt: [
|
|
180
180
|
'输入: topic、genre、substyle、agent-context、evidence summary。',
|
|
181
181
|
'输出: 一个像书名的标题和一个短副题。',
|
|
182
|
-
'要求:
|
|
182
|
+
'要求: 标题可带“札记/小卷/藏录/心法”等书籍意象,但必须保留 topic 的核心名词。',
|
|
183
183
|
].join('\n'),
|
|
184
184
|
outlinePrompt: [
|
|
185
185
|
'输入: 章节目标、证据类别、读者学习路径。',
|