Skip to content

FalkorDB backend (server + embedded Lite), replacing NetworkX - #1379

Open
galshubeli wants to merge 20 commits into
Graphify-Labs:v8from
galshubeli:falkordb-backend
Open

FalkorDB backend (server + embedded Lite), replacing NetworkX#1379
galshubeli wants to merge 20 commits into
Graphify-Labs:v8from
galshubeli:falkordb-backend

Conversation

@galshubeli

@galshubeli galshubeli commented Jun 18, 2026

Copy link
Copy Markdown

Replace NetworkX with FalkorDB (server and embedded Lite)

This migrates graphify's runtime graph backend from in-memory NetworkX to a cache-free FalkorDB GraphStore: the graph lives in the engine and is never materialized in Python. The existing NetworkX-shaped API (G.nodes, G.edges, G.degree, G[u][v], …) is preserved as a thin facade, so the ~18 call-site modules were minimally touched, but every read is now a scoped/streamed Cypher query.

Highlights

  • Cache-free GraphStore — nx-compatible views backed by scoped Cypher; nothing is loaded into Python.
  • Server-side JS UDFs for the algorithms FalkorDB lacks: Louvain, edge-betweenness, simple-cycles (deterministic — sorted iteration, no RNG).
  • FalkorDB Lite — an embedded, in-process engine (falkordblite/redislite, no server) selectable via a falkordb-lite:// URI or GRAPHIFY_FALKORDB_LITE; new lite extra (Python ≥ 3.12).
  • NetworkX fully removed — zero runtime networkx imports; the test suite builds fixtures via a MemGraph-backed nxcompat shim.
  • Query-path optimizations (truncate-before-fetch rendering, in-engine context filtering, cached hub threshold) and an audit removing whole-graph pulls from the interactive paths.
  • CI runs a falkordb/falkordb service container; both test (3.10) and test (3.12) are green.

Benchmark

Per repo, mean over the four operations (query · path · explain · affected), 3 repetitions. Each cell is mean ± run-to-run standard deviation. Bold = better (lower).

repolatency (ms)CPU (ms)memory (MB)
NetworkXFalkorDBNetworkXFalkorDBNetworkXFalkorDB
FalkorDB-repo (9.2k/26k)852 ± 8152 ± 1851 ± 781 ± 189 ± 035 ± 1
llama-stack (15k/35k)708 ± 678 ± 1709 ± 635 ± 0109 ± 034 ± 0
django (50k/142k)438 ± 3409 ± 4438 ± 2120 ± 2311 ± 237 ± 0
**Notes** - **Memory & CPU are the host (Python) process.** FalkorDB offloads the graph + query compute to the engine, so the host stays flat (~34–37 MB) and CPU-light; NetworkX holds the whole graph in-process (88→311 MB, growing with size). - These host figures are identical for the embedded **`falkordblite`** backend (Lite ≈ server on latency). - Averages are lifted by `affected` (NetworkX runs a 0.4–3.2 s in-memory BFS there); the one op NetworkX wins is `query` on a hub node, where its in-RAM traversal beats engine round-trips (verified equivalent work: same seed/depth, ~11.3k nodes reached on django).

🤖 Generated with Claude Code

galshubeli and others added 18 commits June 17, 2026 09:30
Replace NetworkX with FalkorDB as the sole graph store, via a cache-free
GraphStore facade: the graph lives in the engine and is never loaded into
Python. All traversal/query/analysis run as scoped or in-engine Cypher;
algorithms FalkorDB lacks (Louvain, edge-betweenness, simple-cycles) are
server-side JS UDFs (graphify/udfs/graphify_algos.js). multigraph_compat is
removed (native parallel edges).

Query-path optimizations:
- truncate-before-fetch rendering: rank by degree + LIMIT in-engine, fetch
  attrs only for the nodes that fit the token budget (hub query 7-14s -> ~1s)
- in-engine edge-context filtering (no MemGraph copy of the whole graph)
- hub-degree threshold computed once and cached in graph metadata

Audit fixes: MCP tools get_node / graph_stats / audit no longer scan all
nodes/edges (use search_nodes + a confidence_counts aggregation).

