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:
parent
2074225f79
commit
f4ff83d94e
@ -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 helper(created/updated/appended 三态),helper 有 tempdir 单元测试;SyncResult 新增 agents_md_action 字段;cargo test + typecheck 通过
|
||||||
|
|
||||||
|
> 背景:批量同步后用户发现所有项目只落了 CLAUDE.md——跨工具内容被锁进单一工具私有文件,违反宪章「跨工具单一来源 = AGENTS.md」。
|
||||||
|
|
||||||
|
|||||||
@ -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/` 目录维护项目全景架构图,驱动需求讨论和任务管理。
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user