feat(governance): 规则分发跨工具化——AGENTS.md 成为蓝图工作流单一来源

sync_blueprint_rules 改为双文件写入:
- AGENTS.md:完整蓝图工作流 MANAGED 块(开放标准,Qwen/Cursor/Copilot 均读),
  措辞去 Claude 专属化("Claude 应做的"→"AI 应做的",技能引用改为通用流程描述)
- CLAUDE.md:MANAGED 块瘦身为 @AGENTS.md 薄引用(保留 version marker,
  版本检测不受影响),内容永不分叉
- 抽 upsert_managed_block 公共 helper(created/updated/appended 三态),
  附 tempdir 单元测试;SyncResult 新增 agents_md_action

背景:批量同步后发现所有项目只落 CLAUDE.md,跨工具内容被锁进单一工具
私有文件,违反宪章「跨工具单一来源 = AGENTS.md」。
下次批量同步时全部项目自动迁移到新结构(marker 机制天然幂等)。

Rules-Applied: R01, R07
This commit is contained in:
lanrtop 2026-07-02 19:49:22 +09:00
parent 2074225f79
commit f4ff83d94e
4 changed files with 120 additions and 34 deletions

View File

@ -74,3 +74,12 @@ progress: 100
- files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts - files: src/components/dashboard/BlueprintModal.tsx, src/lib/commands.ts
- depends: blueprint-governance - depends: blueprint-governance
- acceptance: GovernancePanel 新增规则来源 badge「内置基线 vX.X.X」或「用户自定义 vX.X.X」新增「应用梦核建议」区域文本框用于粘贴新 CONVENTIONS 内容 + 确认按钮,确认后调用 save_user_conventions成功显示「已同步 N 个项目」;有用户版时显示「重置为基线」按钮,调用 reset_conventions_to_builtin 后来源恢复为内置 - acceptance: GovernancePanel 新增规则来源 badge「内置基线 vX.X.X」或「用户自定义 vX.X.X」新增「应用梦核建议」区域文本框用于粘贴新 CONVENTIONS 内容 + 确认按钮,确认后调用 save_user_conventions成功显示「已同步 N 个项目」;有用户版时显示「重置为基线」按钮,调用 reset_conventions_to_builtin 后来源恢复为内置
### ✅ 跨工具规则分发AGENTS.md 单一来源
- status: done
- complexity: M
- files: src-tauri/src/commands/blueprint.rs, src/lib/commands.ts
- acceptance: sync_blueprint_rules 将完整蓝图工作流 MANAGED 块写入 AGENTS.md开放标准Qwen/Cursor/Copilot 均读),措辞去 Claude 专属化CLAUDE.md 的 MANAGED 块瘦身为 @AGENTS.md 引用(保留 version marker 使版本检测不受影响);两文件共用 upsert helpercreated/updated/appended 三态helper 有 tempdir 单元测试SyncResult 新增 agents_md_action 字段cargo test + typecheck 通过
> 背景:批量同步后用户发现所有项目只落了 CLAUDE.md——跨工具内容被锁进单一工具私有文件违反宪章「跨工具单一来源 = AGENTS.md」。

View File

@ -47,7 +47,7 @@ docs/templates/ 炼境接入包模板草案agents-meta-rules
- 代码重构/简化:/simplify → /code-review - 代码重构/简化:/simplify → /code-review
- 发现深层问题:/Dev-deep-think - 发现深层问题:/Dev-deep-think
<!-- BLUEPRINT_MANAGED_START version="1.8.0" --> <!-- BLUEPRINT_MANAGED_START version="1.9.0" -->
## 项目蓝图 ## 项目蓝图
本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。 本项目使用 `.blueprint/` 目录维护项目全景架构图,驱动需求讨论和任务管理。

View File

