Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3330,7 +3330,7 @@ def _progress(idx: int, total: int, _result: dict) -> None:
f"est. cost: ${cost:.4f}"
)
try:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both")
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target)
except Exception as exc:
print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)
if global_merge:
Expand Down Expand Up @@ -3419,7 +3419,7 @@ def _progress(idx: int, total: int, _result: dict) -> None:
}
analysis_path.write_text(json.dumps(analysis, indent=2), encoding="utf-8")
try:
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both")
_save_manifest(_manifest_files, manifest_path=str(manifest_path), kind="both", root=target)
except Exception as exc:
print(f"[graphify extract] warning: could not write manifest: {exc}", file=sys.stderr)

Expand Down
56 changes: 52 additions & 4 deletions graphify/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ def cache_dir(root: Path = Path("."), kind: str = "ast") -> Path:
return d


def _reanchor_source_file(p: str, root: Path) -> str:
"""If p is a relative path, resolve it against root; otherwise return as-is."""
try:
candidate = Path(p)
if not candidate.is_absolute():
return str((root.resolve() / candidate).resolve())
except (OSError, ValueError):
pass
return p


def load_cached(path: Path, root: Path = Path("."), kind: str = "ast") -> dict | None:
"""Return cached extraction for this file if hash matches, else None.

Expand All @@ -176,18 +187,33 @@ def load_cached(path: Path, root: Path = Path("."), kind: str = "ast") -> dict |
entry = cache_dir(root, kind) / f"{h}.json"
if entry.exists():
try:
return json.loads(entry.read_text(encoding="utf-8"))
result = json.loads(entry.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
# Migration fallback: check legacy flat cache/ dir for AST entries
if kind == "ast":
elif kind == "ast":
legacy = Path(root).resolve() / _GRAPHIFY_OUT / "cache" / f"{h}.json"
if legacy.exists():
try:
return json.loads(legacy.read_text(encoding="utf-8"))
result = json.loads(legacy.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
return None
else:
return None
else:
return None
# Re-anchor relative source_file paths so cached nodes are consistent with
# fresh extractions on this machine (#777)
root_resolved = Path(root).resolve()
for node in result.get("nodes", []):
sf = node.get("source_file")
if sf:
node["source_file"] = _reanchor_source_file(sf, root_resolved)
for edge in result.get("edges", []):
sf = edge.get("source_file")
if sf:
edge["source_file"] = _reanchor_source_file(sf, root_resolved)
return result


def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "ast") -> None:
Expand All @@ -203,6 +229,28 @@ def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "a
p = Path(path)
if not p.is_file():
return

# Relativize source_file fields so cached nodes are portable across machines (#777)
root_resolved = Path(root).resolve()
for node in result.get("nodes", []):
sf = node.get("source_file")
if sf:
try:
rel = Path(sf).relative_to(root_resolved).as_posix()
if not rel.startswith(".."):
node["source_file"] = rel
except ValueError:
pass
for edge in result.get("edges", []):
sf = edge.get("source_file")
if sf:
try:
rel = Path(sf).relative_to(root_resolved).as_posix()
if not rel.startswith(".."):
edge["source_file"] = rel
except ValueError:
pass

h = file_hash(p, root)
target_dir = cache_dir(root, kind)
entry = target_dir / f"{h}.json"
Expand Down
25 changes: 25 additions & 0 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,7 @@ def save_manifest(
manifest_path: str = _MANIFEST_PATH,
*,
kind: str = "both",
root: Path | None = None,
) -> None:
"""Save current file mtimes + content hashes for change detection.

Expand Down Expand Up @@ -1090,6 +1091,20 @@ def _normalise_entry(entry):
# Preserve semantic_hash only when content is unchanged
entry["semantic_hash"] = prev.get("semantic_hash", "") if h == prev.get("ast_hash", "") else ""
manifest[f] = entry
# Relativize keys so the manifest is portable across machines (#777)
if root is not None:
root = Path(root).resolve()
relativized: dict[str, dict] = {}
for key, val in manifest.items():
try:
rel = Path(key).relative_to(root).as_posix()
if rel.startswith(".."):
relativized[key] = val
else:
relativized[rel] = val
except ValueError:
relativized[key] = val
manifest = relativized
Path(manifest_path).parent.mkdir(parents=True, exist_ok=True)
Path(manifest_path).write_text(json.dumps(manifest, indent=2), encoding="utf-8")

Expand Down Expand Up @@ -1129,6 +1144,16 @@ def detect_incremental(
full = detect(root, follow_symlinks=follow_symlinks, google_workspace=google_workspace, extra_excludes=extra_excludes)
manifest = load_manifest(manifest_path)

# Re-anchor relative manifest keys to absolute paths for comparison with detect() output (#777)
root_str = str(root.resolve())
reanchored: dict[str, dict] = {}
for key, val in manifest.items():
if Path(key).is_absolute():
reanchored[key] = val
else:
reanchored[str(Path(root_str) / key)] = val
manifest = reanchored

if not manifest:
# No previous run - treat everything as new
full["incremental"] = True
Expand Down
8 changes: 4 additions & 4 deletions graphify/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ def _rebuild_code(

_relativize_source_files(result, project_root)
out.mkdir(exist_ok=True)
(out / ".graphify_root").write_text(str(watch_root), encoding="utf-8")
(out / ".graphify_root").write_text(str(watch_path), encoding="utf-8")

if no_cluster:
# Normalise to "links" key so schema is consistent with the full clustered path.
Expand Down Expand Up @@ -482,7 +482,7 @@ def _rebuild_code(

try:
from graphify.detect import save_manifest
save_manifest(detected["files"], kind="ast")
save_manifest(detected["files"], kind="ast", root=watch_root)
except Exception:
pass

Expand Down Expand Up @@ -520,7 +520,7 @@ def _rebuild_code(
if same_topology:
try:
from graphify.detect import save_manifest
save_manifest(detected["files"], kind="ast")
save_manifest(detected["files"], kind="ast", root=watch_root)
except Exception:
pass
flag = out / "needs_update"
Expand Down Expand Up @@ -591,7 +591,7 @@ def _rebuild_code(

try:
from graphify.detect import save_manifest
save_manifest(detected["files"], kind="ast")
save_manifest(detected["files"], kind="ast", root=watch_root)
except Exception:
pass

Expand Down
Loading