新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
//! 对抗式评估系统 — 正反方辩论 + AI 分析师
|
||||
//!
|
||||
//! 当前为基于评分与内容信号的启发式实现(稳定、有区分度)。
|
||||
//! TODO: 接入 df-ai LlmProvider 让正反方论点由 LLM 生成,启发式降级为 fallback。
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
use df_core::types::{IdeaId, Priority};
|
||||
use crate::capture::Idea;
|
||||
use crate::scoring::IdeaScores;
|
||||
|
||||
/// 对抗评估结果
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -18,7 +21,7 @@ pub struct AdversarialEval {
|
||||
pub recommendation: Recommendation,
|
||||
}
|
||||
|
||||
/// 正方论点
|
||||
/// 论点(正方/反方共用同一结构)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Argument {
|
||||
pub thesis: String, // 核心观点
|
||||
@@ -27,15 +30,6 @@ pub struct Argument {
|
||||
pub confidence: f64, // 置信度 0-1
|
||||
}
|
||||
|
||||
/// 反方论点
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CounterArgument {
|
||||
pub thesis: String, // 反对观点
|
||||
pub evidence: Vec<String>, // 反对证据
|
||||
pub reasoning: Vec<String>, // 反驳推理
|
||||
pub confidence: f64, // 置信度 0-1
|
||||
}
|
||||
|
||||
/// AI 分析师综合分析
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnalystAnalysis {
|
||||
@@ -55,7 +49,6 @@ pub enum AssessmentLevel {
|
||||
Conditional, // 有条件执行
|
||||
Revised, // 需要修改后执行
|
||||
Defer, // 推迟执行
|
||||
Reject, // 不推荐执行
|
||||
}
|
||||
|
||||
/// 最终建议
|
||||
@@ -66,7 +59,6 @@ pub enum Recommendation {
|
||||
WithResources, // 配置资源后行动
|
||||
ResearchMore, // 需要更多研究
|
||||
Monitor, // 持续监控
|
||||
Cancel, // 取消想法
|
||||
}
|
||||
|
||||
/// 对抗评估引擎
|
||||
@@ -74,133 +66,166 @@ pub struct AdversarialEngine;
|
||||
|
||||
impl AdversarialEngine {
|
||||
/// 执行完整的对抗评估
|
||||
#[allow(clippy::unused_async)] // 签名保留 async,待接 LLM 注入异步调用
|
||||
pub async fn evaluate(idea: &Idea) -> Result<AdversarialEval> {
|
||||
// 1. 生成正方观点
|
||||
let positive = Self::generate_positive_argument(idea).await?;
|
||||
// 先做多维评分,作为正反方论点与置信度的依据
|
||||
let scores = crate::scoring::ScoringEngine::compute_default(idea);
|
||||
|
||||
// 2. 生成反方观点
|
||||
let negative = Self::generate_negative_argument(idea, &positive).await?;
|
||||
|
||||
// 3. AI 分析师综合分析
|
||||
let analyst = Self::analyst_analysis(idea, &positive, &negative).await?;
|
||||
|
||||
// 4. 计算最终分数和建议
|
||||
let (final_score, recommendation) = Self::compute_final_assessment(&analyst);
|
||||
let positive = Self::generate_positive_argument(idea, &scores)?;
|
||||
let negative = Self::generate_negative_argument(idea, &scores)?;
|
||||
let analyst = Self::analyst_analysis(idea, &scores)?;
|
||||
let recommendation = Self::recommendation_for(&analyst.final_assessment);
|
||||
|
||||
Ok(AdversarialEval {
|
||||
idea_id: idea.id.clone(),
|
||||
positive,
|
||||
negative,
|
||||
analyst,
|
||||
final_score,
|
||||
final_score: scores.overall,
|
||||
recommendation,
|
||||
})
|
||||
}
|
||||
|
||||
/// 生成正方观点(支持执行)
|
||||
async fn generate_positive_argument(idea: &Idea) -> Result<Argument> {
|
||||
// TODO: 接入 AI 生成正方观点
|
||||
// 当前使用启发式模板
|
||||
/// 生成正方观点(支持执行)— confidence 由可行性 + 影响力驱动
|
||||
/// 注:返回 Result 为后续 LLM 注入失败预留,启发式阶段恒 Ok
|
||||
fn generate_positive_argument(idea: &Idea, scores: &IdeaScores) -> Result<Argument> {
|
||||
let desc = idea.description.trim();
|
||||
let mut evidence = Vec::new();
|
||||
evidence.push(format!("优先级:{}", priority_label(&idea.priority)));
|
||||
if desc.is_empty() {
|
||||
evidence.push("需求待补充(建议补全描述)".to_string());
|
||||
} else {
|
||||
let head: String = desc.chars().take(60).collect();
|
||||
evidence.push(format!("明确需求:{}", head));
|
||||
}
|
||||
if idea.tags.is_empty() {
|
||||
evidence.push("关联领域待界定".to_string());
|
||||
} else {
|
||||
evidence.push(format!("关联领域:{}", idea.tags.join("、")));
|
||||
}
|
||||
if scores.impact >= 7.0 {
|
||||
evidence.push("业务价值显著,影响面较广".to_string());
|
||||
}
|
||||
|
||||
let title = &idea.title;
|
||||
let desc = &idea.description;
|
||||
// 正方置信度:可行性+影响力等权折算到 [0.1, 0.95],满分≈0.95 留质疑余地
|
||||
let confidence =
|
||||
((scores.feasibility * 0.5 + scores.impact * 0.5) / 10.0).clamp(0.1, 0.95);
|
||||
|
||||
let reasoning = vec![
|
||||
format!("可行性评分 {:.1}/10,路径相对清晰", scores.feasibility),
|
||||
format!("影响力评分 {:.1}/10,预期回报可观", scores.impact),
|
||||
"整体风险可控,适合推进".to_string(),
|
||||
];
|
||||
|
||||
Ok(Argument {
|
||||
thesis: format!("{} 具有很高的价值和可行性,应该优先执行", title),
|
||||
evidence: vec![
|
||||
format!("满足业务需求:{}", desc),
|
||||
"投入产出比高".to_string(),
|
||||
"技术实现可行".to_string(),
|
||||
"时间窗口合适".to_string(),
|
||||
],
|
||||
reasoning: vec![
|
||||
"能够解决现有痛点".to_string(),
|
||||
"竞争优势明显".to_string(),
|
||||
"风险可控".to_string(),
|
||||
],
|
||||
confidence: 0.75,
|
||||
thesis: format!("「{}」具备明确价值与可行性,建议优先推进", idea.title),
|
||||
evidence,
|
||||
reasoning,
|
||||
confidence,
|
||||
})
|
||||
}
|
||||
|
||||
/// 生成反方观点(反对或谨慎)
|
||||
async fn generate_negative_argument(idea: &Idea, positive: &Argument) -> Result<CounterArgument> {
|
||||
// TODO: 接入 AI 生成反方观点,考虑正方观点
|
||||
/// 生成反方观点(反对或谨慎)— 论点基于想法实际缺陷,confidence 随风险上升
|
||||
fn generate_negative_argument(idea: &Idea, scores: &IdeaScores) -> Result<Argument> {
|
||||
let desc = idea.description.trim();
|
||||
let mut evidence = Vec::new();
|
||||
if desc.is_empty() {
|
||||
evidence.push("描述过于简略,需求边界不清".to_string());
|
||||
} else if desc.chars().count() < 50 {
|
||||
evidence.push("描述偏短,实现细节尚未论证".to_string());
|
||||
}
|
||||
if idea.tags.is_empty() {
|
||||
evidence.push("缺少标签,影响范围未界定".to_string());
|
||||
}
|
||||
if scores.feasibility < 6.0 {
|
||||
evidence.push(format!("可行性 {:.1}/10 偏低,实现路径存疑", scores.feasibility));
|
||||
}
|
||||
if matches!(idea.priority, Priority::Low) {
|
||||
evidence.push("优先级偏低,可能非当前关键路径".to_string());
|
||||
}
|
||||
if evidence.is_empty() {
|
||||
evidence.push("机会成本需权衡,可能存在更优替代方案".to_string());
|
||||
}
|
||||
|
||||
let title = &idea.title;
|
||||
// 反方强度:feasibility 每降 1 分 +0.04,impact 每降 1 分 +0.03,基线 0.25(满分也保留最低质疑),clamp [0.1, 0.9]
|
||||
let confidence = ((10.0 - scores.feasibility) * 0.04 + (10.0 - scores.impact) * 0.03 + 0.25)
|
||||
.clamp(0.1, 0.9);
|
||||
|
||||
Ok(CounterArgument {
|
||||
thesis: format!("{} 需要谨慎评估,存在一定风险", title),
|
||||
evidence: vec![
|
||||
"资源投入较大".to_string(),
|
||||
"市场不确定性高".to_string(),
|
||||
"技术挑战存在".to_string(),
|
||||
"机会成本高".to_string(),
|
||||
],
|
||||
reasoning: vec![
|
||||
"ROI 需要进一步验证".to_string(),
|
||||
"优先级可能过高".to_string(),
|
||||
"存在更优替代方案".to_string(),
|
||||
],
|
||||
confidence: 0.65,
|
||||
let reasoning = vec![
|
||||
format!("资源投入与当前综合评分 {:.1} 需匹配", scores.overall),
|
||||
"ROI 需进一步验证".to_string(),
|
||||
"需评估是否存在更优解".to_string(),
|
||||
];
|
||||
|
||||
Ok(Argument {
|
||||
thesis: format!("「{}」需谨慎评估,存在风险与机会成本", idea.title),
|
||||
evidence,
|
||||
reasoning,
|
||||
confidence,
|
||||
})
|
||||
}
|
||||
|
||||
/// AI 分析师综合分析
|
||||
async fn analyst_analysis(
|
||||
idea: &Idea,
|
||||
positive: &Argument,
|
||||
negative: &CounterArgument,
|
||||
) -> Result<AnalystAnalysis> {
|
||||
// TODO: 接入 AI 进行深度分析
|
||||
/// AI 分析师综合分析 — 评估等级由综合评分决定,优势/劣势按维度动态生成
|
||||
fn analyst_analysis(idea: &Idea, scores: &IdeaScores) -> Result<AnalystAnalysis> {
|
||||
let final_assessment = match scores.overall {
|
||||
x if x >= 7.5 => AssessmentLevel::StrongGo,
|
||||
x if x >= 6.0 => AssessmentLevel::Recommended,
|
||||
x if x >= 4.5 => AssessmentLevel::Conditional,
|
||||
x if x >= 3.0 => AssessmentLevel::Revised,
|
||||
_ => AssessmentLevel::Defer,
|
||||
};
|
||||
|
||||
let positive_strengths = vec![
|
||||
"方向正确,符合业务战略".to_string(),
|
||||
"技术创新性较强".to_string(),
|
||||
"用户价值明确".to_string(),
|
||||
];
|
||||
let mut strengths = Vec::new();
|
||||
if scores.impact >= 6.0 {
|
||||
strengths.push("业务价值明确".to_string());
|
||||
}
|
||||
if scores.feasibility >= 6.0 {
|
||||
strengths.push("技术路径清晰".to_string());
|
||||
}
|
||||
if scores.urgency >= 7.0 {
|
||||
strengths.push("时间窗口合适".to_string());
|
||||
}
|
||||
if strengths.is_empty() {
|
||||
strengths.push("方向值得探索".to_string());
|
||||
}
|
||||
|
||||
let weaknesses = vec![
|
||||
"资源需求评估不足".to_string(),
|
||||
"风险控制需要加强".to_string(),
|
||||
"时间规划可能过于乐观".to_string(),
|
||||
];
|
||||
let mut weaknesses = Vec::new();
|
||||
if scores.feasibility < 6.0 {
|
||||
weaknesses.push("可行性论证不足".to_string());
|
||||
}
|
||||
if idea.description.trim().is_empty() {
|
||||
weaknesses.push("需求描述缺失".to_string());
|
||||
}
|
||||
if scores.urgency < 4.0 {
|
||||
weaknesses.push("紧急度偏低,易被搁置".to_string());
|
||||
}
|
||||
if weaknesses.is_empty() {
|
||||
weaknesses.push("资源需求待评估".to_string());
|
||||
}
|
||||
|
||||
// 启发式占位:固定风险模板,与具体想法无关,接 LLM 后改动态生成
|
||||
let risks = vec![
|
||||
"技术实现难度超出预期".to_string(),
|
||||
"市场竞争加剧".to_string(),
|
||||
"用户接受度不确定".to_string(),
|
||||
"技术实现难度可能超出预期".to_string(),
|
||||
"优先级与资源争夺".to_string(),
|
||||
"需求范围蔓延".to_string(),
|
||||
];
|
||||
|
||||
let opportunities = vec![
|
||||
"可能形成新的竞争优势".to_string(),
|
||||
"技术积累价值显著".to_string(),
|
||||
"市场机会窗口良好".to_string(),
|
||||
"可能形成可复用能力".to_string(),
|
||||
"积累技术资产".to_string(),
|
||||
];
|
||||
|
||||
// 基于正反方观点的强度计算
|
||||
let positive_strength = positive.confidence;
|
||||
let negative_strength = negative.confidence;
|
||||
let net_positive = (positive_strength - negative_strength + 1.0) / 2.0;
|
||||
|
||||
let final_assessment = if net_positive > 0.7 {
|
||||
AssessmentLevel::StrongGo
|
||||
} else if net_positive > 0.5 {
|
||||
AssessmentLevel::Recommended
|
||||
} else if net_positive > 0.3 {
|
||||
AssessmentLevel::Conditional
|
||||
} else if net_positive > 0.1 {
|
||||
AssessmentLevel::Revised
|
||||
} else {
|
||||
AssessmentLevel::Defer
|
||||
};
|
||||
let summary = format!(
|
||||
"「{}」综合评分 {:.1}/10,{}。建议{}。",
|
||||
idea.title,
|
||||
scores.overall,
|
||||
assessment_desc(&final_assessment),
|
||||
action_hint(&final_assessment)
|
||||
);
|
||||
|
||||
Ok(AnalystAnalysis {
|
||||
summary: format!(
|
||||
"该想法整体价值评估中等偏上,建议在有条件的情况下执行。主要价值在于{},需要关注{}。",
|
||||
idea.title,
|
||||
if net_positive > 0.5 { "风险控制" } else { "价值验证" }
|
||||
),
|
||||
strengths: positive_strengths,
|
||||
summary,
|
||||
strengths,
|
||||
weaknesses,
|
||||
risks,
|
||||
opportunities,
|
||||
@@ -208,101 +233,148 @@ impl AdversarialEngine {
|
||||
})
|
||||
}
|
||||
|
||||
/// 计算最终评估分数和建议
|
||||
fn compute_final_assessment(analyst: &AnalystAnalysis) -> (f64, Recommendation) {
|
||||
// 基于评估等级映射分数
|
||||
let base_score = match analyst.final_assessment {
|
||||
AssessmentLevel::StrongGo => 8.5,
|
||||
AssessmentLevel::Recommended => 7.0,
|
||||
AssessmentLevel::Conditional => 5.5,
|
||||
AssessmentLevel::Revised => 4.0,
|
||||
AssessmentLevel::Defer => 2.5,
|
||||
AssessmentLevel::Reject => 1.0,
|
||||
};
|
||||
|
||||
// 根据优劣势微调分数
|
||||
let strength_count = analyst.strengths.len() as f64;
|
||||
let weakness_count = analyst.weaknesses.len() as f64;
|
||||
let score_adjustment = (strength_count - weakness_count) * 0.3;
|
||||
|
||||
let final_score = (base_score + score_adjustment).clamp(0.0, 10.0);
|
||||
|
||||
let recommendation = match analyst.final_assessment {
|
||||
/// 评估等级 → 最终建议
|
||||
fn recommendation_for(level: &AssessmentLevel) -> Recommendation {
|
||||
match level {
|
||||
AssessmentLevel::StrongGo => Recommendation::ImmediateAction,
|
||||
AssessmentLevel::Recommended => Recommendation::Soon,
|
||||
AssessmentLevel::Conditional => Recommendation::WithResources,
|
||||
AssessmentLevel::Revised => Recommendation::ResearchMore,
|
||||
AssessmentLevel::Defer => Recommendation::Monitor,
|
||||
AssessmentLevel::Reject => Recommendation::Cancel,
|
||||
};
|
||||
|
||||
(final_score, recommendation)
|
||||
}
|
||||
}
|
||||
|
||||
/// 评估结果展示格式
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EvalDisplay {
|
||||
pub idea_title: String,
|
||||
pub positive_strength: f64,
|
||||
pub negative_strength: f64,
|
||||
pub net_sentiment: f64, // -1 到 1,正为正面
|
||||
pub assessment_level: String,
|
||||
pub key_takeaways: Vec<String>,
|
||||
pub action_items: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<AdversarialEval> for EvalDisplay {
|
||||
fn from(eval: AdversarialEval) -> Self {
|
||||
let net_sentiment = (eval.positive.confidence - eval.negative.confidence) as f64;
|
||||
|
||||
let key_takeaways = vec![
|
||||
format!("优势:{}", eval.analyst.strengths.join("、")),
|
||||
format!("风险:{}", eval.analyst.risks.join("、")),
|
||||
format!("建议:{:?}", eval.recommendation),
|
||||
];
|
||||
|
||||
let action_items = match eval.recommendation {
|
||||
Recommendation::ImmediateAction => vec![
|
||||
"立即组建项目团队".to_string(),
|
||||
"制定详细执行计划".to_string(),
|
||||
"分配必要资源".to_string(),
|
||||
],
|
||||
Recommendation::Soon => vec![
|
||||
"下周启动项目".to_string(),
|
||||
"准备资源需求".to_string(),
|
||||
"制定时间表".to_string(),
|
||||
],
|
||||
Recommendation::WithResources => vec![
|
||||
"确认资源预算".to_string(),
|
||||
"评估ROI".to_string(),
|
||||
"制定风险预案".to_string(),
|
||||
],
|
||||
Recommendation::ResearchMore => vec![
|
||||
"进行市场调研".to_string(),
|
||||
"收集用户反馈".to_string(),
|
||||
"验证技术可行性".to_string(),
|
||||
],
|
||||
Recommendation::Monitor => vec![
|
||||
"持续跟踪相关指标".to_string(),
|
||||
"定期评估进展".to_string(),
|
||||
"等待更好的时机".to_string(),
|
||||
],
|
||||
Recommendation::Cancel => vec![
|
||||
"记录归档原因".to_string(),
|
||||
"释放相关资源".to_string(),
|
||||
"提取经验教训".to_string(),
|
||||
],
|
||||
};
|
||||
|
||||
EvalDisplay {
|
||||
idea_title: eval.positive.thesis.split(' ').take(3).collect::<Vec<_>>().join(" "),
|
||||
positive_strength: eval.positive.confidence,
|
||||
negative_strength: eval.negative.confidence,
|
||||
net_sentiment,
|
||||
assessment_level: format!("{:?}", eval.analyst.final_assessment),
|
||||
key_takeaways,
|
||||
action_items,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn priority_label(p: &Priority) -> &'static str {
|
||||
match p {
|
||||
Priority::Critical => "紧急",
|
||||
Priority::High => "高",
|
||||
Priority::Medium => "中",
|
||||
Priority::Low => "低",
|
||||
}
|
||||
}
|
||||
|
||||
fn assessment_desc(level: &AssessmentLevel) -> &'static str {
|
||||
match level {
|
||||
AssessmentLevel::StrongGo => "价值高且可行性强",
|
||||
AssessmentLevel::Recommended => "整体值得推进",
|
||||
AssessmentLevel::Conditional => "有条件地推进",
|
||||
AssessmentLevel::Revised => "需调整后再评估",
|
||||
AssessmentLevel::Defer => "建议暂缓",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_hint(level: &AssessmentLevel) -> &'static str {
|
||||
match level {
|
||||
AssessmentLevel::StrongGo => "立即立项启动",
|
||||
AssessmentLevel::Recommended => "尽快排期",
|
||||
AssessmentLevel::Conditional => "配置资源后启动",
|
||||
AssessmentLevel::Revised => "补充信息后重新评估",
|
||||
AssessmentLevel::Defer => "持续观察时机",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::Idea;
|
||||
use crate::scoring::ScoringEngine;
|
||||
use df_core::types::{IdeaStatus, Priority};
|
||||
|
||||
fn make_idea(title: &str, desc: &str, priority: Priority, tags: Vec<&str>) -> Idea {
|
||||
Idea {
|
||||
id: "test-id".to_string(),
|
||||
title: title.to_string(),
|
||||
description: desc.to_string(),
|
||||
status: IdeaStatus::Draft,
|
||||
priority,
|
||||
scores: None,
|
||||
tags: tags.into_iter().map(String::from).collect(),
|
||||
source: None,
|
||||
related_ids: Vec::new(),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a1_high_score_immediate_action() {
|
||||
let desc = "面向用户的核心功能,带来显著增长,大幅提升效率。集成成熟方案,复用已有组件。".repeat(3);
|
||||
let idea = make_idea("AI增长引擎", &desc, Priority::Critical, vec!["增长", "核心"]);
|
||||
let scores = ScoringEngine::compute_default(&idea);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a1] 高分想法 → 期望 ImmediateAction");
|
||||
println!(" scores: feas={:.2} impact={:.2} urg={:.2} overall={:.2}", scores.feasibility, scores.impact, scores.urgency, scores.overall);
|
||||
println!(" eval: final_score={:.2} recommendation={:?}", eval.final_score, eval.recommendation);
|
||||
println!(" 正方 confidence={:.2} 反方 confidence={:.2}", eval.positive.confidence, eval.negative.confidence);
|
||||
assert!(eval.final_score >= 7.5, "final_score 应≥7.5, 实际 {:.2}", eval.final_score);
|
||||
assert_eq!(eval.recommendation, Recommendation::ImmediateAction);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a2_mid_score_soon() {
|
||||
let desc = "面向用户的功能,集成已有方案,提升体验".to_string();
|
||||
let idea = make_idea("体验优化", &desc, Priority::Medium, vec!["体验"]);
|
||||
let scores = ScoringEngine::compute_default(&idea);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a2] 中分想法 → 期望 Soon");
|
||||
println!(" scores overall={:.2} eval final_score={:.2} recommendation={:?}", scores.overall, eval.final_score, eval.recommendation);
|
||||
assert_eq!(eval.recommendation, Recommendation::Soon);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a3_low_score_monitor() {
|
||||
let desc = "重构迁移大规模分布式重写从零全新架构高并发底层".to_string();
|
||||
let idea = make_idea("过度工程", &desc, Priority::Low, vec![]);
|
||||
let scores = ScoringEngine::compute_default(&idea);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a3] 低分想法 → 期望 Monitor");
|
||||
println!(" scores overall={:.2} eval final_score={:.2} recommendation={:?}", scores.overall, eval.final_score, eval.recommendation);
|
||||
assert!(eval.final_score < 3.0, "final_score 应<3.0, 实际 {:.2}", eval.final_score);
|
||||
assert_eq!(eval.recommendation, Recommendation::Monitor);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a4_confidence_ranges() {
|
||||
let idea = make_idea("普通想法", "一般描述", Priority::Medium, vec!["标签"]);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a4] confidence 区间校验");
|
||||
println!(" 正方={:.2} (应∈[0.1, 0.95]) 反方={:.2} (应∈[0.1, 0.9])", eval.positive.confidence, eval.negative.confidence);
|
||||
assert!(eval.positive.confidence >= 0.1 && eval.positive.confidence <= 0.95);
|
||||
assert!(eval.negative.confidence >= 0.1 && eval.negative.confidence <= 0.9);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a5_positive_thesis_contains_title() {
|
||||
let idea = make_idea("独家创意", "描述内容", Priority::High, vec![]);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a5] 正方论点含标题");
|
||||
println!(" thesis: {}", eval.positive.thesis);
|
||||
assert!(eval.positive.thesis.contains("独家创意"), "正方 thesis 应含标题");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a6_negative_evidence_nonempty() {
|
||||
let idea = make_idea("待质疑想法", "短", Priority::Low, vec![]);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a6] 反方证据非空 ({} 条)", eval.negative.evidence.len());
|
||||
for (i, e) in eval.negative.evidence.iter().enumerate() {
|
||||
println!(" 证据{}: {}", i + 1, e);
|
||||
}
|
||||
assert!(!eval.negative.evidence.is_empty(), "反方 evidence 不应为空");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a7_final_score_consistency() {
|
||||
let desc = "面向用户的核心功能".to_string();
|
||||
let idea = make_idea("一致性测试", &desc, Priority::High, vec!["核心"]);
|
||||
let scores = ScoringEngine::compute_default(&idea);
|
||||
let eval = AdversarialEngine::evaluate(&idea).await.unwrap();
|
||||
println!("\n[a7] final_score == scores.overall 一致性");
|
||||
println!(" scores.overall={:.2} eval.final_score={:.2}", scores.overall, eval.final_score);
|
||||
println!(" analyst.summary: {}", eval.analyst.summary);
|
||||
assert!((eval.final_score - scores.overall).abs() < 0.001, "final_score 应等于 overall");
|
||||
assert!(eval.analyst.summary.contains("一致性测试"), "summary 应含标题");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
//! 想法评估器 — 对想法进行多维度评估
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
|
||||
use crate::adversarial::{AdversarialEngine, AdversarialEval};
|
||||
use crate::capture::Idea;
|
||||
use crate::scoring::IdeaScores;
|
||||
|
||||
/// 评估维度
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum EvalDimension {
|
||||
/// 可行性
|
||||
Feasibility,
|
||||
/// 影响力
|
||||
Impact,
|
||||
/// 紧急度
|
||||
Urgency,
|
||||
}
|
||||
|
||||
/// 评估结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EvalResult {
|
||||
pub idea_id: IdeaId,
|
||||
pub scores: IdeaScores,
|
||||
pub recommendation: Recommendation,
|
||||
pub comments: Vec<String>,
|
||||
}
|
||||
|
||||
/// 评估建议
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Recommendation {
|
||||
/// 强烈推荐立即执行
|
||||
StrongApprove,
|
||||
/// 推荐执行
|
||||
Approve,
|
||||
/// 需要更多信息
|
||||
NeedsInfo,
|
||||
/// 建议推迟
|
||||
Defer,
|
||||
/// 不推荐
|
||||
Reject,
|
||||
}
|
||||
|
||||
/// 想法评估器
|
||||
pub struct IdeaEvaluator;
|
||||
|
||||
impl IdeaEvaluator {
|
||||
/// 评估一个想法 - 使用对抗式评估
|
||||
pub async fn evaluate_adversarial(idea: &Idea) -> Result<AdversarialEval> {
|
||||
AdversarialEngine::evaluate(idea).await
|
||||
}
|
||||
|
||||
/// 评估一个想法 - 保持向后兼容
|
||||
pub fn evaluate(idea: &Idea) -> Result<EvalResult> {
|
||||
// 使用简单评分作为后备
|
||||
let scores = crate::scoring::ScoringEngine::compute_default(idea);
|
||||
|
||||
let recommendation = if scores.overall >= 8.0 {
|
||||
Recommendation::StrongApprove
|
||||
} else if scores.overall >= 6.0 {
|
||||
Recommendation::Approve
|
||||
} else if scores.overall >= 4.0 {
|
||||
Recommendation::NeedsInfo
|
||||
} else if scores.overall >= 2.0 {
|
||||
Recommendation::Defer
|
||||
} else {
|
||||
Recommendation::Reject
|
||||
};
|
||||
|
||||
Ok(EvalResult {
|
||||
idea_id: idea.id.clone(),
|
||||
scores,
|
||||
recommendation,
|
||||
comments: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//! 想法关联图 — 管理想法之间的关系
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use df_core::types::IdeaId;
|
||||
|
||||
/// 想法之间的关系类型
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RelationKind {
|
||||
/// 相似(语义相近)
|
||||
Similar,
|
||||
/// 依赖(A 依赖 B)
|
||||
DependsOn,
|
||||
/// 衍生(A 衍生自 B)
|
||||
DerivedFrom,
|
||||
/// 互补(A 和 B 可以互补)
|
||||
Complementary,
|
||||
}
|
||||
|
||||
/// 想法关系边
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Relation {
|
||||
pub source_id: IdeaId,
|
||||
pub target_id: IdeaId,
|
||||
pub kind: RelationKind,
|
||||
pub strength: f64, // 0.0 ~ 1.0
|
||||
}
|
||||
|
||||
/// 想法关联图
|
||||
pub struct IdeaGraph {
|
||||
/// 邻接表(idea_id -> 相关关系列表)
|
||||
edges: HashMap<IdeaId, Vec<Relation>>,
|
||||
}
|
||||
|
||||
impl IdeaGraph {
|
||||
/// 创建空图
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
edges: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加关系
|
||||
pub fn add_relation(&mut self, source_id: IdeaId, target_id: IdeaId, kind: RelationKind, strength: f64) {
|
||||
let relation = Relation {
|
||||
source_id: source_id.clone(),
|
||||
target_id: target_id.clone(),
|
||||
kind,
|
||||
strength,
|
||||
};
|
||||
self.edges.entry(source_id).or_default().push(relation.clone());
|
||||
self.edges.entry(target_id).or_default().push(relation);
|
||||
}
|
||||
|
||||
/// 获取与指定想法相关的所有关系
|
||||
pub fn get_relations(&self, idea_id: &IdeaId) -> Vec<&Relation> {
|
||||
self.edges.get(idea_id).map(|r| r.iter().collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 查找相似想法
|
||||
pub fn find_similar(&self, idea_id: &IdeaId) -> Vec<&Relation> {
|
||||
self.get_relations(idea_id)
|
||||
.into_iter()
|
||||
.filter(|r| r.kind == RelationKind::Similar)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// TODO: 基于向量相似度的自动关联发现
|
||||
// TODO: 图遍历、聚类算法
|
||||
}
|
||||
|
||||
impl Default for IdeaGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
//! df-ideas: 想法池 — 捕获、评估、评分、关联图、晋升
|
||||
//! df-ideas: 想法池 — 捕获、评估、评分、晋升
|
||||
|
||||
pub mod adversarial;
|
||||
pub mod capture;
|
||||
pub mod evaluator;
|
||||
pub mod graph;
|
||||
pub mod promotion;
|
||||
pub mod scoring;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
//! 想法晋升 — 将想法转为项目
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
|
||||
use df_core::types::{IdeaId, ProjectId};
|
||||
|
||||
use crate::adversarial::Recommendation;
|
||||
use crate::capture::Idea;
|
||||
use crate::evaluator::Recommendation;
|
||||
|
||||
/// 晋升结果
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PromotionResult {
|
||||
pub idea_id: IdeaId,
|
||||
pub project_id: ProjectId,
|
||||
@@ -44,7 +45,7 @@ impl IdeaPromoter {
|
||||
pub fn try_promote(&self, idea: &Idea, recommendation: &Recommendation) -> Result<PromotionResult> {
|
||||
match self.policy {
|
||||
PromotionPolicy::Auto => {
|
||||
if matches!(recommendation, Recommendation::StrongApprove | Recommendation::Approve) {
|
||||
if matches!(recommendation, Recommendation::ImmediateAction | Recommendation::Soon) {
|
||||
self.do_promote(idea)
|
||||
} else {
|
||||
Ok(PromotionResult {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
//! 评分引擎 — 多维度加权评分
|
||||
//! 评分引擎 — 基于想法内容的多维度启发式评分
|
||||
//!
|
||||
//! 各维度分数均为 0-10(IPC 层会 *10 缩放为 0-100 以匹配前端)。
|
||||
//! 启发式依据:优先级、描述充实度、标签、关键词信号——保证稳定且有区分度。
|
||||
//! TODO: 接入 AI 做语义级深度评分。
|
||||
|
||||
use crate::capture::Idea;
|
||||
|
||||
/// 想法评分详情(重新导出 capture 模块中的定义)
|
||||
pub use crate::capture::IdeaScores;
|
||||
|
||||
use df_core::types::Priority;
|
||||
|
||||
/// 评分权重配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScoringWeights {
|
||||
@@ -28,17 +34,12 @@ pub struct ScoringEngine;
|
||||
|
||||
impl ScoringEngine {
|
||||
/// 使用默认权重计算评分
|
||||
///
|
||||
/// TODO: 接入 AI 进行深度评分,当前返回基于启发式的分数
|
||||
pub fn compute_default(idea: &Idea) -> IdeaScores {
|
||||
let weights = ScoringWeights::default();
|
||||
Self::compute(idea, &weights)
|
||||
Self::compute(idea, &ScoringWeights::default())
|
||||
}
|
||||
|
||||
/// 使用指定权重计算评分
|
||||
pub fn compute(idea: &Idea, weights: &ScoringWeights) -> IdeaScores {
|
||||
// TODO: 基于想法内容、历史数据、AI 分析等多维度评分
|
||||
// 当前使用基于启发式的占位评分
|
||||
let feasibility = Self::heuristic_feasibility(idea);
|
||||
let impact = Self::heuristic_impact(idea);
|
||||
let urgency = Self::heuristic_urgency(idea);
|
||||
@@ -55,21 +56,181 @@ impl ScoringEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// 启发式可行性评分
|
||||
fn heuristic_feasibility(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于描述复杂度、资源需求等评估
|
||||
5.0
|
||||
/// 启发式可行性评分(描述充实度 + 技术/资源信号词)
|
||||
fn heuristic_feasibility(idea: &Idea) -> f64 {
|
||||
let mut score = 5.0_f64;
|
||||
let desc = idea.description.trim();
|
||||
if !desc.is_empty() {
|
||||
score += 1.5;
|
||||
}
|
||||
let len = desc.chars().count();
|
||||
if (50..=500).contains(&len) {
|
||||
score += 1.0;
|
||||
} else if len > 500 {
|
||||
// 过长描述通常意味着实现复杂度上升
|
||||
score -= 0.5;
|
||||
}
|
||||
// 可行性正向信号
|
||||
let pos = count_any(desc, &[
|
||||
"复用", "已有", "简单", "集成", "支持", "成熟", "基于", "现成", "脚手架", "模板",
|
||||
]);
|
||||
score += (pos as f64) * 0.5;
|
||||
// 复杂度负向信号
|
||||
let neg = count_any(desc, &[
|
||||
"重构", "迁移", "大规模", "分布式", "重写", "从零", "全新架构", "高并发", "底层",
|
||||
]);
|
||||
score -= (neg as f64) * 0.6;
|
||||
score.clamp(0.0, 10.0)
|
||||
}
|
||||
|
||||
/// 启发式影响力评分
|
||||
fn heuristic_impact(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于业务价值、用户影响等评估
|
||||
5.0
|
||||
/// 启发式影响力评分(优先级 + 价值信号词 + 标签广度)
|
||||
fn heuristic_impact(idea: &Idea) -> f64 {
|
||||
let mut score = match idea.priority {
|
||||
Priority::Critical => 8.0,
|
||||
Priority::High => 6.5,
|
||||
Priority::Medium => 5.0,
|
||||
Priority::Low => 3.5,
|
||||
};
|
||||
if !idea.tags.is_empty() {
|
||||
score += 0.5;
|
||||
// 标签越多影响面越广,上限 +1.0
|
||||
score += (idea.tags.len().min(4) as f64) * 0.25;
|
||||
}
|
||||
let desc = idea.description.trim();
|
||||
let value_hits = count_any(desc, &[
|
||||
"用户", "增长", "收入", "效率", "体验", "核心", "关键", "痛点", "竞品", "留存",
|
||||
]);
|
||||
score += (value_hits as f64) * 0.5;
|
||||
if desc.chars().count() > 100 {
|
||||
score += 0.5;
|
||||
}
|
||||
score.clamp(0.0, 10.0)
|
||||
}
|
||||
|
||||
/// 启发式紧急度评分
|
||||
fn heuristic_urgency(_idea: &Idea) -> f64 {
|
||||
// TODO: 基于优先级、时间窗口等评估
|
||||
5.0
|
||||
/// 启发式紧急度评分(优先级 + 时效信号词)
|
||||
fn heuristic_urgency(idea: &Idea) -> f64 {
|
||||
let mut score = match idea.priority {
|
||||
Priority::Critical => 9.0,
|
||||
Priority::High => 7.0,
|
||||
Priority::Medium => 5.0,
|
||||
Priority::Low => 3.0,
|
||||
};
|
||||
let desc = idea.description.trim();
|
||||
let time_hits = count_any(desc, &[
|
||||
"立即", "马上", "紧急", "尽快", "本周", "上线", "deadline", "截止", "先行", "阻塞",
|
||||
]);
|
||||
score += (time_hits as f64) * 0.5;
|
||||
score.clamp(0.0, 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 统计 text 中命中任一关键词的数量(小写匹配,兼顾中英文)
|
||||
/// 局限:纯子串匹配,不识别"不复用""无用户增长"等否定前缀,接 LLM 后由语义层修正
|
||||
fn count_any(text: &str, keywords: &[&str]) -> usize {
|
||||
let lower = text.to_lowercase();
|
||||
keywords.iter().filter(|kw| lower.contains(*kw)).count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::Idea;
|
||||
use df_core::types::{IdeaStatus, Priority};
|
||||
|
||||
/// 辅助工厂:构造测试用 Idea(时间/ID 用默认值,不影响评分)
|
||||
fn make_idea(title: &str, desc: &str, priority: Priority, tags: Vec<&str>) -> Idea {
|
||||
Idea {
|
||||
id: "test-id".to_string(),
|
||||
title: title.to_string(),
|
||||
description: desc.to_string(),
|
||||
status: IdeaStatus::Draft,
|
||||
priority,
|
||||
scores: None,
|
||||
tags: tags.into_iter().map(String::from).collect(),
|
||||
source: None,
|
||||
related_ids: Vec::new(),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s1_empty_idea_baseline() {
|
||||
let idea = make_idea("测试想法", "", Priority::Medium, vec![]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s1] 空想法 (Medium / 无描述 / 无标签)");
|
||||
println!(" 可行性={:.2} 影响力={:.2} 紧急度={:.2} 综合={:.2}", s.feasibility, s.impact, s.urgency, s.overall);
|
||||
assert!((s.overall - 5.0).abs() < 0.01, "空想法 overall 应为 5.0, 实际 {:.2}", s.overall);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s2_high_priority_value_desc() {
|
||||
let desc = "面向用户的核心功能,带来显著增长,大幅提升效率。集成成熟方案,复用已有组件,快速交付价值。".repeat(3);
|
||||
let idea = make_idea("增长引擎", &desc, Priority::Critical, vec!["增长", "核心"]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s2] 高优先级 + 价值描述 (Critical / ~120字 / 含价值词)");
|
||||
println!(" 可行性={:.2} 影响力={:.2} 紧急度={:.2} 综合={:.2}", s.feasibility, s.impact, s.urgency, s.overall);
|
||||
assert!(s.impact >= 7.0, "impact 应≥7.0, 实际 {:.2}", s.impact);
|
||||
assert!(s.urgency >= 8.0, "urgency 应≥8.0, 实际 {:.2}", s.urgency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_low_priority_short_desc() {
|
||||
let idea = make_idea("小优化", "一句话", Priority::Low, vec![]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s3] 低优先级 + 短描述 (Low / 3字)");
|
||||
println!(" 可行性={:.2} 影响力={:.2} 紧急度={:.2} 综合={:.2}", s.feasibility, s.impact, s.urgency, s.overall);
|
||||
assert!((s.overall - 4.6).abs() < 0.01, "overall 应为 4.6, 实际 {:.2}", s.overall);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s4_feasibility_positive_signals() {
|
||||
let idea = make_idea("复用方案", "复用已有组件,简单集成现成脚手架", Priority::Medium, vec![]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s4] 可行性正向信号 (含 复用/已有/简单/集成/现成/脚手架)");
|
||||
println!(" 可行性={:.2} (预期 9.5)", s.feasibility);
|
||||
assert!((s.feasibility - 9.5).abs() < 0.01, "正向信号 feasibility 应为 9.5, 实际 {:.2}", s.feasibility);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s5_feasibility_negative_signals() {
|
||||
let idea = make_idea("大重构", "大规模重构迁移,分布式重写从零开始", Priority::Medium, vec![]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s5] 可行性负向信号 (含 大规模/重构/迁移/分布式/重写/从零)");
|
||||
println!(" 可行性={:.2} (预期 ≤5.0)", s.feasibility);
|
||||
assert!(s.feasibility <= 5.0, "负向信号 feasibility 应≤5.0, 实际 {:.2}", s.feasibility);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s6_custom_weights() {
|
||||
let idea = make_idea("高可行低影响", "复用已有简单集成现成", Priority::Low, vec![]);
|
||||
let custom = ScoringWeights { feasibility: 0.7, impact: 0.2, urgency: 0.1 };
|
||||
let s_custom = ScoringEngine::compute(&idea, &custom);
|
||||
let s_default = ScoringEngine::compute_default(&idea);
|
||||
let manual = s_custom.feasibility * 0.7 + s_custom.impact * 0.2 + s_custom.urgency * 0.1;
|
||||
println!("\n[s6] 自定义权重 (feas:0.7 / impact:0.2 / urg:0.1)");
|
||||
println!(" 自定义综合={:.2} 默认综合={:.2} 手算加权={:.2}", s_custom.overall, s_default.overall, manual);
|
||||
assert!((s_custom.overall - manual).abs() < 0.01, "overall 应等于手算加权");
|
||||
assert!(s_custom.overall > s_default.overall, "高 feas 配高权重应让综合更高");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s7_clamp_upper_bound() {
|
||||
let desc = "复用已有简单集成现成成熟基于脚手架模板支持".repeat(20);
|
||||
let idea = make_idea("满分想法", &desc, Priority::Critical, vec!["a", "b", "c", "d"]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s7] clamp 上限 (堆正向词 + 超长描述 + Critical)");
|
||||
println!(" 可行性={:.2} 影响力={:.2} 紧急度={:.2} 综合={:.2}", s.feasibility, s.impact, s.urgency, s.overall);
|
||||
assert!(s.feasibility <= 10.0 && s.impact <= 10.0 && s.urgency <= 10.0, "所有维度应≤10");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s8_clamp_lower_bound() {
|
||||
let desc = "重构迁移大规模分布式重写从零全新架构高并发底层".repeat(20);
|
||||
let idea = make_idea("灾难想法", &desc, Priority::Low, vec![]);
|
||||
let s = ScoringEngine::compute_default(&idea);
|
||||
println!("\n[s8] clamp 下限 (堆负向词 + Low)");
|
||||
println!(" 可行性={:.2} 影响力={:.2} 紧急度={:.2} 综合={:.2}", s.feasibility, s.impact, s.urgency, s.overall);
|
||||
assert!(s.feasibility >= 0.0 && s.impact >= 0.0 && s.urgency >= 0.0, "所有维度应≥0");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user