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:
parent
2246df8e2a
commit
5e824027e5
@ -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
|
||||
|
||||
@ -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)]
|
||||
@ -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()));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1633,6 +1633,25 @@ export interface AgentInfraStack {
|
||||
export const scanAgentInfraStack = (projectPath: string) =>
|
||||
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 {
|
||||
total: number;
|
||||
conventional: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user