From 057c2b3a106057518293ab12cebc4348f80da955 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Mon, 21 Sep 2026 22:42:03 +0900 Subject: [PATCH] fix(onboarding): recognize custom lefthook checks --- .blueprint/manifest.yaml | 2 +- .../modules/agent-client-capability-packs.md | 7 ++ src-tauri/src/commands/onboarding.rs | 91 +++++++++++++++---- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/.blueprint/manifest.yaml b/.blueprint/manifest.yaml index a5148af..28b950c 100644 --- a/.blueprint/manifest.yaml +++ b/.blueprint/manifest.yaml @@ -465,7 +465,7 @@ modules: area: infra status: done progress: 100 - note: "P2 完成:工具无关 canonical context、版本化能力包自检与 Codex Skill-only 投影已闭环;不复制 MCP、凭证或部署能力。" + note: "P2 完成:工具无关 canonical context、版本化能力包自检、Codex Skill-only 投影与项目自定义 lefthook 等价能力识别已闭环;不复制 MCP、凭证或部署能力。" updated: 2026-09-21 position: [2250, 800] edges: diff --git a/.blueprint/modules/agent-client-capability-packs.md b/.blueprint/modules/agent-client-capability-packs.md index 90cdd7c..f61dbde 100644 --- a/.blueprint/modules/agent-client-capability-packs.md +++ b/.blueprint/modules/agent-client-capability-packs.md @@ -56,3 +56,10 @@ Codex 用户级插件 - files: AGENTS.md, docs/ai-context/project-brief.md, docs/ai-context/project-context.yaml, docs/templates/agents-meta-rules.md, resources/onboarding/ - acceptance: "[cargo test] 接入包升级检查 → 旧 context/旧能力包给出明确 upgrade_hint 且不自动覆盖用户配置;[rg] 常驻架构文档将炼境描述为项目控制面、客户端描述为 adapter,且不把客户端插件表述为凭证或部署所有者。" - done_note: "2026-09-21:base-onboarding 升至 v1.1.0;旧能力包会显式返回 version_mismatch 与升级提示。托管模板、Git 工作流与常驻架构文档统一为 canonical context / 控制面 / 客户端 adapter 边界。" + +### ✅ P2-E. 自定义 lefthook 等价能力自检 +- status: done +- complexity: S +- files: src-tauri/src/commands/onboarding.rs +- acceptance: "[cargo test] verify_onboarding 的 lefthook 自检 → web/API 分层 typecheck 与 commit-msg scripts 中的 Conventional 校验被识别为 ready;缺少等价能力时仍返回 missing,不因注释或无关键误报。" +- done_note: "2026-09-21:自检保留模板为基线,但支持命名为 typecheck-* 的分层 pre-commit 命令,以及 commit-msg scripts 中含 conventional 的校验;其余基线能力仍要求明确存在,避免自定义配置被误报或宽松放行。" diff --git a/src-tauri/src/commands/onboarding.rs b/src-tauri/src/commands/onboarding.rs index 3e5eccc..4d8cc5a 100644 --- a/src-tauri/src/commands/onboarding.rs +++ b/src-tauri/src/commands/onboarding.rs @@ -592,27 +592,39 @@ fn check_origin(origin_host: Option<&str>, instance_hosts: &[String]) -> (bool, } } -/// 从 lefthook.yml 内容提取 commands 键集合(` commands:` 下缩进 4 空格的键行)。 -/// 纯函数;注释与更深缩进(run: | 多行体等)忽略。用于 U1 检查项自省—— -/// 期望键集合取自内嵌模板,禁止硬编码具体检查项名(模板演进校验器零维护)。 -pub fn lefthook_command_keys(yml: &str) -> Vec { +/// 从指定 hook 的指定段提取缩进四空格的键集合。 +/// 纯函数;注释与更深缩进(run: | 多行体等)忽略。 +fn lefthook_section_keys(yml: &str, hook: &str, section: &str) -> Vec { let mut keys = Vec::new(); - let mut in_commands = false; + let mut in_hook = false; + let mut in_section = false; for line in yml.lines() { let trimmed = line.trim_start(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } let indent = line.len() - trimmed.len(); - if indent == 2 && trimmed.trim_end() == "commands:" { - in_commands = true; + if indent == 0 && trimmed.ends_with(':') { + in_hook = trimmed.trim_end().trim_end_matches(':') == hook; + in_section = false; continue; } - if indent < 4 { - in_commands = false; + if !in_hook { continue; } - if in_commands && indent == 4 { + if indent == 2 && trimmed.trim_end() == format!("{section}:") { + in_section = true; + continue; + } + if indent == 2 { + in_section = false; + continue; + } + if indent < 2 { + in_section = false; + continue; + } + if in_section && indent == 4 { if let Some(key) = trimmed.trim_end().strip_suffix(':') { keys.push(key.to_string()); } @@ -621,23 +633,43 @@ pub fn lefthook_command_keys(yml: &str) -> Vec { keys } -/// U1 检查:项目 lefthook.yml 是否缺少模板要求的检查段(lefthook 不自动覆盖,只检测落后)。 +/// 从 lefthook.yml 内容提取 commands 键集合。 +pub fn lefthook_command_keys(yml: &str) -> Vec { + let mut keys = lefthook_section_keys(yml, "pre-commit", "commands"); + keys.extend(lefthook_section_keys(yml, "commit-msg", "commands")); + keys +} + +/// U1 检查:项目 lefthook.yml 是否缺少模板要求的检查能力。 +/// +/// lefthook.yml 是项目可定制文件,不能把模板中的键名当成唯一实现。保留模板 +/// 自省作为基线,但识别两类等价能力:分层 typecheck 命令,以及 commit-msg +/// 目录中的 Conventional script。其余模板检查仍要求同名命令,避免将任意注释或 +/// 无关脚本误报为 ready。 fn check_lefthook_keys(project_yml: Option<&str>, template_yml: &str) -> (bool, String) { let Some(yml) = project_yml else { return (false, "lefthook.yml 缺失——重跑接入包写入".to_string()); }; - let have: Vec = lefthook_command_keys(yml); + let commands = lefthook_command_keys(yml); + let commit_msg_scripts = lefthook_section_keys(yml, "commit-msg", "scripts"); let missing: Vec = lefthook_command_keys(template_yml) .into_iter() - .filter(|k| !have.contains(k)) + .filter(|required| match required.as_str() { + "typecheck" => !commands.iter().any(|key| key.contains("typecheck")), + "conventional" => !commands + .iter() + .chain(commit_msg_scripts.iter()) + .any(|key| key.contains("conventional")), + _ => !commands.contains(required), + }) .collect(); if missing.is_empty() { - (true, "模板要求的检查段齐全".to_string()) + (true, "模板要求的检查能力齐全".to_string()) } else { ( false, format!( - "缺少检查段: {}——lefthook.yml 不自动覆盖,请手动对照模板补齐", + "缺少检查能力: {}——lefthook.yml 不自动覆盖,请手动对照模板补齐", missing.join(", ") ), ) @@ -1201,14 +1233,14 @@ mod tests { ); // 项目文件与模板一致 → pass - let (pass, _) = check_lefthook_keys(Some(TMPL_LEFTHOOK), TMPL_LEFTHOOK); - assert!(pass); + let (pass, detail) = check_lefthook_keys(Some(TMPL_LEFTHOOK), TMPL_LEFTHOOK); + assert!(pass, "模板应满足自身检查: {detail}"); // 缺段 → fail 且列出缺失键 let partial = "pre-commit:\n commands:\n typecheck:\n run: pnpm typecheck\n"; let (pass, detail) = check_lefthook_keys(Some(partial), TMPL_LEFTHOOK); assert!(!pass); - assert!(detail.contains("缺少检查段"), "应列出缺失键: {detail}"); + assert!(detail.contains("缺少检查能力"), "应列出缺失键: {detail}"); assert!( !detail.contains("typecheck,") && detail.contains("blueprint-gate"), "已有键不在缺失列表: {detail}" @@ -1216,6 +1248,29 @@ mod tests { // 文件缺失 → fail assert!(!check_lefthook_keys(None, TMPL_LEFTHOOK).0); + + // 项目可将 typecheck 拆到 web/API,并把 Conventional 校验声明为 commit-msg script。 + let custom = r#" +pre-commit: + commands: + typecheck-web: + run: pnpm --filter web typecheck + typecheck-api: + run: pnpm --filter api typecheck + blueprint-gate: + run: node scripts/blueprint-gate.cjs || true +commit-msg: + scripts: + "check-conventional.sh": + runner: bash + commands: + feature-confirmed: + run: test -n "$1" +"#; + assert!( + check_lefthook_keys(Some(custom), TMPL_LEFTHOOK).0, + "等价的项目自定义检查不应被误报缺失" + ); } // ── B4:使者回传覆盖率(沉默项目信号)────────────────────────────────────