@openprd/cli 0.1.9 → 0.1.11
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 +16 -0
- package/.openprd/benchmarks/sources.yaml +53 -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 +25 -0
- package/.openprd/templates/base/intake.md +17 -0
- package/.openprd/templates/base/prd.md +31 -6
- package/AGENTS.md +10 -7
- package/README.md +26 -28
- package/README_EN.md +3 -3
- package/package.json +41 -2
- package/scripts/openprd-codex-isolated-worker.mjs +799 -0
- package/skills/openprd-benchmark-router/SKILL.md +5 -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 +37 -20
- 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 +5 -3
- 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 +108 -0
- package/skills/openprd-router/SKILL.md +13 -5
- package/skills/openprd-shared/SKILL.md +38 -25
- package/src/agent-integration.js +120 -69
- package/src/brainstorm-artifacts.js +1805 -0
- package/src/brainstorm-presentation.js +233 -0
- package/src/brainstorm.js +766 -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 +79 -3
- 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 +838 -58
- package/src/design-starter-support.js +462 -0
- package/src/design-starter.js +1207 -0
- package/src/diagram-core.js +384 -151
- package/src/html-artifacts.js +403 -50
- package/src/knowledge.js +291 -32
- 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 +119 -2
- package/src/quality-html-artifact.js +30 -5
- package/src/quality-learning.js +50 -6
- package/src/quality.js +22 -12
- 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 +175 -0
- package/src/workspace-workflow.js +307 -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 -387
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
|
|
@@ -170,6 +171,10 @@ function normalizeSkillIndexEntry(entry = {}) {
|
|
|
170
171
|
]),
|
|
171
172
|
categories: normalizeStringList(skill.categories),
|
|
172
173
|
triggerHints: normalizeStringList(skill.triggerHints),
|
|
174
|
+
useWhen: firstString(skill.useWhen, skill.use_when),
|
|
175
|
+
applicability: normalizeStringList(skill.applicability),
|
|
176
|
+
antiPatterns: normalizeStringList(skill.antiPatterns),
|
|
177
|
+
reviewFirst: normalizeStringList(skill.reviewFirst),
|
|
173
178
|
touchedFiles: normalizeStringList(skill.touchedFiles),
|
|
174
179
|
evidencePaths: normalizeStringList(skill.evidencePaths),
|
|
175
180
|
rootCauseLabels: normalizeStringList(skill.rootCauseLabels),
|
|
@@ -213,6 +218,37 @@ function sortByLength(items = []) {
|
|
|
213
218
|
return [...items].sort((left, right) => String(right).length - String(left).length);
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
function isGenericKnowledgeDescription(value) {
|
|
222
|
+
const normalized = normalizeSearchText(value);
|
|
223
|
+
if (!normalized) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return [
|
|
227
|
+
'自动沉淀的项目级排查经验',
|
|
228
|
+
'待确认项目经验草案',
|
|
229
|
+
'自动生成的待确认项目经验草案',
|
|
230
|
+
'项目级排查经验',
|
|
231
|
+
'项目经验草案',
|
|
232
|
+
].some((needle) => normalized.includes(normalizeSearchText(needle)));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function stripUseWhenPrefix(value) {
|
|
236
|
+
return String(value ?? '').trim().replace(/^use when\b[::]?\s*/i, '').trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function deriveSkillUseWhen({ useWhen, description, applicability = [], triggerHints = [] }) {
|
|
240
|
+
const explicit = firstString(useWhen);
|
|
241
|
+
if (explicit) {
|
|
242
|
+
return explicit;
|
|
243
|
+
}
|
|
244
|
+
const desc = firstString(description);
|
|
245
|
+
if (desc && !isGenericKnowledgeDescription(desc)) {
|
|
246
|
+
return desc;
|
|
247
|
+
}
|
|
248
|
+
const fallback = firstString(applicability[0], triggerHints[0], desc);
|
|
249
|
+
return fallback ? `Use when ${stripUseWhenPrefix(fallback)}` : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
216
252
|
function scoreQueryAgainstFields(queryText, queryTokens, fields = []) {
|
|
217
253
|
let score = 0;
|
|
218
254
|
const matchedOn = [];
|
|
@@ -264,22 +300,47 @@ function parseMarkdownSectionList(markdown, headings = []) {
|
|
|
264
300
|
return uniq(collected);
|
|
265
301
|
}
|
|
266
302
|
|
|
267
|
-
function
|
|
303
|
+
function parseFrontmatterValue(markdown, key) {
|
|
268
304
|
const text = String(markdown ?? '');
|
|
269
305
|
const frontmatter = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
306
|
+
if (!frontmatter) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const pattern = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:\\s*(.+?)\\s*$`);
|
|
310
|
+
const line = frontmatter[1]
|
|
311
|
+
.split(/\r?\n/)
|
|
312
|
+
.map((item) => item.trim())
|
|
313
|
+
.find((item) => pattern.test(item));
|
|
314
|
+
if (!line) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const match = line.match(pattern);
|
|
318
|
+
return match?.[1]?.trim() ?? null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseSkillMetadataFromText(markdown) {
|
|
322
|
+
const text = String(markdown ?? '');
|
|
323
|
+
const description = parseFrontmatterValue(text, 'description');
|
|
324
|
+
const useWhen = firstString(
|
|
325
|
+
parseFrontmatterValue(text, 'use_when'),
|
|
326
|
+
parseFrontmatterValue(text, 'use-when'),
|
|
327
|
+
/^use when\b/i.test(String(description ?? '')) ? description : null,
|
|
328
|
+
);
|
|
329
|
+
const triggerHints = parseMarkdownSectionList(text, ['触发场景', '触发条件', '同步触发条件', '收尾顺序']);
|
|
330
|
+
const applicability = parseMarkdownSectionList(text, ['适用范围', '适用时机', 'When to Use', 'When to use']);
|
|
331
|
+
const antiPatterns = parseMarkdownSectionList(text, ['不要直接套用', '不要在这些情况使用', '不要在这些情况直接套用', 'When Not to Use', 'Do Not Use When', '反模式', '常见误判']);
|
|
332
|
+
const reviewFirst = parseMarkdownSectionList(text, ['下次触发时先看什么', '先看什么', '先看哪些证据', '判断前先看', '排查顺序']);
|
|
333
|
+
const rootCauseLabels = parseMarkdownSectionList(text, ['可复用模式', '常见根因']);
|
|
277
334
|
const evidencePaths = uniq((text.match(/`([^`]+)`/g) ?? [])
|
|
278
335
|
.map((entry) => entry.replace(/`/g, '').trim())
|
|
279
336
|
.filter((entry) => entry.includes('/') || entry.endsWith('.md') || entry.endsWith('.js') || entry.endsWith('.ts')));
|
|
280
337
|
return {
|
|
281
338
|
description,
|
|
339
|
+
useWhen,
|
|
282
340
|
triggerHints,
|
|
341
|
+
applicability,
|
|
342
|
+
antiPatterns,
|
|
343
|
+
reviewFirst,
|
|
283
344
|
rootCauseLabels,
|
|
284
345
|
evidencePaths,
|
|
285
346
|
};
|
|
@@ -377,6 +438,18 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
|
|
|
377
438
|
...parsedSkill.triggerHints,
|
|
378
439
|
...candidateBundles.flatMap((bundle) => bundle.triggerHints),
|
|
379
440
|
]).slice(0, 24),
|
|
441
|
+
applicability: uniq([
|
|
442
|
+
...current.applicability,
|
|
443
|
+
...parsedSkill.applicability,
|
|
444
|
+
]).slice(0, 24),
|
|
445
|
+
antiPatterns: uniq([
|
|
446
|
+
...current.antiPatterns,
|
|
447
|
+
...parsedSkill.antiPatterns,
|
|
448
|
+
]).slice(0, 24),
|
|
449
|
+
reviewFirst: uniq([
|
|
450
|
+
...current.reviewFirst,
|
|
451
|
+
...parsedSkill.reviewFirst,
|
|
452
|
+
]).slice(0, 24),
|
|
380
453
|
touchedFiles: uniq([
|
|
381
454
|
...current.touchedFiles,
|
|
382
455
|
...candidateBundles.flatMap((bundle) => bundle.touchedFiles),
|
|
@@ -391,6 +464,19 @@ async function hydrateKnowledgeSkillEntry(projectRoot, entry, cache = new Map())
|
|
|
391
464
|
...parsedSkill.rootCauseLabels,
|
|
392
465
|
...candidateBundles.flatMap((bundle) => bundle.rootCauseLabels),
|
|
393
466
|
]).slice(0, 24),
|
|
467
|
+
useWhen: deriveSkillUseWhen({
|
|
468
|
+
useWhen: current.useWhen ?? parsedSkill.useWhen,
|
|
469
|
+
description: current.description ?? parsedSkill.description,
|
|
470
|
+
applicability: uniq([
|
|
471
|
+
...current.applicability,
|
|
472
|
+
...parsedSkill.applicability,
|
|
473
|
+
]),
|
|
474
|
+
triggerHints: uniq([
|
|
475
|
+
...current.triggerHints,
|
|
476
|
+
...parsedSkill.triggerHints,
|
|
477
|
+
...candidateBundles.flatMap((bundle) => bundle.triggerHints),
|
|
478
|
+
]),
|
|
479
|
+
}),
|
|
394
480
|
description: current.description ?? parsedSkill.description,
|
|
395
481
|
summary: current.summary ?? candidateBundles.map((bundle) => bundle.summary).find(Boolean) ?? null,
|
|
396
482
|
adoption: normalizeSkillAdoption(current.adoption),
|
|
@@ -644,6 +730,29 @@ function summarizeReviewSignalKinds(reviewSignals = []) {
|
|
|
644
730
|
return uniq(reviewSignals.map((signal) => signal.kind).filter(Boolean)).slice(0, 6);
|
|
645
731
|
}
|
|
646
732
|
|
|
733
|
+
function summarizeKnowledgeScenario(source = {}, candidate = null) {
|
|
734
|
+
if (source.kind === 'quality-report') {
|
|
735
|
+
return source.attentionGates.length > 0
|
|
736
|
+
? `任务场景: 质量门禁收口(${source.attentionGates.slice(0, 4).join('、')})`
|
|
737
|
+
: '任务场景: 质量门禁收口';
|
|
738
|
+
}
|
|
739
|
+
const topRootCause = source.rootCauseCandidates?.[0]?.title ?? null;
|
|
740
|
+
if (topRootCause) {
|
|
741
|
+
return `任务场景: ${topRootCause}`;
|
|
742
|
+
}
|
|
743
|
+
const topTrigger = normalizeStringList(source.triggers)[0] ?? null;
|
|
744
|
+
if (topTrigger) {
|
|
745
|
+
return `任务场景: ${topTrigger}`;
|
|
746
|
+
}
|
|
747
|
+
const fallbackTitle = firstString(candidate?.title, source.title);
|
|
748
|
+
return fallbackTitle ? `任务场景: ${fallbackTitle}` : null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function summarizeEvidenceKinds(evidenceSources = []) {
|
|
752
|
+
const kinds = uniq(evidenceSources.map((item) => firstString(item.kind)).filter(Boolean));
|
|
753
|
+
return kinds.length > 0 ? `已有证据类型: ${kinds.join('、')}` : null;
|
|
754
|
+
}
|
|
755
|
+
|
|
647
756
|
function buildKnowledgeAbstraction({
|
|
648
757
|
candidate,
|
|
649
758
|
source,
|
|
@@ -670,27 +779,35 @@ function buildKnowledgeAbstraction({
|
|
|
670
779
|
...source.verificationSteps,
|
|
671
780
|
]).slice(0, 8);
|
|
672
781
|
const typicalInputs = uniq([
|
|
673
|
-
|
|
782
|
+
summarizeKnowledgeScenario(source, candidate),
|
|
674
783
|
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,
|
|
784
|
+
summarizeEvidenceKinds(source.evidenceSources),
|
|
678
785
|
reviewSignals.length > 0
|
|
679
786
|
? `验证信号: ${summarizeReviewSignalKinds(reviewSignals).join('、')}`
|
|
680
787
|
: null,
|
|
681
788
|
]).slice(0, 6);
|
|
682
789
|
const typicalOutputs = uniq([
|
|
683
|
-
relativeCandidateDir ?
|
|
684
|
-
|
|
685
|
-
relativeDraftSkillPath ? `draft skill: ${relativeDraftSkillPath}` : null,
|
|
790
|
+
relativeCandidateDir ? '项目经验候选与诊断包' : null,
|
|
791
|
+
relativeDraftSkillPath ? '待确认的项目经验草案' : null,
|
|
686
792
|
verificationSteps[0] ? `验证结论: ${verificationSteps[0]}` : null,
|
|
793
|
+
'可复用的验证链路与收尾动作',
|
|
687
794
|
]).slice(0, 6);
|
|
795
|
+
const antiPatterns = uniq([
|
|
796
|
+
'如果只是文件名、路径或个别词相似,但当前目标和验证方式不同,不要直接套用。',
|
|
797
|
+
source.kind === 'quality-report'
|
|
798
|
+
? '如果当前任务还没进入验证或收尾阶段,不要把这条经验当成通用实现模板。'
|
|
799
|
+
: '如果当前问题没有出现相似症状、事件或证据入口,不要因为改到相似模块就直接照搬旧结论。',
|
|
800
|
+
source.evidenceSources.length > 0
|
|
801
|
+
? '如果本轮已经有更新的现场证据,先核对新证据,再决定是否复用旧经验。'
|
|
802
|
+
: null,
|
|
803
|
+
]).slice(0, 4);
|
|
688
804
|
return {
|
|
689
805
|
triggerConditions,
|
|
690
806
|
applicability,
|
|
691
807
|
verificationSteps,
|
|
692
808
|
typicalInputs,
|
|
693
809
|
typicalOutputs,
|
|
810
|
+
antiPatterns,
|
|
694
811
|
};
|
|
695
812
|
}
|
|
696
813
|
|
|
@@ -707,27 +824,90 @@ function categoryReason(category) {
|
|
|
707
824
|
return '这次实现已经具备沉淀项目经验的价值。';
|
|
708
825
|
}
|
|
709
826
|
|
|
710
|
-
function
|
|
827
|
+
function deriveStablePatternKey(source = {}) {
|
|
828
|
+
if (source.kind === 'quality-report') {
|
|
829
|
+
const gates = normalizeStringList(source.attentionGates);
|
|
830
|
+
if (gates.length > 0) {
|
|
831
|
+
return gates.slice(0, 4).join('-');
|
|
832
|
+
}
|
|
833
|
+
return 'quality-closeout';
|
|
834
|
+
}
|
|
835
|
+
const rootCauseLabels = normalizeStringList(source.rootCauseCandidates?.map((item) => item.title));
|
|
836
|
+
if (rootCauseLabels.length > 0) {
|
|
837
|
+
return rootCauseLabels.slice(0, 3).join('-');
|
|
838
|
+
}
|
|
839
|
+
const triggers = normalizeStringList(source.triggers);
|
|
840
|
+
if (triggers.length > 0) {
|
|
841
|
+
return triggers.slice(0, 4).join('-');
|
|
842
|
+
}
|
|
843
|
+
if (source.abstractPattern) {
|
|
844
|
+
return source.abstractPattern;
|
|
845
|
+
}
|
|
846
|
+
return firstString(source.title, source.sourceId, source.status, 'diagnostic') ?? 'diagnostic';
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function deriveKnowledgeNames(source, options = {}) {
|
|
711
850
|
const sourceRef = source?.sourceId ?? source?.title ?? source?.status ?? 'diagnostic';
|
|
712
851
|
const sourceKind = source?.kind === 'quality-report' ? 'quality' : 'diagnostic';
|
|
852
|
+
const stablePattern = options.stablePattern !== false;
|
|
713
853
|
const incidentId = source?.kind === 'quality-report'
|
|
714
854
|
? `incident-${sourceRef}`
|
|
715
855
|
: `incident-${slugify(sourceRef, 'diagnostic')}`;
|
|
716
|
-
const
|
|
856
|
+
const patternSeed = stablePattern ? deriveStablePatternKey(source) : sourceRef;
|
|
857
|
+
const patternId = `${sourceKind}-${slugify(patternSeed, sourceKind)}`;
|
|
717
858
|
const skillName = `openprd-experience-${slugify(patternId)}`;
|
|
718
859
|
return { incidentId, patternId, skillName };
|
|
719
860
|
}
|
|
720
861
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
862
|
+
const LOW_SIGNAL_TITLE_PATTERN = /^(只回复|回复|继续|可以|好的|没问题|嗯|哦|行|是|对|确认|同意|执行|开始|ok|okay|yes|done|go)([\s,。,.!!??]|$)/i;
|
|
863
|
+
|
|
864
|
+
function titleLooksLikeRawInstruction(text) {
|
|
865
|
+
const value = String(text ?? '').trim();
|
|
866
|
+
if (!value) return true;
|
|
867
|
+
if (value.length < 8) return true;
|
|
868
|
+
return LOW_SIGNAL_TITLE_PATTERN.test(value);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function describeTouchedFilesTitle(touchedFiles) {
|
|
872
|
+
const files = [...new Set((touchedFiles ?? []).map((file) => String(file ?? '').trim()).filter(Boolean))];
|
|
873
|
+
if (files.length === 0) return null;
|
|
874
|
+
const primary = files[0].split('/').filter(Boolean).at(-1);
|
|
875
|
+
return files.length > 1
|
|
876
|
+
? `围绕 ${primary} 等 ${files.length} 个文件的实现经验`
|
|
877
|
+
: `围绕 ${primary} 的实现经验`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function buildTurnReviewTitle(raw, source, touchedFiles = []) {
|
|
881
|
+
const structured = firstString(raw?.title, raw?.summary?.title, source.title);
|
|
882
|
+
if (structured && !titleLooksLikeRawInstruction(structured)) return structured;
|
|
883
|
+
const rootCause = source.rootCauseCandidates?.[0]?.title;
|
|
884
|
+
if (rootCause && !titleLooksLikeRawInstruction(rootCause)) return rootCause;
|
|
885
|
+
const prompt = firstString(raw?.promptPreview, raw?.prompt);
|
|
886
|
+
if (prompt && !titleLooksLikeRawInstruction(prompt)) return prompt;
|
|
887
|
+
return describeTouchedFilesTitle(touchedFiles)
|
|
888
|
+
?? firstString(structured, source.sourceId, '项目经验草案')
|
|
889
|
+
?? '项目经验草案';
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function knowledgeCandidateFingerprint(categories = [], touchedFiles = []) {
|
|
893
|
+
const normalizedCategories = [...new Set((categories ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
|
|
894
|
+
const normalizedFiles = [...new Set((touchedFiles ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
|
|
895
|
+
return crypto.createHash('sha256').update(JSON.stringify([normalizedCategories, normalizedFiles])).digest('hex').slice(0, 16);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function findPendingCandidateByFingerprint(projectRoot, fingerprint, excludeId) {
|
|
899
|
+
const index = await readKnowledgeIndex(projectRoot);
|
|
900
|
+
for (const entry of index.candidates ?? []) {
|
|
901
|
+
if (!entry?.candidateId || entry.candidateId === excludeId) continue;
|
|
902
|
+
const candidate = await readCandidateById(projectRoot, entry.candidateId);
|
|
903
|
+
if (!candidate || !isPendingKnowledgeCandidateStatus(normalizeCandidateStatus(candidate.status))) continue;
|
|
904
|
+
const existingFingerprint = candidate.fingerprint
|
|
905
|
+
?? knowledgeCandidateFingerprint(candidate.categories, candidate.touchedFiles);
|
|
906
|
+
if (existingFingerprint === fingerprint) {
|
|
907
|
+
return candidate;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
731
911
|
}
|
|
732
912
|
|
|
733
913
|
async function loadRawReviewInput(projectRoot, from) {
|
|
@@ -898,9 +1078,11 @@ function renderKnowledgeDraftSkill({ skillName, candidate, source, relativeCandi
|
|
|
898
1078
|
...candidate.touchedFiles.map((file) => `\`${file}\``),
|
|
899
1079
|
...source.evidenceSources.map((item) => `\`${item.path}\``),
|
|
900
1080
|
]);
|
|
1081
|
+
const useWhen = 'Use when the current task overlaps this draft project experience and you should verify fit before reusing it.';
|
|
901
1082
|
return `---
|
|
902
1083
|
name: ${skillName}
|
|
903
|
-
description:
|
|
1084
|
+
description: ${useWhen}
|
|
1085
|
+
use_when: ${useWhen}
|
|
904
1086
|
---
|
|
905
1087
|
|
|
906
1088
|
# ${skillName}
|
|
@@ -929,6 +1111,10 @@ ${renderList(abstraction.typicalOutputs ?? [], '至少产出 knowledge candidate
|
|
|
929
1111
|
|
|
930
1112
|
${renderList(inspectItems, '先看本轮 touched files 和已有诊断证据。')}
|
|
931
1113
|
|
|
1114
|
+
## 不要直接套用
|
|
1115
|
+
|
|
1116
|
+
${renderList(abstraction.antiPatterns ?? [], '如果只是文件名相似,但当前目标、阶段或验证方式不同,不要直接套用。')}
|
|
1117
|
+
|
|
932
1118
|
## 可复用模式
|
|
933
1119
|
|
|
934
1120
|
${renderList(source.rootCauseCandidates.map((candidateItem) => candidateItem.title), '先按本轮诊断线索复走一次,再补最小必要证据。')}
|
|
@@ -1286,13 +1472,18 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1286
1472
|
};
|
|
1287
1473
|
}
|
|
1288
1474
|
|
|
1289
|
-
const title = buildTurnReviewTitle(raw, source);
|
|
1475
|
+
const title = buildTurnReviewTitle(raw, source, substantiveTouchedFiles);
|
|
1290
1476
|
const rawCandidateRef = firstString(raw.knowledgeCandidateId, raw.id);
|
|
1291
|
-
|
|
1477
|
+
let candidateId = rawCandidateRef
|
|
1292
1478
|
? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
|
|
1293
1479
|
: `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
|
|
1480
|
+
const fingerprint = knowledgeCandidateFingerprint(categories, substantiveTouchedFiles);
|
|
1481
|
+
const duplicateCandidate = await findPendingCandidateByFingerprint(projectRoot, fingerprint, candidateId);
|
|
1482
|
+
if (duplicateCandidate?.candidateId) {
|
|
1483
|
+
candidateId = duplicateCandidate.candidateId;
|
|
1484
|
+
}
|
|
1294
1485
|
const promotedSource = { ...source, sourceId: candidateId };
|
|
1295
|
-
const names = deriveKnowledgeNames(promotedSource);
|
|
1486
|
+
const names = deriveKnowledgeNames(promotedSource, { stablePattern: false });
|
|
1296
1487
|
const candidateDir = knowledgePath(projectRoot, cjoin(KNOWLEDGE_CANDIDATES_DIR, candidateId));
|
|
1297
1488
|
const candidatePath = path.join(candidateDir, 'candidate.json');
|
|
1298
1489
|
const diagnosticReportPath = path.join(candidateDir, 'diagnostic-report.json');
|
|
@@ -1336,6 +1527,8 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1336
1527
|
const candidate = {
|
|
1337
1528
|
...draftCandidate,
|
|
1338
1529
|
abstraction,
|
|
1530
|
+
fingerprint,
|
|
1531
|
+
occurrences: (readJsonObject(existingCandidate)?.occurrences ?? 0) + 1,
|
|
1339
1532
|
};
|
|
1340
1533
|
const userFacingExperience = buildKnowledgeUserFacingExperience({
|
|
1341
1534
|
candidate,
|
|
@@ -1387,6 +1580,17 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1387
1580
|
status: candidate.status,
|
|
1388
1581
|
}),
|
|
1389
1582
|
});
|
|
1583
|
+
if (turnStateSource && await exists(knowledgePath(projectRoot, turnStateSource))) {
|
|
1584
|
+
const turnState = await readJson(knowledgePath(projectRoot, turnStateSource)).catch(() => null);
|
|
1585
|
+
const currentTurnState = readJsonObject(turnState);
|
|
1586
|
+
if (currentTurnState) {
|
|
1587
|
+
await writeJson(knowledgePath(projectRoot, turnStateSource), {
|
|
1588
|
+
...currentTurnState,
|
|
1589
|
+
knowledgeCandidateId: candidateId,
|
|
1590
|
+
updatedAt: timestamp(),
|
|
1591
|
+
}).catch(() => null);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1390
1594
|
|
|
1391
1595
|
return {
|
|
1392
1596
|
ok: true,
|
|
@@ -1545,6 +1749,54 @@ function buildKnowledgeMatchQuery(options = {}) {
|
|
|
1545
1749
|
};
|
|
1546
1750
|
}
|
|
1547
1751
|
|
|
1752
|
+
function trimPromptList(items = [], limit = 3, max = 96) {
|
|
1753
|
+
return uniq(
|
|
1754
|
+
normalizeStringList(items)
|
|
1755
|
+
.map((item) => trimPreview(item, max))
|
|
1756
|
+
.filter(Boolean),
|
|
1757
|
+
).slice(0, limit);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function buildKnowledgeMatchCandidateCard(skill) {
|
|
1761
|
+
return {
|
|
1762
|
+
skillName: skill.skillName,
|
|
1763
|
+
useWhen: firstString(skill.useWhen, skill.description),
|
|
1764
|
+
applicability: trimPromptList(skill.applicability, 2, 96),
|
|
1765
|
+
reviewFirst: trimPromptList(skill.reviewFirst, 3, 96),
|
|
1766
|
+
antiPatterns: trimPromptList(skill.antiPatterns, 2, 96),
|
|
1767
|
+
touchedFiles: trimPromptList(skill.touchedFiles, 4, 80),
|
|
1768
|
+
matchedOn: trimPromptList(skill.matchedOn, 3, 96),
|
|
1769
|
+
matchSummary: trimPreview(skill.matchSummary, 160),
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function buildKnowledgeMandatoryCheck(matches, options = {}) {
|
|
1774
|
+
if (!Array.isArray(matches) || matches.length === 0) {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
const focusSignals = trimPromptList([
|
|
1778
|
+
options.recommendationTitle ? `当前目标: ${options.recommendationTitle}` : null,
|
|
1779
|
+
options.recommendationReason ? `目标原因: ${options.recommendationReason}` : null,
|
|
1780
|
+
options.activeChange ? `当前变更: ${options.activeChange}` : null,
|
|
1781
|
+
options.nextTaskTitle ? `下一任务: ${options.nextTaskTitle}` : null,
|
|
1782
|
+
...normalizeStringList(options.relatedFiles).map((file) => `相关文件: ${file}`),
|
|
1783
|
+
], 6, 120);
|
|
1784
|
+
return {
|
|
1785
|
+
required: true,
|
|
1786
|
+
mode: 'prompt-rerank',
|
|
1787
|
+
title: '先做项目经验检查,再决定是否复用',
|
|
1788
|
+
summary: '这些项目经验只是候选,不代表都要复用;先按当前目标、阶段和验证方式判断。',
|
|
1789
|
+
instructions: [
|
|
1790
|
+
'先判断当前任务真正要解决什么,再看候选经验。',
|
|
1791
|
+
'只有当适用时机、范围或证据入口与当前任务一致时,才复用对应经验。',
|
|
1792
|
+
'如果只是文件名、路径或个别词重合,但任务目标、阶段或验证方式不一致,就不要套用。',
|
|
1793
|
+
'最多选择 0 到 3 条最相关经验;如果都不贴切,可以明确本轮不复用项目经验。',
|
|
1794
|
+
],
|
|
1795
|
+
focusSignals,
|
|
1796
|
+
candidates: matches.map((skill) => buildKnowledgeMatchCandidateCard(skill)).slice(0, Math.max(1, Number(options.limit ?? 3))),
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1548
1800
|
function scoreKnowledgeSkillMatch(skill, query) {
|
|
1549
1801
|
const fileHints = uniq([
|
|
1550
1802
|
...skill.touchedFiles,
|
|
@@ -1554,10 +1806,13 @@ function scoreKnowledgeSkillMatch(skill, query) {
|
|
|
1554
1806
|
]);
|
|
1555
1807
|
const fields = [
|
|
1556
1808
|
skill.skillName,
|
|
1557
|
-
skill.
|
|
1809
|
+
skill.useWhen,
|
|
1810
|
+
...(!isGenericKnowledgeDescription(skill.description) ? [skill.description] : []),
|
|
1558
1811
|
skill.summary,
|
|
1559
1812
|
...skill.categories,
|
|
1560
1813
|
...skill.triggerHints,
|
|
1814
|
+
...skill.applicability,
|
|
1815
|
+
...skill.reviewFirst,
|
|
1561
1816
|
...skill.rootCauseLabels,
|
|
1562
1817
|
...fileHints,
|
|
1563
1818
|
];
|
|
@@ -1602,14 +1857,18 @@ export async function resolveKnowledgeSkillMatches(projectRoot, options = {}) {
|
|
|
1602
1857
|
.filter(Boolean)
|
|
1603
1858
|
.sort((left, right) => right.score - left.score || left.skillName.localeCompare(right.skillName))
|
|
1604
1859
|
.slice(0, Math.max(1, Number(options.limit ?? 3)));
|
|
1860
|
+
const mandatoryCheck = buildKnowledgeMandatoryCheck(matches, options);
|
|
1605
1861
|
return {
|
|
1606
1862
|
ok: true,
|
|
1607
1863
|
action: 'knowledge-match',
|
|
1608
1864
|
projectRoot,
|
|
1609
1865
|
query: trimPreview(query.text, 320),
|
|
1610
1866
|
matched: matches,
|
|
1867
|
+
mandatoryCheck,
|
|
1611
1868
|
summary: {
|
|
1612
1869
|
matched: matches.length,
|
|
1870
|
+
reviewRequired: Boolean(mandatoryCheck?.required),
|
|
1871
|
+
reviewMode: mandatoryCheck?.mode ?? null,
|
|
1613
1872
|
},
|
|
1614
1873
|
};
|
|
1615
1874
|
}
|
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
|
'输入: 章节目标、证据类别、读者学习路径。',
|