fix(coexist): 新炼境接管项目文件后,旧炼境的模板分发、蓝图规则同步与 buff 轮询跳过该项目
All checks were successful
Push & PR Check / check (pull_request) Successful in 1m2s
All checks were successful
Push & PR Check / check (pull_request) Successful in 1m2s
共存期两边会来回覆盖同一批托管文件(AGENTS.md、CLAUDE.md、lian-jing 技能、CONVENTIONS、任务卡)。 新炼境(lian-jing-next)接管某项目时在 .lian-jing/context.json 写入 files_managed_by="lian-jing-next"; 旧炼境据此跳过:dispatch_templates_with、sync_blueprint_rules_with、buff check_project。 身份合并写(write_canonical_context_with)本就保留未知字段,测试锁定接管标记不会被自己抹掉。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3cf3f87319
commit
ec672bb027
@ -1323,6 +1323,15 @@ pub(crate) fn sync_blueprint_rules_with(
|
|||||||
root: &Path,
|
root: &Path,
|
||||||
sink: &dyn crate::WriteSink,
|
sink: &dyn crate::WriteSink,
|
||||||
) -> Result<SyncResult, crate::WriteFailure> {
|
) -> Result<SyncResult, crate::WriteFailure> {
|
||||||
|
if crate::mcp_inject::files_managed_by_next(root) {
|
||||||
|
return Ok(SyncResult {
|
||||||
|
success: true,
|
||||||
|
conventions_written: false,
|
||||||
|
claude_md_action: "skipped".into(),
|
||||||
|
agents_md_action: "skipped".into(),
|
||||||
|
message: crate::mcp_inject::FILES_MANAGED_BY_NEXT_SKIP.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
let bp_dir = root.join(".blueprint");
|
let bp_dir = root.join(".blueprint");
|
||||||
let project_name = root
|
let project_name = root
|
||||||
.file_name()
|
.file_name()
|
||||||
@ -2876,6 +2885,22 @@ pub fn archive_project_docs(project_path: String) -> Result<ArchiveResult, Strin
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// 共存期防回写:项目文件已由新炼境接管时,蓝图规则同步零写入
|
||||||
|
#[test]
|
||||||
|
fn sync_skips_projects_managed_by_next() {
|
||||||
|
let base = std::env::temp_dir().join(format!("lianjing_sync_guard_test_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||||
|
std::fs::create_dir_all(base.join(".blueprint")).unwrap();
|
||||||
|
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"lian-jing-next"}"#).unwrap();
|
||||||
|
let result = sync_blueprint_rules_with(&base, &crate::DiskSink).unwrap();
|
||||||
|
assert_eq!((result.agents_md_action.as_str(), result.claude_md_action.as_str()), ("skipped", "skipped"));
|
||||||
|
assert!(!result.conventions_written);
|
||||||
|
assert!(!base.join(".blueprint/CONVENTIONS.md").exists());
|
||||||
|
assert!(!base.join("AGENTS.md").exists() && !base.join("CLAUDE.md").exists());
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
|
|||||||
@ -125,6 +125,10 @@ fn poll_all_buffs(app: &AppHandle) {
|
|||||||
|
|
||||||
fn check_project(app: &AppHandle, project_path: &str) -> Result<(), String> {
|
fn check_project(app: &AppHandle, project_path: &str) -> Result<(), String> {
|
||||||
let root = Path::new(project_path);
|
let root = Path::new(project_path);
|
||||||
|
// 共存期防回写:项目文件已由新炼境接管,不再改写其任务卡
|
||||||
|
if crate::mcp_inject::files_managed_by_next(root) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// 获取当前 HEAD commit
|
// 获取当前 HEAD commit
|
||||||
let current_commit = match get_head_commit(root) {
|
let current_commit = match get_head_commit(root) {
|
||||||
|
|||||||
@ -199,6 +199,14 @@ pub(crate) fn dispatch_templates_with(
|
|||||||
root: &Path,
|
root: &Path,
|
||||||
sink: &dyn crate::WriteSink,
|
sink: &dyn crate::WriteSink,
|
||||||
) -> Vec<TemplateResult> {
|
) -> Vec<TemplateResult> {
|
||||||
|
if crate::mcp_inject::files_managed_by_next(root) {
|
||||||
|
return vec![TemplateResult {
|
||||||
|
name: ".lian-jing/context.json".into(),
|
||||||
|
action: "skipped".into(),
|
||||||
|
detail: crate::mcp_inject::FILES_MANAGED_BY_NEXT_SKIP.into(),
|
||||||
|
warning: None,
|
||||||
|
}];
|
||||||
|
}
|
||||||
vec![
|
vec![
|
||||||
dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS, sink),
|
dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS, sink),
|
||||||
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK, sink),
|
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK, sink),
|
||||||
@ -1243,6 +1251,21 @@ pub fn silent_projects(conn: &rusqlite::Connection) -> Result<Vec<(String, i64)>
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
// 共存期防回写:项目文件已由新炼境接管时,接入包模板分发零写入
|
||||||
|
#[test]
|
||||||
|
fn dispatch_skips_projects_managed_by_next() {
|
||||||
|
let base = std::env::temp_dir().join(format!("lianjing_dispatch_guard_test_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||||
|
std::fs::create_dir_all(base.join(".blueprint")).unwrap();
|
||||||
|
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"lian-jing-next"}"#).unwrap();
|
||||||
|
let results = super::dispatch_templates(base.to_str().unwrap());
|
||||||
|
assert_eq!(results.len(), 1);
|
||||||
|
assert_eq!(results[0].action, "skipped");
|
||||||
|
assert!(!base.join("AGENTS.md").exists() && !base.join(".claude").exists() && !base.join("lefthook.yml").exists());
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -235,6 +235,20 @@ fn write_json_with(
|
|||||||
.map_err(|error| format!("写入 {label} 失败: {error}"))
|
.map_err(|error| format!("写入 {label} 失败: {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 新炼境(lian-jing-next)接管项目文件后写入 `.lian-jing/context.json` 的 `files_managed_by` 取值。
|
||||||
|
pub(crate) const FILES_MANAGED_BY_NEXT: &str = "lian-jing-next";
|
||||||
|
|
||||||
|
/// 共存期防回写守卫:项目文件已由新炼境接管时,旧炼境的模板分发、蓝图规则同步与 buff 轮询一律跳过,
|
||||||
|
/// 避免两边来回覆盖同一批托管文件。只读判断,文件缺失或不合法时视为未接管。
|
||||||
|
pub(crate) fn files_managed_by_next(root: &std::path::Path) -> bool {
|
||||||
|
std::fs::read_to_string(root.join(".lian-jing").join("context.json"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
|
||||||
|
.is_some_and(|ctx| ctx.get("files_managed_by").and_then(Value::as_str) == Some(FILES_MANAGED_BY_NEXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const FILES_MANAGED_BY_NEXT_SKIP: &str = "项目文件已由新炼境接管(.lian-jing/context.json 的 files_managed_by),旧炼境不再分发";
|
||||||
|
|
||||||
/// 写工具无关的项目身份锚点。项目根目录是边界,客户端项目 UI 或用户级配置均不是身份来源。
|
/// 写工具无关的项目身份锚点。项目根目录是边界,客户端项目 UI 或用户级配置均不是身份来源。
|
||||||
/// 合并写入:保留未来字段与项目定制字段,只有炼境拥有的身份字段会被刷新。
|
/// 合并写入:保留未来字段与项目定制字段,只有炼境拥有的身份字段会被刷新。
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -3623,6 +3637,26 @@ trust_level = "trusted"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn files_managed_by_next_guard_reads_context_and_survives_identity_rewrite() {
|
||||||
|
let base = std::env::temp_dir().join(format!("lianjing_files_guard_test_{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
std::fs::create_dir_all(base.join(".lian-jing")).unwrap();
|
||||||
|
assert!(!files_managed_by_next(&base), "没有 context.json 视为未接管");
|
||||||
|
std::fs::write(
|
||||||
|
base.join(".lian-jing/context.json"),
|
||||||
|
r#"{"project_id":"p1","schema_version":1,"files_managed_by":"lian-jing-next"}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(files_managed_by_next(&base));
|
||||||
|
// 旧炼境刷新身份时必须保留接管标记,否则守卫会被自己抹掉
|
||||||
|
write_canonical_context_at(&base, "p1").unwrap();
|
||||||
|
assert!(files_managed_by_next(&base), "身份合并写后接管标记仍在");
|
||||||
|
std::fs::write(base.join(".lian-jing/context.json"), r#"{"project_id":"p1","files_managed_by":"other"}"#).unwrap();
|
||||||
|
assert!(!files_managed_by_next(&base));
|
||||||
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_context_migrates_legacy_project_id_and_preserves_unknown_fields() {
|
fn canonical_context_migrates_legacy_project_id_and_preserves_unknown_fields() {
|
||||||
let base = std::env::temp_dir().join(format!(
|
let base = std::env::temp_dir().join(format!(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user