Skip to content

Commit 5cd42a9

Browse files
phernandezclaude
andcommitted
refactor(core): collapse project-index batch-store Protocols to concrete
ProjectIndexMoveBatchStore and ProjectIndexDeleteBatchStore each had exactly one implementation — RepositoryProjectIndexMaintenanceStore — which cloud uses too (apps/cloud/services/index/project_runtime.py passes the same concrete as both move_store and delete_store). Neither was a local/cloud seam; they were pure test-injection indirection for the batch-orchestration functions. Retype StoreProjectIndexMaintenanceRunner, run_project_index_move_batches, run_project_index_delete_batches, and ProjectIndexRuntime's move_store/ delete_store fields to the concrete store, and delete both Protocols. The orchestration tests that used recording batch-store doubles now inject the real concrete and monkeypatch its single SQL batch method to return canned results, preserving accumulation/progress coverage without a database. Part of the protocol-surface reduction from the local/cloud interface audit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 9015127 commit 5cd42a9

4 files changed

Lines changed: 143 additions & 78 deletions

File tree

src/basic_memory/indexing/project_index_maintenance.py

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,24 +58,6 @@ async def refresh_moved_entities(self, entity_ids: Sequence[int]) -> None:
5858
"""Refresh search rows for moved entity ids."""
5959

6060

