Repository navigation
Absolute paths still land in node id + source_file on 0.9.15: out-of-root reference targets and degenerate-symbol id fallback (residual of #1789) #1899
Description
Summary
Follow-up to #1789 (fixed in 0.9.13 for .sln/.csproj file node ids, and in 0.9.14 for virtual solution-folder ids — thanks for both). On 0.9.15 two code paths still emit machine-specific absolute paths into graph.json:
- Variant A — out-of-root reference targets. A node created for a file outside the scan root (e.g. the target of a
<ProjectReference Include="..\Core\Core.csproj" />when onlyWebApi/is scanned) gets an absolutesource_fileAND an absolute-path-derivedid. The relativization added for Node IDs for file / directory / project nodes embed the absolute scan path (leaks local username, makes committed graph.json non-portable / non-idempotent) #1789 appears to rely onrelative_to(root), which can't apply to out-of-root paths, so both fields fall back to absolute. - Variant B — degenerate symbol nodes from in-root files. Some symbol nodes get a correctly relativized
source_filebut an absolute-path-derivedid. Two triggers observed: a JSON file using the tsconfig"//"comment-key convention (node label//), and a minified vendor bundle (node label$()frommoment.min.js). Symbol-id derivation seems to fall back to_make_id(str(absolute_path))when no usable symbol/module name is available.
Consequences are the same as #1789: the committed graph.json leaks the local username / checkout path, ids differ per machine and per checkout location, so graphify update from another clone re-creates these nodes (diff churn, non-idempotent versioned graphs).
Environment
- graphify (
graphifyy) 0.9.15 (installed viauv tool), Python 3.12.9 - macOS 26.5 (also expected on Linux/Windows — the fallback logic is OS-agnostic)
- Plain AST build (
_origin: "ast"on all affected nodes) — no LLM/deep mode involved, so this is distinct from deep mode: nodes with out-of-scope source_file are warned about but still written to graph.json #1895.
Minimal repro (4 files, no .NET SDK, no API key)
#!/usr/bin/env bash
set -euo pipefail
R="$(mktemp -d)"
mkdir -p "$R/Core" "$R/WebApi"
cat > "$R/Core/Core.csproj" <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
</Project>
EOF
cat > "$R/WebApi/WebApi.csproj" <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>
EOF
cat > "$R/WebApi/Foo.cs" <<'EOF'
namespace WebApi { public class Foo { public int Bar() => 1; } }
EOF
cat > "$R/WebApi/tsconfig.dev.json" <<'EOF'
{
"//": "dev-only override of the parent tsconfig",
"extends": "../tsconfig.json",
"compilerOptions": { "outDir": "../dist" }
}
EOF
GRAPHIFY_OUT="$R/out" graphify "$R/WebApi" >/dev/null 2>&1
python3 - "$R/out/graph.json" <<'EOF'
import json, sys
g = json.load(open(sys.argv[1]))
for n in g["nodes"]:
sf = str(n.get("source_file") or "")
if sf.startswith("/") or n["id"].startswith(("tmp_","private_","var_","users_","home_")):
print(json.dumps({k: n.get(k) for k in ("label","source_file","id")}, indent=2))
EOFObserved output (verbatim, 0.9.15)
{
"label": "Core.csproj",
"source_file": "/private/var/folders/sv/.../T/tmp.eSYW7WLP1S/Core/Core.csproj",
"id": "private_var_folders_sv_..._t_tmp_esyw7wlp1s_core_core_csproj"
}
{
"label": "//",
"source_file": "tsconfig.dev.json",
"id": "private_var_folders_sv_..._t_tmp_esyw7wlp1s_webapi_tsconfig_dev"
}(Only elided the middles of the long tmp path with ... — everything else is exactly as emitted. Note Core.csproj shows Variant A — both fields absolute — while the // node shows Variant B — relative source_file, absolute id.)
Expected
- Node ids derived from scan-root-relative paths in all code paths.
- For out-of-root targets, something machine-independent: relativize with
..segments against the scan root, key off the reference text as written (..\Core\Core.csproj), or mark the node external and key it by basename — anything that doesn't embed the checkout location inid/source_file.
Real-world impact
Hit this rolling graphify out on a large private monorepo (9 scan scopes, ~300k nodes, graphs committed to git): 8 of 9 scope graphs contained 88 machine-specific strings — every scope with a cross-project .csproj reference (the norm in .NET monorepos) leaked the absolute checkout path via Variant A, and vendored/minified bundles plus tsconfig-style JSON hit Variant B.
Current workaround (in case it helps others): post-build textual normalization — strip the <abs-checkout>/ prefix from source_file values and the derived slug prefix from ids; a plain find/replace also fixes the edge references, and node/edge counts stay intact.
Activity
fix(extract): close residual absolute-path/username leaks (#1899)
safishamsi commentedon Jul 14, 2026
Fixed in b3dc15b (ships in 0.9.16). Out-of-root reference targets get a portable ext_ id + walk-up source_file; degenerate symbols (minified $, JSONC // key) that collapse onto the absolute stem are skipped. Verified zero leaking fields on the repro.
fix(detect,cli): excluded files are pruned from graph and manifest, n…
Squashed commit of the following:
fix(extract): anchor source_file on the scan root, not the --out dir (#…
fix(extract): canonicalize out-of-root import/include edge targets (#…
fix(extract): close residual absolute-path/username leaks (Graphify-L…
fix(detect,cli): excluded files are pruned from graph and manifest, n…
fix(extract): anchor source_file on the scan root, not the --out dir (G…
fix(extract): canonicalize out-of-root import/include edge targets (G…
fix(cpp): guard empty-normalize collapse for punctuation-only test na…
feat(rll): module-level declarations, import kinds, load-time effects