diff --git a/.blueprint/CONVENTIONS.md b/.blueprint/CONVENTIONS.md index 55f6f31..9fdae84 100644 --- a/.blueprint/CONVENTIONS.md +++ b/.blueprint/CONVENTIONS.md @@ -1,6 +1,6 @@ --- -version: 1.9.0 -updated: 2026-07-02 +version: 1.10.0 +updated: 2026-07-03 source: dev-manager-tauri --- @@ -928,6 +928,23 @@ scope 描述规则的正向适用范围,**boundaries 描述已知的反向边 - **evidence_count**: 1 - **derived_from**: 规则库只有正向 scope 无反例结构,失败信息流失导致置信度虚高、边界永远探测不到(2026-07-02 经验飞轮设计评审) +#### R12 — 边界单点实现:业务校验只写在能力本体层 +- **scope**: `universal` +- **触发条件**:实现或修改带业务边界的功能(哪些允许、什么条件下允许、非法时如何拒绝) +- **自动动作**:校验逻辑只写在能力本体层(API / command / 服务函数);UI 层只呈现状态(置灰/提示),不复制业务判断。发现前端 guard 承载业务边界时标记为需下沉 +- **confidence**: 0.85 +- **evidence_count**: 2 +- **derived_from**: 空契约生成静默 return(边界错写进 UI 层,与文案承诺冲突)、改 Token 假成功(UI 报成功但本体未执行)——两例均为"人与 agent 验证路径空间分裂"症状(2026-07-03) +- **boundaries**: 纯展示逻辑(排序、折叠、格式化)属 UI 职责,不适用本规则 + +#### R13 — M/L 任务完成前,验收路径须被真实走通 +- **scope**: `universal` +- **触发条件**:M 或 L 复杂度任务卡准备标记 ✅ done +- **自动动作**:acceptance 所述验证路径须由人或 agent **通过功能自身入口**真实走通一次(agent 按钮载体就近选择:Web 服务=HTTP API、CLI=命令、桌面应用=测试调用同一函数、UI 呈现层=Playwright 兜底)。禁止绕过功能手改产物冒充验证;agent 走通的关键路径顺手固化为一条测试(侦察→驻军),一次性验收不固化 +- **confidence**: 0.80 +- **evidence_count**: 2 +- **derived_from**: noteapp manifest 被手改导致 schema 约束修复未获真实验证、autoforphone 接线被命令行代办导致 G7 按钮未走通——手动替代掩盖功能真实状态(2026-07-03 用户裁决 + MCP 治理三件套 agent 实测首证) + ### 规则演进路径 ``` diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index a3642d8..17ebb14 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -6,7 +6,7 @@ description: > 成为每个项目的贴身导师。炼境本身亦被纳入其中, 形成可自我进化的开发智能闭环。 iteration: 1 -updated: 2026-07-02 +updated: 2026-07-03 @@ -369,8 +369,8 @@ modules: name: 接入包 area: backend status: in_progress - progress: 90 - note: "新增体系分发主线(2026-07-03):T1 模板版本化(前置)→ B1-B4 agent 按钮体系四资产" + progress: 95 + note: "体系分发主线(2026-07-03):T1 模板版本化 ✅ + B1 CONVENTIONS 1.10.0(R12/R13) ✅ + B2 AGENTS 模板 agent 按钮章节 ✅;剩 B3 提示词升级、B4 健康诊断遵守度" position: [3500, 0] - id: gitea-integration diff --git a/.blueprint/modules/onboarding-pack.md b/.blueprint/modules/onboarding-pack.md index 22932b1..33487ff 100644 --- a/.blueprint/modules/onboarding-pack.md +++ b/.blueprint/modules/onboarding-pack.md @@ -98,16 +98,18 @@ - files: src-tauri/src/commands/onboarding.rs, src-tauri/src/commands/commit_metrics.rs - acceptance: OnboardingReport 与飞轮展示标注该项目 commit 规范化比例(conventional / 总数);比例低时提示"种 lefthook 后数据才准",避免脏数据被误读为可信(CI 过滤 / 非规范单列已在 C1 落实) -### 📋 T1. 接入包模板版本化(ONBOARDED_MANAGED 块机制)【体系分发前置】 -- status: todo +### ✅ T1. 接入包模板版本化(ONBOARDED_MANAGED 块机制)【体系分发前置】 +- status: done +- done_note: 2026-07-03 dispatch_versioned 三态落地(written/updated/appended + 同版本 skipped),块外用户内容保护有测试兜底;AGENTS/git-workflow 模板入 ONBOARDING_MANAGED v1.1.0 块,lefthook 保持存在即跳过。对 enterprise-system/noteapp 的存量升级实测留给用户走 UI 流程(R13) - complexity: M - files: src-tauri/src/commands/onboarding.rs, src-tauri/resources/onboarding/git-workflow.md.tmpl, src-tauri/resources/onboarding/AGENTS.md.tmpl - acceptance: git-workflow 与 AGENTS.md 模板的炼境管理内容包进 ONBOARDING_MANAGED 标记块(带 version);分发逻辑三态:文件不存在→写全新;存在且有标记→原位更新块(复用 blueprint.rs 的 upsert_managed_block 模式,可提取公共函数);存在无标记→末尾追加块;块外用户内容永不触碰;lefthook.yml 保持"存在即跳过"(用户深度定制不适用块机制);单元测试覆盖三态 + 用户区保护;对 enterprise-system 重跑接入包实测存量升级 > 背景:模板演进无法触达存量项目是版本化闭环的最大缺口(trailer 章节曾手工补)。 -### 📋 agent 按钮体系分发(B1/B2 可派发,B3/B4 待拆) -- status: planned +### 🔵 agent 按钮体系分发(B1/B2 已完成,B3/B4 待做) +- status: in_progress +- progress: 2026-07-03 B1 done——CONVENTIONS 1.10.0 加 R12(边界单点实现)+ R13(M/L 验收路径须真实走通,含载体就近选择表);B2 done——AGENTS.md.tmpl v1.1.0 管理块含「agent 按钮与验证路径」章节(双投影/边界单点/载体表/完成的定义/侦察→驻军)。剩 B3(提示词升级,M,单独一轮)、B4(健康诊断遵守度,后置) - 版本化闭环要求(2026-07-03 用户澄清,体系分发的灵魂): 分发不是"发了就不管"——每类资产带版本分发 → 子项目按该版本实践产生经验(成功/失败)→ 经验自吸反哺炼境(trailer/failure_cases/复盘/返工率)→ 按版本分组对比效果 → 修订出新版 → 再分发。CONVENTIONS 是模范生(version+队列对比+梦核+同步,1.8→1.9 已跑通一整圈);B 系列每类资产按此标准建闭环。**前置依赖提升**:「接入包模板演进分发(MANAGED 块化)」concept 卡——模板无版本机制是当前最大缺口,没有它体系 v2 到不了存量项目 - notes: 2026-07-03 概念探讨收束后的分发需求(体系详见 memory concept_dual_projection 与 mcp-server 模块 concept 卡)。用户全 agent 开发形态下,"给开发者立规范"="给 agent 注入规范",可完全机器化。四条现成管道分发四类资产:① B1(S) CONVENTIONS 1.10.0 加 R12 边界单点实现 + R13 验收须真实走通验证路径 + 载体选择原则;② B2(S) 接入包 AGENTS.md 模板加 agent 按钮章节(三要素/载体谱系/侦察驻军);③ B3(M) init/sync 提示词升级——建蓝图时声明项目按钮载体、acceptance 生成为可执行验证路径格式;④ B4(S,后置) 健康诊断加体系遵守度检查。移植体验=零新增动作,接入流程不变。 diff --git a/src-tauri/resources/onboarding/AGENTS.md.tmpl b/src-tauri/resources/onboarding/AGENTS.md.tmpl index cf6d9c8..7e64e14 100644 --- a/src-tauri/resources/onboarding/AGENTS.md.tmpl +++ b/src-tauri/resources/onboarding/AGENTS.md.tmpl @@ -2,11 +2,21 @@ > 本文件遵循 [AGENTS.md 开放规范](https://agents.md/),供 Claude Code、Qwen Code、Cursor、Copilot 等 AI 工具读取。 > 由炼境接入包分发。请按项目实际补全下方 TODO 段落。 +> `ONBOARDING_MANAGED` 标记块由炼境版本化管理(重跑接入包可原位升级),块外内容永不触碰。 ## 项目概况 +## 架构与模块 + + + +## 关键文档索引 + + + + ## 核心开发约束 ### 包管理 @@ -22,12 +32,27 @@ ## Git 工作流 -见 `docs/ai-context/git-workflow.md`(分支策略 + Conventional Commits + PR 规范)。 +见 `docs/ai-context/git-workflow.md`(分支策略 + Conventional Commits + Rules-Applied trailer + PR 规范)。 **commit 必须符合 Conventional Commits**(lefthook 强制),炼境据此采集飞轮数据。 -## 架构与模块 +## agent 按钮与验证路径 - +功能的本体是代码层的能力;UI 按钮是它给人的投影,**agent 按钮**是给 agent 的投影 +(可发现 + 可调用 + 有边界的能力声明)。开发任何功能时遵守: + +1. **边界单点实现**:业务校验只写在能力本体层(API/command),UI 只呈现不复制逻辑—— + 否则人与 agent 的验证路径会分裂 +2. **载体就近选择**(本项目的 agent 按钮是什么,按项目类型对号入座): + | 项目类型 | agent 按钮载体 | + |---------|---------------| + | Web 服务 | HTTP API 本身(前端按钮与 curl 调同一 endpoint,天然双投影) | + | CLI/脚本 | 命令与 npm scripts | + | 桌面应用 | 测试直接调用 command 函数 | + | UI 呈现层 | Playwright 驱动真界面(最后手段,仅关键路径) | +3. **完成的定义**:功能实现后,由 agent 沿投影路径真实走通一次(起服务 → 调接口 → + 断言行为),再交人做 UI 终验——不许绕过功能手改产物冒充验证 +4. **侦察 → 驻军**:agent 走通的关键路径,顺手固化为一条测试(vitest / cargo test); + 一次性验收不固化(少而稳 > 多而脆) ## 多 AI CLI 协作 @@ -38,10 +63,7 @@ 2. 待验证:<需要确认的地方> 3. 约束继承:<影响后续的关键限制> ``` - -## 关键文档索引 - - + --- diff --git a/src-tauri/resources/onboarding/git-workflow.md.tmpl b/src-tauri/resources/onboarding/git-workflow.md.tmpl index f707e4a..0289e3b 100644 --- a/src-tauri/resources/onboarding/git-workflow.md.tmpl +++ b/src-tauri/resources/onboarding/git-workflow.md.tmpl @@ -1,6 +1,9 @@ # Git 工作流 — AI Agent 操作指南 > 把此文件粘贴给任何 AI Agent,让它了解本项目的 git 工作方式。由炼境接入包分发。 +> `ONBOARDING_MANAGED` 标记块由炼境版本化管理(重跑接入包可原位升级),块外可自由补充项目特有约定。 + + ## 分支策略 @@ -76,3 +79,4 @@ git status --short git log --oneline -10 git reset --soft HEAD~1 # 撤销最后一次 commit(保留改动) ``` + diff --git a/src-tauri/src/commands/onboarding.rs b/src-tauri/src/commands/onboarding.rs index 5735a68..489124c 100644 --- a/src-tauri/src/commands/onboarding.rs +++ b/src-tauri/src/commands/onboarding.rs @@ -1,12 +1,17 @@ //! 接入包模板分发(基线档)。 //! -//! 向项目种入 AGENTS.md / lefthook.yml / git-workflow.md 标准模板: -//! 文件不存在才写、绝不覆盖用户已有;返回每个模板的处理结果。 +//! 版本化分发(T1,2026-07-03):AGENTS.md / git-workflow.md 的炼境管理内容 +//! 包在 ONBOARDING_MANAGED 标记块中——文件不存在写全新;已有标记原位升级块; +//! 无标记则末尾追加块。块外用户内容永不触碰。 +//! lefthook.yml 因用户常深度定制(monorepo filter 等),保持"存在即跳过"。 //! 模板蓝本取自 enterprise-system,已通用化。 use serde::{Deserialize, Serialize}; use std::path::Path; +const MANAGED_START: &str = ""; + // 标准模板(编译期内嵌) const TMPL_AGENTS: &str = include_str!("../../resources/onboarding/AGENTS.md.tmpl"); const TMPL_LEFTHOOK: &str = include_str!("../../resources/onboarding/lefthook.yml.tmpl"); @@ -55,14 +60,91 @@ fn dispatch_one(root: &Path, relative: &str, content: &str) -> TemplateResult { } } +/// 从模板中提取 ONBOARDING_MANAGED 块(含首尾标记行)。 +fn extract_managed_block(template: &str) -> Option<&str> { + let start = template.find(MANAGED_START)?; + let end = template.find(MANAGED_END)? + MANAGED_END.len(); + template.get(start..end) +} + +/// 从标记行提取版本号(`version="x.y.z"`)。 +fn block_version(text: &str) -> Option<&str> { + let start = text.find(MANAGED_START)?; + let rest = &text[start..]; + let v = rest.find("version=\"")? + "version=\"".len(); + let tail = &rest[v..]; + tail.split('"').next() +} + +/// 版本化分发(三态):不存在→写全新;有标记→原位升级块;无标记→末尾追加块。 +fn dispatch_versioned(root: &Path, relative: &str, template: &str) -> TemplateResult { + let target = root.join(relative); + let tpl_block = match extract_managed_block(template) { + Some(b) => b, + None => return dispatch_one(root, relative, template), // 模板无标记,退化为旧行为 + }; + let tpl_ver = block_version(tpl_block).unwrap_or("?"); + + if !target.exists() { + return dispatch_one(root, relative, template); + } + let content = match std::fs::read_to_string(&target) { + Ok(c) => c, + Err(e) => { + return TemplateResult { + name: relative.to_string(), + action: "failed".into(), + detail: format!("读取失败: {e}"), + } + } + }; + + let (new_content, action, detail) = if let (Some(s), Some(e)) = + (content.find(MANAGED_START), content.find(MANAGED_END)) + { + let old_ver = block_version(&content).unwrap_or("?").to_string(); + if old_ver == tpl_ver { + return TemplateResult { + name: relative.to_string(), + action: "skipped".into(), + detail: format!("管理块已是 v{tpl_ver}"), + }; + } + let mut nc = String::new(); + nc.push_str(&content[..s]); + nc.push_str(tpl_block); + nc.push_str(&content[e + MANAGED_END.len()..]); + (nc, "updated", format!("管理块 v{old_ver} → v{tpl_ver},块外内容未动")) + } else { + ( + format!("{}\n\n{}\n", content.trim_end(), tpl_block), + "appended", + format!("追加管理块 v{tpl_ver}(存量文件无标记,原内容保留在上方)"), + ) + }; + + match std::fs::write(&target, new_content) { + Ok(_) => TemplateResult { + name: relative.to_string(), + action: action.into(), + detail, + }, + Err(e) => TemplateResult { + name: relative.to_string(), + action: "failed".into(), + detail: format!("写入失败: {e}"), + }, + } +} + /// 向项目分发全部基线模板,返回每个的处理结果。 /// project_path 为项目根(WSL 项目为 \\wsl.localhost\... UNC 路径,可直接写)。 pub fn dispatch_templates(project_path: &str) -> Vec { let root = Path::new(project_path.trim_end_matches(['/', '\\'])); vec![ - dispatch_one(root, "AGENTS.md", TMPL_AGENTS), + dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS), dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK), - dispatch_one(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW), + dispatch_versioned(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW), ] } @@ -103,13 +185,77 @@ mod tests { assert!(base.join("lefthook.yml").exists()); assert!(base.join("docs/ai-context/git-workflow.md").exists()); - // 改一个文件,验证再次分发不覆盖 + // 改一个文件(无管理块标记),验证用户内容不被覆盖: + // T1 后的契约——AGENTS/git-workflow 追加管理块(原内容保留),lefthook 仍跳过 std::fs::write(base.join("AGENTS.md"), "用户自己的内容").unwrap(); let r2 = dispatch_templates(pp); - assert!(r2.iter().all(|t| t.action == "skipped"), "已存在应全部跳过"); + let agents_r = r2.iter().find(|t| t.name == "AGENTS.md").unwrap(); + assert_eq!(agents_r.action, "appended", "无标记存量文件应追加管理块"); let kept = std::fs::read_to_string(base.join("AGENTS.md")).unwrap(); - assert_eq!(kept, "用户自己的内容", "绝不覆盖用户已有内容"); + assert!(kept.starts_with("用户自己的内容"), "用户内容必须保留在原位"); + let lefthook_r = r2.iter().find(|t| t.name == "lefthook.yml").unwrap(); + assert_eq!(lefthook_r.action, "skipped", "lefthook 保持存在即跳过"); + + let _ = std::fs::remove_dir_all(&base); + } + + // ── T1:版本化三态分发 ──────────────────────────────────────────────── + + #[test] + fn versioned_dispatch_three_states() { + let base = std::env::temp_dir() + .join(format!("lianjing_onb_ver_test_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir_all(&base).unwrap(); + let pp = base.to_str().unwrap(); + + // 态一:不存在 → 写全新(含管理块标记) + let r1 = dispatch_templates(pp); + assert!(r1.iter().all(|t| t.action == "written")); + let agents = std::fs::read_to_string(base.join("AGENTS.md")).unwrap(); + assert!(agents.contains(MANAGED_START) && agents.contains(MANAGED_END)); + + // 态二(同版本):有标记且版本一致 → skipped,文件不动 + let r2 = dispatch_templates(pp); + let agents_r = r2.iter().find(|t| t.name == "AGENTS.md").unwrap(); + assert_eq!(agents_r.action, "skipped"); + + // 态二(旧版本):把版本改旧 + 块外加用户内容 → updated,块升级、用户内容保留 + let old = agents + .replace("version=\"1.1.0\"", "version=\"1.0.0\"") + .replace("## 项目概况", "## 项目概况 + +我的项目说明") + + " +## 用户自加的章节 +内容 +"; + std::fs::write(base.join("AGENTS.md"), &old).unwrap(); + let r3 = dispatch_templates(pp); + let agents_r = r3.iter().find(|t| t.name == "AGENTS.md").unwrap(); + assert_eq!(agents_r.action, "updated"); + let upgraded = std::fs::read_to_string(base.join("AGENTS.md")).unwrap(); + assert!(upgraded.contains("version=\"1.1.0\""), "块应升级到模板版本"); + assert!(upgraded.contains("我的项目说明"), "块外用户内容(前)须保留"); + assert!(upgraded.contains("用户自加的章节"), "块外用户内容(后)须保留"); + assert_eq!(upgraded.matches(MANAGED_START).count(), 1, "只有一个管理块"); + + // 态三:存量文件无标记 → appended,原内容保留在上方 + std::fs::write(base.join("AGENTS.md"), "# 手写的 AGENTS + +自定义内容").unwrap(); + let r4 = dispatch_templates(pp); + let agents_r = r4.iter().find(|t| t.name == "AGENTS.md").unwrap(); + assert_eq!(agents_r.action, "appended"); + let appended = std::fs::read_to_string(base.join("AGENTS.md")).unwrap(); + assert!(appended.starts_with("# 手写的 AGENTS")); + assert!(appended.contains(MANAGED_START)); + + // lefthook 始终保持"存在即跳过" + let lefthook_r = r4.iter().find(|t| t.name == "lefthook.yml").unwrap(); + assert_eq!(lefthook_r.action, "skipped"); let _ = std::fs::remove_dir_all(&base); } } +