Files
DevFlow/crates/df-ideas/src/scoring.rs

237 lines
9.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 评分引擎 — 基于想法内容的多维度启发式评分
//!
//! 各维度分数均为 0-10IPC 层会 *10 缩放为 0-100 以匹配前端)。
//! 启发式依据:优先级、描述充实度、标签、关键词信号——保证稳定且有区分度。
//! TODO: 接入 AI 做语义级深度评分。
use crate::capture::Idea;
/// 想法评分详情(重新导出 capture 模块中的定义)
pub use crate::capture::IdeaScores;
use df_types::types::Priority;
/// 评分权重配置
#[derive(Debug, Clone)]
pub struct ScoringWeights {
pub feasibility: f64,
pub impact: f64,
pub urgency: f64,
}
impl Default for ScoringWeights {
fn default() -> Self {
Self {
feasibility: 0.4,
impact: 0.4,
urgency: 0.2,
}
}
}
/// 评分引擎
pub struct ScoringEngine;
impl ScoringEngine {
/// 使用默认权重计算评分
pub fn compute_default(idea: &Idea) -> IdeaScores {
Self::compute(idea, &ScoringWeights::default())
}
/// 使用指定权重计算评分
pub fn compute(idea: &Idea, weights: &ScoringWeights) -> IdeaScores {
let feasibility = Self::heuristic_feasibility(idea);
let impact = Self::heuristic_impact(idea);
let urgency = Self::heuristic_urgency(idea);
let overall = feasibility * weights.feasibility
+ impact * weights.impact
+ urgency * weights.urgency;
IdeaScores {
feasibility,
impact,
urgency,
overall,
}
}
/// 启发式可行性评分(描述充实度 + 技术/资源信号词)
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 {
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 {
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_types::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");
}
}