You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
graphify merge-graphs collapses nodes with the same stem across different source graphs — produces false cross-runtime edges (v0.9.10)
Severity: High — produces confidently-wrong answers from graphify path queries. The user has no way to detect the silent data loss.
Affects:graphify merge-graphs on v0.9.10 (and likely earlier; related fixes in #1504, #1508, #1158 addressed adjacent cases but not this one).
Summary
When two separately-built graphs both contain a node with the same bare stem (e.g., a backend src/app.js and a frontend App.jsx that both produce a bare app node ID), graphify merge-graphs collapses them into a single node. The result is:
One node where there should be two distinct entities from different runtimes/repos
The node's label and source_file come from one graph (in my case, the second graph won the label), but the edges come from both graphs
graphify path then returns plausible-looking paths that cross runtime boundaries incorrectly
What's still broken: collisions between two separate graphs being combined via merge-graphs. The dedup warning from #1508 doesn't fire here, and the merge silently keeps one node's metadata while attaching edges from both.
Real-world trigger
I hit this building a cross-process knowledge graph for a project that has a backend src/app.js (Express) and a frontend App.jsx (React entry). The two files produce bare app node IDs in their respective builds:
Output:Merged 2 graphs -> 2384 nodes, 3968 edges (expected 2386 if no collision)
The merged graph contains a single src::app node with:
attribute
value
source
id
src::app
prefix from first input
label
App.jsx
from second input (frontend)
source_file
App.jsx
from second input (frontend)
But it has 82+ edges from BACKEND source files (app.js:L1…L43, etc.) and 11 edges from FRONTEND source files (App.jsx:L2…L7). It is a Frankenstein node.
The false-positive impact
graphify path "apiClient" "appointments.js" on the merged graph returns:
The user reads this as: "the frontend App.jsx imports from the backend routes/appointments.js" — but the front and back ends run in different runtimes (Vite/React vs Node/Express), so no such import can exist. The path LOOKS valid because the AST edge from src/app.js:13 (const appointmentsRoutes = require('./routes/appointments')) is attached to the frontend App.jsx node via the merge.
The real wiring is by string convention ('/api/appointments') and isn't represented in the graph at all.
Smaller reproduction (in a clean /tmp working dir)
In this minimal repro the merge correctly gives src::app (backend) and frontend::app (frontend) as separate nodes. The bug only triggers in the real Irmaya case — likely some interaction with the LLM-augmented graph or graph size. I haven't isolated the exact trigger condition beyond "≥ 2 graphs, ≥ ~500 nodes each, ≥ 1 LLM-derived node with a bare stem."
Even simpler: the prefix is wrong when run from a different cwd
Same two graphs, but merging from /tmp instead of from the project root:
cd /tmp
graphify merge-graphs \
/Users/arif/irmaya/src/graphify-out/graph.json \
/Users/arif/irmaya/frontend/src/graphify-out/graph.json \
--out /tmp/merged.json
# → nodes have id=::app, id=::app_express, etc. (empty namespace!)
The namespace prefix depends on the current working directory or the --out path, not on each input graph's source. Two merges of the same two files can produce different namespace prefixes.
Suggested fix
Don't silently collapse — merge-graphs should either:
Refuse to merge graphs that would produce colliding IDs (error out, point to the conflict), or
Add a namespace prefix derived from each input's source directory (e.g. src/app.js → src::app, frontend/src/App.jsx → frontend_src_App::app or similar), or
Auto-prefix with --as <tag> (the flag already exists per graphify help merge-graphs)
Use the --as <tag> flag consistently — the CLI already documents --as <tag> repo tag for --global (default: target directory name), but merge-graphs doesn't apply this to per-input namespacing.
Workaround (what I'm doing)
Don't use graphify path on the merged graph for cross-runtime (frontend ↔ backend) queries.
For cross-process wiring, use graphify only within a single runtime (src/graphify-out/graph.json for backend, frontend/src/graphify-out/graph.json for frontend), or fall back to grep for the string-based URL convention.
The graph.html viz is still useful for visual exploration since the source paths are visible.
Environment
graphifyy v0.9.10 (via uv tool install)
Python 3.12 (uv-managed)
macOS 25.5.0 (arm64)
Backend graph: pure AST extraction on src/
Frontend graph: graphify frontend/src/ --backend=openai --mode deep with local Ollama (qwen3-vl:30b) for the LLM semantic pass
Artifacts
The actual buggy graphs are at /Users/arif/irmaya/:
graphify merge-graphscollapses nodes with the same stem across different source graphs — produces false cross-runtime edges (v0.9.10)Severity: High — produces confidently-wrong answers from
graphify pathqueries. The user has no way to detect the silent data loss.Affects:
graphify merge-graphson v0.9.10 (and likely earlier; related fixes in #1504, #1508, #1158 addressed adjacent cases but not this one).Summary
When two separately-built graphs both contain a node with the same bare stem (e.g., a backend
src/app.jsand a frontendApp.jsxthat both produce a bareappnode ID),graphify merge-graphscollapses them into a single node. The result is:labelandsource_filecome from one graph (in my case, the second graph won the label), but the edges come from both graphsgraphify paththen returns plausible-looking paths that cross runtime boundaries incorrectlyRelated closed issues (not quite the same)
stderrwarning indeduplicate_entities)_file_stemusing only immediate parent → addressed inprefix_graph_for_global/deduplicate_entitiesWhat's still broken: collisions between two separate graphs being combined via
merge-graphs. The dedup warning from #1508 doesn't fire here, and the merge silently keeps one node's metadata while attaching edges from both.Real-world trigger
I hit this building a cross-process knowledge graph for a project that has a backend
src/app.js(Express) and a frontendApp.jsx(React entry). The two files produce bareappnode IDs in their respective builds:Reproduction (real-world evidence)
Run from
/Users/arif/irmaya/:Output:
Merged 2 graphs -> 2384 nodes, 3968 edges(expected2386if no collision)The merged graph contains a single
src::appnode with:idsrc::applabelApp.jsxsource_fileApp.jsxBut it has 82+ edges from BACKEND source files (
app.js:L1…L43, etc.) and 11 edges from FRONTEND source files (App.jsx:L2…L7). It is a Frankenstein node.The false-positive impact
graphify path "apiClient" "appointments.js"on the merged graph returns:The user reads this as: "the frontend
App.jsximports from the backendroutes/appointments.js" — but the front and back ends run in different runtimes (Vite/React vs Node/Express), so no such import can exist. The path LOOKS valid because the AST edge fromsrc/app.js:13(const appointmentsRoutes = require('./routes/appointments')) is attached to the frontendApp.jsxnode via the merge.The real wiring is by string convention (
'/api/appointments') and isn't represented in the graph at all.Smaller reproduction (in a clean
/tmpworking dir)In this minimal repro the merge correctly gives
src::app(backend) andfrontend::app(frontend) as separate nodes. The bug only triggers in the real Irmaya case — likely some interaction with the LLM-augmented graph or graph size. I haven't isolated the exact trigger condition beyond "≥ 2 graphs, ≥ ~500 nodes each, ≥ 1 LLM-derived node with a bare stem."Even simpler: the prefix is wrong when run from a different cwd
Same two graphs, but merging from
/tmpinstead of from the project root:The namespace prefix depends on the current working directory or the
--outpath, not on each input graph's source. Two merges of the same two files can produce different namespace prefixes.Suggested fix
merge-graphsshould either:src/app.js→src::app,frontend/src/App.jsx→frontend_src_App::appor similar), or--as <tag>(the flag already exists pergraphify help merge-graphs)WARNING: nodes 'app' from 'app.js' and 'app' from 'App.jsx' collapsed into one (different source files). Currently nothing.--as <tag>flag consistently — the CLI already documents--as <tag> repo tag for --global (default: target directory name), butmerge-graphsdoesn't apply this to per-input namespacing.Workaround (what I'm doing)
graphify pathon the merged graph for cross-runtime (frontend ↔ backend) queries.graphifyonly within a single runtime (src/graphify-out/graph.jsonfor backend,frontend/src/graphify-out/graph.jsonfor frontend), or fall back togrepfor the string-based URL convention.graph.htmlviz is still useful for visual exploration since the source paths are visible.Environment
graphifyyv0.9.10 (viauv tool install)src/graphify frontend/src/ --backend=openai --mode deepwith local Ollama (qwen3-vl:30b) for the LLM semantic passArtifacts
The actual buggy graphs are at
/Users/arif/irmaya/:src/graphify-out/graph.json— backend (clean)frontend/src/graphify-out/graph.json— frontend (clean)graphify-out/graph.json— merged (bug visible:src::appwith mixed runtime edges)