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's create_semantic_graph factory, 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

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:

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.py D13 bans import neo4j and .session( outside the documented allowlist (_GRAPH_SEAM_ALLOWED); gate green at 0 violations over 653 files (session 225, 2026-07-11). The Neo4jSyncer._driver compat 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