diff --git a/graphify/__main__.py b/graphify/__main__.py index a2678655e..ac3157dd6 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -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: @@ -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) diff --git a/graphify/cache.py b/graphify/cache.py index 2052cf7aa..b09a45e30 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -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. @@ -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: @@ -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" diff --git a/graphify/detect.py b/graphify/detect.py index 7fedfccee..875bed8e3 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -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. @@ -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") @@ -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 diff --git a/graphify/watch.py b/graphify/watch.py index ecc17218d..4701ef268 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -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. @@ -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 @@ -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" @@ -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