@ -84,13 +84,14 @@ fn parse_frontmatter_version_str(content: &str) -> Option<String> {
None None
} }
/// CLAUDE.md 蓝图管理区模板,版本号使用当前生效的 CONVENTIONS 版本。 /// AGENTS.md 蓝图管理区模板(跨工具单一来源,措辞工具无关),版本号使用当前生效的 CONVENTIONS 版本。
fn claude_managed_template() -> String { fn agents_managed_template() -> String {
let ver = effective_conventions_version(); let ver = effective_conventions_version();
format!(r#"<!-- BLUEPRINT_MANAGED_START version="{ver}" --> format!(r#"<!-- BLUEPRINT_MANAGED_START version="{ver}" -->
## ##
使 `.blueprint/` 使 `.blueprint/`
AI Claude / Qwen / Cursor / Copilot
### ###
@ -99,7 +100,7 @@ fn claude_managed_template() -> String {
### ###
| | Claude | | | AI |
|----------|--------------| |----------|--------------|
| "我想加个 XX 功能" | manifest status: concept `.blueprint/modules/<id>.md` | | "我想加个 XX 功能" | manifest status: concept `.blueprint/modules/<id>.md` |
| "XX 功能需要 A、B、C" | | | "XX 功能需要 A、B、C" | |
@ -112,9 +113,9 @@ fn claude_managed_template() -> String {
| complexity | | | complexity | |
|-----------|---------| |-----------|---------|
| S / M | `files` + `acceptance` + `complexity` 📋 | | S / M | `files` + `acceptance` + `complexity` 📋 |
| L | `/architect` `/splitter` S/M | | L | Claude `/architect` CONVENTIONS S/M |
> L `/architect` `/splitter` > L
### ###
@ -125,6 +126,22 @@ fn claude_managed_template() -> String {
<!-- BLUEPRINT_MANAGED_END -->"#) <!-- BLUEPRINT_MANAGED_END -->"#)
} }
/// CLAUDE.md 蓝图管理区模板:薄引用,指向 AGENTS.md 单一来源。
/// 保留 version marker 使 get_blueprint_status 的版本检测继续工作。
fn claude_managed_template() -> String {
let ver = effective_conventions_version();
format!(r#"<!-- BLUEPRINT_MANAGED_START version="{ver}" -->
##
@AGENTS.md BLUEPRINT_MANAGED
1. `.blueprint/manifest.yaml`
2. `.blueprint/CONVENTIONS.md`
3. AGENTS.md
<!-- BLUEPRINT_MANAGED_END -->"#)
}
// ── manifest.yaml 对应结构 ─────────────────────────────────────────────────── // ── manifest.yaml 对应结构 ───────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -1049,13 +1066,41 @@ pub struct SyncResult {
pub success: bool, pub success: bool,
pub conventions_written: bool, pub conventions_written: bool,
pub claude_md_action: String, // created | appended | updated | skipped pub claude_md_action: String, // created | appended | updated | skipped
pub agents_md_action: String, // created | appended | updated
pub message: String, pub message: String,
} }
/// 在指定文件中写入/替换 BLUEPRINT_MANAGED 块:
/// 文件不存在 → 用 header + 模板创建;已有 MANAGED 区 → 原位替换;否则追加。
/// 返回动作created | updated | appended
fn upsert_managed_block(
path: &Path,
template: &str,
header_when_created: &str,
) -> Result<String, String> {
let label = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
if !path.exists() {
let content = format!("{header_when_created}\n\n{template}\n");
std::fs::write(path, content).map_err(|e| format!("创建 {label} 失败: {e}"))?;
return Ok("created".to_string());
}
let content = std::fs::read_to_string(path).map_err(|e| format!("读取 {label} 失败: {e}"))?;
if content.contains("<!-- BLUEPRINT_MANAGED_START") {
let new_content = replace_managed_section(&content, template);
std::fs::write(path, new_content).map_err(|e| format!("更新 {label} 失败: {e}"))?;
Ok("updated".to_string())
} else {
let new_content = format!("{}\n\n{template}\n", content.trim_end());
std::fs::write(path, new_content).map_err(|e| format!("追加 {label} 失败: {e}"))?;
Ok("appended".to_string())
}
}
#[tauri::command] #[tauri::command]
pub fn sync_blueprint_rules(project_path: String) -> Result<SyncResult, String> { pub fn sync_blueprint_rules(project_path: String) -> Result<SyncResult, String> {
let root = Path::new(&project_path); let root = Path::new(&project_path);
let bp_dir = root.join(".blueprint"); let bp_dir = root.join(".blueprint");
let project_name = root.file_name().and_then(|n| n.to_str()).unwrap_or("Project").to_string();
// 确保 .blueprint/ 存在 // 确保 .blueprint/ 存在
std::fs::create_dir_all(&bp_dir) std::fs::create_dir_all(&bp_dir)
@ -1065,40 +1110,29 @@ pub fn sync_blueprint_rules(project_path: String) -> Result<SyncResult, String>
std::fs::write(bp_dir.join("CONVENTIONS.md"), effective_conventions_content()) std::fs::write(bp_dir.join("CONVENTIONS.md"), effective_conventions_content())
.map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?; .map_err(|e| format!("写入 CONVENTIONS.md 失败: {e}"))?;
// 2. 处理 CLAUDE.md // 2. AGENTS.md完整蓝图工作流跨工具单一来源Qwen/Cursor/Copilot 均读)
let claude_path = root.join("CLAUDE.md"); let agents_action = upsert_managed_block(
let managed_template = claude_managed_template(); &root.join("AGENTS.md"),
let claude_action = if !claude_path.exists() { &agents_managed_template(),
// 创建新文件 &format!("# AGENTS.md — {project_name}\n\n> 跨工具 AI 上下文入口Claude / Qwen / Cursor / Copilot 等均读此文件)。"),
let content = format!("# {}\n\n{managed_template}\n", )?;
root.file_name().and_then(|n| n.to_str()).unwrap_or("Project"));
std::fs::write(&claude_path, content)
.map_err(|e| format!("创建 CLAUDE.md 失败: {e}"))?;
"created".to_string()
} else {
let content = std::fs::read_to_string(&claude_path)
.map_err(|e| format!("读取 CLAUDE.md 失败: {e}"))?;
if content.contains("<!-- BLUEPRINT_MANAGED_START") { // 3. CLAUDE.md薄引用Claude 经 @AGENTS.md 接入单一来源)
// 替换现有 MANAGED 区 let claude_action = upsert_managed_block(
let new_content = replace_managed_section(&content, &managed_template); &root.join("CLAUDE.md"),
std::fs::write(&claude_path, new_content) &claude_managed_template(),
.map_err(|e| format!("更新 CLAUDE.md 失败: {e}"))?; &format!("# {project_name}"),
"updated".to_string() )?;
} else {
// 追加 MANAGED 区
let new_content = format!("{}\n\n{managed_template}\n", content.trim_end());
std::fs::write(&claude_path, new_content)
.map_err(|e| format!("追加 CLAUDE.md 失败: {e}"))?;
"appended".to_string()
}
};
Ok(SyncResult { Ok(SyncResult {
success: true, success: true,
conventions_written: true, conventions_written: true,
claude_md_action: claude_action.clone(), claude_md_action: claude_action.clone(),
message: format!("CONVENTIONS.md 已同步至 v{}CLAUDE.md {claude_action}", effective_conventions_version()), agents_md_action: agents_action.clone(),
message: format!(
"CONVENTIONS.md 已同步至 v{}AGENTS.md {agents_action}CLAUDE.md {claude_action}",
effective_conventions_version()
),
}) })
} }
@ -2616,4 +2650,46 @@ mod tests {
let _ = fs::remove_dir_all(&tmp); let _ = fs::remove_dir_all(&tmp);
} }
// ── upsert_managed_block跨工具规则分发────────────────────────────
#[test]
fn upsert_managed_block_three_states() {
let dir = std::env::temp_dir().join(format!("lj-upsert-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("AGENTS.md");
let tpl_v1 = "<!-- BLUEPRINT_MANAGED_START version=\"1.0.0\" -->
A
<!-- BLUEPRINT_MANAGED_END -->";
let tpl_v2 = "<!-- BLUEPRINT_MANAGED_START version=\"2.0.0\" -->
B
<!-- BLUEPRINT_MANAGED_END -->";
// 1. 不存在 → created含 header
let a = upsert_managed_block(&f, tpl_v1, "# 头部").unwrap();
assert_eq!(a, "created");
let c = std::fs::read_to_string(&f).unwrap();
assert!(c.starts_with("# 头部"));
assert!(c.contains("内容A"));
// 2. 已有 MANAGED 区 → updated原位替换header 保留,旧内容消失)
let a = upsert_managed_block(&f, tpl_v2, "# 头部").unwrap();
assert_eq!(a, "updated");
let c = std::fs::read_to_string(&f).unwrap();
assert!(c.contains("内容B") && !c.contains("内容A"));
assert_eq!(c.matches("BLUEPRINT_MANAGED_START").count(), 1);
// 3. 存在但无 MANAGED 区 → appended既有内容保留
let g = dir.join("CLAUDE.md");
std::fs::write(&g, "# 手写内容
").unwrap();
let a = upsert_managed_block(&g, tpl_v1, "# 不该出现").unwrap();
assert_eq!(a, "appended");
let c = std::fs::read_to_string(&g).unwrap();
assert!(c.contains("自定义段落") && c.contains("内容A") && !c.contains("不该出现"));
let _ = std::fs::remove_dir_all(&dir);
}
} }

View File

@ -869,6 +869,7 @@ export interface SyncResult {
success: boolean; success: boolean;
conventions_written: boolean; conventions_written: boolean;
claude_md_action: string; claude_md_action: string;
agents_md_action: string;
message: string; message: string;
} }