Feature: Scoped Backend View Filtering
  As a developer
  I want scoped backend views that filter text/vector/graph queries to project resources
  So that plans never see resources outside their scope (cross-project isolation)

  # ---- ResourceScope ----

  Scenario: Create a valid ResourceScope
    Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
    Then sv the scope should contain resource "RES01"
    And sv the scope should contain resource "RES02"
    And sv the scope should not contain resource "RES99"
    And sv the scope should contain project "local/api"
    And sv the scope should not contain project "local/frontend"

  Scenario: ResourceScope is frozen
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    Then sv modifying the scope should raise an error

  Scenario: ResourceScope temporal_scope defaults to current
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    Then sv temporal_scope should be "current"

  Scenario: ResourceScope rejects invalid temporal_scope
    Then sv creating a scope with temporal_scope "invalid" should raise ValidationError

  Scenario: ResourceScope allows empty resource_ids after filtering
    Then sv creating a scope with no resources should succeed

  Scenario: ResourceScope rejects empty project_names
    Then sv creating a scope with no projects should raise ValidationError

  # ---- ResourceScope path filtering ----

  Scenario: ResourceScope matches_path with include patterns
    Given sv a ResourceScope with include_paths "src/**/*.py", "tests/**/*.py"
    Then sv path "src/main.py" should match
    And sv path "tests/test_foo.py" should match
    And sv path "docs/readme.md" should not match

  Scenario: ResourceScope matches_path with exclude patterns
    Given sv a ResourceScope with exclude_paths "**/node_modules/**", "**/__pycache__/**"
    Then sv path "src/main.py" should match
    And sv path "node_modules/foo/bar.js" should not match
    And sv path "src/__pycache__/foo.pyc" should not match

  Scenario: ResourceScope matches_path with both include and exclude
    Given sv a ResourceScope with include_paths "src/**/*.py" and exclude_paths "**/test_*"
    Then sv path "src/main.py" should match
    And sv path "src/test_main.py" should not match
    And sv path "docs/readme.md" should not match

  # ---- ScopedBackendView: fragment-level filtering ----

  Scenario: ScopedBackendView filters fragments by project name
    Given sv a project-only ScopedBackendView for "proj-a"
    And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
    And sv a fragment "frag-b" in project "proj-b" with resource "RES02"
    Then sv fragment "frag-a" should be visible
    And sv fragment "frag-b" should not be visible

  Scenario: ScopedBackendView excludes fragments with empty project_name
    Given sv a project-only ScopedBackendView for "proj-a"
    And sv a fragment "frag-x" with no project
    Then sv fragment "frag-x" should not be visible

  Scenario: ScopedBackendView filters by resource_ids when set
    Given sv a project-and-resource ScopedBackendView for "proj-a" resources "RES01"
    And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
    And sv a fragment "frag-c" in project "proj-a" with resource "RES99"
    Then sv fragment "frag-a" should be visible
    And sv fragment "frag-c" should not be visible

  Scenario: ScopedBackendView denied_resource_ids blocks specific resources
    Given sv a deny-resource ScopedBackendView for "proj-a" allow "RES01", "RES02" deny "RES02"
    And sv a fragment "frag-a" in project "proj-a" with resource "RES01"
    And sv a fragment "frag-d" in project "proj-a" with resource "RES02"
    Then sv fragment "frag-a" should be visible
    And sv fragment "frag-d" should not be visible

  # ---- ScopedBackendView: backend-level search proxying ----

  Scenario: ScopedBackendView proxies text search with scope injected
    Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
    And sv a ScopedBackendView from that scope
    And sv an InMemoryTextBackend
    When sv I search text for "auth flow"
    Then sv the text search should return an empty list
    And sv the text search should not raise an error

  Scenario: ScopedBackendView proxies vector search with scope injected
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendView from that scope
    And sv an InMemoryVectorBackend
    When sv I search vector with embedding
    Then sv the vector search should return an empty list

  Scenario: ScopedBackendView proxies graph search with scope injected
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendView from that scope
    And sv an InMemoryGraphBackend
    When sv I search graph for "SELECT ?s WHERE { ?s a uko:Container }"
    Then sv the graph search should return an empty GraphResult

  # ---- ScopedBackendView: effective_scope ----

  Scenario: effective_scope returns resource_ids minus denied
    Given sv a triple-resource ScopedBackendView for "p" with "R1", "R2", "R3" denying "R2"
    Then sv effective_scope should contain "R1" and "R3" but not "R2"

  Scenario: effective_scope is empty when all resources denied
    Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
    Then sv effective_scope should be empty

  # ---- ScopedBackendView: from_resource_scope ----

  Scenario: from_resource_scope creates a view with scope's IDs and projects
    Given sv a ResourceScope with two resources "RES01", "RES02" and two projects "local/api", "local/web"
    When sv I create a ScopedBackendView from that ResourceScope
    Then sv the view should allow project "local/api"
    And sv the view should allow project "local/web"
    And sv the view resource_ids should contain "RES01" and "RES02"

  # ---- ScopedBackendSet ----

  Scenario: ScopedBackendSet wraps multiple backends
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with text, vector, and graph backends
    When sv I search text via backend set for "test query"
    Then sv the backend set text search should succeed

  Scenario: ScopedBackendSet with no text backend returns empty list
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with no text backend
    When sv I search text via backend set for "test query"
    Then sv the backend set text search should return empty list

  # ---- ScopeViolationError ----

  Scenario: ScopeViolationError carries resource and project info
    When sv I raise a ScopeViolationError with resource "RES01" and projects "proj-a"
    Then sv the error resource_ids should contain "RES01"
    And sv the error scope_project_names should contain "proj-a"

  # ---- validate_project_scope ----

  Scenario: validate_project_scope passes for valid projects
    Then sv validating projects "proj-a" against available "proj-a", "proj-b" should pass

  Scenario: validate_project_scope rejects out-of-scope projects
    Then sv validating projects "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError

  Scenario: validate_project_scope rejects empty requested projects
    Then sv validating empty requested projects should raise ValueError

  # ---- validate_resource_scope ----

  Scenario: validate_resource_scope passes for in-scope resources
    Given sv a ResourceScope with resources "RES01", "RES02" and projects "local/api"
    Then sv validating resources "RES01" against that scope should pass

  Scenario: validate_resource_scope rejects out-of-scope resources
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    Then sv validating resources "RES99" against that scope should raise ScopeViolationError

  # ---- ResourceAliasResolver ----

  Scenario: Resolve alias to resource_id
    Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
    And sv a ResourceAliasResolver
    When sv I resolve alias "repo"
    Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"

  Scenario: Resolve by resource_id directly
    Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
    And sv a ResourceAliasResolver
    When sv I resolve alias "01HQ8ZDRX50000000000000001"
    Then sv the resolved resource_id should be "01HQ8ZDRX50000000000000001"

  Scenario: Resolve unknown alias returns None
    Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
    And sv a ResourceAliasResolver
    When sv I resolve alias "nonexistent"
    Then sv the resolved resource_id should be None

  Scenario: Resolve rejects empty input
    Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
    And sv a ResourceAliasResolver
    Then sv resolving empty alias should raise ValueError

  Scenario: resolve_many resolves multiple aliases
    Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    And sv a ResourceAliasResolver
    When sv I resolve many aliases "repo", "docs"
    Then sv the resolved set should contain "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"

  Scenario: validate_uniqueness detects duplicate aliases
    Given sv a project "local/myproj" with duplicate alias "repo" on "01HQ8ZDRX50000000000000001" and "01HQ8ZDRX50000000000000002"
    And sv a ResourceAliasResolver
    When sv I validate alias uniqueness
    Then sv there should be 1 uniqueness error mentioning "repo"

  Scenario: validate_uniqueness passes for unique aliases
    Given sv a project "local/myproj" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    And sv a ResourceAliasResolver
    When sv I validate alias uniqueness
    Then sv there should be 0 uniqueness errors

  # ---- resolve_resource_scope ----

  Scenario: resolve_resource_scope collects all resources from projects
    Given sv a project "local/api" with linked resources "01HQ8ZDRX50000000000000001", "01HQ8ZDRX50000000000000002"
    When sv I resolve the resource scope
    Then sv the scope should have 2 resource_ids
    And sv the scope should contain resource "01HQ8ZDRX50000000000000001"
    And sv the scope should contain resource "01HQ8ZDRX50000000000000002"

  Scenario: resolve_resource_scope applies include allowlist
    Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    When sv I resolve the resource scope with include "repo"
    Then sv the scope should have 1 resource_ids
    And sv the scope should contain resource "01HQ8ZDRX50000000000000001"

  Scenario: resolve_resource_scope applies exclude denylist
    Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    When sv I resolve the resource scope with exclude "docs"
    Then sv the scope should have 1 resource_ids
    And sv the scope should contain resource "01HQ8ZDRX50000000000000001"

  Scenario: resolve_resource_scope rejects empty projects
    Then sv resolving scope with empty projects should raise ValueError

  # ---- ContextTierService: scope enforcement ----

  Scenario: get_scoped_by_resource filters by ResourceScope
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
    And sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
    Then sv get_scoped_by_resource should return 1 fragment

  Scenario: store_with_scope_check rejects out-of-scope resource
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    Then sv storing fragment with resource "RES99" should raise ScopeViolationError

  Scenario: store_with_scope_check rejects out-of-scope project
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    Then sv storing fragment with project "evil-proj" and resource "RES01" should raise ScopeViolationError

  Scenario: validate_fragment_scope passes for in-scope fragment
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f3" project "proj-a" resource "RES01" tier "hot"
    Then sv validating fragment "f3" against the scope should pass

  Scenario: validate_fragment_scope rejects out-of-scope fragment
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f4" project "proj-b" resource "RES02" tier "hot"
    Then sv validating fragment "f4" against the scope should raise ScopeViolationError

  # ---- ScopedBackendSet: vector and graph ----

  Scenario: ScopedBackendSet proxies vector search
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with text, vector, and graph backends
    When sv I search vector via backend set
    Then sv the backend set vector search should return empty list

  Scenario: ScopedBackendSet proxies graph search
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with text, vector, and graph backends
    When sv I search graph via backend set for "SELECT ?s WHERE { ?s a uko:X }"
    Then sv the backend set graph search should return empty GraphResult

  # ---- Mixed project scope guard ----

  Scenario: Mixed project scope guard catches unlinked projects
    Then sv validating mixed projects "proj-a", "proj-c" against available "proj-a", "proj-b" should raise ScopeViolationError

  # ---- Coverage: field validator coercion (scoped_view L226, L237) ----

  Scenario: ScopedBackendView coerces list resource_ids to frozenset
    Then sv constructing view with list resource_ids should succeed

  Scenario: ScopedBackendView coerces list denied_resource_ids to frozenset
    Then sv constructing view with list denied_resource_ids should succeed

  # ---- Coverage: effective_scope project-only (scoped_view L273) ----

  Scenario: effective_scope returns empty for project-only view
    Given sv a project-only ScopedBackendView for "proj-a"
    Then sv effective_scope should be empty

  # ---- Coverage: _require_effective_scope raises (scoped_view L326-330) ----

  Scenario: search_text raises when all resources are denied
    Given sv a single-deny ScopedBackendView for "p" allow "R1" deny "R1"
    And sv an InMemoryTextBackend
    Then sv searching text via denied view should raise ScopeViolationError

  # ---- Coverage: ScopedBackendSet None backends (scoped_view L489, L505) ----

  Scenario: ScopedBackendSet search_vector with no vector backend
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with no vector backend
    Then sv searching vector via no-vector set should return empty list

  Scenario: ScopedBackendSet search_graph with no graph backend
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    And sv a ScopedBackendSet with no graph backend
    Then sv searching graph via no-graph set should return empty GraphResult

  # ---- Coverage: resolve_many unresolvable alias (scoped_view L617) ----

  Scenario: resolve_many skips unresolvable aliases
    Given sv a project "local/myproj" with resource "01HQ8ZDRX50000000000000001" aliased "repo"
    And sv a ResourceAliasResolver
    When sv I resolve many aliases "repo", "nonexistent"
    Then sv the resolved set size should be 1
    And sv the resolved set should contain "01HQ8ZDRX50000000000000001"

  # ---- Coverage: validate_uniqueness no-alias branch (scoped_view L638) ----

  Scenario: validate_uniqueness ignores resources without aliases
    Given sv a project with aliased and unaliased resources
    And sv a ResourceAliasResolver
    When sv I validate alias uniqueness
    Then sv there should be 0 uniqueness errors

  # ---- Coverage: validate_project_scope empty available (scoped_view L676) ----

  Scenario: validate_project_scope rejects empty available projects
    Then sv validating projects against empty available should raise ValueError

  # ---- Coverage: validate_resource_scope empty request (scoped_view L710) ----

  Scenario: validate_resource_scope passes for empty requested resources
    Given sv a ResourceScope with resources "RES01" and projects "local/api"
    Then sv validating empty resources against scope should pass

  # ---- Coverage: resolve_resource_scope empty after filter (scoped_view L790) ----

  Scenario: resolve_resource_scope produces empty after include filtering
    Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    When sv I resolve the resource scope with include "nonexistent"
    Then sv the scope should have 0 resource_ids

  # ---- Coverage: context_tiers store empty id (L97) ----

  Scenario: store rejects empty fragment_id
    Given sv a ContextTierService
    Then sv storing fragment with empty id should raise ValueError

  # ---- Coverage: context_tiers get_scoped_view empty (L191) ----

  Scenario: get_scoped_view with empty project_names returns empty
    Given sv a ContextTierService
    Then sv get_scoped_view with empty projects should return empty list

  # ---- Coverage: context_tiers evict_lru empty store (L269) ----

  Scenario: evict_lru on empty tier returns empty list
    Given sv a ContextTierService
    Then sv evict_lru on empty cold tier should return empty list

  # ---- Coverage: context_tiers get_scoped_metrics (L311-316) ----

  Scenario: get_scoped_metrics counts fragments by project
    Given sv a ContextTierService
    When sv I store fragment "m1" project "proj-a" resource "RES01" tier "hot"
    And sv I store fragment "m2" project "proj-b" resource "RES02" tier "warm"
    Then sv get_scoped_metrics for "proj-a" should show 1 hot and 0 warm

  # ---- Coverage: context_tiers get_scoped_by_resource 0 rejected (L355) ----

  Scenario: get_scoped_by_resource returns all when none rejected
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "g1" project "proj-a" resource "RES01" tier "hot"
    Then sv get_scoped_by_resource should return 1 fragment

  # ---- Coverage: context_tiers validate_fragment_scope not found (L382) ----

  Scenario: validate_fragment_scope rejects non-existent fragment
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    Then sv validating non-existent fragment should raise ValueError

  # ---- Coverage: context_tiers store_with_scope_check happy path (L434) ----

  Scenario: store_with_scope_check stores in-scope fragment successfully
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    Then sv store_with_scope_check should accept in-scope fragment

  # ---- Coverage: context_tiers budget property (L443) ----

  Scenario: tier budget property returns TierBudget
    Given sv a ContextTierService
    Then sv tier service budget should be a TierBudget

  # ---- Coverage: context_tiers get_for_actor no project filter (L161) ----

  Scenario: get_for_actor without project filter returns fragments
    Given sv a ContextTierService
    When sv I store fragment "a1" project "proj-a" resource "RES01" tier "hot"
    Then sv get_for_actor without projects should return a list

  # ---- Coverage: context_tiers _summarize_for_cold false branch (L481) ----

  Scenario: demote warm fragment with short content to cold
    Given sv a ContextTierService
    When sv I store a short warm fragment "sf1"
    Then sv demoting "sf1" to cold should keep content intact

  # ---- P0-S1: is_visible denies empty resource_id when resource_ids set ----

  Scenario: is_visible denies fragment with empty resource_id when resource filtering active
    Given sv a ScopedBackendView for projects "proj-a" with resource_ids "RES01"
    When sv I check visibility of a fragment with project "proj-a" and empty resource_id
    Then sv the fragment should NOT be visible

  Scenario: is_visible allows fragment with empty resource_id when no resource filtering
    Given sv a ScopedBackendView for project "proj-a"
    When sv I check visibility of a fragment with project "proj-a" and empty resource_id
    Then sv the fragment should be visible

  # ---- P0-S2: get_scoped enforces scope ----

  Scenario: get_scoped returns fragment when in scope
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f1" project "proj-a" resource "RES01" tier "hot"
    Then sv get_scoped for "f1" should return the fragment

  Scenario: get_scoped returns None for out-of-scope fragment
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f2" project "proj-b" resource "RES02" tier "hot"
    Then sv get_scoped for "f2" should return None

  # ---- P1-SPEC1: DAG expansion via registry_lookup ----

  Scenario: resolve_resource_scope expands child resources via registry_lookup
    Given sv a project "local/api" with two resources "01HQ8ZDRX50000000000000001" aliased "repo" and "01HQ8ZDRX50000000000000002" aliased "docs"
    When sv I resolve scope with registry_lookup expanding "01HQ8ZDRX50000000000000001" to "01HQ8ZDRX50000000000000099"
    Then sv the scope should have 3 resource_ids
    And sv the scope should contain resource "01HQ8ZDRX50000000000000099"

  # ---- P2-B2: tuple coercion in field validators ----

  Scenario: ScopedBackendView accepts tuple for allowed_projects
    When sv I create a ScopedBackendView with tuple allowed_projects
    Then sv the view should have 1 allowed project

  # ---- P0-S2: get_scoped with empty resource_id denied ----

  Scenario: get_scoped denies fragment with empty resource_id when scope has resources
    Given sv a ContextTierService
    And sv a ResourceScope with resources "RES01" and projects "proj-a"
    When sv I store fragment "f3" with project "proj-a" and empty resource_id in tier "hot"
    Then sv get_scoped for "f3" should return None
