华为云码道 CodeArts 实测:让 AI 用 Rust 写一个 Git 仓库健康度体检台「仓衡」

一键开通华为云码道 CodeArts 代码智能体:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1\&sourcead=dmzntgwatomgithd
摘要:本文是华为云码道 AI 编程挑战赛第 4 期的参赛实测记录。我用 CodeArts 代码智能体独立开发了一个跨平台桌面应用「仓衡 · Git 仓库健康度体检台」:对本地 Git 仓库做五维体检(大文件 / 僵尸分支 / 提交习惯 / 依赖健康 / 敏感信息残留),输出带修复建议的结构化报告------纯本地、零网络、只诊断不动手。技术栈是 Rust(检查器内核)+ Tauri 2(桌面壳)+ Vue 3。文章完整记录架构设计(内核/壳分离 + Checker 插件体系 + 两段式扫描编排)、AI 与借用检查器的配合实录、功能演示与踩坑记录,源码已在 AtomGit 开源。这是本系列第四篇:Web 全栈、小程序、鸿蒙原生之后,第四种端形态与第四门语言。
技术栈版本:码道 CodeArts 代码智能体(2026-09 挑战赛版本)| Rust stable(2026-10)| Tauri 2 | git2(libgit2)| Vue 3 + TypeScript | 实测时间: 2026-10

选择目录后显示仓库基本信息------提交总数、分支数、工作区文件数、依赖清单数------确认无误再点「开始体检」;检查器配置区支持调整大文件阈值(保存在 localStorage,下次启动自动恢复),也支持在仓库根目录创建 .repobalance-ignore 忽略特定路径(语法同 .gitignore):

一、项目背景:仓库年久失修的真实痛点

先交代体检对象。我的技术知识库仓库有 664+ 篇文章,Git 历史横跨数年:早期提交的大图片至今躺在历史对象里(删了工作区文件也没用)、临时分支建了忘删、提交信息早期全靠心情写。这些「仓库债」平时无感,直到某天 clone 变慢、想找回某个历史版本才发现问题。

市面上的工具要么面向托管平台(GitHub Insights),要么是单点脚本(如 git-sizer),没有一个「选个本地目录、一键体检、出一份能看懂的报告」的桌面应用。所以第 4 期参赛,我决定做这个工具------而且它有一个天然的加分项:体检对象就是我自己提交参赛的仓库,演示数据全部真实。

产品边界上有三条自我约束,写进了 README 也贯穿了实现:

  1. 纯本地零网络------不做漏洞库联网查询,不做远端平台集成,没有数据出网;
  2. 证据掩码 ------敏感信息扫描只展示掩码后的证据(**** 替代命中内容),报告与导出处处如此;
  3. 只诊断不动手------不做改写历史、不做自动删分支,体检工具不碰你的仓库。

1.1 作品功能一览

模块 功能
大文件检测 工作区 + 历史提交中的超大文件,区分「删文件可解决」与「需重写历史」
分支健康 僵尸分支(陈旧未合并)、未合并提交数、命名规范检查
提交习惯 提交信息规范符合率、超大提交 TOP5、深夜/周末占比
依赖健康 四类清单文件解析 + 本地内置数据的版本陈旧度近似检查
敏感信息扫描 私钥块 / 高熵赋值 / 内网地址,证据掩码展示
体检报告 五维评分雷达 + finding 详情 + 导出 Markdown/JSON
扫描体验 实时进度流、可取消、内核可独立编译为 CLI

技术栈:Rust + git2(内核),Tauri 2(桌面壳),Vue 3 + TypeScript(前端),cargo test 双侧测试。

说明:五类检查器共用同一套 Checker 契约与报告模型(本文的架构设计与代码均以这套契约为主线);截至成稿,桌面应用与 CLI 已上线的规则引擎以大文件检测为主,其余四类检查器在契约与 UI 层已就位、规则实现按里程碑逐步启用,功能一览表是产品完整规划口径。

二、架构设计:三个核心决策

老规矩,架构决策在人、实现在 AI。评审维度里架构与代码合计 60%,这两个决策把两块分都落在实处。

2.1 决策一:内核/壳分离,内核可独立编译为 CLI

Tauri 工程最容易犯的错是把业务逻辑写进 command 处理函数------与框架耦合后既难测试又不可复用。动手前我比过两种组织方式:

方案 可测性 复用性 结论
逻辑全写在 Tauri command 里 差:必须起 Tauri 环境才能测 无 否决
内核独立 crate + Tauri 壳 crate 好:cargo test 直接测内核 内核可发布为 CLI 供 CI 使用 ✅ 采用
text 复制代码
repo-balance/
├── crates/
│   ├── rb-core/     # 检查器内核:零 tauri 依赖,可独立编译为 CLI
│   │   ├── git/       # GitRepo 门面:git2 类型绝不逃逸出这个模块
│   │   ├── model/     # RepoSnapshot / Finding / ScanReport / 评分
│   │   ├── checker/   # Checker trait + 注册表
│   │   ├── engine/    # 扫描编排(并发/取消/进度)
│   │   └── bin/       # CLI 入口:rb scan <path>
│   └── rb-app/      # Tauri 2 壳:只有 command 转发,无业务逻辑
└── ui/              # Vue 3 前端:只读呈现 rb-core 的结构化输出

