definition_file is stored as the build machine's absolute path, while its sibling source_file is repo-relative. Any consumer that reads the graph from a different machine — or a different checkout — gets a path it cannot open, and the graph leaks the build host's directory layout.
Reproduced on 0.9.53 (stock, no local modifications).
Reproduction
Two files are enough — one C++ declaration/definition pair:
repro/
src/Foo.h
src/Foo.cpp
// src/Foo.h
#pragma once
class Foo {
public:
int Bar(int x);
};
// src/Foo.cpp
#include "Foo.h"
int Foo::Bar(int x) {
return x + 1;
}
cd repro && git init -q && git add -A && git commit -qm init
graphify update /absolute/path/to/repro
Resulting node in graphify-out/graph.json (4 nodes total, 1 carries definition_file):
{
"id": "src_foo_foo_bar",
"label": "Bar",
"source_file": "src/Foo.h",
"source_location": "L5",
"definition_file": "/absolute/path/to/repro/src/Foo.cpp",
"definition_location": "L3"
}
source_file is src/Foo.h. definition_file is absolute.
Expected
definition_file should be src/Foo.cpp — the same form as source_file. It names a file inside the scanned tree, so it should be portable for the same reasons source_file is.
Why it matters
The MCP get_node tool prints both, so an agent sees:
Source: src/Foo.h L5
Defined in: /home/ci/build/repos/myproject/src/Foo.cpp L3
If the graph is built anywhere other than where it is consumed — a build server, a container, a shared graph served over --transport http — the second path does not resolve on the client, and the client cannot rewrite it because the prefix is not part of the graph. It also puts the build host's directory layout into a graph that may be shared more widely than the machine that produced it.
We hit this with graphs built on a Linux indexing host and consumed by editors on other machines: 13,682 nodes across three C++ repositories, 100% of them absolute.
Root cause
_merge_decl_def_classes copies the implementation's source_file before any normalization runs:
graphify/extractors/resolution.py:2260 — keeper["definition_file"] = definition["source_file"]
Every pass that makes stored paths portable keys on the literal string "source_file" and therefore never sees the new attribute:
graphify/build.py:975 — node["source_file"] = _norm_source_file(node["source_file"], _root)
graphify/watch.py:321 — _relativize_source_files
graphify/watch.py:339 — _rebase_relative_source_files
grep -c definition_file graphify/build.py graphify/watch.py returns 0 and 0 on 0.9.53.
The field is otherwise inert — resolvers, incremental merge, dedup and the shrink guard do not read it. As far as I can tell the only readers are graphify/serve.py:1809 (get_node output) and exporters/graphdb.py, which passes scalar attributes through. So this is an output-correctness bug, not a graph-integrity one.
Why the existing test does not catch it
tests/test_languages.py:3594:
assert str(bar.get("definition_file", "")).endswith("Foo.cpp"), bar
An absolute path ends with Foo.cpp too, so the assertion passes either way. A full-string comparison against the expected relative path would have caught it. (That test came with the original decl/def change, which I contributed — the gap is mine.)
Existing patch
PR #3023 has the fix: it relativizes definition_file alongside source_file in build_from_json and introduces a _PORTABLE_PATH_KEYS tuple in watch.py so both normalizers cover the pair. It is against v8 and still applies cleanly to 0.9.53 — happy to rebase, split, or hand it over if you would rather write it differently.
definition_fileis stored as the build machine's absolute path, while its siblingsource_fileis repo-relative. Any consumer that reads the graph from a different machine — or a different checkout — gets a path it cannot open, and the graph leaks the build host's directory layout.Reproduced on 0.9.53 (stock, no local modifications).
Reproduction
Two files are enough — one C++ declaration/definition pair:
Resulting node in
graphify-out/graph.json(4 nodes total, 1 carriesdefinition_file):{ "id": "src_foo_foo_bar", "label": "Bar", "source_file": "src/Foo.h", "source_location": "L5", "definition_file": "/absolute/path/to/repro/src/Foo.cpp", "definition_location": "L3" }source_fileissrc/Foo.h.definition_fileis absolute.Expected
definition_fileshould besrc/Foo.cpp— the same form assource_file. It names a file inside the scanned tree, so it should be portable for the same reasonssource_fileis.Why it matters
The MCP
get_nodetool prints both, so an agent sees:If the graph is built anywhere other than where it is consumed — a build server, a container, a shared graph served over
--transport http— the second path does not resolve on the client, and the client cannot rewrite it because the prefix is not part of the graph. It also puts the build host's directory layout into a graph that may be shared more widely than the machine that produced it.We hit this with graphs built on a Linux indexing host and consumed by editors on other machines: 13,682 nodes across three C++ repositories, 100% of them absolute.
Root cause
_merge_decl_def_classescopies the implementation'ssource_filebefore any normalization runs:graphify/extractors/resolution.py:2260—keeper["definition_file"] = definition["source_file"]Every pass that makes stored paths portable keys on the literal string
"source_file"and therefore never sees the new attribute:graphify/build.py:975—node["source_file"] = _norm_source_file(node["source_file"], _root)graphify/watch.py:321—_relativize_source_filesgraphify/watch.py:339—_rebase_relative_source_filesgrep -c definition_file graphify/build.py graphify/watch.pyreturns0and0on 0.9.53.The field is otherwise inert — resolvers, incremental merge, dedup and the shrink guard do not read it. As far as I can tell the only readers are
graphify/serve.py:1809(get_nodeoutput) andexporters/graphdb.py, which passes scalar attributes through. So this is an output-correctness bug, not a graph-integrity one.Why the existing test does not catch it
tests/test_languages.py:3594:An absolute path ends with
Foo.cpptoo, so the assertion passes either way. A full-string comparison against the expected relative path would have caught it. (That test came with the original decl/def change, which I contributed — the gap is mine.)Existing patch
PR #3023 has the fix: it relativizes
definition_filealongsidesource_fileinbuild_from_jsonand introduces a_PORTABLE_PATH_KEYStuple inwatch.pyso both normalizers cover the pair. It is againstv8and still applies cleanly to 0.9.53 — happy to rebase, split, or hand it over if you would rather write it differently.