Skip to content

Node IDs for file / directory / project nodes embed the absolute scan path (leaks local username, makes committed graph.json non-portable / non-idempotent) #1789

Closed

Description

@fremat79

Summary

graphify already relativizes the source_file field of every node so the graph
is portable across machines. However, the node id for structural nodes
(files, directories, and — for .NET — .csproj/.sln/solution/project container
nodes) is still built from the absolute resolved path. As a result:

  • the committed graph.json leaks the local OS username and full home path
    inside those ids, and
  • the same file produces a different id on a different machine (or a different
    checkout location), so graphify update on another clone re-creates those nodes
    → diff churn + non-idempotent output for a versioned graph.

Symbol nodes (classes, functions, methods) are unaffected because their ids are
name/symbol-based; only path-derived structural nodes are impacted.

Environment

  • graphify (graphifyy) 0.8.19
  • Windows (but the logic is OS-agnostic; same on Linux/macOS with /home/<user>/…)

Current behavior

  1. detect() resolves the scan root to an absolute path unconditionally:
    • detect.py → root = root.resolve() (≈ line 866). Every file path handled
      downstream is therefore absolute.
  2. Structural / file nodes get their id from the absolute path string:
    • extract.py → _make_id(str(path)) (e.g. the file-node id assignments around
      lines 1684 / 2526 / 2646). path here is absolute (from step 1).
  3. By contrast, symbol nodes use a name-based id: _make_id(module_name) /
    _make_id(str(resolved)) — no path, no leak.
  4. Crucially, graphify already relativizes source_file but not the id:
    • watch.py::_relativize_source_files (≈ lines 131–143):
      item["source_file"] = str(source_path.resolve().relative_to(root)), called
      from _rebuild_code (≈ line 438). So the scan root is known and the code
      already knows how to make paths relative — it just isn't applied to the id.

Concrete example

Project on machine A, user alice:

C:\Users\alice\repos\MyApp\src\Widgets\MyApp.Widgets.csproj

produces a node whose source_file is nicely relative:

{ "source_file": "src/Widgets/MyApp.Widgets.csproj", "label": "MyApp.Widgets.csproj",
  "id": "c_users_alice_repos_myapp_src_widgets_myapp_widgets_csproj" }

but the id still hard-codes the absolute path (note c_users_alice_repos_myapp_…).

The same repo cloned by user bob to D:\work\MyApp yields the id
d_work_myapp_src_widgets_myapp_widgets_csproj — a different id for the same file.
Directory container nodes behave the same (e.g. a Widgets folder node →
c_users_alice_repos_myapp_src_widgets).

Why this matters

Teams that want to commit graph.json to version control (so colleagues can
query the graph without rebuilding) hit three problems on the affected nodes:

  1. Privacy / hygiene: the committed file contains each contributor's OS username
    and local folder layout.
  2. Non-portability: on a fresh clone the ids don't match the committed graph.
  3. Non-idempotency / churn: graphify update (which re-runs detect() →
    absolute root) re-adds absolute-path-id versions of those nodes on every run and
    on every different machine, producing noisy, per-contributor diffs.

Today the only workarounds are to (a) exclude the files that create these nodes via
.graphifyignore (loses the info, e.g. the .NET project-dependency graph), or
(b) post-process graph.json to rewrite the ids — both external to graphify.

Reproduction (minimal)

mkdir -p /tmp/demo/src && printf '<Project/>' > /tmp/demo/src/Demo.csproj
graphify /tmp/demo            # or: cd /tmp/demo && graphify .
grep -o '"id": *"[^"]*demo[^"]*"' graphify-out/graph.json | head
# -> ids contain the absolute path, e.g. "tmp_demo_src_demo_csproj" and, on a real
#    home dir, "<drive>_users_<username>_…"

Expected behavior / proposed fix

Make structural/file/directory/project node ids relative to the scan root, the
same way source_file is already relativized. Options, smallest first:

  1. Relativize before _make_id: when building a file/dir node id, pass the path
    relative to scan_root (the code already computes relative_to(root) in
    _relativize_source_files). This keeps ids stable and portable and requires no
    new API — it just makes the id consistent with the already-relative source_file.
  2. Or add an opt-in flag / env var (e.g. --relative-ids / GRAPHIFY_RELATIVE_IDS=1)
    for teams that commit graph.json, if a default change is a concern for backward
    compatibility of existing graphs.