NetworkX fully removed (not a dependency): the package has zero runtime nx
imports; the test suite builds fixtures via tests/nxcompat.py, a drop-in nx
shim backed by the in-house MemGraph.

FalkorDB Lite: embedded in-process engine (redislite + bundled falkordb
module), no external server. Select with a `falkordb-lite://<rdb>` URI or the
GRAPHIFY_FALKORDB_LITE env var; install via the new `lite` extra (Python
>=3.12). Warm performance matches the server (no TCP hop) at comparable memory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t load

Post-migration these open a cache-free FalkorDB store handle (a connection)
and never load the graph into memory, so the NetworkX-era "load" naming was
misleading:
- serve._load_graph        -> serve._connect_graph
- affected.load_graph      -> affected.connect_graph
- CLI messages: "Loading existing graph..." -> "Connecting to existing graph...";
  "could not load graph" -> "could not connect to graph"
- clarified the MCP hot-reload docstring (it reconnects when the graph.json
  change-sentinel is rewritten; it does not reload the file)

Genuine loaders are unchanged: callflow_html.load_graph (parses graph.json into
data), the merge-driver graph.json loaders, and _load_graphifyignore/include.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From section-by-section review of the migration:

- __main__.py: `affected` and `cluster-only` no longer require the legacy
  graph.json file to exist on disk. In FalkorDB-only mode the graph lives in the
  engine (located via the falkordb.json pointer) and the artifact may be absent;
  these commands now rely on _connect_graph's pointer resolution + empty-graph
  guard, matching `query`/`path`/`explain`. Previously `query` worked but
  `affected`/`cluster-only` failed with "graph file not found".

- store.py: UDFs (louvain/edgeBetweenness/simpleCycles) are now loaded lazily by
  the algorithm wrappers that use them instead of in GraphStore.__init__, so the
  interactive commands (query/path/explain/affected) — which never use UDFs — no
  longer pay a UDF reload round-trip on every process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From continued section-by-section review:

- analyze.suggest_questions: cap exact node-betweenness at 5000 nodes (same
  guard _cross_community_surprises already uses). Exact betweenness is ~O(V*E);
  without a cap, report/cluster-only could be very slow on large repos. The
  bridge-node questions are supplementary — ambiguous-edge + community questions
  are unaffected.

- export.to_svg: render a top-degree overview (>600 nodes) instead of the full
  graph. The pure-Python force layout (_spring_layout) is O(n^2) per iteration,
  so a full-graph SVG of a large graph would effectively hang (verified: a
  15k-node graph now renders a 600-node overview in ~9s instead of never).
  An SVG of thousands of nodes is an unreadable hairball anyway.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the gaps found reviewing PR #1:

- affected.connect_graph now guards the empty-graph case (raises so the CLI
  prints "Re-run /graphify to build" + exits non-zero) instead of handing back
  an empty store and silently reporting "no unique node match". Parity with
  serve._connect_graph and the query/path/explain commands.

- tests: validate the hand-rolled GraphML writer emits well-formed XML
  (xml.etree parse), add a to_svg export test (skipped without matplotlib), and
  add an affected unbuilt-graph test that asserts a non-zero exit (covers the
  guard above).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: run falkordb/falkordb as a service container so the conftest session
  fixture connects (otherwise every graph test silently skips in CI).
