fix(adapter): close final foundation review

This commit is contained in:
2026-07-11 21:14:32 +02:00
parent 45d57756aa
commit 3e2ad7955c
15 changed files with 460 additions and 14 deletions
@@ -0,0 +1,137 @@
# Adapter Foundations final-review fix report
Date: 2026-07-11
Branch: `codex/portable-deployment`
Worktree: `/Users/mp/projects/ThothII/.worktrees/portable-deployment`
Binding findings: `.superpowers/sdd/adapter-final-review-findings.md`
## Outcome
All seven final-review findings are addressed as one coherent adapter-foundations change:
1. HTTP vector reader and writer clients are independently optional. Capabilities reflect the
configured side; writer-only new and legacy configurations build successfully for targeted
writes; search without a reader raises public `VectorReadUnavailable`.
2. `VectorHealth` now reports read/write configured and reachable state independently, preserves
side-specific errors, and reports expected/observed embedding dimensions plus compatibility.
HTTP diagnostics cover read-only, write-only, both-up, and writer-down cases. Direct health
exposes its configured expected dimension without adding schema or migration work.
3. `ThothRestDwhAdapter` accepts `DatabaseIdentityConfig`, matching its resource contract.
4. Both vector adapters reject bools, floats, zero, and negative search limits using one exact
positive-integer guard.
5. Port tests explicitly cover public exports and frozen capability records.
6. A real `tht` subprocess test proves one legacy deprecation warning per config load on stderr
while JSON stdout remains parseable and uncontaminated.
7. The adapter plan and SDD progress explicitly constrain `build_vector_loader` to transitional
bulk sync and schedule its removal/migration in the local pgvector plan. Targeted memory and
solved-question writes remain on `build_vector_store(..., require_write=True)`.
No pgvector schema or migration changes were made.
## Files changed
- `harness/tht/ports/vector.py`
- `harness/tht/ports/__init__.py`
- `harness/tht/adapters/vector/thoth_http.py`
- `harness/tht/adapters/vector/legacy_direct.py`
- `harness/tht/adapters/factory.py`
- `harness/tht/adapters/dwh/thoth_rest.py`
- `harness/tests/test_vector_port_contract.py`
- `harness/tests/test_adapter_factory.py`
- `harness/tests/test_config_resources.py`
- `harness/tests/test_config_legacy_compat.py`
- `harness/tests/test_adapter_command_regressions.py`
- `harness/tests/test_dwh_port_contract.py`
- `docs/superpowers/plans/2026-07-11-adapter-foundations.md`
- `.superpowers/sdd/progress.md`
- `.superpowers/sdd/adapter-final-fix-report.md`
## TDD and verification evidence
RED:
```text
cd harness && .venv/bin/pytest tests/test_vector_port_contract.py \
tests/test_adapter_factory.py tests/test_config_resources.py \
tests/test_config_legacy_compat.py -q
```
Result: collection failed as expected because `VectorReadUnavailable` did not exist. After the
initial implementation, the same command exposed two expected contract/test-harness corrections:
dimension mismatch makes aggregate health unhealthy, and the installed CLI entry point is `tht`
rather than `python -m tht.cli`.
GREEN, covering adapter/config/command regressions:
```text
cd harness && .venv/bin/pytest tests/test_vector_port_contract.py \
tests/test_adapter_factory.py tests/test_config_resources.py \
tests/test_config_legacy_compat.py tests/test_adapter_command_regressions.py \
tests/test_dwh_port_contract.py tests/test_memory_save_one.py \
tests/test_solved_question.py tests/test_search_similar_kinds.py \
tests/test_vector_dual_key.py -q
```
Result: `66 passed in 0.45s`.
Docker availability:
```text
docker info --format '{{.ServerVersion}}'
```
Result: `29.4.1` (available; command required Docker socket access).
Full repository-default non-L2 harness suite, with Docker available for L0 tests:
```text
cd harness && .venv/bin/pytest -q
```
Result: `433 passed, 5 deselected, 17 warnings in 9.14s`. The five deselections are the configured
L2/live-service tests. Warnings are existing legacy-workspace `FutureWarning` emissions.
Scoped lint and diff hygiene:
```text
cd harness && .venv/bin/ruff check tht/ports tht/adapters \
tests/test_vector_port_contract.py tests/test_adapter_factory.py \
tests/test_config_resources.py tests/test_config_legacy_compat.py \
tests/test_adapter_command_regressions.py tests/test_dwh_port_contract.py
git diff --check
```
Result: `All checks passed!`; `git diff --check` produced no output.
## Commit
Commit subject: `fix(adapter): close final foundation review`
The report is part of that same final commit. A Git object cannot contain its own SHA without
changing that SHA; the exact resulting commit ID is therefore recorded in the task handoff from
`git rev-parse HEAD` after creation.
## Self-review
- Reader/writer separation is preserved: search dereferences only `_reader`; hashes/upsert only
`_writer`; health probes each configured client independently and never substitutes one result
for the other.
- Writer failure contributes to aggregate `ok=False`, even when the reader succeeds.
- Dimension compatibility is derived only from configured embedding dimension and existing
`list_tables` metadata. Missing metadata remains `None`, not a guessed success/failure.
- The shared limit guard uses `type(limit) is int`, intentionally rejecting Python booleans and
numeric coercions before either adapter reaches its transport.
- Existing JSON/CLI behavior is preserved; the subprocess regression parses stdout as JSON and
counts exactly one deprecation marker on stderr.
- Scope remains adapter foundations. No vector DDL, schema initialization, or migration work was
introduced.
## Concerns / follow-up
- Write reachability uses the existing `list_tables` diagnostic on the separately authenticated
writer client. Deployments must allow that non-mutating diagnostic RPC to the writer credential;
failures are intentionally visible rather than hidden by reader success.
- Existing legacy-workspace tests emit 17 `FutureWarning`s in the full suite. This wave pins the
required production stderr behavior but does not migrate unrelated test fixtures.
- `build_vector_loader` remains transitional technical debt only for bulk sync, explicitly assigned
to `2026-07-11-local-pgvector-profile.md`.
+14
View File
@@ -0,0 +1,14 @@
# Portable deployment SDD progress
Plan: `docs/superpowers/plans/2026-07-11-adapter-foundations.md`
Branch: `codex/portable-deployment`
Worktree: `/Users/mp/projects/ThothII/.worktrees/portable-deployment`
Task 1: complete (commits e02e61e..a4eb6cc, review clean)
Task 1 final-review follow-up: public exports and frozen capability records now have explicit regressions.
Task 2: complete (commits a4eb6cc..f6302b3, review clean after authorized contract correction)
Task 3: complete (commits f6302b3..fe8d70d, review clean after authorized write-envelope correction)
Task 4: complete (commits fe8d70d..1e0911b, review clean)
Task 4 final-review follow-up: a real `tht` subprocess now proves exactly one legacy warning on stderr and pristine JSON stdout.
Task 5: complete (commits 1e0911b..dbbab6d, review clean after two fix waves)
Final adapter review fix wave: complete (`fix(adapter): close final foundation review`). HTTP vector reader/writer endpoints are independently optional; writer-only targeted memory/solved writes are supported. Vector health reports each side separately plus configured/observed embedding dimensions. `build_vector_loader` remains an explicitly tracked bulk-sync-only exception scheduled for the local pgvector migration plan; it is not used by interactive/targeted writes.
@@ -294,6 +294,13 @@ positive limit, and direct DWH construction injects `cfg.execution.statement_tim
Targeted vector writes consume the factory-returned `VectorStore` and pass
`VectorWriteRecord` objects to `upsert`.
Transitional exception: `build_vector_loader` remains solely for bulk collection sync
(`vector init`/rebuild/index flows). It may still construct the legacy table-scoped writer
directly until `docs/superpowers/plans/2026-07-11-local-pgvector-profile.md` migrates the
local pgvector/vector schema and bulk-sync path. Interactive and targeted writes
(`memory save-one` and solved-question indexing) are not covered by this exception and must
continue through `build_vector_store(..., require_write=True)` and the public vector port.
- [ ] **Step 1: Write exact factory selection and missing-writer tests**
```python
@@ -82,3 +82,35 @@ def test_memory_command_writes_through_factory_vector_store(monkeypatch):
memory_cmd.save_one_cmd(session="s1", decision=7, json_out=True)
from tht.ports.vector import VectorWriteRecord
assert len(captured) == 1 and isinstance(captured[0], VectorWriteRecord)
def test_solved_index_writes_through_writer_only_factory_store(monkeypatch):
writer_only_store = SimpleNamespace(
capabilities=SimpleNamespace(search=False, upsert=True),
existing_hashes=lambda *args: {},
upsert=lambda table, rows: 1,
)
cfg = SimpleNamespace(embeddings=object(), vector_write_rest=object())
manifest = SimpleNamespace(id="s1")
solved_record = object()
calls = []
monkeypatch.setattr(memory_cmd, "has_vector_write_rest", lambda cfg: True)
monkeypatch.setattr(memory_cmd, "load_session_or_exit", lambda cfg, session: manifest)
monkeypatch.setattr(memory_cmd, "session_dir", lambda *args: None)
monkeypatch.setattr(
"tht.adapters.factory.build_vector_store",
lambda cfg, require_write: calls.append(require_write) or writer_only_store,
)
monkeypatch.setattr("tht.cli.sql_cmd.promoted_tables_for", lambda *args: [])
monkeypatch.setattr("tht.solved.build_solved_record", lambda *args: solved_record)
monkeypatch.setattr(
"tht.solved.save_solved_question",
lambda record, *, store, embedder: int(
record is solved_record and store is writer_only_store
),
)
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda cfg: object())
assert memory_cmd.index_solved_session(cfg, "s1") == 1
assert calls == [True]
+11 -2
View File
@@ -6,7 +6,7 @@ from tht.adapters.factory import build_dwh, build_vector_store
from tht.config import Config, ConfigError
def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", writer=True):
def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", reader=True, writer=True):
dwh = (
{
"type": "thoth_rest",
@@ -28,7 +28,7 @@ def _config(*, dwh_type="thoth_rest", vector_type="thoth_vector_http", writer=Tr
vectors = (
{
"type": "thoth_vector_http",
"reader": {"base_url": "https://vectors.test/", "api_key": "reader"},
**({"reader": {"base_url": "https://vectors.test/", "api_key": "reader"}} if reader else {}),
**(
{"writer": {"base_url": "https://vectors.test/", "api_key": "writer"}}
if writer
@@ -78,6 +78,15 @@ def test_factory_selects_http_vector_and_requires_writer():
build_vector_store(config, require_write=True)
def test_factory_builds_writer_only_http_vector_when_write_is_required():
config = _config(reader=False, writer=True)
store = build_vector_store(config, require_write=True)
assert isinstance(store, ThothHttpVectorStore)
assert store.capabilities.search is False
assert store.capabilities.upsert is True
def test_factory_selects_direct_vector_reader():
config = _config(vector_type="pgvector_direct")
@@ -1,10 +1,14 @@
import json
import os
import subprocess
from pathlib import Path
import pytest
from typer.testing import CliRunner
from tht.cli import app
from tht.config import load_config
from tht.adapters.factory import build_vector_store
def _write_old_workspace(tmp_path):
@@ -103,3 +107,42 @@ def test_legacy_warning_does_not_contaminate_cli_json(tmp_path):
assert "DEPRECATION" not in result.stdout
assert result.stderr == ""
assert len(warnings) == 1
def test_legacy_cli_subprocess_warns_once_on_stderr_and_keeps_json_stdout(tmp_path):
workspace = _write_old_workspace(tmp_path)
result = subprocess.run(
[
str(Path(__file__).parents[1] / ".venv" / "bin" / "tht"),
"session",
"list",
"--json",
"-c",
str(workspace),
],
cwd=tmp_path,
env={**os.environ, "PYTHONWARNINGS": "default"},
text=True,
capture_output=True,
check=False,
)
assert result.returncode == 0
json.loads(result.stdout)
assert "DEPRECATION" not in result.stdout
assert result.stderr.count("DEPRECATION") == 1
def test_legacy_writer_only_vector_config_builds_for_targeted_writes(tmp_path):
workspace = _write_old_workspace(tmp_path)
content = workspace.read_text().replace(
"vector_rest:\n base_url: https://vectors.example.test/\n api_key: vector-reader\n",
"",
)
workspace.write_text(content)
with pytest.warns(FutureWarning):
cfg = load_config(workspace)
store = build_vector_store(cfg, require_write=True)
assert store.capabilities.search is False
assert store.capabilities.upsert is True
+20
View File
@@ -69,3 +69,23 @@ vectors:
assert cfg.database.transport == "direct"
assert isinstance(cfg.vectors, PgvectorDirectConfig)
assert cfg.vector_db.db_schema == "vectors"
def test_loads_writer_only_http_vector_resource(tmp_path):
workspace = tmp_path / "workspace.yaml"
workspace.write_text(
"""
dwh:
type: thoth_rest
database: {database: analytics, schema: mart}
endpoint: {base_url: https://dwh.test/, api_key: reader}
vectors:
type: thoth_vector_http
writer: {base_url: https://vectors.test/, api_key: writer}
embeddings: {base_url: http://ollama:11434, dim: 768}
"""
)
cfg = load_config(workspace)
assert cfg.vectors.reader is None
assert cfg.vectors.writer.api_key == "writer"
+8
View File
@@ -55,8 +55,16 @@ def test_contract_types_are_public_and_capabilities_are_immutable():
def test_all_contract_types_are_exported_from_public_package():
from tht.ports import DwhAdapter as PublicDwhAdapter
from tht.ports import DwhCapabilities as PublicDwhCapabilities
from tht.ports import DwhHealth as PublicDwhHealth
from tht.ports import DistinctValues as PublicDistinctValues
from tht.ports import UnsupportedCapability as PublicUnsupportedCapability
result = PublicDistinctValues(values=["a"], truncated=True)
assert result.values == ["a"]
assert result.truncated is True
assert PublicDwhAdapter is DwhAdapter
assert PublicDwhCapabilities is DwhCapabilities
assert PublicDwhHealth is DwhHealth
assert PublicUnsupportedCapability is UnsupportedCapability
@@ -1,13 +1,16 @@
from dataclasses import FrozenInstanceError
from unittest.mock import MagicMock
import pytest
from tht.adapters.vector.thoth_http import ThothHttpVectorStore
from tht.adapters.vector.legacy_direct import LegacyDirectVectorStore
from tht.evidence.model import EvidenceDoc
from tht.ports.vector import (
VectorHit,
VectorRecord,
VectorStore,
VectorReadUnavailable,
VectorWriteRecord,
VectorWriteUnavailable,
)
@@ -24,6 +27,25 @@ def test_http_store_reports_reader_without_writer():
store.upsert("memory", [])
def test_http_store_supports_writer_without_reader():
writer = MagicMock()
store = ThothHttpVectorStore(reader=None, writer=writer, expected_dimension=768)
assert store.capabilities.search is False
assert store.capabilities.existing_hashes is True
assert store.capabilities.upsert is True
with pytest.raises(VectorReadUnavailable):
store.search(["memory"], [0.1], limit=1)
@pytest.mark.parametrize("limit", [True, False, 1.0, 0, -1])
def test_http_search_requires_a_strict_positive_integer_limit(limit):
store = ThothHttpVectorStore(reader=MagicMock(), writer=None)
with pytest.raises(ValueError, match="positive integer"):
store.search(["memory"], [0.1], limit=limit)
def test_http_store_keeps_reader_and_writer_operations_separate():
reader = MagicMock()
reader.search_similar.return_value = [
@@ -139,10 +161,19 @@ def test_vector_contract_is_exported_from_public_packages():
from tht.adapters.vector import ThothHttpVectorStore as PublicHttpStore
from tht.ports import VectorStore as PublicVectorStore
from tht.ports import VectorWriteRecord as PublicVectorWriteRecord
from tht.ports import VectorReadUnavailable as PublicVectorReadUnavailable
assert PublicHttpStore is ThothHttpVectorStore
assert PublicVectorStore is VectorStore
assert PublicVectorWriteRecord is VectorWriteRecord
assert PublicVectorReadUnavailable is VectorReadUnavailable
capabilities = store_capabilities = ThothHttpVectorStore(
reader=MagicMock(), writer=None
).capabilities
assert capabilities.search is True
with pytest.raises(FrozenInstanceError):
store_capabilities.search = False
def test_http_health_uses_reader_list_tables_and_reports_failure():
@@ -154,3 +185,66 @@ def test_http_health_uses_reader_list_tables_and_reports_failure():
health = store.health()
assert health.ok is False
assert health.detail == "offline"
def test_http_health_reports_read_write_and_dimension_status_independently():
reader = MagicMock()
reader.list_tables.return_value = [
{"table_name": "memory", "vector_dimensions": 768}
]
writer = MagicMock()
writer.list_tables.return_value = [
{"table_name": "memory", "vector_dimensions": 768}
]
store = ThothHttpVectorStore(reader, writer, expected_dimension=768)
health = store.health()
assert health.ok is True
assert health.read_configured is True
assert health.read_reachable is True
assert health.write_configured is True
assert health.write_reachable is True
assert health.expected_dimension == 768
assert health.observed_dimensions == (768,)
assert health.dimension_compatible is True
def test_http_health_does_not_hide_writer_failure_behind_reader_success():
reader = MagicMock()
reader.list_tables.return_value = []
writer = MagicMock()
writer.list_tables.side_effect = RuntimeError("writer offline")
store = ThothHttpVectorStore(reader, writer, expected_dimension=768)
health = store.health()
assert health.ok is False
assert health.read_reachable is True
assert health.write_reachable is False
assert health.write_detail == "writer offline"
assert health.dimension_compatible is None
def test_http_health_covers_read_only_and_write_only_configuration():
reader = MagicMock()
reader.list_tables.return_value = [{"vector_dimensions": 384}]
read_health = ThothHttpVectorStore(reader, None, expected_dimension=768).health()
assert read_health.ok is False
assert read_health.write_configured is False
assert read_health.write_reachable is None
assert read_health.dimension_compatible is False
writer = MagicMock()
writer.list_tables.return_value = [{"vector_dimensions": 768}]
write_health = ThothHttpVectorStore(None, writer, expected_dimension=768).health()
assert write_health.ok is True
assert write_health.read_configured is False
assert write_health.read_reachable is None
assert write_health.dimension_compatible is True
@pytest.mark.parametrize("limit", [True, False, 1.0, 0, -1])
def test_legacy_direct_search_requires_a_strict_positive_integer_limit(limit):
store = LegacyDirectVectorStore(engine=MagicMock())
with pytest.raises(ValueError, match="positive integer"):
store.search(["memory"], [0.1], limit=limit)
+2 -2
View File
@@ -1,6 +1,6 @@
"""Thoth/PostgREST implementation of the DWH port."""
from tht.config import DatabaseConfig, RestConfig
from tht.config import DatabaseIdentityConfig, RestConfig
from tht.db.introspect import introspect_rest
from tht.db import sampling
from tht.execute import ExecResult, ExecutionError, PlanSummary
@@ -13,7 +13,7 @@ from tht.rest.execute import explain_rest, run_controlled_rest
class ThothRestDwhAdapter:
capabilities = DwhCapabilities()
def __init__(self, database: DatabaseConfig, rest: RestConfig):
def __init__(self, database: DatabaseIdentityConfig, rest: RestConfig):
self._database = database
self._client = RestClient(rest)
+2 -3
View File
@@ -41,13 +41,12 @@ def build_vector_store(cfg: Config, *, require_write: bool = False) -> VectorSto
dim=dim,
)
case "thoth_vector_http":
if resource.reader is None:
raise ConfigError("Vector reader non configurato")
if require_write and resource.writer is None:
raise ConfigError("Vector writer non configurato")
return ThothHttpVectorStore(
VectorRestClient(resource.reader),
VectorRestClient(resource.reader) if resource.reader is not None else None,
VectorRestClient(resource.writer) if resource.writer is not None else None,
expected_dimension=cfg.embeddings.dim if cfg.embeddings is not None else None,
)
case other: # pragma: no cover - Pydantic's discriminator rejects this first.
raise ConfigError(f"Adapter vector non supportato: {other}")
+16 -2
View File
@@ -7,6 +7,7 @@ from tht.ports.vector import (
VectorHealth,
VectorWriteRecord,
VectorWriteUnavailable,
require_positive_limit,
)
from tht.vectorstore.store import VectorHit, VectorStore as TableVectorStore
@@ -26,8 +27,20 @@ class LegacyDirectVectorStore:
with self._engine.connect() as connection:
connection.exec_driver_sql("SELECT 1")
except Exception as exc:
return VectorHealth(ok=False, detail=str(exc))
return VectorHealth(ok=True)
return VectorHealth(
ok=False,
detail=str(exc),
read_configured=True,
read_reachable=False,
read_detail=str(exc),
expected_dimension=self._dim,
)
return VectorHealth(
ok=True,
read_configured=True,
read_reachable=True,
expected_dimension=self._dim,
)
def search(
self,
@@ -37,6 +50,7 @@ class LegacyDirectVectorStore:
limit: int,
kinds: list[str] | None = None,
) -> list[VectorHit]:
require_positive_limit(limit)
hits: list[VectorHit] = []
for collection in collections:
table = TableVectorStore(
+52 -5
View File
@@ -4,8 +4,10 @@ from tht.ports.vector import (
VectorCapabilities,
VectorHealth,
VectorHit,
VectorReadUnavailable,
VectorWriteRecord,
VectorWriteUnavailable,
require_positive_limit,
)
from tht.vectorstore.rest_client import VectorRestClient
from tht.vectorstore.store import hit_from_metadata
@@ -18,21 +20,63 @@ def _merge(hits: list[VectorHit], limit: int) -> list[VectorHit]:
class ThothHttpVectorStore:
"""Vector port backed by the existing allowlisted REST RPCs."""
def __init__(self, reader: VectorRestClient, writer: VectorRestClient | None):
def __init__(
self,
reader: VectorRestClient | None,
writer: VectorRestClient | None,
expected_dimension: int | None = None,
):
self._reader = reader
self._writer = writer
self._expected_dimension = expected_dimension
@property
def capabilities(self) -> VectorCapabilities:
writable = self._writer is not None
return VectorCapabilities(search=True, existing_hashes=writable, upsert=writable)
return VectorCapabilities(
search=self._reader is not None, existing_hashes=writable, upsert=writable
)
def health(self) -> VectorHealth:
read_reachable, read_detail, read_tables = self._probe(self._reader)
write_reachable, write_detail, write_tables = self._probe(self._writer)
dimensions = tuple(sorted({
dimension
for row in [*read_tables, *write_tables]
if type(dimension := row.get("vector_dimensions")) is int
}))
compatible = (
None
if self._expected_dimension is None or not dimensions
else dimensions == (self._expected_dimension,)
)
reachable = [
status for status in (read_reachable, write_reachable) if status is not None
]
ok = bool(reachable) and all(reachable) and compatible is not False
details = [detail for detail in (read_detail, write_detail) if detail]
return VectorHealth(
ok=ok,
detail="; ".join(details) or None,
read_configured=self._reader is not None,
read_reachable=read_reachable,
read_detail=read_detail,
write_configured=self._writer is not None,
write_reachable=write_reachable,
write_detail=write_detail,
expected_dimension=self._expected_dimension,
observed_dimensions=dimensions,
dimension_compatible=compatible,
)
@staticmethod
def _probe(client: VectorRestClient | None) -> tuple[bool | None, str | None, list[dict]]:
if client is None:
return None, None, []
try:
self._reader.list_tables()
return True, None, client.list_tables()
except Exception as exc:
return VectorHealth(ok=False, detail=str(exc))
return VectorHealth(ok=True)
return False, str(exc), []
def search(
self,
@@ -42,6 +86,9 @@ class ThothHttpVectorStore:
limit: int,
kinds: list[str] | None = None,
) -> list[VectorHit]:
require_positive_limit(limit)
if self._reader is None:
raise VectorReadUnavailable("Vector reader credential is not configured")
hits: list[VectorHit] = []
for collection in collections:
rows = self._reader.search_similar(collection, embedding, limit, kinds=kinds)
+2
View File
@@ -12,6 +12,7 @@ from tht.ports.vector import (
VectorHealth,
VectorHit,
VectorRecord,
VectorReadUnavailable,
VectorStore,
VectorStoreError,
VectorWriteRecord,
@@ -28,6 +29,7 @@ __all__ = [
"VectorHealth",
"VectorHit",
"VectorRecord",
"VectorReadUnavailable",
"VectorStore",
"VectorStoreError",
"VectorWriteRecord",
+20
View File
@@ -18,6 +18,15 @@ class VectorCapabilities:
class VectorHealth:
ok: bool
detail: str | None = None
read_configured: bool = False
read_reachable: bool | None = None
read_detail: str | None = None
write_configured: bool = False
write_reachable: bool | None = None
write_detail: str | None = None
expected_dimension: int | None = None
observed_dimensions: tuple[int, ...] = ()
dimension_compatible: bool | None = None
@dataclass(frozen=True)
@@ -37,6 +46,16 @@ class VectorWriteUnavailable(VectorStoreError):
"""Raised when a deployment has no vector writer credential."""
class VectorReadUnavailable(VectorStoreError):
"""Raised when a deployment has no vector reader credential."""
def require_positive_limit(limit: int) -> None:
"""Reject coercible values: vector limits are exact positive integers."""
if type(limit) is not int or limit <= 0:
raise ValueError("Vector search limit must be a positive integer")
@runtime_checkable
class VectorStore(Protocol):
@property
@@ -63,6 +82,7 @@ __all__ = [
"VectorHealth",
"VectorHit",
"VectorRecord",
"VectorReadUnavailable",
"VectorStore",
"VectorStoreError",
"VectorWriteRecord",