feat(scopes): warn on path collisions and kind disagreements (#153, #156)#201
Merged
Conversation
) Two warning surfaces, both detected at scope load and surfaced through `LoadedScopes` so the front-end can render them without a second IPC roundtrip. Bundled because they're peer "things the user couldn't otherwise notice" warnings and share the LoadedScopes payload + fixture plumbing. #153 — Path collisions ====================== When the resolved scope paths collapse onto the same on-disk file (common case: opening ClaudeScope from `$HOME` itself, which makes Project and User point at the same `~/.claude/settings.json`), the columns still render but every cross-scope move between the colliding pair is either a no-op or actively destructive. Detection: `detect_path_collisions` groups `Scope::ALL` by their resolved `PathBuf`, returning any group of size >= 2 sorted in broad->narrow scope order to match the column layout. Byte-equal on the paths the resolver produced is sufficient — both sides come from the same `dirs::home_dir()` / `find_project_root` pipeline, so no exotic symlink canonicalization is needed. Surfacing: - New `LoadedScopes.path_collisions: Vec<PathCollision>` field. - `pathCollisionsBanner` renders directly under the sandbox banner: "Scopes share files" header, bulleted list naming each pair + the shared path, explanation that moves between these scopes are disabled. - `validate_move_request` refuses cross-scope moves whose `from`/`to` resolve to the same file with a typed error — same-scope change-kind (`from == to`) is unaffected because the operation still mutates the file meaningfully. #156 — Kind disagreements ========================= A rule string in multiple scopes under different kinds (e.g. `Bash(git push)` is `allow` in User but `deny` in Project) is either a typo or a stale rule — the user has no way to notice it today without scanning every column manually. Detection: `detect_kind_conflicts` walks each scope's permission arrays, builds a `rule -> Vec<(scope, kind)>` map, and emits a `KindConflict` for any rule whose occurrences span >= 2 distinct kinds. Duplicates within a single (scope, kind) bucket collapse to one occurrence (the duplicate-detection issue #17 is the right surface for that, not this one). Surfacing: - New `LoadedScopes.kind_conflicts: Vec<KindConflict>` field. Occurrences are in `Scope::ALL` (precedence) order so the first entry is always the "winner" for the combined union. - `kindConflictBadge` is a ⚠ button paired with a popover — mirrors the existing `lintBadge` shape so the hover/focus/ click/pin behavior is identical and there's only one mental model to learn. Recolored to `--deny` so a row with both a lint warning and a kind conflict shows them as distinct cues; the popover content disambiguates verbally. - Badge attaches to per-scope rule rows (in `treeLeaf`) AND to combined-panel chips (in `combinedPanel`). The combined-panel surface uses `combined_origins[kind][i][0]` as the anchor scope so the badge sits where the rule lives in that kind's column. Tests ===== Rust (274 -> 281, +7): - detect_path_collisions_groups_scopes_sharing_one_file - detect_path_collisions_returns_empty_when_paths_are_distinct - detect_kind_conflicts_surfaces_allow_vs_deny_across_scopes - detect_kind_conflicts_ignores_matching_kinds_in_multiple_scopes - detect_kind_conflicts_collapses_duplicates_in_one_scope_kind - detect_kind_conflicts_handles_three_way_disagreement - validate_move_request_refuses_cross_scope_move_into_a_colliding_pair JS (132 -> 137, +5): - does not render the path-collisions banner when no scopes share files - renders the path-collisions banner with scope labels + shared path - renders a kind-conflict badge on the per-scope rule row - does not render the kind-conflict badge when scopes agree - renders the kind-conflict badge on the combined panel chip Docs: - SMOKE_TEST.md: two new bullets covering banner + badge flows. Closes #153, closes #156. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two warning surfaces, both detected at scope load, surfaced through
LoadedScopes. Bundled because they're peer "things the user couldn't otherwise notice" warnings and share the LoadedScopes payload + fixture plumbing.#153 — Path collisions
Detects scopes whose resolved file paths collapse onto the same on-disk file. Common case: opening ClaudeScope from `$HOME` makes Project and User both resolve to `~/.claude/settings.json` (and Local/UserLocal to `settings.local.json`).
#156 — Kind disagreements
Detects rule strings that appear in multiple scopes under different kinds (e.g. `Bash(git push)` is `allow` in User but `deny` in Project).
Test plan
Reviewer notes
Closes #153, closes #156.
🤖 Generated with Claude Code