这条决策的直接收益是一条命令的评审通道:cargo run -p rb-core -- scan ./demo-repo------不会装桌面应用的评审也能完整验证体检能力,而这本身就是架构分 30% 的活证据。

2.2 决策二:Checker trait 插件体系 + 统一 Finding 模型

五类检查如果各自为政,报告聚合、严重度排序、进度上报就要各写一遍。我把它们收敛为一个契约:

rust 复制代码
// crates/rb-core/src/checker/mod.rs(真实源码节选)
/// 单个检查器:声明所需数据切片(plan),基于就绪切片纯计算产出结论(check)。
///
/// 实现约定:
/// - 无状态(`Send + Sync`,可跨线程共享引用);
/// - `check` 为纯计算,同输入同输出,不引入失败通道;
/// - `evidence` 必须为掩码后的证据字符串;
/// - `suggestion` 必须为可执行中文建议。
pub trait Checker: Send + Sync {
    /// 稳定唯一标识,如 `"big-files"` / `"zombie-branches"`。
    fn id(&self) -> &'static str;

    /// 所属检查维度。
    fn category(&self) -> Category;

    /// 声明本检查器需要的数据切片。
    ///
    /// 幂等:同一上下文多次调用结果等价;切片仅限 [`DataSlice`] 封闭集,
    /// 采集由编排层统一完成(本方法只声明、不采集)。
    fn plan(&self, ctx: &ScanContext) -> ScanPlan;

    /// 基于已就绪切片执行检查,返回结论列表(纯计算,不引入失败通道)。
    fn check(&self, ctx: &ScanContext) -> Vec<Finding>;
}

/// 检查器注册表:持有全部 Checker 实例,供扫描编排消费。
#[derive(Default)]
pub struct CheckerRegistry {
    /// 按注册序保存的检查器实例。
    checkers: Vec<Box<dyn Checker>>,
}

impl CheckerRegistry {
    /// 注册一个检查器。id 冲突时返回 `RbError::DuplicateChecker`,
    /// 且既有注册项不覆盖、不破坏;成功时返回 `&mut Self` 保留链式用法。
    pub fn register(&mut self, checker: Box<dyn Checker>) -> Result<&mut Self> {
        let id = checker.id();
        if self.checkers.iter().any(|c| c.id() == id) {
            return Err(RbError::DuplicateChecker { id: id.to_owned() });
        }
        self.checkers.push(checker);
        Ok(self)
    }

    /// 全部已注册检查器(注册序);`engine::scan` 的唯一查询入口。
    pub fn list(&self) -> &[Box<dyn Checker>] {
        &self.checkers
    }
}

注意这个 trait 契约里写死了三条纪律:无状态可跨线程(Send + Sync)、check 纯计算不引入失败通道、evidence 必须是掩码后的证据------掩码纪律不是口头约定,是签名旁边的一等约束。注册表在 id 冲突时直接返回错误而不是静默覆盖,新检查器接入只需「一个实现 + 注册一行」。

方案 聚合成本 新增检查成本 结论
每个检查独立实现扫描+报告 每个都写一遍聚合 高 否决
Checker trait + 编排层统一采集 聚合一次 新增一个实现 + 注册一行 ✅ 采用

plan/check 两段式是个关键设计:先收集所有 Checker 的数据需求,合并去重后一次性遍历仓库 ,把切片分发给各 Checker------避免五条规则把历史遍历五遍。这个设计在 design 阶段就被 Agent 用「核心设计裁决」的形式固化下来:执行顺序硬前置(契约先于实现编码)、plan 双切片声明(工作区 + 历史两个切片)、历史遍历选型(多起点 revwalk 时间正序、每批独立 Repository 规避 git2 句柄 Send 非 Sync 硬约束)、测试构造细节(「仅历史」场景必须物理 git rm + commit,禁用 --cached------物理文件留存为未跟踪文件会被工作区扫描误命中):

design 评审通过后进入任务分解,14 项落地清单按依赖排序(夹具脚本 → 切片模型 → git 门面采集通道 → BigFilesChecker → engine 窄链路 → CLI 接入 → 单测 → 验收),再由「开始任务执行」按钮进入编码:

2.3 决策三:安全是产品本身------掩码在构造时完成,不做「序列化时才脱敏」

敏感信息扫描是本期核心功能,围绕它有三条实现红线。其中最关键的一条是掩码时机:

