Orchestrata Ultimate › Plans › neo4j-seam-design
Operational-graph repository seam (backlog #163)
Source: plans/neo4j-seam-design.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-08 · 118 lines.
Prose pending.
Plan text
Operational-graph repository seam (backlog #163)
Route all direct operational-Neo4j access through a repository/adapter seam mirroring
semantic_graph'screate_semantic_graphfactory, so the operational store becomes swappable/droppable. Driver construction is already centralized (core/neo4j_factory, D12); this adds the usage seam. Evidence base: full driver-usage catalog of all 23+ touching files (session 224 Explore audit, 2026-07-08).
Goals / non-goals
- Goal: no subsystem file imports
neo4jor callscreate_neo4j/create_async_neo4jdirectly; they depend on aGraphRepositoryprotocol obtained from one factory. - Goal: ownership semantics (shared vs owned) made explicit — today
ad-hoc
_owns_driverflags are re-implemented in 4 files. - Non-goal: touching
semantic_graph/*(Memgraph, already behind its own seam) or changing any Cypher. - Non-goal: dropping Neo4j. This seam is the prerequisite.
Package layout — core/graph/ (new)
Mirrors semantic_graph/'s shape. Lives in core because every subsystem
already imports core, and the raw-driver factory (core/neo4j_factory) is
already there — no new layering edges, no cycles.
core/graph/
__init__.py # re-export: protocols, config, factories, NO_AUTH
interfaces.py # SyncGraphRepository / AsyncGraphRepository / UnitOfWork protocols
config.py # GraphRepositoryConfig.from_env() (NEO4J_URI/USERNAME/PASSWORD/DATABASE)
repository.py # create_graph_repository() / create_async_graph_repository()
adapters/
__init__.py
neo4j_sync.py # Neo4jSyncRepository
neo4j_async.py # Neo4jAsyncRepository
Protocol surface (derived from the usage catalog — covers every real call)
class AsyncGraphRepository(Protocol):
async def execute_read(self, cypher: str, params: Mapping[str, Any] | None = None,
*, database: str | None = None) -> list[dict[str, Any]]: ...
async def execute_write(self, cypher: str, params: Mapping[str, Any] | None = None,
*, database: str | None = None) -> list[dict[str, Any]]: ...
async def verify_connectivity(self) -> None: ...
async def close(self) -> None: ... # no-op when repository is shared
class SyncGraphRepository(Protocol):
def execute_read(self, cypher: str, params: Mapping[str, Any] | None = None,
*, database: str | None = None) -> list[dict[str, Any]]: ...
def execute_write(self, cypher: str, params: Mapping[str, Any] | None = None,
*, database: str | None = None) -> list[dict[str, Any]]: ...
def unit_of_work(self, *, database: str | None = None) -> AbstractContextManager[UnitOfWork]: ...
def verify_connectivity(self) -> None: ...
def close(self) -> None: ...
class UnitOfWork(Protocol): # one transaction, N statements
def run(self, cypher: str, params: Mapping[str, Any] | None = None) -> list[dict[str, Any]]: ...
Decisions locked by real usage:
database=optional — onlymemory/cold_store,knowledge_graph/neo4j_client,session/kg_context,session/kg_sync/_basepass it; everyone else omits.- Params: one
paramsmapping (adapters normalize; callers today mix positional-dict and**kwargs). - Return
list[dict]— coversasync for,.data(), comprehension styles. execute_writeuses managed transactions (session.execute_write) in the adapter → free driver-level transient retry;execute_readusessession.execute_read. This upgrades the plain-session.runcallers to retry-safe semantics without their code changing.unit_of_work(sync) replaces the multi-statement-per-transaction patterns:cxp_auditor(node+rel),pipeline_kg(node+link),neo4j_storetxn functions,kg_contextsession-passing helpers.- Ownership: adapters take
driver=None(create + own) or an injected driver/shared=True(never close). One implementation, not 4 copies of_owns_driver. - Escape hatch: adapters expose
.driver(documented raw handle) for the exotic files below; the protocol does NOT include it — callers needing it type against the concrete adapter, making escapes greppable.
Migration order — one commit per phase, suite + Neo4j up for each
Status: COMPLETE (all phases landed + pushed to origin/master). phase 0 = 17e34088 (+ driver_kwargs edf93ab6), phase 1 = 460b6237, phase 2 = 7db35929, phase 3 = 64aa67eb, phase 4 = bed9e2f8, phase 5/5b = 62bc2ad1, phase 6 (D13 gate) = 219b83c3. The seam is enforced:
scripts/architecture_checker.pyD13 bansimport neo4jand.session(outside the documented allowlist (_GRAPH_SEAM_ALLOWED); gate green at 0 violations over 653 files (session 225, 2026-07-11). TheNeo4jSyncer._drivercompat property flagged in phase 5b was removed.
| Phase | Subsystem / files | Notes |
|---|---|---|
| 0 | core/graph/ package + unit tests (real Neo4j) |
protocols, config, adapters, factory; ownership + uow + retry tests |
| 1 | gateway: server.py builds ONE async repo; rpc.set_neo4j_driver→set_graph_repository; handlers knowledge/projects/session/governance/test_metrics swap _execute_cypher internals for repo.execute_read/write |
Uniform shape; biggest fan-out, mechanical |
| 2 | async internals: gateway/audit.py, kernel/audit/writers.py, kernel/session_manager.py, memory/cold_store.py, orchestrator/neo4j_audit.py, orchestrator/fact_store.py + fact_neo4j.py (helpers take repo, not driver), postman/health.py (verify-only) |
Each file is a self-contained swap |
| 3 | postman cleanup: remove vestigial never-used Neo4j handles in receipt_store.py, replay_engine.py, message_store.py (# noqa import); postman.py pass-through param becomes a repo once CXPAuditor (phase 4) accepts one |
Dead-code deletion + threading a param |
| 4 | sync simple: harness/history.py, knowledge_graph/cxp_auditor.py (uow), knowledge_graph/kg_design.py, knowledge_graph/sync.py, session/kg_context.py, session/neo4j_store.py (uow) |
cxp_auditor/kg_design keep injectable-or-internal via factory |
| 5 | exotic: knowledge_graph/kg_context.py (uow refactor of session-passing helpers), kernel/pipeline_kg.py (stop reaching into ContextInjector._driver; own repo + escape hatch for FileSynchronizer(driver=...)), knowledge_graph/synchronizer.py (dual acquisition → repo + escape hatch), session/kg_sync/_base.py (KEEP raw driver via escape hatch — bespoke deadlock-retry + chunk-splitting needs begin_transaction) |
Judgment per file; escape hatches documented in-code |
| 5b | attribute-coupled files the phase-0 inventory MISSED (they use an injected driver/self._driver and never import neo4j or call the factory, so the import-site scan was blind to them; found by a .session( sweep after QueryMixin surfaced in phase 4): knowledge_graph/_sync_queries.py (QueryMixin — currently bridged by Neo4jSyncer._driver compat property), knowledge_graph/context_injector.py (the live SPEC-106 injector — the very _driver pipeline_kg reaches into), knowledge_graph/context_pack/{frame,selector}.py, integrations/fireflies_sync.py, knowledge_graph/canonical_schema.py |
Migrate these WITH phase 5 (context_injector + pipeline_kg are one coupled unit); after migration, delete the _driver compat property on Neo4jSyncer. Phase 6's gate must ALSO ban .session( outside the seam, not just import neo4j — imports alone provably miss attribute-coupled usage |
| 6 | gate: grep/AST check that import neo4j + create_(async_)neo4j appear only in core/neo4j_factory.py, core/graph/adapters/*, and the documented escape-hatch files; wire into scripts/architecture_checker.py |
Prevents regression |
knowledge_graph/neo4j_client.py (async, has its own retry loop) stays as-is
initially — it is itself nearly a repository; phase 2+ may either retire it
into Neo4jAsyncRepository or have it implement the protocol. Decide at
phase 4 with the evidence from phases 1–3.
Test strategy
- New seam: real-Neo4j tests (no mocks per doctrine) — CRUD via both repos,
database=routing, uow commit/rollback, ownership (shared repoclose()is a no-op; owned closes), transient-retry behavior. - Migrations: existing per-subsystem suites are the safety net (run per phase); add a regression test only where behavior changes (e.g. plain-run → managed-txn upgrade).