diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0b84c22..6ce376de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Fix: a PHP `use` import written with a leading-backslash / fully-qualified prefix now resolves to its target definition instead of being dropped (#2661, thanks @ousamabenyounes). - Fix: an unresolved local JS/TS import (to a file absent from the scan) now emits a stable, portable `ref` target id instead of leaking a per-checkout absolute-path slug (#2457, thanks @rohit-jsfreaky). - Fix: `graphify benchmark` no longer crashes on a node whose label is `None` (#2674, thanks @Arthuro0103). +- Fix: `GRAPH_REPORT.md`'s header no longer embeds the generator's absolute host path (#2628, thanks @AirRocker); it labels the report with the project directory basename, so the same graph produces the same bytes on any machine (same class as the 0.9.40 `graph.html` title fix, #2598). ## 0.9.40 (2026-08-11) diff --git a/graphify/report.py b/graphify/report.py index 248bce9a1..69657f0d9 100644 --- a/graphify/report.py +++ b/graphify/report.py @@ -2,9 +2,34 @@ from __future__ import annotations import re from datetime import date +from pathlib import Path import networkx as nx +def _portable_root_label(root: str) -> str: + """Portable label for the report header — the project directory basename. + + GRAPH_REPORT.md is a tracked artifact in practice, so its header must not + bake the generator host's absolute path into the file: the same graph would + otherwise produce different bytes on different machines and leak the build + machine's directory layout into git history (#2628, same class as #2598). + + Taking the basename strips any leading absolute path without touching the + filesystem, and makes `graphify update .`, `graphify update ./proj`, and + `graphify update /abs/path/proj` all label the header `proj`. Only the + degenerate `.`/``/`..` cases need a cwd resolve to recover the real name; + if even that fails, fall back to the raw value. + """ + raw = str(root).replace("\\", "/") + name = Path(raw).name + if name in ("", ".", ".."): + try: + name = Path(raw).resolve().name + except (OSError, RuntimeError): + name = "" + return name or raw + + def _safe_community_name(label: str) -> str: """Mirrors export.safe_name so community hub filenames and report wikilinks always agree.""" cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip() @@ -101,7 +126,7 @@ def generate( inf_avg = round(sum(inf_scores) / len(inf_scores), 2) if inf_scores else None lines = [ - f"# Graph Report - {root} ({today})", + f"# Graph Report - {_portable_root_label(root)} ({today})", "", "## Corpus Check", ] diff --git a/tests/test_report.py b/tests/test_report.py index 767e2ba34..03bc67acb 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -63,6 +63,30 @@ def test_report_shows_raw_cohesion_scores(): assert "⚠" not in report +def test_report_header_does_not_embed_host_absolute_path(): + """#2628 / #2598: the header must not bake the generator host absolute path + into GRAPH_REPORT.md — it labels with the project directory basename so the + same graph produces the same bytes on any machine.""" + G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs() + report = generate(G, communities, cohesion, labels, gods, surprises, detection, + tokens, "/Users/mike/dev/apps/secretproj") + header = report.splitlines()[0] + assert "/Users/mike" not in header + assert "secretproj" in header + + +def test_portable_root_label(): + from graphify.report import _portable_root_label + # Absolute paths collapse to the basename on both POSIX and Windows. + assert _portable_root_label("/Users/mike/dev/apps/proj") == "proj" + assert _portable_root_label(r"C:\Users\mike\dev\proj") == "proj" + # A trailing slash still yields the directory name, not an empty label. + assert _portable_root_label("/Users/mike/dev/proj/") == "proj" + # Relative names pass through unchanged. + assert _portable_root_label("./project") == "project" + assert _portable_root_label("project") == "project" + + # --- work-memory lessons section ---------------------------------------------- def test_report_work_memory_section_present_with_overlay_and_dead_ends():