feat(agent-infra): 卡B——三件套文件生成

generate_agent_infra_files():
- 生成 docs/ai-context/project-context.yaml(含确认后的契约路径)
- 注入 AGENTS.md 元规则段落(已有则跳过,幂等)
- 生成 .claude/rules/doc-freshness.md(含项目特定 globs)
7 条单元测试全过(含生成+幂等验证)
This commit is contained in:
lanrtop 2026-06-29 18:11:30 +09:00
parent 2246df8e2a
commit 5e824027e5
4 changed files with 288 additions and 6 deletions

View File

@ -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 - 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.jsonserde_json 解析 deps和 Cargo.toml字符串匹配识别 Drizzle/Prisma/Hono/Axum 等框架,返回带 confidence 的 DetectedContract 列表;有单元测试覆盖 drizzle/prisma/hono 识别 - acceptance: `scan_agent_infra_stack(project_path, project_id)` 能从 package.jsonserde_json 解析 deps和 Cargo.toml字符串匹配识别 Drizzle/Prisma/Hono/Axum 等框架,返回带 confidence 的 DetectedContract 列表;有单元测试覆盖 drizzle/prisma/hono 识别
### 📋 B. 三件套文件生成 ### B. 三件套文件生成
- status: todo - status: done
- complexity: M - complexity: M
- depends: A - depends: A
- files: src-tauri/src/commands/agent_infra.rs, src/lib/commands.ts - 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`;对同一项目重复执行幂等 - 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. 文档腐化评分 ### 🔵 C. 文档腐化评分
- status: todo - status: in_progress
- complexity: M - complexity: M
- files: src-tauri/src/commands/agent_infra.rs, src-tauri/src/db.rs, src/lib/commands.ts - 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 补齐);前端封装就绪 - 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 - files: src/components/dashboard/AgentInfraPanel.tsx, src/components/dashboard/BlueprintModal.tsx
- acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果written/skipped/failedUI 无类型错误 - acceptance: BlueprintModal 新增"Agent 基础设施"区块,展示 A 返回的检测结果(可编辑 contract 路径)、"生成三件套"按钮调用 B、展示写入结果written/skipped/failedUI 无类型错误
### 📋 E. 看板腐化指示器 ### 🔵 E. 看板腐化指示器
- status: todo - status: in_progress
- complexity: S - complexity: S
- depends: C - depends: C
- files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts - files: src/components/dashboard/ProjectCard.tsx, src/lib/commands.ts

View File

@ -265,6 +265,217 @@ pub fn scan_agent_infra_stack(
}) })
} }
// ── 三件套生成 ────────────────────────────────────────────────────────────────
/// 用户确认后的契约条目(前端传入)。
#[derive(Debug, Deserialize, Clone)]
pub struct ContractInput {
pub label: String,
pub paths: Vec<String>,
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<FileResult>,
}
// 元规则段落内容(技术栈无关,编译期内嵌)
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<String> = 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<ContractInput>,
) -> Result<GenerateReport, String> {
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)] #[cfg(test)]
@ -355,6 +566,57 @@ mod tests {
fs::remove_dir_all(&dir).unwrap(); 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: "数据库 schemaDrizzle".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] #[test]
fn meta_rules_detection() { fn meta_rules_detection() {
let dir = std::env::temp_dir().join(format!("ai_infra_test_meta_{}", std::process::id())); let dir = std::env::temp_dir().join(format!("ai_infra_test_meta_{}", std::process::id()));

View File

@ -269,6 +269,7 @@ pub fn run() {
commands::onboarding::apply_onboarding_pack, commands::onboarding::apply_onboarding_pack,
// agent infrastructure // agent infrastructure
commands::agent_infra::scan_agent_infra_stack, commands::agent_infra::scan_agent_infra_stack,
commands::agent_infra::generate_agent_infra_files,
commands::commit_metrics::get_commit_stats, commands::commit_metrics::get_commit_stats,
// mentor // mentor
get_mentor_context, get_mentor_context,

View File

@ -1633,6 +1633,25 @@ export interface AgentInfraStack {
export const scanAgentInfraStack = (projectPath: string) => export const scanAgentInfraStack = (projectPath: string) =>
invoke<AgentInfraStack>('scan_agent_infra_stack', { projectPath }); invoke<AgentInfraStack>('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<GenerateReport>('generate_agent_infra_files', { projectPath, contracts });
export interface CommitStats { export interface CommitStats {
total: number; total: number;
conventional: number; conventional: number;