fix: preserve edited vault guidance status

This commit is contained in:
lucaronin
2026-05-28 22:00:29 +02:00
parent 181ff6de8c
commit 6910347bf3
4 changed files with 92 additions and 17 deletions

View File

@@ -2,7 +2,7 @@ use serde::Serialize;
use std::fs;
use std::path::{Path, PathBuf};
use super::getting_started::{agents_content_can_be_refreshed, AGENTS_MD};
use super::getting_started::{agents_content_is_known_managed_template, AGENTS_MD};
/// Content for `type.md` — describes the generic Type metamodel for the vault.
const TYPE_TYPE_DEFINITION: &str = "\
@@ -111,7 +111,9 @@ fn read_file_or_empty(path: &Path) -> String {
}
fn agents_content_can_be_replaced(content: &str) -> bool {
content.contains("See config/agents.md") || agents_content_can_be_refreshed(content)
content.trim().is_empty()
|| content.contains("See config/agents.md")
|| agents_content_is_known_managed_template(content)
}
fn root_agents_can_be_replaced(path: &Path) -> bool {
@@ -157,6 +159,30 @@ fn sync_managed_file(
Ok(true)
}
fn strip_markdown_frontmatter(content: &str) -> &str {
let Some(line_ending) = resolve_frontmatter_line_ending(content) else {
return content;
};
let after_open = &content[3 + line_ending.len()..];
let close_marker = format!("{line_ending}---");
let Some(close_index) = after_open.find(&close_marker) else {
return content;
};
let after_close = &after_open[close_index + close_marker.len()..];
after_close.strip_prefix(line_ending).unwrap_or(after_close)
}
fn resolve_frontmatter_line_ending(content: &str) -> Option<&'static str> {
if content.starts_with("---\r\n") {
return Some("\r\n");
}
content.starts_with("---\n").then_some("\n")
}
fn guidance_content_has_body(content: &str) -> bool {
!strip_markdown_frontmatter(content).trim().is_empty()
}
fn classify_guidance_file(
path: &Path,
matches_managed: fn(&str) -> bool,
@@ -171,6 +197,10 @@ fn classify_guidance_file(
return AiGuidanceFileState::Managed;
}
if !guidance_content_has_body(&content) {
return AiGuidanceFileState::Broken;
}
if can_replace(path) {
return AiGuidanceFileState::Broken;
}
@@ -468,17 +498,17 @@ mod tests {
);
}
fn assert_refreshes_outdated_managed_agents(run: VaultOperation) {
let outdated_agents = AGENTS_MD.replacen(
fn assert_preserves_edited_default_agents(run: VaultOperation) {
let edited_agents = AGENTS_MD.replacen(
"Store note type in the `type:` frontmatter field.",
"`type:` is the preferred type field. Tolaria still understands legacy aliases such as `Is A`.",
1,
);
let (_dir, vault) = run_with_agents(run, Some(&outdated_agents), None);
let (_dir, vault) = run_with_agents(run, Some(&edited_agents), None);
let content = read_root_agents(&vault);
assert!(content.contains("Store note type in the `type:` frontmatter field."));
assert!(!content.contains("Tolaria still understands legacy aliases such as `Is A`."));
assert!(content.contains("Tolaria still understands legacy aliases such as `Is A`."));
assert!(!content.contains("Store note type in the `type:` frontmatter field."));
}
fn assert_legacy_agents_move_to_root(
@@ -584,7 +614,7 @@ mod tests {
}
#[test]
fn test_seed_config_files_refreshes_stale_default_agents() {
fn test_seed_config_files_preserves_edited_stale_default_agents() {
let (_dir, vault) = create_vault();
write_root_agents(
&vault,
@@ -594,15 +624,13 @@ mod tests {
seed_config_files(vault.to_str().unwrap());
let content = read_root_agents(&vault);
assert!(content.contains("Use the first H1 as the note title."));
assert!(content.contains("Tolaria reads notes recursively from all folders"));
assert!(content.contains("views/*.yml"));
assert!(content.contains("Belongs to:"));
assert!(content.contains("Do not add `title:` frontmatter."));
assert!(!content.contains("Use the first H1 as the note title."));
}
#[test]
fn test_seed_config_files_refreshes_outdated_managed_agents() {
assert_refreshes_outdated_managed_agents(run_seed);
fn test_seed_config_files_preserves_edited_default_agents() {
assert_preserves_edited_default_agents(run_seed);
}
#[test]
@@ -724,8 +752,8 @@ mod tests {
}
#[test]
fn test_repair_config_files_refreshes_outdated_managed_agents() {
assert_refreshes_outdated_managed_agents(run_repair);
fn test_repair_config_files_preserves_edited_default_agents() {
assert_preserves_edited_default_agents(run_repair);
}
#[test]
@@ -746,6 +774,48 @@ mod tests {
);
}
#[test]
fn test_get_ai_guidance_status_treats_edited_agents_as_custom() {
let (_dir, vault) = create_vault();
let edited_agents = AGENTS_MD.replacen("type: Note", "type: Vault Guidance", 1)
+ "\n\n- Prefer the user's vault-specific naming conventions.\n";
write_root_agents(&vault, &edited_agents);
write_root_claude(&vault, CLAUDE_MD_SHIM);
write_root_gemini(&vault, GEMINI_MD_SHIM);
let status = get_ai_guidance_status(vault.to_str().unwrap()).unwrap();
assert_eq!(
status,
VaultAiGuidanceStatus {
agents_state: AiGuidanceFileState::Custom,
claude_state: AiGuidanceFileState::Managed,
gemini_state: AiGuidanceFileState::Managed,
can_restore: false,
}
);
}
#[test]
fn test_get_ai_guidance_status_reports_frontmatter_only_agents_as_broken() {
let (_dir, vault) = create_vault();
write_root_agents(&vault, "---\ntype: Vault Guidance\n---\n");
write_root_claude(&vault, CLAUDE_MD_SHIM);
write_root_gemini(&vault, GEMINI_MD_SHIM);
let status = get_ai_guidance_status(vault.to_str().unwrap()).unwrap();
assert_eq!(
status,
VaultAiGuidanceStatus {
agents_state: AiGuidanceFileState::Broken,
claude_state: AiGuidanceFileState::Managed,
gemini_state: AiGuidanceFileState::Managed,
can_restore: true,
}
);
}
#[test]
fn test_restore_ai_guidance_files_repairs_without_overwriting_custom_agents() {
let (_dir, vault) = create_vault();

View File

@@ -292,6 +292,10 @@ pub(super) fn agents_content_can_be_refreshed(content: &str) -> bool {
AgentsContent::new(content).can_be_refreshed()
}
pub(super) fn agents_content_is_known_managed_template(content: &str) -> bool {
AgentsContent::new(content).is_known_legacy_template()
}
/// Default AGENTS.md content — vault instructions for AI agents.
/// Describes Tolaria vault mechanics only; no user-specific structure.
/// The vault scanner will pick this up as a regular entry.