feat(flywheel): 阶段三执行质量预测层(trailer 死链修复 + 风险画像) #2
@ -175,8 +175,9 @@
|
|||||||
- files: src-tauri/src/commands/risk.rs, src-tauri/src/commands/mod.rs, src-tauri/src/mcp_tools.rs
|
- 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 断言"
|
- 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 数据段
|
### ✅ P3-D. 梦核 prompt 高风险 scope 数据段
|
||||||
- status: todo
|
- 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
|
- complexity: S
|
||||||
- depends: flywheel-intelligence(卡 P3-C)
|
- depends: flywheel-intelligence(卡 P3-C)
|
||||||
- files: src-tauri/src/commands/blueprint.rs
|
- files: src-tauri/src/commands/blueprint.rs
|
||||||
|
|||||||
@ -1964,9 +1964,9 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
|
|||||||
let notes_total = stats.project_notes.len();
|
let notes_total = stats.project_notes.len();
|
||||||
|
|
||||||
// ── 规则命中 × 返工交叉(Rules-Applied trailer,飞轮 R1)─────────────────
|
// ── 规则命中 × 返工交叉(Rules-Applied trailer,飞轮 R1)─────────────────
|
||||||
let rule_hits_summary = crate::db::pool()
|
// try_pool:测试环境 pool 未初始化时走占位文案,不 panic
|
||||||
.get()
|
let rule_hits_summary = crate::db::try_pool()
|
||||||
.ok()
|
.and_then(|p| p.get().ok())
|
||||||
.and_then(|conn| crate::commands::commit_metrics::rule_hit_stats(&conn).ok())
|
.and_then(|conn| crate::commands::commit_metrics::rule_hit_stats(&conn).ok())
|
||||||
.filter(|v| !v.is_empty())
|
.filter(|v| !v.is_empty())
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
@ -1980,6 +1980,22 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| "(暂无数据——commit message 附带 Rules-Applied trailer 后自动积累)".to_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)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。
|
format!(r#"你是炼境(Dev Manager)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。
|
||||||
|
|
||||||
## 飞轮运转数据(来自炼境跨项目聚合)
|
## 飞轮运转数据(来自炼境跨项目聚合)
|
||||||
@ -2027,6 +2043,13 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
|
|||||||
|
|
||||||
{rule_hits_summary}
|
{rule_hits_summary}
|
||||||
|
|
||||||
|
### 跨项目高风险 scope(飞轮阶段三预测层)
|
||||||
|
|
||||||
|
> 历史返工率最高的模块热区。反复返工的 scope = 结构性脆弱点,
|
||||||
|
> 是「固化」环节输出预防性规范、或提示该模块需要重构/补测试的信号。
|
||||||
|
|
||||||
|
{high_risk_summary}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 当前 CONVENTIONS.md(v{conventions_version})规则摘要
|
## 当前 CONVENTIONS.md(v{conventions_version})规则摘要
|
||||||
@ -2731,5 +2754,38 @@ mod tests {
|
|||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
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("暂无数据"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,62 @@ fn file_risks(conn: &Connection, project_id: &str, files: &[String]) -> Result<V
|
|||||||
Ok(result)
|
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 和/或文件清单预测任务风险。
|
/// 核心入口:按 scope 和/或文件清单预测任务风险。
|
||||||
pub fn predict_task_risk(
|
pub fn predict_task_risk(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
@ -280,6 +336,27 @@ mod tests {
|
|||||||
assert_eq!(r.risk_level, "high", "hot.rs 返工率 0.5 应分级 high");
|
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]
|
#[test]
|
||||||
fn requires_scope_or_files() {
|
fn requires_scope_or_files() {
|
||||||
let conn = crate::db::conn_with_schema();
|
let conn = crate::db::conn_with_schema();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user