From 373bc8efd831a88fae47180dbb668cded5b8bdf2 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sun, 12 Jul 2026 14:31:50 +0100 Subject: [PATCH] fix(sln): keep Visual Studio solution-folder node ids relative (#1789) A solution folder is a virtual grouping, not a file: VS writes its name as both the display name and the "path" (name == path, no real file). extract_sln resolved it to an absolute filesystem path anyway and keyed the node id off that. The CLI id-relativization pass only remaps ids of real files in the scan set, so a virtual folder never matched and its absolute id (with the local username) survived into a committed graph.json. Detect solution folders (name == path) and key their id/source_file off the folder name only; real project files still resolve as before. Adds a regression test asserting the folder node id is relative. The earlier fix (0.9.13) covered .csproj/.sln file nodes but missed the virtual folders, so #1789 was closed prematurely; this completes it. Reported and diagnosed by @fremat79. Co-Authored-By: fremat79 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ graphify/extractors/sln.py | 19 +++++++++++++++---- pyproject.toml | 2 +- tests/test_dotnet.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c483b0..e144dfb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.9.14 (2026-07-12) + +- Fix: Visual Studio *solution folder* nodes no longer embed the absolute scan path (including the local username) in their `id` and `source_file` (#1789, thanks @fremat79). A solution folder is a virtual grouping, not a file — VS writes its name as both the display name and the "path" — but `extract_sln` resolved it to an absolute filesystem path anyway 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 a committed `graph.json` (e.g. `id=/Users//proj/Plugins` instead of `id=plugins`). Solution folders are now detected (name == path) and keyed off the folder name only; real project files still resolve as before. (The earlier fix covered `.csproj`/`.sln` file nodes but missed the virtual folders — this completes it.) + ## 0.9.13 (2026-07-12) - Fix: the query log is now opt-in (off by default) (#1797, thanks @adam-pond-agent). `querylog` wrote every `query`/`path`/`explain` question and corpus path (and full responses if `GRAPHIFY_QUERY_LOG_RESPONSES`) to a default-on, unbounded, fail-silent plaintext file at `~/.cache/graphify-queries.log` — outside any repo's .gitignore/retention, and undocumented, which contradicts graphify's on-device / no-telemetry posture. Logging is now OFF unless you opt in with `GRAPHIFY_QUERY_LOG_ENABLE=1` (default path) or `GRAPHIFY_QUERY_LOG=`; `GRAPHIFY_QUERY_LOG_DISABLE=1` still forces it off. All the query-log env vars are now documented in the README. diff --git a/graphify/extractors/sln.py b/graphify/extractors/sln.py index d77fa6e6..936ff9ff 100644 --- a/graphify/extractors/sln.py +++ b/graphify/extractors/sln.py @@ -34,10 +34,21 @@ def extract_sln(path: Path) -> dict: proj_path = m.group(2).replace("\\", "/") proj_guid = m.group(3).strip("{}") - try: - abs_proj = str((path.parent / proj_path).resolve()) - except Exception: - abs_proj = proj_path + # A solution folder is a VIRTUAL grouping, not a file: Visual Studio writes + # its name as both the display name and the "path" (proj_name == proj_path, + # no real file). Resolving it to an absolute path and keying the node id off + # that leaked the absolute scan path (incl. the OS username) into graph.json, + # because the CLI's id-relativization only remaps ids of real files in the + # scan set — a virtual folder never matches, so its absolute id survived + # (#1789). Use the folder name itself (relative, no filesystem resolution). + is_solution_folder = proj_path == proj_name + if is_solution_folder: + abs_proj = proj_name + else: + try: + abs_proj = str((path.parent / proj_path).resolve()) + except Exception: + abs_proj = proj_path proj_nid = _make_id(abs_proj) if proj_nid and proj_nid not in seen_ids: seen_ids.add(proj_nid) diff --git a/pyproject.toml b/pyproject.toml index 50a7b63d..b46d6fbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.9.13" +version = "0.9.14" description = "AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_dotnet.py b/tests/test_dotnet.py index eca02b09..37e0ba51 100644 --- a/tests/test_dotnet.py +++ b/tests/test_dotnet.py @@ -45,6 +45,35 @@ def test_sln_project_dependency(): assert "imports" in _relations(r) +def test_sln_solution_folder_ids_are_relative(tmp_path): + """Solution folders are virtual groupings, not files. Their node ids must be + derived from the folder name only — never the resolved absolute scan path, + which would leak the local username into a committed graph.json (#1789).""" + sln = tmp_path / "App.sln" + sln.write_text( + 'Microsoft Visual Studio Solution File, Format Version 12.00\n' + # a solution folder: type GUID 2150E333-... , name == path, no real file + 'Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", ' + '"{11111111-1111-1111-1111-111111111111}"\n' + 'EndProject\n' + # a real project resolves to an absolute path as before + 'Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "App\\App.csproj", ' + '"{22222222-2222-2222-2222-222222222222}"\n' + 'EndProject\n', + encoding="utf-8", + ) + r = extract_sln(sln) + assert "error" not in r + # The virtual solution folder must be keyed off its name, with no trace of the + # absolute scan path. (Real-file nodes — the .sln and .csproj — legitimately + # carry absolute ids here; the CLI's id-relativization pass remaps those, but + # never the virtual folder, which is why the leak had to be fixed at source.) + folder = next(n for n in r["nodes"] if n["label"] == "Plugins") + assert folder["id"] == "plugins" + assert folder["source_file"] == "Plugins" + assert str(tmp_path) not in folder["id"] + + # ── .slnx ──────────────────────────────────────────────────────────────────── def test_slnx_extracts_projects():