Skip to content

feat(scopes): warn on path collisions and kind disagreements (#153, #156)#201

Merged
bminier merged 1 commit into
devfrom
feat/v0.7-warning-surfaces
May 25, 2026
Merged

feat(scopes): warn on path collisions and kind disagreements (#153, #156)#201
bminier merged 1 commit into
devfrom
feat/v0.7-warning-surfaces

Conversation

@bminier

@bminier bminier commented May 25, 2026

Copy link
Copy Markdown
Owner

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`).

  • Banner under the sandbox banner: "Scopes share files" + bulleted list naming each pair + the shared path
  • `validate_move_request` refuses cross-scope moves between colliding scopes with a typed error
  • Same-scope change-kind unaffected (operation still meaningful)

#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).

  • ⚠ badge on every per-scope rule row in each affected (scope, kind) bucket
  • Same badge on the matching combined-panel chips
  • Popover lists every (scope, kind) with the highest-precedence one labeled "wins by precedence"
  • Badge styled with `--deny` so a row with both lint + conflict shows two visually distinct cues

Test plan

  • 281 Rust lib tests (was 274 — +7 for the two detectors + move-refusal)
  • 137 JS unit tests (was 132 — +5 for banner + badge rendering)
  • 21 CLI integration tests
  • `just check` clean (typecheck, biome, clippy, all suites)
  • SMOKE_TEST.md: two new bullets covering banner + badge flows
  • CI confirms

Reviewer notes

  • Both detectors live in `build_loaded` so every `load_scopes` IPC carries the data; no opt-in needed.
  • Path-collision comparison is byte-equal on the `PathBuf` the resolver produced — sufficient because both sides come from the same `dirs::home_dir()` / `find_project_root` pipeline. Canonicalize-if-exists is a defensive nicety we don't need today; if symlink edge cases bite later, the helper is one file to update.
  • Kind-conflict badge reuses `lintBadge`'s shape (focusable ⚠ + popover). Recoloring is the only visual differentiator from the lint badge; the popover content disambiguates verbally.
  • `combined_origins[kind][i][0]` is the anchor scope for the combined-panel badge — the rule's highest-precedence (scope, kind) in this kind's column. The lookup helper checks every occurrence so the badge fires on any chip the rule is part of, not just where it "wins."

Closes #153, closes #156.

🤖 Generated with Claude Code

)

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>
@bminier bminier merged commit 3580d75 into dev May 25, 2026
14 checks passed
@bminier bminier deleted the feat/v0.7-warning-surfaces branch May 25, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn when permission kinds disagree across scopes Warn when project dir == user dir, or user-local == project-local

1 participant