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

@@ -806,6 +806,7 @@ Tolaria tracks managed vault-level AI guidance separately from normal note conte
- `GEMINI.md` is an optional Gemini CLI compatibility shim that points Gemini back to `AGENTS.md`
- `useVaultAiGuidanceStatus` reads `get_vault_ai_guidance_status` and normalizes the backend state into four UI cases: `managed`, `missing`, `broken`, and `custom`
- `restore_vault_ai_guidance` repairs only Tolaria-managed files and creates the optional Gemini shim on explicit request; user-authored custom `AGENTS.md` / `CLAUDE.md` / `GEMINI.md` files are surfaced as custom and left untouched
- Editing a usable `AGENTS.md`, including changing its frontmatter `type`, makes the file custom rather than broken; broken is reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs
- The status bar AI badge and command palette consume that abstraction to expose restore actions only when the managed guidance is missing or broken
Vault guidance is intentionally short and vault-specific. General Tolaria product behavior is delivered through the bundled agent docs resource instead:

View File

@@ -544,7 +544,7 @@ Once a vault is ready, `useAiAgentsOnboarding` can show a one-time `AiAgentsOnbo
`useGettingStartedClone` reuses the same parent-folder semantics for the status-bar / command-palette clone action, and `Toast` is rendered through the AI-agents onboarding gate so the resolved destination path stays visible right after a successful clone.
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
The starter content no longer lives in the app repo. `src-tauri/src/vault/getting_started.rs` holds the public starter repo URL (`refactoringhq/tolaria-getting-started`), delegates the clone to the git backend, then normalizes Tolaria-managed root guidance and type scaffolding (`AGENTS.md`, `CLAUDE.md`, `type.md`, `note.md`) so fresh starter vaults pick up the current defaults even when the remote starter repo still carries a legacy copy or an older pre-`type:` `is_a`-era template. `AGENTS.md` stays the canonical vault guidance file; `CLAUDE.md` is a compatibility shim that imports it for Claude Code without duplicating the instructions, and Tolaria seeds it as an organized `Note` so it stays out of the way in a fresh vault. Optional `GEMINI.md` guidance is created only by the explicit AI guidance restore action. Once a user edits a usable `AGENTS.md`, including changing its frontmatter `type`, the status command treats it as custom guidance instead of broken; repair remains reserved for missing, empty, frontmatter-only, unreadable, or exact replaceable managed templates/stubs. The clone helper still accepts the legacy `LAPUTA_GETTING_STARTED_REPO_URL` environment override so older automation can continue to redirect the starter source during the transition.
After the clone completes, Tolaria removes every configured git remote from the new starter vault. Getting Started vaults therefore open as local-only by default, and users opt into a remote later with the explicit Add Remote flow.

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.