feat(flywheel): 阶段三执行质量预测层(trailer 死链修复 + 风险画像) #2

Merged
lanrtop merged 14 commits from feat/flywheel-intelligence into master 2026-07-04 14:45:28 +08:00
3 changed files with 139 additions and 5 deletions
Showing only changes of commit 66dc6209a8 - Show all commits

View File

@ -175,8 +175,9 @@
- files: src-tauri/src/commands/risk.rs, src-tauri/src/commands/mod.rs, src-tauri/src/mcp_tools.rs
- acceptance: "[MCP tool] predict_task_risk(project_id, scope 或 files) → JSON 含 risk_level/rework_rate/sample_size/confidence/hot_files,无 error 字段;sample_size<5 时 confidence=low;[cargo test] 风险聚合纯函数(conn 注入 + in-memory DB)覆盖高返工 scope、小样本、脏历史(conventional 率低标不可信)三种输入;tools_list_result 含 predict_task_risk 断言"
### 📋 P3-D. 梦核 prompt 高风险 scope 数据段
- status: todo
### ✅ P3-D. 梦核 prompt 高风险 scope 数据段
- status: done
- done_note: 2026-07-04 完成。risk.rs 新增 high_risk_scopes(样本 ≥5 且有非 CI 返工,返工率降序,项目名从 project_profiles 解析缺失回退 id);generate_muhe_prompt 注入「跨项目高风险 scope」表格段(含分析指引:反复返工 scope=结构性脆弱点→固化环节输出预防性规范);顺手把 rule_hits 段的 pool() 改 try_pool()(原实现测试环境必 panic,generate_muhe_prompt 因此不可测);prompt 构建测试 + 聚合排序过滤测试,92 全绿
- complexity: S
- depends: flywheel-intelligence(卡 P3-C)
- files: src-tauri/src/commands/blueprint.rs

View File

@ -1964,9 +1964,9 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
let notes_total = stats.project_notes.len();
// ── 规则命中 × 返工交叉(Rules-Applied trailer,飞轮 R1)─────────────────
let rule_hits_summary = crate::db::pool()
.get()
.ok()
// try_pool:测试环境 pool 未初始化时走占位文案,不 panic
let rule_hits_summary = crate::db::try_pool()
.and_then(|p| p.get().ok())
.and_then(|conn| crate::commands::commit_metrics::rule_hit_stats(&conn).ok())
.filter(|v| !v.is_empty())
.map(|v| {
@ -1980,6 +1980,22 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
})
.unwrap_or_else(|| "(暂无数据——commit message 附带 Rules-Applied trailer 后自动积累)".to_string());
// ── 跨项目高风险 scope(飞轮阶段三预测层,样本 ≥5 且有返工)─────────────
let high_risk_summary = crate::db::try_pool()
.and_then(|p| p.get().ok())
.and_then(|conn| crate::commands::risk::high_risk_scopes(&conn, 5, 10).ok())
.filter(|v| !v.is_empty())
.map(|v| {
let header = "| 项目 | scope | commit 数 | 返工数 | 返工率 |";
let sep = "|------|-------|-----------|--------|--------|";
let rows: Vec<String> = v.iter().map(|s| {
format!("| {} | {} | {} | {} | {:.0}% |",
s.project_name, s.scope, s.commits, s.rework, s.rework_rate * 100.0)
}).collect();
format!("{header}\n{sep}\n{}", rows.join("\n"))
})
.unwrap_or_else(|| "(暂无数据——需 scope 样本 ≥5 且存在非 CI 返工)".to_string());
format!(r#"你是炼境(Dev Manager)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。
## 飞轮运转数据(来自炼境跨项目聚合)
@ -2027,6 +2043,13 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
{rule_hits_summary}
### 跨项目高风险 scope(飞轮阶段三预测层)
> 历史返工率最高的模块热区。反复返工的 scope = 结构性脆弱点,
> 是「固化」环节输出预防性规范、或提示该模块需要重构/补测试的信号。
{high_risk_summary}
---
## 当前 CONVENTIONS.md(v{conventions_version})规则摘要
@ -2731,5 +2754,38 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
// ── generate_muhe_prompt(P3-D 梦核数据段)───────────────────────────────
#[test]
fn muhe_prompt_contains_high_risk_scope_section() {
let stats = FlywheelStats {
cohorts: vec![],
top_blocked_reasons: vec![],
stalled_projects: vec![],
ai_doc_stale_projects: vec![],
iteration_jumps: vec![],
stalled_modules: vec![],
total_projects_scanned: 0,
projects_with_data: 0,
project_notes: vec![],
execution_quality: ExecutionQuality {
complexity_dist: Default::default(),
total_rework_count: 0,
avg_rounds_m: None,
},
};
let prompt = generate_muhe_prompt(stats);
assert!(
prompt.contains("### 跨项目高风险 scope"),
"梦核 prompt 应含高风险 scope 数据段"
);
assert!(
prompt.contains("### 规则命中 × 返工交叉"),
"规则置信度数据段应保留"
);
// 测试环境 pool 未初始化 → 两段都应走占位文案而非 panic
assert!(prompt.contains("暂无数据"));
}
}