掩码方案 泄露风险 结论
构造 Finding 时完成掩码,之后的数据流只有掩码版 无:完整命中内容不进入任何下游 ✅ 采用
序列化/展示时才替换 高:忘了脱敏的导出路径就是事故 否决
红线 实现
掩码前置 Finding 构造时掩码(前后 3 字符 + 命中长度),IPC/导出复用同一结构
降噪 空值不报;example/sample/test 文件名中的命中降为 Info
自证 用仓衡扫描仓衡自己,报告可直接核对(见 4.3/4.4)
Tauri 基线 capabilities 最小能力集,不装 fs 插件(文件读取全在 Rust 侧),CSP 收紧为 'self'

三、用 CodeArts 开发:AI 与借用检查器的配合

3.1 开发前的准备

打开 CodeArts 之前的人肉准备:AtomGit 公开仓库 repo-balance(LICENSE + README 骨架)、Rust stable + Tauri 2 环境跑通官方模板,以及一个特殊产物------合成数据的演示仓库 demo-repo :预置历史中的大文件、僵尸分支、以及一块 SAMPLE-NOT-A-REAL-KEY 占位的「私钥」(刻意不用任何格式上可能有效的 token 前缀,这个红线最终写进了投稿检查清单)。

环境这一步 CodeArts 也参与了:打开仓库后它先做了一轮项目分析,识别出「Rust workspace + Tauri 2.x 桌面应用(依赖 git2/serde,前端 pnpm)」,并列出本机需要安装的环境清单与下载地址------Rust stable、MSVC 生成工具、CMake(git2 依赖 libgit2 源码编译必需)、WebView2、Node.js LTS。照单安装即可:

Windows 侧的 rustup 走标准选项一路装完,stable-x86_64-pc-windows-msvc 工具链就位:

环境也不是装完就一劳永逸------D1 的质量门禁阶段还遇到一次 SDK 缺库:dbghelp.lib 的 x64 版本在 Windows Kits 目录里缺失(x86 版存在),所有 MSVC 链接全部失败(LNK1181)。这类问题代码侧无解,Agent 定位根因后给出修复方式二选一(VS Installer 重装修复 / 独立 SDK 安装器补库),补齐后补跑门禁通过:

流程上,每个特征都走同一条 SDD 流水线:需求规格设计(spec)→ 实现方案创建(design)→ 编码任务规划(tasks)→ 任务执行,四个阶段在会话顶部全程可见。本期共跑四个特征:workspace_init(工作区骨架与 Checker 契约,7 个任务组 15 个子任务)→ checker_plugin(Checker 插件体系)→ fixture_bigfiles(大文件检查器 + demo-repo 幂等夹具,8 个任务组 23 个子任务)→ ui_implementation(Tauri 2 壳 + Vue 3 前端,12 个任务组 44 个子任务)。每块话术写死 trait 签名、模块边界与验收标准------以 D1 的话术为例,结构、rb-core 依赖、内部分层、纪律、验收命令五段全部在投喂前写死,「rb-core 的 Cargo.toml 中不得出现任何 tauri 依赖」这类红线直接进了话术:

Agent 把话术翻译成 spec 与任务清单,任务按依赖顺序排列、每个子任务标注涉及文件与可检查的完成标准:

spec 阶段的契约表把 2.2 节那些 trait 签名逐条写成了验收条目------插件契约四方法、ScanPlan 合并去重(「同一切片一次扫描至多采集一次」的性能红线)、注册表禁止重复 id、Finding 契约(evidence 掩码 + suggestion 可执行建议)、测试全部 mock 纯内存验证,甚至专门留了「环境约束专章」规定 SDK 阻塞时如何如实呈现验收结果:

其中一条贯穿始终的纪律是:cargo clippy --all-targets 零 warning 是验收的一部分,不允许「能跑就行」。

3.2 AI 与借用检查器:一次典型的三方对话

Rust 开发中最有趣的部分是 AI、编译器与我三方的关系。以敏感信息扫描为例,AI 第一版生成的对象遍历代码在 cargo check 阶段被借用检查器打回------生命周期标注不符合 libgit2 的 API 约束。我的处理流程是「报错原文完整喂回」:

text 复制代码
你上一步的修改存在问题,实测结果如下:
【操作】:cargo check -p rb-core
【期望】:编译通过
【实际】:error[E0597]: `repo` does not live long enough
(附完整借用检查器报错,包括它给出的修复建议)

请:1. 先定位根因并解释(不要直接改);
2. 给出最小范围修复;
3. 修复后重新执行 cargo test 与 clippy 并报告。

借用检查器的报错自带修复建议,完整喂回后 AI 一次收敛。这件事改变了我的投喂纪律:Rust 的编译器报错必须贴全------截断的报错等于浪费一次往返。

借用检查器打回之后收敛出的最终形态,就是 GitRepo 门面------git2 类型被彻底关在这个模块里,对外只暴露自有模型:

rust 复制代码
// crates/rb-core/src/git/repo.rs(真实源码节选)
/// `git2::Repository` 的只读门面。外界不接触任何 `git2` 类型。
pub struct GitRepo {
    repo: git2::Repository,
}

