From 5e824027e5def0842872a1b5885e60af678b2187 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Mon, 29 Jun 2026 18:11:30 +0900 Subject: [PATCH] =?UTF-8?q?feat(agent-infra):=20=E5=8D=A1B=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=B8=89=E4=BB=B6=E5=A5=97=E6=96=87=E4=BB=B6=E7=94=9F?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_agent_infra_files(): - 生成 docs/ai-context/project-context.yaml(含确认后的契约路径) - 注入 AGENTS.md 元规则段落(已有则跳过,幂等) - 生成 .claude/rules/doc-freshness.md(含项目特定 globs) 7 条单元测试全过(含生成+幂等验证) --- .blueprint/modules/agent-infrastructure.md | 12 +- src-tauri/src/commands/agent_infra.rs | 262 +++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/lib/commands.ts | 19 ++ 4 files changed, 288 insertions(+), 6 deletions(-) diff --git a/.blueprint/modules/agent-infrastructure.md b/.blueprint/modules/agent-infrastructure.md index bb4fdcc..caae551 100644 --- a/.blueprint/modules/agent-infrastructure.md +++ b/.blueprint/modules/agent-infrastructure.md @@ -92,15 +92,15 @@ - files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/commands/mod.rs, src-tauri/src/lib.rs, src/lib/commands.ts - acceptance: `scan_agent_infra_stack(project_path, project_id)` 能从 package.json(serde_json 解析 deps)和 Cargo.toml(字符串匹配)识别 Drizzle/Prisma/Hono/Axum 等框架,返回带 confidence 的 DetectedContract 列表;有单元测试覆盖 drizzle/prisma/hono 识别 -### 📋 B. 三件套文件生成 -- status: todo +### ✅ B. 三件套文件生成 +- status: done - complexity: M - depends: A - files: src-tauri/src/commands/agent_infra.rs, src/lib/commands.ts - acceptance: `generate_agent_infra_files(project_path, project_id, contracts)` 正确写入 `docs/ai-context/project-context.yaml`(含实际契约路径)、向 AGENTS.md 注入元规则段落(已有则跳过)、生成含项目特定 globs 的 `.claude/rules/doc-freshness.md`;对同一项目重复执行幂等 -### 📋 C. 文档腐化评分 -- status: todo +### 🔵 C. 文档腐化评分 +- status: in_progress - complexity: M - files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/db.rs, src/lib/commands.ts - acceptance: `scan_doc_freshness(project_path, project_id)` 用 `silent_cmd("git").current_dir(project_path)` 读取契约路径 vs `docs/ai-context/project-context.yaml` 的最后 commit 时间戳,project-context.yaml 不存在直接返回 RED;结果写入 `doc_freshness_cache` 表(migration 补齐);前端封装就绪 @@ -112,8 +112,8 @@ - files: src/components/dashboard/AgentInfraPanel.tsx, src/components/dashboard/BlueprintModal.tsx - acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果(written/skipped/failed);UI 无类型错误 -### 📋 E. 看板腐化指示器 -- status: todo +### 🔵 E. 看板腐化指示器 +- status: in_progress - complexity: S - depends: C - files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts diff --git a/src-tauri/src/commands/agent_infra.rs b/src-tauri/src/commands/agent_infra.rs index 0094fdf..ab1bc33 100644 --- a/src-tauri/src/commands/agent_infra.rs +++ b/src-tauri/src/commands/agent_infra.rs @@ -265,6 +265,217 @@ pub fn scan_agent_infra_stack( }) } +// ── 三件套生成 ──────────────────────────────────────────────────────────────── + +/// 用户确认后的契约条目(前端传入)。 +#[derive(Debug, Deserialize, Clone)] +pub struct ContractInput { + pub label: String, + pub paths: Vec, + pub doc: String, +} + +/// 单个文件的生成结果。 +#[derive(Debug, Serialize)] +pub struct FileResult { + /// 相对项目根的路径 + pub path: String, + /// "written" | "updated" | "skipped" | "failed" + pub action: String, + pub detail: String, +} + +/// generate_agent_infra_files 的完整返回值。 +#[derive(Debug, Serialize)] +pub struct GenerateReport { + pub files: Vec, +} + +// 元规则段落内容(技术栈无关,编译期内嵌) +const META_RULES_SECTION: &str = r#" +--- + +## Agent 友好工程四支柱 + +修改代码前,确认这个项目满足四支柱: + +**可导航**:凭文件名/目录名可在 30 秒内定位某功能的代码。通用能力有独立家,不寄生在业务模块下。 + +**可推理**:公共类型只在一处定义。改一个接口,类型检查能报出所有受影响处。 + +**可验证**:typecheck / lint / test 各有一条命令真能跑。关键路径有冒烟测试。 + +**可记忆**:规则按场景注入(不是一份巨型文档)。决策只写"为什么",代码自己回答"是什么"。 + +--- + +## 文档防腐元规则 + +做任何改动时,判断它属于哪类变更: + +### T1 外部契约变更(最高优先级) +其他代码、服务、或 agent 依赖的定义——接口、共享类型、数据结构、配置键名、环境变量名。 +→ 查 `docs/ai-context/project-context.yaml` 找到记录该契约的文档,检查并同步更新。 + +### T2 架构边界变更 +模块划分、目录结构、依赖方向、包边界调整。 +→ 检查架构文档是否需要同步。 + +### T3 工具链变更 +引入或替换库、框架、构建工具。 +→ 更新 AGENTS.md 技术栈声明;通知炼境重新扫描并更新 `project-context.yaml`。 + +### T4 常驻上下文过期 +AGENTS.md 或 `docs/ai-context/project-brief.md` 描述的现实已经改变。 +→ 立即更新。这两份文档常驻 agent 上下文,过期危害最大。 + +### 不触发检查 +内部重构、性能优化、bug fix(外部行为不变)、测试文件、临时脚本。 + +--- + +## 文档防腐铁律 + +- 文档只写稳定的:为什么、铁律、意图、架构决策。 +- 易变的(端口、路由、表名、命令清单)交给工具自省,不写死进常驻文档。写死 = 定时炸弹。 +- 单一事实来源:两处说同一事时,删掉派生的那处。 +- 两份文档打架时,信代码,不信文档。 + +--- + +## 定期防腐自查 + +每隔一阵或大改后,对照以下清单: + +- [ ] AGENTS.md 里的技术栈声明还和代码一致吗? +- [ ] `project-brief.md` 最近 30 天有没有随架构改动更新? +- [ ] 有没有两份文档说同一件事且打架? +- [ ] `project-context.yaml` 的契约路径还准确吗? +"#; + +fn write_file(path: &std::path::PathBuf, content: &str, rel: &str) -> FileResult { + if let Some(parent) = path.parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + return FileResult { + path: rel.into(), + action: "failed".into(), + detail: format!("建目录失败: {e}"), + }; + } + } + match std::fs::write(path, content) { + Ok(_) => FileResult { path: rel.into(), action: "written".into(), detail: "已写入".into() }, + Err(e) => FileResult { path: rel.into(), action: "failed".into(), detail: format!("写入失败: {e}") }, + } +} + +/// 生成 docs/ai-context/project-context.yaml +fn gen_context_yaml(root: &Path, contracts: &[ContractInput]) -> FileResult { + let rel = "docs/ai-context/project-context.yaml"; + let target = root.join(rel); + let existed = target.exists(); + + let mut yaml = String::from( + "# 项目契约路径映射\n\ + # 由炼境根据技术栈扫描生成,技术栈变更后重新运行接入包生成\n\ + # 供 AGENTS.md 元规则 T1 查询:「哪个文件是这个项目的外部契约?」\n\n\ + contracts:\n", + ); + for c in contracts { + yaml.push_str(&format!(" - label: {}\n paths:\n", c.label)); + for p in &c.paths { + yaml.push_str(&format!(" - \"{p}\"\n")); + } + yaml.push_str(&format!(" doc: \"{}\"\n\n", c.doc.replace('"', "'"))); + } + yaml.push_str( + "architecture_docs:\n - \"AGENTS.md\"\n\n\ + agent_docs:\n - \"AGENTS.md\"\n - \"docs/ai-context/project-brief.md\"\n", + ); + + let mut result = write_file(&target, &yaml, rel); + if result.action == "written" && existed { + result.action = "updated".into(); + result.detail = "已更新(覆盖上次生成版本)".into(); + } + result +} + +/// 向 AGENTS.md 注入元规则段落(已有则跳过,幂等) +fn inject_meta_rules(root: &Path) -> FileResult { + let rel = "AGENTS.md"; + let target = root.join(rel); + + match std::fs::read_to_string(&target) { + Ok(content) if content.contains("## 文档防腐元规则") => FileResult { + path: rel.into(), + action: "skipped".into(), + detail: "元规则段落已存在,未修改".into(), + }, + Ok(mut content) => { + content.push_str(META_RULES_SECTION); + match std::fs::write(&target, content) { + Ok(_) => FileResult { + path: rel.into(), + action: "updated".into(), + detail: "已追加元规则段落".into(), + }, + Err(e) => FileResult { + path: rel.into(), + action: "failed".into(), + detail: format!("写入失败: {e}"), + }, + } + } + Err(_) => FileResult { + path: rel.into(), + action: "skipped".into(), + detail: "AGENTS.md 不存在,请先应用接入包(v1)".into(), + }, + } +} + +/// 生成 .claude/rules/doc-freshness.md(含项目特定 globs) +fn gen_doc_freshness_rule(root: &Path, contracts: &[ContractInput]) -> FileResult { + let rel = ".claude/rules/doc-freshness.md"; + let target = root.join(rel); + + // 收集所有非 glob 的具体文件路径作为 globs frontmatter + let globs: Vec = contracts + .iter() + .flat_map(|c| c.paths.iter().cloned()) + .collect(); + let globs_str = globs.join(", "); + + let content = format!( + "---\nglobs: {globs_str}\n---\n\n\ + 这些是本项目的外部契约文件。修改时检查 `docs/ai-context/project-context.yaml` 是否需要同步更新。\n\n\ + 参考 AGENTS.md 元规则 **T1 外部契约变更**。\n" + ); + + write_file(&target, &content, rel) +} + +/// 根据用户确认的契约列表生成三件套文件。 +#[tauri::command] +pub fn generate_agent_infra_files( + project_path: String, + contracts: Vec, +) -> Result { + let root = Path::new(project_path.trim_end_matches(['/', '\\'])); + if !root.exists() { + return Err(format!("目录不存在: {}", root.display())); + } + + let files = vec![ + gen_context_yaml(root, &contracts), + inject_meta_rules(root), + gen_doc_freshness_rule(root, &contracts), + ]; + + Ok(GenerateReport { files }) +} + // ── 单元测试 ────────────────────────────────────────────────────────────────── #[cfg(test)] @@ -355,6 +566,57 @@ mod tests { fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn generate_creates_three_files() { + let dir = std::env::temp_dir().join(format!("ai_infra_test_gen_{}", std::process::id())); + setup(&dir); + // AGENTS.md 存在但无元规则 + write(&dir, "AGENTS.md", "# My Project\n\n## 项目概况\n\nsome content\n"); + + let contracts = vec![ + ContractInput { + label: "数据库 schema(Drizzle)".into(), + paths: vec!["drizzle/schema.ts".into()], + doc: "schema 变更需同步".into(), + }, + ContractInput { + label: "API 接口(Hono)".into(), + paths: vec!["src/routes/**".into()], + doc: "接口变更需同步".into(), + }, + ]; + + let report = generate_agent_infra_files(dir.to_str().unwrap().to_string(), contracts).unwrap(); + let actions: Vec<(&str, &str)> = report.files.iter() + .map(|f| (f.path.as_str(), f.action.as_str())) + .collect(); + + // project-context.yaml 写入 + assert!(actions.iter().any(|(p, a)| p.contains("project-context.yaml") && *a == "written"), + "应写入 project-context.yaml,实际: {actions:?}"); + // AGENTS.md 注入元规则 + assert!(actions.iter().any(|(p, a)| p.contains("AGENTS.md") && *a == "updated"), + "应追加元规则到 AGENTS.md,实际: {actions:?}"); + // .claude/rules/doc-freshness.md 写入 + assert!(actions.iter().any(|(p, a)| p.contains("doc-freshness.md") && *a == "written"), + "应写入 doc-freshness.md,实际: {actions:?}"); + + // 验证 doc-freshness.md 含 globs + let freshness = fs::read_to_string(dir.join(".claude/rules/doc-freshness.md")).unwrap(); + assert!(freshness.contains("drizzle/schema.ts"), "doc-freshness 应含契约路径"); + assert!(freshness.contains("globs:"), "doc-freshness 应有 globs frontmatter"); + + // 验证幂等:重复生成 AGENTS.md 应 skipped + let contracts2 = vec![ContractInput { + label: "L".into(), paths: vec!["x.ts".into()], doc: "d".into(), + }]; + let report2 = generate_agent_infra_files(dir.to_str().unwrap().to_string(), contracts2).unwrap(); + let agents_result = report2.files.iter().find(|f| f.path.contains("AGENTS.md")).unwrap(); + assert_eq!(agents_result.action, "skipped", "元规则已存在,第二次应跳过"); + + fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn meta_rules_detection() { let dir = std::env::temp_dir().join(format!("ai_infra_test_meta_{}", std::process::id())); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 31167c5..164671a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -269,6 +269,7 @@ pub fn run() { commands::onboarding::apply_onboarding_pack, // agent infrastructure commands::agent_infra::scan_agent_infra_stack, + commands::agent_infra::generate_agent_infra_files, commands::commit_metrics::get_commit_stats, // mentor get_mentor_context, diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 86a4e9c..28d36a0 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -1633,6 +1633,25 @@ export interface AgentInfraStack { export const scanAgentInfraStack = (projectPath: string) => invoke('scan_agent_infra_stack', { projectPath }); +export interface ContractInput { + label: string; + paths: string[]; + doc: string; +} + +export interface FileResult { + path: string; + action: 'written' | 'updated' | 'skipped' | 'failed'; + detail: string; +} + +export interface GenerateReport { + files: FileResult[]; +} + +export const generateAgentInfraFiles = (projectPath: string, contracts: ContractInput[]) => + invoke('generate_agent_infra_files', { projectPath, contracts }); + export interface CommitStats { total: number; conventional: number;