61-
class ProjectIndexMoveBatchStore(Protocol):
62-
"""Capability that applies one project-index move-maintenance batch."""
63-
64-
async def apply_project_index_move_batch(
65-
self,
66-
move_batch: ProjectIndexMoveBatch,
67-
) -> ProjectIndexMoveBatchResult: ...
68-
69-
70-
class ProjectIndexDeleteBatchStore(Protocol):
71-
"""Capability that applies one project-index delete-maintenance batch."""
72-
73-
async def apply_project_index_delete_batch(
74-
self,
75-
delete_batch: ProjectIndexDeleteBatch,
76-
) -> ProjectIndexDeleteBatchResult: ...
77-
78-
7961
@dataclass(frozen=True, slots=True)
8062
class ProjectIndexMovedFile:
8163
"""One indexed file move that may need storage-backed metadata repair."""
@@ -655,8 +637,8 @@ async def apply_project_index_delete_batch(
655637
class StoreProjectIndexMaintenanceRunner(ProjectIndexMaintenanceRunner):
656638
"""Run project-index maintenance through explicit move/delete batch stores."""
657639

658-
move_store: ProjectIndexMoveBatchStore
659-
delete_store: ProjectIndexDeleteBatchStore
640+
move_store: RepositoryProjectIndexMaintenanceStore
641+
delete_store: RepositoryProjectIndexMaintenanceStore
660642

661643
async def run_move_batches(
662644
self,
@@ -768,7 +750,7 @@ async def run_project_index_move_batches(
768750
*,
769751
moved_files: Mapping[str, str],
770752
batch_size: int,
771-
move_store: ProjectIndexMoveBatchStore,
753+
move_store: RepositoryProjectIndexMaintenanceStore,
772754
) -> ProjectIndexMoveRun:
773755
"""Apply project-index move maintenance through a storage adapter."""
774756
move_plan = build_project_index_move_batch_plan(
@@ -821,7 +803,7 @@ async def run_project_index_delete_batches(
821803
*,
822804
deleted_paths: Sequence[str],
823805
batch_size: int,
824-
delete_store: ProjectIndexDeleteBatchStore,
806+
delete_store: RepositoryProjectIndexMaintenanceStore,
825807
) -> ProjectIndexDeleteRun:
826808
"""Apply project-index delete maintenance through a storage adapter."""
827809
delete_plan = build_project_index_delete_batch_plan(

src/basic_memory/indexing/project_index_runtime.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
)
3131
from basic_memory.indexing.progress import VectorSyncProgress
3232
from basic_memory.indexing.project_index_maintenance import (
33-
ProjectIndexDeleteBatchStore,
3433
ProjectIndexDeleteRun,
35-
ProjectIndexMoveBatchStore,
3634
ProjectIndexMoveRun,
3735
RepositoryProjectIndexMaintenanceStore,
3836
run_project_index_delete_batches,
@@ -108,8 +106,8 @@ class ProjectIndexRuntime:
108106
project_id: ProjectId
109107
vector_sync: VectorSyncExecutor
110108
vector_entity_source: VectorSyncEntitySource
111-
move_store: ProjectIndexMoveBatchStore
112-
delete_store: ProjectIndexDeleteBatchStore
109+
move_store: RepositoryProjectIndexMaintenanceStore
110+
delete_store: RepositoryProjectIndexMaintenanceStore
113111
forward_reference_relation_source: ForwardReferenceRelationSource
114112
forward_reference_resolution_runtime: ForwardReferenceResolutionRuntime
115113
forward_reference_entity_refresher: ForwardReferenceEntityRefreshRuntime

tests/indexing/test_project_index_maintenance.py

Lines changed: 84 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,30 +31,63 @@
3131
)
3232

3333

34-
@dataclass(slots=True)
35-
class RecordingMoveBatchStore:
36-
results: list[ProjectIndexMoveBatchResult]
37-
batches: list[ProjectIndexMoveBatch] = field(default_factory=list)
38-
39-
async def apply_project_index_move_batch(
40-
self,
34+
def _stub_move_store(
35+
monkeypatch: pytest.MonkeyPatch,
36+
*,
37+
results: list[ProjectIndexMoveBatchResult],
38+
recorded: list[ProjectIndexMoveBatch],
39+
) -> RepositoryProjectIndexMaintenanceStore:
40+
"""Build a real store whose move-batch apply returns canned results.
41+
42+
The orchestration under test only cares that each batch produces a result and
43+
is recorded in order, so we monkeypatch the single SQL method rather than drive
44+
a real database — the store is otherwise the same concrete used in production.
45+
"""
46+
result_iter = iter(results)
47+
48+
async def fake_apply_move_batch(
49+
_self: RepositoryProjectIndexMaintenanceStore,
4150
move_batch: ProjectIndexMoveBatch,
4251
) -> ProjectIndexMoveBatchResult:
43-
self.batches.append(move_batch)
44-
return self.results.pop(0)
52+
recorded.append(move_batch)
53+
return next(result_iter)
4554

55+
monkeypatch.setattr(
56+
RepositoryProjectIndexMaintenanceStore,
57+
"apply_project_index_move_batch",
58+
fake_apply_move_batch,
59+
)
60+
return RepositoryProjectIndexMaintenanceStore(
61+
session_maker=cast(async_sessionmaker[AsyncSession], object()),
62+
project_id=1,
63+
)
4664

47-
@dataclass(slots=True)
48-
class RecordingDeleteBatchStore:
49-
results: list[ProjectIndexDeleteBatchResult]
50-
batches: list[ProjectIndexDeleteBatch] = field(default_factory=list)
5165

52-
async def apply_project_index_delete_batch(
53-
self,
66+
def _stub_delete_store(
67+
monkeypatch: pytest.MonkeyPatch,
68+
*,
69+
results: list[ProjectIndexDeleteBatchResult],
70+
recorded: list[ProjectIndexDeleteBatch],
71+
) -> RepositoryProjectIndexMaintenanceStore:
72+
"""Build a real store whose delete-batch apply returns canned results."""
73+
result_iter = iter(results)
74+
75+
async def fake_apply_delete_batch(
76+
_self: RepositoryProjectIndexMaintenanceStore,
5477
delete_batch: ProjectIndexDeleteBatch,
5578
) -> ProjectIndexDeleteBatchResult:
56-
self.batches.append(delete_batch)
57-
return self.results.pop(0)
79+
recorded.append(delete_batch)
80+
return next(result_iter)
81+
82+
monkeypatch.setattr(
83+
RepositoryProjectIndexMaintenanceStore,
84+
"apply_project_index_delete_batch",
85+
fake_apply_delete_batch,
86+
)
87+
return RepositoryProjectIndexMaintenanceStore(
88+
session_maker=cast(async_sessionmaker[AsyncSession], object()),
89+
project_id=1,
90+
)
5891

5992

6093
class FakeProjectIndexScalarResult:
@@ -232,8 +265,12 @@ def test_project_index_maintenance_batch_plans_require_positive_batch_size() ->
232265

233266

234267
@pytest.mark.asyncio
235-
async def test_project_index_move_runner_applies_batches_and_reports_progress() -> None:
236-
store = RecordingMoveBatchStore(
268+
async def test_project_index_move_runner_applies_batches_and_reports_progress(
269+
monkeypatch: pytest.MonkeyPatch,
270+
) -> None:
271+
recorded_batches: list[ProjectIndexMoveBatch] = []
272+
store = _stub_move_store(
273+
monkeypatch,
237274
results=[
238275
ProjectIndexMoveBatchResult(
239276
updated_files=1,
@@ -246,7 +283,8 @@ async def test_project_index_move_runner_applies_batches_and_reports_progress()
246283
updated_files=1,
247284
moved_entity_ids=frozenset({11}),
248285
),
249-
]
286+
],
287+
recorded=recorded_batches,
250288
)
251289

252290
run = await run_project_index_move_batches(
@@ -259,7 +297,7 @@ async def test_project_index_move_runner_applies_batches_and_reports_progress()
259297
move_store=store,
260298
)
261299

262-
assert store.batches == [
300+
assert recorded_batches == [
263301
ProjectIndexMoveBatch(
264302
completed_batches=1,
265303
targets=(
@@ -284,8 +322,12 @@ async def test_project_index_move_runner_applies_batches_and_reports_progress()
284322

285323

286324
@pytest.mark.asyncio
287-
async def test_project_index_delete_runner_applies_batches_and_reports_progress() -> None:
288-
store = RecordingDeleteBatchStore(
325+
async def test_project_index_delete_runner_applies_batches_and_reports_progress(
326+
monkeypatch: pytest.MonkeyPatch,
327+
) -> None:
328+
recorded_batches: list[ProjectIndexDeleteBatch] = []
329+
store = _stub_delete_store(
330+
monkeypatch,
289331
results=[
290332
ProjectIndexDeleteBatchResult(
291333
deleted_entities=1,
@@ -296,7 +338,8 @@ async def test_project_index_delete_runner_applies_batches_and_reports_progress(
296338
deleted_entities=0,
297339
missing_paths=("notes/c.md",),
298340
),
299-
]
341+
],
342+
recorded=recorded_batches,
300343
)
301344

302345
run = await run_project_index_delete_batches(
@@ -305,7 +348,7 @@ async def test_project_index_delete_runner_applies_batches_and_reports_progress(
305348
delete_store=store,
306349
)
307350

308-
assert store.batches == [
351+
assert recorded_batches == [
309352
ProjectIndexDeleteBatch(
310353
completed_batches=1,
311354
paths=("notes/a.md", "notes/b.md"),
@@ -326,15 +369,25 @@ async def test_project_index_delete_runner_applies_batches_and_reports_progress(
326369

327370

328371
@pytest.mark.asyncio
329-
async def test_store_project_index_maintenance_runner_delegates_to_batch_stores() -> None:
330-
move_store = RecordingMoveBatchStore(results=[ProjectIndexMoveBatchResult(updated_files=1)])
331-
delete_store = RecordingDeleteBatchStore(
372+
async def test_store_project_index_maintenance_runner_delegates_to_batch_stores(
373+
monkeypatch: pytest.MonkeyPatch,
374+
) -> None:
375+
recorded_move_batches: list[ProjectIndexMoveBatch] = []
376+
recorded_delete_batches: list[ProjectIndexDeleteBatch] = []
377+
move_store = _stub_move_store(
378+
monkeypatch,
379+
results=[ProjectIndexMoveBatchResult(updated_files=1)],
380+
recorded=recorded_move_batches,
381+
)
382+
delete_store = _stub_delete_store(
383+
monkeypatch,
332384
results=[
333385
ProjectIndexDeleteBatchResult(
334386
deleted_entities=1,
335387
relation_cleanup_entity_ids=frozenset({99}),
336388
)
337-
]
389+
],
390+
recorded=recorded_delete_batches,
338391
)
339392
runner = StoreProjectIndexMaintenanceRunner(
340393
move_store=move_store,
@@ -351,15 +404,15 @@ async def test_store_project_index_maintenance_runner_delegates_to_batch_stores(
351404
)
352405

353406
assert move_run.total_updated_files == 1
354-
assert move_store.batches == [
407+
assert recorded_move_batches == [
355408
ProjectIndexMoveBatch(
356409
completed_batches=1,
357410
targets=(ProjectIndexMoveTarget("notes/a.md", "archive/a.md"),),
358411
)
359412
]
360413
assert delete_run.total_deleted_entities == 1
361414
assert delete_run.relation_cleanup_entity_ids == frozenset({99})
362-
assert delete_store.batches == [
415+
assert recorded_delete_batches == [
363416
ProjectIndexDeleteBatch(
364417
completed_batches=1,
365418
paths=("notes/deleted.md",),

0 commit comments

Comments
 (0)