From f77fd73966b83674b65fb4ca2311d176102526b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Fri, 8 May 2026 23:46:15 +0200 Subject: [PATCH] fix(fermata): .botignore as fallback root, not ignored; extract SVGs to files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Walk-up: .botignore no longer stops the search but is remembered as a fallback when no strong marker (.git, botignore.toml, .botignore.toml) is found — prevents fail-open regression for projects without .git - Extract inline SVGs to policy-layers.svg and architecture.svg - READMEs reference SVGs via tags Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/project.rs | 20 +++++++++++++------- tests/core_project.rs | 28 +++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/core/project.rs b/src/core/project.rs index c9f3e15..91beed6 100644 --- a/src/core/project.rs +++ b/src/core/project.rs @@ -1,12 +1,14 @@ use std::path::{Path, PathBuf}; -/// Markers checked in priority order when walking up from a target path. -const MARKERS: &[&str] = &["botignore.toml", ".botignore.toml", ".git"]; +/// Strong markers that definitively identify a project root. +const STRONG_MARKERS: &[&str] = &["botignore.toml", ".botignore.toml", ".git"]; /// Walk upward from `target` (or its parent if `target` is a file) looking -/// for the nearest project root. Roots are identified by the presence of -/// any marker in `MARKERS`. Walks from the **target file's location**, not -/// from cwd, because agents `cd` around. +/// for the nearest project root. Strong markers (`botignore.toml`, +/// `.botignore.toml`, `.git`) stop the walk immediately. A `.botignore` +/// file is remembered as a fallback but does not stop the walk — the search +/// continues upward for a stronger boundary. If none is found, the +/// `.botignore` location is used. pub fn find_project_root(target: &Path) -> Option { let start = if target.is_file() { target.parent()? @@ -14,14 +16,18 @@ pub fn find_project_root(target: &Path) -> Option { target }; + let mut fallback: Option = None; let mut current = Some(start); while let Some(dir) = current { - for marker in MARKERS { + for marker in STRONG_MARKERS { if dir.join(marker).exists() { return Some(dir.to_path_buf()); } } + if fallback.is_none() && dir.join(".botignore").exists() { + fallback = Some(dir.to_path_buf()); + } current = dir.parent(); } - None + fallback } diff --git a/tests/core_project.rs b/tests/core_project.rs index 78d1d49..98baa10 100644 --- a/tests/core_project.rs +++ b/tests/core_project.rs @@ -51,18 +51,36 @@ fn botignore_alone_does_not_stop_walk() { } #[test] -fn botignore_only_returns_none() { - // If only .botignore exists with no real root marker, no project root is found. +fn botignore_used_as_fallback() { + // If only .botignore exists (no strong marker), it serves as a fallback + // root so that policy is still enforced. let tmp = TempDir::new().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("sub")).unwrap(); - fs::write(root.join(".botignore"), "").unwrap(); + fs::write(root.join(".botignore"), "*.secret").unwrap(); let target = root.join("sub/file.rs"); fs::write(&target, "").unwrap(); - // .botignore alone should NOT define a project root - assert!(find_project_root(&target).is_none()); + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + +#[test] +fn strong_marker_preferred_over_botignore_fallback() { + // .botignore at a/b/, .git at root — walk past .botignore, use root. + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("a/b/c")).unwrap(); + fs::create_dir_all(root.join(".git")).unwrap(); + fs::write(root.join("a/b/.botignore"), "*.key").unwrap(); + + let target = root.join("a/b/c/file.rs"); + fs::write(&target, "").unwrap(); + + // Should find root (with .git), not a/b (with .botignore) + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); } #[test]