View File

@ -111,6 +111,62 @@ fn file_risks(conn: &Connection, project_id: &str, files: &[String]) -> Result<V
Ok(result)
}
/// 跨项目高风险 scope(梦核数据段)。
#[derive(Debug, Serialize)]
pub struct HighRiskScope {
pub project_name: String,
pub scope: String,
pub commits: u32,
pub rework: u32,
pub rework_rate: f64,
}
/// 跨项目聚合:样本 ≥ min_commits 且至少 1 次返工的 scope,按返工率降序取 top limit。
/// 项目名从 project_profiles 解析,登记缺失时回退 project_id。
pub fn high_risk_scopes(
conn: &Connection,
min_commits: u32,
limit: usize,
) -> Result<Vec<HighRiskScope>, String> {
let mut stmt = conn
.prepare(
"SELECT COALESCE(pp.name, cm.project_id) AS pname, cm.scope,
COUNT(*) AS c,
COALESCE(SUM(CASE WHEN cm.is_rework = 1 AND cm.is_ci_auto = 0 THEN 1 ELSE 0 END), 0) AS rw
FROM commit_metrics cm
LEFT JOIN project_workspaces pw ON pw.id = cm.project_id
LEFT JOIN project_profiles pp ON pp.id = pw.profile_id
WHERE cm.scope IS NOT NULL
GROUP BY cm.project_id, cm.scope
HAVING c >= ?1 AND rw > 0
ORDER BY CAST(rw AS REAL) / c DESC, c DESC
LIMIT ?2",
)
.map_err(|e| e.to_string())?;
let rows = stmt
.query_map(rusqlite::params![min_commits, limit as i64], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, String>(1)?,
r.get::<_, u32>(2)?,
r.get::<_, u32>(3)?,
))
})
.map_err(|e| e.to_string())?;
let mut result = Vec::new();
for row in rows {
let (project_name, scope, commits, rework) = row.map_err(|e| e.to_string())?;
result.push(HighRiskScope {
project_name,
scope,
rework_rate: rate(rework, commits),
commits,
rework,
});
}
Ok(result)
}
/// 核心入口:按 scope 和/或文件清单预测任务风险。
pub fn predict_task_risk(
conn: &Connection,
@ -280,6 +336,27 @@ mod tests {
assert_eq!(r.risk_level, "high", "hot.rs 返工率 0.5 应分级 high");
}
#[test]
fn high_risk_scopes_orders_by_rate_with_min_sample() {
let conn = crate::db::conn_with_schema();
// proj-a/hot:4 commits 2 fix(rate 0.5);proj-b/warm:5 commits 1 fix(rate 0.2)
// proj-a/tiny:2 commits 1 fix——低于 min_commits=3 应被过滤
for (sha, subj) in [
("a1", "fix(hot): x"), ("a2", "fix(hot): y"), ("a3", "feat(hot): z"), ("a4", "chore(hot): w"),
("b1", "fix(warm): x"), ("b2", "feat(warm): a"), ("b3", "feat(warm): b"),
("b4", "feat(warm): c"), ("b5", "feat(warm): d"),
("t1", "fix(tiny): x"), ("t2", "feat(tiny): y"),
] {
let pid = if sha.starts_with('a') || sha.starts_with('t') { "proj-a" } else { "proj-b" };
seed(&conn, pid, sha, subj, &[]);
}
let top = high_risk_scopes(&conn, 3, 10).unwrap();
assert_eq!(top.len(), 2, "tiny 样本不足应被过滤");
assert_eq!(top[0].scope, "hot", "返工率高的排前");
assert_eq!(top[0].project_name, "proj-a", "无登记项目回退 project_id");
assert_eq!(top[1].scope, "warm");
}
#[test]
fn requires_scope_or_files() {
let conn = crate::db::conn_with_schema();