feat(flywheel): 梦核 prompt 注入跨项目高风险 scope 数据段

- risk.rs 新增 high_risk_scopes:样本 ≥5 且有非 CI 返工的 scope
  按返工率降序 top 10,项目名 LEFT JOIN project_profiles(缺失回退 id)
- generate_muhe_prompt 新增「跨项目高风险 scope」表格段,附分析指引
  (反复返工 scope = 结构性脆弱点 → 固化环节输出预防性规范)
- rule_hits 段 pool() 改 try_pool():原实现测试环境必 panic,
  generate_muhe_prompt 因此不可测;改后两段在无 DB 时走占位文案
- prompt 构建测试断言两个数据段存在 + 聚合排序/最小样本过滤测试,92 全绿

Rules-Applied: R06,R07
This commit is contained in:
lanrtop 2026-07-04 14:18:04 +09:00
parent 330ddc24dd
commit 66dc6209a8
3 changed files with 139 additions and 5 deletions

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 解析缺失回退 idgenerate_muhe_prompt 注入「跨项目高风险 scope」表格段含分析指引反复返工 scope=结构性脆弱点→固化环节输出预防性规范);顺手把 rule_hits 段的 pool() 改 try_pool()(原实现测试环境必 panicgenerate_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.mdv{conventions_version}
@ -2731,5 +2754,38 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
// ── generate_muhe_promptP3-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/hot4 commits 2 fixrate 0.5proj-b/warm5 commits 1 fixrate 0.2
// proj-a/tiny2 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();