feat(flywheel): Gitea Actions 模板 + Rules-Applied trailer 自吸——规则有效性闭环接通
Gitea Actions workflow 生成(gitea-integration 收官卡): - generate_workflow 输出 .gitea/workflows/;tauri_oss 模板去掉 GitHub Release 步骤(发布走 OSS),构建改 pnpm tauri build + 签名环境变量,runs-on 改 自建 windows runner 标签;CicdModal Secrets 文案改 Gitea 路径 - 单元测试断言模板无 github.com / GITHUB_TOKEN / gh-release 残留 R1 Rules-Applied trailer 被动自吸(flywheel-intelligence): - smart-commit 技能:commit message 末尾附 Rules-Applied: R01,R06 trailer (仅真实触发的规则,防数据污染) - commit_metrics 新增 rules_applied 列(增量 migration)+ parse_rules_applied 解析(token 校验/去重/规范化)+ rule_hit_stats 跨项目"命中×返工"聚合 - 梦核 prompt 新增「规则命中×返工交叉」数据段 + 【规则置信度】分析任务 (升/降 confidence、按 R11 细化边界)——R01-R10 置信度首次有真实数据来源 验证:cargo test 65 passed(新增 7 测试,其中一个抓到 trailer 解析的 ? 提前返回 bug)、typecheck 全绿 Rules-Applied: R01, R05, R06, R07
This commit is contained in:
parent
bf697b5e28
commit
de961d0e95
@ -304,8 +304,8 @@ modules:
|
|||||||
name: 飞轮智能成长
|
name: 飞轮智能成长
|
||||||
area: backend
|
area: backend
|
||||||
status: in_progress
|
status: in_progress
|
||||||
progress: 85
|
progress: 92
|
||||||
note: "L0+F1+F2 完成;新增 R1 trailer 自吸 + R2 规则边界字段(2026-07-02,救活 rule_effectiveness 死链);阶段三待数据积累"
|
note: "L0+F1+F2+R1+R2 完成(2026-07-02,rule_effectiveness 死链已接通);阶段三待数据覆盖 ≥10 项目"
|
||||||
position: [1750, 650]
|
position: [1750, 650]
|
||||||
|
|
||||||
- id: cloud-products
|
- id: cloud-products
|
||||||
@ -376,8 +376,8 @@ modules:
|
|||||||
name: Gitea 集成
|
name: Gitea 集成
|
||||||
area: backend
|
area: backend
|
||||||
status: in_progress
|
status: in_progress
|
||||||
progress: 55
|
progress: 70
|
||||||
note: "G1-G5 全部完成(控制面+观测面+UI);余:Gitea Actions 生成(📋)、PR/CI 事件、迁移"
|
note: "G1-G5 + Gitea Actions 生成完成(2026-07-02);余:PR/CI 事件联动、DeepSeek review、存量迁移(concept)"
|
||||||
position: [3500, 150]
|
position: [3500, 150]
|
||||||
updated: 2026-07-02
|
updated: 2026-07-02
|
||||||
|
|
||||||
|
|||||||
@ -125,14 +125,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📋 R1. Rules-Applied commit trailer 被动自吸(规则有效性数据源)
|
### ✅ R1. Rules-Applied commit trailer 被动自吸(规则有效性数据源)
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: flywheel-intelligence(L0 已完成即可,不依赖阶段三)
|
- 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
|
- 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 全绿
|
- 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 置信度自动进化(阶段三)的真实数据前提。
|
> 背景:复盘笔记进料口枯竭(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 失败记录规则
|
### ✅ R2. CONVENTIONS 规则边界字段 + R11 失败记录规则
|
||||||
- status: done
|
- status: done
|
||||||
|
|||||||
@ -96,8 +96,8 @@ Gitea push → POST /webhook/gitea/:project_id
|
|||||||
|
|
||||||
> ⚠️ push 事件部分已被 G3/G4 实现取代;剩余范围收窄为「PR/CI/review 事件 + 轮询降级」(见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。
|
> ⚠️ push 事件部分已被 G3/G4 实现取代;剩余范围收窄为「PR/CI/review 事件 + 轮询降级」(见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。
|
||||||
|
|
||||||
### 🔵 Gitea Actions workflow 生成
|
### ✅ Gitea Actions workflow 生成
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: G1
|
- depends: G1
|
||||||
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx
|
- 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)
|
【复盘】G2-G5 控制面 + 观测面全链路(2026-07-02)
|
||||||
任务类型: 后端命令 + MCP工具 + 前端组件
|
任务类型: 后端命令 + MCP工具 + 前端组件
|
||||||
复杂度: M×4
|
复杂度: M×4
|
||||||
|
|||||||
@ -1889,6 +1889,23 @@ 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)─────────────────
|
||||||
|
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<String> = 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)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。
|
format!(r#"你是炼境(Dev Manager)的蓝图架构师,负责对飞轮运转数据进行记忆巩固分析。
|
||||||
|
|
||||||
## 飞轮运转数据(来自炼境跨项目聚合)
|
## 飞轮运转数据(来自炼境跨项目聚合)
|
||||||
@ -1929,6 +1946,13 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
|
|||||||
|
|
||||||
{notes_summary}
|
{notes_summary}
|
||||||
|
|
||||||
|
### 规则命中 × 返工交叉(Rules-Applied trailer 被动自吸)
|
||||||
|
|
||||||
|
> commit message 附带的规则触发记录。返工占比高的规则 = 触发了但没防住问题,
|
||||||
|
> 是降置信度或按 R11 补 failure_case/boundaries 的信号;命中多且返工低 = 升置信度依据。
|
||||||
|
|
||||||
|
{rule_hits_summary}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 当前 CONVENTIONS.md(v{conventions_version})规则摘要
|
## 当前 CONVENTIONS.md(v{conventions_version})规则摘要
|
||||||
@ -1984,6 +2008,16 @@ pub fn generate_muhe_prompt(stats: FlywheelStats) -> String {
|
|||||||
- 归档候选:项目名 + 建议归档的文件路径
|
- 归档候选:项目名 + 建议归档的文件路径
|
||||||
- CONVENTIONS 删改候选:具体条目 + 理由
|
- CONVENTIONS 删改候选:具体条目 + 理由
|
||||||
|
|
||||||
|
### 【规则置信度】哪些规则该升/降 confidence 或补边界?
|
||||||
|
|
||||||
|
仅当"规则命中 × 返工交叉"数据非空时执行,否则跳过本节。判断标准:
|
||||||
|
1. 命中多且返工占比低 → 建议提升该规则 confidence / evidence_count
|
||||||
|
2. 命中多且返工占比高 → 规则触发了但没防住问题:建议降 confidence,或按 R11 输出 failure_case 与 boundaries(把规则细化为带条件的变体,而非简单降分)
|
||||||
|
3. 长期零命中的规则 → 检查触发条件是否过窄或场景已消失
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
- 规则 ID + 建议动作(升 / 降 / 细化边界)+ 依据数据
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
请直接输出结构化分析结果,不要重复我的数据,聚焦在你的判断和建议上。
|
请直接输出结构化分析结果,不要重复我的数据,聚焦在你的判断和建议上。
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// CI/CD 配置管理与 Workflow 生成
|
// CI/CD 配置管理与 Workflow 生成(Gitea Actions,语法兼容 GitHub Actions)
|
||||||
// 完整部署教程与踩坑指南见 docs/tauri-cicd-guide.md
|
// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md(原 GitHub Actions 方案)
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::models::{CicdConfig, WorkflowResult};
|
use crate::models::{CicdConfig, WorkflowResult};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
@ -65,7 +65,7 @@ pub fn save_cicd_config(input: CicdConfig) -> Result<CicdConfig, String> {
|
|||||||
get_cicd_config(input.project_id)?.ok_or_else(|| "save failed".to_string())
|
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\...)
|
/// project_path 必须是 Windows 可访问的路径(WSL 项目请传 UNC 路径 \\wsl.localhost\...)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn generate_workflow(project_id: String, project_path: String) -> Result<WorkflowResult, String> {
|
pub fn generate_workflow(project_id: String, project_path: String) -> Result<WorkflowResult, String> {
|
||||||
@ -85,7 +85,7 @@ pub fn generate_workflow(project_id: String, project_path: String) -> Result<Wor
|
|||||||
_ => build_web_ssh_workflow(&config),
|
_ => 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())?;
|
std::fs::create_dir_all(&workflows_dir).map_err(|e| e.to_string())?;
|
||||||
let file_path = workflows_dir.join(&filename);
|
let file_path = workflows_dir.join(&filename);
|
||||||
std::fs::write(&file_path, &content).map_err(|e| e.to_string())?;
|
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<String>) {
|
|||||||
let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app");
|
let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app");
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
r#"name: Deploy
|
r#"# Gitea Actions(语法兼容 GitHub Actions,action 由 runner 从镜像源拉取)
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -163,8 +164,9 @@ fn build_tauri_oss_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
|
|||||||
.unwrap_or("oss-cn-hangzhou.aliyuncs.com");
|
.unwrap_or("oss-cn-hangzhou.aliyuncs.com");
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布
|
r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布(Gitea Actions)
|
||||||
# 完整部署教程与踩坑指南见 docs/tauri-cicd-guide.md
|
# 需要一台带 'windows' 标签的自建 Windows runner(act_runner)
|
||||||
|
# 发布产物走 OSS 分发(tauri-plugin-updater 读 latest.json),不依赖 Release 页面
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@ -174,9 +176,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: windows-latest
|
runs-on: windows
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@ -212,32 +212,12 @@ jobs:
|
|||||||
$lines = $env:TAURI_SIGNING_PRIVATE_KEY.Split("`n").Count
|
$lines = $env:TAURI_SIGNING_PRIVATE_KEY.Split("`n").Count
|
||||||
Write-Host "TAURI_SIGNING_PRIVATE_KEY is set ($lines lines)"
|
Write-Host "TAURI_SIGNING_PRIVATE_KEY is set ($lines lines)"
|
||||||
|
|
||||||
- name: Create Release
|
- name: Build(签名随环境变量注入)
|
||||||
uses: softprops/action-gh-release@v2
|
shell: pwsh
|
||||||
with:
|
|
||||||
tag_name: ${{{{ github.ref_name }}}}
|
|
||||||
name: ${{{{ github.ref_name }}}}
|
|
||||||
body: |
|
|
||||||
## 更新内容
|
|
||||||
请查看提交记录了解详情。
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Build & Release
|
|
||||||
uses: tauri-apps/tauri-action@v0
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{{{ secrets.GITHUB_TOKEN }}}}
|
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY }}}}
|
TAURI_SIGNING_PRIVATE_KEY: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY }}}}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}}}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}}}
|
||||||
with:
|
run: pnpm tauri build --verbose
|
||||||
tagName: ${{{{ github.ref_name }}}}
|
|
||||||
releaseName: ${{{{ github.ref_name }}}}
|
|
||||||
releaseBody: |
|
|
||||||
## 更新内容
|
|
||||||
请查看提交记录了解详情。
|
|
||||||
releaseDraft: false
|
|
||||||
prerelease: false
|
|
||||||
args: --verbose
|
|
||||||
|
|
||||||
- name: Upload to OSS & generate latest.json
|
- name: Upload to OSS & generate latest.json
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@ -314,3 +294,47 @@ jobs:
|
|||||||
];
|
];
|
||||||
(content, "release.yml".to_string(), secrets)
|
(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 项");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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<String> {
|
||||||
|
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<String> = 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 为主键,重复写入忽略)。
|
/// 解析并写入一条 commit 记录(sha 为主键,重复写入忽略)。
|
||||||
pub fn record_commit(
|
pub fn record_commit(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
@ -84,10 +113,11 @@ pub fn record_commit(
|
|||||||
committed_at: Option<&str>,
|
committed_at: Option<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let p = parse_conventional(message);
|
let p = parse_conventional(message);
|
||||||
|
let rules_applied = parse_rules_applied(message);
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO commit_metrics
|
"INSERT OR IGNORE INTO commit_metrics
|
||||||
(sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at)
|
(sha, project_id, commit_type, scope, is_rework, is_ci_auto, committed_at, rules_applied)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
sha,
|
sha,
|
||||||
project_id,
|
project_id,
|
||||||
@ -96,12 +126,69 @@ pub fn record_commit(
|
|||||||
p.is_rework as i32,
|
p.is_rework as i32,
|
||||||
p.is_ci_auto as i32,
|
p.is_ci_auto as i32,
|
||||||
committed_at,
|
committed_at,
|
||||||
|
rules_applied,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
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<Vec<RuleHitStat>, 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<String, (u32, u32, HashSet<String>)> = 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 自动修复。
|
/// 项目的有效返工次数:type=fix 且非 CI 自动修复。
|
||||||
pub fn rework_count(conn: &Connection, project_id: &str) -> Result<u32, String> {
|
pub fn rework_count(conn: &Connection, project_id: &str) -> Result<u32, String> {
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
@ -614,4 +701,70 @@ mod tests {
|
|||||||
let (total, _) = conventional_stats(&conn, "p3").unwrap();
|
let (total, _) = conventional_stats(&conn, "p3").unwrap();
|
||||||
assert_eq!(total, 1, "重复 sha 应被 INSERT OR IGNORE 忽略");
|
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<String> = 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -535,6 +535,9 @@ fn migrate(conn: &rusqlite::Connection) {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_commit_metrics_project ON commit_metrics(project_id);
|
CREATE INDEX IF NOT EXISTS idx_commit_metrics_project ON commit_metrics(project_id);
|
||||||
").expect("create commit_metrics table");
|
").expect("create commit_metrics table");
|
||||||
|
|
||||||
|
// 增量迁移(飞轮 R1):Rules-Applied trailer 规则命中列(已存在则忽略)
|
||||||
|
let _ = conn.execute("ALTER TABLE commit_metrics ADD COLUMN rules_applied TEXT", []);
|
||||||
|
|
||||||
// 仪表盘全局快启应用(无项目 FK 约束)
|
// 仪表盘全局快启应用(无项目 FK 约束)
|
||||||
conn.execute_batch("
|
conn.execute_batch("
|
||||||
CREATE TABLE IF NOT EXISTS global_tools (
|
CREATE TABLE IF NOT EXISTS global_tools (
|
||||||
|
|||||||
@ -253,10 +253,10 @@ export function CicdModal({
|
|||||||
{generatedSecrets && (
|
{generatedSecrets && (
|
||||||
<div className="bg-zinc-800 rounded-lg p-3 space-y-2">
|
<div className="bg-zinc-800 rounded-lg p-3 space-y-2">
|
||||||
<p className="text-xs font-medium text-zinc-300">
|
<p className="text-xs font-medium text-zinc-300">
|
||||||
✅ Workflow 文件已生成,请在 GitHub 仓库设置以下 Secrets:
|
✅ Workflow 已生成(.gitea/workflows/),请在 Gitea 仓库设置以下 Secrets:
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-zinc-500">
|
<p className="text-xs text-zinc-500">
|
||||||
Settings → Secrets and variables → Actions → New repository secret
|
仓库 Settings → Actions → Secrets → Add Secret
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||||
{generatedSecrets.map((s) => (
|
{generatedSecrets.map((s) => (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user