Skip to content

Commit 069b7db

Browse files
committed
fix(whaleflow): improve scopes_overlap with path-boundary matching
1 parent 574a606 commit 069b7db

1 file changed

Lines changed: 28 additions & 11 deletions

File tree

crates/whaleflow/src/config.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -353,27 +353,44 @@ impl Conflict {
353353

354354
/// Check if two sets of file scope patterns overlap.
355355
///
356-
/// Uses prefix matching: strips glob suffixes (`/**`, `/*`) and checks
357-
/// if one prefix starts with the other. Simple but effective for the
358-
/// typical patterns the model generates (e.g. `src/auth/**`).
356+
/// Strips glob wildcards (`**`, `*`) and then checks whether the
357+
/// resulting directory prefixes overlap at a path-segment boundary.
358+
/// `src/api/**` vs `src/apiv2/**` → no overlap (different segments).
359+
/// `src/*/handler.rs` strips the `*` and checks prefix boundaries.
359360
fn scopes_overlap(a: &[String], b: &[String]) -> bool {
360361
if a.is_empty() || b.is_empty() {
361362
return false;
362363
}
363364

364-
fn strip_glob(s: &str) -> &str {
365-
s.trim_end_matches('/')
366-
.trim_end_matches("**")
367-
.trim_end_matches('*')
368-
.trim_end_matches('/')
365+
/// Strip glob wildcards from the end of a pattern, stopping before
366+
/// the last directory separator so path-boundary matching works.
367+
fn normalize_pattern(s: &str) -> String {
368+
// Remove trailing globs: /** → nothing, /* → nothing, /*.rs → nothing
369+
let mut p = s.trim_end_matches('/').to_string();
370+
while p.ends_with("**") || p.ends_with('*') {
371+
let trimmed = p.trim_end_matches("**").trim_end_matches('*');
372+
if trimmed.len() == p.len() {
373+
break;
374+
}
375+
p = trimmed.to_string();
376+
}
377+
// Ensure we end at a directory boundary for correct prefix matching.
378+
p = p.trim_end_matches('/').to_string();
379+
p
369380
}
370381

371-
let a_prefixes: Vec<&str> = a.iter().map(|s| strip_glob(s)).collect();
372-
let b_prefixes: Vec<&str> = b.iter().map(|s| strip_glob(s)).collect();
382+
let a_prefixes: Vec<String> = a.iter().map(|s| normalize_pattern(s)).collect();
383+
let b_prefixes: Vec<String> = b.iter().map(|s| normalize_pattern(s)).collect();
373384

374385
for ap in &a_prefixes {
375386
for bp in &b_prefixes {
376-
if ap.starts_with(bp) || bp.starts_with(ap) {
387+
// Only flag as overlapping if one is a path-segment prefix of
388+
// the other, i.e. `src/api` matches `src/api` or `src/api/...`
389+
// but NOT `src/apiv2`.
390+
if ap == bp
391+
|| ap.starts_with(&format!("{}/", bp))
392+
|| bp.starts_with(&format!("{}/", ap))
393+
{
377394
return true;
378395
}
379396
}

0 commit comments

Comments
 (0)