"""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: ...