diff --git a/.blueprint/modules/flywheel-intelligence.md b/.blueprint/modules/flywheel-intelligence.md index ac7d216..ebb68c9 100644 --- a/.blueprint/modules/flywheel-intelligence.md +++ b/.blueprint/modules/flywheel-intelligence.md @@ -191,6 +191,13 @@ - files: src/lib/commands.ts, src/components/insight/FlywheelPanel.tsx, src-tauri/src/commands/risk.rs, src-tauri/src/lib.rs - acceptance: "[pnpm typecheck] exit 0;FlywheelPanel 新增跨项目返工热区展示(get_high_risk_scopes 封装同步,R01)" +### 🔵 P3-F. ingest_git_history agent 按钮(全量导入双投影) +- status: in_progress +- complexity: S +- files: src-tauri/src/mcp_tools.rs +- acceptance: "[MCP tool] ingest_git_history() 不传参对所有 win_path 项目执行 ingest_full_git_history,返回逐项目 imported/skipped 汇总;传 project_id 只跑单项目;tools_list_result 含断言;[消费者验证] 实调后 rules_applied 非空行 ≥12(存量 trailer 回填生效)" +- notes: 消费者验证暴露的死角——「全量导入」只有 UI 投影,agent 够不到;按双投影原则补 MCP 投影,能力本体(ingest_full_git_history)不动 + ## 复盘笔记 【复盘】阶段三执行质量预测层 P3-A~E(2026-07-04) diff --git a/src-tauri/src/mcp_tools.rs b/src-tauri/src/mcp_tools.rs index 04345b2..ee16c45 100644 --- a/src-tauri/src/mcp_tools.rs +++ b/src-tauri/src/mcp_tools.rs @@ -246,6 +246,17 @@ pub fn tools_list_result() -> Value { "description": "扫描已登记项目的父目录,找出存在于文件系统但尚未在炼境注册的项目目录。返回每个未注册目录的路径、是否有 .git、技术栈特征文件(package.json / Cargo.toml / go.mod)、是否有 AGENTS.md。用于日常发现漏网项目并决定是否接入炼境。", "inputSchema": { "type": "object", "properties": {}, "required": [] } }, + { + "name": "ingest_git_history", + "description": "全量导入 git 历史到飞轮(等价 UI「全量导入」按钮的 agent 投影):解析最近 500 条 commit 的 conventional 指标、Rules-Applied trailer 与触碰文件,UPSERT 幂等(存量行自动回填 trailer)。不传 project_id 时对所有已登记 Windows 路径项目执行。适用:trailer 存量回填、新项目接入后初始化飞轮数据。", + "inputSchema": { + "type": "object", + "properties": { + "project_id": { "type": "string", "description": "可选。项目 workspace ID;省略则遍历所有已登记项目" } + }, + "required": [] + } + }, { "name": "predict_task_risk", "description": "任务风险预测(飞轮阶段三):按 scope 和/或文件清单查询该项目历史返工率,返回 risk_level(low/medium/high)、样本量与置信度、热点文件排行。agent 开工前调用——任务卡涉及高返工 scope/文件时提高警惕(多写测试、小步提交)。样本 <5 或 commit 历史不规范时 confidence=low,结论仅供参考。", @@ -475,6 +486,52 @@ pub async fn tools_call(group_id: &str, params: Option<&Value>) -> Value { } "list_all_projects" => list_all_projects(), "scan_unregistered_projects" => scan_unregistered_projects(), + "ingest_git_history" => { + let pid = args.get("project_id").and_then(|v| v.as_str()); + let conn = db::pool().get().map_err(|e| e.to_string())?; + let targets: Vec<(String, String)> = { + let mut stmt = conn + .prepare( + "SELECT id, win_path FROM project_workspaces + WHERE win_path IS NOT NULL AND (?1 IS NULL OR id = ?1)", + ) + .map_err(|e| e.to_string())?; + let rows = stmt + .query_map(rusqlite::params![pid], |r| Ok((r.get(0)?, r.get(1)?))) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + rows + }; + drop(conn); + if targets.is_empty() { + Err(format!("未找到可导入的项目(project_id={:?})", pid)) + } else { + let mut lines = Vec::new(); + let (mut ok, mut failed) = (0u32, 0u32); + for (id, path) in &targets { + match crate::commands::commit_metrics::ingest_full_git_history( + id.clone(), + path.clone(), + ) { + Ok(r) => { + ok += 1; + lines.push(format!("- {}: 写入 {} 条,跳过 {} 条", id, r.imported, r.skipped)); + } + Err(e) => { + failed += 1; + lines.push(format!("- {}: ❌ {}", id, e)); + } + } + } + Ok(format!( + "全量导入完成:{} 个项目成功,{} 个失败\n\n{}", + ok, + failed, + lines.join("\n") + )) + } + } "predict_task_risk" => { let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or(""); let scope = args.get("scope").and_then(|v| v.as_str()); @@ -1069,6 +1126,7 @@ mod tests { assert!(names.contains(&"create_pull_request"), "缺少 create_pull_request"); assert!(names.contains(&"poll_now"), "缺少 poll_now"); assert!(names.contains(&"predict_task_risk"), "缺少 predict_task_risk"); + assert!(names.contains(&"ingest_git_history"), "缺少 ingest_git_history"); // 每个工具都有 description 和 inputSchema for tool in tools { let name = tool["name"].as_str().unwrap_or("?");