impl GitRepo {
    /// 打开磁盘上的仓库。
    pub fn open(path: &Path) -> Result<Self> {
        let repo = git2::Repository::discover(path)
            .map_err(|e| RbError::InvalidRepo(format!("{} ({e})", path.display())))?;
        Ok(Self { repo })
    }

    /// 构建只读快照:分支、提交摘要、文件清单、大小与依赖清单文件。
    pub fn snapshot(&self) -> Result<RepoSnapshot> {
        let workdir = self
            .repo
            .workdir()
            .ok_or_else(|| RbError::InvalidRepo("bare 仓库暂不支持".into()))?
            .to_path_buf();

        let branches = self.collect_branches()?;
        let commit_count = self.count_commits()?;
        let recent_commits = self.collect_recent_commits()?;
        let (files, total_bytes) = collect_files(&workdir);
        let dep_manifests = collect_dep_manifests(&files);

        Ok(RepoSnapshot {
            path: workdir.display().to_string(),
            branch_count: branches.len(),
            branches,
            commit_count,
            recent_commits,
            file_count: files.len(),
            total_bytes,
            dep_manifests,
        })
    }
}

门面内部还有一处典型的 libgit2 生命周期妥协值得展开------历史 blob 采集的「分批独立开仓」:

rust 复制代码
// crates/rb-core/src/git/repo.rs · history_blob_metas(真实源码节选)
// 分批处理:每批独立开仓(Repository Send 非 Sync 硬约束)。
// 改为顺序处理以在批次边界检查取消标志(大仓库场景下批次少,
// 取消响应延迟可接受;后续可恢复并行 + 批间检查)。
let width = batch_size.max(1);
let total_commits = commit_oids.len();
if let Some(cb) = on_progress {
    cb(0, total_commits);
}
let mut processed_commits = 0usize;
let mut batch_results: Vec<Vec<(String, String, u64, CommitSummary)>> = Vec::new();
for batch in commit_oids.chunks(width) {
    if let Some(flag) = cancel_flag {
        if flag.load(Ordering::Relaxed) {
            break;
        }
    }
    batch_results.push(process_batch(&workdir, batch));
    processed_commits += batch.len();
    if let Some(cb) = on_progress {
        cb(processed_commits, total_commits);
    }
}

git2::Repository 是 Send 但非 Sync,不能跨线程共享引用,所以每批扫描在 process_batch 里独立开仓;而「批次边界检查取消标志」顺手解决了另一个问题------用户点取消后,最多等一个批次(默认 64 个提交)就停下,已采集的部分 blob 保留为部分报告。这两个约束(生命周期 + 可取消)在一处代码里同时落地,是 libgit2 绑定给上的真实一课。

Rust 的类型系统还带来一个 TS/Java 项目给不了的叙事:把非法状态挡在编译期。Severity 与 Category 是真实源码里的普通枚举而非字符串------「不存在的严重度」在仓衡里不是运行时 bug,是编译错误:

rust 复制代码
// crates/rb-core/src/model/finding.rs(真实源码节选)
/// 问题严重程度。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
    Critical,
    Warning,
    Info,
}

/// 检查维度。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Category {
    Structure,
    History,
    Branches,
    Deps,
    Security,
}

/// 一条检查结论。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finding {
    /// 稳定唯一标识,如 `history.merge-commit-ratio`。
    pub id: String,
    /// 严重程度。
    pub severity: Severity,
    /// 所属维度。
    pub category: Category,
    /// 简短标题。
    pub title: String,
    /// 证据(触发该结论的数据摘要)。
    ///
    /// # 约定
    /// 必须为**掩码后的证据字符串**,禁止包含未掩码的敏感原文;
    /// 安全类检查的强制掩码规则归属 D3 话术落地(一处掩码,处处掩码:
    /// 序列化、导出与 IPC 复用同一 Finding)。
    pub evidence: String,
    /// 改进建议。
    ///
    /// # 约定
    /// 必须是面向开发者的**可执行中文建议**,包含明确的命令或操作步骤。
    /// 正例:`使用 git filter-repo 从历史中移除该文件`;
    /// 反例:仅"请优化"类无操作步骤的文本。
    pub suggestion: String,
}

注意 Finding 的文档注释:掩码纪律和「可执行中文建议」都写在了类型定义上------AI 每次生成新的 Checker 都会看到这份契约,约束跟着类型走,而不是跟着某条会话走。

3.3 会话全程:从需求到验收的完整链路

一个特征从话术到收口,全程在一个会话里完成。D4(Tauri 壳竖切)的 design 摘要把 IPC 命令、数据模型、三视图、样式系统、安全设计一次定清,关键决策四条(rb-core 零改动、前置依赖 stub 策略保证 UI 可独立编译、src-tauri 遗留不改造、前端单页 v-show 切换不引入 vue-router):

随后 tasks.md 生成 12 个任务组 44 个子任务,从「Tauri 2 壳工程搭建与安全基线」到「报告页·总评与雷达图」,按依赖顺序排列,逐组执行:

质量门禁阶段的「AI 与编译器对抗」是最有看点的一幕:cargo clippy --all-targets 一次报出 10 个编译错误,Agent 把它们整理成「错误 / 根因 / 修复方案」三列表格逐项消化------E0423(模块导入写成函数)、E0369(Finding 未 derive PartialEq)、E0277(CheckerRegistry 未实现 Debug)、E0106+E0308(ScanContext 生命周期不匹配):

修复后的完成报告同样以表格收口------每个错误对应文件行号与修法(E0423×3 → use crate::engine::scan::scan、E0369×2 → Finding derive PartialEq, Eq、E0277×2 → 手动 impl Debug for CheckerRegistry 只输出 len),最后一行 clippy 警告也顺手清零:

验收阶段跑完异常场景(不存在路径 / 非 git 目录 / 无参数,退出码与错误信息逐项核对)、只读幂等验证(扫描前后 demo-repo HEAD、status、log 零变更)、源码规模盘点(18 个 .rs 文件 2106 行,三特征全部任务组完成、质量门禁全绿):

IDE 之外,AtomGit 工作台上的华为云码道 Agent 会话也能看到同一条工作线------代码拉到本地、逐文件修改(rb-app/commands.rs 的 scan_with_progress、rb-app/state.rs 的 begin_scan CAS 修复)、任务状态跟随更新,多仓库会话列表并存:

四、功能演示:从选目录到五维雷达

项目完成后,cargo tauri build 产出 dmg(产物体积与启动数据以实测为准),安装后完整走一遍体检链路。

4.1 接入与流式扫描

选择目录后显示仓库基本信息,点击「开始体检」:顶部进度条按阶段推进,当前 Checker 名滚动展示,中途可取消(取消后保留已完成部分并标注「已取消」)。扫描跑在 async_runtime,UI 全程不卡。

进度事件不是前端轮询出来的,是 Rust 侧在引擎阶段边界主动 emit 的。estimate_percent 把 (stage, done, total) 映射为 0-100 的估算百分比------各阶段权重手工分配(初始化 5%、工作区采集到 10%、历史采集占 10-90 按提交比例线性推进、逐检查器检查占 90-99):

rust 复制代码
// crates/rb-app/src/commands.rs(真实源码节选)
/// 启动扫描:异步包装同步 scan(spawn_blocking 避免占死 tokio worker),
/// 完成后 emit scan-done;失败 emit scan-error(与用户取消事件分离)。
#[tauri::command]
pub async fn start_scan(
    app: tauri::AppHandle,
    state: State<'_, ScanState>,
    path: String,
    config: Option<crate::dto::ScanConfigDto>,
) -> Result<(), String> {
    let path_buf = PathBuf::from(&path);
    rb_core::git::GitRepo::open(&path_buf).map_err(map_error)?;

    // CAS 领取扫描权:复位取消标志并原子置位 scanning,拒绝并发扫描。
    if !state.begin_scan() {
        return Err("已有扫描正在进行,请先等待完成或取消".to_owned());
    }
    // ... spawn + spawn_blocking 执行 scan_with_progress,
    //     进度回调里 app.emit("scan-progress", payload),
    //     完成后 emit("scan-done", dto),取消时 dto.cancelled = true
}

同步的 engine::scan 被 spawn_blocking 包起来丢进阻塞线程池,避免占死 tokio worker 冻结 UI------Tauri 异步命令的标准姿势。begin_scan() 用 CAS 方式领取扫描权,并发扫描在门口就被拒绝。

4.2 五维雷达与 finding 详情

扫描完成:顶部总评大数字(≥80 绿 / 60-79 黄 / <60 红),SVG 自绘五维雷达(Structure / History / Branches / Deps / Security),finding 列表按严重度分组排序。点开任意一条 finding:证据、修复建议是可执行的命令(如 git filter-repo 示例)。

这是对真实 Flutter 项目 flutter-ledger 的体检结果:总评 80(绿/健康),结构维度 46 条警告(Flutter 构建产物 .dart_tool/ 下的 app.dill、app.so 等大文件),历史/分支/依赖/安全四个维度零发现满分。点开任意一条 finding:证据、修复建议是可执行的命令(如 git filter-repo 示例),与源码里 Finding.suggestion 的约定一致。

扫描编排的核心循环对应引擎里的真实实现------plan 合并后按切片定点采集,逐检查器纯计算,每个检查器完成即上报进度:

rust 复制代码
// crates/rb-core/src/engine/scan.rs(真实源码节选)
// plan 阶段:base 上下文收集全部检查器的切片请求。
let mut ctx = ScanContext::base(&snapshot, config);
let plans: Vec<_> = registry.list().iter().map(|c| c.plan(&ctx)).collect();
let merged = merge_plans(&plans);

