# Scoped Backend View

The Scoped Backend View layer provides project-resource isolation for the
Advanced Context Management System (ACMS).  It ensures that actors and
strategies never see resources outside their plan's scope, enforcing
cross-project isolation at both the backend query and fragment visibility
levels.

## Architecture

```
Plan ──► ResourceScope ──► ScopedBackendView ──► Backend Protocol ──► Physical Store
              │                    │
              │                    ├── search_text(backend, query)
              │                    ├── search_vector(backend, embedding)
              │                    ├── search_graph(backend, query)
              │                    └── is_visible(fragment) → bool
              │
              ├── resource_ids: frozenset[str]
              ├── project_names: frozenset[str]
              ├── include_paths / exclude_paths
              └── include_resources / exclude_resources
```

Each plan is assigned a `ResourceScope` at creation time.  The scope is
immutable (frozen) — once created, a plan's visible resources never change.
`ScopedBackendView` wraps backend queries to automatically inject the
`scope` parameter, and `ScopedBackendSet` bundles scoped views for all
three backend types (text, vector, graph).

## ResourceScope

Resolved set of resources and projects visible to a plan.  Created by
`resolve_resource_scope()` during plan creation.

Based on `docs/specification.md` lines 42510--42525.

| Field | Type | Description |
|-------|------|-------------|
| `resource_ids` | `frozenset[str]` | Resource ULIDs in scope (empty = no resources after filtering) |
| `project_names` | `frozenset[str]` | Namespaced project names in scope (min 1) |
| `temporal_scope` | `str` | One of `"current"`, `"recent"`, `"all"` (default: `"current"`) |
| `include_resources` | `tuple[str, ...]` | Resource allowlist (names/aliases). Empty = all |
| `exclude_resources` | `tuple[str, ...]` | Resource denylist. Applied after allowlist |
| `include_paths` | `tuple[str, ...]` | Path glob allowlist. Empty = all |
| `exclude_paths` | `tuple[str, ...]` | Path glob denylist. Applied after allowlist |

### Methods

| Method | Signature | Returns |
|--------|-----------|---------|
| `contains_resource` | `(resource_id: str)` | `bool` |
| `contains_project` | `(project_name: str)` | `bool` |
| `matches_path` | `(path: str)` | `bool` — applies include/exclude glob filters |

Path matching uses `pathlib.PurePath.full_match()` which supports recursive
`**` glob patterns (e.g., `src/**/*.py`).

## ScopedBackendView

Wraps a backend to restrict queries to a resource scope.  Operates at two
levels: backend-level (injects `scope` into queries) and fragment-level
(filters `TieredFragment` by project name and resource ID).

Based on `docs/specification.md` lines 42548--42566.

| Field | Type | Description |
|-------|------|-------------|
| `allowed_projects` | `frozenset[str]` | Project names the actor can access (min 1) |
| `resource_ids` | `frozenset[str]` | Resource ULIDs in scope (empty = project-only filtering) |
| `denied_resource_ids` | `frozenset[str]` | Resource ULIDs explicitly denied (denylist) |

### Methods

| Method | Signature | Returns |
|--------|-----------|---------|
| `is_resource_in_scope` | `(resource_id: str)` | `bool` |
| `effective_scope` | `()` | `frozenset[str]` — `resource_ids` minus `denied_resource_ids` |
| `is_visible` | `(fragment: object)` | `bool` — fragment-level project+resource check |
| `search_text` | `(backend: TextBackend, query: str, *, max_results: int = 20)` | `list[TextResult]` |
| `search_vector` | `(backend: VectorBackend, embedding: list[float], *, top_k: int = 20)` | `list[VectorResult]` |
| `search_graph` | `(backend: GraphBackend, query: str)` | `GraphResult` |
| `from_resource_scope` | `(scope: ResourceScope)` (classmethod) | `ScopedBackendView` |

**Preconditions:**

- `allowed_projects` must be non-empty.
- When `resource_ids` is populated, search methods raise `ScopeViolationError` if
  `effective_scope()` is empty (all resources denied).

**Postconditions:**

- Search results are restricted to in-scope resources via the `scope` parameter.
- Fragments with empty `project_name` are always excluded.

## ScopedBackendSet

Bundle of scoped backend views for text, vector, and graph backends.
Created by `create_scoped_backend_set()` during initial context assembly.

| Field | Type | Description |
|-------|------|-------------|
| `view` | `ScopedBackendView` | The scoped view enforcing isolation |
| `text_backend` | `TextBackend \| None` | Text search backend (None if unavailable) |
| `vector_backend` | `VectorBackend \| None` | Vector search backend (None if unavailable) |
| `graph_backend` | `GraphBackend \| None` | Graph query backend (None if unavailable) |

### Methods

| Method | Signature | Returns |
|--------|-----------|---------|
| `search_text` | `(query: str, *, max_results: int = 20)` | `list[TextResult]` — empty if no backend |
| `search_vector` | `(embedding: list[float], *, top_k: int = 20)` | `list[VectorResult]` — empty if no backend |
| `search_graph` | `(query: str)` | `GraphResult` — empty if no backend |