Either way the goal is: the same source file yields the same node id regardless of
where the repo is checked out, and the committed graph.json never contains an
absolute local path.
This would make graph.json a first-class, VCS-friendly
artifact.

Happy to help test a patch. Thanks for graphify!

Activity

  1. safishamsi commented on Jul 11, 2026

    @safishamsi
    Member

    Already fixed in current releases (you're on 0.8.19; current is 0.9.12). On v8, detect() returns file paths relative to the scan root, so the committed graph.json uses relative structural node ids — no absolute path, no username/home leak. I verified it end-to-end: extracting the same repo from two completely different absolute checkout paths (/tmp/alice_home/proj vs /tmp/bob_elsewhere/checkout/proj) produces identical node ids (pkg_app, pkg_mod, …), and directory/.csproj/.sln container nodes are all relative too. Added a regression test locking cross-checkout id portability.

    Note: if you call the extract() Python API directly with absolute paths (rather than the CLI), you'll still get absolute-embedded ids — but the CLI, which produces the committed graph.json, relativizes. Please uv tool install --upgrade graphifyy; 0.8.19 predates the fix. Thanks for the precise write-up.

  2. fremat79 commented on Jul 12, 2026

    @fremat79
    ContributorAuthor

    Hi, and thank you very much for looking into this and for the fix — really appreciated.

    I'm sorry to reopen this, and it's entirely possible I'm doing something wrong on my side, but unfortunately after updating to the latest release we're still seeing the same behaviour in our tests. I put together a tiny reproduction in case it helps, and I apologise in advance if it turns out to be a mistake at our end.

    Environment

    • graphify 0.9.13 (installed via uv tool; graphify --version reports graphify 0.9.13)

    Minimal reproduction

    I've attached graphify-1789-repro.zip. It's a tiny .NET solution and needs nothing but graphify itself — no .NET SDK, no build, no API key. Full steps are in REPRODUCE.md inside the zip; briefly:

    1. unzip, then cd graphify-1789-repro
    2. graphify update .
    3. python check_ids.py (or just open graphify-out/graph.json)

    What we see

    The structural container nodes still carry the absolute local path — including the username — in their id, while source_file is already relative. In this minimal case two nodes are affected (the Plugins and Services solution folders):

    id          = c_users_<user>_..._graphify_1789_repro_plugins
    source_file = Plugins
    

    On our real solutions the same happens for the per-folder/solution nodes, which unfortunately ends up committing our usernames into graph.json and produces per-machine diffs on graphify update.

    What we expected

    That the id would be a slug relative to the scan root, like source_file already is — with no absolute path or username (e.g. id = plugins).

    Sorry again if I've simply missed a setting or a newer build. Please let me know if there's anything else I can provide, and thanks again for your work on graphify.

    graphify-1789-repro.zip

  3. safishamsi commented on Jul 12, 2026

    @safishamsi
    Member

    You're right, and thank you for the clear follow-up, @fremat79. I closed this prematurely: the 0.9.13 fix relativized .sln/.csproj file node ids, but I never checked the virtual solution-folder nodes, and those still carried the absolute local path (with the username) in their id.

    I reproduced it independently with a minimal solution containing solution folders and confirmed the leak, so I did not need to open your attachment.

    Root cause is in extract_sln: a solution folder is a virtual grouping, not a file (Visual Studio writes its name as both the display name and the "path"), but the extractor resolved it to an absolute filesystem path and keyed the node id off that. The CLI's id-relativization pass only remaps ids of real files in the scan set, so a virtual folder never matched and its absolute id survived into graph.json.

    Fixed in 373bc8e: solution folders are now detected (name == path) and keyed off the folder name only, so you get id=plugins as expected; real project files still resolve as before. Added a regression test for it. This ships in 0.9.14.

    Sorry for the churn on this one.

  4. safishamsi commented on Jul 13, 2026

    @safishamsi
    Member

    Shipped in 0.9.14 (now on PyPI: pip install --upgrade graphifyy). The solution-folder node-id leak is fixed in 373bc8e — virtual solution folders are keyed off the folder name only (id=plugins), so no absolute path or username lands in graph.json, while real project files still resolve as before. Verified against the 0.9.14 wheel with nested folders and folder names containing spaces. Thanks again @fremat79 for the precise repro and for catching that the earlier close was premature.

  5. added a commit that references this issue on Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions