From d41e0f170773f6d52dc7323d3cc33bd7005460a1 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Mon, 29 Jun 2026 21:21:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(flywheel):=20=E4=BF=AE=E5=A4=8D=20commit=5F?= =?UTF-8?q?metrics=20project=5Fid=20=E5=8F=8C=E8=B7=AF=E5=BE=84=E5=86=B2?= =?UTF-8?q?=E7=AA=81=EF=BC=8C=E8=A1=A8=E6=A0=BC=E8=83=BD=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=89=80=E6=9C=89=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blueprint_buffs 用 UUID 写 commit_metrics,全量导入用名称字符串写, 同一 sha 在 sha PRIMARY KEY 约束下只能存一条,导致全量导入被 INSERT OR IGNORE 全部跳过, 表格无法查到 UUID 格式的历史数据。 修复点: - ingest_full_git_history 自动查 blueprint_buffs,若该路径已有 UUID project_id 则优先用 UUID - 新增 get_all_commit_health 命令,SQL 关联 blueprint_buffs / projects 解析名称,不需要前端传 project_ids - FlywheelPanel 切换到 getAllCommitHealth,表格直接用后端返回的 project_name,无需前端 lookup --- src-tauri/src/commands/commit_metrics.rs | 104 +++++++++++++++++++- src-tauri/src/lib.rs | 1 + src/components/dashboard/BlueprintModal.tsx | 18 ++-- src/lib/commands.ts | 7 ++ 4 files changed, 118 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/commit_metrics.rs b/src-tauri/src/commands/commit_metrics.rs index fd29ef9..cc16689 100644 --- a/src-tauri/src/commands/commit_metrics.rs +++ b/src-tauri/src/commands/commit_metrics.rs @@ -193,6 +193,7 @@ fn read_git_log(root: &std::path::Path, limit: usize) -> Result(0), + ) + .unwrap_or(project_id); + let tx = conn.unchecked_transaction().map_err(|e| e.to_string())?; let mut imported = 0u32; for (sha, subject, date) in &commits { @@ -215,7 +226,7 @@ pub fn ingest_full_git_history( (sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", rusqlite::params![ - sha, project_id, p.commit_type, p.scope, + sha, effective_id, p.commit_type, p.scope, p.is_rework as i32, p.is_ci_auto as i32, if date.is_empty() { None } else { Some(date.as_str()) }, ], @@ -334,6 +345,97 @@ pub fn get_project_commit_health( project_commit_health_inner(&conn, &project_ids) } +/// commit_metrics 中所有项目的健康度(含项目名解析)。 +#[derive(Debug, Serialize)] +pub struct CommitHealthWithName { + pub project_id: String, + /// 解析出的项目名:优先从 projects 表直接匹配,其次通过 blueprint_buffs 路径关联 projects,再次用路径末段 + pub project_name: String, + pub total_commits: u32, + pub conventional_commits: u32, + pub conventional_rate: f64, + pub rework_commits: u32, + pub rework_rate: f64, + pub top_scopes: Vec, +} + +/// 查询 commit_metrics 里所有有数据的项目,自动关联名称。无需前端传 project_ids。 +#[tauri::command] +pub fn get_all_commit_health() -> Result, String> { + let conn = crate::db::pool().get().map_err(|e| e.to_string())?; + let round3 = |v: f64| (v * 1000.0).round() / 1000.0; + + // 聚合各 project_id 的指标,并通过两条路径解析名称 + let mut stmt = conn.prepare( + "SELECT + cm.project_id, + COALESCE(p_direct.name, p_via_buff.name, bb.project_path, cm.project_id) AS name, + COUNT(*) AS total, + SUM(CASE WHEN cm.commit_type IS NOT NULL THEN 1 ELSE 0 END) AS conventional, + SUM(CASE WHEN cm.is_rework=1 AND cm.is_ci_auto=0 THEN 1 ELSE 0 END) AS rework + FROM commit_metrics cm + LEFT JOIN projects p_direct ON p_direct.id = cm.project_id + LEFT JOIN blueprint_buffs bb ON bb.project_id = cm.project_id + LEFT JOIN projects p_via_buff + ON p_via_buff.win_path = bb.project_path + OR p_via_buff.wsl_path = bb.project_path + GROUP BY cm.project_id + ORDER BY total DESC", + ).map_err(|e| e.to_string())?; + + let rows: Vec<(String, String, u32, u32, u32)> = stmt + .query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)? as u32, + row.get::<_, i64>(3)? as u32, + row.get::<_, i64>(4)? as u32, + )) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + + let mut result = Vec::with_capacity(rows.len()); + for (pid, name, total, conventional, rework) in rows { + // 查 top_scopes + let mut scope_stmt = conn.prepare( + "SELECT scope, + COUNT(*) AS cnt, + SUM(CASE WHEN is_rework=1 AND is_ci_auto=0 THEN 1 ELSE 0 END) AS rc + FROM commit_metrics + WHERE project_id = ?1 AND scope IS NOT NULL + GROUP BY scope + ORDER BY cnt DESC + LIMIT 5", + ).map_err(|e| e.to_string())?; + let top_scopes: Vec = scope_stmt + .query_map(rusqlite::params![pid], |row| { + Ok(ScopeActivity { + scope: row.get(0)?, + commit_count: row.get::<_, i64>(1)? as u32, + rework_count: row.get::<_, i64>(2)? as u32, + }) + }) + .map_err(|e| e.to_string())? + .filter_map(|r| r.ok()) + .collect(); + + result.push(CommitHealthWithName { + conventional_rate: round3(if total > 0 { conventional as f64 / total as f64 } else { 0.0 }), + rework_rate: round3(if conventional > 0 { rework as f64 / conventional as f64 } else { 0.0 }), + project_id: pid, + project_name: name, + total_commits: total, + conventional_commits: conventional, + rework_commits: rework, + top_scopes, + }); + } + Ok(result) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 477effb..4c67beb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -275,6 +275,7 @@ pub fn run() { commands::commit_metrics::get_commit_stats, commands::commit_metrics::ingest_full_git_history, commands::commit_metrics::get_project_commit_health, + commands::commit_metrics::get_all_commit_health, // mentor get_mentor_context, get_project_notes, diff --git a/src/components/dashboard/BlueprintModal.tsx b/src/components/dashboard/BlueprintModal.tsx index b43ed66..3ec0f99 100644 --- a/src/components/dashboard/BlueprintModal.tsx +++ b/src/components/dashboard/BlueprintModal.tsx @@ -43,9 +43,9 @@ import { type TemplateResult, type CommitStats, type UserConventionsStatus, - type ProjectCommitHealth, + type CommitHealthWithName, ingestFullGitHistory, - getProjectCommitHealth, + getAllCommitHealth, } from "../../lib/commands"; interface Props { @@ -1680,12 +1680,9 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) { return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath }; }, [projects]); - const projectIds = useMemo(() => projectsWithPath.map((p) => p.id), [projectsWithPath]); - - const { data: commitHealth, refetch: refetchHealth } = useQuery({ - queryKey: ["commit-health", projectIds], - queryFn: () => getProjectCommitHealth(projectIds), - enabled: projectIds.length > 0, + const { data: commitHealth, refetch: refetchHealth } = useQuery({ + queryKey: ["commit-health-all"], + queryFn: () => getAllCommitHealth(), }); const [ingesting, setIngesting] = useState(false); @@ -1956,11 +1953,10 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) { {commitHealth.map((h) => { - const proj = projectsWithPath.find((p) => p.id === h.project_id); return ( - - {proj?.name ?? h.project_id} + + {h.project_name} {h.total_commits} = 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}> diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 7d29bd6..ce9754b 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -1714,6 +1714,13 @@ export interface ProjectCommitHealth { export const getProjectCommitHealth = (projectIds: string[]) => invoke("get_project_commit_health", { projectIds }); +export interface CommitHealthWithName extends ProjectCommitHealth { + project_name: string; +} + +export const getAllCommitHealth = () => + invoke("get_all_commit_health"); + // ── Beast Global Status (read-only) ─────────────────────────────────────────── export interface BeastGlobalStatus {