## ScopeViolationError

Exception raised when a context request references resources outside scope.

| Attribute | Type | Description |
|-----------|------|-------------|
| `resource_ids` | `tuple[str, ...]` | Out-of-scope resource identifiers |
| `scope_project_names` | `tuple[str, ...]` | Project names that define the scope |

## ResourceAliasResolver

Stateless helper that resolves resource aliases to canonical resource ULIDs.
Used during scope resolution to translate user-facing resource names.

| Method | Signature | Returns |
|--------|-----------|---------|
| `resolve` | `(project: NamespacedProject, alias_or_id: str)` | `str \| None` |
| `resolve_many` | `(project: NamespacedProject, names: tuple[str, ...])` | `frozenset[str]` |
| `validate_uniqueness` | `(project: NamespacedProject)` | `list[str]` — error messages for duplicates |

## Factory and Validation Functions

### `create_scoped_backend_set()`

```python
def create_scoped_backend_set(
    scope: ResourceScope,
    *,
    text_backend: TextBackend | None = None,
    vector_backend: VectorBackend | None = None,
    graph_backend: GraphBackend | None = None,
) -> ScopedBackendSet: ...
```

### `resolve_resource_scope()`

```python
def resolve_resource_scope(
    projects: list[NamespacedProject],
    *,
    include_resources: tuple[str, ...] = (),
    exclude_resources: tuple[str, ...] = (),
    include_paths: tuple[str, ...] = (),
    exclude_paths: tuple[str, ...] = (),
    temporal_scope: str = "current",
    registry_lookup: Callable[[str], frozenset[str]] | None = None,
) -> ResourceScope: ...
```

Collects all resource IDs from linked resources across projects, then applies
allowlist/denylist filtering using alias resolution. When `registry_lookup` is
provided, each resource ID is expanded to include its DAG descendants (per
spec §42517--42521).

### `validate_project_scope()`

```python
def validate_project_scope(
    requested_projects: frozenset[str],
    available_projects: frozenset[str],
) -> None: ...  # raises ScopeViolationError
```

### `validate_resource_scope()`

```python
def validate_resource_scope(
    requested_resource_ids: frozenset[str],
    scope: ResourceScope,
) -> None: ...  # raises ScopeViolationError
```

## Usage Examples

### Basic Scope Resolution

```python
from cleveragents.domain.models.acms.scope_resolution import resolve_resource_scope

# Resolve scope from a project — includes all linked resources
scope = resolve_resource_scope([project])

# Allowlist: only include the "repo" resource (by alias)
scope = resolve_resource_scope([project], include_resources=("repo",))

# Denylist: exclude "docs" resource, keep everything else
scope = resolve_resource_scope([project], exclude_resources=("docs",))

# Combined: include only Python files, exclude test files
scope = resolve_resource_scope(
    [project],
    include_paths=("src/**/*.py",),
    exclude_paths=("**/test_*", "**/*_test.py"),
)
```

### Scoped Backend Queries

```python
from cleveragents.domain.models.acms.scoped_view import (
    ScopedBackendView,
    create_scoped_backend_set,
)

# Create a scoped view from a resolved scope
view = ScopedBackendView.from_resource_scope(scope)

# Search with automatic scope injection
results = view.search_text(text_backend, "authentication flow")
vectors = view.search_vector(vector_backend, embedding)
graph = view.search_graph(graph_backend, "SELECT ?s WHERE { ?s a uko:Class }")

# Or use ScopedBackendSet for convenience
backend_set = create_scoped_backend_set(
    scope,
    text_backend=text_backend,
    vector_backend=vector_backend,
    graph_backend=graph_backend,
)
results = backend_set.search_text("authentication flow")
```

### Scope Validation Guards

```python
from cleveragents.domain.models.acms.scope_resolution import (
    validate_project_scope,
    validate_resource_scope,
)
from cleveragents.domain.models.acms.scoped_view import ScopeViolationError

try:
    validate_project_scope(
        requested_projects=frozenset({"local/api", "local/frontend"}),
        available_projects=frozenset({"local/api"}),
    )
except ScopeViolationError as e:
    # "Projects not in scope: local/frontend. Available: local/api"
    print(e)
```

## Module Locations

| Module | Purpose |
|--------|---------|
| `cleveragents.domain.models.acms.scoped_view` | Core types: `ResourceScope`, `ScopedBackendView`, `ScopedBackendSet` |
| `cleveragents.domain.models.acms.scope_resolution` | Resolution: `ResourceAliasResolver`, `resolve_resource_scope`, validation guards |
| `cleveragents.domain.models.acms.tiers` | Re-exports `ScopedBackendView` for backward compat |
| `cleveragents.domain.models.acms` | Package-level re-exports of all public symbols |
| `cleveragents.application.services.context_tiers` | Enforcement hooks (`get_scoped`, `get_scoped_by_resource`, etc.) |

## Related

- [ACMS Backends Reference](acms_backends.md) — Backend protocol definitions
- [Specification: ACMS > Scoped Views](../specification.md) — Authoritative design (lines 42501--42566)
- [Forgejo Issue #193](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/193) — Feature request