// 定点采集:仅落地合并后需要的切片。
for slice in merged.slices() {
    match slice {
        DataSlice::FileContents { pattern } if pattern == "all" => {
            report_progress("workdir", 0, 0);
            let metas = repo.workdir_file_metas()?;
            let metas: Vec<_> = metas
                .into_iter()
                .filter(|m| !ignore.is_ignored(&m.path))
                .collect();
            ctx.insert_slice(slice.clone(), SliceData::FileContents(metas));
            report_progress("workdir", 1, 1);
        }
        DataSlice::FullHistory => { /* 历史采集,批次级进度回调 */ }
        _ => {}
    }
}

// check 阶段:逐检查器纯计算产出 findings,每个检查器完成即上报进度。
let total_checkers = registry.list().len();
report_progress("check", 0, total_checkers);
let mut findings = Vec::new();
for (i, checker) in registry.list().iter().enumerate() {
    findings.extend(checker.check(&ctx));
    report_progress("check", i + 1, total_checkers);
}

// 评分:按各维度 finding 严重度扣分。
let scores = compute_scores(&findings);

评分规则同样简单透明:每个 finding 按严重度扣分(Critical -15 / Warning -8 / Info -3),各维度独立计算并 clamp 到 0, 100,无 finding 的维度保持 100------所以 flutter-ledger 那份报告里「结构 0 分」的雷达凹角,就是 46 条 Warning × 8 分扣穿的结果。

4.3 历史对比:同一仓库不同时期的体检报告 diff

报告页之外还有一个「历史对比」视图:每次扫描自动落入历史(最多保留 20 条),任意两条可标记为基准/对照做 diff------总分变化、发现项变化、新增/消除/仍存在的 finding 一目了然。双语切换(中/EN)也是一期迭代进去的:

这份对比里基准是 flutter-ledger(46 项发现)、对照是 FlowTime(98 项发现):总分同为 80,但发现项构成完全不同------新增 59 条大文件警告全部来自 FlowTime 的构建产物。对「体检 → 清理 → 复扫」的修复闭环来说,这个 diff 视图就是验收凭证。

双语切换的效果(同一页面中/英文两套文案,localStorage 持久化):

4.4 CLI 通道

不会装桌面应用的评审走 CLI。以下是 cargo run -p rb-core -- scan ./demo-repo 的真实终端输出(demo-repo 是内置合成数据仓库,预置一个 5MB 的 big.bin 与一个未合并分支 old-branch):

bash 复制代码
$ cargo run -p rb-core -- scan ./demo-repo

仓衡 (rb) 仓库摘要: /Users/dickeryang/Desktop/projects/repo-balance/demo-repo/
  提交数: 3
  分支数: 2 (main, old-branch)
  文件数: 3
  总大小: 5242969 bytes
  体检结论: 共 1 条 finding
  [Warning]  big-files.worktree:big.bin --- 工作区存在大文件: big.bin
        路径: big.bin
        当前大小: 5.0 MB
        历史最大: 5.0 MB
        首次引入: 5c4a1a21 chore: big file
        建议: 建议将 big.bin 加入 .gitignore,或使用 git lfs migrate import --include="big.bin" 迁移到大文件存储

一条命令、无需桌面环境,报告结构(finding → 证据 → 可执行建议)与桌面应用完全同源------这就是「内核可独立编译为 CLI」这条架构决策兑现的评审通道。

五、踩坑记录:AI 写对了九成,剩下一成靠人

如实记录开发过程中真实遇到的问题(坑位与数据以开发日志为准,成稿时填入实测细节)。

1:git2 类型逃逸出门面,测试写不下去

现象 :D1 的分支健康规则写单测时发现,AI 生成的 Checker 直接持有了 git2::Repository 引用------单测必须构造真实 libgit2 仓库对象,生命周期与清理纠缠不清,测试写了三版都不干净。

根因:话术里写了「GitRepo 门面」,但没写死「git2 类型不得逃逸出 git 模块」,AI 按最短路径把底层类型透传给了上层。

解决 :用 8.2 重构话术让 AI 列出全部逃逸点,收拢进 GitRepo 门面,对 Checker 只暴露自有 RepoSnapshot 只读模型(借出数据,不借出句柄)。重构后单测只需要构造快照数据,干净了一个数量级。这条纪律随后被固化为每期终检的搜索项。

重构后的边界是可以用一条搜索验证的:全仓库 grep git2:: 只会命中 crates/rb-core/src/git/ 下的文件(外加 Cargo.toml 的依赖声明)------这正是 3.2 节贴出的 GitRepo 门面代码。模块 mod.rs 的头注释也把这个约束写死了:

rust 复制代码
// crates/rb-core/src/checker/mod.rs 模块头(真实源码)
//! checker 模块:检查器插件体系(契约、注册表、切片计划、扫描上下文、占位实现)。
//!
//! 模块依赖方向:mod → plan/context → model/error,noop → mod,无循环依赖;
//! 本模块不 import git2、不引入 tauri,全部公共类型经 `crate::checker::`
//! 单一命名空间导出。