- cluster.py: materialize dict(G.degree()) once instead of per-node G.degree(n)
  calls, which against the cache-free store would be O(N) round-trips (twice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve 39 conflicts, reconciling the FalkorDB migration (cache-free GraphStore,
no networkx) with v8's improvements:

- pyproject: keep falkordb + numpy (v8's _minhash replaced datasketch); drop
  networkx + datasketch.
- build.py: keep v8's dedupe_nodes/dedupe_edges helpers + Graphify-Labs#1257 ghost-collision
  tracking + Graphify-Labs#1327 AST-origin skip, adapted to node_attrs (no in-memory G during
  build); keep the GraphStore build_from_json.
- serve.py: adopt v8 label_tokens matching (Graphify-Labs#1353) + community_name (Graphify-Labs#1305) on
  the cache-free _find_node/_tool_get_node paths.
- analyze.py: keep the simpleCycles UDF path (max_cycle_length bounds it like
  v8's length_bound Graphify-Labs#1196).
- export.py: keep store-native to_json; fold in v8 community_name (Graphify-Labs#1305).
- global_graph.py: keep the GraphStore batch path; adopt v8 remap-and-rewire of
  deduplicated externals.
- affected.py: keep FalkorDB connect_graph + find_node_ids; adopt v8 normalized
  + bare-name matching (Graphify-Labs#1353) in the in-memory fallback.
- __main__.py: keep _connect_graph loading; preserve v8's Graphify-Labs#1019 huge-graph html
  guard by node count; adopt community_name in explain.
- skill docs regenerated from the merged update fragment (prune = deleted +
  changed, Graphify-Labs#1178); update goldens blessed.
- ci.yml: FalkorDB service container.
- tests resolved to the migration versions (seed_graph/store/nxcompat); v8's
  graph.json-loading tests no longer apply.

Verified: package imports, zero runtime networkx, 279/280 merge-relevant tests
pass (1 env tree-sitter fail). Remaining full-suite failures are pre-existing
env (missing tree-sitter parsers) — test_watch prune-edge-case fails at the
pre-merge commit too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pyproject: add `falkordb` extra so the documented `graphifyy[falkordb]`
  install resolves (falkordb is a core dep; the extra just re-lists it).
- conftest: the FalkorDB reachability skip is no longer session-autouse — the
  store/seed_graph/make_store fixtures depend on it, so only DB-backed tests
  skip when no engine is reachable (DB-free tests still run; CI can't silently
  pass with 0 tests).
- update skill doc: the graph-diff step used a global `graphify_diff_scratch`
  graph name (data-loss if a user has that graph; collisions on concurrent
  runs). Derive a per-project + pid scratch name from the current graph name.
  Regenerated skill artifacts + blessed goldens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v8 merge folded label_tokens matching (Graphify-Labs#1353) into _find_node, but the
cache-free rows are (id, norm_label) tuples with no `d` in scope — a NameError
on every _find_node call (explain / get_node / score). Carry the raw label in
rows (3-tuple) and tokenize that, mirroring _score_nodes. This fix existed in
the working tree but was not staged into the merge commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1116)

A symbol removed from a file that still exists was not pruned on
`graphify update` / watch rebuild: the eviction keys off the AST-provenance
marker `_origin` in graph.json, but the FalkorDB migration stripped all
underscore-prefixed keys at the store boundary (_scalar_props) and in to_json,
so `_origin` never reached graph.json and stale AST nodes (e.g. a deleted
`foo()`) survived. (Pre-existing since the migration; surfaced once tree-sitter
parsers are installed.)

- store.add_nodes_from: keep `_origin` (the one underscore-marker that must
  persist); other `_` keys stay dropped.
- export.to_json: keep `_origin` on exported nodes.
- watch._canonical_topology_for_compare: exclude `_origin` (provenance, not
  topology) so an unchanged graph still compares equal across serializers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lockfile predated the migration's pyproject changes, so it had no falkordb
entry (and still pinned networkx). CI installs with `uv sync --all-extras
--frozen`, which used that stale lock and left the falkordb client uninstalled
-> every DB test ImportError'd ("No module named 'falkordb'").

Re-resolved against the current pyproject: adds falkordb 1.6.1, falkordblite
(py>=3.12 marker), redis, psutil; bumps graphifyy to 0.8.40. The FalkorDB
service container (added to ci.yml) provides the server the tests connect to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two CI failures (surfaced once the FalkorDB client installs and tree-sitter
parsers are present in CI):

- test_java_type_resolution used G.in_degree(v); GraphStore only had degree.
  Add in_degree/out_degree (scoped counts) to GraphStore and MemGraph.
- test_build_merge_preserves_call_edge_direction called
  build_merge([], graph_path) — the migration's build_merge takes a graph_name,
  not a graph.json path, so the Path reached select_graph() and raised
  "Expected a string parameter". Reconnect via G1.graph_name instead. (Was
  masked locally because the JS parser isn't installed.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts after upstream advanced (Java records, Swift relationships,
build_merge replace-on-re-extract Graphify-Labs#1344, dedup fixes, HTML XSS hardening, etc.):

- build.py build_merge: adopt v8's replace-on-re-extract (Graphify-Labs#1344/Graphify-Labs#1007) — drop a
  changed file's prior source_file contribution before merging so stale
  nodes/edges don't accumulate — but keep loading the base from the FalkorDB
  store (not graph.json), and keep graph_name/uri binding.
- skillgen update fragment: keep the FalkorDB _store/open_store binding for the
  --update build_merge call; fold in v8's root= prune-relativization note (Graphify-Labs#1361).
  Regenerated skill artifacts + blessed goldens.
- test_build.py: keep the store-based tests; v8's new graph.json/build_merge(path)
  tests don't apply to the store-based build_merge.

Verified: package imports, zero runtime networkx, 118 merge-relevant tests pass
(1 env-only tree-sitter fail), skillgen --check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sort each match tier (exact/prefix/substring) by node id so an
ambiguous label resolves to the same top match regardless of the
backend's row order (FalkorDB scan order vs. in-memory iteration).
Found via native-vs-reference parity review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aging, edge direction, seed resolution)

Four confirmed regressions found in external review of the FalkorDB backend:

- build.py: edges missing source_file crashed build_from_json with
  NameError (referenced an undefined `G`); backfill from node_attrs instead.
- pyproject.toml: package the FalkorDB UDFs (udfs/*.js) so wheel/sdist
  installs don't FileNotFoundError on Louvain/edge-betweenness/cycles.
- store.py: directed edge semantics — remove_edges deletes only src->tgt
  (was nuking reciprocal edges); G[u][v] / _edge_attrs prefer the forward
  edge while still finding a reverse edge (undirected-model lookup).
- affected.py: native resolve_seed gained the bare-callable-name tier
  (`foo` -> `foo()`) it had only on the in-memory fallback path.

Adds regression tests for each.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_traverse marked nodes visited mid-level, so a node reached from several
parents in the same BFS level kept only the first parent's edge — thinning
~1/3 of edges in dense subgraphs that `query` (and MCP) return to the user.
Defer the visited update to end-of-level (mirroring v8 _bfs) and RETURN
DISTINCT to dedup parallel edges. Edge sets now match the in-memory
reference exactly (verified on the 15k-node graph: 1964/1964 on the densest
seed). Found via PR Graphify-Labs#1379 comprehensive review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… graph.json)

A read command (query/path/explain/export/cluster-only) opening an empty
FalkorDB store now imports a node-link graph.json from the output dir on
first use, instead of failing with "graph empty". This preserves the prior
workflow for projects built before the FalkorDB backend and for `--no-cluster`
runs (which write only JSON) — existing graphs stay queryable without a rebuild.

One-time, transparent (prints a migration note); no-op when no graph.json
exists. Found via PR Graphify-Labs#1379 comprehensive review (Graphify-Labs#3).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When no FalkorDB server is reachable at the local default, _connect now
transparently starts the embedded engine (FalkorDB Lite) instead of failing,
so a plain install runs with no server to manage — matching the pre-FalkorDB
zero-setup experience. falkordblite is now a default dependency on Python
>= 3.12; on older interpreters a running server is still required (clear error).

A connection failure to an explicitly-configured REMOTE host still surfaces
as an error rather than silently using a local empty DB. Shared embedded RDB
lives under XDG_DATA_HOME/graphify (override: GRAPHIFY_LITE_DBFILE).

Found via PR Graphify-Labs#1379 comprehensive review (Graphify-Labs#2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v8 split the monolithic __main__.py into a thin shim + graphify/cli.py.
The FalkorDB backend was written against the monolith, so cli.py merged in
clean but entirely un-ported: 84 NetworkX/graph.json references and zero
open_store calls. Since this branch drops networkx as a dependency, that
would have merged green and ImportError'd on a default install.

Resolution:
- __main__.py: take v8's shim; replay our 19 hunks onto cli.py's
  dispatch_command (8 applied, 11 hand-ported).
- serve/build/store/affected/export/benchmark: combine both sides, keeping
  v8's fixes (Graphify-Labs#2074 honest relations, #BUG1 call sites, #BUG2 seed-first
  truncation, Graphify-Labs#1504 re-key, Graphify-Labs#1749 cross-language guard, Graphify-Labs#1831 GraphML
  coercion, Graphify-Labs#2032 label disambiguation) on the store-backed paths.
- 70 generated skill artifacts regenerated from the two conflicted
  fragments; `skillgen --check` passes on all 134.
- uv.lock regenerated from the merged pyproject.

Store extended so upstream fixes survive the backend swap:
- edge_attrs_all(): _edge_attrs used LIMIT 1, collapsing parallel edges and
  defeating Graphify-Labs#2074's honest relation reporting.
- node_connections()/subgraph_render_data(): return the edge's own
  source_file/source_location and true orientation via startNode(r), so
  #BUG1 call sites and edge direction survive.
- incoming_edges()/member_nodes(): give affected's native path the #BUG1
  locations and Graphify-Labs#1669 member seeding the in-memory path already had.

Bugs found and fixed while porting:
- open_store derived the graph name from the PARENT of the output dir, so
  two graph dirs sharing a parent silently shared one FalkorDB graph.
- A relation-less edge was stored with relation="RELATED_TO", so readers
  could not distinguish "no relation" from a real one.
- god-nodes imported the removed affected.load_graph; affected/god-nodes
  also could not read pre-FalkorDB graphs that query/explain could.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review panel

Reviewed by claude, gpt.

This PR migrates the affected and analyze graph operations from an in-memory NetworkX representation to a FalkorDB-backed store, adding native scoped/batched query paths (e.g. find_node_ids, incoming_edges, node_attrs_batch) while keeping in-memory fallbacks. It replaces load_graph with a connect_graph helper that opens the store and imports legacy graph.json on first use, and refactors label/file-node helpers to operate on attribute rows rather than graph objects. It also adds a FalkorDB service container to the CI workflow so graph-dependent tests run against a live backend instead of being skipped. Surface area also includes numerous updated test and skillgen expectation fixtures referenced in the changed-symbols list.

No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

graphify review

Impact — 2485 function(s) in the blast radius of 2070 changed node(s).

Health — regressions introduced by this change:

  • worsened (Ca·Ce 1794→2502): build_from_json() (Ca=139 Ce=18) — fans out to 18 callees (efferent coupling); 139 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 2400→2450): _rebuild_code() (Ca=50 Ce=49) — fans out to 49 callees (efferent coupling); 50 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 218→220): dispatch_command() (Ca=2 Ce=110) — fans out to 110 callees (efferent coupling)
  • worsened (Ca·Ce 144→175): _make_graph() (Ca=25 Ce=7) — fans out to 7 callees (efferent coupling); 25 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 112→128): to_html() (Ca=16 Ce=8) — fans out to 8 callees (efferent coupling); 16 callers depend on it (afferent coupling)
  • new offender: open_store() (Ca=25 Ce=4) — 25 callers depend on it (afferent coupling)
  • new offender: Graph (Ca=44 Ce=2) — 44 callers depend on it (afferent coupling)
  • new offender: _connect_graph() (Ca=12 Ce=7) — fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 39→56): _find_node() (Ca=14 Ce=4) — 14 callers depend on it (afferent coupling)
  • new offender: DiGraph (Ca=21 Ce=2) — 21 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 30→33): resolve_seed() (Ca=11 Ce=3) — 11 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 18→27): to_graphml() (Ca=9 Ce=3) — 9 callers depend on it (afferent coupling)
  • new offender: _connect() (Ca=7 Ce=3) — 7 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 0→21): _make_graph() (Ca=21 Ce=1) — 21 callers depend on it (afferent coupling)
  • worsened (Ca·Ce 0→16): _graph_file() (Ca=16 Ce=1) — 16 callers depend on it (afferent coupling)
  • new offender: _import_graph_json_into_store() (Ca=7 Ce=2) — 7 callers depend on it (afferent coupling)

Verification — 2485 function(s) need re-proving (callee-first):
tools_skillgen_fragments_references_shared_update_graphify_reference_incremental_update_and_cluster_only → tools_skillgen_fragments_references_shared_update_for_update_incremental_re_extraction → tools_skillgen_fragments_references_shared_update_for_cluster_only → tools_skillgen_fragments_references_shared_update → tools_skillgen_fragments_references_query_default_step_1_traversal → tools_skillgen_fragments_references_query_default_step_0_constrained_query_expansion_required_before_traversal → tools_skillgen_fragments_references_query_default_graphify_reference_query_path_explain → tools_skillgen_fragments_references_query_default_for_graphify_path → tools_skillgen_fragments_references_query_default_for_graphify_explain → tools_skillgen_fragments_references_query_default …

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not enforced this run (re-run with --prove to verify the blast radius)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2468 function(s) in blast radius — re-run with --prove to formally verify them

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

Comment thread graphify/affected.py


def resolve_seed(graph: nx.Graph, query: str) -> str | None:
def resolve_seed(graph, query: str) -> str | None:

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 regressionresolve_seed()

11 callers depend on it (afferent coupling).

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

Comment thread graphify/build.py

directed=True produces a DiGraph that preserves edge direction (source→target).
directed=False (default) produces an undirected Graph for backward compatibility.
def build_from_json(

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 regressionbuild_from_json()

fans out to 18 callees (efferent coupling); 139 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py


def _load_graph(graph_path: str) -> nx.Graph:
def _connect_graph(graph_path: str):

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 regression_connect_graph()

fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py
return sample


def _import_graph_json_into_store(gj_path: Path, store) -> bool:

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 regression_import_graph_json_into_store()

7 callers depend on it (afferent coupling).

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

Comment thread graphify/store.py
return Path(out_dir) / "falkordb.json"


def open_store(

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 regressionopen_store()

25 callers depend on it (afferent coupling).

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

Comment thread graphify/store.py
return redislite.FalkorDB(dbfilename=dbfile)


def _connect(uri: str, user: str | None = None, password: str | None = None):

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 regression_connect()

7 callers depend on it (afferent coupling).

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

Comment thread tests/nxcompat.py
return self._build()[key]


class Graph(_MutableGraph):

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 regressionGraph

44 callers depend on it (afferent coupling).

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

Comment thread tests/nxcompat.py
super().__init__(directed=False)


class DiGraph(_MutableGraph):

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 regressionDiGraph

21 callers depend on it (afferent coupling).

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

Comment thread tests/test_serve_http.py
@@ -47,6 +47,19 @@


def _graph_file(tmp_path: Path) -> str:

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 regression_graph_file()

16 callers depend on it (afferent coupling).

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

Clears the 20 failures CI reported on the merge commit.

Behavioral fixes (not test-only):
- build_from_json now records the scan root on the graph (`scan_root` meta) and
  build_merge reads it back when the caller omits root. This is the FalkorDB
  replacement for the `graphify-out/.graphify_root` marker; without it the
  skill's root-less --update never relativized absolute prune_sources and a
  deleted file's nodes survived as ghosts (Graphify-Labs#1571).
- to_graphml dropped graph-level attributes entirely, losing the hyperedge
  layer from every export; they are now emitted as for="graph" keys with the
  same Graphify-Labs#1831 scalar coercion as nodes/edges.
- find_import_cycles required the graph object to expose .simple_cycles(),
  so it crashed on a plain NetworkX graph passed in by a library caller
  (networkx exposes simple_cycles as a module function, not a method). The
  enumeration is a pure function of the edge list, so it moved to
  store.simple_cycles_from_edges with the FalkorDB UDF kept as the fast path.
- merge-graphs emitted directed: true; the combined cross-repo view is
  undirected, as the old nx.compose path produced (Graphify-Labs#1606). merge_node_link
  now takes directed=, defaulting to the first input's flag so the
  same-graph merge-driver still round-trips.
- benchmark could not read a pre-FalkorDB / --no-cluster graph.json; it now
  falls back to the same back-compat import serve and affected use.
- The MCP multi-project loader reported "could not connect to graph"; it is a
  not-found condition and now says so.

Test ports (v8 tests written against file-based loading):
- test_build_merge_hyperedges_and_prune: seed the store, pass graph_name; the
  two Graphify-Labs#1571 cases now pin the recorded scan_root instead of the marker file.
- test_extract: read edge direction from (u, v) — the _src/_tgt markers the
  undirected NetworkX storage needed no longer exist.
- test_export: build the dict/list-attribute graph as a MemGraph, the only
  shape that can carry non-scalar attrs (the engine stores scalars only).
- test_extract_cli: inject the committed semantic layer into the store, which
  is the incremental baseline now, not graph.json.
- test_src_layout_import_resolution: give each build its own store — without
  one, both builds target the default graph name and the second silently
  overwrites the first, comparing a graph with itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@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.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This pull request migrates several graph operations (notably affected.py and analyze.py) away from an in-memory NetworkX representation toward a FalkorDB-backed store, adding native code paths that use scoped/batched queries while retaining in-memory fallbacks. It also replaces the JSON graph loader (load_graph) with a connect_graph function that opens the store and imports legacy graph.json data on demand. Additionally, the CI workflow gains a FalkorDB service container so graph-dependent tests actually run, and the touched test files and skill/reference fixtures are updated accordingly.

Worth a look

  • connect_graph misidentifies extension-less directory pathsgraphify/affected.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
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2870 functions depend on the 2542 node(s) this change touches.

Health — this change adds coupling hotspots:

  • worse: build_from_json() — 141 callers, 18 callees
  • worse: _rebuild_code() — 50 callers, 49 callees
  • worse: dispatch_command() — 2 callers, 110 callees
  • worse: _make_graph() — 25 callers, 7 callees
  • worse: to_html() — 16 callers, 8 callees
  • new: open_store() — 27 callers, 4 callees
  • new: Graph — 44 callers, 2 callees
  • new: _connect_graph() — 12 callers, 7 callees
  • …and 10 more

Verification — 2870 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: 2853 function(s) in the blast radius were not formally verified this run

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

Comment thread graphify/affected.py


def resolve_seed(graph: nx.Graph, query: str) -> str | None:
def resolve_seed(graph, query: str) -> str | None:

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 regressionresolve_seed()

11 callers depend on it (afferent coupling).

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

Comment thread graphify/build.py

directed=True produces a DiGraph that preserves edge direction (source→target).
directed=False (default) produces an undirected Graph for backward compatibility.
def build_from_json(

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 regressionbuild_from_json()

fans out to 18 callees (efferent coupling); 141 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py


def _load_graph(graph_path: str) -> nx.Graph:
def _connect_graph(graph_path: str):

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 regression_connect_graph()

fans out to 7 callees (efferent coupling); 12 callers depend on it (afferent coupling).

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

Comment thread graphify/serve.py
return sample


def _import_graph_json_into_store(gj_path: Path, store) -> bool:

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 regression_import_graph_json_into_store()

9 callers depend on it (afferent coupling).

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

Comment thread graphify/store.py
return Path(out_dir) / "falkordb.json"


def open_store(

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 regressionopen_store()

27 callers depend on it (afferent coupling).

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

Comment thread graphify/store.py
return redislite.FalkorDB(dbfilename=dbfile)


def _connect(uri: str, user: str | None = None, password: str | None = None):

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 regression_connect()

7 callers depend on it (afferent coupling).

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

Comment thread tests/nxcompat.py
return self._build()[key]


class Graph(_MutableGraph):

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 regressionGraph

44 callers depend on it (afferent coupling).

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

Comment thread tests/nxcompat.py
super().__init__(directed=False)


class DiGraph(_MutableGraph):

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 regressionDiGraph

21 callers depend on it (afferent coupling).

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

Comment thread tests/test_serve_http.py
@@ -47,6 +47,19 @@


def _graph_file(tmp_path: Path) -> str:

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 regression_graph_file()

16 callers depend on it (afferent 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