Orchestrata Ultimate › Plans › neo4j-to-orchgraph-migration-plan
Migration Plan — Operational KG: Neo4j → orchgraph
Source: plans/neo4j-to-orchgraph-migration-plan.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-26 · 569 lines.
Prose pending.
Plan text
Migration Plan — Operational KG: Neo4j → orchgraph
Goal: move the operational knowledge graph off Neo4j 5.26.28 Community (
neo4j-db10.92.53.11) onto the orchgraph Memgraph fork, then re-point every tool, service, and test at it. At the end of this plan everything is wired and working: full authoritative suite green against orchgraph, all live consumers (gateway, kernel, KG gRPC server, timers, session sync) writing to it, Neo4j stopped. Grounded in the 2026-07-26 graph-engine analysis (wikiprojects/orchestrata-ultimate/graph-engine-analysis).Conventions: every step has a deterministic verification; no step is done until it passes AND its wiring site is named. Dry-run-by-default for every mutating tool. All connection material from
.env, read at call time.
Live baseline (measured 2026-07-26, post-cleanup)
| value | |
|---|---|
| Nodes / edges | 80,790 / 133,781 (fits orchgraph in-memory: ~0.1 GiB/copy) |
| Labels | 83 |
| Uniqueness constraints | 76 |
| Indexes | 121 (incl. Lucene FTS search_names — 1 call site, knowledge_graph/search.py, CONTAINS fallback already written) |
| APOC | not installed; zero call sites (last one fixed in 0b248bee) |
| GDS | installed, zero call sites — nothing to port |
| Driver coupling | 32 files import the neo4j driver (src+scripts); 35 src + 28 script files read NEO4J_* env; 1 hardcoded IP in src |
| Repository seam | core/graph/repository.py has a backend registry ("additional backends can slot in without changing callers") |
| Existing migration base | scripts/import_neo4j_to_memgraph.py (ontology-mapped, semantic-graph-targeted — NOT full-fidelity; Phase 3 builds the real one) |
Key risk classes: (1) label collision with the shared cluster, (2) Cypher dialect drift, (3) MVCC write-conflict aborts vs Neo4j's pessimistic locks, (4) test-fleet swap, (5) silent consumers still writing to Neo4j after cutover.
Phase 0 — Decisions + mechanical inventory
Step 0.1 — RE-RATIFIED 2026-07-26 (supersedes the dedicated-cluster recommendation):
multi-tenant database orchkg on the EXISTING cluster
- User ruling ("doesn't the graph support namespaces?"): Memgraph
multi-tenancy PROVEN live on the shared cluster 2026-07-26 —
CREATE DATABASE, per-database isolated storage (default db sees 0 of the
probe's nodes), driver
database=session param works, second database REPLICATES to the HA replica, DROP clean. That kills the label-collision argument and (per-database storage) most of the contention argument. - The operational KG becomes database
orchkgon the existing cluster (routingneo4j://10.92.53.160:7691,database="orchkg"on every session).KG_DATABASE=orchkgjoins the env seam. Accepted coupling (user's call): process/upgrade/failover shared with the semantic graph. - The 5
orchgraph-kg-*containers built under the superseded decision were terraform-destroyed the same day (zero residue). - Phase 1.1 (provision cluster) reduces to:
CREATE DATABASE orchkg+ Step 1.2 DDL applied INTO it (kg_schema.py apply --dialect memgraphwith the database param) + Step 1.3 backup covering all databases.
(superseded text follows for the record)
Step 0.1 — DECISION: dedicated cluster, not the shared one (recommendation)
The existing orchgraph cluster is the SHARED semantic/RAG graph; the wiki
indexer writes Project, Session, Decision, Tag, Page there — the
operational KG uses Project, Session, Decision too. Co-mingling the
operational KG into it collides label-for-label and violates the two-graph
doctrine (LEVEL0 #1 confusion source).
- Recommended: provision a dedicated
orchgraph-kgHA cluster (3 coordinators + 2 data instances, same recipe as the semantic cluster). Cost ≈ 1.7 GiB RSS for HA at current graph size; the KG gains the HA that Neo4j Community structurally cannot provide (the migration's whole point). - Rejected alternative: multi-database on the shared cluster (fork has MG_ENTERPRISE=ON) — couples the operational KG's failure/upgrade domain to the wiki's and complicates every routing URI. Revisit only if RAM pressure appears (>10 M nodes).
- Verification: decision recorded via
session choice+ this file updated with the ratified choice before Phase 1 starts.
Step 0.2 — RATIFIED 2026-07-26: NO auth; KG_* env seam stays
- User ruling: no Bolt auth on the new cluster — same auth=None posture as the live semantic cluster. Lab network, user's explicit call.
- Env seam unchanged: canonical
KG_URI/KG_ROUTING_URIin.env(password keys unused/empty), read at call time.NEO4J_*stays as a read-through alias during migration (one resolver, NOT 63 file edits), mechanical rename wave AFTER cutover viaserialize_codebase_ast.py rename(dry-run first). - Verification:
python scripts/validate_env.pypasses with the new keys; one grep proves the alias resolver is the only place reading both.
Step 0.3 — Mechanical coupling inventory (no hand-reading)
- AST toolkit sweep:
who-imports neo4j,refs GraphDatabase,refs Neo4jSyncRepository/Neo4jAsyncRepository/Neo4jClient, plus a Cypher-string extractor (AST: string literals passed torun/execute_read/execute_write/execute_query). Output:plans/kg-migration-inventory.json— every call site, its query text, its owning subsystem, and whether it goes through the repository seam or holds a raw driver. - Raw-driver holders get flagged: they must either adopt the seam (preferred) or be individually listed as direct-port sites.
- Verification: inventory file exists; site count reconciles with the grep
baseline above (32 driver imports); every site classified seam/raw — zero
unclassified.
Step 0.4 — Cypher dialect gate (build EARLY, run continuously)
scripts/kg_dialect_preflight.py(pattern:neo4j_test_preflight.py): takes the inventory's extracted queries andEXPLAINs each against a live orchgraph instance; reports per-query PASS/FAIL with the server error. Parameterized queries get dummy params by type.- Known dialect deltas to expect (from the fork docs + 3.11): Neo4j-5
CREATE CONSTRAINT … REQUIRE→ MemgraphCREATE CONSTRAINT ON … ASSERT;SHOW CONSTRAINTS/INDEXESshapes differ;elementId()absent; no APOC (already clean); string slicing[..n]absent (usesubstring()); temporal function surface differs. - Wiring: added to
run_tests.pygates ANDhealth_check.pyonce Phase 2 starts (it needs the target cluster up to run — mark SKIP-with-warning whenKG_URIis unset). - Verification: gate runs against the full inventory; the initial failure list IS the Phase 2 work queue (written into this plan as an appendix).
Phase 1 — Provision the target (Terraform, not hands)
Step 1.1 — orchgraph-kg cluster via Terraform
- Extend the existing Terraform fleet config (13 containers, zero-diff
imported) with:
orchgraph-kg-coord-{1,2,3}+orchgraph-kg-data-{1,2}, static IPs in the 10.92.53.x range, the packaged release tarball (which now includes MAGE — orchgraphfdf6dd8),--query-modules-directoryset, WAL on + snapshot interval 600s + retention 2 (same as semantic cluster),User=memgraphunit pattern (root-refusal lesson), auth enabled per 0.2. - Register coordinators, set up Raft, verify failover once (kill MAIN, watch re-promotion) — the semantic cluster runbook applies verbatim.
- Verification:
terraform planclean after apply (zero diff);SHOW INSTANCESvia new routing URI shows 5 healthy instances with one MAIN; kill-MAIN failover test passes;CALL mg.procedures()lists MAGE.
Step 1.2 — DDL from canonical schema (ontology before import — constitutional)
kg_schema.pygrows a--dialect memgraphemitter: the SAMEcanonical_schema.pysource generates Memgraph-syntax constraints/indexes. Never hand-write DDL against the new cluster.- FTS: create the
text_search(tantivy) index equivalent forsearch_namesat bootstrap. - Verification:
kg_schema.py apply --dialect memgraph --uri <new>exits 0 and is idempotent (second apply = no-op); constraint count on the new cluster ≥ 76 canonical ones;test_canonical_schema*modules extended for the emitter and green.
Step 1.3 — Backup + retention plumbing
- Replace-in-place pattern: new
orchgraph-kg-backup.service/.timer(daily 03:00, same slot) — triggerCREATE SNAPSHOT;,incus file pullthe snapshot dir, prune to N=14. Reuses the bolt-port readiness guard added to neo4j-backup on 2026-07-26. orchestrata-kg-retention.timerkeeps working unchanged at cutover becausepurge_legacy_orphans.pygoes through the same env seam (verified in Phase 2).- Verification: manual
systemctl start orchgraph-kg-backup.serviceproduces a pulled snapshot; restore drill: load snapshot into a scratch container, node count matches source.
DONE 2026-07-26. Built as scripts/backup_orchgraph.sh +
~/.config/systemd/user/orchgraph-backup.{service,timer} (user units, matching
orchestrata-kg-retention; linger is on, daily 03:00, Persistent=true, the
bolt-port readiness guard on coordinator .160:7691). Named
orchgraph-backup, not orchgraph-kg-backup, because the Step 0.1 pivot put
the operational KG on the EXISTING cluster as the orchkg database — one
cluster, one backup unit, all databases.
- MAIN is resolved from
SHOW INSTANCESeach run (the role moves on failover; no hardcoded data-instance IP).CREATE SNAPSHOT;is issued per logical database — a multi-tenant snapshot covers only its own database. - Memgraph does not expose the database-name → storage-uuid mapping over Bolt,
so the pull step copies the newest snapshot from the storage root plus every
databases/<uuid>/snapshotsdir, and writes amanifest.json({database: node_count}+ MAIN address) so a restore knows what each uuid-named directory holds. Prunes to the newest N=14 runs. - Verified live:
systemctl --user start orchgraph-backup.service→ exit 0, pulledmemgraph(494M, 765,507 nodes) +orchkg(8K, schema-only). Restore drill executed: scratch container from the snapshot file alone → Memgraph came upactivewith 71 constraints + 76 indexes, matching the sourceorchkgexactly; scratch container deleted. (The node-count leg of the drill is schema-only until Phase 3 loads data — re-run the drill against the populatedorchkgas part of Step 4's cutover verification.) - Two
mvcc_probe_test*scratch databases left by the Phase 2 port were dropped; the backup manifest is the standing check for stragglers.
Phase 2 — Port the access layer (behind the seam, testable before any data moves)
Step 2.1 — Memgraph backend in the repository factory
core/graph/repository.py: registermemgraphsync+async backends. Theneo4jPython driver speaks Bolt to Memgraph (proven daily by the semantic graph) — the backend's job is the DELTAS, not a new driver:- MVCC conflict retry: decorator translating Memgraph serialization
aborts (
Cannot resolve conflicting transactions, transientMemgraph.TransientError) into bounded exponential retries. Neo4j blocks on locks; Memgraph aborts — without this, concurrent writers that work today fail tomorrow. ExtendNeo4jClient._is_transient_errorlikewise. - Routing:
neo4j://<coordinator>:7691write-routing to dynamic MAIN (semantic-graph pattern), plus direct-data-instance mode for tests. - Dialect shims for the failure list from Step 0.4 (each fixed at its call site — portable Cypher preferred over shim magic).
- MVCC conflict retry: decorator translating Memgraph serialization
aborts (
- Verification: the whole
tests/core/graph/module family parameterized to run against BOTH backends passes; a purpose-built concurrent-writer test (two threads MERGEing the same node 100×) passes on Memgraph via the retry wrapper and would fail without it (prove both).
Step 2.2 — Port the raw-driver holders
- Every
rawsite from the 0.3 inventory: adopt the factory seam, or apply the retry/dialect treatment locally where the seam genuinely doesn't fit (scripts with standalone--uriargs). Includes:knowledge_graph/(Neo4jClient, sync, query engines, context providers),session/(neo4j_store, kg_sync),memory/cold_store,orchestrator/(fact_neo4j, neo4j_audit),gateway/audit,kernel/pipeline_kg, KG gRPC server, and the ops scripts (neo4j_query.py,graph_browser.py,sweep_test_pollution.py,purge_legacy_orphans.py,backfill_thread_links.py,bootstrap_graphs.py). - FTS call site (
knowledge_graph/search.py): port totext_search.searchwith the CONTAINS fallback retained. - Verification: Step 0.4 dialect gate exits 0 over the FULL inventory;
check-deps+standardsclean; per-module test fan-out green for every touched module.
Step 2.3 — Test fleet swap (workers)
- Terraform: 3 single-node orchgraph worker containers replacing
neo4j-node-2/3/4roles (tmpfs data dirs, same IP slots or new ones — keep.envthe only truth). tests/conftest.pyper-worker mapping,start_test_services.py(readiness = Bolt +RETURN 1, kernel stays REQUIRED — untouched),sweep_test_pollution.pymulti-DB loop,neo4j_test_preflight.pysuccessor = the 0.4 dialect gate.- Verification:
python scripts/run_tests.pyFULL authoritative run (kernel started + gRPC-verified) green with workers on orchgraph while production still points at Neo4j — the suite proves the access layer before any production data moves.
Phase 3 — Data migration (full fidelity, verified, reversible)
Step 3.1 — scripts/migrate_kg_to_orchgraph.py
- NOT the semantic import (
import_neo4j_to_memgraph.pymaps through the ontology and drops operational churn — wrong tool for becoming the primary store). This one is full-fidelity: every label, every relationship type, every property, batched (500), MERGE-only on canonical ids, dry-run by default /--apply, resumable (per-label progress file). - Property-type fidelity audit: temporal values, byte arrays, and lists round-trip or are explicitly converted with a logged rule (no silent coercion).
- Verification: dry run prints per-label/per-reltype source counts;
--applyto the new cluster then a checksum pass: per-label node counts, per-type edge counts, and a per-label property-hash sample (N=1000/label) match Neo4j exactly; second--applycreates nothing (idempotency — proven by graph recount, Thread-backfill pattern).
Step 3.2 — Query-parity harness
scripts/kg_query_parity.py: run the ~30 highest-value read queries (from the inventory: context injection, session status, RTM evidence, thread lookups, graph_browser canned queries) against BOTH graphs; diff normalized results.- Verification: 100% parity or every diff explained in writing (e.g., ordering without ORDER BY) and fixed with explicit ORDER BY where it matters.
Step 3.3 — AST subgraph: re-derive, don't copy (ratified 2026-07-26)
- The code-intelligence subgraph (CodeFunction/CodeClass/Method + DEFINES/ CONTAINS/call edges, ~11K+ nodes) is DERIVED data — bootstrap doctrine says derive from source, never hand-copy. Phase 3's migration tool copies only the record-of-fact subgraph (sessions, threads, packets, decisions, specs, harness history); the AST subgraph is produced by running the (Phase-2- ported) AST sync against the new cluster.
- Verification: re-derived per-label counts match the Neo4j source graph — doubling as a full-fidelity proof of the ported sync layer with real data, before cutover.
Exclusion set RATIFIED 2026-07-26 (orchestrator sign-off). The tool
excludes 11 derived labels, not just the three this step names literally:
Package, Module, Class, Function, Method, Parameter, File, Directory,
Predicate, Rule, Test (live: 30,342 nodes). Predicate/Rule are in the set
because they are parsed out of the Prolog sources by code_parser.py — the
same "derived from source" property that put CodeFunction/CodeClass there;
copying them would violate the doctrine for exactly the same reason.
- Consequence to close at cutover: 16 live edges reference
Rule((:Component)-[:MUST_FOLLOW]->(:Rule)and(:Architecture)-[:HAS_RULE]->(:Rule)). They are NOT migrated and MUST be re-derived along with their endpoints by the Prolog-source parse — Step 3.3's count check coversRulenodes; extend it to those two edge types so a missing re-derivation cannot pass silently. - Dry run against the live graph: would migrate 37,469 nodes / 68,909 relationships, excluding the 11 derived labels above.
Step 3.3 — EXECUTED 2026-07-26
Both graphs were re-derived from the same input (plans/ast-ultimate.json:
783 modules / 1494 classes / 1655 functions / 5573 methods) and compared with
scripts/kg_query_parity.py --include-derived.
Result: the derived subgraph matches exactly — 6/6 labels, 6/6 edge types.
Class 1494 · Function 1655 · Method 5561 · Module 783 · Package 783 · Parameter 10672; CALLS 4573 · CONTAINS 1566 · DEFINES 14271 · HAS_PARAMETER 10672 · IMPORTS 1938 · INHERITS 253. The ported sync layer is
full-fidelity against real data.
Reaching that equality required deleting garbage from Neo4j, not adding data to orchgraph — orchgraph was right first:
- 41 stranded derived nodes (
_test_satn_*,kg_*, pytest tmpdirs) sat in Neo4j as hub-unreachable islands, inflating it to 787 Modules / 1498 Classes / 805 Packages. A subtree is only reachable from the hub once itsProject->Packageedge exists, so a run that died between the node write and the edge write left an island that project-scoped cleanup could not see. Deleted per the root-connectivity invariant (garbage is deleted, never linked); the leak backstop is now intests/scripts/test_sync_ast_to_neo4j.py::_detach_delete_project. - The
Rulepremise above is superseded and is NOT actioned. Ontology ruling 3 (plans/architecture-ontology-draft.md§7) retired theRulelabel in favour ofConstraint{kind:"invariant"};sync_architecture_to_neo4j.pywrites only the canonical shape and explicitly does not migrate the old one. Live Neo4j carried BOTH — 6 Constraints AND 6 Rules, 10MUST_FOLLOW->ConstraintAND 10MUST_FOLLOW->Rule— the retired shape as duplicate residue, not as data of record. orchgraph already held the canonical shape at identical counts (Constraint 6 · MUST_FOLLOW 10 · CONSTRAINS 6 · Pattern 9 · APPLIES 6). Re-derivingRulewould have imported retired residue into the graph that replaces Neo4j. Same forUSES_PATTERN(6 parallel un-MERGEd edges between one node pair, fully covered byAPPLIES) — deleted from both.
The harness now asserts the canonical shape for parity
(component_must_follow_constraint, constraint_constrains_architecture,
component_applies_pattern) and the retired shape as an ABSENCE on the
target (RETIRED_SHAPE / retired_shape_violations, which fails the run on
any residue). Parity on the retired shape would have meant copying it.
Remaining diffs are record-of-fact live-write growth on Neo4j since the delta (Session 752 vs 748, GatewayAuditEvent 1668 vs 1656 — source-greater in every case, never loss). Phase 4's freeze + final delta closes them.
Step 3.4 — Live code intelligence (session-254 requirement: sub-ms
review queries, seconds-fresh sync)
- Ratified SLOs (measured, not asserted):
SLO Target Indexed symbol lookup (canonical id) p99 < 1 ms 2-hop neighborhood (callers/callees/inheritance) p99 < 1 ms Full code-node predicate scan < 50 ms File-save → graph updated (end-to-end) < 5 s Whole-graph MAGE analytical pass < 1 s Honest physics: sub-ms is PER indexed lookup / k-hop on the in-memory engine; a query touching every node is tens of ms; whole-codebase analytics are the MAGE sub-second class. No SLO claims sub-ms full scans. - Live sync pipeline (wiring existing components, not greenfield):
knowledge_graph/file_watcher.py(debounced, built+tested) watchessrc/+tests/→ warmast_daemonre-parses only the changed module (~100–300 ms, tree already in memory) →Neo4jSyncer.incremental_sync(built+tested, fails loudly on unknown modules) pushes the delta over Bolt. Deployed asorchestrata-ast-live-sync.service(user systemd unit, enabled — always on, not a remembered command). - Indexes: canonical-id + name indexes on all code labels emitted by the
Step 1.2
--dialect memgraphDDL; the SLO harness fails if a lookup plan shows a scan where an index applies. - Verification:
scripts/benchmark_code_intel.pymeasures every SLO against the live cluster and exits non-zero on any miss (wired into the Phase 4.3 suite evidence); watcher integration test: touch a real module, poll the graph, assert the delta lands < 5 s; service enabled + surviving reboot (systemctl --user is-enabled).
Step 3.4 — EXECUTED 2026-07-26
All five SLOs met, measured against the live cluster:
| SLO | Target | Measured |
|---|---|---|
| Indexed symbol lookup (canonical id) | p99 < 1 ms | 0.720–0.790 ms |
| 2-hop neighborhood | p99 < 1 ms | 0.709–0.728 ms |
| Full code-node predicate scan | < 50 ms | 8.222 ms |
| File-save → graph updated | < 5 s | 1.31–1.68 s |
| Whole-graph MAGE pass | < 1 s | 69.8 ms (pagerank) / 122.9 ms (wcc) |
Built: scripts/benchmark_code_intel.py (9 SLOs, 24 tests),
scripts/ast_live_sync.py (22 tests), ~/.config/systemd/user/ orchestrata-ast-live-sync.service (enabled + active, WantedBy= default.target, bolt-port readiness guard matching orchgraph-backup).
Live-proven end to end: saved a real module → the service synced it →
the new function was queryable in orchkg within 12 s of the write.
Every one of these was found by RUNNING the thing, not by reading it:
Method.idhad no index — Method is out ofcanonical_schema.py's canon, so it gets no CONSTRAINT and therefore none of the implicit indexes every other code label receives. A lookup by canonical id plannedScanAllover 5,561 nodes. Addedmethod_id+method_nametoschema.py. This is why every latency SLO also EXPLAINs its query and fails on a scan: a latency number alone cannot tell "fast because indexed" from "fast because the graph is small today".- A p99 over 200 samples is noise.
indexed_symbol_lookup_method"missed" at 3.135 ms p99 with a 13.884 ms single-sample max while its p50 (0.348 ms) matched Class and Function exactly. At n=1000 it measured 0.720 ms. Default raised to 1000 — reporting noise as a regression is the failure mode a benchmark exists to prevent. - Parsing
src/instead ofsrc/orchestrata_ultimateinserts a second codebase. Module ids are dotted paths RELATIVE to the analyzer root, so the wrong root renames every module andincremental_syncadds rather than updates: one pass took orchkg from 783 Modules to 1621. Repaired by a full re-derive.load_or_analyzetreats a root mismatch as a cache miss and silently full-reparses — correct for a CLI, catastrophic for a service — soassert_cache_root_matches()now refuses to run instead. touch()measures nothing. Freshness is decided by CONTENT hash, so the first--measurereported PASS at 0.552 s having written 0 nodes. It now writes a real uuid-named probe module, measures, deletes it, and FAILS if nothing reparsed.- Deletions were reported but never applied. A deleted file has no AST
entry left, so
incremental_synccannot express the operation at all (it raises on module paths the AST does not contain) — the sync logged "1 removed" while the module's whole subtree stayed in the graph forever. ExtractedNeo4jSyncer.delete_modules()fromincremental_syncand wired it in; regression test asserts the Module AND its Functions leave the graph. --uri ""silently retargeted the graph.--uri "$KG_ROUTING_URI"with that key still blank in.envexpands to an empty string, whichsync_ast_to_neo4j.pyturned into "use the default graph" — aiming orchgraph's DDL at the live Neo4j. Caught only because--no-authmade the wrong target reject the connection. An empty explicit--uriis now an error. Both new tools resolveKG_ROUTING_URIand fall back toMEMGRAPH_URI, so they follow the Phase 4.2 cutover without an edit.
Phase 4 — Cutover (freeze → delta → flip → verify live)
Step 4.1 — Freeze + final delta
- Announce freeze; stop the write consumers in order: gateway, kernel,
runtime kernel, timers (retention, turn-capture flushes). Re-run
migrate_kg_to_orchgraph.py --apply(MERGE = the delta is whatever changed since 3.1). Re-run the checksum pass. - Verification: checksum parity at freeze point; consumer stop list
checked off with
systemctl/process evidence.
Steps 4.1 + 4.2 — EXECUTED 2026-07-26
Final delta. Three delta runs, each a full --apply (MERGE, so a delta is
whatever changed since the last):
| run | nodes | relationships | dropped edges |
|---|---|---|---|
| delta1 | — | — | — |
| delta2 | 42,243 | 71,892 | 16 (12 `GatewayAuditEvent |
| delta3 | 42,259 | 71,908 | 0 |
delta2's 16 drops were not data loss: a live writer created those Sessions between the node phase and the edge phase of that same run, so the edges had no endpoint yet. delta3 picked up exactly those 16 nodes and 16 edges and dropped nothing — which is the evidence that the delta procedure converges.
Checksum parity: PASS — 118 label/relationship counts, source == target on
every one, 0 mismatches (reports/migration-checksum-2026-07-26.txt).
Query parity: PASS — 31/31, no retired-shape violations
(reports/migration-query-parity-2026-07-26.txt).
Live smoke (each verified, not asserted):
- Live code intelligence: functions written during this very session
(
agents.runtime_main._flush_audit_trail,kernel.pipeline_session.PipelineSessionMixin.ashutdown) were already inorchkgwhen queried, correctly typedFunctionvsMethodand project-namespaced —orchestrata-ast-live-sync.serviceis working. scripts/neo4j_query.pyandscripts/graph_browser.pyanswer againstorchkg(Class 1494, CXPPacket 21853, Session 764, 43 labels).wiki-rag-queryunaffected — the semantic graph still answers (Chunk 2122, ADR 32), confirming the two databases stay separate.purge_legacy_orphans.pydry-runs clean againstorchkg(no dialect errors; 3016 CXPPacket orphans identified) — the gate for re-enablingorchestrata-kg-retention.timer.
Defects the smoke exposed — all fixed, none deferred:
- Wrong-database class. Both graphs now share one URI, so isolation is
the
database=argument. Every session omitting it read the cluster default (the semantic graph):graph_browserservedChunk/ADRcounts when asked for the operational KG,PromotionStorewrote promoted facts into the wrong database, the scheduler parked on watermarks there, and five sync/promotion test modules verified an EMPTY database and passed anyway.Neo4jSyncRepositoryandPromotionStorenow expose a publicsession();test-reviewT18 gates the shape. - ADR-0019 had become FAIL-OPEN. orchgraph emits no Bolt bookmarks and
silently ignores supplied ones, so
await_watermark's "open a bookmarked session, runRETURN 1" probe returned immediately for every token — including one no write ever minted. Replaced with the ADR's own second sanctioned mechanism, an explicit visibility watermark stamped inside the same transaction as the facts. See the ADR's "Mechanism change 2026-07-26". - Neo4j DDL against Memgraph.
Neo4jSyncer'sdialectdefaulted to the literal"neo4j", socreate_schema()raised onCREATE TEXT INDEX ... IF NOT EXISTShours after the cutover.resolve_kg_dialect()now resolves it fromKG_ENGINEat call time, and introspection (SHOW CONSTRAINTS/SHOW INDEXES) gained dialect-aware equivalents keyed on(label, property)— Memgraph's are unnamed.
Measured effect on the affected suites: 35 failures before, 0 after.
Step 4.2 — Flip + restart
.env:KG_URI/KG_ROUTING_URI→ new cluster;NEO4J_URIalias points the same way (nothing left reading the old address).python scripts/gen_level0.pyregenerates LEVEL0 (its content is derived from.env— the doc updates itself;--checkgate confirms).- Restart consumers in reverse order;
neo4j-dbcontainer: stopped, not deleted (rollback = restart it + flip.envback; kept 30 days with its final backup dump). - Live smoke (each with evidence): gateway session round-trip lands in
the new graph; kernel boot health line green; KG gRPC server answers;
session syncwrites;graph_browser.pybrowses; retention timer manual fire clean;wiki-rag-queryunaffected (semantic cluster untouched). - Verification: every smoke item shown;
MATCH (n) RETURN count(n)on the new cluster grows during a live gateway session while Neo4j's count (before stop) stays frozen — proof no consumer still writes to Neo4j.
Step 4.3 — Full authoritative suite, production config
python scripts/run_tests.py— full, kernel verified, against the flipped environment.- Verification: green; run report archived in
reports/.
Phase 5 — Decommission + knowledge propagation
Step 5.1 — Retire Neo4j surface (rollback window WAIVED — user ruling 2026-07-26)
- User ruling: "at the end after all is tested and verified you will stop
and delete the neo4j nodes." The 30-day window is waived; deletion
happens the SAME DAY, gated strictly on: Phase 3 checksums PASS + query
parity PASS + Phase 4 live smoke PASS + full authoritative suite green on
the flipped config. One final
neo4j-admindump file is retained on disk (existing backups dir) before deletion — a file, not a running container. - Then:
neo4j-db+neo4j-node-2/3/4terraform-destroyed,neo4j-backup.timerdisabled (replaced in 1.3). - Mechanical rename wave:
NEO4J_*env alias removed,neo4j_*module/file names renamed where they now lie (neo4j_query.py→kg_query.py, etc.) via AST-toolkitrename/refs— dry-run diffs reviewed, tests green. - Verification:
terraform planzero-diff post-destroy; zero grep hits for the old IP;rulings-grade grep proves noNEO4J_reads outside the changelog/history.
Step 5.2 — Docs, memories, skills, wiki
- Update: LEVEL0 (regenerated),
INFRASTRUCTURE.md, AGENTS.md commands,.claude/skills/(orchgraph-memgraph skill gains the KG cluster; any Neo4j-specific skill text updated), global + project memory files (two-graph doctrine becomes "operational KG on orchgraph-kg cluster; semantic/RAG on shared orchgraph cluster — still never conflate"), wiki graph-engine page gets the migration record + final numbers. - Verification:
gen_level0.py --checkclean; wiki page updated +wiki-rag-ingestre-run; memory files updated in the same session.
Execution order & effort
| Phase | Depends on | Effort | Gate to next |
|---|---|---|---|
| 0 Decisions + inventory + dialect gate | — | 1 day | decisions ratified; inventory complete |
| 1 Provision target | 0 | 1 day | cluster healthy, DDL idempotent, backup drill done |
| 2 Access layer + test fleet | 0, 1 | 2–3 days | FULL suite green on orchgraph workers |
| 3 Data migration + parity | 1, 2 | 1 day | checksums + parity 100% |
| 4 Cutover | 2, 3 | 0.5 day | live smoke + full suite on prod config |
| 5 Decommission + docs | 4 + 30 days | 0.5 day | zero-diff, zero stale references |
Rollback is defined at every point: before 4.2 nothing production-facing moved;
after 4.2 it is "restart neo4j-db + flip .env back" for 30 days.
Definition of done (whole plan)
- Every step's verification shown, not asserted.
- Full authoritative suite (kernel started + gRPC-verified) green against the orchgraph production config AND orchgraph test workers.
- All live consumers demonstrably writing to the new cluster; Neo4j count frozen; container stopped.
- Backups: one successful automatic snapshot pull + one restore drill.
- LEVEL0/INFRASTRUCTURE/AGENTS/skills/memories/wiki all updated — no doc still calls Neo4j the operational KG.
session endwith artifacts, choices, handoff; CHANGELOG + tag.