教训:「封装某依赖」这类约束必须写成「类型不得出现在哪些文件」的可搜索规则,否则 AI 的「封装」只是包了一层皮。

2:Tauri capability 漏配,前端调用 dialog 无响应

现象:竖切联调时,前端点「选择目录」毫无反应------没有报错、没有弹窗,像按钮没绑事件。

根因 :Tauri 2 的权限模型是显式 capability:dialog:open 权限没有加进 capabilities 配置,IPC 调用被静默拒绝。AI 生成的配置文件少了这一项,且静默失败让排查方向先在事件绑定上绕了两圈。

解决:补上 capability 后恢复正常;排障话术中追加「capability 权限名是否为 Tauri 2 当前版本的准确拼写(对照官方文档核实)」检查项。

教训:Tauri 2 的显式权限模型下,「静默拒绝」是配置类问题最迷惑的表现------无报错不等于代码没执行到,先查 capability 再查业务逻辑。

修复后的配置长这样------三个权限就是全部能力面,文件读取始终在 Rust 侧,前端拿不到任意 fs 访问:

json 复制代码
// crates/rb-app/capabilities/default.json(真实源码)
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "最小能力集:窗口管理 + 目录对话框 + 事件机制",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:allow-open",
    "core:event:default"
  ]
}

注意权限名是 dialog:allow-open 而不是文档里常见的 dialog:open------Tauri 2 的权限粒度是「每个 dialog 方法一条 allow 规则」,当时的坑正是栽在这种拼写差异上。这个文件本身就是「capabilities 最小能力集」红线的活证据:没装 fs 插件,CSP 收紧为 'self'。

3:敏感信息规则误报,password = "" 也中招

现象 :敏感信息扫描首次跑真实仓库,一批测试代码里的 password = "" 空赋值、示例文件里的占位符全部报 Warning,误报多到报告没法看。

根因:初版正则只匹配「键名 + 赋值」结构,没有值有效性判断与路径降噪------AI 按「规则覆盖面」实现,而真正决定可用性的是「误报率」。

解决:三条降噪规则落地:空值不报、example/sample/test 文件名中的命中降为 Info、高熵判断过滤纯占位符。用真实误报样本调完规则后,报告信噪比恢复正常。调参过程全程走掩码 evidence,未在会话中展开过任何真实命中内容。

教训:检测类工具的验收指标是误报率不是规则数量------「每条规则一命中一不命中」的用例结构(话术中预先写死)是控制误报最便宜的时机。

5.4 小结

三个坑依旧符合系列规律:AI 的失误都发生在「话术没约束到的地方」------模块边界的可搜索定义、平台权限模型的静默语义、检测工具的验收指标定义。架构决策与验收标准由人写死,AI 在约束内执行。

质量收口的实测数据(2026-09-30 本机实测):

  • cargo test 全绿(rb-core 56 + rb-app 6,共 62 个用例,0 失败);
  • cargo clippy --all-targets 零 warning;非测试代码零 unwrap/expect(搜索确认);
  • rb-core 零 tauri 依赖;全仓库零网络请求代码(reqwest/ureq/fetch 均不存在);
  • 自我扫描:对 repo-balance 仓库本体跑 scan .,引擎正常出报告(构建产物 target/ 下的中间文件会被规则命中,配合 .repobalance-ignore 排除后报告干净)。

六、提效数据:AI 辅助开发到底快了多少

如实给出本次开发的提效记录(基于开发日志的实际耗时统计,为个人单项目样本,仅供参考)。

开发阶段 纯手写预估 AI 辅助实际 提效来源
workspace 骨架 + GitRepo 门面 约 1.5 天 约 2.5 小时 模板与绑定类代码收益最大
大文件检查器 + 引擎编排 + 单测 约 3 天 约 6 小时 规则是纯函数,AI 强项
Tauri 壳 + 前端竖切 约 2 天 约 5 小时 借用检查器往返是额外成本
报告页 + 历史/对比/i18n + 质量收口 + 文档 约 2.5 天 约 6 小时 迭代功能小而密,自查清单执行快

开发节奏与交付物在 git 历史里完全可溯------功能提交的粒度大致是一天 1~2 个特性(以下为真实 commit 序列节选):

text 复制代码
$ git log --oneline(节选)
de81c14 fix: begin_scan 竞态 + 雷达图维度名 i18n + CI 扫描路径
6995a3b fix: i18n 阶段名中英混合 + 历史删除选择残留
a190db2 feat: 国际化 - 中英文切换(极简 i18n,localStorage 持久化)
9db4038 feat: CI 集成 - --ci/--json 标志 + GitHub Actions/AtomGit 流水线示例
a620d24 feat: 忽略规则 - .repobalance-ignore gitignore 风格路径排除
text 复制代码
$ cargo test(workspace 全量)
running 56 tests ...
test result: ok. 56 passed; 0 failed  (rb-core)

