345 lines
14 KiB
Markdown
345 lines
14 KiB
Markdown
# Adapter Foundations Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Route all DWH and vector operations through stable typed contracts while preserving current direct/REST behavior.
|
|
|
|
**Architecture:** Define small Python protocols and capability records, implement adapters around existing modules, and centralize construction in one factory. Introduce a discriminated workspace schema with a compatibility translator for current YAML.
|
|
|
|
**Tech Stack:** Python 3.11+, `typing.Protocol`, Pydantic 2, SQLAlchemy, psycopg2, requests, pytest, Typer.
|
|
|
|
## Global Constraints
|
|
|
|
- Do not change workflow phases or CLI output formats.
|
|
- PostgreSQL direct and current Thoth/PostgREST are the only supported DWH transports in this plan.
|
|
- Preserve reader/writer vector credential separation.
|
|
- All migrations must accept existing `database.transport`, `rest`, `vector_db`, `vector_rest`, and `vector_write_rest` fields.
|
|
- JSON stdout stays pristine; diagnostics go to stderr unless part of the JSON result.
|
|
|
|
---
|
|
|
|
### Task 1: Define DWH ports and capabilities
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/ports/__init__.py`
|
|
- Create: `harness/tht/ports/dwh.py`
|
|
- Test: `harness/tests/test_dwh_port_contract.py`
|
|
|
|
**Interfaces:**
|
|
- Produces: `DwhCapabilities`, `DwhAdapter`, `DwhHealth`, `DistinctValues`, and `UnsupportedCapability`.
|
|
- Consumes: existing catalog models from `tht.db.introspect` and execution result types from `tht.db.execute`.
|
|
|
|
- [ ] **Step 1: Write the failing protocol-shape test**
|
|
|
|
```python
|
|
def test_fake_adapter_satisfies_runtime_protocol():
|
|
adapter = FakeDwhAdapter()
|
|
assert isinstance(adapter, DwhAdapter)
|
|
assert adapter.capabilities.explain is True
|
|
assert adapter.health().ok is True
|
|
```
|
|
|
|
- [ ] **Step 2: Run the focused test and confirm failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_dwh_port_contract.py -q`
|
|
Expected: FAIL because `tht.ports.dwh` does not exist.
|
|
|
|
- [ ] **Step 3: Add the minimal public contract**
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class DwhCapabilities:
|
|
introspection: bool = True
|
|
explain: bool = True
|
|
sampling: bool = True
|
|
distinct_values: bool = True
|
|
|
|
@dataclass(frozen=True)
|
|
class DistinctValues:
|
|
values: list[object]
|
|
truncated: bool
|
|
|
|
@runtime_checkable
|
|
class DwhAdapter(Protocol):
|
|
@property
|
|
def capabilities(self) -> DwhCapabilities: ...
|
|
def health(self) -> DwhHealth: ...
|
|
def introspect(self) -> PhysicalSchema: ...
|
|
def run_query(self, sql: str, *, limit: int) -> ExecResult: ...
|
|
def explain(self, sql: str) -> PlanSummary: ...
|
|
def sample_column(self, table: str, column: str, *, limit: int) -> list[object]: ...
|
|
def distinct_values(self, table: str, column: str) -> DistinctValues: ...
|
|
```
|
|
|
|
- [ ] **Step 4: Run contract test and type-oriented import smoke test**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_dwh_port_contract.py -q`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add harness/tht/ports harness/tests/test_dwh_port_contract.py
|
|
git commit -m "refactor(dwh): define adapter contract"
|
|
```
|
|
|
|
### Task 2: Wrap direct and REST DWH implementations
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/adapters/dwh/__init__.py`
|
|
- Create: `harness/tht/adapters/dwh/postgres.py`
|
|
- Create: `harness/tht/adapters/dwh/thoth_rest.py`
|
|
- Test: `harness/tests/test_dwh_adapters.py`
|
|
- Test: `harness/tests/test_dwh_port_contract.py`
|
|
- Test: `harness/tests/l0/test_db_sampling.py`
|
|
- Modify: `harness/tht/ports/__init__.py`
|
|
- Modify: `harness/tht/ports/dwh.py`
|
|
- Modify: `harness/tht/execute/__init__.py`
|
|
- Modify: `harness/tht/db/execute.py`
|
|
- Modify: `harness/tht/db/sampling.py`
|
|
- Modify: `harness/tht/rest/execute.py`
|
|
- Modify: `docs/superpowers/plans/2026-07-11-adapter-foundations.md`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `DwhAdapter` from Task 1; existing `DatabaseConfig`, `RestConfig`, catalog, sampling, execute, and explain functions.
|
|
- Produces: `PostgresDwhAdapter(config)` and `ThothRestDwhAdapter(database, rest)`.
|
|
|
|
- [ ] **Step 1: Add parametrized contract tests for both wrappers**
|
|
|
|
```python
|
|
@pytest.mark.parametrize("factory", [postgres_factory, rest_factory])
|
|
def test_adapter_rejects_write_sql(factory):
|
|
with pytest.raises(ExecutionError):
|
|
factory().run_query("delete from fact_sales", limit=10)
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_dwh_adapters.py -q`
|
|
Expected: FAIL because the adapter classes are absent.
|
|
|
|
- [ ] **Step 3: Implement thin wrappers, without duplicating transport logic**
|
|
|
|
```python
|
|
class PostgresDwhAdapter:
|
|
capabilities = DwhCapabilities()
|
|
def __init__(self, config: DatabaseConfig):
|
|
self._config = config
|
|
self._engine = make_engine(config)
|
|
def run_query(self, sql: str, *, limit: int) -> ExecResult:
|
|
return run_query(self._engine, sql, limit=limit)
|
|
```
|
|
|
|
Implement the analogous REST wrapper by delegating to `tht.rest.*`; translate transport-specific
|
|
errors only at the adapter boundary. Both wrappers delegate frequency-ranked, distinct sampling to
|
|
the paired implementations in `tht.db.sampling`. Query and sampling limits must be runtime-positive
|
|
integers (booleans and floats are rejected), and `distinct_values` reports any cap through
|
|
`DistinctValues.truncated`.
|
|
|
|
- [ ] **Step 4: Run adapter, read-only, sampling, and REST tests**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_dwh_adapters.py tests/test_readonly_guard.py tests/test_rest_client.py tests/l0/test_db_sampling.py -q`
|
|
Expected: PASS; L0 may deselect when Docker is unavailable.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add docs/superpowers/plans/2026-07-11-adapter-foundations.md \
|
|
harness/tht/ports harness/tht/adapters/dwh harness/tht/execute/__init__.py \
|
|
harness/tht/db/execute.py harness/tht/db/sampling.py harness/tht/rest/execute.py \
|
|
harness/tests/test_dwh_port_contract.py harness/tests/test_dwh_adapters.py \
|
|
harness/tests/l0/test_db_sampling.py
|
|
git commit -m "refactor(dwh): adapt direct and REST transports"
|
|
```
|
|
|
|
### Task 3: Define vector port and wrappers
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/ports/vector.py`
|
|
- Create: `harness/tht/adapters/vector/__init__.py`
|
|
- Create: `harness/tht/adapters/vector/thoth_http.py`
|
|
- Create: `harness/tht/adapters/vector/legacy_direct.py`
|
|
- Test: `harness/tests/test_vector_port_contract.py`
|
|
- Modify: `harness/tht/vectorstore/reader.py`
|
|
|
|
**Interfaces:**
|
|
- Produces: `VectorStore`, `VectorCapabilities`, `VectorHealth`, `VectorRecord`,
|
|
`VectorWriteRecord`, `VectorHit`, `ThothHttpVectorStore`.
|
|
- Preserves: current `VectorRestClient`, `DirectSearcher`, and `RestSearcher` behavior behind wrappers.
|
|
|
|
- [ ] **Step 1: Write read/write capability and dual-credential tests**
|
|
|
|
```python
|
|
def test_http_store_reports_reader_without_writer():
|
|
store = ThothHttpVectorStore(reader=reader, writer=None)
|
|
assert store.capabilities.search is True
|
|
assert store.capabilities.upsert is False
|
|
with pytest.raises(VectorWriteUnavailable):
|
|
store.upsert("memory", [])
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_vector_port_contract.py -q`
|
|
Expected: FAIL because the vector port is absent.
|
|
|
|
- [ ] **Step 3: Implement the vector contract and wrappers**
|
|
|
|
```python
|
|
@runtime_checkable
|
|
class VectorStore(Protocol):
|
|
@property
|
|
def capabilities(self) -> VectorCapabilities: ...
|
|
def health(self) -> VectorHealth: ...
|
|
def search(self, collections: list[str], embedding: list[float], *, limit: int,
|
|
kinds: list[str] | None = None) -> list[VectorHit]: ...
|
|
def existing_hashes(self, collection: str, kinds: list[str]) -> dict[str, str]: ...
|
|
def upsert(self, collection: str, records: list[VectorWriteRecord]) -> int: ...
|
|
```
|
|
|
|
`VectorWriteRecord` is the transport-neutral write envelope: it contains the canonical
|
|
`VectorRecord`, a precomputed embedding, and a content hash. Adapters must preserve
|
|
`VectorRecord.metadata` unchanged, including semantic keys named `embedding` or `content_hash`.
|
|
|
|
- [ ] **Step 4: Run vector regression tests**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_vector_port_contract.py tests/test_vector_dual_key.py tests/test_search_similar_kinds.py tests/test_memory_save_one.py tests/test_solved_question.py -q`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add harness/tht/ports/vector.py harness/tht/adapters/vector harness/tht/vectorstore/reader.py harness/tests/test_vector_port_contract.py
|
|
git commit -m "refactor(vector): define store contract"
|
|
```
|
|
|
|
### Task 4: Introduce discriminated resource configuration with legacy translation
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/config_compat.py`
|
|
- Modify: `harness/tht/config.py`
|
|
- Modify: `harness/workspaces/tht.example.yaml`
|
|
- Test: `harness/tests/test_config_resources.py`
|
|
- Test: `harness/tests/test_config_legacy_compat.py`
|
|
|
|
**Interfaces:**
|
|
- Produces: `DwhResourceConfig`, `VectorResourceConfig`, `WorkspaceRoots` and `translate_legacy_config(raw)`.
|
|
- Consumes: existing YAML environment expansion and `ConfigError` behavior.
|
|
|
|
- [ ] **Step 1: Write new-schema and legacy-equivalence tests**
|
|
|
|
```python
|
|
def test_legacy_rest_workspace_equals_new_resource_schema(tmp_path):
|
|
old = load_config(write_old_workspace(tmp_path))
|
|
new = load_config(write_new_workspace(tmp_path))
|
|
assert old.dwh.model_dump() == new.dwh.model_dump()
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_config_resources.py tests/test_config_legacy_compat.py -q`
|
|
Expected: FAIL because `dwh` and discriminated vector resources are absent.
|
|
|
|
- [ ] **Step 3: Add discriminated models and an isolated translator**
|
|
|
|
```python
|
|
class PostgresDwhConfig(BaseModel):
|
|
type: Literal["postgres_direct"]
|
|
connection: DatabaseConfig
|
|
|
|
class ThothRestDwhConfig(BaseModel):
|
|
type: Literal["thoth_rest"]
|
|
database: DatabaseIdentityConfig
|
|
endpoint: RestConfig
|
|
|
|
DwhResourceConfig = Annotated[
|
|
PostgresDwhConfig | ThothRestDwhConfig,
|
|
Field(discriminator="type"),
|
|
]
|
|
```
|
|
|
|
Translate legacy keys before Pydantic validation and emit one deprecation warning to stderr, never stdout.
|
|
|
|
- [ ] **Step 4: Run all config and workspace tests**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_config_resources.py tests/test_config_legacy_compat.py tests/test_workspace.py tests/test_vector_dual_key.py -q`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add harness/tht/config.py harness/tht/config_compat.py harness/workspaces/tht.example.yaml harness/tests/test_config_resources.py harness/tests/test_config_legacy_compat.py
|
|
git commit -m "feat(config): add typed resource schema"
|
|
```
|
|
|
|
### Task 5: Centralize construction and migrate command call sites
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/adapters/factory.py`
|
|
- Modify: `harness/tht/cli/db_cmd.py`
|
|
- Modify: `harness/tht/cli/schema_cmd.py`
|
|
- Modify: `harness/tht/cli/sql_cmd.py`
|
|
- Modify: `harness/tht/cli/lsh_cmd.py`
|
|
- Modify: `harness/tht/cli/search_cmd.py`
|
|
- Modify: `harness/tht/cli/vector_cmd.py`
|
|
- Modify: `harness/tht/cli/memory_cmd.py`
|
|
- Test: `harness/tests/test_adapter_factory.py`
|
|
|
|
**Interfaces:**
|
|
- Produces: `build_dwh(cfg: Config) -> DwhAdapter` and `build_vector_store(cfg: Config, *, require_write: bool = False) -> VectorStore`.
|
|
- Consumes: resource configs from Task 4 and wrappers from Tasks 2-3.
|
|
|
|
Correction: `DwhAdapter.distinct_values(table, column, *, limit)` requires an explicit
|
|
positive limit, and direct DWH construction injects `cfg.execution.statement_timeout_ms`.
|
|
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
|
|
def test_factory_selects_http_vector_and_requires_writer(config):
|
|
assert isinstance(build_vector_store(config), ThothHttpVectorStore)
|
|
with pytest.raises(ConfigError, match="writer"):
|
|
build_vector_store(config, require_write=True)
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_adapter_factory.py -q`
|
|
Expected: FAIL because the factory does not exist.
|
|
|
|
- [ ] **Step 3: Implement factory and replace per-command branching**
|
|
|
|
```python
|
|
def build_dwh(cfg: Config) -> DwhAdapter:
|
|
match cfg.dwh.type:
|
|
case "postgres_direct": return PostgresDwhAdapter(cfg.dwh.connection)
|
|
case "thoth_rest": return ThothRestDwhAdapter(cfg.dwh.database, cfg.dwh.endpoint)
|
|
case other: raise ConfigError(f"Adapter DWH non supportato: {other}")
|
|
```
|
|
|
|
Delete transport checks from migrated commands; keep CLI wording and exit codes stable.
|
|
|
|
- [ ] **Step 4: Run focused command suites, full harness suite, and ruff**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_adapter_factory.py tests/test_schema_introspect_guard.py tests/test_sql_preview_json.py tests/test_search_pack.py tests/integration/test_gate_cli_signatures.py -q`
|
|
Expected: PASS.
|
|
Run: `cd harness && .venv/bin/pytest -q`
|
|
Expected: all non-L2 tests PASS.
|
|
Run: `cd harness && .venv/bin/ruff check tht tests/test_adapter_factory.py tests/test_dwh_port_contract.py tests/test_dwh_adapters.py tests/test_vector_port_contract.py tests/test_config_resources.py tests/test_config_legacy_compat.py`
|
|
Expected: no errors.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add harness/tht harness/tests harness/workspaces/tht.example.yaml
|
|
git commit -m "refactor(core): route integrations through adapter factory"
|
|
```
|