@openprd/cli 0.1.11 → 0.1.18
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/changes/adaptive-project-framing/.openprd.yaml +2 -0
- package/.openprd/changes/adaptive-project-framing/design.md +47 -0
- package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
- package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
- package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
- package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
- package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
- package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
- package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
- package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
- package/.openprd/changes/openprd/.openprd.yaml +2 -0
- package/.openprd/changes/openprd/design.md +46 -0
- package/.openprd/changes/openprd/proposal.md +36 -0
- package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd/task-events.jsonl +23 -0
- package/.openprd/changes/openprd/tasks.md +242 -0
- package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
- package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
- package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
- package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
- package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
- package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
- package/.openprd/changes/test-strategy-router/design.md +65 -0
- package/.openprd/changes/test-strategy-router/proposal.md +45 -0
- package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
- package/.openprd/changes/test-strategy-router/tasks.md +132 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
- package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
- package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
- package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
- package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
- package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
- package/.openprd/design/README.md +11 -6
- package/.openprd/design/README_EN.md +4 -1
- package/.openprd/design/active/asset-spec.md +6 -0
- package/.openprd/design/active/direction-plan.md +5 -5
- package/.openprd/design/active/selected-direction.md +2 -0
- package/.openprd/design/anti-slop.md +26 -1
- package/.openprd/design/assets/surface-presets.md +6 -0
- package/.openprd/design/checklists/ui-quality-gate.md +7 -0
- package/.openprd/design/lenses/frontend-lenses.md +28 -0
- package/.openprd/design/recipes/content-experience.md +8 -1
- package/.openprd/design/recipes/operational-tool.md +7 -0
- package/.openprd/design/recipes/product-launch.md +8 -1
- package/.openprd/design/templates/README.md +6 -3
- package/.openprd/design/templates/README_EN.md +6 -3
- package/.openprd/design/templates/ops-dashboard.html +8 -1
- package/.openprd/design/templates/product-launch.html +2 -1
- package/.openprd/design/themes/theme-catalog.json +23 -5
- package/.openprd/engagements/active/prd.md +103 -100
- package/.openprd/standards/config.json +7 -1
- package/AGENTS.md +13 -3
- package/README.md +107 -14
- package/README_EN.md +131 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-diagram-review/SKILL.md +27 -2
- package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-frontend-design/SKILL.md +44 -20
- package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
- package/skills/openprd-harness/SKILL.md +22 -16
- package/skills/openprd-quality/SKILL.md +10 -8
- package/skills/openprd-requirement-intake/SKILL.md +2 -0
- package/skills/openprd-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +26 -16
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +808 -0
- package/src/agent-integration.js +258 -777
- package/src/canvas-app.html.js +2910 -0
- package/src/canvas-i18n.js +274 -0
- package/src/canvas-workspace.js +2271 -0
- package/src/cli/args.js +72 -4
- package/src/cli/basic-print.js +3 -0
- package/src/cli/canvas-print.js +106 -0
- package/src/cli/doctor-print.js +20 -0
- package/src/cli/print.js +2 -0
- package/src/cli/quality-commands.js +222 -0
- package/src/cli/quality-print.js +28 -2
- package/src/cli/run-print.js +5 -0
- package/src/cli/workflow-print.js +3 -0
- package/src/codex-app-server-relay.js +251 -0
- package/src/codex-hook-runner-template.mjs +957 -59
- package/src/design-starter-support.js +2 -2
- package/src/design-starter.js +39 -3
- package/src/dev-standards.js +125 -10
- package/src/diagram-core.js +6 -20
- package/src/discovery.js +2 -8
- package/src/docs-compact.js +125 -0
- package/src/execution-strategy.js +1 -1
- package/src/fleet.js +9 -3
- package/src/guide.js +227 -0
- package/src/html-artifacts.js +428 -7
- package/src/knowledge.js +45 -11
- package/src/language-policy.js +63 -3
- package/src/learning-review.js +1 -1
- package/src/loop.js +1 -1
- package/src/openprd.js +68 -131
- package/src/openspec/change-validate.js +2 -2
- package/src/openspec/constants.js +6 -7
- package/src/openspec/execute.js +2 -2
- package/src/openspec/generate.js +318 -102
- package/src/openspec/migration.js +168 -0
- package/src/openspec/paths.js +1 -22
- package/src/prd-core.js +1 -1
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +46 -8
- package/src/review-presentation.js +60 -0
- package/src/run-harness.js +41 -3
- package/src/session-evidence.js +363 -0
- package/src/standards.js +64 -0
- package/src/test-strategy.js +7 -4
- package/src/visual-compare-centering.js +622 -0
- package/src/visual-compare-core.js +1123 -0
- package/src/visual-compare.js +620 -698
- package/src/workspace-core.js +94 -4
- package/src/workspace-workflow.js +46 -1
package/src/html-artifacts.js
CHANGED
|
@@ -491,9 +491,33 @@ const REVIEW_PRESENTATION_CONTRACT = {
|
|
|
491
491
|
USER_CHANGE_SUMMARY_GUIDE.panelExamples.risk,
|
|
492
492
|
],
|
|
493
493
|
},
|
|
494
|
+
designDirections: {
|
|
495
|
+
note: '可选;只有需求涉及界面/页面/交互时提供。先用 imagegen 生成 3 个差异足够大的交互稿,再把图片路径写进来。',
|
|
496
|
+
directions: [
|
|
497
|
+
{
|
|
498
|
+
id: 'direction-1',
|
|
499
|
+
title: '15 字以内方向名',
|
|
500
|
+
image: '.openprd/design/active/direction-1.png',
|
|
501
|
+
summary: '30 字以内一句话说明这个方向的核心体验',
|
|
502
|
+
tradeoff: '30 字以内说明取舍或适用场景',
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
494
506
|
},
|
|
495
507
|
},
|
|
496
508
|
rules: [
|
|
509
|
+
{
|
|
510
|
+
id: 'review-design-direction-count',
|
|
511
|
+
area: '交互方向评审',
|
|
512
|
+
target: '方向数量',
|
|
513
|
+
action: '涉及界面时提供 2 到 4 个差异足够大的方向(默认 3 个),写入 reviewPresentation.designDirections.directions[]。',
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: 'review-design-direction-fields',
|
|
517
|
+
area: '交互方向评审',
|
|
518
|
+
target: '每个方向的字段',
|
|
519
|
+
action: '每个方向必须有唯一 id、image 图片路径;title 15 字内、summary 30 字内、tradeoff 30 字内。',
|
|
520
|
+
},
|
|
497
521
|
{
|
|
498
522
|
id: 'review-map-card-text',
|
|
499
523
|
area: '需求关系图 / 需求流程图',
|
|
@@ -627,12 +651,125 @@ export function buildReviewPresentationFeedback(snapshot) {
|
|
|
627
651
|
});
|
|
628
652
|
});
|
|
629
653
|
|
|
654
|
+
validateReviewDesignDirections(snapshot, violations, addViolation);
|
|
655
|
+
|
|
630
656
|
return {
|
|
631
657
|
contract: REVIEW_PRESENTATION_CONTRACT,
|
|
632
658
|
violations,
|
|
633
659
|
};
|
|
634
660
|
}
|
|
635
661
|
|
|
662
|
+
export function reviewDesignDirections(snapshot) {
|
|
663
|
+
const block = snapshot?.reviewPresentation?.designDirections;
|
|
664
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)) return null;
|
|
665
|
+
const directions = Array.isArray(block.directions) ? block.directions : [];
|
|
666
|
+
if (directions.length === 0) return null;
|
|
667
|
+
return { ...block, directions };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function validateReviewDesignDirections(snapshot, violations, addViolation) {
|
|
671
|
+
const rawBlock = snapshot?.reviewPresentation?.designDirections;
|
|
672
|
+
if (rawBlock == null) return;
|
|
673
|
+
if (typeof rawBlock !== 'object' || Array.isArray(rawBlock) || !Array.isArray(rawBlock.directions)) {
|
|
674
|
+
violations.push({
|
|
675
|
+
ruleId: 'review-design-direction-fields',
|
|
676
|
+
area: '交互方向评审',
|
|
677
|
+
target: 'designDirections 结构',
|
|
678
|
+
jsonPath: 'reviewPresentation.designDirections',
|
|
679
|
+
action: 'designDirections 必须是对象,且 directions 为数组。参考 openprd review-presentation . --template。',
|
|
680
|
+
});
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const directions = rawBlock.directions;
|
|
684
|
+
if (directions.length < 2 || directions.length > 4) {
|
|
685
|
+
violations.push({
|
|
686
|
+
ruleId: 'review-design-direction-count',
|
|
687
|
+
area: '交互方向评审',
|
|
688
|
+
target: '方向数量',
|
|
689
|
+
jsonPath: 'reviewPresentation.designDirections.directions',
|
|
690
|
+
action: `当前 ${directions.length} 个方向;请提供 2 到 4 个差异足够大的方向(默认 3 个)。`,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const seenIds = new Set();
|
|
694
|
+
directions.forEach((direction, index) => {
|
|
695
|
+
const basePath = `reviewPresentation.designDirections.directions[${index}]`;
|
|
696
|
+
if (!direction || typeof direction !== 'object' || Array.isArray(direction)) {
|
|
697
|
+
violations.push({
|
|
698
|
+
ruleId: 'review-design-direction-fields',
|
|
699
|
+
area: '交互方向评审',
|
|
700
|
+
target: `方向 ${index + 1}`,
|
|
701
|
+
jsonPath: basePath,
|
|
702
|
+
action: '每个方向必须是对象,包含 id、title、image、summary 字段。',
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (/字以内/u.test(`${direction.title ?? ''}${direction.summary ?? ''}${direction.tradeoff ?? ''}`)) {
|
|
707
|
+
violations.push({
|
|
708
|
+
ruleId: 'review-design-direction-fields',
|
|
709
|
+
area: '交互方向评审',
|
|
710
|
+
target: `方向 ${index + 1}`,
|
|
711
|
+
jsonPath: basePath,
|
|
712
|
+
action: '仍是模板占位文案;请填入真实方向内容,或在不涉及界面时删除 designDirections 字段。',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const id = normalizedReviewVisibleText(direction.id);
|
|
716
|
+
if (!id) {
|
|
717
|
+
violations.push({
|
|
718
|
+
ruleId: 'review-design-direction-fields',
|
|
719
|
+
area: '交互方向评审',
|
|
720
|
+
target: `方向 ${index + 1} id`,
|
|
721
|
+
jsonPath: `${basePath}.id`,
|
|
722
|
+
action: '缺少方向 id;请使用 direction-1 这类稳定标识。',
|
|
723
|
+
});
|
|
724
|
+
} else if (seenIds.has(id)) {
|
|
725
|
+
violations.push({
|
|
726
|
+
ruleId: 'review-design-direction-fields',
|
|
727
|
+
area: '交互方向评审',
|
|
728
|
+
target: `方向 ${index + 1} id`,
|
|
729
|
+
jsonPath: `${basePath}.id`,
|
|
730
|
+
action: `方向 id ${id} 重复;每个方向必须使用唯一 id。`,
|
|
731
|
+
});
|
|
732
|
+
} else {
|
|
733
|
+
seenIds.add(id);
|
|
734
|
+
}
|
|
735
|
+
if (!normalizedReviewVisibleText(direction.image)) {
|
|
736
|
+
violations.push({
|
|
737
|
+
ruleId: 'review-design-direction-fields',
|
|
738
|
+
area: '交互方向评审',
|
|
739
|
+
target: `方向 ${index + 1} 图片`,
|
|
740
|
+
jsonPath: `${basePath}.image`,
|
|
741
|
+
action: '缺少 image 图片路径;请先用 imagegen 生成交互稿,再写入项目内相对路径。',
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
addViolation({
|
|
745
|
+
ruleId: 'review-design-direction-fields',
|
|
746
|
+
area: '交互方向评审',
|
|
747
|
+
target: `方向 ${index + 1} 名称`,
|
|
748
|
+
value: direction.title,
|
|
749
|
+
maxChars: 15,
|
|
750
|
+
jsonPath: `${basePath}.title`,
|
|
751
|
+
});
|
|
752
|
+
addViolation({
|
|
753
|
+
ruleId: 'review-design-direction-fields',
|
|
754
|
+
area: '交互方向评审',
|
|
755
|
+
target: `方向 ${index + 1} 卖点`,
|
|
756
|
+
value: direction.summary,
|
|
757
|
+
maxChars: 30,
|
|
758
|
+
jsonPath: `${basePath}.summary`,
|
|
759
|
+
});
|
|
760
|
+
if (direction.tradeoff != null) {
|
|
761
|
+
addViolation({
|
|
762
|
+
ruleId: 'review-design-direction-fields',
|
|
763
|
+
area: '交互方向评审',
|
|
764
|
+
target: `方向 ${index + 1} 取舍`,
|
|
765
|
+
value: direction.tradeoff,
|
|
766
|
+
maxChars: 30,
|
|
767
|
+
jsonPath: `${basePath}.tradeoff`,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
636
773
|
function normalizedReviewVisibleText(value) {
|
|
637
774
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
638
775
|
}
|
|
@@ -869,6 +1006,31 @@ function renderReviewProjectVersion(projectRelease) {
|
|
|
869
1006
|
`;
|
|
870
1007
|
}
|
|
871
1008
|
|
|
1009
|
+
function renderReviewStageRail(snapshot) {
|
|
1010
|
+
const hasDirections = Boolean(reviewDesignDirections(snapshot));
|
|
1011
|
+
const stages = [
|
|
1012
|
+
{ title: '需求澄清', status: 'done' },
|
|
1013
|
+
{ title: '需求摘要', status: 'done' },
|
|
1014
|
+
{ title: '评审确认', status: 'current' },
|
|
1015
|
+
...(hasDirections ? [{ title: '方向选择', status: 'pending' }] : []),
|
|
1016
|
+
{ title: '任务拆解', status: 'pending' },
|
|
1017
|
+
{ title: '实现执行', status: 'pending' },
|
|
1018
|
+
{ title: '验证收口', status: 'pending' },
|
|
1019
|
+
];
|
|
1020
|
+
const items = stages.map((stage, index) => `
|
|
1021
|
+
<li class="review-stage ${stage.status}">
|
|
1022
|
+
<span class="review-stage-dot">${stage.status === 'done' ? '✓' : index + 1}</span>
|
|
1023
|
+
<span class="review-stage-name">${escapeHtml(stage.title)}</span>
|
|
1024
|
+
${stage.status === 'current' ? '<span class="review-stage-badge">你在这里</span>' : ''}
|
|
1025
|
+
</li>`);
|
|
1026
|
+
return `
|
|
1027
|
+
<nav class="review-stage-rail" aria-label="流程位置">
|
|
1028
|
+
<ol>
|
|
1029
|
+
${items.join('\n')}
|
|
1030
|
+
</ol>
|
|
1031
|
+
</nav>`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
872
1034
|
function renderReviewOverview(snapshot, sectionsData, agenda = []) {
|
|
873
1035
|
const problem = sectionsData.problem?.problemStatement || '尚未形成明确问题定义';
|
|
874
1036
|
const goal = reviewList(sectionsData.goals?.goals)[0] ?? null;
|
|
@@ -880,7 +1042,7 @@ function renderReviewOverview(snapshot, sectionsData, agenda = []) {
|
|
|
880
1042
|
<li>
|
|
881
1043
|
<a href="#review-panel-${escapeHtml(item.kind)}">
|
|
882
1044
|
<span class="review-agenda-name">${escapeHtml(item.title)}</span>
|
|
883
|
-
<span class="review-agenda-count${item.count === 0 ? ' empty' : ''}">${item.count === 0 ? '暂无内容' : `${item.count}
|
|
1045
|
+
<span class="review-agenda-count${item.count === 0 ? ' empty' : ''}">${item.count === 0 ? '暂无内容' : `${item.count} ${escapeHtml(item.unit || '条')}`}</span>
|
|
884
1046
|
</a>
|
|
885
1047
|
</li>`).join('')}
|
|
886
1048
|
</ol>
|
|
@@ -1336,7 +1498,7 @@ function shellQuote(value) {
|
|
|
1336
1498
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1337
1499
|
}
|
|
1338
1500
|
|
|
1339
|
-
function reviewCommand(snapshot, status, notes = null) {
|
|
1501
|
+
function reviewCommand(snapshot, status, notes = null, extra = {}) {
|
|
1340
1502
|
const parts = ['openprd review . --mark', status];
|
|
1341
1503
|
if (snapshot.versionId) {
|
|
1342
1504
|
parts.push('--version', shellQuote(snapshot.versionId));
|
|
@@ -1347,12 +1509,71 @@ function reviewCommand(snapshot, status, notes = null) {
|
|
|
1347
1509
|
if (snapshot.workUnitId) {
|
|
1348
1510
|
parts.push('--work-unit', shellQuote(snapshot.workUnitId));
|
|
1349
1511
|
}
|
|
1512
|
+
if (extra.direction) {
|
|
1513
|
+
parts.push('--direction', shellQuote(extra.direction));
|
|
1514
|
+
}
|
|
1350
1515
|
if (notes) {
|
|
1351
1516
|
parts.push('--notes', shellQuote(notes));
|
|
1352
1517
|
}
|
|
1353
1518
|
return parts.join(' ');
|
|
1354
1519
|
}
|
|
1355
1520
|
|
|
1521
|
+
function reviewDirectionImageHref(imagePath) {
|
|
1522
|
+
const text = String(imagePath ?? '').trim();
|
|
1523
|
+
if (!text) return null;
|
|
1524
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|\/)/iu.test(text)) {
|
|
1525
|
+
return text;
|
|
1526
|
+
}
|
|
1527
|
+
return `../../${text.split(path.sep).join('/')}`;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function renderReviewDesignDirections(snapshot) {
|
|
1531
|
+
const block = reviewDesignDirections(snapshot);
|
|
1532
|
+
if (!block) return '';
|
|
1533
|
+
const cards = block.directions.map((direction, index) => {
|
|
1534
|
+
const id = normalizedReviewVisibleText(direction.id) || `direction-${index + 1}`;
|
|
1535
|
+
const title = normalizedReviewVisibleText(direction.title) || `方向 ${index + 1}`;
|
|
1536
|
+
const summary = normalizedReviewVisibleText(direction.summary);
|
|
1537
|
+
const tradeoff = normalizedReviewVisibleText(direction.tradeoff);
|
|
1538
|
+
const href = reviewDirectionImageHref(direction.image);
|
|
1539
|
+
const selectCommand = reviewCommand(snapshot, 'confirmed', null, { direction: id });
|
|
1540
|
+
const selectCopy = reviewCopyBundle({
|
|
1541
|
+
label: `选定交互方向:${title}`,
|
|
1542
|
+
command: selectCommand,
|
|
1543
|
+
payload: JSON.stringify({ versionId: snapshot.versionId, direction: id, title }, null, 2),
|
|
1544
|
+
message: `我选定「${title}」这个交互方向,请按这个方向继续推进实现。`,
|
|
1545
|
+
});
|
|
1546
|
+
const image = href
|
|
1547
|
+
? `<a class="review-direction-image" href="${escapeHtml(href)}" target="_blank" rel="noopener"><img src="${escapeHtml(href)}" alt="${escapeHtml(title)} 交互稿" loading="lazy" /></a>`
|
|
1548
|
+
: '<div class="review-direction-image review-direction-image-missing">交互稿图片待生成</div>';
|
|
1549
|
+
return `
|
|
1550
|
+
<article class="review-direction-card" data-direction="${escapeHtml(id)}">
|
|
1551
|
+
${image}
|
|
1552
|
+
<div class="review-direction-body">
|
|
1553
|
+
<div class="review-direction-head">
|
|
1554
|
+
<span class="review-direction-index">方向 ${index + 1}</span>
|
|
1555
|
+
<h3>${escapeHtml(title)}</h3>
|
|
1556
|
+
</div>
|
|
1557
|
+
${summary ? `<p class="review-direction-summary">${escapeHtml(summary)}</p>` : ''}
|
|
1558
|
+
${tradeoff ? `<p class="review-direction-tradeoff">取舍:${escapeHtml(tradeoff)}</p>` : ''}
|
|
1559
|
+
<button type="button" class="review-direction-select" data-copy-value="${escapeHtml(selectCopy)}" title="${escapeHtml(selectCommand)}">
|
|
1560
|
+
选这个方向
|
|
1561
|
+
</button>
|
|
1562
|
+
</div>
|
|
1563
|
+
</article>`;
|
|
1564
|
+
});
|
|
1565
|
+
return `
|
|
1566
|
+
<section class="review-directions" id="review-panel-design-directions" aria-label="交互方向评审">
|
|
1567
|
+
<div class="review-section-heading">
|
|
1568
|
+
<h2>交互方向评审</h2>
|
|
1569
|
+
<p>这是本次界面需求最重要的决定:${block.directions.length} 个交互稿方向选一个。点图片看大图,点“选这个方向”后把复制内容发回给 Agent 即可。</p>
|
|
1570
|
+
</div>
|
|
1571
|
+
<div class="review-direction-grid">
|
|
1572
|
+
${cards.join('\n')}
|
|
1573
|
+
</div>
|
|
1574
|
+
</section>`;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1356
1577
|
function renderReviewDecision(snapshot) {
|
|
1357
1578
|
const payload = JSON.stringify(buildReviewExportPayload(snapshot), null, 2);
|
|
1358
1579
|
const confirmCommand = reviewCommand(snapshot, 'confirmed');
|
|
@@ -1437,11 +1658,17 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1437
1658
|
items: riskPanelItems,
|
|
1438
1659
|
},
|
|
1439
1660
|
];
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1661
|
+
const designDirectionsBlock = reviewDesignDirections(snapshot);
|
|
1662
|
+
const panelAgenda = [
|
|
1663
|
+
...(designDirectionsBlock
|
|
1664
|
+
? [{ kind: 'design-directions', title: '交互方向评审(选一个方向)', count: designDirectionsBlock.directions.length, unit: '个方向' }]
|
|
1665
|
+
: []),
|
|
1666
|
+
...panelSpecs.map((spec) => ({
|
|
1667
|
+
kind: spec.kind,
|
|
1668
|
+
title: spec.title,
|
|
1669
|
+
count: Array.isArray(spec.items) ? spec.items.filter(Boolean).length : 0,
|
|
1670
|
+
})),
|
|
1671
|
+
];
|
|
1445
1672
|
const panels = panelSpecs.map((spec) => renderReviewPanel(spec));
|
|
1446
1673
|
return `<!DOCTYPE html>
|
|
1447
1674
|
<html lang="zh-CN">
|
|
@@ -1487,6 +1714,76 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
1487
1714
|
gap: 16px;
|
|
1488
1715
|
margin-bottom: 16px;
|
|
1489
1716
|
}
|
|
1717
|
+
.review-stage-rail {
|
|
1718
|
+
margin: 0 0 16px;
|
|
1719
|
+
border: 1px solid var(--review-line);
|
|
1720
|
+
border-radius: 14px;
|
|
1721
|
+
background: var(--review-panel);
|
|
1722
|
+
padding: 12px 18px;
|
|
1723
|
+
overflow-x: auto;
|
|
1724
|
+
}
|
|
1725
|
+
.review-stage-rail ol {
|
|
1726
|
+
display: flex;
|
|
1727
|
+
align-items: center;
|
|
1728
|
+
gap: 6px;
|
|
1729
|
+
margin: 0;
|
|
1730
|
+
padding: 0;
|
|
1731
|
+
list-style: none;
|
|
1732
|
+
min-width: max-content;
|
|
1733
|
+
}
|
|
1734
|
+
.review-stage {
|
|
1735
|
+
display: inline-flex;
|
|
1736
|
+
align-items: center;
|
|
1737
|
+
gap: 8px;
|
|
1738
|
+
padding: 6px 10px;
|
|
1739
|
+
border-radius: 999px;
|
|
1740
|
+
font-size: 13px;
|
|
1741
|
+
color: var(--review-muted);
|
|
1742
|
+
white-space: nowrap;
|
|
1743
|
+
}
|
|
1744
|
+
.review-stage + .review-stage::before {
|
|
1745
|
+
content: '→';
|
|
1746
|
+
margin-right: 6px;
|
|
1747
|
+
color: var(--review-line);
|
|
1748
|
+
}
|
|
1749
|
+
.review-stage-dot {
|
|
1750
|
+
display: inline-grid;
|
|
1751
|
+
place-items: center;
|
|
1752
|
+
width: 22px;
|
|
1753
|
+
height: 22px;
|
|
1754
|
+
border-radius: 999px;
|
|
1755
|
+
border: 1px solid var(--review-line);
|
|
1756
|
+
background: var(--review-panel-soft);
|
|
1757
|
+
font-size: 12px;
|
|
1758
|
+
font-weight: 800;
|
|
1759
|
+
}
|
|
1760
|
+
.review-stage.done .review-stage-dot {
|
|
1761
|
+
border-color: #86efac;
|
|
1762
|
+
background: #dcfce7;
|
|
1763
|
+
color: #15803d;
|
|
1764
|
+
}
|
|
1765
|
+
.review-stage.done .review-stage-name {
|
|
1766
|
+
color: var(--review-text);
|
|
1767
|
+
}
|
|
1768
|
+
.review-stage.current {
|
|
1769
|
+
background: #eef2ff;
|
|
1770
|
+
border: 1px solid #c7d2fe;
|
|
1771
|
+
color: var(--review-indigo);
|
|
1772
|
+
font-weight: 800;
|
|
1773
|
+
}
|
|
1774
|
+
.review-stage.current .review-stage-dot {
|
|
1775
|
+
border-color: var(--review-indigo);
|
|
1776
|
+
background: var(--review-indigo);
|
|
1777
|
+
color: #fff;
|
|
1778
|
+
}
|
|
1779
|
+
.review-stage-badge {
|
|
1780
|
+
font-size: 11px;
|
|
1781
|
+
font-weight: 800;
|
|
1782
|
+
color: #fff;
|
|
1783
|
+
background: var(--review-indigo);
|
|
1784
|
+
border-radius: 999px;
|
|
1785
|
+
padding: 2px 8px;
|
|
1786
|
+
}
|
|
1490
1787
|
.review-brand {
|
|
1491
1788
|
display: inline-flex;
|
|
1492
1789
|
align-items: center;
|
|
@@ -2081,6 +2378,113 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
2081
2378
|
box-shadow: 0 4px 10px rgba(15, 23, 42, 0.08);
|
|
2082
2379
|
transform: translateY(0);
|
|
2083
2380
|
}
|
|
2381
|
+
.review-directions {
|
|
2382
|
+
margin-top: 28px;
|
|
2383
|
+
}
|
|
2384
|
+
.review-direction-grid {
|
|
2385
|
+
display: grid;
|
|
2386
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
2387
|
+
gap: 18px;
|
|
2388
|
+
margin-top: 14px;
|
|
2389
|
+
}
|
|
2390
|
+
.review-direction-card {
|
|
2391
|
+
display: flex;
|
|
2392
|
+
flex-direction: column;
|
|
2393
|
+
background: var(--review-panel);
|
|
2394
|
+
border: 1px solid var(--review-line);
|
|
2395
|
+
border-radius: 16px;
|
|
2396
|
+
overflow: hidden;
|
|
2397
|
+
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
|
|
2398
|
+
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
2399
|
+
}
|
|
2400
|
+
.review-direction-card.selected {
|
|
2401
|
+
border-color: var(--review-indigo);
|
|
2402
|
+
box-shadow: 0 0 0 2px var(--review-indigo), 0 12px 24px rgba(79, 70, 229, 0.16);
|
|
2403
|
+
}
|
|
2404
|
+
.review-direction-card.selected .review-direction-select {
|
|
2405
|
+
border-color: var(--review-indigo);
|
|
2406
|
+
background: var(--review-indigo);
|
|
2407
|
+
color: #fff;
|
|
2408
|
+
}
|
|
2409
|
+
.review-direction-image {
|
|
2410
|
+
display: block;
|
|
2411
|
+
background: var(--review-panel-soft);
|
|
2412
|
+
border-bottom: 1px solid var(--review-line);
|
|
2413
|
+
aspect-ratio: 4 / 3;
|
|
2414
|
+
overflow: hidden;
|
|
2415
|
+
}
|
|
2416
|
+
.review-direction-image img {
|
|
2417
|
+
display: block;
|
|
2418
|
+
width: 100%;
|
|
2419
|
+
height: 100%;
|
|
2420
|
+
object-fit: cover;
|
|
2421
|
+
}
|
|
2422
|
+
.review-direction-image-missing {
|
|
2423
|
+
display: flex;
|
|
2424
|
+
align-items: center;
|
|
2425
|
+
justify-content: center;
|
|
2426
|
+
color: var(--review-muted);
|
|
2427
|
+
font-size: 14px;
|
|
2428
|
+
}
|
|
2429
|
+
.review-direction-body {
|
|
2430
|
+
display: flex;
|
|
2431
|
+
flex-direction: column;
|
|
2432
|
+
gap: 8px;
|
|
2433
|
+
padding: 16px;
|
|
2434
|
+
}
|
|
2435
|
+
.review-direction-head {
|
|
2436
|
+
display: flex;
|
|
2437
|
+
align-items: center;
|
|
2438
|
+
gap: 8px;
|
|
2439
|
+
}
|
|
2440
|
+
.review-direction-head h3 {
|
|
2441
|
+
margin: 0;
|
|
2442
|
+
font-size: 17px;
|
|
2443
|
+
}
|
|
2444
|
+
.review-direction-index {
|
|
2445
|
+
flex-shrink: 0;
|
|
2446
|
+
border: 1px solid #c7d2fe;
|
|
2447
|
+
background: #eef2ff;
|
|
2448
|
+
color: var(--review-indigo);
|
|
2449
|
+
border-radius: 999px;
|
|
2450
|
+
padding: 2px 10px;
|
|
2451
|
+
font-size: 12px;
|
|
2452
|
+
font-weight: 800;
|
|
2453
|
+
}
|
|
2454
|
+
.review-direction-summary {
|
|
2455
|
+
margin: 0;
|
|
2456
|
+
color: var(--review-text);
|
|
2457
|
+
font-size: 14px;
|
|
2458
|
+
line-height: 1.6;
|
|
2459
|
+
}
|
|
2460
|
+
.review-direction-tradeoff {
|
|
2461
|
+
margin: 0;
|
|
2462
|
+
color: var(--review-muted);
|
|
2463
|
+
font-size: 13px;
|
|
2464
|
+
line-height: 1.6;
|
|
2465
|
+
}
|
|
2466
|
+
.review-direction-select {
|
|
2467
|
+
cursor: pointer;
|
|
2468
|
+
align-self: flex-start;
|
|
2469
|
+
margin-top: 4px;
|
|
2470
|
+
min-height: 38px;
|
|
2471
|
+
border: 1px solid #c7d2fe;
|
|
2472
|
+
border-radius: 10px;
|
|
2473
|
+
padding: 0 16px;
|
|
2474
|
+
font: inherit;
|
|
2475
|
+
font-size: 14px;
|
|
2476
|
+
font-weight: 800;
|
|
2477
|
+
background: #eef2ff;
|
|
2478
|
+
color: var(--review-indigo);
|
|
2479
|
+
transition: background-color 160ms ease, border-color 160ms ease, transform 160ms ease;
|
|
2480
|
+
}
|
|
2481
|
+
.review-direction-select:hover,
|
|
2482
|
+
.review-direction-select:focus-visible {
|
|
2483
|
+
border-color: #a5b4fc;
|
|
2484
|
+
background: #e0e7ff;
|
|
2485
|
+
transform: translateY(-1px);
|
|
2486
|
+
outline: none;
|
|
2487
|
+
}
|
|
2084
2488
|
@media (max-width: 860px) {
|
|
2085
2489
|
.review-overview {
|
|
2086
2490
|
grid-template-columns: 1fr;
|
|
@@ -2088,6 +2492,9 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
2088
2492
|
.review-panel-grid {
|
|
2089
2493
|
grid-template-columns: 1fr;
|
|
2090
2494
|
}
|
|
2495
|
+
.review-direction-grid {
|
|
2496
|
+
grid-template-columns: 1fr;
|
|
2497
|
+
}
|
|
2091
2498
|
}
|
|
2092
2499
|
@media (max-width: 620px) {
|
|
2093
2500
|
.review-page { padding: 18px 12px 128px; }
|
|
@@ -2126,8 +2533,10 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
2126
2533
|
<div class="review-brand">OpenPrd / 评审面板</div>
|
|
2127
2534
|
${renderReviewProjectVersion(visibleProjectRelease)}
|
|
2128
2535
|
</header>
|
|
2536
|
+
${renderReviewStageRail(snapshot)}
|
|
2129
2537
|
${renderReviewOverview(snapshot, sectionsData, panelAgenda)}
|
|
2130
2538
|
${renderReviewGapPanel(sectionsData)}
|
|
2539
|
+
${renderReviewDesignDirections(snapshot)}
|
|
2131
2540
|
${renderReviewFlowSvg(snapshot, sectionsData)}
|
|
2132
2541
|
<section class="review-panel-grid" aria-label="固定评审项">
|
|
2133
2542
|
${panels.join('\n')}
|
|
@@ -2160,6 +2569,18 @@ function renderReviewPage({ snapshot, sectionsData, projectRelease }) {
|
|
|
2160
2569
|
flashCopied(button);
|
|
2161
2570
|
});
|
|
2162
2571
|
});
|
|
2572
|
+
document.querySelectorAll('.review-direction-select').forEach((button) => {
|
|
2573
|
+
button.addEventListener('click', () => {
|
|
2574
|
+
document.querySelectorAll('.review-direction-card').forEach((card) => {
|
|
2575
|
+
card.classList.remove('selected');
|
|
2576
|
+
const cardButton = card.querySelector('.review-direction-select');
|
|
2577
|
+
if (cardButton && cardButton !== button) cardButton.textContent = '选这个方向';
|
|
2578
|
+
});
|
|
2579
|
+
const ownCard = button.closest('.review-direction-card');
|
|
2580
|
+
if (ownCard) ownCard.classList.add('selected');
|
|
2581
|
+
setTimeout(() => { button.textContent = '已选定,把复制内容发回给 Agent'; }, 1250);
|
|
2582
|
+
});
|
|
2583
|
+
});
|
|
2163
2584
|
</script>
|
|
2164
2585
|
</main>
|
|
2165
2586
|
</body>
|
package/src/knowledge.js
CHANGED
|
@@ -3,6 +3,13 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
|
|
5
5
|
import { resolveQualityLearningSource } from './quality-learning.js';
|
|
6
|
+
import {
|
|
7
|
+
buildSessionEvidenceReviewSource,
|
|
8
|
+
extractSessionEvidenceRecords,
|
|
9
|
+
filterGenericFrameworkTokens,
|
|
10
|
+
normalizeSessionEvidenceRecords,
|
|
11
|
+
sanitizeEvidenceText,
|
|
12
|
+
} from './session-evidence.js';
|
|
6
13
|
import { timestamp } from './time.js';
|
|
7
14
|
|
|
8
15
|
const KNOWLEDGE_DIR = cjoin('.openprd', 'knowledge');
|
|
@@ -211,7 +218,7 @@ function normalizeSearchTokens(value) {
|
|
|
211
218
|
.map((item) => item.trim())
|
|
212
219
|
.filter((item) => item.length >= 3);
|
|
213
220
|
const hanTokens = String(value ?? '').match(/[\u4e00-\u9fa5]{2,}/g) ?? [];
|
|
214
|
-
return uniq([...asciiTokens, ...hanTokens]);
|
|
221
|
+
return filterGenericFrameworkTokens(uniq([...asciiTokens, ...hanTokens]));
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
function sortByLength(items = []) {
|
|
@@ -253,16 +260,17 @@ function scoreQueryAgainstFields(queryText, queryTokens, fields = []) {
|
|
|
253
260
|
let score = 0;
|
|
254
261
|
const matchedOn = [];
|
|
255
262
|
for (const field of fields) {
|
|
256
|
-
const text =
|
|
263
|
+
const text = sanitizeEvidenceText(field);
|
|
257
264
|
if (!text) continue;
|
|
258
265
|
const normalized = normalizeSearchText(text);
|
|
259
266
|
if (!normalized) continue;
|
|
267
|
+
const fieldTokens = normalizeSearchTokens(text);
|
|
268
|
+
if (fieldTokens.length === 0) continue;
|
|
260
269
|
let matched = false;
|
|
261
270
|
if (normalized.length >= 6 && queryText.includes(normalized)) {
|
|
262
271
|
matched = true;
|
|
263
272
|
score += normalized.length >= 18 ? 10 : 7;
|
|
264
273
|
} else {
|
|
265
|
-
const fieldTokens = normalizeSearchTokens(text);
|
|
266
274
|
const overlap = fieldTokens.filter((token) => queryTokens.includes(token));
|
|
267
275
|
if (overlap.length > 0) {
|
|
268
276
|
matched = true;
|
|
@@ -889,10 +897,11 @@ function buildTurnReviewTitle(raw, source, touchedFiles = []) {
|
|
|
889
897
|
?? '项目经验草案';
|
|
890
898
|
}
|
|
891
899
|
|
|
892
|
-
function knowledgeCandidateFingerprint(categories = [], touchedFiles = []) {
|
|
900
|
+
function knowledgeCandidateFingerprint(categories = [], touchedFiles = [], sourceSeed = null) {
|
|
893
901
|
const normalizedCategories = [...new Set((categories ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
|
|
894
902
|
const normalizedFiles = [...new Set((touchedFiles ?? []).map((item) => String(item ?? '').trim()).filter(Boolean))].sort();
|
|
895
|
-
|
|
903
|
+
const normalizedSourceSeed = String(sourceSeed ?? '').trim();
|
|
904
|
+
return crypto.createHash('sha256').update(JSON.stringify([normalizedCategories, normalizedFiles, normalizedSourceSeed])).digest('hex').slice(0, 16);
|
|
896
905
|
}
|
|
897
906
|
|
|
898
907
|
async function findPendingCandidateByFingerprint(projectRoot, fingerprint, excludeId) {
|
|
@@ -1195,6 +1204,7 @@ function buildKnowledgeCandidateMeta({
|
|
|
1195
1204
|
updatedAt: timestamp(),
|
|
1196
1205
|
sourceKind: source.kind,
|
|
1197
1206
|
sourceRef: source.sourceId,
|
|
1207
|
+
evidenceOnly: source.evidenceOnly === true,
|
|
1198
1208
|
title,
|
|
1199
1209
|
summary,
|
|
1200
1210
|
categories,
|
|
@@ -1405,6 +1415,11 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1405
1415
|
const turnStateSource = (await exists(knowledgePath(projectRoot, OPENPRD_HARNESS_TURN_STATE))) ? OPENPRD_HARNESS_TURN_STATE : null;
|
|
1406
1416
|
const from = firstString(options.from, turnStateSource, latestReportPath);
|
|
1407
1417
|
const rawInput = from ? await loadRawReviewInput(projectRoot, from) : { sourcePath: null, raw: null };
|
|
1418
|
+
const evidenceRecords = normalizeSessionEvidenceRecords([
|
|
1419
|
+
...normalizeSessionEvidenceRecords(options.evidenceRecords),
|
|
1420
|
+
...extractSessionEvidenceRecords(options.sessionEvents ?? []),
|
|
1421
|
+
]);
|
|
1422
|
+
const cleanEvidenceRecords = evidenceRecords.filter((record) => record.sourceKind !== 'context-only');
|
|
1408
1423
|
const resolved = from
|
|
1409
1424
|
? await resolveQualityLearningSource(projectRoot, {
|
|
1410
1425
|
from,
|
|
@@ -1414,13 +1429,20 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1414
1429
|
: { ok: false, error: 'no-review-source' };
|
|
1415
1430
|
const source = resolved.ok
|
|
1416
1431
|
? resolved.source
|
|
1432
|
+
: (cleanEvidenceRecords.length > 0
|
|
1433
|
+
? buildSessionEvidenceReviewSource({
|
|
1434
|
+
records: cleanEvidenceRecords,
|
|
1435
|
+
sourceId: firstString(options.sourceId, options.title, cleanEvidenceRecords[0]?.text),
|
|
1436
|
+
sourcePath: rawInput.sourcePath ?? latestReportPath ?? KNOWLEDGE_REVIEW_SIGNAL_LOG,
|
|
1437
|
+
title: firstString(options.title, cleanEvidenceRecords.find((record) => record.sourceKind === 'user-direct')?.text),
|
|
1438
|
+
})
|
|
1417
1439
|
: buildSyntheticReviewSource(projectRoot, {
|
|
1418
1440
|
signal: options.signal,
|
|
1419
1441
|
recentSignals,
|
|
1420
1442
|
touchedFiles: options.touchedFiles,
|
|
1421
1443
|
sourcePath: rawInput.sourcePath ?? latestReportPath ?? KNOWLEDGE_REVIEW_SIGNAL_LOG,
|
|
1422
1444
|
title: firstString(options.title, readJsonObject(rawInput.raw)?.title),
|
|
1423
|
-
});
|
|
1445
|
+
}));
|
|
1424
1446
|
if (!resolved.ok && source.evidenceSources.length === 0 && source.rootCauseCandidates.length === 0) {
|
|
1425
1447
|
return {
|
|
1426
1448
|
ok: true,
|
|
@@ -1455,18 +1477,20 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1455
1477
|
const reviewSignals = reviewContext.reviewSignals;
|
|
1456
1478
|
const categories = buildKnowledgeCategories({ source, touchedFiles: substantiveTouchedFiles, reviewSignals });
|
|
1457
1479
|
const reasons = categories.map(categoryReason);
|
|
1480
|
+
const allowEvidenceOnly = options.allowEvidenceOnly === true || source.evidenceOnly === true || source.kind === 'session-evidence-review';
|
|
1458
1481
|
const hasStrongSignal = categories.length > 0
|
|
1459
1482
|
|| source.rootCauseCandidates.length > 0
|
|
1460
1483
|
|| source.symptoms.length > 1
|
|
1461
1484
|
|| source.kind === 'completion-review'
|
|
1485
|
+
|| source.kind === 'session-evidence-review'
|
|
1462
1486
|
|| reviewSignals.some((signal) => signal.ok === true || signal.productionReady === true);
|
|
1463
1487
|
|
|
1464
|
-
if (substantiveTouchedFiles.length === 0 || !hasStrongSignal) {
|
|
1488
|
+
if ((substantiveTouchedFiles.length === 0 && !allowEvidenceOnly) || !hasStrongSignal) {
|
|
1465
1489
|
return {
|
|
1466
1490
|
ok: true,
|
|
1467
1491
|
action: 'quality-knowledge-review',
|
|
1468
1492
|
skipped: true,
|
|
1469
|
-
reason: substantiveTouchedFiles.length === 0 ? 'no-substantive-touched-files' : 'no-knowledge-signal',
|
|
1493
|
+
reason: substantiveTouchedFiles.length === 0 && !allowEvidenceOnly ? 'no-substantive-touched-files' : 'no-knowledge-signal',
|
|
1470
1494
|
sourceKind: source.kind,
|
|
1471
1495
|
sourcePath: source.sourcePath,
|
|
1472
1496
|
};
|
|
@@ -1477,7 +1501,11 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1477
1501
|
let candidateId = rawCandidateRef
|
|
1478
1502
|
? (rawCandidateRef.startsWith('candidate-') ? rawCandidateRef : `candidate-${slugify(rawCandidateRef, 'knowledge')}`)
|
|
1479
1503
|
: `candidate-${slugify(source.sourceId ?? title, 'knowledge')}`;
|
|
1480
|
-
const fingerprint = knowledgeCandidateFingerprint(
|
|
1504
|
+
const fingerprint = knowledgeCandidateFingerprint(
|
|
1505
|
+
categories,
|
|
1506
|
+
substantiveTouchedFiles,
|
|
1507
|
+
allowEvidenceOnly ? firstString(source.sourceId, source.title, source.sourcePath) : null,
|
|
1508
|
+
);
|
|
1481
1509
|
const duplicateCandidate = await findPendingCandidateByFingerprint(projectRoot, fingerprint, candidateId);
|
|
1482
1510
|
if (duplicateCandidate?.candidateId) {
|
|
1483
1511
|
candidateId = duplicateCandidate.candidateId;
|
|
@@ -1493,11 +1521,15 @@ export async function reviewKnowledgeWorkspace(projectRoot, options = {}) {
|
|
|
1493
1521
|
const existingCandidate = await readJson(candidatePath).catch(() => null);
|
|
1494
1522
|
const relativeCandidateDir = path.relative(projectRoot, candidateDir).split(path.sep).join('/');
|
|
1495
1523
|
const relativeDraftSkillPath = path.relative(projectRoot, draftSkillPath).split(path.sep).join('/');
|
|
1524
|
+
const scopeSummary = substantiveTouchedFiles.length > 0
|
|
1525
|
+
? `本轮围绕 ${substantiveTouchedFiles.length} 个可沉淀文件生成回顾。`
|
|
1526
|
+
: '本轮基于只读证据生成项目经验候选。';
|
|
1496
1527
|
const reviewSummary = [
|
|
1497
|
-
|
|
1528
|
+
scopeSummary,
|
|
1498
1529
|
reasons[0] ?? '这次实现已经具备项目级经验抽象价值。',
|
|
1499
1530
|
reviewSignals.length > 0 ? `已记录 ${reviewSignals.length} 条回顾信号。` : null,
|
|
1500
1531
|
touchedFileSource === 'inferred-recent-files' ? '本轮 touched files 来自最近修改文件推断。' : null,
|
|
1532
|
+
allowEvidenceOnly && substantiveTouchedFiles.length === 0 ? '本轮没有把 AGENTS、system 或 tool schema 命中当成用户证据。' : null,
|
|
1501
1533
|
].filter(Boolean).join(' ');
|
|
1502
1534
|
const draftCandidate = buildKnowledgeCandidateMeta({
|
|
1503
1535
|
projectRoot,
|
|
@@ -1740,7 +1772,9 @@ function buildKnowledgeMatchQuery(options = {}) {
|
|
|
1740
1772
|
options.activeChange,
|
|
1741
1773
|
options.nextTaskTitle,
|
|
1742
1774
|
...(normalizeStringList(options.relatedFiles)),
|
|
1743
|
-
]
|
|
1775
|
+
]
|
|
1776
|
+
.map((field) => sanitizeEvidenceText(field))
|
|
1777
|
+
.filter(Boolean);
|
|
1744
1778
|
const text = candidateFields.join('\n');
|
|
1745
1779
|
return {
|
|
1746
1780
|
text,
|