184 lines
5.1 KiB
Python
184 lines
5.1 KiB
Python
"""Application-owned ports implemented by replaceable persistence adapters."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sqlite3
|
|
from collections.abc import Iterable, Mapping
|
|
from dataclasses import dataclass
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Protocol
|
|
|
|
from gewu.domain.contracts import AutoAcceptPolicy, ProjectFacetLabel, ProjectRecord
|
|
|
|
|
|
class CanonicalRepositoryError(RuntimeError):
|
|
"""A committed-record operation failed without exposing backend details."""
|
|
|
|
|
|
class CanonicalConflictError(CanonicalRepositoryError):
|
|
"""Expected-head publication lost its compare-and-swap race."""
|
|
|
|
|
|
_CANONICAL_PATH = re.compile(r"^[A-Za-z0-9._/-]+$")
|
|
|
|
|
|
def validate_canonical_path(path: str) -> None:
|
|
"""Enforce portable, relative POSIX paths for all repository implementations."""
|
|
|
|
pure = PurePosixPath(path)
|
|
if (
|
|
not path
|
|
or path.startswith("/")
|
|
or "\\" in path
|
|
or not _CANONICAL_PATH.fullmatch(path)
|
|
or pure.as_posix() != path
|
|
or any(part in {"", ".", ".."} for part in pure.parts)
|
|
):
|
|
raise CanonicalRepositoryError("Canonical record path is invalid")
|
|
|
|
|
|
class CanonicalRepository(Protocol):
|
|
"""Committed reads and expected-head atomic publication.
|
|
|
|
``history`` returns newest-first revisions that changed the requested path;
|
|
``introducing_commit`` returns the oldest revision in that path history.
|
|
Paths use ``validate_canonical_path``. Implementations raise the port-owned
|
|
errors above, never backend exceptions.
|
|
"""
|
|
|
|
def head(self) -> str: ...
|
|
|
|
def read_file(self, revision: str, path: str) -> bytes | None: ...
|
|
|
|
def list_files(self, revision: str, prefix: str) -> tuple[str, ...]: ...
|
|
|
|
def publish_files(
|
|
self,
|
|
*,
|
|
expected_head: str,
|
|
files: Mapping[str, bytes],
|
|
message: str,
|
|
index_directory: Path,
|
|
) -> str: ...
|
|
|
|
def introducing_commit(self, revision: str, path: str) -> str | None: ...
|
|
|
|
def history(self, revision: str, path: str, limit: int) -> tuple[str, ...]: ...
|
|
|
|
|
|
class ProjectRegistry(Protocol):
|
|
"""Versioned project and policy registry with serialized lifecycle writes."""
|
|
|
|
def records(self) -> tuple[ProjectRecord, ...]: ...
|
|
|
|
def create(self, project: ProjectRecord, policy: AutoAcceptPolicy) -> ProjectRecord: ...
|
|
|
|
def update_content(
|
|
self, project_id: str, *, description: str, north_star: str, key_questions: tuple[str, ...]
|
|
) -> ProjectRecord: ...
|
|
|
|
def archive(self, project_id: str) -> ProjectRecord: ...
|
|
|
|
def add_facet_labels(
|
|
self, project_id: str, labels: tuple[ProjectFacetLabel, ...]
|
|
) -> ProjectRecord: ...
|
|
|
|
|
|
class EmbeddingProvider(Protocol):
|
|
"""Replaceable, normalized text-vector provider for derived retrieval indexes."""
|
|
|
|
@property
|
|
def dimensions(self) -> int: ...
|
|
|
|
@property
|
|
def model_id(self) -> str: ...
|
|
|
|
@property
|
|
def artifact_sha256(self) -> str: ...
|
|
|
|
def embed_queries(self, texts: tuple[str, ...]) -> tuple[tuple[float, ...], ...]: ...
|
|
|
|
def embed_passages(self, texts: tuple[str, ...]) -> tuple[tuple[float, ...], ...]: ...
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class VectorHit:
|
|
node_id: str
|
|
project_id: str
|
|
distance: float
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class VectorGeneration:
|
|
generation_id: str
|
|
revision: str
|
|
model_id: str
|
|
artifact_sha256: str
|
|
content_profile: str
|
|
dimensions: int
|
|
node_count: int
|
|
|
|
|
|
class VectorIndex(Protocol):
|
|
"""Scoped vector candidate lookup over an activated immutable generation."""
|
|
|
|
def search(
|
|
self,
|
|
*,
|
|
query: tuple[float, ...],
|
|
project_ids: tuple[str, ...],
|
|
include_shared: bool,
|
|
limit: int,
|
|
expected_generation_id: str | None = None,
|
|
) -> tuple[VectorHit, ...]: ...
|
|
|
|
def active_generation(self) -> VectorGeneration | None: ...
|
|
|
|
def generation(self, generation_id: str) -> VectorGeneration | None: ...
|
|
|
|
def build_generation(
|
|
self,
|
|
*,
|
|
generation_id: str,
|
|
revision: str,
|
|
model_id: str,
|
|
artifact_sha256: str,
|
|
content_profile: str,
|
|
dimensions: int,
|
|
vectors: Iterable[tuple[str, str, tuple[float, ...]]],
|
|
) -> None: ...
|
|
|
|
def activate(self, generation_id: str, *, expected_active: str | None) -> str | None: ...
|
|
|
|
def active_generation_in_transaction(
|
|
self, connection: sqlite3.Connection
|
|
) -> VectorGeneration | None: ...
|
|
|
|
def generation_in_transaction(
|
|
self, connection: sqlite3.Connection, generation_id: str
|
|
) -> VectorGeneration | None: ...
|
|
|
|
def build_generation_in_transaction(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
*,
|
|
generation_id: str,
|
|
revision: str,
|
|
model_id: str,
|
|
artifact_sha256: str,
|
|
content_profile: str,
|
|
dimensions: int,
|
|
vectors: Iterable[tuple[str, str, tuple[float, ...]]],
|
|
) -> None: ...
|
|
|
|
def activate_in_transaction(
|
|
self,
|
|
connection: sqlite3.Connection,
|
|
generation_id: str,
|
|
*,
|
|
expected_active: str | None,
|
|
) -> str | None: ...
|
|
|
|
def deactivate_in_transaction(self, connection: sqlite3.Connection) -> None: ...
|