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