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 <fremat79@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-07-12 14:31:50 +01:00
parent eec7a01838
commit 373bc8efd8
4 changed files with 49 additions and 5 deletions
+4
View File
@@ -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/<name>/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=<path>`; `GRAPHIFY_QUERY_LOG_DISABLE=1` still forces it off. All the query-log env vars are now documented in the README.
+15 -4
View File
@@ -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)
+1 -1
View File
@@ -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" }
+29
View File
@@ -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():