running 6 tests ...
test result: ok. 6 passed; 0 failed  (rb-app)

两点如实说明:一是「纯手写预估」基于经验估算,存在主观误差;二是 Rust 路线的「编译器往返」是真实成本------借检报错喂回的每次往返约 1~2 分钟,本期发生约十余次,已计入实际耗时。综合感受:整体工期压缩到传统方式的四分之一左右;Rust 类型系统把一类运行时 bug(非法状态值)提前到了编译期,这在「AI 生成占比高」的项目里价值放大------类型即护栏。

七、开源地址与运行方式

项目已开源,包含可构建源代码、README(作品介绍)与开源协议:

项目仓库:https://atomgit.com/dickeryang/repo-balance.git(AtomGit 公开仓库)

本地运行两条通道:

bash 复制代码
# 通道一:CLI(无需桌面环境,一条命令出报告)
git clone https://atomgit.com/dickeryang/repo-balance.git
cd repo-balance
cargo build --workspace
cargo run -p rb-core -- scan ./demo-repo          # 内置合成数据示例仓库
cargo run -p rb-core -- scan ./你的仓库 --json     # 输出完整 JSON 报告

# 通道二:桌面应用(macOS)
# README 下载 dmg → 首次启动右键打开(Gatekeeper 说明见 README)→ 选择目录体检

仓库内附 docs/ci-integration.md(GitHub Actions / AtomGit 流水线示例,CLI 支持 --ci/--json/--quiet 标志,CI 模式下存在 Critical 时退出码 2)与 docs/prototype/(产品原型 HTML)。示例仓库中所有「敏感信息」均为 SAMPLE-NOT-A-REAL-KEY 类合成数据。项目采用 Apache-2.0 协议开源。

八、总结与平台使用建议

8.1 结论

第四种端形态、第四门语言,方法依然成立:在「人定架构与验收、AI 做实现」的分工下,Rust + Tauri 桌面应用一周完成。

本期的新发现有两个:一是借用检查器意外地成为了 AI 协作的「第三方质检员」------报错原文喂回即收敛,且收敛后的代码比一次生成的更符合 Rust 惯例;二是「内核可独立编译为 CLI」这个架构决策同时服务了可测试性、CI 集成与评审体验,是本期性价比最高的一个决策。

8.2 给平台的建议

四周连续使用下来,对 CodeArts 代码智能体的几点优化建议(如实反馈,供官方参考):

  1. Rust 生态语料:libgit2 这类绑定库的生命周期模式建议有专项感知,减少借检往返;生成代码标注「依据的 crate 版本」会很有帮助;
  2. 配置类文件的文档对齐:Tauri capabilities 这类版本敏感配置,希望 Agent 主动提示与官方文档核对(与第 3 期权限名建议同源);
  3. 项目级约定持久化(连续三期提出,期待最高):模块边界、掩码纪律这类项目级约束,若能持久化就不必每条话术重复投喂。

8.3 写在最后

四种端形态(Web 全栈 / 微信小程序 / 鸿蒙原生 / 桌面应用)、四门语言主线、同一套方法。Rust 这一期额外验证了一件事:AI 协作的质量下限,取决于人把约束写得多可验收------「封装」要写成「类型不得逃逸出哪个目录」,「安全」要写成「掩码在构造时完成」。

收官期(第 5 期,10-12 ~ 10-18)将是零依赖 TypeScript + Canvas 的小游戏「熵减特勤局」,确定性回放引擎登场------一整局游戏就是一个测试用例。如果你也在参加本期华为云码道 AI 编程挑战赛,欢迎在评论区交流。


说明:因 AtomGit 的码道额度用尽,本文部分功能使用码道客户端进行开发,其余功能通过码道 Web 端完成。
觉得有用就点个赞,有疑问或不同看法欢迎评论区交流。

相关推荐
miofly1 小时前
GitHub 今日推荐|lightcraft:纯 Rust 重写的 RAW 照片开发工具
rust·开源·github
geovindu18 小时前
rust: Abstract Factory pattern
开发语言·后端·设计模式·rust·抽象工厂模式
Hi202402171 天前
同一个 NMS 算子,两种语言实现:CUDA C 与 Rust 生成 LLVM IR 和 Vortex 汇编的差异
c语言·汇编·rust·gpgpu·vortex·cudac
滕州市燕猫虎计算机科技工作室个体工商户1 天前
《Rust程序设计》学习笔记一
开发语言·rust·学习笔记·rust程序设计
小道士写程序1 天前
Windows Rust 环境安装指导
开发语言·windows·rust
挖掘狂人2 天前
Git 从 0 到 1:用一个小项目走完 add / commit / reset / merge / rebase / push
git·后端·github
codigger2 天前
Git 三区域模型:把 add、commit、reset、merge 一次讲透
git·github·编程·编程语言
水饺编程2 天前
第1章:开发环境搭建,在 Windows 中安装 Git
linux·c语言·汇编·git·ubuntu