Skip to content

feat: NeuG embedded graph DB, Cypher CLI, and GDS Leiden clustering (opt-in) - #2895

Open
BingqingLyu wants to merge 1 commit into
Graphify-Labs:v8from
BingqingLyu:neug-leiden-integration
Open

feat: NeuG embedded graph DB, Cypher CLI, and GDS Leiden clustering (opt-in)#2895
BingqingLyu wants to merge 1 commit into
Graphify-Labs:v8from
BingqingLyu:neug-leiden-integration

Conversation

@BingqingLyu

Copy link
Copy Markdown

Summary

  • Add NeuG as an optional parallel graph storage engine alongside NetworkX, with native Cypher query support via CLI (graphify cypher) and MCP server (cypher_query tool)
  • Native incremental update via Cypher MERGE — O(delta) vs NetworkX's O(full graph) rebuild
  • GDS Leiden community detection via NeuG's native extension framework — runs clustering directly on graph.db
  • Incremental community detection via freeze-assignment Leiden — existing nodes' communities stay frozen, only new nodes get assigned; makes incremental impact visible for targeted wiki regeneration
  • delta-cluster: incremental community analysis command; --baseline seeds from an external clustering
  • Opt-in by design: enabled via GRAPHIFY_NEUG=1 or an existing graph.db; the default extract path is unchanged

Motivation

Graphify currently uses NetworkX + graph.json as its core graph storage. This architecture has bottlenecks:

  • Limited query capability: No declarative graph query language — only Python API traversal
  • Inefficient incremental updates: Every update requires loading full graph.json → merge → rebuild → re-serialize (O(full graph) even for single-file changes)
  • Performance ceiling at scale: Entire graph must be loaded into memory; NetworkX's pure-Python execution becomes a bottleneck on large graphs
  • Limited graph algorithm extensibility: Adding custom graph algorithms requires Python-level implementation with no native acceleration path
  • No incremental community detection: Existing Leiden/Louvain requires full re-clustering on every change, shifting existing nodes' assignments and obscuring what actually changed — forcing expensive full wiki regeneration instead of targeted updates

Why NeuG?

NeuG is a lightweight embedded graph database (C++ core, Python bindings):

  1. Native Cypher support — Declarative graph query language; AI agents can query the knowledge graph directly without custom Python code
  2. Native incremental updates — Cypher MERGE enables O(delta) upserts in-place, no full-graph reload needed; for 10K+ node graphs, single-file updates are near-instantaneous
  3. Battle-tested performance — LDBC benchmark world record holder; lightweight & embeddable (no standalone server, pip install neug is all it takes)
  4. Extensible graph algorithms — Native C++ extension framework for custom graph algorithms; Louvain/Leiden community detection already available, with more algorithms (PageRank, etc.) in development — can replace the current Python-based algorithm layer with significant performance gains
  5. Incremental Leiden via freeze-assignment — NeuG's GDS extension freezes existing nodes' communities and only assigns new nodes, preserving clustering stability and making incremental wiki impact visible: added files → affected communities → chapters to regenerate

Architecture

Dual-engine coexistence, each independently consuming extraction data:

extraction dict ──┬──> NetworkX (build.py)  → graph.json  (existing, default)
                  └──> NeuG (storage.py)    → graph.db    (opt-in)

When GRAPHIFY_NEUG=1 is set (or graph.db already exists), the NeuG pipeline also runs:

  • Ingests data into graph.db via COPY FROM (bulk) or MERGE (incremental)
  • Runs GDS Leiden clustering natively on graph.db
  • Exports graph.json from graph.db (not dual-write) — ensures downstream tools (wiki generation, HTML visualization, community labeling) continue to work unchanged, maintaining full backward compatibility

Changes

File Description
graphify/storage.py New — NeuG adapter layer (init, schema, ingest via COPY FROM/MERGE, query, close, community detection via GDS Leiden)
graphify/cli.py NeuG opt-in probe, GDS Leiden clustering path, delta-cluster subcommand, segfault guard for tiny graphs
graphify/__main__.py graphify cypher CLI command
graphify/serve.py cypher_query MCP tool for AI agents
graphify/llm.py Minor import for NeuG path
pyproject.toml Add neug>=0.1.3 optional dependency (neug extra + all extra)
README.md Document NeuG commands in Full command reference
ARCHITECTURE.md Add storage.py module description
tests/ Unit tests (test_storage.py, test_cypher_cli.py)

Usage

# Install
pip install graphify[neug]

# Extract with NeuG (opt-in)
GRAPHIFY_NEUG=1 graphify extract ./raw

# Once graph.db exists, NeuG stays active (no env var needed)
graphify extract ./raw

# Cypher query
graphify cypher "MATCH (n:code) RETURN n.label, n.source_file LIMIT 10"
graphify cypher "MATCH (a:code)-[e:edge]->(b:code) RETURN a.label, b.label LIMIT 10" --db path/to/graph.db

# Delta-cluster (incremental community analysis)
graphify delta-cluster ./raw
graphify delta-cluster ./raw --baseline communities.json  # seed from external clustering

