diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index 13b7191..0883edc 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -304,8 +304,8 @@ modules: name: 飞轮智能成长 area: backend status: in_progress - progress: 85 - note: "L0+F1+F2 完成;新增 R1 trailer 自吸 + R2 规则边界字段(2026-07-02,救活 rule_effectiveness 死链);阶段三待数据积累" + progress: 92 + note: "L0+F1+F2+R1+R2 完成(2026-07-02,rule_effectiveness 死链已接通);阶段三待数据覆盖 ≥10 项目" position: [1750, 650] - id: cloud-products @@ -376,8 +376,8 @@ modules: name: Gitea 集成 area: backend status: in_progress - progress: 55 - note: "G1-G5 全部完成(控制面+观测面+UI);余:Gitea Actions 生成(📋)、PR/CI 事件、迁移" + progress: 70 + note: "G1-G5 + Gitea Actions 生成完成(2026-07-02);余:PR/CI 事件联动、DeepSeek review、存量迁移(concept)" position: [3500, 150] updated: 2026-07-02 diff --git a/.blueprint/modules/flywheel-intelligence.md b/.blueprint/modules/flywheel-intelligence.md index 184a2cb..1ffe8cb 100644 --- a/.blueprint/modules/flywheel-intelligence.md +++ b/.blueprint/modules/flywheel-intelligence.md @@ -125,14 +125,15 @@ --- -### 📋 R1. Rules-Applied commit trailer 被动自吸(规则有效性数据源) -- status: todo +### ✅ R1. Rules-Applied commit trailer 被动自吸(规则有效性数据源) +- status: done - complexity: M - depends: flywheel-intelligence(L0 已完成即可,不依赖阶段三) - files: .claude/skills/commit(或 smart-commit 模板), src-tauri/src/commands/commit_metrics.rs, src-tauri/src/db.rs, src-tauri/src/commands/blueprint.rs - acceptance: ① /commit 技能在本次任务触发过规则时,commit message 末尾自动附 `Rules-Applied: R01,R06` trailer;② commit_metrics 解析层提取该 trailer 存入新列(补 migration,R05);③ get_flywheel_stats 聚合"规则命中 × 该 commit 是否 fix 返工"交叉统计注入梦核 prompt;④ 新增解析函数有单元测试(依赖注入 + in-memory DB),cargo test 全绿 > 背景:复盘笔记进料口枯竭(70 条仅 4 条结构化),rule_effectiveness 闭环是死链,R01-R10 的 confidence 全是人工拍的。本卡把规则有效性数据改为 git 被动自吸——与 L0 同构,是 CONVENTIONS 置信度自动进化(阶段三)的真实数据前提。 +> 完成记录(2026-07-02):smart-commit 技能加 trailer 步骤;commit_metrics 新增 rules_applied 列(migration)+ parse_rules_applied + rule_hit_stats 聚合;梦核 prompt 新增「规则命中×返工交叉」数据段与【规则置信度】分析任务;5 个单元测试(测试抓到一个 ? 提前返回 bug)。数据将随日常 commit 自然积累。 ### ✅ R2. CONVENTIONS 规则边界字段 + R11 失败记录规则 - status: done diff --git a/.blueprint/modules/gitea-integration.md b/.blueprint/modules/gitea-integration.md index d23f405..055bb0f 100644 --- a/.blueprint/modules/gitea-integration.md +++ b/.blueprint/modules/gitea-integration.md @@ -96,8 +96,8 @@ Gitea push → POST /webhook/gitea/:project_id > ⚠️ push 事件部分已被 G3/G4 实现取代;剩余范围收窄为「PR/CI/review 事件 + 轮询降级」(见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。 -### 🔵 Gitea Actions workflow 生成 -- status: in_progress +### ✅ Gitea Actions workflow 生成 +- status: done - complexity: M - depends: G1 - files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx @@ -160,6 +160,16 @@ Gitea push → POST /webhook/gitea/:project_id ## 复盘笔记 +【复盘】Gitea Actions workflow 生成(2026-07-02) +任务类型: 后端命令 + 前端组件 +复杂度: M +实际轮数: 1轮(预估 1 轮) +是否返工: 否 +有效策略: 语法兼容性使改造收敛为三处——输出目录 .gitea/workflows、去掉 GitHub Release 步骤(发布走 OSS 不需要 Release 页)、runs-on 改自建 runner 标签;用单元测试断言"无 github.com/GITHUB_TOKEN 残留"把验收标准变成回归测试 +遗留风险: ① tauri_oss 模板需要带 windows 标签的自建 act_runner,尚未实际部署;② actions/cache(setup-node 的 pnpm 缓存)依赖 act_runner 的 cache server,旧版本 runner 可能不生效 +auto_applied_rules: R01, R07 +rule_effectiveness: R01=有效(命令未新增,仅改产出), R07=有效(模板残留断言测试) + 【复盘】G2-G5 控制面 + 观测面全链路(2026-07-02) 任务类型: 后端命令 + MCP工具 + 前端组件 复杂度: M×4 diff --git a/src-tauri/src/commands/blueprint.rs b/src-tauri/src/commands/blueprint.rs index 33dcd46..7be8f9c 100644 --- a/src-tauri/src/commands/blueprint.rs +++ b/src-tauri/src/commands/blueprint.rs @@ -1889,6 +1889,23 @@ 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() + .and_then(|conn| crate::commands::commit_metrics::rule_hit_stats(&conn).ok()) + .filter(|v| !v.is_empty()) + .map(|v| { + let header = "| 规则 | 命中 commit | 其中返工 | 覆盖项目 | 返工占比 |"; + let sep = "|------|------------|----------|----------|----------|"; + let rows: Vec = v.iter().map(|r| { + let ratio = if r.hits > 0 { r.rework_hits as f64 / r.hits as f64 * 100.0 } else { 0.0 }; + format!("| {} | {} | {} | {} | {:.0}% |", r.rule_id, r.hits, r.rework_hits, r.project_count, ratio) + }).collect(); + format!("{header}\n{sep}\n{}", rows.join("\n")) + }) + .unwrap_or_else(|| "(暂无数据——commit message 附带 Rules-Applied trailer 后自动积累)".to_string()); + format!(r#"你是炼境(Dev Manager)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。 ## 飞轮运转数据(来自炼境跨项目聚合) @@ -1929,6 +1946,13 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String { {notes_summary} +### 规则命中 × 返工交叉(Rules-Applied trailer 被动自吸) + +> commit message 附带的规则触发记录。返工占比高的规则 = 触发了但没防住问题, +> 是降置信度或按 R11 补 failure_case/boundaries 的信号;命中多且返工低 = 升置信度依据。 + +{rule_hits_summary} + --- ## 当前 CONVENTIONS.md(v{conventions_version})规则摘要 @@ -1984,6 +2008,16 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String { - 归档候选:项目名 + 建议归档的文件路径 - CONVENTIONS 删改候选:具体条目 + 理由 +### 【规则置信度】哪些规则该升/降 confidence 或补边界? + +仅当"规则命中 × 返工交叉"数据非空时执行,否则跳过本节。判断标准: +1. 命中多且返工占比低 → 建议提升该规则 confidence / evidence_count +2. 命中多且返工占比高 → 规则触发了但没防住问题:建议降 confidence,或按 R11 输出 failure_case 与 boundaries(把规则细化为带条件的变体,而非简单降分) +3. 长期零命中的规则 → 检查触发条件是否过窄或场景已消失 + +输出格式: +- 规则 ID + 建议动作(升 / 降 / 细化边界)+ 依据数据 + --- 请直接输出结构化分析结果,不要重复我的数据,聚焦在你的判断和建议上。 diff --git a/src-tauri/src/commands/cicd.rs b/src-tauri/src/commands/cicd.rs index 1ddfdc8..fc47a22 100644 --- a/src-tauri/src/commands/cicd.rs +++ b/src-tauri/src/commands/cicd.rs @@ -1,5 +1,5 @@ -// CI/CD 配置管理与 Workflow 生成 -// 完整部署教程与踩坑指南见 docs/tauri-cicd-guide.md +// CI/CD 配置管理与 Workflow 生成(Gitea Actions,语法兼容 GitHub Actions) +// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md(原 GitHub Actions 方案) use crate::db; use crate::models::{CicdConfig, WorkflowResult}; use rusqlite::params; @@ -65,7 +65,7 @@ pub fn save_cicd_config(input: CicdConfig) -> Result { get_cicd_config(input.project_id)?.ok_or_else(|| "save failed".to_string()) } -/// 根据项目的 cicd_configs 生成 workflow 文件,写入 {project_path}/.github/workflows/ +/// 根据项目的 cicd_configs 生成 Gitea Actions workflow 文件,写入 {project_path}/.gitea/workflows/ /// project_path 必须是 Windows 可访问的路径(WSL 项目请传 UNC 路径 \\wsl.localhost\...) #[tauri::command] pub fn generate_workflow(project_id: String, project_path: String) -> Result { @@ -85,7 +85,7 @@ pub fn generate_workflow(project_id: String, project_path: String) -> Result build_web_ssh_workflow(&config), }; - let workflows_dir = Path::new(&project_path).join(".github").join("workflows"); + let workflows_dir = Path::new(&project_path).join(".gitea").join("workflows"); std::fs::create_dir_all(&workflows_dir).map_err(|e| e.to_string())?; let file_path = workflows_dir.join(&filename); std::fs::write(&file_path, &content).map_err(|e| e.to_string())?; @@ -107,7 +107,8 @@ fn build_web_ssh_workflow(cfg: &CicdConfig) -> (String, String, Vec) { let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app"); let content = format!( - r#"name: Deploy + r#"# Gitea Actions(语法兼容 GitHub Actions,action 由 runner 从镜像源拉取) +name: Deploy on: push: @@ -163,8 +164,9 @@ fn build_tauri_oss_workflow(cfg: &CicdConfig) -> (String, String, Vec) { .unwrap_or("oss-cn-hangzhou.aliyuncs.com"); let content = format!( - r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布 -# 完整部署教程与踩坑指南见 docs/tauri-cicd-guide.md + r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布(Gitea Actions) +# 需要一台带 'windows' 标签的自建 Windows runner(act_runner) +# 发布产物走 OSS 分发(tauri-plugin-updater 读 latest.json),不依赖 Release 页面 name: Release on: @@ -174,9 +176,7 @@ on: jobs: release: - runs-on: windows-latest - permissions: - contents: write + runs-on: windows steps: - uses: actions/checkout@v4 @@ -212,32 +212,12 @@ jobs: $lines = $env:TAURI_SIGNING_PRIVATE_KEY.Split("`n").Count Write-Host "TAURI_SIGNING_PRIVATE_KEY is set ($lines lines)" - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{{{ github.ref_name }}}} - name: ${{{{ github.ref_name }}}} - body: | - ## 更新内容 - 请查看提交记录了解详情。 - draft: false - prerelease: false - - - name: Build & Release - uses: tauri-apps/tauri-action@v0 + - name: Build(签名随环境变量注入) + shell: pwsh env: - GITHUB_TOKEN: ${{{{ secrets.GITHUB_TOKEN }}}} TAURI_SIGNING_PRIVATE_KEY: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY }}}} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}}} - with: - tagName: ${{{{ github.ref_name }}}} - releaseName: ${{{{ github.ref_name }}}} - releaseBody: | - ## 更新内容 - 请查看提交记录了解详情。 - releaseDraft: false - prerelease: false - args: --verbose + run: pnpm tauri build --verbose - name: Upload to OSS & generate latest.json shell: pwsh @@ -314,3 +294,47 @@ jobs: ]; (content, "release.yml".to_string(), secrets) } + +#[cfg(test)] +mod tests { + use super::*; + + fn cfg(config_type: &str) -> CicdConfig { + CicdConfig { + id: "t".into(), + project_id: "p".into(), + config_type: config_type.into(), + trigger_branch: "main".into(), + build_command: Some("pnpm build".into()), + node_version: Some("20".into()), + server_host: Some("host".into()), + server_user: Some("ubuntu".into()), + deploy_path: Some("/app".into()), + start_command: Some("pm2 restart app".into()), + oss_endpoint: Some("oss-cn-hangzhou.aliyuncs.com".into()), + created_at: None, + } + } + + #[test] + fn web_ssh_template_has_no_github_residue() { + let (content, filename, secrets) = build_web_ssh_workflow(&cfg("web_ssh")); + assert!(!content.contains("github.com"), "模板残留 github.com 引用"); + assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN"); + assert_eq!(filename, "deploy.yml"); + assert!(secrets.contains(&"SSH_KEY".to_string())); + } + + #[test] + fn tauri_oss_template_has_no_github_residue() { + let (content, filename, secrets) = build_tauri_oss_workflow(&cfg("tauri_oss")); + assert!(!content.contains("github.com"), "模板残留 github.com 引用"); + assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN"); + assert!(!content.contains("gh-release"), "模板残留 GitHub Release 步骤"); + assert!(content.contains("pnpm tauri build"), "缺少构建步骤"); + assert!(content.contains("latest.json"), "缺少 updater 产物上传"); + assert_eq!(filename, "release.yml"); + assert!(secrets.contains(&"TAURI_SIGNING_PRIVATE_KEY".to_string())); + assert!(!secrets.iter().any(|s| s.contains("GITHUB")), "secrets 残留 GitHub 项"); + } +} diff --git a/src-tauri/src/commands/commit_metrics.rs b/src-tauri/src/commands/commit_metrics.rs index 57e59c5..fe8a993 100644 --- a/src-tauri/src/commands/commit_metrics.rs +++ b/src-tauri/src/commands/commit_metrics.rs @@ -75,6 +75,35 @@ fn non_conventional(is_ci_auto: bool) -> ParsedCommit { } } +/// 解析 commit message 中的 `Rules-Applied: R01,R06` trailer(飞轮 R1 规则有效性数据源)。 +/// 只接受 R+数字 形式的 token,规范化为大写去重逗号串;无 trailer 或无有效 token 返回 None。 +pub fn parse_rules_applied(message: &str) -> Option { + for line in message.lines() { + let trimmed = line.trim(); + let Some(rest) = trimmed + .strip_prefix("Rules-Applied:") + .or_else(|| trimmed.strip_prefix("rules-applied:")) + .or_else(|| trimmed.strip_prefix("Rules-applied:")) + else { + continue; + }; + let rest = rest.trim(); + let mut ids: Vec = rest + .split([',', ' ', ',']) + .map(|t| t.trim().to_uppercase()) + .filter(|t| { + t.len() >= 2 + && t.starts_with('R') + && t[1..].chars().all(|c| c.is_ascii_digit()) + }) + .collect(); + ids.sort(); + ids.dedup(); + return if ids.is_empty() { None } else { Some(ids.join(",")) }; + } + None +} + /// 解析并写入一条 commit 记录(sha 为主键,重复写入忽略)。 pub fn record_commit( conn: &Connection, @@ -84,10 +113,11 @@ pub fn record_commit( committed_at: Option<&str>, ) -> Result<(), String> { let p = parse_conventional(message); + let rules_applied = parse_rules_applied(message); conn.execute( "INSERT OR IGNORE INTO commit_metrics - (sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + (sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at, rules_applied) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", rusqlite::params![ sha, project_id, @@ -96,12 +126,69 @@ pub fn record_commit( p.is_rework as i32, p.is_ci_auto as i32, committed_at, + rules_applied, ], ) .map_err(|e| e.to_string())?; Ok(()) } +/// 单条规则的命中统计(跨项目聚合) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RuleHitStat { + pub rule_id: String, + /// 附带该规则的 commit 总数 + pub hits: u32, + /// 其中 type=fix(非 CI 自修)的返工 commit 数——规则触发了但仍出问题的信号 + pub rework_hits: u32, + /// 覆盖的项目数 + pub project_count: u32, +} + +/// 聚合所有项目的 Rules-Applied 命中 × 返工交叉统计(供梦核置信度分析)。 +pub fn rule_hit_stats(conn: &Connection) -> Result, String> { + let mut stmt = conn + .prepare( + "SELECT rules_applied, is_rework, is_ci_auto, project_id + FROM commit_metrics WHERE rules_applied IS NOT NULL", + ) + .map_err(|e| e.to_string())?; + let rows = stmt + .query_map([], |r| { + Ok(( + r.get::<_, String>(0)?, + r.get::<_, i64>(1)? != 0, + r.get::<_, i64>(2)? != 0, + r.get::<_, String>(3)?, + )) + }) + .map_err(|e| e.to_string())?; + + use std::collections::{BTreeMap, HashSet}; + let mut hits: BTreeMap)> = BTreeMap::new(); + for row in rows { + let (rules, is_rework, is_ci_auto, project_id) = row.map_err(|e| e.to_string())?; + for rule in rules.split(',').map(str::trim).filter(|s| !s.is_empty()) { + let entry = hits.entry(rule.to_string()).or_default(); + entry.0 += 1; + if is_rework && !is_ci_auto { + entry.1 += 1; + } + entry.2.insert(project_id.clone()); + } + } + + Ok(hits + .into_iter() + .map(|(rule_id, (h, rw, projects))| RuleHitStat { + rule_id, + hits: h, + rework_hits: rw, + project_count: projects.len() as u32, + }) + .collect()) +} + /// 项目的有效返工次数:type=fix 且非 CI 自动修复。 pub fn rework_count(conn: &Connection, project_id: &str) -> Result { conn.query_row( @@ -614,4 +701,70 @@ mod tests { let (total, _) = conventional_stats(&conn, "p3").unwrap(); assert_eq!(total, 1, "重复 sha 应被 INSERT OR IGNORE 忽略"); } + + // ── R1:Rules-Applied trailer ────────────────────────────────────────── + + #[test] + fn parse_rules_applied_normalizes_tokens() { + let msg = "feat(core): thing + +说明 + +Rules-Applied: r06, R01,R01, xx, R10"; + assert_eq!(parse_rules_applied(msg).as_deref(), Some("R01,R06,R10")); + } + + #[test] + fn parse_rules_applied_absent_or_empty() { + assert_eq!(parse_rules_applied("feat: no trailer"), None); + assert_eq!(parse_rules_applied("Rules-Applied: 无效, abc"), None); + } + + #[test] + fn record_commit_stores_rules_applied() { + let conn = crate::db::conn_with_schema(); + record_commit(&conn, "pr", "sha-r1", "feat(x): a + +Rules-Applied: R01,R05", None).unwrap(); + let stored: Option = conn.query_row( + "SELECT rules_applied FROM commit_metrics WHERE sha = 'sha-r1'", [], |r| r.get(0), + ).unwrap(); + assert_eq!(stored.as_deref(), Some("R01,R05")); + } + + #[test] + fn rule_hit_stats_cross_counts_rework() { + let conn = crate::db::conn_with_schema(); + record_commit(&conn, "pa", "s1", "feat(x): ok + +Rules-Applied: R01", None).unwrap(); + record_commit(&conn, "pa", "s2", "fix(x): broke + +Rules-Applied: R01,R06", None).unwrap(); + record_commit(&conn, "pb", "s3", "feat(y): fine + +Rules-Applied: R06", None).unwrap(); + // CI 自修的 fix 不计返工 + record_commit(&conn, "pb", "s4", "fix(y): auto [DeepSeek-V3] + +Rules-Applied: R06", None).unwrap(); + + let stats = rule_hit_stats(&conn).unwrap(); + let r01 = stats.iter().find(|s| s.rule_id == "R01").unwrap(); + assert_eq!(r01.hits, 2); + assert_eq!(r01.rework_hits, 1); + assert_eq!(r01.project_count, 1); + let r06 = stats.iter().find(|s| s.rule_id == "R06").unwrap(); + assert_eq!(r06.hits, 3); + assert_eq!(r06.rework_hits, 1, "CI 自修 fix 不应计入返工"); + assert_eq!(r06.project_count, 2); + } + + #[test] + fn rule_hit_stats_empty_when_no_trailers() { + let conn = crate::db::conn_with_schema(); + record_commit(&conn, "pz", "s9", "feat: plain", None).unwrap(); + assert!(rule_hit_stats(&conn).unwrap().is_empty()); + } } + diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index c898e45..b5860c8 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -535,6 +535,9 @@ fn migrate(conn: &rusqlite::Connection) { CREATE INDEX IF NOT EXISTS idx_commit_metrics_project ON commit_metrics(project_id); ").expect("create commit_metrics table"); + // 增量迁移(飞轮 R1):Rules-Applied trailer 规则命中列(已存在则忽略) + let _ = conn.execute("ALTER TABLE commit_metrics ADD COLUMN rules_applied TEXT", []); + // 仪表盘全局快启应用(无项目 FK 约束) conn.execute_batch(" CREATE TABLE IF NOT EXISTS global_tools ( diff --git a/src/components/dashboard/CicdModal.tsx b/src/components/dashboard/CicdModal.tsx index fe51e3e..9b205f5 100644 --- a/src/components/dashboard/CicdModal.tsx +++ b/src/components/dashboard/CicdModal.tsx @@ -253,10 +253,10 @@ export function CicdModal({ {generatedSecrets && (

- ✅ Workflow 文件已生成,请在 GitHub 仓库设置以下 Secrets: + ✅ Workflow 已生成(.gitea/workflows/),请在 Gitea 仓库设置以下 Secrets:

- Settings → Secrets and variables → Actions → New repository secret + 仓库 Settings → Actions → Secrets → Add Secret

{generatedSecrets.map((s) => (