# MCP server (AI agents query via cypher_query tool)
python -m graphify.serve graphify-out/graph.json

Test Plan

  • pytest tests/test_storage.py tests/test_cypher_cli.py -v — all tests passed
  • Full test suite: 4192 passed (including 7 terraform tests after installing tree-sitter-hcl)
  • MCP server cypher_query tool end-to-end verified
  • Incremental extract → MERGE upsert correct
  • Uninstall neug → graphify extract . runs normally (silent skip)

Note

This PR builds on the NeuG integration proposed in #1056. While #1056 established the core storage layer (graph.db, Cypher queries, MCP tool), this PR adds GDS extension support for native and incremental community detection.

…opt-in)

Add an optional NeuG-backed pipeline alongside the existing NetworkX one:

- storage.py: graph.db layer — single-table schema, COPY FROM bulk and
  incremental ingest with source pruning, backup-on-write protection
- cypher command (CLI + MCP): ad-hoc Cypher queries against graph.db
- GDS Leiden clustering: --resolution, --cluster-on-files, hub-based
  community labels, cohesion, god nodes, surprising connections
- delta-cluster: incremental community analysis via freeze-assign Leiden;
  --baseline seeds from an external clustering (old communities frozen,
  new nodes assigned on top)
- opt-in by design: enabled via GRAPHIFY_NEUG=1 or an existing graph.db;
  the default extract path is unchanged
- tests: test_storage.py, test_cypher_cli.py; neug extra in pyproject
  and uv.lock

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 5 advisory finding(s) below merit a look before merge.

Formal verification. 1 change(s) tested, no difference found (not proven).


Graphify review — findings

Adds an opt-in NeuG embedded-graph-DB pipeline: introduces graphify cypher and graphify delta-cluster CLI commands, a GRAPHIFY_NEUG=1 path in extract that builds graph.db and runs GDS Leiden clustering, plus a --cluster-on-files flag. Wires up storage.py (init/ingest/export, god-node/god-file/surprising-connection queries, Leiden subgraph clustering, freeze-assign delta analysis) and extends serve.py with NeuG-backed context filtering and graph-stats/query tools. Updates README and ARCHITECTURE docs for the new neug extra and commands, and adds storage tests.

Worth a look

  • --allow-partial guard removed from no-cluster overwrite pathgraphify/cli.py · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • NeuG clustered global merge path no longer calls global_addgraphify/cli.py:4004 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • --global no longer merges in the Neug clustered extract pathgraphify/cli.py:4237 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • MCP tool executes arbitrary client-supplied Cypher against graph.dbgraphify/serve.py:1983 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
  • --clear-ast no longer propagated when saving no-cluster manifestgraphify/cli.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 706 functions depend on the 331 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 124 callees
  • new: _query_graph_text() — 20 callers, 9 callees
  • new: _score_query() — 15 callers, 5 callees
  • new: _query_terms() — 20 callers, 3 callees
  • new: delta_analyze() — 5 callers, 10 callees
  • new: run_benchmark() — 16 callers, 3 callees
  • new: _stale_graph_sources() — 7 callers, 6 callees
  • new: _build_server() — 2 callers, 18 callees
  • …and 25 more — each is listed as a finding

Verification — 706 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 706 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify dispatch\_command.

The verifier did not have enough to check dispatch\_command, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in \_run\_cli (not a proof).

The verifier ran both versions of \_run\_cli on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_build\_server.

The verifier did not have enough to check \_build\_server, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly ImportError — names the real obstacle, not a sampling gap)

· 14 grounded finding(s) anchored inline below; 19 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/storage.py
return node_types


def ingest_extraction(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressioningest_extraction()

12 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/storage.py
shutil.copy2(node_csv, dest_dir / f"{tag}_nodes.csv")


def cluster_on_files(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressioncluster_on_files()

high coupling complexity (Ca·Ce = 25).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/storage.py
# ---------------------------------------------------------------------------


def find_god_nodes(conn: object, top_n: int = 10) -> list[dict]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionfind_god_nodes()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/storage.py
return score, reasons


def find_surprising_connections(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionfind_surprising_connections()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/storage.py
# ---------------------------------------------------------------------------


def cluster_by_neug(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressioncluster_by_neug()

fans out to 9 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_storage.py
_close(db, conn)


def test_find_surprising_connections(tmp_db):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_find_surprising_connections()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_storage.py
_close(db, conn)


def test_label_communities_by_hub(tmp_db):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_label_communities_by_hub()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_storage.py
# --- incremental delta analysis (freeze-assign leiden) ---


def test_run_leiden_freeze_assign(tmp_db):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_run_leiden_freeze_assign()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_storage.py
_close(db, conn)


def test_run_leiden_freeze_assign_resolution(tmp_db):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_run_leiden_freeze_assign_resolution()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread tests/test_storage.py
assert changes["dissolved_communities"][0]["old_size"] == 3


def test_delta_analyze(tmp_db):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressiontest_delta_analyze()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant